跟🤡杰哥一起学Flutter (三十四、玩转Flutter手势✋)

1. 引言

😄 在前面的章节《三十、🖌玩转自定义绘制三部曲[中]》中简要介绍过「GestureDetector组件 」,并用它写了一些好玩的 "自定义手势" 案例:

😶 仅仅止步于这个组件的使用是远远不够的,一个完整的手势执行链路可以拆解为如下 "五个阶段":

物理触摸 (屏幕) -> 操作系统 (Android/iOS) -> Flutter 引擎 (Engine, C++) -> Flutter 框架 (Framework, Dart) -> 你的应用代码

😁 本节主要系统研究下 "Flutter框架层 " 的 "手势机制" 的实现原理,对研究过程不感兴趣的可以直接看流程图:

2. 核心概念

😶 先区分清楚两个 "核心概念"~

2.1. 指针事件

图形用户界面 (GUI ) 编程中,物理输入设备与屏幕交互的底层事件流 ,直接反映输入设备 (如手指、鼠标、触控笔等) 等的物理操作,它是所有 用户交互的最基础数据 ,描述了输入设备在屏幕上的 接触状态变化

一个完整的指针交互通常包含一系列完整的事件,形成完整的生命周期,最核心的四类指针事件:

  • 指针按下 :第一次接触到屏幕时触发,所有交互的起点
  • 指针移动 :指针在屏幕上按住并移动时连续触发,实现拖动、滑动等效果的数据基础
  • 指针释放 :当指针离开屏幕时触发,标志着 一次完整的触摸或点击操作 已经完成。
  • 指针取消:系统需要中断当前正在进行中的任何基于指针的操作,如突然的电话夺走了APP的焦点。

2.2. 手势

直接处理这些 原始的指针事件 的话,需要我们自己管理状态、判断事件间隔、移动距离,来识别用户到底是想 "单击" 还是 "长按"。而 "手势 " 是基于 "指针事件序列的高层交互抽象 ",将连续的指针动作解释为 "具有清晰业务含义的交互行为",如:点击、长按、Pan等。不用关注 down-move-up 的具体细节,直接响应 onTap、onPanUpdate 这样清晰的意图。

3. 常用控件 API

3.1. Listener 组件

Flutter最底层的手势监听器组件 ,它直接监听 触摸、鼠标等输入设备的原始指针事件 (PointerEvent) ,不会解释和组合手势,只是简单地 报告指针的状态变化 。支持同时跟踪多个指针的状态 (多指触控),适合自定义手势识别、需要高性能的场景。

dart 复制代码
/// 继承自 SingleChildRenderObjectWidget
class Listener extends SingleChildRenderObjectWidget {
  /// 构造函数
  const Listener({
    super.key,
    this.onPointerDown,      // 指针按下事件,提供 PointerDownEvent
    this.onPointerMove,      // 指针移动事件,提供 PointerMoveEvent
    this.onPointerUp,        // 指针抬起事件,提供 PointerUpEvent
    this.onPointerCancel,    // 指针取消事件,提供 PointerCancelEvent
    this.onPointerHover,     // 指针悬停事件,提供 PointerHoverEvent (主要用于鼠标)
    this.onPointerSignal,    // 指针信号事件,提供 PointerSignalEvent (如滚轮滚动)
    this.behavior,           // 命中测试行为
    super.child,
  });
}

// PointerEvent 的核心属性
pointer - 指针 ID,用于区分多个指针
kind - 设备类型(触摸、鼠标、手写笔等)
position - 全局坐标位置
localPosition - 相对于组件的本地坐标
delta - 移动增量(仅在 Move 事件中)
pressure - 压力值(支持压感的设备)
size - 接触面积大小

/// ✨ Listener使用代码示例:基础指针事件监听
Listener(
  onPointerDown: (PointerDownEvent event) {
    print('指针按下: ${event.localPosition}');
    print('设备类型: ${event.kind}');
    print('指针ID: ${event.pointer}');
  },
  onPointerMove: (PointerMoveEvent event) {
    print('指针移动: ${event.localPosition}');
    print('移动增量: ${event.delta}');
  },
  onPointerUp: (PointerUpEvent event) => print('指针抬起'),
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
    child: const Center(child: Text('触摸区域')),
  ),
)

/// 多指触控事件监听
final Set<int> _activePointers = <int>{};

Listener(
  onPointerDown: (event) {
    _activePointers.add(event.pointer);
    print('当前活跃指针数量: ${_activePointers.length}');
  },
  onPointerUp: (event) {
    _activePointers.remove(event.pointer);
    print('指针${event.pointer}已抬起');
  },
  child: Container(/* ... */),
)

3.2. GestureDecotor 组件

Flutter监听和响应用户手势操作最核心组件 (透明不可见),普通 Widget 想添加手势交互,外层套个 GestureDecotor 即可轻松实现 "点击赋能 🐶" 。先提下容易被忽视的 "behavior " 属性,它决定了 GestureDecotor 如何响应其区域内的指针,有下述可选值 (HitTestBehavior 枚举):

  • deferToChild (默认):只有当 child 的可见、不透明的部分被点击时才响应,比如:你的 child 是一个没有颜色的 Container,或者是一个有大片透明区域的 Image,那么点击这些透明区域是无效的 ❗️ ❗️ ❗️
  • opaque :GestureDetector 将自己视为一个不透明的矩形区域,即便 child 是透明的,点击 GestureDetector 的 整个区域都会触发事件 。同时,它会 阻止事件穿透,后面的组件不会再接收到这个点击事件。当你希望空白/透明区域都能响应点击,切不希望点击事件影响到堆叠在下层的组件时,可用此值。
  • translucent :与 opaque 类似,但它 允许事件穿透

接着梳理下具体的 手势回调属性 (单击、双击、长按、拖拽/平移、缩放 ) 涉及到的 API (不用死记,用到再查):

dart 复制代码
// ===================【💡 单击 💡】====================
onTap (GestureTapCallback?):当用户完成一次点击时触发。
onTapCancel (GestureTapCancelCallback?):当点击操作被取消时触发。
onTapDown (GestureTapDownCallback?):当用户手指按下时触发。
onTapUp (GestureTapUpCallback?):当用户手指抬起时触发。
// ✨ 触发流程:
//
// ① 点击时先触发 onTapDown() → 抬手时触发 onTapUp() → 最后回调 onTap() 表示一个单击事件的触发。
// ② 按下触发onTapDown,滑动再抬起手,会触发 onTapCancel(),而不会触发 onTapUp() 和 onTap()。


// ===================【💡 双击 💡】====================
onDoubleTap (GestureTapCallback?):当用户完成一次双击时触发。
onDoubleTapCancel (GestureTapCancelCallback?):当双击操作被取消时触发。
onDoubleTapDown (GestureTapDownCallback):当用户手指按下双击时触发。
// ✨ 触发流程:
// 
// ① 普通双击先触发 onDoubleTapDown(),然后回调 onDoubleTap()
// ② 触发 onDoubleTapDown(),未完成双击事件(如滑动),会触发 onDoubleTapDown()


// ===================【💡 长按 💡】====================
onLongPressDown (GestureLongPressDownCallback?):当用户手指按下长按时触发。
onLongPressStart (GestureLongPressStartCallback?):当长按开始时触发。
onLongPress (GestureLongPressCallback?):当用户完成一次长按时触发。
onLongPressMoveUpdate (GestureLongPressMoveUpdateCallback?):当长按过程中手指移动时触发。
onLongPressEnd (GestureLongPressEndCallback?):当长按结束时触发。
onLongPressUp (GestureLongPressUpCallback?):当长按结束时手指抬起时触发。
onLongPressCancel (GestureLongPressCancelCallback?):当长按操作被取消时触发。
// ✨ 触发流程:
// 
// ① 从上依次往下走,触点移动会不断触发 onLongPressMoveUpdate(),抬手触发 onLongPressEnd(),最后触发 onLongPressUp() 标志一次长按事件完成。
// ② 在onLongPressStart() 还没触发前滑动,会触发onLongPressCancel() 直接结束。


// ===================【💡 拖拽/平移 💡】====================
onPanDown (GestureDragDownCallback?):当用户手指按下平移时触发。
onPanStart (GestureDragStartCallback?):当平移开始时触发。
onPanUpdate (GestureDragUpdateCallback?):当平移过程中手指移动时触发。
onPanEnd (GestureDragEndCallback?):当平移结束时触发。 
onPanCancel (GestureDragCancelCallback?):当平移操作被取消时触发。
// ✨ 触发流程:
// 正常是从上往下走,onPanCancel() 的触发时机是 onPanStart() 之前取消,一般出现在手势嵌套的场景。
// 💡「HorizontalDrag-水平拖拽」和「VerticalDrag-竖直拖拽」可看作 Pan 事件在水平和垂直方向上的应用。
// 回调对象信息基本一致,方法名也雷同,只处理水平方向拖拽用前者,只处理竖直方向拖拽用后者。
// ❗️ 注:只能用一个,不能同时使用!!!如果需要处理复杂方向拖拽还是用 Pan!


// ===================【💡 缩放 💡】====================
onScaleStart (GestureScaleStartCallback?):当缩放开始时触发。
onScaleUpdate (GestureScaleUpdateCallback?):当缩放过程中手指移动时触发。
onScaleEnd (GestureScaleEndCallback?):当缩放结束时触发。
// ✨ 触发流程:从上往下,触点移动会不断触发 onScaleUpdate()
// ❗️ 注:Scale 是 Pan 的父类,两者不能同时使用,后者能做的,前者也能做。


// ===================【 参数类型 】====================
typedef GestureTapCallback = void Function();
typedef GestureTapCancelCallback = void Function();
typedef GestureTapDownCallback = void Function(TapDownDetails details);
typedef GestureTapUpCallback = void Function(TapUpDetails details);
typedef GestureLongPressDownCallback = void Function(LongPressDownDetails details);
typedef GestureLongPressStartCallback = void Function(LongPressStartDetails details);
typedef GestureLongPressCallback = void Function();
typedef GestureLongPressMoveUpdateCallback = void Function(LongPressMoveUpdateDetails details);
typedef GestureLongPressEndCallback = void Function(LongPressEndDetails details);
typedef GestureLongPressUpCallback = void Function();
typedef GestureLongPressCancelCallback = void Function();
typedef GestureDragDownCallback = void Function(DragDownDetails details);
typedef GestureDragStartCallback = void Function(DragStartDetails details);
typedef GestureDragUpdateCallback = void Function(DragUpdateDetails details);
typedef GestureDragEndCallback = void Function(DragEndDetails details);
typedef GestureScaleStartCallback = void Function(ScaleStartDetails details);
typedef GestureScaleUpdateCallback = void Function(ScaleUpdateDetails details);
typedef GestureScaleEndCallback = void Function(ScaleEndDetails details);

// 💡 TapDownDetails、TapUpDetails、LongPressDownDetails 记录的信息基本一致
// 「localPosition」→ Offset,触点相对于「组件左上角」的偏移量
// 「globalPosition」→ Offset,触点相对于「屏幕左上角」的偏移量
// 「localPosition」→ PointerDeviceKind(枚举值),触点设备类型,可选值:
// touch-点击、mouse-鼠标、stylus-针尖笔、invertedStylus-反向针尖笔、unkown-未知

// 💡 LongPressStartDetails 属性:localPosition、globalPosition
// 💡 LongPressEndDetails 属性:localPosition、globalPosition、velocity-结束时的速度信息
// 💡 LongPressMoveUpdateDetails 属性:localPosition、globalPosition、
//		offsetFromOrigin-开始触点到当前触点的「全局」偏移、localOffsetFromOrigin-开始触点到当前触点的「相对」偏移

// 💡 DragDownDetails 属性:localPosition、globalPosition
// 💡 DragStartDetails 属性:localPosition、globalPosition、
//    sourceTimeStamp-拖拽开始时间戳、kind-触点类型
// 💡 DragUpdateDetails 属性:localPosition、globalPosition
//		delta-每次触发时的偏移量、sourceTimeStamp-移动时的时间戳、primaryDelta-主方轴向上的初始偏移量(如未指定,默认null)
// 💡 DragEndDetails 属性:velocity-速度信息、primaryVelocity-主方向轴上的初始速度(如未指定,默认null)

// 💡 ScaleStartDetails 属性:focalPoint-触点与屏幕接触的焦点(平面左上角)、localFocalPoint-触点与屏幕接触的初始焦点(组件左上角)
//		pointerCount-触点个数
// 💡 ScaleUpdateDetails 属性:focalPoint、localFocalPoint、pointerCount、scale-缩放比例
// 		horizontalScale-水平缩放比例、verticalScale-垂直缩放比例、rotation-旋转角度(弧度)
// 💡 ScaleEndDetails 属性:velocity-速度信息、pointerCount-触点个数

没啥复杂,就是 重写关注手势对应的回调方法 ,写个不同手势行为打印日志的代码示例【--->c34/gesture_detector_demo.dart<---】,运行效果:

😀 日志如期把 对应手势的参数 都打印出来了,但如果细看 "单击" 触发的日志打印,会发现:

单击竟然会走长按的回调,而且是在OnTapDown() 之前

🤨 这是为啥咧?再加下 水平/垂直Drag 相关的回调,同样会触发回调:

😄 恭喜您,提前解锁知识点:手势竞争的幕后机制 ------"手势竞技场 (Gesture Arena) ",不过这个等下再讲,先把另外几个手势控件也提一嘴~

3.3. IgnorePointer & AbsorbPointer

Flutter 中用于 控制指针事件传递 的组件,它们都可以 阻止子组件接收用户的触摸事件。前者作用如下:

  • 忽略指针事件:让事件"穿透"到下层组件
  • 临时禁用交互:在特定条件下禁用某个区域的交互
  • 创建透明遮罩:制作可穿透的覆盖层

后者作用如下:

  • 吸收指针事件:完全阻止事件传递给下层组件
  • 创建模态遮罩:制作不可穿透的覆盖层
  • 防止误操作:在加载或处理过程中阻止用户操作

前者是 跳过当前往下传 ,后者是 直接不再往下传,核心代码:

dart 复制代码
/// IgnorePointer - 忽略指针事件
class IgnorePointer extends SingleChildRenderObjectWidget {
  const IgnorePointer({
    super.key,
    this.ignoring = true,      // 是否忽略指针事件让其直接穿透到下层
    this.ignoringSemantics,    // 是否忽略语义
    super.child,
  });
}

/// AbsorbPointer - 吸收指针事件
class AbsorbPointer extends SingleChildRenderObjectWidget {
  const AbsorbPointer({
    super.key,
    this.absorbing = true,     // 是否吸收指针事件阻止传递到下层
    super.child,
  });
}

/// IgnorePointer 示例 - 事件穿透
Stack(
  children: [
    // 下层按钮
    ElevatedButton(
      onPressed: () => print('下层按钮被点击'),
      child: const Text('下层按钮'),
    ),
    // 上层忽略指针的容器
    IgnorePointer(
      ignoring: true,  // 忽略事件,穿透到下层
      child: Container(
        width: 100,
        height: 100,
        color: Colors.red.withOpacity(0.5),
        child: const Center(child: Text('穿透层')),
      ),
    ),
  ],
)

/// AbsorbPointer 示例 - 事件吸收
Stack(
  children: [
    // 下层按钮
    ElevatedButton(
      onPressed: () => print('下层按钮被点击'),
      child: const Text('下层按钮'),
    ),
    // 上层吸收指针的容器
    AbsorbPointer(
      absorbing: true,  // 吸收事件,阻止传递
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue.withOpacity(0.5),
        child: const Center(child: Text('阻挡层')),
      ),
    ),
  ],
)

3.4. InkWell & InkResponse

Flutter 中提供 Material Design 风格触摸反馈效果-水波纹(Ripple) 的组件,两者创建的形状区域不同,InkWell-矩形-填充整个容器InkResponse-圆形-图标按钮等小组件 。❗️ 注意事项:必须配合 Material 父组件 使用,避免在滚动列表中过度使用,避免影响性能。核心代码:

dart 复制代码
/// InkWell - 矩形水波纹效果
class InkWell extends InkResponse {
  const InkWell({
    super.key,
    super.child,
    super.onTap,               // 点击回调
    super.onDoubleTap,         // 双击回调
    super.onLongPress,         // 长按回调
    super.onTapDown,           // 按下回调
    super.onTapUp,             // 抬起回调
    super.onTapCancel,         // 取消回调
    super.onHighlightChanged,  // 高亮状态变化
    super.onHover,             // 悬停回调(鼠标)
    super.focusColor,          // 焦点颜色
    super.hoverColor,          // 悬停颜色
    super.highlightColor,      // 高亮颜色
    super.overlayColor,        // 覆盖层颜色
    super.splashColor,         // 水波纹颜色
    super.splashFactory,       // 水波纹工厂
    super.radius,              // 水波纹半径
    super.borderRadius,        // 边框圆角
    super.customBorder,        // 自定义边框
    super.enableFeedback,      // 是否启用触觉反馈
    super.excludeFromSemantics, // 是否从语义中排除
    super.focusNode,           // 焦点节点
    super.canRequestFocus,     // 是否可以请求焦点
    super.onFocusChange,       // 焦点变化回调
    super.autofocus,           // 是否自动焦点
  });
}

/// InkResponse - 圆形水波纹效果
class InkResponse extends StatelessWidget {
  const InkResponse({
    super.key,
    this.child,
    this.onTap,
    this.onTapDown,
    this.onTapUp,
    this.onTapCancel,
    this.onDoubleTap,
    this.onLongPress,
    this.onHighlightChanged,
    this.onHover,
    this.containedInkWell = false,    // 是否包含在 InkWell 中
    this.highlightShape = BoxShape.circle, // 高亮形状
    this.radius,                      // 水波纹半径
    this.borderRadius,                // 边框圆角
    this.customBorder,                // 自定义边框
    this.focusColor,                  // 焦点颜色
    this.hoverColor,                  // 悬停颜色
    this.highlightColor,              // 高亮颜色
    this.overlayColor,                // 覆盖层颜色
    this.splashColor,                 // 水波纹颜色
    this.splashFactory,               // 水波纹工厂
    this.enableFeedback = true,       // 是否启用触觉反馈
    this.excludeFromSemantics = false, // 是否从语义中排除
    this.statesController,            // 状态控制器
    this.focusNode,                   // 焦点节点
    this.canRequestFocus = true,      // 是否可以请求焦点
    this.onFocusChange,               // 焦点变化回调
    this.autofocus = false,           // 是否自动焦点
  });
}

// 水波纹动画过程
onTapDown → 创建 InkSplash → 开始扩散动画 → onTapUp → 开始消散动画 → 动画完成 → 移除效果

// 状态管理链路
Normal → Pressed (highlightColor) → Released →
Normal 或 Focused (focusColor) 或 Hovered (hoverColor)

// 💡 基础用法示例

// 1. 简单的 InkWell 使用
Material(
  child: InkWell(
    onTap: () => print('InkWell 被点击'),
    child: Container(
      padding: EdgeInsets.all(16),
      child: Text('点击我'),
    ),
  ),
)

// 2. 自定义颜色的 InkWell
Material(
  child: InkWell(
    onTap: () => print('自定义颜色'),
    splashColor: Colors.blue.withOpacity(0.3),    // 水波纹颜色
    highlightColor: Colors.blue.withOpacity(0.1), // 高亮颜色
    child: Container(
      padding: EdgeInsets.all(16),
      child: Text('自定义颜色'),
    ),
  ),
)

// 3. 圆形的 InkResponse
Material(
  shape: CircleBorder(),
  child: InkResponse(
    onTap: () => print('圆形按钮'),
    radius: 24,  // 设置水波纹半径
    child: Padding(
      padding: EdgeInsets.all(12),
      child: Icon(Icons.favorite),
    ),
  ),
)

// 4. 自定义形状的 InkWell
Material(
  borderRadius: BorderRadius.circular(12),
  child: InkWell(
    onTap: () => print('圆角矩形'),
    customBorder: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    child: Container(
      padding: EdgeInsets.all(16),
      child: Text('圆角矩形'),
    ),
  ),
)

// 💡 高级用法示例

// 1. 多种手势响应
Material(
  child: InkWell(
    onTap: () => print('单击'),
    onDoubleTap: () => print('双击'),
    onLongPress: () => print('长按'),
    onTapDown: (details) => print('按下: ${details.localPosition}'),
    onTapUp: (details) => print('抬起: ${details.localPosition}'),
    onTapCancel: () => print('取消'),
    child: Container(
      width: 100,
      height: 100,
      child: Center(child: Text('多手势')),
    ),
  ),
)

// 2. 状态监听
Material(
  child: InkWell(
    onHighlightChanged: (highlighted) {
      print('高亮状态: $highlighted');
    },
    onHover: (hovered) {
      print('悬停状态: $hovered');  // 仅鼠标/触控板
    },
    onFocusChange: (focused) {
      print('焦点状态: $focused');
    },
    child: Container(
      padding: EdgeInsets.all(16),
      child: Text('状态监听'),
    ),
  ),
)

// 3. 自定义水波纹工厂
Material(
  child: InkWell(
    splashFactory: InkRipple.splashFactory,  // 使用 Ripple 效果
    // splashFactory: InkSplash.splashFactory, // 使用 Splash 效果
    onTap: () => print('自定义工厂'),
    child: Container(
      padding: EdgeInsets.all(16),
      child: Text('自定义水波纹'),
    ),
  ),
)

💁‍♂️ 了解完API,接着开始讲原理咯~

4. Hit Testing - 命中测试

一个系统性的过程,用来确定当用户与屏幕交互时 (如点击、触摸),哪些 视觉元素 应该接收这个事件。简单点说就是:你在屏幕上点了下,坐标是(x,y),框架需要回答这个问题:"在这个点下面,从前往后都有哪些Widget?谁应该对这个点击负责? "这个过程发生在任何手势被识别之前,它只找到事件的 "目标" 列表。

4.1. 命中测试流程

这个过程主要发生在 渲染层 ,涉及的是 RenderObject 树,而不是 Widget 树,可以分为下面五个关键步骤:

4.1.1. 事件的起点

  • 当用户的 物理触摸 (或鼠标点击) 发生时,Flutter引擎 会捕捉到这个 原始指针事件 (如 PointerDownEvent ),并将其传递给 Flutter Framework 的顶层。
  • GestureBinding 这个单例接收到事件,并调用 RendererBinding.hitTest() 启动整个命中测试流程。

4.1.2. "从上到下"的遍历

  • 命中测试从 渲染树的根节点 RenderView 开始,以 深度优先 的方式遍历 RenderObject树,但有一个重要的视觉规则:后绘制的子节点 (视觉上更靠前) 会先被测试。如:在一个 Stack 中,最后一个子节点会最先被测试。
  • 遍历过程会携带一个"侦探的笔记本"------ HitTestResult 对象,它的任务就是记录所有被命中的嫌疑人。

4.1.3. 每个 RenderObject 的"自检"

当命中测试算法访问到每一个 RenderObject 时,其 hitTest() 会被调用,在这个方法中,它需要做两件事:

  • 判断自己是否被命中 :通常是通过检查 触摸点坐标position 是否落在自己的几何边界内,如:size.contains(position),但是,RenderObject 也可以有自己的 特殊规则,如:RenderIgnorePointer 直接撒谎说 "我没被命中",即使坐标在它的范围内。有些 RenderObject 还能实现更复杂的操作,如:只有在非透明像素区域被点击时才算命中。
  • 决定下一步该怎么做hitTest() 的返回值和行为决定了整个过程的走向,如果自己没被命中,通常会返回 false,命中测试会继续进行,去测试同一父节点下的其它兄弟节点,或者父节点后面的节点。如果自己被命中了,调用 result.add(BoxHitTestEntry(this, position)) 将自己添加到 "笔记本 " (HitTestResult ) 中,然后决定 是否让子节点继续测试 ,若有一个子节点返回 true ,那它自己也应该返回 true,告诉上层:"我活我的某个后代被命中了"。

4.1.4. HitTestResult 的形成

HitTestResult 是一个列表,但它的添加顺序非常重要,它记录了一条 从最具体的命中目标 (视觉上最顶层) 到最不具体 (根节点) 的事件分发路径。如:你点击了一个 Scaffold -> Column -> Card -> InkWell -> Text 结构中的 Text,HitTestResult 列表看起来会是这样,即列表的第一个元素是视觉上最顶层的命中目标:

Entry for Text's RenderObject, Entry for InkWell's RenderObject, Entry for Card's RenderObject, Entry for Column's RenderObject, ... Entry for RenderView

4.1.5. 事件的分发 (冒泡)

命中测试结束后,GestureBinding 就拿到了这个填满了嫌疑人的 HitTestResult 列表。现在,指针事件 (如 PointerDownEvent ) 会沿着这个列表从头到尾进行分发,每个 RenderObject 都有机会处理这个事件,这就是为什么父组件可以捕获到子组件的事件。这个过程通常被称为 "事件冒泡",尽管在 Flutter 中它更像是一个沿着预定路径的分发,而不是严格意义上的冒泡。

😄 了解完 HitTest 的机制,现在我们可以更好地理解 IgnorePointerAbsorbPointer 组件的原理啦~

4.2. IgnorePointer 的"穿透"原理

IgnorePointer 的渲染对象是 RenderIgnorePointer ,看下 hitTest() 方法:

ignoring 为 true 时,不调用 super.hitTest() ,即不会进行 子组件 的命中测试,返回 false 表示该组件没有被命中,事件会继续传播到下层组件。而正常时是在 super.hitTest() 中会调用 RenderProxyBoxMixin.hitTestChildren() 将命中测试传递给子组件:

4.3. AbsorbPointer 的"吸收"原理

AbsorbPointer 的渲染对象则是 RenderAbsorbPointer,看下它的 hitTest() 方法:

absorbingtrue 时,返回 size.contains(position) ,即:如果触摸点在组件范围内,返回 true (表示命中),否则返回 false。另外,因为不调用 super.hitTest() ,意味着不会将事件传递给子组件。反之,调用 super.hitTest() 进行正常的命中测试和事件传递。

5. Gesture Arena - 手势竞技场

Hit Testing (命中测试) 完成后,GestureBinding 会将指针事件 (如 PointerDownEvent) 通过 PointerRouter 路由给所有在 HitTestResult 列表中的、且对手势感兴趣的 GestureRecognizer

😶 回顾上面 GestureDetector 组件,同时监听 onTaponLongPressonXxxDrag 时,"单击手势 " 却触发了长按、拖拽相关回调的问题。💁‍♂️ 其实,GestureDetector 背地里创建了好几个,手势识别器 (Gesture Recognizer) 。在你手指刚刚按下那一刻,Flutter 并不知道你接下来想做什么,你得意图可能是:单击、长按、双击的第一次点击、拖拽开始,因为未来是不确定的,所以 Flutter 的策略是:

让所有可能的手势识别器都进入准备状态,并加入手势竞技场进行竞争🤠

🤔 所以,上面的情况实际上是:

手势按下时,PointerDownEvent 发生,TapGestureRecognizer (负责单击和双击) 和 LongPressGestureRecognizer (负责长按) 等识别器被唤醒,都加入"手势竞技场 ",各自进入"准备"状态。

  • LongPressGestureRecognizer 立即触发 onLongPressDown() ,表示:"一个潜在的长按开始了,我启动一个计时器,如果用户按住超过预设时间 (500ms),我就会宣告胜利。"
  • TapGestureRecognizer 则立即触发 onTapDown() ,表示:"一个潜在的单击开始了,我正在观察用户会不会很快抬起手指。"

如果计时器还没到时间,手指就抬起了,PointerUpEvent 发生:

  • LongPressGestureRecognizer 立即意识到 "用户抬手了,长按条件不满足,宣告自己落败",这个失败的动作会触发 onLongPressCancel() ,表示:"之前潜在的长按,现在被取消了"。
  • TapGestureRecognizer 则会触发 onTapUp() ,表示:"用户在有效点击区域内抬起了手指,单击手势已确认完成,宣告胜利"。

😄 还是很好理解的,接着从 "文字层面 " 梳理下 "参赛选手 & 比赛规则 ",加深下对 "手势竞技场" 的认识。

5.1. 参赛选手

Hit Testing 后生成 HitTestResult 列表中的 Widget 中 "注册 " 的 GestureRecognizer (手势识别器)。

这个 "注册 " 指的是:GestureDetector 或其它手势组件,通过其构造函数的属性 (如onTap),实例化了一个或多个具体的 GestureRecognizer 子类,并准备在自己被 "命中" 时将这些识别器送入竞技场参与竞争。比如当你写下 GestureDetector(onTap: () {...}),内部其实做了两件关键的事:

  • 创建一个 TapGestureRecognizer 的实例专门用来识别 "单击" 手势。
  • 把你通过 onTap() 传入的 匿名函数TapGestureRecognizer 实例的 onTap属性 关联起来。

💁‍♂️ 一个完整的流程就是:

手指按下屏幕 (PointerDownEvent) → 命中测试形成包含所有命中Widget的列表遍历命中列表中的每个Widget 检查是否注册了GestureRecognizer → 是的话,把对应的GestureRecognizer加入手势竞技场 → 比赛开始,后续的 PointerMoveEventPointerUpEvent 将决定谁是最终的胜利者。

5.2. 比赛规则

比的是 "谁能活到最后 ",每个手势识别器 (GestureRecognizer) 都有自己的 "生存条件 " 和 "放弃条件 ",比赛的进程就是 不断根据用户的输入 (移动、时间) 来淘汰那些不符合条件的选手。


优先级链条

  • 最优先 (明确、主动胜利) → 拖动 (Drag/Pan)长按 (LongPress) 只要满足各自的 (移动/时间) 条件,就会立即获胜、终结比赛。
  • 次优先 (需要等待) → 双击 (DoubleTap) 有权让其它手势 (尤其是单击-Tap) 等待,看自己是否成功。
  • 默认/兜底选项 → 通常是其他手势都不满足条件后的"默认"赢家,因为需要确认其他可能性 (双击、拖动等) 都已经排除,所以往往它的触发时机最晚,
  • 特殊情况 (多点触碰) → 缩放/旋转 (Scale/Rotate) 通常需要两个或更多手指,当第二个手指按下时,Flutter 会分析多个指针的组合行为。ScaleGestureRecognizer 会观察多个指针的相对运动,如果检测到捏合或旋转,它会战胜那些单指的拖动识别器。

多组件嵌套场景示例

ListView中有一个GestureDetector(onTap) 的 item

  • 用户垂直拖动TapGestureRecognizerListViewVerticalDragGestureRecognizer 同时入场。手指开始垂直移动,前者发现移动超限 (>kTouchSlop,约18像素),立即认输,后者发现移动超过阈值,且方向正确,立即宣告胜利。结果是:ListView 滚动,onTap() 绝不会触发。
  • 用户快速点击:两者同时入场,手指抬起,移动距离很小(<kTouchSlop),前者胜利条件未满足,后者获胜,结果是:onTap() 触发,ListView不滚动。

ListView (垂直) 嵌套 ListView(水平)

  • 定向拖动识别器 (VerticalDragXxx 和 HorizontalDragXxx) 是 "良性竞争" 的关系吗,它们不会互相"抢夺",而是根据用户的 初始拖动方向,非常绅士地决定谁应该响应。

😏 看到这里,部分读者可能想起经典的 "滑动嵌套冲突处理问题 ",这会在后续章节进行探讨,本节主要还是深扒 Flutter手势机制 为主。从"宏观 "层面了解完 "手势竞技场 ",接着深入到源码,从"微观"层面继续深化理解。

5.3. GestureDecotor 源码

继承 StatelessWidget,构造函数参数超过50个,主要分为几大类:

  • 基础属性:child-子组件、behavior-命中测试行为、excludeFromSemantics-是否从语义树中排除、supportedDevices-支持的设备类型、dragStartBehavior-拖拽开始行为 (down-按下位置位置开始、start-手势胜出位置开始)
  • 点击类手势:onTap, onTapDown, onTapUp, onTapCancel、onSecondaryTap、onSecondaryTapDown、onSecondaryTapUp、onTertiaryTap 系列、onDoubleTap 系列
  • 长按类手势:onLongPress, onLongPressStart, onLongPressEnd、onSecondaryLongPress 系列onTertiaryLongPress 系列
  • 拖拽类手势:onVerticalDrag 系列 (垂直拖拽)、onHorizontalDrag 系列 (水平拖拽)、onPan 系列 (平移)
  • 缩放和力度:onScale 系列 (缩放)、onForcePress 系列 (力度按压)

build() 方法可以看出,GestureDecotor 的核心作用其实是:创建和管理各种手势识别器

dart 复制代码
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

// 根据回调函数决定创建哪些手势识别器
// 💡 GestureRecognizerFactoryWithHandlers 具体工厂实现
if (onTapDown != null || onTapUp != null || onTap != null || ...) {
  gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(...);
}

// 💡 最终返回一个【RawGestureDetector】对象
return RawGestureDetector(
  gestures: gestures,
  behavior: behavior,
  excludeFromSemantics: excludeFromSemantics,
  child: child,
);

😄 把里面涉及到的 两个类的核心代码 拎出来讲一讲,这种 "延迟加载" 的玩法值得借鉴:

dart 复制代码
// 💡 GestureRecognizerFactory 抽象工厂基类
// 定义了创建手势识别器的统一接口、泛型约束、
// 分离关注点:创建-constructor() 和 配置-initializer()
@optionalTypeArgs
abstract class GestureRecognizerFactory<T extends GestureRecognizer> {
  const GestureRecognizerFactory();
  T constructor();
  void initializer(T instance);
}

// 💡 GestureRecognizerFactoryWithHandlers 具体工厂实现
class GestureRecognizerFactoryWithHandlers<T extends GestureRecognizer> extends GestureRecognizerFactory<T> {
  const GestureRecognizerFactoryWithHandlers(this._constructor, this._initializer);
  final GestureRecognizerFactoryConstructor<T> _constructor;
  final GestureRecognizerFactoryInitializer<T> _initializer;

  @override
  T constructor() => _constructor();

  @override
  void initializer(T instance) => _initializer(instance);
}

// 💡 使用处实现了【延迟创建】
// ① GestureDetector.build() 这里没调用构造函数创建实例,只是把【构造函数】存储在Factory中。
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

if (onTapDown != null || onTapUp != null || onTap != null || ...) {
  gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
    () => TapGestureRecognizer(debugOwner: this, supportedDevices: supportedDevices),  // 📝 这里只是存储了一个创建函数
    (TapGestureRecognizer instance) {
      instance
        ..onTapDown = onTapDown
        ..onTapUp = onTapUp
        ..onTap = onTap
        // ... 更多配置
    },
  );
}
// ② RawGestureDetectorState.syncAll() 【真正的创建】
void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
  assert(_recognizers != null);
  final Map<Type, GestureRecognizer> oldRecognizers = _recognizers!;
  _recognizers = <Type, GestureRecognizer>{};
  for (final Type type in gestures.keys) {
    assert(gestures[type] != null);
    assert(gestures[type]!._debugAssertTypeMatches(type));
    assert(!_recognizers!.containsKey(type));
    // 🎯 这里才是真正创建手势识别器的地方!
    _recognizers![type] = oldRecognizers[type] ?? gestures[type]!.constructor();  // ← 调用 factory.constructor()
    assert(_recognizers![type].runtimeType == type, 'GestureRecognizerFactory of type $type created a GestureRecognizer of type ${_recognizers![type].runtimeType}. The GestureRecognizerFactory must be specialized with the type of the class that it returns from its constructor method.');
    gestures[type]!.initializer(_recognizers![type]!);  // ← 调用 factory.initializer()
  }
  for (final Type type in oldRecognizers.keys) {
    if (!_recognizers!.containsKey(type)) {
      oldRecognizers[type]!.dispose();  // 🗑️ 销毁不需要的识别器
    }
  }
}

上面的 GestureDecotor.build() 最终返回一个 RawGestureDetector 对象,它是Flutter手势系统的 "基石 ",而 GestureDetector 其实是基于它的一层封装,旨在提供更便捷的 API 供开发者使用。

5.4. RawGestureDetector 源码

它充当了 "原始指针事件 (Pointer Events) " 和 "手势识别器 (Gesture Recognizers) " 间的桥梁,允许你精确配置一个或多个GestureRecognizer,并将它们应用到其 child 子组件上。它还是 "手势竞技场 (Gesture Arena) " 的入口,当一个 指针事件 (如触摸按下) 发生时,将该事件 "喂给" 所有配置好的识别器,并让它们进入竞技场进行"竞争",以决定哪个手势最终胜出。构造方法如下:

dart 复制代码
class RawGestureDetector extends StatefulWidget {
  const RawGestureDetector({
    super.key,
    this.child,                    // 🎯 子组件
    this.gestures = const <Type, GestureRecognizerFactory>{}, // 🌟 手势识别器工厂映射
    this.behavior,                 // 📍 命中测试行为
    this.excludeFromSemantics = false, // ♿ 是否排除语义化
    this.semantics,               // 🎭 语义化手势代理
  });
}

不难看出最核心的属性是 gesturesKey(Type) 是手势识别器的类型,如 TapGestureRecognizer,而Value(GestureRecognizerFactory) 是工厂对象,用于创建和配置手势识别器的实例,还算简单,因为真正的工作其实都是在它的 State类 中完成的。

5.5. _RawGestureDetectorState 源码

😄 代码略多,懒得贴了,直接上时序图 ,先是 "初始化阶段 ":工厂模式创建并配置所有识别器,建立 手势类型识别器实例 的映射关系 → 结果:所有识别器准备就绪,等待接收指针事件

然后是 "指针事件处理阶段 ",将用户的触摸事件分发给所有注册的识别器,让每个识别器都有机会分析这个事件 → 结果:所有识别器都接收到同一个指针事件并进入手势竞技场

接着是 "竞技场仲裁阶段 ",多个手势识别器在竞技场中分析指针轨迹,通过算法确定哪个手势最符合用户意图 → 结果:选出一个胜出的识别器,其他识别器被标记为失败

再接着是 "回调触发与UI更新阶段",胜出的识别器触发对应的用户回调函数,执行业务逻辑并更新界面状态 → 结果:用户看到预期的交互响应,完成一次完整的手势处理流程。

最后是 "资源清理阶段 ",失败的识别器清理资源,重置状态准备处理下一次手势事件 → 结果:系统回到初始状态,等待下一次用户交互

流程图中的 GestureArenaManager-竞技场管理者竞技裁决中 起到了关键作用,跟下源码。

5.6. GestureArenaManager 源码

全局单例 ,它只有一个 _arenas 属性,L类型 MapKey触点idValue_GestureArena (竞技场),即 "每个触点对应一个竞技场。GestureArenaManager 的作用就是:

为每个指针创建竞技场,协调手势识别器间的竞争,通过复杂的仲裁算法决定最终的胜出者。

核心代码:

dart 复制代码
/// 为每个指针维护一个竞技场映射
Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

// 📝【注册阶段】当用户触摸屏幕时
PointerDownEvent → RawGestureDetector._handlePointerDown()
                 ↓
              for (recognizer in _recognizers.values) {
                recognizer.addPointer(event);  // 🏟️ 每个识别器都"进入竞技场"
              }
                 ↓
              GestureArenaManager.add(pointer, recognizer)

/// 添加识别器到竞技场
GestureArenaEntry add(int pointer, GestureArenaMember member) {
  // 为新指针创建竞技场
  final _GestureArena arena = _arenas.putIfAbsent(pointer, () => _GestureArena());
  
  // 将识别器添加到竞技场
  arena.add(member);
  
  return GestureArenaEntry._(arena, member);
}

/// 🧠【监听后续事件 + 识别器 + 仲裁阶段】
时间维度:分析手势持续时间(单击vs长按)
空间维度:分析手势移动轨迹(点击vs拖拽)
频率维度:分析点击频率(单击vs双击)
优先级:某些手势有更高优先级

/// 🏆【决胜阶段】
// 1. 识别器确定自己应该获胜时
GestureArenaEntry entry = GestureBinding.instance.gestureArena.add(pointer, recognizer);
entry.resolve(GestureDisposition.accepted); 

/// 2. GestureArenaManager._resolve() 被调用
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
  final _GestureArena? arena = _arenas[pointer];
  if (arena == null) return;
  
  if (disposition == GestureDisposition.accepted) {
    // 🏆 设置急切获胜者,即第一个调用 resolve(accepted) 的识别器
    arena.eagerWinner ??= member;
    // ⚡ 尝试解决竞技场
    _tryToResolveArena(pointer, arena);
  } else {
    // ❌ 识别器失败,从竞技场移除
    arena.members.remove(member);
    member.rejectGesture(pointer);  // 通知识别器被拒绝
    _tryToResolveArena(pointer, arena);
  }
}

/// 3. _tryToResolveArena() 执行仲裁
void _tryToResolveArena(int pointer, _GestureArena arena) {
  if (arena.isHeld || arena.hasPendingSweep) return;
  
  if (arena.members.length == 1) {
    // 🎯 只有一个识别器,直接获胜
    arena.members.first.acceptGesture(pointer);
  } else if (arena.eagerWinner != null) {
    // 🏆 有急切获胜者,让它获胜
    arena.eagerWinner!.acceptGesture(pointer);
    // ❌ 拒绝所有其他识别器
    for (final GestureArenaMember member in arena.members) {
      if (member != arena.eagerWinner) {
        member.rejectGesture(pointer);
      }
    }
  }
  
  // 🧹 清空竞技场
  arena.members.clear();
  _arenas.remove(pointer);
}

/// 4. 胜出识别器的 acceptGesture() 被调用
@override
void acceptGesture(int pointer) {
  // 📞 触发用户回调
  invokeCallback<void>('onTap', onTap);
}

接着看看竞技场 "核心三剑客"的源码。

5.6.1. GestureArenaMember - 竞技场成员抽象接口

定义了参与者的基本行为契约,响应竞技场的仲裁结果 (接受/失败),核心代码:

dart 复制代码
abstract class GestureArenaMember {
  /// 🏆 当识别器在竞技场中获胜时调用
  // 1. 标记为已接受状态
  // 2. 触发相应的用户回调(如 onTap)
  // 3. 开始正式处理手势事件,如:invokeCallback<void>('onTap', onTap);
  void acceptGesture(int pointer);
  
  /// ❌ 当识别器在竞技场中失败时调用
  // 1. 停止追踪该指针
  // 2. 清理相关资源
  // 3. 可能触发取消回调()
  // stopTrackingPointer(pointer);
  void rejectGesture(int pointer);
}

// ✅ 主要实现类
TapGestureRecognizer          // 单击手势
DoubleTapGestureRecognizer    // 双击手势  
LongPressGestureRecognizer    // 长按手势
PanGestureRecognizer          // 平移手势
ScaleGestureRecognizer        // 缩放手势

5.6.2. _GestureArena - 竞技场实体类

私有类,单个指针的竞技场实体,管理该指针的所有识别器,记录急切获胜者、控制仲裁时机,核心代码:

dart 复制代码
class _GestureArena {
  /// 竞技场成员列表
  final List<GestureArenaMember> members = <GestureArenaMember>[];
  
  /// 急切获胜者(第一个申请获胜的识别器)
  GestureArenaMember? eagerWinner;
  
  /// 是否被持有(延迟仲裁)
  bool isHeld = false;
  
  /// 是否有待处理的清扫
  bool hasPendingSweep = false;

  /// 添加成员到竞技场
  void add(GestureArenaMember member) {
    members.add(member);
  }
  
  /// 从竞技场移除成员
  bool remove(GestureArenaMember member) => members.remove(member);
  
  /// 判断竞技场是否为空
  bool get isEmpty => members.isEmpty;
  
  /// 判断是否为唯一成员
  bool isOnlyMember(GestureArenaMember member) {
    return members.length == 1 && members.first == member;
  }
  
  /// 获取成员数量
  int get length => members.length;

  // 🏆 急切获胜者:第一个主动申请获胜的识别器
  void setEagerWinner(GestureArenaMember member) {
    eagerWinner ??= member;  // 只设置一次,先到先得!
  }
}

5.6.3. GestureArenaEntry - 识别器 & 控制器的唯一桥梁

提供 resolve() 方法让识别器控制自己的竞争结果,生命周期 arena.add() 创建 → resolve() 销毁,核心代码:

dart 复制代码
class GestureArenaEntry {
  /// 解决竞争 - 识别器的唯一出口
  void resolve(GestureDisposition disposition) {
    // 委托给 GestureArenaManager 处理
    GestureBinding.instance.gestureArena._resolve(
      _pointer, 
      _member, 
      disposition
    );
  }
}

/// 手势处置枚举
enum GestureDisposition {
  accepted,  // 申请获胜
  rejected,  // 主动退出
}

// ✅ 识别器确定手势模式后申请获胜
entry.resolve(GestureDisposition.accepted);

// ❌ 识别器发现不匹配时主动退出
entry.resolve(GestureDisposition.rejected);

5.6.4. 完整调用链路图

有点散乱,画个图帮助理解 (识别器有:单击、长按和拖拽,最后裁定单击是胜利者🏆):

🤔 GestureArenaManager 更像是一个 "仲裁员 " 而非 "决策者 ",它的职责是:为每个指针创建独立的竞技场,管理识别器的注册,处理获胜申请 (但不决定谁获胜)、执行仲裁结果 。"真正的决策权" 还是在各个GestureRecognizer (手势识别器) 的手上,每个识别器内部都是一个 "复杂的状态机",用于跟踪指针事件序列以判断是否构成了特定手势。

5.7. GestureRecognizer 源码

😶 "手势识别器 " 的 顶级父类 ,看下 类的继承结构

核心代码

dart 复制代码
abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
  // 调试所有者 - 用于调试信息
  final Object? debugOwner;
  
  // 设备手势配置
  DeviceGestureSettings? gestureSettings;
  
  // 支持的设备类型
  Set<PointerDeviceKind>? supportedDevices;
  
  // 按钮过滤器 - 控制哪些按钮可以触发手势
  final AllowedButtonsFilter _allowedButtonsFilter;
  
  // 指针ID到设备类型的映射
  // PointerDeviceKind 是一个枚举:touch-点击、mouse-鼠标、stylus-针尖笔
  // invertedStylus-反向针尖笔、unknown-未知
  final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};

  // 四个核心方法
  addPointer() - 注册新指针 ✨
  addAllowedPointer() - 注册允许的指针
  handleNonAllowedPointer() - 处理不允许的指针
  isPointerAllowed() - 检查指针是否被允许 

  // 💡 这里的"允许"指的是符合识别器过滤条件的"指针事件",有两个可选条件:
  // supportedDevices-设备类型、allowedButtonsFilter-按钮过滤(鼠标左右键)
  // 调用链条:
  // 触摸/点击事件 → addPointer(PointerDownEvent) → isPointerAllowed() 检查设备类型和按钮
  // 被允许 → addAllowedPointer() → startTrackingPointer(pointer)-开始跟踪这个指针
  // 不被允许 → handleNonAllowedPointer() 处理不允许的情况
}

😐 GestureRecognizer 只是定义了基本的手势识别API,具体的逻辑都交给 对应的子类 去处理:

5.7.1. PointerRouter 源码

startTrackingPointer() 会调用 PointerRouter.addRoute() ,它是 Flutter 手势系统中的 "指针事件路由器 ",负责:将来自引擎的指针事件分发给相应的手势识别器。主要方法:

dart 复制代码
// 为特定指针ID注册一个路由,参数:指针ID、处理事件的回调函数、可选的坐标变换矩阵
void addRoute(int pointer, PointerRoute route, [Matrix4? transform])

// 移除特定指针的路由,GestureRecognizer.stopTrackingPointer() 时会调用
void removeRoute(int pointer, PointerRoute route)

// 注册一个全局路由,接收所有事件,一般用于监听或调试
void addGlobalRoute(PointerRoute route, [Matrix4? transform])

// 移除全局路由
void removeGlobalRoute(PointerRoute route)

// 💡 调用流程
// 
// 【注册阶段】
// GestureRecognizer.addPointer() → GestureRecognizer.startTrackingPointer()
// → PointerRouter.addRoute() 注册路由 → 存储在内部映射: Map<int, List<_RouteEntry>>
//
// 【事件分发阶段】
// 引擎发送 PointerEvent → GestureBinding.handlePointerEvent() 接收
// → PointerRouter.route() 分发事件 → 依次调用所有注册的路由回调
//
// 【清理阶段】
// GestureRecognizer.stopTrackingPointer() → PointerRouter.removeRoute() 移除路由
// → 从映射中删除对应条目

typedef PointerRoute = void Function(PointerEvent event);

// PointerEvent-包含所有关于指针输入的信息,如:位置、标识、时间、状态、压力尺寸
class PointerEvent {
  /// 全局坐标位置 (屏幕坐标系)
  final Offset position;
  
  /// 本地坐标位置 (组件坐标系)
  Offset get localPosition => /* 通过 transform 转换 */;
  
  /// 移动增量 (相对于上一次事件)
  final Offset delta;
  
  /// 本地坐标系的移动增量
  Offset get localDelta => /* 通过 transform 转换 */;

  //...
}

简化事件流

  • 硬件 → 操作系统捕获输入 → Flutter Engine 接收 PointerData → Framework 转换为 PointerEvent
  • 命中测试-HitTestResult → 分发给-GestureRecognizer → 路由到-PointerRouterhandleEvent()

5.7.2. OneSequenceGestureRecognizer 源码

抽象父类,它为所有 追踪单个连续手势 (从一个指针按下开始,直到这个手势序列结束) 的识别器 (如单击、长按、拖动) 提供了核心的逻辑框架,核心方法:

dart 复制代码
// ✨ 【生命周期与状态管理】 (需子类重写)

// 当一个新的指针触摸屏幕时,Flutter 的 GestureBinding 会调用此方法
// 识别器决定是否要开始追踪一个新手势
// 通常在这里记录下指针的ID (event.pointer) 和初始状态,并将自己加入手势竞技场。
void addAllowedPointer(PointerDownEvent event)

// 💡 【核心事件处理器】,在手势序列进行中,每当有新的指针事件发生时调用。
// 在 addAllowedPointer 之后,与该指针相关的所有后续事件 (PointerMoveEvent, PointerUpEvent等)
// 都会被发送到这个方法。子类在这里实现其核心逻辑,如:检查移动距离、计时等,以判断手势是否成立。
void handleEvent(PointerEvent event)

// 【最后的清理工作】当识别器不再追踪任何指针后 (手势成功、失败或取消后),会调用此方法。
// 可以在这里重置所有内部状态,为下一次手势做准备。
void didStopTrackingLastPointer(int pointer)


// ✨【手势竞技场交互方法】 (子类在handleEvent中根据逻辑判断后,调用这些方法来决定自己在竞技场中的状态)

// 声明自己对当前手势的态度。
// GestureDisposition.accepted 表示"我认为我赢了",GestureDisposition.rejected 表示 "我放弃"。
void resolve(GestureDisposition disposition)	

// 更常用的一种,直接解决自己的状态,而不等待其他竞技者
// 如:TapGestureRecognizer 在检测到手指移动过大时,
// 会调用 resolveSelf(GestureDisposition.rejected) 来让自己失败
void resolveSelf(GestureDisposition disposition)

// 告诉框架:"我不再对这个指针的事件感兴趣了",通常在手势被接受或拒绝后调用。
void stopTrackingPointer(int pointer)

// ✨【竞技场结果回调方法】(由框架调用,子类重写以响应)

// 【胜利回调】当你的识别器在竞技场中胜出时,此方法被调用。
// 这是触发最终手势回调(如 onTap、onPanStart)的最佳位置。
void acceptGesture(int pointer)	

// 【失败回调】当你的识别器在竞技场中失败时(例如,另一个识别器胜出),此方法被调用。
// 通常在这里触发取消回调(如 onTapCancel)
void rejectGesture(int pointer)	。

🤔 所以,单序列 类型手势识别器,关注的重点就是 handleEvent() ,它回答了最关键的问题:"这个手势是如何被判定成功或失败的? "。看下它的子类-单击和长按对应的源码。

5.7.3. TapGestureRecognizer 源码

没在 TapGestureRecognizer 直接找着 handleEvent() 重写, 往上找父类:BaseTapGestureRecognizerPrimaryPointerGestureRecognizer

然后就是 等待竞技场结果

设置了获胜标志,然后触发后续的 onTapUp() 和 onTap() 回调。

5.7.4. LongPressGestureRecognizer 源码

也是继承 PrimaryPointerGestureRecognizerhandleEvent() 和单击识别器相同,自己重写了 handlePrimaryPointer()

定时器超时处理:

状态检查

5.7.5. DoubleTapGestureRecognizer 源码

再挑一个 双击手势识别器 的源码看看,和前面个两个不同,它直接继承 GestureRecognizer ,跟下方法调用:addAllowedPointer()_trackTap()_TapTracker.startTrackingPointer()_handleEvent()

dart 复制代码
void _handleEvent(PointerEvent event) {
  // 🔍 根据指针ID获取对应的追踪器
  final _TapTracker tracker = _trackers[event.pointer]!;
  
  if (event is PointerUpEvent) {
    // 👆 指针抬起事件 - 这是最关键的时刻!
    if (_firstTap == null) {
      // 🥇 第一次点击完成 - 注册第一次点击
      _registerFirstTap(tracker);
    } else {
      // 🥈 第二次点击完成 - 双击成功!
      _registerSecondTap(tracker);
    }
  } else if (event is PointerMoveEvent) {
    // 👉 指针移动事件 - 检查是否移动超出容差范围
    if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
      // ❌ 移动距离太大,拒绝这次手势
      _reject(tracker);
    }
  } else if (event is PointerCancelEvent) {
    // 🚫 指针取消事件 - 直接拒绝
    _reject(tracker);
  }
}

/// 注册第一次点击,进入"等待第二次点击"状态
void _registerFirstTap(_TapTracker tracker) {
  // ⏰ 启动双击超时定时器
  // 如果在指定时间内没有第二次点击,就会超时并重置状态
  _startDoubleTapTimer();
  
  // 🔒 【核心机制】hold住竞技场!
  // 这一步非常重要:阻止其他手势识别器(如TapGestureRecognizer)立即获胜
  // 为第二次点击争取时间窗口
  GestureBinding.instance.gestureArena.hold(0); // 模拟hold调用

  // 🧊 冻结追踪器,停止监听指针事件
  _freezeTracker(tracker);
  
  // 🗑️ 从当前活跃追踪器列表中移除
  _trackers.remove(0); // 模拟移除
  
  // 🧹 清理其他所有追踪器
  _clearTrackers();
  
  // 💾 保存第一次点击信息,状态转为"等待第二次点击"
  _firstTap = tracker;
}

/// 第二次点击,双击成功
void _registerSecondTap(_TapTracker tracker) {
  // ✅ 第一次点击的竞技场条目标记为"接受"
  _firstTap!.entry.resolve(GestureDisposition.accepted);
  
  // ✅ 第二次点击的竞技场条目也标记为"接受"
  tracker.entry.resolve(GestureDisposition.accepted);
  
  // 🧊 冻结当前追踪器
  _freezeTracker(tracker);
  
  // 🗑️ 从追踪器列表中移除
  _trackers.remove(0); // 模拟移除
  
  // 🎊 触发双击成功回调!
  _checkUp(tracker.initialButtons);
  
  // 🔄 重置所有状态,准备下一次双击检测
  _reset();
}

5.7.6. ScaleGestureRecognizer 源码

最后再讲一个 "缩放 " 对应的手势识别器 ScaleGestureRecognizer ,也是继承 OneSequenceGestureRecognizer ,只能同时识别一个手势序列,,直接跟 handleEvent()

dart 复制代码
@override
void handleEvent(PointerEvent event) {
  assert(_state != _ScaleState.ready);
  bool didChangeConfiguration = false;
  bool shouldStartIfAccepted = false;
  
  // 🎯 根据事件类型进行不同处理
  if (event is PointerMoveEvent) {
    // 移动事件:更新速度追踪器和位置
    final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
    if (!event.synthesized) {
      tracker.addPosition(event.timeStamp, event.position);
    }
    _pointerLocations[event.pointer] = event.position;
    shouldStartIfAccepted = true;
    _lastTransform = event.transform;
  } else if (event is PointerDownEvent) {
    // 按下事件:添加新指针
    _pointerLocations[event.pointer] = event.position;
    _pointerQueue.add(event.pointer);
    didChangeConfiguration = true;
    shouldStartIfAccepted = true;
    _lastTransform = event.transform;
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    // 抬起/取消事件:移除指针
    _pointerLocations.remove(event.pointer);
    _pointerQueue.remove(event.pointer);
    didChangeConfiguration = true;
    _lastTransform = event.transform;
  } else if (event is PointerPanZoomStartEvent) {
    // 触控板缩放开始
    _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromStartEvent(this, event);
    didChangeConfiguration = true;
    shouldStartIfAccepted = true;
    _lastTransform = event.transform;
  } else if (event is PointerPanZoomUpdateEvent) {
    // 触控板缩放更新
    if (!event.synthesized && !trackpadScrollCausesScale) {
      _velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
    }
    _pointerPanZooms[event.pointer] = _PointerPanZoomData.fromUpdateEvent(this, event);
    _lastTransform = event.transform;
    shouldStartIfAccepted = true;
  } else if (event is PointerPanZoomEndEvent) {
    // 触控板缩放结束
    _pointerPanZooms.remove(event.pointer);
    didChangeConfiguration = true;
  }

  // 🔗 关键调用链
  _updateLines();                                    // ① 更新旋转线
  _update();                                         // ② 更新计算数据
  
  if (!didChangeConfiguration || _reconfigure(event.pointer)) {
    _advanceStateMachine(shouldStartIfAccepted, event); // ③ 推进状态机
  }
  stopTrackingIfPointerNoLongerDown(event);         // ④ 清理追踪
}

状态机核心逻辑在 _advanceStateMachine() 中:

dart 复制代码
void _advanceStateMachine(bool shouldStartIfAccepted, PointerEvent event) {
  // 🟡 状态1: ready → possible
  if (_state == _ScaleState.ready) {
    _state = _ScaleState.possible;
  }

  // 🟠 状态2: possible → accepted (竞争判定)
  if (_state == _ScaleState.possible) {
    final double spanDelta = (_currentSpan - _initialSpan).abs();
    final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
    
    // 🏆 【三重判定条件】
    if (spanDelta > computeScaleSlop(event.kind) ||                    // 跨度变化超阈值 (通常为18.0 像素)
        focalPointDelta > computePanSlop(event.kind, gestureSettings) || // 焦点移动超阈值 (通常为18.0 像素)
        math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) { // 缩放比例变化
      resolve(GestureDisposition.accepted); // 🎯 宣布胜利!
    }
  } else if (_state.index >= _ScaleState.accepted.index) {
    resolve(GestureDisposition.accepted);
  }

  // 🟢 状态3: accepted → started (开始手势)
  if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
    _initialEventTimestamp = event.timeStamp;
    _state = _ScaleState.started;
    _dispatchOnStartCallbackIfNeeded(); // 🔥 触发 onStart 回调
  }

  // 🔵 状态4: started (持续更新)
  if (_state == _ScaleState.started) {
    _scaleVelocityTracker?.addPosition(event.timeStamp, Offset(_scaleFactor, 0));
    if (onUpdate != null) {
      invokeCallback<void>('onUpdate', () {
        onUpdate!(ScaleUpdateDetails(
          scale: _scaleFactor,                    // 总缩放比例
          horizontalScale: _horizontalScaleFactor, // 水平缩放比例
          verticalScale: _verticalScaleFactor,     // 垂直缩放比例
          focalPoint: _currentFocalPoint!,         // 全局焦点
          localFocalPoint: _localFocalPoint,       // 本地焦点
          rotation: _computeRotationFactor(),      // 旋转角度
          pointerCount: pointerCount,              // 指针数量
          focalPointDelta: _delta,                 // 焦点变化
          sourceTimeStamp: event.timeStamp
        ));
      });
    }
  }
}

不难看出竞争判定的关键逻辑在于当状态为 _ScaleState.possible 时执行三重判定条件:

  • 跨度变化 :计算当前指针间平均距离与初始距离的差值,超过 computeScaleSlop() 阈值 (通常18像素) 即认为是 "缩放" 意图。
  • 焦点移动 :计算当前焦点与初始焦点的直线距离,超过 computePanSlop() 阈值(通常18像素)即认为是 "拖拽" 意图。
  • 缩放比例 :计算缩放因子的相对变化,任一方向超过5% (1.05倍 )即认为有 "明显" 缩放。

也绘制下调用方法的时序图:

6. 自定义手势案例:实现N连击

🐶 懒得自己想了,直接抄《掘金小册:Flutter 手势探索 - 执掌天下》里的 "N连击手势检测 " 的例子,关键点就是自定义一个支持N连击的手势识别器------MultipleTapGestureRecognizer,核心:

  • 继承 OneSequenceGestureRecognizer ,重写 handleEvent() 对每次 PointerUpEvent (抬起事件) 进行处理,根据 时间间隔(≤300ms)+点击位置(≤50px) 判断是否为连击,同时满足才认为是连击,否则重置为第1击(这里要hold()避免阻止TapGestureRecognizer立即获胜)。
  • ≥2连击时立即触发连击事件,每次点击后设置300ms定时器,超时后自动重置所有状态 (要release()竞技场)。最后在事件指针跟踪结束回调的 didStopTrackingLastPointer() 中判断:如果连击数少于2,宣布识别失败。

具体代码实现:

dart 复制代码
class MultipleTapGestureRecognizer extends OneSequenceGestureRecognizer {
  /// 连击时间间隔限制(毫秒)
  final int tapInterval;

  /// 连击位置偏移限制(像素)
  final double positionTolerance;

  /// N连击事件回调函数
  Function(int tapCount, Offset position)? onMultipleTap;

  /// 当前连击次数
  int _tapCount = 0;

  /// 上次点击时间
  DateTime? _lastTapTime;

  /// 上次点击位置
  Offset? _lastTapPosition;

  /// 重置定时器
  Timer? _resetTimer;

  MultipleTapGestureRecognizer({
    this.tapInterval = 300,
    this.positionTolerance = 50.0,
  });

  @override
  String get debugDescription => 'MultipleTapGestureRecognizer';

  @override
  void addAllowedPointer(PointerDownEvent event) {
    // 开始跟踪这个指针
    startTrackingPointer(event.pointer, event.transform);
  }

  @override
  void handleEvent(PointerEvent event) {
    // 只处理抬起事件
    if (event is PointerUpEvent) _handleTapUp(event);
  }

  /// 🎯 处理抬起事件
  void _handleTapUp(PointerUpEvent event) {
    final now = DateTime.now();
    final position = event.position;

    // 检查是否在连击时间间隔内
    bool isWithinTapInterval = false;
    if (_lastTapTime != null) {
      final timeDiff = now.difference(_lastTapTime!).inMilliseconds;
      isWithinTapInterval = timeDiff <= tapInterval;
    }

    // 检查是否在连击位置范围内
    bool isWithinPositionTolerance = true;
    if (_lastTapPosition != null) {
      final distance = (position - _lastTapPosition!).distance;
      isWithinPositionTolerance = distance <= positionTolerance;
    }

    // 判断是否为连击
    if (isWithinTapInterval && isWithinPositionTolerance) {
      _tapCount++;
    } else {
      _tapCount = 1;
    }

    // 更新状态
    _lastTapTime = now;
    _lastTapPosition = position;
    // 取消之前的重置定时器
    _resetTimer?.cancel();

    if (_tapCount == 1) {
      // 第一次点击时hold住竞技场,阻止TapGestureRecognizer立即获胜
      GestureBinding.instance.gestureArena.hold(event.pointer);
    } else if (_tapCount >= 2) {
      // 多击成功,立即触发连击事件并获胜
      resolve(GestureDisposition.accepted);
      onMultipleTap?.call(_tapCount, position);
    }

    // 设置重置定时器
    _resetTimer = Timer(Duration(milliseconds: tapInterval), () {
      _tapCount = 0;
      _lastTapTime = null;
      _lastTapPosition = null;
      // 超时后释放竞技场
      GestureBinding.instance.gestureArena.release(event.pointer);
    });
  }

  @override
  void didStopTrackingLastPointer(int pointer) {
    // 如果连击次数小于2,认输
    if (_tapCount < 2)  resolve(GestureDisposition.rejected);
  }

  @override
  void dispose() {
    _tapCount = 0;
    _lastTapTime = null;
    _lastTapPosition = null;
    _resetTimer?.cancel();
    _resetTimer = null;
    super.dispose();
  }
}

然后在需要使用自定义手势的地方使用 RawGestureDetector ,通过 gestures 传入支持的手势类型,这里依次传入了"单击"、"长按" 和 "N连击":

dart 复制代码
RawGestureDetector(
  // 设置手势识别器:Key为TapGestureRecognizer类型,Value为手势识别器的工厂对象
  gestures: {
    TapGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(),
      (GestureRecognizer instance) {
        (instance as TapGestureRecognizer).onTap = () {
          _gestureHistory.value = ['触发单击', ..._gestureHistory.value];
          _bgColorNotifier.value = _tapColor;
        };
      },
    ),
    LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
      () => LongPressGestureRecognizer(),
      (GestureRecognizer instance) {
        (instance as LongPressGestureRecognizer).onLongPress = () {
          _gestureHistory.value = ['触发长按', ..._gestureHistory.value];
          _bgColorNotifier.value = _longPressColor;
        };
      },
    ),
    MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<MultipleTapGestureRecognizer>(
      () => MultipleTapGestureRecognizer(),
      (GestureRecognizer instance) {
        (instance as MultipleTapGestureRecognizer).onMultipleTap = (tapCount, position) {
          _gestureHistory.value = ['触发N连击: $tapCount', ..._gestureHistory.value];
          _bgColorNotifier.value = _multipleTapColors[(tapCount - 2) % _multipleTapColors.length];
        };
      },
    ),
  }, child: ... 

运行效果:

😄 还是非常简单的,完整源码【--->c34/multiple_tap_gesture_demo.dart<---】

7. 画个图收尾

如题,最后梳理下流程,画个图作为收尾:

相关推荐
像风一样自由202033 分钟前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
哲科软件1 小时前
跨平台开发的抉择:Flutter vs 原生安卓(Kotlin)的优劣对比与选型建议
android·flutter·kotlin
天涯海风1 小时前
Kuikly 与 Flutter 的全面对比分析,结合技术架构、性能、开发体验等核心维度
flutter·kuikly
aiprtem1 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊1 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术1 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing1 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止2 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall2 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴2 小时前
简单入门Python装饰器
前端·python