为什么有事件分发机制
-
因为Android的View是树状结构的;View可能会重叠在一起;
-
当我们点击的地方有很多歌View都可以响应时,就很尴尬了;
-
于是事件分发机制就用来解决这种问题
如图,View是一层一层嵌套的;当点击View1时,下面的ViewGroupA、RootView等也能够响应;为了确定到底要由哪个View处理盖点击事件,就需要事件分发机制来处理了;**
(图片来自原文)
View的结构
View的结构是树形的;
- 最顶层 View(Group) 的大小并不是填满父窗体的,留下了大量的空白区域
其中图中的DecorView用于显示主体颜色和标题栏等内容
PhoneWindows
Windows是一个抽象类,是所有视图的最顶层容器;视图的外观和行为都由他管,无论是北京显示,标题栏还是事件处理都是他管理的范畴;
但是,虽然能够管理的事情很多,但是不能直接使用**
- 于是PhoneWindows作为Window的唯一实现类;
而DecorView是PhoneWindow的一个内部类
DecorView还负责消息的传递;PhoneWindow的指示通过DecorView传递给下面的View,而下面的View也通过DecorView回传给PhoneWindow
事件分发、拦截与消费
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | onInterceptTouchEvent | × | √ | × |
事件消费 | onTouchEvent | √ | √ | √ |
三个方法均有一个boolean类型的返回值;通过返回true和false来控制时间传递的流程
- Activity和View都是没有时间拦截的;因为:Activityu作为原始的事件分发这;若Activity拦截了时间,就会导致整个屏幕都无法;View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截
View相关
为什么View会有dispatchTouchEvent
View可以注册很多事件监听器;那么这么多时间便由dispatchTouchEvent管理;
与View事件相关的各个方法的调用顺序
-
**单击事件(onClickListener) **需要两个两个事件(ACTION_DOWN 和 ACTION_UP )才能触发,如果先分配给onClick判断,等它判断完,用户手指已经离开屏幕,黄花菜都凉了,定然造成 View 无法响应其他事件,应该最后调用(最后)
-
**长按事件(onLongClickListener) **同理,也是需要长时间等待才能出结果,肯定不能排到前面,但因为不需要ACTION_UP,应该排在 onClick 前面。(onLongClickListener > onClickListener)
-
**触摸事件(onTouchListener)**如果用户注册了触摸事件,说明用户要自己处理触摸事件了,这个应该排在最前面。(最前)
-
**View自身处理(onTouchEvent)**提供了一种默认的处理方式,如果用户已经处理好了,也就不需要了,所以应该排在 onTouchListener 后面。(onTouchListener > onTouchEvent)
onTouchListener > onTouchEvent > onLongClickListener > onClickListener
-
1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
-
2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
eg:
1 | <RelativeLayout |
ViewGroup相关
ViewGroup不进需要考虑自身,还要考虑各种childview;
ViewGroup 要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉 ChildView;
默认情况下,ViewGroup的时间分发机制如下:
1.判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
2.自身不需要或者不确定,则询问 ChildView ,一般来说是调用手指触摸位置的 ChildView。
3.如果子 ChildView 不需要则调用自身的 onTouchEvent。
用伪代码应该是这样的:
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
此外,还有一些问题需要解决:
- ViewGroup 中可能有多个 ChildView,如何判断应该分配给哪一个?
把所有的 ChildView 遍历一遍,如果手指触摸的点在 ChildView 区域内就分发给这个View
- 当该点的 ChildView 有重叠时应该如何分配?
当 ChildView 重叠时,一般会分配给显示在最上面的 ChildView
后面加载的一般会覆盖掉之前的,所以显示在最上面的是最后加载的;
可点击事件:给View注册了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一个监听器或者设置了 android:clickable=”true” 就代表这个 View 是可点击的
给 View 注册 OnTouchListener 不会影响 View 的可点击状态。即使给 View 注册 OnTouchListener ,只要不返回 true 就不会消费事件
- ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),哪个会执行?
事件优先给 ChildView,会被 ChildView消费掉,ViewGroup 不会响应。
- 所有事件都应该被同一 View 消费
在上面的例子中我们分析后可以了解到,同一次点击事件只能被一个 View 消费,这是为什呢?主要是为了防止事件响应混乱,如果再一次完整的事件中分别将不同的事件分配给了不同的 View 容易造成事件响应混乱。
为了保证所有的事件都是被一个 View 消费的,对第一次的事件( ACTION_DOWN )进行了特殊判断,View 只有消费了 ACTION_DOWN 事件,才能接收到后续的事件(可点击控件会默认消费所有事件),并且会将后续所有事件传递过来,不会再传递给其他 View,除非上层 View 进行了拦截。
如果上层 View 拦截了当前正在处理的事件,会收到一个 ACTION_CANCEL,表示当前事件已经结束,后续事件不会再传递过来
- 要点:
1.事件分发原理: 责任链模式,事件层层传递,直到被消费。
2.View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。
3.View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
4.不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
事件是否被消费由返回值决定,true 表示消费,false表示不消费,与是否使用了事件无关。
5.ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
6.ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
7.一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
8.只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
9.如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。
事件分发流程
1 | Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View |
如果最后分发到View,如果这个View也没有处理事件;
如果没有任何View消费掉事件,那么这个事件会回传,传回到Activity,若Activity也没有处理,本次事件才会被抛弃
1 | Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View |
上层View既可以直接拦截该事件,自己处理,也可以先询问(分发给)子View,如果子View需要就交给子View处理,如果子View不需要还能继续交给上层View处理
// PhoneWindow 和 DecorView 无法直接操作
1.点击View1区域但没有任何View消费事件
当手指在 View1 区域点击了一下之后,如果所有View都不消耗事件,你就能看到一个完整的事件分发流程
红色箭头方向表示事件分发方向。
绿色箭头方向表示事件回传方向。
作者说:按照实际情况绘制,会导致流程图非常复杂和混乱
//我也是这么想的……
事件返回时dispatchTouchEvent直接指向了父View的 onTouchEvent 这一部分是不合理的,实际上它仅仅是给了父View的 dispatchTouchEvent 一个 false 返回值,父View根据返回值来调用自身的 onTouchEvent。
ViewGroup 是根据 onInterceptTouchEvent 的返回值来确定是调用子View的 dispatchTouchEvent 还是自身的 onTouchEvent, 并没有将调用交给 onInterceptTouchEvent。
ViewGroup 的事件分发机制伪代码如下,可以看出调用的顺序。
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
2.点击View1区域且被View1消费
如果被View1消费掉了,测时间会回传告诉上层View这个事件已经被解决了,上层View无需再响应;
3.点击 View1 区域但事件被 ViewGroupA 拦截
上层的View有权拦截事件,不传递给下层View,例如 ListView 滑动的时候,就不会将事件传递给下层的子 View
要点(原则)
1.如果事件被消费,就意味着事件信息传递终止。
2.如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃。
3.判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。
常见事件
- ACTION_DOWN 手指 初次接触到屏幕 时触发。
- ACTION_MOVE 手指 在屏幕上滑动 时触发,会会多次触发。
- ACTION_UP 手指 离开屏幕 时触发。
- ACTION_CANCEL 事件 被上层拦截 时触发。
一般单指触控的交互流程为:ACTION_DOWN->ACTION_MOVE->ACTION_UP;若是单击,则不会出发ACTION_MOVE
出处:http://www.gcssloop.com/customview/dispatch-touchevent-theory
http://www.gcssloop.com/customview/dispatch-touchevent-source