long_press.dart 13.2 KB
Newer Older
1 2 3 4
// Copyright 2015 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.

5 6
import 'arena.dart';
import 'constants.dart';
7
import 'events.dart';
8
import 'recognizer.dart';
9
import 'velocity_tracker.dart';
10

11 12 13
/// Callback signature for [LongPressGestureRecognizer.onLongPress].
///
/// Called when a pointer has remained in contact with the screen at the
14
/// same location for a long period of time.
15
typedef GestureLongPressCallback = void Function();
16

17 18 19 20
/// Callback signature for [LongPressGestureRecognizer.onLongPressUp].
///
/// Called when a pointer stops contacting the screen after a long press
/// gesture was detected.
21 22
typedef GestureLongPressUpCallback = void Function();

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
/// Callback signature for [LongPressGestureRecognizer.onLongPressStart].
///
/// Called when a pointer has remained in contact with the screen at the
/// same location for a long period of time. Also reports the long press down
/// position.
typedef GestureLongPressStartCallback = void Function(LongPressStartDetails details);

/// Callback signature for [LongPressGestureRecognizer.onLongPressMoveUpdate].
///
/// Called when a pointer is moving after being held in contact at the same
/// location for a long period of time. Reports the new position and its offset
/// from the original down position.
typedef GestureLongPressMoveUpdateCallback = void Function(LongPressMoveUpdateDetails details);

/// Callback signature for [LongPressGestureRecognizer.onLongPressEnd].
///
/// Called when a pointer stops contacting the screen after a long press
/// gesture was detected. Also reports the position where the pointer stopped
/// contacting the screen.
typedef GestureLongPressEndCallback = void Function(LongPressEndDetails details);

/// Details for callbacks that use [GestureLongPressStartCallback].
///
/// See also:
///
///  * [LongPressGestureRecognizer.onLongPressStart], which uses [GestureLongPressStartCallback].
///  * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback]
///  * [LongPressEndDetails], the details for [GestureLongPressEndCallback].
class LongPressStartDetails {
  /// Creates the details for a [GestureLongPressStartCallback].
  ///
  /// The [globalPosition] argument must not be null.
55 56 57 58 59
  const LongPressStartDetails({
    this.globalPosition = Offset.zero,
    Offset localPosition,
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
60 61 62

  /// The global position at which the pointer contacted the screen.
  final Offset globalPosition;
63 64 65

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
}

/// Details for callbacks that use [GestureLongPressMoveUpdateCallback].
///
/// See also:
///
///  * [LongPressGestureRecognizer.onLongPressMoveUpdate], which uses [GestureLongPressMoveUpdateCallback].
///  * [LongPressEndDetails], the details for [GestureLongPressEndCallback]
///  * [LongPressStartDetails], the details for [GestureLongPressStartCallback].
class LongPressMoveUpdateDetails {
  /// Creates the details for a [GestureLongPressMoveUpdateCallback].
  ///
  /// The [globalPosition] and [offsetFromOrigin] arguments must not be null.
  const LongPressMoveUpdateDetails({
    this.globalPosition = Offset.zero,
81
    Offset localPosition,
82
    this.offsetFromOrigin = Offset.zero,
83
    Offset localOffsetFromOrigin,
84
  }) : assert(globalPosition != null),
85 86 87
       assert(offsetFromOrigin != null),
       localPosition = localPosition ?? globalPosition,
       localOffsetFromOrigin = localOffsetFromOrigin ?? offsetFromOrigin;
88 89 90 91

  /// The global position of the pointer when it triggered this update.
  final Offset globalPosition;

92 93 94
  /// The local position of the pointer when it triggered this update.
  final Offset localPosition;

95 96 97 98
  /// A delta offset from the point where the long press drag initially contacted
  /// the screen to the point where the pointer is currently located (the
  /// present [globalPosition]) when this callback is triggered.
  final Offset offsetFromOrigin;
99 100 101 102 103

  /// A local delta offset from the point where the long press drag initially contacted
  /// the screen to the point where the pointer is currently located (the
  /// present [localPosition]) when this callback is triggered.
  final Offset localOffsetFromOrigin;
104 105 106 107 108 109 110 111 112 113 114 115 116
}

/// Details for callbacks that use [GestureLongPressEndCallback].
///
/// See also:
///
///  * [LongPressGestureRecognizer.onLongPressEnd], which uses [GestureLongPressEndCallback].
///  * [LongPressMoveUpdateDetails], the details for [GestureLongPressMoveUpdateCallback]
///  * [LongPressStartDetails], the details for [GestureLongPressStartCallback].
class LongPressEndDetails {
  /// Creates the details for a [GestureLongPressEndCallback].
  ///
  /// The [globalPosition] argument must not be null.
117 118 119
  const LongPressEndDetails({
    this.globalPosition = Offset.zero,
    Offset localPosition,
120
    this.velocity = Velocity.zero,
121 122
  }) : assert(globalPosition != null),
       localPosition = localPosition ?? globalPosition;
123 124 125

  /// The global position at which the pointer lifted from the screen.
  final Offset globalPosition;
126 127 128

  /// The local position at which the pointer contacted the screen.
  final Offset localPosition;
129 130 131 132 133

  /// The pointer's velocity when it stopped contacting the screen.
  ///
  /// Defaults to zero if not specified in the constructor.
  final Velocity velocity;
134 135
}

136 137
/// Recognizes when the user has pressed down at the same location for a long
/// period of time.
138 139 140 141 142
///
/// The gesture must not deviate in position from its touch down point for 500ms
/// until it's recognized. Once the gesture is accepted, the finger can be
/// moved, triggering [onLongPressMoveUpdate] callbacks, unless the
/// [postAcceptSlopTolerance] constructor argument is specified.
143 144 145 146
///
/// [LongPressGestureRecognizer] competes on pointer events of [kPrimaryButton]
/// only when it has at least one non-null callback. If it has no callbacks, it
/// is a no-op.
147
class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
148 149
  /// Creates a long-press gesture recognizer.
  ///
150 151 152 153 154 155 156 157 158
  /// Consider assigning the [onLongPressStart] callback after creating this
  /// object.
  ///
  /// The [postAcceptSlopTolerance] argument can be used to specify a maximum
  /// allowed distance for the gesture to deviate from the starting point once
  /// the long press has triggered. If the gesture deviates past that point,
  /// subsequent callbacks ([onLongPressMoveUpdate], [onLongPressUp],
  /// [onLongPressEnd]) will stop. Defaults to null, which means the gesture
  /// can be moved without limit once the long press is accepted.
159 160 161
  ///
  /// The [duration] argument can be used to overwrite the default duration
  /// after which the long press will be recognized.
162
  LongPressGestureRecognizer({
163
    Duration duration,
164
    double postAcceptSlopTolerance,
165
    PointerDeviceKind kind,
166 167
    Object debugOwner,
  }) : super(
168 169 170 171 172
          deadline: duration ?? kLongPressTimeout,
          postAcceptSlopTolerance: postAcceptSlopTolerance,
          kind: kind,
          debugOwner: debugOwner,
        );
173 174

  bool _longPressAccepted = false;
175
  OffsetPair _longPressOrigin;
176 177 178
  // The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
  // different set of buttons, the gesture is canceled.
  int _initialButtons;
179

180
  /// Called when a long press gesture by a primary button has been recognized.
181 182 183
  ///
  /// See also:
  ///
184
  ///  * [kPrimaryButton], the button this callback responds to.
185 186
  ///  * [onLongPressStart], which has the same timing but has data for the
  ///    press location.
187
  GestureLongPressCallback onLongPress;
188

189
  /// Called when a long press gesture by a primary button has been recognized.
190 191 192
  ///
  /// See also:
  ///
193 194 195
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onLongPress], which has the same timing but without details.
  ///  * [LongPressStartDetails], which is passed as an argument to this callback.
196 197
  GestureLongPressStartCallback onLongPressStart;

198 199 200 201 202 203 204
  /// Called when moving after the long press by a primary button is recognized.
  ///
  /// See also:
  ///
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [LongPressMoveUpdateDetails], which is passed as an argument to this
  ///    callback.
205 206
  GestureLongPressMoveUpdateCallback onLongPressMoveUpdate;

207 208
  /// Called when the pointer stops contacting the screen after a long-press
  /// by a primary button.
209 210 211
  ///
  /// See also:
  ///
212
  ///  * [kPrimaryButton], the button this callback responds to.
213 214
  ///  * [onLongPressEnd], which has the same timing but has data for the up
  ///    gesture location.
215 216
  GestureLongPressUpCallback onLongPressUp;

217 218
  /// Called when the pointer stops contacting the screen after a long-press
  /// by a primary button.
219 220 221
  ///
  /// See also:
  ///
222 223 224 225
  ///  * [kPrimaryButton], the button this callback responds to.
  ///  * [onLongPressUp], which has the same timing, but without details.
  ///  * [LongPressEndDetails], which is passed as an argument to this
  ///    callback.
226 227
  GestureLongPressEndCallback onLongPressEnd;

228 229
  VelocityTracker _velocityTracker;

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
  @override
  bool isPointerAllowed(PointerDownEvent event) {
    switch (event.buttons) {
      case kPrimaryButton:
        if (onLongPressStart == null &&
            onLongPress == null &&
            onLongPressMoveUpdate == null &&
            onLongPressEnd == null &&
            onLongPressUp == null)
          return false;
        break;
      default:
        return false;
    }
    return super.isPointerAllowed(event);
  }

247
  @override
248
  void didExceedDeadline() {
249
    // Exceeding the deadline puts the gesture in the accepted state.
250
    resolve(GestureDisposition.accepted);
251
    _longPressAccepted = true;
252
    super.acceptGesture(primaryPointer);
253
    _checkLongPressStart();
254 255
  }

256
  @override
Ian Hickson's avatar
Ian Hickson committed
257
  void handlePrimaryPointer(PointerEvent event) {
258 259 260 261 262 263 264 265 266 267 268
    if (!event.synthesized) {
      if (event is PointerDownEvent) {
        _velocityTracker = VelocityTracker();
        _velocityTracker.addPosition(event.timeStamp, event.localPosition);
      }
      if (event is PointerMoveEvent) {
        assert(_velocityTracker != null);
        _velocityTracker.addPosition(event.timeStamp, event.localPosition);
      }
    }

269
    if (event is PointerUpEvent) {
270
      if (_longPressAccepted == true) {
271
        _checkLongPressEnd(event);
272
      } else {
273
        // Pointer is lifted before timeout.
274 275
        resolve(GestureDisposition.rejected);
      }
276 277 278 279
      _reset();
    } else if (event is PointerCancelEvent) {
      _reset();
    } else if (event is PointerDownEvent) {
280
      // The first touch.
281
      _longPressOrigin = OffsetPair.fromEventPosition(event);
282 283 284 285 286 287 288 289 290 291 292 293 294
      _initialButtons = event.buttons;
    } else if (event is PointerMoveEvent) {
      if (event.buttons != _initialButtons) {
        resolve(GestureDisposition.rejected);
        stopTrackingPointer(primaryPointer);
      } else if (_longPressAccepted) {
        _checkLongPressMoveUpdate(event);
      }
    }
  }

  void _checkLongPressStart() {
    assert(_initialButtons == kPrimaryButton);
295 296 297 298 299
    if (onLongPressStart != null) {
      final LongPressStartDetails details = LongPressStartDetails(
        globalPosition: _longPressOrigin.global,
        localPosition: _longPressOrigin.local,
      );
300 301
      invokeCallback<void>('onLongPressStart',
        () => onLongPressStart(details));
302
    }
303 304 305 306 307 308 309 310
    if (onLongPress != null)
      invokeCallback<void>('onLongPress', onLongPress);
  }

  void _checkLongPressMoveUpdate(PointerEvent event) {
    assert(_initialButtons == kPrimaryButton);
    final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails(
      globalPosition: event.position,
311 312 313
      localPosition: event.localPosition,
      offsetFromOrigin: event.position - _longPressOrigin.global,
      localOffsetFromOrigin: event.localPosition - _longPressOrigin.local,
314 315 316 317 318 319 320 321
    );
    if (onLongPressMoveUpdate != null)
      invokeCallback<void>('onLongPressMoveUpdate',
        () => onLongPressMoveUpdate(details));
  }

  void _checkLongPressEnd(PointerEvent event) {
    assert(_initialButtons == kPrimaryButton);
322 323 324

    final VelocityEstimate estimate = _velocityTracker.getVelocityEstimate();
    final Velocity velocity = estimate == null ? Velocity.zero : Velocity(pixelsPerSecond: estimate.pixelsPerSecond);
325 326
    final LongPressEndDetails details = LongPressEndDetails(
      globalPosition: event.position,
327
      localPosition: event.localPosition,
328
      velocity: velocity,
329
    );
330 331

    _velocityTracker = null;
332 333 334 335 336 337 338 339 340 341
    if (onLongPressEnd != null)
      invokeCallback<void>('onLongPressEnd', () => onLongPressEnd(details));
    if (onLongPressUp != null)
      invokeCallback<void>('onLongPressUp', onLongPressUp);
  }

  void _reset() {
    _longPressAccepted = false;
    _longPressOrigin = null;
    _initialButtons = null;
342
    _velocityTracker = null;
343 344 345 346 347 348 349 350
  }

  @override
  void resolve(GestureDisposition disposition) {
    if (_longPressAccepted && disposition == GestureDisposition.rejected) {
      // This can happen if the gesture has been canceled. For example when
      // the buttons have changed.
      _reset();
351
    }
352
    super.resolve(disposition);
353
  }
354

355 356 357 358 359 360
  @override
  void acceptGesture(int pointer) {
    // Winning the arena isn't important here since it may happen from a sweep.
    // Explicitly exceeding the deadline puts the gesture in accepted state.
  }

361
  @override
362
  String get debugDescription => 'long press';
363
}