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 =
592 context.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 =
760 recognizer
761 ..onStart = _dragStart
762 ..addPointer(event);
763 _recognizerPointer = event.pointer;
764 } else {
765 // TODO(darrenaustin): Can we handle this better, maybe scroll to the item?
766 throw Exception('Attempting to start a drag on a non-visible item');
767 }
768 });
769 }
770
771 /// Cancel any item drag in progress.
772 ///
773 /// This should be called before any major changes to the item list
774 /// occur so that any item drags will not get confused by
775 /// changes to the underlying list.
776 ///
777 /// If a drag operation is in progress, this will immediately reset
778 /// the list to back to its pre-drag state.
779 ///
780 /// If no drag is active, this will do nothing.
781 void cancelReorder() {
782 setState(() {
783 _dragReset();
784 });
785 }
786
787 void _registerItem(_ReorderableItemState item) {
788 if (_dragInfo != null && _items[item.index] != item) {
789 item.updateForGap(_dragInfo!.index, _dragInfo!.index, _dragInfo!.itemExtent, false, _reverse);
790 }
791 _items[item.index] = item;
792 if (item.index == _dragInfo?.index) {
793 item.dragging = true;
794 item.rebuild();
795 }
796 }
797
798 void _unregisterItem(int index, _ReorderableItemState item) {
799 final _ReorderableItemState? currentItem = _items[index];
800 if (currentItem == item) {
801 _items.remove(index);
802 }
803 }
804
805 Drag? _dragStart(Offset position) {
806 assert(_dragInfo == null);
807 final _ReorderableItemState item = _items[_dragIndex!]!;
808 item.dragging = true;
809 widget.onReorderStart?.call(_dragIndex!);
810 item.rebuild();
811
812 _insertIndex = item.index;
813 _dragInfo = _DragInfo(
814 item: item,
815 initialPosition: position,
816 scrollDirection: _scrollDirection,
817 onUpdate: _dragUpdate,
818 onCancel: _dragCancel,
819 onEnd: _dragEnd,
820 onDropCompleted: _dropCompleted,
821 proxyDecorator: widget.proxyDecorator,
822 tickerProvider: this,
823 );
824 _dragInfo!.startDrag();
825
826 final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget);
827 assert(_overlayEntry == null);
828 _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy);
829 overlay.insert(_overlayEntry!);
830
831 for (final _ReorderableItemState childItem in _items.values) {
832 if (childItem == item || !childItem.mounted) {
833 continue;
834 }
835 childItem.updateForGap(_insertIndex!, _insertIndex!, _dragInfo!.itemExtent, false, _reverse);
836 }
837 return _dragInfo;
838 }
839
840 void _dragUpdate(_DragInfo item, Offset position, Offset delta) {
841 setState(() {
842 _overlayEntry?.markNeedsBuild();
843 _dragUpdateItems();
844 _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect);
845 });
846 }
847
848 void _dragCancel(_DragInfo item) {
849 setState(() {
850 _dragReset();
851 });
852 }
853
854 void _dragEnd(_DragInfo item) {
855 setState(() {
856 if (_insertIndex == item.index) {
857 _finalDropPosition = _itemOffsetAt(_insertIndex!);
858 } else if (_reverse) {
859 if (_insertIndex! >= _items.length) {
860 // Drop at the starting position of the last element and offset its own extent
861 _finalDropPosition =
862 _itemOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, _scrollDirection);
863 } else {
864 // Drop at the end of the current element occupying the insert position
865 _finalDropPosition =
866 _itemOffsetAt(_insertIndex!) +
867 _extentOffset(_itemExtentAt(_insertIndex!), _scrollDirection);
868 }
869 } else {
870 if (_insertIndex! == 0) {
871 // Drop at the starting position of the first element and offset its own extent
872 _finalDropPosition = _itemOffsetAt(0) - _extentOffset(item.itemExtent, _scrollDirection);
873 } else {
874 // Drop at the end of the previous element occupying the insert position
875 final int atIndex = _insertIndex! - 1;
876 _finalDropPosition =
877 _itemOffsetAt(atIndex) + _extentOffset(_itemExtentAt(atIndex), _scrollDirection);
878 }
879 }
880 });
881 widget.onReorderEnd?.call(_insertIndex!);
882 }
883
884 void _dropCompleted() {
885 final int fromIndex = _dragIndex!;
886 final int toIndex = _insertIndex!;
887 if (fromIndex != toIndex) {
888 widget.onReorder.call(fromIndex, toIndex);
889 }
890 setState(() {
891 _dragReset();
892 });
893 }
894
895 void _dragReset() {
896 if (_dragInfo != null) {
897 if (_dragIndex != null && _items.containsKey(_dragIndex)) {
898 final _ReorderableItemState dragItem = _items[_dragIndex!]!;
899 dragItem._dragging = false;
900 dragItem.rebuild();
901 _dragIndex = null;
902 }
903 _dragInfo?.dispose();
904 _dragInfo = null;
905 _autoScroller?.stopAutoScroll();
906 _resetItemGap();
907 _recognizer?.dispose();
908 _recognizer = null;
909 _overlayEntry?.remove();
910 _overlayEntry?.dispose();
911 _overlayEntry = null;
912 _finalDropPosition = null;
913 }
914 }
915
916 void _resetItemGap() {
917 for (final _ReorderableItemState item in _items.values) {
918 item.resetGap();
919 }
920 }
921
922 void _handleScrollableAutoScrolled() {
923 if (_dragInfo == null) {
924 return;
925 }
926 _dragUpdateItems();
927 // Continue scrolling if the drag is still in progress.
928 _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect);
929 }
930
931 void _dragUpdateItems() {
932 assert(_dragInfo != null);
933 final double gapExtent = _dragInfo!.itemExtent;
934 final double proxyItemStart = _offsetExtent(
935 _dragInfo!.dragPosition - _dragInfo!.dragOffset,
936 _scrollDirection,
937 );
938 final double proxyItemEnd = proxyItemStart + gapExtent;
939
940 // Find the new index for inserting the item being dragged.
941 int newIndex = _insertIndex!;
942 for (final _ReorderableItemState item in _items.values) {
943 if ((_reverse && item.index == _dragIndex!) || !item.mounted) {
944 continue;
945 }
946
947 final Rect geometry = item.targetGeometry();
948 final double itemStart = _scrollDirection == Axis.vertical ? geometry.top : geometry.left;
949 final double itemExtent =
950 _scrollDirection == Axis.vertical ? geometry.height : 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 =
1074 Directionality.of(context) == TextDirection.ltr
1075 ? localizations.reorderItemLeft
1076 : localizations.reorderItemRight;
1077 }
1078 semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = moveBefore;
1079 }
1080
1081 // If the item can move to after its current position in the list.
1082 if (index < widget.itemCount - 1) {
1083 String reorderItemAfter = localizations.reorderItemDown;
1084 if (isHorizontal) {
1085 reorderItemAfter =
1086 Directionality.of(context) == TextDirection.ltr
1087 ? localizations.reorderItemRight
1088 : localizations.reorderItemLeft;
1089 }
1090 semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = moveAfter;
1091 semanticsActions[CustomSemanticsAction(label: localizations.reorderItemToEnd)] = moveToEnd;
1092 }
1093
1094 // Pass toWrap with a GlobalKey into the item so that when it
1095 // gets dragged, the accessibility framework can preserve the selected
1096 // state of the dragging item.
1097 //
1098 // Also apply the relevant custom accessibility actions for moving the item
1099 // up, down, to the start, and to the end of the list.
1100 return Semantics(container: true, customSemanticsActions: semanticsActions, child: child);
1101 }
1102
1103 @protected
1104 @override
1105 Widget build(BuildContext context) {
1106 assert(debugCheckHasOverlay(context));
1107 final SliverChildBuilderDelegate childrenDelegate = SliverChildBuilderDelegate(
1108 _itemBuilder,
1109 childCount: widget.itemCount,
1110 findChildIndexCallback: widget.findChildIndexCallback,
1111 );
1112 if (widget.itemExtent != null) {
1113 return SliverFixedExtentList(delegate: childrenDelegate, itemExtent: widget.itemExtent!);
1114 } else if (widget.itemExtentBuilder != null) {
1115 return SliverVariedExtentList(
1116 delegate: childrenDelegate,
1117 itemExtentBuilder: widget.itemExtentBuilder!,
1118 );
1119 } else if (widget.prototypeItem != null) {
1120 return SliverPrototypeExtentList(
1121 delegate: childrenDelegate,
1122 prototypeItem: widget.prototypeItem!,
1123 );
1124 }
1125 return SliverList(delegate: childrenDelegate);
1126 }
1127}
1128
1129class _ReorderableItem extends StatefulWidget {
1130 const _ReorderableItem({
1131 required Key super.key,
1132 required this.index,
1133 required this.child,
1134 required this.capturedThemes,
1135 });
1136
1137 final int index;
1138 final Widget child;
1139 final CapturedThemes capturedThemes;
1140
1141 @override
1142 _ReorderableItemState createState() => _ReorderableItemState();
1143}
1144
1145class _ReorderableItemState extends State<_ReorderableItem> {
1146 late SliverReorderableListState _listState;
1147
1148 Offset _startOffset = Offset.zero;
1149 Offset _targetOffset = Offset.zero;
1150 AnimationController? _offsetAnimation;
1151
1152 Key get key => widget.key!;
1153 int get index => widget.index;
1154
1155 bool get dragging => _dragging;
1156 set dragging(bool dragging) {
1157 if (mounted) {
1158 setState(() {
1159 _dragging = dragging;
1160 });
1161 }
1162 }
1163
1164 bool _dragging = false;
1165
1166 @override
1167 void initState() {
1168 _listState = SliverReorderableList.of(context);
1169 _listState._registerItem(this);
1170 super.initState();
1171 }
1172
1173 @override
1174 void dispose() {
1175 _offsetAnimation?.dispose();
1176 _listState._unregisterItem(index, this);
1177 super.dispose();
1178 }
1179
1180 @override
1181 void didUpdateWidget(covariant _ReorderableItem oldWidget) {
1182 super.didUpdateWidget(oldWidget);
1183 if (oldWidget.index != widget.index) {
1184 _listState._unregisterItem(oldWidget.index, this);
1185 _listState._registerItem(this);
1186 }
1187 }
1188
1189 @override
1190 Widget build(BuildContext context) {
1191 if (_dragging) {
1192 final Size size = _extentSize(_listState._dragInfo!.itemExtent, _listState._scrollDirection);
1193 return SizedBox.fromSize(size: size);
1194 }
1195 _listState._registerItem(this);
1196 return Transform.translate(offset: offset, child: widget.child);
1197 }
1198
1199 @override
1200 void deactivate() {
1201 _listState._unregisterItem(index, this);
1202 super.deactivate();
1203 }
1204
1205 Offset get offset {
1206 if (_offsetAnimation != null) {
1207 final double animValue = Curves.easeInOut.transform(_offsetAnimation!.value);
1208 return Offset.lerp(_startOffset, _targetOffset, animValue)!;
1209 }
1210 return _targetOffset;
1211 }
1212
1213 void updateForGap(int dragIndex, int gapIndex, double gapExtent, bool animate, bool reverse) {
1214 // An offset needs to be added to create a gap when we are between the
1215 // moving element (dragIndex) and the current gap position (gapIndex).
1216 // For how to update the gap position, refer to [_dragUpdateItems].
1217 final Offset newTargetOffset;
1218 if (gapIndex < dragIndex && index < dragIndex && index >= gapIndex) {
1219 newTargetOffset = _extentOffset(
1220 reverse ? -gapExtent : gapExtent,
1221 _listState._scrollDirection,
1222 );
1223 } else if (gapIndex > dragIndex && index > dragIndex && index < gapIndex) {
1224 newTargetOffset = _extentOffset(
1225 reverse ? gapExtent : -gapExtent,
1226 _listState._scrollDirection,
1227 );
1228 } else {
1229 newTargetOffset = Offset.zero;
1230 }
1231 if (newTargetOffset != _targetOffset) {
1232 _targetOffset = newTargetOffset;
1233 if (animate) {
1234 if (_offsetAnimation == null) {
1235 _offsetAnimation =
1236 AnimationController(vsync: _listState, duration: const Duration(milliseconds: 250))
1237 ..addListener(rebuild)
1238 ..addStatusListener((AnimationStatus status) {
1239 if (status.isCompleted) {
1240 _startOffset = _targetOffset;
1241 _offsetAnimation!.dispose();
1242 _offsetAnimation = null;
1243 }
1244 })
1245 ..forward();
1246 } else {
1247 _startOffset = offset;
1248 _offsetAnimation!.forward(from: 0.0);
1249 }
1250 } else {
1251 if (_offsetAnimation != null) {
1252 _offsetAnimation!.dispose();
1253 _offsetAnimation = null;
1254 }
1255 _startOffset = _targetOffset;
1256 }
1257 rebuild();
1258 }
1259 }
1260
1261 void resetGap() {
1262 if (_offsetAnimation != null) {
1263 _offsetAnimation!.dispose();
1264 _offsetAnimation = null;
1265 }
1266 _startOffset = Offset.zero;
1267 _targetOffset = Offset.zero;
1268 rebuild();
1269 }
1270
1271 Rect targetGeometry() {
1272 final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox;
1273 final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero) + _targetOffset;
1274 return itemPosition & itemRenderBox.size;
1275 }
1276
1277 void rebuild() {
1278 if (mounted) {
1279 setState(() {});
1280 }
1281 }
1282}
1283
1284/// A wrapper widget that will recognize the start of a drag on the wrapped
1285/// widget by a [PointerDownEvent], and immediately initiate dragging the
1286/// wrapped item to a new location in a reorderable list.
1287///
1288/// See also:
1289///
1290/// * [ReorderableDelayedDragStartListener], a similar wrapper that will
1291/// only recognize the start after a long press event.
1292/// * [ReorderableList], a widget list that allows the user to reorder
1293/// its items.
1294/// * [SliverReorderableList], a sliver list that allows the user to reorder
1295/// its items.
1296/// * [ReorderableListView], a Material Design list that allows the user to
1297/// reorder its items.
1298class ReorderableDragStartListener extends StatelessWidget {
1299 /// Creates a listener for a drag immediately following a pointer down
1300 /// event over the given child widget.
1301 ///
1302 /// This is most commonly used to wrap part of a list item like a drag
1303 /// handle.
1304 const ReorderableDragStartListener({
1305 super.key,
1306 required this.child,
1307 required this.index,
1308 this.enabled = true,
1309 });
1310
1311 /// The widget for which the application would like to respond to a tap and
1312 /// drag gesture by starting a reordering drag on a reorderable list.
1313 final Widget child;
1314
1315 /// The index of the associated item that will be dragged in the list.
1316 final int index;
1317
1318 /// Whether the [child] item can be dragged and moved in the list.
1319 ///
1320 /// If true, the item can be moved to another location in the list when the
1321 /// user taps on the child. If false, tapping on the child will be ignored.
1322 final bool enabled;
1323
1324 @override
1325 Widget build(BuildContext context) {
1326 return Listener(
1327 onPointerDown: enabled ? (PointerDownEvent event) => _startDragging(context, event) : null,
1328 child: child,
1329 );
1330 }
1331
1332 /// Provides the gesture recognizer used to indicate the start of a reordering
1333 /// drag operation.
1334 ///
1335 /// By default this returns an [ImmediateMultiDragGestureRecognizer] but
1336 /// subclasses can use this to customize the drag start gesture.
1337 @protected
1338 MultiDragGestureRecognizer createRecognizer() {
1339 return ImmediateMultiDragGestureRecognizer(debugOwner: this);
1340 }
1341
1342 void _startDragging(BuildContext context, PointerDownEvent event) {
1343 final DeviceGestureSettings? gestureSettings = MediaQuery.maybeGestureSettingsOf(context);
1344 final SliverReorderableListState? list = SliverReorderableList.maybeOf(context);
1345 list?.startItemDragReorder(
1346 index: index,
1347 event: event,
1348 recognizer: createRecognizer()..gestureSettings = gestureSettings,
1349 );
1350 }
1351}
1352
1353/// A wrapper widget that will recognize the start of a drag operation by
1354/// looking for a long press event. Once it is recognized, it will start
1355/// a drag operation on the wrapped item in the reorderable list.
1356///
1357/// See also:
1358///
1359/// * [ReorderableDragStartListener], a similar wrapper that will
1360/// recognize the start of the drag immediately after a pointer down event.
1361/// * [ReorderableList], a widget list that allows the user to reorder
1362/// its items.
1363/// * [SliverReorderableList], a sliver list that allows the user to reorder
1364/// its items.
1365/// * [ReorderableListView], a Material Design list that allows the user to
1366/// reorder its items.
1367class ReorderableDelayedDragStartListener extends ReorderableDragStartListener {
1368 /// Creates a listener for an drag following a long press event over the
1369 /// given child widget.
1370 ///
1371 /// This is most commonly used to wrap an entire list item in a reorderable
1372 /// list.
1373 const ReorderableDelayedDragStartListener({
1374 super.key,
1375 required super.child,
1376 required super.index,
1377 super.enabled,
1378 });
1379
1380 @override
1381 MultiDragGestureRecognizer createRecognizer() {
1382 return DelayedMultiDragGestureRecognizer(debugOwner: this);
1383 }
1384}
1385
1386typedef _DragItemUpdate = void Function(_DragInfo item, Offset position, Offset delta);
1387typedef _DragItemCallback = void Function(_DragInfo item);
1388
1389class _DragInfo extends Drag {
1390 _DragInfo({
1391 required _ReorderableItemState item,
1392 Offset initialPosition = Offset.zero,
1393 this.scrollDirection = Axis.vertical,
1394 this.onUpdate,
1395 this.onEnd,
1396 this.onCancel,
1397 this.onDropCompleted,
1398 this.proxyDecorator,
1399 required this.tickerProvider,
1400 }) {
1401 assert(debugMaybeDispatchCreated('widgets', '_DragInfo', this));
1402 final RenderBox itemRenderBox = item.context.findRenderObject()! as RenderBox;
1403 listState = item._listState;
1404 index = item.index;
1405 child = item.widget.child;
1406 capturedThemes = item.widget.capturedThemes;
1407 dragOffset = itemRenderBox.globalToLocal(initialPosition);
1408 itemSize = item.context.size!;
1409 _rawDragPosition = initialPosition;
1410 if (listState.widget.dragBoundaryProvider != null) {
1411 boundary = listState.widget.dragBoundaryProvider!.call(listState.context);
1412 } else {
1413 boundary = DragBoundary.forRectMaybeOf(listState.context);
1414 }
1415 dragPosition = _adjustedDragOffset(initialPosition);
1416 itemExtent = _sizeExtent(itemSize, scrollDirection);
1417 itemLayoutConstraints = itemRenderBox.constraints;
1418 scrollable = Scrollable.of(item.context);
1419 }
1420
1421 final Axis scrollDirection;
1422 final _DragItemUpdate? onUpdate;
1423 final _DragItemCallback? onEnd;
1424 final _DragItemCallback? onCancel;
1425 final VoidCallback? onDropCompleted;
1426 final ReorderItemProxyDecorator? proxyDecorator;
1427 final TickerProvider tickerProvider;
1428
1429 late DragBoundaryDelegate<Rect>? boundary;
1430 late SliverReorderableListState listState;
1431 late int index;
1432 late Widget child;
1433 late Offset dragPosition;
1434 late Offset dragOffset;
1435 late Size itemSize;
1436 late BoxConstraints itemLayoutConstraints;
1437 late double itemExtent;
1438 late CapturedThemes capturedThemes;
1439 ScrollableState? scrollable;
1440 AnimationController? _proxyAnimation;
1441 late Offset _rawDragPosition;
1442
1443 void dispose() {
1444 assert(debugMaybeDispatchDisposed(this));
1445 _proxyAnimation?.dispose();
1446 }
1447
1448 void startDrag() {
1449 _proxyAnimation =
1450 AnimationController(vsync: tickerProvider, duration: const Duration(milliseconds: 250))
1451 ..addStatusListener((AnimationStatus status) {
1452 if (status.isDismissed) {
1453 _dropCompleted();
1454 }
1455 })
1456 ..forward();
1457 }
1458
1459 @override
1460 void update(DragUpdateDetails details) {
1461 final Offset delta = _restrictAxis(details.delta, scrollDirection);
1462 _rawDragPosition += delta;
1463 dragPosition = _adjustedDragOffset(_rawDragPosition);
1464 onUpdate?.call(this, dragPosition, details.delta);
1465 }
1466
1467 @override
1468 void end(DragEndDetails details) {
1469 _proxyAnimation!.reverse();
1470 onEnd?.call(this);
1471 }
1472
1473 @override
1474 void cancel() {
1475 _proxyAnimation?.dispose();
1476 _proxyAnimation = null;
1477 onCancel?.call(this);
1478 }
1479
1480 Offset _adjustedDragOffset(Offset offset) {
1481 if (boundary == null) {
1482 return offset;
1483 }
1484 final Offset adjOffset =
1485 boundary!
1486 .nearestPositionWithinBoundary((offset - dragOffset) & itemSize)
1487 .shift(dragOffset)
1488 .topLeft;
1489 return adjOffset;
1490 }
1491
1492 void _dropCompleted() {
1493 _proxyAnimation?.dispose();
1494 _proxyAnimation = null;
1495 onDropCompleted?.call();
1496 }
1497
1498 Widget createProxy(BuildContext context) {
1499 return capturedThemes.wrap(
1500 _DragItemProxy(
1501 listState: listState,
1502 index: index,
1503 size: itemSize,
1504 constraints: itemLayoutConstraints,
1505 animation: _proxyAnimation!,
1506 position: dragPosition - dragOffset - _overlayOrigin(context),
1507 proxyDecorator: proxyDecorator,
1508 child: child,
1509 ),
1510 );
1511 }
1512}
1513
1514Offset _overlayOrigin(BuildContext context) {
1515 final OverlayState overlay = Overlay.of(context, debugRequiredFor: context.widget);
1516 final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox;
1517 return overlayBox.localToGlobal(Offset.zero);
1518}
1519
1520class _DragItemProxy extends StatelessWidget {
1521 const _DragItemProxy({
1522 required this.listState,
1523 required this.index,
1524 required this.child,
1525 required this.position,
1526 required this.size,
1527 required this.constraints,
1528 required this.animation,
1529 required this.proxyDecorator,
1530 });
1531
1532 final SliverReorderableListState listState;
1533 final int index;
1534 final Widget child;
1535 final Offset position;
1536 final Size size;
1537 final BoxConstraints constraints;
1538 final AnimationController animation;
1539 final ReorderItemProxyDecorator? proxyDecorator;
1540
1541 @override
1542 Widget build(BuildContext context) {
1543 final Widget proxyChild = proxyDecorator?.call(child, index, animation.view) ?? child;
1544 final Offset overlayOrigin = _overlayOrigin(context);
1545
1546 return MediaQuery(
1547 // Remove the top padding so that any nested list views in the item
1548 // won't pick up the scaffold's padding in the overlay.
1549 data: MediaQuery.of(context).removePadding(removeTop: true),
1550 child: AnimatedBuilder(
1551 animation: animation,
1552 builder: (BuildContext context, Widget? child) {
1553 Offset effectivePosition = position;
1554 final Offset? dropPosition = listState._finalDropPosition;
1555 if (dropPosition != null) {
1556 effectivePosition =
1557 Offset.lerp(
1558 dropPosition - overlayOrigin,
1559 effectivePosition,
1560 Curves.easeOut.transform(animation.value),
1561 )!;
1562 }
1563 return Positioned(
1564 left: effectivePosition.dx,
1565 top: effectivePosition.dy,
1566 child: SizedBox(
1567 width: size.width,
1568 height: size.height,
1569 child: OverflowBox(
1570 minWidth: constraints.minWidth,
1571 minHeight: constraints.minHeight,
1572 maxWidth: constraints.maxWidth,
1573 maxHeight: constraints.maxHeight,
1574 alignment:
1575 listState._scrollDirection == Axis.horizontal
1576 ? Alignment.centerLeft
1577 : Alignment.topCenter,
1578 child: child,
1579 ),
1580 ),
1581 );
1582 },
1583 child: proxyChild,
1584 ),
1585 );
1586 }
1587}
1588
1589double _sizeExtent(Size size, Axis scrollDirection) {
1590 return switch (scrollDirection) {
1591 Axis.horizontal => size.width,
1592 Axis.vertical => size.height,
1593 };
1594}
1595
1596Size _extentSize(double extent, Axis scrollDirection) {
1597 return switch (scrollDirection) {
1598 Axis.horizontal => Size(extent, 0),
1599 Axis.vertical => Size(0, extent),
1600 };
1601}
1602
1603double _offsetExtent(Offset offset, Axis scrollDirection) {
1604 return switch (scrollDirection) {
1605 Axis.horizontal => offset.dx,
1606 Axis.vertical => offset.dy,
1607 };
1608}
1609
1610Offset _extentOffset(double extent, Axis scrollDirection) {
1611 return switch (scrollDirection) {
1612 Axis.horizontal => Offset(extent, 0.0),
1613 Axis.vertical => Offset(0.0, extent),
1614 };
1615}
1616
1617Offset _restrictAxis(Offset offset, Axis scrollDirection) {
1618 return switch (scrollDirection) {
1619 Axis.horizontal => Offset(offset.dx, 0.0),
1620 Axis.vertical => Offset(0.0, offset.dy),
1621 };
1622}
1623
1624// A global key that takes its identity from the object and uses a value of a
1625// particular type to identify itself.
1626//
1627// The difference with GlobalObjectKey is that it uses [==] instead of [identical]
1628// of the objects used to generate widgets.
1629@optionalTypeArgs
1630class _ReorderableItemGlobalKey extends GlobalObjectKey {
1631 const _ReorderableItemGlobalKey(this.subKey, this.index, this.state) : super(subKey);
1632
1633 final Key subKey;
1634 final int index;
1635 final SliverReorderableListState state;
1636
1637 @override
1638 bool operator ==(Object other) {
1639 if (other.runtimeType != runtimeType) {
1640 return false;
1641 }
1642 return other is _ReorderableItemGlobalKey &&
1643 other.subKey == subKey &&
1644 other.index == index &&
1645 other.state == state;
1646 }
1647
1648 @override
1649 int get hashCode => Object.hash(subKey, index, state);
1650}
1651

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com