From 5c71ba0a7bdeb726af8577f2a0e27c39907ab764 Mon Sep 17 00:00:00 2001 From: Sneha Canopas <92501869+cp-sneha-s@users.noreply.github.com> Date: Tue, 31 Dec 2024 19:05:34 +0530 Subject: [PATCH] Allow to prevent reordering (#106) * Add lockedItem * Add lockedItem * Update documentation * Fix dragged issue for different instances * Update documentation * Minor change * Minor change * Update README.md * Update README.md --- CHANGELOG.md | 6 ++ README.md | 4 +- example/lib/main.dart | 47 +++++++++++---- example/lib/model/user_model.dart | 18 ------ example/lib/utils/item_card.dart | 14 +++-- example/lib/utils/item_tile.dart | 14 +++-- example/pubspec.lock | 2 +- lib/src/animated_reorderable_gridview.dart | 5 ++ lib/src/animated_reorderable_listview.dart | 7 +++ lib/src/builder/motion_animated_builder.dart | 62 +++++++++++++++----- lib/src/builder/motion_list_base.dart | 23 +++++++- lib/src/builder/motion_list_impl.dart | 5 ++ pubspec.yaml | 2 +- 13 files changed, 150 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c996112..9132174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.1.5 + +### Improvements +- Add `lockedItems` to make items locked and non-draggable. +- Fix issue with `nonDraggableItems` for different instances of the list. + ## 1.1.4 ### Improvements diff --git a/README.md b/README.md index 4f0d185..eccd083 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,14 @@ drag-and-drop functionality in Flutter. - [x] Pre-built animation like fade,scale, slide, flip etc for Flutter list. - [x] Provides support for both lists and grids - [x] Supports large lists and creates items on demand as they come into the viewport. +- [x] Enable/disable drag and drop functionality. +- [x] Control reordering of item / locked an item. ## Demo ### Reorderable List -| ![Image 1](https://github.com/user-attachments/assets/f68812ad-5106-4154-9e9a-7a09e7d23015?raw=true) | ![Image 2](https://github.com/user-attachments/assets/e954b616-1820-4004-9c0e-23b002a6b36f?raw=true) | ![Image 3](https://github.com/user-attachments/assets/9b5c695b-7874-4ea3-86b0-1b00b2bf8d65?raw=true) | +| ![Image 1](https://github.com/user-attachments/assets/7a31a2c0-f49b-4280-ac8c-b4bc35e5a3db?raw=true) | ![Image 2](https://github.com/user-attachments/assets/3a7d34ec-eb2f-491c-af8f-43b569607d91?raw=true) | ![Image 3](https://github.com/user-attachments/assets/68c1b0f7-481e-4e6e-b995-e1b754354d1f?raw=true) | | :---: | :---: | :---: | | ReorderableGridView | ReorderableListView | Swap Animation | diff --git a/example/lib/main.dart b/example/lib/main.dart index 7091a65..8b9e12f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -30,6 +30,7 @@ class _HomePageState extends State { bool isGrid = true; List nonDraggableItems = []; + List lockedItems = []; List animations = []; @@ -37,7 +38,8 @@ class _HomePageState extends State { void initState() { super.initState(); list = List.generate(8, (index) => User(name: "User $index", id: index)); - nonDraggableItems = list.where((user) => user.id == 0).toList(); + nonDraggableItems = list.where((user) => user.id == 1).toList(); + lockedItems = list.where((user) => user.id == 0).toList(); } void insert() { @@ -63,9 +65,9 @@ class _HomePageState extends State { actions: [ IconButton( onPressed: () { - final child1 = list[1]; + final child1 = list[2]; final child2 = list[7]; - list[1] = child2; + list[2] = child2; list[7] = child1; setState(() {}); }, @@ -103,6 +105,7 @@ class _HomePageState extends State { key: ValueKey(user.id), id: user.id, dragEnabled: !nonDraggableItems.contains(user), + isLocked: lockedItems.contains(user), ); }, sliverGridDelegate: @@ -113,12 +116,24 @@ class _HomePageState extends State { insertDuration: const Duration(milliseconds: 300), removeDuration: const Duration(milliseconds: 300), onReorder: (int oldIndex, int newIndex) { + final Map lockedItemPositions = { + for (int i = 0; i < list.length; i++) + if (lockedItems.contains(list[i])) list[i]: i + }; setState(() { final User user = list.removeAt(oldIndex); list.insert(newIndex, user); + for (var entry in lockedItemPositions.entries) { + list.remove(entry.key); + list.insert( + entry.value, + entry + .key); // Insert based on original position (id in this case) + } }); }, nonDraggableItems: nonDraggableItems, + lockedItems: lockedItems, dragStartDelay: const Duration(milliseconds: 300), onReorderEnd: (int index) { // print(" End index : $index"); @@ -158,6 +173,7 @@ class _HomePageState extends State { key: ValueKey(user.id), id: user.id, dragEnabled: !nonDraggableItems.contains(user), + isLocked: lockedItems.contains(user), ); }, enterTransition: animations, @@ -165,17 +181,24 @@ class _HomePageState extends State { insertDuration: const Duration(milliseconds: 300), removeDuration: const Duration(milliseconds: 300), nonDraggableItems: nonDraggableItems, + lockedItems: lockedItems, dragStartDelay: const Duration(milliseconds: 300), onReorder: (int oldIndex, int newIndex) { - final User user = list.removeAt(oldIndex); - list.insert(newIndex, user); - - // Add isSameItem to compare objects when creating new - - for (int i = 0; i < list.length; i++) { - list[i] = list[i].copyWith(id: list[i].id); - } - setState(() {}); + final Map lockedItemPositions = { + for (int i = 0; i < list.length; i++) + if (lockedItems.contains(list[i])) list[i]: i + }; + setState(() { + final User user = list.removeAt(oldIndex); + list.insert(newIndex, user); + for (var entry in lockedItemPositions.entries) { + list.remove(entry.key); + list.insert( + entry.value, + entry + .key); // Insert based on original position (id in this case) + } + }); }, proxyDecorator: proxyDecorator, isSameItem: (a, b) => a.id == b.id diff --git a/example/lib/model/user_model.dart b/example/lib/model/user_model.dart index 10f8f07..99541c8 100644 --- a/example/lib/model/user_model.dart +++ b/example/lib/model/user_model.dart @@ -3,22 +3,4 @@ class User { final int id; User({required this.name, required this.id}); - - User copyWith({ - String? name, - int? id, - }) { - return User( - name: name ?? this.name, - id: id ?? this.id, - ); - } - - @override - bool operator ==(Object other) { - return other is User && other.name == name && other.id == id; - } - - @override - int get hashCode => Object.hash(name, id); } diff --git a/example/lib/utils/item_card.dart b/example/lib/utils/item_card.dart index 9478930..c1666c4 100644 --- a/example/lib/utils/item_card.dart +++ b/example/lib/utils/item_card.dart @@ -4,8 +4,13 @@ import 'package:flutter/material.dart'; class ItemCard extends StatelessWidget { final int id; final bool dragEnabled; + final bool isLocked; - const ItemCard({super.key, required this.id, this.dragEnabled = true}); + const ItemCard( + {super.key, + required this.id, + this.dragEnabled = true, + this.isLocked = false}); @override Widget build(BuildContext context) { @@ -13,11 +18,12 @@ class ItemCard extends StatelessWidget { height: 150.0, width: 150, child: Card( - color: !dragEnabled + color: isLocked ? containerLowColor - : Colors.primaries[id % Colors.primaries.length], + : Colors.primaries[id % Colors.primaries.length] + .withValues(alpha: dragEnabled ? 1 : 0.3), child: Center( - child: dragEnabled + child: !isLocked ? Text((id).toString(), style: const TextStyle(fontSize: 22, color: Colors.black)) : const Icon(Icons.lock, color: Colors.white), diff --git a/example/lib/utils/item_tile.dart b/example/lib/utils/item_tile.dart index e937124..e0f69b7 100644 --- a/example/lib/utils/item_tile.dart +++ b/example/lib/utils/item_tile.dart @@ -4,11 +4,13 @@ import '../theme/colors.dart'; class ItemTile extends StatelessWidget { final int id; final bool dragEnabled; + final bool isLocked; const ItemTile({ super.key, required this.id, this.dragEnabled = true, + this.isLocked = false, }); @override @@ -18,13 +20,13 @@ class ItemTile extends StatelessWidget { width: double.infinity, margin: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: !dragEnabled - ? containerLowColor - : Colors.primaries[id % Colors.primaries.length], - ), + borderRadius: BorderRadius.circular(10), + color: isLocked + ? containerLowColor + : Colors.primaries[id % Colors.primaries.length] + .withValues(alpha: dragEnabled ? 1 : 0.3)), child: Center( - child: dragEnabled + child: !isLocked ? Text( 'Item $id', style: const TextStyle(fontSize: 25), diff --git a/example/pubspec.lock b/example/pubspec.lock index 1cd0d5b..b16e6b7 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -23,7 +23,7 @@ packages: path: ".." relative: true source: path - version: "1.1.3" + version: "1.1.5" args: dependency: transitive description: diff --git a/lib/src/animated_reorderable_gridview.dart b/lib/src/animated_reorderable_gridview.dart index e3a68ca..9bd5ae2 100644 --- a/lib/src/animated_reorderable_gridview.dart +++ b/lib/src/animated_reorderable_gridview.dart @@ -339,6 +339,9 @@ class AnimatedReorderableGridView extends StatefulWidget { /// The item can't be draggable, but it can be reordered. final List nonDraggableItems; + /// A list of items that are locked and can't be reordered. + final List lockedItems; + /// Whether to enable swap animation when changing the order of the items. /// /// Defaults to true. @@ -377,6 +380,7 @@ class AnimatedReorderableGridView extends StatefulWidget { required this.isSameItem, this.dragStartDelay = const Duration(milliseconds: 500), this.nonDraggableItems = const [], + this.lockedItems = const [], this.enableSwap = true}) : super(key: key); @@ -467,6 +471,7 @@ class AnimatedReorderableGridViewState isSameItem: widget.isSameItem, dragStartDelay: widget.dragStartDelay, nonDraggableItems: widget.nonDraggableItems, + lockedItems: widget.lockedItems, enableSwap: widget.enableSwap, ), ), diff --git a/lib/src/animated_reorderable_listview.dart b/lib/src/animated_reorderable_listview.dart index 6836293..161e580 100644 --- a/lib/src/animated_reorderable_listview.dart +++ b/lib/src/animated_reorderable_listview.dart @@ -355,6 +355,11 @@ class AnimatedReorderableListView extends StatefulWidget { /// The item can't be draggable, but it can be reordered. final List nonDraggableItems; + /// A list of items that are locked and can't be reordered and dragged. + /// + /// Add elements in this list if all the elements in the list has same height. If the elements has different height, the reordered items will not be placed in the correct position. + final List lockedItems; + /// Whether to enable swap animation when changing the order of the items. /// /// Defaults to true. @@ -393,6 +398,7 @@ class AnimatedReorderableListView extends StatefulWidget { required this.isSameItem, this.dragStartDelay = const Duration(milliseconds: 500), this.nonDraggableItems = const [], + this.lockedItems = const [], this.enableSwap = true, }) : super(key: key); @@ -483,6 +489,7 @@ class AnimatedReorderableListViewState isSameItem: widget.isSameItem, dragStartDelay: widget.dragStartDelay, nonDraggableItems: widget.nonDraggableItems, + lockedItems: widget.lockedItems, enableSwap: widget.enableSwap, ), ), diff --git a/lib/src/builder/motion_animated_builder.dart b/lib/src/builder/motion_animated_builder.dart index 4aa4c19..6ff7583 100644 --- a/lib/src/builder/motion_animated_builder.dart +++ b/lib/src/builder/motion_animated_builder.dart @@ -32,6 +32,7 @@ class MotionBuilder extends StatefulWidget { final bool longPressDraggable; final Duration dragStartDelay; final List nonDraggableIndices; + final List lockedIndices; const MotionBuilder( {Key? key, @@ -48,7 +49,8 @@ class MotionBuilder extends StatefulWidget { required this.buildDefaultDragHandles, this.longPressDraggable = false, required this.dragStartDelay, - required this.nonDraggableIndices}) + required this.nonDraggableIndices, + required this.lockedIndices}) : assert(initialCount >= 0), super(key: key); @@ -306,12 +308,18 @@ class MotionBuilderState extends State if (_insertIndex == item.index || isGrid) { _finalDropPosition = _itemOffsetAt(_insertIndex!); } else if (_reverse) { - if (_insertIndex! >= _items.length) { + if (_insertIndex! >= _items.length - 1) { _finalDropPosition = _itemStartOffsetAt(_items.length - 1) - _extentOffset(item.itemExtent, scrollDirection); } else { - final int atIndex = + int atIndex = _dragIndex! < _insertIndex! ? _insertIndex! + 1 : _insertIndex!; + if (_dragIndex! > _insertIndex! && widget.lockedIndices.isNotEmpty) { + atIndex = _insertIndex! + 1; + if (!widget.lockedIndices.contains(atIndex)) { + atIndex = _insertIndex!; + } + } _finalDropPosition = _itemStartOffsetAt(atIndex) + _extentOffset(_itemExtent(atIndex), scrollDirection); } @@ -320,8 +328,17 @@ class MotionBuilderState extends State _finalDropPosition = _itemStartOffsetAt(0) - _extentOffset(item.itemExtent, scrollDirection); } else { - final int atIndex = + int atIndex = _dragIndex! < _insertIndex! ? _insertIndex! : _insertIndex! - 1; + + // if the item is locked, we need to calculate final position from the previous item + // if the previous item is locked, then we calculate position from the previous item + if (_dragIndex! < _insertIndex! && widget.lockedIndices.isNotEmpty) { + atIndex = _insertIndex! - 1; + if (!widget.lockedIndices.contains(atIndex)) { + atIndex = _insertIndex!; + } + } _finalDropPosition = _itemStartOffsetAt(atIndex) + _extentOffset(_itemExtent(atIndex), scrollDirection); } @@ -387,7 +404,9 @@ class MotionBuilderState extends State for (final MotionAnimatedContentState item in _items.values) { if (!item.mounted) continue; final Rect geometry = item.targetGeometryNonOffset(); - + if (widget.lockedIndices.contains(item.index)) { + continue; + } if (geometry.contains(dragCenter)) { newIndex = item.index; break; @@ -398,7 +417,10 @@ class MotionBuilderState extends State _insertIndex = newIndex; for (final MotionAnimatedContentState item in _items.values) { - if (item.index == _dragIndex) continue; + if (item.index == _dragIndex || + widget.lockedIndices.contains(item.index)) { + continue; + } item.updateForGap(true); } } @@ -410,16 +432,24 @@ class MotionBuilderState extends State if (index < minPos || index > maxPos) return Offset.zero; - final int direction = _insertIndex! > _dragIndex! ? -1 : 1; + int direction = _insertIndex! > _dragIndex! ? -1 : 1; + + int targetIndex = index + direction; + + Offset targetOffset = _extentOffset(_dragInfo!.itemExtent, scrollDirection); + + while (widget.lockedIndices.contains(targetIndex)) { + targetIndex += direction; + targetOffset += _extentOffset(_dragInfo!.itemExtent, scrollDirection); + } if (isGrid) { - return _itemOffsetAt(index + direction) - _itemOffsetAt(index); + return _itemOffsetAt(targetIndex) - _itemOffsetAt(index); } else { - final Offset offset = - _extentOffset(_dragInfo!.itemExtent, scrollDirection); - if (_insertIndex! > _dragIndex!) { - return _reverse ? offset : -offset; + if (_reverse) { + return _insertIndex! > _dragIndex! ? targetOffset : -targetOffset; + } else { + return _insertIndex! > _dragIndex! ? -targetOffset : targetOffset; } - return _reverse ? -offset : offset; } } @@ -685,15 +715,19 @@ class MotionBuilderState extends State } Offset _itemStartOffsetAt(int index) { + if (!_items.containsKey(index)) return Offset.zero; return _items[index]!.targetGeometry().topLeft; } double _itemExtent(int index) { + if (!_items.containsKey(index)) return 0; return _sizeExtent(_items[index]!.targetGeometry().size, scrollDirection); } bool _dragEnabled(int index) => - widget.onReorder != null && !widget.nonDraggableIndices.contains(index); + widget.onReorder != null && + !widget.nonDraggableIndices.contains(index) && + !widget.lockedIndices.contains(index); @override Widget build(BuildContext context) { diff --git a/lib/src/builder/motion_list_base.dart b/lib/src/builder/motion_list_base.dart index 1abf829..8bf8986 100644 --- a/lib/src/builder/motion_list_base.dart +++ b/lib/src/builder/motion_list_base.dart @@ -36,6 +36,7 @@ abstract class MotionListBase final bool Function(E a, E b)? isSameItem; final Duration? dragStartDelay; final List nonDraggableItems; + final List lockedItems; final bool enableSwap; const MotionListBase( @@ -59,7 +60,8 @@ abstract class MotionListBase this.isSameItem, this.dragStartDelay, this.enableSwap = true, - required this.nonDraggableItems}) + required this.nonDraggableItems, + required this.lockedItems}) : assert(itemBuilder != null), super(key: key); } @@ -156,7 +158,24 @@ abstract class MotionListBaseState< List get nonDraggableItems => widget.items .asMap() .entries - .where((entry) => widget.nonDraggableItems.contains(entry.value)) + .where((entry) { + final found = + widget.nonDraggableItems.where((e) => isSameItem(e, entry.value)); + return found.isNotEmpty; + }) + .map((entry) => entry.key) + .toList(); + + @nonVirtual + @protected + List get lockedIndices => widget.items + .asMap() + .entries + .where((entry) { + final items = + widget.lockedItems.where((e) => isSameItem(e, entry.value)); + return items.isNotEmpty; + }) .map((entry) => entry.key) .toList(); diff --git a/lib/src/builder/motion_list_impl.dart b/lib/src/builder/motion_list_impl.dart index b04462c..19708bc 100644 --- a/lib/src/builder/motion_list_impl.dart +++ b/lib/src/builder/motion_list_impl.dart @@ -25,6 +25,7 @@ class MotionListImpl extends MotionListBase { bool Function(E a, E b)? isSameItem, Duration? dragStartDelay, List nonDraggableItems = const [], + List lockedItems = const [], bool enableSwap = true, }) : super( key: key, @@ -46,6 +47,7 @@ class MotionListImpl extends MotionListBase { isSameItem: isSameItem, dragStartDelay: dragStartDelay, nonDraggableItems: nonDraggableItems, + lockedItems: lockedItems, enableSwap: enableSwap); const MotionListImpl.grid({ @@ -69,6 +71,7 @@ class MotionListImpl extends MotionListBase { bool Function(E a, E b)? isSameItem, Duration? dragStartDelay, List nonDraggableItems = const [], + List lockedItems = const [], bool enableSwap = true, }) : super( key: key, @@ -91,6 +94,7 @@ class MotionListImpl extends MotionListBase { isSameItem: isSameItem, dragStartDelay: dragStartDelay, nonDraggableItems: nonDraggableItems, + lockedItems: lockedItems, enableSwap: enableSwap); @override @@ -119,6 +123,7 @@ class MotionListImplState longPressDraggable: longPressDraggable, dragStartDelay: dragStartDelay, nonDraggableIndices: nonDraggableItems, + lockedIndices: lockedIndices, ); } } diff --git a/pubspec.yaml b/pubspec.yaml index da87160..7ab812c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: animated_reorderable_list description: A Flutter Reorderable Animated List with simple implementation and smooth transition. -version: 1.1.4 +version: 1.1.5 repository: https://github.com/canopas/animated_reorderable_list environment: