我们在用ScrollView嵌套ListView会两个问题,一个问题是ListView高度不正常,另外一个问题是ListView无法滑动。下面我们就来看看这两个问题怎么解决吧。
第一个问题
ListView只能显示一个Item高度的问题。因为ScrollView在测量ChildView的时候,强制把ChildView的MeasureSpec模式更改为MeasureSpec.UNSPECIFIED,而我们通过查看ListView的onMeasure方法可以发现
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
当heightMode为MeasureSpec.UNSPECIFIED时,ListView的高度会被设置为上下的padding加上一个childView的高度,所以就会出现只显示一个Item高度的问题。
那我们怎么解决呢?
第一种方法,我们可以重写ListView,在onMeasure的时候重新设置高度的MeasureSpec模式。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// MeasureSpec的前两位是模式,所以需要右移两位。
heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
这样ListView就是完全展开的状态了。
第二种方法,我们可以在布局里面为ScollView加一个
android:fillViewport="true"
这样ScrollView就会被强制要求测量ChildView并设置模式MeasureSpec.EXACTLY,所以ListView会完全展开。
第二个问题
ScrollView嵌套ListView时,一般我们有两种需求
第一种是ListVIew完全伸展并跟随ScrollView一起滑动,那只要按照上面的解决了伸展的问题, 就实现这种效果了,因为ScrollView默认是拦截ListView的滑动事件的。
第二种是ScrollView不拦截滑动事件,当我们在ListView区域滑动时,由ListView处理滑动事件,只有在ListView已到达顶部还继续向上滑或者ListView已到达底部还继续向下滑时才重新拦截滑动事件。而当我们在非ListView区域滑动时,则直接由ScrollView处理滑动事件,那么我们看看怎么实现这种效果。
首先,我们先了解为什么ListView无法获取到滑动事件,我们先看一下ScrollView的onInterceptTouchEvent方法
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
return true;
}
省略
//........
}
不重要的部分被我省略了,mIsBeingDragged是最小滑动像素,超过这个值才被认为是发生滑动了。那从上面的代码我们可以看出的,当发生滑动的第一时间,事件就被ScrollView拦截了,所以ListView不能滑动
但是我们注意到,在MotionEvent.ACTION_MOVE之前是会产生一个MotionEvent.ACTION_DOWN的,那么这个事件是没有被拦截的,并且在滑动未超过最小像素之前,
MotionEvent.ACTION_MOVE也是未被拦截的,那么其实我们是可以在ListView里面接收到MotionEvent.ACTION_DOWN和几个MotionEvent.ACTION_MOVE的。
既然如此,那我们在自定义ListView里面请求ScrollView不要拦截事件不就好了吗。
public class MyListview extends ListView{
private ScrollView mParent;
private float mDownY;
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);
}
public void setParent(ScrollView view){
mParent = view;
}
//重写该方法 在按下的时候让父容器不处理滑动事件
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
setParentScrollAble(false);
mDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
//
if (isListViewReachTop() && ev.getY() - mDownY > 0) {
setParentScrollAble(true);
} else if (isListViewReachBottom() && ev.getY() - mDownY < 0) {
setParentScrollAble(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setParentScrollAble(true);
break;
default:
break;
}
return super.onTouchEvent(ev);
}
/**
* @param flag
*/
private void setParentScrollAble(boolean flag) {
mParent.requestDisallowInterceptTouchEvent(!flag);
}
public boolean isListViewReachTop() {
boolean result=false;
if(getFirstVisiblePosition()==0){
View topChildView = getChildAt(0);
if (topChildView != null) {
result=topChildView.getTop()==0;
}
}
return result ;
}
public boolean isListViewReachBottom() {
boolean result=false;
if (getLastVisiblePosition() == (getCount() - 1)) {
View bottomChildView = getChildAt(getLastVisiblePosition() - getFirstVisiblePosition());
if (bottomChildView != null) {
result= (getHeight() >= bottomChildView.getBottom());
}
}
return result;
}
}
在上面的代码中,我们首先需要把ScrollView设置进来,然后在onTouchEvent中接收到MotionEvent.ACTION_DOWN事件时,请求ScrollView不要拦截滑动事件,这样ListView就可以处理滑动事件了,而在顶部仍然向上滑和在底部仍然向下滑 和 MotionEvent.ACTION_UP 和 MotionEvent_ACTION_CANCEL时,都直接重新把事件给回ScrollView。