Android事件分发机制
分发的是什么
首先要说明分发的对象是MotionEvent,其包含的事件有以下几个:
事件 | 简介 |
---|---|
ACTION_DOWN | 手指 初次接触到屏幕 时触发。 |
ACTION_MOVE | 手指 在屏幕上滑动 时触发,会多次触发。 |
ACTION_UP | 手指 离开屏幕 时触发。 |
ACTION_CANCEL | 事件 被上层拦截 时触发。 |
对于单指触控来说,一次简单的交互流程是这样的:
手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)
- 本次事例中 ACTION_MOVE 有多次触发。
- 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。
事件分发、拦截与消费
下表省略了 PhoneWidow 和 DecorView。
√
表示有该方法。
X
表示没有该方法。
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | X | √ | X |
事件消费 | onTouchEvent | √ | √ | √ |
这个三个方法均有一个 boolean(布尔) 类型的返回值,通过返回 true 和 false 来控制事件传递的流程。
PS: 从上表可以看到 Activity 和 View 都是没有事件拦截的,这是因为:
Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。
View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。
Window这个抽象类可以控制顶层View的外观和行为策略,其唯一实现为PhoneWindow。
事件分发流程
责任链模式,层层传递,直到被消费。(公司制)
事件收集之后最先传递给 Activity, 然后依次向下传递,大致如下:
|
|
如果没有任何view消费这个事件,那么这个事件会按照反方向回传,最终传回给Activity,如果最后 Activity 也没有处理,本次事件才会被抛弃:
|
|
dispatchTouchEvent
是事件分发机制中的核心,所有的事件调度都归它管。下面分别就view和viewgroup进行讨论。
ViewGroup相关
分发流程:
- 1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
- 2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
- 3.如果子 ChildView 不需要则调用自身的 onTouchEvent。
用伪代码应该是这样的:
|
|
当然了,上述代码还有很多问题没有解决的,例如:
1. ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?
这个很容易,就是把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View。
2. 当该点的 ChildView 有重叠时应该如何分配?
当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView。
如何判断哪个是显示在最上面的呢?后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的。
如下:
|
|
当手指点击有重叠区域时,分如下几种情况:
- 只有 View1 可点击时,事件将会分配给 View1,即使被 View2 遮挡,这一部分仍是 View1 的可点击区域。
- 只有 View2 可点击时,事件将会分配给 View2。
- View1 和 View2 均可点击时,事件会分配给后加载的 View2,View2 将事件消费掉,View1接收不到事件。
注意:
- 上面说的是可点击,可点击包括很多种情况,只要你给View注册了
onClickListener、onLongClickListener、OnContextClickListener
其中的任何一个监听器或者设置了android:clickable="true"
就代表这个 View 是可点击的。
另外,某些 View 默认就是可点击的,例如,Button,CheckBox 等。 - 给 View 注册
OnTouchListener
不会影响 View 的可点击状态。即使给 View 注册OnTouchListener
,只要不返回true
就不会消费事件。
3. ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),哪个会执行?
事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。
4. 所有事件都应该被同一 View 消费
为了防止事件响应混乱
( View 中 onClick 事件需要同时接收到 ACTION_DOWN 和 ACTION_UP 才能触发,如果分配给了不同的 View,那么 onClick 将无法被正确触发)。
安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。
如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。
源码:
|
|
View相关
Q: 为什么 View 会有 dispatchTouchEvent ?
A: 用于处理自身的事件监听器。
Q: 与 View 事件相关的各个方法调用顺序是怎样的?
A: 如果不去看源码,想一下让自己设计会怎样?
- 单击事件(onClickListener) 需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用。(最后)
- 长按事件(onLongClickListener) 同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
- 触摸事件(onTouchListener) 如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
- View自身处理(onTouchEvent) 提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)
所以事件的调度顺序应该是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener
。
下面我们来看一下实际测试结果:
手指按下,不移动,稍等片刻再抬起。
|
|
可以看到,测试结果也支持我们猜测的结论,因为长按 onLongClickListener 不需要 ACTION_UP 所以会在 ACTION_DOWN 之后就触发。
接下来就看一下源码是怎么设计的(省略了大量无关代码):
|
|
伪代码
123456789 > public boolean dispatchTouchEvent(MotionEvent event) {> if (mOnTouchListener.onTouch(this, event)) {> return true;> } else if (onTouchEvent(event)) {> return true;> }> return false;> }>
点击事件在onTouchEvent()里
注意了(敲黑板)!
注意上面代码中存在一个
return true;
并且是只要 View 可点击就返回 true,就表示事件被消费了。
核心要点
- 事件分发原理: 责任链模式,事件层层传递,直到被消费。
- View 的
dispatchTouchEvent
主要用于调度自身的监听器和 onTouchEvent。 - View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
- 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
- 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
- ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
- ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
- 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
- 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
- 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。
本文主要内容转载自安卓自定义View进阶-事件分发机制详解