Jetpack Compose 动画完全指南

目录

  1. 动画基础概念
  2. 核心动画API
  3. 实用动画示例
  4. 性能优化与最佳实践
  5. 常见问题与避坑指南
  6. 高级动画技巧

动画基础概念

什么是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的动画系统提供了强大而灵活的工具来创建流畅的用户界面。关键要点包括:

  1. 选择合适的API: 根据需求选择animateXxxAsStateAnimatedVisibilityAnimatedContentTransition
  2. 性能优化: 避免不必要的重组,使用graphicsLayer进行变换,合理使用remember
  3. 状态管理: 正确管理动画状态,避免状态冲突
  4. 用户体验: 选择合适的动画时长和缓动函数,提供视觉反馈
  5. 调试和测试: 使用动画标签进行调试,测试不同设备上的性能

通过遵循这些最佳实践和避坑指南,你可以创建出既美观又高性能的动画效果,提升应用的用户体验。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容