LinearLayout源码详解

LinearLayout源码详解Linearlayout源码详解,主要讲解了Linearlayout的源码中的onMeasure方法,

LinearLayout源码详解

需要关注的点

1:linearlayout只进行一次测量,但是在设置了weight之后会进行两次测量

源码分析

一般所有控件类的源码,都会从 measure, layout和draw3个方法入手,查看他们的回调函数onMeasure, onLayout和onDraw 只要明白这3个流程,一般控件的整个实现也就明白了 LinearLayout作为一个ViewGroup的子类,主要作为一个布局容器出现,所以我们需要重点查看onMeasure方法,

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

在onMeasure中,根据orientation的值来进行纵向的测量或者是横向的测量,纵向和横向测量的逻辑相识,所以我们只需要选取其中一个来进行分析,这里,我们只分析纵向的测量

    //记录了内部已经被使用的高度
    mTotalLength = 0;
    //childView 最大的宽度,用于计算LinearLayout的宽度
    int maxWidth = 0;
    int childState = 0;
    int alternativeMaxWidth = 0;
    int weightedMaxWidth = 0;
    boolean allFillParent = true;
    //权重值的总和
    float totalWeight = 0;
    
    //纵向上面子控件的个数
    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;

首先初始化了一大堆常量,这里我们只需要关注我注释了的几个就好了

    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;
        }
        

首先是将子控件取出来,判断控件是否为null,如果为空或者Visibility为gone就直接下一个控件,这里也可以看出来gone与invisible的区别,如果有分割线,再将分割线的高度也加上

    //有时候我们在代码里面通过Inflater服务,动态加载一个布局,然后去设置他的LayoutParams,如果不引用父容器的LayoutParams就会报一个强转错误,原因就在这个 父容器在add,measure的时候都会把子View的LayoutParams强转成自己的类型
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    
    // 得到每个子控件的LayoutParams后,累加权重和,后面用于跟weightSum相比较
    totalWeight += lp.weight;
    
    // 我们都知道,测量模式有三种:
    // * UNSPECIFIED:父控件对子控件无约束,一般只有在ScrollView这种滑动布局中才会用到
    // * Exactly:父控件对子控件强约束,子控件永远在父控件边界内,越界则裁剪。如果要记忆的话,可以记忆为有对应的具体数值或者是Match_parent
    // * AT_Most:子控件为wrap_content的时候,测量值为AT_MOST。
    
    //下面的if/else分支都是跟weight相关
    final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
    if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
        // 这个if里面需要满足三个条件:
        // * LinearLayout的高度为match_parent(或者有具体值)
        // * 子控件的高度为0
        // * 子控件的weight>0
        // 这其实就是我们通常情况下用weight时的写法
        // 测量到这里的时候,会给个标志位,稍后再处理。此时会计算总高度
        // 除开这种情况的子控件不需要measure,其他的子控件都需要被measure一次,所以这样设置属性也可以提升性能
        
        // Optimization: don't bother measuring children who are only
        // laid out using excess space. These views will get measured
        // later if we have space to distribute.
        //优化:不要麻烦测量那些只使用多余空间的孩子。如果我们有空间分布,这些视图将在稍后得到度量。
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
        skippedMeasure = true;
    } else {
        if (useExcessSpace) {
            // 满足这两个条件,意味着父类即LinearLayout是wrap_content,或者mode为UNSPECIFIED
            // 那么此时将当前子控件的高度置为wrap_content
            // 为何需要这么做,主要是因为当父类为wrap_content时,其大小实际上由子控件控制
            // 我们都知道,自定义控件的时候,通常我们会指定测量模式为wrap_content时的默认大小
            // 这里强制给定为wrap_content为的就是防止子控件高度为0.
            
            // The heightMode is either UNSPECIFIED or AT_MOST, and
            // this child is only laid out using excess space. Measure
            // using WRAP_CONTENT so that we can find out the view's
            // optimal height. We'll restore the original height of 0
            // after measurement.
            //heightMode 是 UNSPECIFIED or AT_MOST, childView 只使用剩余空间
            //使用WRAP_CONTENT进行测量,以便找出视图的最佳高度。测量后恢复原来高度 0
            lp.height = LayoutParams.WRAP_CONTENT;
        }
        
        // 如果当前的LinearLayout不是EXACTLY模式,且子View的weight大于0,优先会把当前LinearLayout的全部可用高度用于子View测量
        // 我们在代码中也可以很清晰的看到,在getChildMeasureSpec()中,子控件需要把父控件的padding,自身的margin以及一个可调节的量三者一起测量出自身的大小。那么假如在测量某个子控件之前,weight一直都是0,那么该控件在测量时,需要考虑在本控件之前的总高度,来根据剩余控件分配自身大小。而如果有weight,那么就不考虑已经被占用的控件,因为有了weight,子控件的高度将会在后面重新赋值。
        
        // Determine how big this child would like to be. If this or
        // previous children have given a weight, then we allow it to
        // use all available space (and we will shrink things later
        // if needed).
        //确定这个孩子想要多大。如果这个或前面的孩子给了一个权重,那么我们允许它使用所有可用的空间
        //(如果需要,我们将在稍后缩小内容)。
        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.
            //恢复原来的高度,并记录我们分配给 excess-only children 的空间大小,以便我们能够准确地匹配测量的行为。
            lp.height = 0;
            consumedExcessSpace += childHeight;
        }

        final int totalLength = mTotalLength;
        // getNextLocationOffset返回的永远是0,这里是加上子控件的margin值
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                lp.bottomMargin + getNextLocationOffset(child));
        // 如果设置了measureWithLargestChild属性为true,获取最高子控件的高度
        if (useLargestChild) {
            largestChildHeight = Math.max(childHeight, largestChildHeight);
        }
    }
        

useLargestChild 可以通过 xml 属性 android:measureWithLargestChild 设置的,含义是所有带权重属性的View都会使用最大View的最小尺寸

        //useLargestChild 属性指定
//所以接下来根据 largestChildHeight 重新计算高度
    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));
        }
    }

使用 largestChildHeight 重新计算mTotalLength,在代码中也可以看到,这个属性只在wrap_content情况下生效

到第二次测量中间还有一些其他的计算,就不一一去看了,直接看第二次测量

// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
// 重新计算有 weight 属性的 childView 大小,
// 如果还有可用的空间,则扩展 childView,计算其大小
// 如果 childView 超出了 LinearLayout 的边界,则收缩 childView
int remainingExcess = heightSize - mTotalLength
            + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
    if (skippedMeasure
            || ((sRemeasureWeightedChildren || remainingExcess != 0) && totalWeight > 0.0f)) {
        // 根据 mWeightSum 计算得到 remainingWeightSum,mWeightSum 是通过
        // android:weightSum` 属性设置的,totalWeight 是通过第一次 for 循环计算得到的
        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;
            // 这是设置了 weight 的情况下,最重要的一行代码
            // remainingExcess 剩余高度 * ( childView 的 weight / remainingWeightSum)
            // share 便是此 childView 通过这个公式计算得到的高度,
            // 并重新计算剩余高度 remainingExcess 和剩余权重总和 remainingWeightSum
            if (childWeight > 0) {
                final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                remainingExcess -= share;
                remainingWeightSum -= childWeight;

                final int childHeight;
                if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                    childHeight = largestChildHeight;
                } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                        || heightMode == MeasureSpec.EXACTLY)) {
                    //如果是当前LinearLayout的模式是EXACTLY
                    //那么这个子View是没有被测量过的,就需要测量一次
                    //如果不是EXACTLY的,在第一次循环里就被测量一些了
                    // This child needs to be laid out from scratch using
                    // only its share of excess space. childHeight = share;
                } else {
                    //如果是非EXACTLY模式下的子View就再加上
                    //weight分配占比*剩余高度
                    // This child had some intrinsic height to which we
                    // need to add its share of excess space. childHeight = child.getMeasuredHeight() + share;
                }

                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);
    }
}

举例

在weight计算方面,我们可以清晰的看到,weight为何是针对剩余空间进行分配的原理了。 我们打个比方,假如现在我们的LinearLayout的weightSum=10,总高度100,有两个子控件(他们的height=0dp),他们的weight分别为2:8。

那么在测量第一个子控件的时候,可用的剩余高度为100,第一个子控件的高度则是100*(2/10)=20,接下来可用的剩余高度为80

我们继续第二个控件的测量,此时它的高度实质上是80*(8/8)=80

到目前为止,看起来似乎都是正确的,但关于weight我们一直有一个疑问:**就是我们为子控件给定height=0dp和height=match_parent时我们就会发现我们的子控件的高度比是不同的,前者是2:8而后者是调转过来变成8:2 **

对于这个问题,我们不妨继续看看代码。

接下来我们会看到这么一个分支:

if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) { } else {}

首先我们不管heightMode,也就是父类的测量模式,剩下一个判定条件就是lp.height,也就是子类的高度。

既然有针对这个进行判定,那就是意味着肯定在此之前对child进行过measure,事实上,在这里我们一早就对这个地方进行过描述,这个方法正是measureChildBeforeLayout()。

还记得我们的measureChildBeforeLayout()执行的先行条件吗

YA,just u see,正是不满足(LinearLayout的测量模式非EXACTLY/child.height==0/child.weight/child.weight>0)之中的child.height==0

因为除非我们指定height=0,否则match_parent是等于-1,wrap_content是等于-2.

在执行measureChildBeforeLayout(),由于我们的child的height=match_parent,因此此时可用空间实质上是整个LinearLayout,执行了measureChildBeforeLayout()后,此时的mTotalLength是整个LinearLayout的大小

回到我们的例子,假设我们的LinearLayout高度为100,两个child的高度都是match_parent,那么执行了measureChildBeforeLayout()后,我们两个子控件的高度都将会是这样:

child_1.height=100

child_2.height=100

mTotalLength=100+100=200

在一系列的for之后,执行到我们剩余空间:

int delta = heightSize – mTotalLength;

(delta=100[linearlayout的实际高度]-200=-100)

没错,你看到的的确是一个负数。

接下来就是套用weight的计算公式:

share=(int) (childExtra * delta / weightSum)

即:share=-100(2/10)=-20;*

然后走到我们所说的if/else里面

if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                    // child was measured once already above...
                    // base new measurement on stored values
                    int childHeight = child.getMeasuredHeight() + share;
                    if (childHeight < 0) {
                        childHeight = 0;
                    }
                    
                    child.measure(childWidthMeasureSpec,
                            MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                } 

我们知道child.getMeasuredHeight()=100

接着这里有一条int childHeight = child.getMeasuredHeight() + share;

这意味着我们的childHeight=100+(-20)=80;

接下来就是走child.measure,并把childHeight传进去,因此最终反馈到界面上,我们就会发现,在两个match_parent的子控件中,weight的比是反转的。

小结

在文章的最后,我们小结一下对于测量这里的算法的不同情况下的区别以及原理:

父控件是match_parent(或者精确值),子控件拥有weight,并且高度给定为0:

子控件的高度比例将会跟我们分配的layout_weight一致,原因在于weight二次测量时走了else分支,传入的是计算出来的share值 父控件是match_parent(或者精确值),子控件拥有weight,但高度给定为match_parent(或者精确值):

子控件高度比例将会跟我们分配的layout_weight相反,原因在于在此之前子控件测量过一次,同时子控件的测量高度为父控件的高度,在计算剩余空间的时候得出一个负值,加上自身的测量高度的时候反而更小 父控件是wrap_content,子控件拥有weight:

子控件的高度将会强行置为其wrap_content给的值并以wrap_content模式进行测量 父控件是wrap_content,子控件没有weight:

子控件的高度跟其他的viewgroup一致

今天的文章LinearLayout源码详解分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/13463.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注