From 39fa0117a919bd401c4c8734c97ddb46fbc45cb7 Mon Sep 17 00:00:00 2001 From: Renzo Olivares Date: Wed, 21 Dec 2022 15:29:07 -0800 Subject: [PATCH] Revert "Add support for double tap and drag for text selection (#109573)" (#117497) This reverts commit cd0f15a770992ec41445bd212aac73572c278fd1. Co-authored-by: Renzo Olivares --- .../flutter/lib/src/cupertino/text_field.dart | 4 +- .../lib/src/gestures/drag_details.dart | 6 +- .../flutter/lib/src/gestures/monodrag.dart | 8 - packages/flutter/lib/src/gestures/tap.dart | 18 - .../lib/src/material/selectable_text.dart | 2 +- .../flutter/lib/src/material/text_field.dart | 2 +- .../src/widgets/tap_and_drag_gestures.dart | 1288 ----------------- .../lib/src/widgets/text_selection.dart | 430 +++--- packages/flutter/lib/widgets.dart | 1 - .../test/cupertino/text_field_test.dart | 223 +-- packages/flutter/test/gestures/tap_test.dart | 1 + .../test/material/text_field_test.dart | 259 +--- .../test/widgets/selectable_text_test.dart | 4 +- .../widgets/tap_and_drag_gestures_test.dart | 552 ------- .../test/widgets/text_selection_test.dart | 52 +- 15 files changed, 215 insertions(+), 2635 deletions(-) delete mode 100644 packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart delete mode 100644 packages/flutter/test/widgets/tap_and_drag_gestures_test.dart diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart index 72508e3d02b4..977b9df22ecd 100644 --- a/packages/flutter/lib/src/cupertino/text_field.dart +++ b/packages/flutter/lib/src/cupertino/text_field.dart @@ -102,7 +102,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe final _CupertinoTextFieldState _state; @override - void onSingleTapUp(TapDragUpDetails details) { + void onSingleTapUp(TapUpDetails details) { // Because TextSelectionGestureDetector listens to taps that happen on // widgets in front of it, tapping the clear button will also trigger // this handler. If the clear button widget recognizes the up event, @@ -120,7 +120,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe } @override - void onDragSelectionEnd(TapDragEndDetails details) { + void onDragSelectionEnd(DragEndDetails details) { _state._requestKeyboard(); } } diff --git a/packages/flutter/lib/src/gestures/drag_details.dart b/packages/flutter/lib/src/gestures/drag_details.dart index 0053f20d414f..40308accb8f3 100644 --- a/packages/flutter/lib/src/gestures/drag_details.dart +++ b/packages/flutter/lib/src/gestures/drag_details.dart @@ -109,12 +109,10 @@ class DragStartDetails { String toString() => '${objectRuntimeType(this, 'DragStartDetails')}($globalPosition)'; } -/// {@template flutter.gestures.dragdetails.GestureDragStartCallback} /// Signature for when a pointer has contacted the screen and has begun to move. /// /// The `details` object provides the position of the touch when it first /// touched the surface. -/// {@endtemplate} /// /// See [DragGestureRecognizer.onStart]. typedef GestureDragStartCallback = void Function(DragStartDetails details); @@ -128,7 +126,7 @@ typedef GestureDragStartCallback = void Function(DragStartDetails details); /// * [DragStartDetails], the details for [GestureDragStartCallback]. /// * [DragEndDetails], the details for [GestureDragEndCallback]. class DragUpdateDetails { - /// Creates details for a [GestureDragUpdateCallback]. + /// Creates details for a [DragUpdateDetails]. /// /// The [delta] argument must not be null. /// @@ -197,13 +195,11 @@ class DragUpdateDetails { String toString() => '${objectRuntimeType(this, 'DragUpdateDetails')}($delta)'; } -/// {@template flutter.gestures.dragdetails.GestureDragUpdateCallback} /// Signature for when a pointer that is in contact with the screen and moving /// has moved again. /// /// The `details` object provides the position of the touch and the distance it /// has traveled since the last update. -/// {@endtemplate} /// /// See [DragGestureRecognizer.onUpdate]. typedef GestureDragUpdateCallback = void Function(DragUpdateDetails details); diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index 500d296b2053..15e6d5e9d6e8 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -26,13 +26,11 @@ enum _DragState { accepted, } -/// {@template flutter.gestures.monodrag.GestureDragEndCallback} /// Signature for when a pointer that was previously in contact with the screen /// and moving is no longer in contact with the screen. /// /// The velocity at which the pointer was moving when it stopped contacting /// the screen is available in the `details`. -/// {@endtemplate} /// /// Used by [DragGestureRecognizer.onEnd]. typedef GestureDragEndCallback = void Function(DragEndDetails details); @@ -126,10 +124,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// * [DragDownDetails], which is passed as an argument to this callback. GestureDragDownCallback? onDown; - /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onStart} /// A pointer has contacted the screen with a primary button and has begun to /// move. - /// {@endtemplate} /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [DragStartDetails] object. The [dragStartBehavior] @@ -141,10 +137,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// * [DragStartDetails], which is passed as an argument to this callback. GestureDragStartCallback? onStart; - /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onUpdate} /// A pointer that is in contact with the screen with a primary button and /// moving has moved again. - /// {@endtemplate} /// /// The distance traveled by the pointer since the last update is provided in /// the callback's `details` argument, which is a [DragUpdateDetails] object. @@ -155,11 +149,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { /// * [DragUpdateDetails], which is passed as an argument to this callback. GestureDragUpdateCallback? onUpdate; - /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onEnd} /// A pointer that was previously in contact with the screen with a primary /// button and moving is no longer in contact with the screen and was moving /// at a specific velocity when it stopped contacting the screen. - /// {@endtemplate} /// /// The velocity is provided in the callback's `details` argument, which is a /// [DragEndDetails] object. diff --git a/packages/flutter/lib/src/gestures/tap.dart b/packages/flutter/lib/src/gestures/tap.dart index c2ce8aecda50..a05d839cfaee 100644 --- a/packages/flutter/lib/src/gestures/tap.dart +++ b/packages/flutter/lib/src/gestures/tap.dart @@ -45,13 +45,11 @@ class TapDownDetails { final Offset localPosition; } -/// {@template flutter.gestures.tap.GestureTapDownCallback} /// Signature for when a pointer that might cause a tap has contacted the /// screen. /// /// The position at which the pointer contacted the screen is available in the /// `details`. -/// {@endtemplate} /// /// See also: /// @@ -84,13 +82,11 @@ class TapUpDetails { final PointerDeviceKind kind; } -/// {@template flutter.gestures.tap.GestureTapUpCallback} /// Signature for when a pointer that will trigger a tap has stopped contacting /// the screen. /// /// The position at which the pointer stopped contacting the screen is available /// in the `details`. -/// {@endtemplate} /// /// See also: /// @@ -364,10 +360,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} TapGestureRecognizer({ super.debugOwner, super.supportedDevices }); - /// {@template flutter.gestures.tap.TapGestureRecognizer.onTapDown} /// A pointer has contacted the screen at a particular location with a primary /// button, which might be the start of a tap. - /// {@endtemplate} /// /// This triggers after the down event, once a short timeout ([deadline]) has /// elapsed, or once the gestures has won the arena, whichever comes first. @@ -384,10 +378,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// * [GestureDetector.onTapDown], which exposes this callback. GestureTapDownCallback? onTapDown; - /// {@template flutter.gestures.tap.TapGestureRecognizer.onTapUp} /// A pointer has stopped contacting the screen at a particular location, /// which is recognized as a tap of a primary button. - /// {@endtemplate} /// /// This triggers on the up event, if the recognizer wins the arena with it /// or has previously won, immediately followed by [onTap]. @@ -419,10 +411,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// * [GestureDetector.onTap], which exposes this callback. GestureTapCallback? onTap; - /// {@template flutter.gestures.tap.TapGestureRecognizer.onTapCancel} /// A pointer that previously triggered [onTapDown] will not end up causing /// a tap. - /// {@endtemplate} /// /// This triggers once the gesture loses the arena if [onTapDown] has /// previously been triggered. @@ -438,10 +428,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// * [GestureDetector.onTapCancel], which exposes this callback. GestureTapCancelCallback? onTapCancel; - /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTap} /// A pointer has stopped contacting the screen, which is recognized as a tap /// of a secondary button. - /// {@endtemplate} /// /// This triggers on the up event, if the recognizer wins the arena with it or /// has previously won, immediately following [onSecondaryTapUp]. @@ -456,10 +444,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// * [GestureDetector.onSecondaryTap], which exposes this callback. GestureTapCallback? onSecondaryTap; - /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTapDown} /// A pointer has contacted the screen at a particular location with a /// secondary button, which might be the start of a secondary tap. - /// {@endtemplate} /// /// This triggers after the down event, once a short timeout ([deadline]) has /// elapsed, or once the gestures has won the arena, whichever comes first. @@ -476,10 +462,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// * [GestureDetector.onSecondaryTapDown], which exposes this callback. GestureTapDownCallback? onSecondaryTapDown; - /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTapUp} /// A pointer has stopped contacting the screen at a particular location, /// which is recognized as a tap of a secondary button. - /// {@endtemplate} /// /// This triggers on the up event if the recognizer wins the arena with it /// or has previously won. @@ -498,10 +482,8 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer { /// * [GestureDetector.onSecondaryTapUp], which exposes this callback. GestureTapUpCallback? onSecondaryTapUp; - /// {@template flutter.gestures.tap.TapGestureRecognizer.onSecondaryTapCancel} /// A pointer that previously triggered [onSecondaryTapDown] will not end up /// causing a tap. - /// {@endtemplate} /// /// This triggers once the gesture loses the arena if [onSecondaryTapDown] /// has previously been triggered. diff --git a/packages/flutter/lib/src/material/selectable_text.dart b/packages/flutter/lib/src/material/selectable_text.dart index 226623a83092..aeb5a3a36711 100644 --- a/packages/flutter/lib/src/material/selectable_text.dart +++ b/packages/flutter/lib/src/material/selectable_text.dart @@ -85,7 +85,7 @@ class _SelectableTextSelectionGestureDetectorBuilder extends TextSelectionGestur } @override - void onSingleTapUp(TapDragUpDetails details) { + void onSingleTapUp(TapUpDetails details) { editableText.hideToolbar(); if (delegate.selectionEnabled) { switch (Theme.of(_state.context).platform) { diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 83becd8241ec..c2627bd6a8a0 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -65,7 +65,7 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete } @override - void onSingleTapUp(TapDragUpDetails details) { + void onSingleTapUp(TapUpDetails details) { super.onSingleTapUp(details); _state._requestKeyboard(); _state.widget.onTap?.call(); diff --git a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart b/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart deleted file mode 100644 index e56f7ca074b6..000000000000 --- a/packages/flutter/lib/src/widgets/tap_and_drag_gestures.dart +++ /dev/null @@ -1,1288 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/services.dart' show HardwareKeyboard, LogicalKeyboardKey; - -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 recognizer advances from [ready] to [possible] when it starts tracking -// a pointer in [TapAndDragGestureRecognizer.addAllowedPointer]. Where it advances -// from there depends on the sequence of pointer events that is tracked by the -// recognizer, following the initial [PointerDownEvent]: -// -// * If a [PointerUpEvent] has not been tracked, the recognizer stays in the [possible] -// 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]. -// * 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]. -// * If a [PointerCancelEvent] is tracked then this recognizer moves from its current -// state to [ready]. -// -// Once the recognizer has stopped tracking any remaining pointers, the recognizer -// returns to the [ready] state. -enum _DragState { - // The recognizer is ready to start recognizing a drag. - ready, - - // The sequence of pointer events seen thus far is consistent with a drag but - // it has not been accepted definitively. - possible, - - // The sequence of pointer events has been accepted definitively as a drag. - accepted, -} - -/// {@macro flutter.gestures.tap.GestureTapDownCallback} -/// -/// The consecutive tap count at the time the pointer contacted the -/// screen is given by [TapDragDownDetails.consecutiveTapCount]. -/// -/// Used by [TapAndDragGestureRecognizer.onTapDown]. -typedef GestureTapDragDownCallback = void Function(TapDragDownDetails details); - -/// Details for [GestureTapDragDownCallback], such as the number of -/// consecutive taps. -/// -/// See also: -/// -/// * [TapAndDragGestureRecognizer], which passes this information to its -/// [TapAndDragGestureRecognizer.onTapDown] callback. -/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. -/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. -/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. -/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback]. -class TapDragDownDetails with Diagnosticable { - /// Creates details for a [GestureTapDragDownCallback]. - /// - /// The [globalPosition], [localPosition], [consecutiveTapCount], and - /// [keysPressedOnDown] arguments must be provided and must not be null. - TapDragDownDetails({ - required this.globalPosition, - required this.localPosition, - this.kind, - required this.consecutiveTapCount, - required this.keysPressedOnDown, - }); - - /// The global position at which the pointer contacted the screen. - final Offset globalPosition; - - /// The local position at which the pointer contacted the screen. - final Offset localPosition; - - /// The kind of the device that initiated the event. - final PointerDeviceKind? kind; - - /// If this tap is in a series of taps, then this value represents - /// the number in the series this tap is. - final int consecutiveTapCount; - - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('globalPosition', globalPosition)); - properties.add(DiagnosticsProperty('localPosition', localPosition)); - properties.add(DiagnosticsProperty('kind', kind)); - properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); - } -} - -/// {@macro flutter.gestures.tap.GestureTapUpCallback} -/// -/// The consecutive tap count at the time the pointer contacted the -/// screen is given by [TapDragUpDetails.consecutiveTapCount]. -/// -/// Used by [TapAndDragGestureRecognizer.onTapUp]. -typedef GestureTapDragUpCallback = void Function(TapDragUpDetails details); - -/// Details for [GestureTapDragUpCallback], such as the number of -/// consecutive taps. -/// -/// See also: -/// -/// * [TapAndDragGestureRecognizer], which passes this information to its -/// [TapAndDragGestureRecognizer.onTapUp] callback. -/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. -/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. -/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. -/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback]. -class TapDragUpDetails with Diagnosticable { - /// Creates details for a [GestureTapDragUpCallback]. - /// - /// The [kind], [globalPosition], [localPosition], [consecutiveTapCount], and - /// [keysPressedOnDown] arguments must be provided and must not be null. - TapDragUpDetails({ - required this.kind, - required this.globalPosition, - required this.localPosition, - required this.consecutiveTapCount, - required this.keysPressedOnDown, - }); - - /// The global position at which the pointer contacted the screen. - final Offset globalPosition; - - /// The local position at which the pointer contacted the screen. - final Offset localPosition; - - /// The kind of the device that initiated the event. - final PointerDeviceKind kind; - - /// If this tap is in a series of taps, then this value represents - /// the number in the series this tap is. - final int consecutiveTapCount; - - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('globalPosition', globalPosition)); - properties.add(DiagnosticsProperty('localPosition', localPosition)); - properties.add(DiagnosticsProperty('kind', kind)); - properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); - } -} - -/// {@macro flutter.gestures.dragdetails.GestureDragStartCallback} -/// -/// The consecutive tap count at the time the pointer contacted the -/// screen is given by [TapDragStartDetails.consecutiveTapCount]. -/// -/// Used by [TapAndDragGestureRecognizer.onDragStart]. -typedef GestureTapDragStartCallback = void Function(TapDragStartDetails details); - -/// Details for [GestureTapDragStartCallback], such as the number of -/// consecutive taps. -/// -/// See also: -/// -/// * [TapAndDragGestureRecognizer], which passes this information to its -/// [TapAndDragGestureRecognizer.onDragStart] callback. -/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. -/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. -/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. -/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback]. -class TapDragStartDetails with Diagnosticable { - /// Creates details for a [GestureTapDragStartCallback]. - /// - /// The [globalPosition], [localPosition], [consecutiveTapCount], and - /// [keysPressedOnDown] arguments must be provided and must not be null. - TapDragStartDetails({ - this.sourceTimeStamp, - required this.globalPosition, - required this.localPosition, - this.kind, - required this.consecutiveTapCount, - required this.keysPressedOnDown, - }); - - /// Recorded timestamp of the source pointer event that triggered the drag - /// event. - /// - /// Could be null if triggered from proxied events such as accessibility. - final Duration? sourceTimeStamp; - - /// The global position at which the pointer contacted the screen. - /// - /// See also: - /// - /// * [localPosition], which is the [globalPosition] transformed to the - /// coordinate space of the event receiver. - final Offset globalPosition; - - /// The local position in the coordinate system of the event receiver at - /// which the pointer contacted the screen. - final Offset localPosition; - - /// The kind of the device that initiated the event. - final PointerDeviceKind? kind; - - /// If this tap is in a series of taps, then this value represents - /// the number in the series this tap is. - final int consecutiveTapCount; - - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('sourceTimeStamp', sourceTimeStamp)); - properties.add(DiagnosticsProperty('globalPosition', globalPosition)); - properties.add(DiagnosticsProperty('localPosition', localPosition)); - properties.add(DiagnosticsProperty('kind', kind)); - properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); - } -} - -/// {@macro flutter.gestures.dragdetails.GestureDragUpdateCallback} -/// -/// The consecutive tap count at the time the pointer contacted the -/// screen is given by [TapDragUpdateDetails.consecutiveTapCount]. -/// -/// Used by [TapAndDragGestureRecognizer.onDragUpdate]. -typedef GestureTapDragUpdateCallback = void Function(TapDragUpdateDetails details); - -/// Details for [GestureTapDragUpdateCallback], such as the number of -/// consecutive taps. -/// -/// See also: -/// -/// * [TapAndDragGestureRecognizer], which passes this information to its -/// [TapAndDragGestureRecognizer.onDragUpdate] callback. -/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. -/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. -/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. -/// * [TapDragEndDetails], the details for [GestureTapDragEndCallback]. -class TapDragUpdateDetails with Diagnosticable { - /// Creates details for a [GestureTapDragUpdateCallback]. - /// - /// The [delta] argument must not be null. - /// - /// If [primaryDelta] is non-null, then its value must match one of the - /// coordinates of [delta] and the other coordinate must be zero. - /// - /// The [globalPosition], [localPosition], [offsetFromOrigin], [localOffsetFromOrigin], - /// [consecutiveTapCount], and [keysPressedOnDown] arguments must be provided and must - /// not be null. - TapDragUpdateDetails({ - this.sourceTimeStamp, - this.delta = Offset.zero, - this.primaryDelta, - required this.globalPosition, - this.kind, - required this.localPosition, - required this.offsetFromOrigin, - required this.localOffsetFromOrigin, - required this.consecutiveTapCount, - required this.keysPressedOnDown, - }) : assert(delta != null), - assert( - primaryDelta == null - || (primaryDelta == delta.dx && delta.dy == 0.0) - || (primaryDelta == delta.dy && delta.dx == 0.0), - ); - - /// Recorded timestamp of the source pointer event that triggered the drag - /// event. - /// - /// Could be null if triggered from proxied events such as accessibility. - final Duration? sourceTimeStamp; - - /// The amount the pointer has moved in the coordinate space of the event - /// receiver since the previous update. - /// - /// If the [GestureTapDragUpdateCallback] is for a one-dimensional drag (e.g., - /// a horizontal or vertical drag), then this offset contains only the delta - /// in that direction (i.e., the coordinate in the other direction is zero). - /// - /// Defaults to zero if not specified in the constructor. - final Offset delta; - - /// The amount the pointer has moved along the primary axis in the coordinate - /// space of the event receiver since the previous - /// update. - /// - /// If the [GestureTapDragUpdateCallback] is for a one-dimensional drag (e.g., - /// a horizontal or vertical drag), then this value contains the component of - /// [delta] along the primary axis (e.g., horizontal or vertical, - /// respectively). Otherwise, if the [GestureTapDragUpdateCallback] is for a - /// two-dimensional drag (e.g., a pan), then this value is null. - /// - /// Defaults to null if not specified in the constructor. - final double? primaryDelta; - - /// The pointer's global position when it triggered this update. - /// - /// See also: - /// - /// * [localPosition], which is the [globalPosition] transformed to the - /// coordinate space of the event receiver. - final Offset globalPosition; - - /// The local position in the coordinate system of the event receiver at - /// which the pointer contacted the screen. - /// - /// Defaults to [globalPosition] if not specified in the constructor. - final Offset localPosition; - - /// The kind of the device that initiated the event. - final PointerDeviceKind? kind; - - /// A delta offset from the point where the drag initially contacted - /// the screen to the point where the pointer is currently located in global - /// coordinates (the present [globalPosition]) when this callback is triggered. - /// - /// When considering a [GestureRecognizer] that tracks the number of consecutive taps, - /// this offset is associated with the most recent [PointerDownEvent] that occured. - final Offset offsetFromOrigin; - - /// A local delta offset from the point where the drag initially contacted - /// the screen to the point where the pointer is currently located in local - /// coordinates (the present [localPosition]) when this callback is triggered. - /// - /// When considering a [GestureRecognizer] that tracks the number of consecutive taps, - /// this offset is associated with the most recent [PointerDownEvent] that occured. - final Offset localOffsetFromOrigin; - - /// If this tap is in a series of taps, then this value represents - /// the number in the series this tap is. - final int consecutiveTapCount; - - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('sourceTimeStamp', sourceTimeStamp)); - properties.add(DiagnosticsProperty('delta', delta)); - properties.add(DiagnosticsProperty('primaryDelta', primaryDelta)); - properties.add(DiagnosticsProperty('globalPosition', globalPosition)); - properties.add(DiagnosticsProperty('localPosition', localPosition)); - properties.add(DiagnosticsProperty('kind', kind)); - properties.add(DiagnosticsProperty('offsetFromOrigin', offsetFromOrigin)); - properties.add(DiagnosticsProperty('localOffsetFromOrigin', localOffsetFromOrigin)); - properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); - } -} - -/// {@macro flutter.gestures.monodrag.GestureDragEndCallback} -/// -/// The consecutive tap count at the time the pointer contacted the -/// screen is given by [TapDragEndDetails.consecutiveTapCount]. -/// -/// Used by [TapAndDragGestureRecognizer.onDragEnd]. -typedef GestureTapDragEndCallback = void Function(TapDragEndDetails endDetails); - -/// Details for [GestureTapDragEndCallback], such as the number of -/// consecutive taps. -/// -/// See also: -/// -/// * [TapAndDragGestureRecognizer], which passes this information to its -/// [TapAndDragGestureRecognizer.onDragEnd] callback. -/// * [TapDragDownDetails], the details for [GestureTapDragDownCallback]. -/// * [TapDragUpDetails], the details for [GestureTapDragUpCallback]. -/// * [TapDragStartDetails], the details for [GestureTapDragStartCallback]. -/// * [TapDragUpdateDetails], the details for [GestureTapDragUpdateCallback]. -class TapDragEndDetails with Diagnosticable { - /// Creates details for a [GestureTapDragEndCallback]. - /// - /// The [velocity] argument must not be null. - /// - /// The [consecutiveTapCount], and [keysPressedOnDown] arguments must - /// be provided and must not be null. - TapDragEndDetails({ - this.velocity = Velocity.zero, - this.primaryVelocity, - required this.consecutiveTapCount, - required this.keysPressedOnDown, - }) : assert(velocity != null), - assert( - primaryVelocity == null - || primaryVelocity == velocity.pixelsPerSecond.dx - || primaryVelocity == velocity.pixelsPerSecond.dy, - ); - - /// The velocity the pointer was moving when it stopped contacting the screen. - /// - /// Defaults to zero if not specified in the constructor. - final Velocity velocity; - - /// The velocity the pointer was moving along the primary axis when it stopped - /// contacting the screen, in logical pixels per second. - /// - /// If the [GestureTapDragEndCallback] is for a one-dimensional drag (e.g., a - /// horizontal or vertical drag), then this value contains the component of - /// [velocity] along the primary axis (e.g., horizontal or vertical, - /// respectively). Otherwise, if the [GestureTapDragEndCallback] is for a - /// two-dimensional drag (e.g., a pan), then this value is null. - /// - /// Defaults to null if not specified in the constructor. - final double? primaryVelocity; - - /// If this tap is in a series of taps, then this value represents - /// the number in the series this tap is. - final int consecutiveTapCount; - - /// The keys that were pressed when the most recent [PointerDownEvent] occurred. - final Set keysPressedOnDown; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('velocity', velocity)); - properties.add(DiagnosticsProperty('primaryVelocity', primaryVelocity)); - properties.add(DiagnosticsProperty('consecutiveTapCount', consecutiveTapCount)); - properties.add(DiagnosticsProperty>('keysPressedOnDown', keysPressedOnDown)); - } -} - -/// Signature for when the pointer that previously triggered a -/// [GestureTapDragDownCallback] did not complete. -/// -/// Used by [TapAndDragGestureRecognizer.onCancel]. -typedef GestureCancelCallback = void Function(); - -// A mixin for [OneSequenceGestureRecognizer] that tracks the number of taps -// that occur in a series of [PointerEvent]s and the most recent set of -// [LogicalKeyboardKey]s pressed on the most recent tap down. -// -// A tap is tracked as part of a series of taps if: -// -// 1. The elapsed time between when a [PointerUpEvent] and the subsequent -// [PointerDownEvent] does not exceed [kDoubleTapTimeout]. -// 2. The delta between the position tapped in the global coordinate system -// and the position that was tapped previously must be less than or equal -// to [kDoubleTapSlop]. -// -// This mixin's state, i.e. the series of taps being tracked is reset when -// a tap is tracked that does not meet any of the specifications stated above. -mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer { - // Public state available to [OneSequenceGestureRecognizer]. - - // The [PointerDownEvent] that was most recently tracked in [addAllowedPointer]. - // - // This value will be null if a [PointerDownEvent] has not been tracked yet in - // [addAllowedPointer] or the timer between two taps has elapsed. - // - // This value is only reset when the timer between a [PointerUpEvent] and the - // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in - // [addAllowedPointer]. - PointerDownEvent? get currentDown => _down; - - // The [PointerUpEvent] that was most recently tracked in [handleEvent]. - // - // This value will be null if a [PointerUpEvent] has not been tracked yet in - // [handleEvent] or the timer between two taps has elapsed. - // - // This value is only reset when the timer between a [PointerUpEvent] and the - // [PointerDownEvent] times out or when a new [PointerDownEvent] is tracked in - // [addAllowedPointer]. - PointerUpEvent? get currentUp => _up; - - // The number of consecutive taps that the most recently tracked [PointerDownEvent] - // in [currentDown] represents. - // - // This value defaults to zero, meaning a tap series is not currently being tracked. - // - // When this value is greater than zero it means [addAllowedPointer] has run - // and at least one [PointerDownEvent] belongs to the current series of taps - // being tracked. - // - // [addAllowedPointer] will either increment this value by `1` or set the value to `1` - // depending if the new [PointerDownEvent] is determined to be in the same series as the - // tap that preceded it. If too much time has elapsed between two taps, the recognizer has lost - // in the arena, the gesture has been cancelled, or the recognizer is being disposed then - // this value will be set to `0`, and a new series will begin. - int get consecutiveTapCount => _consecutiveTapCount; - - // The set of [LogicalKeyboardKey]s pressed when the most recent [PointerDownEvent] - // was tracked in [addAllowedPointer]. - // - // This value defaults to an empty set. - // - // When the timer between two taps elapses, the recognizer loses the arena, the gesture is cancelled - // or the recognizer is disposed of then this value is reset. - Set get keysPressedOnDown => _keysPressedOnDown ?? {}; - - // The upper limit for the [consecutiveTapCount]. When this limit is reached - // all tap related state is reset and a new tap series is tracked. - // - // 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; - int _consecutiveTapCount = 0; - Set? _keysPressedOnDown; - - OffsetPair? _originPosition; - int? _previousButtons; - - // For timing taps. - Timer? _consecutiveTapTimer; - Offset? _lastTapOffset; - - // When tracking a tap, the [consecutiveTapCount] is incremented if the given tap - // falls under the tolerance specifications and reset to 1 if not. - @override - void addAllowedPointer(PointerDownEvent event) { - super.addAllowedPointer(event); - if (maxConsecutiveTap == _consecutiveTapCount) { - _tapTrackerReset(); - } - _up = null; - if (_down != null && !_representsSameSeries(event)) { - // The given tap does not match the specifications of the series of taps being tracked, - // reset the tap count and related state. - _consecutiveTapCount = 1; - } else { - _consecutiveTapCount += 1; - } - _consecutiveTapTimerStop(); - // `_down` must be assigned in this method instead of [handleEvent], - // because [acceptGesture] might be called before [handleEvent], - // which may rely on `_down` to initiate a callback. - _trackTap(event); - } - - @override - void handleEvent(PointerEvent event) { - if (event is PointerMoveEvent) { - final bool isSlopPastTolerance = slopTolerance != null && _getGlobalDistance(event, _originPosition) > slopTolerance!; - - if (isSlopPastTolerance) { - _consecutiveTapTimerStop(); - _previousButtons = null; - _lastTapOffset = null; - } - } else if (event is PointerUpEvent) { - _up = event; - if (_down != null) { - _consecutiveTapTimerStop(); - _consecutiveTapTimerStart(); - } - } else if (event is PointerCancelEvent) { - _tapTrackerReset(); - } - } - - @override - void rejectGesture(int pointer) { - _tapTrackerReset(); - } - - @override - void dispose() { - _tapTrackerReset(); - super.dispose(); - } - - void _trackTap(PointerDownEvent event) { - _down = event; - _keysPressedOnDown = HardwareKeyboard.instance.logicalKeysPressed; - _previousButtons = event.buttons; - _lastTapOffset = event.position; - _originPosition = OffsetPair(local: event.localPosition, global: event.position); - } - - bool _hasSameButton(int buttons) { - assert(_previousButtons != null); - if (buttons == _previousButtons!) { - return true; - } else { - return false; - } - } - - bool _isWithinConsecutiveTapTolerance(Offset secondTapOffset) { - assert(secondTapOffset != null); - if (_lastTapOffset == null) { - return false; - } - - final Offset difference = secondTapOffset - _lastTapOffset!; - return difference.distance <= kDoubleTapSlop; - } - - bool _representsSameSeries(PointerDownEvent event) { - return _consecutiveTapTimer != null - && _isWithinConsecutiveTapTolerance(event.position) - && _hasSameButton(event.buttons); - } - - void _consecutiveTapTimerStart() { - _consecutiveTapTimer ??= Timer(kDoubleTapTimeout, _tapTrackerReset); - } - - void _consecutiveTapTimerStop() { - if (_consecutiveTapTimer != null) { - _consecutiveTapTimer!.cancel(); - _consecutiveTapTimer = null; - } - } - - void _tapTrackerReset() { - // The timer has timed out, i.e. the time between a [PointerUpEvent] and the subsequent - // [PointerDownEvent] exceeded the duration of [kDoubleTapTimeout], so the tap belonging - // to the [PointerDownEvent] cannot be considered part of the same tap series as the - // previous [PointerUpEvent]. - _consecutiveTapTimerStop(); - _previousButtons = null; - _originPosition = null; - _lastTapOffset = null; - _consecutiveTapCount = 0; - _keysPressedOnDown = null; - _down = null; - _up = null; - } -} - -/// Recognizes taps and movements. -/// -/// Takes on the responsibilities of [TapGestureRecognizer] and -/// [DragGestureRecognizer] in one [GestureRecognizer]. -/// -/// ### Gesture arena behavior -/// -/// [TapAndDragGestureRecognizer] competes on the pointer events of -/// [kPrimaryButton] only when it has at least one non-null `onTap*` -/// or `onDrag*` callback. -/// -/// It will declare defeat if it determines that a gesture is not a -/// tap (e.g. if the pointer is dragged too far while it's contacting the -/// 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. -/// -/// 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. -/// -/// If this recognizer loses the arena (either by declaring defeat or by -/// another recognizer declaring victory) while the pointer is contacting the -/// screen, it will fire [onCancel] instead of [onTapUp] or [onDragEnd]. -/// -/// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer` -/// -/// Similar to [TapGestureRecognizer] and [DragGestureRecognizer], -/// [TapAndDragGestureRecognizer] will not aggresively declare victory when it detects -/// a tap, so when it is competing with those gesture recognizers and others it has a chance -/// of losing. -/// -/// When competing against [TapGestureRecognizer], if the pointer does not move past the tap -/// tolerance, then the recognizer that entered the arena first will win. In this case the -/// gesture detected is a tap. If the pointer does travel past the tap tolerance then this -/// recognizer will be declared winner by default. The gesture detected in this case is a drag. -/// -/// 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 { - /// Creates a tap and drag gesture recognizer. - /// - /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} - TapAndDragGestureRecognizer({ - super.debugOwner, - super.kind, - super.supportedDevices, - }) : _deadline = kPressTimeout, - dragStartBehavior = DragStartBehavior.start, - slopTolerance = kTouchSlop; - - /// Configure the behavior of offsets passed to [onDragStart]. - /// - /// If set to [DragStartBehavior.start], the [onDragStart] callback will be called - /// with the position of the pointer at the time this gesture recognizer won - /// the arena. If [DragStartBehavior.down], [onDragStart] will be called with - /// the position of the first detected down event for the pointer. When there - /// are no other gestures competing with this gesture in the arena, there's - /// no difference in behavior between the two settings. - /// - /// For more information about the gesture arena: - /// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation - /// - /// By default, the drag start behavior is [DragStartBehavior.start]. - /// - /// See also: - /// - /// * [DragGestureRecognizer.dragStartBehavior], which includes more details and an example. - DragStartBehavior dragStartBehavior; - - /// The frequency at which the [onDragUpdate] callback is called. - /// - /// The value defaults to null, meaning there is no delay for [onDragUpdate] callback. - /// - /// See also: - /// * [TextSelectionGestureDetector], which uses this parameter to avoid excessive updates - /// text layouts in text fields. - Duration? dragUpdateThrottleFrequency; - - /// An upper bound for the amount of taps that can belong to one tap series. - /// - /// When this limit is reached the series of taps being tracked by this - /// recognizer will be reset. - @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 - /// elapsed, or once the gestures has won the arena, whichever comes first. - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [TapDragDownDetails] object. - /// - /// {@template flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.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} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragDownDetails], which is passed as an argument to this callback. - GestureTapDragDownCallback? onTapDown; - - /// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapUp} - /// - /// This triggers on the up event, if the recognizer wins the arena with it - /// or has previously won. - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [TapDragUpDetails] object. - /// - /// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragUpDetails], which is passed as an argument to this callback. - GestureTapDragUpCallback? onTapUp; - - /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onStart} - /// - /// The position of the pointer is provided in the callback's `details` - /// argument, which is a [TapDragStartDetails] object. The [dragStartBehavior] - /// determines this position. - /// - /// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragStartDetails], which is passed as an argument to this callback. - GestureTapDragStartCallback? onDragStart; - - /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onUpdate} - /// - /// 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} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragUpdateDetails], which is passed as an argument to this callback. - GestureTapDragUpdateCallback? onDragUpdate; - - /// {@macro flutter.gestures.monodrag.DragGestureRecognizer.onEnd} - /// - /// The velocity is provided in the callback's `details` argument, which is a - /// [TapDragEndDetails] object. - /// - /// {@macro flutter.gestures.selectionrecognizers.TapAndDragGestureRecognizer.tapStatusTrackerData} - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - /// * [TapDragEndDetails], which is passed as an argument to this callback. - GestureTapDragEndCallback? onDragEnd; - - /// The pointer that previously triggered [onTapDown] did not complete. - /// - /// This is called when a [PointerCancelEvent] is tracked when the [onTapDown] callback - /// was previously called. - /// - /// It may also be called if a [PointerUpEvent] is tracked after the pointer has moved - /// past the tap tolerance but not past the drag tolerance, and the recognizer has not - /// yet won the arena. - /// - /// See also: - /// - /// * [kPrimaryButton], the button this callback responds to. - GestureCancelCallback? onCancel; - - // Tap related state. - bool _pastSlopTolerance = false; - bool _sentTapDown = false; - - // Primary pointer being tracked by this recognizer. - int? _primaryPointer; - Timer? _deadlineTimer; - // The recognizer will call [onTapDown] after this amount of time has elapsed - // since starting to track the primary pointer. - // - // [onTapDown] will not be called if the primary pointer is - // accepted, rejected, or all pointers are up or canceled before [_deadline]. - final Duration _deadline; - - // Drag related state. - _DragState _dragState = _DragState.ready; - PointerEvent? _start; - late OffsetPair _initialPosition; - late double _globalDistanceMoved; - OffsetPair? _correctedPosition; - - // For drag update throttle. - TapDragUpdateDetails? _lastDragUpdateDetails; - Timer? _dragUpdateThrottleTimer; - - final Set _acceptedActivePointers = {}; - - bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) { - return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings); - } - - // 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]. - // - // Once the drag gesture ends, any pending drag update will be fired - // immediately. See [_checkDragEnd]. - void _handleDragUpdateThrottled() { - assert(_lastDragUpdateDetails != null); - if (onDragUpdate != null) { - invokeCallback('onDragUpdate', () => onDragUpdate!(_lastDragUpdateDetails!)); - } - _dragUpdateThrottleTimer = null; - _lastDragUpdateDetails = null; - } - - @override - bool isPointerAllowed(PointerEvent event) { - if (_primaryPointer == null) { - switch (event.buttons) { - case kPrimaryButton: - if (onTapDown == null && - onDragStart == null && - onDragUpdate == null && - onDragEnd == null && - onTapUp == null && - onCancel == null) { - return false; - } - break; - default: - return false; - } - } else { - if (event.pointer != _primaryPointer) { - return false; - } - } - - return super.isPointerAllowed(event as PointerDownEvent); - } - - @override - void addAllowedPointer(PointerDownEvent event) { - if (_dragState == _DragState.ready) { - super.addAllowedPointer(event); - _primaryPointer = event.pointer; - _globalDistanceMoved = 0.0; - _dragState = _DragState.possible; - _initialPosition = OffsetPair(global: event.position, local: event.localPosition); - _deadlineTimer = Timer(_deadline, () => _didExceedDeadlineWithEvent(event)); - } - } - - @override - void handleNonAllowedPointer(PointerDownEvent event) { - // There can be multiple drags simultaneously. Their effects are combined. - if (event.buttons != kPrimaryButton) { - if (!_sentTapDown) { - super.handleNonAllowedPointer(event); - } - } - } - - @override - void acceptGesture(int pointer) { - if (pointer != _primaryPointer) { - return; - } - - _stopDeadlineTimer(); - - assert(!_acceptedActivePointers.contains(pointer)); - _acceptedActivePointers.add(pointer); - - // Called when this recognizer is accepted by the [GestureArena]. - if (currentDown != null) { - _checkTapDown(currentDown!); - } - - if (_start != null) { - _acceptDrag(_start!); - } - - if (currentUp != null) { - _checkTapUp(currentUp!); - } - } - - @override - void didStopTrackingLastPointer(int pointer) { - switch (_dragState) { - case _DragState.ready: - _checkCancel(); - resolve(GestureDisposition.rejected); - break; - - case _DragState.possible: - if (_pastSlopTolerance) { - // This means the pointer was not accepted as a tap. - if (_sentTapDown) { - // If the recognizer has already won the arena for the primary pointer being tracked - // but the pointer has exceeded the tap tolerance, then the pointer is accepted as a - // drag gesture. - if (currentDown != null) { - _acceptDrag(currentDown!); - _checkDragEnd(); - } - } else { - _checkCancel(); - resolve(GestureDisposition.rejected); - } - } else { - // The pointer is accepted as a tap. - if (currentUp != null) { - _checkTapUp(currentUp!); - } - } - break; - - case _DragState.accepted: - // For the case when the pointer has been accepted as a drag. - // Meaning [_checkTapDown] and [_checkDragStart] have already ran. - _checkDragEnd(); - break; - } - - _stopDeadlineTimer(); - _dragState = _DragState.ready; - _pastSlopTolerance = false; - } - - @override - void handleEvent(PointerEvent event) { - if (event.pointer != _primaryPointer) { - return; - } - super.handleEvent(event); - if (event is PointerMoveEvent) { - // Receiving a [PointerMoveEvent], does not automatically mean the pointer - // being tracked is doing a drag gesture. There is some drift that can happen - // between the initial [PointerDownEvent] and subsequent [PointerMoveEvent]s. - // Accessing [_pastSlopTolerance] lets us know if our tap has moved past the - // acceptable tolerance. If the pointer does not move past this tolerance than - // it is not considered a drag. - // - // To be recognized as a drag, the [PointerMoveEvent] must also have moved - // a sufficient global distance from the initial [PointerDownEvent] to be - // accepted as a drag. This logic is handled in [_hasSufficientGlobalDistanceToAccept]. - // - // The recognizer will also detect the gesture as a drag when the pointer - // 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!; - - if (_dragState == _DragState.accepted) { - _checkDragUpdate(event); - } else if (_dragState == _DragState.possible) { - if (_start == null) { - // Only check for a drag if the start of a drag was not already identified. - _checkDrag(event); - } - - // This can occur when the recognizer is accepted before a [PointerMoveEvent] has been - // received that moves the pointer a sufficient global distance to be considered a drag. - if (_start != null && _sentTapDown) { - _acceptDrag(_start!); - } - } - } else if (event is PointerUpEvent) { - if (_dragState == _DragState.possible) { - // The drag has not been accepted before a [PointerUpEvent], therefore the recognizer - // attempts to recognize a tap. - stopTrackingIfPointerNoLongerDown(event); - } else if (_dragState == _DragState.accepted) { - _giveUpPointer(event.pointer); - } - } else if (event is PointerCancelEvent) { - _dragState = _DragState.ready; - _giveUpPointer(event.pointer); - } - } - - @override - void rejectGesture(int pointer) { - if (pointer != _primaryPointer) { - return; - } - super.rejectGesture(pointer); - - _stopDeadlineTimer(); - _giveUpPointer(pointer); - _resetTaps(); - _resetDragUpdateThrottle(); - } - - @override - void dispose() { - _stopDeadlineTimer(); - _resetDragUpdateThrottle(); - super.dispose(); - } - - @override - String get debugDescription => 'tap_and_drag'; - - void _acceptDrag(PointerEvent event) { - _dragState = _DragState.accepted; - if (dragStartBehavior == DragStartBehavior.start) { - _initialPosition = _initialPosition + OffsetPair(global: event.delta, local: event.localDelta); - } - _checkDragStart(event); - if (event.localDelta != Offset.zero) { - final Matrix4? localToGlobal = event.transform != null ? Matrix4.tryInvert(event.transform!) : null; - final Offset correctedLocalPosition = _initialPosition.local + event.localDelta; - final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions( - untransformedEndPosition: correctedLocalPosition, - untransformedDelta: event.localDelta, - transform: localToGlobal, - ); - final OffsetPair updateDelta = OffsetPair(local: event.localDelta, global: globalUpdateDelta); - _correctedPosition = _initialPosition + updateDelta; // Only adds delta for down behaviour - _checkDragUpdate(event); - _correctedPosition = null; - } - } - - void _checkDrag(PointerMoveEvent event) { - final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!); - _globalDistanceMoved += PointerEvent.transformDeltaViaPositions( - transform: localToGlobalTransform, - untransformedDelta: event.localDelta, - untransformedEndPosition: event.localPosition - ).distance * 1.sign; - if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) { - _start = event; - } - } - - void _checkTapDown(PointerDownEvent event) { - if (_sentTapDown) { - return; - } - - final TapDragDownDetails details = TapDragDownDetails( - globalPosition: event.position, - localPosition: event.localPosition, - kind: getKindForPointer(event.pointer), - consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, - ); - - if (onTapDown != null) { - invokeCallback('onTapDown', () => onTapDown!(details)); - } - - _sentTapDown = true; - } - - void _checkTapUp(PointerUpEvent event) { - if (!_sentTapDown) { - return; - } - - final TapDragUpDetails upDetails = TapDragUpDetails( - kind: event.kind, - globalPosition: event.position, - localPosition: event.localPosition, - consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, - ); - - if (onTapUp != null) { - invokeCallback('onTapUp', () => onTapUp!(upDetails)); - } - - _resetTaps(); - if (!_acceptedActivePointers.remove(event.pointer)) { - resolvePointer(event.pointer, GestureDisposition.rejected); - } - } - - void _checkDragStart(PointerEvent event) { - if (onDragStart != null) { - final TapDragStartDetails details = TapDragStartDetails( - sourceTimeStamp: event.timeStamp, - globalPosition: _initialPosition.global, - localPosition: _initialPosition.local, - kind: getKindForPointer(event.pointer), - consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, - ); - - invokeCallback('onDragStart', () => onDragStart!(details)); - } - - _start = null; - } - - void _checkDragUpdate(PointerEvent event) { - final Offset globalPosition = _correctedPosition != null ? _correctedPosition!.global : event.position; - final Offset localPosition = _correctedPosition != null ? _correctedPosition!.local : event.localPosition; - - final TapDragUpdateDetails details = TapDragUpdateDetails( - sourceTimeStamp: event.timeStamp, - delta: event.localDelta, - globalPosition: globalPosition, - kind: getKindForPointer(event.pointer), - localPosition: localPosition, - offsetFromOrigin: globalPosition - _initialPosition.global, - localOffsetFromOrigin: localPosition - _initialPosition.local, - consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, - ); - - if (dragUpdateThrottleFrequency != null) { - _lastDragUpdateDetails = details; - // Only schedule a new timer if there's not one pending. - _dragUpdateThrottleTimer ??= Timer(dragUpdateThrottleFrequency!, _handleDragUpdateThrottled); - } else { - if (onDragUpdate != null) { - invokeCallback('onDragUpdate', () => onDragUpdate!(details)); - } - } - } - - void _checkDragEnd() { - if (_dragUpdateThrottleTimer != null) { - // If there's already an update scheduled, trigger it immediately and - // cancel the timer. - _dragUpdateThrottleTimer!.cancel(); - _handleDragUpdateThrottled(); - } - - final TapDragEndDetails endDetails = - TapDragEndDetails( - primaryVelocity: 0.0, - consecutiveTapCount: consecutiveTapCount, - keysPressedOnDown: keysPressedOnDown, - ); - - invokeCallback('onDragEnd', () => onDragEnd!(endDetails)); - - _resetTaps(); - _resetDragUpdateThrottle(); - } - - void _checkCancel() { - if (!_sentTapDown) { - // Do not fire tap cancel if [onTapDown] was never called. - return; - } - if (onCancel != null) { - invokeCallback('onCancel', onCancel!); - } - _resetDragUpdateThrottle(); - _resetTaps(); - } - - void _didExceedDeadlineWithEvent(PointerDownEvent event) { - _didExceedDeadline(); - } - - void _didExceedDeadline() { - if (currentDown != null) { - _checkTapDown(currentDown!); - - if (consecutiveTapCount > 1) { - // If our consecutive tap count is greater than 1, i.e. is a double tap or greater, - // then this recognizer declares victory to prevent the [LongPressGestureRecognizer] - // from declaring itself the winner if a double tap is held for too long. - resolve(GestureDisposition.accepted); - } - } - } - - void _giveUpPointer(int pointer) { - stopTrackingPointer(pointer); - // If the pointer was never accepted, then it is rejected since this recognizer is no longer - // interested in winning the gesture arena for it. - if (!_acceptedActivePointers.remove(pointer)) { - resolvePointer(pointer, GestureDisposition.rejected); - } - } - - void _resetTaps() { - _sentTapDown = false; - _primaryPointer = null; - } - - void _resetDragUpdateThrottle() { - if (dragUpdateThrottleFrequency == null) { - return; - } - _lastDragUpdateDetails = null; - if (_dragUpdateThrottleTimer != null) { - _dragUpdateThrottleTimer!.cancel(); - _dragUpdateThrottleTimer = null; - } - } - - void _stopDeadlineTimer() { - if (_deadlineTimer != null) { - _deadlineTimer!.cancel(); - _deadlineTimer = null; - } - } -} diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index cbfc61b3c126..8812ec5b5fb0 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -24,7 +24,6 @@ import 'gesture_detector.dart'; import 'magnifier.dart'; import 'overlay.dart'; import 'scrollable.dart'; -import 'tap_and_drag_gestures.dart'; import 'tap_region.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; @@ -36,6 +35,19 @@ export 'package:flutter/services.dart' show TextSelectionDelegate; /// called. const Duration _kDragSelectionUpdateThrottle = Duration(milliseconds: 50); +/// Signature for when a pointer that's dragging to select text has moved again. +/// +/// The first argument [startDetails] contains the details of the event that +/// initiated the dragging. +/// +/// The second argument [updateDetails] contains the details of the current +/// pointer movement. It's the same as the one passed to [DragGestureRecognizer.onUpdate]. +/// +/// This signature is different from [GestureDragUpdateCallback] to make it +/// easier for various text fields to use [TextSelectionGestureDetector] without +/// having to store the start position. +typedef DragSelectionUpdateCallback = void Function(DragStartDetails startDetails, DragUpdateDetails updateDetails); + /// The type for a Function that builds a toolbar's container with the given /// child. /// @@ -1895,23 +1907,6 @@ class TextSelectionGestureDetectorBuilder { && selection.end >= textPosition.offset; } - /// Returns true if position was on selection. - bool _positionOnSelection(Offset position, TextSelection? targetSelection) { - if (targetSelection == null) { - return false; - } - - final TextPosition textPosition = renderEditable.getPositionForPoint(position); - - return targetSelection.start <= textPosition.offset - && targetSelection.end >= textPosition.offset; - } - - /// Returns true if shift left or right is contained in the given set. - static bool _containsShift(Set keysPressed) { - return keysPressed.any({ LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight }.contains); - } - // Expand the selection to the given global position. // // Either base or extent will be moved to the last tapped position, whichever @@ -1999,6 +1994,15 @@ class TextSelectionGestureDetectorBuilder { /// The viewport offset pixels of the [RenderEditable] at the last drag start. double _dragStartViewportOffset = 0.0; + // Returns true iff either shift key is currently down. + bool get _isShiftPressed { + return HardwareKeyboard.instance.logicalKeysPressed + .any({ + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.shiftRight, + }.contains); + } + double get _scrollPosition { final ScrollableState? scrollableState = delegate.editableTextKey.currentContext == null @@ -2009,19 +2013,13 @@ class TextSelectionGestureDetectorBuilder { : scrollableState.position.pixels; } + // True iff a tap + shift has been detected but the tap has not yet come up. + bool _isShiftTapping = false; + // For a shift + tap + drag gesture, the TextSelection at the point of the // tap. Mac uses this value to reset to the original selection when an // inversion of the base and offset happens. - TextSelection? _dragStartSelection; - - // For tap + drag gesture on iOS, whether the position where the drag started - // was on the previous TextSelection. iOS uses this value to determine if - // the cursor should move on drag update. - // - // If the drag started on the previous selection then the cursor will move on - // drag update. If the drag did not start on the previous selection then the - // cursor will not move on drag update. - bool? _dragBeganOnPreviousSelection; + TextSelection? _shiftTapDragSelection; /// Handler for [TextSelectionGestureDetector.onTapDown]. /// @@ -2032,17 +2030,11 @@ class TextSelectionGestureDetectorBuilder { /// /// * [TextSelectionGestureDetector.onTapDown], which triggers this callback. @protected - void onTapDown(TapDragDownDetails details) { + void onTapDown(TapDownDetails details) { if (!delegate.selectionEnabled) { return; } - // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state - // in renderEditable. The gesture callbacks can use the details objects directly - // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap] - // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in - // renderEditable. When this migration is complete we should remove this hack. - // See https://github.com/flutter/flutter/issues/115130. - renderEditable.handleTapDown(TapDownDetails(globalPosition: details.globalPosition)); + renderEditable.handleTapDown(details); // The selection overlay should only be shown when the user is interacting // through a touch screen (via either a finger or a stylus). A mouse shouldn't // trigger the selection overlay. @@ -2056,20 +2048,21 @@ class TextSelectionGestureDetectorBuilder { || kind == PointerDeviceKind.stylus; // Handle shift + click selection if needed. - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); - // It is impossible to extend the selection when the shift key is pressed, if the - // renderEditable.selection is invalid. - final bool isShiftPressedValid = isShiftPressed && renderEditable.selection?.baseOffset != null; + final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: // On mobile platforms the selection is set on tap up. + if (_isShiftTapping) { + _isShiftTapping = false; + } break; case TargetPlatform.macOS: // On macOS, a shift-tapped unfocused field expands from 0, not from the // previous selection. if (isShiftPressedValid) { + _isShiftTapping = true; final TextSelection? fromSelection = renderEditable.hasFocus ? null : const TextSelection.collapsed(offset: 0); @@ -2089,6 +2082,7 @@ class TextSelectionGestureDetectorBuilder { case TargetPlatform.linux: case TargetPlatform.windows: if (isShiftPressedValid) { + _isShiftTapping = true; _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } @@ -2152,24 +2146,25 @@ class TextSelectionGestureDetectorBuilder { /// * [TextSelectionGestureDetector.onSingleTapUp], which triggers /// this callback. @protected - void onSingleTapUp(TapDragUpDetails details) { + void onSingleTapUp(TapUpDetails details) { if (delegate.selectionEnabled) { // Handle shift + click selection if needed. - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); - // It is impossible to extend the selection when the shift key is pressed, if the - // renderEditable.selection is invalid. - final bool isShiftPressedValid = isShiftPressed && renderEditable.selection?.baseOffset != null; + final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; switch (defaultTargetPlatform) { case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: editableText.hideToolbar(); // On desktop platforms the selection is set on tap down. + if (_isShiftTapping) { + _isShiftTapping = false; + } break; case TargetPlatform.android: editableText.hideToolbar(); editableText.showSpellCheckSuggestionsToolbar(); if (isShiftPressedValid) { + _isShiftTapping = true; _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } @@ -2178,6 +2173,7 @@ class TextSelectionGestureDetectorBuilder { case TargetPlatform.fuchsia: editableText.hideToolbar(); if (isShiftPressedValid) { + _isShiftTapping = true; _extendSelection(details.globalPosition, SelectionChangedCause.tap); return; } @@ -2187,6 +2183,7 @@ class TextSelectionGestureDetectorBuilder { if (isShiftPressedValid) { // On iOS, a shift-tapped unfocused field expands from 0, not from // the previous selection. + _isShiftTapping = true; final TextSelection? fromSelection = renderEditable.hasFocus ? null : const TextSelection.collapsed(offset: 0); @@ -2249,7 +2246,7 @@ class TextSelectionGestureDetectorBuilder { /// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers /// this callback. @protected - void onSingleTapCancel() { /* Subclass should override this method if needed. */ } + void onSingleTapCancel() {/* Subclass should override this method if needed. */} /// Handler for [TextSelectionGestureDetector.onSingleLongTapStart]. /// @@ -2419,13 +2416,7 @@ class TextSelectionGestureDetectorBuilder { /// * [onSecondaryTap], which is typically called after this. @protected void onSecondaryTapDown(TapDownDetails details) { - // TODO(Renzo-Olivares): Migrate text selection gestures away from saving state - // in renderEditable. The gesture callbacks can use the details objects directly - // in callbacks variants that provide them [TapGestureRecognizer.onSecondaryTap] - // vs [TapGestureRecognizer.onSecondaryTapUp] instead of having to track state in - // renderEditable. When this migration is complete we should remove this hack. - // See https://github.com/flutter/flutter/issues/115130. - renderEditable.handleSecondaryTapDown(TapDownDetails(globalPosition: details.globalPosition)); + renderEditable.handleSecondaryTapDown(details); _shouldShowSelectionToolbar = true; } @@ -2439,7 +2430,7 @@ class TextSelectionGestureDetectorBuilder { /// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this /// callback. @protected - void onDoubleTapDown(TapDragDownDetails details) { + void onDoubleTapDown(TapDownDetails details) { if (delegate.selectionEnabled) { renderEditable.selectWord(cause: SelectionChangedCause.tap); if (shouldShowSelectionToolbar) { @@ -2457,7 +2448,7 @@ class TextSelectionGestureDetectorBuilder { /// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers /// this callback. @protected - void onDragSelectionStart(TapDragStartDetails details) { + void onDragSelectionStart(DragStartDetails details) { if (!delegate.selectionEnabled) { return; } @@ -2466,18 +2457,8 @@ class TextSelectionGestureDetectorBuilder { || kind == PointerDeviceKind.touch || kind == PointerDeviceKind.stylus; - _dragStartSelection = renderEditable.selection; - _dragStartScrollOffset = _scrollPosition; - _dragStartViewportOffset = renderEditable.offset.pixels; - - if (details.consecutiveTapCount > 1) { - // Do not set the selection on a consecutive tap and drag. - return; - } - - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); - - if (isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) { + if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) { + _isShiftTapping = true; switch (defaultTargetPlatform) { case TargetPlatform.iOS: case TargetPlatform.macOS: @@ -2490,46 +2471,16 @@ class TextSelectionGestureDetectorBuilder { _extendSelection(details.globalPosition, SelectionChangedCause.drag); break; } + _shiftTapDragSelection = renderEditable.selection; } else { - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - case TargetPlatform.android: - case TargetPlatform.fuchsia: - switch (details.kind) { - case PointerDeviceKind.mouse: - case PointerDeviceKind.trackpad: - case PointerDeviceKind.stylus: - case PointerDeviceKind.invertedStylus: - renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - break; - case PointerDeviceKind.touch: - case PointerDeviceKind.unknown: - // For Android, Fucshia, and iOS platforms, a touch drag - // does not initiate unless the editable has focus. - if (renderEditable.hasFocus) { - renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - break; - case null: - break; - } - break; - case TargetPlatform.linux: - case TargetPlatform.macOS: - case TargetPlatform.windows: - renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - break; - } + renderEditable.selectPositionAt( + from: details.globalPosition, + cause: SelectionChangedCause.drag, + ); } + + _dragStartScrollOffset = _scrollPosition; + _dragStartViewportOffset = renderEditable.offset.pixels; } /// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate]. @@ -2542,14 +2493,12 @@ class TextSelectionGestureDetectorBuilder { /// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers /// this callback./lib/src/material/text_field.dart @protected - void onDragSelectionUpdate(TapDragUpdateDetails details) { + void onDragSelectionUpdate(DragStartDetails startDetails, DragUpdateDetails updateDetails) { if (!delegate.selectionEnabled) { return; } - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); - - if (!isShiftPressed) { + if (!_isShiftTapping) { // Adjust the drag start offset for possible viewport offset changes. final Offset editableOffset = renderEditable.maxLines == 1 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) @@ -2558,131 +2507,52 @@ class TextSelectionGestureDetectorBuilder { 0.0, _scrollPosition - _dragStartScrollOffset, ); - final Offset dragStartGlobalPosition = details.globalPosition - details.offsetFromOrigin; - - // Select word by word. - if (details.consecutiveTapCount == 2) { - return renderEditable.selectWordsInRange( - from: dragStartGlobalPosition - editableOffset - scrollableOffset, - to: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - // With a touch device, nothing should happen, unless there was a double tap, or - // there was a collapsed selection, and the tap/drag position is at the collapsed selection. - // In that case the caret should move with the drag position. - // - // With a mouse device, a drag should select the range from the origin of the drag - // to the current position of the drag. - switch (details.kind) { - case PointerDeviceKind.mouse: - case PointerDeviceKind.trackpad: - return renderEditable.selectPositionAt( - from: dragStartGlobalPosition - editableOffset - scrollableOffset, - to: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - case PointerDeviceKind.stylus: - case PointerDeviceKind.invertedStylus: - case PointerDeviceKind.touch: - case PointerDeviceKind.unknown: - _dragBeganOnPreviousSelection ??= _positionOnSelection(dragStartGlobalPosition, _dragStartSelection); - assert(_dragBeganOnPreviousSelection != null); - if (renderEditable.hasFocus - && _dragStartSelection!.isCollapsed - && _dragBeganOnPreviousSelection! - ) { - return renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - break; - case null: - break; - } - return; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - // With a precise pointer device, such as a mouse, trackpad, or stylus, - // the drag will select the text spanning the origin of the drag to the end of the drag. - // With a touch device, the cursor should move with the drag. - switch (details.kind) { - case PointerDeviceKind.mouse: - case PointerDeviceKind.trackpad: - case PointerDeviceKind.stylus: - case PointerDeviceKind.invertedStylus: - return renderEditable.selectPositionAt( - from: dragStartGlobalPosition - editableOffset - scrollableOffset, - to: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - case PointerDeviceKind.touch: - case PointerDeviceKind.unknown: - if (renderEditable.hasFocus) { - return renderEditable.selectPositionAt( - from: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - } - break; - case null: - break; - } - return; - case TargetPlatform.macOS: - case TargetPlatform.linux: - case TargetPlatform.windows: - return renderEditable.selectPositionAt( - from: dragStartGlobalPosition - editableOffset - scrollableOffset, - to: details.globalPosition, - cause: SelectionChangedCause.drag, - ); - } + return renderEditable.selectPositionAt( + from: startDetails.globalPosition - editableOffset - scrollableOffset, + to: updateDetails.globalPosition, + cause: SelectionChangedCause.drag, + ); } - if (_dragStartSelection!.isCollapsed + if (_shiftTapDragSelection!.isCollapsed || (defaultTargetPlatform != TargetPlatform.iOS && defaultTargetPlatform != TargetPlatform.macOS)) { - return _extendSelection(details.globalPosition, SelectionChangedCause.drag); + return _extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag); } // If the drag inverts the selection, Mac and iOS revert to the initial // selection. final TextSelection selection = editableText.textEditingValue.selection; - final TextPosition nextExtent = renderEditable.getPositionForPoint(details.globalPosition); + final TextPosition nextExtent = renderEditable.getPositionForPoint(updateDetails.globalPosition); final bool isShiftTapDragSelectionForward = - _dragStartSelection!.baseOffset < _dragStartSelection!.extentOffset; + _shiftTapDragSelection!.baseOffset < _shiftTapDragSelection!.extentOffset; final bool isInverted = isShiftTapDragSelectionForward - ? nextExtent.offset < _dragStartSelection!.baseOffset - : nextExtent.offset > _dragStartSelection!.baseOffset; - if (isInverted && selection.baseOffset == _dragStartSelection!.baseOffset) { + ? nextExtent.offset < _shiftTapDragSelection!.baseOffset + : nextExtent.offset > _shiftTapDragSelection!.baseOffset; + if (isInverted && selection.baseOffset == _shiftTapDragSelection!.baseOffset) { editableText.userUpdateTextEditingValue( editableText.textEditingValue.copyWith( selection: TextSelection( - baseOffset: _dragStartSelection!.extentOffset, + baseOffset: _shiftTapDragSelection!.extentOffset, extentOffset: nextExtent.offset, ), ), SelectionChangedCause.drag, ); } else if (!isInverted - && nextExtent.offset != _dragStartSelection!.baseOffset - && selection.baseOffset != _dragStartSelection!.baseOffset) { + && nextExtent.offset != _shiftTapDragSelection!.baseOffset + && selection.baseOffset != _shiftTapDragSelection!.baseOffset) { editableText.userUpdateTextEditingValue( editableText.textEditingValue.copyWith( selection: TextSelection( - baseOffset: _dragStartSelection!.baseOffset, + baseOffset: _shiftTapDragSelection!.baseOffset, extentOffset: nextExtent.offset, ), ), SelectionChangedCause.drag, ); } else { - _extendSelection(details.globalPosition, SelectionChangedCause.drag); + _extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag); } } @@ -2696,12 +2566,10 @@ class TextSelectionGestureDetectorBuilder { /// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this /// callback. @protected - void onDragSelectionEnd(TapDragEndDetails details) { - final bool isShiftPressed = _containsShift(details.keysPressedOnDown); - _dragBeganOnPreviousSelection = null; - - if (isShiftPressed) { - _dragStartSelection = null; + void onDragSelectionEnd(DragEndDetails details) { + if (_isShiftTapping) { + _isShiftTapping = false; + _shiftTapDragSelection = null; } } @@ -2740,8 +2608,8 @@ class TextSelectionGestureDetectorBuilder { /// /// An ordinary [GestureDetector] configured to handle events like tap and /// double tap will only recognize one or the other. This widget detects both: -/// the first tap and then any subsequent taps that occurs within a time limit -/// after the first. +/// first the tap and then, if another tap down occurs within a time limit, the +/// double tap. /// /// See also: /// @@ -2776,7 +2644,7 @@ class TextSelectionGestureDetector extends StatefulWidget { /// Called for every tap down including every tap down that's part of a /// double click or a long press, except touches that include enough movement /// to not qualify as taps (e.g. pans and flings). - final GestureTapDragDownCallback? onTapDown; + final GestureTapDownCallback? onTapDown; /// Called when a pointer has tapped down and the force of the pointer has /// just become greater than [ForcePressGestureRecognizer.startPressure]. @@ -2792,18 +2660,16 @@ class TextSelectionGestureDetector extends StatefulWidget { /// Called for a tap down event with the secondary mouse button. final GestureTapDownCallback? onSecondaryTapDown; - /// Called for the first tap in a series of taps, consecutive taps do not call - /// this method. - /// + /// Called for each distinct tap except for every second tap of a double tap. /// For example, if the detector was configured with [onTapDown] and /// [onDoubleTapDown], three quick taps would be recognized as a single tap - /// down, followed by a tap up, then a double tap down, followed by a single tap down. - final GestureTapDragUpCallback? onSingleTapUp; + /// down, followed by a double tap down, followed by a single tap down. + final GestureTapUpCallback? onSingleTapUp; /// Called for each touch that becomes recognized as a gesture that is not a /// short tap, such as a long tap or drag. It is called at the moment when /// another gesture from the touch is recognized. - final GestureCancelCallback? onSingleTapCancel; + final GestureTapCancelCallback? onSingleTapCancel; /// Called for a single long tap that's sustained for longer than /// [kLongPressTimeout] but not necessarily lifted. Not called for a @@ -2818,20 +2684,20 @@ class TextSelectionGestureDetector extends StatefulWidget { /// Called after a momentary hold or a short tap that is close in space and /// time (within [kDoubleTapTimeout]) to a previous short tap. - final GestureTapDragDownCallback? onDoubleTapDown; + final GestureTapDownCallback? onDoubleTapDown; /// Called when a mouse starts dragging to select text. - final GestureTapDragStartCallback? onDragSelectionStart; + final GestureDragStartCallback? onDragSelectionStart; /// Called repeatedly as a mouse moves while dragging. /// /// The frequency of calls is throttled to avoid excessive text layout /// operations in text fields. The throttling is controlled by the constant /// [_kDragSelectionUpdateThrottle]. - final GestureTapDragUpdateCallback? onDragSelectionUpdate; + final DragSelectionUpdateCallback? onDragSelectionUpdate; /// Called when a mouse that was previously dragging is released. - final GestureTapDragEndCallback? onDragSelectionEnd; + final GestureDragEndCallback? onDragSelectionEnd; /// How this gesture detector should behave during hit testing. /// @@ -2846,50 +2712,100 @@ class TextSelectionGestureDetector extends StatefulWidget { } class _TextSelectionGestureDetectorState extends State { - static int? _getDefaultMaxConsecutiveTap() => 2; + // Counts down for a short duration after a previous tap. Null otherwise. + Timer? _doubleTapTimer; + Offset? _lastTapOffset; + // True if a second tap down of a double tap is detected. Used to discard + // subsequent tap up / tap hold of the same tap. + bool _isDoubleTap = false; @override void dispose() { + _doubleTapTimer?.cancel(); + _dragUpdateThrottleTimer?.cancel(); super.dispose(); } // The down handler is force-run on success of a single tap and optimistically // run before a long press success. - void _handleTapDown(TapDragDownDetails details) { + void _handleTapDown(TapDownDetails details) { widget.onTapDown?.call(details); // This isn't detected as a double tap gesture in the gesture recognizer // because it's 2 single taps, each of which may do different things depending // on whether it's a single tap, the first tap of a double tap, the second // tap held down, a clean double tap etc. - - if (details.consecutiveTapCount == 2) { + if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) { + // If there was already a previous tap, the second down hold/tap is a + // double tap down. widget.onDoubleTapDown?.call(details); + + _doubleTapTimer!.cancel(); + _doubleTapTimeout(); + _isDoubleTap = true; } } - void _handleTapUp(TapDragUpDetails details) { - if (details.consecutiveTapCount == 1) { + void _handleTapUp(TapUpDetails details) { + if (!_isDoubleTap) { widget.onSingleTapUp?.call(details); + _lastTapOffset = details.globalPosition; + _doubleTapTimer?.cancel(); + _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); } + _isDoubleTap = false; } void _handleTapCancel() { widget.onSingleTapCancel?.call(); } - void _handleDragStart(TapDragStartDetails details) { + DragStartDetails? _lastDragStartDetails; + DragUpdateDetails? _lastDragUpdateDetails; + Timer? _dragUpdateThrottleTimer; + + void _handleDragStart(DragStartDetails details) { + assert(_lastDragStartDetails == null); + _lastDragStartDetails = details; widget.onDragSelectionStart?.call(details); } - void _handleDragUpdate(TapDragUpdateDetails details) { - widget.onDragSelectionUpdate?.call(details); + void _handleDragUpdate(DragUpdateDetails details) { + _lastDragUpdateDetails = details; + // Only schedule a new timer if there's no one pending. + _dragUpdateThrottleTimer ??= Timer(_kDragSelectionUpdateThrottle, _handleDragUpdateThrottled); } - void _handleDragEnd(TapDragEndDetails details) { + /// Drag updates are being throttled to avoid excessive text layouts in text + /// fields. The frequency of invocations is controlled by the constant + /// [_kDragSelectionUpdateThrottle]. + /// + /// Once the drag gesture ends, any pending drag update will be fired + /// immediately. See [_handleDragEnd]. + void _handleDragUpdateThrottled() { + assert(_lastDragStartDetails != null); + assert(_lastDragUpdateDetails != null); + widget.onDragSelectionUpdate?.call(_lastDragStartDetails!, _lastDragUpdateDetails!); + _dragUpdateThrottleTimer = null; + _lastDragUpdateDetails = null; + } + + void _handleDragEnd(DragEndDetails details) { + assert(_lastDragStartDetails != null); + if (_dragUpdateThrottleTimer != null) { + // If there's already an update scheduled, trigger it immediately and + // cancel the timer. + _dragUpdateThrottleTimer!.cancel(); + _handleDragUpdateThrottled(); + } widget.onDragSelectionEnd?.call(details); + _dragUpdateThrottleTimer = null; + _lastDragStartDetails = null; + _lastDragUpdateDetails = null; } void _forcePressStarted(ForcePressDetails details) { + _doubleTapTimer?.cancel(); + _doubleTapTimer = null; widget.onForcePressStart?.call(details); } @@ -2898,21 +2814,37 @@ class _TextSelectionGestureDetectorState extends State( - () => LongPressGestureRecognizer(debugOwner: this, supportedDevices: { PointerDeviceKind.touch }), + () => LongPressGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.touch), (LongPressGestureRecognizer instance) { instance ..onLongPressStart = _handleLongPressStart @@ -2945,21 +2880,16 @@ class _TextSelectionGestureDetectorState extends State( - () => TapAndDragGestureRecognizer(debugOwner: this, supportedDevices: { PointerDeviceKind.mouse, PointerDeviceKind.touch }), - (TapAndDragGestureRecognizer instance) { + gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers( + () => PanGestureRecognizer(debugOwner: this, supportedDevices: { PointerDeviceKind.mouse }), + (PanGestureRecognizer instance) { instance // Text selection should start from the position of the first pointer // down event. ..dragStartBehavior = DragStartBehavior.down - ..dragUpdateThrottleFrequency = _kDragSelectionUpdateThrottle - ..maxConsecutiveTap = _getDefaultMaxConsecutiveTap() - ..onTapDown = _handleTapDown - ..onDragStart = _handleDragStart - ..onDragUpdate = _handleDragUpdate - ..onDragEnd = _handleDragEnd - ..onTapUp = _handleTapUp - ..onCancel = _handleTapCancel; + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd; }, ); } diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 3eee850c7bec..fbacdec14a82 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -135,7 +135,6 @@ export 'src/widgets/spacer.dart'; export 'src/widgets/spell_check.dart'; export 'src/widgets/status_transitions.dart'; export 'src/widgets/table.dart'; -export 'src/widgets/tap_and_drag_gestures.dart'; export 'src/widgets/tap_region.dart'; export 'src/widgets/text.dart'; export 'src/widgets/text_editing_intents.dart'; diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index efc01e2271bf..b068ba236c53 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -2125,105 +2125,6 @@ void main() { }, ); - testWidgets( - 'Can double click + drag with a mouse to select word by word', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - CupertinoApp( - home: CupertinoPageScaffold( - child: CupertinoTextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(CupertinoTextField), testValue); - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h')); - - // Tap on text field to gain focus, and set selection to '|e'. - final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); - await tester.pump(); - await gesture.up(); - await tester.pump(); - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, testValue.indexOf('e')); - - // Here we tap on '|e' again, to register a double tap. This will select - // the word at the tapped position. - await gesture.down(ePos); - await tester.pump(); - - expect(controller.selection.baseOffset, 4); - expect(controller.selection.extentOffset, 7); - - // Drag, right after the double tap, to select word by word. - // Moving to the position of 'h', will extend the selection to 'ghi'. - await gesture.moveTo(hPos); - await tester.pumpAndSettle(); - - expect(controller.selection.baseOffset, testValue.indexOf('d')); - expect(controller.selection.extentOffset, testValue.indexOf('i') + 1); - }, - ); - - testWidgets( - 'Can double tap + drag to select word by word', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - CupertinoApp( - home: CupertinoPageScaffold( - child: CupertinoTextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(CupertinoTextField), testValue); - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h')); - - // Tap on text field to gain focus, and set selection to '|e'. - final TestGesture gesture = await tester.startGesture(ePos); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, testValue.indexOf('e')); - - // Here we tap on '|e' again, to register a double tap. This will select - // the word at the tapped position. - await gesture.down(ePos); - await tester.pumpAndSettle(); - - expect(controller.selection.baseOffset, 4); - expect(controller.selection.extentOffset, 7); - - // Drag, right after the double tap, to select word by word. - // Moving to the position of 'h', will extend the selection to 'ghi'. - await gesture.moveTo(hPos); - await tester.pumpAndSettle(); - - expect(controller.selection.baseOffset, testValue.indexOf('d')); - expect(controller.selection.extentOffset, testValue.indexOf('i') + 1); - }, - ); - testWidgets('Readonly text field does not have tap action', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); @@ -3550,7 +3451,7 @@ void main() { // The selection doesn't move beyond the left handle. There's always at // least 1 char selected. expect(controller.selection.extentOffset, 5); - }, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets('Dragging between multiple lines keeps the contact point at the same place on the handle on Android', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( @@ -3701,9 +3602,7 @@ void main() { renderEditable, ); handlePos = endpoints[0].point + startHandleAdjustment; - // Move handle a sufficient global distance so it can be considered a drag - // by the selection handle's [PanGestureRecognizer]. - newHandlePos = handlePos - (toNextLine * 2); + newHandlePos = handlePos - toNextLine; gesture = await tester.startGesture(handlePos, pointer: 7); await tester.pump(); await gesture.moveTo(newHandlePos); @@ -3727,12 +3626,6 @@ void main() { handlePos = endpoints[0].point + startHandleAdjustment; newHandlePos = handlePos + toNextLine; gesture = await tester.startGesture(handlePos, pointer: 7); - // Move handle up a small amount before dragging it down so the total global - // distance travelled can be accepted by the selection handle's [PanGestureRecognizer] as a drag. - // This way it can declare itself the winner before the [TapAndDragGestureRecognizer] that - // is on the selection overlay. - await tester.pump(); - await gesture.moveTo(handlePos - toNextLine); await tester.pump(); await gesture.moveTo(newHandlePos); await tester.pump(); @@ -4074,97 +3967,6 @@ void main() { expect(controller.selection.extentOffset, testValue.indexOf('g')); }); - testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - CupertinoApp( - home: CupertinoPageScaffold( - child: CupertinoTextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(CupertinoTextField), testValue); - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i')); - - // 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 [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(); - await tester.pumpAndSettle(kDoubleTapTimeout); - - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, 7); - - // 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 '|g', where our selection was previously, and move to '|i'. - await gesture.down(textOffsetToPosition(tester, 7)); - 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.iOS }), - ); - - testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - CupertinoApp( - home: CupertinoPageScaffold( - child: CupertinoTextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(CupertinoTextField), testValue); - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g')); - - // Tap on text field to gain focus, and set selection to '|e'. - // 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(); - await tester.pumpAndSettle(kDoubleTapTimeout); - - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, testValue.indexOf('e')); - - // Here we tap on '|d', and move to '|g'. - await gesture.down(textOffsetToPosition(tester, testValue.indexOf('d'))); - 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.android, TargetPlatform.fuchsia }), - ); - testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { int selectionChangedCount = 0; const String testValue = 'abc def ghi'; @@ -4598,7 +4400,7 @@ void main() { ); await tester.tapAt(endpoints[0].point + const Offset(1.0, 1.0)); await tester.pump(); - await tester.pump(const Duration(milliseconds: 300)); // skip past the frame where the opacity is zero + await tester.pump(const Duration(milliseconds: 200)); // skip past the frame where the opacity is zero // Verify the selection toolbar position Offset toolbarTopLeft = tester.getTopLeft(find.text('Paste')); @@ -6497,19 +6299,16 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); - await tester.pumpAndSettle(); if (isTargetPlatformMobile) { await gesture.up(); - // Not a double tap + drag. - await tester.pumpAndSettle(kDoubleTapTimeout); } + await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 23); // Expand the selection a bit. if (isTargetPlatformMobile) { await gesture.down(textOffsetToPosition(tester, 24)); - await tester.pumpAndSettle(); } await gesture.moveTo(textOffsetToPosition(tester, 28)); await tester.pumpAndSettle(); @@ -6604,19 +6403,16 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); - await tester.pumpAndSettle(); if (isTargetPlatformMobile) { await gesture.up(); - // Not a double tap + drag. - await tester.pumpAndSettle(kDoubleTapTimeout); } + await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 23); // Expand the selection a bit. if (isTargetPlatformMobile) { await gesture.down(textOffsetToPosition(tester, 24)); - await tester.pumpAndSettle(); } await gesture.moveTo(textOffsetToPosition(tester, 28)); await tester.pumpAndSettle(); @@ -6710,11 +6506,8 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); - await tester.pumpAndSettle(); if (isTargetPlatformMobile) { await gesture.up(); - // Not a double tap + drag. - await tester.pumpAndSettle(kDoubleTapTimeout); } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); @@ -6723,7 +6516,6 @@ void main() { // Expand the selection a bit. if (isTargetPlatformMobile) { await gesture.down(textOffsetToPosition(tester, 7)); - await tester.pumpAndSettle(); } await gesture.moveTo(textOffsetToPosition(tester, 5)); await tester.pumpAndSettle(); @@ -6818,19 +6610,16 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); - await tester.pumpAndSettle(); if (isTargetPlatformMobile) { await gesture.up(); - // Not a double tap + drag. - await tester.pumpAndSettle(kDoubleTapTimeout); } + await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 8); // Expand the selection a bit. if (isTargetPlatformMobile) { await gesture.down(textOffsetToPosition(tester, 7)); - await tester.pumpAndSettle(); } await gesture.moveTo(textOffsetToPosition(tester, 5)); await tester.pumpAndSettle(); diff --git a/packages/flutter/test/gestures/tap_test.dart b/packages/flutter/test/gestures/tap_test.dart index e54dc8ed685b..7001509f8b00 100644 --- a/packages/flutter/test/gestures/tap_test.dart +++ b/packages/flutter/test/gestures/tap_test.dart @@ -313,6 +313,7 @@ void main() { tester.route(down1); expect(tapsRecognized, 0); + tester.route(up2); expect(tapsRecognized, 0); GestureBinding.instance.gestureArena.sweep(2); diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 307f7f1dce32..ea3ba4063214 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2135,97 +2135,6 @@ void main() { expect(controller.selection.extentOffset, testValue.indexOf('g')); }); - testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(TextField), testValue); - await skipPastScrollingAnimation(tester); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i')); - - // 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. - final TestGesture gesture = await tester.startGesture(ePos); - await tester.pump(); - await gesture.up(); - await tester.pumpAndSettle(kDoubleTapTimeout); - - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, 7); - - // 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 '|g', where our selection was previously, and move to '|i'. - await gesture.down(textOffsetToPosition(tester, 7)); - 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.iOS }), - ); - - testWidgets('Can move cursor when dragging (Android)', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(TextField), testValue); - await skipPastScrollingAnimation(tester); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - 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. - final TestGesture gesture = await tester.startGesture(ePos); - await tester.pump(); - await gesture.up(); - await tester.pumpAndSettle(kDoubleTapTimeout); - - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, testValue.indexOf('e')); - - // Here we tap on '|d', and move to '|g'. - await gesture.down(textOffsetToPosition(tester, testValue.indexOf('d'))); - 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.android, TargetPlatform.fuchsia }), - ); - testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { int selectionChangedCount = 0; const String testValue = 'abc def ghi'; @@ -3070,9 +2979,7 @@ void main() { renderEditable, ); handlePos = endpoints[0].point + startHandleAdjustment; - // Move handle a sufficient global distance so it can be considered a drag - // by the selection handle's [PanGestureRecognizer]. - newHandlePos = handlePos - (toNextLine * 2); + newHandlePos = handlePos - toNextLine; gesture = await tester.startGesture(handlePos, pointer: 7); await tester.pump(); await gesture.moveTo(newHandlePos); @@ -3097,12 +3004,6 @@ void main() { newHandlePos = handlePos + toNextLine; gesture = await tester.startGesture(handlePos, pointer: 7); await tester.pump(); - // Move handle up a small amount before dragging it down so the total global - // distance travelled can be accepted by the selection handle's [PanGestureRecognizer] as a drag. - // This way it can declare itself the winner before the [TapAndDragGestureRecognizer] that - // is on the selection overlay. - await gesture.moveTo(handlePos - toNextLine); - await tester.pump(); await gesture.moveTo(newHandlePos); await tester.pump(); await gesture.up(); @@ -8810,105 +8711,6 @@ void main() { expect(widget.selectionControls, equals(materialTextSelectionControls)); }); - testWidgets( - 'Can double click + drag with a mouse to select word by word', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(TextField), testValue); - await skipPastScrollingAnimation(tester); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h')); - - // Tap on text field to gain focus, and set selection to '|e'. - final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); - await tester.pump(); - await gesture.up(); - await tester.pump(); - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, testValue.indexOf('e')); - - // Here we tap on '|e' again, to register a double tap. This will select - // the word at the tapped position. - await gesture.down(ePos); - await tester.pump(); - - expect(controller.selection.baseOffset, 4); - expect(controller.selection.extentOffset, 7); - - // Drag, right after the double tap, to select word by word. - // Moving to the position of 'h', will extend the selection to 'ghi'. - await gesture.moveTo(hPos); - await tester.pumpAndSettle(); - - expect(controller.selection.baseOffset, testValue.indexOf('d')); - expect(controller.selection.extentOffset, testValue.indexOf('i') + 1); - }, - ); - - testWidgets( - 'Can double tap + drag to select word by word', - (WidgetTester tester) async { - final TextEditingController controller = TextEditingController(); - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField( - dragStartBehavior: DragStartBehavior.down, - controller: controller, - ), - ), - ), - ); - - const String testValue = 'abc def ghi'; - await tester.enterText(find.byType(TextField), testValue); - await skipPastScrollingAnimation(tester); - - final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e')); - final Offset hPos = textOffsetToPosition(tester, testValue.indexOf('h')); - - // Tap on text field to gain focus, and set selection to '|e'. - final TestGesture gesture = await tester.startGesture(ePos); - await tester.pump(); - await gesture.up(); - await tester.pump(); - - expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, testValue.indexOf('e')); - - // Here we tap on '|e' again, to register a double tap. This will select - // the word at the tapped position. - await gesture.down(ePos); - await tester.pumpAndSettle(); - - expect(controller.selection.baseOffset, 4); - expect(controller.selection.extentOffset, 7); - - // Drag, right after the double tap, to select word by word. - // Moving to the position of 'h', will extend the selection to 'ghi'. - await gesture.moveTo(hPos); - await tester.pumpAndSettle(); - - expect(controller.selection.baseOffset, testValue.indexOf('d')); - expect(controller.selection.extentOffset, testValue.indexOf('i') + 1); - }, - ); - testWidgets( 'double tap on top of cursor also selects word', (WidgetTester tester) async { @@ -10181,7 +9983,7 @@ void main() { ); await tester.pump(); await gesture.up(); - await tester.pumpAndSettle(kDoubleTapTimeout); + await tester.pumpAndSettle(); expect( controller.selection, const TextSelection.collapsed(offset: 3), @@ -10568,7 +10370,7 @@ void main() { expect(controller.value.selection.extentOffset, 1); }, variant: const TargetPlatformVariant({ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux })); - testWidgets('Force press does not set selection on Android or Fuchsia touch devices', (WidgetTester tester) async { + testWidgets('force press does not select a word', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); @@ -10603,56 +10405,13 @@ void main() { pressureMin: 0, )); - await gesture.up(); - await tester.pump(); - // We don't want this gesture to select any word on Android. expect(controller.selection, const TextSelection.collapsed(offset: -1)); - expect(find.byType(TextButton), findsNothing); - }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia })); - - testWidgets('Force press sets selection on desktop platforms that do not support it', (WidgetTester tester) async { - final TextEditingController controller = TextEditingController( - text: 'Atwater Peel Sherbrooke Bonaventure', - ); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: TextField( - controller: controller, - ), - ), - ), - ); - - final Offset offset = tester.getTopLeft(find.byType(TextField)) + const Offset(150.0, 9.0); - - final int pointerValue = tester.nextPointer; - final TestGesture gesture = await tester.createGesture(); - await gesture.downWithCustomEvent( - offset, - PointerDownEvent( - pointer: pointerValue, - position: offset, - pressure: 0.0, - pressureMax: 6.0, - pressureMin: 0.0, - ), - ); - await gesture.updateWithCustomEvent(PointerMoveEvent( - pointer: pointerValue, - position: offset + const Offset(150.0, 9.0), - pressure: 0.5, - pressureMin: 0, - )); await gesture.up(); await tester.pump(); - - // We don't want this gesture to select any word on Android. - expect(controller.selection, const TextSelection.collapsed(offset: 9)); expect(find.byType(TextButton), findsNothing); - }, variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.windows })); + }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows })); testWidgets('force press selects word', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( @@ -12938,19 +12697,16 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); - await tester.pumpAndSettle(); if (isTargetPlatformMobile) { await gesture.up(); - // Not a double tap + drag. - await tester.pumpAndSettle(kDoubleTapTimeout); } + await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 23); // Expand the selection a bit. if (isTargetPlatformMobile) { await gesture.down(textOffsetToPosition(tester, 23)); - await tester.pumpAndSettle(); } await gesture.moveTo(textOffsetToPosition(tester, 28)); await tester.pumpAndSettle(); @@ -13148,19 +12904,16 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); - await tester.pumpAndSettle(); if (isTargetPlatformMobile) { await gesture.up(); - // Not a double tap + drag. - await tester.pumpAndSettle(kDoubleTapTimeout); } + await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 8); // Expand the selection a bit. if (isTargetPlatformMobile) { await gesture.down(textOffsetToPosition(tester, 8)); - await tester.pumpAndSettle(); } await gesture.moveTo(textOffsetToPosition(tester, 5)); await tester.pumpAndSettle(); diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index ce3bfde3c87e..2a1a992a9662 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -196,7 +196,7 @@ void main() { final TestGesture gesture = await tester.startGesture(textFieldStart, kind: PointerDeviceKind.mouse); await tester.pump(const Duration(seconds: 2)); await gesture.up(); - await tester.pumpAndSettle(kDoubleTapTimeout); + await tester.pumpAndSettle(); final FlutterError error = tester.takeException() as FlutterError; expect( @@ -3907,7 +3907,6 @@ void main() { expect(find.byType(CupertinoButton), findsNWidgets(1)); // Double tap selecting the same word somewhere else is fine. - await tester.pumpAndSettle(kDoubleTapTimeout); await tester.tapAt(selectableTextStart + const Offset(10.0, 5.0)); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. @@ -3929,7 +3928,6 @@ void main() { editableTextState.hideToolbar(); await tester.pumpAndSettle(); - await tester.pumpAndSettle(kDoubleTapTimeout); await tester.tapAt(selectableTextStart + const Offset(150.0, 5.0)); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. diff --git a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart b/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart deleted file mode 100644 index 32e50e3aeb21..000000000000 --- a/packages/flutter/test/widgets/tap_and_drag_gestures_test.dart +++ /dev/null @@ -1,552 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../gestures/gesture_tester.dart'; - -// Anything longer than [kDoubleTapTimeout] will reset the consecutive tap count. -final Duration kConsecutiveTapDelay = kDoubleTapTimeout ~/ 2; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - late List events; - late TapAndDragGestureRecognizer tapAndDrag; - - setUp(() { - events = []; - 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('dragstart#${details.consecutiveTapCount}'); - } - ..onDragUpdate = (TapDragUpdateDetails details) { - events.add('dragupdate#${details.consecutiveTapCount}'); - } - ..onDragEnd = (TapDragEndDetails details) { - events.add('dragend#${details.consecutiveTapCount}'); - } - ..onCancel = () { - events.add('cancel'); - }; - }); - - // Down/up pair 1: normal tap sequence - const PointerDownEvent down1 = PointerDownEvent( - pointer: 1, - position: Offset(10.0, 10.0), - ); - - const PointerUpEvent up1 = PointerUpEvent( - pointer: 1, - position: Offset(11.0, 9.0), - ); - - const PointerCancelEvent cancel1 = PointerCancelEvent( - pointer: 1, - ); - - // Down/up pair 2: normal tap sequence close to pair 1 - const PointerDownEvent down2 = PointerDownEvent( - pointer: 2, - position: Offset(12.0, 12.0), - ); - - const PointerUpEvent up2 = PointerUpEvent( - pointer: 2, - position: Offset(13.0, 11.0), - ); - - // Down/up pair 3: normal tap sequence close to pair 1 - const PointerDownEvent down3 = PointerDownEvent( - pointer: 3, - position: Offset(12.0, 12.0), - ); - - const PointerUpEvent up3 = PointerUpEvent( - pointer: 3, - position: Offset(13.0, 11.0), - ); - - // Down/up pair 4: normal tap sequence far away from pair 1 - const PointerDownEvent down4 = PointerDownEvent( - pointer: 4, - position: Offset(130.0, 130.0), - ); - - const PointerUpEvent up4 = PointerUpEvent( - pointer: 4, - position: Offset(131.0, 129.0), - ); - - // Down/move/up sequence 5: intervening motion - const PointerDownEvent down5 = PointerDownEvent( - pointer: 5, - position: Offset(10.0, 10.0), - ); - - const PointerMoveEvent move5 = PointerMoveEvent( - pointer: 5, - position: Offset(25.0, 25.0), - ); - - const PointerUpEvent up5 = PointerUpEvent( - pointer: 5, - position: Offset(25.0, 25.0), - ); - - testGesture('Recognizes consecutive taps', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - - events.clear(); - tester.async.elapse(kConsecutiveTapDelay); - tapAndDrag.addPointer(down2); - tester.closeArena(2); - tester.route(down2); - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - expect(events, ['down#2', 'up#2']); - - events.clear(); - tester.async.elapse(kConsecutiveTapDelay); - tapAndDrag.addPointer(down3); - tester.closeArena(3); - tester.route(down3); - tester.route(up3); - GestureBinding.instance.gestureArena.sweep(3); - expect(events, ['down#3', 'up#3']); - }); - - testGesture('Resets if times out in between taps', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - - events.clear(); - tester.async.elapse(const Duration(milliseconds: 1000)); - tapAndDrag.addPointer(down2); - tester.closeArena(2); - tester.route(down2); - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Resets if taps are far apart', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - - events.clear(); - tester.async.elapse(const Duration(milliseconds: 100)); - tapAndDrag.addPointer(down4); - tester.closeArena(4); - tester.route(down4); - tester.route(up4); - GestureBinding.instance.gestureArena.sweep(4); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Resets if consecutiveTapCount reaches maxConsecutiveTap', (GestureTester tester) { - // First tap. - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - - // Second tap. - events.clear(); - tapAndDrag.addPointer(down2); - tester.closeArena(2); - tester.route(down2); - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - expect(events, ['down#2', 'up#2']); - - // Third tap. - events.clear(); - tapAndDrag.addPointer(down3); - tester.closeArena(3); - tester.route(down3); - tester.route(up3); - GestureBinding.instance.gestureArena.sweep(3); - expect(events, ['down#3', 'up#3']); - - // Fourth tap. Here we arrived at the `maxConsecutiveTap` for `consecutiveTapCount` - // so our count should reset and our new count should be `1`. - events.clear(); - tapAndDrag.addPointer(down3); - tester.closeArena(3); - tester.route(down3); - tester.route(up3); - GestureBinding.instance.gestureArena.sweep(3); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Should recognize drag', (GestureTester tester) { - final TestPointer pointer = TestPointer(5); - final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0)); - tapAndDrag.addPointer(down); - tester.closeArena(5); - tester.route(down); - tester.route(pointer.move(const Offset(40.0, 45.0))); - tester.route(pointer.up()); - GestureBinding.instance.gestureArena.sweep(5); - expect(events, ['down#1', 'dragstart#1', 'dragupdate#1', 'dragend#1']); - }); - - testGesture('Recognizes consecutive taps + drag', (GestureTester tester) { - final TestPointer pointer = TestPointer(5); - final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0)); - tapAndDrag.addPointer(downA); - tester.closeArena(5); - tester.route(downA); - tester.route(pointer.up()); - GestureBinding.instance.gestureArena.sweep(5); - - tester.async.elapse(kConsecutiveTapDelay); - - final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); - tapAndDrag.addPointer(downB); - tester.closeArena(5); - tester.route(downB); - tester.route(pointer.up()); - GestureBinding.instance.gestureArena.sweep(5); - - tester.async.elapse(kConsecutiveTapDelay); - - final PointerDownEvent downC = pointer.down(const Offset(10.0, 10.0)); - tapAndDrag.addPointer(downC); - tester.closeArena(5); - tester.route(downC); - tester.route(pointer.move(const Offset(40.0, 45.0))); - tester.route(pointer.up()); - expect(events, [ - 'down#1', - 'up#1', - 'down#2', - 'up#2', - 'down#3', - 'dragstart#3', - 'dragupdate#3', - 'dragend#3']); - }); - - testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - before acceptance', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tapAndDrag.addPointer(down2); - tester.closeArena(1); - tester.route(down1); - - tester.closeArena(2); - tester.route(down2); - - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Calls tap up when the recognizer accepts before handleEvent is called', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tester.closeArena(1); - GestureBinding.instance.gestureArena.sweep(1); - tester.route(down1); - tester.route(up1); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Recognizer rejects pointer that is not the primary one (FILO) - before acceptance', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tapAndDrag.addPointer(down2); - tester.closeArena(1); - tester.route(down1); - - tester.closeArena(2); - tester.route(down2); - - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Recognizer rejects pointer that is not the primary one (FIFO) - after acceptance', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - - tapAndDrag.addPointer(down2); - tester.closeArena(2); - tester.route(down2); - - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Recognizer rejects pointer that is not the primary one (FILO) - after acceptance', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - - tapAndDrag.addPointer(down2); - tester.closeArena(2); - tester.route(down2); - - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Recognizer detects tap gesture when pointer does not move past tap tolerance', (GestureTester tester) { - // 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. - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Recognizer detects drag gesture when pointer moves past tap tolerance but not the drag minimum', (GestureTester tester) { - // 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. - tapAndDrag.addPointer(down5); - tester.closeArena(5); - tester.route(down5); - tester.route(move5); - tester.route(up5); - GestureBinding.instance.gestureArena.sweep(5); - expect(events, ['down#1', 'dragstart#1', 'dragend#1']); - }); - - testGesture('Recognizer loses when competing against a DragGestureRecognizer when the pointer travels minimum distance to be considered a drag', (GestureTester tester) { - final PanGestureRecognizer pans = PanGestureRecognizer() - ..onStart = (DragStartDetails details) { - events.add('panstart'); - } - ..onUpdate = (DragUpdateDetails details) { - events.add('panupdate'); - } - ..onEnd = (DragEndDetails details) { - events.add('panend'); - } - ..onCancel = () { - events.add('pancancel'); - }; - - 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. - tapAndDrag.addPointer(downB); - pans.addPointer(downB); - tester.closeArena(5); - tester.route(downB); - tester.route(pointer.move(const Offset(40.0, 45.0))); - tester.route(pointer.up()); - expect(events, [ - 'panstart', - 'panend']); - }); - - testGesture('Beats LongPressGestureRecognizer on a consecutive tap greater than one', (GestureTester tester) { - final LongPressGestureRecognizer longpress = LongPressGestureRecognizer() - ..onLongPressStart = (LongPressStartDetails details) { - events.add('longpressstart'); - } - ..onLongPressMoveUpdate = (LongPressMoveUpdateDetails details) { - events.add('longpressmoveupdate'); - } - ..onLongPressEnd = (LongPressEndDetails details) { - events.add('longpressend'); - } - ..onLongPressCancel = () { - events.add('longpresscancel'); - }; - - final TestPointer pointer = TestPointer(5); - final PointerDownEvent downA = pointer.down(const Offset(10.0, 10.0)); - tapAndDrag.addPointer(downA); - longpress.addPointer(downA); - tester.closeArena(5); - tester.route(downA); - tester.route(pointer.up()); - GestureBinding.instance.gestureArena.sweep(5); - - tester.async.elapse(kConsecutiveTapDelay); - - final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0)); - tapAndDrag.addPointer(downB); - longpress.addPointer(downB); - tester.closeArena(5); - tester.route(downB); - - tester.async.elapse(const Duration(milliseconds: 500)); - - tester.route(pointer.move(const Offset(40.0, 45.0))); - tester.route(pointer.up()); - expect(events, [ - 'longpresscancel', - 'down#1', - 'up#1', - 'down#2', - 'dragstart#2', - 'dragupdate#2', - 'dragend#2']); - }); - - testGesture('Beats TapGestureRecognizer when the pointer has not moved and this recognizer is the first in the arena', (GestureTester tester) { - final TapGestureRecognizer taps = TapGestureRecognizer() - ..onTapDown = (TapDownDetails details) { - events.add('tapdown'); - } - ..onTapUp = (TapUpDetails details) { - events.add('tapup'); - } - ..onTapCancel = () { - events.add('tapscancel'); - }; - - tapAndDrag.addPointer(down1); - taps.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Beats TapGestureRecognizer when the pointer has exceeded the slop tolerance', (GestureTester tester) { - final TapGestureRecognizer taps = TapGestureRecognizer() - ..onTapDown = (TapDownDetails details) { - events.add('tapdown'); - } - ..onTapUp = (TapUpDetails details) { - events.add('tapup'); - } - ..onTapCancel = () { - events.add('tapscancel'); - }; - - tapAndDrag.addPointer(down5); - taps.addPointer(down5); - tester.closeArena(5); - tester.route(down5); - tester.route(move5); - tester.route(up5); - GestureBinding.instance.gestureArena.sweep(5); - expect(events, ['down#1', 'dragstart#1', 'dragend#1']); - - events.clear(); - tester.async.elapse(const Duration(milliseconds: 1000)); - taps.addPointer(down1); - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['tapdown', 'tapup']); - }); - - testGesture('Ties with PanGestureRecognizer when pointer has not met sufficient global distance to be a drag', (GestureTester tester) { - final PanGestureRecognizer pans = PanGestureRecognizer() - ..onStart = (DragStartDetails details) { - events.add('panstart'); - } - ..onUpdate = (DragUpdateDetails details) { - events.add('panupdate'); - } - ..onEnd = (DragEndDetails details) { - events.add('panend'); - } - ..onCancel = () { - events.add('pancancel'); - }; - - tapAndDrag.addPointer(down5); - pans.addPointer(down5); - tester.closeArena(5); - tester.route(down5); - tester.route(move5); - tester.route(up5); - GestureBinding.instance.gestureArena.sweep(5); - expect(events, ['pancancel']); - }); - - testGesture('Defaults to drag when pointer dragged past slop tolerance', (GestureTester tester) { - tapAndDrag.addPointer(down5); - tester.closeArena(5); - tester.route(down5); - tester.route(move5); - tester.route(up5); - GestureBinding.instance.gestureArena.sweep(5); - expect(events, ['down#1', 'dragstart#1', 'dragend#1']); - - events.clear(); - tester.async.elapse(const Duration(milliseconds: 1000)); - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(up1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'up#1']); - }); - - testGesture('Fires cancel and resets for PointerCancelEvent', (GestureTester tester) { - tapAndDrag.addPointer(down1); - tester.closeArena(1); - tester.route(down1); - tester.route(cancel1); - GestureBinding.instance.gestureArena.sweep(1); - expect(events, ['down#1', 'cancel']); - - events.clear(); - tester.async.elapse(const Duration(milliseconds: 100)); - tapAndDrag.addPointer(down2); - tester.closeArena(2); - tester.route(down2); - tester.route(up2); - GestureBinding.instance.gestureArena.sweep(2); - expect(events, ['down#1', 'up#1']); - }); -} diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index 034b3418854d..f8508114870e 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -25,16 +25,16 @@ void main() { late int dragEndCount; const Offset forcePressOffset = Offset(400.0, 50.0); - void handleTapDown(TapDragDownDetails details) { tapCount++; } - void handleSingleTapUp(TapDragUpDetails details) { singleTapUpCount++; } + void handleTapDown(TapDownDetails details) { tapCount++; } + void handleSingleTapUp(TapUpDetails details) { singleTapUpCount++; } void handleSingleTapCancel() { singleTapCancelCount++; } void handleSingleLongTapStart(LongPressStartDetails details) { singleLongTapStartCount++; } - void handleDoubleTapDown(TapDragDownDetails details) { doubleTapDownCount++; } + void handleDoubleTapDown(TapDownDetails details) { doubleTapDownCount++; } void handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; } void handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; } - void handleDragSelectionStart(TapDragStartDetails details) { dragStartCount++; } - void handleDragSelectionUpdate(TapDragUpdateDetails details) { dragUpdateCount++; } - void handleDragSelectionEnd(TapDragEndDetails details) { dragEndCount++; } + void handleDragSelectionStart(DragStartDetails details) { dragStartCount++; } + void handleDragSelectionUpdate(DragStartDetails _, DragUpdateDetails details) { dragUpdateCount++; } + void handleDragSelectionEnd(DragEndDetails details) { dragEndCount++; } setUp(() { tapCount = 0; @@ -173,12 +173,7 @@ void main() { await gesture.moveBy(const Offset(100, 100)); await tester.pump(); expect(singleTapUpCount, 0); - // Before the move to TapAndDragGestureRecognizer the tapCount was 0 because the - // TapGestureRecognizer rejected itself when the initial pointer moved past a certain - // threshold. With TapAndDragGestureRecognizer, we have two thresholds, a normal tap - // threshold, and a drag threshold, so it is possible for the tap count to increase - // even though the original pointer has moved beyond the tap threshold. - expect(tapCount, 1); + expect(tapCount, 0); expect(singleTapCancelCount, 0); expect(doubleTapDownCount, 0); expect(singleLongTapStartCount, 0); @@ -186,7 +181,7 @@ void main() { await gesture.up(); // Nothing else happens on up. expect(singleTapUpCount, 0); - expect(tapCount, 1); + expect(tapCount, 0); expect(singleTapCancelCount, 0); expect(doubleTapDownCount, 0); expect(singleLongTapStartCount, 0); @@ -200,7 +195,7 @@ void main() { await tester.pump(); expect(singleTapUpCount, 0); expect(tapCount, 1); - expect(singleTapCancelCount, 0); + expect(singleTapCancelCount, 1); expect(doubleTapDownCount, 0); expect(singleLongTapStartCount, 0); }); @@ -375,7 +370,7 @@ void main() { expect(singleLongTapStartCount, 0); }); - testWidgets('a touch drag is recognized for text selection', (WidgetTester tester) async { + testWidgets('a touch drag is not recognized for text selection', (WidgetTester tester) async { await pumpGestureDetector(tester); final int pointerValue = tester.nextPointer; @@ -389,12 +384,11 @@ void main() { await gesture.up(); await tester.pumpAndSettle(); - expect(tapCount, 1); + expect(tapCount, 0); expect(singleTapUpCount, 0); - expect(singleTapCancelCount, 0); - expect(dragStartCount, 1); - expect(dragUpdateCount, 1); - expect(dragEndCount, 1); + expect(dragStartCount, 0); + expect(dragUpdateCount, 0); + expect(dragEndCount, 0); }); testWidgets('a mouse drag is recognized for text selection', (WidgetTester tester) async { @@ -412,11 +406,8 @@ void main() { await gesture.up(); await tester.pumpAndSettle(); - // The tap and drag gesture recognizer will detect the tap down, but not the tap up. - expect(tapCount, 1); - expect(singleTapCancelCount, 0); + expect(tapCount, 0); expect(singleTapUpCount, 0); - expect(dragStartCount, 1); expect(dragUpdateCount, 1); expect(dragEndCount, 1); @@ -437,11 +428,6 @@ void main() { await gesture.up(); await tester.pumpAndSettle(); - // The tap and drag gesture recognizer will detect the tap down, but not the tap up. - expect(tapCount, 1); - expect(singleTapCancelCount, 0); - expect(singleTapUpCount, 0); - expect(dragStartCount, 1); expect(dragUpdateCount, 1); expect(dragEndCount, 1); @@ -760,18 +746,12 @@ void main() { final Offset position = textOffsetToPosition(tester, 4); await tester.tapAt(position); - // Don't do a double tap drag. - await tester.pump(const Duration(milliseconds: 300)); + await tester.pump(); expect(controller.selection.isCollapsed, isTrue); expect(controller.selection.baseOffset, 4); final TestGesture gesture = await tester.startGesture(position, kind: PointerDeviceKind.mouse); - - // Checking that double-tap was not registered. - expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, 4); - addTearDown(gesture.removePointer); await tester.pump(); await gesture.moveTo(textOffsetToPosition(tester, 7));