multidrag.dart 14.8 KB
Newer Older
1 2 3 4 5 6 7
// 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';
import 'dart:ui' show Point, Offset;

8 9
import 'package:meta/meta.dart';

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

18
/// Signature for when [MultiDragGestureRecognizer] recognizes the start of a drag gesture.
19 20
typedef Drag GestureMultiDragStartCallback(Point position);

21 22
/// Interface for receiving updates about drags from a [MultiDragGestureRecognizer].
abstract class Drag {
23 24
  /// The pointer has moved.
  void update(DragUpdateDetails details) { }
25

26 27 28 29 30
  /// The pointer is no longer in contact with the screen.
  ///
  /// The velocity at which the pointer was moving when it stopped contacting
  /// the screen is available in the `details`.
  void end(DragEndDetails details) { }
31 32 33 34 35

  /// The input from the pointer is no longer directed towards this receiver.
  ///
  /// For example, the user might have been interrupted by a system-modal dialog
  /// in the middle of the drag.
36 37 38
  void cancel() { }
}

39 40 41 42
/// Per-pointer state for a [MultiDragGestureRecognizer].
///
/// A [MultiDragGestureRecognizer] tracks each pointer separately. The state for
/// each pointer is a subclass of [MultiDragPointerState].
43
abstract class MultiDragPointerState {
44 45 46 47 48 49
  /// Creates per-pointer state for a [MultiDragGestureRecognizer].
  ///
  /// The [initialPosition] argument must not be null.
  MultiDragPointerState(this.initialPosition) {
    assert(initialPosition != null);
  }
50

51
  /// The global coordinates of the pointer when the pointer contacted the screen.
52 53 54 55 56
  final Point initialPosition;

  final VelocityTracker _velocityTracker = new VelocityTracker();
  Drag _client;

57 58 59 60 61 62
  /// 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.
63 64 65 66 67 68 69 70 71 72 73
  Offset get pendingDelta => _pendingDelta;
  Offset _pendingDelta = Offset.zero;

  GestureArenaEntry _arenaEntry;
  void _setArenaEntry(GestureArenaEntry entry) {
    assert(_arenaEntry == null);
    assert(pendingDelta != null);
    assert(_client == null);
    _arenaEntry = entry;
  }

74
  /// Resolve this pointer's entry in the [GestureArenaManager] with the given disposition.
75 76 77 78 79 80 81 82 83
  void resolve(GestureDisposition disposition) {
    _arenaEntry.resolve(disposition);
  }

  void _move(PointerMoveEvent event) {
    assert(_arenaEntry != null);
    _velocityTracker.addPosition(event.timeStamp, event.position);
    if (_client != null) {
      assert(pendingDelta == null);
84
      _client.update(new DragUpdateDetails(delta: event.delta));
85 86 87 88 89 90 91 92 93 94 95 96 97 98
    } else {
      assert(pendingDelta != null);
      _pendingDelta += event.delta;
      checkForResolutionAfterMove();
    }
    return null;
  }

  /// 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.
  void checkForResolutionAfterMove() { }

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

  /// Called when the gesture was rejected.
  ///
  /// [dispose()] will be called immediately following this.
  void rejected() {
108 109
    assert(_arenaEntry != null);
    assert(_client == null);
110
    assert(pendingDelta != null);
111
    _pendingDelta = null;
112
    _arenaEntry = null;
113 114
  }

115
  void _startDrag(Drag client) {
116 117
    assert(_arenaEntry != null);
    assert(_client == null);
118
    assert(client != null);
119
    assert(pendingDelta != null);
120
    _client = client;
121
    _client.update(new DragUpdateDetails(delta: pendingDelta));
122 123 124 125 126 127 128
    _pendingDelta = null;
  }

  void _up() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
129
      _client.end(new DragEndDetails(velocity: _velocityTracker.getVelocity() ?? Velocity.zero));
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
      _client = null;
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
    }
    _arenaEntry = null;
  }

  void _cancel() {
    assert(_arenaEntry != null);
    if (_client != null) {
      assert(pendingDelta == null);
      _client.cancel();
      _client = null;
    } else {
      assert(pendingDelta != null);
      _pendingDelta = null;
    }
    _arenaEntry = null;
  }

151
  /// Releases any resources used by the object.
152
  @mustCallSuper
153
  void dispose() {
154
    _arenaEntry?.resolve(GestureDisposition.rejected);
155 156
    assert(() { _pendingDelta = null; return true; });
  }
157 158
}

159 160
/// Recognizes movement on a per-pointer basis.
///
161 162 163
/// 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.
164 165 166 167 168 169 170 171 172 173 174
///
/// [MultiDragGestureRecognizer] is not intended to be used directly. Instead,
/// consider using one of its subclasses to recognize specific types for drag
/// gestures.
///
/// See also:
///
///  * [HorizontalMultiDragGestureRecognizer]
///  * [VerticalMultiDragGestureRecognizer]
///  * [ImmediateMultiDragGestureRecognizer]
///  * [DelayedMultiDragGestureRecognizer]
175
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
176 177 178 179
  /// 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.
180 181 182 183
  GestureMultiDragStartCallback onStart;

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

184
  @override
185 186 187 188 189 190 191
  void addPointer(PointerDownEvent event) {
    assert(_pointers != null);
    assert(event.pointer != null);
    assert(event.position != null);
    assert(!_pointers.containsKey(event.pointer));
    T state = createNewPointerState(event);
    _pointers[event.pointer] = state;
192
    GestureBinding.instance.pointerRouter.addRoute(event.pointer, _handleEvent);
193
    state._setArenaEntry(GestureBinding.instance.gestureArena.add(event.pointer, this));
194 195
  }

196
  /// Subclasses should override this method to create per-pointer state
197
  /// objects to track the pointer associated with the given event.
198 199
  T createNewPointerState(PointerDownEvent event);

200
  void _handleEvent(PointerEvent event) {
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
    assert(_pointers != null);
    assert(event.pointer != null);
    assert(event.timeStamp != null);
    assert(event.position != null);
    assert(_pointers.containsKey(event.pointer));
    T state = _pointers[event.pointer];
    if (event is PointerMoveEvent) {
      state._move(event);
    } else if (event is PointerUpEvent) {
      assert(event.delta == Offset.zero);
      state._up();
      _removeState(event.pointer);
    } else if (event is PointerCancelEvent) {
      assert(event.delta == Offset.zero);
      state._cancel();
      _removeState(event.pointer);
    } else if (event is! PointerDownEvent) {
      // we get the PointerDownEvent that resulted in our addPointer gettig called since we
      // add ourselves to the pointer router then (before the pointer router has heard of
      // the event).
      assert(false);
    }
  }

225
  @override
226 227 228
  void acceptGesture(int pointer) {
    assert(_pointers != null);
    T state = _pointers[pointer];
229 230
    if (state == null)
      return; // We might already have canceled this drag if the up comes before the accept.
231 232 233 234 235 236 237 238
    state.accepted((Point initialPosition) => _startDrag(initialPosition, pointer));
  }

  Drag _startDrag(Point initialPosition, int pointer) {
    assert(_pointers != null);
    T state = _pointers[pointer];
    assert(state != null);
    assert(state._pendingDelta != null);
239 240
    Drag drag;
    if (onStart != null)
241
      drag = onStart(initialPosition);
242
    if (drag != null) {
243
      state._startDrag(drag);
244 245 246
    } else {
      _removeState(pointer);
    }
247
    return drag;
248 249
  }

250
  @override
251 252 253 254 255 256 257 258 259 260 261 262 263
  void rejectGesture(int pointer) {
    assert(_pointers != null);
    if (_pointers.containsKey(pointer)) {
      T state = _pointers[pointer];
      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) {
    assert(_pointers != null);
    assert(_pointers.containsKey(pointer));
264
    GestureBinding.instance.pointerRouter.removeRoute(pointer, _handleEvent);
265
    _pointers.remove(pointer).dispose();
266 267
  }

268
  @override
269
  void dispose() {
270
    for (int pointer in _pointers.keys.toList())
271
      _removeState(pointer);
272
    assert(_pointers.isEmpty);
273 274 275 276 277 278 279 280
    _pointers = null;
    super.dispose();
  }
}

class _ImmediatePointerState extends MultiDragPointerState {
  _ImmediatePointerState(Point initialPosition) : super(initialPosition);

281
  @override
282 283 284 285 286
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
    if (pendingDelta.distance > kTouchSlop)
      resolve(GestureDisposition.accepted);
  }
287

288
  @override
289 290 291
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
292 293
}

294 295 296 297 298 299 300 301 302 303
/// 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:
///
///  * [PanGestureRecognizer]
///  * [DelayedMultiDragGestureRecognizer]
304
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
305
  @override
306 307 308
  _ImmediatePointerState createNewPointerState(PointerDownEvent event) {
    return new _ImmediatePointerState(event.position);
  }
309

310
  @override
311
  String toStringShort() => 'multidrag';
312 313
}

314 315 316 317

class _HorizontalPointerState extends MultiDragPointerState {
  _HorizontalPointerState(Point initialPosition) : super(initialPosition);

318
  @override
319 320 321 322 323 324
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
    if (pendingDelta.dx.abs() > kTouchSlop)
      resolve(GestureDisposition.accepted);
  }

325
  @override
326 327 328 329 330
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

331 332 333 334 335 336 337 338 339 340
/// 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:
///
///  * [HorizontalDragGestureRecognizer]
341
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
342
  @override
343 344 345
  _HorizontalPointerState createNewPointerState(PointerDownEvent event) {
    return new _HorizontalPointerState(event.position);
  }
346

347
  @override
348
  String toStringShort() => 'horizontal multidrag';
349 350 351 352 353 354
}


class _VerticalPointerState extends MultiDragPointerState {
  _VerticalPointerState(Point initialPosition) : super(initialPosition);

355
  @override
356 357 358 359 360 361
  void checkForResolutionAfterMove() {
    assert(pendingDelta != null);
    if (pendingDelta.dy.abs() > kTouchSlop)
      resolve(GestureDisposition.accepted);
  }

362
  @override
363 364 365 366 367
  void accepted(GestureMultiDragStartCallback starter) {
    starter(initialPosition);
  }
}

368 369 370 371 372 373 374 375 376 377
/// 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:
///
///  * [VerticalDragGestureRecognizer]
378
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
379
  @override
380 381 382
  _VerticalPointerState createNewPointerState(PointerDownEvent event) {
    return new _VerticalPointerState(event.position);
  }
383

384
  @override
385
  String toStringShort() => 'vertical multidrag';
386 387
}

388 389 390 391 392 393 394
class _DelayedPointerState extends MultiDragPointerState {
  _DelayedPointerState(Point initialPosition, Duration delay) : super(initialPosition) {
    assert(delay != null);
    _timer = new Timer(delay, _delayPassed);
  }

  Timer _timer;
395
  GestureMultiDragStartCallback _starter;
396 397 398 399 400 401

  void _delayPassed() {
    assert(_timer != null);
    assert(pendingDelta != null);
    assert(pendingDelta.distance <= kTouchSlop);
    _timer = null;
402 403 404 405 406 407 408
    if (_starter != null) {
      _starter(initialPosition);
      _starter = null;
    } else {
      resolve(GestureDisposition.accepted);
    }
    assert(_starter == null);
409 410
  }

411
  @override
412 413 414 415 416 417
  void accepted(GestureMultiDragStartCallback starter) {
    assert(_starter == null);
    if (_timer == null)
      starter(initialPosition);
    else
      _starter = starter;
418 419
  }

420
  @override
421 422 423 424 425 426 427
  void checkForResolutionAfterMove() {
    assert(_timer != null);
    assert(pendingDelta != null);
    if (pendingDelta.distance > kTouchSlop)
      resolve(GestureDisposition.rejected);
  }

428
  @override
429 430 431 432 433 434 435
  void dispose() {
    _timer?.cancel();
    _timer = null;
    super.dispose();
  }
}

436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
/// Recognizes movement both horizontally and vertically on a per-pointer basis after a delay.
///
/// In constrast to [ImmediateMultiDragGestureRecognizer],
/// [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:
///
///  * [PanGestureRecognizer]
///  * [ImmediateMultiDragGestureRecognizer]
451
class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_DelayedPointerState> {
452 453 454 455 456 457
  /// 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.
458
  DelayedMultiDragGestureRecognizer({
459 460
    this.delay: kLongPressTimeout
  }) {
461 462 463
    assert(delay != null);
  }

464 465
  /// The amount of time the pointer must remain in the same place for the drag
  /// to be recognized.
466
  final Duration delay;
467

468
  @override
469
  _DelayedPointerState createNewPointerState(PointerDownEvent event) {
470
    return new _DelayedPointerState(event.position, delay);
471
  }
472

473
  @override
474
  String toStringShort() => 'long multidrag';
475
}