机器人椭圆大小多行文本视图

我需要一个椭圆大小的多行文本视图。我的组件足够大,可以显示至少4行带椭圆的内容,但只显示2行。我试图更改组件的最小和最大行数,但它什么也没有更改。

117827 次浏览

您应该检查以下几个属性: android: lines、 android: minLines、 android: maxLines。要显示最多4行并使其为椭圆形,只需要 android: maxLines 和 android: 椭圆形:

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

我也遇到过这个问题,有一个相当古老的错误仍然没有得到解决: 虫虫2254

我也遇到过同样的问题,我通过删除 android 修复了这个问题:

你需要在你的文本视图中包含以下内容:

android:singleLine="false"

默认为 true。您需要显式地将其设置为 false。

试试这个,它对我很有用,我有4行,它在最后/第4行的末尾添加了“ ...”。这和士气的答案是一样的,但是我有 singeLine = “ false”在里面。

<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!" />

这里有一个解决这个问题的办法。它是 TextView 的一个子类,实际上可用于椭圆大小调整。在前面的一个答案中列出的 android-textview-multiline-椭圆形代码,我发现在某些情况下存在 bug,而且是在 GPL 下,这对我们大多数人来说并不真正起作用。如果您愿意,可以自由地使用这段代码,不需要任何属性,也可以使用 Apache 许可证。注意,当文本变成省略号时,会有一个监听器通知您,我发现这非常有用。

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

代码运行得非常好! 如果不仅文本需要更改,还可以重载 onSizeChanged 方法。

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

基于 Micah Hainline 的解决方案和 Aleb 的评论,我使用了下面的方法来处理跨文本,这样例如 myTextView.setText(Html.fromHtml("<b>Testheader</b> - Testcontent"));就可以工作了! 注意,这只适用于 Spanned。它可以被修改为与 StringSpanned一起工作。

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

解决了这个问题,最后,我给自己制定了一个简短的解决方案。 您只需要手动调整所需的行的大小,maxLine 属性就会剪切您的文本。

这个例子将你的文本最多剪切3行

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


}
});

对于那些感兴趣的人,这里有一个 C # Xamarin。 Micah 可爱的解决方案的 Android 端口:

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

只需在活动中添加代码

Textview.setEllipsize(TextUtils.TruncateAt.END)

这将在文本视图的末尾添加省略号

扩展 TextView 并覆盖这些方法:

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


}

在我的应用程序中,我遇到了类似的问题: 2行字符串,最后,如果字符串太长,就添加“ ...”。 我将 xml 文件中的这段代码用于 textview 标记:

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

这个也处理了我的 html,

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

原始源 链接

我结合了 Micah Hainline、 Alex B lusomething 和 Paul Imhoff 的解决方案,创建了一个椭圆大小的多行 TextView,它也支持 Spanned文本。

您只需要设置 android:ellipsizeandroid: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);
}
}
}

完整来源: EllipsizingTextView.java

这是我的解决方案。你可以在我的 github 上下载演示。 Https://github.com/krossford/krosslib/tree/master/android-project

这个截图是一个演示,maxLines = 4,我认为它工作得很好。

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

来自 Micah Hainline 的顶级答案非常棒,但是更棒的是用户 aleb 在评论中用 Micah 的回答建立了这个库:

我用这个组件创建了一个 Android 库,并将其更改为 能够显示尽可能多的文本行和省略号的最后 第一,参见 github.com/triposo/barone

它还有一些更多的功能,如果你只需要 TextView,它是 给你

也许这会帮助其他人比我更快地找到它: -)

要在第二行末尾添加 ...,如果文本较短,则保留一行:

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

这很晚了,但是我发现了一个来自 Android 的 Apache 许可类,它在股票邮件应用程序中使用: 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;
}
}

在我的例子中,没有必要用 Java 编写代码。一切正常。不需要像 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" />

但是在 Android Studio (v3.0)的布局预览中似乎有一个 bug: layout preview

考虑到 Android 7.1.1在我的设备上的运行情况: device screenshot

在 java 中不需要做额外的代码,只需要这样做:

<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" />

截图:

Android studio screenshot

我发现使用 ConstraintLayout 加上开始和结束边界设置加上 laywidth = “0dp”可以看到多行 TextView 的省略号。AppWidget 的解决方案仍然很大。