学习Jetpack Compose中的动画

121 阅读5分钟

Jetpack Compose中的动画

动画是创造一种不容易被发现的视觉效果的方法。动画使你的应用程序感觉更有活力和互动性。

Jetpack Compose有一套灵活而动态的接口(API),可以简单地在你的应用程序的用户界面中添加动作,从而大大改善用户体验(UX)。

在本教程中,我们将学习如何使用Jetpack Compose创建简单的动画并对其进行自定义。

前提条件

要跟上本教程,你将需要。

  • 确保你的电脑上安装有最新版本的[Android Studio]。
  • 熟悉Jetpack[Compose]的基本概念。如果你还不熟悉Compose,你可以通过[本节的教程]。

动画的类型

Compose中的动画被分为两个主要的组别。

1.高水平的动画

这些动画由大多数应用程序中使用的最常见的API组成。它们的设计符合Android设计指南Material Design Motion

高级别的动画又分为两组。

  1. 布局中的内容变化。当你想对外观/消失进行动画处理或改变布局中的内容时,就会应用这些动画。它们包括

    • AnimationVisibility
    • 动画内容
    • 交叉渐变
  2. 基于状态的动画。这些动画的重点是用户界面的组成和重新组合。它们使用状态作为运动的决定因素。它们包括。

    • 过渡动画,如rememberInfiniteTransition
    • animate<type>AsState.<type> ,根据你的使用情况,可以取值,如颜色、浮点、Int、偏移、大小、值等。

我们将在本教程的后面用例子来讨论这些。

2.低级别的动画

这些是基础的API,高水平的API是建立在这些基础之上的。

让我们来看看布局变化。

动画化的可见性

@ExperimentalAnimationApi
@Composable
fun AnimVisibility() {
    var isVisible by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        AnimatedVisibility(visible = isVisible) {
            Text(text = "Animating Text")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = {
            isVisible = !isVisible
        }) {
            Text(text = "Animate")
        }
    }
}

在这个例子中,我们正在对一个文本组件的可见性进行动画处理。

注意:我们使用的是AnimatedVisibility API,在编写本教程时,它还是实验性的。由于这个原因,我们必须用@ExperimentalAnimationApi 注解来注释可合成的东西。

默认情况下,文本从其容器的顶部垂直动画到底部。

自定义动画

定制动画是指应用某些默认情况下不应用于动画的属性的能力。你可以使用内置的类或创建你自己的具有所需行为的自定义类。

自定义动画的可见性

AnimatedVisibility 可组成的可以通过提供AnimatedVisibility API中的属性来定制。这些属性包括。

  • Visible - 一个布尔值,决定内容是否可见。
  • Enter - 一个动画,当可合成的内容第一次被显示时,它被播放。
  • Exit - 一个动画,当可合成的内容被隐藏时播放。
  • Modifier - 应用于动画的可合成物的修改属性。

代码示例。

@ExperimentalAnimationApi
@Composable
fun AnimVisibility() {
    var isVisible by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        AnimatedVisibility(
            visible = isVisible,
            enter = fadeIn(
                // customize with tween AnimationSpec
                animationSpec = tween(
                    durationMillis = 1000,
                    delayMillis = 100,
                    easing = LinearOutSlowInEasing
                )
            ),
            // you can also add animationSpec in fadeOut if need be.
            exit = fadeOut() + shrinkHorizontally(),

            ) {
            Text(text = "Animating Text")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = {
            isVisible = !isVisible
        }) {
            Text(text = "Animate")
        }
    }
}

在这里,我们使用fadeInfadeOut 动画规格来对文本的可见性进行动画处理。

Tween是一个预定义的动画规格,可以用来指定动画的delaydurationeasing 。缓和指的是在开始-结束的互操作中动画的加速。

除了tween之外,我们还可以使用。

  • spring - 创建一个弹簧/弹跳的动画。
  • keyframes - 创建一个基于位置的动画。
  • 捕捉
  • 可重复
  • infiniteRepeatable - 创建一个无限循环的动画。

我们将在基于状态的动画部分应用和探索这些。

动画内容(AnimatedContent

这是Transition 类的一个扩展函数,通常与AnimatedVisibility 一起使用。它被用来对一个可组合的内容进行动画处理,如下图所示。

@ExperimentalAnimationApi
@ExperimentalMaterialApi // For material components such as Card.
@Composable
fun AnimContent() {
    var itemExpanded by remember { mutableStateOf(false) }
    val contentTransition = updateTransition(itemExpanded, label = "Expand")

    Card(
        modifier = Modifier.padding(6.dp),
        shape = RoundedCornerShape(12.dp),
        elevation = 4.dp,
        onClick = { itemExpanded = !itemExpanded }
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp)
        ) {
            Text(text = "Hi, Compose!")
            // Add Animated visibility
            contentTransition.AnimatedVisibility(
                visible = { isVisible -> isVisible }
            ) {
                Text(text = "What a beautiful animation!")
            }
            // Add Animated content
            contentTransition.AnimatedContent { targetState ->
                if (targetState) {
                    Text(text = "Expanded")
                } else {
                    Text(text = "Click to expand")
                }
            }
        }
    }
}

💡提示。我们可以对同一个可组合的内容应用一个以上的动画。

交叉渐变

Crossfade的工作原理是接受一个目标,每当这个目标发生变化时,它就在新旧状态之间制作动画。

Crossfade(targetState = myTarget){ myTarget ->
    when(myTarget){
        MyTarget.First -> {
            // render first state
        }
        MyTarget.Second -> {
            // render second state
        }
        ...
    }
}

"myTarget" 参数是传递给可组合的状态。最好的方法是通过使用enum 类来定义不同的目标状态,这样就可以使用when 表达式在它们之间轻松切换。

基于状态的动画

这些也被称为animate as state 动画,因为它们会返回一个状态对象,其值会不断变化,直到动画结束。为了补充我们前面提到的内容,让我们看一下下面这个使用animateDpAsState 的例子。

在这个例子中,我们将使用它的xOffSet ,对一个盒子的位置进行动画处理。xOffset 指的是该组件沿x轴离原点有多远。

xOffset的值将由盒子的当前状态决定,因此我们需要创建一个枚举类来定义不同的可能状态。

private enum class MyBoxState { START, END }

最初,我们将把状态设置为START

var myBoxState by remember { mutableStateOf(MyBoxState.START) }
// swap the target value based on the current state
val xOffset by animateDpAsState(
    targetValue = if (myBoxState == MyBoxState.START) 300.dp else 0.dp
)

因此,当按钮被点击时,我们会改变状态。这将启动一个受影响的可组合物的智能重组

myBoxState =
    when (myBoxState) {
        MyBoxState.START -> MyBoxState.END
        else -> MyBoxState.START
    }

下面是完整的代码示例。

private enum class MyBoxState { START, END }

@Composable
fun AnimMyBox() {
    var myBoxState by remember { mutableStateOf(MyBoxState.START) }

    val xOffset by animateDpAsState(
        targetValue = if (myBoxState == MyBoxState.START) 300.dp else 0.dp
    )

    Column() {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight(fraction = 0.1F)

        ) {
            Box(
                modifier = Modifier
                    .height(50.dp)
                    .absoluteOffset(xOffset)
                    .background(Color.DarkGray)
            ) {
                Text(text = "My Box")
            }
        }

        Row(
            modifier = Modifier.fillMaxSize(fraction = 1F),
            horizontalArrangement = Arrangement.Center
        ) {
            Button(onClick = {
                myBoxState =
                    when (myBoxState) {
                        MyBoxState.START -> MyBoxState.END
                        else -> MyBoxState.START
                    }
            }) {
                Text(text = "Animate")
            }
        }
    }
}

就像在AnimatedVisibility ,我们可以通过提供animationSpec进一步定制这个。让我们用spring 为例。

 val xOffset by animateDpAsState(
        targetValue = if (myBoxState == MyBoxState.START) 0.dp else 300.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessMedium
        )
    )

阻尼比是振荡阻尼与振荡周期的比率。阻尼比可以指定为:High,Medium,Low bouncy。

总结

在本教程中,我们已经介绍了Jetpack Compose中动画的基本概念,以及我们如何定制动画。Compose仍然很年轻,而且在不断发展。请继续学习,以跟上新的功能和API的改进。