Android Jetpack Compose 动画

1,389 阅读13分钟

无论是Jetpack Compose还是原来的view体系,想做出好看的交互效果都离不开动画。Compose同样提供了相应的API来实现动画。这些API大致可以分为两类:高级别API(直接拿来用,就像使用普通组件比如Container一样)和低级别API(需要自己配置一些属性之后才能使用)。接下来就让我们详细的了解一下。

高级别API

高级别的api主要有以下几种

  • AnimatedVisibility:元素进入/退出时的过渡动画
  • AnimatedContent:布局内容变化时的动画
  • Modifier.animateContentSize:布局大小变化时的动画
  • Crossfade:两个布局切换时的淡入/淡出动画

AnimatedVisibility

组件进入或退出时的动画

@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

常用的构造函数如上,只需指定visible来控制是否可见,enterexit为默认添加的进入和退出动画,示例如下

@Composable
fun AnimatedVisibilityDemo() {
    var visibility by remember {
        mutableStateOf(true)
    }
    Column {
        AnimatedVisibility(visible = visibility,
        ) {
            Text(
                text = "Hello",
                Modifier
                    .fillMaxWidth()
                    .height(200.dp)
                    .background(Color.LightGray)
            )
        }
        Button(onClick = { visibility = !visibility }) {
            Text(text = "ChangeVisibility")
        }
    }

}

AnimatedContent

用来实现不同组件间的切换动画

@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {
        fadeIn(animationSpec = tween(220, delayMillis = 90)) +
                scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) with
                fadeOut(animationSpec = tween(90))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
)

这里需要传入两个参数,一个是targetState,一个是content,content是基于targetState创建的Composable,当target变化时content的内容也会随之变化。targetState一定要在content中被使用,否则当targetState变化时,只见动画,却不见内容的变化,视觉上会很奇怪。这里的transitionSpec其实就是定义ContentTransform

class ContentTransform(
    val targetContentEnter: EnterTransition,
    val initialContentExit: ExitTransition,
    targetContentZIndex: Float = 0f,
    sizeTransform: SizeTransform? = SizeTransform()
)

其实就是定义enterTransitionexitTransition以及sizeTransform。这两个transition其实可以使用slideInxxx,slideoutxxx,fadeIn,fadeOut等组合。下面例子就是点击按钮实现从下往上淡入,从下往上淡出的轮播效果(这里使用with来创建ContentTransform)

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentDemo2() {
    var count by remember {
        mutableStateOf(0)
    }
    val testNames = listOf(
        "我是a",
        "我是aaa",
        "我是bbbbbbb",
        "他是ccccccccccccccccccccc",
        "她是cccc+++++ddd++eeee+ffff+gggggg"
    )
    val result by remember {
        derivedStateOf { testNames[count] }
    }
    Row {

        Button(onClick = { if (count >= testNames.size-1) count = 0 else count++ }) {
            Text(text = "Add")
        }
        AnimatedContent(targetState = result, transitionSpec = {
            slideInVertically(initialOffsetY = { it }) + fadeIn() with slideOutVertically() + fadeOut()
        }) { targetCount ->
            Text(text = result)
        }
    }

}

Crossfade

可以说是AnimateContent的简化版,只提供淡入淡出效果

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun <T> Crossfade(
    targetState: T,
    modifier: Modifier = Modifier,
    animationSpec: FiniteAnimationSpec<Float> = tween(),
    content: @Composable (T) -> Unit
)

例子就不提供了可以直接把上面的例子中的AnimatedContent换成Crossfade

Modifieir.animatedContentSize

当容器尺寸发生变化时,会通过动画进行过渡,比如可展开的Text

@Composable
fun AnimatedContentSizeDemo() {
    var expand by remember {
        mutableStateOf(false)
    }
    Column(Modifier.padding(16.dp)) {
        Text(text = "AnimatedContentSizeDemo")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { expand = !expand }) {
            Text(text = if (expand) "Shrink" else "Expand")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Box(modifier = Modifier
            .background(Color.LightGray)
            .animateContentSize()){
            Text(
                text = "This modifier animates its own size when its child modifier (or the child composable if it is already at the tail of the chain) changes size. This allows the parent modifier to observe a smooth size change, resulting in an overall continuous visual change.\n" +
                        "A FiniteAnimationSpec can be optionally specified for the size change animation. By default, spring will be used.\n" +
                        "An optional finishedListener can be supplied to get notified when the size change animation is finished. Since the content size change can be dynamic in many cases, both initial value and target value (i.e. final size) will be passed to the finishedListener. Note: if the animation is interrupted, the initial value will be the size at the point of interruption. This is intended to help determine the direction of the size change (i.e. expand or collapse in x and y dimensions).",
                fontSize = 16.sp,
                textAlign = TextAlign.Justify,
                modifier = Modifier.padding(16.dp),
                maxLines = if (expand)Int.MAX_VALUE else 2,
                overflow = TextOverflow.Ellipsis
            )
        }

    }
}

低级别API

低级别的API主要包括以下几种

  • Animatable:是一个值的集合,属于流程定制型动画
  • animateXXXAsState:对单一值进行动画,类似于传统view系统的属性动画
  • updateTransition:同时对多个属性值进行动画
  • animate:该函数是一个suspend函数,可见只能用于协程中,对动画进行深度定制

Animatable

Animatable是一个值的集合。他通过animateTo自动执行动画。而且如果animateTo在一个正在执行的动画中被触发,那么就会从这个动画的值开始执行到设定的targetValue,以保证动画的连续性。它其实是一个流程定制型动画

使用

首先构造Animatable,比如对Dp进行动画,构造需要两个参数,一个是targetValue,另一个则是animatable最终返回值的类型,最终返回的其实是State<T>,这里的T就是typeConverter定义的类型

val anim = remember { Animatable(initialValue = size1, typeConverter = Dp.VectorConverter) }

其次在compose中使用该值

Box( Modifier .size(anim.value) .background(Color.Green) .clickable { big = !big })

最后在协程中通过animateTo或者snapTo来启动动画

LaunchedEffect(big) {
    //直接变换到相应的值
    anim.snapTo(if (big) 192.dp else 0.dp)
    //然后开始动画
    anim.animateTo(size1)
}

这里的animateTo则是对动画的流程进行定制的,它的定义如下

suspend fun animateTo(
    targetValue: T,
    animationSpec: AnimationSpec<T> = defaultSpringSpec,
    initialVelocity: T = velocity,
    block: (Animatable<T, V>.() -> Unit)? = null
)

这里的定制主要是通过animationSpec进行的。block则是一个回调,动画的每一帧都会回调这个方法。重点来看看animationSpec

AnimationSpec存储了动画信息:

  • 需要做动画的值的类型;
  • 将值转换成AnimationVector时用到的动画配置信息。

其实他就是一个接口

interface AnimationSpec<T> {

    fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedAnimationSpec<V>
}

它的实现一共有以下11种

AnimationSpecImpl.png 接下来我们就分析一下这些实现

1:DurationBasedAnimationSpec

它是一个具有固定持续时间的AnimationSpec。他也是一个接口,它的实现有三种KeyframeSpecTweenSpecSnapSpec。稍后详细说明

2:FiniteAnimationSpec

它是所有有限次数AnimationSpec的都实现的接口,其实也就是KeyframeSpecTweenSpecSnapSpecRepeatableSpecSpringSpec

3:FloatAnimationSpec

同样是一个接口,他只针对float值进行动画,它的实现只有两种FloatSpringSpecFloatTweenSpec

4:FloatSpringSpec

这是一个弹性动画,它的定义如下

class FloatSpringSpec(
    val dampingRatio: Float = Spring.DampingRatioNoBouncy,
    val stiffness: Float = Spring.StiffnessMedium,
    private val visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
)

其实就是配置弹性系数dampingRation阻尼系数stiffness用于实现阻尼效果,比如如下示例

@Composable
private fun FloatSpringSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val float1 = remember(big) { if (big) 100f else 50f }
    val floatAnim = remember { Animatable(float1) }

    LaunchedEffect(key1 = big) {
        floatAnim.animateTo(
            float1,
            FloatSpringSpec(
                dampingRatio = Spring.DampingRatioHighBouncy,
                stiffness = Spring.StiffnessMedium
            )
        )
    }

    Box(
        Modifier
            .width(100.dp)
            .height(floatAnim.value.dp)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}
5:FloatTweenSpec

它是将一个float动画利用easing函数从起始值过渡到最终值。定义如下

class FloatTweenSpec(
    val duration: Int = DefaultDurationMillis,
    val delay: Int = 0,
    private val easing: Easing = FastOutSlowInEasing
) 

duration动画时长delay则是动画开始前的等待时长easing则是差值器,就是根据fraction决定当前值的函数。比如定义好的FastOutSlowInEasingLinearOutSlowInEasingFastOutLinearInEasingLinearEasing。如果不满足,那么可实现Easing接口的transform方法自定义动画运动到某个百分比时的动画值

fun interface Easing {
    fun transform(fraction: Float): Float
}

简单示例如下:

@Composable
private fun FloatTweenSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val float1 = remember(big) { if (big) 100f else 50f }
    val floatAnim = remember { Animatable(float1) }

    LaunchedEffect(key1 = big){
        floatAnim.animateTo(
            float1,
            FloatTweenSpec(
                easing = FastOutSlowInEasing
            )
        )
    }

    Box(
        Modifier
            .size(floatAnim.value.dp)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}
6:InfiniteRepeatableSpec

它是一个无限循环的动画,永远不会自动停止。定义如下

class InfiniteRepeatableSpec<T>(
    val animation: DurationBasedAnimationSpec<T>,
    val repeatMode: RepeatMode = RepeatMode.Restart,
    val initialStartOffset: StartOffset = StartOffset(0)
)

这里的animation则是需要循环的动画,可以看到是DurationBasedAnimationSpec。通过1中介绍可以看出其实只有三种动画可以使用它进行无限循环KeyframeSpecTweenSpecSnapSpecinitialStartOffset则是起始时动画偏移量

//这种是延迟1000ms后开始动画 
StartOffset(1000, StartOffsetType.Delay))) 
//这种是立即从动画的1000ms处开始执行 
StartOffset(1000, StartOffsetType.FastForward)))

通常使用infiniteRepeatable函数来创建

@Stable
fun <T> infiniteRepeatable(
    animation: DurationBasedAnimationSpec<T>,
    repeatMode: RepeatMode = RepeatMode.Restart,
    initialStartOffset: StartOffset = StartOffset(0)
): InfiniteRepeatableSpec<T> =
    InfiniteRepeatableSpec(animation, repeatMode, initialStartOffset)

示例如下,至于这里的tween其实就是TweenSpec,稍后介绍,使用示例如下

@Composable
private fun InfiniteRepeatableSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val size1 = remember(big) { if (big) 96.dp else 48.dp }
    val anim = remember { Animatable(size1,Dp.VectorConverter) }

    LaunchedEffect(key1 = big){
        anim.animateTo(size1, infiniteRepeatable(tween(), RepeatMode.Reverse,
            StartOffset(1000, StartOffsetType.FastForward)))
    }

    Box(
        Modifier
            .size(anim.value)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}
7:KeyframsSpec

它是一个关键帧动画,也就是说需要定义出一系列的关键帧,比如动画执行到100ms时值是多少,它的定义如下

class KeyframesSpec(val config: KeyframesSpecConfig) : DurationBasedAnimationSpec

可以看到是通过KeyframesSpecConfig来定义的关键帧,这个config的定义如下

class KeyframesSpecConfig<T> {
    //动画时长
    var durationMillis: Int = DefaultDurationMillis

    //动画延时时长,单位毫秒

    var delayMillis: Int = 0

    internal val keyframes = mutableMapOf<Int, KeyframeEntity<T>>()

    //添加一个关键帧,使得动画的值在这一时刻是这个值,比如 0.8f at 100(在动画执行到100ms时值为0.8f)
    infix fun T.at(/*@IntRange(from = 0)*/ timeStamp: Int): KeyframeEntity<T> {
        return KeyframeEntity(this).also {
            keyframes[timeStamp] = it
        }
    }

    //添加一个动画差值器,比如 0f at 50 with LinearEasing(从这之前到50ms这个时间段内采用LinearEasing差值器)
    infix fun KeyframeEntity<T>.with(easing: Easing) {
        this.easing = easing
    }
    ...
}

每个方法的作用都做了注释,通常我们使用keyframs函数来创建关键帧动画

@Stable
fun <T> keyframes(
    init: KeyframesSpec.KeyframesSpecConfig<T>.() -> Unit
)

这里示例如下

@Composable
private fun KeyframesSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val size1 = remember(big) { if (big) 96.dp else 48.dp }
    val anim = remember { Animatable(size1,Dp.VectorConverter) }

    LaunchedEffect(key1 = big){
        anim.animateTo(size1, keyframes {
            durationMillis = 450
            delayMillis = 500
            48.dp at 0 with FastOutLinearInEasing
            144.dp at 150 with FastOutSlowInEasing
            20.dp at 300
        })
    }

    Box(
        Modifier
            .size(anim.value)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}
8:RepeatableSpec

他其实跟InfiniteRepeatableSpec一样,只不过这里指定了循环的次数

class RepeatableSpec<T>(
    val iterations: Int,
    val animation: DurationBasedAnimationSpec<T>,
    val repeatMode: RepeatMode = RepeatMode.Restart,
    val initialStartOffset: StartOffset = StartOffset(0)
) 

InfiniteRepeatableSpec相比,只多了一个iterations(次数)参数,通常我们通过repeatable函数来进行构造

@Stable
fun <T> repeatable(
    iterations: Int,
    animation: DurationBasedAnimationSpec<T>,
    repeatMode: RepeatMode = RepeatMode.Restart,
    initialStartOffset: StartOffset = StartOffset(0)
): RepeatableSpec<T> =
    RepeatableSpec(iterations, animation, repeatMode, initialStartOffset)

所以我们将InfiniteRepeatable示例中的循环改成3次如下

@Composable
private fun RepeatableSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val size1 = remember(big) { if (big) 96.dp else 48.dp }
    val anim = remember { Animatable(size1,Dp.VectorConverter) }

    LaunchedEffect(key1 = big){
        anim.animateTo(size1, repeatable(3,tween(), RepeatMode.Reverse,
            StartOffset(0, StartOffsetType.FastForward))
        )
    }

    Box(
        Modifier
            .size(anim.value)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}
9:SnapSpec

A jump-cut type of animation,其实就是直接将动画执行到最终值。类似于view体系的moveTo。通常使用snap函数进行构造。示例如下

@Composable
private fun SnapSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val size1 = remember(big) { if (big) 96.dp else 48.dp }
    val anim = remember { Animatable(size1,Dp.VectorConverter) }

    LaunchedEffect(key1 = big){
        anim.animateTo(size1, snap())
    }

    Box(
        Modifier
            .size(anim.value)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}
10:SpringSpec

弹性动画,跟FloatSpringSpec类似,只不过它不仅仅支持float类型的值。通常通过spring函数创建

@Stable
fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy,
    stiffness: Float = Spring.StiffnessMedium,
    visibilityThreshold: T? = null
): SpringSpec<T> =
    SpringSpec(dampingRatio, stiffness, visibilityThreshold)

该方法的示例如下

@Composable
private fun SpringSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val size1 = remember(big) { if (big) 96.dp else 48.dp }
    val anim = remember { Animatable(size1,Dp.VectorConverter) }

    LaunchedEffect(key1 = big){
        anim.animateTo(size1, spring(0.1f, Spring.StiffnessMedium), 2000.dp)
    }

    Box(
        Modifier
            .size(anim.value)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}
11:TweenSpec

补间动画,它可以指定动画的时长,延迟以及差值器,定义如下

@Immutable
class TweenSpec<T>(
    val durationMillis: Int = DefaultDurationMillis,
    val delay: Int = 0,
    val easing: Easing = FastOutSlowInEasing
) : DurationBasedAnimationSpec<T> 

该动画通常使用tween()函数创建

@Stable
fun <T> tween(
    durationMillis: Int = DefaultDurationMillis,
    delayMillis: Int = 0,
    easing: Easing = FastOutSlowInEasing
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)

该方法示例如下

@Composable
private fun TweenSpecDemo() {
    var big by remember {
        mutableStateOf(false)
    }
    val size1 = remember(big) { if (big) 96.dp else 48.dp }
    val anim = remember { Animatable(size1,Dp.VectorConverter) }

    LaunchedEffect(key1 = big){
        anim.animateTo(size1,  tween())
    }

    Box(
        Modifier
            .size(anim.value)
            .background(Color.Green)
            .clickable {
                big = !big
            })
}

animateXXXAsState

该方法是对单一值进行动画的,它类似于传统视图中的属性动画,可以自动完成从当前值到目标值过渡的估值计算。主要有以下几种

1.png

返回的是一个State<T>,比如animateDpAsState,它返回的就是一个Sate<Dp>。他其实是简化版的Animatable。最终调用animateValueAsState.

@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
    return animateValueAsState(
        targetValue,
        Dp.VectorConverter,
        animationSpec,
        finishedListener = finishedListener
    )
}

这里直接调用的animateValueAsState,并且指定拉倒typeConverterDp.VectorConverter,其实跟我们创建Animatable时候一样,继续看animateValueAsState方法

@Composable
fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember {
        spring(visibilityThreshold = visibilityThreshold)
    },
    visibilityThreshold: T? = null,
    finishedListener: ((T) -> Unit)? = null
): State<T> {

    val animatable = remember { Animatable(targetValue, typeConverter) }
    ...
    LaunchedEffect(channel) {
        for (target in channel) {
          
            val newTarget = channel.tryReceive().getOrNull() ?: target
            launch {
                if (newTarget != animatable.targetValue) {
                    animatable.animateTo(newTarget, animSpec)
                    listener?.invoke(animatable.value)
                }
            }
        }
    }
    return animatable.asState()
}

可以看到剩下的步骤与使用Animatable一样,只不过是已经写好的。创建Animatable,在协程中使用animateTo。 所以该方法使用时

首先需要调动animateXXXAsState指定初始值,比如这里

val size by animateDpAsState(if (big1) 96.dp else 48.dp) // State

然后直接使用该值

@Composable
private fun AnimateXXXAsDemo() {
    //animateXXXAsState是直接确定了动画的起点和终点

    var big1 by remember {
        mutableStateOf(false)
    }

    val size by animateDpAsState(if (big1) 96.dp else 48.dp) // State
    Box(
        Modifier
            .size(size)
            .background(Color.Green)
            .clickable {
                big1 = !big1
            })

}

updateTransition

同时对多个值进行动画,其实就是整合多个Animatable,使其一起动画,所能支持的值与animateXXXAsState是一致的

2.png

所以用法基本上同上述的大同小异

  1. 创建updateTransition:直接传入targetState,一般来说是一个枚举值
@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
)
  1. 利用创建好的transition调用transition.animateXXX来进行具体值的动画,比如这里是对color进行动画。而animateXXX中配置的transitionSpec其实是FiniteAnimationSpec,这在上面已经介绍过了
val color by transition.animateColor(label = "Color", transitionSpec = {
    when{
        BoxState.Small isTransitioningTo BoxState.Large->
            keyframes {
                durationMillis=2500
                Color.Green at 0
                Color.Cyan at 500 with LinearOutSlowInEasing
                Color.DarkGray at 1000 with FastOutLinearInEasing
                Color.Magenta at 1500  with LinearEasing
                Color.Red at 2000 with FastOutSlowInEasing

            }
        else-> tween(durationMillis = 3000)
    }
}) { state ->
    when (state) {
        BoxState.Small -> Color.Blue
        BoxState.Large -> Color.Yellow
    }
}
  1. 将定义好的动画值绑定到需要做动画的compsable组件上
  2. 更改“状态”运行动画

下面的示例为对box同时进行大小和颜色动画

private enum class BoxState{
    Small,
    Large
}
@Composable
private fun UpdateTransitionDemo(){
    var boxState by remember {
        mutableStateOf(BoxState.Small)
    }

    //第一步创建updateTransition
    val transition= updateTransition(targetState = boxState, label = "Box Transition")
    //第二步利用创建好的transition调用transition.animateXXX来进行具体值的动画
    val color by transition.animateColor(label = "Color", transitionSpec = {
        when{
            BoxState.Small isTransitioningTo BoxState.Large->
                keyframes {
                    durationMillis=2500
                    Color.Green at 0
                    Color.Cyan at 500 with LinearOutSlowInEasing
                    Color.DarkGray at 1000 with FastOutLinearInEasing
                    Color.Magenta at 1500  with LinearEasing
                    Color.Red at 2000 with FastOutSlowInEasing

                }
            else-> tween(durationMillis = 3000)
        }
    }) { state ->
        when (state) {
            BoxState.Small -> Color.Blue
            BoxState.Large -> Color.Yellow
        }
    }
    //第二步利用transition.animateXXX来进行具体值的动画
    val size by transition.animateDp(label = "Size", transitionSpec = {
        when{
            BoxState.Small isTransitioningTo BoxState.Large-> keyframes {
                durationMillis=2000
                500.dp at 0
                50.dp at 1000 with LinearOutSlowInEasing
                400.dp at 20000 with FastOutLinearInEasing
                100.dp at 3000  with LinearEasing
                300.dp at 4000 with FastOutSlowInEasing
            }
            else-> spring(dampingRatio = Spring.DampingRatioHighBouncy)
        }
    }) { state ->
        when (state) {
            BoxState.Small -> 100.dp
            BoxState.Large -> 300.dp
        }
    }

    Column {
        //第四步开启动画
        Button(onClick = {
            when(boxState){
                BoxState.Small->boxState=BoxState.Large
                else ->boxState=BoxState.Small
            }
        }){
            Text(text = "Toggle")
        }

        //第三步将动画值绑定到需要动画的组件上
        Box(
            Modifier
                .size(size)
                .background(color = color)) {

        }

    }
}

animate

该函数是一个suspend函数,可见只能用于协程中。这个函数可对动画进行深度定制,也就是控制动画的执行顺序可以更容易,其实主要是通过协程作用域实现的

suspend fun animate(
    initialValue: Float,
    targetValue: Float,
    initialVelocity: Float = 0f,
    animationSpec: AnimationSpec<Float> = spring(),
    block: (value: Float, velocity: Float) -> Unit
)

这里指定了动画的开始值结束值以及AnimationSpec。比如利用这个实现点赞红心的alpha和scale动画效果的demo如下

@Composable
fun AnimateDemo() {
    var alpha by remember {
        mutableStateOf(0f)
    }
    var scale by remember {
        mutableStateOf(0f)
    }

    val scope = rememberCoroutineScope()

    Box(
        Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTapGestures(
                    onDoubleTap = {
                        scope.launch {
                            //point1
                            // 开始出现时的动画
                            coroutineScope {
                                //point3
                                //执行透明度的变换
                                launch {
                                    animate(0f, 1f, animationSpec = keyframes {
                                        durationMillis = 500
                                        0f at 0
                                        0.5f at 100
                                        1f at 225
                                    }) { value, _ ->
                                        alpha = value
                                    }
                                }
                                //point4与point3是并行的。也就达到了updateTransition的效果。
                                //执行scale变换
                                launch {
                                    animate(
                                        0f,
                                        2f,
                                        animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy)
                                    ) { value, _ ->
                                        scale = value
                                    }
                                }

                            }
                            //point2与point1是顺序执行
                            //开始出现动画执行完毕后直接执行消失动画
                            coroutineScope {
                                launch {
                                    animate(1f, 0f, animationSpec = snap()) { value, _ ->
                                        alpha = value
                                    }
                                }
                                launch {
                                    animate(2f, 4f, animationSpec = snap()) { value, _ ->
                                        scale = value
                                    }
                                }
                            }
                        }

                    }
                )
            }
    ) {


        Icon(
            Icons.Filled.Favorite,
            "点赞",
            Modifier
                .align(Alignment.Center)
                .graphicsLayer(
                    alpha = alpha,
                    scaleX = scale,
                    scaleY = scale
                ),
            tint = Color.Red
        )
    }
}

可以看到point1与point2在一个协程作用域中,这两块是顺序执行的。而point3和point4分别通过launch函数又重新创建了一个子协程,所以执行互不干扰也就是并行执行。如果想对动画进行取消则cancel协程就可以了。以上就是compose中动画API的介绍。