// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'basic.dart'; import 'debug.dart'; import 'framework.dart'; import 'inherited_theme.dart'; import 'overlay.dart'; import 'scroll_controller.dart'; import 'scroll_physics.dart'; import 'scroll_position.dart'; import 'scroll_view.dart'; import 'scrollable.dart'; import 'sliver.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; // Examples can assume: // class MyDataObject {} /// A callback used by [ReorderableList] to report that a list item has moved /// to a new position in the list. /// /// Implementations should remove the corresponding list item at [oldIndex] /// and reinsert it at [newIndex]. /// /// If [oldIndex] is before [newIndex], removing the item at [oldIndex] from the /// list will reduce the list's length by one. Implementations will need to /// account for this when inserting before [newIndex]. /// /// {@youtube 560 315 https://www.youtube.com/watch?v=3fB1mxOsqJE} /// /// {@tool snippet} /// /// ```dart /// final List<MyDataObject> backingList = <MyDataObject>[/* ... */]; /// /// void handleReorder(int oldIndex, int newIndex) { /// if (oldIndex < newIndex) { /// // removing the item at oldIndex will shorten the list by 1. /// newIndex -= 1; /// } /// final MyDataObject element = backingList.removeAt(oldIndex); /// backingList.insert(newIndex, element); /// } /// ``` /// {@end-tool} /// /// See also: /// /// * [ReorderableList], a widget list that allows the user to reorder /// its items. /// * [SliverReorderableList], a sliver list that allows the user to reorder /// its items. /// * [ReorderableListView], a material design list that allows the user to /// reorder its items. typedef ReorderCallback = void Function(int oldIndex, int newIndex); /// Signature for the builder callback used to decorate the dragging item in /// [ReorderableList] and [SliverReorderableList]. /// /// The [child] will be the item that is being dragged, and [index] is the /// position of the item in the list. /// /// The [animation] will be driven forward from 0.0 to 1.0 while the item is /// being picked up during a drag operation, and reversed from 1.0 to 0.0 when /// the item is dropped. This can be used to animate properties of the proxy /// like an elevation or border. /// /// The returned value will typically be the [child] wrapped in other widgets. typedef ReorderItemProxyDecorator = Widget Function(Widget child, int index, Animation<double> animation); /// A scrolling container that allows the user to interactively reorder the /// list items. /// /// This widget is similar to one created by [ListView.builder], and uses /// an [IndexedWidgetBuilder] to create each item. /// /// It is up to the application to wrap each child (or an internal part of the /// child such as a drag handle) with a drag listener that will recognize /// the start of an item drag and then start the reorder by calling /// [ReorderableListState.startItemDragReorder]. This is most easily achieved /// by wrapping each child in a [ReorderableDragStartListener] or a /// [ReorderableDelayedDragStartListener]. These will take care of recognizing /// the start of a drag gesture and call the list state's /// [ReorderableListState.startItemDragReorder] method. /// /// This widget's [ReorderableListState] can be used to manually start an item /// reorder, or cancel a current drag. To refer to the /// [ReorderableListState] either provide a [GlobalKey] or use the static /// [ReorderableList.of] method from an item's build method. /// /// See also: /// /// * [SliverReorderableList], a sliver list that allows the user to reorder /// its items. /// * [ReorderableListView], a material design list that allows the user to /// reorder its items. class ReorderableList extends StatefulWidget { /// Creates a scrolling container that allows the user to interactively /// reorder the list items. /// /// The [itemCount] must be greater than or equal to zero. const ReorderableList({ Key? key, required this.itemBuilder, required this.itemCount, required this.onReorder, this.proxyDecorator, this.padding, this.scrollDirection = Axis.vertical, this.reverse = false, this.controller, this.primary, this.physics, this.shrinkWrap = false, this.anchor = 0.0, this.cacheExtent, this.dragStartBehavior = DragStartBehavior.start, this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, this.clipBehavior = Clip.hardEdge, }) : assert(itemCount >= 0), super(key: key); /// {@template flutter.widgets.reorderable_list.itemBuilder} /// Called, as needed, to build list item widgets. /// /// List items are only built when they're scrolled into view. /// /// The [IndexedWidgetBuilder] index parameter indicates the item's /// position in the list. The value of the index parameter will be between /// zero and one less than [itemCount]. All items in the list must have a /// unique [Key], and should have some kind of listener to start the drag /// (usually a [ReorderableDragStartListener] or /// [ReorderableDelayedDragStartListener]). /// {@endtemplate} final IndexedWidgetBuilder itemBuilder; /// {@template flutter.widgets.reorderable_list.itemCount} /// The number of items in the list. /// /// It must be a non-negative integer. When zero, nothing is displayed and /// the widget occupies no space. /// {@endtemplate} final int itemCount; /// {@template flutter.widgets.reorderable_list.onReorder} /// A callback used by the list to report that a list item has been dragged /// to a new location in the list and the application should update the order /// of the items. /// {@endtemplate} final ReorderCallback onReorder; /// {@template flutter.widgets.reorderable_list.proxyDecorator} /// A callback that allows the app to add an animated decoration around /// an item when it is being dragged. /// {@endtemplate} final ReorderItemProxyDecorator? proxyDecorator; /// {@template flutter.widgets.reorderable_list.padding} /// The amount of space by which to inset the list contents. /// /// It defaults to `EdgeInsets.all(0)`. /// {@endtemplate} final EdgeInsetsGeometry? padding; /// {@macro flutter.widgets.scroll_view.scrollDirection} final Axis scrollDirection; /// {@macro flutter.widgets.scroll_view.reverse} final bool reverse; /// {@macro flutter.widgets.scroll_view.controller} final ScrollController? controller; /// {@macro flutter.widgets.scroll_view.primary} final bool? primary; /// {@macro flutter.widgets.scroll_view.physics} final ScrollPhysics? physics; /// {@macro flutter.widgets.scroll_view.shrinkWrap} final bool shrinkWrap; /// {@macro flutter.widgets.scroll_view.anchor} final double anchor; /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} final double? cacheExtent; /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} /// /// The default is [ScrollViewKeyboardDismissBehavior.manual] final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; /// {@macro flutter.material.Material.clipBehavior} /// /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; /// The state from the closest instance of this class that encloses the given /// context. /// /// This method is typically used by [ReorderableList] item widgets that /// insert or remove items in response to user input. /// /// If no [ReorderableList] surrounds the given context, then this function /// will assert in debug mode and throw an exception in release mode. /// /// See also: /// /// * [maybeOf], a similar function that will return null if no /// [ReorderableList] ancestor is found. static ReorderableListState of(BuildContext context) { assert(context != null); final ReorderableListState? result = context.findAncestorStateOfType<ReorderableListState>(); assert((){ if (result == null) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary( 'ReorderableList.of() called with a context that does not contain a ReorderableList.'), ErrorDescription( 'No ReorderableList ancestor could be found starting from the context that was passed to ReorderableList.of().'), ErrorHint( 'This can happen when the context provided is from the same StatefulWidget that ' 'built the ReorderableList. Please see the ReorderableList documentation for examples ' 'of how to refer to an ReorderableListState object:' ' https://api.flutter.dev/flutter/widgets/ReorderableListState-class.html' ), context.describeElement('The context used was') ]); } return true; }()); return result!; } /// The state from the closest instance of this class that encloses the given /// context. /// /// This method is typically used by [ReorderableList] item widgets that insert /// or remove items in response to user input. /// /// If no [ReorderableList] surrounds the context given, then this function will /// return null. /// /// See also: /// /// * [of], a similar function that will throw if no [ReorderableList] ancestor /// is found. static ReorderableListState? maybeOf(BuildContext context) { assert(context != null); return context.findAncestorStateOfType<ReorderableListState>(); } @override ReorderableListState createState() => ReorderableListState(); } /// The state for a list that allows the user to interactively reorder /// the list items. /// /// An app that needs to start a new item drag or cancel an existing one /// can refer to the [ReorderableList]'s state with a global key: /// /// ```dart /// GlobalKey<ReorderableListState> listKey = GlobalKey<ReorderableListState>(); /// ... /// ReorderableList(key: listKey, ...); /// ... /// listKey.currentState.cancelReorder(); /// ``` class ReorderableListState extends State<ReorderableList> { final GlobalKey<SliverReorderableListState> _sliverReorderableListKey = GlobalKey(); /// Initiate the dragging of the item at [index] that was started with /// the pointer down [event]. /// /// The given [recognizer] will be used to recognize and start the drag /// item tracking and lead to either an item reorder, or a cancelled drag. /// The list will take ownership of the returned recognizer and will dispose /// it when it is no longer needed. /// /// Most applications will not use this directly, but will wrap the item /// (or part of the item, like a drag handle) in either a /// [ReorderableDragStartListener] or [ReorderableDelayedDragStartListener] /// which call this for the application. void startItemDragReorder({ required int index, required PointerDownEvent event, required MultiDragGestureRecognizer<MultiDragPointerState> recognizer, }) { _sliverReorderableListKey.currentState!.startItemDragReorder(index: index, event: event, recognizer: recognizer); } /// Cancel any item drag in progress. /// /// This should be called before any major changes to the item list /// occur so that any item drags will not get confused by /// changes to the underlying list. /// /// If no drag is active, this will do nothing. void cancelReorder() { _sliverReorderableListKey.currentState!.cancelReorder(); } @override Widget build(BuildContext context) { return CustomScrollView( scrollDirection: widget.scrollDirection, reverse: widget.reverse, controller: widget.controller, primary: widget.primary, physics: widget.physics, shrinkWrap: widget.shrinkWrap, anchor: widget.anchor, cacheExtent: widget.cacheExtent, dragStartBehavior: widget.dragStartBehavior, keyboardDismissBehavior: widget.keyboardDismissBehavior, restorationId: widget.restorationId, clipBehavior: widget.clipBehavior, slivers: <Widget>[ SliverPadding( padding: widget.padding ?? EdgeInsets.zero, sliver: SliverReorderableList( key: _sliverReorderableListKey, itemBuilder: widget.itemBuilder, itemCount: widget.itemCount, onReorder: widget.onReorder, proxyDecorator: widget.proxyDecorator, ), ), ], ); } } /// A sliver list that allows the user to interactively reorder the list items. /// /// It is up to the application to wrap each child (or an internal part of the /// child) with a drag listener that will recognize the start of an item drag /// and then start the reorder by calling /// [SliverReorderableListState.startItemDragReorder]. This is most easily /// achieved by wrapping each child in a [ReorderableDragStartListener] or /// a [ReorderableDelayedDragStartListener]. These will take care of /// recognizing the start of a drag gesture and call the list state's start /// item drag method. /// /// This widget's [SliverReorderableListState] can be used to manually start an item /// reorder, or cancel a current drag that's already underway. To refer to the /// [SliverReorderableListState] either provide a [GlobalKey] or use the static /// [SliverReorderableList.of] method from an item's build method. /// /// See also: /// /// * [ReorderableList], a regular widget list that allows the user to reorder /// its items. /// * [ReorderableListView], a material design list that allows the user to /// reorder its items. class SliverReorderableList extends StatefulWidget { /// Creates a sliver list that allows the user to interactively reorder its /// items. /// /// The [itemCount] must be greater than or equal to zero. const SliverReorderableList({ Key? key, required this.itemBuilder, required this.itemCount, required this.onReorder, this.proxyDecorator, }) : assert(itemCount >= 0), super(key: key); /// {@macro flutter.widgets.reorderable_list.itemBuilder} final IndexedWidgetBuilder itemBuilder; /// {@macro flutter.widgets.reorderable_list.itemCount} final int itemCount; /// {@macro flutter.widgets.reorderable_list.onReorder} final ReorderCallback onReorder; /// {@macro flutter.widgets.reorderable_list.proxyDecorator} final ReorderItemProxyDecorator? proxyDecorator; @override SliverReorderableListState createState() => SliverReorderableListState(); /// The state from the closest instance of this class that encloses the given /// context. /// /// This method is typically used by [SliverReorderableList] item widgets to /// start or cancel an item drag operation. /// /// If no [SliverReorderableList] surrounds the context given, this function /// will assert in debug mode and throw an exception in release mode. /// /// See also: /// /// * [maybeOf], a similar function that will return null if no /// [SliverReorderableList] ancestor is found. static SliverReorderableListState of(BuildContext context) { assert(context != null); final SliverReorderableListState? result = context.findAncestorStateOfType<SliverReorderableListState>(); assert((){ if (result == null) { throw FlutterError.fromParts(<DiagnosticsNode>[ ErrorSummary( 'SliverReorderableList.of() called with a context that does not contain a SliverReorderableList.'), ErrorDescription( 'No SliverReorderableList ancestor could be found starting from the context that was passed to SliverReorderableList.of().'), ErrorHint( 'This can happen when the context provided is from the same StatefulWidget that ' 'built the SliverReorderableList. Please see the SliverReorderableList documentation for examples ' 'of how to refer to an SliverReorderableList object:' ' https://api.flutter.dev/flutter/widgets/SliverReorderableListState-class.html' ), context.describeElement('The context used was') ]); } return true; }()); return result!; } /// The state from the closest instance of this class that encloses the given /// context. /// /// This method is typically used by [SliverReorderableList] item widgets that /// insert or remove items in response to user input. /// /// If no [SliverReorderableList] surrounds the context given, this function /// will return null. /// /// See also: /// /// * [of], a similar function that will throw if no [SliverReorderableList] /// ancestor is found. static SliverReorderableListState? maybeOf(BuildContext context) { assert(context != null); return context.findAncestorStateOfType<SliverReorderableListState>(); } } /// The state for a sliver list that allows the user to interactively reorder /// the list items. /// /// An app that needs to start a new item drag or cancel an existing one /// can refer to the [SliverReorderableList]'s state with a global key: /// /// ```dart /// GlobalKey<SliverReorderableListState> listKey = GlobalKey<SliverReorderableListState>(); /// ... /// SliverReorderableList(key: listKey, ...); /// ... /// listKey.currentState.cancelReorder(); /// ``` /// /// [ReorderableDragStartListener] and [ReorderableDelayedDragStartListener] /// refer to their [SliverReorderableList] with the static /// [SliverReorderableList.of] method. class SliverReorderableListState extends State<SliverReorderableList> with TickerProviderStateMixin { // Map of index -> child state used manage where the dragging item will need // to be inserted. final Map<int, _ReorderableItemState> _items = <int, _ReorderableItemState>{}; OverlayEntry? _overlayEntry; int? _dragIndex; _DragInfo? _dragInfo; int? _insertIndex; Offset? _finalDropPosition; MultiDragGestureRecognizer<MultiDragPointerState>? _recognizer; bool _autoScrolling = false; late ScrollableState _scrollable; Axis get _scrollDirection => axisDirectionToAxis(_scrollable.axisDirection); bool get _reverse => _scrollable.axisDirection == AxisDirection.up || _scrollable.axisDirection == AxisDirection.left; @override void didChangeDependencies() { super.didChangeDependencies(); _scrollable = Scrollable.of(context)!; } @override void didUpdateWidget(covariant SliverReorderableList oldWidget) { super.didUpdateWidget(oldWidget); if (widget.itemCount != oldWidget.itemCount) { cancelReorder(); } } @override void dispose() { _dragInfo?.dispose(); super.dispose(); } /// Initiate the dragging of the item at [index] that was started with /// the pointer down [event]. /// /// The given [recognizer] will be used to recognize and start the drag /// item tracking and lead to either an item reorder, or a cancelled drag. /// /// Most applications will not use this directly, but will wrap the item /// (or part of the item, like a drag handle) in either a /// [ReorderableDragStartListener] or [ReorderableDelayedDragStartListener] /// which call this method when they detect the gesture that triggers a drag /// start. void startItemDragReorder({ required int index, required PointerDownEvent event, required MultiDragGestureRecognizer<MultiDragPointerState> recognizer, }) { assert(0 <= index && index < widget.itemCount); setState(() { if (_dragInfo != null) { cancelReorder(); } if (_items.containsKey(index)) { _dragIndex = index; _recognizer = recognizer ..onStart = _dragStart ..addPointer(event); } else { // TODO(darrenaustin): Can we handle this better, maybe scroll to the item? throw Exception('Attempting to start a drag on a non-visible item'); } }); } /// Cancel any item drag in progress. /// /// This should be called before any major changes to the item list /// occur so that any item drags will not get confused by /// changes to the underlying list. /// /// If a drag operation is in progress, this will immediately reset /// the list to back to its pre-drag state. /// /// If no drag is active, this will do nothing. void cancelReorder() { _dragReset(); } void _registerItem(_ReorderableItemState item) { _items[item.index] = item; if (item.index == _dragInfo?.index) { item.dragging = true; item.rebuild(); } } void _unregisterItem(int index, _ReorderableItemState item) { final _ReorderableItemState? currentItem = _items[index]; if (currentItem == item) { _items.remove(index); } } Drag? _dragStart(Offset position) { assert(_dragInfo == null); final _ReorderableItemState item = _items[_dragIndex!]!; item.dragging = true; item.rebuild(); _insertIndex = item.index; _dragInfo = _DragInfo( item: item, initialPosition: position, scrollDirection: _scrollDirection, onUpdate: _dragUpdate, onCancel: _dragCancel, onEnd: _dragEnd, onDropCompleted: _dropCompleted, proxyDecorator: widget.proxyDecorator, tickerProvider: this, ); _dragInfo!.startDrag(); final OverlayState overlay = Overlay.of(context)!; assert(_overlayEntry == null); _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy); overlay.insert(_overlayEntry!); for (final _ReorderableItemState childItem in _items.values) { if (childItem == item || !childItem.mounted) continue; childItem.updateForGap(_insertIndex!, _dragInfo!.itemExtent, false, _reverse); } return _dragInfo; } void _dragUpdate(_DragInfo item, Offset position, Offset delta) { setState(() { _overlayEntry?.markNeedsBuild(); _dragUpdateItems(); _autoScrollIfNecessary(); }); } void _dragCancel(_DragInfo item) { _dragReset(); } void _dragEnd(_DragInfo item) { setState(() { if (_insertIndex! < widget.itemCount - 1) { // Find the location of the item we want to insert before _finalDropPosition = _itemOffsetAt(_insertIndex!); } else { // Inserting into the last spot on the list. If it's the only spot, put // it back where it was. Otherwise, grab the second to last and move // down by the gap. final int itemIndex = _items.length > 1 ? _insertIndex! - 1 : _insertIndex!; if (_reverse) { _finalDropPosition = _itemOffsetAt(itemIndex) - _extentOffset(item.itemExtent, _scrollDirection); } else { _finalDropPosition = _itemOffsetAt(itemIndex) + _extentOffset(item.itemExtent, _scrollDirection); } } }); } void _dropCompleted() { final int fromIndex = _dragIndex!; final int toIndex = _insertIndex!; if (fromIndex != toIndex) { widget.onReorder.call(fromIndex, toIndex); } _dragReset(); } void _dragReset() { setState(() { if (_dragInfo != null) { if (_dragIndex != null && _items.containsKey(_dragIndex)) { final _ReorderableItemState dragItem = _items[_dragIndex!]!; dragItem._dragging = false; dragItem.rebuild(); _dragIndex = null; } _dragInfo?.dispose(); _dragInfo = null; _resetItemGap(); _recognizer?.dispose(); _recognizer = null; _overlayEntry?.remove(); _overlayEntry = null; _finalDropPosition = null; } }); } void _resetItemGap() { for (final _ReorderableItemState item in _items.values) { item.resetGap(); } } void _dragUpdateItems() { assert(_dragInfo != null); final double gapExtent = _dragInfo!.itemExtent; final double proxyItemStart = _offsetExtent(_dragInfo!.dragPosition - _dragInfo!.dragOffset, _scrollDirection); final double proxyItemEnd = proxyItemStart + gapExtent; // Find the new index for inserting the item being dragged. int newIndex = _insertIndex!; for (final _ReorderableItemState item in _items.values) { if (item.index == _dragIndex! || !item.mounted) continue; final Rect geometry = item.targetGeometry(); final double itemStart = _scrollDirection == Axis.vertical ? geometry.top : geometry.left; final double itemExtent = _scrollDirection == Axis.vertical ? geometry.height : geometry.width; final double itemEnd = itemStart + itemExtent; final double itemMiddle = itemStart + itemExtent / 2; if (_reverse) { if (itemEnd >= proxyItemEnd && proxyItemEnd >= itemMiddle) { // The start of the proxy is in the beginning half of the item, so // we should swap the item with the gap and we are done looking for // the new index. newIndex = item.index; break; } else if (itemMiddle >= proxyItemStart && proxyItemStart >= itemStart) { // The end of the proxy is in the ending half of the item, so // we should swap the item with the gap and we are done looking for // the new index. newIndex = item.index + 1; break; } else if (itemStart > proxyItemEnd && newIndex < (item.index + 1)) { newIndex = item.index + 1; } else if (proxyItemStart > itemEnd && newIndex > item.index) { newIndex = item.index; } } else { if (itemStart <= proxyItemStart && proxyItemStart <= itemMiddle) { // The start of the proxy is in the beginning half of the item, so // we should swap the item with the gap and we are done looking for // the new index. newIndex = item.index; break; } else if (itemMiddle <= proxyItemEnd && proxyItemEnd <= itemEnd) { // The end of the proxy is in the ending half of the item, so // we should swap the item with the gap and we are done looking for // the new index. newIndex = item.index + 1; break; } else if (itemEnd < proxyItemStart && newIndex < (item.index + 1)) { newIndex = item.index + 1; } else if (proxyItemEnd < itemStart && newIndex > item.index) { newIndex = item.index; } } } if (newIndex != _insertIndex) { _insertIndex = newIndex; for (final _ReorderableItemState item in _items.values) { if (item.index == _dragIndex! || !item.mounted) continue; item.updateForGap(newIndex, gapExtent, true, _reverse); } } } Future<void> _autoScrollIfNecessary() async { if (!_autoScrolling && _dragInfo != null && _dragInfo!.scrollable != null) { final ScrollPosition position = _dragInfo!.scrollable!.position; double? newOffset; const Duration duration = Duration(milliseconds: 14); const double step = 1.0; const double overDragMax = 20.0; const double overDragCoef = 10; final RenderBox scrollRenderBox = _dragInfo!.scrollable!.context.findRenderObject()! as RenderBox; final Offset scrollOrigin = scrollRenderBox.localToGlobal(Offset.zero); final double scrollStart = _offsetExtent(scrollOrigin, _scrollDirection); final double scrollEnd = scrollStart + _sizeExtent(scrollRenderBox.size, _scrollDirection); final double proxyStart = _offsetExtent(_dragInfo!.dragPosition - _dragInfo!.dragOffset, _scrollDirection); final double proxyEnd = proxyStart + _dragInfo!.itemExtent; if (_reverse) { if (proxyEnd > scrollEnd && position.pixels > position.minScrollExtent) { final double overDrag = max(proxyEnd - scrollEnd, overDragMax); newOffset = max(position.minScrollExtent, position.pixels - step * overDrag / overDragCoef); } else if (proxyStart < scrollStart && position.pixels < position.maxScrollExtent) { final double overDrag = max(scrollStart - proxyStart, overDragMax); newOffset = min(position.maxScrollExtent, position.pixels + step * overDrag / overDragCoef); } } else { if (proxyStart < scrollStart && position.pixels > position.minScrollExtent) { final double overDrag = max(scrollStart - proxyStart, overDragMax); newOffset = max(position.minScrollExtent, position.pixels - step * overDrag / overDragCoef); } else if (proxyEnd > scrollEnd && position.pixels < position.maxScrollExtent) { final double overDrag = max(proxyEnd - scrollEnd, overDragMax); newOffset = min(position.maxScrollExtent, position.pixels + step * overDrag / overDragCoef); } } if (newOffset != null && (newOffset - position.pixels).abs() >= 1.0) { _autoScrolling = true; await position.animateTo(newOffset, duration: duration, curve: Curves.linear ); _autoScrolling = false; if (_dragInfo != null) { _dragUpdateItems(); _autoScrollIfNecessary(); } } } } Offset _itemOffsetAt(int index) { final RenderBox itemRenderBox = _items[index]!.context.findRenderObject()! as RenderBox; return itemRenderBox.localToGlobal(Offset.zero); } Widget _itemBuilder(BuildContext context, int index) { if (_dragInfo != null && index >= widget.itemCount) { switch (_scrollDirection) { case Axis.horizontal: return SizedBox(width: _dragInfo!.itemExtent); case Axis.vertical: return SizedBox(height: _dragInfo!.itemExtent); } } final Widget child = widget.itemBuilder(context, index); assert(child.key != null, 'All list items must have a key'); final OverlayState overlay = Overlay.of(context)!; return _ReorderableItem( key: _ReorderableItemGlobalKey(child.key!, index, this), index: index, child: child, capturedThemes: InheritedTheme.capture(from: context, to: overlay.context), ); } @override Widget build(BuildContext context) { assert(debugCheckHasOverlay(context)); return SliverList( // When dragging, the dragged item is still in the list but has been replaced // by a zero height SizedBox, so that the gap can move around. To make the // list extent stable we add a dummy entry to the end. delegate: SliverChildBuilderDelegate(_itemBuilder, childCount: widget.itemCount + (_dragInfo != null ? 1 : 0)), ); } } class _ReorderableItem extends StatefulWidget { const _ReorderableItem({ required Key key, required this.index, required this.child, required this.capturedThemes, }) : super(key: key); final int index; final Widget child; final CapturedThemes capturedThemes; @override _ReorderableItemState createState() => _ReorderableItemState(); } class _ReorderableItemState extends State<_ReorderableItem> { late SliverReorderableListState _listState; Offset _startOffset = Offset.zero; Offset _targetOffset = Offset.zero; AnimationController? _offsetAnimation; Key get key => widget.key!; int get index => widget.index; bool get dragging => _dragging; set dragging(bool dragging) { if (mounted) { setState(() { _dragging = dragging; }); } } bool _dragging = false; @override void initState() { _listState = SliverReorderableList.of(context); _listState._registerItem(this); super.initState(); } @override void dispose() { _offsetAnimation?.dispose(); _listState._unregisterItem(index, this); super.dispose(); } @override void didUpdateWidget(covariant _ReorderableItem oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.index != widget.index) { _listState._unregisterItem(oldWidget.index, this); _listState._registerItem(this); } } @override Widget build(BuildContext context) { if (_dragging) { return const SizedBox(); } _listState._registerItem(this); return Transform( transform: Matrix4.translationValues(offset.dx, offset.dy, 0.0), child: widget.child, ); } @override void deactivate() { _listState._unregisterItem(index, this); super.deactivate(); } Offset get offset { if (_offsetAnimation != null) { final double animValue = Curves.easeInOut.transform(_offsetAnimation!.value); return Offset.lerp(_startOffset, _targetOffset, animValue)!; } return _targetOffset; } void updateForGap(int gapIndex, double gapExtent, bool animate, bool reverse) { final Offset newTargetOffset = (gapIndex <= index) ? _extentOffset(reverse ? -gapExtent : gapExtent, _listState._scrollDirection) : Offset.zero; if (newTargetOffset != _targetOffset) { _targetOffset = newTargetOffset; if (animate) { if (_offsetAnimation == null) { _offsetAnimation = AnimationController( vsync: _listState, duration: const Duration(milliseconds: 250), ) ..addListener(rebuild) ..addStatusListener((AnimationStatus status) { if (status == AnimationStatus.completed) { _startOffset = _targetOffset; _offsetAnimation!.dispose(); _offsetAnimation = null; } }) ..forward(); } else { _startOffset = offset; _offsetAnimation!.forward(from: 0.0); } } else { if (_offsetAnimation != null) { _offsetAnimation!.dispose(); _offsetAnimation = null; } _startOffset = _targetOffset; } rebuild(); } } void resetGap() { if (_offsetAnimation != null) { _offsetAnimation!.dispose(); _offsetAnimation = null; } _startOffset = Offset.zero; _targetOffset = Offset.zero; rebuild(); } Rect targetGeometry() { final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero) + _targetOffset; return itemPosition & itemRenderBox.size; } void rebuild() { if (mounted) { setState(() {}); } } } /// A wrapper widget that will recognize the start of a drag on the wrapped /// widget by a [PointerDownEvent], and immediately initiate dragging the /// wrapped item to a new location in a reorderable list. /// /// See also: /// /// * [ReorderableDelayedDragStartListener], a similar wrapper that will /// only recognize the start after a long press event. /// * [ReorderableList], a widget list that allows the user to reorder /// its items. /// * [SliverReorderableList], a sliver list that allows the user to reorder /// its items. /// * [ReorderableListView], a material design list that allows the user to /// reorder its items. class ReorderableDragStartListener extends StatelessWidget { /// Creates a listener for a drag immediately following a pointer down /// event over the given child widget. /// /// This is most commonly used to wrap part of a list item like a drag /// handle. const ReorderableDragStartListener({ Key? key, required this.child, required this.index, }) : super(key: key); /// The widget for which the application would like to respond to a tap and /// drag gesture by starting a reordering drag on a reorderable list. final Widget child; /// The index of the associated item that will be dragged in the list. final int index; @override Widget build(BuildContext context) { return Listener( onPointerDown: (PointerDownEvent event) => _startDragging(context, event), child: child, ); } /// Provides the gesture recognizer used to indicate the start of a reordering /// drag operation. /// /// By default this returns an [ImmediateMultiDragGestureRecognizer] but /// subclasses can use this to customize the drag start gesture. @protected MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer() { return ImmediateMultiDragGestureRecognizer(debugOwner: this); } void _startDragging(BuildContext context, PointerDownEvent event) { final SliverReorderableListState? list = SliverReorderableList.maybeOf(context); list?.startItemDragReorder( index: index, event: event, recognizer: createRecognizer() ); } } /// A wrapper widget that will recognize the start of a drag operation by /// looking for a long press event. Once it is recognized, it will start /// a drag operation on the wrapped item in the reorderable list. /// /// See also: /// /// * [ReorderableDragStartListener], a similar wrapper that will /// recognize the start of the drag immediately after a pointer down event. /// * [ReorderableList], a widget list that allows the user to reorder /// its items. /// * [SliverReorderableList], a sliver list that allows the user to reorder /// its items. /// * [ReorderableListView], a material design list that allows the user to /// reorder its items. class ReorderableDelayedDragStartListener extends ReorderableDragStartListener { /// Creates a listener for an drag following a long press event over the /// given child widget. /// /// This is most commonly used to wrap an entire list item in a reorderable /// list. const ReorderableDelayedDragStartListener({ Key? key, required Widget child, required int index, }) : super(key: key, child: child, index: index); @override MultiDragGestureRecognizer<MultiDragPointerState> createRecognizer() { return DelayedMultiDragGestureRecognizer(debugOwner: this); } } typedef _DragItemUpdate = void Function(_DragInfo item, Offset position, Offset delta); typedef _DragItemCallback = void Function(_DragInfo item); class _DragInfo extends Drag { _DragInfo({ required _ReorderableItemState item, Offset initialPosition = Offset.zero, this.scrollDirection = Axis.vertical, this.onUpdate, this.onEnd, this.onCancel, this.onDropCompleted, this.proxyDecorator, required this.tickerProvider, }) { final RenderBox itemRenderBox = item.context.findRenderObject()! as RenderBox; listState = item._listState; index = item.index; child = item.widget.child; capturedThemes = item.widget.capturedThemes; dragPosition = initialPosition; dragOffset = itemRenderBox.globalToLocal(initialPosition); itemSize = item.context.size!; itemExtent = _sizeExtent(itemSize, scrollDirection); scrollable = Scrollable.of(item.context); } final Axis scrollDirection; final _DragItemUpdate? onUpdate; final _DragItemCallback? onEnd; final _DragItemCallback? onCancel; final VoidCallback? onDropCompleted; final ReorderItemProxyDecorator? proxyDecorator; final TickerProvider tickerProvider; late SliverReorderableListState listState; late int index; late Widget child; late Offset dragPosition; late Offset dragOffset; late Size itemSize; late double itemExtent; late CapturedThemes capturedThemes; ScrollableState? scrollable; AnimationController? _proxyAnimation; void dispose() { _proxyAnimation?.dispose(); } void startDrag() { _proxyAnimation = AnimationController( vsync: tickerProvider, duration: const Duration(milliseconds: 250), ) ..addStatusListener((AnimationStatus status) { if (status == AnimationStatus.dismissed) { _dropCompleted(); } }) ..forward(); } @override void update(DragUpdateDetails details) { final Offset delta = _restrictAxis(details.delta, scrollDirection); dragPosition += delta; onUpdate?.call(this, dragPosition, details.delta); } @override void end(DragEndDetails details) { _proxyAnimation!.reverse(); onEnd?.call(this); } @override void cancel() { _proxyAnimation?.dispose(); _proxyAnimation = null; onCancel?.call(this); } void _dropCompleted() { _proxyAnimation?.dispose(); _proxyAnimation = null; onDropCompleted?.call(); } Widget createProxy(BuildContext context) { return capturedThemes.wrap( _DragItemProxy( listState: listState, index: index, child: child, size: itemSize, animation: _proxyAnimation!, position: dragPosition - dragOffset - _overlayOrigin(context), proxyDecorator: proxyDecorator, ), ); } } Offset _overlayOrigin(BuildContext context) { final OverlayState overlay = Overlay.of(context)!; final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox; return overlayBox.localToGlobal(Offset.zero); } class _DragItemProxy extends StatelessWidget { const _DragItemProxy({ Key? key, required this.listState, required this.index, required this.child, required this.position, required this.size, required this.animation, required this.proxyDecorator, }) : super(key: key); final SliverReorderableListState listState; final int index; final Widget child; final Offset position; final Size size; final AnimationController animation; final ReorderItemProxyDecorator? proxyDecorator; @override Widget build(BuildContext context) { final Widget proxyChild = proxyDecorator?.call(child, index, animation.view) ?? child; final Offset overlayOrigin = _overlayOrigin(context); return AnimatedBuilder( animation: animation, builder: (BuildContext context, Widget? child) { Offset effectivePosition = position; final Offset? dropPosition = listState._finalDropPosition; if (dropPosition != null) { effectivePosition = Offset.lerp(dropPosition - overlayOrigin, effectivePosition, Curves.easeOut.transform(animation.value))!; } return Positioned( child: SizedBox( width: size.width, height: size.height, child: child, ), left: effectivePosition.dx, top: effectivePosition.dy, ); }, child: proxyChild, ); } } double _sizeExtent(Size size, Axis scrollDirection) { switch (scrollDirection) { case Axis.horizontal: return size.width; case Axis.vertical: return size.height; } } double _offsetExtent(Offset offset, Axis scrollDirection) { switch (scrollDirection) { case Axis.horizontal: return offset.dx; case Axis.vertical: return offset.dy; } } Offset _extentOffset(double extent, Axis scrollDirection) { switch (scrollDirection) { case Axis.horizontal: return Offset(extent, 0.0); case Axis.vertical: return Offset(0.0, extent); } } Offset _restrictAxis(Offset offset, Axis scrollDirection) { switch (scrollDirection) { case Axis.horizontal: return Offset(offset.dx, 0.0); case Axis.vertical: return Offset(0.0, offset.dy); } } // A global key that takes its identity from the object and uses a value of a // particular type to identify itself. // // The difference with GlobalObjectKey is that it uses [==] instead of [identical] // of the objects used to generate widgets. @optionalTypeArgs class _ReorderableItemGlobalKey extends GlobalObjectKey { const _ReorderableItemGlobalKey(this.subKey, this.index, this.state) : super(subKey); final Key subKey; final int index; final SliverReorderableListState state; @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; return other is _ReorderableItemGlobalKey && other.subKey == subKey && other.index == index && other.state == state; } @override int get hashCode => hashValues(subKey, index, state); }