前言
这篇文章会抛弃所有的细枝末节,聚焦在View的Measure过程上,因为这一部分是最难懂,也是最容易思维混乱的地方,由于作者水平有限,文中如果有描述错误,或者不严谨的地方,还请指正,不甚感激。
理解MeasureSpec
MeasureSpec决定了一个View的尺寸,而MeasureSpec受到父ViewGroup和自己的layoutParams的影响
MeasureSpec
MeasureSpec高2位代表SpecMode,低30位代表SpecSize(某种测量模式下的大小)
Api
1 | public static int makeMeasureSpec(int size,int mode) //打包方法 |
2 | public static int getMode(int measureSpec) // 解包方法 |
3 | public static int getSize(int measureSpec) |
SpecMode
1.UNSPECIFIED
父容器不对View有任何限制,要多大给多大,你不要限制我。一般开发者几乎不需要处理这种情况,在ScrollView或者AdapterView中都会处理这样的情况,所以我们可以忽视它,本文中的实例,基本都会跳过它。
2.EXACTLY
父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSIze所指定的值,它对应于LayoutParams中的match_parent和具体的数值这两种模式
3.AT_MOST
父容器指定了一个可用大小即SpecSIze,View大小不能大于这个值,具体是什么值要看不同View的具体实现,它对应于LayoutParams中的wrap_content
MeasureSpec 和 LayoutParams 的对应关系
我们不能直接指定View的MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams,在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高,后面会给出一张表格图。
如何确定一个View的大小
关于这个问题,之前我一直很困惑,其实就是思路不清,View既可以是最下层的View,也可以是ViewGroup
先给出一些结论,便于我们后面阅读源码
不论待测量的是View还是ViewGroup,都满足以下通用的测量规律,以下View泛指包括ViewGroup
在onMeasure中获取到的MeasureSpec有以下几种情况,它们对应的View的测量结果分别如下:
1.SpecMode = EXACTLY :View的最终大小确定等于SpecSize
2.SpecMode = AT_MOST:View的大小需要进行处理之后得出,至于如何处理,就要细分为View和ViewGroup了
3.SpecMode = UNSPECIDFIED:View的大小不受父ViewGroup的约束,想多大就多大,比如ScrollView(少见)
需要注意的是,我们说的所有规律,都是建立在合理的测量的基础上,实际上,不管父ViewGroup传来的是什么鬼,你高兴在onMeasure里面返回啥就是啥,但是这就打乱了正常的测量规律,这么做可以,但是没有必要。因为源代码实际上也是先有普遍规律,再有代码,所以代码要遵循逻辑。
接下来我们分析第二条,从上面可以看出,第一种情况很简单,因为没有分支情况需要我们来考虑,而对于第二种AT_MOST而言,我们如何来得到它的测量值呢?下面我们先分别给出几个简要的结论
1.View
在默认情况下,AT_MOST模式下,VIEW的大小等于父View的大小(这个后面源码分析),我们要想得到我们想要的合适的宽高,就需要对这种情况进行重写,要么给出一个固定值,一般是根据内容的多少来确定它的大小
2.ViewGroup
这种情况下,需要由测量到的子View的大小来决定。
几个问题
关于测量值与最终值的关系
在绝大数情况下,测量值都等于最终值;这是很多书上给出的结论,好奇宝宝可能会问,那么少数情况是什么情况?
这里需要给出的结论是
1.这个测量的大小最后会给到layout过程,当这个大小参数最终传递进onLayout的时候,它的大小就确定了,那么在layout种,我们可能人为地修改传进来的measure大小,这种情况,很显然,最终大小就不等于测量大小了。但是,这样做没有任何意义,所以这种少数情况,其实是我们要避免的情况,没有这样的特殊需求需要我们在layout里去修改这个测量的大小,测量的完整过程,请放在 onMeasure 中。
关于View的多次测量
比如有wight的LinearLayout的子View的大小,需要多次测量才能得到,那么到底什么情况下需要多次测量呢?我暂时还没有得到一个通用的结论,等后面深入学习,再来补充吧(TODO)
接下来,我们从源码来分析一下具体的 Measure过程
Measure过程
measure过程要分情况来看,如果只是一个原始的View,那么通过measure过程就完成了其测量,如果是ViewGroup,则除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,这是一个递归的过程
上面的结论是很多书上以及blog会给出的一段结论,完全没有问题,只是我个人觉得或许这样表述更容易理解:
为什么要先测量子View,再测量自己?
尽管ViewGroup的onMeasure方法会计算所有子元素的大小,但是这一步骤的目的并不完全是为了测量自己的大小,在该ViewGroup的SpecMode为EXACTLY的情况下,它不需要知道子View是个啥情况,就可以直接测量出自己的宽高,也就是等于SepcSize。这一过程(遍历测量子View,如果是子ViewGroup,则递归继续遍历)的最原始的目的,就是将measure过程向下传递下去,使得每一个View都能被测量,要不然测量就会中断,下面的View的测量流程就得不到执行。很多时候我们要厘清主次,不然逻辑很容易混乱。
我们现在理清了为什么要测量子View(主体)。那么测量子View的大小会对该ViewGroup的测量起到影响么(副作用)?
在SpecMode为wrap_content的情况下,该ViewGroup必须知道子View的大小,才能再去测量自己的值,这点很容易理解,wrap_content的字面意思就是适应内容,这点其实和单纯的View也没有区别,因为单一View的wrap_content的计算,我们通常也要去计算它的内容的大小,再得出一个合理的宽高。而ViewGroup的内容不再是图片或者文字,而是一个个的View,所以它的测量内容的大小就是测量子View的大小,这样一来,我们在上面说的wrap——content的分情况理解,其实也在概念上统一了起来,只是操作手段有所差别。
接下来,我们来看看具体的View的measure过程和ViewGroup
View的measure过程
measure是一个final方法,不允许重写,里面主要进行了一些测量的优化工作,真正的测量是在onMeasure函数种,废话说的有点多,我们来看看onMeasure源码
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
2 | setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), |
3 | getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); |
4 | } |
widthMeasureSpec 和 heightMeasureSpec 是父ViewGroup传递过来的,我们再来重申一遍,它是父View的MeasureSpec和子View的layoutParams一起来共同决定的,前者是父ViewGroup自身的情况,而layoutParams则是子View布局的愿景(它希望是多大),结合起来相当于就是父ViewGroup结合了自身的情况再考虑了儿子的要求后给出的结果,有点绕口,但是大抵如此。
这样一个过程,实际上就对应于ViewGroup
的getMeasuredSpec()
方法。
View的 measureSpec 从哪儿来?
ViewGroup 中没有重写 onMeasure 方法,这个过程交给具体的ViewGroup去重写,在 onMeasure 中,ViewGroup会对子View进行测量,第一步是通过 getChildMeasureSpec
来获取到子View的measureSpec,然后调用child.measure(widthMeasureSpec,heightMeasureSpec)
来测量子View。如果您不想打断思维的连续性,可以暂且跳过这一小节。
我们还记得那句话,View的MeasureSpec由自身的layoutParams和父View的MeasureSpec来决定,实际上这句话还不是完全准确的,在大多数情况下,View的measureSpec与父ViewGroup的可用空间有关,反映到getChildMeasureSpec()中,这个可用空间就是
1 | size = Max(0,specSize - padding); |
getChildMeasureSpec的作用就是得到子View的MeasureSpec,从而将它传递到子View的measure函数中,继而完成子View的测量,而子View的测量更多的代表着两个方面的含义:
1.递归的含义:子View可能是一个ViewGroup,这个测量过程需要传递下去
2.测量wrap_content的情况
接下来我们重点分析子View的MeasureSpec与父ViewGroup的对应关系
EXACTLY | AT_MOST | UNSPECIFIED | |
---|---|---|---|
dp/px | EXACTLY | childSize | EXACTLY | childSize | EXACTLY | childSize |
macth_parent | EXACTLY | parentSize | AT_MOST |parentSize | UNSPECIFIED | 0 |
wrap_content | AT_MOST | parentSize | AT_MOST | parentSize | UNSPECIFIED | 0 |
要注意几个地方
1.当child的布局为具体值得时候,最后的SepcSize = childSize(不管是不是超过父ViewGroup)
2.概括一下:形成measureSpec的过程的实际含义是:为子View做一些限制,并以MeasureSpec的形式传递给他,子View就在这个限制下来测量自己的大小
3.具体的规则应该根据业务来,有时候我们不一定要调用 getChildMeasureSpec,而是通过业务逻辑分别得到子View的SpecSize和SpecMode,然后直接makeMeasureSpec(specSize,SpecMode)
1 | protected void measureChildWithMargins(View child, |
2 | int parentWidthMeasureSpec, int widthUsed, |
3 | int parentHeightMeasureSpec, int heightUsed) { |
4 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); |
5 | final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, |
6 | mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin |
7 | + widthUsed, lp.width); |
8 | final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, |
9 | mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin |
10 | + heightUsed, lp.height); |
11 | |
12 | // 传递measureSpec给子View,递归测量 |
13 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); |
14 | } |
15 | |
16 | |
17 | // 为子View做一些限制,并以MeasureSpec的形式传递给它,子View就在这个限制下来测量自己的大小 |
18 | public static int getChildMeasureSpec(int spec, int padding, int childDimension) { |
19 | int specMode = MeasureSpec.getMode(spec); |
20 | int specSize = MeasureSpec.getSize(spec); |
21 | |
22 | // 这个size就是留给子View的可用空间,假使小于0,则令其为0 |
23 | int size = Math.max(0, specSize - padding); |
24 | |
25 | int resultSize = 0; // |
26 | int resultMode = 0; |
27 | |
28 | switch (specMode) { |
29 | // Parent has imposed an exact size on us |
30 | case MeasureSpec.EXACTLY: |
31 | if (childDimension >= 0) { |
32 | resultSize = childDimension; |
33 | resultMode = MeasureSpec.EXACTLY; |
34 | } else if (childDimension == LayoutParams.MATCH_PARENT) { |
35 | // Child wants to be our size. So be it. |
36 | resultSize = size; |
37 | resultMode = MeasureSpec.EXACTLY; |
38 | } else if (childDimension == LayoutParams.WRAP_CONTENT) { |
39 | // Child wants to determine its own size. It can't be |
40 | // bigger than us. |
41 | resultSize = size; |
42 | resultMode = MeasureSpec.AT_MOST; |
43 | } |
44 | break; |
45 | |
46 | // Parent has imposed a maximum size on us |
47 | case MeasureSpec.AT_MOST: |
48 | if (childDimension >= 0) { |
49 | // Child wants a specific size... so be it |
50 | resultSize = childDimension; |
51 | resultMode = MeasureSpec.EXACTLY; |
52 | } else if (childDimension == LayoutParams.MATCH_PARENT) { |
53 | // Child wants to be our size, but our size is not fixed. |
54 | // Constrain child to not be bigger than us. |
55 | resultSize = size; |
56 | resultMode = MeasureSpec.AT_MOST; |
57 | } else if (childDimension == LayoutParams.WRAP_CONTENT) { |
58 | // Child wants to determine its own size. It can't be |
59 | // bigger than us. |
60 | resultSize = size; |
61 | resultMode = MeasureSpec.AT_MOST; |
62 | } |
63 | break; |
64 | |
65 | // Parent asked to see how big we want to be |
66 | case MeasureSpec.UNSPECIFIED: |
67 | if (childDimension >= 0) { |
68 | // Child wants a specific size... let him have it |
69 | resultSize = childDimension; |
70 | resultMode = MeasureSpec.EXACTLY; |
71 | } else if (childDimension == LayoutParams.MATCH_PARENT) { |
72 | // Child wants to be our size... find out how big it should |
73 | // be |
74 | resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; |
75 | resultMode = MeasureSpec.UNSPECIFIED; |
76 | } else if (childDimension == LayoutParams.WRAP_CONTENT) { |
77 | // Child wants to determine its own size.... find out how |
78 | // big it should be |
79 | resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; |
80 | resultMode = MeasureSpec.UNSPECIFIED; |
81 | } |
82 | break; |
83 | } |
84 | //noinspection ResourceType |
85 | return MeasureSpec.makeMeasureSpec(resultSize, resultMode); |
86 | } |
setMeasuredDimension(measuredWidth,measuredHeight)
在测量结束的时候,一定要调用 setMeasuredDimension,否则测量无效
那么这里的 getDefaultSIze是如何得到宽高的测量值的呢?或者说,流程的设计者采用何种规则来得到默认的View的宽高:
1 | public static int getDefaultSize(int size, int measureSpec) { |
2 | int result = size; |
3 | int specMode = MeasureSpec.getMode(measureSpec); |
4 | int specSize = MeasureSpec.getSize(measureSpec); |
5 | |
6 | switch (specMode) { |
7 | case MeasureSpec.UNSPECIFIED: |
8 | result = size; |
9 | break; |
10 | case MeasureSpec.AT_MOST: |
11 | case MeasureSpec.EXACTLY: |
12 | result = specSize; |
13 | break; |
14 | } |
15 | return result; |
16 | } |
17 | |
18 | protected int getSuggestedMinimumWidth() { |
19 | return (mBackground == null) ? mMinWidth |
20 | : max(mMinWidth, mBackground.getMinimumWidth()); |
21 | } |
分析:
在这里,AT_MOST和EXACTLY进行了合并处理,也就是说,在AT_MOST模式下,result也等于specSize,那么AT_MOST模式下的SpecSize又等于什么呢?很明显它等于ParentSize,这就出现了,当我们设置wrap_content却占满了整个父ViewGroup的情况。对这句话不理解的人去翻一下MeasureSpec那张对照表哈。
但是,按照思维常理,我们在设置控件为wrap_content的时候,是希望它能取得一个适合它的大小,那么这里默认为什么设置成parentSize呢?这是因为系统并不知道你定义的View内容有多大,所以直接采取了比较懒惰的策略,给你整个能用的大小,在自定义View的时候,我们应当重写onMeasure方法,自己去测量内容值,并设置为最终的大小,当然,对于有些简单的情况,也可以直接设置成一个固定值(当然并不建议这样做,最好是按照内容的大小来合理的设置,比如TextView,可以根据字体大小,长度来设置wrap_content情况下的测量值)
SpecMode = UNSPECIFIED的情况:就是父ViewGroup不对该View有任何限制,它想要多大就多大,关键是系统也不知道你想要多大哈,你要是重写,都好说,你不重写的话,系统会这样来处理 UNSPECIFIED,会结合mMinWidth和mBackground的大小,取其最大的。这种情况一般不需要考虑,可以跳过这段阅读下文哈。
最后,分析一下UNSPECIFIED的情况,下面是官方的解释:
MeasureSpec.UNSPECIFIED - A view can be whatever size it needs to be in order to show the content it needs to show.
从上面的定义来看,我觉得UNSPECIFIED模式和AT_MOST相似的地方在于它们都是去获取一个它们想要的值,区别在于AT_MOST会将这个值限定在SpecSize以内,而UNSPECIFIED对这个值的大小没有加以限制。而且这里涉及到一个 real_size 和 visible_size 的区别。
看看下面这张图:
ViewGroup 的Measure过程
ViewGroup的measure过程从原理上来说与View的measure过程完全一样,唯一不同的是:
在处理SpecMode为AT_MOST时,对于内容的定义。因为AT_MOST的含义就是,你按需所取,但不要超过我给你的大小,那这个”需”对于普通View而言,就是文字的大小、图片的大小等等,而对于一个ViewGroup而言,就是一个个的子View所占的大小。但是注意,从概念上来讲,二者仍是统一的,都是按需索取。
而对于EXACTLY而言,ViewGroup不需要关注子View的大小,它一定等于SpecSize。类似于
1 | <....> |
2 | <MyLayout |
3 | android:layout_width="200dp" |
4 | android:layout_height="100dp"> |
5 | <View1>....</View1> |
6 | <View2>....</View2> |
7 | </MyLayout> |
8 | </....> <!--你根本不需要知道View1,View2的参数,MyLayout的宽高就是200dp和100dp--> |
我自定义了一个简单的MLinearLayout(1.0版本,不含weight等等属性,只支持orientation属性),我们来看下 onMeasure部分的代码
1 |
|
2 | public void onMeasure(int widthMeasureSpec,int heightMeasureSpec){ |
3 | int widthSize = MeasureSpec.getSize(widthMeasureSpec); |
4 | int heightSize = MeasureSpec.getSize(heightMeasureSpec); |
5 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
6 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
7 | |
8 | // 这里相当于switch语句中的 case:EXACTLY,如果是AT_MOST的话,后面的if语句会处理 |
9 | // 当然,如果是UNSPECIFIED模式的话,它的大小也等于SpecSize |
10 | int resultWidth = widthSize; |
11 | int resultHeight = heightSize; |
12 | |
13 | int count = getChildCount(); |
14 | int widthUsed= 0; |
15 | int heightUsed = 0; |
16 | |
17 | for(int i=0;i<count;i++){ |
18 | View child = getChildAt(i); |
19 | LayoutParams layoutParams = (MarginLayoutParams)child.getLayoutParams(); |
20 | measureChildWithMargins(child,widthMeasureSpec,widthUsed, |
21 | heightMeasureSpec,heightUsed); |
22 | // 分横向和竖直两种情况,used的计算不一样,横向的话,竖直方向的used始终为0,纵向的 //话,横向的used始终为0 |
23 | if("horizentation".equals(mOrientation)) { |
24 | // 这个used不包括ViewGroup的Padding,因为在measureChildWithMargin中, |
25 | //会单独计算Padding |
26 | widthUsed += child.getMeasuredWidth() |
27 | +((MarginLayoutParams) layoutParams).leftMargin |
28 | +((MarginLayoutParams) layoutParams).rightMargin; |
29 | } |
30 | else { |
31 | // 如果是竖直排列,则每次重新测量下个子View的时候,它可以使用整个宽,但是 |
32 | // heightUsed则要加上之前的child的height,以及margin(padding不用) |
33 | heightUsed += child.getMeasuredHeight() |
34 | +((MarginLayoutParams) layoutParams).topMargin |
35 | +((MarginLayoutParams) layoutParams).bottomMargin; |
36 | } |
37 | } |
38 | |
39 | // 相当于 case:AT_MOST |
40 | if(widthMode==MeasureSpec.AT_MOST){ |
41 | resultWidth = widthUsed<widthSize?widthUsed:widthSize; |
42 | } |
43 | if(heightMode==MeasureSpec.AT_MOST){ |
44 | resultHeight = heightUsed<heightSize?heightUsed:heightSize; |
45 | } |
46 | // 别忘了设置测量的宽高值,否则该ViewGroup的测量结果无效 |
47 | setMeasuredDimension(resultWidth,resultHeight); |
48 | } |
上面一开始定义了一个 resultWidth和resultHeight,并且赋初值为自身measureSpec的specSize。为什么说相当于EXACTLY模式呢,因为后面只在AT_MOST的情况下,对result的值进行了重新设定,否则,result的值最终就等于初值,故而,一开始的赋值相当于EXACTLY;
当SpecMode为AT_MOST的情况下,这时,我们已经得到了内容的长度,那么自然就会比较一下,内容和SpecSIze谁更大,如果是SpecSize更大,OK,满足你的使用长度,即result = used。如果used大于Spec Size,那么对不起,最多只能给你SpecSize,这也是AT_MOST的语义所在,你随便量,但是不要超过我给你的值。
这篇文章,我不打算分析 layout 过程,MLinearLayout的 onLayout 函数单独贴出来
1 | //具体的记录 |
2 | |
3 | public void onLayout(boolean changed,int left,int top,int right,int bottom){ |
4 | int mLeftStart = getPaddingLeft(); |
5 | int mTopStart = getPaddingTop(); |
6 | int childWidth = 0; |
7 | int childHeight = 0; |
8 | LayoutParams layoutParams = null; |
9 | View child; |
10 | for(int i=0;i<getChildCount();i++){ |
11 | child = getChildAt(i); |
12 | layoutParams = (MarginLayoutParams)child.getLayoutParams(); |
13 | |
14 | int childW = child.getMeasuredWidth(); |
15 | int childH = child.getMeasuredHeight(); |
16 | |
17 | mLeftStart+=((MarginLayoutParams) layoutParams).leftMargin; |
18 | mTopStart+=((MarginLayoutParams) layoutParams).topMargin; |
19 | |
20 | child.layout(mLeftStart,mTopStart, |
21 | mLeftStart+child.getMeasuredWidth(), |
22 | mTopStart+child.getMeasuredHeight()); |
23 | |
24 | if("vertical".equals(mOrientation)){ |
25 | mLeftStart=getPaddingLeft(); |
26 | mTopStart+=childH+((MarginLayoutParams) layoutParams).bottomMargin; |
27 | }else{ |
28 | mTopStart = getPaddingTop(); |
29 | mLeftStart+=childW+((MarginLayoutParams) layoutParams).rightMargin; |
30 | } |
31 | |
32 | } |
33 | } |
分析:
以上,关于View的测量过程分析的大概差不多了,可能还有很多细节没有分析到位,但是看完这篇文章,你一定有了一个逻辑上清醒的认识,这也是本文想要达到的目的。如果您还有疑问,欢迎添加微信进行讨论:VX - west_wnd
补充:
下面是ScrollView的measureChildWithMargins方法
竖直方向上的,不管你设置layoutParams是什么,最后的SpecMode都是UNSPECIFIED,而UNSPECIFIED则不会对子View的大小做出限制,这样的话,有多少内容就有多大大小,通过滑动即可显示出来
1 |
|
2 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, |
3 | int widthUsed,int parentHeightMeasureSpec, int heightUsed) |
4 | { |
5 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); |
6 | final int childWidthMeasureSpec =getChildMeasureSpec(parentWidthMeasureSpec, |
7 | mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin |
8 | + widthUsed, lp.width); |
9 | final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + |
10 | lp.bottomMargin +heightUsed; |
11 | |
12 | //注意,竖直方向上的,不管你设置layoutParams是什么,最后的SpecMode都是UNSPECIFIED |
13 | final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( |
14 | Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - |
15 | usedTotal),MeasureSpec.UNSPECIFIED); |
16 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec); |
17 | } |