整体流程
onMeasure -> onLayout -> onDraw
onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
...
if (widthMode == MeasureSpec.EXACTLY) {
// 精确模式,以父容器给的宽度作当前TextView的宽度.
width = widthSize;
} else {
if (mLayout != null && mEllipsize == null) {
//调用desired方法计算期望值,如果行数大于1就返回-1,否则返回单行宽度
des = desired(mLayout);
}
//小于0,即行数大于1行,就去判断是否是boring
if (des < 0) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
} else {
fromexisting = true;
}
...
//加上Drawable的宽度
final Drawables dr = mDrawables;
if (dr != null) {
width = Math.max(width, dr.mDrawableWidthTop);
width = Math.max(width, dr.mDrawableWidthBottom);
}
//计算hint的宽度
if (mHint != null) {
...
}
//加上padding边距
width += getCompoundPaddingLeft() + getCompoundPaddingRight();
...
//最后根据上面计算得到的size-padding的值就是我们单行text实际可以展示的大小
int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
if (mLayout == null) {
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(),false);
}else {
...
}
//接下来高度计算,和上面的宽度计算类似
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
//如果设置了Drawable的话,比较2个值得大小,取大的
//如果设置了maxLines或者maxHeight计算下当前高度有没超过最大高度,超过的话取最大高度
//如果设置了minLines或者minHeight的话,比较下当前高度和最小高度,取小的
int desired = getDesiredHeight();
...
}
...
}
}
1、MeasureSpec是一个specMode和specSize信息的32位int值。其中高两位为测量模式specMode,低30位为测量值specSize。测量模式有UNSPECIFIED、EXACTLY、AT_MOST三种。一个View的测量模式由父容器和其自身的LayoutParams共同决定。
一般来说,View测量模式对应的情况如下
EXACTLY:
- View本身设置为固定值,例如20dp/px
- 父容器为EXACTLY,View本身为match_paret
AT_MOST:
- 父容器为AT_MOST,View本身为wrap_content或match_parent
- 父容器为EXACTLY,View本身为wrap_content
UNSPECIFIED:
- 父容器为UNSPECIFIED,View本身为wrap_content或match_parent
2、makeNewLayout为布局的创建入口,在这个方法中,主要进行跑马灯、省略号、HintLayout的处理。核心方法还是调用makeSingleLayout
。通过它,进行判断,选择创建BoringLayout、StaticLayout、DynamicLayout中的一种。具体创建过程可以看前面的文章。
onLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mDeferScroll >= 0) {
int curs = mDeferScroll;
mDeferScroll = -1;
bringPointIntoView(Math.min(curs, mText.length()));
}
// Call auto-size after the width and height have been calculated.
autoSizeText();
}
layout过程,执行方法bringPointIntoView
,根据前面onMeasure确定的各种LayoutParam参数,进行计算确定在布局中的位置。autoSizeText
会在设置了自动调节字体大小(mNeedsAutoSizeText为true)时,自动计算和调节字体大小。
onDraw
TextView#onDraw方法中会有各种获取行数据信息的方法,它们就会访问的前面组织好的数据。
@Override
protected void onDraw(Canvas canvas) {
final int compoundPaddingLeft = getCompoundPaddingLeft();
final int compoundPaddingTop = getCompoundPaddingTop();
final int compoundPaddingRight = getCompoundPaddingRight();
final int compoundPaddingBottom = getCompoundPaddingBottom();
...
if (mEditor != null) {
mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
} else {
layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
}
...
}
绘制之前,取出各个padding,确定绘制区域的范围和坐标。
若mEditor不为null,走Editor#onDraw方法,否则调用Layout#draw。跟踪Editor代码,可以知道onDraw还是会调用Layout#draw或者直接在硬件加速状态下执行drawBackground和drawText方法。
Layout的draw方法会执行drawBackground和drawText方法,前者绘制背景,后者调用TextLine对象,进行逐行绘制。
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;
drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
firstLine, lastLine);
drawText(canvas, firstLine, lastLine);
}
public void drawText(Canvas canvas, int firstLine, int lastLine) {
...
//遍历,一行行绘制
for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) {
...
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify){
// XXX: assumes there's nothing additional to be done
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
//设置该行的各种参数,包括起始位置、结束位置、span的个数和位置
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);
}
}
}
TextLine#draw
void draw(Canvas c, float x, int top, int y, int bottom) {
...
h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom,
runIndex != (runCount - 1) || j != mLen);
...
}
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);
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);
}
drawRun方法会调用handleRun,这里面会获取你设置的Span的样式,比如是否显示下划线、链接文字颜色等。最后会通过TextLine的drawTextRun方法绘制,显示最终的效果。