本文字数:43617字
预计阅读时间:110分钟
前言
之前有两篇文章都围绕着runApp()
进行展开,讲解了布局绘制的详细过程。
https://www.jianshu.com/p/2ef749ff4d40/
https://www.jianshu.com/p/f37f8da235ec
那么接下来我们想详细的说一说Flutter
是如何处理手势事件的。本文将通过源码详细分析Flutter
的事件分发与冲突处理过程,并通过示例说明不同冲突的处理方式。本文的组织架构如下:
手势事件的初始化
命中测试
PointerEvent的封装
hitTest()
dispatchEvent()
GestureDetector
onTap
onLongPress
onDoubleTap
onVerticalDragDown
手势事件拦截
总结
手势事件的初始化
还是先回到我们熟悉的runApp()
:
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}
前面的文章介绍过WidgetsFlutterBinding
混合了很多mixin
:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding
手势相关的是GestureBinding
,我们来看看它的initInstances()
初始化的实现:
@override
void initInstances() {
super.initInstances();
_instance = this;
platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}
为platformDispatcher
注册了onPointerDataPacket
回调,其实现是_handlePointerDataPacket()
。我们先追踪一下onPointerDataPacket
的回调时机:
PointerDataPacketCallback? get onPointerDataPacket => _onPointerDataPacket;
PointerDataPacketCallback? _onPointerDataPacket;
Zone _onPointerDataPacketZone = Zone.root;
set onPointerDataPacket(PointerDataPacketCallback? callback) {
_onPointerDataPacket = callback;
_onPointerDataPacketZone = Zone.current;
}
继续追踪onPointerDataPacket
的调用时机:
// Called from the engine, via hooks.dart
void _dispatchPointerDataPacket(ByteData packet) {
if (onPointerDataPacket != null) {
_invoke1<PointerDataPacket>(
onPointerDataPacket,
_onPointerDataPacketZone,
_unpackPointerDataPacket(packet),
);
}
}
void _invoke1<A>(void Function(A a)? callback, Zone zone, A arg) {
if (callback == null) {
return;
}
assert(zone != null);
if (identical(zone, Zone.current)) {
callback(arg);
} else {
zone.runUnaryGuarded<A>(callback, arg);
}
}
@pragma('vm:entry-point')
void _dispatchPointerDataPacket(ByteData packet) {
PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}
从注释上我们就可得知,_dispatchPointerDataPacket()
就是接收从framework
层传递的手势事件,并进行处理的时机。_invoke1()
其实就是调用onPointerDataPacket
并将_unpackPointerDataPacket()
的返回值回调。其中packet
是一组未经处理的的ByteData
,通过_unpackPointerDataPacket()
方法对其进行解包处理,生成手势事件所需的实体类PointerData
:
static PointerDataPacket _unpackPointerDataPacket(ByteData packet) {
const int kStride = Int64List.bytesPerElement;
const int kBytesPerPointerData = _kPointerDataFieldCount * kStride;
final int length = packet.lengthInBytes ~/ kBytesPerPointerData;
assert(length * kBytesPerPointerData == packet.lengthInBytes);
final List<PointerData> data = <PointerData>[];
for (int i = 0; i < length; ++i) {
int offset = i * _kPointerDataFieldCount;
data.add(PointerData(
embedderId: packet.getInt64(kStride * offset++, _kFakeHostEndian),
timeStamp: Duration(microseconds: packet.getInt64(kStride * offset++, _kFakeHostEndian)),
//...
scale: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
rotation: packet.getFloat64(kStride * offset++, _kFakeHostEndian),
));
assert(offset == (i + 1) * _kPointerDataFieldCount);
}
return PointerDataPacket(data: data);
}
好了到此为止我们可以得知,在启动app后,由GestureBinding
注册手势的回调事件,当engine
层发送来手势事件后,由PlatformDispatcher
封装成PointerData
实体,最终回调至GestureBinding
进行处理,接下来我们看接收到手势事件后的处理流程。
命中测试
PointerEvent的封装
在真正处理手势之前,第一步就是将手势事件封装成业务可用的PointerEvent
,我们回到GestureBinding
的initInstances()
:
@override
void initInstances() {
super.initInstances();
_instance = this;
platformDispatcher.onPointerDataPacket = _handlePointerDataPacket;
}
跟踪_handlePointerDataPacket
的实现:
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
// We convert pointer data to logical pixels so that e.g. the touch slop can be
// defined in a device-independent manner.
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked) {
_flushPointerEventQueue();
}
}
首先就是把解包后的packet.data
通过PointerEventConverter.expand()
再做次转换:
static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, double devicePixelRatio) {
return data
.where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown)
.map((ui.PointerData datum) {
//...
switch (datum.signalKind ?? ui.PointerSignalKind.none) {
case ui.PointerSignalKind.none:
switch (datum.change) {
//...
case ui.PointerChange.down:
return PointerDownEvent(
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
device: datum.device,
position: position,
buttons: _synthesiseDownButtons(datum.buttons, kind),
obscured: datum.obscured,
pressure: datum.pressure,
//...
);
case ui.PointerChange.move:
return PointerMoveEvent(
timeStamp: timeStamp,
pointer: datum.pointerIdentifier,
kind: kind,
device: datum.device,
position: position,
delta: delta,
buttons: _synthesiseDownButtons(datum.buttons, kind),
obscured: datum.obscured,
//...
);
//...
case ui.PointerSignalKind.unknown:
default: // ignore: no_default_cases, to allow adding a new [PointerSignalKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/engine/pull/34402
// This branch should already have 'unknown' filtered out, but
// we don't want to return anything or miss if someone adds a new
// enumeration to PointerSignalKind.
throw StateError('Unreachable');
}
});
}
截取了部分代码,大致就是将packet.data
转换成PointerEvent
,并添加到_pendingPointerEvents
列表中。接下来我们看_flushPointerEventQueue()
的实现:
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty) {
handlePointerEvent(_pendingPointerEvents.removeFirst());
}
}
void handlePointerEvent(PointerEvent event) {
assert(!locked);
if (resamplingEnabled) {
_resampler.addOrDispatch(event);
_resampler.sample(samplingOffset, _samplingClock);
return;
}
// Stop resampler if resampling is not enabled. This is a no-op if
// resampling was never enabled.
_resampler.stop();
_handlePointerEventImmediately(event);
}
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
assert(!_hitTests.containsKey(event.pointer));
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) {
hitTestResult = _hitTests[event.pointer];
}
//...
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
dispatchEvent(event, hitTestResult);
}
}
这段代码的整体思路是:
如果
event
是PointerDownEvent
等四种event
之一时(我们假设创建的是一个移动端app,这四种event
只考虑PointerDownEvent
的情况),创建一个HitTestResult
对象,并调用hitTest()
方法,然后将hitTestResult
赋值给_hitTests[event.pointer]
;如果是
PointerUpEvent
或PointerCancelEvent
,那么将此hitTestResult
从_hitTests
中移除并返回给hitTestResult
对象;最后执行
dispatchEvent(event, hitTestResult)
;
说完PointerEvent
的封装后,Flutter
是如何处理这些手势事件的呢?如何确定哪些Widget
响应手势事件,并确认它们的优先级呢?接下来我们讲一下hitTest()
的实现,即命中测试。
hitTest()
hitTest()
需要确定出都哪些widget
对应的RenderObject
可能会响应手势事件,并给他们设置命中响应的优先级。这个步骤是非常重要的,可以确定最终响应手势事件的Widget
。在此我们先说个结论,子优先于父响应手势事件。我们先来看看HitTestResult
的结构:
HitTestResult()
: _path = <HitTestEntry>[],
_transforms = <Matrix4>[Matrix4.identity()],
_localTransforms = <_TransformPart>[];
HitTestEntry(this.target);
其中_path
是个HitTestEntry
数组,HitTestResult
每执行一次add()
方法就会添加一个HitTestEntry
对象到_path
中,HitTestResult
的target
对象通常就是一个RenderObject
对象,也就是说_path
是用来记录手势命中的RenderObject
对象数组。_transforms
是记录目标RenderObject
对象相对于Global
坐标系的位置。_localTransforms
是记录目标RenderObject
对象相对于Parent
的位置。我们继续看hitTest(hitTestResult, event.position);
的实现:
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
创建一个HitTestEntry
对象并添加到HitTestResult
中。由于mixin
,RendererBinding
是GesturesBinding
的子类,而RendererBinding
实现了hitTest()
方法,所以我们看看RendererBinding
的hitTest()
的实现:
@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);
}
在调用super.hitTest()
之前,先调用了renderView
的hitTest()
方法,renderView
我们很了解了,就是App
的根RenderObject
:
bool hitTest(HitTestResult result, { required Offset position }) {
if (child != null) {
child!.hitTest(BoxHitTestResult.wrap(result), position: position);
}
result.add(HitTestEntry(this));
return true;
}
首先判断是否有child
,如果是的话需要先执行child
的hitTest()
方法,再将自己封装成一个HitTestEntry
添加到HitTestResult
中。我们来看看child!.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;
}
如果手势的position
在当前RenderObject
的_size
范围里,判断hitTestChildren()
或hitTestSelf()
是不是返回true
,如果是的话将自己封装成BoxHitTestEntry
添加到HitTestResult
中。也就是说只要子或自己命中手势事件,就添加到HitTestResult
。在这里要注意的是,会优先判断hitTestChildren()
,这个方法是判断子是否有命中手势,如果子命中了就不会再走hitTestSelf()
的判断,从而子的优先级较高,会被优先加入到HitTestResult
的_path
队列中。我们看看hitTestChildren()
的实现:
@protected
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) => false;
hitTestChildren()
是个抽象方法,由子类实现。我们举个例子,假设当前Wi