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

import 'dart:async';
6
import 'package:vector_math/vector_math_64.dart';
Hixie's avatar
Hixie committed
7 8

import 'arena.dart';
9
import 'binding.dart';
Hixie's avatar
Hixie committed
10 11
import 'constants.dart';
import 'events.dart';
12
import 'gesture_settings.dart';
Hixie's avatar
Hixie committed
13 14
import 'pointer_router.dart';
import 'recognizer.dart';
15
import 'tap.dart';
Hixie's avatar
Hixie committed
16

17 18
/// Signature for callback when the user has tapped the screen at the same
/// location twice in quick succession.
19 20 21 22
///
/// See also:
///
///  * [GestureDetector.onDoubleTap], which matches this signature.
23
typedef GestureDoubleTapCallback = void Function();
Hixie's avatar
Hixie committed
24

25 26
/// Signature used by [MultiTapGestureRecognizer] for when a pointer that might
/// cause a tap has contacted the screen at a particular location.
27
typedef GestureMultiTapDownCallback = void Function(int pointer, TapDownDetails details);
28 29 30

/// Signature used by [MultiTapGestureRecognizer] for when a pointer that will
/// trigger a tap has stopped contacting the screen at a particular location.
31
typedef GestureMultiTapUpCallback = void Function(int pointer, TapUpDetails details);
32 33

/// Signature used by [MultiTapGestureRecognizer] for when a tap has occurred.
34
typedef GestureMultiTapCallback = void Function(int pointer);
35 36 37

/// Signature for when the pointer that previously triggered a
/// [GestureMultiTapDownCallback] will not end up causing a tap.
38
typedef GestureMultiTapCancelCallback = void Function(int pointer);
Hixie's avatar
Hixie committed
39

40 41 42
/// CountdownZoned tracks whether the specified duration has elapsed since
/// creation, honoring [Zone].
class _CountdownZoned {
43
  _CountdownZoned({ required Duration duration })
44
       : assert(duration != null) {
45
    Timer(duration, _onTimeout);
46 47 48 49 50 51 52 53 54 55 56
  }

  bool _timeout = false;

  bool get timeout => _timeout;

  void _onTimeout() {
    _timeout = true;
  }
}

Hixie's avatar
Hixie committed
57 58 59
/// TapTracker helps track individual tap sequences as part of a
/// larger gesture.
class _TapTracker {
60
  _TapTracker({
61
    required PointerDownEvent event,
62
    required this.entry,
63
    required Duration doubleTapMinTime,
64
    required this.gestureSettings,
65 66
  }) : assert(doubleTapMinTime != null),
       assert(event != null),
67
       assert(event.buttons != null),
68
       pointer = event.pointer,
69
       _initialGlobalPosition = event.position,
70
       initialButtons = event.buttons,
71
       _doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
Hixie's avatar
Hixie committed
72

73
  final DeviceGestureSettings? gestureSettings;
Hixie's avatar
Hixie committed
74
  final int pointer;
75
  final GestureArenaEntry entry;
76
  final Offset _initialGlobalPosition;
77
  final int initialButtons;
78
  final _CountdownZoned _doubleTapMinTimeCountdown;
Hixie's avatar
Hixie committed
79 80 81

  bool _isTrackingPointer = false;

82
  void startTrackingPointer(PointerRoute route, Matrix4? transform) {
Hixie's avatar
Hixie committed
83 84
    if (!_isTrackingPointer) {
      _isTrackingPointer = true;
85
      GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform);
Hixie's avatar
Hixie committed
86 87 88
    }
  }

89
  void stopTrackingPointer(PointerRoute route) {
Hixie's avatar
Hixie committed
90 91
    if (_isTrackingPointer) {
      _isTrackingPointer = false;
92
      GestureBinding.instance.pointerRouter.removeRoute(pointer, route);
Hixie's avatar
Hixie committed
93 94 95
    }
  }

96 97
  bool isWithinGlobalTolerance(PointerEvent event, double tolerance) {
    final Offset offset = event.position - _initialGlobalPosition;
Hixie's avatar
Hixie committed
98 99
    return offset.distance <= tolerance;
  }
100 101 102 103

  bool hasElapsedMinTime() {
    return _doubleTapMinTimeCountdown.timeout;
  }
104 105 106 107

  bool hasSameButton(PointerDownEvent event) {
    return event.buttons == initialButtons;
  }
Hixie's avatar
Hixie committed
108 109
}

110 111
/// Recognizes when the user has tapped the screen at the same location twice in
/// quick succession.
112 113 114 115
///
/// [DoubleTapGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has a non-null callback. If it has no callbacks, it is a no-op.
///
116
class DoubleTapGestureRecognizer extends GestureRecognizer {
117
  /// Create a gesture recognizer for double taps.
118
  ///
119
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
120
  DoubleTapGestureRecognizer({
121
    Object? debugOwner,
122 123 124 125
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
126
    PointerDeviceKind? kind,
127 128 129 130 131 132
    Set<PointerDeviceKind>? supportedDevices,
  }) : super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
133

Hixie's avatar
Hixie committed
134
  // Implementation notes:
135
  //
Hixie's avatar
Hixie committed
136 137
  // The double tap recognizer can be in one of four states. There's no
  // explicit enum for the states, because they are already captured by
138
  // the state of existing fields. Specifically:
139 140 141 142 143 144 145 146 147 148 149 150 151
  //
  // 1. Waiting on first tap: In this state, the _trackers list is empty, and
  //    _firstTap is null.
  // 2. First tap in progress: In this state, the _trackers list contains all
  //    the states for taps that have begun but not completed. This list can
  //    have more than one entry if two pointers begin to tap.
  // 3. Waiting on second tap: In this state, one of the in-progress taps has
  //    completed successfully. The _trackers list is again empty, and
  //    _firstTap records the successful tap.
  // 4. Second tap in progress: Much like the "first tap in progress" state, but
  //    _firstTap is non-null. If a tap completes successfully while in this
  //    state, the callback is called and the state is reset.
  //
Hixie's avatar
Hixie committed
152
  // There are various other scenarios that cause the state to reset:
153
  //
Hixie's avatar
Hixie committed
154 155 156 157
  // - All in-progress taps are rejected (by time, distance, pointercancel, etc)
  // - The long timer between taps expires
  // - The gesture arena decides we have been rejected wholesale

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
  /// A pointer has contacted the screen with a primary button at the same
  /// location twice in quick succession, which might be the start of a double
  /// tap.
  ///
  /// This triggers immediately after the down event of the second tap.
  ///
  /// If this recognizer doesn't win the arena, [onDoubleTapCancel] is called
  /// next. Otherwise, [onDoubleTap] is called next.
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [TapDownDetails], which is passed as an argument to this callback.
  ///  * [GestureDetector.onDoubleTapDown], which exposes this callback.
  GestureTapDownCallback? onDoubleTapDown;

174 175 176
  /// Called when the user has tapped the screen with a primary button at the
  /// same location twice in quick succession.
  ///
Dan Field's avatar
Dan Field committed
177 178
  /// This triggers when the pointer stops contacting the device after the
  /// second tap.
179 180 181 182
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
183
  ///  * [GestureDetector.onDoubleTap], which exposes this callback.
184
  GestureDoubleTapCallback? onDoubleTap;
Hixie's avatar
Hixie committed
185

186 187 188 189 190 191 192 193 194 195 196 197 198 199
  /// A pointer that previously triggered [onDoubleTapDown] will not end up
  /// causing a double tap.
  ///
  /// This triggers once the gesture loses the arena if [onDoubleTapDown] has
  /// previously been triggered.
  ///
  /// If this recognizer wins the arena, [onDoubleTap] is called instead.
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [GestureDetector.onDoubleTapCancel], which exposes this callback.
  GestureTapCancelCallback? onDoubleTapCancel;

200 201
  Timer? _doubleTapTimer;
  _TapTracker? _firstTap;
202
  final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
Hixie's avatar
Hixie committed
203

204
  @override
205
  bool isPointerAllowed(PointerDownEvent event) {
206 207 208
    if (_firstTap == null) {
      switch (event.buttons) {
        case kPrimaryButton:
209 210 211
          if (onDoubleTapDown == null &&
              onDoubleTap == null &&
              onDoubleTapCancel == null)
212 213 214 215 216 217
            return false;
          break;
        default:
          return false;
      }
    }
218
    return super.isPointerAllowed(event);
219 220
  }

221
  @override
222
  void addAllowedPointer(PointerDownEvent event) {
223
    if (_firstTap != null) {
224
      if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
225 226
        // Ignore out-of-bounds second taps.
        return;
227 228 229
      } else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) {
        // Restart when the second tap is too close to the first (touch screens
        // often detect touches intermittently), or when buttons mismatch.
230
        _reset();
231 232 233 234 235 236 237 238
        return _trackTap(event);
      } else if (onDoubleTapDown != null) {
        final TapDownDetails details = TapDownDetails(
          globalPosition: event.position,
          localPosition: event.localPosition,
          kind: getKindForPointer(event.pointer),
        );
        invokeCallback<void>('onDoubleTapDown', () => onDoubleTapDown!(details));
239 240
      }
    }
241
    _trackTap(event);
242 243
  }

244
  void _trackTap(PointerDownEvent event) {
Hixie's avatar
Hixie committed
245
    _stopDoubleTapTimer();
246
    final _TapTracker tracker = _TapTracker(
247
      event: event,
248
      entry: GestureBinding.instance.gestureArena.add(event.pointer, this),
249
      doubleTapMinTime: kDoubleTapMinTime,
250
      gestureSettings: gestureSettings,
Hixie's avatar
Hixie committed
251 252
    );
    _trackers[event.pointer] = tracker;
253
    tracker.startTrackingPointer(_handleEvent, event.transform);
Hixie's avatar
Hixie committed
254 255
  }

256
  void _handleEvent(PointerEvent event) {
257
    final _TapTracker tracker = _trackers[event.pointer]!;
Ian Hickson's avatar
Ian Hickson committed
258 259 260 261 262 263
    if (event is PointerUpEvent) {
      if (_firstTap == null)
        _registerFirstTap(tracker);
      else
        _registerSecondTap(tracker);
    } else if (event is PointerMoveEvent) {
264
      if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
Hixie's avatar
Hixie committed
265
        _reject(tracker);
Ian Hickson's avatar
Ian Hickson committed
266 267
    } else if (event is PointerCancelEvent) {
      _reject(tracker);
Hixie's avatar
Hixie committed
268 269 270
    }
  }

271
  @override
272
  void acceptGesture(int pointer) { }
Hixie's avatar
Hixie committed
273

274
  @override
Hixie's avatar
Hixie committed
275
  void rejectGesture(int pointer) {
276
    _TapTracker? tracker = _trackers[pointer];
Hixie's avatar
Hixie committed
277 278 279
    // If tracker isn't in the list, check if this is the first tap tracker
    if (tracker == null &&
        _firstTap != null &&
280
        _firstTap!.pointer == pointer)
Hixie's avatar
Hixie committed
281 282 283 284 285 286 287 288
      tracker = _firstTap;
    // If tracker is still null, we rejected ourselves already
    if (tracker != null)
      _reject(tracker);
  }

  void _reject(_TapTracker tracker) {
    _trackers.remove(tracker.pointer);
289
    tracker.entry.resolve(GestureDisposition.rejected);
Hixie's avatar
Hixie committed
290
    _freezeTracker(tracker);
291 292 293 294 295 296 297 298 299
    if (_firstTap != null) {
      if (tracker == _firstTap) {
        _reset();
      } else {
        _checkCancel();
        if (_trackers.isEmpty)
          _reset();
      }
    }
Hixie's avatar
Hixie committed
300 301
  }

302
  @override
Hixie's avatar
Hixie committed
303 304
  void dispose() {
    _reset();
305
    super.dispose();
Hixie's avatar
Hixie committed
306 307 308 309 310
  }

  void _reset() {
    _stopDoubleTapTimer();
    if (_firstTap != null) {
311 312
      if (_trackers.isNotEmpty)
        _checkCancel();
Hixie's avatar
Hixie committed
313
      // Note, order is important below in order for the resolve -> reject logic
Florian Loitsch's avatar
Florian Loitsch committed
314
      // to work properly.
315
      final _TapTracker tracker = _firstTap!;
Hixie's avatar
Hixie committed
316 317
      _firstTap = null;
      _reject(tracker);
318
      GestureBinding.instance.gestureArena.release(tracker.pointer);
Hixie's avatar
Hixie committed
319 320 321 322 323 324
    }
    _clearTrackers();
  }

  void _registerFirstTap(_TapTracker tracker) {
    _startDoubleTapTimer();
325
    GestureBinding.instance.gestureArena.hold(tracker.pointer);
Hixie's avatar
Hixie committed
326 327 328 329 330 331 332 333 334
    // Note, order is important below in order for the clear -> reject logic to
    // work properly.
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
    _clearTrackers();
    _firstTap = tracker;
  }

  void _registerSecondTap(_TapTracker tracker) {
335 336
    _firstTap!.entry.resolve(GestureDisposition.accepted);
    tracker.entry.resolve(GestureDisposition.accepted);
Hixie's avatar
Hixie committed
337 338
    _freezeTracker(tracker);
    _trackers.remove(tracker.pointer);
339
    _checkUp(tracker.initialButtons);
Hixie's avatar
Hixie committed
340 341 342 343
    _reset();
  }

  void _clearTrackers() {
344
    _trackers.values.toList().forEach(_reject);
Hixie's avatar
Hixie committed
345 346 347 348
    assert(_trackers.isEmpty);
  }

  void _freezeTracker(_TapTracker tracker) {
349
    tracker.stopTrackingPointer(_handleEvent);
Hixie's avatar
Hixie committed
350 351 352
  }

  void _startDoubleTapTimer() {
353
    _doubleTapTimer ??= Timer(kDoubleTapTimeout, _reset);
Hixie's avatar
Hixie committed
354 355 356 357
  }

  void _stopDoubleTapTimer() {
    if (_doubleTapTimer != null) {
358
      _doubleTapTimer!.cancel();
Hixie's avatar
Hixie committed
359 360 361 362
      _doubleTapTimer = null;
    }
  }

363 364 365
  void _checkUp(int buttons) {
    assert(buttons == kPrimaryButton);
    if (onDoubleTap != null)
366
      invokeCallback<void>('onDoubleTap', onDoubleTap!);
367 368
  }

369 370 371 372 373
  void _checkCancel() {
    if (onDoubleTapCancel != null)
      invokeCallback<void>('onDoubleTapCancel', onDoubleTapCancel!);
  }

374
  @override
375
  String get debugDescription => 'double tap';
Hixie's avatar
Hixie committed
376 377 378 379 380 381 382 383
}

/// TapGesture represents a full gesture resulting from a single tap sequence,
/// as part of a [MultiTapGestureRecognizer]. Tap gestures are passive, meaning
/// that they will not preempt any other arena member in play.
class _TapGesture extends _TapTracker {

  _TapGesture({
384 385 386
    required this.gestureRecognizer,
    required PointerEvent event,
    required Duration longTapDelay,
387
    required DeviceGestureSettings? gestureSettings,
388
  }) : _lastPosition = OffsetPair.fromEventPosition(event),
Ian Hickson's avatar
Ian Hickson committed
389
       super(
390
    event: event as PointerDownEvent,
391
    entry: GestureBinding.instance.gestureArena.add(event.pointer, gestureRecognizer),
392
    doubleTapMinTime: kDoubleTapMinTime,
393
    gestureSettings: gestureSettings,
Ian Hickson's avatar
Ian Hickson committed
394
  ) {
395
    startTrackingPointer(handleEvent, event.transform);
396
    if (longTapDelay > Duration.zero) {
397
      _timer = Timer(longTapDelay, () {
Hixie's avatar
Hixie committed
398
        _timer = null;
399
        gestureRecognizer._dispatchLongTap(event.pointer, _lastPosition);
Hixie's avatar
Hixie committed
400 401
      });
    }
Hixie's avatar
Hixie committed
402 403 404 405 406
  }

  final MultiTapGestureRecognizer gestureRecognizer;

  bool _wonArena = false;
407
  Timer? _timer;
Hixie's avatar
Hixie committed
408

409
  OffsetPair _lastPosition;
410
  OffsetPair? _finalPosition;
Hixie's avatar
Hixie committed
411

Ian Hickson's avatar
Ian Hickson committed
412
  void handleEvent(PointerEvent event) {
Hixie's avatar
Hixie committed
413
    assert(event.pointer == pointer);
Ian Hickson's avatar
Ian Hickson committed
414
    if (event is PointerMoveEvent) {
415
      if (!isWithinGlobalTolerance(event, computeHitSlop(event.kind, gestureSettings)))
Hixie's avatar
Hixie committed
416 417
        cancel();
      else
418
        _lastPosition = OffsetPair.fromEventPosition(event);
Ian Hickson's avatar
Ian Hickson committed
419
    } else if (event is PointerCancelEvent) {
Hixie's avatar
Hixie committed
420
      cancel();
Ian Hickson's avatar
Ian Hickson committed
421
    } else if (event is PointerUpEvent) {
422
      stopTrackingPointer(handleEvent);
423
      _finalPosition = OffsetPair.fromEventPosition(event);
Hixie's avatar
Hixie committed
424 425 426 427
      _check();
    }
  }

428
  @override
429
  void stopTrackingPointer(PointerRoute route) {
Hixie's avatar
Hixie committed
430 431
    _timer?.cancel();
    _timer = null;
432
    super.stopTrackingPointer(route);
Hixie's avatar
Hixie committed
433 434
  }

Hixie's avatar
Hixie committed
435 436 437 438 439 440
  void accept() {
    _wonArena = true;
    _check();
  }

  void reject() {
441
    stopTrackingPointer(handleEvent);
442
    gestureRecognizer._dispatchCancel(pointer);
Hixie's avatar
Hixie committed
443 444 445 446 447 448 449 450
  }

  void cancel() {
    // If we won the arena already, then entry is resolved, so resolving
    // again is a no-op. But we still need to clean up our own state.
    if (_wonArena)
      reject();
    else
451
      entry.resolve(GestureDisposition.rejected); // eventually calls reject()
Hixie's avatar
Hixie committed
452 453 454 455
  }

  void _check() {
    if (_wonArena && _finalPosition != null)
456
      gestureRecognizer._dispatchTap(pointer, _finalPosition!);
Hixie's avatar
Hixie committed
457 458 459
  }
}

460 461 462 463 464 465 466 467 468
/// Recognizes taps on a per-pointer basis.
///
/// [MultiTapGestureRecognizer] considers each sequence of pointer events that
/// could constitute a tap independently of other pointers: For example, down-1,
/// down-2, up-1, up-2 produces two taps, on up-1 and up-2.
///
/// See also:
///
///  * [TapGestureRecognizer]
469
class MultiTapGestureRecognizer extends GestureRecognizer {
470 471
  /// Creates a multi-tap gesture recognizer.
  ///
472
  /// The [longTapDelay] defaults to [Duration.zero], which means
473
  /// [onLongTapDown] is called immediately after [onTapDown].
474 475
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
Hixie's avatar
Hixie committed
476
  MultiTapGestureRecognizer({
477
    this.longTapDelay = Duration.zero,
478
    Object? debugOwner,
479 480 481 482
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
483
    PointerDeviceKind? kind,
484 485 486 487 488 489
    Set<PointerDeviceKind>? supportedDevices,
  }) : super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
Hixie's avatar
Hixie committed
490

491 492
  /// A pointer that might cause a tap has contacted the screen at a particular
  /// location.
493
  GestureMultiTapDownCallback? onTapDown;
494 495 496

  /// A pointer that will trigger a tap has stopped contacting the screen at a
  /// particular location.
497
  GestureMultiTapUpCallback? onTapUp;
498 499

  /// A tap has occurred.
500
  GestureMultiTapCallback? onTap;
501 502 503

  /// The pointer that previously triggered [onTapDown] will not end up causing
  /// a tap.
504
  GestureMultiTapCancelCallback? onTapCancel;
505 506

  /// The amount of time between [onTapDown] and [onLongTapDown].
Hixie's avatar
Hixie committed
507
  Duration longTapDelay;
508 509 510

  /// A pointer that might cause a tap is still in contact with the screen at a
  /// particular location after [longTapDelay].
511
  GestureMultiTapDownCallback? onLongTapDown;
Hixie's avatar
Hixie committed
512

513
  final Map<int, _TapGesture> _gestureMap = <int, _TapGesture>{};
Hixie's avatar
Hixie committed
514

515
  @override
516
  void addAllowedPointer(PointerDownEvent event) {
Hixie's avatar
Hixie committed
517
    assert(!_gestureMap.containsKey(event.pointer));
518
    _gestureMap[event.pointer] = _TapGesture(
Hixie's avatar
Hixie committed
519
      gestureRecognizer: this,
Hixie's avatar
Hixie committed
520
      event: event,
521
      longTapDelay: longTapDelay,
522
      gestureSettings: gestureSettings,
Hixie's avatar
Hixie committed
523 524
    );
    if (onTapDown != null)
525
      invokeCallback<void>('onTapDown', () {
526
        onTapDown!(event.pointer, TapDownDetails(
527
          globalPosition: event.position,
528
          localPosition: event.localPosition,
529 530 531
          kind: event.kind,
        ));
      });
Hixie's avatar
Hixie committed
532 533
  }

534
  @override
Hixie's avatar
Hixie committed
535 536
  void acceptGesture(int pointer) {
    assert(_gestureMap.containsKey(pointer));
537
    _gestureMap[pointer]!.accept();
Hixie's avatar
Hixie committed
538 539
  }

540
  @override
Hixie's avatar
Hixie committed
541 542
  void rejectGesture(int pointer) {
    assert(_gestureMap.containsKey(pointer));
543
    _gestureMap[pointer]!.reject();
Hixie's avatar
Hixie committed
544
    assert(!_gestureMap.containsKey(pointer));
Hixie's avatar
Hixie committed
545 546
  }

547 548
  void _dispatchCancel(int pointer) {
    assert(_gestureMap.containsKey(pointer));
Hixie's avatar
Hixie committed
549
    _gestureMap.remove(pointer);
550
    if (onTapCancel != null)
551
      invokeCallback<void>('onTapCancel', () => onTapCancel!(pointer));
552 553
  }

554
  void _dispatchTap(int pointer, OffsetPair position) {
555 556 557
    assert(_gestureMap.containsKey(pointer));
    _gestureMap.remove(pointer);
    if (onTapUp != null)
558
      invokeCallback<void>('onTapUp', () {
559
        onTapUp!(pointer, TapUpDetails(
560
          kind: getKindForPointer(pointer),
561 562 563 564
          localPosition: position.local,
          globalPosition: position.global,
        ));
      });
565
    if (onTap != null)
566
      invokeCallback<void>('onTap', () => onTap!(pointer));
Hixie's avatar
Hixie committed
567 568
  }

569
  void _dispatchLongTap(int pointer, OffsetPair lastPosition) {
Hixie's avatar
Hixie committed
570 571
    assert(_gestureMap.containsKey(pointer));
    if (onLongTapDown != null)
572
      invokeCallback<void>('onLongTapDown', () {
573
        onLongTapDown!(
574 575
          pointer,
          TapDownDetails(
576 577
            globalPosition: lastPosition.global,
            localPosition: lastPosition.local,
578 579 580 581
            kind: getKindForPointer(pointer),
          ),
        );
      });
Hixie's avatar
Hixie committed
582 583
  }

584
  @override
Hixie's avatar
Hixie committed
585
  void dispose() {
586
    final List<_TapGesture> localGestures = List<_TapGesture>.of(_gestureMap.values);
587
    for (final _TapGesture gesture in localGestures)
Hixie's avatar
Hixie committed
588 589 590
      gesture.cancel();
    // Rejection of each gesture should cause it to be removed from our map
    assert(_gestureMap.isEmpty);
591
    super.dispose();
Hixie's avatar
Hixie committed
592 593
  }

594
  @override
595
  String get debugDescription => 'multitap';
Hixie's avatar
Hixie committed
596
}
597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888

/// Signature used by [SerialTapGestureRecognizer.onSerialTapDown] for when a
/// pointer that might cause a serial tap has contacted the screen at a
/// particular location.
typedef GestureSerialTapDownCallback = void Function(SerialTapDownDetails details);

/// Details for [GestureSerialTapDownCallback], such as the tap count within
/// the series.
///
/// See also:
///
///  * [SerialTapGestureRecognizer], which passes this information to its
///    [SerialTapGestureRecognizer.onSerialTapDown] callback.
class SerialTapDownDetails {
  /// Creates details for a [GestureSerialTapDownCallback].
  ///
  /// The `count` argument must be greater than zero.
  SerialTapDownDetails({
    this.globalPosition = Offset.zero,
    Offset? localPosition,
    required this.kind,
    this.buttons = 0,
    this.count = 1,
  }) : assert(count > 0),
       localPosition = localPosition ?? globalPosition;

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

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;

  /// The kind of the device that initiated the event.
  final PointerDeviceKind kind;

  /// Which buttons were pressed when the pointer contacted the screen.
  ///
  /// See also:
  ///
  ///  * [PointerEvent.buttons], which this field reflects.
  final int buttons;

  /// The number of consecutive taps that this "tap down" represents.
  ///
  /// This value will always be greater than zero. When the first pointer in a
  /// possible series contacts the screen, this value will be `1`, the second
  /// tap in a double-tap will be `2`, and so on.
  ///
  /// If a tap is determined to not be in the same series as the tap that
  /// preceded it (e.g. because too much time elapsed between the two taps or
  /// the two taps had too much distance between them), then this count will
  /// reset back to `1`, and a new series will have begun.
  final int count;
}

/// Signature used by [SerialTapGestureRecognizer.onSerialTapCancel] for when a
/// pointer that previously triggered a [GestureSerialTapDownCallback] will not
/// end up completing the serial tap.
typedef GestureSerialTapCancelCallback = void Function(SerialTapCancelDetails details);

/// Details for [GestureSerialTapCancelCallback], such as the tap count within
/// the series.
///
/// See also:
///
///  * [SerialTapGestureRecognizer], which passes this information to its
///    [SerialTapGestureRecognizer.onSerialTapCancel] callback.
class SerialTapCancelDetails {
  /// Creates details for a [GestureSerialTapCancelCallback].
  ///
  /// The `count` argument must be greater than zero.
  SerialTapCancelDetails({
    this.count = 1,
  }) : assert(count != null),
       assert(count > 0);

  /// The number of consecutive taps that were in progress when the gesture was
  /// interrupted.
  ///
  /// This number will match the corresponding count that was specified in
  /// [SerialTapDownDetails.count] for the tap that is being canceled. See
  /// that field for more information on how this count is reported.
  final int count;
}

/// Signature used by [SerialTapGestureRecognizer.onSerialTapUp] for when a
/// pointer that will trigger a serial tap has stopped contacting the screen.
typedef GestureSerialTapUpCallback = void Function(SerialTapUpDetails details);

/// Details for [GestureSerialTapUpCallback], such as the tap count within
/// the series.
///
/// See also:
///
///  * [SerialTapGestureRecognizer], which passes this information to its
///    [SerialTapGestureRecognizer.onSerialTapUp] callback.
class SerialTapUpDetails {
  /// Creates details for a [GestureSerialTapUpCallback].
  ///
  /// The `count` argument must be greater than zero.
  SerialTapUpDetails({
    this.globalPosition = Offset.zero,
    Offset? localPosition,
    this.kind,
    this.count = 1,
  }) : assert(count > 0),
       localPosition = localPosition ?? globalPosition;

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

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;

  /// The kind of the device that initiated the event.
  final PointerDeviceKind? kind;

  /// The number of consecutive taps that this tap represents.
  ///
  /// This value will always be greater than zero. When the first pointer in a
  /// possible series completes its tap, this value will be `1`, the second
  /// tap in a double-tap will be `2`, and so on.
  ///
  /// If a tap is determined to not be in the same series as the tap that
  /// preceded it (e.g. because too much time elapsed between the two taps or
  /// the two taps had too much distance between them), then this count will
  /// reset back to `1`, and a new series will have begun.
  final int count;
}

/// Recognizes serial taps (taps in a series).
///
/// A collection of taps are considered to be _in a series_ if they occur in
/// rapid succession in the same location (within a tolerance). The number of
/// taps in the series is its count. A double-tap, for instance, is a special
/// case of a tap series with a count of two.
///
/// ### Gesture arena behavior
///
/// [SerialTapGestureRecognizer] competes on all pointer events (regardless of
/// button). It will declare defeat if it determines that a gesture is not a
/// tap (e.g. if the pointer is dragged too far while it's contacting the
/// screen). It will immediately declare victory for every tap that it
/// recognizes.
///
/// Each time a pointer contacts the screen, this recognizer will enter that
/// gesture into the arena. This means that this recognizer will yield multiple
/// winning entries in the arena for a single tap series as the series
/// progresses.
///
/// If this recognizer loses the arena (either by declaring defeat or by
/// another recognizer declaring victory) while the pointer is contacting the
/// screen, it will fire [onSerialTapCancel], and [onSerialTapUp] will not
/// be fired.
///
/// ### Button behavior
///
/// A tap series is defined to have the same buttons across all taps. If a tap
/// with a different combination of buttons is delivered in the middle of a
/// series, it will "steal" the series and begin a new series, starting the
/// count over.
///
/// ### Interleaving tap behavior
///
/// A tap must be _completed_ in order for a subsequent tap to be considered
/// "in the same series" as that tap. Thus, if tap A is in-progress (the down
/// event has been received, but the corresponding up event has not yet been
/// received), and tap B begins (another pointer contacts the screen), tap A
/// will fire [onSerialTapCancel], and tap B will begin a new series (tap B's
/// [SerialTapDownDetails.count] will be 1).
///
/// ### Relation to `TapGestureRecognizer` and `DoubleTapGestureRecognizer`
///
/// [SerialTapGestureRecognizer] fires [onSerialTapDown] and [onSerialTapUp]
/// for every tap that it recognizes (passing the count in the details),
/// regardless of whether that tap is a single-tap, double-tap, etc. This
/// makes it especially useful when you want to respond to every tap in a
/// series. Contrast this with [DoubleTapGestureRecognizer], which only fires
/// if the user completes a double-tap, and [TapGestureRecognizer], which
/// _doesn't_ fire if the recognizer is competing with a
/// `DoubleTapGestureRecognizer`, and the user double-taps.
///
/// For example, consider a list item that should be _selected_ on the first
/// tap and _cause an edit dialog to open_ on a double-tap. If you use both
/// [TapGestureRecognizer] and [DoubleTapGestureRecognizer], there are a few
/// problems:
///
///   1. If the user single-taps the list item, it will not select
///      the list item until after enough time has passed to rule out a
///      double-tap.
///   2. If the user double-taps the list item, it will not select the list
///      item at all.
///
/// The solution is to use [SerialTapGestureRecognizer] and use the tap count
/// to either select the list item or open the edit dialog.
///
/// ### When competing with `TapGestureRecognizer` and `DoubleTapGestureRecognizer`
///
/// Unlike [TapGestureRecognizer] and [DoubleTapGestureRecognizer],
/// [SerialTapGestureRecognizer] aggressively declares victory when it detects
/// a tap, so when it is competing with those gesture recognizers, it will beat
/// them in the arena, regardless of which recognizer entered the arena first.
class SerialTapGestureRecognizer extends GestureRecognizer {
  /// Creates a serial tap gesture recognizer.
  SerialTapGestureRecognizer({
    Object? debugOwner,
    Set<PointerDeviceKind>? supportedDevices,
  }) : super(debugOwner: debugOwner, supportedDevices: supportedDevices);

  /// A pointer has contacted the screen at a particular location, which might
  /// be the start of a serial tap.
  ///
  /// If this recognizer loses the arena before the serial tap is completed
  /// (either because the gesture does not end up being a tap or because another
  /// recognizer wins the arena), [onSerialTapCancel] is called next. Otherwise,
  /// [onSerialTapUp] is called next.
  ///
  /// The [SerialTapDownDetails.count] that is passed to this callback
  /// specifies the series tap count.
  GestureSerialTapDownCallback? onSerialTapDown;

  /// A pointer that previously triggered [onSerialTapDown] will not end up
  /// triggering the corresponding [onSerialTapUp].
  ///
  /// If the user completes the serial tap, [onSerialTapUp] is called instead.
  ///
  /// The [SerialTapCancelDetails.count] that is passed to this callback will
  /// match the [SerialTapDownDetails.count] that was passed to the
  /// [onSerialTapDown] callback.
  GestureSerialTapCancelCallback? onSerialTapCancel;

  /// A pointer has stopped contacting the screen at a particular location,
  /// representing a serial tap.
  ///
  /// If the user didn't complete the tap, or if another recognizer won the
  /// arena, then [onSerialTapCancel] is called instead.
  ///
  /// The [SerialTapUpDetails.count] that is passed to this callback specifies
  /// the series tap count and will match the [SerialTapDownDetails.count] that
  /// was passed to the [onSerialTapDown] callback.
  GestureSerialTapUpCallback? onSerialTapUp;

  Timer? _serialTapTimer;
  final List<_TapTracker> _completedTaps = <_TapTracker>[];
  final Map<int, GestureDisposition> _gestureResolutions = <int, GestureDisposition>{};
  _TapTracker? _pendingTap;

  /// Indicates whether this recognizer is currently tracking a pointer that's
  /// in contact with the screen.
  ///
  /// If this is true, it implies that [onSerialTapDown] has fired, but neither
  /// [onSerialTapCancel] nor [onSerialTapUp] have yet fired.
  bool get isTrackingPointer => _pendingTap != null;

  @override
  bool isPointerAllowed(PointerDownEvent event) {
    if (onSerialTapDown == null &&
        onSerialTapCancel == null &&
        onSerialTapUp == null) {
      return false;
    }
    return super.isPointerAllowed(event);
  }

  @override
  void addAllowedPointer(PointerDownEvent event) {
    if ((_completedTaps.isNotEmpty && !_representsSameSeries(_completedTaps.last, event))
        || _pendingTap != null) {
      _reset();
    }
    _trackTap(event);
  }

  bool _representsSameSeries(_TapTracker tap, PointerDownEvent event) {
    return tap.hasElapsedMinTime() // touch screens often detect touches intermittently
        && tap.hasSameButton(event)
        && tap.isWithinGlobalTolerance(event, kDoubleTapSlop);
  }

  void _trackTap(PointerDownEvent event) {
    _stopSerialTapTimer();
    if (onSerialTapDown != null) {
      final SerialTapDownDetails details = SerialTapDownDetails(
        globalPosition: event.position,
        localPosition: event.localPosition,
        kind: getKindForPointer(event.pointer),
        buttons: event.buttons,
        count: _completedTaps.length + 1,
      );
      invokeCallback<void>('onSerialTapDown', () => onSerialTapDown!(details));
    }
    final _TapTracker tracker = _TapTracker(
889
      gestureSettings: gestureSettings,
890
      event: event,
891
      entry: GestureBinding.instance.gestureArena.add(event.pointer, this),
892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
      doubleTapMinTime: kDoubleTapMinTime,
    );
    assert(_pendingTap == null);
    _pendingTap = tracker;
    tracker.startTrackingPointer(_handleEvent, event.transform);
  }

  void _handleEvent(PointerEvent event) {
    assert(_pendingTap != null);
    assert(_pendingTap!.pointer == event.pointer);
    final _TapTracker tracker = _pendingTap!;
    if (event is PointerUpEvent) {
      _registerTap(event, tracker);
    } else if (event is PointerMoveEvent) {
      if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
        _reset();
      }
    } else if (event is PointerCancelEvent) {
      _reset();
    }
  }

  @override
  void acceptGesture(int pointer) {
    assert(_pendingTap != null);
    assert(_pendingTap!.pointer == pointer);
    _gestureResolutions[pointer] = GestureDisposition.accepted;
  }

  @override
  void rejectGesture(int pointer) {
    _gestureResolutions[pointer] = GestureDisposition.rejected;
    _reset();
  }

  void _rejectPendingTap() {
    assert(_pendingTap != null);
    final _TapTracker tracker = _pendingTap!;
    _pendingTap = null;
    // Order is important here; the `resolve` call can yield a re-entrant
    // `reset()`, so we need to check cancel here while we can trust the
    // length of our _completedTaps list.
    _checkCancel(_completedTaps.length + 1);
    if (!_gestureResolutions.containsKey(tracker.pointer)) {
      tracker.entry.resolve(GestureDisposition.rejected);
    }
    _stopTrackingPointer(tracker);
  }

  @override
  void dispose() {
    _reset();
    super.dispose();
  }

  void _reset() {
    if (_pendingTap != null) {
      _rejectPendingTap();
    }
    _pendingTap = null;
    _completedTaps.clear();
    _gestureResolutions.clear();
    _stopSerialTapTimer();
  }

  void _registerTap(PointerUpEvent event, _TapTracker tracker) {
    assert(tracker == _pendingTap);
    assert(tracker.pointer == event.pointer);
    _startSerialTapTimer();
    assert(_gestureResolutions[event.pointer] != GestureDisposition.rejected);
    if (!_gestureResolutions.containsKey(event.pointer)) {
      tracker.entry.resolve(GestureDisposition.accepted);
    }
    assert(_gestureResolutions[event.pointer] == GestureDisposition.accepted);
    _stopTrackingPointer(tracker);
    // Note, order is important below in order for the clear -> reject logic to
    // work properly.
    _pendingTap = null;
    _checkUp(event, tracker);
    _completedTaps.add(tracker);
  }

  void _stopTrackingPointer(_TapTracker tracker) {
    tracker.stopTrackingPointer(_handleEvent);
  }

  void _startSerialTapTimer() {
    _serialTapTimer ??= Timer(kDoubleTapTimeout, _reset);
  }

  void _stopSerialTapTimer() {
    if (_serialTapTimer != null) {
      _serialTapTimer!.cancel();
      _serialTapTimer = null;
    }
  }

  void _checkUp(PointerUpEvent event, _TapTracker tracker) {
    if (onSerialTapUp != null) {
      final SerialTapUpDetails details = SerialTapUpDetails(
        globalPosition: event.position,
        localPosition: event.localPosition,
        kind: getKindForPointer(tracker.pointer),
        count: _completedTaps.length + 1,
      );
      invokeCallback<void>('onSerialTapUp', () => onSerialTapUp!(details));
    }
  }

  void _checkCancel(int count) {
    if (onSerialTapCancel != null) {
      final SerialTapCancelDetails details = SerialTapCancelDetails(
        count: count,
      );
      invokeCallback<void>('onSerialTapCancel', () => onSerialTapCancel!(details));
    }
  }

  @override
  String get debugDescription => 'serial tap';
}