monodrag.dart 28.4 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 6
import 'package:flutter/foundation.dart';

7 8 9 10 11 12
import 'constants.dart';
import 'drag_details.dart';
import 'events.dart';
import 'recognizer.dart';
import 'velocity_tracker.dart';

13 14 15 16 17 18 19 20 21 22
export 'dart:ui' show PointerDeviceKind;

export 'package:flutter/foundation.dart' show DiagnosticPropertiesBuilder;

export 'drag.dart' show DragEndDetails, DragUpdateDetails;
export 'drag_details.dart' show DragDownDetails, DragStartDetails, DragUpdateDetails, GestureDragDownCallback, GestureDragStartCallback, GestureDragUpdateCallback;
export 'events.dart' show PointerDownEvent, PointerEvent, PointerPanZoomStartEvent;
export 'recognizer.dart' show DragStartBehavior;
export 'velocity_tracker.dart' show VelocityEstimate, VelocityTracker;

23 24 25 26 27 28
enum _DragState {
  ready,
  possible,
  accepted,
}

29
/// {@template flutter.gestures.monodrag.GestureDragEndCallback}
30 31 32 33 34
/// 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`.
35
/// {@endtemplate}
36
///
37
/// Used by [DragGestureRecognizer.onEnd].
38
typedef GestureDragEndCallback = void Function(DragEndDetails details);
39 40 41 42

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

46 47 48
/// Signature for a function that builds a [VelocityTracker].
///
/// Used by [DragGestureRecognizer.velocityTrackerBuilder].
49 50
typedef GestureVelocityTrackerBuilder = VelocityTracker Function(PointerEvent event);

51 52 53 54 55 56 57 58 59 60 61
/// 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.
///
62 63
/// [DragGestureRecognizer] competes on pointer events only when it has at
/// least one non-null callback. If it has no callbacks, it is a no-op.
64
///
65 66
/// See also:
///
67 68 69
///  * [HorizontalDragGestureRecognizer], for left and right drags.
///  * [VerticalDragGestureRecognizer], for up and down drags.
///  * [PanGestureRecognizer], for drags that are not locked to a single axis.
70
abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
71
  /// Initialize the object.
72
  ///
73
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
74
  DragGestureRecognizer({
75
    super.debugOwner,
76
    this.dragStartBehavior = DragStartBehavior.start,
77
    this.velocityTrackerBuilder = _defaultBuilder,
78
    this.onlyAcceptDragOnThreshold = false,
79
    super.supportedDevices,
80 81
    AllowedButtonsFilter? allowedButtonsFilter,
  }) : super(allowedButtonsFilter: allowedButtonsFilter ?? _defaultButtonAcceptBehavior);
82

83
  static VelocityTracker _defaultBuilder(PointerEvent event) => VelocityTracker.withKind(event.kind);
84

85 86 87
  // Accept the input if, and only if, [kPrimaryButton] is pressed.
  static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;

88
  /// Configure the behavior of offsets passed to [onStart].
89
  ///
90
  /// If set to [DragStartBehavior.start], the [onStart] callback will be called
91 92 93 94 95
  /// 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.
96 97
  ///
  /// For more information about the gesture arena:
98
  /// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation
99 100 101 102 103
  ///
  /// By default, the drag start behavior is [DragStartBehavior.start].
  ///
  /// ## Example:
  ///
104 105 106 107
  /// 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
108
  /// [dragStartBehavior] set to [DragStartBehavior.down], the [onStart]
109 110 111
  /// 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).
112
  DragStartBehavior dragStartBehavior;
113

114 115
  /// A pointer has contacted the screen with a primary button and might begin
  /// to move.
116 117 118
  ///
  /// The position of the pointer is provided in the callback's `details`
  /// argument, which is a [DragDownDetails] object.
119 120 121
  ///
  /// See also:
  ///
122
  ///  * [allowedButtonsFilter], which decides which button will be allowed.
123
  ///  * [DragDownDetails], which is passed as an argument to this callback.
124
  GestureDragDownCallback? onDown;
125

126
  /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onStart}
127 128
  /// A pointer has contacted the screen with a primary button and has begun to
  /// move.
129
  /// {@endtemplate}
130 131
  ///
  /// The position of the pointer is provided in the callback's `details`
132 133
  /// argument, which is a [DragStartDetails] object. The [dragStartBehavior]
  /// determines this position.
134 135 136
  ///
  /// See also:
  ///
137
  ///  * [allowedButtonsFilter], which decides which button will be allowed.
138
  ///  * [DragStartDetails], which is passed as an argument to this callback.
139
  GestureDragStartCallback? onStart;
140

141
  /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onUpdate}
142 143
  /// A pointer that is in contact with the screen with a primary button and
  /// moving has moved again.
144
  /// {@endtemplate}
145
  ///
146
  /// The distance traveled by the pointer since the last update is provided in
147
  /// the callback's `details` argument, which is a [DragUpdateDetails] object.
148
  ///
149 150 151 152 153 154 155 156
  /// If this gesture recognizer recognizes movement on a single axis (a
  /// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]),
  /// then `details` will reflect movement only on that axis and its
  /// [DragUpdateDetails.primaryDelta] will be non-null.
  /// If this gesture recognizer recognizes movement in all directions
  /// (a [PanGestureRecognizer]), then `details` will reflect movement on
  /// both axes and its [DragUpdateDetails.primaryDelta] will be null.
  ///
157 158
  /// See also:
  ///
159
  ///  * [allowedButtonsFilter], which decides which button will be allowed.
160
  ///  * [DragUpdateDetails], which is passed as an argument to this callback.
161
  GestureDragUpdateCallback? onUpdate;
162

163
  /// {@template flutter.gestures.monodrag.DragGestureRecognizer.onEnd}
164 165 166
  /// 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.
167
  /// {@endtemplate}
168 169 170
  ///
  /// The velocity is provided in the callback's `details` argument, which is a
  /// [DragEndDetails] object.
171
  ///
172 173 174 175 176 177 178 179
  /// If this gesture recognizer recognizes movement on a single axis (a
  /// [VerticalDragGestureRecognizer] or [HorizontalDragGestureRecognizer]),
  /// then `details` will reflect movement only on that axis and its
  /// [DragEndDetails.primaryVelocity] will be non-null.
  /// If this gesture recognizer recognizes movement in all directions
  /// (a [PanGestureRecognizer]), then `details` will reflect movement on
  /// both axes and its [DragEndDetails.primaryVelocity] will be null.
  ///
180 181
  /// See also:
  ///
182
  ///  * [allowedButtonsFilter], which decides which button will be allowed.
183
  ///  * [DragEndDetails], which is passed as an argument to this callback.
184
  GestureDragEndCallback? onEnd;
185 186

  /// The pointer that previously triggered [onDown] did not complete.
187 188 189
  ///
  /// See also:
  ///
190
  ///  * [allowedButtonsFilter], which decides which button will be allowed.
191
  GestureDragCancelCallback? onCancel;
192

193
  /// The minimum distance an input pointer drag must have moved
194 195 196 197
  /// 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.
198
  double? minFlingDistance;
199 200 201 202 203 204

  /// 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.
205
  double? minFlingVelocity;
206 207 208 209

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

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
  /// Whether the drag threshold should be met before dispatching any drag callbacks.
  ///
  /// The drag threshold is met when the global distance traveled by a pointer has
  /// exceeded the defined threshold on the relevant axis, i.e. y-axis for the
  /// [VerticalDragGestureRecognizer], x-axis for the [HorizontalDragGestureRecognizer],
  /// and the entire plane for [PanGestureRecognizer]. The threshold for both
  /// [VerticalDragGestureRecognizer] and [HorizontalDragGestureRecognizer] are
  /// calculated by [computeHitSlop], while [computePanSlop] is used for
  /// [PanGestureRecognizer].
  ///
  /// If true, the drag callbacks will only be dispatched when this recognizer has
  /// won the arena and the drag threshold has been met.
  ///
  /// If false, the drag callbacks will be dispatched immediately when this recognizer
  /// has won the arena.
  ///
  /// This value defaults to false.
  bool onlyAcceptDragOnThreshold;

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
  /// 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;

255
  _DragState _state = _DragState.ready;
256 257 258
  late OffsetPair _initialPosition;
  late OffsetPair _pendingDragOffset;
  Duration? _lastPendingEventTimestamp;
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276

  /// When asserts are enabled, returns the last tracked pending event timestamp
  /// for this recognizer.
  ///
  /// Otherwise, returns null.
  ///
  /// This getter is intended for use in framework unit tests. Applications must
  /// not depend on its value.
  @visibleForTesting
  Duration? get debugLastPendingEventTimestamp {
    Duration? lastPendingEventTimestamp;
    assert(() {
      lastPendingEventTimestamp = _lastPendingEventTimestamp;
      return true;
    }());
    return lastPendingEventTimestamp;
  }

277 278
  // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
  // different set of buttons, the gesture is canceled.
279 280
  int? _initialButtons;
  Matrix4? _lastTransform;
281 282 283 284 285

  /// 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.
286
  late double _globalDistanceMoved;
287

288 289 290 291 292
  /// 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.
293
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind);
294

295 296 297 298 299 300 301
  /// Determines if a gesture is a fling or not, and if so its effective 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.
  DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind);

302
  Offset _getDeltaForDetails(Offset delta);
303
  double? _getPrimaryValueFromOffset(Offset value);
304
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop);
305
  bool _hasDragThresholdBeenMet = false;
306 307 308

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

309 310 311
  @override
  bool isPointerAllowed(PointerEvent event) {
    if (_initialButtons == null) {
312 313 314 315 316 317
      if (onDown == null &&
          onStart == null &&
          onUpdate == null &&
          onEnd == null &&
          onCancel == null) {
        return false;
318 319 320 321 322 323 324
      }
    } else {
      // There can be multiple drags simultaneously. Their effects are combined.
      if (event.buttons != _initialButtons) {
        return false;
      }
    }
325
    return super.isPointerAllowed(event as PointerDownEvent);
326 327
  }

328
  void _addPointer(PointerEvent event) {
329
    _velocityTrackers[event.pointer] = velocityTrackerBuilder(event);
330 331
    if (_state == _DragState.ready) {
      _state = _DragState.possible;
332 333 334
      _initialPosition = OffsetPair(global: event.position, local: event.localPosition);
      _pendingDragOffset = OffsetPair.zero;
      _globalDistanceMoved = 0.0;
335
      _lastPendingEventTimestamp = event.timeStamp;
336
      _lastTransform = event.transform;
337
      _checkDown();
338 339
    } else if (_state == _DragState.accepted) {
      resolve(GestureDisposition.accepted);
340 341 342
    }
  }

343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
  @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);
  }

362 363 364
  @override
  void handleEvent(PointerEvent event) {
    assert(_state != _DragState.ready);
365 366 367 368 369
    if (!event.synthesized &&
        (event is PointerDownEvent ||
         event is PointerMoveEvent ||
         event is PointerPanZoomStartEvent ||
         event is PointerPanZoomUpdateEvent)) {
370
      final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
371 372 373 374 375 376
      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);
377
      }
378 379 380 381 382 383 384 385 386 387
    }
    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);
388
      if (_state == _DragState.accepted) {
389 390
        _checkUpdate(
          sourceTimeStamp: event.timeStamp,
391 392 393 394
          delta: _getDeltaForDetails(localDelta),
          primaryDelta: _getPrimaryValueFromOffset(localDelta),
          globalPosition: position,
          localPosition: localPosition,
395
        );
396
      } else {
397
        _pendingDragOffset += OffsetPair(local: localDelta, global: delta);
398
        _lastPendingEventTimestamp = event.timeStamp;
399
        _lastTransform = event.transform;
400
        final Offset movedLocally = _getDeltaForDetails(localDelta);
401
        final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
402 403 404
        _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
          transform: localToGlobalTransform,
          untransformedDelta: movedLocally,
405
          untransformedEndPosition: localPosition
406
        ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
407
        if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop)) {
408 409 410 411 412 413
          _hasDragThresholdBeenMet = true;
          if (_acceptedActivePointers.contains(event.pointer)) {
            _checkDrag(event.pointer);
          } else {
            resolve(GestureDisposition.accepted);
          }
414
        }
415 416
      }
    }
417
    if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
418
      _giveUpPointer(event.pointer);
419
    }
420 421
  }

422 423
  final Set<int> _acceptedActivePointers = <int>{};

424 425
  @override
  void acceptGesture(int pointer) {
426 427
    assert(!_acceptedActivePointers.contains(pointer));
    _acceptedActivePointers.add(pointer);
428 429
    if (!onlyAcceptDragOnThreshold || _hasDragThresholdBeenMet) {
      _checkDrag(pointer);
430 431 432 433 434
    }
  }

  @override
  void rejectGesture(int pointer) {
435
    _giveUpPointer(pointer);
436 437 438 439
  }

  @override
  void didStopTrackingLastPointer(int pointer) {
440
    assert(_state != _DragState.ready);
441
    switch (_state) {
442 443 444 445 446 447 448 449 450
      case _DragState.ready:
        break;

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

      case _DragState.accepted:
        _checkEnd(pointer);
451
    }
452
    _hasDragThresholdBeenMet = false;
453 454
    _velocityTrackers.clear();
    _initialButtons = null;
455
    _state = _DragState.ready;
456
  }
457

458
  void _giveUpPointer(int pointer) {
459
    stopTrackingPointer(pointer);
460 461
    // If we never accepted the pointer, we reject it since we are no longer
    // interested in winning the gesture arena for it.
462
    if (!_acceptedActivePointers.remove(pointer)) {
463
      resolvePointer(pointer, GestureDisposition.rejected);
464
    }
465 466
  }

467
  void _checkDown() {
468 469 470 471 472
    if (onDown != null) {
      final DragDownDetails details = DragDownDetails(
        globalPosition: _initialPosition.global,
        localPosition: _initialPosition.local,
      );
473
      invokeCallback<void>('onDown', () => onDown!(details));
474
    }
475 476
  }

477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520
  void _checkDrag(int pointer) {
    if (_state == _DragState.accepted) {
      return;
    }
    _state = _DragState.accepted;
    final OffsetPair delta = _pendingDragOffset;
    final Duration? timestamp = _lastPendingEventTimestamp;
    final Matrix4? transform = _lastTransform;
    final Offset localUpdateDelta;
    switch (dragStartBehavior) {
      case DragStartBehavior.start:
        _initialPosition = _initialPosition + delta;
        localUpdateDelta = Offset.zero;
      case DragStartBehavior.down:
        localUpdateDelta = _getDeltaForDetails(delta.local);
    }
    _pendingDragOffset = OffsetPair.zero;
    _lastPendingEventTimestamp = null;
    _lastTransform = null;
    _checkStart(timestamp, pointer);
    if (localUpdateDelta != Offset.zero && onUpdate != null) {
      final Matrix4? localToGlobal = transform != null ? Matrix4.tryInvert(transform) : null;
      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
      _checkUpdate(
        sourceTimeStamp: timestamp,
        delta: localUpdateDelta,
        primaryDelta: _getPrimaryValueFromOffset(localUpdateDelta),
        globalPosition: correctedPosition.global,
        localPosition: correctedPosition.local,
      );
    }
    // 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);
  }

521
  void _checkStart(Duration? timestamp, int pointer) {
522 523 524 525 526 527 528
    if (onStart != null) {
      final DragStartDetails details = DragStartDetails(
        sourceTimeStamp: timestamp,
        globalPosition: _initialPosition.global,
        localPosition: _initialPosition.local,
        kind: getKindForPointer(pointer),
      );
529
      invokeCallback<void>('onStart', () => onStart!(details));
530
    }
531 532 533
  }

  void _checkUpdate({
534 535 536 537 538
    Duration? sourceTimeStamp,
    required Offset delta,
    double? primaryDelta,
    required Offset globalPosition,
    Offset? localPosition,
539
  }) {
540 541 542 543 544 545 546 547
    if (onUpdate != null) {
      final DragUpdateDetails details = DragUpdateDetails(
        sourceTimeStamp: sourceTimeStamp,
        delta: delta,
        primaryDelta: primaryDelta,
        globalPosition: globalPosition,
        localPosition: localPosition,
      );
548
      invokeCallback<void>('onUpdate', () => onUpdate!(details));
549
    }
550 551 552
  }

  void _checkEnd(int pointer) {
553
    if (onEnd == null) {
554
      return;
555
    }
556

557
    final VelocityTracker tracker = _velocityTrackers[pointer]!;
558
    final VelocityEstimate? estimate = tracker.getVelocityEstimate();
559

560
    DragEndDetails? details;
561
    final String Function() debugReport;
562 563
    if (estimate == null) {
      debugReport = () => 'Could not estimate velocity.';
564
    } else {
565 566 567 568
      details = _considerFling(estimate, tracker.kind);
      debugReport = (details != null)
        ? () => '$estimate; fling at ${details!.velocity}.'
        : () => '$estimate; judged to not be a fling.';
569
    }
570 571 572
    details ??= DragEndDetails(primaryVelocity: 0.0);

    invokeCallback<void>('onEnd', () => onEnd!(details!), debugReport: debugReport);
573 574 575
  }

  void _checkCancel() {
576
    if (onCancel != null) {
577
      invokeCallback<void>('onCancel', onCancel!);
578
    }
579 580 581 582 583 584 585
  }

  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }
586 587 588
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
589
    properties.add(EnumProperty<DragStartBehavior>('start behavior', dragStartBehavior));
590
  }
591 592 593 594 595 596 597 598
}

/// Recognizes movement in the vertical direction.
///
/// Used for vertical scrolling.
///
/// See also:
///
599 600 601 602
///  * [HorizontalDragGestureRecognizer], for a similar recognizer but for
///    horizontal movement.
///  * [MultiDragGestureRecognizer], for a family of gesture recognizers that
///    track each touch point independently.
603
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
604
  /// Create a gesture recognizer for interactions in the vertical axis.
605
  ///
606
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
607
  VerticalDragGestureRecognizer({
608 609
    super.debugOwner,
    super.supportedDevices,
610
    super.allowedButtonsFilter,
611
  });
612

613
  @override
614
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
615
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
616
    final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
617 618 619
    return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
  }

620 621 622 623 624 625 626 627 628 629 630 631 632
  @override
  DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
    if (!isFlingGesture(estimate, kind)) {
      return null;
    }
    final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
    final double dy = clampDouble(estimate.pixelsPerSecond.dy, -maxVelocity, maxVelocity);
    return DragEndDetails(
      velocity: Velocity(pixelsPerSecond: Offset(0, dy)),
      primaryVelocity: dy,
    );
  }

633
  @override
634 635
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
    return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
636
  }
637 638

  @override
639
  Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);
640 641 642 643 644

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

  @override
645
  String get debugDescription => 'vertical drag';
646 647 648 649 650 651 652 653
}

/// Recognizes movement in the horizontal direction.
///
/// Used for horizontal scrolling.
///
/// See also:
///
654 655 656 657
///  * [VerticalDragGestureRecognizer], for a similar recognizer but for
///    vertical movement.
///  * [MultiDragGestureRecognizer], for a family of gesture recognizers that
///    track each touch point independently.
658
class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
659
  /// Create a gesture recognizer for interactions in the horizontal axis.
660
  ///
661
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
662
  HorizontalDragGestureRecognizer({
663 664
    super.debugOwner,
    super.supportedDevices,
665
    super.allowedButtonsFilter,
666
  });
667

668
  @override
669
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
670
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
671
    final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
672 673 674
    return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance;
  }

675 676 677 678 679 680 681 682 683 684 685 686 687
  @override
  DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
    if (!isFlingGesture(estimate, kind)) {
      return null;
    }
    final double maxVelocity = maxFlingVelocity ?? kMaxFlingVelocity;
    final double dx = clampDouble(estimate.pixelsPerSecond.dx, -maxVelocity, maxVelocity);
    return DragEndDetails(
      velocity: Velocity(pixelsPerSecond: Offset(dx, 0)),
      primaryVelocity: dx,
    );
  }

688
  @override
689 690
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
    return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings);
691
  }
692 693

  @override
694
  Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0);
695 696 697 698 699

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

  @override
700
  String get debugDescription => 'horizontal drag';
701 702 703 704 705 706
}

/// Recognizes movement both horizontally and vertically.
///
/// See also:
///
707 708 709 710 711
///  * [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.
712
class PanGestureRecognizer extends DragGestureRecognizer {
713
  /// Create a gesture recognizer for tracking movement on a plane.
714
  PanGestureRecognizer({
715 716
    super.debugOwner,
    super.supportedDevices,
717
    super.allowedButtonsFilter,
718
  });
719

720
  @override
721
  bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
722
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
723
    final double minDistance = minFlingDistance ?? computeHitSlop(kind, gestureSettings);
724 725
    return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity
        && estimate.offset.distanceSquared > minDistance * minDistance;
726 727
  }

728 729 730 731 732 733 734 735 736 737
  @override
  DragEndDetails? _considerFling(VelocityEstimate estimate, PointerDeviceKind kind) {
    if (!isFlingGesture(estimate, kind)) {
      return null;
    }
    final Velocity velocity = Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
        .clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
    return DragEndDetails(velocity: velocity);
  }

738
  @override
739 740
  bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind, double? deviceTouchSlop) {
    return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings);
741 742 743 744 745 746
  }

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

  @override
747
  double? _getPrimaryValueFromOffset(Offset value) => null;
748 749

  @override
750
  String get debugDescription => 'pan';
751
}