21、Compose动画入门:Compose动画体系概述、animate*AsState系列函数、Animatable与AnimatableAsState

说实话,刚接触Compose动画时,我有点懵。毕竟做了好几年View动画,突然换了一套全新的思路,心里难免犯嘀咕。但用了一段时间后,我得说——Compose的动画设计,确实比View那套优雅太多了。

今天我们就来聊聊Compose动画的入门三板斧:动画体系整体长什么样animate*AsState怎么用、以及Animatable和AnimatableAsState到底有什么区别

一、Compose动画体系:一张图看懂

先别急着写代码,我们得先搞清楚Compose动画到底有哪些东西。我习惯把Compose动画分成三个层次:

核心思路:Compose动画的本质是「状态驱动」。你改变一个状态值,动画自动帮你过渡过去。不需要手动写监听器、不需要管理动画生命周期。

下面这张图是我自己整理的,基本涵盖了Compose动画的核心脉络:

Compose动画体系 animate*AsState Animatable AnimatableAsState 声明式、最常用 Float/Color/Dp等类型 适合简单属性动画 底层API、更灵活 可手动控制动画 支持重复/反向 Animatable的Compose版本 自动管理生命周期 适合复杂动画场景 共同依赖:AnimationSpec + Easing tween()、spring()、keyframes()、repeatable()...

说白了,这三个东西就是Compose动画的「三驾马车」。你写动画时,90%的场景都逃不出它们的手掌心。下面我们一个一个来看。

二、animate*AsState:最常用的动画入口

如果你只想让某个值变化时有个平滑过渡,用animate*AsState就对了。它是一系列函数的统称,包括:

  • animateFloatAsState — 浮点数动画
  • animateColorAsState — 颜色动画
  • animateDpAsState — 尺寸动画
  • animateIntAsState — 整数动画
  • animateOffsetAsState — 偏移动画

用法极其简单。我给你看个例子:

@Composable
fun FadeInBox() {
    var visible by remember { mutableStateOf(false) }
    
    // 当 visible 变化时,alpha 会自动从 0f 过渡到 1f
    val alpha by animateFloatAsState(
        targetValue = if (visible) 1f else 0f,
        animationSpec = tween(durationMillis = 500)
    )
    
    Box(
        modifier = Modifier
            .size(100.dp)
            .graphicsLayer(alpha = alpha)
            .background(Color.Blue)
            .clickable { visible = !visible }
    )
}

嗯,这里要注意:animateFloatAsState返回的是一个State<Float>,所以要用by委托来解包。我在项目中第一次写的时候忘了加by,结果动画没生效,排查了半天才发现是这个问题。

小技巧:如果你需要自定义动画曲线,可以传animationSpec参数。我个人最常用的是spring(),因为它做弹性效果特别自然,不需要手动算时长。

三、Animatable:更底层的控制力

animate*AsState虽然方便,但有个限制——它只能响应状态变化。如果你想手动控制动画的启停、重复、反向,那就得用Animatable了。

我举个例子你就明白了。假设你要做一个「呼吸灯」效果,让一个圆持续放大缩小:

@Composable
fun BreathingLight() {
    val animatable = remember { Animatable(1f) }
    
    LaunchedEffect(Unit) {
        while (true) {
            // 放大到1.5倍
            animatable.animateTo(1.5f, tween(1000))
            // 缩小回原样
            animatable.animateTo(1f, tween(1000))
        }
    }
    
    Box(
        modifier = Modifier
            .size(100.dp)
            .graphicsLayer(scaleX = animatable.value, scaleY = animatable.value)
            .background(Color.Red, shape = CircleShape)
    )
}

看到区别了吗?Animatable可以让你在LaunchedEffect里用协程的方式手动控制动画流程。你可以写循环、写条件判断、甚至同时控制多个Animatable实例。

注意:Animatable必须在remember中创建,否则每次重组都会重新创建,动画就白写了。我曾经踩过这个坑,调试了一下午才发现是忘记加remember了。

四、AnimatableAsState:两者的结合体

AnimatableAsState又是什么?说白了,它是Animatable的「Compose化」版本。它把Animatable包装成了一个State对象,让你既能享受Animatable的灵活性,又能像animate*AsState那样声明式使用。

看个对比就清楚了:

特性 animate*AsState Animatable AnimatableAsState
声明式使用
手动控制
协程支持
生命周期管理 自动 手动 自动

不过说实话,AnimatableAsState在实际项目中用得并不多。我个人觉得它的定位有点尴尬——简单场景用animate*AsState就够了,复杂场景直接用Animatable更灵活。AnimatableAsState夹在中间,反而让人选择困难。

五、实战:三种方式实现同一个效果

为了让你更直观地感受区别,我用三种方式实现同一个效果——点击按钮让一个方块旋转180度。

方式一:animateFloatAsState

@Composable
fun RotateBoxSimple() {
    var rotated by remember { mutableStateOf(false) }
    val rotation by animateFloatAsState(
        targetValue = if (rotated) 180f else 0f,
        animationSpec = tween(600)
    )
    
    Box(
        modifier = Modifier
            .size(80.dp)
            .rotate(rotation)
            .background(Color.Green)
            .clickable { rotated = !rotated }
    )
}

方式二:Animatable

@Composable
fun RotateBoxManual() {
    val animatable = remember { Animatable(0f) }
    
    Box(
        modifier = Modifier
            .size(80.dp)
            .rotate(animatable.value)
            .background(Color.Blue)
            .clickable {
                CoroutineScope(Dispatchers.Main).launch {
                    val target = if (animatable.value < 90f) 180f else 0f
                    animatable.animateTo(target, tween(600))
                }
            }
    )
}

方式三:AnimatableAsState

@Composable
fun RotateBoxHybrid() {
    var rotated by remember { mutableStateOf(false) }
    val rotation by AnimatableAsState(
        targetValue = if (rotated) 180f else 0f,
        animationSpec = tween(600)
    )
    
    Box(
        modifier = Modifier
            .size(80.dp)
            .rotate(rotation)
            .background(Color.Orange)
            .clickable { rotated = !rotated }
    )
}

你想想看,这三种方式都能实现同样的效果。但实际项目中,我90%的情况都用第一种——简单、直接、不容易出错。只有需要循环动画或精细控制时,我才会掏出Animatable

我的建议:新手先死磕animate*AsState,把它的各种变体(Float、Color、Dp、Int、Offset)都玩熟。等你觉得「不够用了」,再去看Animatable。别一上来就想着用最底层的API,那样反而容易把自己绕晕。

六、避坑指南

最后分享几个我踩过的坑,希望能帮你省点时间:

  1. 动画不生效?检查一下targetValue是否真的变了。Compose动画只会在目标值变化时触发,如果值没变,动画不会执行。
  2. 动画卡顿?看看是不是在动画过程中频繁重组了。可以用derivedStateOfremember缓存中间计算结果。
  3. Animatable的animateTo不执行?确保你在LaunchedEffect或协程作用域里调用它。直接在Composable函数体里调用会报错。
  4. 颜色动画闪白?animateColorAsState时,记得指定ColorSpace,否则默认的插值方式可能导致中间颜色发白。

好了,Compose动画的入门就聊到这里。这三种方式你记住了吗?animate*AsState是日常主力,Animatable是进阶武器,AnimatableAsState是中间过渡。下次写动画时,先想想你的场景适合哪一种,选对了工具,事半功倍。


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