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';
import 'package:flutter/gestures.dart';
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) {
assert(originPosition != null);
final Offset offset = event.position - originPosition!.global;
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
// 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
// recognizer, following the initial [PointerDownEvent]:
//
......@@ -25,7 +32,7 @@ double _getGlobalDistance(PointerEvent event, OffsetPair? originPosition) {
// state as long as it continues to track a pointer.
// * If a [PointerMoveEvent] is tracked that has moved a sufficient global distance
// 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
// distance to be considered a drag, then this recognizer moves from the [possible]
// state to [ready].
......@@ -51,7 +58,7 @@ enum _DragState {
/// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragDownDetails.consecutiveTapCount].
///
/// Used by [TapAndDragGestureRecognizer.onTapDown].
/// Used by [BaseTapAndDragGestureRecognizer.onTapDown].
typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details);
/// Details for [GestureTapDragDownCallback], such as the number of
......@@ -59,8 +66,8 @@ typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details);
///
/// See also:
///
/// * [TapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onTapDown] callback.
/// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [BaseTapAndDragGestureRecognizer.onTapDown] callback.
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
......@@ -110,7 +117,7 @@ class TapDragDownDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragUpDetails.consecutiveTapCount].
///
/// Used by [TapAndDragGestureRecognizer.onTapUp].
/// Used by [BaseTapAndDragGestureRecognizer.onTapUp].
typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details);
/// Details for [GestureTapDragUpCallback], such as the number of
......@@ -118,8 +125,8 @@ typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details);
///
/// See also:
///
/// * [TapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onTapUp] callback.
/// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [BaseTapAndDragGestureRecognizer.onTapUp] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
......@@ -169,7 +176,7 @@ class TapDragUpDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragStartDetails.consecutiveTapCount].
///
/// Used by [TapAndDragGestureRecognizer.onDragStart].
/// Used by [BaseTapAndDragGestureRecognizer.onDragStart].
typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details);
/// Details for [GestureTapDragStartCallback], such as the number of
......@@ -177,8 +184,8 @@ typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details)
///
/// See also:
///
/// * [TapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onDragStart] callback.
/// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [BaseTapAndDragGestureRecognizer.onDragStart] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback].
......@@ -242,7 +249,7 @@ class TapDragStartDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragUpdateDetails.consecutiveTapCount].
///
/// Used by [TapAndDragGestureRecognizer.onDragUpdate].
/// Used by [BaseTapAndDragGestureRecognizer.onDragUpdate].
typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails details);
/// Details for [GestureTapDragUpdateCallback], such as the number of
......@@ -250,8 +257,8 @@ typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails detail
///
/// See also:
///
/// * [TapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onDragUpdate] callback.
/// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [BaseTapAndDragGestureRecognizer.onDragUpdate] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
......@@ -374,7 +381,7 @@ class TapDragUpdateDetails with Diagnosticable {
/// The consecutive tap count at the time the pointer contacted the
/// screen is given by [TapDragEndDetails.consecutiveTapCount].
///
/// Used by [TapAndDragGestureRecognizer.onDragEnd].
/// Used by [BaseTapAndDragGestureRecognizer.onDragEnd].
typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails);
/// Details for [GestureTapDragEndCallback], such as the number of
......@@ -382,8 +389,8 @@ typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails);
///
/// See also:
///
/// * [TapAndDragGestureRecognizer], which passes this information to its
/// [TapAndDragGestureRecognizer.onDragEnd] callback.
/// * [BaseTapAndDragGestureRecognizer], which passes this information to its
/// [BaseTapAndDragGestureRecognizer.onDragEnd] callback.
/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback].
/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback].
/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback].
......@@ -443,7 +450,7 @@ class TapDragEndDetails with Diagnosticable {
/// Signature for when the pointer that previously triggered a
/// [GestureTapDragDownCallback] did not complete.
///
/// Used by [TapAndDragGestureRecognizer.onCancel].
/// Used by [BaseTapAndDragGestureRecognizer.onCancel].
typedef GestureCancelCallback = void Function();
// A mixin for [OneSequenceGestureRecognizer] that tracks the number of taps
......@@ -514,13 +521,6 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
// If this value is null, [consecutiveTapCount] can grow infinitely large.
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.
PointerDownEvent? _down;
PointerUpEvent? _up;
......@@ -560,7 +560,8 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
@override
void handleEvent(PointerEvent event) {
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) {
_consecutiveTapTimerStop();
......@@ -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
/// [DragGestureRecognizer] in one [GestureRecognizer].
///
/// ### 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*`
/// or `onDrag*` callback.
///
......@@ -664,12 +665,13 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
/// screen) or a drag (e.g. if the pointer was not dragged far enough to
/// be considered a drag.
///
/// This recognizer will not immediately declare victory for every tap or drag that it
/// recognizes.
/// This recognizer will not immediately declare victory for every tap that it
/// recognizes, but it declares victory for every drag.
///
/// The recognizer will declare victory when all other recognizer's in
/// 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
/// another recognizer declaring victory) while the pointer is contacting the
......@@ -678,7 +680,7 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
/// ### When competing with `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
/// of losing.
///
......@@ -689,19 +691,86 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
///
/// 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
/// pointer does travel enough distance then the [TapAndDragGestureRecognizer] will lose because
/// the [DragGestureRecognizer] will declare self-victory when the drag threshold is met.
class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _TapStatusTrackerMixin {
/// pointer does travel enough distance then the recognizer that entered the arena
/// first will win. The gesture detected in this case is a drag.
///
/// {@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.
///
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
TapAndDragGestureRecognizer({
BaseTapAndDragGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
}) : _deadline = kPressTimeout,
dragStartBehavior = DragStartBehavior.start,
slopTolerance = kTouchSlop;
dragStartBehavior = DragStartBehavior.start;
/// Configure the behavior of offsets passed to [onDragStart].
///
......@@ -738,23 +807,6 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
@override
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}
///
/// This triggers after the down event, once a short timeout ([kPressTimeout]) has
......@@ -763,7 +815,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The position of the pointer is provided in the callback's `details`
/// 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
/// are also provided in the callback's `details` argument.
/// {@endtemplate}
......@@ -782,7 +834,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The position of the pointer is provided in the callback's `details`
/// argument, which is a [TapDragUpDetails] object.
///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
/// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
///
/// See also:
///
......@@ -796,7 +848,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// argument, which is a [TapDragStartDetails] object. The [dragStartBehavior]
/// determines this position.
///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
/// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
///
/// See also:
///
......@@ -809,7 +861,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The distance traveled by the pointer since the last update is provided in
/// the callback's `details` argument, which is a [TapDragUpdateDetails] object.
///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
/// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
///
/// See also:
///
......@@ -822,7 +874,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
/// The velocity is provided in the callback's `details` argument, which is a
/// [TapDragEndDetails] object.
///
/// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData}
/// {@macro flutter.gestures.selectionrecognizers.BaseTapAndDragGestureRecognizer.tapStatusTrackerData}
///
/// See also:
///
......@@ -864,6 +916,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
PointerEvent? _start;
late OffsetPair _initialPosition;
late double _globalDistanceMoved;
late double _globalDistanceMovedAllAxes;
OffsetPair? _correctedPosition;
// For drag update throttle.
......@@ -872,9 +925,9 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
final Set<int> _acceptedActivePointers = <int>{};
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
}
Offset _getDeltaForDetails(Offset delta);
double? _getPrimaryValueFromOffset(Offset value);
bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind);
// 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].
......@@ -921,6 +974,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
super.addAllowedPointer(event);
_primaryPointer = event.pointer;
_globalDistanceMoved = 0.0;
_globalDistanceMovedAllAxes = 0.0;
_dragState = _DragState.possible;
_initialPosition = OffsetPair(global: event.position, local: event.localPosition);
_deadlineTimer = Timer(_deadline, () => _didExceedDeadlineWithEvent(event));
......@@ -955,7 +1009,11 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
_wonArenaForPrimaryPointer = true;
// resolve(GestureDisposition.accepted) will be called when the [PointerMoveEvent] has
// moved a sufficient global distance.
if (_start != null) {
assert(_dragState == _DragState.accepted);
assert(currentUp == null);
_acceptDrag(_start!);
}
......@@ -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
// drag gesture.
if (currentDown != null) {
if (!_acceptedActivePointers.remove(pointer)) {
resolvePointer(pointer, GestureDisposition.rejected);
}
_dragState = _DragState.accepted;
_acceptDrag(currentDown!);
_checkDragEnd();
}
......@@ -1026,8 +1088,8 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
// 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.
// In this case since the gesture cannot be a tap, it defaults to a drag.
_pastSlopTolerance = _pastSlopTolerance || slopTolerance != null && _getGlobalDistance(event, _initialPosition) > slopTolerance!;
final double computedSlop = computeHitSlop(event.kind, gestureSettings);
_pastSlopTolerance = _pastSlopTolerance || _getGlobalDistance(event, _initialPosition) > computedSlop;
if (_dragState == _DragState.accepted) {
_checkDragUpdate(event);
......@@ -1084,7 +1146,6 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
if (!_wonArenaForPrimaryPointer) {
return;
}
_dragState = _DragState.accepted;
if (dragStartBehavior == DragStartBehavior.start) {
_initialPosition = _initialPosition + OffsetPair(global: event.delta, local: event.localDelta);
}
......@@ -1106,13 +1167,24 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
void _checkDrag(PointerMoveEvent event) {
final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
final Offset movedLocally = _getDeltaForDetails(event.localDelta);
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform,
untransformedDelta: movedLocally,
untransformedEndPosition: event.localPosition
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
_globalDistanceMovedAllAxes += PointerEvent.transformDeltaViaPositions(
transform: localToGlobalTransform,
untransformedDelta: event.localDelta,
untransformedEndPosition: event.localPosition
).distance * 1.sign;
if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) {
if (_hasSufficientGlobalDistanceToAccept(event.kind)
|| (_wonArenaForPrimaryPointer && _globalDistanceMovedAllAxes.abs() > computePanSlop(event.kind, gestureSettings))) {
_start = event;
_dragState = _DragState.accepted;
if (!_wonArenaForPrimaryPointer) {
resolve(GestureDisposition.accepted);
}
}
}
......@@ -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
if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) {
gestures[TapAndDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndDragGestureRecognizer>(
() => TapAndDragGestureRecognizer(debugOwner: this),
(TapAndDragGestureRecognizer 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;
},
);
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
gestures[TapAndHorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapAndHorizontalDragGestureRecognizer>(
() => TapAndHorizontalDragGestureRecognizer(debugOwner: this),
(TapAndHorizontalDragGestureRecognizer 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;
},
);
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) {
......
......@@ -5412,6 +5412,130 @@ void main() {
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 {
final TextEditingController controller = TextEditingController();
......
......@@ -2165,8 +2165,8 @@ void main() {
// 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.
// We await for 300ms after the up event, so our next down event does not
// register as a double tap.
// 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(ePos);
await tester.pump();
await gesture.up();
......@@ -2189,6 +2189,131 @@ void main() {
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 {
final TextEditingController controller = TextEditingController();
......@@ -2211,8 +2336,8 @@ void main() {
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
// 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
// register as a double tap.
// 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(ePos);
await tester.pump();
await gesture.up();
......@@ -2233,6 +2358,122 @@ void main() {
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 {
int selectionChangedCount = 0;
const String testValue = 'abc def ghi';
......
......@@ -16,11 +16,34 @@ void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late List<String> events;
late TapAndDragGestureRecognizer tapAndDrag;
late BaseTapAndDragGestureRecognizer tapAndDrag;
setUp(() {
events = <String>[];
tapAndDrag = TapAndDragGestureRecognizer()
void setUpTapAndPanGestureRecognizer() {
tapAndDrag = TapAndPanGestureRecognizer()
..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
..maxConsecutiveTap = 3
..onTapDown = (TapDragDownDetails details) {
......@@ -30,17 +53,21 @@ void main() {
events.add('up#${details.consecutiveTapCount}');
}
..onDragStart = (TapDragStartDetails details) {
events.add('dragstart#${details.consecutiveTapCount}');
events.add('horizontaldragstart#${details.consecutiveTapCount}');
}
..onDragUpdate = (TapDragUpdateDetails details) {
events.add('dragupdate#${details.consecutiveTapCount}');
events.add('horizontaldragupdate#${details.consecutiveTapCount}');
}
..onDragEnd = (TapDragEndDetails details) {
events.add('dragend#${details.consecutiveTapCount}');
events.add('horizontaldragend#${details.consecutiveTapCount}');
}
..onCancel = () {
events.add('cancel');
};
}
setUp(() {
events = <String>[];
});
// Down/up pair 1: normal tap sequence
......@@ -107,7 +134,29 @@ void main() {
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) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
......@@ -135,6 +184,8 @@ void main() {
});
testGesture('Resets if times out in between taps', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
......@@ -153,6 +204,8 @@ void main() {
});
testGesture('Resets if taps are far apart', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
......@@ -171,6 +224,8 @@ void main() {
});
testGesture('Resets if consecutiveTapCount reaches maxConsecutiveTap', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
// First tap.
tapAndDrag.addPointer(down1);
tester.closeArena(1);
......@@ -209,6 +264,8 @@ void main() {
});
testGesture('Should recognize drag', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final TestPointer pointer = TestPointer(5);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(down);
......@@ -217,10 +274,12 @@ void main() {
tester.route(pointer.move(const Offset(40.0, 45.0)));
tester.route(pointer.up());
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) {
setUpTapAndPanGestureRecognizer();
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0));
tapAndDrag.addPointer(downA);
......@@ -252,12 +311,14 @@ void main() {
'down#2',
'up#2',
'down#3',
'dragstart#3',
'dragupdate#3',
'dragend#3']);
'panstart#3',
'panupdate#3',
'panend#3']);
});
testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - before acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tapAndDrag.addPointer(down2);
tester.closeArena(1);
......@@ -275,6 +336,8 @@ void main() {
});
testGesture('Calls tap up when the recognizer accepts before handleEvent is called', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tester.closeArena(1);
GestureBinding.instance.gestureArena.sweep(1);
......@@ -284,6 +347,8 @@ void main() {
});
testGesture('Recognizer rejects pointer that is not the primary one (FILO) - before acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tapAndDrag.addPointer(down2);
tester.closeArena(1);
......@@ -301,6 +366,8 @@ void main() {
});
testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - after acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
......@@ -319,6 +386,8 @@ void main() {
});
testGesture('Recognizer rejects pointer that is not the primary one (FILO) - after acceptance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tester.closeArena(1);
tester.route(down1);
......@@ -336,6 +405,8 @@ void main() {
});
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
// [kDoubleTapTouchSlop]. It is expected for the recognizer to detect a tap
// and fire drag cancel.
......@@ -348,6 +419,8 @@ void main() {
});
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
// 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.
......@@ -357,10 +430,38 @@ void main() {
tester.route(move5);
tester.route(up5);
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()
..onStart = (DragStartDetails details) {
events.add('panstart');
......@@ -377,8 +478,8 @@ void main() {
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
// When competing against another [DragGestureRecognizer], the [TapAndDragGestureRecognizer]
// will only win when it is the last recognizer in the arena.
// When competing against another [DragGestureRecognizer], the recognizer
// that first in the arena will win after sweep is called.
tapAndDrag.addPointer(downB);
pans.addPointer(downB);
tester.closeArena(5);
......@@ -386,11 +487,168 @@ void main() {
tester.route(pointer.move(const Offset(40.0, 45.0)));
tester.route(pointer.up());
expect(events, <String>[
'panstart',
'panend']);
'pancancel',
'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) {
setUpTapAndPanGestureRecognizer();
final LongPressGestureRecognizer longpress = LongPressGestureRecognizer()
..onLongPressStart = (LongPressStartDetails details) {
events.add('longpressstart');
......@@ -431,12 +689,14 @@ void main() {
'down#1',
'up#1',
'down#2',
'dragstart#2',
'dragupdate#2',
'dragend#2']);
'panstart#2',
'panupdate#2',
'panend#2']);
});
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()
..onTapDown = (TapDownDetails details) {
events.add('tapdown');
......@@ -458,6 +718,8 @@ void main() {
});
testGesture('Beats TapGestureRecognizer when the pointer has exceeded the slop tolerance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final TapGestureRecognizer taps = TapGestureRecognizer()
..onTapDown = (TapDownDetails details) {
events.add('tapdown');
......@@ -476,7 +738,7 @@ void main() {
tester.route(move5);
tester.route(up5);
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();
tester.async.elapse(const Duration(milliseconds: 1000));
......@@ -490,6 +752,8 @@ void main() {
});
testGesture('Ties with PanGestureRecognizer when pointer has not met sufficient global distance to be a drag', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
final PanGestureRecognizer pans = PanGestureRecognizer()
..onStart = (DragStartDetails details) {
events.add('panstart');
......@@ -515,13 +779,15 @@ void main() {
});
testGesture('Defaults to drag when pointer dragged past slop tolerance', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down5);
tester.closeArena(5);
tester.route(down5);
tester.route(move5);
tester.route(up5);
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();
tester.async.elapse(const Duration(milliseconds: 1000));
......@@ -534,6 +800,8 @@ void main() {
});
testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();
tapAndDrag.addPointer(down1);
tester.closeArena(1);
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