Android View的测量(onMeasure)

2019-11-07  本文已影响0人  断了谁的弦

前言

平常我们在写控件的时候除了指定具体的宽高之外,还会经常使用到wrap_content和match_parent。那么控件究竟是怎样知道究竟自己需要多大的宽高呢?这就得从测量规格开始说起了。

MeasureSpec

MeasureSpec代表了控件宽和高的测量规格。总所周知,一个int占4个字节,32位。所以谷歌用高2位来表示测量模式,剩余的低30位来表示测量大小。测量模式目前有三种:

这里的父布局指的是当前控件所在的ViewGroup

如何生成MeasureSpec

MeasureSpec不是控件自己就可以指定的,而是由父布局和控件共同决定的,具体可以参考ViewGroup的getChildMeasureSpec():

    /**
     * @param spec 当前ViewGroup的测量规格
     * @param padding 当前ViewGroup的padding值
     * @param childDimension 子控件指定的尺寸
     * @return 返回子控件的MeasureSpec
     */
    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) {
                //子控件指定了具体的尺寸,ViewGroup也是具体的尺寸 
                //则子控件的测量大小就是指定的尺寸,测量模式就是EXACTLY。
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子控件是MATCH_PARENT,则使用ViewGroup的大小,测量模式也是EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //子控件是WRAP_CONTENT,则需要子控件自己测量大小
                //但最大不能比ViewGroup大,测量模式是AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //子控件指定了具体的尺寸,ViewGroup是AT_MOST
                //ViewGroup的大小最终是由子控件的大小决定的
                //所以子控件的测量大小就是指定的尺寸,测量模式就是EXACTLY
                //这样在子控件测量出自己的大小之后ViewGroup再测量自己该多大
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //子控件是MATCH_PARENT,但是ViewGroup自己的大小也还没确定
                //所以只能给子控件的大小约束为不能超过ViewGroup的大小
                //子控件的测量模式所以是AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //子控件是WRAP_CONTENT,则最大不能比ViewGroup大,测量模式是AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        //该测量模式对子控件没限制,子控件想多大就多大
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                //子控件指定了具体的尺寸,则子控件的测量大小就是指定的尺寸,测量模式就是EXACTLY。
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //View.sUseZeroUnspecifiedMeasureSpec是一个常量,值为false
                //所以子控件大小为ViewGroup大小,测量模式是UNSPECIFIED
                //但是一般子控件都是自己测量大小,不会直接使用这个size
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //同上
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //将测量大小和测量模式合成一个int的测量规格
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

由此可见,控件的MeasureSpec是由父控件的MeasureSpec和控件自身设置的大小共同决定的。

生成了MeasureSpec之后是怎样传递到onMeasure()的

那么控件的MeasureSpec是怎样从父控件传递下来的呢?让我们来看下最简单的FrameLayout的onMeasure():

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        int maxHeight = 0;
        int maxWidth = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            }
        }
    }

可以看到里面调用了measureChildWithMargins(),并把自身的MeasureSpec传了进去,那看看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(childWidthMeasureSpec, childHeightMeasureSpec);
    }

可以看到这里正是调用了getChildMeasureSpec()来获取MeasureSpec,并调用child的measure()当做参数传了进去,而在measure()里面又会调用到onMeasure(),从而实现了父控件根据自身的MeasureSpec和子控件设置的宽高值生成MeasureSpec,并传递到子控件的onMeasure()中。

UNSPECIFIED是怎样产生的

那么问题来了,在getChildMeasureSpec()中我们知道只有父控件的测量模式是UNSPECIFIED的时候,生成给子控件的测量模式才有可能是UNSPECIFIED。但是我们使用的控件是通过setContentView()添加到id为content的FrameLayout里面的,它的测量模式是EXACTLY,那UNSPECIFIED岂不是不会生成了?那当然不是,前面介绍UNSPECIFIED的时候说过类似ScrollView这种就会生成,那么我们来看下ScrollView的measureChildWithMargins()方法:

    @Override
    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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
                heightUsed;
        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
                MeasureSpec.UNSPECIFIED);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

在生成childHeightMeasureSpec的时候它并没有直接使用getChildMeasureSpec()生成MeasureSpec,而是指定了测量模式为UNSPECIFIED,通过makeSafeMeasureSpec来生成。从而使子控件的测量模式变为UNSPECIFIED,因为它作为一个可以滑动的控件,当然是无论子控件想有多高都可以啦。

如何处理MeasureSpec

首先来看下View默认是怎样处理的:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

它直接把getDefaultSize()的值当做最终的测量大小,那么来看下getDefaultSize()和用到的getSuggestedMinimumWidth():

    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这个属性的值,这个值默认为0
    //如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
    protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

对于UNSPECIFIED,则result是等于getSuggestedMinimumWidth()的值,其他两种模式则result都是等于测量规格中的大小,这显然是很难满足实际需求的。那么一般控件测量的时候对于各种MeasureSpec是怎么处理的呢,让我们来看下最常用的TextView的onMeasure():

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        
        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } else {
            //获取显示文字所需的高度
            int desired = getDesiredHeight();

            height = desired;
            mDesiredHeightAtMeasure = desired;

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }
    }

这里只看它是如何处理高度的,首先获取到测量模式和大小,然后对测量模式进行判断。如果是EXACTLY,那高度直接就等于从测量规格取出的高度。否则高度等于文字所需的高度,此时对应测量模式是UNSPECIFIED的情况。最后再判断一下测量模式是不是AT_MOST,如果是则取desired和heightSize的最小值,因为这种模式下的大小不能大于heightSize的。

所以当我们自定义View的时候除非都是明确指定大小的,否则一定要根据具体需求来对AT_MOST和UNSPECIFIED的情况进行测量。因为自定义View都是根据特定需求来的,就像TextView的wrap_content是适应文字大小,ImageView的是适应图片大小,要不最后显示出来的效果往往会差强人意。所以这里并没有给出自定义View的onMeasure()方法应该怎么写,因为并没有一个统一的标准,需要的是开发者根据具体需求来自行实现。

上一篇 下一篇

猜你喜欢

热点阅读