From 0b0942a60caafff56c19fea07d8f7b14e6286e1c Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Wed, 2 Oct 2019 15:28:55 -0700 Subject: [PATCH] Refactor: Base tap gesture recognizer (#41329) * Extracts the logic of TapGestureRecognizer into an abstract class BaseTapGestureRecognizer * Fixes ModalBarrier unable to dismiss when competing --- packages/flutter/lib/src/gestures/arena.dart | 2 +- .../flutter/lib/src/gestures/monodrag.dart | 14 +- packages/flutter/lib/src/gestures/tap.dart | 386 +++++++++++------- .../lib/src/widgets/modal_barrier.dart | 98 +---- .../flutter/test/gestures/debug_test.dart | 24 +- packages/flutter/test/gestures/tap_test.dart | 36 +- .../test/widgets/modal_barrier_test.dart | 48 ++- 7 files changed, 367 insertions(+), 241 deletions(-) diff --git a/packages/flutter/lib/src/gestures/arena.dart b/packages/flutter/lib/src/gestures/arena.dart index 9b4a23e881..1afa901749 100644 --- a/packages/flutter/lib/src/gestures/arena.dart +++ b/packages/flutter/lib/src/gestures/arena.dart @@ -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. diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index 95ca2a3a6a..48203ede25 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -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 diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index 38766dd041..21221f9c69 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -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('finalPosition', _up?.position, defaultValue: null)); + properties.add(DiagnosticsProperty('finalLocalPosition', _up?.localPosition, defaultValue: _up?.position)); + properties.add(DiagnosticsProperty('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('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('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('${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('finalPosition', _finalPosition?.global, defaultValue: null)); - properties.add(DiagnosticsProperty('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 - } } diff --git a/packages/flutter/lib/src/widgets/modal_barrier.dart b/packages/flutter/lib/src/widgets/modal_barrier.dart index d6936b0ed6..cc449ef650 100644 --- a/packages/flutter/lib/src/widgets/modal_barrier.dart +++ b/packages/flutter/lib/src/widgets/modal_barrier.dart @@ -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 { diff --git a/packages/flutter/test/gestures/debug_test.dart b/packages/flutter/test/gestures/debug_test.dart index 1abad2a486..25a2a28521 100644 --- a/packages/flutter/test/gestures/debug_test.dart +++ b/packages/flutter/test/gestures/debug_test.dart @@ -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)')); }); } diff --git a/packages/flutter/test/gestures/tap_test.dart b/packages/flutter/test/gestures/tap_test.dart index 2d377d6506..a2af6282de 100644 --- a/packages/flutter/test/gestures/tap_test.dart +++ b/packages/flutter/test/gestures/tap_test.dart @@ -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(); diff --git a/packages/flutter/test/widgets/modal_barrier_test.dart b/packages/flutter/test/widgets/modal_barrier_test.dart index 9bf6c2eac7..8a447df0eb 100644 --- a/packages/flutter/test/widgets/modal_barrier_test.dart +++ b/packages/flutter/test/widgets/modal_barrier_test.dart @@ -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('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('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 routes = { + '/': (BuildContext context) => FirstWidget(), + '/modal': (BuildContext context) => SecondWidgetWithCompetence (), + }; + + await tester.pumpWidget(MaterialApp(routes: routes)); + + // Initially the barrier is not visible + expect(find.byKey(const ValueKey('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('barrier'))); + await tester.pump(); // begin transition + await tester.pump(const Duration(seconds: 1)); // end transition + + expect(find.byKey(const ValueKey('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 routes = { @@ -316,3 +341,22 @@ class SecondWidget extends StatelessWidget { ); } } + +class SecondWidgetWithCompetence extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Stack( + children: [ + const ModalBarrier( + key: ValueKey('barrier'), + dismissible: true, + ), + GestureDetector( + onVerticalDragStart: (_) {}, + behavior: HitTestBehavior.translucent, + child: Container(), + ) + ], + ); + } +}