Unverified Commit 90f8ac5b authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

TapAndDragGestureRecognizer should declare victory immediately when drag is detected (#123055)

TapAndDragGestureRecognizer should declare victory immediately when drag is detected
parent a690c048
...@@ -8,16 +8,23 @@ import 'package:flutter/foundation.dart'; ...@@ -8,16 +8,23 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/services.dart' show HardwareKeyboard, LogicalKeyboardKey; import 'package:flutter/services.dart' show HardwareKeyboard, LogicalKeyboardKey;
import 'framework.dart';
import 'gesture_detector.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
// late String _last;
double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) { double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) {
assert(originPosition != null); assert(originPosition != null);
final Offset offset = event.position - originPosition!.global; final Offset offset = event.position - originPosition!.global;
return offset.distance; return offset.distance;
} }
// The possible states of a [TapAndDragGestureRecognizer]. // The possible states of a [BaseTapAndDragGestureRecognizer].
// //
// The recognizer advances from [ready] to [possible] when it starts tracking // The recognizer advances from [ready] to [possible] when it starts tracking
// a pointer in [TapAndDragGestureRecognizer.addAllowedPointer]. Where it advances // a pointer in [BaseTapAndDragGestureRecognizer.addAllowedPointer]. Where it advances
// from there depends on the sequence of pointer events that is tracked by the // from there depends on the sequence of pointer events that is tracked by the
// recognizer, following the initial [PointerDownEvent]: // recognizer, following the initial [PointerDownEvent]:
// //
...@@ -25,7 +32,7 @@ double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) { ...@@ -25,7 +32,7 @@ double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) {
// state as long as it continues to track a pointer. // state as long as it continues to track a pointer.
// * If a [PointerMoveEvent] is tracked that has moved a sufficient global distance // * If a [PointerMoveEvent] is tracked that has moved a sufficient global distance
// from the initial [PointerDownEvent] and it came before a [PointerUpEvent], then // from the initial [PointerDownEvent] and it came before a [PointerUpEvent], then
// when this recognizer wins the arena, it will move from the [possible] state to [accepted]. // this recognizer moves from the [possible] state to [accepted].
// * If a [PointerUpEvent] is tracked before the pointer has moved a sufficient global // * If a [PointerUpEvent] is tracked before the pointer has moved a sufficient global
// distance to be considered a drag, then this recognizer moves from the [possible] // distance to be considered a drag, then this recognizer moves from the [possible]
// state to [ready]. // state to [ready].
...@@ -51,7 +58,7 @@ enum _DragState { ...@@ -51,7 +58,7 @@ enum _DragState {
/// The consecutive tap count at the time the pointer contacted the /// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragDownDetails.consecutiveTapCount]. /// screen is given by [TapDragDownDetails.consecutiveTapCount].
/// ///
/// Used by [TapAndDragGestureRecognizer.onTapDown]. /// Used by [BaseTapAndDragGestureRecognizer.onTapDown].
typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details); typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details);
/// Details for [GestureTapDragDownCallback], such as the number of /// Details for [GestureTapDragDownCallback], such as the number of
...@@ -59,8 +66,8 @@ typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details); ...@@ -59,8 +66,8 @@ typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details);
/// ///
/// See also: /// See also:
/// ///
/// * [TapAndDragGestureRecognizer], which passes this information to its /// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onTapDown] callback. /// [BaseTapAndDragGestureRecognizer.onTapDown] callback.
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. /// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. /// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. /// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
...@@ -110,7 +117,7 @@ class TapDragDownDetails with Diagnosticable { ...@@ -110,7 +117,7 @@ class TapDragDownDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the /// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragUpDetails.consecutiveTapCount]. /// screen is given by [TapDragUpDetails.consecutiveTapCount].
/// ///
/// Used by [TapAndDragGestureRecognizer.onTapUp]. /// Used by [BaseTapAndDragGestureRecognizer.onTapUp].
typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details); typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details);
/// Details for [GestureTapDragUpCallback], such as the number of /// Details for [GestureTapDragUpCallback], such as the number of
...@@ -118,8 +125,8 @@ typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details); ...@@ -118,8 +125,8 @@ typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details);
/// ///
/// See also: /// See also:
/// ///
/// * [TapAndDragGestureRecognizer], which passes this information to its /// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onTapUp] callback. /// [BaseTapAndDragGestureRecognizer.onTapUp] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. /// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. /// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. /// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
...@@ -169,7 +176,7 @@ class TapDragUpDetails with Diagnosticable { ...@@ -169,7 +176,7 @@ class TapDragUpDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the /// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragStartDetails.consecutiveTapCount]. /// screen is given by [TapDragStartDetails.consecutiveTapCount].
/// ///
/// Used by [TapAndDragGestureRecognizer.onDragStart]. /// Used by [BaseTapAndDragGestureRecognizer.onDragStart].
typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details); typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details);
/// Details for [GestureTapDragStartCallback], such as the number of /// Details for [GestureTapDragStartCallback], such as the number of
...@@ -177,8 +184,8 @@ typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details) ...@@ -177,8 +184,8 @@ typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details)
/// ///
/// See also: /// See also:
/// ///
/// * [TapAndDragGestureRecognizer], which passes this information to its /// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onDragStart] callback. /// [BaseTapAndDragGestureRecognizer.onDragStart] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. /// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. /// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. /// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
...@@ -242,7 +249,7 @@ class TapDragStartDetails with Diagnosticable { ...@@ -242,7 +249,7 @@ class TapDragStartDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the /// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragUpdateDetails.consecutiveTapCount]. /// screen is given by [TapDragUpdateDetails.consecutiveTapCount].
/// ///
/// Used by [TapAndDragGestureRecognizer.onDragUpdate]. /// Used by [BaseTapAndDragGestureRecognizer.onDragUpdate].
typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails details); typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails details);
/// Details for [GestureTapDragUpdateCallback], such as the number of /// Details for [GestureTapDragUpdateCallback], such as the number of
...@@ -250,8 +257,8 @@ typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails detail ...@@ -250,8 +257,8 @@ typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails detail
/// ///
/// See also: /// See also:
/// ///
/// * [TapAndDragGestureRecognizer], which passes this information to its /// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onDragUpdate] callback. /// [BaseTapAndDragGestureRecognizer.onDragUpdate] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. /// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. /// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. /// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
...@@ -374,7 +381,7 @@ class TapDragUpdateDetails with Diagnosticable { ...@@ -374,7 +381,7 @@ class TapDragUpdateDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the /// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragEndDetails.consecutiveTapCount]. /// screen is given by [TapDragEndDetails.consecutiveTapCount].
/// ///
/// Used by [TapAndDragGestureRecognizer.onDragEnd]. /// Used by [BaseTapAndDragGestureRecognizer.onDragEnd].
typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails); typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails);
/// Details for [GestureTapDragEndCallback], such as the number of /// Details for [GestureTapDragEndCallback], such as the number of
...@@ -382,8 +389,8 @@ typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails); ...@@ -382,8 +389,8 @@ typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails);
/// ///
/// See also: /// See also:
/// ///
/// * [TapAndDragGestureRecognizer], which passes this information to its /// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onDragEnd] callback. /// [BaseTapAndDragGestureRecognizer.onDragEnd] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. /// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. /// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. /// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
...@@ -443,7 +450,7 @@ class TapDragEndDetails with Diagnosticable { ...@@ -443,7 +450,7 @@ class TapDragEndDetails with Diagnosticable {
/// Signature for when the pointer that previously triggered a /// Signature for when the pointer that previously triggered a
/// [GestureTapDragDownCallback] did not complete. /// [GestureTapDragDownCallback] did not complete.
/// ///
/// Used by [TapAndDragGestureRecognizer.onCancel]. /// Used by [BaseTapAndDragGestureRecognizer.onCancel].
typedef GestureCancelCallback = void Function(); typedef GestureCancelCallback = void Function();
// A mixin for [OneSequenceGestureRecognizer] that tracks the number of taps // A mixin for [OneSequenceGestureRecognizer] that tracks the number of taps
...@@ -514,13 +521,6 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { ...@@ -514,13 +521,6 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
// If this value is null, [consecutiveTapCount] can grow infinitely large. // If this value is null, [consecutiveTapCount] can grow infinitely large.
int? get maxConsecutiveTap; int? get maxConsecutiveTap;
// The maximum distance in logical pixels the gesture is allowed to drift
// from the initial touch down position before the [consecutiveTapCount]
// and [keysPressedOnDown] are frozen and the remaining tracker state is
// reset. These values remain frozen until the next [PointerDownEvent] is
// tracked in [addAllowedPointer].
double? get slopTolerance;
// Private tap state tracked. // Private tap state tracked.
PointerDownEvent? _down; PointerDownEvent? _down;
PointerUpEvent? _up; PointerUpEvent? _up;
...@@ -560,7 +560,8 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { ...@@ -560,7 +560,8 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
@override @override
void handleEvent(PointerEvent event) { void handleEvent(PointerEvent event) {
if (event is PointerMoveEvent) { if (event is PointerMoveEvent) {
final bool isSlopPastTolerance = slopTolerance != null && _getGlobalDistance(event, _originPosition) > slopTolerance!; final double computedSlop = computeHitSlop(event.kind, gestureSettings);
final bool isSlopPastTolerance = _getGlobalDistance(event, _originPosition) > computedSlop;
if (isSlopPastTolerance) { if (isSlopPastTolerance) {
_consecutiveTapTimerStop(); _consecutiveTapTimerStop();
...@@ -648,14 +649,14 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { ...@@ -648,14 +649,14 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
} }
} }
/// Recognizes taps and movements. /// A base class for gesture recognizers that recognize taps and movements.
/// ///
/// Takes on the responsibilities of [TapGestureRecognizer] and /// Takes on the responsibilities of [TapGestureRecognizer] and
/// [DragGestureRecognizer] in one [GestureRecognizer]. /// [DragGestureRecognizer] in one [GestureRecognizer].
/// ///
/// ### Gesture arena behavior /// ### Gesture arena behavior
/// ///
/// [TapAndDragGestureRecognizer] competes on the pointer events of /// [BaseTapAndDragGestureRecognizer] competes on the pointer events of
/// [kPrimaryButton] only when it has at least one non-null `onTap*` /// [kPrimaryButton] only when it has at least one non-null `onTap*`
/// or `onDrag*` callback. /// or `onDrag*` callback.
/// ///
...@@ -664,12 +665,13 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { ...@@ -664,12 +665,13 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
/// screen) or a drag (e.g. if the pointer was not dragged far enough to /// screen) or a drag (e.g. if the pointer was not dragged far enough to
/// be considered a drag. /// be considered a drag.
/// ///
/// This recognizer will not immediately declare victory for every tap or drag that it /// This recognizer will not immediately declare victory for every tap that it
/// recognizes. /// recognizes, but it declares victory for every drag.
/// ///
/// The recognizer will declare victory when all other recognizer's in /// The recognizer will declare victory when all other recognizer's in
/// the arena have lost, if the timer of [kPressTimeout] elapses and a tap /// the arena have lost, if the timer of [kPressTimeout] elapses and a tap
/// series greater than 1 is being tracked. /// series greater than 1 is being tracked, or until the pointer has moved
/// a sufficient global distance from the origin to be considered a drag.
/// ///
/// If this recognizer loses the arena (either by declaring defeat or by /// If this recognizer loses the arena (either by declaring defeat or by
/// another recognizer declaring victory) while the pointer is contacting the /// another recognizer declaring victory) while the pointer is contacting the
...@@ -678,7 +680,7 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { ...@@ -678,7 +680,7 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
/// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer` /// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer`
/// ///
/// Similar to [TapGestureRecognizer] and [DragGestureRecognizer], /// Similar to [TapGestureRecognizer] and [DragGestureRecognizer],
/// [TapAndDragGestureRecognizer] will not aggressively declare victory when it detects /// [BaseTapAndDragGestureRecognizer] will not aggressively declare victory when it detects
/// a tap, so when it is competing with those gesture recognizers and others it has a chance /// a tap, so when it is competing with those gesture recognizers and others it has a chance
/// of losing. /// of losing.
/// ///
...@@ -689,19 +691,86 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { ...@@ -689,19 +691,86 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
/// ///
/// When competing against [DragGestureRecognizer], if the pointer does not move a sufficient /// When competing against [DragGestureRecognizer], if the pointer does not move a sufficient
/// global distance to be considered a drag, the recognizers will tie in the arena. If the /// global distance to be considered a drag, the recognizers will tie in the arena. If the
/// pointer does travel enough distance then the [TapAndDragGestureRecognizer] will lose because /// pointer does travel enough distance then the recognizer that entered the arena
/// the [DragGestureRecognizer] will declare self-victory when the drag threshold is met. /// first will win. The gesture detected in this case is a drag.
class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _TapStatusTrackerMixin { ///
/// {@tool snippet}
///
/// This example shows how to hook up [TapAndPanGestureRecognizer]s' to nested
/// [RawGestureDetector]s'. It assumes that the code is being used inside a [State]
/// object with a `_last` field that is then displayed as the child of the gesture detector.
///
/// In this example, if the pointer has moved past the drag threshold, then the
/// the first [TapAndPanGestureRecognizer] instance to receive the [PointerEvent]
/// will win the arena because the recognizer will immediately declare victory.
///
/// The first one to receive the event in the example will depend on where on both
/// containers the pointer lands first. If your pointer begins in the overlapping
/// area of both containers, then the inner-most widget will receive the event first.
/// If your pointer begins in the yellow container then it will be the first to
/// receive the event.
///
/// If the pointer has not moved past the drag threshold, then the first recognizer
/// to enter the arena will win (i.e. they both tie and the gesture arena will call
/// [GestureArenaManager.sweep] so the first member of the arena will win).
///
/// ```dart
/// RawGestureDetector(
/// gestures: <Type, GestureRecognizerFactory>{
/// TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
/// () => TapAndPanGestureRecognizer(),
/// (TapAndPanGestureRecognizer instance) {
/// instance
/// ..onTapDown = (TapDragDownDetails details) { setState(() { _last = 'down_a'; }); }
/// ..onDragStart = (TapDragStartDetails details) { setState(() { _last = 'drag_start_a'; }); }
/// ..onDragUpdate = (TapDragUpdateDetails details) { setState(() { _last = 'drag_update_a'; }); }
/// ..onDragEnd = (TapDragEndDetails details) { setState(() { _last = 'drag_end_a'; }); }
/// ..onTapUp = (TapDragUpDetails details) { setState(() { _last = 'up_a'; }); }
/// ..onCancel = () { setState(() { _last = 'cancel_a'; }); };
/// },
/// ),
/// },
/// child: Container(
/// width: 300.0,
/// height: 300.0,
/// color: Colors.yellow,
/// alignment: Alignment.center,
/// child: RawGestureDetector(
/// gestures: <Type, GestureRecognizerFactory>{
/// TapAndPanGestureRecognizer: GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
/// () => TapAndPanGestureRecognizer(),
/// (TapAndPanGestureRecognizer instance) {
/// instance
/// ..onTapDown = (TapDragDownDetails details) { setState(() { _last = 'down_b'; }); }
/// ..onDragStart = (TapDragStartDetails details) { setState(() { _last = 'drag_start_b'; }); }
/// ..onDragUpdate = (TapDragUpdateDetails details) { setState(() { _last = 'drag_update_b'; }); }
/// ..onDragEnd = (TapDragEndDetails details) { setState(() { _last = 'drag_end_b'; }); }
/// ..onTapUp = (TapDragUpDetails details) { setState(() { _last = 'up_b'; }); }
/// ..onCancel = () { setState(() { _last = 'cancel_b'; }); };
/// },
/// ),
/// },
/// child: Container(
/// width: 150.0,
/// height: 150.0,
/// color: Colors.blue,
/// child: Text(_last),
/// ),
/// ),
/// ),
/// )
/// ```
/// {@end-tool}
sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _TapStatusTrackerMixin {
/// Creates a tap and drag gesture recognizer. /// Creates a tap and drag gesture recognizer.
/// ///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices} /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
TapAndDragGestureRecognizer({ BaseTapAndDragGestureRecognizer({
super.debugOwner, super.debugOwner,
super.supportedDevices, super.supportedDevices,
super.allowedButtonsFilter, super.allowedButtonsFilter,
}) : _deadline = kPressTimeout, }) : _deadline = kPressTimeout,
dragStartBehavior = DragStartBehavior.start, dragStartBehavior = DragStartBehavior.start;
slopTolerance = kTouchSlop;
/// Configure the behavior of offsets passed to [onDragStart]. /// Configure the behavior of offsets passed to [onDragStart].
/// ///
...@@ -738,23 +807,6 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -738,23 +807,6 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
@override @override
int? maxConsecutiveTap; int? maxConsecutiveTap;
// The maximum distance in logical pixels the gesture is allowed to drift
// to still be considered a tap.
//
// Drifting past the allowed slop amount causes the recognizer to reset
// the tap series it is currently tracking, stopping the consecutive tap
// count from increasing. The consecutive tap count and the set of hardware
// keys that were pressed on tap down will retain their pre-past slop
// tolerance values until the next [PointerDownEvent] is tracked.
//
// If the gesture exceeds this value, then it can only be accepted as a drag
// gesture.
//
// Can be null to indicate that the gesture can drift for any distance.
// Defaults to 18 logical pixels.
@override
final double? slopTolerance;
/// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapDown} /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapDown}
/// ///
/// This triggers after the down event, once a short timeout ([kPressTimeout]) has /// This triggers after the down event, once a short timeout ([kPressTimeout]) has
...@@ -763,7 +815,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -763,7 +815,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The position of the pointer is provided in the callback's `details` /// The position of the pointer is provided in the callback's `details`
/// argument, which is a [TapDragDownDetails] object. /// argument, which is a [TapDragDownDetails] object.
/// ///
/// {@template flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} /// {@template flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
/// The number of consecutive taps, and the keys that were pressed on tap down /// The number of consecutive taps, and the keys that were pressed on tap down
/// are also provided in the callback's `details` argument. /// are also provided in the callback's `details` argument.
/// {@endtemplate} /// {@endtemplate}
...@@ -782,7 +834,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -782,7 +834,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The position of the pointer is provided in the callback's `details` /// The position of the pointer is provided in the callback's `details`
/// argument, which is a [TapDragUpDetails] object. /// argument, which is a [TapDragUpDetails] object.
/// ///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
/// ///
/// See also: /// See also:
/// ///
...@@ -796,7 +848,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -796,7 +848,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// argument, which is a [TapDragStartDetails] object. The [dragStartBehavior] /// argument, which is a [TapDragStartDetails] object. The [dragStartBehavior]
/// determines this position. /// determines this position.
/// ///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
/// ///
/// See also: /// See also:
/// ///
...@@ -809,7 +861,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -809,7 +861,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The distance traveled by the pointer since the last update is provided in /// The distance traveled by the pointer since the last update is provided in
/// the callback's `details` argument, which is a [TapDragUpdateDetails] object. /// the callback's `details` argument, which is a [TapDragUpdateDetails] object.
/// ///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
/// ///
/// See also: /// See also:
/// ///
...@@ -822,7 +874,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -822,7 +874,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The velocity is provided in the callback's `details` argument, which is a /// The velocity is provided in the callback's `details` argument, which is a
/// [TapDragEndDetails] object. /// [TapDragEndDetails] object.
/// ///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} /// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
/// ///
/// See also: /// See also:
/// ///
...@@ -864,6 +916,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -864,6 +916,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
PointerEvent? _start; PointerEvent? _start;
late OffsetPair _initialPosition; late OffsetPair _initialPosition;
late double _globalDistanceMoved; late double _globalDistanceMoved;
late double _globalDistanceMovedAllAxes;
OffsetPair? _correctedPosition; OffsetPair? _correctedPosition;
// For drag update throttle. // For drag update throttle.
...@@ -872,9 +925,9 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -872,9 +925,9 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
final Set<int> _acceptedActivePointers = <int>{}; final Set<int> _acceptedActivePointers = <int>{};
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) { Offset _getDeltaForDetails(Offset delta);
return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings); double? _getPrimaryValueFromOffset(Offset value);
} bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind);
// Drag updates may require throttling to avoid excessive updating, such as for text layouts in text // Drag updates may require throttling to avoid excessive updating, such as for text layouts in text
// fields. The frequency of invocations is controlled by the [dragUpdateThrottleFrequency]. // fields. The frequency of invocations is controlled by the [dragUpdateThrottleFrequency].
...@@ -921,6 +974,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -921,6 +974,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
super.addAllowedPointer(event); super.addAllowedPointer(event);
_primaryPointer = event.pointer; _primaryPointer = event.pointer;
_globalDistanceMoved = 0.0; _globalDistanceMoved = 0.0;
_globalDistanceMovedAllAxes = 0.0;
_dragState = _DragState.possible; _dragState = _DragState.possible;
_initialPosition = OffsetPair(global: event.position, local: event.localPosition); _initialPosition = OffsetPair(global: event.position, local: event.localPosition);
_deadlineTimer = Timer(_deadline, () => _didExceedDeadlineWithEvent(event)); _deadlineTimer = Timer(_deadline, () => _didExceedDeadlineWithEvent(event));
...@@ -955,7 +1009,11 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -955,7 +1009,11 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
_wonArenaForPrimaryPointer = true; _wonArenaForPrimaryPointer = true;
// resolve(GestureDisposition.accepted) will be called when the [PointerMoveEvent] has
// moved a sufficient global distance.
if (_start != null) { if (_start != null) {
assert(_dragState == _DragState.accepted);
assert(currentUp == null);
_acceptDrag(_start!); _acceptDrag(_start!);
} }
...@@ -979,6 +1037,10 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -979,6 +1037,10 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
// but the pointer has exceeded the tap tolerance, then the pointer is accepted as a // but the pointer has exceeded the tap tolerance, then the pointer is accepted as a
// drag gesture. // drag gesture.
if (currentDown != null) { if (currentDown != null) {
if (!_acceptedActivePointers.remove(pointer)) {
resolvePointer(pointer, GestureDisposition.rejected);
}
_dragState = _DragState.accepted;
_acceptDrag(currentDown!); _acceptDrag(currentDown!);
_checkDragEnd(); _checkDragEnd();
} }
...@@ -1026,8 +1088,8 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -1026,8 +1088,8 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
// has been accepted and it has moved past the [slopTolerance] but has not moved // has been accepted and it has moved past the [slopTolerance] but has not moved
// a sufficient global distance from the initial position to be considered a drag. // a sufficient global distance from the initial position to be considered a drag.
// In this case since the gesture cannot be a tap, it defaults to a drag. // In this case since the gesture cannot be a tap, it defaults to a drag.
final double computedSlop = computeHitSlop(event.kind, gestureSettings);
_pastSlopTolerance = _pastSlopTolerance || slopTolerance != null && _getGlobalDistance(event, _initialPosition) > slopTolerance!; _pastSlopTolerance = _pastSlopTolerance || _getGlobalDistance(event, _initialPosition) > computedSlop;
if (_dragState == _DragState.accepted) { if (_dragState == _DragState.accepted) {
_checkDragUpdate(event); _checkDragUpdate(event);
...@@ -1084,7 +1146,6 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -1084,7 +1146,6 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
if (!_wonArenaForPrimaryPointer) { if (!_wonArenaForPrimaryPointer) {
return; return;
} }
_dragState = _DragState.accepted;
if (dragStartBehavior == DragStartBehavior.start) { if (dragStartBehavior == DragStartBehavior.start) {
_initialPosition = _initialPosition + OffsetPair(global: event.delta, local: event.localDelta); _initialPosition = _initialPosition + OffsetPair(global: event.delta, local: event.localDelta);
} }
...@@ -1106,13 +1167,24 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -1106,13 +1167,24 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
void _checkDrag(PointerMoveEvent event) { void _checkDrag(PointerMoveEvent event) {
final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!); final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
final Offset movedLocally = _getDeltaForDetails(event.localDelta);
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions( _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform,
untransformedDelta: movedLocally,
untransformedEndPosition: event.localPosition
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
_globalDistanceMovedAllAxes += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform, transform: localToGlobalTransform,
untransformedDelta: event.localDelta, untransformedDelta: event.localDelta,
untransformedEndPosition: event.localPosition untransformedEndPosition: event.localPosition
).distance * 1.sign; ).distance * 1.sign;
if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) { if (_hasSufficientGlobalDistanceToAccept(event.kind)
|| (_wonArenaForPrimaryPointer && _globalDistanceMovedAllAxes.abs() > computePanSlop(event.kind, gestureSettings))) {
_start = event; _start = event;
_dragState = _DragState.accepted;
if (!_wonArenaForPrimaryPointer) {
resolve(GestureDisposition.accepted);
}
} }
} }
...@@ -1288,3 +1360,113 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap ...@@ -1288,3 +1360,113 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
} }
} }
} }
/// Recognizes taps along with movement in the horizontal direction.
///
/// Before this recognizer has won the arena for the primary pointer being tracked,
/// it will only accept a drag on the horizontal axis. If a drag is detected after
/// this recognizer has won the arena then it will accept a drag on any axis.
///
/// See also:
///
/// * [BaseTapAndDragGestureRecognizer], for the class that provides the main
/// implementation details of this recognizer.
/// * [TapAndPanGestureRecognizer], for a similar recognizer that accepts a drag
/// on any axis regardless if the recognizer has won the arena for the primary
/// pointer being tracked.
/// * [HorizontalDragGestureRecognizer], for a similar recognizer that only recognizes
/// horizontal movement.
class TapAndHorizontalDragGestureRecognizer extends BaseTapAndDragGestureRecognizer {
/// Create a gesture recognizer for interactions in the horizontal axis.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
TapAndHorizontalDragGestureRecognizer({
super.debugOwner,
super.supportedDevices,
});
@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) {
return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
}
@override
Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0);
@override
double _getPrimaryValueFromOffset(Offset value) => value.dx;
@override
String get debugDescription => 'tap and horizontal drag';
}
/// {@template flutter.gestures.selectionrecognizers.TapAndPanGestureRecognizer}
/// Recognizes taps along with both horizontal and vertical movement.
///
/// This recognizer will accept a drag on any axis, regardless if it has won the
/// arena for the primary pointer being tracked.
///
/// See also:
///
/// * [BaseTapAndDragGestureRecognizer], for the class that provides the main
/// implementation details of this recognizer.
/// * [TapAndHorizontalDragGestureRecognizer], for a similar recognizer that
/// only accepts horizontal drags before it has won the arena for the primary
/// pointer being tracked.
/// * [PanGestureRecognizer], for a similar recognizer that only recognizes
/// movement.
/// {@endtemplate}
class TapAndPanGestureRecognizer extends BaseTapAndDragGestureRecognizer {
/// Create a gesture recognizer for interactions on a plane.
TapAndPanGestureRecognizer({
super.debugOwner,
super.supportedDevices,
});
@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) {
return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
}
@override
Offset _getDeltaForDetails(Offset delta) => delta;
@override
double? _getPrimaryValueFromOffset(Offset value) => null;
@override
String get debugDescription => 'tap and pan';
}
@Deprecated(
'Use TapAndPanGestureRecognizer instead. '
'TapAndPanGestureRecognizer works exactly the same but has a more disambiguated name from BaseTapAndDragGestureRecognizer. '
'This feature was deprecated after v3.9.0-19.0.pre.'
)
/// {@macro flutter.gestures.selectionrecognizers.TapAndPanGestureRecognizer}
class TapAndDragGestureRecognizer extends BaseTapAndDragGestureRecognizer {
/// Create a gesture recognizer for interactions on a plane.
@Deprecated(
'Use TapAndPanGestureRecognizer instead. '
'TapAndPanGestureRecognizer works exactly the same but has a more disambiguated name from BaseTapAndDragGestureRecognizer. '
'This feature was deprecated after v3.9.0-19.0.pre.'
)
TapAndDragGestureRecognizer({
super.debugOwner,
super.supportedDevices,
});
@override
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) {
return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
}
@override
Offset _getDeltaForDetails(Offset delta) => delta;
@override
double? _getPrimaryValueFromOffset(Offset value) => null;
@override
String get debugDescription => 'tap and pan';
}
...@@ -3112,22 +3112,46 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -3112,22 +3112,46 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
if (widget.onDragSelectionStart != null || if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null || widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) { widget.onDragSelectionEnd != null) {
gestures[TapAndDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndDragGestureRecognizer>( switch (defaultTargetPlatform) {
() => TapAndDragGestureRecognizer(debugOwner: this), case TargetPlatform.android:
(TapAndDragGestureRecognizer instance) { case TargetPlatform.fuchsia:
instance case TargetPlatform.iOS:
// Text selection should start from the position of the first pointer gestures[TapAndHorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndHorizontalDragGestureRecognizer>(
// down event. () => TapAndHorizontalDragGestureRecognizer(debugOwner: this),
..dragStartBehavior = DragStartBehavior.down (TapAndHorizontalDragGestureRecognizer instance) {
..dragUpdateThrottleFrequency = _kDragSelectionUpdateThrottle instance
..onTapDown = _handleTapDown // Text selection should start from the position of the first pointer
..onDragStart = _handleDragStart // down event.
..onDragUpdate = _handleDragUpdate ..dragStartBehavior = DragStartBehavior.down
..onDragEnd = _handleDragEnd ..dragUpdateThrottleFrequency = _kDragSelectionUpdateThrottle
..onTapUp = _handleTapUp ..onTapDown = _handleTapDown
..onCancel = _handleTapCancel; ..onDragStart = _handleDragStart
}, ..onDragUpdate = _handleDragUpdate
); ..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
gestures[TapAndPanGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndPanGestureRecognizer>(
() => TapAndPanGestureRecognizer(debugOwner: this),
(TapAndPanGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..dragUpdateThrottleFrequency = _kDragSelectionUpdateThrottle
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
..onDragEnd = _handleDragEnd
..onTapUp = _handleTapUp
..onCancel = _handleTapCancel;
},
);
}
} }
if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
......
...@@ -5412,6 +5412,130 @@ void main() { ...@@ -5412,6 +5412,130 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
); );
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/122519
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and attempt move
// to '|g'. The cursor will not move because the `VerticalDragGestureRecognizer`
// in the scrollable will beat the `TapAndHorizontalDragGestureRecognizer`
// in the TextField. This is because moving from `|a` to `|g` is a completely
// vertical movement.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// Release the pointer.
await gesture.up();
await tester.pumpAndSettle();
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
// Unlike our previous attempt to drag to `|g`, this works because moving
// to `|i` includes a horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
// in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async { testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
......
...@@ -2165,8 +2165,8 @@ void main() { ...@@ -2165,8 +2165,8 @@ void main() {
// Tap on text field to gain focus, and set selection to '|g'. On iOS // Tap on text field to gain focus, and set selection to '|g'. On iOS
// the selection is set to the word edge closest to the tap position. // the selection is set to the word edge closest to the tap position.
// We await for 300ms after the up event, so our next down event does not // We await for kDoubleTapTimeout after the up event, so our next down event
// register as a double tap. // does not register as a double tap.
final TestGesture gesture = await tester.startGesture(ePos); final TestGesture gesture = await tester.startGesture(ePos);
await tester.pump(); await tester.pump();
await gesture.up(); await gesture.up();
...@@ -2189,6 +2189,131 @@ void main() { ...@@ -2189,6 +2189,131 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
); );
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/122519
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: <Widget>[
TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
],
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and attempt move
// to '|g'. The cursor will not move because the `VerticalDragGestureRecognizer`
// in the scrollable will beat the `TapAndHorizontalDragGestureRecognizer`
// in the TextField. This is because moving from `|a` to `|g` is a completely
// vertical movement.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// Release the pointer.
await gesture.up();
await tester.pumpAndSettle();
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
// Unlike our previous attempt to drag to `|g`, this works because moving
// to `|i` includes a horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
// in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async { testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
...@@ -2211,8 +2336,8 @@ void main() { ...@@ -2211,8 +2336,8 @@ void main() {
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
// Tap on text field to gain focus, and set selection to '|e'. // Tap on text field to gain focus, and set selection to '|e'.
// We await for 300ms after the up event, so our next down event does not // We await for kDoubleTapTimeout after the up event, so our next down event
// register as a double tap. // does not register as a double tap.
final TestGesture gesture = await tester.startGesture(ePos); final TestGesture gesture = await tester.startGesture(ePos);
await tester.pump(); await tester.pump();
await gesture.up(); await gesture.up();
...@@ -2233,6 +2358,122 @@ void main() { ...@@ -2233,6 +2358,122 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
); );
testWidgets('Can move cursor when dragging (Android) - multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
// Tap on text field to gain focus, and set selection to '|a'.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('a'));
// Here we tap on '|c', and move down to '|g'.
await gesture.down(textOffsetToPosition(tester, testValue.indexOf('c')));
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('g'));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets('Can move cursor when dragging (Android) - ListView', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/122519
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: <Widget>[
TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
],
),
),
),
);
const String testValue = 'abc\ndef\nghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset cPos = textOffsetToPosition(tester, testValue.indexOf('c'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
// Tap on text field to gain focus, and set selection to '|c'.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(cPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('c'));
// Here we tap on '|a', and attempt move to '|g'. The cursor will not move
// because the `VerticalDragGestureRecognizer` in the scrollable will beat
// the `TapAndHorizontalDragGestureRecognizer` in the TextField. This is
// because moving from `|a` to `|g` is a completely vertical movement.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('c'));
// Release the pointer.
await gesture.up();
await tester.pumpAndSettle();
// Here we tap on '|c', and move to '|g'. Unlike our previous attempt to
// drag to `|g`, this works because moving from `|c` to `|g` includes a
// horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
// in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
await gesture.down(cPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('g'));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async {
int selectionChangedCount = 0; int selectionChangedCount = 0;
const String testValue = 'abc def ghi'; const String testValue = 'abc def ghi';
......
...@@ -16,11 +16,34 @@ void main() { ...@@ -16,11 +16,34 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
late List<String> events; late List<String> events;
late TapAndDragGestureRecognizer tapAndDrag; late BaseTapAndDragGestureRecognizer tapAndDrag;
setUp(() { void setUpTapAndPanGestureRecognizer() {
events = <String>[]; tapAndDrag = TapAndPanGestureRecognizer()
tapAndDrag = TapAndDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down
..maxConsecutiveTap = 3
..onTapDown = (TapDragDownDetails details) {
events.add('down#${details.consecutiveTapCount}');
}
..onTapUp = (TapDragUpDetails details) {
events.add('up#${details.consecutiveTapCount}');
}
..onDragStart = (TapDragStartDetails details) {
events.add('panstart#${details.consecutiveTapCount}');
}
..onDragUpdate = (TapDragUpdateDetails details) {
events.add('panupdate#${details.consecutiveTapCount}');
}
..onDragEnd = (TapDragEndDetails details) {
events.add('panend#${details.consecutiveTapCount}');
}
..onCancel = () {
events.add('cancel');
};
}
void setUpTapAndHorizontalDragGestureRecognizer() {
tapAndDrag = TapAndHorizontalDragGestureRecognizer()
..dragStartBehavior = DragStartBehavior.down ..dragStartBehavior = DragStartBehavior.down
..maxConsecutiveTap = 3 ..maxConsecutiveTap = 3
..onTapDown = (TapDragDownDetails details) { ..onTapDown = (TapDragDownDetails details) {
...@@ -30,17 +53,21 @@ void main() { ...@@ -30,17 +53,21 @@ void main() {
events.add('up#${details.consecutiveTapCount}'); events.add('up#${details.consecutiveTapCount}');
} }
..onDragStart = (TapDragStartDetails details) { ..onDragStart = (TapDragStartDetails details) {
events.add('dragstart#${details.consecutiveTapCount}'); events.add('horizontaldragstart#${details.consecutiveTapCount}');
} }
..onDragUpdate = (TapDragUpdateDetails details) { ..onDragUpdate = (TapDragUpdateDetails details) {
events.add('dragupdate#${details.consecutiveTapCount}'); events.add('horizontaldragupdate#${details.consecutiveTapCount}');
} }
..onDragEnd = (TapDragEndDetails details) { ..onDragEnd = (TapDragEndDetails details) {
events.add('dragend#${details.consecutiveTapCount}'); events.add('horizontaldragend#${details.consecutiveTapCount}');
} }
..onCancel = () { ..onCancel = () {
events.add('cancel'); events.add('cancel');
}; };
}
setUp(() {
events = <String>[];
}); });
// Down/up pair 1: normal tap sequence // Down/up pair 1: normal tap sequence
...@@ -107,7 +134,29 @@ void main() { ...@@ -107,7 +134,29 @@ void main() {
position: Offset(25.0, 25.0), position: Offset(25.0, 25.0),
); );
// Mouse Down/move/up sequence 6: intervening motion - kPrecisePointerPanSlop
const PointerDownEvent down6 = PointerDownEvent(
kind: PointerDeviceKind.mouse,
pointer: 6,
position: Offset(10.0, 10.0),
);
const PointerMoveEvent move6 = PointerMoveEvent(
kind: PointerDeviceKind.mouse,
pointer: 6,
position: Offset(15.0, 15.0),
delta: Offset(5.0, 5.0),
);
const PointerUpEvent up6 = PointerUpEvent(
kind: PointerDeviceKind.mouse,
pointer: 6,
position: Offset(15.0, 15.0),
);
testGesture('Recognizes consecutive taps', (GestureTester tester) { testGesture('Recognizes consecutive taps', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
tester.route(down1); tester.route(down1);
...@@ -135,6 +184,8 @@ void main() { ...@@ -135,6 +184,8 @@ void main() {
}); });
testGesture('Resets if times out in between taps', (GestureTester tester) { testGesture('Resets if times out in between taps', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
tester.route(down1); tester.route(down1);
...@@ -153,6 +204,8 @@ void main() { ...@@ -153,6 +204,8 @@ void main() {
}); });
testGesture('Resets if taps are far apart', (GestureTester tester) { testGesture('Resets if taps are far apart', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
tester.route(down1); tester.route(down1);
...@@ -171,6 +224,8 @@ void main() { ...@@ -171,6 +224,8 @@ void main() {
}); });
testGesture('Resets if consecutiveTapCount reaches maxConsecutiveTap', (GestureTester tester) { testGesture('Resets if consecutiveTapCount reaches maxConsecutiveTap', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
// First tap. // First tap.
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
...@@ -209,6 +264,8 @@ void main() { ...@@ -209,6 +264,8 @@ void main() {
}); });
testGesture('Should recognize drag', (GestureTester tester) { testGesture('Should recognize drag', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final TestPointer pointer = TestPointer(5); final TestPointer pointer = TestPointer(5);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(down); tapAndDrag.addPointer(down);
...@@ -217,10 +274,12 @@ void main() { ...@@ -217,10 +274,12 @@ void main() {
tester.route(pointer.move(const Offset(40.0, 45.0))); tester.route(pointer.move(const Offset(40.0, 45.0)));
tester.route(pointer.up()); tester.route(pointer.up());
GestureBinding.instance.gestureArena.sweep(5); GestureBinding.instance.gestureArena.sweep(5);
expect(events, <String>['down#1', 'dragstart#1', 'dragupdate#1', 'dragend#1']); expect(events, <String>['down#1', 'panstart#1', 'panupdate#1', 'panend#1']);
}); });
testGesture('Recognizes consecutive taps + drag', (GestureTester tester) { testGesture('Recognizes consecutive taps + drag', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final TestPointer pointer = TestPointer(5); final TestPointer pointer = TestPointer(5);
final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0)); final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(downA); tapAndDrag.addPointer(downA);
...@@ -252,12 +311,14 @@ void main() { ...@@ -252,12 +311,14 @@ void main() {
'down#2', 'down#2',
'up#2', 'up#2',
'down#3', 'down#3',
'dragstart#3', 'panstart#3',
'dragupdate#3', 'panupdate#3',
'dragend#3']); 'panend#3']);
}); });
testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - before acceptance', (GestureTester tester) { testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - before acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tapAndDrag.addPointer(down2); tapAndDrag.addPointer(down2);
tester.closeArena(1); tester.closeArena(1);
...@@ -275,6 +336,8 @@ void main() { ...@@ -275,6 +336,8 @@ void main() {
}); });
testGesture('Calls tap up when the recognizer accepts before handleEvent is called', (GestureTester tester) { testGesture('Calls tap up when the recognizer accepts before handleEvent is called', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
GestureBinding.instance.gestureArena.sweep(1); GestureBinding.instance.gestureArena.sweep(1);
...@@ -284,6 +347,8 @@ void main() { ...@@ -284,6 +347,8 @@ void main() {
}); });
testGesture('Recognizer rejects pointer that is not the primary one (FILO) - before acceptance', (GestureTester tester) { testGesture('Recognizer rejects pointer that is not the primary one (FILO) - before acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tapAndDrag.addPointer(down2); tapAndDrag.addPointer(down2);
tester.closeArena(1); tester.closeArena(1);
...@@ -301,6 +366,8 @@ void main() { ...@@ -301,6 +366,8 @@ void main() {
}); });
testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - after acceptance', (GestureTester tester) { testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - after acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
tester.route(down1); tester.route(down1);
...@@ -319,6 +386,8 @@ void main() { ...@@ -319,6 +386,8 @@ void main() {
}); });
testGesture('Recognizer rejects pointer that is not the primary one (FILO) - after acceptance', (GestureTester tester) { testGesture('Recognizer rejects pointer that is not the primary one (FILO) - after acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
tester.route(down1); tester.route(down1);
...@@ -336,6 +405,8 @@ void main() { ...@@ -336,6 +405,8 @@ void main() {
}); });
testGesture('Recognizer detects tap gesture when pointer does not move past tap tolerance', (GestureTester tester) { testGesture('Recognizer detects tap gesture when pointer does not move past tap tolerance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
// In this test the tap has not travelled past the tap tolerance defined by // In this test the tap has not travelled past the tap tolerance defined by
// [kDoubleTapTouchSlop]. It is expected for the recognizer to detect a tap // [kDoubleTapTouchSlop]. It is expected for the recognizer to detect a tap
// and fire drag cancel. // and fire drag cancel.
...@@ -348,6 +419,8 @@ void main() { ...@@ -348,6 +419,8 @@ void main() {
}); });
testGesture('Recognizer detects drag gesture when pointer moves past tap tolerance but not the drag minimum', (GestureTester tester) { testGesture('Recognizer detects drag gesture when pointer moves past tap tolerance but not the drag minimum', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
// In this test, the pointer has moved past the tap tolerance but it has // In this test, the pointer has moved past the tap tolerance but it has
// not reached the distance travelled to be considered a drag gesture. In // not reached the distance travelled to be considered a drag gesture. In
// this case it is expected for the recognizer to detect a drag and fire tap cancel. // this case it is expected for the recognizer to detect a drag and fire tap cancel.
...@@ -357,10 +430,38 @@ void main() { ...@@ -357,10 +430,38 @@ void main() {
tester.route(move5); tester.route(move5);
tester.route(up5); tester.route(up5);
GestureBinding.instance.gestureArena.sweep(5); GestureBinding.instance.gestureArena.sweep(5);
expect(events, <String>['down#1', 'dragstart#1', 'dragend#1']); expect(events, <String>['down#1', 'panstart#1', 'panend#1']);
});
testGesture('Beats TapGestureRecognizer when mouse pointer moves past kPrecisePointerPanSlop', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
// This is a regression test for https://github.com/flutter/flutter/issues/122141.
final TapGestureRecognizer taps = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
events.add('tapdown');
}
..onTapUp = (TapUpDetails details) {
events.add('tapup');
}
..onTapCancel = () {
events.add('tapscancel');
};
tapAndDrag.addPointer(down6);
taps.addPointer(down6);
tester.closeArena(6);
tester.route(down6);
tester.route(move6);
tester.route(up6);
GestureBinding.instance.gestureArena.sweep(6);
expect(events, <String>['down#1', 'panstart#1', 'panupdate#1', 'panend#1']);
}); });
testGesture('Recognizer loses when competing against a DragGestureRecognizer when the pointer travels minimum distance to be considered a drag', (GestureTester tester) { testGesture('Recognizer declares self-victory in a non-empty arena when pointer travels minimum distance to be considered a drag', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final PanGestureRecognizer pans = PanGestureRecognizer() final PanGestureRecognizer pans = PanGestureRecognizer()
..onStart = (DragStartDetails details) { ..onStart = (DragStartDetails details) {
events.add('panstart'); events.add('panstart');
...@@ -377,8 +478,8 @@ void main() { ...@@ -377,8 +478,8 @@ void main() {
final TestPointer pointer = TestPointer(5); final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
// When competing against another [DragGestureRecognizer], the [TapAndDragGestureRecognizer] // When competing against another [DragGestureRecognizer], the recognizer
// will only win when it is the last recognizer in the arena. // that first in the arena will win after sweep is called.
tapAndDrag.addPointer(downB); tapAndDrag.addPointer(downB);
pans.addPointer(downB); pans.addPointer(downB);
tester.closeArena(5); tester.closeArena(5);
...@@ -386,11 +487,168 @@ void main() { ...@@ -386,11 +487,168 @@ void main() {
tester.route(pointer.move(const Offset(40.0, 45.0))); tester.route(pointer.move(const Offset(40.0, 45.0)));
tester.route(pointer.up()); tester.route(pointer.up());
expect(events, <String>[ expect(events, <String>[
'panstart', 'pancancel',
'panend']); 'down#1',
'panstart#1',
'panupdate#1',
'panend#1']);
});
testGesture('TapAndHorizontalDragGestureRecognizer accepts drag on a pan when the arena has already been won by the primary pointer', (GestureTester tester) {
setUpTapAndHorizontalDragGestureRecognizer();
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(downB);
tester.closeArena(5);
tester.route(downB);
tester.route(pointer.move(const Offset(25.0, 45.0)));
tester.route(pointer.up());
expect(events, <String>[
'down#1',
'horizontaldragstart#1',
'horizontaldragupdate#1',
'horizontaldragend#1']);
});
testGesture('TapAndHorizontalDragGestureRecognizer loses to VerticalDragGestureRecognizer on a vertical drag', (GestureTester tester) {
setUpTapAndHorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer()
..onStart = (DragStartDetails details) {
events.add('verticalstart');
}
..onUpdate = (DragUpdateDetails details) {
events.add('verticalupdate');
}
..onEnd = (DragEndDetails details) {
events.add('verticalend');
}
..onCancel = () {
events.add('verticalcancel');
};
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(downB);
verticalDrag.addPointer(downB);
tester.closeArena(5);
tester.route(downB);
tester.route(pointer.move(const Offset(10.0, 45.0)));
tester.route(pointer.move(const Offset(10.0, 100.0)));
tester.route(pointer.up());
expect(events, <String>[
'verticalstart',
'verticalupdate',
'verticalend']);
});
testGesture('TapAndPanGestureRecognizer loses to VerticalDragGestureRecognizer on a vertical drag', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer()
..onStart = (DragStartDetails details) {
events.add('verticalstart');
}
..onUpdate = (DragUpdateDetails details) {
events.add('verticalupdate');
}
..onEnd = (DragEndDetails details) {
events.add('verticalend');
}
..onCancel = () {
events.add('verticalcancel');
};
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(downB);
verticalDrag.addPointer(downB);
tester.closeArena(5);
tester.route(downB);
tester.route(pointer.move(const Offset(10.0, 45.0)));
tester.route(pointer.move(const Offset(10.0, 100.0)));
tester.route(pointer.up());
expect(events, <String>[
'verticalstart',
'verticalupdate',
'verticalend']);
});
testGesture('TapAndHorizontalDragGestureRecognizer beats VerticalDragGestureRecognizer on a horizontal drag', (GestureTester tester) {
setUpTapAndHorizontalDragGestureRecognizer();
final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer()
..onStart = (DragStartDetails details) {
events.add('verticalstart');
}
..onUpdate = (DragUpdateDetails details) {
events.add('verticalupdate');
}
..onEnd = (DragEndDetails details) {
events.add('verticalend');
}
..onCancel = () {
events.add('verticalcancel');
};
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(downB);
verticalDrag.addPointer(downB);
tester.closeArena(5);
tester.route(downB);
tester.route(pointer.move(const Offset(45.0, 10.0)));
tester.route(pointer.up());
expect(events, <String>[
'verticalcancel',
'down#1',
'horizontaldragstart#1',
'horizontaldragupdate#1',
'horizontaldragend#1']);
});
testGesture('TapAndPanGestureRecognizer beats VerticalDragGestureRecognizer on a horizontal pan', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final VerticalDragGestureRecognizer verticalDrag = VerticalDragGestureRecognizer()
..onStart = (DragStartDetails details) {
events.add('verticalstart');
}
..onUpdate = (DragUpdateDetails details) {
events.add('verticalupdate');
}
..onEnd = (DragEndDetails details) {
events.add('verticalend');
}
..onCancel = () {
events.add('verticalcancel');
};
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(downB);
verticalDrag.addPointer(downB);
tester.closeArena(5);
tester.route(downB);
tester.route(pointer.move(const Offset(45.0, 25.0)));
tester.route(pointer.up());
expect(events, <String>[
'verticalcancel',
'down#1',
'panstart#1',
'panupdate#1',
'panend#1']);
}); });
testGesture('Beats LongPressGestureRecognizer on a consecutive tap greater than one', (GestureTester tester) { testGesture('Beats LongPressGestureRecognizer on a consecutive tap greater than one', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final LongPressGestureRecognizer longpress = LongPressGestureRecognizer() final LongPressGestureRecognizer longpress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) { ..onLongPressStart = (LongPressStartDetails details) {
events.add('longpressstart'); events.add('longpressstart');
...@@ -431,12 +689,14 @@ void main() { ...@@ -431,12 +689,14 @@ void main() {
'down#1', 'down#1',
'up#1', 'up#1',
'down#2', 'down#2',
'dragstart#2', 'panstart#2',
'dragupdate#2', 'panupdate#2',
'dragend#2']); 'panend#2']);
}); });
testGesture('Beats TapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) { testGesture('Beats TapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final TapGestureRecognizer taps = TapGestureRecognizer() final TapGestureRecognizer taps = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { ..onTapDown = (TapDownDetails details) {
events.add('tapdown'); events.add('tapdown');
...@@ -458,6 +718,8 @@ void main() { ...@@ -458,6 +718,8 @@ void main() {
}); });
testGesture('Beats TapGestureRecognizer when the pointer has exceeded the slop tolerance', (GestureTester tester) { testGesture('Beats TapGestureRecognizer when the pointer has exceeded the slop tolerance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final TapGestureRecognizer taps = TapGestureRecognizer() final TapGestureRecognizer taps = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) { ..onTapDown = (TapDownDetails details) {
events.add('tapdown'); events.add('tapdown');
...@@ -476,7 +738,7 @@ void main() { ...@@ -476,7 +738,7 @@ void main() {
tester.route(move5); tester.route(move5);
tester.route(up5); tester.route(up5);
GestureBinding.instance.gestureArena.sweep(5); GestureBinding.instance.gestureArena.sweep(5);
expect(events, <String>['down#1', 'dragstart#1', 'dragend#1']); expect(events, <String>['down#1', 'panstart#1', 'panend#1']);
events.clear(); events.clear();
tester.async.elapse(const Duration(milliseconds: 1000)); tester.async.elapse(const Duration(milliseconds: 1000));
...@@ -490,6 +752,8 @@ void main() { ...@@ -490,6 +752,8 @@ void main() {
}); });
testGesture('Ties with PanGestureRecognizer when pointer has not met sufficient global distance to be a drag', (GestureTester tester) { testGesture('Ties with PanGestureRecognizer when pointer has not met sufficient global distance to be a drag', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final PanGestureRecognizer pans = PanGestureRecognizer() final PanGestureRecognizer pans = PanGestureRecognizer()
..onStart = (DragStartDetails details) { ..onStart = (DragStartDetails details) {
events.add('panstart'); events.add('panstart');
...@@ -515,13 +779,15 @@ void main() { ...@@ -515,13 +779,15 @@ void main() {
}); });
testGesture('Defaults to drag when pointer dragged past slop tolerance', (GestureTester tester) { testGesture('Defaults to drag when pointer dragged past slop tolerance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down5); tapAndDrag.addPointer(down5);
tester.closeArena(5); tester.closeArena(5);
tester.route(down5); tester.route(down5);
tester.route(move5); tester.route(move5);
tester.route(up5); tester.route(up5);
GestureBinding.instance.gestureArena.sweep(5); GestureBinding.instance.gestureArena.sweep(5);
expect(events, <String>['down#1', 'dragstart#1', 'dragend#1']); expect(events, <String>['down#1', 'panstart#1', 'panend#1']);
events.clear(); events.clear();
tester.async.elapse(const Duration(milliseconds: 1000)); tester.async.elapse(const Duration(milliseconds: 1000));
...@@ -534,6 +800,8 @@ void main() { ...@@ -534,6 +800,8 @@ void main() {
}); });
testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) { testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1); tapAndDrag.addPointer(down1);
tester.closeArena(1); tester.closeArena(1);
tester.route(down1); tester.route(down1);
......
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