安卓初学者入门指南-全-
安卓初学者入门指南(全)
一、Android 简介
欢迎开始您的 Android 开发者之旅。也许你已经拥有并使用一部安卓手机、平板电脑或其他设备。你将置身于一个伟大的公司,全世界每天都有超过 10 亿台基于 Android 操作系统的设备在使用。即使你认为你还没有使用 Android,你可能会惊讶地发现它已经进入了一系列惊人的设备和产品,其中许多你可能已经使用过,甚至没有意识到 Android 的强大功能正在帮助你。
Android 现在为手机和平板电脑之外的一系列设备提供支持,包括智能手表、健身设备、汽车娱乐和导航系统、游戏控制台、玩具、厨房电器、花园浇水系统、管道和加热控制,甚至烧烤!是的,烧烤!使用安卓系统的设备的扩张没有放缓的迹象,但最受欢迎和最有可能找到安卓系统的地方仍然是手机和平板电脑。作为一名初露头角的 Android 开发人员,这意味着您将学习适用于这里提到的所有(或至少许多)设备类型的许多方面,但您最初的重点可能是学习如何为 Android 手机和平板电脑构建应用。
使用 Android:最好的部分
移动和智能手机的发展是过去十年或更长时间里最激动人心的技术故事之一。你可能没有亲身经历过每一个转折,但你很可能接触过一个或多个最近智能手机时代的设备,你可能听说过诺基亚、苹果、微软、谷歌和安卓等名字在许多组合中被提及。
今天,Android 向您展示了从过去 10 年这些公司和平台之间的所有重大技术大战中获得的好处,并让您驾驭这一浪潮,提供出色的工具、功能,并帮助您走上 Android 应用开发的道路。在本书撰写之际,这或许是 Android 目前最具吸引力的地方。它与所有智能手机和移动技术(以及苹果的 iOS)并驾齐驱,选择 Android,您可以立即获得它提供的优势,包括
-
开发者工具和平台:我们将在本书中讨论这些项目,包括 Android Studio、Android SDK、Google Play 服务和 Google Play 在线商店等等。
-
一个巨大的现有市场:当我说超过 10 亿的用户在等待你的应用时,我不是在开玩笑,还有更多潜在的未来用户在地平线上。如图 1-1 所示,Android 的全球市场渗透率令人惊叹。
-
一个由志同道合的开发者组成的全球社区:你并不是一个人在追求你的 Android 应用开发梦想。有几万、几十万甚至可能几百万其他开发者有经验可以分享。
-
A rock-solid technology foundation: Android started life using the Java programming language as the predominant technology with which to build applications. Today it also supports Kotlin and C++. We will stick to Java for the examples and techniques explored in this book, as it is one of the most widely used, widely respected, and mature development technologies in existence – the vast majority of Android applications are built with Java.
图 1-1
Android 在智能手机用户中的全球市场份额
我可以继续列举更多关于 Android 开发世界的广度和深度及其优势的例子,但是我将让本书的其余部分来帮助演示更多的例子。
使用 Android:挑战
如果我不指出 Android 在许多方面都是幸运的,但也有挑战,我就不会诚实。好消息是,这些挑战中有许多是众所周知的,也很容易解决。以下是新开发人员可能很快就会遇到的一些领域:
-
你正在开发小型设备。无论我们谈论的是 Android 手机、Android 平板电脑、车载仪表盘,还是一系列其他外形因素,你需要处理的一个常见约束是屏幕的大小。当我们讨论用虚拟设备模拟 Android 设备时,我们将深入探讨这个主题,但请始终记住,您坐在为 Android 构建的台式机或笔记本电脑上的体验与您的用户在手机和平板电脑上的体验是不同的。
-
超越屏幕思考。不要让小屏幕成为你在应用中提供强大特性和超酷功能的唯一途径!正如您将看到的,后面的章节将探讨声音和音频、车载传感器、振动等内容。不要成为屏幕的俘虏——你有更多的工具可以使用。
-
替代品只需轻轻一扫。每个拥有安卓设备的人——实际上是任何智能手机——都有许多应用的第一手经验。用户可以选择,而且用户还可以在手机上运行一系列其他应用。这意味着您的应用可能会共享资源和用户的注意力,用户可以做各种您从未想过的事情!
-
安卓并不总是安卓。我们将在本书中涵盖 Android 的最新版本,包括 Android 版本 11,在撰写本文时它已有近一年的历史,并预览 Android 版本 12,它将在今年晚些时候发布,作为谷歌 Android 更新的典型年度发布周期的一部分。然而,在市场上,我在本章前面提到的十亿甚至更多的用户正在使用早期的 Android 版本。不仅仅是 Android 10 或 Android 9,而是可以追溯到 Android 4 甚至更早的版本!在鼓励(甚至允许)用户升级方面,Android 有着复杂的历史,在决定瞄准这个十亿用户市场的时候,这一点要记住。
无论你做什么,都不要因为这几点而灰心丧气。当你开始开发 Android 应用时,更多地把它们看作是要学习的第一套课程。
了解 Android 的传统及其对您的影响
Android 可以追溯到 2003 年,那一年 10 月,帕洛阿尔托的一群开发人员聚在一起,使用 Linux 内核作为他们梦想的具有更强大界面的设备新时代的基础。在经历了成长期的挣扎后,谷歌于 2005 年收购了 Android 业务,其前联合创始人安迪·鲁宾加入谷歌,继续为智能手机和其他设备开发操作系统。
2008 年,在经历了几次失败后,谷歌及其合作伙伴 HTC 和 T-Mobile 发布了第一款手机,被称为“梦想”或“G1”,这取决于你在哪个国家。我自己的 G1 至今仍在运行,但不再是我的日常电话。图 1-2 显示了我的 G1 在原始锁定屏幕。
图 1-2
作者最初的 G1 安卓手机,来自 2008 年
在第一次发布之后,Android 的势头慢慢增强,操作系统的更新开始以甜点的名字作为代号出现——纸杯蛋糕、甜甜圈、艾克蕾尔等等,代表版本 1.5、2.0 和 2.1。
在第一次公开发布的时候,谷歌还与电信公司、芯片公司和手机制造商合作创建了开放手机联盟,旨在建立一个广泛的联盟来支持 Android 的未来。谷歌还建立了“安卓开源项目”,作为安卓基础开源代码的保管人。
随着许多新版本的软件和许多新制造商的加入,Android 的故事还有很多。但是以上几点是 Android 开发者在构建应用时必须应对的几个问题的未知开端。
随着许多公司开始制造基于 Android 的手机,谷歌并不直接控制如何在每个制造商的设备上维护 Android,也不直接控制主要市场的电信公司如何作为合同、销售等的一部分管理手机的消费者生命周期。在 Android 最初发布后的几年里,出现了一个有许多不同设备的市场,运行许多不同版本的 Android,升级保证非常不完整。这在 Android 世界中被称为碎片问题。
对于开发人员来说,这意味着为 Android 构建应用需要付出额外的努力,考虑野外设备的数量以及它们运行的 Android 版本。在写这本书的时候,谷歌已经做出了一系列努力来减少这种担忧,并鼓励制造商升级设备。这产生了一些影响。Google 还增加了一系列开发人员特性,以减轻开发人员处理这个变化多端的市场所需的工作量,我们将在后面的章节中讨论其中的几个特性。
在结束这个话题时,图 1-3 显示了 2020 年中期全球使用的 Android 版本的当前分布情况。
图 1-3
2020 年使用的 Android 版本和 API 级别
请注意这些数字的自我吹捧。你不用报告每个 Android 版本使用的设备的绝对比例,而是显示运行特定版本或更高版本的设备的“累积分布”。很容易通过算术计算出真实的绝对百分比。例如,11.2%的设备运行 Android 6.0“棉花糖”,这是其 84.9%的累积分布和 Android 7.0 的 73.7%的累积分布之间的差异。在撰写本文时,Android 11 已经发布了几个月,但尚未在统计数据中显示出来。Android 12 定于今年晚些时候发布,对 Android 11 只做了微小的调整。请放心,Android 11 和之后的 Android 12 将会在排行榜上扶摇直上,成为学习 Android 开发的良好基础。
从这个讨论中得出的结论是,为了瞄准一定比例的 Android 设备,你将希望采用过去几个版本的 Android 功能,而不是仅仅迎合前沿。
了解 Android 的未来
像 Android 这样已经拥有数十亿用户的系统,已经可以被认为是一个巨大的成功。但是在大量的新领域,Android 还有大量的增长和机会。例如,现在大量的注意力集中在 Android 将如何成为未来“混合”计算的一部分。在这种混合模式下,一些设备使用 Android 进行部分操作,就像今天的智能手机一样提供应用,但转而使用 Chrome OS 等第二个操作系统来执行其他任务。
Android 本身也将遵循一条定期更新的道路,就像 Google Play 和其他云产品等许多伟大的在线服务一样,这将进一步推动 Android 设备和 Android 应用的可能性。
对这本书的其余部分有什么期待
Android for Absolute Beginners旨在带领读者踏上编写软件的第一次旅程,学习为未来的应用开发目标做准备的实践、技术和方法,以及构建计算机程序的许多基础知识。当然,我们将使用 Android 作为您学习的目标环境,激发您的灵感,以及涵盖您在软件开发的其他领域将会遇到的许多主题的基础。
我把这本书分成了四大部分,每一部分都旨在让你在阅读内容和例子的过程中,不断加深对如何为 Android 编写应用以及 Android 本身的理解。每一部分的章节涵盖了以下主题。
详述剩余章节的更多内容
章 1 ,安卓简介:你在看!您已经介绍了 Android 的大部分内容,只需几页就可以着手开发您的第一个 Android 应用。
第 2 ,介绍 Android Studio: 我们将看看构建 Android 应用最流行的工具集——Android Studio——以及如何获得这款免费软件。我们还将简要介绍 Android Studio 的一些替代产品,以及如何及时了解新版本、新特性和 Android 应用构建方式的其他变化。最后,我们将介绍“仿真”Android 手机的概念,即 Android 虚拟设备(AVD),这是一种让您的计算机模拟 Android 手机的方式,并提供了一个测试您的应用的平台,而无需修改您的实际手机。
章 3 ,你的第一个安卓应用,已经!:对,没错。您将直接创建您的第一个 Android 应用。没必要等到你读完这本书!我们的第一个例子将非常简单,但它将为我们在所有剩余章节中展开奠定基础。
章 4 ,探索你的第一个项目:在这一章中,我们将对你在第三章中创建的例子进行虚拟放大,浏览你的第一个应用的每一部分,开始理解它们来自哪里,它们做什么,以及它们为什么会在那里。
章节 5 ,Android Studio 深度:如果你打算为你的未来开发使用一套集成的工具,那么深入了解它们的功能将是必须的。在这一章中,我们将探索 Android Studio 的所有关键方面,包括代码编辑功能、调试器、分析工具等等。
章节 6 ,掌握你的整个开发者生态系统:这让你对你在开发旅程中可以并且将要使用的所有工具有了更全面的了解。本章将探讨位于 Android Studio 集成环境之外但对其至关重要的工具,包括 Java 开发工具包(JDK)、Gradle、代码和应用的源代码控制系统、Android 虚拟设备的管理以及环境的其他关键部分。我们还会看看你的开发者硬件的哪些方面会影响你的 Android 开发之路。
章节 7 ,介绍 Java 用于 Android 开发:准备用 Java“升一级”?不管你目前的知识水平如何,本章都将强调 Android 开发所需的 Java 编码的关键领域,以及扩展你的 Java 专业知识的更多资源。
章节 8 ,介绍 XML 用于 Android 开发:Android 应用行为的很多方面都是由 XML(可扩展标记语言)数据控制的。本章将带您了解 XML 的基础知识,以及如何将 XML 应用于 Android 应用的各个方面,包括应用的清单、用户界面等等。
第 9 ,探索 Android 概念:核心 UI 小部件:在第八章的基础上,我们探索如何用菜单、屏幕小部件(如字段、列表、图像和其他可视项目)等常规组件来布局 Android 用户界面。本章还将介绍活动的关键概念 Android 用户界面的基础构件。最后,我们将给出 Android Jetpack 的概述,它是一个现代的库,在提供向后兼容性的同时提供了现代的布局方法。
第 第 10 ,探索 Android 概念:布局和更多:本章扩展了您对所有 Android 用户界面组件的理解,并进一步建立在第 8 和 9 章的工作基础上。
章节 11 ,了解活动:借助前面章节的 UI 概念,您将探索活动作为所有 Android 应用的基本构建块的全部功能。
章节 12 ,片段介绍:您将学习更广泛的片段概念,它为许多不同的屏幕尺寸和布局选项提供了动力。
章节 13 ,使用 Android 的声音、音频和音乐:在这一章中,我们将探索您的应用的音频的所有方面,包括在应用中播放音频和使用声音,录制音频,甚至在 Android 设备上创建音频。
章节 14 ,为 Android 处理视频和电影:如果你是崭露头角的史蒂文·斯皮尔伯格、索菲亚·科波拉,甚至只是 YouTube 明星,这一章就是为你准备的。我们将介绍 Android 的视频捕获和回放功能,以及如何将这些功能应用到您的应用中。
章节 15 ,介绍通知:通过使用 Android 提供的事件框架和通知系统,扩展到应用的边界之外。
第 第 16 章,通过通话探索设备功能:除了屏幕上显示的内容,您的 Android 世界还有更多可能性。我们将了解呼叫能力、访问传感器和其他信息。
章节 17 ,理解意图、事件、接收者:在每一个 Android 应用的幕后,丰富的后台功能让事情保持运转。本章涵盖了 Android 平台的核心概念,并展示了它们如何塑造和影响您的应用。
章 18 ,介绍 Android 服务:在这一章中,我们将探索如何使用其他代码和其他应用来丰富您自己的应用,以及它能为您的用户做些什么。
章节 19 ,在 Android 中处理文件: Android 使你能够处理多种数据、配置和其他文件。本章将开始你的旅程,了解 Android 应用可以在哪里以及如何利用传统文件来增强你的用户体验。
章节 20 ,使用 Android 中的数据库:数据驱动着每一个应用,知道如何为应用存储、管理和使用数据是让它们变得伟大的关键。本章将涵盖 Android 提供的多种数据处理方式。
带着对即将完成的事情的预尝,没有比现在更好的开始了。第二章在下一页等待!
二、Android Studio 简介
在这一章中,我们将介绍 Android Studio,它是为 Android 应用编写软件的主要工具。虽然有一系列其他软件可供您依赖,但 Android Studio 集成开发环境(IDE)将是实现您想法的核心。你可以把它想象成你用来编写进一步软件的软件。
如果您过去做过任何类型的软件开发,您可能会发现一些熟悉的概念。如果是这种情况,请随意跳到本章后面的章节,直接进入为您选择的平台安装 Android Studio 的机制。你当然可以继续阅读,看看这个话题是否有新的或有趣的东西。
现在让我们深入研究作为 Android 应用开发人员,您将使用的最重要的工具集——Android Studio!
了解集成开发环境(IDE)的含义
在我们开始之前,让我们为那些没有遇到过 IDE 这个术语或者对编程完全陌生的人定义一下 IDE 的含义。术语集成开发环境几乎是不言自明的,但也不尽然。如果我们把它拆开,“开发环境”只是指开发人员进行开发的环境(从软件的角度来说)。这就好比说微软 Word 或谷歌文档是作者的“写作环境”——他们写作的地方。
术语“集成”也很简单,但是了解什么是集成是全面理解 IDE 的关键。把我们的写作类比延伸得更远一点,作为一名软件开发人员,你也要写作,但是在你的情况下,它将是人类可读的 Java 和其他几种语言的编程代码。为此,每个 IDE 都包括(集成)一个代码编辑器,您可以在其中编写实际的原始代码。目前为止,一切顺利。
正如 Microsoft Word 为作者提供了一些额外的捆绑工具,如拼写检查和在线词典定义查找,IDE 也引入了其他工具,并以集成的方式将它们提供给软件开发人员。除了代码编辑器之外,通常还有语言参考工具,以便您可以查找软件库是如何工作的,构建工具来获取您编写的代码并将其编译成软件的工作片段,调试工具来帮助您识别和理解问题和错误,以及许多其他工具,如性能分析器、代码格式化器、语法高亮器、实时检查工具、网络监视器等等。
IDE 的关键是所有这些工具都集成在一起,或多或少能很好地协同工作,而不需要开发人员在工具之间手动传递东西。在 ide 出现之前,这是开发人员不太光彩的生活。有了像 Android Studio 这样的 IDE 来处理平凡、复杂和重复的操作,您就可以自由地专注于编写应用的创造性问题解决方面,而不必担心底层管道。
Android Studio 的历史和起源
当 Google 首次发布 Android 开发工具时,它瞄准了当时最流行的开源开发环境之一 Eclipse。它发布了一套插入 Eclipse 的工具,称为 Android Developer Tools,简称 ADT。这个组合——Eclipse 和 ADT——让许多 Android 开发者满意了很多年。十年过去了,虽然 Eclipse 仍然是一个非常受欢迎的 IDE,但一系列其他 IDE 的地位已经上升或下降,谷歌如果不热衷于走在这些变化的前沿,那就什么都不是。
2013 年,谷歌宣布它正在与 JetBrains 合作,JetBrains 是一家开发了名为 IntelliJ IDEA 的现代 IDE 的公司,主要专注于为那些构建基于 Java 的应用的人提供一流的开发体验,新的合作产品将基于 IntelliJ IDEA,名为 Android Studio,并作为完全免费的 Android 开发 IDE 发布。Android Studio 版本于 2014 年晚些时候发布。在随后的几年中,谷歌宣布 Android Studio 将成为开发 Android 应用的首选 IDE,并将停止关注和投资其他工具。这并没有阻止 Eclipse 爱好者继续使用 Eclipse、ADT 和一系列其他工具——我们将在本章末尾触及这些替代工具。
为您的平台下载 Android Studio 安装程序
现在,您已经有了足够的背景知识来理解 Android Studio 的重要性,以及它将在您未来的 Android 应用开发中扮演的角色。是时候拿到 Android Studio,开始编码了!
下载 Android Studio 安装包的首选位置是 Android 官方网页本身。你可以在首页 www.android.com/
开始,或者在 https://developer.android.com/studio
直接进入开发者下载页面。
我应该注意的是,当在印刷(或电子)书籍中调用网站 URL 时,一般的警告是适用的。URL 可以而且确实会随着时间而改变。如果你在文章发表后的某个时候读到这篇文章,那么 https://developer.android.com/studio
的直接链接可能已经改变——但是 www.android.com/
的主页将会一直存在,并帮助你导航到谷歌未来可能将下载页面移动到的任何地方。
对于 Linux 操作系统用户来说,至少还有另一种选择,即使用快照打包方法,我们稍后将对此进行讨论。
使用 Android Studio 版本
到达下载网站后,你会立即看到一个“下载 Android Studio”选项,如图 2-1 所示。仔细看按钮下面的文字。在我的例子中,它显示为“4.0 for Linux 64 位(865 MB)”。
图 2-1
developer.android.com/studio 下载页面,显示 Android Studio 4.0
“4.0”指的是 Android Studio 的版本,这就打开了版本、Android、Android Studio 和 Android 软件开发工具包(SDK)的话题。作为一名开发人员,你需要知道的最重要的一点是,Android 版本——在人们的设备上运行的软件——与 Android Studio 的版本没有直接联系,这意味着当你使用 Android Studio 开发应用时,你有多种方法来控制你的应用将支持哪些 Android 版本(以及哪些设备)。
了解 Android Studio 和 Android SDK 如何协同工作
Android 操作系统版本提供了一系列功能,开发人员可以通过将 Android SDK 集成到他们的代码中来访问这些功能。谷歌定期发布 Android SDK 版本,有些情况下一年发布几次。您安装的 Android Studio 可以安装和使用许多不同版本的 Android SDK——事实上您几乎肯定会这样做。然后,在创建应用时,您将能够指定如何选择 SDK 版本以及支持 SDK 特性的 Android 版本。我们将在第三章中详细介绍这一过程,所以不要在这一点上对版本“舞蹈”感到不知所措。
为 Android Studio 做准备
Android Studio 是 Android 开发的领先 IDE,其原因在本章前面已经介绍过。它几乎拥有新手和有经验的 Android 开发者开发各种应用所需的一切。差不多!作为一名新的 Android 应用开发者,你需要考虑一些其他的因素来完善你的开发者工作环境。
总的来说,值得庆幸的是,这些考虑因素数量很少,很容易掌握。实质上,它们如下:
-
我将在 Android 开发中使用什么台式机或笔记本电脑硬件?
-
我将在台式机/笔记本电脑上运行什么操作系统?
-
在使用 Android Studio 之前,我的系统需要什么先决条件?
-
在开发过程中,我将使用哪些 Android 手机(如果有的话)?
让我们从推荐的最低入门要求的角度来依次看看这些,这样你就可以在本章的后面快速安装 Android Studio 了。
为 Android 开发选择台式机或笔记本电脑硬件
首先,好消息。几乎你现有的任何一台电脑都将是你 Android 开发之旅的一个很好的起点。几乎所有过去十年的电脑都有基本的计算“咕噜声”来支持 Android Studio,并允许您在构建第一个应用时学习核心概念。事实上,一些开发人员从来不愿意放弃他们的“日常驱动”机器,因为他们的需求并不那么繁重。
然而,可能会有那么一天,你会想一想你对 Android 应用开发有多认真,以及你能从更好的设备中获得什么好处。或者,您现在可能正在市场上购买一台新计算机,并希望提前计划计算资源,以使应用开发更快、更高效,等等。我们将在第六章探索更强大的开发硬件、附件和完整的开发环境的所有方面。如果你正在考虑如何给你的电脑增压或者为 Android 开发购买新设备,你可以随意跳到前面阅读第六章。但对于那些只想确保他们被现有设备覆盖的人来说,如果你根据谷歌推荐的 Android Studio 最低配置进行确认,这里有一些关键的考虑因素:
-
CPU:这里的好消息是,过去几年的几乎所有 CPU 对于开始 Android 开发都绰绰有余。
-
内存:谷歌推荐最少 4 GB 的内存,并建议 8 GB 是首选的基本级别。大部分内存将用于虚拟设备仿真,其余用于 Android Studio 本身。您可以使用比最小值更少的值勉强度日,但是在测试您的应用时,性能会受到影响。
-
存储:虽然推荐的最低存储容量是 2 GB,而谷歌的偏好是 4 GB,但事实是存储容量越大越好。2 GB 将为您提供一个基本的 Android Studio 安装、一个虚拟设备,以及大量尝试删除不需要的项目以释放空间的常规清理工作。4 GB 好一点。如果可以的话,清理一下,确保你有 5-10gb 的空间,让自己的生活更轻松。
-
屏幕:谷歌推荐屏幕分辨率至少 1280
×
800。然而,没有说明的是,这是对 Android Studio 本身的建议,您将在这里编写代码和测试您的应用。当您考虑到您的开发环境与用户手机上的屏幕有很大的不同时,屏幕分辨率是一个需要考虑的问题。现在,请注意谷歌的指南,但我们将在第 6 和其他章节中重新讨论屏幕的话题。
为您的计算机选择操作系统
谷歌使 Android Studio 可用于所有流行的操作系统,包括 Linux、macOS、Windows 和它自己的 Chrome OS(本质上是 Linux 的一个重新皮肤化的版本)。除非你想买一台新电脑,否则无论你现在的电脑是什么操作系统都是不错的选择。在第六章中,我们将进一步探究理想开发者设置的细节,包括操作系统的选择。
继续下载安装程序
既然您已经熟悉了 Android Studio 版本的细微差别以及您的硬件和操作系统选择,现在您可以继续下载并安装 Android Studio。正如本章前面提到的,从 https://developer.android.com/studio
页面,你的操作系统应该被自动检测到,并且该平台的下载按钮应该是突出的。在前面的例子中,当从 Linux 机器上访问站点时,您看到了这一点。在我的 MacBook 上,我看到一个“下载 Android Studio”的选项出现在前面和中间,下面有“4.0 for Mac”的文字。
如果你从不同的机器上下载,而不是从你计划用于 Android 开发的机器上下载,确保你为你打算使用的机器选择了正确的 Android Studio 版本。如果目标机器有不同的操作系统,向下滚动页面到标题“Android Studio 下载”,在那里你会看到 Android Studio 支持的每个操作系统的选项。
Windows Alternatives
Android 开发者网站上的安装选项列表包括两种 Windows 版本。第一个是可执行安装程序,文件名格式为“Android-studio-ide-193.6514223-windows . exe”(“Android-studio-ide”后面的数字串是内部版本号和发布号,会随时间变化)。第二个是 zip 文件,文件名格式为“Android-studio-ide-193.6514223-windows . zip”,我建议您使用常规可执行安装程序的第一个选项。这将为您解决各种问题,包括您通常在 Windows 下使用的帐户的目录位置和 Windows 权限,以及共享计算机上的任何其他帐户。
单击相关选项,下载适用于您的操作系统的 Android Studio 版本,并记下您的浏览器将下载内容放置在何处。在 macOS、Linux 和 Windows 上,这通常是用户的“下载”目录,但是如果您选择将它放在其他地方,它可能会有所不同。
随着 Android Studio 4.0 的发布,总下载量在大小上接近 1 GB,因此可能需要几分钟才能完成下载。
在 Windows 上安装 Android Studio
假设您遵循使用 Windows 可执行安装程序的建议路径,您可以通过双击可执行文件从下载位置启动安装程序。在撰写本文时,这意味着文件 android-studio-ide-193.6514223-windows.exe。所有最新版本的 Windows 都将提示您允许通过用户访问控制(UAC)机制继续安装,以确保您明确同意完成安装所需的提升权限。
安装完成后,您应该会看到 Android Studio 的一个新的开始菜单项。
在 macOS 上安装 Android Studio
与 macOS 上的大多数应用安装一样,安装 Android Studio 非常简单。在 Mac 上打开 Finder,浏览到您下载 Android Studio 安装程序的目录。您应该看到一个 dmg 文件,其名称类似于“Android-Studio-ide-193.6514223-MAC . dmg”——这是 Android Studio 4.0 安装程序的 DMG 文件。双击这个磁盘镜像文件,你的 Mac 将首先验证下载,这真的意味着它将检查 Google 准备磁盘镜像所使用的代码签名证书和/或公证。一旦验证完成,您应该会看到如图 2-2 所示的典型 DMG 安装窗口。
图 2-2
适用于 macOS 的 Android Studio DMG 安装窗口
将 Android Studio 应用图标拖到 Applications 文件夹,这将触发您的 Mac 复制任何 Mac 应用的安装包。一旦完成,Android Studio 就可以从应用菜单和文件夹中使用了。
在 Linux 上安装 Android Studio
你下载的 Linux 的 Android Studio 安装程序是一个 gzip 压缩的 tarball,文件名如android-studio-ide-193.6514223.tar.gz
。就 Android Studio 而言,将该文件解压缩到您选择的目录中就构成了安装。决定你想把 Android Studio 安装在哪里:例如,你可能想把它放在你的用户账户的主目录下或者其他目录下,比如/opt
。当您打开压缩的 tarball 时,您会看到它在您指定的位置创建了一个名为“android-studio”的目录,所有的文件和子目录都位于该目录下。这意味着您没有必要创建一个名称相似的父目录。例如,我不需要创建一个/home/grant/android-studio
目录,因为解包 tarball 也会创建叶级——我真的不想要一个/home/grant/android-studio/android-studio
路径,因为那是不必要的多余。您为 Android Studio 选择的目录将被许多 Android Studio 内部文档称为“安装主目录”。
打开你喜欢的 shell,比如 bash 或者 zsh,将目录更改为你想要放置 Android Studio 的父目录下。确保您对目标位置拥有写权限。注意您下载压缩的 tarball 文件的位置——在我的例子中,这是/home/grant/Downloads
目录,我也可以将其称为~/Downloads
。如下运行 tar 命令,该命令指示它解压缩、解压缩并验证最终的文件扩展集:
tar -xvzf ~/Downloads/android-studio-ide-193.6514223.tar.gz
几个屏幕的状态应该滚动过去,最终您应该返回到您的 shell 提示符。运行 ls 或打开您最喜欢的文件管理器,您应该会看到现在创建的 android-studio 目录。在该目录中,您将找到一个名为Install-Linux-tar.txt
的文件,您可以阅读该文件以获得一些关于进一步调整安装以及如何实际运行 Android Studio 二进制文件的基本说明。我来给你破悬念!您将在 android-studio 目录下看到一个 bin 目录,其中有一个名为studio.sh
的 shell 脚本。执行这个 shell 脚本来启动 Android Studio。我们将在本书的后面回到Install-Linux-tar.txt
文件中提到的其他一些配置选项。
Snap To It!
除了典型的 Linux 安装选项,还有其他选择,其中一个选择遵循了近年来 Linux 上应用打包版本的趋势,即完全自包含,将所有依赖项和库捆绑到一个包中。这种方法的典型代表是 Canonical(Ubuntu fame)和 Flatpak 推广的 Snap bundling,后者源于 XDG 的 freedesktop.org 作品。
对于支持 Snap 包的 Linux 发行版(理论上是所有发行版),您可以让您的包管理器安装 Snap for Android Studio。例如,在 Ubuntu 20.04 下,你可以简单地运行
sudo snap install android-studio
对 Snap 方法有一点要注意:从官方网站 https://developer.android.com
下载将提供最新的 Android Studio 补丁版本,依赖 Snap 方法意味着依赖 Snap 包的维护者和打包者来保持它的最新。从理论上讲,这种维护是完全可能的,有些人认为这很容易,或者比依赖于正常安装软件的维护和修复更容易。但是,您从在线软件包存储库中检索的快照软件包可能不是最新的。
安装后继续安装 Android Studio
当 Android Studio 安装在您的电脑上时,您需要执行一些一次性设置操作来开始使用。幸运的是,Android Studio 本身会指导您完成这些设置步骤。如果你在安装之后还没有启动 Android Studio,你应该现在就启动。您应该会看到如图 2-3 所示的闪屏。
图 2-3
启动 Android Studio 时显示的闪屏
一旦 Android Studio 第一次加载,闪屏将消失,您应该会看到设置向导的开始,这可能会闪过,然后会提示您从您机器上的任何 Android Studio 早期版本导入设置,如图 2-4 所示。
图 2-4
首次运行 Android Studio 时显示的导入设置选项
出于本章的目的,我们将假设没有要导入的旧设置。您可以选择“不导入设置”选项,然后单击“确定”按钮。然后,设置过程会提示您一个关于共享使用数据的问题,例如使用的功能和访问的库,如图 2-5 所示。
图 2-5
Android Studio 安装期间的数据共享提示
如果你想与谷歌分享你的使用统计数据,这完全取决于你。它不会影响 Android Studio 的功能或行为,尽管它有助于在未来版本中修复错误和改进。
在您做出使用统计选择后,您将看到 Android Studio 设置向导的登录页面,如图 2-6 所示。
图 2-6
Android Studio 安装向导主页
在这一点上你没有太多的选择。您可以单击“下一步”按钮继续安装向导,也可以单击“取消”按钮在以后返回。
假设你喜欢冒险——我觉得这是一个安全的赌注,因为你已经买了这本书——单击下一步开始安装向导的最后部分。您可以选择标准或定制安装类型,如图 2-7 所示。
图 2-7
Android Studio 安装向导中的安装类型选择页面
自定义安装选项将允许您做一些事情,如更改安装位置,选择要下载的 Android SDK 版本,以及类似的选择。我们将在第五章中更详细地讨论这些项目。现在,您可以选择标准安装类型,然后单击 Next 按钮。
你的下一个选择基本上是装饰性的,你可以选择让用户界面使用“亮”或“暗”的主题,如图 2-8 所示。
图 2-8
Android Studio 中选择用户界面主题的选项
对于那些使用有机发光二极管显示器的人来说,亮暗主题之间的用电差异微乎其微。对于其他人来说,这只是一种化妆品偏好。出于本书的目的,我将使用浅色主题,因为这将为本书的任何印刷版本节省墨水。一旦您决定了自己的偏好,请单击“下一步”按钮继续。
倒数第二个设置向导屏幕将会出现,这是图 2-9 所示的验证设置视图。
图 2-9
Android Studio 设置向导中的验证设置屏幕
您可能习惯于简单地跳过这些确认屏幕,但是我鼓励您滚动浏览显示的摘要。不是因为有任何隐藏的“陷阱”,而是为了确保您意识到在安装过程中还会下载多少。如果您选择前面描述的标准安装类型,Android Studio 将继续为您的平台下载最新的 Android SDK 版本和最新的虚拟设备仿真器。这些可以很容易地总计另外 500 MB-1gb 的下载,这你至少应该知道。如果您已经尝试了自定义安装类型选项,在这个阶段,您可能会下载相当多的 SDK 版本和模拟器引擎,这可能会很快增长到数千兆字节的额外下载。一旦你对你所做的选择感到满意,点击下一步按钮,Android Studio 将自动开始所有剩下的自动化和下载步骤。在某些情况下,如果您的硬件支持直接仿真,您将会看到如图 2-10 所示的附加屏幕。
图 2-10
Android Studio 的咨询屏幕通知您加速的仿真性能
使用您的硬件仿真加速功能没有坏处,所以接受这个选项,您最终会看到下载组件进度屏幕,如图 2-11 所示。
图 2-11
下载组件进度屏幕的初始视图
根据计算机和互联网连接的速度,组件下载和配置过程至少需要几分钟。如果您看到细节在某些地方停滞不前,请不要惊慌,因为这通常表示 Android Studio 安装向导正在下载大型组件,如 Android SDK 包。最终,您应该会看到详细信息停止滚动,一条神奇的线显示在窗口的底部,如图 2-12 所示。
图 2-12
Android Studio 安装向导中已完成的组件下载视图
您希望在过程结束时看到的神奇的文本行是“Android SDK 是最新的。”这意味着下载和配置步骤已经完成,Android Studio 已经准备就绪。你可以点击 Finish 按钮,你会看到 Android Studio 本身的启动屏幕,如图 2-13 所示。
图 2-13
欢迎使用 Android Studio 屏幕
我们将在接下来的章节中回到这里介绍的选项。现在,您可以放心了,因为您已经成功安装了 Android Studio 以及我们将在接下来的章节中使用的 SDK 和模拟器组件。
使用 Android Studio 的替代产品
如果说软件世界——尤其是软件开发——提供了什么,那就是选择!无论是网络浏览器、电子邮件包,还是你对游戏的偏好,都有很多选择。为 Android 开发应用没有什么不同,尽管 Android Studio 中有一个很强的默认。尽管如此,还有其他选择,即使你不打算使用其中的任何一个,知道替代 ide 和其他工具的存在也是有用的,因为你会在网上、会议上和其他讨论 Android 开发的论坛上看到它们。
这里有一个简短但不详尽的列表,列出了您在初露头角的 Android 应用开发生涯中可能会遇到的备选方案。
黯然失色
正如本章前面提到的,Eclipse 是谷歌支持 Android 开发的第一个 IDE。大约在 2007 年安卓出现之初,谷歌需要提供引人注目的开发者产品,以使其新收购的智能手机操作系统吸引开发者。当时,它选择开源 Eclipse IDE 作为官方认可的开发环境。这一选择被普遍接受,并被认为是一个巨大的乘数和推动者,让数百万安卓开发人员免费获得工具,使他们能够开发一波又一波的安卓应用。
直到 2014 年 Android Studio 1.0 发布之前,Eclipse 一直备受瞩目。即使在那次发布之后,Eclipse 仍然享受着谷歌的全力支持,直到 2016 年 Android Studio 2.2 的发布。谷歌当时宣布,它将不再支持 Eclipse 作为一流的开发环境,也不会保证 Android 开发工具在未来不会出现错误。
这听起来好像 Eclipse 不再是构建 Android 应用的可行环境。没有比这更偏离事实的了。现实是,Google 不再为想要使用 Eclipse 的开发人员提供修饰、便利或直接支持。然而,两个主要的主题确保了 Eclipse 仍然是寻求使用它的开发人员的一个选择。
首先,Eclipse 在处理 Android 应用的基础上积累了十多年的微调、集成和专业知识。最终,构建 Android 应用的工作归结为处理文本 Java 代码、XML 数据和相关工件。正如我们将在本书的第二部分中看到的,这些方面不会因为您选择了不同的 IDE 来帮助您而改变。
第二,随着时间的推移,Eclipse 是最受欢迎的 IDE 之一,它具有超越 Android 的优势,这意味着它通常是处理构建超越单一目标平台的应用的开发环境中的首选 IDE。
这并不是说选择 Eclipse 作为您的 IDE 会像 Android Studio 一样流畅或高效。一旦你超越了本书的范围,你会发现许多当代的在线信息来源会假设你使用的是 Android Studio 而不是 Eclipse。但是你们中的一些人——尤其是有经验的开发人员——将会看到 Eclipse 已经吸引人的地方。如果您正在开始您的开发之旅,并且还没有足够的 Eclipse 经验,那么我强烈建议您使用 Android Studio 作为您的 IDE。
IntelliJ IDEA
在这一章的开始,我解释了 Android Studio 的起源,并概述了它基于 IntelliJ 理念的基础。所以你可能会想,如果这是 Android Studio 所基于的,为什么我会选择 IntelliJ IDEA,这有什么区别,我为什么要关心呢?
很棒的问题!本质上,作为一个新的开发人员,没有压倒性的理由考虑选择 IntelliJ IDEA 作为您的 IDE 选择。然而,经验丰富的开发人员,或者那些为更多平台而不仅仅是 Android 开发基于 Java 的应用的开发人员,通常会发现在一个 IDE 上实现标准化是提高效率的重要驱动因素。还有一些 IntelliJ 背后的公司 JetBrains 只在完整的商业包中提供的功能,例如与 Spring Framework for Java 无缝协作的能力。如果您属于这些类别,我们非常欢迎您将 IntelliJ IDEA 作为首选 IDE。
要了解更多关于如何使用 IntelliJ IDEA 专门进行 Android 开发的信息,请查看 JetBrains(拥有并构建 IntelliJ IDEA 的公司)关于这一主题的网页,网址为 www.jetbrains.com/help/idea/android.html
和 www.jetbrains.com/help/idea/getting-started-with-android-development.html
。
针对多种操作系统和移动平台的工具
在第一章中,你看到了智能手机市场份额的分解,Android 占据了绝大多数份额。不要让这蒙蔽了你的双眼,让你看不到存在替代品的现实,比如苹果的 iPhone,还有工具可以帮助开发者瞄准这两个平台,甚至一些你可能没有听说过的其他平台。一些关键的多平台 ide 和开发环境包括 Xamarin、PhoneGap、Flutter 和 Apache Cordova 等产品。
跨平台工具本身就应该有一本书,事实上,上面提到的这些工具不止有一本专门针对它们的书,还有数不清的在线内容。如果你对他们所提供的感兴趣,我强烈建议你从他们各自在互联网上的产品主页开始,然后从那里扩大你的搜索范围。
传统的平台无关开发工具
为了总结这一章和开发者工具的主题,我应该带你回到这一章的最开始。ide 的出现是为了帮助开发人员处理所有用于创建现代应用的工具。这包括代码编辑器、调试器、编译器等等。但是 ide 并不适合所有人。
一些读者可能是经验丰富的开发人员,他们非常乐于组装自己的工具来帮助创建 Android 应用。有很多开发人员喜欢选择自己的编辑器,比如 Vim、Emacs 或 Sublime Text 编译和构建管道工具,如 Ant、Jenkins 和 Hudson 以及其他用于性能管理、调试等的工具。
如果你想走这条路,谷歌在本章开始提到的 Android Studio 下载页面提供了一套 Android 命令行开发工具。这些工具包括 sdkmanager,负责下载和管理多个 Android SDK 包;adb,它是 Android 开发者桥工具,用于将你的应用从你的开发者机器移动到你的虚拟和真实设备;以及更多。
虽然这本书不是针对那些想走这条路的开发人员,但至少你知道这是一个可行的选择。现在,我们假设你对使用 Android Studio 很满意,我们将在下一章直接深入开发你的第一个 Android 应用,从下一页开始!
三、你的第一个 Android 应用!
Android Studio 安装在您的计算机上后,您就可以开始创建您的第一个 Android 应用了。是的,现在!对于那些不熟悉 Java 代码开发的读者,不要惊慌。我们将在本章开始我们的例子,没有假定的 Java 知识或诀窍。对于那些了解 Java 的读者来说,请继续阅读,因为我们将涵盖创建 Android 虚拟设备(或 AVD)的重要的第一步,在其上运行本章中的示例以及未来章节中的示例。
创建您的第一个 Android 虚拟设备
更广泛的 Android Studio 集成环境中提供的最有用的特性之一是创建 Android 虚拟设备(avd)的能力。有了 AVD,你就有能力模拟一个真正的 Android 设备,控制它的一系列功能和特性,而不必有一个实际的物理设备。
AVD 方法并不意味着禁止您使用真实的设备来测试和使用您的应用,但是如果您考虑一下我们在前两章中提到的过多的 Android 设备、版本和外形因素,您就会明白,作为一名开发人员,即使只拥有运行 Android 的设备中的一小部分也是不切实际的。avd 可以帮助您缩小您拥有的(和能够负担的)和您的用户群实际使用的之间的差距。
要开始创建你的第一个 AVD,启动 Android Studio(如果它还没有运行的话),你会看到闪屏,然后是欢迎使用 Android Studio 屏幕,你可能还记得第二章中的内容。在欢迎屏幕的右下方,您应该看到一个齿轮图标和菜单选项Configure
,您应该点击它以显示一系列配置选项,如图 3-1 所示。
图 3-1
欢迎使用 Android Studio 屏幕上的配置菜单
您将看到配置项目列表中的第一个选项是 AVD 管理器选项。点击此选项,AVD 管理器将在几秒钟后启动,您将看到如图 3-2 所示的 Android 虚拟设备管理器介绍屏幕。
图 3-2
Android 虚拟设备管理器欢迎屏幕
在屏幕中间,你应该会看到如图 3-2 所示的按钮,上面写着+ Create Virtual Device...
。继续并单击该按钮开始 AVD 创建过程。然后你会看到虚拟设备配置硬件选择屏幕,如图 3-3 所示。
图 3-3
用于创建新 AVD 的选择硬件选项
硬件选择屏幕中有很多内容,但不要感到不知所措。这里的众多选项传达了你将在真实世界的设备中发现的变量,所以这真的不奇怪。让我们一步一步地了解各个领域,这样您就可以开始熟悉 AVD 管理器以及创建和使用 AVD 了。
从硬件选择屏幕的左侧开始,类别列表根据外形和使用模式对设备仿真器进行分组。你应该至少能看到图 3-3 所示的五个普通选项,电视、电话、Wear OS(原 Android Wear)、平板、汽车。您可以随意点击每一项,查看设备列表(屏幕中间)和维度详细信息窗口的变化,但完成后,请选择电话作为类别。选择 Phone 后,您应该会看到如图 3-3 所示的预打包仿真设备的原始列表。
在这一点上,我们不会急于选择第一批器件中的一个作为我们 AVD 的基础。相反,花一点时间滚动列表,记下一些数据点,这些数据点将在本书的后面重新出现,并将逐渐变得更有意义,对您的应用设计产生更大的影响。
在设备定义列表的上端,您会看到智能手机的品牌和型号,其中一些可能非常熟悉。各种像素选项,如 Pixel 3、Pixel 2 等,以及 AVD 图像尽可能模拟具有这些名称的真实物理设备。类似地,Nexus 6、Nexus 5 等是模仿其同名物的物理特征的虚拟设备,无论是板载存储、工作内存、屏幕分辨率还是其他功能。在您滚动查看列表的其余部分之前,请看一下显示在Resolution
和Density
列中的值。您将看到两者的测量值,以像素布局表示分辨率(例如,像素 2 AVD 为 1080 ×
1920)和每英寸点数或 dpi 表示密度(像素 2 也是 420 dpi)。您可能熟悉笔记本电脑或台式机屏幕的分辨率和密度测量,而不会对您在这里看到的值有所顾虑。但值得将这些与列表中的其他 avd 进行比较。
如果您进一步向列表底部滚动,您将开始看到一系列更隐晦的设备名称,如图 3-4 所示。
图 3-4
更多 AVD 模板可供您使用
这些设备没有任何相关的品牌名称,但它们确实拥有一些指标,通过使用通常与各种屏幕尺寸和分辨率相关的缩写来表明它们提供了什么。我们会在本书中遇到这些术语,所以现在你可以用书签标记名称,比如 QVGA、WQVGA、mdpi、hdpi 等等,不要在意它们的意思。
似乎所有这些当前和历史选项还不够,您将在设备定义列表下方看到两个按钮,允许指定您自己的自定义虚拟设备配置文件和从其他来源导入配置文件。就我们的目的而言,不需要这些选项,但您可以欣赏它们提供的价值——特别是对于那些最终可能想要设计针对非常具体、非典型设备的应用的人。
当我们深入研究用户界面设计时,我们将在第 9 、 10 和 11 章中重温 Android 设备分辨率和密度的概念和原理。现在,您可以滚动回到设备定义列表的顶部,并选择 Pixel 2 预配置的设备定义。点击下一步按钮,您应该会看到如图 3-5 所示的系统图像选择屏幕。
图 3-5
AVD 管理器中的 AVD 系统图像选择屏幕
选择系统映像可以被认为是对您之前选择的设备定义的补充。虽然您的设备选择选择了 AVD 的硬件方面,但系统映像决定了关键的软件方面:Android API/SDK 级别和 ABI,这基本上是决定在软件中模拟哪种芯片架构,英特尔的 x86 还是基于 Arm 的架构之一,以及这些部分最匹配的 Android 版本。
Android Software Development Kit: A Second Look
在第二章中,我介绍了术语 SDK,即软件开发工具包。虽然很容易解析这句话,并认为“太好了,这是一个帮助我开发软件的工具包”,但还是有必要回顾一下 Android SDK 真正提供了什么。首先,Android SDK 是一组软件库,是访问任何 Android 智能手机及其操作系统的功能和能力所需的基本工具。你可以将 SDK 视为一个杠杆,让你将谷歌内置于 Android 中的所有功能和能力应用到你自己的应用中,而无需从头开始构建一切。
Android SDKs 每个 Android 版本都有后续版本——提供了到 Android Studio 的连接,让您可以轻松调用 Android 行为;建立工具,让你把你的代码变成一个工作的 Android 程序;允许您管理仍在开发中并以“草案”形式运行的应用的平台工具,包括“adb”,Android 调试桥,用于从仍在开发中的代码控制仿真器或真实设备上的行为;以及更多。哦,SDK 提供了仿真器框架和设备仿真本身!
根据您之前是否在您选择使用的计算机上使用过某个版本的 Android Studio,或者之前是否完成过该向导,您将会看到列出的版本名称,如“R”或“Oreo ”,名称旁边有或没有下载链接。简而言之,如果系统映像已经在你的机器上,它不会显示下载链接。如果它还没有在您的机器上使用,下载链接将会显示,您可以直接从系统映像阶段单击它来获取相关的系统映像。
你需要至少有一个系统映像供你的 AVDs 使用,所以如果你看到的都是带有下载链接的系统映像名,你应该选择下载最新的。在图 3-5 所示的例子中,这将是 Android“R”系统映像。选择下载将触发许可接受屏幕,如图 3-6 所示。
图 3-6
AVD 系统映像的许可接受屏幕示例
我不是律师,当然也不在电视上演律师!更严重的是,这意味着我不能告诉你许可文本的含义。如果你担心,就去找法律顾问。但是从一个外行的角度来看,用于 Android 系统映像的开源许可通常不会让你失眠。我假设您接受了显示的文本,并点击了“下一步”按钮,现在正盯着图 3-7 所示的 Android 虚拟设备(AVD)配置验证屏幕。
图 3-7
Android 虚拟设备(AVD)配置验证屏幕
使用此屏幕检查您对新 AVD 的配置是否满意,最重要的是,给它起一个好记的名字。您可以随意称呼您的 AVDs 无需保留型号或使用可能继承自某个预配置系统映像的名称,尽管将其作为名称的一部分有助于快速提醒您仿真设备的功能。选择一个名称,然后点击完成按钮,前往 AVD 管理器主屏幕,如图 3-8 所示。
图 3-8
填充的 AVD 管理器主屏幕
现在,您应该会在可用模拟设备列表中看到新创建的 AVD。这就是创建您的第一个 Android 虚拟设备的全部内容——您现在可以编写自己的应用并在其上运行了!
已经开始创建您的第一个 Android 应用了!
您已经具备了开始使用您的第一个 Android 应用的所有先决条件。还有很多东西需要学习,但你已经有了关键的东西,包括 Android Studio 和一个新创建的 Android 虚拟设备,可以随时试用你写的任何东西。希望这种蓄势待发的状态不会让你感到惊讶。我在第二章中概述了 ide 的一些好处,您将在设置新应用的脚手架和框架方面直接体验到这些。
没有任何进一步的悬念,让我们开始吧。如果你已经按照本章前面的步骤完成了 AVD 的创建,你可能会回到你在本章前面看到的 Android Studio 主屏幕,如图 3-1 。在主屏幕上,选择屏幕顶部Start a new Android Studio project
选项,您将很快看到创建新项目向导的第一个屏幕。
无论什么原因,如果你看不到主屏幕,或者重启了 Android Studio,它没有显示主屏幕向导,你可能会看到一个空的 Android Studio 屏幕或者一个已经加载了预先存在的项目的屏幕,如图 3-9 所示。
图 3-9
主屏幕消失时显示的 Android Studio
别慌!你也可以从这里开始一个新项目,但是打开文件菜单,选择New
➤ New Project
。这也将启动创建新项目向导。不管你选择了哪条路径,你应该很快就会看到如图 3-10 所示的向导
图 3-10
Android Studio 中的创建新项目向导
向导的第一页已经提示您选择要构建的应用类型。在屏幕上方,您会看到设备类别,如手机和平板电脑、Wear OS、电视等。目前,我们将坚持使用电话和平板电脑向导选项。看着手机和平板电脑下面显示的图标,你可能会挠头,不知道activity
、fragment,
等术语是什么意思。不要害怕。你很快就会掌握这些。在这个阶段,您可以将这个屏幕视为 Android Studio,询问您到底需要为您的新应用进行多少引导,以及您希望它代表您放置哪些现成的部分。例如,如果您希望开发一个依赖地图和导航功能的应用,Google Maps 活动模板为开发人员添加了一系列现成的地图、GPS 和位置支持选项。这个屏幕上的其他选项同样带来了其他好处,比如对 Google AdMob 广告活动的广告库支持。
我们将从一个非常普通的 Android 应用开始,创建这个简单应用的变体,展示我们在接下来的章节中讨论的每个功能。所以现在,你可以选择空活动选项。不要让 Empty 这个词迷惑了你,因为 Empty Activity 选项仍然部署了应用所需的大部分样板文件和基本项目结构。选中 Empty Activity,单击 Next 按钮,您应该会看到 Configure Your Project 屏幕,如图 3-11 所示。
图 3-11
Android Studio 中的配置您的项目屏幕
这个屏幕上显示的选项是你和添加你自己的代码和应用之间的所有障碍。对于当前的 MyFirstApp 项目,您需要指定以下设置:
名称:这应该是一个对您的应用有意义的名称。它是运行应用时将出现在屏幕上的标题,它是将出现在 Android 启动器屏幕上应用图标下的标题,如果你在 Google Play 商店或其他地方发布该应用,它是将出现在那里的名称。出于我们的目的,输入MyFirstApp
作为名称。
包名:在一个有成千上万的开发者为 Android 开发成千上万个应用的世界里,你如何确保两个应用在命名上不冲突?Android 利用了从 Java 标准派生的包名的基本机制,而不是强制使用唯一的名称(尽管 Google Play 这样的地方在这方面有一些限制)。我们将在第七章中讨论更多关于 Java 包命名的内容,所以现在你可以使用一个从我自己的域名中派生出来的包名—org.beginningandroid.myfirstapp.
保存位置:保存位置是磁盘上的文件夹,该应用的整个代码层次将存储在该文件夹中。这将包括菜单子文件夹,保存像源代码,配置文件,图像,视频等内容,这取决于我们如何完善这个应用。首先,新项目使用的空间将低于 1 MB。但这可能会增长很快,所以最好选择磁盘上有足够空闲空间的位置。如果您对缺省值满意,您可以接受它——在您的操作系统上的用户主目录下的一个名为AndroidStudioProject/MyFirstApp
的目录。
语言:打开你看到的选择列表,你会看到两个选项:Kotlin 或 Java。Kotlin 是 Android 支持创作应用的语言家族中的新成员。Java 是第一种,而且仍然是 Android 开发中使用最多、最流行的语言。选择 Java 作为 MyFirstApp 的语言。
最低 SDK:这是我们从第二章和本章开始讨论 Android 版本、Android Studio 版本和 Android SDK 版本的地方。您在这里的选择将决定该应用使用哪个版本的 SDK,因此在很大程度上决定了它将与 Android 设备历史上的哪些设备兼容。较新版本的 Android SDK 提供了对新功能的支持,而旧版本更有可能在旧设备上得到支持,因此支持的设备也更多。您将在您的选择旁边看到额外的文本,表明在全球使用的已激活 Google 设备中,有多少百分比支持您选择的 SDK 版本。对于新功能和广泛支持之间的最佳权衡,没有完美的答案。目前,我们的初始应用不会使用最新发布的 Android SDK 的任何前沿特性,所以您可以使用默认的 API 16,它最初是为了配合几年前的 Android 4.1 Jelly Bean 发布而发布的。如果您愿意,可以滚动列表选择另一个 SDK 版本。如果您选择的 SDK 版本当前没有安装在您的计算机上,它将与完成设置该初始应用所采取的步骤一起被下载–请确保考虑每个 SDK 版本所使用的 100 MB 以上的磁盘空间。
使用传统的 android.support 库:多年来,谷歌尝试了许多方法来处理活跃的 android 设备使用的版本碎片。一种方法是(现在仍然是)使用不依赖于任何特定 SDK 版本的附加支持库,并让应用利用这个支持库。微妙之处在于,与任何特定设备的 Android 安装相比,谷歌更有能力推送支持库的更新。在接下来的章节中,当我们讨论 androidx 和 Jetpack 时,我们会对此进行更多的讨论。目前,您可以将该设置保留为缺省的未选中状态,这意味着我们不会引入对这些库的支持。
你现在离一个可运行的 Android 应用只有一步之遥了。单击 Finish 按钮,Android Studio 就会活跃起来,为您的新 Android 应用及其项目文件夹生成结构和框架。过了一会儿,你最终应该会看到完整的项目和 Android Studio 的完整开发者界面出现,如图 3-12 所示。
图 3-12
Android Studio 显示了打开进行编辑的 MyFirstApp 项目
编写您的第一个代码
如果你查看新项目的布局,如图 3-12 所示,你会看到一个文件夹/目录层次结构,有各种各样的名字,比如manifests
、java
、res,
等等。我们将探索完整的项目布局,您看到的目录,填充它们的启动文件,以及您在下一章构建的部分。
现在,为了让您获得一个具有个人风格的工作应用,我们将跳过对这些项目的解释,直接开始编辑您的第一个应用组件。使用您在 Android Studio 布局左侧看到的项目层次视图,向下点击res
文件夹,然后点击layout
子文件夹。您应该会看到一个名为activity_main.xml
的类似文件的条目(因为这些实际上是文件!).双击activity_main.xml
文件,会在 Android Studio 的编辑器视图中打开,如图 3-13 所示。
图 3-13
在 Android Studio 中打开您的第一个源文件
看代码截图很繁琐,所以让我们看看activity_main.xml
文件的内容。您的文件应该类似于清单 3-1 。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 3-1The contents of the activity_main.xml file in a new Android Studio project
这个文件中有几个不同的部分将来会让我们感兴趣,但是现在我们只对这里的一部分代码感兴趣,你可以看到粗体部分。您会注意到,在文件的中间是这样一行
android:text="Hello World!"
继续编辑双引号内的字符串,因此它不是读"Hello World!"
,而是读类似"Hello Android!"
或任何你喜欢的东西。编辑完该文件后,保存您的更改。您刚刚对 Android 应用进行了首次自定义编辑!
准备运行您的应用
现在,您已经准备好让 Android Studio 在本章前面创建的 AVD 模拟器中运行您的新应用。为了让 Android Studio 做到这一点,当您要求它运行应用时,它需要知道您的特定偏好是什么。例如,您是希望它正常运行,以便与它进行交互,还是希望它以调试模式或其他方式运行,以便仔细检查代码中发生了什么,或者您的应用与 Android 主机设备、您可能调用的其他 API 或基于云的 API 等外部服务之间的交互发生了什么?
Android Studio 通过所谓的运行配置来控制这些运行应用的替代方法。这些是运行应用时关于“运行”确切含义的预设说明。
你可以触发 Android Studio 来引导你设置你的第一次运行配置,只需要立即运行你的应用。打开Run
菜单,选择Run...
选项。在没有任何预先存在的运行配置的情况下,Android Studio 现在会提示您编辑配置,如图 3-14 所示。
图 3-14
编辑首次运行配置的提示
Note
通过从“运行”菜单中选择“编辑配置…”选项,您始终可以直接编辑运行配置,而无需强制运行应用。
无论您以何种方式触发了第一次运行配置的创建,您都会看到显示的运行/调试配置屏幕。屏幕上的文本将指示单击“+
”按钮来添加新的配置,您应该这样做。您将看到如图 3-15 所示的选项列表。
图 3-15
选择新的基本运行配置
选择列表顶部的 Android 应用选项,然后您将看到如图 3-16 所示的详细配置屏幕。
图 3-16
新运行配置的详细配置
您需要进行两项设置。第一个设置是为您的运行配置提供一个容易记住的名称。我建议你使用运行配置 1 这个名字。接下来,您需要指定在运行应用时,运行配置应该启动哪个代码模块。列表中唯一的选项是 app,这很幸运,因为这是你想要的选项。您可以在图 3-16 中看到这两种设置。
单击应用和确定按钮保存您的配置。
安装(附加)SDK 包
根据您在本章前面如何选择应用的最低 SDK 设置以及首次安装 Android Studio 时选择的选项,您的计算机上可能没有安装 Android Studio 用于构建应用的相关 SDK 包。你可以很容易地发现这一点,因为你在 Android Studio 中的默认视图在屏幕的左下角包括了当前的构建状态。
如果您在“Build: Sync”区域看到如下的错误或警告,您可能需要下载一个 SDK 包来匹配为您的项目选择的包:
License for package Android SDK Platform 29 not accepted
或者
Module 'app': platform 'android-29' not found
警告或错误中提到的版本号在您的安装中可能会有所不同。无论是什么版本,都可以通过调用 SDK 管理器特性(IDE 提供的另一项集成)轻松解决这个问题。从 Android Studio 的工具菜单中,选择 SDK 管理器选项,您应该会看到 SDK 管理器出现,如图 3-17 所示。
图 3-17
Android SDK 管理器
要添加您的项目所需的 SDK,只需选中相关版本旁边的框——在我的例子中,就是“Android 10.0 (Q)”,这相当于 API 级别 29,或者换句话说,Android SDK 的版本 29。单击 Apply 按钮,SDK 管理器将触发此版本 SDK 的下载。如果你坐下来想一想,你会意识到,随着你构建越来越多的应用,并可能为它们选择不同的目标 SDK 版本,你最终可能会在你的机器上安装相当多不同版本的 Android SDK,这些将占用相当大的磁盘空间。我们将在第六章中再次讨论这个主题,届时我们将深入了解您的整个开发人员系统设置。
一旦任何所需 Android SDK 版本的下载完成,您应该会看到如图 3-18 所示的状态。
图 3-18
Android SDK 管理器指示成功下载并安装了新的 SDK 版本
此时,您需要让 Android Studio 刷新集成的 Gradle 构建工具的配置,以便它意识到新的 SDK 已经就绪。Android Studio 可以采取所有必要的步骤来做到这一点——因此,如果您对作为构建工具的 Gradle 一无所知,请不要惊慌。只需打开File
菜单,选择显示Sync Project with Gradle Files
的选项。
当这个同步活动完成时,在屏幕左下方的构建窗口中,您应该看到先前的警告和错误现在都消失了,唯一出现的消息应该是MyFirstApp successful at
some-date-and-time
。
运行应用
Android Studio 中的完整设置满足了您的应用和 Gradle 的同步需求,并准备好构建您的应用,您的运行配置现在应该可以实际运行您的新应用了。继续从运行菜单中选择运行,或直接选择Run 'Run Config 1'
跳过运行配置选择步骤。
一系列操作现在将自动发生,但它们可能需要一点时间——最多几分钟,具体取决于您计算机的性能。首先,将调用 Gradle 来构建您的应用。我们将在后面的章节中更详细地介绍这一过程,但现在您需要知道的是,Gradle 正在将您编写的所有代码、您的应用引用的库、相关的 Android SDK 和其他管道整合在一起,并生成一个可以作为您的应用部署的包。这被称为安卓包,或 APK。
接下来,Android Studio 调用您的 AVD,在模拟器启动一段时间后,它会复制 Gradle 构建的 APK。基于您的运行配置,Android Studio 知道您想要运行应用模块,实际上触发您的新 MyFirstApp 应用在 AVD 中运行,就像您在 Android launcher 窗口中按下它的图标一样。
你的应用会运行,你的不朽的话“你好安卓!”(或者你选择的任何东西)应该出现在一个名为 MyFirstApp 的应用屏幕中,就像你在图 3-19 中看到的那样。
图 3-19
在 AVD 中运行 MyFirstApp Android 应用
就这样!您已经编写并运行了您的第一个 Android 应用。干得好。深呼吸,因为在第四章中,我们将深入到引擎盖下,详细检查 Android Studio 刚刚为您构建的一切。
四、探索你的第一个项目
在第三章中,我向你介绍了在 AVD 上编写和运行你的第一个 Android 应用所需要的一切,目标是让你很快沉浸在 Android Studio、仿真器和你的第一部分编码中。我们很快就有结果了!但是当我们快速浏览的时候,你几乎肯定会有一大堆没有答案的问题冒出来。在这一章中,我们将开始深入许多主题,从你的新 Android 项目的结构和特性开始。这应该开始回答这些问题,并建立你的知识。
查看整个 Android 项目结构
为了熟悉 Android 项目各个部分的结构和目的,从 30000 英尺(或者 10000 米,对于公制单位来说)的视角开始会有所帮助。从上一章来看你的项目的整个布局,结果是惊人的大。虽然您只编辑了一个文件,但是 new project 向导已经为您创建了许多其他文件,并且在创建项目时,还会复制或添加其他文件,如 Java 库、Android SDK 等等。
要查看这个完整视图,请使用 Android Studio 窗口的左侧窗格,即所谓的项目浏览器视图,来展开 app 文件夹;然后是它下面的文件夹比如 manifests、java 等等;以及从顶层向下的所有其他文件夹。您将最终得到一个类似于图 4-1 的整个项目的视图(注意,为了节省空间,我并排显示了长项目浏览器的连续视图)。
图 4-1
项目浏览器中项目的逻辑 Android 视图
您看到的默认视图称为 Android 视图,旨在简化项目所有组成部分的显示方式。这种简化的目标是让您专注于您(开发人员)编写或编辑的部分,而将其他大部分支持文件排除在外。
从这个完整的项目 Android 视图中,您可以看到项目被分解成五个主要的文件组,它们代表了项目的不同部分。
使用清单
在项目浏览器的 Android 视图中显示的第一组文件是您的清单文件。对于像 MyFirstApp 这样的新项目,您只需要关注一个清单文件,即名为AndroidManifest.xml
.
的文件
这是您的 Android Manifest 文件,它的作用就像是您的 Android 应用的主控制文件。在 Android Manifest 文件中,您能够控制您的应用的许多主要参数和行为,包括声明您的应用需要什么权限才能运行,它可以与什么服务交互,组成应用的活动,以及应用需要和支持的 Android SDK 级别。
我们将在本章后面更深入地讨论这些核心概念,包括定义 Android 意义上的活动和服务。现在,让我们看看您的AndroidManifest.xml
文件的内容,您可以在清单 4-1 中看到。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.beginningandroid.myfirstapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Listing 4-1The contents of your new AndroidManifest.xml file
正如您可能从文件名中猜到的那样,内容是 XML 格式的。对于第一次接触 XML 的人来说,第八章提供了 Android 开发中 XML 的完整介绍。一旦熟悉了 XML 的基础知识,就可以跳过这个主题,回到这一点。
Android 使用 XML 文档的根元素<manifest>
,然后引入两个关键属性:名称空间声明和包声明。就名称空间而言,Android 惯例是只在属性级别使用它们,而不是在元素中使用它们。包名org.beginningandroid.myfirstapp
对你来说应该很熟悉,因为这是我们在第三章的项目设置向导中提供的包名。除了提供这种必要的映射来确保应用的完全限定包名的唯一性之外,这种技术还允许我们在将来需要引用应用本身时使用简写。
您可以在清单中看到这一点,第一个<activity>
元素以<activity android:name=".
MainActivity">
的形式被引入。这里,前导点是在<manifest>
包属性中引用的完整包名的简写。实际上,这意味着内部引用的名称要短得多,所以在这个例子中,用.MainActivity
代替org.beginningandroid.myfirstapp.MainActivity
。我知道我更喜欢打哪一个。
清单文件还包括其他关键功能。例如,android:icon
和android:roundIcon
属性引用了两个资源文件,这两个文件在 Android 设备的启动器屏幕和小部件屏幕上为您的应用提供了方形和圆形图标版本。l 属性保存了你的应用的名字。
另一个重要的条目是前面提到的<activity android:name=".MainActivity">
。这个条目,包括子元素<intent-filter>
,向 Android 指示当应用启动并且键意图android.intent.action.MAIN
被触发时,它应该响应哪个活动。我们将在这一章中很快详细介绍这些概念,但是现在你可以认为这是一种 Android 方式来标记你的应用启动时第一次向用户显示的内容。
与 Java 共舞
Android 项目浏览器视图的第二和第三个主要区域是 Java 源文件。这些是文本文件,其中编写了 Java 代码,使您的应用具有生命力,并执行您希望应用具有的动作、特性和行为。
您将看到两个高级文件夹,一个用于Java
,一个用于Java (generated)
。事实上,在这个阶段,这两个区域中的所有文件都已经为您生成了,但是一般来说,当您开发应用时,您将更改Java
树中的文件,添加新文件,等等,并让 Android Studio 及其集成工具自动处理Java (generated)
文件。
现在有一个文件需要特别注意,那就是在Java
➤ org.beginningandroid.myfirstapp
文件夹下的MainActivity
文件。磁盘上的文件其实叫MainActivity.java
,项目浏览器中的 Android 视图是隐藏文件扩展名的。这个MainActivity
文件是应用启动时运行的代码,它基于前面提到的 Android Manifest 文件中的配置。
我们将在这一章的后面以及本书以后的许多章节中深入探讨活动的含义。
利用资源变得足智多谋
新项目中文件的第四个主要区域是资源文件,位于res
文件夹及其子文件夹中。在不同类别的资源中,有相当多的项目为您生成。
可抽的
Drawables 是 Android 将在屏幕上绘制或渲染的东西,作为应用的一部分。这意味着既有正常的图像,如艺术作品、图表和其他静态图像,也有在运行时根据指令生成的图像。
第一种 drawables 表示在您可能熟悉的典型图像文件中,如 GIF、JPEG 和 PNG 文件。Android 有放大和缩小图像的机制,以适应各种密度的屏幕/显示器,还可以让你存储同一张图像的不同分辨率版本,以避免缩放。在第十四章中,我们将深入研究图像、照片和艺术品的创作以及 Android 应用的技术。
Android 支持的第二种可绘制图像是它在运行时根据 XML 文件中提供的规范创建的矢量图像。您可以在您的新项目中看到两个提供矢量图形指令的 XML 文件示例—ic_launcher_background.xml
和ic_launcher_foreground.xml
。
布局
在第 9 和 10 章中,我们将详细探讨应用的布局和 UI 设计,涵盖一系列选项和风格。当您在第三章创建新项目时,您会记得为您的项目选择了“空活动”选项。Android Studio 使用这个选项来设置默认布局,它出现在 layout 子文件夹下的文件activity_main.xml
中。
同样,这是一个 XML 文件,Android 在定义活动的初始布局和行为特征时大量使用了 XML。如果您将一个活动想象成 Android 应用中的一个屏幕或窗口,然后由用户使用,那么 XML 布局就提供了如何创建和呈现界面的描述。当您的MainActivity
Java 代码想要在屏幕上绘制活动时,会使用这个activity_main.xml
布局。这个过程在 Android 世界中被称为“膨胀”,适用于布局整个活动界面的过程,以及它的任何子集,如创建一个菜单或在另一个布局中动态添加一个布局。随着应用的增长,该文件夹中活动布局 XML 文件的数量也会增加。
贴图
你可以把mipmap
文件夹看作是drawable
文件夹的一个特例。如果 drawables 是用于任何目的的图像或艺术品,则 mipmap 条目专门用于您的应用和 Android 设备的启动器屏幕的图标文件。ic_launcher
文件夹保存了图标的方形版本,ic_launcher_round
保存了相同图标的圆形版本。每个文件的多个实例以不同的分辨率(显示密度)存储,Android 将根据设备的显示特征以及应用或设备可能使用的任何显式配置覆盖,自动确定使用哪个密度版本。
我们将在后面的章节中更多地讨论显示密度及其含义。
在价值中寻找价值
在values
文件夹中,您会发现专门用于保存字符串、尺寸、样式和其他参考信息的文件。这种方法体现了几乎所有编程语言和环境中都有的简单抽象技术。与其在应用代码和配置文件中添加可能经常在许多地方重复的硬编码值,不如使用抽象来引用代码以外的某个值的单个定义,这样会使代码更干净,更不容易出错。这在许多方面都有帮助,例如为许多不同的屏幕尺寸和分辨率创建和使用价值资源提供了一个管理良好的机制。
在您当前的项目中,您可以看到应用名称出现在strings.xml
文件中。与其将我们的"MyFirstApp"
作为字符串嵌入到项目中任何需要的地方,不如在strings.xml
文件中引用这个条目。如果我们需要改变这个值,我们可以在一个地方完成,并且知道所有的引用都会自动正确地更新。虽然我们自己不会这样做,但这在诸如将应用国际化和本地化为其他语言等方面有具体的好处。
用 Gradle 文件构建一切
到目前为止,组成你的第一个 Android 应用的各种类型的独立文件的数量正在增长,达到两位数。将所有这些文件放在一起创建一个最终的、可工作的 Android 应用是构建系统的工作,Android Studio 默认依赖于一个名为 Gradle 的构建工具集。
Gradle Scripts
文件夹向您展示了各种脚本,这些脚本完成了从组成组件构建完整应用的工作。现在最值得注意的两个文件是同名的build.gradle
文件。您会注意到第一个标记为项目的——在本例中是Project: MyFirstApp
。第二个标注为Module: app
。
“为什么是两个build.gradle
档?”你可能会问。最长的答案是,事实上你可以有两个以上的。你现在看到的是所有 Android 项目都有的项目级build.gradle
文件。然后你会看到为项目定义的每个模块增加了一个build.gradle
文件,允许每个模块处理任何特定于模块的构建活动和参数。因为您的项目只定义了一个"app"
模块,所以您只看到了另外一个build.gradle
文件,总共有两个。随着我们阅读本书的进展,我们将使用将多个模块合并到一个给定项目中的例子,因此您将在相关项目中看到额外的build.gradl
e 文件。
现在,项目级的 build.gradle 文件是最令人感兴趣的,您可以在清单 4-2 中查看它的内容。
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "30.0.0"
defaultConfig {
applicationId "org.beginningandroid.myfirstapp"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
Listing 4-2The project-level build.gradle file
您有各种配置设置来控制如何构建您的应用,这些设置属于几个关键领域。这些是 SDK 版本控制和应用版本控制,目标应用构建的类型,以及作为项目基础所需的依赖项、插件和库的包含。
了解 SDK 版本参数
您将观察到几个提到 SDK 版本的参数,包括compileSdkVersion
、minSdkVersion
和targetSdkVersion
。这些服务有不同但互补的目的:
-
compileSdkVersion:指示 Android Studio 在实际编译和构建应用时使用哪个版本的 SDK——从您可能已经安装的许多版本中选择。通常你会选择最新版本的 Android SDK。
-
minSdkVersion:设置应用中使用本机 API 特性的阈值,并暗示确定支持应用的 Android 和 Android SDK 的最早版本。这反过来会设置可能使用您的应用的最老的设备。在可能的情况下,新 API 级别中引入的特性将由支持库或 Android Jetpack(稍后讨论)处理。
-
targetSdkVersion:控制应用将尝试使用的最新 API 特性,即使
compileSdkVersion
是比这个版本更高的版本。这意味着您可以从较新的 SDK 提供的编译时效率中受益,但是在您明确提升targetSdkVersion
之前,不会被迫对您的应用的行为进行重大更改。
了解应用版本
在发布 Android 应用时,就像几乎所有其他软件一样,版本化的概念用于表示修复 bug、添加新功能、改进现有行为等的后续版本。
Android 使用两个参数,您可以在 build.gradle 文件中找到。最重要的是versionCode
参数,传统上它是跟踪应用给定构建/发布版本号的参数。versionCode
参数是一个整数,当你释放新的值时,它只会增加。
Note
在内部,从 SDK 版本 28 开始,Android 也提供了更新的versionCodeMajor
参数。这是一个long
值,而不是一个integer
,所以可以容纳更高的值。这目前只能通过在 Android Manifest XML 文件中加入versionCodeMajor="n"
符号来成功设置。Gradle 构建文件目前不支持此参数。
Android 还提供了一种向用户显示某种有意义的人类可读版本的机制,来源于versionName
字符串值。作为一名开发人员,如果您想尝试向用户传达诸如主要版本和次要版本之类的信息,这将对您有所帮助,这是软件开发的一个传统(尽管不是必需的)习惯。
包括向前和向后版本支持的库
现在要注意的 Gradle 构建文件的最后一个方面是依赖部分。我不会深入讨论现阶段声明的每个依赖项,因为我们将在后面的测试、手机/版本兼容性等章节中讨论它们的细节时,再回到每个依赖项。我将强调的一个常见方面是许多依赖项的名称空间。您会注意到主要组件的名称是androidx
。名称androidx
指的是 Android Jetpack,它是 Android 方式的最新更新和刷新,Android 允许您作为开发人员针对许多不同版本的 SDK,而无需事先了解它们——实际上,使您的应用免受后来的 SDK 版本中合并的一些更改的影响,同时还允许您构建具有当代 SDK 版本功能的应用,并让 Android 和 Jetpack 担心如何在不明确支持您的 SDK 版本的旧版本 Android 设备上运行时模仿这样的新行为。Jetpack 还有助于减少用 Java 编写软件的冗长,减少样板代码,并帮助您遵循 Android 开发人员社区随着时间的推移而学到的良好实践。
Jetpack 正在取代一种更老的方法,即 Android 支持库,但是在许多参考资料和讨论中,您仍然会看到支持库,并且在未来的一段时间内,这两种支持库将会并存。
解释关键的 Android 应用逻辑构建模块
现在,您对 Android Studio 中的项目结构和布局有了一些了解。但是在这一章的几个地方,我已经提到了 Android 应用的一些基本的逻辑构建块,比如活动和服务。现在是时候多了解一下这些构件是什么,以及它们是如何组合在一起使您的应用变得生动的。
活动
如果你使用过任何现有的 Android 应用,你会体验到它与大多数带有用户界面的软件有一些共同的主题。像其他有用户界面的软件一样,Android 使用了“屏幕”或“视图”的概念,用户可以在这里与你的程序进行交互。Android 调用这些活动,无论你想让你的用户阅读文本,观看视频,输入数据,打电话,玩游戏,或者你有什么,你的用户都会通过与你设计的一个或多个活动屏幕或布局进行交互来完成这些。
Activities 是 Android 中最容易开发、计算成本最低的部分之一,您应该在应用中慷慨地创建和使用它们。这不仅有助于你的用户从你的应用中获得良好的体验,而且 Android 操作系统在设计时就考虑到了活动的扩散,并在管理这些活动方面提供了很多帮助。
意图
Intents 是 Android 的内部消息传递系统,允许在应用之间以及与 Android 环境之间传递信息。通过这种方式,您的应用可以触发操作,并与 Android 和其他应用共享数据,它还可以监听正在发生的事件,并在适当的时候采取行动。
Android 操作系统已经有了非常广泛的应用可以与之交互的意图,作为开发者,你也可以定义和开发自己的意图供自己使用。
服务
服务是计算世界中的一个常见概念,虽然在 Android 中处理它们时有细微的差别,但许多常见概念仍然适用。服务是通常没有用户界面,而是在后台运行的应用。服务提供了多个应用通常需要的一系列特性、功能或行为。服务通常是长期存在的,在后台运行,根据需要帮助您的应用和其他程序。
内容供应器
您的应用可能想要使用许多您自己无法控制的数据类型和派生。许多其他应用都有类似的数据需求,因此 Android 附带了内容供应器的概念,这是一种抽象数据集和数据源的方式。这种抽象试图简化与各种数据源的交互,包括文件、数据库、流协议和其他访问数据的方式,为开发人员带来一些逻辑一致性。您不必为要使用的每组数据或每种数据类型学习不同的自定义数据处理方式,只需学习一次内容提供者方法,就可以在许多不同的内容提供者数据源中重复使用它。
您还可以构建和共享您自己的内容提供者,促进与其他应用的数据共享和交互。
摘要
现在,您已经了解了典型的 Android 开发项目是如何构建的,以及 Android 应用的一些主要构件是如何在逻辑上和构建应用的过程中组合在一起,从而创建一个完整的应用的。我们将继续在本章所描述的基础上进行构建,扩展我们对其中许多部分的探索,以便您了解越来越复杂的应用是如何构建的。
五、Android Studio 深入
像 Android Studio 这样的 IDE 的长期用户通常会对他们选择的 IDE 有多重要形成一种非常强烈、非常狭隘的观点——不仅对他们的生产力,而且对他们编写应用的乐趣。在这一章中,我的目标是展示和探索 Android Studio 的一些关键特性,这些特性将使您在构建 Android 应用时更加高效和快乐。
Android Studio 本身就是一个巨大的话题。有整本书都是关于 Android Studio 本身以及如何充分利用它作为 IDE 的。这本书的发布商 Apress 的一些很好的例子是 Android Studio IDE 快速参考(由 Ted Hagos 编写,ISBN 978-1-4842-4953-6)和 Learn Android Studio 系列(也由 Ted Hagos 编写)。在本书中,我们没有足够的时间将剩下的章节都用来讲述 Android Studio 本身,但是如果这一章让你对一个伟大的 IDE 所提供的可能性感到兴奋,你就知道下一步该去哪里了。现在,让我们深入研究 Android Studio 的一些关键部分,你现在应该开始了解。
从项目浏览器开始
回到第四章,我们简要介绍了项目浏览器和它的一些功能,展示了 Android 透视图和项目文件透视图。我们简要地研究了项目的这两个视图,但是没有详细讨论其他的项目浏览器视图选项。这让我们想知道,所有这些其他的观点是为了什么?
很高兴你问了。概括地说,Project Explorer 的其他视图选项旨在满足两个兼容的需求。首先,这些视图为您提供了查看项目的不同方式,通常强调或专注于一个特定的领域,以便您的时间可以用于完成工作,而不是与 Android Studio 的布局进行斗争。其次,这些观点也支持你作为开发人员的个人偏好和愿望,以及你更喜欢的工作方式。
轻松切换项目浏览器视图
让我们来看看这些视图中的一些,您可以探索所有其他视图,感受一下您喜欢的内容。图 5-1 显示了我们的 MyFirstApp 项目切换到使用项目源文件视图(同样,不是很长的图像,我并排粘贴了两个屏幕)。
图 5-1
Android Studio 项目资源管理器中的项目源文件视图
您可以在 src 文件夹层次结构下看到一些熟悉的条目,例如,myfirstapp Java 源文件及其 MainActivity 代码。如果您在这个视图中稍微浏览一下,就会看到其他的源文件,比如用于测试代码的模板文件,以及一些可编辑的 XML 文件,这些文件控制着可视化布局、可重用的字符串值等等。
但最值得注意的是没有显示出来的东西。您再也看不到 Gradle 构建文件、IntelliJ IDEA 首选项和配置文件、构建工件以及其他非代码项目。所有这些都是隐藏的,所以您可以专注于代码!“为什么要这样做?”我听到你问了。除了我在前面提到的个人偏好和专门化观点之外,Project Files 视图尤其是一些开发人员的最爱,他们发现当他们处于“流动”状态时工作得最好:一种精神状态,在这种状态下,您深深地沉浸在代码中,没有分心。
另一个流行的视图是项目视图,它看起来就像许多传统的 Java IDEs 一样,展示了项目的元素和组成部分。图 5-2 显示了这个视图,你会注意到一些直接的不同,特别是外部库和引用是如何出现的。
图 5-2
Android Studio 项目浏览器中的项目视图
我强烈建议您探索 Project Explorer 中的所有视图选项,并找到最适合您工作方式的变体。
需要记住的一点是,不管你使用什么视图或者切换视图多少次,Android Studio 都不会改变你的项目或者移动文件。它只是给你一个不同的虚拟视图。
使用项目浏览器上下文菜单
与您可能使用过的许多其他应用一样,Android Studio 提供了上下文菜单,您可以通过右键单击鼠标(或在 Mac 上按住 command 键单击)来访问这些菜单。当您在 Project Explorer 中浏览视图时,您应该调用您所看到的所有内容的上下文菜单,以便对所提供的内容有所了解。无论是比较、查找等文件管理操作,快速编辑选择,还是开始更严肃的代码管理操作,上下文菜单都是您应该熟悉使用的专业工具之一。
上下文菜单中的所有选项都可以在 Android Studio 的主菜单中找到,但这些选项通常被隐藏在三到四层菜单中。
使用 Android Studio 运行和调试
正如你在第三章中所看到的,让 Android Studio 运行你的程序的一个主要方法,无论是仿真的还是在设备上运行的,就是配置一个运行配置。我们稍后将再次讨论运行配置,但最好知道还有其他方式来运行您的应用。
使用运行配置运行:概述
我们在第三章的目标是让你的第一个应用尽可能快地运行,所以我们跳过了许多运行配置的细节。有一些关键的方面值得重新审视,因为您很快就会想要创建具有不同特征的进一步运行配置,以便在应用开发的不同阶段为您提供帮助。
通过 Android Studio 的运行➤编辑配置…菜单,调出“运行配置 1”的详细信息(或之前您用来保存运行配置的任何名称)。你应该会看到熟悉的屏幕显示你的运行配置,如第三章的图 3-16 所示。对于任何运行配置,有各种选项分布在四个子窗口中。其中一些您可以在自己的时间里探索,但是您需要熟悉的主要配置项目如下。
运行配置:常规选项
Module:指定 make 过程的目标模块,如果没有指定,则意味着整个项目都应该是目标。实际上,这有助于更大的多模块项目,在某些情况下,您可能只想重建/重新制作一个特定的模块——通常是因为这是唯一改变的模块。
install Options–Deploy:指示 Android 应用构建完成后应该发生什么。是应该在设备或模拟器上运行,因此需要部署它,还是您只对确认应用将干净无误地构建,而不需要实际运行它感兴趣?
启动选项–启动:该设置控制当成功的构建被部署和启动时会发生什么。应用应该按照 Android Manifest XML 文件运行并启动默认活动,还是应该在这个特定的运行配置中使用不同的起点(例如应用中的替代活动或根本没有活动)?
安装标志和启动标志目前不在我们运行配置的讨论范围之内。
运行配置:杂项
杂项选项控制在部署和启动构建时,如何“清理”您的目标环境。所谓清理,我指的是日志、应用的先前版本等等作为运行配置的一部分被留下或清理的工件。
Logcat:我们将在本章的后面更多地讨论 Logcat,但是这些设置决定了 Logcat 的输出是否应该默认地通过 Logcat 工具显示给你,以及之前运行的输出是否应该被清除。如果任其增长,这些日志会变得非常大,所以如果这些日志变得难以处理,请记住清除日志设置。
安装选项:如果什么都没有改变,您可能想跳过再次部署您的应用,这可以在您更广泛的工作流中节省时间。知道自己不会影响正在运行的应用实例是很重要的,例如,在保存状态或从已知的良好启动条件下运行时。
运行配置:调试器
那些在编写任何类型的软件方面经验丰富的读者会意识到,这不是一项完美的任务。事情出错,意外的行为发生,应用由于不可预知的原因而失败,等等!
一旦您花了一些时间编写、调试和审查代码,调试器屏幕上显示的选项随着时间的推移会变得更有意义。无论您的经验水平如何,都需要注意以下关键选项:
调试类型:本质上,这为 Android Studio 提供了关于项目中预期的代码类型的指导。这可以显式地设置为 Java、Native(通常指通过 Android 的本地开发工具包扩展的 C++)、Dual(Java 和 Native)或 autodetect。确切地指定预期的语言,而不是依赖于自动检测的一个优点是,在让 Android Studio 检查您的项目的语言类型时,您将节省少量时间,而不是让它为不存在的语言加载调试工具。
显示静态/全局变量:当出现问题时,许多开发人员首先使用的工具之一是检查工具——也就是说,当出现问题时,显示部分代码的当前状态的工具,例如分配给特定变量的值。该选项将静态/全局变量以及您在整个代码的方法中定义的变量添加到您的视图中。
调试优化代码时发出警告:语言编译器的一个非常有用的特性是,它们能够优化人类编写的代码,使其运行更高效。发生这种情况时,运行的代码严格来说并不是您编写的代码,即使结果应该是相同的。此设置允许您在转到调试已为您优化的代码时得到警告。
符号目录和 LLDB 选项等其他设置超出了本书的范围。
运行配置:分析
任何形式的 Android 评测工具都花了很长时间才达到开发人员所需的成熟度和洞察力,以帮助他们了解他们的应用正在产生的环境需求。甚至在几年前,在 Android Studio 和 Eclipse 中,工具仍然达不到大多数人的期望。今天,这些工具一直在变得越来越好,当我们在后面的章节中深入研究这些领域时,我们将触及其中的一些。其中一些改进的选项可以直接在运行配置中进行配置,如下所示:
启用高级概要分析:对于任何使用 API 级别为 26 或更低的目标 SDK 的人来说,试图确定您的应用的即时环境中正在发生什么可能会令人沮丧,例如,网络行为和流量、处理或丢弃的事件等等。Enable Advanced Profiling 选项打开了为旧的目标 API 级别捕获更多指标的能力。对于任何以当代 API 级别为目标的人来说,例如 30+,这现在是 Android Studio 和相关工具中标准剖析行为的默认部分。
启动时开始记录 CPU 活动:该设置旨在分析应用使用时 CPU 周期的消耗情况。这至少有两个好处:第一是了解你的应用的各个部分将如何执行,这在考虑用户满意度时很重要——一个缓慢的应用可能会导致用户对你的应用感到失望。第二个好处是去掉了一个抽象层次。因为 CPU 使用是智能手机电池最繁重的事情,CPU 活动记录可以帮助您了解您的应用在使用时是否会成为电池消耗的重要因素。CPU 活动设置可能不会在您早期的 Android 学习经历中开始使用,但一旦您开始探索 CPU 密集型 Android 功能,如视频使用、设备上的计算等,它将成为您开发人员工作流程的支柱。
使用 avd 和连接的设备跑得更远
在第三章中,我们经历了创建初始运行配置的步骤,然后使用我们创建的第一个名为Pixel 2 API 30
的 AVD 来实际运行您的应用,并向您显示最终的活动屏幕。使用 AVDs 是 Android Studio 为您提供的最强大的功能之一,使您能够跨大量虚拟设备进行测试和实验。
从技术上来说,AVD 管理器是一个独立的工具,Android Studio 集成并提供它来简化你的生活——这是我在第二章中介绍的“集成”咒语。除了能够从 Android Studio 中的工具菜单或工具栏按钮(显示为微型手机图标)启动 AVD 管理器,您还可以直接从命令行或 shell 运行 AVD 管理器的命令行版本,作为 SDK 工具目录中的独立工具。
为了找到 SDK 工具目录,从您的 Android 安装路径的根目录(例如,在 macOS 上的~/Library/Android
或者在第二章中您为您在 Linux 或 Windows 下的安装选择的 Android 目录),您将找到一个sdk
子目录。在sdk
子目录中,跟随进一步的子目录tools ➤ bin
。包含在其中,您会发现一个名为avdmanager
的二进制可执行文件。你可以从它的名字中猜出这是独立的 AVD 管理器可执行文件。如果您在查找 SDK 工具目录时遇到任何问题,请使用操作系统的搜索功能。
当您从命令行运行时,您会得到命令行输出,如清单 5-1 所示,这取决于您传递给 avdmanager 命令的选项。
$ avdmanager list avd
Available Android Virtual Devices:
Name: Pixel_C_Tablet_API_30
Device: pixel_c (Google)
Path: /Users/alleng/.android/avd/Pixel_C_Tablet_API_30.avd
Target: Google APIs (Google Inc.)
Based on: Android API 30 Tag/ABI: google_apis/x86
Skin: pixel_c
Sdcard: 128 MB
Listing 5-1Command-line output for the avdmanager tool
重温 AVD 管理器
现在你有很多方法来启动 AVD 管理器,继续从 Android Studio 工具菜单启动它,因为我们将为将来的练习创建第二个 AVD。如果你需要帮助,你可以重温第二章 2 的逐屏操作说明,但这里是我们第二个 AVD 的目标:
类别-平板电脑:我们想创造一个平板电脑大小的 AVD,这将有助于用书中的例子测试更大的设备。
名称和类型——Pixel C table t API 30:这是最好的多功能平板电脑之一,也是最近推出的一款直接从谷歌安装了简单安卓系统的平板电脑,是测试平板设备的绝佳基准。
系统映像–R(API level 30):这是最新的 SDK,它带来了 Android 为平板设备提供的最新功能。
注意您可能会看到 R 系统映像旁边有一个蓝色的下载链接。这表明您的计算机上尚未安装必要的英特尔 x86 系统映像。您需要单击下载链接并继续安装系统映像,以便此 AVD 能够成功启动并运行。
AVD 名称–Pixel C table t API 30:您可以使用您喜欢的任何名称,但是当我们在 AVD 管理器中看到 AVD 时,或者当正在运行、测试和调试您的代码时,作为正在运行的仿真器进程之一,此示例名称添加了一些有用的线索。
完成后,您应该在您的虚拟设备窗口中看到(至少)两个 avd,如图 5-3 所示。
图 5-3
AVD 管理器中配置了多个 AVD
创建第二个(或第三个或第四个)AVD 的目的不仅仅是显示 AVD 管理器的另一个屏幕截图。要使用 Android Studio 在多个设备或 avd 上同时运行相同代码的能力,需要有两个或更多 avd。
在您的 Android 开发工作中,有许多原因会让您想在不同的 avd 和设备上同时测试这种能力。在前面的章节中,我们提到了世界各地销售和使用的设备的多样性,支持的 Android 版本和 SDK 版本的分散性,以及其他因素,这些因素意味着您的应用可能会在许多微妙不同的设置中运行。首先,在几个设备上并行运行你的应用可以帮助你发现你的用户在现实世界中可能看到的一些怪癖和差异。这种能力的第二个主要好处是可以看到你的应用在小屏幕上一次显示一个活动的效果,而在大屏幕上——比如平板电脑——你的活动可能会缩放到不同的大小,或者使用片段作为多活动显示的一部分出现(我们将在第十一章中介绍)。使用多设备或 AVD 测试的第三个原因是观察 Android 如何在不同显示密度和分辨率的屏幕上缩放或插入特定分辨率的图像。
您应该立即在多个 avd 上测试运行 MyFirstApp 应用。您可以从工具➤选择器件菜单选项或图 5-4 所示的器件选择等效工具栏下拉菜单中触发此操作。
图 5-4
设备选择工具栏下拉菜单
选择“在多个设备上运行”选项,将会出现一个名为“选择部署目标”的对话框。选择两个或更多的设备或 avd,例如,我们在书中迄今为止已经创建的 Pixel C Tablet 和 Pixel 2 AVDs,如图 5-5 所示。
图 5-5
选择多个目标以在 Android Studio 中启动您的应用
同时启动多个 avd 将花费一些时间,但是经过一点耐心,您应该会看到您的 avd 启动,并且 MyFirstApp 应用部署并运行在所有 avd 上,如图 5-6 所示。
图 5-6
同时部署在多个 avd 上的应用
在真实设备上运行您的代码
虚拟设备对任何开发人员来说都是一个福音,但是有时候在真实设备上看到你的代码是一件很平常的事情。在最近发布的 Android Studio 中,谷歌在真实设备上测试和运行应用方面取得了巨大进步。历史上,将应用部署到真实的手机上需要一长串命令行步骤,虽然今天您仍然可以走这条路,但 Android Studio 使检测和使用通过 USB 电缆连接到您的开发人员机器的 Android 设备变得很容易。
为了在开发应用时使用 Android 手机测试它们,您需要在手机上启用开发者选项。谷歌一直有一个窍门来实现这一点,那就是打开手机上的设置,滚动到关于手机选项。“关于电话”屏幕上的最后一个选项是内部版本号。点击显示的数字七次(是的,七次),你会看到一个倒计时出现,让你知道你只剩下几个点击在开发者模式启用之前。
一旦你点击了足够多的版本号,你会看到一个屏幕通知,开发者选项现在已经启用,一个新的菜单选项将出现在设置下,显示开发者选项。默认情况下,它会启用一个名为 USB 调试的子选项。仔细检查你手机上的情况,如果还没有启用,就打开它。
在手机上启用开发人员模式后,通过 USB 电缆将其连接到您的开发人员计算机。您的手机应该会被自动检测到,然后 Android Studio 会将其识别为运行您的 Android 应用的潜在目标。
为了进行测试,从菜单中选择Run
➤ Select Device
,或者打开 avds 设备工具栏上的下拉菜单,如图 5-7 所示。
图 5-7
从 Android Studio 选择连接的开发人员模式 Android 设备
选择您的连接设备,在我的例子中是 Nexus 5 手机。等待几秒钟,Android Studio 将构建、打包和部署应用到连接的设备,然后您应该会看到您的应用加载并在您的手机上运行,如图 5-8 所示。
图 5-8
在连接的 Android 手机上运行 MyFirstApp
调试而不是运行您的代码
任何刚接触应用开发的人最终都需要了解当您的代码“出错”时会发生什么无论是意外的结果、奇怪的行为、应用崩溃还是其他问题,调试都是解决应用问题的主要方法。
调试是一个巨大的主题,所以我们不会在专门讨论 Android Studio 工具的章节中试图掌握它,我们将首先关注 Android Studio 中的主要调试工具,然后在整本书中,我们将在介绍更复杂的 Android 应用时扩展调试和相关主题。您也可以在本书网站上阅读更多关于调试的主题,网址为 www.beginningandroid.org
。
在 Android Studio 中,有助于调试的四个关键概念如下。
设置和清除断点
断点是源代码中的一个标记,它指示要在何处中断或停止代码的执行,以便更详细地检查行为或问题。要在代码中设置断点,在项目浏览器中双击文件或对象,打开您想要处理的代码——在我的例子中,它将是 MainActivity.java 文件。在文件打开的情况下,点击文件旁边显示的行号右侧的暗灰色空白处,如图 5-9 所示。
图 5-9
在 Android Studio 中为调试设置断点
您应该会看到在您单击的地方出现一个红圈,表示设置了一个断点。如果出于任何原因,这似乎没有使代表断点的红圈出现,请打开“工具”菜单并选择“切换行断点”。再次单击或再次选择切换选项将删除断点。
启动应用进行调试
正如您可以通过单击运行选项或选择已设置的运行配置来运行应用一样,您也可以启动应用进行调试。这实际上做了同样的事情,调用 Gradle 来构建您的应用,将应用部署到指定的设备或 AVD,等等。一个不同之处是 Android Studio 会帮助你调试。
要在调试模式下启动应用,只需从运行菜单中选择调试“运行配置 1”(或类似选项)。除了构建和部署步骤,您还会看到 Android Studio 中的下方视图会自动打开并显示调试窗口,可以访问调试器、控制台和其他调试工具,如图 5-10 所示。
图 5-10
Android Studio 中自动触发的调试视图
根据您正在调试的内容,如果您已经设置了断点,您还会看到运行菜单中的附加菜单项被激活,如图 5-11 所示。
图 5-11
调试选项,可以单步执行、遍历和跳出 Android Studio 中的部分代码
我将在下一个标题下解释这些“步进”选项。
调试时逐句通过代码
你在图 5-11 中看到的“单步执行”、“单步进入”、“单步退出”等选项对于仔细检查你的代码正在做什么是不可或缺的。通过一次单步执行、跳过或跳出一行代码,而不是运行整个代码库,您可以一次单步执行应用逻辑中的一个操作,并且与其他工具(如变量检查和查看活动 ui 中的视觉或行为变化)相结合,您可以看到每行逻辑的结果。
附加调试器
您可能并不总是清楚在哪里设置断点或者单步执行或遍历代码。有时,您可能想更深入地了解应用中发生的事情,以理解一些无法解释的行为或问题。这就是调试器工具的用武之地。我们无法在短短几页,甚至一整章的时间里对调试器进行公正的评价,所以我们不会强调这一点。你可以在本书的网站 www.beginningandroid.org
找到更多关于调试器强大功能的信息。
要调用调试器,并让它将自己附加到正在运行的应用上,以帮助您了解正在发生的事情,您可以使用运行菜单中的最后一个选项——将调试器附加到 Android 进程。您的应用必须已经在运行,这样才能工作。
查看您的跑步记录
现在,您已经看到了足够多的在 AVD 或真实设备上运行的第一个应用的例子,您可能想知道我所说的“查看您运行的内容”是什么意思要解开这个谜团,除了查看完成的应用中呈现的活动之外,还有更多“看到”您的应用运行的内容。
Android Studio 提供了几个非常有用的工具,有些人会说是关键的工具,让您可以查看应用由 Gradle 脚本(或其他工具)构建时发生了什么,以及应用运行时发生了什么的诊断和日志信息。
在 Android Studio 窗口的底部,状态栏的正上方,你会看到一组按钮,可以让你快速访问这些工具,它们的名称类似于TODO, Build, Logcat,
等等,如图 5-12 所示。
图 5-12
在 Android Studio 中轻松访问事件日志、构建输出、Logcat 等
让我们看看这里的一些关键工具,这样您就可以放心地将它们应用到您不断扩展的开发人员工具集中。
了解您的体型
构建工具在构建输出窗口中向您显示构建过程的摘要,这可能是一条“构建:已完成”消息,带有一个绿色勾号和一些计时信息,如图 5-13 所示,也可能是一组阻止构建工作的错误或问题。
图 5-13
Android Studio 中项目构建的构建输出视图
您还将看到实际执行应用构建的任务列表(在我的例子中,有 20 个单独的构建步骤)和一个到 Build Analyzer 的链接,它提供了对每个任务花费的时间以及如何改进的更深入的探究。
了解事件日志中的事件
补充构建工具的另一个工具是事件日志。事件日志显示所发生的操作的高级视图,例如将新构建的应用加载到设备或 AVD 上所采取的步骤,以及这些步骤中发现的任何问题。清单 5-2 显示了构建 MyFirstApp 应用并在多个 AVD 实例上启动它的事件日志输出。
22:17 Executing tasks: [:app:assembleDebug] in project /Users/alleng/AndroidStudioProjects/MyFirstApp
22:17 Gradle build finished in 9 s 5 ms
22:17 Install successfully finished in 11 s 455 ms.
22:17 Install successfully finished in 2 s 612 ms.
22:18 Emulator: emulator: INFO: QtLogger.cpp:68: Warning: Error receiving trust for a CA certificate ((null):0, (null))
22:18 Emulator: Process finished with exit code 0
Listing 5-2The Event Log from building and launching MyFirstApp on two AVDs
您可能在事件日志中看到的典型错误可能会停止构建或后期构建活动,例如为计算机上不存在的模拟设备指定 SDK 或基本映像。事件日志中后一个问题的示例如下:
Emulator: emulator: ERROR: This AVD's configuration is missing a kernel file! Please ensure the file "kernel-ranchu" is in the same location as your system image.
了解 Logcat
Logcat 是测试和调试工具库中最有用的工具之一。它的工作是在您的应用运行时,以及当您的应用意外停止运行、崩溃、出现问题、冻结等时,帮助从设备或 AVD 收集诊断和运行时信息。
Logcat 将提供一个类似控制台的界面来检查和查看来自设备或仿真器的关键系统消息,包括所有重要的堆栈跟踪,如当应用在运行时遇到错误或异常时,发生了什么和发生了什么错误。这种能力在以下情况下非常有用:代码可能在一个环境中运行得非常好,比如在开发人员工作站上运行 AVD,但在特定品牌或型号的手机上运行时会遇到意想不到的问题。
要使用 Logcat,只需在应用运行时,或者在设备或 AVD 执行其他任务(如启动)时,单击工具栏上的按钮。默认情况下,您会看到一长串 Logcat 条目,这些条目是从您的应用和设备中收集的,如图 5-14 所示。
图 5-14
Android Studio 中的 Logcat 输出
这个示例 Logcat 输出是相当无害的,但是当出现问题时,Logcat 中的堆栈跟踪和其他诊断信息是非常宝贵的。
重新审视 SDK 管理器
Android Studio 提供的主要工具的最后一个方面是重温你在第三章中第一次看到的 SDK 管理器。通过打开“工具”菜单并选择 SDK 管理器条目,您可以随时调用和使用 SDK 管理器。您可以这样做来查看您已经安装的 SDK 版本以及任何未安装或有更新的版本,如图 5-15 所示。
图 5-15
重新审视 Android SDK 管理器
正如您所料,您可以检查您没有的任何 SDK 版本,然后单击 Apply,SDK Manager 将开始在后台下载和安装这些版本。当你在两种情况下工作时,你很可能需要这样做:第一,当你确保你有代表普通或流行的 Android 版本的 SDK 时,例如 4.4“kit kat”,第二,当你测试新的功能时,因为谷歌结合新版本的 Android 发布了新的 SDK。
但是使用 SDK 管理器还有另一个原因,那就是它能够帮助你为 Android Studio 体验添加额外的好东西。切换到 SDK 管理器中的第二个标签,标题为 SDK 工具,您将看到一个额外的宝库,很快就会成为您最喜爱的工具。在本书的其余部分,我们将在相关章节中重新讨论这些工具,但是我将标记开发人员经常使用的两种工具。这些是 Android SDK 命令行工具,提供了从命令行管理和控制 SDK 包的额外功能,以及各种附加的 Google Play 库和 SDK,帮助您构建使用谷歌专有的 Google Play 服务的依赖项和库的应用。
您可以现在就下载这些内容,或者等到本书的后面部分再下载。
突出 Android Studio 的其他主要功能
本章介绍了 Android Studio 的一些关键特性,这些特性可用于在构建应用时管理、执行、检查和诊断应用,以及管理和使用相关的 SDK 和构成应用开发工作一部分的其他工具。
但是到目前为止,Android Studio 中您将使用的最大的工具集是那些与编写代码、审查代码、详细检查代码、重写代码等直接相关的工具。我们将在本书的第二部分介绍 Android Studio 的许多方面,同时介绍 Android 应用编码的许多部分。
摘要
本章已经向你介绍了任何软件开发人员的主要工具,IDE。现在,您应该可以放心地探索 Android Studio 中的更多选项,因为您知道您总能找到项目所需的核心工具。
六、掌控您的整个开发者生态系统
为 Android 开发做准备不仅仅是安装 Android Studio。虽然你可以做到这一点,也只能做到这一点,但你最终会发现,你的电脑及其运行的软件的其他方面,将对你作为一名 Android 开发人员的生产力、抱负和能力产生巨大影响。这一章并不详尽,但确实给了你许多重要的基本考虑,以及到哪里去进一步了解的链接。继续读!
选择专用开发人员硬件
在第二章中,我介绍了一份简短的清单,列出了严肃的 Android 开发者可能会感兴趣的电脑方面。这里再总结一下:
-
我将在 Android 开发中使用什么台式机或笔记本电脑硬件?
-
我将在台式机/笔记本电脑上运行什么操作系统?
-
在使用 Android Studio 之前,我的系统需要什么先决条件?
-
在开发过程中,我将使用哪些 Android 手机(如果有的话)?
我当时提出的格言仍然适用。几乎所有你手边最近几年生产的电脑都可以作为不错的开发人员工作站。无论是台式电脑还是笔记本电脑,它都能完成工作。
但是这本书不仅仅是关于完成工作。如果你真的渴望成为一名专业的 Android 开发人员,那么就像其他专业人员依赖工具和设备来提高效率和杠杆作用一样,你也应该考虑一下什么工具会促进你的开发工作和你制作优秀应用的能力。
让我们首先看看如果你正在寻找在构建 Android 应用时会给你带来不公平优势的开发者硬件,你可能要考虑的因素。
确定哪种 CPU 最适合您
2020 年,x86 _ 64 CPUs 有很多选择:AMD 的锐龙和 Threadripper 以及英特尔的 Core 系列 CPU。您可以找到许多关于哪个最好的意见,通常这些判断会考虑原始 CPU 速度(时钟频率)、封装中的内核数量、不同级别的板载高速缓存以及其他几个特性。
对于专门的 Android 开发机器来说,任何当代的英特尔或 AMD CPU 都足够了。如果你正在寻找 2020 年时代 CPU 的具体建议,你应该看看英特尔酷睿 i5 和酷睿 i7 CPUs 以及 AMD 锐龙 7 和锐龙 9 CPUs。这些处理器具有足够的速度、可观的内核数量(通常为四个或更多),以及良好的 L1/L2/三级高速缓存。
AMD 和英特尔都有更强大的 CPU,但通常你会看到这些 CPU 有更多的内核,偶尔还支持其他东西。在 Android 开发领域,内核越多并不总是越好。为您并行运行的关键活动是您在 Android Studio 中工作并在一个或多个 avd 中查看结果的任何同步活动,以及对于足够大的多项目应用,并行构建活动(如果 Gradle 被配置为支持它们)。我的建议是,省下最贵的酷睿 i9 和类似的 AMD 处理器的钱,把这些钱用在开发者设置的其他部分,比如更多的内存或更好的屏幕。
另一个越来越受欢迎的 CPU 架构是 Arm。Arm 是当今世界上几乎所有移动设备的核心,其基础是获得 Arm 架构许可的“片上系统”(SOC)设计的广泛流行。基于 Arm 的台式机已经出现,甚至 Arm 服务器市场也刚刚兴起,但这里有一个有趣的难题。
虽然今天全球销售和使用的绝大多数 Android 设备都是由 Arm 处理器构建的,但使用基于 Arm 的计算机来处理计算繁重的工作负载(如构建/编译 Android 应用)仍然是一个挑战。可以做到;毫无疑问。但是速度和性能的权衡仍然很重要。最大的障碍是你期望使用的工具是否适用于基于 Arm 的计算机。最值得注意的是,Android Studio 不适用于 Arm——它只能在 x86 架构的计算机上运行。同样,谷歌只为 x86 封装了 Android SDK 的许多版本,而不是 Arm。这意味着如果你选择一个基于 Arm 的开发机器,你将需要用你自己的构建工具引导你的所有工作,解决没有容易捆绑的 Android SDKs 的问题,等等。我不会推荐这种挑战。
内存太多永远不够!
我有时希望给自己添加内存就像给台式电脑添加内存一样简单。现在,为了更好的开发者火力,你和我将不得不满足于购买 RAM。
如果你回想一下第二章,你会记得谷歌推荐 Android Studio 的内存最低为 4 GB,首选的基本内存为 8 GB。买只有 4 GB 内存的新电脑其实挺难的。所以,引用一部著名电影的话,8 就够了吧?
别这么快!你当然可以在 8 GB 内存的电脑上高效地开发 Android 应用。事实上,我正在编写这本书的很大一部分,并在 2015 年的 MacBook Air 上构建一些你看到的例子,MacBook Air 有 8 GB 的内存,所以这不仅仅是理论上的可能。然而,我可以告诉你,它有其明显的局限性和挫折。一些值得注意的例子是我试图运行 Android Studio 和 AVD,以及我在本章后面和整本书中讨论的用于图像和视频处理的开源工具。8 GB 很快就耗尽了,MacBook 尽职尽责地开始增加——并越来越多地使用——磁盘上的交换空间,以平衡实际内存和我对它提出的更高要求。
相比之下,我信赖的台式电脑。它甚至更老,是近十年前的“老式”Dell Precision 工作站-我不确定戴尔是否还会生产这些工作站。但它有 32 GB 的内存,使用 Android Studio、多个 avd 和其他几个工具时不会漏拍。
Note
你可以从我的设置中看出,我在推荐你购买硬件时并没有收取加盟费或回扣。如果你有时间机器,请跳回到 2010 年或 2015 年购买你的版本我的硬件!
如果你正在指定你自己的 Android 开发者系统,我给你的简洁建议是:没有人抱怨他们有太多的 RAM!如果可以,至少要 32gb;对于奢侈品来说,64 GB 对于未来几年的 Android 开发来说已经足够了。
对于那些希望购买或获得笔记本电脑作为主要开发机器的人来说,请特别注意您的首选型号可以占用的最大内存是多少,以及在购买后内存是否可以更改。也就是说,内存是否焊接或固定在适当的位置,如果不采取昂贵的、可能导致保修失效的措施,就无法更换或交换?你可能会发现自己被限制在 16 GB 以内——如果你能选择一个支持 32 GB 的型号,你将来会感谢你自己(也许还会感谢我!).
全部储存起来!
随着您开始构建越来越多的 Android 应用,您对磁盘空间的需求将会增加,以容纳各种项目、资源等等。很明显,有些地方会消耗磁盘空间,比如磁盘上用于存储源代码文件(非常小)以及图像和视频文件(可能非常非常大)的空间。
当你想到它们时,有一些 Android 开发的特定方面是显而易见的,例如不同版本的 Android SDKs,每个版本可能需要高达 1 GB。
在考虑您的存储需求时,avd 是一个隐藏因素。因为很容易产生一个新的 AVD 图像来测试屏幕大小或设备格式,所以很容易在你知道之前就有 20 个、30 个或更多的 AVD。如果你真的想测试各种尺寸和分辨率的屏幕,最起码要有几十个 avd。
有了几十个或几十个 avd,您可以轻松地吃掉 50 GB 或更多。avd 最大的存储问题之一是您选择允许多少板载存储或模拟 SD 卡存储。这些选项是通过分配您的磁盘存储来模拟的,这意味着如果您想要模拟最新的 128 GB 存储的一加 7 设备,那么您的 AVD 将消耗 128 GB 或更多的实际磁盘空间!当然,您不必为模拟 SD 卡和设备上存储使用如此大的分配,但通常您至少会有一个可用的数量,所有这些加起来。
当您开始 Android 应用开发之旅时,实际的存储空间应该是 50 GB。这将让你可以制作几代 Android SDK 和大量不同大小和配置的 avd,以及 Android Studio 和其他工具。为了适应未来,特别是对于各种用于测试目的的 avd,您应该以 100 GB 或更多为目标。
与存储大小同样重要的是存储类型及其相关速度。传统的旋转硬盘驱动器当然便宜,但代价是数据传输速度,或 IOPS(每秒输入/输出操作数)。固态硬盘(SSD)等现代存储选项可显著提升数据传输性能,从而加快构建和运行 avd、部署大型应用、依赖重要图像和视频的应用以及任何数据密集型计算或操作的运行速度。
如果可以选择,请始终选择固态存储选项,尤其是如果您计划进行任何数据密集型或计算密集型开发。对于那些正在寻找专用笔记本电脑以满足开发需求的人来说,鉴于近年来这个市场向固态硬盘的巨大转变,你会发现自己被宠坏了。
观看这一切!
如果你想成为一名专注于 Android 应用的开发者,你将会经常盯着屏幕。有鉴于此,你可能要考虑如何充分利用你将看到的屏幕,以及你是否想要一个以上的屏幕。
当谈到显示器或显示器时,许多开发者的快乐将基于深深的个人因素。你喜欢大屏幕吗?你喜欢多屏吗?不同方向的屏幕对你有帮助吗——横向还是纵向?你会把笔记本电脑的屏幕和固定屏幕混合在一起吗?除此之外,还有更多的考虑因素,但我会指出两个将对您产生长期有益影响的关键特征:大小和密度。
当谈到屏幕尺寸时,有一些实用性会让你相信至少一个大屏幕是值得花费的。回头看看第五章的图 5.6,你会看到两个 avd 并排运行,一个代表普通手机屏幕,另一个代表平板电脑屏幕。你不会惊讶地发现,图中的视图几乎占据了我的 MacBook Air 的所有 13 英寸屏幕。如果你仔细看,你可以看到隐藏在 AVD 屏幕后面的我的 Android Studio 会话的微小片段。但你只能看到这些。如果没有一系列非常尴尬的妥协,这种尺寸的屏幕不可能在 Android Studio 和 AVD 中并行开发。你可以在 Android Studio 中调整屏幕和窗口的大小,你可以平铺窗口,试图在运行单个 AVD 的情况下将所有东西都挤进去,如图 6-1 所示
图 6-1
在 13 英寸的笔记本电脑屏幕上同时运行 Android Studio 和 AVD
如果你认为这有助于实时调试或同时比较不同的布局,让我声明这不是那种东西。只是很大的痛苦!相比之下,在 24 英寸的屏幕上,Android Studio 和多个 avd 愉快地生活在一个拥有充足空间的屏幕上,如图 6-2 所示。
图 6-2
在 24 英寸屏幕上运行 Android Studio 和多个 avd
我会让这些数字说一千个字。当你看完之后,我关于屏幕尺寸的结束语就不需要更多的支持了。如果可以,买个大屏!
谈到屏幕质量,特别是像素密度,我们再次进入一些非常主观的领域。在 2020 年的市场上,当这本书正在编写时,你可以从多代“高密度”中选择,以及 4k,5k,甚至 8k!你的个人品味和偏好将是最重要的考虑因素,但我建议你记住以下两点:
-
查看运行中的屏幕。如果可以的话,去一家零售商那里,他们可以向你展示实际连接到任何一种电脑上的屏幕。这可以让您感受显示器的实际表现,并让您体验亮度、白平衡、对比度和刷新率等因素,以满足您对屏幕性能的要求。
-
瞄准一个分辨率密度和你计划开发的应用一样高或者更高的屏幕。我们将在本书的其余部分更多地讨论 Android 屏幕分辨率选项和机制。为了确保您的开发和测试尽可能向您展示您的作品、图像、视频和其他应用资源在真实设备上的外观,请确保您为您的开发系统选择的屏幕至少与您的目标设备一样好,如果不是更好的话。否则,你会对现实世界中运行的事物产生扭曲的想法。
连接这一切!
开发人员设置需要考虑的其他方面包括:
-
键盘和鼠标:看起来很普通,但是你会经常使用这些外设。不要满足于你不喜欢的人!
-
USB 端口和电缆:如果你计划用 Android 设备对你的应用进行广泛的测试,那么有足够的 USB 端口和电缆以及合适的适配器是值得的。大多数旧的 Android 设备使用 micro-USB 连接器,最近的型号切换到 USB-C 风格的连接器。
-
充电电缆、充电座和设备:同样,如果你打算使用很多真正的 Android 设备,保持它们的电源就有点麻烦了。虽然它们可以通过 USB 连接到您的开发人员机器进行充电,但历史 USB 协议版本的功耗是有限的。直到 USB-C,“标准”USB 才可以汲取超过 500 mA 的电流。在可能的情况下,留出空间,并考虑插入墙上插座的电量。
很容易忽略这些要点,但是它们有助于使您的开发人员体验更加顺畅和愉快,如果您忽略它们,您会发现它们会增加摩擦和挫折,破坏您的最佳努力。
测试手机和平板电脑
让我从测试手机这个话题开始,首先我要说明的是,并没有什么铁一般的规则要求你必须有一部 Android 手机来测试你的应用。许多应用都是在完全模拟的环境(如 AVDs 等)中测试的基础上构建和发布的。
然而,许多应用是基于一系列 Android 设备上的真实世界测试和反馈构建的,在真实世界的情况下,可以检测和解决这些古怪和差异。而且有区别!虽然 Android SDK 和 Android 的核心平台功能(几乎)是通用的,但手机供应商和电话公司有无数种方式来调整和改变 Android 的默认行为。通常,这些“电信公司”这样做是为了试图将自己与其他电话公司区分开来,或者从一个更愤世嫉俗的角度来看,他们试图将自己注入一个位置,希望他们的用户认为他们比实际上更重要。
不管是什么原因,你会发现自己在 Android 开发者之旅的某个时候会问自己是否需要花钱购买不同的 Android 手机来测试你的应用。我不能给你一个明确的答案,但我可以给你一些建议,帮助你决定花多少宝贵的时间和金钱来购买测试手机。
选项 1:全虚拟测试
当你开始你的开发之旅时,坚持使用 Android 虚拟设备是一个非常好的策略。老实说,它在许多成功的开发人员的整个职业生涯中为他们服务。随着 Android Studio、Android SDKs 以及来自英特尔等其他供应商的支持工具的每次发布,走这条路而不在专用测试硬件上花费任何额外的时间或金钱越来越可行。
缺点是,有时您将难以复制真实世界的体验,例如欣赏应用的真实性能,并且您还可能成为我在前面提到的电信公司引入的“特殊功能”的受害者。
选项 2:开始虚拟化,并使用顶级设备进行扩充
广泛使用 avd 并有选择地增加一些关键硬件选择的混合方法恰好是我的偏好。这有几个原因:
-
使用 AVDs 越来越有效,如前面选项 1 所述。
-
市场的很大一部分由一家厂商代表:三星。购买一台三星设备(或几台设备)会让你接触到世界各地使用的大多数真正的 Android 设备。
-
谷歌自己已经推出了几代连续品牌的安卓设备,先是 Nexus 系列,最近是 Pixel 系列。拥有这些设备中的一个,您就可以看到您的应用在没有电信“增强”的设备上的外观和行为
如果你采用这种方法,购买少量的手机可以让你测试大量潜在用户的真实体验。
选项 3:买,买,再买一些
尝试测试所有边缘情况、怪癖、奇怪行为和 bug 的选项吸引了一些开发人员。在 Android 领域做到这一点需要很大的耐心,更重要的是要有雄厚的财力!您可以轻松地收集几十个、几十个甚至几百个设备,但仍然无法涵盖您的用户在现实世界中可能遇到的所有奇怪的设备及其奇怪的问题。此时,你的银行余额可能也会出现一些奇怪的问题!
我不推荐这种方法,但是我承认我没有将 Android 开发和应用推进到会暴露很多这些问题的高要求领域。我试图积累一个设备测试库,如图 6-3 所示。
图 6-3
我收集的安卓设备,跨越了十多年的安卓历史
我在 20 多台设备前停下来,我的银行存款感谢了我!
选项 4:其他人的硬件
关于云计算,我最喜欢的一个笑话是,它只是在别人的硬件上运行你的代码。如果你明白我的日常工作,那就更有趣了!然而,幽默的背后是一个有用的见解,即在真实设备上测试您的 Android 应用最好是在其他人负责拥有和提供这些设备时进行。
在写这本书的时候,有一系列的商业选择可以让你在一系列真实的设备上测试你的应用,你不必自己去买。您可以租用或支付访问即服务,并获得在各种手机上进行测试的所有好处,无需购买手机的前期成本,也无需维护手机的后续成本。
这超出了本书的范围,无法一一列举这些服务,但是这里有一些你现在可以从商业供应商那里获得的主要选项,你当然可以从那里搜索到与之竞争的选项:
-
AWS 设备农场:AWS 设备农场来自所有云的供应商——亚马逊。Amazon 提供对来自众多制造商的大约 400 种不同设备的访问,并提供有用的工具来管理您浏览器中的测试工作负载。更多详情请点击
https://aws.amazon.com/device-farm/
。 -
谷歌 Firebase 测试实验室:谷歌版本的许多 Android 设备都可以远程访问(它们也可以访问各种苹果 iPhones 和 iPads)。Google 还提供了一些很棒的附加工具,用于 UI 测试和自动化他们设备上的测试运行。更多详情请点击
https://firebase.google.com/docs/test-lab/
。 -
三星远程测试实验室:作为 800 磅重的 Android 设备的大猩猩,三星是测试你的应用的显而易见的选择。三星的远程测试实验室拥有他们测试实验室中几乎所有可用的设备,加上方便的工具,应该在您的测试即服务候选名单上。更多详情请点击
https://developer.samsung.com/remote-test-lab
。
还有其他可用的商业服务,以及从世界各地的 Android 开发者社区共享或借用设备的选项。将上述内容视为一个起点,而不是一个明确的指南。
使用构建工具构建
如前几章所述,Gradle 是 Android Studio 使用的构建工具,用于将 Android 应用的所有部分整合在一起并创建成品。Gradle 也有丰富的选择,可以(事实上也确实)跨越整本书。我鼓励你开始探索 Gradle 选项来扩展你构建 Android 应用的能力。这里有三个快速提示,让你开始并帮助你找到更多。
更新 gradle.properties 文件
不要害怕打开项目的 gradle.properties 文件,看看它显示的设置。默认文件中有很多解释性的注释,您应该可以放心尝试。例如,您将看到一个为 Java VM 设置内存的条目,如下所示:
org.gradle.jvmargs=-Xmx2048m
尝试将该值更改为Xmx1024m
,看看这会如何影响构建时间——您实际上将 JVM 的可用内存减半。如果你有足够的内存,试试Xmx4096m
给 JVM 两倍的默认内存,看看这样是否能加快速度。随着您对 Gradle config 了解的越来越多,请重新访问您的属性文件以应用您所学到的内容。
使用 Gradle 命令行
Gradle 的许多功能都巧妙地集成到了 Android Studio 中,并在您执行各种操作时触发。但是你的项目也会有一个 Gradle 的命令行工具,名为 gradlew。它位于项目的根目录中。如果您在那里打开一个 shell 或命令提示符并运行命令行可执行文件,如下所示
./gradlew
您将看到一个有用的列表,其中列出了 Gradle 提供的非常有用的命令行选项和工具。你也可以跑步
./gradlew --help
获取所有命令行选项和工具的详尽列表。首先要研究的一个非常有用的选项是概要分析选项,它查看构建应用时需要花费的时间。调用它并查看清单 6-1 中的结果。
Welcome to Gradle 6.1.1.
To run a build, run gradlew <task> ...
To see a list of available tasks, run gradlew tasks
To see a list of command-line options, run gradlew --help
To see more detail about a task, run gradlew help --task <task>
For troubleshooting, visit https://help.gradle.org
BUILD SUCCESSFUL in 24s
1 actionable task: 1 executed
See the profiling report at: file:///Users/alleng/AndroidStudioProjects/MyFirstApp/build/reports/profile/profile-2020-09-01-10-08-13.html
A fine-grained performance profile is available: use the --scan option.
Listing 6-1Invoking Gradle profiling from the command line
Gradle 生成了一个 HTML 格式的报告,并将其放在输出中显示的文件中。在您选择的浏览器中打开该文件,您将看到 Gradle 对项目构建时间的见解,如图 6-4 所示。
图 6-4
Gradle 分析报告
访问 gradle.org 了解更多信息
网上有大量关于 Gradle 的信息,该项目的主页是一个很好的起点。一旦你尝到了它的厉害,你会发现它成了你的首选工具之一。
用源代码管理管理您的代码
许多严肃的开发人员面临的另一个大问题是如何管理他们的源代码、项目和相关资源。源代码控制是使用专用数据库的概念,该数据库擅长管理软件源代码的基于文件的性质,有时也被称为版本控制,并同时解决两个大问题。首先,随着你开发工作的增长,你可能会有几十甚至几百个项目,每个项目都有大量的文件和相关资源,正如你在第四章中看到的。源代码控制系统使得管理这些文件集变得简单得多,并且为您的编码工作提供了可靠的备份和真实的来源。源代码控制的第二个好处是它给你提供了试验、修改和犯错误的自由,如果需要的话,可以恢复到早期的版本(通常是一个可以工作的版本)。
Android Studio 提供了对三种不同系统的直接集成支持:Git、Mercurial 和 Subversion。对于开发人员社区的某些部分来说,每一种都很受欢迎,如果您有偏好,就坚持使用它。要启用与项目的源代码控制集成,请在 Android Studio 中打开 VCS 菜单,并选择“启用版本控制集成”选项。它将显示如图 6-5 所示的对话框,供您选择您选择的版本控制选项。
图 6-5
启用源代码管理
启用源代码管理后,您将会在 VCS 菜单中看到更多可用的选项,在任何项目视图的上下文菜单中也可以看到这些选项。第一次尝试使用一个特性时,比如导入一个 Git 分支或者从 Subversion 中签出一个项目,系统会提示您输入访问所选系统所需的 URL 和凭证。从那里,您将能够使用您在图 6-6 的菜单中看到的一系列功能。
图 6-6
VCS 菜单中启用的源代码管理功能
源代码控制是一个很大的话题,正如您已经猜到的,有大量关于这些工具的专门书籍和资源。如果你是源代码控制工具的新手,我可以推荐你访问 https://git-scm.com/
、 www.mercurial-scm.org/
或 https://subversion.apache.org/
来开始使用 Android Studio 原生支持的任何系统。
完善您的软件库
除了我在本章中已经提到的 Android Studio 的工具和补充系统之外,还有一些你可能需要或想要的额外工具,以便为你的开发构建各种额外的资源。这可以包括图像、动画、录音、音乐等等。
有很多商业工具服务于这些目的,您可能已经知道并使用了其中的一些。我将重点介绍一些关键的开源工具,它们在各自领域都是强劲的竞争对手,在某些情况下还是领导者。我是开源的强烈拥护者,这就是为什么我倾向于在可能的情况下使用开源工具。您可能不同意这种观点,但是下面的列表将概述可用的内容,即使您选择商业替代方案。
您应该为您的开发环境考虑的开源工具包括
搅拌机:对于所有 2D 和 3D 动画的顶级工具,你只需要看看搅拌机。它在专业和业余圈子里有大量的追随者,被用于电影和电视等行业,并且对于你可能想到的几乎任何动画开发都有惊人的能力。点击 www.blender.org
了解更多信息。
GIMP (GNU 图像处理程序):一个众所周知的图像编辑和处理工具,GIMP 通常会根据你问的人找到等量的爱与恨。它非常适合许多常见任务,如调整大小、缩放、裁剪、红眼消除、图层开发/展平等。那些不喜欢它的人通常会指出 Adobe Photoshop 是黄金标准的商业产品。这些人没有错——Photoshop 是真正全面和有能力的。但是对于一个开源的替代方案,让 GIMP 试试( www.gimp.org
)。
Inkscape:矢量图形可能会成为你的 Android 开发世界中非常大的一部分,特别是如果你冒险进入游戏开发的话。Inkscape 是一个奇妙的开源矢量图形程序,历史悠久。还是有人说各种商业包装更好,他们可能是对的。点击 https://inkscape.org
了解更多关于 Inkscape 的信息。
Audacity:另一个被认为是该领域最好的应用之一,Audacity 是首屈一指的多声道音频编辑套件之一,支持所有流行的操作系统。在 www.audacityteam.org/
寻找无畏。
LAME:原始音频工具之一(也是嵌入在本章提到的许多其他工具中的工具),LAME 是一个命令行工具和可嵌入库,它对来自各种源( https://lame.sourceforge.io
)的音频执行 MP3 编码。
OpenShot:许多开源视频(和音频)编辑套件之一,OpenShot 有一些很棒的功能,包括基于曲线的关键帧动画,高级时间轴和逐帧功能,以及跨平台支持。更多详情请点击 www.openshot.org
。
Olive:另一个视频编辑工具,拥有狂热的追随者( www.olivevideoeditor.org/
)。
DaVinci Resolve:另一个非常强大的视频编辑工具,目前号称是支持高达 8k 的非常高分辨率视频的少数工具之一。它还对在同一视频项目上工作的多个用户之间的协作提供了强大的支持。点击 www.blackmagicdesign.com/
了解更多信息。
在本书的第三部分中,当我们在 Android 应用中处理图像、声音、视频和其他资源时,您将会看到这些工具中的一些正在发挥作用。
摘要
正如你从本章所涵盖的主题中所看到的,仔细考虑你的硬件和软件环境可以极大地帮助你成为一名专注的 Android 开发者。还有一个应该涉及的软件主题——Java 和 JDK。这是一个有数百本书撰写的主题,虽然我们没有那么多的篇幅,我们将从第七章的下一页开始,用整整一章来讲述 Java!
七、面向 Android 开发的 Java 简介
在这一章中,我们将探索如何开始使用 Java 开发 Android 应用,特别关注如何处理像 Java 这样庞大的主题,而不被其规模所吓倒。那些已经对 Java 开发有所了解的人可以浏览大部分内容,但是有一些关于 Android 细节的要点,我会推荐给任何读者阅读。
将 Java 学习到专家水平是一项巨大的努力。即使学习到“快乐初学者”的水平,也要比这本书的一个章节,甚至是一整本书本身要多得多。几乎“成千上万”的书籍都是关于 Java 及其许多方面的。我们不会提供一个不完整且最终无用的介绍,而是将重点放在三个关键的准备点上,用本书之外的工具和资源引导您踏上学习 Java 的道路。
首先,我们将从软件角度回顾 Java,包括 Java 开发工具包(JDK)和 Java 运行时环境(JRE),以及 Android 使用和采用 Java 虚拟机环境(JVM)的特点,包括其支持随处运行软件的承诺和 Java 版本的悠久历史。
其次,我们将深入一个基本的纯 Java 应用,您可以在任何装有 Java 的机器上运行该应用,从基础的一两个部分开始,扩展您对 Java 的理解,以及它如何(和不)转化为 Android 应用。
最后,我们将介绍 Java 中的关键领域或主题,您应该随时学习,并链接到一些最好的免费和商业资源,以便在本章之后继续您的旅程。
Other Development Language Choices For Android
Java 有着悠久的历史,当 Android 推出时,Android 是(平等的)首选开发语言。但它不是开发应用所支持的唯一语言。与 Java 一样,Android 从早期就通过“原生开发工具包”或 NDK 支持 C++ 的“原生”开发形式。2016 年,谷歌冒险将 Kotlin 加入受支持语言的行列,并于去年决定 Kotlin 将成为新的首选语言。就应用使用而言,Java 仍然是 Android 开发中的主导语言,接近 90%。但是许多新的应用混合了 Kotlin 和 Java,或者只选择 Kotlin。你可以在 Apress 'Learn Kotlin for Android Development(spth,ISBN 9781484244661)中了解更多关于 kot Lin for Android 的开发。
Java,Java,无处不在
这个吸引人的标题我有两种意思——Java 本来是一种只写一次,可以在任何地方运行的语言。令人惊讶的是,尽管有很多疑问、阻力甚至法律障碍,它还是成功地实现了这个目标。Java 也无处不在,因为它是编程语言的名称,但也常常是用来实现随处运行承诺的软件的简称。Java 虚拟机(或 JVM ),以及它在计算机上部署的打包方式,通常也被称为 Java。如果这还不算是混淆的话,一旦你沉浸在 Java 中,你会发现你自己和其他从业者也把提供基本功能的核心库称为 Java。
作为一名 Java 开发人员,命名的复杂性是一个需要适应的问题,但是当谈到“随处运行”的能力实际上是如何交付的时候,Java 也有过一段艰难的历史。Java 发行版对 JVM 进行了补充,作为运行时,称为 Java 运行时环境(JRE ),或者作为完整的开发工具,称为 Java 开发工具包,或 JDK。版本如何被处理一直是软件中最扭曲的故事之一,因此发现系统试图同时支持五个并行的主要 Java 版本并不罕见——Java 6、Java 8、Java 9、Java 11 以及最近发布的任何 Java 版本。
多亏了 Google 的行动,Android Studio 在其 2.3 版本中冒险隐式捆绑了一个 JDK,并为您完全管理它,所以您再也不需要自己开发了。然而,在 Android 之外测试代码,以及在非 Android 应用中学习更多关于 Java 的知识,有时会很有用。在这一章的后面,如果你还不熟悉这种方法,我们将演示如何去做,如果你是这一领域的绝对初学者,也可以把它作为学习 Java 语言的跳板。
Java Security
如果不讨论安全性,这个主题就不完整。无论是作为独立安装还是作为 Android Studio 的一部分,Java 及其运行时和 JVM 都有一段非常长、非常麻烦的安全问题历史。我不会深究已经发现并修复的数千个漏洞、错误和利用方式!–在过去的二十年里,但我要说的是。总是修补你的 Java 安装,包括总是保持 Android Studio 最新。永远!
要了解更多影响 Android 的 Java 历史漏洞,请点击 www.beginningandroid.org
阅读更多内容。
安卓的 Java 时间扭曲
当第一次发布时,谷歌选择了当时最新的 Java 版本作为 Android 的基础,即 Java 版本 6,即当时所知的 Java 1.6。随着 Android 未来版本的发布,Java 版本的进展相对缓慢,保持 Android 与 Java 6 兼容有很大的好处。随着时间的推移,发布模式发生了变化,特别是 Java,在 Java 7 和 Java 8 的缓慢发布之后,速度加快了。在撰写本书时,我们正处于 Java 15 阶段,但由于各种技术和非技术原因,Java 兼容性在 Android 与 Java 7 和对 Java 8 的部分支持中“停滞不前”。如果你学习 Java 是为了编写 Android 应用,请记住这一点!
在为 Android 开发时,Java 9 和更高版本的概念和特性根本不可用。这意味着诸如 Java 9 中引入的模块、Java 10 中引入的数据类共享,或者 Java 12 中 switch 语句增强的安全性和实用性都不是您所能支配的。在实践中,这并不会造成很大的障碍,但是如果您对现代 Java 很有经验,并且已经习惯了它所提供的最新特性,那么可能需要一些时间来适应。
使用 JDK 安装程序学习 Java
由于 Android Studio 将 JDK 与它的安装捆绑在一起,除了出色的 Android Studio IDE 体验之外,您还可以使用现成的“纯 Java”环境。要单独使用 JDK,您可以在磁盘上查找 Android Studio 放置 JDK 的目录。你可以直接从 Android Studio 本身找到这个,打开任何项目。选择File
➤ Other Settings
➤ Default Project Structure
菜单。你会看到如图 7-1 所示的对话框,显示 JDK 的位置。
图 7-1
Android Studio 显示 JDK 的位置
总结 Android Studio 支持的各种操作系统
-
在 macOS/OSX 上:JDK 在
/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home
被发现。 -
在 Linux 上:根据您指示 Android Studio 安装程序放置 IDE 的位置,JDK 的位置会有所不同,但是它位于该位置下的
./android-studio/jre
文件夹中。 -
在 Windows 上:JDK 的位置会根据您指示 Android Studio 安装程序放置 IDE 的位置而变化,但是默认情况下它在
C:\Android\Android Studio\jre\bin
中。
对于本章中的纯(非 Android) Java 示例,您可以使用命令提示符或 shell 通过"cd"
命令导航到该目录。您还可以测试这个目录是否包含在您计算机上的PATH
环境变量中——这意味着无论您从哪个目录开始工作,您的操作系统都知道在哪里可以找到 JDK 的关键工具。例如,在 macOS 和 Linux 上的"which javac"
命令将返回第一次安装的javac
二进制文件的路径,这是 Java 编译器,如果在PATH
中指定的目录中找到的话。例如,在我的 MacBook Air 上,我看到以下内容:
$ which javac
/usr/bin/javac
还有什么比这更简单的呢?您可能还记得,在前面关于 Java 无处不在的标题中,在同一台机器上安装多个版本的 Java 是很常见的,这些版本可能是完整的 JDK,为 Java 提供开发工具,或者只是 JRE,它提供运行时 JVM 和一些支持实用程序,但不提供像编译器这样的开发工具。
如果您想绝对确定您使用的是 Android Studio 附带的编译器(javac
二进制)和 JVM ( java
二进制),那么在调用这些程序时,请使用它们的完整路径。例如,为了确保我在下面的例子中使用 Android Studio 中的 javac,我将调用它
$ "/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/javac" somejavafile.java
有很多方法可以询问你默认使用的 Java 安装版本,并出于各种原因在它们之间切换——甚至在 Android Studio 内部。然而,这超出了本书的范围。对于初学者,我警告不要在这方面修改 Android Studio 的设置。
代码中的第一个 Java 概念
传统上,通过编写“Hello World”应用来开始学习编程语言。我们将跳过这一步,转到一个稍微高级一点的例子,向您展示 Java 代码的基本工作原理。清单 7-1 展示了我们的非 Android Java 示例应用 FirstJavaDemo 的代码,您可以在Ch07/FirstJavaDemo.zip
中找到它。
import java.io.*;
import java.util.*;
public class FirstJavaDemo {
public static void main(String[] args) {
Console console = System.console();
if (console == null) {
System.out.println("Console not found");
System.out.println("Please re-run from a command line, shell, or console window");
System.exit(0);
}
System.out.print("Tell me something about yourself: ");
String something = console.readLine();
System.out.println("Interesting! You said: " + something);
}
}
Listing 7-1Source code from the FirstJavaDemo.java file
在接下来的章节中,我们将逐步介绍这段代码的各个部分会产生什么样的效果。在稍微大一点的层面上,让我们遍历整个结构,并用简单的英语解释正在发生的事情。这将允许您交叉引用一个外行人的解释,以及特定于 Java 的方面。
第一行是 import 语句,指示我们的程序使用 Java 的库系统(其中捆绑了其他 Java 功能供您使用,而不必自己从头开始构建)。语法看起来有点奇怪,但其核心是我们在指导 Java 引入java.io
库和java.util
库的所有方面。这些库处理诸如文件和控制台输入和输出以及您经常想要或需要的其他无处不在的特性。
然后我们有一个class
声明。类是应用开发的面向对象(OO)设计学派的核心概念,我们将在本章后面更多地讨论它们。重要的是,这是我(或你)选择的一个类定义。我选择了FirstJavaDemo
这个名字。Java 关键字"public"
意味着,如果需要,其他 Java 程序可以导入我的类并重用它。
类的主体和逻辑的其他子块都用花括号括起来——就是你看到的{
和}
符号。这些花括号是重要的标点符号(在编程和编译器的说法中是记号),必须用于分组嵌套的逻辑集——无论是类、它们的方法,还是内部逻辑块(如循环和条件测试)等等。
接下来,我们有强制的main()
方法,我们将其定义为具有一个void
返回类型,这实质上意味着在退出时,程序不会从变量或其他方法调用中返回任何数据,并将一个名为args
的String
数组作为(可选)参数。在实践中,这种方法是许多编程语言的长期风格,其中参数(或自变量)可以在程序首次运行时传递给程序,用于开发人员想到的任何目的,例如打开或关闭程序中的选项,或者用关键的启动数据作为种子。
然后,我们基于由System.console()
方法提供的Console
对象,定义并创建一个名为console
(小写)的对象,由于导入了java.util
库,该方法本身就是我们可以访问的实用程序之一。这种方法隐藏了FirstJavaDemo
程序和 JVM 之间的大量操作系统交互,将你的程序连接到一个控制台——这通常是一个 shell 或命令提示符窗口,或者是更复杂程序的类似风格的一部分,如 Android Studio IDE 中的控制台窗口。
Key Concepts: Data Types and Variables
这个名为“Console”的控制台对象和工作副本或容器的例子介绍了几乎所有编程语言(包括 Java)的两个基本方面。这些概念是变量和数据类型。
变量可以被认为是一个容器或一个尚未确定值的占位符——就像高中数学或代数一样。在 Java 中,变量首先被定义为标识它将包含的信息种类,即它的数据类型。
Java 中的数据类型是用于变量或其他环境的信息或数据种类的规范。数据类型有两种形式:第一种是非常简单的数据类型,称为基元,它以几种形式表示简单的整数或浮点数——int、short、long、float 和 double——以及简单的字符文本,称为 char 和 boolean,它表示 true 或 false 值。
第二种数据类型是更复杂的对象,比如在我们的控制台案例中。您可以将这种更复杂的数据类型视为多个原始数据类型和可用于处理该数据的预定义逻辑的复合集合。这是 Java 和其他面向对象编程语言中类的本质。
有时创建控制台会因为奇怪的原因而失败——权限问题就是一个例子。我们使用 Java 的 If-Then 逻辑来引入一个逻辑测试,用符号==
来表示,以确定我们的console
是否存在或者是未定义的——用 Java 的说法就是null
。如果console
没有定义,我们使用我们可以使用的System.out.println()
方法,感谢java.io
库的引入,写出一些对阅读它的人有意义的有用的字符串文本。然后我们使用System.exit()
方法退出程序。
Key Concept: Branching and Looping Structures In Java – If, While, For
清单 7-1 中的代码展示了 Java 中主要的逻辑控制结构之一——If 语句。还有其他几个这样的结构,允许您在代码中基于测试值做出决策,并基于测试值决定合适的逻辑分支或重复操作,直到条件发生变化。除了 If 语句,Java 还提供了 For 循环、While 循环和 Do While 循环。每一个后面的结构都有许多细微差别,我们将在本书的后面探索,但是你可以在 http://en.wikibooks.org/wiki/Java_Programming/
了解更多。
在更可靠、更常见的场景中,创建console
对象一切顺利,然后我们使用System.print()
方法在控制台中打印问题的文本,使用System.readln()
方法接受您输入的响应,并将其存储在一个名为something
的String
变量中,我们将该变量定义为保存您输入文本的地方。最后,我们再次使用System.println()
和由+
操作符提供的一些String
连接特性来回应您。
这个简短的描述应该有助于你理解FirstJavaDemo
应用是做什么的,但是除了非常粗略的层次之外,并不能真正给你对 Java 语法、结构或规则的深刻感受。我们稍后会进一步加深您的理解。现在,让我们练习编译和运行这个程序。
要编译该程序,打开一个 shell 或命令提示符,如果需要的话,将目录更改为 JDK,如前面针对您的操作系统所述。请注意您将来自Ch07/FirstJavaDemo.zip
的FirstJavaDemo.java
文件放置在磁盘上的什么位置。然后调用javac
二进制文件,给它传递FirstJavaDemo.java
的完整路径和文件名,例如,在 macOS 或 Linux 上如下所示:
$ javac <path-to-where-you-unzipped-code>/FirstJavaDemo.java
过一会儿,您应该会看到命令或 shell 提示符回到您的控制中。发生了什么?javac
实用程序已经处理了源代码,并生成了 JVM 现在可以理解和运行的代码的中间版本。在您的目录中,您应该会看到一个名为FirstJavaDemo.class
的新文件,它是javac
命令的输出。
要运行您的代码,使用java
命令(小写)并传递给它FirstJavaDemo.class
文件的完整路径,但是省略文件名的文件扩展名(.class
部分),例如:
$ java <my-path-to-the-file>/FirstJavaDemo
一旦您回答了屏幕上应该看到的问题提示,输出将如下所示:
$ java FirstJavaDemo
Tell me something about yourself: I program in Java
Interesting! You said you: I program in Java
你做到了!使用特定于 Java 的工具和一些新学到的代码,您已经创建了一个纯 Java 应用。请继续阅读,了解更多关于 Java 语言的知识,以及如何更深入地理解FirstDemoJava
应用中的每个单词、行和古怪的标点符号。
Android 开发的关键 Java 构件
本书剩余部分的所有例子都依赖于一定程度的 Java 知识。在本章的剩余部分,我们将为您提供一个主题主列表,它涵盖了您至少需要遵循的基础知识,但更重要的是,它还将为您提供一个起点,让您从网上、书店、学校和大学等大量免费和商业资源中进一步学习和自学 Java。我们将直接链接到其中的一些链接,这样您的 Java 之旅就不会被打断——我鼓励您在阅读本章时关注其中的一些链接,因为深入研究 Java 主题永远不会太早!
为了最大限度地提高您学习 Java for Android 开发的效率,以下主题是关键:
一般软件开发知识:
-
代码的结构和布局
-
面向对象的设计和编程,包括类和对象
-
类方法和数据成员
特定于 Java 的编码知识:
-
接口和实现
-
线程和并发
-
碎片帐集
-
异常处理
-
文件处理
-
无商标消费品
-
收集
显然,这不是 Java 的全部,但它是 Java 的一个子集,足以让您立即开始 Android 开发的构建块!
代码结构
回头看一下FirstJavaDemo
源代码,就能体会到 Java 代码在最简单的情况下是如何构造的。Java 可以被认为是最基本的构造的洋葱分层,一层一层地构建最深远和最复杂的构造。从最简单到最复杂,我们有
-
Token:Java 的最小构造块,token 代表我们已经创建的或系统提供的对象实例的名称,例如我们的
FirstJavaDemo
代码中的 somethingString
对象,Java 语言中的关键字,如“return,
”操作符,括号等标点符号,等等。 -
表达式:一个或多个标记以及类似于
+
的操作符,或者方法调用,它们构成了一个逻辑。在我们的第一个 JavaDemo 示例中,我们看到一个表达式“有趣!你说:“+ something,是用+运算符连接两个字符串的表达式。 -
语句:由符号和表达式构建的一套完整的逻辑,用分号分隔。这通常与一行代码同义,但实际上它可以跨越任意多行。
-
方法:语句的逻辑分组,形成一组连贯的逻辑,可以通过使用方法名来调用。在我们的
FirstJavaDemo
代码中,我们为我们的类的 main 方法创建了逻辑,但是我们也引用了其他类的方法,比如用于在控制台读取文本输入的readLine()
方法和用于打印回控制台的println()
方法。 -
类:一个概念对象的表示,以及用于表示该对象并允许操作该对象及其功能的所有方法和数据成员。类的概念与面向对象设计的概念紧密联系在一起,我们很快就会谈到这一点。
-
包:虽然在我们的例子中没有显示,包是类的集合,通常用于将那些支持特定相关逻辑组的类捆绑在一起,例如,文件处理或图形渲染。
Java 代码还有许多其他的结构方面,但是掌握前面的内容将使您能够根据对基础知识的理解来解释和利用更深奥的结构。
Android and Main
不,那不是十字路口,但可能是。在第一个 JavaDemo 示例中,您将看到我们定义了一个方法,称为 main。Java 的设计规定,Java 程序的几个强制性要求之一是存在一个名为 main 的方法,并且它将是运行程序时 JVM 调用的第一个方法。可以把它想象成 Java 中的一个大标志,上面写着“从这里开始”。
但是看看我们在前面的章节中已经写过的 Android 例子,比如 MyFirstApp 应用。MainActivity.java 文件缺少一个主方法。这是怎么回事?不要害怕。您的应用确实有一个 main 方法,但是它隐藏在 Android Studio 提供的支持框架中。Android 提供了一个活动生命周期,定义了开发人员可以关注的点,处理创建、使用、暂停和处理活动的点,而不是强迫您修改主要方法,然后修改所有可以为用户提供运行应用体验的流程或事件处理工作。该活动生命周期在第十一章中有详细介绍。
如果你看看你已经编写的 Android Java,包括MyFirstApp
应用,你可以在实践中看到这种 Java 语言层次的许多方面——在没有意识到的情况下,你已经在你的第一个应用中运用了这些概念。
理解面向对象的设计、类和对象
Java 编程语言从一开始就被设计成包含和表达面向对象设计的概念(有时缩写为“OO”)。在本质上,面向对象设计使用这样一种思想:用对象(或实体)来表达您在应用中创建的概念模型的几乎所有内容,例如“人”或“动物”,然后使用一些强大的设计理念来塑造如何定义、操作、扩展和细化对象。这是一个庞大的主题,但是你可以从一些优秀的在线资料开始,这些资料可以帮助你理解关键的 OO 概念,包括封装、继承、多态等等。
使用类方法和数据成员
采用面向对象方法进行编码的一个重要部分是这样一个概念,即关于对象和对象实例的数据属于这些实体,为了操作和查询这些数据,应该使用生成这些对象的类的设计所提供(和规定)的技术。简而言之,这意味着该类定义并提供给开发人员的方法(或函数)。
对于那些刚刚接触面向对象开发的人来说,这并不像看起来那么困难。它体现了面向对象的原则之一,封装。使用对象所需的一切都整齐地包装(封装)在它的类中。这种方法有无数的好处,你可以在许多面向对象的设计文本中广泛地读到,比如 a press 'Interactive Object-Oriented Programming in Java(Sarcar,ISBN 9781484254035)和无数的在线网页中。也要记住,当你写自己的类时,你有自由选择你想要的类方法。
发展您特定于 Java 的编码知识
正如我已经说过的,学习所有的 Java 是一项巨大的努力。学习面向 Android 的 Java 并不那么令人畏惧,但仍然涵盖了 Java 领域的大量内容。为了让你开始,这里是开始掌握的关键领域,带有一些最好的免费在线资源的链接。虽然我将参考 Java 编程 Wikibook,这是一个免费的在线资源,但许多其他在线资源也同样不错。你也可以从专门针对 Android 的 Java 书籍中受益匪浅,比如 Apress 'Learn Java for Android Development(spth 和 Friesen,ISBN 9781484259429)。
接口和实现
理解 Java 对象如何构建和扩展其他对象,并提供一个派生版本应该做什么的模板,以及如何做的机制。 http://en.wikibooks.org/wiki/Java_Programming/Interfaces
见。
线程和并发
特别是在我们的多核 CPU 时代,只要小心,并发和线程提供了同时执行大量工作流的方法。 http://en.wikibooks.org/wiki/Java_Programming/Threads_and_Runnables
见。
碎片帐集
Java 是谨慎管理内存等资源方法的早期采用者,这种方法试图将开发人员从跟踪和手动处理资源分配中解放出来——至少可以说这是一项容易出错的任务!参见 https://en.wikibooks.org/wiki/Java_Programming/Java_Overview
的自动内存垃圾收集部分。
异常处理
当事情确实出错时,目标是提供安全、结构化和信息丰富的方法来优雅地处理问题。 http://en.wikibooks.org/wiki/Java_Programming/Exceptions
见。
文件处理
虽然我们的 FirstJavaDemo 应用让用户通过键入直接在控制台输入数据,但更常见的是使用文件和管理它们的语义来消费和创建数据。 http://en.wikibooks.org/wiki/Java_Programming/BasicIO
见。
无商标消费品
Java 在概念上是一种“强类型”语言,这意味着它提供了防护栏和保护,以确保Strings
、integers
和其他类型永远不会与不兼容的数据有效载荷一起存在,并且一种类型的数据不会意外地放入另一种类型的变量中。这是一个关键的保护机制,但是在某些情况下会造成不灵活。泛型提供了维护强类型的方法,但是当支持不同类型数据的相同通用逻辑时,可以避免不必要的重复和过多的重复。 http://en.wikibooks.org/wiki/Java_Programming/Generics
见。
收集
正如像整数这样的原始数据类型集可以分组到数组中一样,对象也可以分组到方便的集合中。集合是 Java 支持将对象组作为集合进行操作的一种方式(但不是唯一的方式)。 http://en.wikibooks.org/wiki/Java_Programming/Collections
见。
摘要
没有哪一章或者整本书能够给你一本完整的 Java 初学者指南,但是在这一章中,我们尝试了退而求其次的方法。现在,您已经了解了 Java 语言、Java 软件产品、Java 对 Android 的意义,以及学习内容的路线图和帮助您掌握 Java 的资源。我强烈建议刚接触 Java 的人,在开始阅读本书的其余部分之前,先探索一下本章中提到的其他资源,甚至更多的商业和免费在线资源。即使是一点点的阅读和实践也会为你的 Android 理解收获巨大的回报。
八、Android 开发中的 XML 简介
本章将讨论两个主题。首先,我们将从初学者的角度来看一看 XML,对于那些从未接触过 XML,也没有深入研究过它的使用模式、特点、优势和问题的人来说。本章的第二部分将涵盖 Android 开发中的三个主要领域,在这三个领域中,您通常会接触到 XML 的读取、编写和编辑:
-
我们已经介绍过的应用清单
-
资源定义,特别是字符串等常量
-
活动规划,这是一个很大的话题,但是可以一步步来
XML 入门
可扩展标记语言,简称 XML,是标准通用标记语言 SGML 的一个老标准的子集。XML 的目标是以一种标准化的结构来传递数据和有关数据的信息——称为元数据——这种结构可以被查询和使用,而无需事先了解数据的含义或该结构存在什么规则。我知道这是一个相当抽象的定义。维基百科提供了另一种定义:
可扩展标记语言(XML)是一种标记语言,它定义了一组规则,用于以人类可读和机器可读的格式对文档进行编码。
——
https://en.wikipedia.org/wiki/XML
,2020 年 7 月
就定义而言,这也好不到哪里去。一个更实用的定义可能如下:
XML 是一种标记语言,它定义了如何交换数据,并描述了正在交换的数据,以便人类和机器都能理解这些数据及其结构和与其他数据的关系。
—格兰特·艾伦 2020 年 7 月
仍然不完美,但正在接近。遗憾的是,这个评论可以应用于整个 XML,而不仅仅是我简单的描述。也许更好的理解 XML 的方法是深入研究它的结构和工作方式。
关于 XML 的一些要点。严格来说,XML 不是一种编程语言…用 XML 写的东西不“做”任何事情。XML 是一种标记语言,旨在向数据传达含义和结构,以便程序和编程语言可以用这些数据做事情。
当您创建 XML 时,您正在将文本放入一个文档中,该文档的某些方面受 XML 标准和规则的约束,这就是所谓的 XML 文档。无论 XML 文档变得多么复杂或费解,您总是可以后退一步,把它仅仅看作文本——在某些地方有特殊含义、有约定的结构和语法规则的文本,但它仍然是文本。
对于 XML 文档中需要什么样的预定义组件,XML 没有固定的定义。它使用基本的构造块定义了如何在文档中定义自己的组件的规则和语法。XML 及其规则的总体目标是允许一种独立于机器/程序的方式来描述和打包数据,并使描述和结构在 XML 文档本身或相关的 XML 模式中自我描述。
让我们看看关键的 XML 构建块。
XML 版本和编码
在 XML 文档的开头应该有一个特殊的标记,表明它遵循哪个版本的 XML 标准,以及 XML 中的文本使用什么字符编码(ASCII、UTF-8 等)。).默认情况下,在几乎所有的 Android 开发中,该声明将规定 XML 版本 1.0 和 UTF-8 编码。特殊标签采用以下形式,使用特殊的<?xml
和?>
开始和结束标记:
<?xml version="1.0" encoding="utf-8"?>
这有助于任何读取和使用 XML 文档的程序理解如何解释文本和 XML 结构。
XML 元素
元素是 XML 的基本构建块,是为数据提供含义和结构的“标签”,但它们本身不是数据。XML 元素是嵌套的,因此 XML 文档中最外面的第一个元素称为根元素,它可以包含(嵌套)一个或多个子元素。XML 文档中只有一个根元素。
元素标签包含在<
(大于)和>
(小于)符号中。对于我定义和使用的每个元素,我将需要一个开始标记和一个同名的匹配结束标记,并在结束标记中的元素名称前添加一个前导的/
(反斜杠)。必须使用这种方法使我的 XML 语法正确——用 XML 的行话来说就是“格式良好”。例如,我可能决定创建一个 XML 文档,在一个模仿地址簿的数据结构中保存人们的联系信息,并决定我的根元素将被称为<addressbook>
。
我的 XML 文档开始看起来像这样。
<?xml version="1.0" encoding="utf-8"?>
<addressbook>
</addressbook>
这包括版本和编码特殊标记和<addressbook>
根元素开始标记,用匹配的</addressbook>
结束标记正确结束。目前为止,一切顺利。
我可以包含一个人的子元素和一个人的名和姓的子元素,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<addressbook>
<person>
<firstname>Jane</firstname>
<lastname>Smith</lastname>
</person>
</addressbook>
目前为止,一切顺利。请记住,我是决定元素名称的人,也是决定它们的用途的人。
Note
元素的结束标记不必总是单独在一个新行上。事实上,XML 的任何部分都不需要换行。但是这样做确实使您的 XML 更易于阅读。人们普遍接受的 XML 标准是,开始标记出现在新的一行,结束标记出现在开始元素标记出现的同一行的末尾,例如前面的<firstname>
和</firstname>
,当该标记没有子元素时,或者开始和结束元素标记单独出现在一行中,其中嵌套有任何子元素,例如前面的<person>
和</person>
标记。标签也缩进以表示它们是子元素,但是这也是为了方便读者的约定。
如果这是一个真正的通讯录,我会有不止一个人在里面。但是您可能会注意到,对于阅读这个 XML 的计算机(甚至人类)来说,很难区分哪个人是哪个人。如果我们这样做,任何试图跟踪哪个人的程序都会有问题:
<?xml version="1.0" encoding="utf-8"?>
<addressbook>
<person>
<firstname>Jane</firstname>
<lastname>Smith</lastname>
</person>
<person>
<firstname>John</firstname>
<lastname>Jones</lastname>
</person>
</addressbook>
在 XML 中解决这个问题的一个方法是使用属性,它的作用就像元素的参数。
XML 属性
任何 XML 元素都可以有零个或多个相关联的属性。属性不同于元素本身提供的数据,出现在元素名称的< >
标记中,紧跟在标签名称之后。属性采用以下形式
attribute_name="value in double quotes"
继续我们的地址簿示例,我们可以通过向<person>
元素添加 ID 属性来区分 XML 数据中的不同人,使用数值给每个人一个不同的 ID。结果将如下所示:
<?xml version="1.0" encoding="utf-8"?>
<addressbook>
<person id="1">
<firstname>Jane</firstname>
<lastname>Smith</lastname>
</person>
<person id="2">
<firstname>John</firstname>
<lastname>Jones</lastname>
</person>
</addressbook>
请注意,尽管您和我可能认为这里给出的 ID 值是一个整数,甚至已经编写了程序将其视为整数,但是 XML 总是将属性放在双引号中,使它们看起来像是字符串。
如果一个元素有多个属性,它们只是用一个空格隔开。例如,我们可以使用 dateadded 属性跟踪将某人添加到地址簿的日期:
<?xml version="1.0" encoding="utf-8"?>
<addressbook>
<person id="1" dateadded="2020-01-01">
<firstname>Jane</firstname>
<lastname>Smith</lastname>
</person>
<person id="2" dateadded="2020-01-31">
<firstname>John</firstname>
<lastname>Jones</lastname>
</person>
</addressbook>
XML 值
XML 文档中封装的实际数据是包含在开始和结束元素标记中的值。在我们正在进行的地址簿示例中,我们的值类似于“Jane”代表一个<firstname>
元素,而“Smith”代表一个<lastname>
元素。用所有的 XML 语法和元素标记来表达这一点似乎有些多余(一般来说,冗长是对 XML 的一种常见批评),但是作为一个阅读 XML 的人,它很好地进行了自我描述,并且还提供了关于结构、完整性等方面的保证。
说到完整性,元素本身不包含任何数据是完全可能的,正如本章前面我们的第一个地址簿 XML 文档所显示的那样。通过使用您已经看到的开始和结束标记方法,以及使用只让元素标记名出现一次并带有结尾/
(反斜杠)的快速自结束标记方法,有一些选项允许您的元素存在并且不包含任何数据。让我们给我们的<person>
结构添加一个<middleinitial>
元素,显示两个选项,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<addressbook>
<person id="1" dateadded="2020-01-01">
<firstname>Jane</firstname>
<middleinitial></middleinitial>
<lastname>Smith</lastname>
</person>
<person id="2" dateadded="2020-01-31">
<firstname>John</firstname>
<middleinitial/>
<lastname>Jones</lastname>
</person>
</addressbook>
对于没有数据的元素,<middleinitial></middleinitial>
和<middleinitial/>
表单都有效。请注意,不能在自结束标记下嵌套任何子元素。
XML 名称空间
XML 的巨大优势之一是它使使用数据的开发人员和系统更容易交换数据。一旦您习惯了以 XML 格式共享数据,就开始觉得这是您的第二天性。但是你可能会在这种简单的传输和分享中遇到问题。一个特别的问题是,由于您定义了自己的 XML 标记(甚至可能定义了自己的 XML 模式),您可能会发现其他人开发了自己的模式,并使用了与您的模式相冲突的元素名称。如果您和他们从不共享数据,这没什么大不了的,但是如果您成为一名共享 XML 格式数据的从业者,当您试图混合来自多个来源的数据时,您可能会遇到名称冲突。那你是做什么的?
XML 标准提供了一种由两部分组成的方法来帮助解决这种名称冲突。首先,您可以用标识字符串作为任何元素的前缀,标识字符串是一个标记,指明给定元素被认为是从哪个源派生的。因此,如果我们知道我们将来自我们的<addressbook>
的数据与我们自己对<person>
的定义和其他人的数据混合在一起,而这些人可能具有完全不同的结构和元素集,我们可以给我们的元素加上前缀来区分它们,例如:
<?xml version="1.0" encoding="utf-8"?>
<my:addressbook>
<my:person id="1" dateadded="2020-01-01">
<my:firstname>Jane</my:firstname>
<my:middleinitial></my:middleinitial>
<my:lastname>Smith</my:lastname>
</my:person>
<my:person id="2" dateadded="2020-01-31">
<my:firstname>John</my:firstname>
<my:middleinitial/>
<my:lastname>Jones</my:lastname>
</my:person>
</my:addressbook>
这个前缀充当这些元素的名称空间,而另一个 XML 文档中带有 person 混淆版本的项目的名称空间会有不同的前缀,例如:
<?xml version="1.0" encoding="utf-8"?>
<other:list_of_addresses>
<other:person >
<other:name>Judy</other:name>
<other:surname>Walsh</other:surname>
</other:person>
XML 标准要求我们提供一个 URI 作为名称空间的唯一区分符——基本上,这个属性保证是唯一的,并反映我们为文档定义的 XML 结构。这是在元素的开始标记中用 xmlns 属性指定的,通常您会在 XML 文档的根元素中看到这一点,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<my:addressbook xmlns:my="https://www.beginningandroid.org/XMLnamespaces/addressbook">
<my:person id="1" dateadded="2020-01-01">
<my:firstname>Jane</my:firstname>
<my:middleinitial></my:middleinitial>
<my:lastname>Smith</my:lastname>
</my:person>
<my:person id="2" dateadded="2020-01-31">
<my:firstname>John</my:firstname>
<my:middleinitial/>
<my:lastname>Jones</my:lastname>
</my:person>
</my:addressbook>
XML 世界的其他部分
随着知识的增长,我们可能会对本章没有涉及的 XML 标准的其他部分感兴趣:
-
CDATA: CDATA 值是特殊的 XML 值,它们被视为一字不差的数据,不会被处理或解析来发现其中是否有进一步嵌套的 XML 标记。这最初可能没有多大意义,但是您会发现它经常被用来传递其他形式的数据标记,或者像 HTML 这样的表示标记,作为 XML 文档中的数据——它防止 XML 解析器和希望执行 XML 标准的程序无意中出错,这些东西看起来可能是 XML,但实际上不是——当然它们也不遵循 XML 规则。
-
XML Path 和 XQuery: XML Path 和 XQuery 提供了遍历 XML 文档的方法,通过导航标签的层次结构、评估属性等来查找数据。默认情况下,这些工具不用于 Android 应用的基本设计和构建,但是如果您构建的 Android 应用本身可以操作基于 XML 的数据,您当然可以使用它们。
-
XSLT 和转换:XSLT 是 XML 样式表转换语言,这是一种编程语言,旨在查询和转换 XML 的结构和数据,创建派生数据作为输出。XSLT 转换的输出通常是另一个 XML 文档,尽管它也可以是不同格式的数据。
有成千上万的网站可以帮助您了解更多关于 XML 的知识,但是现在您已经了解了足够的基础知识,可以深入了解 Android 如何使用 XML,并开始为 Android 应用编写和编辑 XML 文件。
在 Android 应用中使用 XML
正如本章开头提到的,Android 在三个主要领域使用 XML 来帮助构建和运行应用。概括来说,这些是
-
我们已经介绍过的应用清单
-
资源定义,特别是字符串等常量
-
活动规划,这是一个很大的话题,但是可以一步步来
查看 Android 清单 XML 文件
不管您对 XML 的熟悉程度如何——不管您是否已经使用它很多年了,也不管您是否参加了本章前半部分概述的速成班——我们现在都可以详细探讨 Android 清单。清单 8-1 再次引用了 androidmanifest.xml 内容:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.beginningandroid.myfirstapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Listing 8-1The androidmanifest.xml file revisited
在第四章的中,我们强调了这个文件的一些元素和其他部分,例如,使用一个名称空间来唯一标识所有具有"android"
名称空间的 Android 资源元素,绑定到唯一的 xmlns 引用xmlns:android="
http://schemas.android.com/apk/res/android
"
,以及使用
Note
如果你还没有发现的话,Google 倾向于在 Android 开发中的所有 XML 使用中广泛利用属性。有时,许多人强调这太过分了,许多重属性的元素可以被重新设计为包含数据值的子元素。这是一个永无止境的理论争论,你现在可以加入了!
在您的清单中通常会有一个<application>
元素标签。该标记的突出属性是android:icon
和android:roundIcon
选项以及android:label
和android:theme
属性——后一个示例充当一组其他样式和设计 XML 设置的名称,您可以采用这些设置来赋予您的应用特定的外观和感觉。你会注意到它们都使用了"@path/detail"
形式。"@"
符号是引用项目的res/
层次结构中的关键资源的简写——无论是由 mipmap 图像控制的应用图标,还是值层次结构中的strings.xml
文件中的字符串值(稍后讨论),等等。
我们已经简要介绍了清单 8-1 中的<activity>
元素,并指出这个特定条目如何将您的MainActivity
java 活动链接到 Android 应用启动时触发的"MAIN"
动作。
您的清单可以并且将会包含更多的<activity>
元素,每个元素对应于您为应用创建的一个活动。请记住,对于您可以为您的应用创建多少个活动没有真正的限制,所以您最终可以看到许多这样的元素。
随着本书的进展,我们将继续学习您对 Android 清单及其 XML 细微差别的知识。
使用 XML 进行资源定义
Android 利用 XML 的另一个主要领域是常量或引用值的定义和使用,其中给定的数据在一系列用例中以只读方式使用。有大量的例子可以说明恒定数据如何有助于简化应用的构建,并减少从确保所有活动和可视元素的一致外观到确保关键数据值(如珠穆朗玛峰的高度(8848 米)、应用的名称(“MyFirstApp”)或其他不变的数据值可以在一个地方声明,但在许多其他地方使用。
一些关键的资源定义文件是
-
colors.xml:一个参考文件,用于为各种红绿蓝(RGB)十六进制颜色表示提供对您有意义的有用名称。
-
strings.xml:一个引用文件,用于抽象文本字符串,如简单的单词、短语、句子和段落,以便可以在各种源代码和其他文件中引用它们,而无需将文本复制到许多地方。
-
styles.xml:一个参考文件,用于在自我声明的样式下收集颜色定义和其他样式元素,以便该样式可以在整个应用中用作公共设计主题。
-
dimens.xml:提供不同尺寸和大小设置的抽象表示的参考文件,允许您在一个位置控制密度/大小的变化,而不必在许多不同的文件中编辑许多值。
这些资源文件的定义对于高层次的概念理解来说是很好的,但是对于理解它们是如何使用的来说还有待改进。让我们开始动手混合一些东西,并向这些资源文件中添加新的内容。
改变颜色
最容易修改的 XML 文件是您的colors.xml
文件。该文件可以包含任意数量的颜色别名。默认内容如清单 8-2 所示。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
</resources>
Listing 8-2The default colors.xml file content
是时候对您的MyFirstApp
应用进行更多的修改了。通过用"colorPrimary"
的 name 属性改变 color 元素,我们可以在布局引用这个颜色资源定义的地方改变应用的外观。当在整个应用设计中使用一致的颜色选择和调色板时,您可以看到这是如何使开发变得更加容易的。
选择任何你感兴趣的 RGB 颜色表示。就我个人而言,我非常喜欢漂亮的森林绿色,所以我会选择十六进制值#2F8800
,这样您的colors.xml
定义就会更新,如清单 8-3 所示。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#2F8800</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
</resources>
Listing 8-3Altering color definitions centrally via colors.xml
保存这些更改,然后再次运行您的MyFirstApp
应用。你应该看到颜色变化生效,如图 8-1 所示。
图 8-1
在 MyFirstApp 中更改颜色
将此与您的项目使用 Android 默认设置运行的相同应用进行比较,如第三章图 3-19 所示。在你运行的应用中,颜色的差异应该是显而易见的,即使它们在本书中以黑白形式打印出来看起来没有什么不同。
获得描述性信息
strings.xml
档是安卓世界里一个沉睡的巨人。在这里,您可以定义各种各样的常量,这些常量基本上很少改变,或者至少在您的应用上下文中以固定的初始值开始。当你想到常数时,你可以超越我之前给出的幽默例子(为什么把自己限制在以米为单位的珠穆朗玛峰的高度——为什么不是英尺、杆、弗隆或链?!).
更严重的是,您应该随时注意(没有双关的意思)在您的代码中输入文字字符串,并问自己是否将单个条目放入strings.xml
文件中会更好。从字面上看,程序员几十年的经验教训都可以归结为一句格言“从你的代码中抽象出文字串——你以后会感谢我的。”
让我们向strings.xml
文件添加一个条目,目的是替换您在MyFirstApp
应用中看到的屏幕文本。除了定义新的参考值之外,还有更多工作要做,但我们将在下一节讨论。现在,打开strings.xml
文件,它看起来应该很像清单 8-4 。
<resources>
<string name="app_name">MyFirstApp</string>
</resources>
Listing 8-4The default strings.xml file for MyFirstApp
添加一个新行,创建一个属性为 name 的<string>
元素条目,等于一个名为welcome_text
的文本字符串。<string>
元素的值应该是你希望用户在屏幕上看到的任何文本。您的strings.xml
文件应该类似于清单 8-5 。
<resources>
<string name="app_name">MyFirstApp</string>
<string name="welcome_text">Hello Android, from the strings.xml file!</string>
</resources>
Listing 8-5Adding a new <string> element to strings.xml
记住,这是 XML。name 属性的值可以是您喜欢的任何值—"welcome_text", "look_at_my_great_string", "text1",
等等。只要您在将来正确地引用它,您就会获得可扩展标记语言的“可扩展”好处!让我们在下一节看看如何使用这个新字符串。
用 XML 定义活动布局
也许 Android 依赖 XML 的最大和最复杂的领域是视图布局的定义和管理——也就是说,定义和控制屏幕用户界面如何呈现给用户,以及应用如何以编程方式控制它。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello Android!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 8-6The default activity_main.xml layout file for MyFirstApp
我们默认的MyFirstApp
应用是通过新建项目向导生成的,默认情况下,它为您的MainActivity
活动选择了默认布局"ConstraintLayout"
。这是您的活动可以使用的众多布局类型之一,从父 视图 类派生而来。我们将在接下来的几章中深入研究视图、布局以及它们的排列。现在,让我们开始在你现有的布局上工作。
一个ConstraintLayout
被设计来给你非常灵活的控制其他 UI 元素的大小和位置——被称为窗口小部件——出现在它里面。从清单 8-6 中,您可以看到我们只描述了一个小部件,它是一个TextView
。我们将在接下来的两章中添加更多的小部件。现在,让我们通过使用小部件文本的strings.xml
引用值的能力,而不是硬编码值,使我们的屏幕问候更加灵活。
让我们更新布局 XML,将硬编码的字符串"Hello Android!"
替换为对welcome_text
常量的引用,该常量是我们在前面的strings.xml
文件中定义的。更改android:text
属性,使其引用"@string/welcome_text"
,这是前面创建的字符串别名的名称。您的布局现在应该看起来如清单 8-7 所示,更改以粗体显示。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/welcome_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 8-7Changing TextView text to reference strings.xml constant
Note
有点烦人的是,虽然引用值 XML 文件以复数命名——strings . XML、colors.xml 和 styles . XML——但当我们实际引用它们包含的定义时,我们使用单数引用——string、@color 和@style。Android Studio 会在你输入的时候提示你。
随着我们的布局 XML 被更改为引用我们的 strings.xml welcome_text 值,是时候再次运行您的应用来查看实际效果了,如图 8-2 所示。
图 8-2
MyFirstApp 中新的欢迎文本完全通过 XML 定义控制
瞧啊。您已经用可用的 XML 定义更改了应用中的颜色和欢迎文本。使用 XML 作为定义和控制机制的 Android 应用越来越多,当结合 Java 处理 XML 的能力时,结果可能会令人震惊。但是现在,您已经掌握了继续 Android 之旅所需的 XML 基础知识。
摘要
在这一章中,你已经对 XML 以及 Android 如何利用 XML 进行了快速的介绍。XML 是一个很大的主题,一章无法很好地解释它,我鼓励您在网上和其他书籍中寻找更多的信息。XML 在 Android 开发领域的最大优势之一是既能使用它来控制应用逻辑,又能通过应用逻辑来控制它。我们将在接下来的两章中更深入地探讨这个问题,在那里我们将更深入地了解 Android 布局和小部件的世界!
九、探索 Android 概念:核心 UI 小部件
在本书的第一部分中,您快速浏览了为您的 Android 应用创建新的 Android Studio 项目所需的步骤,并探索了默认 Android 项目的结构、文件和各个部分的用途。您深入研究了使用应用配色方案的 XML 配置创建自己的行为的初始步骤,并利用文本资源在应用的主活动中个性化欢迎消息。
在这一章中,我们将超越前面章节的表面变化,直接进入所有 Android 应用中可用的主要用户界面元素,以及那些将成为您为所有未来 Android 工作开发和部署的许多活动的主体的元素。您将学习如何部署、调整和控制作为 Android 开发人员可用的许多用户界面小部件,并开始打造更复杂的 Android 用户体验之旅。
一切从视野开始
在 Android 世界中,你可以在用户界面上显示的每一个主要元素——一个活动——都继承自一个叫做 View 的基类。任何源自视图的小部件,无论是文本框、按钮、选择列表还是其他什么,都为您提供了一系列源自这种视图谱系的常见行为和好处。这些常见的行为和属性包括以一致的方式设置和控制字体、颜色和其他样式特征。
除了这些共同的特征之外,由于所有小部件都有共同的视图遗产,所以还有一系列的方法和属性可用。接下来我们将讨论这些遗传特征。
从视图派生的关键方法
从 View 基类派生的任何小部件都继承了一系列方法和属性,这些方法和属性有助于管理基本状态管理、与其他小部件分组、布局中的父对象和子对象等等。您将看到的属性包括
-
findViewById()
:查找具有给定 ID 的小部件,广泛用于将 XML 中定义的小部件链接到 Java(和 Kotlin)中的控制逻辑。 -
getParent()
:查找父对象,无论是小工具还是容器。 -
getRootView()
:获取从活动对setContentView()
的原始调用中提供的树根。 -
setEnabled(), isEnabled()
:设置并检查任何小部件的启用状态,例如,复选框、单选按钮等。 -
isClickable()
:报告该视图(如按钮)是否对点击或按压事件做出反应。 -
onClickListener()
:对于像按钮这样可以点击的视图,定义了一个回调,当相关的视图被点击时会被触发。回调实现包含您决定需要的任何逻辑。
从视图派生的关键属性和特性
除了 View 基类的核心方法,所有小部件还继承了一些关键属性。这些属性包括
-
android:contentDescription
:这是一个与辅助工具可以使用的任何部件相关的文本值,其中部件的视觉方面对用户帮助很小或没有帮助。 -
android:visibility
:决定小部件在第一次实例化时是可见还是不可见。 -
android:padding, android:paddingLeft, android:paddingRight, android:paddingTop, android:paddingBottom
:在小工具的所有边上填充值的各种方法。注意小部件的填充也可以在运行时用
setPadding()
方法设置。
介绍 Android 中的核心 UI 小部件
在构建 Android 应用时,您会一次又一次地使用一组核心 UI 小部件,因为它们提供了计算机、智能手机等用户在过去几十年中已经学会期待的许多常见用户界面体验。让我们一步一步地看这些核心部件的例子。
用文本视图标记事物
提供一个可读的文本标签可能是所有 UI 小部件中最基本的,你会在几乎所有发明的设计工具包中找到一个标签或静态文本等价物。Android 提供了TextView
小部件来实现这个功能,允许您在活动 UI 的任何地方放置一个静态字符串(或者至少一开始是静态的,因为TextView
的值可以通过编程来更改)。该字符串的文本完全由您决定,是提供相邻小部件的描述、标题、一些评论还是注释——选择权在您。
Android 提供了两种主要的方法来定义一个TextView
和所有的 UI 小部件。第一种方法是完全通过使用 Java 代码定义您的TextView
,设置像屏幕上的位置、大小、文本有效载荷等属性。任何以前有过开发重要用户界面经验的人都会告诉你这是一个很大的工作量,很容易出错,你会很快看到你的代码变得难以管理。但是有更好的方法,你已经用过了!
使用 Android 的其他 UI 设计方法要快得多,也容易得多,这是通过声明性 XML 实现的。在第三章中,您创建了自己的MyFirstApp
应用,并负责TextView
小部件中的文本。在第七章中,你进一步涉猎了控制TextView
字符串的来源和其他一些装饰属性。您可以随时添加您认为有意义的任意数量的TextView
小部件,或者直接通过在活动的 XML 定义文件中定义更多的<TextView>
元素,或者使用图形化布局编辑器。
图形布局编辑器让您担心视觉样式,并在幕后自动生成必要的匹配 XML 来描述您的TextView
小部件。让我们现在就来测试一下。即使你的MyFirstApp
应用非常好,我们也不要把它放在眼里。在 Android Studio 中,通过File
➤ New...
➤ New Project
菜单选项创建一个新项目。就像我们在第三章做的那样,给你的项目取一个有意义的名字,比如TextViewExample
,选择空的活动模板。这将创建您的新项目,并默认在activity_main.xml
文件中放置一个TextView
小部件(就像您创建MyFirstApp
应用时发生的一样)。您的activity_main.xml
内容应该类似于清单 9-1 。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-1A fresh Android Studio application using the Empty Activity template
与其在这里编辑 XML,不如通过单击 Android Studio 视图最右侧的 Design 按钮来调用图形布局设计器。这应该会隐藏 XML 内容,而是为您的应用呈现等效的图形布局,如图 9-1 所示。
图 9-1
调用图形布局设计器
这个视图中有很多内容,但是最好的学习方法是依次试验每个领域。从 Palette 部分开始,您将看到一个小部件类型列表——Common、Text、Buttons 等等——在它的旁边是一个实际小部件的列表。在这个列表的顶部,你会看到TextView
,如图 9-1 所示。单击并拖动它到你在屏幕中间看到的微型屏幕上,你应该会看到一个微小的浮动标签在移动,直到你松开鼠标键。去做吧,在任何你认为“正确”的地方去做。
第二个TextView
现在已经就位,但是您将会看到一个红色的错误标志——单击它将会通知您还没有对新的TextView
设置“约束”,因此如果您实际构建并运行这个应用,您已经放置它的位置将不会被保留。别慌!我们现在要解决这个问题,但是是在你的 XML 定义中,并且在这个过程中学习更多关于TextView
小部件的知识。通过单击最右侧的代码按钮,切换回布局的代码视图。
您的activity_main.xml
文件现在将类似于清单 9-2 ,添加了一个新的<TextView>
元素和相关属性。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="176dp"
android:layout_marginLeft="176dp"
android:layout_marginBottom="252dp"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-2Your revised activity_main.xml file
我们已经在前面的章节中修改了<TextView>
属性,现在我们要去逛逛了!让我们介绍一些新的属性来帮助您控制TextView
和其他小部件:
-
Android:layout _ margin star
-
Android:layout _ margin
-
android:layout_marginLeft
-
android:layout_marginRight
-
android:layout_marginBottom
顾名思义,这些都有助于在小部件的不同边缘设置边距。
我们还受益于应用的默认布局方法,称为ConstraintLayout
。我们将在接下来的章节中更详细地讨论布局,但是现在你可以把它们看作是帮助你在活动中放置小部件的方法。有些布局会照顾到你的很多设置,代价是稍微少了一些艺术自由,而另一些会给你全权委托,但让你需要做更多的工作。ConstraintLayout
是前一种,试图尽可能帮助你得到好看的布局。它为所有小部件的布局带来的几个关键属性包括
-
app:layout _ constraint top _ toTopOf
-
app:layout _ constraint bottom _ tobottom of
-
app:layout _ constraint left _ toLeftmOf
-
app:layout _ constraint right _ toRightOf
还有许多layout_constraint*
样式属性的组合,它们都是为了能够指定如何根据与另一个小部件的顶部、底部、左侧或右侧边缘的接近度和关系,以及它的中心、相关小部件中任何文本的位置等等来对齐和调整小部件的大小!
我们需要这些布局边距属性和约束属性为我们的新TextView
提供可预测的行为,以粗体显示:
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
android:layout_marginStart="176dp"
android:layout_marginLeft="176dp"
android:layout_marginBottom="252dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
实际值相当容易理解。这三个不同的边距属性以像素为单位设置 TextView 周围的边距大小。布局约束属性将垂直和水平约束默认绑定到称为“父”的东西。在这种情况下,这意味着父活动本身。相反,您可以参考另一个小部件的android:id
,让它作为约束小部件的指导因素。
有了这些修改,你应该保存你的activity_main.xml
文件,然后运行你的TextViewExample
应用,看看你的新标签是否在你指定的位置,如图 9-2 所示。
图 9-2
应用的更多 TextView 标签
为了完整地描述TextView
部件,您应该知道有将近 100 种不同的属性可以控制TextView
标签的行为、样式、大小、颜色等等。更多例子包括
-
android:hint:要显示的提示。
-
android:typeface:设置标签使用的字体(例如,monospace)。
-
android:textStyle:指示应该对文本应用粗体(bold)、斜体(italic)和两者(bold_italic)的什么组合。
-
android.textAppearance:一个综合属性,可以让您一次性组合文本颜色、字体、大小和样式!
-
android:textColor:使用常见的十六进制 RGB 符号来选择标签的文本颜色。比如#0000FF 就是蓝色。
您可以在Ch09/TextViewExample
项目文件夹中找到这个例子的代码。
到目前为止,我们已经看到了 XML 在 Android 中可以做些什么,这是很有说明性的,也很有教育意义,但是开发应用的真正能力和灵活性来自于您选择的编程语言和 Android 的 XML 能力的结合。一旦我们转向更复杂的 UI 小部件,部署您的 Java(或 Kotlin)能力就变得既必要又可取,您将在接下来关于其他 UI 小部件的章节中看到这一点。
扣上完美的用户界面
按钮是任何 UI 开发的基础,可以追溯到模拟电视、收音机等电子设备上真实世界的模拟按钮(还记得那些吗?),还有汽车仪表盘。Android 提供了几种类型的按钮,其中最简单的是 android.widget 包中的Button
小部件,它在按钮的“表面”包含简单的文本。让我们开始创建一个新的应用,它使用一个按钮和一些 Java 逻辑来跟踪和控制出现在按钮上的文本。您可以在Ch09/ButtonExample
项目文件夹中查看这个例子。
首先,使用 Android Studio 新建项目向导创建一个新项目,并选择空的活动模板作为起点。将您的项目命名为ButtonExample
(或您认为同样具有描述性的名称)。创建项目后,打开 activity_main.xml 文件并删除默认的 TextView 元素。切换到布局编辑器中的设计视图,从面板中选择Button
小部件并将其放在屏幕上。按钮的确切位置并不重要,因为我们将在以后让它完全填充我们的应用活动。你应该会看到一个与图 9-3 相似的布局。
图 9-3
向 ButtonExample 的设计蓝图添加按钮
请注意,这两个错误通知您该按钮不受约束。这是因为当您选择使用 ConstraintLayout 布局来容纳其他子 UI 小部件时,这些小部件中的每一个都需要至少一个水平和一个垂直“约束”,或者关于它们应该如何相对于父布局或另一个小部件的顶部、底部、开始和结束进行定位的指令。要解决这个问题,请在每条边的中点使用两个圆中的一个,然后单击并拖动连接器到设计蓝图的边上。这会将按钮的水平约束设置为活动的侧边,减去任何边距。然后选择按钮上边缘或下边缘中心的圆,单击并拖动连接符到活动的上边缘或下边缘,以设置垂直约束。切换到您的activity_main.xml
文件的代码视图,它应该类似于清单 9-3 中所示。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
tools:layout_editor_absoluteX="161dp"
tools:layout_editor_absoluteY="341dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-3ButtonExample activity_main.xml file, showing Button definition
接下来,我们需要为我们的按钮提供一些魅力,让它真正做一些事情。因此,打开ButtonExample
的MainActivity.java
源文件,用清单 9-4 中的 Java 代码替换它的内容。
package org.beginningandroid.buttonexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button myButton;
Integer myInt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myButton = new Button(this);
myButton.setOnClickListener(this);
myInt = 0;
updateClickCounter();
setContentView(myButton);
}
@Override
public void onClick(View v) {
updateClickCounter();
}
private void updateClickCounter() {
myInt++;
myButton.setText(myInt.toString());
}
}
Listing 9-4Modified Java code for ButtonExample
将这些代码一部分一部分地分解将帮助你理解正在发生的事情,并且展示一些你将用于所有开发的通用方法,而不仅仅是一个点击按钮的应用!
前面清单中的前几行介绍了包名和任何所需的或必需的 Java 类导入:
package org.beginningandroid.buttonexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
已经根据您在新建项目向导中输入的名称为您设置了包名。默认情况下,androidx.appcompat.app.AppCompatActivity
和android.os.Bundle
的类库导入也包含在新建项目向导中。然后,我显式地添加了android.widget.Button
类,因为这是一个预构建的类,它提供了我想要连接到我放置在布局中的按钮的所有公共逻辑和行为。我最后添加了android.view.View
类,因为它提供了预构建的能力,可以为从 View 类派生的小部件上发生的事件定义监听器——这意味着我们在本章中讨论的每个小部件。特别是,如下面几段所示,我定义并使用了一个OnClick
监听器,这样ButtonExample
应用就可以被通知点击事件并触发我想要的逻辑。
通过对MainActivity
的类定义的更改,您可以看到启用OnClick
监听的第一步。我修改了新建项目向导提供的默认类声明,还添加了实现View.OnClickListener
,如下所示:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button myButton;
Integer myInt;
// more code here
}
对该类的这一修改使我们能够稍后提供OnClick
逻辑 Android Studio 将通过一个警告提示您这样做,如果您还没有添加android.view.View
导入,还会提示您添加。接下来我定义了两个方便的变量,一个是名为myButton
的Button
,它将被连接到 UI 小部件,另一个是名为 myInt 的整数,我用它作为计数器来跟踪按钮被点击的次数。
接下来,增强了onCreate()
方法,让它在活动第一次运行时执行我们需要的设置任务。我们将在第十二章中深入探讨活动的四种主要生命周期方法,但是现在你可以相信这样一个事实,即onCreate()
只在活动开始时被调用一次。在ButtonExample
代码中,我们正在定义和实例化myButton
对象,为myButton
设置OnClick
监听器,最初将myInt
的值设置为0
(零),然后调用私有方法updateClickCounter()
。
两个非常简单的步骤组成了整个updateClickCounter()
方法,如下所示:
private void updateClickCounter() {
myInt++;
myButton.setText(myInt.toString());
}
首先,myInt
值增加 1。随后,调用 myButton 对象的.setText()
helper 方法,并将myInt
值的String
表示传递给它,以便更新按钮小部件上显示的文本来反映新的myInt
值。简而言之,我们增加点击计数器的值,并将其显示为按钮文本。
我们跳过的最后一段代码是onClick()
方法,每当单击按钮时,我们定义的监听器都会调用该方法。这个方法中的逻辑简单地调用私有的updateClickCounter()
方法,这意味着我们在onCreate()
时间和任何后续的按钮点击中重用相同的逻辑。
继续运行你的代码——或者书中的Ch09/ButtonExample
代码——你应该会看到一个按钮填充的应用,用一个递增计数器作为按钮标签,类似于图 9-4 所示。
图 9-4
正在运行的按钮示例应用
使用 ImageView 和 ImageButton 获取图片
如果你的 Android 应用仅仅由文本和几个按钮组成,事情会变得非常枯燥——也许除了一个纵横字谜游戏。图像和图片是 UI 设计的核心部分,许多应用不只是装饰性地使用它们,而是作为相册、图像编辑器等核心应用功能的一部分。
Android 有一对与您已经使用过的TextView
和Button
相对应的支持图像的部件——它们是ImageView
和ImageButton
小部件,同样,它们是从基础View
类派生而来的。
与本章中的许多小部件一样,通常最好在蓝图/设计模式和附带的代码视图中使用布局编辑器,用 XML 定义您的ImageView
或ImageButton
,而不是在 Java 中费力地以编程方式定义它们。
ImageView
和ImageButton
都在android:src
值中为它们的相关元素 XML 定义引入了一个额外的属性。这个属性的值是对您提供的图像资源的引用,无论它是一个.png
文件、.jpg/.jpeg
文件、.gif
文件还是其他一些受支持的图像格式。在第四章中,我介绍了项目结构,包括res/drawable
层级。在这个文件夹中,您可以放置图像文件以供参考,Gradle 将在这个文件夹中查找参考图像,以便在构建时捆绑到应用包中。
ImageButton
与ImageView
的不同之处在于支持类似按钮的行为,你已经在本章前面看到过常规的Button
小部件。这意味着ImageButton
小部件可以(也应该)定义onClick
监听器,并构建后续逻辑来处理当ImageButton
被点击时您希望您的应用采取的任何动作或行为,就像您对Button
小部件所做的那样。
如果你还记得我们在第三章中对典型项目布局的概述,你可能会猜到指定android:src
值的默认方法是引用你放在项目的res/drawable
目录中的图形资源(和/或其特定密度的变体)。
清单 9-5 展示了一个 ImageView 的例子,它被配置为引用res/drawable
文件夹中的一幅图像,在这个例子中,它是我在大英博物馆拍摄的罗塞塔石碑的照片。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/rosettastone"
android:contentDescription="The Rosetta Stone"
tools:layout_editor_absoluteX="83dp"
tools:layout_editor_absoluteY="144dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-5Using an ImageView widget for the ImageViewExample layout
运行中的应用如图 9-5 所示。
图 9-5
正在运行的 ImageView 示例应用显示了 ImageView 及其资源
您可以在Ch09/ImageViewExample
文件夹中找到这个例子的代码。
使用编辑文本编辑和输入文本
到目前为止,在您的 Android 小部件之旅中,您已经看到了静态文本标签、可以触发活动的按钮以及显示图像的方式。几乎每个应用都需要用户的输入,而输入通常是文本。没有某种可编辑的表单或字段小部件,任何小部件集都是不完整的,Android 通过EditText
小部件满足了这一需求。
EditText
有一个类层次结构,可以看到它是从你已经知道的TextView
类派生出来的,然后最终是View
类。成为TextView
的子类意味着EditText
继承了很多你在TextView
中看到的功能、方法和数据成员,比如textAppearance
。EditText
还引入了一系列新的属性和特性,让您可以精确地控制文本字段的外观和行为。这些新属性包括
-
android:singleLine:管理回车键的行为,决定是否应该在文本字段中创建新行,或者将焦点转移到活动布局中的下一个小部件
-
android:自动图文集:管理内置拼写纠正功能的使用
-
android:password:配置该字段,在输入字符时显示密码点
-
android:digits:限制输入,只接受数字,隐藏字母类型的字符
Android 还提供了一种更细致的——有些人会说是复杂的——方法来指定EditText
的字段特征。可以使用inputType
属性来捆绑EditText
字段的所有期望属性。我们将在第十章中介绍inputType
以及键盘和输入法的相关主题。清单 9-6 显示了inputType
在其他选项中的作用。
清单 9-6 展示了android:inputType
属性的介绍性用法,在这个例子中,标记用户文本的第一个单词的第一个字母应该自动大写。我们还将常规属性android:singleLine
设置为 false,在EditText
字段中启用多行文本。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/myfield"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:inputType="textCapSentences"
android:singleLine="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-6Configuring EditText field behavior with XML properties
清单 9-7 展示了如何从附带的 Java 包中以编程方式处理一个EditText
字段。你可以在Ch09/EditTextExample
中找到这个例子。
package org.beginningandroid.edittextexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
EditText myfield=(EditText)findViewById(R.id.myfield);
myfield.setText("Our EditText widget");
}
}
Listing 9-7The EditText widget can be manipulated easily from your code
注意,我们在这个清单中引入了findViewById
方法。您可以想象,如果您有任意数量的小部件,那么您需要一种编程方式来找到您想要使用哪一个作为您的程序逻辑的一部分。通过使用形式为R.id.named_id_from_XML_definition
的资源 ID 引用,您可以通过小部件的 android:id 属性隐式地查找并链接到 XML 布局中定义的匹配小部件。所以在这种情况下,R.id.myfield
找到并匹配你的EditText
、@+id/myfield
的android:id
。
你可以在图 9-6 中看到来自EditText
示例的结果。
图 9-6
EditText 小部件的运行,包括通过键盘编辑文本
还有其他一些典型的迹象表明这是一个可编辑的字段。内置的字典和拼写检查器是可用的——试着拼错一个单词,它会以红色下划线出现。当字段具有文本输入焦点时,闪烁的光标也很明显。您还可以通过选择称为AutoCompleteTextView
变体的兄弟窗口小部件(再次从 TextView 和 View 派生)来帮助您的用户更快地键入,它将在用户键入时自动提示完整的建议单词。
Note
通过使用被称为TextInputLayout
的布局,您可以在使用EditText
小部件时变得更加复杂,这种布局包装并扩展了默认的EditText
行为,具有文本提示、高亮显示、助手提示等特性。你可以在developer.android.com
找到更多关于TextInputLayout
的信息。
检查复选框
Android 包含的另一个经典 UI 小部件是CheckBox
,它提供二进制开/关、是/否或选中/未选中小部件。CheckBox
是 View(你猜对了)和TextView
(可能会让你吃惊)的子类。这个类的祖先意味着你可以通过继承获得一系列有用的属性。CheckBox
对象为您提供了一些 Java 助手方法,让您的复选框做一些有用的事情:
-
toggle():切换复选框的状态
-
setChecked():选中(设置)复选框,而不考虑当前状态
-
isChecked():检查复选框是否被选中的方法
在清单 9-8 中,我们有一个带有简单 Java 逻辑的示例复选框布局,您可以在Ch09/CheckboxExample
项目中找到。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<CheckBox
android:id="@+id/check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="The checkbox is unchecked" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-8A layout featuring a CheckBox
虽然一个稍微有吸引力的CheckBox
本身可能看起来不错,但它确实需要为你或你的用户做一些有用的事情。我们通过添加 Java 逻辑来配合我们的布局,从而释放了CheckBox
的功能。清单 9-9 是演示如何将逻辑链接到复选框的 Java 包。
package org.beginningandroid.checkboxexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
public class MainActivity extends AppCompatActivity {
CheckBox myCheckbox;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myCheckbox = (CheckBox)findViewById(R.id.check);
myCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.isChecked()) {
myCheckbox.setText("The checkbox is checked");
}
else
{
myCheckbox.setText("The checkbox is unchecked");
}
}
});
}
}
Listing 9-9Firing up a CheckBox with some programmatic logic
很明显,这里的CheckBox
代码比之前的例子如EditText
要多一些。如果您首先查看导入的类,您会对正在发生的事情有所了解。我已经导入了OnCheckedChangeListener
,并提供了onCheckedChanged()
回调方法的实现。这意味着我们已经将CheckBox
设置为它自己的事件监听器,用于状态改变动作,比如被点击。
当用户现在点击CheckBox
来切换它时,就会触发onCheckChanged()
回调。我们的回调实现的逻辑在切换后测试了CheckBox
的当前状态,并用新状态的书面描述更新了复选框的文本。这是一种很好的方式,既可以将小部件的所有行为捆绑在一个地方,又可以让我们在用户输入时在相同的逻辑流中进行表单验证,而不需要传递一包用户或数据状态。您的代码和相关的运行时输入检查优雅地并排在一起。
图 9-7 和 9-8 显示了我们的 CheckBoxExample 应用处于选中和未选中状态。
图 9-8
该复选框现已选中
图 9-7
该复选框未选中
用开关打开它
这个小部件是在 Android 开发的后期引入的,但是它的用途正如它的名字所暗示的那样。一个Switch
就像一个二元开关,提供开/关模式的状态,用户可以通过用手指滑动或拖动来激活它,就像他们正在切换一个灯开关一样。用户也可以像点击Checkbox
一样点击Switch
部件来改变它的状态。在 Android 的几个版本中,Switch 小部件已经被调整以处理兼容性和其他问题,所以其他变体如“SwitchCompat
”小部件有时被用来代替最初的Switch
小部件,但总体目的和处理是相似的。
除了从View
继承的属性之外,还有一个Switch
小部件提供了android:text
属性来显示与Switch
状态相关的文本。文本由两个辅助方法控制,setTextOn()
和setTextOff()
。
对于Switch
小部件,还可以使用其他方法,包括
-
setChecked()
:将当前开关状态变为开(类似复选框) -
getTextOn()
:返回开关打开时使用的文本 -
getTextOff()
:返回开关关闭时使用的文本
Ch09/SwitchExample
项目提供了一个Switch
的工作示例。清单 9-10 显示了一个简单的开关布局。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Switch
android:id="@+id/switchexample"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="The switch is off" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-10The layout for SwitchExample
Note
不要试图让你的android:id
成为@+id/switch
中的“开关”。Java 为其类似 case 的分支逻辑语句保留了单词switch
,所以你将需要使用其他的东西,就像我在这个例子中一样。
配置开关行为的逻辑存在于我们的 Java 代码中,如清单 9-11 所示。
package org.beginningandroid.switchexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Switch;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
public class MainActivity extends AppCompatActivity {
Switch mySwitch;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mySwitch = (Switch) findViewById(R.id.switchexample);
mySwitch.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.isChecked()) {
mySwitch.setText("The switch is on");
} else {
mySwitch.setText("The switch is off");
}
}
});
}
}
Listing 9-11Controlling switch behavior in code
如果代码结构和逻辑对您来说很熟悉,那就应该如此!从概念上讲,a Switch
和 a CheckBox
几乎是相同的,并且你对它们进行操作的逻辑几乎是可以互换的,至少在入门级上是这样。在图 9-9 和 9-10 中,你可以看到开关的动作,包括开和关。
图 9-10
开关打开,逻辑已触发更改其文本
图 9-9
处于关闭位置的开关部件
用单选按钮选择事物
结束我们对 Android 核心 UI 部件的详细观察后,是时候介绍一下RadioButton
。就像在大多数其他小部件工具包中一样,Android 的RadioButton
共享你已经体验过的CheckBox
和Switch
小部件的双态逻辑,并通过成为View
和CompoundButton
的子类获得许多相同的功能。正如您在前面的例子中看到的,您可以通过像toggle()
和isChecked()
这样的方法设置或测试状态,并通过样式属性控制文本大小、颜色等等。
Android RadioButton
widget 通过添加更多的功能层来进一步扩展这些功能,允许将多个单选按钮分组到一个逻辑集合中,然后在任何时候只允许设置其中一个按钮。如果您近年来使用过任何其他 UI 工具包、网页或智能手机应用,这听起来应该很熟悉。为了对多个RadioButtons
进行分组,在 XML 布局中,每个都被添加到一个称为RadioGroup
的容器元素中。
就像其他小部件一样,您可以通过android:id
属性给RadioGroup
分配一个 ID,使用该引用作为起点可以利用整个RadioButtons
组上可用的方法。这些方法包括
-
check():通过 ID 检查/设置特定的单选按钮,而不考虑其当前状态
-
clearCheck():清除 RadioGroup 中的所有单选按钮
-
getCheckedRadioButtonId():返回当前选中的 RadioButton 的 Id(如果没有选中 RadioButton,将返回-1)
清单 9-12 显示了带有单选按钮的单选按钮组的布局。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<RadioGroup
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<RadioButton android:id="@+id/radio1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Red" />
<RadioButton android:id="@+id/radio2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Blue" />
<RadioButton android:id="@+id/radio3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Green" />
<RadioButton android:id="@+id/radio4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Yellow" />
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 9-12RadioButton and RadioGroup layout
你也可以在RadioGroup
结构中散布和添加其他部件,这些部件将按照ConstraintLayout
的约束规则或者我们将在下一章讨论的布局的其他放置规则在组内呈现。你可以在图 9-11 中看到行动中的基本RadioButtons
。
图 9-11
RadioGroup 和 RadioButton 的作用
了解更多 UI 小部件
Android 总是有更多的小工具需要学习或掌握,你可以在网站上找到更多的例子,网址是 www.beginningandroid.org
。在这里,您可以找到更多的小部件示例,例如
-
滑块
-
模拟时钟
-
数字显示式时钟
我们还将在第 13 和 14 章中看到更多以音频和视频为中心的小部件。
摘要
在这一章中,你已经了解了许多基本的小部件——或者视图——在构建 Android 用户界面时是可用的。在下一章中,我们将进一步介绍用户界面设计的概念,并介绍布局作为一个容器和框架,用于在活动和其他用户界面中放置、组织和嵌套您的小部件。
十、探索 Android 概念:布局和更多
了解 Android 中可用的各种 UI 小部件无疑使您能够就应用中的特定功能和行为应该如何呈现做出各种设计选择,但是活动设计不仅仅是选择单选按钮或文本视图。布局是 Android 的声明性机制,让你能够控制应用的整个屏幕。
从概念上讲,布局既是您希望在应用或活动中使用的小部件的容器,也是所有小部件应该如何显示、交互和互补的蓝图和脚手架。一旦您构思出使用不止一个或两个小部件的设计,您就会希望布局的力量能够帮助您避免手动控制位置、缓冲区和空白、分组等等的繁琐。
在接下来的章节中,我们将回顾 Android 支持的一些最有用、最流行的布局类型,该网站还包含一些更专业或很少使用的布局的更多示例。
什么是 Android Jetpack
对 Android 设计的任何粗略搜索或回顾都会浮现出一些有趣的里程碑,有些人会说是 Android 近代史上的岔路口。你很可能会看到两个艺术术语从最近几年的任何结果中冒出来。首先,你会发现术语“材质设计”,这是谷歌在 2012 年向世界推出的一种风格和设计理念,它影响了默认调色板、小部件风格和 Android 中的其他设计功能。你会遇到的第二个术语是“Android Jetpack”,毫无疑问,你会发现关于它进行设计的评论——包括布局——“更容易”、“更好”、“新的和不同的”。这些说法是正确的,但它们可能会给新开发人员带来一些困惑。我在用喷气背包吗?我怎么才能得到喷气背包呢?诸如此类。
在 2018 年的谷歌 I/O 开发者大会上,谷歌推出了 Android Jetpack。在头条新闻的背后,事实证明 Jetpack 本身并没有什么“不同”,而是许多不同的 Android UI 框架片段和基础元素的重新打包,以及对 Android 支持库的修改和扩展。Jetpack 不是一种竞争性的建造方式。相反,Jetpack 会过滤你正在做的大部分事情,从引用androidx
名称空间中的任何库,到管理向后兼容性和历史 Android 版本支持。这样,你并没有真正把 Jetpack“添加”到你的设计工作中。相反,当您利用您在构建应用和使用 Android Studio 来帮助您时,它会隐式地、几乎自动地介入。
谷歌在android.com
网站上提供了一个在 Android Jetpack 支持下的快速浏览。Jetpack 旗下的 Android 现有部分分为四个领域:
-
基础组件
-
建筑构件
-
行为成分
-
UI 组件
谷歌提供了如图 10-1 所示的图表(来源:Android-developers . Google blog . com/2018/05/use-Android-Jetpack-to-accelerate-your . html)来演示 Android Jetpack 在这四个领域的具体部分。
图 10-1
了解 Android Jetpack 概念性组件 1
如果你从这一节学到一件事,那就是你不必担心问自己“我在使用 Jetpack 吗?”因为你已经是了!您可能没有使用所有的特性和功能组,但是没有必要这样做。
使用流行的布局设计
我们将在以下部分中介绍以下主要布局容器:
-
constraint layout:Android 中新项目的当前默认设置,也是 Jetpack 的一部分,其中小部件用最小的定位约束集表示,没有(或很少)树形层次结构。
-
RelativeLayout:在 Android 4.0 之后的许多年里一直是默认的,直到 Jetpack 和
ConstraintLayout
的引入,它使用规则引导的方法来自我排列 UI 元素。 -
LinearLayout:最初的默认设置,在许多早期的 Android 应用中普遍使用,它遵循传统的盒子模型,将所有的小部件想象成盒子,以适合彼此。
在这本书的网站上, www.beginningandroid.org
,你还可以找到 Android 提供的其他布局的额外资料,包括
-
TableLayout:一种类似网格的方法,类似于在 web 开发中使用 HTML 表格。
-
GridLayout:看似与
TableLayout
相似的是,GridLayout
使用任意精度的网格线将你的显示器分割成一个个更具体的区域来放置小部件。
在这些主题之后,我们将转向如何从 Java 代码中操作 XML 布局,并着手修改 ButtonExample 应用的版本,以展示如何在代码中查找和使用布局。
Note
虽然我们将在这一章花很多时间来研究 XML 的布局,但是请记住,图形化的布局编辑器有一个设计模式,允许您可视化地添加小部件、放置和排列它们、设置关系等等。虽然您可能总是混合使用手写 XML 和布局编辑器,但是当您开始喜欢一种方法时,不时地跳到“另一种”是值得的。通过观察 Android Studio 在使用图形化布局编辑器时自动生成的 XML 中做了什么,这也是了解更多布局 XML 细微差别的好方法。
重新审视约束布局
在前面的章节中,你已经接触了非常简单的ConstraintLayout
设计,包括你的MyFirstApp
和第九章中的许多小部件示例。虽然你可以继续使用ConstraintLayout
作为小部件的简单容器,为 Android 提供约束设置,以便它可以为你管理布局,但ConstraintLayout
还有更多高级功能,你一定要了解和探索,以将你的活动设计提升到更高的水平。
ConstraintLayout
提供了许多非常令人兴奋的特性,但我将标记出三个最近最有用的特性。
融入潮流
随着 Android 的最新更新,ConstraintLayout 已经扩展为一个可选功能,可以在活动中“流动”屏幕上的小部件,就像将它们倒在屏幕上一样,并在运行时根据给定设备界面的大小和密度包装和移动小部件。
用流的术语来说,你在一个虚拟流布局中将部件集合链接在一起,作为基础ConstraintLayout
的助手。组的流链接在布局 XML 的<androidx.constraintlayout.helper.widget.Flow>
元素中指定,然后两个关键属性控制流链接和行为。
第一个属性是app:constraint_referenced_ids
属性,它采用逗号分隔的小部件 id 字符串,指示 Android 将界面的哪些部分分组到特定的虚拟流布局中。
第二个属性是app:flow_wrapMode
,它采用三个字符串值中的一个来指示 Android 如何管理流组中的小部件流。app:flow_wrapMode
的可能值和相关行为如下:
-
none:默认方法——这为组中的所有小部件创建了一个逻辑链,如果小部件不适合活动的维度,就会溢出它们。
-
链:如果发生溢出,将溢出的小部件添加到后续链中以包含它们。
-
align:大体上类似于链式方法,只是行与列对齐。
熟悉ConstraintLayouts
中的流程选项非常容易。清单 10-1 显示了 Ch10/FlowExample 中FlowExample
项目的相关布局 XML 文件。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flow TextView 1"
android:textSize="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flow TextView 2"
android:textSize="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toEndOf="@+id/text1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.499" />
<TextView
android:id="@+id/text3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Flow TextView 3"
android:textSize="25dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toEndOf="@+id/text2"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.499" />
<!-- Add/remove the androidx.constraintlayout.helper.widget.Flow spec to see Flow in action -->
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:flow_wrapMode="chain"
app:constraint_referenced_ids="text1, text2, text3"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 10-1Setting up Flow in your ConstraintLayout
FlowExample
的布局非常简单。我们有三个TextViews
,用约束条件定义,这样通常你会(试图)先用text1 TextView
布局,然后用右侧的text2 TextView
,再用右侧的text3 TextView``text2
。我特意选择了长文本和大字体来表达我的观点。在布局的底部,您将看到在一个<androidx.constraintlayout.helper.widget.Flow>
元素中定义的流,其中我指定我的flow_wrapMode
来链接,并将组的引用 id 设置为"text1, text2, text3"
–我的 TextView IDs。
如果我省略了流虚拟布局(参见 XML 布局中控制这一点的注释),Android 试图按照正常的ConstraintLayout
规则呈现活动,结果如图 10-2 所示。
图 10-2
如果没有 Flow,ConstraintLayout 会在屏幕外呈现小部件
你马上就能发现问题。我故意使用虚拟屏幕尺寸较小的 AVD。我的text1 TextView
渲染得很好,text2
的一半也上了屏幕。但是text2
的其余部分被切断了,text3
也不见踪影。实际上,它被渲染,但不可见,因为布局没有办法适应屏幕大小,布局缺少流动选项。您可以尝试在各种不同屏幕大小的 avd 上运行移除了 Flow 元素的示例,以查看小部件是如何以及在哪里被截断或不显示的。
有了 Flow 元素之后,重新运行FlowExample
应用就可以将 Flow 的功能展现出来。图 10-3 显示了同样的三个TextViews
,但是这一次使用了流动虚拟布局功能,Android 已经能够遵循我指定的链规则,并且将小部件流动到新的行,以显示我的布局规范中的所有内容。
图 10-3
使用 Flow,ConstraintLayout 渲染所有
心流是一个超级容易掌握的新特性。您可以使用 FlowExample 示例代码,开始添加更多的小部件,扩展流引用的 id 集,甚至更改 flow_wrapMode,以查看流的行为。
用层分层
将调整小部件和视图的能力向前推进一步的是层。当涉及到界面设计时,名称层是非常重载的,所以非常清楚,一个层并不直接布局小部件或者帮助构建连续 UI 组件集的“栈”。一个ConstraintLayout
可以用一个层来扩展,给你一个单一的方法,用一个机制来旋转、平移和缩放一组小部件和视图。
例如,您可能正在创建一个图形图块游戏,并希望降低用户当前未选择的任何图片图块的重要性。有了层,所有其他的ImageView
小部件都可以通过将它们添加到一个层来应用相同的布局更改,然后所需的转换可以被应用到该层一次,它反过来将应用到它的组成小部件。
随运动而动
Android 最新版本的一个广为人知的特性是被称为 MotionLayout 的ConstraintLayout
扩展。通过定义和使用ConstraintSets
,你可以使用MotionLayout
拍摄各种无聊的静态视图,并构建动画变化,如旋转、淡入淡出、大小变化等等。本质上,描述小部件和视图如何关联和定位的约束本身可以被视为控制运动和流动的变量。
手工构建基于MotionLayout
的应用和它们使用的各种ConstraintSet
配置可能非常繁琐。考虑到这种单调乏味,谷歌在 Android Studio 中引入了运动编辑器,为您提供了一个动画画布,用于构建引人注目的动画布局。
为了开始在动作编辑器中使用动作和MotionLayout
设计,Android 为ConstraintLayout
引入了androidx.constraintlayout.motion.widget.MotionLayout
变体。MotionLayout 仍处于早期阶段,需要相当多的粗糙边缘和手动步骤来设置,并且随着 Android Studio 的单点发布而频繁更改。带有运动效果的布局也不太适合在像这样的静态书籍中呈现。
因此,为了确保您可以获得MotionLayout
的最新演示,并且您可以在 MotionLayout 中看到选项如何在动态、移动的布局中发挥作用,可以从位于 www.beginningandroid.org
的网站上获得演示和MotionExample
演示。
使用相对布局
RelativeLayout
是 Android 的长期默认设置,现在仍然是活动和片段设计非常流行的选择。正如术语“Relative”所暗示的,一个RelativeLayout
使用部件和父活动之间的关系来控制部件的布局。相对性的概念很容易理解,例如,您可以指定一个小部件放置在相对于相对位置的另一个小部件的下面,或者让它的上边缘与相关的小部件对齐,等等。
所有关系设置都利用一组分配给布局 XML 文件中的小部件 XML 定义的标准化属性。
相对于父容器放置小部件
理解RelativeLayout
的一个很好的起点是探索允许您相对于父窗口定位小部件的属性。有一组核心属性可用于基于父对象(例如,活动)及其顶边、底边、边等来定位位置。这些属性包括
-
android:layout_alignParentTop:将小部件的上边缘与容器的顶部对齐。
-
Android:layout _ alignParentBottom:将小部件的下边缘与容器的底部对齐。
-
Android:layout _ alignParentStart:将小部件的开始端与容器的左侧对齐,在考虑从右到左和从左到右书写的脚本时使用。例如,在美国英语从左到右布局中,这将控制小部件的左侧。
-
android:layout_alignParentEnd:将小部件的末端与容器的左侧对齐,在考虑从右向左和从左向右书写的脚本时使用。
-
Android:layout _ center horizontal:将小部件水平放置在容器的中心。
-
android:layout_centerVertical:将小部件垂直放置在容器的中心。如果你想要水平和垂直居中,你可以使用组合的 layout_centerInParent。
在确定小部件边缘的最终位置时,会考虑各种其他属性,包括填充和边距宽度。如果您深入研究像素级精确的相对定位,请注意考虑到这些因素。
用 id 控制相对布局属性
为了RelativeLayout
的目的正确引用小部件的关键是使用被引用的小部件的标识,这您已经遇到过:这是正在讨论的小部件的android:id
标识符。例如,在前面章节的ButtonExample
项目中,按钮有标识符@+id/button
。
为了进一步控制小部件的布局并描述它们相对于布局中其他小部件的位置,您需要在布局容器中提供小部件的身份。这是通过在您想要引用的任何小部件上使用android:id
标识符属性来完成的。
第一次引用android:id
值时,确保使用加号修饰符(例如@+id/button
)。对同一标识符(小部件)的任何进一步引用都可以省略加号。在对标识符的第一次引用中使用加号有助于 Android 林挺工具检测标识符不匹配,即您在布局文件中没有正确命名小部件。这相当于在使用变量之前声明变量。
有了 id,我们前面的例子@+id/button
现在可以被另一个小部件引用,比如另一个按钮button2
,通过在它自己的布局相关属性中引用 id 字符串的机制。
注意像button
、button1
和button2
这样简单的名字对于这样的例子来说是不错的,但是你会非常希望在你的应用中使用有意义的小部件标识符和名字。
相对定位属性
现在您已经对标识符的机制有了很好的理解,您可以使用这六个属性来控制小部件之间的相对位置:
-
Android:layout _ over:用于将 UI 小部件放置在属性中引用的小部件之上
-
android:layout_below:用于在属性中引用的小部件下面放置一个 UI 小部件
-
android:layout_toStartOf:用于指示该小部件的结束边缘应该放置在属性中引用的小部件的开始边缘
-
android:layout_toEndOf:用于指示这个小部件的起始边缘应该放在属性中引用的小部件的结束边缘
-
android:layout_toLeftOf:用于将 UI 小部件放置在属性中引用的小部件的左侧
-
android:layout_toRightOf:用于将 UI 小部件放在属性中引用的小部件的右边
更微妙的是,您还可以使用许多其他属性中的一个来控制一个小部件相对于另一个小部件的对齐。这些属性包括
-
android:layout_alignStart:标记小部件的起始边缘应该与属性中引用的小部件的起始对齐
-
android:layout_alignEnd:标记小部件的结束边缘应该与属性中引用的小部件的结尾对齐
-
android:layout_alignBaseline:标记两个小部件的任何文本的基线,无论边框或填充如何,都应该对齐(参见下面的内容)
-
android:layout_alignTop:标记小部件的顶部应该与属性中引用的小部件的顶部对齐
-
android:layout_alignBottom:标记小部件的底部应该与属性中引用的小部件的底部对齐
使用相对布局示例
您已经对 RelativeLayout 的能力和行为有了足够的了解,可以深入研究一个工作示例了。清单 10-2 使用来自ch10/RelativeLayoutExample
项目的 RelativeLayout 提供了布局 XML。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="URL:"
android:layout_alignBaseline="@+id/entry"
android:layout_alignParentLeft="true"/>
<EditText
android:id="@id/entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/label"
android:layout_alignParentTop="true"/>
<Button
android:id="@+id/ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/entry"
android:layout_alignRight="@id/entry"
android:text="OK" />
<Button
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@id/ok"
android:layout_alignTop="@id/ok"
android:text="Cancel" />
</RelativeLayout>
Listing 10-2The XML layout for the RelativeLayoutExample application
在这个RelativeLayoutExample
布局中,我们基于您在之前章节中的学习,提供更丰富的理解。首先,您会看到根元素是<RelativeLayout>
,这是该活动将使用RelativeLayout
方法进行布局和放置的决定性因素。除了普通的样板 XML 名称空间属性之外,还引入了另外三个属性。
为了确保RelativeLayout
跨越正在使用的任何尺寸屏幕上可用的整个宽度,使用了android:layout_width="match_parent"
属性。我们可以对高度做同样的事情,但是我们也可以告诉RelativeLayout
只使用包含所包含的小部件所需的垂直空间——因此使用了android:layout_height="wrap_content"
属性。
依靠我们在第九章中介绍的小部件,我们为我们的活动引入了四个部件:一个TextView
作为我们的标签,一个EditText
作为我们的可编辑字段,以及按钮OK
和Cancel
。
对于TextView
标签,布局指示 Android 使用android:layout_alignParentLeft="true"
将其左边缘与父标签RelativeLayout
的左边缘对齐。我们还想让TextView
在引入相邻的EditText
后自动管理它的基线,所以我们使用android:layout_alignBaseline="@+id/entry"
调用像素推进完美。注意,我们引入了带加号的 id,因为我们还没有描述EditText
,所以我们需要预先警告它即将存在。
对于EditText
,我们希望它位于标签的右边,它本身位于布局的顶部,占据TextView
右边的所有剩余空间。我们使用android:layout_toRightOf="@id/label"
指示它向右布局(已经介绍过了,所以不需要添加加号)。我们使用android:layout_alignParentTop="true"
迫使EditText
在 RelativeLayout 的剩余空间中坐得尽可能高,并使用android:layout_width="match_parent"
将画布上的剩余空间移到TextView
的右侧。这是可行的,因为我们知道我们也要求父级使用最大可用剩余空间作为宽度。
最后,我们将两个按钮的位置与前面介绍的小部件联系起来。我们希望将OK
放置在EditText
的下方,并与其右侧对齐,因此给它赋予属性android:layout_below="@id/entry"
和android:layout_alignRight="@id/entry"
。然后我们告诉 Android 使用android:layout_toLeft="@id/ok"
和android:layout_alignTop="@id/ok"
将Cancel
按钮放置在OK
按钮的右边,按钮顶部对齐。
图 10-4 显示了在我们对一个普通的新的空活动项目做了这些布局更改之后,我们的布局的运行情况。
图 10-4
行动中的相对布局
相对布局中的重叠部件
Android 支持的每种布局都为你提供了专门和独特的功能,并且RelativeLayout
配备了一些关键功能,包括让小部件相互重叠或看起来好像一个在另一个前面或重叠的能力。这是通过 Android 跟踪来自 XML 定义的子元素并为布局中的每个子元素应用一个层来实现的。这意味着,如果稍后在布局 XML 文件中定义的项目使用 UI 中的相同空间,它们将位于较旧/较早的项目之上。
没有什么比看到它的运行更能理解它是什么样子的,以及它对您的应用设计有何帮助。图 10-5 显示了一个有两个按钮声明的布局,第二个按钮位于第一个按钮的前面或上方。
图 10-5
RelativeLayout 在操作中的重叠特征
就您可能想要覆盖的部件类型和数量而言,天空是一个极限。您可能还想知道为什么有人会想以这种方式覆盖项目。除了针对一些古怪的布局想法,与RelativeLayout
重叠的可能性意味着有可能在屏幕上有小部件但看不见,仍然有助于活动行为。
用线性布局排列
有时候在设计布局时,你不需要或不想要复杂的受约束的或相对的小部件定位,相反,你只想依靠一些古老的经过测试的布局方法。任何熟悉图形设计或界面设计历史的人都会听说过盒子模型,在这个模型中,组成界面的所有项目(或小部件)都被认为是分成行和列的元素。LinearLayout
遵循这种模式,是早期 Android 版本使用的原始默认设置。它可能不再是当今的布局,但是你可以一直依赖LinearLayout
作为一个简单的选项来调整你的小部件如何装箱、嵌套等等。LinearLayout
缺少的是我们在 ConstraintLayout 和 RelativeLayout 中谈到的一些聪明、省时或有用的功能,但有时简单的解决方案才是真正需要的。
掌握 LinearLayout 的五个主要限定符
当使用 LinearLayout 时,一组五个关键属性帮助您控制整个布局和任何包含的小部件的放置和外观的几乎所有方面:
-
方向:对
LinearLayout
的第一个也是最基本的控制是确定盒子模型是否应该被认为是水平、逐行或垂直、逐列填充的。这被称为LinearLayout
的方向,由 android:orientation 属性控制,并接受字符串值HORIZONTAL
或VERTICAL
。也可以在运行时从 Java 代码中设置方向,使用类似参数的setOrientation()
方法。 -
边距:默认情况下,你放置的任何小部件和相邻的小部件之间都没有缓冲区或间距。Margin 允许您控制这一点,并使用属性(如
android:layout_margin
)添加缓冲区(或边距,顾名思义),这将影响小部件的所有边,或者使用单侧属性(如android:layout_marginTop
)添加缓冲区。任何边距属性都以像素大小作为值,例如 10 度。这在概念上类似于RelativeLayout
的填充,但只适用于小部件有不透明背景的时候(比如在按钮上)。 -
填充方法:我们已经为
RelativeLayout
引入了wrap_content
和match_parent
的概念。在LinearLayout
方法中,所有小部件都必须通过属性android:layout_width
和android:layout_height
指定填充方法。这些可以取三个值中的一个:wrap_content
,按照您之前的理解,它指示小部件仅根据需要容纳文本或图像内容;match_parent
,取父节点可用空间的最大值;或者以与设备无关的像素测量的特定像素值,例如 125 度倾斜。 -
权重:当一个
LinearLayout
中的两个或两个以上的 widgets 都指定了match_parent
时,哪一个胜出?他们怎样才能最大限度地利用父母提供的空间?答案是android:layout_weight
属性,它允许您对每个小部件应用一个简单的数值,例如 1、2、3、4 等等。Android 将对LinearLayout
中的所有权重求和,然后根据布局中所有小部件的总权重,为小部件提供 UI 空间。因此,例如,如果我们有两个都配置为match_parent
的TextViews
,第一个TextView
的android:layout_weight
为 5,第二个TextView
为 10,那么第一个按钮将获得三分之一的可用空间(5/(5+10)),第二个按钮将获得三分之二的可用空间。还有其他一些设置权重的方法,但没有一种像android:layout_weight
方法那样直观。 -
重力:当使用
LinearLayout
时,Android 采用的默认布局方法是,如果使用HORIZONTAL
方向,则从左侧对齐所有部件;如果使用VERTICAL
方向,则从顶部对齐所有部件。有时,您可能希望覆盖此行为,以强制您的布局自下而上或向右倾斜。XML 属性android:layout_gravity
允许您在运行时控制行为,方法setGravity()
也是如此。android:layout_gravity
的VERTICAL
方向的可接受值为left
、center_horizontal
和right
。对于HORIZONTAL
方向,Android 将默认与你的小工具的文本的不可见底部对齐。使用值center_vertical
让 Android 使用小部件的假想中心。
线性布局示例
首先回顾一下LinearLayouts
的理论有点让人不知所措,因此开始探索一些概念有助于直观地掌握如何、何时以及在哪里使用关键的LinearLayout
杠杆是有意义的。
从纯理论的角度来看,所有的选择都令人望而生畏。清单 10-3 中的动态例子显示了这些选项中有多少是有效的。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<RadioGroup
android:id="@+id/orientation"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dip">
<RadioButton android:id="@+id/horizontal"
android:text="horizontal"/>
<RadioButton android:id="@+id/vertical"
android:text="vertical"/>
</RadioGroup>
<RadioGroup
android:id="@+id/gravity"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="5dip">
<RadioButton android:id="@+id/left"
android:text="left"/>
<RadioButton android:id="@+id/center"
android:text="center"/>
<RadioButton android:id="@+id/right"
android:text="right"/>
</RadioGroup>
</LinearLayout>
Listing 10-3Demonstrating the options provided by LinearLayout
这个例子使用了两个简单的构件来帮助我们理解方向和重力。我定义了两个独立的 RadioGroup 小部件,第一个小部件有一组RadioButtons
来控制方向,另一个小部件有一组RadioButtons
来控制重力。为了给我们一个起点,我们通过选择android:orientation="vertical"
来使用垂直方向的 XML 定义,这将把RadioGroups
一个放在另一个上面,并且将RadioButtons
也以垂直方式放在其中。
然后我覆盖了初始RadioGroup
中两个单选按钮的垂直堆叠,使用android:orientation="horizontal"
将它们切换到水平布局。虽然我想从父RadioGroup
继承其他属性,但我灵活地使用了覆盖子RadioButtons
中特定属性的能力。最后,我们在 5 dip 的所有小部件周围设置填充,并设置wrap_content
选项。
在没有支持 Java 逻辑的情况下运行这个示例代码——只有布局——显示了这些初始设置的运行情况,尽管还没有其他行为来改变您所看到的内容,但稍后会有其他行为。布局如图 10-6 所示。
图 10-6
设置方向和重力示例
很高兴看到呈现的布局,但实际上最好编写一些 Java 逻辑来根据单选按钮的设置改变方向和重心。这正是清单 10-4 显示的内容。
package org.beginningandroid.linearlayoutexample;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Gravity;
import android.widget.LinearLayout;
import android.widget.RadioGroup;
public class MainActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener {
RadioGroup orientation;
RadioGroup gravity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
orientation=(RadioGroup)findViewById(R.id.orientation);
orientation.setOnCheckedChangeListener(this);
gravity=(RadioGroup)findViewById(R.id.gravity);
gravity.setOnCheckedChangeListener(this);
}
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.horizontal:
orientation.setOrientation(LinearLayout.HORIZONTAL);
break;
case R.id.vertical:
orientation.setOrientation(LinearLayout.VERTICAL);
break;
case R.id.left:
gravity.setGravity(Gravity.LEFT);
break;
case R.id.center:
gravity.setGravity(Gravity.CENTER_HORIZONTAL);
break;
case R.id.right:
gravity.setGravity(Gravity.RIGHT);
break;
}
}
}
Listing 10-4Java logic to implement orientation and gravity changes based on UI selection
理解 Java 代码在做什么非常容易。首先,在应用的onCreate()
调用过程中,我们使用我们在活动中实现的OnCheckedChangeListener
的setOnCheckedChangedListener().
为任一RadioGroup
上的点击注册监听器,这意味着活动本身成为监听器。
当您单击任何一个RadioButtons
时,监听器触发回调onCheckChanged()
。在回调方法的定义中,我们确定了五个被渲染的图片中哪个RadioButton
被点击了。一旦我们知道是哪一个RadioButton
,我们的逻辑就调用setOrientation()
从垂直布局切换到水平布局,或者调用setGravity()
到相关的左、右或重心值。六种可能结果中的两种如图 10-7 和 10-8 所示。
图 10-8
另一个方向和重力的例子
图 10-7
向上、向下和周围的方向和重力
更多布局选项
Android 提供了一系列进一步的布局类型,其中许多随着 Android 的相继发布而受欢迎程度有升有降。以下是需要了解的主要布局,这些布局的更多示例可从网站 www.beginningandroid.org
获得。
表格布局
早在互联网的早期,网页设计者经常使用简陋的 HTML 表格在页面上放置内容。虽然网页设计已经向前迈进了一大步,但基于表格的布局概念仍然有用,Android 在TableLayout
容器中采用了它们。
就像 HTML 一样,Android 中的TableLayout
使用起来很快,并且依赖于这样一个概念,即TableLayout
有TableRow
子元素来控制大小、位置等等。与旧的网页 HTML 方法的另一个相似之处是高保真精度很难达到完美。
网格布局
如果TableLayout
的想法让你的想象力飞速发展,那么你会爱上GridLayout
。作为TableLayout
的近亲,GridLayout
将它的子部件和 UI 元素放在一个由无限细节线条组成的网格上,这些线条将你的活动所渲染的区域分隔成单元格。GridLayout's
精确控制的秘密在于细胞的数量,或用来描述细胞的网格线,没有限制或阈值。只需添加更多的网格线,将您的 UI 分割成更精细的子单元,以放置小部件。
用 Java 逻辑掌握基于 XML 的布局:两全其美!
随着您对 XML 布局和 Java 管理的布局越来越熟悉,您最终会意识到您可以两全其美,并且您可以随时改变主意。您可能更喜欢用 XML 构建原型并使用图形布局编辑器,或者您可能更喜欢使用 Java 尝试真正细微的运行时布局选择。无论采用哪种方法,您都可以随时通过一点代码或调整后的 XML 来改变主意。举例来说,清单 10-5 列出了来自您的ButtonExample
应用的计数按钮代码,转换成一个 XML 布局文件。您可以在Ch10/ButtonAgain
示例项目中找到这段代码。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/button"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 10-5Flexing XML layouts and controlling them with Java logic
在清单 10-5 中,您应该可以看到我们为 ButtonExample 应用组合在一起的 XML 等价物,包括以下内容:
-
将 ConstraintLayout 设置为根元素
-
定义作为点击和计数器显示的按钮,并分配一个 android:id,这样我们就可以从代码中引用它,并在 Java 中执行我们的点击计数
-
Android:layout _ alignParentBottom、Android:layout _ alignParentTop、Android:layout _ alignParentLeft 和 Android:layout _ alignParentRight,每个都有助于按钮的“父”对齐
-
android:layout_width 和 android:layout_heigh,设置按钮消耗整个屏幕,就像我们对 ButtonExample 所做的那样
-
android:text,显示在按钮上的文本,最初是空字符串
虽然这是一个转换成 XML 的非常简单的例子,但是核心概念是重要的,并且它们保持不变。更复杂的例子会有多个用 XML 表示的小部件、子布局和分支来控制复杂性。由于我倾向于基于 XML 的布局,在本书的后半部分,您将会看到更多这样的内容。
用 Java 代码连接 XML 布局定义
当您采用 XML 布局方法并开始完善小部件定位等功能时,一个非常重要的问题出现了:即使我们的活动只有一种布局,如何从 Java 逻辑中决定使用哪种布局?该解决方案基于一个专门为此设计的方法调用,通常在活动的onCreate()
回调中调用,以实现 Java 和 XML 的完美结合:方法setContentView()
。
ButtonExample
应用的 redux 在res/layout/activity_main.xml
中的正常默认位置有它的布局,尽管这种技术不管活动 XML 文件的定制命名如何都能工作。要完成从代码到布局的连接,像这样调用setContentView()
:
setContentView(R.layout.activity_main);
如果你看一下ButtonExample
的原始版本,你会注意到setContentView()
也被调用过。那么,什么发生了变化?在ButtonAgain
中,我们传递一个对我们定义的基于 XML 的视图的引用,这个视图基于 Android Studio 内置的 AAPT 实用程序,它已经解析了您的 XML 并为您生成了R
Java 类。您从R
类中受益,因为它能够对 XML 布局和其中的部分进行简单的代码引用。不管你有多少布局,也不管它们有多复杂,AAPT 都会把它们打包成一个综合 Java 类,并在R.layout
命名空间中提供。使用调用约定R.layout.<your_layout_file_name_without_the_XML_extention>
引用任何布局。
为了在由setContentView()
返回的布局中找到小部件,您调用findViewById()
方法并向其传递小部件的数字引用。再读一遍这句话,你会发现“抓住你了”我说的数字引用是什么意思?还没有 XML 声明是小部件的数字标识符或引用。这是一个容易解决的谜。
在打包时,AAPT 会识别你布局中的每个小部件,给它们分配一个 ID 号,并把它作为成员数据包含在R.java
文件中。您可以随时打开R.java
文件进行检查。但是不要试图用这种方式去记忆东西。你可以让 Android 使用R.id.<widget_android:id_value>
参数来解析你想要引用的任何小部件的 id 号。您可以使用它来解析从基本视图类派生的任何小部件的 ID(这几乎是一切)。
随着机制的消失,你应该能够看到 AAPT 根据你的布局和setContentView()
和findViewById()
方法把东西整齐地打包成R.java
的组合所带来的巨大力量。例如,不同的活动可以被传递不同的View
实例,更有趣的是,您可以基于一些程序逻辑来改变View
,例如,当您检测到不同类型的设备时,您可以使用不同的布局。
再次通过按钮访问 MyFirstApp
在最初的ButtonExample
例子中,按钮的显示屏会显示按钮被按下的次数。当按钮通过onCreate()
加载时,计数器从 1 开始计数。现有的大部分逻辑,比如计算点击次数的代码,在我们修改过的ButtonAgain
版本中仍然有效。与ButtonExample
代码的主要区别如清单 10-6 所示,其中我们用ButtonAgain
app XML ConstraintLayout
布局中的定义替换了活动的onCreate()
方法中之前的 Java 调用。
package org.beginningandroid.buttonagain;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.app.Activity;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
Button myButton;
Integer myInt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myButton=(Button)findViewById(R.id.button);
myButton.setOnClickListener(this);
myInt = 0;
updateClickCounter();
}
public void onClick(View view) {
updateClickCounter();
}
private void updateClickCounter() {
myInt++;
myButton.setText(myInt.toString());
}
}
Listing 10-6ButtonAgain Java code, linking seamlessly to the layout XML
通过回顾onCreate()
方法,您现在可以清楚地看到变化。首先,我们使用setContentView()
为我们指定的 XML 布局自动生成R
Java 类。然后我们使用findViewById()
方法,要求它找到具有android:id
值为“button
”的小部件。我们将返回以编程方式驱动按钮行为所需的引用,包括更改其标签以反映我们计算的点击计数器值。
将所有的代码和 XML 放在一起会产生一个ButtonAgain
应用,其外观和行为与 ButtonExample 应用惊人地相似,如图 10-9 所示。
图 10-9
ButtonAgain 应用,完美地结合了 Java 逻辑和 XML 布局
摘要
浏览了布局和它们的一些特性之后,你现在应该可以想象一下从哪里开始使用本章中的一些容器和布局样式了。还可以玩“猜猜你喜欢的 app 用的是什么布局风格”的游戏。这并不是我们布局之旅的结束,我们将在本书的剩余部分重新审视更多的概念。
十一、理解活动
到目前为止,您对 activities 的介绍主要集中在使用它们作为学习 UI 小部件和布局的工具,知道它们在计算上是“廉价的”,是 Android UIs 的基本构建块,并且设计为您可以在您的应用中尽可能多地制作和使用,完全知道 Android 操作系统会愉快地回收资源并保持您的活动可管理。
理论上说得很好,但实践呢?在这一章中,我们将深入研究 Android“如何”在活动生命周期中管理活动,尝试活动生命的各个阶段,然后通过介绍活动的犯罪伙伴 fragments 来扩展您构建引人注目的用户界面的基线。您可以将片段视为一种合成技术,用于决定何时以及如何为更大的屏幕或维度极端的屏幕利用活动和活动组件的不同组合。
深入研究 Android 活动生命周期
到目前为止,本书中的所有例子都使用了一个 activity,尽管您已经阅读了多次,因为您的应用可以有任意多个 activity。不管您有多少个活动,每个活动的使用都受生命周期的控制,在生命周期中选择要运行的活动;被创建、使用、暂停和/或恢复;并最终被停止和处理。用简单的英语描述生命周期的各个阶段有助于你理解正在发生的事情,让我们看看生命周期状态的实际技术方面和 Android 用来触发状态转换的回调方法。图 11-1 展示了这些生命周期状态和伴随的回调方法的全貌。
图 11-1
使用回调转换方法的 Android 活动生命周期
应用通常以四种主要状态之一存在:
-
已启动:某个动作(通常是用户触发的动作)指示 Android 操作系统运行该应用时,该应用的初始状态。
-
运行中:用户第一次看到你的应用并与之交互的时刻(和正在进行的状态)。一般来说,这是在发射后已经进行了一系列准备步骤之后。
-
killed:Android 操作系统收到不再需要该应用的通知后,该应用进入的状态,原因可能是用户关闭了该应用,或者是发生了一些资源引发的收割。
-
关闭(以及任何不处于启动、运行或终止状态的应用):从操作系统内存中清除所有持久数据、视图层次结构、缓存数据等的最终状态。
这些状态中的每一个都是不言自明的。在生命周期回调方法的领域中,让应用在不同状态之间移动的是乐趣所在。
了解活动生命周期回调方法
图 11-1 中显示的每一种生命周期方法都有你应该熟悉的特定行为和用途。您可能不需要突然开始为您的应用的这些生命周期阶段添加定制逻辑,但是知道在哪里添加东西将为您在未来作为 Android 开发者的快速进步做好准备。我们将逐一介绍每种方法,突出它们的具体用途。
每个方法都有与其他方法相同的特征,例如当调用任何回调方法时,几乎普遍使用各自父类的等效方法作为第一个动作。例如,在本书前面的例子中可以看到,onCreate()
方法的第一步是调用super.onCreate()
,它调用父版本。当您不想自动调用父类时,会有例外,但是我会在遇到这些情况时标记它们。现在,假设一个默认的良好行为是遵循“家长呼叫”实践。
onCreate()
每个活动都以onCreate()
方法开始生命。无论您的用户点击了 Android 主屏幕上的图标来启动您的应用,还是配置更改触发了重新创建显示的需要,都会调用onCreate()
。该方法采用一个 Bundle 对象来考虑这些后来的重建情况,因为 Bundle 将存储任何以前的状态、数据和资源,这些都是使活动变得生动所需要的——统称为实例状态。
对于您活动的任何onCreate()
方法,您都应该考虑以下行动:
-
将您计划使用的所有布局加载到内容视图中,以便在调用 onStart()方法时,Android 可以在显示 UI 之前创建它。
-
初始化您在活动类定义中定义的任何活动级变量。
您可能还需要做一些工作来使用您作为应用的一部分创建的全局资源或变量。当我们讨论onRetainNonConfigurationInstance()
时,我们将很快回到全局资源这个主题,并且当我们在本章的后面和本书的剩余部分讨论首选项时,我们也将探索更多的示例使用。
onStart( )
一旦onCreate()
完成了从布局定义构建所有对象的任务,以及任何其他初始化工作,那么onStart()
的工作就开始了。onStart()
的工作是在屏幕上向用户呈现最终的用户界面。在前面的生命周期模型中,onRestart()
之后的路径上也调用了onStart()
方法。
在很多情况下,您需要覆盖onStart()
并提供自己的逻辑。即使您很想这么做,您也需要意识到,如果您的活动遵循onRestart()
路径,实例状态在这一点上仍然没有完全恢复,直到对onRestoreInstanceState()
的后续调用完成。
实现您自己的onStart()
覆盖非常有用的一种情况是,当您有一些长期存在的自定义资源时,您已经根据您的活动完全冻结或暂停了这些资源——做一个好公民,在不必要时不使用资源。如果您选择建立这样的资源管理,那么onStart()
方法将是您恢复或解冻这样的定制资源的地方。确保调用super.onStart()
方法来调用父类的等价方法,即使你知道父类中没有覆盖代码。Android 本身在这种情况下还是需要做好自己的内功。
onRestoreInstanceState()
如果用户结束一个活动,无论是通过使用 back 按钮,还是通过其他路径将他们从您的活动转移到另一个活动,他们都表明他们已经完成了原始的活动以及与它相关的所有状态。这将向作为应用开发人员的您表明,除了您需要维护的任何长期资源之外,您还可以免除该活动。当 Android 本身终止一个活动时,情况通常是不同的。首先,Android 由于配置的改变而终止了一个活动,并且它确实需要关心需要什么来重新创建活动以满足用户的需求。这意味着不仅要重新创建活动的可视显示,还要重新创建它的所有实例状态。
您可能希望在这里执行任何自定义的实例状态恢复,并记住实例状态与长期存储不同。还有其他机制来处理这个问题,包括我们将在本书后面讨论的偏好。如果您希望持久存储比首选项系统所设计的更复杂或更大类型的数据,那么基本的计算概念(如文件和数据库)是最好的方法,我们将在第十八章中介绍这些内容。
onResume( )
对于 Android 用户界面来说,onResume()
方法是关键时刻。onResume()
方法是使完全渲染和膨胀的布局可见的关键方法,将所有累积的对象、逻辑等转移到前台。与onPause()
一起,当其他活动和应用抢占它、跳转到焦点、被用户关闭等等时,到前台和从前台的转换将在您的活动生命周期中触发多次。
您通常应该考虑添加到onResume()
的唯一逻辑与改变您活动的实时视觉方面相关,包括
-
动画
-
录像
-
自定义视图过渡效果
在所有其他方面,您应该避免在onResume()
中使用任何其他定制逻辑。
onpause()
Android 从一开始就被设计为支持同时运行的许多应用,并依赖于应用能够简单方便地暂停其活动,以便为用户决定使用的其他活动释放资源。为了支持这一点,应用将频繁地从前台状态转移到后台状态。在此之前,将调用onPause()
方法。
在onPause()
调用期间要考虑的最重要的任务包括保存尚未放入活动包中的任何状态,以及处理任何自定义动画、视频、音频或逻辑的其他实时方面。此外,onPause()
调用还充当了一个阈值,超过这个阈值,Android 可以单方面终止您的活动并收回其资源。这意味着一旦onPause()
调用退出,就不能保证您的流程和活动会再次接收到事件——所以保存您需要的东西,不要假设您的活动状态在没有您干预的情况下会持续下去。
onStop()
如果onStart()
管理移动到前台的活动,你可以把onStop()
想成相反的角色:负责把你的活动转移到后台。在onStop()
完成之后,您的活动不再可见,尽管相关的视图层次结构和属性保持不变。
在 Android 积极的资源管理方法下,你应该永远记住onStop()
可能永远不会为你的活动而被调用,所以在这里依赖自定义逻辑是不明智的。您可能会考虑覆盖默认设置的一个领域是与 Android 服务的任何交互,在这种情况下通常不需要前台交互。
onSaveInstanceState()
方法的名称和它在图 11-1 中的生命周期图中的位置都很好地说明了它的用途。这个方法的作用是为任何将要被onDestroy()
方法销毁的活动保存活动状态,从而避免用户发现重要的状态已经丢失而没有意识到他们的活动可能已经结束并被重新创建的情况。这在任何配置更改事件下都会频繁发生,因此用户的体验需要使其透明和自动化。
Note
现在有一种趋势,开始支持ViewModel
的概念,并使用该技术来保存任何瞬态。但是,仍然完全支持onSaveInstanceState()
方法。你可以在developer.android.com
了解更多关于 ViewModel 的信息。
onRestart( )
每次活动从停止状态转移到开始状态时,都会调用onRestart()
方法。该方法为您提供了灵活性,使您能够以不同于那些已停止且现在已重新启动的活动的方式来处理新启动的活动版本,例如在重新启动的情况下保留视图层次结构,您可以利用该层次结构来加速某些初始化活动。
onRetainNonConfigurationInstance()
对于一个方法来说,这是一个很长很奇怪的名字,仔细观察生命周期图会发现它也不存在!这是因为onRetainNonConfigurationInstance()
与活动的生命周期没有严格的联系。相反,它的目的是提供回调机制,以便在 Android 系统经历配置更改时调用。
“什么是配置变更?”你可能会问。有几个突出的动作,Android 认为整个设备的配置发生了变化,所有运行的应用都必须得到通知,并执行任何必要的步骤来适应这种变化。这些配置更改操作包括
-
将设备从横向旋转到纵向,反之亦然
-
将设备连接到 USB 电源
-
将设备停靠在底座中
-
添加或删除 SD 风格的存储设备
-
更改 Android 操作系统的输入或显示语言
您的活动可能需要处理部分或全部这些事件。最明显的例子是需要根据设备的新方向重新绘制(或者严格地说,重新创建)布局。在这些情况下,Android 将保留活动前一个实例的所有资源,调用getLastNonConfiguationInstance()
companion 方法将返回对这些资源的引用,使您能够执行您可能需要的任何进一步的更改处理。
随着无头片段的出现,这种方法在当代应用开发中不太常见。我们将在下一章 12 中讲述片段。
onDestroy()
我们已经到达了生命周期方法的终点,确切地说是onDestroy()
方法,它本身就是关于结束活动的。我们知道,活动是廉价而丰富的,在构建应用时,只要需要,就应该使用然后丢弃。您可以考虑添加到onDestroy()
覆盖中的核心逻辑是任何以活动为中心的清理。因为不能保证onDestroy()
会被你的活动调用,所以你不应该依赖它,也不要期望与其他资源或服务交互。
理解活动生命周期的目标
对于新开发人员来说,掌握回调的整体概念对于更广泛地掌握消息和基于事件的开发至关重要。然而,如果你只把你的 Android 应用看作是在特定状态下的活动,等待回调信号时跳转,你就错过了一些更大的想法。
总的来说,廉价、易操作的活动和回调方法的简洁打包(这意味着每个活动都有自己的从“这里”到“那里”的说明)的最大目标是向用户呈现一个无缝的应用,它可以响应用户的输入和需求。这种以用户为中心的视图对于提供一致的体验是至关重要的,因为你的用户会倾向于把你的应用看作是屏幕的集合或层次结构,而不是那些容易创建和经常破坏的活动的松散联系。
当您开始用越来越多的活动编写应用时,记住以下原则会对您有好处:
-
活动要有重点。你创建的每个活动都应该有助于服务于一个单一的(但不一定是简单的)目的或结果,而不是一个不相关的选项、行动和结果的总称。
-
保存状态,然后再次保存。用户是不可预测的,他们在使用你的应用时会做最糟糕的事情——从关闭手机到反复切换你的应用等等。为了让您的用户相信您的应用可以直观地处理任何意外情况,经常保存状态是很重要的。把它想象成一个既便宜又有效的天然安全网。
-
给每个视图一个 ID 。每个小部件、每个视图和每个视图组都应该用 android:id 属性进行注释。这有助于您自助,因为当每个视图都有一个 ID 时,保存实例状态变得非常容易。
-
应用和活动可以消失。有些情况超出了您的控制范围,特别是无论您的应用多么高效和轻量级,您的用户都决定将它与其他占用大量内存和资源的应用一起安装。如果用户在这样的环境下暂停你的应用,Android 操作系统可能会关闭你正在运行的应用,以释放急需的内存。
管理活动配置更改
通过本章前面的描述,我强调了正常的活动生命周期至少有一个例外,即活动配置变更。配置更改通常被定义为影响整个应用、Android 用户空间或整个 Android 设备的事件。这可能会让你认为我在谈论地震事件——也许是一场 Android 地震。现实要稍微平凡一些。归类为配置更改的事件种类包括更改设备的界面语言、将设备插入电源、与另一个设备配对,或者只是将设备从纵向旋转到横向,反之亦然。
使用配置更改的默认方法
首先,好消息是:Android 为你跟踪设备上发生的所有配置变化,通过使用通知回调来通知所有正在运行的应用特定的变化。除了这个基本通知,Android 还会尽最大努力避免让您承担过多的工作来确定配置更改应该如何在您的应用中体现出来。Android 通过利用程序中的现有结构来做到这一点。
Android 为您采取的最有价值的行动之一是监控已经发生的任何配置更改,并参考您为应用创建的所有可用布局,并执行所有需要的布局工作,以使用与配置更改结果匹配的布局来重新创建您的活动,无论是将旋转匹配到相关的纵向/横向布局,还是将语言从左到右改为从右到左,等等。
布局行为和娱乐已经为您完成,剩下的只需要照顾您为给定活动创建或获得的任何资源。作为配置更改的一部分,您需要保存和恢复这些活动,因为 Android 会破坏之前的活动并重新创建它。您的活动有机会干净地保存其当前状态和资源,以便在重新创建时,您的新活动可以恢复它们,将活动状态恢复到破坏前的状态。以下回调涵盖了允许您跨配置更改保存和恢复状态和资源的流程:
-
onSaveInstanceState():
当触发任何配置更改时,在 Android 销毁当前活动之前,会立即调用onSaveInstanceState()
。这是保存任何临时或正在使用的资源或您需要保留的数据的时间。创建并使用一个捆绑对象来保存您希望保留的所有项目,以便在后续的活动重新创建中使用。如果你回头看看整本书已经展示过的例子,你可以看到在onCreate()
时间使用 Bundle 对象的地方(稍后会有更多)。onSaveInstanceState()
超类祖先还在幕后为您执行一系列非常有用的工作,包括保存任何对象的视图状态——小部件、UI 元素等等——这些对象已经用一个 ID 进行了声明,可以在需要时重新创建。 -
当一个活动开始活动的时候,它或者被重新创建,或者在我们当前的讨论中,它被传递给所需的 Bundle 对象,以便保存的资源和其他数据可以被恢复。除了确保调用父类
super.onCreate()
方法,您不需要做任何更复杂的事情。您可以随时选择添加自定义状态并在onSaveInstanceState()
期间保存它——然后您需要将您的自定义恢复逻辑也提供给onCreate()
,或者提供给在(重新)创建活动时调用的下一个方法onRestoreInstanceState()
。 -
您的 Bundle 对象也被传递给这个方法,此时您可以有选择地检索和恢复您想要的任何进一步的资源。等到活动生命周期中的这一点的一个好处是,您可以确保活动的布局已经展开,内容视图已经设置,这样活动的所有可视方面都已就位。
摘要
现在,您对活动生命周期和回调方法的重要性有了一个坚实的基础,您可以在需要时覆盖这些方法。当我们介绍片段时,我们将在第十二章继续讨论生命周期,然后将展示一个合并的示例应用,展示在现实生活中如何以及在哪里使用生命周期回调方法覆盖。
十二、片段简介
到目前为止,在我们的 Android 布局基础之旅中,我们已经介绍了基本的基于视图的 UI 小部件,并介绍了活动及其生命周期。多年来,这就是在 Android 世界中创建应用的全部内容——设计您的活动,为您的应用添加您想要的逻辑,并在用户浏览您的应用的功能时创建和处理由此产生的膨胀(或呈现)的活动。
2011 年左右,事情爆炸了。或者,更具体地说,随着平板电脑及其更大显示屏的出现和普及,屏幕尺寸出现了爆炸式增长。快进几年,我们已经有了汽车上的平板显示器、巨型电视屏幕显示器等等,所有这些都是由 Android 驱动的。最大限度地利用突然出现的屏幕空间,促使谷歌推出了 Android UI 世界自诞生以来最大的变化之一:片段。
碎片解决了很多问题。从确保没有大面积的空间浪费到克服看起来很糟糕的暴力缩放技巧,片段为您提供了从多组相关的小部件组合 ui 的机制,然后在活动中灵活地显示片段,从而能够在一个屏幕上显示更多的内容,或提供更多的功能。
从片段类开始
随着大屏幕的出现和通常会看到更多设备旋转的用户体验(例如,在纵向模式下阅读,然后在横向模式下观看电影)的出现,开发人员必须在创建更多活动或更好地重用已为应用构建的活动集合之间做出选择。片段偏向于后一种重用范式,并在活动和呈现它们的布局容器以及用于功能的 UI 小部件之间引入了一个中间层。这有助于降低复杂性,同时处理屏幕尺寸几乎无休止的增长。使用应用的某些方面会发生变化,尤其是活动的生命周期。接下来我们将详细讨论这一点。
Fragments And Backward Compatibility
早在 Android 3.0 版本中就引入了片段(称为蜂巢)。通过 Jetpack 或旧的 Android 兼容性库,您甚至可以依赖旧版本的良好支持。
为您的应用使用基于片段的设计
在深入以片段为中心的设计之前,您应该知道使用片段是完全可选的。如果你现在喜欢设计大量活动的想法,没有必要放弃这种方法。但是如果你更喜欢利用片段提供的好处,请继续阅读。
要在设计中考虑片段,请考虑您现有的布局以及各种小部件(如 TextView、Button、RadioGroup 和其他视图)放置的位置。只要有概念上相关并放在一起的小部件子集——比如 TextView 充当 EditText 旁边的标签——就可以认为该子集已经成熟,可以包装在片段中了。为了直观地展示这一点,图 12-1 显示了这种分组,在小部件和整体活动之间有一个片段中间层。
图 12-1
片段作为小部件和活动之间的中间组
使用这个模型,您可以看到片段分组是如何移动的,并且可能会在更大的屏幕上以不同的方向显示更多的片段及其包含的小部件。您仍然能够在更小的手机屏幕上显示完美的 UI,而不必在尺度的两端做出妥协。
为了实现这一壮举,片段首先使用<fragment>
元素块将 XML 添加到布局中。在接下来的示例中,您将会看到这一点。您的布局定义的其余部分基本保持不变,这意味着到目前为止您所学的所有内容在片段设置中仍然是 100%可用的。这使您可以继续使用视图的层次结构,并使这些视图膨胀,以创建用户与之交互的结果屏幕,就像以前一样。
使用片段还引入了一种使用 Bundle 对象的额外情况,提供初始化、状态保存和重新创建,其方式与前面在第十一章中讨论的活动非常相似。片段还有其他值得了解的特性,包括:
-
您可以子类化基本片段类并添加您自己的自定义逻辑,但是您必须为派生类提供一个构造函数。
-
当使用片段时,Android 会创建一个片段管理器来处理你的片段之间的双向交互——你不需要为此编码。
需要注意的另一个主要变化是整体活动和片段生命周期是如何变化的。
使用片段生命周期
在片段生命周期和活动生命周期之间存在许多共享的行为和概念,我在前面的第十一章中介绍过。与活动生命周期一样,片段生命周期的可视化图表有助于概念化状态和转换。完整的片段生命周期如图 12-2 所示。
图 12-2
片段生命周期
原始活动生命周期和片段生命周期之间的主要区别与宿主活动和组成片段的交互有关。与父活动的单个状态转换相比,片段可能会增加复杂性和多个事件转换。
回顾片段生命周期回调方法
许多片段生命周期回调方法与您在活动中看到的方法同名,但是您应该注意这并不意味着它们做完全相同的事情。下面的列表展示了主要的区别,以及只有片段的回调方法。
onInflate()
调用onInflate()
方法是为了使用<fragment>
元素将布局 XML 文件中定义的片段布局扩展到屏幕 UI 中。如果您通过newInstance()
调用在代码中以编程方式显式创建新的片段,您也可以直接调用onInflate(
。传递给它的参数包括片段将存在于其中的引用活动和一个 AttributeSet,以及来自<fragment>
标记的任何附加 XML 属性。在这个阶段,Android 正在决定你的片段在渲染时的样子,尽管它目前不会显示它。该步骤发生在onAttach()
回调期间。
onCreate()
片段的onCreate()
类似于活动的onCreate()
,有一些小的调整。主要的变化是您不能依赖任何活动视图层次结构来引用onCreate()
调用。仔细想想,这是有意义的,因为与片段相关联的活动正在经历它自己的生命周期,而就在您认为可以开始依赖它的时候,它可能会停止存在,或者经历一个配置更改或另一个导致视图层次结构被破坏或重新创建的事件。
onattach()
在 Android 确定片段被附加到哪个活动之后,回调立即发生。在这一点上,您可以安全地处理活动关系,比如为其他操作获取和使用上下文。任何片段都有一个继承的方法getActivity()
,该方法将返回它所附加的活动。您的片段也可以使用getArguments()
方法来获取和处理任何初始化参数。
onCreateView()
onCreateView()
回调为您提供了为片段返回所选视图层次的机制。它接受一个LayoutInflater
对象、ViewGroup
和实例状态的Bundle
,然后依靠 Android 根据所有常见的屏幕大小和密度属性选择一个合适的布局,用。inflate(
)的LayoutInflater
方法,以你认为需要的任何方式修改布局,然后将结果视图对象交回进行渲染。
onViewCreated()
在onCreateView()
返回后,立即触发onViewCreated()
。在使用任何保存的状态修改视图之前,它可以执行进一步的后处理工作。
onViewStateRestored()
在片段的视图层次结构的所有状态都恢复的情况下,调用onViewStateRestored()
方法。这对于区分新的创建和配置更改后的恢复等情况非常方便。
onStart( )
片段的onStart()
回调直接与父活动的对等onStart()
相关联,并在片段显示在用户 UI 中后立即被调用。您想放在活动级onStart()
回调中的任何逻辑都应该放在相关的片段onStart()
方法中。
onResume( )
片段onResume()
方法也紧密地映射到等价的活动onResume()
方法。这是用户完全控制活动及其片段之前的最后一次调用。
onpause()
onPause()
回调也与整体活动的onPause()
方法紧密匹配。如果您将逻辑移动到片段中,那么 activity variant 中关于暂停音频或视频、暂停或释放其他动作和资源等的规则在这里都适用。
onSaveInstanceState()
onSaveInstanceState()
的片段版本与 activity equivalent 相同。您应该使用onSaveInstanceState()
通过片段的Bundle
对象来持久化您想要在片段实例之间保留的任何资源或数据。不要过分保存大量的数据和状态——记住,你可以在片段本身之外使用长寿命对象的标识符引用,只需要引用和保存那些。
onStop()
onStop()方法相当于 activity 的相同方法。
onDestroyView()
onDestroyView()
在片段移动到生命结束阶段时被调用。当 Android 已经分离了与片段相关联的视图层次时,onDestroyView()
被触发。
onDestroy()
一旦片段不再被使用,就调用onDestroy()
方法。此时,该片段仍然与它的活动相关联,尽管它很快就会被送到废料堆!
底部( )
终止一个片段的最后一步是从它的父活动中分离出来。这也标志着应该销毁、移除或释放所有其他资源、引用和其他残留标识符的时刻。
从简单的片段生命周期事件开始
前面的一组生命周期阶段和相关的片段回调方法可能会让您晕头转向,您可能会担心为了使用和受益于片段,您必须立即为所有这些方法编码。你不用担心!正如对活动生命周期的介绍一样,您不必用自己的逻辑覆盖所有的存根方法。只有在特定的状态转换中,您需要或想要做一些事情时,您才需要提供支持代码。你可以从为onCreateView()
方法提供一个覆盖开始,然后退出。
在下面的ColorFragmentsExample
代码中,我就是这么做的,将回调逻辑保持在绝对最小。
创建基于片段的应用
是时候看看片段的作用了!我将使用一组简单的颜色主题小部件和活动来说明开始使用片段的容易程度。
创建片段布局:颜色列表
清单 12-1 显示了基于片段的布局,它将在父活动中用于显示颜色列表。
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/color_list"
android:name="org.beginningandroid.colorfragmentsexample.ColorListFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
tools:context=".ColorListActivity"
tools:layout="@android:layout/list_content" />
Listing 12-1Fragment layout for displaying a list of colors
这个定义包含了<fragment>
XML 元素,并依赖于股票列表内容布局来显示列表中的TextView
条目。我们将把这个片段用于所有不同的可能显示尺寸和方向,无论是电话屏幕上的单窗格视图还是大屏幕上的多窗格视图布局。
创建片段布局:颜色细节
我将使用一个TextView
小部件来显示颜色的细节,然后将它放在片段中,该片段将放在父活动中。在Ch12/ColorFragmentExample
项目的 fragment_color_detail.xml 文件中可以找到TextView
的简单布局。清单 12-2 显示了内容。
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/color_detail"
style="?android:attr/textAppearanceLarge"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:textIsSelectable="true"
tools:context=".ColorDetailFragment" />
Listing 12-2The TextView layout used to show color details
颜色详细信息的单窗格父活动
当在小屏幕设备上运行时,我们会将TextView
布置在一个适合大小的活动中。该活动的唯一任务是创建包含TextView
的片段,您可以在清单 12-3 中看到这段代码。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/color_detail_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ColorDetailActivity"
tools:ignore="MergeRootFrame" />
Listing 12-3The activity_color_detail.xml layout
这是一个简单的<FrameLayout>
带有一些基本的华丽。TextView
将通过一个片段放置在这个布局中。
颜色详细信息的双窗格父活动
当我们移动到一个更大的屏幕上时,一个更合适的布局会将所有的片段和 UI 小部件同时放在屏幕上,最大化地利用空间。
这个activity_color_twopane.xml
布局文件可能会让你觉得有更多的工作要做,但是仔细观察,你会发现它实际上只是一个包含了<fragment>
和<FrameLayout>
的组合,我们把它们放到了小屏幕的单独布局中。清单 12-4 展示了这个 XML。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:baselineAligned="false"
android:divider="?android:attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
tools:context=".ColorListActivity">
<fragment android:id="@+id/color_list"
android:name="com.artifexdigital.android.colorfragmentsexample.ColorListFragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
tools:layout="@android:layout/list_content" />
<FrameLayout android:id="@+id/color_detail_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
</LinearLayout>
Listing 12-4The activity_color_twopane.xml layout
与连接在一起的独立布局相比,唯一的区别是 android:layout_weight 值,它将用于管理两个片段在单个活动中一起呈现时所使用的相对屏幕空间。通过选择 1:3 的比例,这将给主列表片段四分之一的空间,给细节片段剩下的四分之三的空间。
选择要膨胀的布局
你的 Android 应用如何决定使用哪种布局,显示什么样的片段排列?答案在于在项目的res/
资源文件夹层次结构中使用多个 refs.xml 文件。在我们的例子中,我们在每个res/values-large
和res/values-sw600dp
文件夹中都有一个 refs.xml 文件。
当我们的代码运行时,Android 将在运行时检查所有不同大小特定的res/
目录中的任何大小特定的 XML 资源(可以有两个以上,正如您在本书前面对 Android 项目结构的探索中看到的)。对于大尺寸和 sw600dp 尺寸的屏幕,refs.xml 中只有一个子元素,如下所示:
<item type="layout" name="activity_color_list">@layout/activity_color_twopane</item>
任何被 Android 归类为“大”或符合 sw600dp 分辨率标准的屏幕都会触发 Android 使用来自同名 XML 文件的activity_color_twopane
布局。
片段编码
在为基于片段的应用编写代码时,您需要考虑的差异很少。主要的不同之处在于我们在第十一章中提到的与生命周期相关的回调,以及你的以用户界面为中心的逻辑和任何相关的数据处理将转移到片段级别。您的活动仍然存在,并且它们处理活动生命周期事件的逻辑和跨片段的功能也保持不变。
我们的ColorListActivity
是使用片段时低编码负担的一个很好的例子。清单 12-5 展示了我们的应用的完整逻辑,包括处理我们的应用最终在父活动中显示为一个还是两个片段。
package org.beginningandroid.colorfragmentexample;
import android.content.Intent;
import android.os.Bundle;
public class ColorListActivity extends FragmentActivity
implements ColorListFragment.Callbacks {
private boolean mTwoPane;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_color_list);
if (findViewById(R.id.color_detail_container) != null) {
mTwoPane = true;
((ColorListFragment) getSupportFragmentManager()
.findFragmentById(R.id.color_list))
.setActivateOnItemClick(true);
}
}
@Override
public void onItemSelected(String id) {
if (mTwoPane) {
Bundle arguments = new Bundle();
arguments.putString(ColorDetailFragment.ARG_ITEM_ID, id);
ColorDetailFragment fragment = new ColorDetailFragment();
fragment.setArguments(arguments);
getSupportFragmentManager().beginTransaction()
.replace(R.id.color_detail_container, fragment)
.commit();
} else {
Intent detailIntent = new Intent(this, ColorDetailActivity.class);
detailIntent.putExtra(ColorDetailFragment.ARG_ITEM_ID, id);
startActivity(detailIntent);
}
}
}
Listing 12-5The code for the ColorListActivity
总的来说,逻辑非常简单。当调用onCreate()
时,我们将activity_color_list
布局展开到 UI 中。接下来,我们测试以确定 color_detail_container 视图对象是否已经实例化(不管它是否显示)。这给了我们一个代理来确定应用是否运行在 activity_color_twopane 布局中,基于 Android 中的屏幕检测规则和我们的refs.xml
规则。如果我们在这种状态下运行,我们设置一个布尔值mTwoPane
为真,并使用getSupportFragmentManager()
,通过setActivateOnItemClick()
方法设置点击处理。
然后,onItemSelected()
override 承担了决定当用户点击一种颜色时做什么的任务。我们应该使用ColorDetailFragment.java
中的color_detail_fragment
布局和相关代码创建一个额外的片段,还是应该使用startActivity()
来显式调用color_detail_activity
布局和相关的ColorDetailActivity.java
代码?
Ch12/ColorFragmentExample
中的源代码还揭示了显示颜色细节的机制和支持的ColorContent
类,这只是一种为颜色和一些管理功能设置的项目的 Java 打包方法(记住,Android 的 Java 支持尚未超过 Java 8,因此像数据类这样的更现代的方法不可用)。可以提供该列表的其他选项是内容供应器或其他数据源。
ColorFragmentExample 行动范例
应用逻辑、布局和片段完成后,让我们运行应用吧!
为了查看片段在运行中的威力,我们需要两个不同大小的模拟器,它们已经在前面的例子中设置好了。图 12-3 和 12-4 显示了一个小设备上独立活动中的颜色列表和颜色细节片段——我在这个例子中使用了我的 Pixel 2 AVD。
图 12-4
通过触发新的活动来显示颜色细节片段
图 12-3
Pixel 2 模拟器上显示的颜色列表片段
当在更大的屏幕上运行相同的应用时,您可以看到不同之处。图 12-5 和 12-6 展示了片段的威力,应用运行在 Pixel C 仿真器上。
图 12-6
检测到大屏幕后,第二个片段被添加到活动中
图 12-5
ColorListActivity 初始显示,像素 C 上有一个片段
摘要
既然您已经记住了片段的核心概念,那么探索片段方法的全部能力的最佳方式就是在越来越多的应用中进行实践。我们在本书网站 www.beginningandoid.org
上有更多使用片段的例子。你可以在网上找到成千上万的例子。
十三、为 Android 处理声音、音频和音乐
是时候转移到为 Android 开发应用的一些更复杂和有趣的方面了。在接下来的几章中,我们将探讨如何向您的应用添加音频、视频和静止图像,以及创建和记录这些媒体类型以及显示和使用它们的机制。
让我们先来看看音频功能。本章剩余部分有一个警告:印刷书籍不能真正提供音频示例(尽管在线版本可以)。为了充分利用本章中的示例代码,您绝对应该尝试在设备或 AVD 上运行这些应用。
播放音频
Android 提供了在应用中访问和使用音频的丰富方式。事实上,随着时间的推移,方式的数量已经增长得如此之多,以至于选择几乎太多了。不要害怕。Android 还建立了一个结构良好的方法来帮助管理所有这些不同的方法——称为媒体包——我们很快就会谈到。
选择您的音频方法
首先,让我们看看 Android 中利用音频播放的主要方式。根据您的需求以及您计划从何处获取音频,每种不同的方法都为您提供了优势和选择。
使用原始音频资源
原始音频资源是一个声音文件或音频源,它被打包成一个文件,并捆绑在您的应用的raw
文件夹中。raw 方法意味着您可以保证您希望使用的音频始终可用,这种方法非常常用于音频,如游戏、通知等中的声音效果。原始方法的缺点是,只能通过替换(升级)整个应用来更改与应用的 APK 打包在一起的音频。
使用音频素材
音频资源也与您的应用打包在一起,因此具有与原始方法相同的优点和缺点。素材方法提供的额外好处是通过 URI 命名方案使您的音频可用,例如,file:///android_asset/some_URL.
这在您使用任何期望或需要 URI 的 API 时都有好处,Android 中有许多这样的 API。
将文件存储用于音频
对于胆小的人来说,文件存储方法甚至比原始方法更基本。您可以使用设备的板载或可移动存储来保存音频文件。访问是通过文件 I/O API 进行的(我们将在本书后面介绍)。虽然这是一个更大的管理负担,但这意味着理论上您可以下载新文件、更改文件和删除文件,而不必升级应用。
访问音频流服务
如果你想完全摆脱存储音频的烦恼,流媒体就是答案。您可以从其他设备上的服务或 Android 内容供应器(他们自己可能从其他地方流式传输)流式传输音频,或者直接从基于互联网的服务流式传输。流式传输让您不必担心存储、文件管理、空间需求或升级。它用连接焦虑取代了这些担忧——只有在数据连接正常的情况下,你才能进行流媒体播放。
使用媒体包
处理音频的丰富选择可能是一件好事,也可能是一件坏事。有一种机制可以满足你的每一个突发奇想,但是随着选择而来的是复杂性。幸运的是,Android 配备了多才多艺的媒体包,有助于简化您作为开发人员的生活,同时保留您可用的选项。
Media 包提供了两个关键的类供您使用,一个是用于播放音频的 MediaPlayer,我们将首先处理它,另一个是用于设备上录制的 MediaRecorder,我们将在本章的后面处理它。
创建音频应用
为了使媒体包和 MediaPlayer 类变得生动,我们将创建一个您几乎肯定会熟悉的标志性应用。如果你曾经拥有过 iPod、智能手机或类似的设备,你可能会播放你最喜欢的音乐、播客或类似的 MP3 音频文件。是时候制作自己的 MP3 播放器应用了。
在素材和资源之间选择
如本章前面所述,您可以选择使用哪种方法来管理音频文件,以便应用可以访问它们。
如果我们想使用原始音频资源,我们首先需要在项目中创建一个原始文件夹来存放音频文件。这可以在 Android Studio 中完成,通过导航到层级中的 res 文件夹并选择菜单选项File
➤ New
➤ Directory
。将该目录命名为“raw”,您的 raw 文件夹现在就已经准备好了。
如果我们想要使用基于素材的方法,我们同样需要为我们的应用创建素材文件夹。要在 Android Studio 中创建一个 assets 文件夹,请在您的项目层次结构中突出显示 app 父级文件夹,然后在 Android Studio 中选择File
➤ New
➤ Folder
➤ Assets Folder
菜单选项,这将提示为您的项目创建一个 assets 文件夹,如图 13-1 所示。
图 13-1
提示将素材文件夹添加到 Android Studio 中的项目
项目中 Android Studio assets 文件夹对应的文件系统位置是./app/src/main/assets
(在 Windows 下是.\app\source\main\assets
)。
如果想继续使用传统的 raw 文件夹,可以在项目的./app/src/main/res
文件夹下找到(或创建)它。
编写音频回放代码
本着学习 MediaPlayer 的精神以及如何将音频播放器的所有部分组装在一起的基本原则,我们将回避媒体包中的一些其他好东西,包括捆绑特殊的屏幕小部件和其他有吸引力的素材,它们会让您快速获得一个外观精美的媒体播放器,但会剥夺您了解其工作方式和原因的任何机会。
我们将简单地开始,在接下来的几章中逐步建立,并探索媒体框架的后续部分。
以简单的方式播放音频
为了初步探索音频播放的媒体框架,我们将使用一个非常简单的初始界面,如图 13-2 所示。别担心。我们将在接下来的几章中对此进行扩展和改进。
图 13-2
最初简单的音频播放器界面
虽然这种简朴的设计并不花哨,但它让我们探索开始回放和停止回放的机制。布局 XML 如清单 13-1 所示,可作为Ch13/AudioPlayExample
项目的一部分获得。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/startButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/stopButton"
android:layout_marginTop="268dp"
android:onClick="onClick"
android:text="Start ♫"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="-16dp" />
<Button
android:id="@+id/stopButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:onClick="onClick"
android:text="Stop ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/startButton"
tools:layout_editor_absoluteX="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 13-1The layout for the AudioPlayExample project
简而言之,该布局提供了两个按钮,标记为“开始”和“停止”,并附加了一个用于音符的 Unicode 符号。重要的是,这两个按钮中的每一个都添加了android:onClick
属性,这使我们能够连接一个在按钮被单击时调用的方法。我们为每个按钮使用了相同的目标方法名—onClick
。单击任一按钮都会调用这个方法,我们将使用android:id
值驱动该方法中的逻辑来决定要做什么。
为 AudioPlayExample 编写 Java 逻辑
清单 13-2 显示了我们的音频播放示例的 Java 逻辑,它也可以在Ch13/AudioPlayExample
项目中获得。
package org.beginningandroid.audioplayexample;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private MediaPlayer mp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startButton:
doPlayAudio();
break;
case R.id.stopButton:
doStopAudio();
break;
}
}
private void doPlayAudio() {
mp = MediaPlayer.create(this, R.raw.audio_file);
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
mp.start();
}
private void doStopAudio() {
if (mp != null) {
mp.stop();
}
}
public void onPrepared(MediaPlayer mp) {
mp.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mp != null) {
mp.release();
}
}
}
Listing 13-2The Java code supporting the AudioPlayExample application
在我们深入研究代码之前,后退一步,看看有多少行代码。很少,其中许多是您期望看到的生命周期回调方法样板。正如您将看到的,这是因为即使以低级方式使用 MediaPlayer,编码效率仍然很高。
除了预期导入的view.View
(针对所有标准 Android 小部件和其他部件)和os.Bundle
,我们还导入了用于音频播放的关键媒体框架包:
-
AudioManager 提供了一系列支持功能,使各种音频的音频处理变得更加简单。您可以使用 AudioManager 来标记音频源是流、语音、机器生成的音调等等。
-
MediaPlayer 是媒体包的主力,它让您可以完全控制本地和远程音频的准备和播放。
-
异步播放的关键,OnPreparedListener 是一个接口,它可以在线程外准备完成后回调播放音乐。
onCreate()
回调实现做我们非常熟悉的工作,将我们的布局膨胀到应用的 UI 中。然后,它将控制权交给其他方法,以响应用户与按钮的交互。
从前面对布局 XML 的描述中,您知道实现了onClick()
来根据传递给它的View
确定动作。当用户决定点击startButton
或stopButton
按钮时,Android 会将代表被点击按钮的相应View
的引用传递给onClick()
方法。Java switch 语句检测哪个View
被传递给该方法,并暗示哪个按钮被点击。如果点击了startButton
,则调用doPlayAudio()
方法。或者,如果是stopButton
,我们就调用doStopAudio()
方法。
当用户点击“开始”按钮并调用startButton
逻辑时,一系列意料之中和意料之外的事情发生了。事情从创建一个MediaPlayer
对象开始,我们将音频文件绑定到这个对象。R.raw.audio_file
符号在概念上类似于你已经见过的布局膨胀符号,比如R.layout.activity_main
。Android 将遍历与应用打包在一起的 raw 文件夹中的.apk
文件,并尝试查找名为audio_file
的具有任何支持的音频扩展名(例如,mp3、m4a 等)的素材。–我们示例中的文件名audio_file.m4a
。
文件确定后,我们通过mp.setAudioStreamType()
方法第一次使用 AudioManager 类。AudioManager 可以为你做很多事情,其中之一就是为给定的音频资源设置流类型。Android 支持一系列音频流类型,允许它为所讨论的音频提供音量、保真度等方面的最佳支持。我们使用STREAM_MUSIC
流类型,表明我们想要设备支持的最高动态范围等等。其他选项包括 DTMF 音调的STREAM_DTMF
——Android 过滤任何以这种方式标记的流以符合 DTMF 标准——和STREAM_VOICE_CALL
流类型,它触发 Android 调用或抑制语音音频的各种回声消除技术。
因为我们直接使用原始素材,所以我们可以立即调用doPlayAudio()
中的mp.start()
。这将触发 MediaPlayer 对象开始实际播放文件,并向扬声器或耳机发送音频。
用户点击“Stop”触发doStopAudio()
方法,这在很大程度上是不言自明的。如果被实例化,我们首先调用MediaPlayer
对象上的stop()
方法。我们使用if{}
块测试结构进行实例化,以检查如果用户从未点击开始(例如,如果他们打开应用并错误地点击停止作为他们的第一个动作),我们不会试图停止任何事情。
接下来是onPrepared()
回调方法。该方法链接到包定义,其中 AudioExample 实现了OnPreparedListener
接口。从技术上讲,在 AudioExample 应用的第一次调用中,我们不使用onPrepared()
回调,但是在这里包含它是为了强调在MediaPlayer
对象被实例化并且AudioManager
被调用来设置流类型之后,有时您不能立即开始回放。何时以及为何使用onPrepared()
将在流回放示例中进一步讨论。
我们以onDestroy()
回调来结束,以释放 MediaPlayer 对象(如果它之前已经被创建的话)。
至此,您已经准备好亲自尝试一下了。启动一个 AVD 图像,运行这个例子,或者如果你修改了Ch13/AudioPlayExample
代码的话,运行你的变体,让你自己确信最终的工作产品实际上会产生一些噪音!
使用流媒体播放音频
虽然回放 MP3 文件和其他存储的音频在 iPod 和其他音乐播放器出现时被认为是非常流行的,但显然时代在变化,Android 不跟上这种趋势将是失职。Android 完全支持流媒体,包括设备上和远程的其他服务的音频。媒体框架再次为您处理事情。
图 13-3 显示了我们修改的 AudioPlayExample,作为一个新的和改进的 AudioStreamExample。我将把文本中重复出现的 XML 保存下来——请随意在Ch13/AudioStreamExample
中查看。
图 13-3
音频流示例用户界面
为了以流的形式获取和播放音频,我们的 Java 代码需要做更多的工作,这可以在清单 13-3 中看到。
package org.beginningandroid.audiostreamexample
;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity implements OnPreparedListener {
// useful for debugging
// String mySourceFile=
// "https://ia801400.us.archive.org/2/items/rhapblue11924/rhapblue11924_64kb.mp3";
private MediaPlayer mp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startButton:
try {
EditText mySourceFile=(EditText)findViewById(R.id.sourceFile);
doPlayAudio(mySourceFile.toString());
} catch (Exception e) {
// error handling logic here
}
break;
case R.id.stopButton:
doStopAudio();
break;
}
}
private void doPlayAudio(String audioUrl) throws Exception {
mp = new MediaPlayer();
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
mp.setDataSource(audioUrl);
mp.setOnPreparedListener(this);
mp.prepareAsync();
}
private void doStopAudio() {
if (mp != null) {
mp.stop();
}
}
// The onPrepared callback is for you to implement
// as part of the OnPreparedListener interface
public void onPrepared(MediaPlayer mp) {
mp.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mp != null) {
mp.release();
}
}
}
Listing 13-3AudioStreamExample logic
我们的代码从第一个 AudioPlayExample 以两种方式发展而来,这可以在doClick()
和doStartAudio()
方法中看到。doClick()
方法已经改变,接受用户在EditText
对象mySourceFile
中输入的 URL,并将其作为选择的音频文件进行播放。我们使用EditText
的String
值传递给后续调用中修改过的doPlayAudio()
方法。try-catch 块用于覆盖doPlayAudio()
现在可以抛出的异常,例如,如果没有找到 URL 或者没有返回流。
doPlayAudio()
方法现在避免了直接文件访问。相反,我们简单地创建新的 mp MediaPlayer
对象。我们调用AudioManager
包,并声明最终的数据源将是STREAM_MUSIC
。随后,我们使用传递的 URL 调用setDataSource()
(这个方法还有许多其他有用的选项,但是我们将这些留到以后讨论)。
为了成功使用setDataSource()
调用,我们需要在清单文件中授予我们的应用android.permission.INTERNET
权限,这样它就可以获取源(音乐)流。我们将在第十九章中深入介绍权限,但是现在你需要做的就是将以下内容添加到你的项目的AndroidManifest.xml
文件中:
<uses-permission android:name="android.permission.INTERNET" />
最后,调用 MediaPlayer 对象上的.prepareAsync()
。
Synchronous vs. Asynchronous Playback
无论音频来源如何,尝试立即播放音频都有利弊。简而言之,作为一名开发人员,您需要回答这样一个问题:在用户点击 Play(或类似功能)和音乐实际准备好并可以通过设备播放之间的时间间隔内发生了什么?你是阻止所有活动,等待还是让其他事情继续?这是一个有着一系列细微差别的更深入的话题,你可以在本书网站 www.beginningandroid.org
上阅读更多内容。
播放音乐流
掌握了这些变化后,我们的AudioStreamExample
将最终接收到对onPrepared()
的回调,这样音乐(或声音、鸟鸣声或其他声音)将开始播放。onPrepared
的逻辑与前面的例子没有变化。
探索其他回放选项
使用媒体包和MediaPlayer
对象并不是你在 Android 下处理音频和音乐的唯一选择。其他选项包括:
-
SoundPool:media player 的精简版本,sound pool 简化了处理设备上声音文件的方法。它无需任何流或服务提供的音频,并利用文件/资源访问,使用
FileDescriptor
来访问应用打包的音频文件。apk 文件通过简单的方法,包括.load()
和.getAssets().openFd()
。 -
AsyncPlayer: MediaPlayer 为音频回放的异步准备提供了一些支持,但是您在 AudioPlayExample 应用中看到的许多实际回放机制是同步的。AsyncPlayer 使用完全异步的两步方法。首先,实例化一个 AsyncPlayer 对象,然后用要播放的音频的 URI 调用它的
.play()
方法。此后的某个时间,音频将开始播放,但是考虑到这种方法的异步特性,所涉及的时间是不确定的。 -
JetPlayer:复杂性谱的另一端是 JetPlayer。使用 JetPlayer,您可以使用与 Android SDK 和 API 捆绑在一起的外部工具来为您的应用打包和管理音频,并通过 MIDI 标准提供对音频的访问。从那里,你的 Android 应用就可以使用这个音频,并且可以访问一些非常复杂的操作选项,这些已经超出了本书的范围。
-
ExoPlayer:最新且越来越受欢迎的播放方式。ExoPlayer 是谷歌的开源产品,在正常的 Android 机制之外发布,并提供实验性或新颖的功能,如平滑流式自适应播放。它是专门为像您这样的开发人员扩展和修改而设计的。详见
https://github.com/google/ExoPlayer
。
在android.com
网站上有大量关于这些音频替代品的文档。
录制音频
讲述了音频播放的第一个基础知识后,是时候将我们的注意力转向录制音频和分享这些声音了。正如播放有许多替代方法一样,录制音频在 Android 中也有多种可能性。
使用 MediaRecorder 录制音频
对本章开头介绍的MediaPlayer
的补充,MediaRecorder
提供了一套广泛的工具,用于在各种环境下录制声音。了解它能提供什么的最好方法是深入一个例子,我们将通过扩展我们之前的例子,在Ch13/AudioRecordExample
代码中加入记录特性来做到这一点。在图 13-4 中,你可以看到扩展的——尽管仍然很简单——用户界面,它在你已经看过的现有回放示例中添加了录制按钮。
图 13-4
添加到 AudioRecordExample 用户界面的录音按钮
清单 13-4 中显示了 AudioRecordExample 布局 XML。最值得注意的一点是,按照前面的模式,所有的按钮都将触发onClick()
回调方法,这意味着记录和回放(以及二者的停止)将在补充的 Java 代码中处理。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/startRecordingButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/stopRecordingButton"
android:layout_marginTop="236dp"
android:onClick="onClick"
android:text="Start Recording ♫"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/stopRecordingButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:onClick="onClick"
android:text="Stop Recording ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/startRecordingButton"
tools:layout_editor_absoluteX="-16dp" />
<Button
android:id="@+id/startButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/stopRecordingButton"
android:onClick="onClick"
android:text="Start Playback ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/stopRecordingButton"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/stopButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/startButton"
android:onClick="onClick"
android:text="Stop Playback ♫"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/startButton"
tools:layout_editor_absoluteX="0dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 13-4AudioRecordExample layout XML
AudioRecordExample
布局有不言自明的按钮,用于开始和停止录制和回放。与我们前面的例子有一个显著的不同,它隐藏在幕后。为了访问任何 Android 设备的录音功能,您的应用将需要权限。它还需要将记录的内容写入存储的权限(假设您想要存储您记录的内容)。我们将在第十九章中详细介绍权限和安全性,但是现在下面两个权限声明应该添加到AndroidManifest.xml
文件中:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
运行时需要一个额外的权限步骤,提示用户允许写入外部存储器。有了相关的权限设置,您的代码现在可以访问记录并将它们存储起来。清单 13-5 显示了 AudioRecordExample 的 Java 逻辑。
package org.beginningandroid.audiorecordexample;
import androidx.appcompat.app.AppCompatActivity;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.view.View;
import java.io.File;
public class MainActivity extends AppCompatActivity {
private MediaRecorder mr;
private MediaPlayer mp;
private String myRecording="myAudioRecording";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startRecordingButton:
doStartRecording();
break;
case R.id.stopRecordingButton:
doStopRecording();
break;
case R.id.startButton:
doPlayAudio();
break;
case R.id.stopButton:
doStopAudio();
break;
}
}
private void doStartRecording() {
File recFile = new File(myRecording);
if(recFile.exists()) {
try {
recFile.delete();
} catch (Exception e) {
// This code can be extended to deal with errors in recording here.
}
}
mr = new MediaRecorder();
mr.setAudioSource(MediaRecorder.AudioSource.MIC);
mr.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
mr.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
mr.setOutputFile(myRecording);
try {
mr.prepare();
} catch (Exception e) {
// do exception handling here
}
mr.start();
}
private void doStopRecording() {
if (mr != null) {
mr.stop();
}
}
private void doPlayAudio() {
mp = new MediaPlayer();
try {
mp.setDataSource(myRecording);
} catch (Exception e) {
// do exception handling here
}
mp.setAudioStreamType(AudioManager.STREAM_MUSIC);
try {
mp.prepare();
} catch (Exception e) {
// This code can be extended to deal with errors in playback here.
}
mp.start();
}
private void doStopAudio() {
if (mp != null) {
mp.stop();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mr != null) {
mr.release();
}
if(mp != null) {
mp.release();
}
}
}
Listing 13-5The AudioRecordExample code
AudioRecordExample
的代码看起来应该很熟悉,因为它模拟了我们之前在onClick()
方法中使用的控制逻辑。在onClick()
中,我们根据用户点击的按钮进行切换,播放开始和停止基本上模仿了AudioPlayExample
的早期代码,doStopRecording()
的方法与doStopAudio()
几乎相同,只是 MediaRecorder 和 MediaPlayer 对象的基础分别发生了变化。这两个类之间的相似之处是有意的,其中共同的目标由概念上匹配的方法来服务。
我们的代码中主要的新逻辑是使用doStartRecording()
方法。首先,doStartRecording()
确保文件对象 myRecording 是新创建的,如果需要,删除任何先前存在的对象。在这种情况下,我们利用java.io.File
包来提供文件处理能力——严格地说,这是普通的 Java 在运行,访问标准的 Java 库,并且在 Android 的导轨之外。我们将在本书的后面介绍更多使用标准 Java 库的功能。
然后我们创建一个名为 mr 的 MediaRecorder 对象,并调用它的。setAudioSource()
通知应用想要访问麦克风(MIC)以便它可以记录声音的方法。这就是在我们的清单文件中要求RECORD_AUDIO
权限的逻辑。
给予我们的应用对麦克风的访问权后,我们就能够为音频设置所需的输出容器格式,并设置所需的编解码器来对将被记录并放入容器中的声音进行编码。这些是.setOutputFormat()
和.setAudioEncoder()
呼叫。在我们的例子中,我们在每种情况下都使用DEFAULT
选项,它实际上根据硬件设备和使用的 Android 版本所提供的特定支持来选择容器和音频编解码器。
Android 支持的主要输出格式包括
-
AAC_ADTS:苹果和 AAC 音频格式推广的容器。
-
AMR_NB:当您希望在 Android 设备之间实现最大的可移植性时,推荐使用 AMR 窄带容器类型。
-
MPEG _ 4:MPEG-4 容器格式是最古老的格式之一,但也是最有可能在旧的平台和设备上被误解的格式。小心使用。
-
三 _GPP:另一个广泛支持 Android 的推荐容器格式。
-
WEBM:该容器用于谷歌的 WEBM 格式,然后专利-未阻碍 Ogg 编码文件格式。
容器格式以及音频和视频编解码器的主题非常广泛,充满了大量的历史和行业阴谋。它本身会成为一本伟大的书,但是,唉,我们没有空间在这里深入探讨它。为了帮助您开始编写利用音频的 Android 应用,这里列出了 Android 中用于音频(和视频)编码的更流行的编解码器:
-
AAC(以及 AAC_ELD 和 HE_AAC):高级音频编码标准的音频编解码器。受到苹果和其他设备和平台的广泛支持。
-
AMR _ NB:AMR 窄带的实际音频编码器。虽然没有在 Android 之外广泛使用,但这种编解码器提供了跨 Android 版本和设备的广泛支持。
-
VORBIS:Ogg VORBIS 音频编解码器格式。
如果您再次查看.doStartRecording()
方法,.setOutputFile()
调用配置了我们之前创建的 Java 文件对象,作为用户将要记录的音频流的存储库。
最后,我们进入正常的两步,为我们的MediaRecorder
对象调用.prepare()
和.start()
。正如MediaPlayer
对象必须应对一系列潜在的延迟一样,MediaRecorder
也是如此。这些延迟可能是无响应的远程服务、缓慢的机载存储或诸如此类的问题。无论在什么情况下,.prepare()
负责保存您的录音,并在一切就绪后将控制权交还给调用者(您的应用)。此时,对.start()
的调用实际上开始捕获音频输入。
体验这一切的最佳方式是在您选择的设备或 AVD 上亲自运行示例Ch13/AudioRecordExample
。
扩展您的开发者音频工具集
已经向您介绍了 Android 下音频和声音的第一个方面,将音频内容引入 Android 应用的其他部分也值得了解。这些领域包括计算机和移动设备音频的一些基础知识,以及 Android Studio 之外的各种工具,这些工具对于在应用中充分利用声音和音频至关重要。
数字音频——即以数字形式捕获或创建的音频,用于在计算机、电话和其他数字设备上复制和使用——是一个巨大的话题。本章的其余部分将为您提供进一步探索的起点。
了解音频的主要方面
当谈到评估音频有多“好”时,有很多主观的意见——我不会告诉你我的音乐偏好,因为它们很好!但是,您也应该熟悉音频的一些客观方面,以便首先了解哪些属性有助于音频质量,然后了解如何影响这些质量以制造“更好的声音”
音频采样和频率
您的 Android 设备及其音频播放、您编写的声音应用、您用来听音乐的计算机以及大多数现代电子音频设备都基于音频或声音的数字编码。这种编码以两种方式之一产生:或者通过直接创建数字值,或者更典型地(至少在历史上)通过采样连续的音频源或信号,并使用足够频繁地获取的足够多的样本,从而在数字“快照”中创建音频信号的良好近似
这是音频采样的基本原则。当你考虑应该多长时间采样一次模拟信号以获得足够的数字快照来提供一些保真度时,事情变得更加复杂——当重放时,声音几乎无法与模拟原始信号区分开来。这里使用的术语是采样频率,通常音频的采样速率为每秒 44100 次,例如 44.1 kHz。可能还有其他更高或更低的速率,这种选择会影响最终音频的质量和用于表示它的数据量。
音频分辨率
如果我们以 44.1 kHz 的频率对音频进行采样,那么在对模拟信号进行采样时,我们究竟捕捉到了什么?答案是关于声波在样本时间点的振幅(大小)的信息。存储样本的空间越大,我们就能覆盖信号幅度的绝对高低,但更重要的是,我们就能更好地区分信号中的小阶跃。这也称为音频样本的“比特率”。
众所周知的音频分辨率比特率的一些历史示例包括光盘(CD),其在 CD 标准中使用 8 比特比特率。这允许相当好的采样保真度,并产生在 44.1 kHz 采样速率下每秒大约 44 kB 的采样数据。DVD 提升了游戏的比特率,允许高达 16 位的音频,当代的“高清”音频通常被认为是比特率为 24 位或更高的任何东西。这些都有助于更好的数据采集,理论上更好的声音质量和保真度,但代价是需要越来越多的存储来保存结果数据。正是这些存储方面的考虑导致了以质量为代价节省空间的格式的激增,这就是编解码器的领域。
编码、解码和数据丢失
人们希望找到编码音频的方法,从而大大减少所需的空间,这种愿望有很多途径。简而言之,在许多研究和商业环境中开发了一系列研究和技术,对采样的音频进行处理,以丢弃或删除代表声音或声音方面的数据,而一旦丢弃,人们大多不会注意到这些数据丢失了。最著名的例子是由德国弗劳恩霍夫协会开发的标准,MPEG-1 音频第三层——通常被称为 MP3。这种方法使用频率削波技术和其他方法来捕获数量少得多的数据,这些数据仍然可以用于创建原始音频的合理高保真再现。
这种编码音频的方法通常被称为“有损”压缩或编码,因为一些数据会丢失。另一种方法是“无损”编码,即不丢弃任何源数据。包括 WAVE(或 WAV,一种脉码调制形式)和 FLAC 在内的编码标准都是无损格式。你会经常听到所有这些方法被称为编解码器,这是两个缩写的组合:code 来自单词 encode,dec 来自单词 decode。
对于您使用音频开发 Android 应用的工作,您可以看到编解码器和有损与无损采集的选择将如何直接影响您使用的音频的大小和质量。
更多音频理论
关于音频理论,还有很多东西需要学习和理解,这是一本 Android 书籍所缺乏的。诸如序列、合成等主题都有完整的书籍,网上也有大量关于这些主题的信息。Wikipedia 是一个很好的起点,有关采样、比特率、编码格式等实验的实践和教程,请查看下一节介绍的工具。
选择更多音频工具
回放已经创建的音频是对应用的一个很好的补充,无论是音乐、播客还是应用的声音效果。在某种程度上,你可能需要超越使用他人的音频素材,并希望创建自己的或混合和调整其他预先录制的声音。音频录制和编辑软件是一个很大的领域,但为了让你开始,你不会错看一些流行的(和免费的!)可供您选择的选项。
介绍 Audacity
有几个软件领域,自由和开源软件有很强的影响力,甚至占据主导地位,音频编辑就是其中之一。十多年来,Audacity 一直是一个受欢迎的音频编辑套件,用 Audacity 网站
的 about 页面的话说,【Audacity】是一个免费的、易于使用的多声道音频编辑器和记录器,适用于 Windows、macOS、GNU/Linux 和其他操作系统。该界面被翻译成多种语言。
Audacity 有大量的特性,但是最重要的特性可以总结如下:
-
录制现场音频–从您的计算机上可用的任何输入源
-
在计算机上录制回放–从您可能正在运行的其他系统中捕获音频
-
完整的编辑功能和对所有关键音频格式的支持,包括 MP3、WAV、AIFF、FLAC、Ogg Vorbis 等(显然,这涵盖了几乎所有 Android 支持的格式——这是一个意外收获!)
-
其他格式可通过丰富的附加库系统编辑,如 M4A 和 WMA
-
用于拷贝、剪切、拼接、混音、音高和速度调整以及速度改变的完整工具
-
支持 16 位、24 位和 32 位质量和全范围采样速率
-
高级功能,如频谱分析和其他强大的选项
我还可以继续介绍更多的特性和功能,但是你可以在 Audacity 自己的优秀文档网站 https://manual.audacityteam.org
上阅读更多内容。
在写这本书的时候,Audacity 的版本是 2.4.2。由于它是开源的,可以免费获得,我强烈建议您至少下载并安装它,试用一下 Audacity。在 https://manual.audacityteam.org/man/tutorials.html
有非常容易获得的教程,包括我推荐给每个初涉音频发烧友的两个教程:编辑音频文件和创建新录音。您将很快为您的 Android 应用创建素材。
使用 WavePad
另一个强大的音频编辑工具的竞争者是 WavePad,它在最近几年越来越突出。WavePad 拥有至少和 Audacity 一样强大的功能集,在某些情况下甚至更强。
WavePad 更广泛功能的一个关键领域是它支持更深奥和罕见的音频文件格式,如 VOX。也许 WavePad 最有趣的部分是它有自己的移动版本,包括一个 Android 版本。这意味着您可以使用自己的移动设备来创建、编辑和完善您为同一设备编写的新应用所需的音频。
WavePad 使用“免费增值”模式,免费提供一些功能,但其他功能需要付费许可。你可以在 www.nch.com.au/wavepad/index.html
找到更多关于 WavePad 的信息。
对于苹果 Mac 用户来说,有 GarageBand
对于那些拥有 Mac 电脑的人来说,还有一个选择值得考虑。过去十年或更长时间里,每台 Mac 都配备了 GarageBand,这是苹果自己的音乐编辑和创作软件。
GarageBand 有许多很棒的功能,并且围绕它有一个非常强大的社区。显然,由于 GarageBand 是与 Mac 捆绑在一起的,所以我没有给你提供下载,如果你有一台 Mac,GarageBand 已经包含在你购买机器的价格中——所以开始使用它没有边际费用。
摘要
在本章中,我们介绍了 Android 的一些核心音频和声音处理功能,包括媒体包和 MediaPlayer plus MediaRecorder。我们已经看到了各种播放和录制声音的方式,并简要介绍了 Android 为音频提供的替代包,包括 SoundPool 和 JetPlayer。
我们将在下一章讨论这个话题,届时我们将再次看到媒体包的威力。
十四、为 Android 处理视频和电影
近年来,手机应用最繁荣的领域之一是视频。无论是在通勤时观看网飞的流媒体节目,还是在 YouTube 上捕捉猫咪的滑稽动作,或者使用基于视频的聊天和消息应用,视频在安卓系统中从未如此突出。给你的应用添加视频功能非常简单,尽管你应该知道 Android 下的视频有一些奇怪和意想不到的地方。
在这一章中,我们将探讨向应用添加视频内容的最简单方法,然后花时间学习更广泛的视频工具集,如果你打算认真对待 Android 视频的话。在本书的后面,我们还将提到使用 Android 内容供应器机制的视频选项。
回放视频
就像音频和声音一样,Android 提供了一系列将视频回放引入应用的方法。事实上,这些方法中的一些是你在上一章中已经使用过的类和框架,比如媒体框架。
播放视频有其独特的方面,其中最重要的是使用专用的小部件 VideoView,用于实际显示视频和控制视频在播放过程中的一些行为,以及用户在播放时对视频的一些控制。
在 Android 中处理视频非常简单。虽然您可以构建复杂的层次,但从最基本的开始是理解视频播放过程中发生的事情的机制的好方法,也是让自己熟悉更复杂的方法对您隐藏的基本构件的好方法。
我们将通过浏览一个示例应用来开始探索视频回放,您可以在Ch14/VideoPlayExample
项目文件夹中找到该示例应用。
设计基于视频视图的布局
为了显示视频以便回放,我们需要一个合适的带有 VideoView 对象的活动。清单 14-1 显示了 VideoPlayExample 应用的布局,它就有这样一个视频视图。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<VideoView
android:id="@+id/video"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toBottomOf="@+id/stopButton"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/startButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/stopButton"
android:onClick="onClick"
android:text="Start Video 🎦"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="16dp" />
<Button
android:id="@+id/stopButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:onClick="onClick"
android:text="Stop Video 🎦"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="@+id/startButton"
tools:layout_editor_absoluteX="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 14-1Layout XML including VideoView object for VideoPlayExample
回顾我们的布局,您会注意到我们有如下三个小部件:
-
“开始视频”按钮,其
startButton
的android:id
和android:onClick
属性设置为“onClick” -
一个“停止视频”按钮,
stopButton
的android:id
和android:onClick
属性设置为“onClick ”,就像startButton
一样 -
一个
VideoView
小部件,带有一个video
的android:id
我们使用了一个ConstraintLayout
布局,并将startButton
设置为限制在父窗口的顶部(也就是活动窗口的顶部)。stopButton
被约束对齐到startButton
的底部,视频VideoView
被约束对齐到stopButton
的底部。在显示任何视频之前,最终的布局看起来很像图 14-1 中的图像。
图 14-1
VideoPlayExample 应用的可视布局
布局故意非常直接,以便访问视频文件、播放视频文件等的逻辑更加平易近人。鉴于两个按钮都使用了android:onClick="onClick"
属性,您可能已经能够猜出基本结构了。
在代码中控制视频回放
查看伴随我们布局的 Java 逻辑,您将立即发现一个模式,它与我在第十三章中介绍的音频和声音示例相似。正如我们在 AudioPlayExample 和 AudioStreamExample 应用中看到的那样,许多控制逻辑都围绕着使用 onClick()方法来驱动活动行为。我们的 Java 代码如下所示,如清单 14-2 所示。
package org.beginningandroid.videoplayexample;
import androidx.appcompat.app.AppCompatActivity;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.MediaController;
import android.widget.VideoView;
public class MainActivity extends AppCompatActivity {
private VideoView vv;
private MediaController mc;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startButton:
doPlayVideo();
break;
case R.id.stopButton:
doStopVideo();
break;
}
}
private void doPlayVideo() {
vv =(VideoView)findViewById(R.id.video);
mc = new MediaController(this);
mc.setAnchorView(vv);
vv.setMediaController(mc);
vv.setVideoURI(Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.video_file));
vv.requestFocus();
vv.start();
}
private void doStopVideo() {
if (vv != null) {
vv.stopPlayback();
}
}
}
Listing 14-2The Java logic for video playback
Note
这个代码示例使用了一个名为“video_file.m4a”的视频文件。如果您出于任何原因需要访问原始视频文件,可以从beginningandroid.org
网站获得。
从我们的MainActivity
开始,你会看到我们创建了两个对象。第一个是名为vv
的VideoView
对象,稍后将用于绑定到展开布局的<VideoView>
元素。第二个是一个MediaController
对象mc
,我们很快就会谈到它。onCreate()
override 执行扩展布局的基本操作,仅此而已。
接下来,您将看到onClick()
方法,就像在音频示例中一样,它将一个View
作为参数,然后使用一个基于视图的android:id
的switch
语句来确定哪个按钮被点击了:startButton
或stopButton
。这与第十三章中的例子使用的模式非常相似——你可以看出这是我反复发现的一个有价值的模式!
如果startButton
被检测为View
(按钮)被点击,那么doPlayVideo()
方法被调用。该方法首先确保vv VideoView
对象绑定到VideoView
UI 小部件,使用现在已经很熟悉的调用findViewById()
的技术,并使用布局中VideoView
持有的“视频”的 android:id 的R.id.video
样式表示。
接下来,我们实例化新的MediaController
对象mc
,然后立即调用setAnchorView()
方法。这将绑定MediaController
,并允许它呈现一组浮动控件,当在应用中使用时,这些控件将出现在VideoView
对象上。当您运行VideoPlayExample
应用时,您将能够看到其中的一些控件。同样,我们向VideoView
指出mc MediaController
负责管理vv VideoView
中显示的任何视频的回放的某些方面。
对vv.setVideoURI()
的调用构建了一个兼容的 URI,它引用了一个名为video_file.m4v
的视频,该视频已被放入项目的 raw 文件夹中。要想看到完全成型的 URI 是什么样子,可以在 Android Studio 中调试代码并设置断点。
视频的 URI 传递给了VideoView
,我们调用requestFocus()
来确保小部件获得焦点,然后通过调用start()
方法开始回放。假设所有的工作都按照描述进行,你应该会看到视频开始播放,如图 14-2 所示(至少是静态截图)。
图 14-2
视频播放示例在播放过程中显示视频
我们逻辑的最后一部分是doStopVideo()
方法,它被调用以响应检测到用户点击stopVideo
按钮的onClick()
方法。在doStopVideo()
中,我们首先检查以确保VideoView
对象vv
已经被实例化,然后调用它的stopPlayback()
方法。
除了我们非常基本的显式控件之外,如果您在播放期间触摸VideoView
范围内的任何地方,您还将能够看到MediaController's
浮动 UI 元素出现在视频上。MediaController
播放控件将出现,如图 14-3 所示。
图 14-3
回放期间视图中的 MediaController 控件
理解关键视频概念
掌握了视频播放的基本技巧后,你可以通过几个途径来扩展你的视频技能。您可以尝试活动和 VideoView 和 MediaController 视图对象的进一步组合,以精确布局您想要的视频界面类型。您还可以将视频片段组合成更复杂的活动,例如应用或游戏的视频过场动画开场序列。
另一条你可以也应该同时采取的途径是,确保你在数字视频的基础方面有良好的基础,这样你就可以在构建你的 Android 应用时,对内容、大小、质量和用途做出好的选择。接下来,我们将介绍您应该了解的关键概念。
比特率
我们在第十三章讨论音频时引入了比特率的概念。从概念上讲,比特率代表视频的同一个方面——在任何给定的时刻,可用于代表视频各个方面或视频帧的数据量。视频的比特率通常由(至少)两个因素的组合来计算。首先,视频的分辨率是多少,换句话说,控制视频帧实际物理高度和宽度的水平和垂直像素密度是多少?第二,在整体分辨率中,有多少位信息用于描述每个给定像素的颜色、色调和饱和度?
一般来说,正如我们在音频中看到的那样,更高的比特率意味着更好的保真度,这通常会导致观看视频的人感受到更好的质量。权衡也是一样的:更高的比特率需要更多的存储,因为每个视频帧要编码更多的信息。这就引出了对帧率的讨论。
帧频
视频的帧速率几乎是不言自明的。以什么样的速率显示图像帧,人类视觉暂留效应会让我们误以为图像在移动?Android 支持的大多数视频编解码器(在下面讨论)默认为每秒 30 帧。对较低和较高的帧速率有一些支持,但通常只在特殊情况下使用。
编解码器
与音频领域形成鲜明对比的是,Android 支持大量的音频编解码器,而在视频领域,Android 设备支持的视频编解码器和视频容器格式并不多。造成这种情况的原因是既得利益、专利法、行业卡特尔以及供应商和许可证持有者的可疑优先权,这些因素几乎从来没有将用户放在第一位。
我将很快提供我对这些激励因素和限制的看法,但在撰写本文时,这里是对 Android 中视频编解码器支持的客观看法。现代 Android 设备和 Android 操作系统本身支持以下编解码器的视频回放:
-
H.263:由视频编码专家组开发,旨在成为一种低比特率压缩格式。
-
H.264(基线和主要配置文件):旨在提高 H.263 的质量,同时降低比特率和文件大小,H.264 近年来一直主导着视频编解码器领域。
-
H.265:作为 H.264 的继承者,H.265 也被称为 HEVC,或高效视频编码方案。它的设计者认为它将接替 H.264 成为最受欢迎的编解码器,但它的专利阻碍意味着许多在线、媒体和技术公司寻求不同的道路,专注于 AV1 这样的编解码器(在下文中讨论)。
-
MPEG-4 SP:这是一种特殊的编解码器,不要与容器格式 MPEG-4 混淆。您会发现自称为 MPEG 或 MPEG-4 的文件实际上具有 H.263、H.264 或 H.265 编码的视频。他们声称是基于容器格式的 MPEG 或 MPEG-4(其全称是 MPEG-4 part 14),而不是视频编解码器。下面将详细介绍这种区别。
-
VP8:由 On2 Technologies 创建,专门设计为现代使用的更有效的编解码器。当 On2 Technologies 被 Google 收购时,该编解码器作为一个开放的、免版税的编解码器重新发布。
-
VP9:On2 Technologies 的 VP8 编解码器的后继者,提供增强的编码和解码性能。
-
AV1:许多公司在技术和媒体领域的最新合作。AV1 最大的优势在于它是一种“无负担”的格式,这意味着使用它不需要向专利持有者支付版税。
这似乎是一个范围广泛的视频编解码器列表。事实上,这是一个非常有用的设置,可以让你处理各种视频。然而,今天有更多的视频编解码器在使用,包括许多非常流行的,许多容器格式,Android 也只支持其中的一部分。大多数限制和不兼容性与技术无关,而与专利和许可制度以及来自电子、电影和娱乐等行业的一些利益方的反复无常的本性有关。
了解视频容器和子编解码器的复杂世界
为了更好地为未来以视频为中心的开发做准备,深入研究视频容器、格式、编解码器和字幕绝对是值得的,这样您就可以了解视频到底是什么,以及您可能使用和分发的媒体内容中包含和不包含什么。
您可能认为是视频文件的文件,即带有类似。mp4, .m4v, .avi, .mov, .mkv
等等——是潜在的许多视频、音频和字幕资源的表示,打包到一个容器中。容器命名法用于表示文件的专有、行业标准或开放格式,这意味着文件的消费者可以找出视频内容、音频内容和字幕在文件中的位置。
图 14-4 给你一个视频容器格式及其包含的媒体的直观概述。
图 14-4
视频容器格式的概念视图
如您所见,您的视频文件实际上是以下四个方面的组合:
-
整体容器格式,Android 支持 MPEG-4 part 14、Matroska、3GPP 和 WebM,但不支持其他流行的容器,如 AVI
-
使用支持的编解码器编码的一个或多个视频(如本章前面所述)
-
使用支持的音频编解码器编码的一个或多个音频流(如第十三章所述)
-
一个或多个字幕/说明资源
这些因素结合在一起,让你作为一个热衷于视频的开发者的生活变得更加复杂。如果复杂性到此为止,您可能会很高兴使用这些因素。但是还有一个更复杂的问题,那就是 Android 在给定的容器格式中支持的视频和音频编解码器的组合。不幸的是,事情并不都是即插即用的,您可以使用这里列出的选项的任何组合。相反,您应该始终参考 Android 开发者参考文档中当前支持的 Android 组合,您可以在 https://developer.android.com/guide/topics/media/media-formats
找到该文档。
那么,当出现您希望在应用中使用的优秀视频材质,而这些材质恰好位于不支持的容器中,或者当您希望使用不支持的编解码器或容器格式和编解码器的组合时,您该怎么办呢?很高兴你问了!在下一节中,我们将介绍一系列工具,您可以考虑将这些工具添加到您的开发人员工具包中,以提高您的视频内容创建、编辑和管理技能。
扩展开发人员视频工具集
作为一个狂热的 Android 开发人员,你会很快意识到像 Android Studio 这样的工具主要是围绕编码、布局和将代码转化为工作应用的工作流程而设计的。Android Studio 没有配备图形或视频创建或编辑等高级工具,这意味着作为一名开发人员,要构建您的选项,您应该寻找一些工具来补充您的 IDE,这些工具在视频编辑等方面表现出色。
查看您可以使用的视频编辑工具
当涉及到视频制作、视频编辑和视频内容管理领域时,作为开发人员,您可以选择的软件选项多得惊人。其中既包括传统的安装软件,也包括一些非常强大的工具的托管/云版本。
在决定采用和使用何种工具时,您需要问自己几个问题,关于您计划如何在应用中使用视频,因为这将相应地影响工具的选择。要问的一些关键问题如下:
-
我是将视频嵌入到我的应用中,从而将它作为原始文件或素材文件包含在内,还是从在线源进行流式传输?
-
我会创建和录制我自己的视频,还是使用其他来源的视频,并简单地合并我以这种方式获得的任何内容?
-
我是否需要管理一个视频或视频内容库,或者我的视频只是一个非常小的内容集,我可以以特别的方式进行管理?
-
我是否希望编辑、更改或以其他方式改变应用中包含的视频?
仔细考虑这些问题可以帮助您避免选择在您需要的领域没有优势的工具,更不用说通过选择更好的工具来帮助您避免金钱成本和开发时间。
我在下面概述了一系列当代的、流行的、免费的和商业的工具,您应该自己去探索,并判断哪一个或哪些工具最符合您的需求。然后,我使用 hand brake——一个非常受欢迎的免费开源工具——完成了 Android 开发人员最常见的视频工作流程之一:编辑现有视频以便在 Android 上播放。
流行的开源视频编辑套件
这个开源工具列表并不详尽,但涉及了一系列流行的视频编辑工具:
-
FFmpeg:一个非常强大的库和一组命令行工具,用于所有形式的视频编辑、代码转换等。在本书和其他地方提到的一些其他工具中,FFmpeg 也通常被认为是做艰苦工作的引擎。
-
VideoLAN VLC:一个视频播放、编辑和通用的万能工具,VLC 受益于可以在任何可以想象的操作系统平台上使用,包括 Android 本身。
-
手刹:一个单一用途,非常强大的工具。手刹的主要目标是提供最好的视频转码工具。它很好地做到了这一点——事实上,我们将在本章的后面探讨它的用法。
-
Kdenlive:一个非常出色的视频编辑和管理套件,也是最成熟的套件之一。
-
OpenShot:比 Kdenlive 更新,但目标是相同的综合视频编辑和管理工具集。
流行的商业视频编辑套件
-
Adobe Premiere:作为 Premiere 的长期用户,我可以告诉你,它有丰富的功能,与其他 Adobe 产品配合得相当好。它的定价通常会让爱好者或早期开发人员转向别处。
-
Final Cut Pro: Apple 的主要商业产品,一个非常强大的工具,但显然是为 macOS 社区设计的。
-
Lightworks:用于严肃视频编辑和管理的严肃工具,Lightworks 诞生于电影行业,是该领域事实上的标准。
-
da Vinci Resolve:Resolve 是一款更新、非常有趣的产品,采用“免费增值”模式,入门级是免费的(而且非常好),付费版本提供更复杂的特性和功能。
-
苹果 iMovie:如果你是一台 Mac 电脑的所有者,那么你的购买包括捆绑的 iMovie 软件。虽然这是针对家庭和消费者使用的,但它实际上是一个很好的产品,可以满足您最初的电影编辑和管理需求。很明显是 Mac 专用的。
视频编辑手刹介绍
为了让您熟悉视频编辑的工作流程,让我们回顾一下您可能会执行的最简单的任务,即将现有视频从某种任意的容器格式和视频编解码器转码为支持的容器和编解码器,以便在您的 Android 应用中使用。这类工作的突出工具之一是手刹,它类似于视频转码的瑞士军刀。其他工具也可以完成令人钦佩的代码转换工作,但手刹是最容易精通的工具之一。
下载和安装手制动器
我喜欢手刹的原因之一是它适用于所有主要的操作系统。无论你使用的是 macOS、Windows 还是 Linux,你都可以在你的系统上运行原生版本的手刹。
前往位于 https://handbrake.fr/
的手刹网站,下载适用于您的操作系统的软件包或软件包配置。在接下来的例子中,我们将使用 Ubuntu (Debian) Linux 安装,其中包括额外的配置包管理的步骤,以便为您下载手刹。之后的步骤基本上是相同的,不管你的平台是什么。
在 Debian 或基于 Debian 的系统上,按照网站上的说明将手刹库添加到您的库配置中,然后运行
sudo apt install handbrake-gtk
您也可以选择同时安装命令行版本,例如:
sudo apt install handbrake-gtk handbrake-cli
对于基于 RPM 的 Linux 系统,比如 Fedora、CentOS、Red Hat 等等,使用等效的yum
包管理命令。对于 Windows 用户,使用可下载的安装程序安装手刹。对于 macOS 用户,下载手刹DMG
镜像,双击挂载即可。然后将手刹应用拖到应用文件夹中。
跑步和使用手刹
根据您的操作系统启动合适的手刹,您应该会看到如图 14-5 所示的主屏幕。
图 14-5
手刹的主屏幕
正如您所料,在主屏幕的起点,您可以探索许多选项。为了演示手刹的主要功能,我们将选择一个现有的视频文件,以便我们可以将其转码为适合 Android 使用的格式。要选择文件,请单击手刹窗口左上角的“打开源代码”按钮。在我的例子中,我将选择一个我在另一个设备上捕获的文件,在我的例子中它被称为IMG_9111.MOV
。文件选择器如图 14-6 所示——如果你按照这些说明使用你自己的视频文件,显然你自己的源文件名会有所不同。
图 14-6
选择要在手制动中转码的源视频文件
手刹最好的功能之一是它能够确定源文件的容器格式、视频和音频的编解码器等等。您不会被要求指定这些输入参数中的任何一个,尽管您可以根据自己的专业知识来覆盖手制动中的内容。
选择源文件后,您主要关心的是指示 HandBrake 将视频代码转换为基于 Android 支持的编解码器的格式,并将其打包到也支持的容器格式中。手刹在这方面大放异彩,它预装了一系列现有选项,你只需点击几下就可以选择,以满足 Android 规定的格式和编解码器支持要求。
要让手刹使用 Android 的预配置目标格式,点击Preset
字段右侧的向右箭头。这会弹出一个菜单,选项包括General, Web, Devices
,等等。选择Devices
子菜单,将显示进一步的子菜单,类似于图 14-7 所示。
图 14-7
手制动装置子菜单中的预配置选项
如你所见,这里显示了许多预配置选项,适用于许多设备,而不仅仅是 Android 设备。我们对整体分辨率和帧速率之间的良好折衷感兴趣,以确保我们得到的转码视频不会太大。在我的例子中,我选择了Android 720p30
预配置选项,它将自动为我的转码视频设置这些质量:
-
Android 支持的容器格式:在本例中,这将是 MPEG-4 容器格式。
-
Android 支持的视频编解码器:
Android 720p30
预先配置的设置使用 H.264 作为视频编解码器。 -
Android 支持的音频编解码器:AAC 将被用作音频编解码器。
-
分辨率:这将允许视频每帧最多 720 个“渐进”扫描线(因此简称为 720p)。
-
帧率:每秒 30 帧。
我将指定一个目标文件来保存转码后的视频,在本例中称为IMG_9111.m4v
。点按“开始”按钮开始转码,当转码完成时,手刹会显示一条大胆的消息,告诉你放下咖啡,因为你转换的视频已经准备好了。
我的最终产品是一个 1.1 MB 大小的视频文件,在 Android 设备上播放时质量仍然非常好。这与 14 MB 的原始视频文件相比非常有利。
Android 视频更进一步
通过回顾本章开始时介绍的 VideoPlayExample 应用,您可以看到我的示例视频代码转换看起来有多好,以及视频播放的基本机制。我鼓励你用这个例子做更多的事情,感受视频在 Android 下的优势和挑战。首先,尝试替换你自己的视频,包括你用手刹或其他工具编辑和转码的视频。通过使用VideoPlayExample
作为各种视频实验的简单测试工具,您可以开始自己判断哪些方法适合您的应用。
第二件要做的事情是将VideoPlayExample
转换成使用 URI 并流式传输其源文件。从第十三章的音频流示例中复制逻辑,或者在本书的网站 www.beginningandroid.org/
查看更多视频流回放的示例代码。
摘要
在本章中,我们介绍了 Android 应用的视频世界,并展示了如何将基本的视频回放添加到您的应用中。您还看到了如何将其他视频编辑和创作软件添加到您的开发工具包中,从而极大地扩展了您对视频的处理能力。
十五、通知简介
历史上几乎每个操作系统都发明了一种机制来提醒你有趣的、重要的或紧急的通知。从计算“赞”到低电量警告,通知几乎是每个设备体验中无处不在的一部分。Android 提供了一个当代的通知生态系统,并在其通知框架中进一步发展了一些非常有用的功能。
每个 Android 设备,从手机到平板电脑到车载娱乐系统,都拥有一系列通知机制,我们将在本章中探讨这些机制。如果你使用过 Android 设备,你会对屏幕顶部或锁定屏幕上出现的托盘图标很熟悉。您还会看到弹出对话框通知,这有其优点和缺点。
除了软件的通知功能,Android 还提供了各种硬件选项,可以在派对上帮助通知。无论是通过振动来增加新通知的动力,还是通过触觉反馈来确保用户获得即时的触觉反馈,Android 都通过一个全面的框架为您的通知需求提供了统一战线。
配置通知
在使用应用的正常过程中,它有很多方法来呈现新的或重要的信息,以抓住用户的注意力。有时会发生一些值得注意的事情,但用户已经离开了应用,或者它在后台或暂停。对于像没有 UI 的服务这样的应用,没有正常的面向用户的可见性,所以当试图被注意时,在那些情况下存在额外的障碍。
Android 通过其NotificationManager
系统服务处理这些情况以及更多情况。通过将一个适当结构化的参数传递给一个getSystemService()
方法调用,您就可以通过您的应用使用NotificationManager
。这可以像下面的代码片段一样简单:
getSystemService(NOTIFICATION_SERVICE)
对getSystemService()
的调用将为您提供一个结果NotificationManager
对象,然后您可以访问它提供的通知管理方法。你将使用的一些最常见的NotificationManager
对象的方法有
-
顾名思义,这是一种根据你认为值得用户注意的任何触发或情况来激活通知的方法。它将一个
Notification
对象作为参数,该参数在有效负载中携带您的通知的细节——文本、图像等等,以及您希望 Android 通知基础架构警告用户的方式。 -
cancel()
:使用此方法来消除通知。Android 还可以取消通知来响应某些用户动作,包括像“滑动以消除”这样的手势。 -
cancelAll()
:核选项!当您只想让 NotificationManager 对象激活的所有通知都运行时,调用cancelAll()
。
使用通知对象自定义通知
在通知用户时,Android 中的默认通知行为能够完成你想要的大部分事情。但是有时候你想多走一步,真正地寻求用户的注意。对象拥有增强和定制通知的方法。
了解增强通知的新旧方式
Android 的通知已经随着时间的推移而发展,比如有最初的(旧的)增强通知的方法,使用单独的附加方法来调整和放大您的工作,以及最近的方法,使用一个NotificationBuilder
对象来一次性处理所有的定制。我将展示这两种方法,并指出,如果您的目标是版本 7 之前的旧版本 Android,旧方法更有可能满足您的需求。
给通知添加声音
让我们从旧的/传统的方法开始我们的通知探索,特别是 Android 对许多不同类型的通知的声音支持。通过在基本 Android 级别利用一系列用户可配置的声音,如果您不想管理音频资源,您可以避免管理音频资源。您可以让您的Notification
对象通过调用其.defaults()
方法来利用设备的默认声音(无论是否由用户配置),如下所示:
Notification myNotification = new Notification(...);
myNotification.defaults = Notification.DEFAULT_SOUND;
您可以采取额外的步骤,通过使用对音频资源的Uri
引用来提供您自己的声音,无论它是您提供的原始或素材管理的文件,还是对 Android 附带的许多声音之一的引用。
下面显示了一个使用普通 Android“ka limba”声音的示例,通过 ContentResolver 类使用该资源的 Uri 并相应地分配声音:
Notification myNotification = new Notificiation(...);
myNotification.sound = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE +
"://" +
getPackageName() +
"/raw/kalimba");
由于有多种触发通知的机制,因此了解优先级的等级很重要。使用.sound()
分配的任何声音通知(或其他使用其相关方法的通知形式)将被使用.defaults()
方法设置的任何等效通知覆盖,如果这样的调用包含通知类型的参数,如声音的标志DEFAULTS_SOUND
。无论调用这些方法的顺序如何,都会发生这种情况。
在 Android 新的通知世界中,你可以通过建立一个NotificationBuilder
对象来设置你想要使用的声音等属性,最后当你设置好所有想要的属性后,让Notification
使用NotificationBuilder
。使用新方法设置声音的等效工作如下所示:
Notification.Builder myBuilder = new Notification.Builder(this, ...)
myBuilder.setSound(Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE +
"://" +
getPackageName() +
"/raw/kalimba"));
使用设备灯通知
很少有安卓手机或平板电脑缺少内置 LED 灯作为前置显示屏的一部分。这种光可以有很多用途,包括作为通知用户通知的另一个载体(而不仅仅是让我,你信任的作者,在凌晨 3 点保持清醒)。通过基于通知对象的配置进行配置,可以以多种方式控制 Android 设备的内置灯:
-
那个。lights()方法在传递一个布尔值 TRUE 值时激活 LED。
-
在支持的设备上,您可以通过 ledARGB 参数和您希望使用的基于 RGB 的颜色的匹配十六进制代码来更改 LED 的颜色。
-
使用 ledOnMS 和 ledOffMS 值闪烁和循环灯光,以毫秒表示打开和关闭时间。
使用Notification.Builder
的较新 Android 版本的等效方法是.setLights()
方法。您可能开始猜测如何使用构建器方法从使用基本通知对象的旧方法中推断出新方法,反之亦然。
Note
通知。Android 8 引入了 Builder()方法,旧的通知样式已经过时。当 API 级别超过 24 时,应该总是使用通知。Builder()并让 Android 的兼容性库处理旧版本上的行为。
以及您希望使用的特定通知花体,确保设置Notification.flags
字段以包含Notification.FLASH_SHOW_LIGHTS
标志。在使用单色 LED 的基本设备上,您可能会发现您选择的颜色并不适用,相反,设备会改变 LED 的亮度。对于具有支持多色输出的 led 的设备,如果制造商没有为通知类提供必要的智能来控制颜色,也会出现这种情况。
还有一些设备没有用于通知的 LED,包括电视、Android 自动系统和 Android 的一些嵌入式应用。鉴于这种多样的设备状况,你应该考虑将闪烁 LED 通知作为一种额外的奖励,而不是一种吸引注意力的重要方法。
摇晃它!
你的用户拥有比视觉和听觉更多的感官,你可以用它们来吸引他们的注意力。当闪烁的灯光和朗朗上口的声音还不够时,你可以(一语双关)转向振动。Android 的原始通知模型包括一个默认标志,允许使用设备范围的默认设置来改变事情:
myNotification.defaults = Notifcation.DEFAULT_VIBRATE;
新的通知方法使用 Builder 对象上的.setVibrate()
方法来达到同样的效果。
要让任何基于振动的通知实际触发物理振动,您需要在清单中拥有以下权限:
<uses-permission android:name="android.permission.VIBRATE" />
当默认振动不够时,您可以通过.vibrate()
和.setVibrate()
方法执行自定义振动,提供一个以毫秒为单位的long[]
值,例如:
new long[] {1000, 500, 1000, 500, 1000}
是一个有效的序列,将触发三次一秒钟长的振动,每次振动之间有半秒钟的间隔。
添加通知图标
到目前为止,我们介绍的通知方法都是为了抓住用户的注意力。Android 还提供了使用图形的能力,以图标的形式,向用户提供关于通知的更多信息和上下文。
图标是图像文件,因此被认为是用于 Android 资源管理目的的可绘制图标。您需要提供一个contentIntent
值,当用户实际点击您在通知中提供的图标时,该值作为PendingIntent
传递。这个PendingIntent
充当一个占位符和延时功能,允许一个意图被准备好,以便它可以在以后被一个活动或另一种技术触发。
Understanding Pendingintent
挂起内容是 Android 使用的一种机制,用于提前向设备上运行的另一个应用或服务传递令牌或权限。有了 PendingIntent,接收应用就可以在将来的某个时候运行从您的应用中选择的一段代码(无论您的应用本身是否正在运行),并使用您的应用的权限来这样做。
除了能够添加您选择的图标和相关的contentIntent
,您还可以添加带有tickerText
属性的简短文本描述。此文本应该用于您希望用户看到的通知文本的最重要部分,如发送消息的联系人姓名、电子邮件的主题、社交媒体帖子的标题等。setLatestEventInfo()
方法允许您在一次调用中指定全部三个icon
、contentIntent
和tickerText
。
无论您使用的是旧的还是新的通知模型,这种PendingIntent
方法都适用。
不同 Android 版本的图标大小
添加图标可以让你把图标调整到你想要的艺术水平,但是对于不同版本的 Android 你需要记住一些注意事项,因为这些会影响所支持的图标图像的分辨率。
为了最大化您可以支持的 Android 设备范围及其相关的通知样式和大小,您应该创建至少四个代表您的图标的 drawables:
-
一个 12 像素乘 19 像素的边界框,包含一个 12 像素的正方形图标,用于低密度屏幕。这个图标将被放置在 res/drawable-ldpi-v9 项目文件夹中。
-
一个 16 像素乘 25 像素的边界框,包含一个 16 像素的正方形图标,用于中等密度的屏幕。这个图标将被放置在 res/drawable-mdpi 文件夹中。
-
一个 24 像素乘 38 像素的边界框,包含一个 24 像素的正方形图标,用于高密度、超高密度和超高密度屏幕。该图标将放置在 res/drawable-hdpi-v9、res/drawable-xhdpi-v9 和 res/drawable-xxhdpi-v9 文件夹中。
-
对于 2.3 之前的所有 Android 版本,这是一个 25 像素的正方形(不考虑这些旧设备上的实际屏幕密度)。这将被放置在 res/drawable 资源文件夹中。
这些变化会随着时间的推移而改变,正如 Android 支持不同分辨率的建议方法一样,所以请务必在 Android 开发者网站上查看图标样式的详细信息。该网站包括一些有用的信息,关于放大和缩小你决定不会或不能为你的应用提供任何预期的保真度水平的可绘制图形。如果你跳过了这些图标中的一个,不要惊慌,但是要注意,Android 会尝试缩放你的另一个图标来填补空白,并且最终的屏幕图像可能不会很棒。
添加信息的浮点数
通知还有最后一个变化,你可能已经看到了,也可能依赖于它。这是应用启动器图标上的“数字”,它提供了给定应用的类似通知或未读/未回复通知的数量。
浮点数调整是通过使用Notification
对象的一个名为number
的公共数据成员来实现的,您可以将它设置为您希望的任意数字。它将在图标的右上角或左上角显示为应用启动器图标的覆盖图(取决于设备上的区域设置和从右到左或从左到右的约定)。默认情况下,这个值是不设置的,并且被 Android 忽略,除非你为 number 设置一个值。
在 API 级别 26 中引入通知通道
随着 API 26 的出现,Android 抛弃了所有应用和服务的公共通知空间的概念,引入了通道的概念。通道的目标是允许用户(以及隐含的应用)将通知分组并划分到不同的组中,然后以不同的方式对待这些组。
这方面的一个经典用例是让一些通知被认为是信息性的,在“正常”时间显示,但在用户指定一个勿扰时段时隐藏。其他通知可以分配给“紧急关注”或“紧急情况”的频道,并以不同的方式处理。
随着谷歌不断调整和改变通知格局,渠道概念在现实生活中的应用并不全面。然而,作为一名开发人员,如果您在 Android、10.0、11.0 或更高版本的新设备上使用任何形式的通知,您需要考虑这一点。
本章后面的NotificationBuilderExample
展示了在新旧 API 级别支持的新旧 Android 设备上定义和使用通道并处理行为是多么简单。
实际通知
现在,您已经介绍了 Android 许多版本所依赖的最初的、仍然有用的通知概念。让我们来看看在NotificationBuilderExample
应用中使用的通知,您可以在ch15/NotificationBuilderExample
项目文件夹中找到它。
我使用了简单的布局,所以为了节省空间,这里省略了它的 XML。可以看到图 15-1 中的 UI。
图 15-1
基本的 NotificationBuilderExample 布局,不显示任何通知
创建通知的支持逻辑
NotificationBuilderExample
的核心是在与 UI 交互时给用户带来通知的代码,如清单 15-1 所示。
package org.beginningandroid.notificationbuilderexample;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private static final int NOTIFICATION_ID=12345;
private static final String MYCHANNEL = "";
private int notifyCount = 0;
private NotificationManager myNotifyMgr = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myNotifyMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O &&
myNotifyMgr.getNotificationChannel(MYCHANNEL)==null)
{ myNotifyMgr.createNotificationChannel(new NotificationChannel(MYCHANNEL,
"My Channel", NotificationManager.IMPORTANCE_DEFAULT));
}
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.notify:
raiseNotification(view);
break;
case R.id.clearNotify:
dismissNotification(view);
break;
}
}
public void raiseNotification(View view) {
Intent myIntent = new Intent(this, NotificationFollowon.class);
PendingIntent myPendingIntent = PendingIntent.getActivity(MainActivity.this, 1, myIntent, 0);
NotificationCompat.Builder myNotifyBuilder = new NotificationCompat.Builder(MainActivity.this, MYCHANNEL);
myNotifyBuilder.setAutoCancel(false);
myNotifyBuilder.setTicker("Here is your ticker text");
myNotifyBuilder.setContentTitle("An Android Notification");
myNotifyBuilder.setContentText("Notice This!");
myNotifyBuilder.setSmallIcon(R.drawable.wavinghand);
myNotifyBuilder.setContentIntent(myPendingIntent);
myNotifyBuilder.build();
Notification myNotification = myNotifyBuilder.getNotification();
myNotifyMgr.notify(NOTIFICATION_ID, myNotification);
}
public void dismissNotification(View view) {
myNotifyMgr.cancel(NOTIFICATION_ID);
}
}
Listing 15-1Implementing the code for NotificationBuilderExample
虽然这里有相当数量的代码,并且在配套的NotificationFollowon
类中,其中的大部分您应该已经很熟悉了。在onCreate()
中设置活动执行恢复或创建状态和扩展布局的常规任务,另外还要创建绑定到系统通知基础设施的myNotifyMgr
对象。NotificationBuilderExample
类本身也为通知设置了一个虚构的 ID 和一个计数器来跟踪有多少未决通知。请注意,您可以很容易地决定让您的应用发出多种不同类型的通知。如果您决定这样做,请确保使用不同的 ID 来区分每种类型。
onCreate()
中的另一个主要逻辑执行必要的 SDK (API)级别检查,以查看是否有必要使用通知通道来向用户显示所需的通知。如果 SDK 版本处于或高于通道被授权的级别,我们检查MYCHANNEL
是否存在(null
比较),如果它还不存在,我们实例化它以备使用。如果它确实存在,就不需要额外的工作——例如,如果我们已经使用了该应用,并在触发通知至少一次后让它继续运行。
onClick()
方法是我用来将按钮点击处理分组在一起的熟悉模式——尽管在这个例子中,你可以很容易地让每个按钮直接调用相关的raiseNotification()
和dismissNotification()
方法。正是这些方法的实现包含了我们感兴趣的通知逻辑。
在raiseNotification()
方法中,我们执行本章开始时描述的几乎所有可选配置和定制。首先,我们创建一个指向NotificationFollowon
活动的PendingIntent
。如果用户决定点击通知抽屉中的挥动的手图标,这将被触发。
接下来,创建Notification
(或者在本例中是NotificationCompat
) Builder
对象,我们将从这个构建器实例生成的任何结果通知分配给在onCreate()
方法中设置的MYCHANNEL
通道。
然后我们开始使用myNotifyBuilder
对象,为我们将构建的最终Notification
对象添加许多通知功能:
-
。调用 setTicker()来提供一些 Ticker 文本。
-
。调用 setNumber()来增加引发通知的次数。
-
。调用 setSound()并从 raw 文件夹中获取 pop.mp3 声音作为资源。
-
。调用 setVibrate()时,节奏为振动开启 1 秒,关闭半秒。
-
。调用 setAutoCancel()来禁用自动取消选项。
配置好所有选项后,我最终将构建的当前状态传递给myNotification
对象,然后将Notification
和NOTIFICATION_ID
传递给NotificationManager
以呈现给用户。
从用户的角度查看通知
在虚拟设备上运行NotificationBuilderExample
应用提供了大部分通知体验(振动往往是 avd 处理不好的一件事)。图 15-2 显示了出现在主屏幕图标栏中的通知。
图 15-2
左上角触发的通知——挥动的小手图标
如果你仔细看,你会在屏幕顶部看到一个小的挥手图标。如果在打印(或屏幕)页面上很难看到,请确保自己尝试运行该示例,以在自己的虚拟设备上看到它。根据 AVD 支持的 API 级别,您可能会看到也可能看不到与通知对象相关联的附加状态文本。
单击 Clear Notification 按钮会使图标消失,如果您在 pop 声音仍在播放或真实设备仍在振动时足够快地单击它,这些额外的自定义也会停止。
通知贯穿于NotificationBuilderExample
活动的整个生命周期,甚至在您将它发送到后台以便使用其他应用或返回到启动程序主屏幕之后。自己尝试一下,你应该还能看到如图 15-3 所示的通知图标。
图 15-3
通知图标即使在离开活动后仍然存在
一旦发出通知,在此后的任何时间点,用户都可以访问通知抽屉,该抽屉从设备上的所有应用收集所有通知(如果使用足够新的 API 级别和兼容的设备,还可能通过通道对它们进行分组)。在 Android 中,你可以通过“抓住”屏幕顶部的栏并一直拖到底部来打开通知抽屉。在图 15-4 中,您可以看到我添加了附加细节的示例通知,包括通知标题和附加文本。
图 15-4
通知抽屉打开,显示我们的通知
您在通知抽屉中看到的内容直接取决于正在使用的 Android 的版本。上图中的示例来自 Android 10.0 AVD 实例,我们的图标是从适当的屏幕密度资源中选择的,或者是从与应用打包在一起的最近的可用资源中缩放的。我们 25×25 像素的挥动的手在通知抽屉中更容易辨认。显示了标题和附加文本,以及在创建时传递给Notification
对象的时间戳。
你还会注意到在较新的 Android 版本的通知抽屉中显示的数字值,而不是图标栏中图标的覆盖图。谷歌对 Android 进行了这一改变,以处理开始蔓延到较小手机屏幕上的图标显示的混乱。通过将号码转移到通知抽屉,人们的启动屏幕变得不那么拥挤了。
用户可以单击图标来触发后续活动,或者简单地取消通知,就像他们在活动主页上单击清除通知按钮一样。在 Android 的最新版本中,你会在通知下方看到三个稍微偏移的水平条,它们是“全部忽略”选项。甚至新版本的 Android 也会在通知抽屉底部显示“全部清除”和“管理”选项。在我们的示例中,无论您选择哪种清除技术,都将在所有活动的 NotificationManager 对象上触发cancelAll()
方法,从而完全清除流程中的通知抽屉。
一个完全清晰的通知抽屉看起来如图 15-5 所示。
图 15-5
从设备中清除的所有通知
请注意,以这种方式清除通知不一定会清除您可能在应用中跟踪的通知计数。请记住,即使你已经脱离了应用,Android 也不一定会触发onDestroy()
或从应用中获取资源。
摘要
你现在已经有了 Android 通知系统的坚实基础和使用通知的经验。还有更多扩展通知及其用途的高级主题,您可以在本书的网站 www.beginningandroid.org
上找到。这些高级通知主题包括时间线通知、相关通知组或集合的捆绑通知、可扩展通知以及针对 Wear OS 和 Android 嵌入式应用(如 Android Auto)的专用通知类型。
十六、通过呼叫探索设备功能
我们已经在本书中介绍了所有的想法,并且在后面的章节中还会介绍,因此很容易一头扎进各种只处理 Android 提供的强大软件平台的应用开发中。事实上,这太容易了,以至于你经常忽略了作为一个 Android 开发者的另一个巨大的机会:也使用 Android 硬件。
在本章中,我们将简要介绍如何开始使用设备功能,特别是通话和传感器。这将是非常简短的,但应该给你一个开始,继续学习更多关于硬件能力的独立知识。
发号施令
Android 已经从早期的全手机时代走了很长一段路。正如我在第一章中概述的那样,随着现在使用 Android 的设备和外形的爆炸式增长,提前思考您的应用如何以及为什么可能想要添加电话支持,以及它如何适应“没有电话”的环境和设备是值得的
指定电话支持
要向 Android 标记您的应用需要访问与电话相关的硬件特性,您应该将以下硬件要求条目添加到您的AndroidManifest.xml
文件中:
<uses-feature android:name="android.hardware.telephony" android:required="true" />
android.hardware.telephony 功能表明您的应用需要蜂窝接入和支持,并意味着用户可以提前了解该应用是否适合他们的设备,例如在 Google Play 上搜索或下载应用时。
使电话支持成为可选的
如果你觉得把整个应用挂在是否允许蜂窝接入上有点极端,那你就对了。如果您正在考虑构建一个应用,其中电话接入是额外的好处,但不是硬性要求,那么您可以利用技术在应用运行时检查应用逻辑中的电话支持,并处理拥有和不拥有蜂窝硬件接入的情况。
Android 使用PackageManager
类来帮助检测各种硬件,从加速度计和麦克风一直到手机硬件的功能。最常见的方法是使用hasSystemFeature()
方法,如清单 16-1 中的伪代码片段所示。
PackageManager myDevice = getPackageManager();
if (myDevice.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) {
// the user's device has telephony support
// add your call-related logic here
} else {
// the user's device lacks telephony support
// do something that doesn't require making calls
};
Listing 16-1A code fragment showing detection of cellular hardware access
您还可以检查电话支持的其他有用方面,如网络类型和语音实现,例如 VoLTE、LTE、GSM、CDMA 等。
打电话
现在您已经有了确定设备调用支持的需求或可取性的机制,您可以开始在您的应用中利用这一点。令人欣慰的是,Android 让这一点变得非常简单,这要归功于其作为智能手机操作系统的根基——重点是在手机上。
Android 让通话和电话的其他方面变得容易访问和使用的非常有用的方法集中在TelephonyManager
类上。顾名思义,TelephonyManager 负责一系列呼叫管理和相关任务,包括呼叫处理、呼叫状态、网络细节等。您通常会发现自己在使用这些方法:
-
getPhoneType()
:返回电话和网络的详细信息,包括对 GSM、LTE 等的无线电支持。 -
getNetworkType()
:该方法提供了当前连接的蜂窝网络的数据能力的详细信息。这有助于理解网络类别,如 LTE、4G、3G 和其他变体。 -
getCallState()
:这是一种非常方便的方法,可以帮助您确定手机是否空闲(未在通话中)、处于通话设置模式还是正在通话中——即所谓的“摘机”或“摘机”
要真正拨打一个号码并开始通话,您可以调用ACTION_DIAL
或ACTION_CALL
意图之一。这些方法的使用和区别将很快被介绍。这两种方法都有一个共同的出发点,那就是将代表用户希望呼叫的号码的Uri
作为格式为tel:
nnnnnnnn
的字符串。在 Uri 字符串中,nnnnnnnn
代表要呼叫的电话号码的数字。观看实际操作过程将有助于清楚地了解拨打电话的步骤。
Caution
在发起任何新的呼叫之前,检查当前的呼叫状态是一个很好的做法。Android 有一系列选项可以同时处理多个来电和去电,但这个话题完全属于高级蜂窝争论的范畴,超出了本书的范围。现在,好好利用getCallState()
方法,把你的行动建立在它带来的结果上。
布局 CallExample 应用
打电话应用的工作示例可以在Ch16/CallExample
项目中找到。这个项目使用一个非常简单的布局,让您可以专注于它所公开的呼叫和拨号选项。首先,您可以看到清单 16-2 中使用的布局。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Phone Number:" />
<EditText
android:id="@+id/phonenumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
<Button
android:id="@+id/usedialintent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Call with ACTION_DIAL"
android:onClick="callWithActionDialIntent" />
<Button
android:id="@+id/usecallintent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Call with ACTION_CALL"
android:onClick="callWithActionCallIntent" />
</LinearLayout>
Listing 16-2The layout of the CallExample application
查看布局中指定的字段,您会看到一个TextView
和EditText
的组合,它充当用户指定他们希望呼叫的号码的输入字段。然后有两个按钮,"Call with ACTION DIAL"
和"Call with ACTION_CALL
,你可以猜到,这两个按钮触发各自的方法来激发每种类型的意图,从而打电话。
你可以在图 16-1 中看到最终的渲染布局。
图 16-1
用于拨打用户指定号码的 CallExample 布局
除了展示呼叫功能所需的部分之外,我特意选择了一些新功能包含在此布局中——让我们先把这些功能去掉。为了有一点变化,我将这个布局基于一个LinearLayout
。您将看到 id 为phonenumber
的EditText
视图具有属性inputType="number"
。这将触发 Android 修改该视图的输入法参数,以便只为数字和一些有限的标点符号提供输入。您可以在运行应用时看到这种效果,因为为输入显示的虚拟键盘(或输入法编辑器(IME))看起来像电话拨号盘,而不是完整的键盘。
The Android Input Method Framework
Android 提供了一个非常强大的抽象层来处理用户输入,以便它可以灵活地与物理键盘、屏幕上出现的软键盘甚至手写识别硬件和软件一起工作。这是输入法框架。
每当您使用一个触发用户输入的视图时,您通过被呈现一个默认的编辑器视图——IME——来隐式地使用框架。您可以使用默认设置,并像我们在电话拨号示例中所做的那样进行配置,也可以用于其他常见情况,如日期输入。您还可以自定义任何 IME 来添加或限制编辑器中显示的供用户“按”的“键”或值。你可以在这本书的网站上,在 www.beginningandroid.org
阅读更多关于输入法框架和 ime 的内容。
usedialintent
和usecallintent
按钮具有这些 id,当每个按钮被点击时,它们会给出一个强烈的提示。使用第一个按钮"Call with ACTION_DIAL"
将遵循触发ACTION_DIAL
意图的代码路径,并且"Call with ACTION_CALL"
将类似地触发ACTION_CALL
意图。我们将在第十七章更详细地讨论意图,但是现在,这两者之间有什么区别呢?
带着一个ACTION_DIAL
意图,Android 被通知它需要向用户显示一个 IME,以在幕后启动电话魔法之前确认(或调整)要呼叫的号码。在本章后面的图 16-2 中可以看到这一点。在另一种情况下,触发ACTION_CALL
意图会立即使用 Uri 中提供的号码发起呼叫,而无需任何进一步的用户界面或确认。
图 16-2
调用示例应用触发 ACTION_DIAL
这两种不同的方法有许多原因,但主要原因是确保用户知道呼叫即将开始,并通过ACTION_DIAL
为他们提供对过程的控制。当你考虑到这是 Android 的内置部分之一,可能会花费用户真正的金钱时,这是非常重要的。在某些地方打电话很便宜,但在许多国家和地区,打电话仍然是一笔不小的开销。
因为ACTION_CALL
在没有ACTION_DIAL
提供的确认步骤的情况下立即进行呼叫,Android 为ACTION_CALL
提供了一种保护措施,要求在应用中使用它必须在其清单中有CALL_PHONE
的许可,然后带有ACTION_CALL
意图的startActivity()
呼叫才会起作用。还要注意,CALL_PHONE 权限被认为是最高级别的权限,因为有可能被滥用,因此,不仅您必须在应用清单中拥有此权限,而且在运行时,您的用户还会被提示允许应用进行呼叫。可以说是纵深防御。
CallExample 应用的工作逻辑
了解了通话选项并准备好向用户展示简单的布局后,是时候看看让通话变得生动的逻辑了。清单 16-3 展示了为我们的CallExample
应用插入逻辑的 Java 代码。
package org.beginningandroid.callexample;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void callWithActionDialIntent(View view) {
EditText targetNumber=(EditText)findViewById(R.id.phonenumber);
String dialThisNumber="tel:"+targetNumber.getText().toString();
startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse(dialThisNumber)));
}
public void callWithActionCallIntent(View view) {
EditText targetNumber=(EditText)findViewById(R.id.phonenumber);
String callThisNumber="tel:"+targetNumber.getText().toString();
//the following intent only works with CALL_PHONE permission in place
startActivity(new Intent(Intent.ACTION_CALL, Uri.parse(callThisNumber)));
}
}
Listing 16-3Java logic for the CallExample application
我通常使用一个onClick()
方法来指导随后的执行,在这个例子中,我们使用一个 switch 语句作为参数,在这个例子中,我们分别从每个按钮的布局配置中直接调用方法callWithActionDialIntent()
和callWithActionCallIntent()
。
每种方法都做一些类似的处理,首先确定用户在哪个View
(在我们的例子中是EditText
)中输入了想要的电话号码。然后用适当的格式创建Uri
字符串,然后调用startActivity()
,用期望的意图和Uri
作为参数。
拨打操作拨号电话
在图 16-2 中,可以看到用户点击useDialIntent
按钮的结果。来自EditText
字段的数字(如果有的话)已经在我们的callWithActionDialIntent()
中被构造成一个幕后的Uri
,并且ACTION_DIAL
意图已经被触发。
你在图 16-2 中看到的拨号器看起来与图 16-1 中显示的 IME 很接近,但不完全一样。您应该注意到的主要区别不仅包括号码样式、配色方案等的细微差别,还包括添加选项,例如将该号码添加为联系人的能力。你可能已经猜到这是通过激发另一个意图来实现的。
您还会看到数字和任何相关标点符号(如连字符和括号)的格式差异。这些都将根据设备的位置和语言设置进行样式化。用于图 16-2 所示视图的我的 AVD 使用美国地区和英语作为语言,因此你看到的格式的前三个数字被视为区号并放在括号中,数字的连字符是美国和加拿大惯用的。
最后,也是最重要的,是显示屏底部的绿色电话软键,你可以猜测它实际上是用来触发呼叫的。
拨打行动电话
只要按下【开始】按钮,你就能看到——嗯,它也能看到 Android 的拨号屏幕,其他什么都没有。上例中的拨号器未被触发,因此几乎没有其他内容可以显示或解释。你的用户会直接进入“实际打电话”拨号画面,如图 16-3 所示。
图 16-3
使用 ACTION_CALL 在行动中呼叫
处理来电
处理来电比接听电话要复杂得多,超出了本书的范围。但是,并不总是需要对来电承担全部责任,当接到电话时,您可以让您的应用做其他有用的事情,即使您的应用不是处理管理对话的主要任务的应用。
让辅助应用响应传入呼叫的主要方法是在 AndroidManifest.xml 文件中为广播目的注册一个广播接收器。我们将在下一章更深入地探讨广播接收机。现在,知道ACTION_PHONE_STATE_CHANGED
意图是由TelephonyManager
框架在收到调用时触发的就足够了。清单 16-4 展示了清单文件中的接收者声明。
<receiver android:name="MyPhoneStateChangedReceiver">
<intent-filter>
<action
android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
Listing 16-4Setting the receiver for incoming calls in AndroidManifest.xml
当对设备进行调用时,TelephonyManager
触发 intent,任何接收者——包括您的接收者——通过使用对指定的相应方法的回调得到通知。ACTION_PHONE_STATE_CHANGED
intent 还可以包括两个可选的数据片段,您可以使用它们来驱动您的逻辑。一个是呼叫的状态值,如CALL_STATE_OFFHOOK
或CALL_STATE_RINGING
,表示呼叫已被应答或仍在触发等待应答的振铃器。如果使用了CALL_STATE_RINGING
值,还有一个可选的附加值EXTRA_INCOMING_NUMBER
,它提供主叫方 ID(如果网络已经提供的话)。
清单 16-5 是MyPhoneStateChangedReceiver
类的一个示例 Java 方法,它可以让你知道什么时候可以进行回调。
public class MyPhoneStateChangedReceiver extends BroadcastReceiver {
@override
public void onReceive(Context context, Intent intent) {
String deviceCallState = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
if (deviceCallState.equals(TelephonyManager.EXTRA_STATE_RINGING) {
// The phone is still ringing and might have the caller ID
String callerID =
intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
// Try to display the number, etc.
} else {
// do something else
}
}
}
Listing 16-5A Java method fragment for working with an incoming call
与其他硬件功能一样,处理来电被视为敏感任务,需要明确的安全权限。您的应用将需要清单文件中的READ_PHONE_STATE
权限,以便接收ACTION_PHONE_STATE_CHANGED
意图。
十七、理解意图、事件和接收者
当你深入到本书的后半部分时,你已经接触到了许多设计和构建活动的技术。您已经牢牢掌握了活动生命周期,以及在活动开始、等待、暂停和最终结束的各个时间点触发的回调。到目前为止,大多数示例应用都只包含一个活动。这与我在本书开始时所做的评论——创建、使用和处理活动的成本很低,并且您应该多产地使用它们——有什么关系呢?
很高兴你问了!我们希望确保您可以轻松使用和部署多项活动。虽然早期的视频播放器、电话拨号器等示例都是很好的例子,但是这些示例应用都是在应用启动时启动其单个活动的,方法是在 AndroidManifest.xml 文件中指定 activity,并在名为
介绍 Android 意图
关于在应用中使用多个活动的谜题的答案在于 Android 的基于事件或基于消息的系统,即所谓的“意图”。正如许多其他操作系统一样,如 Linux、Windows 和 macOS,它们基于发送和响应事件,Android 使用许多类似的概念,其意图包括触发和响应需要向应用呈现不同活动的操作。这在 Android 中有一些细微的差别,所以请继续阅读!
最简单的形式是,意图是从 Android 上的应用或服务发送的消息,表明该应用或服务的用户想要做些什么。那个“某物”可能是一个非常众所周知的动作,比如根据用户的动作准确地知道向用户显示哪个活动。但是 Android 也提供了从基础平台和其他应用中使用其他活动的可能性,从你自己的应用中。在这种情况下,如果有多种方式来满足用户的意图,您可能无法控制其他可用的活动,也无法控制设备用户可能更喜欢哪些活动。Android 通过使用消息系统的匹配部分(称为接收者)涵盖了已知和未知选项。
接收者的工作——不管是你写的还是其他应用的——是倾听意图,如果接收者有能力处理这种被请求的意图,就以各种方式做出回应。在本章的后面,在我们介绍了意图的机制之后,我们会谈到接受者。意图和接受者共同构成了触发连续活动的中心机制,并将应用中想要的所有活动连接在一起,以创建最终的体验。另外,这种相同的机制允许您利用其他应用的活动,只要这对您的应用有意义。
理解意图行为
剖析 Android 意图的两个基本部分是用户或应用所期望的动作和触发该期望动作的上下文。当我们谈论期望的行动时,我们指的是一些简单的概念,如“查看这个东西”、“制作一个新的”,等等。我们将很快涵盖更详尽的行动清单。就上下文而言,它可以更多样,最好被认为是一系列有助于理解意图的支持数据,以及任何细微差别或特殊情况,因此它可以得到适当的指导,并为最终的活动服务。
这种支持数据的概念采用了Uri
的形式,比如content://contacts/people/4
,这是 Android 联系人存储中第四组联系人详细信息的Uri
符号。如果你把那个Uri
和一个类似ACTION_VIEW
的动作搭配起来,你就拥有了意图的所有基本要素。Android 解释了这一意图,并将找到一个能够向用户显示(或向视图提供)一组联系信息的活动。相反,如果你在指向一个集合的Uri
上做类似于ACTION_PICK
的动作——content://contacts/people
——Android 将寻找任何可以呈现多个联系人的活动,并提供从中进行选择的能力。
虽然与动作捆绑在一起的Uri
是意图的最简单形式,但这并不是可以包含的内容的限制。你可以在你形成的任何意图中包括四个额外的方面,以扩展意图有效载荷,并帮助改善 Android 和应用可以利用意图及其数据有效载荷做什么。这些都是意图对象的一部分:
-
意图类别:意图的类别有助于定义什么活动可以满足其基本动作。举个例子,你希望你的用户开始与你的应用交互的“主要”活动将属于类别
LAUNCHER
。通过这种方式,你向 Android 发出信号,该活动适合包含在启动器菜单(你的 Android 主屏幕)中。其他活动/意图类别包括DEFAULT
和ALTERNATIVE
。 -
MIME 类型规范:有时不可能知道或定义一个特定的 Uri 用于一组项目,比如联系人或照片。为了帮助 Android 找到一个合适的活动来处理这些情况下的数据集,您可以指定一个 MIME 类型,例如,image-jpeg 用于图像文件,以帮助处理照片集。
-
组件提名:Android 的优势之一是能够在运行时利用您在构建应用时不知道的活动。但是在其他时候,您确切地知道您想要调用哪个活动。一种方法是在意图的(期望的)组件中指定活动的类。通过这种方式,您不需要添加其他上下文提示来希望触发正确的意图,代价是承担关于组件类实现的假定知识的风险。这违背了被称为封装的面向对象编程的原则。
-
额外内容:有时你有其他上下文线索和数据,出于各种原因,你想让接收者知道。当这不完全符合一个命名方案时,临时演员会来帮忙。这是一个简单的 Bundle 对象,可以包含您想要的任何内容。一个警告是,提供这样一个包并不能保证接收者会使用它。
适合各种场合的有意行动
你可以在 https://developer.android.com/reference/android/content/Intent#constants_1
找到 Android 文档提供的意图动作和类别的完整列表,所以我不会在这里重复。相反,让我们看看最常见和最有趣的动作,为本章和本书其余部分即将出现的例子提供信息:
-
ACTION_AIRPLANE_MODE_CHANGED:设备的用户已经将飞行模式设置从开切换到关,反之亦然。
-
ACTION_CAMERA_BUTTON:点击了相机按钮(硬按钮或软按钮)。
-
ACTION_DATE_CHANGED:日期已经更改,这意味着您编写的任何使用计时器、运行时间等的应用逻辑都可能受到影响。
-
ACTION_HEADSET_PLUG:设备的用户已经将耳机连接到耳机插座或从耳机插座移除耳机。
如您所见,意图动作涵盖了应用和整个设备环境中发生的各种事情。
注意:Google 会在 Android 的每个新版本中添加新的动作。它还反对(后来删除)一些它认为不再有用的旧操作。随着时间的推移,在维护应用时,您应该检查应用所依赖的操作是否已被否决或移除。
了解意图路由
您可能认为,本章前面提到的组件命名通常是调用您想要的任何和所有活动的好方法,无论是您自己编写的活动还是其他活动。现实有点不一样。
如果你有一个自己编写的活动类,并且理解它是如何以及为什么是意图的最佳接受者,那么这样做是完全可以的。但是当处理来自其他应用的活动时,它是不可靠的,有时是不安全的,并且会导致不良的、意想不到的或者完全不正确的行为。
封装的编程原则是这个警告的核心。考虑一下让其他开发人员依赖您自己的类和它们的内部实现。作为一名成熟的开发人员,您将不断地调整代码和类,这就引入了一种非常真实的可能性,即有人对类内部的假设最终会是错误的,因为您已经改变了逻辑。反过来也是一样,你不应该依赖于实现逻辑在别人的应用中保持不变(甚至根本不存在)。
为了使用意图模型安全地定位您选择的应用或服务,最好使用 uri 和 MIME 类型。如果我们可以窥视其他人的应用的内部,那么我们如何将 Android 定向到偏好的活动或接收者(如服务)来接收意图呢?答案在于 Android 的隐式路由方案,它根据一组资格规则将意图传递给所有应该接收它们的活动等等。
隐式路由方案的规则如下:
-
活动必须通过适当的清单文件条目(将在下一节中讨论)来表明其处理意图的能力。
-
如果 MIME 类型是意图上下文的一部分,则活动必须支持该 MIME 类型。
-
活动还必须支持事件上下文中的每个类别。
这些规则有助于缩小可以接收您的意图的匹配活动的可能集合,只包括那些以适合您使用的方式忠实地执行意图的活动。
在你的清单中包含意图
Android 使用您的AndroidManifest.xml
文件作为保存意图过滤器的位置,指示您的应用中的哪些组件可以得到通知并对给定的意图做出适当的响应。如果您的组件的清单条目没有列出意图动作,那么它不会被选择通过隐式路由机制接收这种意图的通知。您可以将此视为组件处理意图的选择加入方法,构建您希望使用的意图列表。
当您创建一个新的 Android 项目时,Android Studio 会创建您的第一个意图过滤器,作为骨架AndroidManifest.xml
文件的一部分。到目前为止,您已经在本书中的所有示例应用中看到了这一点。作为复习,清单 17-1 是第二章中第一个应用MyFirstApp
的清单。以粗体显示的是活动MainActivity
及其指定的意图文件。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.beginningandroid.myfirstapp">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Listing 17-1Example intent filters from an AndroidManifest.xml file
意图过滤器的两个部分是其操作的关键。首先,我们指定MainActivity
活动属于类别LAUNCHER
,这意味着触发它的任何意图也必须属于该类别。其次,我们还指定了动作android.intent.action.MAIN
,它用于指定任何寻找具有MAIN
能力的活动的意图都可以被接受。您可以为您的MainActivity
提供更多可能的动作以及更多的类别,这表明您通过为活动的类编写的任何逻辑支持它的更多功能。
您的应用的任何其他活动都不会使用动作和类别的MAIN/LAUNCHER
组合——在几乎所有情况下,您在清单中只使用一个这样的指定。虽然有许多类别和动作可供选择,但您最终会非常频繁地使用DEFAULT
类别,尤其是对于任何查看或编辑风格的动作,以及描述活动可以用来查看或编辑的mimeType
的<data>
元素。例如,清单 17-2 显示了一个 notes 风格应用的意图过滤器。
<activity
android:name=".MyNotesActivity"
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.dir/vnd.google.note" />
</intent-filter>
</activity>
Listing 17-2An intent filter for an example secondary activity in your application
前面定义的活动包括一个意图过滤器,它描述了如何启动该过滤器来处理来自任何应用的意图,该应用使用带有vnd.android.cursor.dir/vnd.google.note mimeType
的内容的 Uri 来请求查看内容。这个意图可能来自您自己的应用,比如跟随您的启动器活动中的一个用户操作,或者来自任何其他可以为此活动创建一个格式良好的Uri
并将其用作它所触发的意图的有效负载的应用。
使用 Android 的验证链接
处理意向 uri 的另一个特性是自动验证链接的机制,它专门处理也是有效 URL(即网站地址)的 uri。通过验证链接,您可以在代码中添加一个明确的规则,使来自特定网站的 URL 和配套应用之间的链接 100%得到验证,并成为处理具有匹配 URL 的意图的首选方式。这包括后台机制,以在需要时实现无缝认证,并避免 Android 历史上使用的传统“网站或应用”选择器对话框。
看到行动中的意向启动活动
有了意图的理论和结构的知识,你就可以开始研究一个例子来说明它们的力量和便利。我已经多次提到了 Android 的理念,它有许多活动来支持您的应用所需的一系列功能,例如相册应用有一个给定的活动来查看单个图片,另一个活动来查看组或相册(可能使用GridView
),甚至更多的活动来标记、在社交媒体上共享等等。
在我们深入研究如何实现多活动应用之前,还有最后一点需要考虑。如果您的应用通过一个意图从一个给定的活动启动一个活动,那么调用活动应该关注关于启动的活动的状态的什么信息?您的启动活动是否需要知道第二个(或后续)活动的任何信息,比如它何时完成或完成了什么工作,并被传递一些结果?
决定活动依赖性
为了处理依赖与不依赖的问题,Android 提供了两种主要的方法来调用有意图的活动。
第一种方法是StartActivity()
方法,用于标记 Android 应该找到与意图负载最匹配的活动——包括动作、类别和 MIME 类型。“获胜”活动开始,来自 intent 的有效负载将被传递,以供活动使用。调用活动将继续其生命周期,并且不会收到任何关于被调用活动的工作、生命周期事件、数据变更等的更新或通知。
依靠抽签或类似的方式,听起来可能不是影响应该选择哪种活动来处理意图的最佳方式。你不必担心,因为还有第二种方法叫做startActivityForResult()
方法。在startActivityForResult()
中,不仅仅是一个结构良好的意图被用来影响哪个活动被触发;它还包括对所需活动的特定引用和对活动调用的唯一调用号。当被调用的活动结束时,通知被发送回调用活动,这意味着您可以模拟传统的用户体验,即父窗口或屏幕打开子窗口或屏幕,处理登录请求或从可用列表中选择选项。具体来说,回调包括
-
与特定于该调用的活动和原始
startActivityForResult()
方法相关联的唯一调用号。在这种设计中,您可以选择使用一种切换模式来确定哪些子活动已经完成,并继续适当地执行您的应用逻辑。 -
一个来自 Android 提供的结果
RESULT_OK
和RESULT_CANCELED
的数字结果代码,以及您想要以RESULT_FIRST_USER, RESULT_FIRST_USER + 1
的形式提供的任何自定义数据或结果,等等。 -
(可选)一个包含被调用活动应该返回的任何数据的
String
对象,比如从一个ListAdapter
中选择的项目。 -
(可选)一个
Bundle
,包含不完全符合前三个选项的任何附加信息。
有了在startActivity()
和startActivityForResult()
之间的选择,您主要关心的应该是在应用设计时确定哪一个是最好的。在运行时动态地确定这一点是可能的,但是在潜在的几十或几百个活动中处理所有新出现的可能性可能会变得令人不知所措。
创建意图
了解了如何使用您想要的方法调用活动之后,剩下的工作就是创建Intent
对象,用作触发其启动的有效负载。如果您想在自己的应用领域内启动另一个活动,那么最直接的方法是直接创建您的意图,并明确地声明您希望激活的组件。这将使您创建一个新的意图对象,如下所示:
new Intent(this, SomeOtherActivity.class);
这里,您显式地引用您想要调用您的SomeOtherActivity
活动。对于这种风格的直接调用,您不需要在您的AndroidManifest.xml
文件中构建意图过滤器——无论您的SomeOtherActivity
喜欢与否,它都会启动!显然,作为开发人员,你有责任确保你的SomeOtherActivity
能够做出相应的响应。
我在本章的前几节中概述了使用一个Uri
和匹配的标准集来馈送给 Android 的优雅和偏好,以便它可以为您的使用找到合适的活动。Android 有一系列受支持的Uri
方案,你可以自由创建一个与其中任何一个相匹配的Uri
。作为一个例子,下面是为联系人系统中的一个联系人创建一个Uri
的代码片段:
Int myContactNumber = 4;
Uri myUri = Uri.parse("content://contacts/people/"+myContactNumber.toString());
Intent myIntent = new Intent(Intent.ACTION_VIEW, myUri);
我们使用第四个联系人的号码,然后构造Uri
字符串来引用该联系人。然后我们可以将这个Uri
传递给 Intent 对象。
启动意图调用的活动
你已经建立了你的意图,所以是时候选择打电话给startActivity()
或startActivityForResult()
中的哪一个了。您可以考虑一些更高级的选项,但它们超出了本书的范围。如果你感兴趣,Android 文档对包括startActivities()
、startActivityFromFragment()
和startActivityIfNeeded()
在内的选项有更多的说明。
清单 17-3 展示了一个来自ch17/IntentExample
项目的样本布局,使用了一个非常简单的布局,只有一个标签、一个字段和一个按钮。
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/myContact"
android:layout_alignLeft="@+id/button1"
android:layout_alignBottom="@+id/myContact"
android:text="Contact Number:"
tools:layout_editor_absoluteX="41dp"
tools:layout_editor_absoluteY="31dp" />
<EditText
android:id="@+id/myContact"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="@+id/textView1"
android:ems="10"
android:inputType="number"
app:layout_constraintStart_toEndOf="@+id/textView1"
tools:layout_editor_absoluteY="19dp">
<requestFocus />
</EditText>
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/myContact"
android:onClick="viewContact"
android:text="View Contact"
app:layout_constraintTop_toBottomOf="@+id/myContact"
tools:layout_editor_absoluteX="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Listing 17-3A layout to demonstrate intents
Note
在这个 ConstraintLayout 示例中,我故意让 EditText 和 TextView 在垂直和/或水平方向上不受完全约束。您将在 Android Studio 中看到警告,由于没有这些约束,小部件将“跳到”没有它们的默认位置。在这种设计中,这仍然会产生所需的布局,但是如果您愿意,可以在运行该示例时向这些视图添加约束。
如果您查看布局中基于视图的小部件,您会看到按钮将调用一个viewContact()
方法。为了给用户带来他们期望的体验,我们需要像前面所解释的那样编写创建联系人Uri
的代码,并使用它来创建Intent
对象,该对象将启动一个活动来(希望)显示一个联系人。示例代码如清单 17-4 所示。
package org.beginningandroid.intentexample;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
private EditText myContact;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myContact=(EditText)findViewById(R.id.myContact);
}
public void viewContact(View view) {
String myContactNumber=myContact.getText().toString();
Uri myUri = Uri.parse("content://contacts/people/"+myContactNumber);
startActivity(new Intent(Intent.ACTION_VIEW, myUri));
}
}
Listing 17-4Java logic for our intent-triggering contact application
这里使用的逻辑非常简单和直接。这可以让你专注于我们在本章中讨论的内容。
Note
要在实践中看到这个例子,您需要使用您的 AVD 或设备来创建一些联系人并保存 AVD 的状态,以便它们在重新启动 AVD 时不会丢失。这样意向火了就有联系人可以实际显示了!
运行IntentExample
项目将导致IntentExample
活动,如图 17-1 所示。
图 17-1
发布后显示的主要意图示例活动
您首先看到此活动的原因是因为 Android Studio(或您正在使用的任何其他 IDE)的自动项目设置已经在您的清单文件中添加了启动器类别的必要属性和数据值。
在编辑文本字段中输入一个联系人的号码,然后单击查看联系人按钮,意向人和接收人就会施展他们的魔法。您的联系人 Uri 被捆绑到您的意图中,对 startActivity()的调用将 Android 发送出去,从设备上的所有应用中筛选所有活动,以找到最合适的活动来处理您的意图的 ACTION_VIEW 操作。你可以在图 17-2 中看到结果。
图 17-2
为满足我们的意图而选择的联系活动
你在图 17-2 中看到的是当前 Android 原生联系人视图活动,而不是我在IntentExample
项目中编码的任何内容。您会认为这是从您的应用外部安全地调用一个活动,因为我们采取了正确的步骤来提供一个Uri
,并且我们可以安全地期望 Android 本身找到正确的活动来实现我们的意图。没有使用组件命名方法强行要求特定的活动。
介绍接收器
在本章中,我们探讨了如何使用意图在应用中创建和激活多个活动,目的是在应用中处理各种各样的动作,以响应用户的交互和需求。但是并不是对用户意愿的每个响应,或者触发的意图,都需要在(另一个)活动的范围内处理。有许多满足意图的真实世界的例子,其中你确实不需要一个活动的所有特性和复杂性,尽管它们是轻量级的。一些示例包括数据操作、执行计算等,其中计算或结果可以在不涉及任何 UI 或不想将意图指向 Android 服务而不是任何面向最终用户的应用的情况下确定。例如,您可能希望构建一个音乐共享服务,将所有音乐发送到云存储供应器进行备份,而无需任何用户交互。还有更高级的情况,直到运行时你才知道是否需要一个完整的 UI 活动,这意味着你需要同时为活动驱动和“无活动”的方法进行规划和设计。
不需要 UI 时使用接收器
为了处理这些“无活动”的情况,Android 提供了BroadcastReceiver
接口和接收器的概念。您现在已经熟悉了基于活动的 UI 屏幕的轻量级特性,它可以快速处理用户交互,接收器是对它的补充,它提供了轻量级对象,创建这些对象是为了接收和处理广播意图,然后可以丢弃。
Android 文档显示在 BroadcastReceiver 的定义中只有一个方法,名为onReceive()
。onReceive()
方法可以被认为是“开始吧!”方法。在接收器中实现任何所需的逻辑,并确保它以您所想的方式与相关的意图一起工作,这取决于您。
实现BroadcastReceivers
以与活动相同的方式开始,在AndroidManifest.xml
文件中有一个声明。使用元素名BroadcastReceiver, f
或示例的类的android:name
属性:
<receiver android:name=".ReceiverClass" />
您实现的任何接收器都是短暂的,只在执行您为实现其onReceive()
方法而创建的逻辑的时间内存在,之后它将被丢弃以进行垃圾收集。接收者的行为是有限制的,有些是意料之中的,有些是意料之外的。正如您所期望的,当需要 UI 时,您不能在您的接收器逻辑中调用任何 UI 元素。不太令人期待但仍然重要的是,您不能发出任何回调的限制。对于在服务或活动(允许的组合)上实现的接收者来说,这些限制稍微放松了一些,在这些情况下,接收者在相关对象的生命周期内生存。
您不能通过服务或活动的清单来创建接收者。您可以使用onResume()
中的registerReceiver()
为这些情况动态生成接收者,以标记您的活动能够接收特定动作/类别/mime 类型组合的意图。如果您使用这种方法,您还必须在onPause()
回调期间通过调用unregisterReceiver()
来执行清理。
使用接收器限制
除了上一节提到的限制之外,您还应该了解接收器的一个额外限制。到目前为止所描述的意图和广播接收器的机制可能会让您认为可以将两者结合起来作为应用消息传递的通用方法。这个想法的“扳手”是活动生命周期。特别是,当活动暂停时,它们将不会收到意向。
暂停时无法接收意图意味着您会面临错过消息(意图广播)的问题,您需要避免接收者绑定到活动,而不是更喜欢通过AndroidManifest.xml.
进行声明。您还需要考虑“重新发送消息”和重试逻辑,以便能够从任何错过的消息中恢复,或者考虑使用更复杂的第三方消息总线系统或类似方法。
摘要
在这一章中,我介绍了将应用从单活动程序扩展到多活动领域所需的所有机制!您在 Android 的意图和广播接收器机制方面有很好的基础,应该能够在您计划编写的应用中扩展活动的使用。
十八、Android 服务简介
由于其 Linux 操作系统的传统,Android 拥有许多平台特性,将 Linux 的强大功能带给了一个全新的用户群。这一丰富遗产的一部分包括一组在后台运行的特殊应用,没有用户界面,它们为运行在 Android 上的所有其他程序提供功能。这些在 Android 中被称为服务,类似于 Linux 中的守护进程概念和 Microsoft Windows 中的服务概念。
本章将探索 Android 服务的一些基础知识,向您展示创建、启动和操作服务的步骤。为了增加这种探索,我们还将查看一系列简单的服务示例,以拓宽您对使用服务的理解。
服务背景
服务的基本原理来自许多需求,特别是当功能或任务需要由一个或几个应用来执行,但这些任务不需要任何形式的 UI 交互或面向用户的活动来显示时。在任何 Android 设备上,有数百个服务在任何时间点运行,包括
-
提供本地接口来控制远程 API,如位置服务或地图应用。
-
为持续几天、几周、几个月或更长时间进行“对话”的 messenger 和聊天应用保持长期连接。
-
继续处理用户调用的任务或工作,无需进一步的交互。一个很好的例子是从 Google Play 商店和其他地方下载 Android 应用的更新。
这些只是你现在可以在 Android 设备上找到的一些例子,显然还有更多。例如,回想一下我们的音频和视频示例以及媒体框架,其中实际的音频和视频回放任务依赖于后台服务。
在构建应用时,你没有义务使用服务,但你可以将它们视为袖手旁观随时准备在需要时帮助你的帮手——包括你可能编写的服务和 Android 平台提供的默认服务。
使用工作管理器作为服务的替代
虽然这一章的其余部分集中于针对前一节中概述的需求的经过试验和测试的服务方法,但 Android 确实提供了其他方法。其中之一是 WorkManager,它是本书前面介绍的 Jetpack 库的一部分。WorkManager 允许您在后台执行工作,即使是在应用终止之后。您可以将 WorkManager 视为在应用之外异步执行工作的一种简单的手持方式。
WorkManager 有它的长处,你可以在 https://developer.android.com/topic/libraries/architecture/workmanager/basics
了解更多。服务的通用功能,以及它们的威力和效用,是一个引人注目的主题,需要掌握并添加到您的开发人员工具集中,我们将在本章的剩余部分对此进行深入研究。
从你自己的服务开始
定义和创建您自己的服务应用与您在制作普通的基于活动的 Android 应用时所学的非常相似。您应该对创建 Android 服务的步骤很熟悉:
-
使用 Android 提供的基类,扩展它并添加任何必要的类继承来创建您自己的定制服务。
-
确定需要覆盖哪些回调方法,然后编写代码来实现所需的逻辑。
-
添加必要的
AndroidManifest.xml
条目来提供权限、定义和到更广泛的 Android 平台的链接,以便您的服务可以运行并服务于其他应用。
在下一节中,我们将探索这些领域,让您有一个全面的了解。
实现您的服务类
在默认的 Android 开发框架中,Service
类是作为构建自己的服务的基础而提供的。Service
类还提供了几个现成的有用的子类,这些子类与许多开发人员拥有的通用服务模式相匹配,尽管您可以根据自己的需要从Service
类本身或任何助手子类开始。其中,迄今为止最有用和最常用的是IntentService
子类。
清单 18-1 显示了一个简单服务的基本代码大纲。
package com.artifexdigital.android.serviceskeleton
import android.app.service
//more imports here
public class SkeletonService extends Service {
//overrides and implementation logic here
}
Listing 18-1Outline of a service application in Android
通过回调管理服务生命周期
Service
类及其子类提供了一系列回调,旨在让您用自己的逻辑实现服务控制行为。Service
回调和生命周期概念非常类似于我们在第 11 和 12 章中探索的Activity
和Fragment
生命周期,主要区别在于服务发现自己所处的状态更少。这种简单性意味着在编码支持逻辑时,处理服务行为时只需要考虑五个主要回调:
-
onCreate()
:非常类似于活动的onCreate()
方法,当服务活动的任何触发发生时,服务的onCreate()
方法被调用。 -
onStartCommand()
:当客户端应用调用相关的startService()
方法时,调用onStartCommand()
方法并处理其逻辑。 -
onBind()
:当客户端应用试图通过bindService()
调用绑定到服务时,每次都会调用onBind()
方法。 -
onTrimMemory()
:作为 Android 试图以一种自信的方式管理资源的一部分,当设备内存不足时,为资源回收选择的服务将调用它们的onTrimMemory()
方法。在采取更激烈的措施之前,这为服务提供了一种更可控的方式来尝试返回内存。 -
onDestroy()
:正常正常关机时,调用onDestroy()
。就像正常活动一样,不能保证正常关机,因此也不能保证调用onDestroy()
。
你对来自活动和片段的生命周期管理的理解保持不变。这意味着您的服务应该在它的onCreate()
调用期间创建它需要的东西,并在onDestroy()
期间清理和处置任何延迟的资源,如果不是在此之前的话。
活动和服务之间的一个区别是,对于服务来说,没有onPause()
和onResume()
的等价物。您的服务要么正在运行,要么没有运行。这意味着当服务被认为总是在后台时,没有必要提供后台转换方法。这种没有暂停/恢复的情况意味着您应该始终注意最小化服务保持的任何状态,或者在适当的情况下使用首选项或其他存储,以经受意外的服务终止。Android 不仅可以在任何时候为资源终止服务——绕过任何对onDestroy()
的调用——而且用户还可以通过应用管理系统的设置活动终止你的服务。在这方面,绑定了客户端的服务变得更加复杂,我们稍后将在示例中探讨这一点。
为您的服务提供清单条目
要声明您的服务应用,您需要在AndroidManifest.xml
文件中进行适当的声明。这从作为<application>
元素的子元素的<service>
元素的核心定义开始。清单 18-2 显示了服务的基本条目,包括必需的android:name
属性,在本例中是“Skeleton
”。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.artifexdigital.android.skeletonservice" >
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<!-- any other application child elements would go here -->
<service android:name="Skeleton">
</application>
</manifest>
Listing 18-2A minimal service definition in the AndroidManifest.xml file
您可以自由地在同一个项目中混合服务和活动的定义,因此在同一个AndroidManifest.xml
文件中。您会发现,对于结合自己的服务开发的应用,通常都是这样做的。当您创建服务时,您并不总是希望允许任何应用绑定到它们并使用它们。在这些情况下,您可以在您的<service>
元素中使用一个android:permission
属性来限制访问。
服务通信
定义您的新服务并不比本章已经概述的步骤更复杂。一旦创建了服务,控制客户端应用(如活动和其他服务)如何与您的服务交互就有点复杂了。
与服务通信的客户端(活动或服务)采取两种可能的途径:启动命令或绑定。当涉及到您的服务与客户端进行通信时,事情会扩展到相当多的选项,您作为开发人员可以从中进行选择。
客户端到服务的通信
当任何类型的客户端想要使用服务时,无论该客户端是活动、片段还是其他服务,都有一个中心问题将指导您选择两种通信方法中的哪一种最适合该任务。这是一个一次性的、永不重复的为客户做某事的服务请求吗?在这些单一交互的情况下,服务通信的命令方法是最好的。如果客户端需要通过一系列操作来使用服务,同时保持与服务的持续交互,那么服务绑定就是一种方法。
Invoking Commands with startService()
让您的服务为客户端应用执行任务的最简单的方法是调用startService()
方法,无论是活动还是其他来源。在第十七章中,我们介绍了startActivity()
方法,它可以通过获取一个意图和一些参数来触发一个任意的活动,同样的,startService()
方法也接受一个意图和一组额外的意图作为参数,让你传递一个上下文相关的有效载荷给接收服务。最基本的调用startService()
形式如下:
startService(someSignatureIntent);
这种调用startService()
的基本形式提供了预期的 Intent 类作为唯一的参数。调用startService()
是异步的,应用的主线程——它管理主 UI——不会阻塞等待响应,而是会立即继续正常的生命周期。
调用startService()
会触发 Android 启动未启动的服务,在已启动和待启动两个流中,Android 随后将 intent 从第一个参数传递给onStartCommand()
方法。您的服务实现可以检查和使用意图,并且可以选择在第二个参数中使用数据负载,正如您在为onStartCommand()
方法编写的逻辑中所希望的那样。
尽管调用应用没有被服务调用阻塞,但是您的服务将在其主线程中处理onStartCommand()
方法,因此您应该注意不要承担太多繁重的处理、阻塞外部调用或任何其他可能妨碍快速响应的耗时工作。如果您确实需要将这种长时间运行的工作作为服务逻辑的一部分来执行,那么您可以探索使用 java.util.concurrent 包及其执行器和相关功能来添加更多的线程,以便在主服务线程之外执行这项工作。
使用startService()
的“一劳永逸”特性意味着它不会向调用应用返回有效负载或正常意义上的响应。对于那些需要这样做的环境,服务到客户端的通信方法更合适,将在本章后面介绍。startService()
调用确实返回一个值来表示该调用是成功完成还是由于资源匮乏或其他原因而被终止。该返回值来自一个预定义的小集合,从中您通常会看到以下内容:
-
START_STICKY:一旦 Android 有足够的空闲内存来重新启动服务,但是不要担心触发意图,而是传递一个空意图。
-
START_NON_STICKY:根本不要自动重启服务,即使 Android 资源压力降到足够低的水平允许。这意味着服务将不会启动,直到您的应用或其他程序显式调用
startService()
或再次调用对服务的需求。 -
START _ REDELIVER _ INTENT:一旦 Android 有了足够的空闲内存,就重新启动服务,并尝试重新传递在进行原始(失败)调用时传递给服务的原始 INTENT 对象。
一旦服务启动,无论是从startService()
调用还是其他方式,它都将无限期运行,除非设备上出现任何资源不足的情况,Android 可能会将其作为更广泛的资源管理的一部分杀死。这与这样一种观念是一致的,即一旦你的客户得到了它想要的东西,它并不真正关心服务之后会发生什么。从开发人员的角度来看,您可能会不时地考虑您的服务是否在做任何有用的事情,并且在缺少客户端需求的情况下,确定它没有任何事情可做,然后优雅地关闭。服务自行终止有两个主要选项,如下所示:
-
使用
stopService()
方法。与startService()
类似,您调用stopService()
时使用的参数与用于启动服务的参数或派生类的参数具有相同的意图。服务将被停止,所有资源将被释放,状态将被销毁。由于 Android 不跟踪对某项服务的startService()
调用的来源或数量,它同样不区分哪个客户端发送了适当的stopService()
调用。只需要一个stopService()
命令,不管在服务的生命周期中接收了多少个startService()
调用。 -
用一个
stopSelf()
调用来编码你的服务。您的服务可以控制自己的终结,您可能会将此视为服务执行工作的某种逻辑高潮的一部分,例如当音乐曲目播放完毕或文件下载完成时。
当构建您自己的服务时,请随意探索管理其终止的两种方法。或者,您可以将此留给 Android 的一般服务清理功能。如果您选择使用服务绑定作为处理服务的方法,则清理和关闭机制会有很大的不同。
对服务使用绑定方法
使用startService()
与服务进行一次性交互的一次性方法是一种非常有用的机制。有时候,您会希望与服务进行多次交互,或者在更复杂的情况下,需要不止一次的交互,例如发送连续的命令或双向交换数据,以便您的应用可以为其用户执行一些有用的工作。
这就是使用bindService()
的绑定方法介入的地方。绑定到服务会建立一个双向通信通道,这样您的应用就可以通过其绑定器访问服务的 API。绑定器是从bindService()
调用返回到调用应用的对象,然后用于所有后续活动。使用绑定方法的客户端可以通过使用BIND_AUTO_CREATE
标志向 Android 发出信号,如果服务当前停止,它希望该服务启动。与startService()
方法的一个不同之处在于,一旦客户端释放了与服务的绑定,服务将被标记为可以关闭。我们将在本章后面讨论在这些情况下关机的机制。
Caution
当调用一个可能没有运行的服务时,如果在 bindService()调用中没有提供 BIND_AUTO_CREATE 标志,那么如果该服务还没有运行,该方法就有返回 false(并且没有提供 Binder 对象)的风险。这里的教训是不要依赖预感或假设的服务状态。相反,无论在什么情况下,都要练习干净的异常处理并检查 bindService()是否失败。
当 Android 面临内存压力时,使用BIND_ALLOW_OOM_MANAGEMENT
标志可以帮助发出信号,表明您可以应对正在使用的服务的突然关闭。这个标志表示您的应用的绑定不是关键的,它可以容忍服务在内存不足的情况下突然消失。最简单地说,你是在表明,为了设备和运行在其上的所有其他应用的更大利益,你乐于牺牲你的绑定(和Binder
对象)。
应用对bindService()
的调用是一个异步调用,它包括用于识别服务的意图和可选的BIND_AUTO_CREATE
标志。因为bindService()
是异步的,所以在ServiceConnection
对象被询问并且结果Binder
对象从onBind()
返回之前,你不会知道服务的结果和后续状态。一旦您确认这些已经被实例化并且可用,您就可以开始调用Binder
方法并实际与服务的功能进行交互。对您来说,子类化Binder
方法来实际实现您希望您的服务拥有的任何逻辑是正常的。
您的应用可以让ServiceConnection
对象——以及到服务的隐式连接——存在多久就存在多久。当您完成了对服务的处理后,您调用unbindService()
方法来指示您的服务绑定可以被释放,并且ServiceConnection
对象和相关的资源也被释放。这种解除绑定最终会导致onServiceDisconnected()
被调用,同时Binder
对象也会被调用,这意味着您已经构建并通过类似 API 的方法呈现的任何定制逻辑都不应再被调用。如果任何其他应用已经绑定到该服务,您对unbindService()
的调用将不会导致该服务停止。实际上,最后一个解除绑定的客户端应用会触发 Android 关闭服务。
服务到客户端的通信
到目前为止,我们已经在本章中探讨了服务,您可以看到命令方法和绑定方法是如何很好地支持客户端到服务的通信的。当涉及到从服务到客户端的通信时,有一系列选项可以涵盖几乎任何您可以想象的场景。让我们探索一下主要的选项,记住这些方法不像客户端到服务通信的startService()
和bindService()
选项那样结构化。
对所有通信使用 bindService()方法
当考虑服务如何与它们感兴趣的客户通信时,首先要考虑的是通过bindService()
和您为客户创建的方法进行交互。绑定方法的优点是,您可以精确地控制客户端从您的服务方法返回的对象和信息中接收到的内容,并保证客户端确实得到了它所要求的内容,因为客户端得到它所需要的内容的唯一方式是调用您的 API 方法。
这种方法的明显缺点是,不使用bindService()
与您的服务建立持久双工通道的客户端将无法接收任何东西——任何通过startService()
调用进行交互的客户端都只是运气不好!现在这听起来可能可以接受,但是设计您的服务来适应许多不同的客户端使用模式是一个好的实践。
意图和广播接收器
在第十七章中,我们介绍了 Android 在应用间通信的基本方法,包括意图和广播接收器机制。如前所述,您尝试了从代码中传播意图的示例。服务也可以自由使用这种方法!
在实践中,您通过在客户端应用中使用registerReceiver()
方法注册一个BroadcastReceiver
对象来利用意图和接收者,并使用它来从您将记录的服务命令中捕获特定于组件的广播,以便客户端可以正确地识别广播意图并根据需要处理它们。
采用意向方法有好处,但也有缺点,即意向必须是以行动为导向的,而不是依靠一些活动来自愿采取行动。您还假设客户端活动本身仍在使用其接收器运行,并且没有因为资源压力或其他设备上的事件而暂停或选择终止。
使用挂起内容对象
Android 提供了PendingIntent
对象来表示需要执行的相关动作的意图。当涉及到服务时,一旦服务执行了它的工作,您的客户将调用onActivityResult()
来处理下游逻辑。客户端通过startService()
调用获取额外有效负载的能力,将PendingIntent
对象传递给服务,然后服务通过调用其上的send()
方法向客户端发出信号。
要使这种方法有效,还需要做额外的工作,因为您需要客户端代码来解释和识别使用了各种send()
方法调用中的哪一个。
使用信使和消息对象
如果PendingIntent
对象还不够,Android 还提供了Messenger
对象用于上下文间的通信,比如从服务到活动。单个活动带有一个Handler
对象,该对象可用于活动本身发送消息。然而,Handler
并不公开用于活动到服务或活动内部的交互。这就是Messenger
物体拯救世界的地方。它可以向任何Handler
发送消息,因此可以用来桥接间隙并到达任何活动。
为了利用Messenger
对象,您在调用服务之前添加一个额外的对象。服务以典型的方式接收意图,并可以提取Messenger
对象,当需要与客户端通信时,服务应该创建并填充一个Message
对象,然后调用信使的。send()
将消息作为参数传递回客户端的方法。在您的客户端,您通过处理程序和它的handleMessage()
方法接收这个消息。
虽然这在概念上非常简洁,但是还需要额外的步骤来创建和交换Messenger
和Message
对象。还有一个潜在的性能影响,因为您必须在活动的主应用线程中处理handleMessage()
方法——所以保持这种处理是轻量级的!
使用独立的消息传递
除了我们在前面几节中介绍的各种 Android 原生和设备上的方法,您还可以通过外部消息传递或发布/订阅系统来处理服务到客户端的通信。随着 Google Cloud Messaging 及其克隆产品开发出越来越多有用的功能,这越来越受欢迎。使用这种第三方方法的时候通常是您的用例能够容忍异步消息传递的时候。
有关 Google Cloud Messaging 的更多细节,请查看开发者网站上的文档,网址为 https://developers.google.com/cloud-messaging/android
。
创建回调和侦听器
前面几节中的Messenger
和PendingIntent
示例演示了将对象附加到传递给服务的额外意图是多么容易。使用这种方法要求您的对象是“Parcelable,
”,并且您可以创建自己的对象来满足这种需求,包括您自己的回调或侦听器。
采用这种方法要求您定义侦听器对象,并在运行时让客户端和服务编码在需要通信时部署和处理侦听器。您还将负责注册和收回侦听器,这样您就不会留下任何孤儿以及随之而来的资源浪费/损失。
使用通知
服务本身没有直接的 UI,但是它可以与另一个应用的 UI 进行交互。回想一下我们在第十五章中介绍的通知主题,您可以看到服务如何利用它直接向用户呈现信息和响应。
服务在行动
我们已经讨论了 Android 的服务、客户端通信和整体行为的许多方面,所以现在是时候构建和运行您自己的示例服务和客户端应用了。为了有一个有用的例子,不需要用一整章来介绍它自己,我们将创建一个简单的照片共享服务和一个简单的客户端例子来将服务理论付诸实践。这不会是 Instagram 或 Flickr 的克隆——目标是简单,这样你就可以专注于设计服务。
选择服务设计
一个简单的照片共享应用是使用startService()
服务交互模型的完美场景,在这里我们将向服务发送一个“请共享这个”指令,而不需要做任何需要绑定到服务的后续工作。我们将从基类Service
中设计我们的服务,然后实现onStartCommand()
方法和onBind()
方法(即使我们的示例客户端没有使用它,我们仍然需要这样做来正确地扩展服务基类)。
我们可以选择另一个服务子类,比如IntentService
,它为您提供了大部分的实现,比如整齐地调用stopService()
、startService()
等等,但是由于我们不想要或者不需要这些助手,我们将坚持使用前面的方法。特别是,我希望这个服务在startService()
呼叫之后继续存在,这样我们以后就可以停止分享了。
你选择分享哪些照片对这个例子来说并不重要,所以我在这个例子中把它们剔除了。如果你愿意的话,你可以修改这个并添加图片或其他资源。
为服务创建 Java 逻辑
ServiceExample
的实现非常简单,主要关注基础Service
类覆盖实现的核心部分,加上我为照片共享定制的特定于服务的逻辑。清单 18-3 具有来自ch18/ClientExample
中的例子的完整服务实现。
package com.beginningandroid.clientexample;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class ServiceExample extends Service {
public static final String EXTRA_ALBUM="EXTRA_ALBUM";
private boolean isShared=false;
@Override
public IBinder onBind(Intent intent) {
// We need to implement onBind as a Service subclass
// In this case we do not actually need it, so can simply return
return(null);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String album=intent.getStringExtra(EXTRA_ALBUM);
startSharing(album);
return(START_NOT_STICKY);
}
@Override
public void onDestroy() {
stopSharing(); }
private void startSharing(String album) {
if(!isShared) {
// Simplified logic - you might have much more going on here
Log.w(getClass().getName(), "Album successfully shared");
isShared=true;
}
}
private void stopSharing() {
if(isShared) {
// Simplified logic - you might have much more going on here
Log.w(getClass().getName(), "Album sharing removed");
isShared=false;
}
}
}
Listing 18-3Service implementation for ServiceExample.java
我们没有任何需要在服务启动时执行的特定于服务的设置,所以我们可以在一个onCreate()
调用中省略额外的逻辑,并依靠父类来完成这项工作。我们实现了onStartCommand()
,这样当客户端调用startService()
时,我们可以采取期望的动作。这意味着我们想要检查用于指定服务的意图,并询问ServiceExample
想要的额外内容,比如共享的相册名称。一旦我们有了专辑名,我们就调用为这个特定服务实现的startSharing()
。
如前所述,我已经完成了startSharing()
方法的大部分后续部分。我们可以在这个例子中实现的一个有用的东西是使用 Android 的日志记录基础设施在不同的点发出相关信息,通知我们服务是活跃的和工作的,即使它缺少 UI。这种技术还可以帮助您在实际服务中进行各种调试、使用度量等等。从示例应用和服务中,您将能够通过 Logcat 中的输出来判断它正在运行并在服务逻辑的各个部分中移动。如果您什么也看不到,这也很有用——没有日志记录会提示您出现了问题。
我已经实现了onDestroy()
方法,现在它只是调用我们服务的stopSharing()
方法。就像startSharing()
一样,这基本上被杜绝了,通过一些日志记录来帮助您确认服务代码在被调用时正在工作。
如前所述,即使我们不打算使用绑定方法,我们仍然需要基于我们的服务子类实现onBind()
。在这种情况下,它可以返回 null。未来的增强可以允许其他客户端绑定和做更复杂的事情,如创建照片蒙太奇,显示相册的缩略图,等等。
创建一个示例客户端来使用服务
出于完整性的考虑,看到客户实际使用服务是件好事,而不是仅仅依赖于我的承诺,即服务做到了它所说的。清单 18-4 给出了客户端驱动ServiceExample
服务的简单布局。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/startSharing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Sharing"
android:onClick="onClick" />
<Button
android:id="@+id/stopSharing"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Stop Sharing"
android:layout_below="@id/startSharing"
android:onClick="onClick" />
</RelativeLayout>
Listing 18-4The layout for the ClientExample application
你可以看到我已经创建了一个非常简单的 UI,因为ClientExample
应用只有两个按钮,一个标记为“开始共享”,另一个标记为“停止共享”记住,这里的目标是理解服务机制,而不是 UI 美学。
因为我们使用命令风格的startService()
方法来与服务交互,所以我们的 Java 逻辑也非常简单。清单 18-5 显示了完整的客户端逻辑。
package org.beginningandroid.clientexample;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.startSharing:
startSharing(view);
break;
case R.id.stopSharing:
stopSharing(view);
break;
}
}
public void startSharing(View view) {
Intent myIntent=new Intent(this, ServiceExample.class);
myIntent.putExtra(ServiceExample.EXTRA_ALBUM, "My Holiday Snaps");
startService(myIntent);
}
public void stopSharing(View v) {
stopService(new Intent(this, ServiceExample.class));
}
}
Listing 18-5Java code for the sample ClientExample application
到现在为止,你已经非常熟悉基本的onCreate()
方法,它在ClientExample
中只是扩大了布局。onClick()
实现遵循通过查询传递的视图来检查用户点击了哪个按钮的模式,并根据需要触发startSharing()
或stopSharing()
方法。
如果调用 s tartSharing()
,它将实例化一个服务意图,为我们想要分享的一组照片传递一个非常可信的相册名称。使用传递意图的startService()
调用服务。我们的stopSharing()
实现基本上是用适当类型的新意图调用stopService()
命令,与原始服务调用相匹配,从而将我们的服务作为关闭的目标。
测试运行中的服务
您可以继续测试运行服务并观察结果。确保将
<service android:name=".ServiceExample" />
当您运行ClientExample
应用时,它触发对ServiceExample
服务的调用,使您能够查看 Logcat 中的条目,如下所示:
...org.beginningandroid.clientexample.ServiceExample: Album successfully shared
...org.beginningandroid.clientexample.ServiceExample: Album sharing removed
摘要
现在,您已经探索了服务的所有基础知识,并准备将它们包含在未来的应用设计中。请务必尝试构建您自己的服务变体,这些变体使用我们在本章中探索的不同的服务到客户端的通信方法。
十九、在 Android 中处理文件
在这一章中,我们将详细探讨文件,包括 Android 为应用存储、检索和管理数据的方法。在下一章中,我们将讨论数据库的辅助工具,它们和文件一起代表了应用中数据管理的丰富选项。我们还将这些与内容供应器进行对比,内容供应器是 Android 更复杂的数据访问和管理模型。
本章中的例子集中在 Android 为基于文件的数据提供的两个主要方法上。方法 1 可以被认为是“应用嵌入式”模型,它使用与应用打包在一起的原始资源和素材。方法 2 是“Java I/O”方法,它利用几乎著名的 java.io 包来操作文件、数据流等,就像在任何其他操作系统上操作基于 Java 的文件管理一样。
每种方法都有优点和缺点,我们将一一介绍,您可以放心,没有最好的方法,只有对当前问题最好的方法。
使用资源和原始文件
在第 13 和 14 章中,我们介绍了一些音频和视频例子,这些例子依赖于 Android 的一些直接处理文件的功能。我们在这些章节的示例中探索的原始位置和素材位置的使用不仅限于音频、图像和视频等媒体文件。您可以将几乎任何类型的文件放在这些位置,只要您让开发人员知道如何访问和操作它们的内容。例如,您可以存储一个. csv 文件来保存一些有用的数据。
Android 通过 Resources 类及其getResources()
方法提供了对文件的简单访问。对于原始资源文件,您可以通过调用openRawResources()
方法通过InputStream
来呈现其内容。作为开发人员,你的任务是知道InputStream
中的数据意味着什么。在我们看一个例子之前,需要知道使用来自原始或素材文件的数据源的一些重要的优点和缺点。
基于 raw 的方法的优点包括:
-
多亏了安卓素材打包工具 AAPT,你的文件可以和你的应用打包在一起。
-
您可以在库项目中共同定位资源,以便在需要时可以从许多应用中访问它们。
-
默认情况下,文件是私有的,外部访问需要完整的知识或包名和资源名来引用或适当的库或 API 调用,以及在清单文件中或在运行时授予的文件访问权限。
-
只读和静态数据可以用常见的格式打包,比如 JSON 或 XML。
为了平衡优势,需要注意这种方法的一些主要缺点:
-
默认情况下为只读。编辑与应用打包在一起的现有资源并不简单。
-
对于其他应用或服务用户来说,共享是很重要的。
-
静态特性带来了保持信息更新的问题。
有了这些优点和缺点,您就可以做出明智的选择,确定这是否是您的应用和所需功能的正确方法。
从资源文件填充列表
理解文件管理的优点和缺点最好用一个例子来说明。对于这个例子,我们将引入ListView
UI 小部件和适配器逻辑,并使用它们作为从 XML 文件读取数据的机制,并在应用运行时动态填充文件数据的值列表。清单 19-1 显示了一个简单的布局,提供了一个ListView
来最终显示来自我们的 XML 资源文件的数据。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/mySelection"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawSelectorOnTop="false" />
</RelativeLayout>
Listing 19-1The layout for the RawFileExample
对于这个示例应用,我们将让我们的ListView
显示颜色的名称,并从我们在ch19/RawFileExample
项目中提供的 XML 文件colors.xml
中获取这些颜色名称。你可以在清单 19-2 中看到文件colors.xml
的内容。
<colors>
<color value="red" />
<color value="orange" />
<color value="yellow" />
<color value="green" />
<color value="blue" />
<color value="indigo" />
<color value="violet" />
<color value="black" />
<color value="white" />
</colors>
Listing 19-2The colors.xml file content
可以看到colors.xml
文件很简单,这是故意的。我们关注的是实际打开该文件、读取和解析其内容以及在应用的适当数据结构中使用结果数据所需的逻辑,而不是 XML 的复杂性。清单 19-3 展示了一个简单的基于ListActivity
的应用的逻辑,它将在一个列表中显示来自colors.xml
文件的颜色名称,然后让用户点击选择一种特定的颜色。
package org.beginningandroid.rawfileexample;
import android.app.ListActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import java.io.InputStream;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
public class MainActivity extends ListActivity {
private TextView mySelection;
ArrayList<String> colorItems=new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mySelection=(TextView)findViewById(R.id.mySelection);
try {
InputStream inStream=getResources().openRawResource(R.raw.colors);
DocumentBuilder docBuild= DocumentBuilderFactory
.newInstance().newDocumentBuilder();
Document myDoc=docBuild.parse(inStream, null);
NodeList colors=myDoc.getElementsByTagName("color");
for (int i=0;i<colors.getLength();i++) {
colorItems.add(((Element)colors.item(i)).getAttribute("value"));
}
inStream.close();
}
catch (Exception e) {
e.printStackTrace();
}
setListAdapter(new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, colorItems));
}
public void onListItemClick(ListView parent, View v, int position,
long id) {
mySelection.setText(colorItems.get(position).toString());
}
}
Listing 19-3RawFileExample Java logic for processing the XML resource file
查看RawFileExample
的代码,您会立即注意到我们正在导入的处理文件 I/O 和 XML 解析的外部 Java 库的数量。这就是 Java 遗产在 Android 中的作用。即使您选择将 Kotlin 作为首选编程语言,大量的 Java 库也可以帮助您实现功能。
onCreate()
方法首先创建一个InputStream
对象,然后我们调用getResources().openRawResource()
来执行在.apk
中查找文件的动作,分配它的文件描述符,将它们与InputStream
相关联,最后让系统准备好随后使用来自我们文件的数据流。从那时起,剩下的逻辑就是解释文件中的内容所需要的。
在初始文件处理之后,我们使用一个DocumentBuilder
对象来解析文件的内容,并将结果表示存储在一个名为myDoc
的文档对象中。使用 DOM 语义,我们调用getElementsByTagName()
将所有的<color>
元素收集到我们的NodeList
对象中。考虑到我们文件的简单性,这看起来有些过分,但是想象一个更复杂的 XML 模式,包含其他元素、子元素等等,您可以看到这是如何有效地筛选出我们想要的元素的。
最后,我们使用 for 循环,遍历NodeList <color>
条目,提取 value 属性的文本——这是我们想要在ListView
中呈现的实际颜色名称字符串。填充了我们的NodeList
后,我们可以用配置为使用颜色名称列表的ArrayAdapter
来扩展ListView
,要求它使用默认的simple_list_item_1
内置 XML 布局来呈现结果。
处理用户单击颜色的逻辑检索颜色字符串,并用用户选择的条目填充TextView
。
运行应用会显示在我们的ListView
中呈现的来自colors.xml
文件的数据,如图 19-1 所示。
图 19-1
显示 XML 文件内容的 RawFileExample 应用
使用文件系统中的文件
如果您以前在传统文件系统上使用一般 Java 应用进行过文件 I/O,那么 Android 方法将会非常熟悉。对于那些不熟悉基于 Java 的文件读写的人,这里有一个快速介绍。
从 Java 的角度来看,文件被视为数据流,两个对象成为文件读写的中心:InputStream
和OutputStream
。这些流是通过从代码中调用openFileInput()
和openFileOutput()
方法来提供的。有了流,你的程序逻辑就负责诸如从InputStream
读取或者向OutputStream
写入之类的动作,并且在你完成时清理所有的资源。
Android 的文件系统模型
由于 Android 的历史和谷歌对人们是否应该完全访问自己的设备过于家长式的想法,作为一名开发人员,当处理设备上的本地文件存储时,你将面临两个概念。所有存储都将分为“内部”和“外部”,但这些术语有一种扭曲的含义。在当代的 Android 中,“内部”主要是指你所想的,但“外部”既指传统的外部存储,如 SD 卡,也指普通人认为是内部的一部分板载存储,但 Android 称之为外部,表明你对传统的文件 I/O 有更自由的访问权。
除了将“内部”区域用于与系统相关的目的之外,在考虑 Android 下的文件系统时还存在其他差异,这些差异代表了内部和外部存储的优点和缺点。
内部存储如下:
-
在每一个 Android 设备上都可以找到,并且总是在适当的位置。
-
构成应用的一部分并被指定放在内部存储上的文件被视为应用不可或缺的一部分。这些文件在安装应用时安装,在删除应用时删除。
-
内部保存文件的默认安全边界是您的应用私有的。共享需要明确的附加步骤。
-
通常比可用的外部存储空间小得多,即使有足够的外部存储空间可用,用户也可以看到该存储空间已满,并出现空间管理问题。
外部存储不同,如下所示:
-
Android 为外部存储提供了 USB 抽象层和接口。当用作 USB 设备时,设备上的应用无法访问外部存储器。
-
默认的安全边界是使外部存储上的所有文件都是全局可读的。其他应用可以读取您外部存储的文件,而无需开发人员或用户的知识或许可。
-
根据调用的 save 方法,卸载应用时可能不会移除外部存储的文件。
现在你已经了解了内部和外部存储的这些方面,请继续阅读!
读写文件的权限
如果您选择使用内部存储,那么您的应用总是有权写入和读取为其保留的内部存储部分。要查找应用的任何内部存储的详细信息,请调用getFilesDir()
。您还可以使用getDir()
返回一个命名的(子)目录供您使用,如果它还不存在,就在这个过程中创建它。
您可以通过调用openFileOutput()
打开一个文件进行输出流——也称为写入。如果文件不存在,将为您创建一个。openFileInput()
方法为一个InputStream
执行文件打开,以满足您的读取要求,但是要注意,对于这个调用,您指定的文件必须已经存在。
openFileOutput()
和openFileInput()
都接受许多控制文件和流行为的MODE_*
选项。最常用的MODE_*
选项包括
-
MODE_APPEND:文件中的现有数据不变,字符串中的数据被追加到文件中的现有内容。
-
MODE_PRIVATE:文件上的权限被设置为只允许创建它的应用(以及以相同用户身份运行的任何其他应用)访问该文件。这是默认设置。
-
MODE_WORLD_READABLE:向设备上的所有应用和用户开放读取权限。这被认为是糟糕的安全实践,但当使用内容供应器或服务被认为是过度时,经常会出现这种情况。
-
MODE_WORLD_WRITABLE:比全局可读更危险的是全局可写。任何应用或用户都可以写入该文件。仅仅因为其他开发者使用这个并不意味着你应该这样做!
对于您的应用用户而言,在应用分配的内部文件系统空间内创建、打开或写入内部文件不需要特定权限。创建存储在内部设备存储器中的文件的最简单示例如下:
FILE myFile = new FILE(context.getFilesDir(), "myFileName");
当您开始使用外部存储时,情况会有所不同。您可以使用不同的方法,权限模型严格执行适当的控制和保护措施。为了写入外部存储器,你的 Android 清单需要包含特权android.permission.WRITE_EXTERNAL_STORAGE
,正如我们在第十三章和第十四章的音频和视频示例中看到的。
旧版本的 Android,直到 Android Marshmallow,允许你的应用自由地从外部存储器读取数据,而不需要指定或要求任何特殊的许可。对于最新版本的 Android,您需要在您的清单中包含android.permission.READ_EXTERNAL_STORAGE
。因为包含这个对旧版本没有影响,所以不管您的版本支持计划如何,您应该简单地默认添加这个。
可用于外部存储访问的方法在名称上与前面介绍的用于内部存储的方法非常相似,但倾向于添加“外部”或“公共”一词getExternalStoragePublicDirectory()
方法设计用于分配结构良好的目录和文件,您可以将文档、音频、图片、视频等存储在其中。该方法采用一个表示预定义应用目录之一的枚举和您选择的文件名。
Android 有几十个应用目录,包括
-
DIRECTORY_DOCUMENTS:用于存储用户创建的传统文本或其他可编辑文档。
-
DIRECTORY_MUSIC:存放各种音乐和音频文件的地方。
-
DIRECTORY_PICTURES:用于存储静态图像文件,如照片、绘图等。
所有这些预定义的位置都很有帮助,在它们非常适合的情况下,这些位置具有令人放心的可预测性,但有时您需要存储明显不同类型的文件。对于这些情况,使用通用的getExternalStorageDirectory()
方法,提供与本章前面提到的用于内部存储的getFilesDir()
相似的功能。
检查运行中的外部文件
消化了更多的理论之后,是时候用一个工作示例来探索外部文件了。在ch19/ExternalFilesExample
中找到的ExternalFilesExample
应用,通过保存文件和读回其内容的机制。
图 19-2 是用于提供文本输入域、文件写入和读取按钮以及文本读取域的布局。相应的布局 XML 文件在ch19/ExternalFilesExample
项目中,但是我们将通过不在这里重复它来节省一些空间。
图 19-2
具有用于测试外部文件管理的字段和按钮的活动
我们的应用的支持逻辑遵循我多次使用的模式,一个中央onClick()
方法接收按钮点击,根据用户在运行时选择的视图(按钮)切换到适当的方法。代码如清单 19-4 所示。
package org.beginningandroid.externalfilesexample;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class MainActivity extends AppCompatActivity {
public final static String FILENAME="ExternalFilesExample.txt";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.btnRead:
try {
doReadFromFile();
}
catch (Exception e) {
e.printStackTrace();
}
break;
case R.id.btnSave:
doSaveToFile();
break;
}
}
public void doReadFromFile() throws Exception {
doHideKeyboard();
EditText readField;
readField=(EditText)findViewById(R.id.editTextRead);
try {
InputStream inStrm=openFileInput(FILENAME);
if (inStrm!=null) {
// We will use the traditional Java I/O streams and builders.
// This is cumbersome, and we'll return with a better version
// in chapter 20 using the IOUtils external library
InputStreamReader inStrmRdr=new InputStreamReader(inStrm);
BufferedReader buffRdr=new BufferedReader(inStrmRdr);
String fileContent;
StringBuilder strBldr=new StringBuilder();
while ((fileContent=buffRdr.readLine())!=null) {
strBldr.append(fileContent);
}
inStrm.close();
readField.setText(strBldr.toString());
}
}
catch (Throwable t) {
// perform exception handling here
}
}
public void doSaveToFile() {
doHideKeyboard();
EditText saveField;
saveField=(EditText)findViewById(R.id.editText);
try {
OutputStreamWriter outStrm=
new OutputStreamWriter(openFileOutput
(FILENAME, Context.MODE_PRIVATE));
try {
outStrm.write(saveField.getText().toString());
}
catch (IOException i) {
i.printStackTrace();
}
outStrm.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
public void doHideKeyboard() {
View view = this.getCurrentFocus();
if (view != null) {
InputMethodManager myIMM=(InputMethodManager)
this.getSystemService(Context.INPUT_METHOD_SERVICE);
myIMM.hideSoftInputFromWindow
(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
}
Listing 19-4The ExternalFilesExample Java code
保存和读取文件需要什么
探索ExternalFilesExample
项目,我们看到两个关键方法。首先是doSaveToFile()
方法,它通过调用doHideKeyboard()
(稍后介绍)来执行一些准备和内务处理,然后创建局部saveField
变量并将其绑定到布局中的EditText
视图。这样做是为了我们最终可以引用 UI 中的文本进行保存。
随后是主 try/catch 块,定义输出流,用于将文本传输到由变量FILENAME
指定的文件。然后,我们调用.write()
方法,尝试通过流将文本写入文件。
您可能会注意到在ExternalFilesExample
代码中有许多嵌套的异常处理层。写入文件可能会遇到很多很多问题,从完全存储到用户在写入过程中自发移除正在写入的 SD 卡!简而言之,对于文件访问,对异常要格外小心。
第二,为了从文件中读取,我们使用了doReadFromFile()
方法,遵循与我们使用doSaveToFile()
方法相似的设置工作。我们首先调用doHideKeyboard()
(下面将会介绍),然后本地变量readField
被创建并绑定到editTextRead
小部件。这将用于显示文件读取后的内容。
接下来,我们添加一个try/catch
块,它包含了一些教科书上的 Java 文件处理。我们使用流读取器来访问文件,并传递缓冲区以允许消费者控制对数据的访问。缓冲区用于通过 while 块逐行访问流,我们逐渐在字符串生成器中构建文件的完整内容。从流(以及文件)中读取所有行后,流被关闭,然后我们通过 strBldr 对象将所有内容从缓冲区传输到布局中的readField EditText
小部件。
有更精简、更现代的方法来完成所有这些,但关键是它们隐藏了正在发生的事情的基本机制。在ExternalFilesExample
代码中,您可以看到 Java I/O 如何在最底层发生的混乱细节,以建立对所需对象和工作以及所有可能出错的地方的评估!没有一个头脑正常的人会像今天这样暴露文件访问的编程模式——他们会把它藏起来,即使在幕后所有这些步骤仍然会发生。
帮助简化 ime
我们的代码稍微偏离了使用doHideKeyboard()
方法的严格文件处理。这是一个非常有用的辅助方法,它有助于减少用户在输入文本和执行所需操作时所需的步骤。当用户在EditText
字段中输入文本时,IME 被触发,并显示软键盘供用户输入他们想要的文本。我们可以定制 IME,使用 IME 的“附件按钮”选项添加一个“完成”按钮,但这是一个额外的询问用户的按键。
相反,我精心设计了布局,以确保保存(和读取)按钮即使在 IME 处于活动状态时也是可见的,这意味着用户可以键入,然后立即单击保存按钮。对doSaveToFile()
的调用调用doHideKeyboard()
,它首先确定用户与哪个View
进行了交互,以及输入法框架是否处于活动状态并显示键盘。如果显示了一个,我们调用.hideSoftInputFromWindow()
来隐藏键盘。虽然用户看不到所有这些机制,但他们受益于用户体验中获得的简单性——少按一次键就可以保存他们的文件!
正在保存和读取文件
既然您已经理解了这个ExternalFilesExample
例子,那么是时候看看它是如何实现的了。图 19-3 显示了当用户第一次开始在顶部字段输入文本时,显示屏最初是如何寻找应用的。
图 19-3
输入要保存到外部文件的文本
正如我所承诺的,IME(键盘)出现在屏幕的下半部分,但我们的按钮仍然可以使用。在这个例子中,这更像是一个黑客——它不是一个完全成熟的应用会使用的光鲜亮丽的 UI,而是显示了我们关心的文件 I/O。用户可以随时点击“保存到文件”按钮,触发doSaveToFile()
方法。如本章前面所述,这调用了doHideKeyboard()
方法,此时我们的用户界面将如图 19-4 所示。
图 19-4
IME 随着文件的保存而隐藏
输入到EditText
字段的文本保存在一个名为ExternalFilesExample.txt
的文件中。点击“从文件中读取”按钮,可以随时调出ExternalFilesExample.txt
的内容。这将触发文件的内容被读取,然后通过doReadFromFile()
方法显示。图 19-5 显示了此次文件检索的结果。
图 19-5
调出外部文件的内容
确保外部存储在需要时可用
当我在前面介绍使用外部存储时,我概述了一些潜在的缺点,包括您是否可以在需要时依赖它的不确定性。你的用户可以做一些疯狂的事情,比如从他们的设备中物理移除 SD 卡,甚至对于那些通过内部内存分区模仿外部存储的设备,Android 仍然允许将外部存储作为 USB 设备安装在其他地方,这隐含地切断了其他应用对存储的访问。
作为开发人员,您的目标应该是创建行为良好的应用,即使您的用户并不是这样!这意味着在应用尝试使用外部存储之前,对外部存储的存在和可用性进行健全性检查是明智的。
为此,Android 提供了一些有用的环境方法,其中最有用的是Environment.getExternalStorageState()
,它从一个预定义的 enum 返回一个字符串,描述外部存储的当前状态。您可以使用此状态来确定外部存储的可用性、健康状况等。返回的常见值包括
-
MEDIA_BAD_REMOVAL:此状态表示物理 SD 卡在卸载前已被移除,由于缓存页面未被刷新,可能会使文件处于不一致状态(请参阅本章后面的文件系统讨论)。
-
MEDIA_REMOVED:当没有从板载设备映射外部存储并且不存在 SD 卡时,返回该值。
-
MEDIA_SHARED:当设备将其外部存储作为 USB 设备安装到某个其他外部平台时,这是返回的值,指示此时外部存储不可用,即使它存在于设备中。
-
MEDIA_CHECKING:当插入 SD 卡时,会执行检查以确定该卡是否已被格式化,如果是,则使用哪个文件系统。这是这些过程发生时返回的值。
-
MEDIA_MOUNTED:可以使用的外部存储器的正常状态。
-
MEDIA_MOUNTED_READ_ONLY:通常在 SD 卡的物理开关设置为只读位置时出现,这意味着不能写入外部存储的该部分。
developer.android.com 的 Android 文档有所有可能的外部存储状态值的完整列表。
Android 文件系统的其他考虑事项
现在,您已经熟悉了在 Android 中处理文件的各种方法,要确保在文件系统中使用文件的长期可行性,需要考虑一些微妙和不那么微妙的管理问题。
历史上的 Android 文件系统
在 Android 作为智能手机操作系统的历史上,它支持一系列板载存储文件系统标准。历史上的三种主要形式是
-
YAFFS,或另一个闪存文件系统:基于 NAND 的存储的原始文件系统,它提供了许多有用的好处,包括磨损平衡支持,以便管理闪存存储随时间的衰减,并在一定程度上对操作系统和应用隐藏,以及文件系统级垃圾收集工具,以帮助将存储的坏区域移动到“死池”,而不是用于有意义的存储。
-
YAFFS2 和 YAFFS 的进化和调整版本:为底层存储提供更好的长期健康管理。
-
EXT4,Linux 普及的文件系统:具有成熟文件系统的所有“成熟”管理特性,包括每个文件的锁定语义、权限等等。
基于旧的YAFFS
和YAFFS2
的文件系统以及使用它们的设备的一个问题是缺少文件锁定语义。简而言之,作为开发人员,两者都没有提供锁定单个文件的选项(例如,当编辑共享文件时),相反,您依赖于锁定“整个文件系统”来确保一致的访问。这有一系列的缺点,从阻止其他可能试图同时写入文件的应用,到如果文件 I/O 发生在主线程上,会妨碍有效的 UI 行为。
作为开发人员,您的主要问题是不知道用户的设备可能使用什么文件系统。你很可能会因为 I/O 锁定和阻塞问题而受到性能不佳的指责,即使可能是 Android 本身导致了这个问题。
避免文件 I/O 的 UI 问题
作为开发人员,您可以使用一系列技术来缓解与YAFFS
或YAFFS2
文件系统有关的锁定和争用问题。这些技术通常也可以帮助对网络端点的其他类型的 I/O。
使用 StrictMode 分析应用
Android 生态系统提供了一系列工具来帮助执行应用行为和性能。StrictMode 策略工具就是这样一种工具,它通过分析所有代码的操作来寻找策略中定义的问题,从而帮助解决任何 I/O 延迟问题。
StrictMode 有一系列可用的策略,尽管您可能会发现自己正在使用它的两个原始产品。第一个策略是虚拟机策略,它涵盖了整个应用中通常不良的行为或实践,比如泄漏数据库连接对象。第二组策略是线程策略,这些策略特别关注在主 UI 线程上出现的表现不佳的代码。这有助于发现那些会降低或干扰用户流畅的用户界面体验的代码——无论是你的还是安卓的。
您可以通过从 onCreate()回调调用静态的StrictMode.enableDefaults()
方法来激活 StrictMode 策略。调用这个调用将在 Logcat 输出中报告一系列关于 UI 线程问题的有用信息,包括文件 I/O 问题。如果你愿意,你也可以定义你自己的策略——具体细节超出了本书的范围,但是如果你感兴趣的话,Android 文档有更多的细节。
Caution
尽管 StrictMode 策略非常有用,但千万不要在最终发布的代码和应用中定义它们。保留 StrictMode 将在用户的设备上创建大量的日志数据,消耗掉您一直在努力管理的文件系统空间。
将逻辑移动到异步线程
前面关于 StrictMode 的讨论打开了将逻辑从应用的主 UI 线程和接口移开的世界。几乎在任何时候,都值得考虑应用中是否有其他逻辑不需要发生在关键路径上,例如从在线服务中后台查找数据、消息传递或发布/订阅样式的通知、缓存的项目等等。
关键路径之外可能发生的任何事情都应该被考虑用于异步操作,这就是 Android 的AsyncTask
的亮点,能够产生额外的线程来处理您扔给它的任何逻辑。作为 Android 学习的一部分,这是非常值得掌握的,因为大多数开发人员将它作为管理应用线程的主要工具。
AsyncTask
类是以一种形式提供的,这意味着作为开发人员,您必须对它进行子类化,以便为您想要做的工作创建特定的实现。这是有道理的,因为 Android 不能提前知道你的应用的细节,也不能覆盖全世界开发者希望它处理的数百万个任务。要使用AsyncTask
,您需要获取它提供的doInBackground()
方法,并实现您想要在另一个线程上执行的实际逻辑。您可以实现一些可选的附加方法来提供执行前和执行后的逻辑,以可控的方式与 UI 进行交互,等等。
清单 19-5 给出了一个存根,显示了AsyncTask
的子类,以说明如何使用它来执行文件保存操作。有无数的其他方式可以实现这一点,但你会欣赏整体的想法。
private class SmartFileSaver extends AsyncTask<Void, Void, Void> {
protected void onPreExecute() {
// This method will fire on the UI thread
// Show a Toast message
Toast.makeText(this, "Saving File", Toast.LENGTH_LONG).show();
}
protected void doInBackground() {
// This method will spawn a background thread
// All work happens off the UI thread
// create output stream
// call .write()
// catch exceptions
// etc.
}
protected void onPostExecute() {
// This method will fire on the UI thread
// Show a Toast message
Toast.makeText(this, "File Saved", Toast.LENGTH_LONG).show();
}
}
Listing 19-5An example AsyncTask subclassing
使用SmartFileSaver.execute()
方法将调用我们的各种onPreExecute()
、doInBackground()
和onPostExecute()
方法,由 Android 管理相关的线程生存期和 UI 交互。
摘要
现在,您已经对 Android 下的文件 I/O 的基本机制有了一个很好的了解,特别是对文件系统、文件处理、流和文件内容机制有了一个基本的了解,这些都是处理文件的方法的一部分。
二十、在 Android 中使用数据库
文件并不是将应用中的信息存储到 Android 设备上的唯一方式。Android 提供了另外两种主要的信息管理方法:基于 SQLite 的成熟的关系数据库选项和 Android 内容提供者框架。在这一章中,我们将探索 SQLite 数据库——对于那些对内容供应器感兴趣的人,你可以在本书的网站 www.beginningandroid.org
上了解更多信息。
如果您熟悉 SQLite,您会意识到它代表了一个坚如磐石的数据库引擎,作为一个包含或库提供给任何类型的应用。SQLite 最近作为一个发布产品已经有 25 年的历史了,证明了自己是软件开发史上的中流砥柱之一。
使用 SQLite:世界上最流行的数据库!
任何在数据库领域工作过的人都会对 SQLite 很熟悉,但是尽管它很流行,来自其他领域的人可能从未听说过它。毫不夸张地说,SQLite 是这个星球上“最流行的”关系数据库技术。这是一个大胆的主张,所以让我提供一些支持的证据。
为了让您了解 SQLite 有多普遍和受欢迎,我整理了这个简短的数据点列表来帮助您。
SQLite 是
-
每个智能手机操作系统都提供默认的核心关系数据库。我们显然在这本书里谈论的是 Android,但是你听说过的其他智能手机操作系统——比如 iOS——以及你可能没有听说过的操作系统,比如 Symbian,都使用 SQLite 作为默认数据库来满足几乎所有的内部需求。
-
每个 web 浏览器都选择它作为本地缓存、书签等的关键技术。无论你使用 Chrome、Safari、Opera、Edge 还是 Internet Explorer(还记得吗?!),你每天都在用 SQLite。
-
包括在数以百万计的商业和开源产品中。我说几百万不是夸张!
我最喜欢的说明 SQLite 有多受欢迎的方式之一是将其描述如下。亲爱的读者,从你购买智能手机的第一天起,你每天都在使用 SQLite 或从中受益。即使您没有意识到,您也是一个会走路、会说话的 SQLite 受益人!
Note
为了深入 SQLite 的世界,我可以推荐SQLite的权威指南,第二版,ISBN 9781430232254。为了完全公开,我是那本书的合著者之一。
很多人对 SQLite 一点都不熟悉。您将不会探索 SQLite 在 Android 下的功能或利用它的优势。这完全在意料之中,也是本章存在的原因。从现在开始,我们将介绍使用 SQLite 作为数据库和 Android 核心功能的基础知识,并且我们将构建一个数据库驱动的示例应用来磨练您的技能。
Android 开发快速学习 SQLite
SQLite 的目标是作为一个简单的数据库库,提供您可能希望从关系数据库查询和事务引擎中得到的所有核心内容。这意味着您可以访问完全兼容的结构化查询语言(SQL)接口,尽管记住您可能感兴趣的任何功能中引入的 SQL 级别(如 SQL-92 和 SQL-99)总是很重要的。这一点尤为重要,因为用户设备附带的 SQLite 版本通常比最新版本落后好几年。
SQLite 支持普通的 SQL 命令,如SELECT
、INSERT
、UPDATE
和DELETE
,但根据版本的不同,可能不支持以后 SQL 发展的一些关键特性。这可以包括
-
仅支持 ANSI 外部连接语法的子集
-
最小的 alter table 支持,允许您重命名和添加列,但不能删除列或改变数据类型
-
支持行级触发器,但不支持语句级触发器
-
视图是只读的
-
不支持窗口函数和公共表表达式,即使这些是在 3.25 版中添加到 SQLite 中的——只有支持 Android SDK 级或更高级别的设备才会提供支持窗口函数的 SQLite 版本
您可能会担心这些缺失的特性,但实际上它们大多是在数据库使用的高端,而不是小型嵌入式数据库库的日常需求。即使没有这些更新的特性,您仍然可以获得 SQL 的强大功能。
为您的应用创建 SQLite 数据库
当使用 SQLite 数据库启动应用时,有两种方法可供选择:
-
创建一个 SQLite 数据库文件作为开发环境的一部分,或者从外部获取并作为资源复制到您的 Android 项目中。
-
让您的 Android 应用创建它需要的数据库,并可选地填充任何初始数据。
每种方法都有优点和缺点。通过打包一个预制的 SQLite 数据库,您就可以让数据库模式和代码开发保持同步——尽管这并不是 Android 独有的问题。让应用创建数据库可以减轻您的负担,但是根据 SQLite 数据库可能需要的数据种类和数量,这可能会给应用带来沉重的启动负担。Android 可以在这方面提供帮助,因为它提供了一系列 SQLite 设置帮助选项。
Android 提供了SQLiteOpenHelper
类,专为您在应用中创建子类而设计。SQLiteOpenHelper
负责 SQLite 数据库的所有初始设置,并处理未来的更改和升级。你的工作是实现(至少)父SQLiteOpenHelper
类中的三个方法,第四个降级方法也是可选的。
您的第一项工作是向SQLiteOpenHelper
构造函数添加逻辑,调用父构造函数作为基础。父类负责检查指定的数据库文件是否已经存在,并在需要时创建该文件。构造函数还对所提供的版本进行版本检查,并在必要时调用onUpgrade()
和onDowngrade()
方法,以及其他一些超出本文介绍范围的深奥任务。
其次,您必须实现onCreate()
方法的逻辑。这是您构建和执行数据定义语言(DDL) SQL 命令的地方,根据您的数据库模式设计来创建您的表、索引、视图等等。在 SQLite 数据库中创建对象之后,您应该通过 insert 和 update 语句填充您需要的任何数据。
最后,您需要实现onUpgrade()
(以及可选的onDowngrade()
方法)。这些方法处理实现模式更改的 DDL,以及在升级应用并决定 SQLite 数据库结构需要更改以支持所需应用行为时,您希望进行的任何相关数据更改。
学习了在应用中使用 SQLite 的理论之后,是时候探索一个示例应用了,它将有助于将这些概念付诸实践。
SQLiteExample 应用简介
在本章的剩余部分,ch20/SQLiteExample
应用将用于突出显示在构建数据库驱动的 Android 应用时您可能想要使用的所有关键 SQLite 功能。我们将使用简单的LinearLayout
和ListView
来帮助演示 SQLite 数据库中数据的使用和显示。图 20-1 显示了包含一个ListView
的用户界面,用于显示 SQLite 数据库中已知的 Android 设备型号。我们有添加新设备型号和显示已知设备信息的按钮。
图 20-1
SQLiteExample 的主要活动和外观
这种简单的布局和视图您现在应该已经很熟悉了,所以我们就不赘述了。清单 20-1 显示了布局。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/buttonGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/addNewModel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add New Model"
android:onClick="onClick"/>
<Button
android:id="@+id/getModelInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Show Model Info"
android:onClick="onClick"/>
</LinearLayout>
<ListView
android:id="@android:id/list"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</LinearLayout>
Listing 20-1The SQLiteExample main activity layout
关于这种布局有两点需要注意。首先,我定义了两个嵌套的LinearLayouts
。最外层的LinearLayout
具有属性orientation=vertical
,包含内层的LinearLayout
和具有股票 Android id 的ListView
。内部的LinearLayout
装有两个按钮addNewModel
和getModelInfo
以及orientation=horizontal
。这是一个有用的技巧,可以让 UI 小部件按照我们想要的方式流动,但是一个更优雅的解决方案可以通过合适的权重、重力和布局参考来设计。
第二,我使用两个按钮调用一个onClick()
方法的常见模式,该方法将让匹配的 Java 代码确定哪个按钮被单击,并从那里引导逻辑。
回头看图 20-1 ,可以看到已经列出了几个设备的一些数据。这意味着某个数据库中的数据已经被用来演示应用。我在SQLiteExample
应用中使用的SQLiteOpenHelper
的实现中执行了这个操作。清单 20-2 显示了这个实现的 Java。
package org.beginningandroid.sqliteexample;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class MySQLiteHelper extends SQLiteOpenHelper {
public static final String TABLE_NAME="devices";
public static final int COLNO__ID = 0;
public static final int COLNO_MODEL_NAME = 1;
public static final int COLNO_RELEASE_YEAR = 2;
public static final String COLNAME__ID = "_id";
public static final String COLNAME_MODEL = "model_name";
public static final String COLNAME_YEAR = "release_year";
public static final String[] TABLE_COLUMNS =
new String[]{"_id","model_name","release_year"};
private static final String DBFILENAME="devices.db";
private static final int DBVERSION = 1;
private static final String INITIAL_SCHEMA=
"create table devices (" +
"_id integer primary key autoincrement," +
"model_name varchar(100) not null," +
"release_year integer not null" +
")";
private static final String INITIAL_DATA_INSERT=
"insert into devices (model_name, release_year) values " +
"('LG Nexus 4', 2012)," +
"('LG Nexus 5', 2013)," +
"('Samsung Galaxy S6', 2015)";
public MySQLiteHelper(Context context) {
super(context, DBFILENAME, null, DBVERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(INITIAL_SCHEMA);
db.execSQL(INITIAL_DATA_INSERT);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// perform upgrade logic here
// This can get quite complex
if (oldVersion==1) {
// do upgrade logic to new version
}
// and so on
}
}
Listing 20-2The MySQLiteHelper SQLiteOpenHelper implementation
MySQLiteHelper
的 Java 清单显示了本章前面概述的助手需求。构造函数获取文件名和版本信息,并使用它来检查 SQLite 数据库文件是否存在,如果需要就创建它。我选择了文件名“devices.db
”作为 SQLite 数据库文件的一个有意义的名称。
查看onCreate()
,您可以看到它连续两次调用execSQL()
方法,这是您第一次接触到用于与 SQLite 数据库交互的常用方法。execSQL()
提供了许多不同的参数和返回信息,但是最简单的是将一个String
SQL 语句作为参数,这是由 SQLite 库执行的 SQL 命令。SQL 语句的成功执行通常不会提供返回值——这是“没有消息就是好消息”的原则。
与execSQL()
调用一起使用的每个 SQL 语句都是在类顶部的常量声明中构造的。这种基于常量的语句方法并不新鲜,但是值得注意这里使用的一些额外的常量:
-
三个
COLNO_*
常量中的每一个都代表我定义的表中列的列号(或序号位置)。所以 _id 列(在下面讨论)在位置 0,model_name 列在位置 1,依此类推。这些位置对于一些 SQLite helper 方法非常重要,这些方法隐式地使用表的默认列顺序来返回数据。 -
TABLE_COLUMNS
是表中列名的String
数组。我们将要探索的许多方法都利用了这个名称集合。
回到表的_id
列,这利用了 SQLite 的自动增量特性,让它为每一行生成一个惟一的整数值,作为表的主键。名称_id
是许多内置 Android 工具、助手类和方法使用和期望的约定。我建议对所有 SQLite 表都采用这种设计传统,至少在您理解不同做法的后果之前。
通过execSQL()
调用调用的下一个 SQL 语句是INITIAL_DATA_INSERT
语句。该语句执行多行插入,将一组初始数据引导到模式中的一个表中。这种数据播种完全是可选的,您肯定会遇到想要这样做的情况和不需要这样做的情况。insert 语句本身使用的语法仅在 SQLite 的更高版本中受支持,因此在 Android 的更高版本中也受支持。有关 SQLite 版本和功能与 Android 版本的更多详细信息,请查看本书网站上的额外资料,网址为 www.beginningandroid.org
。
helper 类中的最后一个方法是onUpgrade()
的框架。在示例应用中,我们正在处理应用的第一个版本(DBVERSION
等于 1,用于构造函数调用)。我留下了逻辑概要,您可以用它来处理所提供的oldVersion
和newVersion
值,以决定作为应用升级的一部分,可能需要哪些模式更改、数据更改或其他变更操作。当谈到数据库模式升级时,您会在网上看到许多例子,人们简单地删除并重新创建一个数据库作为onUpgrade()
实现。这是一个务实的黑客,但它在一定程度上起作用,只要你不在乎你的用户的数据!如果您希望您的用户存储任何有价值的数据,并且希望即使在数据库模式升级时也能保留这些数据,那么请小心这种方法!
创建数据库驱动的活动
了解了SQLiteOpenHelper
类的用途和结构后,您就可以用它来构建一个有用的数据库驱动的应用了。在Ch20/SQLiteExample
应用中,您会看到如清单 20-3 所示的逻辑。这个清单很长,尽管我省略了另一个名为DialogWrapper
的助手类以节省空间。继续阅读 Java 代码,然后浏览逻辑。
package org.beginningandroid.sqliteexample;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.ContentValues;
import android.content.DialogInterface;
import android.os.Bundle;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
public class MainActivity extends ListActivity {
private SQLiteDatabase myDB;
private MySQLiteHelper myDBHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myDBHelper = new MySQLiteHelper(this);
myDB = myDBHelper.getWritableDatabase();
displayModels();
}
public void onClick(View view) {
switch(view.getId()) {
case R.id.addNewModel:
addModel();
break;
case R.id.getModelInfo:
getModelInfo(view);
break;
}
}
public List<String> getModels() {
List<String> models = new ArrayList<>();
Cursor cursor = myDB.query(MySQLiteHelper.TABLE_NAME,
MySQLiteHelper.TABLE_COLUMNS, null, null, null, null, null);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String model = cursor.getString(MySQLiteHelper.COLNO_MODEL_NAME);
models.add(model);
cursor.moveToNext();
}
cursor.close();
return models;
}
public void displayModels() {
List<String> modelEntries = getModels();
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,
android.R.layout.simple_list_item_1, modelEntries);
setListAdapter(adapter);
}
public void getModelInfo(View view) {
Cursor cursor = myDB.rawQuery(
"select _id, model_name, release_year " +
"from devices", null);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String model = cursor.getString(MySQLiteHelper.COLNO_MODEL_NAME);
Integer year = cursor.getInt(MySQLiteHelper.COLNO_RELEASE_YEAR);
Toast.makeText(this, "The " + model +
" was released in " + year.toString(),
Toast.LENGTH_LONG).show();
cursor.moveToNext();
}
cursor.close();
}
private void addModel() {
LayoutInflater myInflater=LayoutInflater.from(this);
View addView=myInflater.inflate(R.layout.add_model_edittext, null);
final DialogWrapper myWrapper=new DialogWrapper(addView);
new AlertDialog.Builder(this)
.setTitle(R.string.add_model_title)
.setView(addView)
.setPositiveButton(R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
insertModelRow(myWrapper);
}
})
.setNegativeButton(R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
// Nothing to do here
}
})
.show();
}
private void insertModelRow(DialogWrapper wrapper) {
ContentValues myValues=new ContentValues(2);
myValues.put(MySQLiteHelper.COLNAME_MODEL, wrapper.getModel());
myValues.put(MySQLiteHelper.COLNAME_YEAR,
Calendar.getInstance().get(Calendar.YEAR));
myDB.insert(MySQLiteHelper.TABLE_NAME,
MySQLiteHelper.COLNAME_MODEL, myValues);
//uncomment if you want inserts to be displayed immediately
//displayModels();
}
@Override
public void onDestroy() {
super.onDestroy();
myDB.close();
}
}
Listing 20-3The main SQLiteExample activity
对于所有基于 SQLite 的 Android 开发,需要记住前面代码中的一些关键实现步骤。对于任何基于 SQLite 的 Android 应用来说,关键的一步是从您的助手类创建一个对象,并确保在需要它的活动的生命周期中保留它。这通常不会太难,我在 launcher activity 中创建它的方法很常见。
现在您已经有了助手对象,任何时候您需要使用数据库,您与它的交互将从调用它的getReadableDatabase()
或getWritableDatabase()
方法开始,为您的底层 SQLite 数据库返回一个数据库对象。正如方法名所示,可读的数据库对象仅用于通过SELECT
查询读取数据库,而可写的版本允许 DML 语句,如INSERT
、UPDATE
、DELETE
和 DDL 语句,用于我们的onCreate()
助手类方法中使用的对象创建和更改。
当您使用数据库对象完成一个给定的任务时,只需调用它的.close()
方法,助手类就会整理好。这通常是在onDestroy()
或类似活动中完成的活动的一部分。
SQLiteExample
应用创建助手对象并使用getWritableDatabase()
以可写模式访问数据库,继续使用getModels()
方法填充modelEntries
列表。有了这项工作的结果,它就会向我们的ArrayAdapter
提供它需要的东西,用从数据库返回的数据来膨胀ListView
。getModels()
方法是一个简短但强大的方法,因为它引入并使用了关于 SQLite 数据库和 Android 的两个主要功能。第一个概念是查询助手方法,它从 SQLite 数据库收集数据,第二个概念是用于管理返回结果的游标对象。熟练使用 SQLite(和其他)数据库需要掌握这两种技术,所以让我们更深入地研究一下。
为 SQLite 和 Android 选择查询方法
在应用中使用 SQLite 数据库时,您可以选择两种主要方法来检索它存储的数据。每种方法都利用了SELECT
语句,但是不同之处在于为开发人员提供了多少指导和假设的结构。
使用查询构建过程
正如在SQLiteExample
应用的getModels()
方法中看到的,方法 1 是使用query()
方法。通过使用query()
,您可以访问一个非常结构化的路径,为查询构建您想要或需要的列、源表、谓词逻辑等等,直到您得到将在 SQLite 数据库上发布的最终表单。
要使用query()
,您不需要自己直接编写 SQL SELECT
语句。相反,您将逐步完成一组预定义的构建阶段,使用query()
方法在幕后为您构建 SQL 语句:
-
提供要在查询中使用的表的名称。
-
提供要选择的列名(如果您使用正式的关系数据库命名法,则为“project”)。
-
为 where 子句提供谓词,包括任何可选的位置参数。
-
如果使用位置参数,请提供参数值。
-
提供任何 GROUP BY、HAVING 或 ORDER BY 子句。
如果您不需要查询的某个部分,那么您只需在相关的参数位置为query()
调用提供 null。您可以在我们调用的查询代码SQLiteExample
中看到这一点:
myDB.query(MySQLiteHelper.TABLE_NAME, MySQLiteHelper.TABLE_COLUMNS,
null, null, null, null, null)
这意味着我们没有使用任何谓词向 SQL 语句的WHERE
子句添加逻辑,没有语句的参数,也没有使用任何GROUP BY
、HAVING
或ORDER BY
选项。
这看起来很简单,实际上也是如此。这种方法的一个明显的局限性就潜伏在第一步。您为您的查询提供了表的名称——但是只有一个表。这就是缺点!使用query()
方法,您只能查询单个表,这意味着没有连接,更微妙的是,您也不能使用任何机制,比如子选择、相关查询或任何其他引用任何其他表的技术。
使用 SQL 的原始功能
如果您渴望突破query()
方法的限制,那么rawQuery()
几乎为您提供了 SQL 的全部功能。顾名思义,rawQuery()
接受一个表示 SQL 语句的“原始”字符串作为参数,并接受一个可选的位置参数数组,如果您选择在 SQL 语句中使用它们的话。当查询不需要参数化时,将 null 作为第二个参数传递。
您可以在SQLiteExample
逻辑中的getModelInfo()
实现中看到rawQuery()
的动作:
myDB.rawQuery("select _id, model_name, release_year " +
"from devices", null)
有了rawQuery()
,您可以带着您能找到的任何 SQL 工具进城,包括 SQL 标准中 SQLite 支持的任何东西,比如嵌套子选择、连接等等。这种广泛的权力有其自身的考虑。使用rawQuery()
管理一小组静态查询是没问题的,但是表示查询的字符串变得复杂,或者如果您通过动态创建 SQL 文本来构建越来越大的语句,那么您应该担心一些普遍的问题。首当其冲的是被称为 SQL 注入的一类安全问题。SQL 注入漏洞不是 Android 问题,本质上也不是 SQLite 问题。这些问题会影响任何使用数据库的应用。避免 SQL 注入的关键是字符串“净化”的概念,在这种情况下,您要确保创建的 SQL 不仅有效,而且不会超出您的预期范围。
Caution
当您完全控制并负责查询语法、正确性等时,伴随着原始 SQL 的强大功能而来的是可能突然出现的所有复杂性和问题。如果您是 SQL 的初学者,我建议您先用查询构建器方法进行测试,然后再用 rawQuery()和查询构建器一起工作,这样您就可以看到缺陷和尖锐的地方了!
使用游标管理查询结果
无论您选择哪种查询执行模型来处理您的数据库,结果都会作为一个称为Cursor
的对象呈现给您(或您的应用)。A Cursor
本质上是几乎所有数据库库中都有的相同概念,所以如果您曾经使用过其他数据库及其编程接口,那么下面的描述将会很熟悉。如果您不熟悉数据库和/或游标,您可以将它们视为查询产生的完整数据集和指向结果集中当前感兴趣行的指针(或游标位置)。想想你最喜欢的文本编辑器或文字处理器,以及光标如何位于文档中整个文本主体的某一点,你就有了一个大概的想法。在数据库库中,您可以将光标视为整个结果集和该结果集中的当前位置。
作为开发人员,这种数据集加指针到位置的隐喻是理解Cursor
对象功能的关键。A Cursor
让您能够做以下事情以及更多事情:
-
移动光标位置,用
moveToFirst()
和moveToNext()
等方法迭代结果集,用isAfterLast()
测试位置。 -
使用
getString()
、getInt()
和其他数据类型的等效方法从当前行提取单个列值。 -
使用
getColumnNames()
和getColumnIndex()
查询结果集,以了解列名、顺序列位置等。 -
使用
getCount()
获取结果集的统计信息。但是,请注意,尝试对结果进行计数会强制按顺序读取游标的整个结果集,这可能会消耗大量内存,并且对于较大的结果会花费时间。 -
使用
close()
方法释放所有的Cursor
资源。
在Cursor
中处理结果的一种常见方式是使用循环逻辑构造来遍历它的行,在每行上执行您需要的任何应用逻辑。SQLiteExample
应用在几个地方做到了这一点,比如来自getModels()
的这个片段:
Cursor cursor = myDB.query(MySQLiteHelper.TABLE_NAME,
MySQLiteHelper.TABLE_COLUMNS, null, null, null, null, null);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String model = cursor.getString(MySQLiteHelper.COLNO_MODEL_NAME);
models.add(model);
cursor.moveToNext();
}
cursor.close();
这展示了对 SQLite 数据库对象使用的query()
方法,我们将表名和带有我们感兴趣的列名的String
数组传递给该对象。我们得到的回报是一个Cursor
,其结果集看起来如清单 20-4 所示。
_id model_name release_year
--- ------------------ ------------
1 Pixel 3 2018
2 Pixel 5 2020
3 Samsung Galaxy S21 2021
Listing 20-4Sample cursor result set for the SQLiteExample activity
为了处理结果Cursor
,我们在进入循环之前调用.moveToFirst()
方法,将光标的当前行放在_id=1
行。当我们遍历循环时,我们测试我们没有移动到使用isAfterLast()
的游标的结果集的末尾,从而避免读取超出我们的游标范围的任何错误。我们通过传递常数COLNO_MODEL_NAME
来调用getString()
,表示model_name
列的列位置。这个字符串被添加到我们的ArrayList
中,然后我们调用moveToNext()
继续处理下一行。
Caution
在处理数据库(SQLite 或其他)时,总是依赖这种迭代行处理方法是非常有吸引力的。然而,隐藏在这种模式中的是所有数据库相关编程中最常见的性能陷阱。While 循环和 for 循环易于编码,但是不能以与数据库引擎使用本机 SQL 执行 set 逻辑的能力相匹配的方式进行扩展。无论您是编程新手还是老手,都应该记住,对于大规模计算和处理数据来说,SQL 几乎总是最佳选择。对于数据处理逻辑之外的事情,可以在代码中使用基于 Java 或 Kotlin 的迭代循环,比如将数据绑定到 UI 小部件或执行非 SQL 类处理。但是要保持警惕,以确保你的用户有良好的表现!
超越简单的迭代和手动处理,您可以使用一个Cursor
对象来播种一个SimpleCursorAdapter
,用于与一个ListView
或其他选择 UI 小部件绑定。在使用任何CursorAdapter
选项或子类时,您需要遵循本章前面介绍的表格结构模式。您必须用名为_id
的主键列创建您的表,并用 autoincrement 属性设置它。所有适配器的方法都需要_id
列及其值,比如onListItemClick()
。
当达到光标的极限时,还有更高级的方法来处理结果,比如使用SQLiteDatabase.CursorFactory
对象和queryWithFactory()
和rawQueryWithFactory()
方法。这些超出了本书的范围,但是你可以在developer.android.com
了解更多。
使用您的 Android 应用修改数据
从数据库中读取数据当然非常有用,但是对于数据库驱动的应用来说,这只是故事的一半。几乎在所有情况下,您都希望您的用户添加、更新和删除 SQLite 数据库中的信息,这意味着希望对您的数据库使用 SQL 的INSERT
、UPDATE
和DELETE
SQL DML 语句。
Android 对 SQLite 的支持扩展到提供两种方式来执行修改数据的 DML 语句。首先是使用execSQL()
方法,向其传递一个完整的 SQL 语句。回头看看SQLiteExample
应用,当数据库第一次被创建时,它通过 helper 类使用这种方法。如前所述,execSQL()
适用于任何不期望返回结果或光标的语句。因为INSERT
、UPDATE
和DELETE
语句不返回结果,所以它们符合要求。在最新版本的 Android 以及 SQLite(3.24 及更高版本)中,这包括了INSERT
的“UPSERT
变体,它实现了对通过INSERT
语句更新现有行的ON CONFLICT
支持。
如果execSQL()
让你感觉像是在凭自己的感觉飞行,那么另一种选择是使用SQLiteDatabase
对象的.insert()
、.update()
和.delete()
方法,以与.query()
方法帮助你构建你想要的SELECT
语句相同的方式逐步实现 DML。这些方法都使用了一个ContentValues
对象,它为您提供了一个 SQLite 定制的值和列的映射。
插入数据
从SQLiteExample
开始回顾insertModelRow()
方法,您将看到.insert()
方法正在运行:
private void insertModelRow(DialogWrapper wrapper) {
ContentValues myValues=new ContentValues(2);
myValues.put(MySQLiteHelper.COLNAME_MODEL, wrapper.getModel());
myValues.put(MySQLiteHelper.COLNAME_YEAR,
Calendar.getInstance().get(Calendar.YEAR));
myDB.insert(MySQLiteHelper.TABLE_NAME,
MySQLiteHelper.COLNAME_MODEL, myValues);
//uncomment if you want inserts to be displayed immediately
//displayModels();
}
由于使用了助手类常量,清单中对.insert()
的调用很清楚。看看常量本身,第一个参数采用我们要插入的表的名称,第二个参数提供表中可以接受 nulls 的列,这是 Android 的要求,而不是我的设计选择。它的目的是避免 SQLite 有时奇怪的插入行为,如果最后一个参数—ContentValues
对象为空,被称为“null insert hack”
在使用ContentValues
对象之前,通常用想要使用的相关数据填充它。在SQLiteExample
应用中,我们通过对象的.getModel()
方法用来自DialogWrapper
对象的String
数据填充它。打开DialogWrapper
的 Java 源代码,你会看到.getModel()
方法返回用户在点击“添加新模型”按钮触发的弹出对话框中输入的文本,如图 20-2 所示。
图 20-2
提示在 SQLiteExample 应用中插入新数据
输入的任何文本都将成为对ContentValues
对象的.put()
调用的值,以及COLNAME_MODEL
常量的String
值,后者充当键的角色。为了得出带有年份值的,我们使用一种常见的 Java 技术来确定当前年份。如果您对扩展SQLiteExample
应用感兴趣,您可以将其调整为弹出对话框和相关逻辑的一部分。
更新数据
用.update()
方法更新数据大体上类似于.insert()
方法。除了提供一个表示要更新的表名的String
和一个表示要更新的一列或多列的新值的 ContentValues 对象之外,还有一个额外的考虑是,您还可以提供一个可选的 where 子句。这个 where 子句允许您添加任何谓词逻辑,以细化要更新的目标行。它可以包含问号?
,作为在执行语句之前替换的值的占位符,以及一个最终参数,该参数是一个值列表,用于替换任何?
参数占位符。这是一种非常常见的参数替换技术,也是用于防范 SQL 注入的技术之一(但不是唯一的)。您可以用您确定需要的任何逻辑来保护在.update()
中使用的任何参数值——而不仅仅是相信用户的直接输入。
这种方法很容易使用,但是它的简单是有代价的。用于更新的值必须是实际的静态值。您不能通过.update()
方法将公式或计算传递给 SQLite 进行计算。在你需要这种力量的情况下,用execSQL()
方法来代替。
删除数据
我们管理 SQLite 数据的助手方法之旅的最后一站是.delete()
语句。它也非常类似于您已经看到的.insert()
和.update()
语句,最接近于.update()
。.delete()
的关键区别在于您不需要提供任何新的数据值,因为您是在删除数据,而不是调整数据!
调用.delete()
,方法是向它提供包含要删除的数据的表的名称,还可以选择包含一个 where 子句和谓词所需的任何参数值,以指向您希望受影响的行的子集(假设您不想删除表中的所有内容)。例如:
myDB.delete(MySQLiteHelper.TABLE_NAME, "_id=?", args);
您将提供一个填充的args
值,在我们的例子中,它将是保存手机型号数据的一行的_id
值,例如,“2”表示_id
2。SQLite 看到并执行的语句如下所示:
delete from devices where _id=2
同样的警告也适用于.delete()
,就像你在.update()
中看到的一样。不能在参数中传递任何计算或动态公式以供 SQLite 评估。要达到那个复杂程度,你应该再次选择execSQL()
。
使用房间持久性库
使用原始 SQL 语句,甚至通过查询构建器方法寻求帮助,仍然需要您掌握 SQL 语言和关系数据库概念方面的知识。总的来说,这是一个非常好的想法。一些开发人员回避 SQL,认为与 Java 和 Kotlin 等语言相比,它太难使用或太“奇怪”。
一般来说,还有第三种处理数据库的方法,称为对象关系映射(ORM)库。这些方法各有利弊,但本质上给了您一种更像 Java(或 Kotlin)的方式来处理数据库。在 Android 世界中,Google 已经将 Room Persistence 库作为 androidx (Jetpack)的一部分提供。学习 ORM,包括 Room,可以帮助您的代码不容易在原始 SQL 中出错,并且更容易理解。房间本身是一个非常大的话题,你可以在 https://developer.android.com/jetpack/androidx/releases/room
了解更多。
作为一名新的开发人员,对 SQL 有一个基本的了解对于使用 Android 提供的任何方法都是至关重要的——无论是查询构建器、原始 SQL 还是 Room——所以从 SQL 开始是正确的选择。
为 Android 打包和管理 SQLite 数据库
在将 SQLite 添加到应用中时,除了编码之外,还有一些考虑事项可以帮助您在设计、构建和支持以数据库为中心的应用时做出正确的选择。在 I/O、文件放置和播种数据库等关键问题上投入一点时间和思考会有回报。
管理 Android 存储以提高数据库性能
第十九章讲述了在 Android 应用中管理和使用文件的细节。没有必要在本章中重复这些内容,但是我们没有涉及的一个话题是用于存储这些文件的硬件技术。几乎所有 Android 设备都配备了基于闪存的板载存储,通常由 NAND 硬件构建。不同品牌和制造商的内存质量和可靠性有很大差异。
当使用 SQLite 数据库构建 Android 应用时,当应用代表用户插入、更新和删除数据时,您将隐式触发对该硬件存储的读取和写入。闪存的一个怪癖是它缺乏对快速写入的可预测性。十有八九,您可能会获得出色、快速的写入性能,但却随机发现,由于闪存存储触发了一些内部清理或管理,您发出的下一次写入速度会下降。最常见的原因是“损耗水平管理”,即闪存存储管理层试图保持其部分存储的寿命。
很难发现这可能会导致的性能问题,尤其是在 AVD 中测试应用时,几乎可以肯定的是,AVD 的内存是由笔记本电脑或台式机非常快速、非常可靠的 RAM 模拟的。您可以通过使用第十八章中描述的AsnycTask()
方法来减轻一系列写性能不确定性,使数据库更改发生在异步线程上,远离主 UI 线程。
数据库管理的另一个怪癖是考虑当设备出现故障时会发生什么——无论是因为电量低、意外崩溃,还是用户在数据库活动的关键时刻简单地选择了关机按钮。SQLite 从崩溃和故障中恢复的能力为您提供了很好的支持,它使用了 ACID 数据库原理来保存(用 ACID 的说法是持久地)对数据的更改。意识到这一点很重要,因为任何名副其实的数据库的 ACID 保证的一部分包括确定事务可能需要回滚以保持完整性。
Acid Database Principles
在包括 SQLite 在内的所有关系数据库中,有四个关键原则是确保数据受到保护、保持完整并在需要时可用的标准方法。这些被称为酸性原则,这些字母代表原子性、一致性、孤立性和持久性。
原子性原则是一个事务中针对数据库的所有工作要么成功,要么回滚。不允许工作半途而废。
一致性原则是数据库中的数据始终处于一致状态,事务只能将数据从一种一致状态调整到另一种一致状态。
隔离的原则是,事务的工作对数据库的其他用户是不可见的,直到由事务引起的整个原子的、一致的数据更改集变得可见。
持久性原则是,一旦对数据进行更改,即使发生异常系统事件、灾难等,这些更改也会持续。
您应该考虑在您的应用中添加健全性检查逻辑,以便在您可以预先得到问题警告的情况下进行跟踪并采取行动。低功耗状态是最常见的情况,通过在应用中注册一个接收器来监视类似ACTION_BATTERY_CHANGED
的广播。通过检查意图有效负载,您可以确定电源是否不足,并可能推迟写入繁重的任务。
将 SQLite 数据库与您的应用打包
我的SQLiteExample
应用附带了一个助手类,可以方便地将三行数据填充到设备的表中,作为数据库的种子。对于这个例子来说很好,但是它可能会让您思考,如果我需要几百行,甚至几千行数据来使我的数据库从一开始就有用呢?在第一次创建数据库时执行这种大容量插入活动,可能会导致用户第一次执行程序时速度非常慢,因为所有的 I/O 都是在执行的。这不仅会让用户失望,而且这种长时间运行的工作会增加应用在运行时出现各种错误的可能性。
如果您想提供一个包含大量数据的 SQLite 数据库作为您的应用的一部分,您可以将它与. apk 中的其他资源打包在一起。SQLite 文件的适当位置是在您的assets/
文件夹下的特定子文件夹中,这样它的位置就可以传递给重载的openDatabase()
方法,该方法接受完整的文件位置作为它的第一个参数。
SQLite 数据库文件必须放在 assets/ folder 下的文件系统文件夹/data/data/your.package.name/databases/
中。添加代表您的文件名的String
(例如,我们的示例应用中的devices.db
),您就有了作为参数传递给openDatabase()
的值。实际上,对于SQLiteExample
应用,这个完整路径和文件名是
/data/data/org.beginningandroid.sqliteexample/devices.db
选择 SQLite 管理工具为打包准备数据库
手工制作任何类型的数据库都是一件乏味的事情,所以如果您想用 Android 应用打包一个数据库文件,您几乎肯定需要一些工具来帮助设计您的数据库并填充它。
使用内置工具
SQLite 打包了有用的管理工具,例如几乎所有操作系统 bar Windows 都附带的sqlite3
shell 程序(即使在 Windows 下,从sqlite.org
下载也很简单)。AVD 工具还提供对sqlite3
实用程序的访问,一旦连接到仿真设备,您就可以从adb
shell 实用程序调用它。举个例子,
$ sqlite3 /data/data/org.beginningandroid.sqliteexample/devices.db
查看位于sqlite.org
的文档,了解更多关于使用sqlite3
实用程序的详细信息。adb
工具还提供了一些其他有用的通用文件管理命令,您可以利用这些命令来管理 SQLite 数据库文件,例如adb push
将文件移动到设备,而adb pull
从设备复制文件。
使用第三方数据库工具
当简单的命令行工具不能满足您的高要求时,有许多其他更复杂的 GUI 工具可以帮助您管理 SQLite 数据库。一些最受欢迎的包括
-
DBeaver:一个越来越受欢迎的工具,因为它支持许多数据库,而不仅仅是 SQLite。跨平台,并提供免费的社区版。点击
https://dbeaver.io
了解更多信息。 -
DB Browser for SQLite:另一个跨平台工具,专门用于 SQLite 管理。点击
https://sqlitebrowser.org
了解更多信息。
从历史上看,另一个名为 SQLite Manager 的工具非常流行,非常有用,因为它被打包成了 Firefox 浏览器插件。不幸的是,随着 Firefox 插件工作方式的重大架构变化,SQLite Manager 不再受支持。我在这里提到它,这样你就不会掉进兔子洞试图找到它——它仍然在与 SQLite 相关的历史在线帖子和网站中被广泛提及。
摘要
现在,您已经对将 SQLite 数据库集成到您的应用中,以及构建由其中包含的数据驱动并支持其中包含的数据的特性和逻辑的关键步骤有了扎实的工作知识。
我们也已经到了这本书的结尾——或者至少是印刷和包装版本的结尾。本书中提到的许多主题都有额外的主题和示例,可从网站 www.beginningandroid.org
获取。
祝您在成为 Android 应用开发人员的道路上取得成功!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 【.NET】调用本地 Deepseek 模型