我们在用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。