Flutter中的事件

事件的传递和处理

同Android一样, Flutter主要处理的事件也是DOWN、MOVE、UP、CANCEL等事件组成的事件流.

确定控件

binding.dart中的GestureBinding是Flutter事件处理链的源头, 我们就以它为线索来学习Flutter的事件处理.

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
void _handlePointerEvent(PointerEvent event) {
HitTestResult hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent) {
//Down事件是事件流的开始
hitTestResult = HitTestResult();
//通过事件的位置确定受影响的控件
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
//_hitTests是一个Map
//将这些控件暂存在一个Map中, 以备后面的复用
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
//UP和CANCEL事件标志一个事件流的结束, 受该事件六影响的控件以后不会再有
//被复用的情况, 可以从Map中删除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
//不是DOWN、UP、CANCEL并且event的状态还处于down的情况, 那就是MOVE事件了.
//可以看到, MOVE事件是直接复用的DOWN事件的控件
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
//受影响的控件都已经确定了, 接下来可以进行事件分发了
dispatchEvent(event, hitTestResult);
}
}

上面是GestureBinding处理事件流的函数, 也可以说是事件传递的一个预处理. 通过这个函数, Flutter可以通过事件流的位置来确定哪些控件收到了影响. Flutter将受到影响的控件按照Widget树的层次顺序排序, 然后再对它们进行事件分发.

要理解这个函数的实现, 我们需要先知道几个函数中用到的类.

1
2
3
4
5
abstract class PointerEvent with Diagnosticable {
final int pointer;
final Offset position;
final Offset localPosition;
}

首先是PointerEvent类. 显然, 函数中用到的PointerDownEvent、PointerUpEvent、PointerCancelEvent这是事件类都是这个抽象类的实现. 上面两个属性是函数中用到的两个属性, pointer是一个事件流的唯一标识. 注意, 是事件流的唯一标识. 也就是说, 同一个事件流里, DOWN、UP、CANCEL、MOVE的pointer都是相同的. 这很好理解, 但出现多点触碰的情况时, 会同时产生好几个事件流, 这时就需要通过pointer来对它们分别进行管理.

position用来标识事件发生的位置. 除了position, PointerEvent类还有一个localPosition属性. 因为Flutter是一个跨平台框架, 它在逻辑上处理事件的发生位置时应该是与设备无关的. 也就是说, Flutter框架一开始接收的position数据只是一个逻辑上的值, Flutter通过这个值来确定事件能影响到哪些控件, 但是在反映到设备上时, 使用的是localPosition的数值. 其实它们就是一个映射关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class HitTestTarget {
factory HitTestTarget._() => null;
void handleEvent(PointerEvent event, HitTestEntry entry);
}

class HitTestEntry {
HitTestEntry(this.target);
final HitTestTarget target;
}

class HitTestResult {
Iterable<HitTestEntry> get path => _path;
final List<HitTestEntry> _path;
void add(HitTestEntry entry) {
entry._transform = _transforms.isEmpty ? null : _transforms.last;
_path.add(entry);
}
}

然后是HitTestResult类和它所用到的两个功能类. 上面的源码是HitTestResult类源码中的节选. 通过名字可以大致猜到, 一个HitTestResult保存了一个事件流所影响的所有控件. 控件以HitTestEntry的形式存储在_path中. 对HitTestEntry再拆分, 得到的是一个HitTestTarget接口. 我们看到这个接口是有一个分发事件的handleEvent方法的, 据此我们猜测, 应该所有的控件都实现了这个接口, 通过这个接口可以来检测哪些控件受到了事件流的影响.

看到这儿我们发现, 整个函数中并没有真正把事件传递链放进_path里. 我们注意到, GestureBinding是一个mixin, 那么一定有其它类来重写了它的一些方法.

通过重写查找, 我们找到了一个继承了GestureBinding类的类: RendererBinding.

1
2
3
4
5
6
7
8
abstract RendererBinding{
RenderView renderView;
@override
void hitTest(HitTestResult result, Offset position) {
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
}

可惜的是, RendererBinding是一个抽象类, 我们还要进一步去看它拥有的RenderView.

1
2
3
4
5
6
7
8
9
class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>{
RenderBox child;
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
}

RenderView继承自RenderObject, 而RenderObject又实现了HitTestTarget, 所以其实RenderView是间接实现了HitTestTarget接口的. 我们看到RenderView也有一个并非重写的的hitTest方法, 大致逻辑是先对自己的child进行检测, 然后再将自己加入到受影响的控件集合中.

RenderView的child是一个RenderBox类型的实例, RenderBox也继承自RenderObject, 所以它也间接实现了HitTestTarget接口.

1
2
3
4
5
6
7
8
9
10
11
abstract RenderBox extends RenderObject{
bool hitTest(BoxHitTestResult result, { @required Offset position }) {
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
}

RenderBox是一个抽象类, 所有的Widget都继承自它, 自然所有的Widget都继承了HitTestTarget接口, 这也可以证明我们之前的猜测是正确的. 从RenderBox自己拥有的hitTest方法来看, 所有Widget受某一个事件流的影响有两种情况: 它的child正在监听该事件或者它自己正在监听该事件.

简单总结一下这部分的结论: Flutter的事件流从DOWN事件开始, 经过若干个MOVE事件, 以UP事件或者CANCEL事件结束. 在处理DOWN事件时, 所有Widget按照自己继承的RenderBox类的hitTest方法依次判断自己是否受事件影响, 最终形成了以目标节点–>父节点–>根节点(也就是RenderView)–>GestureBinding的顺序构成的事件处理链, UP、MOVE、CANCEL事件都在复用DOWN事件所产生的事件处理链.

事件分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
·@override 
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
if (hitTestResult == null) {
try {
pointerRouter.route(event);
} catch (exception, stack) {...}
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {...}
}
}

GestureDetector的dispatchEvent函数实现了事件的分发. 在_handlePointerEvent函数中, 我们得到了每一个事件的处理链, dispatchEvent函数其实就是按照处理链的先后顺序来让Widget依次对事件进行响应. 这一点与Android有所不同, Android的事件传递机制中, 如果子控件处理了事件, 那么父控件是直接返回的; 在Flutter中, 子控件是否处理事件, 都不影响父控件是否处理事件.

GestureDetector的实现

这部分的内容非常长且枯燥. 如果了解源码的实现, 那么建议一定要连着看下去, 不然很容易思路就乱了. 如果只是想大致了解一下GestureDetector的实现过程, 那么可以直接跳到文章最后的总结.

手势的判断

前面我们说了Flutter的事件处理链和事件分发, 这里我们说一下Flutter的事件监听.

GestureDetector是Flutter为我们封装好的事件监听的功能类, 可以识别大多数的手势, 我们就以它为线索来学习Flutter的事件监听.

1
2
3
4
5
6
7
8
9
10
11
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

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

上面是GestureDetector的build函数, 中间有一大段生成gestures的内容, 主要是定义各种手势的, 我们先不看. 函数的最后是根据前面定义的手势和行为生成创建RawFestureDetector实例, 也就是说GestureDetector的功能主要是由RawGestureDetector完成的.

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

RawGestureDetector是一个StatefulWidget, 上面是它关联的State类的build函数. 可以看到, 它又是通过Listener来实现功能的.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Listener extends StatelessWidget {

/// Called when a pointer comes into contact with the screen (for touch
/// pointers), or has its button pressed (for mouse pointers) at this widget's
/// location.
final PointerDownEventListener onPointerDown;

/// Called when a pointer that triggered an [onPointerDown] changes position.
final PointerMoveEventListener onPointerMove;

/// Called when a pointer enters the region for this widget.
///
/// This is only fired for pointers which report their location when not down
/// (e.g. mouse pointers, but not most touch pointers).
///
/// If this is a mouse pointer, this will fire when the mouse pointer enters
/// the region defined by this widget, or when the widget appears under the
/// pointer.
final PointerEnterEventListener onPointerEnter;

/// Called when a pointer that has not triggered an [onPointerDown] changes
/// position.
///
/// This is only fired for pointers which report their location when not down
/// (e.g. mouse pointers, but not most touch pointers).
final PointerHoverEventListener onPointerHover;

/// Called when a pointer leaves the region for this widget.
///
/// This is only fired for pointers which report their location when not down
/// (e.g. mouse pointers, but not most touch pointers).
///
/// If this is a mouse pointer, this will fire when the mouse pointer leaves
/// the region defined by this widget, or when the widget disappears from
/// under the pointer.
final PointerExitEventListener onPointerExit;

/// Called when a pointer that triggered an [onPointerDown] is no longer in
/// contact with the screen.
final PointerUpEventListener onPointerUp;

/// Called when the input from a pointer that triggered an [onPointerDown] is
/// no longer directed towards this receiver.
final PointerCancelEventListener onPointerCancel;

/// Called when a pointer signal occurs over this object.
final PointerSignalEventListener onPointerSignal;

/// How to behave during hit testing.
final HitTestBehavior behavior;

// The widget listened to by the listener.
//
// The reason why we don't expose it is that once the deprecated methods are
// removed, Listener will no longer need to store the child, but will pass
// the child to `super` instead.
final Widget _child;
}

上面是Listener的源码, 结合源码给出的注释以及属性的名称, 我们可以知道, Listener就是用来监听DOWN、MOVE等原始事件的.

根据RawGestureDetector的build函数, 我们看到RawGestureDetector持有了一个监听DOWN事件的Listener. 这个可以理解, 因为DOWN事件是事件流的起始.

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

上面是RawGestureDetector在遇到DOWN事件的回调, _recognizers是GestureDetector定义的一系列手势. 那么这个函数的逻辑就很清楚了: 每次遇到一个事件流的开始, RawGestureDetector就将这个事件通知给所有的手势定义类中, 由它们进行后续的判定.

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

继续追踪addPointer函数, _pointToKind是一个Map, 用来存储输入设备, 不是我们关心的重点. isPointerAllowed函数用来判断手势是否接收这个事件, 接受与否要根据手势的定义以及手势当前的状态决定. 如果手势可以接收这个事件, 那么就调用addAllowedPointer函数来改变状态并继续追踪事件流; 否则调用handleNonAllowedPointer来拒绝这个事件. 这三个函数被不同的GestureRecognizer分别进行实现, 从而实现了不同手势的判断.

手势的竞争

只有手势的判断并不能完全解决问题, 很多手势都需要接收一系列的MOVE事件, 要想保证手势之间不发生冲突, 就要解决手势处理事件的优先级问题.

1
2
3
4
5
6
7
@protected
void startTrackingPointer(int pointer) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}

无论是什么GestureRecognizer, 最终都会在调用继承的OneSequenceGestureRecognizer类的startTrackingPointer函数. 顾名思义, 这个方法就是用来追踪事件流的. GestureBinding中的PointerRouter是一个Map, 它的key是事件流的标识pointer, value是手势的一系列回调. _trackedPointers是一个HashSet, 记录这个GestureRecognizer目前都正在追踪哪些事件流. _entries也是一个Map, key值同样是pointer, value值是一个GestureArenaEntry类. 如同HitTestEntry一样, GestureArenaEntry类也是一个封装类, 它由GestureArenaManager、pointer和GestureArenaMember组成. pointer是事件流的标识, GestureArenaMember是GestureRecognizer, 在GestureArenaEntry类里起始就是这个GestureRecognizer自己.

pointerRouter我们先不管, _trackedPointers.add(pointer)也很好理解, 我们先看最后的_addPointerToArena函数.

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

GestureBinding的gestureArena是一个GestureArenaManager类. 它实际上就是维护了一个Map. 这个Map的key值仍然是GestureRecognizer正在追踪的事件流, value是一个_GestureArena类. 这个类也很简单, 就是维护一个GestureArenaMember的List. GestureBinding.instance.gestureArena.add这个函数的实现逻辑, 我们可以用下面的这个比喻去理解:

一个GestureRecognizer是一个竞技场斗士, 它正在追踪的事件流的编号可以理解为是这个斗士被分配的组, 每一组都是都有一个竞技场_GestureArena. GestureArenaManager是比赛的委员会, 它通过add函数来将每一位斗士按照被分配的组引导到对应的竞技场中. 在同一个竞技场中的都是需要进行比赛, 赢的人就可以获得事件的处理权.

但是委员会怎么知道冠军要怎么处理事件呢? 我们再看startTrackingPointer函数中的GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);这句话. 原来斗士要想进入竞技场, 就必须先把自己怎么处理事件告诉委员会才行.

接下来就是正式的竞争了.

1
2
3
4
5
6
7
8
9
10
11
@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);
}
}

由于GestureBinding是宏观上分发所有事件的角色, 所以我们还是要总它的handleEvent开始看. 从函数中可以看到, 一旦遇到了DOWN事件, 就会调用GestureArenaManager的close函数.

1
2
3
4
5
6
7
8
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);
}

close函数的原理很简单, 首先判断这个事件流是不是正常的, 如果是就关闭竞技场并且开始竞争.

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}

_tryToResolveArena的逻辑也很简单. 如果竞技场没有人, 那么直接关闭这个竞技场并返回. 如果竞技场只有一个人, 那么冠军就直接产生了. 如果竞技场有多个人, 那么就要解决优先级问题了. 解决优先级问题的方式也很简单——打假赛. 当eagerWinner不为空的时候, 说明这些GestureRecognizer中有一个高优先级的, 这种情况下就不用比赛了, 直接让他胜出就好了.

当然, 大多数情况下GestureRecognizer的优先级都是相同的. 这种情况也不需要我们特别操心, 因为目前只处理了DOWN事件, 通过后续的事件, 还是可以让对应手势的GestureRecognizer坚持到最后的.

总结

说了这么多, 可能大家关于Flutter的事件机制还是没有一个特别清楚的认识, 所以在这里我们再简单总结一下

  1. GestureDetector定义了一系列手势. 由于所有的手势的第一个事件都是DOWN, 所以只有DOWN事件产生后, 所有手势的GestureRecognizer才会被激活.

  2. GestureRecognizer被激活后, 首先要看是否存在高优先级的GestureRecognizer. 被激活的GestureRecognizer全部被加入到GestureBinding的gestureArena中. gestureArena通过事件流的唯一标识pointer来将被激活的GestureRecognizer进行分组

  3. 所有的Widget都继承自RenderBox类. 当DOWN事件产生时, 所有的Widget首先判断事件是否发生在自己的区域内, 如果是, 那么先通过递归判断自己子控件是否在监听事件, 如果有, 那么就将子控件和自己加入这个事件的处理链; 如果没有那么再看自己是不是在监听事件, 是的话就只将自己加入处理链. 这个过程结束后, 就会形成一条可能会响应该事件流的处理链. MOVE、UP、CANCEL事件都会复用DOWN事件产生的处理链.

  4. 事件链产生完毕后, Flutter认为所有被激活的GestureRecognizer都应该被分组了, 这时关闭对应的事件流的分组, 开始判断优先级.

  5. 少数情况下, 事件流只激活了一个GestureRecognizer或者激活了一个高优先级的GestureRecognizer, 亦或者没有激活GestureRecognizer, 这样就解决了同一时刻多个控件征用事件冲突的问题.

  6. 大多数情况下, 不存在事件冲突的问题. 事件流后续的事件通过每个GestureRecognizer的addPointer函数来判断是否符合手势定义. 如果是, 那么调用addAllowedPointer函数来响应事件; 否则调用addNonAllowedPointer函数来拒绝事件.

  7. addAllowedPointer会根据接受的事件决定GestureRecognizer的新状态, 如果是手势的结束状态, 那么就接受这个手势, 并调用响应的回调; 否则继续等待下一个事件.