1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
// 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 'package:flutter/foundation.dart' show clampDouble;
import 'events.dart';
import 'recognizer.dart';
export 'dart:ui' show Offset, PointerDeviceKind;
export 'events.dart' show PointerDownEvent, PointerEvent;
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,
}) : 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,
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
}) : assert(peakPressure > startPressure);
/// 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
/// 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.pressureMax <= 1.0) {
resolve(GestureDisposition.rejected);
} else {
super.addAllowedPointer(event);
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, gestureSettings)) {
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 = clampDouble(value, 0.0, 1.0);
}
return value;
}
@override
String get debugDescription => 'force press';
}