如何使用LazyColumn的animateItemPlacement()且避免变更时自动滚动
问题:LazyColumn中animateItemPlacement导致自动滚动的解决方法
我通过LazyColumn实现待办清单样式列表,规则是未完成项排在前面、已完成项排在最后,点击列表项可切换其完成状态。当前实现整体正常,但使用Modifier.animateItemPlacement()后出现副作用:切换可见列表项时,LazyListState会跟随元素滚动,不符合预期——我希望列表保持当前滚动位置。现有方案仅能解决顶部可见项为列表第一项的场景,若滚动到中间项点击,列表仍会跟随滚动。想了解如何将自动滚动与项放置动画解耦,两者似乎都依赖LazyColumn的key参数?
以下是最小可复现示例:
data class TodoItem(val id: Int, val label: String, var isDone: Boolean) @Composable fun TodoCard(item: TodoItem, modifier: Modifier, onClick: () -> Unit) { val imagePainterDone = rememberVectorPainter(Icons.Outlined.Done) val imagePainterNotDone = rememberVectorPainter(Icons.Outlined.Add) Card( modifier .padding(8.dp) .fillMaxWidth() .clickable { onClick() }) { Row { Image( if (item.isDone) imagePainterDone else imagePainterNotDone, null, modifier = Modifier.size(80.dp) ) Text(text = item.label) } } } @OptIn(ExperimentalFoundationApi::class) @Composable fun ExampleColumn() { val todoItems = remember { val list = mutableStateListOf<TodoItem>() for (i in 0..20) { list.add(TodoItem(i, "Todo $i", isDone = false)) } list } val sortedTodoItems by remember { derivedStateOf { todoItems.sortedWith(compareBy({ it.isDone }, { it.id })) } } LazyColumn { items(sortedTodoItems, key = {it.label}) { item -> TodoCard(item = item, modifier = Modifier.animateItemPlacement()) { val index = todoItems.indexOfFirst { it.label == item.label } if (index < 0) return@TodoCard todoItems[index] = todoItems[index].copy(isDone = !todoItems[index].isDone) } } } }
解决方案:记录并恢复滚动位置
问题核心是animateItemPlacement()触发项位置变化时,LazyColumn默认会保持该项在视口中,导致自动滚动。要解耦动画与滚动,需在状态变更前后记录、恢复滚动位置:
修改后的完整代码
data class TodoItem(val id: Int, val label: String, var isDone: Boolean) @Composable fun TodoCard(item: TodoItem, modifier: Modifier, onClick: () -> Unit) { val imagePainterDone = rememberVectorPainter(Icons.Outlined.Done) val imagePainterNotDone = rememberVectorPainter(Icons.Outlined.Add) Card( modifier .padding(8.dp) .fillMaxWidth() .clickable { onClick() }) { Row { Image( if (item.isDone) imagePainterDone else imagePainterNotDone, contentDescription = if (item.isDone) "已完成" else "未完成", modifier = Modifier.size(80.dp) ) Text(text = item.label, modifier = Modifier.align(Alignment.CenterVertically)) } } } @OptIn(ExperimentalFoundationApi::class) @Composable fun ExampleColumn() { val todoItems = remember { val list = mutableStateListOf<TodoItem>() for (i in 0..20) { list.add(TodoItem(i, "Todo $i", isDone = false)) } list } val sortedTodoItems by remember { derivedStateOf { todoItems.sortedWith(compareBy({ it.isDone }, { it.id })) } } val listState = rememberLazyListState() // 保存当前滚动的索引和偏移量 var scrollIndex by remember { mutableStateOf(0) } var scrollOffset by remember { mutableStateOf(0) } LazyColumn(state = listState) { // 改用唯一id作为key,避免label重复导致的识别错误 items(sortedTodoItems, key = { it.id }) { item -> TodoCard(item = item, modifier = Modifier.animateItemPlacement()) { val index = todoItems.indexOfFirst { it.id == item.id } if (index < 0) return@TodoCard // 点击时记录当前滚动状态 scrollIndex = listState.firstVisibleItemIndex scrollOffset = listState.firstVisibleItemScrollOffset // 更新项的完成状态 todoItems[index] = todoItems[index].copy(isDone = !todoItems[index].isDone) // 状态更新后恢复滚动位置 LaunchedEffect(sortedTodoItems) { listState.scrollToItem(scrollIndex, scrollOffset) } } } } }
关键说明
- 改用唯一key:将原有的
label替换为id作为项的key,确保每个列表项的标识唯一,避免因label重复导致LazyColumn识别错误。 - 记录滚动状态:在点击项之前,保存当前列表的
firstVisibleItemIndex和firstVisibleItemScrollOffset,这两个参数能精准描述当前滚动位置。 - 恢复滚动位置:通过
LaunchedEffect监听排序后的列表变化,在项完成位置动画后,调用scrollToItem恢复到之前记录的滚动位置,彻底解耦动画与自动滚动行为。
内容的提问来源于stack exchange,提问作者Tim
相关产品推荐
相关产品推荐

