refactor: dragDropItemsIndexed back to use item index

This commit is contained in:
andrekir 2024-07-04 09:23:13 -03:00
parent 3dd0f8ceed
commit 15861c1389
3 changed files with 45 additions and 57 deletions

View file

@ -314,8 +314,8 @@ fun ChannelScreen(
}
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(listState) { from, to ->
updateSettingsList { add(to.index, removeAt(from.index)) }
val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
updateSettingsList { add(toIndex, removeAt(fromIndex)) }
}
LazyColumn(

View file

@ -69,8 +69,10 @@ fun LazyColumnDragAndDropDemo() {
var list by remember { mutableStateOf(List(50) { it }) }
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(listState) { from, to ->
list = list.toMutableList().apply { add(to.index, removeAt(from.index)) }
val dragDropState = rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
if (fromIndex in list.indices && toIndex in list.indices) {
list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
}
}
LazyColumn(
@ -82,29 +84,33 @@ fun LazyColumnDragAndDropDemo() {
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text("Header", Modifier.fillMaxWidth().padding(20.dp))
}
itemsIndexed(list, key = { _, item -> item }) { index, item ->
DraggableItem(dragDropState, index) { isDragging ->
DraggableItem(dragDropState, index + 1) { isDragging ->
val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
Card(elevation = elevation) {
Text("Item $item",
Modifier
.fillMaxWidth()
.padding(20.dp))
Text("Item $item", Modifier.fillMaxWidth().padding(20.dp))
}
}
}
item {
Text("Footer", Modifier.fillMaxWidth().padding(20.dp))
}
}
}
@Composable
fun rememberDragDropState(
lazyListState: LazyListState,
onMove: (LazyListItemInfo, LazyListItemInfo) -> Unit,
headerCount: Int = 0,
onMove: (Int, Int) -> Unit
): DragDropState {
val scope = rememberCoroutineScope()
val state = remember(lazyListState) {
DragDropState(state = lazyListState, onMove = onMove, scope = scope)
}
val state = remember(lazyListState) { DragDropState(lazyListState, headerCount, scope, onMove) }
LaunchedEffect(state) {
while (true) {
val diff = state.scrollChannel.receive()
@ -117,11 +123,12 @@ fun rememberDragDropState(
class DragDropState
internal constructor(
private val state: LazyListState,
private val headerCount: Int,
private val scope: CoroutineScope,
private val onMove: (LazyListItemInfo, LazyListItemInfo) -> Unit
private val onMove: (Int, Int) -> Unit
) {
var draggingItemKey by mutableStateOf<Any?>(null)
private set
private var draggingItemIndex by mutableStateOf<Int?>(null)
val adjustedItemIndex get() = draggingItemIndex?.minus(headerCount)
internal val scrollChannel = Channel<Float>()
@ -133,29 +140,26 @@ internal constructor(
} ?: 0f
private val draggingItemLayoutInfo: LazyListItemInfo?
get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == draggingItemKey }
get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
internal var previousKeyOfDraggedItem by mutableStateOf<Any?>(null)
internal var previousIndexOfDraggedItem by mutableStateOf<Int?>(null)
private set
internal var previousItemOffset = Animatable(0f)
private set
internal fun gridItemKeyAtPosition(offset: Offset): Int? = state.layoutInfo.visibleItemsInfo
.find { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }?.key as? Int
internal fun onDragStart(key: Int) {
internal fun onDragStart(offset: Offset) {
state.layoutInfo.visibleItemsInfo
.firstOrNull { item -> item.key == key }
.firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
?.also {
draggingItemKey = it.key
draggingItemIndex = it.index
draggingItemInitialOffset = it.offset
}
}
internal fun onDragInterrupted() {
if (draggingItemKey != null) {
previousKeyOfDraggedItem = draggingItemKey
if (draggingItemIndex != null) {
previousIndexOfDraggedItem = draggingItemIndex
val startOffset = draggingItemOffset
scope.launch {
previousItemOffset.snapTo(startOffset)
@ -163,11 +167,11 @@ internal constructor(
0f,
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
)
previousKeyOfDraggedItem = null
previousIndexOfDraggedItem = null
}
}
draggingItemDraggedDelta = 0f
draggingItemKey = null
draggingItemIndex = null
draggingItemInitialOffset = 0
}
@ -180,7 +184,6 @@ internal constructor(
val middleOffset = startOffset + (endOffset - startOffset) / 2f
val targetItem = state.layoutInfo.visibleItemsInfo
.filter { it.key is Int }
.find { item ->
middleOffset.toInt() in item.offset..item.offsetEnd &&
draggingItem.index != item.index
@ -204,7 +207,8 @@ internal constructor(
)
}
}
onMove.invoke(draggingItem, targetItem)
onMove.invoke(draggingItem.index - headerCount, targetItem.index - headerCount)
draggingItemIndex = targetItem.index
} else {
val overscroll = when {
draggingItemDraggedDelta > 0 ->
@ -236,10 +240,8 @@ fun Modifier.dragContainer(
dragDropState.onDrag(offset = offset)
},
onDragStart = { offset ->
dragDropState.gridItemKeyAtPosition(offset)?.let { key ->
dragDropState.onDragStart(key)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
}
dragDropState.onDragStart(offset)
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
},
onDragEnd = { dragDropState.onDragInterrupted() },
onDragCancel = { dragDropState.onDragInterrupted() }
@ -251,16 +253,16 @@ fun Modifier.dragContainer(
@Composable
fun LazyItemScope.DraggableItem(
dragDropState: DragDropState,
key: Int,
index: Int,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
) {
val dragging = key == dragDropState.draggingItemKey
val dragging = index == dragDropState.adjustedItemIndex
val draggingModifier = if (dragging) {
Modifier
.zIndex(1f)
.graphicsLayer { translationY = dragDropState.draggingItemOffset }
} else if (key == dragDropState.previousKeyOfDraggedItem) {
} else if (index == dragDropState.previousIndexOfDraggedItem) {
Modifier
.zIndex(1f)
.graphicsLayer { translationY = dragDropState.previousItemOffset.value }
@ -275,30 +277,21 @@ fun LazyItemScope.DraggableItem(
*
* Wraps [itemsIndexed] function with [detectDragGesturesAfterLongPress] to enable long-press
* drag gestures and allow items in the list to be reordered using the provided [DragDropState].
*
* Uses the item's `hashCode()` key internally (instead of the index) to allow handling headers,
* footers, and other non-draggable items.
*
* @param items The list of items to be displayed in the [LazyColumn].
* @param dragDropState The state object managing drag-and-drop interactions.
* @param contentType A function providing the content type of each item, used for item recycling
* optimizations. Defaults to null.
* @param itemContent A composable function defining the UI for each item. It provides the index,
* the item itself, and a boolean indicating if the item is currently being dragged.
*/
inline fun <T> LazyListScope.dragDropItemsIndexed(
items: List<T>,
dragDropState: DragDropState,
noinline key: ((index: Int, item: T) -> Any)? = null,
crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T, isDragging: Boolean) -> Unit
) = itemsIndexed(
items = items,
key = { _, item -> item.hashCode() },
key = key,
contentType = contentType,
itemContent = { index, item ->
DraggableItem(
dragDropState = dragDropState,
key = item.hashCode(),
index = index,
content = { isDragging -> itemContent(index, item, isDragging) }
)
}

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Card
import androidx.compose.material.Chip
import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
@ -36,7 +35,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
@ -78,7 +76,6 @@ fun ChannelCard(
Text(
text = title,
style = MaterialTheme.typography.body1,
color = if (!enabled) MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) else Color.Unspecified,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { onDeleteClick() }) {
@ -105,11 +102,9 @@ fun ChannelSettingsItemList(
val settingsListInput = remember(settingsList) { settingsList.toMutableStateList() }
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(listState) { from, to ->
settingsListInput.apply {
val fromIndex = indexOfFirst { it.hashCode() == from.key }
val toIndex = indexOfFirst { it.hashCode() == to.key }
add(toIndex, removeAt(fromIndex))
val dragDropState = rememberDragDropState(listState, headerCount = 1) { fromIndex, toIndex ->
if (toIndex in settingsListInput.indices && fromIndex in settingsListInput.indices) {
settingsListInput.apply { add(toIndex, removeAt(fromIndex)) }
}
}