在现代移动应用中,流畅的动画效果是提升用户体验的关键因素之一。本文将深入探讨如何在Jetpack Compose中使用AnimatedVisibility实现优雅的列表项动画效果。
AnimatedVisibility
是Jetpack Compose动画库中的核心组件,它可以根据布尔值状态的变化,自动应用进入和退出动画。其工作原理基于Compose的声明式UI特性:
特性 | Jetpack Compose (AnimatedVisibility) | 传统视图系统 (RecyclerView.ItemAnimator) |
---|---|---|
API复杂度 | 声明式,简单直观 | 命令式,需实现多个回调 |
可组合性 | 支持任意组合动画 | 有限组合 |
学习曲线 | 低 | 高 |
性能 | 基于Compose运行时,高效 | 依赖视图系统,可能卡顿 |
灵活性 | 高,可定制任何动画 | 中等,需处理视图操作 |
在build.gradle
中添加必要依赖:
dependencies {
implementation "androidx.compose.animation:animation:1.7.0"
implementation "androidx.compose.material3:material3:1.2.1"
}
// 列表项数据类
data class ListItem(
val id: Int, // 唯一标识符
val title: String, // 显示文本
var visible: Boolean = true // 控制动画的可见状态
)
@Composable
fun ListItemCard(item: ListItem, onRemove: () -> Unit) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
elevation = CardDefaults.cardElevation(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = item.title,
style = MaterialTheme.typography.titleMedium
)
IconButton(
onClick = onRemove,
modifier = Modifier.size(24.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除",
tint = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedListScreen() {
// 创建可变的列表状态
val listItems = remember {
mutableStateListOf(
ListItem(1, "Item 1"),
ListItem(2, "Item 2"),
ListItem(3, "Item 3"),
ListItem(4, "Item 4"),
ListItem(5, "Item 5")
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// 控制按钮区域
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// 添加按钮
Button(
onClick = {
val newId = (listItems.maxOfOrNull { it.id } ?: 0) + 1
listItems.add(ListItem(newId, "New Item $newId"))
}
) {
Text("添加项目")
}
// 重置按钮
Button(
onClick = {
listItems.clear()
listItems.addAll(listOf(
ListItem(1, "Item 1"),
ListItem(2, "Item 2"),
ListItem(3, "Item 3")
))
}
) {
Text("重置列表")
}
}
Spacer(modifier = Modifier.height(16.dp))
// 列表视图
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = listItems,
key = { it.id } // 关键:确保每个项有唯一标识
) { item ->
AnimatedVisibility(
visible = item.visible,
enter = fadeIn(animationSpec = tween(300)) +
expandVertically(
animationSpec = tween(300),
expandFrom = Alignment.Top
),
exit = fadeOut(animationSpec = tween(300)) +
shrinkVertically(
animationSpec = tween(300),
shrinkTowards = Alignment.Top
),
modifier = Modifier.animateEnterExit()
) {
ListItemCard(
item = item,
onRemove = {
// 触发退出动画
val index = listItems.indexOfFirst { it.id == item.id }
if (index != -1) {
// 更新状态触发重组
listItems[index] = listItems[index].copy(visible = false)
// 延迟移除以完成动画
LaunchedEffect(item.id) {
delay(350) // 稍长于动画持续时间
listItems.removeAll { it.id == item.id }
}
}
}
)
}
}
}
}
}
// 滑动动画
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally(
animationSpec = tween(400),
initialOffsetX = { fullWidth -> fullWidth } // 从右侧滑入
) + fadeIn(),
exit = slideOutHorizontally(
animationSpec = tween(400),
targetOffsetX = { fullWidth -> -fullWidth } // 向左侧滑出
) + fadeOut()
) {
// 内容
}
// 缩放动画
AnimatedVisibility(
visible = visible,
enter = scaleIn(animationSpec = tween(300)) + fadeIn(),
exit = scaleOut(animationSpec = tween(300)) + fadeOut()
) {
// 内容
}
// 旋转动画
AnimatedVisibility(
visible = visible,
enter = fadeIn() + rotateIn(degrees = 90),
exit = fadeOut() + rotateOut(degrees = -90)
) {
// 内容
}
// 顺序执行动画
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(100)) +
expandVertically(animationSpec = tween(300)),
exit = shrinkVertically(animationSpec = tween(300)) +
fadeOut(animationSpec = tween(100))
) {
// 内容
}
// 使用不同的缓动曲线
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(500, easing = LinearOutSlowInEasing)) +
expandVertically(animationSpec = tween(500, easing = FastOutSlowInEasing)),
exit = fadeOut(animationSpec = tween(300, easing = LinearEasing)) +
shrinkVertically(animationSpec = tween(300, easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)))
) {
// 内容
}
问题1:列表项跳动或闪烁
key
属性问题2:动画中断不流畅
LaunchedEffect
确保动画完成后再移除数据项问题3:多个动画不同步
animationSpec
配置所有相关动画问题4:退出动画未完成就重组
AnimatedVisibility
内部通过Transition
管理动画状态:
// androidx/compose/animation/core/Transition.kt
internal class TransitionState<S> {
// 管理当前状态和目标状态
var targetState: S by mutableStateOf(initialState)
// 动画状态机
val isRunning: Boolean
get() = // 计算是否正在运行
}
// androidx/compose/animation/AnimatedVisibility.kt
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
// 创建过渡状态
val transition = updateTransition(visible, label = "AnimatedVisibility")
// 根据状态应用动画
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
// 动画组合操作符重载
operator fun EnterTransition.plus(enter: EnterTransition): EnterTransition {
// 合并动画效果
return EnterTransitionImpl(
data = this.data + enter.data,
animations = this.animations + enter.animations
)
}
// 使用Modifier.animateItemPlacement
LazyColumn {
items(items, key = { it.id }) { item ->
Card(
modifier = Modifier
.animateItemPlacement()
.dragAndDrop()
) {
// 内容
}
}
}
val scrollState = rememberLazyListState()
LazyColumn(state = scrollState) {
items(items) { item ->
val visibility by remember {
derivedStateOf {
// 根据滚动位置计算可见性
// ...
}
}
AnimatedVisibility(visible = visibility) {
// 内容
}
}
}
AnimatedVisibility(
visible = expanded,
enter = fadeIn() + expandVertically() +
sharedElementEnterTransition(),
exit = fadeOut() + shrinkVertically() +
sharedElementExitTransition()
) {
// 详情视图
}
+
操作符组合多种动画效果掌握Jetpack Compose的动画能力,可以显著提升应用的用户体验。本文介绍的技术不仅适用于列表项,也可应用于各种UI元素的动画效果实现。