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

如何在Android Compose中实现带拖拽删除区域的可重排网格列表

问题描述

我正在使用Jetpack Compose开发应用功能,需要展示一个可通过拖拽重排的项网格,同时支持将项拖出网格到特定区域删除(类似部分启动器应用的拖拽至回收站功能)。我尝试了org.burnoutcrew.composereorderable、sh.calvin.reorderable这两款知名可重排库,以及官方Drag and Drop in Compose codelab的方法,但均无法同时实现LazyGrid内自由重排和将元素移出结构的需求。现有代码如下:

data class GridItem(val id: Int, val text: String)

@Composable
fun ReorderableGridViewScreen(modifier: Modifier = Modifier) {
    var items by remember { mutableStateOf(List(10) { GridItem(it,"Item $it") }) }

    val lazyGridState = rememberLazyGridState()
    val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to ->
        items = items.toMutableList().apply {
            add(to.index, removeAt(from.index))
        }
    }

    Column(
        modifier = modifier
            .fillMaxSize()
            .background(Color.LightGray)
            .padding(16.dp)
    ) {
        Text(
            text = "Reorderable Grid View",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier.padding(bottom = 16.dp)
        )

        LazyVerticalGrid(
            columns = GridCells.Fixed(2), // 2 columns
            state = lazyGridState,
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            items(items, key = { it.id }) { item ->
                ReorderableItem(reorderableLazyGridState, key = item.id) { isDragging ->

                    val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)

                    Surface(shadowElevation = elevation) {
                        Box(
                            modifier = Modifier
                                .fillMaxSize()
                                .background(Color.White)
                                .padding(8.dp),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(text = item.text, style = MaterialTheme.typography.bodyMedium)
                            IconButton(
                                onClick = {},
                                modifier = Modifier
                                    .align(Alignment.BottomEnd)
                                    .draggableHandle()
                            ) {
                                Icon(Icons.Default.Favorite, contentDescription = null)
                            }
                        }
                    }
                }
            }
        }
    }
}

请问是否有我遗漏的现成解决方案,还是需要通过Modifier.offset()来实现该功能?

解决方案

一、基于现有库扩展(推荐)

你使用的org.burnoutcrew.composereorderable可以通过监听拖拽位置实现删除逻辑,无需完全重构:

  1. 添加回收站区域,与拖拽状态联动显示/隐藏
  2. 监听拖拽项的实时位置,判断是否进入回收站范围,触发删除
  3. 改造示例代码:
@Composable
fun ReorderableGridViewScreen(modifier: Modifier = Modifier) {
    var items by remember { mutableStateOf(List(10) { GridItem(it,"Item $it") }) }
    val lazyGridState = rememberLazyGridState()
    
    // 回收站相关状态
    var showTrash by remember { mutableStateOf(false) }
    val trashHeight = 60.dp
    val density = LocalDensity.current
    val configuration = LocalConfiguration.current

    val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to ->
        items = items.toMutableList().apply {
            add(to.index, removeAt(from.index))
        }
    }

    // 监听拖拽状态,控制回收站显示
    LaunchedEffect(reorderableLazyGridState.dragState.isDragging) {
        showTrash = reorderableLazyGridState.dragState.isDragging
    }

    // 监听拖拽位置,判断是否触发删除
    LaunchedEffect(reorderableLazyGridState.dragState.position) {
        val dragState = reorderableLazyGridState.dragState
        if (dragState.isDragging && dragState.position != null) {
            val trashTopPx = with(density) {
                (configuration.screenHeightDp.dp - trashHeight).toPx()
            }
            if (dragState.position!!.y >= trashTopPx) {
                // 删除当前拖拽项
                items = items.filterNot { it.id == dragState.key }
                dragState.stopDrag()
            }
        }
    }

    Column(
        modifier = modifier
            .fillMaxSize()
            .background(Color.LightGray)
            .padding(16.dp)
    ) {
        Text(
            text = "Reorderable Grid View",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier.padding(bottom = 16.dp)
        )

        LazyVerticalGrid(
            columns = GridCells.Fixed(2),
            state = lazyGridState,
            modifier = Modifier.weight(1f),
            contentPadding = PaddingValues(8.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            items(items, key = { it.id }) { item ->
                ReorderableItem(reorderableLazyGridState, key = item.id) { isDragging ->
                    val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
                    Surface(shadowElevation = elevation) {
                        Box(
                            modifier = Modifier
                                .fillMaxSize()
                                .background(Color.White)
                                .padding(8.dp),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(text = item.text, style = MaterialTheme.typography.bodyMedium)
                            IconButton(
                                onClick = {},
                                modifier = Modifier
                                    .align(Alignment.BottomEnd)
                                    .draggableHandle()
                            ) {
                                Icon(Icons.Default.Favorite, contentDescription = null)
                            }
                        }
                    }
                }
            }
        }

        // 回收站区域
        if (showTrash) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(trashHeight)
                    .background(Color.Red.copy(alpha = 0.8f))
                    .padding(8.dp),
                contentAlignment = Alignment.Center
            ) {
                Text("拖到这里删除", color = Color.White)
            }
        }
    }
}

二、自定义拖拽实现(Modifier.offset)

如果现有库无法满足需求,可以直接基于官方API实现全自定义逻辑:

  1. 为每个网格项添加拖拽手势监听,维护拖拽状态(是否拖拽、偏移量)
  2. 通过Modifier.offset实时更新拖拽项位置
  3. 拖拽过程中分别处理网格内重排和拖出删除逻辑
    核心代码片段:
@Composable
fun CustomGridItem(
    item: GridItem,
    isDragging: Boolean,
    offset: Offset,
    onDragStart: () -> Unit,
    onDrag: (Offset) -> Unit,
    onDragEnd: (Boolean) -> Unit
) {
    Surface(
        elevation = if (isDragging) 8.dp else 0.dp,
        modifier = Modifier
            .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragStart = { onDragStart() },
                    onDrag = { _, dragAmount -> onDrag(dragAmount) },
                    onDragEnd = { onDragEnd(false) },
                    onDragCancel = { onDragEnd(false) }
                )
            }
    ) {
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(Color.White)
                .padding(8.dp),
            contentAlignment = Alignment.Center
        ) {
            Text(text = item.text, style = MaterialTheme.typography.bodyMedium)
        }
    }
}

在父组件中维护拖拽项的ID、偏移量,通过计算拖拽位置判断是重排还是删除。

三、关键注意点

  • 拖拽时需处理LazyGrid的滚动冲突,可临时禁用网格滚动
  • 删除项后要同步更新LazyGridState,避免布局异常
  • 回收站区域的交互反馈要清晰,比如拖拽进入时改变背景色

内容的提问来源于stack exchange,提问作者Trash Panda

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 17:37:04