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