前言
Touch 事件分发机制是面试中非常常见的问题,也是非常重要的问题。网上有很多关于这方面的文章,但是感觉写的不是特别清晰易懂。
基本概念
Touch 事件分发机制分发的是 MotionEvent 对象,取值如下:
- ACTION_DOWN: 按下事件。
- ACTION_MOVE: 移动事件。
- ACTION_UP: 抬起事件。
一个事件序列包含 ACTION_DOWN->ACTION_MOVE->...->ACTION_MOVE->ACTION_UP
,即用户触摸屏幕,移动一些距离,然后抬起。
- 下面说的 view “处理” 了某个事件,表示 view 调用了 onTouchEvent()。
- 下面说的 view “消费” 了某个事件,表示 view 调用了 onTouchEvent() 并返回 true。因此某个 view 可以处理但不消费某个事件。
Touch 事件分发机制涉及三个方法:
dispatchTouchEvent(MotionEvent ev)
: 如果某个触摸事件传递给了某个 View 或 ViewGroup(设为 v),则一定会调用 v.dispatchTouchEvent(),如果 v 是 ViewGroup,则内部会调用 onInterceptTouchEvent() 或 onTouchEvent();如果 v 是 View,则内部会调用 onTouchEvent()。onInterceptTouchEvent(MotionEvent ev)
: 这个方法只有 ViewGroup 才有,判断是否要拦截该事件并且自己处理,如果返回 true,则拦截;如果返回 false,则不拦截。onTouchEvent(MotionEvent ev)
: 处理 Touch 事件的核心方法,如果返回 true,表示消费了事件;如果返回 false,则表示没消费该事件。
ViewGroup 的 dispatchTouchEvent(), onInterceptTouchEvent(), onTouchEvent() 的基本关系如下:
|
上面的代码只是基本概括了整个事件分发的核心流程,具体实现细节会在下面介绍。
总体分发流程
当用户发起触摸事件后,首先触摸事件从 Activity 的 dispatchTouchEvent() 开始,该方法实现如下:
|
解释:
- 其中 getWindow().superDispatchTouchEvent() 会调用 DecorView 的 dispatchTouchEvent() 开始分发,接着 DecorView 会调用根 View 进行事件分发。
- 如果 getWindow().superDispatchTouchEvent() 返回 true,表示有子 View 消费了该触摸事件;如果返回 false,表示没有任何子 View 消费该事件,会调用 Activity 的 onTouchEvent(),即 Activity 自己处理 Touch 事件,并返回 false。
View Touch 事件分发
因为 ViewGroup 也是继承自 View,因此此处分两种情况讨论。
- 如果是最底层的 View,一旦将触摸事件分发给他,就会调用下面的 dispatchTouchEvent();
- 如果是 ViewGroup,则默认并不会调用下面的 dispatchTouchEvent(),而是会调用 ViewGroup 自己的 dispatchTouchEvent(),只有当 ViewGroup 在自己的 dispatchTouchEvent() 方法中经过判断,发现需要自己处理触摸事件时,才会通过
super.dispatchTouchEvent(ev)
的形式调用下面的 dispatchTouchEvent()。
View 的 dispatchTouchEvent() 实现如下:
|
从上面看出:
- onTouchEvent() 不一定会被调用。如果设置了 OnTouchListener, View 是 enabled,并且 OnTouchListener 的 onTouch() 返回 true,则不会调用 onTouchEvent()。
- 如果 View 是 DISABLED,则 onTouch() 不会被调用。
- 如果 OnTouchListener 的 onTouch() 或 View 的 onTouchEvent() 返回 true,则 dispatchTouchEvent() 返回 true;否则返回 false。
接着我们看看 onTouchEvent() 的实现:
|
从上面可以看出:
- onTouchEvent() 内部在 ACTION_UP 事件中调用了 onClick()(即在一个事件序列中,只有在 ACTION_UP 才会调用 onClick(),但是 onTouch() 在每个事件都会被调用),即如果同时注册了 OnTouchListener 和 OnClickListener,则 OnTouchListener 优先级高于 OnClickListener,如果 onTouch() 返回 true,则 onTouchEvent() 不会执行,也就意味着 onClick() 不会执行。
- 在 onTouchEvent() 中,只有 View 是 Clickable 的,才能进入 if 语句,而且一旦进入 if 语句就返回 true。比如 Button 是 Clickable 的,因此 onTouchEvent() 一定返回 true,ImageView 是不可点击的,因此 onTouchEvent() 一定返回 false。
ViewGroup Touch 事件分发
ViewGroup 的 dispatchTouchEvent() 比较复杂,下面我们分析一下。
|
上面的代码中多处调用了 dispatchTransformedTouchEvent(),其中第三个参数有 null(第 52 行) 或者 child(第 40 行、第 59 行)。这个方法的实现如下:
|
从上面代码可以看出,如果第三个参数传入 null,则调用 View 的 dispatchTouchEvent() 即自己处理事件;如果第三个参数传入 child,则将事件分发下去,即调用 child.dispatchTouchEvent()。
解释:
- 第 2 行: handled 变量作为 dispatchTouchEvent() 的返回值。
- 第 4-7 行: 如果是 ACTION_DOWN 操作,则将其状态清空,包括 FLAG_DISALLOW_INTERCEPT。我们可以通过 requestDisallowInterceptTouchEvent() 将 FLAG_DISALLOW_INTERCEPT 设置为 true,表示该 ViewGroup 禁止拦截操作(即直接将 intercepted 设为 false,不调用 onInterceptTouchEvent()),但是这个设置对于 ACTION_DOWN 无效,因为第 4-7 行会将该状态清除,即使设置了该状态,ACTION_DOWN 操作还是会调用 onInterceptTouchEvent()。
- 第 10-22 行: 每个 ViewGroup 都会带有 mFirstTouchTarget 变量,这个变量只有在 ACTION_DOWN 事件时才能设置,这个能从第 28 行的 if 语句看出来,因为设置 mFirstTouchTarget 是在第 40-45 行(只有第 28 行的 if 语句为 true 才能执行第 40-45 行代码),可以看出如果有某个子 View 消费了该事件(这里不一定是直接子 View 消费了该事件,比如有 View 关系: v1->v2->v3,当前调用了 v1.dispatchTouchEvent(),如果 v3 消费了该事件,则表示 v1 的某个子 View 消费了该事件,并将 v1 的 mFirstTouchTarget 设置为 v2,将 v2 的 mFirstTouchTarget 设置为 v3),这样才能使第 40 行的 dispatchTransformedTouchEvent() 返回 true,并设置 mFirstTouchTarget。
- 第 25-48 行: 如果当前是 ACTION_DOWN 事件(第 28 行)并且没有拦截(第 27 行,onInterceptTouchEvent() 返回 false),则会将 ACTION_DOWN 事件分发给合适的在触摸点的直接子 View,在第 40 行的 dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) 中会调用 child 的 dispatchTouchEvent()。如果 child.dispatchTouchEvent() 返回 false,表示 child 或 child 的子 View 没有人消费 ACTION_DOWN 事件,这样并不会给当前 ViewGroup 设置 mFirstTouchTarget。如果 child.dispatchTouchEvent() 返回 true,即表示 child 或 child 的子 View 有人消费 ACTION_DOWN 事件,则为当前 ViewGroup 设置 mFirstTouchTarget = child,并设置 alreadyDispatchedToNewTouchTarget = true,这个变量在后面会用到,表示是不是在这个方法中刚刚设置了 mFirstTouchTarget,因为只有 ACTION_DOWN 操作才能设置 mFirstTouchTarget,如果
alreadyDispatchedToNewTouchTarget == true && mFirstTouchTarget != null
,则表示当前是 ACTION_DOWN 事件并且在该方法中刚刚设置了 mFirstTouchTarget;如果alreadyDispatchedToNewTouchTarget == false && mFirstTouchTarget != null
,则表示 mFirstTouchTarget 并不是该方法中刚刚设置的,即当前不是 ACTION_DOWN 事件。 - 第 51-53 行: 有两种情况会进入第 51 行的 if 语句,(1)当前是 ACTION_DOWN 操作,并且没有子 View 处理该事件 (2)当前不是 ACTION_DOWN 操作,并且在前面的 ACTION_DOWN 操作时没有子 View 处理该事件。第 52 行的第三个参数为 null,因此会执行
super.dispatchTouchEvent()
,即执行 View 的 dispatchTouchEvent(),表示自己处理该事件。 - 第 55-57 行: 因为只有当当前为 ACTION_DOWN 操作并且有子 View 处理了该事件时,alreadyDispatchedToNewTouchTarget 才为 true,这时直接将 handled 设为 true,不需要做额外的操作。
- 第 59-61 行: 能够进入第 58 行的 else 语句意味着当前事件不是 ACTION_DOWN 并且在前面的 ACTION_DOWN 事件存在子 View 处理了该事件(即 mFirstTouchTarget != null,即在前面的 ACTION_DOWN 事件执行过第 40-46 行代码)。此时就执行 dispatchTransformedTouchEvent(),内部会调用 mFirstTouchTarget.dispatchTouchEvent()。
- 第 66-69 行: 做收尾工作。
一些结论的验证
在网上有很多关于 Touch 事件的结论,这些结论其实都可以通过分析上面的代码得出。这里举几个例子:
- “某个 view 一旦开始处理事件,如果不消费 ACTION_DOWN 事件,则同一事件序列中的其他事件不会再交给它来处理”: 如果 view 处理但不消费 ACTION_DOWN 事件,则表示执行了第 40 行代码,但是返回 false(这个 view 作为 child 传入),设这个 view 的父 view 为 v0,此时就没设置 v0 的 mFirstTouchTarget。因此接下来的事件(比如 ACTION_MOVE) 一旦分发到 v0,因为他的 mFirstTouchTarget == null,因此会执行第 52 行代码,即 v0 自己处理该事件。
- “某个 view 一旦决定拦截,那么这一事件序列都只能由它来处理,并且它的 onInterceptTouchEvent 不会被调用”: 因为 view 拦截了事件,因此第 27 行的 if 语句进不去,也就设置不了 view 的 mFirstTouchTarget,接下来的事件分发给 view 时,执行到第 11 行的 if 语句,因为该事件不是 ACTION_DOWN 并且 view 的 mFirstTouchTarget == null,因此 if 语句返回 false,即执行第 21 行设置 intercepted = true,因此不会调用 onInterceptTouchEvent;接着执行到第 51 行,因为 view 的 mFirstTouchTarget 为 null,因此执行第 52 行代码,即自己处理该事件。