前言
本文将会从源码角度分析Flutter中手势竞争即手势竞技场的流程设计,并且分析一些常用GuestureRecognizer对于事件的处理及在竞技场中的取胜和弃权策略来了解Flutter的手势响应原理。
GestureArena
本部分介绍的是手势竞技场,竞技流程及比赛规则
以上类图是竞技场的类图
虽然看起来很复杂,其实主要的就是GestureArenaManager ,_GestureArena 以及GestureArenaMember,下面将会梳理下它们之间的关系。
- GestureArenaManager 是GestureArena的管理类,在GestureBinding中创建,其中的arenas是一个pointer与GestureArena的Map,每一个pointer都有一个GestureArena,当收到PointerDown事件后,相关Gesture会调用GestureBinding.instance.gestureArena.add方法,若manager中无pointer对应的GestureArena,则创建并将自身add到GestureArena中的members中。
- _GestureArena就是竞技场类,其实就是一个记录类,记录竞技场的成员以及竞技场的状态。
- GestureArenaMemeber是竞技场中的成员,一般为GestureRecognizer,需要实现acceptGesture以及rejectGesture方法,由manager来告知其胜出与否,胜出调用acceptGesture,否则调用rejectGesture。
- 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
dart
// 为了防止大家太长不看,进行了简化
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方法
php
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**这个方法。
scss
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (final GestureRecognizer recognizer in _recognizers!.values) {
recognizer.addPointer(event);
}
}
调用每一个需要处理回调的recoginer的addPointer方法,传入PointerDownEvent。
3. GestureRecognizer
csharp
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为例
dart
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
dart
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
dart
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是什么?有三种:
- GestureRecognizerState.ready:默认状态,等待手势
- GestureRecognizerState.possible:处理中状态,可能会处理手势
- GestureRecognizerState.defunct:停止状态,拒绝手势
状态机转换:

2. ScaleGestureRecognizer
这个手势是双指缩放手势,从其对handleEvent的处理,我们可以与前者的PrimaryPointerGestureRecognizer对比一下,了解下差异,以加深对事件处理流程的理解。
dart
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 : 开始状态,初始状态已确定
状态机如下:

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
dart
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
dart
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
dart
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
dart
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
dart
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
ini
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到竞技场的顺序。选择这种做法本身就是将手势自身的主动性降到最低,若想能够更快获胜,应该提升主动性,在适合的时候宣布获胜即可,当然这种对于策略的制定有一定的要求,有一定的难度。