Jetpack Compose中的动画

动画分类概览

Jetpack Compose中没有沿用Android原有的View动画和属性动画,而是新创建了一套全新的动画系统API,这是理所当然的,因为旧的动画系统主要是基于View体系的,而Compose中需要针对的是Composable可组合函数进行处理,那么势必要创造一套新的玩具出来,同时,这也无疑增加了开发者的学习成本。

在这里插入图片描述

乍一看Jetpack Compose中的动画Api,尼玛是真的多呀,我C了,简直令人眼花缭乱、云里雾里、天马行空、小兔乱撞、手脚慌乱、头冒虚汗、四肢抓狂、不知所措呀 。。。😭

但是我们可以对其进行分一下类,如果按照使用的方便程度划分,大概可以分为两大类:高级动画API和低级动画API(这里类比高级开发语言的分类,并不是指效果多高级)。

其中高级动画API使用比较简单方便,封装度高,更加适用于日常业务开发,而低级动画API则使用起来较为麻烦,因为其配置项或流程较多,但是却更加灵活,能对动画效果做出更加精细的控制,适合自定义要求度较高的业务场景。

我们还可以按照功能类型进行一个大概的分类,也就是上图中的划分,下面再按照需求点用表格整理一下:

功能需求点 可能符合的API类型
◻️ 单个组件的显示隐藏转场动画
◻️ 每个子组件需要不同的入场/出场效果
AnimatedVisibility
◻️ 根据组件内容状态变化的动画(数据、尺寸等)
◻️ 不同组件间的切换动画
AnimatedContent
Modifier.animateContentSize
单纯的淡入淡出动画 Crossfade
◻️ 根据数据估值状态自动执行连续动画
◻️ 基于单个数据值的状态变化执行动画
◻️ 基于自定义数据类型进行估值动画
◻️ 指定每一帧/每一时刻的动画状态
◻️ 替代传统 View 属性动画的方案
animateXXXAsState
◻️ 根据不同状态同时管理和运行多个动画
◻️ 进入界面时自动执行一次动画
◻️ 监听动画状态
◻️ 替代传统 View 动画中的 AnimationSet 的方案。
updateTransition
MutableTransitionState
永不停止、无限循环的动画 rememberInfiniteTransition
◻️ 更加底层的低级动画API
◻️ 可高度自由定制的估值属性动画
◻️ 需要在协程中执行的动画
◻️ 需要控制一些动画并行执行
Animatable
◻️ 更加底层的低级动画API
◻️ 需要手动精确控制动画的时间
◻️ 手势动画,fling衰减动画
TargetBasedAnimation
DecayAnimation

高级动画API

AnimatedVisibility

AnimatedVisibility主要用于页面显示状态的动画,即显示/隐藏的过渡动画,或者入场/离场动画。
可以使用 + 运算符组合多个 EnterTransitionExitTransition 对象,并且每个对象都接受可选参数以自定义其行为。

@Composable
fun AnimatedVisibilityExample() {
   
    var visible by remember {
    mutableStateOf(true) }
    val density = LocalDensity.current
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
   
        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically {
    with(density) {
    -40.dp.roundToPx() } } // 从顶部 40dp 的地方开始滑入
                    + expandVertically(expandFrom = Alignment.Top)  // 从顶部开始展开
                    + fadeIn(initialAlpha = 0.3f), // 从初始透明度 0.3f 开始淡入
            exit = slideOutVertically() + shrinkVertically() + fadeOut()
        ) {
   
            Text("Hello",
                Modifier.background(Color.Green).fillMaxWidth().height(200.dp)
                    .wrapContentWidth(Alignment.CenterHorizontally),
                fontSize = 20.sp
            )
        }
        Button(
            onClick = {
    visible = !visible },
            modifier = Modifier.padding(top = 200.dp)
        ) {
   
            Text(text = if(visible) "隐藏" else "显示")
        }
    }
}

运行效果:

默认情况下 EnterTransitionfadeIn() + expandIn() 的效果,而 ExitTransitionshrinkOut() + fadeOut() 的效果, Compose额外提供了RowScope.AnimatedVisibilityColumnScope.AnimatedVisibility两个扩展方法, 当我们在RowColumn中调用时,该组件的默认动画效果会根据父容器的布局特征进行调整,比如在RowEnterTransition默认是fadeIn + expandHorizontally组合,而在ColumnEnterTransition默认是fadeIn + expandVertically组合方案。

EnterTransitionExitTransition 动画分类效果示例:

EnterTransition ExitTransition
FadeIn FadeOut
slideIn slideOut
slideInHorizontally slideOutHorizontally
slideInVertically slideOutVertically
scaleIn scaleOut
expandIn shrinkOut
expandHorizontally shrinkHorizontally
expandVertically shrinkVertically

为子项添加进入和退出动画效果

AnimatedVisibility 中的内容(直接或间接子项)可以使用 Modifier.animateEnterExit 修饰符为每个子项指定不同的动画行为。

其中每个子项的视觉效果均由 AnimatedVisibility 可组合项中指定的动画与子项自己的进入和退出动画构成。

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedVisibilityExample3() {
   
    var visible by remember {
    mutableStateOf(true) }
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
   
        AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) {
   
            // 外层Box组件淡入淡出进出屏幕
            Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
   
                Box(Modifier.align(Alignment.Center)
                    .sizeIn(minWidth = 256.dp, minHeight = 64.dp).background(Color.Green)
                    .animateEnterExit(enter = slideInVertically(), exit = slideOutVertically())
                ) {
   
                    Text(text = "内层Box组件滑动进出屏幕", Modifier.align(Alignment.Center))
                }
                Box(Modifier.padding(top = 150.dp).align(Alignment.Center)
                    .sizeIn(minWidth = 256.dp, minHeight = 64.dp).background(Color.Cyan)
                    .animateEnterExit(enter = scaleIn(), exit = scaleOut())
                ) {
   
                    Text(text = "内层层Box组件缩放进出屏幕", Modifier.align(Alignment.Center))
                }
            }
        }
        Button(
            onClick = {
    visible = !visible },
            modifier = Modifier.padding(top = 50.dp)
        ) {
   
            Text(text = if(visible) "隐藏" else "显示")
        }
    }
}

运行效果:

有时我们希望 AnimatedVisibility 内的每个子组件有不同的过渡动画,此时请在 AnimatedVisibility 可组合项中指定 EnterTransition.NoneExitTransition.None,即完全不应用任何动画,这样子项就可以通过 Modifier.animateEnterExit 拥有各自的不同动画了。

自定义 Enter/Exit 动画

如果想在内置进入和退出动画之外添加自定义动画效果,请在 AnimatedVisibilityScope 内设置 transition, 添加到 Transition 实例的所有动画状态都将与 AnimatedVisibility 的进入和退出动画同时运行。

AnimatedVisibility 会等到 Transition 中的所有动画都完成后再移除其内容。对于独立于 Transition(例如使用 animate*AsState)创建的退出动画,AnimatedVisibility 将无法解释这些动画,因此可能会在完成之前移除内容可组合项。

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedVisibilityExample4() {
   
    var visible by remember {
    mutableStateOf(true) }
    Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
   
        AnimatedVisibility(visible = visible, enter = scaleIn(), exit = scaleOut()) {
   
            // 使用 AnimatedVisibilityScope#transition 添加自定义的动画与AnimatedVisibility同时执行
            val background by transition.animateColor(label = "backgroundTransition") {
    state ->
                if (state == EnterExitState.Visible) Color.Blue else Color.Green
            }
            Box(modifier = Modifier.size(100.dp).background(background))
        }
        Button(
            onClick = {
    visible = !visible },
            modifier = Modifier.padding(top = 120.dp)
        ) {
   
            Text(text = if(visible) "隐藏" else "显示")
        }
    }
}

运行效果:

AnimatedContent

AnimatedContent 可组合项会在内容根据目标状态发生变化时,为内容添加动画效果。
与 AnimatedVisibility 的区别是: AnimatedVisibility用来添加组件自身的入场/离场动画,而AnimatedContent是实现不同组件间的切换动画

AnimatedContent接收一个targetState和一个contentcontent 是基于 targetState 创建的Composable,当targetState变化时,content的内容也会随之变化。AnimatedContent内部维护着targetStatecontent的映射表,查找 targetState新旧值对应的content后,在content发生重组时附加动画效果。

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample() {
   
    Column {
   
        var count by remember {
    mutableStateOf(0) }
        Button(onClick = {
    count++ }) {
    Text("Add") }
        AnimatedContent(targetState = count) {
    targetCount ->
            // 这里要使用lambda的参数 `targetCount`, 而不是 `count`,否则将没有意义(API 会将此值用作键,以标识当前显示的内容)
            Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
        }
    }
}

运行效果:

ContentTransform

AnimatedContent默认是淡入淡出效果,可以为 transitionSpec 参数指定 ContentTransform 对象,以自定义此动画行为。

可以使用 with infix 函数来组合 EnterTransitionExitTransition,以创建 ContentTransform

 @ExperimentalAnimationApi
 infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)

ContentTransform本质上就是currentContent(initial)ExitTransitiontargetContentEnterTransition组合, EnterTransition 定义了目标内容应如何显示,ExitTransition 则定义了初始内容应如何消失。

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample2() {
   
    Column {
   
        var count by remember {
    mutableStateOf(0) }
        Button(onClick = {
    count++ }) {
    Text("Add") }
        AnimatedContent(
            targetState = count,
            transitionSpec = {
   
                // 从右往左切换,并伴随淡入淡出效果(initialOffsetX = width, targetOffsetX = -width)
                slideInHorizontally{
   width -> width} + fadeIn() with
                        slideOutHorizontally{
   width -> -width} + fadeOut()
            }
        ) {
    targetCount ->
            Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
        }
    }
}

运行效果:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample3() {
   
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
   
        var count by remember {
    mutableStateOf(0) }
        Button(onClick = {
    count++ }) {
    Text("Add") }
        val animationSpec = tween<IntOffset>(200)
        val animationSpec2 = tween<Float>(200)
        AnimatedContent(
            targetState = count,
            transitionSpec = {
   
                slideInVertically(animationSpec){
    height -> height} + fadeIn(animationSpec2) with
                    slideOutVertically(animationSpec) {
   height -> height} + fadeOut(animationSpec2)
            }
        ) {
    targetCount ->
            Text(text = "$targetCount", fontSize = 40.sp)
        }
    }
}

运行效果:

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample4() {
   
    Column {
   
        var count by remember {
    mutableStateOf(0) }
        Row(horizontalArrangement = Arrangement.SpaceAround) {
   
            Button(onClick = {
    count-- }) {
    Text("Minus") }
            Spacer(Modifier.size(60.dp))
            Button(onClick = {
    count++ }) {
    Text("Plus ") }
        }
        Spacer(Modifier.size(20.dp))
        AnimatedContent(
            targetState = count,
            transitionSpec = {
   
                if (targetState > initialState) {
   
                    // 如果targetState更大,则从下往上切换并伴随淡入淡出效果
                    slideInVertically {
    height -> height } + fadeIn() with
                            slideOutVertically {
    height -> -height } + fadeOut()
                } else {
   
                    // 如果targetState更小,则从上往下切换并伴随淡入淡出效果
                    slideInVertically {
    height -> -height } + fadeIn() with
                            slideOutVertically {
    height -> height } + fadeOut()
                }.using(
                    // Disable clipping since the faded slide-in/out should be displayed out of bounds.
                    SizeTransform(clip = false)
                )
            }
        ) {
    targetCount ->
            Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
        }
    }
}

运行效果:

slideIntoContainerslideOutOfContainer

除了可用于 AnimatedVisibility 的所有 EnterTransition 和 ExitTransition 函数之外,AnimatedContent 还提供了 slideIntoContainerslideOutOfContainer。这些是 slideInHorizontally/VerticallyslideOutHorizontally/Vertically 的便捷替代方案,它们可根据初始内容的大小和 AnimatedContent 内容的目标内容来计算滑动距离。(官方例子可见:slideIntoContainer)

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SlideIntoContainerSample() {
   
    val transitionSpec: AnimatedContentScope<Int>.() -> ContentTransform = {
   
        if (initialState < targetState) {
   
            slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up) + fadeIn() with
                    slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Up) + fadeOut()
        } else {
   
            slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Down) + fadeIn()  with
                    slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Down) + fadeOut()
        }.apply {
   
            // 这里可指定目标内容的 zIndex ,值越大越上层,值越小越下层
//            targetContentZIndex = when (targetState) {
   
//                NestedMenuState.Level1 -> 1f
//                NestedMenuState.Level2 -> 2f
//                NestedMenuState.Level3 -> 3f
//            }
        }.using(SizeTransform(clip = false))
    }
    Column {
   
        var count by remember {
    mutableStateOf(0) }
        Row(horizontalArrangement = Arrangement.SpaceAround) {
   
            Button(onClick = {
    count-- }) {
    Text("Minus") }
            Spacer(Modifier.size(60.dp))
            Button(onClick = {
    count++ }) {
    Text("Plus ") }
        }
        Spacer(Modifier.size(20.dp))
        AnimatedContent(
            targetState = count,
            transitionSpec = transitionSpec,
        ) {
    targetCount ->
            Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
        }
    }
}

运行效果:同上一个例子一样

SizeTransform

SizeTransform 定义了大小应如何在初始内容与目标内容之间添加动画效果。在创建动画时,您可以访问初始大小和目标大小。 SizeTransform 还可控制在动画播放期间是否应将内容裁剪为组件大小。

@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
fun SizeTransformAnimatedContentSample() {
   
    var expanded by remember {
    mutableStateOf(false) }
    Surface(
        color = MaterialTheme.colors.primary,
        onClick = {
    expanded = !expanded },
        modifier = Modifier.padding(10.dp).onSizeChanged {
     }
    ) {
   
        AnimatedContent(
            targetState = expanded,
            transitionSpec = {
   
                fadeIn(animationSpec = tween(150, 150)) with
                    fadeOut(animationSpec = tween(150)) using
                        SizeTransform {
    initialSize, targetSize ->
                            if (targetState) {
   
                                keyframes {
   
                                    // 展开时,先水平方向展开
                                    // 150ms之前:宽度从initialSize.width增大到targetSize.width,高度保持initialSize.height不变
                                    // 150ms之后:宽度保持targetSize.width不变,高度从initialSize.height开始增大到targetSize.height
                                    IntSize(targetSize.width, initialSize.height) at 150
                                    durationMillis = 300
                                }
                            } else {
   
                                keyframes {
   
                                    // 收缩时,先垂直方向收起
                                    // 150ms之前:宽度保持initialSize.width不变,高度从initialSize.height减小到targetSize.height
                                    // 150ms之后:宽度从initialSize.width减小到targetSize.width,高度保持targetSize.height不变
                                    IntSize(initialSize.width, targetSize
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值