线性布局和相对布局的实现原理

前言

对于中高级的工程师来说,有一定的阅读源码的经验已经必备技能之一了,尤其是 LinearLayout 和 RelativeLayout,我们先准备一下要掌握的知识点:

  • LinearLayout 的 Weight 是如何实现的?
  • LinearLayout 的 Weight 对于测量过程增加了哪些实现成本?
  • LinearLayout 的 Weight 真的会导致需要测量两次吗?
  • RelativeLayout 的 Measure 过程为什么会有两次?
  • RelativeLayout 的 Padding 和 Margin 对于居中属性为什么没有影响?
  • RelativeLayout 在版本 4.2,4.3 和 4.4 做了哪些调整和适配?

还有很多其他的问题困扰着我们,很多技术分享文章都只是讲大概的流程,被问到细节的时候,我们还是会一脸懵逼,不知所措。

这一篇 Chat 我们来详细的分析一下 LinearLayout 和 RelativeLayout 的实现原理,是一句一句的分析,真的十分详细的分析。

LinearLayout

LinearLayout 的特性是线性布局,weight 属性是他的专属利器,可以实现比例布局。LinearLayout 可以设置横向或者是竖向,这里以竖向布局作为分析的目标。

onMeasure

首先我们看一下 LinearLayout 的 onMeasure 方法:

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

非常的简单,根据设置的方向调用指定方向的测量方法,因为我们这次分析的目标是竖向,所以看 measureVertical 方法。其实水平方法的测量原理和竖向是一样的。

    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        // 计算的总高度
        mTotalLength = 0;
        // 最大宽度
        int maxWidth = 0;
        int childState = 0;
        // 记录没有使用weight的child最大宽度
        int alternativeMaxWidth = 0;
        // 记录使用了weight的child的最大宽度
        int weightedMaxWidth = 0;
        // 是否所有的child的宽度都是match_parent
        boolean allFillParent = true;
        // 计算child的总weight
        float totalWeight = 0;
        // 子View的数量
        final int count = getVirtualChildCount();
        // 自身宽度的测量模式
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        // 自身高度的测量模式
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        // 子View是否需要和parent的宽度一致
        boolean matchWidth = false;
        // 是否要跳过measure过程
        boolean skippedMeasure = false;
        // 基线的child的位置
        final int baselineChildIndex = mBaselineAlignedChildIndex;
        // 是否使用最大的child的高度
        final boolean useLargestChild = mUseLargestChild;
        // 上一个child的高度
        int largestChildHeight = Integer.MIN_VALUE;
        // 记录消耗的扩展的空间,给weight服务
        int consumedExcessSpace = 0;
        // 记录总共测量了child的个数
        int nonSkippedChildCount = 0;
        ...
}

打开 measureVertical 方法,一上来就是一堆临时变量,我们先记住他们的作用,便于之后的分析。

然后开始第一次的 for 循环:

    // 开始第一次的循环操作
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);
            // 如果child是空的,跳过,measureNullChild等于0
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }
            // 如果child是隐藏状态,跳过
            if (child.getVisibility() == View.GONE) {
                i += getChildrenSkipCount(child, i);
                continue;
            }
            // 测量的child的个数+1
            nonSkippedChildCount++;
            // 如果在child之前需要绘制divider,加上divider的宽度
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerHeight;
            }
            // 得到child的布局信息
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            // 计算总weight
            totalWeight += lp.weight;
            // 这里判断是否使用weight
            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
            // 如果parent高度是是match_parent或者固定值,child使用了weight
            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                // 记录当前的高度最大值
                final int totalLength = mTotalLength;
                // 总高度加上子View的上边距和下边距,和目前的高度对比取最大值
                // 这里并没有加child的高度,会在之后再次计算weight的占比的时候设置
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                // 有child跳过了测量过程,之后会进入到weight的测量过程
                skippedMeasure = true;
            } else {
                // 如果使用了weight,先把child的高度设置成wrap_content
                // 先测量child的最小高度
                if (useExcessSpace) {
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

                // 如果目前还没child使用weight,高度使用mTotalLength,否则使用0
                // usedHeight表示已使用的高度,之后parent的测量高度会减去这个高度,作为child的高度最大值限制
                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                // 测量child
                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);
                // 得到child的高度
                final int childHeight = child.getMeasuredHeight();
                // 如果child使用了weight
                if (useExcessSpace) {
                    lp.height = 0;
                    // 增加要扩展的空间
                    consumedExcessSpace += childHeight;
                }
                // 计算总高度
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                        lp.bottomMargin + getNextLocationOffset(child));
                // 如果使用了最大child高度,和之前测量的child的高度对比,取最大值
                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }
            ...
    }

上面的代码主要做了两个工作:

  • 如果 LinearLayout 的高度是 match_parent 或者固定值,child 使用了 weight,直接跳过了 child 的测量过程。

  • 如果 LinearLayout 的高度是 wrap_content 或者不受限制,如果 child 使用了 weight,先把 child 的高度设置成 wrap_content,然后进行测量,这样可以知道 child 需要的最小高度。

除此之后还记录了没有使用 weight 的 child 的总高度,这是确定 LinearLayout 的最小高度。还记录了最大 child 的高度,useLargestChild 的作用之后我们就知道了。

看到这里大家还记得一个黄色警告吗:

如果使用了 weight,推荐设置 width 或者 height 等于 0,而不是 wrap_content

从代码上看,如果 height 是 wrap_content,而 weight 不等于 0,仍然会测量这个 child 的高度,这会造成性能的浪费,继续分析我们会发现这次测量是无意义的,这里先记住这个问题。

    for (int i = 0; i < count; ++i) {
            ...
            // 判断下一个要测量的child,是不是我们的baseline,记录当前的总高度,作为基线的top
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
                mBaselineChildTop = mTotalLength;
            }

            // 在基线child之前的View,不可以使用weight
            // 要么去掉weight,要么不要设置基线child
            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;
            // 计算宽度,如果自己是wrap_content,child是match_parent
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // 需要设置child的宽度和parent一致
                matchWidth = true;
                matchWidthLocally = true;
            }
            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            // 计算最大宽度,看看child的宽度是否比自己的宽度要大,取最大值
            maxWidth = Math.max(maxWidth, measuredWidth);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            // 是否所有的child都是match_parent
            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                // child使用了weight,
                // 如果自己是wrap_content,child是match_parent,用margin与当前记录的最大宽度对比
                // 也就是说宽度至少要满足child的边距
                // 否则判断这个child的测量宽度是否是最大的
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                // 如果自己是wrap_content,child是match_parent,用margin与当前记录的最大宽度对比
                // 否则判断这个child的测量宽度是否是最大的
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }
            // 加上要跳过的child的个数,可以忽略,默认是0
            i += getChildrenSkipCount(child, i);
        }

在测量完没有使用 weight 的 child 之后,重点的操作是,判断 child 的最大宽度,记录分为两个:

使用了 weight 的 child:用 child 左右 margin 得到最小的宽度,这个 child 还没有测量,宽度也是 0。

没使用 weight 的 child:margin + 测量宽度。

第一次 for 循环结束,我们总结一下,LinearLayout 都做了哪些工作:

1、测量了没有使用 weight 的 child 的宽度和高度。
2、确定了 child 的最大宽度,这个是为了确定自己的宽度使用的。

接下来继续分析:

    // 第一次的测量结束,如果测量的child的个数大于1,判断在底部是否还要绘制divider
        if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
            mTotalLength += mDividerHeight;
        }

        // 如果使用最大的child的高度
        // 并且height是wrap_content或者不受限制
        if (useLargestChild &&
                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
            mTotalLength = 0;
            // 再次循环测量
            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);
                // 跳过空child
                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }
                // 跳过没有显示的child
                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;
                // 重新计算总高度
                // 只计算最大的child的高度 + 所有的上边距 + 所有的下边距 + 加上所有child的偏移值
                // 请注意,这里没有加上除最大child外,其他child的高度
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }
        }

上面的判断只有在我们使用 useLargestChild 属性或者高度是 wrap_content 后者高度不受限的时候才会进入,这里我们就可以知道 useLargestChild 的作用了:

所有的 child 的高度都是最高的 child 的高度(不包含 weight 的 child 的高度)

所以这里计算总高度的时候,并没有使用 child 的高度,而是直接粗暴的使用之前判断的所有 child 中的最大高度。继续往下看:

    // 添加padding
        mTotalLength += mPaddingTop + mPaddingBottom;

        int heightSize = mTotalLength;

        // 对比计算的总高度和推荐的最小高度,取最大值
        // 推荐最小高度:背景的最小高度和minHeight的最大值
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

        // 这里对比了计算的高度,测量的高度,取最大值
        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
        // 计算剩余高度
        // 这里做了适配,6.0以下true,6.0以上false
        // 可见在6.0以下child最小宽度(wrap_content)是没有保证的
        int remainingExcess = heightSize - mTotalLength
                + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);

上面的代码确定了 LinearLayout 的高度,然后判断是否还有剩余的空间:

heightSize 是最终确定的高度;
mTotalLength 是计算的高度,但是不包含使用了 weight 的 child 的高度;
consumedExcessSpace:使用了 weight 的 child 的 wrap_content 测量下的高度的总和。

这里做了6.0版本的适配:

只有在 LinearLayout 的高度是 wrap_content 或者不受限,其实这里才有差别;因为在 LinearLayout 的高度是 wrap_content 时,child 使用了 weight,才会计算 consumedExcessSpace。

6.0 以下:剩余空间的计算没有减去使用了 weight 的 child 的 wrap_content 高度。

6.0 以上:剩余空间的计算减去使用了 weight 的 child 的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值