From 3d0f5283e988bebfe9fa3f15169a2d783d622d70 Mon Sep 17 00:00:00 2001 From: Calvin Liang Date: Wed, 28 Aug 2024 19:24:23 -0700 Subject: [PATCH] update README --- README.md | 1146 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 1018 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index a1c4171..e87fde2 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ The latest demo app APK can be found in the [releases](https://github.com/Calvin - Supports items of different sizes - Some items can be made non-reorderable - Supports dragging immediately or long press to start dragging +- Supports section headers and footers - Scrolls when dragging to the edge of the screen. (unavailable for [`Column`]() and [`Row`]()) The scroll speed is based on the distance from the edge of the screen. - Uses the new [`Modifier.animateItemPlacement`]() API to animate item movement in [`LazyColumn`](), [`LazyRow`](), [`LazyVerticalGrid`](), [`LazyHorizontalGrid`](), [`LazyVerticalStaggeredGrid`](), and [`LazyHorizontalStaggeredGrid`]() - Supports using a child of an item as the drag handle @@ -106,6 +107,8 @@ See [demo app code](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderab Find more examples in [`SimpleReorderableLazyColumnScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyColumnScreen.kt), [`SimpleLongPressHandleReorderableLazyColumnScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleLongPressHandleReorderableLazyColumnScreen.kt) and [`ComplexReorderableLazyColumnScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/ComplexReorderableLazyColumnScreen.kt) in the demo app. +##### Simple Example + To use this library with [`LazyColumn`](), follow this basic structure: ```kotlin @@ -129,6 +132,88 @@ LazyColumn(state = lazyListState) { ``` +##### Complete Example (with haptic feedback) + +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. + +```kotlin +val view = LocalView.current + +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyListState = rememberLazyListState() +val reorderableLazyListState = rememberReorderableLazyListState(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(reorderableLazyListState, 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") + } + } + } + } + } +} +``` + +##### Section Headers and Footers or Multiple Lists + +The `from.index` and `to.index` in `onMove` are the indices of the items in the `LazyColumn`. If you have section headers and footers, you need to adjust the indices accordingly. For example: + +```kotlin +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyListState = rememberLazyListState() +val reorderableLazyColumnState = rememberReorderableLazyListState(lazyListState) { from, to -> + 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) { + // ... + } + } +} +``` + +##### Passing `Modifier.draggableHandle` to a Child Composable + Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableCollectionItemScope`, you may need to pass `ReorderableCollectionItemScope` to a child composable. For example: ```kotlin @@ -158,6 +243,8 @@ fun DragHandle(scope: ReorderableCollectionItemScope) { } ``` +##### Scroll Trigger Padding + If your [`LazyColumn`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyListState` to move the scroll trigger area out from under the navigation bar or notification bar. ```kotlin @@ -169,7 +256,9 @@ val reorderableLazyListState = rememberReorderableLazyListState( } ``` -Here's a more complete example with (with haptic feedback): +##### Use with [`Card`]() + +If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: ```kotlin val view = LocalView.current @@ -188,15 +277,18 @@ LazyColumn( modifier = Modifier.fillMaxSize(), state = lazyListState, contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - items(list, key = { it }) { - ReorderableItem(reorderableLazyListState, key = it) { isDragging -> - val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp) + items(list, key = { it }) { item -> + ReorderableItem(reorderableLazyListState, key = item) { + val interactionSource = remember { MutableInteractionSource() } - Surface(shadowElevation = elevation) { + Card( + onClick = {}, + interactionSource = interactionSource, + ) { Row { - Text(it, Modifier.padding(horizontal = 8.dp)) + Text(item, Modifier.padding(horizontal = 8.dp)) IconButton( modifier = Modifier.draggableHandle( onDragStarted = { @@ -205,6 +297,7 @@ LazyColumn( onDragStopped = { view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) }, + interactionSource = interactionSource, ), onClick = {}, ) { @@ -217,7 +310,39 @@ LazyColumn( } ``` -If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: +#### LazyRow + +See [`SimpleReorderableLazyRowScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyRowScreen.kt) and [`ComplexReorderableLazyRowScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/ComplexReorderableLazyRowScreen.kt) in the demo app. + +##### Simple Example + +To use this library with [`LazyRow`](), follow this basic structure: + +```kotlin +val lazyListState = rememberLazyListState() +val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> + // Update the list +} + +LazyRow(state = lazyListState) { + items(list, key = { /* item key */ }) { + ReorderableItem(reorderableLazyListState, key = /* item key */) { isDragging -> + // Item content + + IconButton( + modifier = Modifier.draggableHandle(), + /* ... */ + ) + } + } +} + +``` + +##### Complete Example (with haptic feedback) + +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. ```kotlin val view = LocalView.current @@ -232,22 +357,19 @@ val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) } -LazyColumn( +LazyRow( modifier = Modifier.fillMaxSize(), state = lazyListState, contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - items(list, key = { it }) { item -> - ReorderableItem(reorderableLazyListState, key = item) { - val interactionSource = remember { MutableInteractionSource() } + items(list, key = { it }) { + ReorderableItem(reorderableLazyListState, key = it) { isDragging -> + val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp) - Card( - onClick = {}, - interactionSource = interactionSource, - ) { - Row { - Text(item, Modifier.padding(horizontal = 8.dp)) + Surface(shadowElevation = elevation) { + Column { + Text(it, Modifier.padding(vertical = 8.dp)) IconButton( modifier = Modifier.draggableHandle( onDragStarted = { @@ -256,7 +378,6 @@ LazyColumn( onDragStopped = { view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) }, - interactionSource = interactionSource, ), onClick = {}, ) { @@ -269,49 +390,47 @@ LazyColumn( } ``` -#### LazyRow - -See [`SimpleReorderableLazyRowScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyRowScreen.kt) and [`ComplexReorderableLazyRowScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/ComplexReorderableLazyRowScreen.kt) in the demo app. - -You can just replace `Column` with `Row` in the `LazyColumn` examples above. - -#### [`LazyVerticalGrid`]() +##### Section Headers and Footers or Multiple Lists -Find more examples in [`SimpleReorderableLazyVerticalGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyVerticalGridScreen.kt) in the demo app. - -To use this library with [`LazyVerticalGrid`](), follow this basic structure: +The `from.index` and `to.index` in `onMove` are the indices of the items in the `LazyRow`. If you have section headers and footers, you need to adjust the indices accordingly. For example: ```kotlin -val lazyGridState = rememberLazyGridState() -val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> - // Update the list +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyListState = rememberLazyListState() +val reorderableLazyRowState = rememberReorderableLazyListState(lazyListState) { from, to -> + list = list.toMutableList().apply { + add(to.index - 1, removeAt(from.index - 1)) + } } -LazyVerticalGrid(state = lazyGridState) { - items(list, key = { /* item key */ }) { - ReorderableItem(reorderableLazyGridState, key = /* item key */) { isDragging -> - // Item content +LazyRow( + state = lazyListState, + // ... +) { + item { + Text("Header") + } - IconButton( - modifier = Modifier.draggableHandle(), - /* ... */ - ) + items(list, key = { item -> item.id }) { item -> + ReorderableItem(reorderableLazyRowState, item.id) { + // ... } } } - ``` +##### Passing `Modifier.draggableHandle` to a Child Composable + Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableCollectionItemScope`, you may need to pass `ReorderableCollectionItemScope` to a child composable. For example: ```kotlin @Composable -fun Grid() { +fun List() { // ... - LazyVerticalGrid(state = lazyGridState) { - items(Grid, key = { /* item key */ }) { - ReorderableItem(reorderableLazyGridState, key = /* item key */) { isDragging -> + LazyRow(state = lazyListState) { + items(list, key = { /* item key */ }) { + ReorderableItem(reorderableLazyListState, key = /* item key */) { isDragging -> // Item content DragHandle(this) @@ -331,25 +450,29 @@ fun DragHandle(scope: ReorderableCollectionItemScope) { } ``` -If your [`LazyVerticalGrid`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyGridState` to move the scroll trigger area out from under the navigation bar or notification bar. +##### Scroll Trigger Padding + +If your [`LazyRow`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyListState` to move the scroll trigger area out from under the navigation bar or notification bar. ```kotlin -val reorderableLazyGridState = rememberReorderableLazyGridState( - lazyGridState = lazyGridState, +val reorderableLazyListState = rememberReorderableLazyListState( + lazyListState = lazyListState, scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(), ) { from, to -> ... } ``` -Here's a more complete example with (with haptic feedback): +##### Use with [`Card`]() + +If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: ```kotlin val view = LocalView.current var list by remember { mutableStateOf(List(100) { "Item $it" }) } -val lazyGridState = rememberLazyGridState() -val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> +val lazyListState = rememberLazyListState() +val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> list = list.toMutableList().apply { add(to.index, removeAt(from.index)) } @@ -357,21 +480,22 @@ val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) } -LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 96.dp), +LazyRow( modifier = Modifier.fillMaxSize(), - state = lazyGridState, + state = lazyListState, contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - items(list, key = { it }) { - ReorderableItem(reorderableLazyGridState, key = it) { isDragging -> - val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp) + items(list, key = { it }) { item -> + ReorderableItem(reorderableLazyListState, key = item) { + val interactionSource = remember { MutableInteractionSource() } - Surface(shadowElevation = elevation) { - Row { - Text(it, Modifier.padding(horizontal = 8.dp)) + Card( + onClick = {}, + interactionSource = interactionSource, + ) { + Column { + Text(item, Modifier.padding(vertical = 8.dp)) IconButton( modifier = Modifier.draggableHandle( onDragStarted = { @@ -380,6 +504,7 @@ LazyVerticalGrid( onDragStopped = { view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) }, + interactionSource = interactionSource, ), onClick = {}, ) { @@ -392,7 +517,39 @@ LazyVerticalGrid( } ``` -If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: +#### [`LazyVerticalGrid`]() + +Find more examples in [`SimpleReorderableLazyVerticalGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyVerticalGridScreen.kt) in the demo app. + +##### Simple Example + +To use this library with [`LazyVerticalGrid`](), follow this basic structure: + +```kotlin +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(), + /* ... */ + ) + } + } +} + +``` + +##### Complete Example (with haptic feedback) + +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. ```kotlin val view = LocalView.current @@ -415,16 +572,13 @@ LazyVerticalGrid( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - items(list, key = { it }) { item -> - ReorderableItem(reorderableLazyGridState, key = item) { - val interactionSource = remember { MutableInteractionSource() } + items(list, key = { it }) { + ReorderableItem(reorderableLazyGridState, key = it) { isDragging -> + val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp) - Card( - onClick = {}, - interactionSource = interactionSource, - ) { + Surface(shadowElevation = elevation) { Row { - Text(item, Modifier.padding(horizontal = 8.dp)) + Text(it, Modifier.padding(horizontal = 8.dp)) IconButton( modifier = Modifier.draggableHandle( onDragStarted = { @@ -433,7 +587,6 @@ LazyVerticalGrid( onDragStopped = { view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) }, - interactionSource = interactionSource, ), onClick = {}, ) { @@ -446,17 +599,353 @@ LazyVerticalGrid( } ``` -#### [`LazyHorizontalGrid`]() - -Find more examples in [`SimpleReorderableLazyHorizontalGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyHorizontalGridScreen.kt) in the demo app. - -You can just replace `LazyVerticalStaggeredGrid` with `LazyHorizontalGrid` in the `LazyVerticalStaggeredGrid` examples above. - -#### [`LazyVerticalStaggeredGrid`]() - -Find more examples in [`SimpleReorderableLazyVerticalStaggeredGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyVerticalStaggeredGridScreen.kt) in the demo app. +##### Section Headers and Footers or Multiple Lists -To use this library with [`LazyVerticalStaggeredGrid`](), follow this basic structure: +The `from.index` and `to.index` in `onMove` are the indices of the items in the `LazyVerticalGrid`. If you have section headers and footers, you need to adjust the indices accordingly. For example: + +```kotlin +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyGridState = rememberLazyGridState() +val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index - 1, removeAt(from.index - 1)) + } +} + +LazyVerticalGrid( + state = lazyGridState, + // ... +) { + item { + Text("Header") + } + + items(list, key = { item -> item.id }) { item -> + ReorderableItem(reorderableLazyGridState, item.id) { + // ... + } + } +} +``` + +##### Passing `Modifier.draggableHandle` to a Child Composable + +Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableCollectionItemScope`, you may need to pass `ReorderableCollectionItemScope` to a child composable. For example: + +```kotlin +@Composable +fun Grid() { + // ... + + LazyVerticalGrid(state = lazyGridState) { + items(Grid, key = { /* item key */ }) { + ReorderableItem(reorderableLazyGridState, key = /* item key */) { isDragging -> + // Item content + + DragHandle(this) + } + } + } +} + +@Composable +fun DragHandle(scope: ReorderableCollectionItemScope) { + IconButton( + modifier = with(scope) { + Modifier.draggableHandle() + }, + /* ... */ + ) +} +``` + +##### Scroll Trigger Padding + +If your [`LazyVerticalGrid`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyGridState` to move the scroll trigger area out from under the navigation bar or notification bar. + +```kotlin +val reorderableLazyGridState = rememberReorderableLazyGridState( + lazyGridState = lazyGridState, + scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(), +) { from, to -> + ... +} +``` + +##### Use with [`Card`]() + +If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: + +```kotlin +val view = LocalView.current + +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyGridState = rememberLazyGridState() +val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + + view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) +} + +LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 96.dp), + modifier = Modifier.fillMaxSize(), + state = lazyGridState, + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), +) { + items(list, key = { it }) { item -> + ReorderableItem(reorderableLazyGridState, key = item) { + val interactionSource = remember { MutableInteractionSource() } + + Card( + onClick = {}, + interactionSource = interactionSource, + ) { + Row { + Text(item, Modifier.padding(horizontal = 8.dp)) + IconButton( + modifier = Modifier.draggableHandle( + onDragStarted = { + view.performHapticFeedback(HapticFeedbackConstants.DRAG_START) + }, + onDragStopped = { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) + }, + interactionSource = interactionSource, + ), + onClick = {}, + ) { + Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder") + } + } + } + } + } +} +``` + +#### [`LazyHorizontalGrid`]() + +Find more examples in [`SimpleReorderableLazyHorizontalGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyHorizontalGridScreen.kt) in the demo app. + +##### Simple Example + +To use this library with [`LazyHorizontalGrid`](), follow this basic structure: + +```kotlin +val lazyGridState = rememberLazyGridState() +val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + // Update the list +} + +LazyHorizontalGrid(state = lazyGridState) { + items(list, key = { /* item key */ }) { + ReorderableItem(reorderableLazyGridState, key = /* item key */) { isDragging -> + // Item content + + IconButton( + modifier = Modifier.draggableHandle(), + /* ... */ + ) + } + } +} + +``` + +##### Complete Example (with haptic feedback) + +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. + +```kotlin +val view = LocalView.current + +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyGridState = rememberLazyGridState() +val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + + view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) +} + +LazyHorizontalGrid( + rows = GridCells.Adaptive(minSize = 96.dp), + modifier = Modifier.fillMaxSize(), + state = lazyGridState, + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), +) { + items(list, key = { it }) { + ReorderableItem(reorderableLazyGridState, 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") + } + } + } + } + } +} +``` + +##### Section Headers and Footers or Multiple Lists + +The `from.index` and `to.index` in `onMove` are the indices of the items in the `LazyHorizontalGrid`. If you have section headers and footers, you need to adjust the indices accordingly. For example: + +```kotlin +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyGridState = rememberLazyGridState() +val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index - 1, removeAt(from.index - 1)) + } +} + +LazyHorizontalGrid( + state = lazyGridState, + // ... +) { + item { + Text("Header") + } + + items(list, key = { item -> item.id }) { item -> + ReorderableItem(reorderableLazyGridState, item.id) { + // ... + } + } +} +``` + +##### Passing `Modifier.draggableHandle` to a Child Composable + +Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableCollectionItemScope`, you may need to pass `ReorderableCollectionItemScope` to a child composable. For example: + +```kotlin +@Composable +fun Grid() { + // ... + + LazyHorizontalGrid(state = lazyGridState) { + items(Grid, key = { /* item key */ }) { + ReorderableItem(reorderableLazyGridState, key = /* item key */) { isDragging -> + // Item content + + DragHandle(this) + } + } + } +} + +@Composable +fun DragHandle(scope: ReorderableCollectionItemScope) { + IconButton( + modifier = with(scope) { + Modifier.draggableHandle() + }, + /* ... */ + ) +} +``` + +##### Scroll Trigger Padding + +If your [`LazyHorizontalGrid`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyGridState` to move the scroll trigger area out from under the navigation bar or notification bar. + +```kotlin +val reorderableLazyGridState = rememberReorderableLazyGridState( + lazyGridState = lazyGridState, + scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(), +) { from, to -> + ... +} +``` + +##### Use with [`Card`]() + +If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: + +```kotlin +val view = LocalView.current + +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyGridState = rememberLazyGridState() +val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + + view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) +} + +LazyHorizontalGrid( + rows = GridCells.Adaptive(minSize = 96.dp), + modifier = Modifier.fillMaxSize(), + state = lazyGridState, + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), +) { + items(list, key = { it }) { item -> + ReorderableItem(reorderableLazyGridState, key = item) { + val interactionSource = remember { MutableInteractionSource() } + + Card( + onClick = {}, + interactionSource = interactionSource, + ) { + Row { + Text(item, Modifier.padding(horizontal = 8.dp)) + IconButton( + modifier = Modifier.draggableHandle( + onDragStarted = { + view.performHapticFeedback(HapticFeedbackConstants.DRAG_START) + }, + onDragStopped = { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) + }, + interactionSource = interactionSource, + ), + onClick = {}, + ) { + Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder") + } + } + } + } + } +} +``` + +#### [`LazyVerticalStaggeredGrid`]() + +Find more examples in [`SimpleReorderableLazyVerticalStaggeredGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyVerticalStaggeredGridScreen.kt) in the demo app. + +##### Simple Example + +To use this library with [`LazyVerticalStaggeredGrid`](), follow this basic structure: ```kotlin val lazyStaggeredGridState = rememberLazyStaggeredGridState() @@ -479,6 +968,90 @@ LazyVerticalStaggeredGrid(state = lazyStaggeredGridState) { ``` +##### Complete Example (with haptic feedback) + +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. + +```kotlin +val view = LocalView.current + +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyStaggeredGridState = rememberLazyStaggeredGridState() +val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + + view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) +} + +LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(minSize = 96.dp), + modifier = Modifier.fillMaxSize(), + state = lazyStaggeredGridState, + contentPadding = PaddingValues(8.dp), + verticalItemSpacing = 8.dp, + horizontalArrangement = Arrangement.spacedBy(8.dp), +) { + items(list, key = { it }) { + ReorderableItem(reorderableLazyStaggeredGridState, 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") + } + } + } + } + } +} +``` + +##### Section Headers and Footers or Multiple Lists + +The `from.index` and `to.index` in `onMove` are the indices of the items in the `LazyVerticalStaggeredGrid`. If you have section headers and footers, you need to adjust the indices accordingly. For example: + +```kotlin +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyStaggeredGridState = rememberLazyStaggeredGridState() +val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index - 1, removeAt(from.index - 1)) + } +} + +LazyVerticalStaggeredGrid( + state = lazyStaggeredGridState, + // ... +) { + item { + Text("Header") + } + + items(list, key = { item -> item.id }) { item -> + ReorderableItem(reorderableLazyStaggeredGridState, item.id) { + // ... + } + } +} +``` + +##### Passing `Modifier.draggableHandle` to a Child Composable + Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableCollectionItemScope`, you may need to pass `ReorderableCollectionItemScope` to a child composable. For example: ```kotlin @@ -497,29 +1070,119 @@ fun Grid() { } } -@Composable -fun DragHandle(scope: ReorderableCollectionItemScope) { - IconButton( - modifier = with(scope) { - Modifier.draggableHandle() - }, - /* ... */ - ) -} +@Composable +fun DragHandle(scope: ReorderableCollectionItemScope) { + IconButton( + modifier = with(scope) { + Modifier.draggableHandle() + }, + /* ... */ + ) +} +``` + +##### Scroll Trigger Padding + +If your [`LazyVerticalStaggeredGrid`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyStaggeredGridState` to move the scroll trigger area out from under the navigation bar or notification bar. + +```kotlin +val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState( + lazyStaggeredGridState = lazyStaggeredGridState, + scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(), +) { from, to -> + ... +} +``` + +##### Use with [`Card`]() + +If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: + +```kotlin +val view = LocalView.current + +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyStaggeredGridState = rememberLazyStaggeredGridState() +val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to -> + list = list.toMutableList().apply { + add(to.index, removeAt(from.index)) + } + + view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) +} + +LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(minSize = 96.dp), + modifier = Modifier.fillMaxSize(), + state = lazyStaggeredGridState, + contentPadding = PaddingValues(8.dp), + verticalItemSpacing = 8.dp, + horizontalArrangement = Arrangement.spacedBy(8.dp), +) { + items(list, key = { it }) { item -> + ReorderableItem(reorderableLazyStaggeredGridState, key = item) { + val interactionSource = remember { MutableInteractionSource() } + + Card( + onClick = {}, + interactionSource = interactionSource, + ) { + Row { + Text(item, Modifier.padding(horizontal = 8.dp)) + IconButton( + modifier = Modifier.draggableHandle( + onDragStarted = { + view.performHapticFeedback(HapticFeedbackConstants.DRAG_START) + }, + onDragStopped = { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) + }, + interactionSource = interactionSource, + ), + onClick = {}, + ) { + Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder") + } + } + } + } + } +} +``` + +#### [`LazyHorizontalStaggeredGrid`]() + +Find more examples in [`SimpleReorderableLazyHorizontalStaggeredGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyHorizontalStaggeredGridScreen.kt) in the demo app. + +##### Simple Example + +To use this library with [`LazyHorizontalStaggeredGrid`](), follow this basic structure: + +```kotlin +val lazyStaggeredGridState = rememberLazyStaggeredGridState() +val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to -> + // Update the list +} + +LazyHorizontalStaggeredGrid(state = lazyStaggeredGridState) { + items(list, key = { /* item key */ }) { + ReorderableItem(reorderableLazyStaggeredGridState, key = /* item key */) { isDragging -> + // Item content + + IconButton( + modifier = Modifier.draggableHandle(), + /* ... */ + ) + } + } +} + ``` -If your [`LazyVerticalStaggeredGrid`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyStaggeredGridState` to move the scroll trigger area out from under the navigation bar or notification bar. - -```kotlin -val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState( - lazyStaggeredGridState = lazyStaggeredGridState, - scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(), -) { from, to -> - ... -} -``` +##### Complete Example (with haptic feedback) -Here's a more complete example with (with haptic feedback): +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. ```kotlin val view = LocalView.current @@ -534,13 +1197,13 @@ val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridStat view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) } -LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(minSize = 96.dp), +LazyHorizontalStaggeredGrid( + rows = StaggeredGridCells.Adaptive(minSize = 96.dp), modifier = Modifier.fillMaxSize(), state = lazyStaggeredGridState, contentPadding = PaddingValues(8.dp), - verticalItemSpacing = 8.dp, - horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalItemSpacing = 8.dp, ) { items(list, key = { it }) { ReorderableItem(reorderableLazyStaggeredGridState, key = it) { isDragging -> @@ -569,6 +1232,81 @@ LazyVerticalStaggeredGrid( } ``` +##### Section Headers and Footers or Multiple Lists + +The `from.index` and `to.index` in `onMove` are the indices of the items in the `LazyHorizontalStaggeredGrid`. If you have section headers and footers, you need to adjust the indices accordingly. For example: + +```kotlin +var list by remember { mutableStateOf(List(100) { "Item $it" }) } +val lazyStaggeredGridState = rememberLazyStaggeredGridState() +val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState(lazyStaggeredGridState) { from, to -> + list = list.toMutableList().apply { + 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) { + // ... + } + } +} +``` + +##### Passing `Modifier.draggableHandle` to a Child Composable + +Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableCollectionItemScope`, you may need to pass `ReorderableCollectionItemScope` to a child composable. For example: + +```kotlin +@Composable +fun Grid() { + // ... + + LazyHorizontalStaggeredGrid(state = lazyStaggeredGridState) { + items(list, key = { /* item key */ }) { + ReorderableItem(reorderableLazyStaggeredGridState, key = /* item key */) { isDragging -> + // Item content + + DragHandle(this) + } + } + } +} + +@Composable +fun DragHandle(scope: ReorderableCollectionItemScope) { + IconButton( + modifier = with(scope) { + Modifier.draggableHandle() + }, + /* ... */ + ) +} +``` + +##### Scroll Trigger Padding + +If your [`LazyHorizontalStaggeredGrid`]() displays under navigation bar or notification bar, you may want to add `scrollThresholdPadding` to `rememberReorderableLazyStaggeredGridState` to move the scroll trigger area out from under the navigation bar or notification bar. + +```kotlin +val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridState( + lazyStaggeredGridState = lazyStaggeredGridState, + scrollThresholdPadding = WindowInsets.systemBars.asPaddingValues(), +) { from, to -> + ... +} +``` + +##### Use with [`Card`]() + If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: ```kotlin @@ -584,13 +1322,13 @@ val reorderableLazyStaggeredGridState = rememberReorderableLazyStaggeredGridStat view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) } -LazyVerticalStaggeredGrid( - columns = StaggeredGridCells.Adaptive(minSize = 96.dp), +LazyHorizontalStaggeredGrid( + rows = StaggeredGridCells.Adaptive(minSize = 96.dp), modifier = Modifier.fillMaxSize(), state = lazyStaggeredGridState, contentPadding = PaddingValues(8.dp), - verticalItemSpacing = 8.dp, - horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalItemSpacing = 8.dp, ) { items(list, key = { it }) { item -> ReorderableItem(reorderableLazyStaggeredGridState, key = item) { @@ -623,16 +1361,12 @@ LazyVerticalStaggeredGrid( } ``` -#### [`LazyHorizontalStaggeredGrid`]() - -Find more examples in [`SimpleReorderableLazyHorizontalStaggeredGridScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/SimpleReorderableLazyHorizontalStaggeredGridScreen.kt) in the demo app. - -You can just replace `LazyVerticalStaggeredGrid` with `LazyHorizontalStaggeredGrid` in the `LazyVerticalStaggeredGrid` examples above. - #### [`Column`]() Find more examples in [`ReorderableColumnScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/ReorderableColumnScreen.kt) and [`LongPressHandleReorderableColumnScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/LongPressHandleReorderableColumnScreen.kt) in the demo app. +##### Simple Example + To use this library with [`Column`](), follow this basic structure: ```kotlin @@ -650,6 +1384,58 @@ ReorderableColumn( } ``` +##### Complete Example (with haptic feedback) + +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. + +```kotlin +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") + } + } + } + } +} +``` + +##### Passing `Modifier.draggableHandle` to a Child Composable + Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableScope`, you may need to pass `ReorderableScope` to a child composable. For example: ```kotlin @@ -677,7 +1463,9 @@ fun DragHandle(scope: ReorderableScope) { } ``` -Here's a more complete example (with haptic feedback): +##### Use with [`Card`]() + +If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: ```kotlin val view = LocalView.current @@ -698,13 +1486,90 @@ ReorderableColumn( view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) }, verticalArrangement = Arrangement.spacedBy(8.dp), +) { _, item, _ -> + key(item) { + val interactionSource = remember { MutableInteractionSource() } + + Card( + onClick = {}, + interactionSource = interactionSource, + ) { + Row { + Text(item, Modifier.padding(horizontal = 8.dp)) + IconButton( + modifier = Modifier.draggableHandle( + onDragStarted = { + view.performHapticFeedback(HapticFeedbackConstants.DRAG_START) + }, + onDragStopped = { + view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END) + }, + interactionSource = interactionSource, + ), + onClick = {}, + ) { + Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder") + } + } + } + } +} +``` + +#### [`Row`]() + +See [`ReorderableRowScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/ReorderableRowScreen.kt) in the demo app. + +##### Simple Example + +To use this library with [`Row`](), follow this basic structure: + +```kotlin +ReorderableRow( + list = list, + onSettle = { fromIndex, toIndex -> + // Update the list + }, +) { index, item, isDragging -> + key(item.id) { + // Item content + + IconButton(modifier = Modifier.draggableHandle(), /* ... */) + } +} +``` + +##### Complete Example (with haptic feedback) + +> [!NOTE] +> `val view = LocalView.current` and `View.performHapticFeedback` are only available in Android. Comment out these lines if you are using this library in a multiplatform project. + +```kotlin +val view = LocalView.current + +var list by remember { mutableStateOf(List(4) { "Item $it" }) } + +ReorderableRow( + 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)) + Column { + Text(item, Modifier.padding(vertical = 8.dp)) IconButton( modifier = Modifier.draggableHandle( onDragStarted = { @@ -724,6 +1589,37 @@ ReorderableColumn( } ``` +##### Passing `Modifier.draggableHandle` to a Child Composable + +Since `Modifier.draggableHandle` and `Modifier.longPressDraggableHandle` can only be used in `ReorderableScope`, you may need to pass `ReorderableScope` to a child composable. For example: + +```kotlin +@Composable +fun List() { + // ... + + ReorderableRow( + 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() }, /* ... */) +} +``` + +##### Use with [`Card`]() + If you want to use the [material3's Clickable Card](), you can create a [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) and pass it to both the [`Card`]() and the `Modifier.draggableHandle` (or `Modifier.longPressDraggableHandle`), `Modifier.draggableHandle` will emit drag events to the [`MutableInteractionSource`](https://developer.android.com/reference/kotlin/androidx/compose/foundation/interaction/MutableInteractionSource) so that the [`Card`]() can respond to the drag events: ```kotlin @@ -731,7 +1627,7 @@ val view = LocalView.current var list by remember { mutableStateOf(List(4) { "Item $it" }) } -ReorderableColumn( +ReorderableRow( modifier = Modifier .fillMaxSize() .padding(8.dp), @@ -753,8 +1649,8 @@ ReorderableColumn( onClick = {}, interactionSource = interactionSource, ) { - Row { - Text(item, Modifier.padding(horizontal = 8.dp)) + Column { + Text(item, Modifier.padding(vertical = 8.dp)) IconButton( modifier = Modifier.draggableHandle( onDragStarted = { @@ -775,12 +1671,6 @@ ReorderableColumn( } ``` -#### Row - -See [`ReorderableRowScreen.kt`](demoApp/composeApp/src/commonMain/kotlin/sh/calvin/reorderable/demo/ui/ReorderableRowScreen.kt) in the demo app. - -You can just replace `Column` with `Row` in the `Column` examples above. - #### Accessibility See the demo app for examples of how to make the reorderable list accessible.