一、动画分类
Compose的动画API数量较多,刚接触的人不免有些头晕。为了方便大家快速了解,我们从使用场景的维度上将它们大体分为两类:高级别API和低级别API。就像编程语言分为高级语言和低级语言一样,这里的高级和低级指的是API的易用性。
高级别API服务于常见业务,设计上力求开箱即用,例如页面转场、UI元素的过渡等,高级别API大多是一个Composable函数,便于与其他Composable组合使用。
低级别API使用场景更加广泛,可以基于协程完成任何状态驱动的动画效果,相应的接口复杂度也更高。高级别API的底层实际上都是由低级别API支持的。
Compose动画API | ||
---|---|---|
分类 | API | 说明 |
高级别API | AnimatedVisibility | UI元素进入/退化时的过渡动画 |
高级别API | AnimatedContent | 布局内容变化时的动画 |
高级别API | Modifier.animateContentSize | 布局大小变化时的动画 |
高级别API | Crossfade | 两个布局切换时的淡入/淡出动画 |
低级别API | animate*AsState | 单个值动画 |
低级别API | Animatable | 可动画的数值容器 |
低级别API | updateTransition | 组合动画 |
低级别API | rememberInfiniteTransition | 组合无限执行动画 |
低级别API | TargetBasedAnimation | 自定义执行时间的低级别动画 |
上表列举了Compose动画相关的所有API。这么多的API在使用时需要有策略地进行选择。下面的关系图可以反映各API的选择路径,为我们提供指引。
(上图来自于实体书插图)
二、高级别动画API
1、AnimatedVisibility
源码如下
kotlin
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
AnimatedVisibility是一个容器类的Composable,需要接收一个Boolean型的visible参数控制content是否可见,content在出现与消失时,会伴随着过渡动画效果。
AnimatedVisibility是一个容器类的Composable,需要接收一个Boolean型的visible参数控制content是否可见,content在出现与消失时,会伴随着过渡动画效果。
kotlin
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}
在上面的代码中,当editable为true/false时,Text将会淡入/淡出屏幕。
可以通过设置EnterTransition和ExitTransition来定制出场与离场过渡动画,当出场动画完成时,content便会从视图树上移除。下面是一个代码示例:
kotlin
@Composable
fun Greeting() {
var visible by remember { mutableStateOf(true) }
val density = LocalDensity.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一个按钮,显示动画
Button(onClick = { visible = true }) {
Text("Show Animation")
}
// 第二个按钮,隐藏动画
Button(onClick = { visible = false }) {
Text("Hide Animation")
}
// 动画部分
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
// 从顶部40dp的位置开始滑入
with(density) { -40.dp.roundToPx() }
} + expandVertically(
// 从顶部开始展开
expandFrom = Alignment.Top
) + fadeIn(
// 从初始透明度0.1f开始淡入
initialAlpha = 0.1f
),
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text(
"Hello Kotlin",
color = Color.Black,
fontSize = 36.sp
)
}
}
}
UI效果
如上面的示例,可以使用+运算符来组合多个已有的EnterTransition或ExitTransition,并通过enter与exit参数进行设置。
默认情况下EnterTransition是fadeIn+expandIn的效果组合,ExitTransition是fadeOut+shrinkOut的效果组合。Compose额外提供了RowScope.AnimatedVisibility和ColumnScope. AnimatedVisibility两个扩展方法,我们可以在Row或者Column中调用AnimatedVisibility,该组件的默认过渡动画效果会根据父容器的布局特征进行调整,比如在Row中默认EnterTransition是fadeIn+expandHorizontally组合方案,而在Column中默认EnterTransition则是fadeIn+expandVertically组合方案。看下面RowScope的代码示例:
kotlin
@Composable
fun Greeting() {
var visible by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一个按钮,显示动画
Button(onClick = { visible = true }) {
Text("Show Animation")
}
// 第二个按钮,隐藏动画
Button(onClick = { visible = false }) {
Text("Hide Animation")
}
//Row的扩展动画
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) {
Text(
text = "Hello Compose",
fontSize = 36.sp,
color = Color.Black
)
}
}
}
}
UI效果
下表中列举了几种EnterTransition和ExitTransition的动画效果(具体动画效果请访问官网):
EnterTransition | ExitTransition |
---|---|
fadeIn 淡入动画 | fadeOut 淡出动画 |
slideIn滑入动画 | slideOut 滑出动画 |
slideInHorizontally 水平滑入动画 | slideOutHorizontally 水平滑出动画 |
slideInVertically 垂直滑入动画 | slideOutVertically垂直滑出动画 |
scaleIn 缩放进入动画 | scaleOut 缩放退出动画 |
expandIn 展开进入动画 | shrinkOut缩小退出动画 |
expandHorizontally 水平展开动画 | shrinkHorizontally 水平缩小动画 |
expandVertically 垂直展开动画 | shrinkVertically 垂直缩小动画 |
(1)MutableTransitionState监听动画状态
AnimatedVisibility还有一个接收MutableTransitionState类型参数的重载方法。
kotlin
@Composable
fun AnimatedVisibility(
visibleState: MutableTransitionState<Boolean>, //接收MutableTransitionState类型参数
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = fadeOut() + shrinkOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visibleState, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
MutableTransitionState的定义如上所示,关键成员有两个:当前状态currentState和目标状态targetState。两个状态的不同驱动了动画的执行。
kotlin
class MutableTransitionState<S>(initialState: S) {
var currentState: S by mutableStateOf(initialState)
internal set
var targetState: S by mutableStateOf(initialState)
val isIdle: Boolean
get() = (currentState == targetState) && !isRunning
// Updated from Transition
internal var isRunning: Boolean by mutableStateOf(false)
}
看一段示例代码
kotlin
@Composable
fun Greeting() {
val state = remember { MutableTransitionState(false).apply { targetState = true } }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一个按钮,显示动画
Button(onClick = { state.targetState = true }) {
Text("Show Animation")
}
// 第二个按钮,隐藏动画
Button(onClick = { state.targetState = false }) {
Text("Hide Animation")
}
//MutableTransitionState传入AnimatedVisibility
AnimatedVisibility(
visibleState = state,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) {
Text(
text = "Hello Compose",
fontSize = 36.sp,
color = Color.Black
)
}
}
}
UI效果
在上面的代码中,我们在创建MutableTransitionState时,将currentState初始值设置为false,并将targetState设为true,所以当AnimatedVisibility上屏(即Composable组件的OnActive)时,由于两个状态的不同,动画会立即执行。可以用类似的做法实现一些开屏时的动画。
此外,MutableTransitionState的意义还在于通过currentState和isIdle的值,可以获取动画的执行状态。例如下面的代码:
kotlin
@Composable
fun Greeting() {
//动画的监听
TestMutableTransitionState()
}
@Composable
fun TestMutableTransitionState() {
var isStopLog = false
val state = remember { MutableTransitionState(false).apply { targetState = true } }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一个按钮,显示动画
Button(onClick = {
state.targetState = true
isStopLog = false
}) {
Text("Show Animation", fontSize = 22.sp)
}
// 第二个按钮,隐藏动画
Button(onClick = {
state.targetState = false
isStopLog = false
}) {
Text("Hide Animation", fontSize = 22.sp)
}
//MutableTransitionState传入AnimatedVisibility
AnimatedVisibility(
visibleState = state,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) {
Text(
text = "Hello Compose", fontSize = 36.sp, color = Color.Black
)
}
}
//定时获取动画的状态
LaunchedEffect(Unit) {
timer(period = 100) {
if (!isStopLog) {
when (val animationState = state.getAnimationState()) {
AnimState.INVISIBLE -> {
"动画的状态:${animationState.name}".logd()
isStopLog = true
}
AnimState.VISIBLE -> {
"动画的状态:${animationState.name}".logd()
isStopLog = true
}
else -> {
"动画的状态:${animationState.name}".logd()
}
}
}
}
}
}
/**
* 动画的各种状态
*/
private fun MutableTransitionState<Boolean>.getAnimationState(): AnimState {
return when {
this.isIdle && this.currentState -> AnimState.VISIBLE //动画已经结束,当前处于可见状态
this.isIdle && !this.currentState -> AnimState.INVISIBLE //动画已经结束,当前处于不可见状态
!this.isIdle && this.currentState -> AnimState.DISAPPEARING //动画执行中,且逐渐不可见
else -> AnimState.APPEARING //动画执行中,且逐渐可见
}
}
/**
* 动画状态的枚举
*/
enum class AnimState {
VISIBLE, INVISIBLE, APPEARING, DISAPPEARING
}
UI效果
主要看一下日志
kotlin
//进入页面时进入动画的状态
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:VISIBLE
//点击隐藏时的退出页面的动画状态
动画的状态:DISAPPEARING
动画的状态:DISAPPEARING
动画的状态:DISAPPEARING
动画的状态:DISAPPEARING
动画的状态:DISAPPEARING
动画的状态:INVISIBLE
//点击显示时进入页面的动画状态
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:APPEARING
动画的状态:VISIBLE
(2)Modifier.animateEnterExit
在AnimatedVisibility的content中,可以使用Modifier.animateEnterExit为每个子元素单独设置进出屏幕的过渡动画。看下面的示例代码:
kotlin
@Composable
fun AnimatedModifier() {
var visible by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一个按钮,显示动画
Button(onClick = {
visible = true
}) {
Text("Show Animation", fontSize = 22.sp)
}
// 第二个按钮,隐藏动画
Button(onClick = {
visible = false
}) {
Text("Hide Animation", fontSize = 22.sp)
}
AnimatedVisibility(
visible = visible,
enter = fadeIn(), //外层动画淡入淡出
exit = fadeOut()
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.DarkGray)
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.animateEnterExit(
enter = slideInVertically(), //内层组件滑入滑动屏幕
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 256.dp)
) {
Image(
painter = painterResource(id = R.mipmap.rabit2),
contentDescription = null
)
}
}
}
}
}
看下UI效果
比如上面的例子中,后添加的slide动画会覆盖AnimatedVisibility设置的fade动画。有时我们希望AnimatedVisibility内部每个子组件的过渡动画各不相同,此时可以为AnimatedVisibility的enter与exit参数分别设置EnterTransition. None和ExitTransition. None,并在每个子组件分别指定animateEnterExit就可以了。
(3)自定义Enter/Exit动画
如果想在EnterTransition和ExitTransition之外再增加其他动画效果,可以在AnimatedVisibilityScope内设置transition。添加到transition的动画都会在AnimatedVisibility进出屏幕动画的同时运行。AnimatedVisibility会等到Transition中的所有动画都完成后,再移出屏幕。看下面的示例代码:
kotlin
@Composable
fun AnimatedCustom() {
var visible by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// 第一个按钮,显示动画
Button(onClick = {
visible = true
}) {
Text("Show Animation", fontSize = 22.sp)
}
// 第二个按钮,隐藏动画
Button(onClick = {
visible = false
}) {
Text("Hide Animation", fontSize = 22.sp)
}
AnimatedVisibility(
visible = visible,
enter = fadeIn(animationSpec = tween(3000)), //外层动画淡入淡出,动画时长3秒
exit = fadeOut(animationSpec = tween(3000))
) {
//this:AnimatedVisibilityScope
//使用AnimatedVisibilityScope#transition添加自定义动画
//创建一个跟随动画状态改变的背景
val background by transition.animateColor(label = "") { enterExitState ->
when (enterExitState) {
EnterExitState.Visible -> {
Color.Blue //可见时蓝色
}
EnterExitState.PostExit -> {
Color.Green //退出时绿色
}
else -> { //EnterExitState.PreEnter
Color.DarkGray //进入时灰色
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(background) //使用动画背景
) {
Box(
modifier = Modifier
.align(Alignment.Center)
.animateEnterExit(
enter = slideInVertically(), //内层组件滑入滑动屏幕
exit = slideOutVertically()
)
.sizeIn(minWidth = 256.dp, minHeight = 256.dp)
) {
Image(
painter = painterResource(id = R.mipmap.rabit2),
contentDescription = null
)
}
}
}
}
}
UI效果
在上面的代码中,向transition添加了一个颜色渐变动画,并将其设置为Box背景色。关于Transition的更多内容可以参考低级别动画API中的updateTransition。
2、AnimatedContent
AnimatedContent和AnimatedVisibility相类似,都是用来为content添加动画效果的Composable。区别在于AnimatedVisibility用来添加组件的出场与离场过渡动画,而AnimatedContent则是用来实现不同组件间的切换动画。
kotlin
@Composable
fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
(fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
.togetherWith(fadeOut(animationSpec = tween(90)))
},
contentAlignment: Alignment = Alignment.TopStart,
label: String = "AnimatedContent",
contentKey: (targetState: S) -> Any? = { it },
content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
) {...}
AnimatedContent参数上接收一个targetState和一个content, content是基于targetState创建的Composable。当targetState变化时,content的内容也会随之变化。AnimatedContent内部维护着targetState到conent的映射表,查找targetState新旧值对应的content后,在content发生重组时附加动画效果。
kotlin
@Composable
fun TestAnimatedContent() {
var count by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
//点击按钮增加值
Button(onClick = {
count++
}) {
Text("增加", fontSize = 22.sp)
}
//count赋值给targetState
AnimatedContent(targetState = count, label = "") { targetState -> //targetState不使用会报红
Text("count的值:$targetState", fontSize = 22.sp)
}
}
}
UI效果
上述代码中,单击按钮触发count发生变化,AnimatedContent中Text的重组会应用动画效果。需要注意的是targetState一定要在content中被使用。
(1)ContentTransform自定义动画
AnimatedContent默认动画是淡入淡出效果,还可以将transitionSpec参数指定为一个ContentTransform来自定义动画效果。ContentTransform也是由EnterTransition与ExitTransition组合的,可以使用togetherWith中缀运算符将EnterTransition与ExitTransition组合起来。
kotlin
package androidx.compose.animation
infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)
可以很容易猜到,ContentTransform本质上就是currentContent的ExitTransition与targetContent的EnterTransition组合。例如使用ContentTransform实现一个Slide效果的切换动画:
从右到左切换,并伴随淡入淡出效果:
- EnterTransion:使用slideInHorizontally,初始位置initialOffsetX=width
- ExitTransition:使用slideOutHorizontally,目标位置targetOffsetX=-width
看下面的示例代码:
kotlin
@Composable
fun TestAnimatedContent() {
var count by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
//点击按钮增加值
Button(onClick = {
count++
}) {
Text("增加", fontSize = 22.sp)
}
//count赋值给targetState
//targetState不使用会报红
AnimatedContent(targetState = count, transitionSpec = {
//重点就是下面这行
slideInHorizontally { fullWidth -> fullWidth } + fadeIn() togetherWith slideOutHorizontally { fullWidth -> -fullWidth } + fadeOut()
}, label = "") { targetState ->
Text("count的值:$targetState", fontSize = 22.sp)
}
}
}
UI效果
看了上面的效果,做一个竖直跳动的数字也就很简单了,只需要修改动画为垂直方向即可:
kotlin
slideInVertically { fullHeight -> fullHeight } + fadeIn() togetherWith slideOutVertically { fullHeight -> -fullHeight } + fadeOut()
UI效果
(2)SizeTranstion定义大小动画
在使用ContentTransform来创建自定义过渡动画的同时,还可以使用using操作符连接SizeTransform。SizeTransform可以使我们预先获取到currentContent和targetContent的Size值,并允许我们来定制尺寸变化的过渡动画效果。
看一个ContentTransform+SizeTranform的例子:
kotlin
@Composable
fun TestAnimatedSize() {
var expanded by remember { mutableStateOf(false) }
Column(modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.clickable {
expanded = !expanded
}) {
AnimatedContent(targetState = expanded, transitionSpec = {
//淡入淡出
fadeIn(animationSpec = tween(1500, 150)) togetherWith
fadeOut(animationSpec = tween(1500)) using //使用using操作符连接SizeTransform
SizeTransform { initialSize, targetSize ->
if (targetState) {
keyframes { //动画的关键帧
//展开时,先水平方向展开
//在动画的 1500 毫秒处达到目标尺寸
IntSize(targetSize.width, initialSize.height) at 1500
durationMillis = 3000
}
} else {
keyframes { //动画的关键帧
//收起时,先垂直方向收起
//在动画的 1500 毫秒处达到目标尺寸
IntSize(initialSize.width, targetSize.height) at 1500
durationMillis = 3000
}
}
}
}, label = "") { targetState ->
ExpandLayout(targetState)
}
}
}
@Composable
private fun ExpandLayout(targetState: Boolean) {
if (targetState) {
Text(text = stringResource(id = R.string.verse))
} else {
Icon(imageVector = Icons.Filled.AddCircle, contentDescription =null )
}
}
UI效果
如上面的例子中,currentContent是一个小尺寸的icon, targtContent是一段大尺寸的文本,从icon到文本切换的过程中,可以使用SizeTransform实现尺寸变化的过渡动画。在SizeTransform中可以通过关键帧keyframes指定Size在某一个时间点的尺寸,以及对应的动画时长。比如例子中表示expend过程持续时间为3000ms,在1500ms前,高度保持不变,宽度逐渐增大,而在到达1500ms之后,宽度到达目标值将不再变化,高度再逐渐增大。
(3)定义子元素动画
与AnimatedVisibility一样,AnimatedContent内部的子组件也可以通过Modifier.animatedEnterExit单独指定动画。
(4)自定义Enter/Exit动画
通过AnimatedContent的定义可知,其content同样是在AnimatedVisibilityScope作用域中,所以内部也可以通过transition添加额外的自定义动画。
3、Crossfade
Crossfade可以理解为AnimatedContent的一种功能特性,它使用起来更简单,如果只需要淡入淡出效果,可以使用Crossfade替代AnimatedContent。
kotlin
@Composable
fun <T> Crossfade(
targetState: T,
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
label: String = "Crossfade",
content: @Composable (T) -> Unit
) {...}
下面是一个示例
kotlin
@Composable
fun TestAnimatedCrossfade() {
var currentPage by remember { mutableStateOf("A") }
Crossfade(targetState = currentPage, animationSpec = tween(1500), label = "") { screen ->
when (screen) {
"A" -> Text(
text = "PAGE LOGIN",
fontSize = 26.sp,
color = Color.Blue,
modifier = Modifier.clickable { currentPage = "B" })
else -> Text(
text = "PAGE MAIN",
fontSize = 26.sp,
color = Color.Red,
modifier = Modifier.clickable { currentPage = "A" })
}
}
}
UI效果
如上所述,Crossfade内的文本会以淡入淡出的形式进行切换。其实更正确的说法应该是AnimatedContent是Crossfade的一种泛化,Crossfade的API出现后,为了强化切换动画的能力,增加了AnimatedContent。需要注意Crossfade无法实现SizeTransform那样尺寸变化的动画效果,如果content变化前后尺寸不同,想使用动画进行过渡,可以使用AnimatedContent+SizeTranform的组合方案,或者使用Crossfade和接下来要介绍的Modifier.animateContentSize。
4、Modifier.animateContentSize
animateContentSize是一个Modifier修饰符方法。它的用途非常专一,当容器尺寸发生变化时,会通过动画进行过渡,开箱即用。下面是一个简单的例子:
kotlin
@Composable
fun TestAnimatedContentSize() {
var expend by remember { mutableStateOf(false) }
Column(Modifier.padding(16.dp)) {
Button(
onClick = { expend = !expend }
) {
Text(if (expend) "Shrink" else "Expand")
}
Spacer(Modifier.height(16.dp))
Box(
Modifier
.background(Color.LightGray)
.animateContentSize() //父控件使用动画
) {
Text(
text = stringResource(id = R.string.verse),
modifier = Modifier.padding(16.dp),
fontSize = 16.sp,
textAlign = TextAlign.Justify,
maxLines = if (expend) Int.MAX_VALUE else 2
)
}
}
}
UI效果
如上代码所示,expend决定文本的最大行数,也就决定了Box的整体尺寸,正常情况下大小的变化会立即生效,但是为Box添加Modifieir.animatedContentSize后,文本大小的变化会使用动画过渡。
参考资料
本文为学习博客,内容来自书籍《Jetpack Compose 从入门到实战》,代码为具体实践。致谢!