一、前置知识
(一)Span
Span 是功能强大的标记对象,可用于在字符或段落级别为文本设置样式。通过将 Span 附加到文本对象,您能够以各种方式更改文本,包括添加颜色、使文本可点击、缩放文本大小以及以自定义方式绘制文本。Span 还可以更改 TextPaint 属性、在 Canvas 上绘制,以及更改文本布局。
Android为我们提供的Span非常多,可大致分为以下几类:
- 影响文本外观的 Span:如更改文本或背景颜色以及添加下划线或删除线,会触发重新绘制文本,而不会触发重新计算布局。这些 Span 会实现
UpdateAppearance并扩展CharacterStyle。CharacterStyle子类通过提供更新TextPaint的访问权限来定义如何绘制文本。 - 影响文本指标的 Span:如行高和文本大小,会导致观察对象重新测量文本,以实现正确的布局和渲染。这些 Span 通常会扩展
MetricAffectingSpan类,该类为抽象类,它允许子类通过提供对TextPaint的访问权限来定义 Span 影响文本测量的方式。由于MetricAffectingSpan会扩展CharacterSpan,因此子类会影响字符级别的文本外观。 - 影响单个字符的Span:如更新背景颜色、样式或大小等字符元素。影响单个字符的 Span 会扩展
CharacterStyle类 - 影响段落的Span:如更改整个文本块的对齐方式或边距。影响整个段落的 Span 会实现
ParagraphStyle。使用这些 Span 时,必须将其附加到整个段落,不包括末尾换行符【通过换行符分割段落】。如果您尝试将段落 Span 应用于除整个段落以外的其他内容,Android 根本不会应用该 Span。
我们可以根据Span子类实现/继承的类来区分是上述哪种类型:
- Span会影响文本外观,需要实现
UpdateAppearance - Span 会影响文本指标/尺寸,需要实现
UpdateLayout - Span 会在字符级别影响文本,需要实现
CharacterStyle - Span 会在段落级别影响文本,需要实现
ParagraphStyle
如何将span附加到塞入TextView中的string上呢?
Android中定义了SpannedString、SpannableString、SpannableStringBuilder类将CharSequence与Span建立关联,即存储每一个char对应的Span标记。
- SpannedString:实现Spanned接口,仅读取而不设置文本和 span;继承自
SpannableStringInternal内部使用线性数组实现 - SpannableString:实现Spannable接口,设置少量的 Span,而不设置文本;继承自
SpannableStringInternal内部使用线性数组实现 - SpannableStringBuilder:实现Spannable接口,需要设置文本和span;设置大量的Span;内部使用区间数实现
(二)FontMetrics
- BaseLine:在 TextView 中每一行都有一条基线,叫 BaseLine ,文本的绘制是从这里开始的,这是以当前行为坐标系,y 方向为 0 的一条线,也就是说,BaseLine 以上是负数,以下是正数。
- ascent:从 BaseLine 向上到字符的最高处。
- descent:从 BaseLine 向下到字符的最低处。
- top、bottom:在说 top 和 bottom 之前,需要知道,世界上很多国家,文字书写也是不相同的,有些文字可能带有读音符之类的上标或者下标,比如上图中的 A ,它上面的波浪线就是类似于读音符(具体是啥,我也不知道),Android 为了更好的画出这些上标或者下标,特意在每一行的 ascent 和 descent 外都预留了一点距离,即 top 是 ascent 加上上面预留出来的距离所表示的坐标,bottom 也是一样的。
- leading:是表示上一行的descent 到当前行的 ascent 之间的距离
二、本文摘要
TextView是Android控件中较为复杂的控件,也是使用频率较高的控件,通过阅读TextView的源码,帮助我们了解View组件的绘制流程。
阅读源码前,我们先思考一下当我们自己去实现一个TextView时,会考虑哪些点?
1、当我们绘制一行简单的文本,在调用canvas#drawText()之前,如何测量view大小、绘制位置?
2、当我们绘制多行文本时,如何确定需要换行?如何在第一个问题的基础上确定后面几行的绘制位置?
3、当一行文本中插入了扩大文本大小的Span时,如何计算整行的高度?如何定位没有被修改大小属性的文本绘制位置?
在总结上述问题前,我们先将TextView#setText()作为TextView的入口,来跟踪一下源码的绘制流程。
(一)源码跟踪
TextView对外提供了几个 setText 的重载方法,但其实最终都会进入内部的 setText 方法,代码如下:
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
......
if (mLayout != null) {//1
checkForRelayout();//2
}
......
}
在这个方法中,主要做一些初始化工作,如为传入的CharSequence重新设置Span,添加触摸事件等,其中最重要的是checkForRelayout()方法。
首先,我们会对mLayout != null这行代码产生如下疑问:
- mLayout是什么?
mLayout其实就是Layout的子类,并不是我们View绘制中理解的布局,可以理解为一个 TextView展示在屏幕上的布局相关的管理信息类,也负责处理文本测量与绘制的前置工作。Layout有三个子类:
-
BoringLayout
是Layout的最简单的实现,主要用于适配单行文字展示,并且只支持从左到右的展示 方向。不建议在自己的开发过程中直接使用,如果需要使用的话,首先使用isBoring 判断文字是否符合要求。DynamicLayout
支持在排列布局之后修改文字,修改之后会更新text内容。StaticLayout
在文字被排列布局之后不允许修改。
- mLayout什么时候初始化的?
在TextView#onMeasure()做布局测量时,会通过TextView#makeNewLayout()方法初始化mLayout。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
if (mLayout == null) {
//初始化mLayout
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
}
......
}
1、测量布局
由此,我们可以知道,当走到TextView#setText()方法时,mLayout已经被赋值了,因此一定会走到TextView#checkForRelayout()方法,这个方法的主要工作是判断是否需要重新测量布局。
private void checkForRelayout() {
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
//1.如果TextView的宽度是固定值,而不是WRAP_CONTENT,直接重建一个新的mLayout
......
//关注
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false);
//1.1.TextView.mEllipsize == TextUtils.TruncateAt.MARQUEE:设置为跑马灯效果
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
//1.1.1.如果TextView的高度是固定值,则不需要触发重新布局,只要重绘
autoSizeText();
invalidate();
return;
}
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
//1.1.2.如果TextView的高度是动态的,但是布局高度没有改变,则不需要触发重新布局,只要重绘
autoSizeText();
invalidate();
return;
}
}
//1.2.如果TextView的高度是动态的,并且高度发生变化了,就需要触发重新布局
requestLayout();
invalidate();
} else {
//2.如果TextView的宽度是动态的,就需要触发重新布局
nullLayouts();
requestLayout();
invalidate();
}
}
这里的makeNewLayout()方法就是上文所说的初始化mLayout的方法。
public void makeNewLayout(int wantWidth, int hintWidth,
BoringLayout.Metrics boring,
BoringLayout.Metrics hintBoring,
int ellipsisWidth, boolean bringIntoView) {
......
mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
effectiveEllipsize, effectiveEllipsize == mEllipsize);
......
}
此方法主要获取能够创建mLayout所需的参数,如对齐方式、省略方式、文字方向等。最后通过TextView#makeSingleLayout()构建出mLayout。
protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
if (useDynamicLayout()) {
//1.如果文字被选中,或文字设置了Span,则创建DynamicLayout
final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(......);
......
} else {
//2.判断是否符合BoringLayout的创建条件,如果符合,则创建BoringLayout
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
}
if (boring != null) {
if (boring.width <= wantWidth
&& (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
if (useSaved && mSavedLayout != null) {
//2.1用缓存的layout
result = mSavedLayout.replaceOrMake(......);
} else {
//2.2创建BoringLayout
result = BoringLayout.make(......);
}
if (useSaved) {
mSavedLayout = (BoringLayout) result;
}
} else if (shouldEllipsize && boring.width <= wantWidth) {
if (useSaved && mSavedLayout != null) {
//2.1用缓存的layout
result = mSavedLayout.replaceOrMake(......);
} else {
//2.2创建BoringLayout
result = BoringLayout.make(......);
}
}
}
}
if (result == null) {
//3.兜底逻辑,如果上述条件都不符合,则默认创建StaticLayout
StaticLayout.Builder builder = StaticLayout.Builder.obtain(......);
......
}
return result;
}
至此,mLayout的创建逻辑就走完了,接下来,我们看一下TextView的布局、绘制流程。当TextView#checkForRelayout()方法中满足了重新布局的条件时,会通过requestLayout()方法通知ViewRootImpl重新布局,根据view的绘制流程,RootView会递归调用子View的onLayout()方法来通知子view进行测量,因此我们来看一下TextView#onLayout()。
TextView#onLayout()主要是为了计算当前字体大小,如果允许动态测量字体,则需要通过TextView#makeNewLayout()重新创建mLayout。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
......
// Call auto-size after the width and height have been calculated.
autoSizeText();
}
private void autoSizeText() {
if (!isAutoSizeEnabled()) {
return;
}
if (mNeedsAutoSizeText) {
if (getMeasuredWidth() <= 0 || getMeasuredHeight() <= 0) {
return;
}
final int availableWidth = mHorizontallyScrolling
? VERY_WIDE
: getMeasuredWidth() - getTotalPaddingLeft() - getTotalPaddingRight();
final int availableHeight = getMeasuredHeight() - getExtendedPaddingBottom()
- getExtendedPaddingTop();
if (availableWidth <= 0 || availableHeight <= 0) {
return;
}
synchronized (TEMP_RECTF) {
TEMP_RECTF.setEmpty();
TEMP_RECTF.right = availableWidth;
TEMP_RECTF.bottom = availableHeight;
final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
if (optimalTextSize != getTextSize()) {
setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize,
false /* shouldRequestLayout */);
makeNewLayout(availableWidth, 0 /* hintWidth */, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false /* bringIntoView */);
}
}
}
// Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
// after the next layout pass should set this to false.
mNeedsAutoSizeText = true;
}
2、绘制
至此,布局测量的流程就完成了。接着,TextView#checkForRelayout()方法中,会通过invalidate()方法通知view绘制,根据view的绘制流程,RootView会递归调用子View的onDraw()方法来通知子view进行绘制,因此我们来看一下TextView#onDraw()。
2.1、绘制与文本无关的
首先,会处理与文字本身的绘制无关的绘制工作,如绘制控件背景、drawable、hint、padding、shadow,并不断平移canvas,调整文字绘制的锚点。
protected void onDraw(Canvas canvas) {
restartMarqueeIfNeeded();
// Draw the background for this view
super.onDraw(canvas);
......
//1、绘制drawable
final Drawables dr = mDrawables;
if (dr != null) {
......
//如果TextView设置了Drawable,通过drawable#draw(canvas)绘制drawable
//然后,需要根据drawable的位置(Left、Right、Top、Bottom)来将canvas平移,这样就可以依旧将画布左上角作为文字绘制的锚点
if (dr.mShowing[Drawables.LEFT] != null) {
canvas.save();
canvas.translate(scrollX + mPaddingLeft + leftOffset,
scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
dr.mShowing[Drawables.LEFT].draw(canvas);
canvas.restore();
}
......
}
......
//2、绘制hint文本
if (mHint != null && mText.length() == 0) {
//如果文本内容为空,并且存在提示文字,则绘制提示文字
if (mHintTextColor != null) {
color = mCurHintTextColor;
}
layout = mHintLayout;
}
mTextPaint.setColor(color);
mTextPaint.drawableState = getDrawableState();
canvas.save();
//3、调整canvas位置,使文本绘制锚点的x、y轴为0
//3.1、根据TextView设置的padding、shawdow,来对canvas进行裁剪、平移,也是为了能将画布的左上角作为绘制文字的锚点
int extendedPaddingTop = getExtendedPaddingTop();
int extendedPaddingBottom = getExtendedPaddingBottom();
final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
final int maxScrollY = mLayout.getHeight() - vspace;
float clipLeft = compoundPaddingLeft + scrollX;
float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
float clipRight = right - left - getCompoundPaddingRight() + scrollX;
float clipBottom = bottom - top + scrollY
- ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
if (mShadowRadius != 0) {
clipLeft += Math.min(0, mShadowDx - mShadowRadius);
clipRight += Math.max(0, mShadowDx + mShadowRadius);
clipTop += Math.min(0, mShadowDy - mShadowRadius);
clipBottom += Math.max(0, mShadowDy + mShadowRadius);
}
canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
int voffsetText = 0;
int voffsetCursor = 0;
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
voffsetText = getVerticalOffset(false);
voffsetCursor = getVerticalOffset(true);
}
canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
//3.2、根据文字direction、gravity来对canvas进行平移,也是为了能将画布的左上角作为绘制文字的锚点
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
if (isMarqueeFadeEnabled()) {
if (!mSingleLine && getLineCount() == 1 && canMarquee()
&& (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
final int width = mRight - mLeft;
final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
final float dx = mLayout.getLineRight(0) - (width - padding);
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
if (mMarquee != null && mMarquee.isRunning()) {
final float dx = -mMarquee.getScroll();
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
}
}
final int cursorOffsetVertical = voffsetCursor - voffsetText;
Path highlight = getUpdatedHighlightPath();
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
//4、最后,走到mLayout#draw()来绘制文字
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
if (mMarquee != null && mMarquee.shouldDrawGhost()) {
final float dx = mMarquee.getGhostOffset();
canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
canvas.restore();
}
2.2、绘制与文本相关的
真正开始与文字绘制相关的流程在mLayout#onDraw()中。
Layout的所有子类中,BoringLayout会重写onDraw()方法。
public void draw(Canvas c, Path highlight, Paint highlightpaint,
int cursorOffset) {
if (mDirect != null && highlight == null) {
//文字方向确定,并且不需要绘制高亮,直接通过canvas#drawText()绘制文字
c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
} else {
//和其他Layout子类一样走父Layout#draw()方法
super.draw(c, highlight, highlightpaint, cursorOffset);
}
}
因此真正的文字绘制流程还是在父Layout#draw()方法中。
public void draw(Canvas canvas, Path highlight, Paint highlightPaint,
int cursorOffsetVertical) {
final long lineRange = getLineRangeForDraw(canvas);
int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
if (lastLine < 0) return;
//1、绘制背景
drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
firstLine, lastLine);
//2、绘制文字
drawText(canvas, firstLine, lastLine);
}
2.2.1、绘制文本背景
首先需要绘制与文本相关的背景,包括:LineBackgroundSpan、highlight
public void drawBackground(Canvas canvas, Path highlight, Paint highlightPaint,
int cursorOffsetVertical, int firstLine, int lastLine) {
//1、处理LineBackgroundSpan
if (mSpannedText) {
......
if (mLineBackgroundSpans.numberOfSpans > 0) {
......
for (int i = firstLine; i <= lastLine; i++) {
//一行一行处理
......
for (int n = 0; n < spansLength; n++) {
LineBackgroundSpan lineBackgroundSpan = (LineBackgroundSpan) spans[n];
//调用lineBackgroundSpan中重写的drawBackground()绘制
lineBackgroundSpan.drawBackground(canvas, paint, 0, width,
ltop, lbaseline, lbottom,
buffer, start, end, i);
}
}
}
mLineBackgroundSpans.recycle();
}
//2、绘制highlight
if (highlight != null) {
if (cursorOffsetVertical != 0) {
//纵向上移动canvas,去掉highlight占空间,也是为了能将画布的左上角作为绘制文字的锚点
canvas.translate(0, cursorOffsetVertical);
}
canvas.drawPath(highlight, highlightPaint);
if (cursorOffsetVertical != 0) {
canvas.translate(0, -cursorOffsetVertical);
}
}
}
2.2.2、绘制文本
终于到了绘制文本阶段,这里的代码我们详细跟一下。我们可以看到,他会先去查找是否有影响到文本尺寸、位置属性的Span、tab符、缩进,来判断这些文字是否需要特殊处理(即根据这几样来重新确定文字绘制的锚点),如果不需要复杂处理,就会直接调用canvas#drawText(),否则会通过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;
final TextPaint paint = mWorkPaint;
paint.set(mPaint);
CharSequence buf = mText;
Alignment paraAlign = mAlignment;
TabStops tabStops = null;
boolean tabStopsIsInitialized = false;
TextLine tl = TextLine.obtain();
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
//一行一行绘制
int start = previousLineEnd;
previousLineEnd = getLineStart(lineNum + 1);
final boolean justify = isJustificationRequired(lineNum);
//去掉换行符和空格,只保留文字
int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
//当前行的top = 上一行的bottom
int ltop = previousLineBottom;
//当前行的bottom = 下一行的top
int lbottom = getLineTop(lineNum + 1);
previousLineBottom = lbottom;
//baseline = 当前行的bottom-当前行的top
int lbaseline = lbottom - getLineDescent(lineNum);
int dir = getParagraphDirection(lineNum);
int left = 0;
int right = mWidth;
//1、处理span标志
if (mSpannedText) {
Spanned sp = (Spanned) buf;
int textLength = buf.length();
boolean isFirstParaLine = (start == 0 || buf.charAt(start - 1) == '\n');
// New batch of paragraph styles, collect into spans array.
// Compute the alignment, last alignment style wins.
// Reset tabStops, we'll rebuild if we encounter a line with
// tabs.
// We expect paragraph spans to be relatively infrequent, use
// spanEnd so that we can check less frequently. Since
// paragraph styles ought to apply to entire paragraphs, we can
// just collect the ones present at the start of the paragraph.
// If spanEnd is before the end of the paragraph, that's not
// our problem.
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;
}
//画LeadingMargin
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 there is more than one LeadingMarginSpan2, use
// the count that is greatest
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);
}
}
}
}
//2、判断是否有tab
boolean hasTab = getLineContainsTab(lineNum);
// Can't tell if we have tabs for sure, currently
if (hasTab && !tabStopsIsInitialized) {
if (tabStops == null) {
tabStops = new TabStops(TAB_INCREMENT, spans);
} else {
tabStops.reset(TAB_INCREMENT, spans);
}
tabStopsIsInitialized = true;
}
//3、计算绘制的x坐标
Alignment align = paraAlign;
//根据段落的对齐方式判断text绘制的对齐方式
//左对齐、右对齐、居中
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;
final int indentWidth;
//根据对齐方式,计算intentWidth缩进
//结合对齐方式+缩进 计算绘制的x轴坐标
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
x = left + indentWidth;
} else {
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
x = right - indentWidth;
}
} else {
int max = (int)getLineExtent(lineNum, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
x = right - max - indentWidth;
} else {
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
x = left - max + indentWidth;
}
} else { // Alignment.ALIGN_CENTER
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
max = max & ~1;
x = ((right + left - max) >> 1) + indentWidth;
}
}
//4、绘制
Directions directions = getLineDirections(lineNum);
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
//不需要特殊处理,即正常方向、没有span标志、没有tab、不需要调整
//直接绘制text
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
//否则,通过调用TextLine绘制text
tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops,
getEllipsisStart(lineNum),
getEllipsisStart(lineNum) + getEllipsisCount(lineNum));
if (justify) {
tl.justify(right - left - indentWidth);
}
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
}
//5、回收TextLine
TextLine.recycle(tl);
}
2.2.2.1、绘制简单文本
对于简单的不需要额外处理的文本,会直接交给canvas#drawText()方法绘制。
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
@NonNull Paint paint) {
super.drawText(text, start, end, x, y, paint);
}
canvas#drawText()会直接调用其父类BaseCanvas#drawText()方法
public void drawText(@NonNull char[] text, int index, int count, float x, float y,
@NonNull Paint paint) {
//处理异常情况
if ((index | count | (index + count) |
(text.length - index - count)) < 0) {
throw new IndexOutOfBoundsException();
}
throwIfHasHwBitmapInSwMode(paint);
nDrawText(mNativeCanvasWrapper, text, index, count, x, y, paint.mBidiFlags,
paint.getNativeInstance());
}
通过注释可知绘制文本需要知道四个重要信息:
- 要绘制的文本:text
- 要绘制的文本区间:通过start、end来确定
- 绘制的起始锚点:根据x、y坐标确定
- 画笔:paint
其中:
- x:绘制的起始x轴坐标,根据对齐方向和缩进来确定
- y:绘制的起始y轴坐标,即段落的
baseLine
而BaseCanvas#drawText()方法中通过调用native方法执行绘制。
private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
long nativePrecomputedText);
2.2.2.2、绘制复杂文本
对于span、tab等需要特殊处理的情况,Layout#draw()都交由TextLine处理了。
void draw(Canvas c, float x, int top, int y, int bottom) {
float h = 0;
final int runCount = mDirections.getRunCount();
for (int runIndex = 0; runIndex < runCount; runIndex++) {
final int runStart = mDirections.getRunStart(runIndex);
if (runStart > mLen) break;
//这一行能绘制的文字个数 = Math.min(这一行应该绘制的文字个数, 去掉其他因素占用的空间后剩下的空间中所能绘制的文字个数)
final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen);
final boolean runIsRtl = mDirections.isRunRtl(runIndex);
int segStart = runStart;
//如果存在空格,就需要减少这一行能够绘制的文字个数
for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) {
if (j == runLimit || charAt(j) == TAB_CHAR) {
h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
runIndex != (runCount - 1) || j != mLen);
if (j != runLimit) { // charAt(j) == TAB_CHAR
h = mDir * nextTab(h * mDir);
}
segStart = j + 1;
}
}
}
}
private float drawRun(Canvas c, int start,
int limit, boolean runIsRtl, float x, int top, int y, int bottom,
boolean needWidth) {
if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) {
float w = -measureRun(start, limit, limit, runIsRtl, null);
//如果方向是从右向左的,需要重新计算绘制的x坐标
handleRun(start, limit, limit, runIsRtl, c, x + w, top,
y, bottom, null, false);
return w;
}
return handleRun(start, limit, limit, runIsRtl, c, x, top,
y, bottom, null, needWidth);
}
private float handleRun(int start, int measureLimit,
int limit, boolean runIsRtl, Canvas c, float x, int top, int y,
int bottom, FontMetricsInt fmi, boolean needWidth) {
if (measureLimit < start || measureLimit > limit) {
throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of "
+ "start (" + start + ") and limit (" + limit + ") bounds");
}
// Case of an empty line, make sure we update fmi according to mPaint
if (start == measureLimit) {
final TextPaint wp = mWorkPaint;
wp.set(mPaint);
//根据上述逻辑可知, fmi=null
if (fmi != null) {
expandMetricsFromPaint(fmi, wp);
}
return 0f;
}
//1、处理span
//判断是否有Span,需要根据span来计算高宽等属性
////主要有两种Span:
////- MetricAffectingSpan,会影响文字的size属性
////- CharacterStyle,会影响文字的显示属性
final boolean needsSpanMeasurement;
if (mSpanned == null) {
needsSpanMeasurement = false;
} else {
mMetricAffectingSpanSpanSet.init(mSpanned, mStart + start, mStart + limit);
mCharacterStyleSpanSet.init(mSpanned, mStart + start, mStart + limit);
needsSpanMeasurement = mMetricAffectingSpanSpanSet.numberOfSpans != 0
|| mCharacterStyleSpanSet.numberOfSpans != 0;
}
if (!needsSpanMeasurement) {
//如果不需要计算Span,可以直接去绘制文字
final TextPaint wp = mWorkPaint;
wp.set(mPaint);
//处理连字符
wp.setStartHyphenEdit(adjustStartHyphenEdit(start, wp.getStartHyphenEdit()));
wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit()));
return handleText(wp, start, limit, start, limit, runIsRtl, c, x, top,
y, bottom, fmi, needWidth, measureLimit, null);
}
// Shaping needs to take into account context up to metric boundaries,
// but rendering needs to take into account character style boundaries.
// So we iterate through metric runs to get metric bounds,
// then within each metric run iterate through character style runs
// for the run bounds.
final float originalX = x;
for (int i = start, inext; i < measureLimit; i = inext) {//一个字符一个字符处理
final TextPaint wp = mWorkPaint;
wp.set(mPaint);
inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
mStart;
int mlimit = Math.min(inext, measureLimit);
ReplacementSpan replacement = null;
//1.1、处理会影响文字的size属性的MetricAffectingSpan
for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
// Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
// empty by construction. This special case in getSpans() explains the >= & <= tests
if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit)
|| (mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
boolean insideEllipsis =
mStart + mEllipsisStart <= mMetricAffectingSpanSpanSet.spanStarts[j]
&& mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + mEllipsisEnd;
final MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
if (span instanceof ReplacementSpan) {
//如果ReplacementSpan所在的位置,是被设置为省略了,就不处理这个ReplacementSpan了
replacement = !insideEllipsis ? (ReplacementSpan) span : null;
} else {
//否则,就调用MetricAffectingSpan子类中重写的updateDrawState()方法,修改text的textSize等属性
span.updateDrawState(wp);
}
}
//如果需要处理replacement,就调用handleReplacement()方法
//handleReplacement()方法实际是调用ReplacementSpan子类中重写的draw()方法进行绘制
if (replacement != null) {
x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
bottom, fmi, needWidth || mlimit < measureLimit);
continue;
}
//1.2、处理影响文本属性的CharacterStyle类型的span
final TextPaint activePaint = mActivePaint;
activePaint.set(mPaint);
int activeStart = i;
int activeEnd = mlimit;
final DecorationInfo decorationInfo = mDecorationInfo;
mDecorations.clear();
for (int j = i, jnext; j < mlimit; j = jnext) {
jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
mStart;
final int offset = Math.min(jnext, mlimit);
wp.set(mPaint);
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
// Intentionally using >= and <= as explained above
if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
final CharacterStyle span = mCharacterStyleSpanSet.spans[k];
span.updateDrawState(wp);
}
extractDecorationInfo(wp, decorationInfo);
//用activeStart和activeEnd来记录,span属性应用到第几个字符
if (j == i) {
// First chunk of text. We can't handle it yet, since we may need to merge it
// with the next chunk. So we just save the TextPaint for future comparisons
// and use.
activePaint.set(wp);
} else if (!equalAttributes(wp, activePaint)) {
//当遇到span属性与上一个字符的span属性不同时,就先处理上一段属性应用的字符区间
//然后更新activeStart到这个不同的属性的字符位置
activePaint.setStartHyphenEdit(
adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
activePaint.setEndHyphenEdit(
adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Math.min(activeEnd, mlimit), mDecorations);
activeStart = j;
activePaint.set(wp);
mDecorations.clear();
} else {
// The present TextPaint is substantially equal to the last TextPaint except
// perhaps for decorations. We just need to expand the active piece of text to
// include the present chunk, which we always do anyway. We don't need to save
// wp to activePaint, since they are already equal.
}
activeEnd = jnext;
if (decorationInfo.hasDecoration()) {
final DecorationInfo copy = decorationInfo.copyInfo();
copy.start = j;
copy.end = jnext;
mDecorations.add(copy);
}
}
//2、处理剩下的字符
activePaint.setStartHyphenEdit(
adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit()));
activePaint.setEndHyphenEdit(
adjustEndHyphenEdit(activeEnd, mPaint.getEndHyphenEdit()));
x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, x,
top, y, bottom, fmi, needWidth || activeEnd < measureLimit,
Math.min(activeEnd, mlimit), mDecorations);
}
return x - originalX;
}
终于到了绘制文本的地方了。
private float handleText(TextPaint wp, int start, int end,
int contextStart, int contextEnd, boolean runIsRtl,
Canvas c, float x, int top, int y, int bottom,
FontMetricsInt fmi, boolean needWidth, int offset,
@Nullable ArrayList<DecorationInfo> decorations) {
if (mIsJustifying) {
wp.setWordSpacing(mAddedWidthForJustify);
}
//获取FontMetrics
if (fmi != null) {
expandMetricsFromPaint(fmi, wp);
}
// No need to do anything if the run width is "0"
if (end == start) {
return 0f;
}
float totalWidth = 0;
final int numDecorations = decorations == null ? 0 : decorations.size();
if (needWidth || (c != null && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) {
totalWidth = getRunAdvance(wp, start, end, contextStart, contextEnd, runIsRtl, offset);
}
if (c != null) {
final float leftX, rightX;
//再次按照文本方向,计算绘制锚点的x坐标
if (runIsRtl) {
leftX = x - totalWidth;
rightX = x;
} else {
leftX = x;
rightX = x + totalWidth;
}
//1、如果有背景色,先绘制背景
if (wp.bgColor != 0) {
int previousColor = wp.getColor();
Paint.Style previousStyle = wp.getStyle();
wp.setColor(wp.bgColor);
wp.setStyle(Paint.Style.FILL);
c.drawRect(leftX, top, rightX, bottom, wp);
wp.setStyle(previousStyle);
wp.setColor(previousColor);
}
//2、绘制文字
drawTextRun(c, wp, start, end, contextStart, contextEnd, runIsRtl,
leftX, y + wp.baselineShift);
if (numDecorations != 0) {
for (int i = 0; i < numDecorations; i++) {
final DecorationInfo info = decorations.get(i);
final int decorationStart = Math.max(info.start, start);
final int decorationEnd = Math.min(info.end, offset);
float decorationStartAdvance = getRunAdvance(
wp, start, end, contextStart, contextEnd, runIsRtl, decorationStart);
float decorationEndAdvance = getRunAdvance(
wp, start, end, contextStart, contextEnd, runIsRtl, decorationEnd);
final float decorationXLeft, decorationXRight;
if (runIsRtl) {
decorationXLeft = rightX - decorationEndAdvance;
decorationXRight = rightX - decorationStartAdvance;
} else {
decorationXLeft = leftX + decorationStartAdvance;
decorationXRight = leftX + decorationEndAdvance;
}
//3、绘制下划线
if (info.underlineColor != 0) {
drawStroke(wp, c, info.underlineColor, wp.getUnderlinePosition(),
info.underlineThickness, decorationXLeft, decorationXRight, y);
}
if (info.isUnderlineText) {
final float thickness =
Math.max(wp.getUnderlineThickness(), 1.0f);
drawStroke(wp, c, wp.getColor(), wp.getUnderlinePosition(), thickness,
decorationXLeft, decorationXRight, y);
}
if (info.isStrikeThruText) {
final float thickness =
Math.max(wp.getStrikeThruThickness(), 1.0f);
drawStroke(wp, c, wp.getColor(), wp.getStrikeThruPosition(), thickness,
decorationXLeft, decorationXRight, y);
}
}
}
}
return runIsRtl ? -totalWidth : totalWidth;
}
private void drawTextRun(Canvas c, TextPaint wp, int start, int end,
int contextStart, int contextEnd, boolean runIsRtl, float x, int y) {
if (mCharsValid) {
int count = end - start;
int contextCount = contextEnd - contextStart;
c.drawTextRun(mChars, start, count, contextStart, contextCount,
x, y, runIsRtl, wp);
} else {
int delta = mStart;
c.drawTextRun(mText, delta + start, delta + end,
delta + contextStart, delta + contextEnd, x, y, runIsRtl, wp);
}
}
可以看到最终,我们还是走到了canvas#drawTextRun()方法,而canvas#drawtext()直接走到其父类BaseCanvas#drawTextRun()方法,对一些异常情况做处理后,走到BaseCanvas#nDrawTextRun()方法,即native方法。
private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
long nativePrecomputedText);
3、接受软键盘的输入
- Android为所有的View预留了一个接收软键盘输入的接口类,叫InputConnection。软键盘以InputConnection为桥梁把文字输入、文字修改、文字删除等传递给View。任意View只要重写onCheckIsTextEditor()并返回true,然后重写onCreateInputConnection(EditorInfo outAttrs)返回一个InputConnection的实例,便可以接收软键盘的输入。
- TextView的软键盘输入接收,是通过EditableInputConnection类来实现的。
- EditText继承自TextView
4、OnTouchEvent事件处理
- TextView内部能处理触摸事件的,包括自身的触摸处理、Editor的onTouchEvent、MovementMethod的onTouchEvent。
- Editor的onTouchEvent主要处理出于编辑状态下的触摸事件,比如点击选中、长按等。
- MovementMethod则主要负责文本内部有Span的时候的相关处理,比较常见的就是LinkMovementMethod处理ClickableSpan的点击事件。
(二)总结
至此,从setText作为入口,我们已经跟踪完了TextView绘制的完整流程,总结下来就是:
1、首先判断是否需要重新测量布局
-
如果需要就先测量
-
如果不需要就直接进行绘制。
2、绘制流程
- 先从绘制与
TextView控件有关,但与文本无关的,如TextView的背景、hint。 - 接着绘制与文本有关的:
-
首先绘制文本的背景,如
LineBackgroundSpan、highlight; -
接着开始绘制文本。如果是简单文本,即没有
span、tab、缩进会影响到文本属性和绘制锚点的,就直接通知canvas绘制。如果是复杂文本,即文本中包含Span标记、tab、缩进,就交给TextLine来处理。- 首先,处理
Span。对于MetricAffectingSpan,如果是ReplacementSpan,就调用其重写的draw()方法进行绘制;如果是其他Span,就调用其内部重写的updateDrawState更改TextPaint中的属性。对于CharacterStyle的span,就调用span内部重写的updateDrawState更改TextPaint中的属性。 - 接着,绘制文本的背景色
- 然后,绘制文本,即通知
canvas绘制 - 最后,绘制下划线
- 首先,处理
-
接着对我们刚开始提出的三个问题作出解答:
1、当我们绘制一行简单的文本,在调用canvas#drawText()之前,如何测量view大小、绘制位置?
我们再来看一下绘制简单文本的地方,直接调用canvas#drawText()方法进行绘制,而这个方法的入参有:
-
text:文本内容
-
start:要绘制的文本内容的区间的开始位置
-
end:要绘制的文本内容的区间的结束位置
-
x:绘制锚点的x坐标
-
y:绘制锚点的y坐标
-
paint:画笔
1.1、首先,针对view的文本大小,当我们调用TextView#setText()方法时,其内部是通过调用Paint#setText()方法,最终走到native方法。
public void setTextSize(int unit, float size) {
if (!isAutoSizeEnabled()) {
setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
}
}
private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
......
setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),
shouldRequestLayout);
}
private void setRawTextSize(float size, boolean shouldRequestLayout) {
if (size != mTextPaint.getTextSize()) {
//调用paint方法
mTextPaint.setTextSize(size);
if (shouldRequestLayout && mLayout != null) {
// Do not auto-size right after setting the text size.
mNeedsAutoSizeText = false;
nullLayouts();
requestLayout();
invalidate();
}
}
}
@CriticalNative
private static native void nSetTextSize(long paintPtr, float textSize);
对于简单的不需要额外处理的文本,会直接交给canvas#drawText()方法绘制。
public void drawText(@NonNull CharSequence text, int start, int end, float x, float y,
@NonNull Paint paint) {
super.drawText(text, start, end, x, y, paint);
}
1.2、调用此方法的入参处,可以看到,x = x,y = lbasekline
canvas.drawText(buf, start, end, x, lbaseline, paint);
从下面的代码可以看出,x是根据对齐方式和缩进来计算的。
int x;
final int indentWidth;
//根据对齐方式,计算intentWidth缩进
//结合对齐方式+缩进 计算绘制的x轴坐标
if (align == Alignment.ALIGN_NORMAL) {
if (dir == DIR_LEFT_TO_RIGHT) {
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
x = left + indentWidth;
} else {
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
x = right - indentWidth;
}
} else {
int max = (int)getLineExtent(lineNum, tabStops, false);
if (align == Alignment.ALIGN_OPPOSITE) {
if (dir == DIR_LEFT_TO_RIGHT) {
indentWidth = -getIndentAdjust(lineNum, Alignment.ALIGN_RIGHT);
x = right - max - indentWidth;
} else {
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_LEFT);
x = left - max + indentWidth;
}
} else { // Alignment.ALIGN_CENTER
indentWidth = getIndentAdjust(lineNum, Alignment.ALIGN_CENTER);
max = max & ~1;
x = ((right + left - max) >> 1) + indentWidth;
}
}
而baseLine则是
//baseline = 当前行的bottom-当前行的top
int lbaseline = lbottom - getLineDescent(lineNum);
2、当我们绘制多行文本时,如何确定需要换行?如何在第一个问题的基础上确定后面几行的绘制位置?
2.1、首先,Layout#draw()方法中通过getLineRangeForDraw()方法再到getLineCount()方法拿到文本行数
public long getLineRangeForDraw(Canvas canvas) {
int dtop, dbottom;
synchronized (sTempRect) {
if (!canvas.getClipBounds(sTempRect)) {
// Negative range end used as a special flag
return TextUtils.packRangeInLong(0, -1);
}
dtop = sTempRect.top;
dbottom = sTempRect.bottom;
}
final int top = Math.max(dtop, 0);
final int bottom = Math.min(getLineTop(getLineCount()), dbottom);
if (top >= bottom) return TextUtils.packRangeInLong(0, -1);
return TextUtils.packRangeInLong(getLineForVertical(top), getLineForVertical(bottom));
}
在父Layout中,getLineCount()是一个抽象方法,在他的子类中分别实现。
public abstract int getLineCount();
例如,BoringLayout只考虑单行:
@Override
public int getLineCount() {
return 1;
}
而,StaticLayout中返回了mLineCount,而mLineCount是在StaticLayout#generate()时,通过LineBreaker根据段落信息计算的。【generate()方法在创建Layout时被调用】
@Override
public int getLineCount() {
return mLineCount;
}
/* package */ void generate(Builder b, boolean includepad, boolean trackpad) {
.......
mLineCount = 0;
......
final LineBreaker lineBreaker = new LineBreaker.Builder()
.setBreakStrategy(b.mBreakStrategy)
.setHyphenationFrequency(b.mHyphenationFrequency)
// TODO: Support more justification mode, e.g. letter spacing, stretching.
.setJustificationMode(b.mJustificationMode)
.setIndents(indents)
.build();
LineBreaker.ParagraphConstraints constraints =
new LineBreaker.ParagraphConstraints();
......
for (int paraIndex = 0; paraIndex < paragraphInfo.length; paraIndex++) {
......
//计算行数
LineBreaker.Result res = lineBreaker.computeLineBreaks(
measuredPara.getMeasuredText(), constraints, mLineCount);
......
}
......
}
而LineBreaker中则是调用的native方法
private static native long nComputeLineBreaks(
/* non zero */ long nativePtr,
// Inputs
@NonNull char[] text,
/* Non Zero */ long measuredTextPtr,
@IntRange(from = 0) int length,
@FloatRange(from = 0.0f) float firstWidth,
@IntRange(from = 0) int firstWidthLineCount,
@FloatRange(from = 0.0f) float restWidth,
@Nullable float[] variableTabStops,
float defaultTabStop,
@IntRange(from = 0) int indentsOffset);
2.2、拿到了行数之后,就可以用个for循环来一行一行的处理了。每一行,都需要根据上一行的位置来计算这一行的ltop、lbottom、lbaseline属性,并根据对齐方式、缩进等信息计算x、y锚点。
@UnsupportedAppUsage
public void drawText(Canvas canvas, int firstLine, int lastLine) {
......
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
//一行一行绘制
int start = previousLineEnd;
previousLineEnd = getLineStart(lineNum + 1);
final boolean justify = isJustificationRequired(lineNum);
//去掉换行符和空格,只保留文字
int end = getLineVisibleEnd(lineNum, start, previousLineEnd);
paint.setStartHyphenEdit(getStartHyphenEdit(lineNum));
paint.setEndHyphenEdit(getEndHyphenEdit(lineNum));
//当前行的top = 上一行的bottom
int ltop = previousLineBottom;
//当前行的bottom = 下一行的top
int lbottom = getLineTop(lineNum + 1);
previousLineBottom = lbottom;
//baseline = 当前行的bottom-当前行的top
int lbaseline = lbottom - getLineDescent(lineNum);
......
canvas.drawText(buf, start, end, x, lbaseline, paint);
......
}
TextLine.recycle(tl);
}
3、当一行文本中插入了扩大文本大小的Span时,如何计算整行的高度?如何定位没有被修改大小属性的文本绘制位置?
3.1、我们在源码跟踪中说过,复杂的文本会交给TextLine来处理,在TextLine#handleRun()方法中会处理MetricAffectingSpan、CharacterStyle这种会改变文本属性的Span,而这里会调用Span的子类中的updateDrawState(wp)方法来修改TextPaint中保存的文本大小等绘制属性。例如,AbsoluteSizeSpan中重写的updateDrawState()方法,修改了TextSize。
@Override
public void updateDrawState(@NonNull TextPaint ds) {
if (mDip) {
ds.setTextSize(mSize * ds.density);
} else {
ds.setTextSize(mSize);
}
}
3.2、在绘制文本前,会先调用expandMetricsFromPaint()方法更新一下FontMetricsInt信息,
private static void expandMetricsFromPaint(FontMetricsInt fmi, TextPaint wp) {
final int previousTop = fmi.top;
final int previousAscent = fmi.ascent;
final int previousDescent = fmi.descent;
final int previousBottom = fmi.bottom;
final int previousLeading = fmi.leading;
wp.getFontMetricsInt(fmi);
updateMetrics(fmi, previousTop, previousAscent, previousDescent, previousBottom,
previousLeading);
}
一段文本可能存在有的字体大,有的小,为了不让大的文字发生截断,对于上边界(top、ascent)会取最小值,对于下边界(descent、bottom、leading)会去、取最大值,这样保证最终这一行的高度是按照最大字体的来计算的。
static void updateMetrics(FontMetricsInt fmi, int previousTop, int previousAscent,
int previousDescent, int previousBottom, int previousLeading) {
fmi.top = Math.min(fmi.top, previousTop);
fmi.ascent = Math.min(fmi.ascent, previousAscent);
fmi.descent = Math.max(fmi.descent, previousDescent);
fmi.bottom = Math.max(fmi.bottom, previousBottom);
fmi.leading = Math.max(fmi.leading, previousLeading);
}
3.3、这里我们一定要注意上述规则,在需要处理图文混排,或者需要让文本下对齐等ui时,需要对FontMetrics进行修改。
www.jianshu.com/p/cae5e04cc…
4、如果存在Span修改了文本字体,如何计算混合中的换行?
jaeger.itscoder.com/android/201…
有Span的text,创建的是DynamicLayout,在DynamicLayout#generate()中调用DynamicLayout#reflow()
,对存在span的一行单独创建新的StaticLayout,在StaticLayout#generate()方法里计算换行out
out