1. AnimatedVisibility 是什么
AnimatedVisibility
可以实现Compose组件
的显示和隐藏,并且可以指定显示/隐藏时候的动画效果。(EnterTransition/ExitTransition
)
和 animateXxxAsState、animateContentSize
、Crossfade
、AnimatedContent
这几个API一起,都是Compose
的高级别动画API,是比较易用的。
2. AnimatedVisibility 的基础使用
@Composable
fun AnimatedVisibilityPage() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var visible by remember {
mutableStateOf(true)
}
AnimatedVisibility(visible = visible) {
Image(
painter = painterResource(id = R.mipmap.photot1),
modifier = Modifier.width(300.dp),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = { visible = !visible }) {
Text(text = "显示/隐藏")
}
}
}
看上去是不是很简单,只需要在Image
外层包上AnimatedVisibility
就可以了,显示效果如下
我们点进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
){
val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
可以发现其内部调用了updateTransition
,该函数内部会返回Transition
对象。
@Composable
fun <T> updateTransition(
targetState: T,
label: String? = null
): Transition<T> {
//...省略...
}
Transition
可管理一个或多个动画作为其子项,并在多个状态之间同时运行这些动画。
这个我们后续文章会讲到,现在先知道有这样一个概念就好。
3. 入场和出场效果
enter
和exit
,用来配置入场/出场
时候的动画效果。
默认的入场效果是 fadeIn() + expandVertically()
默认的出场效果是 fadeOut() + shrinkVertically()
3.1 EnterTransition
和ExitTransition
支持的动画
enter
的参数类型是EnterTransition
,支持这些动画
fade: fadeIn
scale: scaleIn
slide: slideIn, slideInHorizontally, slideInVertically
expand: expandIn, expandHorizontally, expandVertically
exit
的参数类型是EnterTransition
fade: fadeOut
scale: scaleOut
slide: slideOut, slideOutHorizontally, slideOutVertically
shrink: shrinkOut, shrinkHorizontally, shrinkVertically
可以看到EnterTransition
和ExitTransition
支持的动画只有expand
和shrink
命名上有区别,
其他都是对应的 fadeIn
和 fadeOut
,scaleIn
和 scaleOut
,slideIn
和 slideOut
。
expand
和shrink
命名上做区分,是因为expand
就是展开
的意思,而shrink
是收缩
的意思,它们其实就是相对应的。
3.1.1 fadeIn / fadeOut
fadeIn / fadeOut
是淡出淡出的效果
我们依旧使用上面的那段代码
@Composable
fun AnimatedVisibilityPage() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var visible by remember {
mutableStateOf(true)
}
AnimatedVisibility(visible = visible) {
Image(
painter = painterResource(id = R.mipmap.photot1),
modifier = Modifier.width(300.dp),
contentDescription = null
)
}
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = { visible = !visible }) {
Text(text = "显示/隐藏")
}
}
}
先把图片部分抽取为一个Composable
函数
@Composable
private fun MyImage() {
Image(
painter = painterResource(id = R.mipmap.photot1),
modifier = Modifier.width(300.dp),
contentDescription = null
)
}
然后修改AnimatedVisibility
,配置enter
和exit
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut()
) {
MyImage()
}
效果如下所示
3.1.2 scaleIn / scaleOut
scaleIn / scaleOut
是缩放的效果
AnimatedVisibility(
visible = visible,
enter = scaleIn(),
exit = scaleOut()
) {
MyImage()
}
效果如下所示
3.1.3 slideIn / SlideOut
slideIn / SlideOut
是滑动的效果,这里进入的初始位置和退出的目标位置都设置为了(300,-150)
,所以会从右上角进入/退出
AnimatedVisibility(
visible = visible,
enter = slideIn(initialOffset = {
IntOffset(300, -150) }
),
exit = slideOut(targetOffset = {
IntOffset(300,-150) }
)
) {
MyImage()
}
效果如下所示
3.1.3.1 slideInVertically / slideOutVertically
slideInVertically / slideOutVertically
是垂直方向滑动进入/退出
AnimatedVisibility(
visible = visible,
enter = slideInVertically(),
exit = slideOutVertically()
) {
MyImage()
}
效果如下所示
3.1.3.2 slideInHorizontally / slideOutHorizontally
slideInHorizontally / slideOutHorizontally
是从横向方向滑动进入/退出
AnimatedVisibility(
visible = visible,
enter = slideInHorizontally(),
exit = slideOutHorizontally()
) {
MyImage()
}
效果如下所示
3.1.4 expandIn / shrinkOut
expandIn / shrinkOut
是展开/收缩
的效果
AnimatedVisibility(
visible = visible,
enter = expandIn(),
exit = shrinkOut()
) {
MyImage()
}
效果如下所示
3.1.4.1 expandVertically / shrinkVertically
expandVertically / shrinkVertically
是从垂直方向展开/收缩
AnimatedVisibility(
visible = visible,
enter = expandVertically(),
exit = shrinkVertically()
) {
MyImage()
}
效果如下所示
3.1.4.2 expandHorizontally / shrinkHorizontally
expandHorizontally / shrinkHorizontally
是从横向方向展开/收缩
AnimatedVisibility(
visible = visible,
enter = expandHorizontally(),
exit = shrinkHorizontally()
) {
MyImage()
}
效果如下所示
3.1.5 EnterTransition
和ExitTransition
的源码
再来看下源码,入场动画EnterTransition
和出场动画ExitTransition
内部都有TransitionData
变量
sealed class EnterTransition {
internal abstract val data: TransitionData
//...
}
sealed class ExitTransition {
internal abstract val data: TransitionData
//...
}
TransitionData
即是可配置的动画参数,分别对应fade
、slide
、expand/shrink
、scale
internal data class TransitionData(
val fade: Fade? = null,
val slide: Slide? = null,
val changeSize: ChangeSize? = null,
val scale: Scale? = null
)
我们可以发现 EnterTransition 和 ExitTransition 是 sealed class,密封类
其子类可以出现在定义 sealed class 的不同文件中,但不允许出现在与不同的 module 中,且需要保证 package 一致
这样既可以避免 sealed class 文件过于庞大,又可以确保第三方库无法扩展你定义的 sealed class,达到限制类的扩展目的
3.2 +
号的作用
AnimatedVisibility
源码的部分,入场(enter
)和出场(exit
)的配置,使用了+
,这个加号是用来做什么的呢 ?
首先,+
号是Kotlin
的一个特性 : 重载运算符
我们点击这个+
号,就可以跳转到它的源码
@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
)
)
}
如果data
不为空,就用data
的值,否则用enter/exit
的。
这里的data
就是EnterTransition
和ExitTransition
中的那个变量internal abstract val data: TransitionData
(见 3.1 EnterTransition
和ExitTransition
支持的动画部分)
所以fadeIn() + expandVertically()
,fadeIn()
会赋值给TransitionData
的fade
,expandVertically
会赋值给changeSize
即 : 合并各个动画的效果
3.2.1 两个相同的动画会有什么效果
如果是两个相同的动画,比如fadeIn(initialAlpha = 0.3f) + fadeIn(initialAlpha = 0.5f)
根据源码中的这个规则 如果data不为空,就用data的值,否则用enter/exit的。
可知 :
两个fade
,会重叠了,导致后面那部分不会生效,等同于fadeIn(initialAlpha = 0.3f)
,+
号左边优先级高
3.3 多种动画效果结合
我们来尝试下多种动画效果,使用+
号合并之后的效果
val density = LocalDensity.current
AnimatedVisibility(visible = visible,
enter = slideInVertically {
//从顶部-200dp的位置开始滑入
with(density) { -200.dp.roundToPx() }
} + expandHorizontally(
//展开位置
expandFrom = Alignment.End
) + fadeIn(
//从初始透明度0.3f开始淡入
initialAlpha = 0.3f
),
exit = slideOutHorizontally() + shrinkHorizontally() + fadeOut()
) {
MyImage()
}
效果如下所示
4. 不同的作用域
4.1 AnimatedVisibility的作用域
AnimatedVisibility
有好几种作用域,区别在于在这几种布局中,默认的入场动画
和出场动画
是不同的
Column
默认的出入场动画具有垂直展开
,而Row
的出入场动画具有横向展开
,Transition
和无前缀
的出入场动画是展开/收缩
。
Column
fun ColumnScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandVertically(),
exit: ExitTransition = fadeOut() + shrinkVertically(),
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
)
Row
fun RowScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandHorizontally(),
exit: ExitTransition = fadeOut() + shrinkHorizontally(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
Transition
@ExperimentalAnimationApi //实验的动画API
@Composable
fun <T> Transition<T>.AnimatedVisibility(
visible: (T) -> Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
content: @Composable() AnimatedVisibilityScope.() -> Unit
) = AnimatedEnterExitImpl(this, visible, modifier, enter, exit, content)
无前缀
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
4.2 使用不了AnimatedVisibility
如果我们在Column
里面有个Box
,Box
里面又有AnimatedVisibility
,会发现AnimatedVisibility
会报错
我们把鼠标移到这个报红的地方,可以看到如下的提示
这边提示
'fun ColumnScope.AnimatedVisibility(visible: Boolean, modifier: Modifier = ..., enter: EnterTransition = ..., exit: ExitTransition = ..., label: String = ..., content: AnimatedVisibilityScope.() -> Unit): Unit' can't be called in this context by implicit receiver. Use the explicit one if necessary
注意最后一句
也就是说,不能使用隐式调用,而必须得用显式的。因为AnimatedVisibility
有好几种作用域,ColumnScope
和全局的作用域,IDE
不知道该引用哪个了。
我们可以显示添加this@Column.
,这样,就会引用Column
的那个AnimatedVisibility
了
当然也可以包装一层Compose
函数,使用全局作用域的AnimatedVisibility
这两种都是可行的
5. 其他
Compose 动画系列,后续持续更新,可以先关注
Compose 动画 (一) : animateXxxAsState 实现放大/缩小/渐变等效果
Compose 动画 (二) : 为什么animateDpAsState要用val ? MutableState和State有什么区别 ?