Android Tech And Thoughts.

View的三大流程

Word count: 4.7kReading time: 19 min
2019/11/30 Share

前言

这篇文章会抛弃所有的细枝末节,聚焦在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结合了自身的情况再考虑了儿子的要求后给出的结果,有点绕口,但是大抵如此。

这样一个过程,实际上就对应于ViewGroupgetMeasuredSpec()方法。

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
@Override
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
   @Override
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
@Override
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
    }
CATALOG
  1. 1. 前言
  2. 2. 理解MeasureSpec
    1. 2.0.1. MeasureSpec
    2. 2.0.2. MeasureSpec 和 LayoutParams 的对应关系
  • 3. 如何确定一个View的大小
  • 4. 几个问题
  • 5. Measure过程
  • 6. View的measure过程
    1. 6.0.1. View的 measureSpec 从哪儿来?
    2. 6.0.2. setMeasuredDimension(measuredWidth,measuredHeight)
  • 7. ViewGroup 的Measure过程