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

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

17
/// Signature for determining whether the given data will be accepted by a [DragTarget].
18 19
///
/// Used by [DragTarget.onWillAccept].
20
typedef DragTargetWillAccept<T> = bool Function(T? data);
21 22

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

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

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

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

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

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

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

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

71
/// Signature for the strategy that determines the drag start point of a [Draggable].
72
///
73 74 75 76 77 78 79 80 81
/// 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.
82 83
typedef DragAnchorStrategy = Offset Function(Draggable<Object> draggable, BuildContext context, Offset position);

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

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.)
///
/// 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

121 122 123 124 125
/// 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
126 127
/// the opportunity to accept the [data] carried by the draggable.
///
128 129 130 131 132
/// The [ignoringFeedbackPointer] defaults to true, which means that
/// the [feedback] widget ignores the pointer during hit testing. Similarly,
/// [ignoringFeedbackSemantics] defaults to true, and the [feedback] also ignores
/// semantics when building the semantics tree.
///
133 134 135 136 137 138 139 140 141
/// 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].
142
///
143 144
/// {@youtube 560 315 https://www.youtube.com/watch?v=QzA4c4QHZCY}
///
145
/// {@tool dartpad}
146 147 148 149
/// 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.
///
150
/// ** See code in examples/api/lib/widgets/drag_target/draggable.0.dart **
151 152
/// {@end-tool}
///
153 154 155 156
/// See also:
///
///  * [DragTarget]
///  * [LongPressDraggable]
157
class Draggable<T extends Object> extends StatefulWidget {
158
  /// Creates a widget that can be dragged to a [DragTarget].
159 160
  ///
  /// The [child] and [feedback] arguments must not be null. If
161
  /// [maxSimultaneousDrags] is non-null, it must be non-negative.
162
  const Draggable({
163
    super.key,
164 165
    required this.child,
    required this.feedback,
166
    this.data,
167
    this.axis,
168
    this.childWhenDragging,
169
    this.feedbackOffset = Offset.zero,
170
    this.dragAnchorStrategy = childDragAnchorStrategy,
171
    this.affinity,
172
    this.maxSimultaneousDrags,
173
    this.onDragStarted,
174
    this.onDragUpdate,
175
    this.onDraggableCanceled,
176
    this.onDragEnd,
177
    this.onDragCompleted,
178
    this.ignoringFeedbackSemantics = true,
179
    this.ignoringFeedbackPointer = true,
180
    this.rootOverlay = false,
181
    this.hitTestBehavior = HitTestBehavior.deferToChild,
182 183
  }) : assert(child != null),
       assert(feedback != null),
184
       assert(ignoringFeedbackSemantics != null),
185
       assert(ignoringFeedbackPointer != null),
186 187
       assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0),
       assert(dragAnchorStrategy != null);
188

189
  /// The data that will be dropped by this draggable.
190
  final T? data;
191

192 193 194 195 196 197 198 199 200 201 202
  /// 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].
203
  final Axis? axis;
204

205
  /// The widget below this widget in the tree.
206 207 208 209 210 211 212 213 214 215
  ///
  /// 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].
216
  ///
217
  /// {@macro flutter.widgets.ProxyWidget.child}
218
  final Widget child;
Adam Barth's avatar
Adam Barth committed
219

220
  /// The widget to display instead of [child] when one or more drags are under way.
221
  ///
222 223
  /// 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
224
  /// way).
225 226 227 228 229
  ///
  /// 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].
230
  final Widget? childWhenDragging;
231 232

  /// The widget to show under the pointer when a drag is under way.
233 234 235
  ///
  /// See [child] and [childWhenDragging] for information about what is shown
  /// at the location of the [Draggable] itself when a drag is under way.
236 237
  final Widget feedback;

238 239 240 241
  /// 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
242

243 244
  /// A strategy that is used by this draggable to get the anchor offset when it
  /// is dragged.
245
  ///
246 247
  /// The anchor offset refers to the distance between the users' fingers and
  /// the [feedback] widget when this draggable is dragged.
248
  ///
249 250 251 252 253 254 255 256 257
  /// 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.
  ///
258 259
  /// Defaults to [childDragAnchorStrategy].
  final DragAnchorStrategy dragAnchorStrategy;
260

261 262
  /// Whether the semantics of the [feedback] widget is ignored when building
  /// the semantics tree.
263
  ///
264
  /// This value should be set to false when the [feedback] widget is intended
265
  /// to be the same object as the [child]. Placing a [GlobalKey] on this
266 267 268 269 270 271
  /// 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;

272 273 274 275 276 277 278 279
  /// Whether the [feedback] widget is ignored during hit testing.
  ///
  /// Regardless of whether this widget is ignored during hit testing, it will
  /// still consume space during layout and be visible during painting.
  ///
  /// Defaults to true.
  final bool ignoringFeedbackPointer;

280 281 282 283 284 285 286 287 288 289 290 291 292 293
  /// 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.
294 295 296
  ///
  /// For the directions this widget can be dragged in after the drag event
  /// starts, see [Draggable.axis].
297
  final Axis? affinity;
298

299 300 301 302 303
  /// 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.
304 305 306
  ///
  /// If you set this property to 1, consider supplying an "empty" widget for
  /// [childWhenDragging] to create the illusion of actually moving [child].
307
  final int? maxSimultaneousDrags;
308

309
  /// Called when the draggable starts being dragged.
310
  final VoidCallback? onDragStarted;
311

312
  /// Called when the draggable is dragged.
313 314 315 316 317
  ///
  /// 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;

318
  /// Called when the draggable is dropped without being accepted by a [DragTarget].
319 320 321 322 323 324 325
  ///
  /// 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.
326
  final DraggableCanceledCallback? onDraggableCanceled;
327

328 329 330 331 332 333 334 335
  /// 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.
336
  final VoidCallback? onDragCompleted;
337

338 339 340 341 342 343 344 345
  /// 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).
346
  final DragEndCallback? onDragEnd;
347

348 349 350 351 352 353 354 355 356
  /// 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;

357 358 359 360 361
  /// How to behave during hit test.
  ///
  /// Defaults to [HitTestBehavior.deferToChild].
  final HitTestBehavior hitTestBehavior;

362
  /// Creates a gesture recognizer that recognizes the start of the drag.
363
  ///
364 365 366
  /// Subclasses can override this function to customize when they start
  /// recognizing a drag.
  @protected
367
  MultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
368 369
    switch (affinity) {
      case Axis.horizontal:
370
        return HorizontalMultiDragGestureRecognizer()..onStart = onStart;
371
      case Axis.vertical:
372
        return VerticalMultiDragGestureRecognizer()..onStart = onStart;
373 374
      case null:
        return ImmediateMultiDragGestureRecognizer()..onStart = onStart;
375
    }
Hixie's avatar
Hixie committed
376
  }
377

378
  @override
379
  State<Draggable<T>> createState() => _DraggableState<T>();
380 381
}

Adam Barth's avatar
Adam Barth committed
382
/// Makes its child draggable starting from long press.
383 384 385 386 387
///
/// See also:
///
///  * [Draggable], similar to the [LongPressDraggable] widget but happens immediately.
///  * [DragTarget], a widget that receives data when a [Draggable] widget is dropped.
388
class LongPressDraggable<T extends Object> extends Draggable<T> {
389 390 391
  /// Creates a widget that can be dragged starting from long press.
  ///
  /// The [child] and [feedback] arguments must not be null. If
392
  /// [maxSimultaneousDrags] is non-null, it must be non-negative.
393
  const LongPressDraggable({
394 395 396 397 398 399 400 401 402 403 404 405 406 407
    super.key,
    required super.child,
    required super.feedback,
    super.data,
    super.axis,
    super.childWhenDragging,
    super.feedbackOffset,
    super.dragAnchorStrategy,
    super.maxSimultaneousDrags,
    super.onDragStarted,
    super.onDragUpdate,
    super.onDraggableCanceled,
    super.onDragEnd,
    super.onDragCompleted,
408
    this.hapticFeedbackOnStart = true,
409
    super.ignoringFeedbackSemantics,
410
    super.ignoringFeedbackPointer,
411
    this.delay = kLongPressTimeout,
412
  });
Hixie's avatar
Hixie committed
413

414 415 416
  /// Whether haptic feedback should be triggered on drag start.
  final bool hapticFeedbackOnStart;

417 418 419 420 421
  /// The duration that a user has to press down before a long press is registered.
  ///
  /// Defaults to [kLongPressTimeout].
  final Duration delay;

422
  @override
423
  DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
424
    return DelayedMultiDragGestureRecognizer(delay: delay)
425
      ..onStart = (Offset position) {
426
        final Drag? result = onStart(position);
427
        if (result != null && hapticFeedbackOnStart) {
428
          HapticFeedback.selectionClick();
429
        }
430
        return result;
431
      };
Hixie's avatar
Hixie committed
432 433 434
  }
}

435
class _DraggableState<T extends Object> extends State<Draggable<T>> {
436
  @override
Hixie's avatar
Hixie committed
437 438
  void initState() {
    super.initState();
439
    _recognizer = widget.createRecognizer(_startDrag);
Hixie's avatar
Hixie committed
440 441
  }

442 443
  @override
  void dispose() {
444
    _disposeRecognizerIfInactive();
445 446 447
    super.dispose();
  }

448 449 450 451 452 453
  @override
  void didChangeDependencies() {
    _recognizer!.gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings;
    super.didChangeDependencies();
  }

454 455 456 457 458 459 460 461 462
  // 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.
463
  GestureRecognizer? _recognizer;
464
  int _activeCount = 0;
Hixie's avatar
Hixie committed
465

466
  void _disposeRecognizerIfInactive() {
467
    if (_activeCount > 0) {
468
      return;
469
    }
470
    _recognizer!.dispose();
471 472 473
    _recognizer = null;
  }

474
  void _routePointer(PointerDownEvent event) {
475
    if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags!) {
476
      return;
477
    }
478
    _recognizer!.addPointer(event);
Hixie's avatar
Hixie committed
479 480
  }

481
  _DragAvatar<T>? _startDrag(Offset position) {
482
    if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags!) {
483
      return null;
484
    }
485
    final Offset dragStartPoint;
486
    dragStartPoint = widget.dragAnchorStrategy(widget, context, position);
487 488 489
    setState(() {
      _activeCount += 1;
    });
490
    final _DragAvatar<T> avatar = _DragAvatar<T>(
491
      overlayState: Overlay.of(context, debugRequiredFor: widget, rootOverlay: widget.rootOverlay),
492
      data: widget.data,
493
      axis: widget.axis,
Hixie's avatar
Hixie committed
494
      initialPosition: position,
495
      dragStartPoint: dragStartPoint,
496 497
      feedback: widget.feedback,
      feedbackOffset: widget.feedbackOffset,
498
      ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics,
499
      ignoringFeedbackPointer: widget.ignoringFeedbackPointer,
500 501 502 503 504
      onDragUpdate: (DragUpdateDetails details) {
        if (mounted && widget.onDragUpdate != null) {
          widget.onDragUpdate!(details);
        }
      },
505
      onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) {
506 507 508 509 510
        if (mounted) {
          setState(() {
            _activeCount -= 1;
          });
        } else {
Hixie's avatar
Hixie committed
511
          _activeCount -= 1;
512 513
          _disposeRecognizerIfInactive();
        }
514
        if (mounted && widget.onDragEnd != null) {
515
          widget.onDragEnd!(DraggableDetails(
516 517
              wasAccepted: wasAccepted,
              velocity: velocity,
518
              offset: offset,
519 520
          ));
        }
521
        if (wasAccepted && widget.onDragCompleted != null) {
522
          widget.onDragCompleted!();
523 524
        }
        if (!wasAccepted && widget.onDraggableCanceled != null) {
525
          widget.onDraggableCanceled!(velocity, offset);
526
        }
527
      },
528
    );
529
    widget.onDragStarted?.call();
530
    return avatar;
531 532
  }

533
  @override
534
  Widget build(BuildContext context) {
535
    assert(debugCheckHasOverlay(context));
536
    final bool canDrag = widget.maxSimultaneousDrags == null ||
537
                         _activeCount < widget.maxSimultaneousDrags!;
538
    final bool showChild = _activeCount == 0 || widget.childWhenDragging == null;
539
    return Listener(
540
      behavior: widget.hitTestBehavior,
541
      onPointerDown: canDrag ? _routePointer : null,
542
      child: showChild ? widget.child : widget.childWhenDragging,
543 544 545 546
    );
  }
}

547 548 549 550 551 552 553 554 555 556 557 558 559 560 561
/// 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,
562 563
    required this.velocity,
    required this.offset,
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
  }) : 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;
}

579 580 581 582 583
/// 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.
584
  DragTargetDetails({required this.data, required this.offset}) : assert(offset != null);
585 586

  /// The data that was dropped onto this [DragTarget].
587
  final T data;
588 589 590 591 592 593

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

594
/// A widget that receives data when a [Draggable] widget is dropped.
595 596 597 598 599 600 601 602 603 604 605
///
/// 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]
606
class DragTarget<T extends Object> extends StatefulWidget {
607 608 609
  /// Creates a widget that receives drags.
  ///
  /// The [builder] argument must not be null.
610
  const DragTarget({
611
    super.key,
612
    required this.builder,
613
    this.onWillAccept,
614
    this.onAccept,
615
    this.onAcceptWithDetails,
616
    this.onLeave,
617
    this.onMove,
618
    this.hitTestBehavior = HitTestBehavior.translucent,
619
  });
620

Adam Barth's avatar
Adam Barth committed
621 622 623 624
  /// Called to build the contents of this widget.
  ///
  /// The builder can build different widgets depending on what is being dragged
  /// into this drag target.
625
  final DragTargetBuilder<T> builder;
Adam Barth's avatar
Adam Barth committed
626 627 628

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

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

640 641 642 643
  /// 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].
644
  final DragTargetAcceptWithDetails<T>? onAcceptWithDetails;
645

646 647
  /// Called when a given piece of data being dragged over this target leaves
  /// the target.
648
  final DragTargetLeave<T>? onLeave;
649

650 651 652
  /// Called when a [Draggable] moves within this [DragTarget].
  ///
  /// Note that this includes entering and leaving the target.
653
  final DragTargetMove<T>? onMove;
654

655 656 657 658 659
  /// How to behave during hit testing.
  ///
  /// Defaults to [HitTestBehavior.translucent].
  final HitTestBehavior hitTestBehavior;

660
  @override
661
  State<DragTarget<T>> createState() => _DragTargetState<T>();
662
}
663

664 665
List<T?> _mapAvatarsToData<T extends Object>(List<_DragAvatar<Object>> avatars) {
  return avatars.map<T?>((_DragAvatar<Object> avatar) => avatar.data as T?).toList();
666 667
}

668
class _DragTargetState<T extends Object> extends State<DragTarget<T>> {
669
  final List<_DragAvatar<Object>> _candidateAvatars = <_DragAvatar<Object>>[];
670
  final List<_DragAvatar<Object>> _rejectedAvatars = <_DragAvatar<Object>>[];
671

672 673 674 675 676
  // 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) {
677
    if (kIsWeb && ((type == int && T == double) || (type == double && T == int))) {
678
      return false;
679
    }
680 681 682
    return data is T?;
  }

683
  bool didEnter(_DragAvatar<Object> avatar) {
684 685
    assert(!_candidateAvatars.contains(avatar));
    assert(!_rejectedAvatars.contains(avatar));
686
    if (widget.onWillAccept == null || widget.onWillAccept!(avatar.data as T?)) {
687
      setState(() {
688
        _candidateAvatars.add(avatar);
689 690
      });
      return true;
691 692 693 694 695
    } else {
      setState(() {
        _rejectedAvatars.add(avatar);
      });
      return false;
696 697 698
    }
  }

699
  void didLeave(_DragAvatar<Object> avatar) {
700
    assert(_candidateAvatars.contains(avatar) || _rejectedAvatars.contains(avatar));
701
    if (!mounted) {
702
      return;
703
    }
704
    setState(() {
705 706
      _candidateAvatars.remove(avatar);
      _rejectedAvatars.remove(avatar);
707
    });
708
    widget.onLeave?.call(avatar.data as T?);
709 710
  }

711
  void didDrop(_DragAvatar<Object> avatar) {
712
    assert(_candidateAvatars.contains(avatar));
713
    if (!mounted) {
714
      return;
715
    }
716
    setState(() {
717
      _candidateAvatars.remove(avatar);
718
    });
719 720
    widget.onAccept?.call(avatar.data! as T);
    widget.onAcceptWithDetails?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
721 722
  }

723
  void didMove(_DragAvatar<Object> avatar) {
724
    if (!mounted) {
725
      return;
726
    }
727
    widget.onMove?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
728 729
  }

730
  @override
731
  Widget build(BuildContext context) {
732
    assert(widget.builder != null);
733
    return MetaData(
734
      metaData: this,
735
      behavior: widget.hitTestBehavior,
736
      child: widget.builder(context, _mapAvatarsToData<T>(_candidateAvatars), _mapAvatarsToData<Object>(_rejectedAvatars)),
737
    );
738 739 740
  }
}

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

Hixie's avatar
Hixie committed
744 745
// 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
746
// overlay goes away. _DraggableState has some delicate logic to continue
Josh Soref's avatar
Josh Soref committed
747
// needing this object pointer events even after it has been disposed.
748
class _DragAvatar<T extends Object> extends Drag {
Adam Barth's avatar
Adam Barth committed
749
  _DragAvatar({
750
    required this.overlayState,
751
    this.data,
752
    this.axis,
753
    required Offset initialPosition,
754
    this.dragStartPoint = Offset.zero,
755
    this.feedback,
756
    this.feedbackOffset = Offset.zero,
757
    this.onDragUpdate,
758
    this.onDragEnd,
759
    required this.ignoringFeedbackSemantics,
760
    required this.ignoringFeedbackPointer,
761
  }) : assert(overlayState != null),
762
       assert(ignoringFeedbackSemantics != null),
763
       assert(ignoringFeedbackPointer != null),
764
       assert(dragStartPoint != null),
765 766
       assert(feedbackOffset != null),
       _position = initialPosition {
767
    _entry = OverlayEntry(builder: _build);
768
    overlayState.insert(_entry!);
769
    updateDrag(initialPosition);
770
  }
771

772 773
  final T? data;
  final Axis? axis;
774
  final Offset dragStartPoint;
775
  final Widget? feedback;
776
  final Offset feedbackOffset;
777
  final DragUpdateCallback? onDragUpdate;
778
  final _OnDragEnd? onDragEnd;
779
  final OverlayState overlayState;
780
  final bool ignoringFeedbackSemantics;
781
  final bool ignoringFeedbackPointer;
782

783 784
  _DragTargetState<Object>? _activeTarget;
  final List<_DragTargetState<Object>> _enteredTargets = <_DragTargetState<Object>>[];
785
  Offset _position;
786 787
  Offset? _lastOffset;
  OverlayEntry? _entry;
788

789
  @override
790
  void update(DragUpdateDetails details) {
791
    final Offset oldPosition = _position;
792
    _position += _restrictAxis(details.delta);
793
    updateDrag(_position);
794 795 796
    if (onDragUpdate != null && _position != oldPosition) {
      onDragUpdate!(details);
    }
797
  }
798 799

  @override
800
  void end(DragEndDetails details) {
801
    finishDrag(_DragEndKind.dropped, _restrictVelocityAxis(details.velocity));
802
  }
803

804

805
  @override
806
  void cancel() {
807
    finishDrag(_DragEndKind.canceled);
Hixie's avatar
Hixie committed
808 809
  }

810
  void updateDrag(Offset globalPosition) {
811
    _lastOffset = globalPosition - dragStartPoint;
812
    _entry!.markNeedsBuild();
813
    final HitTestResult result = HitTestResult();
814
    WidgetsBinding.instance.hitTest(result, globalPosition + feedbackOffset);
815

816
    final List<_DragTargetState<Object>> targets = _getDragTargets(result.path).toList();
817 818

    bool listsMatch = false;
819
    if (targets.length >= _enteredTargets.length && _enteredTargets.isNotEmpty) {
820
      listsMatch = true;
821
      final Iterator<_DragTargetState<Object>> iterator = targets.iterator;
822
      for (int i = 0; i < _enteredTargets.length; i += 1) {
823
        iterator.moveNext();
824
        if (iterator.current != _enteredTargets[i]) {
825 826 827 828 829 830
          listsMatch = false;
          break;
        }
      }
    }

831 832
    // If everything's the same, report moves, and bail early.
    if (listsMatch) {
833
      for (final _DragTargetState<Object> target in _enteredTargets) {
834 835
        target.didMove(this);
      }
836
      return;
837
    }
838 839

    // Leave old targets.
840
    _leaveAllEntered();
841 842

    // Enter new targets.
843 844
    final _DragTargetState<Object>? newTarget = targets.cast<_DragTargetState<Object>?>().firstWhere(
      (_DragTargetState<Object>? target) {
845
        if (target == null) {
846
          return false;
847
        }
848
        _enteredTargets.add(target);
849
        return target.didEnter(this);
850
      },
851
      orElse: () => null,
852 853
    );

854
    // Report moves to the targets.
855
    for (final _DragTargetState<Object> target in _enteredTargets) {
856 857 858
      target.didMove(this);
    }

859
    _activeTarget = newTarget;
860 861
  }

862
  Iterable<_DragTargetState<Object>> _getDragTargets(Iterable<HitTestEntry> path) {
863
    // Look for the RenderBoxes that corresponds to the hit target (the hit target
864
    // widgets build RenderMetaData boxes for us for this purpose).
865
    final List<_DragTargetState<Object>> targets = <_DragTargetState<Object>>[];
866
    for (final HitTestEntry entry in path) {
867 868 869
      final HitTestTarget target = entry.target;
      if (target is RenderMetaData) {
        final dynamic metaData = target.metaData;
870
        if (metaData is _DragTargetState && metaData.isExpectedDataType(data, T)) {
871
          targets.add(metaData);
872
        }
873 874
      }
    }
875
    return targets;
876 877
  }

878
  void _leaveAllEntered() {
879
    for (int i = 0; i < _enteredTargets.length; i += 1) {
880
      _enteredTargets[i].didLeave(this);
881
    }
882 883 884
    _enteredTargets.clear();
  }

885
  void finishDrag(_DragEndKind endKind, [ Velocity? velocity ]) {
886
    bool wasAccepted = false;
887
    if (endKind == _DragEndKind.dropped && _activeTarget != null) {
888
      _activeTarget!.didDrop(this);
889 890
      wasAccepted = true;
      _enteredTargets.remove(_activeTarget);
891
    }
892
    _leaveAllEntered();
893
    _activeTarget = null;
894
    _entry!.remove();
895
    _entry = null;
896
    // TODO(ianh): consider passing _entry as well so the client can perform an animation.
897
    onDragEnd?.call(velocity ?? Velocity.zero, _lastOffset!, wasAccepted);
898 899
  }

Adam Barth's avatar
Adam Barth committed
900
  Widget _build(BuildContext context) {
901
    final RenderBox box = overlayState.context.findRenderObject()! as RenderBox;
902
    final Offset overlayTopLeft = box.localToGlobal(Offset.zero);
903
    return Positioned(
904 905
      left: _lastOffset!.dx - overlayTopLeft.dx,
      top: _lastOffset!.dy - overlayTopLeft.dy,
906
      child: IgnorePointer(
907
        ignoring: ignoringFeedbackPointer,
908
        ignoringSemantics: ignoringFeedbackSemantics,
909
        child: feedback,
910
      ),
911
    );
912
  }
913 914 915 916 917

  Velocity _restrictVelocityAxis(Velocity velocity) {
    if (axis == null) {
      return velocity;
    }
918
    return Velocity(
919 920 921 922 923 924 925
      pixelsPerSecond: _restrictAxis(velocity.pixelsPerSecond),
    );
  }

  Offset _restrictAxis(Offset offset) {
    if (axis == null) {
      return offset;
926
    }
927
    if (axis == Axis.horizontal) {
928
      return Offset(offset.dx, 0.0);
929
    }
930
    return Offset(0.0, offset.dy);
931
  }
932
}