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