1. 开始之前
在此 Codelab 中,您将使用Compose 中的状态入门 Codelab 中的解决方案代码,构建一个交互式小费计算器,在您输入账单金额和小费百分比时,该计算器可以自动计算并四舍五入小费金额。您可以在此图片中看到最终的应用程序
前提条件
- Compose 中的状态入门 Codelab。
- 能够向应用程序添加
Text
和TextField
可组合函数。 - 了解
remember()
函数、状态、状态提升,以及有状态和无状态可组合函数之间的区别。
您将学到什么
- 如何向虚拟键盘添加操作按钮。
Switch
可组合函数是什么以及如何使用它。- 向文本字段添加前置图标。
您将构建什么
- 一个根据用户输入的账单金额和小费百分比计算小费金额的 Tip Time 应用程序。
您需要什么
- Android Studio 的最新版本
- Compose 中的状态入门 Codelab 中的解决方案代码
2. 获取入门代码
要开始,请下载入门代码
此外,您也可以克隆代码的 GitHub 代码库
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git $ cd basic-android-kotlin-compose-training-tip-calculator $ git checkout state
您可以在 Tip Time
GitHub 代码库中浏览代码。
3. 入门应用程序概览
此 Codelab 从上一个 Codelab Compose 中的状态入门 中的 Tip Time 应用程序开始,该应用程序提供了计算具有固定小费百分比的小费所需的界面。账单金额文本框允许用户输入服务费用。应用程序会在一个 Text
可组合函数中计算并显示小费金额。
运行 Tip Time 应用程序
- 在 Android Studio 中打开 Tip Time 项目,并在模拟器或设备上运行应用程序。
- 输入账单金额。应用程序会自动计算并显示小费金额。
在当前的实现中,小费百分比硬编码为 15%。在此 Codelab 中,您将使用一个文本字段来扩展此功能,该文本字段允许应用程序计算自定义小费百分比并四舍五入小费金额。
添加必要的字符串资源
- 在 Project(项目)标签页中,点击 res > values > strings.xml。
- 在
strings.xml
文件中的<resources>
标记之间,添加这些字符串资源
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
strings.xml
文件应如下面的代码段所示,其中包含上一个 Codelab 中的字符串
strings.xml
<resources>
<string name="app_name">Tip Time</string>
<string name="calculate_tip">Calculate Tip</string>
<string name="bill_amount">Bill Amount</string>
<string name="how_was_the_service">Tip Percentage</string>
<string name="round_up_tip">Round up tip?</string>
<string name="tip_amount">Tip Amount: %s</string>
</resources>
4. 添加小费百分比文本字段
客户可能会根据提供的服务质量和各种其他原因,想多给一些小费或少给一些。为了适应这种情况,应用程序应允许用户计算自定义小费。在本节中,您将添加一个文本字段供用户输入自定义小费百分比,如您在此图片中看到的
您的应用程序中已经有一个账单金额文本字段可组合函数,它是无状态的 EditNumberField()
可组合函数。在上一个 Codelab 中,您将 amountInput
状态从 EditNumberField()
可组合函数提升到 TipTimeLayout()
可组合函数,这使得 EditNumberField()
可组合函数成为无状态的。
要添加文本字段,您可以重用相同的 EditNumberField()
可组合函数,但使用不同的标签。要进行此更改,您需要将标签作为参数传递,而不是将其硬编码到 EditNumberField()
可组合函数中。
使 EditNumberField()
可组合函数可重用
- 在
MainActivity.kt
文件中,在EditNumberField()
可组合函数的参数中,添加一个Int
类型的label
字符串资源
@Composable
fun EditNumberField(
label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 在函数体中,将硬编码的字符串资源 ID 替换为
label
参数
@Composable
fun EditNumberField(
//...
) {
TextField(
//...
label = { Text(stringResource(label)) },
//...
)
}
- 为了表示
label
参数预期为字符串资源引用,使用@StringRes
注解注释函数参数
@Composable
fun EditNumberField(
@StringRes label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 导入以下内容
import androidx.annotation.StringRes
- 在
TipTimeLayout()
可组合函数的EditNumberField()
函数调用中,将label
参数设置为R.string.bill_amount
字符串资源
EditNumberField(
label = R.string.bill_amount,
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
- 在预览窗格中不应有任何视觉变化。
- 在
TipTimeLayout()
可组合函数中,在EditNumberField()
函数调用之后,添加另一个用于自定义小费百分比的文本字段。使用这些参数调用EditNumberField()
可组合函数
EditNumberField(
label = R.string.how_was_the_service,
value = "",
onValueChanged = { },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
这会添加另一个文本框用于自定义小费百分比。
- 应用程序预览现在显示一个小费百分比文本字段,如您在此图片中看到的
- 在
TipTimeLayout()
可组合函数的顶部,添加一个名为tipInput
的var
属性,用于添加的文本字段的状态变量。使用mutableStateOf("")
初始化变量,并用remember
函数包围调用
var tipInput by remember { mutableStateOf("") }
- 在新的
EditNumberField
()
函数调用中,将名为value
的参数设置为tipInput
变量,然后在onValueChanged
lambda 表达式中更新tipInput
变量
EditNumberField(
label = R.string.how_was_the_service,
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
- 在
TipTimeLayout()
函数中,在tipInput
变量定义之后。定义一个名为tipPercent
的val
,将tipInput
变量转换为Double
类型。如果值为null
,则使用 Elvis 运算符并返回0
。如果文本字段为空,则此值可能为null
。
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
- 在
TipTimeLayout()
函数中,更新calculateTip()
函数调用,将tipPercent
变量作为第二个参数传入
val tip = calculateTip(amount, tipPercent)
TipTimeLayout()
函数的代码现在应如下面的代码段所示
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
var tipInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount, tipPercent)
Column(
modifier = Modifier.padding(40.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.calculate_tip),
modifier = Modifier
.padding(bottom = 16.dp)
.align(alignment = Alignment.Start)
)
EditNumberField(
label = R.string.bill_amount,
value = amountInput,
onValueChanged = { amountInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
EditNumberField(
label = R.string.how_was_the_service,
value = tipInput,
onValueChanged = { tipInput = it },
modifier = Modifier
.padding(bottom = 32.dp)
.fillMaxWidth()
)
Text(
text = stringResource(R.string.tip_amount, tip),
style = MaterialTheme.typography.displaySmall
)
Spacer(modifier = Modifier.height(150.dp))
}
}
- 在模拟器或设备上运行应用程序,然后输入账单金额和小费百分比。应用程序是否正确计算了小费金额?
5. 设置操作按钮
在上一个 Codelab 中,您探索了如何使用 KeyboardOptions
类设置键盘类型。在本节中,您将探索如何使用相同的 KeyboardOptions
设置键盘操作按钮。键盘操作按钮是键盘末尾的一个按钮。您可以在此表中看到一些示例
属性 | 键盘上的操作按钮 |
| |
| |
|
在此任务中,您将为文本框设置两个不同的操作按钮
- 账单金额文本框的下一步操作按钮,表示用户完成了当前输入,想移到下一个文本框。
- 小费百分比文本框的完成操作按钮,表示用户完成了输入。
您可以在这些图片中看到带有这些操作按钮的键盘示例
添加键盘选项
- 在
EditNumberField()
函数的TextField()
函数调用中,将ImeAction.Next
值设置给KeyboardOptions
构造函数的名为imeAction
的参数。使用KeyboardOptions.Default.copy()
函数确保您使用其他默认选项。
import androidx.compose.ui.text.input.ImeAction
@Composable
fun EditNumberField(
//...
) {
TextField(
//...
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
)
)
}
- 在模拟器或设备上运行应用程序。键盘现在显示下一步操作按钮,如您在此图片中看到的
请注意,当选择小费百分比文本字段时,键盘显示相同的下一步操作按钮。但是,您希望为文本字段设置两个不同的操作按钮。您很快就会解决这个问题。
- 检查
EditNumberField()
函数。TextField()
函数中的keyboardOptions
参数是硬编码的。要为文本字段创建不同的操作按钮,您需要将KeyboardOptions
对象作为参数传入,这将在下一步中完成。
// No need to copy, just examine the code.
fun EditNumberField(
@StringRes label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
//...
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
)
)
}
- 在
EditNumberField()
函数定义中,添加一个类型为KeyboardOptions
的keyboardOptions
参数。在函数体中,将其分配给TextField()
函数的名为keyboardOptions
的参数
@Composable
fun EditNumberField(
@StringRes label: Int,
keyboardOptions: KeyboardOptions,
// ...
){
TextField(
//...
keyboardOptions = keyboardOptions
)
}
- 在
TipTimeLayout()
函数中,更新第一个EditNumberField()
函数调用,为账单金额文本字段传入名为keyboardOptions
的参数
EditNumberField(
label = R.string.bill_amount,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
// ...
)
- 在第二个
EditNumberField()
函数调用中,将小费百分比文本字段的imeAction
更改为ImeAction.Done
。您的函数应如下面的代码段所示
EditNumberField(
label = R.string.how_was_the_service,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
// ...
)
- 运行应用程序。它显示下一步和完成操作按钮,如您在这些图片中看到的
- 输入任意账单金额,然后点击下一步操作按钮,接着输入任意小费百分比,然后点击完成操作按钮。这样会关闭键盘。
6. 添加开关
开关用于切换单个项目的状态(开启或关闭)。
切换有两种状态,用户可以在两个选项之间进行选择。切换包含一个轨道、一个滑块和一个可选图标,如您在这些图片中看到的
开关是一种选择控件,可用于进行决策或声明偏好,例如设置,如您在此图片中看到的
用户可以拖动滑块来回选择所选选项,或者只需轻触开关即可切换。您可以在此 GIF 中看到另一个切换示例,其中将“视觉选项”设置切换为深色模式
要了解更多关于开关的信息,请参阅开关文档。
您将使用 Switch
可组合函数,以便用户可以选择是否将小费向上取整到最接近的整数,如您在此图片中看到的
为 Text
和 Switch
可组合函数添加一行
- 在
EditNumberField()
函数之后,添加一个RoundTheTipRow()
可组合函数,然后像EditNumberField()
函数一样,传入一个默认的Modifier
作为参数
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
- 实现
RoundTheTipRow()
函数,添加一个Row
布局可组合函数,并使用以下modifier
将子元素的宽度设置为屏幕最大宽度,居中对齐,并确保大小为48dp
Row(
modifier = modifier
.fillMaxWidth()
.size(48.dp),
verticalAlignment = Alignment.CenterVertically
) {
}
- 导入以下内容
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
- 在
Row
布局可组合函数的 lambda 块中,添加一个Text
可组合函数,该函数使用R.string.round_up_tip
字符串资源来显示一个Round up tip?
字符串
Text(text = stringResource(R.string.round_up_tip))
- 在
Text
可组合函数之后,添加一个Switch
可组合函数,并传递一个名为checked
的参数,将其设置为roundUp
,以及一个名为onCheckedChange
的参数,将其设置为onRoundUpChanged
。
Switch(
checked = roundUp,
onCheckedChange = onRoundUpChanged,
)
此表包含这些参数的信息,这些参数与您为 RoundTheTipRow()
函数定义的参数相同
参数 | 描述 |
| 开关是否选中。这是 |
| 点击开关时要调用的回调。 |
- 导入以下内容
import androidx.compose.material3.Switch
- 在
RoundTheTipRow()
函数中,添加一个Boolean
类型的roundUp
参数和一个接收Boolean
并无返回值的onRoundUpChanged
lambda 函数
@Composable
fun RoundTheTipRow(
roundUp: Boolean,
onRoundUpChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier
)
这会提升开关的状态。
- 在
Switch
可组合函数中,添加此modifier
以将Switch
可组合函数对齐到屏幕末尾
Switch(
modifier = modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.End),
//...
)
- 导入以下内容
import androidx.compose.foundation.layout.wrapContentWidth
- 在
TipTimeLayout()
函数中,为Switch
可组合函数的状态添加一个 var 变量。创建一个名为roundUp
的var
变量,将其设置为mutableStateOf()
,初始值为false
。用remember { }
包围调用。
fun TipTimeLayout() {
//...
var roundUp by remember { mutableStateOf(false) }
//...
Column(
...
) {
//...
}
}
这是 Switch
可组合函数状态的变量,false 将是默认状态。
- 在
TipTimeLayout()
函数的Column
块中,在小费百分比文本字段之后。调用RoundTheTipRow()
函数,参数如下:名为roundUp
的参数设置为roundUp
,名为onRoundUpChanged
的参数设置为更新roundUp
值的 lambda 回调
@Composable
fun TipTimeLayout() {
//...
Column(
...
) {
Text(
...
)
Spacer(...)
EditNumberField(
...
)
EditNumberField(
...
)
RoundTheTipRow(
roundUp = roundUp,
onRoundUpChanged = { roundUp = it },
modifier = Modifier.padding(bottom = 32.dp)
)
Text(
...
)
}
}
这会显示向上取整小费?一行。
- 运行应用程序。应用程序显示向上取整小费?切换。
- 输入账单金额和小费百分比,然后选择向上取整小费?切换。小费金额不会向上取整,因为您仍然需要更新
calculateTip()
函数,这将在下一节中进行。
更新 calculateTip()
函数以向上取整小费
修改 calculateTip()
函数以接受一个 Boolean
变量,以便将小费向上取整到最接近的整数
- 要向上取整小费,
calculateTip()
函数应知道开关的状态,即一个Boolean
。在calculateTip()
函数中,添加一个Boolean
类型的roundUp
参数
private fun calculateTip(
amount: Double,
tipPercent: Double = 15.0,
roundUp: Boolean
): String {
//...
}
- 在
calculateTip()
函数中,在return
语句之前,添加一个检查roundUp
值的if()
条件。如果roundUp
为true
,则定义一个tip
变量并将其设置为kotlin.math.
ceil
()
函数,然后将函数tip
作为参数传递
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
完整的 calculateTip()
函数应如下面的代码段所示
private fun calculateTip(amount: Double, tipPercent: Double = 15.0, roundUp: Boolean): String {
var tip = tipPercent / 100 * amount
if (roundUp) {
tip = kotlin.math.ceil(tip)
}
return NumberFormat.getCurrencyInstance().format(tip)
}
- 在
TipTimeLayout()
函数中,更新calculateTip()
函数调用,然后传入一个roundUp
参数
val tip = calculateTip(amount, tipPercent, roundUp)
- 运行应用程序。现在它会向上取整小费金额,如您在这些图片中看到的
7. 添加对横屏模式的支持
Android 设备有各种外形尺寸——手机、平板电脑、可折叠设备和 ChromeOS 设备——屏幕尺寸范围广泛。您的应用程序应同时支持纵向和横向。
- 在横屏模式下测试您的应用程序,打开自动旋转。
- 向左旋转模拟器或设备,请注意您无法看到小费金额。要解决此问题,您需要一个垂直滚动条,帮助您滚动应用程序屏幕。
- 将
.verticalScroll(rememberScrollState())
添加到修饰符中,以使列可以垂直滚动。rememberScrollState()
创建并自动记住滚动状态。
@Composable
fun TipTimeLayout() {
// ...
Column(
modifier = Modifier
.padding(40.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
//...
}
}
- 导入以下内容
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
- 再次运行应用程序。尝试在横屏模式下滚动!
8. 向文本字段添加前置图标(可选)
图标可以使文本字段更具视觉吸引力,并提供关于文本字段的额外信息。图标可以用于传达文本字段用途的信息,例如预期的数据类型或所需的输入类型。例如,文本字段旁边的电话图标可能表示用户预期输入电话号码。
图标可以通过提供关于预期的视觉提示来引导用户的输入。例如,文本字段旁边的日历图标可能表示用户预期输入日期。
以下是一个带有搜索图标的文本字段示例,表示输入搜索词。
向名为 leadingIcon
的 EditNumberField()
可组合函数添加另一个 Int
类型的参数。使用 @DrawableRes
对其进行注释。
@Composable
fun EditNumberField(
@StringRes label: Int,
@DrawableRes leadingIcon: Int,
keyboardOptions: KeyboardOptions,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 导入以下内容
import androidx.annotation.DrawableRes
import androidx.compose.material3.Icon
- 将前置图标添加到文本字段。
leadingIcon
接受一个可组合函数,您将传入以下Icon
可组合函数。
TextField(
value = value,
leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
//...
)
- 将前置图标传递给文本字段。为方便起见,入门代码中已经存在图标。
EditNumberField(
label = R.string.bill_amount,
leadingIcon = R.drawable.money,
// Other arguments
)
EditNumberField(
label = R.string.how_was_the_service,
leadingIcon = R.drawable.percent,
// Other arguments
)
- 运行应用程序。
恭喜!您的应用程序现在具备计算自定义小费的功能。
9. 获取解决方案代码
要下载已完成 Codelab 的代码,您可以使用此 git 命令
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
此外,您也可以将代码库下载为 zip 文件,解压缩,然后在 Android Studio 中打开。
如果您想查看解决方案代码,请在 GitHub 上查看。
10. 结论
恭喜!您已为 Tip Time 应用程序添加了自定义小费功能。现在您的应用程序允许用户输入自定义小费百分比并向上取整小费金额。在社交媒体上分享您的作品,使用 #AndroidBasics 标签!