赞
踩
Transition
和animateXxxAsState
一样,是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.dp
BoxState.Expanded -> 100.dp
BoxState.HalfExpanded -> 50.dp
}
}
Transition
还支持Compose动画预览(Animation Preview),这也是Transition
很重要的一个特性。
这里的
Transition
和AnimatedVisibility
中的EnterTransition
和ExitTransition
名字上差不多,但其实是不同的东西。
接下来我们会一步一步来实现Transition
过渡动画,从而来说明Transition
的作用。
我们先用最简单的方式,来实现尺寸和圆角的变化
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
}) {
}
效果如下
可以看到,点击后尺寸和圆角虽然变化了,但是却没有动画效果。
根据我们之前的文章,我们知道可以使用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
}) {
}
这样就可以看到过渡动画了
我们可以用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
的效果是一样的
既然animateDpAsState
和Transition
可以实现同样的效果,那为什么还要有Transition
这个API
呢 ?
原因就在于animateXxxAsState
是面向具体的值的,而Transition
是面向状态的。
Transation
对于动画状态做了统一的管理,带来了统一的视野,便于管理。(特别是对于有多个动画多个状态的情况)
而animateDpAsState
是只对单个动画状态负责的,并没有统一多个动画的情况下状态的强关系,比较乱。(在多个动画多个状态的情况下不便于管理)
Transition
和AnimatedVisibility
(内部使用Transition
实现)支持使用Compose动画预览功能,而animateXxxAsState
是不支持Compose
动画预览的 (不排除后期会支持)
我们在预览界面点击下面这个图标(Start Animation Preview
),会进入到动画预览模式
使用animateXxxAsState
的时候,可以看到IDE
提示我们,暂时不支持这个动画
而我们使用Transition
启动动画预览,可以看到我们可以去控制动画
点击展开,也可以看到每个具体的动画的名称 (通过label
进行设置)
可以拖动进度条到动画的任意位置,还能互换动画的初始状态和目标状态,设置动画的倍速等,具体效果如下GIF
所示
Transition
可以使用createChildTransition
创建子动画,子动画的动画数值来自于父动画。
这样各自都只需要关心自己的状态,能够更好地实现关注点分离,父Transition
将会知道子Transition
中的所有动画值。
enum class BoxState {
Collapsed, //收起
Expanded, //展开
HalfExpanded, //半展开
//...
}
@Composable
private fun BoxBlue(
childExpand1: Transition<Boolean>
) {
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<Boolean>
) {
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)
)
}
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 = "全展开")
}
}
我们可以发现,对于BoxBlue
和BoxRed
,它们只关心对应的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
我们点击动画预览,可以很清楚地看到每个子动画的进度
具体如GIF
所示
AnimatedVisibility
和 AnimatedContent
可用作 Transition
的扩展函数,这样AnimatedVisibility
和 AnimatedContent
就不用额外传参了。
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
}
}
对于简单的动画,直接在界面里写Transition
是一种比较高效的方案。
但是,在处理具有大量动画值的复杂组件时,可以将动画的实现和Compose界面分开,从而让代码更优雅,并使Transition
动画可以被复用。
用作封装的函数的返回值
class TransitionData(
size: State<Dp>,
corner: State<Dp>
) {
val size by size
val corner by corner
}
@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)
}
}
可以看到,这里直接
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
}) {
}
InfiniteTransition
是 Transition
的无限循环版本,一进入Compose
阶段就开始运行,除非被移除,否则不会停止。
使用 rememberInfiniteTransition
创建 InfiniteTransition
实例。然后用animateColor
、animatedFloat
或 animatedValue
添加子动画。
还需要通过 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)) {
}
效果如下所示
Compose 动画系列,后续持续更新
Compose 动画 (一) : animateXxxAsState 实现放大/缩小/渐变等效果
Compose 动画 (二) : 为什么animateDpAsState要用val ? MutableState和State有什么区别 ?
Compose 动画 (三) : AnimatedVisibility 从入门到深入
Compose 动画 (四) : AnimatedVisibility 各种入场和出场动画效果
Compose 动画 (五) : animateContentSize / animateEnterExit / Crossfade / AnimatedContent
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。