View 的测量

接着上篇 View 基础 来讲 View 的工作原理,View 的工作原理中最重要的就是测量、布局、绘制三大过程,而其中测量是最复杂的,今天的主要任务是讲明白 View 的测量过程 ,不过在讲测量过程之前还会将一些有关 View 测量中使用到的其他知识进行讲解,例如 View 的展示过程、MeasureSpec 类的原理和使用等。所以整篇内容会稍多,不过条理很简单。让我们开始吧。

View的展示过程

首先呢,让我们对一个界面的根View从创建到显示到屏幕的具体过程有一个简单的认识,从而了解 View 的测量、布局、绘制过程从哪里来的。

  1. 创建一个 Window ,因为界面上显示的内容都是通过 Window 来展示的,所以需要先创建一个 Window

  2. 创建 DecorView(顶级View) ,就是说得到我们需要展示的根 View,一般情况下是一个 Framlayout

  3. Window 要展示到屏幕上是通过 WindowManager.addView() 方法完成的,该方法中会创建会为每个 Window 创建 ViewRootImpl 对象用于连接 WindowManager 和 DecorView ,并且所有 Window 想要对 View 的进行的操作都是通过 ViewRootImpl 来完成的

  4. ViewRootImpl 创建之后,会调用 performTraversals 方法,其中又会依次调用,performMeasure、performLayout、performDraw 方法,performMeasure 方法中会调用 DecorView 的 measure 方法,measure 方法中又会调用 onMeasure 方法将测量过程传递到子 View;performLayout 则会调用 layout 方法,layout 方法中会调用 onLayout 方法将布局过程传递到子 View;performDarw 方法中会调用 darw 方法,draw 方法中调用 dispatchDraw 方法将绘制过程传递到子 View 中

  5. ViewRootImpl 将测量、布局、绘制完成之后,就会通过 IPC 远程调用 WindowManagerService 的方法,将 window 和 view 连接,完成显示。

  6. View 显示更新时,系统也是通过 ViewRootImpl 来重新完成 测量、布局、绘制 三大过程。

View的测量 目的:确定View的测量宽高

上面提到了 View 要显示到界面上必须经过测量、布局、绘制三个阶段,其中的测量是第一个过程,其主要目的是要确定 View 的测量宽高,也就是 View 的 getMeasuredWidth()/getMeasuredHeight() 方法得到的结果

测量的过程是这样的,父 View 调用 measure 方法开始测量,measure 方法中又会调用 onMeasure 方法将测量过程传递到子 View 中,也就是调用子 View 的 measure 方法,子 View 的 measure 方法中调用子 View 的 onMeasure 方法,onMeasure 方法中完成子 View 的测量,子 View 测量完成之后,父 View 根据子 View 的测量结果确定自己的测量结果

measure 的过程是在 onMeasure() 方法中完成的,父容器根据自己的测量模式和测量大小以及子 view 的 LayoutParams 来确定子 View 的测量模式和测量大小,用到了 MeasureSpec 这个类,父容器再根据子的大小确定最终自己的大小,先了解一下 MeasureSpc 这个类

MesureSpc 是 View 中的一个内部类,里面封装了View测量方式和测量大小的信息

  • MesureSpec 是一个32位的int值,其中高 2 位为测量模式,低 30 位为测量的大小,在计算中使用位运算的原因是为了提高优化效率。

  • MeasureSpec 封装了父布局传递给子布局的布局要求,每个 MeasureSpec 代表了一组宽度或高度的要求及大小,测量过程中每个 View 的宽高都由 MeasureSpec 来确定

  • 每个 Viwe 宽高的 MeasureSpec 的确定都由其父 View 的 MeasureSpec 和该 View 的 LayoutParams 确定

三种mode

  1. UNSPECIFIED 父没有对子施加任何约束,子可以是任意大小,使用较少,一般用于系统内部

  2. EXACTLY 确切大小,子被限定在给定值,即在 xml 中设置了宽高的确定值或者是 match_parent 时,模式为 EXACTLY

  3. AT_MOST 表示子 view 的大小不确定,但最多是父 View 目前可使用的大小 会根据这个上限来设置自己的尺寸。表示子布局限制在一个最大值内,一般为 WARP_CONTENT ,此时 MeasureSpc 中的Size值即为父可允许的最大值,展示效果为占满父控件剩余位置

    public class MeasureSpec {

     // 进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和倒数第二位也就是32和31位做标志位) 
     private static final int MODE_SHIFT = 30;  
           
     // 0x3为16进制,10进制为3,二进制为11。3向左进位30,就是 11 00000000000...(11后跟30个0)   
     private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
    
     // 0向左进位30,就是00 00000000000...(00后跟30个0) 
     public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
     
     // 1向左进位30,就是01 00000000000(01后跟30个0)  
     public static final int EXACTLY     = 1 << MODE_SHIFT;  
     
     // 2向左进位30,就是10 00000000000(10后跟30个0)  
     public static final int AT_MOST     = 2 << MODE_SHIFT;  
    
     /** 
      * 根据提供的size和mode得到一个详细的测量结果 
      * measureSpec = size + mode;   (注意:二进制的加法,不是10进制的加法!)  
      * 这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值 
      */
     public static int makeMeasureSpec(int size, int mode) {  
         return size + mode;  
     }  
    
     /** 
      * 通过详细测量结果获得mode 
      * 
      * mode = measureSpec & MODE_MASK;  
      * MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的值,保留32和31位的mode值。
      */  
     public static int getMode(int measureSpec) {  
         return (measureSpec & MODE_MASK);  
     }  
    
     /** 
      * 通过详细测量结果获得size 
      * 
      * size = measureSpec & ~MODE_MASK;  
      * 原理同上,不过这次是将MODE_MASK取反,也就是变成00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
      */  
     public static int getSize(int measureSpec) {  
         return (measureSpec & ~MODE_MASK);  
     }  
    
     /** 
      * 重写的toString方法,打印mode和size的信息,这里省略 
      */  
     public static String toString(int measureSpec) {  
         return null;  
     }       
    

    }

DecorView 的 MeasureSpc的确定

上面提到了 View 测量的过程中,是从 DecorView 开始的,所以我们先看 DecorView 的 MeasureSpc 的是如何确定的,其代码在 ViewRootImpl 中

    // ViewRootImpl
    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;
    }

其中 windowSize 代表窗口大小,rootDimension 代表 LayoutParams 中的宽高参数,由代码可以看出 DecorView 的 MeasureSpc 单纯的由其 LayoutParams 确定

  • match_parent 精确模式,大小就是窗口的大小

  • wrap_content 最大模式,大小不定,最大不能超过窗口大小

  • 固定值 精确模式,大小为 LayoutParams 中指定的宽高大小

普通 View 的 MeasureSpc 的确定

DecorView 的 MeasureSpec 确定之后,ViewRootImpl 就会调用 DecorView 的 measure 方法,measure 方法中会调用 onMeasure 方法,DecorView 是 GroupView 的子类,GroupView 的 onMeasure 方法中会遍历子 View ,并依次调用 measureChildWithMargins 方法

    // ViewGroup 
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            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 = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    // ViewGroup
    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 = 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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }    
    

以上代码非常清晰,就是根据父 View 的 MeasureSpc 和子 View 的 LayoutParams 来共同确定子 View 的 MeasureSpec ,其逻辑可以用一下图片总结

image

测量过程

好了,经过上面那么多的介绍和分析,终于要进入今天的正题了,那就是 View 的测量过程,由上面的分析可以知道 ViewRootImpl 中的 performMeasure 方法启动 View 的测量,其中会将测量过程由 ViewGrop 逐级传递,上面分析子 View 的 MeasureSpec 的确定过程中,子 View 的 MeasureSpec 确定之后,接下来有一行代码:

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

这行代码则将测量过程由 ViewGroup 传递到了子 View,假定该子 View 并不是 ViewGroup ,我们知道 View 的 measure 方法会调用 onMeasure 方法,接下来分析 View 的测量过程

View 的测量过程

/** 
 * 这个方法需要被重写,应该由子类去决定测量的宽高值
 */  
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

onMeasure 中调用了 setMeasuredDimension() 方法,将 View 的测量大小保存在 View 的 mMeasuredWidth 和 mMeasuredHeight 中

onMeasure() 方法应该被子类重写从而确定 View 的测量宽高值,如果子类不重写,则 View 的宽高值为根据 View 的背景大小以及 MeasureSpec 确定的值,其中 View 的测量大小通过 getDefaultSize 方法确定。

默认 View 测量宽高的确定规则为:

  1. 如果 MeasureSpec 的 SpecMode 为无限制条件,就以最小的宽度作为测量结果

  2. 如果 MeasureSpec 的 SpecMode 为 MeasureSpec.AT_MOST 或者 MeasureSpec.EXACTLY,就以 MeasureSpec 中的 SpecSize 为测量结果

/**
 * 返回 View 默认的最小宽度
 */
protected int getSuggestedMinimumWidth() {
    // 如果没有给View设置背景,那么就返回View本身的最小宽度mMinWidth,如果给View设置了背景,那么就取View本身最小宽度mMinWidth和背景的最小宽度的最大值
    return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}

/** 
 * 作用是返回一个默认的值,父没有给子设置限制条件,子就以自己想要的尺寸 size 作为测量的结果,有限制时View 必须使用其父ViewGroup指定的尺寸
 * 第一个参数size为View的设置大小,第二个参数为测量的大小 
 */  
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec); // 得到 MeasureSpc 的限制方式
    int specSize = MeasureSpec.getSize(measureSpec); // 得到 MeasureSpc 的尺寸

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED: // 无限制条件,就以最小的宽度 mMinWidth作为测量结果
            result = size;
            break;
        case MeasureSpec.AT_MOST: // View 使用其父View为其设定的可达的最大尺寸
        case MeasureSpec.EXACTLY: // View 必须使用指定的尺寸
            result = specSize;
            break;
    }
    return result;
}

mMinWidth 来源

  • 第一种情况是,mMinWidth是在View的构造函数中被赋值的,可以为0,View 构造函数中通过读取XML中定义的 minWidth 的值来设置 View 的最小宽度 mMinWidth,如果不设置默认该值为 0

  • 第二种情况是调用 View 的 setMinimumWidth 方法给 View 的最小宽度 mMinWidth 赋值

最后再来看一下 setMeasuredDimension 方法,其只是将测量结果保存并修改 View 的状态位

/** 
 * 这个方法必须由onMeasure(int, int)来调用,来存储测量的宽,高值。 
 */ 
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        //layoutMode是LAYOUT_MODE_OPTICAL_BOUNDS的特殊情况,我们不考虑 
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    
    // 最终将测量的结果保存
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    //最后将View的状态位mPrivateFlags设置为已量算状态
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

由以上的分析可以得出结论,onMeasure() 的任务就是计算准确的 measuredWidth 和 measuredHeight ,源码中的 getDefaultSize() 只是简单的测量了默认宽高值,具体的测量任务就交给了子类中重写的 onMeasure() 方法。所以子 View 需要根据需求重写 onMeasure 方法来保存自己想要的测量值。

在子类中重写 onMeasure() 方法

如果不重写 onMeasure 之前的 MeasureSpec 中保存的是根据父容器以及 View 的 LayoutParams 确定的大小

由上分析可得,在默认的 onMeasure() 方法中支持 EXACTLY 和 UNSPECIFIED 时的准确测量,EXACTLY 即 xml 中设置大小或设置为 match_parent 占满父控件

AT_MOST 情况需要我们在重写 onMeasure() 方法时处理,即属性设置为 wrap_content 时或 match_parent 但父控件测量模式为 AT_MOST,我们需要在 onMeasure() 方法中计算内容的宽高,从而设置准确的测量值。

结论:重写 onMeasure() 方法,即通过判断测量的模式,给出不同的测量值。

到这里 View 的测量过程就已经结束了,测量过程的任务就是确定了 View 的测量宽高值。

ViewGroup的测量

ViewGroup 是一个抽象类,其默认的测量过程只是按照 View 的测量过程完成自己的测量过程

ViewGrop 的子类实现,一般会先测量所有的子 View ,由所有子 View 的测量大小再根据 ViewGrop 要实现的效果来确定测量大小。

具体对子元素 measure() 方法的调用是在 ViewGroup() 的实现类的 onMeasure() 方法中, measureChildWithMargins() 或者 MeasureChildren() 方法,这两个方法中都会确定子元素的宽高的 MeasureSpc 并调用子元素的 measure() 进行子元素的测量。获取子元素宽高的方法为 getChildMeasureSpc(),这个方法中会根据 ViewGrop 的 MeasureSpc,父元素的 padding、子元素的 LayoutParams、以及父元素的可用空间等信息确定子元素的 MeasureSpc。确定子 View 的 MeasureSpec 之后则会调用子 View 的 measure 方法。

推荐阅读更多精彩内容