GestureBinding(手势)
- 接收来自设备的原始事件;
- 将原始事件转化为Flutter事件(
PointerDataPacket=>PointEvent) - 分发事件到相应的Widget
- 由
GestureArenaManager处理多个手势之间的竞争; - 管理手势的生命周期
@override
void initInstances() {
super.initInstances();
_instance = this;
/// 设备手势事件回调,
platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
try {
/// 1.通过`PointerEventConverter.expand` 将 事件(aPointerDataPacket.data)转化为 Flutter 的逻辑事件,以便在处理事件时可以不用考虑不同设备带来的影响。
_pendingPointerEvents.addAll(
PointerEventConverter.expand(packet.data, _devicePixelRatioForView),
);
if (!locked) {
/// 2.处理事件
_flushPointerEventQueue();
}
} catch (error, stack) {
...
}
}
void _flushPointerEventQueue() {
while (_pendingPointerEvents.isNotEmpty) {
handlePointerEvent(_pendingPointerEvents.removeFirst());
}
}
/// 创建每个指针的命中测试结果并储存到_hitTests中
/// 注意的是:设备的上的每个动作都会被捕获并保存进来
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent ||
event is PointerSignalEvent ||
event is PointerHoverEvent ||
event is PointerPanZoomStartEvent) {
hitTestResult = HitTestResult();
/// 因为 mixin RendererBinding on GestureBinding ...,且在RendererBinding 也实现了 `hitTestInView` 方法
/// WidgetFlutterBinding 混入的顺序是 `GestureBinding`,再 `RendererBinding`
/// 所以这里会先调用 RendererBinding 的 `hitTestInView` 再调用 GestureBinding 的`hitTestInView`
/// RendererBinding的`hitTestInView`方法多了这个代码 `_viewIdToRenderView[viewId]?.hitTest(result, position: position);`
/// RenderView及其child添加HitTestResult到首先响应手势的event中
hitTestInView(hitTestResult, event.position, event.viewId);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent ||
event is PointerCancelEvent ||
event is PointerPanZoomEndEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down || event is PointerPanZoomUpdateEvent) {
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
/// 分发事件
dispatchEvent(event, hitTestResult);
}
}
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
if (hitTestResult == null) {
pointerRouter.route(event);
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
}
void hitTestInView(HitTestResult result, Offset position, int viewId) {
/// 这个`this`就是GestureBinding
result.add(HitTestEntry(this));
}
/// 因为GestureBinding的 hitTestInView 方法里面,会创建一个target为GestureBinding 的 HitTestEntry,`dispatchEvent`最终会调用此方法
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
/// `GestureArenaManager`处理事件竞争,
void sweep(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null) {
return;
}
assert(!state.isOpen);
if (state.isHeld) {
state.hasPendingSweep = true;
return;
}
_arenas.remove(pointer);
if (state.members.isNotEmpty) {
// First member wins.
// 这个first就是触发相应的GestureRecognizer,然后由这个GestureRecognizer去触发对应的方法
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);
}
}
}
总结:
- 与RendererBinding的协作,获取渲染树进行命中测试,坐标变换和事件分发;
- 通过GestureDetector等Widget接收手势事件,支持各种内置手势识别器;
GestureBinding是Flutter输入系统的核心,它确保了从底层硬件输入到上层Widget手势事件的完整处理链路;
补充:
/// 这是 RenderView中 hitTest 的处理
bool hitTest(HitTestResult result, {required Offset position}) {
if (child != null) {
/// 判断子节点是否需要添加事件
/// 此方法会对 child 进行层级遍历,对于命中的 child 添加一个 BoxHitTestEntry 节点
/// child为 RenderBox 类型
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
}
/// 添加一个HitTestEntry节点
result.add(HitTestEntry(this));
return true;
}
/// child为 RenderBox 类型,这是 RenderBox 中 hitTest 方法,这是一个父类。
/// 条件方法:hitTestChildren 会被子类重写,
/// 在子类里面 hitTestChildren 基本也都会去调用其 hitTest 方法,这就实现了事件的冒泡,在最上层的节点,会被 result.add 加到第一位。
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;
}
(图片是从网上大佬那里获得的)