本文字数: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


最低0.47元/天 解锁文章
823

被折叠的 条评论
为什么被折叠?



