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 应用,该应用会显示狗狗列表及其信息。

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

获取起始代码
首先,请下载起始代码:
或者,您也可以克隆该代码的 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. 添加“展开”图标
在此部分中,您将为应用添加展开  和收起
 和收起  图标。
 图标。

Icons
图标是一种符号,可以形象地表示预期功能,帮助用户了解界面。图标设计通常会从用户在现实世界中可能接触到的对象中汲取灵感。图标设计通常对细节设计没有什么要求,只需确保用户熟悉即可。例如,在现实世界中,我们使用铅笔书写,因此铅笔图标通常表示创建或修改。
| 
 | 
 | 
Material Design 提供了大量图标,分成若干常见类别,可满足您的大多数需求。

添加 Gradle 依赖项
为您的项目添加 material-icons-extended 库依赖项。您将使用此库中的 Icons.Filled.ExpandLess  和
 和 Icons.Filled.ExpandMore  图标。
 图标。
- 在 Project 窗格中,依次打开 Gradle Scripts > build.gradle.kts (Module :app)。
- 滚动到 build.gradle.kts (Module :app)文件的末尾。在dependencies{}代码块中,添加以下行:
implementation("androidx.compose.material:material-icons-extended")
添加图标可组合项
添加一个函数,以显示 Material 图标库中的展开图标,并将其用作按钮。
- 在 MainActivity.kt中的DogItem()函数后面,创建一个名为DogItemButton()的全新可组合函数。
- 传入针对展开状态的 Boolean、针对 onClick 处理程序的 lambda 表达式和可选的Modifier,如下所示:
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
) {
 
}
- 在 DogItemButton()函数内,添加一个IconButton()可组合项,该可组合项接受onClick具名形参(一个使用尾随 lambda 语法的 lambda,会在按下此图标时调用),以及一个可选的modifier。将IconButton's onClick和modifier value parameters设置为等于传入DogItemButton的函数。
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {
   }
}
- 在 IconButton()lambda 代码块内,添加一个Icon可组合项,并将imageVector value-parameter设置为Icons.Filled.ExpandMore。这是将在列表项 末尾显示的按钮。Android Studio 会向您显示针对 末尾显示的按钮。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
   )
}
- 添加值形参 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() 可组合项添加到布局中来显示它。
- 在 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) }
- 在列表项中显示图标按钮。在 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*/ }
   )
}
- 在 Design 窗格中查看 WoofPreview()。

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

在列表项行中添加分隔符
- 在 DogItem()中的DogInformation()和DogItemButton()之间,添加Spacer。传入带weight(1f)的Modifier。Modifier.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*/ }
   )
}
- 在 Design 窗格中查看 WoofPreview()。请注意,“展开”按钮现在已与列表项的末尾对齐。

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

- 创建一个新的名为 DogHobby()的可组合函数,用于接受狗狗的爱好字符串资源 ID 和可选的Modifier。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
- 在 DogHobby()函数内,创建一个Column并传入在DogHobby()中传入的修饰符。
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) { 
   }
}
- 在 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
   )
}
- 在 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*/ }
       )
   }
}
- 将 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)
               )
           )
       }
   }
}
- 在 Design 窗格中查看 WoofPreview()。请注意,狗狗的爱好信息会显示。

5. 在点击按钮时显示/隐藏爱好信息
您的应用为每个列表项都提供了展开按钮,但该按钮还没有什么作用!在本部分中,您将添加用于在用户点击“展开”按钮时隐藏或显示爱好信息的选项。
- 在 DogItem()可组合函数的DogItemButton()函数调用中,定义onClick()lambda 表达式,在用户点击按钮时将expanded布尔状态值更改为true,在用户再次点击按钮时将其改回false。
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
- 在 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 时,系统才会显示狗狗的爱好信息。
- 预览会显示界面的外观,您还可以与之互动。如要与界面预览对象进行互动,请将鼠标悬停在 Design 窗格中的 WoofPreview 文本上,然后点击 Design 窗格右上角的 Interactive Mode 按钮  。这会在交互模式下启动预览。 。这会在交互模式下启动预览。

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

请注意,在列表项展开时,“展开”按钮图标没有发生变化。为了提供更加出色的用户体验,您将更改图标,使 ExpandMore 显示向下箭头  ,
,ExpandLess 显示向上箭头  。
。
- 在 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 语句只有一行代码,那么大括号是可选的。
- 在设备或模拟器上运行应用,或再次使用预览中的交互模式。请注意,图标会在 ExpandMore 和 和ExpandLess 之间切换。 之间切换。

您已成功更新图标,太棒了!
展开列表项时,您是否注意到高度的变化有些突然?高度的突然变化会使应用看起来不那么精美。为了解决此问题,您接下来将为应用添加动画。
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)
          )
      )
   }
}
- 在 MainActivity.kt的DogItem()内,向Column布局添加modifier形参。
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
- 使用 animateContentSize修饰符链接该修饰符,以便为大小(列表项高度)变化添加动画效果。
import androidx.compose.animation.animateContentSize
Column(
   modifier = Modifier
       .animateContentSize()
)
在当前实现中,您要为应用中的列表项高度变化添加动画效果。但是,动画效果非常细微,在运行应用时很难觉察出来。为解决此问题,请使用可选的 animationSpec 形参来自定义动画。
- 对于 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
           )
       )
)
- 查看 Design 窗格中的 WoofPreview(),然后使用交互模式或者在模拟器或设备上运行应用,以查看实际的弹簧动画效果。

大功告成!尽情使用带有动画效果的精美应用。
7. (可选)尝试使用其他动画
animate*AsState
animate*AsState() 函数是 Compose 中最简单的 Animation API 之一,用于为单个值添加动画效果。您只需提供结束值(或目标值),该 API 就会从当前值开始向指定结束值播放动画。
Compose 为 Float、Color、Dp、Size、Offset 和 Int 等提供了 animate*AsState() 函数。您可以使用接受通用类型的 animateValueAsState() 轻松添加对其他数据类型的支持。
尝试使用 animateColorAsState() 函数更改列表项处于展开状态时的颜色。
- 在 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()
   ...
}
- 根据 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,
   )
   ...
}
- 将 color设置为Column的背景修饰符。
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
- 查看列表项处于展开状态时颜色如何变化。非展开列表项的颜色为 primaryContainer,已展开列表项的颜色为tertiaryContainer。

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 标签在社交媒体上分享您的作品!
 
   照片由
 照片由 
 高弹力
高弹力 无弹力
无弹力 高刚度
高刚度 很低的刚度
很低的刚度