Android TextView 文本展示优化

前言

TextView是相当复杂的UI组件,TextView不仅仅支持纯文本展示,而且还支持图片、SpannableString、文本输入、超链接等诸多功能,因此很多View本身也是直接继承自TextView的,如EditText、Button、Chronometer等。可见TextView功能非常强大,基本上是app中使用率最高的View组件。

不过 TextView 缺点也不少,主要问题点如下:

  • 跑马灯执行的条件过高,且部分属性有一定的重复问题
  • setText 容易触发requestLayout
  • 换行文本容易出现犬牙(很多小说类app自行绘制文本来解决此问题)

当然,以上是大多数情况中我们容易遇到的问题。

优化方法

上面我们列出了3个常见的问题,我们这边逐一来看。

跑马灯问题

TextView对跑马灯的要求比较高,必须是单行文本,而且必须设置MaxLines,而且不支持Lines设置,另外必须是focused或者是selected,这显然增加了一些成本,要知道如果父布局focused,那么子View是不可能focused,显然对TV设备不够有好。但是另外一个问题,View可以同时具备Focused和Selected状态,这显然增加了问题的难度,为此我们需要剥离focused状态。

scss 复制代码
   private void startMarquee() {
        // Do not ellipsize EditText
        if (getKeyListener() != null) return;

        if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
//宽度大于0,或者硬件加速
            return;
        }


        if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected())
                && getLineCount() == 1 && canMarquee()) {

            //获焦或者selected状态,由于focus相对于selected复杂,建议使用selected
            //TextLayout行数必须为1

            if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
                mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
                final Layout tmp = mLayout;
                mLayout = mSavedMarqueeModeLayout;
                mSavedMarqueeModeLayout = tmp;
                setHorizontalFadingEdgeEnabled(true);
                requestLayout();
                invalidate();
            }

            if (mMarquee == null) mMarquee = new Marquee(this);
            mMarquee.start(mMarqueeRepeatLimit);
        }
    }

那么,这里我们通过优化,使其仅在selected状态具备跑马灯,当然,如果你还想用selected状态实现其他用途,显然是无法使用了,不过系统中还有setEnable、setActivated状态供大家使用。

下面是跑马灯兼容逻辑

java 复制代码
public class MarqueeTextView extends AppCompatTextView {

    private static final String TAG = "MarqueeTextView";
    private boolean isMarqueeEnable = false;

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

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

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

        /**
         * TextView.canMarquee() == false 时是不会滚动的
         * 一般原因是行数问题影响,导致宽度不合适,而android:lines是无效的
         *  focus 或者 selected状态才能跑马灯
         */

        setMaxLines(1);
        setSingleLine(true);

        if (isMarqueeEnable) {
            setMarqueeRepeatLimit(-1);
            setEllipsize(TextUtils.TruncateAt.MARQUEE);
        } else {
            setMarqueeRepeatLimit(0);
            setEllipsize(TextUtils.TruncateAt.END);
        }
        super.setSelected(isMarqueeEnable);
    }

    public void setMarqueeEnable(boolean enable) {
        if (isMarqueeEnable != enable) {
            isMarqueeEnable = enable;
            if (enable) {
                super.setSelected(true);
                setMarqueeRepeatLimit(-1);
                setEllipsize(TextUtils.TruncateAt.MARQUEE);
            } else {
                super.setSelected(false);
                setMarqueeRepeatLimit(0);
                setEllipsize(TextUtils.TruncateAt.END);
            }
        }
    }

    public boolean isMarqueeEnable() {
        return isMarqueeEnable;
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (!isMarqueeEnable) {
            return;
        }
        if (getLineCount() > 1) {
            Log.e(TAG, "the marquee will not work if  TextLineCount > 1");
        }
        if (getMarqueeRepeatLimit() <= 0) {
            Log.e(TAG, "the marquee may not work if  MarqueeRepeatLimit != -1");
        }
    }

    @Override
    public void setSelected(boolean selected) {
   //复写此方法,禁止外部调用,保证只有内部调用
    }

  
}

频繁触发requestLayout

TextView很容易触发requestLayout,除非长宽必须是固定大小的,不过固定大小可能遇到文本展示的不全的问题,另外Google也提供了PrecomputedText异步测量文本的方式去优化性能,但是requestLayout造成的性能问题实际上比测量要大,另外PrecomputedText编码方式也不够方便。

那么有没有更好的方法去抑制requestLayout的频繁调用呢?

实际上我们对单行文本的使用远超多行文本,即便是播放器时间进度也是单行文本,因此我们可以自行测量单行文本,比较前后的尺寸差异,选择性调用requestLayout。

方式很多,这里我们利用BoringLayout优化,当然在android 5.0之前的版本BoringLayout 兼容性并不好,因此这里还引入StaticLayout进行兜底。

优化setText

构建Layout protected Layout buildTextLayout(CharSequence text, int wantWidth) { // fixed: Chinese word is not boring in android 4.4,在Android 4.4长度可能小于measureText测量的 float measureTextWidth = mTextPaint.measureText(text, 0, text.length()); BoringLayout.Metrics boring = BoringLayout.isBoring(text, mTextPaint,UNKNOWN_BORING);

ini 复制代码
    float lineSpaceMult = mLineSpacingMult;
    if (lineSpaceMult < 1F) {
        lineSpaceMult = 1.0f;
    }
    if (boring != null) {
        int outWidth = wantWidth != ANY_WIDTH ? wantWidth : (int) (Math.max(boring.width,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
        return BoringLayout.make(text, mTextPaint,
                outWidth,
                Layout.Alignment.ALIGN_NORMAL,
                0,
                mLineSpacingAdd,
                boring,
                mIncludeFontPadding);
    }
    //fix Android  4.4  mLineSpacingMult 必须大于0
    float desiredWidthForStaticLayout = StaticLayout.getDesiredWidth(text, mTextPaint);
    int desiredWidth = (int) (Math.max(desiredWidthForStaticLayout,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
    int outWidth = wantWidth != ANY_WIDTH ? wantWidth : desiredWidth;
    StaticLayout staticLayout = new StaticLayout(text,
            mTextPaint,
            outWidth,
            Layout.Alignment.ALIGN_NORMAL,
            lineSpaceMult,
            mLineSpacingAdd,
            mIncludeFontPadding);
    return staticLayout;
}

设置文本

java 复制代码
    public void setText(final CharSequence text) {
        CharSequence targetText = text == null ? "" : text;
        if (mLayout != null && TextUtils.equals(targetText, this.mText)) {
            return;
        }
        this.mText = targetText;
        if (!isAttachedToWindow()) {
            mLayout = null;
            mHintLayout = null;
            return;
        }
        if (measureWidthMode == -1 || measureHeightMode == -1) {
            mLayout = null;
            mHintLayout = null;
            requestLayout();
            invalidate();
            return;
        }
        int width = measureWidthMode == MeasureSpec.EXACTLY ? getMeasuredWidth() : ANY_WIDTH;
        mHintLayout = buildTextLayout(text, width);

        int desireWidth = mHintLayout.getWidth() + getPaddingLeft() + getPaddingRight();
        int desireHeight = getTextLayoutHeight(mHintLayout)+ getPaddingTop() + getPaddingBottom();

        if (desireWidth != getWidth() || measureHeightMode != MeasureSpec.EXACTLY && desireHeight != getHeight()) {
            mLayout = null;
            requestLayout();
        } else {
            mLayout = mHintLayout;
            mHintLayout = null;
        }
        invalidate();
    }

完整代码

java 复制代码
public class BoringTextView extends View {

    private static final int ANY_WIDTH = -1;
    private static final String TAG = "BoringTextView";
    private TextPaint mTextPaint;
    private DisplayMetrics mDisplayMetrics;
    private int mContentHeight = 0;
    private int mContentWidth = 0;
    private Layout mLayout;
    private Layout mHintLayout;
    private int mTextColor;
    private ColorStateList mTextColorStateList;
    private CharSequence mText = "";
    private boolean mIncludeFontPadding = false;
    private int measureWidthMode = -1;
    private int measureHeightMode = -1;
    // fixed: mSpacingMult in android 4.4 must be greater 0
    private float mLineSpacingMult = 1.0f;
    private float mLineSpacingAdd = 0.0f;

    public static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();


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

    public BoringTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint(context, attrs, 0, 0);

    }

    public BoringTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint(context, attrs, defStyleAttr, 0);
    }

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

    private void initPaint(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        Resources resources = getResources();
        mDisplayMetrics = resources.getDisplayMetrics();
        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextSize(sp2px(12));
        mTextPaint.density = mDisplayMetrics.density;
        mTextColorStateList = ColorStateList.valueOf(Color.GRAY);

        if (attrs != null) {
            int[] attrset = {
                    //注意顺序,从大到小,否则无法正常获取
                    android.R.attr.textSize,
                    android.R.attr.textColor,
                    android.R.attr.text,
                    android.R.attr.includeFontPadding
            };
            TypedArray attributes = context.obtainStyledAttributes(attrs, attrset, defStyleAttr, defStyleRes);
            int length = attributes.getIndexCount();
            for (int i = 0; i < length; i++) {
                int attrIndex = attributes.getIndex(i);
                int attrItem = attrset[attrIndex];
                switch (attrItem) {
                    case android.R.attr.text:
                        CharSequence text = attributes.getText(attrIndex);
                        setText(text);
                        break;
                    case android.R.attr.textColor:
                        //涉及到ColorStateList ,暂不做支持动态切换
                        ColorStateList colorStateList = attributes.getColorStateList(attrIndex);
                        if (colorStateList != null) {
                            mTextColorStateList = colorStateList;
                        }
                        break;
                    case android.R.attr.textSize:
                        int dimensionPixelSize = attributes.getDimensionPixelSize(attrIndex, (int) sp2px(12));
                        mTextPaint.setTextSize(dimensionPixelSize);
                        break;
                    case android.R.attr.includeFontPadding:
                        mIncludeFontPadding = attributes.getBoolean(attrIndex, false);
                        break;

                }
            }
            attributes.recycle();
        }

        setTextColor(mTextColorStateList);

    }

    public void setTypeface(Typeface tf, int style) {
        if (style > 0) {
            if (tf == null) {
                tf = Typeface.defaultFromStyle(style);
            } else {
                tf = Typeface.create(tf, style);
            }

            setTypeface(tf);
            // now compute what (if any) algorithmic styling is needed
            int typefaceStyle = tf != null ? tf.getStyle() : 0;
            int styleFlags = style & ~typefaceStyle;
            mTextPaint.setFakeBoldText((styleFlags & Typeface.BOLD) != 0);
            mTextPaint.setTextSkewX((styleFlags & Typeface.ITALIC) != 0 ? -0.25f : 0);
        } else {
            mTextPaint.setFakeBoldText(false);
            mTextPaint.setTextSkewX(0);
            setTypeface(tf);
        }
    }

    public void setTypeface(Typeface tf) {
        if (mTextPaint.getTypeface() != tf) {
            mTextPaint.setTypeface(tf);
            if (mLayout != null) {
                requestLayout();
                invalidate();
            }
        }
    }

    public Typeface getTypeface() {
        if (mTextPaint != null) {
            return mTextPaint.getTypeface();
        }
        return null;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int defaultWidth = MeasureSpec.getSize(widthMeasureSpec);
        if (measureWidthMode != -1 && measureWidthMode != widthMode) {
            mHintLayout = null;
        }
        int widthSize = defaultWidth;

        if (widthMode != MeasureSpec.EXACTLY) {
            if (mHintLayout == null) {
                //在setText时已经计算过了,直接复用mHintLayout
                mLayout = buildTextLayout(this.mText, ANY_WIDTH);
            } else {
                mLayout = mHintLayout;
            }
            int requestWidth = (getPaddingRight() + getPaddingLeft()) + (mLayout != null ? mLayout.getWidth() : 0);

            if(widthMode == MeasureSpec.AT_MOST){
                widthSize = Math.min(requestWidth,defaultWidth);
            }else {
                widthSize = requestWidth;
            }
        } else {
            if (mHintLayout == null) {
                int contentWidth = (widthSize - (getPaddingRight() + getPaddingLeft()));
                mLayout = buildTextLayout(this.mText, contentWidth);
            } else {
                mLayout = mHintLayout;
            }
        }

        int defaultHeight = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = 0;

        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = Math.min(getTextLayoutHeight(mLayout),defaultHeight);
        } else if (heightMode == MeasureSpec.UNSPECIFIED) {
            int desireHeight = getTextLayoutHeight(mLayout);
            heightSize = (getPaddingTop() + getPaddingBottom()) + desireHeight;
        }

        setMeasuredDimension(widthSize, heightSize);
        Log.i(TAG,"widthSize="+widthSize+", heightSize="+heightSize+",paddingTop="+getPaddingTop()+",paddingBottom="+getPaddingBottom());

        measureHeightMode = heightMode;
        measureWidthMode = widthMode;

        mHintLayout = null;
    }


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        measureWidthMode = -1;
        measureHeightMode = -1;
    }

    @Override
    public void setLayoutParams(ViewGroup.LayoutParams params) {
        measureWidthMode = -1;
        measureHeightMode = -1;
        super.setLayoutParams(params);
    }

    private int getTextLayoutHeight(Layout layout) {
        if(layout == null) {
            return 0;
        }
        int desireHeight = 0;
        desireHeight = layout.getHeight();
        if(desireHeight <= 0){
            int minTextLayoutLines = Math.min(layout.getLineCount(), 1);
            desireHeight =  Math.round(mTextPaint.getFontMetricsInt(null)* mLineSpacingMult + mLineSpacingAdd) * minTextLayoutLines;
        }
        return desireHeight;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mContentHeight = (h - getPaddingTop() - getPaddingBottom());
        mContentWidth = (w - getPaddingLeft() - getPaddingRight());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float strokeWidth = mTextPaint.getStrokeWidth() * 2;
        if (mContentWidth <= strokeWidth || mContentHeight <= strokeWidth) {
            return;
        }
        int save = canvas.save();

        if (mLayout != null) {
            int verticalHeight = getPaddingTop() + getPaddingBottom() + getTextLayoutHeight(mLayout);
            float offset = (getHeight() - verticalHeight) >> 1;
            if(offset < 0){
                offset = 0;
            }
            canvas.translate(getPaddingLeft(), getPaddingTop() + offset);
            mLayout.draw(canvas);
        }
        canvas.restoreToCount(save);
    }

    public void setText(final CharSequence text) {
        CharSequence targetText = text == null ? "" : text;
        if (mLayout != null && TextUtils.equals(targetText, this.mText)) {
            return;
        }
        this.mText = targetText;
        if (!isAttachedToWindow()) {
            mLayout = null;
            mHintLayout = null;
            return;
        }
        if (measureWidthMode == -1 || measureHeightMode == -1) {
            mLayout = null;
            mHintLayout = null;
            requestLayout();
            invalidate();
            return;
        }
        int width = measureWidthMode == MeasureSpec.EXACTLY ? getMeasuredWidth() : ANY_WIDTH;
        mHintLayout = buildTextLayout(text, width);

        int desireWidth = mHintLayout.getWidth() + getPaddingLeft() + getPaddingRight();
        int desireHeight = getTextLayoutHeight(mHintLayout)+ getPaddingTop() + getPaddingBottom();

        if (desireWidth != getWidth() || measureHeightMode != MeasureSpec.EXACTLY && desireHeight != getHeight()) {
            mLayout = null;
            requestLayout();
        } else {
            mLayout = mHintLayout;
            mHintLayout = null;
        }
        invalidate();
    }

    protected Layout buildTextLayout(CharSequence text, int wantWidth) {
        // fixed: Chinese word is not boring in android 4.4,在Android 4.4长度可能小于measureText测量的
        float measureTextWidth = mTextPaint.measureText(text, 0, text.length());
        BoringLayout.Metrics boring = BoringLayout.isBoring(text, mTextPaint,UNKNOWN_BORING);


        float lineSpaceMult = mLineSpacingMult;
        if (lineSpaceMult < 1F) {
            lineSpaceMult = 1.0f;
        }
        if (boring != null) {
            int outWidth = wantWidth != ANY_WIDTH ? wantWidth : (int) (Math.max(boring.width,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
            return BoringLayout.make(text, mTextPaint,
                    outWidth,
                    Layout.Alignment.ALIGN_NORMAL,
                    0,
                    mLineSpacingAdd,
                    boring,
                    mIncludeFontPadding);
        }
        //fix Android  4.4  mLineSpacingMult 必须大于0
        float desiredWidthForStaticLayout = StaticLayout.getDesiredWidth(text, mTextPaint);
        int desiredWidth = (int) (Math.max(desiredWidthForStaticLayout,measureTextWidth) + mLineSpacingMult + mLineSpacingAdd);
        int outWidth = wantWidth != ANY_WIDTH ? wantWidth : desiredWidth;
        StaticLayout staticLayout = new StaticLayout(text,
                mTextPaint,
                outWidth,
                Layout.Alignment.ALIGN_NORMAL,
                lineSpaceMult,
                mLineSpacingAdd,
                mIncludeFontPadding);
        return staticLayout;
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDisplayMetrics);
    }

    public void setIncludeFontPadding(boolean includePad) {
        this.mIncludeFontPadding = includePad;
        mHintLayout = null;
        mLayout = null;
        requestLayout();
        invalidate();
    }

    public void setTextColor(int color) {
        ColorStateList colorStateList = ColorStateList.valueOf(color);
        setTextColor(colorStateList);
    }

    public void setTextColor(ColorStateList colorStateList) {
        if (colorStateList == null) return;
        final int[] drawableState = getDrawableState();
        int forStateColor = colorStateList.getColorForState(drawableState, 0);
        mTextColor = forStateColor;
        mTextColorStateList = colorStateList;
        mTextPaint.setColor(forStateColor);
        postInvalidate();
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        if(mTextColorStateList!=null && mTextColorStateList.isStateful()) {
            setTextColor(mTextColorStateList);
        }
    }


    public int getCurrentTextColor() {
        return mTextColor;
    }

    public void setTextSize(float textSize) {
        mTextPaint.setTextSize(textSize);
    }

    public TextPaint getPaint() {
        return mTextPaint;
    }

    public CharSequence getText() {
        return mText;
    }

    @Override
    public boolean isAttachedToWindow() {
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
            return super.isAttachedToWindow();
        }
        return getWindowToken() != null;
    }
}

犬牙问题:

这种问题的解决方法网上能搜出很多,但是对中文支持最好的得参考下面文章 《关于TextView中换行后对齐问题

其中实现原理是对TextView重写,但是缺点是对英文支持的不够好,不过关系不大,对英文分词即可快速实现。

其核心逻辑是:对最后一行的以外的其他文本行增加文字间距(word space),从而使得看起来犬牙的文本显的规整,但其本身并非是两边对齐。

java 复制代码
    private void drawScaledText(Canvas canvas, int lineStart, String line,
                                float lineWidth) {
        float x = 0;
        if (isFirstLineOfParagraph(lineStart, line)) {
            String blanks = "  ";
            canvas.drawText(blanks, x, mLineY, getPaint());
            float bw = StaticLayout.getDesiredWidth(blanks, getPaint());
            x += bw;

            line = line.substring(3);
        }

        int gapCount = line.length() - 1;
        int i = 0;
        if (line.length() > 2 && line.charAt(0) == 12288
                && line.charAt(1) == 12288) {
            String substring = line.substring(0, 2);
            float cw = StaticLayout.getDesiredWidth(substring, getPaint());
            canvas.drawText(substring, x, mLineY, getPaint());
            x += cw;
            i += 2;
        }

        float d = (mViewWidth - lineWidth) / gapCount;
        for (; i < line.length(); i++) {
            String c = String.valueOf(line.charAt(i));
            float cw = StaticLayout.getDesiredWidth(c, getPaint());
            canvas.drawText(c, x, mLineY, getPaint());
            x += cw + d;
        }
    }

完整代码

java 复制代码
public class TextAlignTextView extends TextView {

    private int mLineY;
    private int mViewWidth;
    public static final String TWO_CHINESE_BLANK = "  ";

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

    @Override
    protected void onLayout(boolean changed, int left, int top, int right,
                            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        TextPaint paint = getPaint();
        paint.setColor(getCurrentTextColor());
        paint.drawableState = getDrawableState();
        mViewWidth = getMeasuredWidth();
        String text = getText().toString();
        mLineY = 0;
        mLineY += getTextSize();
        Layout layout = getLayout();

        // layout.getLayout()在4.4.3出现NullPointerException
        if (layout == null) {
            return;
        }

        Paint.FontMetrics fm = paint.getFontMetrics();

        int textHeight = (int) (Math.ceil(fm.descent - fm.ascent));
        textHeight = (int) (textHeight * layout.getSpacingMultiplier() + layout
                .getSpacingAdd());
        //解决了最后一行文字间距过大的问题
        for (int i = 0; i < layout.getLineCount(); i++) {
            int lineStart = layout.getLineStart(i);
            int lineEnd = layout.getLineEnd(i);
            float width = StaticLayout.getDesiredWidth(text, lineStart,
                    lineEnd, getPaint());
            String line = text.substring(lineStart, lineEnd);

            if(i < layout.getLineCount() - 1) {
                if (needScale(line)) {
                    drawScaledText(canvas, lineStart, line, width);
                } else {
                    canvas.drawText(line, 0, mLineY, paint);
                }
            } else {
                canvas.drawText(line, 0, mLineY, paint);
            }
            mLineY += textHeight;
        }
    }

    private void drawScaledText(Canvas canvas, int lineStart, String line,
                                float lineWidth) {
        float x = 0;
        if (isFirstLineOfParagraph(lineStart, line)) {
            String blanks = "  ";
            canvas.drawText(blanks, x, mLineY, getPaint());
            float bw = StaticLayout.getDesiredWidth(blanks, getPaint());
            x += bw;

            line = line.substring(3);
        }

        int gapCount = line.length() - 1;
        int i = 0;
        if (line.length() > 2 && line.charAt(0) == 12288
                && line.charAt(1) == 12288) {
            String substring = line.substring(0, 2);
            float cw = StaticLayout.getDesiredWidth(substring, getPaint());
            canvas.drawText(substring, x, mLineY, getPaint());
            x += cw;
            i += 2;
        }

        float d = (mViewWidth - lineWidth) / gapCount;
        for (; i < line.length(); i++) {
            String c = String.valueOf(line.charAt(i));
            float cw = StaticLayout.getDesiredWidth(c, getPaint());
            canvas.drawText(c, x, mLineY, getPaint());
            x += cw + d;
        }
    }

    private boolean isFirstLineOfParagraph(int lineStart, String line) {
        return line.length() > 3 && line.charAt(0) == ' '
                && line.charAt(1) == ' ';
    }

    private boolean needScale(String line) {
        if (line == null || line.length() == 0) {
            return false;
        } else {
            return line.charAt(line.length() - 1) != '\n';
        }
    }

}

总结

到这里本篇就结束了,TextView作为Android中最复杂的View组件之一,其中有很多方法的调用也是非公开的,另外其中的Editor也是没有公开的,这显然是造成TextView存在性能问题的原因之一。

本篇这里的优化基本都有线上允许,在播放器中,我们用到了BoringTextView和有跑马灯,有效降低了焦点问题和requestLayout频繁的问题,当然文本的展示并不一定非得用BoringLayout和StaticLayout,也有很多方式可以实现此类优化。文本对齐问题,实际上在一些协议页面使用会获得很好的体验,这里我们就不再赘述了。

相关推荐
xjt_09012 分钟前
浅析Web存储系统
前端
恋猫de小郭15 分钟前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin
foxhuli22940 分钟前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
aqi0044 分钟前
FFmpeg开发笔记(七十七)Android的开源音视频剪辑框架RxFFmpeg
android·ffmpeg·音视频·流媒体
青皮桔1 小时前
CSS实现百分比水柱图
前端·css
影子信息1 小时前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
青阳流月1 小时前
1.vue权衡的艺术
前端·vue.js·开源
样子20181 小时前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿1 小时前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js
孤水寒月2 小时前
给自己网站增加一个免费的AI助手,纯HTML
前端·人工智能·html