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 中,您将通过一个允许应用计算自定义小费百分比并舍入小费金额的文本字段来扩展此功能。
添加必要的字符串资源
- 在**项目**选项卡中,点击**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()
可组合函数的参数中,添加一个label
字符串资源,类型为Int
@Composable
fun EditNumberField(
label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 在函数体中,用
label
参数替换硬编码的字符串资源 ID
@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
类型。使用 Elvis 运算符,如果值为null
,则返回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()
函数调用中,将KeyboardOptions
构造函数的imeAction
命名参数设置为ImeAction.Next
值。使用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()
可组合函数,然后传入一个默认的Modifier
,作为类似于EditNumberField()
函数的参数
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
- 实现
RoundTheTipRow()
函数,添加一个具有以下modifier
的Row
布局可组合组件,以将子元素的宽度设置为屏幕上的最大值,居中对齐,并确保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
字符串资源显示四舍五入小费?
字符串
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()
函数中,添加一个roundUp
参数(类型为Boolean
)和一个onRoundUpChanged
lambda 函数,该函数接受一个Boolean
并返回空值
@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
语句之前,添加一个if()
条件,该条件检查roundUp
的值。如果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. 为文本字段添加前导图标(可选)
图标可以使文本字段在视觉上更具吸引力,并提供有关文本字段的更多信息。图标可用于传达有关文本字段目的的信息,例如预期的数据类型或所需的输入类型。例如,电话旁边的文本字段图标可能表示用户应输入电话号码。
图标可用于通过提供有关预期的视觉提示来指导用户的输入。例如,日历旁边的文本字段图标可能表示用户应输入日期。
以下是以搜索图标为例的文本字段,表示输入搜索词。
为 EditNumberField()
可组合组件添加另一个参数,名为 leadingIcon
,类型为 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!