
2.3 View事件分发机制
从上面 ViewGroup 事件分发机制知道,View事件分发机制从 dispatchTouchEvent() 开始
源码分析
1 | /** |
源码总结
这里需要特别注意的是, onTouch() 的执行 先于 onClick()
核心方法总结
主要包括: dispatchTouchEvent() 、 onTouchEvent()
实例分析
在本示例中,将分析2种情况:
- 注册Touch事件监听 且 在 onTouch() 返回false
- 注册Touch事件监听 且 在 onTouch() 返回true
分析1:注册Touch事件监听 且 在onTouch()返回false
代码示例
1 | // 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false |
测试结果
1 | 执行了onTouch(), 动作是:0 |
测试结果说明
- 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次 onTouch() ;
- 因为 onTouch() 返回了false,所以事件无被消费,会继续往下传递,即调用 View.onTouchEvent() ;
- 调用 View.onTouchEvent() 时,对于抬起View事件,在调用 performClick() 时,因为设置了点击事件,所以会回调 onClick() 。
分析2:注册Touch事件监听 且 在onTouch()返回true
代码示例
1 | // 1. 注册Touch事件监听setOnTouchListener 且 在onTouch()返回false |
测试结果
1 | 执行了onTouch(), 动作是:0 |
测试结果说明
- 点击按钮会产生两个类型的事件-按下View与抬起View,所以会回调两次 onTouch() ;
- 因为 onTouch() 返回true,所以事件被消费,不会继续往下传递, View.dispatchTouchEvent() 直接返回true;
- 所以最终不会调用 View.onTouchEvent() ,也不会调用 onClick() 。
三. 事件分发机制流程总结
类型 | 相关方法 | Activity | ViewGroup | View |
---|---|---|---|---|
事件分发 | dispatchTouchEvent | √ | √ | √ |
事件拦截 | oninterceptTouchEvent | × | √ | × |
事件消费 | onTouchEvent | √ | √ | √ |
这个三个方法均有一个 boolean(布尔) 类型的返回值,通过返回 true 和 false 来控制事件传递的流程。
PS: 从上表可以看到 Activity和 View都是没有事件拦截的,这是因为:
- Activity 作为原始的事件分发者,如果 Activity 拦截了事件会导致整个屏幕都无法响应事件,这肯定不是我们想要的效果。
- View最为事件传递的最末端,要么消费掉事件,要么不处理进行回传,根本没必要进行事件拦截。
View相关
Question: 为什么 View 会有 dispatchTouchEvent ?
A:View 可以注册很多事件监听器,例如:单击事件(onClick)、长按事件(onLongClick)、触摸事件(onTouch),并且View自身也有onTouchEvent方法,那么问题来了,这么多与事件相关的方法应该由谁管理?毋庸置疑就是 dispatchTouchEvent,所以 View 也会有事件分发。
Question: 与 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。
ViewGroup相关
ViewGroup(通常是各种Layout) 的事件分发相对来说就要麻烦一些,因为 ViewGroup 不仅要考虑自身,还要考虑各种ChildView,一旦处理不好就容易引起各种事件冲突,正所谓养儿方知父母难啊。
VIewGroup的事件分发流程又是如何的呢?
我们了解到事件是通过ViewGroup一层一层传递的,最终传递给View,ViewGroup要比它的 ChildView 先拿到事件,并且有权决定是否告诉要告诉ChildView。在默认的情况下 ViewGroup事件分发流程是这样的。
- 判断自身是否需要(询问 onInterceptTouchEvent 是否拦截),如果需要,调用自己的 onTouchEvent。
- 自身不需要或者不确定,则询问ChildView,一般来说是调用手指触摸位置的 ChildView。
- 如果子 ChildView不需要则调用自身的onTouchEvent。
用伪代码应该是这样子的:
1 | // 点击事件产生后 |
安卓为了保证所有的事件都是被一个 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,后续事件不会再传递过来。
本文链接:
http://longzhiye.top/2023/12/23/2023-12-23/