一、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世界 $@");