手把手教你写 Compose 动画 -- 状态转移型动画 API:animate*AsState()

发布时间:2024年01月18日

Jetpack Compose 提供了一系列功能强大且可扩展的 API,可用于在应用界面中轻松实现各种动画效果。这一系列文章会逐个介绍所有的动画 API,通过最直观的 Demo 示例,手把手教你怎么写动画以及带你了解动画背后的原理。


📑 手把手教你写 Compose 动画 - - 状态转移型动画 API:animate*AsState()

📑 手把手教你写 Compose 动画 - - 流程定制型动画 API:Animatable()

📑 手把手教你写 Compose 动画 - - 讲的不能再细的 AnimationSpec 动画规范

📑 手把手教你写 Compose 动画 - - 过渡动画 API:Transition

📑 手把手教你写 Compose 动画 - - 显示与消失 API:AnimatedVisibility

📑 手把手教你写 Compose 动画 - - 简单页面切换动画 API:Crossfade

📑 手把手教你写 Compose 动画 - - 更强大的多组件切换动画 API:AnimatedContent

📑 手把手教你写 Compose 动画 - - 组件大小变化 API:animateContentSize



📓 动画图表


在每一篇文章开头,我都会放一张 Compose 动画 API 的图表,以便你有最直观的感受。

在这里插入图片描述


📓 animate*AsState


animate*AsState 函数应该算是 Compose 中最简单的动画 API,用于为单个值添加动画效果。只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。

一直以来我都认为:探索新技术的最佳方式就是尝试它们,所以我们先构建一个简单场景:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Column{
                Image(
                    painter = painterResource(R.drawable.bicycle),
                    contentDescription = null,
                    modifier = Modifier
                        .height(90.dp)
                        .absoluteOffset(x = 0.dp)
                )

                Button(onClick = {},
                    modifier = Modifier
                        .fillMaxWidth()
                        .wrapContentSize(align = Alignment.Center)
                ) {
                    Text(text = "Ride")
                }
            }
        }
    }
}

这是一个极其简单的场景:一个图片,一个 Button,初始效果如下:

在这里插入图片描述

现在我们让小刺猬动起来(从左到右),代码可以这样写:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var offset by remember { mutableStateOf(0.dp) }  // 定义偏移变量

            Column{
                Image(
                    painter = painterResource(R.drawable.bicycle),
                    contentDescription = null,
                    modifier = Modifier
                        .height(90.dp)
                        .absoluteOffset(x = offset)  // 获取偏移值
                )

                Button(
                    onClick = { offset = 360.dp },  // 修改偏移值
                    modifier = Modifier
                        .fillMaxWidth()
                        .wrapContentSize(align = Alignment.Center)
                ) {
                    Text(text = "Ride")
                }
            }
        }
    }
}

如注释说明,我只修改了三个地方(理解起来没问题吧?),现在来看下效果:

在这里插入图片描述

小刺猬动起来了,但是这种效果给人的感觉就很生硬,完全称不上是动画,而是“瞬间移动”。

现在我们可以开始尝试用 animate*AsState 改善动画效果,写法很简单:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // var offset by remember { mutableStateOf(0.dp) }  // 定义偏移变量
            var offset by remember { animateDpAsState(0.dp) }   // 替换为 animateDpAsState

            Column{
                Image(
                    painter = painterResource(R.drawable.bicycle),
                    contentDescription = null,
                    modifier = Modifier
                        .height(90.dp)
                        .absoluteOffset(x = offset)  // 获取偏移值
                )

                Button(
                    onClick = { offset = 360.dp },  // 修改偏移值
                    modifier = Modifier
                        .fillMaxWidth()
                        .wrapContentSize(align = Alignment.Center)
                ) {
                    Text(text = "Ride")
                }
            }
        }
    }
}

如你所见,我仅仅用 animateDpAsState 替换 mutableStateOf 后,就可以对数值的大小实现渐变的调整,但很遗憾,这些直接替换是有红线报错的。

在这里插入图片描述

接下来我们一起修复这个报错,最终推导出 animateDpAsState 的正确写法。

  1. 首先我们回顾下 remember 的作用:它是防止变量被多次重复初始化的,而 animateDpAsState 天生自带这个能力(它的源码内部是包了 remember 的),所以这里我们可以去掉 remember。

在这里插入图片描述

  1. 再来回顾下 by 的作用:把左边 offset 变量委托给右边的 mutableStateOf,mutableStateOf 提供了读和写的功能,但是有一个细节需要注意了,我们对比下 mutableStateOfanimateDpAsState 函数的定义:
fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)
@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {

? mutableStateOf 返回的是一个 MutableState 对象

? animateDpAsState 返回的是一个 State 对象

在 Compose 中,State 对象只提供读的功能,你是没办法写的!但例子中 var 就代表 offset 是可写的,这就冲突了。Android Studio 的报错提示其实已经说明了:

在这里插入图片描述

所以现在我们把 var 改成 val

在这里插入图片描述

这里特殊说明一下:

? 蓝色地方有波浪线是因为随着官方 API 的更新,不带 label 参数的 animateDpAsState 函数被弃用了。

@Deprecated(
    "animate*AsState APIs now have a new label parameter added.",
    level = DeprecationLevel.HIDDEN
)
@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    finishedListener: ((Dp) -> Unit)? = null
)

官方建议我们使用带 label 参数的 animateDpAsState,所以如果你就不想加,也不会影响程序运行(至于为什么官方这么强烈建议加上这个 label 标签,会在别的动画文章里面说明)。

val offset by animateDpAsState(0.dp, label = "")

? 红色地方报错的原因是显而易见的,因为 offset 不可以手动写,那该怎么修改 offset 值?

  1. 以下是正确的写法:
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        var bicycleStart by mutableStateOf(false)

        setContent {
            val offset by animateDpAsState(if (bicycleStart) 360.dp else 0.dp, label = "")

            Column{
                Image(
                    painter = painterResource(R.drawable.bicycle),
                    contentDescription = null,
                    modifier = Modifier
                        .height(90.dp)
                        .absoluteOffset(x = offset)
                )

                Button(
                    onClick = { bicycleStart = !bicycleStart },
                    modifier = Modifier
                        .fillMaxWidth()
                        .wrapContentSize(align = Alignment.Center)
                ) {
                    Text(text = "Ride")
                }
            }
        }
    }
}

既然我没法在别的地方手动修改值,让就通过改变另外一个 bicycleStart 状态,引发重组,从而改变 animateDpAsState 内部的值。

到这里程序就没有任何错误了,这也是 animateDpAsState 的正确写法

现在,运行下看看效果:(对比 mutableStateOf 和 animateDpAsState)

在这里插入图片描述

效果是很明显的,起码我们能够看出来车是开起来了,而不是瞬间移动。


除了 Dp,Compose 为 Float、Color、Size、Offset、Rect、Int、IntOffset 和 IntSize 都提供了开箱即用的 animate*AsState 函数,用法差不多,不再过多举例了。

在这里插入图片描述

至此,Animate*AsState() 的基本用法了解完了,就这么简单,但是我们还得细看下这个函数的定义:

@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
)

它还有一个核心参数:animationSpec,它是一个 AnimationSpec 类型的,你可以通过可选的 AnimationSpec 参数来自定义动画规范(也就是可以实现不同类型的动画效果)。

AnimationSpec 的内容着实不少,作为单独的知识点放在 【 AnimationSpect 详解 】 一文细说。

文章来源:https://blog.csdn.net/pepsimaxin/article/details/135665460
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。