View绘制过程
抄的《Android开发艺术探索》第四章ViewRoot
对应于ViewRootImpl
类,它是连接WindowManager
和DecorView
的纽带,View
的三大流程均是通过ViewRoot
来完成的。在ActivityThread
中,当Activity
对象被创建完毕后,会将DecorView
添加到Window
中,同时会创建ViewRootImpl
对象,并将ViewRootImpl
对象和DecorView
建立关联,View
的绘制流程是从ViewRoot
的performTraversals
方法开始的,它经过measure
、layout
和draw
三个过程才能最终将一个View
绘制出来,其中measure
用来测量View的宽和高,layout
用来确定View在父容器中的放置位置,而draw
则负责将View绘制在屏幕上。
performTraversals
会依次调用performMeasure
、performLayout
和performDraw
三个方法,这三个方法分别完成顶级View
的measure
、layout
和draw
这三大流程,其中在performMeasure
中会调用measure
方法,在measure
方法中又会调用onMeasure
方法,在onMeasure
方法中则会对所有的子元素进行measure
过程,这个时候measure
流程就从父容器传递到子元素中了,这样就完成了一次measure
过程。接着子元素会重复父容器的measure
过程,如此反复就完成了整个View
树的遍历。同理,performLayout
和performDraw
的传递流程和performMeasure
是类似的,唯一不同的是,performDraw
的传递过程是在draw
方法中通过dispatchDraw
来实现的,不过这并没有本质区别。
measure
过程决定了View
的宽/高,Measure
完成以后,可以通过getMeasuredWidth
和getMeasuredHeight
方法来获取到View
测量后的宽/高,在几乎所有的情况下它都等同于View
最终的宽/高,但是特殊情况除外,这点在本章后面会进行说明。Layout
过程决定了View
的四个顶点的坐标和实际的View的宽/高,完成以后,可以通过getTop
、getBottom
、getLeft
和getRight
来拿到View
的四个顶点的位置,并可以通过getWidth
和getHeight
方法来拿到View
的最终宽/高。Draw
过程则决定了View
的显示,只有draw
方法完成以后View
的内容才能呈现在屏幕上。
DecorView
作为顶级View
,一般情况下它内部会包含一个竖直方向的LinearLayout
,在这个LinearLayout
里面有上下两个部分(具体情况和Android版本及主题有关),上面是标题栏,下面是内容栏。在Activity
中我们通过setContentView
所设置的布局文件其实就是被加到内容栏之中的,而内容栏的id是content
,因此可以理解为Activity指定布局的方法不叫setview而叫setContentView
,因为我们的布局的确加到了id
为content
的FrameLayout
中。如何得到content
呢?可以这样:ViewGroup content= findViewById(R.android.id.content)
。如何得到我们设置的View
呢?可以这样:content.getChildAt(0)
。同时,通过源码我们可以知道,DecorView
其实是一个FrameLayout
,View
层的事件都先经过DecorView
,然后才传递给我们的View。
MeasureSpec
MeasureSpec
在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec
的创建过程。在测量过程中,系统会将View的LayoutParams
根据父容器所施加的规则转换成对应的MeasureSpec
,然后再根据这个measureSpec
来测量出View
的宽/高。这里的宽/高是测量宽/高,不一定等于View
的最终宽/高。MeasureSpec
代表一个32位int值,高2位代表SpecMode
,低30位代表SpecSize
,SpecMode
是指测量模式,而SpecSize
是指在某种测量模式下的规格大小。
1 |
|
MeasureSpec
通过将SpecMode
和SpecSize
打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode
和SpecSize
也是一个int值,一组SpecMode
和SpecSize
可以打包为一个MeasureSpec
,而一个MeasureSpec
可以通过解包的形式来得出其原始的SpecMode
和SpecSize
,需要注意的是这里提到的MeasureSpec
是指MeasureSpec
所代表的int值,而并非MeasureSpec
本身。
SpecMode有三类,每一类都表示特殊的含义,如下所示。
** UNSPECIFIED **
父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
** EXACTLY **
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
** AT_MOST **
父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。
MeasureSpec和LayoutParams
在View测量的时候,系统会将LayoutParams
在父容器的约束下转换成对应的MeasureSpec
,然后再根据这个MeasureSpec
来确定View测量后的宽/高。需要注意的是,MeasureSpec
不是唯一由LayoutParams
决定的,LayoutParams
需要和父容器一起才能决定View
的MeasureSpec
,从而进一步决定View的宽/高。另外,对于顶级View
(即DecorView)和普通View
来说,MeasureSpec
的转换过程略有不同。对于DecorView
,其MeasureSpec
由窗口的尺寸和其自身的LayoutParams
来共同确定;对于普通View,其MeasureSpec
由父容器的MeasureSpec
和自身的LayoutParams
来共同决定,MeasureSpec
一旦确定后,onMeasure
中就可以确定View
的测量宽/高。
对于DecorView
来说,在ViewRootImpl
中的measureHierarchy
方法中有如下一段代码,它展示了DecorView
的MeasureSpec
的创建过程,其中desiredWindowWidth
和desiredWindowHeight
是屏幕的尺寸:
1 |
|
通过上述代码,DecorView
的MeasureSpec
的产生过程就很明确了,具体来说其遵守如下规则,根据它的LayoutParams
中的宽/高的参数来划分。
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
- LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
- 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。
对于普通View
来说,这里是指我们布局中的View
,View
的measure
过程由ViewGroup
传递而来,先看一下ViewGroup
的measureChildWithMargins
方法:上述方法会对子元素进行1
2
3
4
5
6protected void measureChildWithMargins(View child,int parentWidthMeasureSpec,int widthUsed,int parentHeightMeasureSpec,int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayout-Params();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed,lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeight-MeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed,lp.height);
child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
}measure
,在调用子元素的measure
方法之前会先通过getChildMeasureSpec
方法来得到子元素的MeasureSpec
。从代码来看,很显然,子元素的MeasureSpec
的创建与父容器的MeasureSpec
和子元素本身的LayoutParam
s有关,此外还和View
的margin
及padding
有关,具体情况可以看一下ViewGroup
的getChildMeasureSpec
方法,如下所示:它的主要作用是根据父容器的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
62public static int getChildMeasureSpec(int spec,int padding,int child-Dimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0,specSize -padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension => 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} 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;
// Parent has imposed a maximum size on us
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;
// Parent asked to see how big we want to be
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 = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize,resultMode);
}MeasureSpec
同时结合View本身的LayoutParams
来确定子元素的MeasureSpec
,参数中的padding
是指父容器中已占用的空间大小,因此子元素可用的大小为父容器的尺寸减去padding
,具体代码如下所示:这里简单说一下,当View采用固定宽/高的时候,不管父容器的1
2int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0,specSize -padding);MeasureSpec
是什么,View
的MeasureSpec
都是精确模式并且其大小遵循Layoutparams
中的大小。当View
的宽/高是match_parent
时,如果父容器的模式是精准模式,那么View
也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么View
也是最大模式并且其大小不会超过父容器的剩余空间。当View
的宽/高是wrap_content
时,不管父容器的模式是精准还是最大化,View
的模式总是最大化并且大小不能超过父容器的剩余空间。在我们的分析中漏掉了UNSPECIFIED
模式,那是因为这个模式主要用于系统内部多次Measure的情形,一般来说,我们不需要关注此模式。
View的工作流程
measure过程
measure过程要分情况来看,如果只是一个原始的View
,那么通过measure
方法就完成了其测量过程,如果是一个ViewGroup
,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure
方法,各个子元素再递归去执行这个流程。
** View的measure过程 **View
的measure
过程由其measure
方法来完成,measure
方法是一个final
类型的方法,这意味着子类不能重写此方法,在View
的measure
方法中会去调用View
的onMeasure
方法,因此只需要看onMeasure
的实现即可:
1 |
|
可以看出,getDefaultSize
这个方法的逻辑很简单,对于我们来说,我们只需要看AT_MOST
和EXACTLY
这两种情况。简单地理解,其实getDefaultSize
返回的大小就是measureSpec
中的specSize
,而这个specSize
就是View
测量后的大小,这里多次提到测量后的大小,是因为View
最终的大小是在layout
阶段确定的,所以这里必须要加以区分,但是几乎所有情况下View
的测量大小和最终大小是相等的。
至于UNSPECIFIED
这种情况,一般用于系统内部的测量过程,在这种情况下,View
的大小为getDefaultSize
的第一个参数size
,即宽/高分别为getSuggestedMinimumWidth
和getSuggestedMinimumHeight
这两个方法的返回值,看一下它们的源码:
1 |
|
这里只分析getSuggestedMinimumWidth
方法的实现,getSuggestedMinimumHeight
和它的实现原理是一样的。从getSuggestedMinimumWidth
的代码可以看出,如果View
没有设置背景,那么View
的宽度为mMinWidth
,而mMinWidth
对应于android:minWidth
这个属性所指定的值,因此View
的宽度即为android:minWidth
属性所指定的值。这个属性如果不指定,那么mMinWidth
则默认为0;如果View
指定了背景,则View
的宽度为max(mMinWidth,mBackground.getMinimumWidth())
。mMinWidth
的含义我们已经知道了,那么mBackground.getMinimumWidth()
是什么呢?我们看一下Drawable
的getMinimumWidth
方法,如下所示:
1 |
|
可以看出,getMinimumWidth
返回的就是Drawable
的原始宽度,前提是这个Drawable
有原始宽度,否则就返回0。
这里再总结一下getSuggestedMinimumWidth
的逻辑:如果View
没有设置背景,那么返回android:minWidth
这个属性所指定的值,这个值可以为0;如果View
设置了背景,则返回android:minWidth
和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth
和getSuggestedMinimumHeight
的返回值就是View
在UNSPECIFIED
情况下的测量宽/高。
从getDefaultSize
方法的实现来看,View
的宽/高由specSize
决定,所以我们可以得出如下结论:直接继承View
的自定义控件需要重写onMeasure
方法并设置wrap_content
时的自身大小,否则在布局中使用wrap_content
就相当于使用match_parent
。
从上述代码中我们知道,如果View
在布局中使用wrap_content
,那么它的specMode
是AT_MOST
模式,在这种模式下,它的宽/高等于specSize
;这种情况下View
的specSize
是parentSize
,而parentSize
是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View
的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent
完全一致。如何解决这个问题呢?也很简单,代码如下所示:
1 |
|
在上面的代码中,我们只需要给View
指定一个默认的内部宽/高(mWidth
和mHeight
),并在wrap_content
时设置此宽/高即可。对于非wrap_content
情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果查看TextView
、ImageView
等的源码就可以知道,针对wrap_content
情形,它们的onMeasure
方法均做了特殊处理。
** ViewGroup的measure过程 **
对于ViewGroup
来说,除了完成自己的measure
过程以外,还会遍历去调用所有子元素的measure
方法,各个子元素再递归去执行这个过程。和View
不同的是,ViewGroup
是一个抽象类,因此它没有重写View
的onMeasure
方法,但是它提供了一个叫measureChildren
的方法,如下所示。
1 |
|
从上述代码来看,ViewGroup
在measure
时,会对每一个子元素进行measure
,measureChild
这个方法的实现也很好理解,如下所示
1 |
|
很显然,measureChild
的思想就是取出子元素的LayoutParams
,然后再通过getChildMeasureSpec
来创建子元素的MeasureSpec
,接着将MeasureSpec
直接传递给View
的measure
方法来进行测量。我们知道,ViewGroup
并没有定义其测量的具体过程,这是因为ViewGroup
是一个抽象类,其测量过程的onMeasure
方法需要各个子类去具体实现,比如LinearLayout
、RelativeLayout
等。
layout过程
Layout
的作用是ViewGroup
用来确定子元素的位置,当ViewGroup
的位置被确定后,它在onLayout
中会遍历所有的子元素并调用其layout
方法,在layout
方法中onLayout
方法又会被调用。Layout
过程和measure
过程相比就简单多了,layout
方法确定View
本身的位置,而onLayout
方法则会确定所有子元素的位置,先看View
的layout
方法,如下所示。
1 |
|
layout
方法的大致流程如下:首先会通过setFrame
方法来设定View
的四个顶点的位置,即初始化mLeft
、mRight
、mTop
和mBottom
这四个值,View
的四个顶点一旦确定,那么View
在父容器中的位置也就确定了;接着会调用onLayout
方法,这个方法的用途是父容器确定子元素的位置,和onMeasure
方法类似,onLayout
的具体实现同样和具体的布局有关,所以View
和ViewGroup
均没有真正实现onLayout
方法。接下来,我们可以看一下LinearLayout
的onLayout
方法,如下所示。
1 |
|
LinearLayout
中onLayout
的实现逻辑和onMeasure
的实现逻辑类似,这里选择layoutVertical
继续讲解,为了更好地理解其逻辑,这里只给出了主要的代码:
1 |
|
这里分析一下layoutVertical
的代码逻辑,可以看到,此方法会遍历所有子元素并调用setChildFrame
方法来为子元素指定对应的位置,其中childTop
会逐渐增大,这就意味着后面的子元素会被放置在靠下的位置,这刚好符合竖直方向的LinearLayout
的特性。至于setChildFrame
,它仅仅是调用子元素的layout
方法而已,这样父元素在layout
方法中完成自己的定位以后,就通过onLayout
方法去调用子元素的layout
方法,子元素又会通过自己的layout
方法来确定自己的位置,这样一层一层地传递下去就完成了整个View
树的layout
过程。setChildFrame
方法的实现如下所示。
1 |
|
我们注意到,setChildFrame中的width和height实际上就是子元素的测量宽/高,从下面的代码可以看出这一点:
1 |
|
而在layout
方法中会通过setFrame
去设置子元素的四个顶点的位置,在setFrame
中有如下几句赋值语句,这样一来子元素的位置就确定了:
1 |
|
View
的测量宽/高和最终/宽高有什么区别?这个问题可以具体为:View
的getMeasuredWidth
和getWidth
这两个方法有什么区别,至于getMeasuredHeight
和getHeight
的区别和前两者完全一样。为了回答这个问题,首先,我们看一下getwidth和getHeight这两个方法的具体实现:
1 |
|
从getWidth
和getHeight
的源码再结合mLeft
、mRight
、mTop
和mBottom
这四个变量的赋值过程来看,getWidth
方法的返回值刚好就是View
的测量宽度,而getHeight
方法的返回值也刚好就是View
的测量高度。经过上述分析,现在我们可以回答这个问题了:在View
的默认实现中,View
的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于View
的measure
过程,而最终宽/高形成于View
的layout
过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。因此,在日常开发中,我们可以认为View
的测量宽/高
就等于``最终宽/高
,但是的确存在某些特殊情况会导致两者不一致.
draw过程
Draw过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循
如下几步:
- 绘制背景background.draw(canvas)。
- 绘制自己(onDraw)。
- 绘制children(dispatchDraw)。
- 绘制装饰(onDrawScrollBars)。
这一点通过draw方法的源码可以明显看出来,如下所示。
1 |
|
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会遍历调用所有子元素的draw方法,如此draw事件就一层层地传递了下去。View有一个特殊的方法setWillNotDraw,先看一下它的源码,如下所示。
1 |
|
从setWillNotDraw
这个方法的注释中可以看出,如果一个View
不需要绘制任何内容,那么设置这个标记位为true
以后,系统会进行相应的优化。默认情况下,View
没有启用这个优化标记位,但是ViewGroup
会默认启用这个优化标记位。这个标记位对实际开发的意义是:当我们的自定义控件继承于ViewGroup
并且本身不具备绘制功能时,就可以开启这个标记位从而便于系统进行后续的优化。当然,当明确知道一个ViewGroup
需要通过onDraw
来绘制内容时,我们需要显式地关闭WILL_NOT_DRAW
这个标记位。
以上