recognizer.dart 13 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 26
typedef T RecognizerCallback<T>();

27
/// The base class that all gesture recognizers inherit from.
28 29 30 31
///
/// 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.
32 33 34 35
///
/// See also:
///
///  * [GestureDetector], the widget that is used to detect gestures.
36 37
///  * [debugPrintRecognizerCallbacksTrace], a flag that can be set to help
///    debug issues with gesture recognizers.
38
abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin {
39 40 41 42 43 44 45 46 47 48 49 50
  /// Initializes the gesture recognizer.
  ///
  /// The argument is optional and is only used for debug purposes (e.g. in the
  /// [toString] serialization).
  GestureRecognizer({ this.debugOwner });

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

51 52
  /// Registers a new pointer that might be relevant to this gesture
  /// detector.
Florian Loitsch's avatar
Florian Loitsch committed
53
  ///
54 55 56 57 58 59 60
  /// 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
61
  /// the global gesture arena manager (see [GestureArenaManager]) to track
62
  /// that pointer.
Ian Hickson's avatar
Ian Hickson committed
63
  void addPointer(PointerDownEvent event);
64

Florian Loitsch's avatar
Florian Loitsch committed
65 66
  /// Releases any resources used by the object.
  ///
67 68
  /// This method is called by the owner of this gesture recognizer
  /// when the object is no longer needed (e.g. when a gesture
69
  /// recognizer is being unregistered from a [GestureDetector], the
70
  /// GestureDetector widget calls this method).
71
  @mustCallSuper
72 73
  void dispose() { }

74 75
  /// Returns a very short pretty description of the gesture that the
  /// recognizer looks for, like 'tap' or 'horizontal drag'.
76
  String get debugDescription;
77

78 79
  /// Invoke a callback provided by the application, catching and logging any
  /// exceptions.
80 81
  ///
  /// The `name` argument is ignored except when reporting exceptions.
82 83 84 85 86
  ///
  /// 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.
87
  @protected
88 89
  T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) {
    assert(callback != null);
90
    T result;
91
    try {
92 93 94 95 96 97 98 99 100
      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;
101
      }());
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
      result = callback();
    } catch (exception, stack) {
      FlutterError.reportError(new FlutterErrorDetails(
        exception: exception,
        stack: stack,
        library: 'gesture',
        context: 'while handling a gesture',
        informationCollector: (StringBuffer information) {
          information.writeln('Handler: $name');
          information.writeln('Recognizer:');
          information.writeln('  $this');
        }
      ));
    }
    return result;
  }
118 119

  @override
120
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
121 122 123
    super.debugFillProperties(description);
    description.add(new DiagnosticsProperty<Object>('debugOwner', debugOwner, defaultValue: null));
  }
124 125
}

126 127 128 129 130 131 132 133
/// 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.
134
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
135 136 137
  /// Initialize the object.
  OneSequenceGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);

138
  final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
139
  final Set<int> _trackedPointers = new HashSet<int>();
140

141 142
  /// Called when a pointer event is routed to this recognizer.
  @protected
Ian Hickson's avatar
Ian Hickson committed
143
  void handleEvent(PointerEvent event);
144 145

  @override
146
  void acceptGesture(int pointer) { }
147 148

  @override
149
  void rejectGesture(int pointer) { }
150

151
  /// Called when the number of pointers this recognizer is tracking changes from one to zero.
152 153 154
  ///
  /// The given pointer ID is the ID of the last pointer this recognizer was
  /// tracking.
155
  @protected
156
  void didStopTrackingLastPointer(int pointer);
157

158 159
  /// Resolves this recognizer's participation in each gesture arena with the
  /// given disposition.
160 161
  @protected
  @mustCallSuper
162
  void resolve(GestureDisposition disposition) {
163
    final List<GestureArenaEntry> localEntries = new List<GestureArenaEntry>.from(_entries.values);
164 165 166 167 168
    _entries.clear();
    for (GestureArenaEntry entry in localEntries)
      entry.resolve(disposition);
  }

169
  @override
170 171 172
  void dispose() {
    resolve(GestureDisposition.rejected);
    for (int pointer in _trackedPointers)
173
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
174 175
    _trackedPointers.clear();
    assert(_entries.isEmpty);
176
    super.dispose();
177 178
  }

179 180 181 182 183 184 185 186 187 188 189 190
  /// 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;
191
  /// The [team] can only be set once.
192
  set team(GestureArenaTeam value) {
193
    assert(value != null);
194 195 196 197 198 199 200 201 202 203 204 205
    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);
  }

206 207 208 209 210
  /// 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.
211
  @protected
212
  void startTrackingPointer(int pointer) {
213
    GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
214
    _trackedPointers.add(pointer);
215
    assert(!_entries.containsValue(pointer));
216
    _entries[pointer] = _addPointerToArena(pointer);
217 218
  }

219 220 221 222 223 224
  /// 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.
225
  @protected
226
  void stopTrackingPointer(int pointer) {
227 228 229 230 231 232
    if (_trackedPointers.contains(pointer)) {
      GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent);
      _trackedPointers.remove(pointer);
      if (_trackedPointers.isEmpty)
        didStopTrackingLastPointer(pointer);
    }
233 234
  }

235 236
  /// Stops tracking the pointer associated with the given event if the event is
  /// a [PointerUpEvent] or a [PointerCancelEvent] event.
237
  @protected
Ian Hickson's avatar
Ian Hickson committed
238 239
  void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
    if (event is PointerUpEvent || event is PointerCancelEvent)
240 241 242 243
      stopTrackingPointer(event.pointer);
  }
}

244 245 246 247 248 249 250
/// The possible states of a [PrimaryPointerGestureRecognizer].
///
/// The recognizer advances from [ready] to [possible] when starts tracking a
/// primary pointer. When the primary pointer is resolve (either accepted or
/// or rejected), the recognizers advances to [defunct]. Once the recognizer
/// has stopped tracking any remaining pointers, the recognizer returns to
/// [ready].
251
enum GestureRecognizerState {
252
  /// The recognizer is ready to start recognizing a gesture.
253
  ready,
254

255
  /// The sequence of pointer events seen thus far is consistent with the
256 257
  /// gesture the recognizer is attempting to recognize but the gesture has not
  /// been accepted definitively.
258
  possible,
259

260
  /// Further pointer events cannot cause this recognizer to recognize the
261 262 263
  /// gesture until the recognizer returns to the [ready] state (typically when
  /// all the pointers the recognizer is tracking are removed from the screen).
  defunct,
264 265
}

266
/// A base class for gesture recognizers that track a single primary pointer.
267 268 269
///
/// Gestures based on this class will reject the gesture if the primary pointer
/// travels beyond [kTouchSlop] pixels from the original contact point.
270
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
271
  /// Initializes the [deadline] field during construction of subclasses.
272 273 274 275
  PrimaryPointerGestureRecognizer({
    this.deadline,
    Object debugOwner,
  }) : super(debugOwner: debugOwner);
276

277 278
  /// If non-null, the recognizer will call [didExceedDeadline] after this
  /// amount of time has elapsed since starting to track the primary pointer.
279 280
  final Duration deadline;

281 282 283
  /// The current state of the recognizer.
  ///
  /// See [GestureRecognizerState] for a description of the states.
284
  GestureRecognizerState state = GestureRecognizerState.ready;
285 286

  /// The ID of the primary pointer this recognizer is tracking.
287
  int primaryPointer;
288 289

  /// The global location at which the primary pointer contacted the screen.
290
  Offset initialPosition;
291

292 293
  Timer _timer;

294
  @override
Ian Hickson's avatar
Ian Hickson committed
295
  void addPointer(PointerDownEvent event) {
296 297 298 299
    startTrackingPointer(event.pointer);
    if (state == GestureRecognizerState.ready) {
      state = GestureRecognizerState.possible;
      primaryPointer = event.pointer;
Hixie's avatar
Hixie committed
300
      initialPosition = event.position;
301 302 303 304 305
      if (deadline != null)
        _timer = new Timer(deadline, didExceedDeadline);
    }
  }

306
  @override
Ian Hickson's avatar
Ian Hickson committed
307
  void handleEvent(PointerEvent event) {
308 309 310
    assert(state != GestureRecognizerState.ready);
    if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) {
      // TODO(abarth): Maybe factor the slop handling out into a separate class?
Ian Hickson's avatar
Ian Hickson committed
311
      if (event is PointerMoveEvent && _getDistance(event) > kTouchSlop) {
312
        resolve(GestureDisposition.rejected);
Ian Hickson's avatar
Ian Hickson committed
313
        stopTrackingPointer(primaryPointer);
314
      } else {
315
        handlePrimaryPointer(event);
316
      }
317 318 319 320 321
    }
    stopTrackingIfPointerNoLongerDown(event);
  }

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

Florian Loitsch's avatar
Florian Loitsch committed
325
  /// Override to be notified when [deadline] is exceeded.
326
  ///
327
  /// You must override this method if you supply a [deadline].
328
  @protected
329 330 331 332
  void didExceedDeadline() {
    assert(deadline == null);
  }

333
  @override
334
  void rejectGesture(int pointer) {
335
    if (pointer == primaryPointer && state == GestureRecognizerState.possible) {
336
      _stopTimer();
337
      state = GestureRecognizerState.defunct;
338
    }
339 340
  }

341
  @override
342
  void didStopTrackingLastPointer(int pointer) {
343
    assert(state != GestureRecognizerState.ready);
344
    _stopTimer();
345 346 347
    state = GestureRecognizerState.ready;
  }

348
  @override
349 350 351 352 353 354 355 356 357 358 359 360
  void dispose() {
    _stopTimer();
    super.dispose();
  }

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

Ian Hickson's avatar
Ian Hickson committed
361
  double _getDistance(PointerEvent event) {
362
    final Offset offset = event.position - initialPosition;
363 364 365
    return offset.distance;
  }

366
  @override
367
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
368 369 370
    super.debugFillProperties(description);
    description.add(new EnumProperty<GestureRecognizerState>('state', state));
  }
371
}