77

Can you please tell me if there is a way to layout text around an image? Like this:

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

I have gotten a response from an android developer about this question. But I am not sure what he means by doing my own version of TextView? Thank for any tips.

On Mon, Feb 8, 2010 at 11:05 PM, Romain Guy wrote:

Hi,

This is not possible using only the supplied widgets and layouts. You could write your own version of TextView to do this, it shouldn't be hard.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
silverburgh
  • 8,659
  • 10
  • 31
  • 24
  • silverburgh: did you find a solution for this which you could share? – znq Mar 05 '10 at 11:05
  • 4
    http://stackoverflow.com/questions/13526949/how-to-fill-the-empty-spaces-with-content-below-the-image-in-android/13527178#13527178 is the solution probably – Victor Nov 28 '12 at 05:02
  • This is so easy to do on the web. I'm gonna skip this feature for now. – danny117 Sep 29 '14 at 03:40
  • You can use [ImageSpan](https://developer.android.com/reference/android/text/style/ImageSpan.html). Have a look at [this link](http://stackoverflow.com/a/3177667/5373110) – Meet Vora Dec 13 '16 at 13:35

9 Answers9

69

Now it is possible, but only for phones with version higher or equal 2.2 by using the android.text.style.LeadingMarginSpan.LeadingMarginSpan2 interface which is available in API 8.

Here is the article, not in English though, but you can download the source code of the example directly from here.

If you want to make your application compatible with older devices, you can display a different layout without a floating text. Here is an example:

Layout (default for older versions, will be changed programmatically for newer versions)

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">
    <ImageView 
            android:id="@+id/thumbnail_view"
            android:src="@drawable/icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    <TextView android:id="@+id/message_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/thumbnail_view"
            android:textSize="18sp"
            android:text="@string/text" />
</RelativeLayout>

The helper class

class FlowTextHelper {

    private static boolean mNewClassAvailable;

    static {
        if (Integer.parseInt(Build.VERSION.SDK) >= 8) { // Froyo 2.2, API level 8
           mNewClassAvailable = true;
        }
    }

    public static void tryFlowText(String text, View thumbnailView, TextView messageView, Display display){
        // There is nothing I can do for older versions, so just return
        if(!mNewClassAvailable) return;

        // Get height and width of the image and height of the text line
        thumbnailView.measure(display.getWidth(), display.getHeight());
        int height = thumbnailView.getMeasuredHeight();
        int width = thumbnailView.getMeasuredWidth();
        float textLineHeight = messageView.getPaint().getTextSize();

        // Set the span according to the number of lines and width of the image
        int lines = (int)FloatMath.ceil(height / textLineHeight);
        //For an html text you can use this line: SpannableStringBuilder ss = (SpannableStringBuilder)Html.fromHtml(text);
        SpannableString ss = new SpannableString(text);
        ss.setSpan(new MyLeadingMarginSpan2(lines, width), 0, ss.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        messageView.setText(ss);

        // Align the text with the image by removing the rule that the text is to the right of the image
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)messageView.getLayoutParams();
        int[]rules = params.getRules();
        rules[RelativeLayout.RIGHT_OF] = 0;
    }
}

The MyLeadingMarginSpan2 class (updated to support API 21)

public class MyLeadingMarginSpan2 implements LeadingMarginSpan2 {
    private int margin;
    private int lines;
    private boolean wasDrawCalled = false;
    private int drawLineCount = 0;

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

    @Override
    public int getLeadingMargin(boolean first) {
        boolean isFirstMargin = first;
        // a different algorithm for api 21+
        if (Build.VERSION.SDK_INT >= 21) {
            this.drawLineCount = this.wasDrawCalled ? this.drawLineCount + 1 : 0;
            this.wasDrawCalled = false;
            isFirstMargin = this.drawLineCount <= this.lines;
        }

        return isFirstMargin ? this.margin : 0;
    }

    @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) {
        this.wasDrawCalled = true;
    }

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

Example of the usage

ImageView thumbnailView = (ImageView) findViewById(R.id.thumbnail_view);
TextView messageView = (TextView) findViewById(R.id.message_view);
String text = getString(R.string.text);

Display display = getWindowManager().getDefaultDisplay();
FlowTextHelper.tryFlowText(text, thumbnailView, messageView, display);

This is how the application looks on the Android 2.2 device: Android 2.2 the text flows around the image

And this is for the Android 2.1 device:

Android 2.1 the text is situated near the image

Parker
  • 8,539
  • 10
  • 69
  • 98
vortexwolf
  • 13,967
  • 2
  • 54
  • 72
  • 2
    Instead of the Class.forName trick, you could use a simple condition: if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {... – Wojciech Górski Aug 30 '12 at 15:29
  • Am also using this.But When data with Html tags its not supported for Html.fromHtml(html content) please help me i need to show list with adapter of wrapText like above – Harsha Nov 27 '14 at 11:13
  • @Harsha The method `Html.fromHtml` will not work with any html, it supports only simple html with few tags. – vortexwolf Dec 14 '14 at 07:39
  • Great Work, I have wasted my day almost... Thanks a ton!! – Palak Darji Mar 12 '15 at 10:13
  • 1
    This also adds a right margin though, for the lines past the image (so the text never goes all the way accross). Any idea how to fix this bug? – Parker Apr 01 '16 at 02:43
  • I have the same problem, after the image the text dont fit – extmkv Aug 08 '16 at 16:33
  • @vorrtex my image view (or another view) is below of text instead of top of it. 'LeadingMarginSpan2' set margin for top lines. in my case I must be set margin for last lines. Do you have any idea? and this is my question: http://stackoverflow.com/questions/39222319/how-to-implement-leadingmarginspan2-for-last-lines-of-text-instead-of-top-line – Hamidreza Shokouhi Aug 30 '16 at 09:31
  • How can I add flow text if I have another image at the bottom left? @WojciechGórski – Kaustubh Mar 02 '18 at 08:30
9

Nowadays you can use library: https://github.com/deano2390/FlowTextView . Like this:

<uk.co.deanwild.flowtextview.FlowTextView
    android:id="@+id/ftv"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:padding="10dip"
            android:src="@drawable/android"/>
</uk.co.deanwild.flowtextview.FlowTextView>
Nikita Shaposhnik
  • 997
  • 10
  • 13
7

Here's an improvement for the FlowTextHelper (from vorrtex's reply). I added the possibility to add extra padding between the text and the image and improved the line calculation to also take padding into account. Enjoy!

public class FlowTextHelper {
   private static boolean mNewClassAvailable;

   /* class initialization fails when this throws an exception */
   static {
       try {
           Class.forName("android.text.style.LeadingMarginSpan$LeadingMarginSpan2");
           mNewClassAvailable = true;
       } catch (Exception ex) {
           mNewClassAvailable = false;
       }
   }

   public static void tryFlowText(String text, View thumbnailView, TextView messageView, Display display, int addPadding){
       // There is nothing I can do for older versions, so just return
       if(!mNewClassAvailable) return;



       // Get height and width of the image and height of the text line
        thumbnailView.measure(display.getWidth(), display.getHeight());
        int height = thumbnailView.getMeasuredHeight();
        int width = thumbnailView.getMeasuredWidth() + addPadding;
        messageView.measure(width, height); //to allow getTotalPaddingTop
        int padding = messageView.getTotalPaddingTop();
        float textLineHeight = messageView.getPaint().getTextSize();

        // Set the span according to the number of lines and width of the image
        int lines =  (int)Math.round((height - padding) / textLineHeight);
        SpannableString ss = new SpannableString(text);
        //For an html text you can use this line: SpannableStringBuilder ss = (SpannableStringBuilder)Html.fromHtml(text);
        ss.setSpan(new MyLeadingMarginSpan2(lines, width), 0, ss.length(), 0);
        messageView.setText(ss);

        // Align the text with the image by removing the rule that the text is to the right of the image
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)messageView.getLayoutParams();
        int[]rules = params.getRules();
        rules[RelativeLayout.RIGHT_OF] = 0;
   }


}
Ronen Yacobi
  • 844
  • 9
  • 16
  • Hi Ronen. Im having huge trouble understanding the whole idea of 'text wrap around image' issue. Could you please tell me where can I get some information on how to write such code? I'd like to learn how to write the code on my own rather than just copy it. – Ramona Aug 09 '17 at 14:31
  • Hi @Ramona Maybe take a look at this library: https://github.com/deano2390/FlowTextView – Ronen Yacobi Aug 29 '17 at 08:31
  • Hello, thanks a lot for info. Do you know how to achieve this with the image placed on the right side of the screen? – Ramona Oct 14 '17 at 09:18
  • @Ramona looks like I am also looking for the same solution. In our case, we have an image on the right side of the screen. Did you come across any solution or any leads that will be helpful? – Chethan Shetty Oct 06 '21 at 05:41
4

Vorrtex and Ronen's answers are working for me except for one detail - After wrapping text around the image there was a weird "negative" margin below the image and on the opposite side. I figured out that when setting the span on the SpannableString I changed

ss.setSpan(new MyLeadingMarginSpan2(lines, width), 0, ss.length(), 0);

to

ss.setSpan(new MyLeadingMarginSpan2(lines, width), 0, lines, 0);

which stopped the span after the image. Might not be necessary in all cases but thought I would share.

johosher
  • 329
  • 5
  • 11
3

This question seems to same as my question How to fill the empty spaces with content below the Image in android

I found the solution using flowtext library please find the first answer it might help you so far

Community
  • 1
  • 1
Victor
  • 893
  • 3
  • 11
  • 21
2

vorrtex's answer didn't work for me but I took a lot from it and came up with my own solution. Here it is:

package ie.moses.keepitlocal.util;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.IntRange;
import android.text.Layout;
import android.text.style.LeadingMarginSpan;
import android.view.View;
import android.widget.TextView;

import ie.moses.keepitlocal.util.MeasurementUtils;
import ie.moses.keepitlocal.util.TextUtils;

import static com.google.common.base.Preconditions.checkArgument;

public class WrapViewSpan implements LeadingMarginSpan.LeadingMarginSpan2 {

    private final Context _context;
    private final int _lineCount;
    private int _leadingMargin;
    private int _padding;

    public WrapViewSpan(View wrapeeView, TextView wrappingView) {
        this(wrapeeView, wrappingView, 0);
    }

    /**
     * @param padding Padding in DIP.
     */
    public WrapViewSpan(View wrapeeView, TextView wrappingView, @IntRange(from = 0) int padding) {
        _context = wrapeeView.getContext();
        setPadding(padding);

        int wrapeeHeight = wrapeeView.getHeight();
        float lineHeight = TextUtils.getLineHeight(wrappingView);

        int lineCnt = 0;
        float linesHeight = 0F;
        while ((linesHeight += lineHeight) <= wrapeeHeight) {
            lineCnt++;
        }

        _lineCount = lineCnt;
        _leadingMargin = wrapeeView.getWidth();
    }

    public void setPadding(@IntRange(from = 0) int paddingDp) {
        checkArgument(paddingDp >= 0, "padding cannot be negative");
        _padding = (int) MeasurementUtils.dpiToPixels(_context, paddingDp);
    }

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

    @Override
    public int getLeadingMargin(boolean first) {
        if (first) {
            return _leadingMargin + _padding;
        } else {
            return _padding;
        }
    }

    @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) {

    }

}

and in my actual class where the span is used:

ViewTreeObserver headerViewTreeObserver = _headerView.getViewTreeObserver();
headerViewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        String descriptionText = _descriptionView.getText().toString();
        SpannableString spannableDescriptionText = new SpannableString(descriptionText);
        LeadingMarginSpan wrapHeaderSpan = new WrapViewSpan(_headerView, _descriptionView, 12);
        spannableDescriptionText.setSpan(
                wrapHeaderSpan,
                0,
                spannableDescriptionText.length(),
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
        );
        _descriptionView.setText(spannableDescriptionText);
        ViewTreeObserver headerViewTreeObserver = _headerView.getViewTreeObserver();
        headerViewTreeObserver.removeOnGlobalLayoutListener(this);
    }
});

I needed the global layout listener in order to get the right values for getWidth() and getHeight().

Here is the result:

enter image description here

Adam
  • 2,167
  • 5
  • 19
  • 33
1

"But I am not sure what he means by doing my own version of TextView?"

He means that you can extend the class android.widget.TextView (or Canvas or some other renderable surface) and implement your own overriding version that allows embedded images with text flowing around them.

This could be quite a lot of work depending on how general you make it.

Peter vdL
  • 4,953
  • 10
  • 40
  • 60
0

I can offer more comfortable constructor for The MyLeadingMarginSpan2 class

    MyLeadingMarginSpan2(Context cc,int textSize,int height,int width) {                
    int pixelsInLine=(int) (textSize*cc.getResources().getDisplayMetrics().scaledDensity);              
    if (pixelsInLine>0 && height>0) {
        this.lines=height/pixelsInLine;          
    } else  {
        this.lines=0;
    }
    this.margin=width; 
}
  • Hello Evgeny,how to set `text flow around image` for image placed on the right side of the screen? Answer highly appreciated. – Ramona Aug 08 '17 at 13:30
-1

try this simple implementation using kotlin and androidx. first, create leading span helper class:

class LeadingSpan(private val line: Int, private val margin: Int) : LeadingMarginSpan.LeadingMarginSpan2 {

    override fun drawLeadingMargin(canvas: Canvas?, paint: Paint?, x: Int, dir: Int, top: Int, baseline: Int, bottom: Int, text: CharSequence?, start: Int, end: Int, first: Boolean, layout: Layout?) {}

    override fun getLeadingMargin(first: Boolean): Int =  if (first) margin else 0

    override fun getLeadingMarginLineCount(): Int = line
}

Then create a layout using RelativeLayout :

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

    <TextView
        android:id="@+id/about_desc"
        android:text="@string/about_desc"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <androidx.appcompat.widget.AppCompatImageView
        android:id="@+id/logo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</RelativeLayout>

and finally setting up in your activity or fragment like:

val about = view.findViewById<TextView>(R.id.about_desc)
val logoImage = ContextCompat.getDrawable(view.context, R.mipmap.ic_launcher) as Drawable
@Suppress("DEPRECATION")
view.findViewById<AppCompatImageView>(R.id.logo).setBackgroundDrawable(logoImage)
val spannableString = SpannableString(about.text)
spannableString.setSpan(Helpers.LeadingSpan(5, logoImage.intrinsicWidth + 10), 0, spannableString.length, 0)
about.text = spannableString

change number 5 in the Helpers.LeadingSpan(5, logoImage.intrinsicWidth + 10) according to your drawable height.

Gilbert Arafat
  • 441
  • 5
  • 10