mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
fix: improve drag-and-drop handling in _ReorderableGrid with enhanced visual feedback and snap animation
This commit is contained in:
+87
-27
@@ -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,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user