1. ScrollView嵌套ListView,无论有多少个item都只会显示一个,为什么?
明明有多个item,但只显示一个,猜测view的测量出了问题。
1.1 查看ListView的onMeasure方法源码
查看ListView的onMeasure方法源码, 发现一段疑似问题所在的代码:
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
复制代码
这里的childHeight
就是一个item的高度。
那么什么时候走这个代码逻辑呢,也就是当heightMode
为MeasureSpec.UNSPECIFIED
的时候走这里的代码逻辑。
什么时候heightMode
为MeasureSpec.UNSPECIFIED
?
MeasureSpec.UNSPECIFIED
有另外两个兄弟模式 MeasureSpec.EXACTLY
和MeasureSpec.AT_MOST
,他们各自代表的含义:
MeasureSpec.UNSPECIFIED
想要多大就多大,系统内部的测量,开发者很少需要使用MeasureSpec.EXACTLY
布局设置宽高match_parent或具体数值MeasureSpec.AT_MOST
布局设置宽高wrap_content
这就说明得到的测量模式MeasureSpec.UNSPECIFIED
不是我们的xml布局导致的,我们日常开发只可能设置布局宽高wrap_content或match_parent或具体数值。那这里的MeasureSpec.UNSPECIFIED
是怎么来的,平常使用ListView没问题,多嵌套一层ScrollView就出问题。ScrollView是一个ViewGroup,ViewGroup除了测量自己还要测量子View。ListView被ScrollView嵌套,作为ScrollView的子View显示。
1.2 查看ScrollView的onMeasure方法源码
看ScrollView实现的onMeasure方法源码,没发现什么猫腻,但是ScrollView在实现自己的代码前调用了super.onMeasure(widthMeasureSpec, heightMeasureSpec)
,点进去查看发现是FrameLayout的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// count为1
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
// count为1,走这里
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 测量子view(ScrollView中的ListView)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
count = mMatchParentChildren.size();
// 因为ScrollView最多只能有一个子View, 所以这里count最多为1, 不会走这里
if (count > 1) {
...
}
}
复制代码
在FrameLayout的onMeasure方法的for循环中发现调用measureChildWithMargins
方法测量子view(也就是ScrollView中的ListView)。ScrollView是直接继承FrameLayout的,所以直接到ScrollView中查找measureChildWithMargins
方法看看是否有覆写实现,有的话这里调用的measureChildWithMargins
方法就是ScrollView中的measureChildWithMargins
方法,还真有覆写
@Override
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 usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;
// 真相在这里
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
这里可以明确看到获取childHeightMeasureSpec时给了MeasureSpec.UNSPECIFIED
模式。如果还不确定可以继续查看MeasureSpec中makeSafeMeasureSpec静态方法的代码
@UnsupportedAppUsage
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
复制代码
MeasureSpec的makeSafeMeasureSpec方法return调用了makeMeasureSpec
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);
}
}
复制代码
同时我们也知道了MeasureSpec的格式是size + mode
的int,这对我们后面解决ScrollView嵌套ListView的问题也是关键的一点。
2. ScrollView嵌套ListView,无论有多少个item都只会显示一个,怎么解决?
2.1 重写ListView覆写onMeasure方法
现在网上搜一搜都能搜到解决方案:
public class ListViewOnScrollView extends ListView {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
复制代码
继承ListView覆写它的onMeasure方法重新获取onMeasure,替换原来的ListView使用就可以解决问题。
问题是解决了,但是别人为什么可以这么解决,为什么这样写?(习武之人不只关注输赢,更关心招式的精妙所在)
2.2 为什么这样解决?
为什么重写ListView的onMeasure方法应该没什么疑问,布局显示前都会经过测量,都会走onMeasure方法,大家更关注的是为什么在super.onMeasure(widthMeasureSpec, heightMeasureSpec)
调用之前heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
这行代码,为什么这样写就可以解决问题。
这就要再一次查看ListView的onMeasure代码
@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 (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}
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;
}
复制代码
回顾之前我们的分析,首先确定的是这里得到的heightMode不能为MeasureSpec.UNSPECIFIED,那就应该让heightMode == MeasureSpec.AT_MOST的模式得到确定
,从而执行measureHeightOfChildren
方法
在这里,MeasureSpec.AT_MOST
的模式得到确定
public class ListViewOnScrollView extends ListView {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 确定了传入参数 MeasureSpec.AT_MOST
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
复制代码
measureHeightOfChildren
方法
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
int maxHeight, int disallowPartialChildPosition) {
final ListAdapter adapter = mAdapter;
if (adapter == null) {
return mListPadding.top + mListPadding.bottom;
}
// Include the padding of the list
int returnedHeight = mListPadding.top + mListPadding.bottom;
final int dividerHeight = mDividerHeight;
// The previous height value that was less than maxHeight and contained
// no partial children
int prevHeightWithoutPartialChild = 0;
int i;
View child;
// mItemCount - 1 since endPosition parameter is inclusive
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
final AbsListView.RecycleBin recycleBin = mRecycler;
final boolean recyle = recycleOnMeasure();
final boolean[] isScrap = mIsScrap;
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;
}
// Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1);
}
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;
}
复制代码
returnedHeight
是测量得到的ListView里面所有item和分割线的高度值,要让measureHeightOfChildren
方法return这个值就不能让returnedHeight >= maxHeight
条件成立,这个maxHeight就是前面ListView的onMeasure方法调用传入的heightSize, int heightSize = MeasureSpec.getSize(heightMeasureSpec)
,heightMeasureSpec
就是我们要处理的参数,所以为了保证returnedHeight >= maxHeight
条件成立,直接让maxHeight
取得最大值Integer.MAX_VALUE
,所以处理这个问题的时候我们可以覆写ListView的onMeasure方法
public class ListViewOnScrollView extends ListView {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 确定了传入参数 MeasureSpec.AT_MOST
// 也确定了传入参数 Integer.MAX_VALUE
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
复制代码
不对啊,你说确定了传入高度参数值Integer.MAX_VALUE
,那这里怎么是Integer.MAX_VALUE >> 2
移位运算右移2位,牛头不对马嘴?
这就要结合MeasureSpec额外分析了。
在前面分析为什么出现问题的时候我们看过MeasureSpec的makeMeasureSpec方法的代码
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);
}
}
复制代码
可以明确知道heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
这个heightMeasureSpec包含了size和mode信息
MeasureSpec.UNSPECIFIED、MeasureSpec.EXACTLY、MeasureSpec.AT_MOST的参数值如下
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
...
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
...
}
复制代码
比如MeasureSpec.AT_MOST的值就是2 << MODE_SHIFT
,表示2的二进制值左移30位
在Java中int的存储范围是32位,0的二进制值是[前面30个0]00
,1的二进制值是[前面30个0]01
,2的二进制值是[前面30个0]10
2的二进制值左移30位就是10[后面30个0]
,Integer.MAX_VALUE >> 2
即二进制右移2位,原先的第0位置和第1位置变成0,这样得到的
heightMeasureSpec = mode size
二进制位置 0 1 2-31
复制代码
这下总算知道为什么解决ScrollView嵌套ListView只能显示一个item的问题是下面这样子处理了吧
public class ListViewOnScrollView extends ListView {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
复制代码