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

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 的图表,以便你有最直观的感受。
📓 Transition
Transition 是 Compose 中实现过渡动画的关键 API 。所谓过渡动画,即当依赖的某个状态发生改变时连锁发生的一系列动画效果。
前面我们所提到的 animate*AsState 与 Animatable 都是针对一个属性(比如 offset 偏移)进行变换的,而 Transition 允许开发者将多个属性数值绑定到一个状态,当这个状态发生改变时,多个属性同时进行变换。
还是那句话:探索新技术的最佳方式是尝试它们,我们先构建一个简单场景:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column (
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.cr7),
contentDescription = null,
modifier = Modifier
.size(90.dp)
.clip(shape = CircleShape)
.border(color = Color.Red, shape = CircleShape, width = 3.dp)
)
Button(
onClick = {}
) {
Text(text = "切换")
}
}
}
}
}
这段代码极其简单:一个 Image,一个 Button,效果如下:
现在我们假设一个需求场景:
- 图片大小 size 需要变化:小图片(90dp)、大图片(130dp)
- 图片边框颜色 color 需要变化:绿色、红色
- 对应关系:小图片绿色边框,大图片红色边框
如果要实现这个需求,你会怎么做?目前我们掌握的动画仅有 animate*AsState() 和 Animatable.animateTo(),不如我们先用这两个动画 API 试试效果?
📚 animate*AsState()
如果我们用 animate*AsState() 来实现这个需求,代码可以这么写:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var bigPic by remember { mutableStateOf(false) }
val size by animateDpAsState(if (bigPic) 130.dp else 90.dp, label = "")
val borderColor by animateColorAsState(if (bigPic) Color.Red else Color.Green, label = "")
Column (
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.cr7),
contentDescription = null,
modifier = Modifier
.size(size)
.clip(shape = CircleShape)
.border(color = borderColor, shape = CircleShape, width = 3.dp)
)
Button(
onClick = { bigPic = !bigPic}
) {
Text(text = "切换")
}
}
}
}
}
你只要认真看过【 animate*AsState 用法 】这篇文章,看这段代码肯定 so easy。
我们需要定义两个 animateDpAsState,分别控制图片大小和文字颜色,效果如下:
📚 Animatable.animateTo
现在我们再来用 Animatable.animateTo 实现这个需求:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var bigPic by remember { mutableStateOf(false) }
// Size Animatable
val size = remember(bigPic) { if (bigPic) 130.dp else 90.dp }
val sizeAnim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(bigPic) {
sizeAnim.animateTo(size)
}
// Color Animatable
val borderColor = remember(bigPic) { if (bigPic) Color.Red else Color.Green}
val borderColorAnim = remember { Animatable(borderColor) }
LaunchedEffect(bigPic) {
borderColorAnim.animateTo(borderColor)
}
Column (
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(100.dp))
Image(
painter = painterResource(R.drawable.cr7),
contentDescription = null,
modifier = Modifier
.size(sizeAnim.value)
.clip(shape = CircleShape)
.border(color = borderColorAnim.value, shape = CircleShape, width = 3.dp)
)
Button(
onClick = { bigPic = !bigPic}
) {
Text(text = "切换")
}
}
}
}
}
你只要认真看过【 Animatable 用法 】这篇文章,看这段代码肯定也是 so easy。
我们需要定义两个 Animatable,并且需要启动两个协程,分别控制图片大小和文字颜色,效果如下:
📚 updateTransition
重点来了,现在我们开始讲解如何用 Transition 实现这个动画效果。
- 首先我们需要一个状态,状态可以是任何数据类型。我们通常会自定义一个枚举类型:
private enum class ImageState {
Small, Large
}
- 现在我们再创建一个处理状态的变量:
var imageState by remember { mutableStateOf(ImageState.Small) }
- 创建 Transition 对象
Compose 中是通过 updateTransition() 函数来创建 Transition 对象,我们来看下 updateTransition() 函数:
@Composable
fun <T> updateTransition(
targetState: T,
label: String? = null
): Transition<T>
它有两个参数:
- targetState:状态变量,当它被更改时,动画会进行。
- label:动画的标签。
这里的状态就是我们之前定义的:imageState,所以我们可以像下面这样写:
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
updateTransition() 会返回一个 Transition 对象。
现在我们可以使用某个 animate* 扩展函数
来定义此过渡效果中的子动画。为每个状态指定目标值。这些 animate* 函数会返回一个动画值,在动画播放过程中,当使用 updateTransition 更新过渡状态时,该值将逐帧更新。
- 定制边框颜色过渡
val borderColor by transition.animateColor(label = "ImageState Color Transition") {
when (it) {
ImageState.Small -> Color.Green
ImageState.Large -> Color.Magenta
}
}
- 定制图片尺寸过渡
val size by transition.animateDp(label = "ImageState Size Transition") {
when (it) {
ImageState.Small -> 90.dp
ImageState.Large -> 130.dp
}
}
我们为每个属性状态(borderColor、size)声明了其在不同状态(ImageState.Small、ImageState.Large)时所对应的值,当过度动画所依赖状态(imageState)发生改变时,其中每个属性状态都会得到相应的更新。
- 应用到组件上(完整代码)
接下来,我们只需将创建的属性状态应用到我们的组件中即可:
private enum class ImageState {
Small, Large
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var imageState by remember { mutableStateOf(ImageState.Small) }
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
val borderColor by transition.animateColor(label = "ImageState Color Transition") {
when (it) {
ImageState.Small -> Color.Green
ImageState.Large -> Color.Magenta
}
}
val size by transition.animateDp(label = "ImageState Size Transition") {
when (it) {
ImageState.Small -> 90.dp
ImageState.Large -> 130.dp
}
}
Column (
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.cr7),
contentDescription = null,
modifier = Modifier
.size(size)
.clip(shape = CircleShape)
.border(color = borderColor, shape = CircleShape, width = 3.dp)
)
Button(
onClick = {
imageState = if (imageState == ImageState.Small) {
ImageState.Large
} else {
ImageState.Small
}
}
) {
Text(text = "切换")
}
}
}
}
}
效果如下:
📓 灵魂思考
对于这么一个简单的需求,我们同时用 animate*AsState()
、Animatable
和 Transition
都可以实现。
那么我们就该思考一个问题了:
- 我们可以把
animate*AsState()
可以理解为Animatable
的一种更简便直接的用法,Compose 创造出这个 API 的目的可以理解; - 但是
Animatable
不是已经可以完美的实现了我们的需求了吗?为什么还要造一个Transition
出来?
现在我们来解答这个疑惑:
- 首先我们回顾一下
Animatable
和Transition
两种动画原理的核心思想。
Animatable
Animatable 是面向值的,在多个动画、多个状态的情况下存在不便于管理的问题。
var bigPic by remember { mutableStateOf(false) }
// Size Animatable
val size = remember(bigPic) { if (bigPic) 130.dp else 90.dp }
val sizeAnim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(bigPic) {
sizeAnim.animateTo(size)
}
// Color Animatable
val borderColor = remember(bigPic) { if (bigPic) Color.Red else Color.Green}
val borderColorAnim = remember { Animatable(borderColor) }
LaunchedEffect(bigPic) {
borderColorAnim.animateTo(borderColor)
}
针对 size 和 borderColor,我们要创建两个 Animatable,而且还要启动两个协程,如果我们还有多个其他动画,就要像下面这么写:
var bigPic by remember { mutableStateOf(false) }
// Size Animatable
val size = remember(bigPic) { if (bigPic) 130.dp else 90.dp }
val sizeAnim = remember { Animatable(size, Dp.VectorConverter) }
LaunchedEffect(bigPic) {
sizeAnim.animateTo(size)
}
// Color Animatable
val borderColor = remember(bigPic) { if (bigPic) Color.Red else Color.Green}
val borderColorAnim = remember { Animatable(borderColor) }
LaunchedEffect(bigPic) {
borderColorAnim.animateTo(borderColor)
}
// Background Animatable
val background = remember(bigPic) { ... }
val backgroundAnim = remember { Animatable(background) }
LaunchedEffect(bigPic) {
backgroundAnim.animateTo(background)
}
// Alpha Animatable
val alpha = remember(bigPic) { ... }
val alphaAnim = remember { Animatable(alpha) }
LaunchedEffect(bigPic) {
alphaAnim.animateTo(alpha)
}
// .....
你就要不停的启动协程,然后写一堆结构差不多的代码,这还没有算上 bigPic 状态,如果你新增了其他类似 bigPic 的状态,还需要添加更多的状态片段逻辑…
Transition
Transition 是面向状态的,多个动画可以共用一个状态,能够做到统一的管理。
var imageState by remember { mutableStateOf(ImageState.Small) }
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
val borderColor by transition.animateColor(label = "ImageState Color Transition") {
when (it) {
ImageState.Small -> Color.Green
ImageState.Large -> Color.Magenta
}
}
val size by transition.animateDp(label = "ImageState Size Transition") {
when (it) {
ImageState.Small -> 90.dp
ImageState.Large -> 130.dp
}
}
updateTransition 只会创建一次协程,而且只需要根据一种状态的变化,就可以控制不同的动画效果。
如果我们还有多个其他动画,就可以这么写:
var imageState by remember { mutableStateOf(ImageState.Small) }
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
val borderColor by transition.animateColor(label = "ImageState Color Transition") {
when (it) {
ImageState.Small -> Color.Green
ImageState.Large -> Color.Magenta
}
}
val size by transition.animateDp(label = "ImageState Size Transition") {
when (it) {
ImageState.Small -> 90.dp
ImageState.Large -> 130.dp
}
}
val background by transition.animateColor(label = "ImageState Color Transition") {
when (it) {
ImageState.Small -> Color.Yellow
ImageState.Large -> Color.Magenta
}
}
val alpha by transition.animateFloat(label = "ImageState Alpha Transition") {
when (it) {
ImageState.Small -> 0.3f
ImageState.Large -> 0.5f
}
}
结构清晰明了,非常方便。
Transition
除了代码结构和逻辑清晰的优势以外,它还有个更牛逼的功能:支持 Compose 动画预览!
什么是动画预览呢?我们代码写到现在了,不知道你有没有注意到所有的动画 API 都加上了一个 label
标签参数:
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
val borderColor by transition.animateColor(label = "ImageState Color Transition") {
when (it) {
ImageState.Small -> Color.Green
ImageState.Large -> Color.Magenta
}
}
这个 label 有什么用呢?一会你就知道了。
现在我们看看怎么打开这个 Compose 动画预览功能:
- 首先我们把代码调整拆到一个自定义 Composable 函数中,如下:
private enum class ImageState {
Small, Large
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TransitionPreview()
}
}
}
@Composable
fun TransitionPreview() {
var imageState by remember { mutableStateOf(ImageState.Small) }
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
val borderColor by transition.animateColor(label = "ImageState Color Transition") {
when (it) {
ImageState.Small -> Color.Green
ImageState.Large -> Color.Magenta
}
}
val size by transition.animateDp(label = "ImageState Size Transition") {
when (it) {
ImageState.Small -> 90.dp
ImageState.Large -> 130.dp
}
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(100.dp))
Image(
painter = painterResource(R.drawable.cr7),
contentDescription = null,
modifier = Modifier
.size(size)
.clip(shape = CircleShape)
.border(color = borderColor, shape = CircleShape, width = 3.dp)
)
Button(
onClick = {
imageState = if (imageState == ImageState.Small) {
ImageState.Large
} else {
ImageState.Small
}
}
) {
Text(text = "切换")
}
}
}
- 现在我们给 TransitionPreview() 函数添加 @Preview 注解:
@Preview
@Composable
fun TransitionPreview() {
}
加上这个注解,我们就可以直接在 Android Studio 界面右侧预览这个 @Composable 可组合项的效果,而不需要运行到模拟机或者真机。
现在还只是组件的预览界面,我们可以点击 Start Animation Preview
按钮进入动画预览界面:
进入之后会是下面这个样子:
我们现在来分析一下这个动画预览界面,注意看图:
红色框框显示的是什么?不就是对应着我们创建 Transition 时候填的 label 么?
val transition = updateTransition(targetState = imageState, label = "ImageState Transition")
我们现在点击箭头展开它:
现在你知道为什么要加 label
了吧?动画预览界面可操作性很强,你可以拖动进度条到动画的任意位置,还能互换动画的初始状态和目标状态,设置动画的倍速等,这个自行探索吧。
我估计这个时候,你可能就会想一个问题了,难道 animate*AsState 不支持,它不是也加了 label 了吗?我们试一下:
animate*AsState 虽然支持添加 label,但实际上没有任何动画可以调节。




更多推荐








所有评论(0)