计算自定义小费

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. 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

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

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

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

  1. MainActivity.kt 文件中,在 EditNumberField() 可组合函数的参数中,添加一个 Int 类型的 label 字符串资源
@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 类型。如果值为 null,则使用 Elvis 运算符并返回 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() 函数调用中,将 ImeAction.Next 值设置给 KeyboardOptions 构造函数的名为 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
        )
    )
}
  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() 可组合函数,然后像 EditNumberField() 函数一样,传入一个默认的 Modifier 作为参数
@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() 函数中,添加一个 Boolean 类型的 roundUp 参数和一个接收 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 块中,在小费百分比文本字段之后。调用 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() 函数中,添加一个 Boolean 类型的 roundUp 参数
private fun calculateTip(
    amount: Double,
    tipPercent: Double = 15.0,
    roundUp: Boolean
): String { 
    //...
}
  1. calculateTip() 函数中,在 return 语句之前,添加一个检查 roundUp 值的 if() 条件。如果 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

向名为 leadingIconEditNumberField() 可组合函数添加另一个 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 标签!

了解更多