前言
AnimatedVisibility Composable 函数可以让界面中的元素以动画的形式出现和消失。而不是瞬间就消失,然后猛地又出现。
从 if 到 AnimatedVisibility
我们先从一个简单的需求开始:如果我要让一个组件从界面消失,然后又出现在界面,该怎么办?
很简单,我们都写过,把组件放到 if 的代码块中就行了,像这样:
var visible by remember { mutableStateOf(true) }
Box(Modifier.fillMaxSize()){
Column(Modifier.align(Alignment.Center)) {
if (visible) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
)
}
Button(onClick = { visible = !visible }) {
Text(if (visible)"点击消失" else "点击出现")
}
}
}
运行效果:
完成了,但元素出现和消失是瞬间完成的,没有任何的过渡,用户看起来非常累(不信?你再多看几遍)。
那怎么能让这个过程变为动画的呢?
也很简单,把 if 换成 AnimatedVisibility,其它都不用改。
var visible by remember { mutableStateOf(true) }
Box(Modifier.fillMaxSize()){
Column(Modifier.align(Alignment.Center)) {
AnimatedVisibility (visible) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
)
}
Button(onClick = { visible = !visible }) {
Text(if (visible)"点击消失" else "点击出现")
}
}
}
运行效果:
这样 Box 组件的出现和消失就有动画效果了,并且我们还发现动画还是垂直方向的展开、收起效果。正好对应了我们的 Column 布局,这难道是一个巧合?
我们点进去 AnimatedVisibility 的源码看看:
@Composable
public fun ColumnScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandVertically(),
exit: ExitTransition = fadeOut() + shrinkVertically(),
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
)
发现这个函数是 ColumnScope 作用域的扩展函数,简单来说,这个函数只有在 Column 布局里才能使用。并且我们看入场和出场动画的配置(enter、exit参数),默认值包含 Vertically (纵向)的字样。
这么一结合,我们就知道了:这是 Compose 专门针对 Column 布局所做的纵向的入场和出场动画效果。
与 ColumnScope.AnimatedVisibility 类似的扩展函数还有 RowScope.AnimatedVisibility,它则是针对 Row 布局所做的横向的入场和出场动画效果。
@Composable
public fun RowScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandHorizontally(),
exit: ExitTransition = fadeOut() + shrinkHorizontally(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
如果不在以上两个布局中使用 AnimatedVisibility,就会调用通用的 AnimatedVisibility,不带任何前缀:
@Composable
public fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
它的入场和出场效果就是通用的。
定制动画:enter 与 exit
如果要定制我们想要的入场和出场动画效果,就需要通过 AnimatedVisibility 函数的 enter 与 exit 参数来控制。这两个参数的类型分别是 EnterTransition 和 ExitTransition。
我们先来看看控制入场动画的参数 enter 的类型 EnterTransition:
@Immutable
public sealed class EnterTransition
发现是一个密封类,再去看它的唯一的子类 EnterTransitionImpl:
@Immutable private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()
子类只有一个参数 data,其类型为 TransitionData,我们再点进去看看:
@Immutable
internal data class TransitionData(
val fade: Fade? = null, // 淡入淡出
val slide: Slide? = null, // 滑动
val changeSize: ChangeSize? = null, // 改变尺寸
val scale: Scale? = null, // 缩放
val hold: Boolean = false,
val effectsMap: Map<TransitionEffectKey<*>, TransitionEffect> = emptyMap()
)
它就是使用这几个属性来配置的动画的入场动画效果的。我们在填写 enter 参数时,其实就在创建并配置 TransitionData 对象的属性值,来决定动画效果。
出场动画效果(exit参数)也是类似的配置。
但我们通常不会也不能创建 TransitionData 对象来配置动画效果,因为 TransitionData 是 internal 的。而是调用 Compose 提供的一系列函数来定义动画效果,动画效果主要有以下四个维度。
淡入淡出 (Fade)
淡入淡出的效果通过改变透明度来实现。
// 定义淡入效果
@Stable
public fun fadeIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialAlpha: Float = 0f
)
我们可以对这种动画效果进行一个定制:
-
指定初始的透明度(
initialAlpha),默认值是 0f,也就是淡入时以完全透明开始。 -
指定动画曲线(
animationSpec),默认是spring(stiffness = Spring.StiffnessMediumLow)弹跳效果。
// 定义淡出效果
@Stable
public fun fadeOut(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
targetAlpha: Float = 0f,
)
淡出效果的定制是类似的,只是 targetAlpha 参数指定的是目标的透明度,默认值是 0f,表示淡出时以完全透明结束。
AnimatedVisibility(
visible = visible,
enter = fadeIn(
animationSpec = tween(durationMillis = 2000),
initialAlpha = 0.3f
),
exit = fadeOut(
animationSpec = spring(stiffness = Spring.StiffnessVeryLow),
targetAlpha = 0.1f
)
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
)
}
运行效果:
前面我们说调用的这些函数其实是在配置 TransitionData 对象内部的属性值,现在来验证一下,看一下 fadeIn 函数的实现:
@Stable
public fun fadeIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialAlpha: Float = 0f
): EnterTransition {
return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}
它内部确实有创建 EnterTransitionImpl 对象,其参数 TransitionData 中指定了 fade 属性值。
滑动 (Slide)
滑动的效果是通过改变位置实现的。
// 定义滑入效果
@Stable
public fun slideIn(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
initialOffset: (fullSize: IntSize) -> IntOffset,
)
initialOffset 参数是一个 lambda 表达式,没有默认值,它要我们返回初始的偏移量,滑入时会从提供的这个初始的位置慢慢移到组件本身应该到的位置;lambda 表达式的参数 fullSize 提供了组件的尺寸,方便我们基于组件的尺寸进行偏移计算。
// 定义滑出效果
@Stable
public fun slideOut(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
targetOffset: (fullSize: IntSize) -> IntOffset,
)
类似地,targetOffset 定义的是滑出动画的目标偏移量,从组件本身应该在的位置慢慢移到目标位置,然后消失。
并且 Compose 提供了便捷的函数,来提供水平、垂直的滑入滑出:slideInHorizontally、slideOutHorizontally、slideInVertically、slideOutVertically。
其中这些函数的 lambda 表达式提供的参数值不是组件的尺寸,而是组件的宽或高。
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally(
animationSpec = tween(durationMillis = 2000),
initialOffsetX = { width ->
-width / 2
}
), // 从左侧一半宽度处滑入
exit = slideOutVertically(
animationSpec = tween(durationMillis = 2000),
targetOffsetY = { fullHeight ->
fullHeight
}
) // 向下滑出整个高度
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
)
}
运行效果:
改变尺寸 (Change Size)
它是通过改变组件的可见尺寸实现的,本质上是对组件做裁切。
// 定义展开进入的效果
@Stable
public fun expandIn(
animationSpec: FiniteAnimationSpec<IntSize> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntSize.VisibilityThreshold
),
expandFrom: Alignment = Alignment.BottomEnd,
clip: Boolean = true,
initialSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
)
expandFrom 参数是定义展开的起始位置,如 Alignment.TopStart, Alignment.Center 等。
initialSize 参数指定的是初始尺寸,它的 fullSize 参数提供了组件的尺寸,让我们可以基于组件的尺寸来设置初始尺寸。
// 定义收缩退出的效果
@Stable
public fun shrinkOut(
animationSpec: FiniteAnimationSpec<IntSize> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntSize.VisibilityThreshold
),
shrinkTowards: Alignment = Alignment.BottomEnd,
clip: Boolean = true,
targetSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
)
收缩效果也差不多,只是 targetSize 参数指定的是目标尺寸。
AnimatedVisibility(
visible = visible,
enter = expandIn(
expandFrom = Alignment.BottomEnd, // 右下角开始展开
animationSpec = tween(durationMillis = 2000),
initialSize = { IntSize(10, 10) }
),
exit = shrinkOut(
shrinkTowards = Alignment.TopStart, // 左上角开始收缩
animationSpec = tween(durationMillis = 2000),
targetSize = { IntSize(0, 0) }
)
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
)
}
运行效果:
你会发现你怎么调整展开/收缩的位置,好像动画效果都差不多,其实这是因为我们在对一个纯色方块做动画效果,你分不清它的东南西北,所以你只需用一张图片,对它来做动画效果就行了。
还是刚刚的代码,我换了一张图片:
最后一个参数 clip,它非常重要,它表示是否对内容进行裁切。
默认为 true。如果为 false,则组件的显示内容不会被裁切,会完整显示。仅仅是其占用的布局空间会按照动画的效果进行变化,所以其他元素可能会在早期覆盖组件。
AnimatedVisibility(
visible = visible,
enter = expandIn(
clip = false,
expandFrom = Alignment.TopStart, // 左上角开始展开
animationSpec = tween(durationMillis = 2000),
initialSize = { IntSize(10, 10) }
),
exit = shrinkOut(
shrinkTowards = Alignment.BottomEnd, // 右下角开始收缩
animationSpec = tween(durationMillis = 2000),
targetSize = { IntSize(0, 0) }
)
) {
Image(
painter = painterResource(id = R.drawable.girl),
contentDescription = null,
modifier = Modifier.size(100.dp)
)
}
Compose 也提供了便捷的函数:expandHorizontally()、shrinkHorizontally()、expandVertically()、shrinkVertically()。
我们可以方便地完成纵向和横向的展开/收缩动画。
缩放 (Scale)
通过改变组件的尺寸比例实现的。
// 定义放缩进入的效果
@Stable
public fun scaleIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialScale: Float = 0f,
transformOrigin: TransformOrigin = TransformOrigin.Center,
)
initialScale 参数表示初始的缩放比例,默认值为 0f。
而 transformOrigin 是缩放的原点/中心,从哪里开始缩放,默认值是中心点 TransformOrigin.Center。
// 定义缩放离开的效果
@Stable
public fun scaleOut(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
targetScale: Float = 0f,
transformOrigin: TransformOrigin = TransformOrigin.Center
)
定义缩放退出的效果也是类似的配置。
特别注意:缩放改变的只是组件的显示效果,并不会改变所占用的布局空间,所以在放大时,很有可能会与其他组件重叠。如果是用改变尺寸的效果,就会将其他组件“推开”。
AnimatedVisibility(
visible = visible,
enter = scaleIn(
initialScale = 0.5f,
transformOrigin = TransformOrigin(0f, 0f)
) , // 从左上角开始,50%大小缩放进入
exit = scaleOut(
targetScale = 0f
)
) {
Image(
painter = painterResource(id = R.drawable.girl),
contentDescription = null,
modifier = Modifier.size(100.dp)
)
}
以上就是这四个维度的动画效果的配置。
组合动画效果
我们来看看为什么 + 操作符可以组合多个动画效果。
进入看看源码:
@Stable
public operator fun plus(exit: ExitTransition): ExitTransition {
return ExitTransitionImpl(
TransitionData(
fade = exit.data.fade ?: data.fade,
slide = exit.data.slide ?: data.slide,
changeSize = exit.data.changeSize ?: data.changeSize,
scale = exit.data.scale ?: data.scale,
hold = exit.data.hold || data.hold,
// `exit` after plus operator to prioritize its values on the map
effectsMap = data.effectsMap + exit.data.effectsMap
)
)
}
可以发现,当组合多个动画效果时,会进行属性的合并。动画的属性值会优先选取 + 操作符右侧的 EnterTransition 对象的属性:
如果右侧的 EnterTransition 对象的某个属性(比如 fade)不为空,就会使用右侧的该属性值;否则,才使用左侧 EnterTransition 对象的属性值。
比如我们这么填 enter 参数:
enter = fadeIn(animationSpec = spring(), initialAlpha = 0.3f)
+ fadeIn(animationSpec = tween(durationMillis = 1000), initialAlpha = 0.2f)
最终的效果,应该是第二个 fadeIn 函数产生的效果:initialAlpha 为 0.2f,animationSpec 为 tween(durationMillis = 1000)。
Transition.AnimatedVisibility
除了以上三种 AnimatedVisibility,还有一个 Transition 的扩展函数版本。
@Composable
public fun <T> Transition<T>.AnimatedVisibility(
visible: (T) -> Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
与上述的唯一区别就是,visible 参数是一个 lambda 表达式,表达式会传入 Transition 对象当前的目标状态值,我们根据该状态返回一个布尔值,来决定是否显示内容。
使用原则
最后,使用 AnimatedVisibility 有一个重要原则:
你只能往它内部填入一个 Composable 函数,这是因为它就没有设计控制多个 Composable 的显隐。如果要填多个 Composable 函数,可能会导致布局效果不可预料。
如果需要让多个组件同时具有出现和消失的动画,并且要有一定的布局(例如,在 Column 中垂直排列),你应该为每一个需要的组件包裹一层 AnimatedVisibility:
Column {
AnimatedVisibility(visible = showFirst) {
Text("First Item")
}
AnimatedVisibility(visible = showSecond) {
Text("Second Item")
}
}
而不是都挤在一块:
Column {
AnimatedVisibility(visible = show) {
Text("First Item")
Text("Second Item")
}
}
当然你可以把多个组件在 AnimatedVisibility 的内部,套上布局:
AnimatedVisibility(visible = show) {
Column {
Text("First Item")
Text("Second Item")
}
}