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

如何使用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)
                }
            }
        }
    }
}

关键说明

  1. 改用唯一key:将原有的label替换为id作为项的key,确保每个列表项的标识唯一,避免因label重复导致LazyColumn识别错误。
  2. 记录滚动状态:在点击项之前,保存当前列表的firstVisibleItemIndex和firstVisibleItemScrollOffset,这两个参数能精准描述当前滚动位置。
  3. 恢复滚动位置:通过LaunchedEffect监听排序后的列表变化,在项完成位置动画后,调用scrollToItem恢复到之前记录的滚动位置,彻底解耦动画与自动滚动行为。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 17:35:28