本文介绍了重新认识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进行分析。
Layout主要有三个实现类
- StaticLayout
StaticLayout在文字被排列布局之后不允许修改。
- BoringLayout
BoringLayout是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring判断文字是否符合要求。
- DynamicLayout
DynamicLayout支持在排列布局之后修改文字,修改之后会更新text内容。
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
讲完了,后续文章会陆续说明StaticLayout
和DynamicLayout
。
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进行分析。
Layout主要有三个实现类
- StaticLayout
StaticLayout在文字被排列布局之后不允许修改。
- BoringLayout
BoringLayout是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring判断文字是否符合要求。
- DynamicLayout
DynamicLayout支持在排列布局之后修改文字,修改之后会更新text内容。
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
讲完了,后续文章会陆续说明StaticLayout
和DynamicLayout
。
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进行分析。
Layout主要有三个实现类
- StaticLayout
StaticLayout在文字被排列布局之后不允许修改。
- BoringLayout
BoringLayout是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring判断文字是否符合要求。
- DynamicLayout
DynamicLayout支持在排列布局之后修改文字,修改之后会更新text内容。
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
讲完了,后续文章会陆续说明StaticLayout
和DynamicLayout
。
部分转自互联网,侵权删除联系
最新评论