前语
本篇是 Android Compose 动画的第十二篇,本篇介绍一个新的动画组件 AnimatedVisibility
,AnimatedVisibility
是一个容器类的 Composable,效果是在组件的可见性情况之间添加动画效果。当组件从可见情况到不可见情况或从不可见情况到可见情况转化时,AnimatedVisibility 会主动为组件添加动画效果,使组件在情况之间滑润地过渡。
根底运用
前面说了 AnimatedVisibility
是一个容器类的 Composable 组件,其内容就是我们要控制闪现躲藏的 UI 组件,需接收一个 Boolean 类型的 visible
参数用于控制 content 的闪现躲藏。
运用代码如下:
var shown by remember { mutableStateOf(true) }
Box{
AnimatedVisibility(visible = shown) {
Box(
Modifier
.size(100.dp, 100.dp)
.background(Color.Blue)
)
}
Button(onClick = {
// 改动闪现躲藏情况
shown = !shown
}, Modifier.padding(top = 100.dp)) {
Text(text = "Switch", style = TextStyle(fontSize = 10.sp))
}
}
工作看一下效果:
当我们通过点击按钮控制 shown 的 true/false 时,AnimatedVisibility 中包裹的控件就会呈现闪现/躲藏的过渡动画效果。
上面的 AnimatedVisibility 是包裹在 Box 中的,默许效果是从左上角突变翻开或突变缩回去,假设是在 Column 或 Row 中运用则默许效果也会不相同,将上面的代码最外层的 Box 换成 Column 和 Row 分别看一下效果,修改后的代码如下:
var shown by remember { mutableStateOf(true) }
// AnimatedVisibility 放在 Column 中
Column{
AnimatedVisibility(visible = shown) {
Box(...)
}
Button(..)
}
// AnimatedVisibility 放在 Row 中
Row {
AnimatedVisibility(visible = shown) {
Box(...)
}
Button(...)
}
跟之前的代码仅有的改动就是将外面的 Box 分别换成了 Column 和 Row ,下面分别看一下工作效果:
![Android Compose 动画运用详解(十二)AnimatedVisibility动画的运用 Android Compose 动画运用详解(十二)AnimatedVisibility动画的运用](https://www.6hu.cc/storage/2023/05/1683307994-6218e5c6751bcd3.gif)
可以发现,在 Column 中由躲藏到实践的效果是从上方向下翻开,在 Row 中的效果是从左边向右边翻开。为什么运用相同的控件放在不同的容器中展现的效果不相同呢?我们持续向下探求,答案一瞬间揭晓。
自定义动画
上面讲了 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
)
总共有六个参数:
- visible:用于控制内容是否可见
- modifier:通用 Modifier ,用于修饰控件
- enter:进入动画,即控件从躲藏到实践的动画
- exit:退出动画,即控件从闪现到躲藏的动画
- label:动画标签,跟上一篇介绍的 Transition 的 label 效果一直,用于动画预览时起标识效果
- content:内容,是一个函数类型参数,函数内需回来一个 Composable 的控件
关键就在 enter
、exit
参数上,假设需求自定义动画效果就需求传入这两个参数,这两个参数都有默许值,所以我们不传时会有默许的闪现躲藏动画效果,至于这里默许值的意义等我们详细了解了自定义动画后再回过头来看。
enter
、exit
参数的类型分别是 EnterTransition
和 ExitTransition
,我们先来看 EnterTransition
类型的源码定义:
sealed class EnterTransition {
internal abstract val data: TransitionData
@Stable
operator fun plus(enter: EnterTransition): EnterTransition {
return EnterTransitionImpl(...)
}
override fun toString(): String = ...
override fun equals(other: Any?): Boolean {
return other is EnterTransition && other.data == data
}
override fun hashCode(): Int = data.hashCode()
companion object {
val None: EnterTransition = EnterTransitionImpl(TransitionData())
}
}
通过源码发现,EnterTransition
是一个密封内,有一个 TransitionData
类型的 data 特点和一个plus 的操作方法。既然是一个密封类,那必定存在其子类,盯梢发现其子类只需一个 EnterTransitionImpl
,其源码如下:
private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()
其完结就是传入 data ,所以要害就在这个 data 上,所以再往下看一下其类型 TransitionData
的源码:
internal data class TransitionData(
val fade: Fade? = null,
val slide: Slide? = null,
val changeSize: ChangeSize? = null,
val scale: Scale? = null
)
是一个数据类,有四个特点:
- fade: 淡入淡出动画
- slide:滑动出场、出场动画
- changeSize:改动规范大小的出场、出场动画
- scale:缩放方法的出场、出场动画
所以我们只需求分别设置对应的动画是不是就能完结自定义的动画效果了,那怎样运用呢,创立一个 EnterTransitionImpl 方针然后传入 TransitionData 么?下面这样:
AnimatedVisibility(visible = shown, enter = EnterTransitionImpl(TransitionData(...))){
...
}
实践上面这样写的话编辑器就报错了,通过上面的源码可以发现 EnterTransitionImpl
是 private 的,TransitionData
又是 internal 的,都不能直接在项目中运用,那应该怎样用呢?
Compose 对上面四种不同类型的动画分别供给了便利的调用方法,如下所示:
类型 | 出场动画 | 出场动画 |
---|---|---|
Fade | fadeIn() | fadeOut() |
Slide | slideIn() slideInHorizontally() slideInVertically() |
slideOut() slideOutHorizontally() slideOutVertically() |
ChangeSize | expandIn() expandHorizontally() expandVertically() |
shrinkOut() shrinkHorizontally() shrinkVertically() |
Scale | scaleIn() | scaleOut() |
除了 ChangeSize 外,其他都是以称号 + In/Out 指令的方法,很好区分出场与出场动画的运用,同时 Slide 和 ChangeSize 还供给了对应横向(Horizontally)、竖向(Vertically)的便利方法。 下面就详细介绍一下对应方法的运用。
fadeIn
看一下 fadeIn
方法的源码:
fun fadeIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialAlpha: Float = 0f
): EnterTransition {
return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}
有两个参数:
-
animationSpec:动画规范,FiniteAnimationSpec 类型,参阅之前动画规范的介绍,即这里能运用除了
InfiniteRepeatableSpec
无限循环动画规范之外的其他几个,假设对动画规范的装备不了解可以去看看本专栏关于动画规范的介绍 - initialAlpha:初始透明度
回来的是 EnterTransition 类型,其实就是创立了一个 EnterTransitionImpl 传入 TransitionData ,跟我们上面的写法彻底相同,只是我们不能直接调用。
运用示例:
var shown by remember { mutableStateOf(false) }
Box{
AnimatedVisibility(
visible = shown,
// 进入动画设置 fadeIn ,动画规范设置 tween 时长 1000ms,初始透明度 0.3f
enter = fadeIn(animationSpec = tween(1000), initialAlpha = 0.3f)) {
Box(...)
}
Button(onClick = {
shown = !shown
}, Modifier.padding(top = 100.dp)) {
Text(text = "Switch", style = TextStyle(fontSize = 10.sp))
}
}
工作一下看看效果:
这样动画就只需一个初始透明度为 0.3 的淡入效果
slideIn
slideIn
源码如下:
fun slideIn(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
initialOffset: (fullSize: IntSize) -> IntOffset,
): EnterTransition {
return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
}
相同是两个参数,animationSpec 动画规范就不介绍了,跟上面效果相同,首要看一下 initialOffset
即初始偏移,即动画从什么方位滑入,其类型 (fullSize: IntSize) -> IntOffset
是一个函数类型参数,传入 fullSize 是 IntSize
参数,回来 IntOffset
,其间 fullSize
即为控件的大小,所以可以根据控件大小来设置对应动画的偏移量,运用如下:
var shown by remember { mutableStateOf(false) }
Box(Modifier .padding(top = 100.dp, start = 100.dp)) {
// 创立 slide 出场动画
val enter = slideIn {
// 偏移为 x 往左偏移控件的宽度,y 往上偏移控件的高度
IntOffset(-it.width, -it.height)
}
AnimatedVisibility(visible = shown, enter = enter) {
Box(
Modifier
// 运用动画值
.size(100.dp, 100.dp)
.background(Color.Blue)
)
}
Button(onClick = {
shown = !shown
}, Modifier.padding(top = 100.dp)) {
Text(text = "Switch", style = TextStyle(fontSize = 10.sp))
}
}
工作效果:
这样就完结了控件从左上角滑入的效果。
Slide 除了 slideIn 外还有 slideInHorizontally、slideInVertically,即横向滑入和竖向滑入,先看一下 slideInHorizontally 的源码:
fun slideInHorizontally(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
initialOffsetX: (fullWidth: Int) -> Int = { -it / 2 },
): EnterTransition =
slideIn(
initialOffset = { IntOffset(initialOffsetX(it.width), 0) },
animationSpec = animationSpec
)
与 slideIn 方法的区别是将 initialOffset 参数换成了 initialOffsetX,即 x 轴方向的偏移,默许值是向左偏移控件宽度的一半,其完结也是调用的 slideIn 方法,只是 y 方向的偏移传入了 0,这样就完结了横向的滑动动画。
触类旁通,slideInVertically 竖向滑动动画其实就是把 x 方向的偏移设置了 0,如下:
fun slideInVertically(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
initialOffsetY: (fullHeight: Int) -> Int = { -it / 2 },
): EnterTransition =
slideIn(
initialOffset = { IntOffset(0, initialOffsetY(it.height)) },
animationSpec = animationSpec
)
实践上 slideInHorizontally、slideInVertically 就是 slideIn 方法的二次封装。
expandIn
expandIn 的源码:
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)
)
)
}
方法有四个参数,参数阐明如下:
- animationSpec:动画规范
- expandFrom:从哪里初步翻开,Alignment 类型,默许为 Alignment.BottomEnd 即右下角
- clip:是否裁剪,默许为 true 即会裁剪
- initialSize:初始大小,函数类型,传入参数 fullSize 为控件大小
运用示例:
var shown by remember { mutableStateOf(false) }
Box{
val enter = expandIn(
// 动画规范
animationSpec = tween(1000),
// 设置从哪里初步翻开
expandFrom = Alignment.BottomEnd,
// 是否裁剪
clip = true){
// 动画初始大小
IntSize(it.width/3, it.height/3)
}
AnimatedVisibility(visible = shown, enter = enter) {
Box(
Modifier
.size(100.dp, 100.dp)
.background(Color.Blue)
)
}
Button(onClick = {
shown = !shown
}, Modifier.padding(top = 100.dp)) {
Text(text = "Switch", style = TextStyle(fontSize = 10.sp))
}
}
工作效果:
咦,不对啊,expandFrom 不是设置的 Alignment.BottomEnd 么?不是应该从右下角翻开么,怎样动画效果是从左上角翻开的呢?
这是因为 expandIn 本质是 changeSize,即改动控件的大小,且是以裁剪的方法,实践上是先展现右下角,然后逐渐变大,但上面的动画的方块颜色只需一种,所以给我们的视觉效果是从左上角翻开的。我们将其内部添加不同的颜色块再看看效果:
这样就能很明显的看到动画是从右下角翻开的。
上面的代码 clip 我们设置的 true,即裁剪,假设设置为 false 不裁剪又是什么效果呢,将其修改为 false 工作看看效果:
可以发现,动画初步时控件就彻底闪现了,然后逐渐移动到了方针方位,假设以控件右下角为参阅点跟上面裁剪的动画进行比较发现动画的途径是相同的,只是整体一个是从小变大而另一个是不变的,这就是不裁剪的效果。
除了 expandIn 外,还有 expandHorizontally、expandVertically 方法,他们实践上也是对 expandIn 方法的二次封装,前者将动画初始的高度设置为控件大小即完结横向的扩展,后者将动画初始的宽度设置为控件大小即完结竖向的扩展,以 expandHorizontally 为例看一下源码的完结:
fun expandHorizontally(
animationSpec: FiniteAnimationSpec<IntSize> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntSize.VisibilityThreshold
),
expandFrom: Alignment.Horizontal = Alignment.End,
clip: Boolean = true,
// 只需求设置初始宽度
initialWidth: (fullWidth: Int) -> Int = { 0 },
): EnterTransition {
// 调用 expandIn 方法
return expandIn(animationSpec, expandFrom.toAlignment(), clip = clip) {
// 初始宽度运用传入的 initialWidth ,初始高度为控件高度
IntSize(initialWidth(it.width), it.height)
}
}
scaleIn
相同先来看一下 scaleIn 的源码:
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:初始缩放
- transformOrigin:缩放的原点,默许为 TransformOrigin.Center 即控件的中心点
运用示例:
var shown by remember { mutableStateOf(false) }
Box{
val enter = scaleIn(
animationSpec = tween(1000),
initialScale = 0.3f,
transformOrigin = TransformOrigin.Center)
AnimatedVisibility(visible = shown, enter = enter) {
Box(
Modifier
// 运用动画值
.size(100.dp, 100.dp)
.background(Color.Blue)
)
}
Button(onClick = {
shown = !shown
}, Modifier.padding(top = 100.dp)) {
Text(text = "Switch", style = TextStyle(fontSize = 10.sp))
}
}
工作效果:
因为我们 transformOrigin 设置的 TransformOrigin.Center
所以缩放是从中心初步变大的,假设要设置从右下角放大该怎样设置呢?是不是应该设置 TransformOrigin.BottomEnd
啊?但实践并没有这个值,我们来看一下 TransformOrigin.Center
的定义:
val Center = TransformOrigin(0.5f, 0.5f)
其实 TransformOrigin.Center
就是创立了一个 TransformOrigin
方针,分别传入了 0.5f,实践就是宽高的一半,假设要设置到右下角,只需传入 TransformOrigin(1.0f, 1.0f)
即可,如下:
val enter = scaleIn(
animationSpec = tween(1000),
initialScale = 0.3f,
// 缩放原点设置到右下角
transformOrigin = TransformOrigin(1.0f, 1.0f))
工作效果:
出场动画的运用和参数与出场动画一致,学会了出场动画的运用相信你对出场动画的运用必定没有问题
组合
前面介绍的都是单个动画的效果,假设我即想有淡入动画又想有翻开动画呢,应该怎样设置呢?我们再回到最初步的 AnimatedVisibility
的定义:
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
)
发现不管是出场动画仍是出场动画默许设置有两个动画且是以 + 联接,按住ctrl + '+'
点进去看看源码 :
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
)
)
}
实践上就是我们之前看到的 EnterTransition
/ExitTransition
中的 plus 操作方法,通过剖析源码,其完结是判断其时方针(’+’ 左边的)的对应动画是否存在,存在就用其时方针的,不存在就运用传入方针(’+’ 右边的)的。 所以默许的出场动画效果为淡入和翻开的组合动画。
通过这种方法就将多个动画组合起来了,假设我们四个动画都设置则可以如下这么写:
AnimatedVisibility(
visible = shown,
enter = fadeIn()+ slideIn(initialOffset = { IntOffset(it.width, it.height) }) + expandIn() + scaleIn()
)
再回到最初步的疑问,为什么在 Box 、Column、Row 中运用 AnimatedVisibility
但是效果却是不相同的呢?
我们分别从 Box、Column、Row 中的AnimatedVisibility
点进去看看源码,Box 里的我们上面现已看过了,看看其他两个的源码:
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)
}
fun RowScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandHorizontally(),
exit: ExitTransition = fadeOut() + shrinkHorizontally(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
发现虽然都叫 AnimatedVisibility
但实践并不是同一个方法,Column 里的是 ColumnScope.AnimatedVisibility
,Row 中的 RowScope.AnimatedVisibility
,即分别是 ColumnScope
、RowScope
的扩展,而 Column、Row 的 content 参数是分别根据 ColumnScope
、RowScope
调用的,如下:
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
)
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
所以在 Column、Row 中运用 AnimatedVisibility
时会分别调用到对应的ColumnScope
、RowScope
扩展的 AnimatedVisibility
方法,而 Compose 对其动画分别进行了独自设置,比方 Column 中的 enter 动画: fadeIn() + expandVertically()
即淡入和竖向翻开。这样就完结了不同的容器中运用默许的效果不相同。
终究
本篇详细介绍了 Android Compose 动画的 AnimatedVisibility 组件的运用,通过源码解析详细介绍了 AnimatedVisibility 各个 API 的详细运用及简单的完结原理。实践上 AnimatedVisibility 还有一些不常用的 API 运用方法,因为篇幅原因将在后续文章进行弥补阐明,假设你对更多的 Compose 动画运用感兴趣,请持续关注本专栏。
本篇文章的源码地址:ComposeAnimationDemo
本文正在参加「金石计划」