手把手教你写 Compose 动画 -- 显示与消失 API:AnimatedVisibility

2023-12-13 13:52:35

AnimatedVisibility 可组合项可为内容的出现和消失添加动画效果。


老样子,先来一段简单代码示例,然后再慢慢引入 AnimatedVisibility 的用法。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var shown by remember { mutableStateOf(true) }

            Column (
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Spacer(modifier = Modifier.height(20.dp))
                if (shown) {
                    Image(
                        painter = painterResource(R.drawable.cr7),
                        contentDescription = null,
                        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
                    )
                }

                Button(
                    onClick = { shown = !shown}
                ) {
                    Text(text = "控制图片显示/消失")
                }
            }
        }
    }
}

这段代码极其简单,一个图片,一个按钮,一个 shown 状态变量控制图片显示与否。

在这里插入图片描述

从动画效果看确实不行,显示与消失都很粗糙。


📓 AnimatedVisibility


现在我们来用 AnimatedVisibility 改善效果,写法很简单:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var shown by remember { mutableStateOf(true) }

            Column (
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Spacer(modifier = Modifier.height(20.dp))
                AnimatedVisibility (shown) {
                    Image(
                        painter = painterResource(R.drawable.cr7),
                        contentDescription = null,
                        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
                    )
                }

                Button(
                    onClick = { shown = !shown}
                ) {
                    Text(text = "控制图片显示/消失")
                }
            }
        }
    }
}

我只做了一件事情:

if (shown) {
// 只是把 if 改成了 AnimatedVisibility
AnimatedVisibility (shown)

看下效果:

在这里插入图片描述

这里有个很有意思的事情,如果我把布局从 Column 改成 Row(代码我就不贴了),再看下效果:

在这里插入图片描述

不同布局方式,动画效果还不一样了?Column:上下,Row:左右

我们看下 AnimatedVisibility 函数:

Column -> 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
)

Row -> AnimatedVisibility

@Composable
fun RowScope.AnimatedVisibility(
    visible: Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandHorizontally(),
    exit: ExitTransition = fadeOut() + shrinkHorizontally(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
)

原来如此,扩展函数,内部各自指定了默认的 enterexit 动画效果。

除了不同容器搞了各自的 AnimatedVisibility() 扩展函数外,对于任何一个组件,我们都可以单独用 AnimatedVisibility()。

@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) // 它也是基于 updateTransition 做的
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

现在我们来看看 EnterTransitionExitTransition 的含义和具体用法。


📓 EnterTransition


EnterTransition 是一个密封类:

@Immutable
sealed class EnterTransition

它的子实现类是 EnterTransitionImpl:

@Immutable
private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()

然而 EnterTransitionImpl 是私有的,Compose 提供了几个现场的函数来创建 EnterTransitionImpl。

fadeIn():淡入效果

AnimatedVisibility (shown, enter = fadeIn()) {
    Image(
        painter = painterResource(R.drawable.pingtouge),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

我们自定义了 AnimatedVisibility 的 enter 参数,很简单就一个 fadeIn,看下效果:

在这里插入图片描述

现在来看下 fadeIn() 函数:

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

它有两个参数:

  1. animationSpec:我们可以给它设定 FiniteAnimationSpec 类型动画,比如:Tween()、SpringSpec() 等,默认是弹簧效果
  2. initialAlpha:我们也可以给它定制初始透明度

下面来测试一下:

AnimatedVisibility (shown, enter = fadeIn(tween(3000), initialAlpha = 0.3f)) {
    Image(
        painter = painterResource(R.drawable.pingtouge),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

我们分别加了一个 tween 动画(动画时长 3s),然后又加了一个初始 0.3f 的透明度,看下效果:

在这里插入图片描述

slideIn():滑入效果

AnimatedVisibility (shown, enter = slideIn {  }) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

可以发现,slideIn 后面接了一个 lambda 表达式,我们看下函数定义:

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

它有两个参数:

  1. animationSpec:我们可以给它设定 FiniteAnimationSpec 类型动画,比如:Tween()、SpringSpec() 等,默认是弹簧效果
  2. initialOffset:需要给它指定一个初始偏移,是一个 IntOffset 类型

现在我们修改代码:

AnimatedVisibility (shown, enter = slideIn { IntOffset(-200, -200) }) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

设定 IntOffset(-200, -200),相当于让图片从左上角滑入(x 轴左偏移 200 像素,y轴上偏移 200 像素)。

在这里插入图片描述

如果你够细心的话,会发现:

initialOffset: (fullSize: IntSize) -> IntOffset

默认已经提供了一个 fullSize 值,这个 fullSize 值包含了图片的宽、高尺寸,所以我们可以按照图片的宽、高进行滑入:

AnimatedVisibility (shown, enter = slideIn { IntOffset(-it.width, -it.height) }) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

另外,Compose 还提供了 slideInHorizontally 和 slideInVertically 两个横行和纵向滑入的函数,使用也很简单。

在这里插入图片描述

slideInHorizontally:横行滑入

AnimatedVisibility (shown, enter = slideInHorizontally { -it }) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

slideInVertically:纵向滑入

AnimatedVisibility (shown, enter = slideInVertically { -it }) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

expandIn():裁切效果

我们先来看下它的函数定义:

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

跟 slidIn 差不多,它有一个 initialSize 参数,不过有个默认实现,所以我们可以不必非要传这个 lambda 表达式:

AnimatedVisibility (shown, enter = expandIn()) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

直接写 expandIn 即可,为了看到更细的效果,我们添加一个 tween 的动画曲线(5s),看下效果:

在这里插入图片描述

expandIn() 函数内部有一个 expandFrom 参数,控制裁切动画是从哪个方位开始,比如默认是 Alignment.BottomEnd (右下角),Alignment.TopStart(左上角)。

我们试试左上角的效果:

AnimatedVisibility (shown,
    enter = expandIn(tween(5000), expandFrom = Alignment.TopStart)) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

另外,我们再说会 initialSize,前面我们说了它有默认实现,但我们可以进行定制:

AnimatedVisibility (shown,
    enter = expandIn(
        tween(5000),
        expandFrom = Alignment.TopStart) {
        IntSize (it.width / 2, it.height / 2)
    }) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

我们给设定了一个裁剪初始大小值:图片的一半。

在这里插入图片描述

现在就剩下一个 clip 参数:它负责是否裁切,默认为 true,如果设置为 false,则动画只位移,不裁切。

AnimatedVisibility (shown,
    enter = expandIn(
        tween(5000),
        expandFrom = Alignment.TopStart,
        clip = false) {
        IntSize (it.width / 2, it.height / 2)
    }) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

另外,跟 slideIn 一样,Compose 也提供了 expandHorizontally 和 expandVertically 两个横行和纵向裁切的函数。

在这里插入图片描述

expandHorizontally:横向裁切

AnimatedVisibility (shown, enter = expandHorizontally()) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

expandVertically:纵向裁切

AnimatedVisibility (shown, enter = expandVertically()) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

scaleIn():缩放效果

代码写起来很简单:

AnimatedVisibility (shown, enter = scaleIn(tween(3000))) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

来看下函数的定义:

@Stable
@ExperimentalAnimationApi
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))
    )
}

animationSpecinitialScale 就不看了,你也应该知道是干嘛的了,不做演示了,我们只看下 transformOrigin 参数。

transformOrigin 主要是控制缩放的效果的,默认是从中心点开始缩放,我们可以自行定制,比如:

左上角

AnimatedVisibility (shown, enter = scaleIn(
    tween(3000),
    transformOrigin = TransformOrigin(0f, 0f)
)) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

右下角

AnimatedVisibility (shown, enter = scaleIn(
    tween(3000),
    transformOrigin = TransformOrigin(1.0f, 1.0f)
)) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

在这里插入图片描述

📓 + 号原理

EnterTransition 所有的内容全部讲完了,接下来我们看看 + 的原理,是如何把两个动画结合在一起的。

这是我们文章最开始的代码示例:

@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) // 它也是基于 updateTransition 做的
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

我们来看看 “+” 号做了什么:

sealed class EnterTransition {
    internal abstract val data: TransitionData

    @Stable
    operator fun plus(enter: EnterTransition): EnterTransition {
        return EnterTransitionImpl(
            TransitionData(
                fade = data.fade ?: enter.data.fade,
                slide = data.slide ?: enter.data.slide,
                changeSize = data.changeSize ?: enter.data.changeSize,
                scale = data.scale ?: enter.data.scale
            )
        )
    }

}

熟悉 Kotlin 的话,对 plus 操作符肯定不默认,这里内部对左右两个 TransitionDate 会做合并,不过合并的规则比较特殊。

比如 fade 淡入的效果:

enter: EnterTransition = fadeIn() + expandIn(),

// 合并工作

fade = data.fade ?: enter.data.fade,

会判断左边的 TransitionData 是否有 fade?-- 如果有了就只用左边的 fade,而右边的 TransitionData 即使有 fade,也不会合并,直接去除。

如果你的代码这么写:

AnimatedVisibility (shown,
    enter = fadeIn(initialAlpha = 0.3f) + fadeIn(initialAlpha = 0.5f)
) {
    Image(
        painter = painterResource(R.drawable.cr7),
        contentDescription = null,
        modifier = Modifier.size(90.dp).clip(shape = CircleShape)
    )
}

那么只会应用左边的 0.3 透明度,右边的 0.5 透明度直接抛弃。

📓 ExitTransition

讲完 EnterTransition,对于 ExitTransition 的原理就不用细说了,和 EnterTransition 一样,只需要把对应的 fadeIn()slideIn()expandIn()scaleIn() 的函数名中的 In 改成 out 即可。

不过有个另类,对于 fadeIn()slideIn()scaleIn(),他们对应的 ExitTransition 是:fadeOut()slideOutscaleInOut,而 expandIn() 对应的是:shrinkOut,只是改了个名字而已,展开 -> 收缩

文章来源:https://blog.csdn.net/pepsimaxin/article/details/134955185
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。