Jetpack Compose 简单动画

1. 准备工作

在此 Codelab 中,您将学习如何向 Android 应用添加简单动画。动画可以使您的应用更具互动性、趣味性,并帮助用户更轻松地理解。对充满信息的屏幕上的单个更新进行动画处理可以帮助用户查看发生了什么变化。

应用的用户界面 (UI) 中可以使用多种类型的动画。条目在出现时可以淡入,在消失时可以淡出,它们可以在屏幕上移动或移出屏幕,或者以有趣的方式变形。这有助于使应用的 UI 表达丰富且易于使用。

动画还可以为您的应用增添精致感,使其外观和感觉更加优雅,同时也能帮助用户。

前提条件

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

您将学习的内容

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

您将构建的内容

所需物品

  • 最新稳定版 Android Studio。
  • 下载启动代码的互联网连接。

2. 应用概览

使用 Jetpack Compose 进行 Material Design 主题设置 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. 添加展开更多图标

在本部分中,您将向应用添加展开更多 向上箭头图标展开更少 向下箭头图标 图标。

def59d71015c0fbe.png

图标

图标是符号,可以通过视觉传达预期功能来帮助用户理解用户界面。它们通常从用户在现实世界中可能经历过的物体中汲取灵感。图标设计通常会将细节水平降至用户熟悉所需的最低限度。例如,现实世界中的铅笔用于书写,因此其对应的图标通常表示创建编辑

笔记本上的铅笔 图片由 Angelina LitvinUnsplash 提供

Black and white pencil icon

Material Design 提供了许多图标,按常见类别排列,可以满足您的大多数需求。

Material Icon library

添加 Gradle 依赖项

向您的项目添加 material-icons-extended 库依赖项。您将使用此库中的 Icons.Filled.ExpandLess 展开更少图标Icons.Filled.ExpandMore 展开更多图标 图标。

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

添加图标 Composable

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

  1. MainActivity.kt 中,在 DogItem() 函数之后,创建一个新的 Composable 函数,名为 DogItemButton()
  2. 传入一个 Boolean 表示展开状态,一个用于按钮 onClick 处理程序的 lambda 表达式,以及一个可选的 Modifier,如下所示
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
) {
 

}
  1. DogItemButton() 函数内部,添加一个 IconButton() Composable,它接受一个名为 onClick 的参数,一个使用尾随 lambda 语法 (trailing lambda syntax) 的 lambda,当此图标被按下时会调用该 lambda,以及一个可选的 modifier。将 IconButtononClickmodifier value parameters 设置为与传入 DogItemButton 的参数相同。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. IconButton() 的 lambda 代码块中,添加一个 Icon Composable,并将 imageVector value-parameter 设置为 Icons.Filled.ExpandMore。这将在列表项 展开更多图标 的末尾显示。Android Studio 会对 Icon() Composable 参数显示警告,您将在下一步修复这些警告。
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() Composable。

  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() Composable 中,在 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

注意到展开更多按钮没有与列表项末尾对齐。您将在下一步修复这个问题。

对齐展开更多按钮

要将展开更多按钮与列表项末尾对齐,您需要在布局中添加一个填充物 (spacer),并使用 Modifier.weight() 属性。

Woof 应用中,每个列表项行包含狗的图片、狗的信息以及一个展开更多按钮。您将在展开更多按钮之前添加一个权重为 1fSpacer Composable,以正确对齐按钮图标。由于 spacer 是行中唯一具有权重的子元素,它将填满测量其他无权重子元素的宽度后行中剩余的空间。

733f6d9ef2939ab5.png

将填充物 (spacer) 添加到列表项行

  1. DogItem() 中,在 DogInformation()DogItemButton() 之间,添加一个 Spacer。传入一个带有 weight(1f)ModifierModifier.weight() 会使 spacer 填满行中剩余的空间。
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. 添加 Composable 显示爱好

在此任务中,您将添加 Text Composable 来显示狗的爱好信息。

bba8146c6332cc37.png

  1. 创建一个新的 Composable 函数,名为 DogHobby(),它接受一个狗的爱好字符串资源 ID 和一个可选的 Modifier
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
  1. DogHobby() 函数内部,创建一个 Column,并将传入 DogHobby() 的 modifier 传进去。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) { 

   }
}
  1. Column 代码块内部,添加两个 Text Composable – 一个用于显示爱好信息上方的关于文本,另一个用于显示爱好信息。

将第一个的 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() Composable 将放在包含 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. Row 之后添加 DogHobby() 作为 Column 的第二个子项。传入包含狗独特爱好的 dog.hobbies,以及一个包含 DogHobby() Composable 填充的 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 Preview with expanded list items

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

您的应用每个列表项都有一个展开更多按钮,但它目前还没有任何功能!在本部分中,您将添加选项,以便用户点击展开更多按钮时隐藏或显示爱好信息。

  1. DogItem() Composable 函数中,在调用 DogItemButton() 函数时,定义 onClick() lambda 表达式,当按钮被点击时,将 expanded 布尔状态值更改为 true,如果按钮再次被点击,则将其更改回 false
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. DogItem() 函数中,使用一个 if 检查来包装 DogHobby() 函数调用,该检查基于 expanded 布尔值。
@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. 预览可以向您展示 UI 的外观,您也可以与它交互。要与 UI 预览交互,请将鼠标悬停在 Design 窗格中的 WoofPreview 文本上方,然后点击 Design 窗格右上角的互动模式按钮 互动模式按钮。这将以互动模式启动预览。

74e1624d68fb4131.png

  1. 通过点击展开更多按钮与预览交互。注意,当您点击展开更多按钮时,狗的爱好信息会隐藏和显示。

Animation of Woof list items expanding and contracting

注意到当列表项展开时,展开更多按钮的图标保持不变。为了更好的用户体验,您将更改图标,以便 ExpandMore 显示向下箭头 展开更多图标,而 ExpandLess 显示向上箭头 展开更少图标

  1. DogItemButton() 函数中,添加一个 if 语句,它根据 expanded 状态更新 imageVector 值,如下所示
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 展开更多图标ExpandLess 展开更少图标 图标之间切换。

de5dc4a953f11e65.gif

图标更新得很棒!

当您展开列表项时,是否注意到了高度的突然变化?这种突然的高度变化看起来不像是一个精致的应用。为了解决这个问题,您接下来将向应用添加动画。

6. 添加动画

动画可以添加视觉提示,通知用户应用中正在发生的事情。当 UI 改变状态时,动画特别有用,例如加载新内容或新操作可用时。动画还可以为您的应用增添精致感。

在本节中,您将添加弹簧动画来为列表项高度的变化添加动画效果。

弹簧动画

弹簧动画是一种由弹簧力驱动的基于物理的动画。使用弹簧动画时,移动的值和速度是根据应用的弹簧力计算的。

例如,如果您在屏幕上拖动应用图标然后松开手指,图标会通过一个看不见的力弹回其原始位置。

以下动画演示了弹簧效果。手指从图标上松开后,图标会弹回,模仿弹簧。

Spring release effect

弹簧效果

弹簧力受以下两个属性的影响

  • 阻尼比:弹簧的弹性。
  • 刚度等级:弹簧的刚度,即弹簧移向终点的速度。

以下是一些使用不同阻尼比和刚度等级的动画示例。

弹簧效果高弹性

无弹性无弹性

高刚度

低刚度非常低的刚度

查看 DogItem() Composable 函数中的 DogHobby() 函数调用。狗的爱好信息根据 expanded 布尔值包含在组合 (composition) 中。列表项的高度会改变,具体取决于爱好信息是可见还是隐藏。目前,这种过渡非常突兀。在本节中,您将使用 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.kt 中,在 DogItem() 中,向 Column 布局添加一个 modifier 参数。
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
  1. 将 modifier 与 animateContentSize 修饰符链接,以对大小(列表项高度)变化添加动画效果。
import androidx.compose.animation.animateContentSize

Column(
   modifier = Modifier
       .animateContentSize()
)

在当前实现中,您正在对应用中的列表项高度添加动画效果。但是,动画非常微妙,运行应用时很难察觉。为了解决这个问题,可以使用可选的 animationSpec 参数来自定义动画。

  1. 对于 Woof 应用,动画缓入缓出且没有弹性。为了实现这一点,向 animateContentSize() 函数调用添加 animationSpec 参数。将其设置为弹簧动画,使用 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 中最简单的动画 API 之一,用于对单个值添加动画效果。您只需提供结束值(或目标值),API 就会开始从当前值到指定结束值的动画。

Compose 提供了用于 FloatColorDpSizeOffsetInt 等类型的 animate*AsState() 函数。您可以使用接受泛型类型 (generic type) 的 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 animation

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!

了解详情