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;