实践:点击行为

1. 开始之前

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

您将创建一个名为“柠檬水”的应用。首先,阅读柠檬水应用的各项要求,了解应用的外观和行为方式。如果您想挑战自己,可以自行从头开始构建应用。如果您遇到困难,可以阅读后面的章节,以获得更多关于如何分解问题和逐步解决问题的提示和指导。

按照您觉得舒服的节奏来完成这个实践问题。花所需的时间来构建应用的每个部分的功能。柠檬水应用的解决方案代码可在最后找到,但建议您在查看解决方案之前尝试自行构建应用。请记住,提供的解决方案不是构建柠檬水应用的唯一方法,因此只要满足应用要求,以不同的方式构建应用是完全有效的。

前提条件

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

所需物品

  • 一台联网并安装了 Android Studio 的计算机。

2. 应用概览

您将帮助我们将制作数字柠檬水的愿景变为现实!目标是创建一个简单、交互式的应用,让您通过点按屏幕上的图像来榨柠檬汁,直到得到一杯柠檬水。可以将其视为一个比喻,或者仅仅是一种有趣的消磨时间的方式!

dfcc3bc3eb43e4dd.png

应用工作原理如下

  1. 当用户首次启动应用时,会看到一棵柠檬树。有一个标签提示他们点按柠檬树图像来“选择”树上的柠檬。
  2. 点按柠檬树后,用户会看到一个柠檬。系统会提示他们点按柠檬来“榨汁”制作柠檬水。他们需要多次点按柠檬才能榨汁。每次榨汁所需的点按次数都不同,是一个介于 2 到 4 之间(包括 2 和 4)的随机生成的数字。
  3. 在点按柠檬所需的次数后,他们会看到一杯清爽的柠檬水!系统会提示他们点按杯子来“喝”柠檬水。
  4. 点按柠檬水杯后,他们会看到一个空杯子。系统会提示他们点按空杯子重新开始。
  5. 点按空杯子后,他们会看到柠檬树,可以再次开始整个过程。请再来一杯柠檬水!

以下是应用外观的放大截图

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

您的任务是构建应用的界面布局,并实现用户按所有步骤制作柠檬水的逻辑。

3. 入门

创建项目

在 Android Studio 中,使用 Empty Activity 模板创建新项目,并填写以下详细信息

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

应用成功创建且项目构建完成后,请继续下一节。

添加图片

您将获得四个矢量可绘制文件,用于柠檬水应用。

获取文件

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

您的项目文件夹应如下图所示,其中 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 文件中添加以下字符串

  • 点按柠檬树选择一个柠檬
  • 持续点按柠檬进行榨汁
  • 点按柠檬水饮用
  • 点按空杯子重新开始

您的项目中还需要以下字符串。它们不会显示在用户界面的屏幕上,但会用作应用中图片的 Content Description(内容说明),用于描述图片内容。请将这些额外的字符串添加到应用的 strings.xml 文件中

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

如果您不记得如何在应用中声明字符串资源,请参阅创建交互式 Dice Roller 应用 Codelab 或参考String。为每个字符串资源指定一个适当的标识符名称,以描述其包含的值。例如,对于字符串 "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. 规划如何构建应用

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

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

以下是构建应用的高级步骤概览建议

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

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. 实现应用

构建界面布局

首先修改应用,使其在屏幕中心显示柠檬树图片及其相应的文本标签,内容为 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 自动生成的代码。但是,此处定义了一个 LemonApp() 可组合函数,而不是 Greeting() 可组合函数,并且它不接收参数。DefaultPreview() 可组合函数也已更新为使用 LemonApp() 可组合函数,以便您轻松预览代码。

在 Android Studio 中输入此代码后,修改 LemonApp() 可组合函数,其中应包含应用的内容。以下是一些指导您思考过程的问题

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

继续实现此步骤,以便您的应用启动时显示柠檬树和文本标签。在 Android Studio 中预览您的可组合函数,查看修改代码后的界面外观。运行应用,确保其看起来与您在本节前面看到的截图一致。

完成后返回这些说明,如果您需要更多关于如何添加图片点击行为的指导。

添加点击行为

接下来,您将添加代码,以便当用户点按柠檬树图片时,会显示柠檬图片以及文本标签 Keep tapping the lemon to squeeze it。换句话说,点按柠檬树会使文本和图片发生变化。

adbf0d217e1ac77d.png

在本进阶教程的早期,您学习了如何使按钮可点击。在柠檬水应用中,没有 Button 可组合函数。但是,当您在其上指定 clickable 修饰符时,可以使任何可组合函数(而不仅仅是按钮)可点击。例如,请参阅 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 可组合函数会显示它。

将所学知识应用于柠檬水应用

现在回答关于柠檬水应用的类似问题

  • 是否有可以使用来确定屏幕上应显示哪些文本和图片的变量?在您的代码中定义该变量。
  • 您可以使用 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 语句,情况 1 的代码与情况 2 的代码非常相似,只有细微差别。如果对您有帮助,可以创建一个新的可组合函数,例如命名为 LemonTextAndImage(),它在界面中显示图片上方的文本。通过创建一个接受一些输入参数的新可组合函数,您就拥有了一个可重用的函数,只要更改传入的输入参数,它就可以在多种场景中使用。确定输入参数是什么是您的任务。创建此可组合函数后,更新现有代码以在相关位置调用此新函数。

拥有一个单独的可组合函数(如 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 的文档。

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

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

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

6. 获取解决方案代码

下载解决方案代码

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

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

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

您还可以在柠檬水应用 GitHub 仓库中浏览代码。