新闻资讯

新闻资讯 行业动态

View 的工作流程梳理——Measure流程

编辑:008     时间:2020-02-25

Measure流程

View 的Measure流程从ViewGroup 这个类的measureChildWithMargins方法开始,这个方法是ViewGroup需要测量子View大小时调用的,所有ViewGroup的子类都会调用这个方法来测量子View的大小。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        //mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed 已经用了的大小
        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);
    }

我们看到这里传的参数是child(子view)、parentWidthMeasureSpec(父view的宽度测量规格)、widthUsed(父View已经用掉的宽度)、parentHeightMeasureSpec(父view的高度测量规格)、widthUsed(父view已经用掉的高度)。

这里的提到的测量规格是个什么?这里涉及到一个概念叫MeasureSpec。 MeasureSpec是由一个32位的int变量组成的。它包含两个部分:

  1. 头两位,代表SpecMode,即测量的模式。一共有三种模式
    • EXACTLY 精确模式,测量的大小确定。
    • AT_MOST 由父view给定一个specSize,子view最大的大小不能超过这个specSize
    • UNSPECIFIED 不限定子view的大小,用于系统内部测量,我们不用管。

然后在measureChildWithMargins方法里调用getChildMeasureSpec方法分别确定子view的宽度测量规格和高度测量规格。具体方法如下:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //子view还能够使用的大小(MeasureSpec的大小减去已经使用的大小)
        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:
        // 布局文件设置的具体的值如10dp 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.
                //子view的大小最大不超过父view剩余的大小
                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
![](https://user-gold-cdn.xitu.io/2020/2/24/17077ac03eed5f6d?w=399&h=204&f=jpeg&s=36492)
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } break;
        }
        //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

可以看到,这个方法根据父view的specMode和子view的布局设置的大小参数来确定了子view的测量规格。可以的到一个子view的MeasureSpec确定规则的表,如下:

这里看到当子view的LayoutParams 为MATCH_PARENT 和 WRAP_CONTENT 时,子view的specSize是一样的,也就是说这两者的效果是一致的。但我们实际使用布局的时候感觉不是这样的呀,例如一个TextView高宽设置成wrap_content和设置成match_parent 的显示效果是不一样的,一个是有一定的大小,另一个是充满了父布局剩余的空间。那是因为TextView 的onMeasure方法做了特殊的处理,大小变成了内容的大小,而不是充满父view的剩余空间。所以我们在自定义view的时候需要注意到这个坑,解决的方式是重写onMeasure方法设定一个默认的大小。方法如下:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
        val heigthSpecSize = MeasureSpec.getSize(heightMeasureSpec) if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, mHeight)
        } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWidth, heigthSpecSize)
        } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight)
        }
    } 复制代码

接上面的getChildMeasureSpec方法获取到子view的MeasureSpec之后就调用子view的measure方法来测量了,measure方法是final的,无法重写,在measure方法里会调用到onMeasure方法,如下:

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

可以看到里面调用到了getDefaultSize,来获取默认的大小,并调用setMeasuredDimension设置view测量的宽高。getDefaultSize方法如下:

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        //如果是UNSPECIFIED模式,则是getSuggestedMinimumWidth和getSuggestedMinimumHeight方法获取的大小 case MeasureSpec.UNSPECIFIED:
            result = size; break;
        //如果是AT_MOST和EXACTLY模式,则是specSize case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY:
            result = specSize; break;
        } return result;
    } 复制代码

再来看看getSuggestedMinimumWidth:

protected int getSuggestedMinimumWidth() {
//如果背景为空,则是则是view的mMinWidth,如果不为空则是背景最小宽度和view的mMinWidth中取一个最大值,这里的mBackground.getMinimumWidth()指的就是背景的原始宽度。 return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

由此view大致的测量流程是 父view通过getChildMeasureSpec确定子view的MeasureSpec,传递给子view,子view通过父view给的MeasureSpec参数来进行测量操作并设置测量的大小。如果子view还包含有其他的view,则会先测量下级的子view,就这样一直传递下去,等到所有的子view都测量完毕,才会确定下来自己的大小。 由于viewgroup没有实现onMeasure方法,是交给子类去实现的,这里就看一下LinearLayout是怎么实现的。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

可以看到是根据mOrientation来判断是横向测量还是纵向测量。就看一个measureVertical方法:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        ...
        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;
        boolean skippedMeasure = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;
        int consumedExcessSpace = 0;

        int nonSkippedChildCount = 0;

        // See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i); if (child == null) {
                mTotalLength += measureNullChild(i); continue;
            } if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i); continue;
            }

            nonSkippedChildCount++; if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0; if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                skippedMeasure = true;
            } else { if (useExcessSpace) {
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);

                final int childHeight = child.getMeasuredHeight(); if (useExcessSpace) {
                    // Restore the original height and record how much space
                    // we've allocated to excess-only children so that we can
                    // match the behavior of EXACTLY measurement.
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            /**
             * If applicable, compute the additional offset to the child's baseline
             * we'll need later when asked {@link #getBaseline}.
             */
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight.  fail fast to aid the developer.
            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            得到水平方向最大的宽度
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /*
                 * Widths of weighted Views are bogus if we end up
                 * remeasuring, so keep them separate.
                 */
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }

        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        //所有子view高度总和
        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        //根据情况测量自身高度
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        
        ...

        //设置自身大小      
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);

        ...
    }

这里看到LinearLayout会循环遍历子view,调用measureChildBeforeLayout方法,然后这个方法里面调用到了measureChildWithMargins方法,这就是最开始提到的那个方法,到了子view的测量流程,在这个方法里分别去调用子view的measure方法,让子view分别完成自己的测量,然后累加竖直方向的高度到mTotalHeight。而水平的测量则是遍历子view的到最大的maxWidth,然后会测量并设置自己的大小。对于竖直方向排列的LiearLayout,水平方向如果specMode是AT_MOST,则宽度为maxWidth,但是这个不能超过LiearLayout的父view的剩余空间,如果是EXACTLY,则是specSize。竖直方向的测量如果specMode是AT_MOST,则高度是所有子view高度总和,还要考虑margin,padding等等,如果是EXACTLY模式则也是specSize,具体可以看resolveSizeAndState这个方法:

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) { case MeasureSpec.AT_MOST: 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);
    }

到这里view的测量流程就介绍完了。



原文链接:https://juejin.im/post/5e53e031f265da573c0c778f
郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。

回复列表

相关推荐