multidrag.dart 18.3 KB
Newer Older
1 2 3 4 5
// 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.

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

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)
    : assert(initialPosition != null);
32

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

36
  final VelocityTracker _velocityTracker = VelocityTracker();
37 38
  Drag _client;

39 40 41 42 43 44
  /// 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.
45 46 47
  Offset get pendingDelta => _pendingDelta;
  Offset _pendingDelta = Offset.zero;

48 49
  Duration _lastPendingEventTimestamp;

50 51 52 53 54 55 56 57
  GestureArenaEntry _arenaEntry;
  void _setArenaEntry(GestureArenaEntry entry) {
    assert(_arenaEntry == null);
    assert(pendingDelta != null);
    assert(_client == null);
    _arenaEntry = entry;
  }

58
  /// Resolve this pointer's entry in the [GestureArenaManager] with the given disposition.
59
  @protected
60
  @mustCallSuper
61 62 63 64 65 66
  void resolve(GestureDisposition disposition) {
    _arenaEntry.resolve(disposition);
  }

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

  /// Called when the gesture was accepted.
92 93 94
  ///
  /// Either immediately or at some future point before the gesture is disposed,
  /// call starter(), passing it initialPosition, to start the drag.
95
  @protected
96 97 98 99
  void accepted(GestureMultiDragStartCallback starter);

  /// Called when the gesture was rejected.
  ///
100
  /// The [dispose] method will be called immediately following this.
101 102
  @protected
  @mustCallSuper
103
  void rejected() {
104 105
    assert(_arenaEntry != null);
    assert(_client == null);
106
    assert(pendingDelta != null);
107
    _pendingDelta = null;
108
    _lastPendingEventTimestamp = null;
109
    _arenaEntry = null;
110 111
  }

112
  void _startDrag(Drag client) {
113 114
    assert(_arenaEntry != null);
    assert(_client == null);
115
    assert(client != null);
116
    assert(pendingDelta != null);
117
    _client = client;
118
    final DragUpdateDetails details = DragUpdateDetails(
119
      sourceTimeStamp: _lastPendingEventTimestamp,
120 121 122
      delta: pendingDelta,
      globalPosition: initialPosition,
    );
123
    _pendingDelta = null;
124
    _lastPendingEventTimestamp = null;
125 126
    // Call client last to avoid reentrancy.
    _client.update(details);
127 128 129 130 131 132
  }

  void _up() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
133
      final DragEndDetails details = DragEndDetails(velocity: _velocityTracker.getVelocity());
134
      final Drag client = _client;
135
      _client = null;
136 137
      // Call client last to avoid reentrancy.
      client.end(details);
138 139 140
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
141
      _lastPendingEventTimestamp = null;
142 143 144 145 146 147 148
    }
  }

  void _cancel() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
149
      final Drag client = _client;
150
      _client = null;
151 152
      // Call client last to avoid reentrancy.
      client.cancel();
153 154 155
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
156
      _lastPendingEventTimestamp = null;
157 158 159
    }
  }

160
  /// Releases any resources used by the object.
161
  @protected
162
  @mustCallSuper
163
  void dispose() {
164
    _arenaEntry?.resolve(GestureDisposition.rejected);
165
    _arenaEntry = null;
166 167 168 169
    assert(() {
      _pendingDelta = null;
      return true;
    }());
170
  }
171 172
}

173 174
/// Recognizes movement on a per-pointer basis.
///
175 176 177
/// 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.
178 179 180 181 182 183 184
///
/// [MultiDragGestureRecognizer] is not intended to be used directly. Instead,
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
/// See also:
///
185 186 187 188 189 190 191 192
///  * [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.
193
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
194
  /// Initialize the object.
195 196 197 198
  MultiDragGestureRecognizer({
    @required Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
199

200 201 202 203
  /// 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.
204 205 206 207
  GestureMultiDragStartCallback onStart;

  Map<int, T> _pointers = <int, T>{};

208
  @override
209
  void addAllowedPointer(PointerDownEvent event) {
210 211 212 213
    assert(_pointers != null);
    assert(event.pointer != null);
    assert(event.position != null);
    assert(!_pointers.containsKey(event.pointer));
214
    final T state = createNewPointerState(event);
215
    _pointers[event.pointer] = state;
216
    GestureBinding.instance.pointerRouter.addRoute(event.pointer, _handleEvent);
217
    state._setArenaEntry(GestureBinding.instance.gestureArena.add(event.pointer, this));
218 219
  }

220
  /// Subclasses should override this method to create per-pointer state
221
  /// objects to track the pointer associated with the given event.
222
  @protected
223 224
  T createNewPointerState(PointerDownEvent event);

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

253
  @override
254 255
  void acceptGesture(int pointer) {
    assert(_pointers != null);
256
    final T state = _pointers[pointer];
257 258
    if (state == null)
      return; // We might already have canceled this drag if the up comes before the accept.
259
    state.accepted((Offset initialPosition) => _startDrag(initialPosition, pointer));
260 261
  }

262
  Drag _startDrag(Offset initialPosition, int pointer) {
263
    assert(_pointers != null);
264
    final T state = _pointers[pointer];
265 266
    assert(state != null);
    assert(state._pendingDelta != null);
267 268
    Drag drag;
    if (onStart != null)
269
      drag = invokeCallback<Drag>('onStart', () => onStart(initialPosition));
270
    if (drag != null) {
271
      state._startDrag(drag);
272 273 274
    } else {
      _removeState(pointer);
    }
275
    return drag;
276 277
  }

278
  @override
279 280 281
  void rejectGesture(int pointer) {
    assert(_pointers != null);
    if (_pointers.containsKey(pointer)) {
282
      final T state = _pointers[pointer];
283 284 285 286 287 288 289
      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) {
290 291 292 293 294
    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;
    }
295
    assert(_pointers.containsKey(pointer));
296
    GestureBinding.instance.pointerRouter.removeRoute(pointer, _handleEvent);
297
    _pointers.remove(pointer).dispose();
298 299
  }

300
  @override
301
  void dispose() {
302
    _pointers.keys.toList().forEach(_removeState);
303
    assert(_pointers.isEmpty);
304 305 306 307 308 309
    _pointers = null;
    super.dispose();
  }
}

class _ImmediatePointerState extends MultiDragPointerState {
310
  _ImmediatePointerState(Offset initialPosition) : super(initialPosition);
311

312
  @override
313 314 315 316 317
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
    if (pendingDelta.distance > kTouchSlop)
      resolve(GestureDisposition.accepted);
  }
318

319
  @override
320 321 322
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
323 324
}

325 326 327 328 329 330 331 332
/// 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:
///
333 334 335 336 337 338 339 340
///  * [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.
341
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
342
  /// Create a gesture recognizer for tracking multiple pointers at once.
343 344 345 346
  ImmediateMultiDragGestureRecognizer({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
347

348
  @override
349
  _ImmediatePointerState createNewPointerState(PointerDownEvent event) {
350
    return _ImmediatePointerState(event.position);
351
  }
352

353
  @override
354
  String get debugDescription => 'multidrag';
355 356
}

357 358

class _HorizontalPointerState extends MultiDragPointerState {
359
  _HorizontalPointerState(Offset initialPosition) : super(initialPosition);
360

361
  @override
362 363 364 365 366 367
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
    if (pendingDelta.dx.abs() > kTouchSlop)
      resolve(GestureDisposition.accepted);
  }

368
  @override
369 370 371 372 373
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

374 375 376 377 378 379 380 381 382
/// 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:
///
383 384 385 386 387 388
///  * [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.
389
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
390 391
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move horizontally.
392 393 394 395
  HorizontalMultiDragGestureRecognizer({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
396

397
  @override
398
  _HorizontalPointerState createNewPointerState(PointerDownEvent event) {
399
    return _HorizontalPointerState(event.position);
400
  }
401

402
  @override
403
  String get debugDescription => 'horizontal multidrag';
404 405 406 407
}


class _VerticalPointerState extends MultiDragPointerState {
408
  _VerticalPointerState(Offset initialPosition) : super(initialPosition);
409

410
  @override
411 412 413 414 415 416
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
    if (pendingDelta.dy.abs() > kTouchSlop)
      resolve(GestureDisposition.accepted);
  }

417
  @override
418 419 420 421 422
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

423 424 425 426 427 428 429 430 431
/// 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:
///
432 433 434 435 436 437
///  * [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.
438
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
439 440
  /// Create a gesture recognizer for tracking multiple pointers at once
  /// but only if they first move vertically.
441 442 443 444
  VerticalMultiDragGestureRecognizer({
    Object debugOwner,
    PointerDeviceKind kind,
  }) : super(debugOwner: debugOwner, kind: kind);
445

446
  @override
447
  _VerticalPointerState createNewPointerState(PointerDownEvent event) {
448
    return _VerticalPointerState(event.position);
449
  }
450

451
  @override
452
  String get debugDescription => 'vertical multidrag';
453 454
}

455
class _DelayedPointerState extends MultiDragPointerState {
456
  _DelayedPointerState(Offset initialPosition, Duration delay)
457 458
      : assert(delay != null),
        super(initialPosition) {
459
    _timer = Timer(delay, _delayPassed);
460 461 462
  }

  Timer _timer;
463
  GestureMultiDragStartCallback _starter;
464 465 466 467 468 469

  void _delayPassed() {
    assert(_timer != null);
    assert(pendingDelta != null);
    assert(pendingDelta.distance <= kTouchSlop);
    _timer = null;
470 471 472 473 474 475 476
    if (_starter != null) {
      _starter(initialPosition);
      _starter = null;
    } else {
      resolve(GestureDisposition.accepted);
    }
    assert(_starter == null);
477 478
  }

479 480 481 482 483
  void _ensureTimerStopped() {
    _timer?.cancel();
    _timer = null;
  }

484
  @override
485 486 487 488 489 490
  void accepted(GestureMultiDragStartCallback starter) {
    assert(_starter == null);
    if (_timer == null)
      starter(initialPosition);
    else
      _starter = starter;
491 492
  }

493
  @override
494
  void checkForResolutionAfterMove() {
495 496 497 498 499 500 501 502 503
    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;
    }
504
    assert(pendingDelta != null);
505
    if (pendingDelta.distance > kTouchSlop) {
506
      resolve(GestureDisposition.rejected);
507 508
      _ensureTimerStopped();
    }
509 510
  }

511
  @override
512
  void dispose() {
513
    _ensureTimerStopped();
514 515 516 517
    super.dispose();
  }
}

518 519
/// Recognizes movement both horizontally and vertically on a per-pointer basis
/// after a delay.
520
///
521
/// In contrast to [ImmediateMultiDragGestureRecognizer],
522 523 524 525 526 527 528 529 530 531
/// [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:
///
532 533 534 535
///  * [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.
536
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> {
537 538 539 540 541 542
  /// 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.
543
  DelayedMultiDragGestureRecognizer({
544
    this.delay = kLongPressTimeout,
545
    Object debugOwner,
546
    PointerDeviceKind kind,
547
  }) : assert(delay != null),
548
       super(debugOwner: debugOwner, kind: kind);
549

550 551
  /// The amount of time the pointer must remain in the same place for the drag
  /// to be recognized.
552
  final Duration delay;
553

554
  @override
555
  _DelayedPointerState createNewPointerState(PointerDownEvent event) {
556
    return _DelayedPointerState(event.position, delay);
557
  }
558

559
  @override
560
  String get debugDescription => 'long multidrag';
561
}