likes
comments
collection
share

Flutter刨根问底——点击事件(下)

作者站长头像
站长
· 阅读数 9

前言

本文将会从源码角度分析Flutter中手势竞争即手势竞技场的流程设计,并且分析一些常用GuestureRecognizer对于事件的处理及在竞技场中的取胜和弃权策略来了解Flutter的手势响应原理。

GestureArena

本部分介绍的是手势竞技场,竞技流程及比赛规则

Flutter刨根问底——点击事件(下) 以上类图是竞技场的类图

虽然看起来很复杂,其实主要的就是GestureArenaManager_GestureArena以及GestureArenaMember,下面将会梳理下它们之间的关系。

  1. GestureArenaManager是GestureArena的管理类,在GestureBinding中创建,其中的arenas是一个pointer与GestureArena的Map,每一个pointer都有一个GestureArena,当收到PointerDown事件后,相关Gesture会调用GestureBinding.instance.gestureArena.add方法,若manager中无pointer对应的GestureArena,则创建并将自身add到GestureArena中的members中。
  2. _GestureArena就是竞技场类,其实就是一个记录类,记录竞技场的成员以及竞技场的状态。
  3. GestureArenaMemeber是竞技场中的成员,一般为GestureRecognizer,需要实现acceptGesture以及rejectGesture方法,由manager来告知其胜出与否,胜出调用acceptGesture,否则调用rejectGesture。
  4. GestureArenaEntry是一个入口类,每个member和pointer都会生成一个,调用GestureBinding.instance.gestureArena.add方法时创建,由member持有,主要用于调用entry中的resolve进行主动地acceptGesture或rejectGesture。

流程

以上简单介绍了GestureArena的主要角色之间的关系,接下来从源码的角度分析其设计

从上篇中我们知道,一次手势的开端是PointerDownEvent,在此之前没有route(但是有GlobalRoute),Down事件由Listener来处理,更加具体的,在现有组件中,是由GestureDetector里的recognizors来处理,比如我们使用的InkWell,Scrollable其实都是使用的GestureDetector。GuestureDetector中包含丰富的手势回调,onTap设置点击回调,onDoubleTap设置双击, onLongPress设置长按等。因此我们先从GestureDetector入手,了解整个手势的流程,再根据flutter的其他例子加深了解。

1. GestureDetector

// 为了防止大家太长不看,进行了简化
Widget build(BuildContext context) {
  final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
  final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;

  if (...
  ) {
    // 点击
    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 双击
  if (onDoubleTap != null) {
    gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
      () => DoubleTapGestureRecognizer(debugOwner: this),
      (DoubleTapGestureRecognizer instance) {
        instance
        ...
      },
    );
  }
  // 长按
  if (...) {
    gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
      () => LongPressGestureRecognizer(debugOwner: this),
      (LongPressGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 滑动,垂直方向
  if (...) {
    gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
      () => VerticalDragGestureRecognizer(debugOwner: this),
      (VerticalDragGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 滑动,水平方向
  if (...) {
    gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
      () => HorizontalDragGestureRecognizer(debugOwner: this),
      (HorizontalDragGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 拖拽移动,全方向
  if (...) {
    gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
      () => PanGestureRecognizer(debugOwner: this),
      (PanGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 双指缩放
  if (...) {
    gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
      () => ScaleGestureRecognizer(debugOwner: this),
      (ScaleGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 按压力度
  if (...) {
    gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
      () => ForcePressGestureRecognizer(debugOwner: this),
      (ForcePressGestureRecognizer instance) {
        instance
          ...
      },
    );
  }

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

GestureDetector几乎涵盖了我们用到的所有的手势,如果设置了相关手势回调,就会将其添加到gestures中,gestures是一个Map<Type,GestureRecognizerFactory>,Type是recognizer的类型,factory用于创建recognizer的instance以及instance的初始化。

2. RawGestureDetector

RawGestureDetector是一个StatefulWidget,我们直接看State中的build方法

Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    onPointerPanZoomStart: _handlePointerPanZoomStart,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  if (!widget.excludeFromSemantics) {
    result = _GestureSemantics(
      behavior: widget.behavior ?? _defaultBehavior,
      assignSemantics: _updateSemanticsForRenderObject,
      child: result,
    );
  }
  return result;
}

看到了熟悉的Listener,以及onPointerDown的回调(上篇已介绍)。接下来看看_handlePointerDown这个方法。

void _handlePointerDown(PointerDownEvent event) {
  assert(_recognizers != null);
  for (final GestureRecognizer recognizer in _recognizers!.values) {
    recognizer.addPointer(event);
  }
}

调用每一个需要处理回调的recoginer的addPointer方法,传入PointerDownEvent。

3. GestureRecognizer

void addPointer(PointerDownEvent event) {
  // kind: 事件来源,比如手指点击、鼠标、触控笔、触摸板等。
  _pointerToKind[event.pointer] = event.kind;
  // 是否处理pointer
  if (isPointerAllowed(event)) {
    // 接受pointer
    addAllowedPointer(event);
  } else {
    // 拒绝pointer
    handleNonAllowedPointer(event);
  }
}

addPointer,从名字可以看出,是添加pointer。虽然传入的是event,但是需要注意的是该event为PointerDownEvent,是一个pointer的开始事件,所以主要的关注点我们应放在pointer上,即整个pointer对应的event流上,而不是局限于单次的PointerDownEvent,因为通过接下来对该函数中三个调用的方法的分析,会发现其重要的作用是在需要处理该pointer时,添加该pointer的route以及将自身注册到该pointer的arena,对于down事件的处理只是顺带的。

接下来我们以OneSequenceGestureRecognizer中的addAllowedPointer为例,大多数手势都是继承自该类

isPointerAllowed

主要根据是否设置了相应的回调以及是否是支持的kind来确定是否添加pointer,我们以TapGestureRecognizor为例

bool isPointerAllowed(PointerDownEvent event) {
  switch (event.buttons) {
    case kPrimaryButton:
      if (onTapDown == null &&
          onTap == null &&
          onTapUp == null &&
          onTapCancel == null) {
        return false;
      }
      break;
    case kSecondaryButton:
      if (onSecondaryTap == null &&
          onSecondaryTapDown == null &&
          onSecondaryTapUp == null &&
          onSecondaryTapCancel == null) {
        return false;
      }
      break;
    case kTertiaryButton:
      if (onTertiaryTapDown == null &&
          onTertiaryTapUp == null &&
          onTertiaryTapCancel == null) {
        return false;
      }
      break;
    default:
      return false;
  }
  return super.isPointerAllowed(event);
}

文中的buttons是什么?结合前面的kind来看,如果是touch,即我们的手指点击,那么buttons只有kPrimaryButton;如果是鼠标输入,因为鼠标有左右键以及中间键,所以有kPrimaryButton、kSecondaryButton、kTertiaryButton三个,分别表示左键,右键,中间键。对于手机来说,我们只考虑touch的输入即可。

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

void startTrackingPointer(int pointer, [Matrix4? transform]) {
  // 添加route
  GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  // 加入arena
  _entries[pointer] = _addPointerToArena(pointer);
}

GestureArenaEntry _addPointerToArena(int pointer) {
  if (_team != null) {
    return _team!.add(pointer, this);
  }
  return GestureBinding.instance.gestureArena.add(pointer, this);
}

startTrackingPointer中看到了addRoute,这个在GestureBinding中dispatch时会调用route,从而触发handleEvent,将事件交由recognizer处理。添加至竞技场也是在此时,通过调用GestureBinding.instance.gestureArena.add添加。

其中route的回调handleEvent在不同的recognizer中的实现并不相同,接下来看几个例子。

1. PrimaryPointerGestureRecognizer
void handleEvent(PointerEvent event) {
  if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
    // 可容忍的移动距离,大于时将会判断为滚动,将会reject
    // 否则将会调用handlePromaryPointer进行后续的事件处理。
    final bool isPreAcceptSlopPastTolerance =
        !_gestureAccepted &&
        preAcceptSlopTolerance != null &&
        _getGlobalDistance(event) > preAcceptSlopTolerance!;
    final bool isPostAcceptSlopPastTolerance =
        _gestureAccepted &&
        postAcceptSlopTolerance != null &&
        _getGlobalDistance(event) > postAcceptSlopTolerance!;

    if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
      resolve(GestureDisposition.rejected);
      stopTrackingPointer(primaryPointer!);
    } else {
      handlePrimaryPointer(event);
    }
  }
  stopTrackingIfPointerNoLongerDown(event);
}
/// stopTrackingPointer会remove route
void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
  if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
    stopTrackingPointer(event.pointer);
  }
}

PrimaryPointerGestureRecognizer是一个抽象类,其子类为TapGestureRecognizer和LongPressGestureRecognizer,类如其名,就是用于处理单手势的。其中的state是什么?有三种:

  1. GestureRecognizerState.ready:默认状态,等待手势
  2. GestureRecognizerState.possible:处理中状态,可能会处理手势
  3. GestureRecognizerState.defunct:停止状态,拒绝手势

状态机转换:

Flutter刨根问底——点击事件(下)

2. ScaleGestureRecognizer

这个手势是双指缩放手势,从其对handleEvent的处理,我们可以与前者的PrimaryPointerGestureRecognizer对比一下,了解下差异,以加深对事件处理流程的理解。

void handleEvent(PointerEvent event) {
  // 所有的recognizer中handleEvent处理事件时,state都不应该是ready
  assert(_state != _ScaleState.ready);
  // 手势是否改变,收到了DOWN或UP事件,pointer队列改变
  bool didChangeConfiguration = false;
  // 是否应该从accepted转变为started状态,具体看下图状态机
  bool shouldStartIfAccepted = false;
  if (event is PointerMoveEvent) {
    // 速度Tracker,可以计算move的速度
    final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
    if (!event.synthesized) {
      // 记录新的position以及时间戳
      tracker.addPosition(event.timeStamp, event.position);
    }
    // 更新pointer的position
    _pointerLocations[event.pointer] = event.position;
    shouldStartIfAccepted = true;
    _lastTransform = event.transform;
  } else if (event is PointerDownEvent) {
    // 更新pointer的position
    _pointerLocations[event.pointer] = event.position;
    // 如果是DOWN事件,添加pointer
    _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) {
    // PanZoom为鼠标的缩放事件,这里不再介绍
    assert(_pointerPanZooms[event.pointer] == null);
    _pointerPanZooms[event.pointer] = _PointerPanZoomData(
      focalPoint: event.position,
      scale: 1,
      rotation: 0
    );
    didChangeConfiguration = true;
    shouldStartIfAccepted = true;
  } else if (event is PointerPanZoomUpdateEvent) {
    assert(_pointerPanZooms[event.pointer] != null);
    if (!event.synthesized) {
      _velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
    }
    _pointerPanZooms[event.pointer] = _PointerPanZoomData(
      focalPoint: event.position + event.pan,
      scale: event.scale,
      rotation: event.rotation
    );
    _lastTransform = event.transform;
    shouldStartIfAccepted = true;
  } else if (event is PointerPanZoomEndEvent) {
    assert(_pointerPanZooms[event.pointer] != null);
    _pointerPanZooms.remove(event.pointer);
    didChangeConfiguration = true;
  }

  _updateLines();
  _update();

  if (!didChangeConfiguration || _reconfigure(event.pointer)) {
    _advanceStateMachine(shouldStartIfAccepted, event.kind);
  }
  stopTrackingIfPointerNoLongerDown(event);
}

其中的处理比较复杂,很多字段一眼看去也不知道其作用是什么,不过我们可以从分析状态机出发。

_ScaleState有四种:

  • ready : 默认状态,等待手势
  • possible : 处理中状态,可能会处理手势
  • accepted : 已接受状态,目前的一系列手势已被接受为scale手势
  • started : 开始状态,初始状态已确定

状态机如下:

Flutter刨根问底——点击事件(下)

ready与possible很好理解,started与accepted有什么区别呢?accepted用于表示已经接受手势,但是还没有开始处理,此处的处理为生成scale数据,并不是指处理手势;而started表示已经开始可以产生scale数据,因为产生scale数据需要手势的初始位置信息,所以在pointer变更时,initial数据失效,需要重新生成,因此先变为accepted状态。简单的区别就是,在此次event之前有无正确的initial信息。

整个ScaleGestureRecognizer就是在DOWN或UP时生成initial信息,之后MOVE时把此时的信息与initial信息作比生成scaleFactor,line的作用是计算旋转角度,具体的处理这里不再详细介绍,有兴趣的同学可以自行扒下源码。

handleEvent简单总结

从以上两个例子我们可以看出来,handleEvent是recognizer处理事件的核心,根据事件在各种状态间转换,其实广义地说就是三个状态:等待、接收、接受。收到DONW事件,选择处理将进入接收状态,后续该pointer的事件都会接收,根据手势自己的策略,在此之后判断是否接受,若接受,将会调用resolve选择在竞技场中胜出,否则调用resolve选择拒绝。

handleNonAllowedPointer

这个其实没什么可介绍的,就是在所有已接收pointer的竞技场中宣布失败,或者什么都不做。

4. GestureArena

手势竞技场的整体策略其实很简单,相信同学们跟着扒扒源码都能理解。

4.1 GestureArenaManager

先看下manager中的方法,arena是由manager进行管理,需要提前明确一下多指操作下的resolve处理方式,resolve会调用该手势所有的entry的resolve方法,接下来看下这几个主要方法:

Add
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);
}

这个是add方法,我们前面在介绍recognizer时有调用过这个方法。这个是创建一个pointer对应的arena,并将member添加到这个arena中,并返回一个member与pointer对应的entry,这个entry可以说就是为了让member调用manager的resolve方法的,之所以需要一个entry,是为了权限管理,防止胡乱调用resolve,导致整个手势的混乱😊。

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;
    return; // This arena is being held for a long-lived member.
  }
  _arenas.remove(pointer);
  if (state.members.isNotEmpty) {
    // First member wins.
    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);
    }
  }
}

顾名思义,打扫竞技场,如果竞技场中有成员的话,强制产生一个胜者。这个主要是在UP事件时调用,以及UP事件时被hold,推迟了sweep,但在之后的release中也会sweep,总之,就是在收到UP事件之后,一定会sweep,虽然会迟到,但永远不会缺席😉。

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);
}

关闭竞技场,防止进入新的成员。在GestureBinding中收到DOWN事件后就会close,为什么呢?因为GestureBinding是最后加进去的,此时所有的HitTarget都已经加进去了,并且所有可能的手势都已经在addAllowedPointer时加了进去,此时关闭竞技场是合理的,之所以要关闭,是为了防止一个pointer的竞技场产生多个获胜者,试想一下,此时pointer对应的竞技场已经产生了胜者,将其从manager中移除了,之后新的成员加入仍然会添加该pointer的竞技场,虽然一个竞技场只有一个胜者,但是因为创建了多个竞技场,就会导致产生多个胜者。

resolve
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) { // 当有急需获胜的,让他赢!
    _resolveInFavorOf(pointer, state, state.eagerWinner!);
  }
}

/// 默认选择第一个成员作为胜者
void _resolveByDefault(int pointer, _GestureArena state) {
  if (!_arenas.containsKey(pointer)) {
    return; // This arena has already resolved.
  }
  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);
  for (final GestureArenaMember rejectedMember in state.members) {
    if (rejectedMember != member) {
      rejectedMember.rejectGesture(pointer);
    }
  }
  member.acceptGesture(pointer);
}

选出胜者。在竞技场还未关闭时,不能选出胜者,在竞技场关闭后,也即所有成员都已登记,开始竞争😡!这时候有一个人说,让我获胜吧,你们这些小卡乐咪,这时候会调用到_resolveinfavorOf,然后获胜;如果有人选择了放弃,这时候会看下竞技场还有没有人,如果只剩一个,那么剩下的那个人将获胜;如果还剩下不只一个,这时候会选择之前宣布获胜的人,先来后到嘛(此场景几乎不会进入,因为在close时eaegrWinner就已经被真正选为胜者);在有人宣布获胜时,宣布者将作为胜者。

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));
}

保持竞技场,防止在sweep时被回收,在多次点击操作中出现。

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;
  if (state.hasPendingSweep) {
    sweep(pointer);
  }
}

释放竞技场,与hold成对出现,在多次点击操作中出现。

4.2 Summary

先简单总结一下从DOWN事件开始,手势与竞技场的整个流程。

  • 从DOWN事件开始,每个recognizer根据event的kind以及buttons来决定自己是否处理event,若需要处理,则会startTrackingPointer,在此方法中会添加自己的handleEvent作为route的回调添加到该pointer的pointerRoute中,该pointer的所有事件都会在GestureBinding的handleEvent中route到每个recognizer中;同意处理的同时,也会将自身添加到该pointer的arena中。
  • UP之前的事件,recognizer根据自身的策略,比如MOVE事件,CANCEL事件来决定自身是否中止处理,同时也会将自身移出arena,一般的拒绝策略都是超时、距离过远等。
  • UP事件标志着一个pointer的结束,此时,一般会调用sweep清理竞技场,并且会选出一个胜者,有些recognizer如DoubleTapGestureRecognizer会阻碍竞技场的关闭,调用hold,防止竞技场sweep。
  • 竞技场中手势的竞争其实并不是以策略保证公平,而是类似于一种君子协定,你先声明你accepted,那么你就是胜者,比如EagerGestureRecognier永远都会获胜。

Quiz

经过源码的历练,我相信以下问题对各位来说都不在话下。

1. 竞技场可以有多个胜者吗?

答案是不能,同时也因为这个原因,我们在嵌套widget中若分别在parent以及child中设置点击事件,只会有一个能够响应手势。

2. LongPress,DoubleTap,Tap这三个手势之间是怎么在竞技场中竞争的?

根据前面所学习的,竞争的说法其实并不十分准确,其实整个过程是各个手势按照自身的策略在event回调中声明accepted以及rejected以确定胜者,若无胜者最后竞技场sweep时选第一个手势作为结束。那么各自的策略有什么不同呢?

  • 假如我们想让Tap获胜,我们点了一下并且没有长按,因为没有达到最小长按事件500ms,LongPress不会选择accepted,则在处理Up事件时,将会选择reject;DoubleTap则在UP时hold竞技场,阻止sweep,等待第二次点击,在等待超时后(300ms),选择reject,此时release竞技场,进行sweep,此时竞技场中只剩下Tap事件,Tap获胜。
  • 可以再次印证,所有的手势只关心event,time,position,这三者作为输入,手势按照自身策略做出不同的决策:Tap手势不争不抢,不会主动accept,只有在move超过一定距离之后才会reject;DoubleTap需要有两次tap事件,并且两次事件之间的间隔合法,距离合法,才会accept,否则会reject;LongPress需要tap的down与up之间的时间超过阈值才会accept。

3. 怎么实现一个可以在parent以及child中都可以响应回调的手势?

可以使用一个单例维护一个pointer与List<void Function()>的map,在自定义tap手势中,down时将onTap添加到pointer对应的list中,,获胜时调用pointer对应回调列表的所有回调。

4. 自定义手势需要注意什么?

若需要的是在不影响现有手势的条件下自定义一个手势,最简单的做法就是以自定义策略决定是否reject,但不主动accept。但是这样不能获胜怎么办?答案是听天由命,这种注意下add到竞技场的顺序。选择这种做法本身就是将手势自身的主动性降到最低,若想能够更快获胜,应该提升主动性,在适合的时候宣布获胜即可,当然这种对于策略的制定有一定的要求,有一定的难度。

转载自:https://juejin.cn/post/7326758010524483624
评论
请登录