使用 Jetpack Compose 添加简单的动画

1. 前言

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

在应用界面中,可以使用的动画有很多种。内容项在出现或消失时可以采用淡入或淡出的方式,可以移入或移出屏幕,或是以有趣的方式进行转换。这有助于打造生动有趣且易于使用的应用界面。

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

在用户完成某项任务时给予其奖励的动画可以让用户体验历程中的重要时刻变得更有意义。

响应拨号键盘输入的动画元素可提供反馈,表明操作是否成功。

添加了动画效果的列表项可充当占位符,向用户表明相应内容正在加载。

以动画形式呈现的“滑动打开”操作可吸引和鼓励用户完成所需的手势。

图标动画能以有趣的方式补充或增强图标的含义。

前提条件

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

学习内容

  • 如何使用 Jetpack Compose 制作简单的弹簧动画。

构建内容

  • 您将基于“使用 Jetpack Compose 实现 Material 主题效果”Codelab 中的 Woof 应用进行构建,并添加一个简单的动画来确认用户的操作。

所需条件

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

2. 应用概览

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

7252aa244a54ad90.png

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

1e9cf1dbc490924a.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. 添加“展开”图标

如要制作弹簧动画,首先需要添加“展开”f88173321938c003.png 图标。“展开”图标为用户提供一个用于展开列表项的按钮。

9fbd3fb0daf35fd3.png

图标

图标是一种符号,可通过视觉形式表示预期功能,帮助用户了解界面。图标设计通常从用户应该已体验过的现实世界中的物体汲取灵感。图标设计通常会将细节降至最低级别,只需确保用户熟悉即可。例如,在现实世界中,我们使用铅笔书写,因此铅笔图标通常表示创建修改

照片由 Angelina Litvin 拍摄,选自 Unsplash 网站

黑白铅笔图标

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

bfdb896506790c69.png

添加 Gradle 依赖项

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

  1. 在项目窗格中,依次打开 Gradle Scripts > build.gradle (Module: Woof.app)

f7fe58e936bbad3e.png

  1. 滚动到 build.gradle (Module: Woof.app) 文件的末尾。在 dependencies{} 代码块中,添加以下行:
implementation "androidx.compose.material:material-icons-extended:$compose_version"

添加图标可组合项

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

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

}
  1. DogItemButton() 函数内,添加一个 IconButton() 可组合项,该可组合项接受 onClick 具名参数(一个使用尾随 lambda 语法的 lambda),会在按下此图标时调用;然后将具名参数设置为传入的 onClick 参数。
@Composable
private fun DogItemButton(
   // ...
) {
   IconButton(onClick = onClick) {

   }
}
  1. IconButton() lambda 代码块内,添加一个具有名为 imageVector 的具名参数的 Icon 可组合项,并将该具名参数设置为 Icons.Filled.ExpandMore。这是将在列表项末尾显示的图标按钮 f88173321938c003.png。Android Studio 会向您显示针对 Icon() 可组合项参数的警告,您将在后续步骤中修复相应问题。
  2. 添加具名参数 tint,并将图标的颜色设为 MaterialTheme.colors.secondary。添加具名参数 contentDescription,并将其设置为字符串资源 R.string.expand_button_content_description
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore

IconButton(onClick = onClick) {
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       tint = MaterialTheme.colors.secondary,
       contentDescription = stringResource(R.string.expand_button_content_description)
   )
}

显示图标

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

  1. DogItem() 可组合函数的开头,添加 var 以保存列表项的展开状态。将初始值设置为 false
var expanded by remember { mutableStateOf(false) }
  1. DogItem() 可组合函数的 Row 代码块的末尾,调用 DogItemButton() 函数,然后针对回调传入 expanded 状态和空 lambda。此代码会在列表项中显示图标按钮。
  2. 如需在列表项中显示图标按钮,请在 Row 代码块末尾的 DogItem() 可组合函数中,在调用 DogInformation() 之后调用 DogItemButton()。针对回调传入 expanded 状态和空 lambda。您将在后续步骤中定义此 lambda 函数。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. Design 窗格中,点击 Build & Refresh 构建和刷新预览。

a49643f08701a8d.png

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

对齐“展开”按钮

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

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

6c2b523849f0f626.png

在列表项行中添加分隔符

  1. DogItem() 可组合函数的 Row 代码块末尾添加 Spacer。传入带 weight(1f)ModifierModifier.weight() 会使分隔符填充该行中剩余的空间。
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(8.dp)
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(Modifier.weight(1f))
   DogItemButton(
      expanded = expanded,
      onClick = { }
   )
}
  1. Design 窗格中,点击 Build & Refresh 构建和刷新预览。请注意,“展开”按钮现在已与列表项的末尾对齐。

f6a140413de9ad54.png

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

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

66ea5cc5c7253d55.png

  1. 创建一个新的名为 DogHobby() 的可组合函数,用于接受狗狗的爱好字符串资源 ID 和可选的 Modifier
  2. DogHobby() 函数内,创建一个包含以下内边距属性的列,以便增加列与子级可组合项之间的空间。
import androidx.annotation.StringRes

@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
            top = 8.dp,
            bottom = 16.dp,
            end = 16.dp
       )
   ) { }
}
  1. 在列代码块内,添加两个 Text 可组合项:一个用于在爱好信息上方显示 About 文本,另一个用于显示爱好信息。

3051387c4b9c7455.png

  1. 对于 About 文本,将样式设为 h3(标题 3)并将颜色设为 onBackground。对于爱好信息,将样式设为 body1
Column(
   modifier = modifier.padding(
       //..
   )
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.h3,
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.body1,
   )
}
  1. 完成后的 DogHobby() 可组合函数如下所示。
@Composable
fun DogHobby(@StringRes dogHobby: Int, modifier: Modifier = Modifier) {
   Column(
       modifier = modifier.padding(
           start = 16.dp,
           top = 8.dp,
           bottom = 16.dp,
           end = 16.dp
       )
   ) {
       Text(
           text = stringResource(R.string.about),
           style = MaterialTheme.typography.h3
       )
       Text(
           text = stringResource(dogHobby),
           style = MaterialTheme.typography.body1
       )
   }
}
  1. 如要显示 DogHobby() 可组合项,请在 DogItem() 中使用 Column 封装 Row。调用 DogHobby() 函数,传入 dog.hobbies 作为参数,将其放在 Row 之后作为第二个子级。
Column() {
   Row(
       //..
   ) {
       //..
   }
   DogHobby(dog.hobbies)
}

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

@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   var expanded by remember { mutableStateOf(false) }
   Card(
        elevation = 4.dp,
       modifier = modifier.padding(8.dp)
   ) {
       Column() {
           Row(
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { expanded = !expanded },
               )
           }
           DogHobby(dog.hobbies)
       }
   }
}
  1. Design 窗格中,点击 Build & Refresh 构建和刷新预览。请注意,狗狗的爱好信息会显示。

9e2e68a4bc4a8ae1.png

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

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

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

在上面的代码中,只有当 expanded 的值为 true 时,系统才会显示狗狗的爱好信息。

  1. 预览会显示界面的外观,您还可以与之互动。如要与界面预览对象进行互动,请点击“Design”窗格右上角的 Interactive Mode 按钮 42379dbe94a7a497.png。这会在交互模式下启动预览。

2a4ad1f3d2d0bff7.png

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

6ee6774b5b14c7e1.gif

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

  1. DogItemButton() 函数中,根据 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,
           //..
       )
   }
}
  1. 在设备或模拟器上运行应用,或再次使用预览中的交互模式。请注意,图标会在 ExpandMore c761ef298c2aea5a.pngExpandLess b380f933be0b6ff4.png 之间切换。

bf8bb280a774a6d4.gif

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

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

6. 添加动画

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

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

弹簧动画

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

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

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

7b52f63dc639c28d.gif

弹簧效果

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

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

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

弹簧效果高弹力

弹簧效果无弹力

高刚度

很低的刚度

现在,您将在应用中添加弹簧动画!

  1. MainActivity.ktDogItem() 内,向 Column 布局添加 modifier 参数。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
          modifier = Modifier
       ){
           //..
       }
   }
}

查看 DogItem() 可组合函数中的 DogHobby() 函数调用。狗狗的爱好信息基于 expanded 布尔值包含在组合中。根据狗狗的爱好信息处于可见状态还是隐藏状态,列表项的高度会发生变化。您将使用 animateContentSize 修饰符在新高度和旧高度之间添加过渡效果。

// No need to copy over
@Composable
fun DogItem(...) {

        //..
           if (expanded) {
               DogHobby(dog.hobbies)
           }
}
  1. 使用 animateContentSize 修饰符链接该修饰符,以便为大小(列表项高度)变化添加动画效果。
import androidx.compose.animation.animateContentSize

Column(
           modifier = Modifier
               .animateContentSize()
       ) {
            //..
       }

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

  1. animationSpec 参数添加到 animateContentSize() 函数调用中。使用 DampingRatioMediumBouncyStiffnessLow 参数将其设置为弹簧动画。
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioMediumBouncy,
               stiffness = Spring.StiffnessLow
           )
       )
)
  1. Design 窗格中,点击 Build & Refresh 构建和刷新预览,然后使用交互模式或者在模拟器或设备上运行您的应用,以查看实际的弹簧动画效果。

8cf711b8821b4696.gif

在模拟器或设备上重新运行应用,尽情使用您的带有动画效果的精美应用!

1e9cf1dbc490924a.gif

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

animate*AsState

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

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

使用 animateColorAsState() 函数在列表项展开时为颜色添加动画效果。

提示:

  1. 声明一种颜色并将其初始化委托给 animateColorAsState() 函数。
  2. 根据 expanded 布尔值,设置 targetValue 具名参数。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   val color by animateColorAsState(
       targetValue = if (expanded) Green25 else MaterialTheme.colors.surface,
   )
   Card(
       //..
   ) {...}
}
  1. 将您在上面声明的 color 设置为 Column 的背景修饰符。
@Composable
fun DogItem(dog: Dog, modifier: Modifier = Modifier) {
   //..
   Card(
       //..
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   //..
                   )
               )
               .background(color = color)
       ) {...}
}

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 标签在社交媒体上分享您的作品!

了解详情