动画分类概览
Jetpack Compose中没有沿用Android原有的View动画和属性动画,而是新创建了一套全新的动画系统API,这是理所当然的,因为旧的动画系统主要是基于View体系的,而Compose中需要针对的是Composable可组合函数进行处理,那么势必要创造一套新的玩具出来,同时,这也无疑增加了开发者的学习成本。
乍一看Jetpack Compose中的动画Api,尼玛是真的多呀,我C了,简直令人眼花缭乱、云里雾里、天马行空、小兔乱撞、手脚慌乱、头冒虚汗、四肢抓狂、不知所措呀 。。。😭
但是我们可以对其进行分一下类,如果按照使用的方便程度划分,大概可以分为两大类:高级动画API和低级动画API(这里类比高级开发语言的分类,并不是指效果多高级)。
其中高级动画API使用比较简单方便,封装度高,更加适用于日常业务开发,而低级动画API则使用起来较为麻烦,因为其配置项或流程较多,但是却更加灵活,能对动画效果做出更加精细的控制,适合自定义要求度较高的业务场景。
我们还可以按照功能类型进行一个大概的分类,也就是上图中的划分,下面再按照需求点用表格整理一下:
功能需求点 | 可能符合的API类型 |
---|---|
◻️ 单个组件的显示隐藏转场动画 ◻️ 每个子组件需要不同的入场/出场效果 |
AnimatedVisibility |
◻️ 根据组件内容状态变化的动画(数据、尺寸等) ◻️ 不同组件间的切换动画 |
AnimatedContent Modifier.animateContentSize |
单纯的淡入淡出动画 | Crossfade |
◻️ 根据数据估值状态自动执行连续动画 ◻️ 基于单个数据值的状态变化执行动画 ◻️ 基于自定义数据类型进行估值动画 ◻️ 指定每一帧/每一时刻的动画状态 ◻️ 替代传统 View 属性动画的方案 |
animateXXXAsState |
◻️ 根据不同状态同时管理和运行多个动画 ◻️ 进入界面时自动执行一次动画 ◻️ 监听动画状态 ◻️ 替代传统 View 动画中的 AnimationSet 的方案。 |
updateTransition MutableTransitionState |
永不停止、无限循环的动画 | rememberInfiniteTransition |
◻️ 更加底层的低级动画API ◻️ 可高度自由定制的估值属性动画 ◻️ 需要在协程中执行的动画 ◻️ 需要控制一些动画并行执行 |
Animatable |
◻️ 更加底层的低级动画API ◻️ 需要手动精确控制动画的时间 ◻️ 手势动画,fling衰减动画 |
TargetBasedAnimation DecayAnimation |
高级动画API
AnimatedVisibility
AnimatedVisibility主要用于页面显示状态的动画,即显示/隐藏的过渡动画,或者入场/离场动画。
可以使用 + 运算符组合多个 EnterTransition 或 ExitTransition 对象,并且每个对象都接受可选参数以自定义其行为。
@Composable
fun AnimatedVisibilityExample() {
var visible by remember {
mutableStateOf(true) }
val density = LocalDensity.current
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically {
with(density) {
-40.dp.roundToPx() } } // 从顶部 40dp 的地方开始滑入
+ expandVertically(expandFrom = Alignment.Top) // 从顶部开始展开
+ fadeIn(initialAlpha = 0.3f), // 从初始透明度 0.3f 开始淡入
exit = slideOutVertically() + shrinkVertically() + fadeOut()
) {
Text("Hello",
Modifier.background(Color.Green).fillMaxWidth().height(200.dp)
.wrapContentWidth(Alignment.CenterHorizontally),
fontSize = 20.sp
)
}
Button(
onClick = {
visible = !visible },
modifier = Modifier.padding(top = 200.dp)
) {
Text(text = if(visible) "隐藏" else "显示")
}
}
}
运行效果:
默认情况下 EnterTransition 是 fadeIn() + expandIn() 的效果,而 ExitTransition 是 shrinkOut() + fadeOut() 的效果, Compose额外提供了RowScope.AnimatedVisibility和ColumnScope.AnimatedVisibility两个扩展方法, 当我们在Row或Column中调用时,该组件的默认动画效果会根据父容器的布局特征进行调整,比如在Row中EnterTransition默认是fadeIn + expandHorizontally组合,而在Column中EnterTransition默认是fadeIn + expandVertically组合方案。
EnterTransition 和 ExitTransition 动画分类效果示例:
EnterTransition | ExitTransition |
---|---|
FadeIn ![]() |
FadeOut ![]() |
slideIn ![]() |
slideOut ![]() |
slideInHorizontally ![]() |
slideOutHorizontally ![]() |
slideInVertically ![]() |
slideOutVertically ![]() |
scaleIn ![]() |
scaleOut ![]() |
expandIn ![]() |
shrinkOut ![]() |
expandHorizontally ![]() |
shrinkHorizontally ![]() |
expandVertically ![]() |
shrinkVertically ![]() |
为子项添加进入和退出动画效果
AnimatedVisibility 中的内容(直接或间接子项)可以使用 Modifier.animateEnterExit 修饰符为每个子项指定不同的动画行为。
其中每个子项的视觉效果均由 AnimatedVisibility 可组合项中指定的动画与子项自己的进入和退出动画构成。
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedVisibilityExample3() {
var visible by remember {
mutableStateOf(true) }
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) {
// 外层Box组件淡入淡出进出屏幕
Box(Modifier.fillMaxSize().background(Color.DarkGray)) {
Box(Modifier.align(Alignment.Center)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp).background(Color.Green)
.animateEnterExit(enter = slideInVertically(), exit = slideOutVertically())
) {
Text(text = "内层Box组件滑动进出屏幕", Modifier.align(Alignment.Center))
}
Box(Modifier.padding(top = 150.dp).align(Alignment.Center)
.sizeIn(minWidth = 256.dp, minHeight = 64.dp).background(Color.Cyan)
.animateEnterExit(enter = scaleIn(), exit = scaleOut())
) {
Text(text = "内层层Box组件缩放进出屏幕", Modifier.align(Alignment.Center))
}
}
}
Button(
onClick = {
visible = !visible },
modifier = Modifier.padding(top = 50.dp)
) {
Text(text = if(visible) "隐藏" else "显示")
}
}
}
运行效果:
有时我们希望 AnimatedVisibility 内的每个子组件有不同的过渡动画,此时请在 AnimatedVisibility 可组合项中指定 EnterTransition.None 和 ExitTransition.None,即完全不应用任何动画,这样子项就可以通过 Modifier.animateEnterExit 拥有各自的不同动画了。
自定义 Enter/Exit 动画
如果想在内置进入和退出动画之外添加自定义动画效果,请在 AnimatedVisibilityScope 内设置 transition, 添加到 Transition 实例的所有动画状态都将与 AnimatedVisibility 的进入和退出动画同时运行。
AnimatedVisibility 会等到 Transition 中的所有动画都完成后再移除其内容。对于独立于 Transition(例如使用 animate*AsState)创建的退出动画,AnimatedVisibility 将无法解释这些动画,因此可能会在完成之前移除内容可组合项。
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedVisibilityExample4() {
var visible by remember {
mutableStateOf(true) }
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
AnimatedVisibility(visible = visible, enter = scaleIn(), exit = scaleOut()) {
// 使用 AnimatedVisibilityScope#transition 添加自定义的动画与AnimatedVisibility同时执行
val background by transition.animateColor(label = "backgroundTransition") {
state ->
if (state == EnterExitState.Visible) Color.Blue else Color.Green
}
Box(modifier = Modifier.size(100.dp).background(background))
}
Button(
onClick = {
visible = !visible },
modifier = Modifier.padding(top = 120.dp)
) {
Text(text = if(visible) "隐藏" else "显示")
}
}
}
运行效果:
AnimatedContent
AnimatedContent 可组合项会在内容根据目标状态发生变化时,为内容添加动画效果。
与 AnimatedVisibility 的区别是: AnimatedVisibility用来添加组件自身的入场/离场动画,而AnimatedContent是实现不同组件间的切换动画
AnimatedContent接收一个targetState和一个content,content 是基于 targetState 创建的Composable,当targetState变化时,content的内容也会随之变化。AnimatedContent内部维护着targetState到content的映射表,查找 targetState新旧值对应的content后,在content发生重组时附加动画效果。
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample() {
Column {
var count by remember {
mutableStateOf(0) }
Button(onClick = {
count++ }) {
Text("Add") }
AnimatedContent(targetState = count) {
targetCount ->
// 这里要使用lambda的参数 `targetCount`, 而不是 `count`,否则将没有意义(API 会将此值用作键,以标识当前显示的内容)
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
运行效果:
ContentTransform
AnimatedContent默认是淡入淡出效果,可以为 transitionSpec 参数指定 ContentTransform 对象,以自定义此动画行为。
可以使用 with infix 函数来组合 EnterTransition 与 ExitTransition,以创建 ContentTransform
@ExperimentalAnimationApi
infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)
ContentTransform本质上就是currentContent(initial) 的 ExitTransition与targetContent的EnterTransition组合, EnterTransition 定义了目标内容应如何显示,ExitTransition 则定义了初始内容应如何消失。
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample2() {
Column {
var count by remember {
mutableStateOf(0) }
Button(onClick = {
count++ }) {
Text("Add") }
AnimatedContent(
targetState = count,
transitionSpec = {
// 从右往左切换,并伴随淡入淡出效果(initialOffsetX = width, targetOffsetX = -width)
slideInHorizontally{
width -> width} + fadeIn() with
slideOutHorizontally{
width -> -width} + fadeOut()
}
) {
targetCount ->
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
运行效果:
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample3() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var count by remember {
mutableStateOf(0) }
Button(onClick = {
count++ }) {
Text("Add") }
val animationSpec = tween<IntOffset>(200)
val animationSpec2 = tween<Float>(200)
AnimatedContent(
targetState = count,
transitionSpec = {
slideInVertically(animationSpec){
height -> height} + fadeIn(animationSpec2) with
slideOutVertically(animationSpec) {
height -> height} + fadeOut(animationSpec2)
}
) {
targetCount ->
Text(text = "$targetCount", fontSize = 40.sp)
}
}
}
运行效果:
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AnimatedContentExample4() {
Column {
var count by remember {
mutableStateOf(0) }
Row(horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = {
count-- }) {
Text("Minus") }
Spacer(Modifier.size(60.dp))
Button(onClick = {
count++ }) {
Text("Plus ") }
}
Spacer(Modifier.size(20.dp))
AnimatedContent(
targetState = count,
transitionSpec = {
if (targetState > initialState) {
// 如果targetState更大,则从下往上切换并伴随淡入淡出效果
slideInVertically {
height -> height } + fadeIn() with
slideOutVertically {
height -> -height } + fadeOut()
} else {
// 如果targetState更小,则从上往下切换并伴随淡入淡出效果
slideInVertically {
height -> -height } + fadeIn() with
slideOutVertically {
height -> height } + fadeOut()
}.using(
// Disable clipping since the faded slide-in/out should be displayed out of bounds.
SizeTransform(clip = false)
)
}
) {
targetCount ->
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
运行效果:
slideIntoContainer 和 slideOutOfContainer
除了可用于 AnimatedVisibility 的所有 EnterTransition 和 ExitTransition 函数之外,AnimatedContent 还提供了 slideIntoContainer 和 slideOutOfContainer。这些是 slideInHorizontally/Vertically 和 slideOutHorizontally/Vertically 的便捷替代方案,它们可根据初始内容的大小和 AnimatedContent 内容的目标内容来计算滑动距离。(官方例子可见:slideIntoContainer)
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SlideIntoContainerSample() {
val transitionSpec: AnimatedContentScope<Int>.() -> ContentTransform = {
if (initialState < targetState) {
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Up) + fadeIn() with
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Up) + fadeOut()
} else {
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Down) + fadeIn() with
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Down) + fadeOut()
}.apply {
// 这里可指定目标内容的 zIndex ,值越大越上层,值越小越下层
// targetContentZIndex = when (targetState) {
// NestedMenuState.Level1 -> 1f
// NestedMenuState.Level2 -> 2f
// NestedMenuState.Level3 -> 3f
// }
}.using(SizeTransform(clip = false))
}
Column {
var count by remember {
mutableStateOf(0) }
Row(horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = {
count-- }) {
Text("Minus") }
Spacer(Modifier.size(60.dp))
Button(onClick = {
count++ }) {
Text("Plus ") }
}
Spacer(Modifier.size(20.dp))
AnimatedContent(
targetState = count,
transitionSpec = transitionSpec,
) {
targetCount ->
Text(text = "Count: $targetCount", Modifier.background(Color.Green), fontSize = 25.sp)
}
}
}
运行效果:同上一个例子一样
SizeTransform
SizeTransform 定义了大小应如何在初始内容与目标内容之间添加动画效果。在创建动画时,您可以访问初始大小和目标大小。 SizeTransform 还可控制在动画播放期间是否应将内容裁剪为组件大小。
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
fun SizeTransformAnimatedContentSample() {
var expanded by remember {
mutableStateOf(false) }
Surface(
color = MaterialTheme.colors.primary,
onClick = {
expanded = !expanded },
modifier = Modifier.padding(10.dp).onSizeChanged {
}
) {
AnimatedContent(
targetState = expanded,
transitionSpec = {
fadeIn(animationSpec = tween(150, 150)) with
fadeOut(animationSpec = tween(150)) using
SizeTransform {
initialSize, targetSize ->
if (targetState) {
keyframes {
// 展开时,先水平方向展开
// 150ms之前:宽度从initialSize.width增大到targetSize.width,高度保持initialSize.height不变
// 150ms之后:宽度保持targetSize.width不变,高度从initialSize.height开始增大到targetSize.height
IntSize(targetSize.width, initialSize.height) at 150
durationMillis = 300
}
} else {
keyframes {
// 收缩时,先垂直方向收起
// 150ms之前:宽度保持initialSize.width不变,高度从initialSize.height减小到targetSize.height
// 150ms之后:宽度从initialSize.width减小到targetSize.width,高度保持targetSize.height不变
IntSize