31

The issue I'm trying to fix is the following: I'm having a TextView and I'm using a Spannable to set some characters bold. The text needs to have a maxim of 2 lines ( android:maxLines="2") and I want the text to be ellipsized, but for some reason I cannot make the text ellipsized.

Here is the simple code:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">

    <TextView android:id="@+id/name"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:gravity="center"
              android:maxLines="2"
              android:ellipsize="end"
              android:bufferType="spannable"
              android:text="@string/app_name"
              android:textSize="15dp"/>

</LinearLayout>

and the activity:

public class MyActivity extends Activity {

    private TextView name;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        name= (TextView) findViewById(R.id.name);


        name.setText("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy ");
        Spannable spannable = (Spannable)name.getText();
        StyleSpan boldSpan = new StyleSpan( Typeface.BOLD );
        spannable.setSpan( boldSpan, 10, 15, Spannable.SPAN_INCLUSIVE_INCLUSIVE );

    }
}

The text is truncated, no "..." are displayed. enter image description here

Paul
  • 3,812
  • 10
  • 50
  • 73

12 Answers12

16

I realise this is a very old post, but seeing as it's still unanswered and I also ran into this issue today I thought I would post a solution to this. Hopefully it helps someone in the future.

ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener()
{
    @Override
    public void onGlobalLayout()
    {
        ViewTreeObserver viewTreeObserver = textView.getViewTreeObserver();
        viewTreeObserver.removeOnGlobalLayoutListener(this);

        if (textView.getLineCount() > 5)
        {
            int endOfLastLine = textView.getLayout().getLineEnd(4);
            String newVal = textView.getText().subSequence(0, endOfLastLine - 3) + "...";
            textView.setText(newVal);
        }
    }
});
Dallas187
  • 484
  • 4
  • 14
11

Having same problem and seems the following works for me:

Spannable wordtoSpan = new SpannableString(lorem); 
wordtoSpan.setSpan(new ForegroundColorSpan(0xffff0000), 0, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
wordtoSpan.setSpan(new ForegroundColorSpan(0xff00ffff), 20, 35, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(wordtoSpan);

in xml the textView has android:multiLine set, and android:ellipsize="end", and android:singleLine="false;

whlk
  • 15,487
  • 13
  • 66
  • 96
lannyf
  • 9,865
  • 12
  • 70
  • 152
7

You're right in that ellipsize, declared either in xml or in code won't work on spannable text.

However, with a little bit of investigation you can actually do the ellipsizing yourself:

private TextView name;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    name= (TextView) findViewById(R.id.name);
    String lorem = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy "
    name.setText(lorem);

    Spannable spannable = (Spannable)name.getText();
    StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
    spannable.setSpan( boldSpan, 10, 15, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
    int maxLines = 2;
    // in my experience, this needs to be called in code, your mileage may vary.
    name.setMaxLines(maxLines);

    // check line count.. this will actually be > than the # of visible lines
    // if it is long enough to be truncated
    if (name.getLineCount() > maxLines){
        // this returns _1 past_ the index of the last character shown 
        // on the indicated line. the lines are zero indexed, so the last 
        // valid line is maxLines -1; 
        int lastCharShown = name.getLayout().getLineVisibleEnd(maxLines - 1); 
        // chop off some characters. this value is arbitrary, i chose 3 just 
        // to be conservative.
        int numCharsToChop = 3;
        String truncatedText = lorem.substring(0, lastCharShown - numCharsToChop);
        // ellipsize! note ellipsis character.
        name.setText(truncatedText+"…");
        // reapply the span, since the text has been changed.
        spannable.setSpan(boldSpan, 10, 15, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
    }

}
lim
  • 261
  • 2
  • 6
6

This may a little trick by using reflection to solve this problem. After reading the source code of AOSP, in TextView.java, DynamicLayout only contains a static field member named sStaticLayout and it's constructed by new StaticLayout(null) without any params including maxLines.

Therefore, doEllipsis will alway be false as mMaximumVisibleLineCount is set Integer.MAX_VALUE by default.

boolean firstLine = (j == 0);
boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd);

    ......

if (ellipsize != null) {
    // If there is only one line, then do any type of ellipsis except when it is MARQUEE
    // if there are multiple lines, just allow END ellipsis on the last line
    boolean forceEllipsis = moreChars && (mLineCount + 1 == mMaximumVisibleLineCount);

    boolean doEllipsis =
                (((mMaximumVisibleLineCount == 1 && moreChars) || (firstLine && !moreChars)) &&
                        ellipsize != TextUtils.TruncateAt.MARQUEE) ||
                (!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
                        ellipsize == TextUtils.TruncateAt.END);
    if (doEllipsis) {
        calculateEllipsis(start, end, widths, widthStart,
                ellipsisWidth, ellipsize, j,
                textWidth, paint, forceEllipsis);
    }
}

So I extends the TextView and make a View named EllipsizeTextView

public class EllipsizeTextView extends TextView {
public EllipsizeTextView(Context context) {
    super(context);
}

public EllipsizeTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public EllipsizeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
}

public EllipsizeTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    StaticLayout layout = null;
    Field field = null;
    try {
        Field staticField = DynamicLayout.class.getDeclaredField("sStaticLayout");
        staticField.setAccessible(true);
        layout = (StaticLayout) staticField.get(DynamicLayout.class);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

    if (layout != null) {
        try {
            field = StaticLayout.class.getDeclaredField("mMaximumVisibleLineCount");
            field.setAccessible(true);
            field.setInt(layout, getMaxLines());
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (layout != null && field != null) {
        try {
            field.setInt(layout, Integer.MAX_VALUE);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

}

problem solved!

Sheldon Xia
  • 136
  • 1
  • 1
  • I love this answer. Unlike the other answers, this will retain the text in the TextView and not cut it. This is an advantage if you want to expand the TextView later. – Rene B. May 09 '19 at 07:10
  • 2
    @Sheldon Xia: It resolved my problem half. If you have multi paragraph Text, then after placing the dots, it also includes the first alphabet of next paragraph. Something like this: hey how are y...w. Can you please help – Sudhanshu May 22 '19 at 06:59
  • @Sudhanshu I'm getting this bug too, and if I have a multi-paragraph text AND text alignment set to center the last line will break even more – Alex Sim Sep 26 '19 at 20:59
2

Another solution is to overwrite onDraw of TextView. The following suggested solution, doesn't make use any reflection technique. Hence, shouldn't break in the future, in case any member variable naming changes.

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

public class EllipsizeTextView extends TextView {
    private static final String THREE_DOTS = "...";
    private static final int THREE_DOTS_LENGTH = THREE_DOTS.length();

    private volatile boolean enableEllipsizeWorkaround = false;
    private SpannableStringBuilder spannableStringBuilder;

    public EllipsizeTextView(Context context) {
        super(context);
    }

    public EllipsizeTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public EllipsizeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public EllipsizeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    public void setEnableEllipsizeWorkaround(boolean enableEllipsizeWorkaround) {
        this.enableEllipsizeWorkaround = enableEllipsizeWorkaround;
    }

    // https://stackoverflow.com/questions/14691511/textview-using-spannable-ellipsize-doesnt-work
    // https://blog.csdn.net/htyxz8802/article/details/50387950
    @Override
    protected void onDraw(Canvas canvas) {
        if (enableEllipsizeWorkaround && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            final Layout layout = getLayout();

            if (layout.getLineCount() >= getMaxLines()) {
                CharSequence charSequence = getText();
                int lastCharDown = layout.getLineVisibleEnd(getMaxLines()-1);

                if (lastCharDown >= THREE_DOTS_LENGTH && charSequence.length() > lastCharDown) {
                    if (spannableStringBuilder == null) {
                        spannableStringBuilder = new SpannableStringBuilder();
                    } else {
                        spannableStringBuilder.clear();
                    }

                    spannableStringBuilder.append(charSequence.subSequence(0, lastCharDown - THREE_DOTS_LENGTH)).append(THREE_DOTS);
                    setText(spannableStringBuilder);
                }
            }
        }

        super.onDraw(canvas);
    }
}
Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • I think it's not a good idea to have such computations (actually any computations) in onDraw method, better move it to overridden setText – gordinmitya Jun 03 '21 at 13:23
1

This is a known issue in the Android framework: https://code.google.com/p/android/issues/detail?id=67186

Singed
  • 1,053
  • 3
  • 11
  • 28
1

A simple and working solution

This is my code ->

<TextView
        android:id="@+id/textViewProfileContent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:singleLine="false"
        android:ellipsize="end"
        android:maxLines="3"
        android:textSize="14sp"
        android:textColor="#000000" />

 SpannableStringBuilder sb = new SpannableStringBuilder();
 SpannableString attrAdditional = new SpannableString(additionalText);
                 attrAdditional.SetSpan(new StyleSpan(TypefaceStyle.Bold), 0, additionalText.Length, 0);...

 sb.Append(attrAdditional);...

 ProfileContent.SetText(sb, **TextView.BufferType.Normal**);

Result

biggeek
  • 11
  • 2
  • This works in _most_ situations. In my case, I'm using ImageSpans to replace system emoji with custom emoji. If the ending part of the text is actual text, the ellipsize will work as expected. However, if the ending part of the text is an ImageSpan, it will not (sometimes the span will overflow, other times there just won't be an ellipsis). – Greyson Parrelli Apr 20 '18 at 17:46
1

As for single line case, android:maxLines is not working,but android:singleLine is ok.

John
  • 99
  • 1
  • 4
1

Similar to @Dallas187 but doesn't break link formatting

fun TextView.addEllipsizeToSpannedOnLayout() {
    doOnNextLayout {
        if (maxLines != -1 && lineCount > maxLines) {
            val endOfLastLine = layout.getLineEnd(maxLines - 1)
            val spannedDropLast3Chars = text.subSequence(0, endOfLastLine - 3) as? Spanned
            if (spannedDropLast3Chars != null) {
                val spannableBuilder = SpannableStringBuilder()
                    .append(spannedDropLast3Chars)
                    .append("…")

                text = spannableBuilder
            }
        }
    }
}
hmac
  • 267
  • 3
  • 9
  • what is doOnNextLayout in this extension function? Can you give an example to use this function? – Adarsh Sahu Feb 02 '23 at 07:03
  • works fine for strings, which are slightly bigger than `maxLines`. But the longer the string, the earlier "..." cuts the line and the bigger the gap between "..." and the real end of text area. E.g. if you have `maxLines=2` and your text has around 10 or more lines, the ellipsized line will be 30% shorter than the normal one – Alexander Tumanin Feb 06 '23 at 17:06
0
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (maxLength < 0) {
        Layout layout = getLayout();
        if (layout.getLineCount() > getMaxLines()) {
            maxLength = layout.getLineVisibleEnd(getMaxLines() - 1) - 1;
            setSpannableString();
        }
    }
}

The "setSpannableString()" shows below:

private void setSpannableString() {
    // ShowString is real visible string. FullString is original string which
    // is useful when caching and calculating.
    String showString = fullString;
    if (maxLength > 0) {
        showString = fullString.substring(0, maxLength) + THREE_DOTS;
    }
    SpannableStringBuilder builder = new SpannableStringBuilder(showString);
    for (int i = 0; i < mHighLightColor.size(); i++) {
        String highLightString = mHighLightString.get(i);
        int color = mHighLightColor.get(i);
        int start = fullString.indexOf(highLightString);
        int end = Math.min(start + highLightString.length(), showString.length());
        if (mClickableSpanList.get(i) != null) {
            builder.setSpan(mClickableSpanList.get(i), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            setMovementMethod(LinkMovementMethod.getInstance());
        }
        builder.setSpan(new ColorBoldSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
    setText(builder, BufferType.SPANNABLE);
}
0

Use Kotlin's CharSequence instead of using SpannableString directly.

for eg:

finalString = "your very long string"
var spannable :CharSequence? = ""
spannable = SpannableStringBuilder(finalString).apply{
setSpan(.....)
setSpan(.....)
setSpan(.....)
}

textView.text = spannable

Kotlin's CharSequence works perfectly with ellipsize for more than one line.

Narendra_Nath
  • 4,578
  • 3
  • 13
  • 31
-5

Try This Its Work for me.

This is my text view

 <TextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:maxLines="2"
         android:ellipsize="end"
         android:text="Type here maximum characters."
         android:textSize="16sp" />

enter image description here

Rahul Patil
  • 129
  • 1
  • 5