Android 事件分发机制(二)
常见事件
既然是事件分发,总要有事件才能分发吧,所以我们先了解一下常见的几种事件。
根据面向对象思想,事件被封装成 MotionEvent 对象,由于本篇重点不在于此,所以只会涉及到几个与手指触摸相关的常见事件:
事件 | 简介 |
---|---|
ACTION_DOWN | 手指 初次接触到屏幕 时触发。 |
ACTION_MOVE | 手指 在屏幕上滑动 时触发,会会多次触发。 |
ACTION_UP | 手指 离开屏幕 时触发。 |
ACTION_CANCEL | 事件 被上层拦截 时触发。 |
对于单指触控来说,一次简单的交互流程是这样的:
手指落下(ACTION_DOWN) -> 移动(ACTION_MOVE) -> 离开(ACTION_UP)
- 本次事例中 ACTION_MOVE 有多次触发。
- 如果仅仅是单击(手指按下再抬起),不会触发 ACTION_MOVE。
事件分发、拦截与消费
√
表示有该方法。
X
表示没有该方法。
类型 | 相关方法 | ViewGroup | View |
---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ |
事件拦截 | onInterceptTouchEvent | √ | X |
事件消费 | onTouchEvent | √ | √ |
<RelativeLayout
android:background="#CCC"
android:id="@+id/layout"
android:onClick="myClick"
android:layout_width="200dp"
android:layout_height="200dp">
<View
android:clickable="true"
android:layout_width="200dp"
android:layout_height="200dp" />
</RelativeLayout>
现在你有了一个 RelativeLayout - View 你开开心心的为 RelativeLayout 设置了一个点击事件myClick
,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性 android:clickable="true"
正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论:
1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关
当手指点击有重叠区域时,分如下几种情况:
- 只有 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 消费,这是为什呢?主要是为了防止事件响应混乱,如果再一次完整的事件中分别将不同的事件分配给了不同的 View 容易造成事件响应混乱。
View 中 onClick 事件需要同时接收到 ACTION_DOWN 和 ACTION_UP 才能触发,如果分配给了不同的 View,那么 onClick 将无法被正确触发。
安卓为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。
如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来。
核心要点
- 事件分发原理: 责任链模式,事件层层传递,直到被消费。
- 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,后续事件不会再传递过来。