// 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 'arena.dart'; import 'constants.dart'; import 'events.dart'; import 'recognizer.dart'; import 'velocity_tracker.dart'; /// Callback signature for [LongPressGestureRecognizer.onLongPress]. /// /// Called when a pointer has remained in contact with the screen at the /// same location for a long period of time. typedef GestureLongPressCallback = void Function(); /// Callback signature for [LongPressGestureRecognizer.onLongPressUp]. /// /// Called when a pointer stops contacting the screen after a long press /// gesture was detected. typedef GestureLongPressUpCallback = void Function(); /// Callback signature for [LongPressGestureRecognizer.onLongPressStart]. /// /// Called when a pointer has remained in contact with the screen at the /// same location for a long period of time. Also reports the long press down /// position. typedef GestureLongPressStartCallback = void Function(LongPressStartDetails details); /// Callback signature for [LongPressGestureRecognizer.onLongPressMoveUpdate]. /// /// Called when a pointer is moving after being held in contact at the same /// location for a long period of time. Reports the new position and its offset /// from the original down position. typedef GestureLongPressMoveUpdateCallback = void Function(LongPressMoveUpdateDetails details); /// Callback signature for [LongPressGestureRecognizer.onLongPressEnd]. /// /// Called when a pointer stops contacting the screen after a long press /// gesture was detected. Also reports the position where the pointer stopped /// contacting the screen. typedef GestureLongPressEndCallback = void Function(LongPressEndDetails details); /// Details for callbacks that use [GestureLongPressStartCallback]. /// /// See also: /// /// * [LongPressGestureRecognizer.onLongPressStart], which uses [GestureLongPressStartCallback]. /// * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback] /// * [LongPressEndDetails], the details for [GestureLongPressEndCallback]. class LongPressStartDetails { /// Creates the details for a [GestureLongPressStartCallback]. /// /// The [globalPosition] argument must not be null. const LongPressStartDetails({ this.globalPosition = Offset.zero, Offset localPosition, }) : assert(globalPosition != null), localPosition = localPosition ?? globalPosition; /// The global position at which the pointer contacted the screen. final Offset globalPosition; /// The local position at which the pointer contacted the screen. final Offset localPosition; } /// Details for callbacks that use [GestureLongPressMoveUpdateCallback]. /// /// See also: /// /// * [LongPressGestureRecognizer.onLongPressMoveUpdate], which uses [GestureLongPressMoveUpdateCallback]. /// * [LongPressEndDetails], the details for [GestureLongPressEndCallback] /// * [LongPressStartDetails], the details for [GestureLongPressStartCallback]. class LongPressMoveUpdateDetails { /// Creates the details for a [GestureLongPressMoveUpdateCallback]. /// /// The [globalPosition] and [offsetFromOrigin] arguments must not be null. const LongPressMoveUpdateDetails({ this.globalPosition = Offset.zero, Offset localPosition, this.offsetFromOrigin = Offset.zero, Offset localOffsetFromOrigin, }) : assert(globalPosition != null), assert(offsetFromOrigin != null), localPosition = localPosition ?? globalPosition, localOffsetFromOrigin = localOffsetFromOrigin ?? offsetFromOrigin; /// The global position of the pointer when it triggered this update. final Offset globalPosition; /// The local position of the pointer when it triggered this update. final Offset localPosition; /// A delta offset from the point where the long press drag initially contacted /// the screen to the point where the pointer is currently located (the /// present [globalPosition]) when this callback is triggered. final Offset offsetFromOrigin; /// A local delta offset from the point where the long press drag initially contacted /// the screen to the point where the pointer is currently located (the /// present [localPosition]) when this callback is triggered. final Offset localOffsetFromOrigin; } /// Details for callbacks that use [GestureLongPressEndCallback]. /// /// See also: /// /// * [LongPressGestureRecognizer.onLongPressEnd], which uses [GestureLongPressEndCallback]. /// * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback] /// * [LongPressStartDetails], the details for [GestureLongPressStartCallback]. class LongPressEndDetails { /// Creates the details for a [GestureLongPressEndCallback]. /// /// The [globalPosition] argument must not be null. const LongPressEndDetails({ this.globalPosition = Offset.zero, Offset localPosition, this.velocity = Velocity.zero, }) : assert(globalPosition != null), localPosition = localPosition ?? globalPosition; /// The global position at which the pointer lifted from the screen. final Offset globalPosition; /// The local position at which the pointer contacted the screen. final Offset localPosition; /// The pointer's velocity when it stopped contacting the screen. /// /// Defaults to zero if not specified in the constructor. final Velocity velocity; } /// Recognizes when the user has pressed down at the same location for a long /// period of time. /// /// The gesture must not deviate in position from its touch down point for 500ms /// until it's recognized. Once the gesture is accepted, the finger can be /// moved, triggering [onLongPressMoveUpdate] callbacks, unless the /// [postAcceptSlopTolerance] constructor argument is specified. /// /// [LongPressGestureRecognizer] competes on pointer events of [kPrimaryButton] /// only when it has at least one non-null callback. If it has no callbacks, it /// is a no-op. class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer { /// Creates a long-press gesture recognizer. /// /// Consider assigning the [onLongPressStart] callback after creating this /// object. /// /// The [postAcceptSlopTolerance] argument can be used to specify a maximum /// allowed distance for the gesture to deviate from the starting point once /// the long press has triggered. If the gesture deviates past that point, /// subsequent callbacks ([onLongPressMoveUpdate], [onLongPressUp], /// [onLongPressEnd]) will stop. Defaults to null, which means the gesture /// can be moved without limit once the long press is accepted. /// /// The [duration] argument can be used to overwrite the default duration /// after which the long press will be recognized. LongPressGestureRecognizer({ Duration duration, double postAcceptSlopTolerance, PointerDeviceKind kind, Object debugOwner, }) : super( deadline: duration ?? kLongPressTimeout, postAcceptSlopTolerance: postAcceptSlopTolerance, kind: kind, debugOwner: debugOwner, ); bool _longPressAccepted = false; OffsetPair _longPressOrigin; // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a // different set of buttons, the gesture is canceled. int _initialButtons; /// Called when a long press gesture by a primary button has been recognized. /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [onLongPressStart], which has the same timing but has data for the /// press location. GestureLongPressCallback onLongPress; /// Called when a long press gesture by a primary button has been recognized. /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [onLongPress], which has the same timing but without details. /// * [LongPressStartDetails], which is passed as an argument to this callback. GestureLongPressStartCallback onLongPressStart; /// Called when moving after the long press by a primary button is recognized. /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [LongPressMoveUpdateDetails], which is passed as an argument to this /// callback. GestureLongPressMoveUpdateCallback onLongPressMoveUpdate; /// Called when the pointer stops contacting the screen after a long-press /// by a primary button. /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [onLongPressEnd], which has the same timing but has data for the up /// gesture location. GestureLongPressUpCallback onLongPressUp; /// Called when the pointer stops contacting the screen after a long-press /// by a primary button. /// /// See also: /// /// * [kPrimaryButton], the button this callback responds to. /// * [onLongPressUp], which has the same timing, but without details. /// * [LongPressEndDetails], which is passed as an argument to this /// callback. GestureLongPressEndCallback onLongPressEnd; VelocityTracker _velocityTracker; @override bool isPointerAllowed(PointerDownEvent event) { switch (event.buttons) { case kPrimaryButton: if (onLongPressStart == null && onLongPress == null && onLongPressMoveUpdate == null && onLongPressEnd == null && onLongPressUp == null) return false; break; default: return false; } return super.isPointerAllowed(event); } @override void didExceedDeadline() { // Exceeding the deadline puts the gesture in the accepted state. resolve(GestureDisposition.accepted); _longPressAccepted = true; super.acceptGesture(primaryPointer); _checkLongPressStart(); } @override void handlePrimaryPointer(PointerEvent event) { if (!event.synthesized) { if (event is PointerDownEvent) { _velocityTracker = VelocityTracker(); _velocityTracker.addPosition(event.timeStamp, event.localPosition); } if (event is PointerMoveEvent) { assert(_velocityTracker != null); _velocityTracker.addPosition(event.timeStamp, event.localPosition); } } if (event is PointerUpEvent) { if (_longPressAccepted == true) { _checkLongPressEnd(event); } else { // Pointer is lifted before timeout. resolve(GestureDisposition.rejected); } _reset(); } else if (event is PointerCancelEvent) { _reset(); } else if (event is PointerDownEvent) { // The first touch. _longPressOrigin = OffsetPair.fromEventPosition(event); _initialButtons = event.buttons; } else if (event is PointerMoveEvent) { if (event.buttons != _initialButtons) { resolve(GestureDisposition.rejected); stopTrackingPointer(primaryPointer); } else if (_longPressAccepted) { _checkLongPressMoveUpdate(event); } } } void _checkLongPressStart() { assert(_initialButtons == kPrimaryButton); if (onLongPressStart != null) { final LongPressStartDetails details = LongPressStartDetails( globalPosition: _longPressOrigin.global, localPosition: _longPressOrigin.local, ); invokeCallback<void>('onLongPressStart', () => onLongPressStart(details)); } if (onLongPress != null) invokeCallback<void>('onLongPress', onLongPress); } void _checkLongPressMoveUpdate(PointerEvent event) { assert(_initialButtons == kPrimaryButton); final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails( globalPosition: event.position, localPosition: event.localPosition, offsetFromOrigin: event.position - _longPressOrigin.global, localOffsetFromOrigin: event.localPosition - _longPressOrigin.local, ); if (onLongPressMoveUpdate != null) invokeCallback<void>('onLongPressMoveUpdate', () => onLongPressMoveUpdate(details)); } void _checkLongPressEnd(PointerEvent event) { assert(_initialButtons == kPrimaryButton); final VelocityEstimate estimate = _velocityTracker.getVelocityEstimate(); final Velocity velocity = estimate == null ? Velocity.zero : Velocity(pixelsPerSecond: estimate.pixelsPerSecond); final LongPressEndDetails details = LongPressEndDetails( globalPosition: event.position, localPosition: event.localPosition, velocity: velocity, ); _velocityTracker = null; if (onLongPressEnd != null) invokeCallback<void>('onLongPressEnd', () => onLongPressEnd(details)); if (onLongPressUp != null) invokeCallback<void>('onLongPressUp', onLongPressUp); } void _reset() { _longPressAccepted = false; _longPressOrigin = null; _initialButtons = null; _velocityTracker = null; } @override void resolve(GestureDisposition disposition) { if (_longPressAccepted && disposition == GestureDisposition.rejected) { // This can happen if the gesture has been canceled. For example when // the buttons have changed. _reset(); } super.resolve(disposition); } @override void acceptGesture(int pointer) { // Winning the arena isn't important here since it may happen from a sweep. // Explicitly exceeding the deadline puts the gesture in accepted state. } @override String get debugDescription => 'long press'; }