【Flutter】事件分发机制
我正在参加「掘金·启航计划」
缘起GestureBinding
1-onPointerDataPacket指针数据接收
flutter/packages/flutter/lib/src/gestures/binding.dart:GestureBinding
@override
void initInstances() {
super.initInstances();
_instance = this;
platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}
// 事件队列
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
// 平台监听到的事件通过_pendingPointerEvents收集
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked) {
_flushPointerEventQueue();
}
}
void _flushPointerEventQueue() {
// 循环执行事件
while (_pendingPointerEvents.isNotEmpty) {
handlePointerEvent(_pendingPointerEvents.removeFirst());
}
}
- 在
GestureBinding
初始化方法initInstances
中为平台分发器注册_handlePointerDataPacket
- 监听平台回调将事件保存在
_pendingPointerEvents
队列中 - 未锁状态下执行
_flushPointerEventQueue
,从队列中推出第一个事件向下分发,直到队列事件为空为止。
2-handlePointerEvent创建命中测试
flutter/packages/flutter/lib/src/gestures/binding.dart:GestureBinding -> _handlePointerEventImmediately
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
// 按下、开始缩放等操作时新建一个命中测试对象
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
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) {
// 已经按下或者是缩放事件时 可以在map中找到已存事件并获取
hitTestResult = _hitTests[event.pointer];
}
...
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
// 下一步进行分发
dispatchEvent(event, hitTestResult);
}
}
- 事件对象进入
_handlePointerEventImmediately
判断事件类型做相应处理,并根据相应事件类型返回命中测试对象。 - 最后在将事件和生成的命中测试对象去执行
dispatchEvent
方法。
_handlePointerEventImmediately
负责接收事件对象生成对应命中测试对象,将事件对象分三大类区分操作(大致可以分为按下、更新、抬起):按下
会创建HitTestResult
对象并向HitTestResult
添加HitTestEntry
命中对象最后保存到_hitTests
表中;更新
表示当前事件还存在从_hitTests
表中取出即可;抬起
表示当前事件结束了需要从从_hitTests
表中移除。
hitTests命中测试
执行GestureBinding
的hitTest
会先经过RenderBinding
去执行hitTests
方法,首先RenderView
根节点判断是否存在子节点并判断子节点是否在命中范围内。
/flutter/packages/flutter/lib/src/rendering/binding.dart:RendererBinding->hitTest
@override
void hitTest(HitTestResult result, Offset position) {
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
1-RenderView:hitTest
/flutter/packages/flutter/lib/src/rendering/view.dart:RenderView -> hitTest
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null) { // 子布局是否命中
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
}
result.add(HitTestEntry(this));
return true;
}
2-RenderBox:hitTest
RenderView
根节点的子节点是RenderSemanticsAnnotations
根本上是继承自RenderBox
。默认情况下RenderBox
的hitTestSelf
和hitTestChildren
返回都是false
,具体命中逻辑由继承者自行重写实现(hitTestChildren
是具备有多子节点布局来实现默认情况下false
表示无子节点)。
/flutter/packages/flutter/lib/src/rendering/box.dart:RenderBox -> hitTest
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;
}
@protected
bool hitTestSelf(Offset position) => false;
@protected
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;
若命中了自身hitTestSelf
或子级hitTestChildren
时添加BoxHitTestEntry
(继承自HitTestEntry
);hitTestChildren
又是递归调用因此组件树命中测试是深度优先遍历,符合命中规则子节点会比父节点先被添加到HitTestResult
队列中例如RenderDecoratedBox
的hitTestSelf
重写方法通过Decoration
的hitTest
进行判断是否在命中范围内。
@override
bool hitTestSelf(Offset position) {
return _decoration.hitTest(size, position, textDirection: configuration.textDirection);
}
X支线-defaultHitTestChildren命中逻辑
/flutter/packages/flutter/lib/src/rendering/stack.dart:RenderStack -> hitTestChildren
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
return defaultHitTestChildren(result, position: position);
}
Stack
内部RenderStack
的hitTestChildren
是采用RenderBoxContainerDefaultsMixin
默认方法defaultHitTestChildren
判断子节点是否在命中测试。
/flutter/packages/flutter/lib/src/rendering/box.dart:RenderBoxContainerDefaultsMixin -> defaultHitTestChildren
bool defaultHitTestChildren(BoxHitTestResult result, { required Offset position }) {
ChildType? child = lastChild;
while (child != null) {
// The x, y parameters have the top left of the node's box as the origin.
final ParentDataType childParentData = child.parentData! as ParentDataType;
final bool isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child!.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
}
child = childParentData.previousSibling;
}
return false;
}
方法中会循环遍历父节点中每个子节点对象是否命中测试,若有其中一个子节点命中则直接返回,因此其他兄弟子节点并没有机会参与命中测试而被直接忽略。由于父节点是RenderStack
特殊性子节点存在重叠情况,在处理命中测试时就需要特殊处理。
- 类似
Stack
布局空间重叠的父节点其命中测试比较特殊 - 重叠空间中子节点遍历是倒序的,由于下层节点被上层遮盖所以先测试上层子节点。
- 如何打破这种规则让被遮盖的下层节点也能参与命中测试?
X支线-IgnorePointer命中忽略
IgnorePointer
是支持可忽略命中组件,内部RenderIgnorePointer
重写了hitTest
方法,若ignoring
为true
可跳过自身命中条件变为不可命中状态。
/flutter/packages/flutter/lib/src/rendering/proxy_box.dart:RenderIgnorePointer -> hitTest
@override
bool hitTest(BoxHitTestResult result, { required Offset position }) {
return !ignoring && super.hitTest(result, position: position);
}
这就如上所述在Stack
空间重叠布局中,当不可命中的IgnorePointer
子节点在可点击子节点
上层时不影响下层子节点参与命中测试。
dispatchEvent事件分发
flutter/packages/flutter/lib/src/gestures/binding.dart:GestureBinding -> dispatchEvent
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) {
...
}
}
}
pointerRouter
接收事件类- 遍历
HitTestResult
的HitTestEntry
数组执行handleEvent
方法
遍历顺序是从最底层子节点开始往上走,从最后的
HitTestEntry
可以看到分别是RenderView
和GestureBinding
两个类处理事件分发。
命中测试符合要求节点记录下来后通过该方法进行遍历操作并执行每个节点handleEvent
方;handleEvent
由每个节点重写实现具体逻辑。
1-handleEvent子分发
例如TextSpan
重写是handleEvent
方法,分发下来点击事件交给GestureRecognizer
处理。
/flutter/packages/flutter/lib/src/painting/text_span.dart:TextSpan -> handleEvent
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
if (event is PointerDownEvent) {
recognizer?.addPointer(event);
}
}
例子如下,TextSpan
实现TapGestureRecognizer
由其接受来自addPointer
指针事件。
TextSpan(
text: "https://flutterchina.club",
style: const TextStyle(color: Colors.blue),
recognizer: TapGestureRecognizer()
..onTap = () {
showSnackBarMsg(context, "onTap -> TextSpan");
},
),
RawGestureDetectorSate
的_handlePointerDown
方法会调用到addPointer
会执行addAllowedPointer
。
/flutter/packages/flutter/lib/src/gestures/recognizer.dart:GestureRecognizer -> addPointer
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
接着会走到OneSequenceGestureRecognizer
的addAllowedPointer
->startTrackingPointer
-> _addPointerToArena
/flutter/packages/flutter/lib/src/gestures/recognizer.dart:OneSequenceGestureRecognizer
@override
@protected
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer, event.transform);
}
...
@protected
void startTrackingPointer(int pointer, [Matrix4? transform]) {
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);
}
重点在最后调用到GestureBinding.instance.gestureArena
2-GestureArenaManager竞技场
GestureArenaManager
来负责处理指针事件。这和上述提到的TextSpan
实现有所不同。GestureArenaManager
也是处理手势特殊管理类,它好比一个竞技场主要功能是找到真正胜利者来消费手势事件。例如上级下发一个指令只能由一个人来接手完成这个指令,如果每个人都响应指令可能会乱套,因此采用规则来找到唯一胜利者来接手指令。
/flutter/packages/flutter/lib/src/gestures/arena.dart:GestureArenaManager -> add
GestureArenaEntry add(int pointer, GestureArenaMember member) {
final _GestureArena state = _arenas.putIfAbsent(pointer, () {
assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
return _GestureArena();
});
state.add(member);
assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
return GestureArenaEntry._(this, pointer, member);
}
在上述提到OneSequenceGestureRecognizer
事件分发最终会走到GestureArenaManager
的add
会将指针记录下来,等待竞技场开始比拼时做提前指针收集。
3-handleEvent最后分发
GestureBinding
同样实现了HitTestTarget
接口handleEvent
方法,分发最后就是由GestureBinding
接管最终分发结果。
/flutter/packages/flutter/lib/src/gestures/binding.dart:GestureBinding -> handleEvent
@override // from HitTestTarget
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
竞技场做最后决定,主要以三个方法处理:close
、sweep
、resolve
;GestureArenaManager
的add
是在各个子节点handleEvent
分发时实现了。
/flutter/packages/flutter/lib/src/gestures/arena.dart:GestureArenaManager -> sweep
void sweep(int pointer) {
final _GestureArena? state = _arenas[pointer];
if (state == null) {
return; // This arena either never existed or has been resolved.
}
...
if (state.isHeld) {
state.hasPendingSweep = true;
...
return; // This arena is being held for a long-lived member.
}
...
_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);
}
}
}
关键性逻辑在GestureArenaManager
的sweep
方法在竞技场中成员对象头一个将获胜获取到可以接收手势请求的机会;其他成员对象则没有机会。
/flutter/packages/flutter/lib/src/gestures/tap.dart:BaseTapGestureRecognizer -> acceptGesture
@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown();
_wonArenaForPrimaryPointer = true;
_checkUp();
}
}
最后由BaseTapGestureRecognizer
接收到手势执行方法处理手势事件。
转载自:https://juejin.cn/post/7244194707542065212