android ellipsize multiline textview

AndroidMultilineTextviewEllipse

Android Problem Overview


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.

Android Solutions


Solution 1 - Android

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 - Android

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 - Android

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 - Android

I've run into this problem, too. There's a rather old bug about it that remains unanswered: [Bug 2254][1]

[1]: http://code.google.com/p/android/issues/detail?id=2254 "Bug 2254"

Solution 5 - Android

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

Solution 6 - Android

Got this problem to, and finaly, I build myself a short solution. You just have to ellipsize manually the line you want, your maxLine attribute will cut your text.

This example cut your text for 3 lines max

		final TextView title = (TextView)findViewById(R.id.text);
		title.setText("A really long text");
		ViewTreeObserver vto = title.getViewTreeObserver();
		vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

		    @Override
		    public void onGlobalLayout() {
		        ViewTreeObserver obs = title.getViewTreeObserver();
		        obs.removeGlobalOnLayoutListener(this);
		    	if(title.getLineCount() > 3){
		    		Log.d("","Line["+title.getLineCount()+"]"+title.getText());
		    		int lineEndIndex = title.getLayout().getLineEnd(2);
		    		String text = title.getText().subSequence(0, lineEndIndex-3)+"...";
		    		title.setText(text);
		    		Log.d("","NewText:"+text);
		    	}

		    }
		});

Solution 7 - Android

In my case, there is no need to code this in Java. Everything works as expected. No need for something like android:singleLine="false".

<TextView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:ellipsize="end"
  android:maxLines="4"
  android:text="@string/very_long_text" />

But there seems to be a bug in the layout preview of Android Studio (v3.0): layout preview

Given Android 7.1.1 on my device this is working: device screenshot

Solution 8 - Android

Based on the solutions by Micah Hainline and alebs comment, I came of with the following approach that works with Spanned texts, so that e.g. myTextView.setText(Html.fromHtml("<b>Testheader</b> - Testcontent")); works! Note that this only works with Spanned right now. It could maybe be modified to work with String and Spanned either way.

public class EllipsizingTextView extends TextView {
	private static final Spanned ELLIPSIS = new SpannedString("…");

	  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 Spanned fullText;
	  private int maxLines;
	  private float lineSpacingMultiplier = 1.0f;
	  private float lineAdditionalVerticalPadding = 0.0f;

	  public EllipsizingTextView(Context context) {
	    this(context, null);
	  }

	  public EllipsizingTextView(Context context, AttributeSet attrs) {
	    this(context, attrs, 0);
	  }

	  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));
	  }

	  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;
	  }

	  public boolean ellipsizingLastFullyVisibleLine() {
	    return maxLines == Integer.MAX_VALUE;
	  }

	  @Override
	  public void setLineSpacing(float add, float mult) {
	    this.lineAdditionalVerticalPadding = add;
	    this.lineSpacingMultiplier = mult;
	    super.setLineSpacing(add, mult);
	  }
  
	  @Override
	public void setText(CharSequence text, BufferType type) {
		  if (!programmaticChange && text instanceof Spanned) {
		      fullText = (Spanned) 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;
	    }
	  }

	  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(Canvas canvas) {
	    if (isStale) {
	      resetText();
	    }
	    super.onDraw(canvas);
	  }

	  private void resetText() {
	    Spanned workingText = fullText;
	    boolean ellipsized = false;
	    Layout layout = createWorkingLayout(workingText);
	    int linesCount = getLinesCount();
	    if (layout.getLineCount() > linesCount) {
	      // We have more lines of text than we are allowed to display.
	      workingText = (Spanned) fullText.subSequence(0, layout.getLineEnd(linesCount - 1));
	      while (createWorkingLayout((Spanned) TextUtils.concat(workingText, ELLIPSIS)).getLineCount() > linesCount) {
	        int lastSpace = workingText.toString().lastIndexOf(' ');
	        if (lastSpace == -1) {
	          break;
	        }
	        workingText = (Spanned) workingText.subSequence(0, lastSpace);
	      }
	      workingText = (Spanned) TextUtils.concat(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);
	      }
	    }
	  }

	  /**
	   * Get how many lines of text we are allowed to display.
	   */
	  private int getLinesCount() {
	    if (ellipsizingLastFullyVisibleLine()) {
	      int fullyVisibleLinesCount = getFullyVisibleLinesCount();
	      if (fullyVisibleLinesCount == -1) {
	        return 1;
	      } else {
	        return fullyVisibleLinesCount;
	      }
	    } else {
	      return maxLines;
	    }
	  }

	  /**
	   * Get how many lines of text we can display so their full height is visible.
	   */
	  private int getFullyVisibleLinesCount() {
	    Layout layout = createWorkingLayout(new SpannedString(""));
	    int height = getHeight() - getPaddingTop() - getPaddingBottom();
	    int lineHeight = layout.getLineBottom(0);
	    return height / lineHeight;
	  }

	  private Layout createWorkingLayout(Spanned workingText) {
	    return new StaticLayout(workingText, getPaint(),
	        getWidth() - getPaddingLeft() - getPaddingRight(),
	        Alignment.ALIGN_NORMAL, lineSpacingMultiplier,
	        lineAdditionalVerticalPadding, false /* includepad */);
	  }

	  @Override
	  public void setEllipsize(TruncateAt where) {
	    // Ellipsize settings are not respected
	  }
}

Solution 9 - Android

For those who are interested, here's a C# Xamarin.Android port of Micah's lovely solution:

public delegate void EllipsizeEvent(bool ellipsized);

public class EllipsizingTextView : TextView
{
    private const string Ellipsis = "...";

    public event EllipsizeEvent EllipsizeStateChanged;

    private bool isEllipsized;
    private bool isStale;
    private bool programmaticChange;
    private string fullText;
    private int maxLines = -1;
    private float lineSpacingMultiplier = 1.0f;
    private float lineAdditionalVerticalPadding;

    public EllipsizingTextView(Context context) : base(context) 
    {
    }

    public EllipsizingTextView(Context context, IAttributeSet attrs) : base(context, attrs) 
    {
    }

    public EllipsizingTextView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) 
    {
    }

    public EllipsizingTextView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer)
    {
    }

    public bool IsEllipsized 
    {
        get { return isEllipsized; }
    }

    public override void SetMaxLines(int maxLines) {
        base.SetMaxLines(maxLines);
        this.maxLines = maxLines;
        isStale = true;
    }

    public int GetMaxLines() 
    {
        return maxLines;
    }

    public override void SetLineSpacing(float add, float mult) 
    {
        lineAdditionalVerticalPadding = add;
        lineSpacingMultiplier = mult;
        base.SetLineSpacing(add, mult);
    }

    protected override void OnTextChanged(ICharSequence text, int start, int before, int after) 
    {
        base.OnTextChanged(text, start, before, after);
        if (!programmaticChange) 
        {
            fullText = text.ToString();
            isStale = true;
        }
    }

    protected override void OnDraw(Canvas canvas) 
    {
        if (isStale) 
        {
            base.Ellipsize = null;
            ResetText();
        }
        base.OnDraw(canvas);
    }

    private void ResetText() 
    {
        int maxLines = GetMaxLines();
        string workingText = fullText;
        bool ellipsized = false;
        if (maxLines != -1) 
        {
            Layout layout = CreateWorkingLayout(workingText);
            if (layout.LineCount > maxLines) 
            {
                workingText = fullText.Substring(0, layout.GetLineEnd(maxLines - 1)).Trim();
                while (CreateWorkingLayout(workingText + Ellipsis).LineCount > maxLines) 
                {
                    int lastSpace = workingText.LastIndexOf(' ');
                    if (lastSpace == -1) 
                    {
                        break;
                    }
                    workingText = workingText.Substring(0, lastSpace);
                }
                workingText = workingText + Ellipsis;
                ellipsized = true;
            }
        }
        if (workingText != Text) 
        {
            programmaticChange = true;
            try 
            {
                Text = workingText;
            } 
            finally 
            {
                programmaticChange = false;
            }
        }
        isStale = false;
        if (ellipsized != isEllipsized) 
        {
            isEllipsized = ellipsized;
            if (EllipsizeStateChanged != null)
                EllipsizeStateChanged(ellipsized);
        }
    }

    private Layout CreateWorkingLayout(string workingText) 
    {
        return new StaticLayout(workingText, Paint, Width - PaddingLeft - PaddingRight, Layout.Alignment.AlignNormal, lineSpacingMultiplier, lineAdditionalVerticalPadding, false);
    }

    public override TextUtils.TruncateAt Ellipsize
    {
        get 
        { 
	        return base.Ellipsize;
        }
        set 
        { 
        }
    }
}

Solution 10 - Android

To add ... in the end of the second line, saving 1 line if text is short:

android:maxLines="2"
android:ellipsize="end"

Solution 11 - Android

Just add code in your activity

Textview.setEllipsize(TextUtils.TruncateAt.END)

this will add ellipsis at the end of the textview

Solution 12 - Android

Extend TextView and override these methods:

CharSequence origText = "";
int maxLines = 2;

@Override
public void setText(CharSequence text, BufferType type) {
	super.setText(text, type);
	origText = text;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	
	CharSequence text = origText;
	onPreDraw();
	
	while(getLineCount() > maxLines) {
		text = text.subSequence(0, text.length()-1);
		super.setText(text + "...");
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		onPreDraw();
	}

}

Solution 13 - Android

This is my solution. you can download demo on my github. https://github.com/krossford/KrossLib/tree/master/android-project

This screenshot was a demo that maxLines = 4, I think it works well.

enter image description here

package com.krosshuang.krosslib.lib.view;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.TextView;

import java.util.ArrayList;

/*
如何使用?
How to use it?

> 1.在xml或者java代码中常规使用
> 1.use it like other views on xml and java code.

> 2.[必须] setMaxLines 方法替代在xml中的 "android:maxLines" 属性
> 2.[must] call the setMaxLines method to instead of the xml property android:maxLines.

> 3.[可选] 注意调用 setMultilineEllipsizeMode() 方法,具体请查看注释
> 3.[option] you can invoke setMultilineEllipsizeMode method, but I have not implement it.

*/

/**
* android自己的TextView对多行ellipsize处理的不好
* Created by krosshuang on 2015/12/17.
*/
public class EllipsizeEndTextView extends TextView {

    private static final String LOG_TAG = "EllipsizeTextView";

    /** 每一行都有省略号 */
    //TODO 该特性待完成
    public static final int MODE_EACH_LINE = 1;

    /** 最后一行才有省略号 */
    public static final int MODE_LAST_LINE = 2;

    private static final String ELLIPSIZE = "...";


    private ArrayList<String> mTextLines = new ArrayList<String>();

    private CharSequence mSrcText = null;
    private int mMultilineEllipsizeMode = MODE_LAST_LINE;
    private int mMaxLines = 1;
    private boolean mNeedIgnoreTextChangeAndSelfInvoke = false;


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

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

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

    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        if (!mNeedIgnoreTextChangeAndSelfInvoke) {
            super.onTextChanged(text, start, lengthBefore, lengthAfter);
            mSrcText = text;
        }
    }

    @Override
    public void setMaxLines(int maxlines) {
        super.setMaxLines(maxlines);
        mMaxLines = maxlines;
    }

    public int getSupportedMaxLines() {
        return mMaxLines;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        setVisibleText();
        super.onDraw(canvas);
        mNeedIgnoreTextChangeAndSelfInvoke = false;
    }

    private void setVisibleText() {

        if (mSrcText == null) {
            return;
        }

        //获得可使用的width get available width
        final int aw = getWidth() - getPaddingLeft() - getPaddingRight();

        String srcText = mSrcText.toString();

        //将原始的字符串先按原始数据中存在的换行符弄成多行字符串
        String[] lines = srcText.split("\n");
        //Log.i(LOG_TAG, "原始数据有: " + lines.length + " 行 " + Arrays.toString(lines));

        int maxLines = getSupportedMaxLines();

        //将原始文本分成几行后加入list
        mTextLines.clear();
        for (int i = 0; i < lines.length; i++) {
            mTextLines.add(lines[i]);
        }

        switch (mMultilineEllipsizeMode) {

            case MODE_EACH_LINE:
                break;

            default:
            case MODE_LAST_LINE:
                //开始遍历
                String eachLine = null;
                for (int i = 0; i < mTextLines.size() && i < maxLines - 1; i++) {

                    eachLine = mTextLines.get(i);

                    if (getPaint().measureText(eachLine, 0, eachLine.length()) > aw) {

                        //当前行超过可用宽度
                        boolean isOut = true;
                        int end = eachLine.length() - 1;
                        while (isOut) {
                            if (getPaint().measureText(eachLine.substring(0, end), 0, end) > aw) {
                                end--;
                            } else {
                                isOut = false;
                            }
                        }

                        mTextLines.set(i, eachLine.substring(0, end));  //当前行设置为裁剪后的
                        mTextLines.add(i + 1, eachLine.substring(end, eachLine.length()));  //将裁剪剩余的部分,加入下一行,刚好接下来发生的遍历就可以处理它,相当于一个递归

                    }
                }

                //遍历处理结束,所有的行都是在可用宽度以内的
                break;
        }

        //根据 maxLines 和 结果的行数,决定最小需要多少行
        int resultSize = Math.min(maxLines, mTextLines.size());

        //对最后一行做处理
        String lastLine = mTextLines.get(resultSize - 1);

        //最后一行有两种情况需要加...
        //1.最后一行数据本身很长,超过了可用宽度,那么裁剪后尾部加上...
        //2.最后一行不是很长,并没有超过可用宽度,但是它底下还有行没有显示,因此加上...
        if (getPaint().measureText(lastLine, 0, lastLine.length()) > aw || resultSize < mTextLines.size()) {

            boolean isOut = true;
            int end = lastLine.length();
            while (isOut) {
                if (getPaint().measureText(lastLine.substring(0, end) + ELLIPSIZE, 0, end + 3) > aw) {
                    end--;
                } else {
                    isOut = false;
                }
            }

            mTextLines.set(resultSize - 1, lastLine.substring(0, end) + ELLIPSIZE);
        }

        //开始构建结果
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i <  resultSize ; i++) {
            sb.append(mTextLines.get(i));
            if (i != resultSize - 1) {
                sb.append('\n');
            }
        }

        //构建完成,set
        if (sb.toString().equals(getText())) {
            return;
        } else {
            mNeedIgnoreTextChangeAndSelfInvoke = true;
            setText(sb.toString());
        }
    }

    /**
     * 设置ellipsize mode,暂时不支持
     * @deprecated
     * */
    public void setMultilineEllipsizeMode(int mode) {
        mMultilineEllipsizeMode = mode;
    }
}

Solution 14 - Android

This is late, but I found an Apache licensed class from Android, that's used in the stock mail app: https://android.googlesource.com/platform/packages/apps/UnifiedEmail/+/184ec73/src/com/android/mail/ui/EllipsizedMultilineTextView.java

/*
 * Copyright (C) 2013 Google Inc.
 * Licensed to The Android Open Source Project.
 *
 * 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.
 */
package com.android.mail.ui;
import android.content.Context;
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.util.AttributeSet;
import android.widget.TextView;
/**
 * A special MultiLine TextView that will apply ellipsize logic to only the last
 * line of text, such that the last line may be shorter than any previous lines.
 */
public class EllipsizedMultilineTextView extends TextView {
    public static final int ALL_AVAILABLE = -1;
    private int mMaxLines;
    public EllipsizedMultilineTextView(Context context) {
        this(context, null);
    }
    public EllipsizedMultilineTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    @Override
    public void setMaxLines(int maxlines) {
        super.setMaxLines(maxlines);
        mMaxLines = maxlines;
    }
    /**
     * Ellipsize just the last line of text in this view and set the text to the
     * new ellipsized value.
     * @param text Text to set and ellipsize
     * @param avail available width in pixels for the last line
     * @param paint Paint that has the proper properties set to measure the text
     *            for this view
     * @return the {@link CharSequence} that was set on the {@link TextView}
     */
    public CharSequence setText(final CharSequence text, int avail) {
        if (text == null || text.length() == 0) {
            return text;
        }
        setEllipsize(null);
        setText(text);
        if (avail == ALL_AVAILABLE) {
            return text;
        }
        Layout layout = getLayout();
        if (layout == null) {
            final int w = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
            layout = new StaticLayout(text, 0, text.length(), getPaint(), w, Alignment.ALIGN_NORMAL,
                    1.0f, 0f, false);
        }
        // find the last line of text and chop it according to available space
        final int lastLineStart = layout.getLineStart(mMaxLines - 1);
        final CharSequence remainder = TextUtils.ellipsize(text.subSequence(lastLineStart,
                text.length()), getPaint(), avail, TextUtils.TruncateAt.END);
        // assemble just the text portion, without spans
        final SpannableStringBuilder builder = new SpannableStringBuilder();
        builder.append(text.toString(), 0, lastLineStart);
        if (!TextUtils.isEmpty(remainder)) {
            builder.append(remainder.toString());
        }
        // Now copy the original spans into the assembled string, modified for any ellipsizing.
        //
        // Merely assembling the Spanned pieces together would result in duplicate CharacterStyle
        // spans in the assembled version if a CharacterStyle spanned across the lastLineStart
        // offset.
        if (text instanceof Spanned) {
            final Spanned s = (Spanned) text;
            final Object[] spans = s.getSpans(0, s.length(), Object.class);
            final int destLen = builder.length();
            for (int i = 0; i < spans.length; i++) {
                final Object span = spans[i];
                final int start = s.getSpanStart(span);
                final int end = s.getSpanEnd(span);
                final int flags = s.getSpanFlags(span);
                if (start <= destLen) {
                    builder.setSpan(span, start, Math.min(end, destLen), flags);
                }
            }
        }
        setText(builder);
        return builder;
    }
}

Solution 15 - Android

I have had the same Problem. I fixed it by just deleting android:ellipsize="marquee"

Solution 16 - Android

Code worked very well! You can overload onSizeChanged method, if not only Text has to be Changed.

@Override
protected void onSizeChanged (int w, int h, int oldw, int oldh) {
	isStale = true;
	super.onSizeChanged(w, h, oldw, oldh);
}

Solution 17 - Android

This one handled my html as well,

/*
 * Copyright (C) 2013 Google Inc.
 * Licensed to The Android Open Source Project.
 *
 * 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.
 */

package com.android.mail.ui;

import android.content.Context;
import android.content.res.TypedArray;
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.util.AttributeSet;
import android.widget.TextView;
/**
 * A special MultiLine TextView that will apply ellipsize logic to only the last
 * line of text, such that the last line may be shorter than any previous lines.
 */
public class EllipsizedMultilineTextView extends TextView {
    public static final int ALL_AVAILABLE = -1;
    private int mMaxLines;

    public EllipsizedMultilineTextView(Context context) {
        super(context);
    }
    public EllipsizedMultilineTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
	    init(context, attrs);
    }
    public EllipsizedMultilineTextView(Context context, AttributeSet attrs, int defStyle) {
    	super(context, attrs, defStyle);
    	init(context, attrs);
    }

    private final void init(Context context, AttributeSet attrs) {
        final TypedArray a = context.obtainStyledAttributes(attrs,
			new int[] { android.R.attr.maxLines });
        setMaxLines(a.getInt(0, 2));
    }
    @Override
    public void setMaxLines(int maxlines) {
        super.setMaxLines(maxlines);
        mMaxLines = maxlines;
    }
    /**
     * Ellipsize just the last line of text in this view and set the text to the
     * new ellipsized value.
     * @param text Text to set and ellipsize
     * @param avail available width in pixels for the last line
     * @param paint Paint that has the proper properties set to measure the text
     *            for this view
     * @return the {@link CharSequence} that was set on the {@link TextView}
     */
    public CharSequence setText(final CharSequence text, int avail) {
        if (text == null || text.length() == 0) {
            return text;
        }
        setEllipsize(null);
        setText(text);
        if (avail == ALL_AVAILABLE) {
            return text;
        }
        Layout layout = getLayout();
        if (layout == null) {
            final int w = getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
            layout = new StaticLayout(text, 0, text.length(), getPaint(), w, Alignment.ALIGN_NORMAL,
                1.0f, 0f, false);
        }
        // find the last line of text and chop it according to available space
        final int lastLineStart = layout.getLineStart(mMaxLines - 1);
        final CharSequence remainder = TextUtils.ellipsize(text.subSequence(lastLineStart,
            text.length()), getPaint(), avail, TextUtils.TruncateAt.END);
        // assemble just the text portion, without spans
        final SpannableStringBuilder builder = new SpannableStringBuilder();
        builder.append(text.toString(), 0, lastLineStart);
        if (!TextUtils.isEmpty(remainder)) {
            builder.append(remainder.toString());
        }
        // Now copy the original spans into the assembled string, modified for any ellipsizing.
        //
        // Merely assembling the Spanned pieces together would result in duplicate CharacterStyle
        // spans in the assembled version if a CharacterStyle spanned across the lastLineStart
        // offset.
        if (text instanceof Spanned) {
            final Spanned s = (Spanned) text;
            final Object[] spans = s.getSpans(0, s.length(), Object.class);
            final int destLen = builder.length();
            for (int i = 0; i < spans.length; i++) {
                final Object span = spans[i];
                final int start = s.getSpanStart(span);
                final int end = s.getSpanEnd(span);
                final int flags = s.getSpanFlags(span);
                if (start <= destLen) {
                    builder.setSpan(span, start, Math.min(end, destLen), flags);
                }
            }
        }
        setText(builder);
        return builder;
    }
}

Orignal Source LINK

Solution 18 - Android

The top answer here from Micah Hainline works great, but even better is the library that was built from it by user aleb as he posted in the comments under Micahs answer:

> I 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

There are some more features to it, if you only need the TextView, it is here.

Maybe this will help others find it faster than I did :-)

Solution 19 - Android

There's no need to do extra codes in java just do this:

<TextView
    android:id="@+id/productDescription"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:maxLines="4"
    android:ellipsize="end"
    android:singleLine="false"
    android:text="Vehicle Compatibility (Brand/Model) - Hero Ez, Hero Maestro, Hero Pleasure, Honda Activa, Honda Dio, Piaggio Vespa, Suzuki Access, Suzuki Swish, Yamaha Alpha, Yamaha Fascino, Yamaha Ray\nFeatures: Higher NSD with Connected Blocks, Continuous centre Groove, Deeper Shoulder Grooves and Sipes on the tread Blocks\nFunctions: More rubber to wear ; Construction Type: Bias ; Grip: Excellent Dry &amp; Wet Grip ; Functions:Functions: More rubber to wear Better mass transfer, More 'biting' edges to Grip on or Off road" />

Screenshot:

Android studio screenshot

Solution 20 - Android

I've found that using ConstraintLayout with start and end bounds setup plus layout_width="0dp" sees the ellipses for multiline TextView. A solution for AppWidget remains at large.

Solution 21 - Android

There are a few attributes you should check: android:lines, android:minLines, android:maxLines. To display a maximum of 4 lines and ellipsize it, you just need android:maxLines and android:ellipsize:

<TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:maxLines="4"
    android:ellipsize="marquee"
    android:text="Hai!"
    />

Solution 22 - Android

You need to include this in your textview:

android:singleLine="false"

by default its true. You need to explicitly set it false.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAruthaView Question on Stackoverflow
Solution 1 - AndroidMicah HainlineView Answer on Stackoverflow
Solution 2 - AndroidhooloovooView Answer on Stackoverflow
Solution 3 - AndroidLysogenView Answer on Stackoverflow
Solution 4 - AndroidRobert NekicView Answer on Stackoverflow
Solution 5 - AndroidshayousefiView Answer on Stackoverflow
Solution 6 - AndroidPercussView Answer on Stackoverflow
Solution 7 - AndroidTimo BährView Answer on Stackoverflow
Solution 8 - AndroidTillView Answer on Stackoverflow
Solution 9 - AndroidKirk WollView Answer on Stackoverflow
Solution 10 - AndroidIgor SKRYLView Answer on Stackoverflow
Solution 11 - AndroidAmrutView Answer on Stackoverflow
Solution 12 - AndroidsentytheeView Answer on Stackoverflow
Solution 13 - AndroidkrosshjView Answer on Stackoverflow
Solution 14 - AndroidParkerView Answer on Stackoverflow
Solution 15 - AndroidTianDongView Answer on Stackoverflow
Solution 16 - Androiduser1129490View Answer on Stackoverflow
Solution 17 - AndroidSamuelView Answer on Stackoverflow
Solution 18 - AndroidBjörn KechelView Answer on Stackoverflow
Solution 19 - AndroidApoorv PandeyView Answer on Stackoverflow
Solution 20 - AndroidstrayaView Answer on Stackoverflow
Solution 21 - AndroidmoraesView Answer on Stackoverflow
Solution 22 - AndroidLysogenView Answer on Stackoverflow