安卓编程初学者手册第三版-二-

235 阅读1小时+

安卓编程初学者手册第三版(二)

原文:zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:使用 CardView 和 ScrollView 创建美丽的布局

这是我们在花一些时间更专注于 Java 和面向对象编程之前的最后一个布局章节。我们将正式学习一些我们已经遇到的不同属性,并且我们还将介绍两个更酷的布局:ScrollViewCardView。最后,我们将在平板模拟器上运行CardView项目。

在本章中,我们将涵盖以下内容:

  • UI 属性的快速总结

  • 使用ScrollViewCardView构建我们迄今为止最整洁的布局

  • 创建和使用平板模拟器

让我们首先回顾一些属性。

技术要求

您可以在 GitHub 上找到本章的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2005

属性快速总结

在过去的几章中,我们使用和讨论了相当多不同的属性。我认为快速总结和进一步调查一些更常见的属性是值得的。

使用 dp 进行调整

众所周知,有成千上万种不同的安卓设备。为了尝试在不同设备上使用一种通用的测量系统,安卓使用密度无关像素,或者dp,作为测量单位。其工作原理是首先计算应用程序运行设备上的像素密度。

重要提示

我们可以通过将屏幕的水平分辨率除以屏幕的水平尺寸(以英寸为单位)来计算密度。这一切都是在我们的应用程序运行的设备上实时完成的。

我们只需在设置小部件的各种属性的大小时,使用dp和一个数字。使用密度无关的测量单位,我们可以设计布局,使其在尽可能多的不同屏幕上呈现统一的外观。

那么,这是否意味着问题解决了?我们只需在所有地方使用dp,我们的布局就能在任何地方工作?不幸的是,密度无关只是解决方案的一部分。在本书的其余部分中,我们将看到如何使我们的应用程序在各种不同的屏幕上呈现出色。

例如,我们可以通过向其属性添加以下代码来影响小部件的高度和宽度:

...
android:height="50dp"
android:width="150dp"
...

或者,我们可以使用属性窗口,并通过适当的编辑框的舒适性添加它们。

我们还可以使用相同的dp单位来设置其他属性,例如边距和填充。我们将在一分钟内更仔细地研究边距和填充。

使用 sp 调整字体大小

另一种用于调整安卓字体大小的设备相关测量单位是sp,该测量单位用于字体,并且与dp的像素密度相关方式完全相同。

安卓设备在决定您的字体大小时将使用额外的计算,这取决于您使用的sp值,即用户自己的字体大小设置。因此,如果您在具有正常字体大小的设备和模拟器上测试应用程序,那么视力受损的用户(或者只是喜欢大字体的用户)并且将字体设置为大号的用户将看到与您在测试期间看到的内容不同。

如果您想尝试调整安卓设备的字体大小设置,可以在模拟器或真实设备上选择设置 | 显示 | 高级 | 字体大小,然后尝试调整下面截图中突出显示的滑块:

图 5.1 – 使用 sp 调整字体大小

图 5.1 – 使用 sp 调整字体大小

我们可以在任何具有文本的小部件中使用sp来设置字体大小。这包括ButtonTextView和所有textSize属性下的 UI 元素,如下所示:

android:textSize="50sp"

和往常一样,我们也可以使用属性窗口来实现相同的效果。

使用 wrap 或 match 确定大小

我们还可以决定 UI 元素的大小,以及许多其他 UI 元素,相对于包含/父元素的行为。我们可以通过将layoutWidthlayoutHeight属性设置为wrap_contentmatch_parent来实现。

例如,假设我们将布局上孤立的按钮的属性设置为以下内容:

...
android:layout_width="match_parent"
android:layout_height="match_parent"
....

然后,按钮将在高度和宽度上都扩展以匹配****父级。我们可以看到下一个截图中的按钮填满了整个屏幕:

重要提示

我在项目中的上一章中添加了一个新布局上的按钮。

图 5.2 - 按钮在高度和宽度上扩展以匹配父级

图 5.2 - 按钮在高度和宽度上扩展以匹配父级

更常见的是按钮的wrap_content,如下所示:

....
android:layout_width="wrap_content"
android:layout_height="wrap_content"
....

这会导致按钮的大小与它需要的一样大(以dp为单位,文本以sp为单位)。

使用填充和边距

如果你曾经做过任何网页设计,那么你一定对接下来的两个属性非常熟悉。填充是从小部件的边缘到小部件内容的开始的空间。边距是留在小部件外部的空间,留在其他小部件之间 - 包括其他小部件的边距,如果它们有的话。这里有一个可视化表示:

图 5.3 - 使用填充和边距

图 5.3 - 使用填充和边距

我们可以简单地为所有边设置填充和边距,就像这样:

...
android:layout_margin="43dp"
android:padding="10dp"
...

看一下边距和填充的命名约定略有不同。填充只是称为padding,但边距被称为layout_margin。这反映了填充只影响 UI 元素本身,但边距可以影响布局中的其他小部件。

或者,我们可以指定不同的顶部、底部、左侧和右侧边距和填充,就像这样:

android:layout_marginTop="43dp"
android:layout_marginBottom="43dp"
android:paddingLeft="5dp"
android:paddingRight="5dp"

为小部件指定边距和填充值是可选的,如果没有指定任何值,将假定为 0。我们还可以选择指定一些不同边的边距和填充,但不指定其他边,就像前面的例子一样。

很明显,我们设计布局的方式非常灵活,但要精确地实现这么多选项,需要一些练习。我们甚至可以指定负边距值来创建重叠的小部件。

让我们看看一些其他属性,然后我们将继续玩一个时尚布局 - CardView

使用layout_weight属性

重量是相对于其他 UI 元素的相对量。因此,为了使layout_weight有用,我们需要在两个或更多元素上为layout_weight属性分配一个值。

然后,我们可以分配总共加起来为 100%的部分。这对于在 UI 的各个部分之间划分屏幕空间特别有用,我们希望它们占用的相对空间在屏幕大小不同的情况下保持不变。

使用layout_weightspdp单位结合使用可以创建一个简单而灵活的布局。例如,看看这段代码:

<Button
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight=".1"
        android:text="one tenth" />
<Button
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight=".2"
        android:text="two tenths" />
<Button
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight=".3"
        android:text="three tenths" />
<Button
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight=".4"
        android:text="four tenths" />

这段代码将做什么:

图 5.4 - 使用 layout_weight 属性的 UI

图 5.4 - 使用 layout_weight 属性的 UI

请注意,所有的layout_height属性都设置为0dp。实际上,layout_weight正在替换layout_height属性。我们使用layout_weight的上下文很重要,否则它不起作用。还要注意,我们不必使用 1 的分数;我们可以使用整数、百分比或任何其他数字;只要它们相对于彼此,它们可能会实现你想要的效果。请注意,layout_weight只在某些上下文中起作用。

使用重力

重力可以成为我们的朋友,并且可以在布局中以许多方式使用。就像太阳系中的重力一样,它通过将物品朝着给定方向移动来影响物品的位置,就好像它们受到重力的影响一样。了解重力的最佳方法是查看一些示例代码和图片,看看重力能做什么。

假设我们将按钮(或另一个小部件)的gravity属性设置为left|center_vertical,就像这样:

android:gravity="left|center_vertical"

这将产生以下效果:

图 5.5 - 在小部件上设置重力属性

图 5.5 - 在小部件上设置重力属性

请注意,小部件的内容(在本例中为按钮的文本)确实左对齐并垂直居中。

此外,小部件可以使用layout_gravity元素影响其在布局元素中的位置,就像这样:

android:layout_gravity="left"

这将如预期地设置小部件在其布局中,就像这样:

图 5.6 - 将重力布局设置为左

图 5.6 - 将重力布局设置为左

先前的代码允许同一布局中的不同小部件受到影响,就好像布局具有多个不同的重力。

通过使用与小部件相同的代码,可以通过其父布局的gravity属性影响布局中所有小部件的内容:

android:gravity="left"

实际上,有许多属性超出了我们讨论的范围。我们在本书中不需要的很多属性,有些相当晦涩,你可能在整个 Android 职业生涯中都用不到它们。但其他一些是相当常用的,例如backgroundtextColoralignmenttypefacevisibilityshadowColor。让我们现在探索一些更多的属性和布局。

使用 CardView 和 ScrollView 构建 UI

以通常的方式创建一个新项目,并选择CardView Layout

我们将在ScrollView布局中设计我们的CardView杰作,正如其名称所示,它允许用户滚动布局的内容。

展开项目资源管理器窗口中的文件夹,以便您可以看到res文件夹。展开res文件夹以查看layout文件夹。

右键单击layout文件夹,然后选择New。请注意,有一个Layout resource file选项。选择Layout resource file,然后您将看到New Resource File对话框窗口。

main_layout中。名称是任意的,但这个布局将是我们的主要布局,所以名称表明了这一点。

请注意它被设置为ScrollView。这种布局类型似乎与LinearLayout的工作方式完全相同;不同之处在于,当屏幕上有太多内容要显示时,它将允许用户通过用手指滑动来滚动内容。

在名为main_layout的 XML 文件中单击ScrollView,并将其放置在layout文件夹中,以便我们构建基于CardView的 UI。

Android Studio 将打开 UI 设计器,准备就绪。

使用 Java 代码设置视图

与以前一样,我们现在将通过在MainActivity.java文件中调用setContentView方法来加载main_layout.xml文件作为我们应用的布局。

选择app/java/your_package_name,其中your_package_name等于创建项目时自动生成的包名称。

修改onCreate方法中的代码,使其与下一个代码完全相同。我已经突出显示了您需要添加的行:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);

   setContentView(R.layout.main_layout);
}

现在您可以运行应用程序,但除了一个空的ScrollView之外,什么也看不到。

添加图像资源

我们将需要一些图片来完成这个项目。这样我们就可以演示如何将它们添加到项目中(本节)并在CardView小部件中整齐地显示和格式化它们(下一节)。

你从哪里获取图片并不重要;这个练习的目的是实际的动手经验。为了避免版权和版税问题,我将使用一些来自 Packt Publishing 网站的书籍图片。这也使我能够为您提供完成项目所需的所有资源,并且应该减轻您获取自己的图片的麻烦。请随意在第五章/res/drawable文件夹中更换图片。

有三张图片:image_1.pngimage_2.pngimage_3.png。要将它们添加到项目中,请按照以下步骤进行:

  1. 使用您操作系统的文件浏览器找到图片文件。

  2. 将它们全部高亮显示,然后按Ctrl + C进行复制。

  3. 在 Android Studio 项目资源管理器中,通过左键单击选择res/drawable文件夹。

  4. 右键单击drawable文件夹,然后选择粘贴

  5. 在弹出窗口中询问drawable文件夹。

  6. 再次单击确定复制指定文件

现在你应该能够在drawable文件夹中看到你的图片,以及 Android Studio 在项目创建时放置在那里的一些其他文件,如下一个截图所示:

图 5.7 - 扩展 drawable 文件夹

图 5.7 - 扩展 drawable 文件夹

在我们继续讨论CardView小部件本身之前,让我们设计一下我们将放在卡片中的内容。

创建卡片的内容

我们需要做的下一件事是创建卡片的内容。将内容与布局分开是有意义的。我们将创建三个单独的布局,称为card_contents_1card_contents_2card_contents_3。它们将分别包含一个LinearLayout,其中将包含一张图片和一些文本。

让我们创建另外三个带有LinearLayout布局的布局:

  1. 右键单击layout文件夹,然后选择新建布局资源文件

  2. 将文件命名为card_contents_1,并将**…ConstraintLayout更改为LinearLayout**作为根元素。

  3. 点击layout文件夹。

  4. 重复步骤 13两次,每次将文件名更改为card_contents_2,然后是card_contents_3

现在,选择spdpgravity属性使它们看起来漂亮:

  1. 将一个TextView拖到布局的顶部。

  2. TextView下方的布局中拖动一个ImageView

  3. 资源弹出窗口中,选择项目 | image_1,然后单击确定

  4. 在图片下面拖动另外两个TextView

现在你的布局应该是这样的:

图 5.8 - 通过添加 sp、dp 和 gravity 属性使布局看起来更漂亮

图 5.8 - 通过添加 sp、dp 和 gravity 属性使布局看起来更漂亮

现在让我们使用一些 Material Design 指南使布局看起来更吸引人:

重要提示

当您进行这些修改时,底部布局中的 UI 元素可能会消失在设计视图的底部。如果这种情况发生在您身上,请记住您可以随时从组件树窗口下面的调色板中选择任何 UI 元素。

  1. 为顶部的TextView小部件设置textSize属性为24sp

  2. 在顶部的TextView小部件上继续工作,将16dp

  3. text属性设置为Learning Java by Building Android Games(或者适合你的图片的标题)。

  4. ImageView小部件上,将layout_widthlayout_height属性设置为wrap_content

  5. 在继续使用ImageView小部件时,将layout_gravity属性设置为center_horizontal

  6. ImageView小部件下方的TextView上,将textSize设置为16sp

  7. 在相同的TextView小部件上,设置16dp

  8. 在相同的TextView小部件上,将text属性设置为Learn Java and Android from scratch by building 6 playable games(或者描述你的图片的内容)。

  9. 在底部的TextView小部件上,将text属性更改为BUY NOW

  10. 在相同的TextView小部件上,将16dp设置为。

  11. 在相同的TextView小部件上,将textSize属性设置为24sp

  12. 在相同的TextView小部件上,将textColor属性设置为@color/ teal_200

  13. 在包含所有其他元素的LinearLayout布局上,设置为15dp。请注意,从组件树窗口中选择LinearLayout最容易。

此时,您的布局将看起来与以下截图非常相似:

图 5.9-使用一些 Material Design 指南增强布局的吸引力

图 5.9-使用一些 Material Design 指南增强布局的吸引力

现在使用完全相同的尺寸和颜色布局其他两个文件(card_contents_2card_contents_3)。当您获得image_2image_3时,也要相应地更改前两个TextView元素上的所有text属性,以使标题和描述是唯一的。标题和描述并不重要;我们要学习的是布局和外观。

注意

请注意,所有尺寸和颜色都来自 Material Design 网站:material.io/design/introduction和 Android 特定的 UI 指南:developer.android.com/guide/topics/ui/look-and-feel。在完成本书后,这些都值得学习。

现在我们可以继续进行CardView小部件。

为 CardView 定义尺寸

右键单击dimens(缩写为尺寸)并单击dimens.xml。我们将使用这个文件通过引用它们来创建一些CardView将使用的常见值。

为了实现这一点,我们将直接在dimens.xml文件中编辑 XML,使其与以下代码相同:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="card_corner_radius">16dp</dimen>
    <dimen name="card_margin">10dp</dimen>
</resources>

一定要确保它与原来完全相同,因为一个小的遗漏可能会导致错误并阻止项目工作。

我们定义了两个资源,第一个称为card_corner_radius,值为16dp,第二个称为card_margin,值为10dp

我们将从main_layout文件中引用这些资源,并使用它们来一致地配置我们的三个CardView元素。

将 CardView 添加到我们的布局

切换到将滚动我们应用程序内容的ScrollView,有点像 Web 浏览器滚动不适合一个屏幕的网页内容。

ScrollView有一个限制-它只能有一个直接子布局。我们希望它包含三个CardView小部件。

为了解决这个问题,从调色板的布局类别中拖动一个LinearLayout布局。一定要选择LinearLayout (vertical),如调色板中的图标所示:

图 5.10-LinearLayout (vertical)图标

图 5.10-LinearLayout (vertical)图标

我们将把三个CardView放在LinearLayout中,然后整个内容将平稳滚动,没有任何错误。

CardView小部件可以在CardView中找到。

CardView拖放到设计中的LinearLayout上,您可能会收到 Android Studio 中的弹出消息,也可能不会。这是消息:

图 5.11-要求添加 CardView 的弹出窗口

图 5.11-要求添加 CardView 的弹出窗口

如果您收到此消息,请单击CardView功能以将其添加到否则不会具有这些功能的较旧版本的 Android。

现在您的设计中应该有一个CardView。在其中有内容之前,CardView只能在组件树窗口中轻松可见。

通过组件树选择CardView并配置以下属性:

  1. layout_width设置为wrap_content

  2. layout_gravity设置为center

  3. 设置@dimen/card_margin以使用我们在dimens.xml文件中定义的边距值。

  4. cardCornerRadius属性设置为@dimen/card_corner_radius,以使用我们在dimens.xml文件中定义的半径值。

  5. cardElevation设置为2dp

现在切换到代码选项卡,您会发现您有以下代码:

<androidx.cardview.widget.CardView
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:layout_gravity="center"
   android:layout_margin="@dimen/card_margin"
   app:cardCornerRadius="@dimen/card_corner_radius"
   app:cardElevation="2dp" />

前面的代码列表仅显示了CardView的代码。

目前的问题是我们的CardView是空的。让我们通过添加card_contents_1.xml的内容来解决这个问题。以下是如何做到这一点。

在另一个布局文件中包含布局文件

我们需要稍微编辑代码,原因如下。我们需要向代码添加一个include元素。include元素是将从card_contents_1.xml布局中插入内容的代码。问题在于,要添加这段代码,我们需要稍微改变CardView XML 的格式。当前的格式以一个单一标签开始和结束CardView,如下所示:

<androidx.cardview.widget.CardView
…
…/>

我们需要将格式更改为像这样的单独的开放和关闭标签(暂时不要更改任何内容):

<androidx.cardview.widget.CardView
…
…
</androidx.cardview.widget.CardView>

这种格式的改变将使我们能够添加include…代码,我们的第一个CardView小部件将完成。考虑到这一点,编辑CardView的代码,确保与以下代码完全相同。我已经突出显示了两行新代码,但请注意,cardElevation属性后面的斜杠也已经被移除:

<androidx.cardview.widget.CardView
   android:layout_width="wrap_content"
   android:layout_height="match_parent"
   android:layout_gravity="center"
   android:layout_margin="@dimen/card_margin"
   app:cardCornerRadius="@dimen/card_corner_radius"
   app:cardElevation="2dp" >
             <include layout="@layout/card_contents_1" />
</androidx.cardview.widget.CardView>

现在您可以在可视化设计器中查看main_layout文件,并查看CardView元素内部的布局。然而,可视化设计器并不能展示CardView的真正美感。不过很快我们将会在完成的应用程序中看到所有CardView小部件一起滚动得很好。以下是模拟器中屏幕的截图,显示了我们目前的进展:

图 5.12 - CardView 元素内部的布局

图 5.12 - CardView 元素内部的布局

在布局中添加另外两个CardView小部件,并将它们配置与第一个相同,只有一个例外。在第二个CardView上,将cardElevation设置为22dp,在第三个CardView上,将cardElevation设置为42dp。将include代码更改为分别引用card_contents_2card_contents_3

提示

您可以通过复制和粘贴CardView XML 并简单地修改高程和include代码来快速完成这一步,就像前面段落中提到的那样。

当完成后,LinearLayout代码中的CardView相关代码如下所示:

<androidx.cardview.widget.CardView
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     android:layout_gravity="center"
     android:layout_margin="@dimen/card_margin"
     app:cardCornerRadius="@dimen/card_corner_radius"
     app:cardElevation="2dp" >
     <include layout="@layout/card_contents_1" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     android:layout_gravity="center"
     android:layout_margin="@dimen/card_margin"
     app:cardCornerRadius="@dimen/card_corner_radius"
     app:cardElevation="22dp" >
     <include layout="@layout/card_contents_2" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
     android:layout_width="wrap_content"
     android:layout_height="match_parent"
     android:layout_gravity="center"
     android:layout_margin="@dimen/card_margin"
     app:cardCornerRadius="@dimen/card_corner_radius"
     app:cardElevation="42dp" >
     <include layout="@layout/card_contents_3" />
</androidx.cardview.widget.CardView>

现在我们可以运行应用程序,看到我们三个美丽的、凸起的CardView小部件在其中的效果。在下一个截图中,我已经捕捉到了它,这样您就可以看到提升设置对创建令人愉悦的深度和阴影效果的影响:

图 5.13 - 令人愉悦的阴影效果

图 5.13 - 令人愉悦的深度和阴影效果

重要提示

黑白打印版本的截图可能会稍微不清晰。请务必自行构建和运行应用程序,以查看这个酷炫的效果。

让我们在平板电脑模拟器上探索我们的最新应用程序。

创建平板电脑模拟器

我们经常希望在多个不同的设备上测试我们的应用程序。幸运的是,Android Studio 可以轻松地创建任意数量的不同模拟器。按照以下步骤创建平板电脑模拟器。

选择工具 | AVD 管理器,然后单击创建虚拟设备...按钮。您将看到下图所示的选择硬件窗口:

图 5.14 - 选择硬件窗口

图 5.14 - 选择硬件窗口

类别列表中选择平板电脑选项,然后从可用平板电脑中选择Pixel C平板电脑。

提示

如果您是在将来的某个时候阅读本文,Pixel C 选项可能已经更新。选择平板电脑的重要性不如练习创建平板电脑模拟器并测试我们的应用程序重要。

点击下一步按钮。在随后的系统镜像窗口上,只需点击下一步,因为这将选择默认系统镜像。选择自己的镜像可能会导致模拟器无法正常工作。

最后,在Android 虚拟设备屏幕上,您可以保留所有默认选项。如果需要,可以更改您的模拟器的AVD 名称选项或启动方向(纵向或横向)选项。当您准备好时,点击完成按钮。

如果您正在运行手机模拟器,请将其关闭。现在,每当您从 Android Studio 运行您的应用程序时,您将有选择 Pixel C(或您创建的任何平板电脑)的选项。这是我 Pixel C 模拟器运行CardView应用程序的屏幕截图:

图 5.15 - Pixel C 模拟器运行 CardView 应用程序

图 5.15 - Pixel C 模拟器运行 CardView 应用程序

不算太糟,但浪费了相当多的空间,看起来有点奇怪。让我们尝试在横向模式下。如果您尝试在平板电脑横向模式下运行应用程序,结果会更糟。我们可以从中学到的是,我们将不得不为不同尺寸的屏幕和不同方向设计我们的布局。有时它们将是智能设计,可以根据不同的尺寸或方向进行调整,但通常它们将是完全不同的设计,存在于不同的布局文件中。

常见问题

  1. 我需要精通关于 Material Design 的所有这些东西吗?

不,除非您想成为专业设计师。如果您只是想制作自己的应用程序并在 Play 商店上出售或赠送它们,那么只知道基础知识就足够了。

总结

在本章中,我们构建了外观美观的CardView布局,并将它们放在ScrollView中,这样用户就可以通过滑动浏览布局的内容,有点像浏览网页。为了完成本章,我们启动了一个平板模拟器,并看到如果我们想要适应不同的设备尺寸和方向,我们需要在设计布局方面变得聪明起来。在第二十四章**,设计模式、多个布局和片段中,我们将开始将我们的布局提升到下一个水平,并学习如何使用片段来处理如此多样的设备。

然而,在我们开始之前,学习更多关于 Java 以及如何使用它来控制我们的 UI 和与用户交互将对我们有所帮助。这将是接下来七章的重点。

当然,目前的悬而未决的问题是,尽管我们学到了很多关于布局、项目结构、Java 和 XML 之间的连接以及其他许多知识,但是我们的 UI,无论多么漂亮,实际上并没有做任何事情!我们需要认真提升我们的 Java 技能,同时学习如何在 Android 环境中应用它们。在下一章中,我们将做到这一点。我们将看到如何添加 Java 代码,以便在我们需要的时候精确执行,通过与Android Activity 生命周期一起工作。

第六章:Android 生命周期

在本章中,我们将熟悉 Android 应用程序的生命周期。起初,这可能听起来有点奇怪,一个计算机程序有一个生命周期,但很快就会有意义。

生命周期是所有 Android 应用程序与 Android 操作系统交互的方式。就像人类的生命周期与周围世界互动一样,我们别无选择,必须与之互动,并且必须准备好处理不经通知的不同事件,如果我们希望我们的应用程序能够生存下来。

我们将看到应用程序从创建到销毁经历的生命周期阶段,以及这如何帮助我们知道根据我们想要实现的目标在何处放置我们的 Java 代码。

简而言之,在本章中我们将看到以下内容:

  • Android 应用程序的生活和时代

  • 什么是方法重写

  • Android 生命周期的阶段

  • 我们确切需要了解和做什么来编写我们的应用程序

  • 一个生命周期演示迷你应用程序。

  • 快速查看代码结构,准备在下一章中进行 Java 编码

让我们开始学习 Android 的生命周期。

技术要求

您可以在 GitHub 上找到本章中的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2006

Android 应用程序的生活和时代

我们已经谈到了我们代码的结构;我们知道我们可以编写类,并且在这些类中我们有方法,方法包含我们的代码,从而完成任务。我们也知道当我们想要方法中的代码运行时,我们通过使用方法的名称来调用该方法。

此外,在第二章**,初次接触:Java,XML 和 UI 设计师中,我们了解到 Android 本身在应用程序准备启动之前调用onCreate方法。当我们输出到 logcat 并使用Toast类向用户发送弹出消息时,我们看到了这一点。

在本章中,我们将看到我们编写的每个应用程序在其生命周期内发生的情况 - 启动和结束以及中间的一些阶段。我们将看到 Android 在每次运行时都与我们的应用程序进行交互。

Android 如何与我们的应用程序交互

它通过调用包含在Activity类中的方法来实现。即使该方法在我们的 Java 代码中不可见,Android 也会在适当的时间调用它。如果这看起来毫无意义,那么请继续阅读。

你是否曾想过为什么onCreate方法之前有一行奇怪的代码?

@Override

这里发生的是,我们在告诉 Android,当你调用onCreate时,请使用我们重写的版本,因为我们在那个时候有一些事情要做。

此外,您可能还记得onCreate方法中奇怪的第一行代码:

super.onCreate(savedInstanceState)

这是在告诉 Android 在继续使用我们重写的版本之前调用onCreate的原始/官方版本。这不仅仅是 Android 的一个怪癖 - 方法 重写内置在 Java 中。

还有许多其他方法,我们可以选择重写,它们允许我们在 Android 应用程序的生命周期中的适当时间添加我们的代码。就像onCreate在应用程序显示给用户之前被调用一样,还有更多在其他时间被调用的方法。我们还没有看到它们,我们还没有重写它们,但它们存在,它们被调用,它们的代码执行。

我们需要关心 Android 在何时调用我们应用程序的方法,因为它们控制着我们代码的生死。例如,如果我们的应用程序允许用户输入重要的提醒。然后,在输入提醒的一半时,他们的手机响了,我们的应用程序消失了,数据(提醒)也消失了。

我们需要学会何时、为什么以及 Android 将调用我们应用程序生命周期的哪些方法,这是非常重要的,幸运的是也相当简单。然后我们就知道在哪里需要重写方法来添加我们自己的代码,以及在哪里添加定义我们应用程序的真正功能(代码)。

让我们先来研究一下 Android 的生命周期,然后我们可以继续深入了解 Java 的方方面面,我们就会知道在哪里放我们编写的代码。

Android 生命周期的简化解释

如果你曾经使用过 Android 设备,你可能已经注意到它的工作方式与许多其他操作系统有很大不同。例如,你可能正在使用一个应用程序——比如你正在查看 Facebook 上的人在做什么。然后,你收到一封电子邮件通知,你点击通知来阅读它。在阅读电子邮件的过程中,你可能会收到 Twitter 的通知,因为你正在等待某个关注者的重要消息,所以你中断了阅读电子邮件,触摸屏幕切换到 Twitter。

阅读完推文后,你想玩愤怒的小鸟,但在第一次投掷的过程中,你突然想起了 Facebook 的帖子。所以,你退出愤怒的小鸟,点击 Facebook 图标。

然后你恢复 Facebook,可能在你离开的地方。你可以恢复阅读电子邮件,决定回复推文,或者开始一个全新的应用程序。

所有这些来回都需要操作系统进行相当多的管理,独立于应用程序本身。

在我们刚刚讨论的情境中,例如 Windows PC 和 Android 之间的区别是:在 Android 中,尽管用户决定使用哪个应用程序,但操作系统决定何时关闭(销毁)应用程序以及我们用户的数据(比如假设的笔记)也会一并销毁。我们在编写应用程序时需要考虑到这一点。仅仅因为我们可能编写代码来处理用户的输入,这并不意味着 Android 会让代码执行。

生命周期阶段的解密

Android 系统有多个不同的阶段,任何给定的应用程序都可能处于其中之一。根据阶段,Android 系统决定应用程序如何被用户查看,或者是否被用户查看。

Android 有这些阶段,这样它可以决定哪个应用程序正在使用,并且可以给予正确数量的资源,如内存和处理能力。

此外,当用户与设备进行交互时,例如触摸屏幕,Android 必须将该交互的细节传递给正确的应用程序。例如,在愤怒的小鸟中,拖动并释放意味着射击,但在消息应用中,它可能意味着删除短信。

我们已经提出了一个问题,当用户退出我们的应用程序来接听电话时,他们会丢失他们的进度/数据/重要笔记吗?

Android 有一个系统,简化一下以便解释,意味着 Android 设备上的每个应用程序都处于以下阶段之一:

  • 创建中

  • 启动

  • 恢复

  • 运行

  • 暂停

  • 停止

  • 被销毁

阶段列表希望看起来是合乎逻辑的。例如,用户按下 Facebook 应用程序图标,应用程序被创建。然后它被启动。到目前为止一切都很简单,但接下来是恢复

如果我们能暂时接受应用程序在启动后恢复,那么这并不像它一开始看起来那么不合逻辑,随着我们的继续,一切都会变得清晰起来。

恢复后,应用程序是运行的。这时,Facebook 应用程序控制着屏幕和大部分系统内存和处理能力,并接收用户输入的细节。

现在,我们的例子是什么,当我们从 Facebook 应用切换到电子邮件应用时呢?

当我们点击去阅读我们的电子邮件时,Facebook 应用程序将进入暂停阶段,然后是停止阶段,而电子邮件应用程序将进入创建中阶段,然后是恢复,然后是运行

如果我们决定重新访问 Facebook,就像之前的情况一样,Facebook 应用程序可能会直接跳过创建并直接恢复,然后再次运行,很可能会恰好停留在我们离开的地方。

请注意,随时,Android 都可以决定停止然后销毁一个应用程序。在这种情况下,当我们再次运行应用程序时,它将需要在第一个阶段重新创建

因此,如果 Facebook 应用程序长时间不活动,或者 Angry Birds 需要很多系统资源,以至于 Android 已经销毁了 Facebook 应用程序,那么我们之前阅读的确切帖子的体验可能会有所不同。

如果所有这些阶段的东西开始变得令人困惑,那么您会很高兴地知道提到它的唯一原因是以下:

  • 您知道它存在

  • 我们偶尔需要与之交互

  • 当我们这样做时,我们将一步一步地进行

现在我们了解了生命周期阶段,让我们学习如何处理它们。

我们如何处理生命周期阶段

当我们编写应用程序时,我们如何与这种复杂性进行交互?好消息是,当我们创建第一个项目时自动生成的 Android 代码大部分都为我们完成了。

正如我们所讨论的,我们只看不到处理此交互的方法,但是我们有机会覆盖它们并在需要时向该阶段添加我们自己的代码。

这意味着我们可以继续学习 Java 并制作 Android 应用程序,直到我们遇到偶尔需要在某个阶段执行某些操作的情况。

重要说明

如果我们的应用程序有多个活动,它们将各自拥有自己的生命周期。这并不复杂,总体上对我们来说会更容易。

接下来是 Android 提供的方法的简要解释,以方便我们管理生命周期阶段。为了澄清我们对生命周期方法的讨论,它们将列在我们正在讨论的相应阶段旁边。但是,正如您将看到的,方法名称本身清楚地说明了它们在哪个阶段适用。

还有一个简短的解释或建议,说明我们何时可以使用特定的方法,并在特定阶段进行交互。随着我们在书中的进展,我们将遇到大部分这些方法。当然,我们已经看到了onCreate

  • onCreate:当活动正在setContentView(设置内容视图),图形和声音时,将执行此方法。

  • onStart:当应用程序处于启动阶段时执行此方法。

  • onResume:此方法在onStart之后运行,但也可以在我们的活动在先前暂停后恢复时(最合理地)进入。我们可能会重新加载先前保存的用户数据(例如重要笔记),这些数据是在应用程序被中断时保存的,例如电话呼叫或用户运行其他应用程序。

  • onPause:当我们的应用程序是onResume时发生。当另一个 UI 元素显示在当前活动的顶部(例如弹出对话框)或活动即将停止时(例如,用户导航到不同的活动)时,活动总是转换到暂停状态。

  • onStop:这与onCreate有关,例如释放系统资源或将信息写入数据库。如果我们到达这里,我们很可能很快就会被销毁。

  • onDestroy:这是当我们的活动最终被销毁时。从这个阶段开始就没有回头路了。这是我们有序拆除我们的应用程序的最后机会。如果我们到达这里,下次我们将从头开始经历生命周期阶段。

此图显示了方法之间可能的执行流程:

图 6.1 - 执行流程

图 6.1 - 执行流程

所有的方法描述及其相关阶段应该都很简单。唯一真正的问题是运行阶段是什么?当我们在其他方法/阶段中编写代码时,我们将看到onCreateonStartonResume方法将准备应用程序,然后持续形成运行阶段。然后onPauseonStoponDestroy方法将随后发生。

现在我们可以通过一个迷你应用程序来观察这些生命周期方法的作用。我们将通过重写它们并为每个方法添加一个Log消息和一个Toast消息来做到这一点。这将直观地证明我们的应用程序经过了哪些阶段。

生命周期演示应用程序

在这一部分,我们将进行一个快速实验,帮助我们熟悉应用程序使用的生命周期方法,并让我们有机会玩一些更多的 Java 代码。

按照以下步骤开始一个新项目,然后我们可以添加一些代码:

  1. 开始一个新项目。

  2. 选择基本活动模板。

  3. 将项目命名为生命周期演示。当然,如果您希望参考或复制粘贴,代码在第六章文件夹中的下载包中。

  4. 等待 Android Studio 生成项目文件,然后在代码编辑器中打开MainActivity.java文件。

您已经使用所有默认设置创建了一个新项目。对于这个演示,我们只需要MainActivity.java文件,不需要构建 UI。

编写生命周期演示应用程序

MainActivity.java文件中,找到onCreate方法,并在闭合大括号}之前添加以下两行代码,标志着onCreate方法的结束:

Toast.makeText(this, "In onCreate", Toast.LENGTH_SHORT).show();
Log.i("info", "In onCreate");

整个onCreate方法现在应该看起来像下面的代码,其中高亮显示的代码是我们刚刚添加的两行,是我们跳过一些自动生成的代码行,以使书更易读。有关完整的代码清单,请检查下载包中的MainActivity.java文件。以下是代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);    
             Toast.makeText(this, "In onCreate", 
             Toast.LENGTH_SHORT).show();
       Log.i("info", "In onCreate");
}

提示

请记住,您需要使用Alt + Enter键盘组合两次来导入ToastLog所需的类。

onCreate方法的闭合大括号}之后,留出一行空白,并添加以下五个生命周期方法及其包含的代码。还要注意,我们添加重写方法的顺序并不重要。Android 将根据我们输入的顺序正确调用它们:

@Override
public void onStart() {
   // First call the "official" version of this method
   super.onStart();
   Toast.makeText(this, "In onStart", 
         Toast.LENGTH_SHORT).show();
   Log.i("info", "In onStart");
}
@Override
public void onResume() {
   // First call the "official" version of this method
   super.onResume();
   Toast.makeText(this, "In onResume",
         Toast.LENGTH_SHORT).show();
   Log.i("info", "In onResume");
}
@Override
public void onPause() {
   // First call the "official" version of this method
   super.onPause();
   Toast.makeText(this, "In onPause", 
         Toast.LENGTH_SHORT).show();
   Log.i("info", "In onPause");
}
@Override
public void onStop() {
   // First call the "official" version of this method
   super.onStop();
   Toast.makeText(this, "In onStop", 
         Toast.LENGTH_SHORT).show();
   Log.i("info", "In onStop");
}
@Override
public void onDestroy() {
   // First call the "official" version of this method
   super.onDestroy();
   Toast.makeText(this, "In onDestroy", 
         Toast.LENGTH_SHORT).show();
   Log.i("info", "In onDestroy");
}

首先,让我们谈谈代码本身。请注意,方法名称都对应于我们在本章前面讨论过的生命周期方法和阶段。请注意,所有方法声明都在@Override代码行之前。还要看到每个方法内部的第一行代码是super.on...

这里到底发生了什么是以下内容:

  • Android 在我们已经讨论过的各个时间调用我们的方法。

  • @Override关键字表示这些方法替换/重写了作为 Android API 的一部分提供的方法的原始版本。请注意,我们看不到这些重写的方法,但它们是存在的,如果我们没有重写它们,Android 将调用这些原始版本而不是我们的版本。

在重写方法的每个方法内部的第一行代码super.on...,然后调用这些原始版本。因此,我们不仅仅是重写这些原始方法以添加我们自己的代码 - 我们还调用它们,它们的代码也被执行。

提示

对于好奇的人,关键字super是用于超类。随着我们的进展,我们将在几章中探讨方法重写和超类。

最后,您添加的代码将使每个方法输出一个Toast消息和一个Log消息。但是,输出的消息是不同的,可以通过双引号""之间的文本看出。输出的消息将清楚地表明是哪个方法产生了它们。

运行生命周期演示应用程序

现在我们已经查看了代码,我们可以玩一下我们的应用程序,并从发生的事情中了解生命周期:

  1. 在设备或模拟器上运行应用程序。

  2. 观察模拟器屏幕,您将看到以下Toast消息依次出现在屏幕上:在 onCreate在 onStart在 onResume

  3. 注意日志窗口中的以下消息。如果有太多的消息,请记住您可以通过将日志级别下拉菜单设置为信息来过滤它们。

             info:in onCreate
             info:in onStart
             info:in onResume
  1. 现在点击模拟器或设备上的返回按钮。注意您会按照以下顺序收到三条Toast消息:在 onPause在 onStop在 onDestroy。验证我们在 logcat 窗口中有匹配的输出。

  2. 接下来,运行另一个应用程序 - 也许是第一章**,开始 Android 和 Java中的Hello Android应用程序(但任何应用程序都可以)- 通过点击模拟器/设备屏幕上的图标。

  3. 现在尝试以下操作:在模拟器上打开任务管理器。

  4. 如果您不确定,可以参考第三章**,探索 Android Studio 和项目结构以及在模拟器上使用模拟器作为真实设备部分。

  5. 现在您应该在设备上看到所有最近运行的应用程序。

  6. 点击Lifecycle Demo应用程序,注意到通常的三个启动消息被显示出来。这是因为我们的应用程序先前被销毁了。

  7. 然而,现在再次点击任务管理器按钮,并切换到Hello Android应用程序。注意这次只显示了在 onPause在 onStop消息。验证我们在 logcat 中有匹配的输出。应用程序没有被销毁。

  8. 现在,再次使用onCreate不需要重新运行应用程序。这是预期的,因为应用程序先前并没有被销毁,只是停止了。

接下来,让我们谈谈我们运行应用程序时看到的情况。

检查生命周期演示应用程序的输出

当我们第一次启动生命周期演示应用程序时,我们看到onCreateonStartonResume方法被调用。然后,当我们使用onPauseonStoponDestroy方法关闭应用程序时被调用。

此外,我们从我们的代码中知道,所有这些方法的原始版本也被调用了,因为我们在每个重写的方法中都使用了super.on...代码,这是我们做的第一件事。

我们应用程序行为的怪癖出现在我们使用任务管理器在应用程序之间切换时 - 当从生命周期演示切换时,它没有被销毁,因此,当切换回来时,不需要运行onCreate

我的 Toast 在哪里?

开头的三个和结尾的三个Toast消息被排队了,并且这些方法在它们被显示的时候已经完成了。您可以通过再次运行实验来验证这一点,会发现所有三个启动/关闭日志消息在第二个Toast消息甚至显示之前就已经输出了。然而,Toast消息确实加强了我们对顺序的了解,尽管不是时机。

当您按照上述步骤进行时,可能会得到略有不同的结果,这是完全可能的(但不太可能)。我们确定的是,当我们的应用程序在成千上万不同的设备上由数百万不同的用户运行时,这些用户对与设备交互的偏好也不同,Android 会在我们无法轻易预测的时候调用生命周期方法。

例如,当用户按下主页按钮退出应用程序时会发生什么?当我们连续打开两个应用程序,然后使用返回按钮切换到先前的应用程序时,会销毁还是只是停止应用程序?当用户在任务管理器中有十几个应用程序,操作系统需要销毁一些先前只是停止的应用程序时,我们的应用程序会成为“受害者”之一吗?

当然,你可以在模拟器上测试所有前面的场景。但结果只对你测试的那一次有效。不能保证每次都会显示相同的行为,当然也不会在每个不同的 Android 设备上显示相同的行为。

最后一些好消息!解决所有这些复杂性的方法是遵循一些简单的规则:

  • onCreate方法中设置你的应用程序准备运行。

  • onResume方法中加载用户的数据。

  • onPause方法中保存用户的数据。

  • onDestroy方法中整理你的应用程序,使其成为一个良好的 Android 公民。

  • 在整本书中要注意一些情况,我们可能会想使用onStartonStop

如果我们按照上面提到的做(我们将在本书的过程中看到如何做),我们可以不再担心所有这些生命周期的东西,让 Android 来处理它!我们还可以重写一些其他方法。所以,让我们来看看它们。

一些其他重写的方法

你可能已经注意到,在使用基本活动模板的所有项目的代码中,还有另外两个自动生成的方法。它们是onCreateOptionsMenuonOptionsItemSelected。大多数 Android 应用程序都有一个弹出菜单,所以 Android Studio 默认生成一个;包括使其工作的基本代码。

你可以在项目资源管理器中的res/menu/menu_main.xml中看到描述菜单的 XML。XML 代码的关键行是这样的:

<item
      android:id="@+id/action_settings"
      android:orderInCategory="100"
      android:title="@string/action_settings"
      app:showAsAction="never" />

这描述了一个带有文本设置的菜单项目。如果你运行使用我们迄今为止创建的基本活动模板构建的任何应用程序,你将会看到下面显示的按钮:

图 6.2 - 设置按钮

图 6.2 - 设置按钮

如果你点击按钮,你将会看到它的动作如下所示:

图 6.3 - 设置选项

图 6.3 - 设置选项

那么,onCreateOptionsMenuonOptionsItemSelected方法是如何产生这些结果的呢?

onCreateOptionsMenu方法使用以下代码从menu_main.xml文件加载菜单:

getMenuInflater().inflate(R.menu.menu_main, menu);

它是由onCreate方法的默认版本调用的,这就是为什么我们没有看到它发生。

提示

我们将在第十七章**, 数据持久性和共享中使用弹出菜单,在我们的应用程序的不同屏幕之间切换。

onOptionsItemSelected方法在用户点击菜单按钮时被调用。这个方法处理当项目被选中时会发生什么。现在什么都不会发生 - 它只是返回 true

随意在这些方法中添加ToastLog消息,以测试我刚刚描述的顺序和时间。我只是觉得现在是一个好时机来快速介绍这两个方法,因为它们一直潜伏在我们的代码中,没有介绍,我不想让它们感到被忽视。

现在我们已经了解了 Android 生命周期的工作方式,并介绍了一大堆可以重写以与生命周期交互的方法,我们最好学习一下 Java 的基础知识,这样我们就可以编写一些代码放入这些方法以及我们自己的方法了。

Java 代码的结构 - 重新审视

我们已经看到,每次创建一个新的 Android 项目时,我们也会创建一个新的 Java作为我们编写的代码的一种容器。

我们还学习了并尝试了LogToast。我们还使用了AppCompatActivity类,但使用方式与LogToast不同。你可能还记得迄今为止我们所有项目中的第一行代码,在import语句之后使用了extends关键字:

public class MainActivity extends AppCompatActivity {

当我们扩展一个类而不仅仅是导入它时,我们就把它变成了我们自己的。事实上,如果你再看一下代码行,你会发现我们正在创建一个新的类,名为MainActivity,但是基于 Android API 中的AppCompatActivity类。

重要提示

AppCompatActivity类是Activity类的稍微修改版本。它为较旧版本的 Android 提供了额外的功能,否则这些功能将不存在。关于Activity的所有讨论同样适用于AppCompatActivity。随着我们的进展,我们将看到Activity类中的一些变化。有可能你的AppCompatActivity已经被其他类替代,这取决于自此书写以来发生的变化。Android Studio 的更新有时会更改创建新项目时使用的默认Activity类。如果名称以...Activity结尾,那没关系,因为我们讨论过的和将要讨论的一切同样适用。我将简单地将这个类称为Activity

总之:

  • 我们可以导入类来使用它们。

  • 我们可以扩展类来使用它们。

  • 我们最终会创建自己的类。

这里的关键点是:

类以各种形式是 Java 代码的基础。Java 中的一切都是类或是类的一部分。

我们自己的类和其他人编写的类是我们代码的构建块,类中的方法包装了功能代码 - 执行工作的代码。

我们可以在扩展的类中编写方法;就像我们在第二章**,初次接触:Java,XML 和 UI 设计师中所做的topClickbottomClick一样。此外,我们还重写了其他人已经编写的类中的方法,比如onCreateonPause等等。

然而,我们在这些方法中放入的唯一代码是使用ToastLog进行了一些调用。我们不会仅凭这些编写下一个杀手级应用程序。但现在我们可以迈出更多的步伐。

引入片段和生命周期

你可能还记得从第二章**,初次接触:Java,XML 和 UI 设计师中,Basic Activity 模板中的 Java 代码不仅包含在MainActivity.java文件中。还有FirstFragment.javaSecondFragment.java文件。我们了解到,这些文件包含了控制用户在 Basic Activity 模板应用的两个屏幕之间导航时发生的事情的代码。这两个文件中的代码结构与MainActivity.java文件中的代码不同。这里快速看一下FirstFragment.java

public class FirstFragment extends Fragment {
    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState
    ) 
     {
        ...
    }

     ...
}

我从这个文件中省略了大部分代码,因为它对于这个介绍性讨论来说是不必要的。一个Fragment可以,而且在这个应用程序中确实代表着应用程序的一个屏幕。这个应用程序和其他包含Fragment类的应用程序的Fragment类由Activity类控制。我们将在第二十四章**,设计模式,多个布局和片段中仔细研究Fragment类。这里需要注意的是Fragment类有一个onCreateView方法。

当我们的应用程序使用一个或多个Fragment类的实例时,它们也将成为 Android 生命周期的一部分,Fragment类有自己的一组生命周期方法,其中onCreateView是其中之一。

操作系统的生命周期、Activity类和Fragment类之间的交互将在第二十四章**,设计模式,多个布局和片段中得到解释。现在只需要知道它们是相互关联的。

总结

我们已经了解了 Android 生命周期以及操作系统在特定时间调用设置方法。

我们还看到不仅我们可以调用我们的代码。操作系统也可以调用我们重写的方法中包含的代码。通过在各种重写的生命周期方法中添加适当的代码,我们可以确保在正确的时间执行正确的代码。

现在我们需要做的是学习如何编写更多的 Java 代码。在下一章中,我们将开始专注于 Java,因为我们已经在 Android 上有了很好的基础,所以我们将毫无问题地练习和运用我们所学到的一切。

第七章:Java 变量、运算符和表达式

在本章和下一章中,我们将学习和实践 Java 数据的核心基础知识以及如何操作这些数据。在本章中,我们将专注于创建和理解数据本身,在下一章中,我们将看到如何操作和响应它。

我们还将快速回顾一下我们在前几章学到的关于 Java 的知识,然后深入学习如何编写我们自己的 Java 代码。我们即将学习的原则不仅适用于 Java,还适用于其他编程语言。

通过本章结束时,你将能够舒适地编写 Java 代码,在 Android 中创建和使用数据。本章将带你了解以下主题:

  • 理解 Java 语法和行话

  • 使用变量存储和使用数据

  • 使用变量

  • 使用运算符更改变量中的值

  • 尝试表达式

让我们学习一些 Java。

技术要求

你可以在 GitHub 上找到本章的代码文件,网址为github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2007

Java 无处不在

我们即将学习的核心 Java 基础知识适用于我们从中继承的类(比如AppCompatActivity),以及我们自己编写的类(正如我们将在第十章面向对象编程中开始做的那样)。

在我们编写自己的类之前学习基础知识更合乎逻辑,我们将使用扩展的ActivityAppCompatActivity,在一个迷你项目中添加一些 Java 代码。我们将再次使用LogToast类,以在Activity类的重写onCreate方法中看到我们编码的结果,以触发我们代码的执行。

然而,当我们转到第十章面向对象编程并开始编写我们自己的类,以及更多地了解他人编写的类是如何工作的,我们在这里学到的一切也将适用于那个时候——事实上,你在本章和下一章学到的所有 Java 知识,如果你将它从Activity类中剥离出来,粘贴到另一个 Java 环境中,比如以下环境:

  • 任何主要的桌面操作系统

  • 许多现代电视

  • 卫星导航

  • 智能冰箱

Java 也可以在那里运行!

呼唤所有的 Java 大师

如果你已经做过一些 Java 编程并理解以下关键字(ifelsewhiledo whileswitchfor),你可能可以直接跳到第十章面向对象编程。或者,你可能想浏览一下这些信息作为复习。

让我们继续学习如何在 Java 中编码。

理解 Java 语法和行话

在整本书中,我们将使用简单的英语来讨论一些技术问题。你永远不会被要求阅读一个以非技术语言解释的 Java 或 Android 概念的技术解释。

到目前为止,在一些场合我已经要求你接受一个简化的解释,以便在更合适的时候提供更充分的解释,就像我在类和方法中所做的那样。

话虽如此,Java 和 Android 社区充满了讲技术术语的人,要加入并从这些社区中学习,你需要理解他们使用的术语。因此,本书的方法是使用完全通俗的语言学习一个概念或欣赏一个想法,但同时作为学习的一部分介绍术语/技术术语。

Java 语法是我们将 Java 语言元素组合在一起,以在 Dalvik 虚拟机(VM)中产生可工作的代码的方式。Java 语法是我们使用的单词和将这些单词组成类似句子的结构的组合,构成我们的代码。

这些 Java“单词”数量众多,但分成小块学习肯定比任何人类语言更容易。我们称这些单词为关键字

我相信如果您能阅读,那么您就能学会 Java,因为学习 Java 要容易得多。那么,是什么区分了完成初级 Java 课程的人和专业程序员呢?

这正是区分语言学生和大师诗人的相同之处。精通 Java 并不在于我们知道如何使用多少个 Java 关键字,而在于我们如何使用它们。语言的精通来自于实践、进一步学习,并更加熟练地使用关键字。许多人认为编程与科学一样是一门艺术,这也有一定道理。

更多代码注释

随着您在编写 Java 程序方面变得更加高级,您用来创建程序的解决方案将变得更长、更复杂。此外,正如我们将在后面的章节中看到的,Java 被设计为通过让我们将代码分成单独的类,往往跨越多个文件来管理复杂性。

代码注释是 Java 程序中没有任何功能的部分。编译器会忽略这些注释。它们用于帮助程序员记录、解释和澄清他们的代码,使其对自己以后或其他可能需要使用或更改代码的程序员更易理解。

我们已经看到了单行注释。这里重复一下:

// this is a comment explaining what is going on

前面的注释以两个//斜杠字符开头。注释在行末结束。因此,该行上的任何内容仅供人类阅读,而下一行上的任何内容(除非是另一个注释)都需要是语法正确的 Java 代码,如下所示:

// I can write anything I like here
but this line will cause an error

我们可以使用多个单行注释,如下所示:

// Below is an important note
// I am an important note
// We can have as many single line comments like this as we like

单行注释也很有用,如果我们想暂时禁用一行代码。我们可以在代码前面加上//,这样它就不会包含在程序中。记住这行代码,告诉 Android 加载我们的布局?

// setContentView(R.layout.activity_main);

在前面的情况下,当运行时布局将不会被加载,应用程序将显示空白屏幕,因为编译器会忽略整行代码。

重要提示

我们在第五章**, 使用 CardView 和 ScrollView 创建美观的布局中看到了这一点,当我们暂时注释掉了其中一个方法。

Java 中还有另一种类型的注释,称为多行注释。多行注释适用于跨越多行的较长注释,以及在代码文件顶部添加版权信息等内容。与单行注释一样,多行注释可以用于临时禁用代码,通常跨越多行。

/**/之间的所有内容都会被编译器忽略。以下是一些示例:

/*
   You can tell I am good at this because my
   code has so many helpful comments in it.
*/

多行注释中的行数没有限制;使用哪种类型的注释将取决于情况。在本书中,我将始终在文本中明确解释每行代码,但您通常会在代码本身中发现大量的注释,这些注释会进一步解释、洞察或提供上下文。因此,彻底阅读所有代码也是一个好主意。

/*
   The winning lottery numbers for next Saturday are
   9,7,12,34,29,22
   But you still want to make Android apps?
*/

提示

所有优秀的 Java 程序员都会在他们的代码中大量使用注释!

使用变量存储和使用数据

我们可以想象一个variableA。这些名称就像是我们程序员对用户 Android 设备内存的窗口。

变量是内存中的值,通过使用它们的名称可以随时使用或更改。

计算机内存具有高度复杂的寻址系统,幸运的是我们不需要与之交互。Java 变量允许我们为程序需要处理的所有数据制定自己方便的名称。Dalvik VMDVM)将处理与操作系统的交互的所有技术细节,操作系统将再与物理内存交互。

因此,我们可以将我们的 Android 设备的内存想象成一个巨大的仓库,只等着我们添加我们的变量。当我们为变量分配名称时,它们被存储在仓库中,准备在我们需要它们时使用。当我们使用变量的名称时,设备知道我们指的是什么。然后我们可以告诉它做这样的事情:

  • variableA分配一个值

  • variableA添加到variableB

  • 测试variableB的值,并根据结果采取行动

  • ……等等,我们很快就会看到

在一个典型的应用程序中,我们可能会有一个名为unreadMessages的变量,用来保存用户未读消息的数量。当有新消息到达时,我们可以增加它,当用户阅读消息时,我们可以减少它,并在应用程序布局的某个地方向用户显示它,以便他们知道有多少未读消息。

以下是可能出现的一些情况:

  • 用户收到三条新消息,因此将3添加到unreadMessages的值。

  • 用户登录应用程序,因此使用Toast显示一条消息以及存储在unreadMessages中的值。

  • 用户看到一堆消息来自他们不喜欢的人,并删除了六条消息。然后我们可以从unreadMessages中减去 6。

这些是变量名称的任意示例,如果您没有使用 Java 限制的任何字符或关键字,实际上可以随意命名变量。

然而,在实践中,最好采用命名约定,以使您的变量名保持一致。在本书中,我们将使用一种松散的变量命名约定,以小写字母开头。当变量名中有多个单词时,第二个单词将以大写字母开头。这称为驼峰命名法

以下是一些示例:

  • unreadMessages

  • contactName

  • isFriend

在查看一些带有变量的真实 Java 代码之前,我们需要首先看一下我们可以创建和使用的变量的类型。

变量的类型

可以想象,即使是一个简单的应用程序也会有相当多的变量。在上一节中,我们介绍了unreadMessages变量作为一个假设的例子。如果一个应用程序有一个联系人列表,并需要记住每个联系人的名字,那么我们可能需要为每个联系人创建变量。

当应用程序需要知道联系人是否也是朋友还是普通联系人时怎么办?我们可能需要编写代码来测试朋友状态,然后将该联系人的消息添加到适当的文件夹中,以便用户知道这些消息是来自朋友还是其他人。

计算机程序中另一个常见的要求,包括 Android 应用程序,是真或假的错误。

为了涵盖您可能想要存储或操作的各种数据类型,Java 有类型

基本类型

有许多类型的变量,我们甚至可以发明自己的类型。但是现在,我们将看一下最常用的内置 Java 类型,公平地说,它们几乎涵盖了我们可能会遇到的每种情况。提供一些示例是解释类型的最佳方式。

我们已经讨论了假设的unreadMessages变量。这个变量当然是一个数字,因此我们必须告诉 Java 编译器这一点,给它一个适当的类型。

另一方面,假设的contactName变量当然将保存组成联系人姓名的字符。

保存常规数字的类型称为int类型,比如unreadMessages,如果我们尝试将其他类型的数据存储在这样的变量中,我们肯定会遇到麻烦,正如我们从以下截图中可以看到的:

图 7.1 – 存储联系人姓名

正如我们所看到的,Java 被设计成不可能让这些错误进入运行中的程序。

以下是 Java 中的主要变量类型:

  • 整数整数类型用于存储整数,即整数。这种类型使用 32 位()内存,因此可以存储略大于 20 亿的值,包括负值。

  • 长整型:正如名称所暗示的,当需要更大的数字时,可以使用长整型数据类型。长整型类型使用 64 位内存,我们可以在其中存储 2 的 63 次方。如果您想看看它是什么样子,这就是它:9,223,372,036,854,775,807。也许令人惊讶的是,长整型变量有用之处,但关键是,如果较小的变量可以胜任,我们应该使用它,因为我们的程序将使用更少的内存。

重要提示

您可能想知道何时会使用这些大小的数字。明显的例子可能是进行复杂计算的数学或科学应用,但另一个用途可能是用于计时。当您计算某事花费的时间时,Java Date类使用自 1970 年 1 月 1 日以来的毫秒数。毫秒是一秒的千分之一,所以自 1970 年以来已经有相当多的毫秒了。

  • 浮点数:这是用于浮点数的数据类型——也就是说,小数点后有精度的数字。由于数字的小数部分占用的内存空间与整数部分一样多,因此与非浮点数相比,float类型中数字的范围会减少。因此,除非我们的变量需要额外的精度,否则float不会是我们的数据类型选择。

  • 双精度:当float类型的精度不够时,我们有double

  • 布尔:我们将在整本书中使用大量布尔值。布尔变量类型可以是truefalse;没有其他值。布尔值回答以下问题:

联系人是朋友吗?

有新消息吗?

布尔值的两个例子足够吗?

  • 字符:在字符类型中存储单个字母数字字符。它本身不会改变世界,但如果我们将它们放在一起,它可能会有用。

  • short:这种类型类似于int的节省空间的版本。它可以用来存储具有正负值的整数,并且可以对其进行数学运算。它与int的区别在于它只使用 16 位内存,这只是与int相比的内存量的一半。short的缺点是它只能存储与int相比一半范围的值,从-32768 到 32767。

  • 字节:这种类型类似于short的更节省空间的版本。它可以用来存储具有正负值的整数,并且可以对其进行数学运算。它与intshort的区别在于它只使用 8 位内存,这只是与byte相比的内存量的一半,与int相比只有四分之一的内存。byte的缺点是它只能存储与int相比一半范围的值,从-32768 到 32767。总共节省 8 或 16 位的内存是不太可能有影响的;但是,如果您需要在程序中存储数百万个整数,那么shortbyte是值得考虑的。

重要提示

我将这个关于数据类型的讨论保持在一个实用的水平上,这对本书的内容是有用的。如果您对数据类型的值是如何存储以及为什么限制是什么感兴趣,那么请查看Oracle Java 教程网站 docs.oracle.com/javase/tuto…

正如我们刚刚学到的,我们可能想要存储的每种数据类型都需要特定数量的内存。因此,我们必须在开始使用变量之前让 Java 编译器知道变量的类型。

先前描述的变量称为原始类型。大多数原始类型在不同的编程语言中都被使用(以及关键字),因此,如果您对类型和关键字有很好的理解,那么跳到另一种语言将比第一次容易得多!这些类型使用预定义的内存量,因此,使用我们的仓库存储类比,适合预定义大小的存储盒。

正如“原始”标签所示,它们不像引用类型那样复杂。

引用类型

您可能已经注意到,我们没有涵盖我们之前用来介绍保存字母数字数据的变量类型String

字符串

字符串是特殊类型变量的一个例子,称为引用类型。它们简单地指向内存中存储变量的位置,但引用类型本身并不定义特定的内存量。其原因很简单:因为我们并不总是知道在程序运行之前需要存储多少数据。

我们可以将字符串和其他引用类型看作是不断扩展和收缩的存储盒。那么,这些String引用类型中的一个不会最终碰到另一个变量吗?

当我们将设备的内存视为一个充满标记存储盒的巨大仓库时,您可以将 DVM 视为一个超级高效的叉车司机,将不同类型的存储盒放在最合适的位置;如果有必要,DVM 将在几分之一秒内迅速移动物品,以避免碰撞。此外,如果需要,Dalvik,这个叉车司机,甚至会立即蒸发掉任何不需要的存储盒。

所有这些都发生在不断卸载各种类型的新存储盒并将它们放在最适合该类型变量的地方的同时。Dalvik 将引用变量保存在仓库的不同部分,我们将在第十二章堆栈、堆和垃圾收集器中了解更多细节。

字符串可以用来存储任何键盘字符,就像char类型,但长度几乎可以是任意的。从联系人的姓名到整本书都可以存储在单个String类型中。我们将经常使用字符串,包括在本章中。

还有一些其他引用类型我们也会探讨。

数组

数组是一种存储大量相同类型变量的方法,以便快速有效地访问。我们将在第十五章数组、映射和随机数中研究数组。

将数组想象成我们仓库中的一排通道,所有特定类型的变量都按照精确的顺序排列。数组是引用类型,因此 Dalvik 将它们保存在与字符串相同的仓库部分。例如,我们可以使用数组来存储数十个联系人。

另一种引用类型是class类型,我们已经讨论过但没有正确解释。我们将在第十章面向对象编程中熟悉类。

现在,我们知道我们可能想要存储的每种数据类型都需要一定的内存。因此,在我们开始使用变量之前,我们必须让 Java 编译器知道变量的类型。我们用变量声明来做到这一点。

使用变量

这就够理论了。让我们看看我们如何使用我们的变量和类型。请记住,每种原始类型都需要特定数量的真实设备内存。这就是为什么编译器需要知道变量的类型的原因之一。

变量声明

我们必须首先使用名称unreadMessages声明int,我们将输入以下内容:

int unreadMessages;

就是这样——简单地声明类型(在本例中是int),然后留出一个空格,输入你想要用于此变量的名称。还要注意,行末的分号;将告诉编译器我们已经完成了这一行,接下来的内容(如果有的话)不是变量声明的一部分。

同样地,对于几乎所有其他变量类型,声明方式都是相同的。以下是一些示例。示例中的变量名是任意的。这就像在仓库中预留一个带标签的储物箱。

看一下以下代码片段:

long millisecondsElapsed;
float accountBalance;
boolean isFriend;
char contactFirstInitial;
String messageText;

请注意,我说的是几乎所有其他变量类型。其中一个例外是class类型的变量。我们已经看到了一些声明class类型变量的代码。您还记得第三章中的这个代码片段吗?在MainActivity.java文件中,探索 Android Studio 和项目结构

FloatingActionButton fab…

这段编辑过的代码片段声明了一个名为fabFloatingActionButton类型的变量。但我们有点跑题,将在第十章 面向对象编程中回到类。

变量初始化

初始化是下一步。在这里,对于每种类型,我们将一个值初始化到变量中。这就像在仓库的储物箱中放入一个值。

unreadMessages = 10;
millisecondsElapsed = 1438165116841l;// 29th July 2016 11:19am
accountBalance = 129.52f;
isFriend = true;
contactFirstInitial = 'C';
messageText = "Hi reader, I just thought I would let you know that Charles Babbage was an early computing pioneer and he invented the difference engine. If you want to know more about him, you can click find look here: www.charlesbabbage.net";

请注意,char变量在初始化值周围使用'单引号,而String类型使用"双引号。

我们也可以将声明和初始化步骤合并。在这里,我们声明并初始化了与之前相同的变量,但是在一步中完成:

int unreadMessages = 10;
long millisecondsElapsed = 1438165116841l;//29th July 2016 11:19am
float accountBalance = 129.52f;
boolean isFriend = true;
char contactFirstInitial = 'C';
String messageText = " Hi reader, I just thought I would let you know that Charles Babbage was an early computing pioneer and he invented the difference engine. If you want to know more about him, you can click this link www.charlesbabbage.net";

无论我们是分开声明和初始化,还是一起进行,都取决于具体情况。重要的是我们必须在某个时候都要做这两件事。

int a;
// That's me declared and ready to go!
// The line below attempts to output a to the console
Log.i("info", "int a = " + a);
// Oh no I forgot to initialize a!!

这将导致以下情况:

编译器错误:变量 a 可能尚未初始化

这个规则有一个重要的例外。在某些情况下,变量可以有默认值。我们将在第十章 面向对象编程中看到这一点;但是,声明和初始化变量是一个好习惯。

使用运算符更改变量中的值

当然,在几乎任何程序中,我们都需要对这些变量的值进行“操作”。我们使用运算符来操作(更改)变量。以下是一些最常见的 Java 运算符列表,它们允许我们操作变量。您不需要记住它们,因为我们将在第一次使用它们时逐行查看每行代码。我们已经在初始化变量时看到了第一个运算符,但是我们将再次看到它,这次会更加有趣。

赋值运算符

这是赋值运算符:=

它使运算符左侧的变量与右侧的值相同——例如,就像这行代码中的那样:

unreadMessages = newMessages;

在执行了前一行代码之后,存储在unreadMessages中的值将与newMessages中的值相同。

加法运算符

这是加法运算符:+

它将两侧的值相加,通常与赋值运算符一起使用。例如,它可以将两个具有数值的变量相加,就像下一行代码中的那样:

 unreadMessages = newMessages + unreadMessages; 

一旦前面的代码执行了,newMessagesunreadMessages持有的值的组合值现在存储在unreadMessages中。作为同样事情的另一个例子,请看这行代码:

accountBalance = yesterdaysBalance + todaysDeposits; 

重要提示

请注意,同时在运算符的两侧同时使用同一个变量是完全可以接受的。

减法运算符

这是减法运算符:-

它将从左侧的值中减去右侧的值。通常与赋值运算符一起使用,就像这个代码示例中:

unreadMessages = unreadMessages - 1; 

或者,作为一个类似的例子,它在这行代码中使用:

accountBalance = accountBalance - withdrawals;

在上一行代码执行后,accountBalance将保留其原始值减去withdrawals中的值。

除法运算符

这是除法运算符:/

它将把左边的数字除以右边的数字。同样,它通常与赋值运算符一起使用。以下是一个示例代码行:

fairShare = numSweets / numChildren;

如果在上一行代码中numSweets持有九个糖果,numChildren持有三个糖果,那么fairShare现在将持有三个糖果的价值。

乘法运算符

这是乘法运算符:*

它将变量和数字相乘,与许多其他运算符一样,通常与赋值运算符一起使用。例如,看看下一行代码:

answer = 10 * 10; 

或者,看看这行代码:

biggerAnswer = 10 * 10 * 10;

在前两行代码执行后,answer保持值 100,biggerAnswer保持值 1000。

递增运算符

这是递增运算符:++

递增运算符是从某物中加一的快速方法。例如,看看下一行代码,它使用了加法运算符:

myVariable = myVariable + 1; 

上一行代码的结果与这里更紧凑的代码相同:

   myVariable ++; 

递减运算符

这是递减运算符:--

递减运算符(正如你可能猜到的)是从某物中减去一个的快速方法。例如,看看下一行代码,它使用了减法运算符:

myVariable = myVariable -1; 

上一行代码与myVariable --;相同。

重要提示

这些运算符的正式名称与之前解释的略有不同,例如,除法运算符是乘法运算符之一。但是之前给出的名称对于学习 Java 来说更有用,如果你在与 Java 社区的某人交谈时使用术语除法运算符,他们会完全明白你的意思。

Java 中甚至有更多的运算符。在下一章中,当我们学习在 Java 中做出决定时,我们将遇到其中一些。

重要提示

如果你对运算符感到好奇,在Oracle Java 教程网站上有一个完整的运算符列表,网址为docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html。本书中完成项目所需的所有运算符都将在本书中得到充分解释。链接是为我们中更好奇的人提供的。

尝试表达

让我们尝试使用一些声明、赋值和运算符。当我们将这些元素捆绑成一些有意义的语法时,我们称之为ToastLog以检查我们的结果。

表达自己的演示应用程序

创建一个名为Expressing Yourself的新项目,使用下载包的/Expressing Yourself文件夹。

切换到onCreate方法,就在}闭合大括号之前,添加这段代码:

int numMessages;

在上一行代码的下方,我们将初始化一个值为numMessages

接下来,添加这行代码:

numMessages = 10;

在上一行代码之后,onCreate}闭合大括号之前,添加以下代码:

// Output the value of numMessages
Log.i("numMessages = ", "" + numMessages);
numMessages++;
numMessages = numMessages + 1;
Log.i("numMessages = ", "" + numMessages);
// Now a boolean (just true or false)
boolean isFriend = true;
Log.i("isFriend = ", "" + isFriend);
// A contact and an important message
String contact = "James Gosling";
String message = "Dear reader, I invented Java.";
// Now let's play with those String variables
Toast.makeText(this, "Message from " + contact, Toast.LENGTH_SHORT).show();
Toast.makeText(this, "Message is: " + message, Toast.LENGTH_SHORT).show();

重要提示

你需要导入ToastLog类,就像我们之前做的那样。

运行应用程序,我们可以检查输出,然后再检查代码。在 logcat 窗口中,你会看到以下输出:

numMessages =: 10
numMessages =: 12
isFriend =: true

在屏幕上,你会看到两个弹出的Toast消息。第一个说**来自詹姆斯·高斯林的消息。第二个说消息是:亲爱的读者,我发明了 Java。**这在下面的截图中显示:

图 7.2 - 第二个弹出的 Toast 消息

图 7.2 - 第二个弹出的 Toast 消息

让我们逐行检查代码,确保每行都清晰明了,然后再继续。

首先,我们声明并初始化了一个名为numMessagesint类型变量。我们本可以在一行上完成,但我们是这样做的:

int numMessages;
numMessages = 10;

接下来,我们使用Log输出一条消息。这次,我们不是简单地在""双引号之间输入消息,而是使用+运算符将numMessages添加到输出中,正如我们在控制台中看到的,numMessages的实际值被输出,如下所示:

// Output the value of numMessages
Log.i("numMessages = ", "" + numMessages);

为了进一步证明我们的numMessages变量像它应该的那样多才多艺,我们使用了++运算符,这应该将它的值增加1,然后使用+ 1numMessages加到自身上。然后我们输出了numMessages的新值,并确实发现它的值从 10 增加到 12,如下面的代码片段所示:

numMessages ++;
numMessages = numMessages + 1;
Log.i("numMessages = ", "" + numMessages);

接下来,我们创建了一个名为isFriendboolean类型变量,并将其输出到控制台。我们从输出中看到true被显示。当我们在下一节中看到决策制定时,这种变量类型将充分证明其有用性。代码如下所示:

// Now a boolean (just true or false)
boolean isFriend = true;
Log.i("isFriend = ", "" + isFriend);

在此之后,我们声明并初始化了两个String类型的变量,如下所示:

// A contact and an important message
String contact = "James Gosling";
String message = "Dear reader, I invented Java.";

最后,我们使用Toast输出String变量。我们使用了"Message from "消息的硬编码部分,并使用+ contact添加了消息的变量部分。我们也使用了相同的技术来形成第二个Toast消息。

提示

当我们将两个字符串连接在一起以形成一个更长的String类型时,这被称为连接

// Now let's play with those String variables
Toast.makeText(this, "Message from " + contact, Toast.LENGTH_SHORT).show();
Toast.makeText(this, "Message is:" + message, Toast.LENGTH_SHORT).show();

现在,我们可以声明变量,将它们初始化为一个值,稍微改变它们的值,并使用ToastLog输出它们。

总结

最后,我们使用了一些严肃的 Java。我们学习了关于变量、声明和初始化。我们看到了如何使用运算符来改变变量的值。如果你不记得所有的东西,没关系,因为我们将在整本书中不断地使用这些技术和关键字。

在下一章中,让我们看看如何根据这些变量的值做出决定,以及这对我们有多有用。

第八章:Java 决策和循环

我们刚刚学习了关于变量,我们知道如何使用表达式更改它们所持有的值,但是我们如何根据变量的值采取行动呢?

我们当然可以将新消息的数量添加到先前未读消息的数量中,但是例如,当用户已读完所有消息时,我们该如何触发应用程序内的操作呢?

第一个问题是我们需要一种方法来测试变量的值,然后在值落在一系列值范围内或是特定值时做出响应。

编程的一个常见问题是,我们需要根据变量的值执行代码的某些部分一定次数(不止一次或有时根本不执行),这取决于变量的值。

为了解决第一个问题,我们将学习如何使用ifelseswitch在 Java 中做出决定。为了解决后者,我们将学习如何使用whiledo whileforbreak在 Java 中进行循环。

在本章中,我们将涵盖以下内容:

  • 使用ifelseelse ifswitch做出决策

  • switch演示应用程序

  • Java 的while循环和do while循环

  • Java 的for循环

  • 循环演示应用程序

让我们学习更多的 Java 知识。

技术要求

您可以在 GitHub 上找到本章中的代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition/tree/main/chapter%2008

在 Java 中做出决策

我们的 Java 代码将不断做出决定。例如,我们可能需要知道用户是否有新消息,或者是否有一定数量的朋友。我们需要能够测试我们的变量,以查看它们是否满足某些条件,然后根据它们是否满足条件来执行一定的代码部分。

在本节中,随着我们的代码变得更加复杂,有助于以更易读的方式呈现它。让我们看看代码缩进,以使我们对决策的讨论更加容易。

为了清晰起见缩进代码

您可能已经注意到我们项目中的 Java 代码是缩进的。例如,在MainActivity类内的第一行代码被缩进了一个制表符。此外,每个方法内的第一行代码也被缩进。这里有一个带注释的屏幕截图,以便更清楚地说明这一点,另外一个快速的例子:

图 8.1 – 缩进的 Java 代码

图 8.1 – 缩进的 Java 代码

还要注意,当缩进块结束时,通常是用一个闭合大括号}}的缩进程度与开始块的代码行相同。

我们这样做是为了使代码更易读。但是,这并不是 Java 语法的一部分,如果我们不这样做,代码仍将编译。

随着我们的代码变得更加复杂,缩进和注释有助于保持代码的含义和结构清晰。我现在提到这一点是因为当我们开始学习在 Java 中做出决定的语法时,缩进变得特别有用,建议您以相同的方式缩进代码。

大部分缩进是由 Android Studio 为我们完成的,但并非全部。

现在我们知道如何更清晰地呈现我们的代码,让我们学习一些更多的运算符,然后我们可以真正开始使用 Java 做出决定。

更多运算符

我们已经可以使用运算符进行加(+)、减(-)、乘(*)、除(/)、赋值(=)、增量(++)和减量(--)。让我们介绍一些更有用的运算符,然后我们将直接了解如何在 Java 中使用它们。

重要提示

不要担心记住每个后面的运算符。浏览它们和它们的解释,然后快速转到下一节。在那里,我们将使用一些运算符,当我们看到它们允许我们做一些例子时,它们将变得更清晰。它们在这里以列表的形式呈现,这样在不与后面的实现讨论混在一起时更方便参考。

我们使用运算符创建一个表达式,这个表达式要么为真,要么为假。我们用括号括起来,就像这样:(表达式在这里)

比较运算符

这是比较运算符,用于测试是否相等;它要么为真,要么为假:==

例如,表达式(10 == 9)是假的。10 显然不等于 9。然而,表达式(2 + 2 == 4)显然是真的。

注意

除了《1984》中 2 + 2 == 5 (en.wikipedia.org/wiki/Nineteen_Eighty-Four)。

逻辑 NOT 运算符

这是逻辑 NOT 运算符:!

它用于测试表达式的否定。否定意味着如果表达式为假,那么 NOT 运算符会使表达式为真。举个例子会有所帮助。

表达式(!(2 + 2 == 5))计算为真,因为 2 + 2 不是5。但进一步的例子(!(2 + 2 = 4))将是假的。这是因为 2 + 2 显然4。

不等运算符

这是不等运算符,是另一个比较运算符:!=

不等运算符测试是否不相等。例如,表达式(10 != 9)是真的。10 不等于 9。另一方面,(10 != 10)是假的,因为 10 显然等于 10。

大于运算符

另一个比较运算符(还有一些其他的)是大于运算符。它是这样的:>

这个运算符测试是否一个值大于另一个值。表达式(10 > 9)是真的,但表达式(9 > 10)是假的。

小于运算符

你可能猜到了,这个运算符测试值是否小于其他值。这就是这个运算符的样子:<

表达式(10 < 9)是假的,因为 10 不小于 9,而表达式(9 < 10)是真的。

大于或等于运算符

这个运算符测试一个值是否大于或等于另一个值,如果其中一个为真,结果就为真。这就是这个运算符的样子:>=

例如,表达式(10 >= 9)是真的,表达式(10 >= 10)也是真的,但表达式(10 >= 11)是假的,因为 10 既不大于也不等于 11。

小于或等于运算符

与前一个运算符类似,这个运算符测试两个条件,但这次是小于或等于。看看下面显示的运算符,然后我们将看一些例子:<=

表达式(10 <= 9)是假的,表达式(10 <= 10)是真的,表达式(10 <= 11)也是真的。

逻辑 AND 运算符

这个运算符被称为逻辑 AND。它测试表达式的两个或多个单独部分,所有部分必须为真,整个表达式才为真:&&

逻辑 AND 通常与其他运算符一起使用,构建更复杂的测试。表达式((10 > 9) && (10 < 11))是真的,因为两个部分都为真。另一方面,表达式((10 > 9) && (10 < 9))是假的,因为表达式的一个部分为真(10 > 9),另一个部分为假(10 < 9)

逻辑 OR 运算符

这个运算符称为逻辑 OR,它与逻辑 AND 类似,只是表达式的两个或多个部分中只有一个为真,表达式才为真:||

让我们看一下我们用于逻辑 AND 的最后一个例子,但将&&换成||。表达式((10 > 9) || (10 < 9))现在是真的,因为表达式的至少一个部分是真的。

模数运算符

这个运算符叫做模数(%)。它返回两个数字相除后的余数。例如,表达式(16 % 3 > 0)是真的,因为 16 除以 3 是 5 余 1,而 1 当然大于 0。

在本章和本书的其余部分中,以更实际的情境看到这些运算符将有助于澄清不同的用途。现在我们知道如何使用运算符,变量和值来形成表达式。接下来,我们可以看一种结构化和组合表达式的方法来做出一些深刻的决定。

如何使用所有这些运算符来测试变量

所有这些运算符在没有适当使用它们来影响真实变量和代码的真实决策的方法的情况下几乎是无用的。

现在我们有了所有需要的信息,我们可以看一个假设的情况,然后实际看到一些决策的代码。

使用 Java 的 if 关键字

正如我们所看到的,运算符单独使用几乎没有什么意义,但可能有用的是看到我们可以使用的广泛和多样的范围的一部分。现在,当我们开始使用最常见的运算符==时,我们可以开始看到运算符提供给我们的强大而精细的控制。

让我们把之前的例子变得不那么抽象。见识一下 Java 的if关键字。我们将使用if和一些条件运算符以及一个小故事来演示它们的用法。接下来是一个虚构的军事情况,希望它会比之前的例子更具体。

船长快要死了,知道他剩下的下属经验不是很丰富,他决定写一个 Java 程序,在他死后传达他的最后命令。部队必须守住桥的一侧,等待增援 - 但有一些规则来决定他们的行动。

船长想要确保他的部队理解的第一个命令是:

如果他们过桥,就射击他们。

那么,我们如何在 Java 中模拟这种情况呢?我们需要一个布尔变量,isComingOverBridge。下一段代码假设isComingOverBridge变量已经被声明并初始化为truefalse

然后我们可以这样使用if

if(isComingOverBridge){

   // Shoot them

}

如果isComingOverBridge布尔值为true,则在大括号内的代码将执行。如果isComingOverBridgefalse,程序将在if块之后继续执行,而不运行其中的代码。

否则,做这个

船长还想告诉他的部队,如果敌人没有过桥,他们应该留在原地等待。

现在我们介绍另一个 Java 关键字,else。当我们想要在if不为真时明确执行某些操作时,我们可以使用else

例如,要告诉部队如果敌人没有过桥就待在原地,我们可以写这段代码:

if(isComingOverBridge){

   // Shoot them
}else{

   // Hold position
}

船长随后意识到问题并不像他最初想的那么简单。如果敌人过桥,但部队太多怎么办?他的小队将被压垮和屠杀。

所以,他想出了这段代码(这次我们也会使用一些变量):

boolean isComingOverBridge;
int enemyTroops;
int friendlyTroops;
// Code that initializes the above variables one way or another
// Now the if
if(isComingOverBridge && friendlyTroops > enemyTroops){
   // shoot them
}else if(isComingOveBridge && friendlyTroops < enemyTroops) {
   // blow the bridge
}else{
   // Hold position
}

前面的代码有三条可能的执行路径。第一种情况是如果敌人正在过桥,友军人数更多:

if(isComingOverBridge && friendlyTroops > enemyTroops)

第二种情况是如果敌军正在过桥,但超过了友军的人数:

else if(isComingOveBridge && friendlyTroops < enemyTroops)

然后,如果其他两种情况都不成立,将执行第三种可能的结果,由最终的else捕获,没有if条件。

读者挑战

您能发现上述代码的一个缺陷吗?这可能会让一群经验不足的部队陷入彻底的混乱?敌军和友军的数量恰好相等的可能性没有得到明确处理,因此将由最终的else处理,这是指当没有敌军时。我猜任何自尊的船长都会期望他的部队在这种情况下进行战斗,他可以改变第一个if语句以适应这种可能性:

if(isComingOverBridge && friendlyTroops >= enemyTroops)

最后,船长最后关心的是,如果敌人挥舞着白旗过桥,然后被迅速屠杀,那么他的士兵最终会成为战争罪犯。所需的 Java 代码是显而易见的。使用wavingWhiteFlag布尔变量,他编写了这个测试:

if (wavingWhiteFlag){
   // Take prisoners
}

但是,放置这段代码的位置不太清楚。最后,船长选择了以下嵌套解决方案,并将wavingWhiteFlag的测试更改为逻辑非,就像这样:

if (!wavingWhiteFlag){
   // not surrendering so check everything else

   if(isComingOverTheBridge && friendlyTroops >= 
      enemyTroops){
          // shoot them
   }else if(isComingOverTheBridge && friendlyTroops < 
                enemyTroops) {
         // blow the bridge
   }
}else{

   // this is the else for our first if
   // Take prisoners
{
// Holding position

这表明我们可以嵌套ifelse语句,以创建相当深入和详细的决定。

我们可以继续使用ifelse做出更复杂的决定,但是我们已经看到的已经足够作为介绍了。

值得指出的是,很多时候解决问题有多种方法。正确的方法通常是以最清晰和最简单的方式解决问题的方法。

让我们看看在 Java 中做出决定的其他方法,然后我们可以将它们全部放在一个应用程序中。

切换以做出决定

我们已经看到了结合 Java 运算符与ifelse语句的广泛且几乎无限的可能性。但有时,在 Java 中做出决定可能有其他更好的方法。

当我们根据一系列清晰的可能性做出决定时,不涉及复杂的组合,通常使用switch是最好的方法。

我们开始一个switch决定就像这样:

switch(argument){
}

在上一个例子中,argument可以是一个表达式或一个变量。在花括号{}内,我们可以根据casebreak元素对参数做出决定:

case x:
   // code for case x
   break;
case y:
   // code for case y
   break;

您可以看到在上一个例子中,每个case都陈述了可能的结果,每个break都表示该案例的结束,以及不应再评估更多case语句的点。

遇到的第一个break会跳出switch块,继续执行整个switch块的结束花括号}后的下一行代码。

我们还可以使用没有值的default来运行一些代码,以防case语句都不为真,就像这样:

default:// Look no value
   // Do something here if no other case statements are 
      true
   break;

让我们编写一个快速演示应用程序,使用switch

切换演示应用程序

开始时,创建一个名为Switch Demo的新 Android 项目,使用上面编辑器中的MainActivity.java选项卡左键单击MainActivity.java文件,我们就可以开始编码了。

假设我们正在编写一个老式的文字冒险游戏,玩家在游戏中输入命令,比如“向东走”,“向西走”,“拿剑”等等。

在这种情况下,switch可以处理这种情况,例如这个示例代码,我们可以使用default来处理玩家输入的命令,这些命令没有特别处理。

}之前的onCreate方法中输入以下代码:

// get input from user in a String variable called command
String command = "go east";
switch(command){
   case "go east":
         Log.i("Player: ", "Moves to the East" );
         break;
   case "go west":
         Log.i("Player: ", "Moves to the West" );
         break;
   case "go north":
         Log.i("Player: ", "Moves to the North" );
         break;
   case "go south":
         Log.i("Player: ", "Moves to the South" );
         break;

   case "take sword":
         Log.i("Player: ", "Takes the silver sword" );
         break;
   // more possible cases
   default:
         Log.i("Message: ", "Sorry I don't speak Elfish" );
         break;
}

运行应用程序几次。每次,将command的初始化更改为新内容。请注意,当您将command初始化为case语句明确处理的内容时,我们会得到预期的输出。否则,我们会得到默认的抱歉,我不会说精灵语消息。

如果我们有很多要执行的case代码,我们可以将所有代码都包含在一个方法中-也许就像在下一段代码中一样,我已经突出显示了新的一行:

   case "go west":
                         goWest();
         break;

当然,我们随后需要编写新的goWest方法。然后,当command初始化为"go west"时,goWest方法将被执行,当goWest完成时,执行将返回到break语句,这将导致代码继续执行switch块后的内容。

当然,这段代码严重缺乏与 UI 的交互。我们已经看到了如何从按钮点击中调用方法,但即使这样也不足以使这段代码在真正的应用程序中有价值。我们将在第十二章《堆栈、堆和垃圾收集器》中看到我们如何解决这个问题。

我们还有的另一个问题是,代码执行后就结束了!我们需要它不断地询问玩家的指令,不仅仅是一次,而是一遍又一遍。我们将在下一节中解决这个问题。

使用循环重复代码

在这里,我们将学习如何通过查看几种类型的while循环、do while循环和for循环,以受控且精确的方式重复执行我们代码的部分。我们还将了解使用不同类型的循环的最合适的情况。

问循环与编程有什么关系是完全合理的。但它们确实如其名所示。它们是一种重复执行代码的方式——或者循环执行相同的代码部分,尽管每次可能有不同的结果。

这可能意味着重复执行相同的操作,直到被循环的代码(if、else 和 switch,循环是 Java 的控制流语句的一部分。

当条件为真时执行的代码称为do while循环,可以用这个简单的图表来说明:

图 8.2 - 当代码中达到循环时,条件被测试

图 8.2 - 当代码中达到循环时,条件被测试

这个图表说明了当代码中达到循环时,条件被测试。如果条件为真,则执行条件代码。执行条件代码后,再次测试条件。任何时候条件为假,代码继续执行循环后的内容。这可能意味着条件代码从未执行。

我们将研究 Java 提供的所有主要类型的循环,以控制我们的代码,并使用其中一些来实现一个工作迷你应用程序,以确保我们完全理解它们。让我们先看看 Java 中的第一种和最简单的循环类型,称为while循环。

while 循环

Javawhile循环具有最简单的语法。回想一下if语句。我们可以在if语句的条件表达式中放置几乎任何组合的运算符和变量。如果表达式求值为真,则执行if块中的代码。对于while循环,我们也使用一个可以求值为真或假的表达式。看看这段代码:

int x = 10;
while(x > 0){
   x--;
   // x decreases by one each pass through the loop
}

这里发生的是:

  1. while循环之外,声明并初始化了一个名为x的整数,其值为10

  2. 然后,while循环开始。它的条件是x > 0。因此,while循环将执行其循环体中的代码。

  3. 其循环体中的代码将继续执行,直到条件求值为假。

因此,先前的代码将执行 10 次。

在第一次通过时,x = 10,在第二次通过时它等于9,然后是8,依此类推。但一旦x等于0,它当然不再大于 0。此时,程序将退出while循环,并继续执行while循环后的第一行代码。

就像if语句一样,可能while循环甚至不执行一次。看看这个例子,其中while循环中的代码不会执行:

int x = 10;
while(x > 10){
   // more code here.
   // but it will never run 
   // unless x is greater than 10.
}

此外,条件表达式的复杂程度或循环体中的代码量是没有限制的。这里是另一个例子:

int newMessages = 3;
int unreadMessages = 0;
while(newMessages > 0 || unreadMessages > 0){
   // Display next message
   // etc.
}
// continue here when newMessages and unreadMessages equal 0

前面的while循环将继续执行,直到newMessagesunreadMessages都等于或小于 0。由于条件使用了逻辑或运算符||,其中一个条件为真将导致while循环继续执行。

值得注意的是,一旦进入循环体,即使表达式在中途评估为 false,循环体也会始终完成,因为直到代码尝试开始另一个循环时才会再次测试。看下面的例子:

int x = 1;
while(x > 0){
   x--;
   // x is now 0 so the condition is false
   // But this line still runs
   // and this one
   // and me!
}

前面的循环体将执行一次。我们还可以设置一个永远运行的while循环!这或许不足为奇地被称为无限循环。以下是一个无限循环的例子:

int x = 0;
while(true){
   x++; // I am going to get very big!
}

跳出循环

我们可能会像这样使用无限循环,这样我们可以决定何时从其体内的测试中退出循环。当我们准备离开循环体时,我们将使用break关键字。这是一个例子:

int x = 0;
while(true){
   x++; //I am going to get very big!
   break; // No, you're not- ha!
   // code doesn't reach here
}

你可能已经猜到,我们可以在while循环和我们即将看到的所有其他循环中结合使用任何决策工具,比如ifelseswitch。看下面的例子:

int x = 0;
int tooBig = 10;
while(true){
   x++; // I am going to get very big!
   if(x == tooBig){
         break;
   } // No, you're not- ha!

   // code reaches here only until x = 10
}

演示while循环的多样性可能会简单地继续下去很多页,但在某个时候,我们想要回到做一些真正的编程。所以这是与while循环结合的最后一个概念。

continue 关键字

break - 有一个限制。continue关键字将跳出循环体,但之后也会检查条件表达式,所以循环可以再次运行。举个例子:

int x = 0;
int tooBig = 10;
int tooBigToPrint = 5;
while(true){
   x++; // I am going to get very big!
   if(x == tooBig){
         break;
   } // No, you're not- ha!

   // code reaches here only until x = 10
   if(x >= tooBigToPrint){
         // No more printing but keep looping
         continue;
   }
   // code reaches here only until x = 5
   // Print out x 
}

do while 循环

do while循环与while循环非常相似,唯一的区别是do while循环在循环体之后评估其表达式。看看下面修改的前一个图,它表示了do while循环的流程:

图 8.3 - do while 循环

图 8.3 - do while 循环

这意味着do while循环在检查循环条件之前至少会执行一次条件代码:

int x= 1
do{
   x++;
}while(x < 1);
// x now = 2 

在上面的代码中,即使测试为 false,循环也会执行,因为测试是在循环执行之后进行的。然而,测试阻止了循环体再次执行。这导致x增加了一次,现在x等于2

重要提示

请注意,breakcontinue也可以在do while循环中使用。

我们将要介绍的下一种循环类型是for循环。

for 循环

for循环的语法比whiledo while循环稍微复杂一些,因为它需要三个部分来初始化。先看看代码,然后我们将把它分解开来:

for(int i = 0; i < 10;  i++){
   //Something that needs to happen 10 times goes here
}

稍微复杂一点的for循环形式在这样表述时更清晰:

for(declaration and initialization; condition; change after each pass through loop).

进一步澄清,我们有以下内容:

  • 声明和初始化:我们创建一个新的int变量i,并将其初始化为 0。

  • 条件:就像其他循环一样,它指的是必须为真的条件,循环才能继续。

  • 每次通过循环后更改:在例子中,i++表示在每次通过循环时向i添加/递增 1。我们也可以使用i--在每次通过循环时减少/递减i

for(int i = 10; i > 0;  i--){
   // countdown
}
// blast off i = 0

重要提示

请注意,breakcontinue也可以在for循环中使用。

for循环控制初始化、条件评估和变量的修改。

循环演示应用程序

首先,创建一个名为Loops的新 Android 项目,使用Empty Activity模板,并将所有其他设置保持默认。

让我们在 UI 中添加一些按钮,使其更有趣。切换到activity_main.xml文件,并确保你在Design选项卡上,然后按照以下步骤进行:

  1. 将一个按钮拖放到 UI 上,并在水平方向靠近顶部居中。

  2. 在属性窗口中,更改Count Up

  3. 在属性窗口中,更改countUp

  4. 在上一个按钮的正下方放置一个新按钮,并重复步骤 23,但这次在onClick属性中使用Count Down作为countDown的文本属性。

  5. 在上一个按钮的正下方放置一个新按钮,并重复步骤 23,但这次在text属性中使用nested,在onClick属性中使用nested

  6. 点击推断约束按钮以约束三个按钮的位置。

外观对于这个演示并不重要,但运行应用程序并检查布局是否与以下截图类似是很重要的:

图 8.4 – 添加到 UI 的按钮

图 8.4 – 添加到 UI 的按钮

重要提示

我还删除了“Hello World!”的TextView,但这并不是必要的。

重要的是,我们有三个按钮,标有COUNT UPCOUNT DOWNNESTED,分别调用名为countUpcountDownnested的方法。

通过左键单击编辑器上方的MainActivity.java标签切换到MainActivity.java文件,我们可以开始编写我们的方法。

onCreate方法的闭合大括号之后,添加下面显示的countUp方法:

public void countUp(View v){
   Log.i("message:","In countUp method");

   int x = 0;
   // Now an apparently infinite while loop
      while(true){
       // Add 1 to x each time
       x++;
       Log.i("x =", "" + x);
       if(x == 3){
          // Get me out of here
          break;
       }
   }
}

重要提示

使用您喜欢的方法导入LogView类:

import android.util.Log;

import android.view.View;

我们将能够从相应标记的按钮中调用我们刚刚编写的方法。

countUp方法的闭合大括号之后,添加countDown方法:

public void countDown(View v){
   Log.i("message:","In countDown method");
   int x = 4;
   // Now an apparently infinite while loop
   while(true){
       // Add 1 to x each time
       x--;
       Log.i("x =", "" + x);
       if(x == 1){
          // Get me out of here
          break;
       }
   }
}

我们将能够从相应标记的按钮中调用我们刚刚编写的方法。

countDown方法的闭合大括号之后,添加nested方法:

public void nested(View v){
   Log.i("message:","In nested method");
   // a nested for loop
   for(int i = 0; i < 3; i ++){
         for(int j = 3; j > 0; j --){
                // Output the values of i and j
                Log.i("i =" + i,"j=" + j);
         }
   }
}

我们将能够从相应标记的按钮中调用我们刚刚编写的方法。

现在,让我们运行应用程序并开始点击按钮。如果你从上到下依次点击每个按钮一次,你将看到以下控制台输出:

message:: In countUp method
x =: 1
x =: 2
x =: 3
message: : In countDown method
x =: 3
x =: 2
x =: 1
message: : In nested method
i =0: j=3
i =0: j=2
i =0: j=1
i =1: j=3
i =1: j=2
i =1: j=1
i =2: j=3
i =2: j=2
i =2: j=1

我们可以看到countUp方法确实做到了这一点。int x变量初始化为0,进入无限的while循环,并使用递增++运算符递增x。幸运的是,在循环的每次迭代中,我们使用if (x == 3)测试x是否等于3,并在这成立时中断。

接下来,在countDown方法中,我们以相反的方式做同样的事情。int x变量初始化为4,进入无限的while循环,并使用递减--运算符递减x。这次,在循环的每次迭代中,我们使用if (x == 1)测试x是否等于1,并在这成立时中断。

最后,我们在彼此之间嵌套了两个for循环。我们可以从输出中看到,每当i(由外部循环控制)增加时,j(由内部循环控制)从3减少到1。仔细观察这个截图,它显示了每个for循环的开始和结束的位置,以帮助完全理解这一点:

图 8.5 – for 循环

图 8.5 – for 循环

当然,你可以继续点击观察每个按钮的输出,时间长短由你决定。作为一个实验,尝试让循环更长,也许是1000

通过彻底学习和测试循环,让我们在下一章中看看方法的更细节。

总结

在本章中,我们学习了如何使用ifelseswitch来根据表达式做出决策并分支我们的代码。我们看到并练习了whilefordo while来重复我们代码的部分。然后我们在两个快速演示应用程序中将它们整合在一起。

在下一章中,我们将更仔细地学习 Java 方法,这是我们所有代码的所在地。