Compose 动画 + KMM 跨平台开发:从传统View到现代声明式UI动画
本文将深入探讨Compose动画技术和KMM跨平台开发,通过实战项目案例,带你掌握从基础到高级的动画开发技巧,以及如何用一套代码实现Android和iOS的跨平台动画效果,并分享性能优化和最佳实践。(由于需要在公司内部做技术分享,所以暂时不把demo代码地址放出,后续分享之后会贴上github地址)
🎬 动画效果预览
图:层次递进式动画演示 - 从基础波纹到烟花闪烁的完整动画效果
目录
技术背景与概述
为什么选择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)
动画效果演示
技术要点
- 使用
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阶段 - 彩虹渐变色彩动画效果*
技术要点
- 使用
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)
动画效果演示
图:第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)
动画效果演示
图:第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)
动画效果演示
图:第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跨平台效果展示
图:同样的动画效果在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. 项目架构优化
推荐的开发流程:
- 在shared模块中开发所有UI组件
- Android模块只负责平台特定的配置
- iOS模块只负责SwiftUI集成
- 避免在平台模块中重复编写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. 开发工作流
推荐的开发步骤:
- 创建新功能时,直接在shared模块中开发
- 使用Android Studio的KMM插件进行开发
- 在Android设备上测试功能
- 在iOS设备上验证跨平台兼容性
- 避免在平台模块中编写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的优势
- 声明式编程:代码更简洁、可读性更强
- 状态驱动:动画自动响应状态变化,减少手动管理
- 类型安全:编译时检查,减少运行时错误
- 性能优化:智能重组机制,只更新必要部分
- 跨平台支持:一套KMM代码,Android和iOS双平台运行
- 开发效率:减少重复开发,统一维护成本
- 避免代码重复:直接在shared模块开发,无需手动复制代码
- 统一架构:Android和iOS都引用shared模块,保持代码一致性
- 一致性体验:确保不同平台的动画效果完全一致
项目实战收获
- 渐进式学习:从简单波纹到复杂烟花效果
- 性能优化:独立动画状态、内存管理、条件渲染
- 代码复用:组件化设计,便于维护和扩展
- KMM跨平台实现:Android和iOS共享动画逻辑,一套代码双平台运行
- 开发效率提升:减少50%的重复开发工作
- 维护成本降低:统一的代码库,统一的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处理平台差异
- 代码维护:单一代码库,统一维护和更新
未来发展方向
技术演进
- 更丰富的动画API:Google持续完善Compose动画生态
- 更好的性能:编译器优化、运行时优化
- 更强的跨平台能力:Compose Multiplatform的完善
- AI辅助动画:智能动画生成和优化
应用场景扩展
- 游戏开发:Compose for Games
- 桌面应用:Compose for Desktop
- Web应用:Compose for Web
- 嵌入式设备:Compose for Embedded
团队技能提升
- 学习路径:从基础API到高级技巧
- 实践项目:通过实际项目积累经验
- 社区参与:贡献开源项目,分享经验
- 持续学习:关注最新技术动态
推荐学习资源
官方文档
开源项目
社区资源
结语
Compose动画为我们带来了全新的动画开发体验,通过声明式的API设计和强大的性能优化机制,让复杂的动画实现变得简单而高效。
通过本文的实战案例分析,我们看到了从基础波纹效果到复杂烟花动画的完整实现过程,以及如何通过性能优化技巧让动画在各种设备上都能流畅运行。
跨平台动画的实现更是展示了Compose Multiplatform的强大能力,让我们能够用一套代码为Android和iOS用户提供一致的动画体验。
希望这些经验和技巧能够帮助你在项目中更好地运用Compose动画技术,创造出更加优秀的用户体验!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题或建议,也欢迎在评论区交流讨论。
标签: #Compose #KMM #Android #iOS #动画 #跨平台 #性能优化 #KotlinMultiplatform