Compose中的动画API概览及使用方法

描述

 

我们将通过本文介绍 Compose 中的一些动画 API,并探讨如何有效地使用它们。Compose 中的动画 API 是我们构想的全新 API,这些 API 中有许多是声明式的,您可以利用声明式的方式简洁地定义动画。

 

这些动画 API 支持中断,当运行中的动画被另一个动画打断时,运行中动画的值会带入到新动画中。新 API 简单易用,配置了合理的默认行为,可开箱即用,也可高度定制。同时 Android Studio 还提供了强大的工具,可以帮助您制作复杂动画。

 

 

 

Compose 动画概览

 

我们先从一个简单例子开始。下图是一个猫咪图标,当我们点击按钮时,它会在隐藏和显示这两种状态间进行切换:

 

在 Compose 中,实现这一效果非常简单。首先我们声明一个布尔类型的 State 变量——visible,在每次点击按钮时,它的值都会被切换,而它的任何变化都会触发重组,猫咪图标也会随之出现或消失:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
var visible by remember { mutableStateOf(true) } Column {        Button(onClick = { visible = !visible }) {                Text("Click")        }         if (visible) {                  CatIcon( )        }}
 

现在,如果我们想将此过程转变为动画,则只需将 if 语句替换为 AnimatedVisibility 可组合项即可。当 State 的值发生改变时,AnimatedVisibility 可组合项会以其状态运行动画:

  •  
  •  
  •  
  •  
  •  
AnimatedVisibility (visible) {         CatIcon( )}
 

还有一个 API 与 AnimatedVisibility 非常相似,那就是 AnimatedContent。AnimatedVisibility 的运行基于内容的进入和退出,而 AnimatedContent 则可为内容的变化生成过渡动画。

 

在下面的例子中,当我们点击按钮时,计数会随淡出和淡入效果而增加:

 

AnimatedContent 的 State 参数可以是任何类型,在本示例中,我们使用名为 count 的整型 State,在点击按钮时,其数值会随之增加。而每次 State 发生变化时,AnimatedContent 就会运行动画。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
Row {       var count by remember { mutableStateOf (0) }        Button(onClick = { count++ }) {               Text("Add")       }       AnimatedContent (targetState = count) { targetCount ->              Text("Count: $targetCount")       }}
 

我们可以使用 lambda 参数,基于输入的 State 切换内容。AnimatedVisibility 和 AnimatedContent 都提供了合理的默认动画样式,但我们也可对其进行自定义。对于 AnimatedVisibility,可以自定义其进入和退出的过渡动画;对于 AnimatedContent,则可以使用 transitionSpec 参数自定义进入、退出过渡动画的组合。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
AnimatedVisibility (        visible = visible,       enter = fadeIn()+ scaleIn(),       exit = fadeOut() + scaleOut()) {       // ……} AnimatedContent(       targetState = … ,       transitionSpec = {               fadeIn() + scaleIn() with fadeOut() + scaleOut()       }) { targetState ->       // ……}
 

下图中列出了一些进入和退出的过渡动画,其中包括 fadeIn、fadeOut、slideIn、slideOut 以及 scaleIn 和 scaleOut,这些过渡动画效果如下:

 

 

 

AnimatedVisibility 和 AnimatedContent 已经可以应对诸多场景,不过我们还提供了一些更为通用的 API。animate*AsState API 可用于为单个值制作动画,您只需将各种数据类型与 animate*AsState 函数组合,即可将其转换为对应的动画值。在本示例中,我们为 dp 值制作动画,所以我们使用 animateDpAsState。

  •  
  •  
  •  
val offsetX by animateDpAsState(        if (isOn) 512.dp else 0.dp)
 

 

我们开始时有提到,基于 State 的 API 支持中断。也就是说,如果播放中动画的状态发生变化,新动画将从当前的中间值和速度开始,并基于弹簧的物理效果继续播放。我们将这样的动画行为称为 AnimationSpec。

 

Spring 是默认的 AnimationSpec。Compose 还提供了其他类型的 AnimationSpec。例如,tween 是基于持续时间的 AnimationSpec,它根据动画由始至终的持续时间来定义运动效果。

 

 

我们可以通过下面的例子了解如何为 animate*AsState 指定 AnimationSpec。在这个例子中,我们指定动画的播放时长为三秒钟:

  •  
  •  
  •  
  •  
val offsetX by animateDpAsState(        if (isOn) 512.dp else 0.dp,        animationSpec = tween(durationMillis = 3000))

 

 

那么,如果需要同时为多个值制作动画,应该怎么做?您可以使用 updateTransition API,它对构建非常复杂的动画大有助益。我们来看一个简单的例子,下图是一个填充了颜色的方块,我们要为方块的大小和颜色这两个值同时制作动画:

 

首先,我们需要定义 BoxState。这是一个枚举类型,代表动画的目标,可以是 Small 或者 Large:

  •  
  •  
  •  
  •  
private enum class BoxState (        Small,        Large}
 

然后,我们为其创建一个 State 对象,改变 State 的值会触发动画:

  •  
var boxState by remember { mutableStateOf (BoxState.Small) }
 

然后我们使用 updateTransition 创建 Transition 对象。注意,最好为 Transition API 中所使用的对象附上标签,以便 Android Studio 可以更好地展示动画,这点我们稍后再介绍:

  •  
  •  
  •  
  •  
val transition = updateTransition(        targetState = boxState,        label = "Box Transition")
 

之后,我们就可以使用 animateColor 和 animateDp 等扩展函数创建动画值了。这些函数的返回值都是 State 对象,因此其使用方式与其他 State 相同:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
val color by transition.animateColor(label = "Color") { state ->        when (state) {                BoxState.Small -> Blue                 BoxState.Large -> Orange        }}val size by transition.animateDp (label = "Size") { state ->        when (state) {                BoxState.Small -> 32.dp                  BoxState.Large -> 128.dp        }}
 

将目前为止我们了解的所有内容结合,便可以实现非常复杂的动画。

 

示例中使用了 updateTransition 为多个值制作动画,例如表格的高度、位置及其内容的透明度。同时还使用了 AnimatedVisibility 自定义进入和退出过渡动画,从而实现了理想的淡入和淡出效果。

 

 

Android Studio 动画检查工具

   

现在我们已经知道了如何创建复杂的动画,接下来,我们看看 Android Studio 如何帮助我们实现精美的动画效果。Android Studio 提供了动画预览功能来帮您快速验证动画效果,它会自动检测动画的使用,您可以在 Android Studio 中直接播放动画;Android Studio 还可以图形化动画的值,以便您可以快速浏览这些值是如何随时间变化的:

 

这里要注意的是,我们在前面生成 Transition 对象时添加的标签,会在检测到的动画列表中,作为选项卡的名称展示出来。

 

如下图所示,Compose 预览上的对应图标按钮表示界面中存在可检查的动画,点击按钮即可启用动画检查:

 

该工具目前支持 AnimatedVisibility 和 updateTransition,但我们正计划添加对 AnimatedContent 和 animate*AsState 的支持。

 

如下图所示,我们可以使用动画检查窗口来播放、浏览和慢放 AnimatedVisibility:

 

此工具还可绘制动画曲线,以便您将其与设计师所设计的运动参数进行对比,这有助于确保动画值的正确编排:

 

 

使用协程完成复杂动画

 

现在,我们已经了解了基于 State 的各种动画 API,它们十分有助于我们在常见用例中为 State 变化制作动画。而如果是更为复杂的场景,比如需要为动画指定自定义行为时又该怎么做呢?

 

例如,在某些情况下需要对动画进行更多控制,您可能需要对动画或动画集进行排序;又或者,您可能希望在动画中断时执行自定义行为。

 

正如我们所知,当动画中断时,基于 State 的动画 API 会保持动画值和速度的连续性。但在某些情况下,为了强调手势或响应,您可能并不需要连续性。例如,在下图中双击点赞这一动画中,再次双击时,播放中的动画会从头播放:

 

这种情况下,您可能需要使用目标不明确的不确定动画。我们将这种动画称之为投掷行为 (Fling),投掷行为的目标仅来自起始条件及其衰减函数。

 

当我们为了应对复杂的场景,而需要协调动画的编排时,就要用到 Kotlin 的一项强大功能——协程。下面的示例中是一个基础的协程动画 API——animate。使用它创建的动画,会以 initialValue 参数和可选的 initialVelocity 参数所确定的开始条件运行至 targetValue 所指定的值;可选的 animationSpec 可用于自定义运动参数,该参数的默认值为 spring();最后,我们传入函数参数 block,animate 会在每帧动画上使用最新的动画值和速度调用此参数。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
suspend fun animate(        initialValue: Float,        targetValue: Float,         initialVelocity: Float = 0f,        animationSpec: AnimationSpec<Float> = spring(),         block: (value: Float, velocity: Float) -> Unit)

 

注意 animate 函数的 suspend 修饰符,这意味着此函数可在协程中使用,并且可以挂起协程直到动画完成。这是对动画进行排序的关键。下图展示了在协程中执行 animate 函数的过程。您会注意到,一旦调用了 animate 函数,调用动画的协程就会被挂起,直到动画结束。之后,协程将恢复并执行后续工作。

 

 

这有助于我们对操作进行排序,以及在动画后执行任务。以往,我们会将此类任务置于动画结束监听器中,而有了协程,便无需结束监听器。

 

下面是生成上图所示工作流的代码。我们首先使用 rememberCoroutineScope 在组合内部创建 coroutineScope,然后使用 launch 函数在该作用域内创建一个新的协程。在新的协程中,首先调用 animate。animate 只会在动画结束后返回,因此,动画结束后需要完成的任何任务,如更新状态或者启动另一个动画都可以放在 animate 后面。而如果需要取消动画,我们可以直接取消执行动画的协程。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
val scope = rememberCoroutineScope()        scope.launch { // 创建新的协程                animate(...)                // 更新状态、开启另一个动画,等等                subsequentWork()         }
 

如下图所示,如果用另一个 animate 函数替换 subsequentWork 函数,就可以得到两个连续运行的动画。如果查看代码,您会发现我们仅使用了两个连续的 animate 函数便可以实现连续动画。

  •  
  •  
  •  
  •  
  •  
  •  
val scope = rememberCoroutineScope()        scope.launch { // 创建新的协程                animate(...)                 animate(...)        }
 

 

 

现在我们已经了解如何构建连续动画,那么如果我们想同时运行动画的话,该怎么做?

 

我们可以将动画分别放在单独的协程中并行运行。为此,我们需要使用 CoroutineScope。CoroutineScope 定义了在其作用域内所创建的新协程的生命周期。在该作用域内,可使用协程构建器函数 launch 来创建新的协程。launch 是非阻塞函数,所以我们可以并行创建多个协程,并在其中同时运行动画。

 

 

除了高亮的 launch 函数外,下面的示例代码与之前展示的连续动画代码相同,都可以创建新的协程。如前所述,launch 是非阻塞函数,所以,新的协程可以并行创建,并且动画将在同一帧开始运行。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
val scope = rememberCoroutineScope()scope.launch {        launch { // 创建新的协程                animate(...)        }        launch { // 创建新的协程                animate(...)        }}
 

现在,我们完成了同时运行的动画。一言以蔽之,协程有助于极其灵活地协调动画。我们可以在同一个协程中轻松执行两个 animate 函数来创建连续的动画;我们还可以在不同的协程中运行动画,从而同时运行这些动画。这些都是更为复杂动画的组成部分。

 

在接下来的示例中,我们要创建双击点赞的心形动画:

 

如下图所示,这个动画包含两个阶段: 首先,我们需要在心形进入时,淡入并放大心形;进入动画完成后,启动退出动画以淡出,同时进一步放大心形。

 

为此,我们可以创建两个 CoroutineScope,一个用于进入动画,另一个用于退出动画。当作用域内的所有动画运行完成后,CoroutineScope 才会返回,因此,进入和退出动画将连续运行。在每个 CoroutineScope 中,我们使用 launch 函数创建新的协程,使淡入淡出和缩放动画可以同时运行。

 

在使用代码构建此动画时,首先要为 alpha 和 scale 创建 MutableState 对象,以便在动画过程中更新它们的值。然后需要创建两个 CoroutineScopes,以便连续运行进入动画和退出动画。在每个 CoroutineScope 中,我们将使用 launch 函数分别创建单独的协程,从而使淡入淡出和缩放动画可以同时运行。在动画运行期间,我们使用 animate 函数中的 lambda 更新 alpha 或 scale。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
var alpha by remember { mutableStateOf(0f) }var scale by remember { mutableStateOf(0f) }        scope.launch {                 coroutineScope {                        launch { // 淡入                                animate(0f, 1f) { value, _ -> alpha = value }                        }                        launch { // 放大                                animate(0f, 2f) { value, _ -> scale = value }                        }                }                caroutineScope (                        launch { // 淡出                                animate(1f, 0f) { value, _ -> alpha = value }                        }                        launch { // 放大                                 animate(2f, 4f) { value, _ -> scale = value }                        }                }        }
 

在了解协程动画的基础知识之后,接下来我们讲解一个更为复杂的用例。这是一个表示内容正在加载的动画,在等待内容加载时,有一个渐变条从上到下反复扫描。内容加载后,如果渐变条仍在扫描中,我们将等待该次扫描动作完成,然后再次从上到下,执行最后一次扫描并显示内容:

 

 

为了实现这一效果,我们首先需要创建一个 Animatable 对象,它将跟踪动画的值和速度。在使用 Animatable 对象创建新动画时,我们只需提供新的目标值,当前值和速度会默认转为新动画的开始条件。

  •  
  •  
  •  
  •  
@Composable fun LoadingOverlay(isLoading: State<Boolean>) {        val fraction = remember { Animatable(0f) }        …
 

然后在 LaunchedEffect 创建的 coroutineScope 中,我们会使用 Animatable 的两个挂起函数: 一个是 animateTo,另一个是 snapTo。AnimateTo 将从 Animatable 的当前值和速度开始,向新的目标值运行动画;snapTo 会在不使用任何动画的情况下取消任何正在运行的动画,并更新 Animatable 的值。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
var reveal = { mutableStateOf(false) }LaunchedEffect(Unit) {        while(isLoading.value) {                fraction.animateTo(1f, tween (2000))                 fraction, snapTo(Of)        }}
 

由于我们要让渐变条从上到下移动,随后返回顶部,所以需要首先以 1 为目标调用 animateTo,同时使用 2,000 毫秒的补间动画。然后通过 snapTo 让渐变条返回顶部。由于 animateTo 和 snapTo 均为挂起函数,所以我们可对其排序,并在 while 循环中重复该序列,直到加载完成。

 

由于我们只在每次扫描之前检查加载状态,所以任何对加载状态的更改只会在当前扫描完成后生效。这样一来,我们就创建了一个自定义的中断处理行为。它的功能不同于基于 State 的动画 API,内容加载完成后,我们便退出 while 循环,并在执行最后一次扫描前,更改显示状态、制作渐变条移动至底部的动画。

  •  
  •  
reveal = truefraction.animateTo(1f, tween(1000))
 

最后,当 reveal 的值变为 true 时,我们停止在此叠加层中绘制不透明的封面,以便在最后一次扫描时显示下方的内容:

  •  
  •  
  •  
  •  
  •  
  •  
if (!reveal) {        // 渐变条下的不透明覆盖        Box(Modifier.background(backgroundColor))} …

 

这样一来,我们就完成了这个动画效果。完整的代码示例如下:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
@Composable fun LoadingOverlay(isLoading: State<Boolean>) {        val fraction = remember { Animatable(0f) }         var reveal = { mutableStateOf(false) }        LaunchedEffect(Unit) {            while(isLoading.value) {                fraction.animateTo(1f, tween (2000))                fraction. snapTo(0f)            }            reveal = true            fraction.animateTo(1f, tween(1000))        }        if (!reveal) {            // 渐变条下的不透明覆盖            Box(Modifier.background(backgroundColor))        }        ……}
 

 

尾声

 

最后,让我们一同欣赏由社区开发者所构建的精彩动画:

 

 

上面这些动画只是开发者社区创造力的冰山一角。在我们重新构想并为 Compose 构建动画 API 的过程中,我们收到了很多来自社区的反馈。这些反馈帮助我们打造出直观又实用的 API,我们非常感谢大家所有的反馈,欢迎继续提出。  

原文标题:使用 Jetpack Compose 实现精美动画

文章出处:【微信公众号:谷歌开发者】欢迎添加关注!文章转载请注明出处。

  审核编辑:汤梓红
 
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分