2.5 手势识别与交互系统

手势是移动端交互的核心。Flutter 提供了从低层级的 Listener 到高层级的 GestureDetector 一整套手势识别体系,并通过手势竞技场(Gesture Arena)解决手势冲突。


一、手势识别层级

复制代码
Layer 3(高层):GestureDetector / InkWell / Dismissible
  ↓ 封装了各种 Recognizer
Layer 2(中层):GestureRecognizer 系列
  ↓ 基于 PointerEvent 识别手势语义
Layer 1(低层):Listener / RawGestureDetector
  ↓ 直接监听原始指针事件
硬件层:PointerDownEvent / PointerMoveEvent / PointerUpEvent

二、GestureDetector

2.1 基本手势

dart 复制代码
GestureDetector(
  // ========= 点击类 =========
  onTap: () => print('单击'),
  onDoubleTap: () => print('双击'),
  onLongPress: () => print('长按'),
  onLongPressStart: (details) => print('长按开始: ${details.globalPosition}'),
  onLongPressEnd: (details) => print('长按结束'),

  // ========= 拖拽类 =========
  onPanStart: (details) => print('拖拽开始'),
  onPanUpdate: (details) {
    setState(() {
      _offset += details.delta; // 跟随手指移动
    });
  },
  onPanEnd: (details) => print('拖拽结束,速度: ${details.velocity}'),

  // ========= 缩放类(双指缩放 / 旋转)=========
  onScaleStart: (details) {
    _baseScale = _currentScale;
    _baseAngle = _currentAngle;
  },
  onScaleUpdate: (details) {
    setState(() {
      _currentScale = _baseScale * details.scale;
      _currentAngle = _baseAngle + details.rotation;
    });
  },

  // 子 Widget
  child: Transform(
    transform: Matrix4.identity()
      ..translate(_offset.dx, _offset.dy)
      ..scale(_currentScale)
      ..rotateZ(_currentAngle),
    alignment: Alignment.center,
    child: Image.asset('assets/photo.jpg'),
  ),
)

2.2 垂直 / 水平拖拽

dart 复制代码
// 仅监听垂直方向拖拽
GestureDetector(
  onVerticalDragUpdate: (details) {
    setState(() => _offsetY += details.delta.dy);
  },
  child: child,
)

// 仅监听水平方向拖拽
GestureDetector(
  onHorizontalDragUpdate: (details) {
    setState(() => _offsetX += details.delta.dx);
  },
  child: child,
)

// 注意:onPanUpdate 与 onVerticalDrag / onHorizontalDrag 不能同时使用!
// Pan = 垂直 + 水平 的联合手势

三、手势竞技场(Gesture Arena)

当多个 Widget 同时识别到手势时,Flutter 使用手势竞技场来决定谁赢得手势:

复制代码
手指按下 → 多个 GestureRecognizer 注册到 Arena

竞争规则:
  ① 任何 Recognizer 可以直接"声明胜利"(如 TapRecognizer 在手指抬起时)
  ② 如果只剩一个 Recognizer,自动胜出
  ③ 超时后,第一个注册的 Recognizer 胜出

常见冲突场景:
  ListView(ScrollGesture)↔ GestureDetector(Tap)
  → 短按不动 = Tap 胜出
  → 滑动 = ScrollGesture 胜出

3.1 解决手势冲突

dart 复制代码
// 问题:外部 GestureDetector 吞掉了内部的 onTap
GestureDetector(
  onTap: () => print('外部'),
  child: GestureDetector(
    onTap: () => print('内部'), // 可以正常接收
    child: child,
  ),
)
// → onTap 不冲突,因为子节点的 HitTest 先命中

// 问题:onPanUpdate 与 ListView 滚动冲突
// 解决:使用 RawGestureDetector + 自定义竞争策略
class AlwaysWinPanRecognizer extends PanGestureRecognizer {
  @override
  void rejectGesture(int pointer) {
    acceptGesture(pointer); // 强制赢得竞争
  }
}

RawGestureDetector(
  gestures: {
    AlwaysWinPanRecognizer:
        GestureRecognizerFactoryWithHandlers<AlwaysWinPanRecognizer>(
      () => AlwaysWinPanRecognizer(),
      (instance) {
        instance.onUpdate = (details) { /* 拖拽逻辑 */ };
      },
    ),
  },
  child: child,
)

四、Listener(底层指针事件)

dart 复制代码
Listener(
  onPointerDown: (event) => print('手指按下: ${event.position}'),
  onPointerMove: (event) => print('手指移动: ${event.delta}'),
  onPointerUp: (event) => print('手指抬起'),
  onPointerCancel: (event) => print('取消'),
  onPointerHover: (event) => print('悬停(鼠标/触控笔)'),
  onPointerSignal: (event) {
    // 鼠标滚轮事件(桌面/Web)
    if (event is PointerScrollEvent) {
      print('滚轮: ${event.scrollDelta}');
    }
  },
  child: child,
)

五、HitTest(命中测试)

复制代码
手指按下 → HitTest 从 RenderObject 树根节点开始
  ↓ 递归调用每个 RenderBox.hitTest()
  ↓ 收集所有命中的 Widget(HitTestResult 列表)
  ↓ 从内到外分发 PointerEvent

自定义 HitTest 区域:
dart 复制代码
// 忽略 Widget(事件穿透)
IgnorePointer(
  ignoring: true,
  child: OverlayWidget(), // 该 Widget 不响应触摸
)

// 吸收事件(不穿透到下方)
AbsorbPointer(
  absorbing: true,
  child: child, // 下方 Widget 不接收事件
)

// 扩大触摸区域
Padding(
  padding: const EdgeInsets.all(12), // 视觉不变,但可触摸区域增大
  child: GestureDetector(
    behavior: HitTestBehavior.opaque, // 让透明区域也响应
    onTap: () {},
    child: Icon(Icons.close, size: 24),
  ),
)

HitTestBehavior

dart 复制代码
GestureDetector(
  behavior: HitTestBehavior.deferToChild, // 默认:仅子 Widget 区域响应
  // behavior: HitTestBehavior.opaque,     // 自身整个区域都响应(不透传)
  // behavior: HitTestBehavior.translucent,// 自身整个区域响应(但透传给下方)
  child: child,
)

六、拖拽排序(Drag & Drop)

6.1 内置排序组件

dart 复制代码
ReorderableListView(
  onReorder: (oldIndex, newIndex) {
    setState(() {
      if (newIndex > oldIndex) newIndex--;
      final item = _items.removeAt(oldIndex);
      _items.insert(newIndex, item);
    });
  },
  children: _items.map((item) => ListTile(
    key: ValueKey(item.id), // 必须有 Key
    title: Text(item.name),
    trailing: ReorderableDragStartListener(
      index: _items.indexOf(item),
      child: const Icon(Icons.drag_handle),
    ),
  )).toList(),
)

6.2 Draggable + DragTarget

dart 复制代码
// 可拖拽的 Widget
Draggable<Product>(
  data: product, // 携带数据
  feedback: Material( // 拖拽时跟随手指的 Widget
    elevation: 8,
    child: ProductCard(product: product),
  ),
  childWhenDragging: Opacity( // 原位置的占位 Widget
    opacity: 0.3,
    child: ProductCard(product: product),
  ),
  child: ProductCard(product: product),
)

// 拖拽目标区域
DragTarget<Product>(
  onWillAcceptWithDetails: (details) => true, // 是否接受此拖拽
  onAcceptWithDetails: (details) {
    setState(() => _cart.add(details.data));
  },
  builder: (context, candidateData, rejectedData) {
    final isHovering = candidateData.isNotEmpty;
    return AnimatedContainer(
      duration: const Duration(milliseconds: 200),
      decoration: BoxDecoration(
        color: isHovering ? Colors.blue.withOpacity(0.2) : Colors.grey[100],
        border: Border.all(
          color: isHovering ? Colors.blue : Colors.grey,
          width: 2,
        ),
      ),
      child: const Center(child: Text('拖拽商品到此处')),
    );
  },
)

6.3 LongPressDraggable

dart 复制代码
// 长按后才能拖拽(更自然的移动端体验)
LongPressDraggable<String>(
  data: 'hello',
  delay: const Duration(milliseconds: 300), // 长按触发时间
  hapticFeedbackOnStart: true, // 触觉反馈
  feedback: child,
  child: child,
)

七、桌面 / Web 交互适配

dart 复制代码
// 鼠标光标
MouseRegion(
  cursor: SystemMouseCursors.click,
  onEnter: (_) => setState(() => _isHovering = true),
  onExit: (_) => setState(() => _isHovering = false),
  child: AnimatedContainer(
    duration: const Duration(milliseconds: 150),
    transform: Matrix4.identity()..scale(_isHovering ? 1.05 : 1.0),
    child: Card(child: content),
  ),
)

// 键盘快捷键
Shortcuts(
  shortcuts: {
    LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
        const SaveIntent(),
    LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ):
        const UndoIntent(),
  },
  child: Actions(
    actions: {
      SaveIntent: CallbackAction<SaveIntent>(
        onInvoke: (_) => _save(),
      ),
    },
    child: Focus(
      autofocus: true,
      child: child,
    ),
  ),
)

// 右键菜单
Listener(
  onPointerDown: (event) {
    if (event.kind == PointerDeviceKind.mouse &&
        event.buttons == kSecondaryMouseButton) {
      _showContextMenu(event.position);
    }
  },
  child: child,
)

小结

概念 要点
GestureDetector 高层手势封装:tap / drag / scale
手势竞技场 解决多手势冲突,子节点优先
HitTestBehavior deferToChild / opaque / translucent
Draggable 拖拽交互,配合 DragTarget
MouseRegion 桌面 hover、光标适配
Listener 低层指针事件,全平台兼容

👉 下一节:2.6 表单与输入处理

相关推荐
独特的螺狮粉2 小时前
开源鸿蒙跨平台Flutter开发:家庭传统节日记录应用
flutter·华为·开源·harmonyos
空中海2 小时前
2.1 Widget 基础
flutter·dart
亚历克斯神2 小时前
Flutter 组件 genkit 的适配 鸿蒙Harmony 深度进阶 - 驾驭模型幻觉审计、实现鸿蒙端多维 RAG 向量对齐与端云协同 AI 指挥中心方案
flutter·harmonyos·鸿蒙·openharmony
浮芷.2 小时前
开源鸿蒙跨平台Flutter开发:考试资料共享平台应用
科技·flutter·华为·开源·harmonyos·鸿蒙
AI_零食2 小时前
开源鸿蒙跨平台Flutter开发:快递单号批量查询应用
学习·flutter·华为·开源·harmonyos·鸿蒙
旺仔大牛2 小时前
Flutter中StatefulWidget的生命周期
flutter·statefulwidget
浮芷.2 小时前
开源鸿蒙跨平台Flutter开发:校园兼职信息发布应用
科技·flutter·华为·开源·harmonyos·鸿蒙
AI_零食2 小时前
开源鸿蒙跨平台Flutter开发:密码生成器应用
网络·学习·flutter·华为·开源·harmonyos·鸿蒙
AI_零食3 小时前
开源鸿蒙跨平台Flutter开发:生日纪念日提醒应用
运维·flutter·开源·harmonyos·鸿蒙