drag_target.dart 17.1 KB
Newer Older
1 2 3 4
// 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.

Hixie's avatar
Hixie committed
5
import 'package:flutter/gestures.dart';
6
import 'package:flutter/rendering.dart';
Hixie's avatar
Hixie committed
7
import 'package:flutter/services.dart';
8
import 'package:meta/meta.dart';
9 10 11 12

import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
Adam Barth's avatar
Adam Barth committed
13
import 'overlay.dart';
14

15
/// Signature for determining whether the given data will be accepted by a [DragTarget].
16
typedef bool DragTargetWillAccept<T>(T data);
17 18

/// Signature for causing a [DragTarget] to accept the given data.
19
typedef void DragTargetAccept<T>(T data);
20 21 22 23 24 25 26

/// Signature for building children of a [DragTarget].
///
/// The `candidateData` argument contains the list of drag data that is hovering
/// over this [DragTarget] and that has passed [DragTarget.onWillAccept]. The
/// `rejectedData` argument contains the list of drag data that is hovering over
/// this [DragTarget] and that will not be accepted by the [DragTarget].
27
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
Hixie's avatar
Hixie committed
28

29 30
/// Signature for when a [Draggable] is dropped without being accepted by a [DragTarget].
typedef void DraggableCanceledCallback(Velocity velocity, Offset offset);
31

Adam Barth's avatar
Adam Barth committed
32
/// Where the [Draggable] should be anchored during a drag.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
enum DragAnchor {
  /// Display the feedback anchored at the position of the original child. If
  /// feedback is identical to the child, then this means the feedback will
  /// exactly overlap the original child when the drag starts.
  child,

  /// Display the feedback anchored at the position of the touch that started
  /// the drag. If feedback is identical to the child, then this means the top
  /// left of the feedback will be under the finger when the drag starts. This
  /// will likely not exactly overlap the original child, e.g. if the child is
  /// big and the touch was not centered. This mode is useful when the feedback
  /// is transformed so as to move the feedback to the left by half its width,
  /// and up by half its width plus the height of the finger, since then it
  /// appears as if putting the finger down makes the touch feedback appear
  /// above the finger. (It feels weird for it to appear offset from the
  /// original child if it's anchored to the child and not the finger.)
  pointer,
}

52 53
/// Subclass this widget to customize the gesture used to start a drag.
abstract class DraggableBase<T> extends StatefulWidget {
54 55 56 57
  /// Initializes fields for subclasses.
  ///
  /// The [child] and [feedback] arguments must not be null. If
  /// [maxSimultaneousDrags] is non-null, it must be positive.
Hixie's avatar
Hixie committed
58
  DraggableBase({
59
    Key key,
60 61
    @required this.child,
    @required this.feedback,
62
    this.data,
63
    this.childWhenDragging,
64
    this.feedbackOffset: Offset.zero,
65
    this.dragAnchor: DragAnchor.child,
66 67
    this.maxSimultaneousDrags,
    this.onDraggableCanceled
68
  }) : super(key: key) {
69 70
    assert(child != null);
    assert(feedback != null);
71
    assert(maxSimultaneousDrags == null || maxSimultaneousDrags > 0);
72 73
  }

74
  /// The data that will be dropped by this draggable.
Hixie's avatar
Hixie committed
75
  final T data;
76 77

  /// The widget below this widget in the tree.
78
  final Widget child;
Adam Barth's avatar
Adam Barth committed
79

80 81 82 83 84 85 86 87
  /// The widget to show instead of [child] when a drag is under way.
  ///
  /// If this is null, then [child] will be used instead (and so the
  /// drag source representation will change while a drag is under
  /// way).
  final Widget childWhenDragging;

  /// The widget to show under the pointer when a drag is under way.
88 89
  final Widget feedback;

90 91 92 93
  /// The feedbackOffset can be used to set the hit test target point for the
  /// purposes of finding a drag target. It is especially useful if the feedback
  /// is transformed compared to the child.
  final Offset feedbackOffset;
Adam Barth's avatar
Adam Barth committed
94 95

  /// Where this widget should be anchored during a drag.
96 97
  final DragAnchor dragAnchor;

98 99 100 101 102
  /// How many simultaneous drags to support. When null, no limit is applied.
  /// Set this to 1 if you want to only allow the drag source to have one item
  /// dragged at a time.
  final int maxSimultaneousDrags;

103
  /// Called when the draggable is dropped without being accepted by a [DragTarget].
104
  final DraggableCanceledCallback onDraggableCanceled;
105

106 107
  /// Should return a new MultiDragGestureRecognizer instance
  /// constructed with the given arguments.
Hixie's avatar
Hixie committed
108
  MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(GestureMultiDragStartCallback onStart);
Hixie's avatar
Hixie committed
109

110
  @override
Hixie's avatar
Hixie committed
111
  _DraggableState<T> createState() => new _DraggableState<T>();
112 113
}

Adam Barth's avatar
Adam Barth committed
114
/// Makes its child draggable starting from tap down.
Hixie's avatar
Hixie committed
115
class Draggable<T> extends DraggableBase<T> {
116 117 118 119
  /// Creates a widget that can be dragged starting from tap down.
  ///
  /// The [child] and [feedback] arguments must not be null. If
  /// [maxSimultaneousDrags] is non-null, it must be positive.
Hixie's avatar
Hixie committed
120 121
  Draggable({
    Key key,
122 123
    @required Widget child,
    @required Widget feedback,
Hixie's avatar
Hixie committed
124
    T data,
125
    Widget childWhenDragging,
Hixie's avatar
Hixie committed
126
    Offset feedbackOffset: Offset.zero,
127
    DragAnchor dragAnchor: DragAnchor.child,
128
    int maxSimultaneousDrags,
129
    DraggableCanceledCallback onDraggableCanceled
Hixie's avatar
Hixie committed
130 131 132 133
  }) : super(
    key: key,
    child: child,
    feedback: feedback,
134 135
    data: data,
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
136
    feedbackOffset: feedbackOffset,
137
    dragAnchor: dragAnchor,
138 139
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
Hixie's avatar
Hixie committed
140 141
  );

142
  @override
143
  ImmediateMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
144
    return new ImmediateMultiDragGestureRecognizer()..onStart = onStart;
Hixie's avatar
Hixie committed
145 146 147
  }
}

148 149 150
/// Makes its child draggable. When competing with other gestures,
/// this will only start the drag horizontally.
class HorizontalDraggable<T> extends DraggableBase<T> {
151 152 153 154
  /// Creates a widget that can be dragged.
  ///
  /// The [child] and [feedback] arguments must not be null. If
  /// [maxSimultaneousDrags] is non-null, it must be positive.
155 156
  HorizontalDraggable({
    Key key,
157 158
    @required Widget child,
    @required Widget feedback,
159 160 161 162
    T data,
    Widget childWhenDragging,
    Offset feedbackOffset: Offset.zero,
    DragAnchor dragAnchor: DragAnchor.child,
163
    int maxSimultaneousDrags,
164
    DraggableCanceledCallback onDraggableCanceled
165 166 167 168
  }) : super(
    key: key,
    child: child,
    feedback: feedback,
169 170
    data: data,
    childWhenDragging: childWhenDragging,
171 172
    feedbackOffset: feedbackOffset,
    dragAnchor: dragAnchor,
173 174
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
175 176
  );

177
  @override
178
  HorizontalMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
179
    return new HorizontalMultiDragGestureRecognizer()..onStart = onStart;
180 181 182 183 184 185
  }
}

/// Makes its child draggable. When competing with other gestures,
/// this will only start the drag vertically.
class VerticalDraggable<T> extends DraggableBase<T> {
186 187 188 189
  /// Creates a widget that can be dragged.
  ///
  /// The [child] and [feedback] arguments must not be null. If
  /// [maxSimultaneousDrags] is non-null, it must be positive.
190 191
  VerticalDraggable({
    Key key,
192 193
    @required Widget child,
    @required Widget feedback,
194 195 196 197
    T data,
    Widget childWhenDragging,
    Offset feedbackOffset: Offset.zero,
    DragAnchor dragAnchor: DragAnchor.child,
198
    int maxSimultaneousDrags,
199
    DraggableCanceledCallback onDraggableCanceled
200 201 202 203
  }) : super(
    key: key,
    child: child,
    feedback: feedback,
204 205
    data: data,
    childWhenDragging: childWhenDragging,
206 207
    feedbackOffset: feedbackOffset,
    dragAnchor: dragAnchor,
208 209
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
210 211
  );

212
  @override
213
  VerticalMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
214
    return new VerticalMultiDragGestureRecognizer()..onStart = onStart;
215 216 217
  }
}

Adam Barth's avatar
Adam Barth committed
218
/// Makes its child draggable starting from long press.
Hixie's avatar
Hixie committed
219
class LongPressDraggable<T> extends DraggableBase<T> {
220 221 222 223
  /// Creates a widget that can be dragged starting from long press.
  ///
  /// The [child] and [feedback] arguments must not be null. If
  /// [maxSimultaneousDrags] is non-null, it must be positive.
Hixie's avatar
Hixie committed
224 225
  LongPressDraggable({
    Key key,
226 227
    @required Widget child,
    @required Widget feedback,
Hixie's avatar
Hixie committed
228
    T data,
229
    Widget childWhenDragging,
Hixie's avatar
Hixie committed
230
    Offset feedbackOffset: Offset.zero,
231
    DragAnchor dragAnchor: DragAnchor.child,
232
    int maxSimultaneousDrags,
233
    DraggableCanceledCallback onDraggableCanceled
Hixie's avatar
Hixie committed
234 235 236 237
  }) : super(
    key: key,
    child: child,
    feedback: feedback,
238 239
    data: data,
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
240
    feedbackOffset: feedbackOffset,
241
    dragAnchor: dragAnchor,
242 243
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
Hixie's avatar
Hixie committed
244 245
  );

246
  @override
247
  DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
248 249 250
    return new DelayedMultiDragGestureRecognizer()
      ..onStart = (Point position) {
        Drag result = onStart(position);
251
        if (result != null)
252
          HapticFeedback.vibrate();
253
        return result;
254
      };
Hixie's avatar
Hixie committed
255 256 257
  }
}

258
class _DraggableState<T> extends State<DraggableBase<T>> {
259
  @override
Hixie's avatar
Hixie committed
260 261
  void initState() {
    super.initState();
262
    _recognizer = config.createRecognizer(_startDrag);
Hixie's avatar
Hixie committed
263 264
  }

265 266 267 268 269 270
  @override
  void dispose() {
    _recognizer.dispose();
    super.dispose();
  }

Hixie's avatar
Hixie committed
271
  GestureRecognizer _recognizer;
272
  int _activeCount = 0;
Hixie's avatar
Hixie committed
273

Ian Hickson's avatar
Ian Hickson committed
274
  void _routePointer(PointerEvent event) {
275 276
    if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
      return;
Hixie's avatar
Hixie committed
277 278 279
    _recognizer.addPointer(event);
  }

280
  _DragAvatar<T> _startDrag(Point position) {
281
    if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
282
      return null;
283 284 285 286
    Point dragStartPoint;
    switch (config.dragAnchor) {
      case DragAnchor.child:
        final RenderBox renderObject = context.findRenderObject();
Hixie's avatar
Hixie committed
287
        dragStartPoint = renderObject.globalToLocal(position);
288 289 290
        break;
      case DragAnchor.pointer:
        dragStartPoint = Point.origin;
Hixie's avatar
Hixie committed
291
      break;
292
    }
293 294 295
    setState(() {
      _activeCount += 1;
    });
296
    return new _DragAvatar<T>(
297
      overlay: Overlay.of(context, debugRequiredFor: config),
298
      data: config.data,
Hixie's avatar
Hixie committed
299
      initialPosition: position,
300
      dragStartPoint: dragStartPoint,
301
      feedback: config.feedback,
302
      feedbackOffset: config.feedbackOffset,
303
      onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) {
Hixie's avatar
Hixie committed
304 305
        setState(() {
          _activeCount -= 1;
306 307
          if (!wasAccepted && config.onDraggableCanceled != null)
            config.onDraggableCanceled(velocity, offset);
Hixie's avatar
Hixie committed
308
        });
309
      }
310 311 312
    );
  }

313
  @override
314
  Widget build(BuildContext context) {
315
    assert(Overlay.of(context, debugRequiredFor: config) != null);
316 317 318
    final bool canDrag = config.maxSimultaneousDrags == null ||
                         _activeCount < config.maxSimultaneousDrags;
    final bool showChild = _activeCount == 0 || config.childWhenDragging == null;
319
    return new Listener(
320 321
      onPointerDown: canDrag ? _routePointer : null,
      child: showChild ? config.child : config.childWhenDragging
322 323 324 325
    );
  }
}

326
/// A widget that receives data when a [Draggable] widget is dropped.
327
class DragTarget<T> extends StatefulWidget {
328 329 330
  /// Creates a widget that receives drags.
  ///
  /// The [builder] argument must not be null.
331
  const DragTarget({
332
    Key key,
333
    @required this.builder,
334 335 336 337
    this.onWillAccept,
    this.onAccept
  }) : super(key: key);

Adam Barth's avatar
Adam Barth committed
338 339 340 341
  /// Called to build the contents of this widget.
  ///
  /// The builder can build different widgets depending on what is being dragged
  /// into this drag target.
342
  final DragTargetBuilder<T> builder;
Adam Barth's avatar
Adam Barth committed
343 344 345

  /// Called to determine whether this widget is interested in receiving a given
  /// piece of data being dragged over this drag target.
346
  final DragTargetWillAccept<T> onWillAccept;
Adam Barth's avatar
Adam Barth committed
347 348

  /// Called when an acceptable piece of data was dropped over this drag target.
349 350
  final DragTargetAccept<T> onAccept;

351
  @override
352
  _DragTargetState<T> createState() => new _DragTargetState<T>();
353
}
354

355
class _DragTargetState<T> extends State<DragTarget<T>> {
356 357 358 359 360 361
  final List<T> _candidateData = new List<T>();
  final List<dynamic> _rejectedData = new List<dynamic>();

  bool didEnter(dynamic data) {
    assert(!_candidateData.contains(data));
    assert(!_rejectedData.contains(data));
362
    if (data is T && (config.onWillAccept == null || config.onWillAccept(data))) {
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
      setState(() {
        _candidateData.add(data);
      });
      return true;
    }
    _rejectedData.add(data);
    return false;
  }

  void didLeave(dynamic data) {
    assert(_candidateData.contains(data) || _rejectedData.contains(data));
    setState(() {
      _candidateData.remove(data);
      _rejectedData.remove(data);
    });
  }

  void didDrop(dynamic data) {
    assert(_candidateData.contains(data));
    setState(() {
      _candidateData.remove(data);
    });
385 386
    if (config.onAccept != null)
      config.onAccept(data);
387 388
  }

389
  @override
390
  Widget build(BuildContext context) {
391
    assert(config.builder != null);
392 393
    return new MetaData(
      metaData: this,
Hixie's avatar
Hixie committed
394
      behavior: HitTestBehavior.translucent,
Adam Barth's avatar
Adam Barth committed
395
      child: config.builder(context, _candidateData, _rejectedData)
396
    );
397 398 399
  }
}

400

Adam Barth's avatar
Adam Barth committed
401
enum _DragEndKind { dropped, canceled }
402
typedef void _OnDragEnd(Velocity velocity, Offset offset, bool wasAccepted);
403

Hixie's avatar
Hixie committed
404 405 406 407 408
// The lifetime of this object is a little dubious right now. Specifically, it
// lives as long as the pointer is down. Arguably it should self-immolate if the
// overlay goes away, or maybe even if the Draggable that created goes away.
// This will probably need to be changed once we have more experience with using
// this widget.
409
class _DragAvatar<T> extends Drag {
Adam Barth's avatar
Adam Barth committed
410
  _DragAvatar({
Hixie's avatar
Hixie committed
411
    OverlayState overlay,
412
    this.data,
Hixie's avatar
Hixie committed
413
    Point initialPosition,
414 415
    this.dragStartPoint: Point.origin,
    this.feedback,
416 417
    this.feedbackOffset: Offset.zero,
    this.onDragEnd
418
  }) {
Hixie's avatar
Hixie committed
419 420
    assert(overlay != null);
    assert(dragStartPoint != null);
421
    assert(feedbackOffset != null);
Hixie's avatar
Hixie committed
422 423
    _entry = new OverlayEntry(builder: _build);
    overlay.insert(_entry);
424
    _position = initialPosition;
425
    updateDrag(initialPosition);
426
  }
427

Hixie's avatar
Hixie committed
428
  final T data;
429 430
  final Point dragStartPoint;
  final Widget feedback;
431
  final Offset feedbackOffset;
432
  final _OnDragEnd onDragEnd;
433

434
  _DragTargetState<T> _activeTarget;
435
  List<_DragTargetState<T>> _enteredTargets = <_DragTargetState<T>>[];
436
  Point _position;
437
  Offset _lastOffset;
Adam Barth's avatar
Adam Barth committed
438
  OverlayEntry _entry;
439

440
  // Drag API
441
  @override
442 443 444
  void update(DragUpdateDetails details) {
    _position += details.delta;
    updateDrag(_position);
445
  }
446 447

  @override
448 449
  void end(DragEndDetails details) {
    finishDrag(_DragEndKind.dropped, details.velocity);
450
  }
451 452

  @override
453
  void cancel() {
454
    finishDrag(_DragEndKind.canceled);
Hixie's avatar
Hixie committed
455 456
  }

457
  void updateDrag(Point globalPosition) {
458
    _lastOffset = globalPosition - dragStartPoint;
Hixie's avatar
Hixie committed
459
    _entry.markNeedsBuild();
Ian Hickson's avatar
Ian Hickson committed
460
    HitTestResult result = new HitTestResult();
461
    WidgetsBinding.instance.hitTest(result, globalPosition + feedbackOffset);
462 463 464 465

    List<_DragTargetState<T>> targets = _getDragTargets(result.path).toList();

    bool listsMatch = false;
466
    if (targets.length >= _enteredTargets.length && _enteredTargets.isNotEmpty) {
467 468
      listsMatch = true;
      Iterator<_DragTargetState<T>> iterator = targets.iterator;
469
      for (int i = 0; i < _enteredTargets.length; i += 1) {
470
        iterator.moveNext();
471
        if (iterator.current != _enteredTargets[i]) {
472 473 474 475 476 477 478 479
          listsMatch = false;
          break;
        }
      }
    }

    // If everything's the same, bail early.
    if (listsMatch)
480
      return;
481 482

    // Leave old targets.
483
    _leaveAllEntered();
484 485 486

    // Enter new targets.
    _DragTargetState<T> newTarget = targets.firstWhere((_DragTargetState<T> target) {
487
        _enteredTargets.add(target);
488 489 490 491 492 493
        return target.didEnter(data);
      },
      orElse: () => null
    );

    _activeTarget = newTarget;
494 495
  }

496 497
  Iterable<_DragTargetState<T>> _getDragTargets(List<HitTestEntry> path) sync* {
    // Look for the RenderBoxes that corresponds to the hit target (the hit target
498
    // widgets build RenderMetaData boxes for us for this purpose).
Hixie's avatar
Hixie committed
499
    for (HitTestEntry entry in path) {
500 501
      if (entry.target is RenderMetaData) {
        RenderMetaData renderMetaData = entry.target;
502
        if (renderMetaData.metaData is _DragTargetState<T>)
503
          yield renderMetaData.metaData;
504 505
      }
    }
506 507
  }

508 509 510 511 512 513
  void _leaveAllEntered() {
    for (int i = 0; i < _enteredTargets.length; i += 1)
      _enteredTargets[i].didLeave(data);
    _enteredTargets.clear();
  }

514
  void finishDrag(_DragEndKind endKind, [Velocity velocity]) {
515
    bool wasAccepted = false;
516 517 518 519
    if (endKind == _DragEndKind.dropped && _activeTarget != null) {
      _activeTarget.didDrop(data);
      wasAccepted = true;
      _enteredTargets.remove(_activeTarget);
520
    }
521
    _leaveAllEntered();
522
    _activeTarget = null;
Adam Barth's avatar
Adam Barth committed
523
    _entry.remove();
524
    _entry = null;
525
    // TODO(ianh): consider passing _entry as well so the client can perform an animation.
526
    if (onDragEnd != null)
527
      onDragEnd(velocity ?? Velocity.zero, _lastOffset, wasAccepted);
528 529
  }

Adam Barth's avatar
Adam Barth committed
530
  Widget _build(BuildContext context) {
531 532 533 534
    return new Positioned(
      left: _lastOffset.dx,
      top: _lastOffset.dy,
      child: new IgnorePointer(
535
        child: feedback
536 537
      )
    );
538 539
  }
}