目录
动画基础概念
什么是Compose动画
Jetpack Compose提供了强大而灵活的动画系统,让开发者能够轻松创建流畅、自然的用户界面动画。Compose动画基于以下核心原则:
- 声明式: 描述动画的最终状态,而不是具体的执行步骤
- 可组合: 动画可以与其他Composable函数无缝集成
- 类型安全: 编译时检查,减少运行时错误
- 性能优化: 自动优化动画性能,避免不必要的重组
动画类型分类
// 1. 值动画 - 对单个值进行动画处理
val animatedFloat by animateFloatAsState(targetValue = if (expanded) 1f else 0f)
// 2. 内容动画 - 对Composable内容进行动画处理
AnimatedVisibility(visible = isVisible) {
Text("Hello World")
}
// 3. 布局动画 - 对布局变化进行动画处理
AnimatedContent(targetState = currentPage) { page ->
PageContent(page)
}
// 4. 手势动画 - 响应用户手势的动画
val offsetX by animateDpAsState(
targetValue = if (isDragging) dragOffset else 0.dp
)
核心动画API
1. animateXxxAsState 系列
这是最常用的动画API,用于对单个值进行动画处理:
@Composable
fun AnimatedButton() {
var isPressed by remember { mutableStateOf(false) }
// 动画化按钮的缩放比例
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "button_scale" // 用于调试的标签
)
// 动画化按钮的颜色
val backgroundColor by animateColorAsState(
targetValue = if (isPressed) Color.Blue.copy(alpha = 0.8f) else Color.Blue,
animationSpec = tween(durationMillis = 150),
label = "button_color"
)
Button(
onClick = { /* 处理点击 */ },
modifier = Modifier
.scale(scale)
.pointerInput(Unit) {
detectTapGestures(
onPress = {
isPressed = true
tryAwaitRelease()
isPressed = false
}
)
},
colors = ButtonDefaults.buttonColors(containerColor = backgroundColor)
) {
Text("点击我")
}
}
2. AnimatedVisibility
用于控制Composable的显示和隐藏动画:
@Composable
fun ExpandableCard() {
var expanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable { expanded = !expanded }
) {
Column {
Text(
text = "点击展开/收起",
modifier = Modifier.padding(16.dp)
)
// 使用AnimatedVisibility实现展开/收起动画
AnimatedVisibility(
visible = expanded,
enter = expandVertically(
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium
)
) + fadeIn(),
exit = shrinkVertically(
animationSpec = spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = Spring.StiffnessMedium
)
) + fadeOut()
) {
Text(
text = "这是展开的内容区域。可以包含任意复杂的布局和组件。",
modifier = Modifier.padding(16.dp)
)
}
}
}
}
3. AnimatedContent
用于在不同内容之间切换时添加动画:
@Composable
fun TabSwitcher() {
var selectedTab by remember { mutableStateOf(0) }
val tabs = listOf("首页", "发现", "我的")
Column {
// Tab选择器
TabRow(selectedTabIndex = selectedTab) {
tabs.forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = { selectedTab = index },
text = { Text(title) }
)
}
}
// 内容区域,使用AnimatedContent实现切换动画
AnimatedContent(
targetState = selectedTab,
transitionSpec = {
// 定义进入和退出动画
slideInHorizontally(
initialOffsetX = { fullWidth ->
if (targetState > initialState) fullWidth else -fullWidth
}
) + fadeIn() with slideOutHorizontally(
targetOffsetX = { fullWidth ->
if (targetState > initialState) -fullWidth else fullWidth
}
) + fadeOut()
},
label = "tab_content"
) { tabIndex ->
when (tabIndex) {
0 -> HomeContent()
1 -> DiscoverContent()
2 -> ProfileContent()
}
}
}
}
@Composable
fun HomeContent() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Text("首页内容", style = MaterialTheme.typography.headlineMedium)
}
}
@Composable
fun DiscoverContent() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Green.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Text("发现内容", style = MaterialTheme.typography.headlineMedium)
}
}
@Composable
fun ProfileContent() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Blue.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Text("我的内容", style = MaterialTheme.typography.headlineMedium)
}
}
4. Transition API
用于创建复杂的多属性动画:
@Composable
fun MultiPropertyAnimation() {
var isLiked by remember { mutableStateOf(false) }
// 创建Transition来管理多个动画属性
val transition = updateTransition(
targetState = isLiked,
label = "like_transition"
)
// 定义多个动画属性
val scale by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// 点赞时的弹性动画
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
} else {
// 取消点赞时的快速动画
tween(durationMillis = 200)
}
},
label = "scale"
) { liked ->
if (liked) 1.2f else 1f
}
val color by transition.animateColor(
transitionSpec = { tween(durationMillis = 300) },
label = "color"
) { liked ->
if (liked) Color.Red else Color.Gray
}
val rotation by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
tween(durationMillis = 500)
} else {
tween(durationMillis = 200)
}
},
label = "rotation"
) { liked ->
if (liked) 360f else 0f
}
IconButton(
onClick = { isLiked = !isLiked },
modifier = Modifier
.scale(scale)
.rotate(rotation)
) {
Icon(
imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
contentDescription = "点赞",
tint = color,
modifier = Modifier.size(32.dp)
)
}
}
实用动画示例
1. 加载动画
@Composable
fun LoadingAnimation() {
val infiniteTransition = rememberInfiniteTransition(label = "loading")
// 旋转动画
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
),
label = "rotation"
)
// 缩放动画
val scale by infiniteTransition.animateFloat(
initialValue = 0.8f,
targetValue = 1.2f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "scale"
)
Box(
modifier = Modifier
.size(60.dp)
.scale(scale)
.rotate(rotation)
.background(
brush = Brush.sweepGradient(
colors = listOf(
Color.Blue,
Color.Cyan,
Color.Blue
)
),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
modifier = Modifier.size(40.dp),
color = Color.White,
strokeWidth = 3.dp
)
}
}
2. 列表项动画
@Composable
fun AnimatedListItem(
item: String,
index: Int,
onRemove: () -> Unit
) {
var isVisible by remember { mutableStateOf(false) }
// 进入动画延迟,创造错落有致的效果
LaunchedEffect(Unit) {
delay(index * 50L) // 每个项目延迟50ms
isVisible = true
}
AnimatedVisibility(
visible = isVisible,
enter = slideInVertically(
initialOffsetY = { it },
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
) + fadeIn(
animationSpec = tween(durationMillis = 300)
),
exit = slideOutHorizontally(
targetOffsetX = { -it },
animationSpec = tween(durationMillis = 300)
) + fadeOut(
animationSpec = tween(durationMillis = 300)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
.animateContentSize(), // 自动处理内容大小变化的动画
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item,
style = MaterialTheme.typography.bodyLarge
)
IconButton(
onClick = {
isVisible = false
// 等待退出动画完成后再移除项目
// 在实际应用中,你可能需要使用协程来处理这个延迟
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除"
)
}
}
}
}
}
3. 手势驱动动画
@Composable
fun SwipeToRevealCard() {
var offsetX by remember { mutableStateOf(0f) }
val maxSwipeDistance = 200f
// 使用animateFloatAsState来平滑回弹
val animatedOffsetX by animateFloatAsState(
targetValue = offsetX,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
),
label = "swipe_offset"
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
) {
// 背景操作按钮
Row(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = { /* 删除操作 */ },
modifier = Modifier.padding(end = 16.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "删除",
tint = Color.White
)
}
}
// 前景卡片
Card(
modifier = Modifier
.fillMaxSize()
.offset { IntOffset(animatedOffsetX.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragEnd = {
// 根据滑动距离决定是否显示操作按钮
offsetX = if (offsetX < -maxSwipeDistance / 2) {
-maxSwipeDistance
} else {
0f
}
}
) { _, dragAmount ->
// 限制滑动范围
val newOffset = (offsetX + dragAmount).coerceIn(-maxSwipeDistance, 0f)
offsetX = newOffset
}
},
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "向左滑动查看操作",
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
性能优化与最佳实践
1. 避免不必要的重组
// ❌ 错误做法:每次动画更新都会重组整个Composable
@Composable
fun BadAnimationExample() {
var animatedValue by remember { mutableStateOf(0f) }
val animatedFloat by animateFloatAsState(animatedValue)
// 这里会在每次动画更新时重组
Column {
Text("当前值: $animatedFloat") // 这会导致频繁重组
// ... 其他复杂的UI组件
}
}
// ✅ 正确做法:将动画值的使用限制在需要的地方
@Composable
fun GoodAnimationExample() {
var animatedValue by remember { mutableStateOf(0f) }
Column {
// 将动画逻辑封装在单独的Composable中
AnimatedValueDisplay(animatedValue)
// ... 其他不受动画影响的UI组件
}
}
@Composable
fun AnimatedValueDisplay(targetValue: Float) {
val animatedFloat by animateFloatAsState(
targetValue = targetValue,
label = "value_animation"
)
Text("当前值: $animatedFloat")
}
2. 使用合适的AnimationSpec
@Composable
fun AnimationSpecExamples() {
var isExpanded by remember { mutableStateOf(false) }
Column {
// 1. spring - 适用于自然的物理动画
val springScale by animateFloatAsState(
targetValue = if (isExpanded) 1.2f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy, // 阻尼比
stiffness = Spring.StiffnessMedium // 刚度
)
)
// 2. tween - 适用于精确控制时长的动画
val tweenAlpha by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0.5f,
animationSpec = tween(
durationMillis = 300,
delayMillis = 100,
easing = FastOutSlowInEasing // 缓动函数
)
)
// 3. keyframes - 适用于复杂的关键帧动画
val keyframeRotation by animateFloatAsState(
targetValue = if (isExpanded) 180f else 0f,
animationSpec = keyframes {
durationMillis = 600
0f at 0 with LinearEasing
90f at 200 with FastOutSlowInEasing
180f at 600 with LinearOutSlowInEasing
}
)
// 4. snap - 适用于无动画的即时变化
val snapColor by animateColorAsState(
targetValue = if (isExpanded) Color.Blue else Color.Gray,
animationSpec = snap() // 立即变化,无动画
)
}
}
3. 内存和性能优化
@Composable
fun PerformanceOptimizedAnimation() {
var isVisible by remember { mutableStateOf(true) }
// ✅ 使用remember来缓存昂贵的计算
val expensiveAnimationSpec = remember {
spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessMedium
)
}
// ✅ 为动画添加标签,便于调试和性能分析
val animatedAlpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = expensiveAnimationSpec,
label = "visibility_alpha" // 调试标签
)
// ✅ 使用Modifier.graphicsLayer来避免布局重组
Box(
modifier = Modifier
.size(100.dp)
.graphicsLayer {
alpha = animatedAlpha
// 在graphicsLayer中进行变换可以避免重新布局
}
.background(Color.Blue)
)
}
常见问题与避坑指南
1. 动画状态管理
// ❌ 常见错误:在错误的地方管理动画状态
@Composable
fun BadStateManagement() {
// 错误:每次重组都会重置状态
var isAnimating = false
val animatedValue by animateFloatAsState(
targetValue = if (isAnimating) 1f else 0f
)
}
// ✅ 正确做法:使用remember管理状态
@Composable
fun GoodStateManagement() {
// 正确:状态在重组间保持
var isAnimating by remember { mutableStateOf(false) }
val animatedValue by animateFloatAsState(
targetValue = if (isAnimating) 1f else 0f
)
// 可以安全地更新状态
Button(onClick = { isAnimating = !isAnimating }) {
Text("切换动画")
}
}
2. 避免动画冲突
@Composable
fun AvoidAnimationConflicts() {
var state by remember { mutableStateOf(0) }
// ❌ 错误:多个动画可能会冲突
val badScale1 by animateFloatAsState(if (state == 1) 1.2f else 1f)
val badScale2 by animateFloatAsState(if (state == 2) 0.8f else 1f)
// ✅ 正确:使用单一的动画源
val goodScale by animateFloatAsState(
targetValue = when (state) {
1 -> 1.2f
2 -> 0.8f
else -> 1f
}
)
Box(
modifier = Modifier
.size(100.dp)
.scale(goodScale) // 使用单一的缩放值
.background(Color.Blue)
)
}
3. 处理动画完成回调
@Composable
fun AnimationCompletionHandling() {
var isExpanded by remember { mutableStateOf(false) }
var animationCompleted by remember { mutableStateOf(false) }
val animatedHeight by animateDpAsState(
targetValue = if (isExpanded) 200.dp else 100.dp,
animationSpec = tween(durationMillis = 500),
finishedListener = { finalValue ->
// 动画完成时的回调
animationCompleted = true
println("动画完成,最终值: $finalValue")
}
)
Column {
Button(
onClick = {
isExpanded = !isExpanded
animationCompleted = false
}
) {
Text("切换高度")
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(animatedHeight)
.background(Color.Blue)
) {
if (animationCompleted) {
Text(
"动画已完成",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
4. 处理快速状态变化
@Composable
fun HandleRapidStateChanges() {
var counter by remember { mutableStateOf(0) }
// 使用LaunchedEffect来防止过于频繁的状态更新
var debouncedCounter by remember { mutableStateOf(0) }
LaunchedEffect(counter) {
delay(100) // 防抖延迟
debouncedCounter = counter
}
val animatedScale by animateFloatAsState(
targetValue = 1f + (debouncedCounter % 3) * 0.1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy
)
)
Column {
Text("计数器: $counter")
Text("防抖计数器: $debouncedCounter")
Button(
onClick = { counter++ },
modifier = Modifier.scale(animatedScale)
) {
Text("快速点击")
}
}
}
高级动画技巧
1. 自定义动画规格
// 创建自定义的缓动函数
val customEasing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
// 创建自定义的弹簧动画
val customSpring = spring<Float>(
dampingRatio = 0.8f,
stiffness = 300f,
visibilityThreshold = 0.01f
)
// 创建复杂的关键帧动画
val complexKeyframes = keyframes<Float> {
durationMillis = 1000
0f at 0 with LinearEasing
0.5f at 200 with FastOutSlowInEasing
0.8f at 500 with LinearOutSlowInEasing
1f at 1000 with FastOutLinearInEasing
}
@Composable
fun CustomAnimationSpecs() {
var isActive by remember { mutableStateOf(false) }
val customAnimatedValue by animateFloatAsState(
targetValue = if (isActive) 1f else 0f,
animationSpec = complexKeyframes
)
// 使用自定义动画值
Box(
modifier = Modifier
.size(100.dp)
.graphicsLayer {
scaleX = customAnimatedValue
scaleY = customAnimatedValue
rotationZ = customAnimatedValue * 360f
}
.background(Color.Blue)
)
}
2. 链式动画
@Composable
fun ChainedAnimations() {
var triggerAnimation by remember { mutableStateOf(false) }
var currentStep by remember { mutableStateOf(0) }
// 第一步:缩放动画
val scale by animateFloatAsState(
targetValue = if (triggerAnimation && currentStep >= 0) 1.2f else 1f,
animationSpec = tween(300),
finishedListener = {
if (triggerAnimation && currentStep == 0) {
currentStep = 1 // 触发下一步
}
}
)
// 第二步:旋转动画
val rotation by animateFloatAsState(
targetValue = if (triggerAnimation && currentStep >= 1) 180f else 0f,
animationSpec = tween(400),
finishedListener = {
if (triggerAnimation && currentStep == 1) {
currentStep = 2 // 触发下一步
}
}
)
// 第三步:颜色动画
val color by animateColorAsState(
targetValue = if (triggerAnimation && currentStep >= 2) Color.Red else Color.Blue,
animationSpec = tween(300),
finishedListener = {
if (triggerAnimation && currentStep == 2) {
// 动画链完成
triggerAnimation = false
currentStep = 0
}
}
)
Column {
Button(
onClick = {
triggerAnimation = true
currentStep = 0
}
) {
Text("开始链式动画")
}
Box(
modifier = Modifier
.size(100.dp)
.scale(scale)
.rotate(rotation)
.background(color)
)
}
}
3. 物理模拟动画
@Composable
fun PhysicsBasedAnimation() {
var position by remember { mutableStateOf(Offset.Zero) }
var velocity by remember { mutableStateOf(Offset.Zero) }
// 使用Animatable进行更精细的控制
val animatableX = remember { Animatable(0f) }
val animatableY = remember { Animatable(0f) }
LaunchedEffect(position) {
// 模拟重力和阻尼
launch {
animatableX.animateTo(
targetValue = position.x,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
}
launch {
animatableY.animateTo(
targetValue = position.y,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { offset ->
position = offset
}
}
) {
Box(
modifier = Modifier
.size(50.dp)
.offset {
IntOffset(
animatableX.value.roundToInt(),
animatableY.value.roundToInt()
)
}
.background(Color.Red, CircleShape)
)
}
}
总结
Jetpack Compose的动画系统提供了强大而灵活的工具来创建流畅的用户界面。关键要点包括:
-
选择合适的API: 根据需求选择
animateXxxAsState
、AnimatedVisibility
、AnimatedContent
或Transition
-
性能优化: 避免不必要的重组,使用
graphicsLayer
进行变换,合理使用remember
- 状态管理: 正确管理动画状态,避免状态冲突
- 用户体验: 选择合适的动画时长和缓动函数,提供视觉反馈
- 调试和测试: 使用动画标签进行调试,测试不同设备上的性能
通过遵循这些最佳实践和避坑指南,你可以创建出既美观又高性能的动画效果,提升应用的用户体验。