深挖View测量流程之FrameLayout源码详解

背景:在android应用开发中,接触的最多的应该是android中各种各样的控件,或者自定义的各种控件了。应用层开发人员基本上每天都在和控件打交道,但是虽然我们经常在用,但是我们很少去关注源码实现,只有出现问题或者需要实现一些原生控件不能满足的需求的时候,我们可能才去了解View的原理和实现。既然View使用如此频繁,那我们就更需要去了解,这样才能在开发工作中得心应手!
我们都知道View有三大流程,分别是

  1. Measure 测量。
  2. Layout 布局。
  3. Draw 绘制。

我们今天就先从第一步Measure测量开始查看源码,看看里面原理是什么?都做了些什么操作?

好了直接上手分析,首先我们在ViewGroup源码分析那篇文中,我们知道Activity的顶层View是DecorView,而DecorView的本质其实是一个继承自FrameLayout的View。所以既然是继承自FrameLayout,那么我们就以FrameLayout为切入点。

前置过程为:ViewRootImpl.java 中的performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);方法。

 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

performMeasure()方法里面 直接调用了View的measure()方法,measure()方法里面又调用了onMeasure(widthMeasureSpec, heightMeasureSpec); 这里我们找到了onMeasure()方法。因为在FrameLayout里面重写了这个方法,所以我们去FrameLayout里面看看实现逻辑。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        /** 因为Window里面第一层是FrameLayout,所以先看FrameLayout的onMeasure()方法 */

        /** 获取孩子数量 */
        int count = getChildCount();

        /**
         * 如果当前view的父view施加的宽度或者高度模式有一个不是 精确模式 EXACTLY
         * 那么就需要把MatchParent的子view找出来,再次测量
         * */
        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);

            /** 如果孩子不是Gone状态,执行以下逻辑 */
            if (mMeasureAllChildren || child.getVisibility() != GONE) {

                /** 子view测量 */
                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);

                /**
                 * 保存的是所有子view测量之后的宽高信息,有点类似 childState += XXX 的意思,
                 * 只不过这里具体是通过 按位与 和 按位或 运算的,不过可以理解为 += 的意思。
                 * 就是说全部运算后保存在 childState 这个局部变量里面。
                 *
                 * */
                childState = combineMeasuredStates(childState, child.getMeasuredState());

                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        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());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

        /** 孩子宽度或者高度中有Match-Parent属性的View集合 */
        count = mMatchParentChildren.size();
        if (count > 1) {

            /**
             * 如果当前FrameLayout 不是精确模式,那么只要子view的宽度或者高度中有MATCH_PARENT
             * 那么需要单独进行二次测量,以确保给子view MATCH_PARENT 施加精确模式,有利于子view的
             * 测量。
             * */
            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) {

                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

这里的代码并不多,我把这里逻辑大概分成2大块

  1. 测量每个子view的宽度和高度,来确定FrameLayout的宽度和高度,并保存。
  2. 就是把FrameLayout中拥有Match-Parent属性的子view进行重新测量(这是一步优化)。

第一步:确定FrameLayout的宽度和高度

在具体分析源码之前,我们可以简单的思考一下,FrameLayout要如何确定自己的宽高,根据我们在开发中的经验,大概应该是这个样子:FrameLayout的宽度应该是宽度最大的子view的宽度,FrameLayout的高度应该是高度最大子view的高度。目前我们大概就知道这么多,然后我们看看源码是怎么做的。

  /** 最大高度,最大宽度 */
        int maxHeight = 0;
        int maxWidth = 0;

先定义了一个最大宽度和一个最大高度的局部变量,我们可以猜到这应该是最后给FrameLayout赋值宽高的变量。继续向下看:

        /** 遍历自己的孩子 */
        for (int i = 0; i < count; i++) {

            final View child = getChildAt(i);

            /** 如果孩子不是Gone状态,执行以下逻辑 */
            if (mMeasureAllChildren || child.getVisibility() != GONE) {

                /** 子view测量 */
                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);

                /**
                 * 保存的是所有子view测量之后的宽高信息,有点类似 childState += XXX 的意思,
                 * 只不过这里具体是通过 按位与 和 按位或 运算的,不过可以理解为 += 的意思。
                 * 就是说全部运算后保存在 childState 这个局部变量里面。
                 *
                 * */
                childState = combineMeasuredStates(childState, child.getMeasuredState());

                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

是的,没错就是遍历所有的孩子,这里面调用了一个重要的方法来测量子view的宽高。

                  /** 子view测量 */
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

进入这个方法:

   /**
     * 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.
     *
     * 翻译:让view的一个孩子测量自己,考虑到view的测量规格和他的内边距和外边距。孩子必须有
     * MarginLayoutParams,在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);
    }

这个方法的介绍,意思是说,要考虑到FrameLayout的内边距 和 子view的外边距,子view的测量规格是在getChildMeasureSpec()这个方法中获取的。其实也很容易想明白,FrameLayout剩下的空间 = FrameLayout最大空间 - (自己的内边距 + 子view的外边距)。计算出来的空间才是子view真正能获得的最大空间。

继续进入getChildMeasureSpec()方法:

      public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

        /** 当前ViewGroup的测量规格,用来计算得到子view的测量规格 */

        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
        /** 1、父view施加了一个确切的尺寸 */
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                /** 如果子view是一个确切的数值 */
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                /** 子view设置了,充满父view,子view的size = 父view的size - (父view的padding + 子view的marging) */
                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.
                /** 如果子view是自适应,那么它的最大size不能比父view的size大, 即:子view的size <= 父view的size */
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        /** 2、父view施加了一个最大限度的尺寸 */
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                /** 子view设置了具体数值,那么size = 具体数值 */
                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.
                /** 子view的size最大不能超过父view的size */
                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.
                /** 子view的size最大不能超过父view的size */
                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);
    }

看到这个这里,相信不陌生了,这就是根据父view的测量规格和子view的宽高size,计算得到子view的测量规格。测量规格大家应该都知道吧,我简单描述下:

测量规格就是MeasureSpec 这个类,主要用来帮助View的测量,这个类主要包含两部分,一个是测量模式 Mode, 一个是测量尺寸Size。用32位二进制表示:
Mode:占高2位。11000000 00000000 00000000 00000000
Size:占低30位。00111111 11111111 11111111 11111111
因为模式只有三种,所以用两位表示。

在这个方法中,计算出resultMode 和 resultSize 最后通过MeasureSpec.makeMeasureSpec(resultSize, resultMode);这个方法生成一个测量规格,这就是子view的测量规格。
代码中间那部分switch判断可以用一个表格来说明:


计算子view测量规格.png

子view的尺寸其实就是我们在xml布局文件中,写的layout-width= "100dp"这个属性,
这个尺寸可以有三种类型:

  1. 固定数值,类似100dp、130px这种是具体的一个数值,但是必须是大于0的!
  2. Match-Parent, 表示和父控件一样大。
  3. Wrap-Content,表示内容自适应,但是最大不会超过父view。

根据父view的测量规格,和子view自身的尺寸值,会得到一个施加给子view的测量规格。通过这个测量规格,子view可以测量自身的宽高。

到这里我们就把measureChildWithMargins() 方法看完了,这里面其实就是:

  1. 先计算子view宽高的测量规格。
  2. 测量子view。

然后记录maxWidth 和 maxHeight:

maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);

maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }

这一步是将子view中包含有MATCH_PARENT的view添加到mMatchParentChildren这个列表中,第二步中的优化要用到这个列表。

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));

最后调用setMeasuredDimension()这个方法,保存FrameLayout的宽高。这个方法调用了resolveSizeAndState()这个方法,继续进入看看这个方法实现:

/**
     * Utility to reconcile a desired size and state, with constraints imposed
     * by a MeasureSpec. Will take the desired size, unless a different size
     * is imposed by the constraints. The returned value is a compound integer,
     * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
     * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
     * resulting size is smaller than the size the view wants to be.
     *
     * 大概意思是说:这个方法通过view的父view施加的测量规格MeasureSpec,来得到当前view的尺寸size
     * 和状态state。除非这个约束给了一个不一样的size尺寸,否则当前view可以得到自己想要的尺寸size。
     * 返回值是把得到的尺寸size和MEASURED_SIZE_MASK 或者 MEASURED_STATE_TOO_SMALL
     * 这个标志结合起来得到的一个复合值;
     * 如果计算出来的result size 比当前view想要的size要小,
     * 那么就把计算出来的size和MEASURED_STATE_TOO_SMALL复合。
     * 复合其实就是做一个 按位或运算。
     *
     *
     * 当前view想要多大尺寸
     * @param size How big the view wants to be.
     *
     * 当前view的父view施加的约束
     * @param measureSpec Constraints imposed by the parent.
     *
     * 当前view的子view尺寸信息,这个尺寸信息里面包含了标记消息。
     * @param childMeasuredState Size information bit mask for the view's
     *                           children.
     *
     * 返回当前view带有标记的size尺寸信息
     * @return Size information bit mask as defined by
     *         {@link #MEASURED_SIZE_MASK} and
     *         {@link #MEASURED_STATE_TOO_SMALL}.
     *
     *  这个方法其实就是根据计算所有的子view后,当前view得到一个大概的尺寸
     *  比如:FrameLayout 通过测量计算每个子view的宽高,然后记录子view中
     *  宽度最大和高度最大的值。最终把得到的最大宽度和高度,丢到这个方法里面
     *  来重新计算,得到最合适的宽高。
     *
     *  也就是说这个方法就是用来最后计算当前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的模式是,最大尺寸不超过XXX模式 */
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                    /**
                     * 如果传进来的尺寸size 大于 当前view的父view施加的测量值,
                     * 那么result 就等于 父view施加的最大值,并且标记上 MEASURED_STATE_TOO_SMALL。
                     * */
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;

            /**
             *  当前view模式是,精准模式。
             *  精准模式下,当前view的宽高并不会受子view影响,
             *  是多大就是多大不受任何影响。
             *
             *  比如:FrameLayout 设置了宽度为200px。
             *  FrameLayout 有个子view设置了宽度为500px,
             *  那么我们可以知道FrameLayout的宽度并不会因为子view的500px而变大,
             *  FrameLayout依然还是设置好的精准的200px。
             *  specSize 就是我们设置的精准值。
             *
             *  通过源码我们理解的更清楚。
             *  */
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;

            /**
             * 当前view的模式是,未指定大小模式
             * 处于这种模式下,当前view的宽高均为传入的尺寸size,
             * 即当前view计算出来自己的大概的size,多大都不做限制。
             *
             * 比如:有一个经典的案例就是ListView嵌套ListView的冲突问题,
             * 在以前ListView用的很多的时候,网上经常看到有一种解决方案就是
             * 将里面的ListView的高度设置为所有子view的高度之和,即不让里层
             * 的ListView滑动,那么这里就用到了这个姿势,我们将里层ListView
             * 的测量模式改成MeasureSpec.UNSPECIFIED,那么把他所有的子view
             * 的高度加起来,计算出来的size,传到这个方法来的时候,我们发现
             * result = size; 这就表明当前ListView的高度取决于,你传进来的size,
             * 现在传进来的是所有子view高度之和,那么里层ListView的高度也就是所有
             * 子view的高度之和,那么嵌套滑动冲突的问题也就解决了。
             *
             * 这里就是解决上述问题的关键!
             * */
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

详细的注释说明,我都在代码里面加了,概括一下说就是:我们上面遍历每个子view,得到了maxWidth 和 maxHeight ,但是这个maxWidth 和 maxHeight 是有可能超出父view的最大尺寸的,如果是在FrameLayout是AT_MOST模式下,那么当前的FrameLayout的尺寸只能是规格中的specSize,不能超过。当然如果FrameLayout是精准模式,那么就不受影响,我指的不受影响是不受子view的宽高影响,比如FrameLayout设置了宽度为300px那么随便你子view是多宽,FrameLayout都是300px不会有任何影响。最后那种模式UNSPECIFIED,遇到过ListView嵌套问题的开发者,应该很有感觉。

总结下这个方法resolveSizeAndState() 其实就是计算FrameLayout我自己的宽高别搞混了,其实我们之前做的所有工作都是为了最后这一步计算自身的宽高的,当然也顺便把测量事件一层层传递下去。

第二步:优化带有Match-Parent属性的子view

/** 孩子宽度或者高度中有Match-Parent属性的View集合 */
        count = mMatchParentChildren.size();
        if (count > 1) {

            /**
             * 如果当前FrameLayout 不是精确模式,那么只要子view的宽度或者高度中有MATCH_PARENT
             * 那么需要单独进行二次测量,以确保给子view MATCH_PARENT 施加精确模式,有利于子view的
             * 测量。
             * */
            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) {

                    final int width = Math.max(0, getMeasuredWidth()
                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }

先检查列表中是否有值,如果有,进行遍历,遍历里面的主要逻辑是:如果子view的width 或者 height 带有Match-parent 那么会将子view的测量规格设置为EXACTLY,然后对子view进行重新测量。看到这里,就有很大的疑问了:明明上面第一步的时候已经遍历了所有的子view,并且已经测量过了,为什么还要再测量一次了,不浪费资源和性能吗?
首先我们看,在第一步我们确实已经把FrameLayout的宽高已经计算出来了,就是说当前这个ViewGroup已经是明确知道宽度和高度的了,如果子view中有Match-Parent,这种情况下,把子view设置为EXACTLY,确实也是没什么问题的。而且我们在看为什么一定要重新再设置为EXACTLY,在FrameLayout onMeasure() 方法最顶部有一个判断

/**
         * 如果当前view的父view施加的宽度或者高度模式有一个不是 精确模式 EXACTLY
         * 那么就需要把MatchParent的子view找出来,再次测量
         * */
        final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }

这两段代码连起来看,是不是发现了什么,如果当前这个FrameLayout不是精确模式的话,那么在遍历子view的逻辑中就会把包含Match-Parent的子view添加到列表中,这个列表就是我们第二步中关键的遍历列表,也就是说第二步中把子view测量模式改成EXACTLY也正是为了不让FrameLayout进入两次测量;而且相对来说,EXACTLY测量模式相对来说会优于AT-Most这种测量模式。
最后这一点可能有点拗口的感觉,不好理解,我们用demo测试代码来说明一下吧:

  <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

           <com.demo.viewoffset.view.CustomMeasureView
                android:id="@+id/view"
                android:background="#409"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </FrameLayout>

    </LinearLayout>

// 自定义类
public class CustomMeasureView extends View {

    public static final String TAG = "CustomMeasureView";


    public CustomMeasureView(Context context) {
        super(context);
    }

    public CustomMeasureView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomMeasureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        Log.e(TAG, "onMeasure: 执行");

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        Log.e(TAG, "onMeasure: widthMode = " + widthMode);
        Log.e(TAG, "onMeasure: widthMode = " + (widthMode >> 30));
    }
}

我们可以写一个这样的布局,和一个自定义View,我们通过打印日志来看,测量方法执行了几次。
上面的xml文件中,我们看到FrameLayout 宽高都是wrap-content,由此我们可以知道FrameLayout的测量模式是At-most,即满足:
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
这个条件,而此时FrameLayout的子view宽高都是Match-Parent,那么满足我们刚刚的分析,也就是说我们的自定义View的onMeasure()方法会被执行两遍,打印日志我们确实看到了两遍打印:


日志.png

结论:也就是说触发FrameLayout对子view绘制两次的条件有三个:

  1. 当前FrameLayout的宽高的任一一个测量模式是At-most。
  2. 子view的宽高中有一个包含Match-Parent。
  3. 满足条件的子view至少有两个。
    如果满足以上3个条件,那么FrameLayout就会对包含Match-Parent的子view进行第二次绘制。

结尾

FrameLayout的测量流程基本就这些,主要就是做三件事:

  1. 测量子view。
  2. 确定自己的宽高。
  3. 是否对子view进行二次绘制。

看完上面的分析,我们至少要知道,

  1. FrameLayout的测量机制。
  2. ViewGroup是如何计算子view的测量规格的。
  3. 所有的测量都是在onMeasure()方法中做的,每个view都是重写了onMeasure()方法,并在自己的onMeasure()方法中实现自己的逻辑。

下一篇:对比LinearLayout 和 RelativeLayout这个两大容器在测量上的异同。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容