- CompoundButton 源码分析
- LinearLayout 源码分析
- SearchView 源码解析
- LruCache 源码解析
- ViewDragHelper 源码解析
- BottomSheets 源码解析
- Media Player 源码分析
- NavigationView 源码解析
- Service 源码解析
- Binder 源码分析
- Android 应用 Preference 相关及源码浅析 SharePreferences 篇
- ScrollView 源码解析
- Handler 源码解析
- NestedScrollView 源码解析
- SQLiteOpenHelper/SQLiteDatabase/Cursor 源码解析
- Bundle 源码解析
- LocalBroadcastManager 源码解析
- Toast 源码解析
- TextInputLayout
- LayoutInflater 和 LayoutInflaterCompat 源码解析
- TextView 源码解析
- NestedScrolling 事件机制源码解析
- ViewGroup 源码解析
- StaticLayout 源码分析
- AtomicFile 源码解析
- AtomicFile 源码解析
- Spannable 源码分析
- Notification 之 Android 5.0 实现原理
- CoordinatorLayout 源码分析
- Scroller 源码解析
- SwipeRefreshLayout 源码分析
- FloatingActionButton 源码解析
- AsyncTask 源码分析
- TabLayout 源码解析
5. TextView 的文字处理和绘制
TextView 主要的文字排版和渲染并不是在 TextView 里面完成的,而是由 Layout 类来处理文字排版工作。在单纯地使用 TextView 来展示静态文本的时候,这件事情则是由 Layout 的子类 StaticLayout 来完成的。
StaticLayout 接收到字符串后,首先做的事情是根据字符串里面的换行符对字符串进行拆分。
for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) { paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd); if (paraEnd < 0) paraEnd = bufEnd; else paraEnd++;
拆分后的段落(Paragraph) 被分配给辅助类 MeasuredText 进行测量得到每个字符的宽度以及每个段落的 FontMetric。并通过 LineBreaker 进行折行的判断
//把段落载入到 MeasuredText 中,并分配对应的缓存空间 measured.setPara(source, paraStart, paraEnd, textDir, b); char[] chs = measured.mChars; float[] widths = measured.mWidths; byte[] chdirs = measured.mLevels; int dir = measured.mDir; boolean easy = measured.mEasy;
//把相关属性传给 JNI 层的 LineBreaker
nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
firstWidth, firstWidthLineCount, restWidth,
variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency);
int fmCacheCount = 0;
int spanEndCacheCount = 0;
for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
if (fmCacheCount * 4 >= fmCache.length) {
int[] grow = new int[fmCacheCount * 4 * 2];
System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
fmCache = grow;
}
if (spanEndCacheCount >= spanEndCache.length) {
int[] grow = new int[spanEndCacheCount * 2];
System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
spanEndCache = grow;
}
if (spanned == null) {
spanEnd = paraEnd;
int spanLen = spanEnd - spanStart;
//段落没有 Span 的情况下,把整个段落交给 MeasuredText 计算每个字符的宽度和 FontMetric
measured.addStyleRun(paint, spanLen, fm);
} else {
spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
MetricAffectingSpan.class);
int spanLen = spanEnd - spanStart;
MetricAffectingSpan[] spans =
spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
//把对排版有影响的 Span 交给 MeasuredText 测量宽度并计算 FontMetric
measured.addStyleRun(paint, spans, spanLen, fm);
}
//把测量后的 FontMetric 缓存下来方便后面使用
fmCache[fmCacheCount * 4 + 0] = fm.top;
fmCache[fmCacheCount * 4 + 1] = fm.bottom;
fmCache[fmCacheCount * 4 + 2] = fm.ascent;
fmCache[fmCacheCount * 4 + 3] = fm.descent;
fmCacheCount++;
spanEndCache[spanEndCacheCount] = spanEnd;
spanEndCacheCount++;
}
nGetWidths(b.mNativePtr, widths);
//计算段落中需要折行的位置,并返回折行的数量
int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length);
计算完每一行的测量相关信息、Span 宽高以及折行位置,就可以开始按照最终的行数一行一行地保存下来,以供后面绘制和获取对应文本信息的时候使用。
for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) { spanEnd = spanEndCache[spanEndCacheIndex++]; // 获取之前缓存的 FontMetric 信息 fm.top = fmCache[fmCacheIndex * 4 + 0]; fm.bottom = fmCache[fmCacheIndex * 4 + 1]; fm.ascent = fmCache[fmCacheIndex * 4 + 2]; fm.descent = fmCache[fmCacheIndex * 4 + 3]; fmCacheIndex++; if (fm.top < fmTop) { fmTop = fm.top; } if (fm.ascent < fmAscent) { fmAscent = fm.ascent; } if (fm.descent > fmDescent) { fmDescent = fm.descent; } if (fm.bottom > fmBottom) { fmBottom = fm.bottom; } while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) { breakIndex++; } while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) { int endPos = paraStart + breaks[breakIndex]; boolean moreChars = (endPos < bufEnd); //逐行把相关信息储存下来 v = out(source, here, endPos, fmAscent, fmDescent, fmTop, fmBottom, v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad, chs, widths, paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint, moreChars); if (endPos < spanEnd) { fmTop = fm.top; fmBottom = fm.bottom; fmAscent = fm.ascent; fmDescent = fm.descent; } else { fmTop = fmBottom = fmAscent = fmDescent = 0; } here = endPos; breakIndex++; if (mLineCount >= mMaximumVisibleLineCount) { return; } } }
这样 StaticLayout 的排版过程就完成了。文本的绘制则是交给父类 Layout 来做的,Layout 的绘制分为两大部分,drawBackground 和 drawText。drawBackground 做的事情是如果文本内有 LineBackgroundSpan 则绘制所有的 LineBackgroundSpan,然后判断是否有高亮背景(文本选中的背景),如果有则绘制高亮背景。
public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint, int cursorOffsetVertical, int firstLine, int lastLine) { //判断并绘制 LineBackgroundSpan if (mSpannedText) { if (mLineBackgroundSpans == null) { mLineBackgroundSpans = new SpanSet<LineBackgroundSpan>(LineBackgroundSpan.class); } Spanned buffer = (Spanned) mText; int textLength = buffer.length(); mLineBackgroundSpans.init(buffer, 0, textLength); if (mLineBackgroundSpans.numberOfSpans > 0) { int previousLineBottom = getLineTop(firstLine); int previousLineEnd = getLineStart(firstLine); ParagraphStyle[] spans = NO_PARA_SPANS; int spansLength = 0; TextPaint paint = mPaint; int spanEnd = 0; final int width = mWidth; //逐行绘制 LineBackgroundSpan for (int i = firstLine; i <= lastLine; i++) { int start = previousLineEnd; int end = getLineStart(i + 1); previousLineEnd = end; int ltop = previousLineBottom; int lbottom = getLineTop(i + 1); previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(i); if (start >= spanEnd) { spanEnd = mLineBackgroundSpans.getNextTransition(start, textLength); spansLength = 0; if (start != end || start == 0) { //排除不在绘制范围内的 LineBackgroundSpan for (int j = 0; j < mLineBackgroundSpans.numberOfSpans; j++) { if (mLineBackgroundSpans.spanStarts[j] >= end || mLineBackgroundSpans.spanEnds[j] <= start) continue; spans = GrowingArrayUtils.append( spans, spansLength, mLineBackgroundSpans.spans[j]); spansLength++; } } } //对当前行内的 LineBackgroundSpan 进行绘制 for (int n = 0; n < spansLength; n++) { LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n]; lineBackgroundSpan.drawBackground(canvas, paint, 0, width, ltop, lbaseline, lbottom, buffer, start, end, i); } } } mLineBackgroundSpans.recycle(); } //判断并绘制高亮背景(即选中的文本) if (highlight != null) { if (cursorOffsetVertical != 0) canvas.translate(0, cursorOffsetVertical); canvas.drawPath(highlight, highlightPaint); if (cursorOffsetVertical != 0) canvas.translate(0, -cursorOffsetVertical); } }
drawText 用来逐行绘制 Layout 的文本、影响显示效果的 Span、以及 Emoji 表情等。当有 Emoji 或者 Span 的时候,实际绘制工作交给 TextLine 类来完成。
public void drawText(Canvas canvas, int firstLine, int lastLine) { int previousLineBottom = getLineTop(firstLine); int previousLineEnd = getLineStart(firstLine); ParagraphStyle[] spans = NO_PARA_SPANS; int spanEnd = 0; TextPaint paint = mPaint; CharSequence buf = mText; Alignment paraAlign = mAlignment; TabStops tabStops = null; boolean tabStopsIsInitialized = false; //获取 TextLine 实例 TextLine tl = TextLine.obtain(); //逐行绘制文本 for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) { int start = previousLineEnd; previousLineEnd = getLineStart(lineNum + 1); int end = getLineVisibleEnd(lineNum, start, previousLineEnd); int ltop = previousLineBottom; int lbottom = getLineTop(lineNum + 1); previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(lineNum); int dir = getParagraphDirection(lineNum); int left = 0; int right = mWidth; if (mSpannedText) { Spanned sp = (Spanned) buf; int textLength = buf.length(); //检测是否段落的第一行 boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n'); //获得所有的段落风格相关的 Span if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) { spanEnd = sp.nextSpanTransition(start, textLength, ParagraphStyle.class); spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); paraAlign = mAlignment; for (int n = spans.length - 1; n >= 0; n--) { if (spans[n] instanceof AlignmentSpan) { paraAlign = ((AlignmentSpan) spans[n]).getAlignment(); break; } } tabStopsIsInitialized = false; } //获取影响行缩进的 Span final int length = spans.length; boolean useFirstLineMargin = isFirstParaLine; for (int n = 0; n < length; n++) { if (spans[n] instanceof LeadingMarginSpan2) { int count = ((LeadingMarginSpan2) spans[n]).getLeadingMarginLineCount(); int startLine = getLineForOffset(sp.getSpanStart(spans[n])); if (lineNum < startLine + count) { useFirstLineMargin = true; break; } } } for (int n = 0; n < length; n++) { if (spans[n] instanceof LeadingMarginSpan) { LeadingMarginSpan margin = (LeadingMarginSpan) spans[n]; if (dir == DIR_RIGHT_TO_LEFT) { margin.drawLeadingMargin(canvas, paint, right, dir, ltop, lbaseline, lbottom, buf, start, end, isFirstParaLine, this); right -= margin.getLeadingMargin(useFirstLineMargin); } else { margin.drawLeadingMargin(canvas, paint, left, dir, ltop, lbaseline, lbottom, buf, start, end, isFirstParaLine, this); left += margin.getLeadingMargin(useFirstLineMargin); } } } } boolean hasTabOrEmoji = getLineContainsTab(lineNum); if (hasTabOrEmoji && !tabStopsIsInitialized) { if (tabStops == null) { tabStops = new TabStops(TAB_INCREMENT, spans); } else { tabStops.reset(TAB_INCREMENT, spans); } tabStopsIsInitialized = true; } //判断当前行的第五方式 Alignment align = paraAlign; if (align == Alignment.ALIGN_LEFT) { align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_NORMAL : Alignment.ALIGN_OPPOSITE; } else if (align == Alignment.ALIGN_RIGHT) { align = (dir == DIR_LEFT_TO_RIGHT) ? Alignment.ALIGN_OPPOSITE : Alignment.ALIGN_NORMAL; } int x; if (align == Alignment.ALIGN_NORMAL) { if (dir == DIR_LEFT_TO_RIGHT) { x = left + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); } else { x = right + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); } } else { int max = (int)getLineExtent(lineNum, tabStops, false); if (align == Alignment.ALIGN_OPPOSITE) { if (dir == DIR_LEFT_TO_RIGHT) { x = right - max + getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT); } else { x = left - max + getIndentAdjust(lineNum, Alignment.ALIGN_LEFT); } } else { // Alignment.ALIGN_CENTER max = max & ~1; x = ((right + left - max) >> 1) + getIndentAdjust(lineNum, Alignment.ALIGN_CENTER); } } paint.setHyphenEdit(getHyphen(lineNum)); Directions directions = getLineDirections(lineNum); if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) { //没有任何 Emoji 或者 span 的时候,直接调用 Canvas 来绘制文本 canvas.drawText(buf, start, end, x, lbaseline, paint); } else { //当有 Emoji 或者 Span 的时候,交给 TextLine 类来绘制 tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops); tl.draw(canvas, x, ltop, lbaseline, lbottom); } paint.setHyphenEdit(0); } TextLine.recycle(tl); }
我们下面再来看看 TextLine 是如何绘制有特殊情况的文本的
void draw(Canvas c, float x, int top, int y, int bottom) { //判断是否有 Tab 或者 Emoji if (!mHasTabs) { if (mDirections == Layout.DIRS_ALL_LEFT_TO_RIGHT) { drawRun(c, 0, mLen, false, x, top, y, bottom, false); return; } if (mDirections == Layout.DIRS_ALL_RIGHT_TO_LEFT) { drawRun(c, 0, mLen, true, x, top, y, bottom, false); return; } } float h = 0; int[] runs = mDirections.mDirections; RectF emojiRect = null; int lastRunIndex = runs.length - 2; //逐个绘制 for (int i = 0; i < runs.length; i += 2) { int runStart = runs[i]; int runLimit = runStart + (runs[i+1] & Layout.RUN_LENGTH_MASK); if (runLimit > mLen) { runLimit = mLen; } boolean runIsRtl = (runs[i+1] & Layout.RUN_RTL_FLAG) != 0; int segstart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { int codept = 0; Bitmap bm = null; if (mHasTabs && j < runLimit) { codept = mChars[j]; if (codept >= 0xd800 && codept < 0xdc00 && j + 1 < runLimit) { codept = Character.codePointAt(mChars, j); if (codept >= Layout.MIN_EMOJI && codept <= Layout.MAX_EMOJI) { //获取 Emoji 对应的图像 bm = Layout.EMOJI_FACTORY.getBitmapFromAndroidPua(codept); } else if (codept > 0xffff) { ++j; continue; } } } if (j == runLimit || codept == '\t' || bm != null) { //绘制文字 h += drawRun(c, segstart, j, runIsRtl, x+h, top, y, bottom, i != lastRunIndex || j != mLen); if (codept == '\t') { h = mDir * nextTab(h * mDir); } else if (bm != null) { float bmAscent = ascent(j); float bitmapHeight = bm.getHeight(); float scale = -bmAscent / bitmapHeight; float width = bm.getWidth() * scale; if (emojiRect == null) { emojiRect = new RectF(); } //调整 emoji 图像绘制矩形 emojiRect.set(x + h, y + bmAscent, x + h + width, y); //绘制 Emoji 图像 c.drawBitmap(bm, null, emojiRect, mPaint); h += width; j++; } segstart = j + 1; } } } }
这样就完成了文本的绘制工作,简单地总结就是:分析整体文本—>拆分为段落—>计算整体段落的文本包括 Span 的测量信息—>对文本进行折行—>根据最终行数把文本测量信息保存—>绘制文本的行背景—>判断并获取文本种的 Span 和 Emoji 图像—>绘制最终的文本和图像。当然我们省略了一部分内容,比如段落文本方向,单行的文本排版方向的计算,实际的处理要更为复杂。
接下来我们来看一下在测量过程中出现的 FontMetrics,这是一个 Paint 的静态内部类。主要用来储存文字排版的 Y 轴相关信息。内部仅包含 ascent、descent、top、bottom、leading 五个数值。如下图:
除了 leading 以外,其他的数值都是相对于每一行的 baseline 的,也就是说其他的数值需要加上对应行的 baseline 才能得到最终真实的坐标。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论