练习:点击行为

1. 开始之前

在本学习路径中,您学习了如何在应用中添加按钮以及如何修改应用以响应按钮点击。现在,是时候通过构建应用来练习您学到的知识了。

您将创建一个名为 Lemonade 的应用。首先,阅读 Lemonade 应用的需求,了解应用的外观和行为方式。如果您想挑战自己,可以从那里开始自行构建应用。如果您遇到困难,可以阅读后续部分以获取更多提示和指导,了解如何分解问题并逐步解决。

按照您舒适的速度完成此练习题。构建应用每个功能部分时,请花尽可能多的时间。Lemonade 应用的解决方案代码在最后提供,但建议您在查看解决方案之前尝试自己构建应用。请记住,提供的解决方案不是构建 Lemonade 应用的唯一方法,因此,只要满足应用需求,以其他方式构建应用也是完全有效的。

先决条件

  • 能够使用文本和图像组合在 Compose 中创建简单的 UI 布局
  • 能够构建响应按钮点击的交互式应用
  • 对组合和重新组合的基本理解
  • 熟悉 Kotlin 编程语言的基础知识,包括函数、变量、条件语句和 lambda 表达式

您需要什么

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

2. 应用概述

您将帮助我们实现将数字柠檬水变为现实的愿景!目标是创建一个简单、交互式的应用,让您在点击屏幕上的图像时榨取柠檬,直到您得到一杯柠檬水。可以将其视为一个隐喻,或者仅仅是一种打发时间的好方法!

dfcc3bc3eb43e4dd.png

应用的工作原理如下

  1. 当用户首次启动应用时,他们会看到一棵柠檬树。有一个标签提示他们点击柠檬树图像以“选择”树上的柠檬。
  2. 点击柠檬树后,用户会看到一个柠檬。系统会提示他们点击柠檬以“挤压”它来制作柠檬水。他们需要多次点击柠檬才能挤压它。每次挤压柠檬所需的点击次数都不同,并且是在 2 到 4(含)之间随机生成的数字。
  3. 点击柠檬所需次数后,他们会看到一杯清爽的柠檬水!系统会要求他们点击玻璃杯以“饮用”柠檬水。
  4. 点击柠檬水杯后,他们会看到一个空杯子。系统会要求他们点击空杯子以重新开始。
  5. 点击空杯子后,他们会看到柠檬树,并且可以再次开始此过程。再来一杯柠檬水吧!

以下是应用外观的较大屏幕截图

对于制作柠檬水的每个步骤,屏幕上都会显示不同的图像和文本标签,以及应用如何响应点击的不同行为。例如,当用户点击柠檬树时,应用会显示一个柠檬。

您的任务是构建应用的 UI 布局并实现用户完成所有步骤以制作柠檬水的逻辑。

3. 开始使用

创建项目

在 Android Studio 中,使用以下详细信息创建一个使用**空活动**模板的新项目

  • 名称:Lemonade
  • 包名:com.example.lemonade
  • 最低 SDK:24

应用成功创建并构建项目后,继续下一部分。

添加图像

为您提供了四个将在 Lemonade 应用中使用的矢量可绘制文件。

获取文件

  1. 下载应用的图像 zip 文件
  2. 双击 zip 文件。此步骤会将图像解压缩到一个文件夹中。
  3. 将图像添加到应用的drawable文件夹中。如果您不记得如何执行此操作,请参阅创建交互式骰子摇骰器应用代码实验室

您的项目文件夹应如下面的屏幕截图所示,其中lemon_drink.xmllemon_restart.xmllemon_squeeze.xmllemon_tree.xml资产现在显示在**res > drawable**目录下

ccc5a4aa8a7e9fbd.png

  1. 双击矢量可绘制文件以查看图像预览。
  2. 选择**设计**窗格(而不是**代码**或**拆分**视图)以查看图像的全宽视图。

3f3a1763ac414ec0.png

将图像文件包含在您的应用中后,您可以在代码中引用它们。例如,如果矢量可绘制文件名为lemon_tree.xml,则在 Kotlin 代码中,您可以使用其资源 ID 以R.drawable.lemon_tree的格式引用该可绘制文件。

添加字符串资源

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

  • 点击柠檬树选择一个柠檬
  • 继续点击柠檬挤压它
  • 点击柠檬水喝掉它

  • 点击空杯子重新开始

以下字符串也需要在您的项目中使用。它们不会在用户界面屏幕上显示,但用于应用中图像的内容描述,以描述图像的内容。将这些额外的字符串添加到应用的strings.xml文件中

  • 柠檬树
  • 柠檬
  • 一杯柠檬汁
  • 空杯子

如果您不记得如何在应用中声明字符串资源,请参阅创建交互式骰子摇骰器应用代码实验室或参考字符串。为每个字符串资源提供一个合适的标识符名称,以描述其包含的值。例如,对于字符串"Lemon",您可以在strings.xml文件中使用标识符名称lemon_content_description声明它,然后在代码中使用资源 ID 参考它:R.string.lemon_content_description

制作柠檬汁的步骤

现在您已经拥有了实现应用所需的字符串资源和图像资产。以下是应用每个步骤的摘要以及屏幕上显示的内容

步骤 1

  • 文本:点击柠檬树选择一个柠檬
  • 图像:柠檬树 (lemon_tree.xml)

b2b0ae4400c0d06d.png

步骤 2

  • 文本:继续点击柠檬来挤压它
  • 图像:柠檬 (lemon_squeeze.xml)

7c6281156d027a8.png

步骤 3

  • 文本:点击柠檬汁喝掉它
  • 图像:满杯柠檬汁 (lemon_drink.xml)

38340dfe3df0f721.png

步骤 4

  • 文本:点击空杯子重新开始
  • 图像:空杯子 (lemon_restart.xml)

e9442e201777352b.png

添加视觉修饰

要使您版本的应用看起来像这些最终屏幕截图,需要在应用中进行一些额外的视觉调整

  • 增加文本的字体大小,使其大于默认字体大小(例如18sp)。
  • 在文本标签和其下方的图像之间添加额外的间距,以便它们彼此之间不要太靠近(例如16dp)。
  • 为按钮提供一个强调色和略微圆角,让用户知道他们可以点击图像。

如果您想挑战自己,可以根据其工作方式的描述构建应用的其余部分。如果您需要更多指导,请继续下一节。

4. 计划如何构建应用

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

在柠檬汁应用中,请注意应用的关键部分是从一个步骤过渡到另一个步骤,每次显示不同的图像和文本标签。最初,您可以忽略挤压状态的特殊行为,因为您可以在构建应用基础后稍后添加此功能。

以下是构建应用可以采取的步骤的高级概述建议

  1. 构建制作柠檬汁第一步的 UI 布局,提示用户从树上选择一个柠檬。您现在可以跳过图像周围的边框,因为这是一种以后可以添加的视觉细节。

b2b0ae4400c0d06d.png

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

adbf0d217e1ac77d.png

  1. 添加代码,以便当每次点击图像时,应用显示制作柠檬汁的其余步骤。此时,只需点击一次柠檬就可以过渡到显示柠檬汁。

There are 4 boxes in a horizontal row, each with a green border. Each box contains a number from 1 to 4. There is an arrow from box 1 to box 2, from box 2 to box 3, from box 3 to box 4, and from box 4 to box 1. Under box 1, there is a text label that says; Tap the lemon tree to select a lemon; and a lemon tree image. Under box 2, there is a text label that says; Keep tapping the lemon to squeeze it; and a lemon image. Under box 3, there is a text label that says;Tap the lemonade to drink it; and the image of a glass of lemonade. Under box 4, there is a text label that says;Tap the empty glass to start again; and the image of an empty glass.

  1. 为柠檬挤压步骤添加自定义行为,以便用户需要“挤压”或点击柠檬特定次数,该次数是从 2 到 4 随机生成的。

There are 4 boxes in a horizontal row, each with a green border. Each box contains a number from 1 to 4. There is an arrow from box 1 to box 2, from box 2 to box 3, from box 3 to box 4, and from box 4 to box 1. There is an additional arrow from box 2 back to itself with a label that says; Random number of times. Under box 1 is the image of the lemon tree and the corresponding text label. Under box 2 is the image of the lemon and the corresponding text label. Under box 3 is the image of the glass of lemonade and the corresponding text label. Under box 4 is the image of the empty glass and the corresponding text label.

  1. 使用任何其他必要的视觉修饰细节完成应用。例如,更改字体大小并在图像周围添加边框,使应用看起来更精致。验证应用是否遵循良好的编码实践,例如遵守Kotlin 编码风格指南并在代码中添加注释。

如果您能使用这些高级步骤来指导您实施柠檬汁应用,请继续自行构建应用。如果您发现需要针对这五个步骤中的每一个步骤获得更多指导,请继续下一节。

5. 实现应用

构建 UI 布局

首先修改应用,使其在屏幕中央显示柠檬树的图像及其对应的文本标签,该标签显示点击柠檬树选择一个柠檬。文本和其下方的图像之间也应该有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 中的可组合项,以查看在修改代码时 UI 的外观。运行应用以确保其外观与您在本节前面看到的屏幕截图相同。

如果您想获得有关如何在点击图像时添加行为的更多指导,请在完成后返回这些说明。

添加点击行为

接下来,您将添加代码,以便当用户点击柠檬树的图像时,柠檬的图像会与文本标签继续点击柠檬来挤压它一起出现。换句话说,当您点击柠檬树时,它会导致文本和图像发生变化。

adbf0d217e1ac77d.png

在本路径的前面,您学习了如何使按钮可点击。在柠檬汁应用中,没有Button可组合项。但是,当您在其上指定clickable修饰符时,您可以使任何可组合项(不仅仅是按钮)可点击。例如,请参阅clickable文档页面。

单击图像后会发生什么?实现此行为的代码并非易事,因此请退一步重新审视一个熟悉的应用。

查看骰子摇骰器应用

重新审视骰子摇骰器应用中的代码,以观察应用如何根据骰子掷出的值显示不同的骰子图像

骰子摇骰器应用中的 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))
       }
   }
}

...

回答有关骰子摇骰器应用代码的以下问题

  • 哪个变量的值决定了要显示的适当骰子图像?
  • 用户的什么操作会触发该变量发生变化?

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

var result by remember { mutableStateOf(1) }

result变量更新为新值时,Compose 会触发DiceWithButtonAndImage()可组合项的重新组合,这意味着可组合项将再次执行。result值在重新组合中会得到保留,因此当DiceWithButtonAndImage()可组合项再次运行时,将使用最近的result值。使用result变量的值上的when语句,可组合项确定要显示的新可绘制资源 ID,并且Image可组合项会显示它。

将您学到的知识应用于柠檬汁应用

现在回答有关柠檬汁应用的类似问题

  • 是否存在您可以用来确定在屏幕上显示什么文本和图像的变量?在代码中定义该变量。
  • 您是否可以在 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()
                   )
               }
           }
       }
   }
}
...

接下来,你将添加制作柠檬水的其余步骤。点击图片一次,应该将用户引导至制作柠檬水的下一步,文本和图片都会更新。你需要更改你的代码,使其更灵活地处理应用的所有步骤,而不仅仅是前两个步骤。

There are 4 boxes in a horizontal row, each with a green border. Each box contains a number from 1 to 4. There is an arrow from box 1 to box 2, from box 2 to box 3, from box 3 to box 4, and from box 4 to box 1. Under box 1, there is a text label that says; Tap the lemon tree to select a lemon; and the image of a lemon tree . Under box 2, there is a text label that says; Keep tapping the lemon to squeeze it; and the image of a lemon. Under box 3, there is a text label that says; Tap the lemonade to drink it; and the image of a glass of lemonade. Under box 4, there is a text label that says; Tap the empty glass to start again; and the image of an empty glass.

为了每次点击图片时都有不同的行为,你需要自定义可点击行为。更具体地说,在点击图片时执行的 lambda 需要知道我们正在切换到哪个步骤。

你可能会注意到,在你的应用中,制作柠檬水的每个步骤都存在重复的代码。对于前面代码片段中的 when 语句,case 1 的代码与 case 2 非常相似,只有细微差别。如果方便的话,可以创建一个新的可组合函数,例如名为 LemonTextAndImage() 的函数,该函数在 UI 中显示图片上方的文本。通过创建一个接收一些输入参数的新可组合函数,你将获得一个可在多种场景中使用的可重用函数,只要你更改传入的输入即可。你需要确定输入参数应该是什么。创建此可组合函数后,更新现有代码以在相关位置调用此新函数。

拥有像 LemonTextAndImage() 这样的单独可组合函数的另一个好处是,你的代码变得更有条理且更健壮。当你调用 LemonTextAndImage() 时,可以确保文本和图片都会更新为新值。否则,很容易意外地错过一个显示更新文本标签但使用错误图片的情况。

这里有一个额外的提示:你甚至可以将 lambda 函数传递给可组合函数。请务必使用函数类型表示法来指定应该传入哪种类型的函数。在以下示例中,定义了一个 WelcomeScreen() 可组合函数,并接受两个输入参数:一个 name 字符串和一个类型为 () -> UnitonStartClicked() 函数。这意味着该函数不接收任何输入(箭头之前的空括号),也没有返回值(箭头之后的 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)之间的随机数。每次用户从树上摘下一个新柠檬时,此随机数都会不同。

There are 4 boxes in a horizontal row, each with a green border. Each box contains a number from 1 to 4. There is an arrow from box 1 to box 2, from box 2 to box 3, from box 3 to box 4, and from box 4 to box 1. There is an additional arrow from box 2 back to itself with a label that says; Random number of times; Under box 1 is an image of a lemon tree and the corresponding text label. Under box 2 is the image of the lemon and the corresponding text label. Under box 3 is the image of the glass of lemonade and the corresponding text label. Under box 4 is the image of the empty glass and the corresponding text label.

以下是一些问题来指导你的思考过程

  • 如何在 Kotlin 中生成随机数?
  • 在代码的哪个位置应该生成随机数?
  • 如何在用户点击柠檬所需的次数之前移动到下一步?
  • 是否需要使用 remember 可组合函数存储任何变量,以便数据不会在每次屏幕重绘时重置?

完成此更改的实现后,运行应用。验证是否需要多次点击柠檬图片才能移动到下一步,并且每次所需的点击次数都是 2 到 4 之间的随机数。如果点击柠檬图片一次就显示了柠檬水杯,请返回你的代码,找出缺少的内容并重试。

如果你想获得有关如何完成应用的更多指导,请返回这些说明。

完成应用

你快完成了!添加一些最后的细节来完善应用。

提醒一下,以下是应用外观的最终屏幕截图

  • 在屏幕内垂直和水平居中显示文本和图片。
  • 将文本的字体大小设置为 18sp
  • 在文本和图片之间添加 16dp 的间距。
  • 在图片周围添加 2dp 的细边框,并带有 4dp 的略微圆角。边框的 RGB 颜色值为红色 105、绿色 205 和蓝色 216。有关如何添加边框的示例,你可以在 Google 上搜索。或者你可以参考有关 Border 的文档。

完成这些更改后,运行你的应用,然后将其与最终屏幕截图进行比较,以确保它们匹配。

作为良好的编码实践的一部分,请返回并向你的代码添加注释,以便任何阅读你代码的人都能更容易地理解你的思路。删除文件顶部未在代码中使用的任何导入语句。确保你的代码遵循 Kotlin 样式指南。所有这些努力将有助于使你的代码更易于他人阅读和维护!

干得好!你出色地完成了柠檬水应用的实现!这是一个具有许多需要解决的部分的具有挑战性的应用。现在,给自己倒一杯清爽的柠檬水,享受吧!干杯!

6. 获取解决方案代码

下载解决方案代码

或者,你可以克隆代码的 GitHub 存储库

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

请记住,你的代码不需要与解决方案代码完全匹配,因为有多种方法可以实现该应用。

你也可以在 柠檬水应用 GitHub 存储库 中浏览代码。