Flutter之竞技场(Arena)原理解析

5,075 阅读15分钟

Flutter中,给开发者提供了点击(TapGestureRecognizer)、长按(LongPressGestureRecognizer)、水平滑动(HorizontalDragGestureRecognizer)、垂直滑动(VerticalDragGestureRecognizer)等大量手势处理API,简化了各种手势的使用,但同时也引入了手势冲突问题。为了解决这个问题,Flutter给开发者提供了一套解决方案。在该方案中,Flutter引入了Arena竞技场)概念,然后把冲突的手势加入到Arena中并竞争,谁胜利,谁就获得手势的后续处理权。下面来看Arena的具体实现。

为了方便,后面用竞技场来表达Arena

1、竞技场原理解析

Flutter中,竞技场的代码实现很简单。包含注释在内也不到300行,所以其原理也很简单,就是先到先得,只有前面成员拒绝,后续成员才能够获得手势处理权。下面来看一下源码实现。

enum GestureDisposition {
  /// 允许获得后续手势处理权
  accepted,

  /// 放弃后续手势处理权
  rejected,
}

/// 代表竞技场中的成员,也只有继承该类才可使手势加入竞技场来竞争
abstract class GestureArenaMember {
  /// 当手势处理权获取成功时的调用
  void acceptGesture(int pointer);

  /// 当手势处理权获取失败时的调用
  void rejectGesture(int pointer);
}

/// 将信息传递给竞技场的接口
class GestureArenaEntry {
  GestureArenaEntry._(this._arena, this._pointer, this._member);

  final GestureArenaManager _arena;
  final int _pointer;
  final GestureArenaMember _member;

  /// 通知当前成员是接受还是拒绝后续手势处理权
  void resolve(GestureDisposition disposition) {
    _arena._resolve(_pointer, _member, disposition);
  }
}

/// 手势竞技场,
class _GestureArena {
  final List<GestureArenaMember> members = <GestureArenaMember>[];
  bool isOpen = true;
  bool isHeld = false;
  bool hasPendingSweep = false;

  /// 一个“eager winner”,在竞技场未关闭时会寻找一个“eager winner”,如果存在则赋值给eagerWinner。
  /// 关于“eager winner”可以参考EagerGestureRecognizer中的实现。
  GestureArenaMember eagerWinner;

、/// 将成员加入到竞技场中
  void add(GestureArenaMember member) {
    members.add(member);
  }
}

/// 竞技场的管理。第一个接受或者最后一个不拒绝的成员竞争成功。
class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  /// 添加一个新的成员(继承GestureArenaMember的手势识别器)加入竞技场
  GestureArenaEntry add(int pointer, GestureArenaMember member) {
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      return _GestureArena();
    });
    state.add(member);
    return GestureArenaEntry._(this, pointer, member);
  }

  /// 关闭竞技场(不允许新的成员进入竞技场),在framework中处理完down事件后调用
  void close(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; // This arena either never existed or has been resolved.
    state.isOpen = false;
    _tryToResolveArena(pointer, state);
  }
  
  /// 强制解决竞技场问题,将胜利授予第一位成员。
  /// 通常是在对[PointerUpEvent]进行所有其他处理之后。 它可以确保多个被动手势不会造成僵局,从而使用户无法与应用程序进行交互。
  /// 如果需要将竞技场中成员竞争的时间延长到[PointerUpEvent]之后,则应该调用[hold]方法,并在[release]中再次调用当前方法
  void sweep(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; // This arena either never existed or has been resolved.
    if (state.isHeld) {
      state.hasPendingSweep = true;
      return; // This arena is being held for a long-lived member.
    }
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      // 第一个成员获胜
      state.members.first.acceptGesture(pointer);
      // 其他成员都被拒绝
      for (int i = 1; i < state.members.length; i++)
        state.members[i].rejectGesture(pointer);
    }
  }

  /// 延长竞技场的存活时间
  void hold(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; // This arena either never existed or has been resolved.
    state.isHeld = true;
  }
  
  /// 如果竞技场的存活时间被延长,可通过此方法来释放。
  /// 此方法一般与hold方法配对使用
  void release(int pointer) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; // This arena either never existed or has been resolved.
    state.isHeld = false;
    if (state.hasPendingSweep)
      sweep(pointer);
  }

  /// 竞技场成员的竞争
  void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
    final _GestureArena state = _arenas[pointer];
    if (state == null)
      return; // This arena has already resolved.
    if (disposition == GestureDisposition.rejected) {
      /// 从竞技场中移除被拒绝成员
      state.members.remove(member);
      member.rejectGesture(pointer);
      if (!state.isOpen)
        _tryToResolveArena(pointer, state);
    } else {
      if (state.isOpen) {
        state.eagerWinner ??= member;
      } else {
        _resolveInFavorOf(pointer, state, member);
      }
    }
  }

  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) {
      /// 如果竞技场中存在eagerWinner,则eagerWinner获得获胜
      _resolveInFavorOf(pointer, state, state.eagerWinner);
    }
  }

  void _resolveByDefault(int pointer, _GestureArena state) {
    if (!_arenas.containsKey(pointer))
      return; // Already resolved earlier.
    final List<GestureArenaMember> members = state.members;
    _arenas.remove(pointer);
    /// 竞技场中第一个成员获胜
    state.members.first.acceptGesture(pointer);
  }

  void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
    _arenas.remove(pointer);
    /// 拒绝非member的成员
    for (final GestureArenaMember rejectedMember in state.members) {
      if (rejectedMember != member)
        rejectedMember.rejectGesture(pointer);
    }
    member.acceptGesture(pointer);
  }
}

上面代码虽不多,但非常重要,所以先来梳理一下代码中各个类之间的关系。如下图。

从图中,可以看出竞技场中的各个成员是无法直接与竞技场进行沟通的,需要GestureArenaEntry来进行转发。

**注意:**一个GestureArenaMember可以在不同arenas中有不同的GestureArenaEntry,这些不同GestureArenaEntry是根据pointer来区分的

下面再对上图中的四个类进行一一讲解。

GestureArenaMember是个抽象类,实现仅有两个方法。但它代表的是竞技场中的成员,也只有继承该类的手势才有资格进入竞技场竞争。在Flutter中主要有下面这些手势继承自GestureArenaMember

GestureArenaEntry相当于一个桥梁,竞技场的成员只有通过该类才能把自己是放弃还是接受的决定告诉给竞技场。

_GestureArena虽说是竞技场的实现,但实现很简单,主要是存储当前竞技场中所有成员及当前竞技场的状态。

最后再来看GestureArenaManager。它是一个非常重要的类,主要实现了以下功能。

  1. 存储所有竞技场及竞技场成员的添加及移除。
  2. 竞技场状态的管理。如竞技场的创建、关闭、生命周期的延长等。
  3. 每个竞技场中成员竞争的具体实现。

在手指按下时,会遍历能够响应PointerDownEvent事件的所有Widget。在遍历过程中会根据pointer将对应的成员添加进竞技场,如果竞技场不存在,则会创建一个新的竞技场。此时竞技场时打开状态,这时候基本上(有一种例外情况)是仅允许成员的添加,而不允许逻辑的处理。当遍历完成后,竞技场就会关闭,也就不允许往竞技场添加新的成员了。所有只有在处理PointerDownEvent事件时才能添加成员到竞技场,具体实现代码在类GestureArenaManageraddclose方法中。

当竞技场关闭后,就可以根据逻辑在GestureArenaManager中判断竞技场中的某个成员是否获胜。其判断规则很简单,就是前面说的先到先得,只有前面成员拒绝,后续成员才能够获得手势处理权。具体实现代码在类GestureArenaManager_resolve方法中。

**注意:**在整个手势操作中,事件竞争仅会发生一次。也就是某个成员竞争成功后,直到PointerUpEvent事件结束都是该成员来处理后续手势事件。

当手势操作完毕后,会触发PointerUpEvent事件。在该事件执行完毕后会执行最后的收尾操作,也就是调用GestureArenaManagersweep方法。由于竞技场成员竞争失败后会把成员移除竞技场,所以当竞技场中没有任何成员时,sweep不会做任何操作。但如果在调用sweep方法时,竞技场中还存在成员,这时候需要把这些成员处理掉,从而避免影响后面的手势处理。这里的处理规则是直接让第一个成员获胜并拒绝其他成员

根据以上内容,可以把一个完整手势冲突处理流程总结如下。

  1. 在处理PointerDownEvent事件时,竞技场是打开状态,可以向其中添加成员。当PointerDownEvent事件处理完毕后,竞技场就变成关闭状态,那么从此时到PointerUpEvent事件的处理完毕都无法向竞技场中添加成员。
  2. 当触发PointerDownEvent事件的下一个事件时(比如PointerMoveEvent),竞技场中成员会进行竞争,其竞争规则是先到先得,只有前面成员拒绝,后续成员才能够获得手势处理权
  3. PointerUpEvent事件执行完毕后,会进行收尾操作。如果此时竞技场中还存在成员,那么将根据直接让第一个成员获胜并拒绝其他成员的规则来处理掉竞技场中的所有成员。

以上就是Flutter中手势处理的正常处理流程。之所以是正常处理流程,是因为还有两种意外情况。

1.1、手势插队

在上面说过,在竞技场处于打开状态时,仅能向竞技场中添加成员,而无法做其他操作。但竞技场中的成员eagerWinner却是在竞技场处于打开状态下设置的,所以来看eagerWinner的使用。

eagerWinner是在_tryToResolveArena方法中且此时竞技场中成员大于1时才会被使用。而_tryToResolveArena又在close_resolve中被调用,所以就存在下面两种情况。

  1. 当竞技场关闭时,如果竞技场中存在eagerWinner且竞技场中成员大于1,那么成员eagerWinner直接获胜,无论该成员是在什么时候添加进竞技场的。
  2. 再来看_resolve与方法_tryToResolveArena方法,当满足竞技场被关闭、竞技场成员大于3且第一个成员拒绝时,eagerWinner将直接获胜,无论eagerWinner是在什么时候添加进竞技场的。

根据上面两种情况,就可以推断出eagerWinner的作用就是插队,它可以使后加入进竞技场的成员不用等待前面成员的拒绝而直接获胜。

关于eagerWinner的应用场景可以参考EagerGestureRecognizer的使用。代码如下。

class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
  /// Create an eager gesture recognizer.
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
  EagerGestureRecognizer({ PointerDeviceKind kind }) : super(kind: kind);

  @override
  void addAllowedPointer(PointerDownEvent event) {
    //将OneSequenceGestureRecognizer加入竞技场
    startTrackingPointer(event.pointer, event.transform);
    //将OneSequenceGestureRecognizer设置为eagerWinner
    resolve(GestureDisposition.accepted);
    //关闭竞技场
    stopTrackingPointer(event.pointer);
  }

  @override
  String get debugDescription => 'eager';

  @override
  void didStopTrackingLastPointer(int pointer) { }

  @override
  void handleEvent(PointerEvent event) { }
}

1.2、竞技场生命周期的延长

Flutter中,两次点击间隔的时间在40ms-300ms之间,则可说明当前触发了双击手势。而为了判断当前是单击还是双击,那么就需要把这两种手势加入到竞技场并竞争。由于在PointerUpEvent事件执行完毕后会处理掉竞技场中还存在的成员,所以在正常情况下双击永远都无法获胜,也就无法响应双击事件。

那么如何在满足双击条件时触发双击事件尼?Flutter给出了解决方案,就是依赖于GestureArenaManager中的hold方法与release方法。

先来看竞技场中有两个属性isHeldhasPendingSweep。这两个属性是配对使用,当调用GestureArenaManagerhold方法时将会将isHeld设置为true,这样在PointerUpEvent事件执行完毕并调用sweep方法时不会清除竞技场中的成员,此时在sweep方法中也会将hasPendingSweep设置为true。这也就说明当前竞技场还有用,再等等看看。当调用GestureArenaManagerrelease方法时则会将 isHeld重新设置为false并调用sweep方法来根据规则清除竞技场的所有成员,此时当前竞技场已经无用了。这样就延长了当前竞技场的生命周期。

再来看单击与双击冲突的问题,就很好解决了。当第一次点击时,双击手势的处理中就调用hold方法来延长竞技场的生命周期,如果满足双击条件则让竞技场中的双击事件获胜并调用release方法来清除竞技场成员;如果不满足,则单击事件获胜并调用release方法来清除竞技场成员。

下面来看Flutter中双击手势的部分实现代码。

class DoubleTapGestureRecognizer extends GestureRecognizer {

  //响应PointerEvent事件
  void _handleEvent(PointerEvent event) {
    final _TapTracker tracker = _trackers[event.pointer];
    if (event is PointerUpEvent) {//当响应PointerUpEvent事件时
      if (_firstTap == null)//是否存在第一次点击
        _registerFirstTap(tracker);
      else//满足双击条件
        _registerSecondTap(tracker);
    } else if (event is PointerMoveEvent) {
      //判断是否是滑动,如果是滑动,则调用_reject
      if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
        _reject(tracker);//
    } else if (event is PointerCancelEvent) {
      _reject(tracker);
    }
  }
  void _reject(_TapTracker tracker) {
    _trackers.remove(tracker.pointer);
    //_TapTracker在竞技场中竞争失败
    tracker.entry.resolve(GestureDisposition.rejected);
    _freezeTracker(tracker);
    // If the first tap is in progress, and we've run out of taps to track,
    // reset won't have any work to do. But if we're in the second tap, we need
    // to clear intermediate state.
    if (_firstTap != null &&
        (_trackers.isEmpty || tracker == _firstTap))
      _reset();
  }

  void _reset() {
    //停止timer
    _stopDoubleTapTimer();
    if (_firstTap != null) {
      // Note, order is important below in order for the resolve -> reject logic
      // to work properly.
      final _TapTracker tracker = _firstTap;
      _firstTap = null;
      _reject(tracker);
      //释放竞技场
      GestureBinding.instance.gestureArena.release(tracker.pointer);
    }
    _clearTrackers();
  }

  //记录双击中的第一次点击
  void _registerFirstTap(_TapTracker tracker) {
    _startDoubleTapTimer();
    //延长竞技场的生命周期
    GestureBinding.instance.gestureArena.hold(tracker.pointer);
    // Note, order is important below in order for the clear -> reject logic to
    // work properly.
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    _clearTrackers();
    _firstTap = tracker;
  }

  //记录双击中的第二次点击,此时双击手势在竞技场竞争成功
  void _registerSecondTap(_TapTracker tracker) {
    _firstTap.entry.resolve(GestureDisposition.accepted);
    tracker.entry.resolve(GestureDisposition.accepted);
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    _checkUp(tracker.initialButtons);
    _reset();
  }
  
  //使用Timer来判断第二次点击是否在间隔300ms内,如果超过300ms则调用_reset方法
  void _startDoubleTapTimer() {
    _doubleTapTimer ??= Timer(kDoubleTapTimeout, _reset);
  }
  
  //停止timer
  void _stopDoubleTapTimer() {
    if (_doubleTapTimer != null) {
      _doubleTapTimer.cancel();
      _doubleTapTimer = null;
    }
  }
}

2、GestureArenaTeam原理解析

在阅读竞技场相关源码时,发现了一个有意思的类GestureArenaTeam。它的实现代码很简单,如下。

class GestureArenaTeam {
  final Map<int, _CombiningGestureArenaMember> _combiners = <int, _CombiningGestureArenaMember>{};

  //队长。如果captain不为null,则当[GestureArenaTeam]成员中的任何一个赢得胜利时,captain都会接受该手势。 如果为null,则要求胜利的成员接受该手势。
  GestureArenaMember captain;

  //向团队中添加新成员
  GestureArenaEntry add(int pointer, GestureArenaMember member) {
    final _CombiningGestureArenaMember combiner = _combiners.putIfAbsent(
        pointer, () => _CombiningGestureArenaMember(this, pointer));
    return combiner._add(pointer, member);
  }
}

顾名思义,该类就是把多个手势打包成一个team,如果captain不为null,则captain就是该团队的领导。在team中,相同pointer的手势都会添加到_CombiningGestureArenaMember中。由于_CombiningGestureArenaMember继承自GestureArenaMember,所以也就是_CombiningGestureArenaMember来代表其中的所有成员去竞技场中竞争。

为了方便管理,_CombiningGestureArenaMember对于其中成员也有一套属于自己的处理规则,来看代码实现。

class _CombiningGestureArenaMember extends GestureArenaMember {
  _CombiningGestureArenaMember(this._owner, this._pointer);

  final GestureArenaTeam _owner;
  //相关手势
  final List<GestureArenaMember> _members = <GestureArenaMember>[];
  final int _pointer;

  bool _resolved = false;
  //胜利者
  GestureArenaMember _winner;
  GestureArenaEntry _entry;
  
  //_CombiningGestureArenaMember对象竞争成功时调用
  @override
  void acceptGesture(int pointer) {
    _close();
    //如果_winner为null,则captain获胜,如果captain为null,则第一个成员获胜
    _winner ??= _owner.captain ?? _members[0];
    for (final GestureArenaMember member in _members) {
      if (member != _winner)
        member.rejectGesture(pointer);
    }
    //由_winner继续处理后续事件
    _winner.acceptGesture(pointer);
  }

  //_CombiningGestureArenaMember对象竞争失败时调用
  @override
  void rejectGesture(int pointer) {
    _close();
    //所有成员都将竞争失败
    for (final GestureArenaMember member in _members)
      member.rejectGesture(pointer);
  }

  void _close() {
    _resolved = true;
    //从team中移除当前对象
    final _CombiningGestureArenaMember combiner = _owner._combiners.remove(_pointer);
  }

  GestureArenaEntry _add(int pointer, GestureArenaMember member) {
    _members.add(member);
    //将_CombiningGestureArenaMember添加到竞技场
    _entry ??= GestureBinding.instance.gestureArena.add(pointer, this);
    return _CombiningGestureArenaEntry(this, member);
  }

  void _resolve(GestureArenaMember member, GestureDisposition disposition) {
    if (_resolved)
      return;
    if (disposition == GestureDisposition.rejected) {
      _members.remove(member);
      member.rejectGesture(_pointer);
      if (_members.isEmpty)
        _entry.resolve(disposition);
    } else {
      //如果_winner为null则captain获胜,否则member获胜
      _winner ??= _owner.captain ?? member;
      _entry.resolve(disposition);
    }
  }
}

由于_CombiningGestureArenaMember也是竞技场中一员,所以它也与其他成员一样在竞技场中竞争。如果其他成员竞争成功,_CombiningGestureArenaMember也就被抛弃,其中的所有成员也就竞争失败并被抛弃,具体实现在上面代码的rejectGesture方法中;反之则根据有队长(captain)则队长竞争成功,否则第一个成员竞争成功的规则来处理_CombiningGestureArenaMember中的成员。

如果_CombiningGestureArenaMember还在竞技场中,但还未决出胜负。那么_CombiningGestureArenaMember中的各个成员可以通过调用_resolve方法来拒绝并移除某个成员或者使某个成员获胜(没有队长的情况下,如果存在队长还是队长获胜)。

例如当_CombiningGestureArenaMember中存在成员EagerGestureRecognizer时,竞技场关闭后,EagerGestureRecognizer就会立即调用_resolve方法来得到_winner。也可以把EagerGestureRecognizer中调用_resolve方法时的第二个参数修改为GestureDisposition.rejected,这样会在竞技场关闭时,调用_resolve方法来把EagerGestureRecognizer_CombiningGestureArenaMember的成员中移除。

3、iOS设备上WKWebView与侧滑冲突

上面阐述了竞技场的实现原理,那么现在就来看一个案例。

Flutter中,一些时候需要加载一些网页,这时候就可以使用谷歌提供的webview_flutter插件来实现,毕竟该插件是Google出品,还是有保障的。

在使用时,会发现在iOS设备中存在一个问题。就是当在WKWebView内部跳转后,侧滑返回的不是上一个html页面,而是关闭了当前WKWebView页面。如下动画所示。

那么该如何解决这一问题尼?熟悉WKWebView的都知道,该控件是iOS的原生控件,在设置allowsBackForwardNavigationGestures为true后,当WKWebView内部跳转后,侧滑就会返回上一个html页面。但在Flutter中却是触发了侧滑关闭当前页面,所以首先就想到滑动冲突问题。再通过禁止Flutter的页面侧滑返回来验证一下该问题,当在WKWebView内部跳转后,侧滑如预期一样返回了上一个html页面。所以该问题就是因为滑动冲突导致而产生的。

问题原因找到了,再来看解决方案。很简单,就是根据WKWebViewcanGoBack来判断把手势交给谁处理,如果canGoBack为true,就交给WKWebView来处理,此时侧滑返回上一个html页面;否则就给Flutter来处理,此时关闭当前Flutter页面。

问题找到了,解决方案也有了,就来开始实现。由于WKWebView内部跳转后侧滑返回上一html页面触发的是iOS中的事件,而Flutter侧滑返回上一页触发的是Flutter中的事件。所以首当其冲的就是如何把这两个事件放在同一环境下处理,由于一时半会没想到好的方案,所以就去翻了下源码。结果发现webview_flutter已经处理好了该问题,就是当WKWebView需要事件时,Flutter会通过MethodHandler告诉WKWebView。所以此时就转化为Flutter中事件冲突的处理。

根据上节中竞技场的原理,可以在WKWebViewcanGoBack为true时,页面侧滑放弃手势的处理,从而把事件交给WKWebView处理;反之则页面侧滑手势自己处理。具体实现代码如下。

class HorizontalDragGesture extends HorizontalDragGestureRecognizer {
  ///
  HorizontalDragGesture({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);

  ///
  bool webViewCanGoBack = false;

  @override
  void resolve(GestureDisposition disposition) {
    //webViewCanGoBack为true时拒绝,否则走默认流程
    super.resolve(webViewCanGoBack ? GestureDisposition.rejected : disposition);
  }

  ///
  void updateState(bool canGoBack) {
    webViewCanGoBack = canGoBack;
  }
}

再把HorizontalDragGesture作为页面侧滑返回的手势处理,就解决了手势冲突问题。如下动画所示。

由于此方案更改的是Flutter页面的侧滑手势,所以改动比较大,再来看一种改动比较小的方案。

由于webview_flutter的手势处理中用到了GestureArenaTeamcaptain不为null,那么如果让需要返回上一html页面时让captain竞争成功,如果需要返回上一Flutter页面时让页面侧滑手势竞争成功不就可以了嘛。

由于页面侧滑手势先加入竞技场,所以想要captain竞争成功,就需要插队。这时候就是体现EagerGestureRecognizer作用的时候,所以可以根据如下修改来解决上面的滑动冲突问题。

class WebEagerGestureRecognizer extends EagerGestureRecognizer {
  bool canGoBack = false;

  @override
  void addAllowedPointer(PointerDownEvent event) {
    // We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena.
    startTrackingPointer(event.pointer, event.transform);
    resolve(
        canGoBack ? GestureDisposition.accepted : GestureDisposition.rejected);
    stopTrackingPointer(event.pointer);
  }
}

然后把WebEagerGestureRecognizer添加到gestureRecognizers属性中即可,如下。

WebView(
  ...
  gestureNavigationEnabled: true,
  gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>[
    Factory<OneSequenceGestureRecognizer>(
      () => webEagerGestureRecognizer,
    ),
  ].toSet(),
  onCanGoBack: (canGoBack) {
    webEagerGestureRecognizer?.canGoBack = canGoBack;
  },
)

修改后滑动动画如下所示。

4、总结

理解了上面的内容后,想必对于Flutter中的手势冲突能够快速解决。再结合Flutter之事件处理一文,那么对于FLutter中的事件机制就能有一个全面的了解,做到快速实现自定义手势及手势冲突问题的解决。