Jetpack Compose 动画5——AnimatedVisibility

761 阅读5分钟

Jetpack Compose——AnimatedVisibility

AnimatedVisibility 用于制作可见性动画。

AnimatedVisibility.gif
BoxWithConstraints(Modifier.fillMaxSize()) {
    var visible by remember { mutableStateOf(true) }
    AnimatedVisibility(visible) {
        Image(
            painter = painterResource(R.drawable.compose),
            modifier = Modifier.size(200.dp),
            contentDescription = null
        )
    }

    Button(
        modifier = Modifier
        .align(Alignment.TopEnd)
        .padding(16.dp),
        onClick = { visible = !visible }
    ) {
        Text(text = if (visible) "Hide" else "Show")
    }
}

使用非常简单,调用 AnimatedVisibility(visable = ...){ ... } 传入一个 Boolean 状态和内容,当状态改变时,content 就会动画显现或消失。

@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)
    ...
}

从源码可以看到默认入场动画是淡入 fadeIn + 展开 expandIn,默认出场动画是淡出 fadeOut + 缩小 shrinkOut,而且 AnimatedVisibility 的底层实现是 Transition

EnterTransition / ExitTransition

想要对入场出场动画做更多的定制,无疑就是通过参数 enterexit 传入想要的效果了。

入场动画的类型是 EnterTransition,EnterTransition 是一个密封类且仅有一个子类 EnterTransitionImpl

EnterTransition.jpg
private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()

EnterTransitionImpl 只有一个单参数构造函数,参数类型是 TransitionData,再点进去看看

internal data class TransitionData(
    val fade: Fade? = null,
    val slide: Slide? = null,
    val changeSize: ChangeSize? = null,
    val scale: Scale? = null
)

看起来是可见性动画的配置类,可配置的角度有 4 个:

  • fade 淡化
  • slide 滑动
  • changSize 大小改变
  • scale 缩放

密封类 EnterTransition 是无法创建实例的,而它的唯一子类 EnterTransitionImpl 又是 private class,也就是说我们无法手动创建一个 EnterTransition 实例,不仅如此,连 class TransitionData 都不是 public 的... 那怎么来配置可见性动画啊?

前面源码可以看到默认入场动画是这么写的: enter: EnterTransition = fadeIn() + expandIn()

  1. fadeIn() 函数返回一个内部 TransitionData 只配置了淡入的 EnterTransitionImpl;
  2. expandIn() 返回一个内部 TransitionData 只配置了 changeSize 的 EnterTransitionImpl;
  3. 两个 EnterTransitionImpl 相加,最终就得到了一个淡入、展开的入场可见性动画。
fadeIn-expandIn.jpg

两个对象可以用 "+" 来相加,这是 Kotlin 运算符重载功能,这里不过多赘述。

Fade

Fade 用于配置可见性动画的淡化,也就是透明度动画。

EnterTransitionExitTransition
↓ fadeIn()↓ fadeOut()
fade out animation转存失败,建议直接上传图片文件

fadeIn() / fadeOut()

fun fadeIn(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    initialAlpha: Float = 0f
): EnterTransition {
    return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}

fadeIn() 函数参数初始透明度 initialAlpha 默认值是 0,也就是说淡入的时候,默认透明度是从 0 变为 1 的。

如果我们将入场动画设置为 fadeIn(initialAlpha = 0.5f) + expandIn(),那么在淡入的时候,透明度就会从 0.5 开始变化。

AnimatedVisibility(
	visible = ...,
	enter = fadeIn(initialAlpha = 0.5f) + expandIn()
){
	...
}

当然,还可以利用 fadeIn()animationSpec 参数来对动画过程做更详细的配置,例如动画时间、缓动曲线,只需传入一个 FiniteAnimationSpec<T> 实例。

fadeOut()fadeIn() 类似,只不过是淡出,透明度从 1 变为 0,不再赘述。

Scale

Scale 用于配置可见性动画的缩放。

EnterTransitionExitTransition
↓ scaleIn()↓ scaleOut()
scale out animation转存失败,建议直接上传图片文件

scaleIn() / scaleOut()

fun scaleIn(
    animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
    initialScale: Float = 0f,
    transformOrigin: TransformOrigin = TransformOrigin.Center,
): EnterTransition {
    return EnterTransitionImpl(
        TransitionData(scale = Scale(initialScale, transformOrigin, animationSpec))
    )
}
  • animationSpec 用于配置缩放动画的规格;
  • initialScale 用于配置缩放的初始值,因为 scaleIn() 是入场动画,默认值就是 0f;
  • transformOrigin 用于配置缩放的中心,默认是 TransformOrigin.Center

scaleOut()scaleIn() 类似,不再赘述。

Slide

Slide 用于为可见性动画配置滑入/滑出效果。

fun slideIn(
    animationSpec: FiniteAnimationSpec<IntOffset> =
        spring(
            stiffness = Spring.StiffnessMediumLow,
            visibilityThreshold = IntOffset.VisibilityThreshold
        ),
    initialOffset: (fullSize: IntSize) -> IntOffset,
): EnterTransition {
    return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
}

slideIn() 函数有一个必填参数 initialOffset,用于指定内容滑入前的偏移位置。例如要对一个正方形图案做滑入的可见性动画,可能是从左上角滑入,也可能从右上角滑入,滑入前的位置就是通过参数 initialOffset 来指定的,这是一个函数参数,接收一个 fullSize: IntSize,这个 fullSize 就是内容的尺寸,返回的 IntOffset 就是滑入前的偏移位置。

滑入.jpg

其他几个 slideXxx() 函数都类似,不再赘述。

EnterTransitionExitTransition
↓ slideIn()↓ slideOut()
↓ slideInHorizontally()↓ slideOutHorizontally
↓ slideInVertically()↓ slideOutVertically()

ChangSize

ChangeSize 配置的是可见性动画的大小变化。

不过并没有 changSizeInchangSizeSmall ...这些函数,用于创建仅有大小变化的可见性动画的函数是 expandXxxshrinkXxx,expand 是“展开”的意思,而 shrink 是缩小的意思。

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) },
): EnterTransition {
    return EnterTransitionImpl(
        TransitionData(
            changeSize = ChangeSize(expandFrom, initialSize, animationSpec, clip)
        )
    )
}

当你尝试将入场动画设置为 expandIn() 时可能会感到疑惑,expandIn() 的参数 expandFrom 默认值不是 Alignment.BottomEnd 吗?怎么是从左上角进来的啊

Box {
    AnimatedVisibility(
        enter = expandIn(),
        ...
    ) {
        Image(...)
    }
}
为什么expandIn默认从左上角出来.gif

是这样的,所谓的尺寸大小变化,是靠“剪”出来的。expandFrom = Alignment.BottomEnd 的意思就是说从右下角开始剪:

expand剪出来的.jpg

因为剪出来的部分每次都贴到 Box 的左上角,看起来自然就像是从左上角展开的了。当然,剪或不剪可以自由选择,expandIn() 函数有一个 clip: Boolean 参数,默认值为 true。如果将上面的例子设置成 clip = false,那么大小范围外的内容也一样会被绘制出来

clip=false.jpg

因为例子里的图片位于 Box 的左上角,所以看不出区别,我们先把图片放在屏幕中央,再来观察 clip 参数设置为 truefalse 时的差别。

clipTrueAndFalse.gif

最后一个参数 initialSize 相信不用解释了,和 slideXxx() 函数里面的参数一样。

EnterTransitionExitTransition
↓ expandIn()↓ shrinkOut()
↓ expandHorizontally()↓ shrinkHorizontally()
↓ expandVertically()↓ shrinkVertically()