前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >来聊聊 Jetpack Compose 动画,一篇搞定(上篇)

来聊聊 Jetpack Compose 动画,一篇搞定(上篇)

作者头像
玖柒的小窝
发布2021-12-06 22:43:27
9120
发布2021-12-06 22:43:27
举报
文章被收录于专栏:各类技术文章~各类技术文章~

引言

  • Jetpack Compose 作为 Google 近期主推的 Android 开发 UI 框架,得益于其声明式编程的思想以及协程的加持,让 Compose 在开发过程中非常的舒适。
  • 前段时间对 Compose 进行了较系统的学习,特地抽出其中动画相关内容,结合官方文档和自身实践经验和大家一起交流。

文章目的:

  • 本系列文章分为上下两篇,也是希望在读完文章之后能覆盖 Compose 动画中的 80% 的开发 API 需要以及容易遇到的问题;
  • 上篇想跟大家说说 Compose 动画的优点,并着重介绍官方封装好的高级别 API 设计及使用;
  • 下篇会聊聊 Compose 动画偏底层的 API 及简单说说动画的触发流程,同时聊聊多个动画的监听及并发执行写法。

知识储备:

  • 我希望你在阅读本文前对 Kotlin 协程Jetpack Compose 基础都有一定的了解~

一、我为什么喜欢用 Compose 写动画?

1.1 声明式编程

  • 得益于声明式编程的优势,在大多数的动画类型的选择上,你不需要像原来那样在帧动画、补间动画和属性动画中选择太久;也不需要纠结用 XML 动画还是使用 Animation 类;
  • Compose 的动画都用代码的方式写在 @Compose 方法里面,通常只需要确认需要修改的属性(大小、位置、透明度)等,再用合适的动画类型去修改这些属性就可以了。
  • 我们举个例子:比如下面这个非常简单的动画,点击时会切换图片的尺寸和透明度。
simpleAnimation.gif
simpleAnimation.gif
  • 如果是命令式编程的写法,我们需要去思考需要使用 ScaleAnimationAlphaAnimation ,再用 AnimationSet 来将动画合并;并且还要同时写出 toBigAnimateSettoSmallAnimateSet ,最后“命令”指定的视图去执行这个动画。
  • 而在 Compose 声明式编程的世界里,你只需要在原来的代码基础上,对指定的属性做动画就可以了,让我们来感受一下。
代码语言:javascript
复制
enum class HeartState { SMALL, BIG } // 定义红心的两种类型
var action by remember { mutableStateOf(HeartState.SMALL) } // 指定红心状态(也是动画的触发器)
val animationTransition = updateTransition(targetState = action, label = "") // 创建动画类
// 定义不同状态下的尺寸
val size by animationTransition.animateDp(label = "改变大小") { state ->
    if (state == HeartState.SMALL) 40.dp else 60.dp
}
// 定义不同状态下的透明度
val alpha by animationTransition.animateFloat(label = "改变透明度") { state ->
    if (state == HeartState.SMALL) 0.5f else 1f
}
// 绘制红心
Image(
    modifier = Modifier.size(size = size).alpha(alpha = alpha),
    painter = painterResource(id = R.drawable.heart),
    contentDescription = "heart"
)
// 点击触发状态切换
Text(
    text = "切换",
    modifier = Modifier
        .padding(top = 10.dp, bottom = 50.dp)
        .clickable { action = if (action == HeartState.SMALL) HeartState.BIG else HeartState.SMALL }
)
复制代码

1.2 封装好的高级API

  • Compose 提供了部分高级别、开箱即用的动画API,让我们可以通过少量的代码就可以实现想要的动画。比如一个动画的出现和消失,我们可以通过 Compose 提供的 AnimatedVisibility 来实现。甚至再加一两行代码,控制出场退场的方式。
代码语言:javascript
复制
var isVisible by remember { mutableStateOf(true) }
AnimatedVisibility(
    visible = isVisible,
    enter = slideInVertically() + fadeIn(), // 水平方向划入 + 渐变展示
    exit = slideOutHorizontally() + fadeOut() // 垂直方向划出 + 渐变隐藏
) {
    Image(
        modifier = Modifier.size(size = 50.dp),
        painter = painterResource(id = R.drawable.heart),
        contentDescription = "heart"
    )
}
复制代码
  • 那么它就会有这样的效果(很方便吧!)
visibilty_animation.gif
visibilty_animation.gif

1.3 工具的支持

  • IDE 对 Compose 动画进行了工具上的支持。通过 Arctic Fox 版本的 Android Studio,我们可以对动画进行逐帧的检查和调试,播放视图从不同状态间切换的动画,并且能非常直观的观察到视图的具体数据,做出精益求精的效果。
  • ps:目前版本的 AS 对动画并未完全支持,不过相信后续会继续完善。
    • ✅ AnimateVisibility / updateTransition
    • ❎ AnimatedContent / animate*AsState
IDE_Compose.png
IDE_Compose.png

二、如何选择合适的动画实现

除了上面提到的 AnimatedVisibility ,Compose还提供了很多封装好的动画,这些 API 经过专门设计,符合 Material Design 运动的最佳做法。接下来,我将会一一介绍。

  • 具体可以参考下面的这张图,对不同的场景使用不同的 API;
  • 本篇文章会带着大家一起学习一下高级别 API 以及相关的内容。
Compose 动画.png
Compose 动画.png

三、基于内容变化的动画

3.1 出现和消失 → 改变内容

  • 上面的例子有提到,我们可以直接使用 Compose 提供的 AnimatedVisibility 动画,现在我们来看下具体使用:传送门
代码语言:javascript
复制
@Composable
fun AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)
复制代码
  • 参数解析:
    • visible :动画的触发器。当数值从 falsetrue 时,会执行 enter 动画;相反,会执行 exit 动画;
    • enter :对象的进入动画,传入 EnterTransition 的子类。Compose 已经封装好高度易用的动画类,如 fadeslidescaleexpand 等;
    • exit :对象的退出动画,传入 ExitTransition 的子类。上述的进入动画均有一一对应的退出动画;
    • content :需要执行动画的内容。值得注意的是,当前 content 是定义在 AnimatedVisibilityScope 中的,其中提供了 transition 对象可直接使用,可以理解成时刻同步动画状态的对象,通过 transition 对象,我们可以高度定制化地自定义动画过程中的其他动画。(关于Transition类后面会详细介绍)
    • 使用 AnimatedVisibilityScopetransition 来添加自定义动画效果:

例子:在红心出现和消失的同时,我们需要同时改变红心的颜色

代码语言:javascript
复制
var isVisible by remember { mutableStateOf(true) }
AnimatedVisibility(
    visible = isVisible,
    enter = slideInVertically() + fadeIn(),
    exit = slideOutHorizontally() + fadeOut()
) {
  // 此处 transition 是 AnimatedVisibilityScope 中的成员
  val customColor by transition.animateColor(label = "颜色变化") { state ->
    if (state == EnterExitState.Visible) Color.Blue else Color.Red
  }
	Icon(
		modifier = Modifier.size(size = 50.dp),
		painter = painterResource(id = R.drawable.heart),
		tint = customColor,
		contentDescription = "heart"
	)
}
复制代码
  • 一些补充:
    1. 如果需要自定义 AnimatedVisibility子项的进出动画,可以使用 Modifier.animateEnterExit 来重新定制动画;
    2. 出现和消失动画对应的是 Native 中的 VisibleGone 状态,在视图消失的时候会带来布局容器的改变;

3.2 淡入和淡出 → 切换内容

  • 我们可以优先使用 AnimatedContent 动画,我们来看下具体的使用:传送门
代码语言:javascript
复制
@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(
    targetState: S,
    modifier: Modifier = Modifier,
    transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {
        fadeIn(animationSpec = tween(220, delayMillis = 90)) with fadeOut(animationSpec = tween(90))
    },
    contentAlignment: Alignment = Alignment.TopStart,
    content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
)
复制代码
  • 参数解析:
    • <S> 可以看出这是一个可以适配不同内容类型的泛型方法,可以使用 S 来定义其类型;
    • targetState :动画的触发器,传入下个阶段的状态。比如内容从 “Hello” 切换到 “world”,“world” 就是此时传入的 targetState
    • transitionSpec :执行动画的规范。当前参数所需要传入的是能返回 ContentTransform 对象的方法;
      1. ContentTransform 的生成一般是调用 with infix方法生成,如下:

      // ContentTransform 的作用是会记录视图的 进入/退出 动画 infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit) 复制代码

      1. 从签名上看到的 AnimatedContentScope<S>.() -> ContentTransform ,指的是在当前方法内,使用者可以根据 AnimatedContentScope 内的对象返回自己需要的动画规范;
    • contentAlignment :内容的对齐方式。默认会从向着布局的左上方进入和退出;
  • 关于 AnimatedContentScope
    • 对于内容切换的动画,在其 Scope 内部,除了会提供监听动画状态的 transition 对象,还会给调用方提供 initialState (变化前的状态) 和 targetState (变化后的状态) ,供我们使用;
  • 接下来我们会简单写一个数字切换的动画:
number_animation.gif
number_animation.gif
代码语言:javascript
复制
var count by remember { mutableStateOf(0) } // 初始值为 0
AnimatedContent(
    targetState = count,
    transitionSpec = {
        if (targetState > initialState) {
            // 数字变大时,进入的数字从下往上变深划入,退出的数字从下往上变浅划出
            slideInVertically({ height -> height }) + fadeIn() with slideOutVertically({ height -> -height }) + fadeOut()
        } else {
            // 数字变小时,进入的数字从上往下变深划入,退出的数字从上往下变浅划出
            slideInVertically({ height -> -height }) + fadeIn() with slideOutVertically({ height -> height }) + fadeOut()
        }
    }
) { targetCount ->
    Text(text = "$targetCount")
}
// 启动协程切换数字,达到每 1 秒自动切换的效果
LaunchedEffect(Unit) {
    while (true) {
        if (count == 0) count++ else count--
        delay(1000)
    }
}
复制代码
  • 一些补充:
    1. 你仍可以使用其他高度封装的API来实现内容的切换动画,如 animateContentSize (动画效果实现视图的尺寸变化) 和 Crossfade (淡入淡出效果实现布局切换);
    2. 你还可以结合 usingSizeTransform 来定义切换过程中的尺寸变化,此处不作详述。
代码语言:javascript
复制
infix fun ContentTransform.using(sizeTransform: SizeTransform?)
复制代码

四、基于效果状态的动画

4.1 视图单个属性的变化

  • animate*AsState 是一个非常简单的 API,只需要提供最终值,API 就会从当前值开始播放动画;
  • Compose 对 FloatColorDpSizeOffsetRectIntIntOffsetIntSize 等基本类型都提供了 animate*AsState 方法,我们举 animateFloatAsState 为例
代码语言:javascript
复制
@Composable
fun animateFloatAsState(
    targetValue: Float,
    animationSpec: AnimationSpec<Float> = defaultAnimation,
    visibilityThreshold: Float = 0.01f,
    finishedListener: ((Float) -> Unit)? = null
): State<Float>
复制代码
  • 参数解析:
    • targetValue :动画的触发器。当这个值发生变化时,就会触发动画的执行;
    • animationSpec :执行动画的规范。后续会细讲。
    • visibilityThreshold :判断是否已经靠近目标数值的阈值。
    • finishedListener :动画结束监听器。
  • 从参数上都是非常容易理解的,篇幅原因不作例子介绍。

4.2 视图多个属性的变化

  • 对于一个需要同时修改多个属性的视图,我们建议采用 updateTransition
  • 这里的思路是,将视图划分为不同的状态,然后通过状态的变化计算出不同状态下的属性值。
代码语言:javascript
复制
@Composable
fun <T> updateTransition(targetState: T, label: String? = null): Transition<T>
复制代码
  • 同样的, targetState 是动画的触发器,其类型可以是我们自定义的不同状态;
  • 可以看到,这个方法返回的是 Transition<T> 对象,而此类型也提供了返回不同类型的 animate* 方法,和上述的 animate*AsState 类似。
  • 代码例子可见本文开头。

4.3 对于自定义类型的属性变化

  • 无论是 animate*AsState 还是 Transition.animate* ,我们都会遇到变化的属性为自定义类型(非基本类型) 的情况。Compose 提供了便捷的 API 供我们自定义变化规则。而他就是 TwoWayConverter
  • 先来看下他会在哪用到
代码语言:javascript
复制
@Composable
fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    // ...
): State<T>

@Composable
inline fun <S, T, V : AnimationVector> Transition<S>.animateValue(
    typeConverter: TwoWayConverter<T, V>,
    // ...
): State<T>
复制代码
  • convert 的含义是“转换”,而 TwoWayConverter 的作用是定义一个 自定义类型 转成 N个浮点值 来执行动画,再将动画返回的 N个浮点值 转成 自定义类型 的过程。
  • 我们看下 animateRectAsState 的源码,就可以很容易理解上面这句话。
代码语言:javascript
复制
// 1.可以看到 animate*AsState 内部执行的都是 animateValueAsState
@Composable
fun animateRectAsState( /* ... */): State<Rect> {
    return animateValueAsState(
        targetValue, Rect.VectorConverter, animationSpec, finishedListener = finishedListener
    )
}
// 2.对于当前方法,传入的 TwoWayConverter 是 Rect.VectorConverter
private val RectToVector: TwoWayConverter<Rect, AnimationVector4D> =
TwoWayConverter(
    convertToVector = { AnimationVector4D(it.left, it.top, it.right, it.bottom) },
    convertFromVector = { Rect(it.v1, it.v2, it.v3, it.v4) }
)
// 3.TwoWayConverter<T, V>的类型接收两个参数,T指的是原来的类型,V指的是需要中转的类型;
// AnimationVector4D 指的是使用持有 4 个浮点值的 Vector 进行中转;
// 整个过程就是 Rect -> AnimationVector4D -> Rect,非常容易理解。
复制代码

4.4 AnimationSpec

  • AnimationSpec 指的是动画规范,定义了动画以怎样的规则运行。官方定义了以下几种常用的 API,我们可以简单看一下。传送门

API

含义

属性

spring

弹窗动画

dampingRatio:定义弹簧的弹性,可选参数如Spring.DampingRatioHighBouncy;stiffness:定义弹簧向结束值移动的速度,可选参数如 Spring.StiffnessMedium

tween

定时动画

durationMillis:定义动画的持续时间;delayMillis:定义动画开始的延迟时间;easing:定义起始值和结束值之间的动画效果,如LinearOutSlowInEasing

keyframe

关键帧动画

同 tween

repeatable

重复动画

iterations:迭代次数;animation:需要重复执行的动画;repeatMode:重复的模式,如从头开始 (RepeatMode.Restart) 还是从结尾开始 (RepeatMode.Reverse)

五、对相同的动画进行封装的最佳实践

在一些相同的场景下,对于不同的视图执行的对象是一样的,这时候我们就应该对相同的部分进行抽离。当然,这一切都是有办法的,并不是直接抽函数那么简单。

  • 我们将用文章开头的例子进行实践。
  1. 我们可以对动画需要修改的属性进行封装。比如红心的尺寸(size)和透明度(alpha)
代码语言:javascript
复制
// 注意这里传入的是 State 对象,这样才能保证视图能够在重组过程中被持续刷新
private class AnimateTransitionData(size: State<Dp>, alpha: State<Float>) {
    val size by size // kotlin 的语法糖,同名委托表示取值
    val alpha by alpha
}
复制代码
  1. 创建返回上述对象的方法,将动画逻辑进行封装
代码语言:javascript
复制
@Composable
private fun updateTransitionData(targetState: HeartState): AnimateTransitionData {
    val transition = updateTransition(targetState = targetState, label = "")
    // 定义不同状态下的尺寸
    val size = transition.animateDp(label = "改变大小") { state ->
        if (state == HeartState.SMALL) 40.dp else 60.dp
    }
    // 定义不同状态下的透明度
    val alpha = transition.animateFloat(label = "改变透明度") { state ->
        if (state == HeartState.SMALL) 0.5f else 1f
    }
// 此处将 transition 对象作为 remember 的入参,表明每当 transitio 更新,都会触发 AnimateTransitionData 的更新和返回
    return remember(transition) { AnimateTransitionData(size, alpha) }
}
复制代码
  1. 将方法执行套入原来的结构中
代码语言:javascript
复制
var action by remember { mutableStateOf(HeartState.SMALL) }
val data = updateTransitionData(action)
// 绘制红心
Image(
    modifier = Modifier
        .size(size = data.size)
        .alpha(alpha = data.alpha),
    painter = painterResource(id = R.drawable.heart),
    contentDescription = "heart"
)
复制代码

六、小结

  1. 了解了 Compose 开发动画的一些优点:基于声明式编程的 API 设计;提供了基于 Material Design 的高级 API 封装;IDE 新功能的支持;
  2. 了解了怎么写基于内容变化的动画:如控制内容出现隐藏的 AnimatedVisibility 、控制内容变化的 AnimatedContentCrossfade 等;
  3. 了解了怎么写基于状态变化的动画;如控制单状态变化的 animate*AsState 、控制多状态变化的 updateTransition 等;
  4. 了解了提供给非基本类型使用的类型转换器 TwoWayConverter
  5. 了解了对相同动画逻辑代码进行封装的最佳实践;

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 文章目的:
  • 知识储备:
  • 一、我为什么喜欢用 Compose 写动画?
    • 1.1 声明式编程
      • 1.2 封装好的高级API
        • 1.3 工具的支持
        • 二、如何选择合适的动画实现
        • 三、基于内容变化的动画
          • 3.1 出现和消失 → 改变内容
            • 3.2 淡入和淡出 → 切换内容
            • 四、基于效果状态的动画
              • 4.1 视图单个属性的变化
                • 4.2 视图多个属性的变化
                  • 4.3 对于自定义类型的属性变化
                    • 4.4 AnimationSpec
                    • 五、对相同的动画进行封装的最佳实践
                    • 六、小结
                    相关产品与服务
                    容器服务
                    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档