- 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 才能得到最终真实的坐标。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论