Android事件分发机制:
1.MotionEvent概念
在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
①ACTION_DOWN:手指刚接触屏幕
②ACTION_MOVE:手指在屏幕上移动
③ACTION_UP:手指从屏幕上松开的一瞬间
④ACTION_CANCEL:当前手势已终止
正常情况下,一次手指触摸屏幕的行为会触发上述事件①②③所组成的一系列事件。
当子View消费事件的过程中,父View突然进行拦截,则子View会收到ACTION_CANCEL这个事件。
2.View的事件分发机制
所谓的事件分发,就是指对MotionEvent事件的分发过程。当一个MotionEvent产生了后,系统需要把这个事件传递给一个具体的View来处理,这个传递过程就是分发过程。其中有三个重要的方法dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。
public boolean dispatchTouchEvent(MotionEvent event)
用来进行事件的分发。如果此事件能传递到当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法内部调用,用于判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示了是否拦截当前事件。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件序列中,当前View无法再次接收到事件。
如下一段伪代码可以表示三个函数之间的关系:
//事件分发给当前View
public boolean dispatchTouchEvent(MotionEvent e){
boolean consume = false;
//做拦截判断
if(onInterceptTouchEvent(e)){
//拦截该事件,然后处理该事件
consume = onTouchEvent(e);
}else{
//未拦截该事件,事件被分发给子View
consume = child.dispatchTouchEvent(e);
}
return consume;
}
事件分发大致流程:
当一个点击事件产生后,它会由Activity传递到PhoneWindow,PhoneWindow再传递到DecorView即顶级View。顶级View(一般是ViewGroup)接收到事件后,就开始进行事件分发。首先,顶级View的dispatchTouchEvent方法会被调用,如果这个View的onInterceptTouchEvent方法返回true,则表示它要拦截这个事件,那么此事件就会交由它处理,即调用它的onTouchEvent方法来处理事件;如果这个View的onInterceptTouchEvent方法返回false,则表示它不拦截这个事件,这时当前事件就会通过遍历的方式继续传递给它的所有子View,对应的子View的dispatchTouchEvent方法就会被调用。就这样一直传递到事件被处理为止。特殊情况:如果一个View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法将会被调用,以此类推,如果所有元素都不处理这个事件,最终事件会回传到Activity进行处理。
事件处理大致流程:
当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会被调用。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。所以事件处理的优先级onTouchListener的onTouch高于onTouchEvent高于onClick。
Tips:
①某个View一旦决定拦截,那么这一整个事件序列都只能由它来处理,并且它的onInterceptTouchEvent方法不再被调用。
②ViewGroup默认不拦截任何事件。
③View没有onInterceptTouchEvent方法,一旦有事件传给它,那么它就一定会执行。
④View的onTouchEvent默认会消费事件(返回true),除非它是不可点击的。
滑动冲突解决方案:
了解了上面的事件分发机制后,我们会发现所谓的滑动冲突,无非就是父View与子View之间的事件拦截逻辑未做相应处理所造成的。所以想要解决滑动冲突问题,我们可以从父View和子View的事件拦截的处理逻辑上下手。
滑动冲突的解决方案主要分为两大类:
1. 外部拦截法(拦截的处理逻辑交由父View执行)
2. 内部拦截法(拦截的处理逻辑交由子View执行)
外部拦截法
该方法主要重写父View的onInterceptTouchEvent方法,在内部做拦截的逻辑判断。伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
//从MotionEvent中获取xy坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//一定要返回false,否则子View无法接受到该事件序列的后续任何事件
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
//此处做拦截判断,可根据获得的xy坐标判断用户手指的滑动方向...
if(判断父View是否需要当前事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//一定要返回false,否则子View中无法接收到该事件,可能导致子View的onClick方法无法触发
intercepted = false;
break;
default:
break;
}
//记录本次xy坐标
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
该方法只需重写父View的onInterceptTouchEvent方法,实现起来比较简单,思路也符合Android中的事件分发机制的执行流程。
内部拦截法
内部拦截法主要通过重写子View中的dispatchTouchEvent方法来实现对事件的拦截并进行逻辑判断。为代码如下:
public boolean dispatchTouchEvent(MotionEvent event){
//从MotionEvent中获取xy坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//父View在后续事件序列中不准进行拦截
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
//此处做拦截判断,可根据获得的xy坐标判断用户手指的滑动方向...
if(判断父View是否需要当前事件){
//准许父View对后续事件进行拦截
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
//记录本次xy坐标
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
通过requestDisallowInterceptTouchEvent方法,我们可以动态的设置父View能否被准许拦截后续事件。
为了能在父View在被准许拦截后成功的对后续事件进行拦截,我们还需要对父View的onInterceptTouchEvent方法做重写。伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event){
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
//一定要返回false,否则子View无法接受到该事件序列的后续任何事件
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
default:
intercepted = true;
break;
}
return intercepted;
}
可以看到,通过将除了ACTION_DOWN以外的所有事件都设置返回true,可以保证该次事件一定被父View拦截处理。
以上就是两种解决滑动冲突的常见方案。