refactor: use item keys instead of indexes

This commit is contained in:
andrekir
2024-06-30 07:51:13 -03:00
committed by Andre K
parent 218100e9d5
commit c95cba097c

View File

@@ -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<Int?>(null)
var draggingItemKey by mutableStateOf<Any?>(null)
private set
internal val scrollChannel = Channel<Float>()
@@ -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<Int?>(null)
internal var previousKeyOfDraggedItem by mutableStateOf<Any?>(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) }
}