Compose 动画 + KMM 跨平台开发:从传统View到现代声明式UI动画

1,427 阅读12分钟

Compose 动画 + KMM 跨平台开发:从传统View到现代声明式UI动画

本文将深入探讨Compose动画技术和KMM跨平台开发,通过实战项目案例,带你掌握从基础到高级的动画开发技巧,以及如何用一套代码实现Android和iOS的跨平台动画效果,并分享性能优化和最佳实践。(由于需要在公司内部做技术分享,所以暂时不把demo代码地址放出,后续分享之后会贴上github地址)

🎬 动画效果预览

五层组合.gif

图:层次递进式动画演示 - 从基础波纹到烟花闪烁的完整动画效果

目录

  1. 技术背景与概述
  2. Compose动画API体系
  3. 项目实战案例分析
  4. 核心动画技术详解
  5. 性能优化与最佳实践
  6. KMM跨平台动画实现
  7. 总结与展望

技术背景与概述

为什么选择Compose动画?

传统View动画的痛点
// 传统View动画 - 命令式编程
val animator = ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.5f)
animator.duration = 300
animator.interpolator = BounceInterpolator()
animator.start()

// 需要手动管理生命周期、状态同步、内存泄漏等问题
Compose动画的优势
// Compose动画 - 声明式编程
val scale by animateFloatAsState(
    targetValue = if (isClicked) 1.5f else 1f,
    animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
)

Box(modifier = Modifier.scale(scale)) {
    // UI内容
}

Compose动画的核心特性

  • 声明式:描述"是什么"而不是"怎么做"
  • 状态驱动:动画自动响应状态变化
  • 可组合:动画组件可复用和组合
  • 类型安全:编译时检查,减少运行时错误
  • 性能优化:智能重组,只更新必要的部分

Compose动画API体系

动画API层次结构

Compose 动画 API
├── 高级API (开箱即用)
│   ├── AnimatedVisibility     // 显示/隐藏动画
│   ├── AnimatedContent        // 内容切换动画
│   └── Crossfade             // 淡入淡出切换
│
├── 中级API (灵活控制)
│   ├── animate*AsState       // 状态驱动动画
│   ├── rememberInfiniteTransition // 无限循环动画
│   └── updateTransition      // 多状态转换
│
└── 低级API (完全自定义)
    ├── Animatable           // 手动控制动画
    ├── AnimationSpec        // 自定义动画规格
    └── Canvas + 手动绘制    // 完全自定义绘制

1. 高级API - 开箱即用

AnimatedVisibility - 显示隐藏动画
@Composable
fun AnimatedVisibilityDemo() {
    var visible by remember { mutableStateOf(false) }

    Column {
        Button(onClick = { visible = !visible }) {
            Text("切换显示")
        }

        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            Card {
                Text("我是动画内容!", modifier = Modifier.padding(16.dp))
            }
        }
    }
}
AnimatedContent - 内容切换动画
@Composable
fun AnimatedContentDemo() {
    var count by remember { mutableIntStateOf(0) }
    
    Column {
        Button(onClick = { count++ }) {
            Text("增加计数")
        }
        
        AnimatedContent(
            targetState = count,
            transitionSpec = {
                slideInVertically { height -> height } + fadeIn() togetherWith
                slideOutVertically { height -> -height } + fadeOut()
            }
        ) { targetCount ->
            Text(
                text = "计数: $targetCount",
                style = MaterialTheme.typography.headlineMedium
            )
        }
    }
}

2. 中级API - 灵活控制

animate*AsState - 状态驱动动画
@Composable
fun StateBasedAnimationDemo() {
    var isExpanded by remember { mutableStateOf(false) }
    
    // 多个属性同时动画
    val size by animateDpAsState(
        targetValue = if (isExpanded) 200.dp else 100.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    
    val color by animateColorAsState(
        targetValue = if (isExpanded) Color.Red else Color.Blue,
        animationSpec = tween(durationMillis = 500)
    )
    
    val rotation by animateFloatAsState(
        targetValue = if (isExpanded) 180f else 0f,
        animationSpec = spring(stiffness = Spring.StiffnessMedium)
    )
    
    Box(
        modifier = Modifier
            .size(size)
            .rotate(rotation)
            .background(color, CircleShape)
            .clickable { isExpanded = !isExpanded }
    )
}
rememberInfiniteTransition - 无限循环动画
@Composable
fun InfiniteAnimationDemo() {
    val infiniteTransition = rememberInfiniteTransition()
    
    // 脉冲效果
    val scale by infiniteTransition.animateFloat(
        initialValue = 1f,
        targetValue = 1.2f,
        animationSpec = infiniteRepeatable(
            animation = tween(1000),
            repeatMode = RepeatMode.Reverse
        )
    )
    
    // 旋转效果
    val rotation by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        )
    )
    
    Box(
        modifier = Modifier
            .size(100.dp)
            .scale(scale)
            .rotate(rotation)
            .background(Color.Blue, CircleShape)
    )
}

3. 低级API - 完全自定义

Animatable - 手动控制动画
@Composable
fun ManualAnimationDemo() {
    val animatable = remember { Animatable(0f) }
    val coroutineScope = rememberCoroutineScope()
    
    LaunchedEffect(Unit) {
        // 复杂的动画序列
        animatable.animateTo(1f, tween(1000))
        animatable.animateTo(0.5f, spring())
        animatable.animateTo(1f, tween(500))
    }
    
    Box(
        modifier = Modifier
            .size(100.dp)
            .scale(animatable.value)
            .background(Color.Green, CircleShape)
            .clickable {
                coroutineScope.launch {
                    // 手动触发动画
                    animatable.animateTo(
                        targetValue = if (animatable.value == 1f) 0.5f else 1f,
                        animationSpec = spring(
                            dampingRatio = Spring.DampingRatioHighBouncy
                        )
                    )
                }
            }
    )
}

项目实战案例分析

项目架构概览

我们的动画演示项目包含5个渐进式的动画阶段:

动画演示项目
├── 第1阶段:基础波纹效果 (BaseRippleEffect)
├── 第2阶段:彩虹色彩层 (RainbowLayerEffect)  
├── 第3阶段:表情包粒子效果 (EmojiParticleEffect)
├── 第4阶段:旋转表情包效果 (RotatingParticleEffect)
└── 第5阶段:烟花闪烁效果 (FireworkEffect)

案例1:基础波纹效果 (BaseRippleEffect)

动画效果演示

单色波.gif

*图:第1阶段 - 基础波纹扩散动画效果*
技术要点
  • 使用 mutableStateOf 管理波纹列表
  • LaunchedEffect 响应点击事件
  • 协程 + delay 实现平滑动画
  • 动态列表更新和清理
核心实现
@Composable
fun BaseRippleEffect(clickCount: Int) {
    // 存储所有正在显示的波纹效果
    var allRipples by remember { mutableStateOf<List<RippleEffect>>(emptyList()) }
    
    // 记录上次的点击次数,用于检测新的点击
    var lastClickCount by remember { mutableIntStateOf(0) }
    
    // 协程作用域,用于启动动画协程
    val coroutineScope = rememberCoroutineScope()
    
    /**
     * 发射新的波纹效果
     * 创建一个新的波纹并启动其动画
     */
    fun emitNewRipple() {
        // 使用当前点击次数作为波纹的唯一ID
        val currentRippleId = clickCount
        
        // 创建新的波纹效果,初始状态:缩放1倍,透明度0.6
        val newRipple = RippleEffect(
            id = currentRippleId,
            scale = 1f,        // 初始缩放比例
            alpha = 0.6f       // 初始透明度
        )
        
        // 将新波纹添加到列表中
        allRipples = allRipples + newRipple
        
        // 启动波纹动画协程
        coroutineScope.launch {
            var rippleScale: Float   // 当前缩放值
            var rippleAlpha: Float    // 当前透明度值

            // 动画分为180步,每步16毫秒,总时长约3秒 (180 * 16ms = 2880ms)
            repeat(180) { step ->
                // 计算动画进度 (0.0 到 1.0)
                val progress = step / 180f
                
                // 缩放动画:从1倍放大到2.5倍 (1 + 1.5 = 2.5)
                rippleScale = 1f + progress * 1.5f
                
                // 透明度动画:从0.6渐变到0 (完全透明)
                rippleAlpha = 0.6f - progress * 0.6f
                
                // 更新指定ID的波纹状态
                allRipples = allRipples.map { ripple ->
                    if (ripple.id == currentRippleId) {
                        // 更新当前波纹的缩放和透明度
                        ripple.copy(scale = rippleScale, alpha = rippleAlpha)
                    } else {
                        // 其他波纹保持不变
                        ripple
                    }
                }
                
                // 等待16毫秒,实现60FPS的动画效果
                delay(16L)
            }
            
            // 动画完成后,从列表中移除这个波纹
            allRipples = allRipples.filter { it.id != currentRippleId }
        }
    }
    
    // 监听点击次数变化,当有新的点击时触发波纹效果
    LaunchedEffect(clickCount) {
        if (clickCount > lastClickCount) {
            lastClickCount = clickCount
            emitNewRipple()
        }
    }
    
    // 渲染波纹效果的容器
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center  // 波纹从中心开始
    ) {
        // 遍历所有波纹并绘制
        allRipples.forEach { ripple ->
            Box(
                modifier = Modifier
                    .size(200.dp)                    // 波纹基础大小
                    .scale(ripple.scale)            // 应用缩放动画
                    .clip(CircleShape)              // 裁剪为圆形
                    .background(
                        // 橙色背景,应用透明度动画
                        Color(0xFFFF8C42).copy(alpha = ripple.alpha)
                    )
            )
        }
    }
}
数据模型
data class RippleEffect(
    val id: Int,
    val scale: Float,
    val alpha: Float
)

案例2:彩虹色彩层效果 (RainbowLayerEffect)

动画效果演示

彩色波2.gif

*图:第2阶段 - 彩虹渐变色彩动画效果*

技术要点
  • 使用 rememberInfiniteTransition 实现无限循环动画
  • 彩虹色彩渐变计算
  • 多层色彩叠加效果
  • 平滑的颜色过渡
核心实现
@Composable
fun RainbowLayerEffect(clickCount: Int) {
    // 存储所有正在显示的彩虹效果
    var allRainbows by remember { mutableStateOf<List<RainbowEffect>>(emptyList()) }
    
    // 记录上次的点击次数,用于检测新的点击
    var lastClickCount by remember { mutableIntStateOf(0) }
    
    // 协程作用域,用于启动动画协程
    val coroutineScope = rememberCoroutineScope()
    
    // 彩虹颜色
    val colors = listOf(
        Color(0xFFFF6B35), // 橙色
        Color(0xFFFFD23F), // 黄色
        Color(0xFF06FFA5), // 绿色
        Color(0xFF4FC3F7), // 蓝色
        Color(0xFF8B5CF6), // 紫色
        Color(0xFFEF4444)  // 红色
    )
    
    /**
     * 发射新的彩虹效果
     * 创建一个新的彩虹层并启动其动画
     */
    fun emitNewRainbow() {
        // 使用当前点击次数作为彩虹的唯一ID
        val currentRainbowId = clickCount
        
        // 根据点击次数选择颜色(循环使用6种颜色)
        val selectedColor = colors[currentRainbowId % colors.size]
        
        // 创建新的彩虹效果,初始状态:缩放1倍,透明度0.4
        val newRainbow = RainbowEffect(
            id = currentRainbowId,
            scale = 1f,        // 初始缩放比例
            alpha = 0.4f,       // 初始透明度(比波纹稍低)
            color = selectedColor // 选中的颜色
        )
        
        // 将新彩虹添加到列表中
        allRainbows = allRainbows + newRainbow
        
        // 启动彩虹动画协程
        coroutineScope.launch {
            var rainbowScale: Float   // 当前缩放值
            var rainbowAlpha: Float   // 当前透明度值
            
            // 动画分为150步,每步16毫秒,总时长约2.4秒 (150 * 16ms = 2400ms)
            repeat(150) { step ->
                // 计算动画进度 (0.0 到 1.0)
                val progress = step / 150f
                
                // 缩放动画:从1倍放大到2.8倍 (1 + 1.8 = 2.8)
                rainbowScale = 1f + progress * 1.8f
                
                // 透明度动画:从0.4渐变到0 (完全透明)
                rainbowAlpha = 0.4f - progress * 0.4f
                
                // 更新指定ID的彩虹状态
                allRainbows = allRainbows.map { rainbow ->
                    if (rainbow.id == currentRainbowId) {
                        // 更新当前彩虹的缩放和透明度
                        rainbow.copy(scale = rainbowScale, alpha = rainbowAlpha)
                    } else {
                        // 其他彩虹保持不变
                        rainbow
                    }
                }
                
                // 等待16毫秒,实现60FPS的动画效果
                delay(16L)
            }
            
            // 动画完成后,从列表中移除这个彩虹
            allRainbows = allRainbows.filter { it.id != currentRainbowId }
        }
    }
    
    // 监听点击次数变化,当有新的点击时触发彩虹效果
    LaunchedEffect(clickCount) {
        if (clickCount > lastClickCount) {
            lastClickCount = clickCount
            emitNewRainbow()
        }
    }
    
    // 渲染彩虹效果的容器
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center  // 彩虹从中心开始
    ) {
        // 遍历所有彩虹并绘制
        allRainbows.forEach { rainbow ->
            Box(
                modifier = Modifier
                    .size(180.dp)                    // 彩虹基础大小(比波纹稍小)
                    .scale(rainbow.scale)            // 应用缩放动画
                    .clip(CircleShape)               // 裁剪为圆形
                    .background(
                        // 使用彩虹的专属颜色,应用透明度动画
                        rainbow.color.copy(alpha = rainbow.alpha)
                    )
            )
        }
    }
}

案例3:表情包粒子系统 (EmojiParticleEffect)

动画效果演示

弹射表情包.gif

图:第3阶段 - 表情包粒子发射动画效果

技术特点
  • 物理模拟:模拟粒子的抛物线运动
  • 批量管理:一次创建多个粒子
  • 生命周期管理:自动清理过期粒子
  • 随机化:随机方向、距离、表情
核心实现
@Composable
fun EmojiParticleEffect(clickCount: Int) {
    // 存储所有正在显示的表情包粒子
    var allParticles by remember { mutableStateOf<List<EmojiParticle>>(emptyList()) }
    
    // 记录上次的点击次数,用于检测新的点击
    var lastClickCount by remember { mutableIntStateOf(0) }
    
    // 协程作用域,用于启动动画协程
    val coroutineScope = rememberCoroutineScope()
    
    /**
     * 发射新的一批表情包粒子
     * 创建18个表情包粒子并启动它们的动画
     */
    fun emitNewBatch() {
        // 使用当前点击次数作为批次ID
        val currentBatchId = clickCount
        
        // 创建新的一批粒子,为每个粒子分配唯一的ID
        val newParticles = createEmojiParticles().map { particle ->
            particle.copy(id = particle.id + currentBatchId * 100)
        }
        
        // 将新粒子添加到列表中
        allParticles = allParticles + newParticles
        
        // 启动粒子动画协程
        coroutineScope.launch {
            var batchProgress = 0f  // 当前批次动画进度
            
            // 动画分为60步,每步12毫秒,总时长约720毫秒
            repeat(60) { step ->
                // 计算动画进度 (0.0 到 1.0)
                batchProgress = step / 60f
                
                // 更新当前批次的所有粒子状态
                allParticles = allParticles.map { particle ->
                    if (particle.id >= currentBatchId * 100 && particle.id < (currentBatchId + 1) * 100) {
                        // 更新当前批次的粒子进度
                        particle.copy(progress = batchProgress)
                    } else {
                        // 其他批次的粒子保持不变
                        particle
                    }
                }
                
                // 等待12毫秒
                delay(12L)
            }
            
            // 确保粒子到达目标位置(progress = 1f)
            batchProgress = 1f
            allParticles = allParticles.map { particle ->
                if (particle.id >= currentBatchId * 100 && particle.id < (currentBatchId + 1) * 100) {
                    particle.copy(progress = batchProgress)
                } else {
                    particle
                }
            }
            
            // 等待粒子到达目标位置后消失
            delay(200L) // 粒子停留200毫秒后消失
            
            // 移除当前批次的所有粒子
            allParticles = allParticles.filter { 
                it.id < currentBatchId * 100 || it.id >= (currentBatchId + 1) * 100 
            }
        }
    }
    
    // 监听点击次数变化,当有新的点击时触发粒子爆炸效果
    LaunchedEffect(clickCount) {
        if (clickCount > lastClickCount) {
            lastClickCount = clickCount
            emitNewBatch()
        }
    }
    
    // 渲染粒子效果的容器
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center  // 粒子从中心发射
    ) {
        // 遍历所有粒子并绘制
        allParticles.forEach { particle ->
            val progress = particle.progress
            
            // 计算粒子的当前位置(线性插值)
            val x = particle.startX + (particle.endX - particle.startX) * progress
            val y = particle.startY + (particle.endY - particle.startY) * progress
            
            // 抛物线效果 - 增加重力效果,使粒子轨迹更自然
            val gravityY = particle.startY + (particle.endY - particle.startY) * progress + 
                          (progress * progress) * 200f  // 重力系数200f
            
            // 粒子在飞行过程中逐渐缩小
            val scale = 1f - progress * 0.2f  // 最终缩小到80%
            
            // 只绘制未到达终点的粒子
            if (progress < 1f) {
                Box(
                    modifier = Modifier
                        .offset(x = x.dp, y = gravityY.dp)  // 应用位置和重力效果
                        .scale(scale)                       // 应用缩放效果
                        .background(
                            Color.White.copy(alpha = 0.1f), // 半透明白色背景
                            CircleShape                     // 圆形背景
                        )
                        .padding(4.dp)                     // 内边距
                ) {
                    Text(
                        text = particle.emoji,             // 显示表情符号
                        fontSize = 24.sp,                  // 字体大小
                        fontFamily = FontFamily.Monospace, // 等宽字体
                        color = Color.Black                 // 黑色文字
                    )
                }
            }
        }
    }
}

/**
 * 创建表情包粒子
 */
private fun createEmojiParticles(): List<EmojiParticle> {
    val emojis = listOf(
        "😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃",
        "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙",
        "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔",
        "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥",
        "😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮",
        "🤧", "🥵", "🥶", "🥴", "😵", "🤯", "🤠", "🥳", "😎", "🤓",
        "🧐", "😕", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳", "🥺",
        "😦", "😧", "😨", "😰", "😥", "😢", "😭", "😱", "😖", "😣",
        "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬", "😈",
        "👿", "💀", "☠️", "💩", "🤡", "👹", "👺", "👻", "👽", "👾",
        "🤖", "😺", "😸", "😹", "😻", "😼", "😽", "🙀", "😿", "😾"
    )
    
    return (0 until 18).map { index -> // 表情包数量18
        val randomAngle = (kotlin.random.Random.nextFloat() * 360f) * (kotlin.math.PI / 180f) // 随机角度
        val randomDistance = 150f + (kotlin.random.Random.nextFloat() * 200f) // 发射距离
        val endX = cos(randomAngle).toFloat() * randomDistance  // 计算粒子终点的X坐标(水平方向)
        val endY = sin(randomAngle).toFloat() * randomDistance  // 计算粒子终点的Y坐标(垂直方向)
        
        val randomEmoji = emojis.random() // 随机表情包
        
        EmojiParticle(
            id = index,
            startX = 0f,
            startY = 0f,
            endX = endX,
            endY = endY,
            emoji = randomEmoji,
            progress = 0f
        )
    }
}
粒子数据模型
data class EmojiParticle(
    val id: Int,
    val emoji: String,
    val startX: Float,
    val startY: Float,
    val targetX: Float,
    val targetY: Float,
    val currentX: Float = startX,
    val currentY: Float = startY,
    val progress: Float,
    val alpha: Float
)

案例4:旋转表情包效果 (RotatingParticleEffect)

动画效果演示

转转表情包.gif

图:第4阶段 - 表情包旋转动画效果

技术特点
  • 旋转动画:表情包围绕中心点旋转
  • 连接线效果:粒子之间用线条连接
  • 网络状结构:形成动态的粒子网络
  • 同步旋转:所有粒子同步旋转
核心实现
@Composable
fun RotatingParticleEffect(clickCount: Int) {
    // 存储所有批次的粒子数据
    var allParticles by remember { mutableStateOf<List<RotatingEmojiParticle>>(emptyList()) }
    // 记录上次的点击次数,用于检测新的点击事件
    var lastClickCount by remember { mutableIntStateOf(0) }
    // 协程作用域,用于管理动画协程
    val coroutineScope = rememberCoroutineScope()
    
    /**
     * 发射新一批粒子
     * 
     * 为每批粒子分配唯一的ID范围,避免批次间冲突
     * 使用协程实现流畅的动画更新
     */
    fun emitNewBatch() {
        val currentBatchId = clickCount
        // 创建新粒子并分配唯一ID(每批次100个ID)
        val newParticles = createRotatingEmojiParticles().map { particle ->
            particle.copy(id = particle.id + currentBatchId * 100)
        }
        allParticles = allParticles + newParticles
        
        // 启动动画协程
        coroutineScope.launch {
            var batchProgress: Float

            // 200步动画,每步16ms,总共约3.2秒
            repeat(200) { step ->
                batchProgress = step / 200f
                // 只更新当前批次的粒子进度
                allParticles = allParticles.map { particle ->
                    if (particle.id >= currentBatchId * 100 && particle.id < (currentBatchId + 1) * 100) {
                        particle.copy(progress = batchProgress)
                    } else {
                        particle
                    }
                }
                delay(16L) // 60FPS的更新频率
            }
            
            // 确保粒子到达目标位置(progress = 1f)
            batchProgress = 1f
            allParticles = allParticles.map { particle ->
                if (particle.id >= currentBatchId * 100 && particle.id < (currentBatchId + 1) * 100) {
                    particle.copy(progress = batchProgress)
                } else {
                    particle
                }
            }
            
            // 等待粒子到达目标位置后消失
            delay(500L)
            
            // 移除这批粒子,释放内存
            allParticles = allParticles.filter { 
                it.id < currentBatchId * 100 || it.id >= (currentBatchId + 1) * 100 
            }
        }
    }
    
    // 响应点击事件,发射新粒子
    LaunchedEffect(clickCount) {
        if (clickCount > lastClickCount) {
            lastClickCount = clickCount
            emitNewBatch()
        }
    }
    
    // 粒子容器,全屏居中显示
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        // 绘制所有批次的旋转表情包粒子
        allParticles.forEach { particle ->
            val progress = particle.progress
            // 线性插值计算当前X坐标
            val x = particle.startX + (particle.endX - particle.startX) * progress

            // 抛物线效果 - 模拟重力,添加弧度,模拟抛物线轨迹
            val gravityY = particle.startY + (particle.endY - particle.startY) * progress + 
                          (progress * progress) * 220f
            
            // 只渲染未到达终点的粒子
            if (progress < 1f) {
                // 旋转动画:3圈旋转(1080度)
                val rotation = progress * 1080f
                // 缩放动画:从1倍逐渐缩小到0.7倍
                val scale = 1f - progress * 0.3f
                
                // 粒子容器,包含背景和表情符号
                Box(
                    modifier = Modifier
                        .offset(x = x.dp, y = gravityY.dp) // 设置粒子位置
                        .scale(scale) // 应用缩放动画
                        .rotate(rotation) // 应用旋转动画
                        .background(
                            Color.White.copy(alpha = 0.1f), // 半透明白色背景
                            CircleShape // 圆形背景
                        )
                        .padding(4.dp) // 内边距
                ) {
                    // 表情符号文本
                    Text(
                        text = particle.emoji,
                        fontSize = 28.sp,
                        fontFamily = FontFamily.Monospace,
                        color = Color.Black
                    )
                }
            }
        }
    }
}

/**
 * 创建旋转表情包粒子数据
 * 
 * 生成10个粒子,每个粒子都有随机的方向、距离和表情符号
 * 使用三角函数计算粒子的终点坐标,实现圆形分布
 * 
 * @return List<RotatingEmojiParticle> 粒子数据列表
 */
private fun createRotatingEmojiParticles(): List<RotatingEmojiParticle> {
    val emojis = listOf(
        "😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃",
        "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙",
        "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔",
        "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥",
        "😌", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮",
        "🤧", "🥵", "🥶", "🥴", "😵", "🤯", "🤠", "🥳", "😎", "🤓",
        "🧐", "😕", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳", "🥺",
        "😦", "😧", "😨", "😰", "😥", "😢", "😭", "😱", "😖", "😣",
        "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬", "😈",
        "👿", "💀", "☠️", "💩", "🤡", "👹", "👺", "👻", "👽", "👾",
        "🤖", "😺", "😸", "😹", "😻", "😼", "😽", "🙀", "😿", "😾"
    )
    
    // 创建10个粒子,每个都有随机的属性
    return (0 until 10).map { index ->
        // 随机角度:0-360度,转换为弧度
        val randomAngle = (kotlin.random.Random.nextFloat() * 360f) * (kotlin.math.PI / 180f)
        // 随机距离:120-300像素,确保粒子不会太近或太远
        val randomDistance = 120f + (kotlin.random.Random.nextFloat() * 180f)
        
        // 使用三角函数计算终点坐标,实现圆形分布
        val endX = cos(randomAngle).toFloat() * randomDistance  // X坐标:水平方向
        val endY = sin(randomAngle).toFloat() * randomDistance  // Y坐标:垂直方向
        
        // 随机选择表情符号
        val randomEmoji = emojis.random()
        
        // 创建粒子数据对象
        RotatingEmojiParticle(
            id = index,
            startX = 0f, // 起始位置:屏幕中心
            startY = 0f, // 起始位置:屏幕中心
            endX = endX, // 终点X坐标
            endY = endY, // 终点Y坐标
            emoji = randomEmoji, // 表情符号
            progress = 0f // 初始动画进度为0
        )
    }
}
旋转粒子数据模型
data class RotatingEmojiParticle(
    val id: Int,
    val emoji: String,
    val baseAngle: Float,
    val radius: Float
)

案例5:烟花效果 (FireworkEffect)

动画效果演示

烟花闪烁.gif

图:第5阶段 - 烟花闪烁动画效果

性能优化亮点
  • 独立动画状态:每个烟花使用独立的 Animatable
  • 避免列表重组:使用 derivedStateOf 计算属性
  • 内存管理:限制最大烟花数量,自动清理
  • 条件渲染:只渲染可见的烟花
核心实现
@Composable
fun FireworkEffect(clickCount: Int) {
    // 存储当前活跃的烟花ID列表
    var fireworkIds by remember { mutableStateOf<List<Int>>(emptyList()) }
    // 记录上次的点击次数,用于检测新的点击事件
    var lastClickCount by remember { mutableIntStateOf(0) }
    
    // 限制最大烟花数量,防止内存占用过多
    val maxFireworks = 8

    // 响应点击事件,创建新的烟花
    LaunchedEffect(clickCount) {
        if (clickCount > lastClickCount) {
            lastClickCount = clickCount
            
            // 限制烟花数量,移除最老的烟花ID(FIFO策略)
            val updatedIds = if (fireworkIds.size >= maxFireworks) {
                fireworkIds.drop(1) + clickCount
            } else {
                fireworkIds + clickCount
            }
            
            fireworkIds = updatedIds
        }
    }

    // 烟花容器,全屏居中显示
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        // 为每个烟花ID创建独立的Composable,实现并行动画
        fireworkIds.forEach { fireworkId ->
            SingleFirework(
                fireworkId = fireworkId,
                onComplete = {
                    // 动画完成后移除这个烟花ID,释放内存
                    fireworkIds = fireworkIds.filter { it != fireworkId }
                }
            )
        }
        
        // 调试信息:显示当前活跃的烟花数量
        Text(
            text = "烟花数量: ${fireworkIds.size}",
            fontSize = 12.sp,
            color = Color.Gray,
            modifier = Modifier
                .align(Alignment.TopStart)
                .offset(16.dp, 16.dp)
        )
    }
}

/**
 * 单个烟花组件 - 独立管理自己的动画状态
 * 
 * 每个烟花都有独立的动画状态,互不干扰
 * 使用Animatable实现流畅的动画效果
 * 
 * @param fireworkId 烟花的唯一标识符
 * @param onComplete 动画完成后的回调函数
 */
@Composable
private fun SingleFirework(
    fireworkId: Int,
    onComplete: () -> Unit
) {
    // 创建烟花数据(只在首次创建时计算,避免重复计算)
    val firework = remember(fireworkId) { createRandomFirework(fireworkId) }
    
    // 独立的动画状态,控制整个烟花的动画进度(0.0-1.0)
    val progress = remember { Animatable(0f) }
    
    // 计算当前的缩放比例 - 实现从小到大的爆炸效果
    val scale by remember {
        derivedStateOf {
            val p = progress.value
            when {
                // 前30%时间:从0.5倍快速放大到1.5倍
                p < 0.3f -> 0.5f + (p / 0.3f) * 1.0f
                // 中间40%时间:保持1.5倍峰值大小
                p < 0.7f -> 1.5f
                // 后30%时间:从1.5倍慢慢缩小到0.5倍
                else -> 1.5f - ((p - 0.7f) / 0.3f) * 1.0f
            }
        }
    }
    
    // 计算当前的透明度 - 实现淡入淡出效果
    val alpha by remember {
        derivedStateOf {
            val p = progress.value
            when {
                // 前20%时间:从透明快速淡入到完全不透明
                p < 0.2f -> (p / 0.2f) * 1.0f
                // 中间40%时间:保持完全不透明
                p < 0.6f -> 1.0f
                // 后40%时间:从完全不透明慢慢淡出到透明
                else -> 1.0f - ((p - 0.6f) / 0.4f) * 1.0f
            }
        }
    }

    // 启动动画:从0到1的进度动画
    LaunchedEffect(fireworkId) {
        try {
            // 执行2秒的动画,从0到1
            progress.animateTo(
                targetValue = 1f,
                animationSpec = tween(durationMillis = 2000)
            )
        } finally {
            // 动画完成后调用回调,通知父组件移除这个烟花
            onComplete()
        }
    }

    // 只有当透明度大于0.01时才渲染,避免渲染完全透明的元素
    if (alpha > 0.01f) {
        Text(
            text = firework.emoji,
            fontSize = 20.sp,
            fontFamily = FontFamily.Monospace,
            modifier = Modifier
                .offset(x = firework.x.dp, y = firework.y.dp) // 设置烟花位置
                .scale(scale) // 应用缩放动画
                .alpha(alpha) // 应用透明度动画
        )
    }
}

/**
 * 创建随机位置的烟花数据
 * 
 * 为每个烟花生成随机的表情符号、颜色和位置
 * 确保烟花不会出现在屏幕边缘,提供良好的视觉效果
 * 
 * @param id 烟花的唯一标识符
 * @return FireworkAnimation 包含烟花所有属性的数据对象
 */
private fun createRandomFirework(id: Int): FireworkAnimation {
    // 烟花表情符号库 
    val fireworkEmojis = listOf(
        "🎆", "🎇", "✨", "💥", "🌟", "⭐", "💫", "🔥", "💎", "🎊",
        "🎉", "🎈", "🎁", "🎀", "💝", "💖", "💕", "💗", "💓", "💞"
    )

    // 烟花颜色库 - 鲜艳的庆祝色彩
    val fireworkColors = listOf(
        Color(0xFFFF6B35), // 橙色
        Color(0xFFFFD23F), // 黄色
        Color(0xFF06FFA5), // 绿色
        Color(0xFF4FC3F7), // 蓝色
        Color(0xFF8B5CF6), // 紫色
        Color(0xFFEF4444), // 红色
        Color(0xFFFF69B4), // 粉色
        Color(0xFF00CED1)  // 青色
    )

    // 随机位置计算:避免屏幕边缘,留出足够空间
    val screenWidth = 400f // 假设屏幕宽度
    val screenHeight = 600f // 假设屏幕高度
    val margin = 50f // 边距,确保烟花不会出现在屏幕边缘

    // 生成随机坐标,确保在安全区域内
    val randomX = (kotlin.random.Random.nextFloat() * (screenWidth - 2 * margin) + margin)
    val randomY = (kotlin.random.Random.nextFloat() * (screenHeight - 2 * margin) + margin)

    // 创建并返回烟花动画数据对象
    return FireworkAnimation(
        id = id,
        x = randomX - screenWidth / 2, // 转换为相对于屏幕中心的坐标
        y = randomY - screenHeight / 2, // 转换为相对于屏幕中心的坐标
        emoji = fireworkEmojis.random(), // 随机选择表情符号
        color = fireworkColors.random(), // 随机选择颜色
        progress = 0f, // 初始动画进度为0
        scale = 0.5f, // 初始缩放为0.5倍
        alpha = 0f // 初始透明度为0(完全透明)
    )
}
烟花数据模型
data class FireworkAnimation(
    val id: Int,
    val emoji: String,
    val x: Float,
    val y: Float,
    val color: Color
)

核心动画技术详解

1. 动画规格 (AnimationSpec)

Spring 弹簧动画
// 不同的弹簧效果
val bouncySpring = spring(
    dampingRatio = Spring.DampingRatioHighBouncy,
    stiffness = Spring.StiffnessLow
)

val smoothSpring = spring(
    dampingRatio = Spring.DampingRatioNoBouncy,
    stiffness = Spring.StiffnessMedium
)

val quickSpring = spring(
    dampingRatio = Spring.DampingRatioMediumBouncy,
    stiffness = Spring.StiffnessHigh
)
Tween 补间动画
// 线性动画
val linearTween = tween<Float>(
    durationMillis = 1000,
    easing = LinearEasing
)

// 缓动动画
val easeTween = tween<Float>(
    durationMillis = 1000,
    easing = FastOutSlowInEasing
)

// 自定义贝塞尔曲线
val customTween = tween<Float>(
    durationMillis = 1000,
    easing = CubicBezierEasing(0.25f, 0.46f, 0.45f, 0.94f)
)
Keyframes 关键帧动画
val keyframesSpec = keyframes<Float> {
    durationMillis = 2000
    0f at 0 using LinearEasing
    0.5f at 500 using FastOutSlowInEasing
    1.2f at 1000 using LinearOutSlowInEasing
    1f at 2000
}

2. 状态管理模式

单一状态源
@Composable
fun AnimationController() {
    // 单一状态控制多个动画
    var animationPhase by remember { mutableIntStateOf(0) }
    
    val scale by animateFloatAsState(
        targetValue = when (animationPhase) {
            0 -> 1f
            1 -> 1.2f
            2 -> 0.8f
            else -> 1f
        }
    )
    
    val rotation by animateFloatAsState(
        targetValue = animationPhase * 90f
    )
    
    val color by animateColorAsState(
        targetValue = when (animationPhase) {
            0 -> Color.Blue
            1 -> Color.Red
            2 -> Color.Green
            else -> Color.Gray
        }
    )
}
复合状态管理
@Stable
class AnimationState {
    var isExpanded by mutableStateOf(false)
    var isHighlighted by mutableStateOf(false)
    var currentPhase by mutableIntStateOf(0)
    
    fun nextPhase() {
        currentPhase = (currentPhase + 1) % 4
    }
    
    fun reset() {
        isExpanded = false
        isHighlighted = false
        currentPhase = 0
    }
}

@Composable
fun ComplexAnimation() {
    val animationState = remember { AnimationState() }
    
    // 多个动画基于复合状态
    val scale by animateFloatAsState(
        targetValue = if (animationState.isExpanded) 1.5f else 1f
    )
    
    val alpha by animateFloatAsState(
        targetValue = if (animationState.isHighlighted) 1f else 0.7f
    )
}

3. 动画组合与编排

并行动画
@Composable
fun ParallelAnimations() {
    var trigger by remember { mutableStateOf(false) }
    
    // 多个动画同时进行
    val scale by animateFloatAsState(
        targetValue = if (trigger) 1.5f else 1f,
        animationSpec = spring(stiffness = Spring.StiffnessLow)
    )
    
    val rotation by animateFloatAsState(
        targetValue = if (trigger) 360f else 0f,
        animationSpec = tween(1000)
    )
    
    val color by animateColorAsState(
        targetValue = if (trigger) Color.Red else Color.Blue,
        animationSpec = tween(500)
    )
    
    Box(
        modifier = Modifier
            .size(100.dp)
            .scale(scale)
            .rotate(rotation)
            .background(color, CircleShape)
            .clickable { trigger = !trigger }
    )
}
序列动画
@Composable
fun SequentialAnimations() {
    var phase by remember { mutableIntStateOf(0) }
    val coroutineScope = rememberCoroutineScope()
    
    val scale by animateFloatAsState(
        targetValue = when (phase) {
            0 -> 1f
            1 -> 1.5f
            2 -> 0.8f
            else -> 1f
        },
        finishedListener = { 
            // 动画完成后自动进入下一阶段
            if (phase < 3) {
                coroutineScope.launch {
                    delay(200)
                    phase++
                }
            }
        }
    )
    
    LaunchedEffect(Unit) {
        // 启动动画序列
        phase = 1
    }
}
交错动画
@Composable
fun StaggeredAnimations() {
    var startAnimation by remember { mutableStateOf(false) }
    val items = remember { (1..5).toList() }
    
    Column {
        items.forEachIndexed { index, item ->
            val delay = index * 100 // 交错延迟
            
            val scale by animateFloatAsState(
                targetValue = if (startAnimation) 1f else 0f,
                animationSpec = tween(
                    durationMillis = 300,
                    delayMillis = delay
                )
            )
            
            Box(
                modifier = Modifier
                    .size(50.dp)
                    .scale(scale)
                    .background(Color.Blue, CircleShape)
            )
        }
        
        Button(onClick = { startAnimation = !startAnimation }) {
            Text("开始交错动画")
        }
    }
}

性能优化与最佳实践

1. 避免不必要的重组

问题代码
@Composable
fun BadPerformanceExample() {
    var count by remember { mutableIntStateOf(0) }
    
    // ❌ 每次count变化都会重新计算expensive操作
    val expensiveValue = expensiveCalculation(count)
    
    val animatedValue by animateFloatAsState(count.toFloat())
    
    Column {
        Text("Count: $count")
        Text("Expensive: $expensiveValue")
        Text("Animated: $animatedValue")
    }
}
优化代码
@Composable
fun GoodPerformanceExample() {
    var count by remember { mutableIntStateOf(0) }
    
    // ✅ 使用remember缓存expensive操作
    val expensiveValue by remember(count) {
        derivedStateOf { expensiveCalculation(count) }
    }
    
    // ✅ 使用derivedStateOf避免不必要的动画更新
    val animatedValue by animateFloatAsState(count.toFloat())
    
    Column {
        Text("Count: $count")
        Text("Expensive: ${expensiveValue.value}")
        Text("Animated: $animatedValue")
    }
}

2. 合理使用LaunchedEffect

问题代码
@Composable
fun BadLaunchedEffectExample(clickCount: Int) {
    var animations by remember { mutableStateOf<List<Animation>>(emptyList()) }
    
    // ❌ 每次重组都会启动新的协程
    LaunchedEffect(true) {
        while (true) {
            // 无限循环动画
            delay(16)
            animations = updateAnimations(animations)
        }
    }
}
优化代码
@Composable
fun GoodLaunchedEffectExample(clickCount: Int) {
    var animations by remember { mutableStateOf<List<Animation>>(emptyList()) }
    
    // ✅ 使用正确的key,避免重复启动
    LaunchedEffect(clickCount) {
        if (clickCount > 0) {
            // 只在clickCount变化时启动新动画
            val newAnimation = createAnimation(clickCount)
            animations = animations + newAnimation
            
            // 动画完成后清理
            delay(2000)
            animations = animations.filter { it.id != clickCount }
        }
    }
}

3. 内存管理

限制动画数量
@Composable
fun MemoryEfficientAnimations() {
    var activeAnimations by remember { mutableStateOf<List<AnimationData>>(emptyList()) }
    val maxAnimations = 10 // 限制最大数量
    
    fun addAnimation(newAnimation: AnimationData) {
        activeAnimations = (activeAnimations + newAnimation)
            .takeLast(maxAnimations) // 只保留最新的N个动画
    }
    
    fun removeAnimation(animationId: Int) {
        activeAnimations = activeAnimations.filter { it.id != animationId }
    }
}
及时清理资源
@Composable
fun ResourceManagement() {
    val animatables = remember { mutableMapOf<Int, Animatable<Float, AnimationVector1D>>() }
    
    DisposableEffect(Unit) {
        onDispose {
            // 组件销毁时清理所有动画
            animatables.values.forEach { animatable ->
                animatable.stop()
            }
            animatables.clear()
        }
    }
}

4. 动画性能监控

性能监控工具
@Composable
fun AnimationPerformanceMonitor() {
    var frameCount by remember { mutableIntStateOf(0) }
    var lastTime by remember { mutableLongStateOf(System.currentTimeMillis()) }
    var fps by remember { mutableFloatStateOf(0f) }
    
    LaunchedEffect(Unit) {
        while (true) {
            delay(16) // 60fps
            frameCount++
            
            val currentTime = System.currentTimeMillis()
            if (currentTime - lastTime >= 1000) {
                fps = frameCount * 1000f / (currentTime - lastTime)
                frameCount = 0
                lastTime = currentTime
            }
        }
    }
    
    Text(
        text = "FPS: ${fps.toInt()}",
        modifier = Modifier
            .background(Color.Black.copy(alpha = 0.7f))
            .padding(8.dp),
        color = Color.White
    )
}

5. 最佳实践总结

推荐做法
// 1. 使用状态驱动动画
val scale by animateFloatAsState(targetValue = if (isExpanded) 1.5f else 1f)

// 2. 合理使用remember缓存
val expensiveData = remember(key) { computeExpensiveData(key) }

// 3. 使用derivedStateOf避免不必要计算
val computedValue by remember { derivedStateOf { expensiveComputation() } }

// 4. 限制动画数量
val animations = animations.takeLast(MAX_ANIMATIONS)

// 5. 及时清理资源
DisposableEffect(key) {
    onDispose { cleanup() }
}
避免的做法
// 1. 避免在Composable中直接创建动画对象
val animator = ObjectAnimator.ofFloat(...) // ❌

// 2. 避免无限制的动画创建
repeat(1000) { createAnimation() } // ❌

// 3. 避免在动画中进行expensive操作
LaunchedEffect(Unit) {
    while (true) {
        expensiveOperation() // ❌
        delay(16)
    }
}

// 4. 避免不必要的状态更新
var state by mutableStateOf(initialValue)
state = state.copy() // 如果值没变化,避免这样做 ❌

KMM跨平台动画实现

KMM跨平台效果展示

烟花闪烁.gif

图:同样的动画效果在iOS设备上的运行表现 - 一套KMM代码,双平台运行

Kotlin Multiplatform项目结构

项目结构
├── shared/
│   └── src/
│       ├── commonMain/kotlin/     # 共享动画代码
│       │   ├── ui/
│       │   │   ├── effects/       # 动画效果
│       │   │   ├── components/    # UI组件
│       │   │   └── models/        # 数据模型
│       │   └── SharedAnimationDemo.kt
│       ├── androidMain/kotlin/    # Android特定代码
│       └── iosMain/kotlin/        # iOS特定代码
├── app/ (Android)
└── iosApp/ (iOS)

共享动画代码

共享的动画组件
// shared/src/commonMain/kotlin/ui/SharedAnimationDemo.kt
@Composable
fun SharedAnimationDemo() {
    // 这个组件可以在Android和iOS上运行
    LikeAnimationDemo()
}

// 共享的动画效果
@Composable
fun CrossPlatformFireworkEffect(clickCount: Int) {
    // 使用Compose Multiplatform的共享API
    var activeFireworks by remember { mutableStateOf<List<Int>>(emptyList()) }
    
    LaunchedEffect(clickCount) {
        if (clickCount > 0) {
            activeFireworks = activeFireworks + clickCount
        }
    }
    
    Box(modifier = Modifier.fillMaxSize()) {
        activeFireworks.forEach { fireworkId ->
            CrossPlatformSingleFirework(
                fireworkId = fireworkId,
                onComplete = {
                    activeFireworks = activeFireworks.filter { it != fireworkId }
                }
            )
        }
    }
}
平台特定的实现
// shared/src/commonMain/kotlin/Platform.kt
expect fun getPlatformName(): String
expect fun getScreenDensity(): Float

// shared/src/androidMain/kotlin/Platform.android.kt
actual fun getPlatformName(): String = "Android"
actual fun getScreenDensity(): Float {
    return Resources.getSystem().displayMetrics.density
}

// shared/src/iosMain/kotlin/Platform.ios.kt
actual fun getPlatformName(): String = "iOS"
actual fun getScreenDensity(): Float {
    return UIScreen.mainScreen.scale.toFloat()
}

Android集成

1. 项目依赖配置

首先在Android模块的build.gradle.kts中添加对shared模块的依赖:

// app/build.gradle.kts
dependencies {
    implementation(project(":shared"))  // 引用shared模块
    
    // 其他依赖...
    implementation("androidx.compose.ui:ui:$compose_version")
    implementation("androidx.compose.material3:material3:$material3_version")
}
2. Android应用入口
// app/src/main/java/com/frank/anim/MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // 直接使用shared模块的组件,无需复制代码
            SharedAnimationDemo()
        }
    }
}

iOS集成

1. iOS项目配置

在iOS项目中,shared模块会自动编译为framework,无需手动配置依赖:

// iosApp/iosApp.xcodeproj/project.pbxproj
// Xcode会自动处理shared模块的framework引用
2. iOS SwiftUI集成
// iosApp/iosApp/ContentView.swift
import SwiftUI
import Shared  // 自动引用shared模块

struct ContentView: View {
    var body: some View {
        ComposeView()
            .ignoresSafeArea(.keyboard)
    }
}

struct ComposeView: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIViewController {
        // 直接调用shared模块的入口函数
        MainViewControllerKt.MainViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
3. iOS Kotlin入口
// shared/src/iosMain/kotlin/com/frank/anim/shared/MainViewController.kt
import androidx.compose.ui.window.ComposeUIViewController

fun MainViewController(): UIViewController = ComposeUIViewController {
    // 直接使用shared模块的组件,无需复制代码
    SharedAnimationDemo()
}

避免手动复制代码的最佳实践

1. 统一的代码管理

❌ 错误做法:手动复制代码

// 在Android模块中写代码
// app/src/main/java/com/frank/anim/ui/HomePage.kt
@Composable
fun HomePage() {
    // 动画代码...
}

// 然后手动复制到shared模块
// shared/src/commonMain/kotlin/com/frank/anim/ui/HomePage.kt
@Composable
fun HomePage() {
    // 相同的动画代码...
}

✅ 正确做法:直接在shared模块开发

// 直接在shared模块中开发
// shared/src/commonMain/kotlin/com/frank/anim/ui/HomePage.kt
@Composable
fun HomePage() {
    // 动画代码只写一次
}

// Android和iOS都直接引用
// app/src/main/java/com/frank/anim/MainActivity.kt
setContent {
    SharedAnimationDemo()  // 直接使用shared模块
}
2. 项目架构优化

推荐的开发流程:

  1. 在shared模块中开发所有UI组件
  2. Android模块只负责平台特定的配置
  3. iOS模块只负责SwiftUI集成
  4. 避免在平台模块中重复编写UI代码
// 项目结构优化
AnimationDemo/
├── shared/                    # 主要开发区域
│   └── src/
│       ├── commonMain/        # 所有UI组件都在这里
│       │   └── kotlin/
│       │       └── com/frank/anim/
│       │           ├── ui/    # 所有页面组件
│       │           ├── effects/ # 所有动画效果
│       │           └── shared/ # 共享入口
│       ├── androidMain/       # 仅Android特定代码
│       └── iosMain/           # 仅iOS特定代码
├── app/                       # 仅Android配置
│   └── src/main/java/
│       └── MainActivity.kt    # 简单的Activity
└── iosApp/                    # 仅iOS配置
    └── iosApp/
        └── ContentView.swift  # 简单的SwiftUI包装
3. 依赖管理

Android模块依赖配置:

// app/build.gradle.kts
dependencies {
    implementation(project(":shared"))  // 关键:引用shared模块
    
    // 其他Compose依赖
    implementation("androidx.compose.ui:ui:$compose_version")
    implementation("androidx.compose.material3:material3:$material3_version")
}

iOS模块自动处理:

// iOS会自动处理shared模块的framework
// 无需手动配置依赖
import Shared  // 自动可用
4. 开发工作流

推荐的开发步骤:

  1. 创建新功能时,直接在shared模块中开发
  2. 使用Android Studio的KMM插件进行开发
  3. 在Android设备上测试功能
  4. 在iOS设备上验证跨平台兼容性
  5. 避免在平台模块中编写UI代码

跨平台动画差异处理

平台适配
@Composable
fun PlatformAdaptiveAnimation() {
    val platformName = remember { getPlatformName() }
    val density = remember { getScreenDensity() }
    
    // 根据平台调整动画参数
    val animationDuration = when (platformName) {
        "iOS" -> 300 // iOS用户习惯更快的动画
        "Android" -> 400 // Android标准动画时长
        else -> 350
    }
    
    val scale by animateFloatAsState(
        targetValue = if (isExpanded) 1.5f else 1f,
        animationSpec = tween(durationMillis = animationDuration)
    )
    
    // 根据屏幕密度调整大小
    val adjustedSize = (100 * density).dp
    
    Box(
        modifier = Modifier
            .size(adjustedSize)
            .scale(scale)
    )
}
性能优化差异
@Composable
fun PlatformOptimizedAnimation() {
    val platformName = getPlatformName()

    // iOS设备通常性能更好,可以支持更多并发动画
    val maxAnimations = when (platformName) {
        "iOS" -> 15
        "Android" -> 10
        else -> 8
    }

    var activeAnimations by remember { mutableStateOf<List<Animation>>(emptyList()) }

    fun addAnimation(animation: Animation) {
        activeAnimations = (activeAnimations + animation).takeLast(maxAnimations)
    }
}

总结与展望

技术总结

Compose动画 + KMM的优势
  1. 声明式编程:代码更简洁、可读性更强
  2. 状态驱动:动画自动响应状态变化,减少手动管理
  3. 类型安全:编译时检查,减少运行时错误
  4. 性能优化:智能重组机制,只更新必要部分
  5. 跨平台支持:一套KMM代码,Android和iOS双平台运行
  6. 开发效率:减少重复开发,统一维护成本
  7. 避免代码重复:直接在shared模块开发,无需手动复制代码
  8. 统一架构:Android和iOS都引用shared模块,保持代码一致性
  9. 一致性体验:确保不同平台的动画效果完全一致
项目实战收获
  1. 渐进式学习:从简单波纹到复杂烟花效果
  2. 性能优化:独立动画状态、内存管理、条件渲染
  3. 代码复用:组件化设计,便于维护和扩展
  4. KMM跨平台实现:Android和iOS共享动画逻辑,一套代码双平台运行
  5. 开发效率提升:减少50%的重复开发工作
  6. 维护成本降低:统一的代码库,统一的bug修复

最佳实践总结

开发建议
// 1. 优先使用高级API
AnimatedVisibility(visible = isVisible) { /* content */ }

// 2. 状态驱动动画
val scale by animateFloatAsState(targetValue = if (isExpanded) 1.5f else 1f)

// 3. 合理使用remember
val expensiveData = remember(key) { computeExpensiveData(key) }

// 4. 性能监控
val fps = remember { PerformanceMonitor() }

// 5. 及时清理资源
DisposableEffect(key) {
    onDispose { cleanup() }
}
性能优化清单
  • 使用 derivedStateOf 避免不必要计算
  • 限制并发动画数量
  • 条件渲染,只显示可见元素
  • 使用 remember 缓存expensive操作
  • 及时清理动画资源
  • 监控FPS和内存使用
KMM架构最佳实践
  • 统一开发:所有UI组件在shared模块中开发
  • 避免重复:不在Android和iOS模块中重复编写UI代码
  • 依赖管理:Android模块引用shared,iOS自动处理framework
  • 平台适配:使用expect/actual处理平台差异
  • 代码维护:单一代码库,统一维护和更新

未来发展方向

技术演进
  1. 更丰富的动画API:Google持续完善Compose动画生态
  2. 更好的性能:编译器优化、运行时优化
  3. 更强的跨平台能力:Compose Multiplatform的完善
  4. AI辅助动画:智能动画生成和优化
应用场景扩展
  1. 游戏开发:Compose for Games
  2. 桌面应用:Compose for Desktop
  3. Web应用:Compose for Web
  4. 嵌入式设备:Compose for Embedded
团队技能提升
  1. 学习路径:从基础API到高级技巧
  2. 实践项目:通过实际项目积累经验
  3. 社区参与:贡献开源项目,分享经验
  4. 持续学习:关注最新技术动态

推荐学习资源

官方文档
开源项目
社区资源

结语

Compose动画为我们带来了全新的动画开发体验,通过声明式的API设计和强大的性能优化机制,让复杂的动画实现变得简单而高效。

通过本文的实战案例分析,我们看到了从基础波纹效果到复杂烟花动画的完整实现过程,以及如何通过性能优化技巧让动画在各种设备上都能流畅运行。

跨平台动画的实现更是展示了Compose Multiplatform的强大能力,让我们能够用一套代码为Android和iOS用户提供一致的动画体验。

希望这些经验和技巧能够帮助你在项目中更好地运用Compose动画技术,创造出更加优秀的用户体验!


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题或建议,也欢迎在评论区交流讨论。

标签: #Compose #KMM #Android #iOS #动画 #跨平台 #性能优化 #KotlinMultiplatform