通过拖放重新排序 LazyColumn 项目

Reorder LazyColumn items with drag & drop

我想创建一个 LazyColumn,其中包含可以通过拖放重新排序的项目。如果没有 compose,我的方法是使用 ItemTouchHelper.SimpleCallback,但我还没有找到类似的东西用于 compose。

我试过使用 Modifier.longPressDragGestureFilter and Modifier.draggable,但这只允许我使用偏移量来拖动卡片。它没有给我一个列表索引(比如 ItemTouchHelper.SimpleCallback 中的 fromPosition/toPosition),我需要交换列表中的项目。

是否有与 ItemTouchHelper.SimpleCallbackonMove 功能等效的 compose 功能?如果不是,它是计划中的功能吗?

是否possible/feasible自己尝试实现这种事情?

到目前为止,据我所知,Compose 还没有提供处理此问题的方法,尽管我假设这将在工作中进行,因为他们已经添加了 draggable 和 longPressDragGestureFilter 修饰符,正如您已经提到的那样。当他们添加这些内容时,也许这是在惰性列中拖放的先兆。

2021 年 2 月 Google 提出了一个问题,他们的回应是他们不会为 1.0 版本提供官方解决方案,尽管他们在其中提供了一些关于如何处理该解决方案的指导。看起来目前最好的解决方案是使用带有 ItemTouchHelper 的 RecyclerView。

这是提到的问题:https://issuetracker.google.com/issues/181282427


21 年 11 月 23 日更新

虽然这不处理项目本身的点击,它只处理动画,这是朝着内部拖动重新排序迈出的一步。他们添加了使用名为 Modifier.animateItemPlacement() 的新修饰符的选项,并在设置项目时提供了一个键。

https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.1.0-beta03

示例:

var list by remember { mutableStateOf(listOf("A", "B", "C")) }
LazyColumn {
    item {
        Button(onClick = { list = list.shuffled() }) {
            Text("Shuffle")
        }
    }
    items(list, key = { it }) {
        Text("Item $it", Modifier.animateItemPlacement())
    }
}

可以使用 detectDragGesturesAfterLongPressrememberLazyListState.

构建一个简单(不完美)的可重新排序列表

基本思想是向 LazyColumn 添加一个拖动手势修改器并检测我们自己拖动的项目,而不是为每个项目添加一个修改器。

   val listState: LazyListState = rememberLazyListState()
   ...
   LazyColumn(
        state = listState,
        modifier = Modifier.pointerInput(Unit) {
            detectDragGesturesAfterLongPress(....)

使用 LazyListState 提供的布局信息查找项目:

var position by remember {
    mutableStateOf<Float?>(null)
}
...
onDragStart = { offset ->
    listState.layoutInfo.visibleItemsInfo
        .firstOrNull { offset.y.toInt() in it.offset..it.offset + it.size }
        ?.also {
            position = it.offset + it.size / 2f
        }
}

每次拖动时更新位置:

onDrag = { change, dragAmount ->
    change.consumeAllChanges()
    position = position?.plus(dragAmount.y)
    // Start autoscrolling if position is out of bounds
}

为了在滚动时支持重新排序,我们无法在 onDrag 中进行重新排序。 为此,我们创建了一个流程来在每次 position/scroll 更新时找到最近的项目:

var draggedItem by remember {
    mutableStateOf<Int?>(null)
}
....
snapshotFlow { listState.layoutInfo }
.combine(snapshotFlow { position }.distinctUntilChanged()) { state, pos ->
    pos?.let { draggedCenter ->
        state.visibleItemsInfo
            .minByOrNull { (draggedCenter - (it.offset + it.size / 2f)).absoluteValue }
    }?.index
}
.distinctUntilChanged()
.collect { near -> ...}

更新拖动的项目索引并在您的 MutableStateList 中移动项目。

draggedItem = when {
    near == null -> null
    draggedItem == null -> near
    else -> near.also { items.move(draggedItem, it) }
}

fun <T> MutableList<T>.move(fromIdx: Int, toIdx: Int) {
    if (toIdx > fromIdx) {
        for (i in fromIdx until toIdx) {
            this[i] = this[i + 1].also { this[i + 1] = this[i] }
        }
    } else {
        for (i in fromIdx downTo toIdx + 1) {
            this[i] = this[i - 1].also { this[i - 1] = this[i] }
        }
    }
}

计算相对项偏移量:

val indexWithOffset by derivedStateOf {
    draggedItem
        ?.let { listState.layoutInfo.visibleItemsInfo.getOrNull(it - listState.firstVisibleItemIndex) }
        ?.let { Pair(it.index, (position ?: 0f) - it.offset - it.size / 2f) }
}

然后可以用来将偏移应用到拖动的项目(不要使用项目键!):

itemsIndexed(items) { idx, item ->
    val offset by remember {
        derivedStateOf { state.indexWithOffset?.takeIf { it.first == idx }?.second }
    }
    Column(
        modifier = Modifier
            .zIndex(offset?.let { 1f } ?: 0f)
            .graphicsLayer {
                translationY = offset ?: 0f
            }
    )
        ....
}

可以找到示例实现 here