开发的时候遇到滑动冲突是很坑的,比如在界面中有内外两层可以同时滑动,这个时候就会产生滑动冲突,其实解决滑动冲突是有固定套路的,掌握了这个套路滑动冲突就好解决了。
下面先列举一下产生滑动冲突的三个场景再一 一介绍:
1. 外部滑动方向和内部滑动方向不一致
2. 外部滑动方向和内部滑动方向一致
3. 上面两种情况的嵌套
对于场景1的效果,通常出现在ViewPage和Fragment结合使用的情况,这种情况下可以通过左右滑动来切换界面,而每个界面内有个ListView又可以上下滑动。这个时候虽然有滑动冲突但是ViewPage内部已经解决了。所以使用ViewPage的时候我们不要考虑滑动冲突的问题。但是当我们使用ScrollView或者其他的时候就需要我们自己解决滑动冲突了。
场景2比场景1要复杂一点,为什么这么说。因为场景一可以通过判断手指滑动方向来确定哪个View来滑动,但是当内外两层可以在同一个方向滑动时显然存在逻辑问题,系统无法知道用户想让那一层滑动,要么只有一层能滑动,要么就是内外两层都滑动的很卡顿。
场景3是场景1和场景2共同存在的情况,比如SlideMenu,ViewPage和ListView共存的情况。所以他虽然是最复杂的,但当解决了1和2的时候,他也就迎刃而解了。
对于场景1可以根据滑动过程的两个点之间的坐标得到滑动方向。这个很简单,比如以及滑动路径和水平方向的夹角或者水平方向和竖直方向的距离等。但是存在一种情况是,用户正在水平滑动,又迅速进行竖直滑动,如果不做处理可能就导致水平滑动没有到终点而处于中间状态,所以这种情况可以将竖直滑动的事件序列仍然交给父容器处理,完成水平滑动。实际上大部分应用就是这么处理的。对于场景2无法利用滑动方向解决,他可以通过所处的状态判断该内层响应用户的滑动还是外层响应用户的滑动。场景3因为是一种叠加型滑动冲突,可以拆解为单一的滑动冲突,分别解决每一层即可。
这里给出两种办法:内部拦截法和外部拦截法。
1.外部拦截法
所谓外部拦截法是指经过父容器的拦截处理,如果父容器需要此事件就拦截,否者不拦截,就交给下面子元素处理,这样就可以把滑动交给正确的View了。这和View的事件分发机制是一致的。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法中做拦截。伪代码很简单,如下所示:
@Override
public booleanonInterceptTouchEvent(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_DOWN这个事件,是因为父容器拦截了ACTION_DOWN事件后,该事件序列的其他事件也会交给父容器处理,这就没办法传给子元素了。而对于ACTION_UP被写成了true,可以想象如果事件交给了子元素处理,而父容器拦截了最后的ACTION_UP,子元素的onCLick就无法被触发,所以我们返回了false。
2.内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,但是如果子元素不需要此事件就交给父容器处理。这种方法需要配合requestDisallowInterceptTouchEvent方法工作,相较外部拦截法有点复杂。需要先重写子元素的dispatchTouchEvent方法,伪代码如下:
@Override
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);
}
当面对不同的滑动策略时只需要修改里面的if条件即可。当父容器需要此类事件时父容器要默认拦截了除ACTION_DOWN以外的其他事件,这样子元素调用了parent. requestDisallowInterceptTouchEvent(false)时父元素才能继续拦截其他事件。这个机制我们已经说过了。但是父容器是不能拦截ACTION_DOWN事件的,因为一旦父元素拦截了ACTION_DOWN,其他事件就无法传到子元素当中了,内部拦截法就变得毫无意义了,所以父元素修改如下:
@Override
public boolean onInterceptTouchEvent(MotionEventevent) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN: {
return false;
break;
}
default:{
return true;
break;
}
}
}
下面说一个具体的例子,加入我们要在一个类似可以上下滑动的竖直LinearLayout,暂命名为SlipLayout。在他里面有一个ListView,然后有一些其他的子元素。这样内外两层都可以上下滑动,形成了2中的滑动冲突。我们就可以采取下面的滑动规则:当ListView滑动到顶部或者尾部的时候,由SlipLayout拦截事件,其他情况ListView拦截事件。判断ListView是否滑到顶部可用下面的实现:
public boolean giveUpTouchEvent(MotionEvent event){
if(expandableListView.getFirstVisiblePosition(==0))
{
View view= expandableListView.getChildAt(0);
if(view!=null&&view.getTop()>=0)
return true;
}
return false;
}