1 ViewRoot和DecorView
每个Activity都有一个Window,每个Window都有一个DecorView,DecorView是一个FrameLayout,一般可能含有一个垂直布局的LinearLayout用来放置标题栏和ContentView(受Activity主题的影响可能没有标题栏,只有ContentView)。
2.MeasureSpec
2.1 什么是MeasureSpec
MeasureSpec是一个int值,但是包含了两部分内容,最高的2位记录了Mode值,剩下的30位记录了Size值。整个MeasureSpec用来表示一个View在onMeasure时应该遵循的规则,当然你也可以任性的选择不遵守。通常一个View的MeasureSpec是由自己所在的ViewGroup的MeasureSpec和自身的LayoutParams来决定的,但是最底层的DecorView则不是这样,它是由ViewRootImpl根据WindowSize和DecorView的LayoutParams来决定的。
Mode的值有三种可能:EXACTLY,AT_MOST和UNSPECIFIED。分别对应以下内容:
- EXACTLY: 父ViewGroup根据自己的MeasureSpec和你的LayoutParams进行分析后认为你应该把MeasureSpec里的Size作为自己的Size,当然你可以在自定义View中选择拒绝,但是系统实现的View应该都遵循了这个规则。
- AT_MOST: 父ViewGroup根据自己的MeasureSpec和你的LayoutParams进行分析后认为你应该把MeasureSpec里的Size作为自己Size的上限,只要不超过这个大小就随你折腾。
- UNSPECIFIED: 父ViewGroup根据自己的MeasureSpec和你的LayoutParams进行分析后认为你爱咋咋地,想要多大就多大。这种情况一般出现在AdapterView(比如ListView)的item的heightMode,或者ScrollView的childView的heightMode。
2.2 MeasureSpec和LayoutParams
上面说过MeasureSpec是由自己所在的ViewGroup的MeasureSpec和自身的LayoutParams来决定的,那么这个关系究竟是怎么样的呢,让我们一探究竟:
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) { //childDimension根据LayoutParams里的layout_width和layout_height设置
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //MATCH_PARENT=-1
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //WRAP_CONTENT=-2
// 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 = 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;
}
}
根据上面的源码,其实这个对应关系很简单:
- parent的大小确定时,如果child有指定大小(即layout_width=”10dp”这种)或者为match_parent(就是说跟parent一样大,也相当于指明大小了),那child就是EXACTLY;如果是wrap_content,也就是最大为parent大小,为AT_MOST。
- parent只有最大值时,如果child有指定大小,仍然遵循其意愿,并给他EXACTLY;如果是match_parent或者wrap_content的话,还是只能确定一个最大值,并不能确定准确的值,所以都是AT_MOST。
- parent自己都不确定时,仍然还是如果child有指定大小,遵循其意愿,给EXACTLY。其他情况,连parent自己都不知道怎么办呢,也只能给个UNSPECIFIED了。。。
所以看到这里也就很好理解说为什么在onMeasure时如果是EXACTLY就直接把MeasureSpec的Size当作实际的Size:显然是因为之前计算MeasureSpec时child自己指定size了,所以你就老实拿来用就好了嘛,毕竟是自己出的主意。
3 View的工作流程
一个View从构造函数到最后显示,毫无疑问有三个方法最为重要:measure,layout,draw。measure决定了这个View的宽高,layout决定了这个View在parent中的位置,draw决定了这个view好不好看。。。下面分别说说。
3.1 measure
measure本身是View中定义的final方法,无法被重写,其中定义了一系列measure的流程,但是在其中调用的onMeasure这个方法才是真正开始测量,所以我们重写这个方法就可以。View中的onMeasure已经实现了最基本的测量逻辑,EXACTLY和UNSPECIFIED都考虑到了,但是AT_MOST的处理和EXACTLY一样,这就意味着wrap_content的效果和match_parent一样,所以如果我们自定义View的话,务必要重写AT_MOST时的size。
ViewGroup的测量逻辑则要复杂一些。它需要先测量children的size,再由children的size根据一定的逻辑计算得出自己的size,自定义ViewGroup的话还需要考虑child的margin和自身的padding,否则最后得出的size一定不对。
在onMeasure结束后,我们需要把测量结果通过setMeasuredDimension方法进行设置,不然measure方法会报错的。
3.1.1 获取View宽高的方法
- 在onWindowFocusChanged中获取,关于onWindowFocusChanged的调用时机见问题2。
- View.post(Runnable)方法。Runnable会被加到MessageQueue的队尾,当执行到这里时,View也已经初始化好了。
- 利用ViewTreeObserver。给View的ViewTreeObserver添加OnGlobalLayoutListener。
- 手动调用View.measure()方法。此处有两个问题,见问题3和4。
3.2 layout
layout方法本身虽然可以被重写,但如果我们只关心布局位置的话,重写onLayout就可以了。而且对于view来说,这个方法不要管,只有当重写ViewGroup时,才需要管管自己children的布局。在ViewGroup中,通过调用child.layout(参数们…)来为child设定在ViewGroup中的位置,最终每个child得到一块矩形来绘制图案。
3.3 draw
draw方法本身遵循一套流程:
- 绘制背景 background.draw(canvas)
- 绘制自己 onDraw(canvas)
- 绘制children(dispatchDraw())
- 绘制Decoration(ScrollBars)
另外作者在文中提到了setWillNotDraw(boolean)这个方法,通过它能改变View内部的一个标志位。方法的意义在于如果View不具备绘制功能且标志位为true时,系统将进行相应优化。一般来说View都是需要绘制的,而ViewGroup一般不用,所以ViewGroup的标志位默认为true。但是如果你重写了ViewGroup的onDraw()方法,那么你可以通过setWillNotDraw(false)来关闭这个标志位。
问题
1.adjustViewBounds的用法
关于这一点网上很多帖子似乎写的都有点问题。我特意另外写了篇博客记录了一下,戳我查看
2.p190,onWindowFocusChanged的调用时机
从名字来看,onWindowFocusChanged在activity所在的window得到或失去焦点时会被触发。那么window什么时候会得到或失去焦点呢?
- 显示一个Activity时,新Activity的window会获得焦点,具体来说是Activity的onResume,View的measure,layout之后,draw之前调用。所以一般可以在onWindowFocusChanged方法里获取View的MeasuredSize。
- 显示一个Activity时,旧Activity的window会失去焦点,具体来说是onPause之后,onStop之前调用。
- 下拉系统的通知面板时,当前Activity的window会失去焦点。此时onPause并不会调用。
- 弹出dialog或者PopupWindow时,当前Activity的window会失去焦点。此时onPause并不会调用。
另外画面中Focus的转移不会对WindowFocus造成影响。
3.p192最下,作者说wrap_content的View可以手动调用measure方法得到size?
我认为如果一个View的layout是wrap_content的话,用measure方法应该是无法得到最终显示时的size的。作者在示例代码中用2^30-1作为View在measure时的最大值显然并不符合View在实际测量时的情况,一旦parent所剩空间较小,实际测量得到的size肯定会受到parent的限制,最终的值很有可能小于直接手动调用measure方法得到的值。当然为了证实我的想法,我也稍微写了个测试跑了一下,而结果也确实像我所说的。
4.p193,为什么说用unspecified去measure不能保证结果一定正确
当View在XML中直接指定了size时,用unspecified模式去measure这种View应该是没问题的,因为指定尺寸得到的MeasureMode一般都是Exactly,会直接用XML中指定的size作为最终的size。但是如果View的size是match_parent或者wrap_content,由于真正在测量的时候会受到父View尺寸的限制,或者被同级View挤占空间,所以真正测算出的Size应该是和直接用unspecified去测量不同。