android ellipsize multiline textview
Solution 1
Here is a solution to the problem. It is a subclass of TextView that actually works for ellipsizing. The android-textview-multiline-ellipse code listed in an earlier answer I have found to be buggy in certain circumstances, as well as being under GPL, which doesn't really work for most of us. Feel free to use this code freely and without attribution, or under the Apache license if you would prefer. Note that there is a listener to notify you when the text becomes ellipsized, which I found quite useful myself.
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Canvas;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.StaticLayout;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.widget.TextView;
public class EllipsizingTextView extends TextView {
private static final String ELLIPSIS = "...";
public interface EllipsizeListener {
void ellipsizeStateChanged(boolean ellipsized);
}
private final List<EllipsizeListener> ellipsizeListeners = new ArrayList<EllipsizeListener>();
private boolean isEllipsized;
private boolean isStale;
private boolean programmaticChange;
private String fullText;
private int maxLines = -1;
private float lineSpacingMultiplier = 1.0f;
private float lineAdditionalVerticalPadding = 0.0f;
public EllipsizingTextView(Context context) {
super(context);
}
public EllipsizingTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void addEllipsizeListener(EllipsizeListener listener) {
if (listener == null) {
throw new NullPointerException();
}
ellipsizeListeners.add(listener);
}
public void removeEllipsizeListener(EllipsizeListener listener) {
ellipsizeListeners.remove(listener);
}
public boolean isEllipsized() {
return isEllipsized;
}
@Override
public void setMaxLines(int maxLines) {
super.setMaxLines(maxLines);
this.maxLines = maxLines;
isStale = true;
}
public int getMaxLines() {
return maxLines;
}
@Override
public void setLineSpacing(float add, float mult) {
this.lineAdditionalVerticalPadding = add;
this.lineSpacingMultiplier = mult;
super.setLineSpacing(add, mult);
}
@Override
protected void onTextChanged(CharSequence text, int start, int before, int after) {
super.onTextChanged(text, start, before, after);
if (!programmaticChange) {
fullText = text.toString();
isStale = true;
}
}
@Override
protected void onDraw(Canvas canvas) {
if (isStale) {
super.setEllipsize(null);
resetText();
}
super.onDraw(canvas);
}
private void resetText() {
int maxLines = getMaxLines();
String workingText = fullText;
boolean ellipsized = false;
if (maxLines != -1) {
Layout layout = createWorkingLayout(workingText);
if (layout.getLineCount() > maxLines) {
workingText = fullText.substring(0, layout.getLineEnd(maxLines - 1)).trim();
while (createWorkingLayout(workingText + ELLIPSIS).getLineCount() > maxLines) {
int lastSpace = workingText.lastIndexOf(' ');
if (lastSpace == -1) {
break;
}
workingText = workingText.substring(0, lastSpace);
}
workingText = workingText + ELLIPSIS;
ellipsized = true;
}
}
if (!workingText.equals(getText())) {
programmaticChange = true;
try {
setText(workingText);
} finally {
programmaticChange = false;
}
}
isStale = false;
if (ellipsized != isEllipsized) {
isEllipsized = ellipsized;
for (EllipsizeListener listener : ellipsizeListeners) {
listener.ellipsizeStateChanged(ellipsized);
}
}
}
private Layout createWorkingLayout(String workingText) {
return new StaticLayout(workingText, getPaint(), getWidth() - getPaddingLeft() - getPaddingRight(),
Alignment.ALIGN_NORMAL, lineSpacingMultiplier, lineAdditionalVerticalPadding, false);
}
@Override
public void setEllipsize(TruncateAt where) {
// Ellipsize settings are not respected
}
}
Solution 2
In my app, I had similar problem: 2 line of string and, eventually, add "..." if the string was too long. I used this code in xml file into textview tag:
android:maxLines="2"
android:ellipsize="end"
android:singleLine="false"
Solution 3
Try this, it works for me, I have 4 lines and it adds the "..." to the end of the last/fourth line. Its the same as morale's answer but i have singeLine="false" in there.
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:maxLines="4"
android:ellipsize="marquee"
android:singleLine="false"
android:text="Hi make this a very long string that wraps at least 4 lines, seriously make it really really long so it gets cut off at the fourth line not joke. Just do it!" />
Solution 4
I've run into this problem, too. There's a rather old bug about it that remains unanswered: Bug 2254
Solution 5
I combined the solutions by Micah Hainline, Alex Băluț, and Paul Imhoff to create an ellipsizing multiline TextView
that also supports Spanned
text.
You only need to set android:ellipsize
and android:maxLines
.
/*
* Copyright (C) 2011 Micah Hainline
* Copyright (C) 2012 Triposo
* Copyright (C) 2013 Paul Imhoff
* Copyright (C) 2014 Shahin Yousefi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.support.annotation.NonNull;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class EllipsizingTextView extends TextView {
private static final CharSequence ELLIPSIS = "\u2026";
private static final Pattern DEFAULT_END_PUNCTUATION
= Pattern.compile("[\\.!?,;:\u2026]*$", Pattern.DOTALL);
private final List<EllipsizeListener> mEllipsizeListeners = new ArrayList<>();
private EllipsizeStrategy mEllipsizeStrategy;
private boolean isEllipsized;
private boolean isStale;
private boolean programmaticChange;
private CharSequence mFullText;
private int mMaxLines;
private float mLineSpacingMult = 1.0f;
private float mLineAddVertPad = 0.0f;
private Pattern mEndPunctPattern;
public EllipsizingTextView(Context context) {
this(context, null);
}
public EllipsizingTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray a = context.obtainStyledAttributes(attrs,
new int[]{ android.R.attr.maxLines }, defStyle, 0);
setMaxLines(a.getInt(0, Integer.MAX_VALUE));
a.recycle();
setEndPunctuationPattern(DEFAULT_END_PUNCTUATION);
}
public void setEndPunctuationPattern(Pattern pattern) {
mEndPunctPattern = pattern;
}
public void addEllipsizeListener(@NonNull EllipsizeListener listener) {
mEllipsizeListeners.add(listener);
}
public void removeEllipsizeListener(EllipsizeListener listener) {
mEllipsizeListeners.remove(listener);
}
public boolean isEllipsized() {
return isEllipsized;
}
@SuppressLint("Override")
public int getMaxLines() {
return mMaxLines;
}
@Override
public void setMaxLines(int maxLines) {
super.setMaxLines(maxLines);
mMaxLines = maxLines;
isStale = true;
}
public boolean ellipsizingLastFullyVisibleLine() {
return mMaxLines == Integer.MAX_VALUE;
}
@Override
public void setLineSpacing(float add, float mult) {
mLineAddVertPad = add;
mLineSpacingMult = mult;
super.setLineSpacing(add, mult);
}
@Override
public void setText(CharSequence text, BufferType type) {
if (!programmaticChange) {
mFullText = text;
isStale = true;
}
super.setText(text, type);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (ellipsizingLastFullyVisibleLine()) isStale = true;
}
@Override
public void setPadding(int left, int top, int right, int bottom) {
super.setPadding(left, top, right, bottom);
if (ellipsizingLastFullyVisibleLine()) isStale = true;
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
if (isStale) resetText();
super.onDraw(canvas);
}
private void resetText() {
int maxLines = getMaxLines();
CharSequence workingText = mFullText;
boolean ellipsized = false;
if (maxLines != -1) {
if (mEllipsizeStrategy == null) setEllipsize(null);
workingText = mEllipsizeStrategy.processText(mFullText);
ellipsized = !mEllipsizeStrategy.isInLayout(mFullText);
}
if (!workingText.equals(getText())) {
programmaticChange = true;
try {
setText(workingText);
} finally {
programmaticChange = false;
}
}
isStale = false;
if (ellipsized != isEllipsized) {
isEllipsized = ellipsized;
for (EllipsizeListener listener : mEllipsizeListeners) {
listener.ellipsizeStateChanged(ellipsized);
}
}
}
@Override
public void setEllipsize(TruncateAt where) {
if (where == null) {
mEllipsizeStrategy = new EllipsizeNoneStrategy();
return;
}
switch (where) {
case END:
mEllipsizeStrategy = new EllipsizeEndStrategy();
break;
case START:
mEllipsizeStrategy = new EllipsizeStartStrategy();
break;
case MIDDLE:
mEllipsizeStrategy = new EllipsizeMiddleStrategy();
break;
case MARQUEE:
super.setEllipsize(where);
isStale = false;
default:
mEllipsizeStrategy = new EllipsizeNoneStrategy();
break;
}
}
public interface EllipsizeListener {
void ellipsizeStateChanged(boolean ellipsized);
}
private abstract class EllipsizeStrategy {
public CharSequence processText(CharSequence text) {
return !isInLayout(text) ? createEllipsizedText(text) : text;
}
public boolean isInLayout(CharSequence text) {
Layout layout = createWorkingLayout(text);
return layout.getLineCount() <= getLinesCount();
}
protected Layout createWorkingLayout(CharSequence workingText) {
return new StaticLayout(workingText, getPaint(),
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
Alignment.ALIGN_NORMAL, mLineSpacingMult,
mLineAddVertPad, false /* includepad */);
}
protected int getLinesCount() {
if (ellipsizingLastFullyVisibleLine()) {
int fullyVisibleLinesCount = getFullyVisibleLinesCount();
return fullyVisibleLinesCount == -1 ? 1 : fullyVisibleLinesCount;
} else {
return mMaxLines;
}
}
protected int getFullyVisibleLinesCount() {
Layout layout = createWorkingLayout("");
int height = getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
int lineHeight = layout.getLineBottom(0);
return height / lineHeight;
}
protected abstract CharSequence createEllipsizedText(CharSequence fullText);
}
private class EllipsizeNoneStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
return fullText;
}
}
private class EllipsizeEndStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
Layout layout = createWorkingLayout(fullText);
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
int textLength = fullText.length();
int cutOffLength = textLength - cutOffIndex;
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
String workingText = TextUtils.substring(fullText, 0, textLength - cutOffLength).trim();
String strippedText = stripEndPunctuation(workingText);
while (!isInLayout(strippedText + ELLIPSIS)) {
int lastSpace = workingText.lastIndexOf(' ');
if (lastSpace == -1) break;
workingText = workingText.substring(0, lastSpace).trim();
strippedText = stripEndPunctuation(workingText);
}
workingText = strippedText + ELLIPSIS;
SpannableStringBuilder dest = new SpannableStringBuilder(workingText);
if (fullText instanceof Spanned) {
TextUtils.copySpansFrom((Spanned) fullText, 0, workingText.length(), null, dest, 0);
}
return dest;
}
public String stripEndPunctuation(CharSequence workingText) {
return mEndPunctPattern.matcher(workingText).replaceFirst("");
}
}
private class EllipsizeStartStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
Layout layout = createWorkingLayout(fullText);
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
int textLength = fullText.length();
int cutOffLength = textLength - cutOffIndex;
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
String workingText = TextUtils.substring(fullText, cutOffLength, textLength).trim();
while (!isInLayout(ELLIPSIS + workingText)) {
int firstSpace = workingText.indexOf(' ');
if (firstSpace == -1) break;
workingText = workingText.substring(firstSpace, workingText.length()).trim();
}
workingText = ELLIPSIS + workingText;
SpannableStringBuilder dest = new SpannableStringBuilder(workingText);
if (fullText instanceof Spanned) {
TextUtils.copySpansFrom((Spanned) fullText, textLength - workingText.length(),
textLength, null, dest, 0);
}
return dest;
}
}
private class EllipsizeMiddleStrategy extends EllipsizeStrategy {
@Override
protected CharSequence createEllipsizedText(CharSequence fullText) {
Layout layout = createWorkingLayout(fullText);
int cutOffIndex = layout.getLineEnd(mMaxLines - 1);
int textLength = fullText.length();
int cutOffLength = textLength - cutOffIndex;
if (cutOffLength < ELLIPSIS.length()) cutOffLength = ELLIPSIS.length();
cutOffLength += cutOffIndex % 2; // Make it even.
String firstPart = TextUtils.substring(
fullText, 0, textLength / 2 - cutOffLength / 2).trim();
String secondPart = TextUtils.substring(
fullText, textLength / 2 + cutOffLength / 2, textLength).trim();
while (!isInLayout(firstPart + ELLIPSIS + secondPart)) {
int lastSpaceFirstPart = firstPart.lastIndexOf(' ');
int firstSpaceSecondPart = secondPart.indexOf(' ');
if (lastSpaceFirstPart == -1 || firstSpaceSecondPart == -1) break;
firstPart = firstPart.substring(0, lastSpaceFirstPart).trim();
secondPart = secondPart.substring(firstSpaceSecondPart, secondPart.length()).trim();
}
SpannableStringBuilder firstDest = new SpannableStringBuilder(firstPart);
SpannableStringBuilder secondDest = new SpannableStringBuilder(secondPart);
if (fullText instanceof Spanned) {
TextUtils.copySpansFrom((Spanned) fullText, 0, firstPart.length(),
null, firstDest, 0);
TextUtils.copySpansFrom((Spanned) fullText, textLength - secondPart.length(),
textLength, null, secondDest, 0);
}
return TextUtils.concat(firstDest, ELLIPSIS, secondDest);
}
}
}
Complete source: EllipsizingTextView.java
Related videos on Youtube
Arutha
Updated on December 13, 2021Comments
-
Arutha over 2 years
I need to ellipsize a multi-line textview. My component is large enough to display at least 4 lines with the ellipse, but only 2 lines are displayed. I tried to change the minimum and maximum number of rows of the component but it changes nothing.
-
Cheryl Simon over 14 yearsDo you have a style or theme that is applied to your TextView, that could be specifying a maximum size?
-
UMAR-MOBITSOLUTIONS over 13 yearsdid you find solution to this problem or not?
-
STeN over 12 yearsHi, after fighting with the problem of having the 2 lines of text (maxLines=2) and three dots at the end of the text (ellipsize=end) I have found that it works on some devices and on some not (I have ~15 devices to test). It works usually on devices with the resolution higher then HVGA (320x480px), but also on some HTC with 240x320px... The only solution is to have a custom TextView as shown below...
-
HannahMitt over 9 yearsMine worked fine after removing "android:textIsSelectable=true"
-
aleb almost 9 yearsAs Robert Nekic said, there was an Android bug which is now fixed: code.google.com/p/android/issues/detail?id=2254 Would be good to know which is the first Android release where this is fixed.
-
Paul Wintz over 6 yearsFor me, android:ellipsize="end" works, but android:ellipsize="middle" does not.
-
-
Arutha over 14 yearsHow to use these ? The text is still truncated to two lines whatever the value that I give them
-
moraes over 14 yearsHow are you setting it? I think you're missing something but it is hard to guess.
-
Arutha over 14 yearsI used the same settings but my text is always trunked to two lines
-
moraes over 14 yearsEeeks. You are right. I tried that and could not make a 4-line ellipsis. It breaks always at the second line.
-
Lysogen about 13 yearsSeriously, Try this if you don't think it works it does for me:
code <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:maxLines="4" android:ellipsize="marquee" android:singleLine="false" android:text="Hai make this a very long string that wraps at least 4 lines!" />
-
Pzanno almost 13 yearsThis does work, though it should be mentioned that this will no longer add the ellipsis to the end of the last line, but in my opinion this still a good solution to the problem. Also, you will need to use the android:maxLines="4" to limit it to 4 lines.
-
Micah Hainline almost 13 yearsLysogen, this solution doesn't actually work, at least not for others here. Perhaps you are using a device very different from other people, but as a general solution, this is not effective.
-
pents90 almost 13 yearsThanks for this code, it worked very well. One wrinkle for other users: you will need to explicitly call setMaxLines(int) rather than just setting the property in XML.
-
Diego Tori over 12 yearsActually, it should be very trivial to map out these variables to XML attributes via attrs.xml.
-
Matt K over 12 yearsOP's question was specifically for ellipsis on the end, so it won't work.
-
Matt K over 12 yearsyep, doesn't work. still truncated to two lines. this is frustrating!
-
chengbo over 12 yearsi found a problem if workingText is Chinese. because Chinese doesn't have SPACE, so this code works not perfectly, i have modified some code below, hope will help. while (createWorkingLayout(workingText + ELLIPSIS).getLineCount() > maxLines) { // int lastSpace = workingText.lastIndexOf(' '); // if (lastSpace == -1) { // break; // } // workingText = workingText.substring(0, lastSpace); // 由于我们大多数情况下workingText为中文,所以按照之前的逻辑找空格是不合适的 // 这里改成直接替换最后的字符 workingText = workingText.substring(0, workingText.length() - 1 - 1); }
-
stealthcopter over 12 yearsAdding the following into the constructor will allow you to set the maxlines via XML: TypedArray a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.maxLines }); setMaxLines(a.getInt(0, 2));
-
pgsandstrom over 12 yearsThis screws up all the non-string attributes that the TextViews CharSequence holds. For example give the TextView a Spannable with bold text, and the bold is not shown.
-
Jason Robinson over 12 yearsandroid:singleLine is definitely FALSE by default
-
Nguyen Minh Binh about 12 yearsI copy this codes to a demo project and the tesxtview just shows 2 lines.
-
aleb about 12 yearsI created an Android library with this component and changed it to be able to show as many lines of text as possible and ellipsize the last one; see github.com/triposo/barone You can see it in action in any of our travel guides, when displaying Suggestions: play.google.com/store/apps/…
-
Cel about 12 yearsWorks for me on Ice Cream Sandwich tablet!
-
user123321 about 12 yearsThe comment above works better than the original. However, you should modify the constructors to this: public EllipsizingTextView(Context context, AttributeSet attrs) { this(context, attrs, android.R.attr.textViewStyle); } public EllipsizingTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); super.setEllipsize(null); TypedArray a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.maxLines }); setMaxLines(a.getInt(0, Integer.MAX_VALUE)); a.recycle(); }
-
Kevin Parker almost 12 yearsThis answer does not even work... You should try your answers before posting them.
-
moraes almost 12 years@Kevin: This answer is very old. You should try it with an Android version from January 29, 2010 before saying that it doesn't work and downvoting it.
-
PCoder over 11 yearsWorks for me on SII gingerbread. It would be interesting to know more about the devices on which this does not work.
-
Weblance over 11 yearsThe bug seems to have been fixed in 4.0.4. It is resolved on Jelly Bean, at least.
-
desgraci over 11 yearsthis has a ugly issue, when you put a LinearLayout, with a EllipsizingTextView followed by a TextView, it will only have one.
-
desgraci over 11 yearsthe problem is allocated in createWorkingLayout, and it will be showed on the xml editor as IllegalArgumentException: Layout: -40 < 0 at android.text.Layout.<init>(Layout.java:138) at android.text.StaticLayout.<init>(StaticLayout.java:104) StaticLayout.<init>(StaticLayout.java:90) StaticLayout.<init>(StaticLayout.java:68) StaticLayout.<init>(StaticLayout.java:48) at EllipsizingTextView.createWorkingLayout(EllipsizingTextView.java:199) Hope this help...
-
Matt Accola over 11 yearsThis does work on my Gingerbread device (Droid Bionic), however it has the downside of adding several levels in the stack during the draw phase. This has caused me to receive StackOverflowErrors in some cases...which, admittedly, has more to do with the complexity of my view hierarchy than the component itself.
-
Veer over 11 yearsOn Galaxy Ace with 2.2, didn't worked. On Nexus S with ICS, it shows 4 lines but no "..." at end.
-
dvs over 11 yearsThis worked for me on a Droid 3 with 2.3.4 using 2 max lines and a fixed dp height.
-
Paul almost 11 yearsExtended the solution to support the other ellipsize types: gist.github.com/imhoff/6245640
-
Stephane Mathis over 10 yearsAwesome, I was just about to find the original library and translate it myself, thanks !
-
Malachiasz about 10 yearsseems much simpler than extending TextView class
-
Nativ about 10 yearsDoesn't works when doing textview.setMovementMethod(LinkMovementMethod.getInstance())
-
Bart Burg over 9 yearsAnd if it doesn't work with this code (in some versions), add the "special sauce" android:scrollHorizontally="true"
-
Bart Burg over 9 yearshooloovoo's answer is much easier and working solution
-
Bart Burg over 9 yearsYou shouldn't answer a question with an acknowledge
-
Ricardo over 9 yearsThis works for me and is the simplest answer of all.
-
Simas over 8 yearsDoesn't work if text set with
setText(..., TextView.BufferType.SPANNABLE);
. -
CoolMind over 8 yearsThanks, the only working solution (tested several). Wondered, that your rating was 1.
-
GreenROBO about 8 years@aleb Hey man Thanks a Ton! I just Drag Your
EllipsizingTextView
class into My Project and it works! -
Alexey Strakh almost 8 yearsany news on spannable? @Simas. I've ran into the same issue
-
SalutonMondo over 7 yearsit works on my device with a samsung asm9100 run android 6.0.1 android.
-
javaxian over 6 yearsI can confirm that it's the only clean and working solution, just use the EllipsizingTextView class and forget about hacks.
-
Borja almost 6 yearsIt's also possible to use it with EditText if needed
-
Shubham Naik almost 6 yearsthis is same as setting ellipsize attr in xml.
-
Arundas K V over 5 yearsI too had this issue. Android Preview was wrong. In device its fine
-
Jonty800 over 5 yearsDoesn't work correctly when rendering html (Html.fromHtml)
-
Riddhi Shah over 4 yearsHi, your solutions work great! but I want to put Click event on ...ViewMore text. I tried to add clickable span in resetText(). But its not working