事件流
在你点击按钮,滑动列表,缩放图片等等交互过程中,在背后却有成千上百的事件触发,如何处理这些事件?如何掌控事件的流动?无论在web, android或者ios,都是学习的一个难点,在Flutter同理也是一样,究竟Flutter的事件流有啥特别之处,接下来就慢慢展示给大家。
从根源开始
事件从哪里来?一般来说都不需要应用开发者去担心事件是如何从硬件收集起来的,但是事件的传递总需要有个源头。
在Flutter里面主要处理事件和手势相关的就在gestures文件夹下。
而Flutter框架事件的源头就在gestures/binding.dart里的GestureBinding类开始:
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onPointerDataPacket = _handlePointerDataPacket;
}
可见事件是由ui.window.onPointerDataPacket产生,把事件传给GestureBinding._handlePointerDataPacket方法,而ui.window这个就是sky引擎的实现,以后有机会再去深入,现在只需关注上层。
纵观整个代码,会发现有很多binding,SchedulerBinding,GestureBinding,ServicesBinding,RendererBinding和WidgetsBinding等都跟引擎相关的,以后再慢慢逐个分析。
接着继续跟踪方法的调用过程:
先看_handlePointerEvent方法:
void _handlePointerEvent(PointerEvent event) {
assert(!locked);
HitTestResult result;
if (event is PointerDownEvent) {
assert(!_hitTests.containsKey(event.pointer));
result = new HitTestResult();
hitTest(result, event.position);
_hitTests[event.pointer] = result;
assert(() {
if (debugPrintHitTestResults)
debugPrint('$event: $result');
return true;
}());
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
result = _hitTests.remove(event.pointer);
} else if (event.down) {
result = _hitTests[event.pointer];
} else {
return; // We currently ignore add, remove, and hover move events.
}
if (result != null)
dispatchEvent(event, result);
}
当是PointerDownEvent事件的时候,会新建一个HitTestResult对象,而这个HitTestResult对象里面有一个path的属性,可以推测这个属性就是用来记录事件传递所经过的的节点。
新建HitTestResult对象后,接下来重点就是调用GestureBinding.histTest方法。
在看看hitTest方法:
void hitTest(HitTestResult result, Offset position) {
result.add(new HitTestEntry(this));
}
这里把自身添加到HitTestResult上,意味着以后dispatchEvent时候会遍历path上的HitTestEntry,也会调起GestureBinding.handleEvent方法。
接着再看handleEvent方法:
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);
}
}
这里就看到pointerRouter路由事件,以及手势相关的一些处理,手势等会再说。
但是看完整个方法调用都没看到事件是如何传递到节点树上,而pointerRouter仅仅是一个观察者模式的实现,找遍了代码也没找到对应的listener,事件是如何传递?我们的点击事件是如何响应?依然不清楚。
柳暗花明
既然GestureBinding上并没有事件如何传递节点树的实现,再看哪里用到这个类,总有地方需要依赖它的。
很快就注意到WidgetsFlutterBinding这个类了。
class WidgetsFlutterBinding extends BindingBase with SchedulerBinding, GestureBinding, ServicesBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
new WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
WidgetsFlutterBinding这个类mixin了好几个Binding,同时这个类也是框架的初始化入口,当我们跑起整个Flutter应用时:
void main() {
runApp(new MyApp());
}
runApp其实就会执行WidgetsFlutterBinding.ensureInitialized方法初始化各个Binding:
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
然后attachRootWidget方法,就去设置根节点了:
void attachRootWidget(Widget rootWidget) {
_renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
这里看到了真正的根节点renderView,事件怎样也应该从根节点开始传递吧。
沿着renderView就找到RndererBinding.hitTest方法:
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
renderView.hitTest(result, position: position);
// This super call is safe since it will be bound to a mixed-in declaration.
super.hitTest(result, position); // ignore: abstract_super_member_reference
}
到这里基本可以确定先调用RendererBinding.histTest方法接着调用GestureBinding.histTest方法。
再回头GestureBinding的实现也就是先让renderView.hitTest方法去确定事件传递路径,都添加到HitTestResult的path上,最后再添加GestureBinding自身作为最后的一个HitTestEntry。
而GestureBindg.dispatchEvent会遍历这些HitTestEntry调用他们的handleEvent方法:
void dispatchEvent(PointerEvent event, HitTestResult result) {
assert(!locked);
assert(result != null);
for (HitTestEntry entry in result.path) {
try {
entry.target.handleEvent(event, entry);
} catch (exception, stack) {
FlutterError.reportError(new FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
stack: stack,
library: 'gesture library',
context: 'while dispatching a pointer event',
event: event,
hitTestEntry: entry,
informationCollector: (StringBuffer information) {
information.writeln('Event:');
information.writeln(' $event');
information.writeln('Target:');
information.write(' ${entry.target}');
}
));
}
}
}
还有一个重点就是节点上hitTest方法实现,而节点一般都是继承自RenderBox的实现:
bool hitTest(HitTestResult result, { @required Offset position }) {
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(new BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
当然首先判断点击是否在节点位置上,然后再交给children处理,接着自身处理,如果hitTestChildren或者hitTestSelf返回true,就把当前节点加入到HitTestResult上。
这个时候HitTestResult中的路径顺序一般就是:
目标节点-->父节点-->根节点-->GestureBinding
接着PointerDown,PointerMove,PointerUp,PointerCancel等事件分发,都根据这个顺序来遍历调用它们的handleEvent方法,就像浏览器事件的冒泡过程一样,既然像冒泡一样,搞过web开发的同学都知道,浏览器是可以用代码阻止冒泡的,那Flutter行不行尼?答案,暂时还没有发现有方法可以阻止这个冒泡过程。
手势
现在已经清楚框架的事件流,现在开始深入框架的手势系统。
GestureDector
The GestureDetector widget decides which gestures to attempt to recognize based on which of its callbacks are non-null.
根据文档所说GestureDetector控件可以检测手势,并且根据手势调起相应回调。
GestureDector真的支持了相当多的手势,基本上常用都有了,框架实在太给力!
那GestureDector控件为什么有这么大本领,而手势是如何检测的尼?
先对这个控件层层剥皮,看它的build方法:
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
...
return new RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
}
}
可以看到GestureDector其实就是根据注册的回调,添加对应的GestureRecognizer(手势识别器),并全传递到RawGestureDetector。
而RawGestureDetector的build方法:
Widget build(BuildContext context) {
Widget result = new Listener(
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child
);
if (!widget.excludeFromSemantics)
result = new _GestureSemantics(owner: this, child: result);
return result;
}
关键在于_handlePointerDown方法:
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (GestureRecognizer recognizer in _recognizers.values)
recognizer.addPointer(event);
}
遍历_recognizers(手势识别器)调用addPointer方法,一般来说recognizer都是继承自PrimaryPointerGestureRecognizer的实现:
void addPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible;
primaryPointer = event.pointer;
initialPosition = event.position;
if (deadline != null)
_timer = new Timer(deadline, didExceedDeadline);
}
}
到这里先理一下流程,当确定PointerDown事件落在GestureDector控件下的子组件时,在GestureDector上注册的GesutreRecognizer就会追踪这个pointer(就是我们的手指),注意了这里还是设置一个Timer后面再说有什么作用,先看startTrackingPointer方法:
void startTrackingPointer(int pointer) {
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}
啊哈,这里用到了GestureBinding.instance.pointerRouter,还记得上面提到的吗,事件传递的最后一站其实就是GestureBinding,然后调用它的handleEvent方法,到最后就是调用pointer.route方法路由事件,所以还要调用GestureRecognizer的handleEvent方法。
接着再看GestureRcognizer._addPointerToArea方法
GestureArenaEntry _addPointerToArena(int pointer) {
if (_team != null)
return _team.add(pointer, this);
return GestureBinding.instance.gestureArena.add(pointer, this);
}
这里又用到GestureBinding.instance.gestureArena,其实就是GestureArenaManager,再看add方法:
GestureArenaEntry add(int pointer, GestureArenaMember member) {
final _GestureArena state = _arenas.putIfAbsent(pointer, () {
assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
return new _GestureArena();
});
state.add(member);
assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
return new GestureArenaEntry._(this, pointer, member);
}
这里就是新建了一个GestureArenaEntry对象,好吧,我们得整理一下他们的关系:
class GestureArenaManager {
final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
}
class _GestureArena {
final List<GestureArenaMember> members = <GestureArenaMember>[];
}
class OneSequenceGestureRecognizer extends GestureArenaMember {
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
}
下面用个形象一点的方法描述它们的关系:
首先我们有一批竞技选手(各种Recognizer),我们也可能会有好几个竞技场地(_GestureArena),我们的场地管理员(GestureArenaManager)会根据Pointer的多少来构建场地,但是各个选手也要拿到每个竞技场的入场券(GestureArenaEntry)才能入场与其他选手一较高下。
当我们的选手拿着对应的入场券进场后,现在各个场地都聚集了一批选手,叮的一声(PointerDown事件),各个场地入口关闭,过了一会激烈的竞技,又叮的一声(PointerUp事件)竞技结束,我们就要打扫竞技场看一下哪一位选手胜利了。
这里PointerDown事件和PointerUp事件控制场地关闭和打扫,主要代码在GestureBinding.handleEvent方法上,上面就有提到,这里就不贴了。
那么怎么判断哪个手势是最后赢得胜利留下来的呢,不像现实竞技场那么残酷,这里是很斯文优雅的,对手自己会判断是否要退出竞争,判断条件当然是PointerDown,PointerMove,PointerUp事件传递的信息是否符合当前手势的定义,如果不符合就自动退出,如果符合就向竞技场(_GestureArena)申请我符合条件,请判我获取胜利,其他手势只能判断为失败了。
但是这里也会有一些情况需要特别处理:
- 如果参与者只有一个,或者其他参与者退出后只剩一个,就会让唯一剩下的参与为胜利
- 如果没有手势请求获取胜利,竞技场也没被其他手势hold住,怎么办,那么竞技场调用sweep方法会让默认第一个手势会判断为胜利,其他判断为失败
- 如果手势之间有冲突,例如一个DoubleTap和一个Tap,DoubleTap手势可以请求竞技场Hold住(等一下不要那么快打扫,判断优胜者),但是请求竞技场hold住的手势,必须之后主动请求竞技场release(好了,你可以打扫了),等DoubleTap手势决定是否是优胜还是自动退出,就可以知道Tap手势是否最终生效,这样看Tap手势好像不会乱搞事情,就静静的等待所有对手退出,自己最终符合第一或者第二个条件,而判断为胜利。
所以整个竞技场的核心,只是仅仅让当前手势知道已经没有别的手势竞争,可以自己判断是否符合当前手势的定义而触发相应的事件,所以竞技场胜利的一方并不是百分百触发手势的,获得竞技场胜利只是触发手势的必要非充分条件。
当然整个机制还是有点出入的,下面还会继续分析。
举个栗子
例如TapGestureRecognizer,在不存在竞争的情况时,当GestureAreaManager.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方法:
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方法:
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);
}
这里最后就调起TapGestureRecognizer.acceptGesture方法:
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown();
_wonArenaForPrimaryPointer = true;
_checkUp();
}
}
_checkDown会尝试调起onTapDown,这里可以说onTapDown会立即调用,_checkUp会尝试调起onTapUp,onTap的回调(至少等onPointerUp事件触发才会成功)。
接下来我们考虑如果父节点监听了Tap手势,也就是出现竞争情况,两个都是TapGestureRecognizer,情况会怎样的尼?
很明显GestureAreaManager.close方法中的_tryToResolveArena方法并没有起到啥作用,这个时候大家还记得deadline这个超时时间吗,TapGestureRecognizer设置的超时时间为100毫秒,当我们按下的时间超过100毫秒
TapGestureRecognizer.didExceedDeadline就会调用接着调起_checkDown方法(意味着onTapDown触发有可能延迟100毫秒,并不完全是你点下的一瞬间就触发),但是我们点击的时间很快(低于100毫秒)的时候又怎样尼?
别忘了在GestureAreaManager的方法处理之前,pointerRouter先会路由事件,直接调起 TapGestureRecognizer.handleEvent
-->TapGestureRecognizer.handlePrimaryPointer
-->TapGestureRecognizer._checkUp
-->TapGestureRecognizer.stopTrackingIfPointerNoLongerDown
-->TapGestureRecognizer.didStopTrackingLastPointer
既然我们在上面事件流的分析知道,事件流就类似浏览器事件冒泡的方式,所以注册在pointerRouter的监听器也是子组件优先调用接着是父组件。接着stopTrackingIfPointerNoLongerDown方法将注册的监听器从pointerRouter移除,didStopTrackingLastPointer方法把TapGestureRecognizer的状态设置成ready,准备好下次手势处理。
这里再简单介绍一下GestureRecognizer的几个状态:
- ready 初始状态准备好识别手势
- possible 开始追踪pointer,不停接收路由的事件,如果停止追踪,则吧状态转回ready
- defunct 手势已经被决议(accepted或者rejected)
最后就是打扫竞技场了:
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);
}
}
默认会让第一个手势胜出,其他都会调起rejectGesture方法。但是在我们刚才的举的例子已经不起作用了,因为手势都处理完毕,都回到ready状态了。
在看看如果是两个不同类型的手势竞争的情况下会怎样,例如:TapGestureRecognizer 和 LongPressGestureRecognizer。
假设在GestureDector上同时注册了onTap和onLongPress。
这个时候GestureRecognizer注册的顺序就很重要了,在GestureDector里面框架已经设置好各自顺序,这里TapGestureRecognizer先于LongPressGestureRecognizer处理事件,因为最后处理手势的时候默认是第一个胜出的。
LongPressGestureRecognizer设置的超时时间为500毫秒,如果点击时间低于500毫秒时,就好像没有竞争情况一样,onTap回调正常调起,但是点击时间超过500毫秒,又会怎样尼?
这时就会调起LongPressGestureRecognizer.didExceedDeadline方法:
void didExceedDeadline() {
resolve(GestureDisposition.accepted);
if (onLongPress != null)
invokeCallback<Null>('onLongPress', onLongPress);
}
而接着调起的就是GestureArenaManager._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);
}
}
}
因为被决议为accepted,最后调起_resolveInFavorOf方法,至于eagerWinner的设置是在hitTest时候resolve才会起效。
再看_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 (GestureArenaMember rejectedMember in state.members) {
if (rejectedMember != member)
rejectedMember.rejectGesture(pointer);
}
member.acceptGesture(pointer);
}
直接reject了TapGestureRecognizer,TapGestureRecognizer的状态被设置为defunt,LongPressGestureRecognizer成为最后的优胜者。
总结
我们可以在GestureRecognizer.handleEvent判断手势是否符合自己定义,例如滑动多少距离范围;
设置deadline超时时间规定手势需在多少时间内完成,或者超出多少时间才符合定义;
当检测到手势符合我们定义或者不符合时,可以调起resolve决议,让其他手势识别放弃监听手势并重置状态;
我们自定义手势识别器应在rejectGesture做一些清理或者状态重置的工作。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。