| // 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:ui' show Offset; |
| |
| import 'package:flutter/foundation.dart'; |
| |
| import 'arena.dart'; |
| import 'binding.dart'; |
| import 'constants.dart'; |
| import 'drag.dart'; |
| import 'drag_details.dart'; |
| import 'events.dart'; |
| import 'recognizer.dart'; |
| import 'velocity_tracker.dart'; |
| |
| /// Signature for when [MultiDragGestureRecognizer] recognizes the start of a drag gesture. |
| typedef GestureMultiDragStartCallback = Drag Function(Offset position); |
| |
| /// Per-pointer state for a [MultiDragGestureRecognizer]. |
| /// |
| /// A [MultiDragGestureRecognizer] tracks each pointer separately. The state for |
| /// each pointer is a subclass of [MultiDragPointerState]. |
| abstract class MultiDragPointerState { |
| /// Creates per-pointer state for a [MultiDragGestureRecognizer]. |
| /// |
| /// The [initialPosition] argument must not be null. |
| MultiDragPointerState(this.initialPosition) |
| : assert(initialPosition != null); |
| |
| /// The global coordinates of the pointer when the pointer contacted the screen. |
| final Offset initialPosition; |
| |
| final VelocityTracker _velocityTracker = VelocityTracker(); |
| Drag _client; |
| |
| /// The offset of the pointer from the last position that was reported to the client. |
| /// |
| /// After the pointer contacts the screen, the pointer might move some |
| /// distance before this movement will be recognized as a drag. This field |
| /// accumulates that movement so that we can report it to the client after |
| /// the drag starts. |
| Offset get pendingDelta => _pendingDelta; |
| Offset _pendingDelta = Offset.zero; |
| |
| Duration _lastPendingEventTimestamp; |
| |
| GestureArenaEntry _arenaEntry; |
| void _setArenaEntry(GestureArenaEntry entry) { |
| assert(_arenaEntry == null); |
| assert(pendingDelta != null); |
| assert(_client == null); |
| _arenaEntry = entry; |
| } |
| |
| /// Resolve this pointer's entry in the [GestureArenaManager] with the given disposition. |
| @protected |
| @mustCallSuper |
| void resolve(GestureDisposition disposition) { |
| _arenaEntry.resolve(disposition); |
| } |
| |
| void _move(PointerMoveEvent event) { |
| assert(_arenaEntry != null); |
| if (!event.synthesized) |
| _velocityTracker.addPosition(event.timeStamp, event.position); |
| if (_client != null) { |
| assert(pendingDelta == null); |
| // Call client last to avoid reentrancy. |
| _client.update(DragUpdateDetails( |
| sourceTimeStamp: event.timeStamp, |
| delta: event.delta, |
| globalPosition: event.position, |
| )); |
| } else { |
| assert(pendingDelta != null); |
| _pendingDelta += event.delta; |
| _lastPendingEventTimestamp = event.timeStamp; |
| checkForResolutionAfterMove(); |
| } |
| } |
| |
| /// Override this to call resolve() if the drag should be accepted or rejected. |
| /// This is called when a pointer movement is received, but only if the gesture |
| /// has not yet been resolved. |
| @protected |
| void checkForResolutionAfterMove() { } |
| |
| /// Called when the gesture was accepted. |
| /// |
| /// Either immediately or at some future point before the gesture is disposed, |
| /// call starter(), passing it initialPosition, to start the drag. |
| @protected |
| void accepted(GestureMultiDragStartCallback starter); |
| |
| /// Called when the gesture was rejected. |
| /// |
| /// The [dispose] method will be called immediately following this. |
| @protected |
| @mustCallSuper |
| void rejected() { |
| assert(_arenaEntry != null); |
| assert(_client == null); |
| assert(pendingDelta != null); |
| _pendingDelta = null; |
| _lastPendingEventTimestamp = null; |
| _arenaEntry = null; |
| } |
| |
| void _startDrag(Drag client) { |
| assert(_arenaEntry != null); |
| assert(_client == null); |
| assert(client != null); |
| assert(pendingDelta != null); |
| _client = client; |
| final DragUpdateDetails details = DragUpdateDetails( |
| sourceTimeStamp: _lastPendingEventTimestamp, |
| delta: pendingDelta, |
| globalPosition: initialPosition, |
| ); |
| _pendingDelta = null; |
| _lastPendingEventTimestamp = null; |
| // Call client last to avoid reentrancy. |
| _client.update(details); |
| } |
| |
| void _up() { |
| assert(_arenaEntry != null); |
| if (_client != null) { |
| assert(pendingDelta == null); |
| final DragEndDetails details = DragEndDetails(velocity: _velocityTracker.getVelocity()); |
| final Drag client = _client; |
| _client = null; |
| // Call client last to avoid reentrancy. |
| client.end(details); |
| } else { |
| assert(pendingDelta != null); |
| _pendingDelta = null; |
| _lastPendingEventTimestamp = null; |
| } |
| } |
| |
| void _cancel() { |
| assert(_arenaEntry != null); |
| if (_client != null) { |
| assert(pendingDelta == null); |
| final Drag client = _client; |
| _client = null; |
| // Call client last to avoid reentrancy. |
| client.cancel(); |
| } else { |
| assert(pendingDelta != null); |
| _pendingDelta = null; |
| _lastPendingEventTimestamp = null; |
| } |
| } |
| |
| /// Releases any resources used by the object. |
| @protected |
| @mustCallSuper |
| void dispose() { |
| _arenaEntry?.resolve(GestureDisposition.rejected); |
| _arenaEntry = null; |
| assert(() { |
| _pendingDelta = null; |
| return true; |
| }()); |
| } |
| } |
| |
| /// Recognizes movement on a per-pointer basis. |
| /// |
| /// In contrast to [DragGestureRecognizer], [MultiDragGestureRecognizer] watches |
| /// each pointer separately, which means multiple drags can be recognized |
| /// concurrently if multiple pointers are in contact with the screen. |
| /// |
| /// [MultiDragGestureRecognizer] is not intended to be used directly. Instead, |
| /// consider using one of its subclasses to recognize specific types for drag |
| /// gestures. |
| /// |
| /// See also: |
| /// |
| /// * [ImmediateMultiDragGestureRecognizer], the most straight-forward variant |
| /// of multi-pointer drag gesture recognizer. |
| /// * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that |
| /// start horizontally. |
| /// * [VerticalMultiDragGestureRecognizer], which only recognizes drags that |
| /// start vertically. |
| /// * [DelayedMultiDragGestureRecognizer], which only recognizes drags that |
| /// start after a long-press gesture. |
| abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer { |
| /// Initialize the object. |
| MultiDragGestureRecognizer({ |
| @required Object debugOwner, |
| PointerDeviceKind kind, |
| }) : super(debugOwner: debugOwner, kind: kind); |
| |
| /// Called when this class recognizes the start of a drag gesture. |
| /// |
| /// The remaining notifications for this drag gesture are delivered to the |
| /// [Drag] object returned by this callback. |
| GestureMultiDragStartCallback onStart; |
| |
| Map<int, T> _pointers = <int, T>{}; |
| |
| @override |
| void addAllowedPointer(PointerDownEvent event) { |
| assert(_pointers != null); |
| assert(event.pointer != null); |
| assert(event.position != null); |
| assert(!_pointers.containsKey(event.pointer)); |
| final T state = createNewPointerState(event); |
| _pointers[event.pointer] = state; |
| GestureBinding.instance.pointerRouter.addRoute(event.pointer, _handleEvent); |
| state._setArenaEntry(GestureBinding.instance.gestureArena.add(event.pointer, this)); |
| } |
| |
| /// Subclasses should override this method to create per-pointer state |
| /// objects to track the pointer associated with the given event. |
| @protected |
| T createNewPointerState(PointerDownEvent event); |
| |
| void _handleEvent(PointerEvent event) { |
| assert(_pointers != null); |
| assert(event.pointer != null); |
| assert(event.timeStamp != null); |
| assert(event.position != null); |
| assert(_pointers.containsKey(event.pointer)); |
| final T state = _pointers[event.pointer]; |
| if (event is PointerMoveEvent) { |
| state._move(event); |
| // We might be disposed here. |
| } else if (event is PointerUpEvent) { |
| assert(event.delta == Offset.zero); |
| state._up(); |
| // We might be disposed here. |
| _removeState(event.pointer); |
| } else if (event is PointerCancelEvent) { |
| assert(event.delta == Offset.zero); |
| state._cancel(); |
| // We might be disposed here. |
| _removeState(event.pointer); |
| } else if (event is! PointerDownEvent) { |
| // we get the PointerDownEvent that resulted in our addPointer getting called since we |
| // add ourselves to the pointer router then (before the pointer router has heard of |
| // the event). |
| assert(false); |
| } |
| } |
| |
| @override |
| void acceptGesture(int pointer) { |
| assert(_pointers != null); |
| final T state = _pointers[pointer]; |
| if (state == null) |
| return; // We might already have canceled this drag if the up comes before the accept. |
| state.accepted((Offset initialPosition) => _startDrag(initialPosition, pointer)); |
| } |
| |
| Drag _startDrag(Offset initialPosition, int pointer) { |
| assert(_pointers != null); |
| final T state = _pointers[pointer]; |
| assert(state != null); |
| assert(state._pendingDelta != null); |
| Drag drag; |
| if (onStart != null) |
| drag = invokeCallback<Drag>('onStart', () => onStart(initialPosition)); |
| if (drag != null) { |
| state._startDrag(drag); |
| } else { |
| _removeState(pointer); |
| } |
| return drag; |
| } |
| |
| @override |
| void rejectGesture(int pointer) { |
| assert(_pointers != null); |
| if (_pointers.containsKey(pointer)) { |
| final T state = _pointers[pointer]; |
| assert(state != null); |
| state.rejected(); |
| _removeState(pointer); |
| } // else we already preemptively forgot about it (e.g. we got an up event) |
| } |
| |
| void _removeState(int pointer) { |
| if (_pointers == null) { |
| // We've already been disposed. It's harmless to skip removing the state |
| // for the given pointer because dispose() has already removed it. |
| return; |
| } |
| assert(_pointers.containsKey(pointer)); |
| GestureBinding.instance.pointerRouter.removeRoute(pointer, _handleEvent); |
| _pointers.remove(pointer).dispose(); |
| } |
| |
| @override |
| void dispose() { |
| _pointers.keys.toList().forEach(_removeState); |
| assert(_pointers.isEmpty); |
| _pointers = null; |
| super.dispose(); |
| } |
| } |
| |
| class _ImmediatePointerState extends MultiDragPointerState { |
| _ImmediatePointerState(Offset initialPosition) : super(initialPosition); |
| |
| @override |
| void checkForResolutionAfterMove() { |
| assert(pendingDelta != null); |
| if (pendingDelta.distance > kTouchSlop) |
| resolve(GestureDisposition.accepted); |
| } |
| |
| @override |
| void accepted(GestureMultiDragStartCallback starter) { |
| starter(initialPosition); |
| } |
| } |
| |
| /// Recognizes movement both horizontally and vertically on a per-pointer basis. |
| /// |
| /// In contrast to [PanGestureRecognizer], [ImmediateMultiDragGestureRecognizer] |
| /// watches each pointer separately, which means multiple drags can be |
| /// recognized concurrently if multiple pointers are in contact with the screen. |
| /// |
| /// See also: |
| /// |
| /// * [PanGestureRecognizer], which recognizes only one drag gesture at a time, |
| /// regardless of how many fingers are involved. |
| /// * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that |
| /// start horizontally. |
| /// * [VerticalMultiDragGestureRecognizer], which only recognizes drags that |
| /// start vertically. |
| /// * [DelayedMultiDragGestureRecognizer], which only recognizes drags that |
| /// start after a long-press gesture. |
| class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> { |
| /// Create a gesture recognizer for tracking multiple pointers at once. |
| ImmediateMultiDragGestureRecognizer({ |
| Object debugOwner, |
| PointerDeviceKind kind, |
| }) : super(debugOwner: debugOwner, kind: kind); |
| |
| @override |
| _ImmediatePointerState createNewPointerState(PointerDownEvent event) { |
| return _ImmediatePointerState(event.position); |
| } |
| |
| @override |
| String get debugDescription => 'multidrag'; |
| } |
| |
| |
| class _HorizontalPointerState extends MultiDragPointerState { |
| _HorizontalPointerState(Offset initialPosition) : super(initialPosition); |
| |
| @override |
| void checkForResolutionAfterMove() { |
| assert(pendingDelta != null); |
| if (pendingDelta.dx.abs() > kTouchSlop) |
| resolve(GestureDisposition.accepted); |
| } |
| |
| @override |
| void accepted(GestureMultiDragStartCallback starter) { |
| starter(initialPosition); |
| } |
| } |
| |
| /// Recognizes movement in the horizontal direction on a per-pointer basis. |
| /// |
| /// In contrast to [HorizontalDragGestureRecognizer], |
| /// [HorizontalMultiDragGestureRecognizer] watches each pointer separately, |
| /// which means multiple drags can be recognized concurrently if multiple |
| /// pointers are in contact with the screen. |
| /// |
| /// See also: |
| /// |
| /// * [HorizontalDragGestureRecognizer], a gesture recognizer that just |
| /// looks at horizontal movement. |
| /// * [ImmediateMultiDragGestureRecognizer], a similar recognizer, but without |
| /// the limitation that the drag must start horizontally. |
| /// * [VerticalMultiDragGestureRecognizer], which only recognizes drags that |
| /// start vertically. |
| class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> { |
| /// Create a gesture recognizer for tracking multiple pointers at once |
| /// but only if they first move horizontally. |
| HorizontalMultiDragGestureRecognizer({ |
| Object debugOwner, |
| PointerDeviceKind kind, |
| }) : super(debugOwner: debugOwner, kind: kind); |
| |
| @override |
| _HorizontalPointerState createNewPointerState(PointerDownEvent event) { |
| return _HorizontalPointerState(event.position); |
| } |
| |
| @override |
| String get debugDescription => 'horizontal multidrag'; |
| } |
| |
| |
| class _VerticalPointerState extends MultiDragPointerState { |
| _VerticalPointerState(Offset initialPosition) : super(initialPosition); |
| |
| @override |
| void checkForResolutionAfterMove() { |
| assert(pendingDelta != null); |
| if (pendingDelta.dy.abs() > kTouchSlop) |
| resolve(GestureDisposition.accepted); |
| } |
| |
| @override |
| void accepted(GestureMultiDragStartCallback starter) { |
| starter(initialPosition); |
| } |
| } |
| |
| /// Recognizes movement in the vertical direction on a per-pointer basis. |
| /// |
| /// In contrast to [VerticalDragGestureRecognizer], |
| /// [VerticalMultiDragGestureRecognizer] watches each pointer separately, |
| /// which means multiple drags can be recognized concurrently if multiple |
| /// pointers are in contact with the screen. |
| /// |
| /// See also: |
| /// |
| /// * [VerticalDragGestureRecognizer], a gesture recognizer that just |
| /// looks at vertical movement. |
| /// * [ImmediateMultiDragGestureRecognizer], a similar recognizer, but without |
| /// the limitation that the drag must start vertically. |
| /// * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that |
| /// start horizontally. |
| class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> { |
| /// Create a gesture recognizer for tracking multiple pointers at once |
| /// but only if they first move vertically. |
| VerticalMultiDragGestureRecognizer({ |
| Object debugOwner, |
| PointerDeviceKind kind, |
| }) : super(debugOwner: debugOwner, kind: kind); |
| |
| @override |
| _VerticalPointerState createNewPointerState(PointerDownEvent event) { |
| return _VerticalPointerState(event.position); |
| } |
| |
| @override |
| String get debugDescription => 'vertical multidrag'; |
| } |
| |
| class _DelayedPointerState extends MultiDragPointerState { |
| _DelayedPointerState(Offset initialPosition, Duration delay) |
| : assert(delay != null), |
| super(initialPosition) { |
| _timer = Timer(delay, _delayPassed); |
| } |
| |
| Timer _timer; |
| GestureMultiDragStartCallback _starter; |
| |
| void _delayPassed() { |
| assert(_timer != null); |
| assert(pendingDelta != null); |
| assert(pendingDelta.distance <= kTouchSlop); |
| _timer = null; |
| if (_starter != null) { |
| _starter(initialPosition); |
| _starter = null; |
| } else { |
| resolve(GestureDisposition.accepted); |
| } |
| assert(_starter == null); |
| } |
| |
| void _ensureTimerStopped() { |
| _timer?.cancel(); |
| _timer = null; |
| } |
| |
| @override |
| void accepted(GestureMultiDragStartCallback starter) { |
| assert(_starter == null); |
| if (_timer == null) |
| starter(initialPosition); |
| else |
| _starter = starter; |
| } |
| |
| @override |
| void checkForResolutionAfterMove() { |
| if (_timer == null) { |
| // If we've been accepted by the gesture arena but the pointer moves too |
| // much before the timer fires, we end up a state where the timer is |
| // stopped but we keep getting calls to this function because we never |
| // actually started the drag. In this case, _starter will be non-null |
| // because we're essentially waiting forever to start the drag. |
| assert(_starter != null); |
| return; |
| } |
| assert(pendingDelta != null); |
| if (pendingDelta.distance > kTouchSlop) { |
| resolve(GestureDisposition.rejected); |
| _ensureTimerStopped(); |
| } |
| } |
| |
| @override |
| void dispose() { |
| _ensureTimerStopped(); |
| super.dispose(); |
| } |
| } |
| |
| /// Recognizes movement both horizontally and vertically on a per-pointer basis |
| /// after a delay. |
| /// |
| /// In contrast to [ImmediateMultiDragGestureRecognizer], |
| /// [DelayedMultiDragGestureRecognizer] waits for a [delay] before recognizing |
| /// the drag. If the pointer moves more than [kTouchSlop] before the delay |
| /// expires, the gesture is not recognized. |
| /// |
| /// In contrast to [PanGestureRecognizer], [DelayedMultiDragGestureRecognizer] |
| /// watches each pointer separately, which means multiple drags can be |
| /// recognized concurrently if multiple pointers are in contact with the screen. |
| /// |
| /// See also: |
| /// |
| /// * [ImmediateMultiDragGestureRecognizer], a similar recognizer but without |
| /// the delay. |
| /// * [PanGestureRecognizer], which recognizes only one drag gesture at a time, |
| /// regardless of how many fingers are involved. |
| class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> { |
| /// Creates a drag recognizer that works on a per-pointer basis after a delay. |
| /// |
| /// In order for a drag to be recognized by this recognizer, the pointer must |
| /// remain in the same place for [delay] (up to [kTouchSlop]). The [delay] |
| /// defaults to [kLongPressTimeout] to match [LongPressGestureRecognizer] but |
| /// can be changed for specific behaviors. |
| DelayedMultiDragGestureRecognizer({ |
| this.delay = kLongPressTimeout, |
| Object debugOwner, |
| PointerDeviceKind kind, |
| }) : assert(delay != null), |
| super(debugOwner: debugOwner, kind: kind); |
| |
| /// The amount of time the pointer must remain in the same place for the drag |
| /// to be recognized. |
| final Duration delay; |
| |
| @override |
| _DelayedPointerState createNewPointerState(PointerDownEvent event) { |
| return _DelayedPointerState(event.position, delay); |
| } |
| |
| @override |
| String get debugDescription => 'long multidrag'; |
| } |