monodrag.dart 19.7 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 27
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`.
///
/// See [DragGestureRecognizer.onEnd].
28
typedef GestureDragEndCallback = void Function(DragEndDetails details);
29 30 31 32 33

/// Signature for when the pointer that previously triggered a
/// [GestureDragDownCallback] did not complete.
///
/// See [DragGestureRecognizer.onCancel].
34
typedef GestureDragCancelCallback = void Function();
35 36 37 38 39 40 41 42 43 44 45 46

/// 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.
///
47 48 49 50
/// [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.
///
51 52
/// See also:
///
53 54 55
///  * [HorizontalDragGestureRecognizer], for left and right drags.
///  * [VerticalDragGestureRecognizer], for up and down drags.
///  * [PanGestureRecognizer], for drags that are not locked to a single axis.
56
abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
57
  /// Initialize the object.
58 59
  ///
  /// [dragStartBehavior] must not be null.
60 61
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
62 63
  DragGestureRecognizer({
    Object debugOwner,
64
    PointerDeviceKind kind,
65
    this.dragStartBehavior = DragStartBehavior.start,
66
  }) : assert(dragStartBehavior != null),
67
       super(debugOwner: debugOwner, kind: kind);
68 69 70 71 72 73 74 75 76

  /// Configure the behavior of offsets sent to [onStart].
  ///
  /// If set to [DragStartBehavior.start], the [onStart] callback will be called at the time and
  /// position when the gesture detector wins the arena. If [DragStartBehavior.down],
  /// [onStart] will be called at the time and position when a down event was
  /// first detected.
  ///
  /// For more information about the gesture arena:
77
  /// https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation
78 79 80 81 82 83 84 85 86 87 88 89 90
  ///
  /// By default, the drag start behavior is [DragStartBehavior.start].
  ///
  /// ## Example:
  ///
  /// A finger presses down on the screen with offset (500.0, 500.0),
  /// and then moves to position (510.0, 500.0) before winning the arena.
  /// With [dragStartBehavior] set to [DragStartBehavior.down], the [onStart]
  /// callback will be called at the time corresponding to the touch's position
  /// at (500.0, 500.0). If it is instead set to [DragStartBehavior.start],
  /// [onStart] will be called at the time corresponding to the touch's position
  /// at (510.0, 500.0).
  DragStartBehavior dragStartBehavior;
91

92 93
  /// A pointer has contacted the screen with a primary button and might begin
  /// to move.
94 95 96
  ///
  /// The position of the pointer is provided in the callback's `details`
  /// argument, which is a [DragDownDetails] object.
97 98 99 100 101
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragDownDetails], which is passed as an argument to this callback.
102 103
  GestureDragDownCallback onDown;

104 105
  /// A pointer has contacted the screen with a primary button and has begun to
  /// move.
106 107 108
  ///
  /// The position of the pointer is provided in the callback's `details`
  /// argument, which is a [DragStartDetails] object.
109 110 111 112 113
  ///
  /// Depending on the value of [dragStartBehavior], this function will be
  /// called on the initial touch down, if set to [DragStartBehavior.down] or
  /// when the drag gesture is first detected, if set to
  /// [DragStartBehavior.start].
114 115 116 117 118
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragStartDetails], which is passed as an argument to this callback.
119 120
  GestureDragStartCallback onStart;

121 122
  /// A pointer that is in contact with the screen with a primary button and
  /// moving has moved again.
123
  ///
124
  /// The distance traveled by the pointer since the last update is provided in
125
  /// the callback's `details` argument, which is a [DragUpdateDetails] object.
126 127 128 129 130
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragUpdateDetails], which is passed as an argument to this callback.
131 132
  GestureDragUpdateCallback onUpdate;

133 134 135
  /// 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.
136 137 138
  ///
  /// The velocity is provided in the callback's `details` argument, which is a
  /// [DragEndDetails] object.
139 140 141 142 143
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [DragEndDetails], which is passed as an argument to this callback.
144 145 146
  GestureDragEndCallback onEnd;

  /// The pointer that previously triggered [onDown] did not complete.
147 148 149 150
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  GestureDragCancelCallback onCancel;

  /// 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.
  double minFlingDistance;

  /// 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.
  double minFlingVelocity;

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

  _DragState _state = _DragState.ready;
173 174
  OffsetPair _initialPosition;
  OffsetPair _pendingDragOffset;
175
  Duration _lastPendingEventTimestamp;
176 177 178
  // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
  // different set of buttons, the gesture is canceled.
  int _initialButtons;
179 180 181 182 183 184 185
  Matrix4 _lastTransform;

  /// 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.
  double _globalDistanceMoved;
186

187 188 189 190 191 192 193
  /// 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.
  bool isFlingGesture(VelocityEstimate estimate);

194 195
  Offset _getDeltaForDetails(Offset delta);
  double _getPrimaryValueFromOffset(Offset value);
196
  bool get _hasSufficientGlobalDistanceToAccept;
197 198 199

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

200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
  @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;
      }
    }
    return super.isPointerAllowed(event);
  }

224
  @override
225
  void addAllowedPointer(PointerEvent event) {
226
    startTrackingPointer(event.pointer, event.transform);
227
    _velocityTrackers[event.pointer] = VelocityTracker();
228 229
    if (_state == _DragState.ready) {
      _state = _DragState.possible;
230
      _initialPosition = OffsetPair(global: event.position, local: event.localPosition);
231
      _initialButtons = event.buttons;
232 233
      _pendingDragOffset = OffsetPair.zero;
      _globalDistanceMoved = 0.0;
234
      _lastPendingEventTimestamp = event.timeStamp;
235
      _lastTransform = event.transform;
236
      _checkDown();
237 238
    } else if (_state == _DragState.accepted) {
      resolve(GestureDisposition.accepted);
239 240 241 242 243 244
    }
  }

  @override
  void handleEvent(PointerEvent event) {
    assert(_state != _DragState.ready);
245 246
    if (!event.synthesized
        && (event is PointerDownEvent || event is PointerMoveEvent)) {
247 248
      final VelocityTracker tracker = _velocityTrackers[event.pointer];
      assert(tracker != null);
249
      tracker.addPosition(event.timeStamp, event.localPosition);
250 251 252
    }

    if (event is PointerMoveEvent) {
253
      if (event.buttons != _initialButtons) {
254
        _giveUpPointer(event.pointer);
255 256
        return;
      }
257
      if (_state == _DragState.accepted) {
258 259
        _checkUpdate(
          sourceTimeStamp: event.timeStamp,
260 261
          delta: _getDeltaForDetails(event.localDelta),
          primaryDelta: _getPrimaryValueFromOffset(event.localDelta),
262
          globalPosition: event.position,
263
          localPosition: event.localPosition,
264
        );
265
      } else {
266
        _pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta);
267
        _lastPendingEventTimestamp = event.timeStamp;
268 269 270 271 272 273 274 275 276
        _lastTransform = event.transform;
        final Offset movedLocally = _getDeltaForDetails(event.localDelta);
        final Matrix4 localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform);
        _globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
          transform: localToGlobalTransform,
          untransformedDelta: movedLocally,
          untransformedEndPosition: event.localPosition,
        ).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
        if (_hasSufficientGlobalDistanceToAccept)
277 278 279
          resolve(GestureDisposition.accepted);
      }
    }
280 281 282 283 284 285
    if (event is PointerUpEvent || event is PointerCancelEvent) {
      _giveUpPointer(
        event.pointer,
        reject: event is PointerCancelEvent || _state ==_DragState.possible,
      );
    }
286 287 288 289 290 291
  }

  @override
  void acceptGesture(int pointer) {
    if (_state != _DragState.accepted) {
      _state = _DragState.accepted;
292
      final OffsetPair delta = _pendingDragOffset;
293
      final Duration timestamp = _lastPendingEventTimestamp;
294 295
      final Matrix4 transform = _lastTransform;
      Offset localUpdateDelta;
296 297 298
      switch (dragStartBehavior) {
        case DragStartBehavior.start:
          _initialPosition = _initialPosition + delta;
299
          localUpdateDelta = Offset.zero;
300 301
          break;
        case DragStartBehavior.down:
302
          localUpdateDelta = _getDeltaForDetails(delta.local);
303 304
          break;
      }
305
      _pendingDragOffset = OffsetPair.zero;
306
      _lastPendingEventTimestamp = null;
307
      _lastTransform = null;
308
      _checkStart(timestamp);
309 310 311 312 313 314 315 316 317 318
      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
319
        _checkUpdate(
320
          sourceTimeStamp: timestamp,
321 322 323 324
          delta: localUpdateDelta,
          primaryDelta: _getPrimaryValueFromOffset(localUpdateDelta),
          globalPosition: correctedPosition.global,
          localPosition: correctedPosition.local,
325
        );
326 327 328 329 330 331
      }
    }
  }

  @override
  void rejectGesture(int pointer) {
332
    _giveUpPointer(pointer);
333 334 335 336
  }

  @override
  void didStopTrackingLastPointer(int pointer) {
337 338 339 340 341 342 343 344 345 346 347 348 349
    assert(_state != _DragState.ready);
    switch(_state) {
      case _DragState.ready:
        break;

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

      case _DragState.accepted:
        _checkEnd(pointer);
        break;
350
    }
351 352
    _velocityTrackers.clear();
    _initialButtons = null;
353
    _state = _DragState.ready;
354
  }
355

356 357 358 359 360 361 362 363 364 365
  void _giveUpPointer(int pointer, {bool reject = true}) {
    stopTrackingPointer(pointer);
    if (reject) {
      if (_velocityTrackers.containsKey(pointer)) {
        _velocityTrackers.remove(pointer);
        resolvePointer(pointer, GestureDisposition.rejected);
      }
    }
  }

366 367 368
  void _checkDown() {
    assert(_initialButtons == kPrimaryButton);
    final DragDownDetails details = DragDownDetails(
369 370
      globalPosition: _initialPosition.global,
      localPosition: _initialPosition.local,
371 372 373 374 375 376 377 378 379
    );
    if (onDown != null)
      invokeCallback<void>('onDown', () => onDown(details));
  }

  void _checkStart(Duration timestamp) {
    assert(_initialButtons == kPrimaryButton);
    final DragStartDetails details = DragStartDetails(
      sourceTimeStamp: timestamp,
380 381
      globalPosition: _initialPosition.global,
      localPosition: _initialPosition.local,
382 383 384 385 386 387 388 389 390 391
    );
    if (onStart != null)
      invokeCallback<void>('onStart', () => onStart(details));
  }

  void _checkUpdate({
    Duration sourceTimeStamp,
    Offset delta,
    double primaryDelta,
    Offset globalPosition,
392
    Offset localPosition,
393 394 395 396 397 398 399
  }) {
    assert(_initialButtons == kPrimaryButton);
    final DragUpdateDetails details = DragUpdateDetails(
      sourceTimeStamp: sourceTimeStamp,
      delta: delta,
      primaryDelta: primaryDelta,
      globalPosition: globalPosition,
400
      localPosition: localPosition,
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
    );
    if (onUpdate != null)
      invokeCallback<void>('onUpdate', () => onUpdate(details));
  }

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

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

    DragEndDetails details;
    void Function() debugReport;

    final VelocityEstimate estimate = tracker.getVelocityEstimate();
418
    if (estimate != null && isFlingGesture(estimate)) {
419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
      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(
        velocity: Velocity.zero,
        primaryVelocity: 0.0,
      );
      debugReport = () {
        if (estimate == null)
          return 'Could not estimate velocity.';
        return '$estimate; judged to not be a fling.';
      };
438
    }
439 440 441 442 443 444 445
    invokeCallback<void>('onEnd', () => onEnd(details), debugReport: debugReport);
  }

  void _checkCancel() {
    assert(_initialButtons == kPrimaryButton);
    if (onCancel != null)
      invokeCallback<void>('onCancel', onCancel);
446 447 448 449 450 451 452
  }

  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }
453 454 455
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
456
    properties.add(EnumProperty<DragStartBehavior>('start behavior', dragStartBehavior));
457
  }
458 459 460 461 462 463 464 465
}

/// Recognizes movement in the vertical direction.
///
/// Used for vertical scrolling.
///
/// See also:
///
466 467 468 469
///  * [HorizontalDragGestureRecognizer], for a similar recognizer but for
///    horizontal movement.
///  * [MultiDragGestureRecognizer], for a family of gesture recognizers that
///    track each touch point independently.
470
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
471
  /// Create a gesture recognizer for interactions in the vertical axis.
472 473 474 475 476 477
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
  VerticalDragGestureRecognizer({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
478

479
  @override
480
  bool isFlingGesture(VelocityEstimate estimate) {
481 482 483 484 485 486
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
    final double minDistance = minFlingDistance ?? kTouchSlop;
    return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
  }

  @override
487
  bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;
488 489

  @override
490
  Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);
491 492 493 494 495

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

  @override
496
  String get debugDescription => 'vertical drag';
497 498 499 500 501 502 503 504
}

/// Recognizes movement in the horizontal direction.
///
/// Used for horizontal scrolling.
///
/// See also:
///
505 506 507 508
///  * [VerticalDragGestureRecognizer], for a similar recognizer but for
///    vertical movement.
///  * [MultiDragGestureRecognizer], for a family of gesture recognizers that
///    track each touch point independently.
509
class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
510
  /// Create a gesture recognizer for interactions in the horizontal axis.
511 512 513 514 515 516
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
  HorizontalDragGestureRecognizer({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
517

518
  @override
519
  bool isFlingGesture(VelocityEstimate estimate) {
520 521 522 523 524 525
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
    final double minDistance = minFlingDistance ?? kTouchSlop;
    return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance;
  }

  @override
526
  bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;
527 528

  @override
529
  Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0);
530 531 532 533 534

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

  @override
535
  String get debugDescription => 'horizontal drag';
536 537 538 539 540 541
}

/// Recognizes movement both horizontally and vertically.
///
/// See also:
///
542 543 544 545 546
///  * [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.
547
class PanGestureRecognizer extends DragGestureRecognizer {
548 549 550
  /// Create a gesture recognizer for tracking movement on a plane.
  PanGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);

551
  @override
552
  bool isFlingGesture(VelocityEstimate estimate) {
553 554
    final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
    final double minDistance = minFlingDistance ?? kTouchSlop;
555 556
    return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity
        && estimate.offset.distanceSquared > minDistance * minDistance;
557 558 559
  }

  @override
560 561
  bool get _hasSufficientGlobalDistanceToAccept {
    return _globalDistanceMoved.abs() > kPanSlop;
562 563 564 565 566 567 568 569 570
  }

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

  @override
  double _getPrimaryValueFromOffset(Offset value) => null;

  @override
571
  String get debugDescription => 'pan';
572
}