Android View ViewGroup 的measure过程

首先扯点别的:“光阴似箭,日月如梭”,这句话小学就知道了,随着年龄的增长,越来越觉得如此,人生如白驹过隙。毕业工作快一年了,但是感觉自己Android方面的基础知识还是不扎实,所以看看开发艺术探索,巩固提高自己。

View 的measure过程:measure 过程确定了View的测量宽/高。measure完成以后,就可以通过getMeasuredWidth和getMeasuredHeight来获取View测量后的宽高了,几乎所有的情况下它都等于View的最终宽高。

理解MeasureSpec

为了更好的理解View的measure过程,首先要了解MeasureSpec这个概念。MeasureSpec代表一个32位的int值,高两位代表SpecMode低30位表示SpecSize。SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。MeasureSpec参与了View的measure过程,在measure过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。

MeasureSpec类是View类中的一个静态内部类,下面看一下MeasureSpec中一些常量和方法。

  private static final int MODE_SHIFT = 30;
   //MODE_MASK1100 0000 0000 0000 0000 0000 0000 0000
  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;
  
 //生成的MeasureSpec
 public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
          return size + mode;
      } else {
	      //返回生成的MeasureSpec,就是size和mode的按位与的结果。
          return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
 }

三种测量模式说明

  • UNSPECIFIED :父容器没有给当前View强加任何约束,View想多大就多大,此种情况一般用于系统内部。
  • EXACTLY : 父容器已经决定了当前View的准确的大小。这个时候View的最终大小就是SpecSize所指定的值,它对应于LayoutParams中的match_paent和具体的数值。
  • AT_MOST :父容器指定了一个SpecSize,View的大小不能大于这个值,它对应于LayoutParams中的wrap_content。

对于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定,MeasureSpec一旦确定后,onMeasure中就可以确定View的测量宽高。如下图所示:

  • parentSpecMode表示父容器的MeasureSpec。
  • childLayoutParams表示View自身的LayoutParams。
  • childSize表示View的测量宽高。
    普通View的MeasureSpec创建规则 图片来自:http://www.jianshu.com/p/e3049dd24505

View的measure过程

View的测量过程是在measure方法中完成的,measure方法是一个final类型的方法,子类不能重写此方法。View真正的measure工作是在onMeasure方法中完成的,子类可以重写onMeasure方法来实现测量工作。

View的measure方法
final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    //...
    onMeasure(widthMeasureSpec, heightMeasureSpec);
}

View的onMeasure方法
    /**
     * 测量View及其内容来确定View的测量宽高。子类应该重写这个方法来提供对其内容真实有效的测量。
     * 约定:当你重写这个方法,你必须调用  {setMeasuredDimension(int, int)} 方法来存储
     * 该View的测量宽高。否则{measure(int, int)}方法会抛出IllegalStateException。
     * 调用父类的{onMeasure(int, int)}方法是一种有效的方式。 
     *
     * onMeasure的基类实现默认为背景大小,除非MeasureSpec允许更大的大小。 子类应覆盖
     * {onMeasure(int,int}以提供更好的内容测量。
     * 
     * <p>
     * 如果该方法被重写了。子类应该确保View的测量宽高大于等于View的最小宽高
     * getSuggestedMinimumWidth(), getSuggestedMinimumHeight()。
     * 。
     * 
     * @param widthMeasureSpec 父view施加的水平空间约束,封装在
     *                         {@link android.view.View.MeasureSpec}中。
     * 
     * @param heightMeasureSpec 父view施加的竖直空间约束,封装在
     *                         {@link android.view.View.MeasureSpec}中。 
     *
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(
            getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
        );
    }


setMeasuredDimension方法会储存测量的宽高,看一下getDefultSize方法。

View的getDefaultSize方法

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;
        //在这两种模式下都返回specSize。
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
        break;
        }
        return result;
    }
}

如果第二个参数measureSpec类型是AT_MOST或者EXACTLY,就返回specSize。这个specSize就是测量后的宽高。如果measureSpec类型是UNSPECIFIED类型,就直接返回第一个参数size,即宽高分别为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的返回值。

我们看一下getSuggestedMinimumWidth()方法的实现,getSuggestedMinimumHeight()的原理是一样的。

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? 
    mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

如果View没有设置背景(Drawable),宽度就是mMinWidth ,对应于android:minWidth这个属性指定的值。如果这个属性不指定,那么mMinWidth默认为0。如果View设置了背景,那么View的宽度就是就是mMinWidth和mBackground.getMinimumWidth()两者中间的最大值。看一下mBackground.getMinimumWidth()的实现。

Drawable的getMinimumWidth方法

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

如果Drawable的原始宽度不为0,就返回Drawable的原始宽度,否则返回0。

总结:从getDefaultSize方法看,View的宽高由specSize决定。直接继承View需要重写onMeasure方法,并且当View的宽高使用wrap_content方式的时候,需要给View设置一个默认的大小,因为在getDefultSize方法中,AT_MOST模式下返回的也是specSize。根据上面的表格可以看出,在AT_MOST模式下,specSize就是parentSize,也就是父容器最大可用空间。这种效果跟使用match_parent效果一样。那如何解决呢?就是在重写onMeasure 方法的时候,如果View使用了wrap_content(测量模式是AT_MOST),那我们就提供一个默认的宽高。举个例子,如下所示:

//默认大小
private static final int DEFAULT_SIZE = 200;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec,heightMeasureSpec);
    int widthSpecModel = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightSpecModel = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (widthSpecModel == MeasureSpec.AT_MOST && 
            heightSpecModel == MeasureSpec.AT_MOST) {
        setMeasuredDimension(DEFAULT_SIZE, DEFAULT_SIZE);
    } else if (widthSpecModel == MeasureSpec.AT_MOST) {
        setMeasuredDimension(DEFAULT_SIZE, heightSpecSize);
    } else if (heightSpecModel == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, DEFAULT_SIZE);
    }
}

上面就是View的测量过程了。

ViewGroup的测量过程。

ViewGroup过程除了完成自己的measure过程以外,还要遍历调用所有子元素的measure方法,各个子View再递归执行这个过程。和View不同的是,ViewGruop是一个抽象类,没有重写onMeasure方法,但是它提供了一个叫measureChilden 的方法。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    //遍历所有的子View,
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
	        //测量子View的大小。
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

measureChildren方法中会通过调用measureChild对每一个子View进行测量。

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);
}

measureChild方法会根据ViewGroup的测量规格和子View的LayoutParams生成子View的测量规格,然后传递给View的measure方法进行测量。

ViewGroup是一个抽象类,并没有实现onMeasure方法,需要子类(比如LinearLayout,RelativeLayout)去实现onMeasure方法。下面通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
	    //竖直方向测量
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
	    //水平方向测量
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

我们选择竖直布局的LinearLayout测量过程measureVertical方法。

我们先看一下measureVertical方法中的部分代码

    //LinearLayout 最终高度
    mTotalLength = 0;
    //LinearLayout 最大宽度
    int maxWidth = 0;
    //测量模式
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    //遍历所有的child
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        //获取child的LayoutParams
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        totalWeight += lp.weight;
		//标记是否使用剩余的空间,当我们使用weight的并且高度指定为0
		//的时候useExcessSpace为true。我们这里不关注这种方式
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            //...
        } else {
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            //注释1处
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
            //注释2处
            final int childHeight = child.getMeasuredHeight();
            final int totalLength = mTotalLength;
            //注释3处,计算最终高度
            mTotalLength = Math.max(totalLength, totalLength + childHeight + 
            lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
        }

        boolean matchWidthLocally = false;
        //如果测量模式为MeasureSpec.EXACTLY并且存在child的width是MATCH_PARENT
        if (widthMode != MeasureSpec.EXACTLY 
            && lp.width == LayoutParams.MATCH_PARENT) {
            // 至少有一个child想和LinearLayout一样宽,我们在这里设置一个标记
            //指示当我们知道了我们的宽度的时候,我们需要重新测量这个child。 
            matchWidth = true;
            matchWidthLocally = true;
        }

        final int margin = lp.leftMargin + lp.rightMargin;
        //注释4处,获取child的测量宽度
        final int measuredWidth = child.getMeasuredWidth() + margin;
        //注释5处,为最大宽度赋值
        maxWidth = Math.max(maxWidth, measuredWidth);
    }
    //注释6处
    mTotalLength += mPaddingTop + mPaddingBottom;
    int heightSize = mTotalLength;
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    //注释7处
    maxWidth += mPaddingLeft + mPaddingRight;
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    //注释8处
    setMeasuredDimension(
        resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
        

上面代码的注释1处,调用了measureChildBeforeLayout方法来测量child的宽高。

LinearLayout的measureChildBeforeLayout方法

void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
   //调用LinearLayout的measureChildWithMargins方法
    meaureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
}

LinearLayout的measureChildWithMargins方法

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方法完成child的测量。
   child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

到这里我们完成了一个child的测量工作,然后我们回到LinearLayout测量过程measureVertical方法
的注释2处,获取child的测量高度,然后在注释3处,累加mTotalLength,直到遍历完所有的childView。注释4处,获取child的测量宽度,然后在注释5处,为最大宽度maxWidth重新赋值。

在注释6处,mTotalLength加上数值方向上的padding,然后计算最终高度heightSize。在注释7处,计算最终的宽度maxWidth。然后在注释8处调用setMeasuredDimension方法,保存LinearLayout的测量宽高。

LinearLayout的measure过程到此结束。

参考

  1. 《Android开发艺术探索》
  2. View的工作原理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值