LinearLayout
是最常用的布局之一,但某些属性很令人费解。为了更加清楚的认识 LinearLayout
,也为了加深对 ViewGroup
测量过程的理解,决定从源码角度研究一下测量过程。
分析使用版本 6.0.1 r22
源码地址LinearLayout
onMeasure
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
onMeasure()
只是根据 LinearLayout
的方向调用不同的测量方法。下面以 measureVertical()
为例进行分析。
measureVertical
1 | void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { |
第一次测量过程
测量所有子视图的高度,并且记录宽度相关的测量数据。关于宽度测量,详见分析六。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
// 测量为 null 的子视图的高度,目前返回 0,用于以后扩展
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
// getChildrenSkipCount() 计算跳过的子视图个数,返回 0
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
// 将分割线的高度计算在 mTotalLength 内
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// 满足该条件的话,不需要现在计算该子视图的高度。测量工作会在之后进行
// 为什么满足这三个条件的不用计算高度?见下文分析一
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
int oldHeight = Integer.MIN_VALUE;
if (lp.height == 0 && lp.weight > 0) {
// 如果 LiniearLayout 的高度需要根据子视图来测量,为了测量子视图的高度,设置子视图 LayoutParams.height 为 wrap_content。
// heightMode is either UNSPECIFIED or AT_MOST, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
oldHeight = 0;
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).
// 只是调用了 ViewGroup 的 measureChildWithMargins() 对子视图进行测量
// 第四个参数表示当前已使用的宽度,因为是纵向 LinearLayout ,所以一直为 0 。
// 第六个参数表示已使用的高度。如果之前子视图或者当前的子视图有 weight 属性,就允许当前子视图使用 LinearLayout 的所有高度,即已使用的高度为 0 。
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
if (oldHeight != Integer.MIN_VALUE) {
// 测量完成之后,重新设置 LayoutParams.height
lp.height = oldHeight;
}
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
// 重新计算 mTotalLength
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
// 设置最高子视图大小
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
// mBaselineChildTop 表示指定的 baseline 的子视图的顶部高度
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}
// 设置为 baseline 的子视图的前面不允许设置 weiget 属性
// 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;
}
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 {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
分析一:为什么不用计算某些子视图高度?
- 子视图的
height
为 0,weight
不为 0,说明该视图仅仅希望使用LinearLayout
的剩余空间。 LinearLayout
的高度测量规格为Exactly
,说明LinearLayout
的高度已经确定,不依赖子视图的高度计算自己的高度。相反,如果测量规格为AT_MOST
或者UNSPECIFIED
,LinearLayout
只能根据子视图的高度来确定自己的高度,就必须对所有的子视图进行测量。
重新计算 mTotalLength
1 | for (int i = 0; i < count; ++i) { |
分析二:如何计算 mTotalLength
?
mTotalLength
为已测量的子视图的总高度。如果设置了 android:measureWithLargestChild="true"
,每个子视图的高度为:最大子视图高度 + 该子视图的上下外边距。
注意,重新计算
mTotalLength
时,没有将 间隔线的高度 计算在内。造成设置android:measureWithLargestChild="true"
且显示间隔线时,LinearLayout
高度计算会出现问题。
为什么会出现这个明显的问题呢?难道设计时,这两个属性就不会同时出现?
分析三:measureWithLargestChild
如何影响 LinearLayout
高度的测量?
首先来看对 LinearLayout
高度测量的影响。
根据源码可知:
LinearLayout
的测量高度保存在heightSizeAndState
。heightSizeAndState
的值由mTotalLength
和getSuggestedMinimumHeight()
resolveSizeAndState
方法决定。- 重新测量
mTotalLength
的触发条件为:useLargestChild && heightMode != Exactly
考虑以下布局1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:measureWithLargestChild="true"
android:background="@android:color/darker_gray"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_red_dark"
android:textSize="15sp"
android:text="TextView1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_red_light"
android:textSize="20sp"
android:text="TextView2"/>
</LinearLayout>
预览效果如下:
可以看出, LinearLayout
的高度比子视图总高度多出一部分。
测量过程分析:
- 第一次测量时,两个子视图都会进行测量,并记录
mTotalLength
。 - 重新计算
mTotalLength
= 子视图个数 * 子视图最大高度 。 - 重新测量子视图大小时,执行
else
分支。但是,子视图没有weight
属性,不会再次测量,子视图维持之前的测量大小。 LinearLayout
的高度使用了 子视图最大高度 ,但是子视图没有进行拉伸,造成空间剩余。
重新测量子视图大小
根据 weight 分配 LinearLayout
的剩余高度。LinearLayout
的剩余高度可能为负值。见下文分析三。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
// 注意,delta 可能小于零
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
mTotalLength = 0;
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
float childExtra = lp.weight;
if (childExtra > 0) {
// Child said it could absorb extra space -- give him his share
// 计算 weight 属性分配的大小,可能为负值
int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// 子视图已经测量过
// child was measured once already above...
// base new measurement on stored values
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// 子视图第一次测量
// child was skipped in the loop above.
// Measure for this first time here
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}
// 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;
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;
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
} else {
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));
}
}
}
}
分析四:delta 为负值时的情况?
考虑如下布局1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@android:color/darker_gray"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/holo_red_dark"
android:text="TextView1"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="2"
android:background="@android:color/holo_red_light"
android:text="TextView2"/>
</LinearLayout>
预览效果如下:
结论:TextView1 与 TextView2 的高度比为 2 : 1,即 TextView1 的高度为 200 dp,TextView2 的高度为 100 dp。
测量过程分析:
- 第一次测量时,两个子视图都会进行测量。此时,TextView1 的高度 = TextView2 的高度 = 300 dp ,mToatalLength 为测量的子视图高度之和,即 600 dp。
- 跳过重新计算 mTotalLength 阶段。
- 重新测量子视图大小时,delta = -300 dp。
- 重新分配剩余空间。TextView1 的高度为 300 + 1/3 * (-300 ) = 200 dp,同理可计算 TextView2 的高度为 100 dp。
分析五:measureWithLargestChild
如何影响子视图高度的测量?
根据分析三可知,如果 TextView1 设置了 weight
属性,它的高度就会被重新测量,测量高度为子视图最大高度,测量规格为 EXACTLY
。
由分析三和分析五可知:在高度测量规格不是
Exactly
的前提下,子视图不设置layout_weight
属性,LinearLayout
设置measureWithLargestChild
属性,仅仅会影响LinearLayout
的高度测量。同时设置这两个属性,才会拉伸子视图至子视图最大高度。
另外一个问题,如果 TextView2 也设置了 weight
属性呢?
答案是对效果没有影响,但是在测量最后一步, TextView2
进行了一次多余测量。因为子视图最大高度由 TextView2
的高度决定,再次使用子视图最大高度对 TextView2
进行测量,测量结果与之前一致。
分析六:宽度测量过程
仅分析测量规格不为 Exactly
的情况。
- 如果子视图的布局参数均为
match_parent
,LinearLayout
的宽度由子视图最大的宽度决定。 - 如果子视图的布局参数不全是
match_parent
,LinearLayout
宽度由非match_parent
的子视图宽度决定。