Unverified Commit 6a75dc44 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add onSecondaryTap to gesture recognizer and gesture detector. (#55494)

parent f1c24ed9
......@@ -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:
///
......
......@@ -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<void>('onLongPressStart',
() => onLongPressStart(details));
switch (_initialButtons) {
case kPrimaryButton:
if (onLongPressStart != null) {
final LongPressStartDetails details = LongPressStartDetails(
globalPosition: _longPressOrigin.global,
localPosition: _longPressOrigin.local,
);
invokeCallback<void>('onLongPressStart', () => onLongPressStart(details));
}
if (onLongPress != null) {
invokeCallback<void>('onLongPress', onLongPress);
}
break;
case kSecondaryButton:
if (onSecondaryLongPressStart != null) {
final LongPressStartDetails details = LongPressStartDetails(
globalPosition: _longPressOrigin.global,
localPosition: _longPressOrigin.local,
);
invokeCallback<void>(
'onSecondaryLongPressStart', () => onSecondaryLongPressStart(details));
}
if (onSecondaryLongPress != null) {
invokeCallback<void>('onSecondaryLongPress', onSecondaryLongPress);
}
break;
default:
assert(false, 'Unhandled button $_initialButtons');
}
if (onLongPress != null)
invokeCallback<void>('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<void>('onLongPressMoveUpdate',
() => onLongPressMoveUpdate(details));
switch (_initialButtons) {
case kPrimaryButton:
if (onLongPressMoveUpdate != null) {
invokeCallback<void>('onLongPressMoveUpdate',
() => onLongPressMoveUpdate(details));
}
break;
case kSecondaryButton:
if (onSecondaryLongPressMoveUpdate != null) {
invokeCallback<void>('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<void>('onLongPressEnd', () => onLongPressEnd(details));
if (onLongPressUp != null)
invokeCallback<void>('onLongPressUp', onLongPressUp);
switch (_initialButtons) {
case kPrimaryButton:
if (onLongPressEnd != null) {
invokeCallback<void>('onLongPressEnd', () => onLongPressEnd(details));
}
if (onLongPressUp != null) {
invokeCallback<void>('onLongPressUp', onLongPressUp);
}
break;
case kSecondaryButton:
if (onSecondaryLongPressEnd != null) {
invokeCallback<void>('onSecondaryLongPressEnd', () => onSecondaryLongPressEnd(details));
}
if (onSecondaryLongPressUp != null) {
invokeCallback<void>('onSecondaryLongPressUp', onSecondaryLongPressUp);
}
break;
default:
assert(false, 'Unhandled button $_initialButtons');
}
}
void _reset() {
......
......@@ -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<void>('onSecondaryTapDown',
() => onSecondaryTapDown(details));
invokeCallback<void>('onSecondaryTapDown', () => onSecondaryTapDown(details));
break;
default:
}
......@@ -505,8 +523,9 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer {
break;
case kSecondaryButton:
if (onSecondaryTapUp != null)
invokeCallback<void>('onSecondaryTapUp',
() => onSecondaryTapUp(details));
invokeCallback<void>('onSecondaryTapUp', () => onSecondaryTapUp(details));
if (onSecondaryTap != null)
invokeCallback<void>('onSecondaryTap', () => onSecondaryTap());
break;
default:
}
......@@ -523,8 +542,7 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer {
break;
case kSecondaryButton:
if (onSecondaryTapCancel != null)
invokeCallback<void>('${note}onSecondaryTapCancel',
onSecondaryTapCancel);
invokeCallback<void>('${note}onSecondaryTapCancel', onSecondaryTapCancel);
break;
default:
}
......
......@@ -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>(
() => 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
......
......@@ -125,222 +125,258 @@ void main() {
expect(didEndPan, isTrue);
});
testWidgets('Translucent', (WidgetTester tester) async {
bool didReceivePointerDown;
bool didTap;
Future<void> pumpWidgetTree(HitTestBehavior behavior) {
return tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
Listener(
onPointerDown: (_) {
didReceivePointerDown = true;
},
child: Container(
group('Tap', () {
final ButtonVariant buttonVariant = ButtonVariant(
values: <int>[kPrimaryButton, kSecondaryButton],
descriptions: <int, String>{
kPrimaryButton: 'primary',
kSecondaryButton: 'secondary',
},
);
testWidgets('Translucent', (WidgetTester tester) async {
bool didReceivePointerDown;
bool didTap;
Future<void> pumpWidgetTree(HitTestBehavior behavior) {
return tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
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),
),
),
Container(
width: 100.0,
height: 100.0,
child: GestureDetector(
onTap: () {
didTap = true;
},
behavior: behavior,
child: GestureDetector(
onTap: ButtonVariant.button == kPrimaryButton ? () {
didTap = true;
} : null,
onSecondaryTap: ButtonVariant.button == kSecondaryButton ? () {
didTap = true;
} : null,
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,
),
),
);
}
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(),
expect(didTap, isFalse);
await tester.tapAt(const Offset(10.0, 10.0), buttons: ButtonVariant.button);
expect(didTap, isTrue);
}, variant: buttonVariant);
testWidgets('Only container', (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,
child: Container(),
),
),
),
);
);
expect(didTap, isFalse);
await tester.tapAt(const Offset(10.0, 10.0));
expect(didTap, isFalse);
}, variant: buttonVariant);
final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector));
testWidgets('cache render object', (WidgetTester tester) async {
final GestureTapCallback inputCallback = () { };
await tester.pumpWidget(
Center(
child: GestureDetector(
onTap: inputCallback,
child: Container(),
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));
});
);
testWidgets('Tap down occurs after kPressTimeout', (WidgetTester tester) async {
int tapDown = 0;
int tap = 0;
int tapCancel = 0;
int longPress = 0;
final RenderSemanticsGestureHandler renderObj1 = tester.renderObject(find.byType(GestureDetector));
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(
onTapDown: (TapDownDetails details) {
tapDown += 1;
},
onTap: () {
tap += 1;
},
onTapCancel: () {
tapCancel += 1;
},
onLongPress: () {
longPress += 1;
},
onTap: ButtonVariant.button == kPrimaryButton ? inputCallback : null,
onSecondaryTap: ButtonVariant.button == kSecondaryButton ? inputCallback : null,
child: Container(),
),
),
),
);
// Pointer is dragged from the center of the 800x100 gesture detector
// to a point (400,300) below it. This should never call onTap.
Future<void> 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();
}
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;
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,
),
),
),
);
await tester.pumpWidget(
Container(
alignment: Alignment.topLeft,
child: Container(
alignment: Alignment.center,
height: 100.0,
color: const Color(0xFF00FF00),
child: GestureDetector(
onLongPressUp: () {
longPressUp += 1;
},
// Pointer is dragged from the center of the 800x100 gesture detector
// to a point (400,300) below it. This should never call onTap.
Future<void> 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<void> longPress(Duration timeout) async {
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 50.0));
await tester.pump(timeout);
await gesture.up();
}
Future<void> 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);
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<int> {
const ButtonVariant({
@required this.values,
@required this.descriptions,
}) : assert(values.length != 0); // ignore: prefer_is_empty
@override
final List<int> values;
final Map<int, String> descriptions;
static int button;
@override
String describeValue(int value) {
assert(descriptions.containsKey(value), 'Unknown button');
return descriptions[value];
}
@override
Future<int> setUp(int value) async {
final int oldValue = button;
button = value;
return oldValue;
}
@override
Future<void> tearDown(int value, int memento) async {
button = memento;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment