Flutter事件之GestureRecognizer
前言
我们通过前面的知识了解了Listener只能处理比如简单的按下(PointerDownEvent
)、移动(PointerMoveEvent
)、抬起(PointerUpEvent
)、取消(PointerCancelEvent
)等,简单的基本手势;而像长按、缩放、水平移动,还有手势之间的冲突处理等等都需要GestureRecogninzer处理;
RawGestureDetector内部是一个Listener组件,在触发了Listener.handleEvent
回调给RawGestureDetector._handlePointerDown
,_recognizers是一个Map,key是手识识别器类型,而recognizer.addPointer
方法是进入GestureRecognizer的入口,这个地方触发时机是在检测到按下时(PointerDownEvent
)
Map<Type, GestureRecognizer>? _recognizers = const <Type, GestureRecognizer>{};
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (final GestureRecognizer recognizer in _recognizers!.values)
recognizer.addPointer(event);
}
接下来从单一的单击手势(TapGestureRecognizer)和单一的长按手势(LongPressGestureRecognizer)以及两者简单的手势竞技开始了解手势识别器的处理机制;
TapGestureRecognizer
手指按下PointerDownEvent
在手指按下时,单击手势识别器的主要做两件事
- 将自身的
handleEvent
注册到触点路由中(GestureBinding.pointerRouter
) - 创建竞技场并加入竞技场
- 关闭竞技场开始竞技
isPointerAllowed 校验触点
该方法只是对触点类型的校验,规则是:_supportedDevices
(该属性可从外部传入)为空、或者包含该触点类型;校验通过则进行下一步
@protected
bool isPointerAllowed(PointerDownEvent event) {
// Currently, it only checks for device kind. But in the future we could check
// for other things e.g. mouse button.
return _supportedDevices == null || _supportedDevices!.contains(event.kind);
}
addAllowedPointer 注册触点
OneSequenceGestureRecognizer.addAllowedPointer
GestureRecognizer只提供了一个模板方法,需要子类实现;TapGestureRecognizer也没有实现该方法,经过层层调用会先进入OneSequenceGestureRecognizer.addAllowedPointer
## OneSequenceGestureRecognizer ##
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);
}
随后进入到PrimaryPointerGestureRecognizer.addAllowedPointer
PrimaryPointerGestureRecognizer.addAllowedPointer
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
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));
}
}
该方法会记录一些触点行为,和单击手势有关的只有_primaryPointer,_timer则是用来给长按手势用的。_initialPosition则是用来拖拽手势使用。
单击手势的addAllowedPointer(注册触点)作用结束;
此时,回到GestureBinding.handleEvent
方法中
handleEvent
注意此时手指并未抬起
## GestureBinding ##
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
首先会通过pointerRouter.route(event);
调用单击手势识别器的handleEvent
方法,TapGestureRecoginzer并没有实现该方法,通过调用最后会进入PrimaryPointerGestureRecognizer.handleEvent
PrimaryPointerGestureRecognizer.handleEvent
该方法会对触点为止进行校验,这里的校验规则是如果按下后移动距离超过preAcceptSlopTolerance(在这里是18逻辑像素),就会宣布当前单击手势竞技失败,随后调用BaseTapGestureRecognizer.rejectGesture
方法回调onTapCancel
方法,还有很重要的一步stopTrackingPointer
,该方法会移除触点路由pointerRouter的对应的handleEvent
@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!;
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer!);
} else {
handlePrimaryPointer(event);
}
}
stopTrackingIfPointerNoLongerDown(event);
}
如果触点校验成功,会进入到BaseTapGestureRecognizer.handlePrimaryPointer
方法中,
BaseTapGestureRecognizer.handlePrimaryPointer
## BaseTapGestureRecognizer ##
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
_up = event;
_checkUp();
} else if (event is PointerCancelEvent) {
resolve(GestureDisposition.rejected);
if (_sentTapDown) {
_checkCancel(event, '');
}
_reset();
} else if (event.buttons != _down!.buttons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer!);
}
}
可以看到并没有处理按下事件(PointerDownEvent)的方法,所以退出,
开始竞技
@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown();
_wonArenaForPrimaryPointer = true;
_checkUp();
}
}
这个方法很简单,调用_checkDown()
回调onTapDown
,将_wonArenaForPrimaryPointer标记为true,这里要注意一点,在按下手指不抬起时,虽然调用了_checkUp()
,但是该方法有限制,会判断_wonArenaForPrimaryPointer是否为ture
void _checkUp() {
if (!_wonArenaForPrimaryPointer || _up == null) {
return;
}
assert(_up!.pointer == _down!.pointer);
handleTapUp(down: _down!, up: _up!);
_reset();
}
此时按下手势结束,开始抬起手势(PointerUpEvent);
手指抬起PointerUpEvent
hanleEvent
在手指按下时,已经将该触点的handleEvent
添加到触点路由,所以此时会直接触发手势识别器的hanleEvent
方法,这里会进入BaseTapGestureRecognizer.handlePrimaryPointer
## BaseTapGestureRecognizer ##
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent) {
_up = event;
_checkUp();
}
...
}
很明显了,会直接执行_checkUp()
,由于我们之前在手指按下时已经将_wonArenaForPrimaryPointer
标记为true,所以会执行handleTapUp
进行onTapUp
和onTap
事件回调,随后调用_reset()
重置
void _checkUp() {
if (!_wonArenaForPrimaryPointer || _up == null) {
return;
}
assert(_up!.pointer == _down!.pointer);
handleTapUp(down: _down!, up: _up!);
_reset();
}
void _reset() {
_sentTapDown = false;
_wonArenaForPrimaryPointer = false;
_up = null;
_down = null;
}
打扫竞技场
上面执行完后,再回到GestureBinding.handleEvent
中,最后是对竞技场的打扫,gestureArena.sweep(event.pointer);
@override // from HitTestTar。get
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
这样单一单击手势的流程就结束了。
LongPressGestureRecognizer
手指按下PointerDownEvent
addAllowedPointer 注册触点
长按手势和单击手势都是继承自PrimaryPointerGestureRecognizer,也就是说都会在addAllowedPointer
方法里将handEvent
注册给pointerRouter,并且创建和加入竞技场;
不同的是长按手势在手指按下时,会启动一个定时器,deadline外部可传入,默认是500ms,也就是说,按下时间超过500ms就会触发定时器的方法didExceedDeadlineWithEvent
,最后会执行到didExceedDeadline
,计时器的触发后的处理下面再说。这里的主要作用和单击事件一样注册触点;
## PrimaryPointerGestureRecognizer ##
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
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));
}
}
PrimaryPointerGestureRecognizer继承自OneSequenceGestureRecognizer,所以super.addAllowedPointer(event);
会调用OneSequenceGestureRecognizer的方法进行注册;
我在Flutter2.0.6版本上发现OneSequenceGestureRecognizer并没有addAllowedPointer这个方法,而在2.10.3的版本上OneSequenceGestureRecognizer新增了addAllowedPointer这个方法,主要作用就是跟踪注册路由到pointerRouter,本文的版本是2.10.3,这里要注意一下;
这样该手势的handleEvent
也会注册到GestureBinding.pointerRouter
,这里就结束了,
handleEvent
随后执行GestureBinding.handleEvent
,进行手势识别器的hanleEvent调用;
这里要注意,当前GestureBinding.handleEvent
的调用时机是在手指按下那一瞬间,而非等待长按手势结束,所以在手指按下时就会调用GestureBinding.handleEvent
,在这里进行handleEvent;
在手指按下时在GestureBinding.handleEvent
方法中执行长按手势识别器的handleEvent
方法,前面说了因为单击和长按都是继承自PrimaryPointerGestureRecognizer,所以进行18逻辑像素校验后进入LongPressGestureRecognizer.handlePrimaryPointer
## LongPressGestureRecognizer ##
@override
void handlePrimaryPointer(PointerEvent event) {
...
} else if (event is PointerDownEvent) {
// The first touch.
_longPressOrigin = OffsetPair.fromEventPosition(event);
_initialButtons = event.buttons;
_checkLongPressDown(event);
} else if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer!);
} else if (_longPressAccepted) {
_checkLongPressMoveUpdate(event);
}
}
}
为了方便查看,省略一些无关按下事件的代码
当前事件是PointerDownEvent,会用_longPressOrigin记录当前的坐标,用_initialButtons记录当前触摸设备类型,然后调用_checkLongPressDown()
进行onLongPressDown
回调,注意这里会把_longPressOrigin记录的坐标回调回去。
另外长按手势是可以移动的,从代码我们可以看出,触发条件是长按手势获胜后,也就是_longPressAccepted=true
,会调用_checkLongPressMoveUpdate(event)
进行onLongPressMoveUpdate
回调,它与拖拽手势(DragGestureRecognizer)的触发区别就是长按手势是否获胜;
然后在GestureBinding.handleEvent
中关闭竞技场进行竞技,
gestureArena.close(event.pointer)
这里特别说明一下,此时的长按手势并未被裁决,我们知道竞技场的close
方法会调用_tryToResolveArena
尝试裁决,因为现在竞技场只有一个长按手势成员,所以会调用_resolveByDefault
来宣布长按手势获胜,并且回调到LongPressGestureRecognizer.acceptGesture
中,但是这个方法是空实现,所以长按手势并不会在此裁决,手指按下的事件结束!
注意:竞技场裁决成功后,竞技场管理者会将该竞技场移除,所以这里的竞技场也没了。(这里说的只有一个竞技成员时)
@override
void acceptGesture(int pointer) {
// Winning the arena isn't important here since it may happen from a sweep.
// Explicitly exceeding the deadline puts the gesture in accepted state.
}
定时器事件被触发
在注册触点时会启动一个500ms的定时器,该定时器的作用主要是用于检测长按手势的触发时长,也就是说必须长按500ms才能触发该手势,定时器触发后,,该方法在PrimaryPointerGestureRecognizer只有一个断言,是在LongPressGestureRecognizer实现的具体逻辑
## LongPressGestureRecognizer ##
@override
void didExceedDeadline() {
// Exceeding the deadline puts the gesture in the accepted state.
resolve(GestureDisposition.accepted);
_longPressAccepted = true;
super.acceptGesture(primaryPointer!);
_checkLongPressStart();
}
可以看到在resolve(GestureDisposition.accepted);
中,会宣布该手势竞技获胜,由于之前在close
时将竞技场移除了,所以这里不用关心这个了;
继续回到didExceedDeadline
执行,会将_longPressAccepted置为true,然后调用父类的PrimaryPointerGestureRecognizer.acceptGesture
方法
## PrimaryPointerGestureRecognizer ##
@override
void acceptGesture(int pointer) {
if (pointer == primaryPointer) {
_stopTimer();
_gestureAccepted = true;
}
}
在这里会停止计时器,并且将_gestureAccepted
属性置为true(这个只是在触点位移校验用);
最后在LongPressGestureRecognizer.didExceedDeadline
中执行_checkLongPressStart()
方法,该方法主要回调onLongPressStart
和onLongPress
;
定时器的事件也结束,这时候应该抬起手指了!
手指抬起PointerUpEvent
因为在手指按下和定时器触发时已经将所有任务完成了,手指抬起剩下的工作就只是回调长按手势的一些方法;
和单击手势一样,手指抬起时再次执行PrimaryPointerGestureRecognizer.handleEvent
,
最后调用到LongPressGestureRecognizer.handlePrimaryPointer
@override
void handlePrimaryPointer(PointerEvent event) {
...
if (event is PointerUpEvent) {
if (_longPressAccepted == true) {
_checkLongPressEnd(event);
} else {
// Pointer is lifted before timeout.
resolve(GestureDisposition.rejected);
}
_reset();
}
...
}
如果之前长按手势竞技获胜了(_longPressAccepted=true),执行_checkLongPressEnd
方法,回调onLongPressEnd
和onLongPressUp
,否则(_longPressAccepted=true)宣布竞技失败;
这里回顾一下_longPressAccepted=true
的时机,就是定时器被触发时(didExceedDeadline
),这是唯一一处设置的地方;
当然最后还是回到GestureBinding.handleEvent
打扫竞技场,由于我们之前在按下事件关闭竞技场时候(close
),将该竞技场移除了,所以这里并不会做任何事。
gestureArena.sweep(event.pointer);
单一的长按手势就结束了!
转载自:https://juejin.cn/post/7087970234224607268