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'; |
6 | library; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/gestures.dart'; |
10 | import 'package:flutter/rendering.dart'; |
11 | import 'package:flutter/scheduler.dart'; |
12 | |
13 | import 'basic.dart'; |
14 | import 'debug.dart'; |
15 | import 'drag_boundary.dart'; |
16 | import 'framework.dart'; |
17 | import 'inherited_theme.dart'; |
18 | import 'localizations.dart'; |
19 | import 'media_query.dart'; |
20 | import 'overlay.dart'; |
21 | import 'scroll_controller.dart'; |
22 | import 'scroll_delegate.dart'; |
23 | import 'scroll_physics.dart'; |
24 | import 'scroll_view.dart'; |
25 | import 'scrollable.dart'; |
26 | import 'scrollable_helpers.dart'; |
27 | import 'sliver.dart'; |
28 | import 'sliver_prototype_extent_list.dart'; |
29 | import 'ticker_provider.dart'; |
30 | import '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. |
71 | typedef 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. |
85 | typedef 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. |
117 | typedef 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. |
145 | class 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 | /// ``` |
402 | class 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. |
501 | class 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. |
670 | class 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 | |
1129 | class _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 | |
1145 | class _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. |
1298 | class 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. |
1367 | class 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 | |
1386 | typedef _DragItemUpdate = void Function(_DragInfo item, Offset position, Offset delta); |
1387 | typedef _DragItemCallback = void Function(_DragInfo item); |
1388 | |
1389 | class _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 | |
1514 | Offset _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 | |
1520 | class _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 | |
1589 | double _sizeExtent(Size size, Axis scrollDirection) { |
1590 | return switch (scrollDirection) { |
1591 | Axis.horizontal => size.width, |
1592 | Axis.vertical => size.height, |
1593 | }; |
1594 | } |
1595 | |
1596 | Size _extentSize(double extent, Axis scrollDirection) { |
1597 | return switch (scrollDirection) { |
1598 | Axis.horizontal => Size(extent, 0), |
1599 | Axis.vertical => Size(0, extent), |
1600 | }; |
1601 | } |
1602 | |
1603 | double _offsetExtent(Offset offset, Axis scrollDirection) { |
1604 | return switch (scrollDirection) { |
1605 | Axis.horizontal => offset.dx, |
1606 | Axis.vertical => offset.dy, |
1607 | }; |
1608 | } |
1609 | |
1610 | Offset _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 | |
1617 | Offset _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 |
1630 | class _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 |
Definitions
- ReorderableList
- ReorderableList
- of
- maybeOf
- createState
- ReorderableListState
- startItemDragReorder
- cancelReorder
- build
- SliverReorderableList
- SliverReorderableList
- createState
- of
- maybeOf
- SliverReorderableListState
- _scrollDirection
- _reverse
- didChangeDependencies
- didUpdateWidget
- dispose
- startItemDragReorder
- cancelReorder
- _registerItem
- _unregisterItem
- _dragStart
- _dragUpdate
- _dragCancel
- _dragEnd
- _dropCompleted
- _dragReset
- _resetItemGap
- _handleScrollableAutoScrolled
- _dragUpdateItems
- _dragTargetRect
- _itemOffsetAt
- _itemExtentAt
- _itemBuilder
- _wrapWithSemantics
- reorder
- moveToStart
- moveToEnd
- moveBefore
- moveAfter
- build
- _ReorderableItem
- _ReorderableItem
- createState
- _ReorderableItemState
- key
- index
- dragging
- dragging
- initState
- dispose
- didUpdateWidget
- build
- deactivate
- offset
- updateForGap
- resetGap
- targetGeometry
- rebuild
- ReorderableDragStartListener
- ReorderableDragStartListener
- build
- createRecognizer
- _startDragging
- ReorderableDelayedDragStartListener
- ReorderableDelayedDragStartListener
- createRecognizer
- _DragInfo
- _DragInfo
- dispose
- startDrag
- update
- end
- cancel
- _adjustedDragOffset
- _dropCompleted
- createProxy
- _overlayOrigin
- _DragItemProxy
- _DragItemProxy
- build
- _sizeExtent
- _extentSize
- _offsetExtent
- _extentOffset
- _restrictAxis
- _ReorderableItemGlobalKey
- _ReorderableItemGlobalKey
- ==
Learn more about Flutter for embedded and desktop on industrialflutter.com