因为ScrollView 传递给ListView时,用的是 UNSPECIFIED
, ListView 设置了 heightSize
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
解决:
自定义ListView,在onMeasure()方法里重写 heightMeasureSpec
,让它进入到下面这个方法中:
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
ListView 源码分析
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = 0;
int childHeight = 0;
int childState = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height. measureScrapChild(child, 0, widthMeasureSpec, heightSize);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
UNSPECIFIED 模式下,heightSize 计算
1. ListView 默认高度模式 UNSPECIFIED
重写 ListView
,在 onMeasure()
里获取高度模式:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMode = MeasureSpec.getMode(heightMeasureSpec) // 0 UNSPECIFIED
}
得到0,可知 ScrollView
传递给 ListView
的高度模式为 UNSPECIFIED
。
因此它会执行 ListView.java
中的这段代码:
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
当 MeasureSpec
的模式为 UNSPECIFIED
时,这段代码的主要作用是给 ListView
提供一个合理的高度。
2. childHeight 为第一个Item的高度
在这个代码段中,childHeight
代表了 ListView
子项的高度。
通常情况下,当 ListView
开始测量时,它会测量第一个子项的高度,并将其作为 childHeight
。
这个测量过程会调用 ListAdapter
的 getView()
方法来获取子项的 View
,然后测量这个子项的高度。
View child = adapter.getView(0, null, this); // 获取第一个子项
child.measure(0, 0); // 测量子项的宽高
int childHeight = child.getMeasuredHeight(); // 获取测量后的高度
- 如果
ListView
有子项(即有内容),childHeight
是第一个可见子项的高度。 - 如果
ListView
没有子项(即内容为空),则childHeight
通常是0
,因为没有任何内容可供测量。
3. heightSize
-
heightSize
被设定为ListView
的上下内边距 (mListPadding.top
和mListPadding.bottom
) 加上childHeight
(第一个子项的高度),再加上视图淡出长度的两倍(即getVerticalFadingEdgeLength()
* 2)。 -
getVerticalFadingEdgeLength()
:这是ListView
的垂直淡出边缘长度,即列表项在滚动边缘时淡出的区域长度。乘以2
是因为考虑了上下两个边缘的淡出区域。
重写onMeasure
通过上面的步骤,我们知道了想要让ListView显示完全,就要修改高度模式为 AT_MOST,因此重写onMeasure(),并设置
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val heightMeasureSpec = MeasureSpec.makeMeasureSpec(Int.MAX_VALUE shr 2 , MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
1. 为什么 makeMeasureSpec ?
makeMeasureSpec()
是 View
系统中用于创建 MeasureSpec
的方法,它将大小 (size
) 和模式 (mode
) 组合成一个 32 位的 MeasureSpec
整数。
MeasureSpec
用于传递视图的测量需求,决定视图应该占用多大的空间。
makeMeasureSpec()
的作用是将测量模式和尺寸合并在一起,用于传递给 View
进行测量(onMeasure()
方法)。
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
在这个方法中,参数size 指定了范围,表示 size
的有效范围是 0 到 (1 << 30) - 1
,即 0 到 1073741823
1 的二进制为: 0000 0000 0000 0000 0000 0000 0000 0001
左移30位为: 0100 0000 0000 0000 0000 0000 0000 0000
结果: 1 * 2^30 = 1073741824
-1: 1073741823
(size & ~MODE_MASK) | (mode & MODE_MASK)
:
将 size
的低 30 位与 mode
的高 2 位合并在一起,构成完整的 MeasureSpec
值。
2. 为什么是 Int.MAX_VALUE shr 2 ?
shr
是 右移
运算符,相当于 >>
。
Int.MAX_VALUE shr 2
表示将 Int.MAX_VALUE
右移两位,即将其除以 4,得到一个较小的数。右移两位的结果为 536870911
,即 2147483647 / 4
。
Int.MAX_VALUE
0111 1111 1111 1111 1111 1111 1111 1111
最高位为0,表示正数。
= 2^31 -1 (相当于 1000 0000 0000 0000 0000 0000 0000 0000 - 1)
= 2147483647
显然超出了 size
的最大范围 1073741823
,因此要将其值缩小。
通过传递一个非常大的值(如 Int.MAX_VALUE shr 2
),实际上是告诉 ListView
你可以使用最多 536870911
的高度。这足够大,可以让 ListView
计算所有子项的高度并显示出来。
那么缩小到什么值合适呢?
在 onMeasure()
里有这样一段代码:
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
通过设定模式为 AT_MOST
后,会调用 measureHeightOfChildren()
测量 ListView
的子项高度,从而决定整个 ListView
的高度。
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
// ......省略其它代码
int returnedHeight = mListPadding.top + mListPadding.bottom;
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
if (i > 0) {
// Count the divider for all but one child
returnedHeight += dividerHeight;
}
returnedHeight += child.getMeasuredHeight();
if (returnedHeight >= maxHeight) {
// We went over, figure out which height to return. If returnedHeight > maxHeight,
// then the i'th position did not fit completely. return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
&& (i > disallowPartialChildPosition) // We've past the min pos
&& (prevHeightWithoutPartialChild > 0) // We have a prev height
&& (returnedHeight != maxHeight) // i'th child did not fit completely
? prevHeightWithoutPartialChild
: maxHeight;
}
if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
prevHeightWithoutPartialChild = returnedHeight;
}
}
// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight
return returnedHeight;
}
measureHeightOfChildren()
在测量每个子项时会不断累加其高度,并与 maxHeight
(即 Int.MAX_VALUE shr 2
)进行比较。
当累加的高度超过 maxHeight
时,测量过程会停止。
由于 536870911
是一个非常大的值,通常情况下 ListView
的总高度不会超过这个值,所以传递这个值实际上意味着允许 ListView
测量所有子项的高度而不受限制。
3. 为什么不使用 0 或较小的值?
如果传递 0
或较小的值作为 maxHeight
,意味着 ListView
在测量子项时一旦累加高度达到这个值,便会停止测量,这样 ListView
只会显示有限的子项。
例如:
- 传递
0
:ListView
会认为没有任何高度可用,因此可能完全不显示子项。 - 传递较小值:如
100
或200
,ListView
会在测量到超过100
或200
的高度时停止,无法显示所有的子项。
因此,为了确保 ListView
可以显示所有子项,使用一个足够大的 maxHeight
是必不可少的。
4. Int.MAX_VALUE shr 2 的作用
-
给
ListView
足够大的高度限制: 通过使用Int.MAX_VALUE shr 2
,给ListView
提供了一个非常大的maxHeight
,确保它能够自由测量所有子项的高度而不被过早限制。 -
防止溢出或异常: 通过将
Int.MAX_VALUE
右移两位,得到536870911
,避免了直接使用最大值可能带来的溢出或其他系统异常,同时这个值仍然足够大,可以满足实际测量需求。 -
确保子项完全显示: 这段代码有效地确保了
ListView
在ScrollView
或NestedScrollView
中时,能够测量出所有子项的高度并显示出来,而不会被默认的高度限制。