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