View-view的measure过程

前言

上一篇我们梳理了DecorView加载流程,其中涉及到measure、layout、draw方法,这一篇详细介绍view的measure过程。measure的过程主要用于测量一个View的大小,在层级嵌套的View中,View大小首先取决于父View的设置大小的方式,然后与自身的需要大小也有关。在这样的场景中Android将View的测量模式和模式下的大小封装成一个MeasureSpec对象 ,然后结合自身LayoutParams去设置一个View的大小。

MeasureSpec

如果你看过View测量相关的知识,你应该知道MeasureSpec,MeasureSpec是View类的静态内部类,它由一个int 32为组成,高2位表示SpecMode,测量模式;低30位表示SpecSize,某种测量模式下的规格大小, android对它的解释是封装了从父视图传递给子视图的布局要求。

MeasureSpec中的3种测量模式

  1. UNSPECIFIED:父 View 没有施加任何限制给子 View,可以是子 View 想要的任何尺寸,这样一般用于系统测量,用的较少
  2. EXACTLY:父View已确定子View的确切大小。不管怎样,子View都会受到那些限制,也就是精确模式
  3. AT_MOST:子View的大小可以达到指定的大小,也就是最大模式
	   public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
...
}

DecorView

这里我们还是从DecorView开始分析,看看它的具体测量方法,要知道每一个View都有一个MeasureSpec,这是父视图对子视图的限制和要求,根视图也不例外,我们看一下根视图DecorView的MeasureSpec生成规则,相关代码在ViewRootImpl类中


	 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
     int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
	  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

上面就是DecorView的MeasureSpec生成方式,主要看看getRootMeasureSpec方法

根据源码,具体看看其生成的规则:

  1. Window的模式不管是MATCH_PARENT还是WRAP_CONTENT,大小都是windowSize:窗口的可用宽度或高度;但对应的测量模式不一样,一个是精确模式一个是最大模式
  2. 如果没有任何限制那么直接使用窗口的宽高,测量模式为精确模式

windowSize:窗口的可用宽度或高度
rootDimension:窗口的宽或高设置的类型MATCH_PARENT或WRAP_CONTENT

	 private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window无法调整大小,强制根视图为窗口的可用宽度或高度,模式为精确模式
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            //Window可以调整大小,设置根视图为大小为窗口的可用宽度或高度,模式为最大模式
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window 为精确尺寸,强制根视图大小为根部尺寸,模式为精确模式
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

上面是DecorView的MeasureSpec生成过程和规则,我们看下DecorView怎么为子View生产MeasureSpec,以及保存自己的大小。DecorView是继承FrameLayout,在DecorView中调用了父类的onMeasure()方法,那么具体我们看FrameLayout的onMeasure方法

看下面源码

  1. 首先判断了measureMatchParentChildren的值,如果FrameLayout中onMeasure的参数是DecorView传递过来的,那么MeasureSpec在DecorView中经过转换后一定为EXACTLY(可以看源码在DecorView的onMeasure里面),所以measureMatchParentChildren为false。当用户自定义一个FrameLayout属性为WRAP_CONTENT时候measureMatchParentChildren才为true
  2. for循环子View开始调用measureChildWithMargins测量子View大小,并找到子View中的最大宽高保存,用于决定DecorView最后的宽高
  3. 测量完子View后,调用setMeasuredDimension()保存DecorView的最后大小,其中会调用resolveSizeAndState来计算具体的宽高大小。

接下来我们具体看看resolveSizeAndState方法

public class FrameLayout extends ViewGroup {

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //如果宽高中有一个不是精确模式,那么就返回true;如果是DecorView传递过来的,必定都是精确模式,为false。
    final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();

        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) {
            	//使用约束MeasureSpec对子View进行测量,并得到子Viewd的LayoutParams
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //在所有子View中找到最大的宽和高
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                //DecorView不会走这个逻辑,因为DecorView的onMeasure()流程会将MeasureSpec准换成EXACTLY,只有当用户自布局一个FrameLayout属性为WRAP_CONTENT,并且宽高中有为MATCH_PARENT时才添加到集合中
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // 对Padding做一些处理
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // 获取定义的最小宽高和maxHeight相比,去一个大的
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }
		//根据maxWidth以及widthMeasureSpec、childState得到具体的宽高并保存 
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

...

	 if (count > 1) {
	 //当FrameLayout的大小设置其中有一个为wrap_content
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                final int childWidthMeasureSpec;
               
                if (lp.width == LayoutParams.MATCH_PARENT) {
                 //如果子View的width是MATCH_PARENT
                //得到子View的width,设置宽度为得到的宽,模式为精确模式
                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                //如果子View的width是为WRAP_CONTENT或者某一个精确值,调用getChildMeasureSpec(),具体看下面对方法讲解
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

              //同理对高度做同样的处理
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }

	}
...
}

resolveSizeAndState

首先这是一个ViewGroup的方法,方法主要用于计算具体的宽高。因为View的大小首先一般取决于两个因素:1、自身measureSpec的约束;2、子View的大小,接下来我们看看具体实现。

  1. size:最大宽高
  2. measureSpec:当前View的约束和测量模式
  3. childMeasuredState:子View的测量状态
  • 当specMode时AT_MOST时,那么就会判断MeasureSpec中的size,是否比传入的size(子View中的最大size)小,其实这个判断的作用就是:当为AT_MOST时候,子View的最大大小也不要比specSize大

  • 当specMode是EXACTLY时,那么直接返回specSize作为大小,

  • 当对测量模式为没有约束时,则使用size (注:这里的size,对于DecorView来说,是其最大子View的测量宽高,上面源码有讲解)

	  public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
        //当前View约束为warp_content 
            case MeasureSpec.AT_MOST:  
            //warp_content下对应的大小<最大高宽,200<300取200 
                if (specSize < size) {
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

小结:上面我们分析了DecorView的整个过程,DecorView的MeasureSpec产生取决于Window的设置,但后面又将得到的MeasureSpec转成精确模式。在FrameLayout中调用measureChildWithMargins测量子View大小。然后调用resolveSizeAndState()来确定最后的宽高。这个流程中其实要知道为什么要将AT_MOST的模式转成EXACTLY,看resolveSizeAndState中的分析你就知道,其实就是为了让子View受到父View的限制,不管是AT_MOST还是EXACTLY其实就是让子View不要超过父View的大小

ViewGroup

上面介绍了DecorView的和其子类的MeasureSpec创建过程,那如果这个时候DecorView的子View是一个容器类ViewGroup,那么接下来ViewGroup怎么根据DecorView传递过来的MeasureSpec去测量自己的大小,以及怎么设置子View的MeasureSpec。那么我们看看ViewGroup为我们提供了哪些方法。

ViewGroup是一个抽象类,其实它本身是没有实现View的onMeasure方法的,作为一个容器类它将onMeasure给到了它的实现类去完成。让实现类根据需要对自己进行大小设置以及对子View大小的测量。但在ViewGroup中提供可3个方法帮助我们对View进行测量。

measureChildren

measureChildren方法很简单,如果当前View不为GONE的就进行测量,具体测量方法调用measureChild。

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

measureChild

对于这个方法和measureChildWithMargins的区别就是获取的LayoutParams不一样,为了让View支持Margin,我们可以使用measureChildWithMargins方法,在我们自定义View的时候,需要支持Margin的需要重写generateLayoutParams方法,具体原理看这篇文章,说的很清楚了

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

measureChildWithMargins

关键方法还是getChildMeasureSpec

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

getChildMeasureSpec

在之前的版本里面MeasureSpec为UNSPECIFIED,View布局为MATCH_PARENT和WRAP_CONTENT返回都为0,但现在有版本判断。targetSdkVersion<23就为0,不然就返回父容器可使用大小

 static boolean sUseZeroUnspecifiedMeasureSpec = false;
 sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M;

getChildMeasureSpec是一个指导性方法,非常关键。它的作用就是根据父View的MeasureSpec和自身LayoutParams去设置自己的测量模式和模式下的Size,也就是设置自己的MeasureSpec这个值就会在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法的参数里面得到。

	 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) {
        // 父容器规格是精确大小
        case MeasureSpec.EXACTLY:       	
            if (childDimension >= 0) {
                //子View设置的是精确大小,如10dp
            	//size:就是自己设置的大小
            	//mode: 模式为精确模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子View设置的MATCH_PARENT,希望和父View一样大
                //size:使用父View的大小
            	//mode: 父size已经确定,模式为精确模式,
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            	//子View设置的WRAP_CONTENT,想要自己决定有多大,但是不能大于父View
            	//size:使用父View的大小
                //mode: 父size已经确定,模式为最大模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //父容器规格是最大尺寸
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //子View设置的是精确大小,如10dp
            	//size:就是自己设置的大小
            	//mode: 模式为精确模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子View设置的MATCH_PARENT,希望和父View一样大
                //size:使用父View的大小
            	//mode: 模式为最大模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
               //子View设置的WRAP_CONTENT,想要自己决定有多大,但是不能大于父View
            	//size:使用父View的大小
                //mode: 模式为最大模式
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 父容器规格没有做限制
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                //子View设置的是精确大小,如10dp
            	//size:就是自己设置的大小
            	//mode: 模式为精确模式
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子View设置的MATCH_PARENT,希望和父View一样大
                //size:targetSdkVersion<23?0:size
            	//mode: UNSPECIFIED模式,不施加任何约束  
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //子View设置的WRAP_CONTENT,想要自己决定有多大,但是不能大于父View
                //size:targetSdkVersion<23?0:size
            	//mode: UNSPECIFIED模式,不施加任何约束 
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

对照上面流程生成如下表格,多总结看看其规律。

子View的LayoutParams\父容器SpecModeMeasureSpec.EXACTLYMeasureSpec.AT_MOSTMeasureSpec.UNSPECIFIED
精确值(dp)EXACTLY childSizeEXACTLY childSizeEXACTLY childSize
MATCH_PARENTEXACTLY parentSizeAT_MOST parentSizeUNSPECIFIED 0或parentSize
WRAP_CONTENTAT_MOST childSizeAT_MOST parentSizeUNSPECIFIED 0或parentSize

我们分析了上面的知识点后发现,容器类都是自己实现onMeasure, 而ViewGroup类没有onMeasure方法,MeasureSpec最开始是根据window生成并传递给DecorView,用于约束DecorView的子View,并且根据子View的状态和大小以及MeasureSpec决定了DecorView的大小,DecorView的子View的MeasureSpec也是根据父View的MeasureSpec和自身的LayoutParams决定。

View

接下来看看View的相关测量过程

getSuggestedMinimumWidth 是返回视图的建议的最小宽度,关键还是getDefaultSize(),不管是精确模式还是最大模式都是直接返回specSize,也就是说子View的大小取决于getChildMeasureSpec()中返回的大小和模式,大家如果要知道View的测量方法那么就记住getChildMeasureSpec的规则。

	 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
	  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;
    }

小结:上面我们分析了DecorView的测量过程,以及ViewGroup和View中有关测量的方法,其中关键的是getChildMeasureSpec()方法。DecorView和其他View的MeasureSpec生产有点不同是DecorView有自己的测量方法getRootMeasureSpec,对于子view测量不管是RelativeLayout还是LinearLayout使用都getChildMeasureSpec(),理解父view的MeasureSpec和自己LayoutParams的关系就掌握了View的measure原理

MeasureSpec 与 LayoutParams 不得不说的二三事
详细说一下 MeasureSpec.UNSPECIFIED
你必须了解的LayoutParams的那些事儿

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值