练习:点击行为

1. 准备工作

在此在线课程中,您已经学习了如何向应用添加按钮,以及如何修改应用以响应按钮点击操作。现在,是时候通过构建应用来将所学知识应用于实践了。

您将制作一款名为 Lemonade 的应用。首先,请参阅 Lemonade 应用的相关要求,了解该应用应当具备的外观和行为。如果您想挑战自我,可以根据相关要求自行构建该应用。如果遇到问题,您可以参阅后续部分,以获取更多提示和指导,了解如何分解问题并逐步加以解决。

您可以按照自己的节奏来完成此实践项目。请尽力而为,争取将该应用功能的方方面面一一构建出来,不必在乎花费多少时间。Lemonade 应用的解决方案代码就在文末,但我们建议您先尝试自行构建该应用,然后再查看解决方案。请注意,提供的解决方案不是构建 Lemonade 应用的唯一方式,因此只要符合应用要求,您完全可以通过其他方式来构建该应用。

前提条件

  • 能够在 Compose 中使用 Text 和 Image 可组合函数制作简单的界面布局
  • 能够构建可响应按钮点击操作的交互式应用
  • 对组合和重组有基本的了解
  • 熟悉 Kotlin 编程语言的基础知识,包括函数、变量、条件和 lambda

所需条件

  • 一台可连接到互联网并安装了 Android Studio 的电脑。

2. 应用概览

您将协助我们将制作数字柠檬汁的愿景变为现实!我们的目标是构建一款简单的交互式应用,让您只要点击屏幕上的图片即可将柠檬榨成汁,直到您得到满满一杯柠檬汁。您可以将其视为现实场景的数字模拟,或是一种消遣时间的有趣方式!

dfcc3bc3eb43e4dd.png

该应用的运作方式如下:

  1. 用户首次启动应用时,会看到一棵柠檬树。此时会有一个标签提示用户点按柠檬树图片,以“select”(选择)树上的柠檬。
  2. 用户点按柠檬树后,会看到一颗柠檬。系统会提示用户点按柠檬,以将柠檬“榨”成柠檬汁。他们需要多次点按柠檬才能完成榨汁。榨柠檬汁所需的点按次数每次都不一样,并且是随机生成的介于 2 到 4(含 2 和 4)之间的数字。
  3. 在点按柠檬的次数达到要求后,用户会看到一杯清爽的柠檬汁!系统会提示用户点按杯子以“饮用”柠檬汁。
  4. 点按盛着柠檬汁的杯子后,用户会看到空杯子。系统会提示用户点按空杯子以重新开始。
  5. 点按空杯子后,用户会看到柠檬树,并可以重新开始制作流程。再来一杯柠檬汁!

以下是更大尺寸的屏幕截图,展示了这款应用的外观:

对于制作柠檬汁的每个步骤,屏幕中都会显示不同的图片和文本标签,应用也会通过不同的行为来响应点击。例如,当用户点按柠檬树时,应用会显示柠檬。

您的任务是构建应用的界面布局并实现相应逻辑,以便用户能够完成制作柠檬汁的所有步骤。

3. 开始

创建项目

在 Android Studio 中,使用 Empty Activity 模板创建一个新项目,其详细信息如下:

  • 名称:Lemonade
  • 软件包名称:com.example.lemonade
  • 最低 SDK 版本:24

成功创建应用并构建项目后,继续执行下一部分的操作。

添加图片

我们为您提供了四个矢量可绘制对象文件以供您在 Lemonade 应用中使用。

获取文件:

  1. 为应用下载图片的 ZIP 文件
  2. 双击该 ZIP 文件。此步骤会将图片解压缩到一个文件夹中。
  3. 将图片添加到应用的 drawable 文件夹中。如果您不记得如何操作,请参阅 Codelab 课程“创建交互式 Dice Roller 应用”

项目文件夹应如以下屏幕截图所示(其中 lemon_drink.xmllemon_restart.xmllemon_squeeze.xmllemon_tree.xml 资源现在会显示在 res > drawable 目录下):

ccc5a4aa8a7e9fbd.png

  1. 双击矢量可绘制对象文件以查看图片预览。
  2. 选择 Design 窗格(而非 CodeSplit 视图)以全宽视图查看图片。

3f3a1763ac414ec0.png

在应用中添加图片文件后,您可以在代码中引用它们。例如,如果矢量可绘制对象文件名为 lemon_tree.xml,那么在 Kotlin 代码中,您可以使用其资源 ID 以 R.drawable.lemon_tree 格式来引用该可绘制对象。

添加字符串资源

res > values > strings.xml 文件中,将以下字符串添加到项目中:

  • Tap the lemon tree to select a lemon
  • Keep tapping the lemon to squeeze it
  • Tap the lemonade to drink it
  • Tap the empty glass to start again

您的项目中还需要以下字符串。它们不会显示在屏幕上的界面中,但会在应用中用于对图片进行内容说明,从而说明图片包含哪些内容。在应用的 strings.xml 文件中添加以下附加字符串:

  • Lemon tree
  • Lemon
  • Glass of lemonade
  • Empty glass

如果您不记得如何在应用中声明字符串资源,请参阅 Codelab 课程“创建交互式 Dice Roller 应用”字符串。为每个字符串资源提供适当的标识符名称,用于说明其包含的值。例如,对于字符串 "Lemon",您可以在 strings.xml 文件中使用标识符名称 lemon_content_description 声明它,然后使用以下资源 ID 在代码中引用它:R.string.lemon_content_description

制作柠檬汁的步骤

现在,您已具备实现应用所需的字符串资源和图片资源。下面概要介绍了应用的每个步骤以及屏幕上显示的相应内容:

第 1 步:

  • 文本:Tap the lemon tree to select a lemon
  • 图片:柠檬树 (lemon_tree.xml)

b2b0ae4400c0d06d.png

第 2 步:

  • 文本:Keep tapping the lemon to squeeze it
  • 图片:柠檬(lemon_squeeze.xml

7c6281156d027a8.png

第 3 步:

  • 文本:Tap the lemonade to drink it
  • 图片:满满一杯柠檬汁 (lemon_drink.xml)

38340dfe3df0f721.png

第 4 步:

  • 文本:Tap the empty glass to start again
  • 图片:空杯子 (lemon_restart.xml)

e9442e201777352b.png

改进外观

为使您的应用版本如这些最终屏幕截图所示,您还要在应用中进行再一些的视觉调整:

  • 增大文本的字体大小,使其大于默认字体大小(例如 18sp)。
  • 在文本标签及其下方的图片之间添加额外的间距,以免其距离太近(例如 16dp)。
  • 为按钮设置强调色和略圆角,以便用户点按图片。

如果您想挑战自我,可以根据运作方式说明来构建应用的其余部分。如果您需要更多指导,请继续学习下一部分。

4. 规划如何构建应用

构建应用时,最好先完成应用的基础工作版本。然后逐步添加更多功能,直到完成所有需要的功能为止。确定您可以先行构建的一小部分端到端功能。

在 Lemonade 应用中,请注意,应用的关键部分是从一个步骤转换到另一个步骤,并且每次都会显示不同的图片和文本标签。最初,您可以忽略榨汁状态的特殊行为,因为您以后可以在构建应用的基础部分后再添加该功能。

以下提案简要说明了构建应用所需执行的各个步骤:

  1. 为制作柠檬汁的第一个步骤(即提示用户从树上选择柠檬)构建界面布局。您可以暂时跳过图片周围的边框,因为这是一种视觉细节,您可以以后再添加。

b2b0ae4400c0d06d.png

  1. 在应用中实现该行为,以便在用户点按柠檬树时,应用会显示柠檬图片及其对应的文本标签。这涵盖制作柠檬汁的前两个步骤。

adbf0d217e1ac77d.png

  1. 添加代码,以便每当用户点按图片时,应用都能显示制作柠檬汁的其余步骤。此时,点按一次柠檬即可转换到一杯柠檬汁的界面。

4 个方框水平排成一行,每个方框都带有绿色边框。每个方框均包含 1 到 4 之间的一个数字。一个箭头从方框 1 指向方框 2,一个箭头从方框 2 指向方框 3,一个箭头从方框 3 指向方框 4,还有一个箭头从方框 4 指向方框 1。方框 1 下方是内容为“Tap the lemon tree to select a lemon”(点按柠檬树即可选择柠檬)的文本标签,以及柠檬树的图片。方框 2 下方是内容为“Keep tapping the lemon to squeeze it”(连续点按柠檬即可榨汁)的文本标签,以及柠檬的图片。方框 3 下方是内容为“Tap the lemonade to drink it”(点按柠檬汁即可饮用)的文本标签,以及一杯柠檬汁的图片。方框 4 下方是内容为“Tap the empty glass to start again”(点按空杯子即可重新开始)的文本标签,以及空杯子的图片。

  1. 为柠檬榨汁步骤添加自定义行为,以便用户需要点按以“压榨”柠檬达到特定次数(随机生成且在 2 到 4 之间)。

4 个方框水平排成一行,每个方框都带有绿色边框。每个方框均包含 1 到 4 之间的一个数字。一个箭头从方框 1 指向方框 2,一个箭头从方框 2 指向方框 3,一个箭头从方框 3 指向方框 4,还有一个箭头从方框 4 指向方框 1。方框 2 下方还有一个箭头伸出又折回,并带有内容为“Random number of times”(次数随机)的文本标签。方框 1 下方是柠檬树的图片及其对应的文本标签。方框 2 下方是柠檬的图片及其对应的文本标签。方框 3 下方是一杯柠檬汁的图片及其对应的文本标签。方框 4 下方是空杯子的图片及其对应的文本标签。

  1. 进行任何其他必要的视觉细节修饰,以完成应用构建。例如,更改字体大小并在图片周围添加边框,使应用看起来更精美。验证应用是否符合良好的编码做法,例如是否遵循 Kotlin 编码样式准则,以及是否在代码中添加了注释。

如果您可以使用这些简要步骤来指导自己实现 Lemonade 应用,请继续自行构建该应用。如果您发现这 5 个步骤分别需要额外指导才能完成,请继续学习下一部分。

5. 实现应用

构建界面布局

首先修改应用,使其在屏幕中央显示柠檬树的图片及其对应的文本标签(其内容为 Tap the lemon tree to select a lemon)。文本与其下方的图片之间还应留出 16dp 的间距。

b2b0ae4400c0d06d.png

如果有帮助,您可以在 MainActivity.kt 文件中使用以下起始代码:

package com.example.lemonade

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.lemonade.ui.theme.LemonadeTheme

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           LemonadeTheme {
               LemonApp()
           }
       }
   }
}

@Composable
fun LemonApp() {
   // A surface container using the 'background' color from the theme
   Surface(
       modifier = Modifier.fillMaxSize(),
       color = MaterialTheme.colorScheme.background
   ) {
       Text(text = "Hello there!")
   }
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
   LemonadeTheme {
       LemonApp()
   }
}

该代码与 Android Studio 自动生成的代码类似。不过,我们没有定义 Greeting() 可组合函数,而是定义了一个 LemonApp() 可组合函数,并且该可组合函数不需要形参。DefaultPreview() 可组合函数也已更新为使用 LemonApp() 可组合函数,以便您能够轻松预览代码。

在 Android Studio 中输入该代码后,修改 LemonApp() 可组合函数(其中应包含应用的内容)。下面这些问题可以对您的思考过程加以引导:

  • 您将使用哪些可组合函数?
  • 是否有标准的 Compose 布局组件可以帮助您将可组合函数排列到所需位置?

继续实现该步骤,以便在应用启动时让柠檬树和文本标签显示在应用中。在 Android Studio 中预览您的可组合函数,看看在修改代码时界面的显示效果是怎样的。运行应用,确保其如此部分前面显示的屏幕截图所示。

如需获取有关如何在用户点按图片时添加行为的更多指导,请在完成后返回这些说明。

添加点击行为

接下来,您将添加代码,以便当用户点按柠檬树的图片时,系统会显示柠檬的图片以及文本标签 Keep tapping the lemon to squeeze it。也就是说,当您点按柠檬树时,文本和图片都会更改。

adbf0d217e1ac77d.png

在此在线课程的前面部分,您学习了如何将按钮设为可点按。对于 Lemonade 应用,我们没有 Button 可组合函数。不过,您可以将任何可组合函数(而不仅仅是按钮)设为可点击,只要对其指定 clickable 修饰符即可。如需查看示例,请参阅可点击文档页面。

用户点击图片后会发生什么情况?实现该行为所需的代码非常重要,因此请后退一步,回顾一款您所熟悉的应用。

查看 Dice Roller 应用

回顾 Dice Roller 应用中的代码,观察该应用如何根据掷骰子的值来显示不同的骰子图片:

Dice Roller 应用中的 MainActivity.kt

...

@Composable
fun DiceWithButtonAndImage(modifier: Modifier = Modifier) {
   var result by remember { mutableStateOf(1) }
   val imageResource = when(result) {
       1 -> R.drawable.dice_1
       2 -> R.drawable.dice_2
       3 -> R.drawable.dice_3
       4 -> R.drawable.dice_4
       5 -> R.drawable.dice_5
       else -> R.drawable.dice_6
   }
   Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
       Image(painter = painterResource(id = imageResource), contentDescription = result.toString())
       Button(onClick = { result = (1..6).random() }) {
          Text(stringResource(id = R.string.roll))
       }
   }
}

...

请回答以下有关 Dice Roller 应用代码的问题:

  • 哪个变量的值决定了要显示的适当骰子图片?
  • 用户执行的哪个操作会触发该变量发生更改?

DiceWithButtonAndImage() 可组合函数将最新的掷骰子结果存储在 result 变量中,该变量使用以下代码行中的 remember 可组合函数和 mutableStateOf() 函数进行定义:

var result by remember { mutableStateOf(1) }

result 变量更新为新值时,Compose 会触发 DiceWithButtonAndImage() 可组合函数的重组,这意味着该可组合函数将再次执行。系统会在重组时记住 result 值,因此当 DiceWithButtonAndImage() 可组合函数再次运行时,系统会使用最新的 result 值。对 result 变量的值使用 when 语句,此时该可组合函数会确定要显示的新可绘制资源 ID,而 Image 可组合函数会显示此 ID。

将您学到的知识运用到 Lemonade 应用中

现在,请回答有关 Lemonade 应用的类似问题:

  • 是否有哪个变量可用于确定应该在屏幕上显示哪个文本和图片?在代码中定义该变量。
  • 您能否在 Kotlin 中使用条件,让应用根据该变量的值来执行不同的行为?如果能,请在代码中编写相应的条件语句。
  • 用户执行的哪个操作会触发该变量发生更改?在代码中找到发生这种情况的位置。在此处添加代码以更新该变量。

这部分实现起来非常困难,需要对代码进行多处更改才能使其正常运行。如果应用无法立即按预期正常运行,请不要气馁。请注意,实现此行为的正确方法不止一种。

完成后,运行应用并验证其能否正常运行。当您启动应用时,它应该显示柠檬树的图片及其对应的文本标签。如果点按一下柠檬树的图片,系统应该更新文本标签并显示柠檬的图片。现在,点按柠檬的图片应该不会执行任何操作。

添加其余步骤

现在,您的应用可以显示制作柠檬汁的两个步骤!此时,您的 LemonApp() 可组合函数可能会如以下代码段所示。只要应用中的行为相同,代码看起来略有不同也没关系。

MainActivity.kt

...
@Composable
fun LemonApp() {
   // Current step the app is displaying (remember allows the state to be retained
   // across recompositions).
   var currentStep by remember { mutableStateOf(1) }

   // A surface container using the 'background' color from the theme
   Surface(
       modifier = Modifier.fillMaxSize(),
       color = MaterialTheme.colorScheme.background
   ) {
       when (currentStep) {
           1 -> {
               Column (
                   horizontalAlignment = Alignment.CenterHorizontally,
                   verticalArrangement = Arrangement.Center,
                   modifier = Modifier.fillMaxSize()
               ){
                   Text(text = stringResource(R.string.lemon_select))
                   Spacer(modifier = Modifier.height(32.dp))
                   Image(
                       painter = painterResource(R.drawable.lemon_tree),
                       contentDescription = stringResource(R.string.lemon_tree_content_description),
                       modifier = Modifier
                           .wrapContentSize()
                           .clickable {
                               currentStep = 2
                           }
                   )
               }
           }
           2 -> {
               Column (
                   horizontalAlignment = Alignment.CenterHorizontally,
                   verticalArrangement = Arrangement.Center,
                   modifier = Modifier.fillMaxSize()
               ){
                   Text(text = stringResource(R.string.lemon_squeeze))
                   Spacer(modifier = Modifier.height(32
                       .dp))
                   Image(
                       painter = painterResource(R.drawable.lemon_squeeze),
                       contentDescription = stringResource(R.string.lemon_content_description),
                       modifier = Modifier.wrapContentSize()
                   )
               }
           }
       }
   }
}
...

接下来,您将添加制作柠檬汁的其余步骤。如果点按一下图片,系统应该让用户进入制作柠檬汁的下一个步骤,文本和图片也会随即更新。您需要更改代码,使其能够更灵活地处理应用的所有步骤,而不只是前两个步骤。

4 个方框水平排成一行,每个方框都带有绿色边框。每个方框均包含 1 到 4 之间的一个数字。一个箭头从方框 1 指向方框 2,一个箭头从方框 2 指向方框 3,一个箭头从方框 3 指向方框 4,还有一个箭头从方框 4 指向方框 1。方框 1 下方是内容为“Tap the lemon tree to select a lemon”(点按柠檬树即可选择柠檬)的文本标签,以及柠檬树的图片。方框 2 下方是内容为“Keep tapping the lemon to squeeze it”(连续点按柠檬即可榨汁)的文本标签,以及柠檬的图片。方框 3 下方是内容为“Tap the lemonade to drink it”(点按柠檬汁即可饮用)的文本标签,以及一杯柠檬汁的图片。方框 4 下方是内容为“Tap the empty glass to start again”(点按空杯子即可重新开始)的文本标签,以及空杯子的图片。

若要在每次用户点击图片时都呈现不同的行为,您需要自定义可点击行为。更具体地说,在用户点击图片时执行的 lambda 需要知道我们要转到哪个步骤。

您可能开始发现了,您应用中针对制作柠檬汁的各个步骤编写的代码存在有重复内容。对于上一个代码段中的 when 语句,用例 1 的代码与用例 2 非常类似,只存在细微差别。例如,如果有用的话,不妨创建一个名为 LemonTextAndImage() 的新可组合函数,用于在界面中图片上方的位置显示文本。通过创建接受一些输入形参的新可组合函数,您可以获得一个可重复使用的函数,只要您更改所传入的输入,该函数在多个场景中都非常有用。您要负责确定输入形参应该是什么。创建该可组合函数后,更新现有代码以在相关位置调用该新函数。

使用单独的可组合函数(例如 LemonTextAndImage())的另一个好处是,您的代码会变得更有条理、更可靠。调用 LemonTextAndImage() 时,您可以确信文本和图片都会更新为新值。否则,很容易不小心忽略一种情况:显示的图片与更新后的文本标签不匹配。

还有一个提示:您甚至可以将 lambda 函数传递到可组合函数。请务必使用函数类型表示法来指定应传入的函数类型。在以下示例中,我们定义了一个 WelcomeScreen() 可组合函数,它接受两个输入形参:name 字符串和 () -> Unit 类型的 onStartClicked() 函数。也就是说,该函数不接受任何输入(箭头前面的空括号),也不返回任何值(箭头后面的 Unit)。任何与该函数类型 () -> Unit 相匹配的函数都可用于设置此 ButtononClick 处理程序。用户点击该按钮时,系统会调用 onStartClicked() 函数。

@Composable
fun WelcomeScreen(name: String, onStartClicked: () -> Unit) {
    Column {
        Text(text = "Welcome $name!")
        Button(
            onClick = onStartClicked
        ) {
            Text("Start")
        }
    }
}

将 lambda 传递到可组合函数是一种有用的模式,因为这样一来,WelcomeScreen() 可组合函数便可以在不同的场景中重复使用。用户名和按钮的 onClick 行为每次都可能不同,因为它们是作为实参传入的。

掌握这些额外的知识后,请返回您的代码,为应用添加制作柠檬汁的其余步骤。

如果您在如何围绕压榨柠檬的随机次数添加自定义逻辑方面需要更多指导,请回来查看这些说明。

添加榨汁逻辑

太棒了!现在,您已拥有该应用的基础部分。如果点按图片,系统应该会让您从一个步骤进入下一个步骤。接下来要添加需要多次压榨柠檬以制作柠檬汁的行为。用户需要压榨(或点按)柠檬的次数应为介于 2 到 4(含 2 和 4)之间的随机数。每当用户从树上选择新柠檬时,该随机数都不一样。

4 个方框水平排成一行,每个方框都带有绿色边框。每个方框均包含 1 到 4 之间的一个数字。一个箭头从方框 1 指向方框 2,一个箭头从方框 2 指向方框 3,一个箭头从方框 3 指向方框 4,还有一个箭头从方框 4 指向方框 1。方框 2 下方还有一个箭头伸出又折回,并带有内容为“Random number of times”(次数随机)的文本标签。方框 1 下方是柠檬树的图片及其对应的文本标签。方框 2 下方是柠檬的图片及其对应的文本标签。方框 3 下方是一杯柠檬汁的图片及其对应的文本标签。方框 4 下方是空杯子的图片及其对应的文本标签。

下面这些问题可以对您的思考过程加以引导:

  • 如何使用 Kotlin 代码生成随机数?
  • 您应该在代码中的哪个位置生成随机数?
  • 如何确保在用户点按柠檬的次数达到要求后才进入下一步骤?
  • 您是否需要使用 remember 可组合函数来存储任何变量,以便每当屏幕重新绘制时,都能确保数据不会重置。

实现上述更改后,运行应用。验证是否要多次点按柠檬图片才能进入下一个步骤,以及每次所需的点按次数是否为介于 2 到 4 之间的随机数。如果点按一次柠檬图片就会显示盛满柠檬汁的杯子,请返回代码找到缺失的元素,然后重试。

如果您在如何完成应用构建方面需要更多指导,请回来查看这些说明。

完成应用构建

即将大功告成!添加一些最后的细节,让应用更加完善。

请注意,以下是应用外观最终形态的屏幕截图:

  • 在屏幕上垂直和水平居中对齐文本和图片。
  • 将文本的字体大小设为 18sp
  • 在文本和图片之间添加 16dp 的间距。
  • 在图片周围添加 2dp 的细边框,使边框略带 4dp 的圆角。边框的 RGB 颜色值为红色 105、绿色 205、蓝色 216。如需查看有关如何添加边框的示例,您可以使用 Google 搜索。或者,您也可以参阅边框的相关文档。

完成这些更改后,运行应用,然后将其与最终的屏幕截图进行比较,以确保二者一致。

作为一种良好的编程做法,您可以返回代码并在其中添加注释,以便阅读代码的所有人员都能更轻松地理解您的思考过程。将代码中未用到的位于文件顶部的所有 import 语句移除。确保您的代码遵循 Kotlin 样式指南。所有这些工作都有助于让代码更便于他人查看,并使其更易于维护!

祝贺您!您在实现 Lemonade 应用时的表现非常出色!这是一款极具挑战性的应用,许多方面都需要仔细思考。现在,不妨来一杯清爽的柠檬汁犒劳一下自己。干杯!

6. 获取解决方案代码

下载解决方案代码:

或者,您也可以克隆代码的 GitHub 代码库:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-lemonade.git

请注意,您的代码不需要与解决方案代码完全一致,因为该应用有多种实现方式。

此外,您还可以在 Lemonade 应用 GitHub 代码库中浏览相关代码。