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