欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android开发学习之线性布局测量流程源码阅读

程序员文章站 2024-03-25 19:52:28
...

背景

昨天我记录了安卓中相对布局的测量流程源码阅读,之后又读了一下线性布局LinearLayout的测量流程(onMeasure),但由于晚上突然来了个需求,文章记录就推迟到了现在。


onMeasure()

LinearLayout.onMeasure()代码如下

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

众所周知,线性布局的方向分为垂直和水平,两者分别对应measureVertical()方法和measureHorizontal()方法,两个方法思路一样,我就以垂直方向为例,阅读一下它的测量流程,主要解释都在代码中的注释里


measureVertical()

跟相对布局的onMeasure()方法阅读一样,我把measureVertical()的步骤分为了七步

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        /**
         * 步骤:
         *  1、初始化变量
         *  2、遍历所有子view,进行第一次测量,但这不一定能测量所有的
         *  3、更新布局的最大高度
         *  4、如果有子view没有被测量,或者还有剩余空间进行权重分配,就再对所有子view进行一次测量,同时更新布局的最大尺寸
         *     如果所有子view都被测量,也没法进行权重分配,但布局设置了"采用最大子view",并且高度不是精确模式,就把所有用权重要求的子view的高度,设置为最大子view的高度
         *     此时不用更新布局的高度,因为如果到了这种情况,布局最大高度已经 = 子view数目 * 最大子view高度 + 所有间距 + 分割线高度,不可能再高了
         *  5、更新布局最大宽度
         *  6、保存布局信息
         *  7、如果子view是match_parent,但当前布局不是精确模式,强制更新所有子view宽度为布局宽度,宽度模式是精确模式
         */
}

那我们就一步一步来吧


初始化变量

初始化一些用到的变量

        mTotalLength = 0; // 总长度
        int maxWidth = 0; // 所有子view的最大宽度
        int childState = 0; // 子view测量状态
        int alternativeMaxWidth = 0; // 没有权重需求的子view的最大宽度
        int weightedMaxWidth = 0; // 有权重子view的最大宽度
        boolean allFillParent = true; // 子view全都是fill_parent/match_parent
        float totalWeight = 0; // 子view权重之和

        final int count = getVirtualChildCount(); // 子view数量

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec); // 当前布局宽度测量模式
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec); // 当前布局高度测量模式

        boolean matchWidth = false; // 存在子view宽度是match_parent,但当前布局宽度不是精确模式
        boolean skippedMeasure = false; // 是否有子view因为某些原因跳过了测量

        final int baselineChildIndex = mBaselineAlignedChildIndex; // 做为基准线的子view,默认是-1
        final boolean useLargestChild = mUseLargestChild; // 默认为false

        int largestChildHeight = Integer.MIN_VALUE; // 最大子view高度
        int consumedExcessSpace = 0; // 可用来分配权重的剩余空间

        int nonSkippedChildCount = 0; // 已经测量过的子view数目

第一次遍历测量所有子view

这一个遍历代码非常长,140多行代码,我还是分几步来看


处理子view为null或gone的情况,并处理分割线

for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i); // 就是getChildAt(i)
            if (child == null) {
                mTotalLength += measureNullChild(i); // 0
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i); // 0
               continue;
            }

            nonSkippedChildCount++;
            if (hasDividerBeforeChildAt(i)) {
                // 如果这个子view之前有divider,就加上分割线的高度
                mTotalLength += mDividerHeight;
            }
            ....


处理子view高度和权重

            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight; // 累积子view的权重

            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            // 子view高度为0,但权重不是0
            // 说明高度尺寸优先级大于权重

            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // 当前布局既是精确测量,子view又是useExcessSpace,那就先不测量它,并且设定标志位

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); // 但总长度还是要更新
                skippedMeasure = true;
                // 就加上子view的上下外边距,并且设置标志位skippedMeasure为true

            } else {
                if (useExcessSpace) {
                    // 如果当前布局不是精确测量,子view又是useExcessSpace

                    lp.height = LayoutParams.WRAP_CONTENT;
                    // 暂时把参数的height设为内容包裹,以供measureChildBeforeLayout()方法调用
                }

                // 如果之前有子view有权重需求,就给所有的子view以最大高度,事后根据权重再压缩
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
                // 并没有用到参数i,调用viewGroup.measureChildWithMargins()

                final int childHeight = child.getMeasuredHeight();
                if (useExcessSpace) {

                    lp.height = 0; // 恢复子view的参数高度为0
                    consumedExcessSpace += childHeight; // 累加可以用来进行权重分配的空间
                }

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                       lp.bottomMargin + getNextLocationOffset(child));
                // 最后一个方法返回0

                if (useLargestChild) {
                    // 更新最大子view的高度
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }


更新基准线

            // 设置了基准线的话,更新基准线顶端位置
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight.  fail fast to aid the developer.
            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) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                matchWidth = true;
                matchWidthLocally = true; // 仅仅是match_parent,但当前布局不是精确模式,此时当前布局还不知道自己的宽度
            }

            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);
            }
            // 分情况保存当前的最大宽度

            i += getChildrenSkipCount(child, i); // 0


更新当前布局最大高度

        if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
            // 有子view被测量,并且在这个view之前有divider,就把分割线的高度加进总高度中
            mTotalLength += mDividerHeight;
        }

        // 如果设置了useLargetChild,就是以子view中最大为基准测量
        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); // 0
                    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)); // 此处是总高度+n*最大子view高度
            }
        }

        // 更新布局最大高度

        // Add in our padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // Check against our minimum height
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // Reconcile our calculated size with the heightMeasureSpec
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); // 更新当前布局的heightSpec
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;


剩余空间分配

这里分了两种情况,存在子view没有测量或有剩余空间的情况行进行权重分配和useLargestChild模式下的权重分配,但是都要进行剩余空间的计算

计算剩余空间

        // 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.

        // 如果有子view没有被测量,再根据剩余空间分配,或者根据权重分配子view

        int remainingExcess = heightSize - mTotalLength
                + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
        // sdk<=23取前者,否则取后者,计算剩余的可以进行权重分配的空间


第一种情况

        if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
            // 存在子view没有被测量(当前布局是精确模式,而且存在子view没有高度,只有权重),或者还有剩余空间来进行权重分配
            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                // 遍历所有子view
                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) {
                    // 如果当前子view有权重
                    final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                    // 分给他应该有的剩余空间
                    remainingExcess -= share; // 计算剩余的空间
                    remainingWeightSum -= childWeight; // 计算剩余的权重

                    final int childHeight;
                    if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                        // 用最大的子view分配高度
                        childHeight = largestChildHeight;
                    } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                            || heightMode == MeasureSpec.EXACTLY)) {
                        // 子view参数高度为0,并且sdk > 23 或 当前布局模式是精确模式

                        // This child needs to be laid out from scratch using
                        // only its share of excess space.
                        childHeight = share; // 那些只设置了权重,没有设置高度的子view,直接分配应该有的空间
                    } else {
                        // This child had some intrinsic height to which we
                        // need to add its share of excess space.

                        // 如果子view本身有高度,就在原有的基础上加上权重分配来的高度
                        childHeight = child.getMeasuredHeight() + share;
                    }

                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            Math.max(0, childHeight), MeasureSpec.EXACTLY); // 利用计算出来的childHeight计算子view高度信息

                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                            lp.width); // 利用当前布局宽度、间距、子view参数宽度计算子view的宽度信息

                    // 在这里,重新测量所有子view
                    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; // 子view宽度
                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; // 更新是否全部是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 {
            // 全部分配完毕,但又用了useLargestChild模式,就把有权重要求的子view的高度设为最大子view高度

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

这里如果是使用最大子view,当前布局的最大高度并没有更新,原因参见我最开始分步骤时的注释


保存最大宽度

       // 如果子view不都是fill_parent,就保存最大宽度
        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);


存在子view宽度match_parent时的处理

        if (matchWidth) {
            // 子view是match_parent,但当前布局宽度不是精确模式
            forceUniformWidth(count, heightMeasureSpec);
        }

     private void forceUniformWidth(int count, int heightMeasureSpec) {
        // Pretend that the linear layout has an exact size.
        int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
                MeasureSpec.EXACTLY); // 强制精确模式
        for (int i = 0; i< count; ++i) {
           final View child = getVirtualChildAt(i);
           if (child != null && child.getVisibility() != GONE) {
               LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());

               if (lp.width == LayoutParams.MATCH_PARENT) {
                   // Temporarily force children to reuse their old measured height
                   // FIXME: this may not be right for something like wrapping text?
                   int oldHeight = lp.height;
                   lp.height = child.getMeasuredHeight(); // 暂存参数中的height是子view的测量高度,确保高度不会因为measureChildWithMargins()而改变

                   // Remeasue with new dimensions
                   measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
                   // 更新子view宽度为当前布局宽度,模式是精确模式
                   lp.height = oldHeight;
               }
           }
        }
    }


总结

可以看到,线性布局在处理权重分配时耗了比较大的精力,所以我们要尽量避免权重的设置,而要尽量通过跟ui同事的协调来确定准确的dp宽度,从而提高测量效率

通过跟相对布局的比较,会发现相对布局是通过设置四个端点的坐标来确定子view和自身的尺寸,而线性布局是直接测量高度或宽度来确定子view和自身的尺寸。或许从源码上看,线性布局代码要少一些,但它的灵活性要逊于相对布局,甚至可能要使用很多属性或层次,反而降低了效率增大了开销,所以还是要具体情况具体分析,相对布局和线性布局结合起来用,方可相得益彰


安卓开发学习之LinearLayout的布局过程一文里,我将记录线性布局的onLayout()方法的阅读