使用 Jetpack Compose 添加简单的动画

1. 准备工作

在此 Codelab 中,您将学习如何为 Android 应用添加简单的动画。动画可以让您的应用更具互动性和趣味性,也更易于用户理解。在满是信息的屏幕上为状态的更新添加动画效果,有助于用户了解具体变化。

在应用界面中,可以使用的动画有很多种。内容项在出现时可以淡入,在消失时可以淡出,可以移入或移出画面,也可以通过有趣的方式变换。这有助于打造生动有趣且易于使用的应用界面。

动画还可以让应用更加精美,使其拥有精致优雅的外观和风格,同时为用户带来帮助:

前提条件

  • 了解 Kotlin,包括函数、lambda 和无状态可组合项。
  • 了解有关如何在 Jetpack Compose 中构建布局的基础知识。
  • 具备有关如何在 Jetpack Compose 中创建列表的基础知识。
  • 具备有关 Material Design 的基础知识。

学习内容

  • 如何使用 Jetpack Compose 构建简单的弹簧动画。

构建内容

所需条件

  • 最新的稳定版 Android Studio。
  • 互联网连接,以便下载起始代码。

2. 应用概览

使用 Jetpack Compose 实现 Material 主题设置 Codelab 中,您使用 Material Design 创建了一个 Woof 应用,该应用会显示狗狗列表及其信息。

36c6cabd93421a92.png

在本 Codelab 中,您要为 Woof 应用添加动画。您要添加爱好信息,当展开列表项时,系统会显示这些信息。此外,您还要添加弹簧动画,为正在展开的列表项添加动画效果。

c0d0a52463332875.gif

获取起始代码

首先,请下载起始代码:

或者,您也可以克隆该代码的 GitHub 代码库:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

您可以在 Woof app GitHub 代码库中浏览该代码。

3. 添加“展开”图标

在此部分中,您将为应用添加展开 30c384f00846e69b.png收起 f88173321938c003.png 图标。

def59d71015c0fbe.png

图标

图标是一种符号,可以形象地表示预期功能,帮助用户了解界面。图标设计通常会从用户在现实世界中可能接触到的对象中汲取灵感。图标设计通常对细节设计没有什么要求,只需确保用户熟悉即可。例如,在现实世界中,我们使用铅笔书写,因此铅笔图标通常表示创建修改

笔记本上放着一根铅笔 照片由 Angelina Litvin 拍摄,选自 Unsplash 网站

黑白铅笔图标

Material Design 提供了大量图标,分成若干常见类别,可满足您的大多数需求。

Material 图标库

添加 Gradle 依赖项

为您的项目添加 material-icons-extended 库依赖项。您将使用此库中的 Icons.Filled.ExpandLess 30c384f00846e69b.pngIcons.Filled.ExpandMore f88173321938c003.png 图标。

  1. Project 窗格中,依次打开 Gradle Scripts > build.gradle.kts (Module :app)
  2. 滚动到 build.gradle.kts (Module :app) 文件的末尾。在 dependencies{} 代码块中,添加以下行:
implementation("androidx.compose.material:material-icons-extended")

添加图标可组合项

添加一个函数,以显示 Material 图标库中的展开图标,并将其用作按钮。

  1. MainActivity.kt 中的 DogItem() 函数后面,创建一个名为 DogItemButton() 的全新可组合函数。
  2. 传入针对展开状态的 Boolean、针对 onClick 处理程序的 lambda 表达式和可选的 Modifier,如下所示:
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
) {

}
  1. DogItemButton() 函数内,添加一个 IconButton() 可组合项,该可组合项接受 onClick 具名形参(一个使用尾随 lambda 语法的 lambda,会在按下此图标时调用),以及一个可选的 modifier。将 IconButton's onClickmodifier value parameters 设置为等于传入 DogItemButton 的函数。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. IconButton() lambda 代码块内,添加一个 Icon 可组合项,并将 imageVector value-parameter 设置为 Icons.Filled.ExpandMore。这是将在列表项 f88173321938c003.png 末尾显示的按钮。Android Studio 会向您显示针对 Icon() 可组合项形参的警告,您将在下一步中修复相应问题。
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

IconButton(
   onClick = onClick,
   modifier = modifier
) {
   Icon(
       imageVector = Icons.Filled.ExpandMore
   )
}
  1. 添加值形参 tint,并将图标的颜色设为 MaterialTheme.colorScheme.secondary。添加具名形参 contentDescription,并将其设置为字符串资源 R.string.expand_button_content_description
IconButton(
   onClick = onClick,
   modifier = modifier
){
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       contentDescription = stringResource(R.string.expand_button_content_description),
       tint = MaterialTheme.colorScheme.secondary
   )
}

显示图标

通过将 DogItemButton() 可组合项添加到布局中来显示它。

  1. DogItem() 的开头,添加 var 以保存列表项的展开状态。将初始值设置为 false
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

var expanded by remember { mutableStateOf(false) }
  1. 在列表项中显示图标按钮。在 DogItem() 可组合项的 Row 代码块的末尾,调用 DogInformation() 之后添加 DogItemButton()。针对回调传入 expanded 状态和空 lambda。您将在后续步骤中定义 onClick 操作。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. Design 窗格中查看 WoofPreview()

5bbf09cd2828b6.png

请注意,“展开”按钮未与列表项的末尾对齐。您将在下一步中修复该问题。

对齐“展开”按钮

如要将“展开”按钮与列表项的末尾对齐,您需要使用 Modifier.weight() 属性在布局中添加分隔符。

Woof 应用中,每个列表项行都包含狗狗图片、狗狗信息和一个“展开”按钮。您需要在“展开”按钮前添加一个 Spacer 可组合项(权重为 1f),以便与按钮图标适当对齐。由于分隔符是行中唯一加权的子元素,因此该元素会在测量其他未加权子元素的宽度之后,填充行中的剩余空间。

733f6d9ef2939ab5.png

在列表项行中添加分隔符

  1. DogItem() 中的 DogInformation()DogItemButton() 之间,添加 Spacer。传入带 weight(1f)ModifierModifier.weight() 会使分隔符填充该行中剩余的空间。
import androidx.compose.foundation.layout.Spacer

Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(modifier = Modifier.weight(1f))
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. Design 窗格中查看 WoofPreview()。请注意,“展开”按钮现在已与列表项的末尾对齐。

8df42b9d85a5dbaa.png

4. 添加可组合项以显示爱好信息

在此任务中,您将添加 Text 可组合项以显示狗狗的爱好信息。

bba8146c6332cc37.png

  1. 创建一个新的名为 DogHobby() 的可组合函数,用于接受狗狗的爱好字符串资源 ID 和可选的 Modifier
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
  1. DogHobby() 函数内,创建一个 Column 并传入在 DogHobby() 中传入的修饰符。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) {

   }
}
  1. Column 代码块内,添加两个 Text 可组合项:一个用于在爱好信息上方显示 About 文本,另一个用于显示爱好信息。

将第一个可组合项的 text 设置为 strings.xml 文件中的 about,并将 style 设置为 labelSmall。将第二可组合项的 text 设置为传入的 dogHobby,并将 style 设置为 bodyLarge

Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}
  1. DogItem() 中,DogHobby() 可组合项将位于包含 DogIcon()DogInformation()Spacer()DogItemButton()Row 下方。为此,请使用 Column 封装 Row,以便将爱好添加到 Row 下方。
Column() {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
       Spacer(modifier = Modifier.weight(1f))
       DogItemButton(
           expanded = expanded,
           onClick = { /*TODO*/ }
       )
   }
}
  1. DogHobby() 添加在 Row 之后作为 Column 的第二个子级。传入 dog.hobbies,其中包含传入的狗狗的独特爱好,以及包含 DogHobby() 可组合项内边距的 modifier
Column() {
   Row() {
      ...
   }
   DogHobby(
       dog.hobbies,
       modifier = Modifier.padding(
           start = dimensionResource(R.dimen.padding_medium),
           top = dimensionResource(R.dimen.padding_small),
           end = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_medium)
       )
   )
}

完整的 DogItem() 函数应如下所示:

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       modifier = modifier
   ) {
       Column() {
           Row(
               modifier = Modifier
                   .fillMaxWidth()
                   .padding(dimensionResource(R.dimen.padding_small))
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { /*TODO*/ },
               )
           }
           DogHobby(
               dog.hobbies,
               modifier = Modifier.padding(
                   start = dimensionResource(R.dimen.padding_medium),
                   top = dimensionResource(R.dimen.padding_small),
                   end = dimensionResource(R.dimen.padding_medium),
                   bottom = dimensionResource(R.dimen.padding_medium)
               )
           )
       }
   }
}
  1. Design 窗格中查看 WoofPreview()。请注意,狗狗的爱好信息会显示。

带有展开列表项的 Woof 预览

5. 在点击按钮时显示/隐藏爱好信息

您的应用为每个列表项都提供了展开按钮,但该按钮还没有什么作用!在本部分中,您将添加用于在用户点击“展开”按钮时隐藏或显示爱好信息的选项。

  1. DogItem() 可组合函数的 DogItemButton() 函数调用中,定义 onClick() lambda 表达式,在用户点击按钮时将 expanded 布尔状态值更改为 true,在用户再次点击按钮时将其改回 false
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. DogItem() 函数中,使用对 expanded 布尔值进行的 if 检查来封装 DogHobby() 函数调用。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       ...
   ) {
       Column(
           ...
       ) {
           Row(
               ...
           ) {
               ...
           }
           if (expanded) {
               DogHobby(
                   dog.hobbies, modifier = Modifier.padding(
                       start = dimensionResource(R.dimen.padding_medium),
                       top = dimensionResource(R.dimen.padding_small),
                       end = dimensionResource(R.dimen.padding_medium),
                       bottom = dimensionResource(R.dimen.padding_medium)
                   )
               )
           }
       }
   }
}

现在,只有当 expanded 的值为 true 时,系统才会显示狗狗的爱好信息。

  1. 预览会显示界面的外观,您还可以与之互动。如要与界面预览对象进行互动,请将鼠标悬停在 Design 窗格中的 WoofPreview 文本上,然后点击 Design 窗格右上角的 Interactive Mode 按钮 42379dbe94a7a497.png。这会在交互模式下启动预览。

74e1624d68fb4131.png

  1. 点击“展开”按钮与预览对象进行互动。请注意,点击“展开”按钮后,系统会隐藏或显示狗狗的爱好信息。

Woof 列表项的展开和收缩动画

请注意,在列表项展开时,“展开”按钮图标没有发生变化。为了提供更加出色的用户体验,您将更改图标,使 ExpandMore 显示向下箭头 c761ef298c2aea5a.pngExpandLess 显示向上箭头 b380f933be0b6ff4.png

  1. DogItemButton() 函数中,添加一个根据 expanded 状态更新 imageVector 值的 if 语句,如下所示:
import androidx.compose.material.icons.filled.ExpandLess

@Composable
private fun DogItemButton(
   ...
) {
   IconButton(onClick = onClick) {
       Icon(
           imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
           ...
       )
   }
}

请注意您在上一个代码段中是如何编写 if-else 的。

if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore

这与以下代码中使用大括号 { } 一样:

if (expanded) {

`Icons.Filled.ExpandLess`

} else {

`Icons.Filled.ExpandMore`

}

如果 if-else 语句只有一行代码,那么大括号是可选的。

  1. 在设备或模拟器上运行应用,或再次使用预览中的交互模式。请注意,图标会在 ExpandMore c761ef298c2aea5a.pngExpandLess b380f933be0b6ff4.png 之间切换。

de5dc4a953f11e65.gif

您已成功更新图标,太棒了!

展开列表项时,您是否注意到高度的变化有些突然?高度的突然变化会使应用看起来不那么精美。为了解决此问题,您接下来将为应用添加动画。

6. 添加动画

您可以通过动画添加视觉提示,通知用户应用中的动态。当界面状态发生改变时(例如有新内容加载或有新操作可用时),动画尤其有用。动画还可以为您的应用添加精美外观。

在本部分中,您将添加一个弹簧动画,以动画形式呈现列表项高度的变化。

弹簧动画

弹簧动画是一种基于物理特性的动画,依靠弹簧弹力来驱动。使用弹簧动画时,移动的值和速度是根据应用的弹簧弹力计算得出的。

例如,如果您在屏幕上拖动某个应用图标,然后松开手指释放它,那么该图标便会被一股看不见的力量拉回其原始位置。

下图演示了弹簧动画效果。手指从图标上松开后,图标会弹回,就像弹簧弹跳一样。

弹簧释放效果

弹簧效果

弹簧弹力基于以下两个属性:

  • 阻尼比:弹簧的弹力。
  • 刚度:弹簧的刚度,即弹簧移动到终点的速度。

以下是一些具有不同阻尼比和刚度的动画示例。

弹簧效果高弹力

弹簧效果无弹力

高刚度

低刚度很低的刚度

查看 DogItem() 可组合函数中的 DogHobby() 函数调用。狗狗的爱好信息基于 expanded 布尔值包含在组合中。根据狗狗的爱好信息处于可见状态还是隐藏状态,列表项的高度会发生变化。目前,该过渡过程非常流畅。在此部分中,您将使用 animateContentSize 修饰符在展开状态和非展开状态之间更流畅地进行过渡。

// No need to copy over
@Composable
fun DogItem(...) {
  ...
    if (expanded) {
       DogHobby(
          dog.hobbies,
          modifier = Modifier.padding(
              start = dimensionResource(R.dimen.padding_medium),
              top = dimensionResource(R.dimen.padding_small),
              end = dimensionResource(R.dimen.padding_medium),
              bottom = dimensionResource(R.dimen.padding_medium)
          )
      )
   }
}
  1. MainActivity.ktDogItem() 内,向 Column 布局添加 modifier 形参。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
  1. 使用 animateContentSize 修饰符链接该修饰符,以便为大小(列表项高度)变化添加动画效果。
import androidx.compose.animation.animateContentSize

Column(
   modifier = Modifier
       .animateContentSize()
)

在当前实现中,您要为应用中的列表项高度变化添加动画效果。但是,动画效果非常细微,在运行应用时很难觉察出来。为解决此问题,请使用可选的 animationSpec 形参来自定义动画。

  1. 对于 Woof 而言,动画会缓入和缓出,不会有弹跳。为此,请将 animationSpec 形参添加到 animateContentSize() 函数调用中。使用 DampingRatioNoBouncy 将其设置为弹簧动画,使其无弹跳,并使用 StiffnessMedium 形参让弹簧略硬。
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioNoBouncy,
               stiffness = Spring.StiffnessMedium
           )
       )
)
  1. 查看 Design 窗格中的 WoofPreview(),然后使用交互模式或者在模拟器或设备上运行应用,以查看实际的弹簧动画效果。

c0d0a52463332875.gif

大功告成!尽情使用带有动画效果的精美应用。

7. (可选)尝试使用其他动画

animate*AsState

animate*AsState() 函数是 Compose 中最简单的 Animation API 之一,用于为单个值添加动画效果。您只需提供结束值(或目标值),该 API 就会从当前值开始向指定结束值播放动画。

Compose 为 FloatColorDpSizeOffsetInt 等提供了 animate*AsState() 函数。您可以使用接受通用类型的 animateValueAsState() 轻松添加对其他数据类型的支持。

尝试使用 animateColorAsState() 函数更改列表项处于展开状态时的颜色。

  1. DogItem() 中,声明一种颜色并将其初始化委托给 animateColorAsState() 函数。
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState()
   ...
}
  1. 根据 expanded 布尔值,设置 targetValue 具名形参。如果列表项处于展开状态,请将列表项设置为 tertiaryContainer 颜色。否则,请将其设置为 primaryContainer 颜色。
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState(
       targetValue = if (expanded) MaterialTheme.colorScheme.tertiaryContainer
       else MaterialTheme.colorScheme.primaryContainer,
   )
   ...
}
  1. color 设置为 Column 的背景修饰符。
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
  1. 查看列表项处于展开状态时颜色如何变化。非展开列表项的颜色为 primaryContainer,已展开列表项的颜色为 tertiaryContainer

animateAsState 动画

8. 获取解决方案代码

如需下载完成后的 Codelab 代码,您可以使用以下 Git 命令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git

或者,您也可以下载 ZIP 文件形式的代码库,将其解压缩并在 Android Studio 中打开。

如果您想查看解决方案代码,请前往 GitHub 查看

9. 总结

恭喜!您添加了一个用于隐藏和显示狗狗相关信息的按钮。您利用弹簧动画提升了用户体验。您还学习了如何在 Design 窗格中使用交互模式。

您还可以尝试使用其他类型的 Jetpack Compose 动画。别忘了使用 #AndroidBasics 标签在社交媒体上分享您的作品!

了解详情