ScrollView镶嵌ListView问题原因分析

概述

ScrollView内镶嵌ListView会出现显示不全(只显示一个Item)的问题,这里我们要回答具体原因和解决方案。

问题分析

先看关于测量View的几个参数:
在ScrollView中添加ListView,那么ScrollView就是父View,ListView是子View。先看ScrollView的onMeasure()的源码:
ScrollView.class:

1
2
3
4
5
6
7
8
9
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);//先调用父类的onMeasure();
//而ScrollView的父类是FrameLaout


// ... ...

}

父View:
FrameLaout.class :

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

// ... ...

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);//这里先对子View进行处理

//接着开始测量
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);
}
}
}
}

// ... ...
}

到这里就需要看下measureChildWithMargins方法了,点进去之后发现在ViewGroup里面,这里对子View进行了测量处理
ViewGroup.class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

因为他们的继承关系是:ScrollView继承FrameLaout继承ViewGroup,而SrcollView的子View是ListView,所以重点看下ScrollVIew的measureChildWithMargins方法。如下:
ScrollView.class :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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);
}

这句很关键,它给所有的子View的MODE设置为MeasureSpec.UNSPECIFIED

1
2
3
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);

而在ListView的onMeasure中可以看到:
ListView.class:

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
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// ... ...

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) {//测量每个child累加起来的高度
// 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;
}

这里可以看到在MODE是MeasureSpec.UNSPECIFIED的情况下,只计算了一个View的高度

1
2
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;

这就是为什么只看到一个Item的原因。

解决方案

当MODE是MeasureSpec.UNSPECIFIED的情况下,只计算了一个View的高度。所以要设置MODE,当MODE是MeasureSpec.AT_MOST时会调用measureHeightOfChildren方法:

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
/*
* @return The height of this ListView with the given children.
*/
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();//for循环中累加child的高度

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;
}

返回的是child的高度,这就没问题,那么如何制定MODE的值为AT_MOST呢
这里借用了网上的解决方案,继承ListView,重写onMeasure()方法:

1
2
3
4
5
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);//制定MODE和size
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

MeasureSpec.class:

1
2
3
4
5
6
7
8
9
10
11
/*
* @return the measure specification based on size and mode
*/
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);
}
}

之后就会调用父类的onMeasure方法,重新测量。
为什么会是Integer.MAX_VALUE>>2呢?

1
2
3
4
5
6
7
8
9
10
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;
}

我们需要的是returnedHeight,所以该if不能让它成立,要给maxHeight一个最大的值,第一个参数Integer.MAX_VALUE>>2,这个参数是传的一个大小值,它的大小最大值是int的最低30位的最大值,我们先取Integer.MAX_VALUE来获取int值的最大值,然后右移2位就得到这个临界值最大值了。。。恍恍惚惚。。。

总结

在ListView对Item的测量中,在MODE为UNSPECIFIED的情况下,只计算一个child的高度。而ScrollView中对所有的子View的MODE都设置为UNSPECIFIED。所以当ScrollView镶嵌了ListView,ListView成为了子View,那么就会出现显示不全的问题。