| // 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 'package:flutter/gestures.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| |
| import 'basic.dart'; |
| import 'debug.dart'; |
| import 'framework.dart'; |
| import 'inherited_theme.dart'; |
| import 'media_query.dart'; |
| import 'overlay.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_physics.dart'; |
| import 'scroll_view.dart'; |
| import 'scrollable.dart'; |
| import 'sliver.dart'; |
| import 'sliver_prototype_extent_list.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({ |
| super.key, |
| required this.itemBuilder, |
| required this.itemCount, |
| required this.onReorder, |
| this.onReorderStart, |
| this.onReorderEnd, |
| this.itemExtent, |
| this.prototypeItem, |
| 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), |
| assert( |
| itemExtent == null || prototypeItem == null, |
| 'You can only pass itemExtent or prototypeItem, not both', |
| ); |
| |
| /// {@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.onReorderStart} |
| /// A callback that is called when an item drag has started. |
| /// |
| /// The index parameter of the callback is the index of the selected item. |
| /// |
| /// See also: |
| /// |
| /// * [onReorderEnd], which is a called when the dragged item is dropped. |
| /// * [onReorder], which reports that a list item has been dragged to a new |
| /// location. |
| /// {@endtemplate} |
| final void Function(int index)? onReorderStart; |
| |
| /// {@template flutter.widgets.reorderable_list.onReorderEnd} |
| /// A callback that is called when the dragged item is dropped. |
| /// |
| /// The index parameter of the callback is the index where the item is |
| /// dropped. Unlike [onReorder], this is called even when the list item is |
| /// dropped in the same location. |
| /// |
| /// See also: |
| /// |
| /// * [onReorderStart], which is a called when an item drag has started. |
| /// * [onReorder], which reports that a list item has been dragged to a new |
| /// location. |
| /// {@endtemplate} |
| final void Function(int index)? onReorderEnd; |
| |
| /// {@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; |
| |
| /// {@macro flutter.widgets.list_view.itemExtent} |
| final double? itemExtent; |
| |
| /// {@macro flutter.widgets.list_view.prototypeItem} |
| final Widget? prototypeItem; |
| |
| /// 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. |
| /// |
| /// This method can be expensive (it walks the element tree). |
| /// |
| /// 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:\n' |
| ' 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. |
| /// |
| /// This method can be expensive (it walks the element tree). |
| /// |
| /// 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>(); |
| /// // ... |
| /// Widget build(BuildContext context) { |
| /// return ReorderableList( |
| /// key: listKey, |
| /// itemBuilder: (BuildContext context, int index) => const SizedBox(height: 10.0), |
| /// itemCount: 5, |
| /// onReorder: (int oldIndex, int newIndex) { |
| /// // ... |
| /// }, |
| /// ); |
| /// } |
| /// // ... |
| /// 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 canceled 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 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, |
| itemExtent: widget.itemExtent, |
| prototypeItem: widget.prototypeItem, |
| itemBuilder: widget.itemBuilder, |
| itemCount: widget.itemCount, |
| onReorder: widget.onReorder, |
| onReorderStart: widget.onReorderStart, |
| onReorderEnd: widget.onReorderEnd, |
| 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({ |
| super.key, |
| required this.itemBuilder, |
| this.findChildIndexCallback, |
| required this.itemCount, |
| required this.onReorder, |
| this.onReorderStart, |
| this.onReorderEnd, |
| this.itemExtent, |
| this.prototypeItem, |
| this.proxyDecorator, |
| }) : assert(itemCount >= 0), |
| assert( |
| itemExtent == null || prototypeItem == null, |
| 'You can only pass itemExtent or prototypeItem, not both', |
| ); |
| |
| /// {@macro flutter.widgets.reorderable_list.itemBuilder} |
| final IndexedWidgetBuilder itemBuilder; |
| |
| /// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback} |
| final ChildIndexGetter? findChildIndexCallback; |
| |
| /// {@macro flutter.widgets.reorderable_list.itemCount} |
| final int itemCount; |
| |
| /// {@macro flutter.widgets.reorderable_list.onReorder} |
| final ReorderCallback onReorder; |
| |
| /// {@macro flutter.widgets.reorderable_list.onReorderStart} |
| final void Function(int)? onReorderStart; |
| |
| /// {@macro flutter.widgets.reorderable_list.onReorderEnd} |
| final void Function(int)? onReorderEnd; |
| |
| /// {@macro flutter.widgets.reorderable_list.proxyDecorator} |
| final ReorderItemProxyDecorator? proxyDecorator; |
| |
| /// {@macro flutter.widgets.list_view.itemExtent} |
| final double? itemExtent; |
| |
| /// {@macro flutter.widgets.list_view.prototypeItem} |
| final Widget? prototypeItem; |
| |
| @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. |
| /// |
| /// This method can be expensive (it walks the element tree). |
| /// |
| /// 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:\n' |
| ' 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. |
| /// |
| /// This method can be expensive (it walks the element tree). |
| /// |
| /// 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 |
| /// // (e.g. in a stateful widget) |
| /// GlobalKey<SliverReorderableListState> listKey = GlobalKey<SliverReorderableListState>(); |
| /// |
| /// // ... |
| /// |
| /// @override |
| /// Widget build(BuildContext context) { |
| /// return SliverReorderableList( |
| /// key: listKey, |
| /// itemBuilder: (BuildContext context, int index) => const SizedBox(height: 10.0), |
| /// itemCount: 5, |
| /// onReorder: (int oldIndex, int newIndex) { |
| /// // ... |
| /// }, |
| /// ); |
| /// } |
| /// |
| /// // ... |
| /// |
| /// void _stop() { |
| /// 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? _recognizer; |
| int? _recognizerPointer; |
| // To implement the gap for the dragged item, we replace the dragged item |
| // with a zero sized box, and then translate all of the later items down |
| // by the size of the dragged item. This allows us to keep the order of the |
| // list, while still being able to animate the gap between the items. However |
| // for the first frame of the drag, the item has not yet been replaced, so |
| // the calculation for the gap is off by the size of the gap. This flag is |
| // used to determine if the transition to the zero sized box has completed, |
| // so the gap calculation can compensate for it. |
| bool _dragStartTransitionComplete = false; |
| |
| EdgeDraggingAutoScroller? _autoScroller; |
| |
| 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)!; |
| if (_autoScroller?.scrollable != _scrollable) { |
| _autoScroller?.stopAutoScroll(); |
| _autoScroller = EdgeDraggingAutoScroller( |
| _scrollable, |
| onScrollViewScrolled: _handleScrollableAutoScrolled |
| ); |
| } |
| } |
| |
| @override |
| void didUpdateWidget(covariant SliverReorderableList oldWidget) { |
| super.didUpdateWidget(oldWidget); |
| if (widget.itemCount != oldWidget.itemCount) { |
| cancelReorder(); |
| } |
| } |
| |
| @override |
| void dispose() { |
| _dragReset(); |
| 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 canceled 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 recognizer, |
| }) { |
| assert(0 <= index && index < widget.itemCount); |
| setState(() { |
| if (_dragInfo != null) { |
| cancelReorder(); |
| } else if (_recognizer != null && _recognizerPointer != event.pointer) { |
| _recognizer!.dispose(); |
| _recognizer = null; |
| _recognizerPointer = null; |
| } |
| |
| if (_items.containsKey(index)) { |
| _dragIndex = index; |
| _recognizer = recognizer |
| ..onStart = _dragStart |
| ..addPointer(event); |
| _recognizerPointer = event.pointer; |
| } 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() { |
| setState(() { |
| _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; |
| widget.onReorderStart?.call(_dragIndex!); |
| item.rebuild(); |
| _dragStartTransitionComplete = false; |
| SchedulerBinding.instance.addPostFrameCallback((Duration duration) { |
| _dragStartTransitionComplete = true; |
| }); |
| |
| _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, debugRequiredFor: widget); |
| 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(); |
| _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect); |
| }); |
| } |
| |
| void _dragCancel(_DragInfo item) { |
| setState(() { |
| _dragReset(); |
| }); |
| } |
| |
| void _dragEnd(_DragInfo item) { |
| setState(() { |
| if (_insertIndex == item.index) { |
| _finalDropPosition = _itemOffsetAt(_insertIndex! + (_reverse ? 1 : 0)); |
| } else 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); |
| } |
| } |
| }); |
| widget.onReorderEnd?.call(_insertIndex!); |
| } |
| |
| void _dropCompleted() { |
| final int fromIndex = _dragIndex!; |
| final int toIndex = _insertIndex!; |
| if (fromIndex != toIndex) { |
| widget.onReorder.call(fromIndex, toIndex); |
| } |
| setState(() { |
| _dragReset(); |
| }); |
| } |
| |
| void _dragReset() { |
| 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; |
| _autoScroller?.stopAutoScroll(); |
| _resetItemGap(); |
| _recognizer?.dispose(); |
| _recognizer = null; |
| _overlayEntry?.remove(); |
| _overlayEntry = null; |
| _finalDropPosition = null; |
| } |
| } |
| |
| void _resetItemGap() { |
| for (final _ReorderableItemState item in _items.values) { |
| item.resetGap(); |
| } |
| } |
| |
| void _handleScrollableAutoScrolled() { |
| if (_dragInfo == null) { |
| return; |
| } |
| _dragUpdateItems(); |
| // Continue scrolling if the drag is still in progress. |
| _autoScroller?.startAutoScrollIfNecessary(_dragTargetRect); |
| } |
| |
| 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; |
| } |
| |
| Rect geometry = item.targetGeometry(); |
| if (!_dragStartTransitionComplete && _dragIndex! <= item.index) { |
| // Transition is not complete, so each item after the dragged item is still |
| // in its normal location and not moved up for the zero sized box that will |
| // replace the dragged item. |
| final Offset transitionOffset = _extentOffset(_reverse ? -gapExtent : gapExtent, _scrollDirection); |
| geometry = (geometry.topLeft - transitionOffset) & geometry.size; |
| } |
| 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); |
| } |
| } |
| } |
| |
| Rect get _dragTargetRect { |
| final Offset origin = _dragInfo!.dragPosition - _dragInfo!.dragOffset; |
| return Rect.fromLTWH(origin.dx, origin.dy, _dragInfo!.itemSize.width, _dragInfo!.itemSize.height); |
| } |
| |
| 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, debugRequiredFor: widget); |
| return _ReorderableItem( |
| key: _ReorderableItemGlobalKey(child.key!, index, this), |
| index: index, |
| capturedThemes: InheritedTheme.capture(from: context, to: overlay.context), |
| child: child, |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| assert(debugCheckHasOverlay(context)); |
| final SliverChildBuilderDelegate childrenDelegate = SliverChildBuilderDelegate( |
| _itemBuilder, |
| // 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. |
| childCount: widget.itemCount + (_dragInfo != null ? 1 : 0), |
| findChildIndexCallback: widget.findChildIndexCallback, |
| ); |
| if (widget.itemExtent != null) { |
| return SliverFixedExtentList( |
| delegate: childrenDelegate, |
| itemExtent: widget.itemExtent!, |
| ); |
| } else if (widget.prototypeItem != null) { |
| return SliverPrototypeExtentList( |
| delegate: childrenDelegate, |
| prototypeItem: widget.prototypeItem!, |
| ); |
| } |
| return SliverList(delegate: childrenDelegate); |
| } |
| } |
| |
| 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({ |
| super.key, |
| required this.child, |
| required this.index, |
| this.enabled = true, |
| }); |
| |
| /// 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; |
| |
| /// Whether the [child] item can be dragged and moved in the list. |
| /// |
| /// If true, the item can be moved to another location in the list when the |
| /// user taps on the child. If false, tapping on the child will be ignored. |
| final bool enabled; |
| |
| @override |
| Widget build(BuildContext context) { |
| return Listener( |
| onPointerDown: enabled ? (PointerDownEvent event) => _startDragging(context, event) : null, |
| 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 createRecognizer() { |
| return ImmediateMultiDragGestureRecognizer(debugOwner: this); |
| } |
| |
| void _startDragging(BuildContext context, PointerDownEvent event) { |
| final DeviceGestureSettings? gestureSettings = MediaQuery.maybeOf(context)?.gestureSettings; |
| final SliverReorderableListState? list = SliverReorderableList.maybeOf(context); |
| list?.startItemDragReorder( |
| index: index, |
| event: event, |
| recognizer: createRecognizer() |
| ..gestureSettings = gestureSettings, |
| ); |
| } |
| } |
| |
| /// 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({ |
| super.key, |
| required super.child, |
| required super.index, |
| super.enabled, |
| }); |
| |
| @override |
| MultiDragGestureRecognizer 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, |
| size: itemSize, |
| animation: _proxyAnimation!, |
| position: dragPosition - dragOffset - _overlayOrigin(context), |
| proxyDecorator: proxyDecorator, |
| child: child, |
| ), |
| ); |
| } |
| } |
| |
| Offset _overlayOrigin(BuildContext context) { |
| final OverlayState overlay = Overlay.of(context, debugRequiredFor: context.widget); |
| final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox; |
| return overlayBox.localToGlobal(Offset.zero); |
| } |
| |
| class _DragItemProxy extends StatelessWidget { |
| const _DragItemProxy({ |
| required this.listState, |
| required this.index, |
| required this.child, |
| required this.position, |
| required this.size, |
| required this.animation, |
| required this.proxyDecorator, |
| }); |
| |
| 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 MediaQuery( |
| // Remove the top padding so that any nested list views in the item |
| // won't pick up the scaffold's padding in the overlay. |
| data: MediaQuery.of(context).removePadding(removeTop: true), |
| child: 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( |
| left: effectivePosition.dx, |
| top: effectivePosition.dy, |
| child: SizedBox( |
| width: size.width, |
| height: size.height, |
| child: child, |
| ), |
| ); |
| }, |
| 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 => Object.hash(subKey, index, state); |
| } |