Compose 动画 (六) : 使用Transition管理多个动画,实现动画预览
创始人
2024-06-03 08:55:09

1. Transition 是什么

TransitionanimateXxxAsState一样,是Android Compose中的一个动画API
animateXxxAsState是针对单个目标值的动画,而Transition可以面向多个目标值应用动画并保持它们同步结束。
啥意思呢,就是Transition可以把多个动画整合到一起控制,保持状态一致。
animateXxxAsState是面向具体的值的,而Transition是面向状态的。

Transition的状态可以有好多个 (可以用枚举或各种类型表示)

//定义状态BoxState枚举
enum class BoxState {Collapsed, //收起Expanded, //展开HalfExpanded, //半展开//...
}//创建当前状态,传入具体的BoxState值
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
// 创建Transition,管理状态
val transition = updateTransition(currentState, label = "boxTransition")//根据transition来创建动画值
val size by transition.animateDp(label = "size") { state ->when (state) {BoxState.Collapsed -> 0.dpBoxState.Expanded -> 100.dpBoxState.HalfExpanded -> 50.dp}
}

Transition还支持Compose动画预览(Animation Preview),这也是Transition很重要的一个特性。

这里的TransitionAnimatedVisibility中的EnterTransitionExitTransition名字上差不多,但其实是不同的东西。

在这里插入图片描述

接下来我们会一步一步来实现Transition过渡动画,从而来说明Transition的作用。

2. 实现尺寸和圆角变化

我们先用最简单的方式,来实现尺寸和圆角的变化

var expand by remember { mutableStateOf(false) }
val size = if (expand) 100.dp else 50.dp
val corner = if (expand) 0.dp else 25.dp
Box(Modifier.size(size).clip(RoundedCornerShape(corner)).background(Color.Blue).clickable {expand = !expand}) {}

效果如下

在这里插入图片描述
可以看到,点击后尺寸和圆角虽然变化了,但是却没有动画效果。

3. 增加动画过渡效果

根据我们之前的文章,我们知道可以使用animateDpAsState来实现动画过渡效果

val size by animateDpAsState(if (big) 100.dp else 50.dp)
val corner by animateDpAsState(if (big) 0.dp else 25.dp)

完整代码如下

var expand by remember { mutableStateOf(false) }
val size by animateDpAsState(if (expand) 100.dp else 50.dp)
val corner by animateDpAsState(if (expand) 0.dp else 25.dp)
Box(Modifier.size(size).clip(RoundedCornerShape(corner)).background(Color.Blue).clickable {expand = !expand}) {
}

这样就可以看到过渡动画了
在这里插入图片描述

4. 使用Transition进行替换

我们可以用Transition来替换上面的代码

var expand by remember { mutableStateOf(false) }
val expandTransition = updateTransition(targetState = expand, label = "expandTransition")
val size by expandTransition.animateDp(label = "size") {if (it) 100.dp else 50.dp
}
val corner by expandTransition.animateDp(label = "corner") {if (it) 0.dp else 25.dp
}
Box(Modifier.size(size).clip(RoundedCornerShape(corner)).background(Color.Blue).clickable {expand = !expand}) {
}

可以发现和使用animateDpAsState的效果是一样的

在这里插入图片描述

5. 为什么要有Transition

既然animateDpAsStateTransition可以实现同样的效果,那为什么还要有Transition这个API呢 ?

5.1 Transation对动画状态做统一的管理

原因就在于animateXxxAsState是面向具体的值的,而Transition是面向状态的。
Transation对于动画状态做了统一的管理,带来了统一的视野,便于管理。(特别是对于有多个动画多个状态的情况)
animateDpAsState是只对单个动画状态负责的,并没有统一多个动画的情况下状态的强关系,比较乱。(在多个动画多个状态的情况下不便于管理)

5.2 Transition支持Compose动画预览

TransitionAnimatedVisibility(内部使用Transition实现)支持使用Compose动画预览功能,而animateXxxAsState是不支持Compose动画预览的 (不排除后期会支持)

我们在预览界面点击下面这个图标(Start Animation Preview),会进入到动画预览模式

在这里插入图片描述
使用animateXxxAsState的时候,可以看到IDE提示我们,暂时不支持这个动画
在这里插入图片描述
而我们使用Transition启动动画预览,可以看到我们可以去控制动画
在这里插入图片描述
点击展开,也可以看到每个具体的动画的名称 (通过label进行设置)
在这里插入图片描述

可以拖动进度条到动画的任意位置,还能互换动画的初始状态和目标状态,设置动画的倍速等,具体效果如下GIF所示

在这里插入图片描述

6. createChildTransition创建子动画

Transition可以使用createChildTransition创建子动画,子动画的动画数值来自于父动画。
这样各自都只需要关心自己的状态,能够更好地实现关注点分离,父Transition将会知道子Transition中的所有动画值。

6.1 首先先定义状态枚举

enum class BoxState {Collapsed, //收起Expanded, //展开HalfExpanded, //半展开//...
}

6.2 定义蓝色和红色的Box

@Composable
private fun BoxBlue(childExpand1: Transition
) {val size by childExpand1.animateDp(label = "BoxBlue-size") {if (it) 100.dp else 50.dp}val corner by childExpand1.animateDp(label = "BoxBlue-corner") {if (it) 0.dp else 25.dp}Box(Modifier.size(size).clip(RoundedCornerShape(corner)).background(Color.Blue))
}@Composable
private fun BoxRed(childExpand1: Transition
) {val size by childExpand1.animateDp(label = "BoxRed-size") {if (it) 60.dp else 30.dp}val corner by childExpand1.animateDp(label = "BoxRed-corner") {if (it) 0.dp else 15.dp}Box(Modifier.size(size).clip(RoundedCornerShape(corner)).background(Color.Red))
}

6.3 创建Transition和ChildTransition

var expand by remember { mutableStateOf(BoxState.Collapsed) }
val expandTransition = updateTransition(targetState = expand,label = "expandTransition"
)
val childExpand1 = expandTransition.createChildTransition("child-expand-1") {it == BoxState.HalfExpanded || it == BoxState.Expanded
}
val childExpand2 = expandTransition.createChildTransition("child-expand-2") {it == BoxState.Expanded
}
Column() {BoxBlue(childExpand1)BoxRed(childExpand2)Button(onClick = { expand = BoxState.Collapsed }, Modifier.width(100.dp)) {Text(text = "收起")}Button(onClick = { expand = BoxState.HalfExpanded }, Modifier.width(100.dp)) {Text(text = "半展开")}Button(onClick = { expand = BoxState.Expanded }, Modifier.width(100.dp)) {Text(text = "全展开")}
}

6.4 运行程序

我们可以发现,对于BoxBlueBoxRed,它们只关心对应的childTransition就可以了,而对于expandTransition却能够知道子Transition中的所有动画值。
我们可以打印下日志看一下

val stateParent = expandTransition.currentState
val stateChild1 = expandTransition.transitions[0].currentState
val stateChild2 =expandTransition.transitions[1].currentState
Log.i("Heiko","stateParent:$stateParent stateChild1:$stateChild1 stateChild2:$stateChild2")

可以看到日志

stateParent:HalfExpanded stateChild1:true stateChild2:false

6.5 开启动画预览

我们点击动画预览,可以很清楚地看到每个子动画的进度
在这里插入图片描述
具体如GIF所示
在这里插入图片描述

7. 与AnimatedVisibility和AnimatedContent配合使用

AnimatedVisibilityAnimatedContent 可用作 Transition 的扩展函数,这样AnimatedVisibilityAnimatedContent就不用额外传参了。

var state by remember { mutableStateOf(true) }
val transition = updateTransition(targetState = state,label = "myTransition"
)
transition.AnimatedVisibility(visible = { targetSelected -> targetSelected }) {Box(Modifier.size(100.dp).background(Color.Blue).clickable {state = !state}) {}
}
transition.AnimatedContent {targetState ->if (targetState) {//Image1() //当targetState==true,显示组件1} else {//Image2() //当targetState==false,显示组件2}
}

8. 封装并复用Transition动画

对于简单的动画,直接在界面里写Transition是一种比较高效的方案。
但是,在处理具有大量动画值的复杂组件时,可以将动画的实现和Compose界面分开,从而让代码更优雅,并使Transition动画可以被复用。

8.1 定义Bean对象

用作封装的函数的返回值

class TransitionData(size: State,corner: State
) {val size by sizeval corner by corner
}

8.2 抽取并封装Transition动画

@Composable
fun updateTransitionData(expand: Boolean): TransitionData {val expandTransition = updateTransition(targetState = expand,label = "expandTransition")val size = expandTransition.animateDp(label = "size") {if (it) 100.dp else 50.dp}val corner = expandTransition.animateDp(label = "corner") {if (it) 0.dp else 25.dp}return remember(expandTransition) {TransitionData(size, corner)}
}

8.3 进行使用

可以看到,这里直接

var expand by remember { mutableStateOf(false) }
val transitionData = updateTransitionData(expand)
Box(Modifier.size(transitionData.size).clip(RoundedCornerShape(transitionData.corner)).background(Color.Blue).clickable {expand = !expand}) {
}

9. rememberInfiniteTransition

InfiniteTransitionTransition 的无限循环版本,一进入Compose阶段就开始运行,除非被移除,否则不会停止。
使用 rememberInfiniteTransition 创建 InfiniteTransition 实例。然后用animateColoranimatedFloatanimatedValue 添加子动画。
还需要通过 infiniteRepeatable 来设置 AnimationSpec,从而确定动画的时长、动画的重复模式等。

val infiniteTransition = rememberInfiniteTransition()
val color by infiniteTransition.animateColor(initialValue = Color.Blue,targetValue = Color.Red,animationSpec = infiniteRepeatable(animation = tween(1000, easing = LinearEasing),repeatMode = RepeatMode.Reverse)
)
Box(Modifier.size(100.dp).background(color)) {
}

效果如下所示
在这里插入图片描述

10. Compose动画系列

Compose 动画系列,后续持续更新
Compose 动画 (一) : animateXxxAsState 实现放大/缩小/渐变等效果
Compose 动画 (二) : 为什么animateDpAsState要用val ? MutableState和State有什么区别 ?
Compose 动画 (三) : AnimatedVisibility 从入门到深入
Compose 动画 (四) : AnimatedVisibility 各种入场和出场动画效果
Compose 动画 (五) : animateContentSize / animateEnterExit / Crossfade / AnimatedContent

相关内容

热门资讯

猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...