Refactor: Base tap gesture recognizer (#41329)
* Extracts the logic of TapGestureRecognizer into an abstract class BaseTapGestureRecognizer * Fixes ModalBarrier unable to dismiss when competing
This commit is contained in:
parent
bedf46d06e
commit
0b0942a60c
@ -59,7 +59,7 @@ class _GestureArena {
|
||||
bool isHeld = false;
|
||||
bool hasPendingSweep = false;
|
||||
|
||||
/// If a gesture attempts to win while the arena is still open, it becomes the
|
||||
/// If a member attempts to win while the arena is still open, it becomes the
|
||||
/// "eager winner". We look for an eager winner when closing the arena to new
|
||||
/// participants, and if there is one, we resolve the arena in its favor at
|
||||
/// that time.
|
||||
|
@ -68,10 +68,10 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
|
||||
/// Configure the behavior of offsets sent to [onStart].
|
||||
///
|
||||
/// If set to [DragStartBehavior.start], the [onStart] callback will be called at the time and
|
||||
/// position when the gesture detector wins the arena. If [DragStartBehavior.down],
|
||||
/// [onStart] will be called at the time and position when a down event was
|
||||
/// first detected.
|
||||
/// If set to [DragStartBehavior.start], the [onStart] callback will be called
|
||||
/// at the time and position when this gesture recognizer wins the arena. If
|
||||
/// [DragStartBehavior.down], [onStart] will be called at the time and
|
||||
/// position when a down event was first detected.
|
||||
///
|
||||
/// For more information about the gesture arena:
|
||||
/// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation
|
||||
@ -80,9 +80,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
///
|
||||
/// ## Example:
|
||||
///
|
||||
/// A finger presses down on the screen with offset (500.0, 500.0),
|
||||
/// and then moves to position (510.0, 500.0) before winning the arena.
|
||||
/// With [dragStartBehavior] set to [DragStartBehavior.down], the [onStart]
|
||||
/// A finger presses down on the screen with offset (500.0, 500.0), and then
|
||||
/// moves to position (510.0, 500.0) before winning the arena. With
|
||||
/// [dragStartBehavior] set to [DragStartBehavior.down], the [onStart]
|
||||
/// callback will be called at the time corresponding to the touch's position
|
||||
/// at (500.0, 500.0). If it is instead set to [DragStartBehavior.start],
|
||||
/// [onStart] will be called at the time corresponding to the touch's position
|
||||
|
@ -98,6 +98,198 @@ typedef GestureTapCallback = void Function();
|
||||
/// * [TapGestureRecognizer], which uses this signature in one of its callbacks.
|
||||
typedef GestureTapCancelCallback = void Function();
|
||||
|
||||
/// A base class for gesture recognizers that recognize taps.
|
||||
///
|
||||
/// Gesture recognizers take part in gesture arenas to enable potential gestures
|
||||
/// to be disambiguated from each other. This process is managed by a
|
||||
/// [GestureArenaManager].
|
||||
///
|
||||
/// A tap is defined as a sequence of events that starts with a down, followed
|
||||
/// by optional moves, then ends with an up. All move events must contain the
|
||||
/// same `buttons` as the down event, and must not be too far from the initial
|
||||
/// position. The gesture is rejected on any violation, a cancel event, or
|
||||
/// if any other recognizers wins the arena. It is accepted only when it is the
|
||||
/// last member of the arena.
|
||||
///
|
||||
/// The [BaseTapGestureRecognizer] considers all the pointers involved in the
|
||||
/// pointer event sequence as contributing to one gesture. For this reason,
|
||||
/// extra pointer interactions during a tap sequence are not recognized as
|
||||
/// additional taps. For example, down-1, down-2, up-1, up-2 produces only one
|
||||
/// tap on up-1.
|
||||
///
|
||||
/// The [BaseTapGestureRecognizer] can not be directly used, since it does not
|
||||
/// define which buttons to accept, or what to do when a tap happens. If you
|
||||
/// want to build a custom tap recognizer, extend this class by overriding
|
||||
/// [isPointerAllowed] and the handler methods.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TapGestureRecognizer], a ready-to-use tap recognizer that recognizes
|
||||
/// taps of the primary button and taps of the secondary button.
|
||||
/// * [ModalBarrier], a widget that uses a custom tap recognizer that accepts
|
||||
/// any buttons.
|
||||
abstract class BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// Creates a tap gesture recognizer.
|
||||
BaseTapGestureRecognizer({ Object debugOwner })
|
||||
: super(deadline: kPressTimeout , debugOwner: debugOwner);
|
||||
|
||||
bool _sentTapDown = false;
|
||||
bool _wonArenaForPrimaryPointer = false;
|
||||
|
||||
PointerDownEvent _down;
|
||||
PointerUpEvent _up;
|
||||
|
||||
/// A pointer has contacted the screen, which might be the start of a tap.
|
||||
///
|
||||
/// This triggers after the down event, once a short timeout ([deadline]) has
|
||||
/// elapsed, or once the gesture has won the arena, whichever comes first.
|
||||
///
|
||||
/// The parameter `down` is the down event of the primary pointer that started
|
||||
/// the tap sequence.
|
||||
///
|
||||
/// If this recognizer doesn't win the arena, [handleTapCancel] is called next.
|
||||
/// Otherwise, [handleTapUp] is called next.
|
||||
@protected
|
||||
void handleTapDown({ PointerDownEvent down });
|
||||
|
||||
/// A pointer has stopped contacting the screen, which is recognized as a tap.
|
||||
///
|
||||
/// This triggers on the up event, if the recognizer wins the arena with it
|
||||
/// or has previously won.
|
||||
///
|
||||
/// The parameter `down` is the down event of the primary pointer that started
|
||||
/// the tap sequence, and `up` is the up event that ended the tap sequence.
|
||||
///
|
||||
/// If this recognizer doesn't win the arena, [handleTapCancel] is called
|
||||
/// instead.
|
||||
@protected
|
||||
void handleTapUp({ PointerDownEvent down, PointerUpEvent up });
|
||||
|
||||
/// A pointer that previously triggered [handleTapDown] will not end up
|
||||
/// causing a tap.
|
||||
///
|
||||
/// This triggers once the gesture loses the arena, if [handleTapDown] has
|
||||
/// been previously triggered.
|
||||
///
|
||||
/// The parameter `down` is the down event of the primary pointer that started
|
||||
/// the tap sequence; `cancel` is the cancel event, which might be null;
|
||||
/// `reason` is a short description of the cause if `cancel` is null, which
|
||||
/// can be "forced" if other gestures won the arena, or "spontaneous"
|
||||
/// otherwise.
|
||||
///
|
||||
/// If this recognizer wins the arena, [handleTapUp] is called instead.
|
||||
@protected
|
||||
void handleTapCancel({ PointerDownEvent down, PointerCancelEvent cancel, String reason });
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
if (state == GestureRecognizerState.ready) {
|
||||
// `_down` must be assigned in this method instead of `handlePrimaryPointer`,
|
||||
// because `acceptGesture` might be called before `handlePrimaryPointer`,
|
||||
// which relies on `_down` to call `handleTapDown`.
|
||||
_down = event;
|
||||
}
|
||||
super.addAllowedPointer(event);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resolve(GestureDisposition disposition) {
|
||||
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
|
||||
// This can happen if the gesture has been canceled. For example, when
|
||||
// the pointer has exceeded the touch slop, the buttons have been changed,
|
||||
// or if the recognizer is disposed.
|
||||
assert(_sentTapDown);
|
||||
_checkCancel(null, 'spontaneous');
|
||||
_reset();
|
||||
}
|
||||
super.resolve(disposition);
|
||||
}
|
||||
|
||||
@override
|
||||
void didExceedDeadline() {
|
||||
_checkDown();
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
super.acceptGesture(pointer);
|
||||
if (pointer == primaryPointer) {
|
||||
_checkDown();
|
||||
_wonArenaForPrimaryPointer = true;
|
||||
_checkUp();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
super.rejectGesture(pointer);
|
||||
if (pointer == primaryPointer) {
|
||||
// Another gesture won the arena.
|
||||
assert(state != GestureRecognizerState.possible);
|
||||
if (_sentTapDown)
|
||||
_checkCancel(null, 'forced');
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
|
||||
void _checkDown() {
|
||||
if (_sentTapDown) {
|
||||
return;
|
||||
}
|
||||
handleTapDown(down: _down);
|
||||
_sentTapDown = true;
|
||||
}
|
||||
|
||||
void _checkUp() {
|
||||
if (!_wonArenaForPrimaryPointer || _up == null) {
|
||||
return;
|
||||
}
|
||||
handleTapUp(down: _down, up: _up);
|
||||
_reset();
|
||||
}
|
||||
|
||||
void _checkCancel(PointerCancelEvent event, String note) {
|
||||
handleTapCancel(down: _down, cancel: event, reason: note);
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_sentTapDown = false;
|
||||
_wonArenaForPrimaryPointer = false;
|
||||
_up = null;
|
||||
_down = null;
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'base tap';
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
|
||||
properties.add(DiagnosticsProperty<Offset>('finalPosition', _up?.position, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _up?.localPosition, defaultValue: _up?.position));
|
||||
properties.add(DiagnosticsProperty<int>('button', _down?.buttons, defaultValue: null));
|
||||
properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Recognizes taps.
|
||||
///
|
||||
/// Gesture recognizers take part in gesture arenas to enable potential gestures
|
||||
@ -118,17 +310,17 @@ typedef GestureTapCancelCallback = void Function();
|
||||
///
|
||||
/// * [GestureDetector.onTap], which uses this recognizer.
|
||||
/// * [MultiTapGestureRecognizer]
|
||||
class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
class TapGestureRecognizer extends BaseTapGestureRecognizer {
|
||||
/// Creates a tap gesture recognizer.
|
||||
TapGestureRecognizer({ Object debugOwner }) : super(deadline: kPressTimeout, debugOwner: debugOwner);
|
||||
TapGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
|
||||
|
||||
/// A pointer that might cause a tap of a primary button has contacted the
|
||||
/// screen at a particular location.
|
||||
/// A pointer has contacted the screen at a particular location with a primary
|
||||
/// button, which might be the start of a tap.
|
||||
///
|
||||
/// This triggers once a short timeout ([deadline]) has elapsed, or once
|
||||
/// the gestures has won the arena, whichever comes first.
|
||||
/// This triggers after the down event, once a short timeout ([deadline]) has
|
||||
/// elapsed, or once the gestures has won the arena, whichever comes first.
|
||||
///
|
||||
/// If the gesture doesn't win the arena, [onTapCancel] is called next.
|
||||
/// If this recognizer doesn't win the arena, [onTapCancel] is called next.
|
||||
/// Otherwise, [onTapUp] is called next.
|
||||
///
|
||||
/// See also:
|
||||
@ -139,13 +331,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// * [GestureDetector.onTapDown], which exposes this callback.
|
||||
GestureTapDownCallback onTapDown;
|
||||
|
||||
/// A pointer that will trigger a tap of a primary button has stopped
|
||||
/// contacting the screen at a particular location.
|
||||
/// A pointer has stopped contacting the screen at a particular location,
|
||||
/// which is recognized as a tap of a primary button.
|
||||
///
|
||||
/// This triggers once the gesture has won the arena, immediately before
|
||||
/// [onTap].
|
||||
/// This triggers on the up event, if the recognizer wins the arena with it
|
||||
/// or has previously won, immediately followed by [onTap].
|
||||
///
|
||||
/// If the gesture doesn't win the arena, [onTapCancel] is called instead.
|
||||
/// If this recognizer doesn't win the arena, [onTapCancel] is called instead.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -155,12 +347,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// * [GestureDetector.onTapUp], which exposes this callback.
|
||||
GestureTapUpCallback onTapUp;
|
||||
|
||||
/// A tap of a primary button has occurred.
|
||||
/// A pointer has stopped contacting the screen, which is recognized as a tap
|
||||
/// of a primary button.
|
||||
///
|
||||
/// This triggers once the gesture has won the arena, immediately after
|
||||
/// [onTapUp].
|
||||
/// This triggers on the up event, if the recognizer wins the arena with it
|
||||
/// or has previously won, immediately following [onTap].
|
||||
///
|
||||
/// If the gesture doesn't win the arena, [onTapCancel] is called instead.
|
||||
/// If this recognizer doesn't win the arena, [onTapCancel] is called instead.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -169,12 +362,14 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// * [GestureDetector.onTap], which exposes this callback.
|
||||
GestureTapCallback onTap;
|
||||
|
||||
/// The pointer that previously triggered [onTapDown] will not end up causing
|
||||
/// A pointer that previously triggered [onTapDown] will not end up causing
|
||||
/// a tap.
|
||||
///
|
||||
/// This triggers if the gesture loses the arena.
|
||||
/// This triggers once the gesture loses the arena, if [onTapDown] has
|
||||
/// previously been triggered.
|
||||
///
|
||||
/// If the gesture wins the arena, [onTapUp] and [onTap] are called instead.
|
||||
/// If this recognizer wins the arena, [onTapUp] and [onTap] are called
|
||||
/// instead.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -183,14 +378,14 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// * [GestureDetector.onTapCancel], which exposes this callback.
|
||||
GestureTapCancelCallback onTapCancel;
|
||||
|
||||
/// A pointer that might cause a tap of a secondary button has contacted the
|
||||
/// screen at a particular location.
|
||||
/// A pointer has contacted the screen at a particular location with a
|
||||
/// secondary button, which might be the start of a secondary tap.
|
||||
///
|
||||
/// This triggers once a short timeout ([deadline]) has elapsed, or once
|
||||
/// the gestures has won the arena, whichever comes first.
|
||||
/// This triggers after the down event, once a short timeout ([deadline]) has
|
||||
/// elapsed, or once the gestures has won the arena, whichever comes first.
|
||||
///
|
||||
/// If the gesture doesn't win the arena, [onSecondaryTapCancel] is called next.
|
||||
/// Otherwise, [onSecondaryTapUp] is called next.
|
||||
/// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called
|
||||
/// next. Otherwise, [onSecondaryTapUp] is called next.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -200,12 +395,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// * [GestureDetector.onSecondaryTapDown], which exposes this callback.
|
||||
GestureTapDownCallback onSecondaryTapDown;
|
||||
|
||||
/// A pointer that will trigger a tap of a secondary button has stopped
|
||||
/// contacting the screen at a particular location.
|
||||
/// A pointer has stopped contacting the screen at a particular location,
|
||||
/// which is recognized as a tap of a secondary button.
|
||||
///
|
||||
/// This triggers once the gesture has won the arena.
|
||||
/// This triggers on the up event, if the recognizer wins the arena with it
|
||||
/// or has previously won.
|
||||
///
|
||||
/// If the gesture doesn't win the arena, [onSecondaryTapCancel] is called
|
||||
/// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called
|
||||
/// instead.
|
||||
///
|
||||
/// See also:
|
||||
@ -216,12 +412,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// * [GestureDetector.onSecondaryTapUp], which exposes this callback.
|
||||
GestureTapUpCallback onSecondaryTapUp;
|
||||
|
||||
/// The pointer that previously triggered [onSecondaryTapDown] will not end up
|
||||
/// A pointer that previously triggered [onSecondaryTapDown] will not end up
|
||||
/// causing a tap.
|
||||
///
|
||||
/// This triggers if the gesture loses the arena.
|
||||
/// This triggers once the gesture loses the arena, if [onSecondaryTapDown]
|
||||
/// has previously been triggered.
|
||||
///
|
||||
/// If the gesture wins the arena, [onSecondaryTapUp] is called instead.
|
||||
/// If this recognizer wins the arena, [onSecondaryTapUp] is called instead.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -230,13 +427,6 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// * [GestureDetector.onTapCancel], which exposes this callback.
|
||||
GestureTapCancelCallback onSecondaryTapCancel;
|
||||
|
||||
bool _sentTapDown = false;
|
||||
bool _wonArenaForPrimaryPointer = false;
|
||||
OffsetPair _finalPosition;
|
||||
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
|
||||
// different set of buttons, the gesture is canceled.
|
||||
int _initialButtons;
|
||||
|
||||
@override
|
||||
bool isPointerAllowed(PointerDownEvent event) {
|
||||
switch (event.buttons) {
|
||||
@ -259,82 +449,15 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
return super.isPointerAllowed(event);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
super.addAllowedPointer(event);
|
||||
// `_initialButtons` must be assigned here instead of `handlePrimaryPointer`,
|
||||
// because `acceptGesture` might be called before `handlePrimaryPointer`,
|
||||
// which relies on `_initialButtons` to create `TapDownDetails`.
|
||||
_initialButtons = event.buttons;
|
||||
}
|
||||
|
||||
@override
|
||||
void handlePrimaryPointer(PointerEvent event) {
|
||||
if (event is PointerUpEvent) {
|
||||
_finalPosition = OffsetPair(global: event.position, local: event.localPosition);
|
||||
_checkUp();
|
||||
} else if (event is PointerCancelEvent) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
if (_sentTapDown) {
|
||||
_checkCancel('');
|
||||
}
|
||||
_reset();
|
||||
} else if (event.buttons != _initialButtons) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
stopTrackingPointer(primaryPointer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resolve(GestureDisposition disposition) {
|
||||
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
|
||||
// This can happen if the gesture has been canceled. For example, when
|
||||
// the pointer has exceeded the touch slop, the buttons have been changed,
|
||||
// or if the recognizer is disposed.
|
||||
assert(_sentTapDown);
|
||||
_checkCancel('spontaneous ');
|
||||
_reset();
|
||||
}
|
||||
super.resolve(disposition);
|
||||
}
|
||||
|
||||
@override
|
||||
void didExceedDeadlineWithEvent(PointerDownEvent event) {
|
||||
_checkDown(event.pointer);
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
super.acceptGesture(pointer);
|
||||
if (pointer == primaryPointer) {
|
||||
_checkDown(pointer);
|
||||
_wonArenaForPrimaryPointer = true;
|
||||
_checkUp();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
super.rejectGesture(pointer);
|
||||
if (pointer == primaryPointer) {
|
||||
// Another gesture won the arena.
|
||||
assert(state != GestureRecognizerState.possible);
|
||||
if (_sentTapDown)
|
||||
_checkCancel('forced ');
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
|
||||
void _checkDown(int pointer) {
|
||||
if (_sentTapDown) {
|
||||
return;
|
||||
}
|
||||
void handleTapDown({PointerDownEvent down}) {
|
||||
final TapDownDetails details = TapDownDetails(
|
||||
globalPosition: initialPosition.global,
|
||||
localPosition: initialPosition.local,
|
||||
kind: getKindForPointer(pointer),
|
||||
globalPosition: down.position,
|
||||
localPosition: down.localPosition,
|
||||
kind: getKindForPointer(down.pointer),
|
||||
);
|
||||
switch (_initialButtons) {
|
||||
switch (down.buttons) {
|
||||
case kPrimaryButton:
|
||||
if (onTapDown != null)
|
||||
invokeCallback<void>('onTapDown', () => onTapDown(details));
|
||||
@ -346,18 +469,16 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
break;
|
||||
default:
|
||||
}
|
||||
_sentTapDown = true;
|
||||
}
|
||||
|
||||
void _checkUp() {
|
||||
if (!_wonArenaForPrimaryPointer || _finalPosition == null) {
|
||||
return;
|
||||
}
|
||||
@protected
|
||||
@override
|
||||
void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
|
||||
final TapUpDetails details = TapUpDetails(
|
||||
globalPosition: _finalPosition.global,
|
||||
localPosition: _finalPosition.local,
|
||||
globalPosition: up.position,
|
||||
localPosition: up.localPosition,
|
||||
);
|
||||
switch (_initialButtons) {
|
||||
switch (down.buttons) {
|
||||
case kPrimaryButton:
|
||||
if (onTapUp != null)
|
||||
invokeCallback<void>('onTapUp', () => onTapUp(details));
|
||||
@ -371,11 +492,13 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
break;
|
||||
default:
|
||||
}
|
||||
_reset();
|
||||
}
|
||||
|
||||
void _checkCancel(String note) {
|
||||
switch (_initialButtons) {
|
||||
@protected
|
||||
@override
|
||||
void handleTapCancel({PointerDownEvent down, PointerCancelEvent cancel, String reason}) {
|
||||
final String note = reason == '' ? reason : ' $reason';
|
||||
switch (down.buttons) {
|
||||
case kPrimaryButton:
|
||||
if (onTapCancel != null)
|
||||
invokeCallback<void>('${note}onTapCancel', onTapCancel);
|
||||
@ -389,23 +512,6 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
}
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_sentTapDown = false;
|
||||
_wonArenaForPrimaryPointer = false;
|
||||
_finalPosition = null;
|
||||
_initialButtons = null;
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'tap';
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
|
||||
properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition?.global, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _finalPosition?.local, defaultValue: _finalPosition?.global));
|
||||
properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
|
||||
// TODO(tongmu): Add property _initialButtons and update related tests
|
||||
}
|
||||
}
|
||||
|
@ -191,99 +191,41 @@ class AnimatedModalBarrier extends AnimatedWidget {
|
||||
//
|
||||
// It is similar to [TapGestureRecognizer.onTapDown], but accepts any single
|
||||
// button, which means the gesture also takes parts in gesture arenas.
|
||||
class _AnyTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
_AnyTapGestureRecognizer({
|
||||
Object debugOwner,
|
||||
}) : super(debugOwner: debugOwner);
|
||||
class _AnyTapGestureRecognizer extends BaseTapGestureRecognizer {
|
||||
_AnyTapGestureRecognizer({ Object debugOwner })
|
||||
: super(debugOwner: debugOwner);
|
||||
|
||||
VoidCallback onAnyTapDown;
|
||||
|
||||
bool _sentTapDown = false;
|
||||
bool _wonArenaForPrimaryPointer = false;
|
||||
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
|
||||
// different set of buttons, the gesture is canceled.
|
||||
int _initialButtons;
|
||||
|
||||
@protected
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
super.addAllowedPointer(event);
|
||||
// `_initialButtons` must be assigned here instead of `handlePrimaryPointer`,
|
||||
// because `acceptGesture` might be called before `handlePrimaryPointer`,
|
||||
// which relies on `_initialButtons` to create `TapDownDetails`.
|
||||
_initialButtons = event.buttons;
|
||||
bool isPointerAllowed(PointerDownEvent event) {
|
||||
if (onAnyTapDown == null)
|
||||
return false;
|
||||
return super.isPointerAllowed(event);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void handlePrimaryPointer(PointerEvent event) {
|
||||
if (event is PointerUpEvent || event is PointerCancelEvent) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
_reset();
|
||||
} else if (event.buttons != _initialButtons) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
stopTrackingPointer(primaryPointer);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resolve(GestureDisposition disposition) {
|
||||
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
|
||||
// This can happen if the gesture has been canceled. For example, when
|
||||
// the pointer has exceeded the touch slop, the buttons have been changed,
|
||||
// or if the recognizer is disposed.
|
||||
assert(_sentTapDown);
|
||||
_reset();
|
||||
}
|
||||
super.resolve(disposition);
|
||||
}
|
||||
|
||||
@override
|
||||
void didExceedDeadlineWithEvent(PointerDownEvent event) {
|
||||
_checkDown(event.pointer);
|
||||
}
|
||||
|
||||
@override
|
||||
void acceptGesture(int pointer) {
|
||||
super.acceptGesture(pointer);
|
||||
if (pointer == primaryPointer) {
|
||||
_checkDown(pointer);
|
||||
_wonArenaForPrimaryPointer = true;
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
super.rejectGesture(pointer);
|
||||
if (pointer == primaryPointer) {
|
||||
// Another gesture won the arena.
|
||||
assert(state != GestureRecognizerState.possible);
|
||||
_reset();
|
||||
}
|
||||
}
|
||||
|
||||
void _checkDown(int pointer) {
|
||||
if (_sentTapDown)
|
||||
return;
|
||||
void handleTapDown({PointerDownEvent down}) {
|
||||
if (onAnyTapDown != null)
|
||||
onAnyTapDown();
|
||||
_sentTapDown = true;
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_sentTapDown = false;
|
||||
_wonArenaForPrimaryPointer = false;
|
||||
_initialButtons = null;
|
||||
@protected
|
||||
@override
|
||||
void handleTapUp({PointerDownEvent down, PointerUpEvent up}) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void handleTapCancel({PointerDownEvent down, PointerCancelEvent cancel, String reason}) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@override
|
||||
String get debugDescription => 'any tap';
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
|
||||
properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
|
||||
}
|
||||
}
|
||||
|
||||
class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
|
||||
|
@ -25,7 +25,7 @@ void main() {
|
||||
tap.addPointer(event);
|
||||
expect(log, hasLength(2));
|
||||
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ ★ Opening new gesture arena.'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Adding: TapGestureRecognizer#00000(state: ready)'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Adding: TapGestureRecognizer#00000(state: ready, button: 1)'));
|
||||
log.clear();
|
||||
|
||||
GestureBinding.instance.gestureArena.close(1);
|
||||
@ -43,7 +43,7 @@ void main() {
|
||||
GestureBinding.instance.gestureArena.sweep(1);
|
||||
expect(log, hasLength(2));
|
||||
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ Sweeping with 1 member.'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0))'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1)'));
|
||||
log.clear();
|
||||
|
||||
tap.dispose();
|
||||
@ -83,9 +83,9 @@ void main() {
|
||||
|
||||
GestureBinding.instance.gestureArena.sweep(1);
|
||||
expect(log, hasLength(3));
|
||||
expect(log[0], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0)) calling onTapDown callback.'));
|
||||
expect(log[1], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTapUp callback.'));
|
||||
expect(log[2], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTap callback.'));
|
||||
expect(log[0], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1) calling onTapDown callback.'));
|
||||
expect(log[1], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTapUp callback.'));
|
||||
expect(log[2], equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTap callback.'));
|
||||
log.clear();
|
||||
|
||||
tap.dispose();
|
||||
@ -114,7 +114,7 @@ void main() {
|
||||
tap.addPointer(event);
|
||||
expect(log, hasLength(2));
|
||||
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ ★ Opening new gesture arena.'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Adding: TapGestureRecognizer#00000(state: ready)'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Adding: TapGestureRecognizer#00000(state: ready, button: 1)'));
|
||||
log.clear();
|
||||
|
||||
GestureBinding.instance.gestureArena.close(1);
|
||||
@ -132,10 +132,10 @@ void main() {
|
||||
GestureBinding.instance.gestureArena.sweep(1);
|
||||
expect(log, hasLength(5));
|
||||
expect(log[0], equalsIgnoringHashCodes('Gesture arena 1 ❙ Sweeping with 1 member.'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0))'));
|
||||
expect(log[2], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0)) calling onTapDown callback.'));
|
||||
expect(log[3], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTapUp callback.'));
|
||||
expect(log[4], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), sent tap down) calling onTap callback.'));
|
||||
expect(log[1], equalsIgnoringHashCodes('Gesture arena 1 ❙ Winner: TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1)'));
|
||||
expect(log[2], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready, finalPosition: Offset(12.0, 8.0), button: 1) calling onTapDown callback.'));
|
||||
expect(log[3], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTapUp callback.'));
|
||||
expect(log[4], equalsIgnoringHashCodes(' ❙ TapGestureRecognizer#00000(state: ready, won arena, finalPosition: Offset(12.0, 8.0), button: 1, sent tap down) calling onTap callback.'));
|
||||
log.clear();
|
||||
|
||||
tap.dispose();
|
||||
@ -152,7 +152,7 @@ void main() {
|
||||
expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: ready)'));
|
||||
const PointerEvent event = PointerDownEvent(pointer: 1, position: Offset(10.0, 10.0));
|
||||
tap.addPointer(event);
|
||||
tap.didExceedDeadlineWithEvent(event);
|
||||
expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: possible, sent tap down)'));
|
||||
tap.didExceedDeadline();
|
||||
expect(tap.toString(), equalsIgnoringHashCodes('TapGestureRecognizer#00000(state: possible, button: 1, sent tap down)'));
|
||||
});
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ void main() {
|
||||
tap.dispose();
|
||||
});
|
||||
|
||||
testGesture('Should not recognize two overlapping taps', (GestureTester tester) {
|
||||
testGesture('Should not recognize two overlapping taps (FIFO)', (GestureTester tester) {
|
||||
final TapGestureRecognizer tap = TapGestureRecognizer();
|
||||
|
||||
int tapsRecognized = 0;
|
||||
@ -174,6 +174,40 @@ void main() {
|
||||
tap.dispose();
|
||||
});
|
||||
|
||||
testGesture('Should not recognize two overlapping taps (FILO)', (GestureTester tester) {
|
||||
final TapGestureRecognizer tap = TapGestureRecognizer();
|
||||
|
||||
int tapsRecognized = 0;
|
||||
tap.onTap = () {
|
||||
tapsRecognized++;
|
||||
};
|
||||
|
||||
tap.addPointer(down1);
|
||||
tester.closeArena(1);
|
||||
expect(tapsRecognized, 0);
|
||||
tester.route(down1);
|
||||
expect(tapsRecognized, 0);
|
||||
|
||||
tap.addPointer(down2);
|
||||
tester.closeArena(2);
|
||||
expect(tapsRecognized, 0);
|
||||
tester.route(down1);
|
||||
expect(tapsRecognized, 0);
|
||||
|
||||
|
||||
tester.route(up2);
|
||||
expect(tapsRecognized, 0);
|
||||
GestureBinding.instance.gestureArena.sweep(2);
|
||||
expect(tapsRecognized, 0);
|
||||
|
||||
tester.route(up1);
|
||||
expect(tapsRecognized, 1);
|
||||
GestureBinding.instance.gestureArena.sweep(1);
|
||||
expect(tapsRecognized, 1);
|
||||
|
||||
tap.dispose();
|
||||
});
|
||||
|
||||
testGesture('Distance cancels tap', (GestureTester tester) {
|
||||
final TapGestureRecognizer tap = TapGestureRecognizer();
|
||||
|
||||
|
@ -130,7 +130,7 @@ void main() {
|
||||
await tester.pump(); // begin transition
|
||||
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||
|
||||
// Tap on the barrier to dismiss it
|
||||
// Press the barrier to dismiss it
|
||||
await tester.press(find.byKey(const ValueKey<String>('barrier')));
|
||||
await tester.pump(); // begin transition
|
||||
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||
@ -155,7 +155,7 @@ void main() {
|
||||
await tester.pump(); // begin transition
|
||||
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||
|
||||
// Tap on the barrier to dismiss it
|
||||
// Press the barrier to dismiss it
|
||||
await tester.press(find.byKey(const ValueKey<String>('barrier')), buttons: kSecondaryButton);
|
||||
await tester.pump(); // begin transition
|
||||
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||
@ -164,6 +164,31 @@ void main() {
|
||||
reason: 'The route should have been dismissed by tapping the barrier.');
|
||||
});
|
||||
|
||||
testWidgets('ModalBarrier may pop the Navigator when competing with other gestures', (WidgetTester tester) async {
|
||||
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||
'/': (BuildContext context) => FirstWidget(),
|
||||
'/modal': (BuildContext context) => SecondWidgetWithCompetence (),
|
||||
};
|
||||
|
||||
await tester.pumpWidget(MaterialApp(routes: routes));
|
||||
|
||||
// Initially the barrier is not visible
|
||||
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
|
||||
|
||||
// Tapping on X routes to the barrier
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump(); // begin transition
|
||||
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||
|
||||
// Tap on the barrier to dismiss it
|
||||
await tester.tap(find.byKey(const ValueKey<String>('barrier')));
|
||||
await tester.pump(); // begin transition
|
||||
await tester.pump(const Duration(seconds: 1)); // end transition
|
||||
|
||||
expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
|
||||
reason: 'The route should have been dismissed by tapping the barrier.');
|
||||
});
|
||||
|
||||
testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async {
|
||||
bool willPopCalled = false;
|
||||
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
|
||||
@ -316,3 +341,22 @@ class SecondWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SecondWidgetWithCompetence extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
const ModalBarrier(
|
||||
key: ValueKey<String>('barrier'),
|
||||
dismissible: true,
|
||||
),
|
||||
GestureDetector(
|
||||
onVerticalDragStart: (_) {},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Container(),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user