Compose 动画 (六) : 使用Transition管理多个动画,实现动画预览

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.dp
        BoxState.Expanded -> 100.dp
        BoxState.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<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)
    )
}

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<Dp>,
    corner: State<Dp>
) {
    
    
    val size by size
    val 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

猜你喜欢

转载自blog.csdn.net/EthanCo/article/details/129398914