mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-01 11:22:40 -05:00
refactor: use item keys instead of indexes
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user