Compose动画:AnimatedVisibility

1,602 阅读3分钟

我正在参加「掘金·启航计划」。

话不多说,今天继续来学习 AnimatedVisibility

不同 scope 下的 AnimatedVisibility

在《Compose动画初探》中,我们发现一个现象:不同父布局下,使用到的 AnimatedVisibility,并不相同。这些 AnimatedVisibility 用到了不同的「接受者」,比如说,ColumnScope.AnimatedVisibility() 在 Column 中使用,而RowScope.AnimatedVisibility() 则是在 Row 中使用 —— 不同的重载版本,动画效果也是不同的。

那么,什么情况下才会调用「无接受者」的默认 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)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

如上,可以看出其原型和 ColumnScope 或 RowScope 的版本对比,差异就在于两点:

  • 有无接受者
  • enter 和 exit 的默认动画

而除了 ColumnScope 或 RowScope 的版本,也没看到其他接受者的版本,那是不是意思是:只要是非 Column 和 Row,其 scope 下用的就是这个默认的 AnimatedVisibility呢?

来个实验看看:

@Composable
fun Greeting2(name: String) {
    val visible = remember { mutableStateOf(false) }
    // 这里,container改成Box
    Box {
        Button(onClick = { visible.value = !visible.value }) {
            Text(text = if (visible.value) "隐藏" else "显示")
        }
        AnimatedVisibility(visible = visible.value) {
            Text(
                text = "Hello $name!", modifier = Modifier
                    .padding(top = 40.dp)
                    .size(100.dp)
                    .background(Color.Gray), textAlign = TextAlign.Center
            )
        }
    }
}

果然,父布局改成 Box 后,AnimatedVisibility() 调用的就是上述默认的版本了。

按《Compose动画初探》里的说法,默认动画也应该是「渐显+从右下角扩展进入」了?来看看效果:

device-2022-10-13-102908.gif

如预期!

虽然有多个重载且不同重载下的动画效果也不同,但其实际影响也不大,因为参数是可控的嘛,比如,你要在 ColumnScope 下使用默认无接受者的动画效果,或者自己定义的效果,覆盖一下动画参数就行了。

@Composable
fun Greeting(name: String) {
    val visible = remember { mutableStateOf(false) }
    Column {
        Button(onClick = { visible.value = !visible.value }) {
            Text(text = if (visible.value) "隐藏" else "显示")
        }
        // 覆盖 enter 和 exit 的动画
        AnimatedVisibility(visible.value, enter = fadeIn() + expandIn(), exit = shrinkOut() + fadeOut()) {
            Text(text = "Hello $name!", modifier = Modifier
                .size(100.dp)
                .background(Color.Gray), textAlign = TextAlign.Center)
        }
    }
}

如上,Column 下的 AnimatedVisibility 同样可以「渐显+从右下角扩展进入」:

device-2022-10-13-105344.gif

所以说,如果有动画的效果要求,就要注意不同 scope 下的默认动画差异;如果全部自定义,那就无所谓,因为都会覆盖掉。

MutableTransitionState 控制

除了直接指定 visible 状态的版本,AnimatedVisibility 还有传入 MutableTransitionState 对象的版本:

@Composable
fun AnimatedVisibility(
    visibleState: MutableTransitionState<Boolean>,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = fadeOut() + shrinkOut(),
    label: String = "AnimatedVisibility",
    content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
    val transition = updateTransition(visibleState, label)
    AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}

mutable + transition + state?「可变过渡状态」?什么意思?有何用处呢?

源码剖析

这个类非常简单:

class MutableTransitionState<S>(initialState: S) {
    /**
     * 当前状态
     */
    var currentState: S by mutableStateOf(initialState)
        internal set

    /**
     * 目标状态
     */
    var targetState: S by mutableStateOf(initialState)

    /**
     * 过渡动画是否完成
     */
    val isIdle: Boolean
        get() = (currentState == targetState) && !isRunning

    // Updated from Transition
    internal var isRunning: Boolean by mutableStateOf(false)
}

可以看到,MutableTransitionState 就是一个「标志类」,标志了一个过渡动画的运行状态

AnimatedVisibility 中,指定了 state 的泛型实际类型是 Boolean,其实就是把 visible 状态包裹了下,同时,又额外增加了一些状态值。

类似地,state 参数控制的 AnimatedVisibility,也有三类:默认的,以及 Column、Row 的 scope 下的。

案例

说源码还是不够清楚,上案例:

@Composable
fun Greeting3(name: String) {
    Box {
        val state = MutableTransitionState(true)
        Button(onClick = { state.targetState = !state.targetState}) {
            Text(text = if (state.isIdle) (if (state.currentState) "隐藏" else "显示") else "动画中")
        }
        AnimatedVisibility(visibleState = state) {
            Text(
                text = "Hello $name!", modifier = Modifier
                    .padding(top = 60.dp)
                    .size(100.dp)
                    .background(Color.Gray), textAlign = TextAlign.Center
            )
        }
    }
}

动画的触发不再通过 visible 的直接控制,而是由一个 MutableTransitionState 对象来控制。点击 Button 时,取反 targetState,就相当于 visible 状态取反,这将触发动画执行。Button的状态显示,增加了一个「动画中」的描述,它由 isIdle 标志控制。来看看效果:

device-2022-10-13-114521.gif

动画如预期,且 isIdle 状态也正常工作,看起来就像动画结束的回调监听一样。

小结

今天讨论了普通的 AnimatedVisibility 及其相应的重载,应该都学会怎么用了吧? AnimatedVisibility 还有一个版本,是 Transition 的扩展:Transition<T>.AnimatedVisibility(),留到下一篇继续吧!