multidrag.dart 19.6 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 20 21 22 23 24 25
export 'dart:ui' show Offset, PointerDeviceKind;

export 'arena.dart' show GestureDisposition;
export 'drag.dart' show Drag;
export 'events.dart' show PointerDownEvent;
export 'gesture_settings.dart' show DeviceGestureSettings;

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

29 30 31 32
/// Per-pointer state for a [MultiDragGestureRecognizer].
///
/// A [MultiDragGestureRecognizer] tracks each pointer separately. The state for
/// each pointer is a subclass of [MultiDragPointerState].
33
abstract class MultiDragPointerState {
34 35 36
  /// Creates per-pointer state for a [MultiDragGestureRecognizer].
  ///
  /// The [initialPosition] argument must not be null.
37
  MultiDragPointerState(this.initialPosition, this.kind, this.gestureSettings)
38
    : _velocityTracker = VelocityTracker.withKind(kind);
39

40 41 42 43 44 45
  /// Device specific gesture configuration that should be preferred over
  /// framework constants.
  ///
  /// These settings are commonly retrieved from a [MediaQuery].
  final DeviceGestureSettings? gestureSettings;

46
  /// The global coordinates of the pointer when the pointer contacted the screen.
47
  final Offset initialPosition;
48

49 50 51 52 53 54 55
  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;

56
  Drag? _client;
57

58 59 60 61 62 63
  /// 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.
64 65
  Offset? get pendingDelta => _pendingDelta;
  Offset? _pendingDelta = Offset.zero;
66

67
  Duration? _lastPendingEventTimestamp;
68

69
  GestureArenaEntry? _arenaEntry;
70 71 72 73 74 75 76
  void _setArenaEntry(GestureArenaEntry entry) {
    assert(_arenaEntry == null);
    assert(pendingDelta != null);
    assert(_client == null);
    _arenaEntry = entry;
  }

77
  /// Resolve this pointer's entry in the [GestureArenaManager] with the given disposition.
78
  @protected
79
  @mustCallSuper
80
  void resolve(GestureDisposition disposition) {
81
    _arenaEntry!.resolve(disposition);
82 83 84 85
  }

  void _move(PointerMoveEvent event) {
    assert(_arenaEntry != null);
86
    if (!event.synthesized) {
87
      _velocityTracker.addPosition(event.timeStamp, event.position);
88
    }
89 90
    if (_client != null) {
      assert(pendingDelta == null);
91
      // Call client last to avoid reentrancy.
92
      _client!.update(DragUpdateDetails(
93
        sourceTimeStamp: event.timeStamp,
94 95 96
        delta: event.delta,
        globalPosition: event.position,
      ));
97 98
    } else {
      assert(pendingDelta != null);
99
      _pendingDelta = _pendingDelta! + event.delta;
100
      _lastPendingEventTimestamp = event.timeStamp;
101 102 103 104 105 106 107
      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.
108
  @protected
109 110 111
  void checkForResolutionAfterMove() { }

  /// Called when the gesture was accepted.
112 113 114
  ///
  /// Either immediately or at some future point before the gesture is disposed,
  /// call starter(), passing it initialPosition, to start the drag.
115
  @protected
116 117 118 119
  void accepted(GestureMultiDragStartCallback starter);

  /// Called when the gesture was rejected.
  ///
120
  /// The [dispose] method will be called immediately following this.
121 122
  @protected
  @mustCallSuper
123
  void rejected() {
124 125
    assert(_arenaEntry != null);
    assert(_client == null);
126
    assert(pendingDelta != null);
127
    _pendingDelta = null;
128
    _lastPendingEventTimestamp = null;
129
    _arenaEntry = null;
130 131
  }

132
  void _startDrag(Drag client) {
133 134 135
    assert(_arenaEntry != null);
    assert(_client == null);
    assert(pendingDelta != null);
136
    _client = client;
137
    final DragUpdateDetails details = DragUpdateDetails(
138
      sourceTimeStamp: _lastPendingEventTimestamp,
139
      delta: pendingDelta!,
140 141
      globalPosition: initialPosition,
    );
142
    _pendingDelta = null;
143
    _lastPendingEventTimestamp = null;
144
    // Call client last to avoid reentrancy.
145
    _client!.update(details);
146 147 148 149 150 151
  }

  void _up() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
152
      final DragEndDetails details = DragEndDetails(velocity: _velocityTracker.getVelocity());
153
      final Drag client = _client!;
154
      _client = null;
155 156
      // Call client last to avoid reentrancy.
      client.end(details);
157 158 159
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
160
      _lastPendingEventTimestamp = null;
161 162 163 164 165 166 167
    }
  }

  void _cancel() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
168
      final Drag client = _client!;
169
      _client = null;
170 171
      // Call client last to avoid reentrancy.
      client.cancel();
172 173 174
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
175
      _lastPendingEventTimestamp = null;
176 177 178
    }
  }

179
  /// Releases any resources used by the object.
180
  @protected
181
  @mustCallSuper
182
  void dispose() {
183
    _arenaEntry?.resolve(GestureDisposition.rejected);
184
    _arenaEntry = null;
185 186 187 188
    assert(() {
      _pendingDelta = null;
      return true;
    }());
189
  }
190 191
}

192 193
/// Recognizes movement on a per-pointer basis.
///
194 195 196
/// 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.
197 198 199 200 201 202 203
///
/// [MultiDragGestureRecognizer] is not intended to be used directly. Instead,
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
/// See also:
///
204 205 206 207 208 209 210 211
///  * [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.
212
abstract class MultiDragGestureRecognizer extends GestureRecognizer {
213
  /// Initialize the object.
214 215
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
216
  MultiDragGestureRecognizer({
217 218
    required super.debugOwner,
    super.supportedDevices,
219 220
    AllowedButtonsFilter? allowedButtonsFilter,
  }) : super(allowedButtonsFilter: allowedButtonsFilter ?? _defaultButtonAcceptBehavior);
221

222 223 224
  // Accept the input if, and only if, [kPrimaryButton] is pressed.
  static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;

225 226 227 228
  /// 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.
229
  GestureMultiDragStartCallback? onStart;
230

231
  Map<int, MultiDragPointerState>? _pointers = <int, MultiDragPointerState>{};
232

233
  @override
234
  void addAllowedPointer(PointerDownEvent event) {
235
    assert(_pointers != null);
236
    assert(!_pointers!.containsKey(event.pointer));
237
    final MultiDragPointerState state = createNewPointerState(event);
238
    _pointers![event.pointer] = state;
239 240
    GestureBinding.instance.pointerRouter.addRoute(event.pointer, _handleEvent);
    state._setArenaEntry(GestureBinding.instance.gestureArena.add(event.pointer, this));
241 242
  }

243
  /// Subclasses should override this method to create per-pointer state
244
  /// objects to track the pointer associated with the given event.
245
  @protected
246
  @factory
247
  MultiDragPointerState createNewPointerState(PointerDownEvent event);
248

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

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

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

300
  @override
301 302
  void rejectGesture(int pointer) {
    assert(_pointers != null);
303
    if (_pointers!.containsKey(pointer)) {
304
      final MultiDragPointerState state = _pointers![pointer]!;
305 306 307 308 309 310
      state.rejected();
      _removeState(pointer);
    } // else we already preemptively forgot about it (e.g. we got an up event)
  }

  void _removeState(int pointer) {
311 312 313 314 315
    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;
    }
316
    assert(_pointers!.containsKey(pointer));
317
    GestureBinding.instance.pointerRouter.removeRoute(pointer, _handleEvent);
318
    _pointers!.remove(pointer)!.dispose();
319 320
  }

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

class _ImmediatePointerState extends MultiDragPointerState {
331
  _ImmediatePointerState(super.initialPosition, super.kind, super.gestureSettings);
332

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

341
  @override
342 343 344
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
345 346
}

347 348 349 350 351 352 353 354
/// 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:
///
355 356 357 358 359 360 361 362
///  * [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.
363
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
364
  /// Create a gesture recognizer for tracking multiple pointers at once.
365 366
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
367
  ImmediateMultiDragGestureRecognizer({
368 369
    super.debugOwner,
    super.supportedDevices,
370
    super.allowedButtonsFilter,
371
  });
372

373
  @override
374
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
375
    return _ImmediatePointerState(event.position, event.kind, gestureSettings);
376
  }
377

378
  @override
379
  String get debugDescription => 'multidrag';
380 381
}

382 383

class _HorizontalPointerState extends MultiDragPointerState {
384
  _HorizontalPointerState(super.initialPosition, super.kind, super.gestureSettings);
385

386
  @override
387 388
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
389
    if (pendingDelta!.dx.abs() > computeHitSlop(kind, gestureSettings)) {
390
      resolve(GestureDisposition.accepted);
391
    }
392 393
  }

394
  @override
395 396 397 398 399
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

400 401 402 403 404 405 406 407 408
/// 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:
///
409 410 411 412 413 414
///  * [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.
415
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
416 417
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move horizontally.
418 419
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
420
  HorizontalMultiDragGestureRecognizer({
421 422
    super.debugOwner,
    super.supportedDevices,
423
    super.allowedButtonsFilter,
424
  });
425

426
  @override
427
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
428
    return _HorizontalPointerState(event.position, event.kind, gestureSettings);
429
  }
430

431
  @override
432
  String get debugDescription => 'horizontal multidrag';
433 434 435 436
}


class _VerticalPointerState extends MultiDragPointerState {
437
  _VerticalPointerState(super.initialPosition, super.kind, super.gestureSettings);
438

439
  @override
440 441
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
442
    if (pendingDelta!.dy.abs() > computeHitSlop(kind, gestureSettings)) {
443
      resolve(GestureDisposition.accepted);
444
    }
445 446
  }

447
  @override
448 449 450 451 452
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

453 454 455 456 457 458 459 460 461
/// 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:
///
462 463 464 465 466 467
///  * [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.
468
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
469 470
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move vertically.
471 472
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
473
  VerticalMultiDragGestureRecognizer({
474 475
    super.debugOwner,
    super.supportedDevices,
476
    super.allowedButtonsFilter,
477
  });
478

479
  @override
480
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
481
    return _VerticalPointerState(event.position, event.kind, gestureSettings);
482
  }
483

484
  @override
485
  String get debugDescription => 'vertical multidrag';
486 487
}

488
class _DelayedPointerState extends MultiDragPointerState {
489
  _DelayedPointerState(super.initialPosition, Duration delay, super.kind, super.gestureSettings) {
490
    _timer = Timer(delay, _delayPassed);
491 492
  }

493 494
  Timer? _timer;
  GestureMultiDragStartCallback? _starter;
495 496 497 498

  void _delayPassed() {
    assert(_timer != null);
    assert(pendingDelta != null);
499
    assert(pendingDelta!.distance <= computeHitSlop(kind, gestureSettings));
500
    _timer = null;
501
    if (_starter != null) {
502
      _starter!(initialPosition);
503 504 505 506 507
      _starter = null;
    } else {
      resolve(GestureDisposition.accepted);
    }
    assert(_starter == null);
508 509
  }

510 511 512 513 514
  void _ensureTimerStopped() {
    _timer?.cancel();
    _timer = null;
  }

515
  @override
516 517
  void accepted(GestureMultiDragStartCallback starter) {
    assert(_starter == null);
518
    if (_timer == null) {
519
      starter(initialPosition);
520
    } else {
521
      _starter = starter;
522
    }
523 524
  }

525
  @override
526
  void checkForResolutionAfterMove() {
527 528 529 530 531 532 533 534 535
    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;
    }
536
    assert(pendingDelta != null);
537
    if (pendingDelta!.distance > computeHitSlop(kind, gestureSettings)) {
538
      resolve(GestureDisposition.rejected);
539 540
      _ensureTimerStopped();
    }
541 542
  }

543
  @override
544
  void dispose() {
545
    _ensureTimerStopped();
546 547 548 549
    super.dispose();
  }
}

550 551
/// Recognizes movement both horizontally and vertically on a per-pointer basis
/// after a delay.
552
///
553
/// In contrast to [ImmediateMultiDragGestureRecognizer],
554 555 556 557 558 559 560 561 562 563
/// [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:
///
564 565 566 567
///  * [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.
568
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
569 570 571 572 573 574
  /// 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.
575 576
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
577
  DelayedMultiDragGestureRecognizer({
578
    this.delay = kLongPressTimeout,
579 580
    super.debugOwner,
    super.supportedDevices,
581
    super.allowedButtonsFilter,
582
  });
583

584 585
  /// The amount of time the pointer must remain in the same place for the drag
  /// to be recognized.
586
  final Duration delay;
587

588
  @override
589
  MultiDragPointerState createNewPointerState(PointerDownEvent event) {
590
    return _DelayedPointerState(event.position, delay, event.kind, gestureSettings);
591
  }
592

593
  @override
594
  String get debugDescription => 'long multidrag';
595
}