【坑】RelativeLayout与wrap_content

在开发过程中,遇到了一个问题:将RelativeLayout的高度从固定值180dp,修改为wrap_content + minHeight之后,高度为match_parent的子View LinearLayout在测量后就会变成wrap_content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- item.xml -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="180dp"
android:background="?android:attr/selectableItemBackground">

<!-- 省略 -->
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_centerVertical="true"
android:layout_alignParentEnd="true"
android:gravity="center_vertical">
<!-- 省略 -->
</LinearLayout>
</RelativeLayout>

经过分析,原因如下:

因为该RelativeLayout是作为RecyclerViewitem被使用的,来看一下RecyclerViewgetChildMeasureSpec(int, int, int, boolean)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static int getChildMeasureSpec(int parentSize, int padding, int childDimension,
boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
if (canScroll) {
// MATCH_PARENT = -1
// WRAP_CONTENT = -2
// 大于等于0表示为该Item的Layout设置了固定的大小。
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else {
// MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
// instead using UNSPECIFIED.
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
// ... 省略
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

通过上述代码,可以看出,RecyclerView在measure item的时候,传递了一个由 size = 0, measureSpecMode = UNSPECIFIED 生成的measureSpec。接下来看一下RelativeLayoutonMeasure(int, int)方法:

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// ...省略
int myHeight = -1;
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// ...省略
if (heightMode != MeasureSpec.UNSPECIFIED) {
myHeight = heightSize;
}
// ... 省略
for (int i = 0; i < count; i++) {
final View child = views[i];
if (child.getVisibility() != GONE) {
// ...省略
// 此方法根据子View配置的垂直方向上的各种RelativeLayout rules进行计算
// 如果有rules,那么就会给RelativeLayout.LayoutParams的私有成员mTop/mBottom赋值。
// 垂直方向上的rules包括ALIGN_PARENT_BOTTOM、ABOVE、BELOW等。
// 从布局文件可知,此处不会给mTop/mBottom赋值。
applyVerticalSizeRules(params, myHeight, child.getBaseline());
measureChild(child, params, myWidth, myHeight);
// ...省略
}
// ...省略
}
// ...省略
}

结合RecyclerView代码可知,myHeight = -1。再看measureChild(View, LayoutParams , int, int)方法:

1
2
3
4
5
6
7
8
9
10
private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
// ...
// 从上述可知,params.mTop params.mBottom均没有设置。
int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
params.mBottom, params.height,
params.topMargin, params.bottomMargin,
mPaddingTop, mPaddingBottom,
myHeight);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

继续追踪getChildMeasureSpec(int, int, int, int, int, int, int, int)方法:

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
private int getChildMeasureSpec(int childStart, int childEnd,
int childSize, int startMargin, int endMargin, int startPadding,
int endPadding, int mySize) {
int childSpecMode = 0;
int childSpecSize = 0;
// Negative values in a mySize value in RelativeLayout
// measurement is code for, "we got an unspecified mode in the
// RelativeLayout's measure spec."
// 由上述分析可知mySize = -1,childSize = MATCH_PARENT = -1
final boolean isUnspecified = mySize < 0;
// mAllowBrokenMeasureSpecs是为了修复低版本bug引入的变量,只有在API <= 17时才为true
if (isUnspecified && !mAllowBrokenMeasureSpecs) {
// 此处似乎隐含修复bug的可能。只要保证子view的LayoutParams的mTop/mBottom被设置就行了。
if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
// Constraints fixed both edges, so child has an exact size.
childSpecSize = Math.max(0, childEnd - childStart);
childSpecMode = MeasureSpec.EXACTLY;
} else if (childSize >= 0) {
// The child specified an exact size.
childSpecSize = childSize;
childSpecMode = MeasureSpec.EXACTLY;
} else {
// Allow the child to be whatever size it wants.
// 最后走了这里
childSpecSize = 0;
childSpecMode = MeasureSpec.UNSPECIFIED;
}
return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
}
// ...
}

从上述分析,我们了解了产生bug的原因。那么是否有可能修复这个bug?
前面代码分析中,写了一句“此处似乎隐含修复bug的可能”,是否真的有可能呢?让我们重新分析一下applyVerticalSizeRules(LayoutParams, int, int)方法:

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
private void applyVerticalSizeRules(LayoutParams childParams, int myHeight, int myBaseline) {
final int[] rules = childParams.getRules();
// Baseline alignment overrides any explicitly specified top or bottom.
// 如果设置了baseLine则以baseLine为基础设置mTop/mBottom。
int baselineOffset = getRelatedViewBaselineOffset(rules);
if (baselineOffset != -1) {
if (myBaseline != -1) {
baselineOffset -= myBaseline;
}
childParams.mTop = baselineOffset;
// 设置baseLine不能改变mBottm的值,不符合条件。
childParams.mBottom = VALUE_NOT_SET;
return;
}

// 由布局文件可知该子View是顶部和头部是不存在其他View的,因此设置ABOVE/BELOW等属性均不可能。
RelativeLayout.LayoutParams anchorParams;
childParams.mTop = VALUE_NOT_SET;
childParams.mBottom = VALUE_NOT_SET;
anchorParams = getRelatedViewParams(rules, ABOVE);
if (anchorParams != null) {
childParams.mBottom = anchorParams.mTop - (anchorParams.topMargin +
childParams.bottomMargin);
} else if (childParams.alignWithParent && rules[ABOVE] != 0) {
if (myHeight >= 0) {
childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
}
}
anchorParams = getRelatedViewParams(rules, BELOW);
if (anchorParams != null) {
childParams.mTop = anchorParams.mBottom + (anchorParams.bottomMargin +
childParams.topMargin);
} else if (childParams.alignWithParent && rules[BELOW] != 0) {
childParams.mTop = mPaddingTop + childParams.topMargin;
}
anchorParams = getRelatedViewParams(rules, ALIGN_TOP);
if (anchorParams != null) {
childParams.mTop = anchorParams.mTop + childParams.topMargin;
} else if (childParams.alignWithParent && rules[ALIGN_TOP] != 0) {
childParams.mTop = mPaddingTop + childParams.topMargin;
}
anchorParams = getRelatedViewParams(rules, ALIGN_BOTTOM);
if (anchorParams != null) {
childParams.mBottom = anchorParams.mBottom - childParams.bottomMargin;
} else if (childParams.alignWithParent && rules[ALIGN_BOTTOM] != 0) {
if (myHeight >= 0) {
childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
}
}

// 设置ALIGN_PARENT_TOP可以使mTop得到值。
if (0 != rules[ALIGN_PARENT_TOP]) {
childParams.mTop = mPaddingTop + childParams.topMargin;
}
// 因为myHeight = -1,所以设置ALIGN_PARENT_BOTTOM不能使mBottom得到值。
if (0 != rules[ALIGN_PARENT_BOTTOM]) {
if (myHeight >= 0) {
childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
}
}
}
***

经过上述分析,在现有布局条件下,无法通过为子View增加一些rules来修复这个bug。只能通过布局调整来修复。

总结

这个bug实际上是普遍存在的。如果要求RecyclerView的itemView的子view的高度是match_parent,应该尽可能避免使用RelativeLayoutlayout_height="match_parent" minHeight="xxxdp"的写法,或者使用其他Layout。

说明

本文援引的源码如下:

RecyclerView

RelativeLayout