手把手教你写 Compose 动画

您所在的位置:网站首页 手机过渡动画改为0好还是05好 手把手教你写 Compose 动画

手把手教你写 Compose 动画

2024-06-22 01:19| 来源: 网络整理| 查看: 265

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 updateTransition( targetState: T, label: String? = null ): Transition

它有两个参数:

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,但实际上没有任何动画可以调节。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3