Android事件分发机制
View事件分发机制是指Android对MotionEvent事件从产生到被消耗掉的整个处理过程。
MotionEvent即点击事件,当手指接触屏幕后所产生的一系列事件中,最典型的事件类型有以下三种:
- ACTION_DOWN——手指刚接触屏幕;
- ACTION_MOVE——手指在屏幕上移动;
ACTION_UP——手指离开屏幕;
注意在一次完整的点击事件中ACTION_DOWN和ACTION_UP会且只会被触发一次。ACTION_MOVE可能不会被触发(当点击屏幕后松开)或者被多次触发。点击事件的分发由三个很重要的方法来完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面针对这三个方法略作介绍:public boolean dispatchTouchEvent(MotionEvent ev)
用于对事件进行分发。如果事件能被传递给当前View,那么该方法一定会被调用并且是首先被调用,返回值代表当前事件是否被消耗,- public boolean onInterceptTouchEvent(MotionEvent ev)
用于判断是否拦截某个事件,在上述方法的内部被调用。如果View拦截了某个事件,那么在同一事件序列中,此方法不会被再次调用。 - public boolean onTouchEvent(MotionEvent ev)
用于对事件的具体处理,返回值表示是否消耗当前事件。如果onTouchEvent()方法未消耗当前事件,则在同一事件序列中,当前View无法再次接收到事件。
上述三个方法之间的执行顺序及区别可用如下伪代码表示:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consumed = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consumed;
}
点击事件发生时,MotionEvent最先传递给当前Activity,然后经过PhoneWindow(Window的唯一实现类)传递给DecorView,接着DecorView将MotionEvent传递给其父类ViewGroup, 在此期间MotionEvent仅仅是被传递,并无其他实质性操作。MotionEvent传递到ViewGroup之后会经过一系列的处理最终被消耗掉。我们需要关心的就是当MotionEvent来到ViewGroup之后究竟发生了什么?
ViewGroup对MotionEvent的分发过程
当MotionEvent来到ViewGroup,首先会被传递到ViewGroup中的dispatchTouchEvent中。其关键代码如下所示:
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
通过以上代码我们可以直接得出或引申出以下结论:
- ViewGroup会在如下两种情况下判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget != null, 只有个当事件由ViewGroup的子元素处理并且消耗掉时mFirstTouchTarget才会被赋值。
- 这里有一种特殊情况,那就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent方法设置的,一般用于子View中。一旦设置为True后,ViewGroup将无法拦截除了ActionDown之外的其他事件,这是因为ViewGroup在分发事件时,如果是ActionDown就会重置FLAG_DISALLW_INTERCET标记位。因此当面对ACTION_DOWN事件时,Viewgroup总会调用自己的onInterceptTouchEvent方法来询问是否要拦截事件。默认情况下是不拦截的,并且如果ViewGroup一旦拦截ACTION_DOWN那么接下来的一系列事件都会被拦截。ViewGroup拦截事件包含两种情况:第一种是ViewGroup没有子元素,第二种是ViewGroup的所有子元素在dispatchTouchEvent中返回了false。ViewGroup拦截的事件会交个它的父类View进行处理。
- 默认情况下,ViewGroup不会拦截任何事件,它也没有自己的onTouchEvent方法。
具体View对点击事件的处理过程
首先我们先来看View的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event){
boolean result = false;
...
if(onFilerTouchEventForSecurity(event)){
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if(li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) = ENABLED
&& li.mOnTouchListener.onTouch(this, event)){
result = true
}
}
...
if(!result && onTouchEvent(event)){
result = true
}
return result;
}
通过以上代码我们可以直接得出或引申出以下结论:
- View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果onTouchListener中的onTouch方法返回true,那么onTouchEvent方法将不会被调用。可见OnTouchListener的优先级要高于onTouchEvent的优先级。引申一点,在onTouchEvent方法中会调用OnClickListener(如果设置了的话)的onClick方法。所以他们三个方法之间的优先级顺序为:onTouch > onTouchEvent > onClick。
然后我们再来看一下View的onTouchEvent方法对事件的具体处理:
if(((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) = LONG_CLICKABLE)){
swithc (event.getAction()){
case MotionEvent.ACTION_UP:
...
//如果View设置了onClickListener那么performClick方法内部会调用它的onClik方法。
performClick();
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
}
...
return true;
}
通过以上代码可以直接得出或引申出如下结论:
- 只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态。View的LONG_CLICKABLE属性默认是false,而CLICKABLE属性是否为false和true和具体View有关,确切来说可点击的View其CLICKABLE为true,不可点击的View其CLICKABL为false,比如Button是可点击的,TextView是不可点击的。通过setClickable和setLongClickable可以分别改变View的CLICKABLE和LONG_CLICKABLE属性。
View的滑动冲突
外部拦截法
外部拦截法是指点击事件都先经过父容器的拦截处理,如果父事件需要此事件就拦截,如果不需要此事件就不拦截。外部拦截法需要重写父容器的onInterceptTouchEvent方法。这种方法的伪代码如下所示:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
switch(event.getAction()):
case Motion.ACTION_DOWM:
intercepted = false//不拦截
break;
case Motion.ACTION_MOVE:
if(父容器需要当前点击事件){
intertepted = true;
}else{
intercepted = false;
}
break;
case Motion.ACTION_UP:
intercepted = false;
break;
return intercepted;
}
上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需要修改也不能修改。在onTerceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,否则父容器一旦拦截ACTION_DOWM,那么后续所有事件都会交给父容器处理,这个时候就没法在向下传递了。同时ACTION_UP这个事件,父容器也必须返回false,这是因为如果父容器拦截了ACTION__UP,那么其子元素的onClick事件就无法触发。(onClick事件是在View的onTouchEvent方法中被触发的)。
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器进行处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event){
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要此类点击事件){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
同时父元素所做修改如下所示:
public boolean onIntercptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
——2016年6月14日15:03:55
——写于宿舍
——雨