Reorderable is a simple library that allows you to reorder items in LazyColumn
and LazyRow
as well as Column
and Row
in Jetpack Compose with drag and drop.
- Supports items of different sizes
- Some items can be made non-reorderable
- Scrolls when dragging to the edge of the screen (only for
LazyColumn
andLazyRow
) The scroll speed is based on the distance from the edge of the screen - Uses the new
Modifier.animateItemPlacement
API to animate item movement inLazyColumn
andLazyRow
- Supports using a child of an item as the drag handle
Add the following to your build.gradle
file:
dependencies {
implementation("sh.calvin.reorderable:reorderable:1.1.0")
}
dependencies {
implementation 'sh.calvin.reorderable:reorderable:1.1.0'
}
See demo app code for more examples.
Find more examples in SimpleReorderableLazyColumnScreen.kt
and ComplexReorderableLazyColumnScreen.kt
in the demo app.
val lazyListState = rememberLazyListState()
val reorderableLazyColumnState = rememberReorderableLazyColumnState(lazyListState) { from, to ->
// Update the list
}
LazyColumn(state = lazyListState) {
items(list, key = { /* item key */ }) {
ReorderableItem(reorderableLazyColumnState, key = /* item key */) { isDragging ->
// Item content
IconButton(modifier = Modifier.draggableHandle(), /* ... */)
}
}
}
Since Modifier.draggableHandle
can only be used in ReorderableItemScope
, you may need to pass ReorderableItemScope
to a child composable. For example:
@Composable
fun List() {
// ...
LazyColumn(state = lazyListState) {
items(list, key = { /* item key */ }) {
ReorderableItem(reorderableLazyColumnState, key = /* item key */) { isDragging ->
// Item content
DragHandle(this)
}
}
}
}
@Composable
fun DragHandle(scope: ReorderableItemScope) {
IconButton(modifier = with(scope) { Modifier.draggableHandle() }, /* ... */)
}
Here's a more complete example with (with haptic feedback):
val view = LocalView.current
var list by remember { mutableStateOf(List(100) { "Item $it" }) }
val lazyListState = rememberLazyListState()
val reorderableLazyColumnState = rememberReorderableLazyColumnState(lazyListState) { from, to ->
list = list.toMutableList().apply {
add(to.index, removeAt(from.index))
}
view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK)
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(list, key = { it }) {
ReorderableItem(reorderableLazyColumnState, key = it) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Surface(shadowElevation = elevation) {
Row {
Text(it, Modifier.padding(horizontal = 8.dp))
IconButton(
modifier = Modifier.draggableHandle(
onDragStarted = {
view.performHapticFeedback(HapticFeedbackConstants.DRAG_START)
},
onDragStopped = {
view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END)
},
),
onClick = {},
) {
Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder")
}
}
}
}
}
}
Find more examples in ReorderableColumnScreen.kt
in the demo app.
ReorderableColumn(
list = list,
onSettle = { fromIndex, toIndex ->
// Update the list
},
) { index, item, isDragging ->
key(item.id) {
// Item content
IconButton(modifier = Modifier.draggableHandle(), /* ... */)
}
}
Since Modifier.draggableHandle
can only be used in ReorderableScope
, you may need to pass ReorderableScope
to a child composable. For example:
@Composable
fun List() {
// ...
ReorderableColumn(
list = list,
onSettle = { fromIndex, toIndex ->
// Update the list
},
) { index, item, isDragging ->
key(item.id) {
// Item content
DragHandle(this)
}
}
}
@Composable
fun DragHandle(scope: ReorderableScope) {
IconButton(modifier = with(scope) { Modifier.draggableHandle() }, /* ... */)
}
Here's a more complete example (with haptic feedback):
val view = LocalView.current
var list by remember { mutableStateOf(List(4) { "Item $it" }) }
ReorderableColumn(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
list = list,
onSettle = { fromIndex, toIndex ->
list = list.toMutableList().apply {
add(toIndex, removeAt(fromIndex))
}
},
onMove = {
view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK)
},
verticalArrangement = Arrangement.spacedBy(8.dp),
) { _, item, isDragging ->
key(item) {
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
Surface(shadowElevation = elevation) {
Row {
Text(item, Modifier.padding(horizontal = 8.dp))
IconButton(
modifier = Modifier.draggableHandle(
onDragStarted = {
view.performHapticFeedback(HapticFeedbackConstants.DRAG_START)
},
onDragStopped = {
view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END)
},
),
onClick = {},
) {
Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder")
}
}
}
}
}
See SimpleReorderableLazyRowScreen.kt
and ComplexReorderableLazyRowScreen.kt
in the demo app.
You can just replace Column
with Row
in the LazyColumn
examples above.
See ReorderableRowScreen.kt
in the demo app.
You can just replace Column
with Row
in the Column
examples above.
rememberReorderableLazyColumnState
rememberReorderableLazyRowState
ReorderableItem
Modifier.draggableHandle
Copyright 2023 Calvin Liang
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.