区块链技术博客
www.b2bchain.cn

重新认识Android的TextView(二)、BoringLayout求职学习资料

本文介绍了重新认识Android的TextView(二)、BoringLayout求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

TextView是android提供的一个文本展示ui控件,同时也是android开发者最先熟悉的Weight组件,可以配合Html和Spannable进行展示文字、展示html、进行高亮处理,还能通过autolink进行email、tel等功能的识别跳转,本篇文章将带你从系统源码的角度彻底搞定TextView的绘制流程。

在上一篇TextView绘制流程中https://xiaozhuanlan.com/topic/2805964713 简要分析了TextView的onMeasure、onLayout、onDraw。在TextView中,有一个贯穿了整个流程的类-Layout,本节主要对Layout进行分析。

重新认识Android的TextView(二)、BoringLayout

Layout主要有三个实现类

  • StaticLayout

StaticLayout在文字被排列布局之后不允许修改。

  • BoringLayout

BoringLayout是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring判断文字是否符合要求。

  • DynamicLayout

DynamicLayout支持在排列布局之后修改文字,修改之后会更新text内容。

BoringLayout

重新认识Android的TextView(二)、BoringLayout
上面是BoringLayout的类依赖图。

BoringLayout#isBoring

如果想要使用BoringLayout方法的话,首先需要调用BoringLayout.isBoring方法,判断是否支持BoringLayout。

/**  * 如果text支持BoringLayout的话,返回Metrics对象,否则返回null  */ public static Metrics isBoring(CharSequence text, TextPaint paint) {     return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null); } public static Metrics isBoring(CharSequence text, TextPaint paint,             TextDirectionHeuristic textDir, Metrics metrics) {     //返回文字长度     final int textLength = text.length();     // hasAnyInterestingChars 方法用来判断文字如果是t、n或者是bidi unicode字符或者代理对unicode字符的话,直接返回true     if (hasAnyInterestingChars(text, textLength)) {         return null;  // There are some interesting characters. Not boring.     }     //判断文字方向是否是从右向左排列     if (textDir != null && textDir.isRtl(text, 0, textLength)) {         return null;  // The heuristic considers the whole text RTL. Not boring.     }     // 是否是Spanned对象     // text中进行样式操作,比如高亮、可点击等操作,都是spanned支持实现     if (text instanceof Spanned) {         Spanned sp = (Spanned) text;         Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);         if (styles.length > 0) {             return null;  // There are some ParagraphStyle spans. Not boring.         }     }     Metrics fm = metrics;     if (fm == null) {         fm = new Metrics();     } else {         fm.reset();     }     //下面会专门介绍TextLine     TextLine line = TextLine.obtain();     line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,              Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,              0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,              0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);     fm.width = (int) Math.ceil(line.metrics(fm));     TextLine.recycle(line);     return fm; }
  • BoringLayout#hasAnyInterestingChars
private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {         final int MAX_BUF_LEN = 500;         final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);         try {             for (int start = 0; start < textLength; start += MAX_BUF_LEN) {                 final int end = Math.min(start + MAX_BUF_LEN, textLength);                  // No need to worry about getting half codepoints, since we consider surrogate code                 // units "interesting" as soon we see one.                 //根据text的类型判断使用对应的类调用getChars实现将字符串复制到目标数组buffer中                 TextUtils.getChars(text, start, end, buffer, 0);                  final int len = end - start;                 for (int i = 0; i < len; i++) {                     final char c = buffer[i];                     //TextUtils.couldAffectRtl(c) 判断如果是bidi算法unicode、代理对unicode字符的话,直接返回true                     if (c == 'n' || c == 't' || TextUtils.couldAffectRtl(c)) {                         return true;                     }                 }             }             return false;         } finally {             TextUtils.recycle(buffer);         }     }

BoringLayout#make

public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,             Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics,             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics,                             includePad, ellipsize, ellipsizedWidth); }
  • BoringLayout#BoringLayout
public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align,             float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad,             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     /*      * It is silly to have to call super() and then replaceWith(),      * but we can't use "this" for the callback until the call to      * super() finishes.      */     // 注意outerWidth不能<0,在Layout中有判断,<0抛出IllegalArgumentException     super(source, paint, outerWidth, align, spacingMult, spacingAdd);      boolean trust;      //如果ellipsize == null 或者 ellipsize类型是MARQUEE,则返回文字包括的宽度     if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {         mEllipsizedWidth = outerWidth;         mEllipsizedStart = 0;         mEllipsizedCount = 0;         trust = true;     } else {         // replaceWith方法在Layout类中,只是对构造方法中的参数进行了重新赋值         // TextUtils.ellipsize 用于计算并裁剪,如果文字在限定的宽度内,则直接返回原始文本,如果文本内容超过限制,则根据文字方向和TruncateAt类型判断是否展示省略号。         replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),                     paint, outerWidth, align, spacingMult, spacingAdd);         mEllipsizedWidth = ellipsizedWidth;         trust = false;     }     init(getText(), paint, align, metrics, includePad, trust); }
void init(CharSequence source, TextPaint paint, Alignment align,             BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {     int spacing;      //如果mDirect == null,则使用Layout#draw进行绘制,否则使用canvas.drawText绘制       if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {         mDirect = source.toString();     } else {         mDirect = null;     }      mPaint = paint;      //includePad对应于TextView_includeFontPadding属性     //当设置为false的时候,告诉计算会去除top和ascent之间的间距,以及bottom和descent之间的间距,所以字体的整体占用空间会比设置为true的时候较小     //TextView_includeFontPadding属性默认值为true     if (includePad) {         //需要注意的话baseline上方的值为负数,baseline下方的值为正数,所以mertics.bottom - mertics.top实际上就是bottom和top之间的距离,下面的descent和ascent是一样的道理         spacing = metrics.bottom - metrics.top;         mDesc = metrics.bottom;     } else {         spacing = metrics.descent - metrics.ascent;         mDesc = metrics.descent;     }     mBottom = spacing;     //baseline = mBottom - mDesc,mBottom计算出来的值实际上就是文字的高度,减去mdesc的高度,刚好就是baseline高度      //这里计算并返回一个最大的行宽度     if (trustWidth) {         mMax = metrics.width;     } else {         /*          * If we have ellipsized, we have to actually calculate the          * width because the width that was passed in was for the          * full text, not the ellipsized form.          */         //TextLine比较复杂,后续专门介绍         TextLine line = TextLine.obtain();         line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,                  Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,                  mEllipsizedStart, mEllipsizedStart + mEllipsizedCount);         mMax = (int) Math.ceil(line.metrics(null));         TextLine.recycle(line);     }     if (includePad) {         mTopPadding = metrics.top - metrics.ascent;         mBottomPadding = metrics.bottom - metrics.descent;     } }

BoringLayout#replaceOrMake

BoringLayout#replaceOrMake方法和make方法在大体实现上差不多,区别就是make是构建出来一个新的BoringLayout,而replaceOrMake是进行复用

public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth,             Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     boolean trust;      if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {         replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd);          mEllipsizedWidth = outerWidth;         mEllipsizedStart = 0;         mEllipsizedCount = 0;         trust = true;     } else {         //该方法是在Layout中实现,主要是对使用的变量进行一个重新赋值。         //TextUtils.ellipsize 用于计算并裁剪,如果文字在限定的宽度内,则直接返回原始文本,如果文本内容超过限制,则根据文字方向和TruncateAt类型判断是否展示省略号。         replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),                 paint, outerWidth, align, spacingMult, spacingAdd);          mEllipsizedWidth = ellipsizedWidth;         trust = false;     }      init(getText(), paint, align, metrics, includePad, trust);     return this; }
  • layout#replaceWith
void replaceWith(CharSequence text, TextPaint paint,                               int width, Alignment align,                               float spacingmult, float spacingadd) {     if (width< 0) {         throw new IllegalArgumentException("Layout: " + width + " < 0");     }      mText = text;     mPaint = paint;     mWidth = width;     mAlignment = align;     mSpacingMult = spacingmult;     mSpacingAdd = spacingadd;     mSpannedText = text instanceof Spanned; }

BoringLayout#draw

在上一篇文章-TextView绘制流程中有说过,TextView的onDraw中如果是非可编辑文字的话,使用layout#draw方法。那么看一下BoringLayout的draw方法实现。

public void draw(Canvas c, Path highlight, Paint highlightpaint,                  int cursorOffset) {     //在init中,有一个判断用来控制mDirect的值     //source instanceof String && align == Layout.Alignment.ALIGN_NORMAL     if (mDirect != null && highlight == null) {         //mbottom-mdesc用来计算绘制线的baselineY位置         c.drawText(mDirect, 0, mBottom - mDesc, mPaint);     } else {         //调用Layout#draw方法绘制         super.draw(c, highlight, highlightpaint, cursorOffset);     } }

mBottom - mDesc计算baselineY,这块牵扯到Metrics相关的知识,对于Metrics的相关知识,可以参考canvas.drawText理解和FontMetrics文字测量。

TextLine

之前在介绍BoringLayout的时候有提到TextLine这个类,接下来将介绍它。

/**  * Represents a line of styled text, for measuring in visual order and  * for rendering.  *  * <p>Get a new instance using obtain(), and when finished with it, return it  * to the pool using recycle().  *  * <p>Call set to prepare the instance for use, then either draw, measure,  * metrics, or caretToLeftRightOf.  *  *  */ public class TextLine{ ... }

简单回顾下,BoringLayout中使用的地方。

...  TextLine line = TextLine.obtain();  line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,          Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,          0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,          0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);  fm.width = (int) Math.ceil(line.metrics(fm));  TextLine.recycle(line); ...

其实TextLine本身使用到的方法也就这几个,下面挨个做个介绍。

TextLine#obtain

从TextLine的介绍中可以知道,这个方法是用来获取一个TextLine对象。

//创建一个大小为3个TextLine共享数据集合 private static final TextLine[] sCached = new TextLine[3]; ...略若干代码 public static TextLine obtain() {     TextLine tl;     synchronized (sCached) {         for (int i = sCached.length; --i >= 0;) {             //如果集合中有可用的TextLine,则取出使用,使用后并从集合中移除。             if (sCached[i] != null) {                 tl = sCached[i];                 //相当于从集合中移除                 sCached[i] = null;                 return tl;             }         }     }     //如果sCached中没有可用的TextLine,则创建一个新的。     tl = new TextLine();     return tl; }

TextLine#recycle

recycle是与TextLine#obtain方法结对出现的方法

/**  * Puts a TextLine back into the shared pool. Do not use this TextLine once  * it has been returned.  * tl the textLine  * null, as a convenience from clearing references to the provided  * TextLine  * 将TextLine对象设置回sCached共享池中  */ public static TextLine recycle(TextLine tl) {    tl.mText = null;    tl.mPaint = null;    tl.mDirections = null;    tl.mSpanned = null;    tl.mTabs = null;    tl.mChars = null;    tl.mComputed = null;     //这三个都是SpanSet的实现,而SpanSet的作用是将指定范型类型的span对象从Spanned类型数据中拆分出来保存在数组中。可通过getNextTransition方法进行访问。    //private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =    //         new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);    //MetricAffectingSpan是牵扯到文字的宽、高变化的    tl.mMetricAffectingSpanSpanSet.recycle();    //private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =    //         new SpanSet<CharacterStyle>(CharacterStyle.class);    //CharacterStyle从名称就可以看出来是和样式相关的    tl.mCharacterStyleSpanSet.recycle();    //private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =    //         new SpanSet<ReplacementSpan>(ReplacementSpan.class);    //ReplacementSpan从名称可以看出来是支持内容替换的    tl.mReplacementSpanSpanSet.recycle();     synchronized(sCached) {        for (int i = 0; i < sCached.length; ++i) {            //因为之前obtain中,使用的时候会做移除,所以这边,没有共享池还有空位,则丢到共享池中。            if (sCached[i] == null) {                sCached[i] = tl;                break;            }        }    }    return null; }

TextLine#set

/**  * Initializes a TextLine and prepares it for use.  * 主要是用来TextLine的初始化操作  *  * paint the base paint for the line  * text the text, can be Styled  * start the start of the line relative to the text  * limit the limit of the line relative to the text  * dir the paragraph direction of this line  * directions the directions information of this line  * hasTabs true if the line might contain tabs  * tabStops the tabStops. Can be null  * ellipsisStart the start of the ellipsis relative to the line  * ellipsisEnd the end of the ellipsis relative to the line. When there  *                    is no ellipsis, this should be equal to ellipsisStart.  */(visibility = VisibleForTesting.Visibility.PACKAGE) public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,Directions directions, boolean hasTabs, TabStops tabStops,         int ellipsisStart, int ellipsisEnd) {     mPaint = paint;     mText = text;     mStart = start;     mLen = limit - start;     mDir = dir;     mDirections = directions;     if (mDirections == null) {         throw new IllegalArgumentException("Directions cannot be null");     }     mHasTabs = hasTabs;     mSpanned = null;      boolean hasReplacement = false;     //判断类型是否支持Spanned     if (text instanceof Spanned) {         //强转为Spanned         mSpanned = (Spanned) text;         //replementSpan是一般使用的span的类型,前面有介绍过spanset是将spanned数据中的所有         //支持的类型的span对象拆出来,将拆出来的span对象、span开始位置、span结束位         //置、spanFlags分别保存在数组中。spanset提供了getNextTransition方法用于访问         //spanset中保存的数据。         mReplacementSpanSpanSet.init(mSpanned, start, limit);         //numberOfSpans方法用于获取总共拆分了多少个span对象。         hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;     }      mComputed = null;     //PrecomputedText是在android p中提供的一个新的api,PrecomputedTextCompat能够     //使 app 可以事先甚至在后台线程中执行文本布局最耗费时间的部分工作,以缓存布局结果,     //并返回宝贵的测量数据。      //更多关于PrecomputedText可以参考文章:     //https://blog.csdn.net/ecjtuhq/article/details/104366104     if (text instanceof PrecomputedText) {         // Here, no need to check line break strategy or hyphenation frequency since there is no         // line break concept here.         mComputed = (PrecomputedText) text;         if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {             mComputed = null;         }     }      mCharsValid = hasReplacement;     //如果text类型是Spanned && spanset的大小>0,则mCharsValid == true     if (mCharsValid) {         if (mChars == null || mChars.length < mLen) {             mChars = ArrayUtils.newUnpaddedCharArray(mLen);         }         //将text文本拆分为char数据,并赋值到目标mChars中         TextUtils.getChars(text, start, limit, mChars, 0);         //理论上能进入到这里,应该为true         if (hasReplacement) {             // Handle these all at once so we don't have to do it as we go.             // Replace the first character of each replacement run with the             // object-replacement character and the remainder with zero width             // non-break space aka BOM.  Cursor movement code skips these             // zero-width characters.             //用对象替换字符替换每个替换运行的第一个字符,             //并用零宽度不间断空格(又称为BOM)替换其余字符             char[] chars = mChars;             for (int i = start, inext; i < limit; i = inext) {                 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);                 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)                         && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {                     // transition into a span                     chars[i - start] = 'ufffc';                     for (int j = i - start + 1, e = inext - start; j < e; ++j) {                         chars[j] = 'ufeff'; // used as ZWNBS, marks positions to skip                     }                 }             }         }     }     mTabs = tabStops;     mAddedWidthForJustify = 0;     mIsJustifying = false;     //省略号开始和结束位置     mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;     mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0; }

到这里boringLayout讲完了,后续文章会陆续说明StaticLayoutDynamicLayout

TextView是android提供的一个文本展示ui控件,同时也是android开发者最先熟悉的Weight组件,可以配合Html和Spannable进行展示文字、展示html、进行高亮处理,还能通过autolink进行email、tel等功能的识别跳转,本篇文章将带你从系统源码的角度彻底搞定TextView的绘制流程。

在上一篇TextView绘制流程中https://xiaozhuanlan.com/topic/2805964713 简要分析了TextView的onMeasure、onLayout、onDraw。在TextView中,有一个贯穿了整个流程的类-Layout,本节主要对Layout进行分析。

重新认识Android的TextView(二)、BoringLayout

Layout主要有三个实现类

  • StaticLayout

StaticLayout在文字被排列布局之后不允许修改。

  • BoringLayout

BoringLayout是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring判断文字是否符合要求。

  • DynamicLayout

DynamicLayout支持在排列布局之后修改文字,修改之后会更新text内容。

BoringLayout

重新认识Android的TextView(二)、BoringLayout
上面是BoringLayout的类依赖图。

BoringLayout#isBoring

如果想要使用BoringLayout方法的话,首先需要调用BoringLayout.isBoring方法,判断是否支持BoringLayout。

/**  * 如果text支持BoringLayout的话,返回Metrics对象,否则返回null  */ public static Metrics isBoring(CharSequence text, TextPaint paint) {     return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null); } public static Metrics isBoring(CharSequence text, TextPaint paint,             TextDirectionHeuristic textDir, Metrics metrics) {     //返回文字长度     final int textLength = text.length();     // hasAnyInterestingChars 方法用来判断文字如果是t、n或者是bidi unicode字符或者代理对unicode字符的话,直接返回true     if (hasAnyInterestingChars(text, textLength)) {         return null;  // There are some interesting characters. Not boring.     }     //判断文字方向是否是从右向左排列     if (textDir != null && textDir.isRtl(text, 0, textLength)) {         return null;  // The heuristic considers the whole text RTL. Not boring.     }     // 是否是Spanned对象     // text中进行样式操作,比如高亮、可点击等操作,都是spanned支持实现     if (text instanceof Spanned) {         Spanned sp = (Spanned) text;         Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);         if (styles.length > 0) {             return null;  // There are some ParagraphStyle spans. Not boring.         }     }     Metrics fm = metrics;     if (fm == null) {         fm = new Metrics();     } else {         fm.reset();     }     //下面会专门介绍TextLine     TextLine line = TextLine.obtain();     line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,              Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,              0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,              0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);     fm.width = (int) Math.ceil(line.metrics(fm));     TextLine.recycle(line);     return fm; }
  • BoringLayout#hasAnyInterestingChars
private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {         final int MAX_BUF_LEN = 500;         final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);         try {             for (int start = 0; start < textLength; start += MAX_BUF_LEN) {                 final int end = Math.min(start + MAX_BUF_LEN, textLength);                  // No need to worry about getting half codepoints, since we consider surrogate code                 // units "interesting" as soon we see one.                 //根据text的类型判断使用对应的类调用getChars实现将字符串复制到目标数组buffer中                 TextUtils.getChars(text, start, end, buffer, 0);                  final int len = end - start;                 for (int i = 0; i < len; i++) {                     final char c = buffer[i];                     //TextUtils.couldAffectRtl(c) 判断如果是bidi算法unicode、代理对unicode字符的话,直接返回true                     if (c == 'n' || c == 't' || TextUtils.couldAffectRtl(c)) {                         return true;                     }                 }             }             return false;         } finally {             TextUtils.recycle(buffer);         }     }

BoringLayout#make

public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,             Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics,             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics,                             includePad, ellipsize, ellipsizedWidth); }
  • BoringLayout#BoringLayout
public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align,             float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad,             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     /*      * It is silly to have to call super() and then replaceWith(),      * but we can't use "this" for the callback until the call to      * super() finishes.      */     // 注意outerWidth不能<0,在Layout中有判断,<0抛出IllegalArgumentException     super(source, paint, outerWidth, align, spacingMult, spacingAdd);      boolean trust;      //如果ellipsize == null 或者 ellipsize类型是MARQUEE,则返回文字包括的宽度     if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {         mEllipsizedWidth = outerWidth;         mEllipsizedStart = 0;         mEllipsizedCount = 0;         trust = true;     } else {         // replaceWith方法在Layout类中,只是对构造方法中的参数进行了重新赋值         // TextUtils.ellipsize 用于计算并裁剪,如果文字在限定的宽度内,则直接返回原始文本,如果文本内容超过限制,则根据文字方向和TruncateAt类型判断是否展示省略号。         replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),                     paint, outerWidth, align, spacingMult, spacingAdd);         mEllipsizedWidth = ellipsizedWidth;         trust = false;     }     init(getText(), paint, align, metrics, includePad, trust); }
void init(CharSequence source, TextPaint paint, Alignment align,             BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {     int spacing;      //如果mDirect == null,则使用Layout#draw进行绘制,否则使用canvas.drawText绘制       if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {         mDirect = source.toString();     } else {         mDirect = null;     }      mPaint = paint;      //includePad对应于TextView_includeFontPadding属性     //当设置为false的时候,告诉计算会去除top和ascent之间的间距,以及bottom和descent之间的间距,所以字体的整体占用空间会比设置为true的时候较小     //TextView_includeFontPadding属性默认值为true     if (includePad) {         //需要注意的话baseline上方的值为负数,baseline下方的值为正数,所以mertics.bottom - mertics.top实际上就是bottom和top之间的距离,下面的descent和ascent是一样的道理         spacing = metrics.bottom - metrics.top;         mDesc = metrics.bottom;     } else {         spacing = metrics.descent - metrics.ascent;         mDesc = metrics.descent;     }     mBottom = spacing;     //baseline = mBottom - mDesc,mBottom计算出来的值实际上就是文字的高度,减去mdesc的高度,刚好就是baseline高度      //这里计算并返回一个最大的行宽度     if (trustWidth) {         mMax = metrics.width;     } else {         /*          * If we have ellipsized, we have to actually calculate the          * width because the width that was passed in was for the          * full text, not the ellipsized form.          */         //TextLine比较复杂,后续专门介绍         TextLine line = TextLine.obtain();         line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,                  Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,                  mEllipsizedStart, mEllipsizedStart + mEllipsizedCount);         mMax = (int) Math.ceil(line.metrics(null));         TextLine.recycle(line);     }     if (includePad) {         mTopPadding = metrics.top - metrics.ascent;         mBottomPadding = metrics.bottom - metrics.descent;     } }

BoringLayout#replaceOrMake

BoringLayout#replaceOrMake方法和make方法在大体实现上差不多,区别就是make是构建出来一个新的BoringLayout,而replaceOrMake是进行复用

public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth,             Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     boolean trust;      if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {         replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd);          mEllipsizedWidth = outerWidth;         mEllipsizedStart = 0;         mEllipsizedCount = 0;         trust = true;     } else {         //该方法是在Layout中实现,主要是对使用的变量进行一个重新赋值。         //TextUtils.ellipsize 用于计算并裁剪,如果文字在限定的宽度内,则直接返回原始文本,如果文本内容超过限制,则根据文字方向和TruncateAt类型判断是否展示省略号。         replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),                 paint, outerWidth, align, spacingMult, spacingAdd);          mEllipsizedWidth = ellipsizedWidth;         trust = false;     }      init(getText(), paint, align, metrics, includePad, trust);     return this; }
  • layout#replaceWith
void replaceWith(CharSequence text, TextPaint paint,                               int width, Alignment align,                               float spacingmult, float spacingadd) {     if (width< 0) {         throw new IllegalArgumentException("Layout: " + width + " < 0");     }      mText = text;     mPaint = paint;     mWidth = width;     mAlignment = align;     mSpacingMult = spacingmult;     mSpacingAdd = spacingadd;     mSpannedText = text instanceof Spanned; }

BoringLayout#draw

在上一篇文章-TextView绘制流程中有说过,TextView的onDraw中如果是非可编辑文字的话,使用layout#draw方法。那么看一下BoringLayout的draw方法实现。

public void draw(Canvas c, Path highlight, Paint highlightpaint,                  int cursorOffset) {     //在init中,有一个判断用来控制mDirect的值     //source instanceof String && align == Layout.Alignment.ALIGN_NORMAL     if (mDirect != null && highlight == null) {         //mbottom-mdesc用来计算绘制线的baselineY位置         c.drawText(mDirect, 0, mBottom - mDesc, mPaint);     } else {         //调用Layout#draw方法绘制         super.draw(c, highlight, highlightpaint, cursorOffset);     } }

mBottom - mDesc计算baselineY,这块牵扯到Metrics相关的知识,对于Metrics的相关知识,可以参考canvas.drawText理解和FontMetrics文字测量。

TextLine

之前在介绍BoringLayout的时候有提到TextLine这个类,接下来将介绍它。

/**  * Represents a line of styled text, for measuring in visual order and  * for rendering.  *  * <p>Get a new instance using obtain(), and when finished with it, return it  * to the pool using recycle().  *  * <p>Call set to prepare the instance for use, then either draw, measure,  * metrics, or caretToLeftRightOf.  *  *  */ public class TextLine{ ... }

简单回顾下,BoringLayout中使用的地方。

...  TextLine line = TextLine.obtain();  line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,          Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,          0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,          0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);  fm.width = (int) Math.ceil(line.metrics(fm));  TextLine.recycle(line); ...

其实TextLine本身使用到的方法也就这几个,下面挨个做个介绍。

TextLine#obtain

从TextLine的介绍中可以知道,这个方法是用来获取一个TextLine对象。

//创建一个大小为3个TextLine共享数据集合 private static final TextLine[] sCached = new TextLine[3]; ...略若干代码 public static TextLine obtain() {     TextLine tl;     synchronized (sCached) {         for (int i = sCached.length; --i >= 0;) {             //如果集合中有可用的TextLine,则取出使用,使用后并从集合中移除。             if (sCached[i] != null) {                 tl = sCached[i];                 //相当于从集合中移除                 sCached[i] = null;                 return tl;             }         }     }     //如果sCached中没有可用的TextLine,则创建一个新的。     tl = new TextLine();     return tl; }

TextLine#recycle

recycle是与TextLine#obtain方法结对出现的方法

/**  * Puts a TextLine back into the shared pool. Do not use this TextLine once  * it has been returned.  * tl the textLine  * null, as a convenience from clearing references to the provided  * TextLine  * 将TextLine对象设置回sCached共享池中  */ public static TextLine recycle(TextLine tl) {    tl.mText = null;    tl.mPaint = null;    tl.mDirections = null;    tl.mSpanned = null;    tl.mTabs = null;    tl.mChars = null;    tl.mComputed = null;     //这三个都是SpanSet的实现,而SpanSet的作用是将指定范型类型的span对象从Spanned类型数据中拆分出来保存在数组中。可通过getNextTransition方法进行访问。    //private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =    //         new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);    //MetricAffectingSpan是牵扯到文字的宽、高变化的    tl.mMetricAffectingSpanSpanSet.recycle();    //private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =    //         new SpanSet<CharacterStyle>(CharacterStyle.class);    //CharacterStyle从名称就可以看出来是和样式相关的    tl.mCharacterStyleSpanSet.recycle();    //private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =    //         new SpanSet<ReplacementSpan>(ReplacementSpan.class);    //ReplacementSpan从名称可以看出来是支持内容替换的    tl.mReplacementSpanSpanSet.recycle();     synchronized(sCached) {        for (int i = 0; i < sCached.length; ++i) {            //因为之前obtain中,使用的时候会做移除,所以这边,没有共享池还有空位,则丢到共享池中。            if (sCached[i] == null) {                sCached[i] = tl;                break;            }        }    }    return null; }

TextLine#set

/**  * Initializes a TextLine and prepares it for use.  * 主要是用来TextLine的初始化操作  *  * paint the base paint for the line  * text the text, can be Styled  * start the start of the line relative to the text  * limit the limit of the line relative to the text  * dir the paragraph direction of this line  * directions the directions information of this line  * hasTabs true if the line might contain tabs  * tabStops the tabStops. Can be null  * ellipsisStart the start of the ellipsis relative to the line  * ellipsisEnd the end of the ellipsis relative to the line. When there  *                    is no ellipsis, this should be equal to ellipsisStart.  */(visibility = VisibleForTesting.Visibility.PACKAGE) public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,Directions directions, boolean hasTabs, TabStops tabStops,         int ellipsisStart, int ellipsisEnd) {     mPaint = paint;     mText = text;     mStart = start;     mLen = limit - start;     mDir = dir;     mDirections = directions;     if (mDirections == null) {         throw new IllegalArgumentException("Directions cannot be null");     }     mHasTabs = hasTabs;     mSpanned = null;      boolean hasReplacement = false;     //判断类型是否支持Spanned     if (text instanceof Spanned) {         //强转为Spanned         mSpanned = (Spanned) text;         //replementSpan是一般使用的span的类型,前面有介绍过spanset是将spanned数据中的所有         //支持的类型的span对象拆出来,将拆出来的span对象、span开始位置、span结束位         //置、spanFlags分别保存在数组中。spanset提供了getNextTransition方法用于访问         //spanset中保存的数据。         mReplacementSpanSpanSet.init(mSpanned, start, limit);         //numberOfSpans方法用于获取总共拆分了多少个span对象。         hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;     }      mComputed = null;     //PrecomputedText是在android p中提供的一个新的api,PrecomputedTextCompat能够     //使 app 可以事先甚至在后台线程中执行文本布局最耗费时间的部分工作,以缓存布局结果,     //并返回宝贵的测量数据。      //更多关于PrecomputedText可以参考文章:     //https://blog.csdn.net/ecjtuhq/article/details/104366104     if (text instanceof PrecomputedText) {         // Here, no need to check line break strategy or hyphenation frequency since there is no         // line break concept here.         mComputed = (PrecomputedText) text;         if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {             mComputed = null;         }     }      mCharsValid = hasReplacement;     //如果text类型是Spanned && spanset的大小>0,则mCharsValid == true     if (mCharsValid) {         if (mChars == null || mChars.length < mLen) {             mChars = ArrayUtils.newUnpaddedCharArray(mLen);         }         //将text文本拆分为char数据,并赋值到目标mChars中         TextUtils.getChars(text, start, limit, mChars, 0);         //理论上能进入到这里,应该为true         if (hasReplacement) {             // Handle these all at once so we don't have to do it as we go.             // Replace the first character of each replacement run with the             // object-replacement character and the remainder with zero width             // non-break space aka BOM.  Cursor movement code skips these             // zero-width characters.             //用对象替换字符替换每个替换运行的第一个字符,             //并用零宽度不间断空格(又称为BOM)替换其余字符             char[] chars = mChars;             for (int i = start, inext; i < limit; i = inext) {                 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);                 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)                         && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {                     // transition into a span                     chars[i - start] = 'ufffc';                     for (int j = i - start + 1, e = inext - start; j < e; ++j) {                         chars[j] = 'ufeff'; // used as ZWNBS, marks positions to skip                     }                 }             }         }     }     mTabs = tabStops;     mAddedWidthForJustify = 0;     mIsJustifying = false;     //省略号开始和结束位置     mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;     mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0; }

到这里boringLayout讲完了,后续文章会陆续说明StaticLayoutDynamicLayout

TextView是android提供的一个文本展示ui控件,同时也是android开发者最先熟悉的Weight组件,可以配合Html和Spannable进行展示文字、展示html、进行高亮处理,还能通过autolink进行email、tel等功能的识别跳转,本篇文章将带你从系统源码的角度彻底搞定TextView的绘制流程。

在上一篇TextView绘制流程中https://xiaozhuanlan.com/topic/2805964713 简要分析了TextView的onMeasure、onLayout、onDraw。在TextView中,有一个贯穿了整个流程的类-Layout,本节主要对Layout进行分析。

重新认识Android的TextView(二)、BoringLayout

Layout主要有三个实现类

  • StaticLayout

StaticLayout在文字被排列布局之后不允许修改。

  • BoringLayout

BoringLayout是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring判断文字是否符合要求。

  • DynamicLayout

DynamicLayout支持在排列布局之后修改文字,修改之后会更新text内容。

BoringLayout

重新认识Android的TextView(二)、BoringLayout
上面是BoringLayout的类依赖图。

BoringLayout#isBoring

如果想要使用BoringLayout方法的话,首先需要调用BoringLayout.isBoring方法,判断是否支持BoringLayout。

/**  * 如果text支持BoringLayout的话,返回Metrics对象,否则返回null  */ public static Metrics isBoring(CharSequence text, TextPaint paint) {     return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null); } public static Metrics isBoring(CharSequence text, TextPaint paint,             TextDirectionHeuristic textDir, Metrics metrics) {     //返回文字长度     final int textLength = text.length();     // hasAnyInterestingChars 方法用来判断文字如果是t、n或者是bidi unicode字符或者代理对unicode字符的话,直接返回true     if (hasAnyInterestingChars(text, textLength)) {         return null;  // There are some interesting characters. Not boring.     }     //判断文字方向是否是从右向左排列     if (textDir != null && textDir.isRtl(text, 0, textLength)) {         return null;  // The heuristic considers the whole text RTL. Not boring.     }     // 是否是Spanned对象     // text中进行样式操作,比如高亮、可点击等操作,都是spanned支持实现     if (text instanceof Spanned) {         Spanned sp = (Spanned) text;         Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);         if (styles.length > 0) {             return null;  // There are some ParagraphStyle spans. Not boring.         }     }     Metrics fm = metrics;     if (fm == null) {         fm = new Metrics();     } else {         fm.reset();     }     //下面会专门介绍TextLine     TextLine line = TextLine.obtain();     line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,              Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,              0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,              0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);     fm.width = (int) Math.ceil(line.metrics(fm));     TextLine.recycle(line);     return fm; }
  • BoringLayout#hasAnyInterestingChars
private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {         final int MAX_BUF_LEN = 500;         final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);         try {             for (int start = 0; start < textLength; start += MAX_BUF_LEN) {                 final int end = Math.min(start + MAX_BUF_LEN, textLength);                  // No need to worry about getting half codepoints, since we consider surrogate code                 // units "interesting" as soon we see one.                 //根据text的类型判断使用对应的类调用getChars实现将字符串复制到目标数组buffer中                 TextUtils.getChars(text, start, end, buffer, 0);                  final int len = end - start;                 for (int i = 0; i < len; i++) {                     final char c = buffer[i];                     //TextUtils.couldAffectRtl(c) 判断如果是bidi算法unicode、代理对unicode字符的话,直接返回true                     if (c == 'n' || c == 't' || TextUtils.couldAffectRtl(c)) {                         return true;                     }                 }             }             return false;         } finally {             TextUtils.recycle(buffer);         }     }

BoringLayout#make

public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,             Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics,             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics,                             includePad, ellipsize, ellipsizedWidth); }
  • BoringLayout#BoringLayout
public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align,             float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad,             TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     /*      * It is silly to have to call super() and then replaceWith(),      * but we can't use "this" for the callback until the call to      * super() finishes.      */     // 注意outerWidth不能<0,在Layout中有判断,<0抛出IllegalArgumentException     super(source, paint, outerWidth, align, spacingMult, spacingAdd);      boolean trust;      //如果ellipsize == null 或者 ellipsize类型是MARQUEE,则返回文字包括的宽度     if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {         mEllipsizedWidth = outerWidth;         mEllipsizedStart = 0;         mEllipsizedCount = 0;         trust = true;     } else {         // replaceWith方法在Layout类中,只是对构造方法中的参数进行了重新赋值         // TextUtils.ellipsize 用于计算并裁剪,如果文字在限定的宽度内,则直接返回原始文本,如果文本内容超过限制,则根据文字方向和TruncateAt类型判断是否展示省略号。         replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),                     paint, outerWidth, align, spacingMult, spacingAdd);         mEllipsizedWidth = ellipsizedWidth;         trust = false;     }     init(getText(), paint, align, metrics, includePad, trust); }
void init(CharSequence source, TextPaint paint, Alignment align,             BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth) {     int spacing;      //如果mDirect == null,则使用Layout#draw进行绘制,否则使用canvas.drawText绘制       if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {         mDirect = source.toString();     } else {         mDirect = null;     }      mPaint = paint;      //includePad对应于TextView_includeFontPadding属性     //当设置为false的时候,告诉计算会去除top和ascent之间的间距,以及bottom和descent之间的间距,所以字体的整体占用空间会比设置为true的时候较小     //TextView_includeFontPadding属性默认值为true     if (includePad) {         //需要注意的话baseline上方的值为负数,baseline下方的值为正数,所以mertics.bottom - mertics.top实际上就是bottom和top之间的距离,下面的descent和ascent是一样的道理         spacing = metrics.bottom - metrics.top;         mDesc = metrics.bottom;     } else {         spacing = metrics.descent - metrics.ascent;         mDesc = metrics.descent;     }     mBottom = spacing;     //baseline = mBottom - mDesc,mBottom计算出来的值实际上就是文字的高度,减去mdesc的高度,刚好就是baseline高度      //这里计算并返回一个最大的行宽度     if (trustWidth) {         mMax = metrics.width;     } else {         /*          * If we have ellipsized, we have to actually calculate the          * width because the width that was passed in was for the          * full text, not the ellipsized form.          */         //TextLine比较复杂,后续专门介绍         TextLine line = TextLine.obtain();         line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,                  Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,                  mEllipsizedStart, mEllipsizedStart + mEllipsizedCount);         mMax = (int) Math.ceil(line.metrics(null));         TextLine.recycle(line);     }     if (includePad) {         mTopPadding = metrics.top - metrics.ascent;         mBottomPadding = metrics.bottom - metrics.descent;     } }

BoringLayout#replaceOrMake

BoringLayout#replaceOrMake方法和make方法在大体实现上差不多,区别就是make是构建出来一个新的BoringLayout,而replaceOrMake是进行复用

public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth,             Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,             boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {     boolean trust;      if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {         replaceWith(source, paint, outerWidth, align, spacingMult, spacingAdd);          mEllipsizedWidth = outerWidth;         mEllipsizedStart = 0;         mEllipsizedCount = 0;         trust = true;     } else {         //该方法是在Layout中实现,主要是对使用的变量进行一个重新赋值。         //TextUtils.ellipsize 用于计算并裁剪,如果文字在限定的宽度内,则直接返回原始文本,如果文本内容超过限制,则根据文字方向和TruncateAt类型判断是否展示省略号。         replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),                 paint, outerWidth, align, spacingMult, spacingAdd);          mEllipsizedWidth = ellipsizedWidth;         trust = false;     }      init(getText(), paint, align, metrics, includePad, trust);     return this; }
  • layout#replaceWith
void replaceWith(CharSequence text, TextPaint paint,                               int width, Alignment align,                               float spacingmult, float spacingadd) {     if (width< 0) {         throw new IllegalArgumentException("Layout: " + width + " < 0");     }      mText = text;     mPaint = paint;     mWidth = width;     mAlignment = align;     mSpacingMult = spacingmult;     mSpacingAdd = spacingadd;     mSpannedText = text instanceof Spanned; }

BoringLayout#draw

在上一篇文章-TextView绘制流程中有说过,TextView的onDraw中如果是非可编辑文字的话,使用layout#draw方法。那么看一下BoringLayout的draw方法实现。

public void draw(Canvas c, Path highlight, Paint highlightpaint,                  int cursorOffset) {     //在init中,有一个判断用来控制mDirect的值     //source instanceof String && align == Layout.Alignment.ALIGN_NORMAL     if (mDirect != null && highlight == null) {         //mbottom-mdesc用来计算绘制线的baselineY位置         c.drawText(mDirect, 0, mBottom - mDesc, mPaint);     } else {         //调用Layout#draw方法绘制         super.draw(c, highlight, highlightpaint, cursorOffset);     } }

mBottom - mDesc计算baselineY,这块牵扯到Metrics相关的知识,对于Metrics的相关知识,可以参考canvas.drawText理解和FontMetrics文字测量。

TextLine

之前在介绍BoringLayout的时候有提到TextLine这个类,接下来将介绍它。

/**  * Represents a line of styled text, for measuring in visual order and  * for rendering.  *  * <p>Get a new instance using obtain(), and when finished with it, return it  * to the pool using recycle().  *  * <p>Call set to prepare the instance for use, then either draw, measure,  * metrics, or caretToLeftRightOf.  *  *  */ public class TextLine{ ... }

简单回顾下,BoringLayout中使用的地方。

...  TextLine line = TextLine.obtain();  line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,          Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,          0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,          0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */);  fm.width = (int) Math.ceil(line.metrics(fm));  TextLine.recycle(line); ...

其实TextLine本身使用到的方法也就这几个,下面挨个做个介绍。

TextLine#obtain

从TextLine的介绍中可以知道,这个方法是用来获取一个TextLine对象。

//创建一个大小为3个TextLine共享数据集合 private static final TextLine[] sCached = new TextLine[3]; ...略若干代码 public static TextLine obtain() {     TextLine tl;     synchronized (sCached) {         for (int i = sCached.length; --i >= 0;) {             //如果集合中有可用的TextLine,则取出使用,使用后并从集合中移除。             if (sCached[i] != null) {                 tl = sCached[i];                 //相当于从集合中移除                 sCached[i] = null;                 return tl;             }         }     }     //如果sCached中没有可用的TextLine,则创建一个新的。     tl = new TextLine();     return tl; }

TextLine#recycle

recycle是与TextLine#obtain方法结对出现的方法

/**  * Puts a TextLine back into the shared pool. Do not use this TextLine once  * it has been returned.  * tl the textLine  * null, as a convenience from clearing references to the provided  * TextLine  * 将TextLine对象设置回sCached共享池中  */ public static TextLine recycle(TextLine tl) {    tl.mText = null;    tl.mPaint = null;    tl.mDirections = null;    tl.mSpanned = null;    tl.mTabs = null;    tl.mChars = null;    tl.mComputed = null;     //这三个都是SpanSet的实现,而SpanSet的作用是将指定范型类型的span对象从Spanned类型数据中拆分出来保存在数组中。可通过getNextTransition方法进行访问。    //private final SpanSet<MetricAffectingSpan> mMetricAffectingSpanSpanSet =    //         new SpanSet<MetricAffectingSpan>(MetricAffectingSpan.class);    //MetricAffectingSpan是牵扯到文字的宽、高变化的    tl.mMetricAffectingSpanSpanSet.recycle();    //private final SpanSet<CharacterStyle> mCharacterStyleSpanSet =    //         new SpanSet<CharacterStyle>(CharacterStyle.class);    //CharacterStyle从名称就可以看出来是和样式相关的    tl.mCharacterStyleSpanSet.recycle();    //private final SpanSet<ReplacementSpan> mReplacementSpanSpanSet =    //         new SpanSet<ReplacementSpan>(ReplacementSpan.class);    //ReplacementSpan从名称可以看出来是支持内容替换的    tl.mReplacementSpanSpanSet.recycle();     synchronized(sCached) {        for (int i = 0; i < sCached.length; ++i) {            //因为之前obtain中,使用的时候会做移除,所以这边,没有共享池还有空位,则丢到共享池中。            if (sCached[i] == null) {                sCached[i] = tl;                break;            }        }    }    return null; }

TextLine#set

/**  * Initializes a TextLine and prepares it for use.  * 主要是用来TextLine的初始化操作  *  * paint the base paint for the line  * text the text, can be Styled  * start the start of the line relative to the text  * limit the limit of the line relative to the text  * dir the paragraph direction of this line  * directions the directions information of this line  * hasTabs true if the line might contain tabs  * tabStops the tabStops. Can be null  * ellipsisStart the start of the ellipsis relative to the line  * ellipsisEnd the end of the ellipsis relative to the line. When there  *                    is no ellipsis, this should be equal to ellipsisStart.  */(visibility = VisibleForTesting.Visibility.PACKAGE) public void set(TextPaint paint, CharSequence text, int start, int limit, int dir,Directions directions, boolean hasTabs, TabStops tabStops,         int ellipsisStart, int ellipsisEnd) {     mPaint = paint;     mText = text;     mStart = start;     mLen = limit - start;     mDir = dir;     mDirections = directions;     if (mDirections == null) {         throw new IllegalArgumentException("Directions cannot be null");     }     mHasTabs = hasTabs;     mSpanned = null;      boolean hasReplacement = false;     //判断类型是否支持Spanned     if (text instanceof Spanned) {         //强转为Spanned         mSpanned = (Spanned) text;         //replementSpan是一般使用的span的类型,前面有介绍过spanset是将spanned数据中的所有         //支持的类型的span对象拆出来,将拆出来的span对象、span开始位置、span结束位         //置、spanFlags分别保存在数组中。spanset提供了getNextTransition方法用于访问         //spanset中保存的数据。         mReplacementSpanSpanSet.init(mSpanned, start, limit);         //numberOfSpans方法用于获取总共拆分了多少个span对象。         hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;     }      mComputed = null;     //PrecomputedText是在android p中提供的一个新的api,PrecomputedTextCompat能够     //使 app 可以事先甚至在后台线程中执行文本布局最耗费时间的部分工作,以缓存布局结果,     //并返回宝贵的测量数据。      //更多关于PrecomputedText可以参考文章:     //https://blog.csdn.net/ecjtuhq/article/details/104366104     if (text instanceof PrecomputedText) {         // Here, no need to check line break strategy or hyphenation frequency since there is no         // line break concept here.         mComputed = (PrecomputedText) text;         if (!mComputed.getParams().getTextPaint().equalsForTextMeasurement(paint)) {             mComputed = null;         }     }      mCharsValid = hasReplacement;     //如果text类型是Spanned && spanset的大小>0,则mCharsValid == true     if (mCharsValid) {         if (mChars == null || mChars.length < mLen) {             mChars = ArrayUtils.newUnpaddedCharArray(mLen);         }         //将text文本拆分为char数据,并赋值到目标mChars中         TextUtils.getChars(text, start, limit, mChars, 0);         //理论上能进入到这里,应该为true         if (hasReplacement) {             // Handle these all at once so we don't have to do it as we go.             // Replace the first character of each replacement run with the             // object-replacement character and the remainder with zero width             // non-break space aka BOM.  Cursor movement code skips these             // zero-width characters.             //用对象替换字符替换每个替换运行的第一个字符,             //并用零宽度不间断空格(又称为BOM)替换其余字符             char[] chars = mChars;             for (int i = start, inext; i < limit; i = inext) {                 inext = mReplacementSpanSpanSet.getNextTransition(i, limit);                 if (mReplacementSpanSpanSet.hasSpansIntersecting(i, inext)                         && (i - start >= ellipsisEnd || inext - start <= ellipsisStart)) {                     // transition into a span                     chars[i - start] = 'ufffc';                     for (int j = i - start + 1, e = inext - start; j < e; ++j) {                         chars[j] = 'ufeff'; // used as ZWNBS, marks positions to skip                     }                 }             }         }     }     mTabs = tabStops;     mAddedWidthForJustify = 0;     mIsJustifying = false;     //省略号开始和结束位置     mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0;     mEllipsisEnd = ellipsisStart != ellipsisEnd ? ellipsisEnd : 0; }

到这里boringLayout讲完了,后续文章会陆续说明StaticLayoutDynamicLayout

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » 重新认识Android的TextView(二)、BoringLayout求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们