View的绘制流程(二)--------view的measure过程

本文详细探讨了Android中View的measure过程,从MeasureSpec的介绍开始,讲解了MeasureSpec的三种模式及其位运算原理。接着,分析了View在不同模式下的测量行为,并指出MeasureSpec与LayoutParams之间的关系。在ViewGroup的measure过程中,阐述了如何遍历并测量子元素,以及measureChild和measureChildWithMargins方法的工作方式。最后,特别强调了自定义View时,必须关注wrap_content模式下的尺寸计算,以避免与match_parent混淆。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在分析view的measure过程之前,有必要看看什么是measureSpec,因为它在view的measure流程中经常出现,比较重要,不知道的童鞋可能会感觉这是啥玩意儿,好高大上阿。。。其实看过之后会发现measureSpec并不难,下面开始----

一、MeasureSpec简介

MeasureSpec代表一个32位的int值,这个从类的常量中就可以看出来,它是View的内部类,如下:

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;

        /**
         * 精确:子元素会被给予一个确定的值,对应LayoutParams中的Match_parent和具体的数值
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * 不大于:父元素提供一个可用的值,子元素不能超过这个值,对应LayoutParams中的wrap_content
         */
        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提供了三种模式(SpecMode),分别为上面的UNSPECIFIED、EXACTLY、AT_MOST

<<是位运算符,一般用于二进制,代表向左移位,MODE_SHIFT是个常量,30,所以UNSPECIFIED就是0向左移位30位,EXACTLY是1向左移位30位,AT_MOST是2向左移位30位,如下表示:

UNSPECIFIED:

移位前:00000000 00000000 00000000 00000000

移位后:00000000 00000000 00000000 00000000

EXACTLY:

        移位前:00000000 00000000 00000000 00000001

移位后:01000000 00000000 00000000 00000000

AT_MOST:

       移位前:00000000 00000000 00000000 00000010

       移位后:10000000 00000000 00000000 00000000

总的来说,其实就是MeasureSpec中,高两位代表SpecMode,低30位代表SpecSize。MeasureSpec类将SpecMode(模式)和SpecSize(大小)整合成一个int值,方法就是上面代码中的makeMesaureSpec方法,同时为了方便,也提供了方法可以从一个MeasureSpec值当中取出来原始的SpecMode和SpecSize,就是上面代码中的getMode和getSize,方法都很简单,这里可以简单解释下:

MODE_MASK是3向左移位30位,即11000000 00000000 0000000000000000,取反就是00111111 11111111 11111111 11111111。

把已整合的MeasureSpec值和MODE_MASK做&运算,即可得到SpecMode,将已整合的MeasureSpec值和取反后的MODE_MASK即可得到SpecSize。

另外,MeasureSpec和LayoutParams是有对应关系的,系统中是通过measureSpec来测量View的,但是在外部一般都是通过设置LayoutParams来改变View的,比如wrap_content和match_parent等。那么中间是怎么转换的呢,首先需要知道一点,就是View的MeasureSpec并不是唯一由LayoutParams来决定的,LayoutParams是需要和父容器一起才能决定view的MeasureSpec,在view测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec。

PS:顶级view的measureSpec不在此列,它是由窗口尺寸和自身的LayoutParams决定的,这个可以自行查看源码(在ViewRootImpl中的measureHierarchy方法中)

View的measure过程要分情况来看,如果是一个单纯的view,那么通过measure过程就完成了测量过程,如果还是一个viewgroup,那么不仅要完成自身的测量过程外,还要遍历子元素并调用子元素的measure过程,各个子元素继续递归调用,直到所有的子元素测量完毕。

二、View的measure过程

view的测量过程是从measure方法开始的,此方法是final修饰的,所以子类是不能重写此方法的,view方法内会调用onmeasure方法,伪代码如下所示:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {       
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);       
}
所以看看onmeasure方法

/**
     * <p>
     * Measure the view and its content to determine the measured width and the
     * measured height. This method is invoked by {@link #measure(int, int)} and
     * should be overriden by subclasses to provide accurate and efficient
     * measurement of their contents.
     * </p>
     *
     * <p>
     * <strong>CONTRACT:</strong> When overriding this method, you
     * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
     * measured width and height of this view. Failure to do so will trigger an
     * <code>IllegalStateException</code>, thrown by
     * {@link #measure(int, int)}. Calling the superclass'
     * {@link #onMeasure(int, int)} is a valid use.
     * </p>
     *
     * <p>
     * The base class implementation of measure defaults to the background size,
     * unless a larger size is allowed by the MeasureSpec. Subclasses should
     * override {@link #onMeasure(int, int)} to provide better measurements of
     * their content.
     * </p>
     *
     * <p>
     * If this method is overridden, it is the subclass's responsibility to make
     * sure the measured height and width are at least the view's minimum height
     * and width ({@link #getSuggestedMinimumHeight()} and
     * {@link #getSuggestedMinimumWidth()}).
     * </p>
     *
     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     * @param heightMeasureSpec vertical space requirements as imposed by the parent.
     *                         The requirements are encoded with
     *                         {@link android.view.View.MeasureSpec}.
     *
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
注释一大堆,代码没几行。。。看起来很简单,但从方法的注释上可以看到重要性,子类一般都需要重写此方法,并且在重写的时候必须要setMeasuredDimension方法,否则会报异常。
此方法有一个默认的实现,setMeasuredDimension是设置view的measurewidth和measureheight的方法,这里不多说,看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;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

代码很少,逻辑也很简单,当view的MeasureSpec模式为AT_MOST或者EXACTLY的时候,这个方法返回的默认测量值就是参数MeasureSpec中的specSize,specSize就是view测量后的值。从上面的代码可以看出来,当view模式处于AT_MOST的时候和EXACTLY的时候返回的结果都是一样的,都是specSize,那么这个specSize是多少呢,它从measurcSpec中获取,是从父元素中传来的,所以带着这个问题来看看viewgroup的 measure过程吧。

三、ViewGroup的measure过程
对于viewgroup来说,除了要测量自身以外,还要遍历子元素并调用子元素的measure方法,各个子元素继续递归调用这个流程,而viewgroup是个抽象类,所以并没有重写View的onmeasure方法,而是提供了其他的方法,相关的方法有三个:measureChildren、measureChild和measureChildWithMargins三个方法

/**
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *(这个view的所有子元素在这个view的MeasureSpec和padding的约束下绘制自身,跳过那些处于gone状态下的子元素,重要部分在getChildMeasureSpec方法中,下同)
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this view
     */
    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);
            }
        }
}

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this 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);
    }

/**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    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);
    }

看方法别忘了看注释,注释是个很好的帮助,这三个方法都是有关于measure的,measureChildren方法很好理解,就是遍历子元素,调用measureChild方法,measureChild方法内是先取出子元素的LayoutParams,通过一个getChildMeasureSpec方法来获取相应的MeasureSpec,然后递归调用子元素的measure方法。而measureChildWithMargins方法则还需要计算子元素的margins,下图表示父容器和子元素之间的padding和margin。



蓝色的代表父元素的padding(padding是和自身内容之间的距离,view内部),黄色的代表子元素的margin(margin是和外部边界的距离,view外部),中间红色的是子元素。举个不太恰当的例子,如果父容器是房子,子元素是里面的人,那么padding就是父元素--房子的墙壁厚度,margin是子元素--人距离墙壁的距离。

代码注释中可以看到,重点是在getChildMeasureSpec方法,进去看看

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) {//如果子元素的LayoutParams也是确定的值,那么子元素的模式就是EXACTLY,大小为设置的值
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//如果子元素是Match_parent,那么子元素模式就是EXACTLY,大小就是父元素剩余可用的空间
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果子元素是wrap_content,那么子元素模式就是AT_MOST,大小就是父元素剩余可用的空间

                // 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: //如果父元素的模式是AT_MOST

            if (childDimension >= 0) {//如果子元素的LayoutParams是精确的,那么子元素模式就是EXACTLY,大小就是设置的值

                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//如果子元素是Match_parent,那么子元素模式就是AT_MOST,大小就是父元素剩余可用的空间

                // 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) {//如果子元素是wrap_content,那么子元素模式就是EXACTLY,大小就是父元素剩余可用的空间

                // 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: //如果父元素的模式是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);
    }

这个方法的作用就是根据父容器的MeasureSpec并结合View本身的LayoutParams来确定子元素的MeasureSpec,参数中的padding是父元素中已经被占用的空间,所以子元素的可用空间就是父容器的大小specSize减去已经占用的padding。注释已经比较清楚了,总结如下:

1、  当view的Layoutparams采用的是固定宽高的时候,不管父元素的MeasureSpec是什么,view的MeasureSpec的模式都会是精准的EXACTLY,大小都是设置的固定值

2、  当view的LayoutParams采用的是match_parent的时候,如果父元素是精准模式,那么view的模式就会是精准模式(EXACTLY)并且大小是父元素中剩余的所有大小,如果父元素是最大模式,那么view的模式就是最大模式(AT_MOST),并且大小不能超过父元素中剩余的大小。

3、  当view的LayoutParams是wrap_content的时候,不管父元素的MeasureSpec是什么,view的模式都会是最大模式(AT_MOST),并且大小不能超过父元素中剩余的大小。

返回先前的问题,当view的模式是AT_MOST的时候, view的宽高会是specSize,通过上面的代码可以得到,specSize就是父容器剩余的所有空间,如果不作处理,当view的LayoutParams是wrap_content的时候,那么效果和match_parent是一样的,都是父容器剩余的所有空间。所以说直接继承view的自定义控件要记得在onmeasure方法中设置当处于wrap_content的时候的大小,否则wrap_content的时候就跟match_parent是一样的效果。

有兴趣的童鞋可以查看一下常用的View和ViewGroup的measure代码,一般都经过特殊处理的,不过源码虽好,可不要贪看哦(本屌丝连着看了一周的源码,已经快吐了。。。)


measure过程就先写到这里,本人也是菜鸟,欢迎留言,一起学习,如有错误,还望指正。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值