从源码分析View的measure过程

measure过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了需要完成自己的测量外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程.

在分析源码前,先简单介绍下View的测量模式:

MeasureSpec

measureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize,其中SpecMode是指测量模式,而SpecSize是指在某种测量模式下的规格大小。
MeasureSpec将SpecMode和SpecSize打包成一个int值,通过其静态方法getMode(int measureSpec)方法可以获取specMode

 public static int getMode(int measureSpec) {
      return (measureSpec & MODE_MASK);
 }

getSize(int measureSpec)获取specSize

 public static int getSize(int measureSpec) {
      return (measureSpec & ~MODE_MASK);
 }

通过makeMeasureSpec(int size, int mode)可以打包生成MeasureSpec

public static int makeMeasureSpec(int size, int mode) {
   if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

测量模式分为3种:
UNSPECIFIED:父控件不对View有任何限制,要多大给多大,一般用于系统内部

EXACTLY:父控件已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应与LayoutParams中的match_parent或者具体的数值这2种情况.

AT_MOST:父控件指定了一个可用大小即SpecSize,View的大小不能大于这个值,它对应于LayoutParams中的wrap_content。

我们先从ViewGroup源码下手,它是一个抽象类,因此它没有重写View的onMeasure方法,其具体的测量过程需要各个子类去实现,比如LinearLayout、ReleativeLayout等等。但它提供了一个叫做measureChildren的方法,用于测量它所有的子控件.

measureChildren

/**
     * 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.
     *
     * @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) {
                //如果子控件是可见的,则调用下面的方法测量该子控件,传入目标子控件,父控件的宽高测量模式3个参数
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

measureChild

/**
     * 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);

        //最后调用子控件的measure方法进行测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

此外,ViewGroup还提供了一个与上面方法类似的方法叫做measureChildWithMargins,它在计算剩余空间的时候不仅排除了内边距外,而且还排除了子View的外边距,以及父控件自身已使用的空间.

measureChildWithMargins

/**
     * 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();

        //计数子控件的宽度测量模式(比measureChild方法多传入了子控件的外边距和父控件已使用的宽度)
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);

        //计数子控件的高度测量模式(比measureChild方法多传入了子控件的外边距和父控件已使用的高度)
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        //测量该子控件
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

getChildMeasureSpec

/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view 父控件的宽高测量模式
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable 父控件的内边距或者子View的外边距
     * @param childDimension How big the child wants to be in the current
     *        dimension 子控件的宽/高
     * @return a MeasureSpec integer for the child 子控件的宽/高测量模式
     */
    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) {
                //子控件布局中的宽或高不等于0,则采用子控件的布局中定义的宽或高,即lp.width或lp.height
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                // 子控件布局参数是MATCH_PARENT,则获取父控件剩余空间,模式为精确
                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.
                 //子控件布局中宽高参数是WRAP_CONTENT,则获取父控件的剩余空间,模式为最多
                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
                //如果子控件的布局中的宽高不为0,则采用布局中定义的宽或高,模式为精确
                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 = 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的创建与父控件的MeasureSpec和子控件本身的LayoutParams、margin,以及父控件的padding,已使用的宽高有关.

measure

在measureChild方法中还会调用各个子控件的measure方法进行子控件的本身的测量,该方法是View的方法,它是一个final类型方法,这意味着子类不能重写次方法,在View的measure方法中会去调用View的onMeasure方法.

    /**
      * <p>
      * This is called to find out how big a view should be. The parent
      * supplies constraint information in the width and height parameters.
      * </p>
      *
      * <p>
      * The actual measurement work of a view is performed in
      * {@link #onMeasure(int, int)}, called by this method. Therefore, only
      * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
      * </p>
      *
      *
      * @param widthMeasureSpec Horizontal space requirements as imposed by the
      *        parent 子控件的宽度测量模式
      * @param heightMeasureSpec Vertical space requirements as imposed by the
      *        parent 子控件的高度测量模式
      *
      * @see #onMeasure(int, int)
      */
     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //......
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            //看到没,当调用measure方法,它会触发View的onMeasure方法
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } 
        //.......
     }

onMeasure

/* 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}.
     *
     * @see #getMeasuredWidth()
     * @see #getMeasuredHeight()
     * @see #setMeasuredDimension(int, int)
     * @see #getSuggestedMinimumHeight()
     * @see #getSuggestedMinimumWidth()
     * @see android.view.View.MeasureSpec#getMode(int)
     * @see android.view.View.MeasureSpec#getSize(int)
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //最终设置View的大小
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

getSuggestedMinimumWidth

/**
     * Returns the suggested minimum width that the view should use. This
     * returns the maximum of the view's minimum width)
     * and the background's minimum width
     *  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
     * <p>
     * When being used in {@link #onMeasure(int, int)}, the caller should still
     * ensure the returned width is within the requirements of the parent.
     *
     * @return The suggested minimum width of the view.
     */
    protected int getSuggestedMinimumWidth() {
        //返回当前子控件的最小宽度或者背景图的宽度
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

getSuggestedMinimumHeight同理,返回的是当前子控件的最小高度或背景图的高度这里就不贴源码了。
这里有必要解释下最小宽/高度和背景图宽高
mMinWidth :对应布局中android:minWidth属性,默认为0
mMinHeight :对应布局中android:mMinHeight 属性,默认为0
mBackground.getMinimumHeight():返回的是Drawable的原始高度,即图片的尺寸,例如ImageView设置了背景图片就会有
mBackground.getMinimumWidth():返回的是Drawable的原始宽度

getMinimumHeight

 /**
     * Returns the minimum height suggested by this Drawable. If a View uses this
     * Drawable as a background, it is suggested that the View use at least this
     * value for its height. (There will be some scenarios where this will not be
     * possible.) This value should INCLUDE any padding.
     *
     * @return The minimum height suggested by this Drawable. If this Drawable
     *         doesn't have a suggested minimum height, 0 is returned.
     */
    public int getMinimumHeight() {
        final int intrinsicHeight = getIntrinsicHeight();
        return intrinsicHeight > 0 ? intrinsicHeight : 0;
    }

getMinimumHeight代码类似就不贴了。

getDefaultSize

接着看该方法的内部实现

/**
     * Utility to return a default size. Uses the supplied size if the
     * MeasureSpec imposed no constraints. Will get larger if allowed
     * by the MeasureSpec.
     * 子控件的最小宽/高
     * @param size Default size for this view
     * 子控件的宽/高测量模式
     * @param measureSpec Constraints imposed by the parent
     * @return The size this view should be.
     */
    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;
    }

从上面的源码中分析可知,getDefaultSize方法其实返回的其实就是measureSpec的specSize,即子View本身的测量规格大小;而这个measureSpec就是子View在父View中的getChildMeasureSpec计算后的测量模式.

总结

1.从ViewGroup的getChildMeasureSpec中分析可知,无论父控件是精确模式还是最多模式,只要子View的布局参数是match_parent或者wrap_content,那么子View获取的宽高都是父控件的剩余空间,除非子View重写了onMeasure方法去setMeasuredDimension定义宽高,因为View的onMeasure方法其实并没有修改宽高信息,具体从getDefaultSize源码中可以看出.系统提供的控件例如TextView,ImageView等等都是有重写onMeasure方法的.
这也就能解释为什么我们自定义的View明明写着wrap_content,但运行起来却是匹配父窗体的效果.解决这个问题除了重写onMeasure的方法之外,我们还可以在布局文件中定义android:width和android:height为具体的某个dp值也是可以解决的,因为getChildMeasureSpec方法中判断如果子View有具体的布局宽高,则会优先使用.

2.从getChildMeasureSpec还可以知道,只要父控件的宽高模式是最多,那么无论子View的布局参数是match_parent或者wrap_content,它的模式也只能是最多.

3.如果父控件是精确模式,那么只要子View的宽高布局参数不是wrap_content,则子View的模式也是精确模式.

4.只要子View的宽高布局参数是具体的dp值,那么子view的模式一定是精确模式.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值