17、Compose动画之无限循环动画:InfiniteTransition、rememberInfiniteTransition、循环动画场景

各位同学,今天我们来聊聊Compose动画里一个非常实用的东西——无限循环动画。

说实话,我在做传统View动画那会儿,最头疼的就是做那种一直转、一直闪、一直跳的动画效果。你得搞个AnimationDrawable,或者自己写个Handler不断postDelayed,稍不留神就内存泄漏了。嗯,那滋味可不好受。

到了Compose时代,Google终于给了我们一个体面的解决方案——InfiniteTransition。说白了,它就是专门为「无限循环」这种场景量身打造的动画工具。

17.1 什么是InfiniteTransition?

先别急着看代码,咱们先理解一下它的定位。

普通的Animatable或者animate*AsState,它们都是一次性的——动画跑完就停了。但有些场景,比如加载中的转圈、脉冲呼吸灯、不断闪烁的提示,它们需要「永远动下去」。

InfiniteTransition就是干这个的。它内部维护了一个无限循环的动画循环,你只需要告诉它「我要从A变到B,再变回A,反复执行」,剩下的它帮你搞定。

核心概念:InfiniteTransition 是一个可以持续运行动画的容器。它不会自动停止,除非你手动取消或者Composable被移出组合树。

17.2 怎么用?记住这个三板斧

我个人习惯把使用步骤拆成三步,你照着写基本不会出错:

  1. 创建InfiniteTransition对象 —— 用rememberInfiniteTransition()
  2. 定义动画属性 —— 用animateFloatanimateColor
  3. 把属性值应用到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)  // 旋转角度不断变化
    )
}

你看,就这么简单。一个无限旋转的加载图标就出来了。

小提示:我建议你养成习惯,把infiniteRepeatablerepeatMode设置成RepeatMode.Restart或者RepeatMode.Reverse。前者是从头开始,后者是来回弹。具体用哪个,看你的效果需求。

17.3 三种核心动画属性

InfiniteTransition提供了三种常用的动画属性方法,我列个表给你看:

方法 返回值类型 适用场景
animateFloat Float 旋转角度、透明度、缩放、位移
animateColor Color 颜色渐变、呼吸灯、状态指示
animateValue 泛型T 自定义类型,比如Size、Offset等

我在项目中遇到过最常用的就是animateFloatanimateColor。比如做一个脉冲呼吸灯效果:

@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理清知识结构

说了这么多,我画个图帮你梳理一下:

InfiniteTransition 知识结构 rememberInfiniteTransition() animateFloat() animateColor() animateValue() 旋转 / 透明度 / 缩放 animateFloat 典型应用 呼吸灯 / 状态指示 animateColor 典型应用 自定义类型动画 animateValue 典型应用 ⚠️ 注意事项 避免在列表项中大量创建 · 单个InfiniteTransition内不超过5个属性

这张图把核心脉络理清楚了。你从rememberInfiniteTransition出发,选择三种属性方法之一,然后应用到具体的UI场景中。最后别忘了注意性能问题。

17.7 小结

好了,这一章的内容就这些。总结一下:

  • InfiniteTransition是Compose专门为无限循环动画设计的工具
  • rememberInfiniteTransition()创建,用animateFloat/animateColor/animateValue定义动画属性
  • 常见场景包括加载动画、状态指示、引导提示等
  • 注意性能,别在列表里滥用

说实话,Compose这套动画API用起来确实比传统View舒服多了。你不需要关心动画循环的启动和停止,只需要关注「我要动什么」和「怎么动」。剩下的,框架帮你搞定。

下一章我们会聊更进阶的内容——动画与手势的结合。到时候你会发现,动画和手势配合起来,能做出很多让人眼前一亮的效果。


公众号:蓝海资料掘金营,微信deep3321