// 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'; import 'dart:collection'; import 'dart:ui' show Point, Offset; import 'package:meta/meta.dart'; import 'arena.dart'; import 'binding.dart'; import 'constants.dart'; import 'events.dart'; import 'pointer_router.dart'; export 'pointer_router.dart' show PointerRouter; /// The base class that all GestureRecognizers should 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. abstract class GestureRecognizer extends GestureArenaMember { /// 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. void addPointer(PointerDownEvent event); /// 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 /// recogniser 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 toStringShort() => toString(); } /// 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 { final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{}; final Set<int> _trackedPointers = new HashSet<int>(); /// Called when a pointer event is routed to this recongizer. 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. void didStopTrackingLastPointer(int pointer); /// Resolves this recognizer's participation in each gesture arena with the given disposition. void resolve(GestureDisposition disposition) { List<GestureArenaEntry> localEntries = new List<GestureArenaEntry>.from(_entries.values); _entries.clear(); for (GestureArenaEntry entry in localEntries) entry.resolve(disposition); } @override void dispose() { resolve(GestureDisposition.rejected); for (int pointer in _trackedPointers) GestureBinding.instance.pointerRouter.removeRoute(pointer, handleEvent); _trackedPointers.clear(); assert(_entries.isEmpty); super.dispose(); } /// 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. void startTrackingPointer(int pointer) { GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent); _trackedPointers.add(pointer); assert(!_entries.containsValue(pointer)); _entries[pointer] = GestureBinding.instance.gestureArena.add(pointer, this); } /// 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. 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. 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 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]. 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 recognise 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. abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer { /// Initializes the [deadline] field during construction of subclasses. PrimaryPointerGestureRecognizer({ this.deadline }); /// If non-null, the recognizer will call [didExceedDeadline] after this /// amount of time has elapsed since starting to track the primary pointer. final Duration deadline; /// 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 global location at which the primary pointer contacted the screen. Point initialPosition; Timer _timer; @override void addPointer(PointerDownEvent event) { startTrackingPointer(event.pointer); if (state == GestureRecognizerState.ready) { state = GestureRecognizerState.possible; primaryPointer = event.pointer; initialPosition = event.position; if (deadline != null) _timer = new Timer(deadline, didExceedDeadline); } } @override void handleEvent(PointerEvent event) { assert(state != GestureRecognizerState.ready); if (state == GestureRecognizerState.possible && event.pointer == primaryPointer) { // TODO(abarth): Maybe factor the slop handling out into a separate class? if (event is PointerMoveEvent && _getDistance(event) > kTouchSlop) { resolve(GestureDisposition.rejected); stopTrackingPointer(primaryPointer); } else { handlePrimaryPointer(event); } } stopTrackingIfPointerNoLongerDown(event); } /// Override to provide behavior for the primary pointer when the gesture is still possible. void handlePrimaryPointer(PointerEvent event); /// Override to be notified when [deadline] is exceeded. /// /// You must override this method if you supply a [deadline]. void didExceedDeadline() { assert(deadline == null); } @override void rejectGesture(int pointer) { if (pointer == primaryPointer) { _stopTimer(); state = GestureRecognizerState.defunct; } } @override void didStopTrackingLastPointer(int pointer) { _stopTimer(); state = GestureRecognizerState.ready; } @override void dispose() { _stopTimer(); super.dispose(); } void _stopTimer() { if (_timer != null) { _timer.cancel(); _timer = null; } } double _getDistance(PointerEvent event) { Offset offset = event.position - initialPosition; return offset.distance; } }