recognizer.dart 17.6 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: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
export 'pointer_router.dart' show PointerRouter;
20

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

27 28 29 30 31 32 33 34 35
/// 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 {
36
  /// Set the initial offset, at the position where the first down event was
37 38 39 40 41 42 43 44
  /// detected.
  down,

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

45
/// The base class that all gesture recognizers inherit from.
46 47 48 49
///
/// 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.
50 51 52 53
///
/// See also:
///
///  * [GestureDetector], the widget that is used to detect gestures.
54 55
///  * [debugPrintRecognizerCallbacksTrace], a flag that can be set to help
///    debug issues with gesture recognizers.
56
abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
57 58 59 60
  /// Initializes the gesture recognizer.
  ///
  /// The argument is optional and is only used for debug purposes (e.g. in the
  /// [toString] serialization).
61 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}
  GestureRecognizer({ this.debugOwner, PointerDeviceKind kind }) : _kind = kind;
68 69 70 71 72 73 74

  /// 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;

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

79 80
  /// Registers a new pointer that might be relevant to this gesture
  /// detector.
Florian Loitsch's avatar
Florian Loitsch committed
81
  ///
82 83 84 85 86 87 88
  /// 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
89
  /// the global gesture arena manager (see [GestureArenaManager]) to track
90
  /// that pointer.
91 92 93 94 95 96 97 98 99 100 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
  ///
  /// This method is called for each and all pointers being added. In
  /// most cases, you want to override [addAllowedPointer] instead.
  void addPointer(PointerDownEvent event) {
    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.
    return _kind == null || _kind == event.kind;
  }
128

Florian Loitsch's avatar
Florian Loitsch committed
129 130
  /// Releases any resources used by the object.
  ///
131 132
  /// This method is called by the owner of this gesture recognizer
  /// when the object is no longer needed (e.g. when a gesture
133
  /// recognizer is being unregistered from a [GestureDetector], the
134
  /// GestureDetector widget calls this method).
135
  @mustCallSuper
136 137
  void dispose() { }

138 139
  /// Returns a very short pretty description of the gesture that the
  /// recognizer looks for, like 'tap' or 'horizontal drag'.
140
  String get debugDescription;
141

142 143
  /// Invoke a callback provided by the application, catching and logging any
  /// exceptions.
144 145
  ///
  /// The `name` argument is ignored except when reporting exceptions.
146 147 148 149 150
  ///
  /// 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.
151
  @protected
152 153
  T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) {
    assert(callback != null);
154
    T result;
155
    try {
156 157 158 159 160 161 162 163 164
      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;
165
      }());
166 167
      result = callback();
    } catch (exception, stack) {
168
      FlutterError.reportError(FlutterErrorDetails(
169 170 171 172 173 174 175 176
        exception: exception,
        stack: stack,
        library: 'gesture',
        context: 'while handling a gesture',
        informationCollector: (StringBuffer information) {
          information.writeln('Handler: $name');
          information.writeln('Recognizer:');
          information.writeln('  $this');
177
        },
178 179 180 181
      ));
    }
    return result;
  }
182 183

  @override
184 185
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
186
    properties.add(DiagnosticsProperty<Object>('debugOwner', debugOwner, defaultValue: null));
187
  }
188 189
}

190 191 192 193 194 195 196 197
/// 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.
198
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
199
  /// Initialize the object.
200 201 202 203 204 205
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
  OneSequenceGestureRecognizer({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
206

207
  final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
208
  final Set<int> _trackedPointers = HashSet<int>();
209

210 211 212 213 214
  @override
  void handleNonAllowedPointer(PointerDownEvent event) {
    resolve(GestureDisposition.rejected);
  }

215 216
  /// Called when a pointer event is routed to this recognizer.
  @protected
Ian Hickson's avatar
Ian Hickson committed
217
  void handleEvent(PointerEvent event);
218 219

  @override
220
  void acceptGesture(int pointer) { }
221 222

  @override
223
  void rejectGesture(int pointer) { }
224

225
  /// Called when the number of pointers this recognizer is tracking changes from one to zero.
226 227 228
  ///
  /// The given pointer ID is the ID of the last pointer this recognizer was
  /// tracking.
229
  @protected
230
  void didStopTrackingLastPointer(int pointer);
231

232 233
  /// Resolves this recognizer's participation in each gesture arena with the
  /// given disposition.
234 235
  @protected
  @mustCallSuper
236
  void resolve(GestureDisposition disposition) {
237
    final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values);
238 239 240 241 242
    _entries.clear();
    for (GestureArenaEntry entry in localEntries)
      entry.resolve(disposition);
  }

243
  @override
244 245 246
  void dispose() {
    resolve(GestureDisposition.rejected);
    for (int pointer in _trackedPointers)
247
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
248 249
    _trackedPointers.clear();
    assert(_entries.isEmpty);
250
    super.dispose();
251 252
  }

253 254 255 256 257 258 259 260 261 262 263 264
  /// 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;
265
  /// The [team] can only be set once.
266
  set team(GestureArenaTeam value) {
267
    assert(value != null);
268 269 270 271 272 273 274 275 276 277 278 279
    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);
  }

280 281 282 283 284
  /// Causes events related to the given pointer ID to be routed to this recognizer.
  ///
  /// The pointer events are delivered to [handleEvent].
  ///
  /// Use [stopTrackingPointer] to remove the route added by this function.
285
  @protected
286
  void startTrackingPointer(int pointer) {
287
    GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
288
    _trackedPointers.add(pointer);
289
    assert(!_entries.containsValue(pointer));
290
    _entries[pointer] = _addPointerToArena(pointer);
291 292
  }

293 294 295 296 297 298
  /// 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.
299
  @protected
300
  void stopTrackingPointer(int pointer) {
301 302 303 304 305 306
    if (_trackedPointers.contains(pointer)) {
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
      _trackedPointers.remove(pointer);
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
307 308
  }

309 310
  /// Stops tracking the pointer associated with the given event if the event is
  /// a [PointerUpEvent] or a [PointerCancelEvent] event.
311
  @protected
Ian Hickson's avatar
Ian Hickson committed
312 313
  void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
    if (event is PointerUpEvent || event is PointerCancelEvent)
314 315 316 317
      stopTrackingPointer(event.pointer);
  }
}

318 319
/// The possible states of a [PrimaryPointerGestureRecognizer].
///
320 321 322 323 324
/// 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].
325
enum GestureRecognizerState {
326
  /// The recognizer is ready to start recognizing a gesture.
327
  ready,
328

329
  /// The sequence of pointer events seen thus far is consistent with the
330 331
  /// gesture the recognizer is attempting to recognize but the gesture has not
  /// been accepted definitively.
332
  possible,
333

334
  /// Further pointer events cannot cause this recognizer to recognize the
335 336 337
  /// gesture until the recognizer returns to the [ready] state (typically when
  /// all the pointers the recognizer is tracking are removed from the screen).
  defunct,
338 339
}

340
/// A base class for gesture recognizers that track a single primary pointer.
341
///
342 343 344 345 346 347
/// 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.
348
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
349
  /// Initializes the [deadline] field during construction of subclasses.
350 351
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
352 353
  PrimaryPointerGestureRecognizer({
    this.deadline,
354 355
    this.preAcceptSlopTolerance = kTouchSlop,
    this.postAcceptSlopTolerance = kTouchSlop,
356
    Object debugOwner,
357
    PointerDeviceKind kind,
358 359 360 361 362 363 364 365
  }) : assert(
         preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
         'The preAcceptSlopTolerance must be positive or null',
       ),
       assert(
         postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
         'The postAcceptSlopTolerance must be positive or null',
       ),
366
       super(debugOwner: debugOwner, kind: kind);
367

368 369
  /// If non-null, the recognizer will call [didExceedDeadline] after this
  /// amount of time has elapsed since starting to track the primary pointer.
370 371
  final Duration deadline;

372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
  /// 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;

391 392 393
  /// The current state of the recognizer.
  ///
  /// See [GestureRecognizerState] for a description of the states.
394
  GestureRecognizerState state = GestureRecognizerState.ready;
395 396

  /// The ID of the primary pointer this recognizer is tracking.
397
  int primaryPointer;
398 399

  /// The global location at which the primary pointer contacted the screen.
400
  Offset initialPosition;
401

402 403 404
  // Whether this pointer is accepted by winning the arena or as defined by
  // a subclass calling acceptGesture.
  bool _gestureAccepted = false;
405 406
  Timer _timer;

407
  @override
408
  void addAllowedPointer(PointerDownEvent event) {
409 410 411 412
    startTrackingPointer(event.pointer);
    if (state == GestureRecognizerState.ready) {
      state = GestureRecognizerState.possible;
      primaryPointer = event.pointer;
Hixie's avatar
Hixie committed
413
      initialPosition = event.position;
414
      if (deadline != null)
415
        _timer = Timer(deadline, didExceedDeadline);
416 417 418
    }
  }

419
  @override
Ian Hickson's avatar
Ian Hickson committed
420
  void handleEvent(PointerEvent event) {
421
    assert(state != GestureRecognizerState.ready);
xster's avatar
xster committed
422
    if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
423 424 425 426 427 428 429 430 431 432
      final bool isPreAcceptSlopPastTolerance =
          !_gestureAccepted &&
          preAcceptSlopTolerance != null &&
          _getDistance(event) > preAcceptSlopTolerance;
      final bool isPostAcceptSlopPastTolerance =
          _gestureAccepted &&
          postAcceptSlopTolerance != null &&
          _getDistance(event) > postAcceptSlopTolerance;

      if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
433
        resolve(GestureDisposition.rejected);
Ian Hickson's avatar
Ian Hickson committed
434
        stopTrackingPointer(primaryPointer);
435
      } else {
436
        handlePrimaryPointer(event);
437
      }
438 439 440 441 442
    }
    stopTrackingIfPointerNoLongerDown(event);
  }

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

Florian Loitsch's avatar
Florian Loitsch committed
446
  /// Override to be notified when [deadline] is exceeded.
447
  ///
448
  /// You must override this method if you supply a [deadline].
449
  @protected
450 451 452 453
  void didExceedDeadline() {
    assert(deadline == null);
  }

454 455 456 457 458
  @override
  void acceptGesture(int pointer) {
    _gestureAccepted = true;
  }

459 460
  @override
  void rejectGesture(int pointer) {
xster's avatar
xster committed
461
    if (pointer == primaryPointer && state == GestureRecognizerState.possible) {
462
      _stopTimer();
463
      state = GestureRecognizerState.defunct;
464
    }
465 466
  }

467
  @override
468
  void didStopTrackingLastPointer(int pointer) {
469
    assert(state != GestureRecognizerState.ready);
470
    _stopTimer();
471 472 473
    state = GestureRecognizerState.ready;
  }

474
  @override
475 476 477 478 479 480 481 482 483 484 485 486
  void dispose() {
    _stopTimer();
    super.dispose();
  }

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

Ian Hickson's avatar
Ian Hickson committed
487
  double _getDistance(PointerEvent event) {
488
    final Offset offset = event.position - initialPosition;
489 490 491
    return offset.distance;
  }

492
  @override
493 494
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
495
    properties.add(EnumProperty<GestureRecognizerState>('state', state));
496
  }
497
}