深入理解事件分发机制之解决滑动冲突
外部拦截法,即对父容器的onInterceptTouchEvent进行处理,父容器要是需要这个事件就拦截,不要就不拦截
public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { if (父容器需要这个事件) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXintercept = x; mLastYIntercept = y; return intercepted; }先明白一点,down事件必须为false。因为如果down事件拦截了,那么接下来的事件move,up不管怎么设置,也一并会交给父容器处理。move事件看你需求,如果需要就截留。up事件无所谓了,因为你前面的一旦打算截留,up事件也会一并交给父容器处理。
内部拦截法,通过调用父容器的requestDisallowInterceptTouchEvent方法,如果子元素需要这个事件就消耗掉,否则就让父容器处理
public boolean onInterceptTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要这个事件) { parent.requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); }父容器的requestDisallowInterceptTouchEvent方法相当于使得它的拦截机制失效了,所以这是子容器控制父容器的一个手段。不过requestDisallowInterceptTouchEvent方法却无法使得对down事件的拦截也失效(具体原因是FLAG_DISALLOW_INTERCEPT标记位的重置)。所以父容器中还是需要保证他的down事件是返回false,表示不拦截的。
第一种冲突情况,父容器类似ViewPager(因为ViewPager已经帮我们处理好冲突了,所以我们用另外类似的控件举例子,下面称 类ViewPager),子元素是ListView。这种情况就面临一个问题,你是水平滑类ViewPager呢还是竖直滑ListView呢?显然,如果你想滑动的是ListView,虽然你已经发出了向下的手势,但是你有可能水平方向上移动了一点点,这个时候类ViewPager水平方向上也会移动,这个时候的用户体验就不是很好了。所以这就是你需要处理的地方。你应该做的是,判断水平的移动的距离大还是竖直移动的距离大,如果水平的大,那就把事件交给类ViewPager,如果竖直的大,就把事件交给ListView。
采用外部拦截法。
case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; }
mLastXintercept = x; mLastYIntercept = y;这里通过旧坐标点和当前坐标点判断位移,就好了。如果水平大,就拦截,类Viewpager消费,反之ListView消费。
考虑这样一种情况,加深你的理解。如果你一开始是水平滑动,但是你松开之前,又选择了上下滑动,这个时候会发生什么?自然是依然交给父容器处理。因为在最开始的判断中,你的水平比竖直移动的大,那么就这次事件就设置为拦截了已经。那么这一串down-move-up,都是只会让父容器,这个类ViewPager来响应了。你的竖直滑动是无效的。
此外,还要考虑一种情况。假如你的水平滑动,类ViewPager滑到了一半,但是这个时候你突然松开,又进行了上下滑动,那么这个类ViewPager就会很尴尬,处于中间的位置,这是很不友好的用户体验。所以我们还要让当水平滑动未结束的时候,接下来的事件依然交给父容器来进行。这里就是,他会判断,如果你的滑动没结束,那么直接拦截,不多bb。(如果你希望ListView也是这样好的用户体验,可以如法炮制)
case MotionEvent.ACTION_DOWN: { if (!mScroller.isFinished()) { mScroller.abortAnimation(); intercepted = true; } break; }
(此外这种冲突用内部拦截法也是如法炮制,简单的很)
第二种冲突情况,同方向冲突
最外面一个scrollView,可上下滑动,而里面有一个Header和一个ListView。
像这样。
为什么要弄这样一个布局呢?有时候因为业务需求,你需要给ListView添加一个头。你当然可以添加到ListView的里面,不过你肯定尝试过把这个头放到外面过,并且在外面套上一个滑动容器,结果失败了。这里就来解决这个问题。
他的滑动规则是:当Header显示的时候,事件交给ScrollView。当Header刚好隐藏,ListView的顶部在ScrollView的顶部且此时的手势是向下的时候,事件也交ScrollView。此外就交给ListView。而滑动事件落在Header上的时候,(它是一个ViewPager轮播图),那么事件就交给了Header了。此外水平位移大于竖直位移,事件也不会拦截。
case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) { intercepted = 0; } else if (Math.abs(deltaY) <= Math.abs(deltaX)) { intercepted = 0; } else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) { intercepted = 1; } else if (mGiveUpTouchEventListener != null) { if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) { intercepted = 1; } } break; }第一个if,如果落在header内(通过y <= getHeaderHeight()判断,前面那个就是子类是否希望无效化父类的拦截机制,你懂的),不拦截;
第二个if,如果水平位移大于竖直位移,不拦截;
第三个if,如果Header没有被完全隐藏(即有header的区域显示在界面内),并且是向上滑动(位移是负数且小于最小可识别滑动距离),scrollView拦截。
第四个if,如果ListView滑动到顶部了(即header刚好被遮住)并且向下滑动,scrollView拦截。
更复杂点的情况其实也一样,明白规则,根据外部和内部两个模板就可以解决。不过在这之前,还是梳理下需求为好(比如第二种冲突,完全可以把header作为一个item 放到listView里),这样首先就可以节省不小的工作量。