View

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

2020-11-11  本文已影响0人  福later

大家先看一段代码

<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.如何绘制分割线,如果控制是否需要测量?参考分析源码

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

上一篇下一篇

猜你喜欢

热点阅读