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 | import 'package:flutter/foundation.dart' show kIsWeb; |
6 | import 'package:flutter/gestures.dart'; |
7 | import 'package:flutter/rendering.dart'; |
8 | import 'package:flutter/services.dart'; |
9 | |
10 | import 'basic.dart'; |
11 | import 'binding.dart'; |
12 | import 'debug.dart'; |
13 | import 'framework.dart'; |
14 | import 'media_query.dart'; |
15 | import 'overlay.dart'; |
16 | import 'view.dart'; |
17 | |
18 | /// Signature for determining whether the given data will be accepted by a [DragTarget]. |
19 | /// |
20 | /// Used by [DragTarget.onWillAccept]. |
21 | typedef 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]. |
27 | typedef 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]. |
32 | typedef DragTargetAccept<T> = void Function(T data); |
33 | |
34 | /// Signature for determining information about the acceptance by a [DragTarget]. |
35 | /// |
36 | /// Used by [DragTarget.onAcceptWithDetails]. |
37 | typedef 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]. |
47 | typedef 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]. |
52 | typedef 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]. |
57 | typedef 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]. |
66 | typedef DragEndCallback = void Function(DraggableDetails details); |
67 | |
68 | /// Signature for when a [Draggable] leaves a [DragTarget]. |
69 | /// |
70 | /// Used by [DragTarget.onLeave]. |
71 | typedef DragTargetLeave<T> = void Function(T? data); |
72 | |
73 | /// Signature for when a [Draggable] moves within a [DragTarget]. |
74 | /// |
75 | /// Used by [DragTarget.onMove]. |
76 | typedef 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. |
89 | typedef 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. |
102 | Offset 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. |
124 | Offset 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] |
164 | class 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. |
393 | class 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 | |
440 | class _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]. |
560 | class 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]. |
585 | class 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] |
609 | class 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 | |
706 | List<T?> _mapAvatarsToData<T extends Object>(List<_DragAvatar<Object>> avatars) { |
707 | return avatars.map<T?>((_DragAvatar<Object> avatar) => avatar.data as T?).toList(); |
708 | } |
709 | |
710 | class _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 | |
791 | enum _DragEndKind { dropped, canceled } |
792 | typedef _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. |
798 | class _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 | |