Auto Scroll¶
When dragging items in a scrollable container, you often need the container to scroll automatically as the user drags near its edges. Compose DND provides the dragAutoScroll modifier for this purpose.
Experimental API
The auto scroll API is annotated with @ExperimentalDndApi. This means it may change in future releases. You must opt in with @OptIn(ExperimentalDndApi::class) to use it.
LazyColumn / LazyRow¶
Use the dragAutoScroll modifier on a LazyColumn or LazyRow to enable automatic scrolling during drag operations:
@OptIn(ExperimentalDndApi::class)
@Composable
fun AutoScrollLazyColumnExample() {
val dndState = rememberDragAndDropState<String>()
val lazyListState = rememberLazyListState()
DragAndDropContainer(
state = dndState,
) {
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxSize()
.dragAutoScroll(
state = dndState,
lazyListState = lazyListState,
),
) {
// items...
}
}
}
LazyGrid¶
For LazyVerticalGrid or LazyHorizontalGrid, use the grid variant:
@OptIn(ExperimentalDndApi::class)
@Composable
fun AutoScrollLazyGridExample() {
val dndState = rememberDragAndDropState<String>()
val lazyGridState = rememberLazyGridState()
DragAndDropContainer(
state = dndState,
) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
state = lazyGridState,
modifier = Modifier
.fillMaxSize()
.dragAutoScroll(
state = dndState,
lazyGridState = lazyGridState,
),
) {
// items...
}
}
}
ScrollState (Column / Row)¶
For regular scrollable containers using ScrollState, pass the orientation parameter:
@OptIn(ExperimentalDndApi::class)
@Composable
fun AutoScrollColumnExample() {
val dndState = rememberDragAndDropState<String>()
val scrollState = rememberScrollState()
DragAndDropContainer(
state = dndState,
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.dragAutoScroll(
state = dndState,
scrollState = scrollState,
orientation = Orientation.Vertical,
),
) {
// items...
}
}
}
DragAutoScrollConfig¶
You can customize the auto scroll behavior using DragAutoScrollConfig:
@OptIn(ExperimentalDndApi::class)
val config = DragAutoScrollConfig(
minScrollThreshold = 48.dp,
maxScrollThreshold = 160.dp,
maxScrollSpeed = 1500f,
)
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxSize()
.dragAutoScroll(
state = dndState,
lazyListState = lazyListState,
config = config,
),
)
Configuration Options¶
| Parameter | Type | Default | Description |
|---|---|---|---|
minScrollThreshold | Dp | 48.dp | Minimum distance from the edge where scrolling begins. The effective threshold adapts to the first/last visible item size: clamp(itemSize, minScrollThreshold, maxScrollThreshold). |
maxScrollThreshold | Dp | 160.dp | Maximum distance from the edge for auto-scroll activation. Prevents the adaptive threshold from growing too large with tall items or containers. |
maxScrollSpeed | Float | 1500f | The maximum scroll speed in pixels per second at the very edge. |
How It Works¶
The auto scroll mechanism uses a dynamic threshold approach:
- Detection -- When a dragged item is within the scroll threshold distance from the container edge, scrolling begins.
- Quadratic Easing -- Scroll speed increases quadratically as the dragged item moves closer to the edge. This provides a natural feel where small movements near the threshold produce slow scrolling, while pushing toward the edge accelerates.
- Frame-Synced -- Scroll operations are synchronized with the frame rate for smooth, jank-free scrolling.
- Bidirectional -- Scrolling works in both directions: toward the start and toward the end of the container.
Scroll Pinning (dragScrollPin)¶
When reorderable items have different sizes, swapping two items changes the content size above the viewport. Compose's key-based scroll anchoring then adjusts the scroll position to compensate, which shows up as a visible jump during the drag.
The dragScrollPin modifier fixes this by pinning the scroll position right before each reorder swap, so the viewport stays stable:
@OptIn(ExperimentalDndApi::class)
@Composable
fun ScrollPinExample() {
val dndState = rememberDragAndDropState<String>()
val lazyListState = rememberLazyListState()
DragAndDropContainer(
state = dndState,
) {
LazyColumn(
state = lazyListState,
modifier = Modifier
.fillMaxSize()
.dragScrollPin(
state = dndState,
lazyListState = lazyListState,
),
) {
// items with varying heights...
}
}
}
A lazyGridState overload is available for LazyVerticalGrid / LazyHorizontalGrid.
Note
dragAutoScroll already includes the scroll pinning behavior, so you do not need both. Use dragScrollPin on its own only when you want jump-free reordering without automatic edge scrolling.
Full Working Example¶
@OptIn(ExperimentalDndApi::class)
@Composable
fun ReorderWithAutoScroll() {
val dndState = rememberDragAndDropState<String>()
var items by remember {
mutableStateOf(
(1..50).map { "Item $it" }
)
}
val lazyListState = rememberLazyListState()
DragAndDropContainer(
state = dndState,
modifier = Modifier.fillMaxSize().padding(16.dp),
) {
LazyColumn(
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxSize()
.dragAutoScroll(
state = dndState,
lazyListState = lazyListState,
),
) {
items(items, key = { it }) { item ->
val isDragging = dndState.isDragging(item)
ItemCard(
text = item,
modifier = Modifier
.graphicsLayer {
alpha = if (isDragging) 0f else 1f
}
.reorderableItem(
key = item,
data = item,
state = dndState,
onDragEnter = { state ->
items = items.toMutableList().apply {
val index = indexOf(item)
if (index != -1) {
remove(state.data)
add(index, state.data)
}
}
},
draggableContent = {
ItemCard(text = item)
},
)
.fillMaxWidth(),
)
}
}
}
}
@Composable
private fun ItemCard(
text: String,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier.height(56.dp),
) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
) {
Text(text = text)
}
}
}