17、Compose动画之无限循环动画:InfiniteTransition、rememberInfiniteTransition、循环动画场景
各位同学,今天我们来聊聊Compose动画里一个非常实用的东西——无限循环动画。
说实话,我在做传统View动画那会儿,最头疼的就是做那种一直转、一直闪、一直跳的动画效果。你得搞个AnimationDrawable,或者自己写个Handler不断postDelayed,稍不留神就内存泄漏了。嗯,那滋味可不好受。
到了Compose时代,Google终于给了我们一个体面的解决方案——InfiniteTransition。说白了,它就是专门为「无限循环」这种场景量身打造的动画工具。
17.1 什么是InfiniteTransition?
先别急着看代码,咱们先理解一下它的定位。
普通的Animatable或者animate*AsState,它们都是一次性的——动画跑完就停了。但有些场景,比如加载中的转圈、脉冲呼吸灯、不断闪烁的提示,它们需要「永远动下去」。
InfiniteTransition就是干这个的。它内部维护了一个无限循环的动画循环,你只需要告诉它「我要从A变到B,再变回A,反复执行」,剩下的它帮你搞定。
17.2 怎么用?记住这个三板斧
我个人习惯把使用步骤拆成三步,你照着写基本不会出错:
- 创建InfiniteTransition对象 —— 用
rememberInfiniteTransition() - 定义动画属性 —— 用
animateFloat、animateColor等 - 把属性值应用到UI上 —— 比如旋转角度、透明度、缩放等
来,直接看代码:
@Composable
fun LoadingSpinner() {
// 第一步:创建无限循环动画的控制器
val infiniteTransition = rememberInfiniteTransition()
// 第二步:定义一个从0f到360f不断循环的浮点动画
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1000, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
// 第三步:应用到UI
Image(
painter = painterResource(id = R.drawable.ic_loading),
contentDescription = "加载中",
modifier = Modifier
.size(48.dp)
.rotate(rotation) // 旋转角度不断变化
)
}
你看,就这么简单。一个无限旋转的加载图标就出来了。
infiniteRepeatable的repeatMode设置成RepeatMode.Restart或者RepeatMode.Reverse。前者是从头开始,后者是来回弹。具体用哪个,看你的效果需求。
17.3 三种核心动画属性
InfiniteTransition提供了三种常用的动画属性方法,我列个表给你看:
| 方法 | 返回值类型 | 适用场景 |
|---|---|---|
animateFloat |
Float | 旋转角度、透明度、缩放、位移 |
animateColor |
Color | 颜色渐变、呼吸灯、状态指示 |
animateValue |
泛型T | 自定义类型,比如Size、Offset等 |
我在项目中遇到过最常用的就是animateFloat和animateColor。比如做一个脉冲呼吸灯效果:
@Composable
fun PulseIndicator() {
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 0.3f,
targetValue = 1.0f,
animationSpec = infiniteRepeatable(
animation = tween(800),
repeatMode = RepeatMode.Reverse
)
)
val pulseColor by infiniteTransition.animateColor(
initialValue = Color(0xFF4CAF50),
targetValue = Color(0xFF81C784),
animationSpec = infiniteRepeatable(
animation = tween(800),
repeatMode = RepeatMode.Reverse
)
)
Box(
modifier = Modifier
.size(24.dp)
.background(pulseColor.copy(alpha = alpha), shape = CircleShape)
)
}
这个效果就是一个小圆点,颜色在深绿和浅绿之间来回切换,同时透明度也在变化。看起来就像是在「呼吸」。嗯,很多App的在线状态指示器就是这么做的。
17.4 循环动画的常见场景
我整理了几个实际开发中经常遇到的场景,你看看有没有熟悉的:
- 加载中动画:转圈、进度条跑马灯、三点跳动
- 状态指示:在线/离线呼吸灯、录音中闪烁红点
- 引导提示:手指点击波纹扩散、箭头上下浮动
- 装饰效果:背景渐变色流动、星星闪烁、粒子飘动
举个例子,做一个「正在录音」的闪烁红点:
@Composable
fun RecordingIndicator() {
val infiniteTransition = rememberInfiniteTransition()
val visible by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0f,
animationSpec = infiniteRepeatable(
animation = tween(500),
repeatMode = RepeatMode.Reverse
)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(12.dp)
.background(
Color.Red.copy(alpha = visible),
shape = CircleShape
)
)
Spacer(modifier = Modifier.width(8.dp))
Text("录音中...")
}
}
这个效果很常见吧?红点一明一暗地闪烁,告诉用户「我在录着呢」。
rememberInfiniteTransition。每个Item都创建一个无限循环动画,结果列表滑动时卡成PPT。后来我改成只在可见项创建,或者用key控制复用,问题才解决。所以,别在列表里滥用无限循环动画。
17.5 性能与生命周期
说到性能,我得提一嘴。InfiniteTransition本身是跟Composable的生命周期绑定的。当Composable退出组合时,动画会自动取消。这一点比传统View的Handler方案强太多了——你再也不用担心忘记removeCallbacks了。
但是,它也不是完全没有开销。毕竟每一帧都在计算插值,如果你的动画属性很多(比如同时控制10个浮点),还是会有性能压力的。我个人的经验是:一个InfiniteTransition里别超过5个动画属性,超过的话考虑拆分成多个独立的动画。
17.6 用SVG理清知识结构
说了这么多,我画个图帮你梳理一下:
这张图把核心脉络理清楚了。你从rememberInfiniteTransition出发,选择三种属性方法之一,然后应用到具体的UI场景中。最后别忘了注意性能问题。
17.7 小结
好了,这一章的内容就这些。总结一下:
InfiniteTransition是Compose专门为无限循环动画设计的工具- 用
rememberInfiniteTransition()创建,用animateFloat/animateColor/animateValue定义动画属性 - 常见场景包括加载动画、状态指示、引导提示等
- 注意性能,别在列表里滥用
说实话,Compose这套动画API用起来确实比传统View舒服多了。你不需要关心动画循环的启动和停止,只需要关注「我要动什么」和「怎么动」。剩下的,框架帮你搞定。
下一章我们会聊更进阶的内容——动画与手势的结合。到时候你会发现,动画和手势配合起来,能做出很多让人眼前一亮的效果。