计算自定义小费

1. 开始之前

在本 Codelab 中,您将使用来自 Compose 中的状态简介 Codelab 的解决方案代码来构建一个交互式小费计算器,当您输入账单金额和小费百分比时,它可以自动计算和舍入小费金额。您可以在此图片中看到最终应用

d8e768525099378a.png

先决条件

  • Compose 中的状态简介 Codelab。
  • 能够向应用中添加 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 应用开始,该应用提供计算固定小费百分比的小费所需的界面。**账单金额** 文本框允许用户输入服务的费用。该应用在 Text 可组合项中计算并显示小费金额。

运行 Tip Time 应用

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

b6bd5374911410ac.png

在当前实现中,小费百分比硬编码为 15%。在本 Codelab 中,您将通过一个允许应用计算自定义小费百分比并舍入小费金额的文本字段来扩展此功能。

添加必要的字符串资源

  1. 在**项目**选项卡中,点击**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

您的应用中已经有了一个**账单金额**文本字段可组合项,它是无状态的 EditNumberField() 可组合函数。在上一个 Codelab 中,您将 amountInput 状态从 EditNumberField() 可组合项提升到了 TipTimeLayout() 可组合项,这使得 EditNumberField() 可组合项成为无状态的。

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

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

  1. MainActivity.kt 文件中,在 EditNumberField() 可组合函数的参数中,添加一个 label 字符串资源,类型为 Int
@Composable
fun EditNumberField(
    label: Int,
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier
)
  1. 在函数体中,用 label 参数替换硬编码的字符串资源 ID
@Composable
fun EditNumberField(
    //...
) {
     TextField(
         //...
         label = { Text(stringResource(label)) },
         //...
     )
}
  1. 要表示预期 label 参数为字符串资源引用,请使用 @StringRes 注释对函数参数进行注释
@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. 在**预览**窗格中,不应有任何视觉变化。

b223d5ba4a54f792.png

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

这将添加另一个用于自定义小费百分比的文本框。

  1. 应用预览现在显示了一个**小费百分比**文本字段,如本图所示

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. 在模拟器或设备上运行该应用,然后输入账单金额和小费百分比。应用是否正确计算了小费金额?

screen shot with bill amount as 100 tip percentage as 20 and the tip amount is shown as 20 dollars

5. 设置操作按钮

在上一个 Codelab 中,您探讨了如何使用 KeyboardOptions 类来设置键盘的类型。在本节中,您将探讨如何使用相同的 KeyboardOptions 设置键盘操作按钮。键盘操作按钮是键盘末尾的按钮。您可以在此表中看到一些示例

属性

键盘上的操作按钮

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

The image represents the Search icon to execute a search.

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

The image represents the Send icon to send the text in the input field.

ImeAction.Go
当用户想要导航到输入文本的目标时使用。

The image represents the Go icon to navigate to the target of the text in the input.

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

  • **账单金额**文本框的**下一步**操作按钮,表示用户已完成当前输入,并希望转到下一个文本框。
  • **小费百分比**文本框的**完成**操作按钮,表示用户已完成提供输入。

您可以在这些图片中看到带有这些操作按钮的键盘示例

添加键盘选项

  1. 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
        )
    )
}
  1. 在模拟器或设备上运行该应用。键盘现在显示**下一步**操作按钮,如本图所示

82574a95b658f052.png

请注意,当选中**小费百分比**文本字段时,键盘会显示相同的**下一步**操作按钮。但是,您希望文本字段有两个不同的操作按钮。您将很快修复此问题。

  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() 函数调用,为**账单金额**文本字段传递 keyboardOptions 命名参数
EditNumberField(
    label = R.string.bill_amount,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Next
    ),
    // ...
)
  1. 在第二个 EditNumberField() 函数调用中,将**小费百分比**文本字段的 imeAction 更改为 ImeAction.Done。您的函数应如下面的代码片段所示
EditNumberField(
    label = R.string.how_was_the_service,
    keyboardOptions = KeyboardOptions.Default.copy(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Done
    ),
    // ...
)
  1. 运行应用程序。它会显示**下一步**和**完成**操作按钮,如这些图像所示

  1. 输入任意账单金额并点击**下一步**操作按钮,然后输入任意小费百分比并点击**完成**操作按钮。这将关闭键盘。

a9e3fbddfff829c8.gif

6. 添加开关

开关可切换单个项目的打开或关闭状态。

6923dfb1101602c7.png

切换有两个状态,允许用户在两个选项之间进行选择。切换由轨道、滑块和可选图标组成,如这些图像所示

b4f7f68b848bcc2b.png

开关是一种选择控件,可用于输入决策或声明偏好,例如设置,如本图所示

5cd8acb912ab38eb.png

用户可以前后拖动滑块以选择选定的选项,或者只需点击开关即可切换。您可以在此 GIF 中看到切换的另一个示例,其中视觉选项设置切换到**深色模式**

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 块中,添加一个 Text 可组合组件,该组件使用 R.string.round_up_tip 字符串资源显示 四舍五入小费?字符串
Text(text = stringResource(R.string.round_up_tip))
  1. Text 可组合组件之后,添加一个 Switch 可组合组件,并传递一个 checked 命名参数将其设置为 roundUp 和一个 onCheckedChange 命名参数将其设置为 onRoundUpChanged
Switch(
    checked = roundUp,
    onCheckedChange = onRoundUpChanged,
)

此表包含有关这些参数的信息,这些参数与您为 RoundTheTipRow() 函数定义的参数相同

参数

描述

checked

开关是否选中。这是 Switch 可组合组件的状态。

onCheckedChange

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

  1. 导入以下内容
import androidx.compose.material3.Switch
  1. RoundTheTipRow() 函数中,添加一个 roundUp 参数(类型为 Boolean)和一个 onRoundUpChanged lambda 函数,该函数接受一个 Boolean 并返回空值
@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 块中,在**小费百分比**文本字段之后。使用以下参数调用 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(
            ...
        )
    }
}

这将显示**四舍五入小费?**行。

  1. 运行应用程序。应用程序将显示**四舍五入小费?**切换按钮。

5225395a29022a5e.png

  1. 输入账单金额和小费百分比,然后选择**四舍五入小费?**切换按钮。小费金额不会四舍五入,因为您仍然需要更新 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!

了解更多