前言
断断续续学习Compose已经快有一个月了,在编写“正在加载框”这个效果时,遇到了动画相关的问题。当然Lottie框架也已经支持Compose了,但学习和了解Compose动画的基础知识还是很有必要的,本篇文章就来一起了解Compose动画的实现~
动画的种类
动画的种类就很多,根据使用场景有AnimationVisibility、rememberInfiniteTransition、Animation等。如果你想知道在你的需求场景中需要使用什么动画,可以参照官方的这张流程指示图。
AnimationVisibility
AnimationVisibility可以为布局中的内容变化添加动画效果,比如内容的显示、隐藏等效果。
我们用AnimationVisibility来实现控制图片的显示与隐藏,首先定义变量用来控制图片是否显示,代码如下所示:
var visible by remember {
mutableStateOf(false)
}
默认不显示,使用AnimatedVisibility函数将图片组件包裹
AnimatedVisibility(
visible = visible
) {
Image(
painter = painterResource(id = R.mipmap.photon),
contentDescription = null
)
}
添加一个Button,用于控制图片的显示与隐藏,代码如下所示:
Button(modifier = Modifier.padding(vertical = 5.dp), onClick = {
visible = !visible
}) {
val value = if (visible) {
"隐藏"
} else {
"显示"
}
Text(text = value)
}
运行程序,效果图如下所示。
从效果图中可以看出,图片出现时有自上到下弹入的效果,图片消失时有自下到上弹出的效果。那么这个动画效果是如何实现的呢?AnimatedVisibility函数的源码如下所示:
@Composable
fun ColumnScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandVertically(),
exit: ExitTransition = fadeOut() + shrinkVertically(),
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
)
visible参数用于控制是否显示,enter、exit参数分别用来设置动画进入和退出的效果。这里设置了默认效果。在EnterTransition这个密封类中定义了fadeIn、fadeOut、slideIn、slideOut 以及scaleIn、scaleOut动画效果。动画效果是可以自由组合的,如上源码所示为动画进入设置了fadeIn+expandVertically的组合效果。
接着我们自己设置动画效果为scaleIn和scaleOut,修改代码如下所示:
AnimatedVisibility(
visible = visible,
enter = scaleIn(),
exit = scaleOut()
) {
Image(
painter = painterResource(id = R.mipmap.photon),
contentDescription = null
)
}
运行程序,效果图如下所示。
从效果图可以看出scaleIn和scaleOut的效果为从中间扩散和向中间聚集的效果。更多的效果显示,读者可自行尝试。
AnimatedContent
AnimatedContent可以设定目标内容,当目标内容变化时,为内容添加动画效果。以点击按钮改变data变量值为例,代码如下所示:
Column() {
var data by remember { mutableStateOf(0) }
Button(onClick = { data++ }) {
Text("添加数据")
}
AnimatedContent(targetState = data) {
Text(text = "数值:${data}")
}
}
运行程序,效果图如下所示。
从效果图中可以看出,在数值变化的时候,会有淡入淡出的效果。AnimatedContent函数源码如下所示:
@ExperimentalAnimationApi
@Composable
fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) with
fadeOut(animationSpec = tween(90))
},
contentAlignment: Alignment = Alignment.TopStart,
content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
)
targetState参数指定目标, transitionSpec参数用来指定动画行为。编写代码如下所示:
AnimatedContent(targetState = data,
transitionSpec = {
(scaleIn() with scaleOut()).using(SizeTransform(false))
}) {
Text(text = "数值:${data}")
}
我们先来看,代码为什么可以这样写,transitionSpec参与是ContentTransform对象,我们来看ContentTransform的源码,如下所示:
@ExperimentalAnimationApi
class ContentTransform(
val targetContentEnter: EnterTransition,
val initialContentExit: ExitTransition,
targetContentZIndex: Float = 0f,
sizeTransform: SizeTransform? = SizeTransform()
)
可以看到参数指定了进入动画、退出动画 这一点与AnimatedVisibility的使用是相同的。
sizeTransForm参数定义了在初始内容与目标内容之间添加动画效果,进入、退出动画可以使用with函数来组合,sizeTransform参数提供了using扩展函数来使用,代码如下所示:
@ExperimentalAnimationApi
infix fun ContentTransform.using(sizeTransform: SizeTransform?) = this.apply {
this.sizeTransform = sizeTransform
}
运行程序,效果图如下所示。
Crossfade与animateContentSize
animateContentSize可以在尺寸大小改变的时候添加动画,Crossfade是淡入淡出动画,可用于视图切换等操作。首先来看animateContentSize的使用。
animateContentSize
编写一个示例,包含一个Edittext和一个TextView,TextView中实时显示Edittext的输入内容,代码如下所示:
Column() {
var message by remember { mutableStateOf("") }
TextField(value = message, onValueChange = { message = it })
Box(
modifier = Modifier
.background(Color.Red)
.animateContentSize()
) {
Text(text = message)
}
}
为了便于观察效果,我们这里设置背景为红色,运行程序,效果图如下所示。
有一种丝滑般的感觉,一起纵享丝滑吧~
Crossfade
Crossfade可用于两个视图间的切换动画,编写代码:按钮控制当前页面显示Screen1页面或Screen2页面,为了便于区分,两个页面分别设置背景为蓝色和绿色。具体代码此处就省略了。
页面切换部分代码如下所示:
var flag by remember {
mutableStateOf(false)
}
Column() {
Crossfade(targetState = flag, animationSpec = tween(3000)) {
when (it) {
false -> Screen1()
true -> Screen2()
}
}
Button(onClick = { flag = !flag }) {
Text(text = "视图切换")
}
}
为了便于观察效果,此处为动画设置tween的间隔时间为3秒,运行程序,效果图如下所示:
Crossfade函数源码如下所示:
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun <T> Crossfade(
targetState: T,
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
content: @Composable (T) -> Unit
)
animationSpec参数是FiniteAnimationSpec类型的参数,实现类有TweenSpec、SpringSpec等,默认值是tween,tween是一个可配置的曲线动画,源码如下所示:
@Stable
fun <T> tween(
durationMillis: Int = DefaultDurationMillis,
delayMillis: Int = 0,
easing: Easing = FastOutSlowInEasing
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)
我们也可以设置spring等效果,这里读者可自行尝试。
其他
除此之外,还有animate*AsState、rememberInfiniteTransition等低级别的动画API,更多用法,这里不再一一讲解了。回到刚开始前言的问题,如何实现 一个正在加载的动画呢?
这里我们使用rememberInfiniteTransition来定义一个无限加载的动画,并通过infiniteRepeatable来制定动画规范。最后一起来看一下,我的Compose开源项目中所实现的加载框的效果吧~
写在最后
近期越来越感觉,学无止境,需要学习的东西太多太多~ ,期待我们下篇文章 再见~