Android菜鸟一枚,做项目的时候经常碰到滑动冲突,于是痛下狠心学了一下事件分发机制,并且通过翻看源码,稍有心得。
事件分发的基础
说到事件的分发机制,不得不提到三个方法:
dispatchTouchEvent(MotionEvent ev);
onInterceptTouchEvent(MotionEvent ev);
onTouchEvent(MotionEvent event);
在ViewGroup中三个方法全部存在,而在View中,不存在onInterceptTouchEvent(MotionEvent ev);
当点击事件发生的时候,事件首先会传给activity,此时activity的dispatchTouchEvent(MotionEvent ev)方法执行。然后事件传给window,通过window将事件传给顶级view。接下来就是我们的重点了。
我们都知道view是放在ViewGroup中的,而子ViewGroup又是放在父ViewGroup中,这样一层一层就形成了View树,当一个事件发生的时候,应该交给谁来处理呢?
有必要了解一下上面三个方法了:
《Android开发艺术探索》上有一段伪代码把这三个方法的关系表现的淋漓尽致。
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
dispatchTouchEvent(MotionEvent ev): 如果事件能传到这里的话,该方法一定会被调用,用于对事件进行分发。
onInterceptTouchEvent(MotionEvent ev): 如果事件能传到该view,当action为ACTION_DOWN的时候,该方法一定执行,用来询问要不要拦截,如果确定拦截的话,事件传到该view后便不会再往下传,并且之后的一系列事件便不会走该方法;如果该View处理事件的话(ouTouchEvent返回值为true),那么之后的一系列事件都会交给该view处理;
onTouchEvent(MotionEvent event):该方法是大家在自定义控件中最经常重写的方法,用于对事件进行处理。如果返回true的话,那么down事件及其之后的一系列事件都交给所在的view执行;如果该view不处理的话,事件再上传给父View的onTouchEvent方法,而且我测试后发现这种情况下,之后的一系列方法竟然不会再传到该View了,而是传给那个处理事件的View。
假设:ViewGroupFather—>ViewGroupChild—>View
那么首先毫无疑问,事件会传给ViewGroupFather,dispatchTouchEvent(MotionEvent ev)首先执行。事件传递的流程如下所示:
值得注意的是:如果一个View设置了OnTouchListener,OnTouchListener.onTouch()将会执行。伪代码如下:
if (mOnTouchListener!=null){
if (!mOnTouchListener.onTouch(view, event)){
onTouchEvent(event);
}
}else {
onTouchEvent(event);
}
如果还设置有点击监听OnClickListener,将在onTouchEvent(event)的case ACTION_UP:case MotionEvent.ACTION_UP:{}中执行performClick()方法。从下面的代码中,我们可以看到点击事件的执行了。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
...
return result;
}
重要的都在dispatchTouchEvent(MotionEvent ev)中,应该好好看看该方法的代码。
VewGroup的dispatchTouchEvent方法
从该方法的开始看,有以下代码
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
上面代码中我们来看resetTouchState()方法,该方法中调用了clearTouchTargets(),而在这个方法中有这么一句代码mFirstTouchTarget = null,对mFirstTouchTarget 清空。
接下来继续看dispatchTouchEvent中的代码:
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;
}
在上面代码中,如果进入if语句块,需要满足两个条件actionMasked == MotionEvent.ACTION_DOWN和mFirstTouchTarget != null,那么当事件为down时候,肯定会走这个语句块。当事件为move和up的时候就不好说了,这个时候需要看mFirstTouchTarget != null成不成立,上文知道当action为down的时候为null,那么它什么时候赋值呢?由下文可以知道,当ViewGroup的子元素处理事件的时候,mFirstTouchTarget会被赋值。也就是说如果当前ViewGroup拦截该事件,那么onInterceptTouchEvent(ev)不再被调用,之后的一系列事件都会交给该ViewGroup处理。
但是,从代码中我们可以看出来,当前ViewGroup如果想要走onInterceptTouchEvent(ev),还需要disallowIntercept为false,也就是FLAG_DISALLOW_INTERCEPT为false。这个标记位平常很少用到,但是requestDisallowInterceptTouchEvent()方法我们经常在ViewGroup中用到,这个方法便是设置mFirstTouchTarget的。
如果当前ViewGroup不拦截事件,将会走dispatchTouchEvent的以下代码:
for (int i = childrenCount - 1; i >= 0; i--) {
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
上面代码,我只是挑出的一些重点,并不是完整的代码。
首先对ViewGroup中的子view进行遍历,如果不符合条件的就直接continue了,不再往下走了。怎么才是符合条件呢?canViewReceivePointerEvents是判断子元素是否在播放动画,isTransformedTouchPointInView是判断点击的事件坐标是否落在了子元素的区域内。
我们点进去看一下dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
当然此时child不为空,将会走孩子的dispatchTouchEvent方法,并且在addTouchTarget(child, idBitsToAssign)中对mFirstTouchTarget进行赋值,跳出循环。
如果所有子元素都没有处理,那么接着走dispatchTouchEvent以下代码:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
因为子元素没有处理,所以mFirstTouchTarget == null,进入dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS)。这个方法的代码已经贴在上面,此时的child为null。会走super.dispatchTouchEvent(event)。这里就转到了View的dispatchTouchEvent,而在View的dispatchTouchEvent中,不再分发事件,也没有调用onInterceptTouchEvent(MotionEvent ev)方法的情况,而是走的OnTouchEvent。该部分在上文也已经提到,看看代码就明白了:
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;
}