谈起 Android 中 View 的滑动冲突,我们先要了解一下 View 的事件分发机制,因为理解事件分发机制是解决滑动冲突的理论基础。
View 的点击事件分发概述
点击事件的分发过程由三个很重要的方法来共同完成:
- disptchTouchEvent:进行事件分发
- onInterceptTouchEvent:判断是否拦截
- onTouchEvent:处理点击事件 上面三个方法用伪代码表示如下
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
通过上面的伪代码,大致可以了解点击事件的传递规则:
当一个 ViewGroup 接收到一个事件的时候,首先会调用 dispatchTouchEvent() 方法进行事件分发,如果 onInterceptTouchEvent() 返回 true,则代表当前 View 会拦截事件,则直接回调 onTouchEvent() 方法进行事件处理。如果不拦截,则直接回调子 View 的 dispatchTouchEvent() 方法。如此反复,一直到最里面的子 View。
当一个点击事件产生后,它的传递过程遵循以下顺序:Activity => Window => View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 再传递给顶层 DecorView,然后遵循上面的方式一直在最里层 View。
而处理事件则从最里层 View 不断回传给自己的外层 View,如果一直没有 View 进行处理,则直接会回传到 Activity 中。
常见的滑动冲突场景
滑动冲突的场景一般有下面两种情况:
- 场景一:外部滑动方向和内部滑动方向不一致;
- 场景二:外部滑动方向和内部滑动方向一致;
还有的场景是上面两种情况的嵌套,这种场景稍微要复杂。但是不管多复杂的滑动冲突,它们之间的区别仅仅是滑动的规则不同而已。
对于场景一,滑动规则比较简单:根据滑动方向是水平方向还是竖直方向来判断到底由谁来拦截事件。那如何来判断滑动的方向呢?可以根据两个方向的距离差、速度差来做判断。
对于场景二,由于是同一方向的滑动,无法从物理上做出判断,所以这时候需要在业务上来寻找突破点。业务上能确定在某种状态下需要由相应的 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;
}
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递到子元素,由子元素来决定是否消费事件。需要配合 requestDisallowInterceptTouchEvent 方法来完成。
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);
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);
}