引擎层
Flutter Engine 暴露了 FlutterEngineSendPointerEvent
接口供平台层调用。通过向该接口传递 FlutterPointerEvent
对象告知引擎当前指针状态。
FLUTTER_EXPORT
FlutterEngineResult FlutterEngineSendPointerEvent(
FLUTTER_API_SYMBOL(FlutterEngine) engine,
const FlutterPointerEvent* events,
size_t events_count);
SDK 层
事件源
Flutter SDK 则通过 GestureBinding
类管理手势事件。GestureBinding
在初始化时在 window
对象中设置了 _handlePointerDataPacket
回调接收引擎层传回的指针状态。
@override
void initInstances() {
super.initInstances();
_instance = this;
window.onPointerDataPacket = _handlePointerDataPacket;
}
经过指针事件队列 _pendingPointerEvents
缓冲后,handlePointerEvent
将对事件进行重采样处理。之后在 _handlePointerEventImmediately
过程中进行实际事件分发。
命中测试
在 _handlePointerEventImmediately
过程中事件分发前需要先对 PointerDownEvent 事件构造 HitTestResult
。
// @@ void _handlePointerEventImmediately(PointerEvent event) {
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
assert(() {
if (debugPrintHitTestResults)
debugPrint('$event: $hitTestResult');
return true;
}());
}
这里将会涉及 HitTestResult
和 HitTestEntry
两个类。HitTestResult
中存储了一个点击测试顺序队列 _path
,以控制测试顺序。而 HitTestEntry
则是每个点击测试的入口。
final List<HitTestEntry> _path;
在 GestureBinding
类中,hitTest
仅将自己作为命中入口放入 result 中。而在 HitTestResult
类的 add
方法中,都会将 HitTestEntry
存入了 _path
尾部。
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
void add(HitTestEntry entry) {
assert(entry._transform == null);
entry._transform = _lastTransform;
_path.add(entry);
}
但这显然并不满足点击测试的需求,仅保证了根节点自己在点击测试的顺序路径中。
所以实际上 hitTest
方法在 RendererBinding
中被重载。并交由 RenderView
处理,也就是 Flutter 渲染对象树的根节点。
@override
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
assert(result != null);
assert(position != null);
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
在 RenderView
类的 hitTest
方法中,先对子节点 RenderBox
进行了判断,再将自己添加到命中测试路径中。而 RenderBox
将递归处理增加所有子节点的点击测试入口。
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null)
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
事件分发
在 HitTestResult
构造完成后,就是对事件进行分发。
// @@ void _handlePointerEventImmediately(PointerEvent event) {
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
分发实现在 dispatchEvent
方法中,并分为两种情况。
在没有点击测试结果的事件,也就是 Added、Remove 两种事件中,进行路由分发。也就是仅对在 PointerRouter
对象中注册过此事件的路由进行通知。通常在 GestureRecognizer
类中由 addPointer
方法注册。
// @@ void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (hitTestResult == null) {
assert(event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
// ...
}
return;
}
对于其他有点击测试结果的事件,按照点击测试路径顺序进行事件分发。由前面 RenderView
类对点击测试入口追加的顺序来看,Flutter 采用的是由子节点到根节点的冒泡顺序。
// @@ void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
// ...
}
}
根据前面的代码中,点击测试路径将按 RenderBox
树、RenderView
、GestureBinding
的顺序进行调用。GestureBinding
自己也在其中。
在 RenderBox
树中,常见的是由 Listener
Widget 产生的 RenderPointerListener
对象。该对象允许原始指针事件直接回调给 Widget 中的回调函数。
// @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent)
return onPointerDown?.call(event);
if (event is PointerMoveEvent)
return onPointerMove?.call(event);
if (event is PointerUpEvent)
return onPointerUp?.call(event);
if (event is PointerHoverEvent)
return onPointerHover?.call(event);
if (event is PointerCancelEvent)
return onPointerCancel?.call(event);
if (event is PointerSignalEvent)
return onPointerSignal?.call(event);
}
而在 GestureBinding
类自己的事件处理方法中,可以看到在界面渲染树点击测试处理完成后,所有事件也被送入 PointerRouter
中进行路由分发。
// @@ mixin GestureBinding
@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);
}
}
手势探测器
Flutter SDK 推荐使用 GestureDetector
类来监测用户手势输入,而不是直接使用 Listener
来监听原始指针事件。在 GestureDetector
类中,Flutter 封装了多种常见的手势识别器,放入 RawGestureDetector
中的使用。
RawGestureDetecotr
中使用 Listener
Widget 来监听原始指针事件中的指针按下事件,并通知所有手势识别器。
// @@ class RawGestureDetectorState extends State<RawGestureDetector>
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
// ...
return result;
}
GestureRecognizer
类是所有手势识别器的基类,但是这里,仅指针按下事件被发送给识别器。并且其中提供了 debugPrintRecognizerCallbacksTrace
全局变量以供调试手势数据。
// @@ class RawGestureDetectorState extends State<RawGestureDetector>
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (final GestureRecognizer recognizer in _recognizers!.values)
recognizer.addPointer(event);
}
注释中开发者给出了解释,探测器要将自己加入全局指针路由 PointerRouter
,并且加入全局手势竞技场 GestureArenaManager
。
/// It's the GestureRecognizer's responsibility to then add itself
/// to the global pointer router (see [PointerRouter]) to receive
/// subsequent events for this pointer, and to add the pointer to
/// the global gesture arena manager (see [GestureArenaManager]) to track
/// that pointer.
手势识别器
除了基类 GestureRecognizer
,flutter 还提供了几个基础抽象类,方便复用一些通用逻辑。
独占手势 OneSequenceGestureRecognizer
提供了保证同时只有一个手势生效的机制,也实现了全局路由注册管理。
单一主指针手势 PrimaryPointerGestureRecognizer
提供主指针判断机制,用于保证一些单指手势不受其它指针影响。并且提供了指针移动限制、停留时间限制来区分长按、点按、拖动等基础手势。
基于这些抽象类,flutter 提供了几种常见的手势识别器,以供使用。
手势竞技场
所有的将自己放入竞技场的手势识别器,它们将在竞技场中决出胜者。
debugPrintGestureArenaDiagnostics
全局参数提供了竞技场状态信息。