手把手教你写 Compose 动画 -- 显示与消失 API:AnimatedVisibility
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
)
原来如此,扩展函数,内部各自指定了默认的 enter
和 exit
动画效果。
除了不同容器搞了各自的 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)
}
现在我们来看看 EnterTransition
和 ExitTransition
的含义和具体用法。
📓 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)))
}
它有两个参数:
- animationSpec:我们可以给它设定 FiniteAnimationSpec 类型动画,比如:Tween()、SpringSpec() 等,默认是弹簧效果
- 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)))
}
它有两个参数:
- animationSpec:我们可以给它设定 FiniteAnimationSpec 类型动画,比如:Tween()、SpringSpec() 等,默认是弹簧效果
- 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))
)
}
animationSpec
和 initialScale
就不看了,你也应该知道是干嘛的了,不做演示了,我们只看下 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()
、slideOut
、scaleInOut
,而 expandIn()
对应的是:shrinkOut
,只是改了个名字而已,展开
-> 收缩
。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!