一. 开篇
在最开始接触 Android 开发的时候便学习了 LinearLayout 布局控件,它可以在垂直/水平方向依次展开 childView,再配合 weight 属性使用的话,可以高效、方便地完成许多 UI 界面的开发。其实 LinearLayout 还有一些其他用法,可能用的不多,可以参考这篇文章 你对LinearLayout到底有多少了解?(一)-属性篇
以前就知道,在 LinearLayout 布局时,如果不使用 weight 属性,LinearLayout 中每个 childView 只会测量一次,如果使用 weight 属性,每个 childView 会测量两次,分析了源码之后,发现这种说法也不是十分准确,childView 会不会被测量两次,除了依赖是否设置 android:layout_weight
属性,还需要依赖其他属性的
二. 源码解析
在 LinearLayout 中有垂直/水平两个方向的布局,任一方向的布局思想都是相同的,所以我们只需要具体分析其中一个方向即可,另一个方向可以类比,在这里我们分析垂直方向的思想
在 View 和 ViewGroup 中的布局有三大流程,分别是 onMeasure
、onLayout
和 onDraw
,在 LinearLayout 中 onLayout
和 onDraw
两个流程基本都是模板化的写法,而且 LinearLayout 布局简单,无论是垂直方向还是水平方向都是依次排列每个 childView 的,分析起来并不复杂,大家可以自行分析。
但是 onMeasure
流程就比较复杂,分为两种情况:
- 不使用
layout_weight
属性,每个 childView 按照自身的情况计算本身的大小即可 - 使用
layout_weight
属性,需要根据 LinearLayout 的剩余空间和layout_weight
的比例,计算每个 childView 的大小
Ok, let's fuck the source code
2.1 非 weight 的情况
2.1.1 布局文件 & 效果
首先,我们来看一个简单的布局,xml 文件如下所示
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:text="TextView1"
android:gravity="center"
android:textSize="24sp"
android:textColor="@android:color/white"
android:background="@android:color/holo_green_light"/>
<TextView
android:layout_width="match_parent"
android:layout_height="300dp"
android:text="TextView2"
android:gravity="center"
android:textSize="24sp"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
</LinearLayout>
其中,两个 TextView 都没有设置 layout_weight
属性,第一个 TextView 的 layout_height
属性是 200dp
,第二个 TextView 的 layout_height
是 300dp
,我想这样简单的布局只要稍微懂 Android 开发的人都知道是什么样的,它的效果如下图所示,但是说到它的源码执行,不知道又有多少人可以分析得清楚呢?
我们就以这个简单的示例,分析 LinearLayout 中的 onMeasure
流程
2.1.2 onMeasure()
执行流程
在测量阶段,也就是 onMeasure(int widthMeasureSpec, int heightMeasureSpec)
阶段,主要测量 LinearLayout 的整体大小,以及其中每个 childView 的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
源码如上所示,通过 mOrientation
分别处理垂直和水平两个方向的测量,其中的 mOrientation
变量则是我们在 xml 布局文件中通过 android:orientation="vertical"
或者直接通过 setOrientation(@OrientationMode int orientation)
方法设置的 LinearLayout 文件方向变量
我们仅分析垂直方向的测量方法,也就是 measureVertical(int widthMeasureSpec, int heightMeasureSpec)
(水平方向的测量方法 measureHorizontal(int widthMeasureSpec, int heightMeasureSpec)
是类似的原理,有兴趣的朋友可以自己分析)。measureVertical
方法还是很长的,不过整个过程可以分为三个阶段,为了分析的比较清楚,我们也分阶段循序渐进的分析
1. 声明变量
在 measureVertical
开始之前,需要初始化一些类变量 & 声明一些重要的局部变量,重要的变量我都有注释
其中,最重要的就是有三类:
- mTotalLength:所有 childView 的高度和 + 本身的 padding,注意:它和 LinearLayout 本身的高度是不同的
- 三个宽度相关的变量
- maxWidth:所有 childView 中宽度的最大值
- alternativeMaxWidth:所有 layout_weight <= 0 的 childView 中宽度的最大值
- weightedMaxWidth:所有 layout_weight >0 的 childView 中宽度的最大值
- totalWeight:所有 childView 的 weight 之和
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 一些重要的变量
mTotalLength = 0; // 所有 childView 的高度和 + 本身的 padding,注意:它和 LinearLayout 本身的高度是不同的
int maxWidth = 0; // 所有 childView 中宽度的最大值
int childState = 0;
int alternativeMaxWidth = 0; // 所有 layout_weight <= 0 的 childView 中宽度的最大值
int weightedMaxWidth = 0; // 所有 layout_weight >0 的 childView 中宽度的最大值
boolean allFillParent = true;
float totalWeight = 0; // 所有 childView 的 weight 之和
final int count = getChildCount();
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;
}
2. 测量第一阶段
在测量第一阶段会计算那些没有设置 weight 的 childView 的高度、计算 mTotleLength
,并且计算三个宽度相关的变量的值
在看下面代码之前,请想想我们上面提到的 xml 布局是什么样的,我们就按照上面的 xml 布局文件的样式进行分析。其中一些重要的英文注释,我并没有去掉,大家可以仔细思考这些英文注释,有助于理解
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 接上面的代码
// See how tall everyone is. Also remember max width.
// 第一次循环遍历,正如上面的英文注释所说明的意图所在
for (int i = 0; i < count; ++i) {
// 依次得到每一个 childView
// { 在此 xml 布局中,会依次得到 TextView1 & TextView2 }
final View child = getChildAt(index);
// { 在此 xml 布局中的 TextView1 & TextView2 都不满足下面的两个条件 }
if (child == null) {
continue;
}
if (child.getVisibility() == View.GONE) {
continue;
}
// 没有跳过的 childView 个数
// { 在此 xml 布局中,nonSkippedChildCount 最终为 2 }
nonSkippedChildCount++;
// 在总高度中加上每一个 Divider 的 height
// { 在此 xml 布局中,没有设置 `android:divider` 相关属性,跳过此 if 判断 }
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 计算总权重 totalWeight
// { 在此 xml 布局中,两个 childView 都没有设置 `android:layout_weight` 属性,
// 所以 totalWeight 一直为 0}
totalWeight += lp.weight;
// { 在此 xml 布局中,useExcessSpace 为 false }
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 符合这种条件的 childView 先跳过测量,在这里不做测量计算
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// 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.
lp.height = LayoutParams.WRAP_CONTENT;
}
// 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).
// 这是非常重要的一个方法,将会决定每个 childView 的大小
// 如果此 childView 及在此 childView 之前的 childView 中使用了 weight 属性,
// 我们允许此 childView 使用所有的空间(后续如果需要,再做调整)
// { 在此 xml 布局中,在调用时 usedHeight 都是 mTotalLength }
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
// 得到测量之后的 childView 的 childHeight
// { 在此 xml 中,TextView1 的 childHeight 是 200 dp;
// TextView2 的 childHeight 是 300 dp }
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.
lp.height = 0;
consumedExcessSpace += childHeight;
}
// 将此 childView 的 childHeight 加入到 mTotalLength 中
// 并加上 childView 的 topMargin 和 bottomMargin
// getNextLocationOffset 方法返回 0,方便以后扩展使用
// { 在此 xml 中,mTotalLength 最后的结果将是 500 dp }
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
// 下面两个 if 判断都和 `android:baselineAlignedChildIndex` 属性有关
// 在这里不做分析
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
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;
// { 在此 xml 中,`android: layout_width` 是 `match_parent`,
// 所以 widthMode 是 `MeasureSpec.EXACTLY`,不会进入此 if 判断 }
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;
}
// 计算三个和宽度相关的变量值
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) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
// { 在此 xml 布局中,最终都会走到此 代码块 中,matchWidthLocally == false }
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
// 如果存在没有跳过的 childView 并且需要绘制 end divider 则需要加上 end 位置的 divider 的高度
// { 在此 xml 中,没有设置 android:showDividers="end",跳过此 if 代码块 }
if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
......
}
在上面的代码中,我都做了详细的注释,其中有一个方法调用非常重要,即 measureChildBeforeLayout()
方法,在此方法中将会计算每个 childView 的大小
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
在 measureChildBeforeLayout()
方法中,又调用 ViewGroup
的 measureChildWithMargins()
方法计算每个 childView 的大小,在测量垂直方向的 childView 时,有一个非常重要的参数需要注意,即:heightUsed
,根据英文注释,heightUsed
是指在垂直方向,已经被 parentView 或者 parentView 的其他 childView 使用了的空间
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
那么在上面示例的 xml 布局测量过程中 heightUsed
的值是多少呢?
- 在测量
TextView1
时heightUsed
是0
,因为是第一个测量的 childView,在垂直方向的空间还没有被使用 - 在测量
TextView2
时heightUsed
是200 dp
,因为TextView1
已经使用了200 dp
3. 测量第二阶段
如果进入这个 if 条件,会进行第二次的 for 循环遍历 childView,重新计算 mTotalLength
。不过这个 if 条件需要 useLargestChild
为 true
,useLargestChild
可以通过 xml 属性 android:measureWithLargestChild
设置的,不在本文的讨论范围内
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 接上面的代码
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));
}
}
......
}
4. 测量第三阶段
经过上面的分析之后,终于来到了最后的一个阶段,在这里会针对设置了 android:layout_weight
属性的布局,重新计算 mTotalLength
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 接上面的代码
// 加上 LinearLayout 自己的 paddingTop 和 paddingBottom
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// 通过 getSuggestedMinimumHeight() 得到建议最小高度,并和计算得到的
// mTotalLength 比较取最大值
// { 在此 xml 布局中,并没有设置 minHeight 和 background,所以还是取 mTotalHeight 值}
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// 通过 heightMeasureSpec,调整 heightSize 的大小,具体的过程需要
// 看一下 resolveSizeAndState() 方法的实现
// {在此 xml 布局中,heightSize 经过调整之后就是 LinearLayout 的大小了,
// 也就是整个屏幕的高度了 }
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// 重新计算有 weight 属性的 childView 大小,
// 如果还有可用的空间,则扩展 childView,计算其大小
// 如果 childView 超出了 LinearLayout 的边界,则收缩 childView
// { 在此 xml 布局中,不会进入此 if 语句,直接走 else 代码块了,
// 因为不符合条件,skippedMeasure == false,totalWeight == 0 }
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
......
} else {
// 重新计算 alternativeMaxWidth
alternativeMaxWidth = Math.max(alternativeMaxWidth,
weightedMaxWidth);
// useLargestChild 为 false,不在本文讨论范围内
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;
// 调整 width 大小
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 调用 setMeasuredDimension() 设置 LinearLayout 的大小
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
经过上面四步的源码分析,非 weight 情况下的垂直布局 onMeasure() 代码就分析的差不多了。在不使用 android:layout_weight
属性时,LinearLayout 的 onMeasure
流程还是比较简单的,只会进入第一个 for 循环遍历所有的 childView 并计算他们的大小,如果使用了 android:layout_weight
属性则会进入第三个 for 循环并再次遍历所有的 childView,再次重新执行 childView 的 measure()
方法
2.2 使用 weight 的情况
2.2.1 布局文件 & 效果
上面分析了不使用 android:layout_weight
的情况,现在来分析下使用 android:layout_weight
的情况,还是通过一个例子入手,xml 布局如下所示
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:showDividers="end"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:text="TextView1"
android:gravity="center"
android:textSize="24sp"
android:layout_weight="2"
android:textColor="@android:color/white"
android:background="@android:color/holo_green_light"/>
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:text="TextView2"
android:gravity="center"
android:textSize="24sp"
android:layout_weight="3"
android:textColor="@android:color/white"
android:background="@android:color/holo_blue_light"/>
</LinearLayout>
这也是一个我们最常见的 LinearLayout 的用法,TextView1 的 android:layout_height="0dp"
且 android:layout_weight="2"
,TextView2 的 android:layout_height="0dp"
且 android:layout_weight="3"
,如下图所示,TextView1 和 TextView2 在垂直方向上,会以 2 : 3
的比例分配整个屏幕的高度
1. 声明变量
还是和上面同样的思路分析 onMeasure 的流程,由于声明的变量没有区别,我们直接跳过声明变量,从测量第一阶段开始
2. 测量第一阶段
如果是上面这种布局的 xml 代码,在第一次 for 循环遍历 childView 时,会标记 skippedMeasure = true
,并计算所有的 totalWeight
,在第二次 for 循环遍历时,重新计算每个有 weight 属性的 childView 的大小
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 接上面的变量声明
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
// 遍历每个 childView,如果满足下面两个 if 条件之一,则跳过
// { 在此 xml 布局中,两个 TextView 都不会跳过 }
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
// 没有跳过的 childView 个数
// { 在此 xml 布局中,nonSkippedChildCount 最终为 2 }
nonSkippedChildCount++;
// 在总高度中加上每一个 Divider 的 height
// { 在此 xml 布局中,没有设置 `android:divider` 相关属性,跳过此 if 判断 }
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 不同的地方开始了
// 计算总权重 totalWeight
// { 在此 xml 布局中,TextView1 的 weight == 2,TextView2 的 weight == 3
// 所以最终 totalWeight == 5 }
totalWeight += lp.weight;
// {在此 xml 布局中,遍历 TextView1 和 TextView2 时,useExcessSpace 均为 true,
// 并且满足下面的 if 条件判断,skippedMeasure 赋值为 true }
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// 符合这种条件的 childView 先跳过这个循环测量,将 skippedMeasure 赋值为 true,
// 在后面第三个 for 循环重新计算此 childView 大小
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
......
}
// 下面两个 if 判断都和 `android:baselineAlignedChildIndex` 属性有关
// 在这里不做分析
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
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;
}
final int margin = lp.leftMargin + lp.rightMargin;
// { 在此 xml 布局中,直到这里都还没有测量 childView,所以
// child.getMeasuredWidth() == 0}
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
// { 在此 xml 布局中,weightedMaxWidth 一直为 0 }
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
// { 和上面的作用一样,在计算高度时,计算 endDivider 的高度 }
if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
......
}
3. 测量第二阶段
第二阶段的测量和上面提到的一样,都是和 android:measureWithLargestChild
属性设置相关的,不在本文的讨论范围之内
4. 测量第三阶段
在第三阶段的测量之中,针对设置了 android:layout_weight
属性的布局,重新计算 mTotalLength
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// 接上面的代码
// 加上 LinearLayout 自己的 paddingTop 和 paddingBottom
mTotalLength += mPaddingTop + mPaddingBottom;
// { 在此 xml 布局中,经过上面的两次 for 循环之后 mTotalLength == 0 }
int heightSize = mTotalLength;
// 通过 getSuggestedMinimumHeight() 得到建议最小高度,并和计算得到的
// mTotalLength 比较取最大值
// { 在此 xml 布局中,并没有设置 minHeight 和 background,所以还是取 mTotalHeight 值,
// 所以 heightSize == 0 }
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// 通过 heightMeasureSpec,调整 heightSize 的大小,具体的过程需要
// 看一下 resolveSizeAndState() 方法的实现
// { 在此 xml 布局中,heightSize 经过调整之后就是 LinearLayout 的大小了,
// 也就是整个屏幕的高度了 }
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
// 重新计算有 weight 属性的 childView 大小,
// 如果还有可用的空间,则扩展 childView,计算其大小
// 如果 childView 超出了 LinearLayout 的边界,则收缩 childView
// { 在此 xml 布局中,经过上面的第一次 for 循环之后 skippedMeasure == true,
// remainingExcess == 整个屏幕的高度,totalWeight == 5 }
int remainingExcess = heightSize - mTotalLength
+ (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
// 根据 mWeightSum 计算得到 remainingWeightSum,mWeightSum 是通过
// `android:weightSum` 属性设置的,totalWeight 是通过第一次 for 循环计算得到的
float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
// 将 mTotalLength 复位为 0
mTotalLength = 0;
// 开始真正的第二次 for 循环遍历每一个 childView,重新测量每一个 childView
for (int i = 0; i < count; ++i) {
// 得到每一个 childView,如果符合下面的 if 判断则跳过
final View child = getVirtualChildAt(i);
if (child == null || child.getVisibility() == View.GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final float childWeight = lp.weight;
// 如果该 childView 设置了 `weight` 值,则进入 if 语句块
// { 在此 xml 布局中,TextView1 的 layout_weight == 2,
// TextView2 的 layout_weight == 3,都会进入下面的 if 条件判断 }
if (childWeight > 0) {
// 这是设置了 weight 的情况下,最重要的一行代码
// remainingExcess 剩余高度 * ( childView 的 weight / remainingWeightSum)
// share 便是此 childView 通过这个公式计算得到的高度,
// 并重新计算剩余高度 remainingExcess 和剩余权重总和 remainingWeightSum
final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
remainingExcess -= share;
remainingWeightSum -= childWeight;
// 通过下面的 if 条件重新计算,childHeight 是最终 childView 的真正高度
// { 在此 xml 布局中,TextView1 和 TextView2 都会走到第二个条件中去,
// childHeight == share }
final int childHeight;
if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
childHeight = largestChildHeight;
} else if (lp.height == 0 && (!mAllowInconsistentMeasurement
|| heightMode == MeasureSpec.EXACTLY)) {
// This child needs to be laid out from scratch using
// only its share of excess space.
childHeight = share;
} else {
// This child had some intrinsic height to which we
// need to add its share of excess space.
childHeight = child.getMeasuredHeight() + share;
}
// 计算 childHeightMeasureSpec & childWidthMeasureSpec,并调用 child.measure() 方法
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));
}
// 重新计算 maxWidth & alternativeMaxWidth
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;
// 考虑 childView.topMargin & childView.bottomMargin,重新计算 mTotalLength
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// 完成 for 循环之后,加入 LinearLayout 本身的 mPaddingTop & mPaddingBottom
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
......
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
maxWidth += mPaddingLeft + mPaddingRight;
// 调整 width 大小
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// 调用 setMeasuredDimension() 设置 LinearLayout 的大小
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
三. 小结
经过上面对两种情况的分析,其实 onMeasure
流程已经比较清晰了,简单总结一下,我们可以学习到以下几点
- LinearLayout 的设计者有意的对设置了
weight
和不设置weight
的情况分别处理,通过skippedMeasure
变量 &childView.height
&childView.weight
区分,从上面我举的两个例子中就可以明显的感受到,两种测量流程分的还是比较详细清楚的 - 在 LinearLayout 中总共有 3 个 for 循环,分别处理不同的流程
- 第一个 for 循环,只会在不使用
weight
属性时进入,并有可能会测量每个 childView 的大小 - 第二个 for 循环,在使用
android:measureWithLargestChild
时才会进入,并且即使进入也不会调用 childView 的测量方法,只会更新mTotalLength
变量 - 第三个 for 循环,只会在使用
weight
属性时进入,并测量每个 childView 的大小
- 第一个 for 循环,只会在不使用
- 通过上面的分析,即使是使用了
android:layout_weight
属性,childView 也不会一定就测量两次,还需要看android:layout_height
和 LinearLayout 的heightMode
属性 - 通过上面的源码分析,熟悉巩固了
measureChildWithMargins(...)
、resolveSizeAndState(...)
、getChildMeasureSpec(...)
、setMeasuredDimension(...)
等 Api,这些 Api 对于我们自定义控件还是非常重要的
网友评论