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) } // 替换为 animateDpAsStateColumn{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 的正确写法。
- 首先我们回顾下 remember 的作用:它是防止变量被多次重复初始化的,而 animateDpAsState 天生自带这个能力(它的源码内部是包了 remember 的),所以这里我们可以去掉 remember。
- 再来回顾下 by 的作用:把左边 offset 变量委托给右边的 mutableStateOf,mutableStateOf 提供了读和写的功能,但是有一个细节需要注意了,我们对比下
mutableStateOf
和animateDpAsState
函数的定义:
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 值?
- 以下是正确的写法:
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 详解 】 一文细说。