ScrollView嵌套ListView显示不全分析及解决
1.发现问题
当我们使用ScrollView嵌套ListView时会出现ListView只显示一行的高度,如下图:
通过代码获取到ListView的高度是150,只有一行的高度:
image.png Screenshot_2019-12-25-22-51-47-679_com.example.my.png
2.源码解析
为什么会出现这个问题呢?根据猜测应该是ListView在测量的时候出现了问题,我们找到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;
//获取ListView的Item数量,遍历计算宽高
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);
}
//由于是高度测量出现问题,我们只看高度的测量
//我们看到当测量模式为 MeasureSpec.UNSPECIFIED时只计算了一行的高度
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
//我们看到当测量模式为 MeasureSpec.AT_MOST时,会通过measureHeightOfChildren累计
//计算当前所有的item高度
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;
}
根据上源码我们知道,当测量模式为 MeasureSpec.UNSPECIFIED时只计算了一行的高度,而这个heightMode是它的parent(ScrollView)传进来的heightMeasureSpec计算的。我们继续看ScrollView,由于ScrollView继承自FrameLayout,我们来看下FrameLayout的onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
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;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//循环查找自己的子View如果子View不是GONE的话就调用ViewGroup的measureChildWithMargins方法
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);
}
}
}
}
省略。。。。。。。
}
代码比较简单就是循环查找自己的子View如果子View不是GONE的话就调用ViewGroup的measureChildWithMargins方法,我们发现ScrollView是重写了这个方法的,找到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;
//就是这里了 给子view传入的heightMeasureSpec是MeasureSpec.UNSPECIFIED
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
最终是ScrollView重写measureChildWithMargins后将MeasureSpec.UNSPECIFIED传到ListView的onMeasure方法测量。在最开始看到的ListView的onMeasure方法中
//由于是高度测量出现问题,我们只看高度的测量,我们看到当测量模式为 MeasureSpec.UNSPECIFIED时只计算了一行的高度
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
当测量模式为 MeasureSpec.UNSPECIFIED就只会计算一行的高度,最后将高度通过setMeasuredDimension(widthSize, heightSize)方法设置ListView的高度,造成ListView显示不全。
3.解决办法
上述分析以及知道了显示不全的原因,我们只需将将ListView onMeasure方法的heightMeasureSpec模式为AT_MOST,让它进入下面的判断即可正常的显示高度了
//我们看到当测量模式为 MeasureSpec.AT_MOST时,会通过measureHeightOfChildren累计
//计算当前显示多少条的item高度
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);
}
重写ListView的onMeasure方法
public class MyListView extends ListView {
public MyListView(Context context) {
super(context);
}
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2,
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightSpec);
}
}
运行代码获取高度,ListView显示完全:
image.png image.png