使用 Jetpack Compose 实现简单动画

1. 开始之前

在本 Codelab 中,您将学习如何向 Android 应用添加简单动画。动画可以让您的应用更具互动性、趣味性和易用性。为信息丰富的屏幕上的单个更新添加动画可以帮助用户查看发生了哪些更改。

应用的用户界面可以使用许多类型的动画。项目可以在出现时淡入,在消失时淡出,它们可以移入或移出屏幕,或者以有趣的方式进行转换。这有助于使应用的 UI 更具表现力且易于使用。

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

先决条件

  • 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 应用 GitHub 仓库中浏览代码。

3. 添加“展开更多”图标

在本节中,您将向您的应用添加 **展开更多** 30c384f00846e69b.png 和 **展开更少** f88173321938c003.png 图标。

def59d71015c0fbe.png

图标

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

笔记本上的铅笔 图片来自 Angelina LitvinUnsplash 上的照片

Black and white pencil icon

Material Design 提供了大量图标,这些图标按常用类别排列,可以满足您的大部分需求。

Material Icon library

添加 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。将 IconButtononClickmodifier 值参数 设置为传递给 DogItemButton 的值。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. IconButton() 的 lambda 代码块内,添加一个 Icon 可组合项,并将 imageVector 值参数 设置为 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. 在 **设计** 面板中查看 WoofPreview()

5bbf09cd2828b6.png

请注意,展开更多按钮未与列表项的末端对齐。您将在下一步中修复此问题。

对齐展开更多按钮

要将展开更多按钮与列表项的末端对齐,需要使用 Modifier.weight() 属性在布局中添加一个间隔符。

在 **Woof** 应用中,每个列表项行都包含一张狗的图片、狗的信息和一个“展开更多”按钮。您将在“展开更多”按钮之前添加一个权重为 1fSpacer 可组合项,以正确对齐按钮图标。由于间隔符是行中唯一加权的子元素,因此在测量其他未加权子元素的宽度后,它将填充行中剩余的空间。

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. 在 **设计** 面板中查看 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 可组合项——一个用于在爱好信息上方显示 **关于** 文本,另一个用于显示爱好信息。

将第一个的 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. 在 **设计** 面板中查看 WoofPreview()。请注意,狗的爱好已显示。

Woof Preview with expanded list items

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. 预览可以显示 UI 的外观,您还可以与之交互。要与 UI 预览交互,请将鼠标悬停在 **设计** 面板中的 WoofPreview 文本上方,然后点击右上角的 **交互模式** 按钮 42379dbe94a7a497.png。这将以交互模式启动预览。

74e1624d68fb4131.png

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

Animation of Woof list items expanding and contracting

请注意,当列表项展开时,“展开更多”按钮图标保持不变。为了获得更好的用户体验,您需要更改图标,以便ExpandMore显示向下箭头c761ef298c2aea5a.png,而ExpandLess显示向上箭头b380f933be0b6ff4.png

  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. 在设备或模拟器上运行应用程序,或再次使用预览中的交互模式。请注意,图标在ExpandMorec761ef298c2aea5a.pngExpandLessb380f933be0b6ff4.png图标之间交替切换。

de5dc4a953f11e65.gif

更新图标做得很好!

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

6. 添加动画

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

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

弹簧动画

弹簧动画是一种基于物理的动画,由**弹簧力**驱动。使用弹簧动画,运动的值和速度是根据施加的弹簧力计算的。

例如,如果您将应用程序图标拖动到屏幕周围,然后通过抬起手指将其释放,则图标将通过无形的力跳回到其原始位置。

以下动画演示了弹簧效果。一旦手指从图标上移开,图标就会跳回,模拟弹簧。

Spring release effect

弹簧效果

弹簧力由以下两个属性引导

  • **阻尼比:**弹簧的弹性。
  • **刚度等级:**弹簧的刚度,即弹簧向末端移动的速度。

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

Spring effect高弹性

Spring effect无弹性

高刚度

low stiffness 极低刚度

查看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. 查看**设计**面板中的WoofPreview(),并使用交互模式或在模拟器或设备上运行您的应用程序以查看您的弹簧动画。

c0d0a52463332875.gif

您做到了!享受您带有动画的精美应用程序。

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

animate*AsState

animate*AsState()函数是Compose中最简单的动画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 animation

8. 获取解决方案代码

要下载完成的 codelab 代码,您可以使用此 git 命令

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

或者,您可以将存储库下载为 zip 文件,解压缩它,然后在 Android Studio 中打开它。

如果您想查看解决方案代码,请在 GitHub 上查看

9. 结论

恭喜!您添加了一个按钮来隐藏和显示有关狗的信息。您使用弹簧动画增强了用户体验。您还学习了如何在**设计**面板中使用交互模式。

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

了解更多