前文讲了Compose中的低级别API动画,与之对应的,还有高级别API动画,同样也符合Material-Design规范。所有高级别动画 API 都是在低级别动画 API 的基础上构建而成,其对应关系如图:
接下来就对其高级别API逐个分析:
AnimatedVisibility
即可见性动画,原Google官方文档对此API有实验性标记(可能后面更新就删除了或有其余更改,代码中要用ExperimentalAnimationApi标记),到Compose历经多个版本迭代,这些API依旧坚挺在此,估计后续也不会消失了。
可见性动画,主要是为其内容的出现、消失提供动画效果。先来看下其函数定义:
@ExperimentalAnimationApi
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visible)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
在原生View体系动画中,如果想实现隐藏淡入淡出放大缩小等复杂效果,可能需要alpha、scale、transition等动画一起配合实现。但在Compose中,仅通过AnimatedVisibility即可实现,其参数 enter(EnterTransition) 和 exit(ExitTransition)就可以自定义达到淡入淡出的过渡效果。visibile含义就是是否可见,这个没什么好说的。modifier是修饰符,可自定义控件各种属性。content则是指的对应的子控件。从这里可以看出,关键两个参数是EnterTransition和ExitTransition,我们先来了解下它俩。
EnterTransition
Compose对其提供动画函数如下:
fadeIn:从指定的起始alpha到1f淡入。alpha默认为0f,动画规格默认使用spring(spring相关解释在后续的自定义动画中)。
slideIn:从定义的起始偏移量到IntOffset(0,0)的滑动内容。可以通过配置控制滑动方向。正x值表示从右向左滑动,负x值表示从左向右滑动。类似地,正y值和负y值分别对应向上滑动和向下滑动。
expandIn:将显示内容的范围从返回的大小扩展到完整大小。可以控制先显示哪一部分内容。默认情况下,展示内容从IntSize(0,0)至完整大小的动画,从显示内容的右下角(或RTL布局中的左下角)逐渐扩展至显示整个内容。
expandHorizontally:将显示内容的范围从返回的宽度水平扩展到整个宽度。可以控制首先显示哪一部分内容。默认情况下,展示内容从0到全宽的动画,逐渐扩展到显示整个内容。
expandVertically:将显示内容的范围从返回的高度垂直扩展到整个高度。可以控制首先显示哪一部分内容。默认情况下,展示内容从0到全高的动画,首先显示底边,然后显示其余内容。
slideInHorizontally:从定义的起始偏移量到0水平滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值表示从右向左滑动,负值表示从左向右滑动。
slideInVertically:从定义的起始偏移量到0垂直滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值意味着向上滑动,负值意味着向下滑动。
注意,slideIn、slideInVertically、slideInVertically 这种同类型只能同时存在一个。
ExitTransition
同样,Compose对其也提供了一系列动画函数:
fadeOut:从完全不透明到目标alpha淡出效果。默认情况下,内容将淡出为完全透明,动画规格也默认使用spring(spring相关解释在后续的自定义动画中)。
slideOut:从IntOffset(0,0)到定义的目标偏移量的滑动效果。可以通过配置控制方向。x值为正表示从左向右滑动,x值为负表示从右向左滑动。类似地,正y值和负y值分别对应向下滑动和向上滑动。
shrinkOut:展示内容的范围从完整大小缩小到返回的大小的效果。可以控制范围缩小动画的方向。默认情况下,展示内容从完整大小至IntSize(0,0)的动画,并朝内容的右下角(或RTL布局中的左下角)缩小。
shrinkHorizontally:展示内容从整个宽度水平缩小到返回的宽度的效果。可以控制范围缩小动画的方向。默认情况下,剪辑范围从全宽到0设置动画,并朝内容的结尾缩小。
shrinkVertically:展示内容从整个高度垂直缩小到返回的高度消失的效果。可以控制缩小动画的方向。默认情况下,剪辑范围从全高到0设置动画,并朝内容的底部缩小。
slideOutHorizontally:从0到定义的目标偏移量水平滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值表示向右滑动,负值表示向左滑动。
slideOutVertically:从0到定义的目标偏移量垂直滑动内容(以像素为单位)。可以通过配置控制滑动方向。正值表示向下滑动,负值表示向上滑动。
以上内容说起来很抽象,看以下实例:
fun showAnim() {
var isShow by remember { mutableStateOf(true) }
Column(
Modifier.size(300.dp,300.dp),
Arrangement.Top,
Alignment.CenterHorizontally
) {
Button(
onClick = { isShow = !isShow }
) {
Text(text = if (isShow) "Hide" else "Show")
}
Spacer(Modifier.height(1.dp))
AnimatedVisibility(
visible = isShow,
enter = slideInVertically() + fadeIn(initialAlpha = 0.1f),
exit = slideOutVertically() + fadeOut(targetAlpha = 0.1f)
) {
Image(
painter = painterResource(id = R.drawable.icon_pdx),
contentDescription = null,
Modifier.fillMaxSize()
)
}
}
}
其对应效果为:
可以看出,这里有淡入淡出效果的同时,还有上滑下滑效果。通过这种 + 运算符组合多个 EnterTransition 或 ExitTransition 对象(且每个对象都可自定义可选参数和行为)的方式,达到各种效果。其余方法对应效果可以自行验证。
AnimatedContent
内容大小动画,可在内容根据目标状态发生变化时,为内容添加动画效果。其构造函数如下:
@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
) {
val transition = updateTransition(targetState = targetState, label = "AnimatedContent")
transition.AnimatedContent(
modifier,
transitionSpec,
contentAlignment,
content
)
}
这里可以看出,使用 lambda 参数并将动画反映到内容中(可能描述不太准确),默认情况下,初始内容淡出,然后目标内容淡入(即淡出后淡入)。例如以下示例:
fun showAnim() {
var data by remember { mutableStateOf(0) }
Column(
Modifier
.fillMaxWidth()
.fillMaxHeight(),
Arrangement.Center,
Alignment.CenterHorizontally
) {
Button(onClick = { data++ }) {
Text("change")
}
AnimatedContent(targetState = data,
transitionSpec ={ slideInVertically({ fullHeight -> fullHeight}) with
slideOutVertically({height -> -height})+ fadeOut()
}
) {
Text(text = "${data}", fontSize = 36.sp)
}
}
}
对应的效果为:
以上可见,当 targetState(这里是data) 发生变化时,content 在我们设置的动画中完成切换。
animateContentSize
此方法主要为大小变化添加动画效果。其构造函数如下:
fun Modifier.animateContentSize(
animationSpec: FiniteAnimationSpec<IntSize> = spring(),
finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "animateContentSize"
properties["animationSpec"] = animationSpec
properties["finishedListener"] = finishedListener
}
) {
// TODO: Listener could be a fun interface after 1.4
val scope = rememberCoroutineScope()
val animModifier = remember(scope) {
SizeAnimationModifier(animationSpec, scope)
}
animModifier.listener = finishedListener
this.clipToBounds().then(animModifier)
}
不难看出此函数为Modefier的一个扩展函数,示例如下:
fun showAnim() {
var size by remember { mutableStateOf(Size(100F, 100F)) }
Column(
Modifier
.fillMaxWidth()
.fillMaxHeight(),
Arrangement.Center,
Alignment.CenterHorizontally
) {
Box(
Modifier
.animateContentSize()
) {
Image(
painter = painterResource(id = R.drawable.icon_pdx),
contentDescription = null,
Modifier
.animateContentSize()
.size(size = size.height.dp)
)
}
Spacer(Modifier.height(10.dp))
Button(
onClick = {
size = if (size.height == 100F) {
Size(250F, 250F)
} else {
Size(100F, 100F)
}
}
) {
Text(if (size.height == 100F) "收缩" else "展开")
}
}
}
此部分代码通过变量size监听状态变化实现布局大小的动画效果:
Crossfade
此方法主要用于在两个布局之间添加淡入淡出动画,即布局切换动画。通过切换传递给 current 参数的值,使得淡入淡出动画切换内容。其构造函数如下:
@Composable
fun <T> Crossfade(
targetState: T,
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
content: @Composable (T) -> Unit
)
targetState和content,在这里,targetState是指定当前的布局状态,content是显示内容,Modifier修饰符在这是修饰Crossfade,animationSpec则是指定动画类型。例如以下示例:
fun showAnim() {
var page by remember { mutableStateOf(1) }
Column {
Button(onClick = { page = if (page == 1) 2 else 1 }) {
Text("变变变")
}
Crossfade(
targetState = page,
modifier = Modifier
.size(600.dp)
.background(if (page == 1) Color.White else Color.Blue),
animationSpec = spring()
) { screen ->
when (screen) {
1 -> Text("Page White", fontSize = 100.sp, color = Color.Blue)
2 -> Text("Page Blue", fontSize = 100.sp, color = Color.White)
}
}
}
}
对应效果则为:
看到这里,你应该发现了,其实套路都差不多,如果你对低级别API的动画效果还有兴趣,请点这里。下一篇即是Compose的自定义动画。