一文带你学会使用Jetpack Compose Animations

4,531 阅读3分钟

Node:本文基于Jetpack Compose 1.0.0-beta01

1. Animation是由state驱动的

Compose的核心思想状态驱动UI刷新,这一思想同样体现在动画上。

UI = f(state)

Compose动画主要是通过不断计算最新的state值来刷新UI,这类似于传统的ValueAnimator,根据动画的插值器和估值器计算当前value,在映射到View的对应属性。Compose天然是基于state驱动的,相关API变得更加简单、合理。

2. AnimateAsState:属性动画

AnimateAsState提供了传统的属性动画的能力。 如下代码,点击Button可以改变Box的颜色

@Preview
@Composable
fun AnimateAsStateDemo() {
    var blue by remember { mutableStateOf(true) }
    val color = if (blue) Blue else Red,

    Column(Modifier.padding(16.dp)) {
        Text("AnimateAsStateDemo")
        Spacer(Modifier.height(16.dp))

        Button(
            onClick = { blue = !blue }
        ) {
            Text("Change Color")
        }
        Spacer(Modifier.height(16.dp))
        Box(
            Modifier
                .preferredSize(128.dp)
                .background(color)
        )
    }
}

如果想让Color以动画的方式切换,可以借助用animateColorAsState

@Composable
fun AnimateAsStateDemo() {
    var blue by remember { mutableStateOf(true) }
    val color by animateColorAsState(
        if (blue) Blue else Red,
        animationSpec = spring(Spring.StiffnessVeryLow)
    )

 	//...
}

animateColorAsState将Color的变化过程转换为一个可订阅的state;animationSpec用来进行动画配置,比如例子总配置了一个弹簧动画效果

animationSped还可以监听动画结束的回调

val color by animateColorAsState(
    if (blue) Blue else Red,
    animationSpec = spring(stiffness = Spring.StiffnessVeryLow),
    finishedListener = {
        blue = !blue
    }
)

如上,可以实现折返动画的效果

除了AnimateColorAsState以外,还支持其他各类型的动画: 在这里插入图片描述

3. updateTransition:多个动画同步

如果想同时进行多个属性的动画,并保持同步,需要使用updateTransition,类似使用AnimationSet组合多个动画


private sealed class BoxState(val color: Color, val size: Dp) {
    operator fun not() = if (this is Small) Large else Small
    object Small : BoxState(Blue, 64.dp)
    object Large : BoxState(Red, 128.dp)
}

@Composable
fun UpdateTransitionDemo() {

    var boxState: BoxState by remember { mutableStateOf(BoxState.Small) }
    val transition = updateTransition(targetState = boxState)

    Column(Modifier.padding(16.dp)) {
        Text("UpdateTransitionDemo")
        Spacer(Modifier.height(16.dp))

        val color by transition.animateColor {
            boxState.color
        }
        val size by transition.animateDp(transitionSpec = {
            if (targetState == BoxState.Large) {
                spring(stiffness = Spring.StiffnessVeryLow)
            } else {
                spring(stiffness = Spring.StiffnessHigh)
            }
        }) {
            boxState.size
        }

        Button(
            onClick = { boxState = !boxState }
        ) {
            Text("Change Color and size")
        }
        Spacer(Modifier.height(16.dp))
        Box(
            Modifier
                .preferredSize(size)
                .background(color)
        )
    }
}

updateTransition根据targetState创建一个Transition,然后通过Transition的扩展函数可以创建各种属性动画所需的state。

需要注意,Transition的codename容易跟传统的位移动画混淆,其实完全没有联系,这里的Transition是用来同步多个动画的工具。

transitionSpec可以进行动画配置,上面例子中,在Box放大和缩小过程中,分别设置不同的弹性动画效果。

同样,Transition根据属性类型的不同,有多种扩展函数

在这里插入图片描述

4. AnimateVisibility:可见性动画

在View的可见性发生变化时做动画是一个常见需求。传统的View体系中,一般使用alpha值变化实现fadeIn/fadeOut,或者通过transitionX/Y的变化,实现slideIn/slideOut

Compose中则使用AnimatedVisibility

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimateVisibilityDemo() {
    var visible by remember { mutableStateOf(true) }

    Column(Modifier.padding(16.dp)) {
        Text("AnimateVisibilityDemo")
        Spacer(Modifier.height(16.dp))

        Button(
            onClick = { visible = !visible }
        ) {
            Text(text = if (visible) "Hide" else "Show")
        }

        Spacer(Modifier.height(16.dp))

        AnimatedVisibility(visible) {
            Box(
                Modifier
                    .preferredSize(128.dp)
                    .background(Blue)
            )
        }
    }
}

如上,默认的可见性动画效果是淡入/淡出 + 收缩/放大: 在这里插入图片描述

5. AnimateContentSize : 布局大小动画

animateContentSizeModifier的扩展方法,添加这个方法的Composable,会监听子Composable大小的变化,并以动画方式作成相应调整

@Composable
fun AnimateContentSizeDemo() {
    var expend by remember { mutableStateOf(false) }

    Column(Modifier.padding(16.dp)) {
        Text("AnimateContentSizeDemo")
        Spacer(Modifier.height(16.dp))

        Button(
            onClick = { expend = !expend }
        ) {
            Text(if (expend) "Shrink" else "Expand")
        }
        Spacer(Modifier.height(16.dp))

        Box(
            Modifier
                .background(Color.LightGray)
                .animateContentSize()
        ) {
            Text(
                text = "animateContentSize() 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.",
                fontSize = 16.sp,
                textAlign = TextAlign.Justify,
                modifier = Modifier.padding(16.dp),
                maxLines = if (expend) Int.MAX_VALUE else 2
            )
        }
    }
}

animateContentSize同样可以配置animSpec以及endListener

在这里插入图片描述

6. Crossfade : 布局切换动画

Crossfade 本身是一个Composable,其内部的子布局发生切换时,可以添加淡入淡出效果:

@Composable
fun CrossfadeDemo() {

    var scene by remember { mutableStateOf(DemoScene.Text) }

    Column(Modifier.padding(16.dp)) {

        Text("AnimateVisibilityDemo")
        Spacer(Modifier.height(16.dp))

        Button(onClick = {
            scene = when (scene) {
                DemoScene.Text -> DemoScene.Icon
                DemoScene.Icon -> DemoScene.Text
            }
        }) {
            Text("toggle")
        }

        Spacer(Modifier.height(16.dp))

        Crossfade(
            current = scene,
            animation = tween(durationMillis = 1000)
        ) {
            when (scene) {
                DemoScene.Text ->
                    Text(text = "Phone", fontSize = 32.sp)
                DemoScene.Icon ->
                    Icon(
                        imageVector = Icons.Default.Phone,
                        null,
                        modifier = Modifier.preferredSize(48.dp)
                    )
            }
        }

    }
}

参考

Sample Code