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

5
import 'package:flutter/foundation.dart' show kIsWeb;
Hixie's avatar
Hixie committed
6
import 'package:flutter/gestures.dart';
7
import 'package:flutter/rendering.dart';
Hixie's avatar
Hixie committed
8
import 'package:flutter/services.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 17
///
/// Used by [DragTarget.onWillAccept].
18
typedef DragTargetWillAccept<T> = bool Function(T? data);
19 20

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

25 26 27 28 29
/// Signature for determining information about the acceptance by a [DragTarget].
///
/// Used by [DragTarget.onAcceptWithDetails].
typedef DragTargetAcceptWithDetails<T> = void Function(DragTargetDetails<T> details);

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

40 41 42 43 44
/// Signature for when a [Draggable] is dragged across the screen.
///
/// Used by [Draggable.onDragUpdate].
typedef DragUpdateCallback = void Function(DragUpdateDetails details);

45
/// Signature for when a [Draggable] is dropped without being accepted by a [DragTarget].
46 47
///
/// Used by [Draggable.onDraggableCanceled].
48
typedef DraggableCanceledCallback = void Function(Velocity velocity, Offset offset);
49

50 51 52 53 54 55
/// 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.
///
56
/// Used by [Draggable.onDragEnd].
57 58
typedef DragEndCallback = void Function(DraggableDetails details);

59 60 61
/// Signature for when a [Draggable] leaves a [DragTarget].
///
/// Used by [DragTarget.onLeave].
62
typedef DragTargetLeave<T> = void Function(T? data);
63

64 65 66
/// Signature for when a [Draggable] moves within a [DragTarget].
///
/// Used by [DragTarget.onMove].
67
typedef DragTargetMove<T> = void Function(DragTargetDetails<T> details);
68

69
/// Signature for the strategy that determines the drag start point of a [Draggable].
70
///
71 72 73 74 75 76 77 78 79
/// Used by [Draggable.dragAnchorStrategy].
///
/// There are two built-in strategies:
///
///  * [childDragAnchorStrategy], which displays the feedback anchored at the
///    position of the original child.
///
///  * [pointerDragAnchorStrategy], which displays the feedback anchored at the
///    position of the touch that started the drag.
80 81
typedef DragAnchorStrategy = Offset Function(Draggable<Object> draggable, BuildContext context, Offset position);

82 83 84 85 86 87 88 89 90 91 92
/// 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.
///
/// This is the default [DragAnchorStrategy] and replaces [DragAnchor.child].
///
/// See also:
///
///  * [DragAnchorStrategy], the typedef that this function implements.
///  * [Draggable.dragAnchorStrategy], for which this is a built-in value.
93 94 95 96 97
Offset childDragAnchorStrategy(Draggable<Object> draggable, BuildContext context, Offset position) {
  final RenderBox renderObject = context.findRenderObject()! as RenderBox;
  return renderObject.globalToLocal(position);
}

98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
/// 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.)
///
/// This replaces [DragAnchor.pointer], which has been deprecated.
///
/// See also:
///
///  * [DragAnchorStrategy], the typedef that this function implements.
///  * [Draggable.dragAnchorStrategy], for which this is a built-in value.
117 118 119
Offset pointerDragAnchorStrategy(Draggable<Object> draggable, BuildContext context, Offset position) {
  return Offset.zero;
}
120

Adam Barth's avatar
Adam Barth committed
121
/// Where the [Draggable] should be anchored during a drag.
122 123 124 125 126 127
///
/// This has been replaced by the more configurable [DragAnchorStrategy].
@Deprecated(
  'Use dragAnchorStrategy instead. '
  'This feature was deprecated after v2.1.0-10.0.pre.',
)
128
enum DragAnchor {
129 130 131 132 133 134 135
  /// Display the feedback anchored at the position of the original child.
  ///
  /// Replaced by [childDragAnchorStrategy].
  @Deprecated(
    'Use childDragAnchorStrategy instead. '
    'This feature was deprecated after v2.1.0-10.0.pre.',
  )
136 137 138
  child,

  /// Display the feedback anchored at the position of the touch that started
139 140 141 142 143 144 145
  /// the drag.
  ///
  /// Replaced by [pointerDragAnchorStrategy].
  @Deprecated(
    'Use pointerDragAnchorStrategy instead. '
    'This feature was deprecated after v2.1.0-10.0.pre.',
  )
146 147 148
  pointer,
}

149 150 151 152 153
/// 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
154 155 156 157 158 159 160 161 162 163 164
/// 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].
165
///
166 167
/// {@youtube 560 315 https://www.youtube.com/watch?v=QzA4c4QHZCY}
///
168
/// {@tool dartpad --template=stateful_widget_scaffold}
169 170 171 172 173 174 175
///
/// The following example has a [Draggable] widget along with a [DragTarget]
/// in a row demonstrating an incremented `acceptedData` integer value when
/// you drag the element to the target.
///
/// ```dart
/// int acceptedData = 0;
176 177
///
/// @override
178 179 180
/// Widget build(BuildContext context) {
///   return Row(
///     mainAxisAlignment: MainAxisAlignment.spaceEvenly,
181
///     children: <Widget>[
182 183 184 185 186 187 188
///       Draggable<int>(
///         // Data is the value this Draggable stores.
///         data: 10,
///         child: Container(
///           height: 100.0,
///           width: 100.0,
///           color: Colors.lightGreenAccent,
189 190
///           child: const Center(
///             child: Text('Draggable'),
191 192 193 194 195 196
///           ),
///         ),
///         feedback: Container(
///           color: Colors.deepOrange,
///           height: 100,
///           width: 100,
197
///           child: const Icon(Icons.directions_run),
198 199 200 201 202
///         ),
///         childWhenDragging: Container(
///           height: 100.0,
///           width: 100.0,
///           color: Colors.pinkAccent,
203 204
///           child: const Center(
///             child: Text('Child When Dragging'),
205 206 207
///           ),
///         ),
///       ),
208
///       DragTarget<int>(
209 210 211 212 213 214 215 216 217 218
///         builder: (
///           BuildContext context,
///           List<dynamic> accepted,
///           List<dynamic> rejected,
///         ) {
///           return Container(
///             height: 100.0,
///             width: 100.0,
///             color: Colors.cyan,
///             child: Center(
219
///               child: Text('Value is updated to: $acceptedData'),
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
///             ),
///           );
///         },
///         onAccept: (int data) {
///           setState(() {
///             acceptedData += data;
///           });
///         },
///       ),
///     ],
///   );
/// }
///
/// ```
///
/// {@end-tool}
///
237 238 239 240
/// See also:
///
///  * [DragTarget]
///  * [LongPressDraggable]
241
class Draggable<T extends Object> extends StatefulWidget {
242
  /// Creates a widget that can be dragged to a [DragTarget].
243 244
  ///
  /// The [child] and [feedback] arguments must not be null. If
245
  /// [maxSimultaneousDrags] is non-null, it must be non-negative.
246
  const Draggable({
247 248 249
    Key? key,
    required this.child,
    required this.feedback,
250
    this.data,
251
    this.axis,
252
    this.childWhenDragging,
253
    this.feedbackOffset = Offset.zero,
254 255
    @Deprecated(
      'Use dragAnchorStrategy instead. '
256 257
      'Replace "dragAnchor: DragAnchor.child" with "dragAnchorStrategy: childDragAnchorStrategy". '
      'Replace "dragAnchor: DragAnchor.pointer" with "dragAnchorStrategy: pointerDragAnchorStrategy". '
258
      'This feature was deprecated after v2.1.0-10.0.pre.',
259
    )
260
    this.dragAnchor = DragAnchor.child,
261
    this.dragAnchorStrategy,
262
    this.affinity,
263
    this.maxSimultaneousDrags,
264
    this.onDragStarted,
265
    this.onDragUpdate,
266
    this.onDraggableCanceled,
267
    this.onDragEnd,
268
    this.onDragCompleted,
269
    this.ignoringFeedbackSemantics = true,
270
    this.rootOverlay = false,
271
    this.hitTestBehavior = HitTestBehavior.deferToChild,
272 273
  }) : assert(child != null),
       assert(feedback != null),
274
       assert(ignoringFeedbackSemantics != null),
275 276 277
       assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0),
       super(key: key);

278
  /// The data that will be dropped by this draggable.
279
  final T? data;
280

281 282 283 284 285 286 287 288 289 290 291
  /// 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].
292
  final Axis? axis;
293

294
  /// The widget below this widget in the tree.
295 296 297 298 299 300 301 302 303 304
  ///
  /// 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].
305
  ///
306
  /// {@macro flutter.widgets.ProxyWidget.child}
307
  final Widget child;
Adam Barth's avatar
Adam Barth committed
308

309
  /// The widget to display instead of [child] when one or more drags are under way.
310
  ///
311 312
  /// 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
313
  /// way).
314 315 316 317 318
  ///
  /// 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].
319
  final Widget? childWhenDragging;
320 321

  /// The widget to show under the pointer when a drag is under way.
322 323 324
  ///
  /// See [child] and [childWhenDragging] for information about what is shown
  /// at the location of the [Draggable] itself when a drag is under way.
325 326
  final Widget feedback;

327 328 329 330
  /// 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
331 332

  /// Where this widget should be anchored during a drag.
333 334 335 336 337 338
  ///
  /// This property is overridden by the [dragAnchorStrategy] if the latter is provided.
  ///
  /// Defaults to [DragAnchor.child].
  @Deprecated(
    'Use dragAnchorStrategy instead. '
339
    'This feature was deprecated after v2.1.0-10.0.pre.',
340
  )
341 342
  final DragAnchor dragAnchor;

343 344
  /// A strategy that is used by this draggable to get the anchor offset when it
  /// is dragged.
345
  ///
346 347
  /// The anchor offset refers to the distance between the users' fingers and
  /// the [feedback] widget when this draggable is dragged.
348
  ///
349 350 351 352 353 354 355 356 357 358 359 360
  /// This property's value is a function that implements [DragAnchorStrategy].
  /// There are two built-in functions that can be used:
  ///
  ///  * [childDragAnchorStrategy], which displays the feedback anchored at the
  ///    position of the original child.
  ///
  ///  * [pointerDragAnchorStrategy], which displays the feedback anchored at the
  ///    position of the touch that started the drag.
  ///
  /// Defaults to [childDragAnchorStrategy] if the deprecated [dragAnchor]
  /// property is set to [DragAnchor.child], and [pointerDragAnchorStrategy] if
  /// the [dragAnchor] is set to [DragAnchor.pointer].
361 362
  final DragAnchorStrategy? dragAnchorStrategy;

363 364
  /// Whether the semantics of the [feedback] widget is ignored when building
  /// the semantics tree.
365
  ///
366 367 368 369 370 371 372 373
  /// 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;

374 375 376 377 378 379 380 381 382 383 384 385 386 387
  /// 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.
388 389 390
  ///
  /// For the directions this widget can be dragged in after the drag event
  /// starts, see [Draggable.axis].
391
  final Axis? affinity;
392

393 394 395 396 397
  /// 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.
398 399 400
  ///
  /// If you set this property to 1, consider supplying an "empty" widget for
  /// [childWhenDragging] to create the illusion of actually moving [child].
401
  final int? maxSimultaneousDrags;
402

403
  /// Called when the draggable starts being dragged.
404
  final VoidCallback? onDragStarted;
405

406
  /// Called when the draggable is dragged.
407 408 409 410 411
  ///
  /// This function will only be called while this widget is still mounted to
  /// the tree (i.e. [State.mounted] is true), and if this widget has actually moved.
  final DragUpdateCallback? onDragUpdate;

412
  /// Called when the draggable is dropped without being accepted by a [DragTarget].
413 414 415 416 417 418 419
  ///
  /// 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.
420
  final DraggableCanceledCallback? onDraggableCanceled;
421

422 423 424 425 426 427 428 429
  /// 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.
430
  final VoidCallback? onDragCompleted;
431

432 433 434 435 436 437 438 439
  /// 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).
440
  final DragEndCallback? onDragEnd;
441

442 443 444 445 446 447 448 449 450
  /// Whether the feedback widget will be put on the root [Overlay].
  ///
  /// When false, the feedback widget will be put on the closest [Overlay]. When
  /// true, the [feedback] widget will be put on the farthest (aka root)
  /// [Overlay].
  ///
  /// Defaults to false.
  final bool rootOverlay;

451 452 453 454 455
  /// How to behave during hit test.
  ///
  /// Defaults to [HitTestBehavior.deferToChild].
  final HitTestBehavior hitTestBehavior;

456
  /// Creates a gesture recognizer that recognizes the start of the drag.
457
  ///
458 459 460 461 462 463
  /// Subclasses can override this function to customize when they start
  /// recognizing a drag.
  @protected
  MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer(GestureMultiDragStartCallback onStart) {
    switch (affinity) {
      case Axis.horizontal:
464
        return HorizontalMultiDragGestureRecognizer()..onStart = onStart;
465
      case Axis.vertical:
466
        return VerticalMultiDragGestureRecognizer()..onStart = onStart;
467 468
      case null:
        return ImmediateMultiDragGestureRecognizer()..onStart = onStart;
469
    }
Hixie's avatar
Hixie committed
470
  }
471

472
  @override
473
  _DraggableState<T> createState() => _DraggableState<T>();
474 475
}

Adam Barth's avatar
Adam Barth committed
476
/// Makes its child draggable starting from long press.
477 478 479 480 481
///
/// See also:
///
///  * [Draggable], similar to the [LongPressDraggable] widget but happens immediately.
///  * [DragTarget], a widget that receives data when a [Draggable] widget is dropped.
482
class LongPressDraggable<T extends Object> extends Draggable<T> {
483 484 485
  /// Creates a widget that can be dragged starting from long press.
  ///
  /// The [child] and [feedback] arguments must not be null. If
486
  /// [maxSimultaneousDrags] is non-null, it must be non-negative.
487
  const LongPressDraggable({
488 489 490 491 492 493
    Key? key,
    required Widget child,
    required Widget feedback,
    T? data,
    Axis? axis,
    Widget? childWhenDragging,
494
    Offset feedbackOffset = Offset.zero,
495 496 497 498 499 500
    @Deprecated(
      'Use dragAnchorStrategy instead. '
      'Replace "dragAnchor: DragAnchor.child" with "dragAnchorStrategy: childDragAnchorStrategy". '
      'Replace "dragAnchor: DragAnchor.pointer" with "dragAnchorStrategy: pointerDragAnchorStrategy". '
      'This feature was deprecated after v2.1.0-10.0.pre.',
    )
501
    DragAnchor dragAnchor = DragAnchor.child,
502
    DragAnchorStrategy? dragAnchorStrategy,
503 504
    int? maxSimultaneousDrags,
    VoidCallback? onDragStarted,
505
    DragUpdateCallback? onDragUpdate,
506 507 508
    DraggableCanceledCallback? onDraggableCanceled,
    DragEndCallback? onDragEnd,
    VoidCallback? onDragCompleted,
509
    this.hapticFeedbackOnStart = true,
510
    bool ignoringFeedbackSemantics = true,
511
    this.delay = kLongPressTimeout,
Hixie's avatar
Hixie committed
512 513 514 515
  }) : super(
    key: key,
    child: child,
    feedback: feedback,
516
    data: data,
517
    axis: axis,
518
    childWhenDragging: childWhenDragging,
Hixie's avatar
Hixie committed
519
    feedbackOffset: feedbackOffset,
520
    dragAnchor: dragAnchor,
521
    dragAnchorStrategy: dragAnchorStrategy,
522
    maxSimultaneousDrags: maxSimultaneousDrags,
523
    onDragStarted: onDragStarted,
524
    onDragUpdate: onDragUpdate,
525
    onDraggableCanceled: onDraggableCanceled,
526
    onDragEnd: onDragEnd,
527 528
    onDragCompleted: onDragCompleted,
    ignoringFeedbackSemantics: ignoringFeedbackSemantics,
Hixie's avatar
Hixie committed
529 530
  );

531 532 533
  /// Whether haptic feedback should be triggered on drag start.
  final bool hapticFeedbackOnStart;

534 535 536 537 538
  /// The duration that a user has to press down before a long press is registered.
  ///
  /// Defaults to [kLongPressTimeout].
  final Duration delay;

539
  @override
540
  DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
541
    return DelayedMultiDragGestureRecognizer(delay: delay)
542
      ..onStart = (Offset position) {
543
        final Drag? result = onStart(position);
544
        if (result != null && hapticFeedbackOnStart)
545
          HapticFeedback.selectionClick();
546
        return result;
547
      };
Hixie's avatar
Hixie committed
548 549 550
  }
}

551
class _DraggableState<T extends Object> extends State<Draggable<T>> {
552
  @override
Hixie's avatar
Hixie committed
553 554
  void initState() {
    super.initState();
555
    _recognizer = widget.createRecognizer(_startDrag);
Hixie's avatar
Hixie committed
556 557
  }

558 559
  @override
  void dispose() {
560
    _disposeRecognizerIfInactive();
561 562 563
    super.dispose();
  }

564 565 566 567 568 569 570 571 572
  // 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.
573
  GestureRecognizer? _recognizer;
574
  int _activeCount = 0;
Hixie's avatar
Hixie committed
575

576 577 578
  void _disposeRecognizerIfInactive() {
    if (_activeCount > 0)
      return;
579
    _recognizer!.dispose();
580 581 582
    _recognizer = null;
  }

583
  void _routePointer(PointerDownEvent event) {
584
    if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags!)
585
      return;
586
    _recognizer!.addPointer(event);
Hixie's avatar
Hixie committed
587 588
  }

589 590
  _DragAvatar<T>? _startDrag(Offset position) {
    if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags!)
591
      return null;
592
    final Offset dragStartPoint;
593 594 595 596 597 598 599 600 601 602 603
    if (widget.dragAnchorStrategy == null) {
      switch (widget.dragAnchor) {
        case DragAnchor.child:
          dragStartPoint = childDragAnchorStrategy(widget, context, position);
          break;
        case DragAnchor.pointer:
          dragStartPoint = pointerDragAnchorStrategy(widget, context, position);
          break;
      }
    } else {
      dragStartPoint = widget.dragAnchorStrategy!(widget, context, position);
604
    }
605 606 607
    setState(() {
      _activeCount += 1;
    });
608
    final _DragAvatar<T> avatar = _DragAvatar<T>(
609
      overlayState: Overlay.of(context, debugRequiredFor: widget, rootOverlay: widget.rootOverlay)!,
610
      data: widget.data,
611
      axis: widget.axis,
Hixie's avatar
Hixie committed
612
      initialPosition: position,
613
      dragStartPoint: dragStartPoint,
614 615
      feedback: widget.feedback,
      feedbackOffset: widget.feedbackOffset,
616
      ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics,
617 618 619 620 621
      onDragUpdate: (DragUpdateDetails details) {
        if (mounted && widget.onDragUpdate != null) {
          widget.onDragUpdate!(details);
        }
      },
622
      onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) {
623 624 625 626 627
        if (mounted) {
          setState(() {
            _activeCount -= 1;
          });
        } else {
Hixie's avatar
Hixie committed
628
          _activeCount -= 1;
629 630
          _disposeRecognizerIfInactive();
        }
631
        if (mounted && widget.onDragEnd != null) {
632
          widget.onDragEnd!(DraggableDetails(
633 634
              wasAccepted: wasAccepted,
              velocity: velocity,
635
              offset: offset,
636 637
          ));
        }
638
        if (wasAccepted && widget.onDragCompleted != null)
639
          widget.onDragCompleted!();
640
        if (!wasAccepted && widget.onDraggableCanceled != null)
641
          widget.onDraggableCanceled!(velocity, offset);
642
      },
643
    );
644
    widget.onDragStarted?.call();
645
    return avatar;
646 647
  }

648
  @override
649
  Widget build(BuildContext context) {
650
    assert(Overlay.of(context, debugRequiredFor: widget, rootOverlay: widget.rootOverlay) != null);
651
    final bool canDrag = widget.maxSimultaneousDrags == null ||
652
                         _activeCount < widget.maxSimultaneousDrags!;
653
    final bool showChild = _activeCount == 0 || widget.childWhenDragging == null;
654
    return Listener(
655
      behavior: widget.hitTestBehavior,
656
      onPointerDown: canDrag ? _routePointer : null,
657
      child: showChild ? widget.child : widget.childWhenDragging,
658 659 660 661
    );
  }
}

662 663 664 665 666 667 668 669 670 671 672 673 674 675 676
/// 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,
677 678
    required this.velocity,
    required this.offset,
679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
  }) : 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;
}

694 695 696 697 698
/// Represents the details when a pointer event occurred on the [DragTarget].
class DragTargetDetails<T> {
  /// Creates details for a [DragTarget] callback.
  ///
  /// The [offset] must not be null.
699
  DragTargetDetails({required this.data, required this.offset}) : assert(offset != null);
700 701 702 703 704 705 706 707 708

  /// The data that was dropped onto this [DragTarget].
  final T data;

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

709
/// A widget that receives data when a [Draggable] widget is dropped.
710 711 712 713 714 715 716 717 718 719 720
///
/// 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]
721
class DragTarget<T extends Object> extends StatefulWidget {
722 723 724
  /// Creates a widget that receives drags.
  ///
  /// The [builder] argument must not be null.
725
  const DragTarget({
726 727
    Key? key,
    required this.builder,
728
    this.onWillAccept,
729
    this.onAccept,
730
    this.onAcceptWithDetails,
731
    this.onLeave,
732
    this.onMove,
733
    this.hitTestBehavior = HitTestBehavior.translucent,
734 735
  }) : super(key: key);

Adam Barth's avatar
Adam Barth committed
736 737 738 739
  /// Called to build the contents of this widget.
  ///
  /// The builder can build different widgets depending on what is being dragged
  /// into this drag target.
740
  final DragTargetBuilder<T> builder;
Adam Barth's avatar
Adam Barth committed
741 742 743

  /// Called to determine whether this widget is interested in receiving a given
  /// piece of data being dragged over this drag target.
744
  ///
745
  /// Called when a piece of data enters the target. This will be followed by
746 747
  /// either [onAccept] and [onAcceptWithDetails], if the data is dropped, or
  /// [onLeave], if the drag leaves the target.
748
  final DragTargetWillAccept<T>? onWillAccept;
Adam Barth's avatar
Adam Barth committed
749 750

  /// Called when an acceptable piece of data was dropped over this drag target.
751 752
  ///
  /// Equivalent to [onAcceptWithDetails], but only includes the data.
753
  final DragTargetAccept<T>? onAccept;
754

755 756 757 758
  /// Called when an acceptable piece of data was dropped over this drag target.
  ///
  /// Equivalent to [onAccept], but with information, including the data, in a
  /// [DragTargetDetails].
759
  final DragTargetAcceptWithDetails<T>? onAcceptWithDetails;
760

761 762
  /// Called when a given piece of data being dragged over this target leaves
  /// the target.
763
  final DragTargetLeave<T>? onLeave;
764

765 766 767
  /// Called when a [Draggable] moves within this [DragTarget].
  ///
  /// Note that this includes entering and leaving the target.
768
  final DragTargetMove<T>? onMove;
769

770 771 772 773 774
  /// How to behave during hit testing.
  ///
  /// Defaults to [HitTestBehavior.translucent].
  final HitTestBehavior hitTestBehavior;

775
  @override
776
  _DragTargetState<T> createState() => _DragTargetState<T>();
777
}
778

779 780
List<T?> _mapAvatarsToData<T extends Object>(List<_DragAvatar<Object>> avatars) {
  return avatars.map<T?>((_DragAvatar<Object> avatar) => avatar.data as T?).toList();
781 782
}

783
class _DragTargetState<T extends Object> extends State<DragTarget<T>> {
784
  final List<_DragAvatar<Object>> _candidateAvatars = <_DragAvatar<Object>>[];
785
  final List<_DragAvatar<Object>> _rejectedAvatars = <_DragAvatar<Object>>[];
786

787 788 789 790 791 792 793 794 795 796
  // On non-web platforms, checks if data Object is equal to type[T] or subtype of [T].
  // On web, it does the same, but requires a check for ints and doubles
  // because dart doubles and ints are backed by the same kind of object on web.
  // JavaScript does not support integers.
  bool isExpectedDataType(Object? data, Type type) {
    if (kIsWeb && ((type == int && T == double) || (type == double && T == int)))
      return false;
    return data is T?;
  }

797
  bool didEnter(_DragAvatar<Object> avatar) {
798 799
    assert(!_candidateAvatars.contains(avatar));
    assert(!_rejectedAvatars.contains(avatar));
800
    if (widget.onWillAccept == null || widget.onWillAccept!(avatar.data as T?)) {
801
      setState(() {
802
        _candidateAvatars.add(avatar);
803 804
      });
      return true;
805 806 807 808 809
    } else {
      setState(() {
        _rejectedAvatars.add(avatar);
      });
      return false;
810 811 812
    }
  }

813
  void didLeave(_DragAvatar<Object> avatar) {
814
    assert(_candidateAvatars.contains(avatar) || _rejectedAvatars.contains(avatar));
815 816
    if (!mounted)
      return;
817
    setState(() {
818 819
      _candidateAvatars.remove(avatar);
      _rejectedAvatars.remove(avatar);
820
    });
821
    widget.onLeave?.call(avatar.data as T?);
822 823
  }

824
  void didDrop(_DragAvatar<Object> avatar) {
825
    assert(_candidateAvatars.contains(avatar));
826 827
    if (!mounted)
      return;
828
    setState(() {
829
      _candidateAvatars.remove(avatar);
830
    });
831 832
    widget.onAccept?.call(avatar.data! as T);
    widget.onAcceptWithDetails?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
833 834
  }

835 836 837
  void didMove(_DragAvatar<Object> avatar) {
    if (!mounted)
      return;
838
    widget.onMove?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
839 840
  }

841
  @override
842
  Widget build(BuildContext context) {
843
    assert(widget.builder != null);
844
    return MetaData(
845
      metaData: this,
846
      behavior: widget.hitTestBehavior,
847
      child: widget.builder(context, _mapAvatarsToData<T>(_candidateAvatars), _mapAvatarsToData<Object>(_rejectedAvatars)),
848
    );
849 850 851
  }
}

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

Hixie's avatar
Hixie committed
855 856
// 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
857
// overlay goes away. _DraggableState has some delicate logic to continue
Josh Soref's avatar
Josh Soref committed
858
// needing this object pointer events even after it has been disposed.
859
class _DragAvatar<T extends Object> extends Drag {
Adam Barth's avatar
Adam Barth committed
860
  _DragAvatar({
861
    required this.overlayState,
862
    this.data,
863
    this.axis,
864
    required Offset initialPosition,
865
    this.dragStartPoint = Offset.zero,
866
    this.feedback,
867
    this.feedbackOffset = Offset.zero,
868
    this.onDragUpdate,
869
    this.onDragEnd,
870
    required this.ignoringFeedbackSemantics,
871
  }) : assert(overlayState != null),
872
       assert(ignoringFeedbackSemantics != null),
873
       assert(dragStartPoint != null),
874 875
       assert(feedbackOffset != null),
       _position = initialPosition {
876
    _entry = OverlayEntry(builder: _build);
877
    overlayState.insert(_entry!);
878
    updateDrag(initialPosition);
879
  }
880

881 882
  final T? data;
  final Axis? axis;
883
  final Offset dragStartPoint;
884
  final Widget? feedback;
885
  final Offset feedbackOffset;
886
  final DragUpdateCallback? onDragUpdate;
887
  final _OnDragEnd? onDragEnd;
888
  final OverlayState overlayState;
889
  final bool ignoringFeedbackSemantics;
890

891 892
  _DragTargetState<Object>? _activeTarget;
  final List<_DragTargetState<Object>> _enteredTargets = <_DragTargetState<Object>>[];
893
  Offset _position;
894 895
  Offset? _lastOffset;
  OverlayEntry? _entry;
896

897
  @override
898
  void update(DragUpdateDetails details) {
899
    final Offset oldPosition = _position;
900
    _position += _restrictAxis(details.delta);
901
    updateDrag(_position);
902 903 904
    if (onDragUpdate != null && _position != oldPosition) {
      onDragUpdate!(details);
    }
905
  }
906 907

  @override
908
  void end(DragEndDetails details) {
909
    finishDrag(_DragEndKind.dropped, _restrictVelocityAxis(details.velocity));
910
  }
911

912

913
  @override
914
  void cancel() {
915
    finishDrag(_DragEndKind.canceled);
Hixie's avatar
Hixie committed
916 917
  }

918
  void updateDrag(Offset globalPosition) {
919
    _lastOffset = globalPosition - dragStartPoint;
920
    _entry!.markNeedsBuild();
921
    final HitTestResult result = HitTestResult();
922
    WidgetsBinding.instance!.hitTest(result, globalPosition + feedbackOffset);
923

924
    final List<_DragTargetState<Object>> targets = _getDragTargets(result.path).toList();
925 926

    bool listsMatch = false;
927
    if (targets.length >= _enteredTargets.length && _enteredTargets.isNotEmpty) {
928
      listsMatch = true;
929
      final Iterator<_DragTargetState<Object>> iterator = targets.iterator;
930
      for (int i = 0; i < _enteredTargets.length; i += 1) {
931
        iterator.moveNext();
932
        if (iterator.current != _enteredTargets[i]) {
933 934 935 936 937 938
          listsMatch = false;
          break;
        }
      }
    }

939 940
    // If everything's the same, report moves, and bail early.
    if (listsMatch) {
941
      for (final _DragTargetState<Object> target in _enteredTargets) {
942 943
        target.didMove(this);
      }
944
      return;
945
    }
946 947

    // Leave old targets.
948
    _leaveAllEntered();
949 950

    // Enter new targets.
951 952
    final _DragTargetState<Object>? newTarget = targets.cast<_DragTargetState<Object>?>().firstWhere(
      (_DragTargetState<Object>? target) {
953 954
        if (target == null)
          return false;
955
        _enteredTargets.add(target);
956
        return target.didEnter(this);
957
      },
958
      orElse: () => null,
959 960
    );

961
    // Report moves to the targets.
962
    for (final _DragTargetState<Object> target in _enteredTargets) {
963 964 965
      target.didMove(this);
    }

966
    _activeTarget = newTarget;
967 968
  }

969
  Iterable<_DragTargetState<Object>> _getDragTargets(Iterable<HitTestEntry> path) sync* {
970
    // Look for the RenderBoxes that corresponds to the hit target (the hit target
971
    // widgets build RenderMetaData boxes for us for this purpose).
972
    for (final HitTestEntry entry in path) {
973 974 975
      final HitTestTarget target = entry.target;
      if (target is RenderMetaData) {
        final dynamic metaData = target.metaData;
976
        if (metaData is _DragTargetState && metaData.isExpectedDataType(data, T))
977
          yield metaData;
978 979
      }
    }
980 981
  }

982 983
  void _leaveAllEntered() {
    for (int i = 0; i < _enteredTargets.length; i += 1)
984
      _enteredTargets[i].didLeave(this);
985 986 987
    _enteredTargets.clear();
  }

988
  void finishDrag(_DragEndKind endKind, [ Velocity? velocity ]) {
989
    bool wasAccepted = false;
990
    if (endKind == _DragEndKind.dropped && _activeTarget != null) {
991
      _activeTarget!.didDrop(this);
992 993
      wasAccepted = true;
      _enteredTargets.remove(_activeTarget);
994
    }
995
    _leaveAllEntered();
996
    _activeTarget = null;
997
    _entry!.remove();
998
    _entry = null;
999
    // TODO(ianh): consider passing _entry as well so the client can perform an animation.
1000
    onDragEnd?.call(velocity ?? Velocity.zero, _lastOffset!, wasAccepted);
1001 1002
  }

Adam Barth's avatar
Adam Barth committed
1003
  Widget _build(BuildContext context) {
1004
    final RenderBox box = overlayState.context.findRenderObject()! as RenderBox;
1005
    final Offset overlayTopLeft = box.localToGlobal(Offset.zero);
1006
    return Positioned(
1007 1008
      left: _lastOffset!.dx - overlayTopLeft.dx,
      top: _lastOffset!.dy - overlayTopLeft.dy,
1009
      child: IgnorePointer(
1010 1011
        child: feedback,
        ignoringSemantics: ignoringFeedbackSemantics,
1012
      ),
1013
    );
1014
  }
1015 1016 1017 1018 1019

  Velocity _restrictVelocityAxis(Velocity velocity) {
    if (axis == null) {
      return velocity;
    }
1020
    return Velocity(
1021 1022 1023 1024 1025 1026 1027
      pixelsPerSecond: _restrictAxis(velocity.pixelsPerSecond),
    );
  }

  Offset _restrictAxis(Offset offset) {
    if (axis == null) {
      return offset;
1028
    }
1029
    if (axis == Axis.horizontal) {
1030
      return Offset(offset.dx, 0.0);
1031
    }
1032
    return Offset(0.0, offset.dy);
1033
  }
1034
}