recognizer.dart 21.1 KB
Newer Older
1 2 3 4 5
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:collection';
7
import 'dart:ui' show Offset;
8

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

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

20
export 'pointer_router.dart' show PointerRouter;
21

22 23 24 25
/// 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.
26
typedef RecognizerCallback<T> = T Function();
27

28 29 30 31 32 33 34 35 36
/// Configuration of offset passed to [DragStartDetails].
///
/// The settings determines when a drag formally starts when the user
/// initiates a drag.
///
/// See also:
///
///   * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
enum DragStartBehavior {
37
  /// Set the initial offset, at the position where the first down event was
38 39 40 41 42 43 44 45
  /// detected.
  down,

  /// Set the initial position at the position where the drag start event was
  /// detected.
  start,
}

46
/// The base class that all gesture recognizers inherit from.
47 48 49 50
///
/// 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.
51 52 53 54
///
/// See also:
///
///  * [GestureDetector], the widget that is used to detect gestures.
55 56
///  * [debugPrintRecognizerCallbacksTrace], a flag that can be set to help
///    debug issues with gesture recognizers.
57
abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
58 59 60 61
  /// Initializes the gesture recognizer.
  ///
  /// The argument is optional and is only used for debug purposes (e.g. in the
  /// [toString] serialization).
62 63 64 65 66 67
  ///
  /// {@template flutter.gestures.gestureRecognizer.kind}
  /// It's possible to limit this recognizer to a specific [PointerDeviceKind]
  /// by providing the optional [kind] argument. If [kind] is null,
  /// the recognizer will accept pointer events from all device kinds.
  /// {@endtemplate}
68
  GestureRecognizer({ this.debugOwner, PointerDeviceKind kind }) : _kindFilter = kind;
69 70 71 72 73 74 75

  /// 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.
  final Object debugOwner;

76 77
  /// The kind of device that's allowed to be recognized. If null, events from
  /// all device kinds will be tracked and recognized.
78 79 80 81 82
  final PointerDeviceKind _kindFilter;

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

84 85
  /// Registers a new pointer that might be relevant to this gesture
  /// detector.
Florian Loitsch's avatar
Florian Loitsch committed
86
  ///
87 88 89 90 91 92 93
  /// 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
94
  /// the global gesture arena manager (see [GestureArenaManager]) to track
95
  /// that pointer.
96 97 98 99
  ///
  /// This method is called for each and all pointers being added. In
  /// most cases, you want to override [addAllowedPointer] instead.
  void addPointer(PointerDownEvent event) {
100
    _pointerToKind[event.pointer] = event.kind;
101 102 103 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
    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.
132 133 134 135 136 137 138 139 140 141 142
    return _kindFilter == null || _kindFilter == event.kind;
  }

  /// 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));
    return _pointerToKind[pointer];
143
  }
144

Florian Loitsch's avatar
Florian Loitsch committed
145 146
  /// Releases any resources used by the object.
  ///
147 148
  /// This method is called by the owner of this gesture recognizer
  /// when the object is no longer needed (e.g. when a gesture
149
  /// recognizer is being unregistered from a [GestureDetector], the
150
  /// GestureDetector widget calls this method).
151
  @mustCallSuper
152 153
  void dispose() { }

154 155
  /// Returns a very short pretty description of the gesture that the
  /// recognizer looks for, like 'tap' or 'horizontal drag'.
156
  String get debugDescription;
157

158 159
  /// Invoke a callback provided by the application, catching and logging any
  /// exceptions.
160 161
  ///
  /// The `name` argument is ignored except when reporting exceptions.
162 163 164 165 166
  ///
  /// 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.
167
  @protected
168 169
  T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) {
    assert(callback != null);
170
    T result;
171
    try {
172 173 174 175 176 177 178 179 180
      assert(() {
        if (debugPrintRecognizerCallbacksTrace) {
          final String report = debugReport != null ? debugReport() : null;
          // The 19 in the line below is the width of the prefix used by
          // _debugLogDiagnostic in arena.dart.
          final String prefix = debugPrintGestureArenaDiagnostics ? ' ' * 19 + '❙ ' : '';
          debugPrint('$prefix$this calling $name callback.${ report?.isNotEmpty == true ? " $report" : "" }');
        }
        return true;
181
      }());
182 183
      result = callback();
    } catch (exception, stack) {
184
      FlutterError.reportError(FlutterErrorDetails(
185 186 187
        exception: exception,
        stack: stack,
        library: 'gesture',
188 189 190 191
        context: ErrorDescription('while handling a gesture'),
        informationCollector: () sync* {
          yield StringProperty('Handler', name);
          yield DiagnosticsProperty<GestureRecognizer>('Recognizer', this, style: DiagnosticsTreeStyle.errorProperty);
192
        },
193 194 195 196
      ));
    }
    return result;
  }
197 198

  @override
199 200
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
201
    properties.add(DiagnosticsProperty<Object>('debugOwner', debugOwner, defaultValue: null));
202
  }
203 204
}

205 206 207 208 209 210 211 212
/// 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.
213
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
214
  /// Initialize the object.
215 216 217 218 219 220
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
  OneSequenceGestureRecognizer({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
221

222
  final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
223
  final Set<int> _trackedPointers = HashSet<int>();
224

225 226 227 228 229
  @override
  void handleNonAllowedPointer(PointerDownEvent event) {
    resolve(GestureDisposition.rejected);
  }

230 231
  /// Called when a pointer event is routed to this recognizer.
  @protected
Ian Hickson's avatar
Ian Hickson committed
232
  void handleEvent(PointerEvent event);
233 234

  @override
235
  void acceptGesture(int pointer) { }
236 237

  @override
238
  void rejectGesture(int pointer) { }
239

240
  /// Called when the number of pointers this recognizer is tracking changes from one to zero.
241 242 243
  ///
  /// The given pointer ID is the ID of the last pointer this recognizer was
  /// tracking.
244
  @protected
245
  void didStopTrackingLastPointer(int pointer);
246

247 248
  /// Resolves this recognizer's participation in each gesture arena with the
  /// given disposition.
249 250
  @protected
  @mustCallSuper
251
  void resolve(GestureDisposition disposition) {
252
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
253 254 255 256 257
    _entries.clear();
    for (GestureArenaEntry entry in localEntries)
      entry.resolve(disposition);
  }

258 259 260 261 262 263 264 265 266 267 268 269
  /// Resolves this recognizer's participation in the given gesture arena with
  /// the given disposition.
  @protected
  @mustCallSuper
  void resolvePointer(int pointer, GestureDisposition disposition) {
    final GestureArenaEntry entry = _entries[pointer];
    if (entry != null) {
      entry.resolve(disposition);
      _entries.remove(pointer);
    }
  }

270
  @override
271 272 273
  void dispose() {
    resolve(GestureDisposition.rejected);
    for (int pointer in _trackedPointers)
274
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
275 276
    _trackedPointers.clear();
    assert(_entries.isEmpty);
277
    super.dispose();
278 279
  }

280 281 282 283 284 285 286 287 288 289 290 291
  /// 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.
  GestureArenaTeam get team => _team;
  GestureArenaTeam _team;
292
  /// The [team] can only be set once.
293
  set team(GestureArenaTeam value) {
294
    assert(value != null);
295 296 297 298 299 300 301 302 303 304 305 306
    assert(_entries.isEmpty);
    assert(_trackedPointers.isEmpty);
    assert(_team == null);
    _team = value;
  }

  GestureArenaEntry _addPointerToArena(int pointer) {
    if (_team != null)
      return _team.add(pointer, this);
    return GestureBinding.instance.gestureArena.add(pointer, this);
  }

307 308
  /// Causes events related to the given pointer ID to be routed to this recognizer.
  ///
309 310 311 312 313
  /// 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.
314 315
  ///
  /// Use [stopTrackingPointer] to remove the route added by this function.
316
  @protected
317 318
  void startTrackingPointer(int pointer, [Matrix4 transform]) {
    GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
319
    _trackedPointers.add(pointer);
320
    assert(!_entries.containsValue(pointer));
321
    _entries[pointer] = _addPointerToArena(pointer);
322 323
  }

324 325 326 327 328 329
  /// 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.
330
  @protected
331
  void stopTrackingPointer(int pointer) {
332 333 334 335 336 337
    if (_trackedPointers.contains(pointer)) {
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
      _trackedPointers.remove(pointer);
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
338 339
  }

340 341
  /// Stops tracking the pointer associated with the given event if the event is
  /// a [PointerUpEvent] or a [PointerCancelEvent] event.
342
  @protected
Ian Hickson's avatar
Ian Hickson committed
343 344
  void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
    if (event is PointerUpEvent || event is PointerCancelEvent)
345 346 347 348
      stopTrackingPointer(event.pointer);
  }
}

349 350
/// The possible states of a [PrimaryPointerGestureRecognizer].
///
351 352 353 354 355
/// The recognizer advances from [ready] to [possible] when it starts tracking a
/// primary pointer. When the primary pointer is resolved in the gesture
/// arena (either accepted or rejected), the recognizers advances to [defunct].
/// Once the recognizer has stopped tracking any remaining pointers, the
/// recognizer returns to [ready].
356
enum GestureRecognizerState {
357
  /// The recognizer is ready to start recognizing a gesture.
358
  ready,
359

360
  /// The sequence of pointer events seen thus far is consistent with the
361 362
  /// gesture the recognizer is attempting to recognize but the gesture has not
  /// been accepted definitively.
363
  possible,
364

365
  /// Further pointer events cannot cause this recognizer to recognize the
366 367 368
  /// gesture until the recognizer returns to the [ready] state (typically when
  /// all the pointers the recognizer is tracking are removed from the screen).
  defunct,
369 370
}

371
/// A base class for gesture recognizers that track a single primary pointer.
372
///
373 374 375 376 377 378
/// 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.
379
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
380
  /// Initializes the [deadline] field during construction of subclasses.
381 382
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
383 384
  PrimaryPointerGestureRecognizer({
    this.deadline,
385 386
    this.preAcceptSlopTolerance = kTouchSlop,
    this.postAcceptSlopTolerance = kTouchSlop,
387
    Object debugOwner,
388
    PointerDeviceKind kind,
389 390 391 392 393 394 395 396
  }) : assert(
         preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
         'The preAcceptSlopTolerance must be positive or null',
       ),
       assert(
         postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
         'The postAcceptSlopTolerance must be positive or null',
       ),
397
       super(debugOwner: debugOwner, kind: kind);
398

399 400
  /// If non-null, the recognizer will call [didExceedDeadline] after this
  /// amount of time has elapsed since starting to track the primary pointer.
401 402
  final Duration deadline;

403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
  /// 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.
  final double preAcceptSlopTolerance;

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

422 423 424
  /// The current state of the recognizer.
  ///
  /// See [GestureRecognizerState] for a description of the states.
425
  GestureRecognizerState state = GestureRecognizerState.ready;
426 427

  /// The ID of the primary pointer this recognizer is tracking.
428
  int primaryPointer;
429

430 431
  /// The location at which the primary pointer contacted the screen.
  OffsetPair initialPosition;
432

433 434 435
  // Whether this pointer is accepted by winning the arena or as defined by
  // a subclass calling acceptGesture.
  bool _gestureAccepted = false;
436 437
  Timer _timer;

438
  @override
439
  void addAllowedPointer(PointerDownEvent event) {
440
    startTrackingPointer(event.pointer, event.transform);
441 442 443
    if (state == GestureRecognizerState.ready) {
      state = GestureRecognizerState.possible;
      primaryPointer = event.pointer;
444
      initialPosition = OffsetPair(local: event.localPosition, global: event.position);
445
      if (deadline != null)
446
        _timer = Timer(deadline, () => didExceedDeadlineWithEvent(event));
447 448 449
    }
  }

450
  @override
Ian Hickson's avatar
Ian Hickson committed
451
  void handleEvent(PointerEvent event) {
452
    assert(state != GestureRecognizerState.ready);
xster's avatar
xster committed
453
    if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
454 455 456
      final bool isPreAcceptSlopPastTolerance =
          !_gestureAccepted &&
          preAcceptSlopTolerance != null &&
457
          _getGlobalDistance(event) > preAcceptSlopTolerance;
458 459 460
      final bool isPostAcceptSlopPastTolerance =
          _gestureAccepted &&
          postAcceptSlopTolerance != null &&
461
          _getGlobalDistance(event) > postAcceptSlopTolerance;
462 463

      if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
464
        resolve(GestureDisposition.rejected);
Ian Hickson's avatar
Ian Hickson committed
465
        stopTrackingPointer(primaryPointer);
466
      } else {
467
        handlePrimaryPointer(event);
468
      }
469 470 471 472 473
    }
    stopTrackingIfPointerNoLongerDown(event);
  }

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

Florian Loitsch's avatar
Florian Loitsch committed
477
  /// Override to be notified when [deadline] is exceeded.
478
  ///
479 480
  /// You must override this method or [didExceedDeadlineWithEvent] if you
  /// supply a [deadline].
481
  @protected
482 483 484 485
  void didExceedDeadline() {
    assert(deadline == null);
  }

486 487 488 489 490 491 492 493 494 495
  /// Same as [didExceedDeadline] but receives the [event] that initiated the
  /// gesture.
  ///
  /// You must override this method or [didExceedDeadline] if you supply a
  /// [deadline].
  @protected
  void didExceedDeadlineWithEvent(PointerDownEvent event) {
    didExceedDeadline();
  }

496 497 498 499 500
  @override
  void acceptGesture(int pointer) {
    _gestureAccepted = true;
  }

501 502
  @override
  void rejectGesture(int pointer) {
xster's avatar
xster committed
503
    if (pointer == primaryPointer && state == GestureRecognizerState.possible) {
504
      _stopTimer();
505
      state = GestureRecognizerState.defunct;
506
    }
507 508
  }

509
  @override
510
  void didStopTrackingLastPointer(int pointer) {
511
    assert(state != GestureRecognizerState.ready);
512
    _stopTimer();
513 514 515
    state = GestureRecognizerState.ready;
  }

516
  @override
517 518 519 520 521 522 523 524 525 526 527 528
  void dispose() {
    _stopTimer();
    super.dispose();
  }

  void _stopTimer() {
    if (_timer != null) {
      _timer.cancel();
      _timer = null;
    }
  }

529 530
  double _getGlobalDistance(PointerEvent event) {
    final Offset offset = event.position - initialPosition.global;
531 532 533
    return offset.distance;
  }

534
  @override
535 536
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
537
    properties.add(EnumProperty<GestureRecognizerState>('state', state));
538
  }
539
}
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593

/// 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.
class OffsetPair {
  /// Creates a [OffsetPair] combining a [local] and [global] [Offset].
  const OffsetPair({
    @required this.local,
    @required this.global,
  });

  /// 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
  String toString() => '$runtimeType(local: $local, global: $global)';
}