force_press.dart 13.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
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
// 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({
47 48 49
    required this.globalPosition,
    Offset? localPosition,
    required this.pressure,
50
  }) : assert(globalPosition != null),
51 52
       assert(pressure != null),
       localPosition = localPosition ?? globalPosition;
53 54 55 56

  /// The global position at which the function was called.
  final Offset globalPosition;

57 58 59
  /// The local position at which the function was called.
  final Offset localPosition;

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
  /// 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.
///
100 101 102
/// Devices with faux screen pressure sensors like the Pixel 2 and 3 will not
/// send any force press related callbacks.
///
103 104 105 106
/// 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.
107 108 109 110 111 112
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.
  ///
113 114 115 116 117
  /// 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`.
118
  ///
119
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
120 121 122 123
  ForcePressGestureRecognizer({
    this.startPressure = 0.4,
    this.peakPressure = 0.85,
    this.interpolation = _inverseLerp,
124
    Object? debugOwner,
125 126 127 128
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
129
    PointerDeviceKind? kind,
130
    Set<PointerDeviceKind>? supportedDevices,
131 132 133 134
  }) : assert(startPressure != null),
       assert(peakPressure != null),
       assert(interpolation != null),
       assert(peakPressure > startPressure),
135 136 137 138 139
       super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
140 141 142 143 144 145 146 147

  /// 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.
148
  GestureForcePressStartCallback? onStart;
149 150 151 152 153 154 155 156 157

  /// 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,
Dan Field's avatar
Dan Field committed
158
  /// which is a [ForcePressDetails] object.
159
  GestureForcePressUpdateCallback? onUpdate;
160 161 162 163 164 165 166 167

  /// 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.
168
  GestureForcePressPeakCallback? onPeak;
169 170 171 172 173

  /// 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.
174
  GestureForcePressEndCallback? onEnd;
175 176 177 178 179 180 181 182 183 184 185 186 187

  /// 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
188
  /// in the range 0.0 to 1.0.
189
  ///
190 191 192
  /// 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.
193
  ///
194 195
  /// 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
Dan Field's avatar
Dan Field committed
196
  /// `double.NaN` for values that it does not want to support.
197
  ///
198 199 200 201
  /// 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.
202
  ///
203
  /// For example, an ease-in curve can be used to determine the interpolated
204 205 206 207 208 209 210 211 212 213
  /// 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;

214 215
  late OffsetPair _lastPosition;
  late double _lastPressure;
216 217 218
  _ForceState _state = _ForceState.ready;

  @override
219
  void addAllowedPointer(PointerDownEvent event) {
220 221 222
    // 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.
223
    if (event.pressureMax <= 1.0) {
224 225
      resolve(GestureDisposition.rejected);
    } else {
226
      super.addAllowedPointer(event);
227 228
      if (_state == _ForceState.ready) {
        _state = _ForceState.possible;
229
        _lastPosition = OffsetPair.fromEventPosition(event);
230
      }
231 232 233 234 235 236 237 238 239
    }
  }

  @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);
240
      assert(
241
        (pressure >= 0.0 && pressure <= 1.0) || // Interpolated pressure must be between 1.0 and 0.0...
242
        pressure.isNaN, // and interpolation may return NaN for values it doesn't want to support...
243
      );
244

245
      _lastPosition = OffsetPair.fromEventPosition(event);
246 247 248 249 250 251
      _lastPressure = pressure;

      if (_state == _ForceState.possible) {
        if (pressure > startPressure) {
          _state = _ForceState.started;
          resolve(GestureDisposition.accepted);
252
        } else if (event.delta.distanceSquared > computeHitSlop(event.kind)) {
253 254 255 256 257 258 259 260
          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) {
261
          invokeCallback<void>('onStart', () => onStart!(ForcePressDetails(
262
            pressure: pressure,
263 264
            globalPosition: _lastPosition.global,
            localPosition: _lastPosition.local,
265 266 267 268 269 270 271
          )));
        }
      }
      if (onPeak != null && pressure > peakPressure &&
         (_state == _ForceState.started)) {
        _state = _ForceState.peaked;
        if (onPeak != null) {
272
          invokeCallback<void>('onPeak', () => onPeak!(ForcePressDetails(
273 274
            pressure: pressure,
            globalPosition: event.position,
275
            localPosition: event.localPosition,
276 277 278
          )));
        }
      }
279
      if (onUpdate != null &&  !pressure.isNaN &&
280 281
         (_state == _ForceState.started || _state == _ForceState.peaked)) {
        if (onUpdate != null) {
282
          invokeCallback<void>('onUpdate', () => onUpdate!(ForcePressDetails(
283 284
            pressure: pressure,
            globalPosition: event.position,
285
            localPosition: event.localPosition,
286 287 288 289 290 291 292 293 294 295 296 297 298
          )));
        }
      }
    }
    stopTrackingIfPointerNoLongerDown(event);
  }

  @override
  void acceptGesture(int pointer) {
    if (_state == _ForceState.possible)
      _state = _ForceState.accepted;

    if (onStart != null && _state == _ForceState.started) {
299
      invokeCallback<void>('onStart', () => onStart!(ForcePressDetails(
300
        pressure: _lastPressure,
301 302
        globalPosition: _lastPosition.global,
        localPosition: _lastPosition.local,
303 304 305 306 307 308 309 310 311 312 313 314 315
      )));
    }
  }

  @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) {
316
        invokeCallback<void>('onEnd', () => onEnd!(ForcePressDetails(
317
          pressure: 0.0,
318 319
          globalPosition: _lastPosition.global,
          localPosition: _lastPosition.local,
320 321 322 323 324 325 326 327 328 329 330 331 332
        )));
      }
    }
    _state = _ForceState.ready;
  }

  @override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
    didStopTrackingLastPointer(pointer);
  }

  static double _inverseLerp(double min, double max, double t) {
333
    assert(min <= max);
334 335 336 337 338
    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)
339
      value = value.clamp(0.0, 1.0);
340
    return value;
341 342 343 344 345
  }

  @override
  String get debugDescription => 'force press';
}