美文网首页
2019-07-08View测量模式

2019-07-08View测量模式

作者: 猫KK | 来源:发表于2019-07-08 19:40 被阅读0次

    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,最后这样一层一层传递下去,最终得到所有的控件的宽高。

    相关文章

      网友评论

          本文标题:2019-07-08View测量模式

          本文链接:https://www.haomeiwen.com/subject/lofahctx.html