refactor: enhance drag-and-drop functionality in _ReorderableGrid with visual feedback

feature/rearrange-buttons-2
idubnori 2025-12-10 09:46:19 +07:00
parent 598e856322
commit 1517385704
1 changed files with 170 additions and 62 deletions

@ -169,77 +169,185 @@ class _ReorderableGrid extends StatefulWidget {
class _ReorderableGridState extends State<_ReorderableGrid> { class _ReorderableGridState extends State<_ReorderableGrid> {
int? _draggingIndex; int? _draggingIndex;
int? _hoveringIndex; late List<int> _itemOrder;
@override
void initState() {
super.initState();
_itemOrder = List.generate(widget.items.length, (index) => index);
}
@override
void didUpdateWidget(_ReorderableGrid oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.items.length != widget.items.length) {
_itemOrder = List.generate(widget.items.length, (index) => index);
}
}
void _updateHover(int draggedIndex, int targetIndex) {
if (draggedIndex == targetIndex || _draggingIndex == null) return;
setState(() {
// Temporarily reorder for visual feedback
final newOrder = List<int>.from(_itemOrder);
final draggedOrderIndex = newOrder.indexOf(draggedIndex);
final targetOrderIndex = newOrder.indexOf(targetIndex);
newOrder.removeAt(draggedOrderIndex);
newOrder.insert(targetOrderIndex, draggedIndex);
_itemOrder = newOrder;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GridView.builder( return LayoutBuilder(
controller: widget.scrollController, builder: (context, constraints) {
physics: widget.shouldScroll ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(), final tileWidth =
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( (constraints.maxWidth - (widget.crossAxisSpacing * (widget.crossAxisCount - 1))) / widget.crossAxisCount;
crossAxisCount: widget.crossAxisCount, final tileHeight = tileWidth / widget.childAspectRatio;
crossAxisSpacing: widget.crossAxisSpacing, final rows = (_itemOrder.length / widget.crossAxisCount).ceil();
mainAxisSpacing: widget.mainAxisSpacing, final totalHeight = rows * tileHeight + (rows - 1) * widget.mainAxisSpacing;
childAspectRatio: widget.childAspectRatio,
), return SingleChildScrollView(
itemCount: widget.items.length, controller: widget.scrollController,
itemBuilder: (context, index) { physics: widget.shouldScroll ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(),
final item = widget.items[index]; child: SizedBox(
final isDragging = _draggingIndex == index; width: constraints.maxWidth,
final isHovering = _hoveringIndex == index; height: totalHeight,
child: Stack(
return DragTarget<int>( children: List.generate(widget.items.length, (index) {
onWillAcceptWithDetails: (details) { final visualIndex = _itemOrder.indexOf(index);
if (details.data != index) { final item = widget.items[index];
setState(() => _hoveringIndex = index); final isDragging = _draggingIndex == index;
}
return details.data != index; // Calculate position
}, final row = visualIndex ~/ widget.crossAxisCount;
onLeave: (_) { final col = visualIndex % widget.crossAxisCount;
setState(() => _hoveringIndex = null); final left = col * (tileWidth + widget.crossAxisSpacing);
}, final top = row * (tileHeight + widget.mainAxisSpacing);
onAcceptWithDetails: (details) {
final oldIndex = details.data; return _AnimatedGridItem(
if (oldIndex != index) { key: ValueKey(index),
widget.onReorder(oldIndex, index); index: index,
} item: item,
setState(() { isDragging: isDragging,
_hoveringIndex = null; tileWidth: tileWidth,
_draggingIndex = null; tileHeight: tileHeight,
}); left: left,
}, top: top,
builder: (context, candidateData, rejectedData) { onDragStarted: () {
return LongPressDraggable<int>( setState(() => _draggingIndex = index);
data: index, },
feedback: Material( onDragUpdate: (draggedIndex, targetIndex) {
color: Colors.transparent, _updateHover(draggedIndex, targetIndex);
},
onDragEnd: (draggedIndex, targetIndex) {
if (draggedIndex != targetIndex) {
final oldVisualIndex = _itemOrder.indexOf(draggedIndex);
final newVisualIndex = _itemOrder.indexOf(targetIndex);
widget.onReorder(oldVisualIndex, newVisualIndex);
}
setState(() {
_draggingIndex = null;
_itemOrder = List.generate(widget.items.length, (i) => i);
});
},
onDragCanceled: () {
setState(() {
_draggingIndex = null;
_itemOrder = List.generate(widget.items.length, (i) => i);
});
},
);
}),
),
),
);
},
);
}
}
class _AnimatedGridItem extends StatelessWidget {
final int index;
final ActionButtonType item;
final bool isDragging;
final double tileWidth;
final double tileHeight;
final double left;
final double top;
final VoidCallback onDragStarted;
final Function(int draggedIndex, int targetIndex) onDragUpdate;
final Function(int draggedIndex, int targetIndex) onDragEnd;
final VoidCallback onDragCanceled;
const _AnimatedGridItem({
super.key,
required this.index,
required this.item,
required this.isDragging,
required this.tileWidth,
required this.tileHeight,
required this.left,
required this.top,
required this.onDragStarted,
required this.onDragUpdate,
required this.onDragEnd,
required this.onDragCanceled,
});
@override
Widget build(BuildContext context) {
return AnimatedPositioned(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
left: left,
top: top,
width: tileWidth,
height: tileHeight,
child: DragTarget<int>(
onWillAcceptWithDetails: (details) {
if (details.data != index) {
onDragUpdate(details.data, index);
}
return details.data != index;
},
onAcceptWithDetails: (details) {
onDragEnd(details.data, index);
},
builder: (context, candidateData, rejectedData) {
Widget child = _QuickActionTile(index: index, type: item);
if (isDragging) {
child = Opacity(opacity: 0.0, child: child);
}
return Draggable<int>(
data: index,
feedback: Material(
color: Colors.transparent,
child: SizedBox(
width: tileWidth,
height: tileHeight,
child: Opacity( child: Opacity(
opacity: 0.8, opacity: 0.9,
child: Transform.scale( child: Transform.scale(
scale: 1.1, scale: 1.05,
child: _QuickActionTile(index: index, type: item), child: _QuickActionTile(index: index, type: item),
), ),
), ),
), ),
childWhenDragging: Opacity( ),
opacity: 0.3, childWhenDragging: const SizedBox.shrink(),
child: _QuickActionTile(index: index, type: item), onDragStarted: onDragStarted,
), onDragEnd: (_) => onDragCanceled(),
onDragStarted: () { onDraggableCanceled: (_, __) => onDragCanceled(),
setState(() => _draggingIndex = index); child: child,
}, );
onDragEnd: (_) { },
setState(() => _draggingIndex = null); ),
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
transform: isHovering && !isDragging ? Matrix4.translationValues(0, -4, 0) : Matrix4.identity(),
child: _QuickActionTile(index: index, type: item),
),
);
},
);
},
); );
} }
} }