// 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<LogicalKeyboardKey> keysPressedOnDown; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition)); properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition)); properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind)); properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount)); properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('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<LogicalKeyboardKey> keysPressedOnDown; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition)); properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition)); properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind)); properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount)); properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('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<LogicalKeyboardKey> keysPressedOnDown; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty<Duration?>('sourceTimeStamp', sourceTimeStamp)); properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition)); properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition)); properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind)); properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount)); properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('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( 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 occurred. 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 occurred. 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<LogicalKeyboardKey> keysPressedOnDown; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty<Duration?>('sourceTimeStamp', sourceTimeStamp)); properties.add(DiagnosticsProperty<Offset>('delta', delta)); properties.add(DiagnosticsProperty<double?>('primaryDelta', primaryDelta)); properties.add(DiagnosticsProperty<Offset>('globalPosition', globalPosition)); properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition)); properties.add(DiagnosticsProperty<PointerDeviceKind?>('kind', kind)); properties.add(DiagnosticsProperty<Offset>('offsetFromOrigin', offsetFromOrigin)); properties.add(DiagnosticsProperty<Offset>('localOffsetFromOrigin', localOffsetFromOrigin)); properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount)); properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('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( 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<LogicalKeyboardKey> keysPressedOnDown; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty<Velocity>('velocity', velocity)); properties.add(DiagnosticsProperty<double?>('primaryVelocity', primaryVelocity)); properties.add(DiagnosticsProperty<int>('consecutiveTapCount', consecutiveTapCount)); properties.add(DiagnosticsProperty<Set<LogicalKeyboardKey>>('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<LogicalKeyboardKey> get keysPressedOnDown => _keysPressedOnDown ?? <LogicalKeyboardKey>{}; // 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<LogicalKeyboardKey>? _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) { 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 aggressively declare victory when it detects /// a tap, so when it is competing with those gesture recognizers and others it has a chance /// of losing. /// /// 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.supportedDevices, super.allowedButtonsFilter, }) : _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; bool _wonArenaForPrimaryPointer = 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<int> _acceptedActivePointers = <int>{}; 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<void>('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; } 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 (!_wonArenaForPrimaryPointer) { 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!); } _wonArenaForPrimaryPointer = true; if (_start != null) { _acceptDrag(_start!); } if (currentUp != null) { _checkTapUp(currentUp!); } } @override void didStopTrackingLastPointer(int pointer) { switch (_dragState) { case _DragState.ready: _checkCancel(); resolve(GestureDisposition.rejected); case _DragState.possible: if (_pastSlopTolerance) { // This means the pointer was not accepted as a tap. if (_wonArenaForPrimaryPointer) { // 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!); } } case _DragState.accepted: // For the case when the pointer has been accepted as a drag. // Meaning [_checkTapDown] and [_checkDragStart] have already ran. _checkDragEnd(); } _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) { _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) { if (!_wonArenaForPrimaryPointer) { return; } _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 (!_wonArenaForPrimaryPointer) { 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<void>('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<void>('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, ); if (onDragEnd != null) { invokeCallback<void>('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; _wonArenaForPrimaryPointer = 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; } } }