Search code examples
androidhtmlstringbuilderspannablestringleadingmarginspan2

Text coming from a raw file flowing around an image


this is my first question here. I'm trying to develop a "book" with text and images and I'm having problems to show a text (loaded from a raw file) flowing around and image in a textview. I have tried this and it works fine while the text is defined in a string resource. However, when the text comes from an external file (eg a .txt file which includes break lines) the TextView looks something like this:

---------  text text text text text 
|       |  text text text text text 
---------  text text text text text 
text text text text 
text text text text
text text text text
text text text text

That is to say, just after the image, each line leaves an empty space to the right which has the same size than the image.

I don't know why this happens, Am I missing something? this is the code:

ImageView page_im_iv = (ImageView) findViewById(R.id.page_image);
TextView page_text = (TextView) findViewById(R.id.page_text);

Drawable page_image getResources().getDrawable(R.drawable.anyname);
page_im_iv.setBackground(page_image);

float left_margin = page_image.getIntrinsicWidth() + 10;
float top_margin = page_image.getIntrinsicHeight() + 10;

float flines = top_margin/page_text.getTextSize();
int ilines = (int) flines;

StringBuilder raw_text = readRaw(this,res_id);//res_id changes dynamically, it is just the name of the .txt file

SpannableString ss = new SpannableString(raw_text.toString());
ss.setSpan(new MyLeadingMarginSpan2(ilines, left_margin), 0, ss.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

page_text.setText(ss);

And this is the layout:

<ScrollView  xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="example.ActivityBookPage"
xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

        <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">


          <ImageView
          android:id="@+id/page_image"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"/>


          <TextView
          android:id="@+id/page_text"
          android:layout_width="wrap_content"
          android:layout_height="match_parent"
          android:layout_marginBottom="10dp"/>


      </RelativeLayout>

    <!-- Other stuff 
    ...
    -->
     </LinearLayout>
</ScrollView >

Thats all.

In case the problem comes from the readRaw method, this is the code:

public static StringBuilder readRaw(Context ctx,int res_id) {

        StringBuilder text = new StringBuilder();
        InputStream is = ctx.getResources().openRawResource(res_id);
        InputStreamReader isr = new InputStreamReader(is);
        BufferedReader br = new BufferedReader(isr, 8192);

        try {
            String line;
            while ((line = br.readLine()) != null)  {
                text.append(line);
                text.append("\n");

            }
            isr.close();
            is.close();
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return text;
    }

And this is the code for MyLeadingMarginSpan2 class, copy-pasted from the previous link

public class MyLeadingMarginSpan2 implements LeadingMarginSpan2 {
    private int margin;
    private int lines;

    public MyLeadingMarginSpan2(int lines, int margin) {
        this.margin = margin;
        this.lines = lines;
    }

    @Override
    public int getLeadingMargin(boolean first) {
        return first ? margin : 0;
    }

    @Override
    public int getLeadingMarginLineCount() {
        return lines;
    }

    @Override
    public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, 
            int top, int baseline, int bottom, CharSequence text, 
            int start, int end, boolean first, Layout layout) {}
}

Solution

  • Finally I found a solution (thanks to this old reply). The problem was that the text coming from the raw file may include break lines (\n), which are not taken into account when calculating "ilines". Hence, we need to count the number of characters that fit just at the right of the image, then include a new break line and then the rest of the text. This is the code

    int charCount = page_text_layout.getLineEnd(Math.min(ilines - 1, page_text_layout.getLineCount() - 1));//see below what page_text_layout is
    //in case the image is big enough to have all 
    //the text at its right, just use ss.length as 
    //the third parameter for setSpan   
    if (charCount >= ss.length() || charCount <= 0 ) {
                ss.setSpan(new MyLeadingMarginSpan2(ilines, left_margin), 0, ss.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                                page_text.setText(ss);                
    }  
    else { //in case the text is longer, make three blocks
                Spannable s1 = new SpannableStringBuilder(ss, 0, charCount);
                s1.setSpan(new MyLeadingMarginSpan2(ilines, left_margin), 0, charCount, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                Spannable s2 = new SpannableStringBuilder(System.getProperty("line.separator"));
                Spannable s3 = new SpannableStringBuilder(ss, charCount, ss.length());
                page_text.setText(TextUtils.concat(s1, s2, s3));
    }
    

    where page_text_layout is defined previously inside an onGlobalLayout callback (in my case inside the onCreate() method):

    ViewTreeObserver vto = page_text.getViewTreeObserver();
            vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                   page_text_layout = page_text.getLayout();  
                }
            });
    

    Of course this code can be refined (eg. checking whether s1 breaks the text in the middle of a word) but this would be the main structure.

    Hope this helps someone!