view绘制过程之 measure 1.原理

Posted by boredream on January 30, 2015

首先是view的绘制过程~最主要的分三部分 measure layout draw 看字面意思,计算,布局,画~ android中控件相当于是画在一个无限大的画布上的,那就产生了几个问题 画布无限大,但是画的内容肯定是有限的,即我们只需要画布的一小部分,那这部分有多大呢? measure就是计算这个画布所需部分有多大的

决定好我们需要的画布部分,我们可能会在上面画很多内容,每个内容都画在什么位置呢? layout就是决定在选定范围内画在什么位置的

最后,决定好画在具体位置时,我们到底画什么内容呢? draw自然就是决定画什么具体内容的了

而三个步骤对应的处理我们都可以在对应的on…方法中实现 即onMeasure onLayout onDraw 本系列会逐一分析三者的原理以及我们如何去使用, 全部分析完以后会将三者整合起来详细分析


本章,我们研究measure部分~也是绘制三部曲滴第一部 开始之前先解释几个重要的概念,一定要理解这几个概念,不然后面分析会让人无法理解~

  1. measure过程中使用的宽高值是一个特殊的类型,由数值和类型共同组成 在下面分析代码中看到的,比如onMeasure方法的参数width/heightMeasureSpec都是这种特殊值, 虽然是int值,但并非只是个简单数字,而是包含期望类型的一个特殊宽高值~ 即由宽高数值+宽高期望类型值共同拼接而成

这个期望类型有三种模式,分别是 MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY和MeasureSpec.AT_MOST 下面是介绍

  • EXACTLY 表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
  • AT_MOST 表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。
  • UNSPECIFIED 表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

类型实质上也都是数字,数值和类型都是int,如何组成的呢? 原理是类型为左移30位的高位数字,数值为不移位的低位数字,最后拼成一个带有类型和大小数值的特殊int

看2进制结果比较明显(如果直接打印int值看10进制数字可能会奇怪,我操这么大的数字是怎么回事) 比如宽度数值为800,期望类型为EXACTLY=1,最终组成的结果就是 1000000000000000000001100100000 即右起数第31位是类型值1,加粗部分 右边的1100100000就是数值大小部分,换成10进制就是800

如何将数值和类型具体组装成一个这样完整的特殊值呢~ 用MeasureSpec.makeMeasureSpec(数值, 类型)方法生成即可

  1. 一个View的最终大小不是简单的由布局文件中的width和height决定的,还需要考虑parent和child的情况 诸如WRAP_CONTENT和MATCH_PARENT都是需要根据parent和child的情况共同决定的, 而作为parent对child的期望值,也是无法直接决定控件最终大小的,切记,期望值只个参考

补充说明下:

  • 期望值这种东西并不能直接决定child的实际值,只是对child的一个期望~ 换句话说,child可以根据情况自行判断遵从期望或者无视parent对你的期望,自行设置宽高
  • 而不同的child对待同一个期望类型的处理方法也是不同的
  • 对child期望类型的设置一般也要根据child的LayoutParams.width/height等情况来设定
  • 一个ViewGroup有可能作为其他View的parent也可能作为其他view的child(View只能作为child) 所以设计自定义ViewGroup时需要又考虑parent的期望又要考虑对child的期望设置

举个例子对比下,帮助理解 比如parent父控件相当于父母,parent的期望类型就相当于父母希望你发展的方向,上面几点就可以这么理解

  • 父母希望你以后能往艺术方向发展,那你偏不,就非要当程序猿~ 虽然不按套路来但谁也管不了你,最终决定权还是在你手里的
  • 父母有三个孩子,希望他们都往艺术方向发展,熊孩子们也遵从了,但是个人理解不同 熊孩子一号搞了美术,二号整了音乐,三号玩起了人体艺术 但是如果父母希望熊孩子搞经济方面,也就是赚钱为主呢~每个人也还是有不同理解 熊孩子一号和二号都选了开咖啡店,三号则开了洗脚城- -
  • 父母对孩子的期望也看孩子,会在一定意义上遵从孩子的意见 如果孩子从小脑子聪明,那自然就希望他赚钱~ 如果孩子从小就对艺术感兴趣,自然就期望他在艺术方面有发展了~
  • 你可能是当儿女的,又可能是当爹妈的,所有两者都要考虑到

如果自定义控件需要确定自己的大小,可以复写onMeasure方法在其中处理, 若是继承ViewGroup则即可能作为parent又可能作为child,需要处理两部分,对自己和对child的measure 若为View的子类,则只用处理自己的measure即可,没有child所以不用考虑对其的measure

一. 对自己的measure(View和ViewGroup都需要) 是计算控件自己的大小,布局中参数只是一个你想要的值,实际控件的size需要结合其parent的期望大小一起决定, 对于match_parent和wrap_content情况更是需要结合parent和child才能共同决定

处理方法:

  1. 自己直接设置处理 setMeasureDimension(int measureWidth, int measureHeight); 该方法是直接设置控件宽高的,比如自定义View的onMeasure中直接setMeasureDimension(任意值, 任意值) 那无论xml布局文件中的宽高写多少,最终size都还是setMeasureDimension中传入的固定值了~ 当然肯定是不能这么乱来的,按照方法注释里面的说法是,开发者有义务遵循几个原则,要按照套路来,不能瞎搞, 不但要考虑到parent对你的期望值,即复写onMeasure方法时的两个参数, 还有要有最小值等其他处理

  2. 调用父类处理方法 super.onMeasure(); 直接交给parent处理,自己不做处理

一般来说直接继承ViewGroup的,比如LinearLayout,RelativeLayout,Framelayout等都是自己处理的(方法1) 而间接继承ViewGroup的,即是继承于LinearLayout等这样ViewGroup子类的,比如ScrollView, 那如果没有特殊需要则多是直接交给父类处理(方法2)

二. 对child的measure(只有ViewGroup需要) 对child的measure即你对于child的期望值设置,child.measure() 这样child再根据你传给他的期望值结合child自己的情况决定他的最终大小

注意 对child的measure处理是需要自己处理的,换句话说,如果你复写了一个ViewGroup只在里面measure了自己, 那child的onMeasure方法是不会自动调用的,一定要手动调用child.measure方法才能触发, 但若是间接继承,比如继承了FrameLayout,那只需super.onMeasure就行了, 因为FrameLayout自己已经for循环measure过所有child了

这样ViewGroup中measure自身,然后measure child, 如果child也是一个ViewGroup又要measure child自身,然后measure child的child, 这是毅种循环,最终完成对整个View树结构所有控件的measure


到这里可能有细心的同学会有发现一个疑惑

我们知道onMeasure方法的参数是parent传过来的,那问题来了,如果FrameLayout是作为布局文件里的根布局, 那它的parent是谁呢~他的onMeasure方法中的width/heightMeasureSpec是多少呢?谁赋予的呢? 原理就不扯太远了,参考了书中的13.8章节(从书里学到了很多,推荐大家去看看) 直接定位到ViewRootImpl类中的getRootMeasureSpec方法, 看注释就知道是计算根view的measureSpec值的,方法如下

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
/**
 * Figures out the measure spec for the root view in a window based on it's
 * layout params.
 *
 * @param windowSize
 *            The available width or height of the window
 *
 * @param rootDimension
 *            The layout params for one dimension (width or height) of the
 *            window.
 *
 * @return The measure spec to use to measure the root view.
 */
private static int getRootMeasureSpec( int windowSize , int rootDimension) {
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

总结起来就是 如果某个ViewGroup子类作为布局文件中的根布局时,布局参数和onMeasure中的measure值对照关系就如下

  • match对应 EXACTLY+windowSize(屏幕分辨率大小)
  • wrap对应 AT_MOST+windowSize
  • 有size时对应 EXACTLY+size(自定义的大小)

实验下,自定义一个继承View的MyView,作为布局文件的根元素, 依次将宽高改为 数值/wrap_content/match_parent运行并打印onMeasure的width/heightMeasureSpec值

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding= "utf-8"?>
<com.boredream.view.MyView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width= "wrap_content/match_parent"
    android:layout_height= "200dp/400dp" />

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      System.out.println(MeasureSpec. toString(widthMeasureSpec) + ":" + MeasureSpec.toString(heightMeasureSpec));
      System.out.println("---------------------");
}

我是在分辨率为800 600的模拟器上跑 根自定义布局参数width=wrap_content/height=200dp时 打印内容为MeasureSpec: AT_MOST 800:MeasureSpec: EXACTLY 200

根自定义布局参数width=match_parent/height=400dp时 打印内容为MeasureSpec: EXACTLY 800:MeasureSpec: EXACTLY 400


好了,知道要做什么了,不光要设置自身宽高还要设置child的期望值, 那如何根据parent的期望值,结合自身情况(包括child的布局参数值)综合条件来准确获取到一个合适的值呢~ 尤其是类型我用哪个呢~

系统已经考虑到这些了,为我们提供了几个实用有效的方法, 比如getChildMeasureSpec还有resolveSizeAndState等等,问题又来了,什么时候用什么方法呢?

挑常用的分析下

  1. MeasureSpec.makeMeasureSpec(int size, int mode) 上面提过,最简单的,直接将数值size和类型mode拼装成一个所需的measureSpec值~ 在你明确知道自己需要的类型时,直接使用该方法生成

  2. getChildMeasureSpec(int spec, int padding, int childDimension) 是ViewGroup类中定义的,用于处理child的MeasureSpec值的,child的处理也是measure中的难点 对应代码为

    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
    
    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec( int spec, int padding, int childDimension) {
     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);
    }
    

注释大概意思是 计算child的MeasureSpec比较麻烦,这里提供了一个有效滴方法可以准确计算 目标是根据MeasureSpec值和child的LayoutParams值算出一个”最可能”的结果 这个可以理解为一个默认处理,没有特殊要求的话一般就直接用这个~

下面贴一个该方法处理child的对应的关系图, 出自书中的13.8章节 ![mesure1](https://github.com/boredream/boredream.github.io/blob/master/img/in-post/mesure1.png?raw=true)

  1. resolveSizeAndState(int size, int measureSpec, int childMeasuredState) 在FrameLayout和LinearLayout中的onMeasure中都可以看到使用过此方法, 一般对自己进行Measure计算时经常使用该方法~
    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
    
    /**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec.  Will take the desired size, unless a different size
     * is imposed by the constraints.  The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting
     * size is smaller than the size the view wants to be.
     *
     * @param size How big the view wants to be
     * @param measureSpec Constraints imposed by the parent
     * @return Size information bit mask as defined by
     * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}.
     */
    public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
     int result = size;
     int specMode = MeasureSpec.getMode(measureSpec);
     int specSize =  MeasureSpec.getSize(measureSpec);
     switch (specMode) {
     case MeasureSpec.UNSPECIFIED:
         result = size;
         break;
     case MeasureSpec.AT_MOST:
         if (specSize < size) {
             result = specSize | MEASURED_STATE_TOO_SMALL;
         } else {
             result = size;
         }
         break;
     case MeasureSpec.EXACTLY:
         result = specSize;
         break;
     }
     return result | (childMeasuredState& MEASURED_STATE_MASK);
    }
    

    三个参数看注释就明白意思依次为

  2. size view控件想要多大~比如LinearLayout中垂直塞了仨child,每个高度都是200, 那简单来说就是view控件希望要600即size=600 size值根据不同控件需要自己计算~
  3. measureSpec parent对你的限定,这个解释过很多遍了,view最终大小不但要考虑自己希望多大还要考虑parent的期望 一般直接使用onMeasure的参数作为measureSpec的值
  4. childMeasuredState child的measure类型,该参数的生成处理比较麻烦,会在后面实例介绍中分析如何生成处理

再回头看方法代码 size相当于child自己想要的大小,specSize为parent的期望大小

当期望类型是UNSPECIFIED时,即你爱咋地咋地,那就是child希望多大就多大,直接结果用child的size了

期望类型是AT_MOST时,即你最多只能specSize这么大,那child的size大于parent的specSize时, 结果还是只能用specSize,因为规则是”你最多只能这么大,不能超过它”,但是child没有拿到自己想要的size, 就添加了一个标志位MEASURED_STATE_TOO_SMALL打个记号,原理还是利用移位或运算把状态保存至高位

期望类型是EXACTLY时,代表我就想要你这么大,那child就直接取parent期望的specSize了 最后把数值size和类型state用按位或运算| 合并起来

总结 就像2,3方法注释中说明的那样,这些都是系统提供的一些实用的方法, 言外之意不用一定按照方法中的规则处理,只是提供一个辅助工具~ 在你不知道如何处理时就可以用2,3的方法,即给他们当做是默认处理方式

通常使用场景是

  • 2是针对自己的默认处理方法
  • 3是针对child的默认处理方法
  • 1是特殊处理,按照需要直接自己拼装一个measureSpec值进行对自己或者对child的值设置

好了,以上: 算的单位(数值+期望类型的特殊合成值), 算什么(对自己的measure和设置, 对child的期望值设置), 用什么方法算(MeasureSpec.makeMeasureSpec,getChildMeasureSpec,resolveSizeAndState等) 都依次介绍过了~ 然后是重中之重,具体怎么算~

measure过程每个类基本都不同,没有固定套路,但可以参考系统源码找到一些常用的处理方法 请看下一章~源码实例measure分析