在 ScrollView 中嵌套一个 ListView 会出现一个非常严重的问题, 也就是滑动冲突, 现在先看一下这个问题:
可以看到 ListView不能够响应, 点击事件都被 ScrollView拦截了。
1. 内部拦截法:(子元素中处理)
我通过重写 ListView 的 dispatchTouchEvent() 方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { //父容器不拦截 getParent().requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { getParent().requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_UP: { break; } default: break; } return super.dispatchTouchEvent(ev); } getParent() 方法可以获得父容器, 调用父容器的 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 方法,如果 disallowIntercept 为true, 就是让父容器不要拦截该事件, 如果为 false 父容器需要拦截改事件,如果父容器拦截了该事件, 那么当前的这个事件序列(就是当前接触屏幕到离开屏幕的一次点击事件), 都会被父容器拦截, 父容器拦截之后会调用 onTouchEvent() 方法 (如果设置了onTouchEventListener会调用该接口的方法), 如果该方法返回true, 那么该事件就会被消耗, 如果不消费, 事件会依次从底层向上层传递(注意事件先从高层向底层传递, 然后如果低层不消耗, 事件再向高层返回)。
现在来看一下效果:
现在ScrollView和ListView之间的冲突已经完美解决, 但是人机效果方面还不是很好, 如果ListView滑到底部或者滑到顶部的时候, 应该让ScrollView接管事件,我们修改一下 dispatchTouchEvent() 的一些逻辑:
public class ListViewX extends ListView { private static final String TAG = "ListViewX"; private int mLastX = 0; private int mLastY = 0; public ListViewX(Context context) { super(context); } public ListViewX(Context context, AttributeSet attrs) { super(context, attrs); } public ListViewX(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { width = MeasureSpec.makeMeasureSpec(500, MeasureSpec.AT_MOST); height = MeasureSpec.makeMeasureSpec(500, MeasureSpec.AT_MOST); } else if(widthMode == MeasureSpec.AT_MOST) { width = MeasureSpec.makeMeasureSpec(500, MeasureSpec.AT_MOST); height = heightMeasureSpec; } else if(heightMode == MeasureSpec.AT_MOST) { width = widthMeasureSpec; height = MeasureSpec.makeMeasureSpec(500, MeasureSpec.AT_MOST); } else { width = widthMeasureSpec; height = heightMeasureSpec; } super.onMeasure(width, height); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { //父容器不拦截 getParent().requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if(atTopOrEnd(deltaY)) { getParent().requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); } private boolean atTopOrEnd(int len) { int count = getCount(); int topId = getFirstVisiblePosition(); int endId = getLastVisiblePosition(); if((endId == count - 1 && len < 0)) { View lastView = getChildAt(getChildCount() - 1); if(lastView.getBottom() == getHeight()) { return true; } } if(topId == 0 && len > 0) { View firstView = getChildAt(topId); if(firstView.getTop() == 0) { return true; } } return false; } } 在这里我判断如果Listview滑到底端或者顶端时, 让父容器接管事件, 现在看一下效果:
2.外部拦截法:(父容器中处理) 我们也可以通过重写ScrollView的onInterceptedTouchEvent() 方法来解决冲突, 话不多说,直接上代码: public class ScrollViewX extends ScrollView { private static final String TAG = "ScrollViewX"; private ListViewX mListViewX; private ViewPager mViewPager; private int mLastX = 0; private int mLastY = 0; public ScrollViewX(Context context) { super(context); } public ScrollViewX(Context context, AttributeSet attrs) { super(context, attrs); } public ScrollViewX(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); int deltaX = x - mLastX; int deltaY = y - mLastY; Log.i(TAG, "deltaY = " + deltaY); mLastX = x; mLastY = y; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { return super.onInterceptTouchEvent(ev); } case MotionEvent.ACTION_MOVE: { if(mViewPager != null && isTouchInView(mViewPager, ev)){ //点击事件发生在viewpager范围内 if(Math.abs(deltaY) > Math.abs(deltaX)) { //如果竖直方向的滑动距离大于横向, 那么scrollview拦截 return true; } else { return super.onInterceptTouchEvent(ev); } } else if(mListViewX != null && isTouchInView(mListViewX, ev)) { if(atTopOrEnd(deltaY)) { return true; } else { return false; } } else { return super.onInterceptTouchEvent(ev); } } case MotionEvent.ACTION_UP: { return super.onInterceptTouchEvent(ev); } default: break; } return super.onInterceptTouchEvent(ev); } //如果listView滑到顶端时当前事件向上滑动,需要scrollview接管, 在底端时类似。 private boolean atTopOrEnd(int len) { int count = mListViewX.getCount(); int topId = mListViewX.getFirstVisiblePosition(); int endId = mListViewX.getLastVisiblePosition(); if((endId == count - 1 && len < 0)) { View lastView = mListViewX.getChildAt(mListViewX.getChildCount() - 1); if(lastView.getBottom() == mListViewX.getHeight()) { return true; } } if(topId == 0 && len > 0) { View firstView = mListViewX.getChildAt(topId); if(firstView.getTop() == 0) { return true; } } return false; } //判断点击事件是否在当前view中 private boolean isTouchInView(View view, MotionEvent event) { int x = (int) event.getRawX(); int y = (int) event.getRawY(); int[] local = new int[2]; view.getLocationOnScreen(local); int subVX = local[0]; int subVY = local[1]; int subWidth = view.getWidth(); int subHeight = view.getHeight(); if(x > subVX && x < subVX + subWidth && y > subVY && y < subVY + subHeight) { return true; } return false; } public void setListViewX(ListViewX listViewX) { mListViewX = listViewX; } public void setViewPager(ViewPager viewPager) { mViewPager = viewPager; } } 代码中isTouchInView()方法用来判断当前事件是否在目标控件中, atTopOrEnd() 方法判断Listview是否滑到底端或者顶端(为了让滑动效果更好), 如果我们想让当前事件由ScrollView拦截, 那么onInterceptTouchEvent() 方法返回true, 不然要根据情况返回false或者super.onInterceptedTouchEvent(ev)。