multidrag.dart 20.4 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 7
import 'dart:async';

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

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

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

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

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

37 38 39 40 41 42 43
  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;

44
  Drag? _client;
45

46 47 48 49 50 51
  /// 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.
52 53
  Offset? get pendingDelta => _pendingDelta;
  Offset? _pendingDelta = Offset.zero;
54

55
  Duration? _lastPendingEventTimestamp;
56

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

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

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

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

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

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

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

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

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

180 181
/// Recognizes movement on a per-pointer basis.
///
182 183 184
/// 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.
185 186 187 188 189 190 191
///
/// [MultiDragGestureRecognizer] is not intended to be used directly. Instead,
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
/// See also:
///
192 193 194 195 196 197 198 199
///  * [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.
200
abstract class MultiDragGestureRecognizer extends GestureRecognizer {
201
  /// Initialize the object.
202 203
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
204
  MultiDragGestureRecognizer({
205
    required Object? debugOwner,
206 207 208 209
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
210
    PointerDeviceKind? kind,
211 212 213 214 215 216
    Set<PointerDeviceKind>? supportedDevices,
  }) : super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
217

218 219 220 221
  /// 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.
222
  GestureMultiDragStartCallback? onStart;
223

224
  Map<int, MultiDragPointerState>? _pointers = <int, MultiDragPointerState>{};
225

226
  @override
227
  void addAllowedPointer(PointerDownEvent event) {
228 229 230
    assert(_pointers != null);
    assert(event.pointer != null);
    assert(event.position != null);
231
    assert(!_pointers!.containsKey(event.pointer));
232
    final MultiDragPointerState state = createNewPointerState(event);
233 234 235
    _pointers![event.pointer] = state;
    GestureBinding.instance!.pointerRouter.addRoute(event.pointer, _handleEvent);
    state._setArenaEntry(GestureBinding.instance!.gestureArena.add(event.pointer, this));
236 237
  }

238
  /// Subclasses should override this method to create per-pointer state
239
  /// objects to track the pointer associated with the given event.
240
  @protected
241
  @factory
242
  MultiDragPointerState createNewPointerState(PointerDownEvent event);
243

244
  void _handleEvent(PointerEvent event) {
245 246 247 248
    assert(_pointers != null);
    assert(event.pointer != null);
    assert(event.timeStamp != null);
    assert(event.position != null);
249
    assert(_pointers!.containsKey(event.pointer));
250
    final MultiDragPointerState state = _pointers![event.pointer]!;
251 252
    if (event is PointerMoveEvent) {
      state._move(event);
253
      // We might be disposed here.
254 255 256
    } else if (event is PointerUpEvent) {
      assert(event.delta == Offset.zero);
      state._up();
257
      // We might be disposed here.
258 259 260 261
      _removeState(event.pointer);
    } else if (event is PointerCancelEvent) {
      assert(event.delta == Offset.zero);
      state._cancel();
262
      // We might be disposed here.
263 264
      _removeState(event.pointer);
    } else if (event is! PointerDownEvent) {
265
      // we get the PointerDownEvent that resulted in our addPointer getting called since we
266 267 268 269 270 271
      // add ourselves to the pointer router then (before the pointer router has heard of
      // the event).
      assert(false);
    }
  }

272
  @override
273 274
  void acceptGesture(int pointer) {
    assert(_pointers != null);
275
    final MultiDragPointerState? state = _pointers![pointer];
276 277
    if (state == null)
      return; // We might already have canceled this drag if the up comes before the accept.
278
    state.accepted((Offset initialPosition) => _startDrag(initialPosition, pointer));
279 280
  }

281
  Drag? _startDrag(Offset initialPosition, int pointer) {
282
    assert(_pointers != null);
283
    final MultiDragPointerState state = _pointers![pointer]!;
284 285
    assert(state != null);
    assert(state._pendingDelta != null);
286
    Drag? drag;
287
    if (onStart != null)
288
      drag = invokeCallback<Drag?>('onStart', () => onStart!(initialPosition));
289
    if (drag != null) {
290
      state._startDrag(drag);
291 292 293
    } else {
      _removeState(pointer);
    }
294
    return drag;
295 296
  }

297
  @override
298 299
  void rejectGesture(int pointer) {
    assert(_pointers != null);
300
    if (_pointers!.containsKey(pointer)) {
301
      final MultiDragPointerState state = _pointers![pointer]!;
302 303 304 305 306 307 308
      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) {
309 310 311 312 313
    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;
    }
314 315 316
    assert(_pointers!.containsKey(pointer));
    GestureBinding.instance!.pointerRouter.removeRoute(pointer, _handleEvent);
    _pointers!.remove(pointer)!.dispose();
317 318
  }

319
  @override
320
  void dispose() {
321 322
    _pointers!.keys.toList().forEach(_removeState);
    assert(_pointers!.isEmpty);
323 324 325 326 327 328
    _pointers = null;
    super.dispose();
  }
}

class _ImmediatePointerState extends MultiDragPointerState {
329
  _ImmediatePointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
330

331
  @override
332 333
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
334
    if (pendingDelta!.distance > computeHitSlop(kind))
335 336
      resolve(GestureDisposition.accepted);
  }
337

338
  @override
339 340 341
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
342 343
}

344 345 346 347 348 349 350 351
/// 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:
///
352 353 354 355 356 357 358 359
///  * [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.
360
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
361
  /// Create a gesture recognizer for tracking multiple pointers at once.
362 363
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
364
  ImmediateMultiDragGestureRecognizer({
365
    Object? debugOwner,
366 367 368 369
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
370
    PointerDeviceKind? kind,
371 372 373 374 375 376
    Set<PointerDeviceKind>? supportedDevices,
  }) : super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
377

378
  @override
379
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
380
    return _ImmediatePointerState(event.position, event.kind);
381
  }
382

383
  @override
384
  String get debugDescription => 'multidrag';
385 386
}

387 388

class _HorizontalPointerState extends MultiDragPointerState {
389
  _HorizontalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
390

391
  @override
392 393
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
394
    if (pendingDelta!.dx.abs() > computeHitSlop(kind))
395 396 397
      resolve(GestureDisposition.accepted);
  }

398
  @override
399 400 401 402 403
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

404 405 406 407 408 409 410 411 412
/// 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:
///
413 414 415 416 417 418
///  * [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.
419
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
420 421
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move horizontally.
422 423
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
424
  HorizontalMultiDragGestureRecognizer({
425
    Object? debugOwner,
426 427 428 429
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
430
    PointerDeviceKind? kind,
431 432 433 434 435 436
    Set<PointerDeviceKind>? supportedDevices,
  }) : super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
437

438
  @override
439
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
440
    return _HorizontalPointerState(event.position, event.kind);
441
  }
442

443
  @override
444
  String get debugDescription => 'horizontal multidrag';
445 446 447 448
}


class _VerticalPointerState extends MultiDragPointerState {
449
  _VerticalPointerState(Offset initialPosition, PointerDeviceKind kind) : super(initialPosition, kind);
450

451
  @override
452 453
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
454
    if (pendingDelta!.dy.abs() > computeHitSlop(kind))
455 456 457
      resolve(GestureDisposition.accepted);
  }

458
  @override
459 460 461 462 463
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

464 465 466 467 468 469 470 471 472
/// 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:
///
473 474 475 476 477 478
///  * [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.
479
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
480 481
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move vertically.
482 483
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
484
  VerticalMultiDragGestureRecognizer({
485
    Object? debugOwner,
486 487 488 489
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
490
    PointerDeviceKind? kind,
491 492 493 494 495 496
    Set<PointerDeviceKind>? supportedDevices,
  }) : super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
497

498
  @override
499
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
500
    return _VerticalPointerState(event.position, event.kind);
501
  }
502

503
  @override
504
  String get debugDescription => 'vertical multidrag';
505 506
}

507
class _DelayedPointerState extends MultiDragPointerState {
508
  _DelayedPointerState(Offset initialPosition, Duration delay, PointerDeviceKind kind)
509
      : assert(delay != null),
510
        super(initialPosition, kind) {
511
    _timer = Timer(delay, _delayPassed);
512 513
  }

514 515
  Timer? _timer;
  GestureMultiDragStartCallback? _starter;
516 517 518 519

  void _delayPassed() {
    assert(_timer != null);
    assert(pendingDelta != null);
520
    assert(pendingDelta!.distance <= computeHitSlop(kind));
521
    _timer = null;
522
    if (_starter != null) {
523
      _starter!(initialPosition);
524 525 526 527 528
      _starter = null;
    } else {
      resolve(GestureDisposition.accepted);
    }
    assert(_starter == null);
529 530
  }

531 532 533 534 535
  void _ensureTimerStopped() {
    _timer?.cancel();
    _timer = null;
  }

536
  @override
537 538 539 540 541 542
  void accepted(GestureMultiDragStartCallback starter) {
    assert(_starter == null);
    if (_timer == null)
      starter(initialPosition);
    else
      _starter = starter;
543 544
  }

545
  @override
546
  void checkForResolutionAfterMove() {
547 548 549 550 551 552 553 554 555
    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;
    }
556
    assert(pendingDelta != null);
557
    if (pendingDelta!.distance > computeHitSlop(kind)) {
558
      resolve(GestureDisposition.rejected);
559 560
      _ensureTimerStopped();
    }
561 562
  }

563
  @override
564
  void dispose() {
565
    _ensureTimerStopped();
566 567 568 569
    super.dispose();
  }
}

570 571
/// Recognizes movement both horizontally and vertically on a per-pointer basis
/// after a delay.
572
///
573
/// In contrast to [ImmediateMultiDragGestureRecognizer],
574 575 576 577 578 579 580 581 582 583
/// [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:
///
584 585 586 587
///  * [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.
588
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
589 590 591 592 593 594
  /// 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.
595 596
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
597
  DelayedMultiDragGestureRecognizer({
598
    this.delay = kLongPressTimeout,
599
    Object? debugOwner,
600 601 602 603
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
604
    PointerDeviceKind? kind,
605
    Set<PointerDeviceKind>? supportedDevices,
606
  }) : assert(delay != null),
607 608 609 610 611
       super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
612

613 614
  /// The amount of time the pointer must remain in the same place for the drag
  /// to be recognized.
615
  final Duration delay;
616

617
  @override
618
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
619
    return _DelayedPointerState(event.position, delay, event.kind);
620
  }
621

622
  @override
623
  String get debugDescription => 'long multidrag';
624
}