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 函数应该算是 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 的正确写法。
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 详解 】 一文细说。