今天我们来讲讲GestureDetector的深度剖析,只有了解原理了,才能知道手势冲突如何解决以及如何更灵活的运用手势。
我们先来看看GestureDetector的内部结构
1.GestureDetector只是一个包装类,最终还是由Listener的RenderPointListener执行事件的操作
2.点击事件开始时会首先执行RawGestureDetector的_handlePointDown方法。
//GestureDetector class GestureDetector extends StatelessWidget { Widget build(BuildContext context) { if (onTapDown != null || onTapUp != null || ..... ) { //包装相关的手势类(单击手势) gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers(.......... }, ); } if (onDoubleTap != null || onDoubleTapDown != null || onDoubleTapCancel != null ..... ) { //包装相关的手势类(双击手势) gestures[DoubleTapGestureRecognizer]=GestureRecognizerFactoryWithHandlers(.......... }, ); } ........ return RawGestureDetector( gestures: gestures, behavior: behavior, excludeFromSemantics: excludeFromSemantics, child: child, ); } //RawGestureDetector的state方法 class RawGestureDetectorState extends State { @override void initState() { super.initState(); _semantics = widget.semantics ?? _DefaultSemanticsGestureDelegate(this); _syncAll(widget.gestures); } void _syncAll(Map gestures) { //手势保存 final Map oldRecognizers = _recognizers!; _recognizers = {}; //遍历新手势,过滤旧手势。gestures是从GestureDetector传过来的新手势 for (final Type type in gestures.keys) { //如果是旧的手势(oldRecognizers[type])跟新手势一样,就直接赋值 //如果旧手势跟新手势不一样,则需要调用旧手势的构造函数并执行初始化方法。 _recognizers![type] = oldRecognizers[type] ?? gestures[type]!.constructor(); gestures[type]!.initializer(_recognizers![type]!); } //对旧手势执行dispose()方法进行销毁。 for (final Type type in oldRecognizers.keys) { if (!_recognizers!.containsKey(type)) { oldRecognizers[type]!.dispose(); } } } @override Widget build(BuildContext context) { //RawGestureDetector最终调用的是Listener,Listener才是重点需要关注的。 Widget result = Listener( onPointerDown: _handlePointerDown, onPointerPanZoomStart: _handlePointerPanZoomStart, behavior: widget.behavior ?? _defaultBehavior, child: widget.child, ); return result; } } //Listener的类 class Listener extends SingleChildRenderObjectWidget { @override RenderPointerListener createRenderObject(BuildContext context) { //主要的手势类,重点关注这个RenderPointerListener return RenderPointerListener( onPointerDown: onPointerDown, onPointerMove: onPointerMove, ....... behavior: behavior, ); } } abstract class OneSequenceGestureRecognizer extends GestureRecognizer { //...省略100字 } abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin { //...省略100字 }
在点击事件进来时,事件的传递流程梳理
1.每个传进来的手势都会被打包成GestureRecognizer的相关子类,OneSequenceGestureRecognizer是我们最常用的子类,同时它也是众多子类的父类。
2.打包好的GestureRecognizer相关子类会缓存到RawGestureDetector的gestures变量中以供后面手势加入竞技场和竞争的时候使用到
3.RawGestureDetector每一次的初始化的时候都会对gestures列表进行更新,始终保持最新的缓存
4.这个RenderPointerListener是Listener的renderObject,这个类间接继承了HitTestTarget接口,其实所有的renderObject都实现了HitTestTarget这个接口。顾名思义,单击测试目标。后面手势竞争的时候都会对实现HitTestTarget的实例进行单击测试。
5.GestureRecognizer同时也继承了GestureArenaMember(竞技成员),所以子类也可以看作是竞技成员
下图是GestureRecognizer的部分继承关系
//RenderPointerListener(RenderObject) @override void handleEvent(PointerEvent event, HitTestEntry entry) { assert(debugHandleEvent(event, entry)); if (event is PointerDownEvent) { //这里执行的就是上图的_handlePointDown方法 return onPointerDown?.call(event); } if (event is PointerMoveEvent) { return onPointerMove?.call(event); } if (event is PointerUpEvent) { return onPointerUp?.call(event); } .... }
小结:这一部分主要讲述了手势事件如何封装成GestureRecognizer,并最终走到RenderPointerListener的handleEvent对手势类进行处理进行处理。在此之前我们先看看GestureBinding的初始化监听。对于后面的手势竞争有非常大的帮助。
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget { //创建手势竞技场(负责决出竞技胜者) final GestureArenaManager gestureArena = GestureArenaManager(); //创建目标收集类(负责收集单击测试目标) final Map _hitTests = {}; } //手势竞技场 class GestureArenaManager { //管理不同的成员管理类 final Map _arenas = {}; } //手势竞技成员管理 class _GestureArena { //每个成员管理类都持有一个竞技成员列表,即上面说的GestureRecognizer啦 final List members = []; bool isOpen = true; bool isHeld = false; }
1.我们最初在main类中执行runApp()方法的时候,就开始对GestureBinding。的相关监听进行初始化
2.同时创建手势竞技场GestureArenaManager负责管理不同的竞技场,每个竞技场又管理者不同的GestureArenaMember的成员
3.同时创建了目标收集器_hitTests针对不同的事件类型创建不同的HitTestResult(这就是真正的目标收集器,即收集实现了HitTestTarget接口的RenderObject和GestureBinding)单击测试目标收集器
4.注意,GestureBinding也是实现了HitTestTarget的单击目标测试,同上面所说的RenderObject一样会接受单击目标测试。
5.GestureBinding实现了_handlePointerEventImmediately这个方法,由android或ios监听事件并传入,对事件进行进一步的处理。
手势处理主要分两个阶段
1.目标收集
2.手势竞争
一、目标收集
//GestureBinding的实现,监听底层收到的事件进行相关处理并传输 void _handlePointerEventImmediately(PointerEvent event) { HitTestResult? hitTestResult; if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) { //创建一个目标收集器。一般事件开始的时候都会先走这个,这是第一步。 hitTestResult = HitTestResult(); //单击测试,收集可以响应单机测试的实例 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) { //其他类型的down事件,取出单击测试的结果(通过单击测试的实例) hitTestResult = _hitTests[event.pointer]; } if (hitTestResult != null || event is PointerAddedEvent || event is PointerRemovedEvent) { //分配事件给通过单击测试的实例(hitTestResult) dispatchEvent(event, hitTestResult); } } //RendererBinding @override void hitTestInView(HitTestResult result, Offset position, int viewId) { //在RenderBinding中又调用了renderView的hitTest _viewIdToRenderView[viewId]?.hitTest(result, position: position); //这里最后才会调用GestureBinding的hitTestInView super.hitTestInView(result, position, viewId); } bool hitTest(HitTestResult result, { required Offset position }) { if (child != null) { //这里又开始触发子节点的hitTest child!.hitTest(BoxHitTestResult.wrap(result), position: position); } //最后将自身即根renderview加入单击测试结果 result.add(HitTestEntry(this)); return true; } //子节点的单击测试方法 bool hitTest(BoxHitTestResult result, { required Offset position }) { //这里判断单击的position是否在_size的范围内(就是是否在子节点的范围内) if (_size!.contains(position)) { if (hitTestChildren(result, position: position) || hitTestSelf(position)) { //子节点在此刻通过了单击测试,即在点击范围内,则加入到result中。 result.add(BoxHitTestEntry(this, position)); return true; } } return false; } //开始事件分发 void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { if (hitTestResult == null) { try { //hitTestResult为null说明是可能回调了 //PointerHoverEvent,PointerAddedEvent,PointerRemovedEvent //由路由分发出去 pointerRouter.route(event); } catch (exception, stack) { for (final HitTestEntry entry in hitTestResult.path) { try { //执行所有目标收集到的单击测试实例的handleEvent方法。 entry.target.handleEvent(event.transformed(entry.transform), entry); } catch (exception, stack) { } } } //GestureDetector实现的RenderPointerListener(调用子节点的handleEvent) 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); } //省略一万字.... }
目标收集流程
1.当事件下发到GestureBinding时,会分别对不同事件进行判断处理,其中有手势点击和手势抬起事件
2.当进入PointerDownEvent的时候,先进入Rendview根节点的hitTest方法调用hitTestChildren进行子节点遍历,判断子节点是否符合点击范围,如果符合则将自身加入hitTestResult单击测试列表中
3.当遍历完子节点后再单击测试Rendview根节点,只有hitTestSelf条件符合了才能将自身加入到单击测试列表hitTestResult中。
4.最后一步再将GestureBinding加入到单击测试列表中,因为GestureBinding也实现了HitTestTarget单击测试目标接口。都会进行测试。
5.当单击目标测试完成后,将进行事件分发,分发到各个RendObject(都实现了HitTestTarget方法)中,即调用每个实例的handleEvent方法。
6.handleEvent的第一次点击即PointerDownEvent终会执行RawGestureDetector的_handlePointerDown方法
至此,目标收集流程已经结束,现在开始事件竞争
二、手势竞争
手势收集完成后,就进入到了RawGestureDetector的_handlePointerDown方法
void _handlePointerDown(PointerDownEvent event) { assert(_recognizers != null); //这是对前面我们收集的竞技成员进行遍历访问 for (final GestureRecognizer recognizer in _recognizers!.values) { //给每个竞技成员跟踪点击事件 recognizer.addPointer(event); } } } void addPointer(PointerDownEvent event) { _pointerToKind[event.pointer] = event.kind; if (isPointerAllowed(event)) { addAllowedPointer(event); } else { handleNonAllowedPointer(event); } } } //由子类OneSequenceGestureRecognizer(这个比较常用) void addAllowedPointer(PointerDownEvent event) { //开始跟踪事件处理 startTrackingPointer(event.pointer, event.transform); } } @protected void startTrackingPointer(int pointer, [Matrix4? transform]) { //省略部分代码... _entries[pointer] = _addPointerToArena(pointer); } //在这里开始将当前的GestureArenaMember(就是上面的Recognizer)添加到竞技场gestureArena GestureArenaEntry _addPointerToArena(int pointer) { if (_team != null) { return _team!.add(pointer, this); } return GestureBinding.instance.gestureArena.add(pointer, this); }
1._recognizers最初在初始化的时候已经对不同的事件进行手势封装成GestureRecognizer的不同子类,现在就是开始用的时候了
2.往下走最终调用_addPointerToArena方法,即将GestureRecognizer的子类实现添加到GestureBinding的gestureArena手势竞技场中进行手势竞争。
三、 决出最终胜者
由于目标收集中最后一个加入的是GestureBinding,所以GestureBinding也是实现了HitTestTarget接口
@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); } } //关闭竞技场(只是做个标记) void close(int pointer) { final _GestureArena? state = _arenas[pointer]; if (state == null) { return; // This arena either never existed or has been resolved. } state.isOpen = false; _tryToResolveArena(pointer, state); } //决出竞技场最终的胜者 void _tryToResolveArena(int pointer, _GestureArena state) { if (state.members.length == 1) { //只有一个成员,决出胜者 scheduleMicrotask(() => _resolveByDefault(pointer, state)); } else if (state.members.isEmpty) { //没有成员,则移除当前手势的竞技场 _arenas.remove(pointer); } else if (state.eagerWinner != null) { //只有一个胜者,直接宣布获胜 _resolveInFavorOf(pointer, state, state.eagerWinner!); } } void _resolveByDefault(int pointer, _GestureArena state) { if (!_arenas.containsKey(pointer)) { return; // This arena has already resolved. } final List members = state.members; _arenas.remove(pointer); //取第一个作为胜者 state.members.first.acceptGesture(pointer); }
1.前面子类GestureRecognizer添加进竞技场之后,在GestureBinding开始决出胜者,即可以响应点击事件的成员。
2.在竞技场关闭的时候开始决出胜者,也是事件开始的事件即PointerDownEvent事件,开始决策
3.手势成员只有一个的时候,直接取第一个手势为胜者,手势成员为空的时候,直接清理竞技场,手势成员已经有胜者的时候,直接确定竞技场胜者
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. //直接取第一个成员作为胜者并调用其的acceptGesture state.members.first.acceptGesture(pointer); // 其他的败者则调用rejectGesture for (int i = 1; i
只要竞技场没有被挂起或为空,且此时会有多个手势竞技成员,这时候会取第一个成员作为胜者,就是renderbox最里面的那个。下节重点讲解双击事件以及事件的灵活运用。