Ray 的记录站

日常开发实践记录

0%

Flutter 中手势事件的处理原理

在 Flutter 中, 手势系统有两个独立的抽象层组成, 第一层负责提供纯点位数据, 即 Pointers (光标, 下面均使用英文 Pointers)的点击位置和移动, 另外一层负责根据第一层的数据进行手势识别.

GestureBinding 处理手势. 过程是: 用户触摸事件由 Flutter 引擎通过 window.onPointerDataPacket 发送到 Flutter Framework, 由 GestureBinding 接收并进行处理.

GestureBinding 在处理这些手势数据时:

  1. 将引擎发送的坐标数据转换为逻辑像素坐标系下的坐标, 然后,
  2. 请求 renderView (Render tree 的根节点)提供包含该坐标的 RenderObject 子树(条件为: 子树中所有的结点都包含这个坐标).
  3. 遍历这个子树, 将事件派发给每个 RenderObject 节点.
  4. 当某个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// The kind of pointer device.
enum PointerDeviceKind {
/// A touch-based pointer device.
touch,

/// A mouse-based pointer device.
mouse,

/// A pointer device with a stylus.
stylus,

/// A pointer device with a stylus that has been inverted.
invertedStylus,

/// An unknown pointer device.
unknown
}

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 中的顶层工作流程大致如下:

  1. 先把 pointer 数据中的物理像素转换为逻辑像素, 即从屏幕坐标系下的点坐标转换为逻辑坐标系下的点坐标, 这样上层代码就不用去关心到底是何种设备了.

    1
    2
    3
    4
    // 通过 PointerEventConverter 传入物理像素数据和屏幕的像素密度(1x/2x/3x 等),
    // 将物理像素转换为逻辑点.
    // 其中 packet.data 是一个点位的 list.
    PointerEventConverter.expand(packet.data, window.devicePixelRatio)
  2. 转换后的数据(PointerEvent)加入到 _pendingPointerEvents 队列中.

  3. 依次处理队列中的 PointerEvent 数据.

第 3 个步骤即手势处理的核心内容, 所以下面展开来讲.

PointerEvent 队列的具体处理

在处理时根据事件类别对应处理, 总体上将 PointerEvent 分为四类来处理:

  • 启动: event 为 PointerDownEventPointerSignalEvent (需要进行hitTest)
  • 停止: event 为 PointerUpEventPointerCancelEvent(将对应的 hitTest 结果移除)
  • 过程中: event.down 状态为真(使用之前的 hitTest 结果)
  • 其他: event 为 PointerHoverEvent, PointerAddedEvent, PointerRemovedEvent (直接派发事件)

在进行了 hitTest 后获取结果, 将结果进行派发, 派发时按从叶子到树根的顺序依次向 target 发送 handleEvent, handleEvent 时

hitTest 的具体功能入口是在 RendererBinding 中, 通过 super 调用, 最终把结果送回到 GestureBinding 中.

参考

  1. 官方文档.