| // 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 '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, this.kind) |
| : assert(initialPosition != null), |
| _velocityTracker = VelocityTracker.withKind(kind); |
| |
| /// The global coordinates of the pointer when the pointer contacted the screen. |
| final Offset initialPosition; |
| |
| final VelocityTracker _velocityTracker; |
| |
| /// The kind of pointer performing the multi-drag gesture. |
| /// |
| /// Used by subclasses to determine the appropriate hit slop, for example. |
| final PointerDeviceKind kind; |
| |
| 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 = _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 |
| @factory |
| 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, PointerDeviceKind kind) : super(initialPosition, kind); |
| |
| @override |
| void checkForResolutionAfterMove() { |
| assert(pendingDelta != null); |
| if (pendingDelta!.distance > computeHitSlop(kind)) |
| 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, event.kind); |
| } |
| |
| @override |
| String get debugDescription => 'multidrag'; |
| } |
| |
| |
| class _HorizontalPointerState extends MultiDragPointerState { |
| _HorizontalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind); |
| |
| @override |
| void checkForResolutionAfterMove() { |
| assert(pendingDelta != null); |
| if (pendingDelta!.dx.abs() > computeHitSlop(kind)) |
| 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, event.kind); |
| } |
| |
| @override |
| String get debugDescription => 'horizontal multidrag'; |
| } |
| |
| |
| class _VerticalPointerState extends MultiDragPointerState { |
| _VerticalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind); |
| |
| @override |
| void checkForResolutionAfterMove() { |
| assert(pendingDelta != null); |
| if (pendingDelta!.dy.abs() > computeHitSlop(kind)) |
| 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, event.kind); |
| } |
| |
| @override |
| String get debugDescription => 'vertical multidrag'; |
| } |
| |
| class _DelayedPointerState extends MultiDragPointerState { |
| _DelayedPointerState(Offset initialPosition, Duration delay, PointerDeviceKind kind) |
| : assert(delay != null), |
| super(initialPosition, kind) { |
| _timer = Timer(delay, _delayPassed); |
| } |
| |
| Timer? _timer; |
| GestureMultiDragStartCallback? _starter; |
| |
| void _delayPassed() { |
| assert(_timer != null); |
| assert(pendingDelta != null); |
| assert(pendingDelta!.distance <= computeHitSlop(kind)); |
| _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 > computeHitSlop(kind)) { |
| 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, event.kind); |
| } |
| |
| @override |
| String get debugDescription => 'long multidrag'; |
| } |