24、Compose动画与列表:LazyColumn动画、items动画、animateItemPlacement
列表动画,说实话,是Android开发里绕不开的一个坎。
我记得几年前做传统View开发时,给ListView加个删除动画,得写一堆ItemAnimator,还要处理各种边界情况。那时候我就想,要是能像iOS那样丝滑就好了。后来Compose出来了,我第一时间试了它的列表动画——嗯,这才是我想要的。
为什么列表动画这么重要?
你想想看,用户每天滑动列表,删除条目,新增数据,调整顺序。如果这些操作都是生硬的「闪现」,体验就很廉价。而加上动画后,整个App的质感会提升一个档次。
说白了,列表动画就是给数据变化加上「过渡」。数据变了,UI怎么从旧状态平滑到新状态?这就是我们要解决的问题。
LazyColumn的基础动画支持
Compose的LazyColumn默认就带了一些动画能力,但需要你主动开启。我个人习惯是,只要列表有增删改操作,就加上动画,成本很低,收益很高。
这个修饰符是LazyColumn动画的基石。它告诉Compose:当item的位置发生变化时,请用动画过渡过去。
@Composable
fun AnimatedLazyColumn() {
val items = remember { mutableStateListOf("A", "B", "C", "D", "E") }
LazyColumn {
items(items, key = { it }) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.animateItemPlacement() // 关键:位置变化动画
)
}
}
}
这里有个坑,我必须要说——key参数必须传。没有key,Compose就不知道哪个item是哪个,动画自然就失效了。我曾经在这个问题上卡了半小时,后来发现是key没写对。
items动画:增删的优雅处理
光有位置动画还不够。删除一个item时,我们希望它先淡出,然后其他item再移动上来。新增时,希望它从某个方向滑入。
这就需要用到AnimatedVisibility了。把它和animateItemPlacement组合起来,效果就很完整。
@Composable
fun AnimatedListWithAddRemove() {
val items = remember { mutableStateListOf("Item 1", "Item 2", "Item 3") }
var counter by remember { mutableIntStateOf(4) }
LazyColumn {
items(items, key = { it }) { item ->
AnimatedVisibility(
visible = true,
enter = slideInVertically() + fadeIn(),
exit = slideOutVertically() + fadeOut()
) {
ListItem(
text = item,
modifier = Modifier.animateItemPlacement()
)
}
}
}
}
注意看,AnimatedVisibility包裹了每个item,定义了进入和退出的动画。而animateItemPlacement负责处理item之间的位置调整。两者分工明确,互不干扰。
animateItemPlacement 的底层逻辑
你可能会问:这个修饰符到底是怎么工作的?
其实原理不复杂。Compose在布局阶段会记录每个item的位置。当下一次重组时,如果某个item的key还在,但位置变了,Compose就会计算位置差值,然后用动画插值器把这个差值过渡掉。
说白了,就是「旧位置 → 新位置」的平滑过渡。没有魔法,只有扎实的差值计算。
性能考量:别让动画拖垮列表
动画虽好,但也要注意性能。LazyColumn本身是懒加载的,但加上动画后,Compose需要维护更多状态。
| 场景 | 性能影响 | 建议 |
|---|---|---|
| 少量item(<20) | 几乎无影响 | 放心使用 |
| 中等数量(20-100) | 轻微影响 | 注意动画时长,别太长 |
| 大量item(>100) | 明显卡顿风险 | 考虑只对可见item加动画 |
animateItemPlacement的item内部使用复杂的布局或大量Modifier。每个item的动画都会触发重组,复杂布局会让重组成本飙升。
实战:一个完整的可拖拽排序列表
说到列表动画,最经典的应用场景就是拖拽排序了。配合animateItemPlacement,实现起来比传统View简单太多。
@Composable
fun DraggableList() {
val items = remember { mutableStateListOf("A", "B", "C", "D", "E") }
var draggedItem by remember { mutableStateOf<String?>(null) }
LazyColumn {
items(items, key = { it }) { item ->
val isDragged = draggedItem == item
ListItem(
text = item,
modifier = Modifier
.fillMaxWidth()
.animateItemPlacement() // 位置变化动画
.graphicsLayer {
scaleX = if (isDragged) 1.05f else 1f
scaleY = if (isDragged) 1.05f else 1f
shadowElevation = if (isDragged) 8f else 0f
}
)
}
}
}
这里我用了graphicsLayer来给拖拽中的item加一点放大和阴影效果。注意,animateItemPlacement依然在正常工作——当拖拽导致其他item移位时,它们会平滑地让出位置。
常见坑点与避坑指南
- key必须唯一且稳定——用数据本身的ID,不要用index。index变化会导致动画错乱。
- 不要嵌套LazyColumn——嵌套滚动加动画,性能直接崩。我踩过这个坑。
- 动画时长别太长——默认的Spring动画其实挺好,非要改的话,300ms以内就够了。
- 注意item的measure——如果item高度不固定,动画可能会跳。尽量给固定高度或用
height(IntrinsicSize.Min)。
animateItemPlacement放到最外层的Modifier链上。越早应用,Compose的优化空间越大。
说实话,Compose的列表动画已经做得相当成熟了。相比传统View那套繁琐的ItemAnimator,Compose的思路更清晰——用修饰符声明式地描述动画意图,剩下的交给框架。
我个人建议,只要你的列表涉及数据变化,就加上animateItemPlacement。成本几乎为零,但用户体验的提升是实打实的。你想想看,用户删除一条笔记时,其他笔记平滑地补上来,那种「顺滑感」就是好App和普通App的分水岭。
公众号:蓝海资料掘金营,微信deep3321