1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart' show kIsWeb;
6import 'package:flutter/gestures.dart';
7import 'package:flutter/rendering.dart';
8import 'package:flutter/services.dart';
9
10import 'basic.dart';
11import 'binding.dart';
12import 'debug.dart';
13import 'framework.dart';
14import 'media_query.dart';
15import 'overlay.dart';
16import 'view.dart';
17
18/// Signature for determining whether the given data will be accepted by a [DragTarget].
19///
20/// Used by [DragTarget.onWillAccept].
21typedef DragTargetWillAccept<T> = bool Function(T? data);
22
23/// Signature for determining whether the given data will be accepted by a [DragTarget],
24/// based on provided information.
25///
26/// Used by [DragTarget.onWillAcceptWithDetails].
27typedef DragTargetWillAcceptWithDetails<T> = bool Function(DragTargetDetails<T> details);
28
29/// Signature for causing a [DragTarget] to accept the given data.
30///
31/// Used by [DragTarget.onAccept].
32typedef DragTargetAccept<T> = void Function(T data);
33
34/// Signature for determining information about the acceptance by a [DragTarget].
35///
36/// Used by [DragTarget.onAcceptWithDetails].
37typedef DragTargetAcceptWithDetails<T> = void Function(DragTargetDetails<T> details);
38
39/// Signature for building children of a [DragTarget].
40///
41/// The `candidateData` argument contains the list of drag data that is hovering
42/// over this [DragTarget] and that has passed [DragTarget.onWillAccept]. The
43/// `rejectedData` argument contains the list of drag data that is hovering over
44/// this [DragTarget] and that will not be accepted by the [DragTarget].
45///
46/// Used by [DragTarget.builder].
47typedef DragTargetBuilder<T> = Widget Function(BuildContext context, List<T?> candidateData, List<dynamic> rejectedData);
48
49/// Signature for when a [Draggable] is dragged across the screen.
50///
51/// Used by [Draggable.onDragUpdate].
52typedef DragUpdateCallback = void Function(DragUpdateDetails details);
53
54/// Signature for when a [Draggable] is dropped without being accepted by a [DragTarget].
55///
56/// Used by [Draggable.onDraggableCanceled].
57typedef DraggableCanceledCallback = void Function(Velocity velocity, Offset offset);
58
59/// Signature for when the draggable is dropped.
60///
61/// The velocity and offset at which the pointer was moving when the draggable
62/// was dropped is available in the [DraggableDetails]. Also included in the
63/// `details` is whether the draggable's [DragTarget] accepted it.
64///
65/// Used by [Draggable.onDragEnd].
66typedef DragEndCallback = void Function(DraggableDetails details);
67
68/// Signature for when a [Draggable] leaves a [DragTarget].
69///
70/// Used by [DragTarget.onLeave].
71typedef DragTargetLeave<T> = void Function(T? data);
72
73/// Signature for when a [Draggable] moves within a [DragTarget].
74///
75/// Used by [DragTarget.onMove].
76typedef DragTargetMove<T> = void Function(DragTargetDetails<T> details);
77
78/// Signature for the strategy that determines the drag start point of a [Draggable].
79///
80/// Used by [Draggable.dragAnchorStrategy].
81///
82/// There are two built-in strategies:
83///
84/// * [childDragAnchorStrategy], which displays the feedback anchored at the
85/// position of the original child.
86///
87/// * [pointerDragAnchorStrategy], which displays the feedback anchored at the
88/// position of the touch that started the drag.
89typedef DragAnchorStrategy = Offset Function(Draggable<Object> draggable, BuildContext context, Offset position);
90
91/// Display the feedback anchored at the position of the original child.
92///
93/// If feedback is identical to the child, then this means the feedback will
94/// exactly overlap the original child when the drag starts.
95///
96/// This is the default [DragAnchorStrategy].
97///
98/// See also:
99///
100/// * [DragAnchorStrategy], the typedef that this function implements.
101/// * [Draggable.dragAnchorStrategy], for which this is a built-in value.
102Offset childDragAnchorStrategy(Draggable<Object> draggable, BuildContext context, Offset position) {
103 final RenderBox renderObject = context.findRenderObject()! as RenderBox;
104 return renderObject.globalToLocal(position);
105}
106
107/// Display the feedback anchored at the position of the touch that started
108/// the drag.
109///
110/// If feedback is identical to the child, then this means the top left of the
111/// feedback will be under the finger when the drag starts. This will likely not
112/// exactly overlap the original child, e.g. if the child is big and the touch
113/// was not centered. This mode is useful when the feedback is transformed so as
114/// to move the feedback to the left by half its width, and up by half its width
115/// plus the height of the finger, since then it appears as if putting the
116/// finger down makes the touch feedback appear above the finger. (It feels
117/// weird for it to appear offset from the original child if it's anchored to
118/// the child and not the finger.)
119///
120/// See also:
121///
122/// * [DragAnchorStrategy], the typedef that this function implements.
123/// * [Draggable.dragAnchorStrategy], for which this is a built-in value.
124Offset pointerDragAnchorStrategy(Draggable<Object> draggable, BuildContext context, Offset position) {
125 return Offset.zero;
126}
127
128/// A widget that can be dragged from to a [DragTarget].
129///
130/// When a draggable widget recognizes the start of a drag gesture, it displays
131/// a [feedback] widget that tracks the user's finger across the screen. If the
132/// user lifts their finger while on top of a [DragTarget], that target is given
133/// the opportunity to accept the [data] carried by the draggable.
134///
135/// The [ignoringFeedbackPointer] defaults to true, which means that
136/// the [feedback] widget ignores the pointer during hit testing. Similarly,
137/// [ignoringFeedbackSemantics] defaults to true, and the [feedback] also ignores
138/// semantics when building the semantics tree.
139///
140/// On multitouch devices, multiple drags can occur simultaneously because there
141/// can be multiple pointers in contact with the device at once. To limit the
142/// number of simultaneous drags, use the [maxSimultaneousDrags] property. The
143/// default is to allow an unlimited number of simultaneous drags.
144///
145/// This widget displays [child] when zero drags are under way. If
146/// [childWhenDragging] is non-null, this widget instead displays
147/// [childWhenDragging] when one or more drags are underway. Otherwise, this
148/// widget always displays [child].
149///
150/// {@youtube 560 315 https://www.youtube.com/watch?v=q4x2G_9-Mu0}
151///
152/// {@tool dartpad}
153/// The following example has a [Draggable] widget along with a [DragTarget]
154/// in a row demonstrating an incremented `acceptedData` integer value when
155/// you drag the element to the target.
156///
157/// ** See code in examples/api/lib/widgets/drag_target/draggable.0.dart **
158/// {@end-tool}
159///
160/// See also:
161///
162/// * [DragTarget]
163/// * [LongPressDraggable]
164class Draggable<T extends Object> extends StatefulWidget {
165 /// Creates a widget that can be dragged to a [DragTarget].
166 ///
167 /// If [maxSimultaneousDrags] is non-null, it must be non-negative.
168 const Draggable({
169 super.key,
170 required this.child,
171 required this.feedback,
172 this.data,
173 this.axis,
174 this.childWhenDragging,
175 this.feedbackOffset = Offset.zero,
176 this.dragAnchorStrategy = childDragAnchorStrategy,
177 this.affinity,
178 this.maxSimultaneousDrags,
179 this.onDragStarted,
180 this.onDragUpdate,
181 this.onDraggableCanceled,
182 this.onDragEnd,
183 this.onDragCompleted,
184 this.ignoringFeedbackSemantics = true,
185 this.ignoringFeedbackPointer = true,
186 this.rootOverlay = false,
187 this.hitTestBehavior = HitTestBehavior.deferToChild,
188 this.allowedButtonsFilter,
189 }) : assert(maxSimultaneousDrags == null || maxSimultaneousDrags >= 0);
190
191 /// The data that will be dropped by this draggable.
192 final T? data;
193
194 /// The [Axis] to restrict this draggable's movement, if specified.
195 ///
196 /// When axis is set to [Axis.horizontal], this widget can only be dragged
197 /// horizontally. Behavior is similar for [Axis.vertical].
198 ///
199 /// Defaults to allowing drag on both [Axis.horizontal] and [Axis.vertical].
200 ///
201 /// When null, allows drag on both [Axis.horizontal] and [Axis.vertical].
202 ///
203 /// For the direction of gestures this widget competes with to start a drag
204 /// event, see [Draggable.affinity].
205 final Axis? axis;
206
207 /// The widget below this widget in the tree.
208 ///
209 /// This widget displays [child] when zero drags are under way. If
210 /// [childWhenDragging] is non-null, this widget instead displays
211 /// [childWhenDragging] when one or more drags are underway. Otherwise, this
212 /// widget always displays [child].
213 ///
214 /// The [feedback] widget is shown under the pointer when a drag is under way.
215 ///
216 /// To limit the number of simultaneous drags on multitouch devices, see
217 /// [maxSimultaneousDrags].
218 ///
219 /// {@macro flutter.widgets.ProxyWidget.child}
220 final Widget child;
221
222 /// The widget to display instead of [child] when one or more drags are under way.
223 ///
224 /// If this is null, then this widget will always display [child] (and so the
225 /// drag source representation will not change while a drag is under
226 /// way).
227 ///
228 /// The [feedback] widget is shown under the pointer when a drag is under way.
229 ///
230 /// To limit the number of simultaneous drags on multitouch devices, see
231 /// [maxSimultaneousDrags].
232 final Widget? childWhenDragging;
233
234 /// The widget to show under the pointer when a drag is under way.
235 ///
236 /// See [child] and [childWhenDragging] for information about what is shown
237 /// at the location of the [Draggable] itself when a drag is under way.
238 final Widget feedback;
239
240 /// The feedbackOffset can be used to set the hit test target point for the
241 /// purposes of finding a drag target. It is especially useful if the feedback
242 /// is transformed compared to the child.
243 final Offset feedbackOffset;
244
245 /// A strategy that is used by this draggable to get the anchor offset when it
246 /// is dragged.
247 ///
248 /// The anchor offset refers to the distance between the users' fingers and
249 /// the [feedback] widget when this draggable is dragged.
250 ///
251 /// This property's value is a function that implements [DragAnchorStrategy].
252 /// There are two built-in functions that can be used:
253 ///
254 /// * [childDragAnchorStrategy], which displays the feedback anchored at the
255 /// position of the original child.
256 ///
257 /// * [pointerDragAnchorStrategy], which displays the feedback anchored at the
258 /// position of the touch that started the drag.
259 ///
260 /// Defaults to [childDragAnchorStrategy].
261 final DragAnchorStrategy dragAnchorStrategy;
262
263 /// Whether the semantics of the [feedback] widget is ignored when building
264 /// the semantics tree.
265 ///
266 /// This value should be set to false when the [feedback] widget is intended
267 /// to be the same object as the [child]. Placing a [GlobalKey] on this
268 /// widget will ensure semantic focus is kept on the element as it moves in
269 /// and out of the feedback position.
270 ///
271 /// Defaults to true.
272 final bool ignoringFeedbackSemantics;
273
274 /// Whether the [feedback] widget is ignored during hit testing.
275 ///
276 /// Regardless of whether this widget is ignored during hit testing, it will
277 /// still consume space during layout and be visible during painting.
278 ///
279 /// Defaults to true.
280 final bool ignoringFeedbackPointer;
281
282 /// Controls how this widget competes with other gestures to initiate a drag.
283 ///
284 /// If affinity is null, this widget initiates a drag as soon as it recognizes
285 /// a tap down gesture, regardless of any directionality. If affinity is
286 /// horizontal (or vertical), then this widget will compete with other
287 /// horizontal (or vertical, respectively) gestures.
288 ///
289 /// For example, if this widget is placed in a vertically scrolling region and
290 /// has horizontal affinity, pointer motion in the vertical direction will
291 /// result in a scroll and pointer motion in the horizontal direction will
292 /// result in a drag. Conversely, if the widget has a null or vertical
293 /// affinity, pointer motion in any direction will result in a drag rather
294 /// than in a scroll because the draggable widget, being the more specific
295 /// widget, will out-compete the [Scrollable] for vertical gestures.
296 ///
297 /// For the directions this widget can be dragged in after the drag event
298 /// starts, see [Draggable.axis].
299 final Axis? affinity;
300
301 /// How many simultaneous drags to support.
302 ///
303 /// When null, no limit is applied. Set this to 1 if you want to only allow
304 /// the drag source to have one item dragged at a time. Set this to 0 if you
305 /// want to prevent the draggable from actually being dragged.
306 ///
307 /// If you set this property to 1, consider supplying an "empty" widget for
308 /// [childWhenDragging] to create the illusion of actually moving [child].
309 final int? maxSimultaneousDrags;
310
311 /// Called when the draggable starts being dragged.
312 final VoidCallback? onDragStarted;
313
314 /// Called when the draggable is dragged.
315 ///
316 /// This function will only be called while this widget is still mounted to
317 /// the tree (i.e. [State.mounted] is true), and if this widget has actually moved.
318 final DragUpdateCallback? onDragUpdate;
319
320 /// Called when the draggable is dropped without being accepted by a [DragTarget].
321 ///
322 /// This function might be called after this widget has been removed from the
323 /// tree. For example, if a drag was in progress when this widget was removed
324 /// from the tree and the drag ended up being canceled, this callback will
325 /// still be called. For this reason, implementations of this callback might
326 /// need to check [State.mounted] to check whether the state receiving the
327 /// callback is still in the tree.
328 final DraggableCanceledCallback? onDraggableCanceled;
329
330 /// Called when the draggable is dropped and accepted by a [DragTarget].
331 ///
332 /// This function might be called after this widget has been removed from the
333 /// tree. For example, if a drag was in progress when this widget was removed
334 /// from the tree and the drag ended up completing, this callback will
335 /// still be called. For this reason, implementations of this callback might
336 /// need to check [State.mounted] to check whether the state receiving the
337 /// callback is still in the tree.
338 final VoidCallback? onDragCompleted;
339
340 /// Called when the draggable is dropped.
341 ///
342 /// The velocity and offset at which the pointer was moving when it was
343 /// dropped is available in the [DraggableDetails]. Also included in the
344 /// `details` is whether the draggable's [DragTarget] accepted it.
345 ///
346 /// This function will only be called while this widget is still mounted to
347 /// the tree (i.e. [State.mounted] is true).
348 final DragEndCallback? onDragEnd;
349
350 /// Whether the feedback widget will be put on the root [Overlay].
351 ///
352 /// When false, the feedback widget will be put on the closest [Overlay]. When
353 /// true, the [feedback] widget will be put on the farthest (aka root)
354 /// [Overlay].
355 ///
356 /// Defaults to false.
357 final bool rootOverlay;
358
359 /// How to behave during hit test.
360 ///
361 /// Defaults to [HitTestBehavior.deferToChild].
362 final HitTestBehavior hitTestBehavior;
363
364 /// {@macro flutter.gestures.multidrag._allowedButtonsFilter}
365 final AllowedButtonsFilter? allowedButtonsFilter;
366
367 /// Creates a gesture recognizer that recognizes the start of the drag.
368 ///
369 /// Subclasses can override this function to customize when they start
370 /// recognizing a drag.
371 @protected
372 MultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
373 switch (affinity) {
374 case Axis.horizontal:
375 return HorizontalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
376 case Axis.vertical:
377 return VerticalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
378 case null:
379 return ImmediateMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
380 }
381 }
382
383 @override
384 State<Draggable<T>> createState() => _DraggableState<T>();
385}
386
387/// Makes its child draggable starting from long press.
388///
389/// See also:
390///
391/// * [Draggable], similar to the [LongPressDraggable] widget but happens immediately.
392/// * [DragTarget], a widget that receives data when a [Draggable] widget is dropped.
393class LongPressDraggable<T extends Object> extends Draggable<T> {
394 /// Creates a widget that can be dragged starting from long press.
395 ///
396 /// If [maxSimultaneousDrags] is non-null, it must be non-negative.
397 const LongPressDraggable({
398 super.key,
399 required super.child,
400 required super.feedback,
401 super.data,
402 super.axis,
403 super.childWhenDragging,
404 super.feedbackOffset,
405 super.dragAnchorStrategy,
406 super.maxSimultaneousDrags,
407 super.onDragStarted,
408 super.onDragUpdate,
409 super.onDraggableCanceled,
410 super.onDragEnd,
411 super.onDragCompleted,
412 this.hapticFeedbackOnStart = true,
413 super.ignoringFeedbackSemantics,
414 super.ignoringFeedbackPointer,
415 this.delay = kLongPressTimeout,
416 super.allowedButtonsFilter,
417 });
418
419 /// Whether haptic feedback should be triggered on drag start.
420 final bool hapticFeedbackOnStart;
421
422 /// The duration that a user has to press down before a long press is registered.
423 ///
424 /// Defaults to [kLongPressTimeout].
425 final Duration delay;
426
427 @override
428 DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
429 return DelayedMultiDragGestureRecognizer(delay: delay, allowedButtonsFilter: allowedButtonsFilter)
430 ..onStart = (Offset position) {
431 final Drag? result = onStart(position);
432 if (result != null && hapticFeedbackOnStart) {
433 HapticFeedback.selectionClick();
434 }
435 return result;
436 };
437 }
438}
439
440class _DraggableState<T extends Object> extends State<Draggable<T>> {
441 @override
442 void initState() {
443 super.initState();
444 _recognizer = widget.createRecognizer(_startDrag);
445 }
446
447 @override
448 void dispose() {
449 _disposeRecognizerIfInactive();
450 super.dispose();
451 }
452
453 @override
454 void didChangeDependencies() {
455 _recognizer!.gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
456 super.didChangeDependencies();
457 }
458
459 // This gesture recognizer has an unusual lifetime. We want to support the use
460 // case of removing the Draggable from the tree in the middle of a drag. That
461 // means we need to keep this recognizer alive after this state object has
462 // been disposed because it's the one listening to the pointer events that are
463 // driving the drag.
464 //
465 // We achieve that by keeping count of the number of active drags and only
466 // disposing the gesture recognizer after (a) this state object has been
467 // disposed and (b) there are no more active drags.
468 GestureRecognizer? _recognizer;
469 int _activeCount = 0;
470
471 void _disposeRecognizerIfInactive() {
472 if (_activeCount > 0) {
473 return;
474 }
475 _recognizer!.dispose();
476 _recognizer = null;
477 }
478
479 void _routePointer(PointerDownEvent event) {
480 if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags!) {
481 return;
482 }
483 _recognizer!.addPointer(event);
484 }
485
486 _DragAvatar<T>? _startDrag(Offset position) {
487 if (widget.maxSimultaneousDrags != null && _activeCount >= widget.maxSimultaneousDrags!) {
488 return null;
489 }
490 final Offset dragStartPoint;
491 dragStartPoint = widget.dragAnchorStrategy(widget, context, position);
492 setState(() {
493 _activeCount += 1;
494 });
495 final _DragAvatar<T> avatar = _DragAvatar<T>(
496 overlayState: Overlay.of(context, debugRequiredFor: widget, rootOverlay: widget.rootOverlay),
497 data: widget.data,
498 axis: widget.axis,
499 initialPosition: position,
500 dragStartPoint: dragStartPoint,
501 feedback: widget.feedback,
502 feedbackOffset: widget.feedbackOffset,
503 ignoringFeedbackSemantics: widget.ignoringFeedbackSemantics,
504 ignoringFeedbackPointer: widget.ignoringFeedbackPointer,
505 viewId: View.of(context).viewId,
506 onDragUpdate: (DragUpdateDetails details) {
507 if (mounted && widget.onDragUpdate != null) {
508 widget.onDragUpdate!(details);
509 }
510 },
511 onDragEnd: (Velocity velocity, Offset offset, bool wasAccepted) {
512 if (mounted) {
513 setState(() {
514 _activeCount -= 1;
515 });
516 } else {
517 _activeCount -= 1;
518 _disposeRecognizerIfInactive();
519 }
520 if (mounted && widget.onDragEnd != null) {
521 widget.onDragEnd!(DraggableDetails(
522 wasAccepted: wasAccepted,
523 velocity: velocity,
524 offset: offset,
525 ));
526 }
527 if (wasAccepted && widget.onDragCompleted != null) {
528 widget.onDragCompleted!();
529 }
530 if (!wasAccepted && widget.onDraggableCanceled != null) {
531 widget.onDraggableCanceled!(velocity, offset);
532 }
533 },
534 );
535 widget.onDragStarted?.call();
536 return avatar;
537 }
538
539 @override
540 Widget build(BuildContext context) {
541 assert(debugCheckHasOverlay(context));
542 final bool canDrag = widget.maxSimultaneousDrags == null ||
543 _activeCount < widget.maxSimultaneousDrags!;
544 final bool showChild = _activeCount == 0 || widget.childWhenDragging == null;
545 return Listener(
546 behavior: widget.hitTestBehavior,
547 onPointerDown: canDrag ? _routePointer : null,
548 child: showChild ? widget.child : widget.childWhenDragging,
549 );
550 }
551}
552
553/// Represents the details when a specific pointer event occurred on
554/// the [Draggable].
555///
556/// This includes the [Velocity] at which the pointer was moving and [Offset]
557/// when the draggable event occurred, and whether its [DragTarget] accepted it.
558///
559/// Also, this is the details object for callbacks that use [DragEndCallback].
560class DraggableDetails {
561 /// Creates details for a [DraggableDetails].
562 ///
563 /// If [wasAccepted] is not specified, it will default to `false`.
564 ///
565 /// The [velocity] or [offset] arguments must not be `null`.
566 DraggableDetails({
567 this.wasAccepted = false,
568 required this.velocity,
569 required this.offset,
570 });
571
572 /// Determines whether the [DragTarget] accepted this draggable.
573 final bool wasAccepted;
574
575 /// The velocity at which the pointer was moving when the specific pointer
576 /// event occurred on the draggable.
577 final Velocity velocity;
578
579 /// The global position when the specific pointer event occurred on
580 /// the draggable.
581 final Offset offset;
582}
583
584/// Represents the details when a pointer event occurred on the [DragTarget].
585class DragTargetDetails<T> {
586 /// Creates details for a [DragTarget] callback.
587 DragTargetDetails({required this.data, required this.offset});
588
589 /// The data that was dropped onto this [DragTarget].
590 final T data;
591
592 /// The global position when the specific pointer event occurred on
593 /// the draggable.
594 final Offset offset;
595}
596
597/// A widget that receives data when a [Draggable] widget is dropped.
598///
599/// When a draggable is dragged on top of a drag target, the drag target is
600/// asked whether it will accept the data the draggable is carrying. If the user
601/// does drop the draggable on top of the drag target (and the drag target has
602/// indicated that it will accept the draggable's data), then the drag target is
603/// asked to accept the draggable's data.
604///
605/// See also:
606///
607/// * [Draggable]
608/// * [LongPressDraggable]
609class DragTarget<T extends Object> extends StatefulWidget {
610 /// Creates a widget that receives drags.
611 const DragTarget({
612 super.key,
613 required this.builder,
614 @Deprecated(
615 'Use onWillAcceptWithDetails instead. '
616 'This callback is similar to onWillAcceptWithDetails but does not provide drag details. '
617 'This feature was deprecated after v3.14.0-0.2.pre.'
618 )
619 this.onWillAccept,
620 this.onWillAcceptWithDetails,
621 @Deprecated(
622 'Use onAcceptWithDetails instead. '
623 'This callback is similar to onAcceptWithDetails but does not provide drag details. '
624 'This feature was deprecated after v3.14.0-0.2.pre.'
625 )
626 this.onAccept,
627 this.onAcceptWithDetails,
628 this.onLeave,
629 this.onMove,
630 this.hitTestBehavior = HitTestBehavior.translucent,
631 }) : assert(onWillAccept == null || onWillAcceptWithDetails == null, "Don't pass both onWillAccept and onWillAcceptWithDetails.");
632
633 /// Called to build the contents of this widget.
634 ///
635 /// The builder can build different widgets depending on what is being dragged
636 /// into this drag target.
637 final DragTargetBuilder<T> builder;
638
639 /// Called to determine whether this widget is interested in receiving a given
640 /// piece of data being dragged over this drag target.
641 ///
642 /// Called when a piece of data enters the target. This will be followed by
643 /// either [onAccept] and [onAcceptWithDetails], if the data is dropped, or
644 /// [onLeave], if the drag leaves the target.
645 ///
646 /// Equivalent to [onWillAcceptWithDetails], but only includes the data.
647 ///
648 /// Must not be provided if [onWillAcceptWithDetails] is provided.
649 @Deprecated(
650 'Use onWillAcceptWithDetails instead. '
651 'This callback is similar to onWillAcceptWithDetails but does not provide drag details. '
652 'This feature was deprecated after v3.14.0-0.2.pre.'
653 )
654 final DragTargetWillAccept<T>? onWillAccept;
655
656 /// Called to determine whether this widget is interested in receiving a given
657 /// piece of data being dragged over this drag target.
658 ///
659 /// Called when a piece of data enters the target. This will be followed by
660 /// either [onAccept] and [onAcceptWithDetails], if the data is dropped, or
661 /// [onLeave], if the drag leaves the target.
662 ///
663 /// Equivalent to [onWillAccept], but with information, including the data,
664 /// in a [DragTargetDetails].
665 ///
666 /// Must not be provided if [onWillAccept] is provided.
667 final DragTargetWillAcceptWithDetails<T>? onWillAcceptWithDetails;
668
669 /// Called when an acceptable piece of data was dropped over this drag target.
670 /// It will not be called if `data` is `null`.
671 ///
672 /// Equivalent to [onAcceptWithDetails], but only includes the data.
673 @Deprecated(
674 'Use onAcceptWithDetails instead. '
675 'This callback is similar to onAcceptWithDetails but does not provide drag details. '
676 'This feature was deprecated after v3.14.0-0.2.pre.'
677 )
678 final DragTargetAccept<T>? onAccept;
679
680 /// Called when an acceptable piece of data was dropped over this drag target.
681 /// It will not be called if `data` is `null`.
682 ///
683 /// Equivalent to [onAccept], but with information, including the data, in a
684 /// [DragTargetDetails].
685 final DragTargetAcceptWithDetails<T>? onAcceptWithDetails;
686
687 /// Called when a given piece of data being dragged over this target leaves
688 /// the target.
689 final DragTargetLeave<T>? onLeave;
690
691 /// Called when a [Draggable] moves within this [DragTarget]. It will not be
692 /// called if `data` is `null`.
693 ///
694 /// This includes entering and leaving the target.
695 final DragTargetMove<T>? onMove;
696
697 /// How to behave during hit testing.
698 ///
699 /// Defaults to [HitTestBehavior.translucent].
700 final HitTestBehavior hitTestBehavior;
701
702 @override
703 State<DragTarget<T>> createState() => _DragTargetState<T>();
704}
705
706List<T?> _mapAvatarsToData<T extends Object>(List<_DragAvatar<Object>> avatars) {
707 return avatars.map<T?>((_DragAvatar<Object> avatar) => avatar.data as T?).toList();
708}
709
710class _DragTargetState<T extends Object> extends State<DragTarget<T>> {
711 final List<_DragAvatar<Object>> _candidateAvatars = <_DragAvatar<Object>>[];
712 final List<_DragAvatar<Object>> _rejectedAvatars = <_DragAvatar<Object>>[];
713
714 // On non-web platforms, checks if data Object is equal to type[T] or subtype of [T].
715 // On web, it does the same, but requires a check for ints and doubles
716 // because dart doubles and ints are backed by the same kind of object on web.
717 // JavaScript does not support integers.
718 bool isExpectedDataType(Object? data, Type type) {
719 if (kIsWeb && ((type == int && T == double) || (type == double && T == int))) {
720 return false;
721 }
722 return data is T?;
723 }
724
725 bool didEnter(_DragAvatar<Object> avatar) {
726 assert(!_candidateAvatars.contains(avatar));
727 assert(!_rejectedAvatars.contains(avatar));
728 final bool resolvedWillAccept = (widget.onWillAccept == null &&
729 widget.onWillAcceptWithDetails == null) ||
730 (widget.onWillAccept != null &&
731 widget.onWillAccept!(avatar.data as T?)) ||
732 (widget.onWillAcceptWithDetails != null &&
733 avatar.data != null &&
734 widget.onWillAcceptWithDetails!(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!)));
735 if (resolvedWillAccept) {
736 setState(() {
737 _candidateAvatars.add(avatar);
738 });
739 return true;
740 } else {
741 setState(() {
742 _rejectedAvatars.add(avatar);
743 });
744 return false;
745 }
746 }
747
748 void didLeave(_DragAvatar<Object> avatar) {
749 assert(_candidateAvatars.contains(avatar) || _rejectedAvatars.contains(avatar));
750 if (!mounted) {
751 return;
752 }
753 setState(() {
754 _candidateAvatars.remove(avatar);
755 _rejectedAvatars.remove(avatar);
756 });
757 widget.onLeave?.call(avatar.data as T?);
758 }
759
760 void didDrop(_DragAvatar<Object> avatar) {
761 assert(_candidateAvatars.contains(avatar));
762 if (!mounted) {
763 return;
764 }
765 setState(() {
766 _candidateAvatars.remove(avatar);
767 });
768 if (avatar.data != null) {
769 widget.onAccept?.call(avatar.data! as T);
770 widget.onAcceptWithDetails?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
771 }
772 }
773
774 void didMove(_DragAvatar<Object> avatar) {
775 if (!mounted || avatar.data == null) {
776 return;
777 }
778 widget.onMove?.call(DragTargetDetails<T>(data: avatar.data! as T, offset: avatar._lastOffset!));
779 }
780
781 @override
782 Widget build(BuildContext context) {
783 return MetaData(
784 metaData: this,
785 behavior: widget.hitTestBehavior,
786 child: widget.builder(context, _mapAvatarsToData<T>(_candidateAvatars), _mapAvatarsToData<Object>(_rejectedAvatars)),
787 );
788 }
789}
790
791enum _DragEndKind { dropped, canceled }
792typedef _OnDragEnd = void Function(Velocity velocity, Offset offset, bool wasAccepted);
793
794// The lifetime of this object is a little dubious right now. Specifically, it
795// lives as long as the pointer is down. Arguably it should self-immolate if the
796// overlay goes away. _DraggableState has some delicate logic to continue
797// needing this object pointer events even after it has been disposed.
798class _DragAvatar<T extends Object> extends Drag {
799 _DragAvatar({
800 required this.overlayState,
801 this.data,
802 this.axis,
803 required Offset initialPosition,
804 this.dragStartPoint = Offset.zero,
805 this.feedback,
806 this.feedbackOffset = Offset.zero,
807 this.onDragUpdate,
808 this.onDragEnd,
809 required this.ignoringFeedbackSemantics,
810 required this.ignoringFeedbackPointer,
811 required this.viewId,
812 }) : _position = initialPosition {
813 _entry = OverlayEntry(builder: _build);
814 overlayState.insert(_entry!);
815 updateDrag(initialPosition);
816 }
817
818 final T? data;
819 final Axis? axis;
820 final Offset dragStartPoint;
821 final Widget? feedback;
822 final Offset feedbackOffset;
823 final DragUpdateCallback? onDragUpdate;
824 final _OnDragEnd? onDragEnd;
825 final OverlayState overlayState;
826 final bool ignoringFeedbackSemantics;
827 final bool ignoringFeedbackPointer;
828 final int viewId;
829
830 _DragTargetState<Object>? _activeTarget;
831 final List<_DragTargetState<Object>> _enteredTargets = <_DragTargetState<Object>>[];
832 Offset _position;
833 Offset? _lastOffset;
834 OverlayEntry? _entry;
835
836 @override
837 void update(DragUpdateDetails details) {
838 final Offset oldPosition = _position;
839 _position += _restrictAxis(details.delta);
840 updateDrag(_position);
841 if (onDragUpdate != null && _position != oldPosition) {
842 onDragUpdate!(details);
843 }
844 }
845
846 @override
847 void end(DragEndDetails details) {
848 finishDrag(_DragEndKind.dropped, _restrictVelocityAxis(details.velocity));
849 }
850
851
852 @override
853 void cancel() {
854 finishDrag(_DragEndKind.canceled);
855 }
856
857 void updateDrag(Offset globalPosition) {
858 _lastOffset = globalPosition - dragStartPoint;
859 _entry!.markNeedsBuild();
860 final HitTestResult result = HitTestResult();
861 WidgetsBinding.instance.hitTestInView(result, globalPosition + feedbackOffset, viewId);
862
863 final List<_DragTargetState<Object>> targets = _getDragTargets(result.path).toList();
864
865 bool listsMatch = false;
866 if (targets.length >= _enteredTargets.length && _enteredTargets.isNotEmpty) {
867 listsMatch = true;
868 final Iterator<_DragTargetState<Object>> iterator = targets.iterator;
869 for (int i = 0; i < _enteredTargets.length; i += 1) {
870 iterator.moveNext();
871 if (iterator.current != _enteredTargets[i]) {
872 listsMatch = false;
873 break;
874 }
875 }
876 }
877
878 // If everything's the same, report moves, and bail early.
879 if (listsMatch) {
880 for (final _DragTargetState<Object> target in _enteredTargets) {
881 target.didMove(this);
882 }
883 return;
884 }
885
886 // Leave old targets.
887 _leaveAllEntered();
888
889 // Enter new targets.
890 final _DragTargetState<Object>? newTarget = targets.cast<_DragTargetState<Object>?>().firstWhere(
891 (_DragTargetState<Object>? target) {
892 if (target == null) {
893 return false;
894 }
895 _enteredTargets.add(target);
896 return target.didEnter(this);
897 },
898 orElse: () => null,
899 );
900
901 // Report moves to the targets.
902 for (final _DragTargetState<Object> target in _enteredTargets) {
903 target.didMove(this);
904 }
905
906 _activeTarget = newTarget;
907 }
908
909 Iterable<_DragTargetState<Object>> _getDragTargets(Iterable<HitTestEntry> path) {
910 // Look for the RenderBoxes that corresponds to the hit target (the hit target
911 // widgets build RenderMetaData boxes for us for this purpose).
912 final List<_DragTargetState<Object>> targets = <_DragTargetState<Object>>[];
913 for (final HitTestEntry entry in path) {
914 final HitTestTarget target = entry.target;
915 if (target is RenderMetaData) {
916 final dynamic metaData = target.metaData;
917 if (metaData is _DragTargetState && metaData.isExpectedDataType(data, T)) {
918 targets.add(metaData);
919 }
920 }
921 }
922 return targets;
923 }
924
925 void _leaveAllEntered() {
926 for (int i = 0; i < _enteredTargets.length; i += 1) {
927 _enteredTargets[i].didLeave(this);
928 }
929 _enteredTargets.clear();
930 }
931
932 void finishDrag(_DragEndKind endKind, [ Velocity? velocity ]) {
933 bool wasAccepted = false;
934 if (endKind == _DragEndKind.dropped && _activeTarget != null) {
935 _activeTarget!.didDrop(this);
936 wasAccepted = true;
937 _enteredTargets.remove(_activeTarget);
938 }
939 _leaveAllEntered();
940 _activeTarget = null;
941 _entry!.remove();
942 _entry!.dispose();
943 _entry = null;
944 // TODO(ianh): consider passing _entry as well so the client can perform an animation.
945 onDragEnd?.call(velocity ?? Velocity.zero, _lastOffset!, wasAccepted);
946 }
947
948 Widget _build(BuildContext context) {
949 final RenderBox box = overlayState.context.findRenderObject()! as RenderBox;
950 final Offset overlayTopLeft = box.localToGlobal(Offset.zero);
951 return Positioned(
952 left: _lastOffset!.dx - overlayTopLeft.dx,
953 top: _lastOffset!.dy - overlayTopLeft.dy,
954 child: ExcludeSemantics(
955 excluding: ignoringFeedbackSemantics,
956 child: IgnorePointer(
957 ignoring: ignoringFeedbackPointer,
958 child: feedback,
959 ),
960 ),
961 );
962 }
963
964 Velocity _restrictVelocityAxis(Velocity velocity) {
965 if (axis == null) {
966 return velocity;
967 }
968 return Velocity(
969 pixelsPerSecond: _restrictAxis(velocity.pixelsPerSecond),
970 );
971 }
972
973 Offset _restrictAxis(Offset offset) {
974 if (axis == null) {
975 return offset;
976 }
977 if (axis == Axis.horizontal) {
978 return Offset(offset.dx, 0.0);
979 }
980 return Offset(0.0, offset.dy);
981 }
982}
983