view绘制过程之 measure 2.实例分析

Posted by boredream on February 2, 2015

首先研究ViewGroup,需要同时考虑measure自己部分和measure child, 虽然LinearLayout最常用,但是牵涉到weight,算法比较复杂,所以挑了一个简单的帧布局 FrameLayout

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
84
85
86
87
88
89
90
91
/**
 * {@inheritDoc}
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren .clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE ) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams ) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    // Account for padding too
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }

    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));

    count = mMatchParentChildren.size();
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren .get(i);

            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidthMeasureSpec;
            int childHeightMeasureSpec;
           
            if (lp.width == LayoutParams.MATCH_PARENT) {
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() -
                        getPaddingLeftWithForeground() - getPaddingRightWithForeground() -
                        lp.leftMargin - lp.rightMargin,
                        MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }
           
            if (lp.height == LayoutParams.MATCH_PARENT) {
                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() -
                        getPaddingTopWithForeground() - getPaddingBottomWithForeground() -
                        lp.topMargin - lp.bottomMargin,
                        MeasureSpec.EXACTLY);
            } else {
                childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                        lp.topMargin + lp.bottomMargin,
                        lp.height);
            }

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

首先回忆下FrameLayout的特性,帧布局 所有放在布局里的控件,都按照层次堆叠在屏幕的左上角,后加进来的控件覆盖前面的控件

第一次for循环 mMeasureAllChildren标记值无视~ 只计算非gone的child 首先先用measureChildWithMargins计算一下child,点开这个方法内部发现是调用的getChildMeasureSpec方法 即用上面表格的那个默认处理方法对所有的child都measure一遍~ 对child measure以后就可以获取到child的measuredWidth/Height/State了 然后根据获取到的宽高参数计算maxWidth/Height和childState 其中maxWidth/Height计算很简单,直接用Math.max所有的child,最终就获取到child里最大的宽高了 childState的计算是利用combineMeasureStates方法,将child的宽高的期望类型也用移位或的方法合并, 即childState是由width和heigh的期望类型共同组成的 每次循环最后,判断如果onMeasure的宽高模式其中有一个不是EXCTLY的,并且child的宽高参数类型都是MATCH_PARENT, 就将这个child保存至集合measureMatchParentChildren中,用作后面child的特殊measureSpec设置 第一轮循环结束 开始对自己进行measureSpec设置,直接用setMeasuredDimension方法, 而宽高的measureSpec计算需要利用resolveSizeAndState方法~ 在上一章中介绍过这个方法,作用就是将parent的期望类型和期望值width/heightMeasureSpec 和自己算出来的大小maxWidth/Height结合起来考虑,决定最终child的大小和类型

再下面是特殊child的measure处理了~ 这里可能会有疑惑,上面所有的child其实都默认measureChildWithMargins处理过一遍了, 这里为什么又要measure一次呢? 想象下特殊情况,如果FrameLayout宽或高是WRAP_CONTENT的,即这个宽或高将由child中最大的值决定, 而其中有一个child是MATCH_PARENT的,意思是大小和FrameLayout一样~ FrameLayout的大小只能在for循环所有child的以后才能确定maxWidth/Height, 即无法在第一轮for循环进行时就获取到parent的最终大小,自然没办法决定MATCH_PARENT的child大小了 所以在第一轮循环时先将特殊的MATCH_PARENT属性的child保存到一个集合里, 在第一轮循环结束后算出parent宽高以后,再开始对特殊的child进行二次循环处理, 对child的特殊处理为: 如果child是MATCH_PARENT, 则利用FrameLayout自己的宽高去除padding和margin后生成一个EXACTLY的measureSpec, 还要加个else判断不是MATCH_PARENT的情况, 因为上面是宽高其中有一个属性满足要求,就添加到集合里需要特殊处理,另一个参数可能是正常的 所以宽高都在else中添加上默认处理getChildMeasureSpec 最终用生成的measureSpec值再child.measure设置一次,搞定~


下面研究View的子类,举个最基本的例子TextView 由于是非ViewGroup,所以只有对自己身measure部分

代码为了理解方便,会减去child自己想要宽高的计算过程~ 即根据textSize,lines,drawableTop等综合算出大小,这个算法比较复杂,我们也暂时没有研究必要 宽高同理,所以只把宽度计算部分抽取出来,简化后代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec. getMode(widthMeasureSpec);
    int widthSize = MeasureSpec. getSize(widthMeasureSpec);

    int width;

    if (widthMode == MeasureSpec. EXACTLY) {
        // Parent has told us how big to be. So be it.
        width = widthSize;
    } else {
        width = ..........  // 根据TextView一些特性如hint,ems,drawable等计算width值

        // Check against our minimum width
        width = Math. max(width, getSuggestedMinimumWidth());

        if (widthMode == MeasureSpec. AT_MOST) {
            width = Math. min(widthSize, width);
        }
    }

    setMeasuredDimension(width, height);
}

同样是按照套路来的

  • 如果是EXACTLY模式,那告诉我多大我就多大widthSize
  • 如果是AT_MOST模式,那取计算出来的宽度(根据字体大小和字数多少)和parent告诉我的宽度中小的那个,即取一个不超过AT_MOST模式parent的期望数值
  • 如果是UNSPECIFIED模式,则无视parent直接用child自己算出来的大小width

这一段if else的处理其实就等同于FrameLayout中的resolvSizeAndState方法处理, 但由于是View只能作为child,所以setMeasuredDimension就不需要考虑类型问题了


以上最基本的ViewGroup和View子类都介绍完了 作为android sdk中自带的类,肯定都是很靠谱按套路来的,即遵从规则的孩纸,相当于几个具有不同特性的标杆 如果再有特殊需要,可以考虑继承这几个实现子类,而不用直接继承View/ViewGroup, 这样就更加简单了,不用面面俱到,只用考虑自己所需的特殊实现,下面举几个间接继承的例子

ScrollView,可以滚动的,child只有一个且高度可以大于自己(此时可以上下滚动) 从标杆中找~ 果断用FrameLayout,这个类申明中有说明 一般来说只持有一个child(当然可以add多个view,但这个是推荐的最常见应用场景)

特性呢? child的height我们希望不做限定,即child想多高就多高, 如果上面已经理解了,这里肯定能直接想到用UNSPECIFIED模式

回忆下上面的FrameLayout,第一次for循环的时候用measureChildWithMargins计算了所有的child, 而measureChildWithMargins中用的是默认处理方式getChildMeasureSpec方法,即上一章中的那个表格 明显不是我们要的,那复写measureChildWithMargins之~ (这里如果直接复写onMeasure方法也是一样道理,最终目的都是为了让child的height类型设为UNSPECIFIED)

再查看ScrollView源码的measureChildWithMargins可以看到系统的具体处理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, intwidthUsed,
        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 = MeasureSpec.makeMeasureSpec(
            lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

宽度不变还是用默认方法getChildMeasureSpec 高度部分则用宽度则直接用MeasureSpec.makeMeasureSpec方法生成了一个UNSPECIFIED的宽度measure值

注: UNSPECIFIED的意思是child你想多大就多大,不用考虑我~ 所以如果用这个模式的话,数值部分就没用了, 一般直接makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);即可, 看默认处理方法getChildMeasureSpec也可以发现,child期望类型定为UNSPECIFIED时,size就直接设为0了

但是要再次强调,模式只是parent对child的一个期望,虽然UNSPECIFIED的意思是不限定按照child的自己来, 但万一child非要用到margin值呢,那就传吧~


总结 研究了很长时间,网上详细分析的资料也很少,文章来来回回修改了N次 每次觉得已经看懂了过两天又觉得又问题,最后终于勉强懂了大概 现在从头到尾梳理了一遍发出来,可能还是存在很多问题希望大家共同讨论,可能有不太准确的地方, 发表以后可能还会补充说明修改~

android的view显示是最核心的内容,主要分measure layout draw三个部分,而measure又是最复杂最难理解的

文章看懂以后不求能完全写出来一个LinearLayout这样sdk级别的控件, 能在写自定义控件知道onMeasure如何处理就可以了~不会疑惑”诶我操!我的控件为什么不显示”就行了 更主要的是能理解android中view的measure原理,知道大概流程是什么~