爆裂吧现实

不想死就早点睡

0%

Flutter 手势事件解析(二)

  • 什么是手势冲突?它和 Listener 有啥区别?
  • Flutter 是怎么解决手势处理的?

如果你对上述问题的答案没有底,就需要来恶补 flutter 手势处理的规则了 😁

一、什么是手势冲突?它和 Listener 有啥区别?

要解释这个问题,首先先来定义下什么是手势。flutter 官方对于手势的定义是:

一种语义操作(点击、拖动、缩放)。通常是由一系列单独的指针事件 (pointer event) 组成,也可以是多个单独的指针事件组组成。原文

既然是有语义的,那么避免不了冲突,比如横向拖动和竖向拖动,当手指在屏幕上滑动的时候,我是响应横向滚动呢?还是竖向滚动呢?还是两个一起动?

Listener widget 就是一个指针事件消费者,不存在语义的定义,相当于独行侠,自己处理自己的事件,和其它 Listener 不相干,但是手势就不同了,它不能只考虑自己,这样整个 app 的交互就会乱。

二、Flutter 是怎么解决手势处理的?

Flutter  的手势处理源码分析较为复杂,这里直接先说解决方案

当 Flutter 中存在手势冲突的时候,它是根据 手势竞技场(gesture arena) 来解决这个问题的。所有的 手势识别器 GestureRecongnizer 都是一个 手势竞技场成员(GestureArenaMember

当屏幕上的指定指针有多个 手势识别器 时,框架会让每个 手势识别器 加入到 手势竞技场 来处理手势消歧。手势竞技场 会根据以下规则来判定胜出者:

  • 在任何时候,识别器都可以宣告失败并离开竞技场。如果竞技场中只有一个识别器,那么这个识别器就是胜者。
  • 在任何时候,任何识别器都可以宣告胜利,这将导致这个识别器胜出,其他识别器失败。

假如有两个手势识别器,横向拖动、纵向拖动,当指针 down 落下后,这两个手势识别器都会加入到 手势竞技场 中,观测指针的移动事件 (PointerMoveEvent)。如果用户在横向上移动超过了特定像素,横向识别器会宣告胜利,手势也会被当作横向拖动处理。同样的,如果用户在纵向上移动超过了特定的像素,纵向识别器会宣告胜利。

但是如果 手势竞技场 只有一个手势识别器的话,这个识别器就会被立即胜出,比如只有一个横向拖动识别器的时候,就不需要等到横向移动一定距离后了,可以立即响应横向拖动,非常高效灵敏。

1. 如果有两个竖向拖动识别器是怎么处理的?

这里的处理逻辑其实和 一个横向,一个竖向识别器 一样,存在多个识别器的话需要看哪个识别器先宣告成功,或者主动宣告失败直到还剩一个识别器。

一般来说存在两个竖向拖动识别器是 ListView 嵌套 ListView 的情况,在这种情况下只有子 ListView 会响应,因为指针事件(pointer event)是 child 优先消费的。

到这里为止,关于 flutter 怎么处理手势冲突的解决方案就讲完了,接下来就是枯燥的源码分析环节了。对于只需要了解手势冲突解决方案的,看到这里就 OK 了,对于源码和细节想深究的,一起往下看。


三、源码解析

1. 简单回顾下 pointer event 分发逻辑

文章一 中讲述了 pointer event 的分发逻辑

简述就是:pointer down 事件到来后,RendererBinding 会从 RenderObject 树的根开始执行 hitTest 判定,得到 hitTestResult,里面包含了需要消费 pointer event 事件的消费者。然后再将当前的 down 事件和之后的 move / up / cancel 等事件发送给对应的消费者

2. GestureBinding 里面的竞技场管理类 GestureArenaManager

GestureBinding 单例中有个 GestureArenaManager,用于管理手势竞技场,这个管理者是在成员方法 handleEvent(PointerEvent, HitTestEntry) 中被调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {

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

那么这个 handleEvent(..) 什么时候会被调用呢?其实 GestureBinding 自己就是一个 HitTestTarget,也就是它是个 pointer event 消费者,这个在成员方法 hitTest(..) 中可以看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
/// Determine which [HitTestTarget] objects are located at a given position.
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
}

mixin RendererBinding on GestureBinding ...{
@override
void hitTest(HitTestResult result, Offset position) {
rendererView.hitTest(result, position);
// 这里的 surper 就是 GestureBinding 的 hitTest
super.hitTest(result, position);
}
}

GestureBinding 自身也参与到 hitTest(..) 分发中,并且直接把自己加入到 HitTEstResult 的消费者队列中。根据子类 RendererBinding 的实现看,会先执行 rendererView 的 hitTest,然后才执行 GestureBinding 的 hitTest。

这里可以得出结论的是:GestureBinding 会在 RenderObject 树之后去消费 pointer event。


继续解析 handleEvent(..) 的代码:

  1. 在参数 event 是 PointerDownEvent 的时候,会调用 gestureArena.close(event.pointer)方法,这个方法的注释如下:
1
2
3
4
5
6
7
8
9
10
11
/// Prevents new members from entering the arena.
///
/// Called after the framework has finished dispatching the pointer down event.
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 事件的时候,竞技场的报名就结束了,不允许中途报名( GestureBinding 是在 RenderObject 树之后接受 pointer event 的)。所以我们只有在 down 事件的时候可以添加手势识别器到竞技场中。

最后的 _tryToResolveArena(pointer, state); 是用来得出某些情况下的胜利者的,我们来看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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) {
// 激进的获胜者,在 down event 阶段就宣布胜利的
_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;
assert(members.length == 1);
_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);
}

代码中的可以看到,如果 竞技者 只有一位,那么直接宣告这位 竞技者 胜利,如果没有 竞技者,直接移除当前竞技场,如果有个激进的 获胜者(state.eagerWinner,就是在 down 期间就宣告胜利的 竞技者),则把它判定为获胜者。

  1. 当参数 event 是 PointerUpEvent 的时候,会调用 gestureArena.sweep(event.pointer),这个方法的注释如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// Forces resolution of the arena, giving the win to the first member.
/// ...
void sweep(int pointer) {
final _GestureArena state = _arenas[pointer];
if (state == null)
return; // This arena either never existed or has been resolved.
_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);
}
}

PointerUpEvent 是一次完整触摸事件的终点,这时候竞技场管理者就开始清扫竞技场了,如果这时候还没有获胜的竞技者,那么第一位将会获胜!

sweep 可以被延迟(这部分代码省略显示了),需要调用 gestureArena.hold(pointer),可以延迟竞技场的清理直到 gestureArena.release(pointer) 被调用

GestureBinding 中关于竞技场处理的逻辑到这里就完了,GestureBinding 只负责了关闭竞技场报名和最后的清理竞技场,并没有关于 PointerMoveEvent 中间过程中的一些竞技判定逻辑。为了查找这部分的逻辑,我们来分析下 GestureDetector 的源码:

GestureDetector 源码解析

GestureDetector 是一个 StalessWidget,它的大部分代码就是把各种手势回调转化为手势识别器,例如将 onHorizontalDragStart 转换为 HorizontalDragGestureRecognizer,然后将这些识别器代理到 RawGestureDetector 中。

RawGestureDetector 是个 StatefulWidget,在它的 build 方法中,我们可以看到 Listener 的身影:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@override
Widget build(BuildContext context) {
Widget result = Listener(
// 只监听了 onPointerDown 事件
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
if (!widget.excludeFromSemantics)
result = _GestureSemantics(
child: result,
assignSemantics: _updateSemanticsForRenderObject,
);
return result;
}

Listener 是监听原始 pointer event 的组件,这里只监听了 onPointerDown 事件,继续往下追踪:

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

可以看到在 _handlePointerDown 的时候,遍历了所有的 GestureRecognizer 将当前的 down 事件传递了进去。这里和我当初的预期有些差异,我以为这里的 Listener 会监听所有的 down、move、up、cancel,然后代理到 GestureRecognizer 中,看代码并不是。那么手势识别器是在什么时候接受 move、up、cancel 等 pointer event 的呢?

这里就要看下 recognizer.addPointer(event) 的实现了:

1
2
3
4
5
6
7
8
9
10
11
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}

@protected
void addAllowedPointer(PointerDownEvent event) { }

。。。啥都没有,继续看下子类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer, event.transform);
...
}
}

class OneSequenceGestureRecognizer extends GestureRecognizer {
@protected
void startTrackingPointer(int pointer, [Matrix4 transform]) {
// 这里向 GestureBinding 单例中添加了一个人 route,用于接受 pointer event
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}

GestureArenaEntry _addPointerToArena(int pointer) {
if (_team != null)
return _team.add(pointer, this);
// 将当前手势识别器当做竞争者加入到竞争场
return GestureBinding.instance.gestureArena.add(pointer, this);
}
}

class GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {

@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
// router 的分发在 GestureBinding 的 handleEvent 中
pointerRouter.route(event);
...
}
}

可以在子类实现中看到,最后会向 GestureBinding 单例中注册接收 pointer event 用的 handler,并且在 GestureBinding 的竞技场管理员报名自己参与竞技。也就是到这为止,手势识别器已经可以收到 pointer event ,并且报名参加了手势竞争。

3. 如何宣告胜利/失败

既然手势识别器可以收到 pointer event 了,那么它就可以随时宣告胜利/失败了,来看下这部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
@override
void handleEvent(PointerEvent event) {
//... 这里省略处理手势逻辑
// 宣告胜利
resolve(GestureDisposition.accepted);
}

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

class GestureArenaEntry {

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

上述代码是拖动识别器中宣告胜利的代码,宣告胜利后最后会调用到 GestureArenaEntry 中的 resolve 方法。这里 DragGestureRecognizer 会遍历所有的竞技场是因为它是一个 OneSequenceGestureRecognizer,如果存在多个手指触摸事件,会一起处理,要么一起胜利,要么一起失败。

我们继续看下 _arena._resolve(_pointer, _member, disposition) 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class GestureArenaManager {
/// [pointer] 对应哪个指针事件流
/// [member] 宣告 胜利/失败 的竞争者
/// [disposition] 胜利/失败
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
// 先看下 pointer 对应的竞技场是否存在
// 如果不存在说明当前竞技场已经有胜利者了
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 还处于 open 状态,也就是当前竞争者在 down 事件的时候就宣告胜利了,把它
// 设置为 激进胜利者(关于它的处理看之前的分析)
state.eagerWinner ??= member;
} else {
assert(_debugLogDiagnostic(pointer, 'Self-declared winner: $member'));
// 宣告当前 member 为胜利者,其它 member 为失败者
_resolveInFavorOf(pointer, state, member);
}
}
}
}

通过源码分析可以知道,_arena._resolve(_pointer, _member, disposition) 会让当前 _member 可能成为胜利者,为什么是可能呢?因为竞技场可能不存在,只要竞技场有胜利者,当前竞技场就会从 GestureArenaManager 移除,之后的竞技者来获取胜利的时候就发现没有竞技场了。

到这里为止,关于手势指针事件相关的内容就都讲完了!源码分析并不是分析所有的源码,不过道路都一样,核心功能都已经分析了,其它的可以自己需要的时候去分析。