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>
The 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 中,您从 EditNumberField()
可组合项中提升了 amountInput
状态到 TipTimeLayout()
可组合项,这使得 EditNumberField()
可组合项成为无状态的。
要添加文本字段,您可以重用相同的 EditNumberField()
可组合项,但使用不同的标签。要进行此更改,您需要将标签作为参数传递,而不是在 EditNumberField()
可组合函数内部对其进行硬编码。
使 EditNumberField()
可组合函数可重用
- 在
MainActivity.kt
文件的EditNumberField()
可组合函数的参数中,添加一个label
字符串资源,类型为Int
@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
类型。使用 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()
函数,添加一个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()
函数中,添加一个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()
函数中,添加一个roundUp
参数,类型为Boolean
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!