看完这篇文章你能学到什么
- flutter事件流分发流程
- flutter事件冲突怎么处理
事件处理-常用widget
- Listener 监听并识别最底层的down ,up,cancel,move等事件
- GestureDetecotor 识别事件,包括点击,长按,双击,拖动,并解决事件之间的冲突
- IgnorePointer 忽略事件,包括它自己
- AbsorbPointe 忽略它孩子的事件
事件分类
对于移动端,可以先不考虑鼠标事件或者其他的悬浮事件,那么在flutter中,事件的分类和Native是一样的,每组事件可分为down..move..up/cancel
事件总入口
//总的初始化入口
GestureBinding在初始化的时候,设置window的onPointDartPackaget回调
ui.window
set onPointerDataPacket(PointerDataPacketCallback callback) {
_onPointerDataPacket = callback;
_onPointerDataPacketZone = Zone.current;
}
//framework层的初始化入口可以认为是在GestureBinding的_handlePointerDataPacket
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked)
_flushPointerEventQueue();
}
事件分发概览
这里有两个比较重要的对象,HitTestResult,HitTestEntry,在flutter中,事件从顶层分发,开始命中测试流程。
什么是命中测试?
深度优先遍历整颗RenderTree,判断当前事件的落点位置是否在RenderObject中,如果在范围内,就表示命中测试通过了,那会把自己以HitTestEntry添加到HitTestResult对象中。注意这里的事件是down事件,至于为什么不是move、up、cancel事件,后面会解释。hitTest流程如下
image.png在histTest完成后,hitTestResult应该如下图
image (1).png
事件遍历分发的入口在GesturesBinding类里
// 遍历
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty)
_handlePointerEvent(_pendingPointerEvents.removeFirst());
}
// 对每个事件做处理,注意这里走到了 hitTest(hitTestResult, event.position);
void _handlePointerEvent(PointerEvent event) {
HitTestResult hitTestResult;
// 这里先只看pointDown事件,其他的事件后面会讲
if (event is PointerDownEvent || event is PointerSignalEvent) {
hitTestResult = HitTestResult();
// 执行测试流程,其实就是看down的落点是否在这个RenderObject的范围内
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
// 保存结果
_hitTests[event.pointer] = hitTestResult;
}
}
}
但是看 GestureBinding的histTest方法,这里发现,这里什么都没做啊,只做了1个add操作
/// Determine which [HitTestTarget] objects are located at a given position.
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
让我们把视线移到RendererBinding里,由于mixin,RendererBinding是GesturesBinding的子类,注意RendererBinding继承并实现了hitTest方法,所以会先执行这里的hitTest. (这部分原理可以参考flutter的入口初始化方法runApp(),以及mixin,这里不做更多的赘述)
RendererBinding
@override
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
// renderView先执行hitTest
renderView.hitTest(result, position: position);
// 然后才是GesturesBinding的hitTest
super.hitTest(result, position);
}
RenderView是RenderObject树的根,所以从这里开始,会遍历整颗RenderTree,执行histTest
RenderView的hitTest
bool hitTest(HitTestResult result, { Offset position }) {
// 这里的child其实是个RenderBox,当child不为空,就执行child的hitTest,接着往下看
if (child != null)
child.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
RenderBox的hitTest
bool hitTest(BoxHitTestResult result, { @required Offset position }) {
// 事件的position必须在当前组件内
if (_size.contains(position)) {
// 优先判断children,再判断自己,只要有一个为true,就把自己加入到result中
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
注意RenderBox的hitTest里的判断顺序,是优先判断孩子再判断自己,也就是意味着如果孩子和自己都符合条件,那孩子是先加入到队列里的,这个顺序很重要,涉及到手势冲突的解决。 后面会细说
再看hitTestChildren,在RenderBox里是空实现,由子类实现,子类分两种,一种是单孩子的,一种是多孩子的,分别挑一个看下源码
@protected
bool hitTestChildren(BoxHitTestResult result, { Offset position }) => false;
这里单孩子以Padding为例,Padding对应的Render是RenderPadding,RenderPadding的父类是RenderShiftedBox,看下RenderShiftedBox的hitTestChildren方法
@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
if (child != null) {
final BoxParentData childParentData = child.parentData as BoxParentData;
// 根据偏移量计算,实际上就是根据offset做个偏移,然后调用RenderBox里的hitTest
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
// 返回是否命中
return child.hitTest(result, position: transformed);
},
);
}
return false;
}
Padding是单child,那常用的多孩子容器Row,Column呢?
Row和Column的父类都是Flex,直接看Flex对应的RenderFlex。
RenderFlex的hitTestChildren方法,接着往下看
@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
return defaultHitTestChildren(result, position: position);
}
RenderFlex with了 RenderBoxContainerDefaultsMixin, RenderBoxContainerDefaultsMixin是个mixin
它的defaultHitTestChildren实现了hitTestChildren的逻辑,由于Flex里可以有多个孩子,所以
会循环hitTest,直到有一个孩子命中了
bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
// The x, y parameters have the top left of the node's box as the origin.
// lastChild是最后一个孩子
ChildType child = lastChild;
while (child != null) {
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;
}
当hitTest返回ture以后,就会把自己加入到HitTestResult中,后续的事件分发就是根据HitTestResult进行的
down事件的分发讲完了,那move、up、cancel事件呢? 接着看GestureBinding
void _handlePointerEvent(PointerEvent event) {
HitTestResult hitTestResult;
// 只有down事件才会走到hitTest
if (event is PointerDownEvent || event is PointerSignalEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
// 如果是up or cancel,从_hitTests里移除,同时会返回当前的result,继续分发这个事件,
// 但同时也代表这次事件流结束了
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
// 比如move事件的时候,手指一直是down的
hitTestResult = _hitTests[event.pointer];
}
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
//分发事件
dispatchEvent(event, hitTestResult);
}
}
可以看到除了down事件,其他事件只要有RenderObject命中,也就是hitTestResult不为空,就会执行事件分发
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
assert(!locked);
// 没有Render命中,先不看这个
if (hitTestResult == null) {
try {
pointerRouter.route(event);
} catch (exception, stack) { }
return;
}
// hitTestResult不为空,循环调用处理事件,entry 里的target是HitTestTarget
// RenderObject 默认有实现HitTestTarget,但是个空实现,只有需要处理事件的render才会实现
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
}
}
}
到这里我们基本上讲完了point事件的核心分发逻辑,并举例了单孩子容器Padding和多孩子容器Flex的事件分发,简单总结下:
- 当设备触发point事件时,参与分发的binding有RenderBinding和GestureBinding,RenderView是RenderTree分发的总入口
- 当事件为point down时,开始执行hitTest()测试,把符合条件的RenderObject加入到hitTestResult中
- 如果是其他事件,就根据point id 取出之前的hitTestResult并分发
- 最后就是遍历hitTestResult,执行 entry.target.handleEvent();
那疑问来了,没看到move或者cancel事件的处理,而且flutter里可以做到像Android 在父组件拦截事件吗?
答案是可以的,但只能在hitTest这一步中做,IgnorePointer和AbsorbPointer就是这个作用。但如果RenderObject已经在hitTestResult中了,就不能再拦截了,因为在分发代码中,只有遍历分发,没有任何其他处理
事件冲突处理概览
事件分发讲完了,那事件冲突如何处理呢?假设有如下代码
Scaffold(
appBar: AppBar(
title: Text("事件竞争"),
),
body: GestureDetector(
onTap: () {
print('tab parent');
},
child: Container(
width: 300,
height: 300,
color: Colors.red, //父亲是红色
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
print('tab child');
},
child: Container(
width: 100,
height: 100,
color: Colors.green, // 孩子是绿色,居中
),
),
),
));
代码运行后截图如下
image (2).png
当点击红色区域时 输出‘tab parent’,当点击绿色区域时输出‘tab child’,这个大家应该都知道,但有没有想过这是怎么做到的?内部的原理是什么?flutter又是如何处理事件冲突的?一个个来。
先说结论:
- 点击绿色区域时‘tab child’的内部原理,上面我们有提到,在down事件触发hitTest的时候,优先对children做hitTest,然后才是对自己做hitTest,也就意味着child先被加入到hitTestResult中,那在事件竞争时,默认的是第一个加入竞技场的事件胜利,这也是flutter能做到 优先响应child事件的做法
- 点击红色区域时,输出‘tab parent’,绿色区域的child不在点击返回,在_hitTest阶段就没有加入到hitResult中,具体分析可以看第一部分
想要了解事件竞争的底层原理,我们先要认识几个成员,这几个成员都在arena.dart
- 手势竞技场 _GestureArena,记录当前所有的参与者
- 手势竞技场成员 GestureArenaMember ,这是个抽象类,实际开发我们更多看到的是GestureRecognizer
- 手势竞技场管理者 GestureArenaManager,这个是竞技场的核心管理类,可以决定哪个事件胜利或失败
- GestureArenaEntry 这个不需要怎么关心,粘合了member和manager
再看下竞技场的实际参与者,每种手势,都有一个对应的识别者,在竞技场中,他们就是一个个成员
- GestureArenaMember 所有竞技场参与者的父类
- GestureRecognizer 同上,GestureArenaMemberd 子类,是个虚拟类,手势识别的基类
- OneSequenceGestureRecognizer 主要是跟踪单个手势,点击,拖动
- PrimaryPointerGestureRecognizer 单个手势跟踪的实现类,例如tab,相比于它的父亲,加了距离的限制(超过一定滑动距离就不能认定为点击了,这个和native一样),所以这个类是长按,单击手势识别器的父类
- BaseTapGestureRecognizer 单击手势的基类
- TapGestureRecognizer 单击手势识别
- LongPressGestureRecognizer 长按
- DragGestureRecognizer 手势拖动识别的基类
- MultiTapGestureRecognizer 多个点的触摸,好像没怎么用到过
- DoubleTapGestureRecognizer 双击
还记得上面有分析过hitTest的过程,最终GestureBingding都会在最后加入到HitTestResult中,在dispatchEvent的时候,都会调用
entry.target.handleEvent(event.transformed(entry.transform), entry);
而GestureBingdin本身也是HitTestTarget,所以我们看GestureBinding的handleEvent方法
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
// 这里是在分发event,但暂时先不看这里,后面会详细说到
pointerRouter.route(event);
// down事件的时候,竞技场关闭了事件注册
if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
// up事件的时候,竞技场开始清扫
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
//离散事件,这个先不管
pointerSignalResolver.resolve(event);
}
}
接着看GestureArena.close方法
为什么是down事件关闭了注册呢?
注意看方法的注释,阻止新的成员进入竞技场,调用时机在分发完down事件之后,还记得事件分发的流程吗?
RenderView作为根节点最先开始分发,在整个RenderTree分发完之后,才会走到GestureBinding的分发,所以
这里的分发是最后执行的,在这里调用gestureArena.close刚刚好
/// Prevents new members from entering the arena.
///
/// Called after the framework has finished dispatching the pointer down event.
void close(int pointer) {
// 根据事件id获取当前竞技场
final _GestureArena state = _arenas[pointer];
if (state == null)
return; // This arena either never existed or has been resolved.
// 标记为false,不再接受新成员了
state.isOpen = false;
assert(_debugLogDiagnostic(pointer, 'Closing', state));
// 查看有没有胜利者
_tryToResolveArena(pointer, state);
}
void _tryToResolveArena(int pointer, _GestureArena state) {
assert(_arenas[pointer] == state);
assert(!state.isOpen);
if (state.members.length == 1) {
// 如果只有一个竞争者,那直接宣布为胜利者,后续的move,up事件都不用走了
scheduleMicrotask(() => _resolveByDefault(pointer, state));
} else if (state.members.isEmpty) {
// 如果没有竞争者,那直接remove
_arenas.remove(pointer);
assert(_debugLogDiagnostic(pointer, 'Arena empty.'));
} else if (state.eagerWinner != null) {
// 是否有激进的竞争者? 这个怎么来的,后续看
assert(_debugLogDiagnostic(pointer, 'Eager winner: ${state.eagerWinner}'));
_resolveInFavorOf(pointer, state, state.eagerWinner);
}
}
// 这个方法的作用就是宣布第一个竞争者胜利
void _resolveByDefault(int pointer, _GestureArena state) {
//还是要检查下竞技场是不是空了
if (!_arenas.containsKey(pointer))
return; // Already resolved earlier.
final List<GestureArenaMember> members = state.members;
// 移除,都胜利了,就不要存在了
_arenas.remove(pointer);
state.members.first.acceptGesture(pointer);
}
// 宣布指定的竞争者胜利
void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {
_arenas.remove(pointer);
// 遍历,除了指定的竞技者外,其他都是失败者,就跟追妹子一样 哈哈哈
for (final GestureArenaMember rejectedMember in state.members) {
if (rejectedMember != member)
rejectedMember.rejectGesture(pointer);
}
// 宣告这个竞技者胜利
member.acceptGesture(pointer);
}
再看GestureArena sweep 清扫方法
void sweep(int pointer) {
final _GestureArena state = _arenas[pointer];
if (state == null)
return; // This arena either never existed or has been resolved.
// 竞技场是可以被延迟的,如果isHeld设置为true,那就延迟清扫,直到管理者调用release
if (state.isHeld) {
state.hasPendingSweep = true;
return; // This arena is being held for a long-lived member.
}
// 默认情况下都是走到这里,清扫掉这个事件
_arenas.remove(pointer);
// 竞技场成员不为空,默认宣布第一个为胜利者
if (state.members.isNotEmpty) {
// First member wins. 宣布第一个为胜利者
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);
}
}
看到这里,GestureBinding关于事件处理的逻辑已经差不多讲完了,代码并不多,可以看到GestureBinding只处理了down和up事件,donw-> 报名,up->清扫,那move事件呢? 怎么任意让一个事件胜利,让一个事件失败? 失败了还能强制胜利吗? 接着往下看
先说结论
- Listener只是事件的接收者,接受并转发基本的down,up,move等事件,不负责处理事件冲突,也不识别点击,滑动,缩放手势,处理以上这些事件还得看GestureDetector,这是flutter内部封装好的类
- GestureDetector内部封装了各种手势的识别器,当调用方注册了某个类型的回调时,比如onTap,那GestureDetector就会让这个类型的识别器参与竞技场的竞争
- 在后面的分析中会看到,move事件是竞技者自己处理的,down和up也会处理,这里回答了第一部分的问题
- 可以让一个事件马上胜利,也可以让事件失败
- 失败了还能再宣告胜利吗? 可以的!
接着看GestureDetector源码,在文件gesture_detector.dart里,这文件比较长,有1253行,只挑重点看,
GestureDetector是个StatelessWidget,所以直接看build方法
- GestureRecognizerFactoryWithHandlers,注册了各种Recognizer(竞争者),比如TapGestureRecognizer,VerticalDragGestureRecognizer等等,具体待会儿一起看下源码
- RawGestureDetector ,GestureDetector的实现类,底层还是Listener,这个很重要
以TapGestureRecognizer为例,详细看下事件分发,响应的流程
- 当手指触发down事件
- 走到GestureBinding的_handlePointerEvent,获得当前renderTree的HitTestResult,具体原理可以看第一部分
- 接着走到GestureBinding的dispatchEvent
for (final HitTestEntry entry in hitTestResult.path) {
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
这里要注意,RenderTree里的target的handleEvent先被执行,然后才执行GestureBinding里的handleEvent,看看RawGestureDetector里接收了哪些事件
@override
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
// 省略部分代码
return result;
}
可以看到RawGestureDetector只接受了down事件,move和up事件稍后解释,继续看_handlePointerDown方法
// RawGestureDetectorState
void _handlePointerDown(PointerDownEvent event) {
for (final GestureRecognizer recognizer in _recognizers.values)
recognizer.addPointer(event);
}
//GestureRecognizer 接着看addPointer
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
//isPointerAllowed比较简单,跳过了
// 正常情况都会走到addAllowedPointer 以TapGestureRecognizer为例看下实现
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
//GestureRecognizer addAllowedPointer
// 是个空实现 ,看子类
void addAllowedPointer(PointerDownEvent event) { }
// PrimaryPointerGestureRecognizer addAllowedPointer
@override
void addAllowedPointer(PointerDownEvent event) {
// 这个方法里注册了事件回调,
startTrackingPointer(event.pointer, event.transform);
if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible;
primaryPointer = event.pointer;
initialPosition = OffsetPair(local: event.localPosition, global: event.position);
if (deadline != null)
_timer = Timer(deadline, () => didExceedDeadlineWithEvent(event));
}
}
在startTrackingPointer里注册了事件的回调
// OneSequenceGestureRecognizer
@protected
void startTrackingPointer(int pointer, [Matrix4 transform]) {
//往 GestureBinding的单例注册了回调,每次有事件分发时,都会走到 第二个入参handleEvent
//这里是不是很熟悉,还记得GestureBinding的handleEvent方法吗。第一句代码就是对pointerRouter的分发
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
_trackedPointers.add(pointer);
assert(!_entries.containsValue(pointer));
_entries[pointer] = _addPointerToArena(pointer);
}
// 子类里自己实现
/// Called when a pointer event is routed to this recognizer.
@protected
void handleEvent(PointerEvent event);
handleEvent在子类PrimaryPointerGestureRecognizer的实现
@override
void handleEvent(PointerEvent event) {
assert(state != GestureRecognizerState.ready);
if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
final bool isPreAcceptSlopPastTolerance =
!_gestureAccepted &&
preAcceptSlopTolerance != null &&
_getGlobalDistance(event) > preAcceptSlopTolerance;
final bool isPostAcceptSlopPastTolerance =
_gestureAccepted &&
postAcceptSlopTolerance != null &&
_getGlobalDistance(event) > postAcceptSlopTolerance;
// 因为识别的是单击事件,所以如果是move,且move的距离超过最小距离,直接判定失败
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer);
} else {
// 通过判定
handlePrimaryPointer(event);
}
}
stopTrackingIfPointerNoLongerDown(event);
}
// 这里是个空实现,再看子类 BaseTapGestureRecognizer
/// Override to provide behavior for the primary pointer when the gesture is still possible.
@protected
void handlePrimaryPointer(PointerEvent event);
BaseTapGestureRecognizer里handlePrimaryPointer的实现
@override
void handlePrimaryPointer(PointerEvent event) {
// 如果是up事件
if (event is PointerUpEvent) {
_up = event;
// 走到这里
_checkUp();
} else if (event is PointerCancelEvent) {
// cancel事件,判定竞争失败
resolve(GestureDisposition.rejected);
if (_sentTapDown) {
_checkCancel(event, '');
}
_reset();
} else if (event.buttons != _down.buttons) {
// 如果和down记录的button不相等,直接判定失败
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer);
}
}
void _checkUp() {
// 如果没有宣布过胜利 或者 _up为空 直接返回
if (!_wonArenaForPrimaryPointer || _up == null) {
return;
}
handleTapUp(down: _down, up: _up);
_reset();
}
down事件看完了,总结下流程
- hitTest先命中HitResult,然后分发
- RawGestureDetector里的Listener第一个分发到事件,执行_handlePointerDown 回调GestureBinding单例里的pointerRouter注册回调,并调用_addPointerToArena把自己加入的手势竞技场
因为是看单击事件识别器TapGestureRecognizer,所以略过move事件,直接看up事件
up事件还是先走到GestureBinding里的分发,handleEvent
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
// 在down事件里注册了回调,最终会回调到OneSequenceGestureRecognizer的handleEvent
// 所以会先执行BaseTapGestureRecognizer里的handlePrimaryPointer-> _checkUp(),但此时
// _wonArenaForPrimaryPointer还是false,所以执行了个寂寞,并没有竞争胜利,返回了
// 接着up事件走到竞技场的清扫方法
pointerRouter.route(event);
if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
// 紧接着走到这里,竞技场开始清扫了,还记得前面说的吗? 没有hold事件,那默认会宣布
// 第一个成员胜利,那就走到GestureArenaMember的acceptGesture
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
BaseTapGestureRecognizer是GestureArenaMember的实现类
// 竞争胜利,妹子终于追到手了
@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
// 回调了GesturetureDetictord onTapDown()
_checkDown();
// 标记胜利啦
_wonArenaForPrimaryPointer = true;
// 回调了GesturetureDetictord onTapUp()
// 回调了GesturetureDetictord onTap()
// 重置状态,回调结束
_checkUp();
}
}
总结下up事件的流程
- 执行分发,先执行route的分发,如果这个竞争者是个激进者,比如在down事件就宣告了胜利,那
此时就会直接回调,onTapUp(),onTap()了,事件结束 - 竞技场执行清扫,宣布第一个竞争者胜利,执行onTapDown(),onTapUp(),onTap(),GestureRecognizer状态重置,事件结束
down和up讲完了,以单击事件为例,事件最终是竞技场管理GestureArenaManager宣告胜利的,那有没有可能竞争者GestureRecognizer自己宣告胜利呢?答案是可以的。
假设有这个场景,一个竖向的ListView, 它的item设置了单机事件,那假设用TapGestureRecognizer和VerticalDragGestureRecognizer,一个是单击手势识别,一个是竖向移动手势识别,理论上应该是down-> move-> move,move超过一定距离VerticalDragGestureRecognizer就宣布胜利了,TapGestureRecognizer竞争失败,item此时不响应点击事件,来看看VerticalDragGestureRecognizer是怎么宣布胜利的
先看下继承关系
-
GestureRecognizer
- OneSequenceGestureRecognizer
- DragGestureRecognizer
- VerticalDragGestureRecognizer 竖向拖动事件识别
- HorizontalDragGestureRecognizer 横向拖动事件识别
- PanGestureRecognizer 竖向和横向拖动事件识别
VerticalDragGestureRecognizer里没什么代码,直接看DragGestureRecognizer
//DragGestureRecognizer handleEvent
@override
void handleEvent(PointerEvent event) {
assert(_state != _DragState.ready);
// 省略部分代码
// 处理move事件
if (event is PointerMoveEvent) {
if (_state == _DragState.accepted) {
// 省略部分代码,这里是通知监听方更新事件位置,说明之前就已经胜利了
} else {
_pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta);
_lastPendingEventTimestamp = event.timeStamp;
_lastTransform = event.transform;
final Offset movedLocally = _getDeltaForDetails(event.localDelta);
final Matrix4 localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform);
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform,
untransformedDelta: movedLocally,
untransformedEndPosition: event.localPosition,
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
if (_hasSufficientGlobalDistanceToAccept)
// 宣告胜利 入参状态是GestureDisposition.accepted
// 接着看OneSequenceGestureRecognizer的resolve
resolve(GestureDisposition.accepted);
}
}
}
// VerticalDragGestureRecognizer 判断移动距离大于最小距离
@override
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;
OneSequenceGestureRecognizer的resolve
/// Resolves this recognizer's participation in each gesture arena with the
/// given disposition.
@protected
@mustCallSuper
void resolve(GestureDisposition disposition) {
final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
_entries.clear();
// 遍历所有竞技场,宣告我胜利了
for (final GestureArenaEntry entry in localEntries)
// 调用GestureArenaEntry的resolve
entry.resolve(disposition);
}
// GestureArenaEntry
class GestureArenaEntry {
// 看这里 ,竞技场
void resolve(GestureDisposition disposition) {
// 走到竞技场管理者的_resolve
_arena._resolve(_pointer, _member, disposition);
}
}
GestureArenaManager的_resolve
// GestureArenaManager的_resolve
void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {
final _GestureArena state = _arenas[pointer];
// 如果没有竞技场了,返回
if (state == null)
return; // This arena has already resolved.
// 如果是宣告失败
if (disposition == GestureDisposition.rejected) {
state.members.remove(member);
// 通知竞争者 你失败了
member.rejectGesture(pointer);
// 这里重新走了一遍_tryToResolveArena,应该是当前竞争者失败了,有可能只剩下
// 一个竞争者了,那就直接宣告那个竞争者胜利
if (!state.isOpen)
_tryToResolveArena(pointer, state);
} else {
// 如果是宣告胜利
if (state.isOpen) {
// 赋值激进者,这里有可能在down的时候就被调用了,move的时候状态肯定是关闭的
state.eagerWinner ??= member;
} else {
// 宣告自己胜利,其他都是渣渣,你们失败了
_resolveInFavorOf(pointer, state, member);
}
}
}
至此,VerticalDragGestureRecognizer的竞争机制已经讲完了,可以看到对于VerticalDragGestureRecognizer来说,在move阶段就宣告胜利了,同时也通知TapGestureRecognizer失败。
后续的move和up事件,交给胜利者处理,对于TapGestureRecognizer来说,注意还是能收到handlevent的分发的,但是内部的state已经变成GestureRecognizerState.defunct(已废止,失败者,没办法),不会再执行handleEvent后续的代码了
那TapGestureRecognizer失败了还能再次宣告胜利吗?(追妹子失败了还能再追吗) 答案是可以的。看代码
//很简单,集成TapGestureRecognizer,重写rejectGesture,大意是
//拒绝了我?不行,我自己同意一遍...
class TapMultipleGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
// 失败不可怕,我自己宣告胜利
acceptGesture(pointer);
}
}
// 然后把TapAgainGestureRecognizer赋值给RawGestureDetector
RawGestureDetector(
child: your child,
gestures: {
TapAgainGestureRecognizer: GestureRecognizerFactoryWithHandlers<
TapAgainGestureRecognizer>(
() => TapAgainGestureRecognizer(),
(TapAgainGestureRecognizer instance) {
instance.onTap = () {
// 失败后宣告胜利的回调
};
instance.onTapDown = (_) {
};
instance.onTapUp = (_) {
};
})
},
);
至此,事件分发、处理流程都讲完了,整体上讲,比Android要简单的多,而且flutter源码能直接调试!
简单总结下
- 通过hitTest确定哪些Render需要消费这次的事件流,histTest只有down事件的时候才会执行
- 事件分发,dispatchEvent,先到RenderTree里的HitTestTarget->handleEvent 也就是Listener。
- 对于down事件的分发,GestureRecognizer竞争者们都为自己设置了事件的回调,并报名竞技场,方便后续竞争(做好了准备追妹子),在报名完毕后,GestureBinding关闭了竞技场的报名
- 对于move事件,个别性子急的比如drag事件竞争者就可以宣告胜利了,当然急切者的在down事件的时候就可以
- 对于up 和 cancel事件,只有胜利者才能享用up,失败者是没机会的,只能享用cancel事件
- 竞争失败还是能再次胜利的,可以自我宣布胜利
- 竞争失败并不会一定直接从竞技场移除,例如drage在move事件就宣布了胜利,但tap还是能收到后续事件的,只不过都不处理了
网友评论