// 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 'events.dart'; import 'recognizer.dart'; enum _ForceState { // No pointer has touched down and the detector is ready for a pointer down to occur. ready, // A pointer has touched down, but a force press gesture has not yet been detected. possible, // A pointer is down and a force press gesture has been detected. However, if // the ForcePressGestureRecognizer is the only recognizer in the arena, thus // accepted as soon as the gesture state is possible, the gesture will not // yet have started. accepted, // A pointer is down and the gesture has started, ie. the pressure of the pointer // has just become greater than the ForcePressGestureRecognizer.startPressure. started, // A pointer is down and the pressure of the pointer has just become greater // than the ForcePressGestureRecognizer.peakPressure. Even after a pointer // crosses this threshold, onUpdate callbacks will still be sent. peaked, } /// Details object for callbacks that use [GestureForcePressStartCallback], /// [GestureForcePressPeakCallback], [GestureForcePressEndCallback] or /// [GestureForcePressUpdateCallback]. /// /// See also: /// /// * [ForcePressGestureRecognizer.onStart], [ForcePressGestureRecognizer.onPeak], /// [ForcePressGestureRecognizer.onEnd], and [ForcePressGestureRecognizer.onUpdate] /// which use [ForcePressDetails]. class ForcePressDetails { /// Creates details for a [GestureForcePressStartCallback], /// [GestureForcePressPeakCallback] or [GestureForcePressEndCallback]. /// /// The [globalPosition] argument must not be null. ForcePressDetails({ required this.globalPosition, Offset? localPosition, required this.pressure, }) : assert(globalPosition != null), assert(pressure != null), localPosition = localPosition ?? globalPosition; /// The global position at which the function was called. final Offset globalPosition; /// The local position at which the function was called. final Offset localPosition; /// The pressure of the pointer on the screen. final double pressure; } /// Signature used by a [ForcePressGestureRecognizer] for when a pointer has /// pressed with at least [ForcePressGestureRecognizer.startPressure]. typedef GestureForcePressStartCallback = void Function(ForcePressDetails details); /// Signature used by [ForcePressGestureRecognizer] for when a pointer that has /// pressed with at least [ForcePressGestureRecognizer.peakPressure]. typedef GestureForcePressPeakCallback = void Function(ForcePressDetails details); /// Signature used by [ForcePressGestureRecognizer] during the frames /// after the triggering of a [ForcePressGestureRecognizer.onStart] callback. typedef GestureForcePressUpdateCallback = void Function(ForcePressDetails details); /// Signature for when the pointer that previously triggered a /// [ForcePressGestureRecognizer.onStart] callback is no longer in contact /// with the screen. typedef GestureForcePressEndCallback = void Function(ForcePressDetails details); /// Signature used by [ForcePressGestureRecognizer] for interpolating the raw /// device pressure to a value in the range [0, 1] given the device's pressure /// min and pressure max. typedef GestureForceInterpolation = double Function(double pressureMin, double pressureMax, double pressure); /// Recognizes a force press on devices that have force sensors. /// /// Only the force from a single pointer is used to invoke events. A tap /// recognizer will win against this recognizer on pointer up as long as the /// pointer has not pressed with a force greater than /// [ForcePressGestureRecognizer.startPressure]. A long press recognizer will /// win when the press down time exceeds the threshold time as long as the /// pointer's pressure was never greater than /// [ForcePressGestureRecognizer.startPressure] in that duration. /// /// As of November, 2018 iPhone devices of generation 6S and higher have /// force touch functionality, with the exception of the iPhone XR. In addition, /// a small handful of Android devices have this functionality as well. /// /// Devices with faux screen pressure sensors like the Pixel 2 and 3 will not /// send any force press related callbacks. /// /// Reported pressure will always be in the range 0.0 to 1.0, where 1.0 is /// maximum pressure and 0.0 is minimum pressure. If using a custom /// [interpolation] callback, the pressure reported will correspond to that /// custom curve. class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer { /// Creates a force press gesture recognizer. /// /// The [startPressure] defaults to 0.4, and [peakPressure] defaults to 0.85 /// where a value of 0.0 is no pressure and a value of 1.0 is maximum pressure. /// /// The [startPressure], [peakPressure] and [interpolation] arguments must not /// be null. The [peakPressure] argument must be greater than [startPressure]. /// The [interpolation] callback must always return a value in the range 0.0 /// to 1.0 for values of `pressure` that are between `pressureMin` and /// `pressureMax`. /// /// {@macro flutter.gestures.GestureRecognizer.supportedDevices} ForcePressGestureRecognizer({ this.startPressure = 0.4, this.peakPressure = 0.85, this.interpolation = _inverseLerp, Object? debugOwner, @Deprecated( 'Migrate to supportedDevices. ' 'This feature was deprecated after v2.3.0-1.0.pre.', ) PointerDeviceKind? kind, Set<PointerDeviceKind>? supportedDevices, }) : assert(startPressure != null), assert(peakPressure != null), assert(interpolation != null), assert(peakPressure > startPressure), super( debugOwner: debugOwner, kind: kind, supportedDevices: supportedDevices, ); /// A pointer is in contact with the screen and has just pressed with a force /// exceeding the [startPressure]. Consequently, if there were other gesture /// detectors, only the force press gesture will be detected and all others /// will be rejected. /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [ForcePressDetails] object. GestureForcePressStartCallback? onStart; /// A pointer is in contact with the screen and is either moving on the plane /// of the screen, pressing the screen with varying forces or both /// simultaneously. /// /// This callback will be invoked for every pointer event after the invocation /// of [onStart] and/or [onPeak] and before the invocation of [onEnd], no /// matter what the pressure is during this time period. The position and /// pressure of the pointer is provided in the callback's `details` argument, /// which is a [ForcePressDetails] object. GestureForcePressUpdateCallback? onUpdate; /// A pointer is in contact with the screen and has just pressed with a force /// exceeding the [peakPressure]. This is an arbitrary second level action /// threshold and isn't necessarily the maximum possible device pressure /// (which is 1.0). /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [ForcePressDetails] object. GestureForcePressPeakCallback? onPeak; /// A pointer is no longer in contact with the screen. /// /// The position of the pointer is provided in the callback's `details` /// argument, which is a [ForcePressDetails] object. GestureForcePressEndCallback? onEnd; /// The pressure of the press required to initiate a force press. /// /// A value of 0.0 is no pressure, and 1.0 is maximum pressure. final double startPressure; /// The pressure of the press required to peak a force press. /// /// A value of 0.0 is no pressure, and 1.0 is maximum pressure. This value /// must be greater than [startPressure]. final double peakPressure; /// The function used to convert the raw device pressure values into a value /// in the range 0.0 to 1.0. /// /// The function takes in the device's minimum, maximum and raw touch pressure /// and returns a value in the range 0.0 to 1.0 denoting the interpolated /// touch pressure. /// /// This function must always return values in the range 0.0 to 1.0 given a /// pressure that is between the minimum and maximum pressures. It may return /// `double.NaN` for values that it does not want to support. /// /// By default, the function is a linear interpolation; however, changing the /// function could be useful to accommodate variations in the way different /// devices respond to pressure, or to change how animations from pressure /// feedback are rendered. /// /// For example, an ease-in curve can be used to determine the interpolated /// value: /// /// ```dart /// static double interpolateWithEasing(double min, double max, double t) { /// final double lerp = (t - min) / (max - min); /// return Curves.easeIn.transform(lerp); /// } /// ``` final GestureForceInterpolation interpolation; late OffsetPair _lastPosition; late double _lastPressure; _ForceState _state = _ForceState.ready; @override void addAllowedPointer(PointerDownEvent event) { // If the device has a maximum pressure of less than or equal to 1, it // doesn't have touch pressure sensing capabilities. Do not participate // in the gesture arena. if (event is! PointerUpEvent && event.pressureMax <= 1.0) { resolve(GestureDisposition.rejected); } else { startTrackingPointer(event.pointer, event.transform); if (_state == _ForceState.ready) { _state = _ForceState.possible; _lastPosition = OffsetPair.fromEventPosition(event); } } } @override void handleEvent(PointerEvent event) { assert(_state != _ForceState.ready); // A static pointer with changes in pressure creates PointerMoveEvent events. if (event is PointerMoveEvent || event is PointerDownEvent) { final double pressure = interpolation(event.pressureMin, event.pressureMax, event.pressure); assert( (pressure >= 0.0 && pressure <= 1.0) || // Interpolated pressure must be between 1.0 and 0.0... pressure.isNaN, // and interpolation may return NaN for values it doesn't want to support... ); _lastPosition = OffsetPair.fromEventPosition(event); _lastPressure = pressure; if (_state == _ForceState.possible) { if (pressure > startPressure) { _state = _ForceState.started; resolve(GestureDisposition.accepted); } else if (event.delta.distanceSquared > computeHitSlop(event.kind)) { resolve(GestureDisposition.rejected); } } // In case this is the only gesture detector we still don't want to start // the gesture until the pressure is greater than the startPressure. if (pressure > startPressure && _state == _ForceState.accepted) { _state = _ForceState.started; if (onStart != null) { invokeCallback<void>('onStart', () => onStart!(ForcePressDetails( pressure: pressure, globalPosition: _lastPosition.global, localPosition: _lastPosition.local, ))); } } if (onPeak != null && pressure > peakPressure && (_state == _ForceState.started)) { _state = _ForceState.peaked; if (onPeak != null) { invokeCallback<void>('onPeak', () => onPeak!(ForcePressDetails( pressure: pressure, globalPosition: event.position, localPosition: event.localPosition, ))); } } if (onUpdate != null && !pressure.isNaN && (_state == _ForceState.started || _state == _ForceState.peaked)) { if (onUpdate != null) { invokeCallback<void>('onUpdate', () => onUpdate!(ForcePressDetails( pressure: pressure, globalPosition: event.position, localPosition: event.localPosition, ))); } } } stopTrackingIfPointerNoLongerDown(event); } @override void acceptGesture(int pointer) { if (_state == _ForceState.possible) _state = _ForceState.accepted; if (onStart != null && _state == _ForceState.started) { invokeCallback<void>('onStart', () => onStart!(ForcePressDetails( pressure: _lastPressure, globalPosition: _lastPosition.global, localPosition: _lastPosition.local, ))); } } @override void didStopTrackingLastPointer(int pointer) { final bool wasAccepted = _state == _ForceState.started || _state == _ForceState.peaked; if (_state == _ForceState.possible) { resolve(GestureDisposition.rejected); return; } if (wasAccepted && onEnd != null) { if (onEnd != null) { invokeCallback<void>('onEnd', () => onEnd!(ForcePressDetails( pressure: 0.0, globalPosition: _lastPosition.global, localPosition: _lastPosition.local, ))); } } _state = _ForceState.ready; } @override void rejectGesture(int pointer) { stopTrackingPointer(pointer); didStopTrackingLastPointer(pointer); } static double _inverseLerp(double min, double max, double t) { assert(min <= max); double value = (t - min) / (max - min); // If the device incorrectly reports a pressure outside of pressureMin // and pressureMax, we still want this recognizer to respond normally. if (!value.isNaN) value = value.clamp(0.0, 1.0); return value; } @override String get debugDescription => 'force press'; }