Drag & Reorder Animation Implementation in Mobile Apps
Drag & Reorder lets users drag list items to change their order. User long-presses an item; the list understands intent (item rises and slightly scales); then drag works: other items move aside, showing insertion point.
Technically this is one of the most complex list animations: track drag in real time, compute target insertion position, animate neighboring elements without re-layout the entire list.
iOS: UICollectionView with Reordering
UICollectionView has built-in interactive reordering support via UICollectionViewDragDelegate and UICollectionViewDropDelegate (iOS 11+):
collectionView.dragDelegate = self
collectionView.dropDelegate = self
collectionView.dragInteractionEnabled = true
// UICollectionViewDragDelegate:
func collectionView(_ collectionView: UICollectionView,
itemsForBeginning session: UIDragSession,
at indexPath: IndexPath) -> [UIDragItem] {
let item = items[indexPath.item]
let itemProvider = NSItemProvider(object: item.id as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}
// UICollectionViewDropDelegate:
func collectionView(_ collectionView: UICollectionView,
performDropWith coordinator: UICollectionViewDropCoordinator) {
guard let destinationIndexPath = coordinator.destinationIndexPath,
let item = coordinator.items.first,
let sourceIndexPath = item.sourceIndexPath else { return }
collectionView.performBatchUpdates {
items.move(fromOffsets: IndexSet(integer: sourceIndexPath.item),
toOffset: destinationIndexPath.item)
collectionView.moveItem(at: sourceIndexPath, to: destinationIndexPath)
}
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}
UICollectionView automatically animates neighboring cell movement—this is the most valuable part. No manual offset calculation and animation for each element.
For UITableView, use UITableViewDragDelegate/UITableViewDropDelegate, or the older editingStyle approach:
tableView.isEditing = true
// Delegate method:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
items.move(fromOffsets: IndexSet(integer: sourceIndexPath.row),
toOffset: destinationIndexPath.row)
}
In SwiftUI, use List with .onMove:
List {
ForEach(items) { item in
ItemRow(item: item)
}
.onMove { source, destination in
items.move(fromOffsets: source, toOffset: destination)
}
}
.environment(\.editMode, .constant(.active))
.onMove adds standard drag handles. For custom long-press drag without edit mode, use DragGesture with .onChanged and .onEnded, plus manual target index computation. More complex but gives full control over appearance.
Android Compose: LazyColumn + reorderable
Compose has no built-in reorder—use library sh.calvin.reorderable:reorderable:2.4.0:
val listState = rememberLazyListState()
var list by remember { mutableStateOf(items) }
val reorderState = rememberReorderableLazyListState(listState) { from, to ->
list = list.toMutableList().apply { add(to.index, removeAt(from.index)) }
}
LazyColumn(
state = listState,
modifier = Modifier.reorderable(reorderState)
) {
items(list, key = { it.id }) { item ->
ReorderableItem(reorderState, key = item.id) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
val scale by animateFloatAsState(if (isDragging) 1.05f else 1f)
Card(
modifier = Modifier
.fillMaxWidth()
.scale(scale)
.shadow(elevation),
) {
Row {
Text(item.title, modifier = Modifier.weight(1f).padding(16.dp))
Icon(
Icons.Default.DragHandle,
contentDescription = null,
modifier = Modifier
.detectReorderAfterLongPress(reorderState) // or draggableHandle()
.padding(16.dp)
)
}
}
}
}
}
isDragging enables animation of raised element—scale and shadow via animateFloatAsState and animateDpAsState. Other elements animate automatically through LazyColumn's item placement animation.
For custom spacing animation—use Modifier.animateItem() on Compose 1.7+:
items(list, key = { it.id }) { item ->
ItemRow(item, modifier = Modifier.animateItem())
}
animateItem() automatically animates appearance, disappearance, and offset changes in LazyColumn when the list changes.
Flutter
// pubspec: reorderable_list: ^0.2.2 or built-in ReorderableListView
ReorderableListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
key: ValueKey(items[index].id),
title: Text(items[index].title),
trailing: ReorderableDragStartListener(
index: index,
child: const Icon(Icons.drag_handle),
),
);
},
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex--;
final item = items.removeAt(oldIndex);
items.insert(newIndex, item);
});
},
proxyDecorator: (child, index, animation) {
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
final scale = Tween<double>(begin: 1.0, end: 1.05)
.evaluate(CurvedAnimation(parent: animation, curve: Curves.easeOut));
return Transform.scale(scale: scale, child: child);
},
child: child,
);
},
)
proxyDecorator is a replacement widget for the dragged element. Use it to add scale and shadow effects without changing the original ListTile.
Common Mistakes
Lists without key on elements—on reorder, framework can't match old and new positions; animation jumps or breaks. key: ValueKey(item.id) is mandatory.
Mutating list without notifying framework—in Compose use mutableStateOf with list.toMutableList() before mutation. In Flutter use setState. Without this, UI doesn't update.
Saving new order: after onReorder, immediately send new order to backend or local storage. If user leaves—order is saved. Optimistic update: apply to UI immediately, rollback on network error.
Timeline
Drag & Reorder for simple list via built-in API (UITableView, ReorderableListView): half a day. Custom drag with animated proxy, neighbor spacing, and storage persistence: 1–2 days. Cost is calculated individually.







