drag_target.dart 25.7 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.

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
/// Signature for determining whether the given data will be accepted by a [DragTarget].
15 16
///
/// Used by [DragTarget.onWillAccept].
17
typedef DragTargetWillAccept<T> = bool Function(T data);
18 19

/// Signature for causing a [DragTarget] to accept the given data.
20 21
///
/// Used by [DragTarget.onAccept].
22
typedef DragTargetAccept<T> = void Function(T data);
23 24 25 26 27 28 29

/// 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].
30 31
///
/// Used by [DragTarget.builder].
32
typedef DragTargetBuilder<T> = Widget Function(BuildContext context, List<T> candidateData, List<dynamic> rejectedData);
Hixie's avatar
Hixie committed
33

34
/// Signature for when a [Draggable] is dropped without being accepted by a [DragTarget].
35 36
///
/// Used by [Draggable.onDraggableCanceled].
37
typedef DraggableCanceledCallback = void Function(Velocity velocity, Offset offset);
38

39 40 41 42 43 44 45 46 47
/// Signature for when the draggable is dropped.
///
/// The velocity and offset at which the pointer was moving when the draggable
/// was dropped is available in the [DraggableDetails]. Also included in the
/// `details` is whether the draggable's [DragTarget] accepted it.
///
/// Used by [Draggable.onDragEnd]
typedef DragEndCallback = void Function(DraggableDetails details);

48 49 50
/// Signature for when a [Draggable] leaves a [DragTarget].
///
/// Used by [DragTarget.onLeave].
51
typedef DragTargetLeave = void Function(Object data);
52

Adam Barth's avatar
Adam Barth committed
53
/// Where the [Draggable] should be anchored during a drag.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
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,
}

73 74 75 76 77
/// A widget that can be dragged from to a [DragTarget].
///
/// When a draggable widget recognizes the start of a drag gesture, it displays
/// a [feedback] widget that tracks the user's finger across the screen. If the
/// user lifts their finger while on top of a [DragTarget], that target is given
78 79 80 81 82 83 84 85 86 87 88
/// the opportunity to accept the [data] carried by the draggable.
///
/// On multitouch devices, multiple drags can occur simultaneously because there
/// can be multiple pointers in contact with the device at once. To limit the
/// number of simultaneous drags, use the [maxSimultaneousDrags] property. The
/// default is to allow an unlimited number of simultaneous drags.
///
/// This widget displays [child] when zero drags are under way. If
/// [childWhenDragging] is non-null, this widget instead displays
/// [childWhenDragging] when one or more drags are underway. Otherwise, this
/// widget always displays [child].
89
///
90 91
/// {@youtube 560 315 https://www.youtube.com/watch?v=QzA4c4QHZCY}
///
92 93 94 95 96 97
/// See also:
///
///  * [DragTarget]
///  * [LongPressDraggable]
class Draggable<T> extends StatefulWidget {
  /// Creates a widget that can be dragged to a [DragTarget].
98 99
  ///
  /// The [child] and [feedback] arguments must not be null. If
100
  /// [maxSimultaneousDrags] is non-null, it must be non-negative.
101
  const Draggable({
102
    Key key,
103 104
    @required this.child,
    @required this.feedback,
105
    this.data,
106
    this.axis,
107
    this.childWhenDragging,
108 109
    this.feedbackOffset = Offset.zero,
    this.dragAnchor = DragAnchor.child,
110
    this.affinity,
111
    this.maxSimultaneousDrags,
112
    this.onDragStarted,
113
    this.onDraggableCanceled,
114
    this.onDragEnd,
115
    this.onDragCompleted,
116
    this.ignoringFeedbackSemantics = true,
117 118
  }) : assert(child != null),
       assert(feedback != null),
119
       assert(ignoringFeedbackSemantics != null),
120 121 122
       assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0),
       super(key: key);

123

124
  /// The data that will be dropped by this draggable.
Hixie's avatar
Hixie committed
125
  final T data;
126

127 128 129 130 131 132 133 134 135 136 137 138 139
  /// The [Axis] to restrict this draggable's movement, if specified.
  ///
  /// When axis is set to [Axis.horizontal], this widget can only be dragged
  /// horizontally. Behavior is similar for [Axis.vertical].
  ///
  /// Defaults to allowing drag on both [Axis.horizontal] and [Axis.vertical].
  ///
  /// When null, allows drag on both [Axis.horizontal] and [Axis.vertical].
  ///
  /// For the direction of gestures this widget competes with to start a drag
  /// event, see [Draggable.affinity].
  final Axis axis;

140
  /// The widget below this widget in the tree.
141 142 143 144 145 146 147 148 149 150
  ///
  /// This widget displays [child] when zero drags are under way. If
  /// [childWhenDragging] is non-null, this widget instead displays
  /// [childWhenDragging] when one or more drags are underway. Otherwise, this
  /// widget always displays [child].
  ///
  /// The [feedback] widget is shown under the pointer when a drag is under way.
  ///
  /// To limit the number of simultaneous drags on multitouch devices, see
  /// [maxSimultaneousDrags].
151 152
  ///
  /// {@macro flutter.widgets.child}
153
  final Widget child;
Adam Barth's avatar
Adam Barth committed
154

155
  /// The widget to display instead of [child] when one or more drags are under way.
156
  ///
157 158
  /// If this is null, then this widget will always display [child] (and so the
  /// drag source representation will not change while a drag is under
159
  /// way).
160 161 162 163 164
  ///
  /// The [feedback] widget is shown under the pointer when a drag is under way.
  ///
  /// To limit the number of simultaneous drags on multitouch devices, see
  /// [maxSimultaneousDrags].
165 166 167
  final Widget childWhenDragging;

  /// The widget to show under the pointer when a drag is under way.
168 169 170
  ///
  /// See [child] and [childWhenDragging] for information about what is shown
  /// at the location of the [Draggable] itself when a drag is under way.
171 172
  final Widget feedback;

173 174 175 176
  /// 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
177 178

  /// Where this widget should be anchored during a drag.
179 180
  final DragAnchor dragAnchor;

181 182
  /// Whether the semantics of the [feedback] widget is ignored when building
  /// the semantics tree.
183
  ///
184 185 186 187 188 189 190 191
  /// This value should be set to false when the [feedback] widget is intended
  /// to be the same object as the [child].  Placing a [GlobalKey] on this
  /// widget will ensure semantic focus is kept on the element as it moves in
  /// and out of the feedback position.
  ///
  /// Defaults to true.
  final bool ignoringFeedbackSemantics;

192 193 194 195 196 197 198 199 200 201 202 203 204 205
  /// Controls how this widget competes with other gestures to initiate a drag.
  ///
  /// If affinity is null, this widget initiates a drag as soon as it recognizes
  /// a tap down gesture, regardless of any directionality. If affinity is
  /// horizontal (or vertical), then this widget will compete with other
  /// horizontal (or vertical, respectively) gestures.
  ///
  /// For example, if this widget is placed in a vertically scrolling region and
  /// has horizontal affinity, pointer motion in the vertical direction will
  /// result in a scroll and pointer motion in the horizontal direction will
  /// result in a drag. Conversely, if the widget has a null or vertical
  /// affinity, pointer motion in any direction will result in a drag rather
  /// than in a scroll because the draggable widget, being the more specific
  /// widget, will out-compete the [Scrollable] for vertical gestures.
206 207 208
  ///
  /// For the directions this widget can be dragged in after the drag event
  /// starts, see [Draggable.axis].
209 210
  final Axis affinity;

211 212 213 214 215
  /// 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. Set this to 0 if you
  /// want to prevent the draggable from actually being dragged.
216 217 218
  ///
  /// If you set this property to 1, consider supplying an "empty" widget for
  /// [childWhenDragging] to create the illusion of actually moving [child].
219 220
  final int maxSimultaneousDrags;

221 222 223
  /// Called when the draggable starts being dragged.
  final VoidCallback onDragStarted;

224
  /// Called when the draggable is dropped without being accepted by a [DragTarget].
225 226 227 228 229 230 231
  ///
  /// This function might be called after this widget has been removed from the
  /// tree. For example, if a drag was in progress when this widget was removed
  /// from the tree and the drag ended up being canceled, this callback will
  /// still be called. For this reason, implementations of this callback might
  /// need to check [State.mounted] to check whether the state receiving the
  /// callback is still in the tree.
232
  final DraggableCanceledCallback onDraggableCanceled;
233

234 235 236 237 238 239 240 241 242 243
  /// Called when the draggable is dropped and accepted by a [DragTarget].
  ///
  /// This function might be called after this widget has been removed from the
  /// tree. For example, if a drag was in progress when this widget was removed
  /// from the tree and the drag ended up completing, this callback will
  /// still be called. For this reason, implementations of this callback might
  /// need to check [State.mounted] to check whether the state receiving the
  /// callback is still in the tree.
  final VoidCallback onDragCompleted;

244 245 246 247 248 249 250 251 252 253
  /// Called when the draggable is dropped.
  ///
  /// The velocity and offset at which the pointer was moving when it was
  /// dropped is available in the [DraggableDetails]. Also included in the
  /// `details` is whether the draggable's [DragTarget] accepted it.
  ///
  /// This function will only be called while this widget is still mounted to
  /// the tree (i.e. [State.mounted] is true).
  final DragEndCallback onDragEnd;

254
  /// Creates a gesture recognizer that recognizes the start of the drag.
255
  ///
256 257 258 259 260 261
  /// Subclasses can override this function to customize when they start
  /// recognizing a drag.
  @protected
  MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(GestureMultiDragStartCallback onStart) {
    switch (affinity) {
      case Axis.horizontal:
262
        return HorizontalMultiDragGestureRecognizer()..onStart = onStart;
263
      case Axis.vertical:
264
        return VerticalMultiDragGestureRecognizer()..onStart = onStart;
265
    }
266
    return ImmediateMultiDragGestureRecognizer()..onStart = onStart;
Hixie's avatar
Hixie committed
267
  }
268

269
  @override
270
  _DraggableState<T> createState() => _DraggableState<T>();
271 272
}

Adam Barth's avatar
Adam Barth committed
273
/// Makes its child draggable starting from long press.
274
class LongPressDraggable<T> extends Draggable<T> {
275 276 277
  /// Creates a widget that can be dragged starting from long press.
  ///
  /// The [child] and [feedback] arguments must not be null. If
278
  /// [maxSimultaneousDrags] is non-null, it must be non-negative.
279
  const LongPressDraggable({
Hixie's avatar
Hixie committed
280
    Key key,
281 282
    @required Widget child,
    @required Widget feedback,
Hixie's avatar
Hixie committed
283
    T data,
284
    Axis axis,
285
    Widget childWhenDragging,
286 287
    Offset feedbackOffset = Offset.zero,
    DragAnchor dragAnchor = DragAnchor.child,
288
    int maxSimultaneousDrags,
289
    VoidCallback onDragStarted,
290
    DraggableCanceledCallback onDraggableCanceled,
291
    DragEndCallback onDragEnd,
292
    VoidCallback onDragCompleted,
293
    this.hapticFeedbackOnStart = true,
294
    bool ignoringFeedbackSemantics = true,
Hixie's avatar
Hixie committed
295 296 297 298
  }) : super(
    key: key,
    child: child,
    feedback: feedback,
299
    data: data,
300
    axis: axis,
301
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
302
    feedbackOffset: feedbackOffset,
303
    dragAnchor: dragAnchor,
304
    maxSimultaneousDrags: maxSimultaneousDrags,
305
    onDragStarted: onDragStarted,
306
    onDraggableCanceled: onDraggableCanceled,
307
    onDragEnd: onDragEnd,
308 309
    onDragCompleted: onDragCompleted,
    ignoringFeedbackSemantics: ignoringFeedbackSemantics,
Hixie's avatar
Hixie committed
310 311
  );

312 313 314
  /// Whether haptic feedback should be triggered on drag start.
  final bool hapticFeedbackOnStart;

315
  @override
316
  DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
317
    return DelayedMultiDragGestureRecognizer()
318
      ..onStart = (Offset position) {
319
        final Drag result = onStart(position);
320
        if (result != null && hapticFeedbackOnStart)
321
          HapticFeedback.selectionClick();
322
        return result;
323
      };
Hixie's avatar
Hixie committed
324 325 326
  }
}

327
class _DraggableState<T> extends State<Draggable<T>> {
328
  @override
Hixie's avatar
Hixie committed
329 330
  void initState() {
    super.initState();
331
    _recognizer = widget.createRecognizer(_startDrag);
Hixie's avatar
Hixie committed
332 333
  }

334 335
  @override
  void dispose() {
336
    _disposeRecognizerIfInactive();
337 338 339
    super.dispose();
  }

340 341 342 343 344 345 346 347 348
  // This gesture recognizer has an unusual lifetime. We want to support the use
  // case of removing the Draggable from the tree in the middle of a drag. That
  // means we need to keep this recognizer alive after this state object has
  // been disposed because it's the one listening to the pointer events that are
  // driving the drag.
  //
  // We achieve that by keeping count of the number of active drags and only
  // disposing the gesture recognizer after (a) this state object has been
  // disposed and (b) there are no more active drags.
Hixie's avatar
Hixie committed
349
  GestureRecognizer _recognizer;
350
  int _activeCount = 0;
Hixie's avatar
Hixie committed
351

352 353 354 355 356 357 358
  void _disposeRecognizerIfInactive() {
    if (_activeCount > 0)
      return;
    _recognizer.dispose();
    _recognizer = null;
  }

359
  void _routePointer(PointerDownEvent event) {
360
    if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags)
361
      return;
Hixie's avatar
Hixie committed
362 363 364
    _recognizer.addPointer(event);
  }

365
  _DragAvatar<T> _startDrag(Offset position) {
366
    if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags)
367
      return null;
368
    Offset dragStartPoint;
369
    switch (widget.dragAnchor) {
370
      case DragAnchor.child:
371
        final RenderBox renderObject = context.findRenderObject() as RenderBox;
Hixie's avatar
Hixie committed
372
        dragStartPoint = renderObject.globalToLocal(position);
373 374
        break;
      case DragAnchor.pointer:
375
        dragStartPoint = Offset.zero;
376
        break;
377
    }
378 379 380
    setState(() {
      _activeCount += 1;
    });
381
    final _DragAvatar<T> avatar = _DragAvatar<T>(
382 383
      overlayState: Overlay.of(context, debugRequiredFor: widget),
      data: widget.data,
384
      axis: widget.axis,
Hixie's avatar
Hixie committed
385
      initialPosition: position,
386
      dragStartPoint: dragStartPoint,
387 388
      feedback: widget.feedback,
      feedbackOffset: widget.feedbackOffset,
389
      ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics,
390
      onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) {
391 392 393 394 395
        if (mounted) {
          setState(() {
            _activeCount -= 1;
          });
        } else {
Hixie's avatar
Hixie committed
396
          _activeCount -= 1;
397 398
          _disposeRecognizerIfInactive();
        }
399 400 401 402
        if (mounted && widget.onDragEnd != null) {
          widget.onDragEnd(DraggableDetails(
              wasAccepted: wasAccepted,
              velocity: velocity,
403
              offset: offset,
404 405
          ));
        }
406 407
        if (wasAccepted && widget.onDragCompleted != null)
          widget.onDragCompleted();
408 409
        if (!wasAccepted && widget.onDraggableCanceled != null)
          widget.onDraggableCanceled(velocity, offset);
410
      },
411
    );
412 413
    if (widget.onDragStarted != null)
      widget.onDragStarted();
414
    return avatar;
415 416
  }

417
  @override
418
  Widget build(BuildContext context) {
419 420 421 422
    assert(Overlay.of(context, debugRequiredFor: widget) != null);
    final bool canDrag = widget.maxSimultaneousDrags == null ||
                         _activeCount < widget.maxSimultaneousDrags;
    final bool showChild = _activeCount == 0 || widget.childWhenDragging == null;
423
    return Listener(
424
      onPointerDown: canDrag ? _routePointer : null,
425
      child: showChild ? widget.child : widget.childWhenDragging,
426 427 428 429
    );
  }
}

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
/// Represents the details when a specific pointer event occurred on
/// the [Draggable].
///
/// This includes the [Velocity] at which the pointer was moving and [Offset]
/// when the draggable event occurred, and whether its [DragTarget] accepted it.
///
/// Also, this is the details object for callbacks that use [DragEndCallback].
class DraggableDetails {
  /// Creates details for a [DraggableDetails].
  ///
  /// If [wasAccepted] is not specified, it will default to `false`.
  ///
  /// The [velocity] or [offset] arguments must not be `null`.
  DraggableDetails({
    this.wasAccepted = false,
    @required this.velocity,
446
    @required this.offset,
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461
  }) : assert(velocity != null),
       assert(offset != null);

  /// Determines whether the [DragTarget] accepted this draggable.
  final bool wasAccepted;

  /// The velocity at which the pointer was moving when the specific pointer
  /// event occurred on the draggable.
  final Velocity velocity;

  /// The global position when the specific pointer event occurred on
  /// the draggable.
  final Offset offset;
}

462
/// A widget that receives data when a [Draggable] widget is dropped.
463 464 465 466 467 468 469 470 471 472 473
///
/// When a draggable is dragged on top of a drag target, the drag target is
/// asked whether it will accept the data the draggable is carrying. If the user
/// does drop the draggable on top of the drag target (and the drag target has
/// indicated that it will accept the draggable's data), then the drag target is
/// asked to accept the draggable's data.
///
/// See also:
///
///  * [Draggable]
///  * [LongPressDraggable]
474
class DragTarget<T> extends StatefulWidget {
475 476 477
  /// Creates a widget that receives drags.
  ///
  /// The [builder] argument must not be null.
478
  const DragTarget({
479
    Key key,
480
    @required this.builder,
481
    this.onWillAccept,
482 483
    this.onAccept,
    this.onLeave,
484 485
  }) : super(key: key);

Adam Barth's avatar
Adam Barth committed
486 487 488 489
  /// Called to build the contents of this widget.
  ///
  /// The builder can build different widgets depending on what is being dragged
  /// into this drag target.
490
  final DragTargetBuilder<T> builder;
Adam Barth's avatar
Adam Barth committed
491 492 493

  /// Called to determine whether this widget is interested in receiving a given
  /// piece of data being dragged over this drag target.
494
  ///
495
  /// Called when a piece of data enters the target. This will be followed by
496 497
  /// either [onAccept], if the data is dropped, or [onLeave], if the drag
  /// leaves the target.
498
  final DragTargetWillAccept<T> onWillAccept;
Adam Barth's avatar
Adam Barth committed
499 500

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

503 504
  /// Called when a given piece of data being dragged over this target leaves
  /// the target.
505
  final DragTargetLeave onLeave;
506

507
  @override
508
  _DragTargetState<T> createState() => _DragTargetState<T>();
509
}
510

511 512
List<T> _mapAvatarsToData<T>(List<_DragAvatar<T>> avatars) {
  return avatars.map<T>((_DragAvatar<T> avatar) => avatar.data).toList();
513 514
}

515
class _DragTargetState<T> extends State<DragTarget<T>> {
516
  final List<_DragAvatar<T>> _candidateAvatars = <_DragAvatar<T>>[];
517
  final List<_DragAvatar<Object>> _rejectedAvatars = <_DragAvatar<Object>>[];
518

519
  bool didEnter(_DragAvatar<Object> avatar) {
520 521
    assert(!_candidateAvatars.contains(avatar));
    assert(!_rejectedAvatars.contains(avatar));
522
    if (avatar is _DragAvatar<T> && (widget.onWillAccept == null || widget.onWillAccept(avatar.data))) {
523
      setState(() {
524
        _candidateAvatars.add(avatar);
525 526
      });
      return true;
527 528 529 530 531
    } else {
      setState(() {
        _rejectedAvatars.add(avatar);
      });
      return false;
532 533 534
    }
  }

535
  void didLeave(_DragAvatar<Object> avatar) {
536
    assert(_candidateAvatars.contains(avatar) || _rejectedAvatars.contains(avatar));
537 538
    if (!mounted)
      return;
539
    setState(() {
540 541
      _candidateAvatars.remove(avatar);
      _rejectedAvatars.remove(avatar);
542
    });
543 544
    if (widget.onLeave != null)
      widget.onLeave(avatar.data);
545 546
  }

547
  void didDrop(_DragAvatar<Object> avatar) {
548
    assert(_candidateAvatars.contains(avatar));
549 550
    if (!mounted)
      return;
551
    setState(() {
552
      _candidateAvatars.remove(avatar);
553
    });
554
    if (widget.onAccept != null)
555
      widget.onAccept(avatar.data as T);
556 557
  }

558
  @override
559
  Widget build(BuildContext context) {
560
    assert(widget.builder != null);
561
    return MetaData(
562
      metaData: this,
Hixie's avatar
Hixie committed
563
      behavior: HitTestBehavior.translucent,
564
      child: widget.builder(context, _mapAvatarsToData<T>(_candidateAvatars), _mapAvatarsToData<Object>(_rejectedAvatars)),
565
    );
566 567 568
  }
}

Adam Barth's avatar
Adam Barth committed
569
enum _DragEndKind { dropped, canceled }
570
typedef _OnDragEnd = void Function(Velocity velocity, Offset offset, bool wasAccepted);
571

Hixie's avatar
Hixie committed
572 573
// 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
574
// overlay goes away. _DraggableState has some delicate logic to continue
Josh Soref's avatar
Josh Soref committed
575
// needing this object pointer events even after it has been disposed.
576
class _DragAvatar<T> extends Drag {
Adam Barth's avatar
Adam Barth committed
577
  _DragAvatar({
578
    @required this.overlayState,
579
    this.data,
580
    this.axis,
581
    Offset initialPosition,
582
    this.dragStartPoint = Offset.zero,
583
    this.feedback,
584
    this.feedbackOffset = Offset.zero,
585 586
    this.onDragEnd,
    @required this.ignoringFeedbackSemantics,
587
  }) : assert(overlayState != null),
588
       assert(ignoringFeedbackSemantics != null),
589 590
       assert(dragStartPoint != null),
       assert(feedbackOffset != null) {
591
    _entry = OverlayEntry(builder: _build);
592
    overlayState.insert(_entry);
593
    _position = initialPosition;
594
    updateDrag(initialPosition);
595
  }
596

Hixie's avatar
Hixie committed
597
  final T data;
598
  final Axis axis;
599
  final Offset dragStartPoint;
600
  final Widget feedback;
601
  final Offset feedbackOffset;
602
  final _OnDragEnd onDragEnd;
603
  final OverlayState overlayState;
604
  final bool ignoringFeedbackSemantics;
605

606
  _DragTargetState<T> _activeTarget;
607
  final List<_DragTargetState<T>> _enteredTargets = <_DragTargetState<T>>[];
608
  Offset _position;
609
  Offset _lastOffset;
Adam Barth's avatar
Adam Barth committed
610
  OverlayEntry _entry;
611

612
  @override
613
  void update(DragUpdateDetails details) {
614
    _position += _restrictAxis(details.delta);
615
    updateDrag(_position);
616
  }
617 618

  @override
619
  void end(DragEndDetails details) {
620
    finishDrag(_DragEndKind.dropped, _restrictVelocityAxis(details.velocity));
621
  }
622

623

624
  @override
625
  void cancel() {
626
    finishDrag(_DragEndKind.canceled);
Hixie's avatar
Hixie committed
627 628
  }

629
  void updateDrag(Offset globalPosition) {
630
    _lastOffset = globalPosition - dragStartPoint;
Hixie's avatar
Hixie committed
631
    _entry.markNeedsBuild();
632
    final HitTestResult result = HitTestResult();
633
    WidgetsBinding.instance.hitTest(result, globalPosition + feedbackOffset);
634

635
    final List<_DragTargetState<T>> targets = _getDragTargets(result.path).toList();
636 637

    bool listsMatch = false;
638
    if (targets.length >= _enteredTargets.length && _enteredTargets.isNotEmpty) {
639
      listsMatch = true;
640
      final Iterator<_DragTargetState<T>> iterator = targets.iterator;
641
      for (int i = 0; i < _enteredTargets.length; i += 1) {
642
        iterator.moveNext();
643
        if (iterator.current != _enteredTargets[i]) {
644 645 646 647 648 649 650 651
          listsMatch = false;
          break;
        }
      }
    }

    // If everything's the same, bail early.
    if (listsMatch)
652
      return;
653 654

    // Leave old targets.
655
    _leaveAllEntered();
656 657

    // Enter new targets.
658 659
    final _DragTargetState<T> newTarget = targets.firstWhere(
      (_DragTargetState<T> target) {
660
        _enteredTargets.add(target);
661
        return target.didEnter(this);
662
      },
663
      orElse: () => null,
664 665 666
    );

    _activeTarget = newTarget;
667 668
  }

669
  Iterable<_DragTargetState<T>> _getDragTargets(Iterable<HitTestEntry> path) sync* {
670
    // Look for the RenderBoxes that corresponds to the hit target (the hit target
671
    // widgets build RenderMetaData boxes for us for this purpose).
672
    for (final HitTestEntry entry in path) {
673 674 675 676 677
      final HitTestTarget target = entry.target;
      if (target is RenderMetaData) {
        final dynamic metaData = target.metaData;
        if (metaData is _DragTargetState<T>)
          yield metaData;
678 679
      }
    }
680 681
  }

682 683
  void _leaveAllEntered() {
    for (int i = 0; i < _enteredTargets.length; i += 1)
684
      _enteredTargets[i].didLeave(this);
685 686 687
    _enteredTargets.clear();
  }

688
  void finishDrag(_DragEndKind endKind, [ Velocity velocity ]) {
689
    bool wasAccepted = false;
690
    if (endKind == _DragEndKind.dropped && _activeTarget != null) {
691
      _activeTarget.didDrop(this);
692 693
      wasAccepted = true;
      _enteredTargets.remove(_activeTarget);
694
    }
695
    _leaveAllEntered();
696
    _activeTarget = null;
Adam Barth's avatar
Adam Barth committed
697
    _entry.remove();
698
    _entry = null;
699
    // TODO(ianh): consider passing _entry as well so the client can perform an animation.
700
    if (onDragEnd != null)
701
      onDragEnd(velocity ?? Velocity.zero, _lastOffset, wasAccepted);
702 703
  }

Adam Barth's avatar
Adam Barth committed
704
  Widget _build(BuildContext context) {
705
    final RenderBox box = overlayState.context.findRenderObject() as RenderBox;
706
    final Offset overlayTopLeft = box.localToGlobal(Offset.zero);
707
    return Positioned(
708 709
      left: _lastOffset.dx - overlayTopLeft.dx,
      top: _lastOffset.dy - overlayTopLeft.dy,
710
      child: IgnorePointer(
711 712
        child: feedback,
        ignoringSemantics: ignoringFeedbackSemantics,
713
      ),
714
    );
715
  }
716 717 718 719 720

  Velocity _restrictVelocityAxis(Velocity velocity) {
    if (axis == null) {
      return velocity;
    }
721
    return Velocity(
722 723 724 725 726 727 728
      pixelsPerSecond: _restrictAxis(velocity.pixelsPerSecond),
    );
  }

  Offset _restrictAxis(Offset offset) {
    if (axis == null) {
      return offset;
729
    }
730
    if (axis == Axis.horizontal) {
731
      return Offset(offset.dx, 0.0);
732
    }
733
    return Offset(0.0, offset.dy);
734
  }
735
}