Flutter事件之手势竞技场

1,384 阅读6分钟

手势竞技场是用来解决多个手势歧义的;

竞技相关的类如下⬇️

image.png

_GestrueArena

竞技场的实体类,该类有五个成员变量,一个方法

image.png

成员变量

  • isOpen :表示该竞技场是否开放
  • isHeld :表示该竞技场是否挂起
  • hasPendingSweep:表示该竞技场是否等待清扫
  • eagerWinner:渴望胜利的参赛者
  • members:竞技场的成员(参赛者)

方法

add(GestureArenaMember member)

添加成员到竞技场

void add(GestureArenaMember member) {
  assert(isOpen); //添加时,竞技场一定是打开的
  members.add(member);
}

GestureArenaEntry

竞技场信息发送器,该类是私有类,只能在这个文件里被构造

image.png

成员变量

  • _arena:竞技场管理者
  • _member:竞技场成员
  • pointer:触点id

方法

resolve(GestureDisposition disposition)

向竞技场管理者发送信息(胜利或者失败)的接口类,这个类的在GestureArenaManager.add中创建的

void resolve(GestureDisposition disposition) {
  _arena._resolve(_pointer, _member, disposition);
}

GestureDisposition是一个枚举类型⬇️,分别表示胜利、失败;

image.png

GestureArenaMember

image.png

这个类是一个抽象类,有两个抽象方法,分别表示胜利回调和失败回调;

GestureRecognizer继承自该类,实现这两个方法以接收竞技结果。

GestureArenaManager

竞技场的管理者,管理多个竞技场,在整个Flutter生命周期内只有一个实例,保存在GestureBinding类中

image.png

成员变量

_arenas

final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

key是触点id,value是竞技场,也就说,它会管理多个竞技场。

方法

竞技场的维护方法

add
/// Adds a new member (e.g., gesture recognizer) to the arena.
GestureArenaEntry add(int pointer, GestureArenaMember member) {
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
    return _GestureArena();
  });
  state.add(member);
  assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
  return GestureArenaEntry._(this, pointer, member);
}

这个方法的主要作用是创建竞技场(如果不存在),添加参赛者到竞技场,返回一个GestureArenaEntry对象;

关于手势识别器(GestureRecognizer),我们知道手指按下会进行触点注册(addAllowedPointer),其中会调用_addPointerToArena方法,在这个方法里就是调用的它的add方法

## GestureRecognizer ##
GestureArenaEntry _addPointerToArena(int pointer) {
  if (_team != null)
    return _team!.add(pointer, this);
  return GestureBinding.instance!.gestureArena.add(pointer, this);
}
close
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;
  assert(_debugLogDiagnostic(pointer, 'Closing', state));
  _tryToResolveArena(pointer, state);
}

关闭竞技场,阻止新成员入内,调用_tryToResolveArena尝试开始竞技,注意:这个方法并不一定能分出胜负

这个方法的调用是在GestureBinding.handleEvent

## GestureBinding ##
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
  pointerRouter.route(event);
  if (event is PointerDownEvent) {
    gestureArena.close(event.pointer);
  }
  ...
}
hold
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;
  assert(_debugLogDiagnostic(pointer, 'Holding', state));
}

防止竞技场被打扫,这个方法的实现很简单,只是将竞技场的isHeld置为true;

这个方法的调用时机是多组合手势,双击手势(搜遍源码,好像也只有双击手势才会挂起),它是第一次点击时分不出胜负的,还需要第二次点击,所以它在第一次点击抬起进行_registerFirstTap时,会将竞技场挂起,防止被打扫,待第二次点击才进行裁决,宣布自己胜利。

## DoubleTapGestureRecognizer ##
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();
}
sweep
void sweep(int pointer) {
  final _GestureArena? state = _arenas[pointer];
  if (state == null)
    return; // This arena either never existed or has been resolved.
  assert(!state.isOpen);
  if (state.isHeld) {
    state.hasPendingSweep = true;
    assert(_debugLogDiagnostic(pointer, 'Delaying sweep', state));
    return; // This arena is being held for a long-lived member.
  }
  assert(_debugLogDiagnostic(pointer, 'Sweeping', state));
  _arenas.remove(pointer);
  if (state.members.isNotEmpty) {
    // First member wins.
    assert(_debugLogDiagnostic(pointer, 'Winner: ${state.members.first}'));
    state.members.first.acceptGesture(pointer);
    // Give all the other members the bad news.
    for (int i = 1; i < state.members.length; i++)
      state.members[i].rejectGesture(pointer);
  }
}

这个方法的注释是:强制裁决竞技场,使该竞技场的第一个成员获胜,其他失败。

什么意思呢,我们知道竞技场的裁决并不会只有一次,而是多次的,sweep操作发生在手指抬起时(PointerUpEvent),也就是在GestureBinding.handleEvent保证处理如果竞技场成员间没有获胜者而陷入的僵局。可以理解为最后的裁决。

而如果之前已经裁决成功了,这里的state就会为空(因为裁决成功会从_arenas中移除竞技场);

这里还有一个条件,如果竞技场是挂起状态(state.isHeld),则会讲竞技场hasPendingSweep(待打扫)标记为true,并且退出;我们从这句注释中也能看出,这个long-lived member就是我们上面提到的双击手势。

This arena is being held for a long-lived member.

所以,在这里也能理解hold的作用了;

## GestureBinding ##
@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);
  }
  ...
}
release
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;
  assert(_debugLogDiagnostic(pointer, 'Releasing', state));
  if (state.hasPendingSweep)
    sweep(pointer);
}

既然竞技场被挂起时(isHeld),是无法被打扫(sweep)的,所以需要一个操作让竞技场取消挂起以便打扫,这里用到的就是release;

这个方法做了两件事,首先将isHeld标记为false;然后如果竞技场是待打扫状态(hasPendingSweep)则直接进行打扫;

这个方法的调用场景是在DoubleTapGestureRecognizer._reset中被调用的,而且我们也看到只对第一次的tap执行release,第二次不会(因为第二次没被挂起)

## DoubleTapGestureRecognizer ##
void _reset() {
  _stopDoubleTapTimer();
  if (_firstTap != null) {
    if (_trackers.isNotEmpty)
      _checkCancel();
    // 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();
}

竞技场的裁决方法

_resolveInFavorOf
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
  assert(state == _arenas[pointer]);
  assert(state != null);
  assert(state.eagerWinner == null || state.eagerWinner == member);
  assert(!state.isOpen);
  _arenas.remove(pointer);
  for (final GestureArenaMember rejectedMember in state.members) {
    if (rejectedMember != member)
      rejectedMember.rejectGesture(pointer);
  }
  member.acceptGesture(pointer);
}

看这个方法名也能看出来,这个方法是使成员member直接获胜的方法,其他成员失败,是一定会裁决出结果的;

该方法的的触发时机有两处:

  • _resolve:如果disposition == GestureDisposition.accepted,并且竞技场状态是关闭时,会直接调用_resolveInFavorOf进行裁决;
  • _tryToResolveArena:如果当前竞技场有渴望胜利的参赛者(eagerWinner),会直接调用_resolveInFavorOf进行裁决;
_resolveByDefault
void _resolveByDefault(int pointer, _GestureArena state) {
  if (!_arenas.containsKey(pointer))
    return; // Already resolved earlier.
  assert(_arenas[pointer] == state);
  assert(!state.isOpen);
  final List<GestureArenaMember> members = state.members;
  assert(members.length == 1);
  _arenas.remove(pointer);
  assert(_debugLogDiagnostic(pointer, 'Default winner: ${state.members.first}'));
  state.members.first.acceptGesture(pointer);
}

这个方法会使唯一的成员获胜;是一定会裁决出结果的。

该方法的唯一触发时机是在_tryToResolveArena中,当成员只有一个时才会调用的;

_tryToResolveArena
void _tryToResolveArena(int pointer, _GestureArena state) {
  assert(_arenas[pointer] == state);
  assert(!state.isOpen);
  if (state.members.length == 1) {
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    _arenas.remove(pointer);
    assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
  } else if (state.eagerWinner != null) {
    assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
    _resolveInFavorOf(pointer, state, state.eagerWinner!);
  }
}

尝试裁决,听名字我们就知道,这个是不一定能裁决出结果。

代码实现对应三种情况:

  • 竞技场成员只有一个:调用_resolveByDefault宣布该成员获胜,能裁决出结果
  • 竞技场是空的:直接从_arenas移除该竞技场
  • 竞技场有渴望胜利者,调用_resolveInFavorOf宣布该成员获胜,能裁决出结果 否则,直接返回,不会有裁决结果。

该方法的调用时机有两处:

  • close:在竞技场关闭时,会调用该方法尝试裁决
  • _resolve:如果disposition == GestureDisposition.rejected,并且竞技场是关闭状态会调用_tryToResolveArena尝试裁决。
_resolve
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
  final _GestureArena? state = _arenas[pointer];
  if (state == null)
    return; // This arena has already resolved.
  assert(_debugLogDiagnostic(pointer, '${ disposition == GestureDisposition.accepted ? "Accepting" : "Rejecting" }: $member'));
  assert(state.members.contains(member));
  if (disposition == GestureDisposition.rejected) {
    state.members.remove(member);
    member.rejectGesture(pointer);
    if (!state.isOpen)
      _tryToResolveArena(pointer, state);
  } else {
    assert(disposition == GestureDisposition.accepted);
    if (state.isOpen) {
      state.eagerWinner ??= member;
    } else {
      assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
      _resolveInFavorOf(pointer, state, member);
    }
  }
}

关于这个方法,在GestureArenaEntry中提到过,GestureArenaEntry.resolve最终会调用到这里;

这个方法是非常霸道的,直接通过disposition参数控制裁决结果;当入参disposition == GestureDisposition.accepted时,是一定会分出裁决结果的,而如果是失败则不一定能裁决出结果;

再回头看这个方法的实现,

如果决策参数是失败(GestureDisposition.rejected),

  • 从该竞技场移除该成员
  • 向该成员发送失败的回调
  • 如果该竞技场目前是关闭状态,则调用_tryToResolveArena尝试裁决

如果决策参数是胜利(GestureDisposition.accepted)

  • 如果竞技场是打开状态,设置该成员为渴望胜利者(eagerWinner)
  • 如果竞技场是关闭,调用_resolveInFavorOf方法宣布该成员胜利

从手势识别器理解

这么多方法会很乱,很难理解,我们从手势的运行周期捋一下: 如果当前我们有单击事件(TapGestureRecognizer)和长按事件(LongPressGestureRecognizer)

当手指按下(PointerDownEvent):

  1. TapGestureRecognizer和LongPressGestureRecognizer会分别进行触点注册(addAllowedPointer),在触点注册时会创建竞技场、GestureArenaEntry对象,并把自己加入竞技场,这样竞技场内成员就有他们两个了;LongPressGestureRecognizer会在注册时开启一个500ms的定时器;

  2. 此时执行GestureBinding.handleEvent方法,pointerRouter.route(event)这里会调用手势识别器的handleEvent,但是在这里并没有裁决的方法,所以我们忽略退出;

  3. 随后执行gestureArena.close(event.pointer);,前面我们讲过在这里会关闭竞技场调用_tryToResolveArena方法尝试裁决,但是看条件,这里并不会裁决出结果,所以也会退出。这样按下事件的GestureBinding.handleEvent执行完了。

  4. 此时长按手势的500ms定时器事件触发,然后调用GestureArenaEntry.resolve宣布胜利,

@override
void didExceedDeadline() {
  // Exceeding the deadline puts the gesture in the accepted state.
  resolve(GestureDisposition.accepted);
  _longPressAccepted = true;
  super.acceptGesture(primaryPointer!);
  _checkLongPressStart();
}

当手指抬起(PointerUpEvent)

由于之前已经裁决完成,我们知道裁决完成会从GestureArenaManager._arenas移除该竞技场,所有抬起手指后的gestureArena.sweep(event.pointer);并不会做任何事

总结

GestureArenaManager中的五个维护方法中,其中的close、sweep会参与裁决(release也会,但是它是间接调用了sweep);

而这两个方法是只被在GestureBinding.handleEvent中的调用的:

## GestureBinding ##
@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.close(event.pointer);会尝试裁决,但是不一定能分出胜负; gestureArena.sweep(event.pointer);打扫竞技场,是作为竞技流程的最后一道保障,防止出现无法分出胜负的可能。

而在手势识别器内部,他们可以调用GestureArenaEntry.resolve在合适的时机直接宣布胜利或者失败。

还要注意一点的是,一旦某个成员被裁决胜利,该竞技场会宣布其他成员失败,并且从GestureArenaManager._arenas中移除该竞技场;而如果只是宣布该成员失败,在没有成员获胜的情况并不会移除该竞技场,只是从该竞技场中移除该成员,在GestureArenaEntry.resolve能看到相关实现。