前言
使用 AnimatedVisibility 可以实现单个组件出现和消失的动画效果,那多个组件之间进行切换的动画效果,该怎么实现呢?
就要用到Crossfade 和 AnimatedContent。
它们可以让某块区域的内容,根据用户的操作或状态,从内容 A 换成内容 B,并且这种切换是以动画形式完成的。
淡入淡出切换:Crossfade
如果我们只需要一个淡入淡出的动画效果来切换内容,就可以使用 Crossfade,它是通过改变内容的透明度实现的。
让旧元素逐渐消失(透明度从 1 到 0),让新元素逐渐显示(透明度从 0 到 1),从而实现平滑的过渡效果。
@Composable
fun CrossfadeDemo() {
var shown by remember { mutableStateOf(true) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { shown = !shown }) {
Text(if (shown) "切换到内容B" else "切换到内容A")
}
Crossfade(targetState = shown, animationSpec = tween(durationMillis = 2000)) { targetShow ->
if (targetShow) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(painter = painterResource(id = R.drawable.girl),contentDescription = null)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(painter = painterResource(id = R.drawable.dog),contentDescription = null)
}
}
}
}
}
运行效果:
Crossfade 并不是只能针对 true 和 false 做出反应,它的目标状态 targetState 可以是任何类型,比如枚举、Int类型。
对于 Crossfade 的动画效果只有淡入淡出,你无法做更多的效果配置(除了控制动画速度曲线和时长)。如果你不需要淡入淡出效果,需要别的动画效果,并且要对效果进行详细的配置,就只能使用更为强大的 AnimatedContent。
详细定制内容切换:AnimatedContent
AnimatedContent 不仅可以实现 Crossfade 的淡入淡出效果,还可以对入场和出场动画效果做详细的定制。
先来看看它的基本用法,只需将前面的示例中使用的 Crossfade 改为 AnimatedContent(当然 AnimatedContent 函数没有 animationSpec 参数,要去掉):
@Composable
fun AnimatedContentDemo() {
var shown by remember { mutableStateOf(true) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(onClick = { shown = !shown }) {
Text(if (shown) "切换到内容B" else "切换到内容A")
}
AnimatedContent(targetState = shown) { targetShow ->
if (targetShow) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(painter = painterResource(id = R.drawable.girl),contentDescription = null)
}
} else {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green),
contentAlignment = Alignment.Center
) {
Image(painter = painterResource(id = R.drawable.dog),contentDescription = null)
}
}
}
}
}
运行效果:
看起来和使用 Crossfade 还是有一些区别的:
-
新旧内容交替时,
Crossfade是新、旧元素同时地逐渐出现和逐渐消失,而AnimatedContent会等待旧的内容消失后,新的内容再开始出现。 -
并且尺寸变化也不同。内容切换时,
AnimatedContent的尺寸变化是以动画的形式渐变地变化。而不是像Crossfade那样,在某个时间点,尺寸发生突变。
如果你想更清楚地看到这些区别,可以进入动画预览(Animation Preview),仔细观察:
详细配置动画效果
动画效果的配置是主要是通过 transitionSpec 参数来完成的。这个参数是一个 lambda 表达式,需要返回一个 ContentTransform 对象。
并且它具有 Transition.Segment 上下文,我们可以访问到动画切换前的初始状态和切换后的目标状态: initialState 和 targetState。
@Composable
public 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
)
我们先来看看它的默认值,出场动画是一个 fadeOut 效果,时长90ms;入场动画是 fadeIn 效果加上 scaleIn 效果,并且有一个90ms的延迟,动画时长是220ms。
这就是我们刚刚看到的默认效果。如果不想要这种默认效果,就需要自定义 transitionSpec 参数的 lambda 表达式。
我们来看看 lambda 表达式的返回值的类型 ContentTransform:
public class ContentTransform(
public val targetContentEnter: EnterTransition,
public val initialContentExit: ExitTransition,
targetContentZIndex: Float = 0f,
sizeTransform: SizeTransform? = SizeTransform()
) {
public var targetContentZIndex: Float by mutableFloatStateOf(targetContentZIndex)
public var sizeTransform: SizeTransform? = sizeTransform
internal set
}
它有四个属性:
-
targetContentEnter: 配置新内容的入场动画 -
initialContentExit: 配置旧内容的出场动画这两个动画效果是如何配置的我们已经知道了,和
AnimatedVisibility函数中的enter和exit参数的配置方式完全一样。Compose 给我们提供了一个中缀函数
togetherWith,让我们可以轻松地将一个EnterTransition和一个ExitTransition组合成一个ContentTransform对象,像这样:transitionSpec = { fadeIn(animationSpec = tween(durationMillis = 500)) togetherWith fadeOut( animationSpec = tween( durationMillis = 500 ) ) }内部其实就是调用了
ContentTransform的构造函数public infix fun EnterTransition.togetherWith(exit: ExitTransition): ContentTransform = ContentTransform(this, exit) -
targetContentZIndex参数是用于控制新、旧内容的 Z 轴绘制顺序的。一般来说,新内容都会绘制在旧内容的上面。但如果你不想这样,想要旧内容盖在新内容之上,或者是某个内容永远在最底层,就可以通过调整这个参数来完成,参数的值代表层级,值越大,层级越高。你可以通过
ContentTransform的构造函数传入,也可以在创建好的ContentTransform对象上,修改targetContentZIndex属性:transitionSpec = { (fadeIn()).togetherWith(fadeOut()).apply { targetContentZIndex = if (!targetState ) 1f else 0f } } -
sizeTransform参数可以配置当切换内容前后尺寸不相同时,容器尺寸变化的动画效果。我们前面看到的尺寸的渐变效果,就是它完成的。public fun SizeTransform( clip: Boolean = true, sizeAnimationSpec: (initialSize: IntSize, targetSize: IntSize) -> FiniteAnimationSpec<IntSize> = { _, _ -> spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = IntSize.VisibilityThreshold ) } )创建
SizeTransform对象时,我们可以配置两个参数clip和sizeAnimationSpec。clip表示进行尺寸变化的动画时,显示的内容是否裁剪。如果不裁剪,那么在进行尺寸变化的过程中,显示的内容可能会超出或小于组件的布局尺寸。sizeAnimationSpec则是定义尺寸变化的动画速度曲线。你可以使用 Compose 提供的中缀函数
using,往ContentTransform对象上应用SizeTransform对象:transitionSpec = { (fadeIn()).togetherWith(fadeOut()) using SizeTransform( clip = false, sizeAnimationSpec = { _, _ -> tween() }) }当然也可以通过
ContentTransform的构造函数直接传入。
示例
最后看一个完整的示例:
@Composable
fun AnimatedContentDemo() {
var count by remember { mutableIntStateOf(1) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AnimatedContent(
targetState = count,
transitionSpec = {
// 旧内容向下滑出,新内容向上滑入
if (targetState > initialState) {
(slideInVertically { height -> height } + fadeIn()).togetherWith(
exit = slideOutVertically { height -> -height } + fadeOut())
} else {
(slideInVertically { height -> -height } + fadeIn()).togetherWith(
exit = slideOutVertically { height -> height } + fadeOut())
}
},
) { targetCount ->
Text(
text = "第 $targetCount 页",
modifier = Modifier.padding(16.dp),
color = if (targetCount % 2 == 0) Color.Blue else Color.Magenta
)
}
Row {
Button(onClick = { count-- }, enabled = (count > 1)) { Text("上一页") }
Spacer(Modifier.width(16.dp))
Button(onClick = { count++ }) { Text("下一页") }
}
}
}
运行效果: