monodrag.dart 24.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6
import 'package:vector_math/vector_math_64.dart';
7

8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import 'arena.dart';
import 'constants.dart';
import 'drag_details.dart';
import 'events.dart';
import 'recognizer.dart';
import 'velocity_tracker.dart';

enum _DragState {
  ready,
  possible,
  accepted,
}

/// 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`.
///
27
/// Used by [DragGestureRecognizer.onEnd].
28
typedef GestureDragEndCallback = void Function(DragEndDetails details);
29 30 31 32

/// Signature for when the pointer that previously triggered a
/// [GestureDragDownCallback] did not complete.
///
33
/// Used by [DragGestureRecognizer.onCancel].
34
typedef GestureDragCancelCallback = void Function();
35

36 37 38
/// Signature for a function that builds a [VelocityTracker].
///
/// Used by [DragGestureRecognizer.velocityTrackerBuilder].
39 40
typedef GestureVelocityTrackerBuilder = VelocityTracker Function(PointerEvent event);

41 42 43 44 45 46 47 48 49 50 51
/// Recognizes movement.
///
/// In contrast to [MultiDragGestureRecognizer], [DragGestureRecognizer]
/// recognizes a single gesture sequence for all the pointers it watches, which
/// means that the recognizer has at most one drag sequence active at any given
/// time regardless of how many pointers are in contact with the screen.
///
/// [DragGestureRecognizer] is not intended to be used directly. Instead,
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
52 53 54 55
/// [DragGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has at least one non-null callback. If it has no callbacks, it
/// is a no-op.
///
56 57
/// See also:
///
58 59 60
///  * [HorizontalDragGestureRecognizer], for left and right drags.
///  * [VerticalDragGestureRecognizer], for up and down drags.
///  * [PanGestureRecognizer], for drags that are not locked to a single axis.
61
abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
62
  /// Initialize the object.
63 64
  ///
  /// [dragStartBehavior] must not be null.
65
  ///
66
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
67
  DragGestureRecognizer({
68
    super.debugOwner,
69 70 71 72
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
73
    super.kind,
74
    this.dragStartBehavior = DragStartBehavior.start,
75
    this.velocityTrackerBuilder = _defaultBuilder,
76 77
    super.supportedDevices,
  }) : assert(dragStartBehavior != null);
78

79
  static VelocityTracker _defaultBuilder(PointerEvent event) => VelocityTracker.withKind(event.kind);
80 81

  /// Configure the behavior of offsets passed to [onStart].
82
  ///
83
  /// If set to [DragStartBehavior.start], the [onStart] callback will be called
84 85 86 87 88
  /// with the position of the pointer at the time this gesture recognizer won
  /// the arena. If [DragStartBehavior.down], [onStart] 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.
89 90
  ///
  /// For more information about the gesture arena:
91
  /// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation
92 93 94 95 96
  ///
  /// By default, the drag start behavior is [DragStartBehavior.start].
  ///
  /// ## Example:
  ///
97 98 99 100
  /// A [HorizontalDragGestureRecognizer] and a [VerticalDragGestureRecognizer]
  /// compete with each other. A finger presses down on the screen with
  /// offset (500.0, 500.0), and then moves to position (510.0, 500.0) before
  /// the [HorizontalDragGestureRecognizer] wins the arena. With
101
  /// [dragStartBehavior] set to [DragStartBehavior.down], the [onStart]
102 103 104
  /// callback will be called with position (500.0, 500.0). If it is
  /// instead set to [DragStartBehavior.start], [onStart] will be called with
  /// position (510.0, 500.0).
105
  DragStartBehavior dragStartBehavior;
106

107 108
  /// A pointer has contacted the screen with a primary button and might begin
  /// to move.
109 110 111
  ///
  /// The position of the pointer is provided in the callback's `details`
  /// argument, which is a [DragDownDetails] object.
112 113 114 115 116
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragDownDetails], which is passed as an argument to this callback.
117
  GestureDragDownCallback? onDown;
118

119 120
  /// A pointer has contacted the screen with a primary button and has begun to
  /// move.
121 122
  ///
  /// The position of the pointer is provided in the callback's `details`
123 124
  /// argument, which is a [DragStartDetails] object. The [dragStartBehavior]
  /// determines this position.
125 126 127 128 129
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragStartDetails], which is passed as an argument to this callback.
130
  GestureDragStartCallback? onStart;
131

132 133
  /// A pointer that is in contact with the screen with a primary button and
  /// moving has moved again.
134
  ///
135
  /// The distance traveled by the pointer since the last update is provided in
136
  /// the callback's `details` argument, which is a [DragUpdateDetails] object.
137 138 139 140 141
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragUpdateDetails], which is passed as an argument to this callback.
142
  GestureDragUpdateCallback? onUpdate;
143

144 145 146
  /// 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.
147 148 149
  ///
  /// The velocity is provided in the callback's `details` argument, which is a
  /// [DragEndDetails] object.
150 151 152 153 154
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragEndDetails], which is passed as an argument to this callback.
155
  GestureDragEndCallback? onEnd;
156 157

  /// The pointer that previously triggered [onDown] did not complete.
158 159 160 161
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
162
  GestureDragCancelCallback? onCancel;
163 164 165 166 167 168

  /// The minimum distance an input pointer drag must have moved to
  /// to be considered a fling gesture.
  ///
  /// This value is typically compared with the distance traveled along the
  /// scrolling axis. If null then [kTouchSlop] is used.
169
  double? minFlingDistance;
170 171 172 173 174 175

  /// The minimum velocity for an input pointer drag to be considered fling.
  ///
  /// This value is typically compared with the magnitude of fling gesture's
  /// velocity along the scrolling axis. If null then [kMinFlingVelocity]
  /// is used.
176
  double? minFlingVelocity;
177 178 179 180

  /// Fling velocity magnitudes will be clamped to this value.
  ///
  /// If null then [kMaxFlingVelocity] is used.
181
  double? maxFlingVelocity;
182

183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
  /// Determines the type of velocity estimation method to use for a potential
  /// drag gesture, when a new pointer is added.
  ///
  /// To estimate the velocity of a gesture, [DragGestureRecognizer] calls
  /// [velocityTrackerBuilder] when it starts to track a new pointer in
  /// [addAllowedPointer], and add subsequent updates on the pointer to the
  /// resulting velocity tracker, until the gesture recognizer stops tracking
  /// the pointer. This allows you to specify a different velocity estimation
  /// strategy for each allowed pointer added, by changing the type of velocity
  /// tracker this [GestureVelocityTrackerBuilder] returns.
  ///
  /// If left unspecified the default [velocityTrackerBuilder] creates a new
  /// [VelocityTracker] for every pointer added.
  ///
  /// See also:
  ///
  ///  * [VelocityTracker], a velocity tracker that uses least squares estimation
  ///    on the 20 most recent pointer data samples. It's a well-rounded velocity
  ///    tracker and is used by default.
  ///  * [IOSScrollViewFlingVelocityTracker], a specialized velocity tracker for
  ///    determining the initial fling velocity for a [Scrollable] on iOS, to
  ///    match the native behavior on that platform.
  GestureVelocityTrackerBuilder velocityTrackerBuilder;

207
  _DragState _state = _DragState.ready;
208 209 210
  late OffsetPair _initialPosition;
  late OffsetPair _pendingDragOffset;
  Duration? _lastPendingEventTimestamp;
211 212
  // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
  // different set of buttons, the gesture is canceled.
213 214
  int? _initialButtons;
  Matrix4? _lastTransform;
215 216 217 218 219

  /// Distance moved in the global coordinate space of the screen in drag direction.
  ///
  /// If drag is only allowed along a defined axis, this value may be negative to
  /// differentiate the direction of the drag.
220
  late double _globalDistanceMoved;
221

222 223 224 225 226
  /// Determines if a gesture is a fling or not based on velocity.
  ///
  /// A fling calls its gesture end callback with a velocity, allowing the
  /// provider of the callback to respond by carrying the gesture forward with
  /// inertia, for example.
227
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind);
228

229
  Offset _getDeltaForDetails(Offset delta);
230
  double? _getPrimaryValueFromOffset(Offset value);
231
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop);
232 233 234

  final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
  @override
  bool isPointerAllowed(PointerEvent event) {
    if (_initialButtons == null) {
      switch (event.buttons) {
        case kPrimaryButton:
          if (onDown == null &&
              onStart == null &&
              onUpdate == null &&
              onEnd == null &&
              onCancel == null)
            return false;
          break;
        default:
          return false;
      }
    } else {
      // There can be multiple drags simultaneously. Their effects are combined.
      if (event.buttons != _initialButtons) {
        return false;
      }
    }
256
    return super.isPointerAllowed(event as PointerDownEvent);
257 258
  }

259
  void _addPointer(PointerEvent event) {
260
    _velocityTrackers[event.pointer] = velocityTrackerBuilder(event);
261 262
    if (_state == _DragState.ready) {
      _state = _DragState.possible;
263 264 265
      _initialPosition = OffsetPair(global: event.position, local: event.localPosition);
      _pendingDragOffset = OffsetPair.zero;
      _globalDistanceMoved = 0.0;
266
      _lastPendingEventTimestamp = event.timeStamp;
267
      _lastTransform = event.transform;
268
      _checkDown();
269 270
    } else if (_state == _DragState.accepted) {
      resolve(GestureDisposition.accepted);
271 272 273
    }
  }

274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
  @override
  void addAllowedPointer(PointerDownEvent event) {
    super.addAllowedPointer(event);
    if (_state == _DragState.ready) {
      _initialButtons = event.buttons;
    }
    _addPointer(event);
  }

  @override
  void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) {
    super.addAllowedPointerPanZoom(event);
    startTrackingPointer(event.pointer, event.transform);
    if (_state == _DragState.ready) {
      _initialButtons = kPrimaryButton;
    }
    _addPointer(event);
  }

293 294 295
  @override
  void handleEvent(PointerEvent event) {
    assert(_state != _DragState.ready);
296 297 298 299 300
    if (!event.synthesized &&
        (event is PointerDownEvent ||
         event is PointerMoveEvent ||
         event is PointerPanZoomStartEvent ||
         event is PointerPanZoomUpdateEvent)) {
301
      final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
302
      assert(tracker != null);
303 304 305 306 307 308
      if (event is PointerPanZoomStartEvent) {
        tracker.addPosition(event.timeStamp, Offset.zero);
      } else if (event is PointerPanZoomUpdateEvent) {
        tracker.addPosition(event.timeStamp, event.pan);
      } else {
        tracker.addPosition(event.timeStamp, event.localPosition);
309
      }
310 311 312 313 314 315 316 317 318 319
    }
    if (event is PointerMoveEvent && event.buttons != _initialButtons) {
      _giveUpPointer(event.pointer);
      return;
    }
    if (event is PointerMoveEvent || event is PointerPanZoomUpdateEvent) {
      final Offset delta = (event is PointerMoveEvent) ? event.delta : (event as PointerPanZoomUpdateEvent).panDelta;
      final Offset localDelta = (event is PointerMoveEvent) ? event.localDelta : (event as PointerPanZoomUpdateEvent).localPanDelta;
      final Offset position = (event is PointerMoveEvent) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent).pan);
      final Offset localPosition = (event is PointerMoveEvent) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent).localPan);
320
      if (_state == _DragState.accepted) {
321 322
        _checkUpdate(
          sourceTimeStamp: event.timeStamp,
323 324 325 326
          delta: _getDeltaForDetails(localDelta),
          primaryDelta: _getPrimaryValueFromOffset(localDelta),
          globalPosition: position,
          localPosition: localPosition,
327
        );
328
      } else {
329
        _pendingDragOffset += OffsetPair(local: localDelta, global: delta);
330
        _lastPendingEventTimestamp = event.timeStamp;
331
        _lastTransform = event.transform;
332
        final Offset movedLocally = _getDeltaForDetails(localDelta);
333
        final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
334 335 336
        _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
          transform: localToGlobalTransform,
          untransformedDelta: movedLocally,
337
          untransformedEndPosition: localPosition
338
        ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
339
        if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop))
340 341 342
          resolve(GestureDisposition.accepted);
      }
    }
343
    if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
344
      _giveUpPointer(event.pointer);
345
    }
346 347
  }

348 349
  final Set<int> _acceptedActivePointers = <int>{};

350 351
  @override
  void acceptGesture(int pointer) {
352 353
    assert(!_acceptedActivePointers.contains(pointer));
    _acceptedActivePointers.add(pointer);
354 355
    if (_state != _DragState.accepted) {
      _state = _DragState.accepted;
356
      final OffsetPair delta = _pendingDragOffset;
357 358
      final Duration timestamp = _lastPendingEventTimestamp!;
      final Matrix4? transform = _lastTransform;
359
      final Offset localUpdateDelta;
360 361 362
      switch (dragStartBehavior) {
        case DragStartBehavior.start:
          _initialPosition = _initialPosition + delta;
363
          localUpdateDelta = Offset.zero;
364 365
          break;
        case DragStartBehavior.down:
366
          localUpdateDelta = _getDeltaForDetails(delta.local);
367 368
          break;
      }
369
      _pendingDragOffset = OffsetPair.zero;
370
      _lastPendingEventTimestamp = null;
371
      _lastTransform = null;
372
      _checkStart(timestamp, pointer);
373
      if (localUpdateDelta != Offset.zero && onUpdate != null) {
374
        final Matrix4? localToGlobal = transform != null ? Matrix4.tryInvert(transform) : null;
375 376 377 378 379 380 381 382
        final Offset correctedLocalPosition = _initialPosition.local + localUpdateDelta;
        final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions(
          untransformedEndPosition: correctedLocalPosition,
          untransformedDelta: localUpdateDelta,
          transform: localToGlobal,
        );
        final OffsetPair updateDelta = OffsetPair(local: localUpdateDelta, global: globalUpdateDelta);
        final OffsetPair correctedPosition = _initialPosition + updateDelta; // Only adds delta for down behaviour
383
        _checkUpdate(
384
          sourceTimeStamp: timestamp,
385 386 387 388
          delta: localUpdateDelta,
          primaryDelta: _getPrimaryValueFromOffset(localUpdateDelta),
          globalPosition: correctedPosition.global,
          localPosition: correctedPosition.local,
389
        );
390
      }
391 392 393 394
      // This acceptGesture might have been called only for one pointer, instead
      // of all pointers. Resolve all pointers to `accepted`. This won't cause
      // infinite recursion because an accepted pointer won't be accepted again.
      resolve(GestureDisposition.accepted);
395 396 397 398 399
    }
  }

  @override
  void rejectGesture(int pointer) {
400
    _giveUpPointer(pointer);
401 402 403 404
  }

  @override
  void didStopTrackingLastPointer(int pointer) {
405 406 407 408 409 410 411 412 413 414 415 416 417
    assert(_state != _DragState.ready);
    switch(_state) {
      case _DragState.ready:
        break;

      case _DragState.possible:
        resolve(GestureDisposition.rejected);
        _checkCancel();
        break;

      case _DragState.accepted:
        _checkEnd(pointer);
        break;
418
    }
419 420
    _velocityTrackers.clear();
    _initialButtons = null;
421
    _state = _DragState.ready;
422
  }
423

424
  void _giveUpPointer(int pointer) {
425
    stopTrackingPointer(pointer);
426 427 428
    // If we never accepted the pointer, we reject it since we are no longer
    // interested in winning the gesture arena for it.
    if (!_acceptedActivePointers.remove(pointer))
429
      resolvePointer(pointer, GestureDisposition.rejected);
430 431
  }

432 433
  void _checkDown() {
    assert(_initialButtons == kPrimaryButton);
434 435 436 437 438
    if (onDown != null) {
      final DragDownDetails details = DragDownDetails(
        globalPosition: _initialPosition.global,
        localPosition: _initialPosition.local,
      );
439
      invokeCallback<void>('onDown', () => onDown!(details));
440
    }
441 442
  }

443
  void _checkStart(Duration timestamp, int pointer) {
444
    assert(_initialButtons == kPrimaryButton);
445 446 447 448 449 450 451
    if (onStart != null) {
      final DragStartDetails details = DragStartDetails(
        sourceTimeStamp: timestamp,
        globalPosition: _initialPosition.global,
        localPosition: _initialPosition.local,
        kind: getKindForPointer(pointer),
      );
452
      invokeCallback<void>('onStart', () => onStart!(details));
453
    }
454 455 456
  }

  void _checkUpdate({
457 458 459 460 461
    Duration? sourceTimeStamp,
    required Offset delta,
    double? primaryDelta,
    required Offset globalPosition,
    Offset? localPosition,
462 463
  }) {
    assert(_initialButtons == kPrimaryButton);
464 465 466 467 468 469 470 471
    if (onUpdate != null) {
      final DragUpdateDetails details = DragUpdateDetails(
        sourceTimeStamp: sourceTimeStamp,
        delta: delta,
        primaryDelta: primaryDelta,
        globalPosition: globalPosition,
        localPosition: localPosition,
      );
472
      invokeCallback<void>('onUpdate', () => onUpdate!(details));
473
    }
474 475 476 477 478 479 480
  }

  void _checkEnd(int pointer) {
    assert(_initialButtons == kPrimaryButton);
    if (onEnd == null)
      return;

481
    final VelocityTracker tracker = _velocityTrackers[pointer]!;
482 483
    assert(tracker != null);

484 485
    final DragEndDetails details;
    final String Function() debugReport;
486

487
    final VelocityEstimate? estimate = tracker.getVelocityEstimate();
488
    if (estimate != null && isFlingGesture(estimate, tracker.kind)) {
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
      final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
        .clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
      details = DragEndDetails(
        velocity: velocity,
        primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
      );
      debugReport = () {
        return '$estimate; fling at $velocity.';
      };
    } else {
      details = DragEndDetails(
        primaryVelocity: 0.0,
      );
      debugReport = () {
        if (estimate == null)
          return 'Could not estimate velocity.';
        return '$estimate; judged to not be a fling.';
      };
507
    }
508
    invokeCallback<void>('onEnd', () => onEnd!(details), debugReport: debugReport);
509 510 511 512 513
  }

  void _checkCancel() {
    assert(_initialButtons == kPrimaryButton);
    if (onCancel != null)
514
      invokeCallback<void>('onCancel', onCancel!);
515 516 517 518 519 520 521
  }

  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }
522 523 524
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
525
    properties.add(EnumProperty<DragStartBehavior>('start behavior', dragStartBehavior));
526
  }
527 528 529 530 531 532 533 534
}

/// Recognizes movement in the vertical direction.
///
/// Used for vertical scrolling.
///
/// See also:
///
535 536 537 538
///  * [HorizontalDragGestureRecognizer], for a similar recognizer but for
///    horizontal movement.
///  * [MultiDragGestureRecognizer], for a family of gesture recognizers that
///    track each touch point independently.
539
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
540
  /// Create a gesture recognizer for interactions in the vertical axis.
541
  ///
542
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
543
  VerticalDragGestureRecognizer({
544
    super.debugOwner,
545 546 547 548
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
549 550 551
    super.kind,
    super.supportedDevices,
  });
552

553
  @override
554
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
555
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
556
    final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
557 558 559 560
    return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
  }

  @override
561 562
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
    return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
563
  }
564 565

  @override
566
  Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);
567 568 569 570 571

  @override
  double _getPrimaryValueFromOffset(Offset value) => value.dy;

  @override
572
  String get debugDescription => 'vertical drag';
573 574 575 576 577 578 579 580
}

/// Recognizes movement in the horizontal direction.
///
/// Used for horizontal scrolling.
///
/// See also:
///
581 582 583 584
///  * [VerticalDragGestureRecognizer], for a similar recognizer but for
///    vertical movement.
///  * [MultiDragGestureRecognizer], for a family of gesture recognizers that
///    track each touch point independently.
585
class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
586
  /// Create a gesture recognizer for interactions in the horizontal axis.
587
  ///
588
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
589
  HorizontalDragGestureRecognizer({
590
    super.debugOwner,
591 592 593 594
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
595 596 597
    super.kind,
    super.supportedDevices,
  });
598

599
  @override
600
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
601
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
602
    final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
603 604 605 606
    return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance;
  }

  @override
607 608
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
    return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
609
  }
610 611

  @override
612
  Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0);
613 614 615 616 617

  @override
  double _getPrimaryValueFromOffset(Offset value) => value.dx;

  @override
618
  String get debugDescription => 'horizontal drag';
619 620 621 622 623 624
}

/// Recognizes movement both horizontally and vertically.
///
/// See also:
///
625 626 627 628 629
///  * [ImmediateMultiDragGestureRecognizer], for a similar recognizer that
///    tracks each touch point independently.
///  * [DelayedMultiDragGestureRecognizer], for a similar recognizer that
///    tracks each touch point independently, but that doesn't start until
///    some time has passed.
630
class PanGestureRecognizer extends DragGestureRecognizer {
631
  /// Create a gesture recognizer for tracking movement on a plane.
632
  PanGestureRecognizer({
633 634 635
    super.debugOwner,
    super.supportedDevices,
  });
636

637
  @override
638
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
639
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
640
    final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
641 642
    return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity
        && estimate.offset.distanceSquared > minDistance * minDistance;
643 644 645
  }

  @override
646 647
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
    return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
648 649 650 651 652 653
  }

  @override
  Offset _getDeltaForDetails(Offset delta) => delta;

  @override
654
  double? _getPrimaryValueFromOffset(Offset value) => null;
655 656

  @override
657
  String get debugDescription => 'pan';
658
}