1. 准备工作
在本 Codelab 中,我们将使用 Codelab 课程“Compose 中的状态简介”中的解决方案代码构建一款交互式小费计算器;在您输入账单金额和小费百分比后,该计算器可以自动计算小费金额并进行取整处理。最终应用将如下图所示:
前提条件
- 已完成 Codelab 课程“Compose 中的状态简介”。
- 能够向应用添加
Text
和TextField
可组合项。 - 了解
remember()
函数、状态、状态提升,以及有状态和无状态可组合函数之间的差异。
学习内容
- 如何向虚拟键盘添加操作按钮。
- 什么是
Switch
可组合项以及如何使用它。 - 向文本字段添加前置图标。
构建内容
- 您将构建一款 Tip Time 应用,它会根据用户输入的账单金额和小费百分比计算小费金额。
所需条件
- 最新版本的 Android Studio
- Codelab 课程“Compose 中的状态简介”中的解决方案代码
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 应用入手,该应用提供了根据固定小费百分比计算小费所需的界面。用户可以通过 Bill amount 文本框输入服务费用。该应用将计算小费金额,并在 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. 添加小费百分比文本字段
用户可能希望根据所提供的服务质量和其他各种原因提高或降低小费。为适应此需求,应用应允许用户计算自定义小费。在本部分中,我们将添加一个文本字段,供用户输入自定义小费百分比,如下图所示:
您的应用中已包含 Bill Amount 文本字段可组合项,它属于无状态 EditNumberField()
可组合函数。在上一个 Codelab 中,您已将 amountInput
状态从 EditNumberField()
可组合项提升到 TipTimeLayout()
可组合项,从而让 EditNumberField()
可组合项变为无状态。
如需添加文本字段,您可以重复使用同一 EditNumberField()
可组合项,但要提供不同的标签。如需进行此项更改,您需要将标签作为形参传入,而不是在 EditNumberField()
可组合函数内对其进行硬编码。
使 EditNumberField()
可组合函数可重复使用:
- 在
MainActivity.kt
文件中,将Int
类型的label
字符串资源添加到EditNumberField()
可组合函数的形参中:
@Composable
fun EditNumberField(
label: Int,
value: String,
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier
)
- 在函数主体中,将硬编码的字符串资源 ID 替换为
label
形参:
@Composable
fun EditNumberField(
//...
) {
TextField(
//...
label = { Text(stringResource(label)) },
//...
)
}
- 使用
@StringRes
注解为该函数形参添加注解,指明label
形参应为字符串资源引用:
@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()
)
- 在 Preview 窗格中,用户界面应该不会有任何变化。
- 在
TipTimeLayout()
可组合函数的EditNumberField()
函数调用后面,再添加一个文本字段,供用户输入自定义小费百分比。使用以下形参调用EditNumberField()
可组合函数:
EditNumberField(
label = R.string.how_was_the_service,
value = "",
onValueChanged = { },
modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)
这样即可再添加一个文本框,供用户输入自定义小费百分比。
- 现在,应用预览会显示一个内容为 Tip Percentage 的文本字段,如下图所示:
- 在
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
设置键盘操作按钮。键盘操作按钮是指键盘端的按钮。您可以参见下表,了解一些示例:
属性 | 键盘上的操作按钮 |
| |
| |
|
在此任务中,您将为文本框设置两个不同的操作按钮:
- 为 Bill Amount 文本框设置 Next 操作按钮,用于指示用户已完成当前输入并想移到下一个文本框。
- 为 Tip Percentage 文本框设置 Done 操作按钮,用于指示用户已完成输入。
包含这些按钮的键盘示例如下面这些图片所示:
添加键盘选项:
- 在
EditNumberField()
函数的TextField()
函数调用中,向KeyboardOptions
构造函数传递一个值设置为ImeAction.Next
的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
)
)
}
- 在模拟器或设备上运行应用。现在,键盘会显示 Next 操作按钮,如下图所示:
请注意,选择 Tip Percentage 文本字段后,键盘同样会显示 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()
函数调用,为 Bill Amount 文本字段传入keyboardOptions
具名形参:
EditNumberField(
label = R.string.bill_amount,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Next
),
// ...
)
- 在第二个
EditNumberField()
函数调用中,将 Tip Percentage 文本字段的imeAction
更改为ImeAction.Done
。您的函数应如以下代码段所示:
EditNumberField(
label = R.string.how_was_the_service,
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
// ...
)
- 运行应用。它会显示 Next 和 Done 操作按钮,如下面这些图片所示:
- 输入任意账单金额并点击 Next 操作按钮,然后输入任意小费百分比并点击 Done 操作按钮。点击“Done”后会关闭拨号键盘。
6. 添加开关
开关可用来开启或关闭单个项的状态。
切换开关具有两种状态,可让用户在两个选项之间进行选择。切换开关由滑道、滑块和可选图标组成,如下面这些图片所示:
开关属于选择控件,可用于输入决策或声明偏好设置,例如下图所示的设置:
用户可以前后拖动滑块来选择所选选项,或者直接点按开关进行切换。下面的 GIF 显示了另一个切换开关示例,其中的“Visual options”(视觉选项)设置切换为 Dark mode(深色模式):
如需详细了解开关,请参阅开关文档。
您将使用 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 代码块中,添加一个使用R.string.round_up_tip
字符串资源来显示Round up tip?
字符串的Text
可组合项:
Text(text = stringResource(R.string.round_up_tip))
- 在
Text
可组合项后面,添加一个Switch
可组合项,然后传递一个设置为roundUp
的checked
具名形参和一个设置为onRoundUpChanged
的onCheckedChange
具名形参。
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
代码块的 Tip Percentage 文本字段后面,使用以下实参调用RoundTheTipRow()
函数:一个设置为roundUp
的roundUp
具名形参,以及一个设置为 lambda 回调的onRoundUpChanged
具名形参(用于更新roundUp
值):
@Composable
fun TipTimeLayout() {
//...
Column(
...
) {
Text(
...
)
Spacer(...)
EditNumberField(
...
)
EditNumberField(
...
)
RoundTheTipRow(
roundUp = roundUp,
onRoundUpChanged = { roundUp = it },
modifier = Modifier.padding(bottom = 32.dp)
)
Text(
...
)
}
}
这会显示 Round up tip? 行。
- 运行应用。应用会显示 Round up tip? 切换开关。
- 输入账单金额和小费百分比,然后让 Round up tip? 切换开关处于开启状态。小费金额并没有取整,这是因为您还需要更新
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 标签在社交媒体上分享您的作品!