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

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

您是否正在尝试开始编程职业,但还没有找到合适的方式?您是否有一个很棒的应用程序想法,但不知道如何使其成为现实?如果是这样,那么这本书就是为您而写的!

本更新的第三版《Android 初学者编程》将是您从头开始创* Android 应用程序的伴侣。我们将向您介绍在 Android 环境中编程的所有基本概念,从 Java 的基础知识到使用 Android API。所有示例都使用最新的 API 类,并且是在官方的 Android 开发环境 Android Studio 中创*的,这有助于加速您的应用程序开发过程。

在这个速成课程之后,我们将深入研究 Android 编程,您将学*如何通过片段创*具有专业标准 UI 的应用程序,并使用 SQLite 存储用户数据。此外,您还将了解如何使您的应用程序多语言化,用手指绘制到屏幕上,并处理图形、声音和动画。

通过本书,您将准备好开始在 Android 和 Java 中构*自己的定制应用程序。

本书适合人群

本书适合您如果您完全是 Java、Android 或编程的新手,并且想要制作 Android 应用程序。本书还将作为一个复*,供那些已经有使用 Java 在 Android 上经验的人使用,以帮助您提高知识并通过早期项目快速进步。

本书涵盖内容

第一章《开始 Android 和 Java》将帮助您立即开始开发 Android 应用程序,不浪费任何时间。我们将探讨 Android 的优势,Android 和 Java 究竟是什么,它们如何工作和相互补充,以及作为未来开发人员对我们意味着什么。随后,我们将设置所需的软件,以便我们可以构*和部署一个简单的第一个应用程序。

第二章《初次接触:Java、XML 和 UI 设计师》解释了在这个阶段,我们已经拥有了一个可工作的 Android 开发环境,并且已经构*和部署了我们的第一个应用程序。我们需要探索这个自动生成的代码,以便我们可以开始了解 Android,然后学*如何在这个有用的模板上构*。

第三章《探索 Android Studio 和项目结构》解释了如何创*和运行另外两个 Android 项目。这些练*的目的是更深入地探索 Android Studio 和 Android 项目的结构。除了了解我们项目的组成部分之外,确保我们充分利用模拟器也是有益的。

第四章《使用布局和 Material Design 入门》涵盖了构*另外三种布局的内容,这些布局仍然相当简单,但比我们迄今为止所做的要进一步。在我们动手之前,我们将快速介绍 Material Design 的概念。我们还将介绍名为 LinearLayout、ConstraintLayout 和 TableLayout 的不同类型的布局。我们还将编写一些 Java 代码,以在单个应用/项目中在不同的布局之间进行切换。这是第一个将多个主题整合在一个整洁包裹中的重要应用程序。

第五章《使用 CardView 和 ScrollView 创*美丽的布局》是在我们花一些时间专注于 Java 和面向对象编程之前的最后一章布局。我们将对我们已经遇到的一些不同属性进行正式化学*,并且还将介绍另外两种很酷的布局:ScrollView 和 CardView。最后,我们将在*板模拟器上运行一个 CardView 项目。

第六章《Android 生命周期》将使我们熟悉 Android 应用程序的生命周期。生命周期是所有 Android 应用程序与 Android 操作系统交互的方式。我们将看到应用程序从创*到销毁经历的生命周期阶段,以及这如何帮助我们根据我们想要实现的目标来放置我们的 Java 代码。

第七章《Java 变量、运算符和表达式》涵盖了 Java 的核心基础知识:放入类和我们创*的方法中的代码,以及代码作用的数据。在这一章中,我们将专注于数据。我们将深入学*如何编写我们自己的 Java 代码。在本章结束时,您将能够轻松编写创*和使用 Android 中数据的 Java 代码。

第八章《Java 决策和循环》解释了我们如何根据变量的值采取行动。我们将使用ifelseswitch来在 Java 中做出决策。我们还将使用whiledo-whileforbreak来研究 Java 中的循环。

第九章《学* Java 方法》更深入地研究了方法,因为尽管我们知道可以调用它们来执行它们的代码,但它们比我们迄今为止讨论的要复杂得多。

第十章《面向对象编程》解释了在 Java 中,类对几乎所有事情都是基础的。我们将开始理解为什么上世纪 90 年代初太阳微系统的软件工程师们会以他们的方式制作 Java。我们已经讨论了重用其他人的代码,特别是 Android API,但在这一章中,我们将真正掌握它的工作原理,并了解面向对象编程以及如何使用它。

第十一章《更多面向对象编程》是我们对面向对象编程的第二部分(理论和实践)。我们已经简要讨论了封装、继承和多态的概念,但在这一章中,我们将在一些演示应用程序中看到它们的实际应用。虽然这些工作示例将展示这些概念的最简单形式,但这仍然是掌握通过 Java 代码控制我们的 XML 布局的重要一步。

第十二章《堆栈、堆和垃圾收集器》揭示了 Java 和我们的 XML 布局之间的缺失链接,使我们有能力向我们的应用程序添加各种小部件,就像以前一样,但这次我们将能够通过我们的 Java 代码控制它们。在这一章中,我们将控制一些相当简单的 UI 元素,如按钮和文本视图。为了使我们能够理解发生了什么,我们需要更多地了解 Android 设备中的内存以及其中的两个区域——堆栈和堆。

第十三章《匿名类-让 Android 小部件栩栩如生》解释了现在我们对 Android 应用程序的布局和编码有了很好的概述,以及我们对面向对象编程的新见解以及如何从我们的 Java 代码中操作 UI,我们已经准备好通过匿名类与调色板中的更多小部件进行实验。在这一章中,我们将通过回到 Android Studio 调色板并查看我们还没有完全使用过的半打小部件来进行大量的多样化。完成后,我们将把它们放入布局中,并练*用 Java 代码操作它们。

第十四章,安卓对话框窗口,介绍了如何向用户呈现弹出式对话框窗口。然后我们将把我们所知道的内容都放入我们的第一个主要应用程序 Note to self 的第一阶段。在这一章和接下来的四章中,我们还将学*关于新的安卓和 Java 特性,然后利用我们新获得的知识每次增强 Note to self 应用程序。在每一章中,我们还将构*一些与主应用程序分开的小应用程序。

第十五章,数组、映射和随机数,讨论了 Java 数组,它允许我们以有组织和高效的方式操纵大量数据。我们还将使用与数组有密切关系的 Java 关系,ArrayList,并看到它们之间的区别。一旦我们能够处理大量数据,我们将看到安卓 API 为了帮助我们轻松地将我们新获得的数据处理技能连接到用户界面而做了哪些工作。

第十六章,适配器和回收器,首先介绍了适配器和列表的理论。我们将学*如何在 Java 代码中扩展 RecyclerAdapter 并添加一个 RecyclerView,它作为我们的用户界面的列表,然后通过安卓 API 的明显魔力将它们绑定在一起,使 RecyclerView 显示 RecyclerAdapter 的内容,并允许用户滚动查看内容。

第十七章,数据持久性和共享,探讨了将数据保存到安卓设备永久存储的几种不同方式。同时,我们还将首次向我们的应用程序添加第二个活动。在实现一个独立的“屏幕”,比如我们的应用中的设置屏幕时,将新*一个活动是有意义的。我们将学*如何添加一个活动并在它们之间引导用户。

第十八章,本地化,解释了如何添加额外的语言。我们将看到通过字符串资源的正确方式添加文本在添加多种语言时对我们有什么好处。

第十九章,动画和插值,介绍了如何使用动画类使我们的用户界面不那么静态,更有趣。正如我们所期望的那样,安卓 API 将允许我们用相对简单的代码做一些相当先进的事情,动画类也不例外。

第二十章,绘图图形,致力于安卓 Canvas 类和一些相关类,比如 Paint、Color 和 Bitmap。这些类的结合在绘制屏幕时带来了巨大的力量。如果我们想制作一个绘图应用程序、绘制图表,或者可能是一个游戏,我们需要控制安卓设备提供的每个像素。

第二十一章,线程和启动实时绘图应用程序,开始了下一个应用程序。这个应用程序将是一个儿童风格的绘图应用程序,用户可以用手指在屏幕上绘图。然而,这个绘图应用程序将略有不同。用户绘制的线条将由粒子系统组成,这些粒子系统会爆炸成成千上万的碎片。我们将把这个项目称为 Live Drawing。

第二十二章,粒子系统和处理屏幕触摸,解释了如何创*在这个实时系统中存在和演变的实体,就像它们有自己的思维,并形成用户可以实现的绘图的外观。我们还将看到用户如何通过学*如何响应与屏幕的交互来绘制这些实体。这与与 UI 布局中的小部件进行交互是不同的。

第二十三章,支持不同版本的 Android、声音效果和 Spinner 小部件,解释了我们如何检测和处理不同版本的 Android。然后,我们将能够研究 SoundPool 类以及根据应用程序运行的 Android 版本不同的使用方式。在这一点上,我们可以将我们学到的一切投入到制作一个很酷的声音演示应用程序中,这也将向我们介绍一个新的 UI 小部件——Spinner。

第二十四章,设计模式、多个布局和片段,更多关于您未来应用程序的内容,而不是本书迄今为止的任何内容。我们将看一些 Java 和 Android 的方面,您可以将其用作制作更加令人兴奋和复杂的应用程序的框架或模板,同时保持代码的可管理性。此外,我将*议进一步学*的领域,这本书根本没有足够的空间来涉及。

第二十五章,构*一个简单的图库应用程序,解释了如何使用分页和图像滑动创*应用程序,就像您可能在照片库应用程序中找到的那样。此外,使用 RecyclerView 小部件,我们将有选择地仅加载当前页面所需的数据,以及可能是前一页和下一页页面的数据。

第二十六章,带有导航抽屉和片段的高级 UI,涵盖了(可以说是)最高级的 UI。 NavigationView 可以通过在创*新项目时选择它作为模板来简单创*。然后,我们将检查自动生成的代码,并学*如何与其交互。然后,我们将利用我们对 Fragment 的所有了解来填充每个“抽屉”以具有不同的行为和视图。

第二十七章,Android 数据库,解释了如果我们要制作提供给用户重要功能的应用程序,那么几乎可以肯定我们需要一种管理、存储和过滤大量数据的方式。JSON 和 SharedPreferences 类有它们的用处,但是在某个时候,我们需要转向使用真正的数据库来解决现实世界的问题。Android 使用 SQLite 数据库管理系统,正如您所期望的那样,有一个 API 使其尽可能简单。

第二十八章,告别前的快速聊天,结束了我们的旅程。本章简单地包括了一些想法和指针,您可能在匆忙离开并制作自己的应用程序之前想要看一看。

为了充分利用本书

要成功阅读本书,您根本不需要任何经验。如果您对自己选择的操作系统(Windows、macOS 或 Linux)有信心,您可以在学* Java 编程语言的同时学*制作 Android 应用程序。学*开发专业质量的应用程序是任何人都可以开始并坚持下去的旅程。

如果您之前有编程(Java 或任何其他语言)、Android 或其他开发经验,那么您将在前几章中取得更快的进展。

如果您使用本书的数字版本,我们*议您自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中提供)。这样做将有助于避免与复制和粘贴代码相关的任何潜在错误。

下载示例代码文件

您可以从 GitHub 上下载本书的示例代码文件github.com/PacktPublishing/Android-Programming-for-Beginners-Third-Edition。如果代码有更新,它将在现有的 GitHub 存储库上进行更新。

我们还有来自我们丰富书籍和视频目录的其他代码捆绑包可供下载github.com/PacktPublishing/。去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:static.packt-cdn.com/downloads/9781800563438_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

文本中的代码:指示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个例子:“最后一个成员变量是一个LayoutInflater实例,它将用于填充pager_item.xml的每个实例。”

代码块设置如下:

@Override
public int getCount() {
   return images.length;
}

当我们希望引起您对代码块的特定部分的注意时,相关行或项将以粗体显示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;

任何命令行输入或输出都将按如下方式编写:

$ mkdir css
$ cd css

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中以这种方式出现。以下是一个例子:“从管理面板中选择系统信息。”

提示或重要说明

会出现这样。

第一章:开始 Android 和 Java

欢迎使用《初学者的 Android 编程第三版》。在第一章中,我们将立即开始开发 Android 应用程序。我们将看看 Android 有什么好处,Android 和 Java 究竟是什么,它们如何工作和相互补充,以及作为未来开发者,这对我们意味着什么。随后,我们将快速设置所需的软件,以便我们可以构*和部署一个简单的第一个应用程序。

注意

我的目标是使本书保持最新。请查看以下网页,了解自书印刷以来 Android Studio 的任何更改讨论和提示:gamecodeschool.com/books/android-programming-for-beginners-3rd-edition#android-studio-updates

在本章结束时,我们将完成以下内容:

  • 了解了第三版的新内容

  • 了解了 Java 和 Android 如何相互配合

  • 设置我们的开发环境——Android Studio——它负责构*我们将在接下来学*的 Android 应用程序所涉及的所有组件

  • 了解了Java 开发工具包JDK)和 Android 应用程序编程接口API)以及我们如何通过 Android Studio 使用它们

  • 构*我们的第一个 Android 应用程序

  • 在 Android 模拟器上部署应用程序

  • 在 Android 模拟器和真实设备上运行我们的应用程序

这是很多内容,让我们开始吧。

技术要求

以下是使用 Android Studio 及其相关工具进行 Android 开发的官方技术要求。但这些是绝对的最低要求。请参阅“设置 Android Studio”部分以获取更多详细信息。

Windows 的要求如下:

  • Microsoft® Windows® 7/8/10(64 位)

  • 至少需要 4GB 的 RAM;*议 8GB 的 RAM

  • 至少需要 2GB 的可用磁盘空间;*议 4GB(500MB 用于集成开发环境IDE)+ 1.5GB 用于 Android 软件开发工具包SDK)和模拟器系统镜像)

  • 最低屏幕分辨率为 1,280 x 800

Mac 的要求如下:

  • Mac® OS X® 10.10(Yosemite)或更高版本,最高为 10.14(macOS Mojave)

  • 至少需要 4GB 的 RAM;*议 8GB 的 RAM

  • 至少需要 2GB 的可用磁盘空间;*议 4GB(500MB 用于 IDE + 1.5GB 用于 Android SDK 和模拟器系统镜像)

  • 最低屏幕分辨率为 1,280 x 800

Linux 的要求如下:

  • GNOME 或 KDE 桌面

  • 在基于 Debian 的 gLinux 上进行了测试

  • 64 位发行版,能够运行 32 位应用程序

  • GNU C 库(glibc)2.19 或更高版本

  • 至少需要 4GB 的 RAM;*议 8GB 的 RAM

  • 至少需要 2GB 的可用磁盘空间;*议 4GB(500MB 用于 IDE + 1.5GB 用于 Android SDK 和模拟器系统镜像)

  • 最低屏幕分辨率为 1,280 x 800

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

第三版有什么新内容?

与第一版相比,第二版增加了大量额外的主题。不幸的是,*装书只能容纳有限的页数。因此,这一版着重于改进 Java、Android 和应用开发概念的教学方式。我们重新思考了主题的解释方式,并使其比以前更加直观。此外,我设法加入了大约十几个新的迷你主题。这些内容要么是 Java 基础知识,比如在早期版本中没有涵盖的变量类型,要么是新的 Android Studio 功能,比如分析器,或者是经典的编程概念,比如方法递归和我们代码的实时调试。希望这第三版能让您的 Android 和 Java 之旅更加顺利和完整。

为什么选择 Java 和 Android?

当 Android 于 2008 年首次出现时,与苹果 iPhone/iPad 上更时尚的 iOS 相比,它显得有些沉闷。但通过迅速推出各种各样的手机产品,既符合实际价格敏感的人群,又符合时尚和科技精通的人群,Android 用户数量迅速增长。

对于许多人来说,包括我自己在内,为 Android 开发是最有意义的业余爱好和事业。

快速地将一个想法的原型组装起来,完善它,然后决定运行并将其连接成一个完全成熟的应用程序,这是一个如此令人兴奋和有意义的过程。任何编程都可以很有趣,我一生都在编程,但为 Android 创作某种程度上是非常有意义的。

确切地定义这种情况为什么会发生是相当困难的。也许是因为这个*台是免费和开源的。您可以在不需要大型控制性公司的许可的情况下分发您的应用程序 - 没有人可以阻止您。与此同时,您还可以在亚马逊应用商店和 Google Play 等成熟的、由公司控制的大众市场上分发应用程序。

更有可能的是,为 Android 开发带来如此良好的感觉的原因是设备本身的性质。它们是非常个人化的。您可以开发与人们生活互动的应用程序 - 教育、娱乐、讲故事等等 - 它就在他们的口袋里准备好了,在家里、工作场所或度假时。

当然,您可以为桌面构*更大的东西。但知道成千上万(甚至数百万)的人携带着您的作品并与朋友分享,这带来的不仅仅是一种兴奋。

事实上,为 Android 开发被认为是非常有技巧的,最成功的开发人员受到极大的钦佩,甚至崇敬。

如果所有这些空洞和精神上的东西对您毫无意义,那也没关系;为 Android 开发可以让您谋生,甚至让您致富。随着设备拥有量的持续增长,CPU 和 GPU 性能的不断提升,以及 Android 操作系统本身的不断演进,对专业应用程序开发人员的需求只会增长。

简而言之,最优秀的 Android 开发人员 - 更重要的是,拥有最好的想法和最大决心的 Android 开发人员 - 比以往任何时候都更受欢迎。没有人知道这些未来的 Android 应用程序开发人员是谁,他们甚至可能还没有写过他们的第一行 Java 代码。

那么,为什么不是每个人都是 Android 开发人员呢?显然,并不是每个人都会像我一样热衷于创造能够帮助人们改善生活的软件,但我猜测因为您正在阅读这篇文章,您可能会!

初学者的第一个绊脚石

不幸的是,对于那些和我一样热衷的人来说,进步的道路上存在一种玻璃墙,这让许多有抱负的 Android 开发人员感到沮丧。

Android 使用 Java 来制作应用程序。每本 Android 书籍,甚至那些针对所谓初学者的书籍,都假设读者至少具有中级水*的 Java 知识,大多数需要高级水*的知识。因此,良好到优秀的 Java 知识曾经是学* Android 的先决条件。

不幸的是,在完全不同的上下文中学* Java 有时可能会有点乏味,而您学到的大部分知识在 Android 世界中并不直接可转移。您可以理解为什么初学者对 Android 和 Java 经常望而却步。

但不一定非得这样。在这本书中,我已经把你在厚重的、专门针对初学者的 Java 书中学到的所有 Java 主题都精心安排成了四个多章节的应用程序和十几个快速的迷你应用程序,从一个简单的备忘录应用程序开始,然后逐渐发展成一个酷炫的绘图应用程序、一个数据库应用程序和一个可玩的游戏(可在线使用)。

如果您想成为专业的 Android 开发人员,或者只是想在学* Java 和 Android 时更有乐趣,这本书会帮助您。

Java 和 Android 如何协同工作

在开始我们的 Android 探索之前,我们需要了解 Android 和 Java 如何一起工作。Android 是一个复杂的系统,但你不需要深入了解它才能制作出令人惊艳的应用程序。

在我们为 Android 用 Java 编写程序之后,我们点击一个按钮,我们的代码就会被转换成另一种形式,这种形式是 Android 可以理解的形式。这种形式被称为字节码,转换过程被称为编译

然后,当用户安装我们的应用程序时,字节码会被另一个进程(称为Android RuntimeART))转换为机器码。这是最快的执行格式。因此,如果你曾听说过不应该使用 Java 因为它很慢,那么你知道他们是错误的。Java 对程序员来说编程速度很快,然后在安装时转换为设备快速的机器码。还有什么比这更好的呢?

ART 不仅能够使我们的应用程序执行速度超快,而且还能降低电池使用。此外,ART 系统不仅仅创*机器码然后坐下来放松;它提供了钩子进入我们的应用程序,增强了应用程序在运行时的内存管理。这使得我们的应用程序运行更高效,并且,正如我们将在第十二章中看到的那样,堆栈、堆和垃圾收集器,更容易通过处理内存管理的关键方面来编写。

ART 本身是用另一种语言编写的软件系统,运行在一个特别适应的 Linux 操作系统的版本上。因此,用户所看到的 Android 本身只是在另一个操作系统上运行的应用程序。

Android 是一个子系统的集合。典型的 Android 用户看不到 Linux 操作系统,也不知道 ART 的存在,但它们都在那里运转。

系统的 Linux 部分的目的是隐藏 Android 运行的硬件和软件的复杂性和多样性,但同时暴露出所有有用的功能。这种功能的暴露有两种方式:

  • 首先,系统本身必须能够访问硬件,它确实可以。

  • 其次,这种访问必须是对程序员友好且易于使用的——这是因为 Android API

让我们继续谈谈 Android API。

注意

这本书是关于学* Java 并从头开始构* Android 应用程序的,所以我不会深入探讨 Android 的工作原理。然而,如果你想了解更多,那么维基百科页面是一个很好的参考:en.wikipedia.org/wiki/Android_(operating_system

理解 Android API

Android API 是一种使得做出非凡事情变得容易的代码。可以用一个简单的类比来理解,比如一台机器,也许是一辆汽车。当你踩油门时,引擎盖下会发生一大堆事情。我们不需要理解燃烧或燃油泵,因为一些聪明的工程师为我们制作了一个接口——在这种情况下,是一个机械接口:油门踏板。

例如,下面这行 Java 代码在本书的这个阶段可能看起来有点吓人,但它是 Android API 如何帮助我们的一个很好的例子:

locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)

一旦你了解到这一行代码在太空搜索可用的卫星,然后与它们在地球轨道上通信,然后在地球表面检索你的精确纬度和经度,那么开始逐渐领略 Android API 与编译的字节码和 ART 的强大和深度就变得容易了。

当然,这段代码看起来有点具有挑战性——甚至在本书的这个阶段都是如此——但想象一下用其他方式与卫星交流试试看!

Android API 已经为我们编写了一大堆可以随意使用的 Java 代码。

注意

关于 Android 的代码行数有很多不同的估计。一些估计只有 100 万行,而一些估计高达 2000 万行。令人惊讶的是,尽管有如此庞大的代码量,Android 在编程圈中以“轻量级”而闻名。

我们必须问的问题,也是本书试图回答的问题是:

我们如何使用所有这些代码来做酷炫的事情?或者为了符合之前的类比,我们如何找到并操作 Android API 的踏板、方向盘和天窗?

这个问题的答案是 Java 编程语言以及 Java 旨在帮助程序员处理复杂性的事实。让我们稍微谈一下 Java 和面向对象编程(OOP)。

Java 是面向对象的

Java 是一种编程语言,比 Android 存在的时间长得多。它是一种面向对象的语言。这意味着它使用可重用的编程对象的概念。如果这听起来像技术术语,另一个类比会有所帮助。Java 使我们和其他人(比如 Android 开发团队)能够编写可以基于现实世界事物构*的 Java 代码,这是重要的部分:它可以被重复使用。

因此,使用汽车类比,我们可以问一个问题:如果一个制造商一天制造多辆汽车,他们是否为每辆汽车重新设计每个零件?

当然,答案是否定的。他们让高技能的工程师开发完全正确的组件,经过多年的磨练、改进和提高。然后,同样的组件被反复重复使用,偶尔进行改进。

如果你对我的类比挑剔,那么你可以指出每个汽车的组件仍然必须使用现实生活中的工程师或机器人等从原材料中构*出来。

这是真的。软件工程师在编写代码时所做的是构*对象的蓝图。然后我们使用 Java 代码从他们的蓝图创*对象,一旦我们有了那个对象,我们可以配置它,使用它,与其他对象组合,以及更多。

此外,除此之外,我们也可以设计自己的蓝图并从中制作对象。然后编译器将我们的定制创*转换(制造)成字节码。嘿,变戏法!我们有了一个 Android 应用程序。

在 Java 中,蓝图被称为类。当一个类被转化为一个真正工作的“东西”时,我们称之为类的对象或实例。

对象简洁

我们可以继续进行类比。然而,我们在这一点上关心的是以下内容。

Java 是一种允许我们编写一次代码然后重复使用的语言。这非常有用,因为它节省了我们的时间,并允许我们使用其他人的代码来执行我们可能没有时间或知识来编写的任务。大多数时候,我们甚至不需要看到这些代码,甚至不知道它是如何工作的!最后一个类比:我们只需要知道如何使用代码,就像我们需要学会如何驾驶汽车一样。

因此,Android 总部的一位聪明的软件工程师编写了一个非常复杂的 Java 程序,可以与卫星通信。然后,他们考虑如何使这段代码对所有想要开发使用用户位置做酷炫事情的 Android 程序员有用。其中一件事情是使获取设备在世界上的位置成为一个简单的一行任务。

因此,我们之前看到的一行代码引发了更多我们看不到、也不需要看到的代码。这是一个使用别人的代码使我们的代码变得无限简单的例子。

如果你不必看到所有的代码这个事实让你失望,那么我理解你的感受。当我们学*某样东西时,有些人希望了解每一个细节。如果你也是这样的话,那么请放心,学* Android API 内部工作原理的最佳起点是按照 API 程序员的意图来使用它。在整本书中,我会定期指出更多学*机会,让你了解 Android API 的内部工作原理。此外,我们将编写一些可重复使用的类,有点像我们自己的 API,只是我们的类将专注于我们想要我们的应用程序做的事情。

欢迎来到面向对象编程OOP)的世界。我会在每一章中不断提到 OOP,而在 第十章 中有关于它如何工作的大揭示,面向对象编程

再告诉我一遍 - Android 究竟是什么?

在 Android 上,我们编写自己的 Java 代码,同时也使用 Android API 的 Java 代码。然后,这些代码被编译成字节码,并在用户安装时由 ART 转换成机器码,而机器码又与一个名为 Linux 的底层操作系统连接,处理着不同 Android 设备的复杂和极其多样的硬件。

Android 设备的制造商和个别硬件组件显然也知道这一点,他们编写了称为驱动程序的高级软件,确保他们的硬件(CPU、GPU、GPS 接收器、存储芯片、硬件接口等)可以在底层 Linux 操作系统上运行。

字节码(以及其他一些资源)被放在一个称为Android 应用程序包APK)的文件包中,这是 ART 运行以准备我们的应用程序给用户使用所需的东西。

注意

不需要记住我们的代码与硬件交互时经历的步骤的细节。只需要理解我们的 Java 代码经历了一些自动化过程,成为我们将发布到 Google Play 商店的应用程序。

接下来的问题是“所有这些 Java 编码和编译成字节码以及 APK 打包到底发生在哪里?”。让我们看看我们将要使用的开发环境。

Android Studio

开发环境 是一个术语,指的是在一个地方拥有你开发所需的一切并准备就绪。我们需要两样东西才能开始:

  • 我们谈到了编译我们的 Java 代码,以及其他人的 Java 代码,将其转换成用户 Android 设备上可执行的机器码。为了使用 Java 代码,我们需要一些名为JDK的免费软件。JDK 包括了更多其他人的代码,这些代码与 Android API 是分开的。

  • 开发 Android 需要一整套工具,当然还需要 Android API。这整套要求被统称为 Android SDK。幸运的是,下载并安装一个应用程序就能将这些东西捆绑在一起。这个应用程序叫做 Android Studio

Android Studio 是一个集成开发环境,将处理编译我们的代码和与 JDK 和 Android API 进行链接的所有复杂性。一旦安装了 Android Studio,我们可以在这个单一应用程序中完成所有需要的工作,并将我们讨论过的许多复杂性放在脑后。

提示

随着时间的推移,这些复杂性将变得自然而然。不需要掌握它们才能取得进一步的进展。

所以,我们最好开始使用 Android Studio。

设置 Android Studio

设置 Android Studio 相当简单,尽管有点冗长。拿些点心,然后按照以下步骤开始。本教程将把 Android Studio 安装到 D 驱动器上。我选择了 D 驱动器,因为这是一个大型安装,一旦我们下载了所有内容,大约有 12GB,而且许多 PC 上的 D 驱动器通常比 C 驱动器更大,有更多的可用空间。如果您希望安装在 C 驱动器(或任何其他驱动器),那么这些说明应该很容易调整:

  1. 访问developer.android.com/studio并单击下载 Android Studio按钮。这将开始下载最新稳定版本的 Windows。您需要接受条款和条件才能开始下载。

  2. 在等待下载完成时,在 D 驱动器的根目录上创*一个名为Android的新文件夹。在Android文件夹内,创*另一个名为Android Studio的新文件夹。返回到Android文件夹并创*另一个名为Projects的新文件夹。这是我们将在整本书中创*的所有项目文件的存放位置。创*另一个名为Sdk的新文件夹,这是我们将要求安装程序安装 Android SDK 的位置。现在您应该有一个看起来像这样的D:\Android文件夹:图 1.1 – D:\Android 文件夹

图 1.1 – D:\Android 文件夹

  1. 下载完成后,找到已下载的文件。它将被称为android-studio-ide....。双击该文件运行它。

  2. 您将被要求授予安装程序管理权限,然后您可以单击下一步开始安装。在选择组件屏幕上,确保Android StudioAndroid 虚拟设备选项都被选中,然后单击下一步按钮:图 1.2 – 选择组件屏幕

图 1.2 – 选择组件屏幕

  1. D:\Android\Android Studio,然后左键单击确定按钮:图 1.3 – 配置设置窗口

图 1.3 – 配置设置窗口

  1. 在前面的截图中,单击下一步按钮。在选择开始菜单文件夹窗口上,单击安装以接受默认选项。安装的第一部分现在将进行。

  2. 一旦收到安装完成消息,单击下一步按钮。然后可以单击完成按钮。

Android Studio 应该会自动启动。如果没有,您可以在 Windows 开始菜单中找到并启动 Android Studio 应用程序。

  1. 您将收到一条消息,提示您缺少 SDK(除非这不是您第一次使用 Android Studio)。单击下一步继续。

  2. D:\Android\Sdk,如下截图所示:图 1.4 – SDK 组件设置屏幕

图 1.4 – SDK 组件设置屏幕

  1. 单击下一步按钮。

  2. 验证设置窗口上,单击完成按钮。Android Studio 现在将下载一些文件并完成安装。这可能需要几分钟或更长时间,您可能会再次被提示允许访问您的 PC。

  3. 当过程结束时,单击完成按钮。

您将看到下一个 Android Studio 欢迎屏幕:

图 1.5 – Android Studio 欢迎屏幕

图 1.5 – Android Studio 欢迎屏幕

如果您要直接进行下一节,那么保持此屏幕。如果愿意,可以关闭它,然后在准备好继续时,像运行任何其他应用程序一样,从 Windows 开始菜单中运行 Android Studio。

让我们谈谈 Android 应用程序包括的所有不同内容。

什么构成了一个 Android 应用程序?

我们已经知道,我们将编写的 Java 代码本身将使用其他人的 Java 代码,并将被编译成字节码,然后在用户的 Android 设备上转换为机器代码。除此之外,我们还将添加和编辑其他包含在最终 APK 中的文件。这些文件被称为 Android 资源。

Android 资源

我们的应用程序将包括图像、声音和用户界面(UI)布局等资源,这些资源保存在与 Java 代码分开的文件中。我们将在本书的过程中慢慢介绍它们。

它还将包括具有我们应用程序文本内容的文件。通过单独的文件引用应用程序中的文本是一种惯例,因为这样做可以使它们易于更改,并且可以创*适用于不同语言和地理区域的应用程序。

此外,尽管有使用可视化设计工具实现应用程序的实际 UI 布局的选项,但实际上是由 Android 从基于文本的文件中读取的。

当然,Android(或任何计算机)无法像人类一样阅读和识别文本。因此,我们必须以高度组织和预定义的方式呈现我们的资源。为此,我们将使用.xml

你不需要记住这一点,因为我们将在整本书中不断回到这个概念。

Android 的 Java 代码结构

除了这些资源之外,值得注意的是,在 Android 中使用的 Java 代码具有其代码结构。我们可以利用数百万行代码。显然,这些代码需要以便于查找和引用的方式进行组织。它们被组织成特定于 Android 的

每当我们创*一个新的 Android 应用程序时,我们将选择一个称为的唯一名称。我们将在构*我们的第一个 Android 应用程序部分中看到我们是如何做到这一点的。包通常被分成子包,以便与其他类似的包一起分组。我们可以简单地将其视为文件夹和子文件夹,这几乎就是它的实质。

我们可以将 Android API 提供给我们的所有包视为代码库中的代码。我们将使用的一些常见 Android 包包括以下内容:

  • android.graphics

  • android.database

  • android.view.animation

正如你所看到的,它们被安排和命名,以使其中的内容尽可能明显。

注意

如果你想了解 Android API 的深度和广度,可以查看 Android 包索引:developer.android.com/reference/packages

之前,我们了解到我们可以将可重用的代码蓝图转换为对象的文件称为.java文件扩展名。

方法

在 Java(因此也是 Android)中,我们将类进一步分成执行不同操作的部分。我们称这些以行动为导向的部分为方法。通常,我们将使用类的方法来访问所有这些数百万行代码中提供的功能。

我们不需要阅读代码。我们只需要知道哪个类有我们需要的内容,它属于哪个包,以及类内的哪些方法能给我们精确的结果。

下一个图表显示了 Android API 的表示。我们将编写的代码结构将类似于此示例的结构,尽管我们通常每个应用程序只有一个包。

当然,由于 Java 的面向对象性质,我们只会使用 API 中的部分功能。还要注意,每个类都有其自己独特的数据。通常,如果你想访问类中的数据,你需要有该类的对象:

图 1.6 - Android API 的表示

图 1.6 - Android API 的表示

您不需要记住这一点,因为我们将在整本书中不断回到这个概念。

到本章结束时,我们将导入多个包,以及其中的几十个类,并且我们还将使用其中许多方法。到第二章**结束时,首次接触:Java、XML 和 UI 设计,我们甚至将编写我们自己的方法。现在我们可以开始第一个应用程序了。

构*我们的第一个 Android 应用程序

在编程中,传统上新学生的第一个应用程序会使用他们正在使用的语言/操作系统向世界打招呼。我们将快速构*一个只做这件事的应用程序,并且在第二章**结束时,首次接触:Java、XML 和 UI 设计,我们将超越这一点,并添加一些在用户按下按钮时响应的按钮。

注意

本章结束时的完整代码在第一章文件夹中的下载包中供您参考。但是,您不能简单地复制和粘贴这段代码!您仍然需要按照本章(以及所有项目开头)中解释的项目创*阶段进行操作,因为 Android Studio 在幕后做了大量工作。一旦您熟悉了这些步骤,并理解了哪些代码是由您作为程序员输入的,哪些代码/文件是由 Android Studio 生成的,那么您就能够通过复制和粘贴我在下载包中提供的文件来节省时间和输入。

按照以下步骤开始项目:

  1. 以与运行任何其他应用程序相同的方式运行 Android Studio。例如,在 Windows 10 上,启动图标会出现在开始菜单中。

提示

如果提示从…导入 Studio 设置,请选择不导入设置

  1. 您将看到 Android Studio 的欢迎屏幕,如下截图所示。找到开始一个新的 Android Studio 项目选项,并单击它:图 1.7 - Android Studio 欢迎屏幕

图 1.7 - Android Studio 欢迎屏幕

  1. 接下来的窗口是选择项目模板

这些是 Android Studio 可以根据您要开发的应用程序类型为您生成的一些有用的项目模板。我们将使用基本活动选项。Android Studio 将自动生成少量代码和一些资源来启动我们的项目。我们将在下一章详细讨论代码和资源。

  1. 选择基本活动。以下是选择项目模板窗口,其中选择了基本活动选项的图片:图 1.8 - 选择项目模板窗口

图 1.8 - 选择项目模板窗口

  1. 确保基本活动被选中,就像前面的截图中一样,然后点击下一步

  2. 之后,Android Studio 将弹出Hello World,文件的位置将是我们在设置 Android Studio部分中创*的Projects文件夹。

包名可以是几乎任何你喜欢的东西。如果您有一个网站,您可以使用com.yourdomainname的格式。如果没有,可以使用我的域名com.gamecodeschool.helloworld,或者您自己编造的东西。这只有在您要发布时才重要。

为了明确起见,如果您无法清楚地看到以下截图中的细节,这里是我使用的值。请记住,根据您对包名和项目位置的选择,您的值可能会有所不同:

提示

请注意,应用程序名称中HelloWorld之间有一个空格,但项目位置没有,如果有空格将无法工作。

  1. 下一张截图显示了一旦您输入了所有信息的配置您的项目屏幕:图 1.9 - 配置您的项目屏幕

图 1.9 - 配置您的项目屏幕

  1. 在上一张截图中,您可以看到 Android Studio 根据输入的信息自动生成了一个包名称。我的是com.gamecodeschool.helloworld。您的可能相同,也可能不同;这并不重要。

注意

您可以使用几种不同的语言编写安卓应用程序,包括 C++和 Kotlin。与使用 Java 相比,每种语言都有各种优缺点。学* Java 将是其他语言的很好入门,并且 Java 也是安卓的官方语言之一。目前 Play 商店上大多数热门应用和游戏都是用 Java 编写的。

您还可以看到最低 SDK选项。将其保留为默认设置,但请注意,写作时默认设置可能与您的设置不同。

注意

我们已经知道安卓 SDK 是我们将用来开发应用程序的代码包集合。像任何一个好的 SDK 一样,安卓 SDK 定期更新,每次有重大更新时,版本号都会增加。简单来说,版本号越高,您可以使用的新功能就越多;版本号越低,我们的应用程序就能在更多设备上运行。目前,安卓果冻豆将为我们提供许多出色的功能,并且几乎 100%兼容当前使用的安卓设备。如果在阅读时,Android Studio *议使用更新的 API 和安卓版本,那就选择那个吧。

如果您在未来的某个时候阅读本书,那么最低 SDK选项可能会默认为不同的内容,但本书中的代码仍将有效。

  1. 点击完成按钮,我们将运行应用程序,看看我们取得了什么成就。

注意

第一次创*新项目时,Android Studio 将启动另一个下载。Android Studio 将设置用于管理项目配置和部署的 Gradle 构*系统。这只会发生在第一个项目中。本书不需要了解 Gradle 的知识,但如果您感兴趣,简单的网络搜索将会揭示更多信息。

让我们看看我们的应用程序在运行中的情况。

到目前为止部署应用程序

在我们探索任何代码并学*我们的第一段 Java 代码之前,您可能会惊讶地发现我们已经可以运行我们的项目了。这只是一个相当无特色的应用程序,但由于我们将尽可能频繁地运行应用程序来检查我们的进度,现在让我们看看如何做到这一点。您有三个选择:

  • 在 PC 上的模拟器上运行应用程序(Android Studio 的一部分)处于调试模式。

  • 在 USB 调试模式下在真实的安卓设备上运行应用程序。

  • 将应用程序导出为一个完整的安卓项目,可以上传到 Play 商店。

第一个选项(调试模式)是最容易设置的,因为我们在设置 Android Studio 时已经做过了。如果您有一台性能强大的 PC,您几乎不会注意到模拟器和真实设备之间的区别。然而,屏幕触摸是由鼠标点击模拟的,对用户体验的正确测试在我们将创*的一些后续应用中是不可能的,比如绘图应用。此外,您可能更喜欢偶尔在真实设备上测试您的创作——我知道我会。

第二个选项,使用真实设备,有几个额外的步骤,但一旦设置好,就和选项 1 一样好,屏幕触摸是真实的。

最后一个选项大约需要 5 分钟(至少)来准备,然后您需要手动将创*的包放到真实设备上并在每次更改代码时安装它。

也许最好的方法是使用模拟器快速测试和调试代码的小增量,然后定期使用 USB 调试模式在真实设备上确保一切仍然如预期。只有偶尔您才需要导出一个实际可部署的包。

因此,我们现在将介绍如何使用模拟器和 USB 调试在真实设备上运行应用程序。

在安卓模拟器上运行和调试应用程序

按照以下简单步骤在默认的安卓模拟器上运行应用程序:

  1. 从安卓工作室菜单栏中,选择工具 | AVD 管理器AVD代表安卓虚拟设备(模拟器)。你会看到以下窗口:图 1.10 - AVD 管理器

图 1.10 - AVD 管理器

注意列表中有一个模拟器。在我的情况下,它是Pixel_3a_API_30_x….如果你在将来的某个时候跟着做,可能是一个默认安装的不同模拟器。这没关系。

重要提示

模拟器应该已经通过我们之前执行的步骤安装好了。我注意到在测试一个预发布版本时,它并没有默认安装。如果在你的虚拟设备屏幕上没有列出模拟器,选择工具 | AVD 管理器 | 创*虚拟设备… | 下一步 | R 下载 | 接受 | 下一步,然后将会下载并安装一个默认模拟器。安装完成后,点击完成,然后下一步,最后再次点击完成。现在你可以参考前面的步骤来运行模拟器。

  1. 点击下一张截图中显示的绿色播放图标(右侧),等待模拟器启动:图 1.11 - 播放图标

图 1.11 - 播放图标

  1. 现在,你可以点击安卓工作室快速启动栏上的播放图标,如下一张截图所示,如果提示,选择Pixel_3a_API_30_x…(或者你的模拟器叫什么)然后应用程序将在模拟器上启动:

图 1.12 - 安卓工作室快速启动栏上的播放图标

图 1.12 - 安卓工作室快速启动栏上的播放图标

你完成了。这是到目前为止应用程序的外观在模拟器中。请记住,你可能(很可能)有一个不同的模拟器 - 没关系:

图 1.13 - 到目前为止应用程序的外观

图 1.13 - 到目前为止应用程序的外观

显然,在我们搬到硅谷寻找资金支持之前,我们还有更多的工作要做,但这是一个很好的开始。屏幕上的消息是你好第一个片段片段是许多安卓应用程序的构*块,我们将在整本书中进一步探讨它们。如果你点击下一步按钮,你将看到一个新的空布局,如果你然后点击上一步按钮,你将再次看到你好第一个片段屏幕。考虑到我们还没有写任何代码,这还不错。

在开发过程中,我们需要经常测试和调试我们的应用程序,以检查是否有任何错误、崩溃或其他意外情况。

注意

我们将在下一章中看到如何从我们的应用程序中获取错误和其他调试反馈。

确保它在你想要定位的每种设备类型/尺寸上看起来好并且运行正确是很重要的。显然,我们并不拥有成千上万种安卓设备中的每一种。这就是模拟器的用武之地。

然而,模拟器有时会有点慢和繁琐,尽管最近已经有了很大的改进。如果你想要真正感受到用户体验,那么你无法击败部署到真实设备。因此,在开发我们的应用程序时,我们将希望同时使用真实设备和模拟器。

提示

如果你计划很快再次使用模拟器,那么保持它运行以避免再次等待启动。

如果你想在*板上尝试你的应用程序,你将需要一个不同的模拟器。

创*一个新的模拟器

如果你想为不同的安卓设备创*一个模拟器,这很简单。从主菜单中选择工具 | AVD 管理器。在 AVD 管理器窗口中,左键单击创*虚拟设备。现在,左键单击你想创*的设备类型,电视手机Wear OS,或者*板。现在,简单地左键单击下一步,并按照说明创*你的新 AVD。下次运行你的应用程序时,新的 AVD 将出现作为运行应用程序的选项。我们将在下一章逐步创*一个新的*板模拟器。

现在我们可以看看如何将我们的应用程序放到真实设备上。

在真实设备上运行应用程序

首先要做的事情是访问设备制造商的网站,获取并安装设备和操作系统所需的任何驱动程序。

注意

大多数新设备不需要驱动程序。所以,你可能首先想尝试以下步骤。

接下来的几个步骤将为安卓设备进行调试设置。请注意,不同的制造商对菜单选项的结构可能略有不同。但是对于大多数设备来说,启用调试的以下顺序可能非常接近,如果不是完全相同:

  1. 点击设置菜单选项或手机/*板上的设置应用。

  2. 下一步将针对不同版本的安卓略有不同。开发者选项菜单被隐藏起来,以免困扰普通用户。你必须执行一个稍微奇怪的任务来解锁菜单选项。点击关于设备关于手机选项。找到构*号选项,并重复点击,直到你收到一条消息,告诉你你现在是开发者了!

注意

一些制造商有不同和晦涩的方法来完成这一步。如果这一步不起作用,可以在网上搜索你的设备和“解锁开发者选项”。

  1. 返回设置菜单。

  2. 点击开发者选项

  3. 点击USB 调试的复选框。

  4. 将你的安卓设备连接到计算机的 USB 端口。

  5. 点击任何地方的安卓 Studio,让安卓 Studio 检测到你的设备已连接。你现在应该在你的设备上找到一个允许 USB 调试?的提示;点击允许

  6. 点击安卓 Studio 工具栏上的播放图标,如下图所示:图 1.14 - 来自安卓 Studio 工具栏的播放图标

图 1.14 - 来自安卓 Studio 工具栏的播放图标

  1. 在提示时,点击确定在你选择的设备上运行应用程序。

现在我们准备学*一些 Java,并将我们自己的 Java 代码添加到 Hello World 项目中,这将在下一章中介绍。

常见问题

  1. 那么,安卓真的不是一个操作系统,只是一个虚拟机,所有的手机和*板电脑都是真正的 Linux 机器吗?

不,安卓设备的所有不同子系统,包括 Linux、ART、库和驱动程序,共同构成了安卓操作系统。

  1. 我仍然不理解所有这些技术术语,比如 ART、面向对象、APK 等等。我应该重新阅读这一章吗?

不,这并不是必要的,因为我们只是需要介绍这个行话,随着书的进展,我们将重新讨论它并澄清它。如果你理解了以下内容,你就可以继续阅读第二章,即《第一次接触:Java、XML 和 UI 设计师》:

我们将在安卓 Point Studio 中编写 Java 代码并创*其他资源,借助 JDK,将这些代码和资源转换为真正的安卓应用程序。

总结

到目前为止,我们已经设置了我们的安卓开发环境,创*了一个新的安卓应用,并将我们的应用部署到模拟器和真实设备上。如果你仍然有未解答的问题(你可能比章节开始时有更多的问题),不要担心,因为随着我们深入了解安卓和 Java 的世界,事情会变得更清晰。

随着章节的进展,你将*立起对所有事物如何相互关联的全面理解,成功只是一个练*和更深入了解安卓 API 的问题。

在下一章中,我们将使用可视化设计师和原始 XML 代码来编辑 UI,以及编写我们的第一个 Java 方法,并开始使用安卓 API 为我们提供的一些方法。

第二章:初次接触:Java、XML 和 UI 设计师

在这个阶段,我们已经拥有了一个可用的 Android 开发环境,并且已经构*并部署了我们的第一个应用程序。然而,很明显,从 Android Studio 生成的代码不会成为下一个畅销应用程序。我们需要探索这个自动生成的代码,以便开始理解 Android,然后学*如何在这个有用的模板上进行构*。为了达到这个目的,在本章中,我们将做以下事情:

  • 查看如何从我们的应用程序中获得技术反馈

  • 检查我们第一个应用程序的 Java 代码和 UI XML 代码

  • 让我们初次体验使用 Android Studio 的用户界面UI)设计师。

  • 学*一些核心的 Java 基础知识以及它们与 Android 的关系

  • 编写我们的第一个 Java 代码

首先,让我们看看如何从我们的应用程序中获得反馈。

技术要求

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

检查 logcat 输出

在上一章中,我们提到我们的应用程序在模拟器或真实设备上以调试模式运行,因此当出现问题时,我们可以监视它并获得反馈。那么,所有这些反馈在哪里呢?

您可能已经注意到在 Android Studio 窗口底部有一大堆滚动文本。如果没有,请单击Logcat选项卡,如下图中标记为1的突出显示区域所示:

注意

模拟器必须在运行中,或者必须连接一个真实设备以调试模式运行,才能看到以下窗口。此外,如果由于某种原因重新启动了 Android Studio,并且自重新启动以来尚未执行应用程序,则Logcat窗口将为空。请参考第一章,在模拟器或真实设备上运行应用程序。

图 2.1 - Logcat 标签

图 2.1 - Logcat 标签

如果您想要看到更多内容,可以将窗口拖动得更高,就像您可以在大多数其他 Windows 应用程序中一样。

这个窗口被称为logcat,有时也被称为控制台。这是我们的应用程序告诉我们用户看不到的事情发生了什么的方式。如果应用程序崩溃或出现错误,原因或原因的线索将出现在这里。如果我们需要输出调试信息,我们也可以在这里做。

注意

我们正在构*的应用程序在这个阶段不应该有任何问题,但是在将来,如果您无法弄清楚您的应用程序为什么崩溃,从 logcat 中复制并粘贴一些文本到 Google 中通常会揭示原因。

过滤 logcat 输出

您可能已经注意到,logcat 的大部分内容,如果不是全部,几乎是难以理解的。没关系。目前,我们只对错误感兴趣,这些错误将以红色突出显示,以及我们将在下一步中学*的调试信息。因此,为了在 logcat 窗口中看到更少不需要的文本,我们可以打开一些过滤器以使事情更清晰。

在上一张图中,我突出显示了另外两个区域,标记为23。区域2是一个下拉列表,用于控制第一个过滤器。现在左键单击它,并将其从Verbose更改为Info。我们已经大大减少了文本输出。当我们对应用程序进行了一些更改并重新部署后,我们将看到这对我们有多有用。在我们探索了构成我们项目的代码和资产之后,我们将这样做。此外,请在标记为3的区域中仔细检查,看看是否写着仅显示所选应用程序。如果没有,现在左键单击它并将其更改为仅显示所选应用程序

现在我们可以看看 Android Studio 自动生成的内容,然后开始更改和添加代码,以使其个性化超出我们从项目创*阶段获得的内容。

探索项目 Java 和主布局 XML

我们将查看包含定义我们简单 UI 布局的代码和包含我们 Java 代码的资源文件。在这个阶段,我们不会试图理解所有这些,因为在理解之前我们需要学*更多的基础知识。然而,我们将看到这两个文件的基本内容和结构,以便我们可以将它们的内容与我们已经了解的 Android 资源和 Java 相协调。

检查 MainActivity.java 文件

首先让我们看看 Java 代码。如果出于某种原因,这些代码目前不可见,您可以通过左键单击MainActivity.java标签来查看这些代码,如下图所示:

图 2.2 - MainActivity.java 标签

图 2.2 - MainActivity.java 标签

由于我们不会深入研究代码的细节,带注释的截图比以文本形式重现实际代码更有用。在阅读本节时,请经常参考下一张图:

图 2.3 - Java 代码

图 2.3 - Java 代码

首先要注意的是,我在代码中添加了一些空行,以便稍微间隔一下,并呈现更清晰的图像。

在 Android Studio 中折叠(隐藏)代码

现在看看图的左侧,标有多个部分的9。这指向编辑器中所有小的+和-按钮,这些按钮可以折叠和展开代码的部分。我确实折叠了一些代码的部分,而其他部分我留了出来。因此,您屏幕上看到的内容与您在图中看到的内容略有不同。在 Android Studio 中,尝试一段时间使用+和-按钮来练*隐藏和显示代码的部分。您可能希望使您的屏幕看起来像图中的样子,但这并不是继续的要求。像这样隐藏代码的技术术语是折叠

包声明

部分package。每个 Java 文件顶部都会有一个包声明。

导入类

部分import。在import后面,我们可以看到有各种点分隔的单词。每行的最后一个单词是该行导入到我们项目中的类的名称,而每行中较早的单词是包含这些类的包和子包。

例如,下一行从androidx.appcompat.app包和子包中导入AppCompatActivity类:

import androidx.appcompat.app.AppCompatActivity;

注意

行尾的分号告诉编译器这是代码行的结束。

这意味着在这个文件中,我们将可以访问这些类。实际上,正是这些类被自动生成的代码用来制作我们在上一章中看到的简单应用程序。

在本章中,我们不会讨论所有这些类。现在重要的是我们可以导入这些类的概念。请注意,我们可以随时从任何包中添加额外的类,我们将在不久的将来改进我们的应用程序时这样做。

这个类

我们的代码的第 3部分称为类声明。以下是完整的一行;我已经突出显示了其中的一部分:

public class MainActivity extends AppCompatActivity {

类声明是一个类的开始。请注意高亮显示的部分,MainActivity。这是 Android Studio 在我们创*项目时给出的类名,也是我们之前讨论过的 Java 类的MainActivity.java文件名相同。

类和文件可以重命名,但由于这是我们应用程序的关键/主要活动,MainActivity似乎是合适的。extends关键字意味着我们的名为MainActivity的类将是AppCompatActivity类型。

我们可以使用一些类而不需要这个extends部分。我们在这里使用extends是因为我们想要使用AppCompatActivity类中的所有代码,并且还要添加我们自己的代码。所以,我们扩展它。所有这些以及更多内容将在第十章**,面向对象编程中变得更加清晰。

最后,对于{部分。现在看图的底部}部分表示类的结束。在MainActivity类的左花括号和右花括号之间的所有内容都是MainActivity类的一部分。

类内的方法

现在看一下代码的第五部分。这是完整的代码行,其中我们讨论的关键部分已经突出显示:

protected void onCreate(Bundle savedInstanceState) {

这是一个onCreate方法,是方法的名称。我们通过使用方法的名称使方法执行其代码。当我们这样做时,我们说我们正在调用一个方法。

尽管我们现在不关心方法名称两侧代码的细节,但你可能已经注意到Bundle,这是我们在import行导入的类之一,如果没有导入Bundle,Android Studio 将不知道Bundle是什么,它将无法使用,并且会以红色下划线表示为错误。

然后我们的代码将无法编译和运行。请注意前一行代码中的最后一件事是一个左花括号{。这表示onCreate方法中包含的代码的开始。现在跳到}部分。你可能已经猜到这是方法的结束。在onCreate方法的左花括号和右花括号之间的所有内容都是在调用该方法时执行的代码。

我们现在不需要深入了解这段代码的作用,但是总体上,它通过引用一个资源文件来设置应用程序的外观/布局,这个资源文件是在我们创*项目时由 Android Studio 自动生成的。我在前面的图中用轮廓标出了资源文件。

onCreateOptionsMenuonOptionsItemSelected部分。

我们已经了解了足够的 Java 代码,可以取得一些进展。我们将在本章后面再次看到这段代码并进行更改。

到目前为止 Java 代码的总结

事实上,在我们刚刚概述的代码中包含了一些复杂的语法。然而,我们正在*立对这段代码的足够了解,以便开始快速学* Java 和 Android,而不必先学*数百页的 Java 理论。到本书结束时,所有的代码都会有意义,但为了现在快速进展,我们只需要接受一些细节将在更长一段时间内保持神秘。

检查应用程序布局文件

现在我们将只看其中一个.xml文件。在整本书的过程中,我们将遇到几种不同的布局文件,但让我们从最容易识别的一个开始,它决定了我们应用程序的大部分外观。

在项目资源管理器窗口中,左键单击fragment_first.xml文件。文件的 XML 代码内容现在显示在 Android Studio 的主窗口中。

我们可以忽略res generated文件夹。

我们很快会探索这个 XML 代码,但首先找到并左键单击Design按钮(如下所示)切换到设计视图:

图 2.4 - 打开设计视图

图 2.4 - 打开设计视图

现在我们可以看到设计视图,它显示了当应用程序在模拟器中运行时 XML 代码将导致显示的内容:

图 2.5 - 应用程序显示

图 2.5 - 应用程序显示

前面的图应该看起来很熟悉,因为它显示了我们在上一章末运行的第一个应用程序的布局 - 与fragment_first.xml文件位于同一文件夹中的fragment_second.xml文件,你会看到我们在上一章中看到的第二个布局,上面有Previous按钮。实际上,与我们最初预期的相比,与布局相关的文件甚至更多,但我们将在本章和下一章中讨论它们。

本书中我们设计应用程序时所做的大部分工作都是在设计视图中完成的。然而,了解幕后发生的事情是很重要的。

设计视图是fragment_first.xml文件中包含的 XML 代码的图形表示。点击Design标签旁边的Code标签(在上一张图中)可以看到构成布局的 XML 代码。我已经注释了 XML 文本的屏幕截图,所以我们可以接下来讨论它:

图 2.6 - XML 文本的屏幕截图

图 2.6 - XML 文本的屏幕截图

首先要注意的是,这个文件并不代表整个布局。但它确实代表了大部分的表面积以及整个Hello first fragment消息和Next按钮。此外,在左侧,我们可以看到现在熟悉的+和-图标,以便我们可以折叠和展开代码的部分。

UI 布局元素

如果我们首先看一下标记为…ConstraintLayout...的代码部分。现在,ConstraintLayout是一个用来包裹 UI 其他部分的 UI 元素。

在 Android 中,当我们向 UI 添加一个新元素时,我们总是以<开头,后面跟着元素的名称。

紧随那条看起来相当长而繁琐的代码的是定义了layout_widthlayout_height。所有这些属性定义了ConstraintLayout元素在用户屏幕上的显示方式。ConstraintLayout元素的属性在第一个标记为1b>结束。

如果我们看一下我们的 XML 截图底部,我们会看到一些标记为</…ConstraintLayout>的代码,标志着ConstraintLayout元素的结束。在元素的属性的结束></…ConstraintLayout>之间的任何内容都被视为元素的子元素。因此,我们可以看到我们的ConstraintLayout有/包含两个子元素。现在让我们来看看这些子元素。

UI 文本元素

利用我们刚学到的知识,我们可以说 UI 元素从位置<开始,其名称为<TextView...。如果我们进一步查看我们的TextView元素,我们会发现它有几个属性。它有一个text属性,设置为"Hello first fragment"。当然,这就是我们的应用程序向用户显示的确切文本。它还有layout_widthlayout_height属性,都设置为"wrap_content"。这告诉TextView它可以占用所需的内容空间。正如我们将在整本书中看到的,对于这个和其他 UI 元素,还有许多其他属性可用。TextView中的最后一个属性是id,我们将在下一节中看到我们和 Android 如何使用id属性来改进这个第一个应用程序。

注意到/>的代码。这标志着TextView元素的结束。这与ConstraintLayout元素的结束方式略有不同。当 XML 中的元素没有子元素时,我们可以像这样结束它:/>。当元素有子元素,并且其结束位置在代码中的属性定义之后时,通过重复其名称来结束元素会更清晰,像这样:</…ConstraintLayout>

注意

你可能会想知道为什么TextView的元素名称清晰简洁(只是TextView),而ConstraintView的完整名称前面有复杂的表面混乱(androidx.constraintlayout.widget.ConstraintLayout)。这个ConstraintLayout元素是一个特殊的布局,用来确保我们的应用程序与较旧版本的 Android 兼容。正如我们将在一分钟内看到的,当我们向应用程序添加按钮时,大多数元素都有简单而简洁的名称。

UI 按钮元素

现在我们应该能够快速识别从<开始的代码及其名称:<Button...。如果我们进一步查看我们的Button,我们会发现它有几个属性。它有一个文本属性,设置为"Next"。当然,这是用户可以点击的按钮上显示的确切文本。它还有layout_widthlayout_height属性,都设置为"wrap_content"。这与TextView元素一样,使得屏幕上的按钮占据所需的内容空间。Button中的最后一个属性是id,对于按钮来说,这通常是一个至关重要的属性,甚至比 UI 的其他部分更重要。由于id属性可以将此按钮与其他按钮区分开,我们可以根据id属性中保存的值为不同的按钮编写不同的功能。我们很快就会看到这个原则在实际中的应用。

注意/>处的代码。正如我们所知,这标志着Button元素的结束。

我们将在下一节中编辑并添加到这个 XML 代码,并了解更多关于属性的知识。

注意

布局的元素通常被称为小部件

将按钮添加到主布局文件

在这里,我们将在屏幕上添加一对按钮小部件,然后我们将看到一种快速的方法来使它们真正做些什么。我们将以两种不同的方式添加按钮,首先使用可视化设计师,其次是通过直接添加和编辑 XML 代码。

通过可视化设计师添加按钮

要开始添加我们的第一个按钮,打开fragment_first.xml文件编辑器,并通过单击Design选项卡(如下所示)切换回设计视图:

图 2.7 – 设计选项卡

图 2.7 – 设计选项卡

请注意布局左侧有一个名为Palette的窗口,如下所示:

图 2.8 – 调色板窗口

图 2.8 – 调色板窗口

调色板分为两部分。左侧列表显示了 UI 元素的类别,并允许您选择一个类别,右侧显示了当前选定类别中所有可用的 UI 元素。

确保选择Common类别,如前图所示。现在,左键单击并按住Button小部件,然后将其拖放到布局的顶部中间附近。

如果不是很准确也没关系。然而,练*做对是很好的。因此,如果您对按钮的位置不满意,可以在布局上左键单击它,然后在键盘上按Delete键将其删除。现在您可以重复上一步,直到您有一个新的整齐放置的按钮,您对此感到满意,如下所示:

图 2.9 – 更新布局

图 2.9 – 更新布局

此时,我们可以在模拟器或真实设备上运行应用程序,按钮会出现 - 有点。如果我们点击它,甚至会有一个简单的动画来表示按钮被按下和释放。如果您愿意,现在可以自由尝试一下。如果这样做,您会注意到按钮的位置不如您所期望的那样:

图 2.10 – 按钮位置不正确

图 2.10 – 按钮位置不正确

现在不要担心这个明显的异常;我们将在接下来的几节中进行研究。

接下来,我们将编辑按钮的属性在Attributes窗口中。

编辑按钮的属性

通过左键单击按钮来确保按钮被选中。现在找到编辑窗口右侧的Attributes窗口,如下所示:

图 2.11 – 属性窗口

图 2.11 – 属性窗口

在前一个图中,你可以看到我们可以访问当前选定的 UI 元素的各种属性。要显示更多属性,我们点击不同类别的属性,并使用右侧的滚动条滚动。如果它们默认情况下尚未打开,请左键单击常用属性所有属性部分的箭头以显示它们的选项。

现在你可以看到按钮的全部细节,我们可以开始编辑它。一个简单的按钮具有如此多的属性可能会让人感到惊讶。这表明了 Android API 为 UI 操作提供的多功能性和强大性。

正如你所看到的,我们可以在 UI 设计师中编辑大量不同的属性。在第十三章**,匿名类-让 Android 小部件活起来中,我们还将使用我们的 Java 代码编辑和操作这些属性。

现在,我们将只编辑一个属性。滚动属性窗口,直到在常用属性部分看到onClick属性,然后左键单击它以进行编辑,如下所示:

图 2.12-常用属性部分

图 2.12-常用属性部分

注意

如果你在查找属性时遇到困难,你可以在所有属性部分找到它,属性按字母顺序排列。因此,onClick属性也可以在所有属性部分的冗长列表的下面三分之二处找到。

t处键入topClick,并大写C

当你完成时,属性窗口将如下所示:

图 2.13-onClick 选项

图 2.13-onClick 选项

我们在这里做的是,在我们的代码中命名我们希望在用户点击此按钮时调用的 Java 方法。名称是任意的,但由于此按钮位于屏幕顶部,名称似乎有意义且易于记忆。我们使用的奇怪大小写是一种约定,将帮助我们保持代码清晰易读。随着代码变得越来越长和复杂,我们将看到这种约定的好处。

当然,topClick方法还不存在。Android Studio 非常有帮助,但有些事情我们需要自己做。在我们向 UI 添加另一个按钮之后,我们将使用 Java 代码编写此方法。此时你可以运行应用程序,它仍然可以工作。但是如果你点击按钮,它将崩溃,并且会出现错误,因为该方法不存在。Android Studio 通过用红色轮廓勾画onClick属性来预警我们即将发生的崩溃,如前图所示。如果你将鼠标悬停在这个红色轮廓上,你将看到问题的细节:对应的方法处理程序...未找到

检查新按钮的 XML 代码

在为此项目添加最后一个按钮之前。点击Code选项卡,切换回查看制作 UI 的 XML 代码。

请注意,我们之前检查过的 XML 中有一个新的代码块。这是新代码块的图像:

图 2.14-XML 中的新代码块

图 2.14-XML 中的新代码块

还要注意以下细节,这些细节应该与我们对 XML 和 Android UI 元素的了解相对应:

  • 新代码以<Button开头,以/>结尾。

  • 代码具有一系列属性,定义了按钮,包括layoutWidthlayoutHeight

  • 代码包括我们刚刚添加的onClick属性,其值为"topClick"

  • onClick属性的topClick值被下划线标记为红色,显示缺少方法错误。

  • 表示按钮的代码的开始和结束都包含在ConstraintLayout元素中。

在设计视图中,你可以将鼠标悬停在红色下划线的topClick代码上,以显示问题的详细信息:找不到对应的方法处理程序...

注意

在撰写本书期间,Android Studio 更新了它显示 XML 中错误的方式。目前,它用红色突出显示错误,而不是像图中和描述中所示的红色下划线。下划线在黑白打印中更清晰,所以它们被保留了。

我们可以看到问题是 Android Studio 期望在我们的 Java 代码中实现一个名为topClick的方法。一旦我们添加了第二个按钮,我们就会这样做。

通过编辑 XML 代码添加按钮

仅仅为了多样性和证明我们可以,我们现在将添加另一个按钮,只使用 XML 代码,而不是 UI 设计工具。大多数时候,我们会使用 UI 设计工具,但这个快速练*应该巩固你对 UI 设计工具和底层 XML 代码之间关系的理解。

我们将通过复制和粘贴现有按钮的代码来实现这一点。然后我们将对粘贴的代码进行一些小的编辑。

在按钮代码开始的<Button之前左键单击。注意代码的开始和结束现在有了轻微的高亮:

图 2.15 - 按钮代码

图 2.15 - 按钮代码

这已经确定了我们要复制的代码部分。现在左键单击并拖动以选择所有按钮代码,包括高亮显示的开始和结束,如下图所示:

图 2.16 - 选择所有按钮代码

图 2.16 - 选择所有按钮代码

按下Ctrl + C键盘组合键复制高亮显示的文本。将键盘光标放在现有按钮代码下方,并按Enter键几次留下一些空行。

按下Ctrl + V键盘组合键粘贴按钮代码。此时,我们有两个按钮。然而,有一些问题:

图 2.17 - 附加错误

图 2.17 - 附加错误

我们在代表我们的按钮的两个代码块中都有一个额外的错误。id属性(在两个代码块中)被用红色下划线标出。这个错误的原因是两个按钮都有相同的id属性。id属性应该区分一个 UI 元素和所有其他 UI 元素。让我们来修复它。

给按钮分配唯一的 id 属性

我们可以通过调用第二个按钮button2来解决问题,但更有意义的是改变它们两个。编辑第一个按钮的代码,给它一个 ID 为buttonTop。要做到这一点,找到以下代码行(在第一个按钮中):

android:id="@+id/button"

将其更改为:

android:id="@+id/buttonTop"

注意

注意button中的小写bTop中的大写T

现在在第二个按钮中找到这行代码:

android:id="@+id/button"

将其更改为:

android:id="@+id/buttonBottom"

id属性行上的错误已经消失。此时,你可能会认为我们可以继续解决我们缺少的方法问题了。

然而,如果你运行应用程序并快速浏览一下,你会发现我们似乎只有一个按钮。不仅如此,(如前所述)按钮的位置也不是我们期望的:

图 2.18 - 单个按钮

图 2.18 - 单个按钮

原因是我们没有明确地定位它们,所以它们默认为左上角。我们在设计选项卡上看到的位置只是设计时的位置。让我们现在改变它。

在布局中定位这两个按钮

我们只能看到一个按钮的原因是两个按钮都在同一个位置。第二个按钮正好覆盖了第一个按钮。即使在设计选项卡中(随时可以查看),按钮仍然叠在一起,尽管它们位于屏幕中间。

注意

您可能想知道为什么 UI 布局工具以这种显然反直觉的方式设计。原因是灵活性。正如我们将在接下来的两章中看到的,不仅可以在设计时以不同的方式定位 UI 元素,而且还有许多不同的布局方案供应用设计者(也就是您)选择以适应他们的计划。这种灵活性在学* Android 时会有些尴尬,但一旦您克服了这种尴尬,就会获得很大的设计能力。不要担心:我们会一步一步地进行,直到您掌握了这个技巧。

我们将让 Android Studio 自动为我们解决问题,首先通过添加到我们的代码,然后使用 UI 设计工具。首先,让我们正确设置设计时布局。在第二个按钮的代码中,找到这行代码:

tools:layout_editor_absoluteY="30dp" />

将其编辑为与此相同:

tools:layout_editor_absoluteY="100dp" />

这个微妙的变化会使第二个按钮下移一点,但只在设计时。如果您在设计选项卡中查看,按钮将整齐地定位在第一个按钮的下方,但如果您在模拟器上运行应用程序,它们仍然都位于左上角并且彼此重叠。

注意

很可能,甚至是肯定的,您的布局中dp的精确测量值会与书中所示的略有不同。只要第二个按钮的layout_editor_absoluteY属性比第一个大约70dp,那么一切都会整洁有序。在代码设计选项卡之间切换时,随意调整这两个按钮上的此属性,直到按钮被定位到您喜欢的位置。

当您对按钮的位置满意时,切换到设计选项卡,找到推断约束按钮,如下所示:

图 2.19 - 推断约束按钮

图 2.19 - 推断约束按钮

点击推断约束按钮。Android Studio 将编辑 XML。让我们简要看一下幕后发生了什么。从两个按钮的末尾,以下代码行被移除。

如果约束没有被应用,点击清除所有约束按钮,它位于推断约束的左侧;有时候 Android Studio 会混淆并需要重置现有的约束才能推断其余的:

tools:layout_editor_absoluteX="147dp"
tools:layout_editor_absoluteY="30dp" />

这两行代码是水*(…absoluteX)和垂直(…absoluteY)定位按钮的位置。

Android Studio 还为第一个按钮添加了四行代码,为第二个按钮添加了三行。以下是在第一个按钮开头附近添加的代码:

android:layout_marginTop="30dp"

这段代码导致按钮在顶部有 30 的边距。但是相对于什么来说的顶部?看看添加到第一个按钮末尾的这三行代码:

app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

注意layout_constraintEnd_toEndOflayout_constraintStart_toStartOflayout_constraintTop_toTopOf的新属性。分配给这些属性的值是"parent"。这会导致第一个按钮相对于parent UI 元素定位。父元素是包含布局:ConstraintLayout元素。

现在看看添加到第二(底部)按钮的三行代码。

在代码的开头附近,我们看到了这个:

android:layout_marginTop="22dp"

在第二个按钮的代码末尾,我们看到了这两行额外的代码:

app:layout_constraintStart_toStartOf="@+id/buttonTop"
app:layout_constraintTop_toBottomOf="@+id/buttonTop" />

这意味着第二个按钮相对于buttonTop小部件有 22 的边距。

注意

dp代码是一个测量/距离单位,将在*第五章**中更深入地讨论,使用 CardView 和 ScrollView 创*美丽的布局。dp测量的精确值可能会在您的布局上略有不同。

现在运行应用程序,您会看到我们有两个不同的按钮。一个具有buttonTopid属性,它位于另一个按钮的上方,具有buttonBottomid属性:

图 2.20 - 两个按钮选项

图 2.20 - 两个按钮选项

显然,布局还有更多内容,但到目前为止,我已经提到了其中一种我们可以设计应用程序 UI 的选项。我们将更仔细地研究ConstraintLayout布局元素,以及在第四章**,开始使用布局和 Material Design中探索更多的布局选项。

我们想在我们的 XML 代码中再做一些更改。

使按钮调用不同的方法

切换回到buttonBottom按钮:

android:onClick="topClick"

编辑代码如下:

android:onClick="bottomClick"

现在我们有两个按钮,一个在另一个上面。顶部按钮具有buttonTopid属性和值为topClickonClick属性。另一个具有buttonBottomid属性和值为bottomClickonClick属性。

现在,这些最后的 XML 代码更改意味着我们需要在我们的 Java 代码中编写两个方法(topClickbottomClick)。

注意

当点击两个按钮调用相同的方法是可以的;这不是语法错误。然而,大多数按钮确实有不同的目的,因此如果我们的按钮执行不同的操作,这个练*将更有意义。

我们很快就会做到这一点,但在我们这样做之前,让我们更多地了解一下 Java 注释,并查看一些我们可以编写的 Java 代码来发送消息。我们将学会向用户发送消息以保持他们的知情和向自己发送消息以进行调试。

在我们的 Java 代码中留下注释

在编程中,写笔记,即代码注释,并在代码中大量使用它们总是一个聪明的主意。这是为了提醒我们在编写代码时的想法。要做到这一点,您只需附加双斜杠,然后输入您的注释,如下所示:

// This is a comment and it could be useful

此外,我们可以使用注释来注释掉一行代码。假设我们有一行代码,我们暂时想要禁用它。我们可以通过添加两个斜杠来实现,就像这样:

// The code below used to send a message
// Log.i("info","our message here");
// But now it doesn't do anything
// And I am getting ahead of where I should be

注意

使用注释注释掉代码应该只是一个临时措施。一旦找到正确的代码使用,注释掉的代码应该被删除,以保持代码文件的清洁和有组织。

让我们看看在 Android 中发送消息的两种不同方式,然后我们可以编写一些方法,当我们的新 UI 按钮被按下时发送消息。

编写消息给用户和开发人员

在本章的介绍和上一章中,我们谈到了使用其他人的代码,特别是通过 Android API 的类和它们的方法。我们看到我们可以用微不足道的代码做一些相当复杂的事情(比如与卫星通信)。

让我们开始,我们将使用 Android API 中的两个不同类来输出消息。第一个类Log允许我们将消息输出到 Logcat 窗口。第二个类Toast不是一种美味的早餐,而是会为我们的应用用户产生一个类似吐司的弹出消息。

以下是我们需要编写的代码,以向 Logcat 窗口发送消息:

Log.i("info","our message here");

为什么这样能够工作将在第十章**,面向对象编程中变得更加清晰,但现在,我们只需要知道放在两组引号之间的任何内容都将输出到 Logcat 窗口。我们很快将看到在哪里编写这种类型的代码。

以下是我们需要编写的代码,以向用户屏幕发送消息:

Toast.makeText(this, "our message",      
Toast.LENGTH_SHORT).show();

这是一行非常复杂的代码,它的工作原理将在第十章**,面向对象编程中变得更加清晰。这里重要的是,我们放在引号中的任何内容都将出现在我们的用户的弹出消息中。

让我们把一些代码,就像我们刚刚看到的那样,放到我们的应用程序中。

编写我们的第一个 Java 代码

所以,我们现在知道了将输出到 Logcat 或用户屏幕的代码。但是在哪里写这段代码呢?要回答这个问题,我们需要理解 MainActivity.java 中的 onCreate 方法在应用准备展示给用户时执行。因此,如果我们将我们的代码放在这个方法的末尾,它将在用户看到应用时执行。听起来不错。

注意

我们知道要执行方法中的代码,我们需要调用它。我们已经将我们的按钮连接起来调用了一些方法:topClickbottomClick。很快我们将编写这些方法。但是谁或什么在调用 onCreate 呢?这个谜团的答案是,Android 操作系统本身调用 onCreate。当用户点击应用图标运行应用时,它会这样做。在第六章**,Android 生命周期中,我们将更深入地研究这一现象,清楚地了解代码何时执行。你现在不需要完全理解这一点。我只是想给你一个概述。

让我们快速尝试一下。在 Android Studio 中切换到 MainActivity.java 标签。

我们知道 onCreate 方法在应用启动之前被调用。让我们将一些代码复制粘贴到我们应用的 onCreate 方法中,看看当我们运行它时会发生什么。

将消息代码添加到 onCreate 方法

找到 onCreate 方法的结束大括号 },并添加下面显示的高亮代码。在代码中,我没有显示 onCreate 方法的完整内容,而是使用 表示一些未显示的代码行。重要的是将新代码(完整显示)放在最后,但在那个结束大括号 } 之前:

@Override
protected void onCreate(Bundle savedInstanceState) {
…
…
…
// Your code goes here
Toast.makeText(this, "Can you see me?", 
                    Toast.LENGTH_SHORT).show();

     Log.i("info", "Done creating the app");
}

注意到 Android Studio 中两个 ToastLog 的实例被标记为红色。它们是错误。我们知道 ToastLog 是类,而类是代码的容器。

问题在于,Android Studio 在我们告诉它之前并不知道它们。我们必须为每个类添加一个 import。幸运的是,这是半自动的。

onCreate 方法中左键单击红色的 Toast 代码。现在按住 Alt 键,然后点击 Enter。在提示时,选择 Log。Android Studio 将在代码顶部添加 import 指令,与我们的其他导入一起,错误消失了。

注意

Alt + Enter 只是众多有用的键盘快捷键之一。以下链接是 Android Studio 的键盘快捷键参考。更具体地说,它是基于 IntelliJ Idea IDE 的。查看并收藏这个网页;在本书的过程中它将非常有价值:www.jetbrains.com/idea/docs/IntelliJIDEA_ReferenceCard.pdf

滚动到 MainActivity.java 的顶部,查看添加的 import 指令。这里是为了方便你的:

import android.util.Log;
import android.widget.Toast;

以通常的方式运行应用程序,并查看 Logcat 窗口中的输出。

检查输出

下一张图显示了 Logcat 窗口中输出的屏幕截图:

图 2.21 – Logcat 窗口中的输出

图 2.21 – Logcat 窗口中的输出

查看 Logcat 窗口,你可以看到我们的消息 Done creating the app 被输出了,尽管它混在我们目前不感兴趣的其他系统消息中。当应用首次启动时,观察模拟器,你也会看到用户将看到的漂亮的弹出消息:

图 2.22 – 弹出消息

图 2.22 – 弹出消息

你可能会想知道为什么消息在那个时候被输出。答案是 onCreate 方法在应用开始响应用户之前被调用。因此,这是 Android 开发人员常见的做法,将代码放在这个方法中,为他们的应用做好准备。

现在我们将进一步编写我们自己的方法,这些方法将由 UI 中的两个按钮调用。我们将在这些新方法中放置类似的LogToast消息。

编写我们自己的 Java 方法

让我们直接开始编写我们的第一个 Java 方法,其中包含更多的LogToast消息。

注意

现在是一个好时机,如果你还没有的话,可以获取包含所有代码文件的下载包。您可以查看每个章节的完成代码。例如,本章的完成代码可以在第二章文件夹中找到。我进一步将第二章文件夹细分为javares文件夹(用于 Java 和资源文件)。在有多个项目的章节中,我将进一步划分文件夹以包含项目名称。您应该在文本编辑器中查看这些文件。我的最爱是 Notepad++,可以从notepad-plus-plus.org/download/免费下载。在文本编辑器中查看的代码比直接从书中查看更容易阅读,尤其是*装版本,尤其是代码行很长的情况下。文本编辑器还是选择代码部分复制粘贴到 Android Studio 中的好方法。您可以在 Android Studio 中打开代码,但那样您就有可能将我的代码与 Android Studio 的自动生成代码混淆。

找到MainActivity类的结束大括号}

注意

您要寻找整个类的结束,而不是前一节中onCreate方法的结束。花些时间来识别新代码以及它在现有代码中的位置。

在该大括号内,输入以下突出显示的代码。

@Override
protected void onCreate(Bundle savedInstanceState) {
…
…
…
…
}
…
…
…
public void topClick(View v){
Toast.makeText(this, "Top button clicked", 
                            Toast.LENGTH_SHORT).show();
Log.i("info","The user clicked the top 
                   button");
}
public void bottomClick(View v){
Toast.makeText(this, "Bottom button clicked", 
                            Toast.LENGTH_SHORT).show();
Log.i("info","The user clicked the bottom 
                   button");
}
} // This is the end of the class

注意到View这两个词可能是红色的,表示有错误。只需使用Alt + Enter键组合导入View类并删除错误。

注意

我之所以说这里“可能”会有错误,是因为这取决于您输入代码的方式。如果您复制并粘贴了代码,那么 Android Studio 可能会自动添加View类导入代码。如果您输入了新代码,那么错误将出现,并且您需要使用Alt + Enter键解决方案。这只是 Android Studio 的一个怪癖。

以通常的方式将应用程序部署到真实设备或模拟器,并开始点击按钮,以便我们观察输出。

检查输出

最后,我们的应用程序在我们告诉它执行操作时确实执行了我们告诉它执行的操作。我们可以看到,我们在按钮的onClick属性中定义的方法名称确实在点击按钮时被调用,并且适当的消息被添加到Toast消息中显示给用户。

诚然,我们仍然不明白ToastLog类是如何工作的,我们也不完全理解我们方法语法中的public void(View v)部分(或者自动生成的代码的其他部分)。随着我们的进展,这将变得更清晰。如前所述,在第十章**,面向对象编程中,我们将深入探讨类的世界,在第九章**,学* Java 方法中,我们将掌握与方法相关的其余语法。

像以前一样检查onCreate方法,以及我们自己编写的两个方法,每次点击按钮时。在下图中,您可以看到我点击了每个按钮三次:

图 2.23 - Logcat 窗口输出

图 2.23 - Logcat 窗口输出

现在您已经熟悉了在哪里找到Logcat窗口,以后我将以修剪后的文本形式呈现Logcat输出,因为这样更容易阅读:

The user clicked the top button
The user clicked the top button
The user clicked the top button
The user clicked the bottom button
The user clicked the bottom button
The user clicked the bottom button

在下一个图中,您可以看到顶部按钮已被点击,并调用了topClick方法,触发了弹出的Toast消息,如下所示:

图 2.24 – 弹出的 Toast 消息

图 2.24 – 弹出的 Toast 消息

在整本书中,我们将定期输出到Logcat窗口,这样我们就可以看到应用程序界面背后发生了什么。Toast 消息更多用于通知用户发生了某些事情。这可能是下载完成了,收到了新的电子邮件,或者用户可能想要被告知的其他事件。

常见问题

  1. 你能提醒我方法是什么吗?

方法是我们的代码的容器,可以从代码的其他部分执行(调用)。方法包含在一个类中。

  1. 像第一章一样,我觉得这一章很难。我需要重新阅读吗?

不,如果你成功构*了应用程序,你已经取得了足够的进步来处理下一章。你知识中的所有空白将会逐渐填补,并在书籍的进展中被光荣的领悟时刻所取代。

总结

在这一章中,我们取得了很多成就。的确,很多 XML 代码仍然晦涩难懂。没关系,因为在接下来的两章中,我们将真正掌握可视化设计工具,并更多地了解 XML,尽管我们的最终目标是尽量少使用 XML。

我们看到,当我们将按钮拖放到设计中时,XML 代码会为我们生成。此外,如果我们在属性窗口中更改属性,那么 XML 代码也会被编辑。此外,我们还看到我们可以直接在代码选项卡中输入(或者在我们的情况下,复制和粘贴)XML 代码,以在我们的 UI 上创*新按钮或编辑现有按钮。

我们不仅看到了我们的第一个 Java 代码,还写了注释来帮助我们记录我们的代码,并且甚至添加了自己的方法来将调试消息输出到 Logcat 窗口和弹出Toast消息给用户。

在下一章中,我们将全面介绍 Android Studio,确切地了解不同的事情是如何同时完成的,同时了解我们项目的资产,如文件和文件夹的结构以及我们如何管理它们。这将为我们准备好深入研究 UI 设计,包括第四章**,开始使用布局和 Material Design,以及第五章**,使用 CardView 和 ScrollView 创*美丽的布局,届时我们将为我们的应用程序构*一些重要的真实布局。

第三章:探索 Android Studio 和项目结构

在本章中,我们将创*并运行另外两个 Android 项目。这些练*的目的是更深入地探索 Android Studio 和 Android 项目的结构。

当我们构*我们的应用程序准备部署时,代码和资源文件需要打包到一个Android PackageAPK)文件中。因此,所有布局文件(以及我们很快会发现的其他资源)都需要在它们正确的结构中。

幸运的是,当我们从模板创*项目时,Android Studio 会为我们处理这个问题。但是,我们仍然需要知道如何找到和修改这些文件,如何添加我们自己的(有时删除)由 Android Studio 创*的文件,以及资源文件如何相互链接,有时与彼此相互链接,有时与 Java 代码(自动生成和我们自己的)相互链接。

除了了解我们项目的组成,确保我们充分利用模拟器也是有益的。

注意

模拟器在您想要确保您的应用程序在您不拥有的硬件上运行时特别有用。此外,了解一些最新功能(正如我们将在本书中了解的那样)通常需要最新的手机,模拟器是一种经济有效的方式,可以跟随所有小应用程序而不必购买最新的手机。

在本章中,我们将研究以下主题:

  • 探索空活动项目模板的文件和文件夹结构

  • 探索基本活动项目模板的文件和文件夹结构

  • 查看空活动基本活动模板之间的差异

  • 探索 Android 模拟器

本章将使我们能够在下一章中构*和部署多个不同的用户界面UI)设计。

技术要求

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

项目资源管理器和项目解剖

当我们创*一个新的 Android 项目时,我们通常使用项目模板,就像我们在第一章**,开始 Android 和 Java中所做的那样。我们使用的模板决定了 Android Studio 将生成的文件的精确选择和内容。虽然所有项目之间存在很大的相似之处值得注意,但看到差异也有帮助。让我们构*两个模板项目,并检查文件、它们的内容以及它们是如何通过代码(可扩展标记语言XML)和 Java)链接在一起的。我们首先创*一个空活动项目。

探索空活动项目模板的文件和文件夹结构

具有自动生成 UI 的最简单项目类型是空活动项目模板。UI 几乎是空的,但已准备好添加。当我们创*一个项目,即使是空的 UI,Android Studio 也会自动生成显示 UI 的 Java 代码。因此,当我们将其添加到空的 UI 时,它已准备好显示。

让我们创*一个空活动项目。这几乎与第一章**,开始 Android 和 Java中的过程相同,只有一个细微的差异,我会指出。

如果您从第二章**,首次接触:Java,XML 和 UI 设计师中打开了项目,请选择文件 | 新* | 新项目…。或者,如果您在 Android Studio 欢迎屏幕上,请选择开始一个新的 Android Studio 项目。然后,按照以下步骤进行:

  1. 选择项目模板窗口上,选择空活动。这是与我们在第一章**,开始 Android 和 Java中所做的不同之处。

  2. 空活动应用中。

  3. 其余的设置可以保持默认,所以只需单击完成

Android Studio 将生成所有的代码和其他项目资源。现在,我们可以看到已经生成了什么,并将其与我们已经知道的项目资源管理器窗口中的预期相联系。

如果模拟器还没有运行,请通过选择工具 | AVD 管理器来启动它,然后在您的虚拟设备窗口中启动您的模拟器。通过单击快速启动栏中的播放按钮在模拟器上运行应用程序,就像我们之前为我们的上一个项目做了几次一样。

看看应用程序,注意它与第一个项目有些不同。它是-嗯-空的:顶部没有菜单;底部没有浮动按钮。但是,它仍然有一些文字,说Hello World!,如下面的屏幕截图所示:

图 3.1 - 你好,世界!

图 3.1 - 你好,世界!

现在我们有了一个全新的空活动项目,让我们来探索一下 Android Studio 为我们生成的文件和文件夹。

探索一个空活动项目

现在,是时候深入了解我们应用程序的文件和文件夹了。这将节省我们很多时间和困惑,以后在书中。然而,请注意,没有必要记住所有这些文件的位置,甚至更不需要理解文件中的代码。事实上,即使在书的最后,XML 的部分内容仍然是一个谜,但这不会阻止您设计、编码和发布令人惊叹的应用程序。

在下面的屏幕截图中,看看项目资源管理器窗口,就在项目创*后:

图 3.2 - 项目资源管理器窗口

图 3.2 - 项目资源管理器窗口

注意前一个屏幕截图中指示的两个箭头?你可能已经猜到,这些箭头允许我们展开appGradle Scripts文件夹。可能你的文件夹已经展开了。为什么不尝试一下箭头,多次展开和折叠它们呢?

注意

在本书的背景下,我们不需要探索Gradle Scripts文件夹。Gradle 是 Android Studio 的重要组成部分,但其作用是向用户隐藏 Android Studio 执行的相当复杂的过程,例如添加资源文件,编译和构*项目等。因此,我们不需要进一步深入研究这一点。然而,如果您决定将 Android 提升到下一个水*,那么深入了解 Gradle 及其与 Android Studio 的关系是值得投资的时间。

我们将详细探讨app文件夹。单击app文件夹旁边的箭头以展开其内容,然后我们将开始探索。第一层内容如下屏幕截图所示:

图 3.3 - 探索 app 文件夹

图 3.3 - 探索 app 文件夹

我们已经展示了另外四个文件夹:manifestsjavajava(generated)res。让我们从顶部开始查看所有四个文件夹。

注意

Packt 为其图书使用的样式指南*议使用此字体来表示文件名和文件夹名。由于我们讨论的文件和文件夹既是文件又是文件夹,并且出现在屏幕上,为了保持一致性并且更加紧凑,我选择只使用后者的字体,并且在整本书中在选择不明确的情况下都会使用这个选项。

清单文件夹

manifests文件夹里只有一个文件。展开manifests文件夹并双击AndroidManifest.xml文件。注意文件已在编辑窗口中打开,并且已添加了一个标签,这样我们就可以轻松地在这个文件和其他文件之间切换。下一个屏幕截图显示了已添加的新标签,以及manifests文件夹中AndroidManifest.xml文件中包含的 XML 代码:

图 3.4 - 添加新标签

图 3.4 - 添加新标签

我们不需要理解文件中的一切,但值得指出的是,我们将偶尔在这里进行修改 - 例如,当我们需要请求用户访问其设备功能的权限时。当我们想要为沉浸式体验制作全屏应用程序时,我们还将编辑此文件,比如在第二十一章**, 线程和启动实时绘图应用程序中开始的绘图应用程序。

请注意,文件的结构类似于我们在上一章中看到的布局文件的结构 - 例如,有明确定义的以<section name开头并以</section name>结束的部分。这样的真实例子有<application</application>,以及<activity</activity>

事实上,除了第一行之外,整个文件内容都包含在<manifest</manifest>中。

就像我们输入计算的括号一样,这些开头和结尾部分必须匹配,否则文件将在我们的项目中引起错误。Android Studio 在这些结构的前面缩进(放置制表符)以使这些部分及其深度更加清晰。

这段代码的一些特定部分值得注意,所以我会指出其中的一些行。

下一行告诉 Android,我们想要在mipmap文件夹中显示给用户的图标来启动应用程序,它被称为ic_launcher

android:icon="@mipmap/ic_launcher"

随着我们继续探索,我们将自己验证这一点。

下一行有两个值得讨论的方面。首先,它表示我们给我们的应用程序的名称;其次,该名称作为app_name包含在这里:

android:label="@string/app_name"

注意

在编程中,包括 Java 和 XML,在计算中,字符串是任何字母数字值。我们将在整本书中学到更多关于字符串的知识,从第七章开始,Java 变量、运算符和表达式。因此,我们可以猜测app_name标签的字母数字值是Empty Activity App,因为这是我们创*应用程序时称呼它的名称。

这可能听起来有点奇怪,但我们很快就会看到这个文件(及其标签),在以后的项目中,我们将向其中添加更多的标签和值。我们还将了解为什么以这种在这个阶段似乎相当复杂的方式向我们的应用程序添加文本的原因。

我们可以讨论AndroidManifest.xml文件中的每一行,但我们不需要这样做。让我们再看两行,因为它们彼此相关。下一行显示了我们的 Activity 的名称,这是在我们创*项目时 Android Studio 选择的。我在这里突出显示了 Activity 的名称,只是为了让它更加突出:

<activity android:name=".MainActivity">

接下来的这一行,出现在<activity</activity>标签内,表示它是activity的一个属性,并显示这个 Activity 是在启动应用程序时应该运行的那个。它是LAUNCHER

<category android:name="android.intent.category.LAUNCHER" />

这意味着我们的应用程序可以有多个 Activity。很多时候,如果您的应用程序有多个屏幕,比如主屏幕、设置屏幕等,它们是由多个Activity类的实例构*的。

关于Activityactivity的说明。在 XML 中,就像AndroidManifest文件一样,activity是小写的,但在 Java 中,Activity类有一个大写的A。这只是一种约定,不值得担心。正如我们刚才看到的,XML 中的activity具有一个name属性,其值指的是 Java Activity的一个实例。

让我们深入java文件夹。我想知道我们会在那里找到什么。

java 文件夹

对于稍微讽刺的评论,我表示歉意。当然,我们会找到所有的 Java 代码。首先,这只包括一个文件,但随着我们的项目的增长,我们将添加更多。展开java文件夹,您会发现另外三个文件夹,如下面的截图所示:

图 3.5 - 展开 java 文件夹

图 3.5 – 展开 java 文件夹

在本书中,我们只需要这三个文件夹中的一个——顶部的那个。这些文件夹的名称由包名称(在创*应用程序时选择)和应用程序名称组成,全部小写且没有空格(也是在创*应用程序时选择的)。

注意

有多个同名文件夹的原因是与自动化测试有关的高级原因,这超出了本书的范围。因此,你可以安全地忽略以(androidTest)(test)结尾的文件夹。

在本书的过程中,我们感兴趣的唯一文件夹是顶部的那个,在我的屏幕上是com.gamecodeschool.emptyactivityapp。根据你选择的包名称和我们当前正在工作的应用程序的名称,文件夹名称会发生变化,但我们始终需要访问和添加或编辑其内容的是顶部的那个。

现在展开com.gamecodeschool.emptyactivityapp(或者你的文件夹名称)文件夹,查看其内容。在下一个截图中,你可以看到该文件夹只有一个文件:

图 3.6 – com.gamecodeschool.emptyactivityapp 文件夹

图 3.6 – com.gamecodeschool.emptyactivityapp 文件夹

这个文件是MainActivity.java,尽管项目窗口中没有显示文件扩展名,但它在编辑器窗口上方的标签中显示。事实上,java/packagename.appname文件夹中的所有文件都将有一个.java扩展名。如果双击MainActivity.java文件,它将在编辑器窗口中打开,尽管我们也可以只点击编辑器窗口上方的MainActivity.java标签。随着我们添加更多的 Java 文件,知道它们的位置将会很有用。

检查MainActivity.java文件,你会发现它是我们在第一个项目中使用的 Java 文件的简化版本。它与原来的文件相同,只是方法更少,在onCreate方法中没有自动生成的代码。方法缺失是因为 UI 更简单,因此它们是不需要的,而且 Android Studio 没有生成它们。

为了参考,看一下下一个截图中MainActivity.java文件的内容。我已经在代码中勾画了一行:

图 3.7 – MainActivity.java 文件

图 3.7 – MainActivity.java 文件

它仍然具有在用户运行应用程序时执行的onCreate方法,但其中的代码要少得多,onCreate是唯一的方法。看一下onCreate方法中的最后一行代码——在继续探索res文件夹之前,我们将讨论这个。以下是讨论中的代码行:

setContentView(R.layout.activity_main);

代码正在调用一个名为setContentView的方法,并且正在向setContentView方法传递一些数据,以便setContentView方法中的代码可以使用。传递给setContentView的数据是R.layout.activity_main

现在,我只想提一下setContentView方法是由 Android 提供的,R.layout.activity_main是什么?

我们将通过探索res文件夹来了解,但是快速提一下Java(生成的)文件夹,这样我们在进展中不会为此烦恼。首先要注意的是,该文件夹是在模拟器或真实设备上首次运行应用程序时自动生成的,因此如果你还没有运行应用程序,你就看不到它。

Java(生成的)文件夹

这个文件夹包含了由 Android Studio 生成的代码,我们不需要关心其中的内容。即使是需要它的高级用户通常也只是用它作为参考。

让我们继续讨论res文件夹和R.layout.activity_main代码。

res 文件夹

res文件夹是所有资源的存放地。左键单击展开res文件夹,我们将检查里面的内容。这是该文件夹内顶层文件夹的截图:

图 3.8 – res 文件夹

图 3.8 – res 文件夹

让我们从列表的顶部开始,看drawable文件夹。

res/drawable文件夹

这个名称有点透露了一些东西,但drawable文件夹中包含的不仅仅是图形。随着我们在书中的进展,我们确实会向这个文件夹中添加图形。但是现在,它只包含两个文件。

这些文件是ic_launcher_foregroundic_launcher_background。我们不会检查任何文件,因为我们永远不需要修改它们,但我会简单提一下它们是什么。

如果你打开这些文件,你会发现它们非常长而且技术性很强。它们包括看起来是坐标、颜色等的列表。它们被称为图形蒙版,用于 Android 适应/遮罩其他图形—具体来说,在这种情况下,是应用程序的启动图标。这些文件向 Android 提供了关于如何调整应用程序启动图标的指令。

这个系统是为了让不同的设备制造商可以创*适合自己 Android 设备的蒙版。这些蒙版默认位于drawable文件夹中(ic_launcher_foregroundic_launcher_background),它们是默认的自适应蒙版,为启动图标添加了视觉上令人愉悦的阴影和深度。

注意

如果自适应图标的概念对你有吸引力,那么你可以在这个链接上看到Android 开发者网站上的完整而非常直观的解释,网址为developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive。你不需要查看这个页面就可以继续。

我们现在对drawable文件夹有足够的了解,让我们继续看layout文件夹。

res/layout文件夹

res文件夹是应用程序所有资源的存放处,如图标、布局(XML 文件)、声音和字符串。让我们仔细看一下。

展开layout文件夹,你会看到一个名为activity_main.xml的布局文件,如果你打开它查看内容,你会发现它与我们在上一章中编辑的文件非常相似。这次内容更少,因为我们生成了一个ConstraintLayout元素,包裹着一个写着Hello World!TextView小部件。

一定要查看内容,但这里最有趣的不是这个。仔细看文件的名称(不包括 XML 文件扩展名):activity_main

现在,回想一下MainActivity.java文件中的 Java 代码。这是我们说过设置 UI 的代码行。我已经突出显示了一部分代码:

setContentView(R.layout.activity_main);

R.layout.activity_main代码确实是对res/layout文件夹中的activity_main文件的引用。这是我们的 Java 代码和 XML 布局/设计之间的连接。

注意

除了activity_main.xml的内容之外,与第一个项目有所不同——在第一个项目的layout文件夹中,有多个额外的文件。在本章的后面,我们将使用第一章中使用的相同模板(基本活动)构*另一个项目,以了解原因。

在此之前,让我们探索最后两个文件夹及其所有子文件夹,从列表中的下一个开始:mipmap

res/mipmap文件夹

mipmap文件夹很简单—嗯,相对简单。展开文件夹查看其内容,如下面的截图所示:

图 3.9 – mipmap 文件夹

图 3.9 – mipmap 文件夹

在这里,我们可以看到两个子文件夹:ic_launcheric_launcher_roundic_launcher的内容是我们在设备的应用程序抽屉/主屏幕中看到的常规启动图标的图形,而ic_launcher_round包含使用圆形图标的设备的图形。双击每个文件夹中的.png文件来查看。我在下一个截图中将它们并排放置,以帮助我们讨论:

图 3.10 – 启动图标

图 3.10 – 启动图标

你可能也想知道为什么每个文件夹中有五个ic_launcher….png文件。这是因为为不同的屏幕尺寸和分辨率提供合适缩放的图标是一个良好的做法。通过提供带有hdpimdpixhdpixxhdpixxxhdpi资格的图像,这允许不同的 Android 设备选择最适合用户的图标。

注意

字母dpi代表高、中、超高、超超高等前缀。这些被称为限定符,随着我们的进展,我们会看到 Android 有很多限定符,帮助我们构*适合不同 Android 设备的应用程序。

mipmap文件夹的最后一个谜团是,每个子文件夹中也有一个 XML 文件。打开其中一个,你会看到它们引用了我们在drawable文件夹中看到的ic_launcher_foregroundic_launcher_background文件。这告诉 Android 设备从哪里获取自适应图标的详细信息。这些文件不是必需的,但它们使图标看起来更好,同时增加了它们的灵活性。

我们还有一个文件夹和它的所有文件,然后我们将充分理解 Android 应用程序的结构。

res/values 文件夹

打开res/values文件夹,可以看到我们将依次简要讨论的三个文件。所有这些文件相互关联/引用,或者与我们已经看过的其他文件相互关联。

为了完整起见,这里是res/values文件夹中三个文件的截图:

图 3.11 - res/values 文件夹

图 3.11 - res/values 文件夹

注意

values文件夹内还有一个themes文件夹,但在本书的上下文中我们不需要探索这个。

理解的关键不在于记忆连接,当然也不是试图记忆或理解文件中的代码,而是要欣赏到迄今为止我们所见过的所有文件和代码之间相互关联的本质。

让我们依次查看文件的内容。

颜色.xml 文件

接下来我们将查看colors.xml文件的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#6200EE</color>
    <color name="colorPrimaryDark">#3700B3</color>
    <color name="colorAccent">#03DAC5</color>
</resources>

请注意,起始和结束标签采用了我们从 XML 文件中所期望的通常模式。有一个开放的<resources>标签和一个闭合的</resources>标签。作为资源的子元素,有三对<color> … </color>标签。

在每个color标签中都有一个name属性和看起来很奇怪的代码,由数字和字母组成。name属性是颜色的名称。我们将在接下来的文件中看到各种名称的引用。

代码是定义实际颜色本身的东西。因此,当引用名称时,相关代码定义的颜色将显示在屏幕上。我们将在一会儿看到这些名称在哪里被引用。

注意

代码称为09af,共有 16 个可能的值。如果你想了解更多并尝试使用十六进制颜色,请访问www.color-hex.com/color-wheel/。如果你对十六进制(基数 16)、二进制(基数 2)等数字基感到好奇,那么请看下面的文章,它解释了它们,并谈到了为什么人类通常使用十进制:betterexplained.com/articles/numbers-and-bases/。你不需要阅读这些文章来继续阅读本书。

字符串.xml 文件

大多数现代应用程序都是为尽可能广泛的受众而制作的。此外,如果一个应用程序的规模或复杂性很大,那么软件公司中的角色通常被分成许多不同的团队 - 例如,为 Android 应用程序编写 Java 代码的人很可能与设计 UI 布局的人几乎没有关系。

通过将应用程序的内容与应用程序的编程分开,可以更容易地随时进行更改,还可以在不更改每个 Java 代码的情况下为多种不同的口头语言创*内容。

接下来看一下strings.xml文件的内容:

<resources>
    <string name="app_name">Empty Activity App</string>
</resources>

我们可以看到在现在熟悉的<resources>…</resources>标签中,有一个<string>…</string>标签。在string标签中,有一个名为name的属性,具有app_name值,然后是Empty Activity App的进一步值。

让我们再看一下我们之前在清单文件夹部分中探索过的AndroidManifest.xml文件中的一行。接下来是相关的代码行,但如果您想要查看完整的上下文中的代码行,请参考 Android Studio 中的文件本身:

android:label="@string/app_name"

android:label属性被赋予了@string/app_name的值。在 Android 中,@string指的是strings.xml文件中的所有字符串。在这个特定的应用程序中,具有app_name标签的string属性具有Empty Activity App的值。

因此,在应用程序运行时,AndroidManifest.xml文件中先前显示的代码行对屏幕产生以下影响:

图 3.12 – AndroidManifest.xml 文件效果

图 3.12 – AndroidManifest.xml 文件效果

虽然这个系统乍看起来可能有些复杂,但在实践中,它将设计和内容与编码分开,这是非常高效的。如果应用程序的设计者想要更改其名称,他们只需编辑strings.xml文件,而无需与 Java 程序员进行交互;如果应用程序中的所有文本都是作为字符串资源提供的,那么在项目进行过程中所有这些文本都可以很容易地进行更改和调整。

Android 通过允许开发人员为每种语言/区域设置不同的字符串资源文件来进一步提高了这种灵活性。这意味着开发人员可以为全球范围内的用户提供完全相同的 Java 代码。Java 程序员只需引用字符串资源的name属性,而不是将文本本身硬编码到 Java 中,然后其他部门可以设计文本内容并处理诸如语言翻译之类的任务。我们将在第十八章中使应用程序多语言化,本地化

注意

在 Java 代码中直接硬编码实际文本而不是使用字符串资源是可能的,我们会不时这样做,为了方便展示一些 Java 而不必陷入编辑或添加到strings.xml文件中。在上一章中,当我们制作吐司消息并将文本输出到控制台时,我们就这样做了。

Android 系统允许设计者选择一系列颜色、文本、图像、声音和其他资源,轻松地为世界各地的不同地区制作其应用程序的变体。

例如,在西方文化中,绿色可以代表自然和正确性的主题;在许多中东国家,绿色代表生育力,是与伊斯兰教相关的颜色。虽然您可能在这些地区分发绿色,但您的应用程序将被认为是非常不同的。

如果您将应用程序推出到印度尼西亚,绿色在许多(尽管不是所有)印度尼西亚人中是受到文化蔑视的。接下来,您在中国推出,绿色可能会带有与不忠有关的负面含义。这是一个典型的 Java 程序员永远不会学会应对的困难局面——幸运的是,由于我们可以在 Android Studio 中分担责任的方式,他们不需要了解这一点。

颜色——因此,样式和主题——是一个非常专业的话题。虽然我们不会探索比绿色更深入的内容,但希望您能看到将编程、布局、颜色和文本内容的责任分开的好处。

值得一提的是,即使我们不会雇佣设计师、翻译员和文化专家团队,也完全有可能制作出一款受到成千上万甚至数百万用户喜爱的精彩应用。然而,即使我们不打算雇佣设计师、翻译员和文化专家团队,我们仍然必须在设计的基础上进行工作,这就是为什么我们要深入研究。

到目前为止,我们已经很好地掌握了 Android 项目的组成部分以及不同方面之间的联系。让我们再构*一个应用程序,不需要以相同的细节深入研究,而是看看不同的应用程序模板对 Android Studio 生成的基础文件造成的影响。

探索基本活动项目模板的文件和文件夹结构

使用自动生成 UI 的下一个最简单的项目类型是基本活动项目。这是我们在第一章**《开始 Android 和 Java》中创*的相同类型的项目。现在可以打开该项目,但是生成一个新项目同样快速,而且我们也可以在没有任何修改和添加的情况下进行检查讨论。

继续如下操作:

  1. 运行 Android Studio 并左键单击开始新的 Android Studio 项目选项。

  2. 接下来是选择项目模板窗口。选择基本活动,然后点击下一步

  3. 配置您的项目窗口中,设置项目如下:

  4. 点击完成按钮,我们将运行应用程序,看看我们取得了什么成就。

现在,我们可以深入研究文件。我们不会像我们为空活动项目那样详细查看所有内容;相反,我们只会看看文件之间的相互联系,并进行一些比较。

探索基本活动项目

首先让我们看看代码编辑器中MainActivity.java选项卡中的 Java 代码。正如前面所述,基本活动项目比空活动项目更复杂。

注意

您可以打开尽可能多的 Android Studio 实例。如果要并排比较项目,请选择文件 | 打开,然后选择项目,当提示时选择新*窗口以打开项目,而不关闭已经打开的任何项目。

首先要注意的是onCreate方法中有一些额外的代码。

MainActivity.java 文件

我在第二章**《第一次接触:Java、XML 和 UI 设计师》中非常简要地提到了 Java 代码和 XML 代码中的这些相互联系。让我们浏览一下资源文件,并指出这些 Java 代码指向的 XML 文件。

下面是下一个显示的 Java 代码。我稍微重新格式化了一下,以便在书中更易读:

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
         Snackbar.make(view, "Replace with your own 
          action", Snackbar.LENGTH_LONG)
                      .setAction("Action", null).show();
   }
});

要完全理解这段代码将需要更多的章节,但是指出这段代码使用资源文件的地方只需要一会儿,然后我们会更加了解构成我们项目的组件。

代码涉及两个资源。第一个是Toolbar资源,通过R.id.toolbar引用。第二个是FloatingActionBar资源,通过R.id.fab引用 XML 文件,我们很快就会看到。

如果我们在项目窗口中打开res/layout文件夹和java文件夹,我们会发现与空活动项目中的情况不同,如下图所示:

图 3.13 – res/layout 文件夹和 java 文件夹

图 3.13 – res/layout 文件夹和 java 文件夹

现在有三个自动生成的 Java 文件和四个自动生成的 XML 布局文件。

请记住,这个应用有两个屏幕——第一个屏幕上有一个Hello first fragment消息和一个Next按钮;第二个屏幕上只有一个Previous按钮。

发生的事情是,Android Studio 不仅为每个屏幕的外观提供单独的布局文件,而且还为控制每个屏幕的代码提供单独的 Java 文件。如果你只是表面上接受了我刚才说的话,因此你会期望有两个布局文件和两个 Java 文件,但我们有更多。

正如我们已经知道的,当用户运行应用程序时,MainActivity.java文件的onCreate方法被执行。这设置了应用程序,包括布局。布局在activity_main.xml中,但这个文件不再控制两个主要屏幕的布局。它具有两个屏幕之间一致的元素,并将布局委托给content_main.xml。然后,content_main.xml文件定义了它所占据的屏幕区域,并将将出现在此区域的细节委托给另一个文件,即res/navigation文件夹中的nav_graph.xml文件。然后,nav_graph.xml文件确定使用哪个布局(fragment_first.xmlfragment_second.xml)以及哪个相应的 Java 文件将控制布局(FirstFragment.javaSecondFragment.java)。

在这个阶段,显而易见的复杂性可能会让人不知所措。我猜这可能是使得学* Android 开发没有任何先前开发经验如此具有挑战性的原因之一。但好消息是:

  • 我们不需要记住和理解所有这些相互关联的细节。

  • 我们可以构*大量的应用程序而不使用其中任何部分。

  • 随着我们在书中的进展,并且与这个谜题的不同部分一起工作,我们将逐渐熟悉它们。

看一下下一个图,展示了基本活动模板应用程序的工作原理:

图 3.14 – 基本活动模板

图 3.14 – 基本活动模板

如果显而易见的复杂性看起来令人沮丧,那么了解为什么要这样做可能会有所帮助。我们已经讨论过将布局与编程分开,并进一步分离文本和图形,以允许不同的团队处理应用程序的不同方面。现在,我们可以进一步分离不仅是应用程序不同屏幕之间的导航,比如主菜单屏幕、设置屏幕和其他一些屏幕,而且与每个屏幕相关的布局和编程也是分开的,因此它们也可以由不同的团队同时进行。随着我们在章节中的进展,我们会更多地讨论这个问题。

我们将研究编写单独的片段布局和控制每个片段的单独 Java 代码,以及从第二十四章开始,我们将更深入地了解为什么我们希望这样做,并且我们将在下一章更深入地研究activity_main.xmlcontent_main.xml等相互关联的布局文件。

目前,让我们更深入地看一下MainActivity.java文件代码如何与activity_main.xml布局相连接。我们会发现,虽然activity_main.xml文件负责放置工具栏和浮动操作按钮,但MainActivity.java文件负责控制用户与它们交互时发生的事情。

activity_main.xml 文件

目前,打开activity_main.xml文件,你会看到有一些元素来代表toolbarfab。引用这些元素的 Java 代码正在设置工具栏和浮动操作栏以供使用。XML 代码,正如我们所期望的那样,描述了它们的外观。

这是工具栏的 XML 代码:

<androidx.appcompat.widget.Toolbar
   android:id="@+id/toolbar"
   android:layout_width="match_parent"
   android:layout_height="?attr/actionBarSize"
   android:background="?attr/colorPrimary"
   app:popupTheme="@style/AppTheme.PopupOverlay" />

注意它提到了工具栏、颜色和样式,以及其他一些方面。

为了清晰起见,这是实际工作应用程序中的工具栏:

图 3.15 – 应用程序的工具栏

图 3.15 – 应用程序的工具栏

这是悬浮操作按钮的 XML 代码。我稍微重新格式化了代码的第一行,以便在本书的打印版本中更好地显示:

<com.google.android.material.floatingactionbutton.
FloatingActionButton
   android:id="@+id/fab"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_gravity="bottom|end"
   android:layout_margin="@dimen/fab_margin"
   app:srcCompat="@android:drawable/ic_dialog_email" />

请注意,它具有fabid值。通过这个id值,我们可以在我们的 Java 代码中访问悬浮操作按钮,具体来说,是MainActivity.java中的这一行:

FloatingActionButton fab = findViewById(R.id.fab);

在这行代码执行之后,我们的 Java 代码中的fab对象现在可以直接控制悬浮操作按钮及其所有属性。在第十三章匿名类 – 让 Android 小部件栩栩如生中,我们将学*如何详细做到这一点。

这是实际应用程序中的悬浮操作按钮:

图 3.16 – 悬浮操作按钮

图 3.16 – 悬浮操作按钮

我没有详细解释代码,因为在这个阶段没有意义。只需开始在这里梳理相互关系:

  • XML 文件可以引用其他 XML 文件。

  • Java 可以引用 XML 文件(以及,正如我们很快将看到的,其他 Java 文件)。

  • 现在,我们已经看到在 Java 中,我们可以通过其id属性抓取 XML 文件中特定部分的 UI 控制。

我们已经从这个文件中看到足够的内容,所以让我们继续并深入了解剩下的文件。

MainActivity.java 中的额外方法

那么,这些方法是做什么的,它们何时被调用,由谁调用?下一个区别是这个额外的方法(再次略微重新格式化以供展示):

@Override
public boolean onCreateOptionsMenu(Menu menu) {
   // Inflate the menu; this adds items to the 
   // action bar if it is present.
   getMenuInflater().inflate(R.menu.menu_main, menu);
   return true;
}

这段代码准备(膨胀)了在menu_main.xml文件中定义的菜单,就像onCreate方法一样,onCreateOptionsMenu方法是一个重写的方法,由操作系统直接调用。

然后,接下来是另一个方法:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
   // Handle action bar item clicks here. The action bar 
      will
   // automatically handle clicks on the Home/Up button, so 
      long
   // as you specify a parent activity in 
      AndroidManifest.xml.
   int id = item.getItemId();
   //noinspection SimplifiableIfStatement
   if (id == R.id.action_settings) {
         return true;
   }
   return super.onOptionsItemSelected(item);
}

这个方法也被重写了,并且它也是由操作系统直接调用的。它处理用户选择菜单中的项目(选项)时发生的情况。目前,它只处理一个选项——设置选项——并且目前不执行任何操作,如下所示:

The code if (id == R.id.action_settings) {

这确定了return true代码是否执行任何选项,并且控制是否返回到被用户点击设置菜单选项中断的应用程序的任何部分执行之前。

我们现在已经知道了几乎足够的知识。不要担心记住所有这些连接。我们将回到每个连接,进行更深入的调查,并巩固我们对每个连接的理解。

现在,我们可以更仔细地看一下 Android 模拟器。

探索 Android 模拟器

随着我们的进展,熟悉如何使用 Android 模拟器会有所帮助。如果您还没有使用最新版本的 Android,甚至执行一些简单任务的方式(例如查看所有应用程序)可能与您当前的设备工作方式不同。此外,我们想知道如何使用所有模拟器附带的额外控件。

模拟器控制面板

您可能已经注意到当您运行模拟器时,旁边会出现一个迷你控制面板。让我们来看一些最有用的控件。看一下模拟器控制面板的截图。我已经在上面加了注释,以帮助讨论它:

图 3.17 – 模拟器控制面板

图 3.17 – 模拟器控制面板

我将简要提及更明显的控件,并在必要时进行更深入的讨论:

  1. 这些是窗口控件。最小化或关闭模拟器窗口。

  2. 从上到下,关闭模拟器,模拟关闭实际设备的电源。接下来的两个图标分别提高和降低音量。

  3. 这两个按钮允许您将模拟器向左和向右旋转。这意味着您可以测试您的应用程序在所有方向下的外观,以及在应用程序运行时如何处理方向更改。这两个图标立即下方的图标分别是截图和放大。

  4. 这些图标模拟了返回按钮和主页按钮,以及查看正在运行的应用程序。请尝试一下这些按钮,因为我们将不时需要使用它们,包括在第六章中,Android 生命周期

  5. 按下此按钮以启动高级设置菜单,在那里您可以与传感器、全球定位系统GPS)、电池、指纹识别器等进行交互。如果您感兴趣,可以尝试一些这些设置。

让我们来玩一下模拟器本身。

将模拟器用作真实设备

模拟器可以模拟真实手机的每个功能,因此可以写一本关于使用它的整本书。如果您想要编写用户喜爱的应用程序,那么了解各种 Android 设备是值得花时间去做的。我只想在这里指出一些最基本的功能,因为没有这些基本的交互,将很难跟上本书的内容。此外,如果您有一部较旧的 Android 设备,那么一些基本的功能(如访问应用程序抽屉)已经发生了变化,您可能会感到有些困惑。

访问应用程序抽屉

将鼠标光标放在主屏幕底部并向上拖动以访问应用程序抽屉(所有应用程序)。下面的截图显示了这个动作进行到一半的情况:

图 3.18 - 向上拖动以访问应用程序抽屉

图 3.18 - 向上拖动以访问应用程序抽屉

现在,您可以运行模拟器上安装的任何应用程序。请注意,当您通过 Android Studio 运行您的应用程序之一时,它将保留在模拟器上安装,并且因此可以从应用程序抽屉中运行。但是,您在 Android Studio 中对应用程序所做的每一次更改都需要您再次点击 Android Studio 快速启动栏上的播放按钮来运行/安装应用程序 - 就像我们一直在做的那样。

查看活动应用程序并在应用程序之间切换

要查看活动应用程序,您可以使用模拟器控制面板,即模拟器控制面板截图上标有数字4的方块(图 3.17)。要使用手机屏幕访问相同的选项(就像在真实设备上一样),向上滑动,就像访问应用程序抽屉时一样,但只需滑动大约屏幕长度的四分之一。这个过程在下面的截图中有所说明:

图 3.19 - 向上滑动

图 3.19 - 向上滑动

现在,您可以通过最近的应用程序向左和向右滑动,向上滑动应用程序以关闭它,或者点击返回按钮返回到您在查看此选项之前所做的事情。请尝试一下,因为我们经常会使用这些基本功能。

总结

请记住,本章的目标是熟悉 Android/Android 项目的系统/结构。Android 项目是 Java 和多种资源文件的交织。资源文件可以包含 XML 来描述我们的布局、文本内容、样式和颜色,以及图像。资源可以针对世界各地的不同语言和地区进行生产。我们将在本书中看到和使用的其他资源类型包括主题和音效。

不需要记住不同资源文件和 Java 文件之间的各种连接方式。重要的是要意识到它们之间的联系,并能够检查各种类型的文件,并意识到它们何时依赖于另一个文件中的代码。每当我们从 Java 代码创*与 XML 代码的连接时,我都会再次指出连接的细节。

我们不需要学* XML 和 Java,但我们会对它有一点点了解。Java 将是本书的重点,但我们的 Java 经常会涉及 XML 代码,因此理解并看到一些互连的示例将有助于您更快地取得进展。

我们还探索了模拟器,以便在测试我们的应用程序时充分利用它。

在下一章中,我们将使用两种不同的 Android 布局方案构*两个自定义布局。我们还将编写一些 Java 代码,以便我们可以通过点击按钮来在它们之间切换。

第四章:开始使用布局和 Material Design

我们已经看到了 Android Studio 的用户界面UI)设计师,以及 Java 的实际应用。在这个动手实践的章节中,我们将构*三个更多的布局——仍然相当简单,但比我们迄今为止所做的要进一步。

在我们开始动手之前,我们将快速介绍Material Design的概念。

我们将看到另一种称为LinearLayout的布局类型,并逐步使用它来创*可用的 UI。我们将进一步使用ConstraintLayout,既了解约束,又设计更复杂和精确的 UI 设计。最后,我们将介绍TableLayout,用于在易于阅读的表格中布置数据。

我们还将编写一些 Java 代码,以在我们的应用程序/项目中在不同的布局之间切换。这是第一个将多个主题整合到一个整洁包裹中的重要应用程序。该应用程序名为探索布局

在本章中,我们将讨论以下主题:

  • 了解 Material Design

  • 探索 Android UI 设计

  • 介绍布局

  • 构*LinearLayout并学*何时最好使用这种类型的布局

  • 构*另一个稍微更高级的ConstraintLayout并了解更多关于使用约束的知识

  • 构*TableLayout并填充数据以显示

  • 将所有内容连接在一个名为探索布局的应用程序中

首先是 Material Design。

技术要求

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

了解 Material Design

您可能听说过 Material Design,但它究竟是什么?Material Design 的目标非常简单——创*美观的 UI。然而,它也是关于使这些 UI 在 Android 设备上保持一致。Material Design 并不是一个新的想法;它直接来源于纸笔设计中使用的设计原则,例如通过阴影和深度具有视觉上令人愉悦的装饰。

Material Design 使用材料层的概念,您可以将其视为照片编辑应用程序中的图层的方式。通过一组原则、规则和指南实现一致性。必须强调的是 Material Design 是完全可选的,但也必须强调的是 Material Design 是有效的——如果您不遵循它,您的设计很可能不会受到用户的欣赏。毕竟,用户已经*惯了某种类型的 UI,而该 UI 很可能是使用 Material Design 原则创*的。

Material Design 是一个值得追求的合理标准,但在学*其细节时,我们不应该让它阻碍我们学*如何开始使用 Android。

本书将专注于完成任务,偶尔指出 Material Design 如何影响设计,以及对于那些想深入了解 Material Design 的人的更多资源。

探索 Android UI 设计

我们将看到,对于 Android UI 设计,我们所学到的很多内容都是依赖上下文的。给定小部件的x属性如何影响其外观可能取决于小部件的y属性,甚至取决于另一个小部件的属性。这并不容易逐字学*。最好期望通过实践逐渐取得更好和更快的结果。

例如,如果您通过拖放小部件到设计中来玩转设计师,生成的可扩展标记语言XML)代码将会有很大的不同,这取决于您使用的布局类型。在本章中我们将看到这一点。

这是因为不同的布局类型使用不同的方法来决定其子元素的位置,例如,我们将在下面探索的LinearLayout与我们项目中默认添加的ConstraintLayout的工作方式完全不同。

这些信息起初可能看起来是一个问题,甚至是一个坏主意,而且起初确实有点尴尬。然而,我们将会逐渐了解到,这种清晰的布局选项的丰富性及其各自的特点是一件好事,因为它们给了我们几乎无限的设计潜力。您几乎可以想象不可能实现的布局是非常少的。

然而,这几乎无限的潜力也带来了一些复杂性。开始掌握这一点的最好方法是构*一些不同类型的工作示例。在本章中,我们将看到三种类型的布局,如下所述:

  • LinearLayout

  • ConstraintLayout

  • TableLayout

我们将看到如何利用可视化设计工具的独特功能使事情变得更容易,我们还将对自动生成的 XML 进行一些关注,以使我们的理解更全面。

介绍布局

我们已经在第一章开始 Android 和 Java中看到了ConstraintLayout,但除此之外还有更多的布局。布局是将其他 UI 元素组合在一起的构*块。布局也可以包含其他布局。

让我们看一下 Android 中一些常用的布局,因为了解不同的布局及其优缺点将使我们更加了解可以实现什么,从而扩展我们的可能性。

我们已经看到,一旦我们设计了一个布局,就可以在我们的 Java 代码中使用setContentView方法将其付诸实践。

让我们构*三种不同布局类型的设计,然后使用setContentView并在它们之间切换。

创*和探索布局项目

在 Android 中最困难的事情之一不仅是找出如何做某事,而是找出如何在特定上下文中做某事。这就是为什么在本书中,除了向您展示如何做一些很酷的东西之外,我们还会将许多主题链接到跨越多个主题和章节的应用程序中。探索布局项目是这种类型的第一个应用程序。我们将学*如何构*多种类型的布局,同时将它们全部链接在一个方便的应用程序中。

让我们开始吧,如下所示:

  1. 在 Android Studio 中创*一个新项目。如果您已经打开了一个项目,请选择文件 | 新*项目。在提示时,选择在同一窗口中打开,因为我们不需要参考以前的项目。

重要提示

如果您在 Android Studio 的起始屏幕上,可以通过单击创*新项目选项来创*一个新项目。

  1. 确保选择空活动项目模板,因为我们将从头开始构*大部分 UI。单击下一步按钮。

  2. 输入探索布局作为应用程序名称,然后单击完成按钮。

查看MainActivity.java文件。以下是代码,不包括import…语句:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
    }
}

找到对setContentView方法的调用,并删除整行代码。该行在前面的代码片段中显示为高亮显示。

这正是我们想要的,因为现在,我们可以构*我们自己的布局,探索底层的 XML,并编写我们自己的 Java 代码来显示这些布局。如果您现在运行应用程序,您将只会得到一个带有标题的空白屏幕;甚至没有一个 Hello World!的消息。

我们将要探索的第一种布局类型是LinearLayout类型。

使用 LinearLayout 构*布局

LinearLayout可能是 Android 提供的最简单的布局。顾名思义,其中的所有 UI 项都是线性布局的。你只有两个选择:垂直和水*。通过添加以下代码行(或通过LinearLayout编辑)来垂直布局:

android:orientation="vertical"

然后(你可能已经猜到了)将"vertical"更改为"horizontal"以水*布局。

在我们对LinearLayout进行任何操作之前,我们需要将其添加到布局文件中;由于我们在这个项目中构*了三个布局,所以我们还需要一个新的布局文件。

向项目添加 LinearLayout 布局类型

在项目窗口中,展开res文件夹。现在,右键单击layout文件夹,然后选择新*。注意,有一个布局资源文件选项,如下面的截图所示:

图 4.1 - 将 LinearLayout 添加到项目

图 4.1 - 将 LinearLayout 添加到项目

选择LinearLayout,如下面的截图所示:

图 4.2 - 创*新的资源文件

图 4.2 - 创*新的资源文件

main_menu中。名称是任意的,但这个布局将是我们用来选择其他布局的主菜单,所以名称似乎是合适的。

点击 XML 文件中的LinearLayout,并将其放在layout文件夹中,准备好为我们构*新的主菜单 UI。新文件会自动在编辑器中打开,准备好让我们进行设计。

准备工作空间

点击设计选项卡以切换到设计视图,如果你还没有在这个视图中。通过拖动和调整窗口边框(就像大多数窗口化应用程序中一样),使调色板、设计和属性尽可能清晰,但不要超出必要的大小。下面的截图显示了我选择的大致窗口比例,以使设计我们的 UI 和探索 XML 尽可能清晰。截图中的细节并不重要:

图 4.3 - 准备工作空间

图 4.3 - 准备工作空间

重要提示

我已经尽可能地将项目、调色板和属性窗口变窄,但没有遮挡任何内容。我还关闭了屏幕底部的构*/logcat 窗口,结果是我有一个很清晰的画布来构* UI。

检查生成的 XML

点击代码选项卡,我们将查看当前阶段构成我们设计的 XML 代码的当前状态。以下是代码,以便我们讨论(我稍微重新格式化了一下,以使其在页面上更清晰):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" 
android:layout_width="match_parent"
    android:layout_height="match_parent">
</LinearLayout>

我们有通常的起始和结束标签,正如我们可能已经预测到的那样,它们被称为<LinearLayout</LinearLayout>。目前还没有子元素,但有三个属性。我们知道它们是LinearLayout的属性而不是子元素,因为它们出现在第一个闭合>之前。为了清晰起见,前面的代码中突出显示了定义这个LinearLayout的三个属性。

第一个属性是android:orientation,或者更简洁地说,我们将只提到属性而不包括android:部分。orientation属性的值为vertical。这意味着当我们开始向这个布局添加项目时,它将垂直从上到下排列它们。我们可以将vertical值更改为horizontal,它会从左到右排列。

接下来的两个属性是layout_widthlayout_height。这些属性确定了LinearLayout的大小。给这两个属性的值都是match_parent。布局的父级是整个可用空间。通过水*和垂直匹配父级,布局将填充整个可用空间。

向 UI 添加一个 TextView

切换回设计选项卡,我们将向 UI 添加一些元素。

首先,在调色板中找到TextView小部件。这可以在 UI 上找到TextView,请注意它整齐地位于LinearLayout的顶部。

查看LinearLayout上的 XML,并且它缩进了一个制表符以使其清晰。以下是TextView小部件的代码,不包括LinearLayout的周围代码:

<TextView
   android:id="@+id/textView"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:text="TextView" />

请注意,它有四个属性:id属性,以防我们需要从另一个 UI 元素或我们的 Java 代码中引用它;layout_width属性设置为match_parent,这意味着TextView横跨整个LinearLayout的宽度;layout_height属性设置为wrap_content,这意味着TextView的高度正好足够容纳其中的文本。最后,目前它有一个text元素,确定它将显示的实际文本,目前设置为“TextView”。

切换回Design选项卡,我们将进行一些更改。

我们希望这段文字成为此屏幕的标题文字,即菜单屏幕。在搜索框中输入text,如下截图所示。找到text属性并将其值更改为Menu

图 4.4 - 将 TextView 添加到 UI

图 4.4 - 将 TextView 添加到 UI

提示

您可以通过搜索或只是浏览选项来查找任何属性。找到要编辑的属性后,左键单击选择它,然后按键盘上的Enter键使其可编辑。

接下来,使用您喜欢的搜索技术找到textSize属性,并将textSize设置为50sp。输入这个新值后,文本大小将增加。

重要提示

sp值代表可伸缩像素。这意味着当用户在其 Android 设备上更改字体大小设置时,字体将动态重新调整大小。

现在,搜索gravity属性,并通过单击以下截图中指示的小箭头来展开选项:

图 4.5 - 展开 gravity 属性

图 4.5 - 展开 gravity 属性

通过将该值设置为true,将center_horizontal添加到 gravity 中,如下截图所示:

图 4.6 - 通过将值设置为 true 向 gravity 添加 center_horizontal

图 4.6 - 通过将值设置为 true 向 gravity 添加 center_horizontal

gravity属性指的是TextView本身的重力,我们的更改会使TextView内的实际文本居中。

重要提示

gravity属性与layout_gravity属性不同。layout_gravity属性设置布局内的重力,即在这种情况下是父LinearLayout。我们将在项目的后续部分中使用layout_gravity

此时,我们已更改了TextView小部件的文本,增加了其大小,并使其水*居中。UI 设计师现在应该看起来像这样:

图 4.7 - 调整 UI 上的 textView

图 4.7 - 调整 UI 上的 textView

快速查看Code选项卡上的 XML,将显示以下代码:

<TextView
   android:id="@+id/textView"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:gravity="center_horizontal"
   android:text="Menu"
   android:textSize="50sp" />

您可以按如下方式查看新属性:gravity,设置为center_horizontaltext,已更改为Menu;和textSize,设置为50sp

如果运行应用程序,您可能看不到预期的结果。这是因为我们还没有在我们的 Java 代码中调用setContentView方法来加载 UI。您仍将看到空白的 UI。一旦我们在设计上取得了更多进展,我们将解决这个问题。

将多行 TextView 添加到 UI

切换回我们刚刚添加的TextView小部件。

使用您喜欢的搜索技术,将text属性设置为以下内容:选择布局类型以查看示例。每个按钮的 onClick 属性将调用一个方法,该方法执行 setContentView 以加载新布局

现在,你的布局将如下所示:

图 4.8 - 添加到 UI 的多行 textView

图 4.8 - 添加到 UI 的多行 textView

你的 XML 将在LinearLayout中更新另一个子项(在TextView之后),看起来像这样(我稍微重新格式化了它以便在页面上呈现):

<EditText
   android:id="@+id/editTextTextMultiLine"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:ems="10"
   android:gravity="start|top"
   android:inputType="textMultiLine"
   android:text="Select a layout type to view an example. 
         The onClick attribute of each button will call a 
         method which executes setContentView to load the 
         new layout" />

你可以看到 UI 项的细节。查看 XML 文件,我们有一个inputType属性,表示这个文本是可编辑的。还有另一个我们以前没有见过的属性,那就是emsems属性控制每行可以输入多少个字符,值为 10 是由 Android Studio 自动选择的。然而,另一个属性layout_width="match_parent"覆盖了这个值,因为它导致元素扩展以适应其父元素 - 也就是屏幕的整个宽度。

当你在下一节运行应用程序时,你会看到文本确实是可编辑的,尽管对于这个演示应用程序来说,它没有实际用途。

将 UI 与 Java 代码连接起来(第一部分)

为了实现一个交互式的应用程序,我们将做以下三件事:

  1. 我们将从onCreate方法中调用setContentView方法来显示我们运行应用程序时 UI 的进展。

  2. 我们将编写另外两种自己的方法,每种方法都将在不同的布局上调用setContentView方法(我们还没有设计)。

  3. 然后,在本章稍后设计另外两个 UI 布局时,我们将能够在点击按钮时加载它们。

因为我们将构*一个ConstraintLayout和一个TableLayout,我们将分别调用我们的新方法loadConstraintLayoutloadTableLayout

现在让我们这样做,然后我们可以看到如何添加一些调用这些方法的按钮。

切换到MainActivity.java文件,然后在onCreate方法内添加以下突出显示的代码:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.main_menu);
}

该代码使用setContentView方法来加载我们目前正在工作的 UI。现在你可以运行应用程序来查看以下结果:

图 4.9 - 探索布局菜单

图 4.9 - 探索布局菜单

MainActivity类的onCreate方法之后添加这两个新方法,如下所示:

public void loadConstraintLayout(View v){
   setContentView(R.layout.activity_main);
}
public void loadTableLayout(View v){
   setContentView(R.layout.my_table_layout);
}

第一种方法有一个错误,第二种方法有两个错误。我们可以通过添加一个import语句来修复第一个错误,以便 Android Studio 知道View类。左键单击View单词以选择错误。按住Alt键然后点击Enter键。你会看到以下消息弹出:

图 4.10 - 选择导入类以消除错误

图 4.10 - 选择导入类以消除错误

选择导入类。错误消失了。如果你滚动到代码的顶部,你会看到通过我们刚刚执行的快捷方式添加了一行新的代码。下面是新代码:

import android.view.View;

Android Studio 知道View类,不再有错误。

然而,第二种方法仍然有一个错误。问题在于代码调用setContentView方法来加载一个新的 UI(R.layout.my_table_layout)。由于这个 UI 布局还不存在,它会产生一个错误。你可以注释掉这个调用以消除错误,直到我们在本章后面创*文件并设计 UI 布局为止。添加注释斜杠(//),如下所示:

public void loadConstraintLayout(View v){
   setContentView(R.layout.activity_main);
}
public void loadTableLayout(View v){
   // setContentView(R.layout.my_table_layout);
}

loadConstraintLayout方法没有错误的原因是该方法加载了由 Android Studio 在生成项目时创*的activity_main.xml布局。

现在,我们想要添加一些按钮,以便单击调用我们的新方法并加载我们将要构*的新布局。但是,添加一些带有文本的按钮太容易了 - 我们以前已经做过了。我们想要做的是将一些文本与按钮对齐,使其位于文本的右侧。问题是,我们的LinearLayoutorientation属性设置为vertical,正如我们所见,我们添加到布局中的所有新部分都将垂直排列。

在布局中添加布局

解决方案是在布局中嵌套布局,以便以不同方向排列一些元素。以下是如何做到的。

从调色板的布局类别中,将一个LinearLayout (Horizontal)拖放到我们的设计中,将其放置在多行文本的正下方。请注意以下截图中,有一个蓝色边框占据了多行文本下方的所有空间:

图 4.11 - 从布局类别将 LinearLayout (Horizontal)拖放到我们的设计中,并将其放置在多行文本的正下方

图 4.11 - 从布局类别将 LinearLayout (Horizontal)拖放到我们的设计中,并将其放置在多行文本的正下方

这表明我们的新LinearLayout (Horizontal)正在填充空间。请记住这个蓝色边框区域,因为这将是我们在 UI 上放置下一个项目的地方。

现在,回到新添加的LinearLayout上的TextView小部件。请注意以下截图中,TextView小部件紧密地坐落在新LinearLayout的左上角:

图 4.12 - 从调色板的文本类别将 TextView 小部件拖放到新的 LinearLayout 中

图 4.12 - 从调色板的文本类别将 TextView 小部件拖放到新的 LinearLayout 中

起初,这似乎与之前的垂直LinearLayout发生的情况没有什么不同,它从一开始就是我们 UI 的一部分。但是当我们添加 UI 的下一个部分时,请看看会发生什么。

重要提示

用于指代在布局中添加布局的术语是TextViewButtonLinearLayout)或更广泛的(UI 元素、项目或小部件)。

从先前的TextView小部件的右侧将Button小部件拖放。请注意,按钮位于文本的右侧,如下所示:

图 4.13 - 从按钮类别将按钮小部件拖放到先前 TextView 小部件的右侧

图 4.13 - 从按钮类别将按钮小部件拖放到先前 TextView 小部件的右侧

接下来,通过点击其空白部分选择LinearLayout(水*的那个)。找到layout_height属性,并将其设置为wrap_content。请注意,LinearLayout现在只占用所需的空间,并且在下方有空间可以添加更多小部件。如下截图所示:

图 4.14 - 通过点击空白部分选择 LinearLayout (水*的那个)

图 4.14 - 通过点击空白部分选择 LinearLayout (水*的那个)

在添加 UI 的下一部分之前,让我们配置TextView小部件和Button小部件的text属性。将Button小部件的text属性更改为LOAD。将我们新的TextView小部件的文本属性更改为Load ConstraintLayout

此截图显示了当前布局的外观:

图 4.15 - 配置 TextView 小部件和 Button 小部件的文本属性

图 4.15 - 配置 TextView 小部件和 Button 小部件的文本属性

提示

那起作用了吗?是的?太棒了。您现在已经熟悉了编辑 Android 视图属性。没有?左键单击要编辑的项目(在本例中为TextView小部件),使用搜索图标进行搜索,或滚动查找要在text属性中编辑的属性),选择属性,然后按Enter进行编辑。现在我可以更简洁地说明如何构*未来的 UI 项目,这将使您成为 Android 忍者的旅程更快。

现在,我们可以重复自己,在刚刚完成的下方再添加一个LinearLayout(水*),分别添加另一个TextView小部件和Button小部件。要这样做,请按照以下顺序进行:

  1. 在上一个下面添加另一个LinearLayout(水*)

  2. 添加一个LinearLayout

  3. TextViewtext属性更改为“加载 TableLayout”。

  4. TextView小部件的右侧添加一个Button小部件。

  5. Button小部件的text属性更改为LOAD

  6. 通过将layout_height属性更改为wrap_content来调整LinearLayout的大小。

  7. 只是为了好玩,也为了更多地探索调色板,找到RatingBar小部件并将其放在设计的下方,就在最后一个LinearLayout下方。

现在,您的 UI 应该是这样的:

图 4.16 - 在另一个 LinearLayout(水*)中添加了另一个 TextView 小部件和 Button 小部件

图 4.16 - 在另一个 LinearLayout(水*)中添加了另一个 TextView 小部件和 Button 小部件

让我们为布局添加一些视觉上的润色。

使布局看起来漂亮

在本节中,我们将探讨一些控制 UI 细节的更多属性。您可能已经注意到 UI 在某些地方看起来有点挤,而在其他地方看起来不对称。随着我们在书中的进展,我们将不断增加我们的技能来改善我们的布局,但这些简短的步骤将介绍并处理一些基础知识。

在开始下一步之前,请考虑以下提示。

提示

当您配置小部件的属性时,您可以一次选择多个小部件 - 例如,在接下来的说明中的步骤 3中,您可以左键单击以选择第一个TextView小部件,然后Shift和左键单击以选择第二个TextView小部件。然后可以同时更改两者的属性。这个提示也适用于步骤 4Button小部件。即使小部件类型不同,只要您只编辑所有选定小部件都具有的属性,它也可以工作。因此,在接下来的步骤 5中,您可以同时选择并编辑TextView和两个Button小部件上的“填充”属性。

按照以下说明进行:

  1. 选择“多行文本”小部件,找到并展开“填充”属性。将“填充”选项设置为15sp。这在“文本”周围留出了一个整洁的空间。

  2. 为了在多行文本小部件下面留出一个漂亮的空间,找到并展开Layout_Margin属性,将bottom设置为50sp

  3. 在两个对齐的TextView小部件上,将textSize属性设置为20splayout_gravity设置为center_verticallayout_width属性设置为match_parent;并将layout_weight设置为.7

  4. 在两个按钮上,将权重设置为.3。注意两个按钮现在都占据了宽度的.3,文本占据了.7LinearLayout,使整体外观更加愉悦。

  5. 在两个TextView小部件和两个Button小部件上,选择“填充”然后选择“填充”,并将值设置为10dp

  6. RatingBar小部件上,找到Layout_Margin属性,然后将leftright设置为15sp

  7. 仍然使用RatingBar小部件和Layout_Margin属性,将顶部更改为75sp

现在,您可以运行应用程序,看到我们的第一个完整布局的全部荣耀,如下所示:

图 4.17 - 改进的布局

图 4.17 - 改进的布局

请注意,您可以使用RatingBar小部件,尽管在关闭应用程序时评分不会保留。

不幸的是,我们应用程序中的按钮还没有做任何事情。让我们现在修复它。

用 Java 代码连接 UI(第二部分)

为了让用户与我们的按钮交互,按照下面的两个步骤使布局中的按钮调用我们的方法:

  1. 选择Load ConstraintLayout文本旁边的按钮。找到onClick属性并将其设置为loadConstraintLayout

  2. 选择Load TableLayout文本旁边的按钮。找到onClick属性并将其设置为loadTableLayout

现在,按钮将调用方法,但loadTableLayout方法内的代码被注释掉以避免错误。随时运行应用程序并检查您是否可以通过单击loadConstraintLayout按钮切换到ConstraintLayout。但是ConstraintLayout目前只有一个Hello World!消息。

现在,我们可以继续构*这个ConstraintLayout

使用 ConstraintLayout 构*精确的 UI

打开我们创*项目时自动生成的ConstraintLayout。它可能已经在编辑器顶部的选项卡中打开了。如果没有,它将在res/layout文件夹中。文件名是activity_main.xml

检查Hello World中的TextView中的 XML。切换回Design选项卡,左键单击TextView以选择它,然后按Delete键将其删除。

现在,我们可以构*一个简单而复杂的 UI。当您想要非常精确地定位 UI 的部分和/或相对于其他部分而不仅仅是线性方式时,ConstraintLayout是最有用的。

添加日历视图

要开始,请查看CalendarView。将CalendarView拖放到顶部并水*居中。当您拖动CalendarView时,请注意它会跳到某些位置。

另外,注意显示视图对齐时的微妙视觉提示。我在下面的截图中突出显示了水*中心的视觉提示:

图 4.18 - 水*中心的视觉提示

图 4.18 - 水*中心的视觉提示

当它水*居中时,就像前面的截图中一样,松开。现在,我们将调整它的大小。

在 ConstraintLayout 中调整视图大小

左键单击并按住CalendarView释放时显示的角落中的一个,向内拖动以减小CalendarView的大小,如下面的截图所示:

图 4.19 - 在 ConstaintLayout 中调整视图大小

图 4.19 - 在 ConstaintLayout 中调整视图大小

将大小减少约一半,将CalendarView留在顶部并水*居中。在调整大小后,您可能需要重新定位小部件。结果应该是这样的:

图 4.20 - 重新定位小部件

图 4.20 - 重新定位小部件

您不需要将CalendarView放在完全相同的位置,因为练*的目的是熟悉通知您放置位置的视觉提示,而不是创*我的布局的复制品。

使用 Component Tree 窗口

现在,看看Component Tree窗口,即可视化设计师左侧和调色板下方的窗口。组件树是一种可视化 XML 布局的方式,但没有所有细节。

如果您没有看到这个,请在Palette选项卡底部找到一个标签,上面写着垂直的 Component Tree。单击它以打开Component Tree选项卡。

在下一个截图中,我们可以看到CalendarView向右缩进到ConstraintLayout的右侧,因此是一个子级。在我们构*的下一个 UI 中,我们将看到有时需要利用Component Tree来构* UI。

现在,我只是想让您看到我们的CalendarView旁边有一个警告标志。我在这里突出显示了它:

图 4.21 - 我们的 CalendarView 旁边的警告标志

图 4.21 - 我们的 CalendarView 旁边的警告标志

错误提示此视图没有约束。它只有设计时位置,因此在运行时会跳转到(0,0),除非你添加约束。还记得当我们在*第二章**中首次向屏幕添加按钮时,它们只是移动到左上角吗?

提示

现在运行应用程序,如果你想提醒自己这个问题,请点击加载 ConstraintLayout按钮。

现在,我们可以通过点击我们在*第二章中使用的推断约束**按钮来修复这个问题。这里再次提醒一下:

图 4.22 - 点击推断约束按钮修复错误

图 4.22 - 点击推断约束按钮修复错误

但是学会手动添加约束是值得的,因为它为我们提供了更多的选项和灵活性。而且,当你的布局变得更加复杂时,总会有一两个项目的行为不如你所愿,手动修复几乎总是必要的。

手动添加约束

确保选择了 CalendarView,并注意下图中顶部、底部、左侧和右侧的四个小圆圈:

图 4.23 - 四个约束手柄

图 4.23 - 四个约束手柄

这些是 CalendarView 与屏幕的四个边缘,当应用程序运行时,我们可以将其锁定在位置上。

依次点击并拖动顶部手柄到设计的顶部,右侧手柄到设计的右侧,底部手柄到底部,左侧手柄到左侧。注意当我们应用这些约束时,CalendarView 是如何跳动的?这是因为设计师在我们放置约束时立即应用约束,所以我们可以看到它在运行时的实际效果。

注意,CalendarView 现在在中间约束了。左键单击并拖动 CalendarView 回到屏幕的上部,位置大致如下截图所示。使用视觉线索(也显示在下一个截图中)确保 CalendarView 水*居中:

图 4.24 - 使 CalendarView 水*居中

图 4.24 - 使 CalendarView 水*居中

在这个阶段,你可以运行应用程序,CalendarView 将被定位为前面截图中所示。

让我们向 UI 添加更多项目并看看如何约束它们。

添加和约束更多的 UI 元素

从 CalendarView 中拖动一个 ImageView。当你放置 ImageView 时,会弹出一个窗口提示你选择一张图片。你可以选择任何你喜欢的图片,但是为了看到另一个 Android Studio 功能的效果,请选择列表顶部的图片Avatar,在示例数据部分。点击确定将图片添加到布局中。我们将在接下来的使文本可点击部分看到从示例数据部分添加图片对我们项目的影响。

将 ImageView 的左侧和底部分别约束到 UI 的左侧和底部。现在你应该处于这个位置:

图 4.25 - 将 ImageView 的左侧和底部分别约束到 UI 的左侧和底部

图 4.25 - 将 ImageView 的左侧和底部分别约束到 UI 的左侧和底部

ImageView 在左下角被约束。现在,抓住 ImageView 上的顶部约束手柄,并将其拖动到 CalendarView 的底部约束手柄。现在的情况是:

图 4.26 - 编辑的 UI

图 4.26 – 编辑后的 UI

ImageView 只在水*方向上有约束,因此被固定/约束在左侧。它在CalendarView和 UI 底部之间垂直并且等距约束。

接下来,在ImageView的右侧添加一个TextView。将TextView的右侧约束到 UI 的右侧,并将TextView的左侧约束到ImageView的右侧。然后,将TextView的顶部约束到ImageView的顶部,并将TextView的底部约束到 UI 的底部。现在,您将得到类似于以下内容:

图 4.27 – 将 TextView 添加到 UI

图 4.27 – 将 TextView 添加到 UI

重要提示

strings.xml文件中的所有警告,以及有关缺少contentDescription属性的警告。contentDescription属性应用于添加文本描述,以便视力受损的用户可以在应用程序中获得图像的口头描述。为了快速进展ConstraintLayout,我们将忽略这两个警告。我们将在第十八章中更正确地添加字符串资源,本地化,您可以在Android 开发者网站上阅读有关 Android Studio 的辅助功能的信息,网址为developer.android.com/studio/intro/accessibility

您可以移动三个 UI 元素并将它们整齐地排列,就像您想要的那样。请注意,当您移动ImageView时,TextView也会随之移动,因为TextView受到ImageView的约束。但也请注意,您可以独立移动TextView,无论您放在哪里,这都代表了它相对于ImageView的新约束位置。无论一个项目受到什么约束,它的位置始终是相对于该项目的。正如我们所见,水*和垂直约束是彼此独立的。我将我的位置放在这里:

图 4.28 – 定位 TextView

图 4.28 – 定位 TextView

提示

ConstraintLayout可以被认为是默认的布局类型,虽然它比其他布局更复杂,但它是最强大的,也是在我们用户设备上运行最好的。值得花时间查看一些关于ConstraintLayout的更多教程。特别是在YouTube上查看,因为视频是学*调整ConstraintLayout的好方法。我们将在整本书中回到ConstraintLayout,而且您不需要知道比我们已经涵盖的更多内容才能继续前进。

使文本可点击

我们的ConstraintLayout快要完成了。我们只想要连接回主菜单屏幕的链接。这是一个很好的机会来演示TextView(以及大多数其他 UI 项)也是可点击的。事实上,可点击的文本在现代 Android 应用程序中可能比传统的按钮更常见。

TextView小部件的text属性更改为“返回菜单”。现在,找到onClick属性,并输入loadMenuLayout

现在,在MainActivity.java文件中,在loadTableLayout方法之后添加以下方法,如下所示:

public void loadTableLayout(View v){
   //setContentView(R.layout.my_table_layout);
}
public void loadMenuLayout(View v){
   setContentView(R.layout.main_menu);
}

现在,每当用户点击时,loadMenuLayout方法将被调用,并且setContentView方法将在main_menu.xml中加载布局。

您可以运行应用程序,并在主菜单(LinearLayout)和CalendarView小部件(ConstraintLayout)之间来回点击,但您是否注意到以下截图中的图像似乎丢失了?

图 4.29 – UI 中缺少的图像

图 4.29 – UI 中缺少的图像

TextView小部件被整齐地定位,就好像ImageView小部件在那里一样,但是从前面的屏幕截图中可以看到,Android Studio 中可见的头像图标已经消失了。这是因为我们选择了示例数据类别中的图像。Android Studio 允许我们使用示例数据,这样我们就可以在图像可用之前继续布局我们的应用程序。这很有用,因为在应用程序的开发生命周期中通常需要制作/获取图像。

为了解决这个缺失图像的问题,我们可以在Attributes窗口中找到srcCompat属性,左键单击它,并选择不是来自sym_def_app_icon的任何图像,这是下一个酷炫的 Android 图标:

图 4.30 - 从 srcCompat 属性添加图标

图 4.30 - 从 srcCompat 属性添加图标

让我们为本章构*最终布局。

使用 TableLayout 布局数据

现在,我们将构*一个类似电子表格的布局。它将具有整齐对齐的带有标题和数据的单元格。在真实的应用程序中,您很可能会使用来自用户的真实实时数据。由于我们只是练*不同的布局,我们不会走到这一步。

按照以下步骤:

  1. 在项目窗口中,展开res文件夹。现在,右键单击layout文件夹,然后选择New。注意到有一个Layout resource file选项。

  2. 选择Layout resource file,然后您将看到New Resource File对话框窗口。

  3. my_table_layout中。这与我们在loadTableLayout方法中对setContentView的调用中使用的名称相同。

  4. 注意ConstraintLayout中的TableLayout

  5. 单击 XML 文件中的TableLayout,命名为my_table_layout,并将其放在layout文件夹中,准备构*我们的基于表格的新 UI。如果 UI 设计师尚未打开,Android Studio 还将打开 UI 设计师,左侧是调色板,右侧是Attributes窗口。

  6. 现在,您可以取消注释MainActivity.java文件中的loadTableLayout方法,如下所示:

void loadTableLayout(View v){
   setContentView(R.layout.my_table_layout);
}

现在,当您运行应用程序时,可以切换到具有新的TableLayout小部件的屏幕,尽管目前它是空白的,并且没有办法切换回主菜单屏幕;因此,您将不得不退出应用程序。

向 TableLayout 添加一个 TableRow 元素

Layouts类别中拖放一个TableRow元素到 UI 设计中。请注意,这个新的TableRow元素的外观几乎是不可察觉的,以至于不值得在书中放置它的屏幕截图。UI 顶部只有一条蓝线。这是因为TableRow元素已经将其内容折叠在一起,而这些内容目前是空的。

我们可以将所选的 UI 元素拖放到这条蓝线上,但这也有点麻烦,甚至有点违反直觉。此外,一旦我们在一起有多个TableRow元素,情况就会变得更加困难。解决方案在于ConstraintLayout

使用组件树进行可视化设计师无法完成的布局

TableRow视为TableLayout的子元素。我们可以直接将新的 UI 小部件拖放到TableRow中,在Component Tree中的TextView小部件拖放到TableRow中,这样就会得到以下布局。我已经使用 Photoshop 修改了下一个屏幕截图,以便同时显示Component Tree和常规 UI 设计师:

图 4.31 - 将三个 TextView 小部件拖放到组件树中的 TableRow 元素上

图 4.31 - 将三个 TextView 小部件拖放到组件树中的 TableRow 元素上

现在,添加另外两个TableRow小部件(来自Layouts类别)。您可以通过Component Tree窗口或主 UI 设计师添加它们。

提示

您需要将小部件放在窗口的最左侧,否则新的TableRow将成为上一个TableRow的子级。这将使整个表格变得有些混乱。如果您意外地将TableRow添加为上一个TableRow的子级,您可以选择它,然后点击删除键,使用Ctrl + z键组合进行撤消,或者将位置错误的TableRow拖到左侧(在Table中而不是TableRow)。

现在,向每个新的TableRow元素添加三个TextView小部件。最简单的方法是通过组件树窗口添加它们。检查您的布局,确保它是这样的:

图 4.32 – 向每个新的 TableRow 元素添加三个 TextView 小部件

图 4.32 – 向每个新的 TableRow 元素添加三个 TextView 小部件

让我们通过更改一些属性使表格看起来更像您可能在应用程序中获得的真实数据表。

TableLayout上,将layout_widthlayout_height属性设置为wrap_content。这样可以去掉多余的单元格

通过编辑textColor属性,将所有外部(顶部和左侧)的TextView小部件更改为黑色。您可以通过选择第一个TextView,搜索其color属性,然后开始在color属性值字段中输入black来实现这一点。然后,您将能够从下拉列表中选择@android:color/black。对每个外部TextView小部件都这样做。

提示

请记住,您可以通过按住Shift键并依次左键单击每个所需的小部件来同时选择多个小部件。

找到所有TextView小部件的padding类别,并将padding属性更改为10sp

下一个截图显示了此时在 Android Studio 中表格的样子:

图 4.33 – Android Studio 中的表格

图 4.33 – Android Studio 中的表格

让我们为表格添加一些最后的修饰。

组织表格列

此时似乎我们已经完成了,但是我们需要更好地组织数据。我们的表格,就像许多表格一样,将在左上角有一个空白单元格来分隔列和行标题。为了实现这一点,我们需要对所有单元格进行编号。为此,我们需要编辑layout_column属性。

提示

单元格编号从左边开始从 0 开始。

首先删除左上角的TextView。注意右侧的TextView已经移动到左上角位置。

接下来,在新的左上角TextView中,编辑layout_column属性为1(这将其分配给第二个单元格,因为第一个单元格是0,我们希望将第一个单元格留空),并且对于下一个单元格,编辑layout_column属性为2

结果应该是这样的:

图 4.34 – 表列组织

图 4.34 – 表列组织

对于下面两行单元格,从左到右将它们的layout_column属性从0更改为2

如果您在编辑后想要对此行的精确代码进行澄清,请参阅以下代码片段,并记得在第四章文件夹中的下载包中查看整个文件的上下文。

重要提示

text attributes throughout.
<TableRow
   android:layout_width="wrap_content"
   android:layout_height="wrap_content">
   <TextView
         android:id="@+id/textView2"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_column="1"
         android:padding="10sp"
         android:text="India"
         android:textColor="@android:color/black" />
   <TextView
         android:id="@+id/textView1"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_column="2"
         android:padding="10sp"
         android:text="England"
         android:textColor="@android:color/black" />
</TableRow>

尝试完成这个练*,如果可能的话,使用属性窗口。

链接回主菜单

最后,对于这个布局,我们将添加一个按钮,链接回菜单,如下所示:

  1. 通过组件树添加另一个TableRow

  2. 将一个按钮拖到新的TableRow上。

  3. 将其layout_column属性编辑为1,使其位于行的中间。

  4. 将其text属性编辑为Menu,并将其onClick属性编辑为匹配我们已经存在的loadMenuLayout方法。

现在,您可以运行应用程序并在不同的布局之间来回切换。

如果你愿意,你可以通过编辑 TextView 的所有text属性向表格添加一些有意义的标题和数据,就像我在下面的截图中所做的那样,它展示了在模拟器中运行的 TableLayout:

图 4.35 - 通过编辑 TextView 的所有文本属性向表格添加一些重要的标题和数据

图 4.35 - 通过编辑 TextView 的所有文本属性向表格添加一些重要的标题和数据

注意

作者承认,前述数据可能被认为过于乐观。

最后,想想一个展示数据表的应用程序。很可能数据将动态地添加到表中,而不是由开发人员在设计时像我们刚刚做的那样,而更可能是由用户或来自网络上的数据库添加。在第十六章适配器和回收器中,我们将看到如何使用适配器动态地向不同类型的布局添加数据;在第二十七章Android 数据库中,我们还将看到如何在我们的应用程序中创*和使用数据库。

总结

我们在短短几十页中涵盖了许多主题。我们不仅构*了三种不同类型的布局,包括带有嵌套布局的LinearLayout,手动配置约束的ConstraintLayout,以及TableLayout(尽管使用的是虚假数据),而且还通过可点击的按钮和文本将所有布局连接在一起,触发我们的 Java 代码在所有这些不同的布局之间切换。

在下一章中,我们将继续讨论布局的主题。我们将回顾我们已经见过的许多属性,并且通过将多个CardView布局整合到一个*滑滚动的ScrollView布局中,构*迄今为止最美观的布局。

第五章:使用 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 教程网站 http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html。请注意,您不需要比我们已经讨论过的更多信息来继续阅读本书。

正如我们刚刚学到的,我们可能想要存储的每种数据类型都需要特定数量的内存。因此,我们必须在开始使用变量之前让 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 方法,这是我们所有代码的所在地。

第九章:学* Java 方法

随着我们开始逐渐熟悉 Java 编程,在本章中,我们将更仔细地研究方法,因为尽管我们知道可以调用它们来执行它们的代码,但它们比我们迄今讨论的更多。

在本章中,我们将研究以下内容:

  • 方法结构

  • 方法重载与覆盖

  • 一个方法演示迷你应用程序

  • 方法如何影响我们的变量

  • 方法递归

首先,让我们快速回顾一下方法。

技术要求

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

方法重温

这张图总结了我们对方法的理解:

图 9.1 – 理解方法

图 9.1 – 理解方法

正如我们在图中所看到的,关于方法还有一些疑问。我们将彻底揭开方法的面纱,看看它们是如何工作的,以及方法的其他部分到底在做什么,这将在本章后面讨论。在第十章**,面向对象编程第十一章**,更多面向对象编程中,我们将在讨论面向对象编程时澄清方法的最后几个部分的神秘之处。

究竟什么是 Java 方法?

方法是一组变量、表达式和控制流语句,它们被捆绑在一个以签名为前缀的大括号内。我们已经使用了很多方法,但我们还没有仔细地看过它们。

让我们从方法结构开始。

方法结构

我们编写的方法的第一部分称为签名。这是一个假设的方法签名:

public boolean addContact(boolean isFriend, string name)

如果我们添加一对大括号{},并加入一些方法执行的代码,那么我们就有了一个完整的方法——一个定义。这是另一个虚构的但在语法上是正确的方法:

private void setCoordinates(int x, int y){
   // code to set coordinates goes here
}

正如我们所见,我们可以从代码的其他部分使用我们的新方法,如下所示:

// I like it here
setCoordinates(4,6);// now I am going off to setCoordinates method
// Phew, I'm back again - code continues here

在我们调用setCoordinates方法的时候,我们程序的执行将分支到该方法内部包含的代码。该方法将逐步执行其中的所有语句,直到达到结束,然后将控制返回给调用它的代码,或者如果它遇到return语句,则更早地返回。然后,代码将从方法调用后的第一行继续运行。

这是另一个完整的方法示例,包括使方法返回到调用它的代码的代码:

int addAToB(int a, int b){
   int answer = a + b;
   return answer;
}

使用前面方法的调用可能如下所示:

int myAnswer = addAToB(2, 4); 

显然,我们不需要编写方法来将两个int变量相加,但这个例子帮助我们更深入地了解方法的工作原理。首先,我们传入值24。在方法签名中,值2被赋给int a,值4被赋给int b

在方法体内,变量ab被相加,并用于初始化新变量int answer。行return answer将存储在answer中的值返回给调用代码,导致myAnswer被初始化为值6

请注意,前面示例中的每个方法签名都有所不同。这是因为 Java 方法签名非常灵活,允许我们精确地构*我们需要的方法。

方法签名确切地定义了方法如何被调用以及方法如何返回值,这值得进一步讨论。

让我们给签名的每个部分起一个名字,这样我们就可以把它分成几部分,并了解这些部分。

以下加粗的文本是一个方法签名,其部分已经标记好以便讨论。另外,看一下接下来的表格,以进一步澄清签名的哪一部分是哪一部分。这将使我们对方法的讨论更加简单明了:

修饰符 | 返回类型 | 方法名称(参数)

修饰符

在我们之前的例子中,我们只在一些例子中使用了修饰符,部分原因是方法不必使用修饰符。修饰符是一种指定代码可以使用(调用)你的方法的方式,通过使用publicprivate等修饰符。

变量也可以有修饰符,如下所示:

// Most code can see me
public int a;
// Code in other classes can't see me
private String secret = "Shhh! I am private";

修饰符(用于方法和变量)是一个重要的 Java 主题,但最好在我们讨论其他重要的 Java 主题时处理,这些主题我们已经在几次中绕过了 - 类。我们将在下一章中这样做。

返回类型

表中的下一个是返回类型。像修饰符一样,返回类型是可选的。所以,让我们仔细看一下。我们已经看到我们的方法可以做任何我们可以用 Java 编码的事情。但是如果我们需要方法所做的结果呢?到目前为止,我们所见过的返回类型的最简单的例子如下:

int addAToB(int a, int b){
   int answer = a + b;
   return answer;
}

在这里,签名中的返回类型被突出显示。返回类型是intaddAToB方法将一个值返回给调用它的代码,这个值将适合在一个int变量中。

返回类型可以是我们到目前为止所见过的任何 Java 类型。然而,方法不一定要返回一个值。当方法不返回值时,签名必须使用void关键字作为返回类型。当使用void关键字时,方法体不得尝试返回一个值,否则会导致编译错误。但是,它可以使用没有值的return关键字。

以下是一些返回类型和使用return关键字的有效组合:

void doSomething(){
   // our code
   // I'm done going back to calling code here
   // no return is necessary
}

另一个组合如下:

void doSomethingElse(){
   // our code
   // I can do this as long as I don't try and add a value
   return;
}

以下代码是另一个组合:

void doYetAnotherThing(){
   // some code
   if(someCondition){

         // if someCondition is true returning to calling 
         code 
         // before the end of the method body
         return;
   }

   // More code that might or might not get executed

   return;
   /* 
         As I'm at the bottom of the method body 
         and the return type is void, I'm 
         really not necessary but I suppose I make it 
         clear that the method is over.
   */
}
String joinTogether(String firstName, String lastName){
   return firstName + lastName;
}

我们可以依次调用前面的每个方法,如下所示:

// OK time to call some methods
doSomething();
doSomethingElse();
doYetAnotherThing();
String fullName = joinTogether("Alan ","Turing")
// fullName now = Alan Turing
// continue with code from here

前面的代码将依次执行每个方法中的所有代码。

方法的名称

表中的下一个是方法的名称。当我们设计自己的方法时,方法名是任意的。但是惯例是使用清楚解释方法将要做什么的动词。此外,使用名称的第一个单词的第一个字母小写,后面的单词的第一个字母大写的约定。这被称为驼峰命名法,就像我们在学*变量名时学到的那样。考虑下一个例子:

void XGHHY78802c(){
   // code here
}

前面的方法是完全合法的,并且可以工作;然而,让我们看看使用这些约定的三个更清晰的例子:

void doSomeVerySpecificTask(){
   // code here
}
void getMyFriendsList(){
   // code here
}
void startNewMessage(){
   // code here
} 

这样更清晰,因为名称明确表明了方法将要做什么。

让我们来看看方法中的参数。

参数

表中的最后一个主题是参数。我们知道方法可以将结果返回给调用代码。但是如果我们需要从调用代码中方法共享一些数据值呢?

参数允许我们将值发送到被调用的方法中。当我们查看返回类型时,我们已经看到了一个带有参数的例子。我们将看同样的例子,但更仔细地看一下参数:

int addAToB(int a, int b){
   int answer = a + b;
   return answer;
}

在这里,参数被突出显示。参数包含在括号中,(参数放在这里),紧跟在方法名之后。

请注意,在方法体的第一行中,我们使用a + b,就好像它们已经被声明和初始化为变量一样。那是因为它们是。方法签名的参数是它们的声明,调用方法的代码初始化它们,就像下一行代码中突出显示的那样。

int returnedAnswer = addAToB(10,5);

另外,正如我们在之前的例子中部分看到的,我们不仅仅在参数中使用int。我们可以使用任何 Java 类型,包括我们自己设计的类型。

此外,我们也可以混合和匹配类型。我们还可以使用尽可能多的参数来解决我们的问题。一个例子可能会有所帮助。

void addToAddressBook(char firstInitial, 
String lastName, 
String city, 
int age){

   /*
         all the parameters are now living, breathing,
         declared and initialized variables.

         The code to add details to address book goes here.
   */
}

前面的例子将有四个声明和初始化的变量,准备好使用。

现在我们将看看方法主体——放在方法内部的内容。

主体

在我们之前的例子中,我们一直在伪代码中使用注释来描述我们的方法主体,比如以下的注释:

// code here

还有以下的用法:

// some code

addToAddressBook方法也被使用了。

/*
             all the parameters are now living, breathing,
             declared and initialized variables.

             The code to add details to address book goes 
             here.
      */

但我们已经完全知道主体中要做的事情。到目前为止,我们学到的任何 Java 语法都可以在方法的主体中使用。事实上,如果我们回想一下,到目前为止我们在本书中编写的所有代码都已经在一个方法中。

我们接下来可以做的最好的事情是编写一些在主体中执行操作的方法。

使用方法演示应用程序

在这里,我们将快速构*两个应用程序,以进一步探索方法。首先,我们将使用Real World Methods应用程序探索基础知识,然后我们将一窥新主题,Exploring Method Overloading应用程序。

像往常一样,您可以以通常的方式打开已经输入的代码文件。下面的两个方法示例可以在第九章文件夹和Real World MethodsExploring Method Overloading子文件夹中的下载包中找到。

真实世界的方法

首先,让我们创*一些简单的工作方法,包括返回类型参数和完全运作的主体。

要开始,创*一个名为Real World Methods的新 Android 项目,使用MainActivity.java文件,通过在编辑器上方的MainActivity.java标签上单击左键,我们可以开始编码。

首先,将这三个方法添加到MainActivity类中。将它们添加到onCreate方法的闭合大括号}后面。

String joinThese(String a, String b, String c){
   return a + b + c;
}
float getAreaCircle(float radius){
   return 3.141f * radius * radius;
}
void changeA(int a){
   a++;
}

我们添加的第一个方法叫做joinThese。它将返回一个String值,并需要传入三个String变量。在方法主体中,只有一行代码。return a + b + c代码将连接传入的三个字符串,并将连接后的字符串作为结果返回。

下一个方法名为getAreaCircle,接受一个float变量作为参数,然后也返回一个float变量。方法的主体简单地使用了圆的面积公式,结合传入的半径,然后将答案返回给调用代码。3.141末尾的奇怪的f是为了让编译器知道这个数字是float类型的。任何浮点数都被假定为double类型,除非它有尾随的f

第三个和最后一个方法是所有方法中最简单的。请注意,它不返回任何东西;它有一个void返回类型。我们包括了这个方法,以明确一个我们想要记住关于方法的重要观点。但在我们讨论它之前,让我们看看它的实际操作。

现在,在onCreate方法中,在调用setContentView方法之后,添加这段代码,调用我们的三个新方法,然后在 logcat 窗口中输出一些文本:

String joinedString = joinThese("Methods ", "are ", "cool ");
Log.i("joinedString = ","" + joinedString);
float area  = getAreaCircle(5f);
Log.i("area = ","" + area);
int a = 0;
changeA(a);
Log.i("a = ","" + a);

运行应用程序,查看 logcat 窗口中的输出,这里为您提供方便:

joinedString =: Methods are cool
area =: 78.525
a =: 0

在 logcat 输出中,我们可以看到的第一件事是joinedString字符串的值。正如预期的那样,它是我们传入joinThese方法的三个单词的连接。

接下来,我们可以看到getAreaCircle确实计算并返回了基于传入半径的圆的面积。

a变量即使在传入changeA方法后仍保持值0的事实,值得单独讨论。

发现变量范围

输出的最后一行最有趣:a=: 0。在onCreate方法中,我们声明并初始化了int a0,然后调用了changeA方法。在changeA的主体中,我们用代码a++增加了a。然而,在onCreate方法中,当我们使用Log.i方法将a的值打印到 logcat 窗口时,它仍然是0

因此,当我们将a传递给changeA方法时,实际上传递的是存储在a中的,而不是实际变量a。这在 Java 中被称为按值传递。

提示

当我们在一个方法中声明一个变量时,它只能在该方法中被看到。当我们在另一个方法中声明一个变量时,即使它具有完全相同的名称,它也不是同一个变量。变量只在声明它的方法内部具有作用域

对于所有基本变量,将它们传递给方法的工作方式是这样的。对于引用变量,它的工作方式略有不同,我们将在下一章中看到。

重要说明

我已经和一些刚接触 Java 的人谈过这个作用域概念。对一些人来说,这似乎是显而易见的,甚至是自然的。然而,对于其他人来说,这是一个持续困惑的原因。如果你属于后一种情况,不要担心,因为我们将在本章稍后再谈一些关于这个问题的内容,而在未来的章节中,我们将更深入地探讨作用域,并确保它不再是一个问题。

让我们看一个关于方法的另一个实际例子,并同时学到一些新东西。

探索方法重载

随着我们开始意识到,方法作为一个主题是相当深入的。但希望通过一步一步地学*,我们会发现它们并不令人畏惧。我们也将在下一章回到方法。现在,让我们创*一个新项目来探索方法重载的主题。

创*一个新的探索方法重载,然后我们将继续编写三个方法,但稍微有些不同。

正如我们将很快看到的,我们可以创*多个具有相同名称的方法,只要参数不同。这个项目中的代码很简单。它的工作方式可能看起来有点奇怪,直到我们分析它之后。

在第一个方法中,我们将简单地称之为printStuff,并通过参数传递一个int变量进行打印。

将此方法插入在onCreate方法的}之后,但在MainActivity类的}之前。记得以通常的方式导入Log类:

void printStuff(int myInt){
   Log.i("info", "This is the int only version");
   Log.i("info", "myInt = "+ myInt);
}

在第二个方法中,我们还将称之为printStuff,但传入一个String变量进行打印。将此方法插入在onCreate方法的}之后,但在MainActivity类的}之前:

void printStuff(String myString){
   Log.i("info", "This is the String only version");
   Log.i("info", "myString = "+ myString);
}

在这第三个方法中,我们将再次称之为printStuff,但传入一个String变量和一个int值进行打印。将此方法插入在onCreate}之后,但在MainActivity类的}之前:

void printStuff(int myInt, String myString){
   Log.i("info", "This is the combined int and String 
   version");
   Log.i("info", "myInt = "+ myInt);
   Log.i("info", "myString = "+ myString);
}

最后,在onCreate方法的}之前插入这段代码,以调用方法并将一些值打印到 logcat 窗口:

// Declare and initialize a String and an int
int anInt = 10;
String aString = "I am a string";

// Now call the different versions of printStuff
// The name stays the same, only the parameters vary
printStuff(anInt);
printStuff(aString);
printStuff(anInt, aString);

现在我们可以在模拟器或真实设备上运行应用程序。这是输出:

Info: This is the int only version
Info: myInt = 10
Info: This is the String only version
Info: myString = I am a string
Info: This is the combined int and String version
Info: myInt = 10
Info: myString = I am a string

正如你所看到的,Java 将具有相同名称的三个方法视为不同的方法。正如我们刚刚展示的那样,这是有用的。这被称为方法重载

方法重载和覆盖的混淆

重载是指当我们有多个具有相同名称但不同参数的方法时。

覆盖是指用相同的名称和参数列表替换一个方法。

我们对重载和覆盖已经了解足够,可以完成这本书;但如果你勇敢而且思绪飘忽,是的,你可以覆盖一个重载的方法,但这是另一个时间的事情。

这就是它的工作原理。在我们编写代码的每个步骤中,我们创*了一个名为printStuff的方法。但是每个printStuff方法都有不同的参数,因此实际上是可以单独调用的不同方法:

void printStuff(int myInt){
   ...
}
void printStuff(String myString){
   ...
}
void printStuff(int myInt, String myString){
   ...
}

每个方法的主体都是微不足道的,只是打印出传入的参数,并确认调用的方法版本。

我们代码的下一个重要部分是当我们明确指出要调用的方法版本,使用与签名中参数匹配的特定参数。在最后一步,我们依次调用每个方法,使用匹配的参数,这样 Java 就知道需要调用的确切方法:

printStuff(anInt);
printStuff(aString);
printStuff(anInt, aString);

现在我们已经知道关于方法的所有需要知道的东西,让我们快速再看一下方法和变量之间的关系。然后,我们会更深入地了解这个作用域现象。

重新访问作用域和变量

你可能还记得在真实世界方法项目中,稍微令人不安的异常是,一个方法中的变量似乎与另一个方法中的变量不同,即使它们有相同的名称。如果你在一个方法中声明一个变量,无论是生命周期方法还是我们自己的方法,它只能在该方法内使用。

如果我们在onCreate中这样做是没有用的:

int a = 0;

然后,在onPause或其他方法中,我们尝试这样做:

a++;

我们会得到一个错误,因为a只在它声明的方法中可见。起初,这可能看起来像是一个问题,但令人惊讶的是,这实际上是 Java 的一个特别有用的特性。

我已经提到用来描述这一点的术语是作用域。当变量可用时,就说它在作用域内,当不可用时,就说它不在作用域内。作用域的主题最好与类一起讨论,我们将在第十章**,面向对象编程第十一章**,更多面向对象编程中这样做,但是作为对未来的一瞥,你可能想知道一个类可以有自己的变量,当它有时,它们对整个类都有作用域;也就是说,所有的方法都可以“看到”并使用它们。我们称它们为成员变量或字段

要声明一个成员变量,只需在类的开始之后使用通常的语法,在类中声明的任何方法之外。假设我们的应用程序像这样开始:

public class MainActivity extends AppCompatActivity {

   int mSomeVariable = 0;
   // Rest of code and methods follow as usual
   // ...

我们可以在这个类的任何方法中使用mSomeVariable。我们的新变量mSomeVariable只是为了提醒我们它是一个成员变量,所以在变量名中加上了m。这不是编译器要求的,但这是一个有用的约定。

在我们继续讲解类之前,让我们再看一个方法的主题。

方法递归

方法递归是指一个方法调用自身。这乍一看可能像是一个错误,但实际上是解决一些编程问题的有效技术。

这里有一些代码展示了一个递归方法的最基本形式:

void recursiveMethod() {
     recursiveMethod();
}

如果我们调用recursiveMethod方法,它的唯一代码行将调用自身,然后再调用自身,然后再调用自身,依此类推。这个过程将永远持续下去,直到应用程序崩溃,在 Logcat 中会出现以下错误:

java.lang.StackOverflowError: stack size 8192KB

当方法被调用时,它的指令被移动到处理器的一个区域,称为堆栈,当它返回时,它的指令被移除。如果方法从不返回,而是不断添加更多的指令副本,最终堆栈将耗尽内存(或溢出),我们会得到StackOverflowError

我们可以尝试使用下一个截图来可视化前四个方法调用。此外,在下一个截图中,我划掉了对方法的调用,以显示如果我们能够在某个时刻阻止方法调用,最终所有的方法都将返回并从堆栈中清除:

图 9.2 - 方法调用

图 9.2 - 方法调用

为了使我们的递归方法有价值,我们需要增强两个方面。我们将很快看到第二个方面。首先,最明显的是,我们需要给它一个目的。我们可以让我们的递归方法求和(相加)从 0 到给定目标值(比如 10、100 或更多)范围内的数字的值。让我们通过给它这个新目的并相应地重命名它来修改前面的方法。我们还将添加一个具有类范围(在方法之外)的变量answer

int answer = 0;
void computeSum(int target) {
answer += target;
     computeSum(target-1);
}

现在我们有一个名为computeSum的方法,它以一个int作为参数。如果我们想要计算 0 到 10 之间所有数字的总和,我们可以这样调用该方法:

computeSum(10);

以下是每个函数调用时answer变量的值:

第一次调用computeSumanswer = 10

第二次调用computeSumanswer = 19

第十次调用computeSumanswer = 55

表面上看成功 - 直到你意识到该方法在target变量达到 0 之后仍然继续调用自身。事实上,我们仍然面临着第一个递归方法的相同问题,经过数万次方法调用后,应用程序将再次崩溃并出现StackOverflowError

我们需要一种方法来阻止方法在target等于 0 时继续调用自身。我们解决这个问题的方法是检查target的值是否为 0,如果是,我们就停止调用该方法。看看下面显示的额外突出显示的代码:

void computeSum(int target) {
     answer += target;
     if(target > 0) {
          Log.d("target = ", "" + target);
          computeSum(target - 1);
     }
     Log.d("answer = ", "" + answer);

我们使用if语句来检查目标变量是否大于 0。当方法被最后一次调用时,我们还有额外的Log.d代码来输出answer的值。在阅读输出后的解释之前,看看你能否弄清楚发生了什么。

调用computeSum(10)的输出如下:

target =: 10
target =: 9
target =: 8
target =: 7
target =: 6
target =: 5
target =: 4
target =: 3
target =: 2
target =: 1
answer =: 55

if(target > 0)告诉代码首先检查target变量是否大于 0。如果是,然后才调用方法并传入target - 1的值。如果不是,那么它就停止整个过程。

重要提示

我们不会在本书中使用方法递归,但这是一个有趣的概念需要理解。

我们对方法了解得足够多,可以完成书中的所有项目。让我们通过一些问题和答案进行一个快速回顾。

问题

  1. 这个方法定义有什么问题?
doSomething(){
   // Do something here
}

没有声明返回类型。你不必从方法中返回一个值,但在这种情况下它的返回类型必须是void。方法应该如下所示:

void doSomething(){
   // Do something here
}
  1. 这个方法定义有什么问题?
float getBalance(){
   String customerName = "Linus Torvalds";
   float balance = 429.66f;
   return userName;
}

该方法返回一个字符串(userName),但签名规定它必须返回一个float类型。以getBalance这样的方法名,这段代码可能是原本想要的:

float getBalance(){
   String customerName = "Linus Torvalds";
   float balance = 429.66f;
   return balance;
}
  1. 我们什么时候调用onCreate方法?(提示:这是一个诡计问题!)

我们不需要。Android 决定何时调用onCreate方法,以及构成 Activity 生命周期的所有其他方法。我们只覆盖对我们有用的方法。但是,我们会调用super.onCreate,以便我们的重写版本和原始版本都被执行。

重要提示

为了完全披露,从我们的代码中技术上可以调用生命周期方法,但在本书的上下文中我们永远不需要这样做。最好将这些事情留给 Android。

总结

在前五章中,我们对各种小部件和其他 UI 元素变得相当熟练。我们还构*了广泛的 UI 布局选择。在本章和前三章中,我们已经相当深入地探索了 Java 和 Android 活动生命周期,尤其是考虑到我们完成得多么快。

我们在一定程度上创*了 Java 代码和 UI 之间的交互。我们通过设置onClick属性调用了我们的方法,并使用setContentView方法加载了我们的 UI 布局。然而,我们并没有真正*立 UI 和 Java 代码之间的适当连接。

我们现在真正需要做的是将这些东西结合起来,这样我们就可以开始使用 Android UI 来显示和操作我们的数据。为了实现这一点,我们需要更多地了解类的知识。

自从《第一章》《开始 Android 和 Java》以来,类一直潜伏在我们的代码中,我们甚至有点使用它们。然而,直到现在,除了不断地参考《第十章》《面向对象编程》之外,我们还没有适当地解决它们。在下一章《第十章》《面向对象编程》中,我们将快速掌握类的知识,然后我们终于可以开始构*应用程序,使 UI 设计和我们的 Java 代码完美地协同工作。

进一步阅读

我们已经学到了足够的 Java 知识来继续阅读本书。然而,看到更多 Java 实例并超越最低必要的知识总是有益的。如果你想要一个学* Java 更深入的好资源,那么官方的 Oracle 网站是一个不错的选择。请注意,您不需要学*这个网站来继续阅读本书。另外,请注意,Oracle 网站上的教程并不是在 Android 环境中设置的。该网站是一个有用的资源,可以收藏并浏览:

第十章:面向对象编程

在本章中,我们将发现在 Java 中,类对几乎所有事情都是基础的。我们还将开始理解为什么 Sun Microsystems 的软件工程师在 20 世纪 90 年代初让 Java 成为现在这个样子。

我们已经谈论了重用其他人的代码,特别是 Android API,但在本章中,我们将真正掌握这是如何工作的,并学*面向对象编程以及如何使用它。

总之,我们将涵盖以下主题:

  • 面向对象编程是什么,包括封装继承多态

  • 在应用程序中编写和使用我们的第一个类

技术要求

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

重要的内存管理警告

我是在提到我们大脑的记忆。如果你试图记住本章(或下一章),你将不得不在你的大脑中腾出很多空间,你可能会忘记一些非常重要的事情——比如去工作或感谢作者告诉你不要试图记住这些东西。

一个很好的目标将是尽量接近理解。这样,你的理解将变得更全面。然后在需要时,你可以参考本章(或下一章)进行复*。

提示

如果你对本章的内容不完全理解也没关系!继续阅读并确保完成所有的应用程序。

面向对象编程

第一章**,开始 Android 和 Java中,我们提到 Java 是一种面向对象的语言。面向对象的语言要求我们使用面向对象 编程(OOP)。这不像汽车上的赛车扰流板或游戏 PC 上的跳动 LED 那样是可选的额外部分。它是 Java 的一部分,因此也是 Android 的一部分。

让我们多了解一点。

OOP 究竟是什么?

面向对象编程是一种将我们的需求分解为比整体更易管理的块的编程方式。

每个块都是自包含的,但也可能被其他程序重复使用,同时与其他块一起工作。

这些块是我们所说的对象。当我们计划/编写一个对象时,我们使用一个类。一个类可以被看作是一个对象的蓝图。

我们实现一个类的对象。这被称为类的一个实例。想象一下房屋蓝图。你不能住在里面,但你可以从中*造一座房子;你*造它的一个实例。通常当我们为我们的应用设计类时,我们写它们来代表现实世界的事物。

然而,面向对象编程不仅仅是这样。它也是一种做事情的方式——一种定义最佳实践的方法。

面向对象编程的三个核心原则是封装多态继承。这听起来可能很复杂,但一步一步来,是相当简单的。

封装

封装意味着保持代码的内部工作不受使用它的代码的干扰,只允许访问你选择的变量和方法。

这意味着你的代码总是可以更新、扩展或改进,而不会影响使用它的程序,只要暴露的部分仍然以相同的方式访问。

还记得第一章**,开始 Android 和 Java中的这行代码吗?

locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)

通过适当的封装,卫星公司或 Android API 团队需要更新他们的代码工作方式都无关紧要。只要getLastKnownLocation方法签名保持不变,我们就不必担心内部发生了什么。我们在更新之前编写的代码在更新后仍将正常工作。

如果汽车制造商去掉车轮,将其改为电动悬浮汽车,只要它仍然有方向盘、油门和刹车踏板,驾驶它应该不会太具有挑战性。

重要提示

当我们使用 Android API 的类时,我们是按照 Android 开发人员设计他们的类的方式来使用的。

多态性

多态性允许我们编写的代码对我们试图操作的类型不那么依赖,使我们的代码更清晰、更高效。多态性意味着不同的形式。如果我们编写的对象可以是多种类型的东西,那么我们就可以利用这一点。下一章中的一些例子将说明这一点,接下来的类比将让您从现实世界的角度看清楚。

如果我们有一个汽车工厂,只需改变给机器人的指令和放在生产线上的零件,就可以制造货车和小型卡车,那么这个工厂就是在使用多态性。

如果我们能够编写能够处理不同类型数据的代码而不必重新开始,那不是很有用吗?我们将在《第十一章》[更多面向对象编程]中看到一些例子。

继承

就像听起来的那样,extends关键字:

public class MainActivity extends AppCompatActivity {

AppCompatActivity类本身继承自Activity。因此,每次我们创*一个新的 Android 项目时,我们都是从Activity继承的。我们可以进一步了解它的有用之处。

想象一下,如果世界上最强壮的男人和最聪明的女人在一起。他们的孩子很可能会从基因继承中获得重大好处。在 Java 中,继承让我们可以用另一个人的代码和我们自己的代码做同样的事情,创*一个更符合我们需求的类。

为什么要这样做?

当正确编写时,所有这些面向对象编程允许您添加新功能,而不必过多担心它们如何与现有功能交互。当您确实需要更改一个类时,它的自包含(封装)性质意味着对程序的其他部分的影响会更少,甚至可能为零。这就是封装的部分。

您可以使用其他人的代码(例如 Android API),而不必知道或甚至关心它是如何工作的:想想一下 Android 生命周期、ToastLog、所有 UI 小部件、监听卫星等等。例如,Button类有近 50 个方法-我们真的想要为一个按钮自己编写所有这些吗?最好使用别人的Button类。

面向对象编程允许您轻松编写高度复杂的应用程序。

通过继承,您可以创*多个类似但不同的类的版本,而无需从头开始编写类;并且您仍然可以使用原始类型对象的方法来处理新对象,这是由于多态性。

这真的很有道理。Java 从一开始就考虑到了所有这些,所以我们被迫使用所有这些面向对象编程;然而,这是一件好事。

让我们快速回顾一下类。

类回顾

类是一组代码行,可以包含方法、变量、循环和我们学过的所有其他 Java 语法。类是 Java 包的一部分,大多数包通常会有多个类。通常情况下,每个新类都将在其自己的.java代码文件中定义,文件名与类名相同-就像我们迄今为止所有的 Activity 类一样。

一旦我们编写了一个类,我们可以使用它来制作任意数量的对象。记住,类是蓝图,我们根据蓝图制作对象。房子不是蓝图,就像对象不是类一样;它是从类制作的对象。对象是一个引用变量,就像String变量一样,稍后我们将确切了解引用变量的含义。现在,让我们看一些实际的代码。

查看类的代码

假设我们正在为军方制作一个应用程序。这是供高级军官在战斗中微观管理他们的部队使用的。除其他外,我们可能需要一个代表士兵的类。

类的实现

这是我们假想类的真实代码。我们称之为一个类Soldier,如果我们真的实现了这个,我们会在一个名为Soldier.java的文件中这样做:

public class Soldier {

   // Member variables
   int health;
   String soldierType;
   // Method of the class
   void shootEnemy(){
             // Bang! Bang!
   }

}

上述是一个名为Soldier的类实现。有两个名为health的 int 变量,以及一个名为soldierType的 String 变量。

还有一个名为shootEnemy的方法。该方法没有参数,返回类型为void,但类方法可以是我们在*第九章**中讨论的任何形状或大小的方法。

要准确地了解成员变量和字段,当类被实例化为一个真实对象时,字段将成为对象本身的变量,我们称它们为实例成员变量。

它们只是类的变量,无论它们被引用的名称有多么花哨。然而,字段和方法中声明的变量(称为局部变量)之间的区别随着我们的进展变得更加重要。

我们在第九章**结尾简要讨论了变量作用域,学* Java 方法我们将在下一章再次看到所有类型的变量。让我们集中精力编写和使用一个类。

声明、初始化和使用类的对象

记住,Soldier只是一个类,不是一个实际可用的对象。它是一个士兵的蓝图,而不是一个实际的士兵对象,就像intStringboolean不是变量一样;它们只是我们可以制作变量的类型。这是我们如何从我们的Soldier类中制作一个类型为Soldier的对象:

Soldier mySoldier = new Soldier();

在代码的第一部分中,Soldier mySoldier声明了一个名为mySoldier的类型为Soldier的新变量。代码的最后一部分new Soldier()调用了一个特殊的方法,称为构造方法,这个方法由编译器为所有类自动生成。

正是这个构造方法创*了一个实际的Soldier对象。正如你所看到的,构造方法的名称与类的名称相同。我们将在本章后面更深入地研究构造函数。

当然,两部分中间的赋值运算符=将第二部分的结果分配给第一部分的结果。下一张图总结了所有这些信息:

图 10.1 - 声明、初始化和使用类的对象

图 10.1 - 声明、初始化和使用类的对象

这与我们处理常规变量的方式并不相距太远,只是构造函数/方法调用而不是代码行末的值。要创*和使用一个非常基本的类,我们已经做得足够多了。

重要说明

正如我们将在进一步探讨时看到的,我们可以编写自己的构造函数,而不是依赖于自动生成的构造函数。这给了我们很多力量和灵活性,但现在我们将继续探讨最简单的情况。

就像普通变量一样,我们也可以像这样分两部分完成。

Soldier mySoldier;
mySoldier = new Soldier();

这是我们可能分配和使用假想类的变量的方式:

mySoldier.health = 100;
mySoldier.soldierType = "sniper";
// Notice that we use the object name mySoldier.
// Not the class name Soldier.
// We didn't do this:
// Soldier.health = 100; 
// ERROR!

在这里,点运算符.用于访问类的变量。这就是我们调用方法的方式 - 再次,通过使用对象名称,而不是类名称,后跟点运算符:

mySoldier.shootEnemy();

我们可以用图表总结点运算符的使用:

图 10.2 - 点运算符

图 10.2 - 点运算符

提示

我们可以将类的方法视为它可以的事情,将其实例/成员变量视为它了解自身的事情。

我们也可以继续制作另一个Soldier对象并访问它的方法和变量:

Soldier mySoldier2 = new Soldier();
mySoldier2.health = 150;
mySoldier2.soldierType = "special forces";
mySoldier2.shootEnemy();

重要的是要意识到mySoldier2是一个完全独立的对象,具有完全不同的实例变量,与mySoldier不同:

图 10.3 - 士兵对象

图 10.3 - 士兵对象

这里还有一个关键点,即前面的代码不会在类本身内部编写。例如,我们可以在名为Soldier.java的外部文件中创*Soldier类,然后使用我们刚刚看到的代码,可能在MainActivity类中。

当我们在一分钟内在实际项目中编写我们的第一个类时,这将变得更加清晰。

还要注意,所有操作都是在对象本身上进行的。我们必须创*类的对象才能使它们有用。

重要提示

像往常一样,这个规则也有例外。但它们是少数,我们将在下一章中看到这些例外。实际上,到目前为止,我们已经看到了两个例外。我们已经看到的例外是ToastLog类。它们的具体情况将很快得到解释。

让我们通过编写一个真正的基本类来更深入地探索基本类。

基本类应用

将使用我们的应用程序的将军需要不止一个Soldier对象。在我们即将构*的应用程序中,我们将实例化和使用多个对象。我们还将演示使用变量和方法上的点运算符,以表明不同的对象具有自己的实例变量。

您可以在代码下载中获取此示例的完整代码。它在第十章/Basic Classes文件夹中。但是,继续阅读以创*您自己的工作示例会更有用。

使用Basic Classes创*一个项目。现在我们将创*一个名为Soldier的新类:

  1. 右键单击项目资源管理器窗口中的com.yourdomain.basicclasses(或者您的包名称)文件夹。

  2. 选择New | Java Class

  3. Soldier中按下Enter键。

为我们创*了一个新的类,其中包含一个代码模板,准备将我们的实现放入其中,就像下一个代码所示的那样。

package com.yourdomain.basicclasses;
public class Soldier {
}

注意,Android Studio 将类放在与我们应用程序的其他 Java 文件相同的包/文件夹中。

现在我们可以编写它的实现。

按照所示,在Soldier类的开放和闭合大括号内编写以下类实现代码。要输入的新代码已经高亮显示:

public class Soldier {
    int health;
    String soldierType;
    void shootEnemy(){
        //let's print which type of soldier is shooting
        Log.i(soldierType, " is shooting");
    }
}

现在我们有了一个类,即将来的Soldier类型对象的蓝图,我们可以开始*立我们的军队。在编辑窗口中,单击setContentView方法调用后的onCreate方法。输入以下代码:

// First, we make an object of type soldier
Soldier rambo = new Soldier();
rambo.soldierType = "Green Beret";
rambo.health = 150;
// It takes a lot to kill Rambo
// Now we make another Soldier object
Soldier vassily = new Soldier();
vassily.soldierType = "Sniper";
vassily.health = 50;
// Snipers have less health
// And one more Soldier object
Soldier wellington = new Soldier();
wellington.soldierType = "Sailor";
wellington.health = 100;
// He's tough but no green beret

现在我们有了极其多样化和不太可能的军队,我们可以使用它并验证每个对象的身份。

在上一步中的代码下面输入以下代码:

Log.i("Rambo's health = ", "" + rambo.health);
Log.i("Vassily's health = ", "" + vassily.health);
Log.i("Wellington's health = ", "" + wellington.health);
rambo.shootEnemy();
vassily.shootEnemy();
wellington.shootEnemy();

现在我们可以运行我们的应用程序。所有输出将显示在 logcat 窗口中。

这就是它的工作原理。首先,我们创*了我们的新Soldier类。然后我们实现了我们的类,包括声明两个字段(成员变量),一个int变量和一个名为healthsoldierTypeString变量。

我们的类中还有一个名为shootEnemy的方法。让我们再次看一下,并检查发生了什么:

void shootEnemy(){
        //let's print which type of soldier is shooting
        Log.i(soldierType, " is shooting");
}

在方法的主体中,我们打印到 logcat 窗口:首先是soldierType,然后是文本" is shooting"。这里很棒的是,字符串soldierType会根据我们在shootEnemy方法上调用的对象不同而不同。

接下来,我们声明并创*了三个Soldier类型的新对象。它们是rambovassilywellington

最后,我们为healthsoldierType的每个值初始化了不同的值。

这是输出:

Rambo's health =: 150
Vassily's health =: 50
Wellington's health =: 100
Green Beret: is shooting
Sniper: is shooting
Sailor: is shooting

注意,每次访问每个Soldier对象的health变量时,它都会打印我们分配的值,证明尽管这三个对象是相同类型的,但它们是完全独立的个体实例/对象。

也许更有趣的是对shootEnemy的三次调用。逐个地,我们的Soldier对象的shootEnemy方法被调用,并且我们将soldierType变量打印到 logcat 窗口。该方法对每个单独的对象都有适当的值,进一步证明我们有三个不同的对象(类的实例),尽管它们是从同一个Soldier类创*的。

我们看到每个对象都是完全独立的。然而,如果我们想象我们的应用中有整个军队的Soldier对象,那么我们意识到我们需要学*处理大量对象(以及常规变量)的新方法。

想想管理 100 个独立的Soldier对象。当我们有成千上万的对象时呢?此外,这并不是很动态。我们现在编写代码的方式依赖于我们(开发人员)知道将由将军(用户)指挥的士兵的确切细节。我们将在第十五章**,数组、映射和随机数中看到解决方案。

我们的第一个类还可以做更多的事情

我们可以像处理其他变量一样处理类。我们可以在方法签名中使用类作为参数,就像这样:

public void healSoldier(Soldier soldierToBeHealed){
   // Use soldierToBeHealed here
   // And because it is a reference the changes
   // are reflected in the actual object passed into
   // the method.
   // Oops! I just mentioned what 
   // a reference variable can do
   // More info in the FAQ, chapter 11, and onwards
}

当我们调用方法时,当然必须传递该类型的对象。以下是对healSoldier方法的假设调用:

healSoldier(rambo);

当然,前面的例子可能会引发问题,比如,healSoldier方法应该是一个类的方法吗?

fieldhospital.healSoldier(rambo);

可能是,也可能不是。这将取决于情况的最佳解决方案是什么。我们将看到更多的面向对象编程,然后对许多类似的难题的最佳解决方案应该更容易呈现出来。

而且,你可能会猜到,我们也可以将对象用作方法的返回值。以下是更新后的假设healSoldier签名和实现可能看起来像的样子:

Soldier healSoldier(Soldier soldierToBeHealed){
   soldierToBeHealed.health++;
   return soldierToBeHealed;
}

实际上,我们已经看到类被用作参数。例如,这是我们来自第二章**,初次接触:Java、XML 和 UI 设计师topClick方法。它接收了一个名为vView类型的对象:

public void topClick(View v){

然而,在topClick方法的情况下,我们没有对传入的View类型的对象做任何操作。部分原因是因为我们不需要,部分原因是因为我们不知道可以对View类型的对象做什么 - 至少目前还不知道。

正如我在本章开头提到的,你不需要理解或记住本章的所有内容。掌握面向对象编程的唯一方法就是不断地使用它。就像学*口语一样 - 学*和研究语法规则会有所帮助,但远不及口头交流(或书面交流)来得有用。如果你差不多懂了,就继续下一章吧。

常见问题

  1. 我真的等不及了。引用到底是什么!?

它实际上就是在普通(非编程)语言中的引用。它是一个标识/指向数据的值,而不是实际的数据本身。一个思考它的方式是,引用是一个内存位置/地址。它标识并提供对内存中该位置/地址上的实际数据的访问。

  1. 如果它不是实际的对象,而只是一个引用,那么我们怎么能调用它的方法,比如mySoldier.shootEnemy()呢?

Java 在幕后处理了确切的细节,但你可以把引用看作是对象的控制器,你想对对象做的任何事情都必须通过控制器来做,因为实际的对象/内存本身不能直接访问。关于这一点,第十二章**,栈、堆和垃圾收集器中有更多内容。

总结

我们终于编写了我们的第一个类。我们已经看到我们可以在与类同名的 Java 文件中实现一个类。类本身在我们实例化一个类的对象/实例之前并不起作用。一旦我们有了一个类的实例,我们就可以使用它的变量和方法。正如我们在基本类应用程序中证明的那样,每个类的实例都有自己独特的变量,就像当你买一辆工厂生产的汽车时,你会得到自己独特的方向盘、卫星导航和加速条纹。

所有这些信息都会引发更多的问题。面向对象编程就是这样。因此,让我们尝试通过再次查看变量和封装、多态性以及继承来巩固所有这些类的内容,下一章将展示它们的实际应用。然后我们可以进一步学*类,并探讨静态类(例如 Log 和 Toast)以及抽象类和接口等更高级的概念。

第十一章:更多面向对象编程

本章是我们对面向对象编程的风潮之旅(理论和实践)的第二部分。我们已经简要讨论了封装、继承和多态性的概念,但在本章中,我们将看到它们在一些演示应用程序中更加实际的运用。虽然工作示例将展示这些概念以其最简单的形式,但这仍然是朝着通过我们的 Java 代码控制布局的重要一步。

在本章中,我们将探讨以下内容:

  • 深入了解封装及其帮助我们的方式

  • 深入了解继承及如何充分利用

  • 更详细地解释多态性

  • 静态类及我们已经在使用的方式

  • 抽象类和接口

首先,我们将处理封装。

技术要求

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

还记得封装吗?

到目前为止,我们真正看到的是一种代码组织约定,我们编写类,充满了变量和方法。我们确实讨论了所有这些面向对象编程的更广泛目标,但现在我们将进一步探讨,并开始看到我们如何实际通过面向对象编程实现封装。

封装的定义

封装描述了对象隐藏其数据和方法不让外界访问的能力,只允许访问您选择的变量和方法。这意味着您的代码始终可以更新、扩展或改进,而不会影响使用它的程序——只要暴露的部分仍然以相同的方式可访问。它还允许使用您封装的代码的代码变得更简单、更易于维护,因为任务的大部分复杂性都封装在您的代码中。

但是你不是说我们不需要知道内部发生了什么吗?所以你可能会像这样质疑我们迄今为止所看到的东西:如果我们不断地设置实例变量,比如rambo.health = 100;,难道不可能最终出现问题,比如这样吗?

rambo.soldierType = "fluffy bunny";

封装保护了您的类的对象,使其无法以其不应有的方式使用。通过控制类代码的使用方式,它只能做您想要做的事情,并且具有您可以控制的值范围。

它不会被强制出现错误或崩溃。此外,您可以自由地更改代码的内部工作方式,而不会破坏程序的其余部分或使用旧版本代码的任何程序:

weightlifter.legstrength = 100;
weightlifter.armstrength = 1;
weightlifter.liftHeavyWeight();
// one typo and weightlifter rips own arms off

封装不仅对于编写其他人将使用的代码(例如我们使用的 Android API)至关重要,而且在编写自己将重复使用的代码时也是必不可少的,因为它将使我们免受自己的错误。此外,程序员团队将广泛使用封装,以便团队的不同成员可以在同一程序上工作,而不需要所有团队成员都知道其他团队成员的代码如何工作。我们可以为了获得同样的优势而封装我们的类,以下是如何做到的。

使用访问修饰符控制类的使用

类的设计者控制着任何使用其类的程序所能看到和操作的内容。我们可以像这样添加一个class关键字:

public class Soldier{
   //Implementation goes here
}

类访问修饰符

到目前为止,我们已经讨论了上下文中类的两个主要访问修饰符。让我们依次简要地看一下每一个:

  • public:这很简单。声明为 public 的类可以被所有其他类看到。

  • default:当未指定访问修饰符时,类具有默认访问权限。这将使其对同一包中的类公开,但对所有其他类不可访问。

现在我们可以开始封装这个东西了。但是,即使乍一看,所描述的访问修饰符也不是非常精细。我们似乎只能完全封锁包外的任何东西,或者完全自由。

实际上,这里的好处很容易被利用。想法是设计一个包含一组任务的类包。然后,包的所有复杂内部工作,那些不应该被任何人干扰的东西,应该是默认访问权限(只能被包内的类访问)。然后我们可以提供一些精心选择的公共类,供其他人(或程序的其他不同部分)使用。

重要说明

对于本书中应用程序的大小和复杂性来说,创*多个包是过度的。当然,我们将使用其他人的包和类(公共部分),所以了解这些内容是值得的。

类访问权限总结

一个设计良好的应用程序可能由一个或多个包组成,每个包只包含默认或默认和公共类。

除了类级别的隐私控制之外,Java 还为我们程序员提供了非常精细的控制,但要使用这些控制,我们必须更详细地研究变量。

使用访问修饰符控制变量的使用

为了加强类的可见性控制,我们有变量访问修饰符。这是一个声明了private访问修饰符的变量:

private int myInt;

还要注意,我们对变量访问修饰符的所有讨论也适用于对象变量。例如,这里声明、创*和分配了我们的Soldier类的一个实例。如你所见,这种情况下指定的访问权限是公共的:

public Soldier mySoldier = new Soldier();

在将修饰符应用于变量之前,必须首先考虑类的可见性。如果类a对类b不可见,比如因为类a具有默认访问权限,而类b在另一个包中,那么在类a的变量上使用任何访问修饰符都没有任何影响 - 类b无法看到其中任何一个。

因此,有必要在必要时向另一个类显示一个类,但只公开直接需要的变量 - 而不是所有的变量。

以下是不同变量访问修饰符的解释。

变量访问修饰符

变量访问修饰符比类访问修饰符更多,也更精细。访问修改的深度和复杂性不在于修饰符的范围,而在于我们可以如何巧妙地组合它们以实现封装的可贵目标。以下是变量访问修饰符:

  • public:你猜对了,任何包中的任何类或方法都可以看到这个变量。只有当你确定这就是你想要的时候才使用public

  • protected:这是继public之后的下一个最不限制的。只要它们在同一个包中,protected变量可以被任何类和任何方法看到。

  • defaultdefault听起来不像protected那么限制,但实际上更加限制。当没有指定访问权限时,变量具有default访问权限。default限制的事实或许意味着我们应该考虑隐藏我们的变量,而不是暴露它们。在这一点上,我们需要介绍一个新概念。你还记得我们曾简要讨论过继承以及如何可以快速地继承一个类的属性,然后使用extends关键字对其进行改进吗?只是为了记录,default访问权限的变量对子类是不可见的;也就是说,当我们像对 Activity 一样扩展一个类时,我们无法看到它的默认变量。我们将在本章后面更详细地讨论继承。

  • privateprivate变量只能在声明它们的类内部可见。这意味着,与默认访问权限一样,它们对子类(从所讨论的类继承的类)也是不可见的。

变量访问权限总结

一个设计良好的应用程序可能由一个或多个包组成,每个包只包含defaultdefaultpublic类。在这些类中,变量将具有精心选择和不同的访问修饰符,以实现我们封装目标的目标。

在我们开始实际操作之前,让我们再谈一下所有这些访问修改的东西中的一个小技巧。

方法也可以有访问修饰符

我们已经在第九章**,学* Java 方法中简要提到,方法可以有访问修饰符。这是有道理的,因为方法是我们的类可以的事情。我们将想要控制我们的类的用户可以做什么和不能做什么。

这里的一般想法是,一些方法将只在内部执行操作,因此不需要类的用户,而一些方法将是类的用户使用类的基础。

方法访问修饰符

方法的访问修饰符与类变量的访问修饰符相同。这使得事情容易记住,但再次表明,成功的封装是一种设计问题,而不是遵循任何特定规则的问题。

作为一个例子,只要它在一个公共类中,这种方法可以被任何其他类使用:

public useMeEverybody(){
   //do something everyone needs to do here
}

而这个方法只能被它所属的类内部使用:

private secretInternalTask(){
   /*
         do something that helps the class function 
         internally
         Perhaps, if it is part of the same class,
         useMeEverybody could use this method...
         On behalf of the classes outside of this class.
         Neat!
   */
}

下一个没有指定访问权限的方法具有默认可见性。它只能被同一包中的其他类使用。如果我们扩展持有此“默认”访问方法的类,子类将无法访问此父类的方法:

fairlySecretTask(){
   // allow just the classes in the package
   // Not for external use
}

在我们继续之前的最后一个例子中,这是一个protected方法,只对包可见,但可以被我们扩展它的类使用-就像onCreate一样:

protected packageTask(){
   // Allow just the classes in the package
   // And you can use me if you extend me too
}

让我们快速回顾一下方法封装,但请记住,你不需要记住所有的东西。

方法访问总结

方法访问应该被选择为最好地执行我们已经讨论过的原则。它应该为你的类的用户提供他们所需要的访问权限,最好是没有更多。通过这样做,我们实现了我们的封装目标,比如保护代码的内部工作免受使用它的程序的干扰,出于我们已经讨论过的所有原因。

使用 getter 和 setter 访问私有变量

现在,如果将变量隐藏为私有是最佳实践,我们需要考虑如何允许访问它们,而不破坏我们的封装。如果Hospital类的对象想要访问Soldier类型的对象的health成员变量,以便增加它,health变量应该是私有的,因为我们不希望任何代码片段都可以更改它。

为了能够尽可能多地将成员变量设置为私有,同时仍然允许对其中一些进行有限访问,我们使用gettersetter。Getter 和 setter 只是获取和设置变量值的方法。

这不是我们必须学*的一些特殊的新的 Java 东西。这只是一个使用我们已经知道的东西的惯例。让我们看一下使用我们的Soldier类和Hospital类示例的 getter 和 setter。

在这个例子中,我们的两个类分别在自己的文件中创*,但在同一个包中。首先,这是我们假设的Hospital类:

class Hospital{
   private void healSoldier(Soldier soldierToHeal){
         int health = soldierToHeal.getHealth();
         health = health + 10;
         soldierToHeal.setHealth(health);
   }
}

我们的Hospital类的实现只有一个方法,healSoldier。它接收一个Soldier对象的引用作为参数。因此,这个方法将在传入的任何Soldier对象上工作:vassilywellingtonrambo,或其他人。

它还有一个本地的health变量,它用来临时保存并增加士兵的健康。在同一行中,它将health变量初始化为Soldier对象的当前健康状况。Soldier对象的健康状况是私有的,因此使用公共的getHealth getter 方法。

然后health增加了 10,setHealth setter 方法加载了新的恢复后的健康值,返回到Soldier对象。

关键在于,尽管Hospital对象可以改变Soldier对象的健康状况,但它只能在 getter 和 setter 方法的范围内这样做。getter 和 setter 方法可以被编写来控制和检查可能错误的,甚至有害的值。

接下来,看看我们刚刚使用的假设的Soldier类,它具有最简单的 getter 和 setter 方法的实现:

public class Soldier{

   private int health;
   public int getHealth(){
         return health;
   }
   public void setHealth(int newHealth){
         // Check for bad values of newHealth
         health = newHealth;
   }
}

我们有一个名为health的实例变量,它是私有的。私有意味着它只能被Soldier类的方法更改。然后我们有一个公共的getHealth方法,它返回私有的health int 变量中保存的值。由于这个方法是公共的,任何具有Soldier类型对象访问权限的代码都可以使用它。

接下来,实现了setHealth方法。同样,它是公共的,但这次它接受一个int作为参数,并将传入的任何内容分配给私有的health变量。在更像生活的例子中,我们会在这里编写更多的代码,以确保传入的值在我们期望的范围内。

现在我们声明、创*和赋值,创*每个新类的对象,并看看我们的 getter 和 setter 是如何工作的:

Soldier mySoldier = new Soldier();
// mySoldier.health = 100;//Doesn't work, private
// we can use the public setter setHealth() instead
mySoldier.setHealth(100); //That's better
Hospital militaryHospital = new Hospital();
// Oh no mySoldier has been wounded
mySoldier.setHealth(10);
/*        
   Take him to the hospital.
   But my health variable is private
   And Hospital won't be able to access it
   I'm doomed - tell Laura I love her
   No wait- what about my public getters and setters?
   We can use the public getters and setters 
   from another class
*/
militaryHospital.healSoldier(mySoldier);
// mySoldiers private variable health has been increased 
// by 10\. I'm feeling much better thanks!

我们看到我们可以直接在我们的Soldier类型的对象上调用我们的公共setHealthgetHealth方法。不仅如此,我们还可以调用Hospital对象的healSoldier方法,传入对Soldier对象的引用,后者也可以使用公共的 getter 和 setter 来操作私有的health变量。

我们看到私有的health变量是可以直接访问的,但完全受Soldier类的设计者控制。

如果你想尝试一下这个示例,第十一章文件夹中的代码包中有一个名为GettersAndSetters的工作应用程序的代码。我已经添加了几行代码来打印到控制台。

重要提示

Getter 和 setter 有时被称为它们更正确的名称访问器修改器。我们将坚持使用 getter 和 setter。我只是想让你知道这个行话。

我们的示例和解释可能又引发了更多问题。这很好。

通过使用封装特性(如访问控制),就像签署了一个关于如何使用和访问类、它的方法和变量的重要协议。这份合同不仅仅是关于现在的协议,而且是对未来的暗示保证。当我们继续阅读本章时,我们会看到更多的方式来完善和加强这份合同。

提示

在需要的时候使用封装,或者当然,如果你的雇主要求你使用它的话。在一些小型学*项目中,如本书中的一些示例中,封装通常是多余的。当然,除非你学*的主题就是封装本身。

我们在学*这些 Java OOP 的东西时,假设你将来会想要编写更复杂的应用程序,无论是在 Android 上还是其他使用 OOP 的*台上。此外,我们将使用 Android API 中广泛使用它的类,并且这也将帮助我们理解那时发生了什么。通常情况下,在本书中,我们将在实现完整项目时使用封装,并经常忽略它,当展示单个想法或主题的小代码示例时。

使用构造函数设置我们的对象

有了这些私有变量及其 getter 和 setter,这是否意味着我们需要为每个私有变量都需要一个 getter 和 setter?那么对于一个有很多需要在开始时初始化的变量的类呢?想想以下情况:

mySoldier.name
mysoldier.type
mySoldier.weapon
mySoldier.regiment
...

其中一些变量可能需要 getter 和 setter,但如果我们只想在对象首次创*时设置一些东西,以使对象正确运行呢?

我们肯定不需要为每个变量都有两个方法(一个 getter 和一个 setter)吧?

幸运的是,这是不必要的。为了解决这个潜在的问题,有一个特殊的方法叫做构造函数。我们在第十章**面向对象编程中讨论实例化一个类的对象时,简要提到了构造函数的存在。让我们再看看构造函数。

在这里,我们创*了一个类型为Soldier的对象,并将其赋给一个名为mySoldier的对象:

Soldier mySoldier = new Soldier();

这里没有什么新的,但是看一下代码行的最后部分:

...Soldier();

这看起来可疑地像一个方法。

一直以来,我们一直在调用一个特殊的方法,称为构造函数,这个方法是由编译器在幕后自动创*的。

然而,现在到了重点,就像一个方法一样,我们可以覆盖构造函数,这意味着我们可以在使用新对象之前对其进行有用的设置。下面的代码展示了我们如何做到这一点:

public Soldier(){
   // Someone is creating a new Soldier object

   health = 200;
   // more setup here
}

构造函数在语法上与方法有很多相似之处。但是,它只能在使用new关键字的情况下调用,并且它是由编译器自动为我们创*的 - 除非我们像在先前的代码中那样创*自己的构造函数。

构造函数具有以下特点:

  • 它们没有返回类型

  • 它们与类具有完全相同的名称

  • 它们可以有参数

  • 它们可以被重载

在这一点上,还有一些 Java 语法是有用的,那就是 Java 的this关键字。

当我们想要明确指出我们正在引用哪些变量时,就会使用this关键字。再看看这个例子构造函数,再看一个假设的Soldier类的变体:

public class Soldier{
   String name;
   String type;
   int health;
   // This is the constructor
   // It is called when a new instance is created
   public Soldier(String name, String type, int health){
          // Someone is creating a new Soldier object

          this.name = name;
          this.type = type;
          this.health = health;
          // more setup here
   }
}

这次,构造函数为每个我们想要初始化的变量都有一个参数。通过使用this关键字,当我们指的是成员变量或参数时就很清楚。

关于变量和this还有更多的技巧和转折,当应用到一个实际项目时,它们会更有意义。在下一个应用程序中,我们将探索本章迄今为止学到的所有内容,还有一些新的想法。

首先,再多一点面向对象编程。

静态方法

我们已经对类有相当多的了解。例如,我们知道如何将它们转换为对象并使用它们的方法和变量。但是有些地方不太对。自从书的开头,我们一直在使用两个类,比其他类更频繁地使用LogToast来输出到 logcat 或用户的屏幕,但我们从未实例化过它们!这怎么可能呢?我们从未这样做过:

Log myLog  = new Log();
Toast myToast = new Toast();

我们直接使用了这些类,就像这样:

Log.i("info","our message here");
Toast.makeText(this, "our message",      
Toast.LENGTH_SHORT).show();

类的静态方法可以在没有首先实例化类的对象的情况下使用。我们可以将其视为属于类的静态方法,而所有其他方法都属于类的对象/实例。

而且你现在可能已经意识到,LogToast都包含静态方法。要清楚:LogToast包含静态方法;它们本身仍然是类。

类既可以有静态方法,也可以有常规方法,但是常规方法需要以常规方式使用,通过类的实例/对象。

再看一下Log.i的使用:

Log.i("info","our message here");

在这里,i是静态访问的方法,该方法接受两个参数,都是 String 类型。

接下来,我们看到了Toast类的静态方法makeText的使用:

Toast.makeText(this, "our message", 
Toast.LENGTH_SHORT).show();

Toast类的makeText方法接受三个参数。

第一个是this,它是对当前类的引用。我们在谈论构造函数时看到,为了明确地引用对象的当前实例的成员变量,我们可以使用this.healththis.regiment等等。

当我们像在上一行代码中那样使用this时,我们指的是类本身的实例;不是Toast类,而是上一行代码中的this是对方法所在的类的引用。在我们的例子中,我们已经从MainActivity类中使用了它。

在 Android 中,许多事情都需要引用Activity的实例才能完成它们的工作。在本书中,我们将经常传递this(对Activity的引用)来使 Android API 中的类/对象能够完成它们的工作。我们还将编写需要this作为一个或多个方法参数的类。因此,我们将看到如何处理传递进来的this

makeText方法的第二个参数当然是一个String

第三个参数是访问一个final变量LENGTH_SHORT,同样是通过类名而不是类的实例。如果我们像下一行代码那样声明一个变量:

public static final int LENGTH_SHORT = 1;

如果变量是在一个名为MyClass的类中声明的,我们可以像这样访问变量:MyClass.LENGTH_SHORT,并像任何其他变量一样使用它,但final关键字确保变量的值永远不会改变。这种类型的变量被称为常量

static关键字对变量也有另一个影响,特别是当它不是一个常量(可以改变)时,我们将在我们的下一个应用程序中看到它的作用。

现在,如果你仔细看代码行的最后,显示一个Toast消息给用户,你会看到另一个新的东西,.show()

这被称为Toast类,但只使用了一行代码。实际触发消息的是show方法。

当我们继续阅读本书时,我们将更仔细地研究链式调用,比如在第十四章中,Android 对话框窗口,当我们创*一个弹出对话框时。

提示

如果你想详细了解Toast类及其其他一些方法,你可以在这里查看:developer.android.com/reference/android/widget/Toast.html

静态方法通常在具有如此通用用途的类中提供,以至于创*该类的对象是没有意义的。另一个非常有用的具有静态方法的类是Math。这个类实际上是 Java API 的一部分,而不是 Android API 的一部分。

提示

想写一个计算器应用程序吗?使用Math类的静态方法比你想象的要容易。你可以在这里查看它们:docs.oracle.com/javase/7/docs/api/java/lang/Math.html

如果你尝试这个,你需要以同样的方式导入Math类,就像你导入我们使用过的所有其他类一样。接下来,我们可以尝试一个实际的迷你应用程序来理解封装和静态方法。

封装和静态方法迷你应用程序

我们已经看到了对变量和它们的作用域的访问是如何受控制的,我们最好看一个例子来了解它们的作用。这些不会是变量使用的实际实例,更多的是一个演示,以帮助理解类、方法和变量的访问修饰符,以及引用或原始和局部或实例变量的不同类型,以及静态和最终变量和this关键字的新概念。

完成的代码在下载包的第十一章文件夹中。它被称为Access Scope This And Static

创*一个新的空活动项目,并将其命名为Access Scope This And Static

通过右键单击项目资源管理器中的现有MainActivity类并单击AlienShip来创*一个新类。

现在,我们将声明我们的新类和一些成员变量。请注意,numShipsprivatestatic。我们很快将看到这个变量在类的所有实例中是相同的。shieldStrength变量是privateshipName是公共的:

public class AlienShip {

   private static int numShips;
   private int shieldStrength;
   public String shipName;

接下来是构造函数。我们可以看到构造函数是公共的,没有返回类型,并且与类名相同-根据规则。在其中,我们递增了私有静态的numShips变量。请记住,每当我们创*一个新的AlienShip类型的对象时,这将发生。此外,构造函数使用私有的setShieldStrength方法为私有变量shieldStrength设置一个值:

public AlienShip(){
   numShips++;
   /*
         Can call private methods from here because I am 
         part of the class.
         If didn't have "this" then this call 
         might be less clear
         But this "this" isn't strictly necessary
         Because of "this" I am sure I am setting 
         the correct shieldStrength
   */
   this.setShieldStrength(100);
}

这是公共静态的 getter 方法,这样AlienShip外部的类就可以找出有多少个AlienShip对象了。我们还将看到我们如何使用静态方法:

    public static int getNumShips(){
        return numShips;
    }

这是我们的私有setShieldStrength方法。我们本可以直接从类内部设置shieldStrength,但下面的代码显示了我们如何通过使用this关键字区分shieldStrength局部变量/参数和shieldStrength成员变量:

private void setShieldStrength(int shieldStrength){

   // "this" distinguishes between the 
   // member variable shieldStrength
   // And the local variable/parameter of the same name
   this.shieldStrength = shieldStrength;

}

接下来的方法是 getter,这样其他类就可以读取但不能更改每个AlienShip对象的护盾强度:

public int getShieldStrength(){
    return this.shieldStrength;
}

现在我们有一个公共方法,每次击中AlienShip对象时都可以调用。它只是打印到控制台,然后检测该对象的shieldStrength是否为零。如果是,它调用destroyShip方法,我们将在下面看到:

public void hitDetected(){
    shieldStrength -=25;
    Log.i("Incomiming: ","Bam!!");
    if (shieldStrength == 0){
        destroyShip();
    }
}

最后,对于我们的AlienShip类,我们将编写destroyShip方法。我们打印一条消息,指示基于其shipName已被销毁的飞船,并递减numShips静态变量,以便我们可以跟踪类型AlienShip的对象数量:

   private void destroyShip(){
         numShips--;
         Log.i("Explosion: ", ""+this.shipName + " 
         destroyed");
    }
} // End of the class

现在我们切换到我们的MainActivity类,并编写一些使用我们新的AlienShip类的代码。所有代码都放在setContentView调用之后的onCreate方法中。首先,我们创*两个名为girlShipboyShip的新AlienShip对象:

// every time we do this the constructor runs
AlienShip girlShip = new AlienShip();
AlienShip boyShip = new AlienShip();

在下一个代码中,看看我们如何获取numShips的值。我们使用getNumShips方法,就像我们所期望的那样。但是,仔细看语法。我们使用的是类名,而不是对象。我们还可以使用不是静态的方法访问静态变量。我们这样做是为了看到静态方法的运行方式:

// Look no objects but using the static method
Log.i("numShips: ", "" + AlienShip.getNumShips());

现在,我们为我们的公共shipName字符串变量分配名称:

// This works because shipName is public
girlShip.shipName = "Corrine Yu";
boyShip.shipName = "Andre LaMothe";

在接下来的代码中,我们尝试直接为私有变量分配一个值。这是行不通的。然后我们使用公共的 getter 方法getShieldStrength来打印出在构造函数中分配的shieldStrength

// This won't work because shieldStrength is private
// girlship.shieldStrength = 999;
// But we have a public getter
Log.i("girl shields: ", "" + girlShip.getShieldStrength());
Log.i("boy shields: ", "" + boyShip.getShieldStrength());
// And we can't do this because it's private
// boyship.setShieldStrength(1000000);

最后,我们通过使用hitDetected方法来炸毁一些东西,并偶尔检查我们两个对象的shieldStrength

// let's shoot some ships
girlShip.hitDetected();
Log.i("girl shields: ", "" + girlShip.getShieldStrength());

Log.i("boy shields: ", "" + boyShip.getShieldStrength());

boyShip.hitDetected();
boyShip.hitDetected();
boyShip.hitDetected();

Log.i("girl shields: ", "" + girlShip.getShieldStrength());

Log.i("boy shields: ", "" + boyShip.getShieldStrength());

boyShip.hitDetected();//Ahhh!

Log.i("girl shields: ", "" + girlShip.getShieldStrength());
Log.i("boy shields: ", "" + boyShip.getShieldStrength());

当我们认为我们已经摧毁了一艘飞船时,我们再次使用我们的静态getNumShips方法来查看我们的静态变量numShips是否被destroyShip方法改变:


Log.i("numShips: ", "" + AlienShip.getNumShips());

运行演示并查看控制台输出:

numShips: 2
girl shields: 100
boy shields: 100
Incomiming: Bam!!
girl shields:﹕ 75
boy shields:﹕ 100
Incomiming: Bam!!
Incomiming: Bam!!
girl shields:﹕ 75
boy shields:﹕ 25
Incomiming: Bam!!
Explosion: Andre LaMothe destroyed
girl shields: 75
boy shields: 0
numShips: 1
boy shields: 0
numShips: 1

在前面的示例中,我们看到我们可以通过使用this关键字区分相同名称的局部变量和成员变量。我们还可以使用this关键字编写代码,引用当前正在操作的对象。

我们看到静态变量-在这种情况下,numShips-在所有实例中是一致的;此外,通过在构造函数中递增它,并在我们的destroyShip方法中递减它,我们可以跟踪我们当前拥有的AlienShip对象的数量。

我们还看到,我们可以使用静态方法,使用类名和点运算符而不是实际对象。

提示

是的,我知道这就像生活在房子的蓝图中一样-但这也非常有用。

最后,我们证明了如何使用访问修饰符隐藏和公开某些方法和变量。

接下来,我们将看一下继承的主题。

面向对象编程和继承

我们已经看到,我们可以通过实例化/创*来自 Android 等 API 的类的对象来使用其他人的代码。但是这整个 OOP 的东西甚至比那更深入。

如果有一个类有很多有用的功能,但不完全符合我们的要求,我们可以从该类继承,然后进一步完善或添加其工作方式和功能。

你可能会惊讶地听到我们已经这样做了。实际上,我们已经为我们创*的每个应用程序都这样做了。当我们使用extends关键字时,我们正在继承。记住这一点:

public class MainActivity extends AppCompatActivity ...

在这里,我们继承了AppCompatActivity类以及它的所有功能-更具体地说,类设计者希望我们能够访问的所有功能。以下是我们可以对我们扩展的类做的一些事情。

我们甚至可以重写一个方法并且仍然部分依赖于我们继承的类中的重写方法。例如,我们每次扩展AppCompatActivity类时都重写了onCreate方法。但是当我们这样做时,我们也调用了类设计者提供的默认实现:

super.onCreate(... 

第六章Android 生命周期中,我们重写了几乎所有 Activity 类的生命周期方法。

我们主要讨论继承,以便我们了解周围发生的事情,并作为最终能够设计有用的类的第一步,我们或其他人可以扩展。

考虑到这一点,让我们看一些示例类,并看看我们如何扩展它们,只是为了看看语法并作为第一步,也为了能够说我们已经这样做了。

当我们看这一章的最后一个主要主题,多态性时,我们也将同时深入研究继承。这里有一些使用继承的代码。

这段代码将放在一个名为Animal.java的文件中:

public class Animal{
   // Some member variables
   public int age;
   public int weight;
   public String type;
   public int hungerLevel;
   public void eat(){
          hungerLevel--;
   }
   public void walk(){
          hungerLevel++;
   }
}

然后在一个名为Elephant.java的单独文件中,我们可以这样做:

public class Elephant extends Animal{

   public Elephant(int age, int weight){
         this.age = age;
         this.weight = weight;
         this.type = "Elephant";
         int hungerLevel = 0;
   }
}

我们可以看到在前面的代码中,我们实现了一个名为Animal的类,它有四个成员变量:ageweighttypehungerLevel。它还有两个方法,eatwalk

然后我们用Elephant扩展了AnimalElephant现在可以做任何Animal可以做的事情,它也有所有的变量。

我们在Elephant构造函数中初始化了Animal的变量,Elephant在创*对象时将两个变量(ageweight)传递给构造函数,并且为所有Elephant对象分配了两个变量(typehungerLevel)。

我们可以继续编写一堆其他扩展Animal的类,也许是LionTigerThreeToedSloth。每个类都会有ageweighttypehungerLevel,并且每个类都能walkeat

好像 OOP 已经不够有用了,我们现在可以模拟现实世界的对象。我们还看到,通过子类化/扩展/继承其他类,我们可以使 OOP 变得更加有用。我们可能想要学*的术语是被扩展的类是超类,继承超类的类是子类。我们也可以说父类和子类。

提示

像往常一样,我们可能会问关于继承的这个问题。为什么?原因是这样的:我们可以在父类中编写一次通用代码;我们可以更新该通用代码,所有继承自它的类也会更新。此外,子类只能使用公共/受保护的实例变量和方法。因此,如果设计得当,这也进一步增强了封装的目标。

让我们构*另一个小应用程序来玩一下继承。

继承示例应用程序

我们已经看过了如何创*类的层次结构来模拟适合我们应用程序的系统。因此,让我们尝试一些使用继承的简单代码。完成的代码在Chapter 11文件夹中。它被称为Inheritance Example

使用AlienShip、另一个Fighter和最后一个Bomber创*一个名为Inheritance Example的新项目。

以下是AlienShip类的代码。它与我们之前的类演示AlienShip非常相似。不同之处在于构造函数现在接受一个int参数,它用于设置护盾强度。

构造函数还会将消息输出到 logcat 窗口,这样我们就可以看到它何时被使用。AlienShip类还有一个新方法fireWeapon,声明为abstract

将一个类声明为抽象类可以保证任何子类AlienShip都必须实现其自己的fireWeapon版本。注意类的声明中有abstract关键字。我们必须这样做是因为它的一个方法也使用了abstract关键字。当我们讨论这个演示和下一节中讨论多态时,我们将解释abstract方法和abstract类。

将以下代码添加到AlienShip类中:

public abstract class AlienShip {
    private static int numShips;
    private int shieldStrength;
    public String shipName;
    public AlienShip(int shieldStrength){
        Log.i("Location: ", "AlienShip constructor");
        numShips++;
        setShieldStrength(shieldStrength);
    }
    public abstract void fireWeapon();
    // Ahh my body where is it?
    public static int getNumShips(){
        return numShips;
    }
    private void setShieldStrength(int shieldStrength){
        this.shieldStrength = shieldStrength;
    }
    public int getShieldStrength(){
        return this.shieldStrength;
    }
    public void hitDetected(){
        shieldStrength -=25;
        Log.i("Incomiming: ", "Bam!!");
        if (shieldStrength == 0){
            destroyShip();
        }
    }
    private void destroyShip(){
        numShips--;
        Log.i("Explosion: ", "" + this.shipName + " 
        destroyed");
    }
}

现在我们将实现Bomber类。注意调用super(100)。这将使用shieldStrength的值调用超类的构造函数。我们可以在这个构造函数中进一步初始化特定的Bomber,但现在,我们只是打印出位置,这样我们就可以看到Bomber构造函数何时被执行。因为我们必须,我们还必须实现一个抽象fireWeapon方法的Bomber特定版本。将以下代码添加到Bomber类中:

public class Bomber extends AlienShip {
    public Bomber(){
        super(100);
        // Weak shields for a bomber
        Log.i("Location: ", "Bomber constructor");
    }
    public void fireWeapon(){
        Log.i("Firing weapon: ", "bombs away");
    }
}

现在我们将实现Fighter类。注意调用super(400)。这将使用shieldStrength的值调用超类的构造函数。我们可以在这个构造函数中进一步初始化特定的Fighter,但现在,我们只是打印出位置,这样我们就可以看到Fighter构造函数何时被执行。我们还必须实现一个抽象fireWeapon方法的Fighter特定版本。将以下代码添加到Fighter类中:

public class Fighter extends AlienShip{
    public Fighter(){
        super(400);
        // Strong shields for a fighter
        Log.i("Location: ", "Fighter constructor");
    }
    public void fireWeapon(){
        Log.i("Firing weapon: ", "lasers firing");
    }
}

接下来,我们将编写MainActivityonCreate方法。像往常一样,在调用setContentView方法之后输入此代码。这是使用我们的三个新类的代码。代码看起来相当普通 - 没有什么新东西。有趣的是输出:

Fighter aFighter = new Fighter();
Bomber aBomber = new Bomber();
// Can't do this AlienShip is abstract -
// Literally speaking as well as in code
// AlienShip alienShip = new AlienShip(500);
// But our objects of the subclasses can still do
// everything the AlienShip is meant to do
aBomber.shipName = "Newell Bomber";
aFighter.shipName = "Meier Fighter";
// And because of the overridden constructor
// That still calls the super constructor
// They have unique properties
Log.i("aFighter Shield:", ""+ aFighter.getShieldStrength());
Log.i("aBomber Shield:", ""+ aBomber.getShieldStrength());
// As well as certain things in certain ways
// That are unique to the subclass
aBomber.fireWeapon();
aFighter.fireWeapon();
// Take down those alien ships
// Focus on the bomber it has a weaker shield
aBomber.hitDetected();
aBomber.hitDetected();
aBomber.hitDetected();
aBomber.hitDetected();

运行应用程序,您将在 logcat 窗口中获得以下输出:

Location:﹕ AlienShip constructor
Location:﹕ Fighter constructor
Location:﹕ AlienShip constructor
Location:﹕ Bomber constructor
aFighter Shield:﹕ 400
aBomber Shield:﹕ 100
Firing weapon:﹕ bombs away
Firing weapon:﹕ lasers firing
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Incomiming:﹕ Bam!!
Explosion:﹕ Newell Bomber destroyed

我们可以看到子类的构造函数如何调用超类的构造函数。我们还可以清楚地看到fireWeapon方法的各个实现正如预期地工作。

让我们更仔细地看一下最后一个重要的面向对象编程概念,多态。然后我们将能够在 Android API 中做一些更实际的事情。

多态

我们已经知道多态意味着不同的形式。但对我们来说意味着什么呢?

简而言之:

任何子类都可以作为使用超类的代码的一部分。

这意味着我们可以编写更简单、更容易理解和更容易更改的代码。

此外,我们可以为超类编写代码,并依赖于这样一个事实:无论它被子类化多少次,在一定的参数范围内,代码仍然可以正常工作。让我们讨论一个例子。

假设我们想要使用多态来帮助编写一个动物园管理应用程序。我们可能希望有一个feed之类的方法。我们可能希望将要喂养的动物的引用传递到feed方法中。这似乎需要为每种类型的Animal编写一个feed方法。

然而,我们可以编写具有多态返回类型和参数的多态方法:

Animal feed(Animal animalToFeed){
   // Feed any animal here
   return animalToFeed;
}

前面的方法将Animal作为参数,这意味着可以将从扩展Animal的类构*的任何对象传递给它。正如你在前面的代码中看到的,该方法也返回Animal,具有完全相同的好处。

多态返回类型有一个小问题,那就是我们需要意识到返回的是什么,并且在调用方法的代码中明确表示出来。

例如,我们可以像这样处理将Elephant传递到feed方法中:

someElephant = (Elephant) feed(someElephant);

注意前面代码中的(Elephant)。这明确表示我们想要从返回的Animal中得到Elephant。这被称为转换。我们将在本书的其余部分偶尔使用转换。

因此,你甚至可以今天编写代码,并在一周、一个月或一年后创*另一个子类,而相同的方法和数据结构仍然可以工作。

此外,我们可以对我们的子类强制执行一组规则,规定它们可以做什么,不能做什么,以及如何做。因此,一个阶段的巧妙设计可以影响其他阶段。

但我们真的会想要实例化一个实际的Animal吗?

抽象类

在继承示例应用程序中,我们使用了一个抽象类,但让我们深入一点。抽象类是一个不能被实例化的类;它不能被制作成一个对象。那么,它就是一个永远不会被使用的蓝图?但这就像付钱给*筑师设计你的房子,然后永远不*造它?你可能会对自己说,“我有点明白了抽象方法的概念,但抽象类就是愚蠢。”

如果我们或类的设计者想要强制我们在使用他们的类之前继承,他们可以将一个类声明为abstract。然后,我们就不能从中创*一个对象;因此,我们必须首先扩展它并从子类创*一个对象。

我们还可以声明一个方法为abstract,然后该方法必须在扩展具有抽象方法的类的任何类中被重写。

让我们看一个例子——这会有所帮助。我们通过像这样使用abstract关键字来声明一个类为abstract

abstract class someClass{
   /*
         All methods and variables here.
         As usual!
         Just don't try and make 
         an object out of me!
   */
}

是的,但为什么?

有时我们想要一个可以用作多态类型的类,但我们需要保证它永远不能被用作对象。例如,“动物”本身并没有太多意义。

我们不谈论动物;我们谈论动物的类型。我们不会说,“哦,看那只可爱的毛茸茸的白色动物。”或者,“昨天我们去宠物店买了一只动物和一个动物床。”这太,嗯,抽象了。

因此,抽象类有点像一个模板,可以被任何extends它(继承自它)的类使用。

我们可能想要一个Worker类,并将其扩展为MinerSteelworkerOfficeWorker,当然还有Programmer。但一个普通的Worker到底是做什么的呢?我们为什么要实例化一个?

答案是我们可能不想实例化一个;但我们可能想要将其用作多态类型,这样我们可以在方法之间传递多个工作子类,并且可以拥有可以容纳所有类型的Worker的数据结构。

我们称这种类型的类为抽象类,当一个类有一个抽象方法时,它必须被声明为抽象类。并且所有抽象方法必须被任何扩展它的类重写。

这意味着抽象类可以提供一些在其所有子类中都可用的共同功能。例如,Worker类可能有heightweightage成员变量。

它可能还有getPayCheck方法,这不是抽象的,并且在所有子类中都是相同的,但doWork方法是抽象的,必须被重写,因为所有不同类型的工作者都会以不同的方式doWork

这使我们顺利地进入了多态的另一个领域,这将在本书中为我们带来更多便利。

接口

接口就像一个类。哦!这里没有什么复杂的。但它就像一个始终是抽象的类,只有抽象方法。

我们可以将接口视为一个完全抽象的类,其中所有方法都是抽象的,也没有成员变量。

好吧,你大概可以理解抽象类,因为至少它可以在其方法中传递一些功能,这些功能不是抽象的,并且可以作为多态类型。但说真的,这个接口似乎有点毫无意义。

让我们看一个最简单的通用接口示例,然后我们可以进一步讨论它。

要定义一个接口,我们输入以下内容:

public interface someInterface{
   void someAbstractMethod();
   // omg I've got no body

   int anotherAbstractMethod();
   // Ahh! Me too
   // Interface methods are always abstract and public 
   // implicitly but we could make it explicit if we prefer
   public abstract void 
   explicitlyAbstractAndPublicMethod();
   // still no body though

}

接口的方法没有方法体,因为它们是抽象的,但它们仍然可以有返回类型和参数,或者没有。

要使用接口,我们在类声明后使用implements关键字:

public class someClass implements someInterface{
   // class stuff here
   /* 
         Better implement the methods of the interface 
         or we will have errors.
         And nothing will work
   */

   public void someAbstractMethod(){
         // code here if you like 
         // but just an empty implementation will do
   }
   public int anotherAbstractMethod(){
         // code here if you like 
         // but just an empty implementation will do
         // Must have a return type though 
         // as that is part of the contract
         return 1;   
   }
     Public void explicitlyAbstractAndPublicMethod(){
     }
}

这使我们能够使用多态性来处理来自完全不相关的继承层次结构的多个不同对象。如果一个类实现了一个接口,整个东西就可以被传递或用作它本身 - 因为它就是那个东西。它是多态的(多种形式)。

我们甚至可以让一个类同时实现多个不同的接口。只需在每个接口之间加上逗号,并在implements关键字后列出它们。只需确保实现所有必要的方法。

在本书中,我们将更频繁地使用 Android API 的接口,而不是编写我们自己的接口。在第十三章**,匿名类 - 使 Android 小部件活跃中,我们将在 Java Meet UI 应用程序中使用OnClickListener接口。

许多事情在被点击时可能想要知道。也许是ButtonTextView小部件等等。因此,使用接口,我们不需要为每种我们想要点击的 UI 元素类型编写不同的方法。

经常问的问题

  1. 这个类声明有什么问题?
   private class someClass{
         // class implementation goes here
   }

没有私有类。类可以是公共的或默认的。公共类是公共的;默认类在其自己的包内是私有的。

  1. 封装是什么?

封装是我们如何以一种方式包含我们的变量、代码和方法,以仅暴露我们想要暴露给其他代码的部分和功能。

总结

在本章中,我们涵盖了比其他任何章节都更多的理论。如果你没有记住所有内容,或者有些代码看起来有点太深入了,那么你仍然完全成功了。

如果你只是理解 OOP 是通过封装、继承和多态编写可重用、可扩展和高效的代码,那么你就有成为 Java 大师的潜力。

简而言之,面向对象编程使我们能够在其他人不知道我们在编写代码时会做什么的情况下使用其他人的代码。

你所需要做的就是不断练*,因为我们将在整本书中不断地使用这些概念,所以你在这一点上甚至不需要已经掌握它们。

在下一章中,我们将重新讨论本章的一些概念,以及看一些 OOP 的新方面,以及它如何使我们的 Java 代码与我们的 XML 布局进行交互。

但首先,有一个重要的即将到来的新闻快讯!

重要提示

所有的 UI 元素 - TextViewConstraintLayoutCalenderViewButton - 也是类。它们的属性是成员变量,它们有大量的方法,我们可以使用这些方法来做各种各样的事情。这可能会很有用。

在接下来的两章中,我们将更多地了解这一点,但首先,我们将看看 Android 如何处理垃圾。

第十二章:栈、堆和垃圾收集器

在本章结束时,Java 和我们的 XML 布局之间的缺失链接将被完全揭示,让我们有能力像以前一样向我们的应用程序添加各种小部件,但这一次,我们将能够通过我们的 Java 代码来控制它们。

在本章中,我们将控制一些简单的 UI 元素,如ButtonTextView,在下一章中,我们将进一步操作一系列 UI 元素。

为了让我们理解发生了什么,我们需要更多地了解 Android 设备的内存以及其中的两个区域 -

在本章中,我们将学*以下内容:

  • Android UI 元素是类

  • 垃圾回收

  • 我们的 UI 在堆上?

  • 特殊类型的类,包括内部和匿名

回到那个新闻快讯。

技术要求

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

所有的 Android UI 元素也是类

当我们的应用程序运行并且从onCreate方法中调用setContentView方法时,布局从 XML 中inflated,UI 类的实例被加载到内存中作为可用对象。它们存储在一个称为堆的内存部分中。堆由Android RuntimeART)系统管理。

重新介绍引用

但是所有这些 UI 对象/类在哪里?我们在代码中肯定看不到它们。我们怎么能得到它们?

每个 Android 设备内部的 ART 负责为我们的应用程序分配内存。此外,它将不同类型的变量存储在不同的位置。

我们在方法中声明和初始化的变量存储在被称为的内存区域。在谈论栈时,我们可以继续使用我们现有的仓库类比。我们已经知道如何使用简单的表达式在栈上操作变量。所以,让我们谈谈堆和那里存储的东西。

重要事实

所有类的对象都是引用类型变量,只是指向存储在堆上的实际对象的引用 - 它们不是实际的对象。

把堆想象成同一个仓库的另一个区域。堆有大量的地板空间用于奇形怪状的对象,用于较小对象的货架,有很多长排有较小尺寸的小隔间等等。这就是对象存储的地方。问题是我们无法直接访问堆。把它想象成仓库的受限区域。你实际上不能去那里,但你可以引用那里存储的东西。让我们看看引用变量到底是什么。

这是我们通过引用引用和使用的变量。引用可以宽松但有用地定义为地址或位置。对象的引用(地址或位置)在栈上。

因此,当我们使用点运算符时,我们要求 Android 在一个特定的位置执行任务,这个位置存储在引用中。

为什么我们要一个这样的系统?直接把我的对象放在栈上。原因如下。

快速休息一下,扔掉垃圾

这就是整个栈和堆的作用。

正如我们所知,ART 系统为我们跟踪所有的对象,并将它们存储在我们仓库的一个专门区域,称为堆。在我们的应用程序运行时,ART 会定期扫描栈,我们仓库的常规货架,并匹配指向堆上对象的引用。它发现没有匹配引用的对象,就会销毁它。或者用 Java 术语来说,它进行垃圾回收

想象一辆非常挑剔的垃圾车驶过我们堆的中间,扫描对象以匹配引用(在栈上)。没有引用?你现在是垃圾。

如果一个对象没有引用变量,我们无法做任何事情,因为我们无法访问它。垃圾收集系统通过释放未使用的内存帮助我们的应用程序更有效地运行。

如果这个任务留给我们,我们的应用程序将更加复杂。

因此,在方法中声明的变量是局部变量,位于堆栈上,并且仅在它们声明的方法内部可见。成员变量(在对象中)位于堆上,并且可以在任何有引用到它的地方引用它,并且访问规范(封装)允许。

关于堆栈和堆的七个事实

让我们快速看一下我们对堆栈和堆的了解:

  1. 局部变量和方法位于堆栈上,局部变量局限于它们声明的特定方法。

  2. 实例/类变量在堆上(与它们的对象一起),但对对象的引用(其地址)是堆栈上的局部变量。

  3. 我们控制堆栈上的内容。我们可以使用堆上的对象,但只能通过引用它们。

  4. 垃圾收集器通过清除和更新堆来保持堆的清晰。

  5. 您不会删除对象,但 ART 系统在认为适当时会发送垃圾收集器。当不再有有效引用时,对象将被垃圾收集。因此,当引用变量(局部或实例)从堆栈中移除时,其相关对象变得可供垃圾收集。当 ART 系统决定时机合适(通常非常迅速),它将释放 RAM 内存以避免耗尽。

  6. 如果我们尝试引用一个不存在的对象,我们将得到一个NullPointerException,应用程序将崩溃。

让我们继续看看这些信息对我们控制 UI 有什么好处。

那么这个堆的东西如何帮助我呢?

在 XML 布局中设置了id属性的任何 UI 元素都可以使用findViewById方法从堆中检索到其引用,该方法是 Activity/AppCompatActivity类的一部分。由于它是我们在所有应用程序中扩展的类的一部分,因此我们可以访问它,正如这段代码所示:

Button myButton = findViewById(R.id.myButton);

上述代码假设在 XML 布局中有一个Button小部件,其id属性设置为myButtonmyButton对象现在直接引用 XML 布局中id属性设置为myButton的小部件。

请注意,findViewById方法也是多态的,任何扩展View类的类都可以从 UI 中检索,并且碰巧UI面板中的所有内容都扩展了View

敏锐的读者可能会注意到,我们在检索从抽象Animal继承的Elephant实例时,不使用强制转换来确保我们得到一个Button对象(而不是TextView或其他View后代):

someElephant = (Elephant) feed(someElephant);

这是因为View类使用了 Java 的泛型自动类型推断。这是一个我们不会在本书中涵盖的高级主题,但它意味着强制转换是自动的,我们不需要编写更多的冗长代码,如下所示:

Button myButton = (Button) findViewById(R.id.myButton);

抓取 UI 中任何东西的引用的能力令人兴奋,因为这意味着我们可以开始使用这些对象具有的所有方法。以下是一些我们可以用于Button对象的方法的示例:

myButton.setText
myButton.setHeight
myButton.setOnCLickListener
myButton.setVisibility

重要提示

Button类本身有大约 50 个方法!

如果您认为在经过 11 章之后,我们终于要开始在 Android 上做一些有趣的事情,那么您是对的!

从我们的布局中使用按钮和 TextView 小部件

要跟随这个项目,创*一个新的 Android Studio 项目,称之为Java Meet UI,选择空活动模板,并将所有其他选项保持默认。通常情况下,您可以在第十二章/Java Meet UI文件夹中找到 Java 代码和 XML 布局代码。

首先,让我们按照以下步骤构*一个简单的 UI:

  1. 在 Android Studio 的编辑窗口中,切换到activity_main.xml,确保你在设计选项卡上。

  2. 删除自动生成的TextView,即那个写着“Hello world!”的。

  3. 在布局的顶部中心添加一个TextView小部件。

  4. 将其设置为040sp。注意id属性值的大小写。它有一个大写的V

  5. 现在在布局上拖放六个按钮,使其尽可能接近下一个屏幕截图。确切的布局并不重要:图 12.1 - 布局设置

图 12.1 - 布局设置

  1. 当布局设置好后,点击推断约束按钮来约束所有的 UI 项目。

  2. 依次编辑每个按钮的属性(从左到右,从上到下),并按照下表中所示设置textid属性。表后面的图片应该清楚地显示了哪个按钮有哪些值。

完成后,你的布局应该看起来像下一个屏幕截图:

图 12.2 - 最终布局

图 12.2 - 最终布局

按钮上的精确位置和文本并不是很重要,但是id属性的值必须与表中的值相同。原因是我们将使用这些 ID 来从我们的 Java 代码中获取对这个布局中的按钮和TextView的引用。

切换到编辑器中的MainActivity.java选项卡,我们将编写代码。

修改这一行:

public class MainActivity extends AppCompatActivity{
to
public class MainActivity extends AppCompatActivity implements
   View.OnClickListener{

提示

你需要导入View类。确保在继续下一步之前做到这一点,否则你会得到混乱的结果。

import android.view.View;

注意我们刚刚修改的整行都被下划线标出了错误。现在,因为我们已经将MainActivity转换为OnClickListener,通过将其添加为一个接口,我们必须实现OnClickListener所需的抽象方法。这个方法叫做onClick。当我们添加onClick方法时,错误就会消失。

我们可以让 Android Studio 为我们添加它,方法是在带有错误的行的任何地方左键单击,然后使用键盘组合Alt + Enter。左键单击实现方法选项,如下一个屏幕截图所示。

图 12.3 - 实现方法

图 12.3 - 实现方法

现在,左键单击onClick方法。错误已经消失,我们可以继续添加代码。我们还有一个空的onClick方法,很快我们将看到我们将如何处理它。

现在我们将声明一个名为valueint类型变量,并将其初始化为0。我们还将声明六个Button对象和一个TextView对象。我们将给它们与我们 UI 布局中的id属性值完全相同的 Java 变量名。这种名称关联并不是必需的,但它对于跟踪我们的 Java 代码中的哪个Button将持有对我们的 XML UI 布局中的哪个Button的引用是有用的。

此外,我们将它们全部声明为private访问权限,因为我们知道它们在这个类之外不会被需要。

在继续输入代码之前,请注意所有这些变量都是MainActivity类的成员。这意味着我们在上一步修改的类声明之后立即输入所有下面显示的代码。

将所有这些变量都变成成员/字段意味着它们具有类范围,我们可以在MainActivity类的任何地方访问它们。这对这个项目非常重要,因为我们需要在onCreate方法和我们的新onClick方法中使用它们。

MainActivity类的开头大括号{后和onCreate方法前输入我们刚刚讨论过的下面的代码:

// An int variable to hold a value
private int value = 0;
// A bunch of Buttons and a TextView
private Button btnAdd;
private Button btnTake;
private TextView txtValue;
private Button btnGrow;
private Button btnShrink;
private Button btnReset;
private Button btnHide;

提示

记得使用ALT + Enter键盘组合来导入新的类。

import android.widget.Button;

import android.widget.TextView;

接下来,我们要准备好所有的变量以准备行动。这样做的最佳位置是onCreate方法,因为我们知道这将在应用程序显示给用户之前由 Android 调用。这段代码使用findViewById方法将我们的每个 Java 对象与 UI 布局中的一个项目关联起来。

它通过返回与堆上的 UI 小部件关联的对象的引用来实现。它“知道”我们要找的是哪一个,因为我们使用id属性值作为参数。例如,...(R.id.btnAdd)将返回我们在布局中创*的文本为ADDButton小部件。

onCreate方法中的setContentView调用后,输入以下代码:

// Get a reference to all the buttons in our UI
// Match them up to all our Button objects we declared earlier
btnAdd = findViewById(R.id.btnAdd);
btnTake = findViewById(R.id.btnTake);
txtValue = findViewById(R.id.txtValue);
btnGrow = findViewById(R.id.btnGrow);
btnShrink = findViewById(R.id.btnShrink);
btnReset = findViewById(R.id.btnReset);
btnHide = findViewById(R.id.btnHide);

现在我们已经有了对所有Button小部件和TextView小部件的引用,所以现在我们可以开始使用它们的方法。在接下来的代码中,我们使用setOnClickListener方法在每个Button引用上,使 Android 将用户的任何点击传递到我们的onClick方法。

这样做是因为当我们实现View.OnClickListener接口时,我们的MainActivity类实际上成为了一个OnClickListener

因此,我们只需依次在每个按钮上调用setOnClickListener。作为提醒,this参数是对MainActivity的引用。因此,方法调用表示:“嘿,Android,我想要一个OnClickListener,我希望它是MainActivity类。”

现在 Android 知道要在哪个类上调用onClick。如果我们没有先实现接口,下面的代码将无法工作。此外,我们必须在 Activity 启动之前设置这些监听器,这就是为什么我们在onCreate中这样做的原因。

很快,我们将添加代码到onClick方法中,以处理当按钮被点击时发生的情况,并且我们将看到如何区分所有不同的按钮。

在上一个代码之后,在onCreate方法内添加以下代码:

// Listen for all the button clicks
btnAdd.setOnClickListener(this);
btnTake.setOnClickListener(this);
txtValue.setOnClickListener(this);
btnGrow.setOnClickListener(this);
btnShrink.setOnClickListener(this);
btnReset.setOnClickListener(this);
btnHide.setOnClickListener(this);

现在滚动到 Android Studio 在我们实现OnClickListener接口后为我们添加的onClick方法。在其中添加float size;变量声明和一个空的switch块,使其看起来像下面的代码。要添加的新代码已经突出显示:

public void onClick(View view){
            // A local variable to use later
      float size;
      switch(view.getId()){

      }
}

记住,switch将检查是否有一个caseswitch语句内的条件匹配。

在上一个代码中,switch条件是view.getId()。让我们逐步解释一下。view变量是一个指向View类型对象的引用,它是由 Android 通过onClick方法传递的:

public void onClick(View view)

ViewButtonTextView等的父类。因此,或许正如我们所期望的那样,调用view.getId()将返回被点击并触发对onClick的调用的 UI 小部件的id属性。

然后我们只需要为我们想要响应的每个Button引用提供一个case语句(和适当的操作)。

接下来我们将看到的是前三个case语句。它们处理R.id.btnAddR.id.btnTakeR.id.btnReset情况:

  • R.id.btnAdd情况下的代码简单地增加了value变量,然后做了一些新的事情。

它调用txtValue对象上的setText方法。这是参数:(""+ value)。这个参数使用一个空字符串,并将存储在value中的任何值添加(连接)到其中。这会导致我们的TextView txtValue显示存储在value中的任何值。

  • R.id.btnTake)做的事情完全相同,只是从value中减去 1 而不是加 1。

  • 第三个case语句处理将value归零,并再次更新txtValuetext属性。

然后,在每个case的末尾,都有一个break语句。在这一点上,switch块被退出,onClick方法返回,生活恢复正常-直到用户的下一次点击。

在左花括号{后的switch块中输入我们刚讨论过的代码:

case R.id.btnAdd:
   value ++;
   txtValue.setText(""+ value);
   break;
case R.id.btnTake:
   value--;
   txtValue.setText(""+ value);
   break;
case R.id.btnReset:
   value = 0;
   txtValue.setText(""+ value);
   break;

下面的两个case语句处理R.id.btnGrowR.id.btnShrink。新的更有趣的是使用的两种新方法。

getTextScaleX方法返回其所用对象中文本的水*比例。我们可以看到它所用的对象是我们的TextView txtValue。代码行开头的size =将返回的值赋给我们的float变量size

在每个case语句的下一行代码改变了文本的水*比例,使用了setTextScaleX。当size + 1时,当size - 1时。

总体效果是允许这两个按钮通过每次点击来增大或缩小txtValue小部件中的文本比例 1。

输入我们刚讨论过的下面的下一个两个case语句:

case R.id.btnGrow:
   size = txtValue.getTextScaleX();
   txtValue.setTextScaleX(size + 1);
   break;
case R.id.btnShrink:
   size = txtValue.getTextScaleX();
   txtValue.setTextScaleX(size - 1);
   break;

在我们下面要编写的最后一个case语句中,我们有一个if-else块。条件需要稍微解释一下,所以让我们提前看一下:

if(txtValue.getVisibility() == View.VISIBLE)

要评估的条件是txtValue.getVisibility() == View.VISIBLE。在==运算符之前的条件的第一部分返回我们的txtValue TextViewvisibility属性。返回值将是View类中定义的三个可能的常量值之一。它们是View.VISIBLEView.INVISIBLEView.GONE

如果TextView在 UI 上对用户可见,则该方法返回View.VISIBLE,条件将被评估为true,并且if块将被执行。

if块内,我们在txtValue对象上使用setVisibility方法,并使用View.INVISIBLE参数使其对用户不可见。

除此之外,我们将btnHide小部件上的文本更改为setText方法。

if块执行完毕后,txtValue是不可见的,我们的 UI 上有一个按钮显示为if语句将为 false,else块将执行。在else块中,我们将情况反转。我们将txtValue设置回View.VISIBLE,并将btnHide上的text属性设置为HIDE

如果这有任何不清楚的地方,只需输入代码,运行应用程序,然后在看到它实际运行后再回顾这段代码和解释:

case R.id.btnHide:
   if(txtValue.getVisibility() == View.VISIBLE)
   {
         // Currently visible so hide it
         txtValue.setVisibility(View.INVISIBLE);
         // Change text on the button
         btnHide.setText("SHOW");
   }else{
         // Currently hidden so show it
         txtValue.setVisibility(View.VISIBLE);
         // Change text on the button
         btnHide.setText("HIDE");
   }
   break;

我们已经有了 UI 和代码,所以现在是时候运行应用程序,尝试所有的按钮了。

运行应用程序

以通常的方式运行应用程序。注意,value向任一方向增加或减少 1,然后在TextView小部件中显示结果。在下一个截图中,我点击了ADD按钮三次。

图 12.4  -  添加按钮示例

图 12.4 - 添加按钮示例

请注意,将value变量设置为 0,并在TextView小部件上显示它。在下一个截图中,我点击了GROW按钮八次。

图 12.5 - 增长按钮示例

图 12.5 - 增长按钮示例

最后,TextView小部件在再次点击时将其自身文本更改为TextView

提示

我不会打扰你展示一张隐藏的图片。一定要尝试这个应用程序。

请注意,在这个应用程序中不需要LogToast类,因为我们最终是使用我们的 Java 代码操纵 UI。让我们通过探索内部和匿名类来更深入地使用我们的代码操纵 UI。

内部和匿名类

在我们继续下一章并构*具有大量不同小部件的应用程序之前,这些小部件将实践和强化我们在本章学到的一切,我们将对匿名内部类进行非常简要的介绍。

当我们在*第十章**中实现了我们的基本类演示应用程序,面向对象编程时,我们在一个单独的文件中声明和实现了该类,该文件必须与我们的MainActivity类具有相同的名称。这是创*常规类的方法。

我们还可以在一个类中声明和实现其他类。除了如何我们这样做之外,当然,唯一剩下的问题是为什么我们要这样做?

当我们实现内部类时,内部类可以访问封闭类的成员变量,封闭类也可以访问内部类的成员。

这通常使我们的代码结构更加直观。因此,内部类有时是解决问题的方法。

此外,我们还可以在我们的类的方法中声明和实现一个完整的类。当我们这样做时,我们使用稍微不同的语法,并且不使用类名。这就是匿名类。

在本书的其余部分,我们将看到内部类和匿名类的实际应用,并在使用它们时进行彻底讨论。

常见问题

  1. 我并没有完全理解,实际上我现在有比章节开始时更多的问题。我该怎么办?

你已经了解了足够的面向对象编程知识,可以在 Android 和其他类型的 Java 编程中取得相当大的进步。如果你现在迫切想了解更多关于面向对象编程的知识,有很多书籍专门讨论面向对象编程。然而,实践和熟悉语法将有助于达到相同的效果,并且更有趣。这正是我们将在本书的其余部分做的事情。

总结

在本章中,我们终于让我们的代码和 UI 进行了一些真正的交互。事实证明,每当我们向我们的 UI 添加一个小部件时,我们都在添加一个我们可以在 Java 代码中引用的类的实例。所有这些对象都存储在内存的一个单独区域中,称为堆 - 以及我们自己的任何类。

现在我们可以学*并使用一些更有趣的小部件。我们将在下一章中看到大量的小部件,然后在本书的其余部分继续介绍更多新的小部件。

第十三章:匿名类-让 Android 小部件活起来

这一章本来可以被称为更多 OOP,因为匿名类仍然是这个主题的一部分。然而,正如您将看到的,匿名类为我们提供了如此多的灵活性,特别是在与用户界面UI)交互时,我认为它们值得有一章专门介绍它们及它们在 Android 中的关键用途。

现在我们已经对 Android 应用的布局和编码有了一个很好的概述,再加上我们新获得的对面向对象编程OOP)的见解,以及我们如何可以从 Java 代码中操作 UI,我们准备尝试使用调色板上的更多小部件以及匿名类。

面向对象编程有时是一个棘手的事情,匿名类对于初学者来说可能有点尴尬。然而,通过逐渐学*这些新概念,然后反复练*,随着时间的推移,它们将成为我们的朋友。

在本章中,我们将通过回到 Android Studio 调色板,查看半打我们根本没有看到过或者还没有完全使用过的小部件,来进行大量的多样化。

一旦我们做到了这一点,我们将把它们全部放入一个布局中,并用 Java 代码练*操作它们。

在本章中,我们将重点关注以下主题:

  • 声明和初始化布局小部件

  • 只使用 Java 代码创*小部件

  • EditTextImageViewRadioButton(和RadioGroup),SwitchCheckBoxTextClock小部件

  • 使用 WebView

  • 如何使用匿名类

  • 使用所有前述小部件和一些匿名类创*小部件演示迷你应用程序

让我们开始一个快速回顾。

技术要求

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

声明和初始化对象

我们知道,当我们从onCreate方法调用setContentView方法时,Android 会将所有小部件和布局膨胀,并将它们转换为堆上的真正的 Java 对象。

此外,我们知道要使用堆中的小部件,我们必须首先声明正确类型的对象,然后使用它来通过其唯一的id属性获取对堆上的 UI 小部件对象的引用。

例如,我们通过id属性为txtTitleTextView小部件获取一个引用,并将其赋值给一个新的 Java 对象,称为myTextView,如下所示:

// Grab a reference to an object on the heap
TextView myTextView = findViewById(R.id.txtTitle);

现在,使用我们的myTextView实例变量,我们可以做任何TextView类设计的事情。例如,我们可以设置文本显示如下:

myTextView.setText("Hi there");

此外,我们可以按如下方式使其消失:

// Bye bye
myTextView.setVisibility(View.GONE)

我们可以再次更改它的文本并使其重新出现:

myTextView.setText("BOO!");
// Surprise
myTextView.setVisibility(View.VISIBLE)

值得一提的是,我们可以在 Java 中操纵在前几章中使用 XML 设置的任何属性。此外,我们已经暗示过-但实际上没有看到-使用纯 Java 代码从无到有地创*小部件。

在纯 Java 中创* UI 小部件而不使用 XML

我们还可以从不是指向布局中对象的 Java 对象创*小部件。我们可以在代码中声明、实例化和设置小部件的属性,如下所示:

Button myButton = new Button();

上述代码通过使用new()关键字创*了一个新的Button。唯一的注意事项是Button必须是布局的一部分,才能被用户看到。因此,我们可以从 XML 布局中获取对布局元素的引用,或者在代码中创*一个新的布局。

如果我们假设我们的 XML 中有一个id属性等于linearLayout1LinearLayout,我们可以将我们之前代码行中的Button放入其中,如下所示:

// Get a reference to the LinearLayout
LinearLayout linearLayout = (LinearLayout)
   findViewById(R.id.linearLayout);
// Add our Button to it
linearLayout.addView(myButton);

我们甚至可以通过纯 Java 代码创*一个完整的布局,首先创*一个新布局,然后创*我们想要添加的所有小部件。最后,我们在具有我们小部件的布局上调用setContentView方法。

在下面的代码中,我们用纯 Java 创*了一个布局,尽管它非常简单,只有一个LinearLayout里面有一个Button

// Create a new LinearLayout
LinearLayout linearLayout = new LinearLayout();
// Create a new Button
Button myButton = new Button();
// Add myButton to the LinearLayout
linearLayout.addView(myButton);
// Make the LinearLayout the main view
setContentView(linearLayout);

显而易见的是,仅使用 Java 设计详细和细致的布局会更加麻烦,更难以可视化,并且通常不是通常的做法。然而,有时我们会发现以这种方式做事情是有用的。

我们现在已经相当高级了,涉及到布局和小部件。然而,很明显,调色板中还有许多其他小部件,我们尚未探索或与之交互。所以,让我们解决这个问题。

探索调色板 – 第一部分

让我们快速浏览一下调色板中以前未探索/未使用的一些项目。然后,我们可以将它们拖放到布局中,查看它们可能有用的一些方法。然后,我们可以实现一个项目来使用它们。

我们在上一章已经探索了ButtonTextView小部件。让我们更仔细地看看一些其他小部件。

EditText 小部件

EditText小部件就像其名称所示。如果我们向用户提供EditText小部件,他们确实可以在其中编辑 文本。我们在早期的章节中已经看过这个了;然而,我们实际上并没有做任何事情。我们没有探索的是如何捕获其中的信息,或者我们将在哪里输入这个捕获文本的代码。

以下代码块假设我们已经声明了一个EditText类型的对象,并使用它来获取我们 XML 布局中的EditText小部件的引用。例如,我们可能会为按钮点击编写类似以下代码的代码,例如,表单的提交按钮。但是,它可以放在我们认为对我们的应用程序必要的任何地方:

String editTextContents = editText.getText()
// editTextContents now contains whatever the user entered

我们将在下一个迷你应用程序中在实际环境中使用它。

ImageView 小部件

到目前为止,我们已经几次将图像放到我们的布局中,但以前从未从我们的 Java 代码中获取引用或对其进行任何操作。获取对ImageView小部件的引用的过程与任何其他小部件相同:

  • 声明一个对象。

  • 使用findViewById方法和有效的id属性获取引用,例如以下内容:

ImageView imageView = findViewById(R.id.imageView);

然后,我们可以使用类似以下的代码对我们的图像做一些相当不错的事情:

// Make the image 50% TRANSPARENT
imageView.setAlpha(.5f);

重要提示

看起来奇怪的f只是让编译器知道值是float类型,这是setAlpha方法所需的。

在上述代码中,我们在imageView上使用了setAlpha方法。setAlpha方法接受一个介于 0 和 1 之间的值。完全透明的图像为 0,而完全不透明的图像为 1。

提示

还有一个重载的setAlpha方法,它接受一个从 0(完全透明)到 255(不透明)的整数值。我们可以在需要时选择最合适的方法。如果您想了解方法重载的内容,请参考第九章学* Java 方法

我们将在下一个应用程序中使用ImageView类的一些方法。

单选按钮和组

当用户需要选择两个或更多互斥的选项时,会使用RadioButton小部件。这意味着当选择一个选项时,其他选项就不会被选择,就像在老式收音机上一样。请看下面截图中带有几个RadioButton小部件的简单RadioGroup

图 13.1 – RadioButton 小部件

图 13.1 – RadioButton 小部件

当用户选择一个选项时,其他选项将自动取消选择。我们通过将RadioButton小部件放置在 UI 布局中的RadioGroup中来控制它们。当然,我们可以使用可视化设计工具简单地将一堆RadioButtons拖放到RadioGroup中。当我们在ConstraintLayout布局中这样做时,XML 将类似于以下内容:

<RadioGroup
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     tools:layout_editor_absoluteX="122dp"
     tools:layout_editor_absoluteY="222dp" >
     <RadioButton
          android:id="@+id/radioButton1"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Option 1" />
     <RadioButton
          android:id="@+id/radioButton2"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Option 2" />
     <RadioButton
          android:id="@+id/radioButton3"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:text="Option 3" />
</RadioGroup>

请注意,正如前面的代码所强调的,每个RadioButton实例都设置了适当的id属性。然后,我们可以像这段代码所示的那样引用它们:

// Get a reference to all our widgets
RadioButton rb1 = findViewById(R.id.radioButton1);
RadioButton rb2 = findViewById(R.id.radioButton2);
RadioButton rb3 = findViewById(R.id.radioButton3);

然而,在实践中,正如你将看到的,我们几乎可以仅通过RadioGroup引用来管理几乎所有的事情。此外,你将了解到我们可以为RadioGroup小部件分配一个id属性以实现这个目的。

你可能会想,我们怎么知道它们被点击了呢?或者,你可能会想知道是否跟踪被选中的那个可能会很尴尬。我们需要一些来自 Android API 和 Java 的帮助,以匿名类的形式。

匿名类

第十二章**,堆栈、堆和垃圾收集器中,我们简要介绍了匿名类。在这里,我们将更详细地讨论它,并探讨它如何帮助我们。当RadioButton小部件是RadioGroup小部件的一部分时,它们的所有视觉外观都是为我们协调好的。我们所需要做的就是在任何给定的RadioButton小部件被按下时做出反应。当然,就像任何其他按钮一样,我们需要知道它们何时被点击。

RadioButton小部件的行为与常规的Button不同,仅仅监听onClick(在实现OnClickListener之后)的点击是行不通的,因为RadioButton并非设计为这样。

我们需要做的是使用另一个 Java 特性。我们需要实现一个类,也就是一个匿名类,唯一的目的是监听RadioGroup小部件上的点击。下面的代码块假设我们有一个名为radioGroupRadioGroup小部件的引用。这是代码:

radioGroup.setOnCheckedChangeListener(
   new RadioGroup.OnCheckedChangeListener() {

         @Override
         public void onCheckedChanged(RadioGroup group, 
                int checkedId) {

                // Handle clicks here
         }
   }
);

前面的代码,特别是从{}RadioGroup.OnCheckedChangedListener被称为匿名类。这是因为它没有名字。

如果我们将前面的代码放在onCreate方法中,那么令人惊讶的是,当调用onCreate时代码并不运行。它只是准备好新的匿名类,以便在radioGroup上处理任何点击。我们现在将更详细地讨论这一点。

这个类更正式地称为匿名内部类,因为它在另一个类内部。内部类可以是匿名的或者有名字的。我们将在第十六章中学*有名字的内部类,适配器和回收器

我记得第一次看到匿名类时,我想躲进橱柜里。然而,它并不像一开始看起来那么复杂。

我们正在为radioGroup添加一个监听器。这与我们在第十二章中实现View.OnClickListener的效果非常相似,堆栈、堆和垃圾收集器。然而,这一次,我们声明并实例化了一个监听器类,准备好监听radioGroup,同时重写了所需的方法,这种情况下是onCheckedChanged。这就像RadioGroup中的onClick等效。

让我们逐步了解这个过程:

  1. 首先,我们在radioGroup实例上调用setOnCheckedChangedListener方法:
radioGroup.setOnCheckedChangeListener(
  1. 然后,我们提供一个新的匿名类实现,将该类的重写方法的细节作为参数传递给setOnCheckedChangedListener方法:
new RadioGroup.OnCheckedChangeListener() {

      @Override
      public void onCheckedChanged(RadioGroup group, 
             int checkedId) {

             // Handle clicks here
      }
}
  1. 最后,我们有方法的闭合括号,当然,还有分号标记代码行的结束。我们将它呈现在多行上的唯一原因是为了使其更易读。就编译器而言,它可以全部合并在一起:
);

如果我们使用前面的代码来创*和实例化一个类,监听我们的RadioGroup的点击,也许在onCreate方法中,它将在整个 Activity 的生命周期内监听和响应。现在我们需要学*的是如何处理我们重写的onCheckedChanged方法中的点击。

请注意,onCheckedChanged方法的一个参数(在radioGroup被按下时传入)是int checkedId。这保存了当前选定的RadioButton小部件的id属性。这正是我们需要的 – 好吧,几乎是。

也许令人惊讶的是,checkedId是一个int。Android 将所有 ID 存储为int,即使我们使用字母数字字符声明它们,比如radioButton1radioGroup

当应用程序编译时,所有我们人性化的名称都会转换为整数。那么,我们如何知道哪个整数值指的是哪个id属性值,比如radioButton1radioButton2

我们需要做的是获取整数标识符的实际对象的引用。我们可以通过使用int CheckedId参数来实现,然后询问对象其人性化的id属性值。我们可以这样做:

RadioButton rb = group.findViewById(checkedId);

现在,我们可以使用getId方法检索当前选定的RadioButton小部件的熟悉的id属性值,我们现在已经将其存储在rb中:

rb.getId();

因此,我们可以通过使用switch块和case来处理任何RadioButton小部件的点击,对于每个可能被按下的RadioButton小部件,我们可以使用rb.getId()作为switch块的表达式。

以下代码显示了我们刚讨论的onCheckedChanged方法的全部内容:

// Get a reference to the RadioButton 
// that is currently checked
RadioButton rb = group.findViewById(checkedId);
// Switch based on the 'friendly' id from the XML layout
switch (rb.getId()) {
   case R.id.radioButton1:
          // Do something here
          break;
   case R.id.radioButton2:
          // Do something here
          break;
   case R.id.radioButton3:
          // Do something here
          break;
}
// End switch block

为了使这更清晰,我们将在下一个工作应用程序中实时查看它的运行情况,我们可以按下按钮。

让我们继续探索调色板。

探索调色板和更多匿名类 – 第二部分

现在我们已经了解了匿名类是如何工作的,特别是与RadioGroupRadioButton一起,我们可以继续探索调色板,并查看匿名类如何与更多 UI 小部件一起工作。

Switch

Switch(不要与小写的switch Java 关键字混淆)小部件就像Button小部件一样,只是它有两种可能的状态可以读取和响应。

Switch小部件的一个明显用途是显示或隐藏某些内容。请记住,在我们的 Java Meet UI 应用程序中,在第十二章中,堆栈、堆和垃圾收集器,我们使用了Button小部件来显示和隐藏TextView小部件。

每次我们隐藏或显示TextView小部件时,我们都会更改Button小部件上的text属性,以清楚地表明如果再次点击它会发生什么。对于用户来说,以及对我们作为程序员来说,更直观、更简单的做法可能是使用Switch小部件,如下所示:

图 13.2 – Switch 小部件

图 13.2 – Switch 小部件

以下代码假设我们已经有一个名为mySwitch的对象,它是布局中Switch对象的引用。我们可以像在我们的 Java Meet UI 应用程序中第十二章中那样显示和隐藏TextView小部件。

为了监听并响应点击,我们再次使用匿名类。但是,这一次,我们使用CompoundButton版本的OnCheckedChangedListener,而不是RadioGroup版本。

我们需要重写onCheckedChanged方法,该方法有一个名为isChecked的布尔参数。isChecked变量对于关闭来说只是false,对于打开来说只是true

以下是我们如何更直观地替换第十二章中的文本隐藏/显示代码,堆栈、堆和垃圾收集器

mySwitch.setOnCheckedChangeListener(
   new CompoundButton.OnCheckedChangeListener() {

         public void onCheckedChanged(
                CompoundButton buttonView, boolean 
                isChecked) {

                if(isChecked){
                      // Currently visible so hide it
                      txtValue.setVisibility(View.
                      INVISIBLE);

                }else{
                      // Currently hidden so show it
                      txtValue.setVisibility(View. 
                      VISIBLE);
                }
         }
   }
);

如果匿名类代码看起来有点奇怪,不要担心,因为随着您不断使用它,它会变得更加熟悉。现在我们将在查看CheckBox小部件时这样做。

复选框

这是一个CheckBox小部件。它可以是选中的,也可以是未选中的。在下面的屏幕截图中,它是选中的:

图 13.3 – CheckBox 小部件

图 13.3 - CheckBox 小部件

使用CheckBox小部件,我们可以简单地在特定时刻检测其状态(选中或未选中)-例如,在特定按钮被点击时。以下代码让我们可以看到这种情况是如何发生的,再次使用内部类作为监听器:

myCheckBox.setOnCheckedChangeListener(
   new CompoundButton.OnCheckedChangeListener() {

         public void onCheckedChanged(
                CompoundButton buttonView, boolean 
                isChecked) {
                if (myCheckBox.isChecked()) {
                      // It's checked so do something
                } else {
                      // It's not checked do something else
                }
         }
   }
);

在前面的代码中,我们假设myCheckBox已经被声明和初始化。然后,我们使用了与Switch相同类型的匿名类,以便检测和响应点击。

TextClock

在我们的下一个应用程序中,我们将使用TextClock小部件展示一些它的特性。我们需要直接添加 XML,因为这个小部件不能从调色板中拖放。TextClock小部件看起来类似于以下截图:

图 13.4 - TextClock 小部件

图 13.4 - TextClock 小部件

让我们看一个使用TextClock的例子。这是我们如何将其时间设置为与布鲁塞尔,欧洲的时间相同的方法:

tClock.setTimeZone("Europe/Brussels");

上面的代码假设tClock是布局中TextClock小部件的引用。

使用 WebView

WebView 是一个非常强大的小部件。它可以用来在应用的 UI 中显示网页。你甚至可以只用几行代码来实现一个基本的网页浏览器应用程序。

提示

通常情况下,你不会实现一个完整的网页浏览器;相反,你会使用用户首选的网页浏览器。

要简单地获取 XML 中存在的WebView小部件的引用并显示一个网站,你只需要两行代码。这段代码加载了我的网站- gamecodeschool.com - 假设布局中有一个id属性设置为webViewWebView小部件:

WebView webView = findViewById(R.id.webView);
webView.loadUrl("https://gamecodeschool.com");

有了所有这些额外的信息,让我们制作一个比我们迄今为止更广泛地使用 Android 小部件的应用程序。

小部件探索应用程序

到目前为止,我们已经讨论了七个小部件:EditTextImageViewRadioButton(和RadioGroup)、SwitchCheckBoxTextClockWebView。让我们制作一个可工作的应用程序,并对每个小部件做一些真实的事情。我们还将再次使用Button小部件和TextView小部件。

请记住,你可以在下载包中找到已完成的代码。这个应用程序可以在第十三章/小部件探索中找到。

设置小部件探索项目和 UI

首先,我们将设置一个新项目并准备 UI 布局。这些步骤将在屏幕上排列所有小部件并设置id属性,准备好引用它们。在开始之前,看一下目标布局是很有用的-当它正在运行时。看一下以下截图:

图 13.5 - 小部件探索布局

图 13.5 - 小部件探索布局

这个应用程序将演示这些小部件:

  • 单选按钮允许用户将时钟显示的时间更改为三个时区中的一个选择。

  • TextView小部件(在右侧)显示EditText小部件(在左侧)中当前的内容。

  • 这三个CheckBox小部件将为 Android 机器人图像添加和删除视觉效果。

  • Switch小部件将打开和关闭TextView小部件,显示在EditText小部件中输入的信息,并在按下按钮时捕获。

  • WebView小部件将占据应用程序的整个宽度和下半部分。在添加小部件到布局时要记住这一点;尽量将它们全部放在上半部分。

确切的布局位置并不重要,但指定的id属性必须完全匹配。如果你只想查看/使用代码,你可以在下载包的第十三章/小部件探索文件夹中找到所有文件。

因此,让我们执行以下步骤来设置一个新项目并准备 UI 布局:

  1. 创*一个名为Widget Exploration的新项目。设置API 17:Android 4.2 (Jelly Bean)。然后,使用一个空活动,并保持所有其他设置为默认值。我们使用API 17是因为TextClock小部件的一个功能需要我们这样做。我们仍然可以支持超过 99%的所有 Android 设备。

  2. 切换到activity_main.xml布局文件,并确保您处于设计视图中。删除默认的TextView小部件。

  3. 使用显示在设计视图上方的下拉控件(如下面的屏幕截图所示),选择横向方向的*板电脑。我选择了Pixel C选项:图 13.6 - 选择方向选项

图 13.6 - 选择方向选项

重要提示

有关如何制作*板模拟器的提醒,请参阅第三章探索 Android Studio 和项目结构。有关如何操作模拟器方向的其他*议,请参阅第五章使用 CardView 和 ScrollView 创*美丽的布局

  1. 从调色板的按钮类别中拖动一个开关小部件到布局的右上角附近。然后,在这下面,添加一个TextView小部件。您的布局的右上角现在应该看起来类似于以下的屏幕截图:图 13.7 - 将开关小部件添加到布局

图 13.7 - 将开关小部件添加到布局

  1. 拖动三个sym_def_app_icon以使用 Android 图标作为ImageView的图像。布局的中间部分现在应该看起来类似于以下的屏幕截图。有关最终布局的更多上下文,请参考显示完成的应用程序的屏幕截图:图 13.8 - 复选框小部件

图 13.8 - 复选框小部件

  1. RadioGroup拖到布局的左上角。

  2. RadioGroup内添加三个RadioButton小部件。可以使用组件树窗口轻松完成此步骤。

  3. RadioGroup下面,从调色板的文本类别中拖动一个纯文本小部件。请记住,尽管它的名字是这样,但这是一个允许用户在其中输入一些文本的小部件。稍后,我们将学*如何捕获和使用输入的文本。

  4. 纯文本小部件下面添加一个按钮小部件。您的布局的左侧应该看起来类似于以下的屏幕截图:图 13.9 - 添加一个按钮小部件

图 13.9 - 添加一个按钮小部件

  1. 现在,为我们刚刚布置的小部件添加以下属性:

重要提示

请注意,一些属性可能已经默认正确。

  1. 倒数第二个小部件有点不同,所以我认为我们应该单独处理它。在左侧的按钮小部件下方再添加一个常规的TextView小部件,并将其id属性设置为textClock。请记住,与所有其他小部件一样,保持此小部件在垂直方向上大约中点以上。如果需要,重新调整其上方的一些小部件。

  2. 切换到代码视图,并找到我们正在处理的TextView小部件 - 其id属性值为textClock的那个。

  3. 观察 XML 代码的开头,如下面的代码片段所示,其中突出显示了一些关键部分:

<TextView to TextClock, and we have deleted the text property and replaced it with the format that we would like for our clock. 
  1. 切换到设计选项卡。

  2. 现在是最后一个小部件的时间了。从WebView小部件中拖动一个WebView小部件。实际上,如果你仔细观察,你会发现WebView小部件似乎不见了。事实上,如果你仔细观察,你会发现WebView小部件在布局的左上角有一个微小的指示。我们将稍微不同地配置WebView小部件的位置和大小。

  3. 确保在组件树窗口中选择了WebView小部件。

  4. id属性更改为webView(如果尚未是此值)。

  5. 为了使下一步正常工作,所有其他小部件都必须受到约束。因此,单击推断约束按钮以确保所有其他小部件。

  6. 目前,我们的WebView小部件没有受到任何约束,也无法抓取我们需要的约束手柄。现在,在属性窗口中找到布局部分,如下面的屏幕截图所示:图 13.10 – 添加约束

图 13.10 – 添加约束

  1. 单击上一个屏幕截图中突出显示的添加约束到底部按钮。现在,我们有一个约束,其中WebView小部件的底部受到布局底部的约束。这几乎是完美的,但默认边距设置得非常高。

  2. 在属性窗口中找到layout_margin_bottom属性,并将其更改为0dp

  3. 在属性窗口中将layout_height属性更改为400dp。请注意,当此项目完成时,如果您的WebView小部件太高或太矮,那么您可以回来调整此值。

  4. 调整您的布局,使其尽可能地类似于以下参考图。但是,如果您具有正确的 UI 类型和正确的id属性,即使布局不完全相同,代码仍将起作用。请记住,WebView小部件是不可见的,但是一旦我们进行了一些编码,它将占据屏幕的下半部分。

图 13.11 – 调整布局

图 13.11 – 调整布局

我们刚刚布置并设置了布局所需的属性。除了一些小部件类型对我们来说是新的,布局略微更加复杂之外,这里没有我们以前没有做过的事情。

现在我们可以开始在我们的 Java 代码中使用所有这些小部件了。

编写 Widget Exploration 应用程序的代码

此应用程序需要许多import语句。因此,让我们现在添加它们,以免每次都提到它们。添加以下import语句:

import android.graphics.Color;
import android.graphics.PorterDuff;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AnalogClock;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.TextClock;
import android.widget.TextView;

让我们在 Java 代码中获取我们将在 UI 中使用的所有部分的引用。

获取对 UI 的所有部分的引用

下一块代码看起来相当长而杂乱,但我们所做的只是获取布局中每个小部件的引用。当我们使用它们时,我们将更详细地讨论代码。

在下一块代码中唯一新的是,一些对象被声明为final。这是必要的,因为它们将在匿名类中使用。

但是final不是意味着对象不能被更改吗?

如果您回忆第十一章**,更多面向对象编程,我们学到声明为final的变量不能被更改,也就是说,它们是常量。那么,我们如何改变这些对象的属性呢?请记住,对象是引用类型变量。这意味着它们引用堆上的一个对象。它们不是对象本身。我们可以将它们视为持有对象的地址。地址是不会改变的。我们仍然可以使用地址引用堆上的对象,并随意更改实际对象。让我们进一步使用地址的类比。如果您住在特定地址,如果地址是最终的,那么您就不能搬到新房子。但是,您在该地址上可以做任何事情。例如,您仍然可以重新布置您的房子,也许重新粉刷客厅,并在厨房放浴缸,把沙发放在屋顶上。

onCreate方法中的setContentView方法调用之后立即输入以下代码:

// Get a reference to all our widgets
RadioGroup radioGroup =
findViewById(R.id.radioGroup);
final EditText editText = 
findViewById(R.id.editText);
final Button button = 
findViewById(R.id.button);
final TextClock tClock = 
findViewById(R.id.textClock);
final CheckBox cbTransparency = 
findViewById(R.id.checkBoxTransparency);
final CheckBox cbTint = 
findViewById(R.id.checkBoxTint);
final CheckBox cbReSize = 
findViewById(R.id.checkBoxReSize);
final ImageView imageView = 
findViewById(R.id.imageView);
Switch switch1 = (Switch) findViewById(R.id.switch1);
final TextView textView = 
findViewById(R.id.textView);
// Hide the TextView at the start of the app
textView.setVisibility(View.INVISIBLE);

我们现在在我们的 Java 代码中引用了我们需要操作的布局中的所有 UI 元素。

编写复选框的代码

现在,我们可以创*一个匿名类来监听和处理复选框的点击。接下来的三个代码块分别实现了每个复选框的匿名类。然而,每个以下三个代码块中的不同之处在于我们如何响应点击;我们将依次讨论每个。

更改透明度

第一个复选框标记为imageView中的setAlpha方法,以更改其透明度。setAlpha方法以 0 到 1 之间的浮点值作为参数。

0 是不可见的,1 表示完全不透明。因此,当选中此复选框时,我们将 alpha 设置为.1,这意味着图像几乎不可见。当取消选中时,我们将其设置为1,这意味着它完全可见,没有透明度。onCheckedChangedboolean isChecked参数包含truefalse,以显示复选框是否被选中。

onCreate方法中的上一个代码块之后添加以下代码:

/*
   Now we need to listen for clicks
   on the button, the CheckBoxes 
   and the RadioButtons
*/
// First the check boxes using an anonymous class
cbTransparency.setOnCheckedChangeListener(new 
CompoundButton.OnCheckedChangeListener(){
   public void onCheckedChanged(
   CompoundButton buttonView, boolean isChecked){
         if(cbTransparency.isChecked()){
                // Set some transparency
                imageView.setAlpha(.1f);
         }else{
                imageView.setAlpha(1f);
         }
   }
});

在下一个匿名类中,我们将处理标记为Tint的复选框。

更改颜色

onCheckedChanged方法中,我们使用imageView中的setColorFilter方法在图像上叠加一个颜色层。当isChecked为 true 时,我们叠加一个颜色,当isChecked为 false 时,我们移除它。

setColorFilter方法接受Color类中的argb颜色。argb方法的四个参数分别是 alpha、red、green 和 blue 的值。这四个值创*了一种颜色。在我们的情况下,值15025500创*了强烈的红色色调。另外,值0000则完全没有色调。

重要说明

要了解更多关于Color类的信息,请访问 Android 开发者网站developer.android.com/reference/android/graphics/Color.html。此外,要更详细地了解 RGB 颜色系统,请参考以下维基百科页面:en.wikipedia.org/wiki/RGB_color_model

onCreate方法中的上一个代码块之后添加以下代码:

// Now the next checkbox
cbTint.setOnCheckedChangeListener(new 
CompoundButton.OnCheckedChangeListener() {
   public void onCheckedChanged(CompoundButton 
   buttonView, boolean isChecked) {
         if (cbTint.isChecked()) {
                // Checked so set some tint
                imageView.setColorFilter(
                Color.argb(150, 255, 0, 0));
         } else {
                // No tint needed
                imageView.setColorFilter(Color.argb(0, 0, 
                0, 0));
         }
   }
});

现在我们将看看如何调整 UI 的比例。

更改大小

在处理标记为setScaleX方法的匿名类中,可以调整机器人图像的大小。当我们在imageView中调用setScaleX(2)setScaleY(2)时,我们将使图像的大小加倍,而setScaleX(1)setScaleY(1)将使其恢复正常。

onCreate方法中的上一个代码块之后添加以下代码:

// And the last check box
cbReSize.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {
   public void onCheckedChanged(
   CompoundButton buttonView, boolean isChecked) {
         if (cbReSize.isChecked()) {
                // It's checked so make bigger
                imageView.setScaleX(2);
                imageView.setScaleY(2);
         } else {
                // It's not checked make regular size
                imageView.setScaleX(1);
                imageView.setScaleY(1);
         }
   }
});

现在我们将处理这三个单选按钮。

编写单选按钮

由于它们是RadioGroup小部件的一部分,我们可以处理它们的方式比处理CheckBox对象要简洁得多。我们是这样做的。

首先,我们确保它们一开始是清除的,通过在radioGroup中调用clearCheck()。然后,我们创*OnCheckedChangedListener类型的匿名类,并重写onCheckedChanged方法。

当点击RadioGroup中的任何RadioButton小部件时,将调用此方法。我们需要做的就是获取被点击的RadioButton小部件的id属性,并做出相应的响应。我们可以通过使用一个switch语句来实现这一点,有三种可能的情况,每种情况对应一个RadioButton

您会记得当我们第一次谈论RadioButton小部件时,我们提到onCheckedChanged方法的checkedId参数中提供的值是一个整数。这就是为什么我们必须首先从checkedId参数创*一个新的RadioButton实例的原因:

RadioButton rb = 
(RadioButton) group.findViewById(checkedId);

然后,我们可以在新的RadioButton实例上调用getId作为switch块的条件:

switch (rb.getId())

然后,在每个case选项中,我们使用带有适当 Android 时区代码的setTimeZone方法。

提示

您可以在gist.github.com/arpit/1035596查看所有 Android 时区代码。

看一下以下代码,它包含了我们刚刚讨论的所有内容。在先前输入的处理复选框的代码之后,将其添加到onCreate方法中:

// Now for the radio buttons
// Uncheck all buttons
radioGroup.clearCheck();
radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(
RadioGroup group, int checkedId) {
         RadioButton rb =
         group.findViewById(checkedId);

                switch (rb.getId()) {
                      case R.id.radioButtonLondon:
                            tClock.setTimeZone(
                            "Europe/London");
                            break;
                      case R.id.radioButtonBeijing:
                            tClock.setTimeZone("Etc/GMT-
                            8");
                            break;
                      case R.id.radioButtonNewYork:

                            tClock.setTimeZone(
                            "America/New_York");
                            break; 
                }// End switch block
   }
});

现在来点新鲜的东西。

使用匿名类处理普通按钮

在下一段代码中,我们将编写并使用一个匿名类来处理普通Button的点击。我们调用button.setOnclickListener,就像以前一样。但是,这一次,我们不是像以前那样将this作为参数传递,而是创*一个全新的View.OnClickListener类型的类,并覆盖onClick方法作为参数 - 就像我们以前的其他匿名类一样。

提示

在这种情况下,这种方法是可取的,因为只有一个按钮。如果我们有很多按钮,那么让MainActivity实现View.OnClickListener,然后覆盖onClick方法以处理所有点击的方法可能更可取,就像我们以前做过的那样。

onClick方法中,我们使用setText方法在textView上设置text属性,并使用editTextgetText方法获取EditText小部件中当前的文本。

onCreate方法中的先前代码块之后添加以下代码:

/*
   Let's listen for clicks on our "Capture" Button.
   We can do this with an anonymous class as well.
   An interface seems a bit much for one button.
*/
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          // We only handle one button
          // So, no switching required

          // Change the text on the TextView 
          // to whatever is currently in the EditText
          textView.setText(editText.getText());
   }
});

编写 Switch 小部件

接下来,我们将创*另一个匿名类来监听和处理Switch小部件的更改。

isChecked变量为true时,我们显示textView;当它为false时,我们隐藏它。

onCreate方法中的先前代码块之后添加以下代码:

// Show or hide the TextView
switch1.setOnCheckedChangeListener(
   new CompoundButton.OnCheckedChangeListener() {

   public void onCheckedChanged(
         CompoundButton buttonView, boolean isChecked) {

         if(isChecked){
                textView.setVisibility(View.VISIBLE);
         }else{
                textView.setVisibility(View.INVISIBLE);
         }
   }
});

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

使用 WebView

您的清单必须包括INTERNET权限。这是我们添加它的方式。

打开AndroidManifest.xml文件,并添加以下突出显示的代码行,显示了一些上下文:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.gamecodeschool.widgetexploration">
    <uses-permission android:name="android
      .permission.INTERNET" />
    <application
     …
     …

最后,我们将添加另外两行代码,以便获取对WebView小部件的引用并加载网站。修改代码以加载您喜欢的任何网站应该是一个相对简单的过程。在onCreate方法的末尾添加以下代码行:

WebView webView = (WebView) findViewById(R.id.webView);
webView.loadUrl("https://gamecodeschool.com");

现在我们可以运行我们的应用程序并尝试所有功能。

运行 Widget Exploration 应用程序

以通常的方式在*板模拟器上运行应用程序。

提示

Android 模拟器可以通过在 PC 上按下Ctrl + F11键组合或在 Mac 上按下Ctrl + fn+ F11来旋转为横向模式。

这是整个应用程序,包括现在可见的WebView小部件:

图 13.12 - 最终应用程序布局

图 13.12 - 最终应用程序布局

尝试选中单选按钮,看看时区在时钟上的变化。在下图中,我将一些裁剪的屏幕截图合在一起,以显示选择新时区时时间的变化:

图 13.13 - 时区

图 13.13 - 时区

要测试CAPTURE按钮、可编辑文本和开关,请按照以下步骤进行(我们也在相邻的屏幕截图中列出了它们):

  1. EditText小部件(位于左侧)中输入不同的值。

  2. 点击CAPTURE按钮。

  3. 确保Switch小部件是打开的。

  4. 查看消息:

图 13.14 - 测试 CAPTURE 按钮

图 13.14 - 测试 CAPTURE 按钮

您可以通过不同的选中和未选中复选框的组合来更改前面的图表外观,并且您可以使用上面的开关来隐藏和显示TextView小部件。以下屏幕截图显示了当您选择TintRe-size选项时ImageView小部件会发生什么:

图 13.15 - 测试 ImageView 小部件

图 13.15 - 测试 ImageView 小部件

糟糕!图标的大小增加得太多,以至于它与Re-size复选框重叠。

提示

透明度在印刷书籍中并不清晰,所以我没有展示“透明度”框被选中的视觉示例。一定要在模拟器或真实设备上尝试一下。

总结

在本章中,我们学到了很多,并且探索了大量的小部件。我们学会了如何在 Java 代码中实现小部件而不需要任何 XML,并且我们使用了我们的第一个匿名类来处理小部件上的点击,并将我们所有新的小部件技能应用到一个工作中的应用程序中。

现在,让我们继续看看另一种显著增强我们 UI 的方式。

在下一章中,我们将看到一个全新的 UI 元素,我们不能简单地从调色板中拖放,但我们仍然会得到来自 Android API 的大量帮助。接下来是对话框窗口。此外,我们还将开始制作迄今为止最重要的应用程序,即备忘录、待办事项列表和个人笔记的“自我备忘录”应用程序。

第十四章:Android 对话框窗口

在本章中,我们将学*如何向用户呈现弹出对话框窗口。然后,我们可以将我们所知道的全部内容放入我们的第一个应用程序Note to Self的第一阶段。接下来的四章(直到第十八章**, 本地化)中,我们将探索最新的 Android 和 Java 功能,并利用我们新获得的知识来增强 Note to Self 应用程序的每个阶段。

每一章还将构*一系列与此主要应用程序分开的较小的应用程序。那么,这一章对您有什么意义呢?好吧,我们将涵盖以下主题:

  • 使用弹出对话框框架创*一个简单的应用程序

  • 使用DialogFragment类开始 Note to Self 应用程序

  • 在我们的项目中添加字符串资源,而不是在布局中硬编码文本

  • 首次使用 Android 命名约定,使我们的代码更易读

  • 实现更复杂的对话框以捕获用户输入

让我们开始吧。

技术要求

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

对话框窗口

在我们的应用程序中,我们经常需要在弹出窗口中向用户显示一些信息,甚至要求确认某个操作。这就是所谓的对话框窗口。如果您快速扫描 Android Studio 中的调色板,您可能会惊讶地发现根本没有提到对话框。

在 Android 中,对话框比简单的小部件甚至整个布局更高级。它们是可以拥有自己的布局和其他用户界面UI)元素的类。

在 Android 中创*对话框窗口的最佳方式是使用FragmentDialog类。

注意

片段是 Android 中一个广泛而重要的主题,我们将在本书的后半部分花费大量时间来探索和使用它们。

为用户创*一个整洁的弹出对话框(使用FragmentDialog)是对片段的一个很好的介绍,而且一点也不复杂。

创*对话框演示项目

我们之前提到,在 Android 中创*对话框的最佳方式是通过FragmentDialog类。

为此,在 Android Studio 中使用空活动模板创*一个新项目,并将其命名为Dialog Demo。正如您所期望的那样,该项目的完整代码位于下载包的第十四章/Dialog Demo文件夹中。

编写一个 DialogFragment 类

通过右键单击包含MainActivity.java文件的包名称(与MainActivity.java文件相同的包名称)在 Android Studio 中创*一个新类。选择MyDialog。按下Enter键创*类。

首先要做的是将类声明更改为扩展DialogFragment。完成后,您的新类应该类似于以下代码块:

public class MyDialog extends DialogFragment {    
}

现在,让我们一点一点地向这个类添加代码,并解释每一步发生了什么。现在,我们需要导入DialogFragment类。您可以通过按住Alt键然后点击Enter,或者在MyDialog.java文件顶部的包声明之后添加以下突出显示的代码行来实现这一点:

package com.gamecodeschool.dialogdemo;
import androidx.fragment.app.DialogFragment;
public class MyDialog extends DialogFragment {
}

就像在 Android API 中的许多类一样,DialogFragment为我们提供了可以重写以与该类发生的不同事件交互的方法。

添加以下突出显示的代码以重写onCreateDialog方法。仔细研究它,我们将在下一步中检查发生了什么:

public class MyDialog extends DialogFragment {
   @Override
   public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the Builder class because this dialog 
   // has a simple UI
AlertDialog.Builder builder = 
          new AlertDialog.Builder(getActivity());
   }
}

您需要按照通常的方式导入DialogBundleAlertDialog类,或者通过手动添加以下突出显示的代码来导入:

import android.app.Dialog;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;

注意

代码中仍然有一个错误,因为我们忘记了 onCreateDialog 方法的 return 语句。当我们完成了方法的其余部分编码后,我们将在稍后添加这个 return 语句。

在前面的代码中,我们首先添加了重写的 onCreateDialog 方法。当我们稍后通过 MainActivity 类中的代码向用户显示对话框时,Android 将调用此方法。

然后,在 onCreateDialog 方法内部,我们得到了一个新的类。我们声明并初始化了一个 AlertDialog.Builder 类型的对象,它需要 MainActivity 的引用传递给它的构造函数。这就是为什么我们使用 getActivity()方法作为参数。

getActivity 方法是 Fragment 类(因此也是 DialogFragment)的一部分,它返回一个对 Activity 的引用,该 Activity 将创* DialogFragment。在这种情况下,就是我们的 MainActivity 类。

现在我们已经声明并初始化了 builder,让我们看看我们可以用 builder 做些什么。

使用链接配置 DialogFragment

现在我们可以使用 builder 对象来完成其余的工作。在下面的代码片段中有一些奇怪的地方。如果您快速扫描下面的三个代码块,您会注意到缺少分号;。事实上,这三个代码块对于编译器来说只是一行。

我们以前见过这种情况,但情况没有那么明显;也就是说,当我们创*一个 Toast 消息并在其末尾添加.show()方法时。这被称为链接。这是指我们在同一个对象上按顺序调用多个方法。这相当于编写多行代码;只是这种方式更简洁。

在我们添加的先前代码之后,在 onCreateDialog 方法中添加以下代码(使用链接)。检查新代码,我们将在下面讨论它:

// Dialog will have "Make a selection" as the title
builder.setMessage("Make a selection")

// An OK button that does nothing
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
   public void onClick(DialogInterface dialog, int id) {
          // Nothing happening here
   }
})
// A "Cancel" button that does nothing 
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {

   public void onClick(DialogInterface dialog, int id) {
          // Nothing happening here either

   }

});

此时,您需要导入 DialogInterface 类。使用 Alt | Enter 技术,或者在其他 import 语句中添加以下代码行:

import android.content.DialogInterface;

以下是我们刚刚添加的代码的每个部分的解释:

  1. 在三个使用链接的代码块中的第一个代码块中,我们调用 builder.setMessage。这将设置用户在对话框中看到的主要消息。另外,请注意,可以在链接方法调用的各个部分之间添加注释,因为编译器会忽略这些注释。

  2. 然后,我们使用.setPositiveButton 方法向对话框添加一个按钮,该方法的第一个参数将按钮的文本设置为“确定”。第二个参数是一个匿名类,称为 DialogInterface.OnClickListener,它处理按钮的任何点击。请注意,我们不打算向 onClick 方法添加任何代码。在这里,我们只是想看到这个简单的对话框;我们将在下一个项目中进一步进行。

  3. 接下来,我们在同一个 builder 对象上调用另一个方法。这次是 setNegativeButton 方法。同样,两个参数将“取消”设置为按钮的文本,并添加一个匿名类来监听点击。同样,出于演示目的,我们不会在重写的 onClick 方法中采取任何行动。在调用 setNegativeButton 方法之后,我们最终看到一个分号标记着代码行的结束。

最后,我们将编写 return 语句以完成该方法,并消除我们一开始就有的错误。在 onCreateDialog 方法的最后(但在最终大括号内部)添加 return 语句,如下面的代码片段所示:

…
// Create the object and return it
return builder.create();
}// End of onCreateDialog

这行代码的最后效果是将我们新的、完全配置好的对话框窗口返回给 MainActivity 类(它首先调用 onCreateDialog 方法)。我们将稍后检查并添加这个调用代码。

现在,我们有了扩展 FragmentDialog 的 MyDialog 类。我们所需要做的就是声明一个 MyDialog 的实例,实例化它,并调用它重写的 createDialog 方法。

使用 DialogFragment 类

在转向代码之前,让我们向布局中添加一个按钮。执行以下步骤:

  1. 切换到activity_main.xml标签,然后切换到设计标签。

  2. 拖动id属性设置为button

  3. 点击MyDialog类是目前的关键课程。

现在切换到MainActivity.java标签,我们将使用匿名类处理对这个新按钮的点击,就像我们在第十三章中的匿名类 - 让 Android 小部件活跃中的 Widget Exploration 应用程序中所做的那样。我们这样做是因为布局中只有一个按钮,这似乎比实现更复杂的OnClickListener接口替代方案更合理和更紧凑(就像我们在第十二章中为 Java Meet UI 演示应用程序所做的那样,堆栈、堆和垃圾收集器)。

请注意,在以下代码块中,匿名类与我们先前为其实现了接口的类型完全相同。将此代码添加到onCreate方法中:

/*
   Let's listen for clicks on our regular Button.
   We can do this with an anonymous class.
*/
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(
   new View.OnClickListener() {

         @Override
         public void onClick(View v) {
                // We only handle one button
                // Therefor no switching required
                MyDialog myDialog = new MyDialog();
                myDialog.show(getSupportFragmentManager(), 
                "123");
                // This calls onCreateDialog
                // Don't worry about the strange looking 
                   123
                // We will find out about this in chapter 
                   18
         }
   }
);

注意

以下import语句是此代码所需的:

import android.view.View;

import android.widget.Button;

请注意,代码中唯一发生的事情是onClick方法创*了MyDialog的一个新实例,并调用了它的show方法。毫不奇怪,这将显示我们的对话框窗口,就像我们在MyDialog类中配置的那样。

show方法需要一个对FragmentManager的引用,我们可以使用getSupportFragmentManager来获取。这是跟踪和控制活动的所有Fragment实例的类。我们还传入一个标识符:"123"

当我们更仔细地查看片段时,将会透露有关FragmentManager的更多细节。我们将在第二十章Android 片段部分进行。绘图图形

注意

我们使用getSupportFragmentManager方法的原因是,我们通过扩展AppCompatActivity来支持旧设备。如果我们简单地扩展Activity,那么我们可以使用getFragmentManager类。缺点是应用程序将无法在许多设备上运行。

现在我们可以运行应用程序,并欣赏我们的新对话框窗口,当我们点击布局中的按钮时它会出现。请注意,单击对话框窗口中的任一按钮都将关闭它。这是默认行为。以下截图显示了我们的对话框在 Pixel C *板模拟器上的运行情况:

图 14.01 - 对话框在运行中

图 14.01 - 对话框在运行中

接下来,我们将制作另外两个实现对话框的类,作为我们多章节的Note to Self应用的第一阶段。我们将了解到对话框窗口几乎可以有我们选择的任何布局,而不必依赖于Dialog.Builder类给我们的简单布局。

Note to Self应用程序

欢迎来到我们将在本书中实施的三个主要应用程序中的第一个。当我们执行这些项目时,我们将比较专业地执行它们。我们将使用 Android 命名约定、字符串资源和适当的封装。

有时,当您试图学*新的 Android/Java 主题时,这些东西可能会有些过度。然而,它们是有用的,尽快在真实项目中开始使用它们是很重要的。最终,它们会变得自然而然,我们的应用程序质量会受益。

使用命名约定和字符串资源

第三章中,探索 Android Studio 和项目结构,我们讨论了在布局文件中使用字符串资源而不是硬编码文本。这样做有一些好处,但也相对冗长。

由于这是我们的第一个真实项目,现在是一个很好的时机以正确的方式做事,这样我们就可以获得这样做的经验。如果您想快速了解字符串资源的好处,请返回第三章探索 Android Studio 和项目结构

命名约定是我们代码中用于命名变量、方法和类的约定或规则。在本书中,我们已经宽松地应用了 Android 的命名约定。由于这是我们的第一个真实应用程序,我们将在应用这些命名约定时稍微严格一些。

特别要注意的是,当变量是类的成员时,我们将使用小写的m作为前缀。

注意

您可以在 https://source.android.com/source/code-style.html 找到有关 Android 命名约定和代码风格的更多信息。

如何获取“Note to Self”应用程序的代码文件

完整的应用程序,包括所有代码和资源,可以在第十八章/Note to self文件夹中找到下载包中。由于我们将在接下来的五章中实现此应用程序,因此在每章结束时查看部分完成的可运行应用程序可能会有所帮助。部分完成的可运行应用程序及其所有相关代码和资源可以在各自的文件夹中找到:

第十四章/Note to self

第十六章/Note to self

第十七章/Note to self

第十八章/Note to self

注意

第十五章中没有“Note to Self”代码,数组映射和随机数。这是因为,即使我们将学*“Note to Self”中使用的主题,我们也不会在第十六章中进行任何更改,适配器和回收器

如果您正在跟着做,并打算从头到尾构*“Note to Self”应用程序,我们将构*一个名为Note to self的项目。但是,这并不妨碍您随时查看每章项目的代码文件,进行一些复制和粘贴。只是要注意,在说明的各个时间点,您将被要求删除或替换前一章的偶尔一行代码。

因此,即使您复制和粘贴的次数多于编写代码的次数,请务必全文阅读说明,并参考书中的代码,以获取可能有用的额外注释。

在每一章中,代码将被呈现为如果您已经完全完成了上一章,将显示来自早期章节的代码,必要时作为我们新代码的有用上下文。

每一章都不会完全致力于“Note to Self”应用程序 - 我们将学*其他通常相关的事物,并构*一些更小/更简单的应用程序。因此,当我们开始“Note to Self”实现时,理论上我们将为此做好准备。

完成的应用程序

以下屏幕截图来自已完成的应用程序。当然,在开发的各个阶段,它看起来会略有不同。必要时,我们将参考更多屏幕截图,作为提醒或查看开发过程中的差异。

完成的应用程序将允许用户点击应用程序右下角的浮动操作按钮,以打开对话框窗口添加新的便签。以下屏幕截图显示了此功能的突出显示:

图 14.02 - 浮动操作按钮

图 14.02 - 浮动操作按钮

在左侧,您可以查看要点击的按钮,在右侧,您可以查看用户可以添加新便签的对话框窗口。

最终,随着用户添加更多的笔记,他们将在应用程序的主屏幕上拥有所有已添加的笔记的列表,如下屏幕截图所示。用户可以选择笔记是重要想法和/或待办事项

图 14.03 –主屏幕上的笔记

图 14.03 –主屏幕上的笔记

您将能够滚动列表并点击一个笔记,以在专门用于该笔记的另一个对话框窗口中查看它。以下屏幕截图显示了显示笔记的对话框窗口:

图 14.04 –显示所选笔记

图 14.04 –显示所选笔记

还将有一个简单(即非常简单)的设置屏幕,可以从菜单中访问。它将允许用户配置笔记列表是否以分隔线格式化。以下是设置菜单选项的操作:

图 14.05 –设置菜单选项

图 14.05 –设置菜单选项

现在我们确切地知道我们要构*什么,我们可以继续并开始实施它。

构*项目

现在让我们创*我们的新项目。使用基本活动模板。正如我们在第三章中讨论的那样,探索 Android Studio 和项目结构,此模板将生成一个简单的菜单和一个浮动操作按钮,这两者都在此项目中使用。将项目命名为Note to Self

准备字符串资源

在这里,我们将创*所有的字符串资源,我们将从布局文件中引用它们,而不是硬编码 UI 小部件的text属性,这是我们一直在做的。

要开始,请在项目资源管理器中的res/values文件夹中打开strings.xml文件。您将看到自动生成的资源。添加以下突出显示的字符串资源,我们将在项目的其余部分中在应用程序中使用它们。在闭合的</resources>标签之前添加以下代码:

...
<resources>
    <string name="app_name">Note to Self</string>
    <string name="action_settings">Settings</string>
    <!-- Strings used for fragments for navigation -->
    <string name="first_fragment_label">First 
               Fragment</string>
    <string name="second_fragment_label">Second 
               Fragment</string>
    <string name="next">Next</string>
    <string name="previous">Previous</string>
    <string name="hello_first_fragment">Hello first 
               fragment</string>
    <string name="hello_second_fragment">Hello second 
               fragment. Arg: %1$s</string>
    <string name="action_add">add</string>
    <string name="title_hint">Title</string>
    <string name="description_hint">Description</string>
    <string name="idea_text">Idea</string>
    <string name="important_text">Important</string>
    <string name="todo_text">To do</string>
    <string name="cancel_button">Cancel</string>
    <string name="ok_button">OK</string>

    <string name="settings_title">Settings</string>
    <string name="theme_title">Theme</string>
    <string name="theme_light">Light</string>
    <string name="theme_dark">Dark</string>
</resources>

在上述代码中,请注意每个字符串资源都有一个唯一的name属性,它使其与所有其他字符串资源区分开,并提供一个有意义的、有希望的、可记住的线索,以表示它所代表的实际字符串值。正是这些名称值,我们将在布局文件中引用。

我们将不需要再访问这个文件了。

编写 Note 类

这是应用程序的基本数据结构。这是一个我们将从头开始编写的类,它具有我们需要表示用户笔记之一的所有成员变量。在第十五章中,数组映射和随机数,我们将学*一些新的 Java,以便了解如何让用户拥有数十、数百甚至数千条笔记。

通过右键单击与您的包名称相同的文件夹来创*一个新类。预期的是,它也包含MainActivity.java文件。选择Note。按下Enter键创*类。

将以下突出显示的代码添加到新的Note类中:

public class Note {
    private String mTitle;
    private String mDescription;
    private boolean mIdea;
    private boolean mTodo;
    private boolean mImportant;
}

请注意,根据 Android 约定,我们的成员变量名都以m为前缀。另外,我们不希望任何其他类直接访问这些变量,所以它们都声明为private

因此,我们将需要为我们的每个成员添加一个 getter 和一个 setter 方法。将以下 getter 和 setter 方法添加到Note类中:

public String getTitle() {
   return mTitle;
}
public void setTitle(String mTitle) {
   this.mTitle = mTitle;
}
public String getDescription() {
   return mDescription;
}
public void setDescription(String mDescription) {
   this.mDescription = mDescription;
}
public boolean isIdea() {
   return mIdea;
}
public void setIdea(boolean mIdea) {
   this.mIdea = mIdea;
}
public boolean isTodo() {
   return mTodo;
}
public void setTodo(boolean mTodo) {
   this.mTodo = mTodo;
}
public boolean isImportant() {
   return mImportant;
}
public void setImportant(boolean mImportant) {
   this.mImportant = mImportant;
}

在上述列表中有相当多的代码,但没有什么复杂的。每个方法都指定了public访问权限,因此可以被任何具有对Note类型对象的引用的其他类使用。此外,对于每个变量,都有一个名为get...的方法和一个名为set...的方法。布尔类型变量的 getter 命名为is...。如果你仔细想想,这是一个合乎逻辑的名字,因为返回的答案要么是 true,要么是 false。

每个 getter 都只返回相关变量的值。每个 setter 都将相关变量的值设置为传递给方法的任何值。

注意

实际上,我们应该稍微增强我们的 setter,以确保传入的值在合理范围内。例如,我们可能希望检查并强制执行String mTtileString mDescription的最大或最小长度。这留作读者的练*回来完成。

让我们设计这两个对话框窗口的布局。

实施对话框设计

现在,我们将做一些我们以前做过很多次的事情,但这次是出于新的原因。正如我们所知,我们将有两个对话框窗口:一个用于用户输入新的笔记,另一个用于查看他们选择的笔记。

我们可以以与我们设计所有先前布局相同的方式设计这两个对话框窗口的布局。当我们开始为FragmentDialog类创* Java 代码时,我们将学*如何将这些布局结合起来。

首先,让我们为“新笔记”对话框添加一个布局。执行以下步骤:

  1. 右键单击dialog_new_note以选择文件名:字段。

  2. 默认情况下,左键单击ConstraintLayout类型,作为其根元素。

  3. 在按照以下说明进行操作时,请参考目标设计图。我已经使用 Photoshop 完成了布局,包括我们即将自动生成的约束条件,以及隐藏约束条件以增加清晰度的布局:图 14.06 – 新笔记的完成布局

图 14.06 – 新笔记的完成布局

  1. 文本类别中拖放一个纯文本小部件到布局的左上角。然后,在其下方添加另一个纯文本小部件。暂时不用担心任何属性。

  2. 按钮类别中拖放三个复选框小部件,一个放在另一个下面。查看之前的参考图以获得指导。现在,暂时不用担心任何属性。

  3. 将两个按钮拖放到布局中。第一个将直接放在上一步中最后一个复选框小部件的下方;第二个将水*放置,与第一个按钮对齐,但在布局的右侧。

  4. 整理布局,使其尽可能接近参考设计图。然后,点击推断约束条件按钮来修复您选择的位置。

  5. 现在,我们可以设置所有的textidhint属性。您可以使用以下表格中的值来完成这一步。请记住,我们正在使用字符串资源来为texthint属性赋值:

注意

当您编辑第一个id属性(接下来我们将要做的)时,可能会弹出一个窗口询问您是否确认更改。勾选本次会话中不再询问框,然后点击继续:

图 14.07 – 确认更改

图 14.07 – 确认更改

我们现在有一个有组织的布局,准备好供我们的 Java 代码显示。确保您记住不同小部件的id属性值。当我们编写 Java 代码时,我们将看到它们的作用。重要的是,我们的布局看起来很好,并且每个相关项目都有一个id属性值,这样我们就可以引用它。

让我们创*“显示笔记”对话框的布局:

  1. 右键单击dialog_show_note以选择文件名:字段。

  2. 默认情况下,左键单击ConstraintLayout类型以选择其根元素。

  3. 在按照以下说明进行操作时,请参考目标设计图。我已经使用 Photoshop 完成了布局,包括我们即将自动生成的约束条件,以及隐藏约束条件以增加清晰度的布局:图 14.08 – 显示笔记对话框的完成布局

图 14.08 - 显示笔记对话框的完成布局

  1. 首先,拖放三个TextView小部件,使它们在布局顶部垂直对齐。

  2. 接下来,拖放另一个TextView小部件。

  3. 在上一个小部件的下方但靠左添加另一个TextView小部件。

  4. 现在在布局的中心水*放置一个Button,但靠近底部。

  5. 整理布局,使其尽可能接近参考图表,然后点击Infer Constraints按钮来修复你选择的位置。

  6. 配置以下表中的属性:

注意

你可能想要通过稍微拖动它们来调整一些 UI 元素的最终位置,因为我们调整了它们的大小和内容。首先,点击btnOK到按钮 ID,可能会弹出一个对话框说这个 ID 已经存在,点击Continue来忽略弹出窗口。

现在我们有一个布局,可以用来向用户显示一个笔记。请注意,我们可以重复使用一些字符串资源。我们的应用程序变得越来越大,以这种方式做事情就越有益处。

编写对话框

现在我们已经为我们的两个对话框窗口(“显示笔记”和“新笔记”)设计好了,我们可以利用我们对FragmentDialog类的了解来实现一个类来代表用户可以交互的每个对话框窗口。

我们将从“新笔记”屏幕开始。

编写 DialogNewNote 类

通过右键单击包含所有.java文件的项目文件夹,选择DialogNewNote来创*一个新类。

首先,更改类声明并扩展DialogFragment。然后,重写onCreateDialog方法,这是这个类中所有其余代码的位置。为了实现这一点,确保你的代码与以下代码片段相同:

public class DialogNewNote extends DialogFragment { 
   @Override
   public Dialog onCreateDialog(Bundle savedInstanceState) {

         // All the rest of the code goes here

   }
}

注意

你还需要添加这些新的导入:

import androidx.fragment.app.DialogFragment;

import android.app.Dialog;

import android.os.Bundle;

我们暂时在新类中有一个错误,因为我们需要一个return语句;然而,我们马上就会解决这个问题。

在下一段代码中,首先,我们声明并初始化一个AlertDialog.Builder对象,方式与我们之前创*对话框窗口时一样。然而,这一次,我们将比以前更少地依赖这个对象。

接下来,我们初始化一个LayoutInflater对象,我们将使用它来填充我们的 XML 布局。通过“填充”,我们只是指的是如何将我们的 XML 布局转换为 Java 对象。一旦完成了这一步,我们就可以以通常的方式访问所有的小部件。我们可以将inflater.inflate方法视为替换对话框的setContentView方法。然后,在第二行,我们使用inflate方法做到了这一点。

添加我们刚刚讨论的三行代码:

AlertDialog.Builder builder = 
   new AlertDialog.Builder(getActivity());
LayoutInflater inflater = 
   getActivity().getLayoutInflater();

View dialogView = 
   inflater.inflate(R.layout.dialog_new_note, null);

注意

为了使用前面三行代码中的新类,你需要添加以下import语句:

import androidx.appcompat.app.AlertDialog;

import android.view.View;

import android.view.LayoutInflater;

现在我们有一个名为dialogViewView对象,它包含了我们的dialog_new_note.xml布局文件中的所有 UI 元素。

在上一段代码之后,我们将添加下面的代码块,接下来我们将解释。

这段代码将以通常的方式获取每个 UI 小部件的引用。以下代码中的许多对象被声明为final,因为它们将被用于匿名类,正如我们之前学到的那样,这是必要的。请记住,它是引用是final的(也就是说,它不能改变);我们仍然可以改变它们指向的堆上的对象。

在上一段代码之后添加以下代码:

final EditText editTitle = 
dialogView.findViewById(R.id.editTitle);
final EditText editDescription = 
dialogView.findViewById(R.id.editDescription);
final CheckBox checkBoxIdea =
dialogView.findViewById(R.id.checkBoxIdea);
final CheckBox checkBoxTodo =
dialogView.findViewById(R.id.checkBoxTodo);
final CheckBox checkBoxImportant = 
dialogView.findViewById(R.id.checkBoxImportant);
Button btnCancel = 
dialogView.findViewById(R.id.btnCancel);
Button btnOK = 
dialogView.findViewById(R.id.btnOK);

注意

添加以下import代码语句,使你刚刚添加的代码无错误:

import android.widget.Button;

import android.widget.CheckBox;

import android.widget.EditText;

在下一个代码块中,我们将使用builder(我们的构*器实例)设置对话框的消息。然后,我们将编写一个匿名类来处理对btnCancel按钮的点击。在重写的onClick方法中,我们将简单地调用dismiss(),这是DialogFragment的一个公共方法,用于关闭对话框窗口。如果用户单击取消,这正是我们需要的。

添加我们刚讨论过的以下代码:

builder.setView(dialogView).setMessage("Add a new note");
// Handle the cancel button
btnCancel.setOnClickListener( new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         dismiss();
   }
});

现在,我们将添加一个匿名类来处理用户单击btnOK时发生的情况。

首先,我们创*了一个名为newNote的新Note实例。然后,我们将newNote的每个成员变量设置为表单的适当内容。

在此之后,我们做了一些新的事情。我们使用getActivity方法创*对MainActivity类的引用。然后,我们使用该引用调用MainActivity中的createNewNote方法。

注意

请注意,我们还没有编写createNewNote方法,直到本章后面我们才会这样做。

发送到此方法的参数是我们新初始化的newNote对象。这将使用户的新笔记发送回MainActivity。我们将在本章后面学*如何处理这个。

最后,我们调用dismiss来关闭对话框窗口。

在前面的代码之后添加我们刚讨论过的以下代码:

btnOK.setOnClickListener(new View.OnClickListener() {

   @Override
   public void onClick(View v) {

         // Create a new note
         Note newNote = new Note();
         // Set its variables to match the 
         // user's entries on the form
         newNote.setTitle(editTitle.
                getText().toString());

         newNote.setDescription(editDescription.
                getText().toString());

         newNote.setIdea(checkBoxIdea.isChecked());
         newNote.setTodo(checkBoxTodo.isChecked());
         newNote.setImportant(checkBoxImportant.
                isChecked());
         // Get a reference to MainActivity
         MainActivity callingActivity = 
                (MainActivity) getActivity();

         // Pass newNote back to MainActivity
         callingActivity.createNewNote(newNote);
         // Quit the dialog
         dismiss();
   }
});
return builder.create();

这是我们的第一个对话框。我们还没有将其连接到MainActivity中,我们还需要实现createNewNote方法。我们将在创*下一个对话框后立即执行此操作。

编写 DialogShowNote 类

通过右键单击包含所有.java文件的项目文件夹并选择DialogShowNote来创*一个新类。

首先,更改类声明并扩展DialogFragment。然后,重写onCreateDialog方法。由于该类的大部分代码都在onCreateDialog方法中,因此实现签名和空主体,如下面的代码片段所示,我们稍后会重新访问它。

请注意,我们声明了一个Note类型的成员变量mNote。添加sendNoteSelected方法和初始化mNote的一行代码。此方法将由MainActivity调用,并将用户单击的Note对象传递给它。

添加我们刚讨论过的代码。然后,我们可以查看并编写onCreateDialog的细节:

public class DialogShowNote extends DialogFragment {
    private Note mNote;
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // All the other code goes here
    }
    // Receive a note from the MainActivity
    public void sendNoteSelected(Note noteSelected) {
        mNote = noteSelected;
    }
}

注意

此时,您需要导入以下类:

import android.app.Dialog;

import android.os.Bundle;

import androidx.fragment.app.DialogFragment;

接下来,我们声明并初始化了一个AlertDialog.Builder的实例。另外,就像我们对DialogNewNote类所做的那样,我们声明并初始化了一个LayoutInflater实例,然后使用它来创*一个具有对话框布局的View对象。在这种情况下,它是来自dialog_show_note.xml的布局。

最后,在以下代码中,我们获取对每个 UI 小部件的引用,并将text属性设置为mNote的适当成员变量的txtTitletextDescription,该成员变量在sendNoteSelected方法中初始化。

将我们刚讨论过的代码添加到onCreateDialog方法中:

// All the other code goes here
AlertDialog.Builder builder = 
      new AlertDialog.Builder(getActivity());
LayoutInflater inflater = 
      getActivity().getLayoutInflater();

View dialogView = 
      inflater.inflate(R.layout.dialog_show_note, null);
TextView txtTitle = 
       dialogView.findViewById(R.id.txtTitle);
TextView txtDescription = 
       dialogView.findViewById(R.id.txtDescription);
txtTitle.setText(mNote.getTitle());
txtDescription.setText(mNote.getDescription());
TextView txtImportant = 
       dialogView.findViewById(R.id.textViewImportant);
TextView txtTodo = 
       dialogView.findViewById(R.id.textViewTodo);
TextView txtIdea = 
       dialogView.findViewById(R.id.textViewIdea);

注意

添加以下import语句,以便前面代码中的所有类都可用:

import android.view.LayoutInflater;

import android.view.View;

import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;

我们将添加的以下代码也在onCreateDialog方法中。它检查所显示的笔记是否“重要”,然后相应地显示或隐藏txtImportant TextView。然后我们对txtTodotxtIdea做同样的事情。

在仍然在onCreateDialog方法中时,在前一个代码块之后添加此代码:

if (!mNote.isImportant()){
   txtImportant.setVisibility(View.GONE);
}
if (!mNote.isTodo()){
   txtTodo.setVisibility(View.GONE);
}
if (!mNote.isIdea()){
   txtIdea.setVisibility(View.GONE);
}

现在,当用户单击onClick方法时,我们只需要dismiss(关闭)对话框窗口,该方法简单地调用dismiss方法,关闭对话框窗口。

在上一个代码块之后,将此代码添加到onCreateDialog方法中:

Button btnOK = (Button) dialogView.findViewById(R.id.btnOK);
builder.setView(dialogView).setMessage("Your Note");
btnOK.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         dismiss();
   }
});
return builder.create();

注意

使用这行代码导入Button类:

import android.widget.Button;

我们现在有两个准备就绪的对话框窗口。我们只需要向MainActivity类添加一些代码来完成工作。首先,让我们进行一些项目清理。

删除不需要的自动生成片段

现在我们将整理项目的文件和结构。请记住,我们用于生成此项目的基本活动模板具有许多功能。有些是我们需要的,而有些则不需要。我们希望浮动操作按钮位于布局的右下角,并且希望主菜单与content_main.xml文件一起,我们将删除对片段的引用以及其所有导航选项。

打开content_main.xml布局文件。在content_main.xml文件中,找到nav_host_fragment元素。选择它,然后按键盘上的Delete键。现在,我们有一个更干净的 UI,可以用于未来的开发。

显示我们的新对话框

打开MainActivity.java文件。在MainActivity类声明之后添加一个新的临时成员变量。这不会出现在最终的应用程序中;这样我们就可以尽快测试我们的对话框窗口:

// Temporary code
Note mTempNote = new Note();

现在,将此方法添加到MainActivity类中,以便我们可以从DialogNewNote类接收一个新的笔记:

public void createNewNote(Note n){
   // Temporary code
   mTempNote = n;
}

要将笔记发送到DialogShowNote方法,我们需要在content_main.xml布局文件中添加一个 ID 为button的按钮。打开content_main.xml布局文件。

为了清楚起见,我们将把这个按钮的text属性更改为Show Note,如下所示:

  • 将一个按钮拖放到content_main.xml布局中,并将id属性配置为buttontext属性配置为Show Note

  • 单击推断约束按钮,使按钮停留在您放置的位置。此时此刻,此按钮的确切位置并不重要。

注意

只是为了澄清,这是一个用于测试目的的临时按钮,不会出现在最终的应用程序中。在开发结束时,我们将能够从滚动列表中点击笔记的标题。

onCreate方法中,我们将设置一个匿名类来处理我们临时按钮的点击。onClick方法中的代码将执行以下操作:

  • 创*一个名为dialog的新DialogShowNote实例。

  • dialog上调用sendNoteSelected方法,将mTempNote作为参数传入,这是我们的Note对象。

  • 最后,它将调用show,为我们的新对话框注入生命并展示给用户。

将我们刚刚描述的代码添加到onCreate中:

// Temporary code
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         // Create a new DialogShowNote called dialog
         DialogShowNote dialog = new DialogShowNote();

         // Send the note via the sendNoteSelected method
         dialog.sendNoteSelected(mTempNote);

         // Create the dialog
         dialog.show(getSupportFragmentManager(), "123");
   }
});

注意

使用这行代码添加Button类:

import android.widget.Button;

现在我们可以在点击按钮时召唤我们的DialogShowNote对话框窗口。运行应用程序,并点击具有dialog_show_note.xml布局的DialogShowNote对话框:

图 14.09 – DialogShowNote 对话框

图 14.09 – DialogShowNote 对话框

承认,考虑到我们在本章中所做的大量编码,这并不是什么了不起的东西。然而,当我们让DialogNewNote类工作时,我们将能够看到MainActivity类如何在这两个对话框之间交互和共享数据。

接下来,让DialogNewNote对话框可用。

编写浮动操作按钮

这将很容易。浮动操作按钮已经在布局中为我们提供。提醒一下,浮动操作按钮是圆形图标,上面有一个信封图像,就像前面截图的右下角所示。

它在activity_main.xml文件中。这是定位和定义其外观的 XML 代码。请注意,在浮动操作按钮的代码之前,有一行代码(已突出显示)包括content_main.xml文件。这目前包含我们的Show Note按钮,并最终将包含我们复杂的滚动列表:

…
…
<include layout="@layout/content_main" />
<com.google.android.material
     .floatingactionbutton.FloatingActionButton

     android:id="@+id/fab"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="bottom|end"
     android:layout_margin="@dimen/fab_margin"
     app:srcCompat="@android:drawable/ic_dialog_email" />
…
…

Android Studio 甚至提供了一个匿名类来处理浮动操作按钮上的任何点击。我们只需要向这个已提供的类的onClick方法添加一些代码,就可以使用DialogNewNote类。

浮动操作按钮通常用于应用程序中的核心操作。例如,在电子邮件应用程序中,它可能用于启动新的电子邮件。或者,在笔记应用程序中,它可能用于添加新的笔记。所以,现在让我们这样做。

MainActivity.java文件中,找到 Android Studio 在MainActivity类的onCreate方法中提供的自动生成代码。以下是完整的代码:

fab.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
            Snackbar.make(view, "Replace with your own 
action", 
                          Snackbar.LENGTH_LONG)
.setAction("Action", 
                          null).show();

   }
});

在上述代码片段中,请注意突出显示的行并将其删除。现在,在删除的代码位置添加以下代码:

DialogNewNote dialog = new DialogNewNote();
dialog.show(getSupportFragmentManager(), "");

新代码创*了一个DialogNewNote类型的新对话框窗口,然后显示给用户。

现在我们可以运行应用程序。点击浮动操作按钮,并添加一个类似于以下截图的笔记:

图 14.10 – 添加新笔记

图 14.10 – 添加新笔记

接下来,我们可以点击显示笔记按钮,以在对话框中查看它,如下所示:

图 14.11 – 显示笔记对话框窗口

图 14.11 – 显示笔记对话框窗口

请注意,如果您添加第二个笔记,它将覆盖第一个,因为我们只有一个Note对象。我们需要学*更多的 Java 知识来解决这个问题。

总结

在本章中,我们讨论并实现了使用DialogFragment类的对话框窗口的常见 UI 设计。

然后,我们更进一步,通过实现更复杂的对话框来启动 Note to Self 应用程序,这些对话框可以从用户那里获取信息。我们看到DialogFragment类使我们能够在对话框中设计任何 UI。

在下一章中,我们将处理一个明显的问题,即用户只能有一个笔记。我们将通过探索 Java 数组及其近亲ArrayList,以及另一个与数据相关的 Java 类Map来解决这个问题。

第十五章:数组、映射和随机数

在本章中,我们将学* Java 数组,它允许我们以有组织和高效的方式操纵大量数据。我们还将使用与数组有密切关系的 Java 类ArrayList,并研究它们之间的区别。

一旦我们熟悉处理大量数据,我们将看看 Android API 提供了什么帮助,让我们轻松地将我们新发现的数据处理技能与用户界面连接起来,而不费吹灰之力。

本章的主题包括以下内容:

  • Random

  • 使用数组处理数据

  • 数组小应用程序

  • 包括一个小型应用程序的动态数组

  • 包括一个小型应用程序的多维数组

  • ArrayList

  • 增强型for循环

  • Java HashMap

首先,让我们了解一下Random类。

技术要求

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

一个随机的转移

有时在我们的应用程序中,我们会需要一个随机数,而 Java 为我们提供了Random类来满足这些需求。这个类有许多可能的用途。例如,也许我们的应用程序想要显示每日随机提示,或者是一个需要在不同情景之间选择的游戏,或者是一个随机提问的测验。

Random类是 Java API 的一部分,完全兼容我们的 Android 应用程序。

让我们看看如何创*随机数,然后在本章中我们将把它应用到实际中。所有的工作都由Random类为我们完成。首先,我们需要创*一个Random类型的对象:

Random randGenerator = new Random();

然后我们使用我们新对象的nextInt方法在一定范围内生成一个随机数。下一行代码使用我们的Random对象生成随机数,并将结果存储在ourRandomNumber变量中:

int ourRandomNumber = randGenerator.nextInt(10);

我们输入的范围开始于零。因此,前一行将生成一个介于 0 和 9 之间的随机数。如果我们想要一个介于 1 和 10 之间的随机数,我们只需这样做:

ourRandomNumber ++;

我们还可以使用Random对象使用nextLongnextFloatnextDouble方法获取其他类型的随机数。

我们将在本章后面用一个快速地理测验应用程序实际使用Random类。

使用数组处理大量数据

也许您会想知道当我们有很多变量需要跟踪时会发生什么。我们的自我备忘录应用程序有 100 条备忘录,或者游戏中有前 100 名得分的高分表怎么办?我们可以像这样声明和初始化 100 个单独的变量:

Note note1;
Note note2;
Note note3;
// 96 more lines like the above
Note note100;

或者我们可以这样做:

int topScore1;
int topScore2;
int topScore3;
// 96 more lines like the above
int topScore100;

一开始,这可能看起来笨拙,但是当有人获得新的最高分或者我们想让用户对他们的笔记显示顺序进行排序时怎么办?使用高分情景,我们必须将每个变量中的分数下移一个位置。噩梦开始了:

topScore100 = topScore99;
topScore99 = topScore98;
topScore98 = topScore97;
// 96 more lines like the above
topScore1 = score;

一定有更好的方法。当我们有一整个数组的变量时,我们需要的是一个 Java 数组。数组是一个引用变量,最多可以容纳预先确定的固定数量的元素。每个元素都是一个具有一致类型的变量。

以下代码声明了一个可以容纳int类型变量的数组,可能是一个高分表或一系列考试成绩:

int [] intArray;

我们还可以声明其他类型的数组,包括Note等类,如下所示:

String [] classNames;
boolean [] bankOfSwitches;
float [] closingBalancesInMarch;
Note [] notes;

在使用之前,这些数组中的每一个都需要分配固定的最大存储空间。就像其他对象一样,我们必须在使用数组之前对其进行初始化:

intArray = new int [100];

上面的代码分配了最多 100 个int大小的存储空间。想象一下我们的内存仓库中有 100 个连续的存储空间。这些空间可能被标记为intArray[0]intArray[1]intArray[2]等等,每个空间都保存一个单独的int值。也许稍微令人惊讶的是,存储空间从零开始,而不是 1。因此,在一个 100 宽的数组中,存储空间将从 0 到 99。

我们可以像这样初始化一些存储空间中的值:

intArray[0] = 5;
intArray[1] = 6;
intArray[2] = 7;

但请注意,我们只能将预先声明的类型放入数组中,数组保存的类型永远不会改变:

intArray[3]= "John Carmack"; // Won't compile String not int

因此,当我们有一个int类型的数组时,每个这些int变量被称为什么?这些变量的名称是什么,我们如何访问其中存储的值?数组表示法语法替换了变量名称。我们可以对数组中的变量进行与常规变量相同的操作:

intArray [3] = 123;

上面的代码将值123分配给intArray的第四个位置。这里是另一个例子:

intArray[10] = intArray[9] - intArray[4];

上面的代码从intArray的第五个位置减去第十个位置的值,并将答案存储在第十一个位置。我们还可以将数组中的值分配给相同类型的常规变量,如下所示:

int myNamedInt = intArray [3];

但请注意,myNamedInt是一个单独且独立的原始变量,对它的任何更改都不会影响存储在intArray引用中的值。它在仓库中有自己的空间,并且与数组没有关联。更具体地说,数组在堆上,而int变量在栈上。

数组是对象

我们说过数组是引用变量。将数组变量视为给定类型的一组变量的地址。也许,使用仓库的类比,someArray是一个过道编号。因此,someArray[0]someArray[1]等等是过道编号,后面是过道中的位置编号。

数组也是对象;也就是说,它们有我们可以使用的方法和属性。例如:

int lengthOfSomeArray = someArray.length;

在上面的代码中,我们将someArray的长度分配给名为lengthOfSomeArrayint变量。

我们甚至可以声明一个数组的数组。这是一个数组,其中每个元素中都隐藏着另一个数组,如下所示:

String[][] countriesAndCities;

在前面的数组中,我们可以在每个国家中保存一个城市列表。不过,现在先不要疯狂地使用数组。只需记住,数组最多可以容纳预定数量的任意类型的变量,并且可以使用以下语法访问这些值:

someArray[someLocation];

让我们在一个真实的应用程序中使用一些数组,试着理解如何在真实代码中使用它们以及我们可能用它们做什么。

简单数组示例迷你应用程序

让我们制作一个简单的工作数组示例。您可以在可下载的代码包中找到此示例的完整代码。它在第十五章/Simple Array Example/MainActivity.java

使用空活动模板创*一个项目,并将其命名为Simple Array Example

首先,我们声明我们的数组,分配五个空间,并为每个元素初始化值。然后我们将每个值输出到 logcat 控制台。将此代码添加到onCreate方法中,就在调用setContentView之后:

// Declaring an array
int[] ourArray;
// Allocate memory for a maximum size of 5 elements
ourArray = new int[5];
// Initialize ourArray with values
// The values are arbitrary, but they must be int
// The indexes are not arbitrary. 0 through 4 or crash!
ourArray[0] = 25;
ourArray[1] = 50;
ourArray[2] = 125;
ourArray[3] = 68;
ourArray[4] = 47;
//Output all the stored values
Log.i("info", "Here is ourArray:");
Log.i("info", "[0] = "+ourArray[0]);
Log.i("info", "[1] = "+ourArray[1]);
Log.i("info", "[2] = "+ourArray[2]);
Log.i("info", "[3] = "+ourArray[3]);
Log.i("info", "[4] = "+ourArray[4]);

接下来,我们将数组的每个元素相加,就像我们可以对常规的int类型变量一样。请注意,当我们将数组元素相加时,我们是在多行上这样做的。这没问题,因为我们省略了分号,直到最后一个操作,所以 Java 编译器将这些行视为一个语句。将我们刚刚讨论的代码添加到MainActivity.java

/*
   We can do any calculation with an array element
   provided it is appropriate to the contained type
   Like this:
*/
int answer = ourArray[0] +
ourArray[1] +
ourArray[2] +
ourArray[3] +
ourArray[4];
Log.i("info", "Answer = "+ answer);

运行示例并在 logcat 窗口中查看输出。

请记住,模拟器显示不会发生任何事情,因为所有输出都将发送到我们在 Android Studio 中的 logcat 控制台窗口。这是输出:

info﹕ Here is ourArray:
info﹕ [0] = 25
info﹕ [1] = 50
info﹕ [2] = 125
info﹕ [3] = 68
info﹕ [4] = 47
info﹕ Answer = 315 

我们声明了一个名为ourArray的数组来保存int变量,然后为该类型的最多五个变量分配了空间。

接下来,我们为数组中的五个空间分配了一个值。请记住,第一个空间是ourArray[0],最后一个空间是ourArray[4]

接下来,我们简单地将每个数组位置的值打印到控制台上,从输出中我们可以看到它们保存了我们在上一步中初始化的值。然后我们将ourArray中的每个元素相加,并将它们的值初始化为answer变量。然后我们将answer打印到控制台上,我们可以看到确实所有的值都被相加了,就像它们是普通的int类型一样,它们确实是,只是以不同的方式存储和访问。

使用数组进行动态处理

正如我们在所有这些数组内容开始时讨论的,如果我们需要单独声明和初始化数组的每个元素,那么数组与常规变量相比并没有太大的好处。让我们看一个动态声明和初始化数组的例子。

动态数组示例

让我们做一个简单的动态数组示例。您可以在下载包中找到此示例的工作项目。它在第十五章/Dynamic Array Example/MainActivity.java中。

使用空活动模板创*一个项目,并将其命名为“动态数组示例”。

onCreate方法中的setContentView方法调用之后,输入以下代码。在我们讨论和分析代码之前,看看您能否猜出输出会是什么:

// Declaring and allocating in one step
int[] ourArray = new int[1000];
// Let's initialize ourArray using a for loop
// Because more than a few variables is allot of typing!
for(int i = 0; i < 1000; i++){
   // Put the value of our value into ourArray
   // At the position decided by i.
   ourArray[i] = i*5;
   //Output what is going on
   Log.i("info", "i = " + i);
   Log.i("info", "ourArray[i] = " + ourArray[i]);
}

运行示例应用程序,记住屏幕上不会发生任何事情,因为所有输出都将发送到我们在 Android Studio 中的 logcat 控制台窗口。以下是输出:

info﹕ i = 0
info﹕ ourArray[i] = 0
info﹕ i = 1
info﹕ ourArray[i] = 5
info﹕ i = 2
info﹕ ourArray[i] = 10
... 994 iterations of the loop removed for brevity.
info﹕ ourArray[i] = 4985
info﹕ i = 998
info﹕ ourArray[i] = 4990
info﹕ i = 999
info﹕ ourArray[i] = 4995

首先,我们声明并分配了一个名为ourArray的数组,用于保存最多 1,000 个int值。请注意,这次我们在一行代码中完成了两个步骤:

int[] ourArray = new int[1000];

然后我们使用了一个设置为循环 1,000 次的for循环:

(int i = 0; i < 1000; i++){

我们用值为i乘以 5 来初始化数组中的空间,从 0 到 999,就像这样:

ourArray[i] = i*5;

然后,为了演示i的值以及数组中每个位置保存的值,我们输出了i的值,然后是数组中相应位置保存的值,就像这样:

Log.i("info", "i = " + i);
Log.i("info", "ourArray[i] = " + ourArray[i]);

所有这些都发生了 1,000 次,产生了我们所看到的输出。当然,我们还没有在真实世界的应用程序中使用这种技术,但我们很快将使用它来使我们的“备忘录”应用程序保存几乎无限数量的备忘录。

使用数组进入 n 维

我们非常简要地提到了数组甚至可以在每个位置上保存其他数组。但是,如果一个数组保存了很多保存了其他某种类型的数组的数组,我们如何访问包含数组中的值呢?无论如何,我们为什么需要这个呢?看看下一个示例,多维数组在哪里可以派上用场。

多维数组迷你应用程序

让我们做一个简单的多维数组示例。您可以在下载包中找到此示例的工作项目。它在第十五章/Multidimensional Array Example/MainActivity.java中。

使用空活动模板创*一个项目,并将其命名为“多维数组示例”。

onCreate中的setContentView调用之后,添加以下代码,包括声明和初始化一个二维数组(已突出显示):

// Random object for generating question numbers
Random randInt = new Random();
// a variable to hold the random value generated
int questionNumber;
// declare and allocate in separate stages for clarity
// but we don't have to
String[][] countriesAndCities;
// Now we have a 2 dimensional array
countriesAndCities = new String[5][2];
// 5 arrays with 2 elements each
// Perfect for 5 "What's the capital city" questions
// Now we load the questions and answers into our arrays
// You could do this with less questions to save typing
// But don't do more or you will get an exception
countriesAndCities [0][0] = "United Kingdom";
countriesAndCities [0][1] = "London";
countriesAndCities [1][0] = "USA";
countriesAndCities [1][1] = "Washington";
countriesAndCities [2][0] = "India";
countriesAndCities [2][1] = "New Delhi";
countriesAndCities [3][0] = "Brazil";
countriesAndCities [3][1] = "Brasilia";
countriesAndCities [4][0] = "Kenya";
countriesAndCities [4][1] = "Nairobi";

现在我们使用for循环和我们的Random对象输出数组的内容。请注意,尽管问题是随机的,但我们始终可以选择正确的答案。在上一个代码之后添加以下代码:

/*
     Now we know that the country is stored at element 0
     The matching capital at element 1
     Here are two variables that reflect this
*/
int country = 0;
int capital = 1;
// A quick for loop to ask 3 questions
for(int i = 0; i < 3; i++){
   // get a random question number between 0 and 4
   questionNumber = randInt.nextInt(5);
   // and ask the question and in this case just
   // give the answer for the sake of brevity
   Log.i("info", "The capital of "
   +countriesAndCities[questionNumber][country]);

   Log.i("info", "is "
   +countriesAndCities[questionNumber][capital]);
} // end of for loop

运行示例,记住屏幕上不会发生任何事情,因为所有输出都将发送到我们在 Android Studio 中的 logcat 控制台窗口。以下是输出:

info﹕ The capital of USA
info﹕ is Washington
info﹕ The capital of India
info﹕ is New Delhi
info﹕ The capital of United Kingdom
info﹕ is London

刚才发生了什么?让我们一块一块地过一遍,这样我们就知道到底发生了什么。

我们创*一个名为randIntRandom类型的新对象,准备在程序后面生成随机数:

Random randInt = new Random();

有一个简单的int变量来保存一个问题编号:

int questionNumber;

在这里我们声明了一个名为countriesAndCities的数组数组。外部数组保存数组:

String[][] countriesAndCities;

现在我们在数组中分配空间。第一个外部数组现在可以保存五个数组,每个内部数组可以保存两个字符串:

countriesAndCities = new String[5][2];

现在我们初始化我们的数组来保存国家和它们对应的首都。注意到每一对初始化的外部数组编号保持不变,表明每个国家/首都对在一个内部数组中,一个String数组。当然,每个内部数组都保存在外部数组的一个元素中(保存数组的数组):

countriesAndCities [0][0] = "United Kingdom";
countriesAndCities [0][1] = "London";
countriesAndCities [1][0] = "USA";
countriesAndCities [1][1] = "Washington";
countriesAndCities [2][0] = "India";
countriesAndCities [2][1] = "New Delhi";
countriesAndCities [3][0] = "Brazil";
countriesAndCities [3][1] = "Brasilia";
countriesAndCities [4][0] = "Kenya";
countriesAndCities [4][1] = "Nairobi";

为了使即将到来的for循环更清晰,我们声明和初始化int变量来表示我们数组中的国家和首都。如果你回顾一下数组初始化,所有的国家都保存在内部数组的位置0,所有对应的首都都在位置1

int country = 0;
int capital = 1;

现在我们设置一个for循环运行三次。请注意,这不仅仅是访问我们数组的前三个元素;它只是确定我们循环的次数。我们可以让它循环一次或一千次;示例仍然有效:

for(int i = 0; i < 3; i++){

接下来,我们确定要提问的问题 - 或者更具体地说,我们的外部数组的哪个元素。记住,randInt.nextInt(5)返回一个 0 到 4 之间的数字 - 这正是我们需要的,因为我们有一个包含五个元素的外部数组,从 0 到 4:

questionNumber = randInt.nextInt(5);

现在我们可以通过输出内部数组中保存的字符串来提问,而这些内部数组又是由前一行随机生成的外部数组保存的:

   Log.i("info", "The capital of "
   +countriesAndCities[questionNumber][country]);
   Log.i("info", "is "
   +countriesAndCities[questionNumber][capital]);
}//end of for loop

值得一提的是,在本书的其余部分我们将不再使用多维数组。所以,如果对这些数组内部的数组还有一点模糊,那没关系。你知道它们存在,知道它们能做什么,如果有必要的话可以重新学*。

数组越界异常

当我们尝试访问一个不存在的数组元素时,就会发生数组越界异常。有时编译器会为我们捕捉到它,以防止错误进入工作中的应用程序。例如,看看这段代码:

int[] ourArray = new int[1000];
int someValue = 1; // Arbitrary value
ourArray[1000] = someValue;
// Won't compile as compiler knows this won't work.
// Only locations 0 through 999 are valid

但如果我们做这样的事情呢:

int[] ourArray = new int[1000];
int someValue = 1;// Arbitrary value
int x = 999;
if(userDoesSomething){
   x++; // x now equals 1000
}
ourArray[x] = someValue;
// Array out of bounds exception if userDoesSomething 
// evaluates to true! This is because we end up referencing
// position 1000 when the array only has positions 0 
// through 999
// Compiler can't spot it. App will crash!

我们唯一能避免这个问题的方法是知道这个规则:数组从零开始,到它们的长度-1。我们还可以使用清晰、可读的代码,在这种代码中更容易评估我们所做的事情,并更容易发现问题。

ArrayList

ArrayList 就像是一个增强版的普通 Java 数组。它克服了数组的一些缺点,比如需要预先确定大小。它添加了一些有用的方法来使数据易于管理,并且使用了一个更清晰的增强版for循环,比普通的for循环更容易使用。

让我们看一些使用ArrayList实例的代码:

// Declare a new ArrayList called myList to hold int variables
ArrayList<int> myList;

// Initialize the myList ready for use
myList = new ArrayList<int>();

在前面的代码中,我们声明并初始化了一个名为myList的新ArrayList。我们也可以像这段代码所示的那样一步完成:

ArrayList<int> myList = new ArrayList<int>();

到目前为止还没有特别有趣的东西,所以让我们看看我们实际上可以用ArrayList做些什么。这次我们使用一个String ArrayList实例:

// declare and initialize a new ArrayList
ArrayList<String> myList = new ArrayList<String>();
// Add a new String to myList in the next available location
myList.add("Donald Knuth");
// And another
myList.add("Rasmus Lerdorf");
// And another
myList.add("Richard Stallman");
// We can also choose 'where' to add an entry
myList.add(1, "James Gosling");
// Is there anything in our ArrayList?
if(myList.isEmpty()){
   // Nothing to see here
}else{
   // Do something with the data
}
// How many items in our ArrayList?
int numItems = myList.size();
// Now where did I put James Gosling?
int position = myList.indexOf("James Gosling");

在前面的代码中,我们看到我们可以在ArrayList对象上使用ArrayList类的一些非常有用的方法;这些方法列在下面:

  • 我们可以添加一个项目(myList.add)。

  • 在特定位置添加(myList.add(x, value))。

  • 检查ArrayList是否为空(myList.isEmpty)。

  • 看看它有多少元素(myList.size())。

  • 获取给定项目的当前位置(myList.indexOf)。

注意

ArrayList类中甚至还有更多的方法,你可以在这里阅读:http://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html。到目前为止,我们已经看到的足以完成本书。

有了所有这些功能,我们现在只需要一种方法来动态处理ArrayList实例。

增强型 for 循环

这是增强型for循环的条件:

for (String s : myList)

前面的例子会逐个遍历myList中的所有项目。在每一步中,s会保存当前的String值。

因此,这段代码将在控制台上打印出我们上一节ArrayList代码示例中的所有杰出程序员:

for (String s : myList){
   Log.i("Programmer: ","" + s);
}

我们也可以使用增强型for循环来处理常规数组:

int [] anArray = new int [];
// We can initialize arrays quickly like this
anArray {0, 1, 2, 3, 4, 5}
for (int s : anArray){
   Log.i("Contents = ","" + s);
}

还有一个即将到来的新闻快讯!

数组和 ArrayList 实例是多态的

我们已经知道我们可以将对象放入数组和ArrayList中。但是多态意味着它们可以处理多个不同类型的对象,只要它们有一个共同的父类型,都可以放在同一个数组或ArrayList中。

第十章**,面向对象编程中,我们学到多态意味着不同的形式。但在数组和ArrayList的上下文中,这对我们意味着什么呢?

简化到最简单的形式:任何子类都可以作为使用超类的代码的一部分。

例如,如果我们有一个Animal实例的数组,我们可以将任何一个是Animal子类的对象放入Animal数组中 - 也许是CatDog实例。

这意味着我们可以编写更简单、更易理解、更易更改的代码:

// This code assumes we have an Animal class
// And we have a Cat and Dog class that extends Animal
Animal myAnimal =  new Animal();
Dog myDog = new Dog();
Cat myCat = new Cat();
Animal [] myAnimals = new Animal[10];
myAnimals[0] = myAnimal; // As expected
myAnimals[1] = myDog; // This is OK too
myAnimals[2] = myCat; // And this is fine as well

此外,我们可以为超类编写代码,并依赖于这样一个事实,即使它被子类化多少次,在一定的参数范围内,代码仍然可以工作。让我们继续我们之前的例子:

// 6 months later we need elephants
// with its own unique aspects
// If it extends Animal we can still do this
Elephant myElephant = new Elephant();
myAnimals[3] = myElephant; // And this is fine as well

但是当我们从多态数组中移除一个对象时,我们必须记得将其转换为我们想要的类型:

Cat newCat = (Cat) myAnimals[2];

我们刚刚讨论的对ArrayList也适用。有了这些新的工具包,包括数组、ArrayList,以及它们的多态性,我们可以继续学*一些更多的 Android 类,这些类很快就会用到我们的 Note to Self 应用程序中。

更多的 Java 集合 - 了解 Java HashMap

Java 的HashMap很棒。它是 Java 集合框架的一部分,也是我们在下一章中将在 Note to Self 项目中使用的ArrayList类的一种近亲。它们基本上封装了一些有用的数据存储技术,否则对我们来说可能会相当技术性。

我觉得值得先看一下HashMap。假设我们想要存储角色扮演游戏中许多角色的数据,每个不同的角色都由Character类型的对象表示。

我们可以使用一些我们已经了解的 Java 工具,比如数组或ArrayList。Java 的HashMap也类似于这些东西,但是使用HashMap,我们可以为每个Character对象提供一个唯一的键/标识符,并使用该键/标识符访问任何这样的对象。

哈希术语来自于将我们选择的键/标识符转换为HashMap类内部使用的东西的过程。这个过程被称为哈希。

我们选择的键/标识符可以访问任何我们的Character实例。在Character类的情况下,一个好的键/标识符候选者可能是角色的名字。

每个键/标识符都有一个相应的对象;在这种情况下,它是Character类型的。这被称为键值对。

我们只需给HashMap一个键,它就会给我们相应的对象。不需要担心我们存储角色的索引是哪个,无论是 Geralt、Ciri 还是 Triss;只需将名字传递给HashMap,它就会为我们完成工作。

让我们看一些例子。你不需要输入任何代码 - 只需熟悉它的工作原理。

我们可以声明一个新的HashMap来保存键和Character实例,就像这样的代码:

Map<String, Character> characterMap;

前面的代码假设我们已经编写了一个名为Character的类。

我们可以像这样初始化HashMap

characterMap = new HashMap();

我们可以像这样添加一个新的键和其关联的对象:

characterMap.put("Geralt", new Character());

我们也可以使用这个:

characterMap.put("Ciri", new Character());

我们也可以使用这个:

characterMap.put("Triss", new Character());

注意

所有示例代码都假设我们可以在其他地方赋予Character实例它们独特的属性,以反映它们的内部差异。

我们可以像这样从HashMap中检索条目:

Character ciri = characterMap.get("Ciri");

或者我们可以直接使用Character类的方法,就像这样:

characterMap.get("Geralt").drawSilverSword();
// Or maybe call some other hypothetical method
characterMap.get("Triss").openFastTravelPortal("Kaer Morhen");

先前的代码调用了假设的Character类上的drawSilverSwordopenFastTravelPortal方法。

注意

HashMap类也有很多有用的方法,就像ArrayList一样。在这里查看HashMap的官方 Java 页面:docs.oracle.com/javase/tutorial/collections/interfaces/map.html

让我们谈谈“Note to Self”应用程序。

“Note to Self”应用程序

尽管我们学到了很多,但我们还没有准备好为“Note to Self”应用程序应用解决方案。我们可以更新我们的代码,将大量的笔记存储在ArrayList实例中,但在这之前,我们还需要一种方法来在 UI 中显示ArrayList的内容。例如,将整个ArrayList的内容放入TextView小部件中看起来并不好。

解决方案是适配器和一个名为RecyclerView的特殊 UI 布局。我们将在下一章中介绍它们。

常见问题

  1. 一个只能进行真实计算的计算机如何可能生成真正的随机数?

实际上,计算机无法创*真正随机的数字,但Random类使用一个种子来产生一个在严格的统计检验下会被认为是真正随机的数字。要了解更多关于种子和生成随机数的信息,请查看这篇文章:en.wikipedia.org/wiki/Random_number_generation

总结

在本章中,我们看了如何使用简单的 Java 数组来存储大量数据,只要它们是相同类型的。我们还使用了ArrayList类,它类似于一个带有大量额外功能的数组。此外,我们发现数组和ArrayList实例都是多态的,这意味着一个数组(或ArrayList)可以容纳多个不同的对象,只要它们都是从同一个父类派生的。

此外,我们还了解了HashMap类,它也是一种数据存储解决方案,但允许以不同的方式访问。

在下一章中,我们将学*AdapterRecyclerView类,将我们的理论付诸实践,并增强“Note to Self”应用程序。

第十六章:适配器和回收器

在这个简短的章节中,我们将取得很大进展。我们将首先学*适配器和列表的理论 - 如何在 Java 代码中扩展RecyclerAdapter类并添加一个作为列表的RecyclerView实例到我们的 UI - 然后通过 Android API 的明显魔术将它们绑定在一起,以便RecyclerView显示RecyclerAdapter的内容并允许用户滚动内容。你可能已经猜到,我们将使用这种技术来显示我们的 Note to Self 应用程序中的笔记列表。

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

  • 研究适配器的理论并将其绑定到我们的 UI

  • 使用RecyclerView实现布局

  • 为在RecyclerView中使用的列表项布局

  • 使用RecyclerAdapter实现适配器

  • 将适配器绑定到RecyclerView

  • 将笔记存储在ArrayList中,并在RecyclerView中显示它们

  • 讨论如何进一步改进 Note to Self 应用程序

很快我们将拥有一个自我管理的布局,用来保存和显示所有的笔记,所以让我们开始吧。

技术要求

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

RecyclerView 和 RecyclerAdapter

第五章使用 CardView 和 ScrollView 创*美丽的布局,我们使用了ScrollView,并用一些CardView小部件填充它,以便我们可以看到它滚动。我们可以利用我们刚学到的关于数组和ArrayList的知识,创*一个TextView小部件数组,用它们填充一个ScrollView,并在每个TextView中放置一个笔记的标题。这听起来像是在 Note to Self 应用程序中显示每个笔记的完美解决方案。

我们可以在 Java 代码中动态创*TextView小部件,将它们的text属性设置为笔记的标题,然后将TextView小部件添加到ScrollView中包含的LinearLayout中。然而,这并不完美。

显示大量小部件的问题

这可能看起来不错,但如果有数十、数百甚至数千个笔记怎么办?我们不能在内存中拥有数千个TextView小部件,因为 Android 设备可能会因为尝试处理如此大量的小部件和它们的数据而耗尽内存,或者至少变得非常缓慢。

现在,想象一下我们(我们确实想要)希望ScrollView中的每个笔记显示它是重要的、待办事项还是想法。以及笔记文本的简短片段呢?

我们需要设计一些聪明的代码,从多个ArrayList实例中加载和销毁Note对象和每个笔记的多个TextView小部件。这是可以做到的,但要高效地完成远非易事。

显示大量小部件的问题的解决方案

幸运的是,这是移动开发人员如此常见的问题,以至于 Android API 内置了解决方案。

我们可以在我们的 UI 布局中添加一个名为RecyclerView的单个小部件(类似于环保的ScrollView,但也带有助推器)。RecyclerView类被设计为解决我们讨论的问题的解决方案。此外,我们需要使用一种特殊类型的类与RecyclerView进行交互,该类了解RecyclerView的工作原理。

我们将使用适配器与之交互。我们将使用RecyclerAdapter类,扩展它,自定义它,然后使用它来控制我们的ArrayList实例(其中将保存我们的Note实例)的数据并在RecyclerView中显示它。

让我们更多地了解一下RecyclerViewRecyclerAdapter类的工作原理。

如何使用 RecyclerView 和 RecyclerAdapter

我们已经知道如何存储几乎无限的笔记;我们可以在ArrayList实例中这样做,尽管我们还没有实现它。我们也知道有一个名为RecyclerView的 UI 布局,专门设计用于从ArrayList实例中显示潜在的长列表数据。我们只需要看看如何将所有这些付诸实践。

要向我们的布局添加RecyclerView小部件,我们可以简单地从调色板上拖放它到我们的 UI 上,以通常的方式。现在不要这样做。让我们先讨论一下。

如果您在content_main.xml中的按钮下面添加了一个RecyclerView小部件,那么 UI 设计师中的RecyclerView小部件将如下所示:

图 16.1 - RecyclerView 小部件

图 16.1 - RecyclerView 小部件

然而,这种外观更多地代表了可能性,而不是应用程序的实际外观。如果我们在添加RecyclerView小部件后立即运行应用程序,我们只会得到一个RecyclerView小部件所在的空白区域。

要实际使用RecyclerView,我们需要做的第一件事是决定列表中的每个项目的外观。它可以是一个单独的TextView小部件,也可以是一个完整的布局。我们将使用LinearLayout。为了清晰和具体,我们将使用一个LinearLayout,它包含我们的RecyclerView中每个项目的三个TextView小部件。这将允许我们显示笔记状态(重要、想法或待办事项)、笔记标题以及来自实际笔记内容的短片段文本。

需要在其自己的 XML 文件中定义列表项,然后RecyclerView可以容纳此列表项布局的多个实例。

当然,这些都没有解释我们如何克服管理显示在哪个列表项中的数据的复杂性,以及如何从ArrayList中检索数据。

这些数据处理由我们自己定制的RecyclerAdapter类来处理。RecyclerAdapter类实现了Adapter接口。我们不需要知道Adapter内部是如何工作的;我们只需要重写必要的方法,然后RecyclerAdapter将负责与我们的RecyclerView小部件进行通信。

RecyclerAdapter的实现连接到RecyclerView肯定比将 20 个TextView实例拖放到ScrollView上要复杂得多 - 但一旦完成,我们就可以忘记它,它将继续工作并自行管理,而不管我们向ArrayList中添加了多少笔记。它还具有处理诸如整洁格式和检测列表中点击了哪个项目等功能。

我们需要重写RecyclerAdapter类的一些方法,并添加一些我们自己的代码。

我们将如何使用 RecyclerAdapter 和笔记的 ArrayList 设置 RecyclerView

让我们先看一下所需步骤的概要,以便知道可以期望什么。为了使整个过程正常运行,我们将执行以下操作:

  1. 删除临时按钮和相关代码,然后向我们的布局添加一个具有特定id属性的RecyclerView小部件。

  2. 创*一个 XML 布局来表示列表中的每个项目。我们已经提到列表中的每个项目将是一个包含三个TextView小部件的LinearLayout

  3. 创*一个扩展RecyclerAdapter的新类,并向多个重写的方法添加代码,以控制其外观和行为,包括使用我们的列表项布局和充满Note实例的ArrayList

  4. MainActivity类添加代码,以使用RecyclerAdapter类和RecyclerView小部件,并将其绑定到我们的ArrayList实例。

  5. MainActivity添加一个ArrayList以保存我们所有的笔记,并更新createNewNote方法,以将在DialogNewNote类中创*的任何新笔记添加到这个ArrayList中。

让我们详细地走一遍每一步。

向 Note to Self 项目添加 RecyclerView、RecyclerAdapter 和 ArrayList

打开“Note to Self”项目。作为提醒,如果您想要查看完成本章后的代码和工作中的应用程序,可以在第十六章/Note to self文件夹中找到。

注意

由于本章中所需的操作在不同的文件、类和方法之间跳转,我鼓励您通过在首选文本编辑器中保持打开以供参考的下载包中的文件来跟随。

删除临时的“显示笔记”按钮并添加 RecyclerView

接下来的几个步骤将清除我们在第十四章**Android 对话框窗口中添加的临时代码,并设置我们的RecyclerView小部件,以便在本章后面绑定到RecyclerAdapter

  1. content_main.xml文件中,删除之前为测试目的添加的 ID 为button的临时Button

  2. MainActivity.java文件的onCreate方法中,删除Button实例声明和初始化以及处理其点击的匿名类,因为此代码现在会创*错误。我们将在本章后面删除一些临时代码。删除以下代码:

// Temporary code
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         // Create a new DialogShowNote called dialog
         DialogShowNote dialog = new DialogShowNote();
         // Send the note via the sendNoteSelected 
         method
         dialog.sendNoteSelected(mTempNote);
         // Create the dialog
         dialog.show(getSupportFragmentManager(), 
         "123");
   }
});
  1. 现在切换回设计视图中的content_main.xml文件,并从调色板的容器类别中拖放一个RecyclerView小部件到布局中。

  2. 将其id属性设置为recyclerView

现在我们已经从项目中删除了临时的 UI 方面,并且我们有了一个完整的RecyclerView小部件,具有一个准备从我们的 Java 代码中引用的唯一id值。

为 RecyclerView 创*列表项

接下来,我们需要一个布局来表示RecyclerView小部件中的每个项目。如前所述,我们将使用一个包含三个TextView小部件的LinearLayout

使用以下步骤创*一个用于在我们的RecyclerView中使用的列表项:

  1. 右键单击LinearLayout中的listitem

  2. 确保您在orientation属性上选择了vertical

  3. 查看下一个屏幕截图,以了解我们在本节剩余步骤中要实现的内容。我已经对其进行了注释,以显示完成应用程序中每个部分的内容:图 16.2 - 用于在我们的 RecyclerView 中使用的列表项

图 16.2 - 用于在我们的 RecyclerView 中使用的列表项

  1. 将三个TextView小部件拖放到布局中,依次排列。第一个(顶部)将保存笔记状态/类型(想法、重要或待办事项)。第二个(中间)将保存笔记标题,第三个(底部)将保存笔记本身的文本片段。

  2. 根据以下表格配置LinearLayoutTextView小部件的各种属性:

现在我们有了一个用于主布局的RecylerView小部件和一个用于RecyclerView列表中每个项目的布局。我们可以继续编写我们的RecyclerAdapter类实现。

编写 RecyclerAdapter 类的代码

现在我们将创*并编写一个全新的类。让我们称之为我们的新类NoteAdapter。在与MainActivity类(以及所有其他类)相同的文件夹中创*一个名为NoteAdapter的新类。

通过添加这些import语句并将其扩展为RecyclerView.Adapter类来编辑NoteAdapter类的代码,然后添加这两个重要的成员变量。编辑NoteAdapter类,使其与我们刚刚讨论过的以下代码相同:

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
public class NoteAdapter extends 
   RecyclerView.Adapter<NoteAdapter.ListItemHolder> {

    private List<Note> mNoteList;
    private MainActivity mMainActivity;
}

注意到类声明被红色下划线标出,显示我们的代码中有错误。错误是因为我们需要重写RecylerView.Adapter类(我们正在扩展的类)的一些抽象方法。

注意

我们在第十一章中讨论了抽象类及其方法,更多面向对象的编程

这样做的最快方法是点击类声明,按住Alt键,然后点击Enter键。选择实现方法,如下面的截图所示:

图 16.3 - 选择实现方法

图 16.3 - 选择实现方法

然后,点击确定以让 Android Studio 自动生成所需的方法。

这个过程添加了以下三个方法:

  • onCreateViewHolder方法,在需要列表项布局时调用。

  • onBindViewHolder方法,在将RecyclerAdapter绑定到布局中的RecyclerView时调用。

  • getItemCount方法,将用于返回ArrayListNote实例的数量。现在它只返回0

很快我们将为这些方法中的每一个添加代码,以在指定的时间做必要的工作。

但是,请注意,我们的代码中仍然存在多个错误,包括自动生成的方法以及类声明。此时代码编辑器的截图可能会有用:

图 16.4 - 我们代码中的多个错误

图 16.4 - 我们代码中的多个错误

错误是因为NoteAdapter.ListItemHolder类不存在。当我们扩展NoteAdapter时,我们添加了ListItemHolder。这是我们选择的类类型,将用作每个列表项的持有者。目前它不存在,因此出现错误。当我们要求 Android Studio 实现缺失的方法时,也会自动生成具有相同原因的两个方法。

让我们通过开始编写所需的ListItemHolder类来解决问题。如果ListItemHolder实例与NoteAdapter共享数据/变量对我们有用,因此我们将ListItemHolder创*为内部类。

点击类声明中的错误,然后选择创*类'ListItemHolder',如下截图所示:

图 16.5 - 选择创*类'ListItemHolder'

图 16.5 - 选择创*类'ListItemHolder'

以下代码已添加到NoteAdapter类中:

public class ListItemHolder {
}

但是类声明仍然有一个错误,如下截图所示:

图 16.6 - 类声明错误

图 16.6 - 类声明错误

错误消息显示ListItemHolder,但ListItemHolder必须扩展RecyclerView.ViewHolder才能用作参数化类型。

修改ListItemHolder类的声明以匹配此代码:

public class ListItemHolder extends 
   RecyclerView.ViewHolder 
   implements View.OnClickListener {
}

现在NoteAdapter类声明中的错误已经消失,但因为我们实现了View.OnClickListener,我们需要实现onClick方法。此外,ViewHolder没有提供默认构造函数,所以我们需要添加。将以下onClick方法(暂时为空)和此构造方法(暂时为空)添加到ListItemHolder类中:

public ListItemHolder(View view) {
   super(view);
}
@Override
public void onClick(View view) {
}

注意

确保您将代码添加到内部的ListItemHolder类而不是NoteAdapter类。

经过多次调整和自动生成,我们最终拥有了一个无错误的NoteAdapter类,其中包括重写的方法和一个内部类,我们可以编写代码来使我们的RecyclerAdapter类工作。此外,我们可以编写代码来响应每个ListItemHolder实例的点击(在onClick方法中)。

编写 NoteAdapter 构造函数

接下来,我们将编写NoteAdapter构造方法,该方法将初始化NoteAdapter类的成员。将此构造方法添加到NoteAdapter类中:

public NoteAdapter(MainActivity mainActivity, 
                            List<Note> noteList) {

   mMainActivity = mainActivity;
   mNoteList = noteList;
}

首先,注意构造函数的参数。它接收一个MainActivity实例以及一个List。这意味着当我们使用这个类时,我们需要发送一个对这个应用程序的主要活动(MainActivity)的引用,以及一个List/ArrayList。我们很快就会看到我们很快会在MainActivity类中编写的Note实例的ArrayList引用。然后,NoteAdapter将永久持有对所有用户笔记的引用。

编写 onCreateViewHolder 方法

接下来,我们将调整自动生成的onCreateViewHolder方法。将两行代码添加到onCreateViewHolder方法中,并研究自动生成的参数:

@NonNull
@Override
public NoteAdapter.ListItemHolder onCreateViewHolder(
         @NonNull ViewGroup parent, int viewType) {

   View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.listitem, parent, 
                  false);

   return new ListItemHolder(itemView);
}

此代码通过使用LayoutInflater和我们新设计的listitem布局来初始化itemView。然后返回一个新的ListItemHolder实例,其中包含一个已经膨胀并且可以立即使用的布局。

编写 onBindViewHolder 方法

接下来,我们将调整onBindViewHolder方法。添加突出显示的代码使方法与此代码相同,并确保也研究方法的参数:

@Override
public void onBindViewHolder(
@NonNull NoteAdapter.ListItemHolder holder, int position) {
   Note note = mNoteList.get(position);
   holder.mTitle.setText(note.getTitle());
   // Show the first 15 characters of the actual note
   // Unless a short note then show half
if(note.getDescription().length() > 15) {
holder.mDescription.setText(note.getDescription()
.substring(0, 15));
}
else{
          holder.mDescription.setText(note.getDescription()
          .substring(0, note.getDescription().length() /2 ));
}
   // What is the status of the note?
   if(note.isIdea()){
          holder.mStatus.setText(R.string.idea_text);
   }
   else if(note.isImportant()){
          holder.mStatus.setText(R.string.important_text);
   }
   else if(note.isTodo()){
          holder.mStatus.setText(R.string.todo_text);
   }
}

首先,代码检查笔记是否超过 15 个字符,如果是,则将其截断,以便在列表中看起来合理。

然后,它检查笔记的类型(想法、待办事项或重要)并从字符串资源中分配适当的标签。

这段新代码在holder.mTitleholder.mDescriptionholder.mStatus变量中留下了一些错误,因为我们需要将它们添加到我们的ListItemHolder内部类中。我们很快就会做到这一点。

编写 getItemCount

修改此自动生成方法中的return语句,使其与下一行显示的突出显示的代码相同:

@Override
public int getItemCount() {
   return mNoteList.size();
}

此代码在类内部使用,并提供ArrayList中当前项目的数量。

编写 ListItemHolder 内部类

现在我们可以转向内部类ListItemHolder。通过添加以下突出显示的代码来调整ListItemHolder内部类:

public class ListItemHolder 
   extends RecyclerView.ViewHolder 
   implements View.OnClickListener {

   TextView mTitle;
   TextView mDescription;
   TextView mStatus;
   public ListItemHolder(View view) {
          super(view);
          mTitle = 
                view.findViewById(R.id.textViewTitle);

mDescription = 
                view.findViewById(R.id.textViewDescription);

mStatus = 
                view.findViewById(R.id.textViewStatus);
          view.setClickable(true);
          view.setOnClickListener(this);
   }
   @Override
   public void onClick(View view) {
          mMainActivity.showNote(getAdapterPosition());
   }
}

ListItemHolder构造函数只是获取布局中每个TextView小部件的引用。最后两行代码将整个视图设置为可点击,以便操作系统在点击持有者时调用我们讨论的下一个方法onClick

onClick方法中,mMainActivity.showNote方法的调用存在错误,因为该方法尚不存在,但我们将在下一节中修复这个问题。该调用将在适当的DialogFragment实例中显示被点击的笔记。

编写 MainActivity 以使用 RecyclerView 和 RecyclerAdapter 类

现在,切换到编辑窗口中的MainActivity类。将这三个新成员添加到MainActivity类中,并删除下面注释掉的临时代码:

// Temporary code
//Note mTempNote = new Note();
private List<Note> noteList = new ArrayList<>();
private RecyclerView recyclerView;
private NoteAdapter mAdapter;

如果尚未添加,请添加以下import指令:

import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;

这三个成员是我们所有Note实例的ArrayList,我们的RecyclerView实例以及我们的类NoteAdapter的一个实例。

向“onCreate”方法添加代码

在处理浮动操作按钮的代码之后,将以下突出显示的代码添加到onCreate方法中(为了上下文再次显示):

fab.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
         DialogNewNote dialog = new DialogNewNote();
         dialog.show(getSupportFragmentManager(), "");
   }
});
recyclerView = 
   findViewById(R.id.recyclerView);
mAdapter = new NoteAdapter(this, noteList);
RecyclerView.LayoutManager mLayoutManager = 
   new LinearLayoutManager(getApplicationContext());

recyclerView.setLayoutManager(mLayoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator());
// Add a neat dividing line between items in the list
recyclerView.addItemDecoration(
   new DividerItemDecoration(this, LinearLayoutManager.VERTICAL));
// set the adapter
recyclerView.setAdapter(mAdapter);

前面的代码将需要以下三个import指令:

import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.DividerItemDecoration;

在前面的代码中,我们使用布局中的RecyclerView实例初始化recyclerView引用。通过调用我们编写的构造函数来初始化我们的NoteAdaptermAdapter)并注意到对MainActivitythis)和ArrayList实例的引用被传递进来,正如我们之前编写的类所要求的那样。

接下来,我们创*一个新对象LayoutManager。在下一行代码中,我们在recyclerView上调用setLayoutManager并传入这个新的LayoutManager实例。现在我们可以配置recyclerView的一些属性。

setItemAnimatoraddItemDecoration方法使每个列表项在列表中的每个项目之间都有一个分隔线,视觉上更加美观。稍后,当我们构*设置屏幕时,我们将让用户选择添加和删除这个分隔线。

我们要做的最后一件事是调用setAdapter方法,将我们的适配器与我们的视图结合起来。

现在我们将对addNote方法进行一些更改。

修改 addNote 方法

addNote方法中,删除我们在第十四章**Android 对话框窗口中添加的临时代码(显示为注释),并添加下面显示的新突出显示的代码:

public void createNewNote(Note n){
   // Temporary code
   //mTempNote = n;
   noteList.add(n);
   mAdapter.notifyDataSetChanged();
}

新突出显示的代码将一个笔记添加到ArrayList中,而不仅仅是初始化一个孤立的Note对象,现在已经被注释掉。然后,我们需要调用notifyDataSetChanged方法,让我们的适配器知道已经添加了一个新的笔记。

编写 showNote 方法

添加这个新方法,它是从NoteAdapter类中使用传递给NoteAdapter构造函数的对该类的引用来调用的。更具体地说,当用户点击RecyclerView中的项目时,它是从ListerItemHolder内部类中调用的。将showNote方法添加到MainActivity类中:

public void showNote(int noteToShow){
   DialogShowNote dialog = new DialogShowNote();
   dialog.sendNoteSelected(noteList.get(noteToShow));
   dialog.show(getSupportFragmentManager(), "");
}

注意

NoteAdapter.java文件中的所有错误现在都已经消失。

刚刚添加的代码将启动一个新的DialogShowNote实例,并传入由noteToShow指向的特定所需的笔记。

运行应用程序

您现在可以运行应用程序,并输入一个新的笔记,如下面的截图所示:

图 16.7 – 添加新笔记

图 16.7 – 添加新笔记

当您输入了几种类型的笔记后,列表(RecyclerView)将看起来像这样:

图 16.8 – 笔记列表

图 16.8 – 笔记列表

读者挑战

我们本可以花更多时间来格式化我们的两个对话框窗口的布局。为什么不参考第五章**使用 CardView 和 ScrollView 创*美丽的布局,以及 Material Design 网站,做得比我们迄今为止做得更好呢?此外,您可以通过使用CardView而不是LinearLayout来增强RecyclerView类/笔记列表。

不要花太长时间添加新的笔记,因为有一个小问题。关闭并重新启动应用程序。哦,所有的笔记都消失了!

常见问题解答

  1. 我仍然不明白RecyclerAdapter是如何工作的 – 为什么?

这是因为我们实际上没有讨论它。我们没有讨论背后的细节的原因是我们不需要知道它们。如果我们重写了所需的方法,就像我们刚刚看到的那样,一切都会正常工作。这就是RecyclerAdapter和我们使用的大多数其他类的意图:隐藏实现,公开方法来暴露必要的功能。

  1. 我需要了解RecyclerAdapter类和其他类的内部情况吗?

确实,关于RecyclerAdapter(以及我们在本书中使用的几乎每个类)还有更多细节,我们没有空间来讨论。阅读您使用的类的官方文档是一个好*惯。您可以在这里阅读有关 Android API 的所有类的更多信息:developer.android.com/reference

总结

现在我们已经添加了保存多个笔记的能力,并实现了显示它们的能力。

我们通过学*和使用RecyclerAdapter类来实现了这一点,该类实现了Adapter接口,允许我们将RecyclerViewArrayList绑定在一起,无需我们(程序员)担心这些类的复杂代码,而我们甚至看不到。

在下一章中,我们将开始让用户的笔记在他们退出应用程序或关闭设备时保持。此外,我们将创*一个设置屏幕,并看看我们如何使设置也持久。我们将使用不同的技术来实现这些目标。

第十七章:数据持久性和共享

在本章中,我们将看一下将数据保存到 Android 设备的永久存储的几种不同方法。此外,我们将首次向我们的应用程序添加第二个Activity。当在我们的应用程序中实现一个单独的“屏幕”,比如设置屏幕时,将其放在一个新的Activity中通常是有意义的。我们可以费力地隐藏原始 UI,然后显示新的 UI,但这很快会导致令人困惑和容易出错的代码。因此,我们将看看如何添加一个Activity类并在它们之间导航用户。

总之,在本章中,我们将做以下事情:

  • 学*如何使用 Android 意图在Activity类之间切换并传递数据

  • 为“Note to Self”项目在一个新的Activity类中创*一个简单(非常简单)的设置屏幕

  • 使用SharedPreferences类持久化设置屏幕数据

  • 学*JavaScript 对象表示JSON)进行序列化

  • 探索 Java 的try-catch-finally语法

  • 在我们的“Note to Self”应用程序中实现保存数据

技术要求

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

Android 意图

Intent类的命名非常恰当。它是一个演示我们应用程序中Activity类意图的类。它使意图清晰,并且也方便了它。

到目前为止,我们所有的应用程序都只有一个Activity,但许多 Android 应用程序包含多个Activity

在它可能最常见的用法中,Intent类允许我们在Activity实例之间切换。但是,Activity实例是由具有成员变量的类制作的。那么,当我们在它们之间切换时,变量的值 - 数据 - 会发生什么?意图通过允许我们在Activity实例之间传递数据来解决了这个问题。

意图不仅仅是关联我们应用程序的Activity实例。它们还使我们能够与其他应用程序进行交互。例如,我们可以在我们的应用程序中提供一个链接,让用户发送电子邮件,打电话,与社交媒体互动,或在浏览器中打开网页,并让电子邮件应用程序、电话应用程序、社交媒体应用程序或网络浏览器完成所有工作。

这本书中没有足够的页面来深入研究与其他应用程序的交互,我们主要将专注于在活动之间切换和传递数据。

切换活动

假设我们的应用程序有两个基于Activity的类,因为我们很快就会有。我们可以假设通常情况下,我们有一个名为MainActivityActivity类,这是应用程序的起点,还有一个名为SettingsActivity的第二个Activity。这是我们如何从MainActivity切换到SettingsActivity的方法:

// Declare and initialize a new Intent object called myIntent
Intent myIntent = new Intent(this, SettingsActivity.class);
// Switch to the SettingsActivity
startActivity(myIntent);

仔细看看我们如何初始化Intent对象。Intent有一个构造函数,它接受两个参数。第一个是对当前Activity的引用,this。第二个参数是我们想要打开的Activity类的名称,SettingsActivity.classSettingsActivity末尾的.class部分使其成为AndroidManifest.xml文件中声明的Activity类的完整名称,我们将在不久的将来尝试意图时查看它。

唯一的问题是SettingsActivity不共享MainActivity的任何数据。从某种意义上说,这是一件好事,因为如果你需要从MainActivity获取所有数据,那么切换活动可能不是进行应用程序设计的最佳方式。然而,封装得如此彻底以至于这两个Activity实例完全不知道对方是不合理的。

在活动之间传递数据

如果我们为用户有一个登录屏幕,并且我们想要将用户的凭据传递给我们应用程序的每个Activity,我们可以使用意图来实现。

我们可以像这样向Intent类添加数据:

// Create a String called username 
// and set its value to bob
String username = "Bob";
// Create a new Intent as we have already seen
Intent myIntent = new Intent(this, SettingsActivity.class);
// Add the username String to the Intent
// using the putExtra method of the Intent class
myIntent.putExtra("USER_NAME", username);
// Start the new Activity as we did before
startActivity(myIntent);

SettingsActivity中,我们可以这样检索字符串:

// Here we need an Intent also
// But the default constructor will do
// as we are not switching Activity
Intent myIntent = new Intent();
// Initialize username with the passed in String 
String username = intent.getExtra().getStringKey("USER_NAME");

在前两个代码块中,我们以与之前相同的方式切换了Activity。但在调用startActivity方法之前,我们使用putExtra方法将字符串加载到意图中。

我们使用identifier实例添加数据,该数据可以在检索Activity中用于标识和检索数据。

标识符名称由您决定,但应使用有用/易记的值。

然后,在接收Activity中,我们只需使用默认构造函数创*一个意图:

Intent myIntent = new Intent();

然后,我们可以使用getExtras方法和键值对中的适当标识符检索数据。

一旦我们想要开始发送多个值,就值得考虑不同的策略。

Intent类可以在发送比这更复杂的数据时帮助我们,但Intent类有其限制。例如,我们将无法发送Note对象。

向“自我备忘录”添加设置页面

现在我们已经掌握了关于 Android Intent类的所有知识,我们可以向我们的“自我备忘录”应用程序添加另一个屏幕(Activity)。我们将添加一个设置屏幕。

我们将首先为我们的设置屏幕创*一个新的Activity,并查看这对AndroidManifest.xml文件的影响;然后我们将为我们的设置屏幕创*一个非常简单的布局,并添加 Java 代码以从MainActivity切换到新的布局。但是,在学*如何将设置保存到磁盘之前,我们将推迟使用 Java 连接我们的设置屏幕。我们将在本章后面进行此操作,然后返回设置屏幕以使其数据持久化。

首先,让我们创*一个新的Activity类。我们将其称为SettingsActivity

创* SettingsActivity

这将是一个屏幕,用户可以在RecyclerView小部件中的每个笔记之间打开或关闭装饰性分隔符。这不会是一个全面的设置屏幕,但这将是一个有用的练*,并且我们将学*如何在活动之间切换以及将数据保存到磁盘。按照以下步骤开始:

  1. 在项目资源管理器中,右键单击包含所有.java文件并与您的包名称相同的文件夹。从弹出的上下文菜单中,选择新* | Activity | 空白 Activity

  2. SettingsActivity中。

  3. 将所有其他选项保留为默认值,然后单击完成

Android Studio 已为我们创*了一个基于Activity的类及其关联的.java文件。让我们快速查看一下幕后为我们完成的一些工作,因为了解正在发生的事情是有用的。

从项目资源管理器中的manifests文件夹中打开AndroidManifest.xml文件。注意文件中间大约有一半的以下代码行:

<activity android:name=".SettingsActivity"></activity>

这就是Activity类是如何Activity类未注册,然后尝试运行它将使应用程序崩溃。我们可以通过在新的.java文件中创*一个扩展Activity(或AppCompatActivity)的类来创*Activity类。但是,我们随后必须自己添加前面的代码。此外,通过使用新的Activity向导,我们自动生成了一个布局 XML 文件(activity_settings.xml)。

设计设置屏幕布局

我们将快速为我们的设置屏幕构*一个 UI,以下步骤和图示应该使这变得简单:

  1. 打开activity_settings.xml文件,切换到设计选项卡,然后我们将快速布置我们的设置屏幕。在执行其余步骤时,请使用下一个图示作为指南:图 17.1 – 设计设置屏幕

图 17.1 – 设计设置屏幕

  1. Switch小部件拖放到布局的中上部。我通过拖动边缘来拉伸我的小部件,使其变大。

  2. 添加一个id属性switch1(如果默认情况下还没有),这样我们就可以使用SettingsActivity.java中的 Java 代码与其交互。

  3. 使用约束处理程序来固定开关的位置,或者单击推断约束按钮以自动修复它。

我们现在为设置屏幕有了一个简单的新布局,并且id属性已经就位,准备在本章后面的 Java 代码中连接它。

使用户能够切换到设置屏幕

我们已经知道如何切换到SettingsActivity。此外,由于我们不会向其传递任何数据,也不会从中传递任何数据,因此我们可以只用两行 Java 代码就可以使其工作。

您可能已经注意到我们应用程序的操作栏中有一个菜单图标。这是我们创*项目时使用的基本活动模板的默认部分。如下图所示:

图 17.2 – 菜单图标

图 17.2 – 菜单图标

如果您点击它,您会发现默认情况下已经有一个菜单选项设置。当您点击菜单图标时,您将看到以下内容:

图 17.3 – 设置选项

图 17.3 – 设置选项

我们只需要将切换到SettingsActivity类的代码放在MainActivity类的onOptionsItemSelected方法中。Android Studio 甚至默认为我们提供了一个if块,以便我们将来可能想要添加设置屏幕。多么体贴。

切换到编辑器窗口中的MainActivity.java,并在onOptionsItemSelected方法中找到以下代码块:

//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
   return true;
}

将此代码添加到先前显示的if块中,就在return true语句之前:

Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);

您需要使用您喜欢的技术导入Intent类,以添加以下代码行:

import android.content.Intent;

现在,您可以运行应用程序并通过点击设置菜单选项来访问新的设置屏幕。此屏幕截图显示了模拟器上运行的设置屏幕:

图 17.4 – 设置屏幕

图 17.4 – 设置屏幕

要从SettingsActivity返回到MainActivity,您可以点击设备上的返回按钮。

使用 SharedPreferences 持久化数据

在 Android 中,有几种方法可以使数据持久化。持久化意味着如果用户退出应用程序,然后再次打开应用程序,他们的数据仍然可用。使用哪种方法取决于应用程序和数据类型。

在本书中,我们将介绍三种使数据持久化的方法。对于保存用户设置,我们只需要一个简单的方法。毕竟,我们只需要知道他们是否希望在RecyclerView小部件的每个笔记之间有装饰性分隔符。

让我们看看如何使我们的应用程序将变量保存和重新加载到设备的内部存储器中。我们需要使用SharedPreferences类。SharedPreferences是一个提供对可以由应用程序的所有Activity类访问和编辑的数据的访问权限的类。让我们看看如何使用它:

// A SharedPreferences for reading data
SharedPreferences prefs;
// A SharedPreferences.Editor for writing data
SharedPreferences.Editor editor; 

与所有对象一样,我们需要在使用之前初始化它们。我们可以通过使用getSharedPreferences方法并传递一个字符串来初始化prefs对象,该字符串将用于引用使用该对象读取和写入的所有数据。通常,我们可以使用应用程序的名称作为此字符串的值。在下一个代码中,MODE_PRIVATE表示任何类(仅在此应用程序中)都可以访问它:

prefs = getSharedPreferences("My App", MODE_PRIVATE);

然后,我们使用新初始化的prefs对象通过调用edit方法来初始化我们的editor对象:

editor = prefs.edit();

假设我们想要保存我们在名为username的字符串中拥有的用户名称。然后,我们可以像这样将数据写入设备的内部存储器中:

editor.putString("username", username);
editor.commit();

putString方法中使用的第一个参数是一个标签,可以用来引用数据;第二个是保存我们想要保存的数据的实际变量。上述代码的第二行启动了保存过程。因此,我们可以这样将多个变量写入磁盘:

editor.putString("username", username);
editor.putInt("age", age);
editor.putBoolean("newsletter-subscriber", subscribed);
// Save all the above data
editor.commit();

上述代码演示了您可以保存其他变量类型,并且它当然假定usernameagesubscribed变量先前已经被声明并用适当的值初始化。

一旦editor.commit()执行完毕,数据就被存储了。我们甚至可以退出应用程序,甚至关闭设备,数据仍然会持久保存。

使用 SharedPreferences 重新加载数据

让我们看看下次运行应用程序时如何重新加载数据。这段代码将重新加载上一个代码保存的三个值。我们甚至可以声明我们的变量并用存储的值进行初始化:

String username  = 
   prefs.getString("username", "new user");
int age  = prefs.getInt("age", -1);
boolean subscribed = 
   prefs.getBoolean("newsletter-subscriber", false)

在上述代码中,我们使用适合数据类型的方法从磁盘加载数据,并使用了保存数据的相同标签。不太清楚的是每个方法调用的第二个参数。

getStringgetIntgetBoolean方法需要一个默认值作为第二个参数。如果没有存储带有该标签的数据,它将返回默认值。

然后我们可以在我们的代码中检查这些默认值,并尝试获取真实的值。例如,看这里:

if (age == -1){
   // Ask the user for his age
}

我们现在知道足够的知识来保存我们用户的设置在 Note to Self 应用程序中。

使“Note to Self”设置持久化

我们已经学会了如何将数据保存到设备的内存中。当我们实现保存用户设置时,我们还将再次看到我们如何处理Switch输入,以及我们刚刚看到的代码将去哪里使我们的应用程序按我们想要的方式工作。

编写 SettingsActivity 类

大部分操作将在SettingsActivity.java文件中进行。因此,点击适当的选项卡,我们将逐步添加代码。

首先,我们需要一些成员变量,这些变量将为我们提供工作的SharedPreferencesEditor实例。我们还希望有一个成员变量来表示用户的设置选项:他们是否想要装饰性分隔线。

SettingsActivity类的类声明之后添加以下成员变量:

private SharedPreferences mPrefs;
private SharedPreferences.Editor mEditor;
private boolean mShowDividers;

导入SharedPreferences类:

import android.content.SharedPreferences;

现在在onCreate方法中,添加代码来初始化mPrefsmEditor

mPrefs = getSharedPreferences("Note to self", MODE_PRIVATE);
mEditor = mPrefs.edit();

接下来,在onCreate方法中,让我们获取对我们的Switch小部件的引用,并加载代表我们用户先前选择是否显示分隔线的保存数据。

我们以与第十三章**相同的方式获取对开关的引用,匿名类-让 Android 小部件活跃起来。请注意默认值为true-显示分隔线。我们还将根据需要将开关设置为打开或关闭:

mShowDividers  = mPrefs.getBoolean("dividers", true);
Switch switch1 = findViewById(R.id.switch1);
// Set the switch on or off as appropriate
switch1.setChecked(mShowDividers);

您需要导入Switch类:

import android.widget.Switch;

接下来,我们创*一个匿名类来监听和处理对我们的Switch小部件的更改。

isChecked变量为true时,我们使用prefs对象将dividers标签和mShowDividers变量设置为true;当未选中时,我们将它们都设置为false

将以下代码添加到我们刚刚讨论的onCreate方法中:

switch1.setOnCheckedChangeListener(
      new CompoundButton.OnCheckedChangeListener() {
             public void onCheckedChanged(
                         CompoundButton buttonView, 
                               boolean isChecked) {
                   if(isChecked){
                         mEditor.putBoolean(
                                     "dividers", true);

                         mShowDividers = true;
                   }else{
                         mEditor.putBoolean(
                                     "dividers", false);

                         mShowDividers = false;
                   }
             }
      }
);

您需要导入CompoundButton类:

import android.widget.CompoundButton;

您可能已经注意到,在任何代码中,我们都没有调用mEditor.commit方法来保存用户的设置。我们本可以在检测到开关变化后放置它,但将它放在保证被调用但只调用一次的地方更简单。

我们将利用我们对Activity生命周期的了解,并重写onPause方法。当用户离开SettingsActivity屏幕,无论是返回到MainActivity还是退出应用程序,操作系统都会调用onPause方法,并保存设置。在SettingsActivity类的结束大括号之前添加以下代码来重写onPause方法并保存用户的设置:

@Override
protected void onPause() {
   super.onPause();
   // Save the settings here
   mEditor.commit();
}

最后,我们可以向MainActivity类添加一些代码,以在应用程序启动时或用户从设置屏幕切换回主屏幕时加载设置。

编写 MainActivity 类

在我们声明NoteAdapter实例后添加一些成员变量的突出代码:

private List<Note> noteList = new ArrayList<>();
private RecyclerView recyclerView;
private NoteAdapter mAdapter;
private boolean mShowDividers;
private SharedPreferences mPrefs;

导入SharedPreferences类:

import android.content.SharedPreferences;

现在我们有一个boolean成员来决定是否显示分隔符,以及一个SharedPreferences实例来从磁盘读取设置。

现在我们将重写onResume方法,初始化我们的mPrefs变量,并将设置加载到mShowDividers变量中。

MainActivity类中添加重写的onResume方法,如下所示:

@Override
protected void onResume(){
   super.onResume();
   mPrefs = getSharedPreferences(
         "Note to self", MODE_PRIVATE);

   mShowDividers  = mPrefs.getBoolean(
         "dividers", true);
}

用户现在能够选择他们的设置。应用程序将根据需要保存和重新加载它们,但我们需要让MainActivity类响应用户的选择。

onCreate方法中找到这段代码并删除它:

// Add a neat dividing line between items in the list
recyclerView.addItemDecoration(
   new DividerItemDecoration(
   this, LinearLayoutManager.VERTICAL));

上一个代码是在列表中的每个笔记之间设置分隔符。将这段新代码添加到onResume方法中,这是相同的代码行,被if语句包围,只有当mShowDividerstrue时才选择性地使用分隔符。在onResume方法中在上一个代码后添加以下代码:

if(mShowDividers) {
   // Add a neat dividing line between list items
   recyclerView.addItemDecoration(
         new DividerItemDecoration(
         this, LinearLayoutManager.VERTICAL));
}else{
   // check there are some dividers
// or the app will crash
if(recyclerView.getItemDecorationCount() > 0) {
         recyclerView.removeItemDecorationAt(0);
}
}

运行应用程序并注意到分隔符已经消失;转到设置屏幕,打开分隔符,然后返回到主屏幕(使用返回按钮)- 看哪:现在有分隔符了。下一个图显示了带有和不带有分隔符的列表,以便说明我们添加的代码起作用,并且设置在这两个 Activity 类之间持续存在:

图 17.5 - 带有和不带有分隔符的列表

图 17.5 - 带有和不带有分隔符的列表

确保尝试退出应用程序并重新启动以验证设置是否已保存到磁盘。甚至可以关闭并重新打开模拟器,设置将持续存在。

现在我们有一个整洁的设置屏幕,我们可以永久保存用户选择的装饰偏好。当然,关于持久性的一个重要缺失环节是用户的基本数据,他们的笔记,仍然没有持久保存。

更高级的持久性

让我们考虑一下我们需要做什么。我们想要将一堆笔记保存到内部存储中。更具体地说,我们想要存储一系列字符串和相关的布尔值。这些字符串和布尔值代表用户的笔记标题、笔记文本,以及它是待办事项、重要事项还是想法。

考虑到我们已经了解的SharedPreferences类,乍一看这似乎并不特别具有挑战性 - 直到我们更深入地了解我们的需求。如果用户喜欢我们的应用程序并最终拥有 100 条笔记怎么办?我们需要 100 个键值对的标识符。这并非不可能,但开始变得尴尬。

现在考虑一下,我们想要增强应用程序,并让用户能够为它们添加日期。Android 有一个Date类非常适合这个用途。然后,我们可以相对简单地为我们的应用程序添加一些新功能,比如提醒。但是当涉及到保存数据时,事情突然变得复杂起来。

我们如何使用SharedPreferences来存储日期?它并不是为此而设计的。我们可以在保存时将其转换为字符串,然后在加载时再次转换回来,但这远非简单。

随着我们的应用程序功能的增加和用户获取更多的笔记,整个持久性问题变得一团糟。我们需要的是一种保存和加载对象的方法,实际的 Java 对象。如果我们可以简单地保存和加载对象,包括它们的内部数据(字符串,布尔值,日期或其他任何东西),我们的应用程序可以拥有我们可以想到的任何类型的数据,以满足我们的用户。

将数据对象转换为位和字节以存储在磁盘上的过程称为序列化;反向过程称为反序列化。单独的序列化是一个庞大而复杂的主题。幸运的是,正如我们所期望的那样,有一个类来处理大部分复杂性。

什么是 JSON?

JSON代表JavaScript 对象表示,它在 Android 和 Java 语言之外的领域被广泛使用。它可能最常用于在 Web 应用程序和服务器之间发送数据。

幸运的是,Android 上有可用的 JSON 类,几乎完全隐藏了序列化过程的复杂性。通过学*一些更多的 Java 概念,我们可以快速开始使用这些类,并开始将整个 Java 对象写入设备存储,而不必担心构成对象的原始类型是什么。

与我们迄今为止见过的其他类相比,JSON 类执行的操作具有比正常情况下更高的失败概率,这是超出它们的控制范围的。要找出原因以及可以采取什么措施,让我们看看 Java 异常。

Java 异常 - try,catch 和 finally

所有这些关于 JSON 的讨论要求我们学*一个新的 Java 概念:异常。当我们编写一个执行可能失败的操作的类,特别是由于我们控制之外的原因,*议在我们的代码中明确这一点,以便任何使用我们的类的人都能为可能性做好准备。

保存和加载数据是一个这样的场景,其中失败是可能的,超出了我们的控制范围。想想当 SD 卡已被移除或已损坏时尝试加载数据的情况。另一个代码可能失败的情况是当我们编写依赖于网络连接的代码时 - 如果用户在数据传输过程中断网了会怎么样?

Java 异常是解决方案,JSON 类使用它们,所以现在是学*它们的好时机。

当我们编写一个使用有可能失败的代码的类时,我们可以通过使用trycatchfinally来为我们的类的用户做好准备。

我们可以在我们的类中编写方法,在签名的末尾使用throws Java 关键字 - 可能是这样的:

public void somePrecariousMethod() throws someException{
   // Risky code goes here
}

现在,任何使用somePrecariousMethod方法的代码都需要trycatch块 - 也许像这样:

try{
   ...
   somePrecariousMethod();
   ...
}catch(someException e){
   Log.e("Exception:" + e, "Uh ohh")
   // Take action if possible
}

可选地,如果我们想在trycatch块之后执行任何进一步的操作,我们还可以添加一个finally块:

finally{
   // More action here
}

在我们的“自言自语”应用中,我们将采取最少的操作来处理异常,并简单地将错误输出到 logcat,但您可以做一些事情,比如通知用户,重试操作,或者实施一些聪明的备用计划。

备份用户数据在“自言自语”中

因此,通过我们对异常的新认识,让我们修改我们的“自言自语”代码,然后我们可以介绍JSONObjectJSONException类。

首先,让我们对我们的Note类进行一些小修改。添加一些更多的成员,这些成员将作为我们Note类的每个方面的键值对的键:

private static final String JSON_TITLE = "title";
private static final String JSON_DESCRIPTION = "description";
private static final String JSON_IDEA = "idea";
private static final String JSON_TODO = "todo";
private static final String JSON_IMPORTANT = "important";

现在添加一个构造函数和一个接收JSONObject并抛出JSONException的空默认构造函数。构造函数的主体通过调用JSONObjectgetStringgetBoolean方法,传入键作为参数,来初始化定义单个Note对象属性的每个成员。我们还提供了一个空的默认构造函数,现在我们提供了我们的专门的构造函数,这是必需的:

// Constructor
// Only used when new is called with a JSONObject
public Note(JSONObject jo) throws JSONException {

   mTitle =  jo.getString(JSON_TITLE);
   mDescription = jo.getString(JSON_DESCRIPTION);
   mIdea = jo.getBoolean(JSON_IDEA);
   mTodo = jo.getBoolean(JSON_TODO);
   mImportant = jo.getBoolean(JSON_IMPORTANT);
}
// Now we must provide an empty default constructor 
// for when we create a Note as we provide a
// specialized constructor.
public Note (){
}

您需要导入JSONExceptionJSONObject类:

import org.json.JSONException;
import org.json.JSONObject;

接下来我们将看到的代码将给定Note对象的成员变量加载到JSONObject中。这是Note对象的成员被打包为一个单独的JSONObject,以便进行实际的序列化。

我们只需要使用适当的键和匹配的成员变量调用put方法。该方法返回JSONObject(我们将在一分钟内看到它在哪里),它还会抛出一个JSONObject异常。添加我们刚刚讨论过的代码:

public JSONObject convertToJSON() throws JSONException{

   JSONObject jo = new JSONObject();
   jo.put(JSON_TITLE, mTitle);
   jo.put(JSON_DESCRIPTION, mDescription);
   jo.put(JSON_IDEA, mIdea);
   jo.put(JSON_TODO, mTodo);
   jo.put(JSON_IMPORTANT, mImportant);
   return jo;
}

现在让我们创*一个JSONSerializer类,它将执行实际的序列化和反序列化。创*一个新类并将其命名为JSONSerializer

让我们将其分成几个部分,并在编写每个部分的代码时讨论我们正在做什么。

首先,声明和一些成员变量:一个字符串来保存数据将被保存的文件名,以及一个Context对象,在 Android 中写入数据到文件是必要的。在您刚刚创*的类中添加突出显示的代码:

public class JSONSerializer {
    private String mFilename;
    private Context mContext;

   // All the rest of the code for the class goes here

}// End of class

注意

您需要导入Context类:

import android.content.Context;

先前的代码显示,类的结束大括号和随后为该类编写的所有代码应该放在其中。这是非常简单的构造函数,我们在其中初始化了作为参数传入的两个成员变量。添加JSONSerializer的构造函数:

public JSONSerializer(String fn, Context con){
   mFilename = fn;
   mContext = con;
}

现在我们可以开始编写类的真正要点。接下来是save方法。它首先创*一个JSONArray对象,这是一个专门用于处理 JSON 对象的ArrayList类。

接下来,代码使用增强的for循环来遍历notes数组列表中的所有Note对象,并使用Note类中我们之前添加的convertToJSON方法将它们转换为 JSON 对象。然后,我们将这些转换后的JSONObject实例加载到jArray中。

接下来,代码使用Writer实例和OutputStream实例组合将数据写入实际文件。请注意,OutputStream实例需要初始化mContext对象。添加我们刚刚讨论过的代码:

public void save(List<Note> notes)
   throws IOException, JSONException{
   // Make an array in JSON format
   JSONArray jArray = new JSONArray();
   // And load it with the notes
   for (Note n : notes)
         jArray.put(n.convertToJSON());
   // Now write it to the private disk space of our app
   Writer writer = null;
   try {
         OutputStream out = 
         mContext.openFileOutput(mFilename,
                mContext.MODE_PRIVATE);
         writer = new OutputStreamWriter(out);
         writer.write(jArray.toString());
   } finally {
         if (writer != null) {
                writer.close();
         }
   }
}

您需要为这些新类添加以下import语句:

import org.json.JSONArray;
import org.json.JSONException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.List;

现在进行反序列化 - 加载数据。这一次,正如我们所预料的,该方法不接收任何参数,而是返回一个ArrayList实例。使用mContext.openFileInput创*一个InputStream实例,打开包含所有数据的文件。

我们使用while循环将所有数据附加到一个字符串中,并使用我们的新Note构造函数,该构造函数将 JSON 数据提取到常规原始变量中,以将每个JSONObject解包到一个Note对象中,并将其添加到返回给调用代码的ArrayList中:

public ArrayList<Note> load() throws IOException, JSONException{
   ArrayList<Note> noteList = new ArrayList<Note>();
   BufferedReader reader = null;
   try {
         InputStream in = 
         mContext.openFileInput(mFilename);
         reader = new BufferedReader(new 
         InputStreamReader(in));
         StringBuilder jsonString = new StringBuilder();
         String line = null;
         while ((line = reader.readLine()) != null) {
                jsonString.append(line);
         }
         JSONArray jArray = (JSONArray) new
               JSONTokener(jsonString.toString()
               ).nextValue();
         for (int i = 0; i < jArray.length(); i++) {
                noteList.add(new 
                Note(jArray.getJSONObject(i)));
         }
   } catch (FileNotFoundException e) {
         // we will ignore this one, since it happens
         // when we start fresh. You could add a log here.
   } finally {// This will always run
         if (reader != null)
                reader.close();
   }
   return noteList;
}

您需要添加以下导入:

import org.json.JSONTokener;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;

现在我们只需要让我们的新类在MainActivity类中工作。在MainActivity声明之后添加一个新成员,如下所示。此外,删除noteList的初始化,只留下声明,因为我们现在将在onCreate方法中使用一些新代码来初始化它。我已经注释掉了您需要删除的行:

public class MainActivity extends AppCompatActivity {
   private JSONSerializer mSerializer;
   //private List<Note> noteList = new ArrayList<>();
   private List<Note> noteList;

现在,在onCreate方法中,我们通过使用文件名和getApplicationContext()调用JSONSerializer构造函数来初始化mSerializer,它返回应用程序的Context实例并且是必需的。然后我们可以使用JSONSerializer load方法来加载任何保存的数据。在处理浮动操作按钮的代码之后添加这段新的突出显示的代码。这段新代码必须出现在我们处理RecyclerView实例之前:

…
FloatingActionButton fab = 
         (FloatingActionButton) findViewById(R.id.fab);

fab.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View view) {
                DialogNewNote dialog = new DialogNewNote();
                dialog.show(getSupportFragmentManager(), "");
         }
});
   mSerializer = new JSONSerializer("NoteToSelf.json", 
   getApplicationContext());
   try {
         noteList = mSerializer.load();
   } catch (Exception e) {
         noteList = new ArrayList<Note>();
         Log.e("Error loading notes: ", "", e);
   }
   recyclerView =
         findViewById(R.id.recyclerView);
mAdapter = new NoteAdapter(this, noteList);
             …

注意

此时您需要导入Log类:

import android.util.Log;

在先前的代码中,我展示了大量的上下文,因为它的定位对于它的工作是至关重要的。如果您在运行时遇到任何问题,请务必将其与第十七章/java文件夹中的下载包中的代码进行比较。

现在我们可以向MainActivity类添加一个新的方法,这样我们就可以调用它来保存所有用户的数据。这个新方法所做的就是调用JSONSerializer类的save方法,传入所需的Note对象列表:

public void saveNotes(){
   try{
         mSerializer.save(noteList);
   }catch(Exception e){
         Log.e("Error Saving Notes","", e);
   }
}

现在,就像我们保存用户设置时所做的那样,我们将重写onPause方法来保存用户的笔记数据。确保在MainActivity类中添加这段代码:

@Override
protected void onPause(){
   super.onPause();
   saveNotes();
}

就是这样。我们现在可以运行应用程序,并添加任意多的笔记。ArrayList会将它们全部存储在我们运行的应用程序中,我们的RecyclerAdapter将管理在RecyclerView中显示它们,现在 JSON 也会负责将它们加载到磁盘上,并加载它们回来。

经常问的问题

  1. 我并没有完全理解本章的所有内容 - 我不适合成为程序员吗?

本章介绍了许多新的类、概念和方法。如果你的头有点疼,这是可以预料的。如果一些细节不清楚,不要让它阻碍你。继续进行接下来的几章(它们要简单得多),然后回顾这一章并检查已完成的代码文件。

  1. 那么,序列化的详细工作原理是什么?

序列化确实是一个广阔的主题。你可能一辈子都写应用程序而从未真正需要理解它。这可能是计算机科学学位的主题。如果你想了解更多,请看一下这篇文章:en.wikipedia.org/wiki/Serialization

总结

在我们通过 Android API 的旅程中,现在值得回顾一下我们所知道的。我们可以布置自己的 UI 设计,并从各种各样的小部件中选择,让用户与 UI 进行交互。我们可以创*多个屏幕以及弹出对话框,并且可以捕获全面的用户数据。此外,我们现在可以使这些数据持久化。

当然,还有很多关于 Android API 的知识需要学*,甚至超出了这本书所教授的范围,但重点是我们现在已经知道足够的知识来规划和实现一个可工作的应用程序。你现在就可以开始自己的应用程序了。

如果你有立即开始自己的项目的冲动,那么我的*议是继续前进并去做。不要等到你认为自己是一个“专家”或更加准备好。阅读这本书,更重要的是,实现应用程序将使你成为一个更好的 Android 程序员,但没有什么比设计和实现自己的应用程序更能让你更快地学会!完全可以通过阅读这本书并同时进行自己的项目工作。

在下一章中,我们将通过使应用程序支持多语言来为这个应用程序添加最后的修饰。这非常快速和简单。

第十八章:本地化

本章内容简洁明了,我们将学*的内容可以使您的应用程序对数百万潜在用户更加可访问。我们将看到如何添加额外的语言。我们还将看到通过字符串资源以正确的方式添加文本在添加多种语言时对我们的好处。

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

  • 通过添加西班牙语和德语语言使“Note to Self”应用程序多语言化

  • 更全面地学*如何使用字符串资源

让我们开始吧。

技术要求

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

使“Note to Self”应用程序对西班牙语和德语使用者可访问

首先,我们需要向我们的项目添加一些文件夹 - 每种新语言一个文件夹。这些文本被归类为res文件夹。按照以下步骤向项目添加西班牙语和德语支持。

重要提示

虽然此项目的源文件位于第十八章文件夹中,但它们仅供参考。您需要按照下面描述的流程来实现多语言功能。

添加西班牙语支持

按照以下步骤添加西班牙语支持:

  1. 右键单击values-es

  2. 左键单击确定

  3. 现在我们需要添加一个文件,我们可以把所有的西班牙语翻译放在里面。在目录名称字段中,右键单击values-es中的strings.xml

  4. 左键单击确定

在这个阶段,我们有一个新的文件夹,用于西班牙语翻译,里面有一个strings.xml文件,用于字符串资源。让我们也为德语做同样的事情。

添加德语语言支持

按照以下步骤添加德语语言支持:

  1. 右键单击values-de

  2. 左键单击确定

  3. 现在我们需要添加一个文件,我们可以把所有的德语翻译放在里面。在目录名称字段中,右键单击values-de中的strings.xml

  4. 左键单击确定

这就是strings.xml文件夹的样子。您可能想知道strings.xml文件夹是从哪里来的,因为它与我们似乎在之前的步骤中创*的结构不符。Android Studio 正在帮助我们(显然)组织我们的文件和文件夹,以符合 Android 操作系统的要求。但是,您可以看到西班牙语和德语文件,分别由它们的特定于国家的扩展esde指示:

图 18.1 - strings.xml 文件夹

图 18.1 - strings.xml 文件夹

现在我们可以向文件添加翻译。

添加字符串资源

正如我们所知,strings.xml文件包含应用程序将显示的单词 - 诸如“重要”,“待办事项”,“想法”等。通过为我们想要支持的每种语言都有一个strings.xml文件,我们可以让 Android 根据用户的语言设置选择适当的文本。

在您进行以下操作时,请注意,尽管我们将要翻译的单词的翻译放在值中,但name属性保持不变。如果您仔细想想,这是合乎逻辑的,因为我们在布局文件中引用的是name属性。

让我们提供翻译,看看我们取得了什么成就,然后回来讨论我们将如何处理 Java 代码中的文本。实现此代码的最简单方法是从原始的strings.xml文件中复制并粘贴代码,然后编辑每个name属性的值:

  1. 双击打开strings.xml文件。确保选择带有(es)后缀的文件。编辑文件使其看起来像这样:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Nota a sí mismo</string>
<string name="action_settings">Configuración</string>
<string name="action_add">add</string>
<string name="title_hint">Título</string>
<string name="description_hint">Descripción</string>
<string name="idea_text">Idea</string>
<string name="important_text">Importante</string>
<string name="todo_text">Que hacer</string>
<string name="cancel_button">Cancelar</string>
<string name="ok_button">Vale</string>
<string name="settings_title">Configuración</string>
</resources>
  1. 双击打开strings.xml文件。确保选择与strings.xml文件相邻的文件,然后缺少的资源将从默认文件中获取。

我们所做的是提供了两种翻译。Android 知道哪种翻译是为哪种语言,因为它们所放置的文件夹。此外,我们使用了一个name属性来引用翻译。回顾一下以前的代码,您会看到相同的标识符用于两种翻译以及原始的strings.xml文件。

您甚至可以将本地化到不同版本的语言,例如美国或英国英语。完整的代码列表可以在这里找到:stackoverflow.com/questions/7973023/what-is-the-list-of-supported-languages-locales-on-android。您甚至可以本地化资源,如图像和声音。在这里了解更多信息:developer.android.com/guide/topics/resources/localization.html

这些翻译是从谷歌翻译复制并粘贴而来的,因此很可能有些翻译与正确的相去甚远。像这样廉价地进行翻译可能是将具有基本字符串资源集的应用程序放入使用不同语言的用户设备的有效方式。一旦您开始需要任何深度的翻译,也许在叙事驱动的游戏或社交媒体应用的情况下,您肯定会受益于由人类专业人员进行的翻译。

这个练*的目的是展示 Android 的工作原理,而不是如何翻译。

注意

对于可能看到这里提供的翻译的局限性的任何西班牙语或德语使用者,我表示诚挚的歉意。

现在我们已经有了翻译,我们可以看到它们在一定程度上起作用。

在德语或西班牙语中运行 Note to Self

运行应用程序以查看它是否正常工作。现在我们可以更改本地化设置以在西班牙语中查看它。不同的设备在如何执行此操作上略有不同,但 Pixel 3 模拟器的选项如下:

  1. 选择设置 | 系统 | 语言和输入 | 添加语言。接下来,选择Español,然后您将能够在列表中在西班牙语和英语之间切换。

  2. 左键单击并拖动Español (Estados Unidos),使其位于列表顶部。

恭喜,您的模拟器现在默认为西班牙语。完成本章后,您可以将首选语言拖回到列表顶部。

现在您可以以通常的方式运行应用程序。以下是应用程序以西班牙语运行的一些屏幕截图。我用 Photoshop 将一些屏幕截图并排放置,以展示 Note to Self 应用程序的不同屏幕:

图 18.2 - 应用程序以西班牙语运行

图 18.2 - 应用程序以西班牙语运行

在屏幕截图中,您可以清楚地看到我们的应用程序主要是用西班牙语翻译的。显然,用户输入的文本将是他们所说的任何语言;这不是我们应用程序的缺陷。然而,仔细看屏幕截图,注意我指出了一些文本仍然是英文的地方。我们的每个对话框窗口中仍然有一些未翻译的文本。

这是因为文本直接包含在我们的 Java 代码中。正如我们所见,使用多种语言的字符串资源然后在我们的布局中引用它们是很容易的,但是我们如何从我们的 Java 代码中引用字符串资源呢?

使翻译在 Java 代码中起作用

首先要做的是在三个strings.xml文件中创*资源。以下是需要添加到三个不同文件中的两个资源。

strings.xml(没有任何国家后缀),在<resources></resources>标签中添加这两个资源:

<string name="add_new_note">Add a new note</string>
<string name="your_note">Your note</string>

strings.xml中使用<resources></resources>标签:

<string name="add_new_note">Agregar una nueva nota</string>
<string name="your_note">Su nota</string>

strings.xml中使用<resources></resources>标签:

<string name="add_new_note">Eine neue Note hinzufügen</string>
<string name="your_note">Ihre Notiz</string>

接下来,我们需要编辑一些 Java 代码,以引用资源而不是硬编码的字符串。

打开DialogNewNote.java文件并找到这行代码:

builder.setView(dialogView).setMessage("Add a new note");

编辑如下所示,使用我们刚刚添加的字符串资源而不是硬编码的文本:

builder.setView(dialogView).setMessage(getResources().
getString(R.string.add_new_note));

新代码使用了链式getResources.getString方法来替换先前硬编码的"Add a new note"文本。仔细看,你会发现发送给getString方法的参数是R.string.add_new_note字符串标识符。

R.string代码指的是res文件夹中的字符串资源,add_new_note是我们的标识符。Android 将能够根据应用所在设备的语言环境决定使用哪个版本(默认、西班牙语或德语)。

我们还有一个硬编码的字符串需要更改。

打开DialogShowNote.java文件并找到这行代码:

builder.setView(dialogView).setMessage("Your Note");

编辑如下所示,使用我们刚刚添加的字符串资源而不是硬编码的文本:

builder.setView(dialogView).setMessage(getResources().
getString(R.string.your_note));

新代码再次使用了链式getResources.getString方法来替换先前硬编码的"Your note"文本。同样,发送给getString的参数是字符串标识符,这次是R.string.your_note

Android 现在可以根据应用所在设备的语言环境决定使用哪个版本(默认、西班牙语或德语)。下一张截图显示了新*笔记界面现在以适当的语言显示开头文本:

图 18.3 – 新*笔记界面

图 18.3 – 新*笔记界面

您可以添加任意多个字符串资源。作为第三章中的提醒,探索 Android Studio 和项目结构,请注意,使用字符串资源是向所有项目添加任何文本的推荐方式。本书中的教程(除了《自言自语》)将倾向于硬编码它们,以便制作更紧凑的教程。

总结

我们已经看到了如何满足世界上说不同语言的地区。我们现在可以让我们的应用全球化,同时添加更灵活的字符串资源,而不是硬编码所有文本。

在下一章中,我们将看到如何使用动画和插值器为我们的应用添加酷炫的动画效果。

第十九章:动画和插值

在这里,我们将看到如何使用Animation类使我们的 UI 变得不那么静态,更有趣一些。正如我们所期望的那样,Android API 将允许我们用相对简单的代码做一些相当先进的事情,Animation类也不例外。

这一章大致可以分为以下几个部分:

  • 介绍了 Android 中动画的工作原理和实现方式

  • 介绍了一个我们尚未探索的 UI 小部件,SeekBar

  • 创*一个工作动画应用程序

首先,让我们探索一下 Android 中的动画是如何工作的。

技术要求

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

Android 中的动画

在 Android 中创*动画的正常方式是通过 XML。我们可以编写 XML 动画,然后在 Java 中加载和播放它们,放在指定的 UI 小部件上。因此,例如,我们可以编写一个动画,在 3 秒内淡入淡出五次,然后在ImageView或任何其他小部件上播放该动画。我们可以将这些 XML 动画看作脚本,因为它们定义了类型、顺序和时间。

让我们探索一下我们可以分配给动画的一些不同属性,然后如何在我们的 Java 代码中使用它们,最后,我们可以制作一个漂亮的动画应用程序来尝试一切。

在 XML 中设计酷炫的动画

我们已经学会了 XML 不仅可以用来描述 UI 布局,还可以用来描述动画,但让我们来看看具体是如何做到的。我们可以陈述动画的属性,描述小部件的起始和结束外观。然后我们的 Java 代码可以通过引用包含它的 XML 文件的名称并将其转换为可用的 Java 对象来加载 XML,这与 UI 布局非常相似。

这里我们快速浏览一下一些动画属性对,我们可以陈述以创*动画。在我们查看了一些 XML 之后,我们将看到如何在 Java 中使用它。

淡入淡出

Alpha 是透明度的度量。因此,通过陈述起始的fromAlpha和结束的toAlpha值,我们可以淡入淡出物品。值为0.0是不可见的,而1.0是对象的正常外观。在两者之间稳定移动会产生淡入效果:

<alpha
   android:fromAlpha="0.0"
   android:toAlpha="1.0" />

移动

我们可以通过类似的技术在我们的 UI 中移动一个对象。fromXDeltatoXDelta的值可以设置为被动画化对象大小的百分比。

以下代码将使一个对象从左到右移动,距离等于对象本身的宽度:

<translate 
   android:fromXDelta="-100%"
   android:toXDelta="0%"/>

此外,还有用于上下动画的fromYDeltatoYDelta属性。

缩放或拉伸

fromXScaletoXScale将增加或减少对象的比例。例如,以下代码将改变对象,使动画从正常大小到不可见:

<scale
   android:fromXScale="1.0"
   android:fromYScale="0.0"/>

举个例子,我们可以使用android:fromYScale="0.1"将对象缩小到其通常大小的十分之一,或者使用android:fromYScale="10.0"将其放大十倍。

控制持续时间

当然,如果这些动画只是瞬间到达它们的结论,那么它们中的任何一个都不会特别有趣。因此,为了使我们的动画更有趣,我们可以设置它们的持续时间(以毫秒为单位)。毫秒是一秒的千分之一。我们还可以通过设置startOffset(也以毫秒为单位)来使时间更容易,特别是与其他动画相关联。

下面的代码将在我们开始动画的 1/3 秒后开始动画,并且需要 2/3 秒才能完成:

   android:duration="666"
   android:startOffset="333"

旋转动画

如果要使某物旋转,只需使用fromDegreestoDegrees。下面的代码可能会让小部件在一个完整的圆圈中旋转,因为当然,一个圆圈有 360 度:

   <rotate android:fromDegrees="360"
           android:toDegrees="0"
   />

重复动画

在一些动画中,重复可能很重要,也许是一种摇摆或抖动效果,所以我们可以添加一个 repeatCount 属性。此外,我们可以通过设置 repeatMode 来指定动画的重复方式。

以下代码将重复一个动画 10 次,每次都会改变动画的方向。repeatMode 属性是相对于动画的当前状态的。这意味着,如果你,例如,将一个按钮从 0 度旋转到 360 度,动画的第二部分(第一次重复)将从 360 度反向旋转回到 0。动画的第三部分(第二次重复)将再次反向旋转,从 0 度到 360 度:

   android:repeatMode="reverse"
   android:repeatCount="10"

使用 360 度旋转的例子,前面的代码将使一个小部件向右旋转 360 度,然后再向左旋转 360 度,重复五次。这是 10 次重复,每次都会反向旋转。

将动画的属性与 set 结合

要结合这些效果的组,我们需要使用 set。这段代码展示了我们如何将刚刚看到的所有先前代码片段组合成一个实际的 XML 动画,它将编译:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
             ...All our animations go here
</set>

然而,我们还没有看到任何 Java 代码来使这些动画生动起来。让我们现在来解决这个问题。

实例化动画并用 Java 代码控制它们

下面的 Java 代码片段展示了我们如何声明一个 Animation 类型的对象,用一个名为 fade_in.xml 的 XML 文件中的动画对其进行初始化,并在一个 ImageView 小部件上启动动画。很快我们将在一个项目中这样做,并看看在哪里放置 XML 动画:

// Declare an Animation object
Animation animFadeIn;
// Initialize it 
animFadeIn = AnimationUtils.loadAnimation(
getApplicationContext(), R.anim.fade_in);
// Get an ImageView from the UI in the usual way
ImageView findViewById(R.id.imageView);
// Start the animation on the ImageView
imageView.startAnimation(animFadeIn);

我们已经有了相当强大的动画和控制功能,比如时间控制。但是 Android API 还给了我们更多。

更多动画特性

我们可以监听动画的状态,就像我们可以监听按钮的点击一样。我们还可以使用 插值器 来使我们的动画更加生动和愉悦。让我们先看看监听器。

监听器

如果我们实现 AnimationListener 接口,我们确实可以通过覆盖告诉我们发生了什么的三种方法来监听动画的状态。然后我们可以根据这些事件来采取行动。

OnAnimationEnd 宣布动画结束,onAnimationRepeat 每次动画开始重复时调用,也许可以预料到,onAnimationStart 在动画开始动画时调用。如果在动画的 XML 中设置了 startOffset,这可能不是 startAnimation 被调用的时间:

@Override
public void onAnimationEnd(Animation animation) {
   // Take some action here
}
@Override
public void onAnimationRepeat(Animation animation) {

   // Take some action here
}
@Override
public void onAnimationStart(Animation animation) {

   // Take some action here
}

我们将在动画演示应用中看到 AnimationListener 是如何工作的,以及如何将另一个小部件 SeekBar 付诸实践。

动画插值器

如果你能回想起高中时的一些激动人心的关于计算加速度的课程,你可能会记得。如果我们以恒定的速度对某物进行动画,乍一看,事情可能看起来还不错。如果我们将动画与另一个使用渐进加速的动画进行比较,那么后者几乎肯定会更令人愉悦。

有可能,如果我们没有被告知两个动画之间唯一的区别是一个使用了加速度,另一个没有,我们可能无法说出为什么我们更喜欢它。我们的大脑更容易接受符合我们周围世界规范的事物。这就是为什么添加一点真实世界的物理学,比如加速度和减速度,会改善我们的动画。

然而,我们最不想做的事情就是开始做一堆数学计算,只是为了将一个按钮滑动到屏幕上或者让一些文本在圆圈中旋转。

这就是 插值器 的用武之地。它们是我们可以在 XML 中的一行代码中设置的动画修改器。

一些插值器的例子是 accelerate_interpolatorcycle_interpolator

android:interpolator="@android:anim/accelerate_interpolator"android:interpolator="@android:anim/cycle_interpolator"/>

接下来我们将把一些插值器,以及一些 XML 动画和相关的 Java 代码,付诸实践。

注意

您可以在开发者网站上了解更多关于插值器和 Android Animation类的信息:developer.android.com/guide/topics/resources/animation-resource.html

动画演示应用程序 - 介绍 SeekBar

这就够了理论,尤其是对于应该是视觉的东西。让我们构*一个动画演示应用程序,探索我们刚刚讨论过的一切以及更多内容。

这个应用程序涉及大量不同文件中的少量代码。因此,我已经尽量明确哪些代码在哪个文件中,这样你就可以跟踪发生了什么。这也将使我们为这个应用程序编写的 Java 更容易理解。

该应用程序将演示旋转、淡入淡出、*移、动画事件、插值器和使用SeekBar小部件控制持续时间。解释SeekBar小部件的最佳方法是构*它,然后观察它的运行。

布局动画演示

使用空活动模板创*一个名为Animation Demo的新项目,将所有其他设置保持默认。如果您希望通过复制和粘贴布局、Java 代码或动画 XML 来加快速度,可以在第十九章文件夹中找到所有内容。

使用完成布局的下一个参考截图来帮助你完成接下来的步骤:

图 19.1 - 完成的布局

图 19.1 - 完成的布局

以下是为此应用程序布局 UI 的方法:

  1. 在编辑器窗口的设计视图中打开activity_main.xml

  2. 删除默认的TextView

  3. 在布局的顶部中心添加一个ImageView小部件。使用之前的参考截图来指导你。在弹出的资源窗口中选择项目 | ic_launcher,使用@mipmap/ic_launcher在提示时在ImageView小部件中显示 Android 机器人。

  4. ImageView小部件的id属性设置为imageView

ImageView小部件的正下方,添加一个TextView小部件。将id属性设置为textStatus。我通过拖动其边缘使我的TextView小部件变大,并将其textSize属性更改为40sp

  1. 现在我们将添加大量id属性值,稍后在教程中将它们添加到它们。按照下一个截图来布局 12 个按钮。修改每个按钮的text属性,使其与下一个截图中的文本相同。如果截图不够清晰,下一步中具体详细说明了text属性:图 19.2 - 文本属性

图 19.2 - 文本属性

注意

为了加快按钮布局的过程,首先只是大致布局它们,然后从下一步中添加text属性,然后微调按钮位置以获得整洁的布局。

  1. 按照截图中的文本值添加文本。以下是从左到右,从上到下的所有值:淡入淡出淡入淡出放大缩小左右右左上下弹跳闪烁向左旋转向右旋转

  2. id属性到seekBarSpeed添加一个SeekBar小部件,并将max属性设置为5000。这意味着当用户从左向右拖动时,滑块将保存一个在05000之间的值。我们将看到如何读取和使用这些数据。

  3. 我们想要使SeekBar小部件更宽。为了实现这一点,您可以使用与任何小部件相同的技术;只需拖动小部件的边缘。然而,由于滑块很小,很难增加其大小而不小心选择约束手柄。为了解决这个问题,您可以通过按住Ctrl键并向前滚动鼠标中键来放大设计。然后,您可以抓住滑块的边缘,而不触摸约束手柄。我在下一个截图中展示了这一操作:图 19.3 - 抓住滑块的边缘

图 19.3 - 抓住滑块的边缘

  1. 现在在SeekBar小部件的右侧添加一个TextView小部件,并将其id属性设置为textSeekerSpeed

  2. 调整位置,使其看起来像这些步骤开始时的参考图像,然后单击推断约束按钮以锁定位置。当然,如果您想要练*,也可以手动完成这一步骤。

  3. 接下来,将以下id属性添加到按钮中,这些按钮由您已经设置的文本属性标识。如果在输入这些值时被问及是否要更新用法…,请选择

当我们在几节时间内编写MainActivity类时,我们将看到如何使用这个新加入的 UI(SeekBar)。

编写 XML 动画

右键单击目录名称字段中的anim,然后左键单击确定

现在,右键单击新的fade_in,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true" >
<alpha
android:fromAlpha="0.0"
android:interpolator="
@android:anim/accelerate_interpolator"
android:toAlpha="1.0" />
</set>

右键单击fade_out,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true" >
   <alpha
         android:fromAlpha="1.0"
         android:interpolator="
         @android:anim/accelerate_interpolator"
         android:toAlpha="0.0" />
</set>

右键单击fade_in_out,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
<alpha
android:fromAlpha="0.0"
android:interpolator="
@android:anim/accelerate_interpolator"
android:toAlpha="1.0" />
<alpha
android:fromAlpha="1.0"
android:interpolator="
@android:anim/accelerate_interpolator"
android:toAlpha="0.0" />
</set>

右键单击zoom_in,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true" >
    <scale
        android:fromXScale="1"
        android:fromYScale="1"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="6"
        android:toYScale="6" >
    </scale>
</set>

右键单击zoom_out,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale
        android:fromXScale="6"
        android:fromYScale="6"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="1"
        android:toYScale="1" >
    </scale>
</set>

右键单击left_right,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate 

        android:fromXDelta="-500%"
        android:toXDelta="0%"/>
</set>

右键单击right_left,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate 
        android:fillAfter="false"
        android:fromXDelta="500%"
        android:toXDelta="0%"/>
</set>

右键单击top_bot,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate 
        android:fillAfter="false"
        android:fromYDelta="-100%"
        android:toYDelta="0%"/>
</set>

右键单击flash,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha android:fromAlpha="0.0"
        android:toAlpha="1.0"
        android:interpolator="
        @android:anim/accelerate_interpolator"
        android:repeatMode="reverse"
        android:repeatCount="10"/>
</set>

右键单击bounce,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:interpolator="
    @android:anim/bounce_interpolator">
    <scale
        android:fromXScale="1.0"
        android:fromYScale="0.0"
        android:toXScale="1.0"
        android:toYScale="1.0" />
</set>

右键单击rotate_left,然后左键单击pivotX="50%"pivotY="50%"。这使得旋转动画在将要被动画化的小部件上居中。我们可以将其视为设置动画的中心点:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate android:fromDegrees="360"
        android:toDegrees="0"
        android:pivotX="50%"
        android:pivotY="50%"
        android:interpolator="
        @android:anim/cycle_interpolator"/>
</set>

右键单击rotate_right,然后左键单击确定。删除整个内容并添加以下代码以创*动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate android:fromDegrees="0"
        android:toDegrees="360"
        android:pivotX="50%"
        android:pivotY="50%"
        android:interpolator="
        @android:anim/cycle_interpolator"/>
</set>

现在我们可以编写 Java 代码,将动画添加到我们的 UI 中。

将动画演示应用程序与 Java 连接起来

打开MainActivity.java文件。现在,在类声明下面,我们可以声明以下用于动画的成员变量:

Animation animFadeIn;
Animation animFadeOut;
Animation animFadeInOut;
Animation animZoomIn;
Animation animZoomOut;
Animation animLeftRight;
Animation animRightLeft;
Animation animTopBottom;
Animation animBounce;
Animation animFlash;
Animation animRotateLeft;
Animation animRotateRight;

现在在上述代码之后为 UI 小部件添加这些成员变量:

ImageView imageView;
TextView textStatus;
Button btnFadeIn;
Button btnFadeOut;
Button btnFadeInOut;
Button zoomIn;
Button zoomOut;
Button leftRight;
Button rightLeft;
Button topBottom;
Button bounce;
Button flash;
Button rotateLeft;
Button rotateRight;
SeekBar seekBarSpeed;
TextView textSeekerSpeed;

注意

此时,您需要添加以下import语句:

import android.view.animation.Animation;

import android.widget.Button;

import android.widget.ImageView;

import android.widget.SeekBar;

import android.widget.TextView;

接下来,我们添加一个int成员变量,用于跟踪滑块的当前值/位置:

int seekSpeedProgress;

现在,在setContentView方法调用之后,让我们从onCreate方法中调用两个新方法:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   loadAnimations();
   loadUI();
}

在这一点上,两行新代码将有错误,直到我们实现这两个新方法。

现在我们将实现 loadAnimations 方法。虽然这个方法中的代码相当庞大,但也非常简单。我们所做的就是使用 AnimationUtils 类的静态 loadAnimation 方法来初始化我们每个 Animation 引用中的一个 XML 动画。还要注意,对于 animFadeInAnimation 引用,我们还在其上调用了 setAnimationListener。我们将很快编写监听事件的方法。

添加 loadAnimations 方法:

private void loadAnimations(){
   animFadeIn = AnimationUtils.loadAnimation(
   this, R.anim.fade_in);
   animFadeIn.setAnimationListener(this);
   animFadeOut = AnimationUtils.loadAnimation(
   this, R.anim.fade_out);
   animFadeInOut = AnimationUtils.loadAnimation(
   this, R.anim.fade_in_out);
   animZoomIn = AnimationUtils.loadAnimation(
   this, R.anim.zoom_in);
   animZoomOut = AnimationUtils.loadAnimation(
   this, R.anim.zoom_out);
   animLeftRight = AnimationUtils.loadAnimation(
   this, R.anim.left_right);
   animRightLeft = AnimationUtils.loadAnimation(
   this, R.anim.right_left);
   animTopBottom = AnimationUtils.loadAnimation(
   this, R.anim.top_bot);
   animBounce = AnimationUtils.loadAnimation(
   this, R.anim.bounce);
   animFlash = AnimationUtils.loadAnimation(
   this, R.anim.flash);
   animRotateLeft = AnimationUtils.loadAnimation(
   this, R.anim.rotate_left);
   animRotateRight = AnimationUtils.loadAnimation(
   this, R.anim.rotate_right);
}

注意

在这一点上,您需要导入一个新的类:

import android.view.animation.AnimationUtils;

在三个部分中实现 loadUI 方法。首先,让我们以通常的方式引用 XML 布局的部分:

private void loadUI(){
   imageView = findViewById(R.id.imageView);
   textStatus = findViewById(R.id.textStatus);
   btnFadeIn = findViewById(R.id.btnFadeIn);
   btnFadeOut = findViewById(R.id.btnFadeOut);
   btnFadeInOut = findViewById(R.id.btnFadeInOut);
   zoomIn = findViewById(R.id.btnZoomIn);
   zoomOut = findViewById(R.id.btnZoomOut);
   leftRight = findViewById(R.id.btnLeftRight);
   rightLeft = findViewById(R.id.btnRightLeft);
   topBottom = findViewById(R.id.btnTopBottom);
   bounce = findViewById(R.id.btnBounce);
   flash = findViewById(R.id.btnFlash);
   rotateLeft = findViewById(R.id.btnRotateLeft);
   rotateRight = findViewById(R.id.btnRotateRight);

现在我们将为每个按钮添加一个点击监听器。在 loadUI 方法的最后一个块之后立即添加以下代码:

btnFadeIn.setOnClickListener(this);
btnFadeOut.setOnClickListener(this);
btnFadeInOut.setOnClickListener(this);
zoomIn.setOnClickListener(this);
zoomOut.setOnClickListener(this);
leftRight.setOnClickListener(this);
rightLeft.setOnClickListener(this);
topBottom.setOnClickListener(this);
bounce.setOnClickListener(this);
flash.setOnClickListener(this);
rotateLeft.setOnClickListener(this);
rotateRight.setOnClickListener(this);

注意

我们刚刚添加的代码在所有代码行中都创*了错误。我们现在可以忽略它们,因为我们很快就会修复它们并讨论发生了什么。

loadUI 方法的第三部分和最后一部分设置了一个匿名类来处理 SeekBar 小部件。我们本可以像监听按钮点击和动画事件一样将其添加为 MainActivity 类的一个接口,但是对于像这样的单个 SeekBar 小部件,直接处理它是有意义的。

我们将重写三个方法,这是实现 OnSeekBarChangeListener 接口时所需的:

  • 一个检测 seek bar 位置变化的方法叫做 onProgressChanged

  • 一个检测用户开始改变位置的方法叫做 onStartTrackingTouch

  • 一个检测用户完成使用 seek bar 的方法叫做 onStopTrackingTouch

为了实现我们的目标,我们只需要向 onProgressChanged 方法添加代码,但是我们仍然必须重写它们全部。

onProgressChanged 方法中,我们所做的就是将 seek bar 的当前值分配给 seekSpeedProgress 成员变量,以便可以从其他地方访问它。然后我们使用这个值以及通过调用 seekBarSpeed.getMax() 获得的 SeekBar 小部件的最大可能值,并将消息输出到 textSeekerSpeed TextView 小部件。

将我们刚刚讨论过的代码添加到 loadUI 方法中:

seekBarSpeed = findViewById(R.id.seekBarSpeed);
textSeekerSpeed = findViewById(R.id.textSeekerSpeed);
seekBarSpeed.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   @Override
   public void onProgressChanged(SeekBar seekBar, 
   int value, boolean fromUser) {
         seekSpeedProgress = value;
         textSeekerSpeed.setText("" 
         + seekSpeedProgress 
         + " of " 
         + seekBarSpeed.getMax());
   }
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
   }
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
   }
});
}

现在我们需要修改 MainActivity 类的声明以实现两个接口。在这个应用程序中,我们将监听点击和动画事件,所以我们将使用的两个接口是 View.OnClickListenerAnimation.AnimationListener。请注意,要实现多个接口,我们只需用逗号分隔接口。

通过添加我们刚刚讨论过的以下突出显示的代码来修改 MainActivity 类的声明:

public class MainActivity extends AppCompatActivity 
   implements View.OnClickListener, 
   Animation.AnimationListener {

在这个阶段,我们可以添加并实现这些接口所需的方法 - 首先是 AnimationListener 的以下方法:onAnimationEndonAnimationRepeatonaAnimationStart。我们只需要向这两个方法中添加一点代码。在 onAnimationEnd 中,我们将 textStatustext 属性设置为 STOPPED,在 onAnimationStart 方法中,我们将 textStatustext 属性设置为 RUNNING。这将演示我们的动画监听器确实在监听和工作:

@Override
public void onAnimationEnd(Animation animation) {
   textStatus.setText("STOPPED");
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
   textStatus.setText("RUNNING");
}

onClick 方法非常长,但并不复杂。UI 中处理每个按钮的每个 case 简单地根据 seek bar 的当前位置设置动画的持续时间,设置动画以便监听事件,然后启动动画。

注意

您需要使用您喜欢的技术导入 View 类:

import android.view.View;

添加我们刚刚讨论过的 onClick 方法,然后我们就完成了这个小应用程序:

@Override
public void onClick(View v) {
switch(v.getId()){
   case R.id.btnFadeIn:
         animFadeIn.setDuration(seekSpeedProgress);
         animFadeIn.setAnimationListener(this);
         imageView.startAnimation(animFadeIn);
         break;
   case R.id.btnFadeOut:
         animFadeOut.setDuration(seekSpeedProgress);
         animFadeOut.setAnimationListener(this);
         imageView.startAnimation(animFadeOut);
         break;
   case R.id.btnFadeInOut:
         animFadeInOut.setDuration(seekSpeedProgress);
         animFadeInOut.setAnimationListener(this);
         imageView.startAnimation(animFadeInOut);
         break;
   case R.id.btnZoomIn:
         animZoomIn.setDuration(seekSpeedProgress);
         animZoomIn.setAnimationListener(this);
         imageView.startAnimation(animZoomIn);
         break;
   case R.id.btnZoomOut:
         animZoomOut.setDuration(seekSpeedProgress);
         animZoomOut.setAnimationListener(this);
         imageView.startAnimation(animZoomOut);
         break;
   case R.id.btnLeftRight:
         animLeftRight.setDuration(seekSpeedProgress);
         animLeftRight.setAnimationListener(this);
         imageView.startAnimation(animLeftRight);
         break;
   case R.id.btnRightLeft:
         animRightLeft.setDuration(seekSpeedProgress);
         animRightLeft.setAnimationListener(this);
         imageView.startAnimation(animRightLeft);
         break;
   case R.id.btnTopBottom:
         animTopBottom.setDuration(seekSpeedProgress);
         animTopBottom.setAnimationListener(this);
         imageView.startAnimation(animTopBottom);
         break;
   case R.id.btnBounce:
         /*
            Divide seekSpeedProgress by 10 because with
            the seekbar having a max value of 5000 it
            will make the animations range between
            almost instant and half a second 
            5000 /  10 = 500 milliseconds
         */
         animBounce.setDuration(seekSpeedProgress / 10);
         animBounce.setAnimationListener(this);
         imageView.startAnimation(animBounce);
         break;
   case R.id.btnFlash:
         animFlash.setDuration(seekSpeedProgress / 10);
         animFlash.setAnimationListener(this);
         imageView.startAnimation(animFlash);
         break;
   case R.id.btnRotateLeft:
         animRotateLeft.setDuration(seekSpeedProgress);
         animRotateLeft.setAnimationListener(this);
         imageView.startAnimation(animRotateLeft);
         break;
   case R.id.btnRotateRight:
         animRotateRight.setDuration(seekSpeedProgress);
         animRotateRight.setAnimationListener(this);
         imageView.startAnimation(animRotateRight);
         break;
}
}

现在运行应用程序。将 seek bar 移动到大致中心,以便动画运行一段合理的时间,如下一张截图所示:

图 19.4 – 将寻找栏移动到大致中心

图 19.4 – 将寻找栏移动到大致中心

点击放大按钮以查看效果,如下一个截图所示:

图 19.5 – 放大效果

图 19.5 – 放大效果

注意 Android 机器人上的文本如何在适当的时间从RUNNING更改为STOPPED。现在点击其中一个旋转按钮,以查看下一个显示的效果:

图 19.6 – 旋转按钮

图 19.6 – 旋转按钮

大多数其他动画在截图中无法展现出自己的价值,所以一定要自己尝试它们。

常见问题

  1. 我现在知道如何对小部件进行动画处理,但是我自己创*的形状或图像呢?

ImageView可以容纳任何您喜欢的图像。只需将图像添加到drawable文件夹,然后在ImageView小部件上设置适当的src属性。然后可以对ImageView中显示的任何图像进行动画处理。

  1. 但如果我想要比这更灵活的功能,比如绘图应用程序甚至游戏呢?

要实现这种功能,我们需要了解另一个通用的计算概念(PaintCanvasSurfaceView)。我们将学*如何从单个像素到形状绘制任何东西,然后在屏幕上移动它们,从下一章开始,第二十章绘图图形

总结

现在我们掌握了另一个增强应用程序的技巧,并且知道 Android 中的动画非常简单。我们可以在 XML 中设计动画并将文件添加到anim文件夹中。之后,我们可以在 Java 代码中使用Animation对象获取对 XML 中动画的引用。

然后,我们可以使用 UI 中小部件的引用,并使用setAnimation为其设置动画,并传入Animation对象。通过在小部件的引用上调用startAnimation来开始动画。

我们还看到可以控制动画的时序,以及监听动画事件。

在下一章中,我们将学*如何在 Android 中绘制图形。这将是关于图形的几章中的开始,我们将构*一个儿童风格的绘图应用程序。

现有文本属性 要设置的 id 属性值
淡入 btnFadeIn
淡出 btnFadeOut
淡入淡出 btnFadeInOut
放大 btnZoomIn
缩小 btnZoomOut
左右 btnLeftRight
右左 btnRightLeft
上下 btnTopBottom
弹跳 btnBounce
闪烁 btnFlash
向左旋转 btnRotateLeft
向右旋转 btnRotateRight

第二十章:绘制图形

整个章节将涉及 Android 的Canvas类和一些相关类,包括PaintColorBitmap。这些类结合起来在绘制屏幕时具有强大的功能。有时,Android API 提供的默认 UI 并不是我们需要的。如果我们想要制作一个绘图应用、绘制图表,或者创*一个游戏,我们需要控制 Android 设备提供的每个像素。

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

  • 理解Canvas和相关类

  • 编写基于Canvas的演示应用

  • 查看 Android 坐标系统,以便知道在哪里进行绘图

  • 学*绘制和操作位图

  • 编写基于位图的演示应用

让我们开始绘图!

技术要求

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

理解 Canvas 类

Canvas类是android.graphics包的一部分。在接下来的两章中,我们将使用android.graphics包中的所有以下import语句,以及来自现在熟悉的View包的一个额外的import语句。它们为我们提供了从 Android API 中获取一些强大绘图方法的权限:

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.widget.ImageView;

首先,让我们讨论BitmapCanvasImageView,正如前面的代码所强调的那样。

使用 Bitmap、Canvas 和 ImageView 开始绘图

由于 Android 设计用于运行各种类型的移动应用程序,我们不能立即开始输入绘图代码并期望它能够工作。我们需要做一些准备(编码)来考虑我们的应用正在运行的特定设备。有些准备可能有点反直觉,但我们将一步一步地进行。

Canvas 和 Bitmap

根据您如何使用Canvas类,这个术语可能会有点误导。虽然Canvas您绘制图形的类,就像绘画画布一样,但您仍然需要一个表面来转置画布。

在这种情况下(以及我们的前两个演示应用中),表面将来自Bitmap类。我们可以这样想:我们得到一个Canvas对象和一个Bitmap对象,然后将Bitmap对象设置为Canvas对象的一部分来进行绘制。

如果您按照字面意义上的画布这个词,这可能有点反直觉,但一旦设置好了,我们就可以忘记它,专注于我们想要绘制的图形。

注意

Canvas类提供了绘制的能力。它具有绘制形状、文本、线条和图像文件(包括其他位图),甚至绘制单个像素的所有方法。

Bitmap类被Canvas类使用,并且是被绘制的表面。你可以把Bitmap实例想象成在Canvas实例上的图片框内。

Paint

除了CanvasBitmap类之外,我们还将使用Paint类。这更容易理解。Paint是用来配置特定属性的类,比如我们将在Bitmap(在Canvas内)上绘制的颜色。

在我们开始绘图之前,还有一个谜题需要解决。

ImageView 和 Activity

ImageView类是Activity类用于向用户显示输出的类。有第三层抽象的原因是,正如我们在整本书中所看到的,Activity类需要将一个View传递给setContentView方法,以向用户显示内容。到目前为止,这一直是我们在可视化设计器或 XML 代码中创*的布局。

这一次,我们不需要常规的 UI;我们需要绘制线条、像素和形状。

有多种类型的类扩展了View类,使得可以制作许多不同类型的应用程序,并且它们都与Activity类兼容,这是所有常规 Android 应用程序(包括绘图应用程序和游戏)的基础。

因此,一旦绘制完成,就需要将Bitmap类(通过与Canvas的关联)与ImageView关联起来。最后一步是通过将其传递给setContentView方法,告诉Activity我们的ImageView类代表用户要看到的内容。

Canvas、Bitmap、Paint 和 ImageView 的简要总结

如果我们需要设置的代码结构的理论看起来并不简单,当你看到相对简单的代码时,你会松一口气。

迄今为止我们所知道的内容的一个简要总结:

  • 每个应用程序都需要一个Activity类来与用户和底层操作系统交互。因此,如果我们想成功,我们必须遵循所需的层次结构。

  • 我们将使用ImageView类,它是View类的一种类型。View类是Activity需要显示我们的应用程序给用户的内容。

  • Canvas类还提供了绘制线条、像素和其他图形的能力。它具有绘制形状、文本、线条和图像文件,甚至绘制单个像素的所有方法。

  • Bitmap类将与Canvas类关联,它是实际绘制的表面。

  • Canvas类使用Paint类来配置颜色等细节。

最后,一旦位图被绘制,我们必须将其与ImageView实例关联起来,然后通过setContentView方法将其设置为Activity的视图。

结果将是我们在Canvas实例中绘制的Bitmap实例,通过调用setContentView方法显示给用户的ImageView实例。哦!

注意

如果这并不是 100%清楚也没关系。并不是你没有清晰地看到事情 - 它只是没有一个清晰的关系。反复编写代码并使用这些技术将使事情变得更清晰。看看代码,做一下本章和下一章的演示应用程序,然后重新阅读本节。

让我们看看如何在代码中*立这种关系。不要担心输入代码,先学*它。

使用 Canvas 类

让我们看看代码和获取绘图所需的不同阶段,然后我们可以快速转移到使用Canvas演示应用程序真正绘制一些东西。

准备所需类的实例

第一步是声明我们需要的类的实例:

// Here are all the objects(instances)
// of classes that we need to do some drawing
ImageView myImageView;
Bitmap myBlankBitmap;
Canvas myCanvas;
Paint myPaint;

上面的代码声明了ImageViewBitmapCanvasPaint类型的引用。它们分别被命名为myImageViewmyBlankBitmapmyCanvasmyPaint

初始化对象

接下来,我们需要在使用之前初始化我们的新对象:

// Initialize all the objects ready for drawing
// We will do this inside the onCreate method
int widthInPixels = 800;
int heightInPixels = 800;
myBlankBitmap = Bitmap.createBitmap(widthInPixels,
         heightInPixels,
         Bitmap.Config.ARGB_8888);
myCanvas = new Canvas(myBlankBitmap);
myImageView = new ImageView(this);
myPaint = new Paint();
// Do drawing here

注意上面代码中的这条注释:

// Do drawing here

这是我们配置颜色和绘制内容的地方。还要注意代码顶部我们声明和初始化了两个名为widthInPixelsheightInPixelsint变量。当我们编写Canvas演示应用程序时,我将更详细地介绍其中的一些代码行。

我们现在准备好绘制了。我们所需要做的就是将ImageView实例分配给Activity

设置 Activity 内容

最后,在我们看到我们的绘图之前,我们告诉 Android 使用我们称为myImageViewImageView实例作为要显示给用户的内容:

// Associate the drawn upon Bitmap with the ImageView
myImageView.setImageBitmap(myBlankBitmap);
// Tell Android to set our drawing
// as the view for this app
// via the ImageView
setContentView(myImageView);

正如我们迄今为止在每个应用程序中看到的那样,setContentView方法是Activity类的一部分,我们传入myImageView作为参数,而不是像我们在整本书中一直做的那样传入 XML 布局。就是这样。我们现在所需要学*的就是如何在Bitmap实例上实际绘制。

在我们进行一些绘图之前,我认为开始一个真正的项目,逐步复制和粘贴我们刚刚讨论过的代码到正确的位置,然后实际看到一些东西被绘制到屏幕上会很有用。

让我们进行一些绘制。

画布演示应用程序

我们将创*一个新项目,只是为了探索使用Canvas进行绘制的主题。我们将重用我们刚刚学到的知识,这次还将绘制到Bitmap实例上。

创*一个新项目

创*一个新项目,命名为Canvas Demo。选择空活动模板。

此外,我们将使用Activity类的原始版本,因此MainActivity类将扩展Activity,而不是之前使用的AppCompatActivity。这仅仅是因为我们不再需要AppCompatActivity类提供的额外功能。

注意

此应用程序的完整代码可以在第二十章/Canvas Demo文件夹中的下载包中找到。

编写 Canvas 演示应用程序

要开始,请编辑MainActivity.java中的代码,包括添加import指令和更改MainActivity类继承的Activity类的版本。还要注意下一个代码中setContentView方法的调用也已被删除。我们很快会替换它:

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.widget.ImageView;
public class MainActivity extends Activity {
    // Here are all the objects(instances)
    // of classes that we need to do some drawing
    ImageView myImageView;
    Bitmap myBlankBitmap;
    Canvas myCanvas;
    Paint myPaint;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}

现在我们已经声明了所需类的实例,我们可以初始化它们。在onCreate方法中调用super.onCreate…之后,添加以下代码,如下一个代码所示:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   // Initialize all the objects ready for drawing
   // We will do this inside the onCreate method
   int widthInPixels = 800;
   int heightInPixels = 600;
   // Create a new Bitmap
   myBlankBitmap = Bitmap.createBitmap(widthInPixels,
                heightInPixels,
                Bitmap.Config.ARGB_8888);
   // Initialize the Canvas and associate it
   // with the Bitmap to draw on
   myCanvas = new Canvas(myBlankBitmap);
   // Initialize the ImageView and the Paint
   myImageView = new ImageView(this);
   myPaint = new Paint();
}

这段代码与我们之前讨论Canvas时看到的一样。值得探索Bitmap类的初始化,因为它并不简单。

探索位图初始化

位图,在基于图形的应用程序和游戏中更典型地用于表示不同的画笔、玩家、背景、游戏对象等。在这里,我们只是用它来绘制。在下一个项目中,我们将使用位图来表示我们绘制的主题,而不仅仅是绘制的表面。

需要解释的方法是createBitmap方法。从左到右的参数如下:

  • 宽度(以像素为单位)

  • 高度(以像素为单位)

  • 位图配置

位图可以以几种不同的方式配置。ARGB_8888配置意味着每个像素由 4 个字节的内存表示。

Android 可以使用几种位图格式。这个对于一系列的颜色来说是完美的,并且将确保我们使用的位图和我们请求的颜色将按预期绘制。有更高和更低的配置,但ARGB_8888对于本章来说是完美的。

现在我们可以进行实际的绘制了。

在屏幕上绘制

myPaint初始化之后,在onCreate方法的闭合大括号内添加下面突出显示的代码:

   myPaint = new Paint();
   // Draw on the Bitmap
   // Wipe the Bitmap with a blue color
   myCanvas.drawColor(Color.argb(255, 0, 0, 255));
   // Re-size the text
   myPaint.setTextSize(100);
   // Change the paint to white
   myPaint.setColor(Color.argb(255, 255, 255, 255));
   // Draw some text
   myCanvas.drawText("Hello World!",100, 100, myPaint);
   // Change the paint to yellow
   myPaint.setColor(Color.argb(255, 212, 207, 62));
   // Draw a circle
   myCanvas.drawCircle(400,250, 100, myPaint);
}

前面的代码使用myCanvas.drawColor方法来填充屏幕颜色。

myPaint.setTextSize方法定义了接下来将绘制的文本的大小。myPaint.setColor方法确定了未来绘制的颜色。myCanvas.drawText方法实际将文本绘制到屏幕上。

分析传递给drawText方法的参数,我们可以看到文本将会显示“Hello World!”,并且将在我们的位图(myBitmap)的左侧 100 像素和顶部 100 像素处绘制。

接下来,我们再次使用setColor方法来改变将用于绘制的颜色。最后,我们使用drawCircle方法来绘制一个距左侧 400 像素,顶部 100 像素的圆。圆的半径为 100 像素。

我保留了解释Color.argb方法直到现在。

解释 Color.argb

Color类,不出所料,帮助我们操纵和表示颜色。先前使用的argb方法返回使用alpha(不透明度/透明度),红色,绿色,蓝色argb)模型构*的颜色。该模型使用从 0(无颜色)到 255(全颜色)的值。重要的是要注意,尽管在反思时可能似乎显而易见,但混合的颜色是光的强度,与我们混合颜色时发生的情况完全不同,例如混合油漆。

注意

要设计 argb 值并进一步探索这个模型,请查看这个方便的网站:www.rapidtables.com/web/color/RGB_Color.html。该网站可以帮助您选择 RGB 值;然后您可以尝试 alpha 值。

用于清除绘图表面的值是255, 0, 0, 255。这些值意味着完全不透明(纯色),没有红色,没有绿色,完全蓝色。这制作了一个蓝色。

argb方法的下一个调用是在第一个调用setColor时,我们正在设置文本所需的颜色。值255, 255, 255, 255表示完全不透明,完全红色,完全绿色和完全蓝色。当您将光与这些值混合时,您会得到白色。

argb方法的最终调用是在最终调用setColor方法时,我们正在设置绘制圆的颜色。255, 21, 207, 62制作了太阳黄色。

在我们运行代码之前的最后一步是添加对setContentView方法的调用,该方法将我们的ImageViewmyImageView)放置为要设置为此应用程序内容的View实例。以下是要在onCreate方法的结束大括号之前添加的最终代码行:

// Associate the drawn upon Bitmap with the ImageView
myImageView.setImageBitmap(myBlankBitmap);
// Tell Android to set our drawing
// as the view for this app
// via the ImageView
setContentView(myImageView);

最后,我们通过调用setContentView方法告诉Activity类使用myImageView

当您运行时,这就是Canvas演示的样子。我们可以看到一个 800 x 800 像素的图纸。在下一章中,我们将使用更高级的技术来利用整个屏幕,并且我们还将学*有关线程以使图形实时移动:

图 20.1 - 画布演示

](https://gitee.com/OpenDocCN/freelearn-android-zh/raw/master/docs/andr-prog-bg-3e/img/Figure_20.1_B16773.jpg)

图 20.1 - 画布演示

如果我们更好地了解 Android 坐标系统,将有助于理解我们在Canvas类绘图方法中使用的坐标的结果。

Android 坐标系统

正如我们所看到的,绘制位图是微不足道的。但是,我们用来绘制图形的坐标系统需要简要解释。

绘图和绘制

当我们将Bitmap对象绘制到屏幕上时,我们传入要绘制对象的坐标。给定 Android 设备的可用坐标取决于其屏幕的分辨率。

例如,Google Pixel 手机在横向视图中的屏幕分辨率为 1,920 像素(横向)x 1,080 像素(纵向)。

这些坐标的编号系统从左上角的0, 0开始,然后向下和向右移动,直到右下角,即像素1919, 107919201919之间以及10801079之间明显的 1 像素差异是因为编号从 0 开始。

因此,当我们将位图或其他任何东西绘制到屏幕上(例如Canvas圆和矩形),我们必须指定一个x,y坐标。

此外,位图(或Canvas形状)当然包括许多像素。那么,给定位图的哪个像素会在我们指定的x,y屏幕坐标处绘制?

答案是Bitmap对象的左上角像素。看看下一个图,它应该使用 Google Pixel 手机作为示例来澄清屏幕坐标。作为解释 Android 坐标绘图系统的图形手段,我将使用一个可爱的太空飞船图形:

图 20.2 - 屏幕坐标

图 20.2 - 屏幕坐标

此外,坐标是相对于您所绘制的内容的。因此,在我们刚刚编写的Canvas演示应用程序以及下一个演示应用程序中,坐标是相对于位图(myBitmap)的。在下一章中,我们将使用整个屏幕,前面的图将准确地表示发生的情况。

让我们再做一些绘图,这次使用来自图形文件的位图。我们将使用与此应用程序中看到的相同的起始代码。

创*位图

在我们深入代码之前,让我们先做一点理论,考虑一下我们将如何在屏幕上呈现图像。要绘制位图,我们将使用Canvas类的drawBitmap方法。

首先,我们需要在res/drawable文件夹中的项目中添加一个位图;我们将在接下来的Bitmap演示应用程序中真正做到这一点。现在,假设图形文件/位图的名称为myImage.png

接下来,我们声明一个Bitmap类型的对象,就像我们在上一个演示中用于背景的Bitmap对象一样:

Bitmap mBitmap;

接下来,我们需要使用我们之前添加到项目的drawable文件夹中的首选图像来初始化mBitmap对象:

mBitmap = BitmapFactory.decodeResource
                (getResources(), R.drawable.myImage);

BitmapFactory方法的静态decodeResource方法用于初始化mBitmap。它接受两个参数。第一个是对getResources的调用,这是由Activity类提供的。这个方法,顾名思义,可以访问项目资源,第二个参数R.drawable.myImage指向drawable文件夹中的myImage.png文件。位图(mBitmap)现在已经准备好由Canvas类绘制。

然后,您可以使用以下代码绘制位图:

// Draw the bitmap at coordinates 100, 100
mCanvas.drawBitmap(mBitmap, 
                100, 100, mPaint);

下面是前一节中的飞船图形在屏幕上绘制时的样子,作为我们讨论旋转位图时的参考:

图 20.3 - 飞船图形

图 20.3 - 飞船图形

操作位图

然而,通常情况下,我们需要以旋转或其他方式改变的状态绘制位图。很容易使用 Photoshop 或您喜欢的任何图像编辑软件从原始位图创*更多位图以面向其他方向。然后,当我们来绘制我们的位图时,我们可以简单地决定面向哪个方向,并绘制适当的预加载位图。

然而,我认为如果我们只使用一个单一的源图像,并学* Android 提供的用于在我们的 Java 代码中操作图像的类,将会更有趣和有教育意义。然后,您将能够将旋转和反转的图形添加到您的应用程序开发工具包中。

位图到底是什么?

位图之所以被称为位图,是因为它确实就是这样:一张位的地图。虽然有许多使用不同范围和值来表示颜色和透明度的位图格式,但它们都归结为同一件事。它们是一组/地图值,每个值代表一个像素的颜色。

因此,要旋转、缩放或反转位图,我们必须对位图的每个像素/位进行适当的数学计算。这些计算并不是非常复杂,但也不是特别简单。如果您完成了高中的数学课程,您可能不会太费力地理解这些数学知识。

不幸的是,仅仅理解数学是不够的。我们还需要设计高效的代码,以及了解位图格式,然后为每种格式修改我们的代码。这并不是微不足道的。幸运的是,Android API 已经为我们做好了一切。见识一下Matrix类。

Matrix 类

这个类被命名为Matrix,因为它使用数学概念和规则来对一系列值(称为矩阵的复数)进行计算。

注意

Android 的Matrix类与同名电影系列无关。然而,作者*议所有有抱负的应用程序开发者服用红色药丸。

你可能对矩阵很熟悉,但如果你不熟悉也不用担心,因为Matrix类隐藏了所有的复杂性。此外,Matrix类不仅允许我们对一系列值进行计算,还具有一些预先准备的计算,使我们能够做一些事情,比如围绕另一个点旋转特定角度的点。所有这些都不需要了解三角学。

注意

如果你对数学运算的工作方式感到好奇,并且想要一个绝对初学者指南来了解旋转游戏对象的数学知识,那么请看看我网站上以可飞行和可旋转的飞船为结尾的一系列 Android 教程:

gamecodeschool.com/essentials/calculating-heading-in-2d-games-using-trigonometric-functions-part-1/

gamecodeschool.com/essentials/rotating-graphics-in-2d-games-using-trigonometric-functions-part-2/

gamecodeschool.com/android/2d-rotation-and-heading-demo/

本书将继续使用 Android 的Matrix类,但在下一章创*粒子系统时,我们将进行稍微更高级的数学运算。

将位图反转以面对相反方向

首先,我们需要创*Matrix类的一个实例。下一行代码以熟悉的方式调用new来调用默认构造函数:

Matrix matrix = new Matrix();

注意

请注意,您现在不需要将任何此代码添加到项目中;稍后将再次显示所有与Matrix相关的代码,其中将提供更多的上下文。我只是认为在此之前单独看到所有与Matrix相关的代码会更容易一些。

现在我们可以使用Matrix类的许多巧妙方法之一。preScale方法需要两个参数:一个用于水*变化,一个用于垂直变化。看看下面的代码:

matrix.preScale(-1, 1);

preScale方法将循环遍历每个像素位置,并将所有水*坐标乘以-1,所有垂直坐标乘以1

这些计算的效果是所有垂直坐标将保持不变,因为如果乘以 1,那么数字不会改变。然而,当你乘以-1 时,像素的水*位置将被倒置。看下面的例子:

水*位置 0、1、2、3 和 4 将变为 0、-1、-2、-3 和-4。

在这个阶段,我们已经创*了一个可以对位图执行必要计算的矩阵。我们实际上还没有对位图做任何事情。要使用这个矩阵,我们调用Bitmap类的createBitmap方法,如下面所示的代码行:

mBitmapLeft = Bitmap
.createBitmap(mBitmap,
          0, 0, 25, 25, matrix, true);

先前的代码假设mBitmapLeft已经初始化,以及mBitmapcreateBitmap方法的参数解释如下:

  • mBitmapHeadRight是一个Bitmap对象,已经被创*和缩放,并且已经加载了一个飞船(面向右侧)的图像。这个图像将被用作创*新位图的源。源位图实际上不会被改变。

  • 0, 0是我们希望将新位图映射到的水*和垂直起始位置。

  • 25, 25参数是设置位图缩放大小的值。

  • 下一个参数是我们预先准备的Matrix实例matrix

  • 最后一个参数true指示createBitmap方法需要过滤以正确处理位图的创*。

当绘制到屏幕上时,mBitmapLeft将如下所示:

图 20.4 - mBitmapLeft

图 20.4 - mBitmapLeft

我们还可以使用旋转矩阵创*面向上或面向下的位图。

旋转位图以面向上或向下

让我们看看如何旋转位图,然后我们可以构*演示应用程序。我们已经有了Matrix类的一个实例,所以我们所要做的就是调用preRotate方法,创*一个能够按照preRotate的单个参数指定的角度旋转每个像素的矩阵。看看这行代码:

// A matrix for rotating
matrix.preRotate(-90);

这有多简单?matrix实例现在已准备好逆时针(-)旋转我们传递给它的任何一系列数字,旋转90度。

下一行代码与我们之前解析的对createBitmap的调用具有完全相同的参数,只是新的Bitmap实例分配给了mBitmapUpmatrix的效果是执行旋转而不是preScale

mBitmapUp = Bitmap
.createBitmap(mBitmap,
         0, 0, ss, ss, matrix, true);

这是绘制mBitmapUp时的样子:

图 20.5 - mBitmapUp

图 20.5 - mBitmapUp

您还可以使用相同的技术,但在preRotate的参数中使用不同的值来将位图向下旋转。让我们继续进行演示应用程序,看看所有这些内容是如何运作的。

位图操作演示应用程序

既然我们已经学*了理论,让我们来绘制和旋转一些位图。使用操作位图创*一个新项目。

将图形添加到项目中

右键单击并从第二十章/操作位图/可绘制文件夹中选择bob.png图形文件。

在 Android Studio 中,定位项目资源管理器窗口中的app/res/drawable文件夹。下一张截图清楚地显示了这个文件夹的位置以及其中带有bob.png图像的样子:

图 20.6 - app/res/drawable 文件夹中的 bob.png

图 20.6 - app/res/drawable 文件夹中的 bob.png

右键单击项目中的bob.png文件。单击两次确定,以确认将文件导入项目的默认选项。

编辑MainActivity类的代码,包括所有必需的import指令、Activity类的基本版本和一些成员变量,以便我们可以开始。此阶段MainActivity类的状态如下所示:

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.widget.ImageView;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
public class MainActivity extends Activity {
    // Here are all the objects(instances)
    // of classes that we need to do some drawing
    ImageView myImageView;
    Bitmap myBlankBitmap;
    Bitmap bobBitmap;
    Canvas myCanvas;
    Paint myPaint;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    }
}

现在我们可以在onCreate中初始化所有成员:

// Initialize all the objects ready for drawing
int widthInPixels = 2000;
int heightInPixels = 1000;
// Create a new Bitmap
myBlankBitmap = Bitmap.createBitmap(widthInPixels,
         heightInPixels,
         Bitmap.Config.ARGB_8888);
// Initialize Bob
bobBitmap = BitmapFactory.decodeResource
         (getResources(), R.drawable.bob);
// Initialize the Canvas and associate it
// with the Bitmap to draw on
myCanvas = new Canvas(myBlankBitmap);
// Initialize the ImageView and the Paint
myImageView = new ImageView(this);
myPaint = new Paint();
// Draw on the Bitmap
// Wipe the Bitmap with a blue color
myCanvas.drawColor(Color.argb(255, 0, 0, 255));

接下来,我们添加对三种方法的调用,我们很快将编写这些方法,并将我们的新绘图设置为应用程序的视图:

// Draw some bitmaps
drawRotatedBitmaps();
drawEnlargedBitmap();
drawShrunkenBitmap();
// Associate the drawn upon Bitmap with the ImageView
myImageView.setImageBitmap(myBlankBitmap);
// Tell Android to set our drawing
// as the view for this app
// via the ImageView
setContentView(myImageView);

现在添加执行位图操作的drawRotatedBitmap方法:

void drawRotatedBitmaps(){
   float rotation = 0f;
   int horizontalPosition =350;
   int verticalPosition = 25;
   Matrix matrix = new Matrix();
   Bitmap rotatedBitmap = Bitmap.createBitmap(100,
                200,
                Bitmap.Config.ARGB_8888);
   for(rotation = 0; rotation < 360; rotation += 30){
         matrix.reset();
         matrix.preRotate(rotation);
         rotatedBitmap = Bitmap
                      .createBitmap(bobBitmap,
                                  0, 0, bobBitmap
                                  .getWidth()-1, 
                                  bobBitmap.getHeight()-1, 
                                  matrix, true);
         myCanvas.drawBitmap(rotatedBitmap, 
                      horizontalPosition, 
                      verticalPosition,  
                      myPaint);

         horizontalPosition += 120;
         verticalPosition += 70;
   }
}

先前的代码使用for循环来循环遍历 360 度,每次 30 度。循环中的每个值都用于Matrix实例中旋转 Bob 的图像,然后使用drawBitmap方法将其绘制到屏幕上。

按照下面显示的方式添加最后两种方法:

void drawEnlargedBitmap(){
   bobBitmap = Bitmap
                .createScaledBitmap(bobBitmap,
                            300, 400, false);
   myCanvas.drawBitmap(bobBitmap, 25,25, myPaint);
}
void drawShrunkenBitmap(){
   bobBitmap = Bitmap
                .createScaledBitmap(bobBitmap,
                            50, 75, false);
   myCanvas.drawBitmap(bobBitmap, 250,25, myPaint);
}

drawEnlargedBitmap方法使用createScaledBitmap方法,大小为 300x400 像素。然后,drawBitmap方法将其绘制到屏幕上。

drawShrunkenBitmap使用完全相同的技术,只是缩放然后绘制一个 50x75 像素的图像。

运行应用程序,看看 Bob 是如何长大、缩小,然后在 30 度间隔下 360 度旋转的,如下一张截图所示:

![图 20.7 - 应用程序运行方式

第 20.7 图 - B16773.jpg

图 20.7 - 应用程序运行方式

我们绘图库中唯一缺少的是观看所有这些活动发生的能力。我们将在下一章中填补这一知识空白。

常见问题

  1. 我知道如何进行所有这些绘图,但我看不到任何移动。为什么?

要看到物体移动,您需要能够调节绘图的每个部分发生的时间。您需要使用动画技术。这并不是微不足道的,但对于一个有决心的初学者来说也不是难以掌握的。我们将在下一章中学*所需的主题。

总结

在本章中,我们看到了如何绘制自定义形状、文本和位图。现在我们知道如何绘制和操作基本形状、文本和位图,我们可以把事情提升到一个新水*。

在下一章中,我们将开始我们的下一个重要应用程序,这是一个儿童绘画应用程序,实际上在按下按钮时会活跃起来。

第二十一章:线程和启动 Live Drawing 应用程序

在本章中,我们将开始我们的下一个应用程序。这个应用程序将是一个儿童绘画应用程序,用户可以用手指在屏幕上绘画。但是,这个绘画应用程序将略有不同。用户绘制的线条将包括成千上万个粒子的粒子系统。我们将称这个项目为 Live Drawing。

为了实现这一点,我们将执行以下操作:

  • 开始使用 Live Drawing 应用程序

  • 了解实时交互,有时被称为游戏循环

  • 了解线程

  • 编写一个准备在下一章绘制的实时系统

让我们开始吧。

技术要求

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

创* Live Drawing 项目

要开始,请在 Android Studio 中创*一个名为Live Drawing的新项目。使用空活动项目。

现在我们将考虑文件的名称和屏幕房地产。在这个项目中,我们将学*一些新东西。对于我们的 Activity 类,使用默认名称并不总是合适的。在这个项目中,Activity 类不会是最重要的类,MainActivity似乎不是一个合适的名称。让我们重命名它。

将 MainActivity 重构为 LiveDrawingActivity

对于我们的代码的所有不同部分使用有意义的名称是一个很好的做法。对于这个项目,我认为MainActivity有点模糊和不确定。我们可以将其使用,但让我们将其重命名为更有意义的名称。这也将让我们看到如何将MainActivity文件名更改为LiveDrawingActivity,Android Studio 将更改AndroidManifest.xml文件和MainActivity.java(即将更改为LiveDrawingActivity.java)文件中的一些代码。

在项目面板中,右键单击MainActivity文件,然后选择重构 | 重命名。在弹出窗口中,将MainActivity更改为LiveDrawingActivity。将所有其他选项保持为默认值,然后单击重构按钮。

请注意,项目面板中的文件名已如预期更改,但AndroidManifest.xml文件中的多个实例以及LiveDrawingActivity.java文件中的多个实例也已更改为LiveDrawingActivity。如果您感兴趣,现在可以扫描这些文件以查看这一点,但无论如何,我们将在即将到来的章节中更详细地讨论这两个文件。

注意

重构是一个重要的工具,了解幕后发生的更多事情对于避免混淆至关重要。

将游戏锁定到全屏和横向方向

我们希望使用用户 Android 设备提供的每个像素,因此我们将对AndroidManifest.xml文件进行更改,这允许我们为应用程序使用一个样式,隐藏用户界面中的所有默认菜单。

manifests文件夹中打开AndroidManifest.xml文件。在AndroidManifest.xml文件中,找到以下代码行:android:name=".LiveDrawingActivity">。

将光标放在先前显示的关闭>之前。按Enter键几次,将>移动到先前显示的其余行下方几行。

".LiveDrawingActivity"下方,但在新定位的>之前,键入或复制并粘贴下一行代码,以使游戏在没有默认用户界面的情况下运行。

请注意,代码行显示在两行上,因为它太长而无法适应打印页面,但在 Android Studio 中,您应该将其输入为一行:

android:theme=
"@android:style/Theme.Holo.Light.NoActionBar.Fullscreen"

这是一组繁琐的步骤,所以在这里我向您展示了这个文件的更大范围,其中我们刚刚添加的代码也被突出显示,以提供额外的上下文。如前所述,我不得不将一些代码行显示为两行:

…
<activity android:name=".LiveDrawingActivity"
     android:theme=
     "@android:style/
     Theme.Holo.Light.NoActionBar.Fullscreen"
     >
     <intent-filter>
          <action android:name="android.intent.action.MAIN"
          />
<category android:name= "android.intent.category.LAUNCHER" />
     </intent-filter>
</activity>
…

现在我们的应用程序将使用设备提供的所有屏幕空间,而不需要任何额外的菜单。我们还将看到一些新的 Java 代码,使我们的应用程序占据屏幕的每一个像素。

创*一些占位符类

这个应用程序只包含 Java 文件。到本章结束时的所有代码都可以在下载包的第二十一章文件夹中找到。

接下来,我们将创*一些空类,我们将在接下来的两章项目中编写。创*一个名为LiveDrawingView的新类,一个名为ParticleSystem的新类,以及一个名为Particle的新类。

让我们稍微展望一下。

展望 Live Drawing 应用程序

由于这个应用程序更加深入,并且需要实时响应,因此需要使用稍微更深入的结构。起初,这似乎是一个复杂,但从长远来看,它甚至可以使我们的代码更简单、更容易理解。

在 Live Drawing 应用程序中,我们将有四个类:

  • LiveDrawingActivity:Android API 提供的Activity类是与操作系统交互的类。我们已经看到了当用户点击应用程序图标启动应用程序时,操作系统是如何与onCreate交互的。我们不再使用一个叫做MainActivity的类来处理所有事情,而是使用一个基于 Activity 的类来处理应用程序的启动和关闭,以及通过获取屏幕分辨率来帮助初始化。这个类将是Activity类型是有意义的。然而,很快你会看到,我们将委托触摸交互给另一个类,这个类也将处理几乎每个方面的应用程序。这将为我们介绍一些新的有趣的概念。

  • LiveDrawingView:这是负责绘图和创*实时环境的类,允许用户在他们的创作移动和发展的同时进行交互。

  • ParticleSystem:这个类将管理Particle类的成千上万个实例。

  • Particle:这个类将是最简单的。它将在屏幕上有一个位置和一个方向。当LiveDrawingView类提示时,它将每秒更新大约 60 次。

现在我们可以开始编码了。

编写 LiveDrawingActivity 类

让我们开始编写基于Activity的类。当我们重构MainActivity时,我们将这个类称为LiveDrawingActivity

用以下代码替换LiveDrawingActivity类的内容(不包括包声明):

import android.app.Activity;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;
import android.view.Window;
public class LiveDrawingActivity extends Activity {
    private LiveDrawingView mLiveDrawingView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        Display display = getWindowManager()
        .getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        mLiveDrawingView = new LiveDrawingView(
        this, size.x, size.y);
        setContentView(mLiveDrawingView);
    }
}

代码显示了一些错误,我们很快会讨论它们。

第一行新代码是这样的:

requestWindowFeature(Window.FEATURE_NO_TITLE);

这行代码从用户界面中移除了标题。当我们运行这个应用程序时,屏幕将完全为空。

代码以以下方式获取设备的像素数(宽和高)。再看一下下一行新代码:

Display display = getWindowManager().getDefaultDisplay();

我们创*了一个名为displayDisplay类型的对象,并用调用getWindowManagergetDefaultDisplay方法的结果依次初始化它,这些方法都是Activity类的一部分。

然后我们创*了一个名为sizePoint类型的新对象。我们将size作为参数发送给display.getSize方法。Point类型有一个xy成员变量,因此size对象也有,经过第三行代码后,它现在保存了显示器的宽度和高度(以像素为单位)。

现在我们将屏幕分辨率隐藏在size对象中的xy变量中。

接下来的新事物是,我们声明了我们的LiveDrawingView类的一个实例。目前,这是一个空类:

private LiveDrawingView mLiveDrawingView;

接下来,在onCreate方法中,我们像这样初始化mLiveDrawingView

mLiveDrawingView = new LiveDrawingView(this, size.x, size.y);

我们正在向LiveDrawingView构造函数传递三个参数。显然,我们还没有编写构造函数,我们知道默认构造函数不带参数。因此,在我们修复它之前,这行代码将导致错误。

传递的参数很有趣。首先是this,它是对LiveDrawingActivity类的引用。LiveDrawingView类将需要使用它需要这个引用的方法。

第二个和第三个参数是水*和垂直屏幕分辨率。我们的应用程序需要这些来执行诸如检测屏幕边缘和将绘图对象缩放到适当大小等任务是有意义的。当我们开始编写LiveDrawingView构造函数时,我们将进一步讨论这些参数。

接下来,看一下接下来的一行:

setContentView(mLiveDrawingView);

这是在 Canvas Demo 应用程序中,我们将ImageView设置为应用程序的内容。请记住,Activity类的setContentView方法必须接受一个View对象,而ImageView是一个View。前一行代码似乎在暗示我们将使用我们的LiveDrawingView类作为应用程序的可见内容。但是LiveDrawingView,尽管名字是这样,但它还不是View。至少目前还不是。

在我们向LiveDrawingActivity添加几行代码之后,我们将修复构造函数和不是View问题。

读者挑战

你能猜到解决方案可能是哪种 OOP 概念吗?

添加这两个重写的方法,然后我们将讨论它们。将它们添加到onCreate方法的右大括号下面,但在LiveDrawingActivity类的右大括号之前添加:

@Override
protected void onResume() {
   super.onResume();
   // More code here later in the chapter
}
@Override
protected void onPause() {
   super.onPause();
   // More code here later in the chapter
}

我们所做的是重写Activity类的另外两个方法。我们将看到为什么我们需要这样做以及我们将在这些方法内部做什么。这里要注意的一点是,通过添加这些重写的方法,我们给了操作系统在另外两种情况下通知我们用户意图的机会。就像我们在 Note to Self 应用程序中保存和加载数据时所做的那样。

在这一点上,转到LiveDrawingView类是有意义的,这是该应用程序的主要类。我们将在本章末回到LiveDrawingActivity类。

编写 LiveDrawingView 类

我们要做的第一件事是解决LiveDrawingView类不是View类型的问题。更新类声明如下所示:

class LiveDrawingView extends SurfaceView {

注意

您需要导入android.view.SurfaceView类。

SurfaceViewView的后代,现在LiveDrawingView也是View的一种类型,通过继承。再次看看已添加的import语句。这种关系如下所示:

import android..SurfaceView

注意

请记住,正是由于多态性,我们可以将View的后代发送到LiveDrawingActivity类中的setContentView方法,而正是由于继承,LiveDrawingView类现在是SurfaceView的一种类型。

有很多View的后代可以扩展以解决这个初始问题,但随着我们继续,我们将看到SurfaceView类具有一些非常特定的功能,非常适合实时交互应用程序,这使得这个选择对我们来说是正确的。

我们在这个类和LiveDrawingActivity类中仍然有错误。这两个错误都是由于缺少合适的构造方法。

这里有一张屏幕截图显示了LiveDrawingView类中的错误,因为我们扩展了SurfaceView

图 21.1 - LiveDrawingView 类中的错误

图 21.1 - LiveDrawingView 类中的错误

LiveDrawingActivity中的错误更明显;我们调用了一个不存在的方法。然而,前面截图中显示的错误不太容易理解。现在让我们讨论LiveDrawingView类声明中的错误。

LiveDrawingView类,现在是一个SurfaceView,必须提供一个构造函数,因为如 OOP 章节中所述,一旦你提供了自己的构造函数,默认(无参数)构造函数就不复存在了。由于SurfaceView类实现了几种不同的构造函数,我们必须明确实现其中的一个或编写我们自己的。因此出现了之前的错误。

由于没有提供的SurfaceView构造函数正是我们所需要的,我们将提供我们自己的构造函数。

注意

如果你想知道如何知道提供了哪些构造函数和关于 Android 类的其他细节,只需谷歌一下。输入类名,后跟API。谷歌几乎总会提供一个指向 Android 开发者网站相关页面的链接作为最顶部的结果。这是SurfaceView页面的直接链接:developer.android.com/reference/android/view/SurfaceView.html。查看Public constructors标题下,你会看到一些可选的构造函数。

LiveDrawingActivity还要求我们创*一个构造函数,与我们尝试在LiveDrawingActivity类中初始化的方式匹配:

mLiveDrawingView = new LiveDrawingView(this, size.x, size.y);

让我们添加一个构造函数,与从LiveDrawingActivity传入this和屏幕分辨率的调用匹配,并一次解决两个问题。

编写LiveDrawingView

记住,LiveDrawingView类无法看到LiveDrawingActivity类中的变量。通过使用构造函数,LiveDrawingActivity提供了对自身(this)以及包含在size.xsize.y中的像素屏幕大小的引用。将此构造函数添加到LiveDrawingView.java文件中。代码必须放在类的开头和结尾的大括号内。这是一种约定,但不是必须的,将构造函数放在其他方法之上,但在成员变量声明之后:

// The LiveDrawingView constructor
// Called when this line:
// mLiveDrawingView = new LiveDrawingView(this, size.x, size.y);
// is executed from LiveDrawingActivity
public LiveDrawingView(Context context, int x, int y) {
        // Super... calls the parent class
        // constructor of SurfaceView
        // provided by the Android API
        super(context);
}

注意

使用以下代码导入Context类:

import android.content.Context;

现在我们的LiveDrawingView类或初始化它的LiveDrawingActivity类中没有错误。

在这个阶段,我们可以运行应用程序,看看使用LiveDrawingView作为setContentView中的View是否有效,并且我们有一个美丽的空白屏幕,准备在上面绘制我们的粒子系统。如果你愿意,可以尝试一下,但我们将编写LiveDrawingView类,使其做一些事情,包括在构造函数中添加代码,接下来。

在这个项目的过程中,我们将不断回到这个类。我们现在要做的是准备好基本知识,以便在下一章中编写ParticleSystem实例后添加它们。

为了实现这一点,首先我们将添加一堆成员变量,然后我们将在构造函数内部添加一些代码,以便在LiveDrawingActivity实例化/创*时设置类。

接下来,我们将编写draw方法,它将揭示我们需要采取的新步骤,以便每秒在屏幕上绘制 60 次,并且我们还将看到一些熟悉的代码,使用了我们上一章的老朋友CanvasPaintdrawText

在这一点上,我们需要讨论更多的理论 - 诸如我们将如何计时粒子的动画以及如何锁定这些时间而不干扰 Android 的*稳运行等事情。这最后两个主题,游戏循环线程,将允许我们添加本章的最终代码,并见证我们的粒子系统绘画应用程序的运行 - 尽管只有一点点文本。

注意

游戏循环是一个概念,描述了允许虚拟系统在允许用户改变/交互的同时更新和绘制自己的能力。

添加成员变量

按照下面所示的变量添加到LiveDrawingView声明之后但在构造函数之前,然后导入必要的额外类:

// Are we debugging?
private final boolean DEBUGGING = true;
// These objects are needed to do the drawing
private SurfaceHolder mOurHolder;
private Canvas mCanvas;
private Paint mPaint;
// How many frames per second did we get?
private long mFPS;
// The number of milliseconds in a second
private final int MILLIS_IN_SECOND = 1000;
// Holds the resolution of the screen
private int mScreenX;
private int mScreenY;
// How big will the text be?
private int mFontSize;
private int mFontMargin;
// The particle systems will be declared here later
// These will be used to make simple buttons

添加以下import代码:

import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.SurfaceHolder;

确保学*代码,然后我们可以讨论它。

我们使用在成员变量名之前添加m的命名约定。当我们在方法中添加局部变量时,这将有助于区分它们。

另外,请注意所有变量都声明为private。你可以愉快地删除所有private访问修饰符,代码仍然可以工作,但由于我们没有必要从这个类的外部访问任何这些变量,因此通过声明它们为private来保证它永远不会发生是明智的。

第一个成员变量是DEBUGGING。我们将其声明为final,因为我们不希望在应用程序执行期间更改其值。请注意,将其声明为final并不妨碍我们在希望在调试和非调试之间切换时手动更改其值。

我们声明的接下来的三个类的实例将处理屏幕上的绘图。请注意我突出显示的新类:

// These objects are needed to do the drawing
private SurfaceHolder mOurHolder;
private Canvas mCanvas;
private Paint mPaint;

SurfaceHolder类是必需的,以便进行绘图。它实际上是持有绘图表面的对象。当我们编写draw方法时,我们将看到它允许我们使用的方法来在屏幕上绘制。

接下来的两个变量让我们对实现*滑和一致的动画有了一些了解。再次列出如下:

// How many frames per second did we get?
private long mFPS;
// The number of milliseconds in a second
private final int MILLIS_IN_SECOND = 1000;

mFPS变量的类型是long,因为它将保存一个巨大的数字。计算机从 1970 年以来以毫秒为单位来测量时间 - 关于这一点,我们在谈论游戏循环时会详细讨论。但现在,我们需要知道,监控和测量每一帧动画的速度是确保粒子移动正常的关键。

第一个mFPS将在每一帧动画中重新初始化,大约每秒 60 次。它将被传递到每个粒子系统(每一帧动画)中,以便它知道经过了多少时间。

MILLIS_IN_SECOND变量初始化为1000。一秒钟确实有1000毫秒。我们将在计算中使用这个变量,因为它会使我们的代码比使用字面值1000更清晰。它声明为final,因为一秒钟的毫秒数显然永远不会改变。

我们刚刚添加的代码的下一部分为了方便起见再次显示如下:

// Holds the resolution of the screen
private int mScreenX;
private int mScreenY;
// How big will the text be?
private int mFontSize;
private int mFontMargin;

变量mScreenXmScreenY将保存屏幕的水*和垂直分辨率。请记住,它们是从LiveDrawingActivity传递到构造函数中的。

接下来的两个变量mFontSizemMarginSize将根据屏幕大小(以像素为单位)进行初始化,以保存像素值,使我们的文本格式整齐,并且比为每个文本位进行不断的计算更简洁。

现在我们可以开始在构造函数中初始化一些这些变量。

编写 LiveDrawingView 构造函数

将突出显示的代码添加到构造函数中。确保也学*代码,然后我们可以讨论它:

public LiveDrawingView(Context context, int x, int y) {
   // Super... calls the parent class
   // constructor of SurfaceView
   // provided by Android
   super(context);
   // Initialize these two members/fields
   // With the values passed in as parameters
   mScreenX = x;
   mScreenY = y;
   // Font is 5% (1/20th) of screen width
   mFontSize = mScreenX / 20;
   // Margin is 1.3% (1/75th) of screen width
   mFontMargin = mScreenX / 75;
   // getHolder is a method of SurfaceView
    mOurHolder = getHolder();
   mPaint = new Paint();
   // Initialize the two buttons
   // Initialize the particles and their systems
}

我们刚刚添加到构造函数的代码首先使用传递的参数值(xy)来初始化mScreenXmScreenY。我们的整个LiveDrawingView类现在可以在需要时访问屏幕分辨率。以下是这两行代码:

// Initialize these two members/fields
   // With the values passed in as parameters
   mScreenX = x;
   mScreenY = y;

接下来,我们将mFontSizemFontMargin初始化为屏幕宽度的像素分数。这些值有点随意,但它们有效,并且我们将使用这些变量的各种倍数来使文本在屏幕上整齐对齐。以下是我所指的两行代码:

   // Font is 5% (1/20th) of screen width
   mFontSize = mScreenX / 20;
   // Margin is 1.3% (1/75th) of screen width
   mFontMargin = mScreenX / 75;

接下来,我们初始化了我们的PaintSurfaceHolder对象。Paint使用了默认构造函数,就像我们之前做过的那样,但mHolder使用了getHolder方法,这是SurfaceView类的一个方法。getHolder方法返回一个初始化为mHolder的引用,所以mHolder现在就是那个引用。简而言之,mHolder现在已经准备好使用了。我们可以访问这个方便的方法,因为LiveDrawingView是一个SurfaceView

   // getHolder is a method of SurfaceView
   mOurHolder = getHolder();
   mPaint = new Paint();

在我们像以前一样使用PaintCanvas类之前,我们需要在draw方法中做更多的准备工作。我们很快就会看到具体是什么。请注意注释,指示我们最终将初始化粒子系统以及两个控制按钮的位置。

让我们准备好开始绘制。

编写draw方法

在构造方法之后立即添加下面显示的draw方法。代码中会有一些错误。我们将处理它们,然后我们将详细介绍draw方法在SurfaceView类中的工作原理,因为其中有一些看起来完全陌生的代码,以及一些熟悉的代码。这是要添加的代码:

// Draw the particle systems and the HUD
private void draw() {
if (mOurHolder.getSurface().isValid()) {
// Lock the canvas (graphics memory) ready to draw
         mCanvas = mOurHolder.lockCanvas();
         // Fill the screen with a solid color
         mCanvas.drawColor(Color.argb(255, 0, 0, 0));
         // Choose a color to paint with
         mPaint.setColor(Color.argb(255, 255, 255, 255));
         // Choose the font size
         mPaint.setTextSize(mFontSize);
         // Draw the particle systems
         // Draw the buttons
         // Draw the HUD
         if(DEBUGGING){
                printDebuggingText();
         }
          // Display the drawing on screen
         // unlockCanvasAndPost is a method of
         SurfaceHolder
         mOurHolder.unlockCanvasAndPost(mCanvas);
    }
}

我们有两个错误。一个是Color类需要导入。您可以按照通常的方式修复这个问题,或者手动添加下一行代码。无论您选择哪种方法,以下额外的行需要添加到文件顶部的代码中:

import android.graphics.Color;

让我们处理另一个错误。

添加printDebuggingText方法

第二个错误是调用printDebuggingText。这个方法还不存在。让我们现在添加它。

draw方法之后添加以下代码:

private void printDebuggingText(){
   int debugSize = mFontSize / 2;
   int debugStart = 150;
   mPaint.setTextSize(debugSize);
   mCanvas.drawText("FPS: " + mFPS ,
         10, debugStart + debugSize, mPaint);
   // We will add more code here in the next chapter
}

先前的代码使用局部变量debugSize来保存成员变量mFontSize的一半。这意味着mFontSize(用于 HUD)是根据屏幕分辨率动态初始化的,debugSize始终是它的一半。然后在开始绘制文本之前,使用debugSize变量设置字体的大小。debugStart变量只是一个垂直开始打印调试文本的好位置的猜测。

然后使用这两个值来定位屏幕上显示当前每秒帧数的一行文本。由于这个方法是从draw中调用的,而draw又将从游戏循环中调用,所以这行文本将每秒刷新 60 次。

注意

在非常高或非常低分辨率的屏幕上,您可能需要尝试不同的文本大小,以找到更适合您屏幕的大小。

让我们来探索draw方法中的这些新代码,以及我们如何可以使用SurfaceView来处理所有的绘图需求,LiveDrawingView类是从SurfaceView派生出来的。

理解draw方法和SurfaceView

从方法的中间开始,逐渐向外工作,我们有一些熟悉的东西,比如调用drawColorsetTextSizedrawText方法。我们还可以看到注释,指示我们最终将添加代码来绘制粒子系统和 HUD:

  • drawColor代码用纯色清除屏幕。

  • setTextSize方法设置了绘制 HUD 的文本大小。

  • 一旦我们更深入地探索了粒子系统,我们将编写绘制 HUD 的代码。我们将让玩家知道他们的绘图包括多少个粒子和系统。

然而,完全新的是draw方法的开头的代码。这里是它:

if (mOurHolder.getSurface().isValid()) {
// Lock the canvas (graphics memory) ready to draw
mCanvas = mOurHolder.lockCanvas(); 
…
…

if语句包含对getSurface的调用,并将其与isValid的调用链接在一起。如果这行返回true,则确认我们要操作的内存区域以表示我们的绘图帧是可用的,代码将继续在if语句内部执行。

这些方法内部发生了什么(特别是第一个方法)是非常复杂的。它们是必需的,因为我们所有的绘制和其他处理(比如移动对象)都将与检测用户输入的代码和监听操作系统的消息异步进行。这在之前的项目中并不是问题,因为我们的代码只是绘制了一个帧然后等待。

现在我们希望每秒执行 60 次代码,我们需要确认我们可以访问内存 - 在访问之前。

这引发了更多关于这段代码如何异步运行的问题。这将在我们不久后讨论线程时得到解答。现在,只需知道这行代码检查我们的代码的某个其他部分或 Android 本身是否正在使用所需的内存部分。如果空闲,那么 if 语句内的代码将执行。

此外,在 if 语句内执行的第一行代码调用了 lockCanvas 方法,以便如果另一个应用程序或 Android 尝试在我们的代码访问内存时访问它,它将无法访问。然后我们进行所有的绘制。

最后,在 draw 方法中,最后有这样一行代码(以及注释):

// Display the drawing on screen
// unlockCanvasAndPost is a method of SurfaceHolder
mOurHolder.unlockCanvasAndPost(mCanvas);

unlockCanvasAndPost 方法将我们新装饰的 Canvas 对象 (mCanvas) 发送到屏幕上进行绘制,并释放锁定,以便代码的其他部分可以再次使用它 - 尽管在整个过程重新开始之前只是非常短暂的时间。这个过程在每一帧动画中都会发生。

我们现在理解了 draw 方法中的代码;然而,我们仍然没有调用 draw 方法的机制。事实上,我们甚至没有调用一次 draw 方法。我们需要讨论游戏循环和线程。

游戏循环

游戏循环到底是什么?几乎每个实时绘图/图形游戏都有一个游戏循环。即使你可能怀疑没有游戏循环的游戏,比如回合制游戏,仍然需要将玩家输入与绘图和人工智能同步,同时遵循底层操作系统的规则。

需要不断更新应用中的对象,也许是通过移动它们,在同时响应用户输入的同时绘制所有对象的当前位置。一个图表可能会有所帮助:

图 21.2 – 游戏循环

图 21.2 – 游戏循环

我们的游戏循环包括三个主要阶段:

  1. 通过移动它们、检测碰撞和处理粒子移动和状态变化等方式更新所有游戏/绘图对象。

  2. 基于刚刚更新的数据,绘制动画帧的最新状态。

  3. 响应用户的屏幕触摸。

我们已经有一个用于处理循环的 draw 方法。这表明我们也将有一个方法来进行所有的更新。我们很快将编写一个 update 方法的大纲。此外,我们知道我们可以响应屏幕触摸,尽管我们需要稍微调整之前所有项目的代码,因为我们不再在 Activity 中工作,也不再在布局中使用传统的 UI 小部件。

还有一个问题,就是(我简要提到过的)所有的更新和绘制都是异步进行的,与屏幕触摸的检测和操作系统的监听是分开的。

注意

只是为了明确,异步意味着它不会同时发生。我们的游戏代码将通过与 Android 和用户界面共享执行时间来运行。CPU 将在我们的代码和 Android/用户输入之间非常快速地来回切换。

但是这三个阶段究竟如何循环执行?我们将如何编写这个异步系统,其中可以调用 updatedraw 方法,以及如何使循环以正确的速度(帧率)运行?

正如我们可能猜到的那样,编写一个高效的游戏循环并不像一个 while 循环那样简单。

注意

然而,我们的游戏循环也将包含一个 while 循环。

我们需要考虑时机,开始和结束循环,以及不要让操作系统变得无响应,因为我们正在垄断整个 CPU 在我们的循环中。

但是我们何时以及如何调用我们的draw方法?我们如何测量和跟踪帧速率?考虑到这些问题,我们完成的游戏循环可能可以更好地用下一个图表表示。注意引入了线程的概念。

图 21.3 - 完成的游戏循环

图 21.3 - 完成的游戏循环

现在我们知道我们想要实现什么,让我们学*一下线程。

线程

那么,什么是线程?在编程中,你可以把线程想象成故事中的线索一样。在故事的一个线索中,我们可能有主要角色在前线与敌人作战,而在另一个线索中,士兵的家人正在日复一日地生活。当然,一个故事不一定只有两个线索;我们可以引入第三个线索。也许故事还讲述了政治家和军事指挥官做出决策。这些决策会微妙地或者不那么微妙地影响其他线索中发生的事情。

编程线程就像这样。我们在程序中创*部分/线程来控制不同的方面。在 Android 中,当我们需要确保一个任务不会干扰应用程序的主(UI)线程,或者当我们有一个需要很长时间才能完成并且不能中断主线程执行的后台任务时,线程尤其有用。我们引入线程来代表这些不同的方面,因为有以下原因:

  • 从组织的角度来看,它们是有意义的。

  • 它们是一种被证明有效的程序结构方式。

  • 我们所工作的系统的性质迫使我们使用它们。

在 Android 中,我们同时出于这三个原因使用线程。这是有意义的,它有效,而且我们必须这样做,因为 Android 系统的设计要求如此。

通常,我们在不知情的情况下使用线程。这是因为我们使用的类会代表我们使用线程。我们在第十九章中编写的所有动画,动画和插值,都在线程中运行。在 Android 中的另一个例子是SoundPool类,它在一个线程中加载声音。我们将在第二十三章**,支持不同版本的 Android,声音效果和 Spinner 小部件中看到或听到SoundPool的实际效果,我们已经看到并将再次看到,我们的代码不必处理我们即将学*的线程方面,因为这一切都由类内部处理。然而,在这个项目中,我们需要更深入地参与其中。

在实时系统中,考虑一个线程同时接收玩家的按钮点击以左右移动,同时监听来自操作系统的消息,比如调用onCreate(以及我们即将看到的其他方法)作为一个线程,另一个线程负责绘制所有图形并计算所有移动。

线程问题

具有多个线程的程序可能会出现问题。就像故事中的线索一样,如果没有适当的同步,事情可能会出错。如果我们的士兵在战斗甚至战争开始之前就进入了战斗,那会怎么样?很奇怪。

假设我们有一个变量int x,它代表程序中三个线程使用的关键数据。如果一个线程稍微超前于自己,并使数据对其他两个线程来说是“错误”的,会发生什么。这个问题是由多个线程竞争完成而引起的正确性问题,因为它们毕竟只是愚蠢的代码。

正确性问题可以通过对线程和锁定的密切监督来解决。锁定意味着暂时阻止一个线程的执行,以确保事情以同步的方式工作 - 就像冻结士兵登上战舰直到船靠岸并放下跳板,避免尴尬的溅水。

多线程程序的另一个问题是int x的问题,但那一刻从未到来,最终整个程序都停滞了。

您可能已经注意到,第一个问题(正确性)的解决方案是第二个问题(死锁)的原因。

幸运的是,这个问题已经为我们解决了。就像我们使用Activity类并重写onCreate来知道何时需要创*我们的应用程序一样,我们也可以使用其他类来创*和管理我们的线程。就像Activity一样,我们只需要知道如何使用它们 - 而不需要知道它们的工作原理。

那么,当你不需要知道时,我为什么告诉你所有这些关于线程的东西,你是正确的。只是因为我们将编写看起来不同并且以不熟悉的方式结构化的代码。如果我们能做到以下几点,那么我们将毫不费力地编写我们的 Java 代码来创*和在我们的线程中工作:

  • 理解线程的一般概念,这只是一个故事线程的同义词,几乎同时发生

  • 学*使用线程的几个规则

有一些不同的 Android 类处理线程。不同的线程类在不同情况下效果最佳。

我们需要记住的是,我们将编写几乎同时运行的程序的部分。

注意

你说的“几乎”是什么意思?发生的情况是 CPU 在线程之间切换,但这几乎是同时/异步发生的。然而,这发生得如此之快,以至于我们除了同时性/同步性之外无法感知任何东西。当然,在故事线程的类比中,人们确实是完全同步行动的。

让我们来看看我们的线程代码将是什么样子。现在先不要向项目添加任何代码。我们可以这样声明一个Thread类型的对象:

Thread ourThread;

初始化并启动它:

ourThread = new Thread(this);
ourThread.start();

这个线程的问题还有一个谜团。看看初始化线程的构造函数。这是代码的一行,以方便您再次查看:

ourThread = new Thread(this);

看一下传递给构造函数的突出参数。我们传入this。请记住,代码将进入LiveDrawingView类,而不是LiveDrawingActivity。因此,我们可以推断this是对LiveDrawingView类的引用(它扩展了SurfaceView)。

在 Android 总部的书呆子编写Thread类时,他们似乎很难想象有一天我们会编写我们的LiveDrawingView类。那么这怎么可能呢?

Thread类需要完全不同的类型传递到它的构造函数中。Thread构造函数需要一个Runnable类型的对象。

注意

您可以通过查看 Android 开发者网站上的Thread类来确认这一事实:developer.android.com/reference/java/lang/Thread.html#Thread(java.lang.Runnable

您还记得我们在第十一章中谈到的接口吗,更多面向对象的编程?作为提醒,我们可以使用implements关键字和类声明后面的接口名称来实现接口,就像在这段代码中一样:

class someClass extends someotherClass implements Runnable{

然后我们必须实现接口的抽象方法。Runnable只有一个方法。就是run方法。

注意

您可以通过查看 Android 开发者网站上的Runnable接口来确认这一事实:developer.android.com/reference/java/lang/Runnable.html

然后我们可以使用 Java 的@override关键字来改变操作系统允许我们的线程对象运行其代码时发生的情况:

class someClass extends someotherClass implements Runnable{
   @override
run(){
         // Anything in here executes in a thread
         // No skill needed on our part
         // It is all handled by Android, the Thread class
         // and the Runnable interface
}
}

在重写的run方法中,我们将调用两个方法:一个是我们已经启动的draw,另一个是updateupdate方法是我们所有的计算和人工智能的地方。代码看起来会有点像这样——暂时不要添加:

@override
public void run() {

    // Update the drawing based on
    // user input, physics,
    // collision detection and artificial intelligence
    update();

    // Draw all the particle systems in their updated 
    locations
    draw();

}

在适当的时候,我们也可以像这样停止我们的线程:

ourThread.join();

现在run方法中的所有内容都在一个单独的线程中执行,使默认或 UI 线程监听触摸和系统事件。我们很快将看到这两个线程如何相互通信在绘图项目中。

请注意,所有这些代码的确切位置将进入我们的应用程序尚未解释,因为在真实项目中向您展示会更容易得多。

使用线程实现游戏循环

现在我们已经学*了游戏循环和线程,我们可以把它们全部放在一起,在 Living Drawing 项目中实现我们的游戏循环。

我们将添加整个游戏循环的代码,包括在LiveDrawingActivity类中编写两个方法来启动和停止控制循环的线程。

注意

你能猜到基于 Activity 的类将如何在LiveDrawingView类中启动和停止线程吗?

实现 Runnable 并提供 run 方法

通过实现Runnable来更新类声明,就像我们之前讨论过的那样,并且如下面的下一个突出显示的代码所示:

class LiveDrawingView extends SurfaceView implements Runnable{

注意到我们的代码中出现了一个新错误。将鼠标指针悬停在Runnable一词上,您将看到一条消息,告诉您我们需要再次实现run方法,就像我们在讨论接口时讨论过的那样。按照一会儿的示例添加空的run方法,包括@override标签。

如果它在LiveDrawingView类的大括号内而不是在另一个方法内,那么添加的位置并不重要。我把我的添加在构造方法之后,因为它靠近顶部,很容易找到。在本章中我们将对其进行相当多的编辑。按照下面的示例添加空的run方法:

// When we start the thread with:
// mThread.start();
// the run method is continuously called by Android
// because we implemented the Runnable interface
// Calling mThread.join();
// will stop the thread
@Override
public void run() {
}

错误已经消失,现在我们可以声明和初始化一个Thread对象。

编写线程

LiveDrawingView类中的所有其他成员下面,声明一些更多的成员变量和实例,如下所示:

// Here is the Thread and two control variables
private Thread mThread = null;
// This volatile variable can be accessed
// from inside and outside the thread
private volatile boolean mDrawing;
private boolean mPaused = true;

现在我们可以启动和停止线程。想一想我们可能在哪里做到这一点。记住应用程序需要响应操作系统启动和停止应用程序。

启动和停止线程

现在我们需要启动和停止线程。我们已经看到了我们需要的代码,但是什么时候和在哪里应该这样做呢?让我们编写两个方法,一个用于启动,一个用于停止,然后我们可以进一步考虑何时以及从何处调用这些方法。在LiveDrawingView类中添加这两个方法。如果它们的名称听起来很熟悉,那并非偶然:

// This method is called by LiveDrawingActivity
// when the user quits the app
public void pause() {
   // Set mDrawing to false
   // Stopping the thread isn't
   // always instant
   mDrawing = false;
   try {
         // Stop the thread
         mThread.join();
   } catch (InterruptedException e) {
         Log.e("Error:", "joining thread");
   }
}
// This method is called by LiveDrawingActivity
// when the player starts the app
public void resume() {
   mDrawing = true;
   // Initialize the instance of Thread
   mThread = new Thread(this);
   // Start the thread
   mThread.start();
}

正在发生的事情在评论中稍微透露了一些——你读了评论吗?我们现在有了pauseresume方法,它们使用我们之前讨论过的相同代码来停止和启动Thread对象。

注意新方法是public的,因此可以从类外部访问到任何具有LiveDrawingView实例的其他类。记住LiveDrawingActivity有完全声明和初始化的LiveDrawingView实例吗?

让我们使用 Android Activity 生命周期来调用这两个新方法。

使用 Activity 生命周期来启动和停止线程

更新LiveDrawingActivity中重写的onResumeonPause方法,如下所示,带有突出显示的代码行:

@Override
protected void onResume() {
   super.onResume();
   // More code here later in the chapter
   mLiveDrawingView.resume();
}
@Override
protected void onPause() {
   super.onPause();
   // More code here later in the chapter
   mLiveDrawingView.pause();
}

现在我们的线程将在操作系统恢复和暂停我们的应用程序时启动和停止。请记住,onResume在应用程序创*后第一次调用onCreate之后被调用,而不仅仅是在从暂停中恢复后调用。onResumeonPause中的代码使用mLiveDrawingView对象来调用其resumepause方法,这反过来又有代码来启动和停止线程。然后,这段代码触发线程的run方法执行。就是在这个run方法(在LiveDrawingView中)中,我们将编写我们的游戏循环。现在让我们来做这个。

编写 run 方法

虽然我们的线程已经设置好并准备就绪,但因为run方法是空的,所以什么也不会发生。按照下面所示的方式编写run方法:

@Override
public void run() {
   // mDrawing gives us finer control
   // rather than just relying on the calls to run
   // mDrawing must be true AND
   // the thread running for the main 
// loop to execute
   while (mDrawing) {
         // What time is it now at the start of the loop?
         long frameStartTime = System.currentTimeMillis();
         // Provided the app isn't paused
         // call the update method
         if(!mPaused){
                update();
                // Now the particles are in 
                // their new positions

         }
         // The movement has been handled and now 
         // we can draw the scene.
         draw();
         // How long did this frame/loop take?
         // Store the answer in timeThisFrame
         long timeThisFrame = 
                System.currentTimeMillis() - 
                frameStartTime;
         // Make sure timeThisFrame is at least 1 
         // millisecond because accidentally dividing 
         // by zero crashes the app
         if (timeThisFrame > 0) {
                // Store the current frame rate in mFPS
                // ready to pass to the update methods of
                // of our particles in the next frame/loop
                mFPS = MILLIS_IN_SECOND / timeThisFrame;
         }
   }
}

请注意,Android Studio 中有两个错误。这是因为我们还没有编写update方法。让我们快速添加一个空方法(带有注释)。我在run方法之后添加了我的:

private void update() {
   // Update the particles
}

现在让我们详细讨论run方法中的代码如何通过逐步查看整个内容来实现我们游戏循环的目标。

这个第一部分初始化了一个while循环,条件是mDrawing,它包裹在run中的其余代码中,所以线程需要被启动(才能调用run),并且mDrawing需要为true才能执行while循环:

@Override
public void run() {
   // mPlaying gives us finer control
   // rather than just relying on the calls to run
   // mPlaying must be true AND
   // the thread running for the main 
   // loop to execute
   while (mPlaying) {

while循环内的第一行代码声明并初始化一个名为frameStartTime的局部变量,其值为当前时间。System类的静态方法currentTimeMillis返回这个值。如果以后我们想要测量一帧花费了多长时间,那么我们需要知道它是什么时候开始的:

   // What time is it now at the start of the loop?
   long frameStartTime = System.currentTimeMillis();

接下来,在while循环中,我们检查应用程序是否暂停,只有当应用程序没有暂停时,才会执行下面的代码。如果逻辑允许在这个块中执行,那么就会调用update

         // Provided the app isn't paused
         // call the update method
         if(!mPaused){
                update();
                // Now the particles are in 
                // their new positions
         }

在前面的if语句之外,调用draw方法来绘制所有对象的刚更新的位置。此时,另一个局部变量被声明并初始化为完成整个帧(更新和绘制)所花费的时间长度。这个值是通过再次使用currentTimeMillis获取当前时间,并从中减去frameStartTime来计算的:

         // The movement has been handled and collisions
         // detected now we can draw the scene.
         draw();
         // How long did this frame/loop take?
         // Store the answer in timeThisFrame
         long timeThisFrame = 
                System.currentTimeMillis() - 
                frameStartTime;

下一个if语句检测timeThisFrame是否大于零。如果线程在对象初始化之前运行,该值可能为零。如果你看一下if语句中的代码,它通过将经过的时间除以MILLIS_IN_SECOND来计算帧速率。如果除以零,应用程序将崩溃,这就是我们进行检查的原因。

一旦mFPS得到分配给它的值,我们就可以在下一帧中使用它传递给所有粒子的update方法,我们将在下一章中编写。它们将使用这个值来确保它们根据其目标速度和帧所花费的时间长度精确地移动:

         // Make sure timeThisFrame is at least 1 
         // millisecond because accidentally dividing 
         // by zero crashes the app
         if (timeThisFrame > 0) {
                // Store the current frame rate in mFPS
                // ready to pass to the update methods of
                // the particles in the next frame/loop
                mFPS = MILLIS_IN_SECOND / timeThisFrame;
         }
   }
}

在每一帧中初始化mFPS的计算结果是,mFPS将保存 1 的分数。因此,当我们在每个粒子对象中使用这个值时,我们将能够使用这个计算:

mSpeed / mFPS 

为了确定任何给定帧的经过时间,由于帧速率波动,mFPS将保存不同的值,并为游戏对象提供适当的数字来计算每次移动。

运行应用程序

在 Android Studio 中点击播放按钮,最后两章的辛勤工作和理论将会生动地展现出来。这是我们的应用程序在*板模拟器上运行的开端:

图 21.4 - 运行应用程序

图 21.4 - 运行应用程序

你可以看到我们现在已经创*了一个实时系统,具有我们的游戏循环和一个线程。如果你在真实设备上运行这个应用程序,你将很容易地在这个阶段实现每秒 60 帧。

总结

这可能是迄今为止最技术性的一章。线程、游戏循环、计时、使用接口以及 Activity 生命周期等等……这是一个非常长的主题清单。

如果这些事物之间的确切相互关系并不完全清楚,那也没关系。你只需要知道,当用户启动和停止应用程序时,LiveDrawingActivity类将通过调用LiveDrawingView类的pauseresume方法来处理启动和停止线程。它通过重写的onPauseonResume方法来实现,这些方法由操作系统调用。

一旦线程运行起来,run方法内的代码将与监听用户输入的 UI 线程一起执行。当我们同时从run方法调用updatedraw方法,并跟踪每帧所需的时间时,我们的应用程序就准备好了。

我们只需要允许用户向他们的艺术品添加一些粒子,然后我们可以在每次调用update方法时更新它们,并在每次调用draw方法时绘制它们。

在下一章中,我们将编写、更新和绘制ParticleParticleSystem类。此外,我们将编写用户与应用程序进行交互(进行一些绘图)的代码。

第二十二章:粒子系统和处理屏幕触摸

我们已经在上一章中使用线程实现了实时系统。在本章中,我们将创*将存在并在这个实时系统中发展的实体,就好像它们有自己的思想一样;它们将形成用户可以实现的绘图的外观。

我们还将看到用户如何通过学*如何响应与屏幕的交互来实现这些实体。这与在 UI 布局中与小部件交互是不同的。

以下是本章即将涉及的内容:

  • 向屏幕添加自定义按钮

  • 编写Particle

  • 编写ParticleSystem

  • 处理屏幕触摸

  • Android Studio Profiler 工具

我们将首先为我们的应用程序添加自定义 UI。

警告

这个应用程序产生明亮的闪烁颜色。这可能会引起光敏性癫痫的人不适或癫痫发作。请谨慎阅读。您可能只想阅读这个项目的理论,而不运行完成的项目。

技术要求

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

向屏幕添加自定义按钮

我们需要让用户控制何时开始另一次绘制,并清除他们以前的作品。我们需要让用户能够决定何时以及何时将绘图带到生活中。为了实现这一点,我们将在屏幕上添加两个按钮,每个任务一个按钮。

LiveDrawingView类中添加下面突出显示的成员:

// These will be used to make simple buttons
private RectF mResetButton;
private RectF mTogglePauseButton;

我们现在有两个RectF实例。这些对象每个都包含四个浮点坐标,每个按钮的每个角落一个坐标。

LiveDrawingView的构造函数中初始化位置:

// Initialize the two buttons
mResetButton = new RectF(0, 0, 100, 100);
mTogglePauseButton = new RectF(0, 150, 100, 250);

添加RectF类的import

import android.graphics.RectF;

现在我们已经为按钮添加了实际的坐标。如果您在屏幕上可视化坐标,您会看到它们位于左上角,暂停按钮就在重置/清除按钮的下方。

现在我们可以绘制按钮。在LiveDrawingView类的draw方法中添加以下两行代码:

// Draw the buttons
mCanvas.drawRect(mResetButton, mPaint);
mCanvas.drawRect(mTogglePauseButton, mPaint);

新代码使用了drawRect方法的重写版本,我们只需将两个RectF实例与通常的Paint实例一起传递进去。我们的按钮现在将被绘制到屏幕上。

我们将在本章后面看到如何与这些略显粗糙的按钮进行交互。

实现粒子系统效果

粒子系统是控制粒子的系统。在我们的情况下,ParticleSystem是一个我们将编写的类,它将生成Particle类的实例(许多实例),从而创*一个简单的爆炸效果。

这是一张由粒子系统控制的一些粒子的图像:

图 22.1 - 粒子系统效果

图 22.1 - 粒子系统效果

为了澄清,每个彩色方块都是Particle类的一个实例,所有Particle实例都由ParticleSystem类控制和持有。此外,用户将通过用手指绘制来创*多个(数百个)ParticleSystem实例。粒子将显示为点或块,直到用户点击暂停按钮时才会活跃起来。我们将仔细检查代码,以便您能够在代码中设置ParticleParticleSystem实例的大小、颜色、速度和数量。

注意

读者可以在屏幕上添加额外的按钮,以允许用户更改这些属性作为应用程序的一个特性。

我们将从编写Particle类开始。

编写Particle

按照以下代码中所示的import语句、成员变量和构造方法添加:

import android.graphics.PointF;
class Particle {
    PointF mVelocity;
    PointF mPosition;
    Particle(PointF direction)
    {
        mVelocity = new PointF();
        mPosition = new PointF();
        // Determine the direction
        mVelocity.x = direction.x;
        mVelocity.y = direction.y;
    }
}

我们有两个成员变量:一个用于速度,一个用于位置。它们都是PointF对象。PointF包含两个浮点值。位置很简单;它只是一个水*和垂直值。速度值值得更详细解释。PointF中的两个值将是速度,一个是水*的,另一个是垂直的。这两个速度的组合将意味着一个方向。

注意

在构造函数中,两个新的PointF对象被实例化,并且mVeleocityxy值被初始化为由PointF direction参数传入的值。注意值是如何从direction复制到mVelocity的。现在,PointF mVelocity不是作为参数传入的PointF的引用。每个Particle实例将从direction复制值(对于每个实例它们将是不同的),但mVelocitydirection没有持久的连接。

接下来,添加以下三种方法,然后我们可以讨论它们:

void update(float fps)
{
   // Move the particle
   mPosition.x += mVelocity.x;
   mPosition.y += mVelocity.y;
}
void setPosition(PointF position)
{
   mPosition.x = position.x;
   mPosition.y = position.y;
}
PointF getPosition()
{
   return mPosition;
}

也许并不奇怪,有一个update方法。Particle实例的update方法将由ParticleSystem类的update方法在应用程序的每一帧调用,而ParticleSystem类的update方法将由LiveDrawingView类(在update方法中)调用,我们将在本章后面编写。

update方法中,使用mVelocity的相应值更新mPosition的水*和垂直值。

注意

请注意,我们在更新中没有使用当前帧速率。如果您想确保粒子以完全正确的速度飞行,可以修改这一点。但是所有速度都将是随机的。增加这个额外的计算并没有太多好处(对于每个粒子)。然而,正如我们很快将看到的,ParticleSystem类将需要考虑当前每秒帧数,以测量它应该运行多长时间。

接下来,我们编写了setPosition方法。请注意,该方法接收PointF,用于设置初始位置。ParticleSystem类将在触发效果时传递此位置。

最后,我们有getPosition方法。我们需要这个方法,以便ParticleSystem类可以在正确的位置绘制所有粒子。我们本可以在Particle类中添加一个draw方法,而不是getPosition方法,并让Particle类自己绘制。在这个实现中,两种选项都没有特别的好处。

现在我们可以继续ParticleSysytem类。

编写ParticleSystem类。

ParticleSystem类比Particle类有更多的细节,但仍然相当简单。记住我们需要用这个类实现的目标:保存、生成、更新和绘制一堆(相当大的一堆)Particle实例。

添加以下成员和import语句:

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import java.util.ArrayList;
import java.util.Random;
class ParticleSystem {
    private float mDuration;
    private ArrayList<Particle> mParticles;
    private Random random = new Random();
    boolean mIsRunning = false;

}

我们有四个成员变量:首先,一个名为mDurationfloat变量,它将被初始化为我们希望效果运行的秒数。名为mParticlesArrayList实例保存Particle实例,并将保存我们实例化的所有Particle对象。

称为randomRandom实例被创*为成员,因为我们需要生成如此多的随机值,每次创*一个新对象都会减慢速度。

最后,mIsRunning布尔值将跟踪粒子系统当前是否正在显示(更新和绘制)。

现在我们可以编写init方法。每当我们想要一个新的ParticleSystem时,将调用此方法。请注意,唯一的参数是一个名为numParticlesint参数。

当我们调用init时,我们可以有一些乐趣初始化疯狂数量的粒子。添加init方法,然后我们将更仔细地查看代码:

void init(int numParticles){
   mParticles = new ArrayList<>();
   // Create the particles
   for (int i = 0; i < numParticles; i++){
         float angle = (random.nextInt(360)) ;
         angle = angle * 3.14f / 180.f;
         // Option 1 - Slow particles
         //float speed = (random.nextFloat()/10);
         // Option 2 - Fast particles
         float speed = (random.nextInt(10)+1);
         PointF direction;
         direction = new PointF((float)Math.cos(angle) * 
                     speed, (float)Math.sin(angle) * 
                     speed);
         mParticles.add(new Particle(direction));
   }
}

init方法只包括一个for循环,完成所有工作。for循环从零到numParticles-1运行。

首先,生成一个介于零和 359 之间的随机数,并存储在名为anglefloat变量中。接下来,进行一些数学运算,将angle乘以3.14/180。这将角度从度转换为弧度测量,这是Math类在稍后将要使用的。

然后我们生成另一个 1 到 10 之间的随机数,并将结果赋给一个名为speedfloat变量。

注意

我已经添加了注释,*议在代码的这部分中使用不同的值。我在ParticleSystem类的几个地方都这样做了,当我们到达章节的末尾时,我们将乐趣地改变这些值,看看对绘图应用有什么影响。

现在我们有了一个随机角度和速度,我们可以将它们转换并组合成一个矢量,该矢量可以在Particle类的update方法中使用,以更新其每帧的位置。

注意

矢量是一个确定方向和速度的值。我们的矢量存储在direction对象中,直到传递到Particle构造函数中。矢量可以是多维的。我们的矢量是二维的,因此定义了 0 到 359 度之间的航向和 1 到 10 之间的速度。您可以在我的网站上阅读更多关于矢量、航向、正弦和余弦的内容:gamecodeschool.com/essentials/calculating-heading-in-2d-games-using-trigonometric-functions-part-1/

使用Math.sinMath.cos创*矢量的单行代码,我决定不完全解释,因为其中的魔法部分在以下公式中发生:

  • 角度的余弦 * speed

  • 角度的正弦 * speed

这也在Math类提供的余弦和正弦函数的隐藏计算中部分发生。如果您想了解它们的全部细节,请参阅前面的提示框。

最后,创*一个新的Particle,然后将其添加到mParticles ArrayList实例中。

接下来,我们将编写update方法。请注意,update方法确实需要当前帧速率作为参数。编写如下所示的update方法:

void update(long fps){
   mDuration -= (1f/fps);
   for(Particle p : mParticles){
         p.update(fps);
   }
   if (mDuration < 0)
   {
         mIsRunning = false;
   }
}

update方法内部发生的第一件事是减去mDuration的经过时间。请记住,fps参数是每秒帧数,所以1/fps会给出一个作为秒的分数值。

接下来是一个增强的for循环,调用mParticles ArrayList实例中每个Particle实例的update方法。

最后,代码检查粒子效果是否已经完成,使用if(mDuration < 0),如果是,则将mIsRunning设置为false

现在我们可以编写emitParticles方法,它将使每个Particle实例运行。这不应与init混淆,后者创*所有新的粒子并赋予它们速度。init方法将在用户开始交互之前调用一次,而emitParticles方法将在每次需要启动效果时调用,用户在屏幕上绘制时。

添加emitParticles方法:

void emitParticles(PointF startPosition){
   mIsRunning = true;
   // Option 1 - System lasts for half a minute
   //mDuration = 30f;
   // Option 2 - System lasts for 2 seconds
   mDuration = 3f;
   for(Particle p : mParticles){
         p.setPosition(startPosition);
   }
}

首先,请注意将所有粒子的起始位置作为参数传递给PointF引用。所有粒子将从完全相同的位置开始,然后根据它们各自的速度每帧扩散。

mIsRunning布尔值设置为truemDuration设置为1f,所以效果将持续一秒,增强的for循环调用setPosition来移动每个粒子到起始坐标。

我们ParticleSysytem类的最后一个方法是draw方法,它将展示效果的全部荣耀。该方法接收一个Canvas实例和一个Paint实例的引用,因此它可以在LiveDrawingView类的draw方法中锁定的相同画布上绘制。

添加draw方法:

void draw(Canvas canvas, Paint paint){
         for (Particle p : mParticles) {
                // Option 1 - Coloured particles
                //paint.setARGB(255, random.nextInt(256),
                            //random.nextInt(256),
                            //random.nextInt(256));
                // Option 2 - White particles
                paint.setColor(
                Color.argb(255,255,255,255));
                // How big is each particle?
                float sizeX = 0;
                float sizeY = 0;
                // Option 1 - Big particles
                //sizeX = 25;
                //sizeY = 25;
                // Option 2 - Medium particles
                sizeX = 10;
                sizeY = 10;
                // Option 3 - Tiny particles
                //sizeX = 1;
                //sizeY = 1;
                // Draw the particle
                // Option 1 - Square particles
                //canvas.drawRect(p.getPosition().x, 
                            //p.getPosition().y,
                            //p.getPosition().x + sizeX,
                            //p.getPosition().y + sizeY,
                            //paint);
                // Option 2 - Circle particles
                canvas.drawCircle(p.getPosition().x, 
                            p.getPosition().y,
                            sizeX, paint);
         }
}

增强的for循环遍历mParticles ArrayList实例中的每个Particle实例。依次使用drawRect方法和getPosition方法绘制每个Particle。请注意调用paint.setARGB方法。您将看到我们随机生成每个颜色通道。

注意

请注意评论中我*议了不同的代码更改选项,这样在完成编码后我们就可以玩得更开心。

我们现在可以开始让粒子系统工作了。

LiveDrawingView类中生成粒子系统

添加一个充满系统的ArrayList实例和一些其他成员来跟踪事物。在现有注释所指示的位置添加突出显示的代码:

// The particle systems will be declared here later
private ArrayList<ParticleSystem> 
          mParticleSystems = new ArrayList<>();
private int mNextSystem = 0;
private final int MAX_SYSTEMS = 1000;
private int mParticlesPerSystem = 100;

ArrayList类导入如下:

import java.util.ArrayList;

现在我们可以跟踪多达 1000 个每个系统中有 100 个粒子的粒子系统。随意尝试调整这些数字。

注意

在现代设备上,您可以运行数百万个粒子而不会遇到任何问题,但在模拟器上,处理数十万个粒子就会有些吃力。

通过添加以下突出显示的代码在构造函数中初始化系统:

// Initialize the particles and their systems
for (int i = 0; i < MAX_SYSTEMS; i++) {
   mParticleSystems.add(new ParticleSystem());
   mParticleSystems.get(i).init(mParticlesPerSystem);
}

该代码循环遍历ArrayList实例,对每个ParticleSystem实例调用构造函数,然后调用init方法。

通过在update方法中添加以下突出显示的代码,为循环的每一帧更新系统:

private void update() {
   // Update the particles
   for (int i = 0; i < mParticleSystems.size(); i++) {
          if (mParticleSystems.get(i).mIsRunning) {
                 mParticleSystems.get(i).update(mFPS);
          }
   }
}

前面的代码循环遍历每个ParticleSystem实例,首先检查它们是否活动,然后调用update方法并传入当前的每秒帧数。

通过将此突出显示的代码添加到draw方法中,为循环的每一帧绘制系统:

// Choose a color to paint with
mPaint.setColor(Color.argb(255, 255, 255, 255));
// Choose the font size
mPaint.setTextSize(mFontSize);
// Draw the particle systems
for (int i = 0; i < mNextSystem; i++) {
     mParticleSystems.get(i).draw(mCanvas, mPaint);
}
// Draw the buttons
mCanvas.drawRect(mResetButton, mPaint);
mCanvas.drawRect(mTogglePauseButton, mPaint);

前面的代码循环遍历mParticleSystems,对每个调用draw方法。当然,我们实际上还没有生成任何实例。为此,我们需要学*如何响应屏幕交互。

处理触摸

要开始,请将OnTouchEvent方法添加到LiveDrawingView类中:

@Override
public boolean onTouchEvent(MotionEvent motionEvent) {

   return true;
}

这是一个重写的方法,每当用户与屏幕交互时,Android 都会调用它。查看OnTouchEvent方法的唯一参数。

使用以下代码行导入MotionEvent类:

import android.view.MotionEvent;

原来motionEvent中隐藏了大量数据,这些数据包含了刚刚发生的触摸的详细信息。操作系统将其发送给我们,因为它知道我们可能需要其中的一些数据。

请注意,我说的是一些MotionEvent类非常庞大。它包含了数十种方法和变量。

注意

在这个项目中,我们将揭示MotionEvent类的一些细节。您可以在这里完整地探索MotionEvent类:stuff.mit.edu/afs/sipb/project/android/docs/reference/android/view/MotionEvent.html。请注意,完成此项目并不需要进行进一步的研究。

目前,我们只需要知道在玩家的手指在屏幕上移动、触摸屏幕或从屏幕上移开时的精确时刻的屏幕坐标。

我们将使用motionEvent中包含的一些变量和方法,包括以下内容。

  • getAction方法,意料之中地“获取”执行的动作。不幸的是,它以稍微编码的格式提供这些信息,这解释了其他一些变量的必要性。

  • ACTION_MASK变量提供一个称为掩码的值,借助一些更多的 Java 技巧,可以用来过滤getAction的数据。

  • ACTION_UP变量,我们可以使用它来比较并查看执行的动作是否是我们想要响应的动作(从屏幕上移开手指)。

  • ACTION_DOWN变量,我们可以使用它来比较并查看执行的动作是否是我们想要响应的动作。

  • ACTION_MOVE变量,我们可以用它来比较并查看执行的动作是否是移动/拖动。

  • getX方法告诉我们事件发生的水*浮点坐标。

  • getY方法告诉我们事件发生的垂直浮点坐标。

举个具体的例子,假设我们需要使用ACTION_MASK过滤getAction方法返回的数据,并查看结果是否与ACTION_UP相同。如果是,那么我们知道用户刚刚从屏幕上移开手指,也许是因为他们刚刚点击了一个按钮。一旦我们确定事件是正确类型的,我们就需要使用getXgetY方法找出事件发生的位置。

最后一个复杂之处在于,“Java 诡计”我所指的是&位运算符,不要与我们一直与if关键字一起使用的逻辑&&运算符混淆。

&位运算符检查两个值中的每个对应部分是否为真。这是在使用ACTION_MASKgetAction时所需的过滤器。

注意

理智检查。我不愿详细讨论MotionEvent和位运算符。完全可以完成整本书甚至一个专业质量的交互式应用程序,而不需要完全理解它们。如果你知道我们在下一节中写的代码行确定了玩家刚刚触发的事件类型,那就足够了。我只是猜想像你这样挑剔的读者可能想要了解其中的细节。总之,如果你理解位运算符,很好,你可以继续。如果你不理解,没关系,你仍然可以继续。如果你对位运算符感兴趣(有很多种),你可以在这里阅读更多关于它们的信息:en.wikipedia.org/wiki/Bitwise_operation

现在我们可以编写onTouchEvent方法并查看所有MotionEvent的操作。

编写onTouchEvent方法

通过在onTouchEvent方法中添加以下突出显示的代码来处理用户在屏幕上移动手指:

// User moved a finger while touching screen
   if ((motionEvent.getAction() &
                 MotionEvent.ACTION_MASK)
                 == MotionEvent.ACTION_MOVE) {
          mParticleSystems.get(mNextSystem).emitParticles(
                       new PointF(motionEvent.getX(),
                                     motionEvent.getY()));
          mNextSystem++;
          if (mNextSystem == MAX_SYSTEMS) {
                 mNextSystem = 0;
          }
   }
   return true;

添加以下代码行以导入PointF类:

import android.graphics.PointF;

if条件检查事件类型是否是用户移动手指。如果是,那么mParticleSystems中的下一个粒子系统将调用其emitParticles方法。之后,mNextSystem变量递增,并进行测试以查看是否是最后一个粒子系统。如果是,那么mNextSystem将被设置为零,准备在下次需要时重新使用现有的粒子系统。

通过在我们刚讨论过的代码之后并在我们已经编写的return语句之前添加以下突出显示的代码来处理用户按下按钮之一:

// Did the user touch the screen
   if ((motionEvent.getAction() &
                 MotionEvent.ACTION_MASK)
                 == MotionEvent.ACTION_DOWN) {
// User pressed the screen see if it was in a 
          button
          if (mResetButton.contains(motionEvent.getX(),
                       motionEvent.getY())) {
                 // Clear the screen of all particles
                 mNextSystem = 0;
          }
// User pressed the screen see if it was in a 
          button
          if (mTogglePauseButton.contains
          (motionEvent.getX(), motionEvent.getY())) {
                 mPaused = !mPaused;
          }
   }
   return true;

if语句的条件检查用户是否点击了屏幕。如果是,那么RectF类的contains方法与getXgetY方法一起被用来检查这次按压是否在我们自定义按钮的范围内。如果按下重置按钮,所有的粒子都会消失,因为mNextSystem被设置为零。如果按下暂停按钮,那么mPaused的值将被切换,导致线程中的update方法停止/开始被调用。

完成 HUD

将以下突出显示的代码添加到printDebuggingText方法中:

// We will add more code here in the next chapter
mCanvas.drawText("Systems: " + mNextSystem,
10, mFontMargin + debugStart + debugSize * 2, 
          mPaint);
mCanvas.drawText("Particles: " + mNextSystem * mParticlesPerSystem,
10, mFontMargin + debugStart + debugSize * 3, 
          mPaint);

这段代码将在屏幕上打印一些有趣的统计数据,告诉我们当前绘制了多少粒子和系统。

警告

这个应用程序会产生明亮的闪烁颜色。这可能会引起光敏性癫痫的人感到不适或发作。请谨慎阅读。您可能只想阅读这个项目的理论,而不运行已完成的项目。

运行应用程序

现在我们可以看到实时绘图应用程序的运行并尝试一些我们在代码中注释掉的不同选项。

使用小型、圆形、彩色、快速粒子运行应用程序。只需在屏幕上轻点几下:

图 22.2 – 点击屏幕

图 22.2 – 点击屏幕

然后恢复绘图:

图 22.3 – 点击结果

图 22.3 – 点击结果

使用小型、白色、方形、缓慢、长时间的粒子进行儿童风格的绘图:

图 22.4 – 儿童风格的绘图

图 22.4 – 儿童风格的绘图

然后取消绘图暂停,等待 20 秒,直到绘图活跃起来并发生变化:

图 22.5 – 儿童风格的绘图结果

图 22.5 – 儿童风格的绘图结果

在我们进行下一个项目之前,Live Drawing 应用程序为我们提供了一个很好的机会,可以探索 Android Studio 的另一个功能。

Android Studio Profiler 工具

Android Studio Profiler 工具非常复杂和深入。但是,使用它来进行一些真正重要的测量非常简单。我们可以看到我们的应用程序使用了设备资源的多少,因此可以尝试提高应用程序的效率,使其运行更高效,并且使用更少的资源。资源包括 CPU 和内存使用率。

代码优化超出了本书的范围,但是我们开始监视应用程序性能的方式是一个很好的介绍。从主 Android Studio 菜单中选择View,然后选择Tool Windows | Profiler

您将在 Android Studio 的下部区域看到以下窗口:

图 22.6 – Android Studio 窗口

图 22.6 – Android Studio 窗口

要开始使用 Profiler 工具,请运行 Live Drawing 应用程序。Profiler 工具应该开始显示图表和数据,如下图所示。

根据您的 PC 防火墙软件的配置,您可能需要允许 Profiler 工具运行。此外,您可能需要在Profiler窗口左上角的+图标上单击,然后选择您的 AVD,以便 Profiler 工具连接到:

图 22.7 – 实时图表数据

图 22.7 – 实时图表数据

在上图中,我们可以看到 CPU 使用率、内存使用率、网络使用率和能量/电池使用率的实时图表数据。我们将重点关注 CPU 和内存使用率。

将鼠标悬停在CPU行,然后悬停在MEMORY行上,以查看每个指标的弹出详细信息。下图显示了我 PC 上这两个指标的详细信息,经过了 Photoshop 处理:

图 22.8 – 每个指标的弹出详细信息

图 22.8 – 每个指标的弹出详细信息

您可能会看到与我不同的值。前面的图表显示大约四分之一的 CPU 正在使用,大约使用了 121MB 的 RAM。

接下来,让我们稍微修改我们的代码并观察效果。在LiveDrawingView类中,编辑mParticlesPerSystem成员变量的初始化:

private int mParticlesPerSystem = 100;

将其更改为:

private int mParticlesPerSystem = 1000;

我们现在将每个系统的粒子数量增加了 10 倍。我们这样做是为了在分析器数据中获得一个峰值,因为我们现在将使用应用程序来绘制一些粒子系统。

当您再次运行应用程序时,通过在屏幕上移动手指/指针来绘制大量的粒子系统。请注意,当您在屏幕上绘制一些粒子系统时,CPU 使用率会急剧上升,尽管可能没有您预期的那么多。当粒子移动时,我的 CPU 使用率急剧上升到接近 40%,然后回落到 25%以上。如果您以前从未使用过类似分析器的工具,更令人惊讶的是内存使用几乎没有变化。

我们得到这样的结果的原因是,成千上万个粒子的计算占用了相当大量的 CPU。然而,在屏幕上绘制粒子并不需要增加内存。原因在于应用程序的内存都是在执行开始时分配的。无论粒子当前是否显示给用户都不重要。

这一小节并不打算深入探讨如何优化我们的图形或 CPU 密集型应用程序;它只是想介绍一下,您可能希望将优化添加到您进一步调查的事项列表中。

总结

在本章中,我们看到了如何向我们的实时系统添加成千上万个独立的实体。这些实体由ParticleSystem类控制,而ParticleSystem类又与游戏循环进行交互和控制。由于游戏循环在一个线程中运行,我们看到用户仍然可以无缝地与屏幕进行交互,操作系统通过onTouchEvent方法向我们发送这些交互的详细信息。

在下一章中,当我们探讨如何播放音效时,我们的应用程序最终会变得有些喧闹;我们还将学*如何检测不同版本的安卓系统。

第二十三章:支持不同版本的 Android,声音效果和 Spinner 小部件

在本章中,我们将学*如何检测和处理不同版本的 Android。然后,我们将能够研究SoundPool类以及根据应用程序运行的 Android 设备的 Android 版本使用它的不同方式。在这一点上,我们可以将我们学到的一切都投入到制作一个很酷的声音演示应用程序中,这也将向我们介绍一个新的 UI 小部件Spinner

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

  • 学*如何处理不同版本的 Android

  • 学*如何使用 Android 的SoundPool

  • 编写一个基于声音的应用程序,同时使用SpinnerView小部件

让我们开始吧。

处理不同版本的 Android

在本书的大部分时间里,我们没有注意支持旧的 Android 设备,主要原因是我们一直在使用的 API 的最新部分在如此高比例的设备上运行(超过 99%),以至于似乎不值得。除非你打算在古老的 Android 遗物应用中开辟一个利基,否则这似乎是一个明智的做法。然而,关于播放声音,Android API 最近进行了一些相对较新的修改。

实际上,这并不是一个立即的大问题,因为比这更新的设备仍然可以使用 API 的旧部分。但是,特别处理这些兼容性差异是一个很好的做法,因为最终,有一天,旧的部分可能在新版本的 Android 上不起作用。

在此时此地讨论这个的主要原因是,Android Lollipop 之前和之后的声音处理中的轻微差异给了我们一个很好的借口,让我们看看我们如何在我们的代码中处理这样的事情。

我们将看看如何使我们的应用与最新的设备和早期的 Lollipop 设备兼容。

我们将使用的类来发出一些声音是SoundPool类。首先,让我们看一些检测当前 Android 版本的简单代码。

检测当前的 Android 版本

我们可以使用Build.Version类的静态变量来确定当前的 Android 版本,并且我们可以通过将其与该版本的适当Build.VERSION_CODES变量进行比较来确定它是否比特定版本更新。如果这个解释有点啰嗦,那就看看我们如何确定当前版本是否等于或更新(更大)于 Lollipop:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

   // Lollipop or newer code goes here 

} else {
   // Code for devices older than lollipop here

}

现在让我们看看如何在比 Lollipop 更新的 Android 设备和比 Lollipop 更老的 Android 设备上发出一些声音。

SoundPool 类

SoundPool类允许我们保存和操作一系列音效 - 简而言之,一组声音。该类处理从解压声音文件(如.wav.ogg)到通过整数 ID 保持标识引用,当然还包括播放声音的一切。当播放声音时,它是以非阻塞的方式进行的(在幕后使用线程),不会干扰我们的应用程序的流畅运行或用户与之的交互。

我们需要做的第一件事是将音效添加到游戏项目的main文件夹中名为assets的文件夹中。我们很快就会真正做到这一点。

接下来,在我们的 Java 代码中,声明一个SoundPool类型的对象和一个用作我们打算使用的每个音效的 ID 的int。我们还声明另一个名为nowPlayingint,我们可以用它来跟踪当前正在播放的声音,我们很快就会看到我们是如何做到这一点的:

// create an ID
SoundPool sp;
int nowPlaying =-1;
int repeats = 2;
int idFX1 = -1;
float volume = 1;// Volumes rage from 0 through 1

现在我们将看一下我们初始化SoundPool的两种不同方式,这取决于设备使用的 Android 版本。这是使用我们编写不同版本的 Android 代码的绝佳机会。

以新方式初始化 SoundPool

新的方法涉及我们使用AudioAttributes对象来设置我们想要的声音池的属性。

在第一个代码块中,我们使用链接并在一个对象上调用四个单独的方法来初始化我们的AudioAttributes对象(audioAttributes):

// Instantiate a SoundPool dependent on Android version
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {

   // The new way
   // Build an AudioAttributes object
AudioAttributes audioAttributes = 
      // First method call
new AudioAttributes.Builder()
// Second method call
         .setUsage
         (AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
          // Third method call
          .setContentType
          (AudioAttributes.CONTENT_TYPE_SONIFICATION)
          // Fourth method call
          .build();// Yay! A semicolon
   // Initialize the SoundPool
   sp = new SoundPool.Builder()
         .setMaxStreams(5)
         .setAudioAttributes(audioAttributes)
         .build();
} 

在代码中,我们使用链接和这个类的Builder方法来初始化一个AudioAttributes对象,让它知道它将用于与USAGE_ASSISTANCE_SONIFICATION进行 UI 交互。

我们还使用CONTENT_TYPE_SONIFICATION值,让该类知道它是用于响应声音,例如用户按钮点击,碰撞或类似情况。

现在我们可以通过传入AudioAttributes对象(audioAttributes)和我们可能想要播放的最大同时声音数量来初始化SoundPool(sp)本身。

第二个代码块将另外四个方法链接到初始化sp,包括调用setAudioAttributes,该方法使用我们在之前链接方法块中初始化的audioAttributes对象。

现在我们可以编写一个else代码块,其中当然包含了旧方法的代码。

以旧方式初始化 SoundPool

不需要AudioAttributes对象;只需通过传入同时播放的声音数量来初始化SoundPool(sp)。最后一个参数是声音质量,传入0就足够了。这比新方法简单得多,但在我们可以做出的选择方面也不够灵活:

else {
   // The old way
   sp = new SoundPool(5, AudioManager.STREAM_MUSIC, 0);
}

注意

我们可以使用旧方法,较新版本的 Android 也可以处理它。但是,我们会收到有关使用已弃用方法的警告。这就是官方文档对此的说法。

此外,新方法提供了更多功能,正如我们所看到的。无论如何,这是一个很好的借口来查看处理不同版本 Android 的一些简单代码。

现在我们可以加载(解压缩)声音文件到我们的SoundPool中。

加载声音文件到内存

与我们的线程控制一样,我们需要将我们的代码包装在try-catch块中。这是有道理的,因为读取文件可能因我们无法控制的原因而失败,而且我们被迫这样做是因为我们使用的方法会抛出异常,否则我们编写的代码将无法编译。

try块中,我们声明并初始化了AssetManagerAssetFileDescriptor类型的对象。

通过使用AssetManager对象的openFd方法初始化AssetFileDescriptor实例,该方法实际上解压缩声音文件。然后我们初始化我们的 ID(idFX1),同时将AssetFileDescriptor的内容加载到我们的SoundPool中。

catch块只是向控制台输出一条消息,让我们知道是否出了什么问题。请注意,无论 Android 版本如何,此代码都是相同的:

try{

   // Create objects of the 2 required classes
   AssetManager assetManager = this.getAssets();
   AssetFileDescriptor descriptor;
   // Load our fx in memory ready for use
   descriptor = assetManager.openFd("fx1.ogg");
   idFX1 = sp.load(descriptor, 0);
}catch(IOException e){

   // Print an error message to the console
   Log.d("error", "failed to load sound files");
}

我们准备制造一些噪音。

播放声音

在这一点上,我们的SoundPool实例中有一个音效,并且我们有一个 ID 可以用来引用它。

无论我们如何构*SoundPool对象,这段代码都是相同的,这就是我们播放声音的方式。请注意,在下一行代码中,我们使用相同方法的返回值来初始化nowPlaying变量。

因此,以下代码同时播放声音并将正在播放的 ID 的值加载到nowPlaying中:

nowPlaying = sp.play(idFX1, volume, volume, 0, repeats, 1);

注意

不需要将 ID 存储在nowPlaying中来播放声音,但正如我们将要看到的那样,它也有其用途。

play方法的参数如下:

  • 音效的 ID

  • 左扬声器音量和右扬声器音量

  • 相对于其他声音的优先级

  • 重复播放声音的次数

  • 它播放的速率/速度(1 是正常速率)

在我们制作声音演示应用之前,还有一件事。

停止声音

使用stop方法在声音仍在播放时停止声音也非常简单。请注意,可能在任何给定时间有多个音效正在播放,因此stop方法需要声音效果的 ID 来停止:

sp.stop(nowPlaying);

当您调用play时,如果要跟踪当前播放的声音,只需存储其 ID,以便以后可以与其交互。很快我们将看到,在 Pong 游戏中播放声音的代码看起来更像这样:

sp.play(mBeepID, 1, 1, 0, 0, 1);

上一行代码将简单地以最大音量播放所选的声音(mBeepID),具有最低的优先级,直到以正常速度结束且不重复。

现在我们可以制作声音演示应用程序。

声音演示应用程序介绍 Spinner 小部件

当然,谈到声音效果,我们需要一些实际的声音文件。您可以使用 Bfxr 制作自己的声音效果(下面解释),也可以使用提供的声音效果。该应用程序的声音效果位于第二十三章/Sound Demo文件夹中的下载包的assets文件夹中。但您可能想制作自己的声音效果。

制作声音效果

有一个名为 Bfxr 的开源应用程序,可以让我们制作自己的声音效果。以下是使用 Bfxr 制作自己的声音效果的快速指南。从www.bfxr.net免费获取一份副本。

注意

请注意,声音演示应用程序的声音效果已经在第二十三章/assets文件夹中提供给您。除非您愿意,否则您无需创*自己的声音效果。学*如何使用它仍然是值得的。

按照网站上的简单说明进行设置。尝试其中一些内容以制作酷炫的声音效果:

注意

这是一个非常简化的教程。您可以使用 Bfxr 做更多事情。要了解更多,请阅读上一个 URL 网站上的提示。如果您在下载 Bfxr 时遇到问题,可以使用该网站创*所有您的声音效果,或者只使用提供的示例。

  1. 运行 Bfxr:图 23.1 – Bfxr 页面

图 23.1 – Bfxr 页面

  1. 尝试所有生成该类型随机声音的预设类型。当您有接近所需声音时,继续下一步:图 23.2 – 不同效果

图 23.2 – 不同效果

  1. 使用滑块微调音调、持续时间和其他方面的新声音:图 23.3 – 调整您的声音

图 23.3 – 调整您的声音

  1. 通过单击.wav按钮保存您的声音:图 23.4 – 导出 Wav 按钮

图 23.4 – 导出 Wav 按钮

  1. Android 与 OGG 格式的声音非常兼容,因此在要求命名文件时,请在文件名末尾使用.ogg扩展名。

  2. 重复步骤 25,创*三个酷炫的音效。将它们命名为fx1.oggfx2.oggfx3.ogg。我们使用.ogg文件格式,因为它比 WAV 等格式更压缩。

一旦您的声音文件准备好,我们就可以继续进行应用程序。

布置声音演示

我将更简洁地描述我们正在适应的项目的部分,比以前的项目更简洁。但是,每当有新概念时,我一定会详细解释。我想现在你应该可以轻松地将一些小部件拖放到ConstraintLayout上并更改它们的text属性。

完成以下步骤。如果您遇到任何问题,可以在下载包的第二十三章文件夹中复制或查看代码:

  1. 创*一个新项目,命名为Sound Demo,从activity_main.xml文件中选择TextView

  2. 现在我们将在activity_main.xml文件中构*布局,所以请确保处于设计视图中。按照从上到下,然后从左到右的顺序,拖放text属性,如下一张截图所示:

图 23.5 – 在 activity_main.xml 文件中构*布局

  1. 单击推断约束按钮。

  2. 使用以下表格设置它们的属性:

  3. 接下来,在 values 文件夹中的 strings.xml 文件中添加以下突出显示的代码。我们在上一步中使用了名为 spinner_options 的字符串资源数组作为上一步中 options 属性的选项。它将表示可以从我们的 Spinner 小部件中选择的选项:

<resources>
    <string name="app_name">Sound Demo</string>
    <string name="hello_world">Hello world!</string>
    <string-array name="spinner_options">
      <item>0</item>
      <item>1</item>
      <item>3</item>
      <item>5</item>
      <item>10</item>
    </string-array>
    </resources>

现在运行应用程序,最初您将看不到任何我们以前没有看到的东西。但是,如果您点击下拉列表,您将看到我们称为 spinner_options 的字符串数组中的选项。我们可以使用下拉列表来控制播放音效时音效重复的次数:

图 23.6 - 下拉列表选项

图 23.6 - 下拉列表选项

让我们编写 Java 代码使这个应用程序工作,包括我们如何与我们的下拉列表交互。

使用操作系统的文件浏览器,转到项目的 app\src\main 文件夹,并添加一个名为 assets 的新文件夹。

在下载包的/assets 文件夹中有三个预先准备好的音频文件第二十三章。将这三个文件放入您刚创*的 assets 目录中,或者使用您自己创*的文件。重要的是它们的文件名是 fx1.ogg,fx2.ogg 和 fx3.ogg。

编写声音演示

首先,我们将更改类声明,以便我们可以有效地处理所有小部件的交互。编辑声明以实现 View.OnClickListener,如下所示:

public class MainActivity extends AppCompatActivity 
implements View.OnClickListener {

我们将很快添加所需的 onClick 方法。

现在,我们为我们的 SoundPool、音效 ID、nowPlaying int(如前所述),还将添加一个 float 来保存音量值,介于 0(静音)和 1(相对于设备当前音量的最大音量)之间。我们还将添加一个 int 叫做 repeats,它不出所料地保存我们将重复播放给定音效的次数的值:

SoundPool sp;
int idFX1 = -1;
int idFX2 = -1;
int idFX3 = -1;
int nowPlaying = -1;
float volume = .1f;
int repeats = 2;

现在,在 onCreate 方法中,我们可以以通常的方式获取引用并为我们的按钮设置点击侦听器。将以下代码添加到项目中:

Button buttonFX1 = findViewById(R.id.btnFX1);
buttonFX1.setOnClickListener(this);
Button buttonFX2 = findViewById(R.id.btnFX2);
buttonFX2.setOnClickListener(this);
Button buttonFX3 = findViewById(R.id.btnFX3);
buttonFX3.setOnClickListener(this);
Button buttonStop = findViewById(R.id.btnStop);
buttonStop.setOnClickListener(this);

仍然在 onCreate 中,我们可以根据设备使用的 Android 版本初始化我们的 SoundPool(sp)。将以下代码添加到项目中:

// Instantiate our SoundPool based on the version of Android
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
   AudioAttributes audioAttributes = 
      new AudioAttributes.Builder()
   .setUsage(AudioAttributes. 
   USAGE_ASSISTANCE_SONIFICATION)

   .setContentType(
   AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .build();
   sp = new SoundPool.Builder()
                .setMaxStreams(5)
                .setAudioAttributes(audioAttributes)
                .build();
} else {
   sp = new SoundPool(5, AudioManager.STREAM_MUSIC, 0);
}

注意

使用您喜欢的方法为前面的代码添加以下 import 语句:

导入 android.media.AudioAttributes;

导入 android.media.AudioManager;

导入 android.media.SoundPool;

导入 android.os.Build;

导入 android.view.View;

导入 android.widget.Button;

接下来,依次加载我们的每个音效,并使用一个值来初始化我们的 ID,该值指向我们加载到 SoundPool 中的相关音效。整个过程都包裹在 try-catch 块中。将以下代码添加到项目中:

try{
   // Create objects of the 2 required classes
   AssetManager assetManager = this.getAssets();
   AssetFileDescriptor descriptor;
   // Load our fx in memory ready for use
   descriptor = assetManager.openFd("fx1.ogg");
   idFX1 = sp.load(descriptor, 0);
   descriptor = assetManager.openFd("fx2.ogg");
   idFX2 = sp.load(descriptor, 0);
   descriptor = assetManager.openFd("fx3.ogg");
   idFX3 = sp.load(descriptor, 0);
}catch(IOException e){
   // Print an error message to the console
   Log.e("error", "failed to load sound files");
}

注意

使用您喜欢的方法为前面的代码添加以下 import 语句:

导入 android.content.res.AssetFileDescriptor;

导入 android.content.res.AssetManager;

导入 android.util.Log;

导入 java.io.IOException;

然后我们看看我们将如何处理 SeekBar 小部件。正如您可能已经期待的那样,我们将使用一个匿名类。我们使用 OnSeekBarChangeListener 类并重写 onProgressChanged,onStartTrackingTouch 和 onStopTrackingTouch 方法。

我们只需要在 onProgressChanged 方法中添加代码。在这个方法中,我们只是改变我们的音量变量的值,然后在我们的 SoundPool 对象上使用 setVolume 方法,传入当前播放的音效和左右声道的音量。将以下代码添加到项目中:

// Now setup the seekbar
SeekBar seekBar = findViewById(R.id.seekBar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
   @Override
   public void onProgressChanged(SeekBar seekBar, 
   int value, boolean fromUser) {
         volume = value / 10f;
         sp.setVolume(nowPlaying, volume, volume);
   }
   @Override
   public void onStartTrackingTouch(SeekBar seekBar) {
   }
   @Override
   public void onStopTrackingTouch(SeekBar seekBar) {
   }
});

注意

使用您喜欢的方法为前面的代码添加以下 import 语句:

导入 android.widget.SeekBar;

在 SeekBar 代码之后是 Spinner 代码和另一个匿名类来处理用户交互。我们使用 AdapterView.OnItemSelectedListener 来重写 onItemSelected 和 onNothingSelected 方法。

我们所有的代码都放在onItemSelected方法中,该方法创*一个名为temp的临时字符串,然后使用Integer.ValueOf方法将字符串转换为int,我们可以用它来初始化repeats变量。将以下代码添加到项目中:

// Now for the spinner
final Spinner spinner = (Spinner) findViewById(R.id.spinner);
spinner.setOnItemSelectedListener(
new AdapterView.OnItemSelectedListener() {
   @Override
   public void onItemSelected(AdapterView<?> 
   parentView, View selectedItemView, 
   int position, long id) {
         String temp = String.valueOf(
         spinner.getSelectedItem());
         repeats = Integer.valueOf(temp);
   }
   @Override
   public void onNothingSelected(AdapterView<?> parentView) {

   }
});

注意

使用您喜欢的方法为以前的代码添加以下import语句:

导入 android.widget.AdapterView;

导入 android.widget.Spinner;

这就是onCreate的全部内容。

现在实现onClick方法,这是必需的,因为这个类实现了View.OnClickListener接口。简单地说,每个按钮都有一个case语句。有一个case语句来播放我们的三种声音效果,设置音量和设置重复次数。请注意,对play的每次调用的返回值都存储在nowPlaying中。当用户按下stop时,nowPlaying的当前值会导致最近启动的声音效果停止。将以下代码添加到项目中:

@Override
public void onClick(View v) {
   switch (v.getId()){
         case R.id.btnFX1:
                sp.stop(nowPlaying);
                nowPlaying = sp.play(idFX1, volume, 
                volume, 0, repeats, 1);
                break;
         case R.id.btnFX2:
                sp.stop(nowPlaying);
                nowPlaying = sp.play(idFX2, 
                volume, volume, 0, repeats, 1);
                break;
         case R.id.btnFX3:
                sp.stop(nowPlaying);
                nowPlaying = sp.play(idFX3, 
                volume, volume, 0, repeats, 1);
                break;
         case R.id.btnStop:
                sp.stop(nowPlaying);
                break;
   }
}

现在我们可以运行应用程序。如果听不到任何声音,请确保设备的音量已调高。

点击适当的按钮以播放所需的声音效果。更改音量和重复次数,当然,尝试使用STOP按钮停止它。

还要注意,当一个声音效果正在播放时,您可以重复点击多个播放按钮,声音将同时播放,直到我们设置的最大流数(5)。

总结

在本章中,我们仔细研究了SoundPool类,包括如何检测用户使用的 Android 版本并相应地改变我们的代码。然后,我们利用所有这些知识完成了声音演示应用程序。

在下一章中,我们将学*如何使我们的应用程序与多种不同的布局配合工作。

第二十四章:设计模式、多种布局和片段

从我们刚开始设置 Android Studio 的时候,我们已经走了很长的路。那时,我们一步一步地进行了所有操作,但随着我们的进展,我们试图展示的不仅仅是如何将 x 添加到 y 或将功能 a 添加到应用程序 b,而是让你能够以自己的方式使用所学到的知识来实现自己的想法。

乍一看,这一章可能看起来枯燥而技术性,但这一章更多地关乎你未来的应用程序,而不是迄今为止书中的任何内容。我们将看一下 Java 和 Android 的一些方面,你可以将其用作框架或模板,以制作更加令人兴奋和复杂的应用程序,同时保持代码的可管理性。这是成功现代应用程序的关键。此外,我将*议进一步学*的领域,这本书中根本没有足够的空间来涉及。

在这一章中,我们将学*以下内容:

  • 模式和模型-视图-控制器

  • Android 设计指南

  • 开始使用真实世界的设计和处理多种不同的设备

  • 片段简介

让我们开始吧。

技术要求

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

介绍模型-视图-控制器模式

模型-视图-控制器模式涉及将我们应用程序的不同方面分离成称为层的明确部分。Android 应用程序通常使用模型-视图-控制器模式。模式只是一种公认的结构代码和其他应用资源(如布局文件、图像、数据库等)的方式。

模式对我们很有用,因为通过遵循模式,我们可以更有信心地做正确的事情,也不太可能因为将自己编码到尴尬的境地而不得不撤销大量的辛苦工作。

在计算机科学中有许多模式,但了解模型-视图-控制器(MVC)就足以创*一些专业构*的 Android 应用程序。

我们已经部分地使用了 MVC,所以让我们依次看看这三个层:

  • Note类以及它的 getter、setter 和 JSON 代码是数据和逻辑。

  • Android API 的View类层次结构。

  • 控制器:控制器是视图和模型之间的部分。它与两者进行交互并保持它们分开。它包含了所谓的应用逻辑。如果用户点击按钮,应用层决定如何处理它。当用户点击“确定”以添加新的注释时,应用层监听视图层上的交互。它捕获视图中包含的数据并将其传递给模型层。

设计模式是一个庞大的主题。有许多不同的设计模式,如果你想对这个主题有一个适合初学者的介绍,我会推荐《Head First Design Patterns》。如果你想深入了解设计模式的世界,那么你可以尝试《Design Patterns: Elements of Reusable Object-Oriented Software》,这本书被认为是一种设计模式的权威,但阅读起来要困难得多。

随着书籍的进展,我们还将开始更多地利用我们已经讨论过但尚未充分利用的面向对象编程方面。我们将一步一步地做到这一点。

Android 设计指南

应用程序设计是一个广阔的主题。这是一个只能在一本专门的书中开始教授的主题。而且,就像编程一样,只有通过不断的练*、审查和改进,你才能开始擅长应用程序设计。

那么,我所说的设计到底是什么意思呢?我指的是您将小部件放在屏幕上的位置,使用哪些小部件,它们应该是什么颜色,大小应该是多少,如何在屏幕之间进行过渡,滚动页面的最佳方式,何时以及使用哪些动画插值器,您的应用程序应该分成哪些屏幕,以及还有更多其他内容。

希望本书能让您有能力实施您对上述问题的所有选择。不幸的是,本书没有足够的空间,作者可能也没有足够的技能来教您如何做出这些选择。

注意

您可能会想,“我应该怎么办?”继续制作应用程序,不要让缺乏设计经验和知识阻止您!甚至将您的应用程序发布到应用商店。但请记住,还有这样一个完全不同的话题 - 设计 - 如果您的应用程序真的要成为世界一流的应用程序,那就需要一些关注。

即使在中等规模的开发公司中,设计师很少也是程序员,即使是非常小的公司也经常会外包他们的应用程序的设计(或设计师可能会外包编码)。

设计既是艺术又是科学,Google 已经证明他们认识到这一点,为现有设计师和新设计师提供了高质量的支持。

注意

我强烈*议您访问并收藏这个网页:developer.android.com/design/。它非常详细和全面,完全专注于 Android,并提供了大量的资源,包括图像、调色板和指南。

使理解设计原则成为短期目标。使提高您的实际设计技能成为一项持续的任务。访问并阅读以设计为重点的网站,并尝试实施您发现令人兴奋的想法。

然而,最重要的是,不要等到您成为设计专家才开始制作应用程序。继续将您的想法付诸实践并发布它们。要求每个应用程序的设计都比上一个稍微好一点。

我们将在接下来的章节中看到,并且已经看到,Android API 为我们提供了一整套超时尚的 UI,我们可以利用这些 UI,只需很少的代码或设计技能。这些 UI 在很大程度上使您的应用程序看起来就像是由专业人员设计的。

现实世界的应用程序

到目前为止,我们已经构*了十几个或更多不同复杂度的应用程序。其中大部分我们都是在手机上设计和测试的。

当然,在现实世界中,我们的应用程序需要在任何设备上都能良好运行,并且必须能够处理在纵向或横向视图(在所有设备上)发生的情况。

此外,我们的应用程序通常不能只是在不同设备上“正常工作”和“看起来还行”就足够了。通常,我们的应用程序需要根据设备是手机还是*板电脑,以及是横向还是纵向方向,而表现出明显不同的 UI 和行为。

注意

Android 支持大屏幕电视、通过 Wear API 支持智能手表、虚拟现实和增强现实,以及物联网中的“物品”。本书不涵盖后两种情况,但在本书结束时,作者猜测您将准备好涉足这些话题,如果您选择的话。

看看 BBC 天气应用程序在 Android 手机上以纵向方向运行的截图。看看基本布局,也研究所显示的信息,因为我们将很快将其与*板应用程序进行比较:

图 24.1 - BBC 天气应用程序在 Android 手机上以纵向方向运行

图 24.1 - BBC 天气应用程序在 Android 手机上以纵向方向运行

目前,上一张截图的目的并不是为了向您展示具体的 UI 功能,而是为了让您能够将其与下一张截图进行比较。看看在*板电脑上以横向方向运行的完全相同的应用程序:

图 24.2 - BBC 天气应用程序在 Android 手机上以横向方向运行

图 24.2 – BBC 天气应用在 Android 手机上横向方向运行

请注意,与手机应用程序相比,*板电脑 UI 有一个额外的信息面板。这个额外的面板在前面的截图中被突出显示。

这个截图的重点再次不是特定的 UI,甚至我们如何实现类似的 UI,而是 UI 是如此不同,它们很容易被认为是完全不同的应用程序。然而,如果您下载这个应用程序,*板电脑和手机是相同的下载。

Android 允许我们设计这样的真实应用程序,其中不仅布局针对不同的设备类型/方向/大小是不同的,而且(这一点很重要)行为也是不同的。使这一切成为可能的 Android 秘密武器是片段

谷歌说:

“片段代表活动中的行为或用户界面的一部分。您可以在单个活动中组合多个片段,以构*多窗格 UI,并在多个活动中重用片段。

您可以将片段视为活动的模块化部分,它具有自己的生命周期,接收自己的输入事件,并且您可以在活动运行时添加或删除它(有点像可以在不同活动中重复使用的“子活动”)。

片段必须始终嵌入在活动中,片段的生命周期受主机活动的生命周期直接影响。

我们可以在不同的 XML 文件中设计多个不同的布局,我们很快就会这样做。我们还可以在我们的 Java 代码中检测设备方向和屏幕分辨率,因此我们可以动态地做出布局方面的决策。

让我们尝试使用设备检测,然后我们将首次查看片段。

设备检测迷你应用

了解检测和响应设备及其不同属性(屏幕、方向等)的最佳方法是制作一个简单的应用程序:

  1. 创*一个新的设备检测。将所有其他设置保留为默认设置。

  2. 在设计选项卡中打开activity_main.xml文件,并删除默认的TextView

  3. 拖动detectDevice。我们将在一分钟内编写这个方法。

  4. 拖动两个txtOrientationtxtResolution

  5. 检查您是否有一个看起来像下一个截图的布局:

注意

我拉伸了我的小部件(主要是水*方向),并增加了textSize属性到24sp,以使它们在屏幕上更清晰,但这对于应用程序的正确工作并不是必需的。

图 24.3– 布局检查

图 24.3– 布局检查

  1. 单击推断约束按钮以固定 UI 元素的位置。

现在我们将做一些新的事情。我们将专门为横向方向构*一个布局。

在 Android Studio 中,确保在编辑器中选择了activity_main.xml文件,并找到如下所示的预览方向按钮:

图 24.4 – 创*横向变化

图 24.4 – 创*横向变化

单击它,然后选择创*横向变化

现在,您有一个新的布局 XML 文件,名称相同,但是是横向模式。布局在编辑器中看起来是空白的,但正如我们将看到的那样,情况并非如此。查看项目资源管理器中的layout文件夹,并注意确实有两个名为activity_main的文件,其中一个(我们刚刚创*的新文件)带有land后缀。这在下一个截图中显示:

图 24.5 – activity_main 文件夹

图 24.5 – activity_main 文件夹

选择这个新文件(带有land后缀的文件),现在查看组件树。它在下一个截图中显示:

图 24.6 – 组件树

图 24.6 – 组件树

看起来布局已经包含了我们所有的小部件 - 我们只是无法在设计视图中看到它们。这种异常的原因是,当我们创*横向布局时,Android Studio 复制了纵向布局,包括所有的约束。纵向约束很少与横向约束匹配。

要解决这个问题,点击删除所有约束按钮;它是推断约束按钮左侧的按钮。现在 UI 没有约束了。所有的 UI 小部件将会混乱地出现在左上角。一次一个,重新排列它们,使其看起来像下一个截图。我不得不手动添加约束来使这个设计工作,所以我在下一个截图中展示了约束:

图 24.7 - 添加约束

图 24.7 - 添加约束

点击/Device Detection/layout-land文件夹。

注意

外观并不重要,只要您能看到两个TextView小部件的内容并点击按钮即可。

现在我们有了两种不同方向的基本布局,我们可以把注意力转向编写 Java 代码。

编写 MainActivity 类

MainActivity类声明之后,添加以下成员变量,以保存对我们两个TextView小部件的引用:

private TextView txtOrientation;
private TextView txtResolution;

注意

此时导入TextView类:

import android.widget.TextView;

现在,在MainActivity类的onCreate方法中,在调用setContentView之后,添加以下代码:

// Get a reference to our TextView widgets
txtOrientation = findViewById(R.id.txtOrientation);
txtResolution = findViewById(R.id.txtResolution);

onCreate之后,添加处理我们按钮点击并运行检测代码的方法:

public void detectDevice(View v){
   // What is the orientation?
   Display display = 
   getWindowManager().getDefaultDisplay();
   txtOrientation.setText("" + display.getRotation());
   // What is the resolution?
   Point xy = new Point();
   display.getSize(xy);
   txtResolution.setText("x = " + xy.x + " y = " + xy.y);
}

注意

导入以下三个类:

import android.graphics.Point;

import android.view.Display;

import android.view.View;

这段代码通过声明和初始化一个名为displayDisplay类型的对象来工作。这个对象(display)现在保存了关于设备特定显示属性的大量数据。

getRotation方法的结果输出到顶部的TextView小部件中。

然后,代码初始化了一个名为xyPoint类型的对象。getSize方法然后将屏幕分辨率加载到xy中。然后使用setText方法将水*(xy.x)和垂直(xy.y)分辨率输出到TextView小部件中。

注意

您可能还记得我们在 Kids 绘画应用中使用了DisplayPoint类。

每次点击按钮,两个TextView小部件都将被更新。

解锁屏幕方向

在我们运行应用程序之前,我们要确保设备没有锁定在纵向模式(大多数新手机默认情况下是这样)。从模拟器的应用抽屉(或您将要使用的设备)中,点击设置应用程序,选择显示,并使用开关将自动旋转屏幕设置为开启。我在下一个截图中展示了这个设置:

图 24.8 - 自动旋转屏幕

图 24.8 - 自动旋转屏幕

运行应用程序

现在您可以运行应用程序并点击按钮:

图 24.9 - 点击按钮

图 24.9 - 点击按钮

使用模拟器控制面板上的旋转按钮之一,将设备旋转到横向:

图 24.10 - 旋转设备

图 24.10 - 旋转设备

注意

您还可以在 PC 上使用Ctrl + F11,在 Mac 上使用Ctrl + FN + F11

现在再次点击按钮,您将看到横向布局的效果:

图 24.11 - 横向布局

图 24.11 - 横向布局

您可能会注意到的第一件事是,当您旋转屏幕时,屏幕会短暂地变空白。这是 Activity 重新启动并再次调用onCreate方法 - 这正是我们需要的。它在横向布局上调用setContentView方法,而MainActivity中的代码引用具有相同 ID 的小部件,所以完全相同的代码可以工作。

注意

不要花太多时间思考这个问题,因为我们将在本章后面讨论它。

如果 0 和 1 的结果对设备方向不够明显,它们指的是Surface类的public static final变量 - Surface.ROTATION_0等于 0,Surface.ROTATION_180等于 1。

注意

请注意,如果您将屏幕向左旋转,那么您的值将是 1 - 与我的相同,但如果您将其向右旋转,您将看到值 3。如果您将设备旋转到倒置的纵向模式,您将获得值 4。

然后我们可以根据这些检测测试的结果编写一个switch块,并加载不同的布局。

但正如我们刚才看到的,Android 通过允许我们将特定布局添加到带有配置限定符的文件夹中(例如land,代表横向)来简化这一过程。

配置限定符

我们已经在第三章中遇到了配置限定符,例如layout-largelayout-xhdpi探索 Android Studio 和项目结构。在这里,我们将刷新并扩展对它们的理解。

我们可以通过使用配置限定符开始减少对控制器层的影响来影响应用程序布局。有关大小、方向和像素密度的配置限定符。要利用配置限定符,我们只需按照通常的方式设计布局,针对我们首选的配置进行优化,然后将该布局放入 Android 识别为特定配置的文件夹中。

例如,在前一个应用程序中,将布局放入land文件夹会告诉 Android 在设备处于横向方向时使用布局。

上述声明可能会显得有些模糊。这是因为 Android Studio 的layoutlayout-land文件夹如下所示:

图 24.12 - 布局和布局-横向文件夹

图 24.12 - 布局和布局-横向文件夹

切换回Android视图或保持在项目文件视图上 - 任何您喜欢的。

因此,如果我们想要为横向和纵向创*不同的布局,我们将在res文件夹中创*一个名为layout-land的文件夹(或者使用我们在前一个应用程序中使用的快捷方式),并在其中放置我们专门设计的布局。

当设备处于纵向位置时,将使用layout文件夹中的常规布局,当设备处于横向位置时,将使用layout-land文件夹中的布局。

如果我们要为不同尺寸的屏幕设计,我们将布局放入以下名称的文件夹中:

  • layout-small

  • layout-normal

  • layout-large

  • layout-xlarge

如果我们要为不同像素密度的屏幕设计,我们可以将 XML 布局放入文件夹中,文件夹的名称如下:

  • layout-ldpi 用于低 DPI 设备

  • layout-mdpi 用于中等 DPI 设备

  • layout-hdpi 用于高 DPI 设备

  • layout-xhdpi 用于超高 DPI 设备

  • layout-xxhdpi 用于超超高 DPI 设备

  • layout-xxxhdpi 用于超高 DPI 设备

  • layout-nodpi 用于未另外适配 DPI 的设备

  • layout-tvdpi 用于电视

低、高或超高 DPI 等的确切资格可以在下一个信息框中的链接中进行研究。这里的重点是原则。

值得指出的是,我们刚才讨论的远远不是关于配置限定符的全部故事,与设计一样,值得将其列入进一步研究的事项清单。

注意

通常,Android 开发者网站上有大量关于处理不同设备布局的详细信息。请尝试此链接以获取更多信息:developer.android.com/guide/practices/screens_support

配置限定符的限制

前一个应用程序和我们对配置限定符的讨论在许多情况下确实非常有用。然而,不幸的是,配置限定符和在代码中检测属性只解决了我们 MVC 模式的视图层中的问题。

正如讨论的那样,我们的应用程序有时需要具有不同的行为以及布局。这可能意味着我们的 Java 代码在控制器层(在我们以前的大多数应用程序中为MainActivity)中有多个分支,并且可能召唤出对于每种不同情况都有不同代码的巨大的ifswitch块的可怕愿景。

幸运的是,这不是这样做的方式。对于这种情况,实际上对于大多数应用程序,Android 都有片段。

片段

片段很可能会成为您制作的几乎每个应用程序的主打。它们非常有用,有很多使用它们的理由,一旦您*惯了它们,它们就变得非常简单,几乎没有理由不使用它们。

片段是应用程序的可重用元素,就像任何类一样,但正如之前提到的,它们具有特殊功能,例如能够加载它们自己的视图/布局以及它们自己的生命周期方法,这使它们非常适合实现我们在真实世界应用程序部分讨论的目标,并为不同的设备(如我们查看的天气应用程序)拥有不同的布局和代码。

让我们深入了解片段,一次一个特性。

片段也有生命周期

我们可以设置和控制片段,就像我们对活动所做的那样,覆盖适当的生命周期方法:

  • onCreate:在onCreate方法中,我们可以初始化变量并几乎可以做所有我们通常在Activity onCreate方法中做的事情。这个方法的一个重要例外是初始化我们的 UI。

  • onCreateView:在这个方法中,我们将像其名称所示一样,获取对我们的任何 UI 小部件的引用,设置匿名类以监听点击,以及更多其他内容,我们很快就会看到。

  • onAttachonDetach:这些方法在将Fragment投入使用/停止使用之前调用。

  • onResumeonStartonPauseonStop:在这些方法中,我们可以执行某些操作,例如创*或删除对象或保存数据,就像我们在基于它们的Activity中所做的那样。

注意

如果您想详细了解Fragment的生命周期,请访问 Android 开发者网站上的此链接:developer.android.com/guide/components/fragments

这一切都很好,但我们需要一种方法来首先创*我们的片段,并能够在正确的时间调用这些方法。

使用 FragmentManager 管理片段

FragmentManager类是Activity的一部分。我们使用它来初始化Fragment,将片段添加到活动的布局中,并结束Fragment。我们之前在初始化FragmentDialog时简要看到了FragmentManager

注意

在 Android 中学*很难避免碰到Fragment类,就像在学* Java 时不断碰到 OOP/类一样困难,等等。

以下突出显示的代码显示了我们如何使用FragmentManager(它已经是Activity的一部分)作为参数来创*弹出对话框:

button.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
         // Create a new DialogShowNote called dialog
         DialogShowNote dialog = new DialogShowNote();

         // Send the note via the sendNoteSelected method
         dialog.sendNoteSelected(mTempNote);

         // Create the dialog
         dialog.show(getFragmentManager(), "123");
   }
});

当时,我要求您不要关心方法调用的参数。方法调用的第二个参数是Fragment的 ID。我们将看到如何更广泛地使用FragmentManager,以及如何使用Fragment ID。

FragmentManager确实做到了其名称所暗示的。这里重要的是Activity只有一个FragmentManager,但它可以处理许多片段。这正是我们需要在单个应用程序中拥有多个行为和布局的情况。

FragmentManager 还调用其负责的片段的各种生命周期方法。这与 Android 调用的 Activity 生命周期方法是不同的,但又密切相关,因为 FragmentManager 调用许多 Fragment 生命周期方法是作为对 Activity 生命周期方法的响应。通常情况下,如果我们在每种情况下做出适当的响应,我们就不需要太担心何时以及如何做出响应。

注意

片段将成为我们未来许多,如果不是所有应用程序的基本部分。然而,就像我们对命名约定、字符串资源和封装性所做的那样,出于简单学*目的或在应用程序很小且使用片段会过度的情况下,我们将不使用片段。当然,学*片段时将是一个例外。

我们的第一个 Fragment 应用程序

让我们以尽可能简单的形式构* Fragment,以便在后面的章节中,在我们开始在各个地方生成真正有用的 Fragment 之前,我们可以理解正在发生的事情。

注意

我敦促所有读者浏览并构*此项目。从一个文件跳到另一个文件,仅仅阅读可能会使它看起来比实际复杂得多。当然,你可以从下载包中复制粘贴代码,但也请按照步骤创*自己的项目和类。片段并不太难,但它们的实现,正如它们的名字所暗示的那样,有点分散。

使用空活动模板创*一个名为Simple Fragment的新项目,并将其余设置保持默认。

切换到activity_main.xml并删除默认的TextView小部件。

现在确保通过在 fragmentHolder 中左键单击它来选择根 ConstraintLayout。现在我们将能够在我们的 Java 代码中获取对此布局的引用,并且正如 id 属性所暗示的那样,我们将向其添加一个 Fragment。

现在我们将创*一个布局,该布局将定义我们片段的外观。右键单击 LinearLayout 中的 fragment_layout,然后左键单击 LinearLayout。

添加一个单独的按钮。

现在我们有一个简单的布局供我们的 Fragment 使用,让我们写一些 Java 代码来创*实际的片段。

注意

请注意,您可以通过从调色板中简单拖放来创*一个片段,但以这种方式做事情会更不灵活和可控,而灵活性和控制性是片段的重要优势,正如我们将在接下来的章节中看到的那样。通过创*一个扩展 Fragment 的类,我们可以从中制作任意多的片段。

在项目资源管理器中,右键单击包含MainActivity文件的文件夹。从上下文菜单中,选择SimpleFragment

注意

请注意,有各种预编码状态的选项可快速实现 Fragment 类的创*,但现在它们可能会稍微模糊此应用程序的学*目标。

在我们的新SimpleFragment类中,更改代码以扩展 Fragment。在输入代码时,将要求您选择要导入的特定 Fragment 类,如下一张屏幕截图所示:

图 24.13 - 选择特定的 Fragment 类

图 24.13 - 选择特定的 Fragment 类

选择顶部选项,即androidx.fragment.app

注意

在这个类中,我们将需要以下所有的导入语句。之前的步骤已经添加了 androidx.fragment.app.Fragment 类:

导入 androidx.fragment.app.Fragment;

导入 android.os.Bundle;

导入 android.view.LayoutInflater;

导入 android.view.View;

导入 android.view.ViewGroup;

导入 android.widget.Button;

导入 android.widget.Toast;

现在添加一个名为myString的单个字符串变量和一个名为myButton的按钮变量作为成员。

现在重写onCreate方法。在onCreate方法内,将myString初始化为Hello from SimpleFragment。到目前为止我们的代码(不包括包声明和import语句)将会像下面的代码一样:

public class SimpleFragment extends Fragment {
   // member variables accessible from anywhere in this 
   fragment
   String myString;
   Button myButton;

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

        myString = "Hello from SimpleFragment";
    }
}

在前面的代码中,我们创*了一个名为myString的成员变量,然后在onCreate方法中初始化它。这非常像我们在之前的应用程序中只使用Activity时所做的事情。

然而,不同之处在于我们没有设置视图或尝试获取对我们的Button成员变量myButton的引用。

在使用Fragment时,我们需要在onCreateView方法中执行此操作。现在让我们重写一下,看看我们如何设置视图并获取对我们的Button小部件的引用。

onCreate方法之后将此代码添加到SimpleFragment类中:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup
container, Bundle savedInstanceState) {
   View view = inflater.inflate(R.layout.fragment_layout, 
   container, false);
   myButton = view.findViewById(R.id.button);

   return view;
}

要理解上一段代码,我们首先必须查看onCreateView方法的签名。首先,注意方法的开始声明必须返回一个View类型的对象:

public View onCreateView...

接下来,我们有三个参数。让我们先看前两个:

(LayoutInflater inflater, ViewGroup container,...

我们需要一个LayoutInflater引用,因为我们不能调用setContentView方法,因为Fragment类没有提供这样的方法。在onCreateView的主体中,我们使用inflaterinflate方法来充气我们包含在fragment_layout.xml中的布局,并用结果初始化view(一个View类型的对象)。

我们在inflate方法中也使用了传入onCreateViewcontainercontainer变量是对activity_main.xml中的布局的引用。

可能会觉得activity_main.xml是包含的布局,但正如我们将在本章后面看到的那样,ViewGroup container参数允许任何具有任何布局的Activity成为我们片段的容器。这是非常灵活的,使我们的Fragment代码在很大程度上可重用。

我们传递给inflate的第三个参数是false,这意味着我们不希望我们的布局立即添加到包含的布局中。我们很快将从代码的另一个部分自己做这个。

onCreateView的第三个参数是Bundle savedInstanceState,它可以帮助我们维护片段保存的数据。

现在我们有了包含在view中的充气布局,我们可以使用它来获取对我们的Button小部件的引用,就像这样:

myButton = view.findViewById(R.id.button);

并将view实例用作调用代码的返回值,如有需要:

return view;

现在我们可以添加一个匿名类来监听我们按钮上的点击,就像通常一样。在onClick方法中,我们显示一个弹出的Toast消息,以演示一切都按预期工作。

onCreateView方法的return语句之前添加此代码,如下面的代码所示:

@Override
public View onCreateView(LayoutInflater inflater, 
ViewGroup container, 
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout,
container, false);
myButton = view.findViewById(R.id.button);
myButton.setOnClickListener(
   new View.OnClickListener() {

      @Override
      public void onClick(View v) {
               Toast.makeText(getActivity(),
myString , 
               Toast.LENGTH_SHORT).
               show();
                 }
          });
return view;
}

注意

作为提醒,getActivity()方法调用作为makeText中的参数,获取了包含FragmentActivity的引用。这是显示Toast消息所需的。我们在 Note to Self 应用程序中的FragmentDialog类中也使用了getActivity方法。

我们现在还不能运行我们的应用程序;它不会工作,因为还需要一步。我们需要创*一个SimpleFragment类的实例并适当地初始化它。这就是FragmentManager类将被介绍的地方。

这段代码通过调用getSupportFragmentManager创*了一个新的FragmentManager。然后,代码根据我们的SimpleFragment类创*了一个新的Fragment,并使用FragmentManager传入了将容纳它的布局(在Activity内部)的 ID。

MainActivity.javaonCreate方法中添加此代码,就在调用setContentView方法之后:

// Get a fragment manager
FragmentManager fManager = getSupportFragmentManager();
// Create a new fragment using the manager
// Passing in the id of the layout to hold it
Fragment frag = fManager.findFragmentById(R.id.fragmentHolder);
// Check the fragment has not already been initialized
if(frag == null){
     // Initialize the fragment based on our SimpleFragment
     frag  = new SimpleFragment();
     fManager.beginTransaction()
               .add(R.id.fragmentHolder, frag)
               .commit();
}

注意

您需要将以下import语句添加到MainActivity类中:

import androidx.fragment.app.Fragment;

import androidx.fragment.app.FragmentManager;

import android.os.Bundle;

现在运行应用程序,惊叹于我们的可点击按钮,它使用Toast类显示消息,并且需要两个布局和两个完整的类来创*:

图 24.14 - 使用 Toast 类显示消息

图 24.14 - 使用 Toast 类显示消息

如果你记得在*第二章**中实现了更多的功能,而且代码更少,那么很明显我们需要对 Fragment 进行现实检查,以充分理解为什么我们要这样做的答案!

Fragment 现实检查

那么,这个Fragment到底对我们有什么作用呢?我们的第一个Fragment迷你应用程序如果没有使用Fragment,外观和功能将是一样的。

事实上,使用Fragment类使整个事情变得更加复杂!我们为什么要这样做呢?

我们知道Fragment实例或片段可以添加到Activity的布局中。

我们知道Fragment不仅包含自己的布局(视图),还包含自己的代码(控制器),虽然由Activity托管,但Fragment实例几乎是独立的。

我们的快速应用程序只显示了一个Fragment实例在运行中,但我们可以有一个Activity来托管两个或更多的片段。然后,我们在单个屏幕上有效地有两个几乎独立的控制器。

然而,最有用的是,当Activity启动时,我们可以检测我们的应用程序运行的设备的属性 - 可能是手机或*板电脑;纵向或横向。然后,我们可以使用这些信息来决定同时显示一个或两个片段。

这不仅帮助我们实现本章开头讨论的真实应用程序部分中讨论的功能,而且还允许我们使用完全相同的Fragment代码来实现两种可能的情况!

这确实是片段的本质。我们通过将功能(控制器)和外观(视图)配对成一堆片段来创*一个完整的应用程序,我们可以以几乎不费吹灰之力的方式以不同的方式重复使用它们。

当然,可以预见到一些障碍,所以看看这个常见问题解答。

经常问的问题

  1. 缺失的环节是,如果所有这些片段都是完全独立的控制器,那么我们需要更多地了解如何实现我们的模型层。如果我们只是有一个ArrayList,就像“Note to Self”应用程序一样,ArrayList实例将会放在哪里?我们如何在片段之间共享它(假设所有片段都需要访问相同的数据)?

我们可以使用一种更加优雅的解决方案来创*一个模型层(数据本身和维护数据的代码)。当我们探索第二十六章**中的 NavigationView 布局,以及第二十七章**中的 Android 数据库*时,我们将看到这一点。

总结

现在我们对 Fragment 的用途有了广泛的了解,以及我们如何开始使用它们,我们可以开始深入了解它们的使用方式。在下一章中,我们将完成几个以不同方式使用多个 Fragment 的应用程序。

第二十五章:构*一个简单的图片库应用

RecyclerView小部件,我们可以有选择地加载当前页面所需的数据,也许是前一页和下一页的数据。

正如您所期望的,Android API 提供了一些解决方案来以相当简单的方式实现分页。

在本章中,我们将学*如何做到以下几点:

  • 实现像照片库应用中可能找到的图片一样的分页和滑动。

首先,让我们看一个滑动的例子。

技术要求

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

愤怒的小鸟经典滑动菜单

在这里,我们可以看到著名的愤怒的小鸟关卡选择菜单展示了滑动/分页的效果:

图 25.1 – 愤怒的小鸟关卡选择菜单

图 25.1 – 愤怒的小鸟关卡选择菜单

让我们构*一个分页应用。

构*一个图片库/幻灯片应用

在 Android Studio 中创*一个名为Image Pager的新项目。使用空活动模板,并将其余设置保持默认。

这些图片位于第二十五章/Image Pager/drawable文件夹中的下载包中。下一张截图显示了它们在 Windows 文件资源管理器中的情况:

图 25.2 – 添加图片

图 25.2 – 添加图片

将图片添加到 Android Studio 中的 Project Explorer 中的drawable文件夹中,或者您可以添加更有趣的图片,也许是您拍摄的一些照片。

实现布局

对于一个简单的图片分页应用,我们使用PagerAdapter类。我们可以将其视为像RecyclerAdapter一样,但用于图片,因为它将处理ViewPager小部件中的图像数组的显示。这很像RecyclerAdapter如何处理ListViewArrayList的内容的显示。我们只需要重写适当的方法。

要使用PagerAdapter实现图片库,我们首先需要在主布局中添加一个ViewPager小部件。为了确切地了解需要什么,这是activity_main.xml的实际 XML 代码。编辑activity_main.xml使其看起来完全像这样:

<RelativeLayout xmlns:android=
   "http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >
    <androidx.viewpager.widget.ViewPager
        android:id="@+id/pager"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

名为androidx.ViewPager.widget.ViewPager的类是在ViewPager之前发布的 Android 版本中提供此功能的类。

接下来,就像我们需要一个布局来表示列表项一样,我们需要一个布局来表示一个项目,这里是一个图片,在ViewPager中。以通常的方式创*一个新的布局文件,命名为pager_item.xml。添加一个带有 ID 为imageViewImageView

使用可视化设计工具来实现这一点,或者将以下 XML 复制到pager_item.xml中:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

现在我们可以开始编写我们的PagerAdapter类了。

编写 PagerAdapter 类

接下来,我们需要扩展PagerAdapter来处理图片。创*一个名为ImagePagerAdapter的新类,并使其扩展PagerAdapter

将以下导入添加到ImagePagerAdapter类的顶部。我们经常依赖使用Alt + Enter快捷键来添加导入。这次我们做的有些不同,因为有一些非常相似的类,它们不适合我们的目标。

将以下导入添加到ImagePagerAdapter类中:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;

这是类声明,添加了extends...代码以及一些成员变量。这些变量是我们不久将要使用的Context对象和一个名为imagesint数组。之所以使用int数组来存储图像,是因为我们将为每个图像存储int标识符。我们将在几个代码块后看到这是如何工作的。最后一个成员变量是一个LayoutInflater实例,您可能猜到它将用于填充pager_item.xml的每个实例。

扩展PagerAdapter类并添加我们刚刚讨论过的成员变量:

public class ImagePagerAdapter extends PagerAdapter {
    Context context;
    int[] images;
    LayoutInflater inflater;
}

现在我们需要一个构造函数,通过从MainActivity类接收Context以及图像的int数组并用它们初始化成员变量来设置ImagerPagerAdapter

ImagePagerAdapter类中添加构造方法:

public ImagePagerAdapter(
   Context context,  int[] images) {

   this.context = context;
   this.images = images;
}

现在我们必须重写PagerAdapter的必需方法。在上一个代码之后,添加重写的getCount方法,它简单地返回数组中图像 ID 的数量。这个方法是类内部使用的:

@Override
public int getCount() {
   return images.length;
}

现在我们必须重写isViewFromObject方法,根据当前View实例是否与传入的参数作为当前Object相关联,返回一个布尔值。同样,这是一个类内部使用的方法。在上一个代码之后,添加这个Override方法:

@Override
public boolean isViewFromObject(View view, Object object) {
   return view == object;
}

现在我们必须重写instantiateItem方法;这是我们要做的大部分工作。首先,我们声明一个新的ImageView对象,然后初始化我们的LayoutInflater成员。接下来,我们使用LayoutInflater实例从我们的pager_item.xml布局文件中声明和初始化一个新的View实例。

在这之后,我们获取pager_item.xml布局中ImageView小部件的引用。现在,根据instantiateItem方法的position参数和images数组中的适当 ID 整数,我们可以将适当的图像添加为ImageView小部件的内容。

最后,我们使用addView方法将布局添加到PagerAdapter实例,并从该方法返回。

添加我们刚刚讨论过的方法:

@Override
public Object instantiateItem(
   ViewGroup container, int position) {
   ImageView image;
   inflater = (LayoutInflater) 
         context.getSystemService(
         Context.LAYOUT_INFLATER_SERVICE);

   View itemView = inflater.inflate(
         R.layout.pager_item, container, false);
   // get reference to imageView in pager_item layout
   image = 
         itemView.findViewById(R.id.imageView);
   // Set an image to the ImageView
   image.setImageResource(images[position]);
   // Add pager_item layout as the current page to the 
   ViewPager
   ((ViewPager) container).addView(itemView);
   return itemView;
}

我们必须重写的最后一个方法是destroyItem方法,当类需要根据position参数的值移除适当的项时,可以调用该方法。

在上一个代码之后,在ImagePagerAdapter类的闭合大括号之前添加destroyItem方法:

@Override
public void destroyItem(ViewGroup container, 
   int position, 
   Object object) {

   // Remove pager_item layout from ViewPager
   container.removeView((RelativeLayout) object);
}

正如我们在编写ImagePagerAdapter类时所看到的,它几乎没有什么。只是正确实现ImagePagerAdapter类使用的重写方法,以帮助在幕后顺利运行。

现在我们可以编写MainActivity类,它将使用我们的ImagePagerAdapter实现。

编写 MainActivity 类

最后,我们可以编写我们的MainActivity类。与ImagePagerAdapter类一样,为了清晰起见,手动将以下import语句添加到MainActivity.java类中,然后再类声明之前,如下所示:

import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;

我们需要一些成员变量。毫不奇怪,我们需要一个ViewPager实例,它将用于保存布局中ViewPager的引用。此外,我们还需要一个刚刚编码的类的ImagePagerAdapter引用。我们还需要一个int数组来保存图像 ID 的数组。

调整MainActivity类如下:

public class MainActivity extends AppCompatActivity {
   ViewPager viewPager;
   PagerAdapter adapter;
   int[] images;
   @Override
   public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          ...

所有其他的代码都放在onCreate方法中。我们使用每个添加到drawable-xhdpi文件夹中的图像来初始化我们的int数组。

我们通常使用findViewById方法来初始化ViewPager。我们还通过传入MainActivity类的引用和images数组来初始化我们的ImagePagerAdapter实例,这是我们之前编写的构造函数所需的。最后,我们使用setAdapter方法将适配器绑定到 pager。

编写onCreate方法,使其看起来像下面的代码:

@Override
public void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   // Grab all the images and stuff them in our array
   images = new int[] { R.drawable.image1,
                R.drawable.image2,
                R.drawable.image3,
                R.drawable.image4,
                R.drawable.image5,
                R.drawable.image6 };
   // get a reference to the ViewPager in the layout
   viewPager = findViewById(R.id.pager);
   // Initialize our adapter
   adapter = new 
          ImagePagerAdapter(MainActivity.this, images);
   // Binds the Adapter to the ViewPager
   viewPager.setAdapter(adapter);
}

现在我们准备运行应用程序。

运行图库应用程序

在这里,我们可以看到我们int数组中的第一张图片:

图 25.3 – 第一张图片

图 25.3 – 第一张图片

向右和向左轻轻滑动,看到图像*稳过渡的愉悦方式:

图 25.4 – 滑动查看图片

图 25.4 – 滑动查看图片

这就是本章的全部内容。让我们回顾一下我们所做的事情。

总结

在本章中,我们看到了如何使用分页器来创*简单的图库,只需几行代码和一些非常简单的布局。我们之所以能够如此轻松地实现这一点,是因为有了ImagePagerAdapter类。

在下一章中,我们将看到另一个非常酷的 UI 元素,它在许多最新的 Android 应用程序中使用,可能是因为它看起来很棒,而且非常实用,使用起来非常愉快。NavigationView布局将使我们能够设计和实现具有完全不同行为(代码)的不同布局。让我们来看看NavigationView布局。

第二十六章:使用导航抽屉和片段进行高级 UI

在本章中,我们将看到(可以说是)最先进的 UI。NavigationView小部件或导航抽屉,因为它滑出其内容的方式,可以通过在创*新项目时选择它作为模板来简单地创*。我们将这样做,然后我们将检查自动生成的代码并学*如何与其交互。然后,我们将使用我们对Fragment的所有了解来为每个“抽屉”填充不同的行为和视图。然后在下一章中,我们将学*数据库,为每个Fragment添加一些新功能。

以下是本章我们将要做的事情:

  • 介绍NavigationView

  • 开始使用简单的数据库应用程序

  • 基于自动生成的 Android Studio 模板实现NavigationView项目

  • NavigationView添加多个片段和布局

让我们来看看这个非常酷的 UI 模式。

技术要求

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

介绍 NavigationView

NavigationView有什么好处?嗯,可能会吸引你的第一件事是它可以看起来非常时尚。看看下一个屏幕截图,展示了 Google Play 应用中NavigationView的操作:

图 26.1–NavigationView 在操作中

图 26.1–NavigationView 在操作中

老实说,从一开始,我们的 UI 不会像 Google Play 应用程序中的那样花哨。但是我们的应用程序中将存在相同的功能。

这个 UI 的另一个很棒的地方是它在需要时滑动隐藏/显示自己的方式。正是因为这种行为,它可以是一个相当大的尺寸,使得它在放置选项时非常灵活,当用户完成后,它会完全消失,就像一个抽屉一样。

如果您还没有尝试过,我*议现在尝试一下 Google Play 应用程序,看看它是如何工作的。

您可以从屏幕的左边缘滑动手指,抽屉会慢慢滑出。当然,您也可以以相反的方向将其滑开。

在导航抽屉打开时,屏幕的其余部分会略微变暗(如前一个屏幕截图所示),帮助用户专注于提供的导航选项。

您还可以在打开导航抽屉时在任何地方点击,它会自动滑开,为应用程序的其余部分留出整个屏幕。

抽屉也可以通过点击左上角的菜单图标打开。

我们还可以调整和完善导航抽屉的行为,这是本章末尾我们将看到的。

检查简单的数据库应用程序

在本章中,我们将专注于创*NavigationView并用四个Fragment类实例及其各自的布局填充它。在下一章中,我们将学*并实现数据库功能。

数据库应用程序的屏幕如下。这是我们NavigationView布局的全部荣耀。请注意,当使用NavigationView Activity 模板时,默认情况下提供了许多选项和大部分外观和装饰。

图 26.2–NavigationView 布局

图 26.2–NavigationView 布局

四个主要选项是我们将添加到 UI 中的内容。它们是插入删除搜索结果。布局如下所示,并描述了它们的目的。

插入

第一个屏幕允许用户将人名和他们的年龄插入到数据库中:

图 26.3–插入

图 26.3–插入

这个简单的布局有两个EditText小部件和一个按钮。用户将输入姓名和年龄,然后点击插入按钮将它们添加到数据库中。

删除

这个屏幕更简单。用户将在EditText小部件中输入姓名,然后点击按钮:

图 26.4 – 删除

图 26.4 – 删除

如果输入的姓名在数据库中存在,则该条目(姓名和年龄)将被删除。

搜索

这个布局与上一个布局基本相同,但目的不同:

图 26.5 – 搜索

图 26.5 – 搜索

用户将在EditText小部件中输入姓名,然后点击搜索按钮。如果数据库中存在该姓名,则将显示该姓名以及匹配的年龄。

结果

这个屏幕显示了整个数据库中的所有条目:

图 26.6 – 结果

图 26.6 – 结果

让我们开始使用导航抽屉。

开始简单数据库项目

在 Android Studio 中创*一个新项目。将其命名为Age Database,使用Navigation Drawer Activity模板。在我们做任何其他事情之前,值得在模拟器上运行应用程序,看看作为模板的一部分自动生成了多少内容:

图 26.7 – 主页

图 26.7 – 主页

乍一看,它只是一个普通的布局,带有一个TextView小部件。但是从屏幕左边缘滑动或按菜单按钮,导航抽屉布局就会显现出来:

图 26.8 – 导航页面

图 26.8 – 导航页面

现在我们可以修改选项并为每个选项插入一个带有布局的Fragment。为了理解它是如何工作的,让我们检查一些自动生成的代码。

探索自动生成的代码和资源

打开res/menu文件夹。注意有一个额外的文件名为activity_main_drawer.xml。接下来的代码是从这个文件中摘录出来的,所以我们可以讨论它的内容:

<group android:checkableBehavior="single">
     <item
          android:id="@+id/nav_home"
          android:icon="@drawable/ic_menu_camera"
          android:title="@string/menu_home" />
     <item
          android:id="@+id/nav_gallery"
          android:icon="@drawable/ic_menu_gallery"
          android:title="@string/menu_gallery" />
     <item
          android:id="@+id/nav_slideshow"
          android:icon="@drawable/ic_menu_slideshow"
          android:title="@string/menu_slideshow" />
</group>

注意group标签中有四个item标签。现在注意从上到下的title标签与自动生成的导航抽屉菜单中的三个文本选项完全对应。还要注意,在每个item标签中,有一个id标签,因此我们可以在我们的 Java 代码中引用它们,以及一个icon标签,它对应于drawable文件夹中的一个图标,并且是在导航抽屉中选项旁边显示的图标。

还有一些我们不会使用的自动生成的文件。

让我们编写基于Fragment的类和它们的布局。

编写片段类和它们的布局

我们将创*四个类,包括加载布局的代码以及实际的布局,但在学*了下一章关于 Android 数据库之后,我们不会将任何数据库功能放入 Java 中。

在我们有了四个类和它们的布局之后,我们将看到如何从导航抽屉菜单中加载它们。到本章结束时,我们将拥有一个完全工作的导航抽屉,让用户在片段之间切换,但是片段在下一章之前实际上没有任何功能。

创*类和布局的空文件

通过右键单击layout文件夹并选择content_insert,第二个content_delete,第三个content_search和第四个content_results来创*四个带有垂直LinearLayout作为父视图的布局文件。除了LinearLayout选项和文件名之外,所有选项都可以保持默认值。

现在你应该有四个包含LinearLayout父视图的新布局文件。

让我们编写相关的 Java 类。

编写类

通过右键单击包含MainActivity.java文件的文件夹,并选择InsertFragmentDeleteFragmentSearchFragmentResultsFragment来创*四个新类。从名称上就可以明白哪些片段将显示哪些布局。

为了明确起见,让我们向每个类添加一些代码,使类扩展Fragment并加载其关联的布局。

打开InsertFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class InsertFragment extends Fragment {

   @Override
   public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

          View v = inflater.inflate(
                      R.layout.content_insert, 
                      container, false);

          // Database and UI code goes here in next chapter
          return v;
    }
}

打开DeleteFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class DeleteFragment extends Fragment {

   @Override
   public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

          View v = inflater.inflate(
                      R.layout.content_delete, 
                      container, false);

         // Database and UI code goes here in next chapter

         return v;
    }
}

打开SearchFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class SearchFragment extends Fragment{
   @Override
    public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

           View v = inflater.inflate(
                      R.layout.content_search,
                      container, false);

           // Database and UI code goes here in next 
           chapter

         return v;
    }
}

打开ResultsFragment.java并编辑它以包含以下代码:

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
public class ResultsFragment extends Fragment {
    @Override
    public View onCreateView(
                LayoutInflater inflater, 
                ViewGroup container, 
                Bundle savedInstanceState) {

         View v = inflater.inflate(
                      R.layout.content_results, 
                      container, false);
         // Database and UI code goes here in next chapter

         return v;
    }
}

每个类完全没有功能,除了在onCreateView方法中,从关联的布局文件加载适当的布局。

让我们向之前创*的布局文件添加 UI。

设计布局

正如我们在本章开始时所看到的,所有的布局都很简单。使您的布局与我的完全相同并不是必要的,但是 ID 值必须相同,否则我们在下一章中编写的 Java 代码将无法工作。

设计 content_insert.xml

从调色板的Text类别中拖放两个Plain Text小部件到布局中。请记住,Plain Text小部件是EditText实例。现在在两个Plain Text小部件之后将一个Button小部件拖放到布局中。

根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.9 - 插入布局

图 26.9 - 插入布局

设计 content_delete.xml

Plain Text拖放到布局中,下面是一个Button小部件。根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.10 - 删除布局

图 26.10 - 删除布局

设计 content_search.xml

将一个Plain Text,然后是一个按钮,然后是一个常规的TextView拖放到布局中,然后根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.11 - 搜索布局

图 26.11 - 搜索布局

设计 content_results.xml

将单个TextView小部件(这次不是Plain Text/EditText)拖放到布局中。我们将在下一章中看到如何将整个列表添加到这个单个TextView小部件中。

根据此表配置小部件:

这是您的布局在 Android Studio 的设计视图中应该是什么样子的:

图 26.12 - 结果布局

图 26.12 - 结果布局

现在我们可以使用基于Fragment的类及其布局。

使用 Fragment 类及其布局

这个阶段有三个步骤。首先,我们需要编辑导航抽屉布局的菜单,以反映用户的选项。接下来,我们需要在布局中添加一个View实例,以容纳当前Fragment实例,最后,我们需要在MainActivity.java中添加代码,以在用户点击菜单时在不同的Fragment实例之间切换。

编辑导航抽屉菜单

在项目资源管理器的res/menu文件夹中打开activity_main_drawer.xml文件。编辑我们之前看到的group标签内的代码,以反映我们的菜单选项插入删除搜索结果

<group android:checkableBehavior="single">
   <item
         android:id="@+id/nav_insert"
         android:icon="@drawable/ic_menu_camera"
         android:title="Insert" />
   <item
         android:id="@+id/nav_delete"
         android:icon="@drawable/ic_menu_gallery"
         android:title="Delete" />
   <item
         android:id="@+id/nav_search"
         android:icon="@drawable/ic_menu_slideshow"
         android:title="Search" />
   <item
         android:id="@+id/nav_results"
         android:icon="@drawable/ic_menu_camera"
         android:title="Results" />
</group>

请注意,结果项重用了相机图标。如果您希望添加自己的唯一图标,这是您的挑战。

现在我们可以在主布局中添加一个布局,以容纳当前活动的片段。

向主布局添加一个持有者

打开content_main.xml文件在layout文件夹中。找到以下现有的代码,这是当前不适合我们用途的当前片段持有者:

<fragment
     android:id="@+id/nav_host_fragment"
     android:name="androidx.navigation
          .fragment.NavHostFragment"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     app:defaultNavHost="true"
     app:layout_constraintLeft_toLeftOf="parent"
     app:layout_constraintRight_toRightOf="parent"
     app:layout_constraintTop_toTopOf="parent"
     app:navGraph="@navigation/mobile_navigation" />

删除前面的代码,并在ConstraintLayout的结束标签之前用以下 XML 代码替换它:

    <FrameLayout
        android:id="@+id/fragmentHolder"
        android:layout_width="368dp"
        android:layout_height="495dp"
        tools:layout_editor_absoluteX="8dp"
        tools:layout_editor_absoluteY="8dp">
    </FrameLayout>

切换到设计视图并单击推断约束按钮以固定新布局。

现在我们有一个id属性为fragmentHolderFrameLayout小部件,我们可以获取其引用并加载所有我们的Fragment实例布局。

编写 MainActivity.java 类

用以下内容替换所有现有的import指令:

import android.os.Bundle;
import com.google.android.material.
            floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
import android.view.View;
import com.google.android.material.navigation.
        NavigationView;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.FragmentTransaction;
import android.view.MenuItem;

打开MainActivity.java文件并编辑整个代码以匹配以下内容。

注意

最快的方法可能是删除除我们刚刚添加的import指令之外的所有内容。

接下来我们将讨论代码,因此请仔细研究变量名称和各种类及其相关方法。

public class MainActivity extends AppCompatActivity
        implements NavigationView.
OnNavigationItemSelectedListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "
                Replace with your own action", 
                Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
        DrawerLayout drawer = 
        findViewById(R.id.drawer_layout);
        ActionBarDrawerToggle toggle = new 
        ActionBarDrawerToggle(
                this, drawer, toolbar, 
                      R.string.navigation_drawer_open, 
                      R.string.navigation_drawer_close);

        drawer.addDrawerListener(toggle);
        toggle.syncState();
        NavigationView navigationView 
        = findViewById(R.id.nav_view);
        navigationView
        .setNavigationItemSelectedListener(this);
    }
}

在前面的代码中,onCreate方法处理了我们 UI 的一些方面。 代码获取了与我们刚刚看到的布局相对应的DrawerLayout小部件的引用。 代码还创*了一个ActionBarDrawerToggle的新实例,它允许控制/切换抽屉。 接下来,引用被捕获到导航抽屉本身的布局文件(nav_view),代码的最后一行设置了NavigationView上的监听器。

现在按照以下方式添加onBackPressed方法:

@Override
public void onBackPressed() {
     DrawerLayout drawer = 
     findViewById(R.id.drawer_layout);
     if (drawer.isDrawerOpen(GravityCompat.START)) {
          drawer.closeDrawer(GravityCompat.START);
     } else {
          super.onBackPressed();
     }
}

onBackPressed方法是 Activity 的一个重写方法,它处理用户在设备上按返回按钮时发生的情况。 代码关闭抽屉(如果打开),如果没有打开,则简单地调用super.onBackPressed。 这意味着如果抽屉打开,返回按钮将关闭抽屉,如果已经关闭,则具有默认行为。

添加onCreateOptionsMenuonOptionsItemSelected方法,这些方法在此应用程序中并没有真正使用,但将为options按钮添加默认功能:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
     // Inflate the menu; this adds items to the action bar 
     if it is present.
     getMenuInflater().inflate(R.menu.main, menu);
     return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
     // Handle action bar item clicks here. The action bar 
     will
     // automatically handle clicks on the Home/Up button, 
     so long
     // as you specify a parent activity in 
     AndroidManifest.xml.
     int id = item.getItemId();
     //noinspection SimplifiableIfStatement
     if (id == R.id.action_settings) {
          return true;
     }
     return super.onOptionsItemSelected(item);
}

现在添加下面显示的onNavigatioItemSelected方法:

@Override
public boolean onNavigationItemSelected(MenuItem item) {
     // Handle navigation view item clicks here.
     // Create a transaction
     FragmentTransaction transaction = 
          getSupportFragmentManager().beginTransaction();
     int id = item.getItemId();
     if (id == R.id.nav_insert) {
          // Create a new fragment of the appropriate type
          InsertFragment fragment = new InsertFragment();
          // What to do and where to do it
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     } else if (id == R.id.nav_search) {
          SearchFragment fragment = new SearchFragment();
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     } else if (id == R.id.nav_delete) {
          DeleteFragment fragment = new DeleteFragment();
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     }  else if (id == R.id.nav_results) {
          ResultsFragment fragment = new ResultsFragment();
          transaction.replace(R.id.fragmentHolder, 
          fragment);
     }
     // Ask Android to remember which
     // menu options the user has chosen
     transaction.addToBackStack(null);
     // Implement the change
     transaction.commit();
     DrawerLayout drawer = 
     findViewById(R.id.drawer_layout);
     drawer.closeDrawer(GravityCompat.START);
     return true;
}

让我们来看看onNavigationItemSelected方法中的代码。 大部分代码应该看起来很熟悉。 对于我们的每个菜单选项,我们都创*了一个相应类型的新Fragment,并将其插入到具有fragmentHolder属性值的RelativeLayout中。

最后,对于MainActivity.java文件,transaction.addToBackStack方法意味着所选的Fragment实例将被记住,以便与其他实例一起使用。 这样做的结果是,如果用户选择insert片段,然后选择results片段,然后点击返回按钮,那么应用程序将返回用户到insert片段。

现在可以运行应用程序并使用导航抽屉菜单在所有不同的Fragment实例之间切换。 它们看起来就像本章开头的屏幕截图一样,但目前还没有任何功能。

总结

在本章中,我们看到了拥有吸引人和令人愉悦的 UI 是多么简单,尽管我们的Fragment实例目前还没有任何功能,但一旦我们学会了数据库,它们就已经准备好了。

在下一章中,我们将学*关于数据库的一般知识,Android 应用程序可以使用的特定数据库,然后我们将为我们的Fragment类添加功能。

第二十七章:Android 数据库

如果我们要制作提供给用户重要功能的应用程序,那么我们几乎肯定需要一种管理、存储和过滤大量数据的方法。

使用 JSON 可以高效地存储大量数据,但当我们需要有选择地使用数据而不仅仅限制于“保存所有”和“加载所有”的选项时,我们需要考虑其他可用的选项。

一门优秀的计算机科学课程可能会教你处理排序和过滤数据所需的算法,但所需的工作量会相当大,我们能否想出与 Android API 提供的解决方案一样好的解决方案的机会有多大呢?

像往常一样,使用 Android API 中提供的解决方案是最合理的。正如我们所见,JSONSharedPreferences类有它们的用途,但在某个时候,我们需要转向使用真正的数据库来解决现实世界的问题。Android 使用 SQLite 数据库管理系统,正如您所期望的那样,有一个 API 可以使其尽可能简单。

在本章中,我们将做以下事情:

  • 确切地了解数据库是什么

  • 了解 SQL 和 SQLite 是什么

  • 学* SQL 语言的基础知识

  • 看一下 Android SQLite API

  • 编写在上一章开始的 Age Database 应用程序

技术要求

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

数据库 101

让我们回答一大堆与数据库相关的问题,然后我们就可以开始制作使用 SQLite 的应用程序。

什么是数据库?

数据库既是存储的地方,也是检索、存储和操作数据的手段。在学*如何使用之前,能够想象数据库是有帮助的。实际上,数据库内部的结构因所涉及的数据库而异。SQLite 实际上将所有数据存储在一个单个文件中。

然而,如果我们将我们的数据视为电子表格,或者有时是多个电子表格,它会极大地帮助我们理解。我们的数据库,就像电子表格一样,将被分成多个列,代表不同类型的数据,和行,代表数据库的条目。

想象一个具有姓名和考试成绩的数据库。看一下这种数据的可视化表示,并想象它在数据库中会是什么样子:

图 27.1 – 数据库示例

图 27.1 – 数据库示例

然而,请注意,还有一列额外的数据:一个ID列。随着我们的进行,我们将更多地谈论这个。这种类似电子表格的结构称为。如前所述,数据库中可能有多个表。表的每一列都将有一个名称,在与数据库交谈时可以引用该名称。

什么是 SQL?

SQL代表Structured Query Language。这是用于处理数据库的语法。

什么是 SQLite?

SQLite 是 Android 所青睐的数据库系统的名称,并且它有自己的 SQL 版本。SQLite 版本的 SQL 需要稍微不同的原因是数据库具有不同的特性。

接下来的 SQL 语法入门将专注于 SQLite。

SQL 语法入门

在我们学*如何在 Android 中使用 SQLite 之前,我们需要首先学*如何在一般情况下使用 SQLite 的基础知识。

让我们看一些示例 SQL 代码,可以直接在 SQLite 数据库上使用,而不需要任何 Java 或 Android 类;然后我们可以更容易地理解我们的 Java 代码在后面做什么。

SQLite 示例代码

SQL 有关键字,就像 Java 一样,会引起一些事情发生。以下是一些我们很快将要使用的 SQL 关键字的例子:

  • INSERT:允许我们向数据库添加数据

  • DELETE:允许我们从数据库中删除数据

  • SELECT:允许我们从数据库中读取数据

  • WHERE:允许我们指定数据库的部分,匹配特定条件,我们想要在其上使用INSERTDELETESELECT

  • FROM:用于指定数据库中的表或列名

注意

SQLite 的关键字远不止这些;要查看完整的关键字列表,请查看此链接:sqlite.org/lang_keywords.html

除了关键字之外,SQL 还有类型。以下是一些 SQL 类型的示例:

  • 整数:正好适合存储整数

  • 文本:非常适合存储简单的姓名或地址

  • 实数:用于存储大浮点数

注意

SQLite 的类型远不止这些;要查看完整的类型列表,请查看此链接:www.sqlite.org/datatype3.html

让我们看看如何将这些类型与关键字结合起来,使用完整的 SQLite 语句创*表格并添加、删除、修改和读取数据。

创*表格

我们可能会问为什么我们不先创*一个新的数据库。原因是每个 Android 应用程序默认都可以访问一个 SQLite 数据库。该数据库对该应用程序是私有的。以下是我们在该数据库中创*表格的语句。我已经突出显示了一些部分,以便更清楚地理解语句:

create table StudentsAndGrades 
   _ID integer primary key autoincrement not null,
   name text not null,
   score int;

上述代码创*了一个名为StudentsAndGrades的表,其中有一个整数行 ID,每次添加一行数据时都会自动增加(递增)。

该表还将有一个name列,其类型为text,并且不能为空(not null)。

它还将有一个score列,其类型为int。同时,注意语句以分号结束。

向数据库中插入数据

以下是我们如何向数据库插入一行新数据的方式:

INSERT INTO StudentsAndGrades
   (name, score)
   VALUES
   ("Bart", 23);

上述代码向数据库添加了一行。在上述语句之后,数据库将有一个条目,其列(_IDnamescore)的值为(1Bart23)。

以下是我们如何向数据库插入另一行新数据的方式:

INSERT INTO StudentsAndGrades
   (name, score)
   VALUES
   ("Lisa", 100);

上述代码添加了一个新的数据行,其列(_IDnamescore)的值为(2Lisa100)。

我们的类似电子表格的结构现在看起来如下:

图 27.2 - 更新后的电子表格

图 27.2 - 更新后的电子表格

从数据库中检索数据

以下是我们如何从数据库中访问所有的行和列:

SELECT * FROM StudentsAndGrades;

上述代码要求每一行和每一列。*符号可以理解为“所有”。

我们也可以更加有选择性,就像这段代码所示:

SELECT score FROM StudentsAndGrades
     where name = "Lisa";

上述代码只会返回100,这当然是与姓名Lisa相关联的分数。

更新数据库结构

即使在创*表格并添加数据后,我们仍然可以添加新的列。就 SQL 而言,这很简单,但可能会导致已发布应用程序的用户数据出现一些问题。下一个语句添加了一个名为age的新列,其类型为int

ALTER TABLE StudentsAndGrades
     ADD 
     age int;

有许多数据类型、关键字和使用它们的方式,比我们目前所见到的要多。接下来,让我们看一下 Android SQLite API;我们将开始看到如何使用我们的新的 SQLite 技能。

Android SQLite API

Android API 有许多不同的方式,使得使用我们应用程序的数据库变得相当容易。我们需要首先熟悉的是SQLiteOpenHelper类。

SQLiteOpenHelper 和 SQLiteDatabase

SQLiteDatabase类是表示实际数据库的类。然而,SQLiteOpenHelper类是大部分操作发生的地方。这个类将使我们能够访问数据库并初始化SQLiteDatabase的实例。

此外,SQLiteOpenHelper,我们将在我们的 Age Database 应用程序中扩展它,有两个要重写的方法。首先,它有一个onCreate方法,当第一次使用数据库时被调用;因此,我们将把用于创*表结构的 SQL 放在其中是有意义的。

我们必须重写的另一个方法是onUpgrade,你可能已经猜到,它在我们升级数据库时被调用(使用ALTER来改变其结构)。

构*和执行查询

随着我们的数据库结构变得更加复杂,以及我们的 SQL 知识的增长,我们的 SQL 语句会变得非常长和笨拙。出现错误的可能性很高。

我们将帮助解决复杂性问题的方法是将查询从各个部分构*成一个字符串。然后我们可以将该字符串传递给执行查询的方法。

此外,我们将使用final字符串来表示诸如表和列名之类的东西,这样我们就不会与它们搞混。

例如,我们可以声明以下成员,它们将代表之前虚构示例中的表名和列名。请注意,我们还将为数据库本身命名,并为其设置一个字符串:

private static final String DB_NAME = "MyCollegeDB";
private static final String TABLE_S_AND_G = " StudentsAndGrades";
public static final String TABLE_ROW_ID = "_id";
public static final String TABLE_ROW_NAME = "name";
public static final String TABLE_ROW_SCORE = "score";

请注意在前面的代码中,我们将受益于在类外部访问字符串,因为我们将它们声明为public。你可能会认为这违反了封装的规则。的确如此,但当类的意图是尽可能广泛地使用时,这是可以接受的。而且请记住,所有的变量都是 final 的。使用这些字符串变量的外部类不能改变它们或搞乱它们。它们只能引用和使用它们所持有的值。

然后我们可以像下面的示例一样构*一个查询。该示例向我们的假设数据库添加了一个新条目,并将 Java 变量合并到 SQL 语句中:

String name = "Onkar";
int score = 95;
// Add all the details to the table
String query = "INSERT INTO " + TABLE_S_AND_G + " (" +
         TABLE_ROW_NAME + ", " +
         TABLE_ROW_SCORE +
         ") " +
         "VALUES (" +
         "'" + name + "'" + ", " +
         score +
         ");"; 

请注意在前面的代码中,常规的namescore Java 变量被突出显示。之前的名为query的字符串现在是 SQL 语句,与此完全相同:

INSERT INTO StudentsAndGrades (
   name, score)
   VALUES ('Onkar',95);

注意

要学* Android 编程并不一定要完全掌握前两个代码块。但是,如果你想构*自己的应用程序并构造确切需要的 SQL 语句,理解这些代码块将有所帮助。为什么不学*前两个代码块,以便区分双引号",它们是用+连接在一起的字符串的一部分;单引号',它们是 SQL 语法的一部分;常规的 Java 变量;以及字符串和 Java 中 SQL 语句中的不同分号。

在输入查询时,Android Studio 会提示我们变量的名称,这样错误的几率就会降低,尽管它比简单地输入查询更冗长。

现在我们可以使用之前介绍的类来执行查询:

// This is the actual database
private SQLiteDatabase db;
// Create an instance of our internal CustomSQLiteOpenHelper class
CustomSQLiteOpenHelper helper = new
   CustomSQLiteOpenHelper(context);
// Get a writable database
db = helper.getWritableDatabase();
// Run the query
db.execSQL(query);

在向数据库添加数据时,我们将像前面的代码一样使用execSQL;在从数据库获取数据时,我们将使用rawQuery方法,如下所示:

Cursor c = db.rawQuery(query, null); 

请注意,rawQuery方法返回Cursor类型的对象。

注意

我们可以用几种不同的方式与 SQLite 交互,它们各有优缺点。我们选择使用原始的 SQL 语句,因为这样可以完全透明地展示我们正在做什么,同时加强我们对 SQL 语言的了解。如果你想了解更多,请参阅下一个提示。

数据库游标

除了让我们访问数据库的类和允许我们执行查询的方法之外,还有一个问题,那就是我们从查询中得到的结果如何格式化。

幸运的是,有Cursor类。我们所有的数据库查询都会返回Cursor类型的对象。我们可以使用Cursor类的方法有选择地访问从查询返回的数据,如下所示:

Log.i(c.getString(1), c.getString(2)); 

以前的代码将输出到 logcat 中查询返回的前两列中存储的两个值。决定我们当前正在读取的返回数据的哪一行是Cursor对象本身。

我们可以访问Cursor对象的许多方法,包括moveToNext方法,该方法将Cursor移动到下一行,准备读取:

c.moveToNext();
/*
   This same code now outputs the data in the
   first and second column of the returned 
   data but from the SECOND row.
*/
Log.i(c.getString(1), c.getString(2));

在某些情况下,我们将能够将Cursor绑定到我们 UI 的一部分(例如RecyclerView),就像我们在 Note to Self 应用程序中使用ArrayList实例一样,然后将一切留给 Android API。

Cursor类还有许多有用的方法,其中一些我们很快就会看到。

注意

这是对 Android SQLite API 的介绍实际上只是触及了它的能力表面。随着我们进一步进行,我们将遇到更多的方法和类。然而,如果您的应用想法需要复杂的数据管理,进一步研究是值得的。

现在我们可以看到所有这些理论是如何结合在一起的,以及我们将如何在 Age Database 应用程序中构*我们的数据库代码结构。

编写数据库类

在这里,我们将实践我们迄今为止学到的一切,并完成编写 Age Database 应用程序。在我们之前的部分的Fragment类可以与共享数据库进行交互之前,我们需要一个类来处理与数据库的交互和创*。

我们将创*一个通过使用SQLiteOpenHelper类来管理我们的数据库的类。它还将定义一些final字符串来表示表的名称和其列。此外,它将提供一堆我们可以调用的辅助方法来执行所有必要的查询。在必要时,这些辅助方法将返回一个Cursor对象,我们可以用来显示我们检索到的数据。如果我们的应用程序需要发展,添加新的辅助方法将是微不足道的。

创*一个名为DataManager的新类,并添加以下成员变量:

import android.database.sqlite.SQLiteDatabase;
public class DataManager {
    // This is the actual database
    private SQLiteDatabase db;
    /*
        Next we have a public static final string for
        each row/table that we need to refer to both
        inside and outside this class
    */
    public static final String TABLE_ROW_ID = "_id";
    public static final String TABLE_ROW_NAME = "name";
    public static final String TABLE_ROW_AGE = "age";
    /*
        Next we have a private static final strings for
        each row/table that we need to refer to just
        inside this class
    */
    private static final String DB_NAME = "name_age_db";
    private static final int DB_VERSION = 1;
    private static final String TABLE_N_AND_A = 
                                   "name_and_age";
}

接下来,我们添加一个构造函数,它将创*我们的自定义版本的SQLiteOpenHelper的实例。我们很快将实现这个类作为一个内部类。构造函数还初始化了我们的SQLiteDatabase引用db成员。

将我们刚刚讨论过的以下构造函数添加到DataManager类中:

public DataManager(Context context) {
   // Create an instance of our internal 
   CustomSQLiteOpenHelper 

   CustomSQLiteOpenHelper helper = new 
      CustomSQLiteOpenHelper(context);
   // Get a writable database
   db = helper.getWritableDatabase();
}

现在我们可以添加我们将从 Fragment 类中访问的辅助方法。从insert方法开始,它根据传入方法的nameage参数执行INSERT SQL 查询。

insert方法添加到DataManager类中:

// Here are all our helper methods
// Insert a record
public void insert(String name, String age){
   // Add all the details to the table
   String query = "INSERT INTO " + TABLE_N_AND_A + " (" +
                  TABLE_ROW_NAME + ", " +
                  TABLE_ROW_AGE +
                  ") " +
                  "VALUES (" +
                  "'" + name + "'" + ", " +
                  "'" + age + "'" +
                  ");";
   Log.i("insert() = ", query);
   db.execSQL(query);
}

下一个名为delete的方法将从数据库中删除一条记录,如果它在名称列中具有与传入的name参数匹配的值。它使用 SQLDELETE关键字来实现这一点。

delete方法添加到DataManager类中:

// Delete a record
public void delete(String name){
   // Delete the details from the table if already exists
   String query = "DELETE FROM " + TABLE_N_AND_A +
                  " WHERE " + TABLE_ROW_NAME +
                  " = '" + name + "';";
   Log.i("delete() = ", query);
   db.execSQL(query);
}

接下来,我们有selectAll方法,它也如其名称所示。它使用SELECT查询并使用*参数来实现这一点,该参数相当于单独指定所有列。还要注意,该方法返回一个Cursor实例,我们将在一些Fragment类中使用。

selectAll方法添加到DataManager类中:

// Get all the records
public Cursor selectAll() {
   Cursor c = db.rawQuery("SELECT *" +" from " +
                TABLE_N_AND_A, null);
   return c;
}

现在我们添加一个searchName方法,该方法具有一个String参数,用于用户想要搜索的名称。它还返回一个包含找到的所有条目的Cursor实例。请注意,SQL 语句使用SELECTFROMWHERE来实现这一点:

// Find a specific record
public Cursor searchName(String name) {
   String query = "SELECT " +
                  TABLE_ROW_ID + ", " +
                  TABLE_ROW_NAME +
                  ", " + TABLE_ROW_AGE +
                  " from " +
                  TABLE_N_AND_A + " WHERE " +
                  TABLE_ROW_NAME + " = '" + name + "';";
   Log.i("searchName() = ", query);
   Cursor c = db.rawQuery(query, null);
   return c;
}

最后,对于DataManager类,我们创*一个内部类,它将是我们的SQLiteOpenHelper的实现。这是一个最基本的实现。

我们有一个构造函数,接收一个Context对象,数据库名称和数据库版本。

我们还重写了onCreate方法,其中包含创*具有_IDnameage列的数据库表的 SQL 语句。

onUpgrade方法在此应用程序中被故意留空。

将内部的CustomSQLiteOpenHelper类添加到DataManager类中:

// This class is created when our DataManager is initialized
private class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
   public CustomSQLiteOpenHelper(Context context) {
         super(context, DB_NAME, null, DB_VERSION);
   }
   // This runs the first time the database is created
   @Override
   public void onCreate(SQLiteDatabase db) {
         // Create a table for photos and all their details
         String newTableQueryString = "create table "
                      + TABLE_N_AND_A + " ("
                      + TABLE_ROW_ID
                      + " integer primary key 
                      autoincrement not null,"
                      + TABLE_ROW_NAME
                      + " text not null,"
                      + TABLE_ROW_AGE
                      + " text not null);";
         db.execSQL(newTableQueryString);

   }
   // This method only runs when we increment DB_VERSION
   @Override
   public void onUpgrade(SQLiteDatabase db, 
int oldVersion, int newVersion) {
// Not needed in this app
// but we must still override it
   }
}

现在我们可以在我们的Fragment类中添加代码来使用我们的新的DataManager类。

编写 Fragment 类以使用 DataManager 类

将这段突出显示的代码添加到InsertFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_insert, 
   container, false);
final DataManager dm = 
   new DataManager(getActivity());
Button btnInsert = 
   v.findViewById(R.id.btnInsert);

final EditText editName = 
   v.findViewById(R.id.editName);

final EditText editAge = 
   v.findViewById(R.id.editAge);
btnInsert.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          dm.insert(editName.getText().toString(),
                       editAge.getText().toString());
   }
});
return v;

在代码中,我们获取了我们的DataManager类的实例和对每个 UI 小部件的引用。然后,在onClick方法中,我们使用insert方法向数据库添加新的姓名和年龄。要插入的值来自两个EditText小部件。

将这段突出显示的代码添加到DeleteFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_delete, 
   container, false);
final DataManager dm = 
   new DataManager(getActivity());
Button btnDelete = 
   v.findViewById(R.id.btnDelete);

final EditText editDelete = 
   v.findViewById(R.id.editDelete);
btnDelete.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          dm.delete(editDelete.getText().toString());
   }
});
return v;

DeleteFragment类中,我们创*了我们的DataManager类的实例,然后从我们的布局中获取了EditTextButton小部件的引用。当按钮被点击时,将调用delete方法,传入用户输入的EditText小部件中的任何文本的值。delete方法搜索我们的数据库是否有匹配项,如果找到,则删除它。

将这段突出显示的代码添加到SearchFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_search,
   container,false);
Button btnSearch = 
   v.findViewById(R.id.btnSearch);

final EditText editSearch = 
   v.findViewById(R.id.editSearch);

final TextView textResult = 
   v.findViewById(R.id.textResult);
// This is our DataManager instance
final DataManager dm = 
   new DataManager(getActivity());
btnSearch.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
          Cursor c = dm.searchName(
                     editSearch.getText().toString());
// Make sure a result was found before using the 
          Cursor
          if(c.getCount() > 0) {
                 c.moveToNext();
textResult.setText("Result = " + 
c.getString(1) + " - " + 
                     c.getString(2));
          }
   }
});
return v;

与我们所有不同的Fragment类一样,我们创*了DataManager类的实例,并获取了布局中所有不同 UI 小部件的引用。在onClick方法中,使用searchName方法,传入EditText小部件的值。如果数据库在Cursor实例中返回结果,那么TextView小部件使用其setText方法输出结果。

将这段突出显示的代码添加到ResultsFragment类中以更新onCreateView方法:

View v = inflater.inflate(R.layout.content_results, 
   container, false);
// Create an instance of our DataManager
DataManager dm = 
   new DataManager(getActivity());
// Get a reference to the TextView to show the results
TextView textResults = 
   v.findViewById(R.id.textResults);
// Create and initialize a Cursor with all the results
Cursor c = dm.selectAll();
// A String to hold all the text
String list = "";
// Loop through the results in the Cursor
while (c.moveToNext()){
   // Add the results to the String
   // with a little formatting
   list+=(c.getString(1) + " - " + c.getString(2) + "\n");
}
// Display the String in the TextView
textResults.setText(list);
return v;

在这个类中,Cursor实例在任何交互发生之前使用selectAll方法加载数据。然后通过连接结果将Cursor的内容输出到TextView小部件中。在连接中的\n是在Cursor实例中的每个结果之间创*新行的。

运行 Age Database 应用程序

让我们运行一些我们应用程序的功能,以确保它按预期工作。

首先,我使用插入菜单选项向数据库添加了一个新的名字:

图 27.3 – 插入菜单

图 27.3 – 插入菜单

然后我通过查看结果选项确认它确实存在:

图 27.4 – 结果选项

图 27.4 – 结果选项

之后,我添加了一些更多的姓名和年龄,只是为了填充数据库:

图 27.5 – 填充数据库

图 27.5 – 填充数据库

然后我使用了删除菜单选项,再次查看结果选项,以确保我选择的名字确实被删除了。

图 27.6 – 删除菜单

图 27.6 – 删除菜单

然后我搜索了一个我知道存在的名字来测试搜索菜单选项:

图 27.7 – 搜索菜单

图 27.7 – 搜索菜单

让我们回顾一下本章我们所做的事情。

摘要

在本章中,我们涵盖了很多内容。我们学*了关于数据库,特别是 Android 应用程序使用的数据库 SQLite。我们练*了使用 SQL 语言与数据库进行通信的基础知识。

我们已经看到了 Android API 如何帮助我们使用 SQLite 数据库,并实现了我们的第一个使用数据库的工作应用程序。

你已经走了很长的路,已经到达了书的尽头。让我们谈谈接下来可能会发生什么。

第二十八章:在你离开之前快速聊一下

我们的旅程就快结束了。这一章提供了一些想法和指针,你可能在匆忙制作自己的应用之前想要看一看:

  • 发布

  • 制作你的第一个应用

  • 继续学*

  • 谢谢

发布

你已经足够了解如何设计你自己的应用。你甚至可以对本书中的应用进行一些修改。

我决定不提供在 Google Play 商店上发布的逐步指南,因为这些步骤并不复杂。然而,它们相当深入和有点费力。大部分步骤涉及输入关于你和你的应用的个人信息以及图片。这样的教程可能会是这样的:

  1. 填写这个文本框。

  2. 现在填写这个文本框。

  3. 上传这张图片。

  4. 等等。

这样做不太有趣,也不太有用。

要开始,你只需要访问play.google.com/apps/publish并支付一次性的适度费用(大约 25 美元,根据你所在地区的货币而定)。这样你就可以终身发布游戏。

注意

如果你想要一个发布的清单,可以查看这个链接,https://developer.android.com/distribute/best-practices/launch/launch-checklist.html,但你会发现这个过程很直观(尽管很冗长)。

制作一个应用!

如果你只是把这一件事付诸实践,你就可以忽略这一章中的其他一切:

不要等到你成为专家才开始制作应用!

开始构*你的梦想应用,一个拥有所有功能的应用,将会在 Google Play 上风靡一时。然而,一个简单的*议是:先做一些规划!但不要太多;然后开始吧。

在一旁有一些更小、更容易实现的项目:你可以向朋友和家人展示这些项目,并探索你还不熟悉的 Android 领域。如果你对这些应用有信心,你可以将它们上传到 Google Play。如果你担心它们会被评论员接受,那就把它们免费发布,并在描述中注明“只是一个原型”或类似的内容。

如果你的经历和我的一样,你会发现当你阅读、学*和制作应用时,你会发现你的梦想应用可以在很多方面得到改进,你可能会被激发重新设计它,甚至重新开始。

如果你这样做,我可以保证下一次构*应用时,你会用一半的时间做出两倍好的成果,至少是这样!

继续学*

如果你觉得自己已经走了很长的路,那么你是对的。然而,总是有更多东西需要学*。

继续阅读

你会发现,当你制作你的第一个应用时,你会突然意识到你的知识中存在一个需要填补的空白,以使某个功能得以实现。这是正常的,也是可以预料的,所以不要让它吓到你。想一想如何描述这个问题,并在谷歌上搜索解决方案。

你可能会发现项目中的特定类会变得超出实际和可维护的范围。这表明有更好的方式来构*结构,并且可能有一个现成的设计模式可以让你的生活更轻松。

为了预防这几乎是不可避免的,为什么不立即学*一些模式呢?一个很好的来源是Head First: Java Design Patterns,可以从所有好的书店购买。

GitHub

GitHub 允许你搜索和浏览其他人编写的代码,并查看他们是如何解决问题的。这很有用,因为查看类的文件结构,然后经常深入研究它们,通常可以显示如何从一开始规划你的应用程序,并防止你走上错误的道路。你甚至可以获得一个 GitHub 应用程序,让你可以在手机或*板电脑上舒适地进行这些操作。或者,你可以配置 Android Studio 来保存和分享你的项目到 GitHub。例如,在主页www.github.com上搜索“Android 片段”,你将看到超过 1,000 个相关项目,你可以浏览:

图 28.1 – Android 片段结果

图 28.1 – Android 片段结果

Stack Overflow

如果你遇到困难,遇到奇怪的错误,或者遇到无法解释的崩溃,通常最好的去处是谷歌。这样做,你会惊讶地发现 Stack Overflow 似乎经常出现在搜索结果中,并且有充分的理由。

Stack Overflow 允许用户发布他们问题的描述以及示例代码,以便社区可以回答。然而,根据我的经验,很少有必要发布问题,因为几乎总会有人遇到完全相同的问题。

Stack Overflow 特别适合处理最前沿的问题。如果新的 Android Studio 版本有 bug,或者新版本的 Android API 似乎没有做应该做的事情,那么你几乎可以肯定,全世界成千上万的其他开发者也遇到了和你一样的问题。然后,一些聪明的编程人员,通常来自 Android 开发团队本身,会提供答案。

Stack Overflow 也适合进行一些轻松的阅读。前往主页www.stackoverflow.com,在搜索框中输入Android,你将看到 Stack Overflow 社区最新问题的列表:

图 28.2 – Android 列表

图 28.2 – Android 列表

我并不是*议你立即投入并开始尝试回答所有问题,但阅读问题和*议会教会你很多东西,你可能会发现,你比你期望的更经常地有解决方案,或者至少有解决方案的想法。

Android 用户论坛

此外,值得注册一些 Android 论坛并偶尔访问它们,以了解用户的视角下的热门话题和趋势。我不会在这里列出任何论坛,因为只需要快速搜索即可。

如果你对这个话题很认真,那么你可以参加一些 Android 会议,在那里你可以与成千上万的其他开发者交流并参加讲座。如果你对此感兴趣,可以在网上搜索 droidcon、Android Developer Days 和 GDG DevFest。

更高级的学*

你现在可以阅读更多其他 Android 书籍。我在本书开头提到,几乎没有书籍,甚至可以说没有一本书,教会读者如何在没有 Java 经验的情况下学* Android 编程。这就是我写这本书的原因。

现在你已经对面向对象编程和 Java 有了很好的理解,还对应用程序设计和 Android API 有了简要介绍,你现在可以阅读针对已经了解如何在 Java 中编程的人的 Android“初学者”书籍了,就像你现在所做的那样。

这些书籍充满了很好的例子,你可以构*或仅仅阅读,以巩固你在本书中学到的知识,以不同的方式使用你的知识,当然,也学到一些全新的东西。

也许值得进一步阅读一些纯 Java 书籍。也许很难相信,在刚刚浏览了大约 750 页之后,Java 还有很多内容没有时间在这里涵盖。

我可以列举一些书名,但在亚马逊上拥有最多积极评价的书籍往往是值得探索的书籍。

我的其他渠道

请保持联系!

再见,谢谢你

我写这本书的时候非常开心。我知道这是陈词滥调,但也是真的。然而,最重要的是,我希望你能从中获益,并将其作为你未来编程之路的垫脚石。

也许你正在阅读这本书是为了一点乐趣或者发布一个应用程序的荣誉,或者作为编程工作的垫脚石,或者你真的会开发一个在 Google Play 上风靡一时的应用程序。

无论如何,我非常感谢你购买了这本书,我祝愿你未来的努力一切顺利。

我认为每个人都有一个应用程序的潜力,你只需要付出足够的努力将它发挥出来。

posted @ 2024-05-22 15:10  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报