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/gestures.dart';
6import 'package:flutter/rendering.dart';
7import 'package:flutter/scheduler.dart';
8
9import 'basic.dart';
10import 'debug.dart';
11import 'framework.dart';
12import 'inherited_theme.dart';
13import 'localizations.dart';
14import 'media_query.dart';
15import 'overlay.dart';
16import 'scroll_controller.dart';
17import 'scroll_delegate.dart';
18import 'scroll_physics.dart';
19import 'scroll_view.dart';
20import 'scrollable.dart';
21import 'scrollable_helpers.dart';
22import 'sliver.dart';
23import 'sliver_prototype_extent_list.dart';
24import 'sliver_varied_extent_list.dart';
25import 'ticker_provider.dart';
26import 'transitions.dart';
27
28// Examples can assume:
29// class MyDataObject {}
30
31/// A callback used by [ReorderableList] to report that a list item has moved
32/// to a new position in the list.
33///
34/// Implementations should remove the corresponding list item at [oldIndex]
35/// and reinsert it at [newIndex].
36///
37/// If [oldIndex] is before [newIndex], removing the item at [oldIndex] from the
38/// list will reduce the list's length by one. Implementations will need to
39/// account for this when inserting before [newIndex].
40///
41/// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE}
42///
43/// {@tool snippet}
44///
45/// ```dart
46/// final List<MyDataObject> backingList = <MyDataObject>[/* ... */];
47///
48/// void handleReorder(int oldIndex, int newIndex) {
49/// if (oldIndex < newIndex) {
50/// // removing the item at oldIndex will shorten the list by 1.
51/// newIndex -= 1;
52/// }
53/// final MyDataObject element = backingList.removeAt(oldIndex);
54/// backingList.insert(newIndex, element);
55/// }
56/// ```
57/// {@end-tool}
58///
59/// See also:
60///
61/// * [ReorderableList], a widget list that allows the user to reorder
62/// its items.
63/// * [SliverReorderableList], a sliver list that allows the user to reorder
64/// its items.
65/// * [ReorderableListView], a Material Design list that allows the user to
66/// reorder its items.
67typedef ReorderCallback = void Function(int oldIndex, int newIndex);
68
69/// Signature for the builder callback used to decorate the dragging item in
70/// [ReorderableList] and [SliverReorderableList].
71///
72/// The [child] will be the item that is being dragged, and [index] is the
73/// position of the item in the list.
74///
75/// The [animation] will be driven forward from 0.0 to 1.0 while the item is
76/// being picked up during a drag operation, and reversed from 1.0 to 0.0 when
77/// the item is dropped. This can be used to animate properties of the proxy
78/// like an elevation or border.
79///
80/// The returned value will typically be the [child] wrapped in other widgets.
81typedef ReorderItemProxyDecorator = Widget Function(Widget child, int index, Animation<double> animation);
82
83/// A scrolling container that allows the user to interactively reorder the
84/// list items.
85///
86/// This widget is similar to one created by [ListView.builder], and uses
87/// an [IndexedWidgetBuilder] to create each item.
88///
89/// It is up to the application to wrap each child (or an internal part of the
90/// child such as a drag handle) with a drag listener that will recognize
91/// the start of an item drag and then start the reorder by calling
92/// [ReorderableListState.startItemDragReorder]. This is most easily achieved
93/// by wrapping each child in a [ReorderableDragStartListener] or a
94/// [ReorderableDelayedDragStartListener]. These will take care of recognizing
95/// the start of a drag gesture and call the list state's
96/// [ReorderableListState.startItemDragReorder] method.
97///
98/// This widget's [ReorderableListState] can be used to manually start an item
99/// reorder, or cancel a current drag. To refer to the
100/// [ReorderableListState] either provide a [GlobalKey] or use the static
101/// [ReorderableList.of] method from an item's build method.
102///
103/// See also:
104///
105/// * [SliverReorderableList], a sliver list that allows the user to reorder
106/// its items.
107/// * [ReorderableListView], a Material Design list that allows the user to
108/// reorder its items.
109class ReorderableList extends StatefulWidget {
110 /// Creates a scrolling container that allows the user to interactively
111 /// reorder the list items.
112 ///
113 /// The [itemCount] must be greater than or equal to zero.
114 const ReorderableList({
115 super.key,
116 required this.itemBuilder,
117 required this.itemCount,
118 required this.onReorder,
119 this.onReorderStart,
120 this.onReorderEnd,
121 this.itemExtent,
122 this.itemExtentBuilder,
123 this.prototypeItem,
124 this.proxyDecorator,
125 this.padding,
126 this.scrollDirection = Axis.vertical,
127 this.reverse = false,
128 this.controller,
129 this.primary,
130 this.physics,
131 this.shrinkWrap = false,
132 this.anchor = 0.0,
133 this.cacheExtent,
134 this.dragStartBehavior = DragStartBehavior.start,
135 this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
136 this.restorationId,
137 this.clipBehavior = Clip.hardEdge,
138 this.autoScrollerVelocityScalar,
139 }) : assert(itemCount >= 0),
140 assert(
141 (itemExtent == null && prototypeItem == null) ||
142 (itemExtent == null && itemExtentBuilder == null) ||
143 (prototypeItem == null && itemExtentBuilder == null),
144 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
145 );
146
147 /// {@template flutter.widgets.reorderable_list.itemBuilder}
148 /// Called, as needed, to build list item widgets.
149 ///
150 /// List items are only built when they're scrolled into view.
151 ///
152 /// The [IndexedWidgetBuilder] index parameter indicates the item's
153 /// position in the list. The value of the index parameter will be between
154 /// zero and one less than [itemCount]. All items in the list must have a
155 /// unique [Key], and should have some kind of listener to start the drag
156 /// (usually a [ReorderableDragStartListener] or
157 /// [ReorderableDelayedDragStartListener]).
158 /// {@endtemplate}
159 final IndexedWidgetBuilder itemBuilder;
160
161 /// {@template flutter.widgets.reorderable_list.itemCount}
162 /// The number of items in the list.
163 ///
164 /// It must be a non-negative integer. When zero, nothing is displayed and
165 /// the widget occupies no space.
166 /// {@endtemplate}
167 final int itemCount;
168
169 /// {@template flutter.widgets.reorderable_list.onReorder}
170 /// A callback used by the list to report that a list item has been dragged
171 /// to a new location in the list and the application should update the order
172 /// of the items.
173 /// {@endtemplate}
174 final ReorderCallback onReorder;
175
176 /// {@template flutter.widgets.reorderable_list.onReorderStart}
177 /// A callback that is called when an item drag has started.
178 ///
179 /// The index parameter of the callback is the index of the selected item.
180 ///
181 /// See also:
182 ///
183 /// * [onReorderEnd], which is a called when the dragged item is dropped.
184 /// * [onReorder], which reports that a list item has been dragged to a new
185 /// location.
186 /// {@endtemplate}
187 final void Function(int index)? onReorderStart;
188
189 /// {@template flutter.widgets.reorderable_list.onReorderEnd}
190 /// A callback that is called when the dragged item is dropped.
191 ///
192 /// The index parameter of the callback is the index where the item is
193 /// dropped. Unlike [onReorder], this is called even when the list item is
194 /// dropped in the same location.
195 ///
196 /// See also:
197 ///
198 /// * [onReorderStart], which is a called when an item drag has started.
199 /// * [onReorder], which reports that a list item has been dragged to a new
200 /// location.
201 /// {@endtemplate}
202 final void Function(int index)? onReorderEnd;
203
204 /// {@template flutter.widgets.reorderable_list.proxyDecorator}
205 /// A callback that allows the app to add an animated decoration around
206 /// an item when it is being dragged.
207 /// {@endtemplate}
208 final ReorderItemProxyDecorator? proxyDecorator;
209
210 /// {@template flutter.widgets.reorderable_list.padding}
211 /// The amount of space by which to inset the list contents.
212 ///
213 /// It defaults to `EdgeInsets.all(0)`.
214 /// {@endtemplate}
215 final EdgeInsetsGeometry? padding;
216
217 /// {@macro flutter.widgets.scroll_view.scrollDirection}
218 final Axis scrollDirection;
219
220 /// {@macro flutter.widgets.scroll_view.reverse}
221 final bool reverse;
222
223 /// {@macro flutter.widgets.scroll_view.controller}
224 final ScrollController? controller;
225
226 /// {@macro flutter.widgets.scroll_view.primary}
227 final bool? primary;
228
229 /// {@macro flutter.widgets.scroll_view.physics}
230 final ScrollPhysics? physics;
231
232 /// {@macro flutter.widgets.scroll_view.shrinkWrap}
233 final bool shrinkWrap;
234
235 /// {@macro flutter.widgets.scroll_view.anchor}
236 final double anchor;
237
238 /// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
239 final double? cacheExtent;
240
241 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
242 final DragStartBehavior dragStartBehavior;
243
244 /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
245 ///
246 /// The default is [ScrollViewKeyboardDismissBehavior.manual]
247 final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
248
249 /// {@macro flutter.widgets.scrollable.restorationId}
250 final String? restorationId;
251
252 /// {@macro flutter.material.Material.clipBehavior}
253 ///
254 /// Defaults to [Clip.hardEdge].
255 final Clip clipBehavior;
256
257 /// {@macro flutter.widgets.list_view.itemExtent}
258 final double? itemExtent;
259
260 /// {@macro flutter.widgets.list_view.itemExtentBuilder}
261 final ItemExtentBuilder? itemExtentBuilder;
262
263 /// {@macro flutter.widgets.list_view.prototypeItem}
264 final Widget? prototypeItem;
265
266 /// {@macro flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
267 ///
268 /// {@macro flutter.widgets.SliverReorderableList.autoScrollerVelocityScalar.default}
269 final double? autoScrollerVelocityScalar;
270
271 /// The state from the closest instance of this class that encloses the given
272 /// context.
273 ///
274 /// This method is typically used by [ReorderableList] item widgets that
275 /// insert or remove items in response to user input.
276 ///
277 /// If no [ReorderableList] surrounds the given context, then this function
278 /// will assert in debug mode and throw an exception in release mode.
279 ///
280 /// This method can be expensive (it walks the element tree).
281 ///
282 /// See also:
283 ///
284 /// * [maybeOf], a similar function that will return null if no
285 /// [ReorderableList] ancestor is found.
286 static ReorderableListState of(BuildContext context) {
287 final ReorderableListState? result = context.findAncestorStateOfType<ReorderableListState>();
288 assert(() {
289 if (result == null) {
290 throw FlutterError.fromParts(<DiagnosticsNode>[
291 ErrorSummary('ReorderableList.of() called with a context that does not contain a ReorderableList.'),
292 ErrorDescription(
293 'No ReorderableList ancestor could be found starting from the context that was passed to ReorderableList.of().',
294 ),
295 ErrorHint(
296 'This can happen when the context provided is from the same StatefulWidget that '
297 'built the ReorderableList. Please see the ReorderableList documentation for examples '
298 'of how to refer to an ReorderableListState object:\n'
299 ' https://api.flutter.dev/flutter/widgets/ReorderableListState-class.html',
300 ),
301 context.describeElement('The context used was'),
302 ]);
303 }
304 return true;
305 }());
306 return result!;
307 }
308
309 /// The state from the closest instance of this class that encloses the given
310 /// context.
311 ///
312 /// This method is typically used by [ReorderableList] item widgets that insert
313 /// or remove items in response to user input.
314 ///
315 /// If no [ReorderableList] surrounds the context given, then this function will
316 /// return null.
317 ///
318 /// This method can be expensive (it walks the element tree).
319 ///
320 /// See also:
321 ///
322 /// * [of], a similar function that will throw if no [ReorderableList] ancestor
323 /// is found.
324 static ReorderableListState? maybeOf(BuildContext context) {
325 return context.findAncestorStateOfType<ReorderableListState>();
326 }
327
328 @override
329 ReorderableListState createState() => ReorderableListState();
330}
331
332/// The state for a list that allows the user to interactively reorder
333/// the list items.
334///
335/// An app that needs to start a new item drag or cancel an existing one
336/// can refer to the [ReorderableList]'s state with a global key:
337///
338/// ```dart
339/// GlobalKey<ReorderableListState> listKey = GlobalKey<ReorderableListState>();
340/// // ...
341/// Widget build(BuildContext context) {
342/// return ReorderableList(
343/// key: listKey,
344/// itemBuilder: (BuildContext context, int index) => const SizedBox(height: 10.0),
345/// itemCount: 5,
346/// onReorder: (int oldIndex, int newIndex) {
347/// // ...
348/// },
349/// );
350/// }
351/// // ...
352/// listKey.currentState!.cancelReorder();
353/// ```
354class ReorderableListState extends State<ReorderableList> {
355 final GlobalKey<SliverReorderableListState> _sliverReorderableListKey = GlobalKey();
356
357 /// Initiate the dragging of the item at [index] that was started with
358 /// the pointer down [event].
359 ///
360 /// The given [recognizer] will be used to recognize and start the drag
361 /// item tracking and lead to either an item reorder, or a canceled drag.
362 /// The list will take ownership of the returned recognizer and will dispose
363 /// it when it is no longer needed.
364 ///
365 /// Most applications will not use this directly, but will wrap the item
366 /// (or part of the item, like a drag handle) in either a
367 /// [ReorderableDragStartListener] or [ReorderableDelayedDragStartListener]
368 /// which call this for the application.
369 void startItemDragReorder({
370 required int index,
371 required PointerDownEvent event,
372 required MultiDragGestureRecognizer recognizer,
373 }) {
374 _sliverReorderableListKey.currentState!.startItemDragReorder(index: index, event: event, recognizer: recognizer);
375 }
376
377 /// Cancel any item drag in progress.
378 ///
379 /// This should be called before any major changes to the item list
380 /// occur so that any item drags will not get confused by
381 /// changes to the underlying list.
382 ///
383 /// If no drag is active, this will do nothing.
384 void cancelReorder() {
385 _sliverReorderableListKey.currentState!.cancelReorder();
386 }
387
388 @override
389 Widget build(BuildContext context) {
390 return CustomScrollView(
391 scrollDirection: widget.scrollDirection,
392 reverse: widget.reverse,
393 controller: widget.controller,
394 primary: widget.primary,
395 physics: widget.physics,
396 shrinkWrap: widget.shrinkWrap,
397 anchor: widget.anchor,
398 cacheExtent: widget.cacheExtent,
399 dragStartBehavior: widget.dragStartBehavior,
400 keyboardDismissBehavior: widget.keyboardDismissBehavior,
401 restorationId: widget.restorationId,
402 clipBehavior: widget.clipBehavior,
403 slivers: <Widget>[
404 SliverPadding(
405 padding: widget.padding ?? EdgeInsets.zero,
406 sliver: SliverReorderableList(
407 key: _sliverReorderableListKey,
408 itemExtent: widget.itemExtent,
409 prototypeItem: widget.prototypeItem,
410 itemBuilder: widget.itemBuilder,
411 itemCount: widget.itemCount,
412 onReorder: widget.onReorder,
413 onReorderStart: widget.onReorderStart,
414 onReorderEnd: widget.onReorderEnd,
415 proxyDecorator: widget.proxyDecorator,
416 autoScrollerVelocityScalar: widget.autoScrollerVelocityScalar,
417 ),
418 ),
419 ],
420 );
421 }
422}
423
424/// A sliver list that allows the user to interactively reorder the list items.
425///
426/// It is up to the application to wrap each child (or an internal part of the
427/// child) with a drag listener that will recognize the start of an item drag
428/// and then start the reorder by calling
429/// [SliverReorderableListState.startItemDragReorder]. This is most easily
430/// achieved by wrapping each child in a [ReorderableDragStartListener] or
431/// a [ReorderableDelayedDragStartListener]. These will take care of
432/// recognizing the start of a drag gesture and call the list state's start
433/// item drag method.
434///
435/// This widget's [SliverReorderableListState] can be used to manually start an item
436/// reorder, or cancel a current drag that's already underway. To refer to the
437/// [SliverReorderableListState] either provide a [GlobalKey] or use the static
438/// [SliverReorderableList.of] method from an item's build method.
439///
440/// See also:
441///
442/// * [ReorderableList], a regular widget list that allows the user to reorder
443/// its items.
444/// * [ReorderableListView], a Material Design list that allows the user to
445/// reorder its items.
446class SliverReorderableList extends StatefulWidget {
447 /// Creates a sliver list that allows the user to interactively reorder its
448 /// items.
449 ///
450 /// The [itemCount] must be greater than or equal to zero.
451 const SliverReorderableList({
452 super.key,
453 required this.itemBuilder,
454 this.findChildIndexCallback,
455 required this.itemCount,
456 required this.onReorder,
457 this.onReorderStart,
458 this.onReorderEnd,
459 this.itemExtent,
460 this.itemExtentBuilder,
461 this.prototypeItem,
462 this.proxyDecorator,
463 double? autoScrollerVelocityScalar,
464 }) : autoScrollerVelocityScalar = autoScrollerVelocityScalar ?? _kDefaultAutoScrollVelocityScalar,
465 assert(itemCount >= 0),
466 assert(
467 (itemExtent == null && prototypeItem == null) ||
468 (itemExtent == null && itemExtentBuilder == null) ||
469 (prototypeItem == null && itemExtentBuilder == null),
470 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.',
471 );
472
473 // An eyeballed value for a smooth scrolling experience.
474 static const double _kDefaultAutoScrollVelocityScalar = 50;
475
476 /// {@macro flutter.widgets.reorderable_list.itemBuilder}
477 final IndexedWidgetBuilder itemBuilder;
478
479 /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
480 final ChildIndexGetter? findChildIndexCallback;
481
482 /// {@macro flutter.widgets.reorderable_list.itemCount}
483 final int itemCount;
484
485 /// {@macro flutter.widgets.reorderable_list.onReorder}
486 final ReorderCallback onReorder;
487
488 /// {@macro flutter.widgets.reorderable_list.onReorderStart}
489 final void Function(int)? onReorderStart;
490
491 /// {@macro flutter.widgets.reorderable_list.onReorderEnd}
492 final void Function(int)? onReorderEnd;
493
494 /// {@macro flutter.widgets.reorderable_list.proxyDecorator}
495 final ReorderItemProxyDecorator? proxyDecorator;
496
497 /// {@macro flutter.widgets.list_view.itemExtent}
498 final double? itemExtent;
499
500 /// {@macro flutter.widgets.list_view.itemExtentBuilder}
501 final ItemExtentBuilder? itemExtentBuilder;
502
503 /// {@macro flutter.widgets.list_view.prototypeItem}
504 final Widget? prototypeItem;
505
506 /// {@macro flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
507 ///
508 /// {@template flutter.widgets.SliverReorderableList.autoScrollerVelocityScalar.default}
509 /// Defaults to 50 if not set or set to null.
510 /// {@endtemplate}
511 final double autoScrollerVelocityScalar;
512
513 @override
514 SliverReorderableListState createState() => SliverReorderableListState();
515
516 /// The state from the closest instance of this class that encloses the given
517 /// context.
518 ///
519 /// This method is typically used by [SliverReorderableList] item widgets to
520 /// start or cancel an item drag operation.
521 ///
522 /// If no [SliverReorderableList] surrounds the context given, this function
523 /// will assert in debug mode and throw an exception in release mode.
524 ///
525 /// This method can be expensive (it walks the element tree).
526 ///
527 /// See also:
528 ///
529 /// * [maybeOf], a similar function that will return null if no
530 /// [SliverReorderableList] ancestor is found.
531 static SliverReorderableListState of(BuildContext context) {
532 final SliverReorderableListState? result = context.findAncestorStateOfType<SliverReorderableListState>();
533 assert(() {
534 if (result == null) {
535 throw FlutterError.fromParts(<DiagnosticsNode>[
536 ErrorSummary(
537 'SliverReorderableList.of() called with a context that does not contain a SliverReorderableList.',
538 ),
539 ErrorDescription(
540 'No SliverReorderableList ancestor could be found starting from the context that was passed to SliverReorderableList.of().',
541 ),
542 ErrorHint(
543 'This can happen when the context provided is from the same StatefulWidget that '
544 'built the SliverReorderableList. Please see the SliverReorderableList documentation for examples '
545 'of how to refer to an SliverReorderableList object:\n'
546 ' https://api.flutter.dev/flutter/widgets/SliverReorderableListState-class.html',
547 ),
548 context.describeElement('The context used was'),
549 ]);
550 }
551 return true;
552 }());
553 return result!;
554 }
555
556 /// The state from the closest instance of this class that encloses the given
557 /// context.
558 ///
559 /// This method is typically used by [SliverReorderableList] item widgets that
560 /// insert or remove items in response to user input.
561 ///
562 /// If no [SliverReorderableList] surrounds the context given, this function
563 /// will return null.
564 ///
565 /// This method can be expensive (it walks the element tree).
566 ///
567 /// See also:
568 ///
569 /// * [of], a similar function that will throw if no [SliverReorderableList]
570 /// ancestor is found.
571 static SliverReorderableListState? maybeOf(BuildContext context) {
572 return context.findAncestorStateOfType<SliverReorderableListState>();
573 }
574}
575
576/// The state for a sliver list that allows the user to interactively reorder
577/// the list items.
578///
579/// An app that needs to start a new item drag or cancel an existing one
580/// can refer to the [SliverReorderableList]'s state with a global key:
581///
582/// ```dart
583/// // (e.g. in a stateful widget)
584/// GlobalKey<SliverReorderableListState> listKey = GlobalKey<SliverReorderableListState>();
585///
586/// // ...
587///
588/// @override
589/// Widget build(BuildContext context) {
590/// return SliverReorderableList(
591/// key: listKey,
592/// itemBuilder: (BuildContext context, int index) => const SizedBox(height: 10.0),
593/// itemCount: 5,
594/// onReorder: (int oldIndex, int newIndex) {
595/// // ...
596/// },
597/// );
598/// }
599///
600/// // ...
601///
602/// void _stop() {
603/// listKey.currentState!.cancelReorder();
604/// }
605/// ```
606///
607/// [ReorderableDragStartListener] and [ReorderableDelayedDragStartListener]
608/// refer to their [SliverReorderableList] with the static
609/// [SliverReorderableList.of] method.
610class SliverReorderableListState extends State<SliverReorderableList> with TickerProviderStateMixin {
611 // Map of index -> child state used manage where the dragging item will need
612 // to be inserted.
613 final Map<int, _ReorderableItemState> _items = <int, _ReorderableItemState>{};
614
615 OverlayEntry? _overlayEntry;
616 int? _dragIndex;
617 _DragInfo? _dragInfo;
618 int? _insertIndex;
619 Offset? _finalDropPosition;
620 MultiDragGestureRecognizer? _recognizer;
621 int? _recognizerPointer;
622 // To implement the gap for the dragged item, we replace the dragged item
623 // with a zero sized box, and then translate all of the later items down
624 // by the size of the dragged item. This allows us to keep the order of the
625 // list, while still being able to animate the gap between the items. However
626 // for the first frame of the drag, the item has not yet been replaced, so
627 // the calculation for the gap is off by the size of the gap. This flag is
628 // used to determine if the transition to the zero sized box has completed,
629 // so the gap calculation can compensate for it.
630 bool _dragStartTransitionComplete = false;
631
632 EdgeDraggingAutoScroller? _autoScroller;
633
634 late ScrollableState _scrollable;
635 Axis get _scrollDirection => axisDirectionToAxis(_scrollable.axisDirection);
636 bool get _reverse =>
637 _scrollable.axisDirection == AxisDirection.up ||
638 _scrollable.axisDirection == AxisDirection.left;
639
640 @override
641 void didChangeDependencies() {
642 super.didChangeDependencies();
643 _scrollable = Scrollable.of(context);
644 if (_autoScroller?.scrollable != _scrollable) {
645 _autoScroller?.stopAutoScroll();
646 _autoScroller = EdgeDraggingAutoScroller(
647 _scrollable,
648 onScrollViewScrolled: _handleScrollableAutoScrolled,
649 velocityScalar: widget.autoScrollerVelocityScalar,
650 );
651 }
652 }
653
654 @override
655 void didUpdateWidget(covariant SliverReorderableList oldWidget) {
656 super.didUpdateWidget(oldWidget);
657 if (widget.itemCount != oldWidget.itemCount) {
658 cancelReorder();
659 }
660
661 if (widget.autoScrollerVelocityScalar != oldWidget.autoScrollerVelocityScalar) {
662 _autoScroller?.stopAutoScroll();
663 _autoScroller = EdgeDraggingAutoScroller(
664 _scrollable,
665 onScrollViewScrolled: _handleScrollableAutoScrolled,
666 velocityScalar: widget.autoScrollerVelocityScalar,
667 );
668 }
669 }
670
671 @override
672 void dispose() {
673 _dragReset();
674 _recognizer?.dispose();
675 super.dispose();
676 }
677
678 /// Initiate the dragging of the item at [index] that was started with
679 /// the pointer down [event].
680 ///
681 /// The given [recognizer] will be used to recognize and start the drag
682 /// item tracking and lead to either an item reorder, or a canceled drag.
683 ///
684 /// Most applications will not use this directly, but will wrap the item
685 /// (or part of the item, like a drag handle) in either a
686 /// [ReorderableDragStartListener] or [ReorderableDelayedDragStartListener]
687 /// which call this method when they detect the gesture that triggers a drag
688 /// start.
689 void startItemDragReorder({
690 required int index,
691 required PointerDownEvent event,
692 required MultiDragGestureRecognizer recognizer,
693 }) {
694 assert(0 <= index && index < widget.itemCount);
695 setState(() {
696 if (_dragInfo != null) {
697 cancelReorder();
698 } else if (_recognizer != null && _recognizerPointer != event.pointer) {
699 _recognizer!.dispose();
700 _recognizer = null;
701 _recognizerPointer = null;
702 }
703
704 if (_items.containsKey(index)) {
705 _dragIndex = index;
706 _recognizer = recognizer
707 ..onStart = _dragStart
708 ..addPointer(event);
709 _recognizerPointer = event.pointer;
710 } else {
711 // TODO(darrenaustin): Can we handle this better, maybe scroll to the item?
712 throw Exception('Attempting to start a drag on a non-visible item');
713 }
714 });
715 }
716
717 /// Cancel any item drag in progress.
718 ///
719 /// This should be called before any major changes to the item list
720 /// occur so that any item drags will not get confused by
721 /// changes to the underlying list.
722 ///
723 /// If a drag operation is in progress, this will immediately reset
724 /// the list to back to its pre-drag state.
725 ///
726 /// If no drag is active, this will do nothing.
727 void cancelReorder() {
728 setState(() {
729 _dragReset();
730 });
731 }
732
733 void _registerItem(_ReorderableItemState item) {
734 if (_dragInfo != null && _items[item.index] != item) {
735 item.updateForGap(_dragInfo!.index, _dragInfo!.itemExtent, false, _reverse);
736 }
737 _items[item.index] = item;
738 if (item.index == _dragInfo?.index) {
739 item.dragging = true;
740 item.rebuild();
741 }
742 }
743
744 void _unregisterItem(int index, _ReorderableItemState item) {
745 final _ReorderableItemState? currentItem = _items[index];
746 if (currentItem == item) {
747 _items.remove(index);
748 }
749 }
750
751 Drag? _dragStart(Offset position) {
752 assert(_dragInfo == null);
753 final _ReorderableItemState item = _items[_dragIndex!]!;
754 item.dragging = true;
755 widget.onReorderStart?.call(_dragIndex!);
756 item.rebuild();
757 _dragStartTransitionComplete = false;
758 SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
759 _dragStartTransitionComplete = true;
760 }, debugLabel: 'SliverReorderableList.completeDragStartTransition');
761
762 _insertIndex = item.index;
763 _dragInfo = _DragInfo(
764 item: item,
765 initialPosition: position,
766 scrollDirection: _scrollDirection,
767 onUpdate: _dragUpdate,
768 onCancel: _dragCancel,
769 onEnd: _dragEnd,
770 onDropCompleted: _dropCompleted,
771 proxyDecorator: widget.proxyDecorator,
772 tickerProvider: this,
773 );
774 _dragInfo!.startDrag();
775
776 final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget);
777 assert(_overlayEntry == null);
778 _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy);
779 overlay.insert(_overlayEntry!);
780
781 for (final _ReorderableItemState childItem in _items.values) {
782 if (childItem == item || !childItem.mounted) {
783 continue;
784 }
785 childItem.updateForGap(_insertIndex!, _dragInfo!.itemExtent, false, _reverse);
786 }
787 return _dragInfo;
788 }
789
790 void _dragUpdate(_DragInfo item, Offset position, Offset delta) {
791 setState(() {
792 _overlayEntry?.markNeedsBuild();
793 _dragUpdateItems();
794 _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect);
795 });
796 }
797
798 void _dragCancel(_DragInfo item) {
799 setState(() {
800 _dragReset();
801 });
802 }
803
804 void _dragEnd(_DragInfo item) {
805 setState(() {
806 if (_insertIndex == item.index) {
807 // Although it's at its original position, the original position has been replaced by a zero-size box
808 // So when reversed, it should offset its own extent
809 _finalDropPosition = _reverse ? _itemOffsetAt(_insertIndex!) - _extentOffset(item.itemExtent, _scrollDirection) : _itemOffsetAt(_insertIndex!);
810 } else if (_reverse) {
811 if (_insertIndex! >= _items.length) {
812 // Drop at the starting position of the last element and offset its own extent
813 _finalDropPosition = _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection);
814 } else {
815 // Drop at the end of the current element occupying the insert position
816 _finalDropPosition = _itemOffsetAt(_insertIndex!) + _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection);
817 }
818 } else {
819 if (_insertIndex! == 0) {
820 // Drop at the starting position of the first element and offset its own extent
821 _finalDropPosition = _itemOffsetAt(0) - _extentOffset(item.itemExtent, _scrollDirection);
822 } else {
823 // Drop at the end of the previous element occupying the insert position
824 final int atIndex = _insertIndex! - 1;
825 _finalDropPosition = _itemOffsetAt(atIndex) + _extentOffset(_itemExtentAt(atIndex), _scrollDirection);
826 }
827 }
828 });
829 widget.onReorderEnd?.call(_insertIndex!);
830 }
831
832 void _dropCompleted() {
833 final int fromIndex = _dragIndex!;
834 final int toIndex = _insertIndex!;
835 if (fromIndex != toIndex) {
836 widget.onReorder.call(fromIndex, toIndex);
837 }
838 setState(() {
839 _dragReset();
840 });
841 }
842
843 void _dragReset() {
844 if (_dragInfo != null) {
845 if (_dragIndex != null && _items.containsKey(_dragIndex)) {
846 final _ReorderableItemState dragItem = _items[_dragIndex!]!;
847 dragItem._dragging = false;
848 dragItem.rebuild();
849 _dragIndex = null;
850 }
851 _dragInfo?.dispose();
852 _dragInfo = null;
853 _autoScroller?.stopAutoScroll();
854 _resetItemGap();
855 _recognizer?.dispose();
856 _recognizer = null;
857 _overlayEntry?.remove();
858 _overlayEntry?.dispose();
859 _overlayEntry = null;
860 _finalDropPosition = null;
861 }
862 }
863
864 void _resetItemGap() {
865 for (final _ReorderableItemState item in _items.values) {
866 item.resetGap();
867 }
868 }
869
870 void _handleScrollableAutoScrolled() {
871 if (_dragInfo == null) {
872 return;
873 }
874 _dragUpdateItems();
875 // Continue scrolling if the drag is still in progress.
876 _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect);
877 }
878
879 void _dragUpdateItems() {
880 assert(_dragInfo != null);
881 final double gapExtent = _dragInfo!.itemExtent;
882 final double proxyItemStart = _offsetExtent(_dragInfo!.dragPosition - _dragInfo!.dragOffset, _scrollDirection);
883 final double proxyItemEnd = proxyItemStart + gapExtent;
884
885 // Find the new index for inserting the item being dragged.
886 int newIndex = _insertIndex!;
887 for (final _ReorderableItemState item in _items.values) {
888 if (item.index == _dragIndex! || !item.mounted) {
889 continue;
890 }
891
892 Rect geometry = item.targetGeometry();
893 if (!_dragStartTransitionComplete && _dragIndex! <= item.index) {
894 // Transition is not complete, so each item after the dragged item is still
895 // in its normal location and not moved up for the zero sized box that will
896 // replace the dragged item.
897 final Offset transitionOffset = _extentOffset(_reverse ? -gapExtent : gapExtent, _scrollDirection);
898 geometry = (geometry.topLeft - transitionOffset) & geometry.size;
899 }
900 final double itemStart = _scrollDirection == Axis.vertical ? geometry.top : geometry.left;
901 final double itemExtent = _scrollDirection == Axis.vertical ? geometry.height : geometry.width;
902 final double itemEnd = itemStart + itemExtent;
903 final double itemMiddle = itemStart + itemExtent / 2;
904
905 if (_reverse) {
906 if (itemEnd >= proxyItemEnd && proxyItemEnd >= itemMiddle) {
907 // The start of the proxy is in the beginning half of the item, so
908 // we should swap the item with the gap and we are done looking for
909 // the new index.
910 newIndex = item.index;
911 break;
912
913 } else if (itemMiddle >= proxyItemStart && proxyItemStart >= itemStart) {
914 // The end of the proxy is in the ending half of the item, so
915 // we should swap the item with the gap and we are done looking for
916 // the new index.
917 newIndex = item.index + 1;
918 break;
919
920 } else if (itemStart > proxyItemEnd && newIndex < (item.index + 1)) {
921 newIndex = item.index + 1;
922 } else if (proxyItemStart > itemEnd && newIndex > item.index) {
923 newIndex = item.index;
924 }
925 } else {
926 if (itemStart <= proxyItemStart && proxyItemStart <= itemMiddle) {
927 // The start of the proxy is in the beginning half of the item, so
928 // we should swap the item with the gap and we are done looking for
929 // the new index.
930 newIndex = item.index;
931 break;
932
933 } else if (itemMiddle <= proxyItemEnd && proxyItemEnd <= itemEnd) {
934 // The end of the proxy is in the ending half of the item, so
935 // we should swap the item with the gap and we are done looking for
936 // the new index.
937 newIndex = item.index + 1;
938 break;
939
940 } else if (itemEnd < proxyItemStart && newIndex < (item.index + 1)) {
941 newIndex = item.index + 1;
942 } else if (proxyItemEnd < itemStart && newIndex > item.index) {
943 newIndex = item.index;
944 }
945 }
946 }
947
948 if (newIndex != _insertIndex) {
949 _insertIndex = newIndex;
950 for (final _ReorderableItemState item in _items.values) {
951 if (item.index == _dragIndex! || !item.mounted) {
952 continue;
953 }
954 item.updateForGap(newIndex, gapExtent, true, _reverse);
955 }
956 }
957 }
958
959 Rect get _dragTargetRect {
960 final Offset origin = _dragInfo!.dragPosition - _dragInfo!.dragOffset;
961 return Rect.fromLTWH(origin.dx, origin.dy, _dragInfo!.itemSize.width, _dragInfo!.itemSize.height);
962 }
963
964 Offset _itemOffsetAt(int index) {
965 return _items[index]!.targetGeometry().topLeft;
966 }
967
968 double _itemExtentAt(int index) {
969 return _sizeExtent(_items[index]!.targetGeometry().size, _scrollDirection);
970 }
971
972 Widget _itemBuilder(BuildContext context, int index) {
973 if (_dragInfo != null && index >= widget.itemCount) {
974 switch (_scrollDirection) {
975 case Axis.horizontal:
976 return SizedBox(width: _dragInfo!.itemExtent);
977 case Axis.vertical:
978 return SizedBox(height: _dragInfo!.itemExtent);
979 }
980 }
981 final Widget child = widget.itemBuilder(context, index);
982 assert(child.key != null, 'All list items must have a key');
983 final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget);
984 return _ReorderableItem(
985 key: _ReorderableItemGlobalKey(child.key!, index, this),
986 index: index,
987 capturedThemes: InheritedTheme.capture(from: context, to: overlay.context),
988 child: _wrapWithSemantics(child, index),
989 );
990 }
991
992 Widget _wrapWithSemantics(Widget child, int index) {
993 void reorder(int startIndex, int endIndex) {
994 if (startIndex != endIndex) {
995 widget.onReorder(startIndex, endIndex);
996 }
997 }
998
999 // First, determine which semantics actions apply.
1000 final Map<CustomSemanticsAction, VoidCallback> semanticsActions = <CustomSemanticsAction, VoidCallback>{};
1001
1002 // Create the appropriate semantics actions.
1003 void moveToStart() => reorder(index, 0);
1004 void moveToEnd() => reorder(index, widget.itemCount);
1005 void moveBefore() => reorder(index, index - 1);
1006 // To move after, go to index+2 because it is moved to the space
1007 // before index+2, which is after the space at index+1.
1008 void moveAfter() => reorder(index, index + 2);
1009
1010 final WidgetsLocalizations localizations = WidgetsLocalizations.of(context);
1011 final bool isHorizontal = _scrollDirection == Axis.horizontal;
1012 // If the item can move to before its current position in the list.
1013 if (index > 0) {
1014 semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToStart)] = moveToStart;
1015 String reorderItemBefore = localizations.reorderItemUp;
1016 if (isHorizontal) {
1017 reorderItemBefore = Directionality.of(context) == TextDirection.ltr
1018 ? localizations.reorderItemLeft
1019 : localizations.reorderItemRight;
1020 }
1021 semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = moveBefore;
1022 }
1023
1024 // If the item can move to after its current position in the list.
1025 if (index < widget.itemCount - 1) {
1026 String reorderItemAfter = localizations.reorderItemDown;
1027 if (isHorizontal) {
1028 reorderItemAfter = Directionality.of(context) == TextDirection.ltr
1029 ? localizations.reorderItemRight
1030 : localizations.reorderItemLeft;
1031 }
1032 semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = moveAfter;
1033 semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToEnd)] = moveToEnd;
1034 }
1035
1036 // Pass toWrap with a GlobalKey into the item so that when it
1037 // gets dragged, the accessibility framework can preserve the selected
1038 // state of the dragging item.
1039 //
1040 // Also apply the relevant custom accessibility actions for moving the item
1041 // up, down, to the start, and to the end of the list.
1042 return Semantics(
1043 container: true,
1044 customSemanticsActions: semanticsActions,
1045 child: child,
1046 );
1047 }
1048
1049 @override
1050 Widget build(BuildContext context) {
1051 assert(debugCheckHasOverlay(context));
1052 final SliverChildBuilderDelegate childrenDelegate = SliverChildBuilderDelegate(
1053 _itemBuilder,
1054 // When dragging, the dragged item is still in the list but has been replaced
1055 // by a zero height SizedBox, so that the gap can move around. To make the
1056 // list extent stable we add a dummy entry to the end.
1057 childCount: widget.itemCount + (_dragInfo != null ? 1 : 0),
1058 findChildIndexCallback: widget.findChildIndexCallback,
1059 );
1060 if (widget.itemExtent != null) {
1061 return SliverFixedExtentList(
1062 delegate: childrenDelegate,
1063 itemExtent: widget.itemExtent!,
1064 );
1065 } else if (widget.itemExtentBuilder != null) {
1066 return SliverVariedExtentList(
1067 delegate: childrenDelegate,
1068 itemExtentBuilder: widget.itemExtentBuilder!,
1069 );
1070 } else if (widget.prototypeItem != null) {
1071 return SliverPrototypeExtentList(
1072 delegate: childrenDelegate,
1073 prototypeItem: widget.prototypeItem!,
1074 );
1075 }
1076 return SliverList(delegate: childrenDelegate);
1077 }
1078}
1079
1080class _ReorderableItem extends StatefulWidget {
1081 const _ReorderableItem({
1082 required Key key,
1083 required this.index,
1084 required this.child,
1085 required this.capturedThemes,
1086 }) : super(key: key);
1087
1088 final int index;
1089 final Widget child;
1090 final CapturedThemes capturedThemes;
1091
1092 @override
1093 _ReorderableItemState createState() => _ReorderableItemState();
1094}
1095
1096class _ReorderableItemState extends State<_ReorderableItem> {
1097 late SliverReorderableListState _listState;
1098
1099 Offset _startOffset = Offset.zero;
1100 Offset _targetOffset = Offset.zero;
1101 AnimationController? _offsetAnimation;
1102
1103 Key get key => widget.key!;
1104 int get index => widget.index;
1105
1106 bool get dragging => _dragging;
1107 set dragging(bool dragging) {
1108 if (mounted) {
1109 setState(() {
1110 _dragging = dragging;
1111 });
1112 }
1113 }
1114 bool _dragging = false;
1115
1116 @override
1117 void initState() {
1118 _listState = SliverReorderableList.of(context);
1119 _listState._registerItem(this);
1120 super.initState();
1121 }
1122
1123 @override
1124 void dispose() {
1125 _offsetAnimation?.dispose();
1126 _listState._unregisterItem(index, this);
1127 super.dispose();
1128 }
1129
1130 @override
1131 void didUpdateWidget(covariant _ReorderableItem oldWidget) {
1132 super.didUpdateWidget(oldWidget);
1133 if (oldWidget.index != widget.index) {
1134 _listState._unregisterItem(oldWidget.index, this);
1135 _listState._registerItem(this);
1136 }
1137 }
1138
1139 @override
1140 Widget build(BuildContext context) {
1141 if (_dragging) {
1142 return const SizedBox();
1143 }
1144 _listState._registerItem(this);
1145 return Transform(
1146 transform: Matrix4.translationValues(offset.dx, offset.dy, 0.0),
1147 child: widget.child,
1148 );
1149 }
1150
1151 @override
1152 void deactivate() {
1153 _listState._unregisterItem(index, this);
1154 super.deactivate();
1155 }
1156
1157 Offset get offset {
1158 if (_offsetAnimation != null) {
1159 final double animValue = Curves.easeInOut.transform(_offsetAnimation!.value);
1160 return Offset.lerp(_startOffset, _targetOffset, animValue)!;
1161 }
1162 return _targetOffset;
1163 }
1164
1165 void updateForGap(int gapIndex, double gapExtent, bool animate, bool reverse) {
1166 final Offset newTargetOffset = (gapIndex <= index)
1167 ? _extentOffset(reverse ? -gapExtent : gapExtent, _listState._scrollDirection)
1168 : Offset.zero;
1169 if (newTargetOffset != _targetOffset) {
1170 _targetOffset = newTargetOffset;
1171 if (animate) {
1172 if (_offsetAnimation == null) {
1173 _offsetAnimation = AnimationController(
1174 vsync: _listState,
1175 duration: const Duration(milliseconds: 250),
1176 )
1177 ..addListener(rebuild)
1178 ..addStatusListener((AnimationStatus status) {
1179 if (status == AnimationStatus.completed) {
1180 _startOffset = _targetOffset;
1181 _offsetAnimation!.dispose();
1182 _offsetAnimation = null;
1183 }
1184 })
1185 ..forward();
1186 } else {
1187 _startOffset = offset;
1188 _offsetAnimation!.forward(from: 0.0);
1189 }
1190 } else {
1191 if (_offsetAnimation != null) {
1192 _offsetAnimation!.dispose();
1193 _offsetAnimation = null;
1194 }
1195 _startOffset = _targetOffset;
1196 }
1197 rebuild();
1198 }
1199 }
1200
1201 void resetGap() {
1202 if (_offsetAnimation != null) {
1203 _offsetAnimation!.dispose();
1204 _offsetAnimation = null;
1205 }
1206 _startOffset = Offset.zero;
1207 _targetOffset = Offset.zero;
1208 rebuild();
1209 }
1210
1211 Rect targetGeometry() {
1212 final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox;
1213 final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero) + _targetOffset;
1214 return itemPosition & itemRenderBox.size;
1215 }
1216
1217 void rebuild() {
1218 if (mounted) {
1219 setState(() {});
1220 }
1221 }
1222}
1223
1224/// A wrapper widget that will recognize the start of a drag on the wrapped
1225/// widget by a [PointerDownEvent], and immediately initiate dragging the
1226/// wrapped item to a new location in a reorderable list.
1227///
1228/// See also:
1229///
1230/// * [ReorderableDelayedDragStartListener], a similar wrapper that will
1231/// only recognize the start after a long press event.
1232/// * [ReorderableList], a widget list that allows the user to reorder
1233/// its items.
1234/// * [SliverReorderableList], a sliver list that allows the user to reorder
1235/// its items.
1236/// * [ReorderableListView], a Material Design list that allows the user to
1237/// reorder its items.
1238class ReorderableDragStartListener extends StatelessWidget {
1239 /// Creates a listener for a drag immediately following a pointer down
1240 /// event over the given child widget.
1241 ///
1242 /// This is most commonly used to wrap part of a list item like a drag
1243 /// handle.
1244 const ReorderableDragStartListener({
1245 super.key,
1246 required this.child,
1247 required this.index,
1248 this.enabled = true,
1249 });
1250
1251 /// The widget for which the application would like to respond to a tap and
1252 /// drag gesture by starting a reordering drag on a reorderable list.
1253 final Widget child;
1254
1255 /// The index of the associated item that will be dragged in the list.
1256 final int index;
1257
1258 /// Whether the [child] item can be dragged and moved in the list.
1259 ///
1260 /// If true, the item can be moved to another location in the list when the
1261 /// user taps on the child. If false, tapping on the child will be ignored.
1262 final bool enabled;
1263
1264 @override
1265 Widget build(BuildContext context) {
1266 return Listener(
1267 onPointerDown: enabled ? (PointerDownEvent event) => _startDragging(context, event) : null,
1268 child: child,
1269 );
1270 }
1271
1272 /// Provides the gesture recognizer used to indicate the start of a reordering
1273 /// drag operation.
1274 ///
1275 /// By default this returns an [ImmediateMultiDragGestureRecognizer] but
1276 /// subclasses can use this to customize the drag start gesture.
1277 @protected
1278 MultiDragGestureRecognizer createRecognizer() {
1279 return ImmediateMultiDragGestureRecognizer(debugOwner: this);
1280 }
1281
1282 void _startDragging(BuildContext context, PointerDownEvent event) {
1283 final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
1284 final SliverReorderableListState? list = SliverReorderableList.maybeOf(context);
1285 list?.startItemDragReorder(
1286 index: index,
1287 event: event,
1288 recognizer: createRecognizer()
1289 ..gestureSettings = gestureSettings,
1290 );
1291 }
1292}
1293
1294/// A wrapper widget that will recognize the start of a drag operation by
1295/// looking for a long press event. Once it is recognized, it will start
1296/// a drag operation on the wrapped item in the reorderable list.
1297///
1298/// See also:
1299///
1300/// * [ReorderableDragStartListener], a similar wrapper that will
1301/// recognize the start of the drag immediately after a pointer down event.
1302/// * [ReorderableList], a widget list that allows the user to reorder
1303/// its items.
1304/// * [SliverReorderableList], a sliver list that allows the user to reorder
1305/// its items.
1306/// * [ReorderableListView], a Material Design list that allows the user to
1307/// reorder its items.
1308class ReorderableDelayedDragStartListener extends ReorderableDragStartListener {
1309 /// Creates a listener for an drag following a long press event over the
1310 /// given child widget.
1311 ///
1312 /// This is most commonly used to wrap an entire list item in a reorderable
1313 /// list.
1314 const ReorderableDelayedDragStartListener({
1315 super.key,
1316 required super.child,
1317 required super.index,
1318 super.enabled,
1319 });
1320
1321 @override
1322 MultiDragGestureRecognizer createRecognizer() {
1323 return DelayedMultiDragGestureRecognizer(debugOwner: this);
1324 }
1325}
1326
1327typedef _DragItemUpdate = void Function(_DragInfo item, Offset position, Offset delta);
1328typedef _DragItemCallback = void Function(_DragInfo item);
1329
1330class _DragInfo extends Drag {
1331 _DragInfo({
1332 required _ReorderableItemState item,
1333 Offset initialPosition = Offset.zero,
1334 this.scrollDirection = Axis.vertical,
1335 this.onUpdate,
1336 this.onEnd,
1337 this.onCancel,
1338 this.onDropCompleted,
1339 this.proxyDecorator,
1340 required this.tickerProvider,
1341 }) {
1342 final RenderBox itemRenderBox = item.context.findRenderObject()! as RenderBox;
1343 listState = item._listState;
1344 index = item.index;
1345 child = item.widget.child;
1346 capturedThemes = item.widget.capturedThemes;
1347 dragPosition = initialPosition;
1348 dragOffset = itemRenderBox.globalToLocal(initialPosition);
1349 itemSize = item.context.size!;
1350 itemExtent = _sizeExtent(itemSize, scrollDirection);
1351 scrollable = Scrollable.of(item.context);
1352 }
1353
1354 final Axis scrollDirection;
1355 final _DragItemUpdate? onUpdate;
1356 final _DragItemCallback? onEnd;
1357 final _DragItemCallback? onCancel;
1358 final VoidCallback? onDropCompleted;
1359 final ReorderItemProxyDecorator? proxyDecorator;
1360 final TickerProvider tickerProvider;
1361
1362 late SliverReorderableListState listState;
1363 late int index;
1364 late Widget child;
1365 late Offset dragPosition;
1366 late Offset dragOffset;
1367 late Size itemSize;
1368 late double itemExtent;
1369 late CapturedThemes capturedThemes;
1370 ScrollableState? scrollable;
1371 AnimationController? _proxyAnimation;
1372
1373 void dispose() {
1374 _proxyAnimation?.dispose();
1375 }
1376
1377 void startDrag() {
1378 _proxyAnimation = AnimationController(
1379 vsync: tickerProvider,
1380 duration: const Duration(milliseconds: 250),
1381 )
1382 ..addStatusListener((AnimationStatus status) {
1383 if (status == AnimationStatus.dismissed) {
1384 _dropCompleted();
1385 }
1386 })
1387 ..forward();
1388 }
1389
1390 @override
1391 void update(DragUpdateDetails details) {
1392 final Offset delta = _restrictAxis(details.delta, scrollDirection);
1393 dragPosition += delta;
1394 onUpdate?.call(this, dragPosition, details.delta);
1395 }
1396
1397 @override
1398 void end(DragEndDetails details) {
1399 _proxyAnimation!.reverse();
1400 onEnd?.call(this);
1401 }
1402
1403 @override
1404 void cancel() {
1405 _proxyAnimation?.dispose();
1406 _proxyAnimation = null;
1407 onCancel?.call(this);
1408 }
1409
1410 void _dropCompleted() {
1411 _proxyAnimation?.dispose();
1412 _proxyAnimation = null;
1413 onDropCompleted?.call();
1414 }
1415
1416 Widget createProxy(BuildContext context) {
1417 return capturedThemes.wrap(
1418 _DragItemProxy(
1419 listState: listState,
1420 index: index,
1421 size: itemSize,
1422 animation: _proxyAnimation!,
1423 position: dragPosition - dragOffset - _overlayOrigin(context),
1424 proxyDecorator: proxyDecorator,
1425 child: child,
1426 ),
1427 );
1428 }
1429}
1430
1431Offset _overlayOrigin(BuildContext context) {
1432 final OverlayState overlay = Overlay.of(context, debugRequiredFor: context.widget);
1433 final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox;
1434 return overlayBox.localToGlobal(Offset.zero);
1435}
1436
1437class _DragItemProxy extends StatelessWidget {
1438 const _DragItemProxy({
1439 required this.listState,
1440 required this.index,
1441 required this.child,
1442 required this.position,
1443 required this.size,
1444 required this.animation,
1445 required this.proxyDecorator,
1446 });
1447
1448 final SliverReorderableListState listState;
1449 final int index;
1450 final Widget child;
1451 final Offset position;
1452 final Size size;
1453 final AnimationController animation;
1454 final ReorderItemProxyDecorator? proxyDecorator;
1455
1456 @override
1457 Widget build(BuildContext context) {
1458 final Widget proxyChild = proxyDecorator?.call(child, index, animation.view) ?? child;
1459 final Offset overlayOrigin = _overlayOrigin(context);
1460
1461 return MediaQuery(
1462 // Remove the top padding so that any nested list views in the item
1463 // won't pick up the scaffold's padding in the overlay.
1464 data: MediaQuery.of(context).removePadding(removeTop: true),
1465 child: AnimatedBuilder(
1466 animation: animation,
1467 builder: (BuildContext context, Widget? child) {
1468 Offset effectivePosition = position;
1469 final Offset? dropPosition = listState._finalDropPosition;
1470 if (dropPosition != null) {
1471 effectivePosition = Offset.lerp(dropPosition - overlayOrigin, effectivePosition, Curves.easeOut.transform(animation.value))!;
1472 }
1473 return Positioned(
1474 left: effectivePosition.dx,
1475 top: effectivePosition.dy,
1476 child: SizedBox(
1477 width: size.width,
1478 height: size.height,
1479 child: child,
1480 ),
1481 );
1482 },
1483 child: proxyChild,
1484 ),
1485 );
1486 }
1487}
1488
1489double _sizeExtent(Size size, Axis scrollDirection) {
1490 switch (scrollDirection) {
1491 case Axis.horizontal:
1492 return size.width;
1493 case Axis.vertical:
1494 return size.height;
1495 }
1496}
1497
1498double _offsetExtent(Offset offset, Axis scrollDirection) {
1499 switch (scrollDirection) {
1500 case Axis.horizontal:
1501 return offset.dx;
1502 case Axis.vertical:
1503 return offset.dy;
1504 }
1505}
1506
1507Offset _extentOffset(double extent, Axis scrollDirection) {
1508 switch (scrollDirection) {
1509 case Axis.horizontal:
1510 return Offset(extent, 0.0);
1511 case Axis.vertical:
1512 return Offset(0.0, extent);
1513 }
1514}
1515
1516Offset _restrictAxis(Offset offset, Axis scrollDirection) {
1517 switch (scrollDirection) {
1518 case Axis.horizontal:
1519 return Offset(offset.dx, 0.0);
1520 case Axis.vertical:
1521 return Offset(0.0, offset.dy);
1522 }
1523}
1524
1525// A global key that takes its identity from the object and uses a value of a
1526// particular type to identify itself.
1527//
1528// The difference with GlobalObjectKey is that it uses [==] instead of [identical]
1529// of the objects used to generate widgets.
1530@optionalTypeArgs
1531class _ReorderableItemGlobalKey extends GlobalObjectKey {
1532
1533 const _ReorderableItemGlobalKey(this.subKey, this.index, this.state) : super(subKey);
1534
1535 final Key subKey;
1536 final int index;
1537 final SliverReorderableListState state;
1538
1539 @override
1540 bool operator ==(Object other) {
1541 if (other.runtimeType != runtimeType) {
1542 return false;
1543 }
1544 return other is _ReorderableItemGlobalKey
1545 && other.subKey == subKey
1546 && other.index == index
1547 && other.state == state;
1548 }
1549
1550 @override
1551 int get hashCode => Object.hash(subKey, index, state);
1552}
1553