概述
ScrollView内镶嵌ListView会出现显示不全(只显示一个Item)的问题,这里我们要回答具体原因和解决方案。
问题分析
先看关于测量View的几个参数:
在ScrollView中添加ListView,那么ScrollView就是父View,ListView是子View。先看ScrollView的onMeasure()的源码:
ScrollView.class:1
2
3
4
5
6
7
8
9
    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
    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
14protected 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
    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.UNSPECIFIED1
2
3final 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
    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
2heightSize = 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
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
10if (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,那么就会出现显示不全的问题。