Android View绘制onMeasure流程

前言 

onMeasure()方法的作用就是测量View需要多大的空间,就是宽和高。大家可能会有这样的疑问。就是我在xml文件中已经指定好了某个View宽高尺寸了,那么自定义View时不就没有必要再次获取宽高并设置宽高吗?onMeasure方法不就没有了吗?

其实不然,我们就知道,在xml布局文件中,我们的layout_width和layout_height参数可以不用写具体的尺寸,而是wrap_content或者是match_parent。其意思我们都知道,就是将尺寸设置为“包住内容”和“填充父布局给我们的所有空间”。这两个设置并没有指定View的真正大小,可是我们绘制到屏幕上的View必须是要有具体的宽高的,正是因为这个原因,我们必须自己去处理和设置尺寸。当然了,View类给了默认的处理,但是如果View类的默认处理不满足我们的要求,我们就得重写onMeasure函数。

对于 View 的默认测量很简单,大部分情况就是拿计算出来的 MeasureSpec 的 size 当做最终测量的大小。

而对于其他的一些 View 的派生类,如 TextView、Button、ImageView 等,它们的 onMeasure 方法系统了都做了重写,不会这么简单直接拿 MeasureSpec 的 size 来用的,而会先去测量字符或者图片的宽高等,然后拿到 View 本身 content 这个宽高(字符宽高等),如果 MeasureSpec 是 AT_MOST,而且 View 本身 content 的宽高不超出 MeasureSpec 的 size,那么可以直接用 View 本身 content 的宽高(字符宽高等),而不是像 View.java 直接用 MeasureSpec 的 size 做为 View 的大小。

一 onMeasure流程及相关方法

1.1 View树的measure时序图

我们每次自定义view都会重写onMeasure方法,我们总结下这个流程

  1. 测量控件大小是父控件发起的,顶层是Decorview.measure
  2. 父控件要测量子控件大小,需要重写onMeasure方法,然后调用measureChildren或者measureChildWithMargin方法
  3. onMeasure方法的参数是通过getChildMeasureSpec生成的
  4. 测量控件的步骤:
    父控件onMeasure -》measureChildren(measureChildWithMargin)-》getChildMeasureSpec -》子控件的measure -》onMeasure -》setMeasureDimension- 》父控件onMeasure结束调用setMeasureDimension保存自己的大小

我们看下相关代码:


protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
    int widthUsed, int parentHeightMeasureSpec, int heightUsed) { 

    // 子View的LayoutParams,你在xml的layout_width和layout_height,
    // layout_xxx的值最后都会封装到这个LayoutParams。
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();   

    //根据父View的测量规格和父View自己的Padding,还有子View的Margin和已经用掉的空间大小(widthUsed)
    //就能算出子View的MeasureSpec,具体计算过程看getChildMeasureSpec方法。 
    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);  

    //通过父View的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,
    //然后父容器传递给子容器的,然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)
    //去测量自己,如果子View是ViewGroup 那还会递归往下测量。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
    setMeasuredDimension(
    getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),            
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}

//获取的是android:minWidth属性的值或者View背景图片的大小值
protected int getSuggestedMinimumWidth() { 
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); 
} 

//@param size参数一般表示设置了android:minHeight属性或者该View背景图片的大小值  
public static int getDefaultSize(int size, int measureSpec) {    
    int result = size;    
    int specMode = MeasureSpec.getMode(measureSpec);    
    int specSize = MeasureSpec.getSize(measureSpec);    
    switch (specMode) {    
        case MeasureSpec.UNSPECIFIED: //表示该View的大小父视图未定,设置为默认值 
            result = size;  
            break;
        case MeasureSpec.AT_MOST:    
        case MeasureSpec.EXACTLY:        
            result = specSize;  
            break;   
    }    
    return result;
}

//getDefaultSize 的第一个参数 size 等于 getSuggestedMinimumXXXX 返回的的值(建议的最小宽度
//和高度),而建议的最小宽度和高度都是由 View 的 Background 尺寸与通过设置 View 的 minXXX 
//属性共同决定的,这个 size 可以理解为 View 的默认长度,而第二个参数 measureSpec,是父 View 
//传给自己的 MeasureSpec,这个 MeasureSpec 是通过测量计算出来的,具体的计算测量过程在1.2.5
// 讲解 MeasureSpec 的时候已经讲得比较清楚了(是由父 View 的 MeasureSpec 和子 View 自己的
// LayoutParams 共同决定的)只要这个测量的 mode 不是 UNSPECIFIED,那么默认的就会用这个测量
//的数值当做 View 的宽高。



// spec: 表示父View的MeasureSpec 
// padding: 父View的Padding+子View的Margin,父View的大小减去这些边距,才能精确算出
//          子View的MeasureSpec的size
// childDimension: 表示该子View内部LayoutParams属性的值(lp.width或者lp.height)
//                 可以是wrap_content、match_parent、一个精确指(an exactly size)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { 
    // 此处代码省略 
    // 具体计算规则见 1.2.5
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}    
-> measureChildWithMargins(View child, int parentWidthMeasureSpec,
    int widthUsed, int parentHeightMeasureSpec, int heightUsed)
-> child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
-> onMeasure(widthMeasureSpec, heightMeasureSpec) 
-> setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec))

通过parentWidthMeasureSpec、parentHeightMeasureSpec和 child.getLayoutParams()计算出子view的childWidthMeasureSpec、childHeightMeasureSpec传给子view的onMeasure方法,调用getDefaultSize计算出子view的height和width。

所以我们必须先知道什么事MeasureSpec 和 LayoutParams。

1.2 什么是MeasureSpec

1.2.1 MeasureSpec 的理解

对于 View 的测量,肯定会和 MeasureSpec 接触,MeasureSpec 是由两个单词组成的,翻译过来就是 “测量规格” 或者 “测量参数”,很多博客包括官方文档对他的说明基本都是 “一个 MeasureSpec 封装了从父容器传递给子容器的布局要求”,这个 MeasureSpec 封装的是父容器传递给子容器的布局要求,而不是父容器对子容器的布局要求,“传递” 两个字很重要,更精确的说法应该是:这个 MeasureSpec 是由父 View 的 MeasureSpec 和子 View 的 LayoutParams 通过简单的计算得出一个针对子 View 的测量要求,这个测量要求就是 MeasureSpec。通过子view的MeasureSpec传给setDefaultSize方法计算出子view的实际宽高。

如果从代码上来看 view.measure(int widthMeasureSpec, int heightMeasureSpec) 的两个 MeasureSpec 是父类传递过来的,但并不是完全是父 View 的要求,而是父 View 的 MeasureSpec 和子 View 自己的 LayoutParams 共同决定的,而子 View 的 LayoutParams 其实就是我们在书写 xml 的时候设置的 layout_width 和 layout_height 转化而来的。

1.2.2 简介图
 1.2.3 组成
  • 测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)
  • 测量规格(MeasureSpec):32位、int类型
  • 测量模式(mode):占MeasureSpec的高2位
  • 测量大小(size):占MeasureSpec的低30位

注:-1 代表的是 EXACTLY,-2 是 AT_MOST

我们知道一个 MeasureSpec 是一个大小和模式的组合值,MeasureSpec 中的值是一个整型(32位)将 size 和 mode 打包成一个 Int 型,其中高两位是 mode,后面 30 位存的是 size,是为了减少对象的分配开支。MeasureSpec 类似于下图,只不过这边用的是十进制的数,而MeasureSpec 是二进制存储的。

其中,测量模式(Mode)的类型有3种:UNSPECIFIED、EXACTLY 和AT_MOST。具体如下:

1.2.4  具体使用和代码
  • MeasureSpec 被封装在View类中的一个内部类里:MeasureSpec
  • MeasureSpec类 用1个变量封装了2个数据(size,mode)通过使用二进制,将测量模式(mode) & 测量大小(size)打包成一个int值来,并提供了打包 & 解包的方法

该措施的目的 = 减少对象内存分配

// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)

// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)

// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
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)  
  // 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
  private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

  // UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
  // 通过高2位
  public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
  
  // EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
  public static final int EXACTLY = 1 << MODE_SHIFT;  

  // AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
  public static final int AT_MOST = 2 << MODE_SHIFT;  

  /**
    * makeMeasureSpec()方法
    * 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec
    **/ 
      public static int makeMeasureSpec(int size, int mode) {  
      
          return size + mode;  
      // measureSpec = size + mode;此为二进制的加法 而不是十进制
      // 设计目的:使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
      // 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100  

      }  

  /**
    * getMode()方法
    * 作用:通过measureSpec获得测量模式(mode)
    **/    

      public static int getMode(int measureSpec) {  
       
          return (measureSpec & MODE_MASK);  
          // 即:测量模式(mode) = measureSpec & MODE_MASK;  
          // MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
          //原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
          // 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值

      }  
  /**
    * getSize方法
    * 作用:通过measureSpec获得测量大小size
    **/       
      public static int getSize(int measureSpec) {  
       
          return (measureSpec & ~MODE_MASK);  
          // size = measureSpec & ~MODE_MASK;  
         // 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size  
      } 
} 
1.2.5 MeasureSpec值的计算

子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。

/**
* 源码分析:getChildMeasureSpec()
* 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
**/

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

 //参数说明
 * @param spec 父view的详细测量值(MeasureSpec) 
 * @param padding view当前尺寸的的内边距和外边距(padding,margin) 
 * @param childDimension 子视图的布局参数(宽/高)

    //父view的测量模式
    int specMode = MeasureSpec.getMode(spec);     

    //父view的大小
    int specSize = MeasureSpec.getSize(spec);     
  
    //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   
    int size = Math.max(0, specSize - padding);  
  
    //子view想要的实际大小和模式(需要计算)  
    int resultSize = 0;  
    int resultMode = 0;  
  
    //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  


    // 当父view的模式为EXACITY时,父view强加给子view确切的值
   //一般是父view设置为match_parent或者固定值的ViewGroup 
    switch (specMode) {  
    case MeasureSpec.EXACTLY:  
        // 当子view的LayoutParams>0,即有确切的值  
        if (childDimension >= 0) {  
            //子view大小为子自身所赋的值,模式大小为EXACTLY  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为MATCH_PARENT时(-1)  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            //子view大小为父view大小,模式为EXACTLY  
            resultSize = size;  
            resultMode = MeasureSpec.EXACTLY;  

        // 当子view的LayoutParams为WRAP_CONTENT时(-2)      
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  
    case MeasureSpec.AT_MOST:  
        // 道理同上  
        if (childDimension >= 0) {  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            resultSize = size;  
            resultMode = MeasureSpec.AT_MOST;  
        }  
        break;  
  
    // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大
    // 多见于ListView、GridView  
    case MeasureSpec.UNSPECIFIED:  
        if (childDimension >= 0) {  
            // 子view大小为子自身所赋的值  
            resultSize = childDimension;  
            resultMode = MeasureSpec.EXACTLY;  
        } else if (childDimension == LayoutParams.MATCH_PARENT) {  
            // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
            // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  
            resultSize = 0;  
            resultMode = MeasureSpec.UNSPECIFIED;  
        }  
        break;  
    }  
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
} 

  • 关于getChildMeasureSpec()里对子View的测量模式 & 大小的判断逻辑有点复杂;别担心,我已帮大家总结好。具体请看下表:

其中的规律总结:(以子View为标准,横向观察)

 由于UNSPECIFIED模式适用于系统内部多次measure情况,很少用到,故此处不讨论。

1.3 LayoutParams详解

在平时的开发过程中,我们一般是通过XML文件去定义布局,所以对于LayoutParams的使用可能相对较少。但是在需要动态改变View的布局参数(比如宽度、位置)时,就必须要借助这个重要的类了。本文将结合具体源码详细讲解LayoutParams的相关知识。

1.3.1 LayoutParams是什么

LayoutParams翻译过来就是布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己。从这个定义中也可以看出来LayoutParams与ViewGroup是息息相关的,因此脱离ViewGroup谈LayoutParams是没有意义的。

事实上,每个ViewGroup的子类都有自己对应的LayoutParams类,典型的如LinearLayout.LayoutParams和FrameLayout.LayoutParams等,可以看出来LayoutParams都是对应ViewGroup子类的内部类。

最基础的LayoutParams是定义在ViewGroup中的静态内部类,封装着View的宽度和高度信息,对应着XML中的layout_width和layout_height属性。主要源码如下:

public static class LayoutParams {
    public static final int FILL_PARENT = -1;
    public static final int MATCH_PARENT = -1;
    public static final int WRAP_CONTENT = -2;

    public int width;
    public int height;

    ......

    /**
     * XML文件中设置的以layout_开头的属性将在这个方法中解析
     */
    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        // 解析width和height属性
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }

    /**
     * 使用传入的width和height构建LayoutParams
     */
    public LayoutParams(int width, int height) {
        this.width = width;
        this.height = height;
    }

    /**
     * 通过传入的LayoutParams构建新的LayoutParams
     */
    public LayoutParams(LayoutParams source) {
        this.width = source.width;
        this.height = source.height;
    }
    ......
}

弄清楚了LayoutParams的意义,就可以解释为什么在XML中View的某些属性是以layout_开头的了。因为这些属性并不直接属于View,而是属于这些View的LayoutParams,这样的命名方式也就显得很贴切了。

1.3.2 MarginLayoutParams

在ViewGroup中还定义一个LayoutParams的子类——MarginLayoutParams。从名字就可以猜出来,MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的。MarginLayoutParams的主要源码如下:

public static class MarginLayoutParams extends ViewGroup.LayoutParams {
    /**
     * The left margin in pixels of the child. Margin values should be positive.
     * Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
     * to this field.
     */
    public int leftMargin;

    public int topMargin;

    public int rightMargin;

    public int bottomMargin;

    /**
     * 解析XML中以layout_开头的属性
     */
    public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();

        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
        setBaseAttributes(a,
                R.styleable.ViewGroup_MarginLayout_layout_width,
                R.styleable.ViewGroup_MarginLayout_layout_height);

        int margin = a.getDimensionPixelSize(
                com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
        if (margin >= 0) {
            leftMargin = margin;
            topMargin = margin;
            rightMargin= margin;
            bottomMargin = margin;
        } else {
            int horizontalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
            int verticalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);

            if (horizontalMargin >= 0) {
                leftMargin = horizontalMargin;
                rightMargin = horizontalMargin;
            } else {
                leftMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                        UNDEFINED_MARGIN);
                rightMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                        UNDEFINED_MARGIN);            
            }
            .........
        a.recycle();
    }
}

从源码中也可以看到,MarginLayoutParams主要就是增加了上下左右4种外间距。在构造方法中,先是获取了margin属性;如果该值不合法,就获取horizontalMargin;如果该值不合法,再去获取leftMargin和rightMargin属性(verticalMargin、topMargin和bottomMargin同理)。我们可以据此总结出这几种属性的优先级:

margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释:

Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

也就是说,如果我们更改了MarginLayoutParams中这几种属性的值,就应该调用View的setLayoutParams方法重新设置更改后的MarginLayoutParams,这样我们所做的更改才会生效(其实主要是因为在setLayoutParams方法中调用了requestLayout方法)。、

1.3.3 LayoutParams与View如何建立联系

说了这么多LayoutParams的作用,这里再简单谈一下LayoutParams是何时被创建出来的,又是怎样和View建立联系。归纳起来,View的使用方式无非有两种:在XML中定义View和在Java代码中直接生成View对应的实例对象,因此我们也分这两个方向进行探索。

在Java代码中实例化View
在代码中实例化View后,如果调用setLayoutParams方法为View设置指定的LayoutParams,那么LayoutParams就已经和View建立起联系了。针对不同的ViewGroup子类,我们要选择合适的LayoutParams。

实例化View后,一般还会调用addView方法将View对象添加到指定的ViewGroup中。可以想到,在ViewGroup中肯定也会为还没有LayoutParams的子View设置合适的LayoutParams,下文将通过分析代码说明这一过程。ViewGroup实现了以下五种addView方法的重载版本:

/**
 * 重载方法1:添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 */
public void addView(View child) {
    addView(child, -1);
}

/**
 * 重载方法2:在指定位置添加一个子View
 * 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
 * @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
 */
public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

/**
 * 重载方法3:添加一个子View
 * 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
 */
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();  // 生成当前ViewGroup默认的LayoutParams
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

/**
 * 重载方法4:添加一个子View,并使用传入的LayoutParams
 */
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

/**
 * 重载方法4:在指定位置添加一个子View,并使用传入的LayoutParams
 */
public void addView(View child, int index, LayoutParams params) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

以上代码已经添加了必要的注释,这里就不再赘述了。总之,只要子View没有LayoutParams,ViewGroup就会为其设置默认的LayoutParams。默认的LayoutParams对象通过generateDefaultLayoutParams方法生成,ViewGroup中的代码实现如下:

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

实际上,addView的前四个重载方法最终都会调用第五个重载版本,即addView(View child, int index, LayoutParams params)。在这个方法中调用了requestLayout和invalidate方法,引起视图重新布局(onMeasure->onLayout->onDraw)和重绘。这也很好理解,既然我们添加了新的View,那么原有的视图结构自然会发生变化。同时,在这个方法中还调用了addViewInner方法,关键代码如下:

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    .....
    if (mTransition != null) {
        mTransition.addChild(this, child);
    }

    if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
        params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
    }

    if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
        child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
    } else {
        child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
    }

    if (index < 0) {
        index = mChildrenCount;
    }

    addInArray(child, index);

    // tell our children
    if (preventRequestLayout) {
        child.assignParent(this);
    } else {
        child.mParent = this;
    }
    .....
}

可以看到,在代码①的位置先判断传入的LayoutParams是否合法,ViewGroup中这个方法只是简单判断了传入的LayoutParams是否为空:

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return  p != null;
}

如果LayoutParams不合法,将使用generateLayoutParams方法对其进行转化,ViewGroup中这个方法仅仅将传入的LayoutParams原样返回:

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return p;
}

最后,在代码②的位置为子View设置LayoutParams。这里分为了两种情况:如果不希望引起子View重新布局(onMeasure->onLayout->onDraw)就直接为子View的LayoutParams变量赋值;否则调用子View的setLayoutParams方法传入LayoutParams。到这一步,LayoutParams和View的联系就建立起来了。

1.3.4 在XML中定义View

在XML中定义的View首先会被解析为对应的实例化对象,这项工作将通过LayoutInflater的inflate方法完成。inflater方法有多个重载版本,最终将会调用inflate(XmlPullParser parser,ViewGroup root, boolean attachToRoot),关键代码如下:

/**
 * 解析XML文件中的View
 * @param parser 解析器
 * @param root 父容器(可能为null)
 * @param attachToRoot View是否需要附加到父容器中
 */
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    ......
    View result = root;

    ......
    final String name = parser.getName();

    if (TAG_MERGE.equals(name)) { // 针对<merge>标签
        ......
    } else { // 针对普通标签
        // ① 通过XML生成对应的View对象
        // Temp指的是XML文件中的根View
        final View temp = createViewFromTag(root, name, inflaterContext, attrs); 

        ViewGroup.LayoutParams params = null;

        if (root != null) {
            // ② 通过XML中的布局参数生成对应的LayoutParams
            params = root.generateLayoutParams(attrs); 
            if (!attachToRoot) {
                // ③ 如果不需要将View附加到父容器中,就直接为View设置LayoutParams
                temp.setLayoutParams(params);
            }
        }

        rInflateChildren(parser, temp, attrs, true); // 解析View中包含的子View(如果存在的话)

        // ④ 如果父容器不为null,且需要将View附加到父容器中,就使用addView方法
        if (root != null && attachToRoot) {
            root.addView(temp, params);
        }

        // Decide whether to return the root that was passed in or the top view found in xml.
        if (root == null || !attachToRoot) {
            result = temp;
        }
    }
    ......
    return result;
}

可以看到,如果父容器(ViewGroup)不为空,在代码②的位置将通过父容器的generateLayoutParams方法生成LayoutParams,这也间接说明了LayoutParams是与ViewGroup息息相关的,脱离ViewGroup谈LayoutParams是没有意义的。

在代码③的位置,如果attachToRoot参数为false,代表不需要将View添加到父容器中,那就直接为View设置LayoutParams;否则在代码④的位置通过addView(temp, params)将View添加到父容器中。到了这一步,后续逻辑就和在Java代码中实例化View是一样的了。

其实最典型的例子就是在Activity中调用setContentView方法,系统会通过LayoutInflater将整个XML文件解析为View Tree,从根布局开始为每个View和ViewGroup设置相应的LayoutParams。

1.3.5 自定义LayoutParams

如果我们需要自定义ViewGroup的话,一般也会自定义LayoutParams,这样可以提供一些个性化的布局参数。为了支持设置外间距,自定义的LayoutParams一般会选择继承ViewGroup.MarginLayoutParams。此外,还需要在XML文件中定义declare-styleable资源属性,一般会创建一个名为attrs.xml文件放置这些属性。这里假设我们要实现一个名为SimpleViewGroup的自定义ViewGroup,示例代码如下:

<resources>
    <declare-styleable name="SimpleViewGroup_Layout">
        <!-- 自定义的属性 -->
        <attr name="layout_simple_attr" format="integer"/>
        <!-- 使用系统预置的属性 -->
        <attr name="android:layout_gravity"/>
    </declare-styleable>
</resources>

这里将declare-styleable的name设置为SimpleViewGroup_Layout,也就是自定义ViewGroup的名称加上_Layout。这里一共定义了两个属性,第一个属性使用了自定义的名称,需要提供name和format参数,format用于限制自定义属性的类型;第二个属性使用了系统预置的属性,比如这里的android:layout_gravity,好处是可以让用户使用熟悉的属性(在系统提供的属性语义合适时可以考虑这种方式)。不过要注意,这种情况下就不要为它定义format参数了,因为系统已经设置好了。

之后,需要在自定义的LayoutParams中解析这些属性,下面是一个简单的示例:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public int simpleAttr;
    public int gravity;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析布局属性
        TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
        simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
        gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);

        typedArray.recycle();//释放资源
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(MarginLayoutParams source) {
        super(source);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}

后,我们还需要重写ViewGroup中几个与LayoutParams相关的方法,示例代码如下:

// 检查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 
    return p instanceof SimpleViewGroup.LayoutParams;
}

// 生成默认的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 
    return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}

// 对传入的LayoutParams进行转化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 
    return new SimpleViewGroup.LayoutParams(p);
}

// 对传入的LayoutParams进行转化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 
    return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}
1.3.6  LayoutParams常见的子类

在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:

  • ViewGroup.MarginLayoutParams
  • FrameLayout.LayoutParams
  • LinearLayout.LayoutParams
  • RelativeLayout.LayoutParams
  • RecyclerView.LayoutParams
  • GridLayoutManager.LayoutParams
  • StaggeredGridLayoutManager.LayoutParams
  • ViewPager.LayoutParams
  • WindowManager.LayoutParams
  • ConstrainLayout.LayoutParams
1.3.7 view.getLayoutParams 的实例instance of是父布局对应类型
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <LinearLayout
        android:id="@+id/ll"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <ImageView
            android:id="@+id/image_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

比如id为ll的view.getLayoutParams instance of ConstrainLayout.LayoutParams,因为ll的布局参数layout_constraintTop_toTopOf等都是ConstrainLayout的属性,所以view.getLayoutParams可以强转成ConstrainLayout.LayoutParams,而不是LinearLayout.LayoutParams。

二 实例分析

到目前为止,基本把 Measure 主要原理都过了一遍,接下来我们来结合实例来讲解整个 match 的过程,首先看下面的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"    
   android:id="@+id/linear"
   android:layout_width="match_parent"    
   android:layout_height="wrap_content"    
   android:layout_marginTop="50dp"    
   android:background="@android:color/holo_blue_dark"    
   android:paddingBottom="70dp"    
   android:orientation="vertical">    
   <TextView        
    android:id="@+id/text"       
    android:layout_width="match_parent"     
    android:layout_height="wrap_content"  
    android:background="@color/material_blue_grey_800"       
    android:text="TextView"        
    android:textColor="@android:color/white"        
    android:textSize="20sp" />    
   <View       
      android:id="@+id/view"       
     android:layout_width="match_parent" 
     android:layout_height="150dp"    
     android:background="@android:color/holo_green_dark" />
</LinearLayout>

上面的代码对应出来的布局是下面的一张图

说明:

整个图是一个 DecorView,DecorView 可以理解成整个窗口的根 View,DecorView 是一个 FrameLayout,它里边有 LinearLayout 这个子View 这个 LinearLayout 比较重要,它包含一个 title 和一个 content,title 很好理解其实就是 TitleBar 或者 ActionBar,content 就更简单了,setContentView() 方法你应该用过吧,android.R.id.content 你应该听过吧,没错就是它,content 是一个 FrameLayout,你写的页面布局通过 setContentView 加进来就成了 content 的直接子 View。

整个 View 的布局图如下:

这张图在下面分析 measure 方法的时候,会经常用到,主要用于了解递归的时候 view 的 measure 顺序

说明:

  • header 是个 ViewStub,用来惰性加载 ActionBar,为了便于分析整个测量过程,我们把 Theme 设成 NoActionBar,避免 ActionBar 相关的 measure 干扰整个过程,这样可以忽略掉 ActionBar 的测量。
  • 包含 Header(ActionBar)和 id/content 的那个父 View,我不知道叫什么名字好,我们姑且叫它 ViewRoot,它是垂直的 LinearLayout,放着整个页面除 statusBar 的之外所有的东西,叫它 ViewRoot 应该还 ok,一个代号而已。

既然我们知道整个 View 的 Root 是 DecorView,那么 View 的绘制是从哪里开始的呢,我们知道每个 Activity 均会创建一个 PhoneWindow 对象,是 Activity 和整个 View 系统交互的接口,每个 Window 都对应着一个 View 和一个 ViewRootImpl,Window 和 View 通过 ViewRootImpl 来建立联系,对于 Activity 来说,ViewRootImpl 是连接 WindowManager 和 DecorView 的纽带,绘制的入口是由 ViewRootImpl 的 performTraversals 方法来发起 Measure,Layout,Draw 等流程的。

我们来看 ViewRootImpl 的 performTraversals 方法:

private void performTraversals() { 
    ...... 
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); 
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); 
    ...... 
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    performLayout(lp, mWidth, mHeight);
    ...... 
    performDraw(); 
    ......
}


private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
     if (mView == null) {
         return;
     }
     Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
     try {
         mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
     } finally {
         Trace.traceEnd(Trace.TRACE_TAG_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; 
    ...... 
    } 
    return measureSpec; 
}

performMeasure 中我们看到的 mView 其实就是 DecorView,View 的绘制从 DecorView 开始, 在 mView.measure() 的时候调用 getRootMeasureSpec 获得两个 MeasureSpec 做为参数,getRootMeasureSpec 的两个参数 mWith 和 mHeight 是屏幕的宽度和高度, lp 是 WindowManager.LayoutParams,它的 lp.width 和 lp.height 的默认值是 MATCH_PARENT,所以通过 getRootMeasureSpec 生成的测量规格 MeasureSpec 的 mode 是 EXACTLY ,size 是屏幕的高宽。

因为 DecorView 是一个 FrameLayout 那么接下来会进入 FrameLayout 的 measure 方法,measure 的两个参数就是刚才调用 getRootMeasureSpec 生成的两个 MeasureSpec,DecorView 的测量开始了。

首先是 DecorView 的 MeasureSpec ,根据上面的分析 DecorView 的 MeasureSpec 是 Windows 传过来的,我们画出 DecorView 的 MeasureSpec 图:

注:
1、-1 代表的是 EXACTLY,-2 是 AT_MOST
2、由于屏幕的像素是 1440x2560,所以 DecorView 的 MeasureSpec 的 size 对应于这两个值

那么接下来在 FrameLayout 的 onMeasure() 方法 DecorView 开始 for 循环测量自己的子 View,测量完所有的子 View 再来测量自己,由下图可知,接下来要测量 ViewRoot 的大小

//FrameLayout 的测量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    ......
    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) {   
    // 遍历自己的子View,只要不是GONE的都会参与测量,measureChildWithMargins方法在最上面
    // 的源码已经讲过了,如果忘了回头去看看,基本思想就是父View把自己的MeasureSpec 
    // 传给子View结合子View自己的LayoutParams 算出子View 的MeasureSpec,然后继续往下传,
    // 传递叶子节点,叶子节点没有子View,根据传下来的这个MeasureSpec测量自己就好了。
             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);  
             ......
        }
    }
    ......
//所有的孩子测量之后,经过一系类的计算之后通过setMeasuredDimension设置自己的宽高,
//对于FrameLayout 可能用最大的子View的大小,对于LinearLayout,可能是高度的累加,
//具体测量的原理去看看源码。总的来说,父View是等所有的子View测量结束之后,再来测量自己。
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),        
resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));
......
} 

 DecorView 测量 ViewRoot 的时候把自己的 widthMeasureSpec 和 heightMeasureSpec 传进去了,接下来就要去看measureChildWithMargins 的源码了

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
    int widthUsed, int parentHeightMeasureSpec, int heightUsed) { 

    // 子View的LayoutParams,你在xml的layout_width和layout_height,
    // layout_xxx的值最后都会封装到这个LayoutParams。
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();   

    //根据父View的测量规格和父View自己的Padding,还有子View的Margin和已经用掉的空间大小(widthUsed)
    //就能算出子View的MeasureSpec,具体计算过程看getChildMeasureSpec方法。 
    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);  

    //通过父View的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,
    //然后父容器传递给子容器的,然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)
    //去测量自己,如果子View是ViewGroup 那还会递归往下测量。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}

ViewRoot 是系统的 View,它的 LayoutParams 默认都是 match_parent,根据我们文章最开始介绍的 MeasureSpec 的计算规则,ViewRoot 的 MeasureSpec mode 应该等于 EXACTLY( DecorView MeasureSpec 的 mode 是 EXACTLY,ViewRoot 的 layoutparams 是 match_parent),size 也等于 DecorView 的 size,所以 ViewRoot 的 MeasureSpec 图如下:

算出 ViewRoot 的 MeasureSpec 之后,开始调用 ViewRoot.measure 方法去测量 ViewRoot 的大小,然而 ViewRoot 是一个 LinearLayout ,ViewRoot.measure 最终会执行 LinearLayout 的 onMeasure 方法,LinearLayout 的onMeasure 方法又开始逐个测量它的子 View,上面的 measureChildWithMargins 方法又会被调用,那么根据 View 的层级图,接下来测量的是 header(ViewStub),由于 header 的 Gone,所以直接跳过不做测量工作,所以接下来轮到 ViewRoot 的第二个 child content(android.R.id.content),我们要算出这个 content 的 MeasureSpec,所以又要拿 ViewRoot 的 MeasureSpec 和 android.R.id.content 的 LayoutParams 做计算了,计算过程就是调用 getChildMeasureSpec 的方法。

protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
    int widthUsed, int parentHeightMeasureSpec, int heightUsed) { 

    // 子View的LayoutParams,你在xml的layout_width和layout_height,
    // layout_xxx的值最后都会封装到这个LayoutParams。
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();   

    //根据父View的测量规格和父View自己的Padding,还有子View的Margin和已经用掉的空间大小(widthUsed)
    //就能算出子View的MeasureSpec,具体计算过程看getChildMeasureSpec方法。 
    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);  

    //通过父View的MeasureSpec和子View的自己LayoutParams的计算,算出子View的MeasureSpec,
    //然后父容器传递给子容器的,然后让子View用这个MeasureSpec(一个测量要求,比如不能超过多大)
    //去测量自己,如果子View是ViewGroup 那还会递归往下测量。
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

}


public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
    int specMode = MeasureSpec.getMode(spec);  //获得父View的mode  
    int specSize = MeasureSpec.getSize(spec);  //获得父View的大小  
//父View的大小-自己的Padding+子View的Margin,得到值才是子View可能的最大值
    int size = Math.max(0, specSize - padding);
    .....
}

由上面的代码可以算出 android.R.id.content 的 MeasureSpec 的 size,由于 ViewRoot 的 mPaddingBottom = 100px (这个可能和状态栏的高度有关,我们测量最后会发现 id/statusBarBackground 的 View 的高度刚好等于 100px,ViewRoot 是系统 View,它的 Padding 我们没法改变,所以计算出来 Content(android.R.id.content)的 MeasureSpec 的高度少了100px ,它的宽高的 mode 根据算出来也是 EXACTLY(ViewRoot 是 EXACTLY 和 android.R.id.content 是 match_parent)。所以 Content(android.R.id.content)的M easureSpec 如下(高度少了 100px):

Content(android.R.id.content)是 FrameLayout,递归调用开始准备计算 id/linear 的 MeasureSpec,我们先给出结果: 

图中有两个要注意的地方:

  • id/linear 的 heightMeasureSpec 的 mode = AT_MOST,因为 id/linear 的 LayoutParams 的l ayout_height=“wrap_content”
  • id/linear 的 heightMeasureSpec 的 size 少了 200px

由上面的代码

padding=mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed;
int size = Math.max(0, specSize - padding);

由于 id/linear 的 android:layout_marginTop=“50dp” 使得 lp.topMargin = 200px (本设备的 density=4,px=4*pd),在计算后 id/linear 的heightMeasureSpec 的 size 少了200px。(布局代码前面已给出,可自行查看 id/linear 控件 xml 中设置的属性)

linear.measure 接着往下算 linear 的子 View 的 MeasureSpec,看下 View 层级图,往下走应该是 id/text,接下来是计算 id/text 的 MeasureSpec,直接看图,mode = AT_MOST ,size 少了 280,大家可以自己理解原因。

算出 id/text 的 MeasureSpec 后,接下来 text.measure(childWidthMeasureSpec, childHeightMeasureSpec);准备测量 id/text 的高宽,这时候已经到底了,id/text 是 TextView,已经没有子类了,这时候跳到 TextView 的 onMeasure 方法了。TextView 拿着刚才计算出来的heightMeasureSpec(mode = AT_MOST,size = 1980),这个就是对 TextView 的高度和宽度的约束,进到 TextView 的 onMeasure(widthMeasureSpec,heightMeasureSpec) 方法,在 onMeasure 方法执行调试过程中,我们发现下面的代码:

TextView 字符的高度(也就是 TextView 的 content 高度)测出来 = 107px,107px 并没有超过 1980px (允许的最大高度),所以实际测量出来 TextView 的高度是 107px。

最终算出 id/text 的 mMeasureWidth = 1440px,mMeasureHeight = 107px。

贴一下布局代码,免得忘了具体布局。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"    
   android:id="@+id/linear"
   android:layout_width="match_parent"    
   android:layout_height="wrap_content"    
   android:layout_marginTop="50dp"    
   android:background="@android:color/holo_blue_dark"    
   android:paddingBottom="70dp"    
   android:orientation="vertical">    
   <TextView        
    android:id="@+id/text"       
    android:layout_width="match_parent"     
    android:layout_height="wrap_content"  
    android:background="@color/material_blue_grey_800"       
    android:text="TextView"        
    android:textColor="@android:color/white"        
    android:textSize="20sp" />    
   <View       
      android:id="@+id/view"       
     android:layout_width="match_parent" 
     android:layout_height="150dp"    
     android:background="@android:color/holo_green_dark" />
</LinearLayout>

 TextView 的高度已经测量出来了,接下来测量 id/linear 的第二个 child(id/view),同样的原理测出 id/view 的 MeasureSpec。

id/view 的 MeasureSpec 计算出来后,调用 view.measure(childWidthMeasureSpec, childHeightMeasureSpec) 的测量 id/view 的高宽,之前已经说过 View measure 的默认实现是

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
    setMeasuredDimension(
    getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),            
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

最终算出 id/view 的 mMeasureWidth=1440px,mMeasureHeight=600px。

id/linear 的子 View 的高度都计算完毕了,接下来 id/linear 就通过所有子 View 的测量结果计算自己的高宽,id/linear 是 LinearLayout,所以它的高度计算简单理解就是子 View 的高度的累积+自己的 Padding。

最终算出 id/linear 出来后,id/content 就要根据它唯一的子 View id/linear 的测量结果和自己的之前算出的 MeasureSpec 一起来测量自己的结果,具体计算的逻辑去看 FrameLayout onMeasure 函数的计算过程。以此类推,接下来测量 ViewRoot,然后再测量 id/statusBarBackground,虽然不知道 id/statusBarBackground 是什么,但是调试的过程中,测出的它的高度 =100px,和 id/content 的paddingTop 刚好相等。最后测量 DecorView 的高宽,最终整个测量过程结束。所有的 View 的大小测量完毕。所有的 getMeasureWidth 和 getMeasureWidth 都已经有值了。Measure 分析到此为止。

  • 21
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值