- 概念:滑动冲突即某些特定的滑动事件被父View拦截导致子View接收不到该事件无法滑动。
基本类型:
其他复杂类型都是由基本类型组成的。˼·
从滑动冲突的概念可知,只需让子View接收到特定的滑动事件即可解决冲突。
子View要接收到ACTION_MOVE必须:ACTION_DOWN:从Android事件分发机制本质是树的深度遍历(图+源码)的结论(即ACTION_DOWN会深度遍历“分发树”并确定“消耗树”,后续同一序列事件都是沿着这一“消耗树”分发(深度遍历,但通常都是线性结构)的,且可被中途拦截但“消耗树”不变。)可知,要让ACTION_DOWN至少能分发到子View并且被子View或更下层的View消耗,其实就是让“消耗树”能够到达子View,这样后续的ACTION_MOVE事件才有机会到达子View。总之,ACTION_DOWN必须被子View或它的下层消耗。
解决办法:在子View的在onTouchEvent()中消耗ACTION_DOWN。
ACTION_MOVE:父View不拦截子View需要的特定ACTION_MOVE。
解决办法:- 外部拦截:重写父View的onInterceptTouchEvent(),不拦截子View需要的特定滑动事件。(“自控”:父View自己控制拦截ACTION_MOVE与否)
- 内部拦截:在子View的dispatchTouchEvent()中通过调用父View的requestDisallowInterceptTouchEvent()方法阻止父View对子View所需的特定滑动事件的拦截。(“子控”:子View控制父View拦截ACTION_MOVE与否)
以上两种方法在必要时(若子View的下层View没有消耗ACTION_DOWN事件时)还应重写子View的onTouchEvent()方法,在事件的“结果返回过程”(参考:Android事件分发机制本质是树的深度遍历(图+源码))中消耗ACTION_DOWN事件。
若子View的下层View没有消耗ACTION_DOWN事件时,需确保MotionEvent为ACTION_DOWN时onTouchEvent()返回true。
注意:这一点是很多技术博客都没讲的,在处理冲突时要注意。
例子1:ViewPager在onTouchEvent()中ACTION_DOWN时默认返回true。
@Override public boolean onTouchEvent(MotionEvent ev) { ...... switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { mScroller.abortAnimation(); mPopulatePending = false; populate(); // Remember where the motion event started mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = ev.getPointerId(0); break; } ...... } ...... return true;//ACTION_DOWN时默认消耗 }
经典的PullToRefresh在onTouchEvent()中也是默认消耗ACTION_DOWN,参考:Android-PullToRefresh 之二:详细设计(一、PullToRefresh)中PullToRefreshBase类的onTouchEvent()源码。
“自控”:父View自己控制拦截ACTION_MOVE与否。
重写父View的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; }
缺点:若父View本身已重写onInterceptTouchEvent()且比较复杂则再一次重写必须考虑原来onInterceptTouchEvent()的实现。
“子控”:子View控制父View拦截ACTION_MOVE与否。
重写子View的dispatchTouchEvent()方法:
public boolean dispatchTouchEvent(MotionEvent event) { //控制逻辑 int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true);//不允许父View拦截后续事件 break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此类点击事件) { parent.requestDisallowInterceptTouchEvent(false);//允许父View拦截后续事件 } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event);//调用子View原来的dispatchTouchEvent()方法。 }
重写父View的onInterceptTouchEvent()方法:
实际上,父View一般都会自己重写onInterceptTouchEvent(),无需我们再次重写(如ViewPager),若要重写则必须考虑父View原先的onInterceptTouchEvent()。
public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } }
优点:相比外部拦截,这个方法更容易,因为重写dispatchTouchEvent()只是在子View原先的dispatchTouchEvent()的基础上添加控制父View对特定滑动事件拦截的功能,不用考虑子View原先dispatchTouchEvent()的实现,直接调用就好。
无论是“自控”还是“子控”,都应该让ACTION_MOVE的控制逻辑只在第一个ACTION_MOVE事件的时候触发,第一个ACTION_MOVE事件决定交给父View消耗则后续ACTION_MOVE事件都直接交给父View,不要再次调用控制逻辑。
原因:多次触发控制逻辑可能会导致同一系列的不同ACTION_MOVE交给不同的View处理。
注意:这一点是很多技术博客都没讲的,在处理冲突时要注意。
具体例子看ViewPager源码(内部拦截):
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; ...... // Nothing more to do here if we have decided whether or not we // are dragging. if (action != MotionEvent.ACTION_DOWN) { if (mIsBeingDragged) {//若ACTION_MOVE已交给ViewPager处理,则后续事件同样交给ViewPager处理,无需再次触发后面的控制逻辑 if (DEBUG) Log.v(TAG, "Intercept returning true!"); return true; } if (mIsUnableToDrag) {//若ACTION_MOVE已交给ViewPager的子View处理,则后续事件同样交给该子View处理,无需再次触发后面的控制逻辑 if (DEBUG) Log.v(TAG, "Intercept returning false!"); return false; } } //控制逻辑:判断滑动事件交给谁消耗。 switch (action) { case MotionEvent.ACTION_MOVE: ...... } /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; }
当我们不想或者无法重写父View和子View的方法时,还有另一中方式解决滑动冲突。前提是ACTION_DOWN不被子View的下层消耗。
- 解决办法:为子View设置OnTouchListener,在onTouch()中控制父View对ACTION_MOVE的拦截。
- 原理:ACTION_DOWN在“结果返回过程”中到达子View时因尚未被消耗从而触发子View调用基类View(子View的父类)的dispatchTouchEvent()方法。而子View设置OnTouchListener后基类View的dispatchTouchEvent()在执行时会调用已设置的OnTouchListener的onTouch()。
更详细的解释参考:Android事件分发机制本质是树的深度遍历(图+源码)
为子View设置OnTouchListener:
childView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true);//不允许父View拦截后续事件 break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此类点击事件) { parent.requestDisallowInterceptTouchEvent(false);//允许父View拦截后续事件 } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return false; } });
滑动冲突即某些特定的滑动事件被父View拦截导致子View接收不到该事件无法滑动。解决滑动冲突就是让子View接收到它需要的特定滑动事件。为此,必须:
1. ACTION_DOWN必须被子View或它的下层消耗。
2. 控制父View不拦截子View需要的特定ACTION_MOVE。