前言
对于中高级的工程师来说,有一定的阅读源码的经验已经必备技能之一了,尤其是 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 的