diff --git a/examples/android/app/build.gradle b/examples/android/app/build.gradle index 18f276f..5c2bd9a 100644 --- a/examples/android/app/build.gradle +++ b/examples/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 30 + compileSdkVersion 32 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -45,7 +45,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.examples" minSdkVersion 16 - targetSdkVersion 30 + targetSdkVersion 32 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/examples/android/app/src/main/AndroidManifest.xml b/examples/android/app/src/main/AndroidManifest.xml index 7234537..8a1de3c 100644 --- a/examples/android/app/src/main/AndroidManifest.xml +++ b/examples/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ - - diff --git a/examples/android/build.gradle b/examples/android/build.gradle index ed45c65..09fbd64 100644 --- a/examples/android/build.gradle +++ b/examples/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() diff --git a/examples/lib/examples/reorderable_list.dart b/examples/lib/examples/reorderable_list.dart new file mode 100644 index 0000000..5d21d94 --- /dev/null +++ b/examples/lib/examples/reorderable_list.dart @@ -0,0 +1,53 @@ +import 'package:examples/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; + +class ReorderablePage extends StatelessWidget { + ReorderablePage({ + Key? key, + }) : super(key: key); + + final List widgets = [ + const StaggeredGridTile.count( + crossAxisCellCount: 2, + mainAxisCellCount: 2, + child: Tile(index: 0), + ), + const StaggeredGridTile.count( + crossAxisCellCount: 2, + mainAxisCellCount: 1, + child: Tile(index: 1), + ), + const StaggeredGridTile.count( + crossAxisCellCount: 1, + mainAxisCellCount: 1, + child: Tile(index: 2), + ), + const StaggeredGridTile.count( + crossAxisCellCount: 1, + mainAxisCellCount: 1, + child: Tile(index: 3), + ), + const StaggeredGridTile.count( + crossAxisCellCount: 4, + mainAxisCellCount: 2, + child: Tile(index: 4), + ), + ]; + + @override + Widget build(BuildContext context) { + return AppScaffold( + title: 'Reorderable', + child: ReorderableStaggeredLayout( + crossAxisCount: 4, + mainAxisSpacing: 4, + crossAxisSpacing: 4, + children: widgets, + onReorder: (int oldIndex, int newIndex) { + widgets.insert(newIndex, widgets.removeAt(oldIndex)); + }, + ), + ); + } +} diff --git a/examples/lib/main.dart b/examples/lib/main.dart index 18675b2..ddad669 100644 --- a/examples/lib/main.dart +++ b/examples/lib/main.dart @@ -1,4 +1,5 @@ import 'package:examples/common.dart'; +import 'package:examples/examples/reorderable_list.dart'; import 'package:examples/pages/aligned.dart'; import 'package:examples/pages/masonry.dart'; import 'package:examples/pages/quilted.dart'; @@ -41,37 +42,42 @@ class HomePage extends StatelessWidget { mainAxisSpacing: 16, crossAxisSpacing: 16, ), - children: const [ - MenuEntry( + children: [ + const MenuEntry( title: 'Staggered', imageName: 'staggered', destination: StaggeredPage(), ), - MenuEntry( + const MenuEntry( title: 'Masonry', imageName: 'masonry', destination: MasonryPage(), ), - MenuEntry( + const MenuEntry( title: 'Quilted', imageName: 'quilted', destination: QuiltedPage(), ), - MenuEntry( + const MenuEntry( title: 'Woven', imageName: 'woven', destination: WovenPage(), ), - MenuEntry( + const MenuEntry( title: 'Staired', imageName: 'staired', destination: StairedPage(), ), - MenuEntry( + const MenuEntry( title: 'Aligned', imageName: 'aligned', destination: AlignedPage(), ), + MenuEntry( + title: 'Reorderable', + imageName: 'quilted', + destination: ReorderablePage(), + ), ], ), ); diff --git a/examples/lib/main_examples.dart b/examples/lib/main_examples.dart index 5cfb2d7..51b9d73 100644 --- a/examples/lib/main_examples.dart +++ b/examples/lib/main_examples.dart @@ -2,6 +2,7 @@ import 'package:examples/common.dart'; import 'package:examples/examples/aligned.dart'; import 'package:examples/examples/masonry.dart'; import 'package:examples/examples/quilted.dart'; +import 'package:examples/examples/reorderable_list.dart'; import 'package:examples/examples/staggered.dart'; import 'package:examples/examples/staired.dart'; import 'package:examples/examples/woven.dart'; @@ -41,37 +42,42 @@ class HomePage extends StatelessWidget { mainAxisSpacing: 16, crossAxisSpacing: 16, ), - children: const [ - MenuEntry( + children: [ + const MenuEntry( title: 'Staggered', imageName: 'staggered', destination: StaggeredPage(), ), - MenuEntry( + const MenuEntry( title: 'Masonry', imageName: 'masonry', destination: MasonryPage(), ), - MenuEntry( + const MenuEntry( title: 'Quilted', imageName: 'quilted', destination: QuiltedPage(), ), - MenuEntry( + const MenuEntry( title: 'Woven', imageName: 'woven', destination: WovenPage(), ), - MenuEntry( + const MenuEntry( title: 'Staired', imageName: 'staired', destination: StairedPage(), ), - MenuEntry( + const MenuEntry( title: 'Aligned', imageName: 'aligned', destination: AlignedPage(), ), + MenuEntry( + title: 'Reorderable', + imageName: 'reorderable', + destination: ReorderablePage(), + ), ], ), ); diff --git a/examples/pubspec.yaml b/examples/pubspec.yaml index e898104..3ca33e7 100644 --- a/examples/pubspec.yaml +++ b/examples/pubspec.yaml @@ -1,5 +1,5 @@ name: examples -description: A new Flutter project. +description: Example for collection of Flutter grids layouts (staggered, masonry, quilted, woven, etc.). publish_to: 'none' version: 1.0.0+1 @@ -9,7 +9,6 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^1.0.2 flutter_lints: any flutter_staggered_grid_view: path: ../ diff --git a/lib/flutter_staggered_grid_view.dart b/lib/flutter_staggered_grid_view.dart index 924f0d3..0c0764b 100644 --- a/lib/flutter_staggered_grid_view.dart +++ b/lib/flutter_staggered_grid_view.dart @@ -1,6 +1,7 @@ library flutter_staggered_grid_view; export 'src/layouts/quilted.dart'; +export 'src/layouts/reorderable_staggered_layout.dart'; export 'src/layouts/staired.dart'; export 'src/layouts/woven.dart'; export 'src/rendering/sliver_masonry_grid.dart'; diff --git a/lib/src/layouts/reorderable_staggered_layout.dart b/lib/src/layouts/reorderable_staggered_layout.dart new file mode 100644 index 0000000..4e70c2b --- /dev/null +++ b/lib/src/layouts/reorderable_staggered_layout.dart @@ -0,0 +1,737 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_staggered_grid_view/src/widgets/staggered_grid.dart'; +import 'package:flutter_staggered_grid_view/src/widgets/staggered_grid_tile.dart'; + +/// Callback for when reordering is complete. +typedef ReorderCallback = void Function(int oldIndex, int newIndex); + +/// Callback when feedback is being built. +typedef IndexedFeedBackWidgetBuilder = Widget Function( + BuildContext context, + int index, + Widget child, +); + +/// A list whose items the user can interactively reorder by dragging. +/// +/// This class is appropriate for views with a small number of +/// children because constructing the [List] requires doing work for every +/// child that could possibly be displayed in the list view instead of just +/// those children that are actually visible. +/// +/// All [children] must have a key. +/// +class ReorderableStaggeredLayout extends StatefulWidget { + /// Creates a reorderable list. + ReorderableStaggeredLayout({ + Key? key, + this.header, + required this.children, + required this.onReorder, + this.scrollController, + this.scrollDirection = Axis.vertical, + this.padding, + this.crossAxisCount = 3, + this.reverse = false, + this.longPressToDrag = true, + this.mainAxisSpacing = 0.0, + this.crossAxisSpacing = 0.0, + this.feedBackWidgetBuilder, + }) : super(key: key); + + /// A non-reorderable header widget to show before the list. + /// + /// If null, no header will appear before the list. + final Widget? header; + + /// The widgets to display. + final List children; + + /// The [Axis] along which the list scrolls. + /// + /// List [children] can only drag along this [Axis]. + final Axis scrollDirection; + + /// Creates a [ScrollPosition] to manage and determine which portion + /// of the content is visible in the scroll view. + /// + /// This can be used in many ways, such as setting an initial scroll offset, + /// (via [ScrollController.initialScrollOffset]), reading the current scroll position + /// (via [ScrollController.offset]), or changing it (via [ScrollController.jumpTo] or + /// [ScrollController.animateTo]). + final ScrollController? scrollController; + + /// The amount of space by which to inset the [children]. + final EdgeInsets? padding; + + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + final bool reverse; + + /// Called when a list child is dropped into a new position to shuffle the + /// underlying list. + /// + /// This [ReorderableStaggeredLayout] calls [onReorder] after a list child is dropped + /// into a new position. + final ReorderCallback onReorder; + + /// Used when we are building a GridView + final int crossAxisCount; + + /// Used when we are building a GridView + final bool longPressToDrag; + + /// Used when we are building a GridView + final double mainAxisSpacing; + + /// Used when we are building a GridView + final double crossAxisSpacing; + + /// Feedback widget + final IndexedFeedBackWidgetBuilder? feedBackWidgetBuilder; + + @override + _ReorderableStaggeredLayoutState createState() => _ReorderableStaggeredLayoutState(); +} + +/// This top-level state manages an Overlay that contains the list and +/// also any Draggables it creates. +/// +/// _ReorderableListContent manages the list itself and reorder operations. +/// +/// The Overlay doesn't properly keep state by building new overlay entries, +/// and so we cache a single OverlayEntry for use as the list layer. +/// That overlay entry then builds a _ReorderableListContent which may +/// insert Draggables into the Overlay above itself. +class _ReorderableStaggeredLayoutState extends State { + @override + Widget build(BuildContext context) { + return Overlay( + key: GlobalKey(debugLabel: '$ReorderableStaggeredLayout overlay'), + initialEntries: [ + OverlayEntry( + opaque: true, + builder: (BuildContext context) { + return _ReorderableListContent( + header: widget.header, + children: widget.children, + scrollController: widget.scrollController, + scrollDirection: widget.scrollDirection, + onReorder: widget.onReorder, + padding: widget.padding, + reverse: widget.reverse, + crossAxisCount: widget.crossAxisCount, + longPressToDrag: widget.longPressToDrag, + mainAxisSpacing: widget.mainAxisSpacing, + crossAxisSpacing: widget.crossAxisSpacing, + feedBackWidgetBuilder: widget.feedBackWidgetBuilder, + ); + }, + ) + ], + ); + } +} + +/// This widget is responsible for the inside of the Overlay in the +/// ReorderableItemsView. +class _ReorderableListContent extends StatefulWidget { + const _ReorderableListContent({ + required this.header, + required this.children, + required this.scrollController, + required this.scrollDirection, + required this.padding, + required this.onReorder, + required this.reverse, + required this.crossAxisCount, + required this.longPressToDrag, + required this.mainAxisSpacing, + required this.crossAxisSpacing, + required this.feedBackWidgetBuilder, + }); + + final Widget? header; + final List children; + final ScrollController? scrollController; + final Axis scrollDirection; + final EdgeInsets? padding; + final ReorderCallback onReorder; + final bool reverse; + final int crossAxisCount; + final bool longPressToDrag; + final double mainAxisSpacing; + final double crossAxisSpacing; + final IndexedFeedBackWidgetBuilder? feedBackWidgetBuilder; + + @override + _ReorderableListContentState createState() => _ReorderableListContentState(); +} + +class _ReorderableListContentState extends State<_ReorderableListContent> + with TickerProviderStateMixin<_ReorderableListContent> { + /// The extent along the widget.scrollDirection axis to allow a child to + /// drop into when the user reorders list children. + /// + /// This value is used when the extents haven't yet been calculated from + /// the currently dragging widget, such as when it first builds. + static const double _defaultDropAreaExtent = 100.0; + + /// The additional margin to place around a computed drop area. + static const double _dropAreaMargin = 8.0; + + /// How long an animation to reorder an element in the list takes. + static const Duration _reorderAnimationDuration = Duration(milliseconds: 200); + + /// How long an animation to scroll to an off-screen element in the + /// list takes. + static const Duration _scrollAnimationDuration = Duration(milliseconds: 200); + + /// Controls scrolls and measures scroll progress. + ScrollController? _scrollController; + + /// This controls the entrance of the dragging widget into a new place. + late AnimationController _entranceController; + + /// This controls the 'ghost' of the dragging widget, which is left behind + /// where the widget used to be. + late AnimationController _ghostController; + + /// The member of children currently being dragged. + /// + /// Null if no drag is underway. + Widget? _dragging; + + /// The last computed size of the feedback widget being dragged. + Size? _draggingFeedbackSize; + + /// The location that the dragging widget occupied before it started to drag. + int _dragStartIndex = 0; + + /// The index that the dragging widget most recently left. + /// This is used to show an animation of the widget's position. + int _ghostIndex = 0; + + /// The index that the dragging widget currently occupies. + int _currentIndex = 0; + + /// The widget to move the dragging widget too after the current index. + int _nextIndex = 0; + + /// Whether or not we are currently scrolling this view to show a widget. + bool _scrolling = false; + + double get _dropAreaExtent { + if (_draggingFeedbackSize == null) { + return _defaultDropAreaExtent; + } + double dropAreaWithoutMargin; + switch (widget.scrollDirection) { + case Axis.horizontal: + dropAreaWithoutMargin = _draggingFeedbackSize!.width; + break; + case Axis.vertical: + default: + dropAreaWithoutMargin = _draggingFeedbackSize!.height; + break; + } + return dropAreaWithoutMargin + _dropAreaMargin; + } + + @override + void initState() { + super.initState(); + _entranceController = + AnimationController(vsync: this, duration: _reorderAnimationDuration); + _ghostController = + AnimationController(vsync: this, duration: _reorderAnimationDuration); + _entranceController.addStatusListener(_onEntranceStatusChanged); + } + + @override + void didChangeDependencies() { + _scrollController = widget.scrollController ?? + PrimaryScrollController.of(context) ?? + ScrollController(); + super.didChangeDependencies(); + } + + @override + void dispose() { + _entranceController.dispose(); + _ghostController.dispose(); + super.dispose(); + } + + /// Animates the droppable space from _currentIndex to _nextIndex. + void _requestAnimationToNextIndex() { + if (_entranceController.isCompleted) { + _ghostIndex = _currentIndex; + if (_nextIndex == _currentIndex) { + return; + } + _currentIndex = _nextIndex; + _ghostController.reverse(from: 1.0); + _entranceController.forward(from: 0.0); + } + } + + /// Requests animation to the latest next index if it changes during an animation. + void _onEntranceStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.completed) { + setState(_requestAnimationToNextIndex); + } + } + + /// Scrolls to a target context if that context is not on the screen. + void _scrollTo(BuildContext context) { + if (_scrolling) return; + final RenderObject contextObject = context.findRenderObject()!; + final RenderAbstractViewport viewport = + RenderAbstractViewport.of(contextObject)!; + // If and only if the current scroll offset falls in-between the offsets + // necessary to reveal the selected context at the top or bottom of the + // screen, then it is already on-screen. + final double margin = _dropAreaExtent; + final double scrollOffset = _scrollController!.offset; + final double topOffset = max( + _scrollController!.position.minScrollExtent, + viewport.getOffsetToReveal(contextObject, 0.0).offset - margin, + ); + final double bottomOffset = min( + _scrollController!.position.maxScrollExtent, + viewport.getOffsetToReveal(contextObject, 1.0).offset + margin, + ); + final bool onScreen = + scrollOffset <= topOffset && scrollOffset >= bottomOffset; + + // If the context is off screen, then we request a scroll to make it visible. + if (!onScreen) { + _scrolling = true; + _scrollController!.position + .animateTo( + scrollOffset < bottomOffset ? bottomOffset : topOffset, + duration: _scrollAnimationDuration, + curve: Curves.easeInOut, + ) + .then((void value) { + setState(() { + _scrolling = false; + }); + }); + } + } + + /// Wraps children in Row or Column, so that the children flow in + /// the widget's scrollDirection. + Widget _buildContainerForScrollDirection({required List children}) { + if (widget.header != null) { + if (children[1] is StaggeredGridTile) + return StaggeredGrid.count( + crossAxisCount: widget.crossAxisCount, + children: children, + mainAxisSpacing: widget.mainAxisSpacing, + crossAxisSpacing: widget.crossAxisSpacing, + ); + } else { + if (children.first is StaggeredGridTile) + return StaggeredGrid.count( + crossAxisCount: widget.crossAxisCount, + children: children, + mainAxisSpacing: widget.mainAxisSpacing, + crossAxisSpacing: widget.crossAxisSpacing, + ); + } + + switch (widget.scrollDirection) { + case Axis.horizontal: + return Row(children: children); + case Axis.vertical: + default: + return Column(children: children); + } + } + + /// Wraps one of the widget's children in a DragTarget and Draggable. + /// Handles up the logic for dragging and reordering items in the list. + StaggeredGridTile _wrap( + StaggeredGridTile toWrap, + int index, + BoxConstraints constraints, + ) { + if (toWrap.disableDrag) { + return toWrap; + } + final GlobalObjectKey keyIndexGlobalKey = GlobalObjectKey(toWrap); + // We pass the toWrapWithGlobalKey into the Draggable so that when a list + // item gets dragged, the accessibility framework can preserve the selected + // state of the dragging item. + + // Starts dragging toWrap. + void onDragStarted() { + setState(() { + _dragging = toWrap; + _dragStartIndex = index; + _ghostIndex = index; + _currentIndex = index; + _entranceController.value = 1.0; + _draggingFeedbackSize = keyIndexGlobalKey.currentContext!.size; + }); + } + + /// Places the value from startIndex one space before the element at endIndex. + void reorder(int startIndex, int endIndex) { + setState(() { + if (startIndex != endIndex && endIndex < widget.children.length) + widget.onReorder(startIndex, endIndex); + _ghostController.reverse(from: 0.1); + _entranceController.reverse(from: 0.1); + _dragging = null; + }); + } + + /// Drops toWrap into the last position it was hovering over. + void onDragEnded() { + reorder(_dragStartIndex, _currentIndex); + } + + Widget wrapWithSemantics() { + // First, determine which semantics actions apply. + final Map semanticsActions = + {}; + + // Create the appropriate semantics actions. + void moveToStart() => reorder(index, 0); + void moveToEnd() => reorder(index, widget.children.length); + void moveBefore() => reorder(index, index - 1); + // To move after, we go to index+2 because we are moving it to the space + // before index+2, which is after the space at index+1. + void moveAfter() => reorder(index, index + 2); + + final MaterialLocalizations localizations = + MaterialLocalizations.of(context); + + // If the item can move to before its current position in the list. + if (index > 0) { + semanticsActions[CustomSemanticsAction( + label: localizations.reorderItemToStart, + )] = moveToStart; + String reorderItemBefore = localizations.reorderItemUp; + if (widget.scrollDirection == Axis.horizontal) { + reorderItemBefore = Directionality.of(context) == TextDirection.ltr + ? localizations.reorderItemLeft + : localizations.reorderItemRight; + } + semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] = + moveBefore; + } + + // If the item can move to after its current position in the list. + if (index < widget.children.length - 1) { + String reorderItemAfter = localizations.reorderItemDown; + if (widget.scrollDirection == Axis.horizontal) { + reorderItemAfter = Directionality.of(context) == TextDirection.ltr + ? localizations.reorderItemRight + : localizations.reorderItemLeft; + } + semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] = + moveAfter; + semanticsActions[ + CustomSemanticsAction(label: localizations.reorderItemToEnd)] = + moveToEnd; + } + + // We pass toWrap with a GlobalKey into the Draggable so that when a list + // item gets dragged, the accessibility framework can preserve the selected + // state of the dragging item. + // + // We also apply the relevant custom accessibility actions for moving the item + // up, down, to the start, and to the end of the list. + return KeyedSubtree( + key: keyIndexGlobalKey, + child: MergeSemantics( + child: Semantics( + customSemanticsActions: semanticsActions, + child: toWrap.child, + ), + ), + ); + } + + Widget buildDragTarget( + BuildContext context, + List acceptedCandidates, + List rejectedCandidates, + ) { + final Widget toWrapWithSemantics = wrapWithSemantics(); + + double mainAxisExtent = 0.0; + double crossAxisExtent = 0.0; + + BoxConstraints newConstraints = constraints; + + if (_dragging == null && + index < widget.children.length) { + final StaggeredGridTile tile = + widget.children[index]; + + final double usableCrossAxisExtent = constraints.biggest.width; + final double cellExtent = usableCrossAxisExtent / widget.crossAxisCount; + final num mainAxisCellCount = tile.mainAxisCellCount ?? 1.0; + + mainAxisExtent = tile.mainAxisExtent ?? + (mainAxisCellCount * cellExtent) + (mainAxisCellCount - 1); + + crossAxisExtent = cellExtent * tile.crossAxisCellCount; + + newConstraints = constraints.copyWith( + minWidth: crossAxisExtent, + maxWidth: crossAxisExtent, + minHeight: mainAxisExtent, + maxHeight: mainAxisExtent, + ); + } else { + newConstraints = constraints.copyWith( + minWidth: 0.0, + maxWidth: constraints.maxWidth, + minHeight: 0.0, + maxHeight: constraints.maxHeight, + ); + } + + // We build the draggable inside of a layout builder so that we can + // constrain the size of the feedback dragging widget. + Widget child = widget.longPressToDrag + ? LongPressDraggable( + maxSimultaneousDrags: 1, + axis: null, + data: toWrap, + ignoringFeedbackSemantics: false, + feedback: widget.feedBackWidgetBuilder != null + ? widget.feedBackWidgetBuilder!( + context, + index, + toWrapWithSemantics, + ) + : Container( + alignment: Alignment.topLeft, + // These constraints will limit the cross axis of the drawn widget. + constraints: newConstraints, + color: Colors.transparent, + child: Material( + elevation: 6.0, + child: toWrapWithSemantics, + ), + ), + child: + _dragging == toWrap ? const SizedBox() : toWrapWithSemantics, + childWhenDragging: const SizedBox(), + onDragStarted: onDragStarted, + dragAnchorStrategy: childDragAnchorStrategy, + // When the drag ends inside a DragTarget widget, the drag + // succeeds, and we reorder the widget into position appropriately. + onDragCompleted: onDragEnded, + // When the drag does not end inside a DragTarget widget, the + // drag fails, but we still reorder the widget to the last position it + // had been dragged to. + onDraggableCanceled: (Velocity velocity, Offset offset) { + onDragEnded(); + }, + ) + : Draggable( + maxSimultaneousDrags: 1, + axis: null, + data: toWrap, + ignoringFeedbackSemantics: false, + feedback: widget.feedBackWidgetBuilder != null + ? widget.feedBackWidgetBuilder!( + context, + index, + toWrapWithSemantics, + ) + : Container( + alignment: Alignment.topLeft, + // These constraints will limit the cross axis of the drawn widget. + constraints: newConstraints, + child: Material( + elevation: 6.0, + color: Colors.transparent, + child: toWrapWithSemantics, + ), + ), + child: + _dragging == toWrap ? const SizedBox() : toWrapWithSemantics, + childWhenDragging: const SizedBox(), + onDragStarted: onDragStarted, + dragAnchorStrategy: childDragAnchorStrategy, + // When the drag ends inside a DragTarget widget, the drag + // succeeds, and we reorder the widget into position appropriately. + onDragCompleted: onDragEnded, + // When the drag does not end inside a DragTarget widget, the + // drag fails, but we still reorder the widget to the last position it + // had been dragged to. + onDraggableCanceled: (Velocity velocity, Offset offset) { + onDragEnded(); + }, + ); + + // The target for dropping at the end of the list doesn't need to be + // draggable. + if (index >= widget.children.length) { + child = toWrap; + } + + // Determine the size of the drop area to show under the dragging widget. + Widget spacing; + switch (widget.scrollDirection) { + case Axis.horizontal: + spacing = SizedBox(width: _dropAreaExtent); + break; + case Axis.vertical: + default: + spacing = SizedBox(height: _dropAreaExtent); + break; + } + + // We open up a space under where the dragging widget currently is to + // show it can be dropped. + if (_currentIndex == index && + _dragging != null) { + return _buildContainerForScrollDirection( + children: [ + SizeTransition( + sizeFactor: _entranceController, + axis: widget.scrollDirection, + child: spacing, + ), + child, + ], + ); + } + // We close up the space under where the dragging widget previously was + // with the ghostController animation. + if (_ghostIndex == index && + _dragging != null) { + return _buildContainerForScrollDirection( + children: [ + SizeTransition( + sizeFactor: _ghostController, + axis: widget.scrollDirection, + child: spacing, + ), + child, + ], + ); + } + + if (_ghostIndex == index && + _dragging != null) { + return Opacity( + opacity: .5, + child: child, + ); + } + return child; + } + + Widget target = Builder( + builder: (BuildContext context) { + return DragTarget( + builder: buildDragTarget, + onWillAccept: (Widget? toAccept) { + setState(() { + _nextIndex = index; + _requestAnimationToNextIndex(); + }); + _scrollTo(context); + // If the target is not the original starting point, then we will accept the drop. + return _dragging == toAccept && toAccept != toWrap; + }, + onAccept: (Widget accepted) {}, + onLeave: (Object? leaving) {}, + ); + }, + ); + + // We wrap the drag target in a Builder so that we can scroll to its specific context. + + if (toWrap.mainAxisCellCount != null) { + return StaggeredGridTile.count( + crossAxisCellCount: toWrap.crossAxisCellCount, + mainAxisCellCount: toWrap.mainAxisCellCount!, + child: target, + ); + } else if (toWrap.mainAxisExtent != null) { + return StaggeredGridTile.extent( + crossAxisCellCount: toWrap.crossAxisCellCount, + mainAxisExtent: toWrap.mainAxisExtent!, + child: target, + ); + } else { + return StaggeredGridTile.fit( + crossAxisCellCount: toWrap.crossAxisCellCount, + child: target, + ); + } + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterialLocalizations(context)); + // We use the layout builder to constrain the cross-axis size of dragging child widgets. + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + const Key endWidgetKey = Key('DraggableList - End Widget'); + Widget finalDropArea; + switch (widget.scrollDirection) { + case Axis.horizontal: + finalDropArea = SizedBox( + key: endWidgetKey, + width: _defaultDropAreaExtent, + height: constraints.maxHeight, + ); + break; + case Axis.vertical: + default: + finalDropArea = SizedBox( + key: endWidgetKey, + height: _defaultDropAreaExtent, + width: constraints.maxWidth, + ); + break; + } + return SingleChildScrollView( + scrollDirection: widget.scrollDirection, + padding: widget.padding, + controller: _scrollController, + reverse: widget.reverse, + child: _buildContainerForScrollDirection( + children: [ + //TODO: Check if neccessary with current changes + // if (widget.reverse) + // _wrap(finalDropArea, widget.children.length, constraints), + if (widget.header != null) widget.header!, + for (int i = 0; i < widget.children.length; i += 1) + _wrap(widget.children[i], i, constraints), + // if (!widget.reverse) + // _wrap(finalDropArea, widget.children.length, constraints), + ], + ), + ); + }, + ); + } +} diff --git a/lib/src/widgets/staggered_grid_tile.dart b/lib/src/widgets/staggered_grid_tile.dart index 45fd201..2f1187e 100644 --- a/lib/src/widgets/staggered_grid_tile.dart +++ b/lib/src/widgets/staggered_grid_tile.dart @@ -10,6 +10,7 @@ class StaggeredGridTile extends ParentDataWidget { required this.mainAxisCellCount, required this.mainAxisExtent, required Widget child, + this.disableDrag = false, }) : assert(crossAxisCellCount > 0), assert(mainAxisCellCount == null || mainAxisCellCount > 0), assert(mainAxisExtent == null || mainAxisExtent > 0), @@ -22,12 +23,14 @@ class StaggeredGridTile extends ParentDataWidget { required int crossAxisCellCount, required num mainAxisCellCount, required Widget child, + bool disableDrag = false, }) : this._( key: key, crossAxisCellCount: crossAxisCellCount, mainAxisCellCount: mainAxisCellCount, mainAxisExtent: null, child: child, + disableDrag: disableDrag, ); /// Creates a [StaggeredGrid]'s tile that takes a specific amount of space @@ -37,12 +40,14 @@ class StaggeredGridTile extends ParentDataWidget { required int crossAxisCellCount, required double mainAxisExtent, required Widget child, + bool disableDrag = false, }) : this._( key: key, crossAxisCellCount: crossAxisCellCount, mainAxisCellCount: null, mainAxisExtent: mainAxisExtent, child: child, + disableDrag: disableDrag, ); /// Creates a [StaggeredGrid]'s tile that fits its main axis extent to its @@ -51,12 +56,14 @@ class StaggeredGridTile extends ParentDataWidget { Key? key, required int crossAxisCellCount, required Widget child, + bool disableDrag = false, }) : this._( key: key, crossAxisCellCount: crossAxisCellCount, mainAxisCellCount: null, mainAxisExtent: null, child: child, + disableDrag: disableDrag, ); /// The number of cells that this tile takes along the cross axis. @@ -68,6 +75,9 @@ class StaggeredGridTile extends ParentDataWidget { /// The amount of space that this tile takes along the main axis. final double? mainAxisExtent; + /// Disables the possibility to drag the item + final bool disableDrag; + @override void applyParentData(RenderObject renderObject) { final parentData = renderObject.parentData;