Flutter事件之GestureRecognizer

1,355 阅读8分钟

前言

我们通过前面的知识了解了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);
}

startTrackingPointer中会首先将handleEvent添加到触点路由中,方便在GestureBinding.handleEvent时调用; 然后会通过_addPointerToArena方法创建竞技场并且添加进竞技场,将得到的GestureArenaEntry保存到_entries中;

随后进入到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)的方法,所以退出,

开始竞技

回到GestureBinding.handleEvent里,接下来会执行gestureArena.close(event.pointer);关于手势竞技场,我们知道关闭竞技场意味着不能再有成员进入,并且开始竞技,而当前手势只有单击手势,所以会直接宣布单击手势获胜(关于竞技场的规则这里不再赘述,不了解可以看这里),在单击手势获胜后会进入BaseTapGestureRecognizer.acceptGesture

@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进行onTapUponTap事件回调,随后调用_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()方法,该方法主要回调onLongPressStartonLongPress

定时器的事件也结束,这时候应该抬起手指了!

手指抬起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方法,回调onLongPressEndonLongPressUp,否则(_longPressAccepted=true)宣布竞技失败;

这里回顾一下_longPressAccepted=true的时机,就是定时器被触发时(didExceedDeadline),这是唯一一处设置的地方;

当然最后还是回到GestureBinding.handleEvent打扫竞技场,由于我们之前在按下事件关闭竞技场时候(close),将该竞技场移除了,所以这里并不会做任何事。

gestureArena.sweep(event.pointer);

单一的长按手势就结束了!