From 6a75dc449e2247944a45ae394cb9b02469be7d7f Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Fri, 1 May 2020 13:04:01 -0700 Subject: [PATCH] Add onSecondaryTap to gesture recognizer and gesture detector. (#55494) --- packages/flutter/lib/src/gestures/events.dart | 2 +- .../flutter/lib/src/gestures/long_press.dart | 153 +++++- packages/flutter/lib/src/gestures/tap.dart | 34 +- .../lib/src/widgets/gesture_detector.dart | 95 +++- .../test/widgets/gesture_detector_test.dart | 465 ++++++++++-------- 5 files changed, 517 insertions(+), 232 deletions(-) diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index 23f9b26f1e..b67e4deb13 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -30,7 +30,7 @@ const int kPrimaryButton = 0x01; /// It is equivalent to: /// /// * [kPrimaryStylusButton]: The stylus contacts the screen. -/// * [kSecondaryMouseButton]: The primary mouse button. +/// * [kSecondaryMouseButton]: The secondary mouse button. /// /// See also: /// diff --git a/packages/flutter/lib/src/gestures/long_press.dart b/packages/flutter/lib/src/gestures/long_press.dart index a82a43e27b..ec41712855 100644 --- a/packages/flutter/lib/src/gestures/long_press.dart +++ b/packages/flutter/lib/src/gestures/long_press.dart @@ -141,9 +141,9 @@ class LongPressEndDetails { /// moved, triggering [onLongPressMoveUpdate] callbacks, unless the /// [postAcceptSlopTolerance] constructor argument is specified. /// -/// [LongPressGestureRecognizer] competes on pointer events of [kPrimaryButton] -/// only when it has at least one non-null callback. If it has no callbacks, it -/// is a no-op. +/// [LongPressGestureRecognizer] may compete on pointer events of +/// [kPrimaryButton] and/or [kSecondaryButton] if at least one corresponding +/// callback is non-null. If it has no callbacks, it is a no-op. class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { /// Creates a long-press gesture recognizer. /// @@ -225,6 +225,57 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { /// callback. GestureLongPressEndCallback onLongPressEnd; + /// Called when a long press gesture by a secondary button has been + /// recognized. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPressStart], which has the same timing but has data for + /// the press location. + GestureLongPressCallback onSecondaryLongPress; + + /// Called when a long press gesture by a secondary button has been recognized. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPress], which has the same timing but without details. + /// * [LongPressStartDetails], which is passed as an argument to this + /// callback. + GestureLongPressStartCallback onSecondaryLongPressStart; + + /// Called when moving after the long press by a secondary button is + /// recognized. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [LongPressMoveUpdateDetails], which is passed as an argument to this + /// callback. + GestureLongPressMoveUpdateCallback onSecondaryLongPressMoveUpdate; + + /// Called when the pointer stops contacting the screen after a long-press by + /// a secondary button. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPressEnd], which has the same timing but has data for + /// the up gesture location. + GestureLongPressUpCallback onSecondaryLongPressUp; + + /// Called when the pointer stops contacting the screen after a long-press by + /// a secondary button. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPressUp], which has the same timing, but without + /// details. + /// * [LongPressEndDetails], which is passed as an argument to this callback. + GestureLongPressEndCallback onSecondaryLongPressEnd; + VelocityTracker _velocityTracker; @override @@ -238,6 +289,14 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { onLongPressUp == null) return false; break; + case kSecondaryButton: + if (onSecondaryLongPressStart == null && + onSecondaryLongPress == null && + onSecondaryLongPressMoveUpdate == null && + onSecondaryLongPressEnd == null && + onSecondaryLongPressUp == null) + return false; + break; default: return false; } @@ -291,37 +350,67 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { } void _checkLongPressStart() { - assert(_initialButtons == kPrimaryButton); - if (onLongPressStart != null) { - final LongPressStartDetails details = LongPressStartDetails( - globalPosition: _longPressOrigin.global, - localPosition: _longPressOrigin.local, - ); - invokeCallback('onLongPressStart', - () => onLongPressStart(details)); + switch (_initialButtons) { + case kPrimaryButton: + if (onLongPressStart != null) { + final LongPressStartDetails details = LongPressStartDetails( + globalPosition: _longPressOrigin.global, + localPosition: _longPressOrigin.local, + ); + invokeCallback('onLongPressStart', () => onLongPressStart(details)); + } + if (onLongPress != null) { + invokeCallback('onLongPress', onLongPress); + } + break; + case kSecondaryButton: + if (onSecondaryLongPressStart != null) { + final LongPressStartDetails details = LongPressStartDetails( + globalPosition: _longPressOrigin.global, + localPosition: _longPressOrigin.local, + ); + invokeCallback( + 'onSecondaryLongPressStart', () => onSecondaryLongPressStart(details)); + } + if (onSecondaryLongPress != null) { + invokeCallback('onSecondaryLongPress', onSecondaryLongPress); + } + break; + default: + assert(false, 'Unhandled button $_initialButtons'); } - if (onLongPress != null) - invokeCallback('onLongPress', onLongPress); } void _checkLongPressMoveUpdate(PointerEvent event) { - assert(_initialButtons == kPrimaryButton); final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails( globalPosition: event.position, localPosition: event.localPosition, offsetFromOrigin: event.position - _longPressOrigin.global, localOffsetFromOrigin: event.localPosition - _longPressOrigin.local, ); - if (onLongPressMoveUpdate != null) - invokeCallback('onLongPressMoveUpdate', - () => onLongPressMoveUpdate(details)); + switch (_initialButtons) { + case kPrimaryButton: + if (onLongPressMoveUpdate != null) { + invokeCallback('onLongPressMoveUpdate', + () => onLongPressMoveUpdate(details)); + } + break; + case kSecondaryButton: + if (onSecondaryLongPressMoveUpdate != null) { + invokeCallback('onSecondaryLongPressMoveUpdate', + () => onSecondaryLongPressMoveUpdate(details)); + } + break; + default: + assert(false, 'Unhandled button $_initialButtons'); + } } void _checkLongPressEnd(PointerEvent event) { - assert(_initialButtons == kPrimaryButton); - final VelocityEstimate estimate = _velocityTracker.getVelocityEstimate(); - final Velocity velocity = estimate == null ? Velocity.zero : Velocity(pixelsPerSecond: estimate.pixelsPerSecond); + final Velocity velocity = estimate == null + ? Velocity.zero + : Velocity(pixelsPerSecond: estimate.pixelsPerSecond); final LongPressEndDetails details = LongPressEndDetails( globalPosition: event.position, localPosition: event.localPosition, @@ -329,10 +418,26 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { ); _velocityTracker = null; - if (onLongPressEnd != null) - invokeCallback('onLongPressEnd', () => onLongPressEnd(details)); - if (onLongPressUp != null) - invokeCallback('onLongPressUp', onLongPressUp); + switch (_initialButtons) { + case kPrimaryButton: + if (onLongPressEnd != null) { + invokeCallback('onLongPressEnd', () => onLongPressEnd(details)); + } + if (onLongPressUp != null) { + invokeCallback('onLongPressUp', onLongPressUp); + } + break; + case kSecondaryButton: + if (onSecondaryLongPressEnd != null) { + invokeCallback('onSecondaryLongPressEnd', () => onSecondaryLongPressEnd(details)); + } + if (onSecondaryLongPressUp != null) { + invokeCallback('onSecondaryLongPressUp', onSecondaryLongPressUp); + } + break; + default: + assert(false, 'Unhandled button $_initialButtons'); + } } void _reset() { diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index 002321a0a4..2737e5a26a 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -369,7 +369,7 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// of a primary button. /// /// This triggers on the up event, if the recognizer wins the arena with it - /// or has previously won, immediately following [onTap]. + /// or has previously won, immediately following [onTapUp]. /// /// If this recognizer doesn't win the arena, [onTapCancel] is called instead. /// @@ -396,6 +396,22 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// * [GestureDetector.onTapCancel], which exposes this callback. GestureTapCancelCallback onTapCancel; + /// A pointer has stopped contacting the screen, which is recognized as a tap + /// of a secondary button. + /// + /// This triggers on the up event, if the recognizer wins the arena with it or + /// has previously won, immediately following [onSecondaryTapUp]. + /// + /// If this recognizer doesn't win the arena, [onSecondaryTapCancel] is called + /// instead. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryTapUp], which has the same timing but with details. + /// * [GestureDetector.onSecondaryTap], which exposes this callback. + GestureTapCallback onSecondaryTap; + /// A pointer has contacted the screen at a particular location with a /// secondary button, which might be the start of a secondary tap. /// @@ -424,6 +440,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// /// See also: /// + /// * [onSecondaryTap], a handler triggered right after this one that doesn't + /// pass any details about the tap. /// * [kSecondaryButton], the button this callback responds to. /// * [onTapUp], a similar callback but for a primary button. /// * [TapUpDetails], which is passed as an argument to this callback. @@ -456,7 +474,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { return false; break; case kSecondaryButton: - if (onSecondaryTapDown == null && + if (onSecondaryTap == null && + onSecondaryTapDown == null && onSecondaryTapUp == null && onSecondaryTapCancel == null) return false; @@ -482,8 +501,7 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { break; case kSecondaryButton: if (onSecondaryTapDown != null) - invokeCallback('onSecondaryTapDown', - () => onSecondaryTapDown(details)); + invokeCallback('onSecondaryTapDown', () => onSecondaryTapDown(details)); break; default: } @@ -505,8 +523,9 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { break; case kSecondaryButton: if (onSecondaryTapUp != null) - invokeCallback('onSecondaryTapUp', - () => onSecondaryTapUp(details)); + invokeCallback('onSecondaryTapUp', () => onSecondaryTapUp(details)); + if (onSecondaryTap != null) + invokeCallback('onSecondaryTap', () => onSecondaryTap()); break; default: } @@ -523,8 +542,7 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { break; case kSecondaryButton: if (onSecondaryTapCancel != null) - invokeCallback('${note}onSecondaryTapCancel', - onSecondaryTapCancel); + invokeCallback('${note}onSecondaryTapCancel', onSecondaryTapCancel); break; default: } diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index d1b12a5139..c0a2914198 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -189,6 +189,7 @@ class GestureDetector extends StatelessWidget { this.onTapUp, this.onTap, this.onTapCancel, + this.onSecondaryTap, this.onSecondaryTapDown, this.onSecondaryTapUp, this.onSecondaryTapCancel, @@ -198,6 +199,11 @@ class GestureDetector extends StatelessWidget { this.onLongPressMoveUpdate, this.onLongPressUp, this.onLongPressEnd, + this.onSecondaryLongPress, + this.onSecondaryLongPressStart, + this.onSecondaryLongPressMoveUpdate, + this.onSecondaryLongPressUp, + this.onSecondaryLongPressEnd, this.onVerticalDragDown, this.onVerticalDragStart, this.onVerticalDragUpdate, @@ -304,6 +310,18 @@ class GestureDetector extends StatelessWidget { /// * [kPrimaryButton], the button this callback responds to. final GestureTapCancelCallback onTapCancel; + /// A tap with a secondary button has occurred. + /// + /// This triggers when the tap gesture wins. If the tap gesture did not win, + /// [onSecondaryTapCancel] is called instead. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryTapUp], which is called at the same time but includes details + /// regarding the pointer position. + final GestureTapCallback onSecondaryTap; + /// A pointer that might cause a tap with a secondary button has contacted the /// screen at a particular location. /// @@ -324,6 +342,8 @@ class GestureDetector extends StatelessWidget { /// /// See also: /// + /// * [onSecondaryTap], a handler triggered right after this one that doesn't + /// pass any details about the tap. /// * [kSecondaryButton], the button this callback responds to. final GestureTapUpCallback onSecondaryTapUp; @@ -394,6 +414,59 @@ class GestureDetector extends StatelessWidget { /// details. final GestureLongPressEndCallback onLongPressEnd; + /// Called when a long press gesture with a secondary button has been + /// recognized. + /// + /// Triggered when a pointer has remained in contact with the screen at the + /// same location for a long period of time. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPressStart], which has the same timing but has gesture + /// details. + final GestureLongPressCallback onSecondaryLongPress; + + /// Called when a long press gesture with a secondary button has been + /// recognized. + /// + /// Triggered when a pointer has remained in contact with the screen at the + /// same location for a long period of time. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPress], which has the same timing but without the + /// gesture details. + final GestureLongPressStartCallback onSecondaryLongPressStart; + + /// A pointer has been drag-moved after a long press with a secondary button. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + final GestureLongPressMoveUpdateCallback onSecondaryLongPressMoveUpdate; + + /// A pointer that has triggered a long-press with a secondary button has + /// stopped contacting the screen. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPressEnd], which has the same timing but has gesture + /// details. + final GestureLongPressUpCallback onSecondaryLongPressUp; + + /// A pointer that has triggered a long-press with a secondary button has + /// stopped contacting the screen. + /// + /// See also: + /// + /// * [kSecondaryButton], the button this callback responds to. + /// * [onSecondaryLongPressUp], which has the same timing but without the + /// gesture details. + final GestureLongPressEndCallback onSecondaryLongPressEnd; + /// A pointer has contacted the screen with a primary button and might begin /// to move vertically. /// @@ -601,6 +674,7 @@ class GestureDetector extends StatelessWidget { onTapUp != null || onTap != null || onTapCancel != null || + onSecondaryTap != null || onSecondaryTapDown != null || onSecondaryTapUp != null || onSecondaryTapCancel != null @@ -613,6 +687,7 @@ class GestureDetector extends StatelessWidget { ..onTapUp = onTapUp ..onTap = onTap ..onTapCancel = onTapCancel + ..onSecondaryTap = onSecondaryTap ..onSecondaryTapDown = onSecondaryTapDown ..onSecondaryTapUp = onSecondaryTapUp ..onSecondaryTapCancel = onSecondaryTapCancel; @@ -647,6 +722,24 @@ class GestureDetector extends StatelessWidget { ); } + if (onSecondaryLongPress != null || + onSecondaryLongPressUp != null || + onSecondaryLongPressStart != null || + onSecondaryLongPressMoveUpdate != null || + onSecondaryLongPressEnd != null) { + gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance + ..onSecondaryLongPress = onSecondaryLongPress + ..onSecondaryLongPressStart = onSecondaryLongPressStart + ..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate + ..onSecondaryLongPressEnd =onSecondaryLongPressEnd + ..onSecondaryLongPressUp = onSecondaryLongPressUp; + }, + ); + } + if (onVerticalDragDown != null || onVerticalDragStart != null || onVerticalDragUpdate != null || @@ -1110,7 +1203,7 @@ abstract class SemanticsGestureDelegate { // For readers who come here to learn how to write custom semantics delegates: // this is not a proper sample code. It has access to the detector state as well // as its private properties, which are inaccessible normally. It is designed -// this way in order to work independenly in a [RawGestureRecognizer] to +// this way in order to work independently in a [RawGestureRecognizer] to // preserve existing behavior. // // Instead, a normal delegate will store callbacks as properties, and use them diff --git a/packages/flutter/test/widgets/gesture_detector_test.dart b/packages/flutter/test/widgets/gesture_detector_test.dart index adb772147e..6ad59cfc49 100644 --- a/packages/flutter/test/widgets/gesture_detector_test.dart +++ b/packages/flutter/test/widgets/gesture_detector_test.dart @@ -125,222 +125,258 @@ void main() { expect(didEndPan, isTrue); }); - testWidgets('Translucent', (WidgetTester tester) async { - bool didReceivePointerDown; - bool didTap; + group('Tap', () { + final ButtonVariant buttonVariant = ButtonVariant( + values: [kPrimaryButton, kSecondaryButton], + descriptions: { + kPrimaryButton: 'primary', + kSecondaryButton: 'secondary', + }, + ); - Future pumpWidgetTree(HitTestBehavior behavior) { - return tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Stack( - children: [ - Listener( - onPointerDown: (_) { - didReceivePointerDown = true; - }, - child: Container( + testWidgets('Translucent', (WidgetTester tester) async { + bool didReceivePointerDown; + bool didTap; + + Future pumpWidgetTree(HitTestBehavior behavior) { + return tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Stack( + children: [ + Listener( + onPointerDown: (_) { + didReceivePointerDown = true; + }, + child: Container( + width: 100.0, + height: 100.0, + color: const Color(0xFF00FF00), + ), + ), + Container( width: 100.0, height: 100.0, - color: const Color(0xFF00FF00), + child: GestureDetector( + onTap: ButtonVariant.button == kPrimaryButton ? () { + didTap = true; + } : null, + onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { + didTap = true; + } : null, + behavior: behavior, + ), ), - ), - Container( - width: 100.0, - height: 100.0, - child: GestureDetector( - onTap: () { - didTap = true; - }, - behavior: behavior, - ), - ), - ], + ], + ), + ), + ); + } + + didReceivePointerDown = false; + didTap = false; + await pumpWidgetTree(null); + await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); + expect(didReceivePointerDown, isTrue); + expect(didTap, isTrue); + + didReceivePointerDown = false; + didTap = false; + await pumpWidgetTree(HitTestBehavior.deferToChild); + await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); + expect(didReceivePointerDown, isTrue); + expect(didTap, isFalse); + + didReceivePointerDown = false; + didTap = false; + await pumpWidgetTree(HitTestBehavior.opaque); + await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); + expect(didReceivePointerDown, isFalse); + expect(didTap, isTrue); + + didReceivePointerDown = false; + didTap = false; + await pumpWidgetTree(HitTestBehavior.translucent); + await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); + expect(didReceivePointerDown, isTrue); + expect(didTap, isTrue); + }, variant: buttonVariant); + + testWidgets('Empty', (WidgetTester tester) async { + bool didTap = false; + await tester.pumpWidget( + Center( + child: GestureDetector( + onTap: ButtonVariant.button == kPrimaryButton ? () { + didTap = true; + } : null, + onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { + didTap = true; + } : null, ), ), ); - } + expect(didTap, isFalse); + await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button); + expect(didTap, isTrue); + }, variant: buttonVariant); - didReceivePointerDown = false; - didTap = false; - await pumpWidgetTree(null); - await tester.tapAt(const Offset(10.0, 10.0)); - expect(didReceivePointerDown, isTrue); - expect(didTap, isTrue); - - didReceivePointerDown = false; - didTap = false; - await pumpWidgetTree(HitTestBehavior.deferToChild); - await tester.tapAt(const Offset(10.0, 10.0)); - expect(didReceivePointerDown, isTrue); - expect(didTap, isFalse); - - didReceivePointerDown = false; - didTap = false; - await pumpWidgetTree(HitTestBehavior.opaque); - await tester.tapAt(const Offset(10.0, 10.0)); - expect(didReceivePointerDown, isFalse); - expect(didTap, isTrue); - - didReceivePointerDown = false; - didTap = false; - await pumpWidgetTree(HitTestBehavior.translucent); - await tester.tapAt(const Offset(10.0, 10.0)); - expect(didReceivePointerDown, isTrue); - expect(didTap, isTrue); - - }); - - testWidgets('Empty', (WidgetTester tester) async { - bool didTap = false; - await tester.pumpWidget( - Center( - child: GestureDetector( - onTap: () { - didTap = true; - }, - ), - ), - ); - expect(didTap, isFalse); - await tester.tapAt(const Offset(10.0, 10.0)); - expect(didTap, isTrue); - }); - - testWidgets('Only container', (WidgetTester tester) async { - bool didTap = false; - await tester.pumpWidget( - Center( - child: GestureDetector( - onTap: () { - didTap = true; - }, - child: Container(), - ), - ), - ); - expect(didTap, isFalse); - await tester.tapAt(const Offset(10.0, 10.0)); - expect(didTap, isFalse); - }); - - testWidgets('cache render object', (WidgetTester tester) async { - final GestureTapCallback inputCallback = () { }; - - await tester.pumpWidget( - Center( - child: GestureDetector( - onTap: inputCallback, - child: Container(), - ), - ), - ); - - final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector)); - - await tester.pumpWidget( - Center( - child: GestureDetector( - onTap: inputCallback, - child: Container(), - ), - ), - ); - - final RenderSemanticsGestureHandler renderObj2 = tester.renderObject(find.byType(GestureDetector)); - - expect(renderObj1, same(renderObj2)); - }); - - testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async { - int tapDown = 0; - int tap = 0; - int tapCancel = 0; - int longPress = 0; - - await tester.pumpWidget( - Container( - alignment: Alignment.topLeft, - child: Container( - alignment: Alignment.center, - height: 100.0, - color: const Color(0xFF00FF00), + testWidgets('Only container', (WidgetTester tester) async { + bool didTap = false; + await tester.pumpWidget( + Center( child: GestureDetector( - onTapDown: (TapDownDetails details) { - tapDown += 1; - }, - onTap: () { - tap += 1; - }, - onTapCancel: () { - tapCancel += 1; - }, - onLongPress: () { - longPress += 1; - }, + onTap: ButtonVariant.button == kPrimaryButton ? () { + didTap = true; + } : null, + onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { + didTap = true; + } : null, + child: Container(), ), ), - ), - ); + ); + expect(didTap, isFalse); + await tester.tapAt(const Offset(10.0, 10.0)); + expect(didTap, isFalse); + }, variant: buttonVariant); - // Pointer is dragged from the center of the 800x100 gesture detector - // to a point (400,300) below it. This should never call onTap. - Future dragOut(Duration timeout) async { - final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0)); - // If the timeout is less than kPressTimeout the recognizer will not - // trigger any callbacks. If the timeout is greater than kLongPressTimeout - // then onTapDown, onLongPress, and onCancel will be called. - await tester.pump(timeout); - await gesture.moveTo(const Offset(400.0, 300.0)); - await gesture.up(); - } + testWidgets('cache render object', (WidgetTester tester) async { + final GestureTapCallback inputCallback = () { }; - await dragOut(kPressTimeout * 0.5); // generates nothing - expect(tapDown, 0); - expect(tapCancel, 0); - expect(tap, 0); - expect(longPress, 0); - - await dragOut(kPressTimeout); // generates tapDown, tapCancel - expect(tapDown, 1); - expect(tapCancel, 1); - expect(tap, 0); - expect(longPress, 0); - - await dragOut(kLongPressTimeout); // generates tapDown, longPress, tapCancel - expect(tapDown, 2); - expect(tapCancel, 2); - expect(tap, 0); - expect(longPress, 1); - }); - - testWidgets('Long Press Up Callback called after long press', (WidgetTester tester) async { - int longPressUp = 0; - - await tester.pumpWidget( - Container( - alignment: Alignment.topLeft, - child: Container( - alignment: Alignment.center, - height: 100.0, - color: const Color(0xFF00FF00), + await tester.pumpWidget( + Center( child: GestureDetector( - onLongPressUp: () { - longPressUp += 1; - }, + onTap: ButtonVariant.button == kPrimaryButton ? inputCallback : null, + onSecondaryTap: ButtonVariant.button == kSecondaryButton ? inputCallback : null, + child: Container(), ), ), - ), - ); + ); - Future longPress(Duration timeout) async { - final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0)); - await tester.pump(timeout); - await gesture.up(); - } + final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector)); - await longPress(kLongPressTimeout + const Duration(seconds: 1)); // To make sure the time for long press has occurred - expect(longPressUp, 1); + await tester.pumpWidget( + Center( + child: GestureDetector( + onTap: ButtonVariant.button == kPrimaryButton ? inputCallback : null, + onSecondaryTap: ButtonVariant.button == kSecondaryButton ? inputCallback : null, + child: Container(), + ), + ), + ); + + final RenderSemanticsGestureHandler renderObj2 = tester.renderObject(find.byType(GestureDetector)); + + expect(renderObj1, same(renderObj2)); + }, variant: buttonVariant); + + testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async { + int tapDown = 0; + int tap = 0; + int tapCancel = 0; + int longPress = 0; + + await tester.pumpWidget( + Container( + alignment: Alignment.topLeft, + child: Container( + alignment: Alignment.center, + height: 100.0, + color: const Color(0xFF00FF00), + child: GestureDetector( + onTapDown: ButtonVariant.button == kPrimaryButton ? (TapDownDetails details) { + tapDown += 1; + } : null, + onSecondaryTapDown: ButtonVariant.button == kSecondaryButton ? (TapDownDetails details) { + tapDown += 1; + } : null, + onTap: ButtonVariant.button == kPrimaryButton ? () { + tap += 1; + } : null, + onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () { + tap += 1; + } : null, + onTapCancel: ButtonVariant.button == kPrimaryButton ? () { + tapCancel += 1; + } : null, + onSecondaryTapCancel: ButtonVariant.button == kSecondaryButton ? () { + tapCancel += 1; + } : null, + onLongPress: ButtonVariant.button == kPrimaryButton ? () { + longPress += 1; + } : null, + onSecondaryLongPress: ButtonVariant.button == kSecondaryButton ? () { + longPress += 1; + } : null, + ), + ), + ), + ); + + // Pointer is dragged from the center of the 800x100 gesture detector + // to a point (400,300) below it. This should never call onTap. + Future dragOut(Duration timeout) async { + final TestGesture gesture = + await tester.startGesture(const Offset(400.0, 50.0), buttons: ButtonVariant.button); + // If the timeout is less than kPressTimeout the recognizer will not + // trigger any callbacks. If the timeout is greater than kLongPressTimeout + // then onTapDown, onLongPress, and onCancel will be called. + await tester.pump(timeout); + await gesture.moveTo(const Offset(400.0, 300.0)); + await gesture.up(); + } + + await dragOut(kPressTimeout * 0.5); // generates nothing + expect(tapDown, 0); + expect(tapCancel, 0); + expect(tap, 0); + expect(longPress, 0); + + await dragOut(kPressTimeout); // generates tapDown, tapCancel + expect(tapDown, 1); + expect(tapCancel, 1); + expect(tap, 0); + expect(longPress, 0); + + await dragOut(kLongPressTimeout); // generates tapDown, longPress, tapCancel + expect(tapDown, 2); + expect(tapCancel, 2); + expect(tap, 0); + expect(longPress, 1); + }, variant: buttonVariant); + + testWidgets('Long Press Up Callback called after long press', (WidgetTester tester) async { + int longPressUp = 0; + + await tester.pumpWidget( + Container( + alignment: Alignment.topLeft, + child: Container( + alignment: Alignment.center, + height: 100.0, + color: const Color(0xFF00FF00), + child: GestureDetector( + onLongPressUp: ButtonVariant.button == kPrimaryButton ? () { + longPressUp += 1; + } : null, + onSecondaryLongPressUp: ButtonVariant.button == kSecondaryButton ? () { + longPressUp += 1; + } : null, + ), + ), + ), + ); + + Future longPress(Duration timeout) async { + final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0), buttons: ButtonVariant.button); + await tester.pump(timeout); + await gesture.up(); + } + + await longPress(kLongPressTimeout + const Duration(seconds: 1)); // To make sure the time for long press has occurred + expect(longPressUp, 1); + }, variant: buttonVariant); }); testWidgets('Force Press Callback called after force press', (WidgetTester tester) async { @@ -707,3 +743,36 @@ class _EmptySemanticsGestureDelegate extends SemanticsGestureDelegate { void assignSemantics(RenderSemanticsGestureHandler renderObject) { } } + +/// A [TestVariant] that runs tests multiple times with different buttons. +class ButtonVariant extends TestVariant { + const ButtonVariant({ + @required this.values, + @required this.descriptions, + }) : assert(values.length != 0); // ignore: prefer_is_empty + + @override + final List values; + + final Map descriptions; + + static int button; + + @override + String describeValue(int value) { + assert(descriptions.containsKey(value), 'Unknown button'); + return descriptions[value]; + } + + @override + Future setUp(int value) async { + final int oldValue = button; + button = value; + return oldValue; + } + + @override + Future tearDown(int value, int memento) async { + button = memento; + } +}