You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose拖拽列表可变状态更新异常问题求助

Jetpack Compose多列拖拽列表项数据显示异常问题

我是Jetpack Compose新手,正在实现一个可在多列间拖拽移动列表项的界面,编写了如下代码。预期功能是将黑色方块在列间移动,但实际问题是:移动第一个列表项正常,但若移动后续项(原第二项变为列表第一项),显示的值与之前移动的项相同,如动画所示:

拖拽异常动画

问题代码

TaskColumns.kt

@Preview
@Composable
fun TaskColumns() {

    var columnWidth by remember {
        mutableStateOf(0.dp)
    }

    val density = LocalDensity.current

    val items = remember {
        mutableListOf(
            mutableListOf(5,6,7,8),
            mutableListOf(),
            mutableListOf(),
            mutableListOf())
    }

    Surface(
        Modifier.fillMaxSize()
    ) {
        LongPressDraggable {
            Row(Modifier.padding(4.dp)) {
                repeat(4){
                    val column = it
                    DropTarget<IntArray>(modifier = Modifier.weight(1f)) { isInBound, data ->
                        val n = data?.get(0)
                        val i = data?.get(1)
                        val c = data?.get(2)

                        var color = if (isInBound) Color.Green
                        else Color.Blue

                        if (i != null && n != null && c != null) {
                            if (c != column){
                                Log.d("DND", "${data[0]}, ${data[1]}")
                                items[column].add(n)
                                items[c].remove(n)
                            }
                            color = Color.Blue
                        }
                        Column(
                            Modifier
                                .fillMaxHeight()
                                .fillMaxWidth()
                                .padding(4.dp)
                                .background(color)
                                .onPlaced {
                                    columnWidth = with(density) { it.size.width.toDp() }
                                }
                        ) {
                            items[column].forEachIndexed { index, item ->
                                DragTarget(
                                    modifier = Modifier,
                                    dataToDrop = intArrayOf(item, index, column),
                                    onDragCustomAction = {
                                        Log.d("DND", "$item")
                                    }
                                ) {
                                    Column(
                                        Modifier
                                            .size(columnWidth)
                                            .aspectRatio(1f / 1f)
                                            .background(Color.Black),
                                        horizontalAlignment = Alignment.CenterHorizontally,
                                        verticalArrangement = Arrangement.Center
                                    ) {
                                        Text(text = "$item", color = Color.White)
                                    }
                                }
                                Spacer(modifier = Modifier.height(8.dp))
                            }
                        }
                    }
                }
            }
        }
    }
}

DragAndDrop.kt

internal val LocalDragTargetInfo = compositionLocalOf { DragTargetInfo() }

@Composable
fun LongPressDraggable(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit
) {
    val state = remember { DragTargetInfo() }
    CompositionLocalProvider(
        LocalDragTargetInfo provides state
    ) {
        Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.TopStart) {
            content()
            if (state.isDragging) {
                var targetSize by remember {
                    mutableStateOf(IntSize.Zero)
                }
                Box(modifier = Modifier
                    .graphicsLayer {
                        val offset = (state.dragPosition + state.dragOffset)
                        alpha = if (targetSize == IntSize.Zero) 0f else .9f
                        translationX = offset.x.minus(targetSize.width / 2)
                        translationY = offset.y.minus(targetSize.height / 2)
                    }
                    .onGloballyPositioned {
                        targetSize = it.size
                    }
                ) {
                    state.draggableComposable?.invoke()
                }
            }
        }
    }
}

@Composable
fun <T> DragTarget(
    modifier: Modifier,
    dataToDrop: T,
    onDragCustomAction: ()->Unit = {},
    onDragCancelCustomAction: ()->Unit = {},
    content: @Composable () -> Unit
) {
    var currentPosition by remember { mutableStateOf(Offset.Zero) }
    val currentState = LocalDragTargetInfo.current

    Box(modifier = modifier
        .onGloballyPositioned {
            currentPosition = it.localToWindow(Offset.Zero)
        }
        .pointerInput(Unit) {
            detectDragGesturesAfterLongPress(onDragStart = {
                currentState.dataToDrop = dataToDrop
                currentState.isDragging = true
                currentState.dragPosition = currentPosition + it
                currentState.draggableComposable = content
            }, onDrag = { change, dragAmount ->
                change.consume()
                currentState.dragOffset += Offset(dragAmount.x, dragAmount.y)
                onDragCustomAction()
            }, onDragEnd = {
                currentState.isDragging = false
                currentState.dragOffset = Offset.Zero
            }, onDragCancel = {
                onDragCancelCustomAction()
                currentState.dragOffset = Offset.Zero
                currentState.isDragging = false
            })
        }) {
        content()
    }
}

@Composable
fun <T> DropTarget(
    modifier: Modifier,
    content: @Composable BoxScope.(isInBound: Boolean, data: T?) -> Unit
) {
    val dragInfo = LocalDragTargetInfo.current
    val dragPosition = dragInfo.dragPosition
    val dragOffset = dragInfo.dragOffset
    var isCurrentDropTarget by remember {
        mutableStateOf(false)
    }

    Box(modifier = modifier.onGloballyPositioned {
        it.boundsInWindow().let { rect ->
            isCurrentDropTarget = rect.contains(dragPosition + dragOffset)
        }
    }) {
        val data = if (isCurrentDropTarget && !dragInfo.isDragging) dragInfo.dataToDrop as T? else null
        content(isCurrentDropTarget, data)
    }
}

internal class DragTargetInfo {
    var isDragging: Boolean by mutableStateOf(false)
    var dragPosition by mutableStateOf(Offset.Zero)
    var dragOffset by mutableStateOf(Offset.Zero)
    var draggableComposable by mutableStateOf<(@Composable () -> Unit)?>(null)
    var dataToDrop by mutableStateOf<Any?>(null)
}

问题原因与解决方案

核心问题分析

  1. Compose状态感知缺失:使用普通mutableListOf存储列数据,Compose无法感知列表内容的修改,导致数据变化后界面不重组。
  2. 列表项标识缺失:forEachIndexed生成DragTarget时未指定唯一key,Compose无法区分不同列表项,重组时复用旧节点,数据未更新。
  3. 代码语法错误:原DragAndDrop.kt中存在函数定义不完整的语法问题,影响逻辑执行。

修改方案

  1. 替换为可观察列表:将mutableListOf替换为mutableStateListOf,让Compose追踪列表变化:
val items = remember {
    mutableStateListOf(
        mutableStateListOf(5,6,7,8),
        mutableStateListOf(),
        mutableStateListOf(),
        mutableStateListOf()
    )
}
  1. 为DragTarget添加唯一Key:在forEachIndexed中给DragTarget的Modifier添加key,确保每个列表项被正确识别:
items[column].forEachIndexed { index, item ->
    DragTarget(
        modifier = Modifier.key("$column-$index-$item"), // 用列索引+项索引+值组合作为唯一key
        dataToDrop = intArrayOf(item, index, column),
        onDragCustomAction = {
            Log.d("DND", "$item")
        }
    ) {
        // 原有内容不变
    }
    Spacer(modifier = Modifier.height(8.dp))
}
  1. 修正语法错误:完善DragAndDrop.kt中所有函数的定义(比如补全-> Unit、修复变量类型定义),确保代码能正常编译运行。

修改后效果

  • 拖拽列表项后,mutableStateListOf会触发Compose重组,界面实时更新数据。
  • 唯一key让Compose正确区分每个DragTarget,避免复用错误节点,保证拖拽数据与显示内容一致。

内容的提问来源于stack exchange,提问作者Bernard Borio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 00:07:07