计算自定义小费

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>

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. 添加一个小费百分比文本字段

客户可能希望根据提供的服务质量和其他各种原因支付更多或更少的小费。为了适应这一点,应用程序应该允许用户计算自定义小费。在本节中,您将添加一个文本字段,供用户输入自定义小费百分比,如您在此图像中看到的

391b4b1a090687ef.png

您的应用程序中已经有一个**账单金额**文本字段可组合项,它是一个无状态的 EditNumberField() 可组合函数。在上一个 Codelab 中,您从 EditNumberField() 可组合项中提升了 amountInput 状态到 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. 在函数体中,将硬编码的字符串资源 ID 替换为 label 参数
@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() 函数,添加一个 Row 布局可组合项,并使用以下 modifier 将子元素的宽度设置为屏幕上的最大宽度,居中对齐,并确保大小为 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 字符串资源显示 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() 函数中,添加一个 roundUp 参数,类型为 Boolean
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!

了解更多