原因很简单,在设置 RelativeLayout 的子View高度时,若高度不设置成一个固定值,在测量的过程中,则会设置成0。
为什么呢?
我们可以从以下代码进行分析:
首先来看一下RecyclerView的getChildMeasureSpec()方法源码:
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 在测量 itemView 的时候,返回给 RelativeLayout 一个由 size = 0,measureSpecMode = UNSPECIFIED 生成的 measureSpec。
我们再来看 RelativeLayout 中的代码,先来看 onMeasure(int,int) 方法的源码:
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 的 getChildMeasureSpec() 方法可知 myHeight = -1,且 params.Top 及 params.Bottom 都没有赋值,并将这些参数传入 measureChild() 方法中,那么我们追踪到 measureChiid() 方法中来看看:
private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
// ...
int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
params.mBottom, params.height,
params.topMargin, params.bottomMargin,
mPaddingTop, mPaddingBottom,
myHeight);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
该方法将这些参数传入了 RelativeLayout 的 getChildMeasureSpec() 方法中,那么继续追踪到 getChildMeasureSpec() 方法的源码:
private int (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) {
// 当子View设置了指定大小时
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);
}
// ...
}
最后可知,当 RecyclerView 的子 View 未设置指定高度时,高度便为0,并指定了子 View 的 mode 为 MeasureSpec.UNSPECIFIED。
上述代码也说过,有一个修复该bug的可能,那么我们重新分析一下,看是否真的可以,我们来查看 applyVerticalSizeRules() 方法;
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。只能通过布局调整来修复。
总结:
若要求RecyclerVIew的子View的高度为match_parent,应避免使用RelativeLayout 。