ScorllView嵌套ListView会出现显示不完整,还有解决了显示不完整问题,却发现无法设置ListView高度。折腾了很久,我把ListView换成RecyclerView,发现没有出现这个问题,这让我很怀疑当时的折腾就是白搭。后来我在ScorllView里面多嵌套几个RecyclerView,也碰到了显示不全的问题,这让我想起了解决ListView显示不全的过程,于是我顺着当初的思路,解决了RecyclerView显示不全的问题。当初的折腾还是有意义的,所以写这篇文章来记录解决ListView问题的过程。
布局一:ScorllView只嵌套ListView
ScorllView只嵌套ListView,布局代码xml文件如下(为了讲解代码方便,暂时不去讨论这种嵌套方式是否具备应用价值。)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestActivity">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
这样嵌套以后,ListView显示的只有一个Item,这就意味着ListView的高度测量有问题,所以先看ListView是如何测量高度的,也就是查看onMeasure()方法里面的代码。代码如下(文中只是粘贴了部分与本文有关的代码)。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childHeight = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {//代码1
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();//代码2
}
if (heightMode == MeasureSpec.UNSPECIFIED) {//代码3
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);//代码4
mWidthMeasureSpec = widthMeasureSpec;
}
在代码里看到,模式为MeasureSpec.UNSPECIFIED时,高度是等于一个Item,刚好和遇到的问题一致,下面详细讲解。
代码1是一个if判断语句,如果Item的条目大于0并且高或宽的测量模式为UNSPECIFIED,那么就进入到这个方法。方法里面我们得到了它的一个item高度,也就是标注为代码2的地方。接下来我们看代码3,如果测量模式为UNSPECIFIED,那么ListView高度就是一个item的高度,代码4中传入的就是代码3中的heightSize。正常情况下item都是大于0的,只要父布局ScorllView给出的测量模式为UNSPECIFIED,判断的结果就是为ture(代码1处为true),就会进入该方法,这样出现问题的原因就能找到了。
想要知道ScorllView给出的测量模式,先看ScorllView里面的onMeasure()方法,没看到设置测量模式的代码。ScorllView继承自FrameLayout,所以就看FrameLayout里面的onMeasure(),发现在遍历子View的时候,会调用measureChildWithMargins()方法。代码如下
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
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);
}
}
}
}
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);,这个方法测量了子View,猜测是在这里设置了测量模式。顺着这个思路,发现ScorllView重写此方法,并且默认了高度的测量模式为UNSPECIFIED,代码如下
@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);//代码1
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);//代码2
}
从代码1中看到,childHeightMeasureSpec默认的是UNSPECIFIED,然后代码2传入的就是childHeightMeasureSpec,代码看到这里,也就找到了ScorllView嵌套ListView只显示1行的原因。因为正常情况下ListView的item是大于0的,而ScorllView默认的高度的测量模式是UNSPECIFIED,从onMeasure()方法的代码里可知,这种情况下,ListView的高度就是一个item高度。知道了原因,就找到了解决问题的方向。
解决方法
以上知道了ListView只显示一行,是因为ScorllView默认测量模式为UNSPECIFIED,只要我们重写ListView的onMeasure方法,并且把高的测量模式改为MeasureSpec.AT_MOST,就可以解决问题。
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) {
super.onMeasure(widthMeasureSpec
,MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2, View.MeasureSpec.AT_MOST));
}
}
MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2, View.MeasureSpec.AT_MOST)),返回一个int值(32位,前两位表示模式,后30位得到大小),包含大小和测量模式,Integer.MAX_VALUE>>2表示右移两位,表示大小,这样得到的是最大值。把ListView换成自定义的View,就解决了。由于现实的需求中,通常ScorllView不会只包含一个ListView,还会有一些其它的布局,接下来将讨论加入其它布局能不能解决问题。
布局二:加入TextView
在ScorllView中多加一个TextView,验证自定义的MyListView能不能解决问题。xml如下
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TestActivity">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:text="内容一"
android:gravity="center"
android:background="@android:color/holo_orange_light"
/>
<com.example.testslidingconflict.MyListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent">
</com.example.testslidingconflict.MyListView>
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
运行以后,这样的布局也能显示正常。后来有个需求,MyListView的高度需要固定,于是我改一个高度,将高度设为300dp,发现并不起作用。后来一想,将模式改为MeasureSpec.AT_MOST,ListView加载的就是全部Item的高度,并且绘制出来,所以设置的高度就无效了。当时没有想到更好的解决方法,我只能将ListView换成RecyclerView,在这里也希望哪位大神指点一下,用ListView怎么解决这个问题。
以上就是当时解决ListView显示不全的所有过程。现在有RecyclerView和NestedScrollView等性能更好的控件提供给我们使用,这些控件本身已经解决了一些冲突。但是现实的应用场景中,还是会出现滑动冲突,显示不全等问题。虽然现在很少用ScorllView,ListView,但是当时的经历,让我碰到其它控件出现相同问题的时候,处理起来更得心应手。所有解决bug时的折腾,就是为你解决下一个类似的bug做准备,并非无用功。