前言
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,也有很多方式可以实现此类优化。文本对齐问题,实际上在一些协议页面使用会获得很好的体验,这里我们就不再赘述了。