- 什么是滑动冲突?
- 具体解决办法
- ACTION_DOWN:子View消耗ACTION_DOWN
- ACTION_MOVE:外部拦截
- ACTION_MOVE:内部拦截
- ACTION_MOVE:特殊的内部拦截
- 总结
什么是滑动冲突?
- 概念:滑动冲突即某些特定的滑动事件被父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()方法,在事件的“结果返回过程”中消耗ACTION_DOWN事件。
具体解决办法
ACTION_DOWN:子View消耗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()源码。
ACTION_MOVE:外部拦截
“自控”:父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()的实现。
ACTION_MOVE:内部拦截
“子控”:子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;
}
ACTION_MOVE:特殊的内部拦截
当我们不想或者无法重写父View和子View的方法时,还有另一中方式解决滑动冲突。前提是ACTION_DOWN不被子View的下层消耗。
- 解决办法:为子View设置OnTouchListener,在onTouch()中控制父View对ACTION_MOVE的拦截。
- 原理:ACTION_DOWN在“结果返回过程”中到达子View时因尚未被消耗从而触发子View调用基类View(子View的父类)的dispatchTouchEvent()方法。而子View设置OnTouchListener后基类View的dispatchTouchEvent()在执行时会调用已设置的OnTouchListener的onTouch()。
为子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。