事件分发机制
事件分发机制是 Android 开发者的必备技能,关于这方面的博客也有很多,最近准备夯实一下自己的基础知识,整理一下零散的知识点,为突破做准备,所以再来梳理一下事件分发机制。
大多介绍事件分发机制的文章都只叙述了 View 与 ViewGroup 的事件分发机制,我这里将从 Activity 开始介绍。
一般来说,事件传递的顺序是:Activity -> ViewGroup -> View。
那么我们先说 Activity。
Activity 中的事件分发机制
Activity 中包含两个事件分发与处理的方法:
//事件分发
public boolean dispatchTouchEvent(MotionEvent ev);
//事件消费
public boolean onTouchEvent(MotionEvent event);
我们知道,事件最先是传到 Activity 中,然后在其内部分发之后再传递给ViewGroup 。也就是说 ViewGroup 中的事件上游其实是 Activity,但实际上,ViewGroup 事件的上游不仅仅可以是 Activity,还可以是 Dialog,更进一步的说:ViewGroup 事件的上游是一个 Window。
我这里只介绍 Activity 中的事件分发机制,其它的有兴趣可以自行阅读源码。
首先,Activity 中的事件同样起源于 dispatchTouchEvent 方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
第一步是判断事件如果是点击事件,则调用 onUserInteraction 方法,该方法的方法体为空,我们可以重写这个方法,用于处理一些交互问题。
第二步是调用了 Window 的 superDispatchTouchEvent 方法,如果返回 true,则表示此事件已被消费,结束此次分发流程,false 则继续调用该 Activity 的 onTouchEvent 方法处理该事件。
再来说 superDispatchTouchEvent 这个方法是什么,我们知道 Window 是个抽象类,具体实现为 PhoneWindow,我们看看 PhoneWindow 中的这个方法的实现:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
其中的 MDecor 指的是 DecorView, DecorView 是整个视图树的根节点视图,也就是说 Activity 的事件被传到了视图树的根节点,我们来看看 DecorView 中的 superDispatchTouchEvent 方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
只是调用了父类的 dispatchTouchEvent 方法,它的父类是 FrameLayout,此时的事件已经传递到了视图树,开始通过 ViewGroup 的逻辑进行分发,事已至此,一切都很明确了,Activity 已经将事件传递给了 ViewGroup。
总结一下,事件进入 Activity#dispatchTouchEvent 开始分发,首先会将该事件传递给该页面的 ViewGroup,ViewGroup 如果消费了该事件,则分发结束,未消费则继续调用 Activity#onTouchEvent 方法处理事件,流程如下图:
ViewGroup 中的事件分发机制
ViewGroup 中包含三个事件分发相关的方法:
//事件分发
public boolean dispatchTouchEvent(MotionEvent ev);
//事件拦截
public boolean onInterceptTouchEvent(MotionEvent ev);
//事件消费
public boolean onTouchEvent(MotionEvent event);
事件首先会进入 dispatchTouchEvent 方法开始分发,该方法通过调用 onInterceptTouchEvent 方法判断是否需要拦截事件。
如果需要拦截(方法返回 true),则表示当前 ViewGroup 希望处理该事件,或者不希望子 View 处理该事件,此时将直接调用 onTouchEvent 方法。
如果不需要拦截,则将该事件传递给子 View 的 onTouchEvent 方法,或者子 ViewGroup 的 dispatchTouchEvent 方法。
现在再来说 onTouchEvent 方法, 该方法用于消费事件,返回值表示是否已消费,在两种情况下会被调用:onInterceptTouchEvent 方法确认需要拦截该事件以及子 View/ViewGroup 未消费该事件。
如果该方法返回 true,则表示事件已消费,此次事件分发就成结束;
如果返回 false,则表示未消费该事件,会继续调用父 ViewGroup 或 Activity 的 onTouchEvent 方法。
对于 ViewGroup 来说,事件拦截与事件消费是两个概念。
事件拦截对应 onInterceptTouchEvent 方法,返回 true 表示拦截该事件。
事件消费对应 onTouchEvent 方法,返回 true 表示消费此事件。
拦截事件表示该事件将不会向下传递,也就是说不会传递给子 View。
消费事件表示该事件将不会向上传递,也就是说不会传递给父 ViewGroup 的 onTouchEvent 方法。
我们可以拦截但不消费某个事件,同样,可以也消费但不拦截某个事件。
其分发流程如下图所示:
View 中的事件分发机制
View 中包含如下两个用于处理事件分发相关的方法:
//事件分发
public boolean dispatchTouchEvent(MotionEvent event);
//事件消费
public boolean onTouchEvent(MotionEvent event);
ViewGroup 会将事件传递给 dispatchTouchEvent 方法,由于不是 ViewGroup,不需要考虑向下传递事件的逻辑,所以 View 中的事件处理流程很简单。
抛开 dispatchTouchEvent 中其它的代码来看,其中主要是用来调用 onTouchEvent 方法,并返回 onTouchEvent 的返回值,流程如下:
上述便是 Android 事件机制的完整流程,实际上事件分发机制还有很多要处理的东西,我这里只是介绍了一些重点及关键点。
下面我来用一张图总结一下:
其他
另外还有一些需要注意点的点。
除了上面说的这几个事件分发相关的方法外,我们经常会调用 View#setOnTouchListener 来处理事件,例如下面这样:
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
当我们调用了这个方法后,dispatchTouchEvent 会优先调用 OnTouchListener#onTouch 方法判断是否消费该事件,不消费才会继续调用 onTouchEvent 方法。
还有一点,我们设置 OnTouchListener 时 IDE 会提示一个警告,或者我们直接重写 View#OnTouchEvent 方法时同样会提示这个警告:
Custom view has setOnTouchListener called on it but does not override performClick
提示我们没有重写 performClick 方法,那我重写之后还是有警告,不过这次变成了这样:
onTouchEvent should call performClick when a click detected
意思就是由于我们重写了 onTouchEvent 方法,可能会导致设置 setOnClickListener 方法失效,我们需要在合适的时候去调用 performClick 方法,我们可以这样:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(TAG, "ViewGroup->onTouchEvent:" + event.toString());
if(event.getAction() == MotionEvent.ACTION_DOWN){
return performClick();
}
return super.onTouchEvent(event);
}
@Override
public boolean performClick() {
return super.performClick();
}