Jetpack Compose LazyColumn 实现带动画的滑动删除并解决滚动卡顿问题
问题根因
你遇到的滚动卡顿、上滑跳顶部的问题由以下几个错误实现导致:
- 每个列表项内部嵌套
AnimatedVisibility自行控制显隐,会破坏LazyColumn的布局测量逻辑,滚动时条目位置计算错乱,是跳转顶部的核心诱因 rememberDismissState、unread等状态没有绑定条目唯一标识,列表滚动条目复用时状态会发生串位,引发卡顿和状态异常- 全局
columnAppeared变量控制入场动画的逻辑错误,新条目滚动进可视区域时会触发不必要的重绘
正确实现方案
Jetpack Compose 已经提供了原生的列表条目位移动画支持animateItemPlacement,效果和原生RecyclerView搭配DiffUtil的插入/移除动画完全一致,不需要手动嵌套AnimatedVisibility控制显隐,修改后的代码如下:
// UI层先收集ViewModel中的列表数据,确保用生命周期感知的方式收集 val products by viewModel.products.collectAsStateWithLifecycle() LazyColumn(state = listState) { items( items = products, key = { listItem: InventoryEntity -> listItem.inventoryId } ) { item -> // 状态绑定item的唯一id,避免复用错乱 var unread by remember(item.inventoryId) { mutableStateOf(false) } val dismissState = rememberDismissState( key1 = item.inventoryId, confirmStateChange = { if (it == DismissValue.DismissedToEnd) unread = !unread it != DismissValue.DismissedToEnd } ) val isDismissed = dismissState.isDismissed(DismissDirection.EndToStart) if (isDismissed) { LaunchedEffect(item.inventoryId) { // 等待动画执行完成再删除数据,和动画时长保持一致 delay(300) viewModel.deleteProduct(item.inventoryId) } } SwipeToDismiss( // 加animateItemPlacement实现条目插入/移除的平滑位移动画 modifier = Modifier .padding(vertical = 4.dp) .animateItemPlacement( animationSpec = tween(durationMillis = 300) ), state = dismissState, directions = setOf( DismissDirection.StartToEnd, DismissDirection.EndToStart ), dismissThresholds = { direction -> FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f) }, background = { val direction = dismissState.dismissDirection ?: return@SwipeToDismiss val color by animateColorAsState( when (dismissState.targetValue) { DismissValue.Default -> Color.LightGray DismissValue.DismissedToEnd -> Color.Green DismissValue.DismissedToStart -> Color.Red }, label = "swipeBgColor" ) val alignment = when (direction) { DismissDirection.StartToEnd -> Alignment.CenterStart DismissDirection.EndToStart -> Alignment.CenterEnd } val icon = when (direction) { DismissDirection.StartToEnd -> Icons.Default.Done DismissDirection.EndToStart -> Icons.Default.Delete } val scale by animateFloatAsState( if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f, label = "swipeIconScale" ) Box( Modifier .fillMaxSize() .background(color) .padding(horizontal = 20.dp), contentAlignment = alignment ) { Icon( icon, contentDescription = "Localized description", modifier = Modifier.scale(scale) ) } }, dismissContent = { Card( elevation = animateDpAsState( if (dismissState.dismissDirection != null) 4.dp else 0.dp, label = "cardElevation" ).value ) { ProductRow(product = item, number = item.inventoryId) } } ) } }
同时你需要在ViewModel层维护products状态,删除操作直接更新products对应的状态流即可,Compose的items函数传入key之后会自动做Diff计算,和原生DiffUtil效果一致。
额外优化建议
- 给
InventoryEntity类添加@Immutable注解,确保所有字段都是不可变的val类型,提升Compose重绘判断效率,滚动会更流畅 - 所有动画的
label参数可以按实际业务命名,方便调试,release包会自动移除不会影响性能 - 避免在LazyColumn的
items作用域内创建未绑定条目key的状态,防止条目复用时出现状态异常
内容的提问来源于stack exchange,提问作者F.Mysir
相关产品推荐
相关产品推荐

