Android的view到底是怎么绘制出来的

我们都知道,在android中,一个Activity对应一个PhoneWindow,而在PhoneWindow中包含一个DecorView,并且这个DecorView就是页面的顶级视图,DecorView又分为TitleActionBar和contentView,我们平常所填充的布局一般都是在content中,好,这个是大的背景知识。

我们写了很多代码后也都会发现,其实在android中,每一个布局或者视图空间都是继承View实现的,这些控件或者说布局,都是通过View的绘制机制和流程才能在手机屏幕上显示。不管是看书也好还是看网上的资料也好,我们都会被告知说android系统中一个View的成功绘制过程都必须经过measure、layout和draw三个部分。

缘起

我们今天就来阅读源码来看看这三步到底是怎么走的。

既然绘制是按照measure,layout, draw三步走的,那有个疑问是measure又是从哪过来的呢?我们这里先给出答案:

整个View的视图绘制过程,是从ViewRootImpl类中的performTraversals()开始的,performTraversals有上千行代码,主要是根据之前设置的状态和标记,来判断是否要重新Measure,layout和draw视图。我们看看这里的核心源码。

1
2
3
4
5
6
7
8
9
10
11
12
private void performTraversals() {
......
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
performDraw();
......
}

可以看到,核心的过程就是根据各种状态和条件来进行相关的绘制流程,关键代码就是上面所示,我们再看看上面列的几个的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// window不能重新改变大小,必须和窗口大小一样。
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window 能更改大小,最大可以到root view的大小。
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window 需要指定大小。并且将root view 设为这个大小.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

这个方法主要是用来测量root view的大小的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
final View host = mView;
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
......
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
......
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

private void performDraw() {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw");
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

所以看到这里,大致的流程我们就可以掌握了。

从ViewRoorImpl的performTraversals()方法开始,然后通过状态标记的判断,进行performMeasure,performLayout,performDraw,分别调用view的measure, layout, draw方法进行视图的绘制。

发展

既然从performTraversals开始分析到了measure, layout, draw,那我们现在就来详细看看这三个方法。

measure分析

在上文讲到,在performMeasure中会调用view的measure方法。我们看看measure的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this); //当前的View控件是一个ViewGroup并且这个ViewGroup的边界模式是最佳的(刚刚好盖住一个子内容)
if (optical != isLayoutModeOptical(mParent)) { //如果当前ViewGroup和父view的模式不一样,就裁剪一下。
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}

// 将宽高存在一个64位long 数据中,低32位的与运算是去掉高32的符号
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {

// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

resolveRtlPropertiesIfNeeded();

int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
......
}

mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;

mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

这个方法主要是用来确定这个view到底应该多大,并且由父view提供宽高参数的限制大小的信息。也就是说每个View的实际宽高都是由父view和自身决定的。这个类是final的,也就是不能继承重写,实际的测量
是在第28行 onMeasure中实现的,自定义的控件或者view视图都是在onMeasure中实现,这个方法是可以重写的。

我们看一下measure有两个参数,是由父视图传过来的,是父对子的一个限制信息。这个spec的int值分为两个部分,高2位表示MODE,所以最多能表示4中MODE,而实际上在MeasureSpec类中只定义了三种,即

  1. MeasureSpec.EXACTLY 指定了确定的大小
  2. MeasureSpec.AT_MOST 最大的大小
  3. MeasureSpec.UNSPECIFIED 未指定的大小

低30位表示size,也就是父view的大小。对于DecorView的mode,一般都是MeasureSpec.EXACTLY, 而size是屏幕的宽高。对于子view来说,宽高就是父和自己一起决定的。

我们看看onMeasure的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

我们可以看看这段注释,其实写的很清楚了,就是测量视图的大小。这个方法是可以重写的。其中setMeasuredDimension是一个final方法,通过调用setMeasuredDimensionRaw来保存测量到的宽高值。

1
2
3
4
5
6
7
8
9
10
11
12
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;

measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

setMeasuredDimension能够通过setMeasuredDimensionRaw对mMeasuredWidth和mMeasuredHeight变量赋值。我们measure的主要目的就是对View树中的每个View的mMeasuredWidth和mMeasuredHeight进行赋值,所以当这两个变量被赋值意味着该View的测量工作就全部完成。

在这里我们还需要注意一个方法,在onMeasure中的setMeasuredDimension中传入的参数,是通过getDefaultSize获取的一个默认大小。我们看看getDefaultSize。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

可以看到,specMode等于AT_MOST或EXACTLY就返回specSize,等于UNSPECIFIED就返回size。这些就是系统默认的尺寸。
回头看onMeasure方法,其中getDefaultSize参数的widthMeasureSpec和heightMeasureSpec都是由父View丢过来的。getSuggestedMinimumWidth与getSuggestedMinimumHeight都是View的方法,我们可以看看。

1
2
3
4
5
6
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

这两个方法分别是返回建议视图需要使用的最小宽高,并且这两个宽高是由背景尺寸和设置的最小宽(或高)共同决定的。

如果一个view不再是ViewGroup,那么通过这一步就可以得到view的测量的大小,如果是viewGroup,还需要进一步测量子view的大小。

当一个view是一个viewGroup,那么会走到measureChildren方法,主要就是通过一个循环来要求所有children测量自己的大小。

1
2
3
4
5
6
7
8
9
10
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

可以看到,当中还传入了一个widthMeasureSpec和heightMeasureSpec。这就是对这个子view的宽高的要求。我们继续看到measureChild.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

我在这里列出了两个方法,分别是measureChild和measureChildWithMargins。其实这里的逻辑很简单,两者的区别就是measureChildWithMargins在测量时除了父视图提供的measureSpec参数外还会把margin以及
padding也考虑在内。而measureChild只考虑了padding。除此之外,这两个方法干的事情差不多,都是通过getChildMeasureSpec调整child的宽高的easureSpec,然后调用child视图的measure方法,也就是我们
之前分析的方法,去测量自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父视图传来的mode和size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

//计算size减去padding的大小,如果小于0就返回0
int size = Math.max(0, specSize - padding);

//最后返回的结果值。
int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// 父视图指定了一个准确的大小
case MeasureSpec.EXACTLY:
if (childDimension >= 0) { //说明开发者在xml文件或者java中设置了一个具体的大于等于0的大小值,所以将这个值设为结果,并且mode为EXACTLY.
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 子视图就设为当前值
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 子视图自己决定大小,但是最大是当前这个值。所以mode设为AT_MOST, 大小设为size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//以下两种case的逻辑思路和上述的默认case差不多,这里就不再赘述了,大家看代码应该很容易理解。
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;

}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

通过上面的代码我们很容易知道,getChildMeasureSpec做的事情就是,对于传给特定子view的MeasureSpec解析出specMode和specSize,然后根据不同的mode进入不同的case,通过子view的宽高大小来计算自身正确的MeasureSpec,也就是说对这个view的宽高MeasureSpec进行调整和修改。

这里计算出的所有测量的结果,都是onMeasure的参数

讲到这里,我们从上面的分析可以看到,最终决定View的测量大小是View的setMeasuredDimension方法,所以我们在自定义view的时候可以直接通过setMeasuredDimension设定一个大小值来设置View的mMeasuredWidth和mMeasuredHeight的大小,但是这样缺少了灵活性,因此还是要尽量避免这种写法。

另外我们还可以发现,当通过setMeasuredDimension方法最终设置完成View的measure之后View的mMeasuredWidth和mMeasuredHeight才会有真实的数值,所以如果当一个View想通过getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后使用才能返回非0的值。

那么回过头回到measureChild和measureChildWithMargins中,在通过getChildMeasureSpec对传入的measureSpec做出调整修改后,就会
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)进行正确的一个测量过程。

measure小结

好了,贴了这么多代码,现在来小结一下,其实在测量过程一句话概括就是从顶层父View向子View递归调用view.measure方法,每次measure方法通过onMeasure方法进行MeasureSpec的计算。
几个比较重要的地方就是:

  1. MeasureSpec 一个int值。高2位模式specMode和低30位尺寸specSize组成。其中specMode有三种值:
  2. MeasureSpec.EXACTLY 指定了确定值,父View希望子View的大小是确定的,由specSize决定
  3. MeasureSpec.AT_MOST 最大模式,父View希望子View的大小最多是specSize指定的值
  4. MeasureSpec.UNSPECIFIED 未指定,父View完全依据子View的自己值来决定

当然,

  1. 另外还需要注意的是,View.measure方法是final的,不能重写,view子类只能通过重写onMeasure实现自己的测量计算逻辑。
  2. 最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的,也即是LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize是屏幕大小。
  3. ViewGroup类需要通过一个for循环对所有的children 视图进行逐一measure。
  4. ViewGroup的子类的LayoutParams必须继承MarginLayoutParams。
  5. View的布局大小由父View和子View共同决定。
  6. 使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流之后被调用才能得到想要的结果。

layout分析

在最初的背景缘起中我们看到了ViewRootImpl中的performLayout负责view的layout步骤。

实际上layout的过程和measure有点类似,也是从顶层view开始一步步的往下递归,其实也就是从viewGroup一直layout到view为止的过程。好了,先说这么多,下面我们看看源码。

我们看到layout时,performLayout中的关键一步是host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()),可以看到其中传入了4个参数,分别是左上右下的四个坐标。

我们先说一个结论,View的layout方法和ViewGroupLayout方法略微不太一样。由于ViewGroup是View的子类,我们先看看View的layout方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

//保存layout之前的四个坐标
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

//本质上都是通过setFrame给四个坐标参数进行复制
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//关键方法
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}

mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

其中setOpticalFrame只是对parent和Child的Inset进行一个计算,最后还是调用了setFrame。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
   protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
......
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;

// 保存drawn位
int drawn = mPrivateFlags & PFLAG_DRAWN;

int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

// 将旧位置置为无效
invalidate(sizeChanged);
// 分配新的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

mPrivateFlags |= PFLAG_HAS_BOUNDS;


if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}

if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
// If we are visible, force the DRAWN bit to on so that
// this invalidate will go through (at least to our parent).
// This is because someone may have invalidated this view
// before this call to setFrame came in, thereby clearing
// the DRAWN bit.
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
// 需要显示列表的父view可能因为子view的边界改变而需要重新创建
invalidateParentCaches();
}

// 重置drawn位
mPrivateFlags |= drawn;

mBackgroundSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}

notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}

可以看到,在setFrame中始终是返回了changed,而changed就是根据新位置和旧位置的不同得出的。这样就可以在Veiw.layout中进行的适当的调用onLayout方法
以及位置更改的回调。记得这里的layout是可以重写的。
我们再看看View中的onLayout方法。

1
2
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

看到没,View中的onLayout方法是一个空方法,虽然是在layout中调用,但是需要在自定义View时自己去实现。参数就是一个changed指明当前是否是新的位置或者大小,然后需要一个相对父view的
四个角的位置坐标。

我们再看看ViewGroup的layout方法。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}

我们可以看到ViewGroup的layout方法是final的,也就是子类不能重写,其中super.layout方法是走到了View.layout方法。
接下来看看ViewGroup的onLayout方法。

1
2
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

什么情况? ViewGroup的onLayout方法是抽象方法,需要在子类ViewGroup中去实现。所一个可以预期的流程就是在一个自定义的ViewGroup中,onLayout需要和onMeasure一起实现视图的布局过程。
当一个自定义的view通过onMeasure获得了自身和子视图的宽高大小后,就通过onLayout来进行布局,主要是对children的位置布局等一一进行摆放,一般是一个for循环进行。我们可以看看一个具体的实
现,我们知道LinearLayout是直接继承ViewGroup的,我们看看它是如何实现父类的抽象方法onLayout的。

1
2
3
4
5
6
7
8
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}

这里的逻辑很简单,我们平常其实也是这么用的,在使用LinearLayout时必须要在xml中指定它的orientation,就是在这里进行一个判断。
其实这水平和竖直的onLayout也差不多,我们看看我们平常用的比较多的竖直线性布局吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;

int childTop;
int childLeft;

// 计算子view在右边界的位置
final int width = right - left;
int childRight = width - mPaddingRight;

// 减去左右两边的padding后子view可以利用的空闲空间
int childSpace = width - paddingLeft - mPaddingRight;

final int count = getVirtualChildCount(); //实际是调用getChildCount方法返回的mChildrenCount,也就是这个group中子view的个数

final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
//根据majorGravity计算childTop,也就是子view的竖直的起始位置
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;

// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;

case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
//for循环遍历子view进行位置的摆放布局
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//当前view为子view,此处获得了在measure过程中得到的宽高大小信息
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();

final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
//计算左边的起始位置
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;

case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;

case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}

//可能有divider的, 也要考虑进去
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}

childTop += lp.topMargin;
//熟悉的setFrame,在这里完成位置的设置和摆放
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

i += getChildrenSkipCount(child, i);
}
}
}

以上就是LinearLayout的onLayout的一个竖直布局的layoutVertical的过程,可以看到,主要就是对传入的四个参数(左上右下)四个位置的处理,通过一个for循环遍历ViewGroup中所有的子view,然后
完成各个view的位置设置。实际上中间有获取在onMeasure中测量得到的宽高信息,但是在这里并不是必须的,比如一些自定义的view的大小位置是固定的情况下。

为什么说到这里就完成了布局了呢?我们看layoutVertical方法的第77行,setChildFrame方法。

1
2
3
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}

通过这一步就回到了View的layout方法,然后走到onLayout方法,这四个参数就是在layoutVertical里丢过来的左上右下四个位置坐标。就是沿着我们之前的分析了。

layout小结

Layout步骤就到这儿了,逻辑还是比measure过程要简单一点的。整个过程是比较容易理解的。
整个过程也是和measure一样,从顶层父View向子view递归调用view的layout方法,每一层将子view放在合适的位置上。

回顾起来尤其是在我们自定义view的实现时需要注意以下几个方面:

  1. View的layout方法可被重写,但是ViewGroup的layout是final的不能被重写,ViewGroup.onLayout为abstract的,子类必须重写实现自己的布局逻辑。
  2. layout操作完成之后得到的是对每个View进行位置摆放布局后的左上右下四个坐标,这些都是相对于父View的。
  3. 在xml等设置的layout_xx的布局属性都是的是包含当前子View的ViewGroup的设置,对自己以及对没有父view的view是没有意义的。

draw分析

现在我们可以进行第三步了,draw过程。

我们在本文最初有提到过,在ViewRootImpl中的performTraversals方法里,会逐步调用measure, layout, draw的步骤。关于measure, layout我们已经讲的差不多了,第三步就是draw,在
performTraversals里是走到了performDraw执行canvas的绘制。

关于View的绘制,详细的过程可以参阅 方立的博客, 方立秉持了处女座的追究细节的特点,
讲的很细,我这里就不很详细讲了,只大致讲一下过程吧。

在performDraw中调用了draw(boolean fullRedrawNeeded)方法。

1
2
3
4
5
6
7
8
9
10
private void draw(boolean fullRedrawNeeded) {
Surface surface = mSurface;
......
final Rect dirty = mDirty;
......
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
......
}

我们看到,在draw中关键一步是调用了ViewRootImpl的drawSoftware方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
final Canvas canvas;
try {
final int left = dirty.left;
final int top = dirty.top;
final int right = dirty.right;
final int bottom = dirty.bottom;

canvas = mSurface.lockCanvas(dirty);
......
}......
canvas.translate(-xoff, -yoff);
......
mView.draw(canvas);
......
surface.unlockCanvasAndPost(canvas);
......
}

我们看到,在drawSoftware中调用了View的draw(Canvas canvas)方法。需要指出的是,在View中有两个draw方法,分别是

  • draw(Canvas canvas)
  • draw(Canvas canvas, ViewGroup parent, long drawingTime)。

在drawSoftware中走到的实际是draw(Canvas canvas),另一个我们过会再看。
draw(Canvas canvas)的话,主要做的事情就是手动将一个已经完成了layout的view画到给定的canvas上去,在自定义view时一般不需要重写这个方法,而是用onDraw重写。

网上已经讨论了很多了,这里的draw主要分为6步。

  1. 画背景
  2. 如果必要的话,保存canvas的图层
  3. 画当前view的内容
  4. 画子view
  5. 如果必要的话,画渐弱边缘以及恢复图层,这一步主要是和第二步对应。
  6. 画一些装饰view,比如滚动条等。

其中2和5不是必需的

第三步画自身的内容,是通过onDraw(canvas)完成的。
第四步画子view,是通过dispatchDraw(canvas)完成的。

1
2
3
4
5
6
7
protected void onDraw(Canvas canvas) {

}

protected void dispatchDraw(Canvas canvas) {

}

可以看到View的onDraw是空方法,需要在自定义view时自己去实现。
dispatchDraw也是一个空方法,需要子类自己实现,例如ViewGroup.

那我们看看ViewGroup的dispatchDraw方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
protected void dispatchDraw(Canvas canvas) {
......
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
// Draw any disappearing views that have animations
if (mDisappearingChildren != null) {
......
for (int i = disappearingCount; i >= 0; i--) {
......
more |= drawChild(canvas, child, drawingTime);
}
}
......
if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) {
invalidate(true);
}
......
}

我们看一下ViewGroup的关键代码,主要是一个for循环遍历了所有的child视图,并且调用drawChild(canvas, child, drawingTime)方法进行每个子view的绘制。我们跟着这个走下去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Draw one child of this View Group. This method is responsible for getting
* the canvas in the right state. This includes clipping, translating so
* that the child's scrolled origin is at 0, 0, and applying any animation
* transformations.
*
* @param canvas The canvas on which to draw the child
* @param child Who to draw
* @param drawingTime The time at which draw is occurring
* @return True if an invalidate() was issued
*/

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}

可以看到,drawChild调用了View的draw方法,但是这里调用的View的draw方法还不是我们之前分析的draw方法,而是重载的一个draw。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
*
* This is where the View specializes rendering behavior based on layer type,
* and hardware acceleration.
*/

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
......
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
}
......
}

在这个draw方法中,主要是干了两件事,一件事是对画布canvas中做了调整,多次调用了translate方法进行移动和裁剪,然后如果子视图还是ViewGroup那就走到dispatchDraw,
如果子视图是View,那就走到View的draw(canvas)方法。

当然,这里的canvas都是ViewRootImpl中传过来的

这个draw方法只能是由ViewGroup来调用,所以本质上最后还是要走到draw(canvas)方法,然后调用onDraw方法,在传入的canvas上进行绘制。

好了,draw的过程到这儿也算大致讲清楚了。之前有说,如果对具体的canvas等绘制的过程也很感兴趣,可以看看
方立的博客 的博客阅读。

draw小结

其实这样分析下来,发现整个的逻辑流程和measure和layout是差不多的。都是从父view开始一层层往子view递归进行绘制。那么我们可以总结出什么呢?

  1. View本身不进行绘制,绘制的内容是要在子view中进行实现的。
  2. View动画和ViewGroup动画并不是一回事,View动画是自身的动画,而ViewGroup动画是显示子元素时的动画,是layoutAnimation.

好了,明白了Android上的view的绘制,才能方便我们更好的去在自定义view以及一些布局的处理上有着更好的理解和实现,并且对理解其他相关机制有着更好的掌握。