计算自定义小费

1. 准备工作

在本 Codelab 中,我们将使用 Codelab 课程“Compose 中的状态简介”中的解决方案代码构建一款交互式小费计算器;在您输入账单金额和小费百分比后,该计算器可以自动计算小费金额并进行取整处理。最终应用将如下图所示:

d8e768525099378a.png

前提条件

  • 已完成 Codelab 课程“Compose 中的状态简介”。
  • 能够向应用添加 TextTextField 可组合项。
  • 了解 remember() 函数、状态、状态提升,以及有状态和无状态可组合函数之间的差异。

学习内容

  • 如何向虚拟键盘添加操作按钮。
  • 什么是 Switch 可组合项以及如何使用它。
  • 向文本字段添加前置图标。

构建内容

  • 您将构建一款 Tip Time 应用,它会根据用户输入的账单金额和小费百分比计算小费金额。

所需条件

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 应用

  1. 在 Android Studio 中打开 Tip Time 项目,并在模拟器或设备上运行应用。
  2. 输入账单金额。应用会自动计算并显示小费金额。

b6bd5374911410ac.png

在当前的实现中,小费百分比已硬编码为 15%。在本 Codelab 中,您将使用文本字段扩展该功能,让应用能够根据自定义小费百分比计算小费金额并进行取整处理。

添加必要的字符串资源

  1. Project 标签页中,依次点击 res > values > strings.xml
  2. 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. 添加小费百分比文本字段

用户可能希望根据所提供的服务质量和其他各种原因提高或降低小费。为适应此需求,应用应允许用户计算自定义小费。在本部分中,我们将添加一个文本字段,供用户输入自定义小费百分比,如下图所示:

391b4b1a090687ef.png

您的应用中已包含 Bill Amount 文本字段可组合项,它属于无状态 EditNumberField() 可组合函数。在上一个 Codelab 中,您已将 amountInput 状态从 EditNumberField() 可组合项提升到 TipTimeLayout() 可组合项,从而让 EditNumberField() 可组合项变为无状态。

如需添加文本字段,您可以重复使用同一 EditNumberField() 可组合项,但要提供不同的标签。如需进行此项更改,您需要将标签作为形参传入,而不是在 EditNumberField() 可组合函数内对其进行硬编码。

使 EditNumberField() 可组合函数可重复使用:

  1. MainActivity.kt 文件中,将 Int 类型的 label 字符串资源添加到 EditNumberField() 可组合函数的形参中:
@Composable
fun EditNumberField(
    label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
)
  1. 在函数主体中,将硬编码的字符串资源 ID 替换为 label 形参:
@Composable
fun EditNumberField(
    //...
) {
     TextField(
         //...
         label = { Text(stringResource(label)) },
         //...
     )
}
  1. 使用 @StringRes 注解为该函数形参添加注解,指明 label 形参应为字符串资源引用:
@Composable
fun EditNumberField(
    @StringRes label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
) 
  1. 导入以下代码:
import androidx.annotation.StringRes
  1. 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()
)
  1. Preview 窗格中,用户界面应该不会有任何变化。

b223d5ba4a54f792.png

  1. TipTimeLayout() 可组合函数的 EditNumberField() 函数调用后面,再添加一个文本字段,供用户输入自定义小费百分比。使用以下形参调用 EditNumberField() 可组合函数:
EditNumberField(
    label = R.string.how_was_the_service,
    value = "",
    onValueChanged = { },
    modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth()
)

这样即可再添加一个文本框,供用户输入自定义小费百分比。

  1. 现在,应用预览会显示一个内容为 Tip Percentage 的文本字段,如下图所示:

a5f5ef5e456e185e.png

  1. TipTimeLayout() 可组合函数的顶部,为已添加的文本字段的状态变量添加一个名为 tipInputvar 属性。使用 mutableStateOf("") 初始化该变量,并使用 remember 函数将调用括起来:
var tipInput by remember { mutableStateOf("") }
  1. 在新的 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()
)
  1. TipTimeLayout() 函数中 tipInput 变量的定义后面,定义一个名为 tipPercentval,它会将 tipInput 变量转换为 Double 类型。使用 Elvis 运算符,如果值为 null,则返回 0。如果文本字段为空,则此值可以为 null
val tipPercent = tipInput.toDoubleOrNull() ?: 0.0
  1. 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))
    }
}
  1. 在模拟器或设备上运行应用,然后输入账单金额和小费百分比。应用会正确计算小费金额吗?

屏幕截图,里面显示账单金额为 100,小费百分比为 20,并且小费金额显示为 20 美元

5. 设置操作按钮

在上一个 Codelab 中,您学习了如何使用 KeyboardOptions 类设置键盘类型。在本部分中,您将学习如何使用相同的 KeyboardOptions 设置键盘操作按钮。键盘操作按钮是指键盘端的按钮。您可以参见下表,了解一些示例:

属性

键盘上的操作按钮

ImeAction.Search
用户想要执行搜索时使用。

该图片表示用于执行搜索的“Search”图标。

ImeAction.Send
用户想要发送输入字段中的文本时使用。

该图片表示用于发送输入字段中的文本的“Send”图标。

ImeAction.Go
用户想要跳转到输入文本的目的地时使用。

该图片表示用于前往输入字段中的文本目标的“Go”图标。

在此任务中,您将为文本框设置两个不同的操作按钮:

  • Bill Amount 文本框设置 Next 操作按钮,用于指示用户已完成当前输入并想移到下一个文本框。
  • Tip Percentage 文本框设置 Done 操作按钮,用于指示用户已完成输入。

包含这些按钮的键盘示例如下面这些图片所示:

添加键盘选项:

  1. EditNumberField() 函数的 TextField() 函数调用中,向 KeyboardOptions 构造函数传递一个值设置为 ImeAction.NextimeAction 具名实参。使用 KeyboardOptions.Default.copy() 函数,确保使用其他默认选项。
import androidx.compose.ui.text.input.ImeAction

@Composable
fun EditNumberField(
    //...
) {
    TextField(
        //...
        keyboardOptions = KeyboardOptions.Default.copy(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Next
        )
    )
}
  1. 在模拟器或设备上运行应用。现在,键盘会显示 Next 操作按钮,如下图所示:

82574a95b658f052.png

请注意,选择 Tip Percentage 文本字段后,键盘同样会显示 Next 操作按钮。不过,您需要为文本字段设置两个不同的操作按钮。您很快将解决该问题。

  1. 检查 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
        )
    )
}
  1. EditNumberField() 函数定义中,添加类型为 KeyboardOptionskeyboardOptions 形参。在函数主体中,将其分配给 TextField() 函数的 keyboardOptions 具名形参:
@Composable
fun EditNumberField(
    @StringRes label: Int,
    keyboardOptions: KeyboardOptions,
    // ...
){
    TextField(
        //...
        keyboardOptions = keyboardOptions
    )
}
  1. TipTimeLayout() 函数中,更新第一个 EditNumberField() 函数调用,为 Bill Amount 文本字段传入 keyboardOptions 具名形参:
EditNumberField(
    label = R.string.bill_amount,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Next
    ),
    // ...
)
  1. 在第二个 EditNumberField() 函数调用中,将 Tip Percentage 文本字段的 imeAction 更改为 ImeAction.Done。您的函数应如以下代码段所示:
EditNumberField(
    label = R.string.how_was_the_service,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Done
    ),
    // ...
)
  1. 运行应用。它会显示 NextDone 操作按钮,如下面这些图片所示:

  1. 输入任意账单金额并点击 Next 操作按钮,然后输入任意小费百分比并点击 Done 操作按钮。点击“Done”后会关闭拨号键盘。

a9e3fbddfff829c8.gif

6. 添加开关

开关可用来开启或关闭单个项的状态。

6923dfb1101602c7.png

切换开关具有两种状态,可让用户在两个选项之间进行选择。切换开关由滑道、滑块和可选图标组成,如下面这些图片所示:

b4f7f68b848bcc2b.png

开关属于选择控件,可用于输入决策或声明偏好设置,例如下图所示的设置:

5cd8acb912ab38eb.png

用户可以前后拖动滑块来选择所选选项,或者直接点按开关进行切换。下面的 GIF 显示了另一个切换开关示例,其中的“Visual options”(视觉选项)设置切换为 Dark mode(深色模式)

eabf96ad496fd226.gif

如需详细了解开关,请参阅开关文档。

您将使用 Switch 可组合项,以便用户选择是否将小费向上取整为最接近的整数,如下图所示:

b42af9f2d3861e4.png

TextSwitch 可组合项添加一个代码行:

  1. EditNumberField() 函数后面,添加一个 RoundTheTipRow() 可组合函数,然后将默认的 Modifier 作为实参传入,类似于 EditNumberField() 函数:
@Composable
fun RoundTheTipRow(modifier: Modifier = Modifier) {
}
  1. 实现 RoundTheTipRow() 函数,添加一个具有以下 modifierRow 布局可组合项,以将子元素的宽度设置为屏幕上的最大值,居中对齐,并确保尺寸为 48dp
Row(
   modifier = modifier
       .fillMaxWidth()
       .size(48.dp),
   verticalAlignment = Alignment.CenterVertically
) {
}
  1. 导入以下代码:
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
  1. Row 布局可组合项的 lambda 代码块中,添加一个使用 R.string.round_up_tip 字符串资源来显示 Round up tip? 字符串的 Text 可组合项:
Text(text = stringResource(R.string.round_up_tip))
  1. Text 可组合项后面,添加一个 Switch 可组合项,然后传递一个设置为 roundUpchecked 具名形参和一个设置为 onRoundUpChangedonCheckedChange 具名形参。
Switch(
    checked = roundUp,
    onCheckedChange = onRoundUpChanged,
)

下表列出了这些形参(与您为 RoundTheTipRow() 函数定义的形参相同)的相关信息:

形参

说明

checked

指示开关是否处于选中状态。这是 Switch 可组合项的状态。

onCheckedChange

点击开关时要调用的回调。

  1. 导入以下代码:
import androidx.compose.material3.Switch
  1. RoundTheTipRow() 函数中,添加一个类型为 BooleanroundUp 形参和一个接受 Boolean 且不返回任何内容的 onRoundUpChanged lambda 函数:
@Composable
fun RoundTheTipRow(
    roundUp: Boolean,
    onRoundUpChanged: (Boolean) -> Unit,
    modifier: Modifier = Modifier
)

这会提升开关的状态。

  1. Switch 可组合项中,添加以下 modifier,以将 Switch 可组合项与屏幕末端对齐:
       Switch(
           modifier = modifier
               .fillMaxWidth()
               .wrapContentWidth(Alignment.End),
           //...
       )
  1. 导入以下代码:
import androidx.compose.foundation.layout.wrapContentWidth
  1. TipTimeLayout() 函数中,为 Switch 可组合项的状态添加一个 var 变量。创建一个名为 roundUpvar 变量,将其设置为 mutableStateOf(),并将 false 作为初始值。使用 remember { } 括住调用。
fun TipTimeLayout() {
    //...
    var roundUp by remember { mutableStateOf(false) }

    //...
    Column(
        ...
    ) {
      //...
   }
}

这就是 Switch 可组合项状态的变量,默认状态为 false。

  1. TipTimeLayout() 函数的 Column 代码块的 Tip Percentage 文本字段后面,使用以下实参调用 RoundTheTipRow() 函数:一个设置为 roundUproundUp 具名形参,以及一个设置为 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? 行。

  1. 运行应用。应用会显示 Round up tip? 切换开关。

5225395a29022a5e.png

  1. 输入账单金额和小费百分比,然后让 Round up tip? 切换开关处于开启状态。小费金额并没有取整,这是因为您还需要更新 calculateTip() 函数,您将在下一部分中执行此操作。

更新 calculateTip() 函数以对小费进行取整

修改 calculateTip() 函数以接受 Boolean 变量,从而将小费向上取整为最接近的整数:

  1. calculateTip() 函数需要知道开关的状态(该状态为 Boolean 值)才能对小费进行向上取整。在 calculateTip() 函数中,添加类型为 BooleanroundUp 形参:
private fun calculateTip(
    amount: Double,
    tipPercent: Double = 15.0,
    roundUp: Boolean
): String { 
    //...
}
  1. calculateTip() 函数中的 return 语句前面,添加一个 if() 条件来检查 roundUp 值。如果 roundUptrue,则定义一个 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)
}
  1. TipTimeLayout() 函数中,更新 calculateTip() 函数调用,然后传入 roundUp 形参:
val tip = calculateTip(amount, tipPercent, roundUp)
  1. 运行应用。现在,它会将小费金额向上取整,如下面这些图片所示:

7. 添加对横屏显示的支持

Android 设备有多种外形规格(手机、平板电脑、可折叠设备和 ChromeOS 设备),并且屏幕尺寸各异。您的应用应同时支持竖屏和横屏显示。

  1. 在横屏模式下测试应用,开启自动屏幕旋转。

8566fc367d5a5b2f.png

  1. 将模拟器或设备向左旋转,注意您看不到小费金额。要解决此问题,您需要一个垂直滚动条,用于滚动应用界面。

28d23a73c2a5ea24.png

  1. 向修饰符添加 .verticalScroll(rememberScrollState()),使列能够垂直滚动。rememberScrollState() 会创建并自动记住滚动状态。
@Composable
fun TipTimeLayout() {
    // ...
    Column(
        modifier = Modifier
            .padding(40.dp)
            .verticalScroll(rememberScrollState()),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        //...
    }
}
  1. 导入以下代码:
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
  1. 再次运行应用。尝试在横屏模式中滚动!

179866a0fae00401.gif

8. 向文本字段添加前置图标(可选)

使用图标可以使文本字段在视觉上更具吸引力,并且能提供有关文本字段的更多信息。图标可用于传达文本字段用途的相关信息,例如预期输入的数据类型或所需的输入类型。例如,在文本字段旁边显示手机图标,可能表示用户需要输入电话号码。

图标可以为用户提供关于预期输入内容的提示,从而为用户的输入提供指导。例如,在文本字段旁边放置一个日历图标,可能表示用户需要输入日期。

以下是一个带有搜索图标的文本字段示例,提示用户输入搜索字词。

9318c9a2414c4add.png

EditNumberField() 可组合项中添加一个名为 leadingIcon 的参数,类型为 Int,并使用 @DrawableRes 为其添加注解。

@Composable
fun EditNumberField(
    @StringRes label: Int,
    @DrawableRes leadingIcon: Int,
    keyboardOptions: KeyboardOptions,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
) 
  1. 导入以下代码:
import androidx.annotation.DrawableRes
import androidx.compose.material3.Icon
  1. 为文本字段添加前置图标。leadingIcon 采用可组合项作为参数,您要传入以下 Icon 可组合项。
TextField(
    value = value,
    leadingIcon = { Icon(painter = painterResource(id = leadingIcon), null) },
    //...
)
  1. 将前置图标传入文本字段。为方便起见,起始代码中已包含图标。
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
)
  1. 运行应用。

bff007b9d67ede83.png

恭喜!您的应用现在可以用来计算自定义小费了。

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 标签在社交媒体上分享您的作品!

了解详情