Flutter手势冲突与手势竞争

2,839 阅读14分钟

上一篇文章讲解了Flutter原始手势的传递过程,如果我们业务都是监听的原始手势事件,那么我们很多业务场景写起来就会很复杂,比如同一个区域我同时绑定长按事件和点击事件,我长按后点击事件也同时响应;嵌套的父子组件都绑定了点击事件,点击同时响应等。从用户角度来说,大部分场景下他的一个操作只会一个响应预期,如果响应过多用户就糊涂了,轻则卸载软件,重则精神崩溃。

所以像Android可以拦截事件,Web也提供了阻止冒泡来防止手势事件向上传递。Flutter则提供了一个手势竞争机制来决定哪个手势事件响应,下面我们根据源码看看Flutter如何实现手势竞争的。

源码基于Flutter 2.8.1

组件层

GestureDetector

Flutter提供了我们最常用的GestureDetector组件接收事件

GestureDetector(
  onTap: () {
    print('green');
  },
  child: Container(
    width: 200,
    height: 200,
    color: Colors.green,
    alignment: Alignment.center,
    child: GestureDetector(
        onTap: () {
          print('red');
        },
        child: Container(
          width: 100,
          height: 100,
          color: Colors.red,
        )),
  ),
)

上面绘制了一个200x200的绿色框,中间是一个100x100红色框,点击红色部分,输出redgreen不会输出。这就是一个最简单的竞争手势案例。

我们看下GestureDetector做了什么

class GestureDetector extends StatelessWidget {
///...
@override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (onTapDown != null ||
        onTapUp != null ||
        onTap != null ||
       ///....
    ) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ///...
        },
      );
    }

    /// ...
    return RawGestureDetector(
      gestures: gestures,
      behavior: behavior,
      excludeFromSemantics: excludeFromSemantics,
      child: child,
    );
  }
}
///...

GestureDetectorbuild方法中使用了GestureRecognizerFactoryWithHandlers用于制造TapGestureRecognizerGestureRecognizerFactoryWithHandlers很简单,就是用于TapGestureRecognizer的初始化与事件绑定,然后还使用了RawGestureDetector。我们先看看RawGestureDetectorTapGestureRecognizer放到后面分析。

RawGestureDetector

RawGestureDetector是一个StatefulWidget,其RawGestureDetectorStatebuild中使用Listener接收原始手势事件

class RawGestureDetectorState extends State<RawGestureDetector> {
  Map<Type, GestureRecognizer>? _recognizers = const <Type, GestureRecognizer>{};
  ///...
  @override
  void initState() {
    super.initState();
    ///...
    _syncAll(widget.gestures);
  }
  /// ...
  @override
  Widget build(BuildContext context) {
    Widget result = Listener(
      onPointerDown: _handlePointerDown,
      behavior: widget.behavior ?? _defaultBehavior,
      child: widget.child,
    );
    ///...
    return result;
  }
  void _handlePointerDown(PointerDownEvent event) {
    assert(_recognizers != null);
    for (final GestureRecognizer recognizer in _recognizers!.values) {
      // 执行addPointer将原始事件注册进来
      recognizer.addPointer(event);
    }
  }
  /// ...
  void _syncAll(Map<Type, GestureRecognizerFactory> gestures) {
    final Map<Type, GestureRecognizer> oldRecognizers = _recognizers!;
    _recognizers = <Type, GestureRecognizer>{};
    for (final Type type in gestures.keys) {
      // 将接收的Map<Type, GestureRecognizerFactory>类型转成ap<Type, GestureRecognizer>类型,同时也实例化了GestureRecognizer
      _recognizers![type] = oldRecognizers[type] ?? gestures[type]!.constructor();
      // 执行initializer将 [GestureDetector]中绑定的事件们拿过来
      gestures[type]!.initializer(_recognizers![type]!);
    }
    // dispose已被移除的recognizers
    for (final Type type in oldRecognizers.keys) {
      if (!_recognizers!.containsKey(type))
        oldRecognizers[type]!.dispose();
    }
  }
}

通过调用_syncAll对传入的gestures进行转化,当onPointerDown发生时,会循环并调用addPointer来告知GestureRecognizer进行一系列处理。

阶段总结

其实做了这么多事情,如果对于我只想实现一个点击事件的监听,可以简化如下:

class MyGestureDetector extends StatelessWidget {
  final VoidCallback? onTap;
  final Widget child;

  const MyGestureDetector({Key? key, this.onTap, required this.child}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    GestureRecognizer recognizer = TapGestureRecognizer()..onTap = onTap;
    return Listener(
      child: child,
      onPointerDown: (PointerDownEvent event) {
        recognizer.addPointer(event);
      },
    );
  }
}

我们可以把MyGestureDetector替换最上面的最小demo中的GestureDetector,效果是一样的。

手势识别器

GestureArenaMember

手势识别器的基类。

abstract class GestureArenaMember {
  /// 当接受事件时执行
  void acceptGesture(int pointer);
  /// 当拒绝事件时执行
  void rejectGesture(int pointer);
}

GestureRecognizer

TapGestureRecognizer是一个GestureRecognizer,它有很多层的继承关系才走到GestureRecognizer,每一个父类都封装了一些实用的能力,我们先看看GestureRecognizer类的作用。

abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
  /// ...
  GestureRecognizer({
    this.debugOwner,
    Set<PointerDeviceKind>? supportedDevices,
  }) : assert(kind == null || supportedDevices == null),
       _supportedDevices = kind == null ? supportedDevices : <PointerDeviceKind>{ kind };

  final Object? debugOwner;

  /// 可以设置支持的设备类型的列表,比如我想只监听鼠标点击,手写笔点击不响应,就可以传入[PointerDeviceKind.mouse]
  final Set<PointerDeviceKind>? _supportedDevices;

  /// 缓存手势id与手势类型对应关系
  final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};

  /// 这就是上面我们在PointerDownEvent回调中执行的函数
  /// 一般我们只需要重写[addAllowedPointer]来注册事件
  void addPointer(PointerDownEvent event) {
    _pointerToKind[event.pointer] = event.kind;
    if (isPointerAllowed(event)) {
      addAllowedPointer(event);
    } else {
      handleNonAllowedPointer(event);
    }
  }
  @protected
  void addAllowedPointer(PointerDownEvent event) { }
  
  /// 当事件被拒绝了执行的函数
  @protected
  void handleNonAllowedPointer(PointerDownEvent event) { }

  /// 判断手势支持的设备类型,不支持的设备将返回false
  @protected
  bool isPointerAllowed(PointerDownEvent event) {
    return _supportedDevices == null || _supportedDevices!.contains(event.kind);
  }

  /// 根据手势id获取手势的设备类型
  @protected
  PointerDeviceKind getKindForPointer(int pointer) {
    return _pointerToKind[pointer]!;
  }

  /// 手势销毁时调用
  @mustCallSuper
  void dispose() { }

  /// 简化了一下,其实就是直接执行调用了callback
  /// 当调用出错时会输出更多有意义的信息给调用者
  @protected
  @pragma('vm:notify-debugger-on-exception')
  T? invokeCallback<T>(String name, RecognizerCallback<T> callback, { String Function()? debugReport }) {
    T? result;
    try {
      result = callback();
    } catch (exception, stack) {
      /// ...
    }
    return result;
  }
  /// ...
}

简单来讲,GestureRecognizer就是定义了一些函数并处理了支持的设备列表的判断,想要接受事件,还需要子类自行实现。

OneSequenceGestureRecognizer

OneSequenceGestureRecognizer用户跟踪单个手势,如点击、拖动等,在大部分场景下都会用到它。GestureArenaManager是手势竞争场管理类,它是主要决定哪些可以胜出的关键,下一节分析。

abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
  /// GestureArenaEntry
  final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
  final Set<int> _trackedPointers = HashSet<int>();

  @override
  @protected
  void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer, event.transform);
  }

   @protected
  void startTrackingPointer(int pointer, [Matrix4? transform]) {
    // 将handleEvent放到pointerRouter中,这样handleEvent方法会在每个事件触发时调用(它没有竞争)
    GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform);
    // 记录pointer
    _trackedPointers.add(pointer);
    // 将自己添加到GestureArenaManager中,返回一个GestureArenaEntry
    _entries[pointer] = _addPointerToArena(pointer);
  }

  GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null)
      return _team!.add(pointer, this);
    return GestureBinding.instance!.gestureArena.add(pointer, this);
  }
  /// 手势识别器可以主动让手势胜出或淘汰
  /// 这也是手势竞争中非常重要的方法,比如拖动、长按等场景就是靠此方法来产生胜利者
  /// 它对接到手势竞争场管理类(GestureArenaManager)的_resolve方法
  @protected
  @mustCallSuper
  void resolve(GestureDisposition disposition) {
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
    _entries.clear();
    for (final GestureArenaEntry entry in localEntries)
      entry.resolve(disposition);
  }
  /// 针对单个手势过程的胜出和淘汰(比如在移动端双指操作其实会对应多个手势过程)
  @protected
  @mustCallSuper
  void resolvePointer(int pointer, GestureDisposition disposition) {
    final GestureArenaEntry? entry = _entries[pointer];
    if (entry != null) {
      _entries.remove(pointer);
      entry.resolve(disposition);
    }
  }
  /// 提前知道该手势与自己没有关系了,可以提前移除自己,不再响应handleEvent
  /// 比如长按事件中发生了PointerMoveEvent,就可以调用此函数移除
  @protected
  void stopTrackingPointer(int pointer) {
    if (_trackedPointers.contains(pointer)) {
      GestureBinding.instance!.pointerRouter.removeRoute(pointer, handleEvent);
      _trackedPointers.remove(pointer);
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
  }
  /// 注销,一般用于当Widget组件被移除时调用
  @override
  void dispose() {
    resolve(GestureDisposition.rejected);
    for (final int pointer in _trackedPointers)
      GestureBinding.instance!.pointerRouter.removeRoute(pointer, handleEvent);
    // ...
  }
  /// GestureArenaTeam是将竞争场控制到一个组合里面,而不是所有事件共同竞争
  GestureArenaTeam? get team => _team;
  GestureArenaTeam? _team;
  set team(GestureArenaTeam? value) {
    _team = value;
  }
  ///...
}

OneSequenceGestureRecognizeraddAllowedPointer方法中将自己添加到全局的竞争场中,也同时提供了handleEvent方法让手势识别器也有监听所有事件的能力。

在简单的手势竞争中如点击,只需要GestureArenaManager自己决定让谁胜出,它都是让第一个加入到手势竞争场中的手势识别器胜出的(后面分析),所以这里有个很重要的resolve方法,它是可以自行决定胜出者,比如长按和点击同时存在时,长按可以自行决定自己胜出,然后点击事件就被淘汰了。为了印证这个结论,我们再简单分析下LongPressGestureRecognizer

LongPressGestureRecognizer

class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
  LongPressGestureRecognizer({
    Duration? duration,
    double? postAcceptSlopTolerance,
    Set<PointerDeviceKind>? supportedDevices,
    Object? debugOwner,
  }) : super(
         deadline: duration ?? kLongPressTimeout, // 默认延时500毫秒
         postAcceptSlopTolerance: postAcceptSlopTolerance,
         kind: kind,
         supportedDevices: supportedDevices,
         debugOwner: debugOwner,
       );
  // ... 删除不必要代码

  /// 当deadline时间后(此处默认为500毫秒)还未抬起会执行,如果事件过程不足该时间则不会执行,此能力在[PrimaryPointerGestureRecognizer]中实现
  @override
  void didExceedDeadline() {
    /// 直接宣布胜出事件,此时其它事件将会失败(rejected)
    resolve(GestureDisposition.accepted);
    _longPressAccepted = true;
    super.acceptGesture(primaryPointer!);
    /// 当按下超过500毫秒,直接执行pressStart
    _checkLongPressStart();
  }

  /// 接收原始事件,触发长按相关事件
  @override
  void handlePrimaryPointer(PointerEvent event) {
    //...
    if (event is PointerUpEvent) {
      /// 如果_longPressAccepted为true,也就是didExceedDeadline执行了,
      if (_longPressAccepted == true) {
        _checkLongPressEnd(event);
      } else {
        // 说明事件过程不足500毫秒,直接宣布自己退出手势竞争
        resolve(GestureDisposition.rejected);
      }
      _reset();
    } else if (event is PointerCancelEvent) {
      _checkLongPressCancel();
      _reset();
    } else if (event is PointerDownEvent) {
      _longPressOrigin = OffsetPair.fromEventPosition(event);
      _initialButtons = event.buttons;
      // 默认会执行onLongPressDown
      _checkLongPressDown(event);
    } else if (event is PointerMoveEvent) {
      if (event.buttons != _initialButtons) {
        resolve(GestureDisposition.rejected);
        stopTrackingPointer(primaryPointer!);
      } else if (_longPressAccepted) {
        _checkLongPressMoveUpdate(event);
      }
    }
  }
  // ... 删除不必要代码
  @override
  void resolve(GestureDisposition disposition) {
    if (disposition == GestureDisposition.rejected) {
      if (_longPressAccepted) {
        _reset();
      } else {
        // 当调用rejected时,执行LongPressCancel,此时会经过onLongPressDown->LongPressCancel
        _checkLongPressCancel();
      }
    }
    super.resolve(disposition);
  }
  @override
  void acceptGesture(int pointer) {
    // 长按是一个比较特殊的情况,它的acceptGesture不做任何事情
    // 因为如果只有一个长按手势加入到手势竞争场中时,此处也会被调用,这不合理
  }
  // ... 
}

当事件超过500(默认)毫秒后就被认为是长按事件了,会调用didExceedDeadline方法,此时会调用resolve(GestureDisposition.accepted)宣布自己胜出手势竞争,如果此时还有其它事件如点击事件会被淘汰。此时事件执行顺序是onLongPressDown->onLongPressStart->onLongPress->onLongPressMove->onLongPressEnd->onLongPressUp->onLongPressDown->onLongPressCancel

当事件还未超过500毫秒就执行了PointerUpEvent,此时didExceedDeadline还未调用,那么会调用resolve(GestureDisposition.rejected)来淘汰自己在手势竞争场的竞争。此时事件执行顺序是onLongPressDown->onLongPressCancel

其实手势识别器还有很多其它类,我也不一一分析了,下面列出来稍微解释一下

  • PrimaryPointerGestureRecognizer:继承OneSequenceGestureRecognizer,当手势移出该区域一定距离后将停止手势流程
  • BaseTapGestureRecognizer:继承PrimaryPointerGestureRecognizer,单击手势的基类,提供了handleTapDownhandleTapUphandleTapCancel的手势识别的能力
  • TapGestureRecognizer:继承BaseTapGestureRecognizer,提供更加丰富的能力如鼠标右键的点击
  • LongPressGestureRecognizer:继承PrimaryPointerGestureRecognizer,提供长按事件相关能力
  • DragGestureRecognizer:继承OneSequenceGestureRecognizer,提供拖动的能力的基类
  • VerticalDragGestureRecognizer:继承DragGestureRecognizer,提供上下拖动能力
  • HorizontalDragGestureRecognizer:继承DragGestureRecognizer,提供水平拖动能力
  • PanGestureRecognizer:继承DragGestureRecognizer,提供水平和垂直拖动能力
  • ScaleGestureRecognizer:继承OneSequenceGestureRecognizer,为移动端提供缩放能力,如图片的放大缩小
  • ForcePressGestureRecognizer:继承OneSequenceGestureRecognizer,移动端上可以获取用户手指按下的压力值
  • DoubleTapGestureRecognizer:继承GestureRecognizer,提供双击事件能力
  • MultiTapGestureRecognizer:继承GestureRecognizer,提供多指同时操控的能力
  • SerialTapGestureRecognizer:继承GestureRecognizer,当出现连续的点击手势时,它能提供手势连续的次数

竞争场

入口

手势竞争场,由GestureBinding持有,之前说过,GestureBinding是分发事件的主要对象,在所有命中测试中完成后,会将自己添加到命中测试结果中,每次事件分发最后,GestureBinding中的handleEvent都会被调用。

@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    // 关闭竞争场
    gestureArena.close(event.pointer);
  } else if (event is PointerUpEvent) {
    // 如果竞争场中还没胜出者,强制第一个添加的手势胜出
    gestureArena.sweep(event.pointer);
  } else if (event is PointerSignalEvent) {
    // 针对鼠标滚动条事件的竞争
    pointerSignalResolver.resolve(event);
  }
}

gestureArena就是保存在GestureBinding中的手势竞争场管理者,如果当前事件指针是PointerDownEvent,就调用GestureArenaManagerclose方法关闭竞争场;如果此时是PointerUpEvent,就会调用sweep方法宣布一个胜利者。

GestureArenaManager

class GestureArenaManager {
  /// 终极竞争场(保存着所有的手势竞争场,一个手势过程会存在一个手势竞争场)
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
  // 添加一个手势识别器到竞争场中,同一个手势过程(pointerID相同)只会存在一个state
  GestureArenaEntry add(int pointer, GestureArenaMember member) {
    //putIfAbsent方法是在_arenas中如果存在pointer这个key,就返回该key对应的value,如果不存在该key,就返回新的_GestureArena
    final _GestureArena state = _arenas.putIfAbsent(pointer, () { 
      return _GestureArena();
    });
    state.add(member);
    return GestureArenaEntry._(this, pointer, member);
  }
  // 关闭竞争场,此时就不能再像竞争场中添加手势识别器了
  void close(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null)
      return; // 如果state为null,就直接结束
    state.isOpen = false; // 将该手势对应的isOpen状态置为false
    _tryToResolveArena(pointer, state); // 尝试竞争(当只有一个手势识别器或者有手势识别器将自己标记为胜出时将会宣布胜出者)
  }
  /// 当发生PointerUpEvent事件时(手指抬起、鼠标按键抬起),表示该手势已经结束,如果此时还没有胜出者,就让一个添加到竞争场中的手势识别器胜出
  void sweep(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null)
      return; // state为null,直接结束
    // 即使手势已经结束,某些特殊情况下需要保持该手势过程在竞争场中,会先调用hold方法将isHeld置为true
    if (state.isHeld) {
      state.hasPendingSweep = true;
      return;
    }
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      // 第一个手势胜利
      state.members.first.acceptGesture(pointer);
      // 其它手势调用rejectGesture方法
      for (int i = 1; i < state.members.length; i++)
        state.members[i].rejectGesture(pointer);
    }
  }
  /// 某些特殊情况下需要保持该手势过程在竞争场中,此方法与release方法对应使用
  void hold(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null)
      return; 
    state.isHeld = true;
  }
  /// 让hold住的手势状态宣布胜利者
  void release(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null)
      return;
    state.isHeld = false;
    if (state.hasPendingSweep)
      sweep(pointer);
  }
  /// 由GestureArenaEntry传递到手势识别器中,手势识别器中调用的resolve方法会调用到此处
  void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
    final _GestureArena? state = _arenas[pointer];
    if (state == null)
      return; 
    // 宣布竞争失败
    if (disposition == GestureDisposition.rejected) {
      state.members.remove(member); //  移除自己
      member.rejectGesture(pointer); // 调用手势识别器rejectGesture方法
      if (!state.isOpen)
        _tryToResolveArena(pointer, state); // 如果竞争场已经关闭,尝试竞争
    } else { // 宣布竞争胜利
      if (state.isOpen) { // 如果当前竞争场还未关闭,就对该手势状态的eagerWinner赋值,当手势关闭时会直接宣布该手势胜出
        state.eagerWinner ??= member;
      } else {
        // 竞争场已经关闭,直接宣布该手势识别器胜利
        _resolveInFavorOf(pointer, state, member);
      }
    }
  }
  /// 尝试宣布胜利,条件1.只有一个手势识别器在竞争场中 2. 当有手势识别器提前调用resolve宣布自己胜出竞争
  void _tryToResolveArena(int pointer, _GestureArena state) {
    if (state.members.length == 1) {
      scheduleMicrotask(() => _resolveByDefault(pointer, state)); // 当只有一个手势识别器时,直接宣布该手势识别器胜利
    } else if (state.members.isEmpty) { 
      _arenas.remove(pointer); // 当竞争场为空,则移除该手势的竞争场
    } else if (state.eagerWinner != null) { 
      _resolveInFavorOf(pointer, state, state.eagerWinner!);// 如果eagerWinner不为null,宣布它胜利
    }
  }
  /// 当手势竞争场只有一个手势识别器时,直接宣布胜利
  void _resolveByDefault(int pointer, _GestureArena state) {
    if (!_arenas.containsKey(pointer))
      return;
    final List<GestureArenaMember> members = state.members;
    _arenas.remove(pointer);
    state.members.first.acceptGesture(pointer);
  }
  /// 宣布手势id为pointer的手势过程胜利者为member并调用其它手势识别器的rejectGesture
  void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    _arenas.remove(pointer); // 先从终极竞争场移除该手势过程
    for (final GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
        rejectedMember.rejectGesture(pointer); // 宣布其它手势识别器失败
    }
    member.acceptGesture(pointer); // 宣布member胜利
  }

  //...
}

现在我们来模拟一个长按手势识别器添加到竞争场中到最后执行的过程:

  1. 当发生PointerDownEvent时,手势识别器自行调用add方法将自己添加到竞争场中,最后会调用close关闭竞争场
  2. 当按下超过500毫秒,长按手势识别器自行宣布胜利,将调用_resolve方法再调用_resolveInFavorOf方法淘汰其它竞争者并移除该手势竞争场,此时执行onLongPressStart->onLongPress
  3. 当发生PointerUpEvent,手势竞争场调用sweep,此时state为null,直接返回

滚动条的手势竞争

当在PC电脑上运行Flutter时,我们可以使用鼠标滚动条来滚动一些长列表,我们也会经常遇到长列表嵌套的情况,如果此时滚动条事件响应能影响父子组件的滚动,那也不是非常好的体验,于是Flutter也提供了滚动条事件的手势竞争机制,说得准确一点应该是事件优先执行机制。

由于滚动条事件是一次性的事件,它整个过程都只会产生PointerScrollEvent事件,所以Flutter针对这种类型事件专门做了手势竞争机制。我们回到上面提过的GestureBinding中的handleEvent方法

@override 
void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
   // ...
  } else if (event is PointerSignalEvent) {
    // 针对鼠标滚动条事件的竞争
    pointerSignalResolver.resolve(event);
  }
}

PointerSignalEvent是针对像滚动条这种一次性事件定义的父类,目前也只有滚动条事件(PointerScrollEvent)继承使用。pointerSignalResolverPointerSignalResolver类型,它专为解决类似一次性事件的竞争而存在的

PointerSignalResolver

class PointerSignalResolver {
  PointerSignalResolvedCallback? _firstRegisteredCallback; // 记录第一次注册的回调
  PointerSignalEvent? _currentEvent; // 缓存源事件
  /// 组件中收到`PointerSignalEvent`事件后,可以调用此方法注册回调
  void register(PointerSignalEvent event, PointerSignalResolvedCallback callback) {
    if (_firstRegisteredCallback != null) {  // 当已经注册过回调且还没消费时,直接return,丢弃其它注册的回调
      return;
    }
    _currentEvent = event;
    _firstRegisteredCallback = callback;
  }

  /// 这里就是上面handleEvent调用的方法,它会去执行注册过的回调
  @pragma('vm:notify-debugger-on-exception')
  void resolve(PointerSignalEvent event) {
    if (_firstRegisteredCallback == null) {
      return;
    }
    try {
      _firstRegisteredCallback!(_currentEvent!);
    } catch (exception, stack) {
      // ...
    }
    _firstRegisteredCallback = null;
    _currentEvent = null;
  }
}

PointerSignalResolver很简单,就做了两个事情,提供回调注册,消费回调。因为回调注册只能先注册先成功,后注册的就失败,所以一般都是最底层的子组件能够成功消费,上层后注册的组件就不会执行。我们来看看Flutter的Scrollable组件的ScrollableState实现

ScrollableState

class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, RestorationMixin
    implements ScrollContext {
  // ...
  @override
  Widget build(BuildContext context) {
    Widget result = _ScrollableScope(
      scrollable: this,
      position: position,
      child: Listener(
        onPointerSignal: _receivedPointerSignal,
        child: RawGestureDetector(
          //...
        ),
      ),
    );
    // ...省略代码
    return _configuration.buildScrollbar(
      context,
      _configuration.buildOverscrollIndicator(context, result, details),
      details,
    );
  }
  void _receivedPointerSignal(PointerSignalEvent event) {
    if (event is PointerScrollEvent && _position != null) { // 只处理滚动条事件
      if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) {
        return;
      }
      final double delta = _pointerSignalEventDelta(event);
      final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
      // 当滚动发生位移时
      if (delta != 0.0 && targetScrollOffset != position.pixels) {
        // 像pointerSignalResolver中注册实际执行的回调
        GestureBinding.instance!.pointerSignalResolver.register(event, _handlePointerScroll);
      }
    }
  }
  // 获取位移数据,将列表滚动到对应位置
  void _handlePointerScroll(PointerEvent event) {
    final double delta = _pointerSignalEventDelta(event as PointerScrollEvent);
    final double targetScrollOffset = _targetScrollOffsetForPointerScroll(delta);
    if (delta != 0.0 && targetScrollOffset != position.pixels) {
      position.pointerScroll(delta);
    }
  }
}

其build方法像Listener组件中注册了onPointerSignal,根据事件执行原理,最下层的组件会先执行_receivedPointerSignal方法,所以会优先执行,这样就不会造成父子滚动条同时滚动的现象。

当然在某些场景下我们就是想让父子联动执行滚动怎么办,不向pointerSignalResolver注册,直接执行你逻辑就行了呗。

总结

  1. Flutter提供了手势竞争场的机制来控制多个手势冲突时执行哪个手势
  2. 手势竞争场默认会宣布第一个加入到竞争场中的手势胜出
  3. 手势识别器可以自行宣布自己胜出
  4. 其中一个手势胜出了,它的acceptGesture会执行,其它手势识别器的rejectGesture会执行
  5. 当然失败的手势识别器也不一定不响应手势,具体可以手势识别器自己定义(一般都需要遵循规则,否则就用Listener了)
  6. 滚动条这种单一过程的事件有专门的PointerSignalResolver处理竞争,遵循先注册才能执行的原则