Android的事件分发机制是个老生常谈的问题了,网上文章汗牛充栋,但别人的文章毕竟只是『参考』,基本不可能和自己的理解完全重合,所以对于这些老问题还是得自己理一遍。希望我的这篇文章也能给读者一点启发,击中读者在理解中的某个小盲区。
事件与事件序列:
用户点击(触摸)一下屏幕,最少包含一个Down和一个Up事件,中间还可能包含着若干个Move。即每次点击,触发的都不是一个事件,而是一个事件序列。一个事件序列的触发完成(以Up结尾)会需要不定长度的时间,系统不可能等事件全部触发完毕再处理,而是每触发一个事件就处理一个事件,这样系统对用户操作的响应才是及时的。事件分发不仅要了解单个事件在View中的传递路径,还要了解当前事件的处理是如何影响后续事件的分发的。
很明显,如果一个事件序列中的某个事件在ViewA中被处理了,那后续的事件我们肯定也需要ViewA来处理,即一个事件序列必定在同一个View中得到处理,不会出现同一个事件序列中的事件被不同的View处理掉。
最先捕获到事件的是Activity,事件从Activity传到Window再经过顶级View传到一个View,这是事件在类中的传递路线。假设现在一个事件已经传到了一个View这里,即该事件没有被它的上一级ViewGroup拦截,看看它在View中会经过怎样的路线。
对于一个非ViewGroup的View来说,View的dispatchTouchEvent的方法是第一个接收到事件的方法。进入该方法经过一些初始化工作,判断如果View是enable的,就开始检查OnTouchListener。
如果设置了OnTouchListener的话,这个时候会调用OnTouchListener的onTouch方法。如果onTouch方法返回true,则导致onTouchEvent方法得不到调用,并最终让dispatchTouchEvent方法返回true,该事件到此分发完毕。
如果onTouch返回false,即在onTouch中不消费事件,则事件会继续传给onTouchEvent方法,在这个方法中,会展开一系列判断。
2.1 如果控件是disabled,并且控件是可点击的,那么直接返回true消费掉事件。这种情况常发生在点击灰色按钮时。此时我们希望用户的点击是针对按钮的,但是不给他任何效果。那么最直接的方式就是让按钮接收事件并直接消费掉而不运行任何逻辑。
2.2 如果控件没disabled,并且控件可点击, 如果发现这个事件和之前的事件组合成了click事件,并且控件是可点击的,则会触发performClick方法。并且会让很自然的,触发了该方法中说明检测到了用户的一次点击,那么会检查有没有设置OnClickListener,如果设置了,则去触发其onClick方法。但不管有没有设置OnClickListener,只要控件可点击,最终onTouchEvent方法都会返回true。
2.3 如果这个控件是不可点击的,则不管控件时否disabled,onTouchEvent方法都会返回false,而onTouchEvent方法返回false会导致dispatchTouchEvent方法返回false。也就会让这个View不再接受该事件序列的后续事件。同时它的父类会发现它的dispatchTouchEvent返回了false,因为它的dispatchTouchEvent方法就是在父类中调用的,父类当然会知道它的返回值。当父类知道它无法处理时,便会自己去处理这个事件。
从上面的路线中可以看到,当一个事件传到View时,只要它消费了事件(两种途径,1是onTouch返回true,二是控件可点击),都会使dispatchTouchEvent方法最终返回true。而dispatchTouchEvent返回true,意味着包含该事件的事件序列要在本View中处理。下面会谈到为什么dispatchTouchEvent返回true就意味着该事件所在的事件序列中的其他事件都要在本View中处理。
关于控件的clickable和enable
clickable:如果一个控件是无法点击的,即clickable和longclick都未false,那么事件就不要派发给它,因为它不可点击,也就不应该接受点击事件。
enable:如果一个控件是enable的,并且事件派发给它了,那么当它在处理事件时应该给出一些响应。如果它是disable的,并且事件派发给它了,那么它应该不给出任何响应而直接消费掉。注意是消费掉,不是向上返回false。
一句话总结:clickable决定了控件是否有权消费事件,enable决定了控件怎样消费事件。
View的dispatchTouchEvent方法(经过部分删减)
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;//注意这个标志位,下面的操作会影响其值
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//在这里开始检查是否有OnTouchListener
//如果onTouch方法返回true,则会将result置true,导致下面的onTouchEvent方法进不去,这也就是为什么onTouch方法返回true之后OnClickListener得不到调用的原因
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//调用onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
对于ViewGroup来讲,首先接收到事件的也是dispatchTouchEvent方法,但是,这个dispatchTouchEvent方法被重写了,和View的dispatchTouchEvent方法完全不一样。事件在ViewGroup中传递时会经过onInterceptTouchEvent方法,而且不会onTouchEvent方法。
事件进入到dispatchTouchEvent中,会首先检查该ViewGroup是否要拦截该View,即事件会派发给onInterceptTouchEvent。在这里遇到第一个分支:
ViewGroup的dispatchTouchEvent(大部分删减)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查是否拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//disallowIntercept是是否禁止拦截的标志位
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//该方法检查是否拦截,默认返回false
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//注意这个超大的if语句,如果之前被拦截掉了,这个if语句进不去
if (!canceled && !intercepted) {
//具体逻辑省略,如果进到这里,会对View进行遍历,找到相应的子View,最终会调用dispatchTransformedTouchEvent方法并把子View传进去
}
// 如果该ViewGroup拦截了事件,则跳过上面的if语句,从这里开始执行
if (mFirstTouchTarget == null) {
//依然调用了dispatchTransformedTouchEvent方法,但第三个参数View传入了null,这很关键,待会看下面dispatchTransformedTouchEvent方法的源码
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else {
...
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
ViewGroup的dispatchTransformedTouchEvent方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
//关键在这里,根据child是否为null来判断这个事件是分发给子view还是自己消费
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
// Done.
transformedEvent.recycle();
return handled;
}
当一个View对象作为ViewGroup时,它不具备处理事件的能力,而只具备传递事件的能力。如果它想要自己处理事件,即拦截该事件,那么此时该View就不再是一个ViewGroup,而是一个View。所以事件传递要分为两部分去理解,一是事件在View中是如何被消费的,二是事件在ViewGroup中是如何传递的。
参考:
http://blog.csdn.net/guolin_blog/article/details/9097463
http://blog.csdn.net/guolin_blog/article/details/9153747
http://blog.csdn.net/yanbober/article/details/45912661