美文网首页View
LinearLayout之垂直布局测量分析详解

LinearLayout之垂直布局测量分析详解

作者: 福later | 来源:发表于2020-11-11 15:39 被阅读0次

    大家先看一段代码

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:orientation="vertical"
            android:background="@mipmap/ic_launcher"
            >
            <View
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:layout_weight="1"
                android:background="@color/colorAccent"
                />
            <View
                android:layout_width="match_parent"
                android:layout_height="60dp"
                android:layout_weight="1"
                android:background="@color/colorPrimary"
                />
        </LinearLayout>
    
    </LinearLayout>
    

    你觉得上面布局中,两个View实际分配到的高度是多少?大家可以将代码运行下,其实最终第一个View分配到的高度是170dp,第二个是30dp;为什么呢,这就要熟读下面的分析了。

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    //记录测量的高度
            mTotalLength = 0;
    //记录LineraLayout 的最大宽度,每次测量子view时,都会对maxWidth赋值
            int maxWidth = 0;
            int childState = 0;
            int alternativeMaxWidth = 0;
    //记录设有权重的view的最大宽度
            int weightedMaxWidth = 0;
            boolean allFillParent = true;
    //所有权重总和
            float totalWeight = 0;
    //所有子view数量
            final int count = getVirtualChildCount();
    //获取宽度测量模式
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    //获取高度测量模式
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            boolean matchWidth = false;
            boolean skippedMeasure = false;
    //这个baselineChildIndex 是什么东西
            final int baselineChildIndex = mBaselineAlignedChildIndex;
    //是否使用最大View的尺寸,默认情况下很少使用
            final boolean useLargestChild = mUseLargestChild;
    
            int largestChildHeight = Integer.MIN_VALUE;
    //记录被消费的控件
            int consumedExcessSpace = 0;
    //记录真正测量的子view,即排除view==null或者view可见性为View.GONE
            int nonSkippedChildCount = 0;
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                if (child == null) {
    //measureNullChild一般默认返回0;
                    mTotalLength += measureNullChild(i);
                    continue;
                }
    
                if (child.getVisibility() == View.GONE) {
    //getChildrenSkipCount一般默认返回0;
                   i += getChildrenSkipCount(child, i);
                   continue;
                }
    
                nonSkippedChildCount++;
    //如果子view之间有分割线,记录分割线的高度
                if (hasDividerBeforeChildAt(i)) {
                    mTotalLength += mDividerHeight;
                }
    
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    //累加设有权重的view
                totalWeight += lp.weight;
    
                final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
                if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
    //如果是精确测量模式,并且子view属性layout_height=0;layout_weight>0 ;对于这种view,我们先不对view测量,先记录下topMargin和bottomMargin  ,为什么要这样?把其他view消耗的高度测量出来,剩余高度然后根据权重比重新分配给设有权重的View,因此,我们在写LineraLayout布局时,layout_heigith>0的View,总是优先分配高度
                    mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                    skippedMeasure = true;
                } else {
    /**
    这个分支分以下几种情况
    1.AT_MOST
        1.1.height=0 && weight =0
        1.2.height>0&&weight=0
        1.3.height=0&&weight>0(在后面源码中设置成了同1.5)
            1.4.height>0&&weihgt>0
            1.5.height=wrap_parent&&weight>0
            1.6.height=wrap_parent&&weight=0
            1.7.height=match_parent&&weight>0
            1.8.height=match_parent&&weight=0
    2.EXACTLY
        2.1.height=0 && weight =0
        2.2.height>0&&weight=0
            2.3.height>0&&weihgt>0
            2.4.height=wrap_parent&&weight>0
            2.5.height=wrap_parent&&weight=0
            2.6.height=match_parent&&weight>0
            2.7.height=match_parent&&weight=0
            2.8.height=0&&weight>0(这个情况不会出现在这个分支,放一起好分析)
    从上面情况结合下面measureChildBeforeLayout方法可以得出结论:
    a.1.1;1.2;1.4;2.1;2.2;2.3;测得子view的height为view中的LayoutParams中的height,自己对自己的子view的测量模式为EXACTLY
    b.1.3;1.5;1.6;1.7;1.8;2.4;2.5;测得子view的height为父view中期望高度-已使用的高度-子view的padding-子view中的margin,自己对自己的子view的测量模式为AT_MOST
    c.2.6;2.7;测得子view的height为父view中期望高度-已使用的高度-子view的padding-子view中的margin,自己对自己的子view的测量模式为EXACTLY
    d.2.8;只有某一个子view是2.8这种情况或者在a,b,c 这几种情况测量完成后剩余空间不为0(大于或小于0)并且子view中有weight>0d的,二者满足其一会再一次对权重大于0 的子view调试尺寸
    */
                    if (useExcessSpace) {
                   //对于1.3.height=0&&weight>0;为什么要这样设置,因为这样能给这类子view于父View中剩余最大高度尺寸,能最大限度的满足子view;无疑这样设计是最佳的选择
                        lp.height = LayoutParams.WRAP_CONTENT;
                    }
    /*usedHeight 已使用的高度,如果totalWeight>0,说明之前有view设有权重,这里就采用了一种策略,先尽可能的给予每个设有权重的子view最大尺寸高度,后面再根据策略调整。*/
    
                    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
    //测量子view的尺寸
                    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                            heightMeasureSpec, usedHeight);
              
                    final int childHeight = child.getMeasuredHeight();
                    if (useExcessSpace) {
                        consumedExcessSpace += childHeight;
                    }
    
                    final int totalLength = mTotalLength;
    //累计总高度
                    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                           lp.bottomMargin + getNextLocationOffset(child));
    //是否用最大view尺寸,不分析,默认不使用
                    if (useLargestChild) {
                        largestChildHeight = Math.max(childHeight, largestChildHeight);
                    }
                }
                if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
                   mBaselineChildTop = mTotalLength;
                }
    //view索引小于baselineChildIndex,如果设置了lp.weight 会有异常
                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) {
    //这种模式下,如果缩放LineraLayout ,view也需要相应重新测量,所以这里先做个标记,如果子view中有该种模式;
                    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) {
                   
                    weightedMaxWidth = Math.max(weightedMaxWidth,
                            matchWidthLocally ? margin : measuredWidth);
                } else {
                    alternativeMaxWidth = Math.max(alternativeMaxWidth,
                            matchWidthLocally ? margin : measuredWidth);
                }
              //获取需要跳过测量的标示,getChildrenSkipCount默认返回0,如果返回1,则代表下一个view不需要测量
                i += getChildrenSkipCount(child, i);
            }
    /*for循环结束,初步测量完成,总结下上面for循环干了啥事
    1.测量出部分子view的高度,(父View为EXACTLY并且子view高度和weight都为0  ,这里view先不测量其高度,)
    2.记录总的需要的高度如:mTotalLength,mTotalLength包含已测量的部分view 的高度和margin,分割线,父View为EXACTLY并且子view高度和weight都为0这类view的margin*/
    
            if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
                mTotalLength += mDividerHeight;
            }
    //如果使用了使用最大子view尺寸,并且父view模式为Wrap_content,则需要重新计算mTotalLength,所以最好不要设置useLargestChild,效率低
            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;
    
            int heightSize = mTotalLength;
    
            // Check against our minimum height
           //看下是否有背景宽高,取二者大的
            heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    //对比下heightSize 与父view期望的高度
    //1.如果父view测量模式是EXACTLY,heightSizeAndState 赋值为父view的期望高度
    //2.如果父view测量模式是AT_MOST,heightSizeAndState 赋值为父view的期望高度与heightSize 哪个小就用哪个;
            int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
            heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
    /**
    remainingExcess的值用来判断剩余空间以便来决定是否需要缩小或者扩大weight>0的子view,如果remainingExcess的值大于0,则会扩大部分View高度,如果remainingExcess小于0,则会缩小部分view高度,remainingExcess等于0,就不用再测量,这是效率最高的测量模式。平时开发应该尽量满足。为什么remainingExcess会小于0,在上面的测量中,有如果view的高度在xml明确写明了的话,会直接将该高度作为初步测量高度。mTotalLength是每个测量好的view高度的累计,比如父view高度为100dp,但是子view有几个50dp的,那最终mTotalLength的高度肯定高于heightSize*/
            int remainingExcess = heightSize - mTotalLength
                    + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
    /*下面的if条件满足以下其一都执行,if里面的逻辑主要是缩放子view
    1.如果子view中满足父View为EXACTLY,子view高度和weight都为0
    2.remainingExcess!=0 && 至少一个子view中的weight>0*/
            if (skippedMeasure
                    || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
                float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
    //下面要重新计量高度了
                mTotalLength = 0;
    
                for (int i = 0; i < count; ++i) {
                    final View child = getVirtualChildAt(i);
                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }
    
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    final float childWeight = lp.weight;
                    if (childWeight > 0) {
    //share的值如果大于0;说明是给满足条件的view放大;如果小于0;则是缩小满足条件的
                        final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                        remainingExcess -= share;
                        remainingWeightSum -= childWeight;
    
                        final int childHeight;
    //mUseLargestChild  默认为false
                        if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                            childHeight = largestChildHeight;
                        } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                                || heightMode == MeasureSpec.EXACTLY)) {
           //这个就是为父View为EXACTLY,子view高度和weight都为0的这类view高度赋值,这类view在前面并没有参与测量。这类view的高度分配权最低;
                            childHeight = share;
                        } else {
                           //这类view为前面参与测量的view,重新分配高度
                            childHeight = child.getMeasuredHeight() + share;
                        }
    //经过这次调整的View,都将自己对子view的高度测量模式设置为EXACTLY了,因为这次所有的view的高度尺寸都已知道了。
                        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                                Math.max(0, childHeight), MeasureSpec.EXACTLY);
                        final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                                lp.width);
                        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
                        // Child may now not fit in vertical dimension.
                        childState = combineMeasuredStates(childState, child.getMeasuredState()
                                & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                    }
    
                    final int margin =  lp.leftMargin + lp.rightMargin;
                    final int measuredWidth = child.getMeasuredWidth() + margin;
                    maxWidth = Math.max(maxWidth, measuredWidth);
    
                    boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                            lp.width == LayoutParams.MATCH_PARENT;
    
                    alternativeMaxWidth = Math.max(alternativeMaxWidth,
                            matchWidthLocally ? margin : measuredWidth);
    
                    allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
    
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                            lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
                }
    
                // Add in our padding
                mTotalLength += mPaddingTop + mPaddingBottom;
                // TODO: Should we recompute the heightSpec based on the new total length?
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                               weightedMaxWidth);
    
    
                // We have no limit, so make all weighted views as tall as the largest child.
                // Children will have already been measured once.
                if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                    for (int i = 0; i < count; i++) {
                        final View child = getVirtualChildAt(i);
                        if (child == null || child.getVisibility() == View.GONE) {
                            continue;
                        }
    
                        final LinearLayout.LayoutParams lp =
                                (LinearLayout.LayoutParams) child.getLayoutParams();
    
                        float childExtra = lp.weight;
                        if (childExtra > 0) {
                            child.measure(
                                    MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                            MeasureSpec.EXACTLY),
                                    MeasureSpec.makeMeasureSpec(largestChildHeight,
                                            MeasureSpec.EXACTLY));
                        }
                    }
                }
            }
    
            if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
                maxWidth = alternativeMaxWidth;
            }
    
            maxWidth += mPaddingLeft + mPaddingRight;
    
            // Check against our minimum width
            maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
      
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    heightSizeAndState);
    
            if (matchWidth) {
                forceUniformWidth(count, heightMeasureSpec);
            }
        }
    

    通过上面的分析,可以总结如下集中模式,以后LinearLayout布局高度尺寸分配有什么疑惑直接套用就行了
    1.父View高度测量模式AT_MOST
    1.1.height=0 && weight =0(最终测量的高度为0)
    1.2.height>0&&weight=0 (最终测量的高度为height)
    1.3.height=0&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
    1.4.height>0&&weihgt>0(height+调整尺寸)
    1.5.height=wrap_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
    1.6.height=wrap_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
    1.7.height=match_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
    1.8.height=match_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
    2.父View高度测量模式EXACTLY
    2.1.height=0 && weight =0(最终测量的高度为0)
    2.2.height>0&&weight=0(最终测量的高度为height)
    2.3.height>0&&weihgt>0(height+调整尺寸)
    2.4.height=wrap_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
    2.5.height=wrap_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
    2.6.height=match_parent&&weight>0(最终测量的高度父view期望高度-已消耗的高度-padding-margin+调整尺寸)
    2.7.height=match_parent&&weight=0(最终测量的高度父view期望高度-已消耗的高度-padding-margin)
    2.8.height=0&&weight>0(调整尺寸)
    以上如果最终高度小0,则设置为0;
    我们用这几种模式来套用开头的案例,案例中:父View高度为200dp,测量模式为EXACTLY,有两个子View,view1 高度为200dp,weight=1;view2高度为60dp,weight=1;
    分析:两个子view都符合2.3这种情况;那么我们来看看其最终高度view1=200+调整尺寸;view2=60+调整尺寸;调整尺寸 = (父View高度-子view1高度-子view2高度)/2 = (200-200-60)/2 = -30 ;所以最终
    view1的高度=200+(-30)=170;
    view2的高度=60+(-30)=30;
    当然以上计算还不严谨,但基本上可以用这个模型来阐述整个测量流程。算是有理有据吧。
    好了再总结下流程和关键点
    1.第一个for循环,首先是筛选出一批View先测量(父View高度测量模式== MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ;这类view放后面测量)
    2.计算下剩余高度(这个高度控制在父View期望高度以内,参看这句代码heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0))
    3.a,如果高度有剩余,b.至少有一个子view为:父View高度测量模式== MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ;c.至少一个子view的weight>0;如果满足a||(b&&c)则进入第二个for循环调整权重大于0的子view的高度
    经过上面3个主要流程关键点,LinearLayout内部测量算是完成了。

    通过以上学习,我们自问自答下。
    1.通过这么多的测量,最终测量出来的高度会不会超过父view期望的高度?回答是不会。具体原因请参看文中int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);这句代码上的解释。
    2.如何提高测量效率?少用weight,少用userLargest模式
    3.如何绘制分割线,如果控制是否需要测量?参考分析源码

    写在后文:忘各位同道指正评论。

    相关文章

      网友评论

        本文标题:LinearLayout之垂直布局测量分析详解

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