Add onLongPressDown, onLongPressCancel (#81260)

This adds support for GestureDetector.onLongPressDown and
GestureDetector.onLongPressCancel, allowing callers to register
interest in the initial pointer contact that might turn into
a long-press (and the associated cancel event if the gesture
loses).
This commit is contained in:
Todd Volkert 2021-04-28 12:51:27 -07:00 committed by GitHub
parent 99f19d05c3
commit 91de4bd3e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 792 additions and 237 deletions

View file

@ -9,16 +9,50 @@ import 'events.dart';
import 'recognizer.dart';
import 'velocity_tracker.dart';
/// Callback signature for [LongPressGestureRecognizer.onLongPressDown].
///
/// Called when a pointer that might cause a long-press has contacted the
/// screen. The position at which the pointer contacted the screen is available
/// in the `details`.
///
/// See also:
///
/// * [GestureDetector.onLongPressDown], which matches this signature.
/// * [GestureLongPressStartCallback], the signature that gets called when the
/// pointer has been in contact with the screen long enough to be considered
/// a long-press.
typedef GestureLongPressDownCallback = void Function(LongPressDownDetails details);
/// Callback signature for [LongPressGestureRecognizer.onLongPressCancel].
///
/// Called when the pointer that previously triggered a
/// [GestureLongPressDownCallback] will not end up causing a long-press.
///
/// See also:
///
/// * [GestureDetector.onLongPressCancel], which matches this signature.
typedef GestureLongPressCancelCallback = void Function();
/// Callback signature for [LongPressGestureRecognizer.onLongPress].
///
/// Called when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// See also:
///
/// * [GestureDetector.onLongPress], which matches this signature.
/// * [GestureLongPressStartCallback], which is the same signature but with
/// details of where the long press occurred.
typedef GestureLongPressCallback = void Function();
/// Callback signature for [LongPressGestureRecognizer.onLongPressUp].
///
/// Called when a pointer stops contacting the screen after a long press
/// gesture was detected.
///
/// See also:
///
/// * [GestureDetector.onLongPressUp], which matches this signature.
typedef GestureLongPressUpCallback = void Function();
/// Callback signature for [LongPressGestureRecognizer.onLongPressStart].
@ -26,6 +60,12 @@ typedef GestureLongPressUpCallback = void Function();
/// Called when a pointer has remained in contact with the screen at the
/// same location for a long period of time. Also reports the long press down
/// position.
///
/// See also:
///
/// * [GestureDetector.onLongPressStart], which matches this signature.
/// * [GestureLongPressCallback], which is the same signature without the
/// details.
typedef GestureLongPressStartCallback = void Function(LongPressStartDetails details);
/// Callback signature for [LongPressGestureRecognizer.onLongPressMoveUpdate].
@ -33,6 +73,10 @@ typedef GestureLongPressStartCallback = void Function(LongPressStartDetails deta
/// Called when a pointer is moving after being held in contact at the same
/// location for a long period of time. Reports the new position and its offset
/// from the original down position.
///
/// See also:
///
/// * [GestureDetector.onLongPressMoveUpdate], which matches this signature.
typedef GestureLongPressMoveUpdateCallback = void Function(LongPressMoveUpdateDetails details);
/// Callback signature for [LongPressGestureRecognizer.onLongPressEnd].
@ -40,8 +84,46 @@ typedef GestureLongPressMoveUpdateCallback = void Function(LongPressMoveUpdateDe
/// Called when a pointer stops contacting the screen after a long press
/// gesture was detected. Also reports the position where the pointer stopped
/// contacting the screen.
///
/// See also:
///
/// * [GestureDetector.onLongPressEnd], which matches this signature.
typedef GestureLongPressEndCallback = void Function(LongPressEndDetails details);
/// Details for callbacks that use [GestureLongPressDownCallback].
///
/// See also:
///
/// * [LongPressGestureRecognizer.onLongPressDown], whose callback passes
/// these details.
/// * [LongPressGestureRecognizer.onSecondaryLongPressDown], whose callback
/// passes these details.
/// * [LongPressGestureRecognizer.onTertiaryLongPressDown], whose callback
/// passes these details.
class LongPressDownDetails {
/// Creates the details for a [GestureLongPressDownCallback].
///
/// The `globalPosition` argument must not be null.
///
/// If the `localPosition` argument is not specified, it will default to the
/// global position.
const LongPressDownDetails({
this.globalPosition = Offset.zero,
Offset? localPosition,
this.kind,
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
final Offset globalPosition;
/// The kind of the device that initiated the event.
final PointerDeviceKind? kind;
/// The local position at which the pointer contacted the screen.
final Offset localPosition;
}
/// Details for callbacks that use [GestureLongPressStartCallback].
///
/// See also:
@ -59,10 +141,10 @@ class LongPressStartDetails {
}) : assert(globalPosition != null),
localPosition = localPosition ?? globalPosition;
/// The global position at which the pointer contacted the screen.
/// The global position at which the pointer initially contacted the screen.
final Offset globalPosition;
/// The local position at which the pointer contacted the screen.
/// The local position at which the pointer initially contacted the screen.
final Offset localPosition;
}
@ -178,21 +260,63 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
// different set of buttons, the gesture is canceled.
int? _initialButtons;
/// Called when a long press gesture by a primary button has been recognized.
/// Called when a pointer has contacted the screen at a particular location
/// with a primary button, which might be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If this recognizer doesn't win the arena, [onLongPressCancel] is called
/// next. Otherwise, [onLongPressStart] is called next.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryLongPressDown], a similar callback but for a secondary button.
/// * [onTertiaryLongPressDown], a similar callback but for a tertiary button.
/// * [LongPressDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onLongPressDown], which exposes this callback in a widget.
GestureLongPressDownCallback? onLongPressDown;
/// Called when a pointer that previously triggered [onLongPressDown] will
/// not end up causing a long-press.
///
/// This triggers once the gesture loses the arena if [onLongPressDown] has
/// previously been triggered.
///
/// If this recognizer wins the arena, [onLongPressStart] and [onLongPress]
/// are called instead.
///
/// If the gesture is deactivated due to [postAcceptSlopTolerance] having
/// been exceeded, this callback will not be called, since the gesture will
/// have already won the arena at that point.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
GestureLongPressCancelCallback? onLongPressCancel;
/// Called when a long press gesture by a primary button has been recognized.
///
/// This is equivalent to (and is called immediately after) [onLongPressStart].
/// The only difference between the two is that this callback does not
/// contain details of the position at which the pointer initially contacted
/// the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressStart], which has the same timing but has data for the
/// press location.
GestureLongPressCallback? onLongPress;
/// Called when a long press gesture by a primary button has been recognized.
///
/// This is equivalent to (and is called immediately before) [onLongPress].
/// The only difference between the two is that this callback contains
/// details of the position at which the pointer initially contacted the
/// screen, whereas [onLongPress] does not.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPress], which has the same timing but without details.
/// * [LongPressStartDetails], which is passed as an argument to this callback.
GestureLongPressStartCallback? onLongPressStart;
@ -208,40 +332,90 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Called when the pointer stops contacting the screen after a long-press
/// by a primary button.
///
/// This is equivalent to (and is called immediately after) [onLongPressEnd].
/// The only difference between the two is that this callback does not
/// contain details of the state of the pointer when it stopped contacting
/// the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressEnd], which has the same timing but has data for the up
/// gesture location.
GestureLongPressUpCallback? onLongPressUp;
/// Called when the pointer stops contacting the screen after a long-press
/// by a primary button.
///
/// This is equivalent to (and is called immediately before) [onLongPressUp].
/// The only difference between the two is that this callback contains
/// details of the state of the pointer when it stopped contacting the
/// screen, whereas [onLongPressUp] does not.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressUp], which has the same timing, but without details.
/// * [LongPressEndDetails], which is passed as an argument to this
/// callback.
GestureLongPressEndCallback? onLongPressEnd;
/// Called when a long press gesture by a secondary button has been
/// recognized.
/// Called when a pointer has contacted the screen at a particular location
/// with a secondary button, which might be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If this recognizer doesn't win the arena, [onSecondaryLongPressCancel] is
/// called next. Otherwise, [onSecondaryLongPressStart] is called next.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onLongPressDown], a similar callback but for a primary button.
/// * [onTertiaryLongPressDown], a similar callback but for a tertiary button.
/// * [LongPressDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onSecondaryLongPressDown], which exposes this callback
/// in a widget.
GestureLongPressDownCallback? onSecondaryLongPressDown;
/// Called when a pointer that previously triggered [onSecondaryLongPressDown]
/// will not end up causing a long-press.
///
/// This triggers once the gesture loses the arena if
/// [onSecondaryLongPressDown] has previously been triggered.
///
/// If this recognizer wins the arena, [onSecondaryLongPressStart] and
/// [onSecondaryLongPress] are called instead.
///
/// If the gesture is deactivated due to [postAcceptSlopTolerance] having
/// been exceeded, this callback will not be called, since the gesture will
/// have already won the arena at that point.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
GestureLongPressCancelCallback? onSecondaryLongPressCancel;
/// Called when a long press gesture by a secondary button has been
/// recognized.
///
/// This is equivalent to (and is called immediately after)
/// [onSecondaryLongPressStart]. The only difference between the two is that
/// this callback does not contain details of the position at which the
/// pointer initially contacted the screen.
///
/// 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.
///
/// This is equivalent to (and is called immediately before)
/// [onSecondaryLongPress]. The only difference between the two is that this
/// callback contains details of the position at which the pointer initially
/// contacted the screen, whereas [onSecondaryLongPress] does not.
///
/// 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;
@ -259,40 +433,89 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Called when the pointer stops contacting the screen after a long-press by
/// a secondary button.
///
/// This is equivalent to (and is called immediately after)
/// [onSecondaryLongPressEnd]. The only difference between the two is that
/// this callback does not contain details of the state of the pointer when
/// it stopped contacting the screen.
///
/// 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.
///
/// This is equivalent to (and is called immediately before)
/// [onSecondaryLongPressUp]. The only difference between the two is that
/// this callback contains details of the state of the pointer when it
/// stopped contacting the screen, whereas [onSecondaryLongPressUp] does not.
///
/// 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;
/// Called when a pointer has contacted the screen at a particular location
/// with a tertiary button, which might be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If this recognizer doesn't win the arena, [onTertiaryLongPressCancel] is
/// called next. Otherwise, [onTertiaryLongPressStart] is called next.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [onLongPressDown], a similar callback but for a primary button.
/// * [onSecondaryLongPressDown], a similar callback but for a secondary button.
/// * [LongPressDownDetails], which is passed as an argument to this callback.
/// * [GestureDetector.onTertiaryLongPressDown], which exposes this callback
/// in a widget.
GestureLongPressDownCallback? onTertiaryLongPressDown;
/// Called when a pointer that previously triggered [onTertiaryLongPressDown]
/// will not end up causing a long-press.
///
/// This triggers once the gesture loses the arena if
/// [onTertiaryLongPressDown] has previously been triggered.
///
/// If this recognizer wins the arena, [onTertiaryLongPressStart] and
/// [onTertiaryLongPress] are called instead.
///
/// If the gesture is deactivated due to [postAcceptSlopTolerance] having
/// been exceeded, this callback will not be called, since the gesture will
/// have already won the arena at that point.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
GestureLongPressCancelCallback? onTertiaryLongPressCancel;
/// Called when a long press gesture by a tertiary button has been
/// recognized.
///
/// This is equivalent to (and is called immediately after)
/// [onTertiaryLongPressStart]. The only difference between the two is that
/// this callback does not contain details of the position at which the
/// pointer initially contacted the screen.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [onTertiaryLongPressStart], which has the same timing but has data for
/// the press location.
GestureLongPressCallback? onTertiaryLongPress;
/// Called when a long press gesture by a tertiary button has been recognized.
///
/// This is equivalent to (and is called immediately before)
/// [onTertiaryLongPress]. The only difference between the two is that this
/// callback contains details of the position at which the pointer initially
/// contacted the screen, whereas [onTertiaryLongPress] does not.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [onTertiaryLongPress], which has the same timing but without details.
/// * [LongPressStartDetails], which is passed as an argument to this
/// callback.
GestureLongPressStartCallback? onTertiaryLongPressStart;
@ -310,21 +533,27 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// Called when the pointer stops contacting the screen after a long-press by
/// a tertiary button.
///
/// This is equivalent to (and is called immediately after)
/// [onTertiaryLongPressEnd]. The only difference between the two is that
/// this callback does not contain details of the state of the pointer when
/// it stopped contacting the screen.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [onTertiaryLongPressEnd], which has the same timing but has data for
/// the up gesture location.
GestureLongPressUpCallback? onTertiaryLongPressUp;
/// Called when the pointer stops contacting the screen after a long-press by
/// a tertiary button.
///
/// This is equivalent to (and is called immediately before)
/// [onTertiaryLongPressUp]. The only difference between the two is that
/// this callback contains details of the state of the pointer when it
/// stopped contacting the screen, whereas [onTertiaryLongPressUp] does not.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [onTertiaryLongPressUp], which has the same timing, but without
/// details.
/// * [LongPressEndDetails], which is passed as an argument to this callback.
GestureLongPressEndCallback? onTertiaryLongPressEnd;
@ -334,7 +563,9 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
bool isPointerAllowed(PointerDownEvent event) {
switch (event.buttons) {
case kPrimaryButton:
if (onLongPressStart == null &&
if (onLongPressDown == null &&
onLongPressCancel == null &&
onLongPressStart == null &&
onLongPress == null &&
onLongPressMoveUpdate == null &&
onLongPressEnd == null &&
@ -342,7 +573,9 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
return false;
break;
case kSecondaryButton:
if (onSecondaryLongPressStart == null &&
if (onSecondaryLongPressDown == null &&
onSecondaryLongPressCancel == null &&
onSecondaryLongPressStart == null &&
onSecondaryLongPress == null &&
onSecondaryLongPressMoveUpdate == null &&
onSecondaryLongPressEnd == null &&
@ -350,7 +583,9 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
return false;
break;
case kTertiaryButton:
if (onTertiaryLongPressStart == null &&
if (onTertiaryLongPressDown == null &&
onTertiaryLongPressCancel == null &&
onTertiaryLongPressStart == null &&
onTertiaryLongPress == null &&
onTertiaryLongPressMoveUpdate == null &&
onTertiaryLongPressEnd == null &&
@ -394,11 +629,13 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
}
_reset();
} else if (event is PointerCancelEvent) {
_checkLongPressCancel();
_reset();
} else if (event is PointerDownEvent) {
// The first touch.
_longPressOrigin = OffsetPair.fromEventPosition(event);
_initialButtons = event.buttons;
_checkLongPressDown(event);
} else if (event is PointerMoveEvent) {
if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
@ -409,6 +646,52 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
}
}
void _checkLongPressDown(PointerDownEvent event) {
assert(_longPressOrigin != null);
final LongPressDownDetails details = LongPressDownDetails(
globalPosition: _longPressOrigin!.global,
localPosition: _longPressOrigin!.local,
kind: getKindForPointer(event.pointer),
);
switch (_initialButtons) {
case kPrimaryButton:
if (onLongPressDown != null)
invokeCallback<void>('onLongPressDown', () => onLongPressDown!(details));
break;
case kSecondaryButton:
if (onSecondaryLongPressDown != null)
invokeCallback<void>('onSecondaryLongPressDown', () => onSecondaryLongPressDown!(details));
break;
case kTertiaryButton:
if (onTertiaryLongPressDown != null)
invokeCallback<void>('onTertiaryLongPressDown', () => onTertiaryLongPressDown!(details));
break;
default:
assert(false, 'Unhandled button $_initialButtons');
}
}
void _checkLongPressCancel() {
if (state == GestureRecognizerState.possible) {
switch (_initialButtons) {
case kPrimaryButton:
if (onLongPressCancel != null)
invokeCallback<void>('onLongPressCancel', onLongPressCancel!);
break;
case kSecondaryButton:
if (onSecondaryLongPressCancel != null)
invokeCallback<void>('onSecondaryLongPressCancel', onSecondaryLongPressCancel!);
break;
case kTertiaryButton:
if (onTertiaryLongPressCancel != null)
invokeCallback<void>('onTertiaryLongPressCancel', onTertiaryLongPressCancel!);
break;
default:
assert(false, 'Unhandled button $_initialButtons');
}
}
}
void _checkLongPressStart() {
switch (_initialButtons) {
case kPrimaryButton:
@ -531,10 +814,14 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
@override
void resolve(GestureDisposition disposition) {
if (_longPressAccepted && disposition == GestureDisposition.rejected) {
// This can happen if the gesture has been canceled. For example when
// the buttons have changed.
_reset();
if (disposition == GestureDisposition.rejected) {
if (_longPressAccepted) {
// This can happen if the gesture has been canceled. For example when
// the buttons have changed.
_reset();
} else {
_checkLongPressCancel();
}
}
super.resolve(disposition);
}

View file

@ -394,6 +394,7 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer {
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryTap], a similar callback but for a secondary button.
/// * [onTapUp], which has the same timing but with details.
/// * [GestureDetector.onTap], which exposes this callback.
GestureTapCallback? onTap;

View file

@ -230,16 +230,27 @@ class GestureDetector extends StatelessWidget {
this.onDoubleTapDown,
this.onDoubleTap,
this.onDoubleTapCancel,
this.onLongPressDown,
this.onLongPressCancel,
this.onLongPress,
this.onLongPressStart,
this.onLongPressMoveUpdate,
this.onLongPressUp,
this.onLongPressEnd,
this.onSecondaryLongPressDown,
this.onSecondaryLongPressCancel,
this.onSecondaryLongPress,
this.onSecondaryLongPressStart,
this.onSecondaryLongPressMoveUpdate,
this.onSecondaryLongPressUp,
this.onSecondaryLongPressEnd,
this.onTertiaryLongPressDown,
this.onTertiaryLongPressCancel,
this.onTertiaryLongPress,
this.onTertiaryLongPressStart,
this.onTertiaryLongPressMoveUpdate,
this.onTertiaryLongPressUp,
this.onTertiaryLongPressEnd,
this.onVerticalDragDown,
this.onVerticalDragStart,
this.onVerticalDragUpdate,
@ -458,15 +469,55 @@ class GestureDetector extends StatelessWidget {
/// * [kPrimaryButton], the button this callback responds to.
final GestureTapCancelCallback? onDoubleTapCancel;
/// The pointer has contacted the screen with a primary button, which might
/// be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If the user completes the long-press, and this gesture wins,
/// [onLongPressStart] will be called after this callback. Otherwise,
/// [onLongPressCancel] will be called after this callback.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onSecondaryLongPressDown], a similar callback but for a secondary button.
/// * [onTertiaryLongPressDown], a similar callback but for a tertiary button.
/// * [LongPressGestureRecognizer.onLongPressDown], which exposes this
/// callback at the gesture layer.
final GestureLongPressDownCallback? onLongPressDown;
/// A pointer that previously triggered [onLongPressDown] will not end up
/// causing a long-press.
///
/// This triggers once the gesture loses if [onLongPressDown] has previously
/// been triggered.
///
/// If the user completed the long-press, and the gesture won, then
/// [onLongPressStart] and [onLongPress] are called instead.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPressCancel], which exposes this
/// callback at the gesture layer.
final GestureLongPressCancelCallback? onLongPressCancel;
/// Called when a long press gesture with a primary 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.
///
/// This is equivalent to (and is called immediately after) [onLongPressStart].
/// The only difference between the two is that this callback does not
/// contain details of the position at which the pointer initially contacted
/// the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressStart], which has the same timing but has gesture details.
/// * [LongPressGestureRecognizer.onLongPress], which exposes this
/// callback at the gesture layer.
final GestureLongPressCallback? onLongPress;
/// Called when a long press gesture with a primary button has been recognized.
@ -474,49 +525,107 @@ class GestureDetector extends StatelessWidget {
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately before) [onLongPress].
/// The only difference between the two is that this callback contains
/// details of the position at which the pointer initially contacted the
/// screen, whereas [onLongPress] does not.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPress], which has the same timing but without the gesture details.
/// * [LongPressGestureRecognizer.onLongPressStart], which exposes this
/// callback at the gesture layer.
final GestureLongPressStartCallback? onLongPressStart;
/// A pointer has been drag-moved after a long press with a primary button.
/// A pointer has been drag-moved after a long-press with a primary button.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onLongPressMoveUpdate], which exposes this
/// callback at the gesture layer.
final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate;
/// A pointer that has triggered a long-press with a primary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately after) [onLongPressEnd].
/// The only difference between the two is that this callback does not
/// contain details of the state of the pointer when it stopped contacting
/// the screen.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressEnd], which has the same timing but has gesture details.
/// * [LongPressGestureRecognizer.onLongPressUp], which exposes this
/// callback at the gesture layer.
final GestureLongPressUpCallback? onLongPressUp;
/// A pointer that has triggered a long-press with a primary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately before) [onLongPressUp].
/// The only difference between the two is that this callback contains
/// details of the state of the pointer when it stopped contacting the
/// screen, whereas [onLongPressUp] does not.
///
/// See also:
///
/// * [kPrimaryButton], the button this callback responds to.
/// * [onLongPressUp], which has the same timing but without the gesture
/// details.
/// * [LongPressGestureRecognizer.onLongPressEnd], which exposes this
/// callback at the gesture layer.
final GestureLongPressEndCallback? onLongPressEnd;
/// The pointer has contacted the screen with a secondary button, which might
/// be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If the user completes the long-press, and this gesture wins,
/// [onSecondaryLongPressStart] will be called after this callback. Otherwise,
/// [onSecondaryLongPressCancel] will be called after this callback.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onLongPressDown], a similar callback but for a secondary button.
/// * [onTertiaryLongPressDown], a similar callback but for a tertiary button.
/// * [LongPressGestureRecognizer.onSecondaryLongPressDown], which exposes
/// this callback at the gesture layer.
final GestureLongPressDownCallback? onSecondaryLongPressDown;
/// A pointer that previously triggered [onSecondaryLongPressDown] will not
/// end up causing a long-press.
///
/// This triggers once the gesture loses if [onSecondaryLongPressDown] has
/// previously been triggered.
///
/// If the user completed the long-press, and the gesture won, then
/// [onSecondaryLongPressStart] and [onSecondaryLongPress] are called instead.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPressCancel], which exposes
/// this callback at the gesture layer.
final GestureLongPressCancelCallback? onSecondaryLongPressCancel;
/// 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.
///
/// This is equivalent to (and is called immediately after)
/// [onSecondaryLongPressStart]. The only difference between the two is that
/// this callback does not contain details of the position at which the
/// pointer initially contacted the screen.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onSecondaryLongPressStart], which has the same timing but has gesture
/// details.
/// * [LongPressGestureRecognizer.onSecondaryLongPress], which exposes
/// this callback at the gesture layer.
final GestureLongPressCallback? onSecondaryLongPress;
/// Called when a long press gesture with a secondary button has been
@ -525,11 +634,16 @@ class GestureDetector extends StatelessWidget {
/// Triggered when a pointer has remained in contact with the screen at the
/// same location for a long period of time.
///
/// This is equivalent to (and is called immediately before)
/// [onSecondaryLongPress]. The only difference between the two is that this
/// callback contains details of the position at which the pointer initially
/// contacted the screen, whereas [onSecondaryLongPress] does not.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onSecondaryLongPress], which has the same timing but without the
/// gesture details.
/// * [LongPressGestureRecognizer.onSecondaryLongPressStart], which exposes
/// this callback at the gesture layer.
final GestureLongPressStartCallback? onSecondaryLongPressStart;
/// A pointer has been drag-moved after a long press with a secondary button.
@ -537,28 +651,149 @@ class GestureDetector extends StatelessWidget {
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onSecondaryLongPressMoveUpdate], which exposes
/// this callback at the gesture layer.
final GestureLongPressMoveUpdateCallback? onSecondaryLongPressMoveUpdate;
/// A pointer that has triggered a long-press with a secondary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately after)
/// [onSecondaryLongPressEnd]. The only difference between the two is that
/// this callback does not contain details of the state of the pointer when
/// it stopped contacting the screen.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onSecondaryLongPressEnd], which has the same timing but has gesture
/// details.
/// * [LongPressGestureRecognizer.onSecondaryLongPressUp], which exposes
/// this callback at the gesture layer.
final GestureLongPressUpCallback? onSecondaryLongPressUp;
/// A pointer that has triggered a long-press with a secondary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately before)
/// [onSecondaryLongPressUp]. The only difference between the two is that
/// this callback contains details of the state of the pointer when it
/// stopped contacting the screen, whereas [onSecondaryLongPressUp] does not.
///
/// See also:
///
/// * [kSecondaryButton], the button this callback responds to.
/// * [onSecondaryLongPressUp], which has the same timing but without the
/// gesture details.
/// * [LongPressGestureRecognizer.onSecondaryLongPressEnd], which exposes
/// this callback at the gesture layer.
final GestureLongPressEndCallback? onSecondaryLongPressEnd;
/// The pointer has contacted the screen with a tertiary button, which might
/// be the start of a long-press.
///
/// This triggers after the pointer down event.
///
/// If the user completes the long-press, and this gesture wins,
/// [onTertiaryLongPressStart] will be called after this callback. Otherwise,
/// [onTertiaryLongPressCancel] will be called after this callback.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [onLongPressDown], a similar callback but for a primary button.
/// * [onSecondaryLongPressDown], a similar callback but for a secondary button.
/// * [LongPressGestureRecognizer.onTertiaryLongPressDown], which exposes
/// this callback at the gesture layer.
final GestureLongPressDownCallback? onTertiaryLongPressDown;
/// A pointer that previously triggered [onTertiaryLongPressDown] will not
/// end up causing a long-press.
///
/// This triggers once the gesture loses if [onTertiaryLongPressDown] has
/// previously been triggered.
///
/// If the user completed the long-press, and the gesture won, then
/// [onTertiaryLongPressStart] and [onTertiaryLongPress] are called instead.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressCancel], which exposes
/// this callback at the gesture layer.
final GestureLongPressCancelCallback? onTertiaryLongPressCancel;
/// Called when a long press gesture with a tertiary 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.
///
/// This is equivalent to (and is called immediately after)
/// [onTertiaryLongPressStart]. The only difference between the two is that
/// this callback does not contain details of the position at which the
/// pointer initially contacted the screen.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPress], which exposes
/// this callback at the gesture layer.
final GestureLongPressCallback? onTertiaryLongPress;
/// Called when a long press gesture with a tertiary 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.
///
/// This is equivalent to (and is called immediately before)
/// [onTertiaryLongPress]. The only difference between the two is that this
/// callback contains details of the position at which the pointer initially
/// contacted the screen, whereas [onTertiaryLongPress] does not.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressStart], which exposes
/// this callback at the gesture layer.
final GestureLongPressStartCallback? onTertiaryLongPressStart;
/// A pointer has been drag-moved after a long press with a tertiary button.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressMoveUpdate], which exposes
/// this callback at the gesture layer.
final GestureLongPressMoveUpdateCallback? onTertiaryLongPressMoveUpdate;
/// A pointer that has triggered a long-press with a tertiary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately after)
/// [onTertiaryLongPressEnd]. The only difference between the two is that
/// this callback does not contain details of the state of the pointer when
/// it stopped contacting the screen.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressUp], which exposes
/// this callback at the gesture layer.
final GestureLongPressUpCallback? onTertiaryLongPressUp;
/// A pointer that has triggered a long-press with a tertiary button has
/// stopped contacting the screen.
///
/// This is equivalent to (and is called immediately before)
/// [onTertiaryLongPressUp]. The only difference between the two is that
/// this callback contains details of the state of the pointer when it
/// stopped contacting the screen, whereas [onTertiaryLongPressUp] does not.
///
/// See also:
///
/// * [kTertiaryButton], the button this callback responds to.
/// * [LongPressGestureRecognizer.onTertiaryLongPressEnd], which exposes
/// this callback at the gesture layer.
final GestureLongPressEndCallback? onTertiaryLongPressEnd;
/// A pointer has contacted the screen with a primary button and might begin
/// to move vertically.
///
@ -804,30 +1039,52 @@ class GestureDetector extends StatelessWidget {
);
}
if (onLongPress != null ||
onLongPressUp != null ||
if (onLongPressDown != null ||
onLongPressCancel != null ||
onLongPress != null ||
onLongPressStart != null ||
onLongPressMoveUpdate != null ||
onLongPressUp != null ||
onLongPressEnd != null ||
onSecondaryLongPressDown != null ||
onSecondaryLongPressCancel != null ||
onSecondaryLongPress != null ||
onSecondaryLongPressUp != null ||
onSecondaryLongPressStart != null ||
onSecondaryLongPressMoveUpdate != null ||
onSecondaryLongPressEnd != null) {
onSecondaryLongPressUp != null ||
onSecondaryLongPressEnd != null ||
onTertiaryLongPressDown != null ||
onTertiaryLongPressCancel != null ||
onTertiaryLongPress != null ||
onTertiaryLongPressStart != null ||
onTertiaryLongPressMoveUpdate != null ||
onTertiaryLongPressUp != null ||
onTertiaryLongPressEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(debugOwner: this),
(LongPressGestureRecognizer instance) {
instance
..onLongPressDown = onLongPressDown
..onLongPressCancel = onLongPressCancel
..onLongPress = onLongPress
..onLongPressStart = onLongPressStart
..onLongPressMoveUpdate = onLongPressMoveUpdate
..onLongPressEnd = onLongPressEnd
..onLongPressUp = onLongPressUp
..onLongPressEnd = onLongPressEnd
..onSecondaryLongPressDown = onSecondaryLongPressDown
..onSecondaryLongPressCancel = onSecondaryLongPressCancel
..onSecondaryLongPress = onSecondaryLongPress
..onSecondaryLongPressStart = onSecondaryLongPressStart
..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate
..onSecondaryLongPressUp = onSecondaryLongPressUp
..onSecondaryLongPressEnd = onSecondaryLongPressEnd
..onSecondaryLongPressUp = onSecondaryLongPressUp;
..onTertiaryLongPressDown = onTertiaryLongPressDown
..onTertiaryLongPressCancel = onTertiaryLongPressCancel
..onTertiaryLongPress = onTertiaryLongPress
..onTertiaryLongPressStart = onTertiaryLongPressStart
..onTertiaryLongPressMoveUpdate = onTertiaryLongPressMoveUpdate
..onTertiaryLongPressUp = onTertiaryLongPressUp
..onTertiaryLongPressEnd = onTertiaryLongPressEnd;
},
);
}
@ -1029,7 +1286,8 @@ class RawGestureDetector extends StatefulWidget {
/// * During a semantic tap, it calls [TapGestureRecognizer]'s
/// `onTapDown`, `onTapUp`, and `onTap`.
/// * During a semantic long press, it calls [LongPressGestureRecognizer]'s
/// `onLongPressStart`, `onLongPress`, `onLongPressEnd` and `onLongPressUp`.
/// `onLongPressDown`, `onLongPressStart`, `onLongPress`, `onLongPressEnd`
/// and `onLongPressUp`.
/// * During a semantic horizontal drag, it calls [HorizontalDragGestureRecognizer]'s
/// `onDown`, `onStart`, `onUpdate` and `onEnd`, then
/// [PanGestureRecognizer]'s `onDown`, `onStart`, `onUpdate` and `onEnd`.
@ -1344,6 +1602,7 @@ class _DefaultSemanticsGestureDelegate extends SemanticsGestureDelegate {
return null;
return () {
longPress.onLongPressDown?.call(const LongPressDownDetails());
longPress.onLongPressStart?.call(const LongPressStartDetails());
longPress.onLongPress?.call();
longPress.onLongPressEnd?.call(const LongPressEndDetails());

View file

@ -62,344 +62,342 @@ void main() {
setUp(ensureGestureBinding);
group('Long press', () {
late LongPressGestureRecognizer longPress;
late bool longPressDown;
late bool longPressUp;
late LongPressGestureRecognizer gesture;
late List<String> recognized;
void setUpHandlers() {
gesture
..onLongPressDown = (LongPressDownDetails details) {
recognized.add('down');
}
..onLongPressCancel = () {
recognized.add('cancel');
}
..onLongPress = () {
recognized.add('start');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('move');
}
..onLongPressUp = () {
recognized.add('end');
};
}
setUp(() {
longPress = LongPressGestureRecognizer();
longPressDown = false;
longPress.onLongPress = () {
longPressDown = true;
};
longPressUp = false;
longPress.onLongPressUp = () {
longPressUp = true;
};
recognized = <String>[];
gesture = LongPressGestureRecognizer();
setUpHandlers();
});
testGesture('Should recognize long press', (GestureTester tester) {
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(longPressDown, isTrue);
longPress.dispose();
expect(recognized, const <String>['down', 'start']);
gesture.dispose();
expect(recognized, const <String>['down', 'start']);
});
testGesture('Should recognize long press with altered duration', (GestureTester tester) {
longPress = LongPressGestureRecognizer(duration: const Duration(milliseconds: 100));
longPressDown = false;
longPress.onLongPress = () {
longPressDown = true;
};
longPressUp = false;
longPress.onLongPressUp = () {
longPressUp = true;
};
longPress.addPointer(down);
gesture = LongPressGestureRecognizer(duration: const Duration(milliseconds: 100));
setUpHandlers();
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 50));
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 50));
expect(longPressDown, isTrue);
longPress.dispose();
expect(recognized, const <String>['down', 'start']);
gesture.dispose();
expect(recognized, const <String>['down', 'start']);
});
testGesture('Up cancels long press', (GestureTester tester) {
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.route(up);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
expect(longPressDown, isFalse);
longPress.dispose();
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving before accept cancels', (GestureTester tester) {
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.route(move);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
tester.route(up);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isFalse);
expect(longPressUp, isFalse);
longPress.dispose();
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving after accept is ok', (GestureTester tester) {
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(seconds: 1));
expect(longPressDown, isTrue);
expect(recognized, const <String>['down', 'start']);
tester.route(move);
expect(recognized, const <String>['down', 'start', 'move']);
tester.route(up);
expect(recognized, const <String>['down', 'start', 'move', 'end']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isTrue);
expect(longPressUp, isTrue);
longPress.dispose();
expect(recognized, const <String>['down', 'start', 'move', 'end']);
gesture.dispose();
expect(recognized, const <String>['down', 'start', 'move', 'end']);
});
testGesture('Should recognize both tap down and long press', (GestureTester tester) {
final TapGestureRecognizer tap = TapGestureRecognizer();
bool tapDownRecognized = false;
tap.onTapDown = (_) {
tapDownRecognized = true;
recognized.add('tap_down');
};
tap.addPointer(down);
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(tapDownRecognized, isFalse);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(tapDownRecognized, isFalse);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(tapDownRecognized, isTrue);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down', 'tap_down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(tapDownRecognized, isTrue);
expect(longPressDown, isTrue);
expect(recognized, const <String>['down', 'tap_down', 'start']);
tap.dispose();
longPress.dispose();
gesture.dispose();
expect(recognized, const <String>['down', 'tap_down', 'start']);
});
testGesture('Drag start delayed by microtask', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
bool isDangerousStack = false;
bool dragStartRecognized = false;
drag.onStart = (DragStartDetails details) {
expect(isDangerousStack, isFalse);
dragStartRecognized = true;
recognized.add('drag_start');
};
drag.addPointer(down);
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(dragStartRecognized, isFalse);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(dragStartRecognized, isFalse);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(dragStartRecognized, isFalse);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
isDangerousStack = true;
longPress.dispose();
gesture.dispose();
isDangerousStack = false;
expect(dragStartRecognized, isFalse);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down', 'cancel']);
tester.async.flushMicrotasks();
expect(dragStartRecognized, isTrue);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down', 'cancel', 'drag_start']);
drag.dispose();
});
testGesture('Should recognize long press up', (GestureTester tester) {
bool longPressUpRecognized = false;
longPress.onLongPressUp = () {
longPressUpRecognized = true;
};
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressUpRecognized, isFalse);
expect(recognized, const <String>[]);
tester.route(down); // kLongPressTimeout = 500;
expect(longPressUpRecognized, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressUpRecognized, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(recognized, const <String>['down', 'start']);
tester.route(up);
expect(longPressUpRecognized, isTrue);
longPress.dispose();
expect(recognized, const <String>['down', 'start', 'end']);
gesture.dispose();
expect(recognized, const <String>['down', 'start', 'end']);
});
testGesture('Should not recognize long press with more than one buttons', (GestureTester tester) {
longPress.addPointer(const PointerDownEvent(
gesture.addPointer(const PointerDownEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kSecondaryMouseButton | kTertiaryButton,
position: Offset(10, 10),
));
tester.closeArena(5);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(up);
expect(longPressUp, isFalse);
longPress.dispose();
expect(recognized, const <String>[]);
gesture.dispose();
expect(recognized, const <String>[]);
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressDown, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressDown, isFalse);
expect(recognized, const <String>['down']);
tester.route(const PointerMoveEvent(
pointer: 5,
kind: PointerDeviceKind.mouse,
buttons: kTertiaryButton,
position: Offset(10, 10),
));
expect(longPressDown, isFalse);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(longPressDown, isFalse);
expect(recognized, const <String>['down', 'cancel']);
tester.route(up);
expect(longPressUp, isFalse);
longPress.dispose();
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('non-allowed pointer does not inadvertently reset the recognizer', (GestureTester tester) {
longPress = LongPressGestureRecognizer(kind: PointerDeviceKind.touch)..onLongPress = () {};
gesture = LongPressGestureRecognizer(kind: PointerDeviceKind.touch);
setUpHandlers();
// Accept a long-press gesture
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 500));
expect(recognized, const <String>['down', 'start']);
// Add a non-allowed pointer (doesn't match the kind filter)
longPress.addPointer(const PointerDownEvent(
gesture.addPointer(const PointerDownEvent(
pointer: 101,
kind: PointerDeviceKind.mouse,
position: Offset(10, 10),
));
expect(recognized, const <String>['down', 'start']);
// Moving the primary pointer should result in a normal event
tester.route(const PointerMoveEvent(
pointer: 5,
position: Offset(15, 15),
));
expect(recognized, const <String>['down', 'start', 'move']);
});
});
group('long press drag', () {
late LongPressGestureRecognizer longPressDrag;
late bool longPressStart;
late bool longPressUp;
late LongPressGestureRecognizer gesture;
Offset? longPressDragUpdate;
late List<String> recognized;
void setUpHandlers() {
gesture
..onLongPressDown = (LongPressDownDetails details) {
recognized.add('down');
}
..onLongPressCancel = () {
recognized.add('cancel');
}
..onLongPress = () {
recognized.add('start');
}
..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
recognized.add('move');
longPressDragUpdate = details.globalPosition;
}
..onLongPressUp = () {
recognized.add('end');
};
}
setUp(() {
longPressDrag = LongPressGestureRecognizer();
longPressStart = false;
longPressDrag.onLongPressStart = (LongPressStartDetails details) {
longPressStart = true;
};
longPressUp = false;
longPressDrag.onLongPressEnd = (LongPressEndDetails details) {
longPressUp = true;
};
longPressDragUpdate = null;
longPressDrag.onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) {
longPressDragUpdate = details.globalPosition;
};
gesture = LongPressGestureRecognizer();
setUpHandlers();
recognized = <String>[];
});
testGesture('Should recognize long press down', (GestureTester tester) {
longPressDrag.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressStart, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressStart, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressStart, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 700));
expect(longPressStart, isTrue);
longPressDrag.dispose();
expect(recognized, const <String>['down', 'start']);
gesture.dispose();
expect(recognized, const <String>['down', 'start']);
});
testGesture('Short up cancels long press', (GestureTester tester) {
longPressDrag.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressStart, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressStart, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressStart, isFalse);
expect(recognized, const <String>['down']);
tester.route(up);
expect(longPressStart, isFalse);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
expect(longPressStart, isFalse);
longPressDrag.dispose();
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving before accept cancels', (GestureTester tester) {
longPressDrag.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressStart, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressStart, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressStart, isFalse);
expect(recognized, const <String>['down']);
tester.route(move);
expect(longPressStart, isFalse);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(seconds: 1));
tester.route(up);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressStart, isFalse);
expect(longPressUp, isFalse);
longPressDrag.dispose();
expect(recognized, const <String>['down', 'cancel']);
gesture.dispose();
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Moving after accept does not cancel', (GestureTester tester) {
longPressDrag.addPointer(down);
gesture.addPointer(down);
tester.closeArena(5);
expect(longPressStart, isFalse);
expect(recognized, const <String>[]);
tester.route(down);
expect(longPressStart, isFalse);
expect(recognized, const <String>['down']);
tester.async.elapse(const Duration(seconds: 1));
expect(longPressStart, isTrue);
expect(recognized, const <String>['down', 'start']);
tester.route(move);
expect(recognized, const <String>['down', 'start', 'move']);
expect(longPressDragUpdate, const Offset(100, 200));
tester.route(up);
tester.async.elapse(const Duration(milliseconds: 300));
expect(longPressStart, isTrue);
expect(longPressUp, isTrue);
longPressDrag.dispose();
expect(recognized, const <String>['down', 'start', 'move', 'end']);
gesture.dispose();
expect(recognized, const <String>['down', 'start', 'move', 'end']);
});
});
@ -411,12 +409,17 @@ void main() {
position: Offset(10, 10),
);
late LongPressGestureRecognizer gesture;
final List<String> recognized = <String>[];
late LongPressGestureRecognizer longPress;
setUp(() {
longPress = LongPressGestureRecognizer()
gesture = LongPressGestureRecognizer()
..onLongPressDown = (LongPressDownDetails details) {
recognized.add('down');
}
..onLongPressCancel = () {
recognized.add('cancel');
}
..onLongPressStart = (LongPressStartDetails details) {
recognized.add('start');
}
@ -426,40 +429,43 @@ void main() {
});
tearDown(() {
longPress.dispose();
gesture.dispose();
recognized.clear();
});
testGesture('Should cancel long press when buttons change before acceptance', (GestureTester tester) {
// First press
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
expect(recognized, <String>[]);
expect(recognized, const <String>['down', 'cancel']);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
expect(recognized, <String>[]);
expect(recognized, const <String>['down', 'cancel']);
});
testGesture('Buttons change before acceptance should not prevent the next long press', (GestureTester tester) {
// First press
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
expect(recognized, <String>['down']);
tester.async.elapse(const Duration(milliseconds: 300));
tester.route(moveR);
expect(recognized, <String>['down', 'cancel']);
tester.async.elapse(const Duration(milliseconds: 700));
tester.route(up);
recognized.clear();
// Second press
longPress.addPointer(down2);
gesture.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
expect(recognized, <String>['down']);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
expect(recognized, <String>['down', 'start']);
recognized.clear();
tester.route(up2);
@ -468,11 +474,11 @@ void main() {
testGesture('Should cancel long press when buttons change after acceptance', (GestureTester tester) {
// First press
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
expect(recognized, <String>['down', 'start']);
recognized.clear();
tester.route(moveR);
@ -483,7 +489,7 @@ void main() {
testGesture('Buttons change after acceptance should not prevent the next long press', (GestureTester tester) {
// First press
longPress.addPointer(down);
gesture.addPointer(down);
tester.closeArena(down.pointer);
tester.route(down);
tester.async.elapse(const Duration(milliseconds: 1000));
@ -492,11 +498,11 @@ void main() {
recognized.clear();
// Second press
longPress.addPointer(down2);
gesture.addPointer(down2);
tester.closeArena(down2.pointer);
tester.route(down2);
tester.async.elapse(const Duration(milliseconds: 1000));
expect(recognized, <String>['start']);
expect(recognized, <String>['down', 'start']);
recognized.clear();
tester.route(up2);

View file

@ -41,6 +41,7 @@ void main() {
editable.attach(owner);
// This should register pointer into GestureBinding.instance.pointerRouter.
editable.handleEvent(const PointerDownEvent(), BoxHitTestEntry(editable, const Offset(10,10)));
GestureBinding.instance!.pointerRouter.route(const PointerDownEvent());
expect(spy.routeCount, greaterThan(0));
editable.detach();
expect(spy.routeCount, 0);

View file

@ -922,6 +922,7 @@ void main() {
// Move back to reset.
await dragScrollbarGesture.moveBy(const Offset(0.0, -scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
expect(scrollController.offset, 0.0);
expect(
find.byType(RawScrollbar),