滑动冲突是如何产生的呢? 其实在界面中只要内外层同时可以滑动,这个时候就产生滑动冲突。
一、滑动冲突常见场景
常见的滑动冲突场景可以简单分为以下三种。
- 场景1:外部滑动方向和内部滑动方向不一致; 外层的ViewGroup可以横向滑动,内层的View可以竖向滑动,如ViewPager内每个页面是ListView (ViewPager 已经处理了滑动冲突,所以在使用中并没有出现滑动冲突)
- 场景2:外部滑动方向和内部滑动方向一致;外层的ViewGroup可以竖向滑动,内层的View也可以竖向滑动,如ScrollView 内部是ListView
- 场景3:场景1和场景2 的嵌套
二、滑动冲突处理规则
场景一
处理规则: 当用户左右滑动时,需要让外部的View拦截点击事件;当用户上下滑动时,需要让内部View拦截点击事件。
至于如何获得滑动方向,我们可以得到滑动过程中的两个点的坐标。一般情况下根据水平和竖直方向滑动的距离差就可以判断方向,当然也可以根据滑动路径形成的夹角、水平和竖直方向滑动速度差来判断。
场景二
由于外部与内部的滑动方向一致,那么不能根据滑动角度、距离差或者速度差来判断。这种情况下必需通过业务逻辑来进行判断。 比如业务上有规定: 当处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态时则需要内部View来响应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;
}
- 根据业务逻辑需要,在ACTION_MOVE方法中进行判断,如果需要父View处理则返回true,否则返回false,事件分发给子View去处理。
- ACTION_DOWN 一定返回false,不要拦截它,否则根据View事件分发机制,后续ACTION_MOVE 与 ACTION_UP事件都将默认交给父View去处理。
- 原则上ACTION_UP也需要返回false,如果返回true,并且滑动事件交给子View处理,那么子View将接收不到ACTION_UP事件,子View的onClick事件也无法触发。而父View不一样,如果父View在ACTION_MOVE中开始拦截事件,那么后续ACTION_UP也将默认交给父View处理。
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗,否则,就交给父容器进行处理; 这种方法和Android 中的事件分发机制不一致, 需要配合requestDisallowInterceptTouchEvent 方法才能正常工作。下面是子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);
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);
}
父View需要重写onInterceptTouchEvent方法,如下所示:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
- 内部拦截法要求父View不能拦截ACTION_DOWN事件,由于ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标志位控制,一旦父容器拦截ACTION_DOWN那么所有的事件都不会传递给子View。
- 滑动策略的逻辑放在子View的dispatchTouchEvent方法的ACTION_MOVE中,如果父容器需要获取点击事件则调用 parent.requestDisallowInterceptTouchEvent(false)方法,让父容器去拦截事件。