2019-07-08View测量模式

2019-07-08  本文已影响0人  猫KK

view的测量模式有如下三种:

  1. EXACTLY:精确的,确切的知道大小值,如在控件中设置xxdp,或者设置match_parent
  2. AT_MOST:尽可能大,如在控件中设置wrap_content
  3. UNSPECIFIED:系统自带的一种,一般只是系统使用,自定义控件使用比较少

下面通过LinearLayout来分析这些模式,通过绘制流程可以知道测量是在 onMeasure 方法中实现:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //判断 LinearLayout 的 Orientation 是水平还是垂直
        //我们分析垂直的情况,水平的也差不多
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
  void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        //总高度
        mTotalLength = 0;
        //最大宽度
        int maxWidth = 0;
        int childState = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;
        //获取子view的个数
        final int count = getVirtualChildCount();
        //获取测量模式
        //注意,这个widthMeasureSpec 是通过父布局传过来的
        //说明自身view的测量模式是在父布局中生成的
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //循环遍历所有的子view
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            //子view为null跳过
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }
            //判断子view是否设置成GONE
            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            //.......
            //获取子view的LayoutParams
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //获取权重
            totalWeight += lp.weight;
            //判断子view的高度是否设置成0并且设置了weight
            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            //如果当前LinearLayout设置的是EXACTLY,并且 useExcessSpace 为true
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                //设置 skippedMeasure 为true,后面在测量子 view
                skippedMeasure = true;
            } else {
                if (useExcessSpace) {
                    //如果子 view 的height为0 并且设置了weight,将子 view的height设置为WRAP_CONTENT
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                //测量子 view,就是调用子view的 onMeasure 方法
                //这个后面分析
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
                //获取子 view的测量后高度
                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {
                    //将子 view height 设置为0
                    lp.height = 0;
                    consumedExcessSpace += childHeight;
                }
                //定义一个中间变量,保存上一个子 view的高度
                final int totalLength = mTotalLength;
                //将 mTotalLength 设置为子 view的高度+topMargin+bottomMargin
                //totalLength 为前一个子 view的高度,所以去最大值,应为上一个子 view的高度+当前子 view高度+topMargin+bottomMargin,是一个累加过程
                //这样,就将所有的子 view高度累加到一块
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));

            //........
            //获取左右的Margin,去所有子 view的最大宽度为maxWidth
            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());

         //......
        // 最大高度加上自身的Padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // 检测计算出来的高度和默认高度,取最大值
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // 根据模式,得到最后的高度
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        
        //.....

        //设置当前LinearLayou的宽高,也就是设置 mMeasuredWidth 和 mMeasuredHeight 的值,到此LinearLayou的测量就完成
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                heightSizeAndState);
    }

先来看 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;
        //判断当前的测量模式,根据测量模式来分别判断最后取值是取 specSize 还是传过来的size
        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);
    }

根据判断当前的测量模式,根据测量模式来分别判断最后取值是取 specSize 还是传过来的size其中 specSize 是父布局传过来的,size 是自身测量得到的
那么父布局传过来的 specSize 是怎么计算呢?回到 measureChildBeforeLayout 方法中,假如一个LinearLayou中包裹一个LinearLayou,来看

    void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }

    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);
        //调用子 view的measure方法,最后也会调用到 onMeasure 方法中
        //这个在绘制流程中分析过
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

通过上面的方法可以知道子 view的MeasureSpec 是根据当前的MeasureSpec和子 view的LayoutParams决定的

    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) {
        // 如果当前的为EXACTLY
        case MeasureSpec.EXACTLY:
            //如果子 view有实际大小
            if (childDimension >= 0) {
                //将resultSize置为子 view实际大小
                resultSize = childDimension;
                //将子 view的模式置为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                 //如果子 view为MATCH_PARENT
                //将resultSize 设置为当前view 的大小
                resultSize = size;
                //将子 view的模式设置为EXACTLY
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
               //如果子 view为WRAP_CONTENT
              //将resultSize 设置为当前view 的大小
                resultSize = size;
               //将子 view的模式设置为AT_MOST
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

         //.....

        //根据resultSize 和 resultMode 生成MeasureSpec
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面子分析了一个,其他的模式都差不多,自己分析即可。所以MeasureSpec 是由父布局一层一层的往下传递,最后当最底层view测量出宽高之后,再一层一层往上传递。那么最顶层的view的MeasureSpec是怎么来的呢?
通过view的绘制流程可以知道,view开始绘制是在 ViewRootImpl 的 performTraversals 方法中:


    public final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
    private void performTraversals() {

       WindowManager.LayoutParams lp = mWindowAttributes;
        
      //.....

      //其中lp.width为LayoutParams.MATCH_PARENT
       int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
       int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        //测量流程开始,最后会调用到 onMeasure 方法中
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            //得到对应的MeasureSpec
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

到此,从ViewRootImpl得到最顶层的 MeasureSpec ,然后在根据子 view的 LayoutParams 来得到子 view的MeasureSpec,最后这样一层一层传递下去,最终得到所有的控件的宽高。

上一篇下一篇

猜你喜欢

热点阅读