drag_target.dart 15.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 9 10 11

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

typedef bool DragTargetWillAccept<T>(T data);
typedef void DragTargetAccept<T>(T data);
16
typedef Widget DragTargetBuilder<T>(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
Hixie's avatar
Hixie committed
17

18 19 20
/// Called when a [Draggable] is dropped without being accepted by a [DragTarget].
typedef void OnDraggableCanceled(Velocity velocity, Offset offset);

Adam Barth's avatar
Adam Barth committed
21
/// Where the [Draggable] should be anchored during a drag.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
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,
}

41 42
/// Subclass this widget to customize the gesture used to start a drag.
abstract class DraggableBase<T> extends StatefulWidget {
Hixie's avatar
Hixie committed
43
  DraggableBase({
44 45 46
    Key key,
    this.data,
    this.child,
47
    this.childWhenDragging,
48 49
    this.feedback,
    this.feedbackOffset: Offset.zero,
50
    this.dragAnchor: DragAnchor.child,
51 52
    this.maxSimultaneousDrags,
    this.onDraggableCanceled
53
  }) : super(key: key) {
54 55
    assert(child != null);
    assert(feedback != null);
56
    assert(maxSimultaneousDrags == null || maxSimultaneousDrags > 0);
57 58
  }

Hixie's avatar
Hixie committed
59
  final T data;
60 61

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

64 65 66 67 68 69 70 71
  /// 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.
72 73
  final Widget feedback;

74 75 76 77
  /// 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
78 79

  /// Where this widget should be anchored during a drag.
80 81
  final DragAnchor dragAnchor;

82 83 84 85 86
  /// 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;

87 88 89
  /// Called when the draggable is dropped without being accepted by a [DragTarget].
  final OnDraggableCanceled onDraggableCanceled;

90 91
  /// Should return a new MultiDragGestureRecognizer instance
  /// constructed with the given arguments.
Hixie's avatar
Hixie committed
92
  MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(GestureMultiDragStartCallback onStart);
Hixie's avatar
Hixie committed
93

94
  @override
Hixie's avatar
Hixie committed
95
  _DraggableState<T> createState() => new _DraggableState<T>();
96 97
}

Adam Barth's avatar
Adam Barth committed
98
/// Makes its child draggable starting from tap down.
Hixie's avatar
Hixie committed
99 100 101 102 103
class Draggable<T> extends DraggableBase<T> {
  Draggable({
    Key key,
    T data,
    Widget child,
104
    Widget childWhenDragging,
Hixie's avatar
Hixie committed
105 106
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
107
    DragAnchor dragAnchor: DragAnchor.child,
108 109
    int maxSimultaneousDrags,
    OnDraggableCanceled onDraggableCanceled
Hixie's avatar
Hixie committed
110 111 112 113
  }) : super(
    key: key,
    data: data,
    child: child,
114
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
115 116
    feedback: feedback,
    feedbackOffset: feedbackOffset,
117
    dragAnchor: dragAnchor,
118 119
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
Hixie's avatar
Hixie committed
120 121
  );

122
  @override
123
  ImmediateMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
124
    return new ImmediateMultiDragGestureRecognizer()..onStart = onStart;
Hixie's avatar
Hixie committed
125 126 127
  }
}

128 129 130 131 132 133 134 135 136 137 138
/// Makes its child draggable. When competing with other gestures,
/// this will only start the drag horizontally.
class HorizontalDraggable<T> extends DraggableBase<T> {
  HorizontalDraggable({
    Key key,
    T data,
    Widget child,
    Widget childWhenDragging,
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
    DragAnchor dragAnchor: DragAnchor.child,
139 140
    int maxSimultaneousDrags,
    OnDraggableCanceled onDraggableCanceled
141 142 143 144 145 146 147 148
  }) : super(
    key: key,
    data: data,
    child: child,
    childWhenDragging: childWhenDragging,
    feedback: feedback,
    feedbackOffset: feedbackOffset,
    dragAnchor: dragAnchor,
149 150
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
151 152
  );

153
  @override
154
  HorizontalMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
155
    return new HorizontalMultiDragGestureRecognizer()..onStart = onStart;
156 157 158 159 160 161 162 163 164 165 166 167 168 169
  }
}

/// Makes its child draggable. When competing with other gestures,
/// this will only start the drag vertically.
class VerticalDraggable<T> extends DraggableBase<T> {
  VerticalDraggable({
    Key key,
    T data,
    Widget child,
    Widget childWhenDragging,
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
    DragAnchor dragAnchor: DragAnchor.child,
170 171
    int maxSimultaneousDrags,
    OnDraggableCanceled onDraggableCanceled
172 173 174 175 176 177 178 179
  }) : super(
    key: key,
    data: data,
    child: child,
    childWhenDragging: childWhenDragging,
    feedback: feedback,
    feedbackOffset: feedbackOffset,
    dragAnchor: dragAnchor,
180 181
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
182 183
  );

184
  @override
185
  VerticalMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
186
    return new VerticalMultiDragGestureRecognizer()..onStart = onStart;
187 188 189
  }
}

Adam Barth's avatar
Adam Barth committed
190
/// Makes its child draggable starting from long press.
Hixie's avatar
Hixie committed
191 192 193 194 195
class LongPressDraggable<T> extends DraggableBase<T> {
  LongPressDraggable({
    Key key,
    T data,
    Widget child,
196
    Widget childWhenDragging,
Hixie's avatar
Hixie committed
197 198
    Widget feedback,
    Offset feedbackOffset: Offset.zero,
199
    DragAnchor dragAnchor: DragAnchor.child,
200 201
    int maxSimultaneousDrags,
    OnDraggableCanceled onDraggableCanceled
Hixie's avatar
Hixie committed
202 203 204 205
  }) : super(
    key: key,
    data: data,
    child: child,
206
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
207 208
    feedback: feedback,
    feedbackOffset: feedbackOffset,
209
    dragAnchor: dragAnchor,
210 211
    maxSimultaneousDrags: maxSimultaneousDrags,
    onDraggableCanceled: onDraggableCanceled
Hixie's avatar
Hixie committed
212 213
  );

214
  @override
215
  DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
216 217 218
    return new DelayedMultiDragGestureRecognizer()
      ..onStart = (Point position) {
        Drag result = onStart(position);
219 220 221
        if (result != null)
          userFeedback.performHapticFeedback(HapticFeedbackType.virtualKey);
        return result;
222
      };
Hixie's avatar
Hixie committed
223 224 225
  }
}

226
class _DraggableState<T> extends State<DraggableBase<T>> {
Hixie's avatar
Hixie committed
227

228
  @override
Hixie's avatar
Hixie committed
229 230
  void initState() {
    super.initState();
231
    _recognizer = config.createRecognizer(_startDrag);
Hixie's avatar
Hixie committed
232 233 234
  }

  GestureRecognizer _recognizer;
235
  int _activeCount = 0;
Hixie's avatar
Hixie committed
236

Ian Hickson's avatar
Ian Hickson committed
237
  void _routePointer(PointerEvent event) {
238 239
    if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
      return;
Hixie's avatar
Hixie committed
240 241 242
    _recognizer.addPointer(event);
  }

243
  _DragAvatar<T> _startDrag(Point position) {
244
    if (config.maxSimultaneousDrags != null && _activeCount >= config.maxSimultaneousDrags)
245
      return null;
246 247 248 249
    Point dragStartPoint;
    switch (config.dragAnchor) {
      case DragAnchor.child:
        final RenderBox renderObject = context.findRenderObject();
Hixie's avatar
Hixie committed
250
        dragStartPoint = renderObject.globalToLocal(position);
251 252 253
        break;
      case DragAnchor.pointer:
        dragStartPoint = Point.origin;
Hixie's avatar
Hixie committed
254
      break;
255
    }
256 257 258
    setState(() {
      _activeCount += 1;
    });
259
    return new _DragAvatar<T>(
260
      overlay: Overlay.of(context, debugRequiredFor: config),
261
      data: config.data,
Hixie's avatar
Hixie committed
262
      initialPosition: position,
263
      dragStartPoint: dragStartPoint,
264
      feedback: config.feedback,
265
      feedbackOffset: config.feedbackOffset,
266
      onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) {
Hixie's avatar
Hixie committed
267 268
        setState(() {
          _activeCount -= 1;
269 270
          if (!wasAccepted && config.onDraggableCanceled != null)
            config.onDraggableCanceled(velocity, offset);
Hixie's avatar
Hixie committed
271
        });
272
      }
273 274 275
    );
  }

276
  @override
277
  Widget build(BuildContext context) {
278
    assert(Overlay.of(context, debugRequiredFor: config) != null);
279 280 281
    final bool canDrag = config.maxSimultaneousDrags == null ||
                         _activeCount < config.maxSimultaneousDrags;
    final bool showChild = _activeCount == 0 || config.childWhenDragging == null;
282
    return new Listener(
283 284
      onPointerDown: canDrag ? _routePointer : null,
      child: showChild ? config.child : config.childWhenDragging
285 286 287 288
    );
  }
}

Adam Barth's avatar
Adam Barth committed
289
/// Receives data when a [Draggable] widget is dropped.
290
class DragTarget<T> extends StatefulWidget {
291
  const DragTarget({
292 293 294 295 296 297
    Key key,
    this.builder,
    this.onWillAccept,
    this.onAccept
  }) : super(key: key);

Adam Barth's avatar
Adam Barth committed
298 299 300 301
  /// Called to build the contents of this widget.
  ///
  /// The builder can build different widgets depending on what is being dragged
  /// into this drag target.
302
  final DragTargetBuilder<T> builder;
Adam Barth's avatar
Adam Barth committed
303 304 305

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

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

311
  @override
312
  _DragTargetState<T> createState() => new _DragTargetState<T>();
313
}
314

315
class _DragTargetState<T> extends State<DragTarget<T>> {
316 317 318 319 320 321
  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));
322
    if (data is T && (config.onWillAccept == null || config.onWillAccept(data))) {
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
      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);
    });
345 346
    if (config.onAccept != null)
      config.onAccept(data);
347 348
  }

349
  @override
350 351 352
  Widget build(BuildContext context) {
    return new MetaData(
      metaData: this,
Hixie's avatar
Hixie committed
353
      behavior: HitTestBehavior.translucent,
Adam Barth's avatar
Adam Barth committed
354
      child: config.builder(context, _candidateData, _rejectedData)
355
    );
356 357 358
  }
}

359

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

Hixie's avatar
Hixie committed
363 364 365 366 367
// 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.
368
class _DragAvatar<T> extends Drag {
Adam Barth's avatar
Adam Barth committed
369
  _DragAvatar({
Hixie's avatar
Hixie committed
370
    OverlayState overlay,
371
    this.data,
Hixie's avatar
Hixie committed
372
    Point initialPosition,
373 374
    this.dragStartPoint: Point.origin,
    this.feedback,
375 376
    this.feedbackOffset: Offset.zero,
    this.onDragEnd
377
  }) {
Hixie's avatar
Hixie committed
378 379
    assert(overlay != null);
    assert(dragStartPoint != null);
380
    assert(feedbackOffset != null);
Hixie's avatar
Hixie committed
381 382
    _entry = new OverlayEntry(builder: _build);
    overlay.insert(_entry);
383
    _position = initialPosition;
Hixie's avatar
Hixie committed
384
    update(initialPosition);
385
  }
386

Hixie's avatar
Hixie committed
387
  final T data;
388 389
  final Point dragStartPoint;
  final Widget feedback;
390
  final Offset feedbackOffset;
391
  final _OnDragEnd onDragEnd;
392

393
  _DragTargetState<T> _activeTarget;
394
  List<_DragTargetState<T>> _enteredTargets = <_DragTargetState<T>>[];
395
  Point _position;
396
  Offset _lastOffset;
Adam Barth's avatar
Adam Barth committed
397
  OverlayEntry _entry;
398

399
  // Drag API
400
  @override
401 402 403 404
  void move(Offset offset) {
    _position += offset;
    update(_position);
  }
405 406

  @override
407
  void end(Velocity velocity) {
408
    finish(_DragEndKind.dropped, velocity);
409
  }
410 411

  @override
412 413
  void cancel() {
    finish(_DragEndKind.canceled);
Hixie's avatar
Hixie committed
414 415
  }

416
  void update(Point globalPosition) {
417
    _lastOffset = globalPosition - dragStartPoint;
Hixie's avatar
Hixie committed
418
    _entry.markNeedsBuild();
Ian Hickson's avatar
Ian Hickson committed
419
    HitTestResult result = new HitTestResult();
420
    WidgetsBinding.instance.hitTest(result, globalPosition + feedbackOffset);
421 422 423 424

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

    bool listsMatch = false;
425
    if (targets.length >= _enteredTargets.length && _enteredTargets.isNotEmpty) {
426 427
      listsMatch = true;
      Iterator<_DragTargetState<T>> iterator = targets.iterator;
428
      for (int i = 0; i < _enteredTargets.length; i += 1) {
429
        iterator.moveNext();
430
        if (iterator.current != _enteredTargets[i]) {
431 432 433 434 435 436 437 438
          listsMatch = false;
          break;
        }
      }
    }

    // If everything's the same, bail early.
    if (listsMatch)
439
      return;
440 441

    // Leave old targets.
442
    _leaveAllEntered();
443 444 445

    // Enter new targets.
    _DragTargetState<T> newTarget = targets.firstWhere((_DragTargetState<T> target) {
446
        _enteredTargets.add(target);
447 448 449 450 451 452
        return target.didEnter(data);
      },
      orElse: () => null
    );

    _activeTarget = newTarget;
453 454
  }

455 456 457
  Iterable<_DragTargetState<T>> _getDragTargets(List<HitTestEntry> path) sync* {
    // Look for the RenderBoxes that corresponds to the hit target (the hit target
    // widgets build RenderMetadata boxes for us for this purpose).
Hixie's avatar
Hixie committed
458
    for (HitTestEntry entry in path) {
459 460
      if (entry.target is RenderMetaData) {
        RenderMetaData renderMetaData = entry.target;
461
        if (renderMetaData.metaData is _DragTargetState<T>)
462
          yield renderMetaData.metaData;
463 464
      }
    }
465 466
  }

467 468 469 470 471 472
  void _leaveAllEntered() {
    for (int i = 0; i < _enteredTargets.length; i += 1)
      _enteredTargets[i].didLeave(data);
    _enteredTargets.clear();
  }

473 474
  void finish(_DragEndKind endKind, [Velocity velocity]) {
    bool wasAccepted = false;
475 476 477 478
    if (endKind == _DragEndKind.dropped && _activeTarget != null) {
      _activeTarget.didDrop(data);
      wasAccepted = true;
      _enteredTargets.remove(_activeTarget);
479
    }
480
    _leaveAllEntered();
481
    _activeTarget = null;
Adam Barth's avatar
Adam Barth committed
482
    _entry.remove();
483
    _entry = null;
484
    // TODO(ianh): consider passing _entry as well so the client can perform an animation.
485
    if (onDragEnd != null)
486
      onDragEnd(velocity ?? Velocity.zero, _lastOffset, wasAccepted);
487 488
  }

Adam Barth's avatar
Adam Barth committed
489
  Widget _build(BuildContext context) {
490 491 492 493
    return new Positioned(
      left: _lastOffset.dx,
      top: _lastOffset.dy,
      child: new IgnorePointer(
494
        child: feedback
495 496
      )
    );
497 498
  }
}