What You Will Build
A list of items that can be reordered by long-pressing and dragging. The dragged item elevates and scales up slightly for visual feedback, and items swap positions in real time as you drag over them. This is the pattern used in to-do apps, playlist editors, and settings screens.
Why This Pattern Matters
Drag-to-reorder is a fundamental mobile interaction. Implementing it from scratch teaches you detectDragGesturesAfterLongPress, offset-based position tracking, and animated elevation changes during gesture state transitions.
The Drag and Drop List
@Composable
fun DragDropScreen() {
val items = remember {
mutableStateListOf(
"Apples", "Bananas", "Cherries", "Dates",
"Elderberries", "Figs", "Grapes", "Honeydew"
)
}
var draggedIndex by remember { mutableIntStateOf(-1) }
var dragOffsetY by remember { mutableFloatStateOf(0f) }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text("Long press and drag to reorder",
color = Color.Gray, fontSize = 14.sp)
Spacer(modifier = Modifier.height(12.dp))
items.forEachIndexed { index, item ->
val isDragging = index == draggedIndex
val elevation by animateDpAsState(
if (isDragging) 12.dp else 2.dp, label = "elev"
)
val scale by animateFloatAsState(
if (isDragging) 1.05f else 1f, label = "scale"
)
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.zIndex(if (isDragging) 1f else 0f)
.offset {
IntOffset(0,
if (isDragging) dragOffsetY.roundToInt() else 0)
}
.scale(scale)
.shadow(elevation, RoundedCornerShape(12.dp))
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(
onDragStart = { draggedIndex = index },
onDrag = { change, offset ->
change.consume()
dragOffsetY += offset.y
val targetIndex =
(draggedIndex + (dragOffsetY / 72f)
.roundToInt())
.coerceIn(0, items.size - 1)
if (targetIndex != draggedIndex) {
val movedItem =
items.removeAt(draggedIndex)
items.add(targetIndex, movedItem)
draggedIndex = targetIndex
dragOffsetY = 0f
}
},
onDragEnd = {
draggedIndex = -1
dragOffsetY = 0f
},
onDragCancel = {
draggedIndex = -1
dragOffsetY = 0f
}
)
},
shape = RoundedCornerShape(12.dp),
tonalElevation = 2.dp
) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("\u2630", fontSize = 20.sp, color = Color.Gray)
Spacer(modifier = Modifier.width(16.dp))
Text(item, fontSize = 16.sp,
fontWeight = FontWeight.Medium)
}
}
}
}
}
Tips and Pitfalls
- 72f divisor in the swap calculation should approximate item height in pixels. Adjust to match your actual row height for accurate targeting.
- Reset dragOffsetY to 0f after each swap so the offset recalculates from the new position.
- Always call change.consume() to prevent the drag from propagating to parent scrollables.
- zIndex(1f) on the dragged item ensures it renders above sibling items during the drag.