主要介绍内容:
在上一章中我们已经介绍了 View 的基础知识以及 View 的滑动,想了解的请戳 Android——View的事件体系(一)View的滑动
这里本节将介绍 View 的一个核心知识点:事件分发机制。在真正进入详细探讨之前,大家可以先去瞅下这篇博文 Android事件分发完全解析之为什么是她 来找到自己要学习 View 事件的出发点。
下面我们开始进入今天的正题
在介绍点击事件的传递规则之前,首先我们要明白这里要分析的对象就是 MotionEvent,关于 MotionEvent 我们在上一章中已经进行了较为详细的介绍,有兴趣的可以回头去瞅下。所谓点击事件的事件分发,其实就是对 MotionEvent 事件的分发过程,即当一个 MotionEvent 产生了以后,系统需要吧这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。点击事件的分发过程由三个很重要的方法来共同完成; dispatchTouchEvent、onInterceptTouchEvent 和 onTouchEvent, 下面我们先来介绍下这几个方法:
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会调用,返回结果受当前 View 的 onTouchEvent 和 下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
public boolean onInterceptTouchEvent(MotionEvent ev)
在上诉方法内部中调用,用来判断师傅拦截某个事件,如果当前 View 拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
public boolean onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。
上述的三个方法其实我们可以使用伪代码的形式来描述下它们之间的关系,伪代码如下:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
上述提供的伪代码已经将 三者之间的关系表现的淋漓尽致。通过上面的伪代码,我们也可以大致了解点击事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这是它的 dispatchTouchEvent 就会被调用,如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 true 就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 进行处理,即它的 onTouchEvent 方法就会被调用;如果这个 ViewGroup 的 onInterceptTouchEvent 方法返回 false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的 dispatchTouchEvent 方法就会被调用,如此反复直到事件被最总处理。
当一个View需要处理事件时,如果它设置了 onTouchListener, 那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回 false,则当前 View 的 onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。由此可见,给 View 设置的 OnTouchListener ,其优先级比 onTouchEvent 要高,在 onTouchEvent 方法中,如果当前设置的有 OnClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 OnClickListener,其优先级最低,即处于事件传递的尾端。
想要验证上面结论的真实性的可以参考这两篇博文:
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity -> Window -> View,即事件总是先传递给 Activity,Activity 再传递给 Window,最后 Window 在传递给顶级 View。顶级 View 在接收到事件后,就会按照事件分发机制去分发事件。考虑这么一种情况,如果一个 View 的 onTouchEvent 返回 false,那么它的父容器的 onTouchEvent 将会被调用,依此类推。如果所有的元素都不处理这个事件,那么这个事件将会最终传递给 Activity 进行处理,即 Activity 的 onTouchEvent 方法会被调用。这个过程其实也很好理解,我们可以换一种思路,假如点击事件是一个难题,这个难题最终被上级领导分给了一个程序员去处理(这是事件分发过程),结果这个程序员搞不定(onTouchEvent返回了false),现在该怎么办呢?难题必须要解决,那只能交给水平更高的上级解决(上级的 onTouchEvent 被调用),如果上级在搞不定,那就只能交给上级的上级去解决,就这样将难题一层层向上抛,这是公司内部一种很常见的处理问题的过程。从这个角度来看,View 的事件传递过程还是很贴近现实的,毕竟程序员也生活在现实中。
下面我们来通过一个例子来描述一下 View 事件拦截在生活中的体现:假设你所在的公司,有一个总经理,级别最高;他下面有一个部长,级别次之;最底层,就是干活的你,没有级别。现在董事会交给总经理一项任务,总经理将这项任务布置给了部长,部长又把任务安排给了你。而当你好不容易干完活了,你就把任务交给部长,部长觉得任务完成得不错,于是就签上他的名字交给总经理,总经理看了也觉得不错,就也签了名字交给董事会。这样,一个任务就顺利完成了。如果大家能非常清楚地理解这样一个场景,那么对应事件拦截机制,你就已经基本入门了。下面我们通过代码来模拟一下该场景:
一个总经理 —— MyViewGroupA,最外层的 ViewGroup
一个部长 —— MyViewGroupB,中间的 ViewGroup
一个干活的你 —— MyView,在最底层
对于 MyViewGroupA 和 MyViewGroupB来说,我们通过继承 LinearLayout 来实现,重写如下所示的三个方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "ViewGroupA dispatchTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "ViewGroupA dispatchTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "ViewGroupA dispatchTouchEvent————up");
break;
}
boolean flag = super.dispatchTouchEvent(ev);
// Log.e("mk", "ViewGroupA dispatchTouchEvent======" + flag);
return flag;
// return false;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "ViewGroupA onInterceptTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "ViewGroupA onInterceptTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "ViewGroupA onInterceptTouchEvent————up");
break;
}
boolean flag = super.onInterceptTouchEvent(ev);
// Log.e("mk", "ViewGroupA onInterceptTouchEvent======" + flag);
return flag;
// return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "ViewGroupA onTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "ViewGroupA onTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "ViewGroupA onTouchEvent————up");
break;
}
boolean flag = super.onTouchEvent(event);
// Log.e("mk", "ViewGroupA onTouchEvent======" + flag);
return flag;
// return false;
}
而对于 MyView 来说,我们通过继承 View来实现,重写如下所示的两个方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "MyView dispatchTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "MyView dispatchTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "MyView dispatchTouchEvent————up");
break;
}
boolean flag = super.dispatchTouchEvent(ev);
// Log.e("mk", "MyView dispatchTouchEvent======" + flag);
return flag;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("mk", "MyView onTouchEvent————down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("mk", "MyView onTouchEvent————move");
break;
case MotionEvent.ACTION_UP:
Log.e("mk", "MyView onTouchEvent————up");
break;
}
// boolean flag = super.onTouchEvent(event);
// Log.e("mk", "MyView onTouchEvent======" + false);
return false;
}
从上面的代码中可以看到, ViewGroup 级别比较高,比 View 多了一个方法——onInterceptTouchEvent()。这个方法我们前面已经介绍过是用来做拦截事件用的。我们来看一下输出的log:
09-28 00:28:22.360 12206-12206/? E/mk: ViewGroupA dispatchTouchEvent————down
09-28 00:28:22.360 12206-12206/? E/mk: ViewGroupA onInterceptTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: ViewGroupB dispatchTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: ViewGroupB onInterceptTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: MyView dispatchTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: MyView onTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: ViewGroupB onTouchEvent————down
09-28 00:28:22.361 12206-12206/? E/mk: ViewGroupA onTouchEvent————down
可以看见,正常情况下,事件的传递顺序是:
总经理(MyViewGroupA) ——> 部长(MyViewGroupB) ——> 你(MyView)。事件传递的时候,先执行 dispatchTouchEvent 方法,再执行 onInterceptTouchEvent 方法。
事件的处理顺序是:
你(MyView) ——> 部长(MyViewGroupB) ——> 总经理(MyViewGroupA)。事件的处理都是执行 onTouchEvent 方法。
事件传递的返回值非常容易理解: true,拦截,不继续;false,不拦截,继续流程。
事件处理的返回值也类似:true,处理了,不用上级审核;false,未处理,交由上级处理。
初始情况下,返回值都是false。
这里为了能够方便大家理解事件拦截的过程,在事件传递过程中,我们只关心 onInterceptTouchEvent 方法,而 dispatchTouchEvent 方法虽然是事件分发的第一步,但一般情况下,我们不太会去改写这个方法,所以暂时不去管这个方法。可以把上面的整个事件过程整理成一张流程图,如下所示:
下面我们稍微改动下代码,假设总经理(MyVIewGroupA)发现这个任务太简单了,觉得自己完全可以顺手完成,完全没有麻烦下属。因此事件就被总经理(MyViewGroupA)使用 onInterceptTouchEvent 方法把事件给拦截了,即让 MyViewGroupA 的 onInterceptTouchEvent 方法返回true,我们再来看下log:
09-28 00:55:02.313 12531-12531/? E/mk: ViewGroupA dispatchTouchEvent————down
09-28 00:55:02.313 12531-12531/? E/mk: ViewGroupA onInterceptTouchEvent————down
09-28 00:55:02.313 12531-12531/? E/mk: ViewGroupA onTouchEvent————down
是不是跟我们想象的一样,因为这里我们仅仅是让总经理(MyViewGroupA)拦截了事件,但并没有去消费事件,所以看不到 move 和 up 事件,如果我们向看到 move 和 up 事件也很简单,仅仅需要在总经理(MyViewGroupA)的 onTouchEvent 方法中返回 true 即可,看下log:
09-28 00:56:00.181 12857-12857/? E/mk: ViewGroupA dispatchTouchEvent————down
09-28 00:56:00.181 12857-12857/? E/mk: ViewGroupA onInterceptTouchEvent————down
09-28 00:56:00.181 12857-12857/? E/mk: ViewGroupA onTouchEvent————down
09-28 00:56:00.209 12857-12857/? E/mk: ViewGroupA dispatchTouchEvent————move
09-28 00:56:00.209 12857-12857/? E/mk: ViewGroupA onTouchEvent————move
09-28 00:56:00.291 12857-12857/? E/mk: ViewGroupA dispatchTouchEvent————up
09-28 00:56:00.291 12857-12857/? E/mk: ViewGroupA onTouchEvent————up
既然总经理(MyViewGroupA)都可以自己处理,那么部长也可以自己处理吧,下面我们就把部长(MyViewGroupB)的 onInterceptTouchEvent 方法改为 true,看下效果:
09-28 19:57:40.497 21762-21762/? E/mk: ViewGroupA dispatchTouchEvent————down
09-28 19:57:40.498 21762-21762/? E/mk: ViewGroupA onInterceptTouchEvent————down
09-28 19:57:40.498 21762-21762/? E/mk: ViewGroupB dispatchTouchEvent————down
09-28 19:57:40.498 21762-21762/? E/mk: ViewGroupB onInterceptTouchEvent————down
09-28 19:57:40.498 21762-21762/? E/mk: ViewGroupB onTouchEvent————down
09-28 19:57:40.498 21762-21762/? E/mk: ViewGroupA onTouchEvent————down
可以看到,其实结果应该和我们想象的一样,下面我们就给出上面两个例子的执行图:
总经理(MyViewGroupA) 拦截事件:
部长(MyViewGroupB)拦截事件:
就剩下最后一个了,就是你自己消费事件,将 MyView 的 onTouchEvent 方法返回 true,看log输出:
09-28 20:09:37.358 22340-22340/? E/mk: ViewGroupA dispatchTouchEvent————down
09-28 20:09:37.358 22340-22340/? E/mk: ViewGroupA onInterceptTouchEvent————down
09-28 20:09:37.359 22340-22340/? E/mk: ViewGroupB dispatchTouchEvent————down
09-28 20:09:37.359 22340-22340/? E/mk: ViewGroupB onInterceptTouchEvent————down
09-28 20:09:37.359 22340-22340/? E/mk: MyView dispatchTouchEvent————down
09-28 20:09:37.359 22340-22340/? E/mk: MyView onTouchEvent————down
09-28 20:09:37.395 22340-22340/? E/mk: ViewGroupA dispatchTouchEvent————move
09-28 20:09:37.395 22340-22340/? E/mk: ViewGroupA onInterceptTouchEvent————move
09-28 20:09:37.395 22340-22340/? E/mk: ViewGroupB dispatchTouchEvent————move
09-28 20:09:37.395 22340-22340/? E/mk: ViewGroupB onInterceptTouchEvent————move
09-28 20:09:37.395 22340-22340/? E/mk: MyView dispatchTouchEvent————move
09-28 20:09:37.395 22340-22340/? E/mk: MyView onTouchEvent————move
09-28 20:09:37.467 22340-22340/? E/mk: ViewGroupA dispatchTouchEvent————up
09-28 20:09:37.467 22340-22340/? E/mk: ViewGroupA onInterceptTouchEvent————up
09-28 20:09:37.467 22340-22340/? E/mk: ViewGroupB dispatchTouchEvent————up
09-28 20:09:37.468 22340-22340/? E/mk: ViewGroupB onInterceptTouchEvent————up
09-28 20:09:37.468 22340-22340/? E/mk: MyView dispatchTouchEvent————up
09-28 20:09:37.468 22340-22340/? E/mk: MyView onTouchEvent————up
可以看到事件传递跟之前的一样,但是事件处理,到你(MyView) 就结束了,因为你消费了事件,不会再向上级传递,同样我们也给出执行图:
这里还需要在说明一种情况,比如说你(MyView)没有消费事件,在事件进行向上传递给部长(MyViewGroupB)的时候,这里可以把事件看做是你向上级(部长)提交的报告,而部长看完你的报告之后觉得写的太烂,觉得太丢人,不敢给他的上级(总经理)看,所以就偷偷地返回了true,整个事件也到此位置了,即部长(MyViewGroupB)将自己的 onTouchEvent 返回 true,log显示如下:
09-28 20:20:55.144 22767-22767/? E/mk: ViewGroupA dispatchTouchEvent————down
09-28 20:20:55.144 22767-22767/? E/mk: ViewGroupA onInterceptTouchEvent————down
09-28 20:20:55.145 22767-22767/? E/mk: ViewGroupB dispatchTouchEvent————down
09-28 20:20:55.146 22767-22767/? E/mk: ViewGroupB onInterceptTouchEvent————down
09-28 20:20:55.146 22767-22767/? E/mk: MyView dispatchTouchEvent————down
09-28 20:20:55.146 22767-22767/? E/mk: MyView onTouchEvent————down
09-28 20:20:55.146 22767-22767/? E/mk: ViewGroupB onTouchEvent————down //因为你(MyView未消费事件,事件向上传递交由部长(MyViewGroupB)进行处理)
09-28 20:20:55.185 22767-22767/? E/mk: ViewGroupA dispatchTouchEvent————move
09-28 20:20:55.186 22767-22767/? E/mk: ViewGroupA onInterceptTouchEvent————move
09-28 20:20:55.186 22767-22767/? E/mk: ViewGroupB dispatchTouchEvent————move
09-28 20:20:55.186 22767-22767/? E/mk: ViewGroupB onTouchEvent————move //从这里开始不在执行你(MyView)的相关事件,是因为你在 down 的时候就没有消费事件,事件不会再传递给你,直接交由部长(MyViewGroupB)进行处理
09-28 20:20:55.275 22767-22767/? E/mk: ViewGroupA dispatchTouchEvent————up
09-28 20:20:55.275 22767-22767/? E/mk: ViewGroupA onInterceptTouchEvent————up
09-28 20:20:55.275 22767-22767/? E/mk: ViewGroupB dispatchTouchEvent————up
09-28 20:20:55.275 22767-22767/? E/mk: ViewGroupB onTouchEvent————up
它们之间的关系图如下所示:
关于事件传递的机制,我们这里先给出一些结论,然后后面会给出一些测试代码,根据这些结论我们可以更好的理解这个传递机制,如下所示:
上面我们已经分析了 View 的事件分发机制,下面我们将从源码的角度去进一步分析、证实上面的结论。
点击事件用 MotionEvent 来表示,当一个点击操作发生时,事件最先传递给当前的 Activity,由 Activity 的 dispatchTouchEvent 来进行事件的派发,具体的工作是由 Activity 内部的 Window 来完成的。Window 会将事件传递给 decor view,decor view 一般就是当前界面的底层容器(即 setContentView 所设置的 View 的父容器)(关于 decor view 在会在后续博客对自定义 View 讲解的过程中进行详细介绍),通过 Activity.getWindow.getDecorView()可以获得。我们先从 Activity 的 dispatchTouchEvent 开始分析。
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
现在分析上面的代码。首先事件开始交给 Activity 所附属的 Window 进行分发,如果返回 true,整个事件循环就结束了,返回 false 意味着事件没人处理,所有 View 的 onTouchEvent 都返回了 false,那么 Activity 的 onTouchEvent 就会被调用。 接下来看 Window 是如果将事件传递给 ViewGroup 的。通过源码我们知道,Window 是个抽象类,而 Window 的 superDispatchTouchEvent 方法也是个抽象方法,因此我们必须找到 Window 的实现类才行。 Window#superDispatchTouchEvent:
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
那么到底 Window 的实现类是什么呢?其实是 PhoneWindow,这一点从 Window 的源码中可以看出来,在 Window 的说明中,有这么一段话:
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
上面这段话的大概意思是: Window 类可以控制顶级 View 的外观和行为策略,它的唯一实现位于 android.policy.PhoneWindow 中,当你要实例化这个 Window 类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。尽管这看起来有点模糊,不过我们可以看一下 android.policy.PhoneWindow 这个类,尽管实例化的时候此类会被重构,仅是重构而已,功能是类似的。 由于 Window 的唯一实现是 PhoneWindow,因此接下来看一下 PhoneWindow 是如何处理点击事件的,如下所示: PhoneWindow#superDispatchTouchEvent:
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
到这里逻辑就很清晰了,PhoneWindow 将事件直接传递给了 DecorView,这个 DecorView 是什么呢? 请看下面:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker{
//This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public final View getDecorView(){
if(mDecor == null){
installDecor();
}
return mDecor;
}
}
我们知道,通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0) 这种方式就可以获取 Activity 所设置的 View,这个 mDecor 显然就是 getWindow().getDecorView() 返回的 View ,而我们通过 setContentView 设置的 View 是它的一个子 View。目前事件传递到了 DecorView 这里,由于 DecorView 继承自 FrameLayout 且是父 View,所以最终事件会传递给 View。话句话来说,事件肯定会传递到 View,不然应用如何响应点击事件呢? 不过这不是我们的重点,重点是事件到了 View 以后应该如何传递,这对我们更有用。从这里开始,事件已经传递到了顶级 View 了,即在 Activity 中通过 setContentView 所设置的 View,另外顶级 View 也叫根 View,顶级 View 一般来说都是 ViewGroup。
关于点击事件如何在 View 中进行分发,上面我们已经做了详细介绍,这里在大致回顾一下。点击事件到达顶级 View (一般是一个 ViewGroup)以后,会调用 View 的 dispatchTouchEvent 方法,然后的逻辑是这样的:如果顶级 ViewGroup 拦截事件即 onInterceptTouchEvent 方法返回 true,则事件交由 ViewGroup 处理,这时如果 ViewGroup 的 mOnTouchListener 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。也就是说,如果都提供的话,onTouch 会屏蔽掉 onTouchEvent。在 onTouchEvent 中,如果设置了 mOnClickListener,则 onClick 会被调用。如果顶级 ViewGroup 不拦截事件,则事件会传递给它所在的点击事件链上的子 View,这时子 View 的 dispatchTouchEvent 会被调用。到此为止,事件已经从顶级 View 传递给了下一层 View,接下来的传递过程和顶级 View 是一致的,如此循环,完成整个事件的分发。
首先看 ViewGroup 对点击事件的分发过程,其主要实现在 ViewGroup 的 dispatchTouchEvent 方法中,这个方法比较长,这里分段说明。先看下面一段,很显然,它描述的是当前 View 是否拦截点击事件这个逻辑:
// 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。ACTION-DOWN 事件好理解,那么 mFristTouchTarget != null 是什么意思呢?这个从后面的代码逻辑可以看出来,当事件由 ViewGroup 的子元素成功处理时,mFirstTouchTarget 会被赋值并指向子元素,换种方式来说,当 ViewGroup 不拦截事件并将事件交由子元素处理时,mFristTouchTarget != null。反过来,一旦事件由当前 ViewGroup 拦截时, mFristTouchTarget != null 就不成立。那么当 ACTION_MOVE 和 ACTION_UP 事件到来时,由于 (actionMasked == MotionEvent.ACTION_DOWN || mFristTouchTarget != null)这个条件为 false,将导致 ViewGroup 的 onInterceptTouchEvent 不会在被调用,并且同一序列中的其他事件都会默认交给它处理。
当然,这里有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT 标记位,这个标记位是通过 requestDisallowInterceptTouchEvent 方法来设置的,一般用于子 View 中。 FLAG_DISALLOW_INTERCEPT 一旦设置后,ViewGroup 将无法拦截除了 ACTION_DOWN 以外的其他点击事件。为什么说是除了 ACTION_DOWN 以外的其他事件呢?这是因为 ViewGroup 在分发事件时,如果是 ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置的这个标记位无效。因此,当面对 ACTION_DOWN 事件时,ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。在下面的代码中,ViewGroup 会在 ACTION_DOWN 事件到来时 做重置状态操作,而在 resetTouchState 方法中会对 FLAG_DISALLOW_INTERCEPT 进行重置,因此子 View 调用 requestDisallowInterceptTouchEvent 方法并不能影响 ViewGroup 对 ACTION_DOWN 事件的处理。
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
从上面的源码分析,我们可以得出结论:当 ViewGroup 决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不在调用它的 onInterceptTouchEvent 方法,这也证实了咱们前面总结的结论的 第(三)条结论。 FLAG_DISALLOW_INTERCEPT 这个标志的作用是让 ViewGroup 不再拦截事件,当然前提是 ViewGroup 不拦截 ACTION_DOWN 事件,因为 ViewGroup 一旦拦截了 ACTION_DOWN 事件,那么子 View 是捕获不到任何事件的,这也证实了咱们前面总结的结论的第(十一)条结论。
那么这段分析对我们来说有什么价值呢?总结起来有两点:第一点,onInterceptTouchEvent 不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择 dispatchTouchEvent 方法,只有这个方法能确保每次都会被调用,当然前提是事件能够传递到当前的 ViewGroup;另外一点,FLAG_DISALLOW_INTERCEPT 这个标记位的作用给我们提供了一个思路,当面对滑动冲突时,我们可以是不是考虑使用这种方法去解决问题,冠以滑动冲突,我们会在下一篇博客中进行介绍
接着再看当 ViewGroup 不拦截事件的时候,事件会向下分发交由它的子 View 进行处理,这段源码如下所示。
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
上面这段代码逻辑也很清楚,首先遍历 ViewGroup 的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理。可以看到,dispatchTransformedTouchEvent 实际上调用的就是子元素的 dispatchTouchEvent 方法,在它的内部有如下一段内容,而在上面的代码中 child 传递的不是 null, 因此它会直接调用子元素的 dispatchTouchEvent 方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的 dispatchTouchEvent 返回 true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么 mFirstTouchTarget 就会被赋值同时跳出 for 循环,如下所示:
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
这几行代码完成了 mFirstTouchTarget 的赋值并终止对子元素的遍历。如果子元素的 dispatchTouchEvent 返回 false,ViewGroup 就会把事件分发给下一个子元素(如何还有下一个子元素的话)。
其实 mFirstTouchTarget 真正的赋值过程是在 addTouchTarget 内部完成的,从下面的 addTouchTarget 方法的内部结构可以看出, mFristTouchTarget 其实是一种单链表结构。mFirstTouchTarget 是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,如果 mFirstTouchTarget 为 null,那么 ViewGroup就就默认拦截接下来同一序列中所有的点击事件,这一点在前面已经做了分析。
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:第一种是 ViewGroup 没有子元素;第二种是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为子元素在 onTouchEvent 中返回了 false。在这两种情况下,ViewGroup会自己处理点击事件,这里就证实了咱们前面所总结的结论中的第(四)条结论。
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
*注意上面这段代码,这里第三个参数 child 为 null,从前面的分析可以知道,它会调用 super.dispatchTouchEven,很显然,这里就转到了 View 的 dispatchTouchEvent 方法,即点击事件开始交由 View 来处理,请看下面的分析。
关于 ViewGroup 对事件的分发有兴趣的可以看一下文章继续深入:
- 1、Android事件分发机制完全解析,带你从源码的角度彻底理解(下) 代码是经过精简后的代码
- 2、 Android ViewGroup事件分发机制 代码是经过精简后的代码
- 3、Android Touch事件分发详解
- 4、Android Touch事件分发过程
- 5、Android ViewGroup拦截触摸事件详解
View 对点击事件的处理过程稍微简单一些,主要这里的 View 不包含 ViewGroup,先看它的 dispatchTouchEvent 方法,如下所示:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(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 对点击事件的处理过程就比较简单了,因为 View(这里不包含 ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。从上面的源码中可以看出 View 对点击事件的处理过程,首先会判断有没有设置 OnTouchListener,如果 OnTouchListener 中的 OnTouch 方法返回 true,那么 onTouchEvent 反复就不会被调用,可见 OnTouchListener 的优先级高于 onTouchEvent,这样做的好处是方便在外界处理点击事件。
接着在分析 onTouchEvent 的实现,先看当 View 处于不可用状态下点击事件的处理过程,如下所示。很显然,不可用状态下的 View 照样会消耗点击事件,尽管它看起来不可用。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
接着,如果 View 设置有代理,那么还会执行 TouchDelegate 的 onTouchEvent 方法,这个 onTouchEvent 的工作机制看起来和 OnTouchListener 类型,这里就不深入研究了。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
接着再看一下 onTouchEvent 中对点击事件的具体处理,如下所示:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
*从 上面的代码来看,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,那么它就会消耗这个事件,即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 状态,这也证实了咱们前面所总结的结论的第(八)、(九) 和 (十)条结论。然后就是当 ACTION_UP 事件发生时,会触发 performClick 方法,如果 View 设置了 OnClickListener,那么 performClick 方法内部会调用它的 onClick 方法,如下所示:
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;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
***View 的 LONG_CLICKABLE 属性默认为 false,而 CLICKABLE 属性是否为 false 和 具体的 View 有关,确切来说是可点击的 View 其 CLICKABLE 为 true,不可点击的 View 其 CLICKABLE 为 false,比如 Button 是可点击的, TextView 是不可点击的。通过 setClickable 和 setLongClickable 可以分别改变 View 的 CLICKABLE 和 LONG_CLICKABLE 属性,另外,setOnClickListener 会自动将 View 的 CLICKABLE 设为 true,这一点从源码中可以看出来,如下所示:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
到这里,点击事件的分发机制的源码实现就结束了,关于 View 对点击事件的处理过程可以参照下面两篇博客:
好了,由于篇幅的原因,关于 View 的滑动冲突以及常见的解决方法我们就放到下篇来进行介绍了….