我正在参加「掘金·启航计划」。
话不多说,今天继续来学习 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动画初探》里的说法,默认动画也应该是「渐显+从右下角扩展进入」了?来看看效果:
如预期!
虽然有多个重载且不同重载下的动画效果也不同,但其实际影响也不大,因为参数是可控的嘛,比如,你要在 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 同样可以「渐显+从右下角扩展进入」:
所以说,如果有动画的效果要求,就要注意不同 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 标志控制。来看看效果:
动画如预期,且 isIdle 状态也正常工作,看起来就像动画结束的回调监听一样。
小结
今天讨论了普通的 AnimatedVisibility 及其相应的重载,应该都学会怎么用了吧? AnimatedVisibility 还有一个版本,是 Transition 的扩展:Transition<T>.AnimatedVisibility(),留到下一篇继续吧!