如何使用Jetpack Compose实现可拖拽排序列表?
Jetpack Compose 实现可拖拽排序列表完整方案
核心实现不需要引入第三方库,基于Compose原生手势检测、状态管理和列表动画能力即可完成,同时支持列表和网格布局的拖拽排序。
1. 基础状态与数据定义
首先定义可观察的数据列表和拖拽相关状态,列表修改时会自动触发界面重组:
// 示例数据类,必须包含唯一key用于列表动画识别 data class SortItem(val id: Int, val content: String) @Composable fun DragSortListDemo() { // 可观察列表,元素交换时自动触发重绘 val itemList = remember { mutableStateListOf<SortItem>( *(1..20).map { SortItem(it, "列表条目 $it") }.toTypedArray() ) } // 记录当前正在拖拽的条目索引,null为无拖拽状态 var draggingIndex by remember { mutableStateOf<Int?>(null) } // 列表状态,用于获取可见条目位置信息 val listState = rememberLazyListState() // 协程作用域,用于拖拽到边缘时自动滚动列表 val coroutineScope = rememberCoroutineScope() }
2. 列表条目拖拽手势实现
给每个列表条目添加长按拖拽手势监听,拖拽过程中实时计算目标位置并交换列表元素:
LazyColumn( state = listState, modifier = Modifier.fillMaxSize() ) { items(itemList, key = { it.id }) { item -> val currentIndex = itemList.indexOf(item) val isDragging = currentIndex == draggingIndex Box( modifier = Modifier .fillMaxWidth() .animateItemPlacement() // 条目移动平滑动画 // 拖拽状态视觉效果:放大+阴影 .then( if (isDragging) Modifier .scale(1.05f) .shadow(8.dp) .background(Color.White) else Modifier ) // 长按拖拽手势核心逻辑 .pointerInput(Unit) { detectDragGesturesAfterLongPress( onDragStart = { touchOffset -> // 拖拽开始时匹配当前触摸位置对应的条目索引 draggingIndex = listState.layoutInfo.visibleItemsInfo .firstOrNull { itemInfo -> touchOffset.y >= itemInfo.offset && touchOffset.y <= itemInfo.offset + itemInfo.size }?.index }, onDrag = { change, dragAmount -> change.consume() val dragPosY = change.position.y // 计算当前触摸位置对应的目标条目索引 val targetIndex = listState.layoutInfo.visibleItemsInfo .firstOrNull { itemInfo -> dragPosY >= itemInfo.offset && dragPosY <= itemInfo.offset + itemInfo.size }?.index // 索引有效且和当前拖拽索引不一致时,交换元素 if (draggingIndex != null && targetIndex != null && draggingIndex != targetIndex) { Collections.swap(itemList, draggingIndex!!, targetIndex) draggingIndex = targetIndex } // 拖拽到列表边缘自动滚动逻辑 val scrollThreshold = 100.dp.toPx() val listHeight = listState.layoutInfo.run { viewportEndOffset - viewportStartOffset } when { dragPosY < scrollThreshold -> coroutineScope.launch { listState.scrollBy(-15f) } dragPosY > listHeight - scrollThreshold -> coroutineScope.launch { listState.scrollBy(15f) } } }, onDragEnd = { draggingIndex = null }, onDragCancel = { draggingIndex = null } ) } .padding(16.dp) ) { Text(text = item.content, fontSize = 16.sp) } } }
3. 网格布局适配
如果要实现类似drag-sort-gridview的网格拖拽效果,只需要做两处修改:
- 把
LazyColumn替换为LazyVerticalGrid,设置对应列数 - 拖拽位置匹配逻辑不需要修改,原生适配网格布局的条目索引计算
4. 可选优化点
- 拖拽过程中可以给条目添加半透明效果,提升视觉辨识度
- 拖拽时可以拦截列表的其他点击事件,避免误操作
- 如果需要支持跨列表拖拽,只需要把列表状态和数据列表提升到父级即可
内容的提问来源于stack exchange,提问作者nigripes Felis
相关产品推荐
相关产品推荐

