From c95cba097ce367c8b22dfca762a4c01dba36cfda Mon Sep 17 00:00:00 2001 From: andrekir Date: Sun, 30 Jun 2024 07:51:13 -0300 Subject: [PATCH] refactor: use item `keys` instead of `indexes` --- .../components/LazyColumnDragAndDropDemo.kt | 151 +++++++++++------- 1 file changed, 93 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/LazyColumnDragAndDropDemo.kt b/app/src/main/java/com/geeksville/mesh/ui/components/LazyColumnDragAndDropDemo.kt index f30c98eec..3692a4faa 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/LazyColumnDragAndDropDemo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/LazyColumnDragAndDropDemo.kt @@ -20,8 +20,10 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.spring +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.gestures.stopScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -48,7 +50,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex @@ -56,19 +61,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch +// Derived in part from: https://github.com/androidx/androidx/blob/c92ad2941368202b2d78b8d14c71bf81e9525944/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt @Preview @Composable fun LazyColumnDragAndDropDemo() { var list by remember { mutableStateOf(List(50) { it }) } val listState = rememberLazyListState() - val dragDropState = - rememberDragDropState(listState) { fromIndex, toIndex -> - list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) } - } + val dragDropState = rememberDragDropState(listState) { from, to -> + list = list.toMutableList().apply { add(to.index, removeAt(from.index)) } + } LazyColumn( - modifier = Modifier.dragContainer(dragDropState), + modifier = Modifier.dragContainer( + dragDropState = dragDropState, + haptics = LocalHapticFeedback.current, + ), state = listState, contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) @@ -77,7 +85,10 @@ fun LazyColumnDragAndDropDemo() { DraggableItem(dragDropState, index) { 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)) } } } @@ -85,12 +96,14 @@ fun LazyColumnDragAndDropDemo() { } @Composable -fun rememberDragDropState(lazyListState: LazyListState, onMove: (Int, Int) -> Unit): DragDropState { +fun rememberDragDropState( + lazyListState: LazyListState, + onMove: (LazyListItemInfo, LazyListItemInfo) -> Unit, +): DragDropState { val scope = rememberCoroutineScope() - val state = - remember(lazyListState) { - DragDropState(state = lazyListState, onMove = onMove, scope = scope) - } + val state = remember(lazyListState) { + DragDropState(state = lazyListState, onMove = onMove, scope = scope) + } LaunchedEffect(state) { while (true) { val diff = state.scrollChannel.receive() @@ -104,9 +117,9 @@ class DragDropState internal constructor( private val state: LazyListState, private val scope: CoroutineScope, - private val onMove: (Int, Int) -> Unit + private val onMove: (LazyListItemInfo, LazyListItemInfo) -> Unit ) { - var draggingItemIndex by mutableStateOf(null) + var draggingItemKey by mutableStateOf(null) private set internal val scrollChannel = Channel() @@ -114,32 +127,34 @@ internal constructor( private var draggingItemDraggedDelta by mutableFloatStateOf(0f) private var draggingItemInitialOffset by mutableIntStateOf(0) internal val draggingItemOffset: Float - get() = - draggingItemLayoutInfo?.let { item -> - draggingItemInitialOffset + draggingItemDraggedDelta - item.offset - } ?: 0f + get() = draggingItemLayoutInfo?.let { item -> + draggingItemInitialOffset + draggingItemDraggedDelta - item.offset + } ?: 0f private val draggingItemLayoutInfo: LazyListItemInfo? - get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex } + get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.key == draggingItemKey } - internal var previousIndexOfDraggedItem by mutableStateOf(null) + internal var previousKeyOfDraggedItem by mutableStateOf(null) private set internal var previousItemOffset = Animatable(0f) private set - internal fun onDragStart(offset: Offset) { + 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) { state.layoutInfo.visibleItemsInfo - .firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) } + .firstOrNull { item -> item.key == key } ?.also { - draggingItemIndex = it.index + draggingItemKey = it.key draggingItemInitialOffset = it.offset } } internal fun onDragInterrupted() { - if (draggingItemIndex != null) { - previousIndexOfDraggedItem = draggingItemIndex + if (draggingItemKey != null) { + previousKeyOfDraggedItem = draggingItemKey val startOffset = draggingItemOffset scope.launch { previousItemOffset.snapTo(startOffset) @@ -147,11 +162,11 @@ internal constructor( 0f, spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f) ) - previousIndexOfDraggedItem = null + previousKeyOfDraggedItem = null } } draggingItemDraggedDelta = 0f - draggingItemIndex = null + draggingItemKey = null draggingItemInitialOffset = 0 } @@ -163,8 +178,9 @@ internal constructor( val endOffset = startOffset + draggingItem.size val middleOffset = startOffset + (endOffset - startOffset) / 2f - val targetItem = - state.layoutInfo.visibleItemsInfo.find { item -> + val targetItem = state.layoutInfo.visibleItemsInfo + .filter { it.key is Int } + .find { item -> middleOffset.toInt() in item.offset..item.offsetEnd && draggingItem.index != item.index } @@ -173,22 +189,31 @@ internal constructor( draggingItem.index == state.firstVisibleItemIndex || targetItem.index == state.firstVisibleItemIndex ) { - state.requestScrollToItem( - state.firstVisibleItemIndex, - state.firstVisibleItemScrollOffset - ) - } - onMove.invoke(draggingItem.index, targetItem.index) - draggingItemIndex = targetItem.index - } else { - val overscroll = - when { - draggingItemDraggedDelta > 0 -> - (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) - draggingItemDraggedDelta < 0 -> - (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) - else -> 0f +// state.requestScrollToItem( FIXME 1.7.0 method +// state.firstVisibleItemIndex, +// state.firstVisibleItemScrollOffset +// ) + scope.launch { + if (state.isScrollInProgress) { + state.stopScroll() + } + state.scrollToItem( + state.firstVisibleItemIndex, + state.firstVisibleItemScrollOffset + ) } + } + onMove.invoke(draggingItem, targetItem) + } else { + val overscroll = when { + draggingItemDraggedDelta > 0 -> + (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + + draggingItemDraggedDelta < 0 -> + (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + + else -> 0f + } if (overscroll != 0f) { scrollChannel.trySend(overscroll) } @@ -199,37 +224,47 @@ internal constructor( get() = this.offset + this.size } -fun Modifier.dragContainer(dragDropState: DragDropState): Modifier { - return pointerInput(dragDropState) { +fun Modifier.dragContainer( + dragDropState: DragDropState, + haptics: HapticFeedback, +): Modifier { + return this.pointerInput(dragDropState) { detectDragGesturesAfterLongPress( onDrag = { change, offset -> change.consume() dragDropState.onDrag(offset = offset) }, - onDragStart = { offset -> dragDropState.onDragStart(offset) }, + onDragStart = { offset -> + dragDropState.gridItemKeyAtPosition(offset)?.let { key -> + dragDropState.onDragStart(key) + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + } + }, onDragEnd = { dragDropState.onDragInterrupted() }, onDragCancel = { dragDropState.onDragInterrupted() } ) } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun LazyItemScope.DraggableItem( dragDropState: DragDropState, - index: Int, + key: Int, modifier: Modifier = Modifier, content: @Composable ColumnScope.(isDragging: Boolean) -> Unit ) { - val dragging = index == dragDropState.draggingItemIndex - val draggingModifier = - if (dragging) { - Modifier.zIndex(1f).graphicsLayer { translationY = dragDropState.draggingItemOffset } - } else if (index == dragDropState.previousIndexOfDraggedItem) { - Modifier.zIndex(1f).graphicsLayer { - translationY = dragDropState.previousItemOffset.value - } - } else { - Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null) - } + val dragging = key == dragDropState.draggingItemKey + val draggingModifier = if (dragging) { + Modifier + .zIndex(1f) + .graphicsLayer { translationY = dragDropState.draggingItemOffset } + } else if (key == dragDropState.previousKeyOfDraggedItem) { + Modifier + .zIndex(1f) + .graphicsLayer { translationY = dragDropState.previousItemOffset.value } + } else { + Modifier.animateItemPlacement() + } Column(modifier = modifier.then(draggingModifier)) { content(dragging) } }