注意:
- 阅读本文需要了解《Android事件分发机制》
- 在此知识点,本人也有部分困惑尚未完全解决,也会在文中标出出来。
常见的滑动冲突场景及对应的处理规则
- 外部滑动方向和内部滑动方向不一致
面对这种情况的滑动冲突,解决规则是:根据滑动是水平滑动还是竖直滑动来判断由谁来拦截事件。判断滑动方向的方法是:比较水平方向和竖直方向滑动距离的大小,或者滑动路径和水平方向的夹角,或者根据水平方向和竖直方向的速度差。 - 外部滑动方向和内部滑动方向一致
这种情况的滑动冲突,无法根据滑动的角度判断,一般都是根据业务需要来进行判断。 - 上面两种情况的结合
这种情况比较复杂,一般也需要从业务上找到突破点。
场景一
场景一:假如打算做个像ViewPager一样的效果,父容器如horizonal方向的LinearLayout一样,容纳了三个ListView。
外部拦截法
就是指点击事件都先经过父容器的拦截处理,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。外部拦截法需要重写父容器的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; //注解1
if (!mScroller.isFinished()){ //注解2
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE: //注解3
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYintercept;
if (Math.abs(deltaX) > Math.abs(deltaY)){ //在这里的if中加入是否拦截的判断
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = fasle; //注解4
break;
default:
break;
}
mLastXIntercept = x;
mLastYintercept = y;
return intercepted;
}
注解1:
为什么要在ACTION_DOWN时,intercepted = false
?因为如果在ACTION_DOWN时,intercepted == true
,那么根据Android事件分发机制,后面的MOVE事件和UP都会无条件的交给父容器去处理。这样的话,事件永远无法传递给子View。
此处的困惑:如果仅仅在父容器中设置ACTION_DOWN时,intercepted = false
还是不够的。intercepted = false
,然后DOWN事件传递给了子View,按照Android事件分发机制,如果子View没有成功处理DOWN事件(即返回了false),最终还是会调用父容器的处理方法。如果这样的话,后面的MOVE事件和UP依然会无条件的交给父容器去处理。
注解2:
这个if内的语句,针对的是下面的情况:如果用户此时在进行父容器的滑动方向(这里是水平滑动),但是在水平滑动之前如果用户再迅速进行竖直滑动,就会导致界面在水平方向无法滑动到终点从而处于一种中间状态。为了避免这种情况,当水平滑动时,下一个序列的点击事件仍然交给父容器处理(哪怕竖直方向滑动距离大于水平方向滑动距离,此时仍然判定是水平滑动)。
注解3:
这一块代码是解决滑动冲突的关键。在MOVE事件中,判断这一滑动事件是水平滑动还是竖直滑动,如果是水平滑动,作为父容器就拦截事件,如果是水平滑动,就不拦截事件,交给子view去处理。
此处的困惑:如果在一系列的MOVE事件中,前部分是水平移动,后部分是竖直移动的,那怎么办?因为没有拦截DOWN事件,所有很有可能事件拦截过程中的mFirstTouchTarget != null
,所以后部分的MOVE事件仍然要调用onInterceptTouchEvent()
,此时,intercept = false;
,那么接着交给子View处理?
如何解答这个困惑呢?有一个结论是:
一旦父容器开始拦截任何一个事件,那么后续的事件都会交给它来处理。
这个结论先记住吧,暂时还没有搞明白为什么会这样。
注解4:
如果父容器在UP事件中返回了true,就会导致子View无法接受到UP事件,这个时候子元素中的onClick
事件就无法处罚法。同样的,
因为一旦父容器开始拦截任何一个事件,那么后续的事件都会交给它来处理,所以UP作为最后一个事件也必定可以传递给父容器,即便父容器的
onInterceptTouchEvent
方法在UP时返回了false
但是依然没有弄明白为什么有这个结论。
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这种方法需要配合requestDisallowInterceptTouchEvent()
方法才能正常工作。
第一步,修改父容器的onInterceptTouchEvent()
,让其在DOWN事件返回false,其他情况下返回true。
//父容器内
public boolean onInterceptTouchEvent(MotionEvent event){
int x = (int)event.getX();
int y = (int)event.getY();
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN){ //注解5
mLastX = x;
mLastY = y;
if (!mScroller.isFinished()){
mScroller.abortAnimation()
return true;
}
return true;
} else {
return true;
}
}
注解5:
父容器拦截了除了DOWN事件以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)
方法时,父元素才能继续拦截所需的事件。
第二步,修改子元素的dispatchTouchEvent()
方法
//在子View中
public boolean dispatchTouchEvent(MotionEvent event){
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
XXX.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY)){ //在这里的if中加入是否拦截的判断
XXX.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event); //注解6
}
注解6:因为子view是自定义view,重写的dispatchTouchEvent()
方法,在解决了滑动冲突后,调用父类的dispatchTouchEvent()
方法来进行原来的事件分发。
场景二和场景三
在总体的实现方法和场景一是一样的,仅仅是在MOVE事件中判断的条件不一样,场景一仅仅是通过滑动方向来进行判断,而场景二和场景三需要判断业务逻辑。这里就不详细介绍了。