Android多行TextView末尾省略问题解决

2023/08/29 note 共 13864 字,约 40 分钟

背景

Android上TextView多行且末尾省略时,末尾省略号不能显示

解决方案

自定义EllipsizeEndTextView,换用setFullText()方法设置文案。
这是一个取巧的方案,没有动系统的setText()方法。如果要适配系统的setText()方法,需要重写setText()方法,还要重写其他几个设置文字相关的方法,可以参考EllipsizingEndTextView的做法,我实际使用发现末尾没有出现省略号,代码需要改一点,懒的改这个代码。

import android.content.Context  
import android.graphics.Canvas  
import android.text.Layout  
import android.text.StaticLayout  
import android.util.AttributeSet  
import androidx.appcompat.widget.AppCompatTextView  
  
/**  
 * 多行文本末尾省略号TextView  
 *  
 * ref:  
 * https://gist.github.com/stepango/1dcf6055a80f840f9185  
 * https://blog.csdn.net/chuekup/article/details/7989427  
 *  
 * Created by guanliang on 2023/8/17  
 */  
class EllipsizeEndTextView @JvmOverloads constructor(  
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.textViewStyle  
) : AppCompatTextView(context, attrs, defStyleAttr) {  
  
    private var mFullText: String = ""  
    private var mLineSpacingMult = 1.0f  
    private var mLineAddVertPad = 0.0f  
    private var mNeedEllipsize = false  
  
    fun setFullText(text: String) {  
        mFullText = text  
        mNeedEllipsize = true  
        invalidate()  
    }  
  
    override fun setLineSpacing(add: Float, mult: Float) {  
        mLineAddVertPad = add  
        mLineSpacingMult = mult  
        super.setLineSpacing(add, mult)  
    }  
  
    override fun onDraw(canvas: Canvas?) {  
        if (mNeedEllipsize) {  
            super.setEllipsize(null)  
            resetText()  
        }  
        super.onDraw(canvas)  
    }  
  
    private fun resetText() {  
        val maxLines = maxLines  
        var workingText = mFullText  
        if (maxLines != -1) {  
            val layout = createWorkingLayout(workingText)  
            if (layout.lineCount > maxLines) {  
                workingText = mFullText.substring(0, layout.getLineEnd(maxLines - 1))  
                while (createWorkingLayout(workingText + ELLIPSIS).lineCount > maxLines) {  
                    val endIndex = workingText.length - 1  
                    if (endIndex <= 0) {  
                        break  
                    }  
                    workingText = workingText.substring(0, endIndex)  
                }  
                workingText += ELLIPSIS  
            }  
        }  
        text = workingText  
        mNeedEllipsize = false  
    }  
  
    private fun createWorkingLayout(workingText: CharSequence?): Layout {  
        return StaticLayout(  
            workingText, paint,  
            width - compoundPaddingLeft - compoundPaddingRight,  
            Layout.Alignment.ALIGN_NORMAL, mLineSpacingMult,  
            mLineAddVertPad, false /* includepad */  
        )  
    }  
  
    companion object {  
        private const val ELLIPSIS = "…" // \u2026  
    }  
}  

上面的EllipsizingEndTextView代码备份

Android TextView not support ellipsize if your text fills more than i line. This implementation solved this problem and allow you to use Instagram style end of line mark “[…]”

/*  
 * Copyright (C) 2011 Micah Hainline  
 * Copyright (C) 2012 Triposo  
 * Copyright (C) 2013 Paul Imhoff  
 * Copyright (C) 2014 Shahin Yousefi  
 * Copyright (C) 2015 Stepan Goncharov  
 *  
 * 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.bandlab.bandlab.views;  
  
import android.content.Context;  
import android.content.res.TypedArray;  
import android.graphics.Canvas;  
import android.graphics.Color;  
import android.support.annotation.NonNull;  
import android.support.v7.widget.AppCompatTextView;  
import android.text.Layout;  
import android.text.Layout.Alignment;  
import android.text.Spannable;  
import android.text.SpannableString;  
import android.text.SpannableStringBuilder;  
import android.text.Spanned;  
import android.text.StaticLayout;  
import android.text.TextUtils;  
import android.text.TextUtils.TruncateAt;  
import android.text.style.ForegroundColorSpan;  
import android.util.AttributeSet;  
  
import java.util.ArrayList;  
import java.util.List;  
import java.util.regex.Pattern;  
  
/**  
 * A {@link android.widget.TextView} that ellipsizes more intelligently.  
 * This class supports ellipsizing multiline text through setting {@code android:ellipsize}  
 * and {@code android:maxLines}.  
 * <p/>  
 * Note: {@link android.text.TextUtils.TruncateAt#MARQUEE} ellipsizing type is not supported.  
 */  
public class EllipsizingTextView extends AppCompatTextView {  
    public static final int ELLIPSIZE_ALPHA = 0x88;  
    private SpannableString ELLIPSIS = new SpannableString("[\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;  
  
    /**  
     * The end punctuation which will be removed when appending {@link #ELLIPSIS}.  
     */  
    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, android.R.attr.ellipsize}, defStyle, 0);  
        setMaxLines(a.getInt(0, Integer.MAX_VALUE));  
        a.recycle();  
        setEndPunctuationPattern(DEFAULT_END_PUNCTUATION);  
        final int currentTextColor = getCurrentTextColor();  
        final int ellipsizeColor = Color.argb(ELLIPSIZE_ALPHA, Color.red(currentTextColor), Color.green(currentTextColor), Color.blue(currentTextColor));  
        ELLIPSIS.setSpan(new ForegroundColorSpan(ellipsizeColor), 0, ELLIPSIS.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);  
    }  
  
    public void setEndPunctuationPattern(Pattern pattern) {  
        mEndPunctPattern = pattern;  
    }  
  
    @SuppressWarnings("unused")  
    public void addEllipsizeListener(@NonNull EllipsizeListener listener) {  
        mEllipsizeListeners.add(listener);  
    }  
  
    @SuppressWarnings("unused")  
    public void removeEllipsizeListener(@NonNull EllipsizeListener listener) {  
        mEllipsizeListeners.remove(listener);  
    }  
  
    @SuppressWarnings("unused")  
    public boolean isEllipsized() {  
        return isEllipsized;  
    }  
  
    /**  
     * @return The maximum number of lines displayed in this {@link android.widget.TextView}.  
     */  
    public int getMaxLines() {  
        return mMaxLines;  
    }  
  
    @Override  
    public void setMaxLines(int maxLines) {  
        super.setMaxLines(maxLines);  
        mMaxLines = maxLines;  
        isStale = true;  
    }  
  
    /**  
     * Determines if the last fully visible line is being ellipsized.  
     *  
     * @return {@code true} if the last fully visible line is being ellipsized;  
     * otherwise, returns {@code false}.  
     */  
    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 instanceof Spanned ? (Spanned) text : 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);  
    }  
  
    /**  
     * Sets the ellipsized text if appropriate.  
     */  
    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);  
            }  
        }  
    }  
  
    /**  
     * Causes words in the text that are longer than the view is wide to be ellipsized  
     * instead of broken in the middle. Use {@code null} to turn off ellipsizing.  
     * <p/>  
     * Note: Method does nothing for {@link android.text.TextUtils.TruncateAt#MARQUEE}  
     * ellipsizing type.  
     *  
     * @param where part of text to ellipsize  
     */  
    @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:  
            default:  
                mEllipsizeStrategy = new EllipsizeNoneStrategy();  
                break;  
        }  
    }  
  
    /**  
     * A listener that notifies when the ellipsize state has changed.  
     */  
    public interface EllipsizeListener {  
        void ellipsizeStateChanged(boolean ellipsized);  
    }  
  
    /**  
     * A base class for an ellipsize strategy.  
     */  
    private abstract class EllipsizeStrategy {  
        /**  
         * Returns ellipsized text if the text does not fit inside of the layout;  
         * otherwise, returns the full text.  
         *  
         * @param text text to process  
         * @return Ellipsized text if the text does not fit inside of the layout;  
         * otherwise, returns the full text.  
         */  
        public CharSequence processText(CharSequence text) {  
            return !isInLayout(text) ? createEllipsizedText(text) : text;  
        }  
  
        /**  
         * Determines if the text fits inside of the layout.  
         *  
         * @param text text to fit  
         * @return {@code true} if the text fits inside of the layout;  
         * otherwise, returns {@code false}.  
         */  
        public boolean isInLayout(CharSequence text) {  
            Layout layout = createWorkingLayout(text);  
            return layout.getLineCount() <= getLinesCount();  
        }  
  
        /**  
         * Creates a working layout with the given text.  
         *  
         * @param workingText text to create layout with  
         * @return {@link android.text.Layout} with the given text.  
         */  
        protected Layout createWorkingLayout(CharSequence workingText) {  
            return new StaticLayout(workingText, getPaint(),  
                    getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight(),  
                    Alignment.ALIGN_NORMAL, mLineSpacingMult,  
                    mLineAddVertPad, false /* includepad */);  
        }  
  
        /**  
         * Get how many lines of text we are allowed to display.  
         */  
        protected int getLinesCount() {  
            if (ellipsizingLastFullyVisibleLine()) {  
                int fullyVisibleLinesCount = getFullyVisibleLinesCount();  
                return fullyVisibleLinesCount == -1 ? 1 : fullyVisibleLinesCount;  
            } else {  
                return mMaxLines;  
            }  
        }  
  
        /**  
         * Get how many lines of text we can display so their full height is visible.  
         */  
        protected int getFullyVisibleLinesCount() {  
            Layout layout = createWorkingLayout("");  
            int height = getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();  
            int lineHeight = layout.getLineBottom(0);  
            return height / lineHeight;  
        }  
  
        /**  
         * Creates ellipsized text from the given text.  
         *  
         * @param fullText text to ellipsize  
         * @return Ellipsized text  
         */  
        protected abstract CharSequence createEllipsizedText(CharSequence fullText);  
    }  
  
    /**  
     * An {@link EllipsizingTextView.EllipsizeStrategy} that  
     * does not ellipsize text.  
     */  
    private class EllipsizeNoneStrategy extends EllipsizeStrategy {  
        @Override  
        protected CharSequence createEllipsizedText(CharSequence fullText) {  
            return fullText;  
        }  
    }  
  
    /**  
     * An {@link EllipsizingTextView.EllipsizeStrategy} that  
     * ellipsizes text at the end.  
     */  
    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();  
            CharSequence workingText = TextUtils.substring(fullText, 0, textLength - cutOffLength).trim();  
  
            while (!isInLayout(TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS))) {  
                int lastSpace = TextUtils.lastIndexOf(workingText, ' ');  
                if (lastSpace == -1) {  
                    break;  
                }  
                workingText = TextUtils.substring(workingText, 0, lastSpace).trim();  
            }  
  
            workingText = TextUtils.concat(stripEndPunctuation(workingText), ELLIPSIS);  
            SpannableStringBuilder dest = new SpannableStringBuilder(workingText);  
  
            if (fullText instanceof Spanned) {  
                TextUtils.copySpansFrom((Spanned) fullText, 0, workingText.length(), null, dest, 0);  
            }  
            return dest;  
        }  
  
        /**  
         * Strips the end punctuation from a given text according to {@link #mEndPunctPattern}.  
         *  
         * @param workingText text to strip end punctuation from  
         * @return Text without end punctuation.  
         */  
        public String stripEndPunctuation(CharSequence workingText) {  
            return mEndPunctPattern.matcher(workingText).replaceFirst("");  
        }  
    }  
  
    /**  
     * An {@link EllipsizingTextView.EllipsizeStrategy} that  
     * ellipsizes text at the start.  
     */  
    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();  
            CharSequence workingText = TextUtils.substring(fullText, cutOffLength, textLength).trim();  
  
            while (!isInLayout(TextUtils.concat(ELLIPSIS, workingText))) {  
                int firstSpace = TextUtils.indexOf(workingText, ' ');  
                if (firstSpace == -1) {  
                    break;  
                }  
                workingText = TextUtils.substring(workingText, firstSpace, workingText.length()).trim();  
            }  
  
            workingText = TextUtils.concat(ELLIPSIS, workingText);  
            SpannableStringBuilder dest = new SpannableStringBuilder(workingText);  
  
            if (fullText instanceof Spanned) {  
                TextUtils.copySpansFrom((Spanned) fullText, textLength - workingText.length(),  
                        textLength, null, dest, 0);  
            }  
            return dest;  
        }  
    }  
  
    /**  
     * An {@link EllipsizingTextView.EllipsizeStrategy} that  
     * ellipsizes text in the middle.  
     */  
    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(TextUtils.concat(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);  
        }  
    }  
}  

文档信息

Search

    Table of Contents