自定义ViewGroup中onMeasure()的来龙去脉

目录

1.引言

2.谁来调用onMeasure()?

3.widthMeasureSpec和heightMeasureSpec

4.wrap_content之谜

5.总结


 

1.引言

刚入职不满一年的Android新人,如有错漏请轻拍。在自定义View的过程中,通常不需要重写onMeasure(),但到了ViewGroup中,常常需要自定义子View的排列,遇到LinearLayout或RelativeLayout很难甚至没办法实现的需求时,就必须要重写onMeasure。我也曾查阅过网上许多的文章,但总感觉不够系统和完善,于是决定自己摸索一番。

2.谁来调用onMeasure()?

稍微看一下源码应该都能看到,onMeasure是被View#measure()调用的,那么我们为什么还要去探索这个问题呢。因为我很想知道onMeasure(int widthMeasureSpec, int heightMeasureSpec),两个参数是怎么来的。我们在XML放置自定义的ViewGroup时候,往往会放置为顶级布局。那么这两个参数,在文档上说是父布局对自定义ViewGroup的约束,是哪里传递过来的呢。

 XML文件

<?xml version="1.0" encoding="utf-8"?>

<com.example.customsizeview.ViewGroupTouch.ViewPagerCustomSize
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.customsizeview.ViewGroupTouch.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/darker_gray" />

</com.example.customsizeview.ViewGroupTouch.ViewPagerCustomSize>

    布局文件仅仅是自定义ViewGroup嵌套一个自定义View,这个自定义View只重写了onMeasure()用于输出。

    其实通过AndroidStudio的Layout Inspector可以看出来即使在XML中是顶级布局,在实际的布局中也是子布局。

从onMeasure()的定义看,我们需要先找出自定义ViewGroup的父ViewGroup也就是ContentFrameLayout,然后一层层往上追溯,直到DecorView怎么生成widthMeasureSpec、heightMeasureSpec两个参数才行。最后再一直处理和传递到自定义View Group才是,但这样未免过于复杂,而且看DecorView的源码,我并没用完全看懂。但至少我们现在清楚了两点,一个是XML的顶部布局拿到的widthMeasureSpec和heightMeasureSpec也是从上层传入的,一个是DecorView其本身代表的是手机界面的大小,比如我的小米6就是1920*1080,是个确切的数值。

3.widthMeasureSpec和heightMeasureSpec

widthMeasureSpec和heightMeasureSpec一个是宽一个是高,只要研究一个另一个是一样的。我们就来分析widthMeasureSpec。既然没办法很容易的搞清楚widthMeasureSpec怎么产生的,那么我们看看widthMeasureSpec到底是什么吧。widthMeasureSpec其实是包含了父ViewGroup对子View 宽度模式和宽度尺寸两个要求。

可以通过MeasureSpec.getMode(widthMeasureSpec)和MeasureSpec.getSize(widthMeasureSpec)获取到 mode和size。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        measureChildren(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

然后我们将mode和size打印出来(我通过debug打断点来打印的)。。。。。

发现size还很好理解,mode是个啥?别急,我们看看官方控件中是怎么处理这个widthMeasureSpec的吧。measureChildren()就是ViewGroup自带的处理子View布局的方法。让我们来看看measureChildren()是怎么处理widthMeasureSpec的。

 

首先进入ViewGroup#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) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

viewgroup会遍历自己的子View,对每个子view都设置一次measureChild(),并将widthMeasureSpec作为参数之一传入。

 

再进入ViewGroup#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);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这里会获取到刚才传入的child,通过child.getLayoutParam获取布局属性,最后通过lp.width获取child的layout_width作为getChildMeasureSpec()的参数之一。

 

再进入viewGroup#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
     * @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) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                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.
                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
                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 = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

spec就是measureChildren()传入的widthMeasureSpec,然后获取widthMeasureSpec的mode和size,先判断mode,再判断childDimension,而childDimension就是child的layout_width。然后会给resultSize和resultMode赋值,你会发现,这两个值似乎也很熟悉,似乎和widthMeasureSpec的size和mode是一样的。的确是的,MeasureSpec.makeMeasureSpec(resultSize, resultMode)会将resultSize和resultMode组装成一个MeasureSpec(其实就是两个int通过位运算放进要给int里面,算是节省了空间),也放一下代码吧。

View#makeMeasureSpec

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * <ul>
         *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
         *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
         *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
         * </ul>
         *
         * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
         * implementation was such that the order of arguments did not matter
         * and overflow in either value could impact the resulting MeasureSpec.
         * {@link android.widget.RelativeLayout} was affected by this bug.
         * Apps targeting API levels greater than 17 will get the fixed, more strict
         * behavior.</p>
         *
         * @param size the size of the measure specification
         * @param mode the mode of the measure specification
         * @return the measure specification based on size and mode
         */
        public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

现在我们了解了ViewGroup测量子View的操作后,会发现其实MeasureSpec的mode只会有三个值:

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

通过打印出来会发现对应的是

UNSPECIFIED = 0

EXACTLY = 1073741824

AT_MOST = -2147483648

回到最开始,自定义ViewGroup的widthMeasureSpec的mode 其实就是EXACTLY,通过注释会发现,这是父ViewGroup对子View尺寸的精确要求。子View的大小完全按照父ViewGroup的要求来设置。(这里是自定义ViewGroup的父ViewGroup对自定义ViewGroup的要求)。

4.wrap_content之谜

尝试过在xml文件中,子View的layout属性设置为wrap而父ViewGroup设置为match时,你往往会发现,有时候wrap_content代表的是不占空间,有时候则和设置为match_parent一样,这使得我这种看不怪表里不一的人很是头大,你咋能一天一个样呢?那么我们来看看wrap_content是如何影响测量的。

我们将XML中CustomView的layout属性设置为wrap_content

    <com.example.customsizeview.ViewGroupTouch.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/darker_gray" />

从之前的分析,我们看到现在我们自定义的ViewGroup收到的widthMeasureSpec mode = EXACTLY,而其调用measureChildren()时,将收到的widthMeasureSpec直接传入,最后getChildMeasureSpec()判断widthMeasureSpec的mode = EXACTLY,而子View的lp.width = wrap_content ,于是resultSize = 1080 ,resultMode = AT_MOST,最后会通过 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);将子View的测量结果保存到子View的全局变量中。

这里child.measure()就是View#measure(),最后会执行View#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 overridden 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}.
     *
     * @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) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

别看这么多其实就一行代码,就做了一件事情,将getDefaultSize()的结果作为setMeasuredDimension()的参数。(这里就不细看setMeasuredDimension()了,很简单,就是将其做一些处理后保存为view的全局变量mMeasuredWidth,getSuggestedMinimumWidth()则是判断若有背景,则返回背景图的最小值),我们看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;
    }

判断子View#onMeasure()的widthMeasureSpec,其mode = AT_MOST和EXACTLY的时候,result都 = specSize,specSize就是widthMeasureSpec的size。最后返回result。那么最后setMeasuredDimension()的其实就是widthMeasureSpec的size,也就是铺满父容器。那么如何才能让wrap_content变成宽度为0呢,当然可以自己仿照getDefaultSize()来重写onMeasure(),也可以看看RelativeLayout,它用了resolveSize()来设置wrap_content的宽度。

5.总结

最后总结一下,自定义ViewGroup重写onMeasure()的时候,onMeasure()的两个参数也是由父ViewGroup的child.measure()传过来的,而XML中自定义ViewGroup的layout属性,在父ViewGroup执行getChildMeasureSpec()的时候会和父ViewGroup给出的widthMeasureSpec一起影响,最终生成自定义ViewGroup#onMeasure()的两个参数。概括下就是View#onMeasure()的两个参数,受父ViewGruop的MeasureSpec和View在xml中设置的layout属性一起影响。这里还有一个问题,就是顶级的布局,其父ViewGroup给顶级布局的MeasureSpec的mode到底是什么?

 

表中 父ViewGoup代表的是顶级布局(自定义布局)。通过这样测试,我发现其实MeasureSpec的mode就是EXACTLY(结合getChildMeasureSpec反推出来的)。

onMeasure的重写其实比较有限,主要就是要完善wrap_content的功能才去重写。重写的步骤很简单:

  • ViewGroup要给每个子View发布测量命令并配置他们的MeasureSpec(用ViewGroup已经给出的方法也是可以的)。
  • 根据getChildCount()和getChildAt()来遍历子View,然后通过view#getMeasuredWidth()来获取子View测量好的宽高。
  • 设置自定义ViewGroup的宽高,setMeasuredDimension()
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            measureChildren(widthMeasureSpec, heightMeasureSpec);
    
            View view = getChildAt(0);
            int viewWidth = view.getMeasuredWidth();
            int viewHeight = view.getMeasuredHeight();
            Log.d("debug","viewWidth---:"+viewWidth+"---viewHeight---:"+viewHeight);
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值