Flutter 中手势事件的处理原理
本文最后更新于 2021年6月3日 晚上
在 Flutter 中, 手势系统有两个独立的抽象层组成, 第一层负责提供纯点位数据, 即 Pointers (光标, 下面均使用英文 Pointers)的点击位置和移动, 另外一层负责根据第一层的数据进行手势识别.
由 GestureBinding
处理手势. 过程是: 用户触摸事件由 Flutter 引擎通过 window.onPointerDataPacket
发送到 Flutter Framework, 由 GestureBinding
接收并进行处理.
GestureBinding
在处理这些手势数据时:
- 将引擎发送的坐标数据转换为逻辑像素坐标系下的坐标, 然后,
- 请求
renderView
(Render tree 的根节点)提供包含该坐标的 RenderObject 子树(条件为: 子树中所有的结点都包含这个坐标). - 遍历这个子树, 将事件派发给每个 RenderObject 节点.
- 当某个 RenderObject 需要处理这样的事件时, 它就会进行处理.
Pointers
Pointers 表示用户和屏幕交互时候产生的数据, 有如下四种类型:
- PointerDownEvent: 比如手指在屏幕某个具体位置按下
- PointerMoveEvent: 比如手势在屏幕上进行位置移动
- PointerUpEvent: 比如手指离开了屏幕
- PointerCancelEvent: 这个 Pointer 相关的输入不会再发送给这个 APP.
在 PointerDownEvent 发生后, Framework 会先进行 hit test, hit test 的目的是找到 Pointer 所在位置的 Widget. 然后将 PointerDownEvent 派发(dispatch)给 hit test 的结果集合中的最里面那个(最靠近叶子的 Widget), 而后这个事件会从该 Widget 往上传递(顺着包含该 Pointer 的 Widget 层级), 最终到达树根.
在 Flutter 中没有提供 cancel/stop 这个派发(dispatch)过程的方法, 意味着事件肯定会最终到达树根.
在 Widget 层中可以使用 Listener 来监听纯 Pointer 事件, 但不推荐. 一般来说, 都是使用 GestureDetector
来监听手势.
在 Flutter 中有如下 Pointer 设备类型:
1 |
|
Gestures
手势(Gesture)是通过多条独立的 pointer event 计算而来的语义化对象, 这些 event 可以来自多个不同的 pointer. 同一个 Gesture 可以在其生命期内派发多个事件, 比如 drag start, drag update, 以及 drag end.
有如下 Gesture:
- Tap
- Double tap
- Long press
- Vertical drag
- Horizontal drag
- Pan
在 Widget 层中使用 GestureDetector
识别手势.
手势冲突处理
在屏幕的某一区域, 可能并存多个手势识别器, 它们都在监听一系列的 pointer 事件流, 并且尝试识别为具体手势. 若某个 pointer 有多个手势被识别到, framework 会将这些相关的识别器加入到一个手势竞技场 gesture arena 中, 在竞技场中通过如下规则判断应该让哪个手势胜出(识别):
At any time, a recognizer can declare defeat and leave the arena. If there’s only one recognizer left in the arena, that recognizer is the winner.
At any time, a recognizer can declare victory, which causes it to win and all the remaining recognizers to lose.
Gesture Binding 源码分析
Framework 层通过 GestureBinding
这个 mixin 启动 Gesture 系统的运行. 在 initInstances
方法中会将 window
上的 pointer 事件对接到它内部处理: window.onPointerDataPacket = _handlePointerDataPacket;
.
_handlePointerDataPacket
中的顶层工作流程大致如下:
先把 pointer 数据中的物理像素转换为逻辑像素, 即从屏幕坐标系下的点坐标转换为逻辑坐标系下的点坐标, 这样上层代码就不用去关心到底是何种设备了.
1
2
3
4// 通过 PointerEventConverter 传入物理像素数据和屏幕的像素密度(1x/2x/3x 等),
// 将物理像素转换为逻辑点.
// 其中 packet.data 是一个点位的 list.
PointerEventConverter.expand(packet.data, window.devicePixelRatio)转换后的数据(
PointerEvent
)加入到_pendingPointerEvents
队列中.依次处理队列中的
PointerEvent
数据.
第 3 个步骤即手势处理的核心内容, 所以下面展开来讲.
PointerEvent 队列的具体处理
在处理时根据事件类别对应处理, 总体上将 PointerEvent
分为四类来处理:
- 启动: event 为
PointerDownEvent
或PointerSignalEvent
(需要进行hitTest) - 停止: event 为
PointerUpEvent
或PointerCancelEvent
(将对应的 hitTest 结果移除) - 过程中:
event.down
状态为真(使用之前的 hitTest 结果) - 其他: event 为 PointerHoverEvent, PointerAddedEvent, PointerRemovedEvent (直接派发事件)
在进行了 hitTest 后获取结果, 将结果进行派发, 派发时按从叶子到树根的顺序依次向 target 发送 handleEvent
, handleEvent 时
hitTest 的具体功能入口是在 RendererBinding
中, 通过 super 调用, 最终把结果送回到 GestureBinding
中.
参考
- 官方文档.