Android 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;
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();
}
}
}
@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) {
heightSize = (getPaddingTop() + getPaddingBottom()) + (mLayout != null ? mLayout.getHeight() : 0);
}
setMeasuredDimension(widthSize, heightSize);
measureWidthMode = widthMode;
mHintLayout = null;
}
@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 disireWidth = mHintLayout.getWidth() + getPaddingLeft() + getPaddingRight();
int disireHeight = mHintLayout.getHeight() + getPaddingTop() + getPaddingBottom();
if (disireWidth != getWidth() || disireHeight != getHeight()) {
mLayout = null;
requestLayout();
} else {
mLayout = mHintLayout;
mHintLayout = null;
}
invalidate();
}
protected Layout buildTextLayout(CharSequence text, int wantWidth) {
BoringLayout.Metrics boring = BoringLayout.isBoring(text, mTextPaint);
if (boring != null) {
int outWidth = wantWidth != ANY_WIDTH ? wantWidth : boring.width;
return BoringLayout.make(text, mTextPaint,
outWidth, Layout.Alignment.ALIGN_NORMAL,
0, 0.0f,
boring, mIncludeFontPadding);
}
//下面是兜底邏輯
int desiredWidth = (int) StaticLayout.getDesiredWidth(text, mTextPaint);
int outWidth = wantWidth != ANY_WIDTH ? wantWidth : desiredWidth;
StaticLayout staticLayout = new StaticLayout(text,
mTextPaint,
outWidth,
Layout.Alignment.ALIGN_NORMAL,
0.0f,
0.0f,
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);
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
setTextColor(mTextColor);
}
}