346 lines
14 KiB
Dart
346 lines
14 KiB
Dart
// 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(PointerEvent 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';
|
|
}
|