recognizer.dart 27.3 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 'dart:async';
7
import 'dart:collection';
8

9
import 'package:flutter/foundation.dart';
10

11
import 'arena.dart';
12
import 'binding.dart';
13
import 'constants.dart';
14
import 'debug.dart';
15
import 'events.dart';
16
import 'pointer_router.dart';
17
import 'team.dart';
18

19 20 21 22 23 24 25 26
export 'dart:ui' show Offset, PointerDeviceKind;

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

export 'arena.dart' show GestureDisposition;
export 'events.dart' show PointerDownEvent, PointerEvent, PointerPanZoomStartEvent;
export 'gesture_settings.dart' show DeviceGestureSettings;
27
export 'team.dart' show GestureArenaTeam;
28

29 30 31 32
/// Generic signature for callbacks passed to
/// [GestureRecognizer.invokeCallback]. This allows the
/// [GestureRecognizer.invokeCallback] mechanism to be generically used with
/// anonymous functions that return objects of particular types.
33
typedef RecognizerCallback<T> = T Function();
34

35 36 37 38
/// Configuration of offset passed to [DragStartDetails].
///
/// See also:
///
39 40
///  * [DragGestureRecognizer.dragStartBehavior], which gives an example for the
///  different behaviors.
41
enum DragStartBehavior {
42
  /// Set the initial offset at the position where the first down event was
43 44 45
  /// detected.
  down,

46 47
  /// Set the initial position at the position where this gesture recognizer
  /// won the arena.
48 49 50
  start,
}

51
/// The base class that all gesture recognizers inherit from.
52 53 54 55
///
/// Provides a basic API that can be used by classes that work with
/// gesture recognizers but don't care about the specific details of
/// the gestures recognizers themselves.
56 57 58
///
/// See also:
///
59 60
///  * [GestureDetector], the widget that is used to detect built-in gestures.
///  * [RawGestureDetector], the widget that is used to detect custom gestures.
61 62
///  * [debugPrintRecognizerCallbacksTrace], a flag that can be set to help
///    debug issues with gesture recognizers.
63
abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
64 65 66 67
  /// Initializes the gesture recognizer.
  ///
  /// The argument is optional and is only used for debug purposes (e.g. in the
  /// [toString] serialization).
68
  ///
69 70 71
  /// {@template flutter.gestures.GestureRecognizer.supportedDevices}
  /// It's possible to limit this recognizer to a specific set of [PointerDeviceKind]s
  /// by providing the optional [supportedDevices] argument. If [supportedDevices] is null,
72 73
  /// the recognizer will accept pointer events from all device kinds.
  /// {@endtemplate}
74 75 76 77 78 79 80 81 82 83
  GestureRecognizer({
    this.debugOwner,
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
    PointerDeviceKind? kind,
    Set<PointerDeviceKind>? supportedDevices,
  }) : assert(kind == null || supportedDevices == null),
       _supportedDevices = kind == null ? supportedDevices : <PointerDeviceKind>{ kind };
84 85 86 87 88

  /// The recognizer's owner.
  ///
  /// This is used in the [toString] serialization to report the object for which
  /// this gesture recognizer was created, to aid in debugging.
89
  final Object? debugOwner;
90

91 92 93 94
  /// Optional device specific configuration for device gestures that will
  /// take precedence over framework defaults.
  DeviceGestureSettings? gestureSettings;

95 96 97 98 99
  /// The kind of devices that are allowed to be recognized as provided by
  /// `supportedDevices` in the constructor, or the currently deprecated `kind`.
  /// These cannot both be set. If both are null, events from all device kinds will be
  /// tracked and recognized.
  final Set<PointerDeviceKind>? _supportedDevices;
100 101 102 103

  /// Holds a mapping between pointer IDs and the kind of devices they are
  /// coming from.
  final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
104

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
  /// Registers a new pointer pan/zoom that might be relevant to this gesture
  /// detector.
  ///
  /// A pointer pan/zoom is a stream of events that conveys data covering
  /// pan, zoom, and rotate data from a multi-finger trackpad gesture.
  ///
  /// The owner of this gesture recognizer calls addPointerPanZoom() with the
  /// PointerPanZoomStartEvent of each pointer that should be considered for
  /// this gesture.
  ///
  /// It's the GestureRecognizer's responsibility to then add itself
  /// to the global pointer router (see [PointerRouter]) to receive
  /// subsequent events for this pointer, and to add the pointer to
  /// the global gesture arena manager (see [GestureArenaManager]) to track
  /// that pointer.
  ///
  /// This method is called for each and all pointers being added. In
  /// most cases, you want to override [addAllowedPointerPanZoom] instead.
  void addPointerPanZoom(PointerPanZoomStartEvent event) {
    _pointerToKind[event.pointer] = event.kind;
    if (isPointerPanZoomAllowed(event)) {
      addAllowedPointerPanZoom(event);
    } else {
      handleNonAllowedPointerPanZoom(event);
    }
  }

  /// Registers a new pointer pan/zoom that's been checked to be allowed by this
  /// gesture recognizer.
  ///
  /// Subclasses of [GestureRecognizer] are supposed to override this method
  /// instead of [addPointerPanZoom] because [addPointerPanZoom] will be called for each
  /// pointer being added while [addAllowedPointerPanZoom] is only called for pointers
  /// that are allowed by this recognizer.
  @protected
  void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) { }

142 143
  /// Registers a new pointer that might be relevant to this gesture
  /// detector.
Florian Loitsch's avatar
Florian Loitsch committed
144
  ///
145 146 147 148 149 150 151
  /// The owner of this gesture recognizer calls addPointer() with the
  /// PointerDownEvent of each pointer that should be considered for
  /// this gesture.
  ///
  /// It's the GestureRecognizer's responsibility to then add itself
  /// to the global pointer router (see [PointerRouter]) to receive
  /// subsequent events for this pointer, and to add the pointer to
152
  /// the global gesture arena manager (see [GestureArenaManager]) to track
153
  /// that pointer.
154 155 156 157
  ///
  /// This method is called for each and all pointers being added. In
  /// most cases, you want to override [addAllowedPointer] instead.
  void addPointer(PointerDownEvent event) {
158
    _pointerToKind[event.pointer] = event.kind;
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
    if (isPointerAllowed(event)) {
      addAllowedPointer(event);
    } else {
      handleNonAllowedPointer(event);
    }
  }

  /// Registers a new pointer that's been checked to be allowed by this gesture
  /// recognizer.
  ///
  /// Subclasses of [GestureRecognizer] are supposed to override this method
  /// instead of [addPointer] because [addPointer] will be called for each
  /// pointer being added while [addAllowedPointer] is only called for pointers
  /// that are allowed by this recognizer.
  @protected
  void addAllowedPointer(PointerDownEvent event) { }

  /// Handles a pointer being added that's not allowed by this recognizer.
  ///
  /// Subclasses can override this method and reject the gesture.
  ///
  /// See:
  /// - [OneSequenceGestureRecognizer.handleNonAllowedPointer].
  @protected
  void handleNonAllowedPointer(PointerDownEvent event) { }

  /// Checks whether or not a pointer is allowed to be tracked by this recognizer.
  @protected
  bool isPointerAllowed(PointerDownEvent event) {
    // Currently, it only checks for device kind. But in the future we could check
    // for other things e.g. mouse button.
190
    return _supportedDevices == null || _supportedDevices!.contains(event.kind);
191 192
  }

193 194 195 196 197 198 199 200 201 202 203 204
  /// Handles a pointer pan/zoom being added that's not allowed by this recognizer.
  ///
  /// Subclasses can override this method and reject the gesture.
  @protected
  void handleNonAllowedPointerPanZoom(PointerPanZoomStartEvent event) { }

  /// Checks whether or not a pointer pan/zoom is allowed to be tracked by this recognizer.
  @protected
  bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) {
    return _supportedDevices == null || _supportedDevices!.contains(event.kind);
  }

205 206 207 208 209 210 211
  /// For a given pointer ID, returns the device kind associated with it.
  ///
  /// The pointer ID is expected to be a valid one i.e. an event was received
  /// with that pointer ID.
  @protected
  PointerDeviceKind getKindForPointer(int pointer) {
    assert(_pointerToKind.containsKey(pointer));
212
    return _pointerToKind[pointer]!;
213
  }
214

Florian Loitsch's avatar
Florian Loitsch committed
215 216
  /// Releases any resources used by the object.
  ///
217 218
  /// This method is called by the owner of this gesture recognizer
  /// when the object is no longer needed (e.g. when a gesture
219
  /// recognizer is being unregistered from a [GestureDetector], the
220
  /// GestureDetector widget calls this method).
221
  @mustCallSuper
222 223
  void dispose() { }

224 225
  /// Returns a very short pretty description of the gesture that the
  /// recognizer looks for, like 'tap' or 'horizontal drag'.
226
  String get debugDescription;
227

228 229
  /// Invoke a callback provided by the application, catching and logging any
  /// exceptions.
230 231
  ///
  /// The `name` argument is ignored except when reporting exceptions.
232 233 234 235 236
  ///
  /// The `debugReport` argument is optional and is used when
  /// [debugPrintRecognizerCallbacksTrace] is true. If specified, it must be a
  /// callback that returns a string describing useful debugging information,
  /// e.g. the arguments passed to the callback.
237
  @protected
238
  @pragma('vm:notify-debugger-on-exception')
239
  T? invokeCallback<T>(String name, RecognizerCallback<T> callback, { String Function()? debugReport }) {
240
    assert(callback != null);
241
    T? result;
242
    try {
243 244
      assert(() {
        if (debugPrintRecognizerCallbacksTrace) {
245
          final String? report = debugReport != null ? debugReport() : null;
246 247
          // The 19 in the line below is the width of the prefix used by
          // _debugLogDiagnostic in arena.dart.
248
          final String prefix = debugPrintGestureArenaDiagnostics ? '${' ' * 19}❙ ' : '';
249
          debugPrint('$prefix$this calling $name callback.${ (report?.isNotEmpty ?? false) ? " $report" : "" }');
250 251
        }
        return true;
252
      }());
253 254
      result = callback();
    } catch (exception, stack) {
255
      InformationCollector? collector;
256
      assert(() {
257 258 259 260
        collector = () => <DiagnosticsNode>[
          StringProperty('Handler', name),
          DiagnosticsProperty<GestureRecognizer>('Recognizer', this, style: DiagnosticsTreeStyle.errorProperty),
        ];
261 262
        return true;
      }());
263
      FlutterError.reportError(FlutterErrorDetails(
264 265 266
        exception: exception,
        stack: stack,
        library: 'gesture',
267
        context: ErrorDescription('while handling a gesture'),
268
        informationCollector: collector,
269 270 271 272
      ));
    }
    return result;
  }
273 274

  @override
275 276
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
277
    properties.add(DiagnosticsProperty<Object>('debugOwner', debugOwner, defaultValue: null));
278
  }
279 280
}

281 282 283 284 285 286 287 288
/// Base class for gesture recognizers that can only recognize one
/// gesture at a time. For example, a single [TapGestureRecognizer]
/// can never recognize two taps happening simultaneously, even if
/// multiple pointers are placed on the same widget.
///
/// This is in contrast to, for instance, [MultiTapGestureRecognizer],
/// which manages each pointer independently and can consider multiple
/// simultaneous touches to each result in a separate tap.
289
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
290
  /// Initialize the object.
291
  ///
292
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
293
  OneSequenceGestureRecognizer({
294
    super.debugOwner,
295 296 297 298
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
299 300 301
    super.kind,
    super.supportedDevices,
  });
302

303
  final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
304
  final Set<int> _trackedPointers = HashSet<int>();
305

306
  @override
307 308 309 310 311 312 313
  @protected
  void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer, event.transform);
  }

  @override
  @protected
314 315 316 317
  void handleNonAllowedPointer(PointerDownEvent event) {
    resolve(GestureDisposition.rejected);
  }

318
  /// Called when a pointer event is routed to this recognizer.
319 320 321 322 323 324 325 326 327 328 329 330 331 332
  ///
  /// This will be called for every pointer event while the pointer is being
  /// tracked. Typically, this recognizer will start tracking the pointer in
  /// [addAllowedPointer], which means that [handleEvent] will be called
  /// starting with the [PointerDownEvent] that was passed to [addAllowedPointer].
  ///
  /// See also:
  ///
  ///  * [startTrackingPointer], which causes pointer events to be routed to
  ///    this recognizer.
  ///  * [stopTrackingPointer], which stops events from being routed to this
  ///    recognizer.
  ///  * [stopTrackingIfPointerNoLongerDown], which conditionally stops events
  ///    from being routed to this recognizer.
333
  @protected
Ian Hickson's avatar
Ian Hickson committed
334
  void handleEvent(PointerEvent event);
335 336

  @override
337
  void acceptGesture(int pointer) { }
338 339

  @override
340
  void rejectGesture(int pointer) { }
341

342
  /// Called when the number of pointers this recognizer is tracking changes from one to zero.
343 344 345
  ///
  /// The given pointer ID is the ID of the last pointer this recognizer was
  /// tracking.
346
  @protected
347
  void didStopTrackingLastPointer(int pointer);
348

349 350
  /// Resolves this recognizer's participation in each gesture arena with the
  /// given disposition.
351 352
  @protected
  @mustCallSuper
353
  void resolve(GestureDisposition disposition) {
354
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.of(_entries.values);
355
    _entries.clear();
356
    for (final GestureArenaEntry entry in localEntries) {
357
      entry.resolve(disposition);
358
    }
359 360
  }

361 362 363 364 365
  /// Resolves this recognizer's participation in the given gesture arena with
  /// the given disposition.
  @protected
  @mustCallSuper
  void resolvePointer(int pointer, GestureDisposition disposition) {
366
    final GestureArenaEntry? entry = _entries[pointer];
367
    if (entry != null) {
368
      _entries.remove(pointer);
369
      entry.resolve(disposition);
370 371 372
    }
  }

373
  @override
374 375
  void dispose() {
    resolve(GestureDisposition.rejected);
376
    for (final int pointer in _trackedPointers) {
377
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
378
    }
379 380
    _trackedPointers.clear();
    assert(_entries.isEmpty);
381
    super.dispose();
382 383
  }

384 385 386 387 388 389 390 391 392 393
  /// The team that this recognizer belongs to, if any.
  ///
  /// If [team] is null, this recognizer competes directly in the
  /// [GestureArenaManager] to recognize a sequence of pointer events as a
  /// gesture. If [team] is non-null, this recognizer competes in the arena in
  /// a group with other recognizers on the same team.
  ///
  /// A recognizer can be assigned to a team only when it is not participating
  /// in the arena. For example, a common time to assign a recognizer to a team
  /// is shortly after creating the recognizer.
394 395
  GestureArenaTeam? get team => _team;
  GestureArenaTeam? _team;
396
  /// The [team] can only be set once.
397
  set team(GestureArenaTeam? value) {
398
    assert(value != null);
399 400 401 402 403 404 405
    assert(_entries.isEmpty);
    assert(_trackedPointers.isEmpty);
    assert(_team == null);
    _team = value;
  }

  GestureArenaEntry _addPointerToArena(int pointer) {
406
    if (_team != null) {
407
      return _team!.add(pointer, this);
408
    }
409
    return GestureBinding.instance.gestureArena.add(pointer, this);
410 411
  }

412 413
  /// Causes events related to the given pointer ID to be routed to this recognizer.
  ///
414 415 416 417 418
  /// The pointer events are transformed according to `transform` and then delivered
  /// to [handleEvent]. The value for the `transform` argument is usually obtained
  /// from [PointerDownEvent.transform] to transform the events from the global
  /// coordinate space into the coordinate space of the event receiver. It may be
  /// null if no transformation is necessary.
419 420
  ///
  /// Use [stopTrackingPointer] to remove the route added by this function.
421 422 423 424 425
  ///
  /// This method also adds this recognizer (or its [team] if it's non-null) to
  /// the gesture arena for the specified pointer.
  ///
  /// This is called by [OneSequenceGestureRecognizer.addAllowedPointer].
426
  @protected
427
  void startTrackingPointer(int pointer, [Matrix4? transform]) {
428
    GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
429
    _trackedPointers.add(pointer);
430
    assert(!_entries.containsValue(pointer));
431
    _entries[pointer] = _addPointerToArena(pointer);
432 433
  }

434 435 436 437 438 439
  /// Stops events related to the given pointer ID from being routed to this recognizer.
  ///
  /// If this function reduces the number of tracked pointers to zero, it will
  /// call [didStopTrackingLastPointer] synchronously.
  ///
  /// Use [startTrackingPointer] to add the routes in the first place.
440
  @protected
441
  void stopTrackingPointer(int pointer) {
442
    if (_trackedPointers.contains(pointer)) {
443
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
444
      _trackedPointers.remove(pointer);
445
      if (_trackedPointers.isEmpty) {
446
        didStopTrackingLastPointer(pointer);
447
      }
448
    }
449 450
  }

451 452
  /// Stops tracking the pointer associated with the given event if the event is
  /// a [PointerUpEvent] or a [PointerCancelEvent] event.
453
  @protected
Ian Hickson's avatar
Ian Hickson committed
454
  void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
455
    if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
456
      stopTrackingPointer(event.pointer);
457
    }
458 459 460
  }
}

461 462
/// The possible states of a [PrimaryPointerGestureRecognizer].
///
463
/// The recognizer advances from [ready] to [possible] when it starts tracking a
464 465 466 467 468 469 470 471 472
/// primary pointer. Where it advances from there depends on how the gesture is
/// resolved for that pointer:
///
///  * If the primary pointer is resolved by the gesture winning the arena, the
///    recognizer stays in the [possible] state as long as it continues to track
///    a pointer.
///  * If the primary pointer is resolved by the gesture being rejected and
///    losing the arena, the recognizer's state advances to [defunct].
///
473 474
/// Once the recognizer has stopped tracking any remaining pointers, the
/// recognizer returns to [ready].
475
enum GestureRecognizerState {
476
  /// The recognizer is ready to start recognizing a gesture.
477
  ready,
478

479
  /// The sequence of pointer events seen thus far is consistent with the
480 481
  /// gesture the recognizer is attempting to recognize but the gesture has not
  /// been accepted definitively.
482
  possible,
483

484
  /// Further pointer events cannot cause this recognizer to recognize the
485 486 487
  /// gesture until the recognizer returns to the [ready] state (typically when
  /// all the pointers the recognizer is tracking are removed from the screen).
  defunct,
488 489
}

490
/// A base class for gesture recognizers that track a single primary pointer.
491
///
492 493 494 495 496 497
/// Gestures based on this class will stop tracking the gesture if the primary
/// pointer travels beyond [preAcceptSlopTolerance] or [postAcceptSlopTolerance]
/// pixels from the original contact point of the gesture.
///
/// If the [preAcceptSlopTolerance] was breached before the gesture was accepted
/// in the gesture arena, the gesture will be rejected.
498
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
499
  /// Initializes the [deadline] field during construction of subclasses.
500
  ///
501
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
502 503
  PrimaryPointerGestureRecognizer({
    this.deadline,
504 505
    this.preAcceptSlopTolerance = kTouchSlop,
    this.postAcceptSlopTolerance = kTouchSlop,
506
    super.debugOwner,
507 508 509 510
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
511 512
    super.kind,
    super.supportedDevices,
513 514 515 516 517 518 519
  }) : assert(
         preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
         'The preAcceptSlopTolerance must be positive or null',
       ),
       assert(
         postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
         'The postAcceptSlopTolerance must be positive or null',
520
       );
521

522 523
  /// If non-null, the recognizer will call [didExceedDeadline] after this
  /// amount of time has elapsed since starting to track the primary pointer.
524 525 526
  ///
  /// The [didExceedDeadline] will not be called if the primary pointer is
  /// accepted, rejected, or all pointers are up or canceled before [deadline].
527
  final Duration? deadline;
528

529 530 531 532 533 534 535
  /// The maximum distance in logical pixels the gesture is allowed to drift
  /// from the initial touch down position before the gesture is accepted.
  ///
  /// Drifting past the allowed slop amount causes the gesture to be rejected.
  ///
  /// Can be null to indicate that the gesture can drift for any distance.
  /// Defaults to 18 logical pixels.
536
  final double? preAcceptSlopTolerance;
537 538 539 540 541 542 543 544 545

  /// The maximum distance in logical pixels the gesture is allowed to drift
  /// after the gesture has been accepted.
  ///
  /// Drifting past the allowed slop amount causes the gesture to stop tracking
  /// and signaling subsequent callbacks.
  ///
  /// Can be null to indicate that the gesture can drift for any distance.
  /// Defaults to 18 logical pixels.
546
  final double? postAcceptSlopTolerance;
547

548 549 550
  /// The current state of the recognizer.
  ///
  /// See [GestureRecognizerState] for a description of the states.
551 552
  GestureRecognizerState get state => _state;
  GestureRecognizerState _state = GestureRecognizerState.ready;
553 554

  /// The ID of the primary pointer this recognizer is tracking.
555 556 557 558 559 560 561 562 563 564
  ///
  /// If this recognizer is no longer tracking any pointers, this field holds
  /// the ID of the primary pointer this recognizer was most recently tracking.
  /// This enables the recognizer to know which pointer it was most recently
  /// tracking when [acceptGesture] or [rejectGesture] is called (which may be
  /// called after the recognizer is no longer tracking a pointer if, e.g.
  /// [GestureArenaManager.hold] has been called, or if there are other
  /// recognizers keeping the arena open).
  int? get primaryPointer => _primaryPointer;
  int? _primaryPointer;
565

566
  /// The location at which the primary pointer contacted the screen.
567 568 569 570 571
  ///
  /// This will only be non-null while this recognizer is tracking at least
  /// one pointer.
  OffsetPair? get initialPosition => _initialPosition;
  OffsetPair? _initialPosition;
572

573 574 575
  // Whether this pointer is accepted by winning the arena or as defined by
  // a subclass calling acceptGesture.
  bool _gestureAccepted = false;
576
  Timer? _timer;
577

578
  @override
579
  void addAllowedPointer(PointerDownEvent event) {
580
    super.addAllowedPointer(event);
581
    if (state == GestureRecognizerState.ready) {
582 583 584
      _state = GestureRecognizerState.possible;
      _primaryPointer = event.pointer;
      _initialPosition = OffsetPair(local: event.localPosition, global: event.position);
585
      if (deadline != null) {
586
        _timer = Timer(deadline!, () => didExceedDeadlineWithEvent(event));
587
      }
588 589 590
    }
  }

591 592 593 594 595 596 597
  @override
  void handleNonAllowedPointer(PointerDownEvent event) {
    if (!_gestureAccepted) {
      super.handleNonAllowedPointer(event);
    }
  }

598
  @override
Ian Hickson's avatar
Ian Hickson committed
599
  void handleEvent(PointerEvent event) {
600
    assert(state != GestureRecognizerState.ready);
xster's avatar
xster committed
601
    if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
602 603 604
      final bool isPreAcceptSlopPastTolerance =
          !_gestureAccepted &&
          preAcceptSlopTolerance != null &&
605
          _getGlobalDistance(event) > preAcceptSlopTolerance!;
606 607 608
      final bool isPostAcceptSlopPastTolerance =
          _gestureAccepted &&
          postAcceptSlopTolerance != null &&
609
          _getGlobalDistance(event) > postAcceptSlopTolerance!;
610 611

      if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
612
        resolve(GestureDisposition.rejected);
613
        stopTrackingPointer(primaryPointer!);
614
      } else {
615
        handlePrimaryPointer(event);
616
      }
617 618 619 620 621
    }
    stopTrackingIfPointerNoLongerDown(event);
  }

  /// Override to provide behavior for the primary pointer when the gesture is still possible.
622
  @protected
Ian Hickson's avatar
Ian Hickson committed
623
  void handlePrimaryPointer(PointerEvent event);
624

Florian Loitsch's avatar
Florian Loitsch committed
625
  /// Override to be notified when [deadline] is exceeded.
626
  ///
627
  /// You must override this method or [didExceedDeadlineWithEvent] if you
628 629
  /// supply a [deadline]. Subclasses that override this method must _not_
  /// call `super.didExceedDeadline()`.
630
  @protected
631 632 633 634
  void didExceedDeadline() {
    assert(deadline == null);
  }

635 636 637 638
  /// Same as [didExceedDeadline] but receives the [event] that initiated the
  /// gesture.
  ///
  /// You must override this method or [didExceedDeadline] if you supply a
639 640
  /// [deadline]. Subclasses that override this method must _not_ call
  /// `super.didExceedDeadlineWithEvent(event)`.
641 642 643 644 645
  @protected
  void didExceedDeadlineWithEvent(PointerDownEvent event) {
    didExceedDeadline();
  }

646 647
  @override
  void acceptGesture(int pointer) {
648 649 650 651
    if (pointer == primaryPointer) {
      _stopTimer();
      _gestureAccepted = true;
    }
652 653
  }

654 655
  @override
  void rejectGesture(int pointer) {
xster's avatar
xster committed
656
    if (pointer == primaryPointer && state == GestureRecognizerState.possible) {
657
      _stopTimer();
658
      _state = GestureRecognizerState.defunct;
659
    }
660 661
  }

662
  @override
663
  void didStopTrackingLastPointer(int pointer) {
664
    assert(state != GestureRecognizerState.ready);
665
    _stopTimer();
666 667 668
    _state = GestureRecognizerState.ready;
    _initialPosition = null;
    _gestureAccepted = false;
669 670
  }

671
  @override
672 673 674 675 676 677 678
  void dispose() {
    _stopTimer();
    super.dispose();
  }

  void _stopTimer() {
    if (_timer != null) {
679
      _timer!.cancel();
680 681 682 683
      _timer = null;
    }
  }

684
  double _getGlobalDistance(PointerEvent event) {
685
    final Offset offset = event.position - initialPosition!.global;
686 687 688
    return offset.distance;
  }

689
  @override
690 691
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
692
    properties.add(EnumProperty<GestureRecognizerState>('state', state));
693
  }
694
}
695 696 697 698 699 700

/// A container for a [local] and [global] [Offset] pair.
///
/// Usually, the [global] [Offset] is in the coordinate space of the screen
/// after conversion to logical pixels and the [local] offset is the same
/// [Offset], but transformed to a local coordinate space.
701
@immutable
702 703 704
class OffsetPair {
  /// Creates a [OffsetPair] combining a [local] and [global] [Offset].
  const OffsetPair({
705 706
    required this.local,
    required this.global,
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
  });

  /// Creates a [OffsetPair] from [PointerEvent.localPosition] and
  /// [PointerEvent.position].
  factory OffsetPair.fromEventPosition(PointerEvent event) {
    return OffsetPair(local: event.localPosition, global: event.position);
  }

  /// Creates a [OffsetPair] from [PointerEvent.localDelta] and
  /// [PointerEvent.delta].
  factory OffsetPair.fromEventDelta(PointerEvent event) {
    return OffsetPair(local: event.localDelta, global: event.delta);
  }

  /// A [OffsetPair] where both [Offset]s are [Offset.zero].
  static const OffsetPair zero = OffsetPair(local: Offset.zero, global: Offset.zero);

  /// The [Offset] in the local coordinate space.
  final Offset local;

  /// The [Offset] in the global coordinate space after conversion to logical
  /// pixels.
  final Offset global;

  /// Adds the `other.global` to [global] and `other.local` to [local].
  OffsetPair operator+(OffsetPair other) {
    return OffsetPair(
      local: local + other.local,
      global: global + other.global,
    );
  }

  /// Subtracts the `other.global` from [global] and `other.local` from [local].
  OffsetPair operator-(OffsetPair other) {
    return OffsetPair(
      local: local - other.local,
      global: global - other.global,
    );
  }

  @override
748
  String toString() => '${objectRuntimeType(this, 'OffsetPair')}(local: $local, global: $global)';
749
}