Androd BoringTextView解决setText性能问题

原创
2022/04/22 14:31
阅读数 351

一、Android setText性能优化

在Android系统中,TextView作为最复杂的View组建,自然功能很强,而随之带来的性能问题也很多,尤其是在大多数情况下,调用setText方法回导致requestLayout 和 invalidate,前者对性能影响非常大。事实上,很多情况下并不需要RequestLayout,我们通过测量文字的情况下,只需要调用invalidate即可,当然,TextView复杂的实现,对这种优化极为不利,因此有必要通过自定义方式解决。

这里我们通过StaticLayout和BoringLayout加缓存对setText进行优化

 

二、源码实现 BoringTextView

public class BoringTextView extends View {

    private static final int ANY_WIDTH = -1;
    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;
    // fixed: mSpacingMult in android 4.4 must be greater 0
    private float mSpacingMult = 1.0f;
    private float mSpacingAdd = 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) {
        this(context, attrs, 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 widthSize = MeasureSpec.getSize(widthMeasureSpec);
        if (measureWidthMode != -1 && measureWidthMode != widthMode) {
            mHintLayout = null;
        }
        if (widthMode != MeasureSpec.EXACTLY) {
            if (mHintLayout == null) {
                //在setText时已经计算过了,直接复用mHintLayout
                mLayout = buildTextLayout(this.mText, ANY_WIDTH);
            } else {
                mLayout = mHintLayout;
            }
            widthSize = (getPaddingRight() + getPaddingLeft()) + (mLayout != null ? mLayout.getWidth() : 0);
        } else {
            if (mHintLayout == null) {
                int contentWidth = (widthSize - (getPaddingRight() + getPaddingLeft()));
                mLayout = buildTextLayout(this.mText, contentWidth);
            } else {
                mLayout = mHintLayout;
            }
        }

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

        if (heightMode != MeasureSpec.EXACTLY) {
            int desireHeight = getTextLayoutHeight(mLayout);
            heightSize = (getPaddingTop() + getPaddingBottom()) + desireHeight;
        }
        setMeasuredDimension(widthSize, heightSize);
        ViewGroup.LayoutParams params = getLayoutParams();
        if (params instanceof ViewGroup.LayoutParams) {
            if (measureWidthMode == -1) {
                measureWidthMode = widthMode;
            }
        } else {
            measureWidthMode = widthMode;
        }
        mHintLayout = null;
    }

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

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

    private int getTextLayoutHeight(Layout layout) {
        if(layout==null) {
            return 0;
        }
        int desireHeight = 0;
        desireHeight = layout.getHeight();
        if(desireHeight<=0){
            desireHeight =  Math.round(mTextPaint.getFontMetricsInt(null)*mSpacingMult + mSpacingAdd) * layout.getLineCount();
        }
        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();
        canvas.translate(getPaddingLeft(), getPaddingTop());
        if (mLayout != null) {
            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) {
            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() || 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);
        if (boring != null) {
            int outWidth = wantWidth != ANY_WIDTH ? wantWidth : (int) (Math.max(boring.width,measureTextWidth) + mSpacingMult + mSpacingAdd);
            return BoringLayout.make(text, mTextPaint,
                    outWidth, Layout.Alignment.ALIGN_NORMAL,
                    mSpacingMult, mSpacingAdd,
                    boring, mIncludeFontPadding);
        }
        //下面是兜底逻辑
        float desiredWidthForStaticLayout = StaticLayout.getDesiredWidth(text, mTextPaint);
        int desiredWidth = (int) (Math.max(desiredWidthForStaticLayout,measureTextWidth) + mSpacingMult+mSpacingAdd);
        int outWidth = wantWidth != ANY_WIDTH ? wantWidth : desiredWidth;
        StaticLayout staticLayout = new StaticLayout(text,
                mTextPaint,
                outWidth,
                Layout.Alignment.ALIGN_NORMAL,
                mSpacingMult,
                mSpacingAdd,
                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;
    }
}

三、用法

BoringTextView textView = findViewById(R.id.boringTextView);
textView.setBackgroundColor(Color.MAGENTA);
textView.setTextColor(Color.BLACK);
textView.setText("HelloWorld - 你好,欢迎来到Android世界 $@");
展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部