fix: improve drag-and-drop handling in _ReorderableGrid with enhanced visual feedback and snap animation

This commit is contained in:
idubnori
2025-12-10 13:41:04 +09:00
parent 1517385704
commit f91d5d7da8
@@ -170,6 +170,8 @@ class _ReorderableGrid extends StatefulWidget {
class _ReorderableGridState extends State<_ReorderableGrid> { class _ReorderableGridState extends State<_ReorderableGrid> {
int? _draggingIndex; int? _draggingIndex;
late List<int> _itemOrder; late List<int> _itemOrder;
int? _lastHoveredIndex;
bool _snapNow = false;
@override @override
void initState() { void initState() {
@@ -189,6 +191,7 @@ class _ReorderableGridState extends State<_ReorderableGrid> {
if (draggedIndex == targetIndex || _draggingIndex == null) return; if (draggedIndex == targetIndex || _draggingIndex == null) return;
setState(() { setState(() {
_lastHoveredIndex = targetIndex;
// Temporarily reorder for visual feedback // Temporarily reorder for visual feedback
final newOrder = List<int>.from(_itemOrder); final newOrder = List<int>.from(_itemOrder);
final draggedOrderIndex = newOrder.indexOf(draggedIndex); final draggedOrderIndex = newOrder.indexOf(draggedIndex);
@@ -197,6 +200,65 @@ class _ReorderableGridState extends State<_ReorderableGrid> {
newOrder.removeAt(draggedOrderIndex); newOrder.removeAt(draggedOrderIndex);
newOrder.insert(targetOrderIndex, draggedIndex); newOrder.insert(targetOrderIndex, draggedIndex);
_itemOrder = newOrder; _itemOrder = newOrder;
// ignore: avoid_print
print('[D&D] Hover: dragged=$draggedIndex -> target=$targetIndex, visualOrder=$_itemOrder');
});
}
void _handleDragEnd(int draggedIndex, int? targetIndex) {
// ignore: avoid_print
print('[D&D] DragEnd called: draggedIndex=$draggedIndex, targetIndex=$targetIndex, visualOrder=$_itemOrder');
// Use targetIndex if available, otherwise check if visual position changed
final effectiveTargetIndex =
targetIndex ??
(() {
final currentVisualIndex = _itemOrder.indexOf(draggedIndex);
// If visual position changed from original, use the item at current visual position
if (currentVisualIndex != draggedIndex) {
return _itemOrder[currentVisualIndex];
}
return null;
})();
// ignore: avoid_print
print('[D&D] Effective target: $effectiveTargetIndex');
if (effectiveTargetIndex != null && draggedIndex != effectiveTargetIndex) {
// Find the visual positions in _itemOrder
final oldVisualPosition = _itemOrder.indexOf(draggedIndex);
final newVisualPosition = _itemOrder.indexOf(effectiveTargetIndex);
// ignore: avoid_print
print('[D&D] Visual positions: old=$oldVisualPosition, new=$newVisualPosition');
// Pass the actual indices (draggedIndex is old, effectiveTargetIndex is new)
// But we need to pass the position in the visual order
widget.onReorder(draggedIndex, effectiveTargetIndex);
// ignore: avoid_print
print('[D&D] Called onReorder: oldIndex=$draggedIndex, newIndex=$effectiveTargetIndex');
} else {
// ignore: avoid_print
print('[D&D] Skipping onReorder: no valid target or same position');
}
// Trigger snap animation for all items
_armSnapNow();
setState(() {
_draggingIndex = null;
_lastHoveredIndex = null;
_itemOrder = List.generate(widget.items.length, (i) => i);
});
}
void _armSnapNow() {
// ignore: avoid_print
print('[D&D] Snap animation triggered for all items');
// 直後のレイアウト更新でだけ duration を 0 にする
setState(() => _snapNow = true);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() => _snapNow = false);
}); });
} }
@@ -233,32 +295,24 @@ class _ReorderableGridState extends State<_ReorderableGrid> {
index: index, index: index,
item: item, item: item,
isDragging: isDragging, isDragging: isDragging,
snapNow: _snapNow,
tileWidth: tileWidth, tileWidth: tileWidth,
tileHeight: tileHeight, tileHeight: tileHeight,
left: left, left: left,
top: top, top: top,
onDragStarted: () { onDragStarted: () {
setState(() => _draggingIndex = index); // ignore: avoid_print
print('[D&D] DragStarted: index=$index');
setState(() {
_draggingIndex = index;
_lastHoveredIndex = index;
});
}, },
onDragUpdate: (draggedIndex, targetIndex) { onDragUpdate: (draggedIndex, targetIndex) {
_updateHover(draggedIndex, targetIndex); _updateHover(draggedIndex, targetIndex);
}, },
onDragEnd: (draggedIndex, targetIndex) { onDragCompleted: (draggedIndex) {
if (draggedIndex != targetIndex) { _handleDragEnd(draggedIndex, _lastHoveredIndex);
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);
});
}, },
); );
}), }),
@@ -274,34 +328,37 @@ class _AnimatedGridItem extends StatelessWidget {
final int index; final int index;
final ActionButtonType item; final ActionButtonType item;
final bool isDragging; final bool isDragging;
final bool snapNow;
final double tileWidth; final double tileWidth;
final double tileHeight; final double tileHeight;
final double left; final double left;
final double top; final double top;
final VoidCallback onDragStarted; final VoidCallback onDragStarted;
final Function(int draggedIndex, int targetIndex) onDragUpdate; final Function(int draggedIndex, int targetIndex) onDragUpdate;
final Function(int draggedIndex, int targetIndex) onDragEnd; final Function(int draggedIndex) onDragCompleted;
final VoidCallback onDragCanceled;
const _AnimatedGridItem({ const _AnimatedGridItem({
super.key, super.key,
required this.index, required this.index,
required this.item, required this.item,
required this.isDragging, required this.isDragging,
required this.snapNow,
required this.tileWidth, required this.tileWidth,
required this.tileHeight, required this.tileHeight,
required this.left, required this.left,
required this.top, required this.top,
required this.onDragStarted, required this.onDragStarted,
required this.onDragUpdate, required this.onDragUpdate,
required this.onDragEnd, required this.onDragCompleted,
required this.onDragCanceled,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// ドロップ直後は全アイテムが 0ms でスナップ
final Duration animDuration = snapNow ? Duration.zero : const Duration(milliseconds: 150);
return AnimatedPositioned( return AnimatedPositioned(
duration: const Duration(milliseconds: 200), duration: animDuration,
curve: Curves.easeInOut, curve: Curves.easeInOut,
left: left, left: left,
top: top, top: top,
@@ -314,9 +371,6 @@ class _AnimatedGridItem extends StatelessWidget {
} }
return details.data != index; return details.data != index;
}, },
onAcceptWithDetails: (details) {
onDragEnd(details.data, index);
},
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
Widget child = _QuickActionTile(index: index, type: item); Widget child = _QuickActionTile(index: index, type: item);
@@ -342,8 +396,14 @@ class _AnimatedGridItem extends StatelessWidget {
), ),
childWhenDragging: const SizedBox.shrink(), childWhenDragging: const SizedBox.shrink(),
onDragStarted: onDragStarted, onDragStarted: onDragStarted,
onDragEnd: (_) => onDragCanceled(), onDragCompleted: () {
onDraggableCanceled: (_, __) => onDragCanceled(), // DragTargetに受け入れられた場合
onDragCompleted(index);
},
onDraggableCanceled: (_, __) {
// DragTarget外にドロップした場合
onDragCompleted(index);
},
child: child, child: child,
); );
}, },