// Copyright 2014 The Flutter 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'; import 'dart:collection'; import 'dart:ui' show Offset; import 'package:vector_math/vector_math_64.dart'; import 'package:flutter/foundation.dart'; import 'arena.dart'; import 'binding.dart'; import 'constants.dart'; import 'debug.dart'; import 'events.dart'; import 'pointer_router.dart'; import 'team.dart'; export 'pointer_router.dart' show PointerRouter; /// 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. typedef RecognizerCallback<T> = T Function(); /// 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 { /// Set the initial offset, at the position where the first down event was /// detected. down, /// Set the initial position at the position where the drag start event was /// detected. start, } /// The base class that all gesture recognizers inherit from. /// /// 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. /// /// See also: /// /// * [GestureDetector], the widget that is used to detect gestures. /// * [debugPrintRecognizerCallbacksTrace], a flag that can be set to help /// debug issues with gesture recognizers. abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableTreeMixin { /// Initializes the gesture recognizer. /// /// The argument is optional and is only used for debug purposes (e.g. in the /// [toString] serialization). /// /// {@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 }) : _kindFilter = kind; /// 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; /// The kind of device that's allowed to be recognized. If null, events from /// all device kinds will be tracked and recognized. 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>{}; /// Registers a new pointer that might be relevant to this gesture /// detector. /// /// 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 /// 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 [addAllowedPointer] instead. void addPointer(PointerDownEvent event) { _pointerToKind[event.pointer] = event.kind; 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 _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]; } /// Releases any resources used by the object. /// /// This method is called by the owner of this gesture recognizer /// when the object is no longer needed (e.g. when a gesture /// recognizer is being unregistered from a [GestureDetector], the /// GestureDetector widget calls this method). @mustCallSuper void dispose() { } /// Returns a very short pretty description of the gesture that the /// recognizer looks for, like 'tap' or 'horizontal drag'. String get debugDescription; /// Invoke a callback provided by the application, catching and logging any /// exceptions. /// /// The `name` argument is ignored except when reporting exceptions. /// /// 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. @protected T invokeCallback<T>(String name, RecognizerCallback<T> callback, { String debugReport() }) { assert(callback != null); T result; try { 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; }()); result = callback(); } catch (exception, stack) { InformationCollector collector; assert(() { collector = () sync* { yield StringProperty('Handler', name); yield DiagnosticsProperty<GestureRecognizer>('Recognizer', this, style: DiagnosticsTreeStyle.errorProperty); }; return true; }()); FlutterError.reportError(FlutterErrorDetails( exception: exception, stack: stack, library: 'gesture', context: ErrorDescription('while handling a gesture'), informationCollector: collector )); } return result; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty<Object>('debugOwner', debugOwner, defaultValue: null)); } } /// 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. abstract class OneSequenceGestureRecognizer extends GestureRecognizer { /// Initialize the object. /// /// {@macro flutter.gestures.gestureRecognizer.kind} OneSequenceGestureRecognizer({ Object debugOwner, PointerDeviceKind kind, }) : super(debugOwner: debugOwner, kind: kind); final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{}; final Set<int> _trackedPointers = HashSet<int>(); @override void handleNonAllowedPointer(PointerDownEvent event) { resolve(GestureDisposition.rejected); } /// Called when a pointer event is routed to this recognizer. @protected void handleEvent(PointerEvent event); @override void acceptGesture(int pointer) { } @override void rejectGesture(int pointer) { } /// Called when the number of pointers this recognizer is tracking changes from one to zero. /// /// The given pointer ID is the ID of the last pointer this recognizer was /// tracking. @protected void didStopTrackingLastPointer(int pointer); /// Resolves this recognizer's participation in each gesture arena with the /// given disposition. @protected @mustCallSuper void resolve(GestureDisposition disposition) { final List<GestureArenaEntry> localEntries = List<GestureArenaEntry>.from(_entries.values); _entries.clear(); for (final GestureArenaEntry entry in localEntries) entry.resolve(disposition); } /// 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); } } @override void dispose() { resolve(GestureDisposition.rejected); for (final int pointer in _trackedPointers) GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent); _trackedPointers.clear(); assert(_entries.isEmpty); super.dispose(); } /// 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; /// The [team] can only be set once. set team(GestureArenaTeam value) { assert(value != null); 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); } /// Causes events related to the given pointer ID to be routed to this recognizer. /// /// 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. /// /// Use [stopTrackingPointer] to remove the route added by this function. @protected void startTrackingPointer(int pointer, [Matrix4 transform]) { GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform); _trackedPointers.add(pointer); assert(!_entries.containsValue(pointer)); _entries[pointer] = _addPointerToArena(pointer); } /// 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. @protected void stopTrackingPointer(int pointer) { if (_trackedPointers.contains(pointer)) { GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent); _trackedPointers.remove(pointer); if (_trackedPointers.isEmpty) didStopTrackingLastPointer(pointer); } } /// Stops tracking the pointer associated with the given event if the event is /// a [PointerUpEvent] or a [PointerCancelEvent] event. @protected void stopTrackingIfPointerNoLongerDown(PointerEvent event) { if (event is PointerUpEvent || event is PointerCancelEvent) stopTrackingPointer(event.pointer); } } /// The possible states of a [PrimaryPointerGestureRecognizer]. /// /// 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]. enum GestureRecognizerState { /// The recognizer is ready to start recognizing a gesture. ready, /// The sequence of pointer events seen thus far is consistent with the /// gesture the recognizer is attempting to recognize but the gesture has not /// been accepted definitively. possible, /// Further pointer events cannot cause this recognizer to recognize the /// gesture until the recognizer returns to the [ready] state (typically when /// all the pointers the recognizer is tracking are removed from the screen). defunct, } /// A base class for gesture recognizers that track a single primary pointer. /// /// 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. abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer { /// Initializes the [deadline] field during construction of subclasses. /// /// {@macro flutter.gestures.gestureRecognizer.kind} PrimaryPointerGestureRecognizer({ this.deadline, this.preAcceptSlopTolerance = kTouchSlop, this.postAcceptSlopTolerance = kTouchSlop, Object debugOwner, PointerDeviceKind kind, }) : assert( preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0, 'The preAcceptSlopTolerance must be positive or null', ), assert( postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0, 'The postAcceptSlopTolerance must be positive or null', ), super(debugOwner: debugOwner, kind: kind); /// If non-null, the recognizer will call [didExceedDeadline] after this /// amount of time has elapsed since starting to track the primary pointer. /// /// The [didExceedDeadline] will not be called if the primary pointer is /// accepted, rejected, or all pointers are up or canceled before [deadline]. final Duration deadline; /// 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; /// The current state of the recognizer. /// /// See [GestureRecognizerState] for a description of the states. GestureRecognizerState state = GestureRecognizerState.ready; /// The ID of the primary pointer this recognizer is tracking. int primaryPointer; /// The location at which the primary pointer contacted the screen. OffsetPair initialPosition; // Whether this pointer is accepted by winning the arena or as defined by // a subclass calling acceptGesture. bool _gestureAccepted = false; Timer _timer; @override void addAllowedPointer(PointerDownEvent event) { startTrackingPointer(event.pointer, event.transform); if (state == GestureRecognizerState.ready) { state = GestureRecognizerState.possible; primaryPointer = event.pointer; initialPosition = OffsetPair(local: event.localPosition, global: event.position); if (deadline != null) _timer = Timer(deadline, () => didExceedDeadlineWithEvent(event)); } } @override void handleEvent(PointerEvent event) { assert(state != GestureRecognizerState.ready); if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) { final bool isPreAcceptSlopPastTolerance = !_gestureAccepted && preAcceptSlopTolerance != null && _getGlobalDistance(event) > preAcceptSlopTolerance; final bool isPostAcceptSlopPastTolerance = _gestureAccepted && postAcceptSlopTolerance != null && _getGlobalDistance(event) > postAcceptSlopTolerance; if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) { resolve(GestureDisposition.rejected); stopTrackingPointer(primaryPointer); } else { handlePrimaryPointer(event); } } stopTrackingIfPointerNoLongerDown(event); } /// Override to provide behavior for the primary pointer when the gesture is still possible. @protected void handlePrimaryPointer(PointerEvent event); /// Override to be notified when [deadline] is exceeded. /// /// You must override this method or [didExceedDeadlineWithEvent] if you /// supply a [deadline]. @protected void didExceedDeadline() { assert(deadline == null); } /// 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(); } @override void acceptGesture(int pointer) { if (pointer == primaryPointer) { _stopTimer(); _gestureAccepted = true; } } @override void rejectGesture(int pointer) { if (pointer == primaryPointer && state == GestureRecognizerState.possible) { _stopTimer(); state = GestureRecognizerState.defunct; } } @override void didStopTrackingLastPointer(int pointer) { assert(state != GestureRecognizerState.ready); _stopTimer(); state = GestureRecognizerState.ready; } @override void dispose() { _stopTimer(); super.dispose(); } void _stopTimer() { if (_timer != null) { _timer.cancel(); _timer = null; } } double _getGlobalDistance(PointerEvent event) { final Offset offset = event.position - initialPosition.global; return offset.distance; } @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(EnumProperty<GestureRecognizerState>('state', state)); } } /// 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() => '${objectRuntimeType(this, 'OffsetPair')}(local: $local, global: $global)'; }