Jetpack Compose中的动画

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose中的动画相关的知识,希望对你有一定的参考价值。

Jetpack Compose中没有沿用android原有的View动画和属性动画,而是新创建了一套全新的动画系统API,这是理所当然的,因为旧的动画系统主要是基于View体系的,而Compose中需要针对的是Composable可组合函数进行处理,那么势必要创造一套新的玩具出来,同时,这也无疑增加了开发者的学习成本。

乍一看Jetpack Compose中的动画Api,尼玛是真的多呀,我C了,简直令人眼花缭乱、云里雾里、天马行空、小兔乱撞、手脚慌乱、头冒虚汗、四肢抓狂、不知所措呀 。。。😭

但是我们可以对其进行分一下类,如果按照使用的方便程度划分,大概可以分为两大类:高级动画API和低级动画API(这里类比高级开发语言的分类,并不是指效果多高级)。

其中高级动画API使用比较简单方便,封装度高,更加适用于日常业务开发,而低级动画API则使用起来较为麻烦,因为其配置项或流程较多,但是却更加灵活,能对动画效果做出更加精细的控制,适合自定义要求度较高的业务场景。

我们还可以按照功能类型进行一个大概的分类,也就是上图中的划分,这里再用表格归类一下:

功能需求点可能符合的API类型
单个组件的显示隐藏转场动画
每个子组件需要不同的入场/出场效果
AnimatedVisibility
根据组件内容状态变化的动画(数据、尺寸等)
不同组件间的切换动画
AnimatedContent
Modifier.animateContentSize
单纯的淡入淡出动画Crossfade
根据数据估值状态自动执行连续动画
基于单个数据值的状态变化执行动画
基于自定义数据类型进行估值动画
指定每一帧/每一时刻的动画状态
替代传统属性动画的方案
animateXXXAsState
根据不同状态同时管理和运行多个动画
进入界面时自动执行一次动画
监听动画状态
替代传统View动画中的AnimationSet的方案。
updateTransition
MutableTransitionState
永不停止、无限循环的动画rememberInfiniteTransition
更加底层的低级动画API
可高度自由定制的估值属性动画
需要在协程中执行的动画
需要控制一些动画并行执行
Animatable
更加底层的低级动画API
需要手动精确控制动画的时间
手势动画,fling衰减动画
TargetBasedAnimation
DecayAnimation

高级动画API

AnimatedVisibility

AnimatedVisibility主要用于页面显示状态的动画,即显示/隐藏的过渡动画,或者入场/离场动画。
可以使用 + 运算符组合多个 EnterTransitionExitTransition 对象,并且每个对象都接受可选参数以自定义其行为。

@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 "显示")
        
    

运行效果:

默认情况下 EnterTransitionfadeIn() + expandIn() 的效果,而 ExitTransitionshrinkOut() + fadeOut() 的效果, Compose额外提供了RowScope.AnimatedVisibilityColumnScope.AnimatedVisibility两个扩展方法, 当我们在RowColumn中调用时,该组件的默认动画效果会根据父容器的布局特征进行调整,比如在RowEnterTransition默认是fadeIn + expandHorizontally组合,而在ColumnEnterTransition默认是fadeIn + expandVertically组合方案。

EnterTransitionExitTransition 动画分类效果示例:

EnterTransitionExitTransition
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.NoneExitTransition.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和一个contentcontent 是基于 targetState 创建的Composable,当targetState变化时,content的内容也会随之变化。AnimatedContent内部维护着targetStatecontent的映射表,查找 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 函数来组合 EnterTransitionExitTransition,以创建 ContentTransform

 @ExperimentalAnimationApi
 infix fun EnterTransition.with(exit: ExitTransition) = ContentTransform(this, exit)

ContentTransform本质上就是currentContent(initial)ExitTransitiontargetContentEnterTransition组合, 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)
                slideInHorizontallywidth -> width + fadeIn() with
                        slideOutHorizontallywidth -> -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)
        
    

运行效果:

slideIntoContainerslideOutOfContainer

除了可用于 AnimatedVisibility 的所有 EnterTransition 和 ExitTransition 函数之外,AnimatedContent 还提供了 slideIntoContainerslideOutOfContainer。这些是 slideInHorizontally/VerticallyslideOutHorizontally/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

以上是关于Jetpack Compose中的动画的主要内容,如果未能解决你的问题,请参考以下文章

Jetpack Compose 中使用 Lottie 动画

Jetpack Compose 列表的展开与收起颜色动画效果

Jetpack Compose 中的垂直 LinearProgressIndicator

Jetpack Compose 从入门到入门

Jetpack Compose 中可绘制动画的替代方法是啥

Jetpack Compose - 动画的几种结束机制