multidrag.dart 18.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5

6
import 'dart:async';
7
import 'dart:ui' show Offset;
8

9
import 'package:flutter/foundation.dart';
10

11
import 'arena.dart';
12
import 'binding.dart';
13
import 'constants.dart';
14
import 'drag.dart';
15
import 'drag_details.dart';
16 17 18 19
import 'events.dart';
import 'recognizer.dart';
import 'velocity_tracker.dart';

20
/// Signature for when [MultiDragGestureRecognizer] recognizes the start of a drag gesture.
21
typedef GestureMultiDragStartCallback = Drag? Function(Offset position);
22

23 24 25 26
/// Per-pointer state for a [MultiDragGestureRecognizer].
///
/// A [MultiDragGestureRecognizer] tracks each pointer separately. The state for
/// each pointer is a subclass of [MultiDragPointerState].
27
abstract class MultiDragPointerState {
28 29 30
  /// Creates per-pointer state for a [MultiDragGestureRecognizer].
  ///
  /// The [initialPosition] argument must not be null.
31 32
  MultiDragPointerState(this.initialPosition, this.kind)
    : assert(initialPosition != null),
33
      _velocityTracker = VelocityTracker.withKind(kind);
34

35
  /// The global coordinates of the pointer when the pointer contacted the screen.
36
  final Offset initialPosition;
37

38 39 40 41 42 43 44
  final VelocityTracker _velocityTracker;

  /// The kind of pointer performing the multi-drag gesture.
  ///
  /// Used by subclasses to determine the appropriate hit slop, for example.
  final PointerDeviceKind kind;

45
  Drag? _client;
46

47 48 49 50 51 52
  /// The offset of the pointer from the last position that was reported to the client.
  ///
  /// After the pointer contacts the screen, the pointer might move some
  /// distance before this movement will be recognized as a drag. This field
  /// accumulates that movement so that we can report it to the client after
  /// the drag starts.
53 54
  Offset? get pendingDelta => _pendingDelta;
  Offset? _pendingDelta = Offset.zero;
55

56
  Duration? _lastPendingEventTimestamp;
57

58
  GestureArenaEntry? _arenaEntry;
59 60 61 62 63 64 65
  void _setArenaEntry(GestureArenaEntry entry) {
    assert(_arenaEntry == null);
    assert(pendingDelta != null);
    assert(_client == null);
    _arenaEntry = entry;
  }

66
  /// Resolve this pointer's entry in the [GestureArenaManager] with the given disposition.
67
  @protected
68
  @mustCallSuper
69
  void resolve(GestureDisposition disposition) {
70
    _arenaEntry!.resolve(disposition);
71 72 73 74
  }

  void _move(PointerMoveEvent event) {
    assert(_arenaEntry != null);
75 76
    if (!event.synthesized)
      _velocityTracker.addPosition(event.timeStamp, event.position);
77 78
    if (_client != null) {
      assert(pendingDelta == null);
79
      // Call client last to avoid reentrancy.
80
      _client!.update(DragUpdateDetails(
81
        sourceTimeStamp: event.timeStamp,
82 83 84
        delta: event.delta,
        globalPosition: event.position,
      ));
85 86
    } else {
      assert(pendingDelta != null);
87
      _pendingDelta = _pendingDelta! + event.delta;
88
      _lastPendingEventTimestamp = event.timeStamp;
89 90 91 92 93 94 95
      checkForResolutionAfterMove();
    }
  }

  /// Override this to call resolve() if the drag should be accepted or rejected.
  /// This is called when a pointer movement is received, but only if the gesture
  /// has not yet been resolved.
96
  @protected
97 98 99
  void checkForResolutionAfterMove() { }

  /// Called when the gesture was accepted.
100 101 102
  ///
  /// Either immediately or at some future point before the gesture is disposed,
  /// call starter(), passing it initialPosition, to start the drag.
103
  @protected
104 105 106 107
  void accepted(GestureMultiDragStartCallback starter);

  /// Called when the gesture was rejected.
  ///
108
  /// The [dispose] method will be called immediately following this.
109 110
  @protected
  @mustCallSuper
111
  void rejected() {
112 113
    assert(_arenaEntry != null);
    assert(_client == null);
114
    assert(pendingDelta != null);
115
    _pendingDelta = null;
116
    _lastPendingEventTimestamp = null;
117
    _arenaEntry = null;
118 119
  }

120
  void _startDrag(Drag client) {
121 122
    assert(_arenaEntry != null);
    assert(_client == null);
123
    assert(client != null);
124
    assert(pendingDelta != null);
125
    _client = client;
126
    final DragUpdateDetails details = DragUpdateDetails(
127
      sourceTimeStamp: _lastPendingEventTimestamp,
128
      delta: pendingDelta!,
129 130
      globalPosition: initialPosition,
    );
131
    _pendingDelta = null;
132
    _lastPendingEventTimestamp = null;
133
    // Call client last to avoid reentrancy.
134
    _client!.update(details);
135 136 137 138 139 140
  }

  void _up() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
141
      final DragEndDetails details = DragEndDetails(velocity: _velocityTracker.getVelocity());
142
      final Drag client = _client!;
143
      _client = null;
144 145
      // Call client last to avoid reentrancy.
      client.end(details);
146 147 148
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
149
      _lastPendingEventTimestamp = null;
150 151 152 153 154 155 156
    }
  }

  void _cancel() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
157
      final Drag client = _client!;
158
      _client = null;
159 160
      // Call client last to avoid reentrancy.
      client.cancel();
161 162 163
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
164
      _lastPendingEventTimestamp = null;
165 166 167
    }
  }

168
  /// Releases any resources used by the object.
169
  @protected
170
  @mustCallSuper
171
  void dispose() {
172
    _arenaEntry?.resolve(GestureDisposition.rejected);
173
    _arenaEntry = null;
174 175 176 177
    assert(() {
      _pendingDelta = null;
      return true;
    }());
178
  }
179 180
}

181 182
/// Recognizes movement on a per-pointer basis.
///
183 184 185
/// In contrast to [DragGestureRecognizer], [MultiDragGestureRecognizer] watches
/// each pointer separately, which means multiple drags can be recognized
/// concurrently if multiple pointers are in contact with the screen.
186 187 188 189 190 191 192
///
/// [MultiDragGestureRecognizer] is not intended to be used directly. Instead,
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
/// See also:
///
193 194 195 196 197 198 199 200
///  * [ImmediateMultiDragGestureRecognizer], the most straight-forward variant
///    of multi-pointer drag gesture recognizer.
///  * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that
///    start horizontally.
///  * [VerticalMultiDragGestureRecognizer], which only recognizes drags that
///    start vertically.
///  * [DelayedMultiDragGestureRecognizer], which only recognizes drags that
///    start after a long-press gesture.
201
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
202
  /// Initialize the object.
203
  MultiDragGestureRecognizer({
204 205
    required Object? debugOwner,
    PointerDeviceKind? kind,
206
  }) : super(debugOwner: debugOwner, kind: kind);
207

208 209 210 211
  /// Called when this class recognizes the start of a drag gesture.
  ///
  /// The remaining notifications for this drag gesture are delivered to the
  /// [Drag] object returned by this callback.
212
  GestureMultiDragStartCallback? onStart;
213

214
  Map<int, T>? _pointers = <int, T>{};
215

216
  @override
217
  void addAllowedPointer(PointerDownEvent event) {
218 219 220
    assert(_pointers != null);
    assert(event.pointer != null);
    assert(event.position != null);
221
    assert(!_pointers!.containsKey(event.pointer));
222
    final T state = createNewPointerState(event);
223 224 225
    _pointers![event.pointer] = state;
    GestureBinding.instance!.pointerRouter.addRoute(event.pointer, _handleEvent);
    state._setArenaEntry(GestureBinding.instance!.gestureArena.add(event.pointer, this));
226 227
  }

228
  /// Subclasses should override this method to create per-pointer state
229
  /// objects to track the pointer associated with the given event.
230
  @protected
231
  @factory
232 233
  T createNewPointerState(PointerDownEvent event);

234
  void _handleEvent(PointerEvent event) {
235 236 237 238
    assert(_pointers != null);
    assert(event.pointer != null);
    assert(event.timeStamp != null);
    assert(event.position != null);
239 240
    assert(_pointers!.containsKey(event.pointer));
    final T state = _pointers![event.pointer]!;
241 242
    if (event is PointerMoveEvent) {
      state._move(event);
243
      // We might be disposed here.
244 245 246
    } else if (event is PointerUpEvent) {
      assert(event.delta == Offset.zero);
      state._up();
247
      // We might be disposed here.
248 249 250 251
      _removeState(event.pointer);
    } else if (event is PointerCancelEvent) {
      assert(event.delta == Offset.zero);
      state._cancel();
252
      // We might be disposed here.
253 254
      _removeState(event.pointer);
    } else if (event is! PointerDownEvent) {
255
      // we get the PointerDownEvent that resulted in our addPointer getting called since we
256 257 258 259 260 261
      // add ourselves to the pointer router then (before the pointer router has heard of
      // the event).
      assert(false);
    }
  }

262
  @override
263 264
  void acceptGesture(int pointer) {
    assert(_pointers != null);
265
    final T? state = _pointers![pointer];
266 267
    if (state == null)
      return; // We might already have canceled this drag if the up comes before the accept.
268
    state.accepted((Offset initialPosition) => _startDrag(initialPosition, pointer));
269 270
  }

271
  Drag? _startDrag(Offset initialPosition, int pointer) {
272
    assert(_pointers != null);
273
    final T state = _pointers![pointer]!;
274 275
    assert(state != null);
    assert(state._pendingDelta != null);
276
    Drag? drag;
277
    if (onStart != null)
278
      drag = invokeCallback<Drag?>('onStart', () => onStart!(initialPosition));
279
    if (drag != null) {
280
      state._startDrag(drag);
281 282 283
    } else {
      _removeState(pointer);
    }
284
    return drag;
285 286
  }

287
  @override
288 289
  void rejectGesture(int pointer) {
    assert(_pointers != null);
290 291
    if (_pointers!.containsKey(pointer)) {
      final T state = _pointers![pointer]!;
292 293 294 295 296 297 298
      assert(state != null);
      state.rejected();
      _removeState(pointer);
    } // else we already preemptively forgot about it (e.g. we got an up event)
  }

  void _removeState(int pointer) {
299 300 301 302 303
    if (_pointers == null) {
      // We've already been disposed. It's harmless to skip removing the state
      // for the given pointer because dispose() has already removed it.
      return;
    }
304 305 306
    assert(_pointers!.containsKey(pointer));
    GestureBinding.instance!.pointerRouter.removeRoute(pointer, _handleEvent);
    _pointers!.remove(pointer)!.dispose();
307 308
  }

309
  @override
310
  void dispose() {
311 312
    _pointers!.keys.toList().forEach(_removeState);
    assert(_pointers!.isEmpty);
313 314 315 316 317 318
    _pointers = null;
    super.dispose();
  }
}

class _ImmediatePointerState extends MultiDragPointerState {
319
  _ImmediatePointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
320

321
  @override
322 323
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
324
    if (pendingDelta!.distance > computeHitSlop(kind))
325 326
      resolve(GestureDisposition.accepted);
  }
327

328
  @override
329 330 331
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
332 333
}

334 335 336 337 338 339 340 341
/// Recognizes movement both horizontally and vertically on a per-pointer basis.
///
/// In contrast to [PanGestureRecognizer], [ImmediateMultiDragGestureRecognizer]
/// watches each pointer separately, which means multiple drags can be
/// recognized concurrently if multiple pointers are in contact with the screen.
///
/// See also:
///
342 343 344 345 346 347 348 349
///  * [PanGestureRecognizer], which recognizes only one drag gesture at a time,
///    regardless of how many fingers are involved.
///  * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that
///    start horizontally.
///  * [VerticalMultiDragGestureRecognizer], which only recognizes drags that
///    start vertically.
///  * [DelayedMultiDragGestureRecognizer], which only recognizes drags that
///    start after a long-press gesture.
350
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
351
  /// Create a gesture recognizer for tracking multiple pointers at once.
352
  ImmediateMultiDragGestureRecognizer({
353 354
    Object? debugOwner,
    PointerDeviceKind? kind,
355
  }) : super(debugOwner: debugOwner, kind: kind);
356

357
  @override
358
  _ImmediatePointerState createNewPointerState(PointerDownEvent event) {
359
    return _ImmediatePointerState(event.position, event.kind);
360
  }
361

362
  @override
363
  String get debugDescription => 'multidrag';
364 365
}

366 367

class _HorizontalPointerState extends MultiDragPointerState {
368
  _HorizontalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
369

370
  @override
371 372
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
373
    if (pendingDelta!.dx.abs() > computeHitSlop(kind))
374 375 376
      resolve(GestureDisposition.accepted);
  }

377
  @override
378 379 380 381 382
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

383 384 385 386 387 388 389 390 391
/// Recognizes movement in the horizontal direction on a per-pointer basis.
///
/// In contrast to [HorizontalDragGestureRecognizer],
/// [HorizontalMultiDragGestureRecognizer] watches each pointer separately,
/// which means multiple drags can be recognized concurrently if multiple
/// pointers are in contact with the screen.
///
/// See also:
///
392 393 394 395 396 397
///  * [HorizontalDragGestureRecognizer], a gesture recognizer that just
///    looks at horizontal movement.
///  * [ImmediateMultiDragGestureRecognizer], a similar recognizer, but without
///    the limitation that the drag must start horizontally.
///  * [VerticalMultiDragGestureRecognizer], which only recognizes drags that
///    start vertically.
398
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
399 400
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move horizontally.
401
  HorizontalMultiDragGestureRecognizer({
402 403
    Object? debugOwner,
    PointerDeviceKind? kind,
404
  }) : super(debugOwner: debugOwner, kind: kind);
405

406
  @override
407
  _HorizontalPointerState createNewPointerState(PointerDownEvent event) {
408
    return _HorizontalPointerState(event.position, event.kind);
409
  }
410

411
  @override
412
  String get debugDescription => 'horizontal multidrag';
413 414 415 416
}


class _VerticalPointerState extends MultiDragPointerState {
417
  _VerticalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
418

419
  @override
420 421
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
422
    if (pendingDelta!.dy.abs() > computeHitSlop(kind))
423 424 425
      resolve(GestureDisposition.accepted);
  }

426
  @override
427 428 429 430 431
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

432 433 434 435 436 437 438 439 440
/// Recognizes movement in the vertical direction on a per-pointer basis.
///
/// In contrast to [VerticalDragGestureRecognizer],
/// [VerticalMultiDragGestureRecognizer] watches each pointer separately,
/// which means multiple drags can be recognized concurrently if multiple
/// pointers are in contact with the screen.
///
/// See also:
///
441 442 443 444 445 446
///  * [VerticalDragGestureRecognizer], a gesture recognizer that just
///    looks at vertical movement.
///  * [ImmediateMultiDragGestureRecognizer], a similar recognizer, but without
///    the limitation that the drag must start vertically.
///  * [HorizontalMultiDragGestureRecognizer], which only recognizes drags that
///    start horizontally.
447
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
448 449
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move vertically.
450
  VerticalMultiDragGestureRecognizer({
451 452
    Object? debugOwner,
    PointerDeviceKind? kind,
453
  }) : super(debugOwner: debugOwner, kind: kind);
454

455
  @override
456
  _VerticalPointerState createNewPointerState(PointerDownEvent event) {
457
    return _VerticalPointerState(event.position, event.kind);
458
  }
459

460
  @override
461
  String get debugDescription => 'vertical multidrag';
462 463
}

464
class _DelayedPointerState extends MultiDragPointerState {
465
  _DelayedPointerState(Offset initialPosition, Duration delay, PointerDeviceKind kind)
466
      : assert(delay != null),
467
        super(initialPosition, kind) {
468
    _timer = Timer(delay, _delayPassed);
469 470
  }

471 472
  Timer? _timer;
  GestureMultiDragStartCallback? _starter;
473 474 475 476

  void _delayPassed() {
    assert(_timer != null);
    assert(pendingDelta != null);
477
    assert(pendingDelta!.distance <= computeHitSlop(kind));
478
    _timer = null;
479
    if (_starter != null) {
480
      _starter!(initialPosition);
481 482 483 484 485
      _starter = null;
    } else {
      resolve(GestureDisposition.accepted);
    }
    assert(_starter == null);
486 487
  }

488 489 490 491 492
  void _ensureTimerStopped() {
    _timer?.cancel();
    _timer = null;
  }

493
  @override
494 495 496 497 498 499
  void accepted(GestureMultiDragStartCallback starter) {
    assert(_starter == null);
    if (_timer == null)
      starter(initialPosition);
    else
      _starter = starter;
500 501
  }

502
  @override
503
  void checkForResolutionAfterMove() {
504 505 506 507 508 509 510 511 512
    if (_timer == null) {
      // If we've been accepted by the gesture arena but the pointer moves too
      // much before the timer fires, we end up a state where the timer is
      // stopped but we keep getting calls to this function because we never
      // actually started the drag. In this case, _starter will be non-null
      // because we're essentially waiting forever to start the drag.
      assert(_starter != null);
      return;
    }
513
    assert(pendingDelta != null);
514
    if (pendingDelta!.distance > computeHitSlop(kind)) {
515
      resolve(GestureDisposition.rejected);
516 517
      _ensureTimerStopped();
    }
518 519
  }

520
  @override
521
  void dispose() {
522
    _ensureTimerStopped();
523 524 525 526
    super.dispose();
  }
}

527 528
/// Recognizes movement both horizontally and vertically on a per-pointer basis
/// after a delay.
529
///
530
/// In contrast to [ImmediateMultiDragGestureRecognizer],
531 532 533 534 535 536 537 538 539 540
/// [DelayedMultiDragGestureRecognizer] waits for a [delay] before recognizing
/// the drag. If the pointer moves more than [kTouchSlop] before the delay
/// expires, the gesture is not recognized.
///
/// In contrast to [PanGestureRecognizer], [DelayedMultiDragGestureRecognizer]
/// watches each pointer separately, which means multiple drags can be
/// recognized concurrently if multiple pointers are in contact with the screen.
///
/// See also:
///
541 542 543 544
///  * [ImmediateMultiDragGestureRecognizer], a similar recognizer but without
///    the delay.
///  * [PanGestureRecognizer], which recognizes only one drag gesture at a time,
///    regardless of how many fingers are involved.
545
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> {
546 547 548 549 550 551
  /// Creates a drag recognizer that works on a per-pointer basis after a delay.
  ///
  /// In order for a drag to be recognized by this recognizer, the pointer must
  /// remain in the same place for [delay] (up to [kTouchSlop]). The [delay]
  /// defaults to [kLongPressTimeout] to match [LongPressGestureRecognizer] but
  /// can be changed for specific behaviors.
552
  DelayedMultiDragGestureRecognizer({
553
    this.delay = kLongPressTimeout,
554 555
    Object? debugOwner,
    PointerDeviceKind? kind,
556
  }) : assert(delay != null),
557
       super(debugOwner: debugOwner, kind: kind);
558

559 560
  /// The amount of time the pointer must remain in the same place for the drag
  /// to be recognized.
561
  final Duration delay;
562

563
  @override
564
  _DelayedPointerState createNewPointerState(PointerDownEvent event) {
565
    return _DelayedPointerState(event.position, delay, event.kind);
566
  }
567

568
  @override
569
  String get debugDescription => 'long multidrag';
570
}