Reorderable
repository·main·Indexed 23 days ago
https://github.com/calvin-ll/reorderableA Jetpack Compose and Compose Multiplatform library that adds drag-and-drop reordering capabilities to standard and lazy layout components, including LazyColumn, LazyRow, LazyVerticalGrid, LazyHorizontalGrid, LazyVerticalStaggeredGrid, LazyHorizontalStaggeredGrid, Column, and Row. It supports Android, iOS, Desktop/JVM, Wasm, and JS, featuring customizable drag handles, support for items of different sizes, and automatic scrolling when dragging near screen edges.
What's inside Reorderable
- Reorderable is a library for Jetpack Compose and Compose Multiplatform that enables drag-and-drop reordering of items within various layout components. It provides support for both standard layouts and lazy-loading lists/grids, allowing users to intuitively rearrange content via touch gestures.
Key features of Reorderable
mainReorderable is a Compose Multiplatform library designed for handling item reordering in lazy layouts. Key capabilities include:
- Multiplatform Support: Works with Android, iOS, Desktop/JVM, Wasm, and JS.
- Flexible Layouts: Supports items of different sizes, section headers, and footers.
- Customizable Reordering:
- Some items can be marked as non-reorderable.
- Supports using a specific child of an item as a drag handle.
- Supports both immediate dragging and long-press to start dragging.
- Advanced Animation & Scrolling:
- Uses
Modifier.animateItemfor smooth movement within lazy components. - Supports dragging and animating the first visible item.
- Automatic scrolling when dragging near the edge of the screen (note: scrolling is unavailable when using standard
ColumnorRowinstead of lazy components; scroll speed scales with distance from the edge).
- Uses
- Compatible Lazy Components: Works with
LazyColumn,LazyRow,LazyVerticalGrid,LazyHorizontalGrid,LazyVerticalStaggeredGrid, andLazyHorizontalStaggeredGrid.
Handle section headers and footers in reordering
mainWhen using
LazyVerticalStaggeredGridwith non-item elements like section headers or footers, thefrom.indexandto.indexprovided in theonMovelambda refer to the absolute indices within the grid. You must adjust these indices to account for the offset caused by headers/footers when updating your underlying data list.Example: If you have one header at index 0, you must subtract 1 from the indices to access the correct item in your list.
var list by remember { mutableStateOf(List(100) { "Item $it" }) } val lazyStaggeredGridState = rememberLazyStaggeredGridState() val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to -> list = list.toMutableList().apply { // Subtract 1 to account for the header at index 0 add(to.index - 1, removeAt(from.index - 1)) } } LazyVerticalStaggeredGrid( state = lazyStaggeredGridState, // ... ) { item { Text("Header") } items(list, key = { item -> item.id }) { ReorderableItem(reorderableLazyStaggeredGridState, item.id) { // ... } } }Handle section headers and footers in reorderable lists
mainWhen your
LazyRoworLazyColumncontains non-reorderable items like section headers or footers, thefrom.indexandto.indexprovided in theonMovecallback will reflect the absolute indices in the list. You must manually adjust these indices to map to your actual data list to avoid reordering the headers or causing index out of bounds errors.var list by remember { mutableStateOf(List(100) { "Item $it" }) } val lazyListState = rememberLazyListState() val reorderableLazyRowState = rememberReorderableLazyListState(lazyListState) { from, to -> // Adjusting indices because the first item in the LazyRow is a header list = list.toMutableList().apply { add(to.index - 1, removeAt(from.index - 1)) } } LazyRow( state = lazyListState, // ... ) { item { Text("Header") } items(list, key = { item -> item.id }) { item -> ReorderableItem(reorderableLazyRowState, item.id) { // ... } } }Supported Compose Layouts for Reordering
mainReorderable supports the following Jetpack Compose and Compose Multiplatform components:
Lazy Components:
LazyColumnLazyRowLazyVerticalGridLazyHorizontalGridLazyVerticalStaggeredGridLazyHorizontalStaggeredGrid
Standard Layouts:
ColumnRow
Adjust indices for Section Headers and Footers
mainThe
from.indexandto.indexprovided in theonMovecallback ofrememberReorderableLazyStaggeredGridStatecorrespond to the indices of the items within theLazyHorizontalStaggeredGrid. If your list contains non-reorderable items like section headers or footers, you must manually adjust the indices in your list update logic to account for their presence.var list by remember { mutableStateOf(List(100) { "Item $it" }) } val lazyStaggeredGridState = rememberLazyStaggeredGridState() val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to -> list = list.toMutableList().apply { // Adjusting for a header at index 0 add(to.index - 1, removeAt(from.index - 1)) } } LazyHorizontalStaggeredGrid( state = lazyStaggeredGridState, // ... ) { item { Text("Header") } items(list, key = { item -> item.id }) { item -> ReorderableItem(reorderableLazyStaggeredGridState, item.id) { // ... } } }Handle Section Headers and Footers in LazyColumn
mainWhen using
LazyColumnwith non-item elements like section headers or footers, thefrom.indexandto.indexprovided in theonMovelambda refer to the absolute indices in theLazyColumn. You must adjust these indices to match your data list indices. For example, if a header is at index 0, your list items start at index 1, so you may need to subtract 1 from the indices.var list by remember { mutableStateOf(List(100) { "Item $it" }) } val lazyListState = rememberLazyListState() val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to -> // Adjusting indices for a header at index 0 list = list.toMutableList().apply { add(to.index - 1, removeAt(from.index - 1)) } } LazyColumn( state = lazyListState, // ... ) { item { Text("Header") } items(list, key = { item -> item.id }) { item -> ReorderableItem(reorderableLazyColumnState, item.id) { // ... } } }Use Reorderable with Material3 Clickable Cards
mainTo use Material3's
Cardwith anonClickhandler alongside reordering, you must share aMutableInteractionSourcebetween theCardand the drag handle (Modifier.draggableHandleorModifier.longPressDraggableHandle). This allows theCardto correctly respond to drag events emitted by the handle.Steps:
- Create a
MutableInteractionSourceusingremember { MutableInteractionSource() }. - Pass this instance to the
Card'sinteractionSourceparameter. - Pass the same instance to the
Modifier.draggableHandle'sinteractionSourceparameter.
val interactionSource = remember { MutableInteractionSource() } Card( onClick = {}, interactionSource = interactionSource, ) { Row { Text(item, Modifier.padding(horizontal = 8.dp)) IconButton( modifier = Modifier.draggableHandle( onDragStarted = { hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate) }, onDragStopped = { hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd) }, interactionSource = interactionSource, ), onClick = {}, ) { Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder") } } }- Create a
Improve accessibility for reorderable lists
mainWhen making reorderable lists accessible for TalkBack users:
- If items only contain a drag handle, add custom actions like "Move Up", "Move Down", "Move Left", or "Move Right" using
SemanticsPropertyReceiver.customActions. - Apply
Modifier.clearAndSetSemanticsto the drag handle button to make it non-focusable for TalkBack, preventing redundant focus points.
- If items only contain a drag handle, add custom actions like "Move Up", "Move Down", "Move Left", or "Move Right" using
Install Reorderable using Version Catalog
mainTo use Reorderable with Gradle Version Catalogs, add the version and library definition to your
libs.versions.tomlfile, then implement it in your dependencies block.[versions] #... reorderable = "3.1.0" [libraries] #... reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }dependencies { // ... implementation(libs.reorderable) }Use Reorderable with LazyColumn
mainTo implement reordering in a
LazyColumn, follow these steps:- Create a
lazyListStateusingrememberLazyListState(). - Create a
reorderableLazyListStateusingrememberReorderableLazyListState(lazyListState) { from, to -> ... }. The lambda is where you update your underlying data source. - Pass the
lazyListStateto theLazyColumn. - Wrap your items in
ReorderableItem(reorderableLazyListState, key = /* item key */). - Use
Modifier.draggableHandle()on a component within theReorderableItemto act as the drag trigger.
val lazyListState = rememberLazyListState() val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> // Update the list } LazyColumn(state = lazyListState) { items(list, key = { /* item key */ }) { ReorderableItem(reorderableLazyListState, key = /* item key */) { isDragging -> // Item content IconButton( modifier = Modifier.draggableHandle(), /* ... */ ) } } }- Create a
Implement reordering in LazyVerticalGrid
mainReordering in a
LazyVerticalGridfollows a similar pattern toLazyRow. UserememberReorderableLazyGridState(passing theLazyGridState) and wrap items inReorderableItem.Note: The
onMovecallback provides indices that correspond to the grid positions.val lazyGridState = rememberLazyGridState() val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> // Update the list } LazyVerticalGrid(state = lazyGridState) { items(list, key = { /* item key */ }) { ReorderableItem(reorderableLazyGridState, key = /* item key */) { isDragging -> // Item content IconButton( modifier = Modifier.draggableHandle(), /* ... */ ) } } }