long_press.dart 20.2 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 7
import 'arena.dart';
import 'constants.dart';
8
import 'events.dart';
9
import 'recognizer.dart';
10
import 'velocity_tracker.dart';
11

12 13 14
/// Callback signature for [LongPressGestureRecognizer.onLongPress].
///
/// Called when a pointer has remained in contact with the screen at the
15
/// same location for a long period of time.
16
typedef GestureLongPressCallback = void Function();
17

18 19 20 21
/// Callback signature for [LongPressGestureRecognizer.onLongPressUp].
///
/// Called when a pointer stops contacting the screen after a long press
/// gesture was detected.
22 23
typedef GestureLongPressUpCallback = void Function();

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
/// Callback signature for [LongPressGestureRecognizer.onLongPressStart].
///
/// Called when a pointer has remained in contact with the screen at the
/// same location for a long period of time. Also reports the long press down
/// position.
typedef GestureLongPressStartCallback = void Function(LongPressStartDetails details);

/// Callback signature for [LongPressGestureRecognizer.onLongPressMoveUpdate].
///
/// Called when a pointer is moving after being held in contact at the same
/// location for a long period of time. Reports the new position and its offset
/// from the original down position.
typedef GestureLongPressMoveUpdateCallback = void Function(LongPressMoveUpdateDetails details);

/// Callback signature for [LongPressGestureRecognizer.onLongPressEnd].
///
/// Called when a pointer stops contacting the screen after a long press
/// gesture was detected. Also reports the position where the pointer stopped
/// contacting the screen.
typedef GestureLongPressEndCallback = void Function(LongPressEndDetails details);

/// Details for callbacks that use [GestureLongPressStartCallback].
///
/// See also:
///
///  * [LongPressGestureRecognizer.onLongPressStart], which uses [GestureLongPressStartCallback].
///  * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback]
///  * [LongPressEndDetails], the details for [GestureLongPressEndCallback].
class LongPressStartDetails {
  /// Creates the details for a [GestureLongPressStartCallback].
  ///
  /// The [globalPosition] argument must not be null.
56 57
  const LongPressStartDetails({
    this.globalPosition = Offset.zero,
58
    Offset? localPosition,
59 60
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
61 62 63

  /// The global position at which the pointer contacted the screen.
  final Offset globalPosition;
64 65 66

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
}

/// Details for callbacks that use [GestureLongPressMoveUpdateCallback].
///
/// See also:
///
///  * [LongPressGestureRecognizer.onLongPressMoveUpdate], which uses [GestureLongPressMoveUpdateCallback].
///  * [LongPressEndDetails], the details for [GestureLongPressEndCallback]
///  * [LongPressStartDetails], the details for [GestureLongPressStartCallback].
class LongPressMoveUpdateDetails {
  /// Creates the details for a [GestureLongPressMoveUpdateCallback].
  ///
  /// The [globalPosition] and [offsetFromOrigin] arguments must not be null.
  const LongPressMoveUpdateDetails({
    this.globalPosition = Offset.zero,
82
    Offset? localPosition,
83
    this.offsetFromOrigin = Offset.zero,
84
    Offset? localOffsetFromOrigin,
85
  }) : assert(globalPosition != null),
86 87 88
       assert(offsetFromOrigin != null),
       localPosition = localPosition ?? globalPosition,
       localOffsetFromOrigin = localOffsetFromOrigin ?? offsetFromOrigin;
89 90 91 92

  /// The global position of the pointer when it triggered this update.
  final Offset globalPosition;

93 94 95
  /// The local position of the pointer when it triggered this update.
  final Offset localPosition;

96 97 98 99
  /// A delta offset from the point where the long press drag initially contacted
  /// the screen to the point where the pointer is currently located (the
  /// present [globalPosition]) when this callback is triggered.
  final Offset offsetFromOrigin;
100 101 102 103 104

  /// A local delta offset from the point where the long press drag initially contacted
  /// the screen to the point where the pointer is currently located (the
  /// present [localPosition]) when this callback is triggered.
  final Offset localOffsetFromOrigin;
105 106 107 108 109 110 111
}

/// Details for callbacks that use [GestureLongPressEndCallback].
///
/// See also:
///
///  * [LongPressGestureRecognizer.onLongPressEnd], which uses [GestureLongPressEndCallback].
112
///  * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback].
113 114 115 116 117
///  * [LongPressStartDetails], the details for [GestureLongPressStartCallback].
class LongPressEndDetails {
  /// Creates the details for a [GestureLongPressEndCallback].
  ///
  /// The [globalPosition] argument must not be null.
118 119
  const LongPressEndDetails({
    this.globalPosition = Offset.zero,
120
    Offset? localPosition,
121
    this.velocity = Velocity.zero,
122 123
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
124 125 126

  /// The global position at which the pointer lifted from the screen.
  final Offset globalPosition;
127 128 129

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
130 131 132 133 134

  /// The pointer's velocity when it stopped contacting the screen.
  ///
  /// Defaults to zero if not specified in the constructor.
  final Velocity velocity;
135 136
}

137 138
/// Recognizes when the user has pressed down at the same location for a long
/// period of time.
139 140 141 142 143
///
/// The gesture must not deviate in position from its touch down point for 500ms
/// until it's recognized. Once the gesture is accepted, the finger can be
/// moved, triggering [onLongPressMoveUpdate] callbacks, unless the
/// [postAcceptSlopTolerance] constructor argument is specified.
144
///
145
/// [LongPressGestureRecognizer] may compete on pointer events of
146 147
/// [kPrimaryButton], [kSecondaryButton], and/or [kTertiaryButton] if at least
/// one corresponding callback is non-null. If it has no callbacks, it is a no-op.
148
class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
149 150
  /// Creates a long-press gesture recognizer.
  ///
151 152 153 154 155 156 157 158 159
  /// Consider assigning the [onLongPressStart] callback after creating this
  /// object.
  ///
  /// The [postAcceptSlopTolerance] argument can be used to specify a maximum
  /// allowed distance for the gesture to deviate from the starting point once
  /// the long press has triggered. If the gesture deviates past that point,
  /// subsequent callbacks ([onLongPressMoveUpdate], [onLongPressUp],
  /// [onLongPressEnd]) will stop. Defaults to null, which means the gesture
  /// can be moved without limit once the long press is accepted.
160 161 162
  ///
  /// The [duration] argument can be used to overwrite the default duration
  /// after which the long press will be recognized.
163
  LongPressGestureRecognizer({
164 165 166 167
    Duration? duration,
    double? postAcceptSlopTolerance,
    PointerDeviceKind? kind,
    Object? debugOwner,
168
  }) : super(
169 170 171 172 173
          deadline: duration ?? kLongPressTimeout,
          postAcceptSlopTolerance: postAcceptSlopTolerance,
          kind: kind,
          debugOwner: debugOwner,
        );
174 175

  bool _longPressAccepted = false;
176
  OffsetPair? _longPressOrigin;
177 178
  // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
  // different set of buttons, the gesture is canceled.
179
  int? _initialButtons;
180

181
  /// Called when a long press gesture by a primary button has been recognized.
182 183 184
  ///
  /// See also:
  ///
185
  ///  * [kPrimaryButton], the button this callback responds to.
186 187
  ///  * [onLongPressStart], which has the same timing but has data for the
  ///    press location.
188
  GestureLongPressCallback? onLongPress;
189

190
  /// Called when a long press gesture by a primary button has been recognized.
191 192 193
  ///
  /// See also:
  ///
194 195 196
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onLongPress], which has the same timing but without details.
  ///  * [LongPressStartDetails], which is passed as an argument to this callback.
197
  GestureLongPressStartCallback? onLongPressStart;
198

199 200 201 202 203 204 205
  /// Called when moving after the long press by a primary button is recognized.
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [LongPressMoveUpdateDetails], which is passed as an argument to this
  ///    callback.
206
  GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate;
207

208 209
  /// Called when the pointer stops contacting the screen after a long-press
  /// by a primary button.
210 211 212
  ///
  /// See also:
  ///
213
  ///  * [kPrimaryButton], the button this callback responds to.
214 215
  ///  * [onLongPressEnd], which has the same timing but has data for the up
  ///    gesture location.
216
  GestureLongPressUpCallback? onLongPressUp;
217

218 219
  /// Called when the pointer stops contacting the screen after a long-press
  /// by a primary button.
220 221 222
  ///
  /// See also:
  ///
223 224 225 226
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onLongPressUp], which has the same timing, but without details.
  ///  * [LongPressEndDetails], which is passed as an argument to this
  ///    callback.
227
  GestureLongPressEndCallback? onLongPressEnd;
228

229 230 231 232 233 234 235 236
  /// Called when a long press gesture by a secondary button has been
  /// recognized.
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
  ///  * [onSecondaryLongPressStart], which has the same timing but has data for
  ///    the press location.
237
  GestureLongPressCallback? onSecondaryLongPress;
238 239 240 241 242 243 244 245 246

  /// Called when a long press gesture by a secondary button has been recognized.
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
  ///  * [onSecondaryLongPress], which has the same timing but without details.
  ///  * [LongPressStartDetails], which is passed as an argument to this
  ///    callback.
247
  GestureLongPressStartCallback? onSecondaryLongPressStart;
248 249 250 251 252 253 254 255 256

  /// Called when moving after the long press by a secondary button is
  /// recognized.
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
  ///  * [LongPressMoveUpdateDetails], which is passed as an argument to this
  ///    callback.
257
  GestureLongPressMoveUpdateCallback? onSecondaryLongPressMoveUpdate;
258 259 260 261 262 263 264 265 266

  /// Called when the pointer stops contacting the screen after a long-press by
  /// a secondary button.
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
  ///  * [onSecondaryLongPressEnd], which has the same timing but has data for
  ///    the up gesture location.
267
  GestureLongPressUpCallback? onSecondaryLongPressUp;
268 269 270 271 272 273 274 275 276 277

  /// Called when the pointer stops contacting the screen after a long-press by
  /// a secondary button.
  ///
  /// See also:
  ///
  ///  * [kSecondaryButton], the button this callback responds to.
  ///  * [onSecondaryLongPressUp], which has the same timing, but without
  ///    details.
  ///  * [LongPressEndDetails], which is passed as an argument to this callback.
278
  GestureLongPressEndCallback? onSecondaryLongPressEnd;
279

280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
  /// Called when a long press gesture by a tertiary button has been
  /// recognized.
  ///
  /// See also:
  ///
  ///  * [kTertiaryButton], the button this callback responds to.
  ///  * [onTertiaryLongPressStart], which has the same timing but has data for
  ///    the press location.
  GestureLongPressCallback? onTertiaryLongPress;

  /// Called when a long press gesture by a tertiary button has been recognized.
  ///
  /// See also:
  ///
  ///  * [kTertiaryButton], the button this callback responds to.
  ///  * [onTertiaryLongPress], which has the same timing but without details.
  ///  * [LongPressStartDetails], which is passed as an argument to this
  ///    callback.
  GestureLongPressStartCallback? onTertiaryLongPressStart;

  /// Called when moving after the long press by a tertiary button is
  /// recognized.
  ///
  /// See also:
  ///
  ///  * [kTertiaryButton], the button this callback responds to.
  ///  * [LongPressMoveUpdateDetails], which is passed as an argument to this
  ///    callback.
  GestureLongPressMoveUpdateCallback? onTertiaryLongPressMoveUpdate;

  /// Called when the pointer stops contacting the screen after a long-press by
  /// a tertiary button.
  ///
  /// See also:
  ///
  ///  * [kTertiaryButton], the button this callback responds to.
  ///  * [onTertiaryLongPressEnd], which has the same timing but has data for
  ///    the up gesture location.
  GestureLongPressUpCallback? onTertiaryLongPressUp;

  /// Called when the pointer stops contacting the screen after a long-press by
  /// a tertiary button.
  ///
  /// See also:
  ///
  ///  * [kTertiaryButton], the button this callback responds to.
  ///  * [onTertiaryLongPressUp], which has the same timing, but without
  ///    details.
  ///  * [LongPressEndDetails], which is passed as an argument to this callback.
  GestureLongPressEndCallback? onTertiaryLongPressEnd;

331
  VelocityTracker? _velocityTracker;
332

333 334 335 336 337 338 339 340 341 342 343
  @override
  bool isPointerAllowed(PointerDownEvent event) {
    switch (event.buttons) {
      case kPrimaryButton:
        if (onLongPressStart == null &&
            onLongPress == null &&
            onLongPressMoveUpdate == null &&
            onLongPressEnd == null &&
            onLongPressUp == null)
          return false;
        break;
344 345 346 347 348 349 350 351
      case kSecondaryButton:
        if (onSecondaryLongPressStart == null &&
            onSecondaryLongPress == null &&
            onSecondaryLongPressMoveUpdate == null &&
            onSecondaryLongPressEnd == null &&
            onSecondaryLongPressUp == null)
          return false;
        break;
352 353 354 355 356 357 358 359
      case kTertiaryButton:
        if (onTertiaryLongPressStart == null &&
            onTertiaryLongPress == null &&
            onTertiaryLongPressMoveUpdate == null &&
            onTertiaryLongPressEnd == null &&
            onTertiaryLongPressUp == null)
          return false;
        break;
360 361 362 363 364 365
      default:
        return false;
    }
    return super.isPointerAllowed(event);
  }

366
  @override
367
  void didExceedDeadline() {
368
    // Exceeding the deadline puts the gesture in the accepted state.
369
    resolve(GestureDisposition.accepted);
370
    _longPressAccepted = true;
371
    super.acceptGesture(primaryPointer!);
372
    _checkLongPressStart();
373 374
  }

375
  @override
Ian Hickson's avatar
Ian Hickson committed
376
  void handlePrimaryPointer(PointerEvent event) {
377 378
    if (!event.synthesized) {
      if (event is PointerDownEvent) {
379
        _velocityTracker = VelocityTracker.withKind(event.kind);
380
        _velocityTracker!.addPosition(event.timeStamp, event.localPosition);
381 382 383
      }
      if (event is PointerMoveEvent) {
        assert(_velocityTracker != null);
384
        _velocityTracker!.addPosition(event.timeStamp, event.localPosition);
385 386 387
      }
    }

388
    if (event is PointerUpEvent) {
389
      if (_longPressAccepted == true) {
390
        _checkLongPressEnd(event);
391
      } else {
392
        // Pointer is lifted before timeout.
393 394
        resolve(GestureDisposition.rejected);
      }
395 396 397 398
      _reset();
    } else if (event is PointerCancelEvent) {
      _reset();
    } else if (event is PointerDownEvent) {
399
      // The first touch.
400
      _longPressOrigin = OffsetPair.fromEventPosition(event);
401 402 403 404
      _initialButtons = event.buttons;
    } else if (event is PointerMoveEvent) {
      if (event.buttons != _initialButtons) {
        resolve(GestureDisposition.rejected);
405
        stopTrackingPointer(primaryPointer!);
406 407 408 409 410 411 412
      } else if (_longPressAccepted) {
        _checkLongPressMoveUpdate(event);
      }
    }
  }

  void _checkLongPressStart() {
413 414 415 416
    switch (_initialButtons) {
      case kPrimaryButton:
        if (onLongPressStart != null) {
          final LongPressStartDetails details = LongPressStartDetails(
417 418
            globalPosition: _longPressOrigin!.global,
            localPosition: _longPressOrigin!.local,
419
          );
420
          invokeCallback<void>('onLongPressStart', () => onLongPressStart!(details));
421 422
        }
        if (onLongPress != null) {
423
          invokeCallback<void>('onLongPress', onLongPress!);
424 425 426 427 428
        }
        break;
      case kSecondaryButton:
        if (onSecondaryLongPressStart != null) {
          final LongPressStartDetails details = LongPressStartDetails(
429 430
            globalPosition: _longPressOrigin!.global,
            localPosition: _longPressOrigin!.local,
431 432
          );
          invokeCallback<void>(
433
              'onSecondaryLongPressStart', () => onSecondaryLongPressStart!(details));
434 435
        }
        if (onSecondaryLongPress != null) {
436
          invokeCallback<void>('onSecondaryLongPress', onSecondaryLongPress!);
437 438
        }
        break;
439 440 441 442 443 444 445 446 447 448 449 450 451
      case kTertiaryButton:
        if (onTertiaryLongPressStart != null) {
          final LongPressStartDetails details = LongPressStartDetails(
            globalPosition: _longPressOrigin!.global,
            localPosition: _longPressOrigin!.local,
          );
          invokeCallback<void>(
              'onTertiaryLongPressStart', () => onTertiaryLongPressStart!(details));
        }
        if (onTertiaryLongPress != null) {
          invokeCallback<void>('onTertiaryLongPress', onTertiaryLongPress!);
        }
        break;
452 453
      default:
        assert(false, 'Unhandled button $_initialButtons');
454
    }
455 456 457 458 459
  }

  void _checkLongPressMoveUpdate(PointerEvent event) {
    final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails(
      globalPosition: event.position,
460
      localPosition: event.localPosition,
461 462
      offsetFromOrigin: event.position - _longPressOrigin!.global,
      localOffsetFromOrigin: event.localPosition - _longPressOrigin!.local,
463
    );
464 465 466 467
    switch (_initialButtons) {
      case kPrimaryButton:
        if (onLongPressMoveUpdate != null) {
          invokeCallback<void>('onLongPressMoveUpdate',
468
            () => onLongPressMoveUpdate!(details));
469 470 471 472 473
        }
        break;
      case kSecondaryButton:
        if (onSecondaryLongPressMoveUpdate != null) {
          invokeCallback<void>('onSecondaryLongPressMoveUpdate',
474
            () => onSecondaryLongPressMoveUpdate!(details));
475 476
        }
        break;
477 478 479 480 481 482
      case kTertiaryButton:
        if (onTertiaryLongPressMoveUpdate != null) {
          invokeCallback<void>('onTertiaryLongPressMoveUpdate',
                  () => onTertiaryLongPressMoveUpdate!(details));
        }
        break;
483 484 485
      default:
        assert(false, 'Unhandled button $_initialButtons');
    }
486 487 488
  }

  void _checkLongPressEnd(PointerEvent event) {
489
    final VelocityEstimate? estimate = _velocityTracker!.getVelocityEstimate();
490 491 492
    final Velocity velocity = estimate == null
        ? Velocity.zero
        : Velocity(pixelsPerSecond: estimate.pixelsPerSecond);
493 494
    final LongPressEndDetails details = LongPressEndDetails(
      globalPosition: event.position,
495
      localPosition: event.localPosition,
496
      velocity: velocity,
497
    );
498 499

    _velocityTracker = null;
500 501 502
    switch (_initialButtons) {
      case kPrimaryButton:
        if (onLongPressEnd != null) {
503
          invokeCallback<void>('onLongPressEnd', () => onLongPressEnd!(details));
504 505
        }
        if (onLongPressUp != null) {
506
          invokeCallback<void>('onLongPressUp', onLongPressUp!);
507 508 509 510
        }
        break;
      case kSecondaryButton:
        if (onSecondaryLongPressEnd != null) {
511
          invokeCallback<void>('onSecondaryLongPressEnd', () => onSecondaryLongPressEnd!(details));
512 513
        }
        if (onSecondaryLongPressUp != null) {
514
          invokeCallback<void>('onSecondaryLongPressUp', onSecondaryLongPressUp!);
515 516
        }
        break;
517 518 519 520 521 522 523 524
      case kTertiaryButton:
        if (onTertiaryLongPressEnd != null) {
          invokeCallback<void>('onTertiaryLongPressEnd', () => onTertiaryLongPressEnd!(details));
        }
        if (onTertiaryLongPressUp != null) {
          invokeCallback<void>('onTertiaryLongPressUp', onTertiaryLongPressUp!);
        }
        break;
525 526 527
      default:
        assert(false, 'Unhandled button $_initialButtons');
    }
528 529 530 531 532 533
  }

  void _reset() {
    _longPressAccepted = false;
    _longPressOrigin = null;
    _initialButtons = null;
534
    _velocityTracker = null;
535 536 537 538 539 540 541 542
  }

  @override
  void resolve(GestureDisposition disposition) {
    if (_longPressAccepted && disposition == GestureDisposition.rejected) {
      // This can happen if the gesture has been canceled. For example when
      // the buttons have changed.
      _reset();
543
    }
544
    super.resolve(disposition);
545
  }
546

547 548 549 550 551 552
  @override
  void acceptGesture(int pointer) {
    // Winning the arena isn't important here since it may happen from a sweep.
    // Explicitly exceeding the deadline puts the gesture in accepted state.
  }

553
  @override
554
  String get debugDescription => 'long press';
555
}