Android事件传递机制

总结,细化,推演

一、总结

四个事件,三个方法,两套机制

1.1 四个事件

  • Down
  • Move
  • Up/Cancel

1.2 三个方法

  • dispatchTouchEvent
  • onInterceptTouchEvent
  • onTouchEvent

1.3 两套机制

Talk is cheap, show me the code.

ViewGroup处理机制:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (!onInterceptTouchEvent(ev)) {                   // 1. 判断是否拦截
        for (int i = childrenCount - 1; i >= 0; i--) {  // 2. 逆序遍历子View,寻找意欲消费该事件的子View
            if (children[i].dispatchTouchEvent(ev)) {
                return true;
            }
        }
    }
    return super.dispatchTouchEvent(ev);               // 3. 如果没有子View消费事件,则走View处理机制
}

View处理机制:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (onTouchListener != null) {
        return onTouchListener.onTouch(ev) || onTouchEvent(ev);
    }
    return onTouchEvent(ev);
}

注意
1、上述机制是极度简化后的(后文详述)
2、ViewGroup本身也是View

二、细化

2.1 四个事件

只考虑单点触控的情况下,一次手势操作产生的事件序列如下图所示:


基本事件序列

注意
事件消费以手势为单位,如果View不消费Down事件,则它将不再收到其它事件;反之,如果它消费了Down事件,则必然以一个Up或Cancel事件结束。

2.2 三个方法

dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent都接收一个TouchEvent作为参数,返回值类型都是boolean。

dispatchTouchEvent自上而下分发,onTouchEvent自下而上处理,onInterceptTouchEvent居中拦截,构成了Android事件的处理机制。

2.2.1 dispatchTouchEvent

dispatchTouchEvent定义在Activity、ViewGroup和View中,控制TouchEvent走向,其目的就是找出哪个View应该处理该TouchEvent,简言之,它用于事件的分发。

Activity.dispatchTouchEvent总是最先收到事件,经过一系列下发后,传递给布局。本文只考虑布局对手势事件的处理。

dispatchTouchEvent有几个原则非常关键

  1. 其对Down事件的返回值表征是否消费该事件(对其它事件的返回值无所谓)
  2. 自上而下分发,采用逐层负责制
  3. 分发之前先判断是否要拦截
2.2.2 onInterceptTouchEvent

onInterceptTouchEvent定义在ViewGroup中,用于拦截事件。

onInterceptTouchEvent被调用需要满足两个条件:

  1. 当前传递的是Down事件或者有子View消费了之前的Down事件
  2. 允许拦截

如果onInterceptTouchEvent拦截的是Down事件,Down事件不会继续向下传递;如果拦截的是Move或Up事件,则会下发Cancel事件给下层。

onInterceptTouchEvent一旦返回true,就不会再调用。

2.2.3 onTouchEvent

onTouchEvent定义在Activity、ViewGroup和View中,消费事件。

2.3 两套机制

1.3节中所示的机制确切来说是不正确的,实际的事件传递机制要复杂得多。

Android事件处理涉及三个操作:分发、拦截与消费。

  • 分发
  • dispatchTouchEvent负责分发
  • 分发自上而下
  • 分发前判断是否拦截
  • dispatchTouchEvent对Down事件返回true表征View/ViewGroup消费该事件
  • dispatchTouchEvent对其余事件的返回值对分发没有任何影响
  • ViewGroup会逆序遍历边界包含触控点的所有子View,调用其dispatchTouchEvent判断其是否消费
  • 一旦找到消费事件的子View
    • ViewGroup会停止遍历
    • 后续事件会直接分发给该子View,不再遍历
  • 拦截
  • onInterceptTouchEvent负责拦截
  • onInterceptTouchEvent返回true表征拦截
  • 如果ViewGroup A拦截Down事件
    • 所有事件由拦截事件的ViewGroup处理,不向下分发
  • 如果ViewGroup A拦截Move/Up事件
    • 事件转为Cancel向下分发
    • A.onTouchEvent在本次事件中不会被调用
    • A.dispatchTouchEvent会将后续事件直接传递给A.onTouchEvent,不向下分发
  • 一旦onInterceptTouchEvent返回true,就不会再被调用
  • 消费
    • OnTouchListener或onTouchEvent负责消费
    • OnTouchListener优先于onTouchEvent
    • 如果OnTouchListener.onTouch返回true,则onTouchEvent不会再被调用
    • 如果没有注册OnTouchListener或OnTouchListener.onTouch返回false,onTouchEvent会再被调用

第三章通过推演详述这两套机制。

三、推演

假定有一个如下布局:L里包了一个R,R里包了B、T1、T2三个子View,T2叠在T1上。



L继承自LinearLayout,R继承自RelativeLayout,B继承自Button,T1、T2继承自TextView,在它们各事件方法的头部和尾部分别插入日志以观察其行为:

// public boolean onInterceptTouchEvent(MotionEvent ev) {
// public boolean onTouchEvent(MotionEvent event) {
public boolean dispatchTouchEvent(MotionEvent ev) {
    L.e(MotionEvent.actionToString(ev.getAction()));
    boolean result = super.dispatchTouchEvent(ev);
    L.e(MotionEvent.actionToString(ev.getAction()) + " " + result);
    return result;
}

默认情况
在T1的右侧(未被T2覆盖的部分)滑动手指,生成如下日志:

1. L.dispatchTouchEvent(L.java:41) ==> ACTION_DOWN
2. L.onInterceptTouchEvent(L.java:33) ==> ACTION_DOWN
3. L.onInterceptTouchEvent(L.java:35) ==> ACTION_DOWN false
4.  R.dispatchTouchEvent(R.java:45) ==> ACTION_DOWN
5.  R.onInterceptTouchEvent(R.java:37) ==> ACTION_DOWN
6.  R.onInterceptTouchEvent(R.java:39) ==> ACTION_DOWN false
7.      T1.dispatchTouchEvent(T1.java:35) ==> ACTION_DOWN
8.      T1.onTouchEvent(T1.java:43) ==> ACTION_DOWN
9.      T1.onTouchEvent(T1.java:45) ==> ACTION_DOWN false
10.     T1.dispatchTouchEvent(T1.java:37) ==> ACTION_DOWN false
11. R.onTouchEvent(R.java:53) ==> ACTION_DOWN
12. R.onTouchEvent(R.java:55) ==> ACTION_DOWN false
13. R.dispatchTouchEvent(R.java:47) ==> ACTION_DOWN false
14. L.onTouchEvent(L.java:49) ==> ACTION_DOWN
15. L.onTouchEvent(L.java:51) ==> ACTION_DOWN false
16. L.dispatchTouchEvent(L.java:43) ==> ACTION_DOWN false

从日志中可以看出,事件传递经历了如下步骤:

  1. 首先产生的是Down事件,传递给L.dispatchTouchEvent
  2. L随之调用自身的onInterceptTouchEvent方法
  3. L.onInterceptTouchEvent返回false,表明L不拦截
  4. L于是把事件分发给R(调用R.dispatchTouchEvent)
  5. 同样的,R先调用自身的onInterceptTouchEvent
  6. R.onInterceptTouchEvent返回false,表明R也不拦截
  7. R于是把事件分发给T1(调用T1.dispatchTouchEvent)
  8. T1是普通View,不再需要向下分发,于是调用自身的onTouchEvent
    (自此,事件分发完成,开始处理事件)
  9. T1.onTouchEvent返回false
  10. T1.dispatchTouchEvent收到T1.onTouchEvent的返回值,同样返回false,表明T1不消费Down事件
    (第9、10步参见1.3节View处理机制)
  11. R根据T1.dispatchTouchEvent的返回值判断T1没有消费事件,于是尝试自己处理该事件,调用自身的onTouchEvent
  12. R.onTouchEvent返回false
  13. 从而导致R.dispatchTouchEvent返回false,表明R不消费Down事件
  14. 由于R没有消费事件,L尝试自己处理事件,调用自身的onTouchEvent
  15. L.onTouchEvent返回false
  16. 从而导致L.dispatchTouchEvent返回false
    (由于Down事件没有被消费,因此它们都将不再收到其它事件,事件处理结束)

结论
1、dispatchTouchEvent自上而下分发
2、onTouchEvent自下而上消费
3、ViewGroup.dispatchTouchEvent向下分发之前,会先调用onInterceptTouchEvent
4、如果Down事件没有被消费,将不再收到其余事件(Move,Up)

消费事件
如果View是clickable的,或者View注册了OnTouchListener且其onTouch方法返回true,或者实现了自定义的返回值为true的onTouchEvent,则认为View消费了事件。

根据我们的假定,B继承自Button,它是clickable的,因此可用于做消费事件的实验,但为了同时说明ViewGroup寻找子View的机制,我们在T1上注册一个OnTouchListener回调并返回true。

在T2上点击,日志如下:

1. L.dispatchTouchEvent(L.java:41) ==> ACTION_DOWN
2. L.onInterceptTouchEvent(L.java:33) ==> ACTION_DOWN
3. L.onInterceptTouchEvent(L.java:35) ==> ACTION_DOWN false
4.     R.dispatchTouchEvent(R.java:43) ==> ACTION_DOWN
5.     R.onInterceptTouchEvent(R.java:35) ==> ACTION_DOWN
6.     R.onInterceptTouchEvent(R.java:37) ==> ACTION_DOWN false
7.          T2.dispatchTouchEvent(T2.java:35) ==> ACTION_DOWN
8.          T2.onTouchEvent(T2.java:43) ==> ACTION_DOWN
9.          T2.onTouchEvent(T2.java:45) ==> ACTION_DOWN false
10.         T2.dispatchTouchEvent(T2.java:37) ==> ACTION_DOWN false
11.         T1.dispatchTouchEvent(T1.java:35) ==> ACTION_DOWN
12.         MainActivity$1.onTouch(MainActivity.java:21) ==> ACTION_DOWN true
13.         T1.dispatchTouchEvent(T1.java:37) ==> ACTION_DOWN true
14.    R.dispatchTouchEvent(R.java:45) ==> ACTION_DOWN true
15. L.dispatchTouchEvent(L.java:43) ==> ACTION_DOWN true

16. L.dispatchTouchEvent(L.java:41) ==> ACTION_UP
17. L.onInterceptTouchEvent(L.java:33) ==> ACTION_UP
18. L.onInterceptTouchEvent(L.java:35) ==> ACTION_UP false
19.     R.dispatchTouchEvent(R.java:43) ==> ACTION_UP
20.     R.onInterceptTouchEvent(R.java:35) ==> ACTION_UP
21.     R.onInterceptTouchEvent(R.java:37) ==> ACTION_UP false
22.         T1.dispatchTouchEvent(T1.java:35) ==> ACTION_UP
23.         MainActivity$1.onTouch(MainActivity.java:21) ==> ACTION_UP true
24.         T1.dispatchTouchEvent(T1.java:37) ==> ACTION_UP true
25.     R.dispatchTouchEvent(R.java:45) ==> ACTION_UP true
26. L.dispatchTouchEvent(L.java:43) ==> ACTION_UP true

日志分析:
日志1至日志10与默认情况无异,下面主要说说几处不同。
11至13:事件透过T2继续向T1分发,且T1注册的回调返回了true,因此T1消费了Down事件
14至15:由于有子View消费了事件,因此L、R的onTouchEvent方法没有被调用
16至26:由于Down事件被消费,因此系统继续下发后续事件(Up)
(手指滑动小于20像素,不会产生Move事件,本例为简化没有令其产生)
另外注意,R没有将Up事件传递给T2,而是直接传给了T1

结论
1、如果消费了Down事件,则会继续收到后续事件
2、ViewGroup会以子View添加顺序的逆序查找边界包含触控点的所有子View
(2.1 事实上,如果遍历时发现某子View消费了该事件,ViewGroup会停止遍历)
3、一旦查找到消费事件的子View,后续事件会直接传递给它,不会再遍历

关于结论中2.1的论证,可以做这样一个实验:
在T2上注册OnTouchListener回调,实验方式不变,会发现事件不会传递给T1,而是由T2消费Down和Up事件。

如何判断事件是否被消费?
关于事件是否被消费,有说法说是看onTouchEvent是否返回true,这是不对的,实际上要看dispatchTouchEvent是否返回true,之所以会有这种说法,是因为默认情况下如果onTouchEvent返回true,则dispatchTouchEvent必然返回true(见1.3)。

做两个实验验证:

  1. 令T1.onTouchEvent返回true,而T1.dispatchTouchEvent返回false
  2. 令T1.onTouchEvent返回false,而T1.dispatchTouchEvent返回true

实验一日志输出除第9条外与默认情况完全一样,这里从略。
实验二日志如下:

1. L.dispatchTouchEvent(L.java:41) ==> ACTION_DOWN
2. L.onInterceptTouchEvent(L.java:33) ==> ACTION_DOWN
3. L.onInterceptTouchEvent(L.java:35) ==> ACTION_DOWN false
4.  R.dispatchTouchEvent(R.java:45) ==> ACTION_DOWN
5.  R.onInterceptTouchEvent(R.java:37) ==> ACTION_DOWN
6.  R.onInterceptTouchEvent(R.java:39) ==> ACTION_DOWN false
7.          T1.dispatchTouchEvent(T1.java:35) ==> ACTION_DOWN
8.          T1.onTouchEvent(T1.java:44) ==> ACTION_DOWN
9.          T1.onTouchEvent(T1.java:46) ==> ACTION_DOWN false
10.         T1.dispatchTouchEvent(T1.java:38) ==> ACTION_DOWN true
11.     R.dispatchTouchEvent(R.java:47) ==> ACTION_DOWN true
12. L.dispatchTouchEvent(L.java:43) ==> ACTION_DOWN true

13. L.dispatchTouchEvent(L.java:41) ==> ACTION_UP
14. L.onInterceptTouchEvent(L.java:33) ==> ACTION_UP
15. L.onInterceptTouchEvent(L.java:35) ==> ACTION_UP false
16.     R.dispatchTouchEvent(R.java:45) ==> ACTION_UP
17.     R.onInterceptTouchEvent(R.java:37) ==> ACTION_UP
18.     R.onInterceptTouchEvent(R.java:39) ==> ACTION_UP false
19.         T1.dispatchTouchEvent(T1.java:35) ==> ACTION_UP
20.         T1.onTouchEvent(T1.java:44) ==> ACTION_UP
21.         T1.onTouchEvent(T1.java:46) ==> ACTION_UP false
22.         T1.dispatchTouchEvent(T1.java:38) ==> ACTION_UP true
23.     R.dispatchTouchEvent(R.java:47) ==> ACTION_UP true
24. L.dispatchTouchEvent(L.java:43) ==> ACTION_UP true

从日志中可以看出,
(实验1)虽然T1.onTouchEvent返回true,但由于T1.dispatchTouchEvent返回false,系统仍然认为T1没有消费Down事件,因此T1不再收到后续事件。
(实验2)反之,强制使T1.dispatchTouchEvent返回true后,T1(及T1的父控件L、R)会收到后续事件(Up),
由此可知

dispatchTouchEvent返回true <=> View消费了事件

逐层负责制
为了说明事件下发是采用逐层负责制,做两个实验(点击T1):

  1. 令T1.dispatchTouchEvent返回false(默认情况),R.dispatchTouchEvent返回true(点击T1)
  2. 令R.dispatchTouchEvent返回false,L.dispatchTouchEvent返回true(点击B)

实验1日志如下:

1.  L.dispatchTouchEvent(L.java:41) ==> ACTION_DOWN
2.  L.onInterceptTouchEvent(L.java:33) ==> ACTION_DOWN
3.  L.onInterceptTouchEvent(L.java:35) ==> ACTION_DOWN false
4.      R.dispatchTouchEvent(R.java:43) ==> ACTION_DOWN
5.      R.onInterceptTouchEvent(R.java:35) ==> ACTION_DOWN
6.      R.onInterceptTouchEvent(R.java:37) ==> ACTION_DOWN false
7.          T1.dispatchTouchEvent(T1.java:35) ==> ACTION_DOWN
8.          T1.onTouchEvent(T1.java:43) ==> ACTION_DOWN
9.          T1.onTouchEvent(T1.java:45) ==> ACTION_DOWN false
10.         T1.dispatchTouchEvent(T1.java:37) ==> ACTION_DOWN false
11.     R.onTouchEvent(R.java:52) ==> ACTION_DOWN
12.     R.onTouchEvent(R.java:54) ==> ACTION_DOWN false
13.     R.dispatchTouchEvent(R.java:46) ==> ACTION_DOWN true
14. L.dispatchTouchEvent(L.java:43) ==> ACTION_DOWN true

15. L.dispatchTouchEvent(L.java:41) ==> ACTION_UP
16. L.onInterceptTouchEvent(L.java:33) ==> ACTION_UP
17. L.onInterceptTouchEvent(L.java:35) ==> ACTION_UP false
18.     R.dispatchTouchEvent(R.java:43) ==> ACTION_UP
19.     R.onTouchEvent(R.java:52) ==> ACTION_UP
20.     R.onTouchEvent(R.java:54) ==> ACTION_UP false
21.     R.dispatchTouchEvent(R.java:46) ==> ACTION_UP true
22. L.dispatchTouchEvent(L.java:43) ==> ACTION_UP true

实验2日志如下:

1.  L.dispatchTouchEvent(L.java:41) ==> ACTION_DOWN
2.  L.onInterceptTouchEvent(L.java:33) ==> ACTION_DOWN
3.  L.onInterceptTouchEvent(L.java:35) ==> ACTION_DOWN false
4.      R.dispatchTouchEvent(R.java:43) ==> ACTION_DOWN
5.      R.onInterceptTouchEvent(R.java:35) ==> ACTION_DOWN
6.      R.onInterceptTouchEvent(R.java:37) ==> ACTION_DOWN false
7.          B.dispatchTouchEvent(B.java:35) ==> ACTION_DOWN
8.          B.onTouchEvent(B.java:43) ==> ACTION_DOWN
9.          B.onTouchEvent(B.java:45) ==> ACTION_DOWN true
10.         B.dispatchTouchEvent(B.java:37) ==> ACTION_DOWN true
11.     R.dispatchTouchEvent(R.java:46) ==> ACTION_DOWN false
12. L.onTouchEvent(L.java:50) ==> ACTION_DOWN
13. L.onTouchEvent(L.java:52) ==> ACTION_DOWN false
14. L.dispatchTouchEvent(L.java:44) ==> ACTION_DOWN true

15. L.dispatchTouchEvent(L.java:41) ==> ACTION_UP
16. L.onTouchEvent(L.java:50) ==> ACTION_UP
17. L.onTouchEvent(L.java:52) ==> ACTION_UP false
18. L.dispatchTouchEvent(L.java:44) ==> ACTION_UP true

从实验1日志可以看出,T1没有消费Down事件,R消费了Down事件,所以Up事件继续传递给R,但R不再向T1分发。
从实验2日志可以看出,虽然B消费了Down事件,但由于R.dispatchTouchEvent返回了false,表征R没有消费Down事件,因此系统不再向R下发后续事件,也就不会向B下发后续事件。

结论
如果ViewGroup没有消费Down事件,即使其子View消费了Down事件,该ViewGroup及其子View都不会再收到后续事件。

拦截机制
做两个实验:

  1. (拦截Down事件)令L.onInterceptTouchEvent(ACTION_DOWN)返回true,同时L.dispatchTouchEvent(ACTION_DOWN)返回true,其它默认(点击T1)
  2. (拦截Move事件)令L.onInterceptTouchEvent(ACTION_MOVE)返回true,其它默认(点击B)

实验1日志输出如下:

1.  L.dispatchTouchEvent(L.java:44) ==> ACTION_DOWN
2.  L.onInterceptTouchEvent(L.java:33) ==> ACTION_DOWN
3.  L.onInterceptTouchEvent(L.java:38) ==> ACTION_DOWN true
4.  L.onTouchEvent(L.java:55) ==> ACTION_DOWN
5.  L.onTouchEvent(L.java:57) ==> ACTION_DOWN false
6.  L.dispatchTouchEvent(L.java:49) ==> ACTION_DOWN true

7.  L.dispatchTouchEvent(L.java:44) ==> ACTION_UP
8.  L.onTouchEvent(L.java:55) ==> ACTION_UP
9.  L.onTouchEvent(L.java:57) ==> ACTION_UP false
10. L.dispatchTouchEvent(L.java:49) ==> ACTION_UP false

实验2日志输出如下:

1.  L.dispatchTouchEvent(L.java:44) ==> ACTION_DOWN
2.  L.onInterceptTouchEvent(L.java:33) ==> ACTION_DOWN
3.  L.onInterceptTouchEvent(L.java:38) ==> ACTION_DOWN false
4.      R.dispatchTouchEvent(R.java:43) ==> ACTION_DOWN
5.      R.onInterceptTouchEvent(R.java:35) ==> ACTION_DOWN
6.      R.onInterceptTouchEvent(R.java:37) ==> ACTION_DOWN false
7.          B.dispatchTouchEvent(B.java:35) ==> ACTION_DOWN
8.          B.onTouchEvent(B.java:43) ==> ACTION_DOWN
9.          B.onTouchEvent(B.java:45) ==> ACTION_DOWN true
10.         B.dispatchTouchEvent(B.java:37) ==> ACTION_DOWN true
11.     R.dispatchTouchEvent(R.java:45) ==> ACTION_DOWN true
12. L.dispatchTouchEvent(L.java:46) ==> ACTION_DOWN true

13. L.dispatchTouchEvent(L.java:44) ==> ACTION_MOVE
14. L.onInterceptTouchEvent(L.java:33) ==> ACTION_MOVE
15. L.onInterceptTouchEvent(L.java:38) ==> ACTION_MOVE true
16.     R.dispatchTouchEvent(R.java:43) ==> ACTION_CANCEL
17.     R.onInterceptTouchEvent(R.java:35) ==> ACTION_CANCEL
18.     R.onInterceptTouchEvent(R.java:37) ==> ACTION_CANCEL false
19.         B.dispatchTouchEvent(B.java:35) ==> ACTION_CANCEL
20.         B.onTouchEvent(B.java:43) ==> ACTION_CANCEL
21.         B.onTouchEvent(B.java:45) ==> ACTION_CANCEL true
22.         B.dispatchTouchEvent(B.java:37) ==> ACTION_CANCEL true
23.     R.dispatchTouchEvent(R.java:45) ==> ACTION_CANCEL true
24. L.dispatchTouchEvent(L.java:46) ==> ACTION_MOVE true

25. L.dispatchTouchEvent(L.java:44) ==> ACTION_MOVE
26. L.onTouchEvent(L.java:52) ==> ACTION_MOVE
27. L.onTouchEvent(L.java:54) ==> ACTION_MOVE false
28. L.dispatchTouchEvent(L.java:46) ==> ACTION_MOVE false

29. L.dispatchTouchEvent(L.java:44) ==> ACTION_UP
30. L.onTouchEvent(L.java:52) ==> ACTION_UP
31. L.onTouchEvent(L.java:54) ==> ACTION_UP false
32. L.dispatchTouchEvent(L.java:46) ==> ACTION_UP false

实验1日志分析:

  1. (日志1至6)L拦截了Down事件,事件由L进行处理,不再向下分发
  2. (日志7至10)系统下发Up事件给L后,L不再调用自身的onInterceptTouchEvent。

实验2日志分析:

  1. L拦截了第一个Move事件
  • 该事件转为Cancel事件,继续向下分发
  • L.onTouchEvent不会被调用
  1. 后续事件(Move/Up)直接由L处理
  • 不再调用onInterceptTouchEvent
  • 也不再向下分发

结论
1、 如果ViewGroup拦截了Down事件,则该ViewGroup不向下分发任何事件,该ViewGroup会消费整个手势事件。
2、如果ViewGroup拦截了Move事件,则该事件会转为Cancel事件继续向下分发,但不会调用自身的onTouchEvent,而后续的事件则会直接有该ViewGroup处理,不在询问是否拦截,也不再向下分发
3、一旦onInterceptTouchEvent返回true,就不会再被调用
4、 如果子View没有消费事件而ViewGroup消费了Down事件,则后续不会再调用ViewGroup的onInterceptTouchEvent

拓展阅读

推荐阅读更多精彩内容