我正在参加「掘金·启航计划」。
到目前为止,AnimatedVisibility 的使用及其特性,相信已经很清楚了吧?不清楚的,再找找之前的几篇看看,也可以留言,咱一起讨论讨论。
今天来谈谈 AnimatedVisibility 的 Transition。你可能会问,怎么还是 Transition?之前不是讲了吗?确实,上篇我们学习 Transition.AnimatedVisibility 扩展时,已经搞清楚了 Transition,不过呢,今天讲的是另一个东西: EnterTransition 和 ExitTransition。在讲 AnimatedVisibility 时,一直没有深入地分析这二位,但是它们却是最重要的,因为这就是动画本尊啊!
EnterTransition
EnterTransition 是一个密封类,源码如下,很简单。
@Immutable
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
)
)
}
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 重载了加法运算符,这就是各个动画的结合可以使用「+」的原因(比如默认的 enter 动画是 fadeIn() + expandIn())。
从功能上来讲,EnterTransition 用于定义 AnimatedVisibility 类型的组件在变为 visible 时使用的动画。动画共 4 类,包括:
- 渐变:fadeIn(渐入)
- 缩放:scaleIn(放大)
- 划动:slideIn(划入), slideInHorizontally(横向划入), slideInVertically(纵向划入)
- 扩展:expandIn(右下扩展入), expandHorizontally(右扩展入), expandVertically(下扩展入)
TransitionData
从前面源码看到,EnterTransition 内部有一个抽象域 data 待实现, 为TransitionData 类型,该类型是一个纯数据类。
@Immutable
internal data class TransitionData(
val fade: Fade? = null,
val slide: Slide? = null,
val changeSize: ChangeSize? = null,
val scale: Scale? = null
)
数据类包括四个数据域,是和四类动画对应的,用于存储动画参数。比如说,默认动画里用到的 fadeIn(),是这样实现的:
@Stable
fun fadeIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialAlpha: Float = 0f
): EnterTransition {
return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}
@Immutable
private class EnterTransitionImpl(override val data: TransitionData) : EnterTransition()
方法返回 EnterTransition 类型,使用 EnterTransitionImpl 构造实例,传入的参数是 Fade 对象,也就是渐变,因为初始 alpha 是 0,所以就是「渐入」动画了。
其它诸如 fadeOut()、slideIn() 等,都是类似的,加入需要关注的参数来构造 TransitionData 对象,从而得到动画数据。
我们再来看看动画结合的实现:
// ...
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
)
)
实际上,就是数据的选择:针对特定参数域,如果当前的值为 null,那就使用新加 data 里面的值,否则保持原样。所以说,已存在的特定类型的动画,无法通过叠加进行覆盖,动画叠加是作用于不同类型的动画的。比如,我们定义一个 enter 动画 anim1,「扩展+渐入」
val anim1 = remember { expandIn(tween(3_000)) + fadeIn(tween(3_000)) }
AnimatedVisibility(visible.value, enter = anim1, exit = shrinkOut(tween(3_000), Alignment.Center)) {
Text(text = "Hello${name}Hello${name}Hello${name}", modifier = Modifier
.width(width = 100.dp)
.background(Color.Gray), textAlign = TextAlign.Center)
}
我们再用另一个动画来叠加更改:
val anim1 = remember { expandIn(tween(3_000)) + fadeIn(tween(3_000)) }
// anim1 基础上,添加动画
val anim2 = remember { anim1 + expandIn() + scaleIn(tween(3_000)) }
AnimatedVisibility(visible.value, enter = anim2, exit = shrinkOut(tween(3_000), Alignment.Center)) {
// ...
anim2 在 anim1 基础上,又添加了一个「扩展」,只不过,是默认的效果,另外又加了一个「放大」。按照源码的逻辑,新的默认「扩展」是无法生效的,因为 anim1 本来就有了一个,而最终的动画将是「anim1 + scaleIn」。
效果确实如此。不过呢,调整一下顺序,也能实现「扩展」覆盖。如下:
val anim1 = remember { expandIn(tween(3_000)) + fadeIn(tween(3_000)) }
val anim2 = remember { expandIn() /*expandIn提前*/ + anim1 + scaleIn(tween(3_000)) }
AnimatedVisibility(visible.value, enter = anim2, exit = shrinkOut(tween(3_000), Alignment.Center)) {
// ...
想想为什么?
动画类型及其控制
四类动画包括了渐变、缩放、划动和扩展,分别对应的类型为:Fade, Scale, Slide 和 ChangeSize。来看看 Fade:
@Immutable
internal data class Fade(val alpha: Float, val animationSpec: FiniteAnimationSpec<Float>)
参数 alpha 存储渐变的初始值,而参数 animationSpec 是「有限动画控制参数」,为 FiniteAnimationSpec 类型。默认的 fadeIn(),使用了 spring() ,得到了一个「跳跃类型」。
其它几个均是类似的实现方式,很简单,这里不再赘述。
But!有一点需要提一下,就是关于动画时长的问题。在之前的文章里,为了方便观察动画效果,我们曾经重新设置了 animationSpec 参数,使用的是 tween() 方法(补间)。之所以改用「补间动画」,是因为 spring 是不包含时间控制的。tween() 获取一个 TweenSpec 对象,即「补间参数」。类似的,还有 KeyframesSpec 和 SnapSpec,它们均是 DurationBasedAnimationSpec 的实现类,可以控制动画时间或动画启动延时。
ExitTransition
ExitTransition 和 EnterTransition 的实现,几乎是一样的。所以,略掉。
// 和 EnterTransition 的实现几乎一样
@Immutable
sealed class ExitTransition {
internal abstract val data: TransitionData
@Stable
operator fun plus(exit: ExitTransition): ExitTransition {
return ExitTransitionImpl(
TransitionData(
fade = data.fade ?: exit.data.fade,
slide = data.slide ?: exit.data.slide,
changeSize = data.changeSize ?: exit.data.changeSize,
scale = data.scale ?: exit.data.scale
)
)
}
override fun equals(other: Any?): Boolean {
return other is ExitTransition && other.data == data
}
override fun hashCode(): Int = data.hashCode()
companion object {
val None: ExitTransition = ExitTransitionImpl(TransitionData())
}
}
「扩展」动画
单独再来讨论一下「扩展」动画,即 expandIn()、expandOut() 得到的类型。因为本人在理解它的效果的时候,就出现了很懵比不理解的状态,相信我不是唯一的吧?
@Stable
@ExperimentalAnimationApi
fun expandIn(
expandFrom: Alignment = Alignment.BottomEnd,
initialSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
animationSpec: FiniteAnimationSpec<IntSize> =
spring(visibilityThreshold = IntSize.VisibilityThreshold),
clip: Boolean = true
): EnterTransition {
return EnterTransitionImpl(
TransitionData(
changeSize = ChangeSize(expandFrom, initialSize, animationSpec, clip)
)
)
}
首先,动画数据实际是由 ChangeSize 给出:
@Immutable
internal data class ChangeSize(
val alignment: Alignment,
val size: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
val animationSpec: FiniteAnimationSpec<IntSize> =
spring(visibilityThreshold = IntSize.VisibilityThreshold),
val clip: Boolean = true
)
而构造一个 ChangeSize 所需要的参数,直接来自于 expandIn 方法:
- expandFrom: 扩展边界的启动点,默认为 Alignment.BottomEnd(右下)
- initialSize: 初始尺寸
- animationSpec: 动画参数,默认为 spring
- clip: 动画边界外的内容是否剪切,默认 true
第一个参数是最难理解的。再来看看之前的例子:
@Composable
fun Greeting(name: String) {
val visible = remember { mutableStateOf(false) }
Column(Modifier.padding(start = 100.dp, top = 100.dp)) /*设置一个距离,方便观察*/ {
Button(onClick = { visible.value = !visible.value }) {
Text(text = if (visible.value) "隐藏" else "显示")
}
// 仅使用扩展动画,并调长动画时间
AnimatedVisibility(visible.value, enter = expandIn(tween(3_000)), exit = shrinkOut(tween(3_000))) {
Text(text = "Hello${name}Hello${name}Hello${name}", modifier = Modifier
.width(width = 100.dp)
.background(Color.Gray), textAlign = TextAlign.Center)
}
}
}
组件从左上角开始,朝着右下角方向「移入」。退出时,反之。那么,这和「从右下角开始扩展」是什么关联?
我们把 expandFrom 参数改成 Alignment.TopStart 试试:
效果变了:组件在放大,左上角最先显示 —— 这下就知道为什么前面的移入要加引号吧,因为其实它根本不是移入,它只是在扩展放大而已,因为它是「从内容右下角开始扩展」,所以只是看起来像是在移入!
现在,再给退出动画也修改一下启动点,设成 Alignment.Center,对于退出来讲,这个参数的意思是指:缩小边界的终点 —— 这里就是「往中间缩小」。
观察下最后的还在显示的区域,就明白意思了。
另外值得注意的是,ChangeSize 的放大缩小,是组件的大小,内容未受影响;而 Scale 的放大缩小,是实际的整个组件的等比例缩放。
小结
至此,AnimatedVisibility 涉及到的相关参数以及部分实现细节(例如 scope、transition 这些),我们已经掌握,相信也已经具备在适当场景下正确使用的能力。
有什么问题和疑问,欢迎留言讨论。下篇再见!