force_press.dart 13.2 KB
Newer Older
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
// Copyright 2018 The Chromium 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 '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].
///  * [ForcePressUpdateDetails], the details for [ForcePressUpdateCallback].
class ForcePressDetails {
  /// Creates details for a [GestureForcePressStartCallback],
  /// [GestureForcePressPeakCallback] or [GestureForcePressEndCallback].
  ///
  /// The [globalPosition] argument must not be null.
  ForcePressDetails({
    @required this.globalPosition,
    @required this.pressure,
  }) : assert(globalPosition != null),
       assert(pressure != null);

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

  /// 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.
///
101 102 103
/// Devices with faux screen pressure sensors like the Pixel 2 and 3 will not
/// send any force press related callbacks.
///
104 105 106 107
/// 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.
108 109 110 111 112 113
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.
  ///
114 115 116 117 118
  /// 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`.
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
  ForcePressGestureRecognizer({
    this.startPressure = 0.4,
    this.peakPressure = 0.85,
    this.interpolation = _inverseLerp,
    Object debugOwner,
  }) : assert(startPressure != null),
       assert(peakPressure != null),
       assert(interpolation != null),
       assert(peakPressure > startPressure),
       super(debugOwner: debugOwner);

  /// 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 [ForcePressUpdateDetails] 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
177
  /// in the range 0.0 to 1.0.
178
  ///
179 180 181
  /// 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.
182
  ///
183 184 185
  /// 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.
186
  ///
187 188 189 190
  /// 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.
191
  ///
192
  /// For example, an ease-in curve can be used to determine the interpolated
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  /// 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;

  Offset _lastPosition;
  double _lastPressure;
  _ForceState _state = _ForceState.ready;

  @override
  void addPointer(PointerEvent event) {
209 210 211 212 213 214 215 216 217 218 219 220 221
    assert(event.pressureMax >= 1.0);
    // If the device has a maximum pressure of less than or equal to 1,
    // indicating a faux pressure sensor on this device or a device without a
    // pressure sensor (ie. on a non iOS device) we want do not want any
    // callbacks to be called.
    if (!(event is PointerUpEvent) && event.pressureMax == 1.0) {
      resolve(GestureDisposition.rejected);
    } else {
      startTrackingPointer(event.pointer);
      if (_state == _ForceState.ready) {
        _state = _ForceState.possible;
        _lastPosition = event.position;
      }
222 223 224 225 226 227 228 229
    }
  }

  @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) {
230 231 232 233 234 235 236 237
      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(),
        );
      }

238
      final double pressure = interpolation(event.pressureMin, event.pressureMax, event.pressure);
239
      assert(
240 241
        (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...
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
      _lastPosition = event.position;
      _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,
            globalPosition: _lastPosition,
          )));
        }
      }
      if (onPeak != null && pressure > peakPressure &&
         (_state == _ForceState.started)) {
        _state = _ForceState.peaked;
        if (onPeak != null) {
          invokeCallback<void>('onPeak', () => onPeak(ForcePressDetails(
            pressure: pressure,
            globalPosition: event.position,
          )));
        }
      }
276
      if (onUpdate != null &&  !pressure.isNaN &&
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
         (_state == _ForceState.started || _state == _ForceState.peaked)) {
        if (onUpdate != null) {
          invokeCallback<void>('onUpdate', () => onUpdate(ForcePressDetails(
            pressure: pressure,
            globalPosition: event.position,
          )));
        }
      }
    }
    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,
      )));
    }
  }

  @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,
        )));
      }
    }
    _state = _ForceState.ready;
  }

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

  static double _inverseLerp(double min, double max, double t) {
327
    assert(min <= max);
328 329 330 331 332 333 334
    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;
335 336 337 338 339
  }

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