Android——View的事件体系(二)View的事件分发机制

主要介绍内容:

  • View的事件分发机制
    • 点击事件的传递规则
    • 事件分发的源码解析
  • View的滑动冲突
    • 常见的滑动冲突场景
    • 滑动冲突的处理规则
    • 滑动冲突的解决方式

在上一章中我们已经介绍了 View 的基础知识以及 View 的滑动,想了解的请戳 Android——View的事件体系(一)View的滑动

这里本节将介绍 View 的一个核心知识点:事件分发机制。在真正进入详细探讨之前,大家可以先去瞅下这篇博文 Android事件分发完全解析之为什么是她 来找到自己要学习 View 事件的出发点。

下面我们开始进入今天的正题

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

它们之间的关系图如下所示:

这里写图片描述

关于事件传递的机制,我们这里先给出一些结论,然后后面会给出一些测试代码,根据这些结论我们可以更好的理解这个传递机制,如下所示:

  • (一)、同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
  • (二)、正常情况下,一个事件序列只能被一个 View 拦截且消耗。这一条的原因可以参考(三),因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个 View 同时处理,但是通过特殊手段可以做的,比如一个 View 将本应该自己处理的事件通过 onTouchEvent 强行传递给其他 View 处理。
  • (三)、某个 View 一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。这条也很好理解,就是说当一个 View 决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用在调用这个 View 的 onInterceptTouchEvent 方法去询问它是否要拦截了(其实上面的几个例子中我们已经证实了,通过上面几个例子中打印的log可以看出)。
  • (四)、某个 View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不会在交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。意思就是事件一旦交给一个 View 处理,那么它就必须消费掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员去做一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了,二者是类似的道理。(这个结论其实在上面的最后一个例子中也已经证实了,因为你(MyView)未处理事件,onTouchEvent 返回了 false,所以通过log可以看出在后面的一系列事件中都未交由你(MyView)进行处理)
  • (五)、如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件就会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续事件,最终这些消失的点击事件会传递给 Activity 处理。(这个可自行测试,需要在 down 事件中返回 true,move 和 up 事件中返回 false)
  • (六)、ViewGroup 默认不拦截任何事件。Android 源码中 ViewGroup 的 onInterceptTouchEvent 方法默认返回 false。
  • (七)、View没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  • (八)、View的 onTouchEvent 默认都会消耗事件(返回 true),除非它是不可点击的(clickable 和 longClickable) 同时为false。View 的 longClickable 属性都为 false,clickable 属性要分情况,比如 Button 的 clickable 属性默认为 true,而 Textview 的clickable 属性默认为 false。
  • (九)、View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
  • (十)、onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。
  • (十一)、事件的传递过程是由外向内的,即事件总是先传递给父元素,然后在由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。
事件分发机制源码解析

上面我们已经分析了 View 的事件分发机制,下面我们将从源码的角度去进一步分析、证实上面的结论。

  • 1、Activity 对点击事件的分发过程
    点击事件用 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.
 *
 * <p>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。
  • 2、顶级 View 对点击事件的分发过程

关于点击事件如何在 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<View> 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拦截触摸事件详解

  • 3、View 对点击事件的处理过程

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 的滑动冲突以及常见的解决方法我们就放到下篇来进行介绍了….

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值