Flutter刨根问底——点击事件(下)

前言

本文将会从源码角度分析Flutter中手势竞争即手势竞技场的流程设计,并且分析一些常用GuestureRecognizer对于事件的处理及在竞技场中的取胜和弃权策略来了解Flutter的手势响应原理。

GestureArena

本部分介绍的是手势竞技场,竞技流程及比赛规则

以上类图是竞技场的类图

虽然看起来很复杂,其实主要的就是GestureArenaManager_GestureArena 以及GestureArenaMember,下面将会梳理下它们之间的关系。

  1. GestureArenaManager 是GestureArena的管理类,在GestureBinding中创建,其中的arenas是一个pointer与GestureArena的Map,每一个pointer都有一个GestureArena,当收到PointerDown事件后,相关Gesture会调用GestureBinding.instance.gestureArena.add方法,若manager中无pointer对应的GestureArena,则创建并将自身add到GestureArena中的members中。
  2. _GestureArena就是竞技场类,其实就是一个记录类,记录竞技场的成员以及竞技场的状态。
  3. GestureArenaMemeber是竞技场中的成员,一般为GestureRecognizer,需要实现acceptGesture以及rejectGesture方法,由manager来告知其胜出与否,胜出调用acceptGesture,否则调用rejectGesture。
  4. GestureArenaEntry是一个入口类,每个member和pointer都会生成一个,调用GestureBinding.instance.gestureArena.add方法时创建,由member持有,主要用于调用entry中的resolve进行主动地acceptGesture或rejectGesture。

流程

以上简单介绍了GestureArena的主要角色之间的关系,接下来从源码的角度分析其设计

从上篇中我们知道,一次手势的开端是PointerDownEvent,在此之前没有route(但是有GlobalRoute),Down事件由Listener来处理,更加具体的,在现有组件中,是由GestureDetector里的recognizors来处理,比如我们使用的InkWell,Scrollable其实都是使用的GestureDetector。GuestureDetector中包含丰富的手势回调,onTap设置点击回调,onDoubleTap设置双击, onLongPress设置长按等。因此我们先从GestureDetector入手,了解整个手势的流程,再根据flutter的其他例子加深了解。

1. GestureDetector

dart 复制代码
// 为了防止大家太长不看,进行了简化
Widget build(BuildContext context) {
  final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
  final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;

  if (...
  ) {
    // 点击
    gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
      () => TapGestureRecognizer(debugOwner: this),
      (TapGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 双击
  if (onDoubleTap != null) {
    gestures[DoubleTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
      () => DoubleTapGestureRecognizer(debugOwner: this),
      (DoubleTapGestureRecognizer instance) {
        instance
        ...
      },
    );
  }
  // 长按
  if (...) {
    gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
      () => LongPressGestureRecognizer(debugOwner: this),
      (LongPressGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 滑动,垂直方向
  if (...) {
    gestures[VerticalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
      () => VerticalDragGestureRecognizer(debugOwner: this),
      (VerticalDragGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 滑动,水平方向
  if (...) {
    gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
      () => HorizontalDragGestureRecognizer(debugOwner: this),
      (HorizontalDragGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 拖拽移动,全方向
  if (...) {
    gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
      () => PanGestureRecognizer(debugOwner: this),
      (PanGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 双指缩放
  if (...) {
    gestures[ScaleGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ScaleGestureRecognizer>(
      () => ScaleGestureRecognizer(debugOwner: this),
      (ScaleGestureRecognizer instance) {
        instance
          ...
      },
    );
  }
  // 按压力度
  if (...) {
    gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
      () => ForcePressGestureRecognizer(debugOwner: this),
      (ForcePressGestureRecognizer instance) {
        instance
          ...
      },
    );
  }

  return RawGestureDetector(
    gestures: gestures,
    behavior: behavior,
    excludeFromSemantics: excludeFromSemantics,
    child: child,
  );
}

GestureDetector几乎涵盖了我们用到的所有的手势,如果设置了相关手势回调,就会将其添加到gestures中,gestures是一个Map<Type,GestureRecognizerFactory>,Type是recognizer的类型,factory用于创建recognizer的instance以及instance的初始化。

2. RawGestureDetector

RawGestureDetector是一个StatefulWidget,我们直接看State中的build方法

php 复制代码
Widget build(BuildContext context) {
  Widget result = Listener(
    onPointerDown: _handlePointerDown,
    onPointerPanZoomStart: _handlePointerPanZoomStart,
    behavior: widget.behavior ?? _defaultBehavior,
    child: widget.child,
  );
  if (!widget.excludeFromSemantics) {
    result = _GestureSemantics(
      behavior: widget.behavior ?? _defaultBehavior,
      assignSemantics: _updateSemanticsForRenderObject,
      child: result,
    );
  }
  return result;
}

看到了熟悉的Listener,以及onPointerDown的回调(上篇已介绍)。接下来看看**_handlePointerDown**这个方法。

scss 复制代码
void _handlePointerDown(PointerDownEvent event) {
  assert(_recognizers != null);
  for (final GestureRecognizer recognizer in _recognizers!.values) {
    recognizer.addPointer(event);
  }
}

调用每一个需要处理回调的recoginer的addPointer方法,传入PointerDownEvent。

3. GestureRecognizer

csharp 复制代码
void addPointer(PointerDownEvent event) {
  // kind: 事件来源,比如手指点击、鼠标、触控笔、触摸板等。
  _pointerToKind[event.pointer] = event.kind;
  // 是否处理pointer
  if (isPointerAllowed(event)) {
    // 接受pointer
    addAllowedPointer(event);
  } else {
    // 拒绝pointer
    handleNonAllowedPointer(event);
  }
}

addPointer ,从名字可以看出,是添加pointer。虽然传入的是event,但是需要注意的是该event为PointerDownEvent ,是一个pointer的开始事件,所以主要的关注点我们应放在pointer上,即整个pointer对应的event流上,而不是局限于单次的PointerDownEvent,因为通过接下来对该函数中三个调用的方法的分析,会发现其重要的作用是在需要处理该pointer时,添加该pointer的route 以及将自身注册到该pointer的arena,对于down事件的处理只是顺带的。

接下来我们以OneSequenceGestureRecognizer中的addAllowedPointer为例,大多数手势都是继承自该类

isPointerAllowed

主要根据是否设置了相应的回调以及是否是支持的kind来确定是否添加pointer,我们以TapGestureRecognizor为例

dart 复制代码
bool isPointerAllowed(PointerDownEvent event) {
  switch (event.buttons) {
    case kPrimaryButton:
      if (onTapDown == null &&
          onTap == null &&
          onTapUp == null &&
          onTapCancel == null) {
        return false;
      }
      break;
    case kSecondaryButton:
      if (onSecondaryTap == null &&
          onSecondaryTapDown == null &&
          onSecondaryTapUp == null &&
          onSecondaryTapCancel == null) {
        return false;
      }
      break;
    case kTertiaryButton:
      if (onTertiaryTapDown == null &&
          onTertiaryTapUp == null &&
          onTertiaryTapCancel == null) {
        return false;
      }
      break;
    default:
      return false;
  }
  return super.isPointerAllowed(event);
}

文中的buttons是什么?结合前面的kind来看,如果是touch,即我们的手指点击,那么buttons只有kPrimaryButton;如果是鼠标输入,因为鼠标有左右键以及中间键,所以有kPrimaryButton、kSecondaryButton、kTertiaryButton三个,分别表示左键,右键,中间键。对于手机来说,我们只考虑touch的输入即可。

addAllowedPointer
dart 复制代码
void addAllowedPointer(PointerDownEvent event) {
  startTrackingPointer(event.pointer, event.transform);
}

void startTrackingPointer(int pointer, [Matrix4? transform]) {
  // 添加route
  GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
  _trackedPointers.add(pointer);
  assert(!_entries.containsValue(pointer));
  // 加入arena
  _entries[pointer] = _addPointerToArena(pointer);
}

GestureArenaEntry _addPointerToArena(int pointer) {
  if (_team != null) {
    return _team!.add(pointer, this);
  }
  return GestureBinding.instance.gestureArena.add(pointer, this);
}

startTrackingPointer 中看到了addRoute,这个在GestureBinding中dispatch时会调用route,从而触发handleEvent,将事件交由recognizer处理。添加至竞技场也是在此时,通过调用GestureBinding.instance.gestureArena.add添加。

其中route的回调handleEvent在不同的recognizer中的实现并不相同,接下来看几个例子。

1. PrimaryPointerGestureRecognizer
dart 复制代码
void handleEvent(PointerEvent event) {
  if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
    // 可容忍的移动距离,大于时将会判断为滚动,将会reject
    // 否则将会调用handlePromaryPointer进行后续的事件处理。
    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);
}
/// stopTrackingPointer会remove route
void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
  if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
    stopTrackingPointer(event.pointer);
  }
}

PrimaryPointerGestureRecognizer是一个抽象类,其子类为TapGestureRecognizer和LongPressGestureRecognizer,类如其名,就是用于处理单手势的。其中的state是什么?有三种:

  1. GestureRecognizerState.ready:默认状态,等待手势
  2. GestureRecognizerState.possible:处理中状态,可能会处理手势
  3. GestureRecognizerState.defunct:停止状态,拒绝手势

状态机转换:

2. ScaleGestureRecognizer

这个手势是双指缩放手势,从其对handleEvent的处理,我们可以与前者的PrimaryPointerGestureRecognizer对比一下,了解下差异,以加深对事件处理流程的理解。

dart 复制代码
void handleEvent(PointerEvent event) {
  // 所有的recognizer中handleEvent处理事件时,state都不应该是ready
  assert(_state != _ScaleState.ready);
  // 手势是否改变,收到了DOWN或UP事件,pointer队列改变
  bool didChangeConfiguration = false;
  // 是否应该从accepted转变为started状态,具体看下图状态机
  bool shouldStartIfAccepted = false;
  if (event is PointerMoveEvent) {
    // 速度Tracker,可以计算move的速度
    final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
    if (!event.synthesized) {
      // 记录新的position以及时间戳
      tracker.addPosition(event.timeStamp, event.position);
    }
    // 更新pointer的position
    _pointerLocations[event.pointer] = event.position;
    shouldStartIfAccepted = true;
    _lastTransform = event.transform;
  } else if (event is PointerDownEvent) {
    // 更新pointer的position
    _pointerLocations[event.pointer] = event.position;
    // 如果是DOWN事件,添加pointer
    _pointerQueue.add(event.pointer);
    didChangeConfiguration = true;
    shouldStartIfAccepted = true;
    _lastTransform = event.transform;
  } else if (event is PointerUpEvent || event is PointerCancelEvent) {
    // 清除记录的信息
    _pointerLocations.remove(event.pointer);
    _pointerQueue.remove(event.pointer);
    didChangeConfiguration = true;
    _lastTransform = event.transform;
  } else if (event is PointerPanZoomStartEvent) {
    // PanZoom为鼠标的缩放事件,这里不再介绍
    assert(_pointerPanZooms[event.pointer] == null);
    _pointerPanZooms[event.pointer] = _PointerPanZoomData(
      focalPoint: event.position,
      scale: 1,
      rotation: 0
    );
    didChangeConfiguration = true;
    shouldStartIfAccepted = true;
  } else if (event is PointerPanZoomUpdateEvent) {
    assert(_pointerPanZooms[event.pointer] != null);
    if (!event.synthesized) {
      _velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
    }
    _pointerPanZooms[event.pointer] = _PointerPanZoomData(
      focalPoint: event.position + event.pan,
      scale: event.scale,
      rotation: event.rotation
    );
    _lastTransform = event.transform;
    shouldStartIfAccepted = true;
  } else if (event is PointerPanZoomEndEvent) {
    assert(_pointerPanZooms[event.pointer] != null);
    _pointerPanZooms.remove(event.pointer);
    didChangeConfiguration = true;
  }

  _updateLines();
  _update();

  if (!didChangeConfiguration || _reconfigure(event.pointer)) {
    _advanceStateMachine(shouldStartIfAccepted, event.kind);
  }
  stopTrackingIfPointerNoLongerDown(event);
}

其中的处理比较复杂,很多字段一眼看去也不知道其作用是什么,不过我们可以从分析状态机出发。

_ScaleState有四种:

  • ready : 默认状态,等待手势
  • possible : 处理中状态,可能会处理手势
  • accepted : 已接受状态,目前的一系列手势已被接受为scale手势
  • started : 开始状态,初始状态已确定

状态机如下:

ready与possible很好理解,started与accepted有什么区别呢?accepted用于表示已经接受手势,但是还没有开始处理,此处的处理为生成scale数据,并不是指处理手势;而started表示已经开始可以产生scale数据,因为产生scale数据需要手势的初始位置信息,所以在pointer变更时,initial数据失效,需要重新生成,因此先变为accepted状态。简单的区别就是,在此次event之前有无正确的initial信息。

整个ScaleGestureRecognizer就是在DOWN或UP时生成initial信息,之后MOVE时把此时的信息与initial信息作比生成scaleFactor,line的作用是计算旋转角度,具体的处理这里不再详细介绍,有兴趣的同学可以自行扒下源码。

handleEvent简单总结

从以上两个例子我们可以看出来,handleEvent是recognizer处理事件的核心,根据事件在各种状态间转换,其实广义地说就是三个状态:等待、接收、接受。收到DONW事件,选择处理将进入接收状态,后续该pointer的事件都会接收,根据手势自己的策略,在此之后判断是否接受,若接受,将会调用resolve选择在竞技场中胜出,否则调用resolve选择拒绝。

handleNonAllowedPointer

这个其实没什么可介绍的,就是在所有已接收pointer的竞技场中宣布失败,或者什么都不做。

4. GestureArena

手势竞技场的整体策略其实很简单,相信同学们跟着扒扒源码都能理解。

4.1 GestureArenaManager

先看下manager中的方法,arena是由manager进行管理,需要提前明确一下多指操作下的resolve处理方式,resolve会调用该手势所有的entry的resolve方法,接下来看下这几个主要方法:

Add
dart 复制代码
GestureArenaEntry add(int pointer, GestureArenaMember member) {
  final _GestureArena state = _arenas.putIfAbsent(pointer, () {
    assert(_debugLogDiagnostic(pointer, '★ Opening new gesture arena.'));
    return _GestureArena();
  });
  state.add(member);
  assert(_debugLogDiagnostic(pointer, 'Adding: $member'));
  return GestureArenaEntry._(this, pointer, member);
}

这个是add方法,我们前面在介绍recognizer时有调用过这个方法。这个是创建一个pointer对应的arena,并将member添加到这个arena中,并返回一个member与pointer对应的entry,这个entry可以说就是为了让member调用manager的resolve方法的,之所以需要一个entry,是为了权限管理,防止胡乱调用resolve,导致整个手势的混乱😊。

sweep
dart 复制代码
void sweep(int pointer) {
  final _GestureArena? state = _arenas[pointer];
  if (state == null) {
    return; // This arena either never existed or has been resolved.
  }
  assert(!state.isOpen);
  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);
    }
  }
}

顾名思义,打扫竞技场,如果竞技场中有成员的话,强制产生一个胜者。这个主要是在UP事件时调用,以及UP事件时被hold,推迟了sweep,但在之后的release中也会sweep,总之,就是在收到UP事件之后,一定会sweep,虽然会迟到,但永远不会缺席😉。

close
dart 复制代码
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;
  assert(_debugLogDiagnostic(pointer, 'Closing', state));
  _tryToResolveArena(pointer, state);
}

关闭竞技场,防止进入新的成员。在GestureBinding中收到DOWN事件后就会close,为什么呢?因为GestureBinding是最后加进去的,此时所有的HitTarget都已经加进去了,并且所有可能的手势都已经在addAllowedPointer时加了进去,此时关闭竞技场是合理的,之所以要关闭,是为了防止一个pointer的竞技场产生多个获胜者,试想一下,此时pointer对应的竞技场已经产生了胜者,将其从manager中移除了,之后新的成员加入仍然会添加该pointer的竞技场,虽然一个竞技场只有一个胜者,但是因为创建了多个竞技场,就会导致产生多个胜者。

resolve
dart 复制代码
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);
    // 已经关闭,在移除成员后,尝试下能不能选出胜者
    if (!state.isOpen) {
      _tryToResolveArena(pointer, state);
    }
  } else {
    // 因为还没有关闭,此时不能选出胜者,需要给后来的人一个机会
    if (state.isOpen) {
      state.eagerWinner ??= member;
    } else {
      _resolveInFavorOf(pointer, state, member);
    }
  }
}

/// 尝试完成竞技场
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<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);
}

选出胜者。在竞技场还未关闭时,不能选出胜者,在竞技场关闭后,也即所有成员都已登记,开始竞争😡!这时候有一个人说,让我获胜吧,你们这些小卡乐咪,这时候会调用到**_resolveinfavorOf**,然后获胜;如果有人选择了放弃,这时候会看下竞技场还有没有人,如果只剩一个,那么剩下的那个人将获胜;如果还剩下不只一个,这时候会选择之前宣布获胜的人,先来后到嘛(此场景几乎不会进入,因为在close时eaegrWinner就已经被真正选为胜者);在有人宣布获胜时,宣布者将作为胜者。

hold
dart 复制代码
void hold(int pointer) {
  final _GestureArena? state = _arenas[pointer];
  if (state == null) {
    return; // This arena either never existed or has been resolved.
  }
  state.isHeld = true;
  assert(_debugLogDiagnostic(pointer, 'Holding', state));
}

保持竞技场,防止在sweep时被回收,在多次点击操作中出现。

release
ini 复制代码
void release(int pointer) {
  final _GestureArena? state = _arenas[pointer];
  if (state == null) {
    return; // This arena either never existed or has been resolved.
  }
  state.isHeld = false;
  if (state.hasPendingSweep) {
    sweep(pointer);
  }
}

释放竞技场,与hold成对出现,在多次点击操作中出现。

4.2 Summary

先简单总结一下从DOWN事件开始,手势与竞技场的整个流程。

  • 从DOWN事件开始,每个recognizer根据event的kind以及buttons来决定自己是否处理event,若需要处理,则会startTrackingPointer,在此方法中会添加自己的handleEvent作为route的回调添加到该pointer的pointerRoute中,该pointer的所有事件都会在GestureBinding的handleEvent中route到每个recognizer中;同意处理的同时,也会将自身添加到该pointer的arena中。
  • UP之前的事件,recognizer根据自身的策略,比如MOVE事件,CANCEL事件来决定自身是否中止处理,同时也会将自身移出arena,一般的拒绝策略都是超时、距离过远等。
  • UP事件标志着一个pointer的结束,此时,一般会调用sweep清理竞技场,并且会选出一个胜者,有些recognizer如DoubleTapGestureRecognizer会阻碍竞技场的关闭,调用hold,防止竞技场sweep。
  • 竞技场中手势的竞争其实并不是以策略保证公平,而是类似于一种君子协定,你先声明你accepted,那么你就是胜者,比如EagerGestureRecognier永远都会获胜。

Quiz

经过源码的历练,我相信以下问题对各位来说都不在话下。

1. 竞技场可以有多个胜者吗?

答案是不能,同时也因为这个原因,我们在嵌套widget中若分别在parent以及child中设置点击事件,只会有一个能够响应手势。

2. LongPress,DoubleTap,Tap这三个手势之间是怎么在竞技场中竞争的?

根据前面所学习的,竞争的说法其实并不十分准确,其实整个过程是各个手势按照自身的策略在event回调中声明accepted以及rejected以确定胜者,若无胜者最后竞技场sweep时选第一个手势作为结束。那么各自的策略有什么不同呢?

  • 假如我们想让Tap获胜,我们点了一下并且没有长按,因为没有达到最小长按事件500ms,LongPress不会选择accepted,则在处理Up事件时,将会选择reject;DoubleTap则在UP时hold竞技场,阻止sweep,等待第二次点击,在等待超时后(300ms),选择reject,此时release竞技场,进行sweep,此时竞技场中只剩下Tap事件,Tap获胜。
  • 可以再次印证,所有的手势只关心event,time,position,这三者作为输入,手势按照自身策略做出不同的决策:Tap手势不争不抢,不会主动accept,只有在move超过一定距离之后才会reject;DoubleTap需要有两次tap事件,并且两次事件之间的间隔合法,距离合法,才会accept,否则会reject;LongPress需要tap的down与up之间的时间超过阈值才会accept。

3. 怎么实现一个可以在parent以及child中都可以响应回调的手势?

可以使用一个单例维护一个pointer与List<void Function()>的map,在自定义tap手势中,down时将onTap添加到pointer对应的list中,,获胜时调用pointer对应回调列表的所有回调。

4. 自定义手势需要注意什么?

若需要的是在不影响现有手势的条件下自定义一个手势,最简单的做法就是以自定义策略决定是否reject,但不主动accept。但是这样不能获胜怎么办?答案是听天由命,这种注意下add到竞技场的顺序。选择这种做法本身就是将手势自身的主动性降到最低,若想能够更快获胜,应该提升主动性,在适合的时候宣布获胜即可,当然这种对于策略的制定有一定的要求,有一定的难度。

相关推荐
江上清风山间明月14 小时前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能1 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人1 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen1 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang1 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang1 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1231 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-2 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力2 天前
Flutter应用开发:对象存储管理图片
flutter