1. 准备工作
在此 Codelab 中,您将学习如何向 Android 应用添加简单动画。动画可以使您的应用更具互动性、趣味性,并帮助用户更轻松地理解。对充满信息的屏幕上的单个更新进行动画处理可以帮助用户查看发生了什么变化。
应用的用户界面 (UI) 中可以使用多种类型的动画。条目在出现时可以淡入,在消失时可以淡出,它们可以在屏幕上移动或移出屏幕,或者以有趣的方式变形。这有助于使应用的 UI 表达丰富且易于使用。
动画还可以为您的应用增添精致感,使其外观和感觉更加优雅,同时也能帮助用户。
前提条件
- 了解 Kotlin,包括函数、lambda 表达式和无状态可组合项 (stateless composables)。
- 了解如何在 Jetpack Compose 中构建布局的基础知识。
- 了解如何在 Jetpack Compose 中创建列表的基础知识。
- 了解 Material Design 的基础知识。
您将学习的内容
- 如何使用 Jetpack Compose 构建简单的弹簧动画 (spring animation)。
您将构建的内容
- 您将以上一个 使用 Jetpack Compose 进行 Material Design 主题设置 Codelab 中的 Woof 应用为基础,并添加一个简单动画来响应用户的操作。
所需物品
- 最新稳定版 Android Studio。
- 下载启动代码的互联网连接。
2. 应用概览
在使用 Jetpack Compose 进行 Material Design 主题设置 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. 添加展开更多图标
在本部分中,您将向应用添加展开更多 和展开更少
图标。
图标
图标是符号,可以通过视觉传达预期功能来帮助用户理解用户界面。它们通常从用户在现实世界中可能经历过的物体中汲取灵感。图标设计通常会将细节水平降至用户熟悉所需的最低限度。例如,现实世界中的铅笔用于书写,因此其对应的图标通常表示创建或编辑。
|
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")
添加图标 Composable
添加一个函数来显示 Material 图标库中的展开更多图标,并将其用作按钮。
- 在
MainActivity.kt
中,在DogItem()
函数之后,创建一个新的 Composable 函数,名为DogItemButton()
。 - 传入一个
Boolean
表示展开状态,一个用于按钮 onClick 处理程序的 lambda 表达式,以及一个可选的Modifier
,如下所示
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
}
- 在
DogItemButton()
函数内部,添加一个IconButton()
Composable,它接受一个名为onClick
的参数,一个使用尾随 lambda 语法 (trailing lambda syntax) 的 lambda,当此图标被按下时会调用该 lambda,以及一个可选的modifier
。将IconButton
的onClick
和modifier value parameters
设置为与传入DogItemButton
的参数相同。
@Composable
private fun DogItemButton(
expanded: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
){
IconButton(
onClick = onClick,
modifier = modifier
) {
}
}
- 在
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
)
}
- 添加值参数
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。
- 在
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()
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*/ }
)
}
- 在 Design 窗格中查看
WoofPreview()
。
注意到展开更多按钮没有与列表项末尾对齐。您将在下一步修复这个问题。
对齐展开更多按钮
要将展开更多按钮与列表项末尾对齐,您需要在布局中添加一个填充物 (spacer),并使用 Modifier.weight()
属性。
在 Woof 应用中,每个列表项行包含狗的图片、狗的信息以及一个展开更多按钮。您将在展开更多按钮之前添加一个权重为 1f
的 Spacer
Composable,以正确对齐按钮图标。由于 spacer 是行中唯一具有权重的子元素,它将填满测量其他无权重子元素的宽度后行中剩余的空间。
将填充物 (spacer) 添加到列表项行
- 在
DogItem()
中,在DogInformation()
和DogItemButton()
之间,添加一个Spacer
。传入一个带有weight(1f)
的Modifier
。Modifier.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*/ }
)
}
- 在 Design 窗格中查看
WoofPreview()
。注意,展开更多按钮现在已与列表项末尾对齐。
4. 添加 Composable 显示爱好
在此任务中,您将添加 Text
Composable 来显示狗的爱好信息。
- 创建一个新的 Composable 函数,名为
DogHobby()
,它接受一个狗的爱好字符串资源 ID 和一个可选的Modifier
。
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
) {
}
- 在
DogHobby()
函数内部,创建一个Column
,并将传入DogHobby()
的 modifier 传进去。
@Composable
fun DogHobby(
@StringRes dogHobby: Int,
modifier: Modifier = Modifier
){
Column(
modifier = modifier
) {
}
}
- 在
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
)
}
- 在
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*/ }
)
}
}
- 在
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)
)
)
}
}
}
- 在 Design 窗格中查看
WoofPreview()
。注意狗的爱好信息已显示。
5. 在按钮点击时显示或隐藏爱好信息
您的应用每个列表项都有一个展开更多按钮,但它目前还没有任何功能!在本部分中,您将添加选项,以便用户点击展开更多按钮时隐藏或显示爱好信息。
- 在
DogItem()
Composable 函数中,在调用DogItemButton()
函数时,定义onClick()
lambda 表达式,当按钮被点击时,将expanded
布尔状态值更改为true
,如果按钮再次被点击,则将其更改回false
。
DogItemButton(
expanded = expanded,
onClick = { expanded = !expanded }
)
- 在
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
时才会显示。
- 预览可以向您展示 UI 的外观,您也可以与它交互。要与 UI 预览交互,请将鼠标悬停在 Design 窗格中的 WoofPreview 文本上方,然后点击 Design 窗格右上角的互动模式按钮
。这将以互动模式启动预览。
- 通过点击展开更多按钮与预览交互。注意,当您点击展开更多按钮时,狗的爱好信息会隐藏和显示。
注意到当列表项展开时,展开更多按钮的图标保持不变。为了更好的用户体验,您将更改图标,以便 ExpandMore
显示向下箭头 ,而
ExpandLess
显示向上箭头 。
- 在
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
语句只有一行代码,花括号是可选的。
- 在设备或模拟器上运行应用,或者再次使用预览中的互动模式。注意图标在
ExpandMore
和
ExpandLess
图标之间切换。
图标更新得很棒!
当您展开列表项时,是否注意到了高度的突然变化?这种突然的高度变化看起来不像是一个精致的应用。为了解决这个问题,您接下来将向应用添加动画。
6. 添加动画
动画可以添加视觉提示,通知用户应用中正在发生的事情。当 UI 改变状态时,动画特别有用,例如加载新内容或新操作可用时。动画还可以为您的应用增添精致感。
在本节中,您将添加弹簧动画来为列表项高度的变化添加动画效果。
弹簧动画
弹簧动画是一种由弹簧力驱动的基于物理的动画。使用弹簧动画时,移动的值和速度是根据应用的弹簧力计算的。
例如,如果您在屏幕上拖动应用图标然后松开手指,图标会通过一个看不见的力弹回其原始位置。
以下动画演示了弹簧效果。手指从图标上松开后,图标会弹回,模仿弹簧。
弹簧效果
弹簧力受以下两个属性的影响
- 阻尼比:弹簧的弹性。
- 刚度等级:弹簧的刚度,即弹簧移向终点的速度。
以下是一些使用不同阻尼比和刚度等级的动画示例。
|
|
|
|
查看 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)
)
)
}
}
- 在
MainActivity.kt
中,在DogItem()
中,向Column
布局添加一个modifier
参数。
@Composable
fun DogItem(
dog: Dog,
modifier: Modifier = Modifier
) {
...
Card(
...
) {
Column(
modifier = Modifier
){
...
}
}
}
- 将 modifier 与
animateContentSize
修饰符链接,以对大小(列表项高度)变化添加动画效果。
import androidx.compose.animation.animateContentSize
Column(
modifier = Modifier
.animateContentSize()
)
在当前实现中,您正在对应用中的列表项高度添加动画效果。但是,动画非常微妙,运行应用时很难察觉。为了解决这个问题,可以使用可选的 animationSpec
参数来自定义动画。
- 对于 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
)
)
)
- 在 Design 窗格中查看
WoofPreview()
,并使用互动模式或在模拟器或设备上运行您的应用,以查看弹簧动画效果。
您成功了!享受您漂亮的动画应用吧。
7.(可选)尝试其他动画
animate*AsState
animate*AsState()
函数是 Compose 中最简单的动画 API 之一,用于对单个值添加动画效果。您只需提供结束值(或目标值),API 就会开始从当前值到指定结束值的动画。
Compose 提供了用于 Float
、Color
、Dp
、Size
、Offset
和 Int
等类型的 animate*AsState()
函数。您可以使用接受泛型类型 (generic type) 的 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!
了解详情
- Jetpack Compose 动画
- Codelab:在 Jetpack Compose 中为元素添加动画
- 视频:动画新构想
- 视频:Jetpack Compose:动画