View 和 ViewGroup的 onMeasure

1. onMeasure何时会被调用

  onMeasure方法的做用时测量空间的大小,何时须要测量控件的大小呢?咱们举个栗子,作饭的时候咱们炒一碗菜,炒菜的过程咱们并不要求知道这道菜有多少份量,只有在菜作熟了咱们要拿个碗盛放的时候,咱们才须要掂量拿多大的碗盛放,这时候咱们就要对菜的份量进行估测。 
  而咱们的控件也正是如此,建立一个View(执行构造方法)的时候不须要测量控件的大小,只有将这个view放入一个容器(父控件)中的时候才须要测量,而这个测量方法就是父控件唤起调用的。当控件的父控件要放置该控件的时候,父控件会调用子控件的onMeasure方法询问子控件:“你有多大的尺寸,我要给你多大的地方才能容纳你?”,而后传入两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可得到的空间以及关于这个空间的约束条件(比如我在思考须要多大的碗盛菜的时候我要看一下碗柜里最大的碗有多大,菜的份量不能超过这个容积,这就是碗对菜的约束),子控件拿着这些条件就能正确的测量自身的宽高了。 java

2. onMeasure方法执行流程

  上面说到onMeasure方法是由父控件调用的,全部父控件都是ViewGroup的子类,ViewGroup是一个抽象类,它里面有一个抽象方法onLayout,这个方法的做用就是摆放它全部的子控件(安排位置),由于是抽象类,不能直接new对象,因此咱们在布局文件中可使用View可是不能直接使用 ViewGroup。android

  在给子控件肯定位置以前,必需要获取到子控件的大小(只有肯定了子控件的大小才能正确的肯定上下左右四个点的坐标),而ViewGroup并无重写View的onMeasure方法,也就是说抽象类ViewGroup没有为子控件测量大小的能力,它只能测量本身的大小。可是既然ViewGroup是一个能容纳子控件的容器,系统固然也考虑到测量子控件的问题,因此ViewGroup提供了三个测量子控件相关的方法(measuireChildren\measuireChild\measureChildWithMargins),只是在ViewGroup中没有调用它们,因此它自己不具有为子控件测量大小的能力,可是他有这个潜力哦。布局

  为何都有测量子控件的方法了而ViewGroup中不直接重写onMeasure方法,而后在onMeasure中调用呢?由于不一样的容器摆放子控件的方式不一样,好比RelativeLayout,LinearLayout这两个ViewGroup的子类,它们摆放子控件的方式不一样,有的是线性摆放,而有的是叠加摆放,这就致使测量子控件的方式会有所差异,因此ViewGroup就干脆不直接测量子控件,他的子类要测量子控件就根据本身的布局特性重写onMeasure方法去测量。这么看来ViewGroup提供的三个测量子控件的方法岂不是没有做用?答案是NO,既然提供了就确定有做用,这三个方法只是按照一种通用的方式去测量子控件,不少ViewGruop的子类测量子控件的时候就使用了ViewGroup的measureChildxxx系列方法;还有一个做用就是为咱们自定义ViewGroup提供方便咯,自定义ViewGroup我会在之后的博客中专门探讨,这里就不大费篇章了。学习

  测量的时候父控件的onMeasure方法会遍历他全部的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,而后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的全部子控件都会完成测量工做;而后开始测量第二个子控件…;最后父控件全部的子控件都完成测量之后会调用setMeasureDimension方法保存本身的测量大小。值得注意的是,这个过程不仅执行一次,也就是说有可能重复执行,由于有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会从新测量一遍。ui

举个栗子,看下图: 
    这里写图片描述这里写图片描述spa

下面是测量的时序图: 
这里写图片描述 .net

3. MeasureSpec类

  上面说到MeasureSpec约束是由父控件传递给子控件的,这个类里面到底封装了什么东西?咱们看一看源码:code

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0x3 << MODE_SHIFT;
    /**
     * 父控件不强加任何约束给子控件,它能够是它想要任何大小
     */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    /**
     * 父控件已为子控件肯定了一个确切的大小,孩子将被给予这些界限,无论子控件本身但愿的是多大
     */
    public static final int EXACTLY = 1 << MODE_SHIFT;
    /**
     * 父控件会给子控件尽量大的尺寸
     */
    public static final int AT_MOST = 2 << MODE_SHIFT;

    /**
     * 根据所提供的大小和模式建立一个测量规范
     */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    /**
     * 从所提供的测量规范中提取模式
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    /**
     * 从所提供的测量规范中提取尺寸
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
}

        从源码中咱们知道,MeasureSpec其实就是尺寸和模式经过各类位运算计算出的一个整型值,它提供了三种模式,还有三个方法(合成约束、分离模式、分离尺寸)。在《Android自定义View(1、初体验)》(若是不太了解的能够参考这篇博客)这篇博客中,咱们获得了这三种模式对应的意思和布局文件中的参数值的对应关系(父控件填充屏幕MATCH_PARENT的状况,若是其余状况,请参考下面getChildMeasureSpec方法的结果):orm

约束 布局参数 说明
UNSPECIFIED(未指定)   0 父控件没有对子控件施加任何约束,子控件能够获得任意想要的大小(使用较少)。
EXACTLY(彻底) match_parent/具体宽高值 1073741824 父控件给子控件决定了确切大小,子控件将被限定在给定的边界里而忽略它自己大小。特别说明若是是填充父窗体,说明父控件已经明确知道子控件想要多大的尺寸了(就是剩余的空间都要了)栗子:碗柜最大的碗就这么大,菜有多少都只能盛到这个最大的碗里,盛不下的我就管不了了(吃掉或者倒掉)
AT_MOST(至多) wrap-content -2147483648 子控件至多达到指定大小的值。包裹内容就是父窗体并不知道子控件到底须要多大尺寸(具体值),须要子控件本身测量以后再让父控件给他一个尽量大的尺寸以便让内容所有显示但不能超过包裹内容的大小栗子:碗柜有各类大小的碗,菜少就拿小碗放,菜多就拿大碗放,可是不能浪费碗的容积,要保证碗恰好盛满菜

 

4. 从ViewGroup的onMeasure到View的onMeasure

①. ViewGroup中三个测量子控件的方法:

  经过上面的介绍,咱们知道,若是要自定义ViewGroup就必须重写onMeasure方法,在这里测量子控件的尺寸。子控件的尺寸怎么测量呢?ViewGroup中提供了三个关于测量子控件的方法:xml

/**
  *遍历ViewGroup中全部的子控件,调用measuireChild测量宽高
  */
 protected void measureChildren (int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            //测量某一个子控件宽高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

/**
* 测量某一个child的宽高
*/
protected void measureChild (View child, int parentWidthMeasureSpec,
       int parentHeightMeasureSpec) {
   final LayoutParams lp = child.getLayoutParams();
   //获取子控件的宽高约束规则
   final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
           mPaddingLeft + mPaddingRight, lp. width);
   final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
           mPaddingTop + mPaddingBottom, lp. height);

   child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

/**
* 测量某一个child的宽高,考虑margin值
*/
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);
}

  这三个方法分别作了那些工做你们应该比较清楚了吧?measureChildren 就是遍历全部子控件挨个测量,最终测量子控件的方法就是measureChild 和measureChildWithMargins 了,咱们先了解几个知识点:

  • measureChildWithMargins跟measureChild的区别就是父控件支不支持margin属性

  支不支持margin属性对子控件的测量是有影响的,好比咱们的屏幕是1080x1920的,子控件的宽度为填充父窗体,若是使用了marginLeft并设置值为100; 
在测量子控件的时候,若是用measureChild,计算的宽度是1080,而若是是使用measureChildWithMargins,计算的宽度是1080-100 = 980。

  • 怎样让ViewGroup支持margin属性?

  ViewGroup中有两个内部类ViewGroup.LayoutParams和ViewGroup. MarginLayoutParams,MarginLayoutParams继承自LayoutParams ,这两个内部类就是VIewGroup的布局参数类,好比咱们在LinearLayout等布局中使用的layout_width\layout_hight等以“layout_ ”开头的属性都是布局属性。在View中有一个mLayoutParams的变量用来保存这个View的全部布局属性。

  • LayoutParams和MarginLayoutParams 的关系: 
    LayoutParams 中定义了两个属性(如今知道咱们用的layout_width\layout_hight的来头了吧?):
<declare-styleable name= "ViewGroup_Layout">
    <attr name ="layout_width" format="dimension">
        <enum name ="fill_parent" value="-1" />
        <enum name ="match_parent" value="-1" />
        <enum name ="wrap_content" value="-2" />
    </attr >
    <attr name ="layout_height" format="dimension">
        <enum name ="fill_parent" value="-1" />
        <enum name ="match_parent" value="-1" />
        <enum name ="wrap_content" value="-2" />
    </attr >
</declare-styleable >

MarginLayoutParams 是LayoutParams的子类,它固然也延续了layout_width\layout_hight 属性,可是它扩充了其余属性:

< declare-styleable name ="ViewGroup_MarginLayout">
    <attr name ="layout_width" />   <!--使用已经定义过的属性-->
    <attr name ="layout_height" />
    <attr name ="layout_margin" format="dimension"  />
    <attr name ="layout_marginLeft" format= "dimension"  />
    <attr name ="layout_marginTop" format= "dimension" />
    <attr name ="layout_marginRight" format= "dimension"  />
    <attr name ="layout_marginBottom" format= "dimension"  />
    <attr name ="layout_marginStart" format= "dimension"  />
    <attr name ="layout_marginEnd" format= "dimension"  />
</declare-styleable >

是否是对布局属性有了一个全新的认识?原来咱们使用的margin属性是这么来的。

  • 为何LayoutParams 类要定义在ViewGroup中? 
      你们都知道ViewGroup是全部容器的基类,一个控件须要被包裹在一个容器中,这个容器必须提供一种规则控制子控件的摆放,好比你的宽高是多少,距离那个位置多远等。因此ViewGroup有义务提供一个布局属性类,用于控制子控件的布局属性。

  • 为何View中会有一个mLayoutParams 变量? 
      咱们在以前学习自定义控件的时候学过自定义属性,咱们在构造方法中,初始化布局文件中的属性值,咱们姑且把属性分为两种。一种是本View的绘制属性,好比TextView的文本、文字颜色、背景等,这些属性是跟View的绘制相关的。另外一种就是以“layout_”打头的叫作布局属性,这些属性是父控件对子控件的大小及位置的一些描述属性,这些属性在父控件摆放它的时候会使用到,因此先保存起来,而这些属性都是ViewGroup.LayoutParams定义的,因此用一个变量保存着。

  • 怎样让ViewGroup支持margin属性? 
    这一部分知识点咱们在下一篇博客《自定义ViewGroup》中再去讲解 

②. getChildMeasureSpec方法

  measureChildWithMargins跟measureChild 都调用了这个方法,其做用就是经过父控件的宽高约束规则和父控件加在子控件上的宽高布局参数生成一个子控件的约束。咱们知道View的onMeasure方法须要两个参数(父控件对View的宽高约束),这个宽高约束就是经过这个方法生成的。有人会问为何不直接拿着子控件的宽高参数去测量子控件呢?打个比方,父控件的宽高约束为wrap_content,而子控件为match_perent,是否是颇有意思,父控件说个人宽高就是包裹个人子控件,个人子控件多大我就多大,而子控件说个人宽高填充父窗体,父控件多大我就多大。最后该怎么肯定大小呢?因此咱们须要为子控件从新生成一个新的约束规则。只要记住,子控件的宽高约束规则是父控件调用getChildMeasureSpec方法生成。 
    这里写图片描述

getChildMeasure方法代码很少,也比较简单,就是几个switch将各类状况考虑后生成一个子控件的新的宽高约束,这个方法的结果可以用一个表来归纳:

父控件的约束规则 子控件的宽高属性 子控件的约束规则 说明
EXACTLY(父控件是填充父窗体,或者具体size值) 具体的size(20dip)/MATCH_PARENT EXACTLY 子控件若是是具体值,约束尺寸就是这个值,模式为肯定的;子控件为填充父窗体,约束尺寸是父控件剩余大小,模式为肯定的。
  WRAP-CONTENT AT_MOST 子控件若是是包裹内容,约束尺寸值为父控件剩余大小 ,模式为至多
AT_MOST(父控件是包裹内容) 具体的size(20dip) EXACTLY 子控件若是是具体值,约束尺寸就是这个值,模式为肯定的;
  MATCH_PARENT/WRAP_CONTENT AT_MOST 子控件为填充父窗体或者包裹内容 ,约束尺寸是父控件剩余大小 ,模式为至多
UNSPECIFIED(父控件未指定) 具体的size(20dip) EXACTLY 子控件若是是具体值,约束尺寸就是这个值,模式为肯定的;
  MATCH_PARENT/WRAP_CONTENT UNSPECIFIED 子控件为填充父窗体或者包裹内容 ,约束尺寸0,模式为未指定


进行了上面的步骤,接下来就是在measureChildWithMarginsh或者measureChild中 调用子控件的measure方法测量子控件的尺寸了。

 

③. View的onMeasure

  View中onMeasure方法已经默认为咱们的控件测量了宽高,咱们看看它作了什么工做:

protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
 * 为宽度获取一个建议最小值
 */
protected int getSuggestedMinimumWidth () {
    return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}
/**
 * 获取默认的宽高值
 */
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:
        result = size;
        break;
    case MeasureSpec. AT_MOST:
    case MeasureSpec. EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

从源码咱们了解到:

  • 若是View的宽高模式为未指定,他的宽高将设置为android:minWidth/Height =”“值与背景宽高值中较大的一个;
  • 若是View的宽高 模式为 EXACTLY (具体的size ),最终宽高就是这个size值;
  • 若是View的宽高模式为EXACTLY (填充父控件 ),最终宽高将为填充父控件;
  • 若是View的宽高模式为AT_MOST (包裹内容),最终宽高也是填充父控件。

也就是说若是咱们的自定义控件在布局文件中,只须要设置指定的具体宽高,或者MATCH_PARENT 的状况,咱们能够不用重写onMeasure方法。

但若是自定义控件须要设置包裹内容WRAP_CONTENT ,咱们须要重写onMeasure方法,为控件设置须要的尺寸;默认状况下WRAP_CONTENT 的处理也将填充整个父控件。

 

④. setMeasuredDimension

  onMeasure方法最后须要调用setMeasuredDimension方法来保存测量的宽高值,若是不调用这个方法,可能会产生不可预测的问题。


这篇博客咱们学习了onMeasure方法测量控件大小的流程,以及里面执行的一些细节,总结一下知识点:

  1. 测量控件大小是父控件发起的
  2. 父控件要测量子控件大小,须要重写onMeasure方法,而后调用measureChildren或者measureChildWithMargin方法
  3. on Measure方法的参数是经过getChildMeasureSpec生成的
  4. 若是咱们自定义控件须要使用wrap_content,咱们须要重写onMeasure方法
  5. 测量控件的步骤: 
    父控件onMeasure->measureChildren`measureChildWithMargin->getChildMeasureSpec-> 
    子控件的
    measure->onMeasure->setMeasureDimension-> 
    父控件
    onMeasure结束调用setMeasureDimension`保存本身的大小
相关文章
相关标签/搜索