mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: use item keys instead of indexes
This commit is contained in:
parent
218100e9d5
commit
c95cba097c
1 changed files with 93 additions and 58 deletions
|
|
@ -20,8 +20,10 @@ import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
import androidx.compose.foundation.gestures.scrollBy
|
import androidx.compose.foundation.gestures.scrollBy
|
||||||
|
import androidx.compose.foundation.gestures.stopScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
|
@ -48,7 +50,10 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
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.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
|
|
@ -56,19 +61,22 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.launch
|
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
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun LazyColumnDragAndDropDemo() {
|
fun LazyColumnDragAndDropDemo() {
|
||||||
var list by remember { mutableStateOf(List(50) { it }) }
|
var list by remember { mutableStateOf(List(50) { it }) }
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val dragDropState =
|
val dragDropState = rememberDragDropState(listState) { from, to ->
|
||||||
rememberDragDropState(listState) { fromIndex, toIndex ->
|
list = list.toMutableList().apply { add(to.index, removeAt(from.index)) }
|
||||||
list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.dragContainer(dragDropState),
|
modifier = Modifier.dragContainer(
|
||||||
|
dragDropState = dragDropState,
|
||||||
|
haptics = LocalHapticFeedback.current,
|
||||||
|
),
|
||||||
state = listState,
|
state = listState,
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
|
@ -77,7 +85,10 @@ fun LazyColumnDragAndDropDemo() {
|
||||||
DraggableItem(dragDropState, index) { isDragging ->
|
DraggableItem(dragDropState, index) { isDragging ->
|
||||||
val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
|
val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
|
||||||
Card(elevation = elevation) {
|
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
|
@Composable
|
||||||
fun rememberDragDropState(lazyListState: LazyListState, onMove: (Int, Int) -> Unit): DragDropState {
|
fun rememberDragDropState(
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
onMove: (LazyListItemInfo, LazyListItemInfo) -> Unit,
|
||||||
|
): DragDropState {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val state =
|
val state = remember(lazyListState) {
|
||||||
remember(lazyListState) {
|
DragDropState(state = lazyListState, onMove = onMove, scope = scope)
|
||||||
DragDropState(state = lazyListState, onMove = onMove, scope = scope)
|
}
|
||||||
}
|
|
||||||
LaunchedEffect(state) {
|
LaunchedEffect(state) {
|
||||||
while (true) {
|
while (true) {
|
||||||
val diff = state.scrollChannel.receive()
|
val diff = state.scrollChannel.receive()
|
||||||
|
|
@ -104,9 +117,9 @@ class DragDropState
|
||||||
internal constructor(
|
internal constructor(
|
||||||
private val state: LazyListState,
|
private val state: LazyListState,
|
||||||
private val scope: CoroutineScope,
|
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
|
private set
|
||||||
|
|
||||||
internal val scrollChannel = Channel<Float>()
|
internal val scrollChannel = Channel<Float>()
|
||||||
|
|
@ -114,32 +127,34 @@ internal constructor(
|
||||||
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
||||||
private var draggingItemInitialOffset by mutableIntStateOf(0)
|
private var draggingItemInitialOffset by mutableIntStateOf(0)
|
||||||
internal val draggingItemOffset: Float
|
internal val draggingItemOffset: Float
|
||||||
get() =
|
get() = draggingItemLayoutInfo?.let { item ->
|
||||||
draggingItemLayoutInfo?.let { item ->
|
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
||||||
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
} ?: 0f
|
||||||
} ?: 0f
|
|
||||||
|
|
||||||
private val draggingItemLayoutInfo: LazyListItemInfo?
|
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
|
private set
|
||||||
|
|
||||||
internal var previousItemOffset = Animatable(0f)
|
internal var previousItemOffset = Animatable(0f)
|
||||||
private set
|
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
|
state.layoutInfo.visibleItemsInfo
|
||||||
.firstOrNull { item -> offset.y.toInt() in item.offset..(item.offset + item.size) }
|
.firstOrNull { item -> item.key == key }
|
||||||
?.also {
|
?.also {
|
||||||
draggingItemIndex = it.index
|
draggingItemKey = it.key
|
||||||
draggingItemInitialOffset = it.offset
|
draggingItemInitialOffset = it.offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun onDragInterrupted() {
|
internal fun onDragInterrupted() {
|
||||||
if (draggingItemIndex != null) {
|
if (draggingItemKey != null) {
|
||||||
previousIndexOfDraggedItem = draggingItemIndex
|
previousKeyOfDraggedItem = draggingItemKey
|
||||||
val startOffset = draggingItemOffset
|
val startOffset = draggingItemOffset
|
||||||
scope.launch {
|
scope.launch {
|
||||||
previousItemOffset.snapTo(startOffset)
|
previousItemOffset.snapTo(startOffset)
|
||||||
|
|
@ -147,11 +162,11 @@ internal constructor(
|
||||||
0f,
|
0f,
|
||||||
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
|
spring(stiffness = Spring.StiffnessMediumLow, visibilityThreshold = 1f)
|
||||||
)
|
)
|
||||||
previousIndexOfDraggedItem = null
|
previousKeyOfDraggedItem = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
draggingItemDraggedDelta = 0f
|
draggingItemDraggedDelta = 0f
|
||||||
draggingItemIndex = null
|
draggingItemKey = null
|
||||||
draggingItemInitialOffset = 0
|
draggingItemInitialOffset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,8 +178,9 @@ internal constructor(
|
||||||
val endOffset = startOffset + draggingItem.size
|
val endOffset = startOffset + draggingItem.size
|
||||||
val middleOffset = startOffset + (endOffset - startOffset) / 2f
|
val middleOffset = startOffset + (endOffset - startOffset) / 2f
|
||||||
|
|
||||||
val targetItem =
|
val targetItem = state.layoutInfo.visibleItemsInfo
|
||||||
state.layoutInfo.visibleItemsInfo.find { item ->
|
.filter { it.key is Int }
|
||||||
|
.find { item ->
|
||||||
middleOffset.toInt() in item.offset..item.offsetEnd &&
|
middleOffset.toInt() in item.offset..item.offsetEnd &&
|
||||||
draggingItem.index != item.index
|
draggingItem.index != item.index
|
||||||
}
|
}
|
||||||
|
|
@ -173,22 +189,31 @@ internal constructor(
|
||||||
draggingItem.index == state.firstVisibleItemIndex ||
|
draggingItem.index == state.firstVisibleItemIndex ||
|
||||||
targetItem.index == state.firstVisibleItemIndex
|
targetItem.index == state.firstVisibleItemIndex
|
||||||
) {
|
) {
|
||||||
state.requestScrollToItem(
|
// state.requestScrollToItem( FIXME 1.7.0 method
|
||||||
state.firstVisibleItemIndex,
|
// state.firstVisibleItemIndex,
|
||||||
state.firstVisibleItemScrollOffset
|
// state.firstVisibleItemScrollOffset
|
||||||
)
|
// )
|
||||||
}
|
scope.launch {
|
||||||
onMove.invoke(draggingItem.index, targetItem.index)
|
if (state.isScrollInProgress) {
|
||||||
draggingItemIndex = targetItem.index
|
state.stopScroll()
|
||||||
} else {
|
}
|
||||||
val overscroll =
|
state.scrollToItem(
|
||||||
when {
|
state.firstVisibleItemIndex,
|
||||||
draggingItemDraggedDelta > 0 ->
|
state.firstVisibleItemScrollOffset
|
||||||
(endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
|
)
|
||||||
draggingItemDraggedDelta < 0 ->
|
|
||||||
(startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
|
|
||||||
else -> 0f
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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) {
|
if (overscroll != 0f) {
|
||||||
scrollChannel.trySend(overscroll)
|
scrollChannel.trySend(overscroll)
|
||||||
}
|
}
|
||||||
|
|
@ -199,37 +224,47 @@ internal constructor(
|
||||||
get() = this.offset + this.size
|
get() = this.offset + this.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Modifier.dragContainer(dragDropState: DragDropState): Modifier {
|
fun Modifier.dragContainer(
|
||||||
return pointerInput(dragDropState) {
|
dragDropState: DragDropState,
|
||||||
|
haptics: HapticFeedback,
|
||||||
|
): Modifier {
|
||||||
|
return this.pointerInput(dragDropState) {
|
||||||
detectDragGesturesAfterLongPress(
|
detectDragGesturesAfterLongPress(
|
||||||
onDrag = { change, offset ->
|
onDrag = { change, offset ->
|
||||||
change.consume()
|
change.consume()
|
||||||
dragDropState.onDrag(offset = offset)
|
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() },
|
onDragEnd = { dragDropState.onDragInterrupted() },
|
||||||
onDragCancel = { dragDropState.onDragInterrupted() }
|
onDragCancel = { dragDropState.onDragInterrupted() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LazyItemScope.DraggableItem(
|
fun LazyItemScope.DraggableItem(
|
||||||
dragDropState: DragDropState,
|
dragDropState: DragDropState,
|
||||||
index: Int,
|
key: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
|
content: @Composable ColumnScope.(isDragging: Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
val dragging = index == dragDropState.draggingItemIndex
|
val dragging = key == dragDropState.draggingItemKey
|
||||||
val draggingModifier =
|
val draggingModifier = if (dragging) {
|
||||||
if (dragging) {
|
Modifier
|
||||||
Modifier.zIndex(1f).graphicsLayer { translationY = dragDropState.draggingItemOffset }
|
.zIndex(1f)
|
||||||
} else if (index == dragDropState.previousIndexOfDraggedItem) {
|
.graphicsLayer { translationY = dragDropState.draggingItemOffset }
|
||||||
Modifier.zIndex(1f).graphicsLayer {
|
} else if (key == dragDropState.previousKeyOfDraggedItem) {
|
||||||
translationY = dragDropState.previousItemOffset.value
|
Modifier
|
||||||
}
|
.zIndex(1f)
|
||||||
} else {
|
.graphicsLayer { translationY = dragDropState.previousItemOffset.value }
|
||||||
Modifier.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
} else {
|
||||||
}
|
Modifier.animateItemPlacement()
|
||||||
|
}
|
||||||
Column(modifier = modifier.then(draggingModifier)) { content(dragging) }
|
Column(modifier = modifier.then(draggingModifier)) { content(dragging) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue