force_press.dart 13.6 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 47 48 49 50 51 52
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' show Offset;

import 'package:flutter/foundation.dart';

import 'arena.dart';
import 'constants.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,
53
    Offset localPosition,
54 55
    @required this.pressure,
  }) : assert(globalPosition != null),
56 57
       assert(pressure != null),
       localPosition = localPosition ?? globalPosition;
58 59 60 61

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

62 63 64
  /// The local position at which the function was called.
  final Offset localPosition;

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
  /// 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.
///
105 106 107
/// Devices with faux screen pressure sensors like the Pixel 2 and 3 will not
/// send any force press related callbacks.
///
108 109 110 111
/// 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.
112 113 114 115 116 117
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.
  ///
118 119 120 121 122
  /// 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`.
123 124
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
125 126 127 128 129
  ForcePressGestureRecognizer({
    this.startPressure = 0.4,
    this.peakPressure = 0.85,
    this.interpolation = _inverseLerp,
    Object debugOwner,
130
    PointerDeviceKind kind,
131 132 133 134
  }) : assert(startPressure != null),
       assert(peakPressure != null),
       assert(interpolation != null),
       assert(peakPressure > startPressure),
135
       super(debugOwner: debugOwner, kind: kind);
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153

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

210
  OffsetPair _lastPosition;
211 212 213 214
  double _lastPressure;
  _ForceState _state = _ForceState.ready;

  @override
215
  void addAllowedPointer(PointerEvent event) {
216 217 218
    // 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.
219
    if (event is! PointerUpEvent && event.pressureMax <= 1.0) {
220 221
      resolve(GestureDisposition.rejected);
    } else {
222
      startTrackingPointer(event.pointer, event.transform);
223 224
      if (_state == _ForceState.ready) {
        _state = _ForceState.possible;
225
        _lastPosition = OffsetPair.fromEventPosition(event);
226
      }
227 228 229 230 231 232 233 234
    }
  }

  @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) {
235 236 237 238 239 240 241 242
      if (event.pressure > event.pressureMax || event.pressure < event.pressureMin) {
        debugPrint(
          'The reported device pressure ' + event.pressure.toString() +
          ' is outside of the device pressure range where: ' +
          event.pressureMin.toString() + ' <= pressure <= ' + event.pressureMax.toString(),
        );
      }

243
      final double pressure = interpolation(event.pressureMin, event.pressureMax, event.pressure);
244
      assert(
245 246
        (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...
247
      );
248

249
      _lastPosition = OffsetPair.fromEventPosition(event);
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
      _lastPressure = pressure;

      if (_state == _ForceState.possible) {
        if (pressure > startPressure) {
          _state = _ForceState.started;
          resolve(GestureDisposition.accepted);
        } else if (event.delta.distanceSquared > kTouchSlop) {
          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,
267 268
            globalPosition: _lastPosition.global,
            localPosition: _lastPosition.local,
269 270 271 272 273 274 275 276 277 278
          )));
        }
      }
      if (onPeak != null && pressure > peakPressure &&
         (_state == _ForceState.started)) {
        _state = _ForceState.peaked;
        if (onPeak != null) {
          invokeCallback<void>('onPeak', () => onPeak(ForcePressDetails(
            pressure: pressure,
            globalPosition: event.position,
279
            localPosition: event.localPosition,
280 281 282
          )));
        }
      }
283
      if (onUpdate != null &&  !pressure.isNaN &&
284 285 286 287 288
         (_state == _ForceState.started || _state == _ForceState.peaked)) {
        if (onUpdate != null) {
          invokeCallback<void>('onUpdate', () => onUpdate(ForcePressDetails(
            pressure: pressure,
            globalPosition: event.position,
289
            localPosition: event.localPosition,
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
          )));
        }
      }
    }
    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,
305 306
        globalPosition: _lastPosition.global,
        localPosition: _lastPosition.local,
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
      )));
    }
  }

  @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,
322 323
          globalPosition: _lastPosition.global,
          localPosition: _lastPosition.local,
324 325 326 327 328 329 330 331 332 333 334 335 336
        )));
      }
    }
    _state = _ForceState.ready;
  }

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

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

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