安卓应用开发秘籍-全-
安卓应用开发秘籍(全)
原文:
zh.annas-archive.org/md5/856ead42db2154620ae9d16f9313d1bf译者:飞龙
前言
Android 首次于 2007 年发布,在谷歌公司收购后。最初,Android 主要用于手机。Android 3.0 增加了功能,以利用不断增长的平板电脑市场。
2014 年,谷歌宣布 Android 拥有超过 10 亿活跃用户!在 Google Play 上有超过 100 万的应用程序,现在是加入 Android 社区最激动人心的时刻!
随着我们进入 2016 年,我们迎来了最近发布的 Android 6.0,它为用户和开发者带来了令人兴奋的新功能。
本书涵盖的内容
第一章,活动,讨论了活动,它是大多数应用程序的基本构建块。查看最常见任务的示例,例如创建活动以及从一个活动传递控制到另一个活动。
第二章,布局,讨论了布局选项;虽然 Activity 对于 UI 是基本的,但布局实际上定义了用户在屏幕上看到的内容。了解可用的主要布局选项和最佳实践。
第三章,视图、小部件和样式,探讨了基本 UI 对象,所有布局都是基于此构建的。小部件包括从按钮和文本框到更复杂的 NumberPicker 和日历对话框等。
第四章,菜单,教您如何在 Android 中使用菜单。了解如何创建菜单以及如何在运行时控制其行为。
第五章,探索碎片、AppWidgets 和系统 UI,展示了如何通过重用 UI 组件(碎片)来创建更灵活的用户界面。利用新的操作系统功能,如半透明系统栏,甚至可以使用沉浸模式完全隐藏系统 UI。
第六章,与数据交互,帮助您发现 Android 提供的多种持久化数据的方法,并了解何时使用每个选项最为合适。Loader 类示例展示了在不占用 UI 线程的情况下高效展示数据的方法。
第七章,警报和通知,展示了向用户显示通知的多种选项。选项包括在您的应用程序中显示警报、使用系统通知以及抬头显示通知。
第八章,使用触摸屏和传感器,帮助您学习处理标准用户交互的事件,例如按钮点击、长按和手势。访问设备硬件传感器以确定方向变化、设备移动和指南针方位。
第九章, 图形和动画,帮助您通过动画使您的应用生动起来!利用 Android 提供的多种创建动画的选项,从简单的位图到自定义属性动画。
第十章, 初探 OpenGL ES,讨论了 OpenGL;当您需要高性能的 2D 和 3D 图形时,转向 Open Graphics 库。Android 支持 OpenGL,这是一个跨平台的图形 API。
第十一章, 多媒体,利用硬件功能播放音频。使用 Android 意图调用默认相机应用或深入相机 API 直接控制相机。
第十二章, 电话、网络和互联网,使用电话功能发起电话呼叫并监听传入的电话事件。了解如何发送和接收短信(文本)消息。在您的应用中使用 WebView 显示网页,并学习如何使用 Volley 直接与网络服务通信。
第十三章, 获取位置和使用地理围栏,向您展示如何确定用户的位置以及最佳实践,以便您的应用不会耗尽电池。使用新的位置 API 接收位置更新并创建地理围栏。
第十四章, 为 Play 商店准备您的应用,帮助您为 Play 商店润色您的应用,并学习如何实现更高级的功能,例如闹钟和后台处理中的 AsyncTask。了解如何将 Google Cloud Messaging(推送通知)添加到您的应用中,并利用 Google Sign-in。
第十五章, 作为服务的后端选项,探讨了作为服务的后端提供商可以为您的应用提供什么。比较几个提供原生 Android 支持和免费订阅选项的顶级提供商。
您需要为本书准备的内容
开发 Android 应用需要 Android SDK,它可在多个平台上使用,包括 Windows、Mac 和 Linux。
虽然不是必需的,但本书使用 Android Studio,这是官方的 Android IDE。如果您是 Android 开发的新手,请访问以下链接,查看当前系统要求,并下载适用于您平台的带有 SDK 捆绑包的 Android Studio:
Android SDK 和 Android Studio 都是免费的。
本书面向的对象
本书假设您对编程概念和 Android 基础知识有基本的了解。否则,如果您是 Android 的新手,并且通过编写代码来学习效果最好,本书提供了最常见任务的大量内容。
作为一本“烹饪书”,您可以轻松跳转到您感兴趣的主题,并尽快在自己的应用程序中使代码生效。
部分
在本书中,您将找到几个经常出现的标题(准备、如何操作、它是如何工作的、还有更多、参见)。
为了清楚地说明如何完成食谱,我们使用以下这些部分:
准备工作
本节将向您介绍在食谱中可以期待的内容,并描述如何设置任何软件或任何为食谱所需的初步设置。
如何操作…
本节包含遵循食谱所需的步骤。
它是如何工作的...
本节通常包含对上一节发生情况的详细解释。
还有更多...
本节包含有关食谱的附加信息,以便使读者对食谱有更深入的了解。
参见
本节提供了对其他有用信息的链接,这些信息对食谱很有帮助。
惯例
在本书中,您将找到许多经常出现的标题(准备、如何操作、它是如何工作的、还有更多、参见),以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入如下所示:"使用JsonObjectRequest()请求 JSON 响应基本上与StringRequest()相同。"
代码块设置如下:
<activity
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"使用默认的电话和平板电脑选项,并在提示活动类型时选择空活动。"
注意
警告或重要注意事项以如下框的形式出现。
小贴士
小贴士和技巧以如下形式出现。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要发送给我们一般性的反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书籍的标题。
如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您的账户中下载本书的示例代码文件www.packtpub.com。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的 支持 选项卡上。
-
点击 代码下载 & 错误清单。
-
在 搜索 框中输入书籍的名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书的来源。
-
点击 代码下载。
一旦文件下载完成,请确保您使用最新版本的软件解压缩或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击 错误清单提交表单 链接,并输入您的错误清单详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的 错误清单 部分下的现有错误清单中。
要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍的名称。所需信息将出现在 错误清单 部分下。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。
询问
如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章。活动
本章涵盖了以下食谱:
-
声明活动
-
使用意图对象启动新活动
-
在活动之间切换
-
将数据传递给另一个活动
-
从活动返回结果
-
保存活动的状态
-
存储持久活动数据
-
理解活动生命周期
简介
Android SDK 提供了一个强大的工具来编程移动设备,掌握这样一个工具的最佳方式是直接上手。虽然您可以从头到尾阅读这本书,因为它是烹饪书,但它特别设计为允许您跳转到特定任务并立即获得结果。
活动是大多数 Android 应用程序的基本构建块,因为活动类提供了应用程序和屏幕之间的接口。大多数 Android 应用程序至少将有一个活动,如果不是几个(但不是必需的)。没有用户界面的后台服务应用程序不一定需要活动。
本章解释了如何在应用程序中声明和启动活动,以及如何通过在它们之间共享数据、从它们请求结果和在其中一个活动中调用另一个活动来同时管理多个活动。
本章还简要探讨了意图对象,该对象通常与活动一起使用。意图可以用于在您的应用程序中的活动之间以及在外部应用程序之间(例如,Android 操作系统中的应用程序)传输数据(一个常见的例子是使用意图启动默认的网络浏览器)。
注意事项
要开始开发 Android 应用程序,请转到Android Studio页面下载新的 Android Studio IDE 和Android SDK套件:
developer.android.com/sdk/index.html
声明活动
活动和其他应用程序组件,如服务,在AndroidManifest XML 文件中声明。声明活动是告诉系统我们的活动以及如何请求它的方式。例如,应用程序通常会指示至少有一个活动应作为桌面图标可见,并作为应用程序的主要入口点。
准备工作
Android Studio 是用于开发 Android 应用程序的新工具,取代了现在已弃用的Eclipse ADT解决方案。本书中展示的所有食谱都将使用 Android Studio,因此如果您尚未安装它,请访问 Android Studio 网站(链接已在之前提供)以安装 IDE 和 SDK 套件。
如何做到...
对于这个第一个示例,我们将引导您创建一个新项目。Android Studio 提供了一个快速入门向导,这使得整个过程变得极其简单。按照以下步骤开始:
-
启动 Android Studio,将弹出欢迎使用 Android Studio对话框。
-
点击启动新的 Android Studio 项目选项。
-
输入一个应用程序名称;在这个例子中,我们使用了
DeclareAnActivity。点击下一步。 -
在添加活动到移动设备对话框中,点击空白活动按钮,然后点击下一步。
-
在目标 Android 设备对话框中,选择Android 6.0(API 23)作为最低 SDK(对于这个例子,您选择的 API 级别实际上并不重要,因为活动自 API 级别 1 以来就存在了,但选择最新版本被认为是最佳实践)。点击下一步。
-
由于我们之前选择了空白活动选项,因此会显示自定义活动对话框。您可以将默认值保持不变,但请注意默认活动名称是
MainActivity。点击完成。
在完成向导后,Android Studio 将创建项目文件。对于这个配方,我们将检查的两个文件是MainActivity.java(对应于第 6 步中提到的活动名称)和AndroidManifest.xml。
如果您查看MainActivity.java文件,您会意识到它相当基础。这是因为我们选择了空白活动选项(在第 4 步中)。现在看看AndroidManifest.xml文件。这是我们实际声明活动的地方。在<application>元素内是<activity>元素:
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
注意
当在 Android Studio 中查看此xml时,您可能会注意到标签元素显示的是在strings.xml资源文件中定义的实际文本。这只是新 IDE 中增强功能的一个小例子。
它是如何工作的...
声明一个活动只是声明<activity>元素并使用android:name属性指定活动类的名称。通过将<activity>元素添加到AndroidManifest中,我们指定了我们的意图是将此组件包含在我们的应用程序中。任何未在清单中声明的活动(或任何其他组件)将不会包含在应用程序中。尝试访问或使用未声明的组件将在运行时抛出异常。
在前面的代码中,还有一个属性——android:label。此属性表示屏幕上显示的标题以及如果是启动器活动,则图标标签。
注意
要查看可用应用程序属性的完整列表,请参阅此资源:
developer.android.com/guide/topics/manifest/activity-element.html
使用意图对象启动新的活动
Android 应用程序模型可以看作是一个面向服务的模型,其中活动是组件,意图是它们之间发送的消息。在这里,使用意图启动一个显示用户通话记录的活动,但意图可以用于做很多事情,我们将在整本书中遇到它们。
准备工作
为了保持简单,我们将使用意图对象来启动 Android 的一个内置应用程序,而不是创建一个新的应用程序。这只需要一个非常基础的应用程序,因此使用 Android Studio 创建一个新的 Android 项目,并将其命名为ActivityStarter。
如何操作...
再次,为了使示例简单,以便我们可以专注于手头的任务,我们将创建一个显示意图操作的函数,并从我们的活动中的按钮调用此函数。
一旦在 Android Studio 中创建了新的项目,请按照以下步骤操作:
-
打开
MainActivity.java类,并添加以下函数:public void launchIntent(View view) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("https://www.packtpub.com/")); startActivity(intent); }当您输入此代码时,Android Studio 将在视图和意图上给出以下警告:无法解析符号 'Intent'。
这意味着您需要将库引用添加到项目中。您可以通过在
import部分输入以下代码手动完成此操作:import android.view.View; import android.content.Intent;或者,只需单击(红色字体中的)文字,按Alt + Enter,让 Android Studio 为您添加库引用。
-
打开
activity_main.xml文件,并添加以下 XML:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Launch Browser" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="launchIntent"/>![如何操作...]()
-
现在是时候运行应用程序并查看意图的操作了。您需要创建一个 Android 模拟器(在 Android Studio 中,转到工具 | Android | AVD 管理器)或将物理设备连接到您的计算机。
-
当您按下启动浏览器按钮时,您将看到默认的网页浏览器打开,并显示指定的 URL。
它是如何工作的...
虽然简单,但这个应用程序展示了 Android 操作系统背后的强大功能。意图对象只是一个消息对象。意图可以用来在您的应用程序组件之间(如服务和广播接收器)以及与设备上的其他应用程序(如我们在本食谱中所做的那样)进行通信。
注意
要在物理设备上进行测试,您可能需要安装您设备的驱动程序(驱动程序针对硬件制造商特定)。您还需要在您的设备上启用开发者模式。启用开发者模式的方式根据 Android 操作系统版本而有所不同。如果您在设备设置中看不到开发者模式选项,请打开关于手机选项,并开始连续点击构建号。点击三次后,您应该会看到一个Toast消息告诉您您正在成为开发者的路上。再点击四次将启用该选项。
在这个食谱中,我们通过指定ACTION_VIEW作为我们想要执行的操作(我们的意图)来创建了一个意图对象。您可能已经注意到,当您输入Intent然后按点时,Android Studio 提供了一个可能的弹出列表(这是自动完成功能),如下所示:

ACTION_VIEW,连同数据中的 URL,表示意图是查看网站,因此默认浏览器被启动(不同的数据可以启动不同的应用)。在这个例子中,我们的意图只是查看 URL,所以我们只使用startActivity()方法调用意图。根据我们的需求,还有其他调用意图的方法。在从活动中返回结果的配方中,我们将使用startActivityForResult()方法。
还有更多...
对于 Android 用户来说,下载他们喜欢的应用进行网页浏览、拍照、短信等是非常常见的。使用意图,你可以让你的应用利用用户喜欢的应用,而不是试图重新发明所有这些功能。
相关内容
要从菜单选择中启动活动,请参考第四章中的处理菜单选择配方。
在活动之间切换
经常情况下,我们可能需要在另一个活动内部激活一个活动。尽管这不是一个困难的任务,但与之前的方法相比,它需要更多的设置,因为它需要两个活动。我们将创建两个活动类,并在清单中声明它们。我们还将创建一个按钮,就像在之前的方法中做的那样,用于切换到活动。
准备工作
我们将在 Android Studio 中创建一个新的项目,就像之前的方法中做的那样,并将其命名为ActivitySwitcher。Android Studio 将创建第一个活动ActivityMain,并在清单中自动声明它。
如何实现...
-
由于 Android Studio 新建项目向导已经创建了第一个活动,我们只需要创建第二个活动。打开ActivitySwitcher项目,导航到文件 | 新建 | 活动 | 空白活动,如图所示:
![如何实现...]()
-
在自定义活动对话框中,你可以保留默认的活动名称不变,它是
Main2Activity,或者将其更改为SecondActivity,如图所示:![如何实现...]()
-
打开
MainActivity.java文件,并添加以下函数:public void onClickSwitchActivity(View view) { Intent intent = new Intent(this, SecondActivity.class); startActivity(intent); } -
现在,打开位于
\res\layout文件夹中的activity_main.xml文件,并添加以下 XML 以创建按钮:<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:text="Launch SecondActivity" android:onClick="onClickSwitchActivity"/> -
实际上,你现在可以运行代码,看到第二个活动出现。我们将进一步添加一个按钮到
SecondActivity中,用于关闭它,这将带我们回到第一个活动。打开SecondActivity.java文件,并添加以下函数:public void onClickClose(View view) { finish(); } -
最后,将关闭按钮添加到
SecondActivity布局中。打开activity_second.xml文件,并在自动生成的<TextView>元素之后添加以下<Button>元素:<Button android:id="@+id/buttonClose" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Close" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="onClickClose"/> -
在你的设备或模拟器上运行应用程序,并查看按钮的实际效果。
工作原理...
这个练习的真正工作在于第 3 步中的onClickSwitchActivity()方法。这是我们在意图中使用SecondActivity.class声明第二个活动的地方。我们更进一步,在第二个活动中添加了关闭按钮,以展示一个常见的现实世界情况——启动一个新的活动,然后关闭它,并返回到原始调用活动。这种行为是在onClickClose()函数中实现的。它所做的只是调用finish(),但这告诉系统我们已经完成了活动。finish()实际上不会返回我们到调用活动或任何特定的活动;它只是关闭当前活动并依赖于返回栈。如果我们想要特定的活动,我们还可以再次使用意图对象(我们只需在创建意图时更改类名)。
这种活动切换并不构成一个令人兴奋的应用程序。我们的活动除了演示如何从一个活动切换到另一个活动之外,没有做任何事情,这当然将是几乎所有我们开发的应用程序的基本方面之一。
如果我们手动创建了活动,我们需要将它们添加到清单中。通过使用这些步骤,Android Studio 已经处理好了 XML。要查看 Android Studio 做了什么,请打开AndroidManifest.xml文件并查看<application>元素:
<activity
android:name=".MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER/>
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:label="@string/title_activity_second">
</activity>
在前面的自动生成代码中需要注意的一点是,第二个活动没有<intent-filter>元素。主活动通常是启动应用程序时的入口点。这就是为什么定义了MAIN和LAUNCHER——这样系统就会知道在应用程序启动时启动哪个活动。
相关内容
- 要了解更多关于嵌入小部件(如按钮)的信息,请访问第三章,视图、小部件和样式。
将数据传递给另一个活动
意图对象被定义为消息对象。作为一个消息对象,它的目的是与应用程序的其它组件进行通信。在这个示例中,我们将向您展示如何使用意图传递信息以及如何再次获取它。
准备工作
这个示例将从上一个示例结束的地方继续。我们将把这个项目命名为SendData。
如何实现...
由于这个示例是基于上一个示例构建的,大部分工作已经完成。我们将在主活动中添加一个EditText元素,以便我们可以向SecondActivity发送一些内容。我们将使用(自动生成的)TextView视图来显示消息。以下是完整的步骤:
-
打开
activity_main.xml,删除现有的<TextView>元素,并添加以下<EditText>元素:<EditText android:id="@+id/editTextData" android:layout_width="match_parent" android:layout_height="wrap_content"/>我们在之前的示例中创建的
<Button>元素没有变化。 -
现在,打开
MainActivity.java文件,并按照以下方式修改onClickSwitchActivity()方法:public void onClickSwitchActivity(View view) { EditText editText = (EditText)findViewById(R.id.editTextData); String text = editText.getText().toString(); Intent intent = new Intent(this, SecondActivity.class); intent.putExtra(Intent.EXTRA_TEXT,text); startActivity(intent); } -
接下来,打开
activity_second.xml文件,并修改<TextView>元素以包含 ID 属性:<TextView android:id="@+id/textViewText" android:text="@string/hello_world" android:layout_width="wrap_content" android:layout_height="wrap_content"/> -
最后一个更改是编辑第二个活动以查找新数据并在屏幕上显示它。打开
SecondActivity.java并按以下方式编辑onCreate():protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); TextView textView = (TextView)findViewById(R.id.textViewText); if (getIntent()!=null && getIntent().hasExtra(Intent.EXTRA_TEXT)) { textView.setText(getIntent().getStringExtra(Intent.EXTRA_TEXT)); } } -
现在运行项目。在主活动上输入一些文本,然后按下启动第二个活动以查看它发送的数据。
它是如何工作的...
如预期的那样,intent 对象正在做所有的工作。我们就像在先前的食谱中一样创建了一个 intent,并添加了一些额外数据。你注意到putExtra()方法的调用吗?在我们的例子中,我们使用了已经定义的Intent.EXTRA_TEXT作为标识符,但并不一定非得这样做。我们可以使用任何我们想要的键(如果你熟悉名称/值对,你之前已经见过这个概念)。
使用名称/值对的关键点是您必须使用相同的名称来获取数据。这就是为什么我们在使用getStringExtra()读取额外数据时使用了相同的键标识符。
第二个活动是通过我们创建的 intent 启动的,所以这只是一个获取 intent 并检查随它发送的数据的问题。我们在onCreate()中这样做:
textView.setText(getIntent().getStringExtra(Intent.EXTRA_TEXT));
还有更多...
我们不仅限于发送String数据。intent 对象非常灵活,并且已经支持基本数据类型。回到 Android Studio,点击putExtra方法。然后按Ctrl和空格键。Android Studio 将显示自动完成列表,以便您可以看到可以存储的不同数据类型。
从一个活动返回结果
能够从一个活动启动另一个活动固然很好,但我们将经常需要知道被调用的活动在任务中的表现,甚至哪个活动被调用了。startActivityForResult()方法提供了解决方案。
准备工作
从一个活动返回结果与我们在前面的食谱中调用活动的方式非常相似。您可以使用上一个食谱中的项目,或者启动一个新的项目并将其命名为GettingResults。无论哪种方式,一旦您有一个包含两个活动和调用第二个活动所需代码的项目,您就可以开始了。
如何做到这一点...
只需要做几个更改就可以获取结果:
-
首先,打开
MainActivity.java并将以下常量添加到类中:public static final String REQUEST_RESULT="REQUEST_RESULT"; -
接下来,通过修改
onClickSwitchActivity()方法来更改调用 intent 的方式,使其期望一个结果:public void onClickSwitchActivity(View view) { EditText editText = (EditText)findViewById(R.id.editTextData); String text = editText.getText().toString(); Intent intent = new Intent(this, SecondActivity.class); intent.putExtra(Intent.EXTRA_TEXT,text); startActivityForResult(intent,1); } -
然后,添加这个新方法来接收结果:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode==RESULT_OK) { Toast.makeText(this, Integer.toString(data.getIntExtra(REQUEST_RESULT, 0)), Toast.LENGTH_LONG).show(); } } -
最后,修改
SecondActivity.java中的onClickClose以设置返回值如下:public void onClickClose(View view) { Intent returnIntent = new Intent(); returnIntent.putExtra(MainActivity.REQUEST_RESULT,42); setResult(RESULT_OK, returnIntent); finish(); }
它是如何工作的...
如您所见,获取结果相对简单。我们只是使用startActivityForResult调用 intent,这样它就知道我们想要一个结果。我们设置了onActivityResult()回调处理程序来接收结果。最后,在关闭活动之前,我们确保第二个活动通过setResult()返回一个结果。在这个例子中,我们只是设置一个静态值的结果。我们只是显示我们接收到的内容来演示这个概念。
检查结果代码以确保用户没有取消操作是一个好习惯。技术上它是一个整数,但系统将其用作布尔值。检查RESULT_OK或RESULT_CANCEL并根据情况相应处理。在我们的例子中,第二个活动没有取消按钮,为什么还要检查?如果用户点击返回按钮怎么办?系统会将结果代码设置为RESULT_CANCEL并将 intent 设置为 null,这将导致我们的代码抛出异常。
我们使用了Toast对象,这是一个方便的弹出消息,可以用来不引人注目地通知用户。它还作为一个方便的调试方法,因为它不需要特殊的布局或屏幕空间。
更多...
除了结果代码外,onActivityResult()还包括一个请求代码。你在想它从哪里来吗?它只是与startActivityForResult()调用一起传递的整数值,其形式如下:
startActivityForResult(Intent intent, int requestCode);
我们没有检查请求代码,因为我们知道我们只有一个结果需要处理——但在有多个活动的简单应用中,这个值可以用来识别请求的来源。
小贴士
如果startActivityForResult()使用负请求代码调用,它将表现得就像调用startActivity()一样——也就是说,它不会返回结果。
参见
-
要了解更多关于创建新活动类,请参阅在活动之间切换配方
-
更多关于 Toast 的信息,请参阅第七章中的制作 Toast配方,警报和通知
保存活动的状态
移动环境非常动态,用户比在桌面电脑上更频繁地更改任务。由于移动设备上通常资源较少,因此预期您的应用在某个时刻会被中断。系统也可能完全关闭您的应用,以向当前任务提供额外的资源。这是移动设备的本质。
用户可能在您的应用中开始输入某些内容,然后被电话打断,或者切换到另一个应用发送短信,当他们回到您的应用时,系统可能已经完全关闭它以释放内存。为了提供最佳的用户体验,您需要预料到这种行为,并让用户更容易从他们离开的地方继续。好事是,Android 操作系统通过提供回调来通知您的应用状态变化,使得这个过程变得更容易。
注意
简单地旋转设备将导致操作系统销毁并重新创建您的活动。这看起来可能有点过于强硬,但这是出于良好的原因——在纵向和横向布局中,非常常见,这确保了您的应用正在使用正确的资源。
在这个菜谱中,您将看到如何处理onSaveInstanceState()和onRestoreInstanceState()回调来保存应用程序的状态。我们将通过创建一个计数器变量并在每次按下计数按钮时增加它来演示这一点。我们还将有一个EditText和TextView小部件来观察它们的默认行为。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为StateSaver。我们只需要一个活动,因此自动生成的 MainActivity 就足够了。然而,我们需要一些小部件,包括EditText、Button和TextView。它们的布局(在activity_main.xml中)将如下所示:
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Count"
android:onClick="onClickCounter"/>
<TextView
android:id="@+id/textViewCounter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button"/>
如何做到这一点...
执行以下步骤:
-
为了跟踪计数器,我们需要在项目中添加一个全局变量,以及一个用于保存和恢复的键。将以下代码添加到
MainActivity.java类中:static final String KEY_COUNTER = "COUNTER"; private int mCounter=0; -
然后添加处理按钮点击的代码;它增加计数器并在
TextView小部件中显示结果:public void onClickCounter(View view) { mCounter++; ((TextView)findViewById(R.id.textViewCounter)).setText("Counter: " + Integer.toString(mCounter)); } -
要接收应用程序状态变化的通知,我们需要将
onSaveInstanceState()和onRestoreInstanceState()方法添加到我们的应用程序中。打开MainActivity.java并添加以下代码:@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(KEY_COUNTER,mCounter); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); mCounter=savedInstanceState.getInt(KEY_COUNTER); } -
运行程序并尝试更改方向以查看其行为(如果您使用的是模拟器,Ctrl + F11 将旋转设备)。
它是如何工作的...
所有活动在其生命周期中都会经历多个状态。通过设置回调来处理事件,我们可以在活动被销毁之前保存重要信息。
第 3 步是实际保存和恢复发生的地方。系统向方法发送一个Bundle(一个也使用名称/值对的 数据对象)。我们使用onSaveInstanceState()回调来保存数据,并在onRestoreInstanceState()回调中提取它。
但是等等!您在旋转设备之前尝试在EditText视图中输入文本了吗?如果是这样,您会注意到文本也被恢复了,但我们没有处理该视图的代码。默认情况下,如果系统具有唯一的 ID(并非所有视图都会自动保存其状态,例如TextView,但如果我们想,我们可以手动保存它),系统将自动保存状态。
小贴士
注意,如果您想让 Android 自动保存和恢复视图的状态,该视图必须具有唯一的 ID(在布局中通过android:id=属性指定)。小心;并非所有视图类型都会自动保存和恢复视图的状态。
还有更多...
onRestoreInstanceState()回调不是唯一可以恢复状态的地方。看看onCreate()的签名:
onCreate(Bundle savedInstanceState)
这两种方法都接收同一个名为savedInstanceState的Bundle实例。您可以将恢复代码移动到onCreate()方法,它将按相同的方式工作。但有一个陷阱是,如果没有数据,例如在活动的初始创建期间,savedInstanceState包将是 null。如果您想从onRestoreInstanceState()回调中移动代码,只需确保数据不为 null,如下所示:
if (savedInstanceState!=null) {
mCounter = savedInstanceState.getInt(KEY_COUNTER);
}
参见
-
存储持久活动数据菜谱将介绍持久存储。
-
请参阅第六章,处理数据,以获取更多关于 Android 活动的示例。
-
理解活动生命周期菜谱解释了 Android 活动生命周期。
存储持久活动数据
能够暂时存储有关我们活动的信息非常有用,但更常见的情况是我们希望我们的应用程序能够在多个会话之间记住信息。
Android 支持 SQLite,但对于像用户名或高分这样的简单数据,这可能会带来很多开销。幸运的是,Android 还提供了这些场景的轻量级选项,即SharedPreferences。
准备工作
您可以使用之前菜谱中的项目,或者开始一个新的项目并将其命名为PersistentData(在现实世界的应用中,您可能无论如何都会这样做)。在之前的菜谱中,我们在会话状态中保存了mCounter。在这个菜谱中,我们将添加一个新的方法来处理onPause()并将mCounter保存到SharedPreferences中。我们将在onCreate()中恢复该值。
如何做到这一点...
我们只需要做两个更改,而且这两个更改都在MainActivity.java中:
-
在活动关闭前,添加以下
onPause()方法以保存数据:@Override protected void onPause() { super.onPause(); SharedPreferences settings = getPreferences(MODE_PRIVATE); SharedPreferences.Editor editor = settings.edit(); editor.putInt(KEY_COUNTER, mCounter); editor.commit(); } -
然后在
onCreate()的末尾添加以下代码以恢复计数器:SharedPreferences settings = getPreferences(MODE_PRIVATE); int defaultCounter = 0; mCounter = settings.getInt(KEY_COUNTER, defaultCounter); -
运行程序并尝试它。
它是如何工作的...
如您所见,这与保存状态数据非常相似,因为它也使用名称/值对。在这里,我们只存储了一个int,但我们同样可以轻松地存储其他原始数据类型之一。每种数据类型都有等效的获取器和设置器,例如SharedPreferences.getBoolean()或SharedPreferences.setString()。
保存我们的数据需要使用SharedPreferences.Editor服务。这可以通过edit()方法调用,并接受remove()和clear()等过程以及putInt()等设置器。请注意,我们必须使用commit()语句来结束我们在这里所做的任何存储。
还有更多...
getPreferences()访问器的稍微复杂一些的变体是getSharedPreferences()。它可以用来存储多个首选项集。
使用多个首选项文件
使用getSharedPreferences()与使用其对应物没有区别,但它允许使用多个首选项文件。它具有以下形式:
getSharedPreferences(String name, int mode)
在这里,name 是文件名。mode 可以是 MODE_PRIVATE、MODE_WORLD_READABLE 或 MODE_WORLD_WRITABLE,它描述了文件的可访问级别。
参见
- 第六章,处理数据,了解更多关于数据存储的示例
理解活动生命周期
Android 操作系统对活动来说是一个危险的地方。在电池供电平台上对资源的需求被系统管理得相当残酷。我们的活动可以在运行时被从内存中移除,甚至没有一点通知,以及它们包含的任何数据。因此,理解活动生命周期是至关重要的。
以下图表显示了活动在其生命周期中经过的阶段:

除了阶段之外,图表还显示了可以覆盖的方法。正如你所见,我们已经在先前的菜谱中使用了这些方法中的大多数。希望了解整体情况能帮助你理解。
准备工作
在 Android Studio 中创建一个新的项目,并使用一个 空白活动,命名为 ActivityLifecycle。我们将使用(自动生成的)TextView 方法来显示状态信息。
如何做到这一点...
为了看到应用程序通过各个阶段,我们将为所有阶段创建方法:
-
打开
activity_main.xml并为自动生成的TextView添加一个 ID:android:id="@+id/textViewState" -
剩余的步骤将在
MainActivity.java中进行。添加以下全局声明:private TextView mTextViewState; -
修改
onCreate()方法以保存TextView并设置初始文本:mTextViewState = (TextView)findViewById(R.id.textViewState); mTextViewState.setText("onCreate()\n"); -
添加以下方法来处理剩余的事件:
@Override protected void onStart() { super.onStart(); mTextViewState.append("onStart()\n"); } @Override protected void onResume() { super.onResume(); mTextViewState.append("onResume()\n"); } @Override protected void onPause() { super.onPause(); mTextViewState.append("onPause()\n"); } @Override protected void onStop() { super.onStop(); mTextViewState.append("onStop()\n"); } @Override protected void onRestart() { super.onRestart(); mTextViewState.append("onRestart()\n"); } @Override protected void onDestroy() { super.onDestroy(); mTextViewState.append("onDestroy()\n"); } -
运行应用程序并观察当通过按下返回键和主页键中断活动时会发生什么。尝试其他操作,例如任务切换,以查看它们如何影响您的应用程序。
它是如何工作的...
我们的活动可以处于以下三种状态之一:active、paused 或 stopped。还有一个第四种状态,destroyed,但我们可以安全地忽略它:
-
当活动的界面对用户可用时,活动处于
active状态。它从onResume()开始持续到onPause(),这是当另一个活动进入前台时发生的。如果这个新活动并没有完全遮挡我们的活动,那么我们的活动将保持在paused状态,直到新活动完成或被取消。然后它将立即调用onResume()并继续。 -
当一个新启动的活动填满屏幕或使我们的活动不可见时,我们的活动将进入
stopped状态,并且恢复将始终调用onRestart()。 -
当活动处于
paused或stopped状态时,操作系统可以在内存低或其他应用程序需要时将其从内存中移除。 -
值得注意的是,我们实际上从未看到
onDestroy()方法的执行结果,因为到这时活动已经被移除。如果您想进一步探索这些方法,那么使用Activity.isFinishing()来查看在onDestroy()执行之前活动是否真的正在结束是非常有价值的,如下面的代码片段所示:@Override public void onPause() { super.onPause(); mTextView.append("onPause()\n "); if (isFinishing()){ mTextView.append(" ... finishing"); } }
小贴士
在实现这些方法时,始终在执行任何工作之前调用超类。
还有更多...
关闭活动
要关闭一个活动,直接调用其 finish() 方法,该方法会进一步调用 onDestroy()。要从子活动中执行相同操作,请使用 finishFromChild(Activity child),其中 child 是调用子活动。
有时了解活动是被关闭还是仅仅暂停是有用的,isFinishing(boolean) 方法返回一个值,指示活动处于这两种状态中的哪一种。
第二章. 布局
在本章中,我们将涵盖以下主题:
-
定义和填充布局
-
使用 RelativeLayout
-
使用 LinearLayout
-
创建表格 – TableLayout 和 GridLayout
-
使用 ListView、GridView 和适配器
-
在运行时更改布局属性
-
使用 Hierarchy Viewer 优化布局
简介
在 Android 中,用户界面是在布局中定义的。布局可以在 XML 中声明或在代码中动态创建。(建议在 XML 中声明布局,而不是在代码中,以保持表示层与实现层的分离。)布局可以定义单个ListItem、片段,甚至整个 Activity。布局文件存储在/res/layout文件夹中,并在代码中使用以下标识符引用:R.layout.<filename_without_extension>。
Android 提供了一系列有用的Layout类,它们包含和组织活动(如按钮、复选框和其他Views)的各个元素。ViewGroup对象是一个容器对象,作为 Android 家族Layout类的基类。放置在布局中的视图形成一个层次结构,最顶层的布局是父布局。
Android 提供了一些内置的布局类型,专为特定目的设计,例如RelativeLayout,它允许视图相对于其他元素进行定位。LinearLayout可以根据指定的方向堆叠视图或使它们水平对齐。TableLayout可用于布局视图网格。在各种布局中,我们还可以使用Gravity来对齐视图,并使用Weight控制提供比例大小。布局和ViewGroups可以嵌套在彼此内部以创建复杂的配置。提供了十几种不同的布局对象来管理小部件、列表、表格、画廊和其他显示格式,并且您始终可以从基类派生来自定义布局。
定义和填充布局
当使用 Android Studio 向导创建新项目时,它会自动创建res/layout/activity_main.xml文件(如下截图所示)。然后,在onCreate()回调中使用setContentView(R.layout.activity_main)来填充 XML 文件。

对于这个菜谱,我们将创建两个略有不同的布局,并通过按钮在它们之间切换。
准备工作
在 Android Studio 中创建一个新的项目,命名为InflateLayout。项目创建完成后,展开res/layout文件夹,以便我们可以编辑activity_main.xml文件。
如何操作...
-
编辑
res/layout/activity_main.xml文件,使其包含如下定义的按钮:<Button android:id="@+id/buttonLeft" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Left Button" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:onClick="onClickLeft"/> -
现在复制
activity_main.xml并命名为activity_main2.xml。修改按钮以匹配以下内容:<Button android:id="@+id/buttonRight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Right Button" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:onClick="onClickRight"/> -
打开
MainActivity.java,添加以下两个方法来处理按钮点击:public void onClickLeft(View view) { setContentView(R.layout.activity_main2); } public void onClickRight(View view) { setContentView(R.layout.activity_main); } -
在设备或模拟器上运行此应用程序,以查看其效果。
它是如何工作的...
关键在于对setContentView()的调用,我们之前在自动生成的onCreate()代码中遇到过。只需将布局 ID 传递给setContentView(),它就会自动展开布局。
这段代码的目的是使概念易于理解,但仅用于更改按钮(在本例中,我们可以在按钮点击时更改对齐方式)的属性时可能会过度。展开布局通常在onCreate()方法中只需要一次,但有时您可能希望手动展开布局,就像我们在这里所做的那样。(如果您手动处理方向变化,这将是一个很好的例子。)
更多内容...
除了使用资源 ID 标识布局,如我们在这里所做的那样,setContentView()还可以接受一个视图作为参数,例如:
findViewById(R.id.myView)
setContentView(myView);
参见
- 如前所述,请参阅Fragment主题,在第五章,探索 Fragment、AppWidget 和系统 UI,了解更改屏幕布局的替代方法
使用 RelativeLayout
如简介中所述,RelativeLayout允许视图相对于彼此和父视图进行定位。RelativeLayout特别适用于减少嵌套布局的数量,这对于减少内存和处理需求非常重要。
准备工作
创建一个新的项目,并将其命名为RelativeLayout。默认布局使用RelativeLayout,我们将使用它来水平和垂直对齐视图。
如何实现...
-
打开
res/layout/activity_main.xml文件并按以下方式修改:<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Centered" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Below TextView1" android:layout_below="@+id/textView1" android:layout_toLeftOf="@id/textView1" /> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Bottom Right" android:layout_alignParentBottom="true" android:layout_alignParentEnd="true" /> -
运行代码,或在设计选项卡中查看布局
它是如何工作的...
这是一个非常简单的练习,但它演示了几个RelativeLayout选项:layout_centerVertical、layout_centerHorizontal、layout_below、layout_alignParentBottom等等。
最常用的RelativeLayout布局属性包括:
-
layout_below: 此视图应在指定的视图之下 -
layout_above: 此视图应在指定的视图之上 -
layout_alignParentTop: 将此视图与父视图的顶部边缘对齐 -
layout_alignParentBottom: 将此视图与父视图的底部边缘对齐 -
layout_alignParentLeft: 将此视图与父视图的左侧边缘对齐 -
layout_alignParentRight: 将此视图与父视图的右侧边缘对齐 -
layout_centerVertical: 在父视图中垂直居中此视图 -
layout_centerHorizontal: 在父视图中水平居中此视图 -
layout_center: 在父视图中水平和垂直居中此视图注意
要查看
RelativeLayout参数的完整列表,请访问:developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html。
更多内容...
与我们之前看到的不同,这里是一个使用 LinearLayout 仅用于居中 TextView 的例子(创建与 RelativeLayout 的 layout_center 参数相同的效果):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center" >
<TextView
android:id="@+id/imageButton_speak"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Centered" />
</LinearLayout>
</LinearLayout>
注意这个布局比等效的 RelativeLayout(它是一个嵌套在父 LinearLayout 中的 LinearLayout)深一级。虽然这是一个简单的例子,但避免不必要的嵌套是一个好主意,因为它可能会影响性能,尤其是在布局被反复填充时(例如 ListItem)。
参见
-
下一个菜谱,使用 LinearLayout,它将提供一个替代布局
-
查看关于高效布局设计的 使用 Hierarchy Viewer 优化布局 菜谱以获取更多信息
使用 LinearLayout
另一个常见的布局选项是 LinearLayout,它根据指定的方向排列子视图,可以是单列或单行。默认方向(如果没有指定)是垂直的,它将视图对齐在单列中。
LinearLayout 有一个 RelativeLayout 中没有的关键特性——weight 属性。我们可以在定义视图时指定 layout_weight 参数,以便视图可以根据可用空间动态调整大小。选项包括让视图填充所有剩余空间(如果视图具有更高的权重),让多个视图适应给定空间(如果所有视图都具有相同的权重),或者按权重成比例地分配视图空间。
我们将创建一个包含三个 EditText 视图的 LinearLayout 来演示如何使用权重属性。在这个例子中,我们将使用三个 EditText 视图——一个用于输入 To Address 参数,另一个用于输入 Subject,第三个用于输入 Message。To 和 Subject 视图将各占一行,剩余空间留给 Message 视图。
准备工作
创建一个新的项目,并将其命名为 LinearLayout。我们将用 LinearLayout 替换 activity_main.xml 中创建的默认 RelativeLayout。
如何操作...
-
打开
res/layout/activity_main.xml文件,并按以下方式替换:<LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:id="@+id/editTextTo" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="To" /> <EditText android:id="@+id/editTextSubject" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Subject" /> <EditText android:id="@+id/editTextMessage" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="top" android:hint="Message" /> </LinearLayout> -
运行代码,或在 设计 选项卡中查看布局。
它是如何工作的...
当使用 LinearLayout 的垂直方向时,子视图将在单列中创建(堆叠在彼此之上)。前两个视图使用 android:layout_height="wrap_content" 属性,每个视图占一行。editTextMessage 使用以下方式指定高度:
android:layout_height="0dp"
android:layout_weight="1"
当使用 LinearLayout 时,它告诉 Android 根据权重计算高度。权重为 0(如果没有指定则为默认值)表示视图不应扩展。在这个例子中,editTextMessage 是唯一一个定义了权重的视图,因此它将独自扩展以填充父布局中的任何剩余空间。
小贴士
当使用水平方向时,指定 android:layout_height="0dp"(连同权重)以让 Android 计算宽度。
将权重属性视为百分比可能会有所帮助。在这种情况下,定义的总权重是 1,因此此视图获得剩余空间的 100%。如果我们给另一个视图分配权重 1,总数将是 2,因此此视图将获得 50%的空间。尝试给其他视图之一添加权重(同时确保将高度更改为0dp)以查看其效果。
如果你给其他视图(或两个视图)之一(或两个)添加了权重,你是否注意到了文本的位置?如果没有指定gravity的值,文本就会保持在视图空间中心。editTextMessage指定了:android:gravity="top",这会强制文本位于视图的顶部。
更多内容...
可以使用位运算符OR组合多个属性选项。(Java 使用管道字符(|)表示OR)。例如,我们可以组合两个重力选项,使文本既沿父视图的顶部对齐,又在其可用空间内居中:
android:layout_gravity="top|center"
应该注意的是,layout_gravity和gravity标签不是同一回事。layout_gravity决定了视图在其父视图中的位置,而gravity控制视图内内容的定位——例如,按钮上文本的对齐方式。
参见
- 之前的菜谱,使用 RelativeLayout
创建表格 – TableLayout 和 GridLayout
当你需要在你的 UI 中创建表格时,Android 提供了两个方便的布局选项:TableLayout(以及TableRow)和GridLayout(在 API 14 中添加)。这两个布局选项都可以创建类似外观的表格,但每个都使用不同的方法。使用TableLayout时,随着你构建表格,行和列会动态添加。使用GridLayout时,行和列的大小在布局定义中定义。
两个布局都没有更好,这只是使用最适合你需求的布局。我们将使用每个布局创建一个 3 x 3 的网格以进行比较,因为你可能会很容易地在同一个应用程序中使用这两个布局。
准备工作
为了专注于布局并提供更简单的比较,我们将为这个菜谱创建两个独立的应用程序。创建两个新的 Android 项目,第一个命名为TableLayout,另一个命名为GridLayout。
如何操作...
-
从
TableLayout项目开始,打开activity_main.xml。将根布局更改为TableLayout。 -
向每个
TableRow添加三个TextViews的集合,以创建一个 3 x 3 的矩阵。为了演示目的,列被标记为 A-C,行标记为 1-3,因此TextViews的第一行将是 A1、B1 和 C1。最终结果将如下所示:<TableLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A1" android:id="@+id/textView1" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="B1" android:id="@+id/textView2" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="C1" android:id="@+id/textView3" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A2" android:id="@+id/textView4" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="B2" android:id="@+id/textView5" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="C2" android:id="@+id/textView6" /> </TableRow> <TableRow android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A3" android:id="@+id/textView7" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="B3" android:id="@+id/textView8" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="C3" android:id="@+id/textView9" /> </TableRow> </TableLayout> -
现在,打开
GridLayout项目以编辑activity_main.xml。将根布局更改为GridLayout。向GridLayout元素添加columnCount=3和rowCount=3属性。 -
现在,向
GridLayout添加九个TextViews。我们将使用与前面TableLayout相同的文本,以便进行一致的比较。由于GridView不使用TableRows,前三个TextViews在第 1 行,接下来的三个在第 2 行,依此类推。最终结果将如下所示:<GridLayout android:layout_width="match_parent" android:layout_height="match_parent" android:columnCount="3" android:rowCount="3"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A1" android:id="@+id/textView1" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="B1" android:id="@+id/textView2" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="C1" android:id="@+id/textView3" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A2" android:id="@+id/textView4" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="B2" android:id="@+id/textView5" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="C2" android:id="@+id/textView6" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="A3" android:id="@+id/textView7" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="B3" android:id="@+id/textView8" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="C3" android:id="@+id/textView9" /> </GridLayout> -
您可以选择运行应用程序或使用 设计 选项卡来查看结果。
它是如何工作的...
如您在查看创建的表格时所见,表格在屏幕上基本上看起来相同。主要区别是创建它们的代码。
在 TableLayout XML 中,每行都是通过 TableRow 添加到表格中的。每个视图成为一个列。这不是一个要求,因为可以跳过单元格或将其留空。(参见下一节中如何指定 TableRow 中的单元格位置。)
GridLayout 使用相反的方法。在创建表格时指定行数和列数。我们不需要指定行或列信息(尽管我们可以,如下文所述)。Android 会自动按顺序将每个视图添加到单元格中。
更多...
首先,让我们看看布局之间的更多相似之处。两种布局都有拉伸列以使用剩余屏幕空间的能力。对于 TableLayout,在 xml 声明中添加以下属性:
android:stretchColumns="1"
stretchColumns 指定要拉伸的列的(基于零的)索引。 (android:shrinkColumns 是可以收缩的列的基于零的索引,因此表格可以适应屏幕。)
要使用 GridLayout 实现相同的效果,请将以下属性添加到 B 列中所有视图(textView2、textView5 和 textView8):
android:layout_columnWeight="1"
注意
给定列中的所有单元格都必须定义权重,否则它将不会拉伸。
现在,让我们看看一些差异,因为这确实是确定针对特定任务使用哪种布局的关键。首先要注意的是列和行是如何实际定义的。在 TableLayout 中,行是具体定义的,使用 TableRow。 (Android 将根据具有最多单元格的行来确定表格中的列数。)在定义视图时使用 android:layoutColumn 属性来指定列。
相比之下,使用 GridLayout,行和列的数量是在定义表格时指定的(使用前面显示的 columnCount 和 rowCount。)
在前面的示例中,我们只是将 TextViews 添加到 GridLayout 中,并让系统自动定位它们。我们可以通过在定义视图时指定行和列位置来改变这种行为,例如:
android:layout_row="2"
android:layout_column="2"
提示
Android 在添加每个视图后自动增加单元格计数器,因此 下一个 视图也应该指定行和列,否则您可能不会得到预期的结果。
如同 LinearLayout 菜单中显示的 LinearLayout,GridLayout 也提供了支持水平(默认)和垂直(vertical)方向的属性。方向决定了单元格的放置方式。(水平方向首先填充列,然后移动到下一行。垂直方向首先填充每行的第一列,然后移动到下一列。)
使用 ListView、GridView 和适配器
ListView 和 GridView 都是 ViewGroup 的子类,但它们更像是视图,因为它们是数据驱动的。换句话说,我们不是在设计时定义所有可能填充 ListView(或 GridView)的视图,而是从传递给视图的数据动态创建内容。(ListItem 的布局可能在设计时创建,以控制运行时数据的显示。)
例如,如果你需要向用户展示国家列表,你可以创建一个 LinearLayout 并为每个国家添加一个按钮。这种方法有几个问题:确定可用的国家,保持按钮列表的更新,确保屏幕空间足够容纳所有国家,等等。否则,你可以创建一个国家列表来填充 ListView,然后为每个条目创建一个按钮。
我们将创建一个示例,使用第二种方法,从国家名称数组中填充 ListView。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 ListView。默认的 ActivityMain 类扩展了 Activity 类。我们将将其更改为扩展 ListActivity 类。然后我们将创建一个简单的字符串列表并将其绑定到 ListView,以在运行时派生按钮。
如何做到这一点...
-
打开 MainActivity.java 文件,并更改基本声明,使其扩展
ListActivity而不是Activity类:public class MainActivity extends ListActivity { -
将
onCreate()改变为以下内容:protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"}; ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, countries); setListAdapter(countryAdapter); getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, Viewview, int position, long id) { String s = ((TextView) view).getText() + " " +position; Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show(); } }); } -
现在在模拟器或设备上运行应用程序,以查看填充的
ListView。
它是如何工作的...
我们首先创建一个简单的国家名称数组,然后使用它来填充 ListAdapter。在这个例子中,我们在构建 ListAdapter 时使用了 ArrayAdapter,但 Android 还提供了其他几种适配器类型。例如,如果你的数据存储在数据库中,你可以使用 CursorAdapter。如果内置类型不能满足你的需求,你总是可以使用 CustomAdapter。
我们使用以下代码创建适配器:
ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, countries);
在这里,我们使用我们的字符串数组(最后一个参数)实例化 ArrayAdapter。注意 android.R.layout.simple_list_item_1 参数吗?这定义了按钮的布局。在这里,我们使用 Android 提供的布局之一,但我们可以创建自己的布局并传递我们的 ID。
一旦我们准备好了适配器,我们只需通过setListAdapter()调用将其传递给底层的ListView。(底层的ListView来自扩展ListViewActivity。)最后,我们实现setOnItemClickListener以在用户按下列表中的按钮(代表一个国家)时显示 Toast。
ListView在 Android 中非常常见,因为它们通过滚动视图有效地利用屏幕空间,这在小屏幕上非常有用。ScrollView布局提供了一个创建类似滚动效果的替代方法。这两种方法的主要区别在于,ScrollView布局在显示给用户之前完全填充,而ListView只填充将可见的视图。对于有限的数据,这可能不是问题,但对于更大的数据集,应用在列表显示之前可能会耗尽内存。
此外,由于ListView是由数据适配器驱动的,数据可以轻松更改。即使在我们的有限示例中,将一个新国家添加到屏幕上也只需将名称添加到国家列表中。更重要的是,列表可以在用户使用应用时更新(例如,从网站下载更新列表以显示实时选项)。
还有更多...
ListView还支持使用setChoiceMode()方法的多选模式。要看到它的实际效果,请在setListAdapter()之后添加以下代码行:
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
然后,将ListItem布局从android.R.layout.simple_list_item_1更改为android.R.layout.simple_list_item_checked。
虽然大多数需要滚动列表的应用都转向使用ListView,但 Android 还提供了GridView。它们在功能上非常相似,甚至使用相同的数据适配器。主要区别是视觉上的,允许多列。为了更好地理解,让我们将ListView示例更改为GridView。
首先,我们需要将MainActivity改为从Activity扩展,而不是ListActivity。(这将撤销前面的步骤 1。)然后,用以下代码替换onCreate():
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GridView gridView = new GridView(this);
setContentView(gridView);
String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"};
ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, countries);
gridView.setAdapter(countryAdapter);
gridView.setNumColumns(2);
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String s = ((TextView) view).getText() + " " + position;
Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show();
}
});
}
如您所见,为GridView设置的代码比ListView多。onCreate()方法创建一个新的GridView,并在setContentView()调用中传递它。(正如在定义和填充布局中提到的,我们使用了这种setContentView变体,而不是仅创建一个包含GridView的布局,但最终结果是一样的。)
ListViewActivity基类处理了其中大部分工作,但GridView没有相应的活动类可以扩展。
在运行时更改布局属性
在 Android 开发中,通常的做法是用 XML 定义 UI,用 Java 定义应用程序代码,保持用户界面代码与应用程序代码分离。有时,从 Java 代码中修改(甚至构建)UI 会更容易或更高效。幸运的是,Android 支持这种做法。
我们在上一道菜谱中看到了一个修改布局的例子,其中我们在代码中设置了要显示的 GridView 列数。在本道菜谱中,我们将获取 LayoutParams 对象的引用,以便在运行时更改边距。
准备工作
在这里,我们将使用 XML 设置一个简单的布局,并使用 LinearLayout.LayoutParams 对象在运行时更改视图的边距。
如何做到这一点……
-
打开
activity_main.xml文件,将布局从RelativeLayout更改为LinearLayout。它将如下所示:<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"> </LinearLayout> -
添加一个
TextView并包含以下 ID:android:id="@+id/textView" -
添加
Button并包含以下 ID:android:id="@+id/button" -
打开
MainActivity.java并将以下代码添加到onCreate()方法中,以设置onClick事件监听器:Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { ((TextView)findViewById(R.id.textView)).setText("Changed at runtime!"); LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)view.getLayoutParams(); params.leftMargin += 5; } }); -
在设备或模拟器上运行程序。
它是如何工作的……
每个视图(以及因此 ViewGroup)都有一组与其关联的布局参数。特别是,所有视图都有参数来通知其父视图其期望的高度和宽度。这些参数由 layout_height 和 layout_width 参数定义。我们可以通过 getLayoutParams() 方法从代码中访问这些布局信息。布局信息包括布局高度、宽度、边距以及任何类特定的参数。在这个例子中,我们通过获取按钮的 LayoutParams 并更改边距来在每次点击时移动按钮。
使用层次结构查看器优化布局
在开始优化布局之前,了解 Android 布局过程很有帮助。布局填充开始于活动首次显示时。发生三个步骤:
-
测量:这是视图确定其大小的地方,从父视图开始,遍历所有子视图。父视图可能需要多次调用其子视图来确定最终大小。
-
布局:这是父视图确定其子视图位置的地方
-
绘制:这是视图实际渲染的地方
这个过程从父节点开始,然后遍历其所有子节点。这些子节点再遍历它们的子节点。这创建了布局树,其中父节点成为树中的根节点。
层次结构查看器是 Android SDK 中包含的一个工具,用于检查布局。它以图形方式显示布局树以及每个视图/节点的计时结果。通过检查布局树和计时结果,你可以查找低效的设计和瓶颈。有了这些信息,你就可以优化你的布局。
对于这个菜谱,我们将使用层次结构查看器来检查 使用 RelativeLayout 菜谱中给出的示例布局。
准备工作
在更多内容…部分的使用 RelativeLayout菜谱中,展示了LinearLayout示例以突出显示布局之间的差异。评论指出LinearLayout需要一个嵌套布局。我们将使用示例LinearLayout创建一个名为OptimizingLayouts的新项目。然后我们将使用 Hierarchy Viewer 来检查布局。我们为此菜谱需要一个 rooted Android 设备或模拟器。
注意事项
Hierarchy Viewer 只能连接到 rooted 设备,例如模拟器。
如何操作...
-
在 Android Studio 中打开
OptimizingLayouts项目。在你的 rooted 设备(或模拟器)上运行项目,并确保屏幕可见(如有需要,请解锁)。 -
在 Android Studio 中,通过转到以下菜单选项来启动 Android Device Monitor:Tools | Android | Android Device Monitor。
-
在 Android Device Monitor 中,通过转到Window | Open Perspective…来切换到 Hierarchy View 视角,这将弹出以下对话框:
![如何操作...]()
-
现在点击Hierarchy Viewer和OK。
-
在左侧的Windows部分是运行中的进程的设备列表。点击
OptimizingLayouts进程以检查布局。![如何操作...]()
-
在TreeView部分(位于中心面板,占据了大部分的 Hierarchy Viewer 视角)中查看此活动的图形表示。
![如何操作...]()
它是如何工作的...
树布局部分显示了构成此布局的视图的图形层次结构,以及布局时间。(不幸的是,对于这个演示,渲染时间太快,无法用于视觉颜色编码参考。)对于这个例子来说,重要的是之前显示的嵌套LinearLayouts。(花些时间探索构成此布局的其他视图是有益的,这样你就可以看到 Android 在我们背后做了什么。)
如同在RelativeLayout示例中已提到的,解决方案是使用RelativeLayout重新设计这个布局。理想情况下,我们希望有一个更宽、更平的布局,而不是深层嵌套的布局,以减少在尺寸步骤中所需的迭代次数。对于时间来说,这显然是一个简单的例子,但即使是这个例子也可能产生影响。想象一下,用户基于这个低效的布局在包含数千项的ListView中快速翻页。如果你在滚动时遇到卡顿,你的优化步骤可以从检查 Hierarchy Viewer 中的布局开始。
更多内容...
Lint是 Android SDK 中包含的另一个工具,由 Android Studio 内置支持。默认情况下,你已经在使用 Lint 来检查你的代码中的问题,如已弃用的 API 调用、针对目标 API 级别的未支持 API 调用、安全问题等。对于我们的优化布局关注点,Lint 将自动检查的一些条件包括以下内容:
-
深层布局——默认最大深度为 10 级
-
嵌套权重,这会影响性能
-
无用的父节点
-
无用的叶子节点
如果你检查 Android Studio 中此布局的 Lint 警告,你将在第二个 LinearLayout 元素上看到以下警告:

ViewStub 也可以用来优化布局。将 ViewStub 想象成布局的“懒加载”。在 ViewStub 中的布局只有在需要时才会被填充,这减少了需要填充的视图数量。布局将渲染得更快,并使用更少的内存。这是一个很好的方法,可以在需要时提供很少使用的功能,例如 打印 功能,但在不需要时不会占用内存。以下是一个 ViewStub 的示例:
<ViewStub
android:id="@+id/viewStubPrint"
android:inflatedId="@id/print"
android:layout="@layout/print"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
实际上,有两种方法可以填充 ViewStub:
-
将
ViewStub的可见性参数设置为VISIBLE:((ViewStub) findViewById(R.id.viewStubPrint)).setVisibility(View.VISIBLE); -
在
ViewStub上调用inflate()方法:View view = ((ViewStub) findViewById(R.id.viewStubPrint)).inflate();
一旦 ViewStub 被填充,ViewStub 的 ID 将从布局中移除,并替换为填充后的 ID。
第三章. 视图、组件和样式
在本章中,我们将涵盖以下主题:
-
将组件插入布局中
-
使用图形来显示按钮状态
-
在运行时创建组件
-
创建自定义组件
-
将样式应用于视图
-
将样式转换为主题
-
根据 Android 操作系统版本选择主题
简介
在 Android 中,组件一词可以指代几个不同的概念。当大多数人谈论组件时,他们指的是应用组件,通常在主屏幕上看到。应用组件作为独立的迷你应用程序存在,因为它们通常提供基于其主要应用程序的功能子集。(通常,大多数应用组件与应用程序一起安装,但这不是必需的。它们可以作为独立的应用程序以组件格式存在。)一个常见的应用组件示例是提供多个不同主屏幕应用组件的天气应用程序。第五章,探索片段、应用组件和系统 UI,将讨论主屏幕应用组件并提供创建自己的食谱。
在为 Android 开发时,组件一词通常指的是放置在布局文件中的专用视图,例如按钮、TextView、CheckBox 等。在本章中,我们将专注于应用开发中的组件。
要查看Android SDK提供的组件列表,请在 Android Studio 中打开布局文件,并点击设计选项卡。在设计视图的左侧,您将在布局部分下方看到组件部分,如下面的截图所示:

如您从列表中看到的,Android SDK提供了许多有用的组件——从简单的 TextView、Button 或 Checkbox 到更复杂的组件,如时钟、日期选择器和日历。尽管内置组件很有用,但也很容易在 SDK 提供的内容上扩展。我们可以扩展现有的组件以自定义其功能,或者我们可以通过扩展基本视图类从头开始创建自己的组件。(我们将在创建自定义组件食谱中提供一个示例。)
组件的视觉外观也可以进行自定义。这些设置可以用来创建样式,进而可以用来创建主题。就像在其他开发环境中一样,创建主题的好处是可以通过最小的努力轻松地更改整个应用程序的外观。最后,Android SDK 还提供了许多内置的主题和变体,例如 Android 3/4 的 Holo 主题和 Android 5 的 Material 主题。(Android 6.0 没有发布新的主题。)
将组件插入布局中
如您从前面的示例中看到的,小部件是在布局文件中声明的,或者是在代码中创建的。对于这个示例,我们将一步一步地使用 Android Studio Designer 添加一个按钮。 (对于后续的示例,我们只展示 TextView 的布局 XML。) 创建按钮后,我们将创建一个onClickListener()。
准备工作
在 Android Studio 中启动一个新项目,并将其命名为InsertWidget。使用创建手机和平板项目的默认选项,并在提示活动类型时选择Empty Activity。您可以删除默认的 TextView(或者保留它),因为在这个示例中不需要它。
如何操作...
要将小部件插入布局中,请按照以下步骤操作:
-
在 Android Studio 中打开activity_main.xml文件,并点击设计选项卡。
![如何操作...]()
-
在小部件列表中找到Button并将其拖到右侧活动屏幕的中心。Android 会根据按钮放置的位置自动设置布局参数。如果您将按钮居中,如图所示,Android Studio 会在 XML 中设置这些参数。
![如何操作...]()
-
要查看创建的
xml,请点击文本选项卡,如图所示。看看按钮是如何使用RelativeLayout参数居中的。同时,请注意默认 ID,我们将在下一步需要它。![如何操作...]()
-
现在,打开
MainActivity.java文件进行代码编辑。将以下代码添加到onCreate()方法中,以设置onClickListener():Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this,"Clicked",Toast.LENGTH_SHORT).show(); } }); -
在设备或模拟器上运行应用程序。
它是如何工作的...
使用 Android Studio 创建 UI 就像拖放视图一样简单。您还可以直接在设计选项卡中编辑视图的属性。切换到 XML 代码就像点击文本选项卡一样简单。
我们在这里所做的是 Android 开发中非常常见的事情——在 XML 中创建 UI,然后在 Java 代码中连接 UI 组件(视图)。要从代码中引用视图,它必须与一个资源标识符相关联。这是通过使用id参数来完成的:
android:id="@+id/button"
当按钮被按下时,我们的onClickListener函数会在屏幕上显示一个名为Toast的弹出消息。
更多内容...
再次查看我们之前创建的标识符的格式,@+id/button。@指定这是一个资源,而+符号表示这是一个新资源。(如果我们没有包含加号,我们会得到一个编译时错误,指出没有资源与指定的名称匹配)。
参考信息
- Butter Knife(开源项目)——Android 视图的字段和方法绑定:
jakewharton.github.io/butterknife/
使用图形来显示按钮状态
我们已经讨论了 Android 视图的通用性以及如何自定义行为和视觉外观。在这个菜谱中,我们将创建一个可绘制状态选择器,这是一个在 XML 中定义的资源,它根据视图的状态指定要使用的可绘制资源。最常用的状态及其可能的值包括:
-
state_pressed=["true" | "false"] -
state_focused=["true" | "false"] -
state_selected=["true" | "false"] -
state_checked=["true" | "false"] -
state_enabled=["true" | "false"]
要定义状态选择器,创建一个包含<selector>元素的 XML 文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<selector >
</selector>
在<selector>元素内,我们定义一个<item>来识别基于指定状态(s)要使用的可绘制资源。以下是一个使用多个状态的<item>元素的示例:
<item
android:drawable="@android:color/darker_gray"
android:state_checked="true"
android:state_selected="false"/>
小贴士
重要的是要记住文件是从上到下读取的,所以第一个满足状态要求的项将被使用。一个默认的可绘制资源,即不包含任何状态的资源,需要放在最后。
对于这个菜谱,我们将使用状态选择器根据ToggleButton状态改变背景颜色。
准备工作
在 Android Studio 中创建一个新的项目,命名为StateSelector,使用默认的手机和平板选项。当提示活动类型时,选择空活动。为了使编写此菜谱的代码更简单,我们将使用颜色作为表示按钮状态的图形。
如何做...
我们将首先创建状态选择器,这是一个用 XML 代码定义的资源文件。然后我们将设置按钮使用我们新的状态选择器。以下是步骤:
-
在
res/drawable文件夹中创建一个新的XML文件,命名为:state_selector.xml。该文件应包含以下 XML 代码:<?xml version="1.0" encoding="utf-8"?> <selector > <item android:drawable="@android:color/darker_gray" android:state_checked="true"/> <item android:drawable="@android:color/white" android:state_checked="false"/> </selector> -
现在打开activity_main.xml文件,并按照以下方式添加一个
ToggleButton:<ToggleButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="New ToggleButton" android:id="@+id/toggleButton" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:background="@drawable/state_selector" /> -
在设备或模拟器上运行应用程序。
它是如何工作的...
在这里要理解的主要概念是 Android 状态选择器。如步骤 2 所示,我们创建了一个资源文件,它指定了基于state_checked的可绘制资源(在这种情况下是一个颜色)。
Android 支持许多其他状态条件,除了已检查状态。在输入android:state时,查看自动完成下拉列表以查看其他选项列表。
一旦创建了可绘制资源(第 1 步中的 XML),我们只需告诉视图使用它。由于我们希望背景颜色根据状态改变,所以我们使用android:background属性。
state_selector.xml是一个可绘制资源,可以被传递给任何接受可绘制资源的属性。例如,我们可以用以下 XML 替换复选框中的按钮:
android:button="@drawable/state_selector"
还有更多...
如果我们想要实际的图像而不是仅仅改变颜色,这就像更改项目状态中引用的可绘制资源一样简单。
可以下载的源代码使用了两个图形图像,下载自:pixabay.com/(之所以选择这些图像,是因为它们免费使用且不需要登录。)
一旦你有了你想要的图片,将它们放置在res/drawable文件夹中。然后,将 XML 中的状态项行更改为引用你的图片。以下是一个示例:
<item
android:drawable="@drawable/checked_on"
android:state_checked="true"/>
(将check_on更改为与你的图像资源名称匹配。)
使用特定屏幕的资源文件夹
当 Android 遇到@drawable引用时,它期望在res/drawable文件夹中的一个找到目标。这些是为不同的屏幕密度设计的:ldpi(每英寸低点)、mdpi(中等)、hdpi(高)和xhdpi(超高),它们允许我们为特定的目标设备创建资源。当应用程序在特定设备上运行时,Android 将从最接近实际屏幕密度的指定文件夹中加载资源。
如果它发现这个文件夹为空,它将尝试下一个最近的匹配,依此类推,直到找到命名的资源。出于教程目的,不需要为每个可能的密度设置一组单独的文件,因此将我们的图片放在drawable文件夹中是运行任何设备上的练习的简单方法。
小贴士
要获取可用的资源标识符的完整列表,请访问developer.android.com/guide/topics/resources/providing-resources.html。
参见
对于另一个关于 Android 资源选择的示例,请参阅根据操作系统版本选择主题的配方。
在运行时创建小部件
如前所述,通常 UI 在XML文件中声明,然后在运行时通过 Java 代码进行修改。虽然可以在 Java 代码中完全创建 UI,但对于复杂的布局,通常不会将其视为最佳实践。
上一章的 GridView 示例是在代码中创建的。但与 GridView 配方不同,在这个配方中,我们将向在activity_main.xml中定义的现有布局添加一个视图。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为RuntimeWidget。当被提示选择Activity 类型时,选择Empty Activity选项。
如何做到这一点...
我们将首先向现有的布局添加一个 ID 属性,这样我们就可以在代码中访问布局。一旦我们在代码中有了布局的引用,我们就可以向现有的布局添加新的视图。以下是步骤:
-
打开
res/layout/activity_main.xml,并将 ID 属性添加到主RelativeLayout中,如下所示:android:id="@+id/layout" -
完全移除默认的
<TextView>元素。 -
打开
MainActivity.java文件,以便我们可以向onCreate()方法添加代码。在setContentView()之后添加以下代码以获取RelativeLayout的引用:RelativeLayout layout = (RelativeLayout)findViewById(R.id.layout); -
创建一个 DatePicker 并将其添加到布局中,如下所示:
DatePicker datePicker = new DatePicker(this); layout.addView(datePicker); -
在设备或模拟器上运行程序。
它是如何工作的...
这应该是非常直接的代码。首先,我们使用findViewById获取父布局的引用。我们在步骤 1 中添加了 ID 到现有的RelativeLayout,以便更容易引用。我们通过addView()方法在代码中创建一个 DatePicker 并将其添加到布局中。
还有更多...
如果我们想完全通过代码创建布局呢?虽然这不一定被认为是最佳实践,但在某些情况下,从代码创建布局确实更容易(且更简单)。让我们看看如果不使用activity_main.xml中的布局,这个示例会是什么样子。以下是onCreate()方法的样子:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RelativeLayout layout = new RelativeLayout(this);
DatePicker datePicker = new DatePicker(this);
layout.addView(datePicker);
setContentView(layout);
}
在这个例子中,实际上并没有太大的不同。如果你在代码中创建了一个视图并希望在以后引用它,你要么需要保留对对象的引用,要么给视图分配一个 ID 以使用findViewByID()。要给视图分配一个 ID,可以使用**setID()**方法并通过传递**View.generateViewId()**(以生成一个唯一的 ID)或使用 xml 中的**<resources>**来定义 ID。
创建一个自定义组件
正如我们在之前的菜谱中看到的,Android SDK 提供了广泛的各种组件。但当你找不到适合你独特需求的预构建组件时会发生什么?你总是可以创建自己的!
在这个菜谱中,我们将通过创建一个继承自View类的自定义组件,就像内置小部件一样。以下是高级概述:
-
创建一个新的类,它扩展了
View。 -
创建自定义构造函数。
-
重写
onMeasure(),默认实现返回 100 x 100 的大小。 -
重写
onDraw(),默认实现不绘制任何内容。 -
定义自定义方法和监听器(例如
on<*Event*>())。 -
实现自定义功能。
小贴士
虽然onMeasure()和onDraw()方法不是强制要求的,但默认行为可能并不是你想要的。
准备工作
在 Android Studio 中启动一个新的项目,并将其命名为CustomView。使用默认向导选项,包括Phone & Tablet SDK,并在被提示选择活动类型时选择Empty Activity。一旦项目文件创建并打开在 Android Studio 中,你就可以开始工作了。
如何做到这一点...
我们将为自定义组件创建一个新的类,它将继承自 Android 的View类。我们的自定义组件可以是现有类的子类,例如Activity,但我们将它创建在单独的文件中,以便更容易维护。以下是步骤:
-
首先,创建一个新的 Java 类,并将其命名为
CustomView。这就是我们将实现自定义组件的地方,正如介绍中所描述的。 -
修改类构造函数,使其扩展
View。它应该如下所示:public class CustomView extends View { -
为类定义一个
Paint对象,它将在onDraw()中使用:final Paint mPaint = new Paint(); -
创建一个默认构造函数,它需要一个活动
Context,这样我们就可以填充视图。我们也将在这里设置画笔属性。构造函数应该如下所示:public CustomView(Context context) { super(context); mPaint.setColor(Color.BLACK); mPaint.setTextSize(30); } -
如下重写
onDraw()方法:@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); setBackgroundColor(Color.CYAN); canvas.drawText("Custom Text", 100, 100, mPaint); invalidate(); } -
最后,在
MainActivity.java中通过将setContentView()替换为我们的视图来扩展我们的自定义视图,如下所示:setContentView(new CustomView(this)); -
在设备或模拟器上运行应用程序以查看其实际效果。
它是如何工作的...
我们首先扩展了View类,就像内置组件一样。接下来,我们创建了默认构造函数。这很重要,因为我们需要将上下文传递给父类,我们通过调用以下方式实现:
super(context);
我们需要重写onDraw(),否则,如引言中提到的,我们的自定义视图将不会显示任何内容。当onDraw()被调用时,系统会传递一个画布对象。画布是屏幕上我们的视图区域。(由于我们没有重写onMeasure(),我们的视图将是 100 x 100,但由于我们的整个活动只包含这个视图,所以我们得到整个屏幕作为我们的画布。)
我们在类级别创建了Paint对象,并将其声明为final,以提高内存分配的效率。(onDraw()应该尽可能高效,因为它可能每秒被调用多次。)正如你在运行程序时看到的那样,我们的onDraw()实现只是将背景颜色设置为青色,并在屏幕上打印文本(使用drawText())。
更多内容...
实际上,还有很多。我们只是触及了使用自定义组件可以做的事情的表面。幸运的是,正如你从这个例子中看到的那样,实现基本功能不需要很多代码。我们很容易花上一整章来讨论诸如将布局参数传递给视图、添加监听器回调、重写onMeasure()、在 IDE 中使用我们的视图等问题。这些都是你可以根据需要添加的功能。
虽然自定义组件应该能够处理任何解决方案,但还有其他可能需要更少编码的选项。扩展现有小部件通常足以避免从头开始创建自定义组件的开销。如果你需要的是包含多个小部件的解决方案,还有复合控件。例如,组合框就是一个或多个控件组合在一起作为一个单独的小部件。
复合控件通常从布局而不是视图开始扩展,因为你将添加多个小部件。你可能不需要重写onDraw()和onMeasure()方法,因为每个小部件都会在其相应的方法中处理绘制。
参见
- 关于绘制的更多信息,请参阅第九章,图形和动画。关于视图对象的完整详细信息,请参考 Android 开发者资源:
developer.android.com/reference/android/view/View.html
将样式应用于视图
样式是一组属性设置,用于定义视图的外观。正如你在定义布局时已经看到的,视图提供了许多设置来决定其外观以及功能。我们已经在视图中设置了高度、宽度、背景颜色和填充,还有许多其他设置,如文本颜色、字体、文本大小、边距等。创建样式就像将这些设置从布局中提取出来,并将它们放入样式资源中。
在本菜谱中,我们将逐步讲解创建样式并将其连接到视图的步骤。
与层叠样式表(Cascading Style Sheets)类似,Android 样式允许你将设计设置与 UI 代码分开指定。
准备工作
创建一个新的 Android Studio 项目,并将其命名为 Styles。使用默认向导选项创建一个手机和平板项目,并在提示选择 Activity 时选择空 Activity。默认情况下,向导还会创建一个 styles.xml 文件,我们将使用这个文件来完成本菜谱。
如何操作...
我们将创建自己的样式资源来改变 TextView 的外观。我们可以使用以下步骤将我们的新样式添加到 Android Studio 创建的 styles.xml 资源中:
-
打开默认的
styles.xml文件,该文件位于res/values目录中,如图所示:![如何操作...]()
-
我们将通过在现有的
AppTheme样式下方添加以下 XML 来创建一个名为MyStyle的新样式:<style name="MyStyle"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> <item name="android:background">#000000</item> <item name="android:textColor">#AF0000</item> <item name="android:textSize">20sp</item> <item name="android:padding">8dp</item> <item name="android:gravity">center</item> </style> -
现在告诉视图使用这个样式。打开
activity_main.xml文件,并将以下属性添加到现有的<TextView>元素中:style="@style/MyStyle" -
要么运行应用程序,要么在 设计 选项卡中查看结果。
工作原理...
样式是一个资源,通过在 xml 文件的 <resources> 元素中嵌套 <style> 元素来定义。我们使用了现有的 styles.xml 文件,但这不是必需的,因为我们可以使用任何我们想要的文件名。正如本菜谱所示,一个 xml 文件中可以包含多个 <style> 元素。
一旦创建了样式,你就可以轻松地将它应用到任意数量的其他视图上。如果你想有一个具有相同样式的按钮?只需在布局中添加一个按钮,并分配相同的样式。
如果我们创建了一个新的按钮,但希望按钮扩展到视图的全宽?我们如何只为该视图覆盖样式?很简单,就像你以前做的那样,在布局中指定属性。局部属性将优先于 style 中的属性。
还有更多...
样式还有一个特性:继承。通过在定义样式时指定父样式,我们可以让样式相互构建,创建一个样式层次结构。如果你查看 styles.xml 中的默认样式 AppTheme,你会看到以下行:
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
AppTheme 继承自 Android SDK 中定义的主题。
小贴士
如果你想要继承你创建的自定义样式,有一个快捷方法。你不需要使用父属性,而是首先指定父名称,然后跟一个点号,接着是新名称,例如:
<style name="MyParent.MyStyle" >
你看到了如何为视图指定样式,但如果我们想让应用程序中的所有 TextView 使用特定的样式怎么办?我们不得不回到每个 TextView 并指定样式。但还有另一种方法。我们可以在样式中包含一个 textViewStyle 项,以自动将样式分配给所有 TextView。(每种小部件类型都有一个样式,因此你可以为按钮、切换按钮、TextView 等执行此操作。)
要设置所有 TextView 的样式,将以下行添加到 AppTheme 样式中:
<item name="android:textViewStyle">@style/MyStyle</item>
由于我们的应用程序主题已经使用了 AppThem,我们只需将这一行添加到 AppTheme 中,就可以让所有 TextView 使用我们的自定义 MyStyle。
参见
在以下位置查看 Android 设计支持库:
android-developers.blogspot.de/2015/05/android-design-support-library.html
将样式转换为主题
主题是应用于 Activity 或整个应用程序的样式。要设置主题,请使用 AndroidManifest.xml 文件中的 android:theme 属性。theme 属性适用于 <Application> 元素以及 <Activity> 元素。该元素内的所有视图都将使用指定的主题进行样式化。
设置应用程序主题很常见,但随后会覆盖特定的 Activity 使用不同的主题。
在之前的菜谱中,我们使用 AppTheme 样式(由向导自动创建)设置了 textViewStyle。在这个菜谱中,你将学习如何设置应用程序和 Activity 主题。
除了我们已经探索过的样式设置外,还有一些我们没有讨论的额外样式选项,因为它们不适用于视图,而是适用于整个窗口。例如,隐藏应用程序标题或操作栏,以及设置窗口背景等设置,都适用于窗口,因此必须作为主题设置。
对于这个菜谱,我们将基于自动生成的 AppTheme 创建一个新的主题。我们的新主题将修改窗口外观,使其成为一个对话框。我们还将查看 AndroidManifest.xml 中的 theme 设置。
准备工作
在 Android Studio 中启动一个新的项目,并将其命名为 Themes。使用默认向导选项,并在被提示选择 Activity 类型时选择空活动。
如何做到这一点...
我们首先向现有的 styles.xml 文件中添加一个新的主题,使我们的活动看起来像对话框。以下是创建新主题并将活动设置为使用新主题的步骤:
-
由于主题与样式定义在相同的资源中,请打开位于
res/values的styles.xml文件并创建一个新的样式。我们将基于已提供的 AppTheme 创建一个新的样式,并设置windowIsFloating。XML 将如下所示:<style name="AppTheme.MyDialog"> <item name="android:windowIsFloating">true</item> </style> -
接下来,设置活动使用这个新的对话框主题。打开
AndroidManifest.xml文件,并将theme属性添加到活动元素中,如下所示:<activity android:name=".MainActivity" android:theme="@style/AppTheme.MyDialog">注意,现在应用程序和活动都将指定一个主题。
-
现在,在设备或模拟器上运行应用程序,以查看对话框主题的实际效果。
它是如何工作的...
我们的新主题 MyDialog 使用替代父声明继承了基本 AppTheme,因为 AppTheme 在我们的代码中定义(而不是系统主题)。如介绍中所述,一些设置适用于整个窗口,这就是我们看到的 windowIsFloating 设置。一旦我们声明了新的主题,我们就在 AndroidManifest 文件中将我们的主题分配给活动。
还有更多...
你可能已经注意到,我们只需将 windowIsFloating 添加到现有的 AppTheme 中,就可以完成。由于此应用程序只有一个活动,最终结果将是相同的,但这样,任何新的活动也会显示为对话框。
根据 Android 版本选择主题
大多数用户更喜欢看到使用 Android 提供的最新主题的应用程序。"现在支持 Material 主题" 对于升级到 Android Lollipop 的应用程序来说是常见的。为了在市场上与其他许多应用程序竞争,你可能也想升级你的应用程序,但你的用户还在运行较旧的 Android 版本怎么办?通过正确设置我们的资源,我们可以使用 Android 中的 资源选择 来自动根据用户运行的 Android 操作系统版本定义父主题。
首先,让我们探索 Android 中可用的三个主要主题:
-
主题 – 蜜饯面包和更早版本
-
Theme.Holo – Honeycomb (API 11)
-
Theme.Material – Lollipop (API 21)
(截至撰写本文时,Android 6.0 中似乎没有新的主题。)
本食谱将展示如何为 Android 正确设置资源目录,以便根据应用程序运行的 API 版本使用最合适的主题。
准备工作
在 Android Studio 中创建一个新的项目,命名为 AutomaticThemeSelector。使用默认向导选项创建一个手机和平板项目。当提示活动类型时,选择空活动。
如何做到这一点...
根据选择的 API 版本,Android Studio 可能会使用应用程序兼容性库。我们不想在这个项目中使用这些库,因为我们想明确设置要使用哪个主题。我们将首先确保我们扩展了通用的 Activity 类,然后我们可以添加我们的新样式资源来根据 API 选择主题。以下是步骤:
-
我们需要确保
MainActivity扩展自Activity而不是AppCompatActivity。打开ActivityMain.java并在必要时将其更改为以下内容:public class MainActivity extends Activity { -
打开
activity_main.xml并添加两个视图:一个按钮和一个复选框。 -
打开
styles.xml并删除AppTheme,因为它将不会使用。添加我们的新主题,使文件如下所示:<resources> <style name="AutomaticTheme" parent="android:Theme.Light"> </style> </resources> -
我们需要为 API 11 和 21 创建两个新的值文件夹。为此,我们需要将 Android Studio 更改为使用项目视图而不是 Android 视图。(否则,我们不会在下一步看到新文件夹。)在项目窗口的顶部,它显示Android,将其更改为项目以使用项目视图。请参阅以下截图:
![如何操作...]()
-
通过在
res文件夹上右键单击并导航到新建 | 目录来创建一个新的目录,如图所示:![如何操作...]()
使用以下名称为第一个目录:
values-v11使用
values-v21重复此操作为第二个目录 -
现在在每个新目录中创建一个
styles.xml文件。(在values-v11目录上右键单击,然后选择新建 | 文件选项。)对于values-v11,使用以下样式来定义 Holo 主题:<resources> <style name="AutomaticTheme" parent="android:Theme.Holo.Light"> </style> </resources> For the values-v21, use the following code to define the Material theme: <resources> <style name="AutomaticTheme" parent="android:Theme.Material.Light"> </style> </resources> -
最后一步是告诉应用程序使用我们新的主题。为此,打开
AndroidManifest.xml并将android:theme属性更改为AutomaticTheme。它应该如下所示:android:theme="@style/AutomaticTheme" -
现在在物理设备或模拟器上运行应用程序。如果您想看到三种不同的主题,您将需要一个运行不同版本 Android 的设备或模拟器。
它是如何工作的...
在这个菜谱中,我们使用 Android 资源选择过程根据 API 版本分配适当的主题(这是一个资源)。由于我们需要根据发布时的操作系统版本来选择主题,因此我们创建了两个新的值文件夹来指定 API 版本。这使我们总共有三个styles.xml文件:默认样式,一个在values-v11目录中,最后一个在values-v21目录中。
注意,在所有三个styles.xml文件中定义了相同的主题名称。这就是资源选择的工作方式。Android 将使用最适合我们值的目录中的资源。在这里,我们使用 API 级别,但还有其他标准可用。根据屏幕大小、屏幕密度,甚至方向定义单独的资源是非常常见的。
最后一步是将我们的新主题指定为应用程序主题,我们在 AndroidManifest 中做到了这一点。
还有更多…
关于资源选择的更多信息,请参阅前一个菜谱中“使用指定文件夹为屏幕特定资源”主题,使用图形显示按钮状态。
第四章。菜单
在本章中,我们将涵盖以下主题:
-
创建选项菜单
-
在运行时修改菜单和菜单项
-
为视图启用上下文操作模式
-
使用 ListView 的上下文批量模式
-
创建弹出菜单
简介
Android 操作系统是一个不断变化的环境。最早的 Android 设备(在 Android 3.0 之前),需要有一个硬件菜单按钮。尽管硬件按钮不再需要,但菜单同样重要。事实上,菜单 API 已扩展到现在支持三种不同类型的菜单:
-
选项菜单和操作栏:这是标准菜单,用于应用程序的全局选项。用于搜索、设置等附加功能。
-
上下文 模式(上下文操作模式):通常通过长按激活。(将其视为类似于桌面上的右键点击。)这用于对按下的项目执行操作,例如回复电子邮件或删除文件。
-
弹出 菜单:这为附加操作提供了一个弹出选择(类似于旋转选择器)。菜单选项不旨在影响按下的项目,而是使用前面描述的上下文模式。一个例子是点击分享按钮并获取额外的分享选项列表。
菜单资源类似于其他 Android UI 组件;它们通常在 XML 中创建,但也可以在代码中创建。我们将在以下部分中展示的第一个菜谱,将展示 XML 菜单格式以及如何填充它。
创建选项菜单
在我们实际创建和显示菜单之前,让我们看看一个菜单以查看最终结果。以下是一个显示 Chrome 菜单部分的屏幕截图:

最明显的特点是菜单的外观会根据屏幕大小而有所不同。默认情况下,菜单项将被添加到溢出菜单中——即你按下最右侧边缘的三个点时看到的菜单。
菜单通常使用 XML(如许多其他 Android 资源)在资源文件中创建,但它们存储在 res/menu 目录中,尽管也可以在代码中创建。要创建菜单资源,请使用如下所示的 <menu> 元素:
<menu >
</menu>
<item> 元素定义每个单独的菜单项,并包含在 <menu> 元素中。一个基本的菜单项看起来如下:
<item
android:id="@+id/settings"
android:title="@string/settings" />
最常见的 <item> 属性如下:
-
id:这是一个标准的资源标识符 -
title:这表示要显示的文本 -
图标:这是一个可绘制的资源 -
showAsAction:如下所述(见下一段) -
enabled:默认启用
让我们更详细地看看 showAsAction。
showAsAction 属性控制菜单项的显示方式。选项包括以下内容:
-
ifRoom:如果空间足够,则应将此菜单项包含在操作栏中 -
withText:这表示标题和图标都应该显示 -
never:这表示菜单项永远不会包含在操作栏中;始终显示在溢出菜单中 -
always:这表示菜单项应该始终包含在操作栏中(谨慎使用,因为空间有限)注意
可以使用管道(|)分隔符组合多个选项,例如
showAsAction="ifRoom|withText"。
在了解了菜单资源的根本之后,我们现在可以创建一个标准的选项菜单并将其填充。
准备工作
使用 Android Studio 创建一个名为OptionsMenu的新项目。使用默认的手机和平板选项,并在提示活动类型时选择空活动选项。由于向导默认不会创建res/menu文件夹,请在继续之前导航到文件 | 新建 | 目录来创建它。
如何做到这一点...
根据前述章节中创建的新项目,你现在可以创建一个菜单。然而,首先,我们将向strings.xml文件添加一个字符串资源作为菜单标题。当创建菜单的 XML 时,我们将使用这个新字符串作为菜单标题。以下是步骤:
-
首先打开
strings.xml文件,并在<resources>元素中添加以下<string>元素:<string name="menu_settings">Settings</string> -
在
res/menu目录中创建一个新文件,并将其命名为menu_main.xml。 -
打开
menu_main.xml文件,并添加以下 XML 以定义菜单:<?xml version="1.0" encoding="utf-8"?> <menu > <item android:id="@+id/menu_settings" android:title="@string/menu_settings" app:showAsAction="never"> </item> </menu> -
菜单现在已创建,我们只需在
ActivityMain.java中重写onCreateOptionsMenu()方法以填充菜单:@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); return true; } -
在设备或模拟器上运行程序,以查看操作栏中的菜单。
它是如何工作的...
这里有两个基本步骤:
-
在 XML 中定义菜单。
-
在活动创建时填充菜单。
作为良好的编程习惯,我们在strings.xml文件中定义字符串,而不是在XML中硬编码它。然后我们使用标准的 Android 字符串标识符在第 3 步中设置菜单的标题。由于这是一个“设置”菜单项,我们不希望它在操作栏中显示。为了确保它永远不会显示,使用showAsAction="never"。
菜单定义完成后,我们将在第 4 步中使用菜单填充器在活动创建期间加载菜单。注意R.menu.menu_main菜单资源语法?这就是为什么我们在res/menu目录中创建 XML 的原因——这样系统就会知道这是一个菜单资源。
在第 4 步中,我们使用了app:showAsAction而不是 Android 的android:showAsAction。这是因为我们正在使用AppCompat库(也称为 Android 支持库)。默认情况下,Android Studio 的新项目向导会将支持库包含在项目中。
更多内容...
如果你运行了第 5 步的程序,那么当你按下菜单溢出按钮时,你必须已经看到了设置菜单项。但仅此而已。没有其他发生。显然,如果应用程序不响应用户的菜单项,菜单项就没什么用了。响应用户的选项菜单是通过onOptionsItemSelected()回调完成的。
将以下方法添加到应用程序中,以便在选择设置菜单时显示 Toast:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menu_settings) {
Toast.makeText(this, "Settings", Toast.LENGTH_LONG).show();
} else {
return super.onContextItemSelected(item);
}
return true;
}
就这些了。你现在有一个可以工作的菜单了!
小贴士
如前例所示,当处理回调时返回true;否则,如else语句所示调用超类。
使用菜单项启动活动
在这个例子中,我们显示了一个 Toast,以便我们可以看到工作示例;然而,如果需要,我们也可以轻松地启动一个新的活动。正如你在第一章的“使用 Intent 对象启动新活动”食谱中所做的那样,创建一个 Intent 并使用startActivity()调用它。
创建子菜单
子菜单的创建和访问几乎与其他菜单元素完全相同,可以放置在任何提供的菜单中,尽管它们不能放置在其他子菜单中。要定义子菜单,请在<item>元素内包含一个<menu>元素。以下是本食谱中添加了两个子菜单项的 XML 形式:
<?xml version="1.0" encoding="utf-8"?>
<menu
>
<item android:id="@+id/menu_settings
android:title="@string/menu_settings"
app:showAsAction="never">
<menu>
<item android:id="@+id/menu_sub1"
android:title="Storage Settings" />
<item android:id="@+id/menu_sub2"
android:title="Screen Settings" />
</menu>
</item>
</menu>
分组菜单项
Android 支持的其他菜单功能之一是分组菜单项。Android 提供了几个用于分组的函数,包括以下内容:
-
setGroupVisible(): 显示或隐藏所有项目 -
setGroupEnabled(): 启用或禁用所有项目 -
setGroupCheckable(): 设置可勾选行为
小贴士
Android 会将所有带有showAsAction="ifRoom"的分组项一起保留。这意味着组中所有带有showAsAction="ifRoom"的项都将显示在操作栏中,或者所有项都将显示在溢出菜单中。
要创建一个组,将<item>菜单元素添加到<group>元素中。以下是一个使用本食谱中菜单 XML 的示例,其中包含了一个组中的两个附加项:
<?xml version="1.0" encoding="utf-8"?>
<menu
>
<group android:id="@+id/group_one" >
<item android:id="@+id/menu_item1"
android:title="Item 1"
app:showAsAction="ifRoom"/>
<item android:id="@+id/menu_item2"
android:title="Item 2"
app:showAsAction="ifRoom"/>
</group>
<item android:id="@+id/menu_settings"
android:title="@string/menu_settings"
app:showAsAction="never"/>
</menu>
参见
- 要获取有关菜单的完整详细信息,请访问 Android 开发者菜单资源网站
developer.android.com/guide/topics/resources/menu-resource.html
在运行时修改菜单和菜单项
虽然已经多次提到,但被认为的“最佳”编程实践是在 XML 中而不是在 Java 中创建 UI。仍然有需要通过代码执行的时候。这尤其适用于你想要根据某些外部标准使菜单项可见(或启用)的情况。菜单也可以包含在资源文件夹中,但有时你需要代码来执行逻辑。一个例子可能是,如果你想只在用户登录你的应用时提供上传菜单项。
在这个食谱中,我们将仅通过代码创建和修改菜单。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为RuntimeMenu,使用默认的手机和平板选项。当被提示添加活动时,选择空活动选项。由于我们将在代码中完全创建和修改菜单,因此我们不需要创建res/menu目录。
如何实现...
首先,我们将添加用于我们的菜单项和切换菜单可见性的按钮的字符串资源。打开res/strings.xml文件并按照以下步骤操作:
-
将以下两个字符串添加到现有的
<resources>元素中:<string name="menu_download">Download</string> <string name="menu_settings">Settings</string> -
在
activity_main.xml中添加一个按钮,并将其onClick()设置为toggleMenu,如下所示:<Button android:id="@+id/buttonToggleMenu" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Toggle Menu" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="toggleMenu"/> -
打开
ActivityMain.java并在类声明下方添加以下三行代码:private final int MENU_DOWNLOAD = 1; private final int MENU_SETTINGS = 2; private boolean showDownloadMenu = false; -
为按钮添加以下方法以供调用:
public void toggleMenu(View view) { showDownloadMenu=!showDownloadMenu; } -
当活动首次创建时,Android 会调用
onCreateOptionsMenu()来创建菜单。以下是动态构建菜单的代码:@Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, MENU_DOWNLOAD, 0, R.string.menu_download); menu.add(0, MENU_SETTINGS, 0, R.string.menu_settings); return true; } -
为了最佳编程实践,不要使用
onCreateOptionsMenu()来更新或更改你的菜单;相反,使用onPrepareOptionsMenu()。以下是根据我们的标志更改下载菜单项可见性的代码:@Override public boolean onPrepareOptionsMenu(Menu menu) { MenuItem menuItem = menu.findItem(MENU_DOWNLOAD); menuItem.setVisible(showDownloadMenu); return true; } -
虽然这个菜谱技术上不是必需的,但这个
onOptionsItemSelected()代码展示了如何响应每个菜单项:@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_DOWNLOAD: Toast.makeText(this, R.string.menu_download, Toast.LENGTH_LONG).show(); break; case MENU_SETTINGS: Toast.makeText(this, R.string.menu_settings, Toast.LENGTH_LONG).show(); break; default: return super.onContextItemSelected(item); } return true; } -
在设备或模拟器上运行程序以查看菜单更改。
它是如何工作的...
我们为onCreateOptionsMenu()创建了一个覆盖,就像我们在之前的菜谱中做的那样,创建一个选项菜单。但是,我们不是填充现有的菜单资源,而是使用Menu.add()方法创建菜单。由于我们希望在以后也修改菜单项以及响应菜单项事件,我们定义了自己的菜单 ID 并将它们传递给add()方法。
onOptionsItemSelected()会为所有菜单项调用,因此我们获取菜单 ID 并使用基于我们创建的 ID 的switch语句。如果我们正在处理菜单事件,我们返回true,否则我们将事件传递给父类。
在onPrepareOptionsMenu()方法中发生菜单更改。为了模拟外部事件,我们创建了一个按钮来切换一个布尔标志。下载菜单的可见性由这个标志决定。这就是你根据设定的任何标准创建自定义代码的地方。你可以使用当前播放器级别或者当新级别准备发布时设置这个标志;你发送一个推送消息,以启用菜单项。
还有更多...
如果我们想让这个下载选项容易被注意到,以指示它是否可用?我们可以通过在onPrepareOptionsMenu()中添加以下代码来告诉 Android 我们想在操作栏中显示菜单(在返回语句之前):
menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
现在如果你运行代码,你将在操作栏中看到下载菜单项,但行为是不正确的。
在之前我们没有在操作栏中添加菜单项时,每次打开溢出菜单,Android 都会调用 onPrepareOptionsMenu() 方法来确保可见性始终更新。为了纠正这种行为,请将以下代码行添加到按钮调用的 toggleMenu() 方法中:
invalidateOptionsMenu();
调用 invalidateOptionsMenu() 方法告诉 Android 我们的操作菜单已不再有效,这会强制调用 onPrepareOptionsMenu() 方法,从而得到我们期望的行为。
注意
如果在操作栏中显示菜单项,Android 会认为菜单始终处于打开状态。
为视图启用上下文操作模式
上下文菜单提供与特定视图相关的附加选项——这与桌面上的右键点击概念相同。Android 目前支持两种不同的方法:浮动上下文菜单和上下文模式。上下文操作模式是在 Android 3.0 中引入的。较老的浮动上下文菜单可能会导致混淆,因为没有指示当前选中的项,并且它不支持对多个项执行操作——例如,在一次操作中删除多个电子邮件。
创建浮动上下文菜单
如果你需要使用旧式的上下文菜单,例如,为了支持 Android 3.0 之前的设备,它与选项菜单 API 非常相似,只是方法名不同。要创建菜单,请使用 onCreateContextMenu() 而不是 onCreateOptionsMenu()。要处理菜单项选择,请使用 onContextItemSelected() 而不是 onOptionsItemSelected()。最后,调用 registerForContextMenu() 来让系统知道你想要为视图获取上下文菜单事件。
由于上下文模式被认为是显示上下文选项的首选方式,本食谱将侧重于较新的 API。上下文模式提供了与浮动上下文菜单相同的特性,但通过允许在批量模式下进行多项选择,还增加了额外的功能。
本食谱将演示为单个视图设置上下文模式。一旦激活,通过长按,一个 上下文操作栏(CAB)将替换操作栏,直到上下文模式完成。
注意
上下文操作栏与操作栏不同,你的活动不需要包含操作栏。
准备工作
使用 Android Studio 创建一个新的项目,并将其命名为 ContextualMode。使用默认的 Phone & Tablet 选项,并在被提示添加活动时选择 Empty Activity。创建一个菜单目录 (res/menu),就像我们在第一个食谱中做的那样,用于存储上下文菜单的 XML。
如何实现...
我们将创建一个 ImageView 作为宿主视图以初始化上下文模式。由于上下文模式通常通过长按触发,我们将在 onCreate() 方法中为 ImageView 设置一个长按监听器。当被调用时,我们将启动上下文模式,并传递一个 ActionMode 回调来处理上下文模式事件。以下是步骤:
-
我们将首先添加两个新的字符串资源。打开
strings.xml文件并添加以下内容:<string name="menu_cast">Cast</string> <string name="menu_print">Print</string> -
字符串创建完成后,我们现在可以创建菜单,通过在
res/menu中创建一个名为context_menu.xml的新文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <menu > <item android:id="@+id/menu_cast" android:title="@string/menu_cast" /> <item android:id="@+id/menu_print" android:title="@string/menu_print" /> </menu> -
现在将一个
ImageView添加到activity_main.xml中,作为启动 Contextual Mode 的源。以下是 ImageView 的 XML:<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:src="img/ic_launcher"/> -
现在 UI 已经设置好了,我们可以添加 Contextual Mode 的代码。首先,我们需要一个全局变量来存储在调用
startActionMode()时返回的ActionMode实例。在MainActivity.java中的类构造函数下方添加以下代码行:ActionMode mActionMode; -
接下来,创建一个
ActionMode回调并将其传递给startActionMode()。在上一步骤中的代码下方添加以下代码到MainActivity类中:private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { mode.getMenuInflater().inflate(R.menu.context_menu, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id. menu_cast: Toast.makeText(MainActivity.this, "Cast", Toast.LENGTH_SHORT).show(); mode.finish(); return true; case R.id. menu_print: Toast.makeText(MainActivity.this, "Print", Toast.LENGTH_SHORT).show(); mode.finish(); return true; default: return false; } } @Override public void onDestroyActionMode(ActionMode mode) { mActionMode = null; } }; -
创建了
ActionMode回调后,我们只需要调用startActionMode()来开始 Contextual Mode。将以下代码添加到onCreate()方法中以设置长按监听器:ImageView imageView = (ImageView)findViewById(R.id.imageView); imageView.setOnLongClickListener(new View.OnLongClickListener() { public boolean onLongClick(View view) { if (mActionMode != null) return false; mActionMode = startActionMode(mActionModeCallback); return true; } }); -
在设备或模拟器上运行程序,以查看 CAB 的实际效果。
它是如何工作的...
正如你在第二步中看到的,我们使用了相同的菜单 XML 来定义上下文菜单,就像其他菜单一样。
需要理解的主要代码是ActionMode回调。这是我们处理 Contextual Mode 事件的地点:初始化菜单、处理菜单项选择和清理。我们在长按事件中通过调用startActionMode()并传递在第五步中创建的ActionMode回调来启动 Contextual Mode。
当触发操作模式时,系统会调用onCreateActionMode()回调,它会展开菜单并在 Contextual Action Bar 中显示它。用户可以通过按返回箭头或返回键来关闭 Contextual Action Bar。当用户进行菜单选择时,CAB 也会关闭。我们显示一个 Toast 来为这个菜谱提供视觉反馈,但这是你实现功能的地方。
更多内容...
在这个例子中,我们存储了从startActionMode()调用返回的ActionMode。我们使用它来防止在 Action Mode 已经激活时创建新的实例。我们也可以使用这个实例来对 Contextual Action Bar 本身进行更改,例如使用以下方式更改标题:
mActionMode.setTitle("New Title");
当我们将在下一道菜谱中看到的多项选择一起工作时,这尤其有用。
参见
- 参见下一道菜谱,使用 ListView 与 Contextual Batch Mode,以处理多项选择
使用 ListView 与 Contextual Batch Mode
如前一道菜谱中讨论的,Contextual Mode 支持两种使用形式:单视图模式(如演示所示)和多项选择(或批量)模式。批量模式是 Contextual Mode 优于旧式上下文菜单的地方,因为批量选择不受支持。
如果你曾经使用过像 Gmail 或文件浏览器这样的电子邮件应用,你可能在选择多个项时见过上下文模式。以下是从 Solid Explorer 的截图,它展示了出色的 Material 主题和上下文模式的实现:

在这个菜谱中,我们将创建一个填充多个国家名称的ListView来演示多选或批量模式。此示例将使用正常的长按事件以及项点击事件来启动上下文模式。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为ContextualBatchMode。使用默认的手机和平板电脑选项,并在被提示添加活动时选择空活动。为上下文菜单创建一个菜单目录(res/menu)。
如何做...
与上一个菜谱类似,我们首先创建一个在上下文模式开始时展开的 XML 菜单。我们需要定义MultiChoiceModeListener来处理与ListView的批量模式。然后我们设置ListView以允许多选,并传入MultiChoiceModeListener。以下是步骤:
-
打开
strings.xml文件,并添加两个新的字符串资源用于菜单项,如下所示:<string name="menu_move">Move</string> <string name="menu_delete">Delete</string> -
在
res/menu文件夹中创建一个名为contextual_menu.xml的新文件,内容如下所示:<?xml version="1.0" encoding="utf-8"?> <menu > <item android:id="@+id/menu_move" android:title="@string/menu_move" /> <item android:id="@+id/menu_delete android:title="@string/menu_delete" /> </menu> -
由于我们需要
ListView,我们将MainActivity更改为从ListActivity扩展,如下所示:public class MainActivity extends ListActivity -
创建一个
MultiChoiceModeListener来处理上下文操作栏事件。在MainActivity.java中的类构造函数下方添加以下代码:AbsListView.MultiChoiceModeListener mMultiChoiceModeListener = new AbsListView.MultiChoiceModeListener() { @Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Inflate the menu for the CAB MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.contextual_menu, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { // Handle menu selections switch (item.getItemId()) { case R.id.menu_move Toast.makeText(MainActivity.this, "Move", Toast.LENGTH_SHORT).show(); mode.finish(); return true; case R.id.menu_delete Toast.makeText(MainActivity.this, "Delete", Toast.LENGTH_SHORT).show(); mode.finish(); return true; default: return false; } } @Override public void onDestroyActionMode(ActionMode mode) { } }; -
接下来,我们将更改
onCreate()以设置ListView并使用国家名称的字符串数组填充ListAdapter,如下所示:@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"}; ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_checked, countries); setListAdapter(countryAdapter); getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); getListView().setMultiChoiceModeListener(mMultiChoiceModeListener); getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { ((ListView)parent).setItemChecked(position, true); } }); } -
在设备或模拟器上运行程序,以查看上下文操作栏的实际效果。
它是如何工作的...
使批量模式中的操作模式工作有三个关键元素:
-
创建上下文菜单以展开
-
定义
MultiChoiceModeListener以传递给setMultiChoiceModeListener() -
将
ListView的ChoiceMode设置为CHOICE_MODE_MULTIPLE_MODAL。
MultiChoiceModeListener的作用与在单视图上下文模式中使用的ActionMode回调相同,并且实际上实现了ActionMode.Callback。与ActionMode.Callback一样,当MultiChoiceModeListener调用onCreateActionMode()时,菜单会被展开。
默认情况下,上下文模式是通过在ListView中的项上长按来启动的。我们将更进一步,通过使用onItemClick()事件来检查项时启动上下文模式。如果我们不这样做,启动上下文模式的唯一方法将是长按,这可能会让许多用户不了解额外的功能。
更多内容...
如本章引言所述,您的活动不需要包含操作栏即可使用上下文操作栏。如果您确实有操作栏并且它是可见的,它将被上下文操作栏覆盖。如果您没有操作栏作为此菜谱的默认设置,布局将被重新绘制以包含上下文操作栏(当上下文操作栏关闭时再次重新绘制)。如果您希望操作栏可见,则可以更改活动的主题或更改基本类并手动设置ListView。
相关内容
- 有关
ListView的更多信息,请参阅第二章,布局
创建弹出菜单
弹出菜单类似于选择器下拉的视图。弹出菜单的目的是提供额外的选项以完成操作。一个常见的例子可能是电子邮件应用中的回复按钮。按下时,会显示几个回复选项,例如:回复、全部回复和转发。
这是菜谱中弹出菜单的示例:

如果有空间,Android 将在锚点视图下方显示菜单选项;否则,它将在视图上方显示。
小贴士
弹出菜单不是用来影响视图本身的。这是上下文菜单的目的。相反,请参阅启用视图的上下文操作模式菜谱中描述的浮动菜单/上下文模式。
在此菜谱中,我们将使用ImageButton作为锚点视图创建之前显示的弹出菜单。
准备工作
在 Android Studio 中创建一个新的项目并将其命名为PopupMenu。在提示添加活动时,使用默认的手机和平板电脑选项并选择空活动。像之前一样,创建一个菜单目录(res/menu)以存储菜单 XML。
如何实现...
我们首先创建一个 XML 菜单,以便在按钮按下时展开。在展开弹出菜单后,我们通过传递回调来调用setOnMenuItemClickListener()以处理菜单项选择。以下是步骤:
-
将以下字符串添加到
strings.xml中:<string name="menu_reply">Reply</string> <string name="menu_reply_all">Reply All</string> <string name="menu_forward">Forward</string> -
在
res/menu目录中创建一个名为menu_popup.xml的新文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <menu > <item android:id="@+id/menu_reply android:title="@string/menu_reply" /> <item android:id="@+id/menu_reply_all android:title="@string/menu_reply_all" /> <item android:id="@+id/menu_forward android:title="@string/menu_forward" /> </menu> -
在
activity_main.xml中创建一个ImageButton以提供弹出菜单的锚点视图。按照以下 XML 代码创建它:<ImageButton android:id="@+id/imageButtonReply" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:src="img/ic_menu_revert" android:onClick="showPopupMenu"/> -
打开
MainActivity.java并在类构造函数下方添加以下OnMenuItemClickListener:private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener = new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { // Handle menu selections switch (item.getItemId()) { case R.id.menu_reply Toast.makeText(MainActivity.this, "Reply", Toast.LENGTH_SHORT).show(); return true; case R.id.menu_reply_all Toast.makeText(MainActivity.this,"Reply All",Toast.LENGTH_SHORT).show(); return true; case R.id.menu_forward Toast.makeText(MainActivity.this, "Forward", Toast.LENGTH_SHORT).show(); return true; default: return false; } } }; -
最终的代码是处理按钮
onClick()事件,如下所示:public void showPopupMenu(View view) { PopupMenu popupMenu = new PopupMenu(MainActivity.this,view); popupMenu.inflate(R.menu.menu_popup); popupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener); popupMenu.show(); } -
在设备或模拟器上运行程序以查看弹出菜单。
它是如何工作的...
如果您已经阅读了之前的菜单菜谱,这可能会看起来非常熟悉。基本上,我们只是在ImageButton被按下时展开一个弹出菜单。我们设置一个菜单项监听器以响应菜单选择。
关键在于理解 Android 中可用的每个菜单选项,这样你就可以为特定场景选择正确的菜单类型。这将通过提供一致的用户体验和降低学习曲线来帮助你的应用程序。
第五章:探索 Fragment、AppWidget 和系统 UI
在本章中,我们将涵盖以下主题:
-
创建和使用 Fragment
-
在运行时添加和删除 Fragment
-
在 Fragment 之间传递数据
-
在主屏幕上创建快捷方式
-
创建主屏幕小工具
-
向 ActionBar 添加搜索功能
-
全屏显示你的应用
简介
在对第二章中的布局有牢固理解的基础上,布局,我们将深入探讨使用 Fragment 的 UI 开发。Fragment 是一种将 UI 分割成更小部分的方法,这些部分可以轻松重用。将 Fragment 视为迷你活动,它们有自己的类、布局和生命周期。您不必在一个 Activity Layout 中设计整个屏幕,可能还会在多个布局中重复功能,而是可以将屏幕分割成更小、更逻辑的部分,并将它们转换为 Fragment。根据需要,您的 Activity Layout 可以引用一个或多个 Fragment。前三个菜谱将深入探讨 Fragment。
在理解 Fragment 的基础上,我们准备扩展对 Widgets 的讨论。在第三章,视图、小工具和样式中,我们讨论了如何向您的应用添加小工具。现在,我们将探讨如何创建 App Widget,以便用户可以将他们的应用放在主屏幕上。
本章的最后几节将探索系统 UI 选项。我们有一个菜谱,使用 Android 的SearchManager API 向 ActionBar 添加搜索选项。最后一个菜谱展示了全屏模式以及改变系统 UI 的几种额外变体。
创建和使用 Fragment
Android 并非始终支持 Fragment。Android 的早期版本是为手机设计的,当时屏幕相对较小。直到 Android 开始在平板电脑上使用时,才需要将屏幕分割成更小的部分。Android 3.0 引入了Fragments类和 Fragment Manager。
随着新类一起到来的还有 Fragment 生命周期。Fragment 生命周期与在第一章中引入的活动生命周期相似,活动,因为大多数事件都与活动生命周期并行。
这里是主要回调的简要概述:
-
onAttach(): 当 Fragment 与 Activity 关联时被调用。 -
onCreate(): 当 Fragment 首次创建时被调用。 -
onCreateView(): 当 Fragment 即将首次显示时被调用。 -
onActivityCreated(): 当相关 Activity 被创建时被调用。 -
onStart(): 当 Fragment 将变为用户可见时被调用。 -
onResume(): 它在 Fragment 显示之前被调用。 -
onPause(): 当 Fragment 首次暂停时被调用。用户可能会返回到 Fragment,但这是你应该持久化任何用户数据的地方。 -
onStop(): 当 Fragment 不再对用户可见时被调用。 -
onDestroyView():用于允许最终的清理。 -
onDetach():当 Fragment 不再与 Activity 关联时被调用。
在我们的第一个练习中,我们将创建一个新的由标准 Fragment 类派生的 Fragment。但我们可以从几个其他的 Fragment 类中派生,包括:
-
DialogFragment:用于创建浮动对话框 -
ListFragment:它在 Fragment 中创建一个ListView,类似于ListActivity -
PreferenceFragment:它创建了一个偏好对象列表,通常用于设置页面
在这个菜谱中,我们将逐步演示如何创建一个由 Fragment 类派生的基本 Fragment 并将其包含在 Activity 布局中。
准备工作
在 Android Studio 中创建一个新的项目,命名为:CreateFragment。使用默认的 手机和平板 选项,并在提示活动类型时选择 空活动 选项。
如何操作...
在这个菜谱中,我们将创建一个新的 Fragment 类及其相应的布局文件。然后我们将 Fragment 添加到 Activity 布局中,以便在 Activity 启动时可见。以下是创建和显示新 Fragment 的步骤:
-
使用以下 XML 创建一个新的布局文件
fragment_one.xml:<RelativeLayout android:layout_height="match_parent" android:layout_width="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Fragment One" android:id="@+id/textView" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> </RelativeLayout> -
创建一个新的 Java 文件
FragmentOne,包含以下代码:public class FragmentOne extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_one, container, false); } } -
打开
main_activity.xml文件,将现有的<TextView>元素替换为以下<fragment>元素:<fragment android:name="com.packtpub.androidcookbook.createfragment.FragmentOne" android:id="@+id/fragment" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_centerHorizontal="true" tools:layout="@layout/fragment_one" /> -
在设备或模拟器上运行程序。
它是如何工作的...
我们首先创建一个新的类,与创建 Activity 时的做法相同。在这个菜谱中,我们只创建了一个覆盖 onCreateView() 方法来加载我们的 Fragment 布局的覆盖。但是,就像 Activity 事件一样,我们可以根据需要覆盖其他事件。一旦创建了新的 Fragment,我们就将其添加到 Activity 布局中。由于 Activity 类是在 Fragments 存在之前创建的,它们不支持 Fragments。如果我们使用纯框架类,我们希望使用 FragmentActivity。如果你使用了 Android Studio 新建项目向导,那么默认情况下 MainActivity 继承自 AppCompatActivity,它已经包含了 Fragment 的支持。
更多内容...
在这个菜谱中,我们只创建了一个简单的 Fragment 来教授 Fragment 的基础知识。但这是一个指出 Fragment 强大功能的好时机。如果我们正在创建多个 Fragment(通常我们是这样做的,因为这是使用 Fragment 的目的),在创建 Activity 布局时(如步骤 4 所示),我们可以使用 Android 资源文件夹创建不同的布局配置。纵向布局可能只有一个 Fragment,而横向布局可能有两个或更多。
在运行时添加和移除 Fragment
在布局中定义片段,就像我们在前面的菜谱中所做的那样,被称为静态片段,在运行时无法更改。我们不会使用<fragment>元素,而是创建一个容器来容纳片段,然后在 Activity 的onCreate()方法中动态创建片段。
FragmentManager提供了在运行时使用FragmentTransaction添加、删除和更改片段的 API。一个片段事务包括:
-
开始事务
-
执行一个或多个操作
-
提交事务
这个菜谱将通过在运行时添加和删除片段来演示FragmentManager。
准备工作
在 Android Studio 中创建一个新的项目,命名为:RuntimeFragments。使用默认的手机和平板选项,并在提示活动类型时选择空活动选项。
如何操作...
为了演示添加和删除片段,我们首先需要创建片段,我们将通过扩展Fragment类来实现。在创建了新的片段之后,我们需要修改主活动的布局以包含Fragment容器。从那里,我们只需添加处理片段事务的代码。以下是步骤:
-
创建一个名为
fragment_one.xml的新布局文件,并包含以下 XML:<RelativeLayout android:layout_height="match_parent" android:layout_width="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Fragment One" android:id="@+id/textView" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> </RelativeLayout> -
第二个布局文件
fragment_two.xml几乎与它完全相同,唯一的区别是文本:android:text="Fragment Two" -
创建一个名为
FragmentOne的新 Java 文件,并包含以下代码:public class FragmentOne extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_one, container, false); } }从以下库导入:
android.support.v4.app.Fragment -
创建第二个 Java 文件,命名为
FragmentTwo,并包含以下代码:public class FragmentTwo extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_two, container, false); } }从以下库导入:
android.support.v4.app.Fragment -
现在我们需要在主活动布局中添加一个容器和一个按钮。按照以下方式更改
main_activity.xml:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/buttonSwitch" android:layout_alignParentTop="true"> </FrameLayout> <Button android:id="@+id/buttonSwitch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Switch" android:layout_alignParentBottom="true" android:layout_centerInParent="true" android:onClick="switchFragment"/> </RelativeLayout> -
在创建了片段并将容器添加到布局中之后,我们现在可以编写操作片段的代码。打开
MainActivity.java文件,并在类构造函数下方添加以下代码:FragmentOne mFragmentOne; FragmentTwo mFragmentTwo; int showingFragment=0; -
在现有的
onCreate()方法中,在setContentView()下方添加以下代码:mFragmentOne = new FragmentOne(); mFragmentTwo = new FragmentTwo(); FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.frameLayout, mFragmentOne); fragmentTransaction.commit(); showingFragment=1;从以下库导入:
android.support.v4.app.FragmentManager android.support.v4.app.FragmentTransaction -
最后需要添加的代码处理了片段切换,由按钮调用:
public void switchFragment(View view) { FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); if (showingFragment==1) { fragmentTransaction.replace(R.id.frameLayout, mFragmentTwo); showingFragment = 2; } else { fragmentTransaction.replace(R.id.frameLayout, mFragmentOne); showingFragment=1; } fragmentTransaction.commit(); } -
在设备或模拟器上运行程序。
它是如何工作的...
这个菜谱的大部分步骤都涉及设置片段。一旦片段被声明,我们就在onCreate()方法中创建它们。尽管代码可以压缩成一行,但为了便于阅读和理解,这里以长形式展示。
首先,我们获取FragmentManager以便开始一个FragmentTransaction。一旦我们有了FragmentTransaction,我们就通过调用beginTransaction()开始事务。事务中可以发生多个操作,但这里我们只需要add()我们的初始片段。我们调用commit()方法来最终确定事务。
现在你已经了解了片段事务,以下是onCreate()方法的简洁版本:
getFragmentManager().beginTransaction().add(R.id.framLayout, mFragmentOne).commit();
switchFragment基本上执行相同的片段事务。我们不是调用add()方法,而是调用带有现有片段的replace()方法。我们通过showingFragment变量跟踪当前片段,以便我们知道要显示哪个片段。我们也不限于在两个片段之间切换。如果我们需要额外的片段,我们只需创建它们即可。
还有更多...
在第一章的在活动之间切换食谱中,我们讨论了返回栈。大多数用户都期望返回键可以向后移动通过“屏幕”,他们不知道或不在乎那些屏幕是活动还是片段。幸运的是,Android 通过在调用commit()之前添加对addToBackStack()的调用,使得将片段添加到返回栈变得非常容易。
小贴士
当一个片段在没有添加到返回栈的情况下被移除或替换时,它会被立即销毁。如果它被添加到返回栈中,它会被停止,并且如果用户返回到该片段,它会被重新启动,而不是重新创建。
在片段之间传递数据
经常会出现需要在片段之间传递信息的需求。电子邮件应用程序是一个经典的例子。通常,电子邮件列表在一个片段中,而在另一个片段中显示电子邮件详情(这通常被称为主/详细模式)。片段使得创建这种模式变得更容易,因为我们只需要为每个片段编写一次代码,然后我们就可以将它们包含在不同的布局中。我们可以轻松地在一个纵向布局中放置一个片段,当选择电子邮件时,可以用详细片段替换主片段。我们还可以创建一个双面板布局,其中列表和详细片段并排显示。无论哪种方式,当用户点击列表中的电子邮件时,电子邮件就会在详细面板中打开。这就是我们需要在两个片段之间进行通信的时候。
由于片段的主要目标之一是它们应该是完全自包含的,因此不建议片段之间进行直接通信,这有很好的理由。如果片段必须依赖于其他片段,那么当布局发生变化且只有一个片段可用时,你的代码很可能会出错。幸运的是,在这种情况下也不需要直接通信。所有片段通信都应该通过宿主活动进行。宿主活动负责管理片段,并且可以正确地路由消息。
现在的问题是:片段如何与活动通信?答案是使用一个interface。你可能已经熟悉接口,因为这是视图将事件回传给活动的方式。按钮点击是一个常见的例子。
在本食谱中,我们将创建两个片段来演示通过宿主活动从一个片段向另一个片段传递数据。我们还将利用之前食谱中学到的知识,包括两个不同的活动布局——一个用于纵向,一个用于横向。在纵向模式下,活动将根据需要交换片段。以下是应用程序首次在纵向模式下运行的截图:

这是点击国家名称时显示详细片段的屏幕:

在横向模式下,两个片段将并排显示,如横向截图所示:

由于主/详细模式通常涉及一个主列表,我们将利用 ListFragment(在 创建和使用片段 介绍中提到)。当列表中的项目被选中时,项目文本(在我们的例子中是国家名称)将通过宿主活动发送到详细片段。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:Fragmentcommunication。使用默认的 手机和平板 选项,并在提示 活动类型 时选择 空活动。
如何实现...
为了完全演示工作的片段,我们需要创建两个片段。第一个片段将继承自 ListFragment,因此它不需要布局。我们将更进一步,为我们的活动创建纵向和横向布局。在纵向模式下,我们将交换片段,在横向模式下,我们将并排显示两个片段。
注意
当输入此代码时,Android Studio 将提供两种不同的库导入选项。由于新项目向导自动引用了 AppCompat 库,我们需要使用支持库 API 而不是框架 API。尽管非常相似,以下代码使用支持片段 API。
下面是步骤,从第一个片段开始:
-
创建一个名为
MasterFragment的新 Java 类,并将其修改为继承ListFragment,如下所示:public class MasterFragment extends ListFragment从以下库导入:
android.support.v4.app.ListFragment -
在
MasterFragment类内部创建以下interface:public interface OnMasterSelectedListener { public void onItemSelected(String countryName); } -
使用以下代码设置接口回调监听器:
private OnMasterSelectedListener mOnMasterSelectedListener=null; public void setOnMasterSelectedListener(OnMasterSelectedListener listener) { mOnMasterSelectedListener=listener; } -
MasterFragment的最后一步是创建一个ListAdapter来填充ListView,我们在onViewCreated()方法中这样做。我们将使用setOnItemClickListener()在选择国家名称时调用我们的OnMasterSelectedListener接口,如下所示:public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"}; ListAdapter countryAdapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, countries); setListAdapter(countryAdapter); getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (mOnMasterSelectedListener != null) { mOnMasterSelectedListener.onItemSelected(((TextView) view).getText().toString()); } } }); } -
接下来我们需要创建
DetailFragment,从布局开始。创建一个名为:fragment_detail.xml的新布局文件,其 XML 如下所示:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textViewCountryName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> </RelativeLayout> -
创建一个名为
DetailFragment的新 Java 类,它继承自Fragment,如下所示:public class DetailFragment extends Fragment从以下库导入:
android.support.v4.app.Fragment -
将以下常量添加到类中:
public static String KEY_COUNTRY_NAME="KEY_COUNTRY_NAME"; -
如下重写
onCreateView():public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_detail, container, false); } -
编写
onViewCreated()如下:public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); Bundle bundle = getArguments(); if (bundle != null && bundle.containsKey(KEY_COUNTRY_NAME)) { showSelectedCountry(bundle.getString(KEY_COUNTRY_NAME)); } } -
对于这个 Fragment 的最后一步是在我们接收到选定的国家名称时更新 TextView。向类中添加以下方法:
public void showSelectedCountry(String countryName) { ((TextView)getView().findViewById(R.id.textViewCountryName)).setText(countryName); } -
现有的
activity_main.xml布局将处理纵向模式布局。删除现有的<TextView>并将其替换为以下<FrameLayout>:<FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent"/> -
在res文件夹中为横向布局创建一个新的目录:
res/layout-land。提示
如果您看不到新的
res/layout-land目录,请从Android view更改为Project view。 -
在
res/layout-land中创建一个新的activity_main.xml布局,如下所示:<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <FrameLayout android:id="@+id/frameLayoutMaster" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent"/> <FrameLayout android:id="@+id/frameLayoutDetail" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent"/> </LinearLayout> -
最后的步骤是将
MainActivity设置为处理 Fragment。打开MainActivity.java文件,并添加以下类变量以跟踪单/双面板:boolean dualPane; -
接下来,按照以下方式更改
onCreate():protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MasterFragment masterFragment=null; FrameLayout frameLayout = (FrameLayout)findViewById(R.id.frameLayout); if (frameLayout != null) { dualPane=false; FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); masterFragment=(MasterFragment)getSupportFragmentManager().findFragmentByTag("MASTER"); if (masterFragment == null) { masterFragment = new MasterFragment(); fragmentTransaction.add(R.id.frameLayout, masterFragment, "MASTER"); } DetailFragment detailFragment = (DetailFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutDetail); if (detailFragment != null) { fragmentTransaction.remove(detailFragment); } fragmentTransaction.commit(); } else { dualPane=true; FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); masterFragment=(MasterFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutMaster); if (masterFragment==null) { masterFragment = new MasterFragment(); fragmentTransaction.add(R.id.frameLayoutMaster, masterFragment); } DetailFragment detailFragment=(DetailFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutDetail); if (detailFragment==null) { detailFragment = new DetailFragment(); fragmentTransaction.add(R.id.frameLayoutDetail, detailFragment); } fragmentTransaction.commit(); } masterFragment.setOnMasterSelectedListener(new MasterFragment.OnMasterSelectedListener() { @Override public void onItemSelected(String countryName) { sendCountryName(countryName); } }); } -
需要添加的最后一段代码是
sendCountryName()方法,它处理将国家名称发送到DetailFragment:private void sendCountryName(String countryName) { DetailFragment detailFragment; if (dualPane) { //Two pane layout detailFragment = (DetailFragment)getSupportFragmentManager().findFragmentById(R.id.frameLayoutDetail); detailFragment.showSelectedCountry(countryName); } else { // Single pane layout detailFragment = new DetailFragment(); Bundle bundle = new Bundle(); bundle.putString(DetailFragment.KEY_COUNTRY_NAME, countryName); detailFragment.setArguments(bundle); FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); fragmentTransaction.replace(R.id.frameLayout, detailFragment); fragmentTransaction.addToBackStack(null); fragmentTransaction.commit(); } } -
在设备或模拟器上运行程序。
它是如何工作的...
我们首先创建MasterFragment。在我们使用的 Master/Detail 模式中,这通常代表一个列表,因此我们通过扩展ListFragment创建一个列表。ListFragment是ListActivity的 Fragment 等价物。除了扩展 Fragment 之外,它基本上是相同的。
如食谱介绍中所述,我们不应尝试直接与其他 Fragment 通信。
为了提供一个通信列表项选择的方法,我们暴露了接口:OnMasterSelectedListener。每次在列表中选择一个项目时,我们都调用onItemSelected()。
在 Fragment 之间传递数据的大部分工作是在宿主活动中完成的,但最终,接收数据的 Fragment 需要一种接收数据的方式。DetailFragment通过两种方式支持这一点:
-
在创建时通过参数包传递国家名称。
-
活动可以直接调用的公共方法。
当活动创建 Fragment 时,它也会创建一个bundle来保存我们想要发送的数据。在这里,我们使用在步骤 7 中定义的KEY_COUNTRY_NAME添加国家名称。我们在onViewCreated()中使用getArguments()检索这个 bundle。如果键在 bundle 中找到,它将通过showSelectedCountry()方法提取并显示。这是活动如果 Fragment 已经可见(在两面板布局中)将直接调用的相同方法。
这个食谱的大部分工作都在活动中。我们创建了两个布局:一个用于纵向,一个用于横向。Android 将使用在步骤 12中创建的res/layout-land目录选择横向布局。这两个布局都使用一个<FrameLayout>占位符,类似于之前的练习。我们在onCreate()和sendCountryName()中管理 Fragment。
在onCreate()中,我们通过检查当前布局是否包含frameLayout视图来设置dualPane标志。如果找到frameLayout(它不会为 null),那么我们只有一个面板,因为frameLayout ID 只在纵向布局中。如果没有找到frameLayout,那么我们有两个<FrameLayout>元素:一个用于MasterFragment,另一个用于DetailFragment。
在onCreate()中我们做的最后一件事是通过创建匿名回调来设置MasterFragment监听器,该回调将国家名称传递给sendCountryName()。
sendCountryName()是数据实际上传递给DetailFragment的地方。如果我们处于纵向(或单面板)模式,我们需要创建一个DetailFragment并替换现有的MasterFragment。这是我们在其中创建包含国家名称的 bundle 并调用setArguments()的地方。注意我们在提交事务之前调用addToBackStack()?这允许返回键将用户带回列表(MasterFragment)。如果我们处于横向模式,DetailFragment已经可见,所以我们直接调用showSelectedCountry()公共方法。
更多内容...
在MasterFragment中,在发送onItemSelected()事件之前,我们使用以下代码检查监听器是否为 null:
if (mOnMasterSelectedListener != null)
虽然设置回调以接收事件是活动的职责,但我们不希望在没有监听器的情况下代码崩溃。另一种方法是在 Fragment 的onAttach()回调中验证活动是否扩展了我们的接口。
参见
-
有关 ListView 的更多信息,请参阅第二章中的使用 ListView、GridView 和适配器,布局。
-
有关资源目录的更多信息,请参阅第三章中的根据 Android 版本选择主题,视图、小部件和样式。
在主屏幕上创建快捷方式
这个菜谱解释了如何在用户的 Home 屏幕上创建链接或创建应用的快捷方式。为了不过于侵扰,通常最好将其作为用户可以启动的选项,例如在设置中。
这里是一张显示我们在主屏幕上的快捷方式的截图:

如您所见,这只是一个快捷方式,但我们将探索在下一个菜谱中创建主屏幕(AppWidget)。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:HomescreenShortcut。使用默认的电话和平板电脑选项,并在提示活动类型时选择空活动选项。
如何做...
第一步是添加适当的权限。以下是步骤:
-
打开
AndroidManifest文件并添加以下权限:<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" /> -
接下来,打开
activity_main.xml并将现有的 TextView 替换为以下按钮:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Create Shortcut" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="createShortcut"/> -
将以下方法添加到
ActivityMain.java:public void createShortcut(View view) { Intent shortcutIntent = new Intent(this, MainActivity.class); shortcutIntent.setAction(Intent.ACTION_MAIN); Intent intent = new Intent(); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.app_name)); intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Intent.ShortcutIconResource.fromContext(this, R.mipmap.ic_launcher)); intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT"); sendBroadcast(intent); } -
在设备或模拟器上运行程序。注意,每次你按下按钮,应用都会在主屏幕上创建一个快捷方式。
它是如何工作的...
一旦你设置了适当的权限,这便是一个相当直接的任务。当按钮被点击时,代码会创建一个新的意图,称为:shortcutIntent。这是当在主屏幕上按下图标时将被调用的意图。接下来创建的意图 installIntent 负责实际创建快捷方式。
还有更多...
如果你还想删除快捷方式,你需要以下权限:
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />
而不是使用 INSTALL_SHORTCUT 动作,你将设置以下动作:
com.android.launcher.action.UNINSTALL_SHORTCUT
创建主屏幕小部件
在我们深入研究创建 App Widget 的代码之前,让我们先了解基础知识。有三个必需组件和一个可选组件:
-
AppWidgetProviderInfo文件:它是一个稍后描述的 XML 资源 -
AppWidgetProvider类:这是一个 Java 类 -
视图布局文件:它是一个带有一些限制的标准布局 XML 文件
-
App Widget 配置 Activity(可选):当放置小部件以设置配置选项时启动此 Activity
AppWidgetProvider 也必须在 AndroidManifest 文件中声明。由于 AppWidgetProvider 是基于广播接收器的辅助类,它在 Manifest 中使用 <receiver> 元素声明。以下是一个示例 Manifest 条目:
<receiver android:name="AppWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info" />
</receiver>
元数据指向 AppWidgetProviderInfo 文件,该文件位于 res/xml 目录中。以下是一个示例 AppWidgetProviderInfo.xml 文件:
<appwidget-provider
android:minWidth="40dp"
android:minHeight="40dp"
android:updatePeriodMillis="1800000"
android:previewImage="@drawable/preview_image"
android:initialLayout="@layout/appwidget"
android:configure="com.packtpub.androidcookbook.AppWidgetConfiguration"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>
这里是可用属性的简要概述:
-
minWidth:放置在主屏幕上的默认宽度 -
minHeight:放置在主屏幕上的默认高度 -
updatePeriodMillis:它是onUpdate()轮询间隔的一部分(以毫秒为单位) -
initialLayout:AppWidget 布局 -
previewImage(可选):浏览 App Widget 时显示的图像 -
configure(可选):用于配置设置的 Activity -
resizeMode(可选):标志指示调整大小选项 —horizontal、vertical、none -
minResizeWidth(可选):调整大小时允许的最小宽度 -
minResizeHeight(可选):调整大小时允许的最小高度 -
widgetCategory(可选):Android 5+ 仅支持主屏幕小部件
AppWidgetProvider 类扩展了 BroadcastReceiver 类,这就是为什么在 Manifest 中声明 AppWidget 时使用 <receiver> 的原因。由于它是 BroadcastReceiver,该类仍然接收操作系统广播事件,但辅助类将这些事件过滤到适用于 App Widget 的事件。AppWidgetProvider 类公开了以下方法:
-
onUpdate():它在首次创建时和指定的时间间隔被调用。 -
onAppWidgetOptionsChanged():它在首次创建和任何时间大小改变时被调用。 -
onDeleted():任何时间删除小部件时都会被调用。 -
onEnabled(): 当小部件首次放置时调用(添加第二个和后续小部件时不会调用)。 -
onDisabled(): 当最后一个小部件被移除时调用。 -
onReceive(): 在接收到每个事件时调用,包括前面的事件。通常不重写,因为默认实现只发送适用的事件。
最后必需的组件是布局。远程视图只支持可用布局的子集。由于小部件是远程视图,因此只支持以下布局:
-
FrameLayout -
LinearLayout -
RelativeLayout -
GridLayout
以及以下小部件:
-
AnalogClock -
Button -
Chronometer -
ImageButton -
ImageView -
ProgressBar -
TextView -
ViewFlipper -
ListView -
GridView -
StackView -
AdapterViewFlipper
在了解了小部件的基本知识后,现在是时候开始编码了。我们的示例将涵盖基础知识,这样您就可以根据需要扩展功能。这个示例使用了一个带有时钟的视图,按下时将打开我们的活动。
此截图显示了添加到主屏幕时小部件在部件列表中的外观:

注意
小部件列表的外观因启动器而异。
这是一张截图,显示了添加到主屏幕后的小部件外观:

准备工作
在 Android Studio 中创建一个新的项目并命名为:AppWidget。使用默认的手机和平板选项,并在提示活动类型时选择空活动选项。
如何操作...
我们首先创建小部件布局,该布局位于标准布局资源目录中。然后我们将创建 xml 资源目录以存储AppWidgetProviderInfo文件。我们将添加一个新的 Java 类并扩展AppWidgetProvider,该类处理小部件的onUpdate()调用。创建接收器后,我们就可以将其添加到 AndroidManifest 中。
这里是详细步骤:
-
在
res/layout中创建一个名为widget.xml的新文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <AnalogClock android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/analogClock" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> </RelativeLayout> -
在资源目录中创建一个名为
xml的新目录。最终结果将是:res/xml。 -
在
res/xml中创建一个名为appwidget_info.xml的新文件,使用以下 xml:<appwidget-provider android:minWidth="40dp" android:minHeight="40dp" android:updatePeriodMillis="0" android:initialLayout="@layout/widget" android:resizeMode="none" android:widgetCategory="home_screen"> </appwidget-provider>提示
如果您看不到新的 xml 目录,请在项目面板的下拉菜单中从Android视图切换到项目视图。
-
创建一个名为
HomescreenWidgetProvider的新 Java 类,并扩展AppWidgetProvider。 -
将以下
onUpdate()方法添加到HomescreenWidgetProvider类中:public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { super.onUpdate(context, appWidgetManager, appWidgetIds); for (int count=0; count<appWidgetIds.length; count++) { RemoteViews appWidgetLayout = new RemoteViews(context.getPackageName(), R.layout.widget); Intent intent = new Intent(context, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); appWidgetLayout.setOnClickPendingIntent(R.id.analogClock, pendingIntent); appWidgetManager.updateAppWidget(appWidgetIds[count], appWidgetLayout); } } -
使用以下 XML 声明在
<application>元素内将HomescreenWidgetProvider添加到AndroidManifest:<receiver android:name=".HomescreenWidgetProvider" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_info" /> </receiver> -
在设备或模拟器上运行程序。在首次运行应用程序后,小部件将可供添加到主屏幕。
如何工作...
我们的第一步是为小部件创建布局文件。这是一个标准的布局资源,其限制基于 App Widget 是一个远程视图,如配方介绍中所述。尽管我们的示例使用了一个模拟时钟小部件,但这是您根据应用程序需求扩展功能的地方。
xml 资源目录用于存储AppWidgetProviderInfo,它定义了默认的小部件设置。配置设置决定了小部件在最初浏览可用小部件时的显示方式。我们为这个配方使用了非常基本的设置,但它们可以很容易地扩展以包括其他功能,例如显示一个功能小部件的预览图像和尺寸选项。updatePeriodMillis属性设置了更新频率。由于更新会唤醒设备,这需要在最新的数据和电池寿命之间做出权衡。(这就是可选的设置活动有用的地方,可以让用户决定。)
AppWidgetProvider类是我们处理由updatePeriodMillis轮询触发的onUpdate()事件的地方。我们的示例不需要任何更新,所以我们把轮询设置为零。当最初放置小部件时,更新仍然会被调用。onUpdate()是我们设置挂起意图以在时钟被按下时打开我们的应用的地方。
由于onUpdate()方法可能是 AppWidgets 中最复杂的一部分,我们将对此进行详细解释。首先,值得注意的是,对于由该提供程序创建的所有小部件,onUpdate()方法在每个轮询间隔内只会发生一次。(在第一个之后创建的小部件将处于第一个小部件的周期中。)这解释了for循环,因为我们需要它来遍历所有现有的小部件。这就是我们创建一个挂起意图,在时钟被按下时调用我们的应用的地方。如前所述,AppWidget 是一个远程视图。因此,为了获取布局,我们使用我们的完全限定包名和布局 ID 调用RemoteViews()。一旦我们有了布局,我们就可以使用setOnClickPendingIntent()将挂起意图附加到时钟视图。我们调用名为updateAppWidget()的AppWidgetManager来启动我们所做的更改。
使所有这些工作完成的最后一步是在 AndroidManifest 中声明小部件。我们使用<intent-filter>标识我们想要处理的操作。大多数 App Widgets 可能希望处理更新事件,就像我们的那样。声明中需要注意的另一项是这一行:
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/appwidget_info" />
这告诉系统在哪里可以找到我们的配置文件。
还有更多...
添加 App Widget 配置活动可以使您的部件更加灵活。您不仅可以选择轮询选项,还可以提供不同的布局、点击行为等。用户通常非常欣赏灵活的 App Widgets。
添加配置活动需要几个额外的步骤。该活动需要像往常一样在 Manifest 中声明,但需要包含 APPWIDGET_CONFIGURE 动作,如下例所示:
<activity android:name=".AppWidgetConfigureActivity">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
该活动还需要在 AppWidgetProviderInfo 文件中使用配置属性进行指定,如下例所示:
android:configure="com.packtpub.androidcookbook.appwidget.AppWidgetConfigureActivity"
configure 属性需要完全限定的包名,因为此活动将从应用程序外部调用。
小贴士
记住,当使用配置活动时,onUpdate() 方法不会被调用。配置活动负责处理任何所需的初始设置。
参见
- 对于 App Widget 设计指南,请访问 Google 的页面:
developer.android.com/design/patterns/widgets.html
将搜索添加到操作栏
除了操作栏外,Android 3.0 还引入了 SearchView 小部件,可以在创建菜单时将其作为菜单项包含。现在这是提供一致用户体验的推荐 UI 模式。
以下截图显示了搜索图标在操作栏中的初始外观:

此截图显示了按下时搜索选项的展开方式:

如果您想向您的应用程序添加搜索功能,本食谱将指导您设置用户界面并正确配置搜索管理器 API。
准备工作
在 Android Studio 中创建一个新的项目,命名为:SearchView。使用默认的 Phone & Tablet 选项,并在提示活动类型时选择 Empty Activity。
如何操作...
要设置搜索 UI 模式,我们需要创建搜索菜单项和一个名为 searchable 的资源。然后我们将创建第二个活动来接收搜索查询。然后我们将在 AndroidManifest 文件中将所有这些连接起来。要开始,请打开 res/values 中的 strings.xml 文件并按照以下步骤操作:
-
添加以下字符串资源:
<string name="search_title">Search</string> <string name="search_hint">Enter text to search</string> -
创建菜单目录:
res/menu。 -
在
res/menu中创建一个新的菜单资源menu_options.xml,使用以下 xml:<?xml version="1.0" encoding="utf-8"?> <menu > <item android:id="@+id/menu_search" android:title="@string/search_title" android:icon="@android:drawable/ic_menu_search" app:showAsAction="collapseActionView|ifRoom" app:actionViewClass="android.support.v7.widget.SearchView" /> </menu> -
重写
onCreateOptionsMenu()来填充菜单并设置搜索管理器如下:public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_options, menu); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.menu_search)); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); return true; } -
创建一个新的 xml 资源目录:
res/xml。 -
在
res/xml中创建一个新的文件searchable.xml,使用以下 xml:<?xml version="1.0" encoding="utf-8"?> <searchable android:label="@string/app_name" android:hint="@string/search_hint" /> -
使用以下 xml 创建一个新的布局
activity_search_result.xml:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:id="@+id/textViewSearchResult" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> </RelativeLayout> -
创建一个新的活动,命名为
SearchResultActivity。 -
向类中添加以下变量:
TextView mTextViewSearchResult; -
将
onCreate()改为加载我们的布局,设置 TextView 并检查QUERY动作:protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search_result); mTextViewSearchResult = (TextView)findViewById(R.id.textViewSearchResult); if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) { handleSearch(getIntent().getStringExtra(SearchManager.QUERY)); } -
添加以下方法来处理搜索:
private void handleSearch(String searchQuery) { mTextViewSearchResult.setText(searchQuery); } -
界面和代码现在已完成,我们只需在
AndroidManifest中正确连接一切。以下是包含两个活动的完整 Manifest:<?xml version="1.0" encoding="utf-8"?> <manifest package="com.packtpub.androidcookbook.searchview" > <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" > <meta-data android:name="android.app.default_searchable" android:value=".SearchResultActivity" /> <activity android:name=".MainActivity" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".SearchResultActivity" > <intent-filter> <action android:name="android.intent.action.SEARCH" /> </intent-filter> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable" /> </activity> </application> </manifest> -
在设备或模拟器上运行应用程序。输入一个搜索查询并点击搜索按钮(或按回车键)。
SearchResultActivity将显示并显示输入的搜索查询。
它是如何工作的...
由于新项目向导使用AppCompat库,我们的示例使用支持库 API。使用支持库提供了最大的设备兼容性,因为它允许在较旧的 Android OS 版本上使用现代功能(如操作栏)。这有时会带来额外的挑战,因为官方文档通常关注框架 API。尽管支持库通常紧跟框架 API,但它们并不总是可以互换。搜索 UI 模式就是这样一种情况,因此值得特别注意之前概述的步骤。
我们首先为searchable创建字符串资源,正如第 6 步所声明的。
在第 3 步中,我们创建菜单资源,就像我们多次做的那样。一个不同之处在于我们使用app命名空间来为showAsAction和actionViewClass属性。Android OS 的早期版本不包括它们在 Android 命名空间中的这些属性。这可以作为将新功能引入较旧版本的 Android OS 的一种方式。
在第 4 步中,我们设置了SearchManager,再次使用支持库 API。
第 6 步是我们定义searchable的地方,这是一个由SearchManager使用的 xml 资源。唯一必需的属性是label,但推荐使用hint,这样用户就会知道应该在字段中输入什么。
小贴士
android:label必须与应用程序名称或活动名称匹配,并且必须使用字符串资源(因为它不适用于硬编码的字符串)。
第 7 步至第 11 步是针对SearchResultActivity的。调用第二个活动不是SearchManager的要求,但通常这样做是为了提供一个活动来处理应用程序中启动的所有搜索。
如果你现在运行应用程序,你会看到搜索图标,但什么都不会工作。第 12 步是我们将所有内容组合到AndroidManifest文件中的地方。首先要注意的是以下内容:
<meta-data
android:name="android.app.default_searchable"
android:value=".SearchResultActivity" />
注意这位于application元素中,而不是任一<activity>元素中。
我们在SearchResultActivity <meta-data>元素中指定可搜索的资源:
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />
我们还需要像这里一样设置SearchResultActivity的 intent 过滤器:
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
当用户启动搜索时,SearchManager会广播SEARCH意图。这个声明将意图指向SearchResultActivity活动。一旦搜索被触发,查询文本将通过SEARCH意图发送到SearchResultActivity。我们在onCreate()中检查SEARCH意图,并使用以下代码提取查询字符串:
if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
handleSearch(getIntent().getStringExtra(SearchManager.QUERY));
}
您现在已完全实现了搜索 UI 模式。随着 UI 模式的完成,您如何处理搜索将具体取决于您的应用程序需求。根据您的应用程序,您可能需要搜索本地数据库或可能是一个网络服务。
相关内容
要将搜索扩展到互联网,请参阅第十二章 “Internet 查询”,电话、网络和互联网。
展示应用全屏
Android 4.4 (API 19) 引入了一个名为沉浸模式的 UI 功能。与之前的全屏标志不同,在沉浸模式下,您的应用会接收到所有触摸事件。这种模式非常适合某些活动,例如阅读书籍和新闻、全屏绘图、游戏或观看视频。全屏有几种不同的方法,每种方法都有最佳的使用场景:
-
阅读书籍/文章等:带有轻松访问系统 UI 的沉浸模式
-
游戏/绘图应用:全屏使用沉浸模式但最小化系统 UI
-
观看视频:全屏和正常系统 UI
两种模式之间的关键区别在于系统 UI 的响应方式。在前两种场景中,您的应用期望用户交互,因此系统 UI 被隐藏,以便更容易使用(例如,在玩游戏时不会误按返回按钮)。在使用带有正常系统 UI 的全屏模式,如观看视频时,您不会期望用户使用屏幕,因此当用户这样做时,系统 UI 应该正常响应。在所有模式下,用户可以通过在隐藏的系统栏上向内滑动来恢复系统 UI。
由于观看视频不需要新的 沉浸模式,全屏模式可以使用两个标志:SYSTEM_UI_FLAG_FULLSCREEN 和 SYSTEM_UI_FLAG_HIDE_NAVIGATION 实现,这两个标志自 Android 4.0 (API 14) 以来可用。
我们的配方将演示如何设置沉浸模式。我们还将添加通过屏幕点击切换系统 UI 的功能。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:ImmersiveMode。使用默认的 手机和平板 选项,并在提示 活动类型 时选择 空活动。在选择 最小 API 级别 时,选择 API 19 或更高版本。
如何做到这一点...
我们将创建两个处理系统 UI 可见性的函数,然后创建一个手势监听器来检测屏幕点击。这个配方的所有步骤都是在 MainActivity.java 中添加代码,所以请打开文件,让我们开始:
-
添加以下方法以隐藏系统 UI:
private void hideSystemUi() { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); } -
添加以下方法以显示系统 UI:
private void showSystemUI() { getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } -
添加以下类变量:
private GestureDetectorCompat mGestureDetector; -
在类级别,在之前的类变量下方添加以下
GestureListener类:private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDown(MotionEvent event) { return true; } @Override public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { return true; } @Override public boolean onSingleTapUp(MotionEvent e) { if (getSupportActionBar()!= null && getSupportActionBar().isShowing()) { hideSystemUi(); } else { showSystemUI(); } return true; } } -
用以下代码覆盖
onTouchEvent()回调:public boolean onTouchEvent(MotionEvent event){ mGestureDetector.onTouchEvent(event); return super.onTouchEvent(event); } -
将以下代码添加到
onCreate()方法中,以设置GestureListener并隐藏系统 UI:mGestureDetector = new GestureDetectorCompat(this, new GestureListener()); hideSystemUi(); -
在设备或模拟器上运行应用程序。向内滑动隐藏的系统栏将显示系统界面。轻触屏幕将切换系统界面。
它是如何工作的...
我们通过在应用程序窗口上使用setSystemUiVisibility()来创建showSystemUI()和hideSystemUI()方法。我们设置的(和未设置的)标志控制着什么可见和什么隐藏。当我们设置可见性而不使用SYSTEM_UI_FLAG_IMMERSIVE标志时,实际上我们禁用了沉浸模式。
如果我们只想隐藏系统界面,我们只需在onCreate()中添加hideSystemUI()方法,任务就完成了。问题是它不会保持隐藏。一旦用户退出沉浸模式,它就会保持在常规显示模式。这就是我们创建GestureListener的原因。(我们将在第八章中再次讨论手势,使用触摸屏和传感器。)因为我们只想对onSingleTapUp()手势做出响应,所以我们没有实现所有手势范围。当检测到onSingleTapUp时,我们切换系统界面。
还有更多...
让我们看看可以执行的一些其他重要任务:
粘性沉浸
如果我们想要系统界面自动隐藏,还有一个选项可以使用。我们不是使用SYSTEM_UI_FLAG_IMMERSIVE来隐藏 UI,而是可以使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY。
调暗系统界面
如果你只需要减少导航栏的可见性,还有SYSTEM_UI_FLAG_LOW_PROFILE来调暗 UI。
使用与沉浸模式标志相同的setSystemUiVisibility()调用此标志:
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
使用 0 调用setSystemUiVisibility()以清除所有标志:
getWindow().getDecorView().setSystemUiVisibility(0);
将操作栏设置为叠加层
如果你只需要隐藏或显示操作栏,请使用以下方法:
getActionBar().hide();
getActionBar().show();
这种方法的一个问题是,每次调用任一方法时,系统都会重新调整布局的大小。相反,你可能想考虑使用主题选项来使系统界面表现得像一个叠加层。要启用叠加模式,请将以下内容添加到主题中:
<item name="android:windowActionBarOverlay">true</item>
透明系统栏
这两个主题启用了透明设置:
Theme.Holo.NoActionBar.TranslucentDecor
Theme.Holo.Light.NoActionBar.TranslucentDecor
如果你正在创建自己的主题,请使用以下主题设置:
<item name="android:windowTranslucentNavigation">true</item>
<item name="android:windowTranslucentStatus">true</item>
参见
第八章中关于识别手势的配方,使用触摸屏和传感器。
第六章. 与数据一起工作
在本章中,我们将涵盖以下主题:
-
存储简单数据
-
在内部存储中读写文本文件
-
在外部存储中读写文本文件
-
在项目中包含资源文件
-
创建和使用 SQLite 数据库
-
使用 Loader 在后台访问数据
简介
几乎任何大小应用都需要保存某种类型的数据,Android 提供了许多选项。从保存简单值到使用 SQLite 创建完整数据库,存储选项包括以下内容:
-
共享偏好设置:简单的名称/值对
-
内部存储:私有存储中的数据文件
-
外部存储:私有或公共存储中的数据文件
-
SQLite 数据库:私有数据可以通过内容提供者公开数据
-
云存储:私有服务器或服务提供商
使用内部和外部存储有优点和权衡。我们将列出一些差异,以帮助您决定是否使用内部或外部存储:
-
内部存储:
-
与外部存储不同,内部存储始终可用,但通常有更少的空闲空间
-
文件对用户不可访问(除非设备具有 root 权限)
-
当您的应用被卸载时(或在应用管理器的清除缓存/清理文件选项中)会自动删除文件
-
-
外部存储:
-
设备可能没有外部存储,或者可能无法访问(例如,当它连接到计算机时)
-
文件对用户(和其他应用)是可访问的,无需 root 权限
-
当您的应用被卸载时,文件不会被删除(除非您使用
getExternalFilesDir()获取应用特定的公共存储)
-
在本章中,我们将演示如何使用共享偏好设置、内部和外部存储以及 SQLite 数据库。有关云存储,请参阅第十二章电话、网络和互联网中的互联网食谱,以及第十五章后端即服务选项中的在线服务提供商。
存储简单数据
存储简单数据是一个常见需求,Android 通过 Preferences API 使其变得简单。这不仅限于用户偏好设置;您可以使用名称/值对存储任何原始数据类型。
我们将演示如何从EditText保存一个名称,并在应用启动时显示它。以下截图显示了第一次启动时没有保存名称的应用外观,以及保存名称后的启动画面:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:Preferences。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
如何操作...
我们将使用现有的TextView来显示欢迎回来消息,并创建一个新的EditText按钮来保存名称。首先打开activity_main.xml:
-
替换现有的 TextView 并添加以下新视图:
<TextView android:id="@+id/textView" android:text="Hello World!" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <EditText android:id="@+id/editTextName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:hint="Enter your name" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" android:layout_centerHorizontal="true" android:layout_below="@id/editTextName" android:onClick="saveName"/> -
打开
ActivityMain.java并添加以下全局声明:private final String NAME="NAME"; private EditText mEditTextName; -
在
onCreate()中添加以下代码以保存对EditText的引用并加载任何已保存的名称:TextView textView = (TextView)findViewById(R.id.textView); SharedPreferences sharedPreferences = getPreferences(MODE_PRIVATE); String name = sharedPreferences.getString(NAME,null); if (name==null) { textView.setText("Hello"); } else { textView.setText("Welcome back " + name + "!"); } mEditTextName = (EditText)findViewById(R.id.editTextName); -
添加以下
saveName()方法:public void saveName(View view) { SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); editor.putString(NAME, mEditTextName.getText().toString()); editor.commit(); } -
在设备或模拟器上运行程序。由于我们正在演示持久化数据,它会在
onCreate()方法中加载名称,因此保存一个名称并重新启动程序以查看它是否加载。
它是如何工作的...
要加载名称,我们首先获取对 SharedPreference 的引用,这样我们就可以调用 getString() 方法。我们传入我们的名称/值对的键以及如果找不到键则返回的默认值。
要保存首选项,我们首先需要获取对首选项编辑器的引用。我们使用 putString() 并跟随 commit()。如果没有 commit(),则更改不会被保存。
还有更多...
我们的示例将所有首选项存储在单个文件中。我们也可以使用 getSharedPreferences() 并传入名称来在不同的文件中存储首选项。如果想要为多个用户创建单独的配置文件,可以使用此选项。
读取和写入内部存储的文本文件
当简单的键值对不足以满足需求时,Android 也支持常规的文件操作,包括处理文本和二进制数据。
以下示例演示了如何读取和写入内部或私有存储的文件。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:InternalStorageFile。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
为了演示读取和写入文本,我们需要一个包含 EditText 和两个按钮的布局。首先打开 main_activity.xml 并按照以下步骤操作:
-
替换现有的
<TextView>元素,并添加以下视图:<EditText android:id="@+id/editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:inputType="textMultiLine" android:ems="10" android:layout_above="@+id/buttonRead" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Read" android:id="@+id/buttonRead" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="readFile"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Write" android:id="@+id/buttonWrite" android:layout_below="@+id/buttonRead" android:layout_centerHorizontal="true" android:onClick="writeFile"/> -
现在打开
ActivityMain.java并添加以下全局变量:private final String FILENAME="testfile.txt"; EditText mEditText; -
在
onCreate()方法中setContentView ()之后添加以下内容:mEditText = (EditText)findViewById(R.id.editText); -
添加以下
writeFile()方法:public void writeFile(View view) { try { FileOutputStream fileOutputStream = openFileOutput(FILENAME, Context.MODE_PRIVATE); fileOutputStream.write(mEditText.getText().toString().getBytes()); fileOutputStream.close(); } catch (java.io.IOException e) { e.printStackTrace(); } } -
现在添加
readFile()方法:public void readFile(View view) { StringBuilder stringBuilder = new StringBuilder(); try { InputStream inputStream = openFileInput(FILENAME); if ( inputStream != null ) { InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String newLine = null; while ((newLine = bufferedReader.readLine()) != null ) { stringBuilder.append(newLine+"\n"); } inputStream.close(); } } catch (java.io.IOException e) { e.printStackTrace(); } mEditText.setText(stringBuilder); } -
在设备或模拟器上运行程序。
它是如何工作的...
我们使用 InputStream 和 FileOutputStream 类分别进行读取和写入。写入文件就像从 EditText 获取文本并调用 write() 方法一样简单。
读取内容稍微复杂一些。我们可以使用 FileInputStream 类进行读取,但在处理文本时,辅助类会使操作更简单。在我们的示例中,我们使用 openFileInput() 打开文件,它返回一个 InputStream 对象。然后我们使用 InputStream 获取一个 BufferedReader,它提供了 ReadLine() 方法。我们遍历文件中的每一行并将其追加到我们的 StringBuilder 中。当我们完成文件读取后,我们将文本分配给 EditText。
小贴士
我们之前创建的文件位于应用程序的私有数据文件夹中。要查看文件内容,您可以使用 Android 设备监控器将文件拉到您的计算机上。完整文件路径是:/data/data/com.packtpub.androidcookbook.internalstoragetile/files/testfile.txt。
以下截图显示了通过Android 设备监控器查看文件时的样子:

注意
您需要一个具有 root 访问权限的设备来查看之前显示的私有文件夹。
还有更多...
让我们看看一些可能有助于了解的额外信息。
缓存文件
如果您只需要临时存储数据,您也可以使用缓存文件夹。以下方法返回缓存文件夹作为File对象(下一个菜谱将演示如何处理File对象):
getCacheDir()
缓存文件夹的主要好处是,当系统存储空间不足时,可以清除缓存。(用户也可以在设置中的应用程序管理中清除缓存文件夹。)
例如,如果您的应用程序下载新闻文章,您可以将这些文章存储在缓存中。当您的应用程序启动时,您可以显示已下载的新闻。这些文件不是使您的应用程序工作所必需的。如果系统资源不足,可以清除缓存而不会对您的应用程序产生不利影响。(即使系统可能会清除缓存,但您的应用程序删除旧文件仍然是一个好主意。)
相关内容
- 下一个菜谱,读取和写入外部存储的文本文件。
读取和写入外部存储的文本文件
将文件读取和写入外部存储的过程基本上与使用内部存储相同。区别在于获取存储位置的引用。此外,如简介中所述,外部存储可能不可用,因此在尝试访问之前最好检查其可用性。
本菜谱将读取和写入文本文件,就像我们在前一个菜谱中所做的那样。我们还将演示在访问它之前如何检查外部存储状态。
准备就绪
在 Android Studio 中创建一个新的项目,并将其命名为:ExternalStorageFile。使用默认的手机和平板选项,并在提示活动类型时选择空活动。我们将使用与前一个菜谱相同的布局,所以如果您已经输入了它,可以直接复制粘贴。否则,使用前一个菜谱中的步骤 1 的布局,读取和写入内部存储的文本文件。
如何操作...
如前文在准备就绪部分所述,我们将使用前一个菜谱中的布局。布局文件完成后,第一步将是添加访问外部存储写入权限。以下是步骤:
-
打开 AndroidManifest.xml 并添加以下权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
接下来,打开
ActivityMain.java并添加以下全局变量:private final String FILENAME="testfile.txt"; EditText mEditText; -
在
onCreate()方法中,在setContentView()之后添加以下内容:mEditText = (EditText)findViewById(R.id.editText); -
添加以下两个方法来检查存储状态:
public boolean isExternalStorageWritable() { if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { return true; } return false; } public boolean isExternalStorageReadable() { if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment.getExternalStorageState())) { return true; } return false; } -
添加以下
writeFile()方法:public void writeFile(View view) { if (isExternalStorageWritable()) { try { File textFile = new File(Environment.getExternalStorageDirectory(), FILENAME); FileOutputStream fileOutputStream = new FileOutputStream(textFile); fileOutputStream.write(mEditText.getText().toString().getBytes()); fileOutputStream.close(); } catch (java.io.IOException e) { e.printStackTrace(); Toast.makeText(this, "Error writing file", Toast.LENGTH_LONG).show(); } } else { Toast.makeText(this, "Cannot write to External Storage", Toast.LENGTH_LONG).show(); } } -
添加以下
readFile()方法:public void readFile(View view) { if (isExternalStorageReadable()) { StringBuilder stringBuilder = new StringBuilder(); try { File textFile = new File(Environment.getExternalStorageDirectory(), FILENAME); FileInputStream fileInputStream = new FileInputStream(textFile); if (fileInputStream != null ) { InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String newLine = null; while ( (newLine = bufferedReader.readLine()) != null ) { stringBuilder.append(newLine+"\n"); } fileInputStream.close(); } mEditText.setText(stringBuilder); } catch (java.io.IOException e) { e.printStackTrace(); Toast.makeText(this, "Error reading file", Toast.LENGTH_LONG).show(); } } else { Toast.makeText(this, "Cannot read External Storage", Toast.LENGTH_LONG).show(); } } -
在具有外部存储的设备或模拟器上运行程序。
它是如何工作的...
读取和写入文件对于内部和外部存储基本上是相同的。主要区别在于在尝试访问外部存储之前,我们应该检查其可用性,这可以通过 isExternalStorageWritable() 和 isExternalStorageReadable() 方法来完成。当检查存储状态时,MEDIA_MOUNTED 表示我们可以对其进行读写操作。
与内部存储示例不同,我们像以下代码行中那样请求工作路径:
File textFile = new File(Environment.getExternalStorageDirectory(), FILENAME);
实际的读取和写入操作使用相同的类,因为只是位置不同。
小贴士
将外部文件夹路径硬编码是不可取的。路径可能在操作系统版本之间以及特别是在硬件制造商之间有所不同。始终最好调用 getExternalStorageDirectory(),如下所示。
更多内容...
以下是对一些附加信息的讨论。
获取公共文件夹
getExternalStorageDirectory() 方法返回外部存储的根文件夹。如果你想获取特定的公共文件夹,例如 Music 或 Ringtone 文件夹,请使用 getExternalStoragePublicDirectory() 并传入所需的文件夹类型,例如:
getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
检查可用空间
内部和外部存储之间一个一致的问题是空间有限。如果你事先知道你需要多少空间,你可以在 File 对象上调用 getFreeSpace() 方法。(getTotalSpace() 将返回总空间。)以下是一个使用 getFreeSpace() 调用的简单示例:
if (Environment.getExternalStorageDirectory().getFreeSpace() < RQUIRED_FILE_SPACE) {
//Not enough space
} else {
//We have enough space
}
删除文件
通过 File 对象,有许多辅助方法可用,包括删除文件。如果我们想删除示例中创建的文本文件,我们可以按照以下方式调用 delete() 方法:
textFile.delete()
与目录一起工作
虽然它被称为 File 对象,但它也支持目录命令,例如创建和删除目录。如果你想创建或删除目录,构建 File 对象,然后调用相应的方法:mkdir() 和 delete()。还有一个名为 mkdirs()(复数)的方法,它还会创建父目录。有关完整列表,请参阅以下链接。
防止文件被包含在相册中
Android 使用一个 媒体扫描器,它会自动将声音、视频和图像文件包含在系统集合中,例如图像库。要排除你的目录,在你要排除的文件所在的目录中创建一个名为 .nomedia(注意前面的点)的空文件。
相关内容
- 要获取
File类中所有可用方法的完整列表,请访问developer.android.com/reference/java/io/File.html
在项目中包含资源文件
Android 为包含项目中的文件提供了两种选项:raw文件夹和Assets文件夹。您使用哪种选项取决于您的需求。为了开始,我们将简要概述每个选项,以帮助您决定何时使用每个选项:
-
原始文件
-
包含在资源目录中:
/res/raw -
作为资源,通过原始标识符访问:
R.raw.<resource> -
存储媒体文件(如 MP3、MP4 和 OOG 文件)的好地方。
-
-
资产文件
-
在您的 APK 中编译的文件系统(不提供资源 ID)
-
使用文件名访问文件,通常这使得它们在使用动态创建的名称时更容易使用。
-
一些 API 不支持资源标识符,因此需要将其作为资产包含。
-
通常,raw文件更容易处理,因为它们是通过资源标识符访问的。正如我们将在此配方中展示的,主要区别在于您如何访问文件。在这个例子中,我们将加载一个raw文本文件和一个asset文本文件,并显示其内容。
准备工作
在 Android Studio 中创建一个新的项目,命名为:ReadingResourceFiles。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
如何操作...
为了演示从资源位置读取内容,我们将创建一个分割布局。我们还需要创建这两个资源文件夹,因为它们默认不包括在 Android 项目中。以下是步骤:
-
打开
activity_main.xml,将内容替换为以下布局:<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/textViewRaw" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center_horizontal|center_vertical"/> <TextView android:id="@+id/textViewAsset" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center_horizontal|center_vertical"/> </LinearLayout> -
在
res文件夹中创建raw资源文件夹。它将读取为:res/raw。 -
通过在
raw文件夹上右键单击并选择新建 | 文件来创建一个新的文本文件。将文件命名为raw_text.txt并在文件中输入一些文本。(这段文本将在您运行应用程序时显示。) -
创建
asset文件夹。由于位置的原因,asset文件夹比较复杂。幸运的是,Android Studio 提供了一个菜单选项,使得创建它变得非常简单。转到文件菜单(或在app节点上右键单击)并选择新建 | 文件夹 | 资产文件夹,如图所示:![如何操作...]()
-
在资产文件夹中创建另一个名为
asset_text.txt的文本文件。同样,您在这里输入的任何文本都会在运行应用程序时显示。以下是创建两个文本文件后的最终结果:![如何操作...]()
-
现在是时候编写代码了。打开
MainActivity.java并添加以下方法来读取文本文件(该文件作为参数传递给方法):private String getText(InputStream inputStream) { StringBuilder stringBuilder = new StringBuilder(); try {; if ( inputStream != null ) { InputStreamReader inputStreamReader = new InputStreamReader(inputStream); BufferedReader bufferedReader = new BufferedReader(inputStreamReader); String newLine = null; while ((newLine = bufferedReader.readLine()) != null ) { stringBuilder.append(newLine+"\n"); } inputStream.close(); } } catch (java.io.IOException e) { e.printStackTrace(); } return stringBuilder.toString(); } -
最后,将以下代码添加到
onCreate()方法中:TextView textViewRaw = (TextView)findViewById(R.id.textViewRaw); textViewRaw.setText(getText(this.getResources().openRawResource(R.raw.raw_text))); TextView textViewAsset = (TextView)findViewById(R.id.textViewAsset); try { textViewAsset.setText(getText(this.getAssets().open("asset_text.txt"))); } catch (IOException e) { e.printStackTrace(); } -
在设备或模拟器上运行程序。
它是如何工作的...
总结来说,唯一的区别在于我们如何获取每个文件的引用。这一行代码读取了raw资源:
this.getResources().openRawResource(R.raw.raw_text)
这段代码读取了asset文件:
this.getAssets().open("asset_text.txt")
这两个调用都返回一个 InputStream,getText() 方法使用它来读取文件内容。然而,值得注意的是,打开 asset 文本文件的调用需要额外的 try/catch。如配方介绍中所述,资源是索引的,因此我们有编译时验证,而 asset 文件夹没有。
还有更多...
一种常见的做法是将资源包含在您的 APK 中,但下载新的资源作为它们可用时。(参见第十二章 Chapter 12 中的网络通信,电话、网络和互联网。)如果新的资源不可用,您始终可以回退到 APK 中的资源。
另请参阅
- 在第十二章 Chapter 12 中介绍了网络通信的配方,电话、网络和互联网。
创建和使用 SQLite 数据库
在本配方中,我们将演示如何与 SQLite 数据库一起工作。如果您已经熟悉来自其他平台上的 SQL 数据库,那么您所知道的大部分内容都将适用。如果您是 SQLite 的新手,请查看“另请参阅”部分中的参考链接,因为本配方假设您对数据库概念有基本的了解,包括模式、表、游标和原始 SQL。
为了让您快速上手 SQLite 数据库,我们的示例实现了基本的 CRUD 操作。通常,在 Android 中创建数据库时,您会创建一个继承自 SQLiteOpenHelper 的类,这是您实现数据库功能的地方。以下是提供每个基本操作的功能列表:
-
创建:
insert() -
读取:
query()和rawQuery() -
更新:
update() -
删除:
delete()
为了演示一个完全工作的数据库,我们将创建一个简单的 Dictionary 数据库,以便我们可以存储单词及其定义。我们将通过允许添加新的单词(及其定义)和更新现有单词的定义来演示 CRUD 操作。我们将使用游标在 ListView 中显示单词。在 ListView 中按下单词将读取数据库中的定义并在 Toast 消息中显示它。长按将删除该单词。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:SQLiteDatabase。使用默认的 手机和平板 选项,并在提示 活动类型 时选择 空活动。
如何做到这一点...
首先,我们将创建 UI,它将包括两个 EditText 字段、一个按钮和一个 ListView。当我们向数据库添加单词时,它们将填充 ListView。首先,打开 activity_main.xml 并按照以下步骤操作:
-
用以下新视图替换现有的
<TextView>:<EditText android:id="@+id/et_word" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:hint="Word"/> <EditText android:id="@+id/et_definition" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/editTextWord" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:hint="Definition"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Save" android:id="@+id/button_add_update" android:layout_alignParentRight="true" android:layout_alignParentTop="true" /> <ListView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/listView" android:layout_below="@+id/et_definition" android:layout_alignParentLeft="true" android:layout_alignParentBottom="true" /> -
将名为
DictionaryDatabase的新 Java 类添加到项目中。这个类继承自SQLiteOpenHelper并处理所有 SQLite 功能。以下是类的声明:public class DictionaryDatabase extends SQLiteOpenHelper { -
在声明下方添加以下常量:
private static final String DATABASE_NAME = "dictionary.db"; private static final String TABLE_DICTIONARY = "dictionary"; private static final String FIELD_WORD = "word"; private static final String FIELD_DEFINITION = "definition"; private static final int DATABASE_VERSION = 1; -
添加以下构造函数、
OnCreate()和onUpgrade()方法:DictionaryDatabase(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_DICTIONARY + "(_id integer PRIMARY KEY," + FIELD_WORD + " TEXT, " + FIELD_DEFINITION + " TEXT);"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //Handle database upgrade as needed } -
以下方法负责创建、更新和删除记录:
public void saveRecord(String word, String definition) { long id = findWordID(word); if (id>0) { updateRecord(id, word,definition); } else { addRecord(word,definition); } } public long addRecord(String word, String definition) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put(FIELD_WORD, word); values.put(FIELD_DEFINITION, definition); return db.insert(TABLE_DICTIONARY, null, values); } public int updateRecord(long id, String word, String definition) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put("_id", id); values.put(FIELD_WORD, word); values.put(FIELD_DEFINITION, definition); return db.update(TABLE_DICTIONARY, values, "_id = ?", new String[]{String.valueOf(id)}); } public int deleteRecord(long id) { SQLiteDatabase db = getWritableDatabase(); return db.delete(TABLE_DICTIONARY, "_id = ?", new String[]{String.valueOf(id)}); } -
以及以下方法处理从数据库中读取信息:
public long findWordID(String word) { long returnVal = -1; SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT _id FROM " + TABLE_ DICTIONARY + " WHERE " + FIELD_WORD + " = ?", new String[]{word}); Log.i("findWordID","getCount()="+cursor.getCount()); if (cursor.getCount() == 1) { cursor.moveToFirst(); returnVal = cursor.getInt(0); } return returnVal; } public String getDefinition(long id) { String returnVal = ""; SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT definition FROM " + TABLE_ DICTIONARY + " WHERE _id = ?", new String[]{String.valueOf(id)}); if (cursor.getCount() == 1) { cursor.moveToFirst(); returnVal = cursor.getString(0); } return returnVal; } public Cursor getWordList() { SQLiteDatabase db = getReadableDatabase(); String query = "SELECT _id, " + FIELD_WORD + " FROM " + TABLE_DICTIONARY + " ORDER BY " + FIELD_WORD + " ASC"; return db.rawQuery(query, null); } -
数据库类完成后,打开
MainActivity.java。在类声明下方添加以下全局变量:EditText mEditTextWord; EditText mEditTextDefinition; DictionaryDatabase mDB; ListView mListView; -
当按钮被点击时,添加以下方法以保存字段:
private void saveRecord() { mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString()); mEditTextWord.setText(""); mEditTextDefinition.setText(""); updateWordList(); } -
添加以下方法以填充
ListView:private void updateWordList() { SimpleCursorAdapter simpleCursorAdapter = new SimpleCursorAdapter( this, android.R.layout.simple_list_item_1, mDB.getWordList(), new String[]{"word"}, new int[]{android.R.id.text1}, 0); mListView.setAdapter(simpleCursorAdapter); } -
最后,将以下代码添加到
onCreate()中:mDB = new DictionaryDatabase(this); mEditTextWord = (EditText)findViewById(R.id.editTextWord); mEditTextDefinition = (EditText)findViewById(R.id.editTextDefinition); Button buttonAddUpdate = (Button)findViewById(R.id.buttonAddUpdate); buttonAddUpdate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { saveRecord(); } }); mListView = (ListView)findViewById(R.id.listView); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, mDB.getDefinition(id),Toast.LENGTH_SHORT).show(); } }); mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "Records deleted = " + mDB.deleteRecord(id), Toast.LENGTH_SHORT).show(); updateWordList(); return true; } }); updateWordList(); -
在设备或模拟器上运行程序并尝试使用它。
它是如何工作的...
我们首先解释DictionaryDatabase类,因为它是 SQLite 数据库的核心。首先要注意的是构造函数:
DictionaryDatabase(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
注意DATABASE_VERSION?只有当你更改数据库模式时,才需要增加这个值。
接下来是onCreate(),这是数据库实际上创建的地方。这仅在数据库第一次创建时调用,而不是每次创建类时调用。还值得注意的是_id字段。Android 不需要表有主字段,除了某些类如SimpleCursorAdapter需要_id。
我们需要实现onUpgrade()回调,但因为这个是一个新数据库,没有需要做的事情。当数据库版本增加时,这个方法将被调用。
saveRecord()方法处理调用addRecord()或updateRecord(),根据需要。由于我们打算修改数据库,这两个方法都调用getWritableDatabase(),这样我们就可以进行更改。可写数据库需要更多的资源,所以如果你不需要进行更改,就获取一个只读数据库。
最后要注意的方法是getWordList(),它使用游标对象返回数据库中的所有单词。我们使用这个游标来填充ListView,这把我们带到了ActivityMain.java。onCreate()方法执行我们之前看到的常规初始化,并使用以下代码创建数据库的一个实例:
mDB = new DictionaryDatabase(this);
onCreate()方法还设置了事件,当按下项目时显示单词定义(使用 Toast),以及在长按时删除单词。最复杂的代码可能在updateWordList()中。
这不是我们第一次使用适配器,但这是第一个游标适配器,所以我们将解释。我们使用SimpleCursorAdapter在游标中的字段和ListView项目之间创建映射。我们使用layout.simple_list_item_1布局,它只包含一个 ID 为android.R.id.text1的单个文本字段。在实际应用中,我们可能会创建一个自定义布局,并在ListView项中包含定义,但我们的目的是演示从数据库中读取定义的方法。
我们在三个地方调用updateWordList()——在onCreate()中创建初始列表,然后在我们添加/更新列表之后再次调用,最后在删除列表时调用。
还有更多...
虽然这是一个完整的 SQLite 示例,但它仍然是基础知识。关于 Android 的 SQLite,可以写一本书,实际上也已经写了。
升级数据库
正如我们之前提到的,当我们增加数据库版本时,onUpgrade() 方法将被调用。你在这里所做的是依赖于变化的。如果你更改了现有的表,理想情况下,你将通过查询现有数据并将其插入到新格式中来迁移用户数据。请注意,没有保证用户会按顺序升级——例如,他们可能从版本 1 跳到版本 4。
参见
-
SQLite 主页:
www.sqlite.org/ -
SQLite 数据库 Android 参考:
developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html
使用加载器在后台访问数据
任何可能长时间运行的操作都不应该在 UI 线程上执行,因为这可能导致你的应用程序变慢或无响应。当应用程序无响应时,Android OS 将弹出 应用程序无响应 (ANR) 对话框。
由于查询数据库可能耗时,Android 在 Android 3.0 中引入了 Loader API。Loader 在后台线程上处理查询,并在完成时通知 UI 线程。
加载器的主要好处包括:
-
查询数据库是在后台线程(自动)处理的
-
查询自动更新(当使用内容提供者数据源时)
为了演示加载器,我们将修改之前的 SQLite 数据库示例,使用 CursorLoader 来填充 ListView。
准备工作
我们将使用前一个示例中的项目,即 创建和使用 SQLite 数据库,作为本菜谱的基础。在 Android Studio 中创建一个新的项目,并将其命名为:Loader。选择默认的 Phone & Tablet 选项,并在提示 Activity 类型 时选择 Empty Activity。复制前一个菜谱中的 DictionaryDatabase 类和布局。虽然我们将使用前一个 ActivityMain.java 代码的部分,但我们将从这个菜谱的开始部分开始,以便更容易跟随。
如何做到这一点...
根据之前描述的项目设置,我们将首先创建两个新的 Java 类,然后在 ActivityMain.java 中将它们全部连接起来。以下是步骤:
-
创建一个新的 Java 类,命名为
DictionaryAdapter,它扩展了CursorAdapter。这个类替换了我们在前一个菜谱中使用的SimpleCursorAdapater。以下是完整的代码:public class DictionaryAdapter extends CursorAdapter { public DictionaryAdapter(Context context, Cursor c, int flags) { super(context, c, flags); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1,parent,false); } @Override public void bindView(View view, Context context, Cursor cursor) { TextView textView = (TextView)view.findViewById(android.R.id.text1); textView.setText(cursor.getString(getCursor().getColumnIndex("word"))); } } -
接下来,创建另一个新的 Java 类,并将其命名为
DictionaryLoader。尽管这个类处理后台线程中的数据加载,但实际上它非常简单:public class DictionaryLoader extends CursorLoader { Context mContext; public DictionaryLoader(Context context) { super(context); mContext = context; } @Override public Cursor loadInBackground() { DictionaryDatabase db = new DictionaryDatabase(mContext); return db.getWordList(); } } -
接下来,打开
ActivityMain.java文件。我们需要将声明改为实现LoaderManager.LoaderCallbacks<Cursor>接口,如下所示:public class MainActivity extends AppCompatActivity implements { -
将适配器添加到全局声明中。完整的列表如下:
EditText mEditTextWord; EditText mEditTextDefinition; DictionaryDatabase mDB; ListView mListView; DictionaryAdapter mAdapter; -
将
onCreate()改为使用新的适配器,并在删除记录后添加一个调用更新加载器的调用。最终的onCreate()方法应如下所示:protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mDB = new DictionaryDatabase(this); mEditTextWord = (EditText) findViewById(R.id.editTextWord); mEditTextDefinition = (EditText) findViewById(R.id.editTextDefinition); Button buttonAddUpdate = (Button) findViewById(R.id.buttonAddUpdate); buttonAddUpdate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { saveRecord(); } }); mListView = (ListView) findViewById(R.id.listView); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, mDB.getDefinition(id), Toast.LENGTH_SHORT).show(); } }); mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(MainActivity.this, "Records deleted = " + mDB.deleteRecord(id), Toast.LENGTH_SHORT).show(); getSupportLoaderManager().restartLoader(0, null, MainActivity.this); return true; } }); getSupportLoaderManager().initLoader(0, null, this); mAdapter = new DictionaryAdapter(this,mDB.getWordList(),0); mListView.setAdapter(mAdapter); } -
我们不再有
updateWordList()方法,因此将saveRecord()改如下:private void saveRecord() { mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString()); mEditTextWord.setText(""); mEditTextDefinition.setText(""); getSupportLoaderManager().restartLoader(0, null, MainActivity.this); } -
最后,实现加载器接口的这三个方法:
@Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { return new DictionaryLoader(this); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { mAdapter.swapCursor(data); } @Override public void onLoaderReset(Loader<Cursor> loader) { mAdapter.swapCursor(null); } -
在设备或模拟器上运行程序。
它是如何工作的...
默认的 CursorAdapter 需要一个内容提供者 URI。由于我们直接访问 SQLite 数据库(而不是通过内容提供者),我们没有 URI 可以传递,因此我们通过扩展 CursorAdapter 类创建了一个自定义适配器。DictionaryAdapter 仍然执行与之前配方中的 SimpleCursorAdapter 相同的功能,即从游标映射数据到项目布局。
我们添加的下一个类是 DictionaryLoader,这是实际的加载器。正如你所见,它实际上非常简单。它所做的只是从 getWordList() 返回游标。关键在于这个查询是在后台线程中处理的,并在完成时调用 onLoadFinished() 回调(在 MainActivity.java 中)。幸运的是,大部分繁重的工作都在基类中处理。
这带我们到 ActivityMain.java,在那里我们实现了来自 LoaderManager.LoaderCallbacks 接口的三种回调:
-
onCreateLoader():最初在onCreate()中通过initLoader()调用。在修改数据库后,它还会在restartLoader()调用中再次被调用。 -
onLoadFinished():当加载器的loadInBackground()完成时会被调用。 -
onLoaderReset():当加载器正在被重新创建时(例如使用restart()方法)会被调用。我们将旧游标设置为null,因为它将被无效化,我们不希望保留引用。
还有更多...
正如你在前面的例子中看到的,我们需要手动通知加载器使用 restartLoader() 重新查询数据库。使用加载器的一个好处是它可以自动更新,但它需要一个内容提供者作为数据源。内容提供者支持使用 SQLite 数据库作为数据源,对于严肃的应用程序,这是推荐的。查看以下内容提供者链接以开始。
参见
-
第十四章 AsyncTask 配方,为 Play 商店准备你的应用。
-
创建内容提供者:
developer.android.com/guide/topics/providers/content-provider-creating.html
第七章. 警报和通知
在本章中,我们将涵盖以下主题:
-
灯光、动作和声音——吸引用户的注意力!
-
使用自定义布局创建 Toast
-
使用 AlertDialog 显示消息框
-
显示进度对话框
-
使用通知的灯光、动作和声音重制版
-
创建一个媒体播放器通知
-
使用抬头显示通知制作手电筒
简介
Android 提供了多种方式来通知用户——从非视觉方法,包括声音、灯光和震动,到视觉方法,包括 Toast、对话框和状态栏通知。
请记住,通知会分散用户的注意力,所以在使用任何通知时非常谨慎是个好主意。用户喜欢控制自己的设备(毕竟,那是他们的设备),所以请给他们提供按需启用和禁用通知的选项。否则,您的用户可能会感到烦恼,甚至完全卸载您的应用程序。
我们将首先回顾以下非 UI 基于的通知选项:
-
闪烁 LED
-
震动手机
-
播放铃声
然后,我们将转向视觉通知,包括:
-
Toast
-
AlertDialog -
ProgressDialog -
状态栏通知
下面的食谱将向您展示如何在您的应用程序中实现这些功能。阅读以下链接了解使用通知时的“最佳实践”是值得的:
小贴士
请参考developer.android.com/design/patterns/notifications.html上的Android 通知设计指南
灯光、动作和声音——吸引用户的注意力!
本章中的大多数食谱都使用通知对象来提醒用户,所以这个食谱将展示当您实际上不需要通知时的替代方法。
正如食谱标题所暗示的,我们将使用灯光、动作和声音:
-
灯光:通常,您会使用 LED 设备,但这是通过通知对象提供的,我们将在本章后面演示。相反,我们将利用这个机会使用
setTorchMode()(在 API 23——Android 6.0 中添加)来使用相机闪光灯作为手电筒。(注意:正如您将在代码中看到的,此功能仅在具有闪光灯的 Android 6.0 设备上工作。) -
动作:我们将使手机震动。
-
声音:我们将使用
RingtoneManager播放默认的通知声音。
正如您将看到的,这些代码的每个都非常简单。
如以下 使用通知的“灯光、动作和声音重制”食谱 所示,所有三个选项:LED、振动和声音,都可通过通知对象获得。当用户没有在您的应用中积极互动时,通知对象无疑是提供警报和提醒的最合适方法。但那些您想在用户使用您的应用时提供反馈的时候,这些选项都是可用的。振动选项是一个很好的例子;如果您想为按钮按下提供触觉反馈(常见于键盘应用),可以直接调用振动方法。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:LightsActionSound。当提示 API 级别时,我们需要 API 21 或更高版本来编译项目。当被提示选择 Activity 类型 时,选择 Empty Activity。
如何做到这一点...
我们将使用三个按钮来启动每个动作,所以首先打开 activity_main.xml 并按照以下步骤操作:
-
将现有的
<TextView>元素替换为以下三个按钮:<ToggleButton android:id="@+id/buttonLights" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Lights" android:layout_centerHorizontal="true" android:layout_above="@+id/buttonAction" android:onClick="clickLights" /> <Button android:id="@+id/buttonAction" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Action" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="clickVibrate"/> <Button android:id="@+id/buttonSound" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Sound" android:layout_below="@+id/buttonAction" android:layout_centerHorizontal="true" android:onClick="clickSound"/> -
将以下权限添加到 Android Manifest 中:
<uses-permission android:name="android.permission.VIBRATE"></uses-permission> -
打开
ActivityMain.java并添加以下全局变量:private CameraManager mCameraManager; private String mCameraId=null; private ToggleButton mButtonLights; -
添加以下方法以获取相机 ID:
private String getCameraId() { try { String[] ids = mCameraManager.getCameraIdList(); for (String id : ids) { CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id); Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); Integer facingDirection = c.get(CameraCharacteristics.LENS_FACING); if (flashAvailable != null && flashAvailable && facingDirection != null && facingDirection == CameraCharacteristics.LENS_FACING_BACK) { return id; } } } catch (CameraAccessException e) { e.printStackTrace(); } return null; } -
将以下代码添加到
onCreate()方法中:mButtonLights = (ToggleButton)findViewById(R.id.buttonLights); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); mCameraId = getCameraId(); if (mCameraId==null) { mButtonLights.setEnabled(false); } else { mButtonLights.setEnabled(true); } } else { mButtonLights.setEnabled(false); } -
现在添加处理每个按钮点击的代码:
public void clickLights(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { mCameraManager.setTorchMode(mCameraId, mButtonLights.isChecked()); } catch (CameraAccessException e) { e.printStackTrace(); } } } public void clickVibrate(View view) { ((Vibrator)getSystemService(VIBRATOR_SERVICE)).vibrate(1000); } public void clickSound(View view) { Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); Ringtone ringtone = RingtoneManager.getRingtone(getApplicationContext(), notificationSoundUri); ringtone.play(); } -
您现在可以运行应用程序在物理设备上了。这里展示的代码需要 Android 6.0(或更高版本)才能使用闪光灯选项。
它是如何工作的...
如前几段所示,大部分代码都与查找和打开相机以使用闪光灯功能相关。setTorchMode() 是在 API 23 中引入的,这就是为什么我们需要 API 版本检查:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){}
此应用演示了使用新引入的 camera2 库,这些库在 Lollipop(API 21)中引入。vibrate 和 ringtone 方法自 API 1 以来都可用。
getCameraId() 方法是我们检查相机的地方。我们想要一个带有闪光灯的外向相机。如果找到了,则返回 ID,否则为 null。如果相机 ID 为 null,则禁用按钮。
对于播放声音,我们使用 RingtoneManager 中的 Ringtone 对象。除了实现起来相对容易外,这种方法的好处还包括我们可以使用默认的通知声音,如下代码所示:
Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
这样,如果用户更改了他们首选的通知声音,我们会自动使用它。
最后是调用振动手机的代码。这是最简单的代码,但它确实需要权限,我们在 Manifest 中添加了权限:
<uses-permission android:name="android.permission.VIBRATE"></uses-permission>
更多内容...
在生产级应用中,您不希望在不必要的情况下简单地禁用按钮。在这种情况下,有其他方法可以使用相机闪光灯作为手电筒。查看多媒体章节,以获取使用相机的更多示例,我们将再次看到 getCameraId() 的使用。
相关阅读
-
请参考本章后面的使用通知对象重置灯光、动作和声音食谱,以查看使用通知对象等效的功能
-
请参考第十一章,多媒体,以了解使用新的相机 API 和其他声音选项的示例
使用自定义布局创建 Toast
我们已经在之前的章节中多次使用 Toast,因为它们提供了一种快速简单的方式来显示信息——既适用于用户,也适用于我们在调试时的需要。
之前的所有示例都使用了简单的单行语法,但 Toast 并不局限于这一点。Toast,就像 Android 中的大多数组件一样,可以自定义,正如我们将在本食谱中展示的那样。
Android Studio 提供了一个快捷方式来制作简单的 Toast 语句。当你开始输入 Toast 命令时,按Ctrl + 空格键,你会看到以下内容:

按Enter键自动完成。然后,再次按Ctrl + 空格键,你会看到以下内容:

当你再次按Enter键时,它会自动完成以下内容:
Toast.makeText(MainActivity.this, "", Toast.LENGTH_SHORT).show();
在本食谱中,我们将使用 Toast Builder 来更改默认布局,并使用重力创建一个自定义 Toast,如图所示:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:CustomToast。在提示活动类型时,使用默认的手机和平板选项,并选择空活动。
如何操作...
我们将更改 Toast 的形状为正方形,并创建一个自定义布局来显示图像和文本消息。首先打开activity_main.xml并按照以下步骤操作:
-
将现有的
<TextView>元素替换为<Button>,如下所示:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Toast" android:id="@+id/button" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:onClick="showToast"/> -
在
res/drawable文件夹中创建一个新的资源文件,命名为:border_square.xml,并输入以下代码:<?xml version="1.0" encoding="utf-8"?> <layer-list > <item android:left="4px" android:top="4px" android:right="4px" android:bottom="4px"> <shape android:shape="rectangle" > <solid android:color="@android:color/black" /> <stroke android:width="5px" android:color="@android:color/white"/> </shape> </item> </layer-list> -
在
res/layout文件夹中创建一个新的资源文件,命名为:toast_custom.xml,并输入以下代码:<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:id="@+id/toast_layout_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:background="@drawable/border_square"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageView" android:layout_weight="1" android:src="img/ic_launcher" /> <TextView android:id="@android:id/message" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:textColor="@android:color/white" android:padding="10dp" /> </LinearLayout> -
现在打开
ActivityMain.java并输入以下方法:public void showToast(View view) { LayoutInflater inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View layout = inflater.inflate(R.layout.toast_custom, null); ((TextView) layout.findViewById(android.R.id.message)).setText("Custom Toast"); Toast toast = new Toast(this); toast.setGravity(Gravity.CENTER, 0, 0); toast.setDuration(Toast.LENGTH_LONG); toast.setView(layout); toast.show(); } -
在设备或模拟器上运行程序。
它是如何工作的...
这个自定义 Toast 更改了默认的重力、形状,并添加了一个图像,仅为了展示“它可以做到”。
第一步是创建一个新的 Toast 布局,我们通过填充我们的custom_toast布局来实现。一旦我们有了新的布局,我们需要获取TextView以便我们可以设置我们的消息,我们使用标准的setText()方法来完成。完成此操作后,我们创建一个 Toast 对象并设置其单个属性。我们使用setGravity()方法设置 Toast 的重力。重力决定了 Toast 将在屏幕上的哪个位置显示。我们使用setView()方法调用指定我们的自定义布局。就像在单行变体中一样,我们使用show()方法显示 Toast。
使用 AlertDialog 显示消息框
在第四章“菜单”中,我们创建了一个主题,使 Activity 看起来像对话框。在本例中,我们将演示如何使用AlertDialog类创建对话框。AlertDialog提供了一个标题,最多三个按钮,以及一个列表或自定义布局区域,如本例所示:

注意
按钮位置可能会根据操作系统版本的不同而有所变化。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:AlertDialog。使用默认的手机和平板选项,并在提示活动类型时选择空活动选项。
如何操作...
为了演示,我们将创建一个确认删除对话框,在按下删除按钮后提示用户确认。首先打开main_activity.xml布局文件,并按照以下步骤操作:
-
添加以下
<Button>:<Button android:id="@+id/buttonClose" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Delete" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="confirmDelete"/> -
添加由按钮调用的
confirmDelete()方法:public void confirmDelete(View view) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("Delete") .setMessage("Are you sure you?") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { Toast.makeText(MainActivity.this, "OK Pressed", Toast.LENGTH_SHORT).show(); }}) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { Toast.makeText(MainActivity.this, "Cancel Pressed", Toast.LENGTH_SHORT).show(); }}); builder.create().show(); } -
在设备或模拟器上运行应用程序。
工作原理...
此对话框旨在作为一个简单的确认对话框——例如确认删除操作。基本上,只需创建一个AlertDialog.Builder对象,并根据需要设置属性。我们使用 Toast 消息来指示用户选择,甚至不需要关闭对话框;这由基类处理。
还有更多...
如食谱介绍截图所示,AlertDialog还有一个名为中立按钮的第三个按钮,可以使用以下方法设置:
builder.setNeutralButton()
添加图标
要向对话框添加图标,请使用setIcon()方法。以下是一个示例:
.setIcon(R.mipmap.ic_launcher)
使用列表
我们还可以使用各种列表设置方法创建一个用于选择的项列表,包括:
.setItems()
.setAdapter()
.setSingleChoiceItems()
.setMultiChoiceItems()
如您所见,还有用于单选(使用单选按钮)和多选列表(使用复选框)的方法。
小贴士
您不能同时使用消息和列表,因为setMessage()将具有优先级。
自定义布局
最后,我们还可以创建一个自定义布局,并使用以下方法设置:
.setView()
如果您使用自定义布局并替换标准按钮,您还负责关闭对话框。如果您计划重用对话框,请使用hide();完成时使用dismiss()来释放资源。
显示进度对话框
ProgressDialog自 API 1 以来一直可用,并且被广泛使用。正如我们将在本例中演示的那样,它简单易用,但请记住以下声明(来自 Android 对话框指南网站):
避免使用 ProgressDialog
Android 还包括另一个名为 ProgressDialog 的对话框类,它显示带有进度条的对话框。但是,如果您需要指示加载或不确定的进度,您应遵循进度和活动的设计指南,并在布局中使用 ProgressBar。
developer.android.com/guide/topics/ui/dialogs.html
这条消息并不意味着 ProgressDialog 已被弃用或代码不好。它只是建议应避免使用 ProgressDialog,因为当对话框显示时,用户无法与你的应用交互。如果可能,使用包含进度条的布局,而不是使用 ProgressDialog。
Google Play 应用提供了一个很好的例子。在添加下载项时,Google Play 会显示一个进度条,但它不是一个对话框,因此用户可以继续与应用交互,甚至可以添加更多下载项。如果可能,请使用那种方法。
有时候你可能没有这样的奢侈,比如在下单后,用户会期待订单确认。(即使是在 Google Play 上,在购买应用时你仍然会看到一个确认对话框。)所以,记住,如果可能的话,避免使用进度对话框。但是,对于那些必须完成某些操作才能继续的情况,这个配方提供了一个如何使用 ProgressDialog 的示例。以下截图显示了配方中的 ProgressDialog:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:ProgressDialog。使用默认的 Phone & Tablet 选项,并在提示选择 Activity Type 时选择 Empty Activity。
如何操作...
-
由于这只是一个关于使用
ProgressDialog的演示,我们将创建一个按钮来显示对话框。为了模拟等待服务器响应,我们将使用延迟消息来关闭对话框。首先,打开activity_main.xml并按照以下步骤操作: -
将
<TextView>替换为以下<Button>:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Dialog" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="startProgress"/> -
打开
MainActivity.java并添加以下两个全局变量:private ProgressDialog mDialog; final int THIRTY_SECONDS=30*1000; -
添加按钮点击时引用的
showDialog()方法:public void startProgress(View view) { mDialog= new ProgressDialog(this); mDialog.setMessage("Doing something..."); mDialog.setCancelable(false); mDialog.show(); new Handler().postDelayed(new Runnable() { public void run() { mDialog.dismiss(); }}, THIRTY_SECONDS); -
在设备或模拟器上运行程序。当你按下 Show Dialog 按钮时,你会看到如图所示的对话框。
它是如何工作的...
我们使用 ProgressDialog 类来显示对话框。选项应该是自解释的,但这个设置值得注意:
mDialog.setCancelable(false);
通常,对话框可以通过 返回 键取消,但当你将其设置为 false 时,用户将卡在对话框上,直到它从代码中隐藏/消失。为了模拟服务器响应的延迟,我们使用 Handler 和 postDelayed() 方法。在指定的毫秒数(本例中为 30,000 毫秒,代表 30 秒)后,run() 方法将被调用,这将关闭我们的对话框。
还有更多...
我们为这个配方使用了默认的 ProgressDialog 设置,这会创建一个不确定的对话框指示器,例如,持续旋转的圆圈。如果你可以测量手头的任务,例如加载文件,你可以使用确定样式。添加并运行以下代码行:
mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
你将得到以下对话框样式作为前一行代码的输出:

使用通知重演灯光、动作和声音
您可能已经熟悉通知功能了,因为它们已经成为一个突出的特性(甚至进入桌面环境),而且有充分的理由。它们为向用户发送信息提供了极佳的方式。它们提供了所有可用警报和通知选项中最不干扰的选择。
正如我们在第一个配方中看到的,灯光、动作和声音 – 引起用户的注意!,灯光、振动和声音对于引起用户的注意都非常有用。这就是为什么通知对象包括对这三种方法的支持,我们将在本配方中展示。鉴于这种引起用户注意的能力,仍然应该注意不要滥用用户。否则,他们可能会卸载您的应用。通常,给用户选择启用/禁用通知以及如何呈现通知(带声音或无声等)是一个好主意。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:LightsActionSoundRedux。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
如何实现...
我们需要权限来使用振动选项,所以首先打开 Android Manifest 文件,并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.VIBRATE"/> -
打开
activity_main.xml,并用以下按钮替换现有的<TextView>:<Button android:id="@+id/buttonSound" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Lights, Action, and Sound" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="clickLightsActionSound"/> -
现在打开
MainActivity.java,并添加以下方法来处理按钮点击:public void clickLightsActionSound(View view) { Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("LightsActionSoundRedux") .setContentText("Lights, Action & Sound") .setSound(notificationSoundUri) .setLights(Color.BLUE, 500, 500) .setVibrate(new long[]{250,500,250,500,250,500}); NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notificationBuilder.build()); } -
在设备或模拟器上运行程序。
它是如何工作的...
首先,我们因为可以做到,所以将所有三个动作合并为一个单一的通知。您不必使用所有三个额外的通知选项,甚至不需要任何。只需以下内容是必需的:
.setSmallIcon()
.setContentText()
如果您没有设置图标和文本,通知将不会显示。
第二,我们使用了NotificationCompat来构建我们的通知。这来自支持库,使得与较老的操作系版本向后兼容变得更容易。如果我们请求用户操作系统中不可用的通知功能,它将被简单地忽略。
产生我们额外通知选项的三个代码行包括以下内容:
.setSound(notificationSoundUri)
.setLights(Color.BLUE, 500, 500)
.setVibrate(new long[]{250,500,250,500,250,500});
值得注意的是,我们使用与之前灯光、动作和声音配方中RingtoneManager相同的音效 URI 来与通知一起使用。振动功能也需要与之前配方相同的振动权限,但请注意我们发送的值是不同的。我们不是只发送振动的持续时间,而是发送一个振动模式。第一个值代表关闭持续时间(以毫秒为单位),下一个值代表振动开启持续时间,并重复。
小贴士
在带有 LED 通知功能的设备上,当屏幕处于活动状态时,您将看不到 LED 通知。
更多内容...
这个配方展示了通知的基本知识,但就像 Android 上的许多功能一样,选项随着后续操作系统的发布而扩展。
使用 addAction()添加按钮到通知
在添加操作按钮时,你应该注意几个设计考虑因素,如章节引言中链接的通知指南中所述。你可以在通知构建器上使用addAction()方法添加一个按钮(最多三个)。以下是一个包含一个操作按钮的通知示例:

这是创建此通知的代码:
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("LightsActionSoundRedux")
.setContentText("Lights, Action & Sound");
Intent activityIntent = new Intent(this,MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,activityIntent,0);
notificationBuilder.addAction(android.R.drawable.ic_dialog_email, "Email", pendingIntent);
notificationManager.notify(0, notificationBuilder.build());
一个Action需要三个参数——图像、文本和一个PendingIntent。前两项用于视觉显示,而第三项,即PendingIntent,在用户按下按钮时被调用。
之前的代码创建了一个非常简单的PendingIntent;它只是启动应用。这可能是通知中最常见的意图,通常用于用户按下通知时。要设置通知意图,请使用以下代码:
.setContentIntent(pendingIntent)
按钮操作可能需要更多信息,因为它应将用户带到应用中的特定项目。你还应该创建一个应用程序回退栈以获得最佳用户体验。请参阅以下链接中的主题“启动 Activity 时保留导航”:
developer.android.com/guide/topics/ui/notifiers/notifications.html#NotificationResponse
展开的通知
展开的通知自 Android 4.1(API 16)引入,可以通过在通知构建器上使用setStyle()方法来使用。如果用户的操作系统不支持展开的通知,通知将显示为正常通知。
目前在NotificationCompat库中可用的三种展开样式包括:
-
InboxStyle -
BigPictureStyle -
BigTextStyle
这里是每种通知样式的示例,以及创建示例所使用的代码:

-
InboxStyle:NotificationCompat.Builder notificationBuilderInboxStyle = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher); NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); inboxStyle.setBigContentTitle("InboxStyle - Big Content Title") .addLine("Line 1") .addLine("Line 2"); notificationBuilderInboxStyle.setStyle(inboxStyle); notificationManager.notify(0, notificationBuilderInboxStyle.build());![展开的通知]()
-
BigPictureStyle:NotificationCompat.Builder notificationBuilderBigPictureStyle = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher).setContentTitle("LightsActionSoundRedux").setContentText("BigPictureStyle"); NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); bigPictureStyle.bigPicture(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)); notificationBuilderBigPictureStyle.setStyle(bigPictureStyle); notificationManager.notify(0, notificationBuilderBigPictureStyle.build());![展开的通知]()
-
BigTextStyleNotificationCompat.Builder notificationBuilderBigTextStyle = new NotificationCompat.Builder(this).setSmallIcon(R.mipmap.ic_launcher).setContentTitle("LightsActionSoundRedux"); NotificationCompat.BigTextStyle BigTextStyle = new NotificationCompat.BigTextStyle(); BigTextStyle.bigText("This is an example of the BigTextStyle expanded notification."); notificationBuilderBigTextStyle.setStyle(BigTextStyle); notificationManager.notify(0, notificationBuilderBigTextStyle.build());
锁屏通知
Android 5.0(API 21)及以上版本可以根据用户的锁屏可见性在锁屏上显示通知。使用setVisibility()指定通知可见性,以下是一些值:
-
VISIBILITY_PUBLIC: 可以显示所有内容 -
VISIBILITY_SECRET: 不应显示任何内容 -
VISIBILITY_PRIVATE: 显示基本内容(标题和图标),但其余内容被隐藏
参见
- 参见“创建媒体播放器通知”和“使用抬头显示通知制作手电筒”的配方,以获取 Android 5.0(API 21)及以上版本中额外的通知选项
创建一个媒体播放器通知
这个菜谱将要探讨 Android 5.0(API 21)中引入的新媒体播放器样式。与之前使用NotificationCompat的菜谱不同,即*使用通知的“灯光、动作和声音重制”,这个菜谱不使用NotificationCompat,因为这个样式在支持库中不可用。
这是一张显示通知将如何显示的截图:

这张截图显示了锁屏上媒体播放器通知的示例:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:MediaPlayerNotification。当提示 API 级别时,我们需要为这个项目选择 API 21(或更高)。当被提示选择Activity 类型时,选择Empty Activity。
如何实现...
我们只需要一个按钮来调用我们的代码以发送通知。打开activity_main.xml并按照以下步骤操作:
-
用以下按钮代码替换现有的
<TextView>:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Show Notification" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="showNotification"/> -
打开
MainActivity.java并添加showNotification()方法:@Deprecated public void showNotification(View view) { Intent activityIntent = new Intent(this,MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, activityIntent, 0); Notification notification; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { notification = new Notification.Builder(this).setVisibility(Notification.VISIBILITY_PUBLIC) .setSmallIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) .addAction(new Notification.Action.Builder(Icon.createWithResource(this, android.R.drawable.ic_media_previous), "Previous", pendingIntent).build()) .addAction(new Notification.Action.Builder(Icon.createWithResource(this, android.R.drawable.ic_media_pause), "Pause", pendingIntent).build()) .addAction(new Notification.Action.Builder(Icon.createWithResource(this, android.R.drawable.ic_media_next), "Next", pendingIntent).build()) .setContentTitle("Music") .setContentText("Now playing...") .setLargeIcon(Icon.createWithResource(this, R.mipmap.ic_launcher)) .setStyle(new Notification.MediaStyle().setShowActionsInCompactView(1)).build(); } else { notification = new Notification.Builder(this) .setVisibility(Notification.VISIBILITY_PUBLIC) .setSmallIcon(R.mipmap.ic_launcher) .addAction(new Notification.Action.Builder(android.R.drawable.ic_media_previous, "Previous", pendingIntent).build()) .addAction(new Notification.Action.Builder(android.R.drawable.ic_media_pause, "Pause", pendingIntent).build()) .addAction(new Notification.Action.Builder(android.R.drawable.ic_media_next, "Next", pendingIntent).build()) .setContentTitle("Music") .setContentText("Now playing...") .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setStyle(new Notification.MediaStyle() .setShowActionsInCompactView(1)).build(); } NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notification); } -
在设备或模拟器上运行程序。
它是如何工作的...
首先要注意的细节是,我们在showNotification()方法上进行了装饰:
@Deprecated
这告诉编译器我们知道我们正在使用已弃用的调用。(如果没有这个,编译器将会标记代码。)我们接着进行 API 检查,使用以下调用:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
API 23 中改变了图标资源,但我们要使这个应用程序在 API 21(Android 5.0)及更高版本上运行,因此当在 API 21 和 API 22 上运行时,我们仍然需要调用旧的方法。
如果用户正在运行 Android 6.0(或更高版本),我们使用新的Icon类来创建我们的图标,否则我们使用旧的构造函数。(您会注意到 IDE 会以删除线显示已弃用的调用。)在运行时检查当前操作系统版本是保持向后兼容的常见策略。
我们使用addAction()创建三个操作来处理媒体播放器功能。由于我们实际上并没有媒体播放器在运行,所以我们为所有操作使用相同的 intent,但您可能希望在您的应用程序中为每个操作创建单独的 intent。
要使通知在锁屏上可见,我们需要将可见性级别设置为VISIBILITY_PUBLIC,这可以通过以下调用完成:
.setVisibility(Notification.VISIBILITY_PUBLIC)
这个调用值得注意:
.setShowActionsInCompactView(1)
正如方法名所暗示的,这将在通知以简化布局显示时设置要显示的操作。(参见菜谱介绍中的锁屏图像。)
还有更多...
在这个菜谱中,我们只创建了视觉通知。如果我们正在创建一个实际的媒体播放器,我们可以实例化一个MediaSession类,并通过这个调用传入会话令牌:
.setMediaSession(mMediaSession.getSessionToken())
这将允许系统识别媒体内容并相应地做出反应,例如更新锁屏上的当前专辑封面。
参见
-
请参考开发者文档 – 媒体会话。
-
在使用通知的灯光、动作和声音 Redux菜谱中,锁屏可见性部分讨论了可见性选项。
使用抬头显示通知制作手电筒
Android 5.0—Lollipop(API 21)引入了一种新的通知类型,称为抬头显示通知。许多人不喜欢这种新的通知,因为它可能会非常侵扰性,因为它会强制其方式覆盖其他应用程序。(见以下截图。)在使用此类通知时请记住这一点。我们将通过使用手电筒来演示抬头显示通知,因为这展示了良好的用例场景。
这是一张显示我们将创建的抬头显示通知的截图:

如果你有一台运行 Android 6.0 的设备,你可能已经注意到了新的闪光灯设置选项。作为演示,我们将在本菜谱中创建类似的功能。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:FlashlightWithHeadsUp。当提示 API 级别时,我们需要 API 23(或更高)来使用这个项目。当被提示选择活动类型时,选择空活动。
如何做到这一点...
我们的活动布局将只包含一个ToggleButton来控制手电筒模式。我们将使用与之前展示的灯光、动作和声音 – 引起用户的注意!菜谱中相同的setTorchMode()代码,并添加一个抬头显示通知。我们需要权限来使用振动选项,因此首先打开 AndroidManifest 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.VIBRATE"/> -
通过在
<MainActivity>元素中添加android:launchMode="singleInstance"来指定我们只想有一个MainActivity的实例。它看起来如下所示:<activity android:name=".MainActivity" android:launchMode="singleInstance"> -
在完成对
AndroidManifest的更改后,打开activity_main.xml布局,并用以下<ToggleButton>代码替换现有的<TextView>元素:<ToggleButton android:id="@+id/buttonLight" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Flashlight" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="clickLight"/> -
现在打开
ActivityMain.java并添加以下全局变量:private static final String ACTION_STOP="STOP"; private CameraManager mCameraManager; private String mCameraId=null; private ToggleButton mButtonLight; -
将以下代码添加到
onCreate()中,以设置相机:mButtonLight = (ToggleButton)findViewById(R.id.buttonLight); mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE); mCameraId = getCameraId(); if (mCameraId==null) { mButtonLight.setEnabled(false); } else { mButtonLight.setEnabled(true); } -
添加以下方法来处理用户按下通知时的响应:
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); if (ACTION_STOP.equals(intent.getAction())) { setFlashlight(false); } } -
添加获取相机 ID 的方法:
private String getCameraId() { try { String[] ids = mCameraManager.getCameraIdList(); for (String id : ids) { CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id); Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); Integer facingDirection = c.get(CameraCharacteristics.LENS_FACING); if (flashAvailable != null && flashAvailable && facingDirection != null && facingDirection == CameraCharacteristics.LENS_FACING_BACK) { return id; } } } catch (CameraAccessException e) { e.printStackTrace(); } return null; } -
添加这两个方法来处理手电筒模式:
public void clickLight(View view) { setFlashlight(mButtonLight.isChecked()); if (mButtonLight.isChecked()) { showNotification(); } } private void setFlashlight(boolean enabled) { mButtonLight.setChecked(enabled); try { mCameraManager.setTorchMode(mCameraId, enabled); } catch (CameraAccessException e) { e.printStackTrace(); } } -
最后,添加此方法来创建通知:
private void showNotification() { Intent activityIntent = new Intent(this,MainActivity.class); activityIntent.setAction(ACTION_STOP); PendingIntent pendingIntent = PendingIntent.getActivity(this,0,activityIntent,0); final Builder notificationBuilder = new Builder(this).setContentTitle("Flashlight") .setContentText("Press to turn off the flashlight") .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentIntent(pendingIntent) .setVibrate(new long[]{DEFAULT_VIBRATE}) .setPriority(PRIORITY_MAX); NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notificationBuilder.build()); } -
你已经准备好在物理设备上运行应用程序了。如前所述,你需要一台 Android 6.0(或更高版本)的设备,并且具有外置摄像头闪光灯。
它是如何工作的...
由于这个菜谱使用与灯光、动作和声音 – 引起用户的注意!相同的闪光灯代码,我们将跳转到showNotification()方法。大多数通知构建器调用与之前的示例相同,但有两大显著差异:
.setVibrate()
.setPriority(PRIORITY_MAX)
小贴士
除非优先级设置为 HIGH(或更高),并且使用振动或声音,否则通知不会被提升为“抬头显示”通知。
注意以下内容,这是在开发者文档中给出的:developer.android.com/reference/android/app/Notification.html#headsUpContentView
“系统界面可以自行决定是否将其显示为“抬头显示”通知。”
我们创建一个 PendingIntent,就像我们之前做的那样,但在这里我们使用以下方式设置动作:
activityIntent.setAction(ACTION_STOP);
我们在 AndroidManifest 文件中将应用程序设置为只允许一个实例,因为我们不希望在用户按下通知时启动应用程序的新实例。我们创建的 PendingIntent 设置了动作,我们在 onNewIntent() 回调中检查这个动作。如果用户在不按下通知的情况下打开应用程序,他们仍然可以使用 ToggleButton 禁用手电筒。
还有更多...
就像之前在 使用自定义布局创建吐司 的食谱中一样,我们可以在通知中使用自定义布局。在构建器上使用以下方法来指定布局:
headsupContentView()
参见
- 请参阅 灯光、动作和声音 – 引起用户的注意! 食谱
第八章。使用触摸屏和传感器
在本章中,我们将涵盖以下主题:
-
监听点击和长按事件
-
识别点击和其他常见手势
-
使用多点触控手势进行捏合缩放
-
滑动刷新
-
列出可用传感器 - Android 传感器框架简介
-
读取传感器数据 - 使用 Android 传感器框架事件
-
读取设备方向
简介
这些日子,移动设备充满了传感器,通常包括陀螺仪、磁性、重力、压力和/或温度传感器,更不用说触摸屏了。这为与用户交互提供了许多新的和令人兴奋的选项。通过传感器,您可以确定三维设备位置以及设备本身的使用方式,例如摇晃、旋转、倾斜等。即使是触摸屏也提供了从简单的点击到手势和多点触控的许多新的输入方法。
我们将从这个章节开始,通过探索触摸屏交互来开始,从简单的点击和长按开始,然后继续使用SimpleOnGestureListener类检测常见的手势。接下来,我们将查看使用ScaleGestureDetector的多点触控,通过捏合缩放手势。
这本书旨在为您提供快速指南,以添加功能和功能到您自己的应用程序中。因此,它侧重于所需的代码。强烈建议您花些时间阅读设计指南。
小贴士
Google 手势设计指南,请参阅www.google.com/design/spec/patterns/gestures.html
在本章的后面部分,我们将探讨 Android 中的传感器功能,使用 Android 传感器框架。我们将演示如何获取所有可用传感器的列表,以及如何检查特定传感器。一旦我们获取了传感器,我们将演示设置监听器以读取传感器数据。最后,我们将以确定设备方向的方法结束本章。
监听点击和长按事件
几乎每个应用程序都需要识别和响应对基本事件,如点击和长按。这在大多数食谱中是如此基本,我们通常使用 XML 的onClick属性,但对于更高级的监听器则需要通过代码设置。
Android 提供了一个事件监听器接口,用于在发生某些操作时接收单个通知,如下列所示:
-
onClick():当视图被按下时调用 -
onLongClick():当视图被长按时调用 -
onFocusChange():当用户导航到或从视图时调用 -
onKey():当硬件键被按下或释放时调用 -
onTouch():当发生触摸事件时调用
这个食谱将演示如何响应用户的点击事件以及长按事件。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:PressEvents。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何做到这一点...
设置接收基本视图事件非常简单。首先我们将创建一个视图;在我们的例子中,我们将使用按钮,然后在 Activity 的 onCreate() 方法中设置事件监听器。以下是步骤:
-
打开
activity_main.xml并将现有的TextView替换为以下Button:<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> -
现在,打开
MainActivy.java并将以下代码添加到现有的onCreate()方法中:Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "Click", Toast.LENGTH_SHORT).show(); } }); button.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { Toast.makeText(MainActivity.this, "Long Press", Toast.LENGTH_SHORT).show(); return true; } }); -
在设备或模拟器上运行应用程序,并尝试常规点击和长按。
它是如何工作的...
在本书中使用的多数示例中,我们使用以下属性在 XML 中设置了 onClick 监听器:
android:onClick=""
您可能会注意到 XML 的 onClick() 方法回调需要与 setOnClickListener 相同的方法签名。onClick() 回调:
public void onClick(View v) {}
这是因为当我们使用 XML 的 onClick 属性时,Android 会自动为我们设置回调。这个例子还演示了我们可以在一个视图上拥有多个监听器。
最后一点需要注意的是,onLongClick() 方法返回一个布尔值,就像大多数其他事件监听器一样。返回 true 以指示事件已被处理。
还有更多...
尽管按钮通常用于指示用户应“按下”的位置,但我们可以在任何视图上使用 setOnClickListener() 和 setOnLongClickListener(),甚至是 TextView。
如介绍中所述,还有其他事件监听器。您可以使用 Android Studio 的自动完成功能。首先输入以下命令:
button.setOn
然后按 Ctrl + Spacebar 来查看列表。
识别点击和其他常见手势
与前一个配方中描述的事件监听器不同,手势需要两步过程:
-
收集运动数据
-
分析数据以确定它是否与已知的手势匹配
第一步开始于用户触摸屏幕时,这会触发 onTouchEvent() 回调,并带有在 MotionEvent 对象中发送的运动数据。幸运的是,Android 通过 GestureDetector 类使第二步,即分析数据,变得更容易,该类可以检测以下手势:
-
onTouchEvent() -
onDown() -
onFling() -
onLongPress() -
onScroll() -
onShowPress() -
onDoubleTap() -
onDoubleTapEvent() -
onSingleTapConfirmed()
本配方将演示使用 GestureDetector.SimpleOnGestureListener 来识别触摸和双击手势。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:CommonGestureDetector。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何做到这一点...
我们将使用活动本身来检测手势,因此我们不需要在布局中添加任何视图。打开 MainActivity.java 并按照以下步骤操作:
-
添加以下全局变量:
private GestureDetectorCompat mGestureDetector; -
在
MainActivity类中添加以下GestureListener类:private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapConfirmed(MotionEvent e) { Toast.makeText(MainActivity.this, "onSingleTapConfirmed", Toast.LENGTH_SHORT).show(); return super.onSingleTapConfirmed(e); } @Override public boolean onDoubleTap(MotionEvent e) { Toast.makeText(MainActivity.this, "onDoubleTap", Toast.LENGTH_SHORT).show(); return super.onDoubleTap(e); } } -
如下重写
onTouchEvent()方法:public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); return super.onTouchEvent(event); } -
最后,将以下代码行添加到
onCreate()方法中:mGestureDetector = new GestureDetectorCompat(this, new GestureListener()); -
在设备或模拟器上运行此应用程序。
工作原理...
我们使用GestureDetectorCompat,它来自 Support Library,允许在运行 Android 1.6 及更高版本的设备上支持手势。
如食谱介绍中所述,检测手势是一个两步过程。为了收集运动或手势数据,我们开始通过触摸事件跟踪运动。每次调用onTouchEvent()时,我们将该数据发送到GestureDetector。GestureDetector处理第二步,即分析数据。一旦检测到手势,就会调用适当的回调。我们的示例处理了单次和双击手势。
还有更多...
您的应用程序可以通过简单地重写适当的回调来轻松添加对GestureDetector检测到的剩余手势的支持。
相关内容
- 查看下一食谱,使用多指手势进行缩放,了解多指手势
使用多指手势进行缩放
之前的食谱使用了SimpleOnGestureListener来提供简单、单指手势的检测。在这个食谱中,我们将使用SimpleOnScaleGestureListener类演示多指的缩放手势。
以下截图显示了使用以下食谱创建的应用程序缩放后的图标:

以下截图显示了缩放后的图标:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:MultiTouchZoom。使用默认的手机和平板电脑选项,并在提示活动类型时选择空活动。
如何操作...
为了提供缩放手势的视觉指示,我们将使用一个带有应用程序图标的ImageView。打开activity_main.xml文件,按照以下步骤操作:
-
将现有的
TextView替换为以下ImageView:<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="img/ic_launcher" android:layout_centerVertical="true" android:layout_centerHorizontal="true" /> -
现在打开
MainActivity.java文件,并将以下全局变量添加到类中:private ScaleGestureDetector mScaleGestureDetector; private float mScaleFactor = 1.0f; private ImageView mImageView; -
如下重写
onTouchEvent()方法:public boolean onTouchEvent(MotionEvent motionEvent) { mScaleGestureDetector.onTouchEvent(motionEvent); return true; } -
将以下
ScaleListener类添加到MainActivity类中:private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector scaleGestureDetector) { mScaleFactor *= scaleGestureDetector.getScaleFactor(); mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f)); mImageView.setScaleX(mScaleFactor); mImageView.setScaleY(mScaleFactor); return true; } } -
将以下代码添加到现有的
onCreate()方法中:mImageView=(ImageView)findViewById(R.id.imageView); mScaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener()); -
要实验缩放功能,请在具有触摸屏的设备上运行应用程序。
工作原理...
ScaleGestureDetector通过分析手势数据并通过onScale()回调报告最终缩放因子来完成所有工作。我们通过在ScaleGestureDetector上调用getScaleFactor()来获取实际的缩放因子。
我们使用一个ImageView来显示应用程序图标,通过设置ImageView的缩放来提供缩放的视觉表示。为了防止缩放过大或过小,我们添加了以下检查:
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));
滑动刷新
通过下拉列表来指示手动刷新的称为 Swipe-to-Refresh 手势。这是一个如此常见的功能,以至于这个功能被封装在一个名为SwipeRefreshLayout的单个小部件中。
这个菜谱将展示如何使用这个小部件通过ListView添加 Swipe-to-Refresh 功能。以下截图显示了刷新动作:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:SwipeToRefresh。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何做到这一点...
首先,我们需要将 SwipeRefreshLayout 小部件和 ListView 添加到活动布局中,然后我们将在 Java 代码中实现刷新监听器。以下是详细步骤:
-
打开
activity_main.xml并将现有的<TextView>替换为以下内容:<android.support.v4.widget.SwipeRefreshLayout android:id="@+id/swipeRefresh" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout> -
现在打开
MainActivity.java并为该类添加以下全局变量:SwipeRefreshLayout mSwipeRefreshLayout; ListView mListView; List mArrayList = new ArrayList<>(); private int mRefreshCount=0; -
添加以下方法来处理刷新:
private void refreshList() { mRefreshCount++; mArrayList.add("Refresh: " + mRefreshCount); ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mArrayList); mListView.setAdapter(countryAdapter); mSwipeRefreshLayout.setRefreshing(false); } -
将以下代码添加到现有的
onCreate()方法中:mSwipeRefreshLayout = (SwipeRefreshLayout)findViewById(R.id.swipeRefresh); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { refreshList(); } }); mListView = (ListView)findViewById(android.R.id.list); final String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"}; mArrayList = new ArrayList<String>(Arrays.asList(countries)); ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mArrayList); mListView.setAdapter(countryAdapter); -
在设备或模拟器上运行应用程序。
它是如何工作的...
这个菜谱的大部分代码都是为了模拟每次调用刷新方法时向 ListView 添加项目来模拟刷新。实现 Swipe-to-Refresh 的主要步骤包括:
-
添加
SwipeRefreshLayout小部件。 -
在
SwipeRefreshLayout内包含ListView。 -
添加
OnRefreshListener以调用你的刷新方法。 -
完成更新后调用
setRefreshing(false)。
就这样。这个小部件使得添加 Swipe-to-Refresh 变得非常简单!
还有更多...
虽然 Swipe-to-Refresh 手势现在是应用程序的常见功能,但仍然是一个好习惯包括一个菜单项(尤其是为了可访问性原因)。以下是菜单布局的 XML 片段:
<menu >
<item
android:id="@+id/menu_refresh"
android:showAsAction="never"
android:title="@string/menu_refresh"/>
</menu>
在onOptionsItemSelected()回调中调用你的刷新方法。当从代码执行刷新,例如从菜单项事件时,你想要通知SwipeRefreshLayout刷新,以便它可以更新 UI。使用以下代码执行此操作:
SwipeRefreshLayout.setRefreshing(true);
这告诉SwipeRefreshLayout一个刷新开始,以便它可以显示正在进行的指示器。
列出可用传感器 - Android Sensor Framework 简介
Android 使用 Android Sensor Framework 支持硬件传感器。该框架包括以下类和接口:
-
SensorManager -
Sensor -
SensorEventListener -
SensorEvent
大多数 Android 设备都包括硬件传感器,但它们在不同制造商和型号之间差异很大。如果你的应用程序使用传感器,你有两种选择:
-
在 Android Manifest 中指定传感器
-
在运行时检查传感器
要指定你的应用程序使用传感器,请在 Android Manifest 中包含<uses-feature>声明。以下是一个需要罗盘可用的示例:
<uses-feature android:name="android.hardware.sensor.compass" android:required="true"/>
如果您的应用程序使用指南针,但不需要它来运行,则应将 android:required="false" 设置为 false,否则应用程序将无法通过 Google Play 获取。
传感器被分为以下三个类别:
-
运动传感器
-
环境传感器
-
位置传感器
Android SDK 提供了对以下传感器类型的支持:
| 传感器 | 检测 | 使用 |
|---|---|---|
TYPE_ACCELEROMETER |
包括重力在内的运动检测 | 用于确定震动、倾斜等 |
TYPE_AMBIENT_TEMPERATURE |
测量环境室温 | 用于确定局部温度 |
TYPE_GRAVITY |
测量三个轴上的重力 | 用于运动检测 |
TYPE_GYROSCOPE |
测量三个轴上的旋转 | 用于确定转向、旋转等 |
TYPE_LIGHT |
测量光照强度 | 用于设置屏幕亮度 |
TYPE_LINEAR_ACCELERATION |
排除重力在内的运动检测 | 用于确定加速度 |
TYPE_MAGNETIC_FIELD |
测量地磁场 | 用于创建指南针或确定方位 |
TYPE_PRESSURE |
测量空气压力 | 用于气压计 |
TYPE_PROXIMITY |
测量屏幕相对于物体的距离 | 用于确定在通话期间设备是否被紧贴耳朵 |
TYPE_RELATIVE_HUMIDITY |
测量相对湿度 | 用于确定露点和湿度 |
TYPE_ROTATION_VECTOR |
测量设备方向 | 用于检测运动和旋转 |
有两个额外的传感器:TYPE_ORIENTATION 和 TYPE_TEMPERATURE,它们已经被弃用,因为它们已经被新的传感器所取代。
本教程将演示如何检索可用传感器的列表。以下是来自物理设备的截图:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:ListDeviceSensors。使用默认的 Phone & Tablet 选项,并在提示选择 Activity Type 时选择 Empty Activity。
如何实现...
首先,我们将查询可用的传感器列表,然后将在 ListView 中显示结果。以下是详细步骤:
-
打开
activity_main.xml并将现有的TextView替换为以下内容:<ListView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="match_parent" /> -
接下来,打开
ActivityMain.java并将以下代码添加到现有的onCreate()方法中:ListView listView = (ListView)findViewById(R.id.list); List sensorList = new ArrayList<String>(); List<Sensor> sensors = ((SensorManager) getSystemService(Context.SENSOR_SERVICE)).getSensorList(Sensor.TYPE_ALL); for (Sensor sensor : sensors ) { sensorList.add(sensor.getName()); } ListAdapter sensorAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, sensorList); listView.setAdapter(sensorAdapter); -
在设备或模拟器上运行程序。
工作原理...
以下代码行负责获取可用传感器的列表;其余代码用于填充 ListView:
List<Sensor> sensors = ((SensorManager) getSystemService(Context.SENSOR_SERVICE)).getSensorList(Sensor.TYPE_ALL);
注意,我们返回了一个 Sensor 对象的列表。我们只获取传感器名称以在 ListView 中显示,但还有其他属性可用。请参阅 另请参阅 部分中提供的链接,以获取完整的列表。
还有更多...
如 Nexus 9 的介绍截图所示,一个设备可以拥有多个相同类型的传感器。如果你在寻找特定的传感器,你可以传递介绍中显示的表中的一个常量。在这种情况下,如果你想查看所有可用的加速度计传感器,你可以使用以下调用:
List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
如果你不是在寻找传感器列表,而是需要与特定传感器一起工作,你可以使用以下代码检查默认传感器:
if (sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null){
//Sensor is available - do something here
}
参见
- Android 开发者传感器网站:
developer.android.com/reference/android/hardware/Sensor.html
读取传感器数据 – 使用 Android 传感器框架事件
前一个示例,列出可用传感器 – Android 传感器框架简介,提供了对 Android 传感器框架的介绍。现在我们将通过使用SensorEventListener来查看如何读取传感器数据。SensorEventListener接口只有两个回调:
-
onSensorChanged() -
onAccuracyChanged()
当传感器有新的数据要报告时,它会调用带有SensorEvent对象的onSensorChanged()。本示例将演示如何读取光传感器,但由于所有传感器都使用相同的框架,因此将此示例适配到任何其他传感器都非常容易。(参见前一个示例介绍中提供的传感器类型列表。)
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:ReadingSensorData。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
如何操作...
我们将在活动布局中添加一个TextView来显示传感器数据,然后我们将添加SensorEventListener到 Java 代码中。我们将使用onResume()和onPause()事件来启动和停止我们的事件监听器。要开始,请打开activity_main.xml并按照以下步骤操作:
-
按照以下方式修改现有的
TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="0"/> -
现在打开
MainActivity.java并添加以下全局变量声明:private SensorManager mSensorManager; private Sensor mSensor; private TextView mTextView; -
按照以下方式将
SensorListener类添加到MainActivity类中:private SensorEventListener mSensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { mTextView.setText(String.valueOf(event.values[0])); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { //Nothing to do } }; -
我们将在
onResume()和onPause()中注册和注销传感器事件,如下所示:@Override protected void onResume() { super.onResume(); mSensorManager.registerListener(mSensorListener, mSensor, SensorManager.SENSOR_DELAY_NORMAL); } @Override protected void onPause() { super.onPause(); mSensorManager.unregisterListener(mSensorListener); } -
将以下代码添加到
onCreate()中:mTextView = (TextView)findViewById(R.id.textView); mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); -
你现在可以在物理设备上运行应用程序,以查看来自光传感器的原始数据。
它是如何工作的...
使用 Android 传感器框架首先是从获取传感器开始,我们在onCreate()中这样做。在这里,我们调用getDefaultSensor(),请求TYPE_LIGHT。我们在onResume()中注册监听器,并在onPause()中再次注销以减少电池消耗。当我们调用registerListener()时,我们传递我们的mSensorListener对象。
在我们的案例中,我们只关注传感器数据,这些数据是在onSensorChanged()回调中发送的。当传感器发生变化时,我们会用传感器数据更新TextView。
还有更多...
现在你已经使用了一个传感器,你知道如何使用所有传感器,因为它们都使用相同的框架。当然,你对数据的处理将因读取的数据类型而大不相同。环境传感器,如这里所示,返回单个值,但位置和运动传感器也可以返回额外的元素,如下所示。
环境传感器
Android 支持以下四种环境传感器:
-
湿度
-
光线
-
压力
-
温度
环境传感器通常更容易处理,因为返回的数据是单个元素,通常不需要校准或过滤。我们使用光线传感器进行此演示,因为大多数设备都包含光线传感器以控制屏幕亮度。
位置传感器
位置传感器包括:
-
地磁场
-
接近度
以下传感器类型使用地磁场:
-
TYPE_GAME_ROTATION_VECTOR -
TYPE_GEOMAGNETIC_ROTATION_VECTOR -
TYPE_MAGNETIC_FIELD -
TYPE_MAGNETIC_FIELD_UNCALIBRATED
这些传感器在onSensorChanged()事件中返回三个值,除了TYPE_MAGNETIC_FIELD_UNCALIBRATED,它发送六个值。
第三个传感器,方向传感器已被弃用,现在建议使用getRotation()和getRotationMatrix()来计算方向变化。(有关设备方向,如纵向和横向模式,请参阅下一配方:读取设备方向。)
运动传感器
运动传感器包括以下内容:
-
加速度计
-
惯性传感器
-
重力
-
线性加速度
-
旋转向量
这些包括以下传感器类型:
-
TYPE_ACCELEROMETE -
TYPE_GRAVITY -
TYPE_GYROSCOPE -
TYPE_GYROSCOPE_UNCALIBRATED -
TYPE_LINEAR_ACCELERATION -
TYPE_ROTATION_VECTOR -
TYPE_SIGNIFICANT_MOTION -
TYPE_STEP_COUNTER -
TYPE_STEP_DETECTOR
这些传感器也包括三个数据元素,除了最后三个。TYPE_SIGNIFICANT_MOTION和TYPE_STEP_DETECTOR表示一个事件,而TYPE_STEP_COUNTER返回自上次启动(传感器处于活动状态)以来的步数。
参见
-
列出可用传感器 - Android 传感器框架简介配方
-
在第九章 图形和动画的使用传感器数据和 RotateAnimation 创建指南针配方中
-
关于设备方向,请参阅读取设备方向配方
-
第十三章 获取位置和使用地理围栏,涵盖了GPS 和位置配方
读取设备方向
虽然 Android 框架会在方向改变时自动加载新的资源(如布局),但有时你可能希望禁用此行为。如果你希望被通知方向改变而不是 Android 自动处理,请将以下属性添加到 AndroidManifest 中的 Activity:
android:configChanges="keyboardHidden|orientation|screenSize"
当以下配置更改之一发生时,系统将通过onConfigurationChanged()方法通知你,而不是自动处理:
-
keyboardHidden -
orientation -
screenSize
onConfigurationChanged()的签名如下:
onConfigurationChanged (Configuration newConfig)
你将在newConfig.orientation中找到新的方向。
小贴士
禁用自动配置更改(这会导致布局重新加载并重置状态信息)不应作为正确保存状态信息的替代方案。你的应用程序仍然可能在任何时候被中断或完全停止,并由系统杀死。(有关正确保存状态信息,请参阅第一章中的保存活动状态。)
本配方将演示如何确定当前设备方向。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:GetDeviceOrientation。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
如何操作...
我们将在布局中添加一个按钮以按需检查方向。首先打开activity_main.xml,然后按照以下步骤操作:
-
将现有的
TextView替换为以下Button:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Check Orientation" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="checkOrientation"/> -
添加以下方法来处理按钮点击:
public void checkOrientation(View view){ int orientation = getResources().getConfiguration().orientation; switch (orientation) { case Configuration.ORIENTATION_LANDSCAPE: Toast.makeText(MainActivity.this, "ORIENTATION_LANDSCAPE", Toast.LENGTH_SHORT).show(); break; case Configuration.ORIENTATION_PORTRAIT: Toast.makeText(MainActivity.this, "ORIENTATION_PORTRAIT", Toast.LENGTH_SHORT).show(); break; case Configuration.ORIENTATION_UNDEFINED: Toast.makeText(MainActivity.this, "ORIENTATION_UNDEFINED", Toast.LENGTH_SHORT).show(); break; } } -
在设备或模拟器上运行应用程序。
小贴士
使用Ctrl + F11旋转模拟器。
的工作原理...
要获取当前方向,我们只需调用以下代码行:
getResources().getConfiguration().orientation
方向以int类型返回,我们将其与三个可能值之一进行比较,如下所示。
还有更多...
获取当前设备旋转
另一个你可能需要知道当前方向的情况是在处理相机数据——图片和/或视频时。通常,图像可能会根据设备方向或为了补偿当前方向而旋转。在这种情况下,还有另一个选项可以获取旋转:
int rotation = getWindowManager().getDefaultDisplay().getRotation();
在前面的代码行中,rotation将是以下值之一:
-
Surface.ROTATION_0 -
Surface.ROTATION_90 -
Surface.ROTATION_180 -
Surface.ROTATION_270
小贴士
旋转值将来自其正常方向。例如,当使用具有正常方向为横屏的表格时,如果以竖屏方向拍照,值将是ROTATION_90或ROTATION_270。
参见
第九章. 图形和动画
在本章中,我们将涵盖以下主题:
-
缩小大型图像以避免内存不足异常
-
过渡动画 - 定义场景并应用过渡
-
使用传感器数据和 RotateAnimation 创建指南针
-
使用 ViewPager 创建幻灯片
-
使用片段创建卡片翻转动画
-
使用自定义过渡创建缩放动画
简介
动画可以既美观又实用,如简单的按钮按下所示。按钮按下的图形表示使应用程序生动起来,同时它通过给用户提供对事件的视觉响应来提供功能价值。
Android 框架提供了几个动画系统,以使在您的应用程序中包含动画变得更加容易。它们包括以下内容:
-
视图动画:(原始的动画系统。)它通常需要更少的代码,但动画选项有限
-
属性动画:这是一个更灵活的系统,允许动画化任何对象的任何属性
-
可绘制动画:它使用可绘制资源来创建帧帧动画(如电影)
属性动画系统是在 Android 3.0 中引入的,由于它的灵活性,通常比视图动画更受欢迎。视图动画的主要缺点包括:
-
可以动画化的方面有限 - 例如缩放和旋转
-
只能动画视图的内容 - 它不能改变视图在屏幕上的绘制位置(因此不能动画化在屏幕上移动的球)
-
只能动画视图对象
这里是一个简单的示例,演示了如何使用视图动画来“闪烁”视图(按钮按下的简单模拟):
Animation blink =AnimationUtils.loadAnimation(this,R.anim.blink);
view.startAnimation(blink);
这里是位于res/anim文件夹中的blink.xml资源文件的 内容:
<?xml version="1.0" encoding="utf-8"?>
<set >
<alpha android:fromAlpha="1.0"
android:toAlpha="0.0"
android:background="#000000"
android:interpolator="@android:anim/linear_interpolator"
android:duration="100"
android:repeatMode="restart"
android:repeatCount="0"/>
</set>
如您所见,创建此动画非常简单,因此如果视图动画实现了您的目标,请使用它。当它不符合您的需求时,转向属性动画系统。我们将在使用片段创建卡片翻转动画和使用自定义过渡创建缩放动画菜谱中演示属性动画。
过渡动画 - 定义场景并应用过渡 菜单将提供有关 Android 过渡框架的更多信息,我们将在许多菜谱中使用它。
注意
插值器是一个定义动画变化率的函数。
在本章和前面的闪烁示例中,将提到Interpolators。插值器定义了过渡的计算方式。线性插值器将在设定的持续时间内均匀地计算变化,而AccelerateInterpolator函数将创建一个在持续时间内更快的移动。以下是可用的完整插值器列表,以及相应的 XML 标识符:
-
AccelerateDecelerateInterpolator (@android:anim/accelerate_decelerate_interpolator) -
AccelerateInterpolator (@android:anim/accelerate_interpolator) -
AnticipateInterpolator (@android:anim/anticipate_interpolator) -
AnticipateOvershootInterpolator (@android:anim/anticipate_overshoot_interpolator) -
BounceInterpolator (@android:anim/bounce_interpolator) -
CycleInterpolator (@android:anim/cycle_interpolator) -
DecelerateInterpolator (@android:anim/decelerate_interpolator) -
LinearInterpolator (@android:anim/linear_interpolator) -
OvershootInterpolator (@android:anim/overshoot_interpolator)
虽然动画通常不需要太多内存,但图形资源通常需要。你可能想要处理的许多图像往往超过了可用设备内存。在本章的第一个菜谱中,将大图像缩小以避免内存不足异常,我们将讨论如何子采样(或缩小)图像。
将大图像缩小以避免内存不足异常
处理图像可能会非常消耗内存,通常会导致你的应用程序因 内存不足 异常而崩溃。这尤其适用于用设备相机拍摄的图片,因为它们的分辨率通常比设备本身要高得多。
由于在这个例子中,加载比 UI 支持的更高分辨率的图像不会提供任何视觉上的好处,这个菜谱将演示如何为显示获取图像的小样本。我们将使用 BitmapFactory 首先检查图像大小,然后加载一个缩小后的图像。
这里是一个从这个菜谱中截取的屏幕截图,显示了非常大的图像的缩略图:

准备就绪
在 Android Studio 中创建一个新的项目,并将其命名为:LoadLargeImage。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
为了这个菜谱,我们需要一个大图像;所以我们转向 www.Pixabay.com 获取一个图像。由于图像本身并不重要,我们下载了当时显示的第一张图像。(完整尺寸的图像是 6000 x 4000 和 3.4MB。)
如何操作...
如前所述在 准备就绪 中,我们需要一个大图像来演示缩放。一旦你有了图像,请按照以下步骤操作:
-
将图像复制到
res/drawable目录下,命名为image_large.jpg(如果你选择不同的文件类型,请使用适当的扩展名)。 -
打开
activity_main.xml并将现有的TextView替换为以下ImageView:<ImageView android:id="@+id/imageViewThumbnail" android:layout_width="100dp" android:layout_height="100dp" android:layout_centerInParent="true" /> -
现在打开
MainActivity.java并添加以下方法,我们将在稍后解释:public Bitmap loadSampledResource(int imageID, int targetHeight, int targetWidth) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), imageID, options); final int originalHeight = options.outHeight; final int originalWidth = options.outWidth; int inSampleSize = 1; while ((originalHeight / (inSampleSize *2)) > targetHeight && (originalWidth / (inSampleSize *2)) > targetWidth) { inSampleSize *= 2; } options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(getResources(), imageID, options); } -
将以下代码添加到现有的
onCreate()方法中:ImageView imageView = (ImageView)findViewById(R.id.imageViewThumbnail); imageView.setImageBitmap(loadSampledResource(R.drawable.image_large, 100, 100)); -
在设备或模拟器上运行应用程序。
它是如何工作的...
loadSampledResource()方法的目的在于加载一个较小的图像,以减少图像的内存消耗。如果我们尝试加载从www.Pixabay.Com(见前面的准备就绪部分)选择的完整图像,应用程序将需要超过 3 MB 的 RAM 来加载。这比大多数设备能处理的内存还要多(至少目前是这样),即使它能完全加载,也不会为我们的缩略图视图提供任何视觉上的好处。
为了避免内存不足的情况,我们使用BitmapFactory.Options的inSampleSize属性来减少或子采样图像。(如果我们设置inSampleSize=2,则图像将减半。如果我们使用inSampleSize=4,则图像将减为四分之一)要计算inSampleSize,首先我们需要知道图像大小。我们可以使用以下inJustDecodeBounds属性:
options.inJustDecodeBounds = true;
这告诉BitmapFactory获取图像尺寸而不实际存储图像内容。一旦我们有了图像大小,我们使用以下代码计算样本:
while ((originalHeight / (inSampleSize *2)) > targetHeight && (originalWidth / (inSampleSize *2)) > targetWidth) {
inSampleSize *= 2;
}
这段代码的目的是确定最大的样本大小,不会将图像降低到目标尺寸以下。为此,我们将样本大小加倍并检查其是否超过目标尺寸。如果没有,我们保存加倍后的样本大小并重复。一旦减小后的尺寸低于目标尺寸,我们就使用最后保存的inSampleSize。
注意
从inSampleSize文档(以下另请参阅部分的链接)中,请注意,解码器使用基于 2 的幂的最终值,任何其他值都将四舍五入到最接近的 2 的幂。
一旦我们有了样本大小,我们设置inSampleSize属性并将inJustDecodeBounds设置为false,以正常加载。以下是代码:
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
需要注意的是,这个菜谱展示了将任务应用于您自己的应用程序的概念。加载和处理图像可能是一个耗时的操作,这可能导致您的应用程序停止响应。这不是一个好现象,可能会导致 Android 显示应用程序无响应(ANR)对话框。建议在后台线程上执行长时间任务以保持 UI 线程的响应性。AsyncTask类可用于执行后台网络处理,但还有许多其他库可供选择(菜谱末尾的链接):
-
Volley:在网络中执行快速、可扩展的 UI 操作(见第十二章,电话、网络和互联网)
-
Picasso:一个强大的 Android 图像下载和缓存库
-
Android 通用图像加载器:一个用于加载、缓存和显示图像的强大且灵活的库
还有更多...
重要的是要注意,我们传递给 loadSampledResource() 方法的 targetHeight 和 targetWidth 参数实际上并没有设置图片大小。如果你使用与我们相同的尺寸的图片运行应用程序,样本大小将为 32,导致加载的图片大小为 187 x 125。
如果你的布局需要特定大小的图片,你可以在布局文件中设置大小,或者可以直接使用 Bitmap 类修改图片大小。
参见
-
开发者文档:BitmapFactory.inSampleSize() 在
developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inSampleSize -
参考位于
github.com/nostra13/Android-Universal-Image-Loader的 Android Universal Image Loader 页面 -
请参考毕加索的
square.github.io/picasso/ -
检查 第十四章 中的 AsyncTask 任务,为 Play 商店准备你的应用,以在后台线程上处理长时间运行的操作。
过渡动画 - 定义场景并应用过渡
Android Transition 框架提供了以下功能:
-
组级动画:动画应用于层次结构中的所有视图
-
基于过渡的动画:基于起始和结束属性变化的动画
-
内置动画:一些常见的过渡效果,如淡入/淡出和移动
-
资源文件支持:将动画值保存到资源(XML)文件中,在运行时加载
-
生命周期回调:在动画期间接收回调通知
过渡动画由以下组成:
-
起始场景:动画开始时的视图(或
ViewGroup) -
Transition:更改类型(见后文)
-
结束场景:结束视图(或
ViewGroup) -
过渡效果:Android 提供了对以下三种过渡效果的内置支持:
-
AutoTransition(默认过渡):淡出,移动,然后调整大小,最后淡入(按此顺序)
-
淡入淡出:淡入,淡出(默认),或两者(指定顺序)
-
ChangeBounds:移动和调整大小
-
Transition 框架将自动创建从起始场景到结束场景所需的所有帧以进行动画。
以下是在使用以下类时 Transition 框架的一些已知限制:
-
SurfaceView:由于
SurfaceView动画是在非 UI 线程上执行的,因此动画可能不正确,可能与应用程序不同步 -
TextView:动画文本大小变化可能无法正确执行,导致文本跳转到最终状态
-
AdapterView:扩展
AdapterView的类,如ListView和GridView,可能会挂起 -
TextureView:某些过渡可能无法正常工作
本菜谱提供了一个关于使用过渡动画系统的快速教程。我们将从定义场景和过渡资源开始,然后应用过渡,从而创建动画。以下步骤将指导你创建 XML 中的资源,因为这通常是推荐的。资源也可以通过代码创建,我们将在还有更多部分中讨论。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:TransitionAnimation。在目标 Android 设备对话框中,选择手机和平板选项,并选择 API 19(或更高)作为最小 SDK。当提示活动类型时,选择空活动。
如何操作...
创建资源文件和应用过渡动画的步骤如下:
-
按如下方式更改现有的
activity.main.xml布局文件:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Top" android:id="@+id/textViewTop" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Bottom" android:id="@+id/textViewBottom" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Go" android:id="@+id/button" android:layout_centerInParent="true" android:onClick="goAnimate"/> </RelativeLayout> -
使用以下 XML 创建一个名为
activity_main_end.xml的新布局文件:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Bottom" android:id="@+id/textViewBottom" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Top" android:id="@+id/textViewTop" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Go" android:id="@+id/button" android:layout_centerInParent="true"/> </RelativeLayout> -
创建一个新的过渡资源目录(文件 | 新建 | Android 资源目录,并选择过渡作为资源类型)。
-
在
res/transition文件夹中创建一个名为transition_move.xml的新文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <changeBounds /> -
使用以下代码添加
goAnimate()方法:public void goAnimate(View view) { ViewGroup root = (ViewGroup) findViewById(R.id.layout); Scene scene = Scene.getSceneForLayout(root, R.layout.activity_main_end, this); Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.transition_move); TransitionManager.go(scene, transition); } -
你现在可以运行应用程序在设备或模拟器上。
它是如何工作的...
你可能觉得代码本身相当简单。正如在菜谱介绍中概述的那样,我们只需要创建起始场景和结束场景,并设置过渡类型。以下是代码的详细分解。
创建起始场景
执行以下代码行将创建起始场景:
ViewGroup root = (ViewGroup) findViewById(R.id.layout);
创建过渡:
执行以下代码行将创建过渡:
Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.transition_move);
定义结束场景:
执行以下代码行将定义结束场景:
Scene scene = Scene.getSceneForLayout(root, R.layout.activity_main_end, this);
开始过渡:
执行以下代码行将开始过渡:
TransitionManager.go(scene, transition);
虽然简单,但这个菜谱的大部分工作都在于创建必要的资源文件。
还有更多...
现在,我们将查看如何仅使用代码创建相同的过渡动画(尽管我们仍然会使用初始的activity_main.xml布局文件):
ViewGroup root = (ViewGroup) findViewById(R.id.layout);
Scene scene = new Scene(root);
Transition transition = new ChangeBounds();
TransitionManager.beginDelayedTransition(root,transition);
TextView textViewTop = (TextView)findViewById(R.id.textViewTop);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)textViewTop.getLayoutParams();
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM,1);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, 0);
textViewTop.setLayoutParams(params);
TextView textViewBottom = (TextView)findViewById(R.id.textViewBottom);
params = (RelativeLayout.LayoutParams)textViewBottom.getLayoutParams();
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM,0);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, 1);
textViewBottom.setLayoutParams(params);
TransitionManager.go(scene);
我们仍然需要起始场景、结束场景和过渡;唯一的区别是我们如何创建资源。在之前的代码中,我们使用当前布局创建了起始场景。
在我们开始通过代码修改布局之前,我们使用过渡类型调用TransitionManager的beginDelayedTransition()方法。TransitionManager将跟踪结束场景的变化。当我们调用go()方法时,TransitionManager会自动动画化变化。
相关内容
使用传感器数据和 RotateAnimation 创建指南针
在上一章中,我们展示了从物理设备传感器读取传感器数据的方法。在那个配方中,我们使用了光传感器,因为环境传感器的数据通常不需要任何额外的处理。虽然获取磁场强度数据很容易,但这些数字本身并没有太多意义,当然也不会创建一个吸引人的显示。
在这个配方中,我们将展示如何获取磁场数据以及加速度计数据来计算磁北。我们将使用 SensorManager.getRotationMatrix 来在响应设备移动时动画化指南针。以下是我们在物理设备上的指南针应用程序的截图:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:Compass。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
我们需要一个图像来表示指南针指示器。同样,我们可以转向 www.Pixabay.Com 来获取图像。我们使用了以下图像:
pixabay.com/en/geography-map-compass-rose-plot-42608/
虽然不是必需的,但这个图像有一个透明的背景,当旋转图像时看起来更好。
如何操作...
如前一个 准备工作 部分所述,我们需要一个指南针的图像。你可以下载之前链接的图像,或者使用你喜欢的任何图像,然后按照以下步骤操作:
-
将你的图像复制到
res/drawable文件夹,并命名为compass.png。 -
打开
activity_main.xml并将现有的TextView替换为以下ImageView:<ImageView android:id="@+id/imageViewCompass" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="img/compass"/> -
现在打开
MainActivity.java并添加以下全局变量声明:private SensorManager mSensorManager; private Sensor mMagnetometer; private Sensor mAccelerometer; private ImageView mImageViewCompass; private float[] mGravityValues=new float[3]; private float[] mAccelerationValues=new float[3]; private float[] mRotationMatrix=new float[9]; private float mLastDirectionInDegrees = 0f; -
将以下
SensorEventListener类添加到MainActivity类中:private SensorEventListener mSensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { calculateCompassDirection(event); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { //Nothing to do } }; -
如下重写
onResume()和onPause():@Override protected void onResume() { super.onResume(); mSensorManager.registerListener(mSensorListener, mMagnetometer, SensorManager.SENSOR_DELAY_FASTEST); mSensorManager.registerListener(mSensorListener, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST); } @Override protected void onPause() { super.onPause(); mSensorManager.unregisterListener(mSensorListener); } -
将以下代码添加到现有的
onCreate()方法中:mImageViewCompass=(ImageView)findViewById(R.id.imageViewCompass); mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); -
最终的代码执行实际的计算和动画:
private void calculateCompassDirection(SensorEvent event) { switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: mAccelerationValues = event.values.clone(); break; case Sensor.TYPE_MAGNETIC_FIELD: mGravityValues = event.values.clone(); break; } boolean success = SensorManager.getRotationMatrix(mRotationMatrix, null, mAccelerationValues, mGravityValues); if(success){ float[] orientationValues = new float[3]; SensorManager.getOrientation(mRotationMatrix, orientationValues); float azimuth = (float)Math.toDegrees(-orientationValues[0]); RotateAnimation rotateAnimation = new RotateAnimation(mLastDirectionInDegrees, azimuth, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); rotateAnimation.setDuration(50); rotateAnimation.setFillAfter(true); mImageViewCompass.startAnimation(rotateAnimation); mLastDirectionInDegrees = azimuth; } -
你已经准备好运行应用程序了。虽然你可以在模拟器上运行这个应用程序,但没有加速度计和磁力计,你将看不到指南针移动。
工作原理...
由于我们已经在 读取传感器数据 – 使用 Android 传感器框架(来自上一章)中介绍了读取传感器数据,因此我们不会重复解释传感器框架,而是直接跳到 calculateCompassDirection() 方法。
我们直接从onSensorChanged()回调中调用此方法。由于我们使用同一个类来处理磁力计和加速度计的传感器回调,我们首先检查SensorEvent中报告的是哪个传感器。然后我们调用SensorManager.getRotationMatrix(),传入最后的传感器数据。如果计算成功,它将返回一个RotationMatrix,我们使用它来调用SensorManager.getOrientation()方法。getOrientation()将在orientationValues数组中返回以下数据:
-
方位角:
value [0] -
俯仰:
value [1] -
翻滚:
value [2]
方位角以弧度报告,方向相反,因此我们反转符号并使用Math.toDegrees()将其转换为度数。方位角表示北方的方向,所以我们使用它在我们的RotateAnimation中。
由于SensorManager已经完成了数学计算,实际的指南针动画非常简单。我们使用前一个方向和新方向创建一个RotateAnimation。我们使用Animation.RELATIVE_TO_SELF标志和 0.5f(或 50%)将图像的中心设置为旋转点。在调用startAnimation()更新指南针之前,我们使用setDuration()和setFillAfter(true)设置动画持续时间。(使用true表示我们希望在动画完成后将图像保留“原样”,否则图像将重置回原始图像。)最后,我们保存下一次传感器更新时的方位角。
更多内容...
值得花些时间实验RotationAnimation设置和传感器更新时间。在我们的注册传感器监听器的调用中,我们使用SensorManager.SENSOR_DELAY_FASTEST以及 50 毫秒的setDuration()来创建快速动画。你也可以尝试使用较慢的传感器更新和较慢的动画,并比较结果。
参见
-
在上一章中关于使用 Android 传感器框架读取传感器数据的详细说明。
-
请参考[getRotationMatrix() 开发者文档](http://developer.android.com/reference/android/hardware/SensorManager.html#getRotationMatrix(float[], float[], float[], float[]))
-
请参考[getOrientation() 开发者文档](http://developer.android.com/reference/android/hardware/SensorManager.html#getOrientation(float[], float[]))
使用 ViewPager 创建幻灯片
这个菜谱将展示如何使用 ViewPager 类创建幻灯片。以下是显示从一张图片切换到另一张图片的截图:

准备工作
在 Android Studio 中创建一个新的项目,命名为:SlideShow。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
我们需要为幻灯片准备几个图像。为了演示目的,我们从 www.Pixabay.com 下载了四张图像并将其包含在项目源文件中,但你可以使用任何图像。
如何做到这一点...
我们将创建一个 Fragment 来显示幻灯片中的每一张图片,然后在主活动中设置 ViewPager。以下是步骤:
-
将四个图像复制到
/res/drawable文件夹,并命名为slide_0至slide_3,保持它们的原始文件扩展名。 -
使用以下 XML 创建一个名为
fragment_slide.xml的新布局文件:<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageView" android:layout_gravity="center_horizontal" /> </LinearLayout> -
现在创建一个新的 Java 类
SlideFragment。它将继承Fragment,如下所示:public class SlideFragment extends Fragment {使用以下导入:
import android.support.v4.app.Fragment; -
添加以下全局声明:
private int mImageResourceID; -
添加以下空的默认片段构造函数:
public SlideFragment() {} -
添加以下方法以保存图像资源 ID:
public void setImage(int resourceID) { mImageResourceID=resourceID; } -
重写
onCreateView()如下:@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_slide, container, false); ImageView imageView = (ImageView)rootView.findViewById(R.id.imageView); imageView.setImageResource(mImageResourceID); return rootView; } -
我们的主活动将仅显示一个
ViewPager。打开activity_main.xml并替换文件内容如下:<android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" /> -
现在打开
MainActivity.java并将MainActivity改为继承FragmentActivity,如下所示:public class MainActivity extends FragmentActivity {使用以下导入:
import android.support.v4.app.FragmentActivity; -
添加以下全局声明:
private final int PAGE_COUNT=4; private ViewPager mViewPager; private PagerAdapter mPagerAdapter;使用以下导入:
import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; -
在
MainActivity中创建以下子类:private class SlideAdapter extends FragmentStatePagerAdapter { public SlideAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { SlideFragment slideFragment = new SlideFragment(); switch (position) { case 0: slideFragment.setImage(R.drawable.slide_0); break; case 1: slideFragment.setImage(R.drawable.slide_1); break; case 2: slideFragment.setImage(R.drawable.slide_2); break; case 3: slideFragment.setImage(R.drawable.slide_3); break; } return slideFragment; } @Override public int getCount() { return PAGE_COUNT; } }使用以下导入:
import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; -
重写
onBackPressed()如下:@Override public void onBackPressed() { if (mViewPager.getCurrentItem() == 0) { super.onBackPressed(); } else { mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1); } } -
将以下代码添加到
onCreate()方法中:mViewPager = (ViewPager) findViewById(R.id.viewPager); mPagerAdapter = new SlideAdapter(getSupportFragmentManager()); mViewPager.setAdapter(mPagerAdapter); -
在设备或模拟器上运行应用程序。
它是如何工作的...
第一步是创建一个 Fragment。由于我们正在进行幻灯片展示,我们只需要一个 ImageViewer。我们还把 MainActivity 改为继承 FragmentActivity 以将片段加载到 ViewPager 中。
ViewPager 使用 FragmentStatePagerAdapter 作为片段切换的来源。我们创建 SlideAdapter 来处理来自 FragmentStatePagerAdapter 类的两个回调:
-
getCount() -
getItem()
getCount() 简单地返回我们在幻灯片中拥有的页面数。getItem() 返回要显示的实际片段。这是我们指定要显示的图像的地方。正如你所见,添加或更改幻灯片会非常简单。
处理 Back 键不是 ViewPager 的要求,但它确实提供了更好的用户体验。onBackPressed() 减少当前页面直到达到第一页,然后它将 Back 键发送到超类,从而退出应用程序。
更多内容...
如示例所示,ViewPager 负责处理大部分工作,包括处理过渡动画。如果我们想自定义过渡,可以通过在 ViewPager.PageTransformer 接口上实现 transformPage() 回调来实现。(请参阅下一个菜谱以获取自定义动画的示例。)
创建设置向导
ViewPager 也可以用来创建设置向导。不是创建一个用于显示图像的单个片段,而是为向导的每个步骤创建一个片段,并在 getItem() 回调中返回适当的片段。
参考信息
-
请参阅Android ViewPager 文档中的Android ViewPager Documentation。
-
请参阅 Creating a custom Zoom Animation 菜单以获取创建自定义动画的示例。
使用片段创建卡片翻转动画
卡片翻转是一种常见的动画,我们将通过片段转换来演示。我们将使用两个不同的图像——一个用于正面,一个用于背面,以创建卡片翻转效果。我们需要四个动画资源:两个用于正面转换,两个用于背面转换,我们将使用 objectAnimator 在 XML 中定义它们。
下面是应用程序的截图,展示了正在运行的卡片翻转动画:

准备工作
在 Android Studio 中创建一个新的项目,命名为:CardFlip。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
对于游戏牌的正反两面图像,我们在 www.Pixabay.com 找到了以下图像:
-
[
]
-
[
]
如何实现...
我们需要两个片段——一个用于卡片的前面,另一个用于背面。每个片段将定义卡片的图像。然后我们需要四个动画文件来实现完整的卡片翻转效果。以下是正确设置项目结构和创建所需资源的步骤:
-
一旦你有卡片的前面和背面图像,将它们复制到
res/drawable文件夹中,命名为card_front.jpg和card_back.jpg(如果图像的文件扩展名不同,请保留原始文件扩展名)。 -
创建一个动画资源目录:
res/animator。(在 Android Studio 中,转到 File | New | Android resource directory。当 New Android Resource 对话框显示时,在 Resource Type 下拉菜单中选择animator。) -
在
res/animator目录下创建card_flip_left_enter.xml,使用以下 XML 格式:<set > <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:duration="0" /> <objectAnimator android:valueFrom="-180" android:valueTo="0" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full"/> <objectAnimator android:valueFrom="0.0" android:valueTo="1.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set> -
在
res/animator目录下创建card_flip_left_exit.xml,使用以下 XML 格式:<set > <objectAnimator android:valueFrom="0" android:valueTo="180" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full"/> <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set> -
在
res/animator目录下创建card_flip_right_enter.xml,使用以下 XML 格式:<set > <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:duration="0" /> <objectAnimator android:valueFrom="180" android:valueTo="0" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full" /> <objectAnimator android:valueFrom="0.0" android:valueTo="1.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set> -
在
res/animator中创建card_flip_right_exit.xml,使用以下 XML:<set > <objectAnimator android:valueFrom="0" android:valueTo="-180" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full" /> <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set> -
在
res/values中创建一个新的资源文件,命名为timing.xml,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <resources> <integer name="card_flip_duration_full">1000</integer> <integer name="card_flip_duration_half">500</integer> </resources> -
在
res/layout中创建一个新的文件,命名为fragment_card_front.xml,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="img/card_front" android:scaleType="centerCrop" /> -
在
res/layout中创建一个新的文件,命名为fragment_card_back.xml,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="img/card_back" android:scaleType="centerCrop" /> -
使用以下代码创建一个新的 Java 类
CardFrontFragment:public class CardFrontFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_front, container, false); } } -
使用以下代码创建一个新的 Java 类
CardBackFragment:public class CardBackFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_back, container, false); } } -
将现有的
activity_main.xml文件替换为以下 XML:<FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" /> -
打开
MainActivity.java并添加以下全局声明:boolean mShowingBack = false; -
将以下代码添加到现有的
onCreate()方法中:FrameLayout frameLayout = (FrameLayout)findViewById(R.id.frameLayout); frameLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { flipCard(); } }); if (savedInstanceState == null) { getFragmentManager() .beginTransaction() .add(R.id.frameLayout, new CardFrontFragment()) .commit(); } -
添加以下方法,它处理实际的片段转换:
private void flipCard() { if (mShowingBack) { mShowingBack = false; getFragmentManager().popBackStack(); } else { mShowingBack = true; getFragmentManager() .beginTransaction() .setCustomAnimations( R.animator.card_flip_right_enter, R.animator.card_flip_right_exit, R.animator.card_flip_left_enter, R.animator.card_flip_left_exit) .replace(R.id.frameLayout, new CardBackFragment()) .addToBackStack(null) .commit(); } } -
你已经准备好在设备或模拟器上运行应用程序。
工作原理...
创建卡片翻页的大部分工作在于设置资源。由于我们想要卡片的正反两面视图,我们创建了两个包含适当图像的片段。当卡片被按下时,我们调用flipCard()方法。实际的动画由setCustomAnimations()处理。这就是我们传入在 XML 中定义的四个动画资源的地方。正如你所见,Android 使这变得非常简单。
重要的是要注意,我们没有使用 Support Library Fragment Manager,因为支持库不支持objectAnimator。如果你想要支持预 Android 3.0,你需要包含旧的anim资源,并在运行时检查操作系统版本,或者直接在代码中创建动画资源。(参见下一个配方。)
相关内容
-
参考下一个配方,使用自定义转换创建缩放动画,以查看在代码中创建的动画资源示例
-
参考关于Integer Resource Type的网页
developer.android.com/guide/topics/resources/more-resources.html#Integer
使用自定义转换创建缩放动画
之前的配方,使用片段创建卡片翻页动画,演示了使用动画资源文件创建的转换动画。在这个配方中,我们将使用在代码中创建的动画资源创建缩放效果。应用程序显示缩略图图像,然后按下时扩展到放大图像。
以下图像包含三个截图,展示了正在进行的缩放动画:

准备工作
在 Android Studio 中创建一个新的项目,命名为<project name>。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
对于本配方所需的图像,我们从www.Pixabay.com下载了一张图片,并将其包含在项目源文件中,但你也可以使用任何图像。
如何实现...
一旦你的图像准备就绪,按照以下步骤操作:
-
将您的图像复制到
res/drawable文件夹,并命名为image.jpg(如果不是 jpeg 图像,则保留原始文件扩展名)。 -
现在打开
activity_main.xml并用以下内容替换现有的 XML:<?xml version="1.0" encoding="utf-8"?> <FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <ImageButton android:id="@+id/imageViewThumbnail" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:background="@android:color/transparent"/> </LinearLayout> <ImageView android:id="@+id/imageViewExpanded" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" /> </FrameLayout> -
现在打开
MainActivity.java并声明以下全局变量:private Animator mCurrentAnimator; private ImageView mImageViewExpanded; -
将我们在 缩小大图像以避免内存不足异常 菜单中创建的
loadSampledResource()方法添加到zoomFromThumbnail()方法中,以调整图像大小:public Bitmap loadSampledResource(int imageID, int targetHeight, int targetWidth) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), imageID, options); final int originalHeight = options.outHeight; final int originalWidth = options.outWidth; int inSampleSize = 1; while ((originalHeight / (inSampleSize *2)) > targetHeight && (originalWidth / (inSampleSize *2)) > targetWidth) { inSampleSize *= 2; } options.inSampleSize =inSampleSize; options.inJustDecodeBounds = false; return (BitmapFactory.decodeResource(getResources(), imageID, options)); } -
将以下代码添加到
onCreate()方法中:final ImageView imageViewThumbnail = (ImageView)findViewById(R.id.imageViewThumbnail); imageViewThumbnail.setImageBitmap(loadSampledResource(R.drawable.image, 100, 100)); imageViewThumbnail.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { zoomFromThumbnail((ImageView) view); } }); mImageViewExpanded = (ImageView) findViewById(R.id.imageViewExpanded); mImageViewExpanded.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mImageViewExpanded.setVisibility(View.GONE); mImageViewExpanded.setImageBitmap(null); imageViewThumbnail.setVisibility(View.VISIBLE); } }); -
添加以下
zoomFromThumbnail()方法,它处理实际的动画,稍后解释:private void zoomFromThumbnail(final ImageView imageViewThumb) { if (mCurrentAnimator != null) { mCurrentAnimator.cancel(); } final Rect startBounds = new Rect(); final Rect finalBounds = new Rect(); final Point globalOffset = new Point(); imageViewThumb.getGlobalVisibleRect(startBounds); findViewById(R.id.frameLayout).getGlobalVisibleRect(finalBounds, globalOffset); mImageViewExpanded.setImageBitmap(loadSampledResource(R.drawable.image, finalBounds.height(), finalBounds.width())); startBounds.offset(-globalOffset.x, -globalOffset.y); finalBounds.offset(-globalOffset.x, -globalOffset.y); float startScale; if ((float) finalBounds.width() / finalBounds.height() > (float) startBounds.width() / startBounds.height()) { startScale = (float) startBounds.height() / finalBounds.height(); float startWidth = startScale * finalBounds.width(); float deltaWidth = (startWidth - startBounds.width()) / 2; startBounds.left -= deltaWidth; startBounds.right += deltaWidth; } else { startScale = (float) startBounds.width() / finalBounds.width(); float startHeight = startScale * finalBounds.height(); float deltaHeight = (startHeight - startBounds.height()) / 2; startBounds.top -= deltaHeight; startBounds.bottom += deltaHeight; } imageViewThumb.setVisibility(View.GONE); mImageViewExpanded.setVisibility(View.VISIBLE); mImageViewExpanded.setPivotX(0f); mImageViewExpanded.setPivotY(0f); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(ObjectAnimator.ofFloat(mImageViewExpanded, View.X,startBounds.left, finalBounds.left)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.Y,startBounds.top, finalBounds.top)) .with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_X, startScale, 1f)) .with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_Y, startScale, 1f)); animatorSet.setDuration(1000); animatorSet.setInterpolator(new DecelerateInterpolator()); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCurrentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { mCurrentAnimator = null; } }); animatorSet.start(); mCurrentAnimator = animatorSet; } -
在设备或模拟器上运行应用程序。
它是如何工作的...
首先,看看我们使用的布局文件。有两个部分——包含缩略图 ImageView 的 LinearLayout 和扩展的 ImageView。我们控制这两个视图的可见性,当图像被点击时。我们使用与 缩小大图像以避免内存不足异常 菜单中讨论的相同 loadSampledResource() 方法设置起始缩略图图像。
zoomFromThumbnail() 是这个演示中实际工作的地方。有很多代码,分解如下。
首先,我们将当前动画存储在 mCurrentAnimator 中,这样我们就可以在动画正在运行时取消它。
接下来,我们使用 getGlobalVisibleRect() 方法获取图像的起始位置。这个方法返回视图的屏幕位置。当我们获取扩展的 ImageView 的可见边界时,我们也获取视图的 GlobalOffset,以将坐标从应用坐标转换为屏幕坐标。
设置起始边界后,下一步是计算结束边界。我们希望保持最终图像的相同宽高比,以防止其变形。我们需要计算边界需要如何调整,以保持宽高比在扩展的 ImageView 内。介绍中展示的截图显示了如何调整此图像的大小,但这一点会因图像和设备而异。
计算起始和结束边界后,我们现在可以创建动画——实际上,在这个例子中有四个动画。每个动画对应矩形的一个点,如代码所示:
animatorSet.play(ObjectAnimator.ofFloat(mImageViewExpanded, View.X,startBounds.left, finalBounds.left)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.Y,startBounds.top, finalBounds.top)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_X,startScale, 1f)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_Y, startScale, 1f));
这两行代码控制动画的显示方式:
animatorSet.setDuration(1000);
animatorSet.setInterpolator(new AccelerateInterpolator());
setDuration() 方法告诉动画器对象动画所需的时间。setInterpolator() 控制平移的方式。(在 介绍 中提到了 Interpolator,并且稍后会提供链接。)使用 start() 方法开始动画后,我们将当前动画保存到 mCurrentAnimator 变量中,这样如果需要的话可以取消动画。我们创建一个 AnimatorListenerAdapter 来响应动画事件,以清除 mCurrentAnimator 变量。
还有更多...
当用户按下展开的图像时,应用程序只是隐藏展开的ImageView并将缩略图设置为可见。我们可以在mImageViewExpanded点击事件中使用展开的边界作为起点,返回到缩略图边界来创建反向缩放动画。(在zoomFromThumbnail()中创建mImageViewExpanded事件可能更容易,这样可以避免再次重复计算起始和结束边界。)
获取默认动画持续时间
我们在设置持续时间时使用了 1000 毫秒,通过setDuration()方法。我们故意使用较长的持续时间以便更容易查看动画。我们可以使用以下代码获取默认的 Android 动画持续时间:
getResources().getInteger(android.R.integer.config_shortAnimTime)
参见
-
第一道菜谱,将大图像缩小以避免内存不足异常,对
loadSampledResource()方法的详细解释。 -
请参阅插值器开发者文档
第十章. 初识 OpenGL ES
在本章中,我们将涵盖以下主题:
-
设置 OpenGL ES 环境
-
在 GLSurfaceView 上绘制形状
-
在绘图时应用投影和相机视图
-
通过旋转移动三角形
-
根据用户输入旋转三角形
简介
正如我们在上一章中看到的,Android 提供了许多处理图形和动画的工具。尽管画布和可绘制对象是为自定义绘图设计的,但在你需要高性能图形,尤其是 3D 游戏图形时,Android 也支持 OpenGL ES。嵌入式系统开放图形库(OpenGL ES)针对嵌入式系统。(嵌入式系统包括游戏机和手机。)
本章旨在作为在 Android 上使用 OpenGL ES 的入门介绍。像往常一样,我们将提供步骤并解释其工作原理,但不会深入探讨 OpenGL 的数学或技术细节。如果你已经熟悉来自其他平台(如 iOS)的 OpenGL ES,本章应该能让你快速上手。如果你是 OpenGL 的新手,希望这些食谱能帮助你决定是否想在这个领域继续深入。
Android 支持以下版本的 OpenGL:
-
OpenGL ES 1.0:Android 1.0
-
OpenGL ES 2.0:在 Android 2.2(API 8)中引入
-
OpenGL ES 3.0:在 Android 4.3(API 18)中引入
-
OpenGL ES 3.1:在 Android 5.0(API 21)中引入
本章的食谱具有入门性质,针对 OpenGL ES 2.0 及以上版本。OpenGL ES 2.0 几乎适用于目前所有可用的设备。与 OpenGL ES 2.0 及以下版本不同,OpenGL 3.0 及以上版本需要硬件制造商提供驱动程序实现。这意味着,即使你的应用程序运行在 Android 5.0 上,OpenGL 3.0 及以上版本可能不可用。因此,在运行时检查可用的 OpenGL 版本是一种良好的编程实践。或者,如果你的应用程序需要 3.0 及以上版本的功能,你可以在 Android 清单中添加一个<uses-feature/>元素。(我们将在接下来的第一个食谱中讨论这个问题。)
与本书中的其他章节不同,本章更像是一个教程,每个食谱都是基于前一个食谱中学到的经验。每个食谱的“准备”部分将明确说明先决条件。
设置 OpenGL ES 环境
我们的第一道食谱将从展示如何设置一个活动来使用 OpenGL GLSurfaceView的步骤开始。类似于 Canvas,GLSurfaceView是你将执行 OpenGL 绘图的地方。由于这是起点,其他食谱在需要创建GLSurfaceView时,将把这个食谱作为基础步骤进行引用。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:SetupOpenGL。使用默认的手机和平板电脑选项,并在提示活动类型时选择空活动。
如何操作...
我们首先在 AndroidManifest 中指明应用程序使用 OpenGL,然后我们将 OpenGL 类添加到活动中。以下是步骤:
-
打开 AndroidManifest 并添加以下 XML:
<uses-feature android:glEsVersion="0x00020000" android:required="true" /> -
打开
MainActivity.java并添加以下全局变量:private GLSurfaceView mGLSurfaceView; -
将以下内部类添加到
MainActivity类中:class CustomGLSurfaceView extends GLSurfaceView { private final GLRenderer mGLRenderer; public CustomGLSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mGLRenderer = new GLRenderer(); setRenderer(mGLRenderer); } } -
将另一个内部类添加到
MainActivity类中:class GLRenderer implements GLSurfaceView.Renderer { public void onSurfaceCreated(GL10 unused, EGLConfig config) { GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); } public void onDrawFrame(GL10 unused) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); } public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height);} } -
将以下代码添加到现有的
onCreate()方法中:mGLSurfaceView = new CustomGLSurfaceView(this); setContentView(mGLSurfaceView); -
你现在可以运行应用程序在设备或模拟器上了。
它是如何工作的...
如果你运行了前面的应用程序,你应该看到了创建的活动和背景被设置为灰色。由于这些是设置 OpenGL 的基本步骤,你将在本章的其他菜谱中重用此代码。以下是详细解释的过程:
在 AndroidManifest 中声明 OpenGL
我们首先通过以下行在 AndroidManifest 中声明我们使用 OpenGL ES 版本 2.0 的需求:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果我们使用版本 3.0,我们会使用这个:
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
对于版本 3.1,使用这个:
<uses-feature android:glEsVersion="0x00030001" android:required="true" />
扩展 GLSurfaceView 类
通过扩展GLSurfaceView创建一个自定义的 OpenGL SurfaceView类,就像我们在代码中所做的那样:
class CustomGLSurfaceView extends GLSurfaceView {
private final GLRenderer mGLRenderer;
public CustomGLSurfaceView(Context context){
super(context);
setEGLContextClientVersion(2);
mGLRenderer = new GLRenderer();
setRenderer(mGLRenderer);
}
}
在这里,我们实例化一个 OpenGL 渲染类,并通过setRenderer()方法将其传递给GLSurfaceView类。OpenGL SurfaceView为我们提供了 OpenGL 绘图的表面,类似于Canvas和SurfaceView对象。实际的绘图是在Renderer中完成的,我们将在下一步创建它:
创建一个 OpenGL 渲染类
最后一步是创建GLSurfaceView.Renderer类并实现以下三个回调:
-
onSurfaceCreated() -
onDrawFrame() -
onSurfaceChanged()
这里是代码:
class GLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
目前,我们使用这个类所做的只是设置回调和清除屏幕,使用我们通过glClearColor()指定的颜色(在这种情况下是灰色)。
还有更多...
在设置好 OpenGL 环境后,我们将继续到下一个菜谱,我们将实际在视图中绘制。
在 GLSurfaceView 上绘制形状
前一个菜谱设置了活动以使用 OpenGL。这个菜谱将继续展示如何在OpenGLSurfaceView上绘制。
首先,我们需要定义形状。使用 OpenGL 时,重要的是要意识到形状顶点的定义顺序非常重要,因为它们决定了形状的前面(面)和背面。通常(并且是默认行为)是逆时针定义顶点。(尽管这种行为可以改变,但它需要额外的代码,并且不是标准实践。)
理解 OpenGL 屏幕坐标系也很重要,因为它与 Android 画布不同。默认坐标系定义(0,0,0)为屏幕中心。四个边缘点如下:
-
左上角:(-1.0, 1.0, 0)
-
右上角:(1.0, 1.0, 0)
-
左下角:(-1.0, -1.0, 0)
-
右下角:(1.0, -1.0, 0)
Z轴直接从屏幕中出来或直接在屏幕后面。
这里是一个显示X,Y和Z轴的插图:

我们将创建一个Triangle类,因为它是基本形状。在 OpenGL 中,通常使用三角形的集合来创建对象。要使用 OpenGL 绘制形状,我们需要定义以下内容:
-
顶点着色器:这是用来绘制形状的
-
片段着色器:这是用来着色形状的
-
程序:这是用于前面着色器的 OpenGL ES 对象
着色器使用OpenGL 着色语言(GLSL)定义,然后编译并添加到 OpenGL 程序对象中。
这里有两张截图显示了肖像和横幅方向下的三角形:


准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:ShapesWithOpenGL。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
本菜谱使用前一个菜谱中创建的 OpenGL 环境设置 OpenGL 环境。如果你还没有完成这些步骤,请参考前一个菜谱。
如何操作...
如前所述,我们将使用前一个菜谱中创建的 OpenGL 环境。以下步骤将指导你创建一个用于三角形形状的类,并在 GLSurfaceView 上绘制它:
-
创建一个名为
Triangle的新 Java 类。 -
将以下全局声明添加到
Triangle类中:private final String vertexShaderCode ="attribute vec4 vPosition;" +"void main() {" +" gl_Position = vPosition;" +"}"; private final String fragmentShaderCode ="precision mediump float;" +"uniform vec4 vColor;" +"void main() {" +" gl_FragColor = vColor;" +"}"; final int COORDS_PER_VERTEX = 3; float triangleCoords[] = { 0.0f, 0.66f, 0.0f, -0.5f, -0.33f, 0.0f, 0.5f, -0.33f, 0.0f }; float color[] = { 0.63f, 0.76f, 0.22f, 1.0f }; private final int mProgram; private FloatBuffer vertexBuffer; private int mPositionHandle; private int mColorHandle; private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX; private final int vertexStride = COORDS_PER_VERTEX * 4; -
将以下
loadShader()方法添加到Triangle类中:public int loadShader(int type, String shaderCode){ int shader = GLES20.glCreateShader(type); GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); return shader; } -
按照以下方式添加
Triangle构造函数:public Triangle() { int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode); mProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(mProgram, vertexShader); GLES20.glAttachShader(mProgram, fragmentShader); GLES20.glLinkProgram(mProgram); ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(triangleCoords); vertexBuffer.position(0); } -
按照以下方式添加
draw()方法:public void draw() { GLES20.glUseProgram(mProgram); mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,GLES20.GL_FLOAT, false,vertexStride, vertexBuffer); mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor"); GLES20.glUniform4fv(mColorHandle, 1, color, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount); GLES20.glDisableVertexAttribArray(mPositionHandle); } -
现在打开
MainActivity.java文件,并在GLRenderer类中添加一个Triangle变量,如下所示:private Triangle mTriangle; -
在
onSurfaceCreated()回调中初始化Triangle变量,如下所示:mTriangle = new Triangle(); -
在
onDrawFrame()回调中调用draw()方法:mTriangle.draw(); -
你已经准备好在设备或模拟器上运行应用程序了。
它是如何工作的...
如介绍中所述,要使用 OpenGL 绘制,我们首先必须定义着色器,这可以通过以下代码完成:
private final String vertexShaderCode ="attribute vec4 vPosition;" +"void main() {" +" gl_Position = vPosition;" +"}";
private final String fragmentShaderCode ="precision mediump float;" +"uniform vec4 vColor;" +"void main() {" +" gl_FragColor = vColor;" +"}";
由于这是未编译的OpenGL 着色语言(OpenGLSL),下一步是将它编译并附加到我们的 OpenGL 对象上,我们使用以下两个 OpenGL ES 方法来完成:
-
glAttachShader() -
glLinkProgram()
在设置好着色器后,我们创建ByteBuffer来存储三角形顶点,这些顶点在triangleCoords中定义。draw()方法是在onDrawFrame()回调中调用 GLES20 库调用进行实际绘制的位置。
还有更多...
你可能已经注意到,从介绍中的截图来看,肖像和横幅模式下的三角形看起来确实相同。正如你从代码中看到的,我们在绘制时没有对方向进行区分。我们将解释为什么会发生这种情况,并展示如何在下一个菜谱中纠正这个问题。
参见
关于 OpenGL 着色语言(OpenGL Shading Language)的更多信息,请参考以下链接:
www.opengl.org/documentation/glsl/
绘制时应用投影和相机视图
如前一个配方中看到的,当我们将形状绘制到屏幕上时,形状会因为屏幕方向而倾斜。原因是默认情况下,OpenGL 假设屏幕是完美的正方形。我们之前提到,默认屏幕坐标中右上角是 (1,1,0),左下角是 (-1,-1,0)。
由于大多数设备屏幕不是完美的正方形,我们需要将显示坐标映射到与我们的物理设备匹配。在 OpenGL 中,我们通过 投影 来做这件事。这个配方将展示如何使用投影来匹配 GLSurfaceView 坐标与设备坐标。除了投影之外,我们还将展示如何设置相机视图。以下是显示最终结果的截图:

准备中
在 Android Studio 中创建一个新的项目,并将其命名为:ProjectionAndCamera。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
这个配方建立在之前的配方 在 GLSurfaceView 上绘制形状 之上。如果您还没有输入之前的配方,请在开始这些步骤之前这样做。
如何做到这一点...
如前所述,这个配方将建立在之前的配方之上,所以在开始之前完成这些步骤。我们将修改之前的代码以添加投影和相机视图到绘制计算中。以下是步骤:
-
打开
Triangle类,并在现有声明中添加以下全局声明:private int mMVPMatrixHandle; -
在
vertexShaderCode中添加一个矩阵变量,并在位置计算中使用它。以下是最终结果:private final String vertexShaderCode = "attribute vec4 vPosition;" + "uniform mat4 uMVPMatrix;" + "void main() {" + " gl_Position = uMVPMatrix * vPosition;" + "}"; -
将
draw()方法修改为传入一个矩阵参数,如下所示:public void draw(float[] mvpMatrix) { -
要使用变换矩阵,请在
draw()方法中在GLES20.glDrawArrays()方法之前添加以下代码:mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0); -
打开
MainActivity.java并将以下类变量添加到GLRenderer类中:private final float[] mMVPMatrix = new float[16]; private final float[] mProjectionMatrix = new float[16]; private final float[] mViewMatrix = new float[16]; -
修改
onSurfaceChanged()回调以计算位置矩阵如下:public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height); float ratio = (float) width / height; Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7); } -
修改
onDrawFrame()回调以计算相机视图如下:public void onDrawFrame(GL10 unused) { Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f); Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); mTriangle.draw(mMVPMatrix); } -
您现在可以运行应用程序在设备或模拟器上。
它是如何工作的...
首先,我们修改 vertexShaderCode 以包含一个矩阵变量。我们使用传入的参数高度和宽度在 onSurfaceChanged() 回调中计算矩阵。我们将变换矩阵传递给 draw() 方法,以便在计算绘制位置时使用。
在我们调用 draw() 方法之前,我们计算相机视图。这两行代码计算相机视图:
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
没有这段代码,实际上不会绘制三角形,因为相机视角不会“看到”我们的顶点。(这回到了我们之前关于顶点顺序决定图像前后关系的讨论。)
当您现在运行程序时,您将看到 简介 中显示的输出。注意我们现在有一个均匀的三角形,即使显示被旋转。
更多...
在下一个配方中,我们将开始展示通过旋转三角形 OpenGL 的功能。
通过旋转移动三角形
我们到目前为止用 OpenGL 展示的内容可能使用传统的画布或可绘制对象会更简单。此配方将通过旋转三角形展示 OpenGL 的一些功能。虽然我们也可以用其他绘图方法创建运动,但使用 OpenGL 来做这一点是多么容易!
此配方将演示如何旋转三角形,如此截图所示:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:CreatingMovement。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
此配方基于之前的配方 在绘图时应用投影和相机视图。如果您还没有输入之前的配方,请在继续之前这样做。
如何操作...
由于我们是从上一个配方继续的,所以我们几乎没有工作要做。打开 MainActivity.java 并按照以下步骤操作:
-
在
GLRendered类中添加一个矩阵:private float[] mRotationMatrix = new float[16]; -
在
onDrawFrame()回调中,将现有的mTriangle.draw(mMVPMatrix);语句替换为以下代码:float[] tempMatrix = new float[16]; long time = SystemClock.uptimeMillis() % 4000L; float angle = 0.090f * ((int) time); Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f); Matrix.multiplyMM(tempMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0); mTriangle.draw(tempMatrix); -
您现在可以运行应用程序在设备或模拟器上。
它是如何工作的...
我们使用 Matrix.setRotateM() 方法根据我们传递的角度计算一个新的旋转矩阵。在这个例子中,我们使用系统运行时间来计算一个角度。我们可以使用任何我们想要的方法来推导一个角度,例如传感器读取或触摸事件。
更多...
使用系统时钟提供了创建连续运动的额外好处,这对于演示目的当然看起来更好。下一个配方将演示如何使用用户输入来推导旋转三角形的角。
渲染模式
OpenGL 提供了一个 setRenderMode() 选项,仅在视图脏时绘制。这可以通过在 CustomGLSurfaceView() 构造函数中添加以下代码来实现,位于 setRenderer() 调用下方:
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
这将导致显示更新一次,然后等待我们使用 requestRender() 请求更新。
通过用户输入旋转三角形
之前的例子演示了根据系统时钟旋转三角形。这创建了一个根据我们使用的渲染模式连续旋转的三角形。但如果你想要响应用户的输入怎么办?
在这个配方中,我们将展示如何通过覆盖 GLSurfaceView 的 onTouchEvent() 回调来响应用户输入。我们仍然会使用 Matrix.setRotateM() 方法来旋转三角形,但不是从系统时间推导角度,而是基于触摸位置计算角度。
下面是一张显示此配方在物理设备上运行的截图(为了突出触摸,已启用显示触摸开发者选项):

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:RotateWithUserInput。使用默认的 手机和平板 选项,并在提示 活动类型 时选择 空活动。
此配方演示了与先前配方不同的替代方法,因此将基于 在绘制时应用投影和相机视图(与先前配方相同的起点)。
如何做到...
如前所述,我们将继续,而不是从先前的配方开始,而是从 在绘制时应用投影和相机视图 配方开始。打开 MainActivity.java 并按照以下步骤操作:
-
将以下全局变量添加到
MainActivity类中:private float mCenterX=0; private float mCenterY=0; -
将以下代码添加到
GLRendered类中:private float[] mRotationMatrix = new float[16]; public volatile float mAngle; public void setAngle(float angle) { mAngle = angle; } -
在同一类中,通过将现有的
mTriangle.draw(mMVPMatrix);语句替换为以下代码来修改onDrawFrame()方法:float[] tempMatrix = new float[16]; Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f); Matrix.multiplyMM(tempMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0); mTriangle.draw(tempMatrix); -
将以下代码添加到
onSurfaceChanged()回调中:mCenterX=width/2; mCenterY=height/2; -
将以下代码添加到
CustomGLSurfaceView构造函数中,该构造函数位于setRenderer()下方:setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); -
将以下
onTouchEvent()添加到CustomGLSurfaceView类中:@Override public boolean onTouchEvent(MotionEvent e) { float x = e.getX(); float y = e.getY(); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: double angleRadians = Math.atan2(y-mCenterY,x-mCenterX); mGLRenderer.setAngle((float)Math.toDegrees(-angleRadians)); requestRender(); } return true; } -
您现在可以在设备或模拟器上运行应用程序。
它是如何工作的...
与先前的配方相比,此示例的明显区别在于我们如何推导出传递给 Matrix.setRotateM() 调用的角度。我们还使用 setRenderMode() 改变了 GLSurfaceView 的渲染模式,使其仅在请求时绘制。我们在 onTouchEvent() 回调中计算了一个新角度后,使用 requestRender() 发出了请求。
我们还展示了自定义 GLSurfaceView 类的重要性。如果没有我们的 CustomGLSurfaceView 类,我们就无法覆盖 onTouchEvent 回调,或从 GLSurfaceView 的任何其他回调。
还有更多...
这就结束了 OpenGL ES 配方,但我们只是刚刚触及 OpenGL 的强大功能。如果您认真想学习 OpenGL,请查看下一节中的链接,并查看关于 OpenGL 的许多书籍之一。
值得注意的是,检查可用的许多框架之一,例如虚幻引擎:
小贴士
虚幻引擎 4 是由游戏开发者为游戏开发者制作的完整游戏开发工具套件。
www.unrealengine.com/what-is-unreal-engine-4
相关内容
-
OpenGL: 高性能图形的行业标准
-
OpenGL ES: 嵌入式加速 3D 图形的行业标准
-
虚幻引擎: Android 快速入门
docs.unrealengine.com/latest/INT/Platforms/Android/GettingStarted/index.html
第十一章. 多媒体
在本章中,我们将涵盖以下主题:
-
使用 SoundPool 播放声音效果
-
使用 MediaPlayer 播放音频
-
在您的应用程序中响应用户媒体控制
-
使用默认相机应用程序拍照
-
使用(旧)Camera API 拍照
-
使用 Camera2(新)API 拍照
简介
现在我们已经在前几章中探讨了图形和动画,现在是时候看看 Android 中可用的声音选项了。播放声音最流行的两种选项包括:
-
SoundPool:这是用于短声音剪辑的
-
MediaPlayer:这是为较大的声音文件(如音乐)和视频文件设计的
前两个食谱将探讨如何使用这些库。我们还将探讨如何使用与声音相关的硬件,例如音量控制媒体播放控制(通常在耳机上找到的播放、暂停等)。
本章的其余部分将专注于使用相机,无论是通过 Intents 间接(将相机请求传递到默认相机应用程序)还是直接使用相机 API。我们将检查与 Android 5.0 Lollipop(API 21)一起发布的新的 Camera2 API,但也会查看原始的 Camera API,因为大约 75%的市场还没有 Lollipop。(为了帮助您利用 Camera2 API 提供的新功能,我们将展示一种使用旧 Camera API 的新方法,以便更容易地在您的应用程序中使用这两个 Camera API。)
使用 SoundPool 播放声音效果
当您在应用程序中需要声音效果时,SoundPool 通常是一个好的起点。
SoundPool 很有趣,因为它允许我们通过改变播放速率以及允许同时播放多个声音来创建特殊效果。
支持的流行音频文件类型包括:
-
3GPP (
.3gp) -
3GPP (
.3gp) -
FLAC (
.flac) -
MP3 (
.mp3) -
MIDI 类型 0 和 1(
.mid,.xmf和.mxmf) -
Ogg (
.ogg) -
WAVE (
.wav)
请参阅支持的媒体格式链接以获取完整列表,包括网络协议。
在 Android 中很常见,新版本的操作系统会带来 API 的变化。SoundPool也不例外,原始的SoundPool构造函数在 Lollipop(API 21)中被弃用。我们不会将我们的最小 API 设置为 21 或依赖于弃用的代码(这可能在某个时刻停止工作),我们将实现旧的和新的方法,并在运行时检查操作系统版本以使用适当的方法。
本食谱将演示如何使用 Android SoundPool库播放声音效果。为了演示同时播放声音,我们将创建两个按钮,每个按钮在被按下时都会播放一个声音。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:SoundPool。使用默认的手机和平板电脑选项,并在提示活动类型时选择空活动。
为了演示同时播放声音,我们需要在项目中至少包含两个音频文件。我们去了 SoundBible.com(soundbible.com/royalty-free-sounds-5.html ) 并找到了两个免费公共领域声音,包括在下载项目文件中:
第一个声音是较长时间播放的声音:
soundbible.com/2032-Water.html
第二个声音较短:
soundbible.com/1615-Metal-Drop.html
如何做到...
如前所述,我们需要在项目中包含两个音频文件。一旦你准备好了音效文件,请按照以下步骤操作:
-
创建一个新的原始文件夹(文件 | 新建 | Android 资源目录)并在 资源类型 下拉菜单中选择
raw。 -
将你的音效文件复制到
res/raw目录下,命名为sound_1和sound_2。 (保留它们的原始扩展名。) -
打开
activity_main.xml并将现有的TextView替换为以下按钮:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play Sound 1" android:id="@+id/button1" android:layout_centerInParent="true" android:onClick="playSound1"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play Sound 2" android:id="@+id/button2" android:layout_below="@+id/button1" android:layout_centerHorizontal="true" android:onClick="playSound2"/> -
现在打开
ActivityMain.java并添加以下全局变量:HashMap<Integer, Integer> mHashMap= null; SoundPool mSoundPool; -
修改现有的
onCreate()方法,如下所示:final Button button1=(Button)findViewById(R.id.button1); button1.setEnabled(false); final Button button2=(Button)findViewById(R.id.button2); button2.setEnabled(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { createSoundPoolNew(); }else{ createSoundPooolOld(); } mSoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { button1.setEnabled(true); button2.setEnabled(true); } }); mHashMap = new HashMap<>(); mHashMap.put(1, mSoundPool.load(this, R.raw.sound_1, 1)); mHashMap.put(2, mSoundPool.load(this, R.raw.sound_2, 1)); -
添加
createSoundPoolNew()方法:@TargetApi(Build.VERSION_CODES.LOLLIPOP) private void createSoundPoolNew() { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); mSoundPool = new SoundPool.Builder() .setAudioAttributes(audioAttributes) .setMaxStreams(2) .build(); } -
添加
createSoundPooolOld()方法:@SuppressWarnings("deprecation") private void createSoundPooolOld(){ mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0); } -
添加按钮的
onClick()方法:public void playSound1(View view){ mSoundPool.play(mHashMap.get(1), 0.1f, 0.1f, 1, 0, 1.0f); } public void playSound2(View view){ mSoundPool.play(mHashMap.get(2), 0.9f, 0.9f, 1, 1, 1.0f); } -
重写
onStop()回调,如下所示:protected void onStop() { super.onStop(); mSoundPool.release(); } -
在设备或模拟器上运行应用程序。
它是如何工作的...
首先要注意的细节是如何构建这个对象本身。正如我们在介绍中提到的,SoundPool 构造函数在 Lollipop (API 21) 中发生了变化。旧的构造函数已被弃用,转而使用 SoundPool.Builder()。在像 Android 这样不断变化的环境中,API 的变化非常常见,因此学习如何应对这些变化是个好主意。正如你所见,在这种情况下并不困难。我们只需检查当前的操作系统版本并调用相应的方法。值得注意的是方法注释:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
以及:
@SuppressWarnings("deprecation")
在创建 SoundPool 之后,我们设置了一个 setOnLoadCompleteListener() 监听器。启用按钮主要是为了演示目的,以说明 SoundPool 在可用之前需要加载音效资源。
在使用 SoundPool 时,最后一点是调用 play()。我们需要传递 soundID,这是我们在使用 load() 加载声音时返回的。Play() 给我们一些选项,包括声音音量(左右)、循环次数和播放速率。为了展示灵活性,我们将第一个声音(较长的声音)以较低的音量播放,以创造更多的背景效果。第二个声音以较高的音量播放,并且我们播放了两次。
还有更多...
如果你只需要一个基本的音效,例如点击声,你可以使用 AudioManager 的 playSoundEffect() 方法。以下是一个示例:
AudioManager audioManager =(AudioManager)
this.getSystemService(Context.AUDIO_SERVICE);
audioManager.playSoundEffect(SoundEffectConstants.CLICK);
你只能指定来自 SoundEffectConstants 的声音;你不能使用自己的音效文件。
参见
-
开发者文档:SoundPool
developer.android.com/reference/android/media/SoundPool.html -
开发者文档:
developer.android.com/reference/android/media/AudioManager.html
使用 MediaPlayer 播放音频
MediaPlayer 可能是为你的应用程序添加多媒体功能最重要的类之一。它支持以下媒体源:
-
项目资源
-
本地文件
-
外部资源(例如 URL,包括流媒体)
MediaPlayer 支持以下流行的音频文件:
-
3GPP (
.3gp) -
3GPP (
.3gp) -
FLAC (
.flac) -
MP3 (
.mp3) -
MIDI 类型 0 和 1 (
.mid,.xmf, 和.mxmf) -
Ogg (
.ogg) -
WAVE (
.wav)
以及以下流行的文件类型:
-
3GPP (
.3gp) -
Matroska (
.mkv) -
WebM (
.webm) -
MPEG-4 (
.mp4,.m4a)
请参阅支持的媒体格式链接,获取完整列表,包括网络协议。
本教程将演示如何在你的应用程序中设置 MediaPlayer 以播放项目中的声音。(有关 MediaPlayer 提供的完整功能的详细说明,请参阅本教程末尾的开发者文档链接。)
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:MediaPlayer。使用默认的手机和平板电脑选项,并在提示活动类型时选择空活动。
我们还需要为这个教程准备一个声音文件,并将使用之前教程中使用的相同较长的“水”声音。
第一个声音是一个较长的播放声音:soundbible.com/2032-Water.html
如何实现...
如前所述,我们需要一个声音文件包含在项目中。一旦你准备好了你的声音文件,请按照以下步骤操作:
-
创建一个新的 raw 文件夹(文件 | 新建 | Android 资源目录),并在资源类型下拉菜单中选择
raw。 -
将你的声音文件复制到
res/raw目录下,命名为sound_1。(保留原始扩展名。) -
打开
activity_main.xml文件,并用以下按钮替换现有的TextView:<Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Play" android:id="@+id/buttonPlay" android:layout_above="@+id/buttonPause" android:layout_centerHorizontal="true" android:onClick="buttonPlay" /> <Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Pause" android:id="@+id/buttonPause" android:layout_centerInParent="true" android:onClick="buttonPause"/> <Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Stop" android:id="@+id/buttonStop" android:layout_below="@+id/buttonPause" android:layout_centerHorizontal="true" android:onClick="buttonStop"/> -
现在打开
ActivityMain.java文件,并添加以下全局变量:MediaPlayer mMediaPlayer; -
添加
buttonPlay()方法:public void buttonPlay(View view){ if (mMediaPlayer==null) { mMediaPlayer = MediaPlayer.create(this, R.raw.sound_1); mMediaPlayer.setLooping(true); mMediaPlayer.start(); } else { mMediaPlayer.start(); } } -
添加
buttonPause()方法:public void buttonPause(View view){ if (mMediaPlayer!=null && mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); } } -
添加
buttonStop()方法:public void buttonStop(View view){ if (mMediaPlayer!=null) { mMediaPlayer.stop(); mMediaPlayer.release(); mMediaPlayer = null; } } -
最后,使用以下代码覆盖
onStop()回调:protected void onStop() { super.onStop(); if (mMediaPlayer!=null) { mMediaPlayer.release(); mMediaPlayer = null; } } -
你现在可以在设备或模拟器上运行应用程序了。
工作原理...
这里的代码相当简单。我们使用我们的声音创建 MediaPlayer 并开始播放声音。按钮将依次重播、暂停和停止。
即使这个基本示例也说明了关于 MediaPlayer 的一个非常重要的概念,那就是状态。如果你正在认真使用 MediaPlayer,请查看以下提供的链接以获取详细信息。
更多内容...
为了使我们的演示更容易理解,我们使用 UI 线程进行所有操作。对于这个例子,使用项目附带的一个短音频文件,我们不太可能遇到任何 UI 延迟。通常,在准备 MediaPlayer 时使用后台线程是一个好主意。为了使这个常见任务更容易,MediaPlayer 已经包含了一个异步准备方法,称为prepareAsync()。以下代码将创建一个OnPreparedListener()监听器并使用prepareAsync()方法:
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
}
});
try {
mMediaPlayer.setDataSource(*//*URI, URL or path here*//*));
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.prepareAsync();
在后台播放音乐
我们的例子旨在在应用处于前台时播放音频,并在onStop()回调中释放 MediaPlayer 资源。如果你正在创建音乐播放器,并想在用户使用其他应用时在后台播放音乐,那会怎样?在这种情况下,你将需要在服务中使用 MediaPlayer 而不是 Activity。你将以相同的方式使用 MediaPlayer 库;你只需要从 UI 传递信息(如声音选择)到你的服务。
注意
注意,由于服务在同一个 UI 线程中运行,因此你仍然不希望在服务中执行可能阻塞的操作。MediaPlayer 可以处理后台线程以防止阻塞 UI 线程,否则,你可能需要自己进行线程处理。(有关线程和选项的更多信息,请参阅第十四章,为应用商店准备你的应用。)
使用硬件音量键控制你应用的音频音量
如果你希望音量控制控制你应用中的音量,请使用setVolumeControlStream()方法指定你应用的音频流,如下所示:
setVolumeControlStream(AudioManager.STREAM_MUSIC);
参见以下AudioManager链接以获取其他流选项。
参见
-
支持的媒体格式
developer.android.com/guide/appendix/media-formats.html -
开发者文档:MediaPlayer
developer.android.com/reference/android/media/MediaPlayer.html -
开发者文档:AudioManager
developer.android.com/reference/android/media/AudioManager.html
在你的应用中响应用户媒体控制
你的应用响应用户媒体控制,如播放、暂停、跳过等,是用户会欣赏的贴心功能。
Android 通过媒体库实现了这一点。与之前提到的使用 SoundPool 播放声音效果配方一样,Lollipop 版本改变了这样做的方式。与SoundPool示例不同,这个配方能够利用另一种方法——兼容性库。
这个菜谱将向你展示如何设置MediaSession以响应硬件按钮,这将适用于 Lollipop 及以后的版本,以及使用MediaSessionCompat库的以前的Lollilop版本。(兼容性库将负责检查操作系统版本并自动使用正确的 API 调用。)
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:HardwareMediaControls。使用默认的手机和平板电脑选项,并在提示活动类型时选择空活动。
如何操作...
我们将仅使用 Toast 消息来响应硬件事件,因此不需要对活动布局进行任何更改。要开始,请打开ActivityMain.java并按照以下步骤操作:
-
创建以下
mMediaSessionCallback以响应媒体按钮:MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() { @Override public void onPlay() { super.onPlay(); Toast.makeText(MainActivity.this, "onPlay()", Toast.LENGTH_SHORT).show(); } @Override public void onPause() { super.onPause(); Toast.makeText(MainActivity.this, "onPause()", Toast.LENGTH_SHORT).show(); } @Override public void onSkipToNext() { super.onSkipToNext(); Toast.makeText(MainActivity.this, "onSkipToNext()", Toast.LENGTH_SHORT).show(); } @Override public void onSkipToPrevious() { super.onSkipToPrevious(); Toast.makeText(MainActivity.this, "onSkipToPrevious()", Toast.LENGTH_SHORT).show(); } }; -
将以下代码添加到现有的
onCreate()回调中:MediaSessionCompat mediaSession = new MediaSessionCompat(this, getApplication().getPackageName()); mediaSession.setCallback(mMediaSessionCallback); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS); mediaSession.setActive(true); PlaybackStateCompat state = new PlaybackStateCompat.Builder() .setActions( PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS).build(); mediaSession.setPlaybackState(state); -
在具有媒体控制功能(如耳机)的设备或模拟器上运行应用程序,以查看 Toast 消息。
它是如何工作的...
设置此功能有四个步骤:
-
创建一个
MediaSession.Callback并将其附加到 MediaSession -
将媒体会话标志设置为指示我们想要媒体按钮
-
将
SessionState设置为active -
使用我们将要处理的操作设置
PlayBackState
步骤 4 和 1 协同工作,因为回调将只获取在PlayBackState中设置的的事件。
由于我们在这个菜谱中实际上并没有控制任何播放,所以我们只是演示如何响应硬件事件。你将需要在PlayBackState中实现实际的功能,并在调用setActions()之后调用setState()。
这是一个非常棒的示例,说明了 API 的更改如何使事情变得更容易。由于新的MediaSession和PlaybackState被整合到兼容性库中,我们可以在旧版本的操作系统上利用这些新 API。
更多...
检查正在使用的硬件
如果你想你的应用程序根据当前输出硬件以不同的方式响应,你可以使用AudioManager来检查。以下是一个示例:
AudioManager audioManager =(AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
if (audioManager.isBluetoothA2dpOn()) {
// Adjust output for Bluetooth.
} else if (audioManager.isSpeakerphoneOn()) {
// Adjust output for Speakerphone.
} else if (audioManager.isWiredHeadsetOn()) {
//Only checks if a wired headset is plugged in
//May not be the audio output
} else {
// Regular speakers?
}
参见
-
开发者文档:MediaSession
developer.android.com/reference/android/media/session/MediaSession.html -
开发者文档:MediaSessionCompat
developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html -
开发者文档:PlaybackState
developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat.html -
开发者文档:PlaybackStateCompat
developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat.html
使用默认相机应用拍照
如果你的应用程序需要从相机获取图像,但不是相机替换应用,可能最好允许“默认”相机应用拍照。这也尊重了用户对首选相机应用的偏好。
当你拍照时,除非它仅针对你的应用程序,否则将其公开是良好的实践。(这允许它被包含在用户的照片库中。)这个菜谱将演示使用默认的拍照应用拍照,将其保存到公共文件夹,并显示图像。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:UsingTheDefaultCameraApp。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
我们将创建一个包含 ImageView 和按钮的布局。按钮将创建一个 Intent 来启动默认的相机应用。当相机应用完成后,我们的应用将收到回调。首先打开 Android Manifest 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> -
打开
activity_main.xml文件,并用以下视图替换现有的TextView:<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageView" android:src="img/ic_launcher" android:layout_centerInParent="true"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/> -
打开
MainActivity.java并将以下全局变量添加到MainActivity类中:final int PHOTO_RESULT=1; private Uri mLastPhotoURI=null; -
添加以下方法来创建照片的 URI:
private Uri createFileURI() { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; return Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName)); } -
添加以下方法来处理按钮点击:
public void takePicture(View view) { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_ CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { mLastPhotoURI = createFileURI(); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI); startActivityForResult(takePictureIntent, PHOTO_RESULT); } } -
添加一个新的方法来覆盖
onActivityResult(),如下所示:@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PHOTO_RESULT && resultCode == RESULT_OK ) { mImageView.setImageBitmap(BitmapFactory.decodeFile(mLastPhotoURI.getPath())); } } -
你现在可以开始在设备或模拟器上运行应用程序了。
它是如何工作的...
与默认相机应用一起工作的有两部分。第一部分是设置启动应用的意图。我们使用 MediaStore.ACTION_IMAGE_CAPTURE 创建 Intent,表示我们想要一个拍照应用。通过检查 resolveActivity() 的结果来验证是否存在默认应用。只要它不为 null,我们就知道有一个应用程序可以处理这个意图。(否则,我们的应用将会崩溃。)我们创建一个文件名并将其添加到意图中:putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI)。
当我们在 onActivityResult() 中收到回调时,我们首先确保它是 PHOTO_RESULT 和 RESULT_OK(用户可能已取消),然后我们在 ImageView 中加载照片。
还有更多...
如果你不在乎图片存储在哪里,你可以不使用 MediaStore.EXTRA_OUTPUT 额外信息来调用 intent。如果你没有指定输出文件,onActivityResult() 将在数据 Intent 中包含图像的缩略图。以下是显示缩略图的方法:
if (data != null) {
imageView
.setImageBitmap((Bitmap) data.getExtras().get("data"));
}
这是加载全分辨率图像的代码,使用 data Intent 返回的 URI:
if (data != null) {
try {
imageView.setImageBitmap(
MediaStore.Images.Media. getBitmap(getContentResolver(),
Uri.parse(data.toUri(Intent.URI_ALLOW_UNSAFE))));
} catch (IOException e) {
e.printStackTrace();
}
}
调用默认视频应用
如果你想调用默认的视频捕获应用,过程是相同的。只需在步骤 5 中更改 intent,如下所示:
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
你可以在 onActivityResult() 中获取视频的 URI,如下所示:
Uri videoUri = intent.getData();
参考内容
- 在 第九章 的 将大图像缩小以避免内存不足异常 菜谱中,图形和动画。
使用(旧版)Camera API 拍照
之前的菜谱演示了如何使用 intent 调用默认的图片应用程序。如果您只需要快速拍照,intent 可能是理想的解决方案。如果不是,并且您需要更多对摄像头的控制,这个菜谱将向您展示如何直接使用 Camera API 使用摄像头。
实际上有两种使用 Camera API 的方法——一种是 Android 1.0(API 1)中发布的原始 Camera API 和 Android 5.0(API 21)中发布的 Camera2 API。我们将涵盖新旧两种 API。理想情况下,您可能希望编写适用于最新和最佳 API 的应用程序,但在撰写本文时,Android 5.0(API 21)的市场份额只有大约 23%。如果您只使用 Camera2 API,您将排除超过 75% 的市场。
编写您的应用程序以使用 Camera2 API 来利用新功能,同时仍然使用原始 Camera API 为您的其他用户提供一个功能性的应用程序。为了帮助使用两者,这个菜谱将利用 Android 的新功能,特别是 Android 4.0(API 14)中引入的 TextureView。我们将使用 TextureView 来代替更传统的 SurfaceView,用于显示摄像头预览。这将允许您使用与较新的 Camera2 API 相同的布局,因为它也使用 TextureView。(将最小 API 设置为 Android 4.0(API 14)及以上,这占市场份额超过 96%,并不会太多地限制您的用户基础。)
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 CameraAPI。在 目标 Android 设备 对话框中,选择 手机和平板 选项,并为 最小 SDK 选择 API 14(或更高)。当提示 活动类型 时,选择 空活动。
如何操作...
首先打开 AndroidManifest.xml 并按照以下步骤操作:
-
添加以下两个权限:
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
现在打开
activity_main.xml并将现有的 TextView 替换为以下视图:<TextureView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/textureView" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/> -
打开
MainActivity.java并修改MainActivity类声明以实现SurfaceTextureListener,如下所示:public class MainActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener { -
将以下全局声明添加到
MainActivity中:@Deprecated private Camera mCamera; private TextureView mTextureView; -
创建以下
PictureCallback以处理保存照片:Camera.PictureCallback pictureCallback = new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { try { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; File pictureFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName); FileOutputStream fileOutputStream =new FileOutputStream(pictureFile.getPath()); fileOutputStream.write(data); fileOutputStream.close(); Toast.makeText(MainActivity.this, "Picture Taken", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); } } }; -
将以下代码添加到现有的
onCreate()回调中:mTextureView = (TextureView)findViewById(R.id.textureView); mTextureView.setSurfaceTextureListener(this); -
添加以下方法以实现
SurfaceTextureListener接口:public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mCamera = Camera.open(); if (mCamera!=null) { try { mCamera.setPreviewTexture(surface); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } } public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { if (mCamera!=null) { mCamera.stopPreview(); mCamera.release(); } return true; } public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { // Unused } public void onSurfaceTextureUpdated(SurfaceTexture surface) { // Unused } -
添加以下方法来处理按钮点击:
public void takePicture(View view) { if (mCamera!=null) { mCamera.takePicture(null, null, pictureCallback); } } -
在带有摄像头的设备或模拟器上运行应用程序。
它是如何工作的...
首先,请注意,当您在 Android Studio 中查看此代码时,您将看到很多带有以下警告的删除线代码:
'android.hardware.Camera' is deprecated
如介绍中所述,android.hardware.camera2 API 在 Android 5.0 (API 19) 中引入,并取代了 android.hardware.camera API。
小贴士
你可以添加以下注释来抑制弃用警告:
@SuppressWarnings("deprecation")
使用 Camera API 时有两个主要步骤:
-
设置预览
-
捕获图像
我们从布局中获取TextureView,然后使用以下代码将我们的活动(实现了SurfaceTextureListener)作为监听器分配:
mTextureView.setSurfaceTextureListener(this);
当TextureView表面准备好时,我们会在我们创建的Camera.PictureCallback类中收到onSurfaceTextureAvailable回调,其中我们使用以下代码设置预览表面:
mCamera.setPreviewTexture(surface);
mCamera.startPreview();
下一步是在按钮按下时拍照。我们使用以下代码来完成这个操作:
mCamera.takePicture(null, null, pictureCallback);
当图片准备好时,我们会在我们创建的Camera.PictureCallback类中收到onPictureTaken()回调。
更多内容...
请记住,这段代码的目的是向您展示它是如何工作的,而不是创建一个完整的商业应用程序。正如大多数开发者所知,编码中的真正挑战是处理所有的问题情况。一些需要改进的领域包括添加切换相机的功能,因为当前应用程序使用的是默认相机。此外,请查看预览和保存图片时的设备方向。一个更复杂的应用程序会在后台线程上处理一些工作,以避免 UI 线程上的延迟。(请查看下一个菜谱,了解我们如何在后台线程上执行一些相机处理。)
设置相机参数
Camera API 包括参数,允许我们调整相机设置。在这个例子中,我们可以更改预览的大小:
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(mPreviewSize.width,
mPreviewSize.height);
mCamera.setParameters(parameters);
请记住,硬件也必须支持我们想要的设置。在这个例子中,我们首先想要查询硬件以获取所有可用的预览模式,然后设置符合我们要求的那个。 (在下一个菜谱中设置图片分辨率时,请参见此示例。)请参阅 Camera 文档链接中的getParameters()。
参见
-
下一个菜谱:使用 Camera2(新)API 拍照
-
在第八章中,读取设备方向的菜谱,使用触摸屏和传感器提供了检测当前设备方向的示例
-
开发者文档:构建相机应用,请访问:
developer.android.com/guide/topics/media/camera.html#custom-camera -
developer.android.com/reference/android/hardware/Camera.html
使用 Camera2(新)API 拍照
现在我们已经了解了旧的 Camera API,是时候学习新的 Camera2 API 了。不幸的是,由于 API 的异步特性,它稍微复杂一些。幸运的是,整体概念与之前的 Camera API 相同。
准备工作
在 Android Studio 中创建一个新的项目,命名为Camera2API。在目标 Android 设备对话框中,选择手机和平板电脑选项,并将最小 SDK设置为 API 21(或更高)。当提示活动类型时,选择空活动。
如何操作...
正如您将看到的,这个菜谱有很多代码。首先,打开 AndroidManifest 文件,按照以下步骤操作:
-
添加以下两个权限:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
现在打开
activity_main.xml,并用以下视图替换现有的 TextView:<TextureView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/textureView" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/> -
现在打开
MainActivity.java文件,并将以下全局变量添加到MainActivity类中:private CameraDevice mCameraDevice = null; private CaptureRequest.Builder mCaptureRequestBuilder = null; private CameraCaptureSession mCameraCaptureSession = null; private TextureView mTextureView = null; private Size mPreviewSize = null; -
添加以下
Comparator类:static class CompareSizesByArea implements Comparator<Size> { @Override public int compare(Size lhs, Size rhs) { return Long.signum((long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); } } -
添加以下
CameraDevice.StateCallback:private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; SurfaceTexture texture = mTextureView.getSurfaceTexture(); if (texture == null) { return; } texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface surface = new Surface(texture); try { mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); } catch (CameraAccessException e){ e.printStackTrace(); } mCaptureRequestBuilder.addTarget(surface); try { mCameraDevice.createCaptureSession(Arrays.asList(surface), mPreviewStateCallback, null); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onError(CameraDevice camera, int error) {} @Override public void onDisconnected(CameraDevice camera) {} }; -
添加以下
SurfaceTextureListener:private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) {} @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; } @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { openCamera(); } }; -
添加以下
CameraCaptureSession.StateCallback:private CameraCaptureSession.StateCallback mPreviewStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { startPreview(session); } @Override public void onConfigureFailed(CameraCaptureSession session) {} }; -
将以下代码添加到现有的
onCreate()回调中:mTextureView = (TextureView) findViewById(R.id.textureView); mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); -
添加以下方法以重写
onPause()和onResume():@Override protected void onPause() { super.onPause(); if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } @Override public void onResume() { super.onResume(); if (mTextureView.isAvailable()) { openCamera(); } else { mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); } } -
添加
openCamera()方法:private void openCamera() { CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE); try{ String cameraId = manager.getCameraIdList()[0]; CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); mPreviewSize = map.getOutputSizes(SurfaceTexture.class) [0]; manager.openCamera(cameraId, mStateCallback, null); } catch(CameraAccessException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } } -
添加
startPreview()方法:private void startPreview(CameraCaptureSession session) { mCameraCaptureSession = session; mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE,CameraMetadata.CONTROL_MODE_AUTO); HandlerThread backgroundThread = new HandlerThread("CameraPreview"); backgroundThread.start(); Handler backgroundHandler = new Handler(backgroundThread. getLooper()); try { mCameraCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } -
添加
getPictureFile()方法:private File getPictureFile() { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss"). format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName); } -
添加保存图像文件的
takePicture()方法:protected void takePicture(View view) { if (null == mCameraDevice) { return; } CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { CameraCharacteristics characteristics = manager.getCameraCharacteristics(mCameraDevice.getId()); StreamConfigurationMap configurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (configurationMap == null) return; Size largest = Collections.max( Arrays.asList(configurationMap.getOutputSizes(ImageFormat.JPEG)), new CompareSizesByArea()); ImageReader reader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, 1); List < Surface > outputSurfaces = new ArrayList < Surface > (2); outputSurfaces.add(reader.getSurface()); outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture())); final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_ CAPTURE); captureBuilder.addTarget(reader.getSurface()); captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = null; try { image = reader.acquireLatestImage(); ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes); OutputStream output = new FileOutputStream( get PictureFile()); output.write(bytes); output.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (image != null) { image.close(); } } } }; HandlerThread thread = new HandlerThread("CameraPicture"); thread.start(); final Handler backgroudHandler = new Handler(thread.getLooper()); reader.setOnImageAvailableListener(readerListener, backgroudHandler); final CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted( CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); Toast.makeText(MainActivity.this, "Picture Saved", Toast.LENGTH_SHORT).show(); startPreview(session); } }; mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { @Override public vod onConfigured(CameraCaptureSession session) { try { session.capture(captureBuilder.build(), captureCallback, backgroudHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(CameraCaptureSession session) { } }, backgroudHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } -
在具有摄像头的设备或模拟器上运行应用程序。
工作原理...
由于我们在前面的菜谱中学习了 TextureView,我们可以跳转到新的 Camera2 API 信息。
尽管涉及更多的类,就像旧的 Camera API 一样,这里有两个基本步骤:
-
设置预览
-
捕获图像
设置预览
下面是代码如何设置预览的概述:
-
首先,我们在
onCreate()中使用setSurfaceTextureListener()方法设置TextureView.SurfaceTextureListener。 -
当我们收到
onSurfaceTextureAvailable()回调时,我们打开相机。 -
我们将我们的
CameraDevice.StateCallback类传递给openCamera()方法,它最终调用onOpened()回调。 -
onOpened()通过调用getSurfaceTexture()获取预览的表面,并通过调用createCaptureSession()将其传递给 CameraDevice。 -
最后,当
CameraCaptureSession.StateCallback onConfigured()被调用时,我们使用setRepeatingRequest()方法开始预览。
捕获图像
尽管takePicture()方法看起来像是过程性的,捕获图像也涉及到多个类并依赖于回调。以下是代码如何捕获图像的分解:
-
用户点击拍照按钮。
-
然后查询相机以找到最大的可用图像大小。
-
然后创建一个
ImageReader。 -
接下来,他/她设置了
OnImageAvailableListener,并在onImageAvailable()回调中保存图像。 -
然后,创建
CaptureRequest.Builder并包含ImageReader表面。 -
接下来,创建
CameraCaptureSession.CaptureCallback,它定义了onCaptureCompleted()回调。捕获完成后,它重新开始预览。 -
然后,调用
createCaptureSession()方法,创建一个CameraCaptureSession.StateCallback。这是调用capture()方法的地方,传入之前创建的CameraCaptureSession.CaptureCallback。
更多内容...
与之前的 Camera 示例一样,我们只是创建了基础代码来演示一个可工作的 Camera 应用程序。同样,还有改进的空间。首先,你应该处理设备方向,无论是预览还是保存图像时。(参见之前的配方以获取链接。)此外,随着 Android 6.0(API 23)的现在可用,现在是开始使用新的权限模型的好时机。而不是像我们在openCamera()方法中所做的那样只是检查异常,最好是检查所需的权限。
参见
-
之前的配方:使用(旧)Camera API 拍照
-
第十四章中新的 Android 6.0 运行时权限模型 第十四章,为 Play 商店准备你的应用
-
开发者文档:Camera2 API
-
developer.android.com/reference/android/hardware/camera2/package-summary.html
第十二章:电话、网络和 Web
在本章中,我们将涵盖以下主题:
-
如何打电话
-
监听电话事件
-
如何发送短信(文本)消息
-
接收短信消息
-
在您的应用程序中显示网页
-
检查在线状态和连接类型
-
使用 Volley 进行网络请求入门
-
取消 Volley 请求
-
使用 Volley 请求 JSON 响应
-
使用 Volley 请求图片
-
使用 Volley 的 NetworkImageView 和 ImageLoader
简介
我们将首先通过查看带有如何打电话的 Telephony 功能来开始本章。在探索如何打电话之后,我们将查看如何使用监听电话事件来监控电话。然后,我们将继续使用如何发送短信消息进行短信通信,然后我们将介绍如何使用接收短信消息接收短信。
然后,我们将探索WebView,以向您的应用程序添加浏览器功能。在基本层面上,WebView是一个基本的 HTML 查看器。我们将展示如何扩展WebViewClient类并通过WebSettings修改设置来创建完整的浏览器功能,包括 JavaScript 和缩放功能。
以下章节将介绍 Volley,这是一个通过 AOSP 提供的全新库。关于 Volley 如何进行网络请求的入门介绍将提供有关 Android 上可用的在线库的背景信息,并讨论为什么创建 Volley。它还提供了将 Volley 添加到您的 Android Studio 项目的完整指南。
如何打电话
如前几章所示,我们可以通过使用 Intent 简单地调用默认应用程序。要打电话,请在创建 Intent 时使用Intent.ACTION_DIAL。您可以使用setData()方法包含一个电话号码。以下是一个示例代码,它将使用指定的电话号码调用拨号器应用程序:
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:" + number));
startActivity(intent);
由于您的应用程序不进行拨号,并且用户必须按下拨号按钮,因此您不需要在您的应用程序中设置任何拨号权限。以下食谱将向您展示如何直接拨打电话,绕过Dial活动。(为此,您需要添加一个权限。)
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为DialPhone。使用默认的电话和平板电脑选项,并在提示活动类型时选择空活动。
如何做到...
首先,我们需要添加适当的权限来拨打电话。然后,我们需要添加一个按钮来调用我们的Dial方法。首先打开 AndroidManifest.xml,按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission> -
打开
activity_main.xml,并用以下按钮替换现有的TextView:<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Dial" android:layout_centerInParent="true" android:onClick="dialPhone"/> -
添加以下方法,该方法将检查您的应用程序是否已被授予
CALL_PHONE权限:private boolean checkPermission(String permission) { int permissionCheck = ContextCompat.checkSelfPermission( this, permission); return (permissionCheck == PackageManager.PERMISSION_GRANTED); } -
添加拨打电话的代码:
public void dialPhone(View view){ if (checkPermission("android.permission.CALL_PHONE")) { Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:0123456789")); startActivity(intent); } } -
在您的设备上运行此代码之前,请务必将
0123456789替换为有效的电话号码。
它是如何工作的...
如我们从引言中的代码所看到的,调用默认拨号应用时我们不需要任何权限。但如果我们想直接拨打一个号码,我们需要添加CALL_PHONE权限。从 Android 6.0 Marshmallow(API 23)开始,权限不再在安装期间授予,因此,我们在尝试拨打电话之前会检查应用程序是否有权限。
参见
- 更多信息,请参阅第十四章中的新的运行时权限模型菜谱,你的应用准备好 Play 商店。
监听电话事件
在前面的菜谱中,我们展示了如何进行电话呼叫,无论是通过 Intent 调用默认应用,还是通过没有 UI 直接拨打号码。
如果你想在通话结束时收到通知怎么办?这会变得稍微复杂一些,因为你需要监控电话事件并跟踪电话状态。在这个菜谱中,我们将演示如何创建一个PhoneStateListener来读取电话状态事件。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为PhoneStateListener。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
虽然这不是必需的,但你可以使用前面的菜谱来发起电话呼叫以查看事件。否则,使用默认拨号器,或从来电中观察事件。(下载文件中提供的示例代码包含了前面的菜谱,以便更容易查看事件。)
如何做到这一点...
我们只需要在布局中添加一个TextView来显示事件信息。如果你是从前面的菜谱继续,或者开始一个新的菜谱,打开activity_main.xml文件并按照以下步骤操作:
-
添加或修改
TextView如下:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> -
在 AndroidManifest.xml 中添加以下权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE"> </uses-permission> -
打开
MainActivity.java并将以下PhoneStateListener类添加到MainActivity类中:PhoneStateListener mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String number) { String phoneState = number; switch (state) { case TelephonyManager.CALL_STATE_IDLE: phoneState += "CALL_STATE_IDLE\n"; case TelephonyManager.CALL_STATE_RINGING: phoneState += "CALL_STATE_RINGING\n"; case TelephonyManager.CALL_STATE_OFFHOOK: phoneState += "CALL_STATE_OFFHOOK\n"; } TextView textView = (TextView)findViewById(R.id.textView); textView.append(phoneState); } }; -
修改
onCreate()以设置监听器:final TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); telephonyManager.listen(mPhoneStateListener,PhoneStateListener.LISTEN_CALL_STATE); -
在设备上运行应用程序并发起和/或接收电话呼叫以查看事件。
它是如何工作的...
为了演示使用监听器,我们在onCreate()中使用这两行代码创建 Telephony 监听器:
final TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(mPhoneStateListener,PhoneStateListener.LISTEN_CALL_STATE);
当发生PhoneState事件时,它会被发送到我们的PhoneStateListener类。
更多内容...
在这个菜谱中,我们正在监控呼叫状态事件,如以下常量所示:LISTEN_CALL_STATE。其他有趣的选项包括以下内容:
-
LISTEN_CALL_FORWARDING_INDICATOR -
LISTEN_DATA_CONNECTION_STATE -
LISTEN_SIGNAL_STRENGTHS
查看以下PhoneStateListener链接以获取完整列表。
当我们完成事件监听后,调用listen()方法并传递LISTEN_NONE,如下所示:
telephonyManager.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
参见
- 开发者文档:PhoneStateListener 在
developer.android.com/reference/android/telephony/PhoneStateListener.html
如何发送短信(文本)消息
由于你可能已经熟悉短信(或文本)消息,我们不会花时间解释它们是什么或为什么它们很重要。(如果你不熟悉短信或需要更多信息,请参阅本食谱另请参阅部分提供的链接。)本食谱将演示如何发送短信消息。(下一个食谱将演示如何接收新消息的通知以及如何读取现有消息。)
准备工作
在 Android Studio 中创建一个新的项目,命名为SendSMS。使用默认的电话和平板电脑选项,并在提示活动类型时选择空活动。
如何做到...
首先,我们将添加发送短信所需的必要权限。然后,我们将创建一个包含电话号码、消息字段和发送按钮的布局。当点击发送按钮时,我们将创建并发送短信。以下是步骤:
-
打开 AndroidManifest 文件并添加以下权限:
<uses-permission android:name="android.permission.SEND_SMS"/> -
打开
activity_main.xml并将现有的TextView替换为以下 XML:<EditText android:id="@+id/editTextNumber" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="number" android:ems="10" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:hint="Number"/> <EditText android:id="@+id/editTextMsg" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/editTextNumber" android:layout_centerHorizontal="true" android:hint="Message"/> <Button android:id="@+id/buttonSend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send" android:layout_below="@+id/editTextMsg" android:layout_centerHorizontal="true" android:onClick="send"/> -
打开
MainActivity.java并添加以下全局变量:final int SEND_SMS_PERMISSION_REQUEST_CODE=1; Button mButtonSend; -
将以下代码添加到现有的
onCreate()回调中:mButtonSend = (Button)findViewById(R.id.buttonSend); mButtonSend.setEnabled(false); if (checkCallPermission(Manifest.permission.SEND_SMS)) { mButtonSend.setEnabled(true); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SEND_SMS}, SEND_SMS_PERMISSION_REQUEST_CODE); } -
添加以下方法来检查权限:
private boolean checkPermission(String permission) { int permissionCheck = ContextCompat.checkSelfPermission(this,permission); return (permissionCheck == PackageManager.PERMISSION_GRANTED); } -
重写
onRequestPermissionsResult()以处理权限请求响应:@Override public void onRequestPermissionsResult(int requestCode,String permissions[], int[] grantResults) { switch (requestCode) { case SEND_SMS_PERMISSION_REQUEST_CODE: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mButtonSend.setEnabled(true); } return; } } } -
最后,添加实际发送短信的方法:
public void send(View view) { String phoneNumber = ((EditText)findViewById(R.id.editTextNumber)).getText().toString(); String msg = ((EditText)findViewById(R.id.editTextMsg)).getText().toString(); if (phoneNumber==null || phoneNumber.length()==0 || msg==null || msg.length()==0 ) { return; } if (checkPermission(Manifest.permission.SEND_SMS)) { SmsManager smsManager = SmsManager.getDefault(); smsManager.sendTextMessage(phoneNumber, null, msg, null, null); } else { Toast.makeText(MainActivity.this, "No Permission", Toast.LENGTH_SHORT).show(); } } -
你现在可以运行应用程序在设备或模拟器上。(发送到另一个模拟器时,请使用模拟器设备号,例如 5556。)
它是如何工作的...
发送短信的代码只有两行,如下所示:
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage(phoneNumber, null, msg, null, null);
sendTextMessage()方法负责实际发送。这个食谱的大部分代码都是为了设置权限,因为 Android 6.0 Marshmallow(API 23)中改变了权限模型。
还有更多...
虽然发送短信很简单,但我们还有一些其他选项。
多部分消息
虽然这取决于运营商,但通常每个短信允许的最大字符数是 160 个。你可以修改前面的代码来检查消息是否超过 160 个字符,如果是,你可以调用 SMSManager 的divideMessage()方法。该方法返回一个ArrayList,你可以将其发送到sendMultipartTextMessage()。以下是一个示例:
ArrayList<String> messages=smsManager.divideMessage(msg);
smsManager.sendMultipartTextMessage(phoneNumber, null, messages, null, null);
小贴士
注意,使用sendMultipartTextMessage()发送的消息在使用模拟器时可能无法正确工作,因此请务必在真实设备上进行测试。
送达状态通知
如果你希望收到消息状态的更新,你可以使用两个可选字段。以下是在SMSManager文档中定义的sendTextMessage()方法的示例:
sendTextMessage(String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent)
您可以包含一个挂起的 Intent 来通知发送状态和/或投递状态。在接收到您的挂起 Intent 时,它将包含一个结果代码,要么是Activity.RESULT_OK,如果发送成功,要么是SMSManager文档(在以下另请参阅部分中提到的链接)中定义的错误代码:
-
RESULT_ERROR_GENERIC_FAILURE:通用失败原因 -
RESULT_ERROR_NO_SERVICE:失败,因为服务当前不可用 -
RESULT_ERROR_NULL_PDU:失败,因为没有提供 PDU -
RESULT_ERROR_RADIO_OFF:失败,因为无线电被明确关闭
另请参阅
-
在维基百科上查看短信服务
en.wikipedia.org/wiki/Short_Message_Service -
开发者文档:SMSManager
developer.android.com/reference/android/telephony/SmsManager.html
接收短信消息
本食谱将演示如何设置广播接收器以通知您新的短信消息。请注意,您的应用无需运行即可接收短信意图。Android 会启动您的服务来处理短信。
准备工作
在 Android Studio 中创建一个新的项目,命名为ReceiveSMS。使用默认的电话和平板电脑选项,并在提示活动类型时选择空活动。
如何实现...
在本演示中,我们不会使用布局,因为所有的工作都将由广播接收器完成。我们将使用 Toast 显示传入的短信消息。打开 Android 清单并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.RECEIVE_SMS" /> -
将以下广播接收器声明添加到
<application>元素中:<receiver android:name=".SMSBroadcastReceiver"> <intent-filter> <action android:name="android.provider.Telephony.SMS_RECEIVED"> </action> </intent-filter> </receiver> -
打开
MainActivity.java并添加以下方法:private boolean checkPermission(String permission) { int permissionCheck = ContextCompat.checkSelfPermission( this, permission); return (permissionCheck == PackageManager.PERMISSION_GRANTED); } -
修改现有的
onCreate()回调以检查权限:if (!checkPermission(Manifest.permission.RECEIVE_SMS)) { ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.RECEIVE_SMS}, 0); } -
在项目中添加一个名为
SMSBroadcastReceiver的新 Java 类,使用以下代码:public class SMSBroadcastReceiver extends BroadcastReceiver { final String SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"; @Override public void onReceive(Context context, Intent intent) { if (SMS_RECEIVED.equals(intent.getAction())) { Bundle bundle = intent.getExtras(); if (bundle != null) { Object[] pdus = (Object[]) bundle.get("pdus"); String format = bundle.getString("format"); final SmsMessage[] messages = new SmsMessage[pdus.length]; for (int i = 0; i < pdus.length; i++) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i], format); } else { messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]); } Toast.makeText(context, messages[0].getMessageBody(), Toast.LENGTH_SHORT).show(); } } } } } -
您现在可以开始在设备或模拟器上运行应用程序了。
它是如何工作的...
就像在之前的发送短信食谱中一样,我们首先需要检查应用是否有权限。(在 Android 6.0 之前的设备上,清单声明将自动提供权限,但对于 Marshmallow 及以后的版本,我们需要像这里一样提示用户。)
如您所见,广播接收器接收新的短信消息通知。我们通过在 Android 清单中的此代码告诉系统我们想要接收新的短信接收广播:
<receiver android:name=".SMSBroadcastReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"></action>
</intent-filter>
</receiver>
通知是通过标准的onReceive()回调传入的,因此我们使用以下代码检查操作:
if (SMS_RECEIVED.equals(intent.getAction())) {}
这可能是本演示中最复杂的一行代码:
messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
基本上,它调用 SmsMessage 库从 PDU 创建 SMSMessage 对象。(PDU,即协议数据单元,是 SMS 消息的二进制数据格式。)如果您不熟悉 PDU 的构成,您不需要了解。SmsMessage 库会为您处理并返回 SMSMessage 对象。
小贴士
如果您的应用程序没有接收 SMS 广播消息,可能有一个现有的应用程序正在阻止您的应用程序。您可以尝试增加 intent-filter 中的优先级值,如所示,或者禁用/卸载其他应用程序:
<intent-filter android:priority="100">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
还有更多...
此配方演示了如何显示接收到的 SMS 消息,但关于读取现有消息怎么办?
读取现有 SMS 消息
首先,为了读取现有消息,您需要以下权限:
<uses-permission android:name="android.permission.READ_SMS" />
这里是一个使用 SMS 内容提供者获取游标的示例:
Cursor cursor = getContentResolver().query(Uri.parse("content://sms/"), null, null, null, null);
while (cursor.moveToNext()) {
textView.append("From :" + cursor.getString(1) + " : " + cursor.getString(11)+"\n");
}
在撰写本文时,SMS 内容提供者有超过 30 列。以下是前 12 列,它们是最有用的(记住,列数从零开始):
0. _id
1. thread_id
2. address
3. person
4. date
5. protocol
6. read
7. status
8. type
9. reply_path_present
10. subject
11. body
请记住,内容提供者不是公共 API 的一部分,并且可以在不通知的情况下更改。
参见
-
开发者文档:SmsManager 在
developer.android.com/reference/android/telephony/SmsManager.html -
PDU(协议数据单元)在
en.wikipedia.org/wiki/Protocol_data_unit -
开发者文档:Telephony.Sms.Intents 在
developer.android.com/reference/android/provider/Telephony.Sms.Intents.html
在您的应用程序中显示网页
当您想在网页上显示 HTML 内容时,您有两个选择:调用默认浏览器或在其应用程序内显示。如果您只想调用默认浏览器,请使用以下 Intent:
Uri uri = Uri.parse("https://www.packtpub.com/");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
如果您需要在您的应用程序中显示内容,您可以使用 WebView。此配方将展示如何在您的应用程序中显示网页,如图中所示:

准备工作
在 Android Studio 中创建一个新的项目,命名为 WebView。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
我们将通过代码创建 WebView,因此我们不会修改布局。我们首先打开 Android Manifest 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> -
修改现有的
onCreate()以包含以下代码:WebView webview = new WebView(this); setContentView(webview); webview.loadUrl("https://www.packtpub.com/"); -
您现在可以开始在设备或模拟器上运行应用程序。
它是如何工作的...
我们创建一个 WebView 作为我们的布局,并使用 loadUrl() 加载我们的网页。前面的代码是有效的,但在这一级别,它非常基础,仅显示第一页。如果您点击任何链接,默认浏览器将处理请求。
更多内容...
如果您想要完整的网络浏览功能,以便点击的任何链接都在您的 WebView 中加载,请创建一个 WebViewClient,如下所示:
webview.setWebViewClient(new WebViewClient());
控制页面导航
如果您想要对页面导航有更多控制,例如仅允许您自己的网站内的链接,您可以创建自己的 WebViewClient 类并重写 shouldOverrideUrlLoading() 回调,如下所示:
private class mWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (Uri.parse(url).getHost().equals("www.packtpub.com")) {
return false; //Don't override since it's the same //host
} else {
return true; //Stop the navigation since it's a //different site
}
}
}
如何启用 JavaScript
我们可以通过 WebView 的 WebSettings 进行许多其他自定义更改。如果您想启用 JavaScript,获取 WebView 的 WebSettings 并调用 setJavaScriptEnabled(),如下所示:
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
启用内置缩放
另一个 webSetting 选项是 setBuiltInZoomControls()。从前面的代码继续,只需添加:
webSettings.setBuiltInZoomControls(true);
在下一节中检查 webSetting 链接,以获取大量附加选项。
相关内容
-
开发者文档:WebView 在
developer.android.com/reference/android/webkit/WebView.html -
开发者文档:WebSettings 在
developer.android.com/reference/android/webkit/WebSettings.html -
开发者文档:android.webkit 在
developer.android.com/reference/android/webkit/package-summary.html
检查在线状态和连接类型
这是一个简单的配方,但非常常见,可能包含在您构建的每个互联网应用程序中:检查在线状态。在检查在线状态时,我们还可以检查连接类型:WIFI 或 MOBILE。
准备工作
在 Android Studio 中创建一个新的项目,命名为 isOnline。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何实现...
首先,我们需要添加必要的权限以访问网络。然后,我们将创建一个包含 Button 和 TextView 的简单布局。要开始,打开 Android Manifest 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -
打开
activity_main.xml文件,并用以下视图替换现有的TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Check" android:layout_centerInParent="true" android:onClick="checkStatus"/> -
将此方法添加以报告连接状态:
private boolean isOnline() { ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected()); } -
添加以下方法以处理按钮点击:
public void checkStatus(View view) { TextView textView = (TextView)findViewById(R.id.textView); if (isOnline()) { ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); textView.setText(networkInfo.getTypeName()); } else { textView.setText("Offline"); } } -
您现在可以运行应用程序在设备或模拟器上。
它是如何工作的...
我们创建了 isOnline() 方法,使其易于重用此代码。
要检查状态,我们获取 ConnectivityManager 的一个实例来读取 NetworkInfo 状态。如果它报告我们已连接,我们将通过调用 getType() 获取活动网络的名称,该函数返回以下常量之一:
-
TYPE_MOBILE -
TYPE_WIFI -
TYPE_WIMAX -
TYPE_ETHERNET -
TYPE_BLUETOOTH
此外,请参阅后面的ConnectivityManager链接以获取更多常量。为了显示目的,我们调用getTypeName()。我们也可以调用getType()来获取一个数字常量。
还有更多...
让我们来看看ConnectivityManager的一些其他常量。
监控网络状态变化
如果您的应用需要响应网络状态的变化,请查看ConnectivityManager中的CONNECTIVITY_ACTION。您需要创建一个广播接收器,然后注册该事件。以下是如何通过 AndroidManifest.xml 将操作包含在接收器的 intent filter 中的示例:
<receiver android:name="com.vcs.timetrac.VCSBroadcastReceiver">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
使用 Android 清单文件时要小心,因为它会在网络状态改变时通知您的应用,即使您的应用没有被使用。这可能会导致电池不必要的消耗。如果您的应用只需要在用户实际使用您的应用时响应网络变化,请在代码中创建监听器。
参见
-
开发者文档:ConnectivityManager 在
developer.android.com/reference/android/net/ConnectivityManager.html -
开发者文档:NetworkInfo 在
developer.android.com/reference/android/net/NetworkInfo.html
开始使用 Volley 进行网络请求
Android 包括多个用于网络查询的库,包括 Apache HttpClient 和 HttpURLConnection。在 Android 2.3 Gingerbread(API 9)之前,Apache HttpClient 是推荐的库。Android 2.3 Gingerbread(API 9)对HttpURLConnection库进行了许多改进,使其成为推荐的库,并且至今仍然是推荐的库。随着 Android 6.0 的发布,Apache HttpClient 已完全从 SDK 中移除,留下了HttpURLConnection库作为推荐的替代品。
虽然HttpURLConnection库仍然有效并且有其用途,但也有一些缺点:如果您是编写网络请求的新手,它不是最容易使用的库,并且需要大量的重复开销代码。幸运的是,我们从Ficus Kirkpatrick那里有一个新的选择,他是来自 Google Play 团队的 Google 开发者。他发布了一个名为 Volley 的库,它提供了一个简化的包装器。(默认情况下,它使用HttpURLConnection库,也可以与其他库一起使用。)
注意
您可以在这里查看他的 Google I/O 演讲:
www.youtube.com/watch?v=yhv8l9F44qo
使用 Volley 代替HttpURLConnection的几个原因包括以下:
-
线程池(默认为四个线程)
-
透明磁盘缓存
-
队列优先级设置
有额外的优势,但仅这三个就足以使学习 Volley 变得值得。第四个优势,如果您曾经使用过HttpURLConnection,将会很明显,那就是缺乏样板代码。您不需要在许多调用周围编写大量的标准try/catch代码,库将内部处理检查,让您能更多地专注于手头的具体任务。
Volley 内置了对以下请求类型的支持:
-
String
-
JSON
-
图片
-
自定义
虽然 Volley 在处理多个小请求调用(如滚动ListView时)表现出色,但它不擅长大文件下载,因为返回的对象是在内存中解析的。对于大文件下载,请查看DownloadManager(见食谱末尾的链接)。同样,出于同样的原因,它也不是流式内容的解决方案;对于这一点,请参考HttpURLConnection。
由于 Volley 目前不在 Android SDK 中,我们需要下载代码并将其添加到我们的项目中。本食谱将指导您将 Volley 添加到应用程序项目中并执行一个简单的请求。
准备工作
在创建新项目之前,使用以下 Git 命令下载托管在Android 开源项目(AOSP)网站上的 Volley 项目文件:
git clone https://android.googlesource.com/platform/frameworks/volley
如果您不熟悉 Git,请参阅本食谱末尾的 Git(软件)链接以获取更多信息以及帮助您找到适合您平台的 Git 客户端。Git 是一种在许多平台上使用的版本控制软件(VCS)。(安装后,您还可以在 Android Studio 中集成 Git VCS。)
在 Android Studio 中创建一个新项目,并将其命名为SetupVolley。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
如何操作...
在开始以下步骤之前,请确保您已按照之前描述的方式下载了 Volley 项目。我们将通过将 Volley 添加到我们的项目中来执行简单的网络调用开始以下步骤。我们将在布局中使用一个按钮来启动请求,并使用TextView来显示结果。以下是步骤:
-
打开 AndroidManifest 文件并添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> -
通过文件 | 新建 | 导入模块(见下方的截图)导入
Volley模块,并按照向导操作。![如何操作...]()
-
在新模块导入向导的第二页(见下方的截图),您需要指定 Volley 文件的存放位置并分配模块名称。这是我们在下一步需要用到的名称:
![如何操作...]()
-
在
Gradle Scripts部分,打开build.gradle (Module: app)文件。见下方的截图:![如何操作...]()
-
在
dependencies部分添加/验证以下声明:compile project(":Volley")注意
括号中的值需要与您在上一步骤中指定的模块名称相匹配。
-
在
Gradle Scripts下,打开settings.gradle文件并验证内容如下:include ':app', ':Volley' -
打开
activity_main.xml文件,将现有的TextView替换为以下TextView和Button元素:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_above="@+id/button" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Request" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="sendRequest"/> -
添加由按钮点击调用的
sendRequest()方法:public void sendRequest(View view) { final TextView textView = (TextView)findViewById(R.id.textView); RequestQueue queue = Volley.newRequestQueue(this); String url ="https://www.packtpub.com/"; StringRequest stringRequest = new StringRequest(Request.Method.GET, url,new Response.Listener<String>() { @Override public void onResponse(String response) { textView.setText(response.substring(0,500)); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { textView.setText("onErrorResponse(): "+ error.getMessage()); } }); queue.add(stringRequest); } -
你已经准备好在设备或模拟器上运行应用程序了。
它是如何工作的...
可能有助于理解,在 Volley 中,网络事务被称为请求。要执行请求,将其添加到队列中。要实现这一点,我们首先创建一个 Volley RequestQueue的实例,然后创建一个StringRequest并将其添加到队列中。StringRequest正如其名;我们正在请求一个字符串响应。
对于这个菜谱,我们只是调用 Packt Publishing 网站并获取作为字符串响应的页面。由于这只是为了说明,我们只显示前 500 个字符。
更多内容...
现在你已经正确设置了 Volley 并开始进行网络请求,这个菜谱将是后续 Volley 菜谱的基石。
相关内容
-
Volley:Git 在 Google,
android.googlesource.com/platform/frameworks/volley -
Git(软件):维基百科,免费百科全书,
en.wikipedia.org/wiki/Git_(software) -
开发者文档:DownloadManager,
developer.android.com/reference/android/app/DownloadManager.html -
开发者文档:HttpURLConnection,
developer.android.com/reference/java/net/HttpURLConnection.html
取消 Volley 请求
在上一个菜谱中,我们展示了如何将请求添加到 Volley 队列。如果你不再需要响应会怎样?这可能会发生在用户正在滚动ListView,而你正在通过从网络获取信息来更新ListItems的情况下。知道你只是要丢弃响应,允许请求完成将会浪费带宽、电力和 CPU 周期。
如果你使用的是HTTPURLConnection库,你需要跟踪所有请求并手动取消它们。这个菜谱将展示在 Volley 中取消请求是多么简单。
准备工作
如果你还没有完成前面的菜谱,即使用 Volley 进行网络请求的入门,你需要遵循步骤 1-5 将 Volley 模块添加到你的应用程序中。
在 Android Studio 中创建一个新的项目,命名为CancelVolleyRequest。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
如何操作...
如果你还没有将 Volley 模块添加到你的应用程序中,请回顾前面的部分。在将 Volley 添加到你的项目后,按照以下步骤操作:
-
打开
activity_main.xml并替换现有的TextView为以下 XML:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_above="@+id/button" /> <Button android:id="@+id/button" android:layout_width="100dp" android:layout_height="wrap_content" android:text="Request" android:layout_centerInParent="true" android:onClick="sendRequest"/> <Button android:id="@+id/buttonClose" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_below="@+id/button" android:layout_centerHorizontal="true" android:text="Close" android:onClick="close"/> -
打开
MainActivity.java并添加以下全局变量:RequestQueue mRequestQueue; -
编辑现有的
onCreate()以初始化RequestQueue:mRequestQueue = Volley.newRequestQueue(this); -
添加以下
sendRequest()方法(注意,这与前一个食谱中的sendRequest()方法类似,但有一些更改):public void sendRequest(View view) { final TextView textView = (TextView)findViewById(R.id.textView); String url ="https://www.packtpub.com/"; StringRequest stringRequest = new StringRequest(Request.Method.GET, url,new Response.Listener<String>() { @Override public void onResponse(String response) { textView.setText(response.substring(0,500)); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { textView.setText("onErrorResponse(): "+ error.getMessage()); } }); stringRequest.setTag(this); mRequestQueue.add(stringRequest); finish(); } -
添加 Close 按钮的
onClick方法:public void close(View view){ finish(); } -
为
onStop()回调创建以下覆盖:@Override protected void onStop() { super.onStop(); mRequestQueue.cancelAll(this); } -
你已经准备好在设备或模拟器上运行应用程序。
工作原理...
要取消请求,我们可以调用 RequestQueue 的 cancelAll() 方法并传入我们的标签。在这个例子中,我们使用了活动 this 作为标签,但我们可以使用任何对象作为标签。这允许你为请求创建所需的任何分组。
更多...
我们不仅展示了取消请求有多容易,还展示了防御性编程策略。通过确保所有请求都被取消,我们不需要在响应中添加代码来检查活动是否为空,因为 Volley 保证在请求被取消后,我们不会收到 任何 响应。
使用 Volley 请求 JSON 响应
由于 JavaScript 对象表示法 (JSON) 可能是使用最广泛的数据交换格式,你可能会发现自己需要调用 JSON 网络服务。(如果你对 JSON 不熟悉,请查看本食谱末尾的链接。)本食谱将演示如何使用 Volley 进行 JSON 请求。(如果你对 JSON 不熟悉,请查看本食谱末尾的链接。)本食谱将演示如何使用 Volley 进行 JSON 请求。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 JSONRequest。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
本食谱将使用在 使用 Volley 进行网络请求入门 中描述的 Volley 设置。按照步骤 1-5 将 Volley 添加到你的新项目中。
如何操作...
如前所述,将 Volley 添加到你的项目中后,按照以下步骤操作:
-
打开
activity_main.xml并将现有的TextView替换为以下 XML:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_above="@+id/button" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Request" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="sendRequest"/> -
添加以下
sendRequest()方法:public void sendRequest(View view) { final TextView textView = (TextView)findViewById(R.id.textView); RequestQueue queue = Volley.newRequestQueue(this); String url ="<json service>"; //"http://ip.jsontest.com/" JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, url, null, new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { textView.setText(response.toString()); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { textView.setText("onErrorResponse(): "+ error.getMessage()); } }); queue.add(jsonObjectRequest); } -
在运行此应用程序之前,替换代码中的
url字符串。
工作原理...
使用 JsonObjectRequest() 请求 JSON 响应基本上与 StringRequest() 的工作方式相同。区别在于响应,它以 JSONObject 的形式返回。
要运行此代码,你需要将 url 参数替换为你的网络服务 URL。如果你没有网络服务进行测试,你可以尝试 JSON 测试网站上的链接 (www.jsontest.com/)。
更多...
在前面的例子中,我们使用 JsonObjectRequest 请求了一个 JSONObject。我们还可以使用 JsonArrayRequest 请求一个 JSONArray。
相关内容
-
访问 JSON 网页
json.org/ -
开发者文档:org.json (JSON 库) 在
developer.android.com/reference/org/json/package-summary.html
使用 Volley 请求图片
一旦您按照前面配方的示例制作了 JSON 请求,您接下来最可能进行的调用是获取一个图像。本配方将演示如何请求一个图像来更新 ImageView。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 ImageRequest。在提示选择 Activity 类型 时,使用默认的 Phone & Tablet 选项,并选择 Empty Activity。
本配方将使用 Getting started with Volley for Internet requests 配方中描述的设置。按照步骤 1-5 将 Volley 添加到您的新项目中。
如何做到...
如前所述,将 Volley 添加到您的项目中后,请按照以下步骤操作:
-
打开
activity_main.xml并将现有的TextView替换为以下 XML:<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Request" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="sendRequest"/> -
添加以下
sendRequest()方法:public void sendRequest(View view) { final ImageView imageView = (ImageView)findViewById(R.id.imageView); RequestQueue queue = Volley.newRequestQueue(this); String url ="http://www.android.com/static/img/logos-2x/android-wordmark-8EC047.png"; ImageRequest imageRequest = new ImageRequest(url, new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap bitmap) { imageView.setImageBitmap(bitmap); } }, 0, 0, ImageView.ScaleType.CENTER, null, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { error.printStackTrace(); } }); queue.add(imageRequest); } -
在设备或模拟器上运行应用程序。
它是如何工作的...
本配方基本上与前面两个 Volley 请求的方式相同。在本配方中,我们向一个图像传递一个 URL 并在响应中加载 ImageView。
我们现在已经涵盖了三种基本请求类型:String、JSON 和 Image。
更多内容...
虽然基本类型可能已经覆盖了您的大部分需求,但 Volley 是可扩展的,您也可以通过扩展 Request<T> 来实现自定义响应。
本配方演示了我们示例代码中的一个问题。如果您更改设备的方向,您会看到图像在活动重新创建时闪烁。
创建 Volley 单例
建议将 Volley 实例化为单例。 (另一种方法是在应用程序类中创建队列。) 要在 Android Studio 中创建单例类,请转到 New | File | Singleton 并为其指定一个类名,例如 VolleySingleton。
将创建请求队列的代码移动到单例类中。如果您创建一个如下所示的方法:
public <T> void addToRequestQueue(Request<T> req) {
mRequestQueue.add(req);
}
然后,您可以使用以下代码从任何地方添加到您的队列中:
VolleySingleton.getInstance(this).addToRequestQueue(stringRequest);
使其正常工作的关键是始终使用应用程序上下文(而不是 Activity 或 Broadcast Receiver 上下文),通过在传入的上下文中调用 getApplicationContext() 来实现。
参见
- 开发者文档:应用程序(类) 在
developer.android.com/reference/android/app/Application.html
使用 Volley 的 NetworkImageView 和 ImageLoader
我们关于 Volley 的最后一个配方将不会是一个请求本身,而是 ImageView 的替代品。请求一个图像来填充 ImageView 是一项非常常见的任务;Volley 将此功能结合到一个新的视图 NetworkImageView 中。本配方将演示如何使用 NetworkImageView。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 NetworkImageView。在提示选择 Activity 类型 时,使用默认的 Phone & Tablet 选项,并选择 Empty Activity。
本配方将使用 Getting started with Volley for Internet requests 配方中描述的设置。按照步骤 1-5 将 Volley 添加到您的新项目中。
如何操作...
如前所述将 Volley 添加到你的项目中,按照以下步骤操作:
-
打开
activity_main.xml并将现有的TextView替换为以下 XML:<com.android.volley.toolbox.NetworkImageView android:id="@+id/networkImageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> -
将以下代码添加到现有的
onCreate()回调中:NetworkImageView networkImageView = (NetworkImageView)findViewById(R.id.networkImageView); String url="http://www.android.com/static/img/logos-2x/android-wordmark-8EC047.png"; RequestQueue queue = Volley.newRequestQueue(this); ImageLoader imageLoader = new ImageLoader(queue,new ImageLoader.ImageCache() { private final LruCache<String, Bitmap>cache = new LruCache<String, Bitmap>(20); @Override public Bitmap getBitmap(String url) { return cache.get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { cache.put(url, bitmap); } }); networkImageView.setImageUrl(url,imageLoader); -
你现在可以运行应用程序在设备或模拟器上了。
它是如何工作的...
这个例子与之前的 Volley 示例非常不同。我们不是创建一个请求对象,而是创建一个 ImageLoader。ImageLoader 类允许我们覆盖默认的缓存行为,例如位图的数量或如何计算大小。(我们可以将缓存改为基于总内存而不是图像数量。)有关更多信息,请参阅后面的 LruCache 链接。
创建了 ImageLoader 后,你可以将图像 URL 分配给 NetworkImageView 并将 ImageLoader 作为第二个参数传递。
还有更多...
如前一个食谱中提到的,我们 Volley 示例的问题是我们创建队列在活动中。这在图像中尤为明显,但无论如何,建议创建一个 Volley 单例。有关更多信息,请参阅前一个食谱中的 创建 Volley 单例 部分。
如果你按照前一个食谱中的描述创建了一个单例,你还可以将 ImageLoader 代码移动到单例中,并像这样公开 ImageLoader:
public ImageLoader getImageLoader() {
return mImageLoader;
}
创建单例后,这个食谱可以这样编写:
NetworkImageView networkImageView = (NetworkImageView)findViewById(R.id.networkImageView);
String url="http://www.android.com/static/img/logos-2x/android-wordmark-8EC047.png";
networkImageView.setImageUrl(url, VolleySingleton.getInstance(this).getImageLoader());
参见
- 开发者文档:LruCache 在
developer.android.com/reference/android/util/LruCache.html
第十三章:获取位置和使用地理围栏
在本章中,我们将涵盖以下主题:
-
如何获取最后位置
-
解决 GoogleApiClient OnConnectionFailedListener 报告的问题
-
如何接收位置更新
-
创建并监控地理围栏
简介
位置感知为应用提供了许多好处,实际上如此之多,以至于现在甚至桌面应用也试图获取用户的位置。位置的使用范围从逐个方向指示,到“查找最近的”应用程序,基于位置的警报,现在甚至还有基于位置的游戏,让你用设备探索世界。
Google API 提供了许多丰富的功能,用于创建具有位置感知的应用程序和地图功能。我们的第一个配方如何获取最后位置将探讨如何获取存储在设备上的最后已知位置。如果你的应用不是位置密集型,这可能提供了一种在不大量消耗资源的情况下获取用户位置的理想方式。如果你需要持续更新,那么请转向如何接收位置更新配方。虽然持续的位置更新需要更多资源,但用户可能会在你提供逐个方向指示时理解这一点。如果你需要请求接近位置的位置更新,请查看创建和监控地理围栏配方中的 Geofence 选项。
本章中的所有配方都使用 Google 库。如果你还没有下载 SDK 包,请按照 Google 的说明操作。
小贴士
从developer.android.com/sdk/installing/adding-packages.html添加 SDK 包。
现在你已经获得了位置,很可能你还会想将其映射出来。这也是 Google 在 Android 上使用 Google Maps API 使这一过程变得非常简单的一个领域。要开始使用 Google Maps,请在新项目创建时查看 Android Studio 中的Google Maps Activity选项。不要选择空白活动,正如我们通常在这些配方中所做的那样,选择Google Maps Activity,如以下截图所示:

如何获取最后位置
我们将从这个章节开始,介绍一个常见的简单配方:如何获取最后已知的位置。这是一种使用 API 且资源消耗非常小的简单方法。(这意味着,你的应用不会负责耗尽电池。)
此配方还提供了设置 Google 位置 API 的良好介绍。
准备工作
在 Android Studio 中创建一个新的项目,命名为:GetLastLocation。使用默认的手机和平板选项,当提示选择活动类型时,选择空活动。
如何操作...
首先,我们将添加必要的权限到 AndroidManifest.xml 文件中,然后创建一个包含Button和TextView元素的布局。最后,我们将创建一个GoogleAPIClient API 来访问最后的位置。打开 AndroidManifest.xml 文件,按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> -
在 Gradle Scripts 部分,打开 build.gradle (Module: app) 文件,如图所示:
![如何做...]()
-
在
dependencies部分添加以下语句:compile 'com.google.android.gms:play-services:8.4.0' -
打开
activity_main.xml并将现有的TextView替换为以下 XML:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Get Location" android:layout_centerInParent="true" android:onClick="getLocation"/> -
打开
MainActivity.java并添加以下全局变量:GoogleApiClient mGoogleApiClient; TextView mTextView; Button mButton; -
添加
ConnectionCallbacks的类:GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() { @Override public void onConnected(Bundle bundle) { mButton.setEnabled(true); } @Override public void onConnectionSuspended(int i) {} }; -
添加处理
OnConnectionFailedListener回调的类:GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult connectionResult) { Toast.makeText(MainActivity.this, connectionResult.toString(), Toast.LENGTH_LONG).show(); } }; -
将以下代码添加到现有的
onCreate()方法中:mTextView = (TextView) findViewById(R.id.textView); mButton = (Button) findViewById(R.id.button); mButton.setEnabled(false); setupGoogleApiClient(); -
添加设置
GoogleAPIClient的方法:protected synchronized void setupGoogleApiClient() { mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(mConnectionCallbacks) .addOnConnectionFailedListener(mOnConnectionFailedListener) .addApi(LocationServices.API) .build(); mGoogleApiClient.connect(); } -
为按钮点击添加以下方法:
public void getLocation(View view) { try { Location lastLocation = LocationServices.FusedLocationApi.getLastLocation( mGoogleApiClient); if (lastLocation != null) { mTextView.setText( DateFormat.getTimeInstance().format(lastLocation.getTime()) + "\n" + "Latitude="+lastLocation.getLatitude() + "\n" + "Longitude=" + lastLocation.getLongitude()); } else { Toast.makeText(MainActivity.this, "null", Toast.LENGTH_LONG).show(); } } catch (SecurityException e) {e.printStackTrace();} } -
您现在可以运行应用在设备或模拟器上了。
它是如何工作的...
在我们能够调用 getLastLocation() 方法之前,我们需要设置 GoogleApiClient。我们在 setupGoogleApiClient() 方法中调用 GoogleApiClient.Builder 方法,然后连接到库。当库准备好时,它调用我们的 ConnectionCallbacks.onConnected() 方法。出于演示目的,这就是我们启用按钮的地方。(我们将在后面的菜谱中使用这个回调来启动其他功能。)
我们使用一个按钮来展示我们可以按需调用 getLastLocation();这不是一次性的调用。系统负责更新位置,并且可能在重复调用时返回相同的位置。(这可以在时间戳中看到——这是位置时间戳,而不是按钮按下时的时间戳。)
这种按需调用位置的方法在您只需要在应用中发生某些事件时(例如对对象进行地理编码)需要位置的情况下很有用。由于系统负责位置更新,您的应用将不会因位置更新而耗电。
我们收到的位置对象的精度基于我们的权限设置。我们使用了 ACCESS_COARSE_LOCATION,但如果我们想要更高的精度,我们可以请求 ACCESS_FINE_LOCATION,如下所示权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
最后,为了使代码专注于 GoogleApiClient,我们只需将 getLastLocation() 包装在 SecurityException 中。在生产应用中,您应该检查并请求权限,如前一章所示。(参见 新的运行时权限模型。)
更多内容...
如果连接到 GoogleApiClient 时出现问题,将调用 OnConnectionFailedListener。在这个例子中,我们显示一个 Toast。下一个菜谱,解决 GoogleApiClient OnConnectionFailedListener 报告的问题,将展示处理这种情况的更健壮的方法。
测试位置可能是一个挑战,因为在测试和调试时很难实际移动设备。幸运的是,我们有能力使用模拟器模拟 GPS 数据。(在物理设备上创建模拟位置也是可能的,但并不容易。)
模拟位置
使用模拟器模拟位置有三种方法:
-
Android Studio
-
DDMS
-
通过 Telnet 的
Geo命令
在 Android Studio 中设置模拟位置,请按照以下步骤操作:
-
导航到 工具 | Android | Android 设备监控器。
-
在设备窗口中选择 模拟器控制 选项卡。
-
在 位置控制 下输入 GPS 坐标。
下面是一个显示 位置 控制 的截图:

小贴士
并非通过发送 GPS 数据来模拟位置有效。因此,为了让您的应用接收模拟位置,它需要接收 GPS 数据。测试 lastLocation() 可能不会发送模拟 GPS 数据,因为它并不完全依赖于 GPS 来确定设备位置。尝试使用 如何接收位置更新 的配方来使用模拟位置,我们可以在其中请求优先级。(我们无法强制系统使用任何特定的位置传感器,我们只能提出请求。系统将选择最佳解决方案来提供结果。)
参见
-
在第十四章 “为 Play 商店准备您的应用” 的 “新的 Android 6.0 运行时权限模型” 配方中,“为 Play 商店准备您的应用”
-
设置 Google Play 服务:
developers.google.com/android/guides/setup -
FusedLocationProviderApi 接口:
developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderApi
解决 GoogleApiClient OnConnectionFailedListener 报告的问题
由于 Google API 的不断变化,您的用户可能会尝试使用您的应用程序,但由于他们的文件过时,可能无法使用。在之前的示例中,我们只显示了一个 Toast,但我们可以做得更好。我们可以使用 GoogleApiAvailability 库来显示一个对话框,以帮助用户解决问题。
我们将继续使用之前的配方,并向 onConnectionFailed() 回调中添加代码。我们将使用错误结果向用户显示额外的信息以解决问题。
准备就绪
此配方将从之前的配方 如何获取最后位置 继续进行。如果您是从下载的源文件加载项目,它被称为 HandleGoogleAPIError。
如何操作...
由于我们正在继续之前的配方,我们只需涵盖更新之前代码所需的步骤。打开 ActivityMain.java 并按照以下步骤操作:
-
将以下行添加到全局类变量中:
private final int REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR=1; boolean mResolvingError; -
将以下方法添加以显示 Google API 错误对话框:
private void showGoogleAPIErrorDialog(int errorCode) { GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); Dialog errorDialog = googleApiAvailability.getErrorDialog(this, errorCode, REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR); errorDialog.show(); } -
将以下代码添加以覆盖
onActivityResult():@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR) { mResolvingError = false; if (resultCode == RESULT_OK && !mGoogleApiClient.isConnecting() && !mGoogleApiClient.isConnected()) { mGoogleApiClient.connect(); } } } -
在
onConnectionFailed()中,替换现有的调用 Toast 的代码行,使用以下代码:if (mResolvingError) { return; } else if (connectionResult.hasResolution()) { mResolvingError = true; try { connectionResult.startResolutionForResult(MainActivity.this, REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR); } catch (IntentSender.SendIntentException e) { mGoogleApiClient.connect(); } } else { showGoogleAPIErrorDialog(connectionResult.getErrorCode()); } -
您现在可以运行应用程序在设备或模拟器上了。
它是如何工作的...
与我们之前使用 Toast 显示错误消息不同,我们现在检查 connectionResult 来查看我们可以做什么。GoogleAPIClient 使用 connectionResult 来指示可能的行动方案。我们可以调用 hasResolution() 方法,如下所示:
connectionResult.hasResolution()
如果响应是 true,那么这是用户可以解决的问题,例如启用位置服务。如果响应是 false,我们得到一个 GoogleApiAvailability 实例并调用 getErrorDialog() 方法。完成后,我们的 onActivityResult() 回调被调用,其中我们重置 mResolvingError,如果成功,尝试重新连接。
小贴士
如果你没有设备用于测试较旧的 Google API,你可以尝试在具有较旧 Google API 版本的模拟器上测试。
还有更多...
如果你的应用程序正在使用片段,你可以使用此代码获取一个对话框片段:
ErrorDialogFragment errorFragment = new ErrorDialogFragment();
Bundle args = new Bundle();
args.putInt("dialog_error", errorCode);
errorFragment.setArguments(args);
errorFragment.show(getSupportFragmentManager(), "errordialog");
相关内容
- 访问 Google API:
developers.google.com/android/guides/api-client
如何接收位置更新
如果你的应用程序需要频繁的位置更新,你的应用程序可以请求周期性更新。这个菜谱将使用 GoogleApiClient 的 requestLocationUpdates() 方法来演示这一点。
准备工作
在 Android Studio 中创建一个新的项目并命名为:LocationUpdates。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
由于我们从系统中接收更新,我们不需要为这个菜谱添加按钮。我们的布局将只包含 TextView 来查看位置数据。打开 AndroidManifest.xml 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> -
打开文件
build.gradle (Module: app)并在dependencies部分添加以下语句:compile 'com.google.android.gms:play-services:8.4.0' -
打开
activity_main.xml并将现有的TextView替换为以下 XML:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> -
打开
MainActivity.java并添加以下全局变量:GoogleApiClient mGoogleApiClient; LocationRequest mLocationRequest; TextView mTextView; -
创建以下
LocationListener类:LocationListener mLocationListener = new LocationListener() { @Override public void onLocationChanged(Location location) { if (location != null) { mTextView.setText( DateFormat.getTimeInstance().format(location.getTime()) + "\n" + "Latitude="+location.getLatitude()+"\n" + "Longitude="+location.getLongitude()); } } }; -
创建一个
ConnectionCallbacks类来接收位置更新:GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() { @Override public void onConnected(Bundle bundle) { Log.i("onConnected()", "start"); try { LocationServices.FusedLocationApi.requestLocationUpdates( mGoogleApiClient, mLocationRequest, mLocationListener); } catch (SecurityException e) { Log.i("onConnected()","SecurityException: "+e.getMessage()); } } @Override public void onConnectionSuspended(int i) {} }; -
创建一个
OnConnectionFailedListener类:GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult connectionResult) { Toast.makeText(MainActivity.this, connectionResult.toString(), Toast.LENGTH_LONG).show(); Log.i("onConnected()", "SecurityException: " +connectionResult.toString()); } }; -
将以下代码添加到现有的
onCreate()回调中:mTextView = (TextView) findViewById(R.id.textView); setupLocationRequest(); -
创建
setupLocationRequest()方法:protected synchronized void setupLocationRequest() { mLocationRequest = new LocationRequest(); mLocationRequest.setInterval(10000); mLocationRequest.setFastestInterval(10000); mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(mConnectionCallbacks) .addOnConnectionFailedListener(mOnConnectionFailedListener) .addApi(LocationServices.API) .build(); mGoogleApiClient.connect(); } -
你已经准备好在设备或模拟器上运行应用程序了。
它是如何工作的...
这个菜谱与 如何获取最后位置 菜谱类似,因为我们需要像之前一样设置 GoogleApiClient。但是,我们不是在需要时调用 lastLocation() 方法,而是调用 requestLocationUpdates() 方法通过 LocationListener 类接收周期性的位置更新。
requestLocationUpdates() 方法需要三个参数:
-
GoogleApiClient -
LocationRequest -
LocationListener
我们像之前一样创建 GoogleApiClient。这是创建我们的 LocationRequest 的代码:
mLocationRequest = new LocationRequest();
mLocationRequest.setInterval(10000);
mLocationRequest.setFastestInterval(10000);
mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
当调用 setInterval() 时,通常最好使用最慢的延迟,这样对设备资源的需求更少。当调用 setPriority() 时,这个想法同样适用。第三个参数,即 LocationListener,是我们定义回调方法 onLocationChanged() 的地方。在这里,我们只是显示位置数据以及位置时间戳。
更多内容...
与之前的 Android API 不同,GoogleApiClient API 不允许选择特定的传感器进行位置更新。如 如何获取最后位置 部分的 模拟位置 所述,使用 LocationRequest.PRIORITY_HIGH_ACCURACY 与 ACCESS_FINE_LOCATION 权限应使用 GPS 传感器。请参阅 模拟位置 部分以获取模拟位置的说明。
停止接收位置更新
当你的应用程序不再需要位置更新时,调用 removeLocationUpdates() 方法,如下所示:
LocationServices.FusedLocationApi.removeLocationUpdates(
mGoogleApiClient, mLocationListener);
通常,当你的应用程序不再在前台时,你会想要禁用更新,但这取决于你具体的应用程序需求。如果你的应用程序需要持续更新,可能更希望创建一个后台服务来处理回调。
参见
- 开发者文档:onLocationChanged 在
developer.android.com/reference/com/google/android/gms/location/LocationRequest.html
创建和监控地理围栏
如果你的应用程序需要知道用户何时进入某个特定位置,有一个替代方案,即不需要持续检查用户的位置:地理围栏。地理围栏是一个位置(纬度和经度)以及一个半径。你可以创建一个地理围栏,并让系统在用户进入你指定的位置附近时通知你。(Android 目前允许每个用户最多 100 个地理围栏。)
地理围栏属性包括:
-
位置:经度和纬度
-
半径:圆的大小(以米为单位)
-
逗留延迟:用户在发送通知之前可以在半径内停留多长时间
-
过期时间:地理围栏自动过期前的时间
-
过渡 类型:以下列出了这些类型:
-
GEOFENCE_TRANSITION_ENTER -
GEOFENCE_TRANSITION_EXIT -
INITIAL_TRIGGER_DWELL
-
这个菜谱将展示如何创建地理围栏对象,并使用它来创建 GeofencingRequest 的实例。
准备工作
在 Android Studio 中创建一个新的项目,命名为:Geofence。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
我们不需要为这个菜谱创建布局,因为我们将会使用 Toasts 和通知来进行用户交互。我们需要创建一个额外的 Java 类来处理 IntentService,它负责处理地理围栏警报。打开 Android Manifest 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> -
打开文件
build.gradle (Module: app)并在dependencies部分添加以下语句:compile 'com.google.android.gms:play-services:8.4.0' -
创建一个新的 Java 类
GeofenceIntentService并扩展IntentService类。声明如下:public class GeofenceIntentService extends IntentService { -
添加以下构造函数:
public GeofenceIntentService() { super("GeofenceIntentService"); } -
添加
onHandleIntent()以接收地理围栏警报:protected void onHandleIntent(Intent intent) { GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); if (geofencingEvent.hasError()) { Toast.makeText(getApplicationContext(), "Geofence error code= " + geofencingEvent.getErrorCode(), Toast.LENGTH_SHORT).show(); return; } int geofenceTransition = geofencingEvent.getGeofenceTransition(); if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_DWELL) { sendNotification(); } } -
添加
sendNotification()方法以向用户显示消息:private void sendNotification() { Log.i("GeofenceIntentService", "sendNotification()"); Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle("Geofence Alert") .setContentText("GEOFENCE_TRANSITION_DWELL") .setSound(notificationSoundUri) .setLights(Color.BLUE, 500, 500); NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(0, notificationBuilder.build()); } -
打开 Android 清单并在
<application>元素内添加以下内容,与<activity>元素处于同一级别:<service android:name=".GeofenceIntentService"/> -
打开
MainActivity.java并添加以下全局变量:private final int MINIMUM_RECOMENDED_RADIUS=100; GoogleApiClient mGoogleApiClient; PendingIntent mGeofencePendingIntent; -
创建以下
ResultCallback类:ResultCallback mResultCallback = new ResultCallback() { @Override public void onResult(Result result) { Log.i("onResult()", "result: " + result.getStatus().toString()); } }; -
创建一个
ConnectionCallbacks类:GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() { @Override public void onConnected(Bundle bundle) { try { LocationServices.GeofencingApi.addGeofences( mGoogleApiClient, createGeofencingRequest(), getGeofencePendingIntent() ).setResultCallback(mResultCallback); } catch (SecurityException e) { Log.i("onConnected()", "SecurityException: " + e.getMessage()); } } @Override public void onConnectionSuspended(int i) {} }; -
创建一个
OnConnectionFailedListener类:GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult connectionResult) { Log.i("onConnectionFailed()", "connectionResult: " +connectionResult.toString()); } }; -
将以下代码添加到现有的
onCreate()回调中:setupGoogleApiClient(); -
添加设置
GoogleAPIClient的方法:protected synchronized void setupGoogleApiClient() { mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(mConnectionCallbacks) .addOnConnectionFailedListener(mOnConnectionFailedListener) .addApi(LocationServices.API) .build(); mGoogleApiClient.connect(); } -
创建
setupGoogleApiClient()方法:protected synchronized void setupGoogleApiClient() { mGoogleApiClient = new GoogleApiClient.Builder(this) .addConnectionCallbacks(mConnectionCallbacks) .addOnConnectionFailedListener(mOnConnectionFailedListener) .addApi(LocationServices.API) .build(); mGoogleApiClient.connect(); } -
使用以下方法创建一个挂起意图:
private PendingIntent getGeofencePendingIntent() { if (mGeofencePendingIntent != null) { return mGeofencePendingIntent; } Intent intent = new Intent(this, GeofenceIntentService.class); return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } -
创建
geofence对象并将其添加到请求的列表中:private List createGeofenceList() { List<Geofence> geofenceList = new ArrayList<Geofence>(); geofenceList.add(new Geofence.Builder() .setRequestId("GeofenceLocation") .setCircularRegion( 37.422006, //Latitude -122.084095, //Longitude MINIMUM_RECOMENDED_RADIUS) .setLoiteringDelay(30000) .setExpirationDuration(Geofence.NEVER_EXPIRE) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL) .build()); return geofenceList; } -
按如下方式创建
createGeofencingRequest()方法:private GeofencingRequest createGeofencingRequest() { GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL); builder.addGeofences(createGeofenceList()); return builder.build(); } -
您现在可以运行应用程序在设备或模拟器上。
它是如何工作的...
首先,我们添加 ACCESS_FINE_LOCATION 权限,因为这是地理围栏所必需的。我们设置 GoogleApiClient,就像我们在之前的菜谱中所做的那样,并等待 onConnected() 被调用以设置 GeofencingApi。
在我们可以调用 GeofencingApi.addGeofences() 方法之前,我们必须准备三个对象:
-
GoogleApiClient -
地理围栏请求
-
悬挂意图
我们已经创建了 GoogleApiClient,并将其保存在 mGoogleApiClient 中。
要创建地理围栏请求,我们使用 GeofencingRequest.Builder。构建器需要地理围栏对象的列表,这些对象是在 createGeofenceList() 方法中创建的。(尽管我们只创建了一个地理围栏对象,但构建器需要一个列表,所以我们只需将我们的单个地理围栏添加到 ArrayList 中。)这就是我们设置地理围栏属性的地方:
.setRequestId("GeofenceLocation")
.setCircularRegion(
37.422006, //Latitude
-122.084095, //Longitude
MINIMUM_RECOMENDED_RADIUS)
.setLoiteringDelay(30000)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL)
只有逗留延迟是可选的,但由于我们正在使用 DWELL 转换,我们需要它。当调用 setTransitionTypes() 时,我们可以使用 OR 运算符组合多个转换类型,如下所示,这里使用 ENTER 和 EXIT 作为示例:
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)
对于这个示例,我们使用了与模拟器相同的默认纬度和经度。根据需要更改这些值。
我们对 Geofence.Builder() 的调用创建了地理围栏对象。有了准备好的地理围栏列表,我们调用 GeofencingRequest.Builder 并将我们的初始触发设置为 INITIAL_TRIGGER_DWELL。(如果您更改了前面的转换类型,您可能还想更改初始触发。)
我们需要的最后一个对象是 Pending Intent,这是系统在地理围栏条件满足时通知我们的应用的方式。我们创建了GeofenceIntentService来处理地理围栏意图,通过向用户发送通知来处理。(有关通知的更多信息,请参阅第七章中的使用通知的“灯光、动作和声音重置”配方,警报和通知。)
创建了所有三个对象后,我们只需调用LocationServices.GeofencingApi.addGeofences()并等待通知到达。
还有更多...
要停止接收地理围栏通知,你可以使用removeGeofences()方法,并传入RequestID参数或PendingIntent。以下示例使用了与通知相同的PendingIntent方法:
LocationServices.GeofencingApi.removeGeofences(
mGoogleApiClient,
getGeofencePendingIntent()
).setResultCallback(mResultCallback);
参见
-
Geofence.Builder类位于:developers.google.com/android/reference/com/google/android/gms/location/Geofence.Builder.html -
GeofencingRequest.Builder类位于:developers.google.com/android/reference/com/google/android/gms/location/GeofencingRequest.Builder
第十四章。让你的应用为 Play Store 做好准备
在本章中,我们将涵盖以下主题:
-
新的 Android 6.0 运行时权限模型
-
如何安排闹钟
-
接收设备启动通知
-
使用 AsyncTask 进行后台工作
-
将语音识别添加到你的应用中
-
使用 Google Cloud Messaging 进行推送通知
-
如何将 Google sign-in 添加到你的应用
简介
随着我们接近本书的结尾,是时候在你将应用发布到 Play Store 之前,给你的应用添加一些最后的修饰了。本章中的菜谱涵盖了那些可能让用户保留还是删除你的应用的话题。
我们的第一道菜谱,新的 Android 6.0 运行时权限模型,无疑是一个重要的话题,可能是 Android 从 5.x 版本跃升至 6.0 版本的主要原因!对 Android 权限模型的改变已经呼吁了一段时间,所以这个新模型是一个受欢迎的改变,至少对于用户来说是这样。
接下来,我们将探讨在 Android 中使用闹钟。闹钟的主要好处之一是操作系统负责维护闹钟,即使你的应用没有运行。由于闹钟在设备重启后不会持续存在,我们还将探讨如何检测设备重启,这样你就可以在接收设备启动通知中重新创建你的闹钟。
几乎任何严肃的 Android 应用都需要一种方法来在主线程之外执行可能阻塞的任务。否则,你的应用可能会被认为运行缓慢,或者更糟,完全无响应。AsyncTask就是为了使创建后台工作任务变得更加容易而设计的,正如我们将在使用 AsyncTask 进行后台工作菜谱中所展示的那样。
如果你希望你的应用能够从免提输入或语音识别中受益,请查看将语音识别添加到你的应用中菜谱,我们将探讨 Google 语音 API。
与用户通信的最有趣的功能之一可能是推送通知或Google Cloud Messaging(GCM),正如谷歌所称呼的。使用 Google Cloud Messaging 进行推送通知菜谱将指导你如何将 GCM 添加到你的应用中,并解释更大的背景。
最后,我们将以一个菜谱结束本章,展示如何让你的应用更加舒适,并鼓励用户使用如何将 Google Sign-In 添加到你的应用菜谱进行登录。
新的 Android 6.0 运行时权限模型
旧的安全模型是 Android 中许多人的痛点。经常可以看到评论中提到应用所需的权限。有时,权限超出了界限(比如手电筒应用需要互联网权限),但有时,开发者有很好的理由请求某些权限。主要问题是这是一个全有或全无的选择。
这最终随着 Android 6 Marshmallow(API 23)的发布而改变。新的权限模型仍然在清单中声明权限,就像之前一样,但用户可以选择性地接受或拒绝每个权限。用户甚至可以撤销之前授予的权限。
虽然这对许多人来说是一个受欢迎的变革;然而,对于开发者来说,它有可能破坏之前正常工作的代码。我们已经在之前的菜谱中讨论了此权限变更,因为它具有深远的影响。这个菜谱将把所有这些放在一起,以便在您自己的应用中实施此更改时作为一个单独的参考点。
一个需要记住的重要点是,此更改仅影响 Android 6.0(API 23)及以上的用户。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 RuntimePermission。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
示例源代码将最小 API 设置为 23,但这不是必需的。如果您的 compileSdkVersion 是 API 23 或更高,编译器将标记您的代码以使用新的安全模型。
如何操作...
我们需要首先将所需的权限添加到清单中,然后我们将添加一个按钮来调用检查权限的代码。打开 Android 清单并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.SEND_SMS"/> -
打开
activity_main.xml并将现有的TextView替换为以下按钮:<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Do Something" android:layout_centerInParent="true" android:onClick="doSomething"/> -
打开
MainActivity.java并将以下常量添加到类中:private final int REQUEST_PERMISSION_SEND_SMS=1; -
添加此方法以进行权限检查:
private boolean checkPermission(String permission) { int permissionCheck = ContextCompat.checkSelfPermission( this, permission); return (permissionCheck == PackageManager.PERMISSION_GRANTED); } -
添加此方法以显示解释对话框:
private void showExplanation(String title, String message, final String permission, final int permissionRequestCode) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(title).setMessage(message).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { requestPermission(permission, permissionRequestCode); } }); builder.create().show(); } -
添加此方法以请求权限:
private void requestPermission(String permissionName, int permissionRequestCode) { ActivityCompat.requestPermissions(this, new String[]{permissionName}, permissionRequestCode); } -
添加按钮点击的方法:
public void doSomething(View view) { if (!checkPermission(Manifest.permission.SEND_SMS)) { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.SEND_SMS)) { showExplanation("Permission Needed", "Rationale", Manifest.permission.SEND_SMS, REQUEST_PERMISSION_SEND_SMS); } else { requestPermission(Manifest.permission.SEND_SMS, REQUEST_PERMISSION_SEND_SMS); } } else { Toast.makeText(MainActivity.this, "Permission (already) Granted!", Toast.LENGTH_SHORT).show(); } } -
如下重写
onRequestPermissionsResult():@Override public void onRequestPermissionsResult( int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case REQUEST_PERMISSION_SEND_SMS: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { Toast.makeText(MainActivity.this, "Permission Granted!", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "Permission Denied!", Toast.LENGTH_SHORT).show(); } return; } } } -
现在,您已经准备好在设备或模拟器上运行应用程序了。
工作原理...
使用新的运行时权限模型涉及以下步骤:
-
检查您是否具有所需的权限。
-
如果不是,检查是否应该显示理由(即,请求之前已被拒绝)。
-
请求权限;只有操作系统可以显示权限请求。
-
处理请求响应。
这里是相应的函数:
-
ContextCompat.checkSelfPermission -
ActivityCompat.requestPermissions -
ActivityCompat.shouldShowRequestPermissionRationale -
onRequestPermissionsResult注意
尽管您是在运行时请求权限,但所需的权限必须在 Android 清单中列出。如果没有指定权限,操作系统将自动拒绝请求。
还有更多...
您可以通过以下方式使用 ADB 授予/撤销权限:
adb shell pm [grant|revoke] <package> <permission-name>
这里有一个示例,为我们的测试应用授予发送短信权限:
adb shell pm grant com.packtpub.androidcookbook.runtimepermissions android.permission.SEND_SMS
参见
如何安排闹钟
Android 提供了 AlarmManager 来创建和安排闹钟。闹钟提供以下功能:
-
安排特定时间或间隔的警报
-
由操作系统维护,而不是您的应用程序,因此即使您的应用程序没有运行或设备处于睡眠状态,警报也会被触发
-
可以用来触发周期性任务(如每小时新闻更新),即使应用程序没有运行
-
您的应用程序不使用资源(如计时器或后台服务),因为操作系统管理调度
如果在应用程序运行时需要简单的延迟,例如对 UI 事件的短暂延迟,警报并不是最佳解决方案。对于短暂的延迟,使用处理器(Handler)会更简单、更高效,正如我们在之前的几个菜谱中做的那样。
使用警报时,请记住以下最佳实践:
-
尽可能使用不频繁的警报时间
-
避免唤醒设备
-
尽可能使用不精确的时间——时间越精确,所需的资源就越多
-
避免基于时钟时间设置警报时间(如 12:00);如果可能,添加随机调整以避免服务器(尤其是在检查新内容,如天气或新闻时)拥堵
警报有三个属性,如下所示:
-
警报类型(见以下列表)
-
触发时间(如果时间已经过去,警报将立即触发)
-
悬挂意图(Pending Intent)
重复警报具有相同的三个属性,加上一个间隔:
-
警报类型(见以下列表)
-
触发时间(如果时间已经过去,它将立即触发)
-
间隔
-
悬挂意图
有四种警报类型:
-
RTC(实时时钟):这是基于墙上的时钟时间。这不会唤醒设备。 -
RTC_WAKEUP:这是基于墙上的时钟时间。如果设备处于睡眠状态,则会唤醒设备。 -
ELAPSED_REALTIME:这是基于设备启动以来经过的时间。这不会唤醒设备。 -
ELAPSED_REALTIME_WAKEUP:这是基于设备启动以来经过的时间。如果设备处于睡眠状态,则会唤醒设备。
实际经过的时间(Elapsed Real Time)更适合时间间隔警报——例如每 30 分钟一次。
小贴士
警报在设备重启后不会持久化。当设备关闭时,所有警报都会被取消,因此,在设备启动时重置警报是您应用程序的责任。(有关更多信息,请参阅 接收设备启动通知。)
以下菜谱将演示如何使用 AlarmManager 创建警报。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:Alarms。使用默认的 手机和平板电脑 选项,并在提示 活动类型 时选择 空活动。
如何操作...
设置警报需要悬挂意图(Pending Intent),当警报被触发时,Android 会发送它。因此,我们需要设置广播接收器来捕获警报意图。我们的 UI 将仅包含一个简单的按钮来设置警报。首先,打开 AndroidManifest.xml,并按照以下步骤操作:
-
将以下
<receiver>添加到<application>元素中,与现有的<activity>元素处于同一级别:<receiver android:name=".AlarmBroadcastReceiver"> <intent-filter> <action android:name="com.packtpub.androidcookbook.alarms.ACTION_ALARM" /> </intent-filter> </receiver> -
打开
activity_main.xml并将现有的 TextView 替换为以下按钮:<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Set Alarm" android:layout_centerInParent="true" android:onClick="setAlarm"/> -
使用以下代码创建一个新的 Java 类
AlarmBroadcastReceiver:public class AlarmBroadcastReceiver extends BroadcastReceiver { public static final String ACTION_ALARM="com.packtpub.androidcookbook.alarms.ACTION_ALARM"; @Override public void onReceive(Context context, Intent intent) { if (ACTION_ALARM.equals(intent.getAction())) { Toast.makeText(context, ACTION_ALARM, Toast.LENGTH_SHORT).show(); } } } -
打开
ActivityMain.java并添加按钮点击的方法:public void setAlarm(View view) { Intent intentToFire = new Intent(getApplicationContext(), AlarmBroadcastReceiver.class); intentToFire.setAction(AlarmBroadcastReceiver.ACTION_ALARM); PendingIntent alarmIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intentToFire, 0); AlarmManager alarmManager = (AlarmManager)getApplicationContext().getSystemService(Context.ALARM_SERVICE); long thirtyMinutes=SystemClock.elapsedRealtime() + 30 * 60 * 1000; alarmManager.set(AlarmManager.ELAPSED_REALTIME, thirtyMinutes, alarmIntent); } -
你已经准备好在设备或模拟器上运行应用程序。
它是如何工作的...
创建闹钟是通过这一行代码完成的:
alarmManager.set(AlarmManager.ELAPSED_REALTIME, thirtyMinutes, alarmIntent);
这是方法签名:
set(AlarmType, Time, PendingIntent);
注意
在 Android 4.4 KitKat(API 19)之前,这是请求精确时间的方法。Android 4.4 及以后的版本将考虑这为效率而近似的时间,但不会在请求时间之前传递意图。(如果你需要精确时间,请参见以下 setExact()。)
要设置闹钟,我们创建一个带有我们之前定义的闹钟动作的 Pending Intent:
public static final String ACTION_ALARM="com.packtpub.androidcookbook.alarms.ACTION_ALARM";
(这是一个任意的字符串,可以是任何我们想要的,但它需要是唯一的,因此我们在包名前加上。)我们在广播接收器的 onReceive() 回调中检查这个动作。
还有更多...
如果你点击 Set Alarm 按钮并等待三十分钟,你将在闹钟触发时看到 Toast。如果你太急躁,在第一个闹钟触发之前再次点击 Set Alarm 按钮,你不会得到两个闹钟。相反,操作系统将用新的闹钟替换第一个闹钟,因为它们都使用了相同的 Pending Intent。(如果你需要多个闹钟,你需要创建不同的 Pending Intent,例如使用不同的 Actions。)
取消闹钟
如果你想取消闹钟,通过传递创建闹钟时使用的相同的 Pending Intent 来调用 cancel() 方法。如果我们继续我们的配方,它看起来会是这样:
alarmManager.cancel(alarmIntent);
重复闹钟
如果你想要创建一个重复的闹钟,请使用 setRepeating() 方法。签名与 set() 方法类似,但有一个间隔。如下所示:
setRepeating(AlarmType, Time (in milliseconds), Interval, PendingIntent);
对于间隔,你可以指定以毫秒为单位的间隔时间,或者使用预定义的 AlarmManager 常量之一:
-
INTERVAL_DAY -
INTERVAL_FIFTEEN_MINUTES -
INTERVAL_HALF_DAY -
INTERVAL_HALF_HOUR -
INTERVAL_HOUR
参见
- 开发者文档:AlarmManager 在
developer.android.com/reference/android/app/AlarmManager.html
接收设备启动通知
Android 在其生命周期中发送出许多意图。最早发送的意图之一是 ACTION_BOOT_COMPLETED。如果你的应用程序需要知道设备何时启动,你需要捕获这个意图。
这个配方将指导你完成在设备启动时接收通知所需的步骤。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 DeviceBoot。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
首先,打开 AndroidManifest.xml 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> -
将以下
<receiver>添加到<application>元素中,与现有的<activity>元素处于同一级别:<receiver android:name=".BootBroadcastReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver> -
使用以下代码创建一个新的 Java 类
BootBroadcastReceiver:public class BootBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) { Toast.makeText(context, "BOOT_COMPLETED", Toast.LENGTH_SHORT).show(); } } } -
重新启动设备以查看 Toast。
它是如何工作的...
当设备启动时,Android 会发送 BOOT_COMPLETED 意图。只要我们的应用程序有接收意图的权限,我们就会在我们的广播接收器中收到通知。
要使此功能正常工作,有三个方面需要考虑:
-
RECEIVE_BOOT_COMPLETED的权限 -
将
BOOT_COMPLETED添加到接收器意图过滤器 -
在广播接收器中检查
BOOT_COMPLETED动作
显然,您会想用您自己的代码替换 Toast 消息,例如,用于重新创建您可能需要的任何闹钟。
更多内容...
如果您遵循了前面的食谱,那么您已经有一个广播接收器。您不需要为每个动作创建单独的 BroadcastReceiver,只需按需检查每个动作即可。以下是一个示例,如果我们需要处理另一个动作:
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("android.intent.action.BOOT_COMPLETED")) {
Toast.makeText(context, "BOOT_COMPLETED", Toast.LENGTH_SHORT).show();
} else if (intent.getAction().equals("<another_action>")) {
//handle another action
}
}
参见
- 开发者文档:Intent 在
developer.android.com/reference/android/content/Intent.html
使用 AsyncTask 进行后台工作
在整本书中,我们提到了不阻塞主线程的重要性。在主线程上执行长时间运行的操作可能会导致您的应用程序看起来反应迟缓,或者更糟,挂起。如果您的应用程序在约 5 秒内没有响应,系统可能会显示 应用程序未响应 (ANR) 对话框,并提供终止您的应用程序的选项。(您会希望避免这种情况,因为这可能是您的应用程序被卸载的好方法。)
Android 应用程序使用单线程模型,有两个简单的规则,如下所示:
-
不要阻塞主线程
-
在主线程上执行所有 UI 操作
当 Android 启动您的应用程序时,它会自动创建主(或 UI)线程。这是所有 UI 操作必须调用的线程。第一条规则是“不要阻塞主线程”。这意味着您需要为任何长时间运行或可能阻塞的任务创建一个后台或工作线程。这就是为什么所有基于网络的任务都应该在主线程之外执行。
在处理后台线程时,Android 提供以下选项:
-
Activity.runOnUiThread() -
View.post() -
View.postDelayed() -
Handler -
AsyncTask
本食谱将探讨 AsyncTask 类;由于它已经被创建,您就不必直接使用 Handler 或 post 方法。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为:AsyncTask。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何做到这一点...
在此示例中,我们只需要一个按钮。打开 activity_main.xml 并按照以下步骤操作:
-
用以下按钮替换现有的 TextView:
<Button android:id="@+id/buttonStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start" android:layout_centerInParent="true" android:onClick="start" /> -
打开
MainActivity.java并添加以下全局变量:Button mButtonStart; -
添加
AsyncTask类:private class CountingTask extends AsyncTask<Integer, Integer, Integer> { @Override protected Integer doInBackground(Integer... params) { int count = params[0]; for (int x=0;x<=count; x++){ //Nothing to do } return count; } @Override protected void onPostExecute(Integer integer) { super.onPostExecute(integer); mButtonStart.setEnabled(true); } } -
将以下代码添加到
onCreate()中以初始化按钮:mButtonStart=(Button)findViewById(R.id.buttonStart); -
添加按钮点击的方法:
public void start(View view){ mButtonStart.setEnabled(false); new CountingTask().execute(10000000); } -
您现在可以开始在设备或模拟器上运行应用程序。
它是如何工作的...
这是一个 AsyncTask 的非常简单的例子,只是为了让它工作。技术上,只需要 doInBackground(),但通常,你可能希望在它完成时通过 onPostExecute() 接收通知。
AsyncTask 通过为 doInBackground() 方法创建一个工作线程来工作,然后在 onPostExecute() 回调中在 UI 线程上做出响应。
注意我们是如何等待 onPostExecute() 被调用之后才执行任何 UI 操作,例如启用按钮。如果我们尝试在工作线程中修改 UI,则要么无法编译,要么抛出运行时异常。你还应该注意我们是如何在每次按钮点击时实例化一个新的 CountingTask 对象。这是因为 AsyncTask 只能执行一次。再次调用 execute 也会抛出异常。
还有更多...
在其最简单的情况下,AsyncTask 可以非常简单,但如果需要,它仍然非常灵活。当使用 AsyncTask 与 Activity 时,了解 Activity 是否被销毁并重新创建(例如在方向改变期间)或 AsyncTask 是否继续运行是很重要的。这可能会导致你的 AsyncTask 成孤儿,并且它可能会对现在已销毁的活动做出响应(导致 NullPointer 异常)。因此,通常使用 AsyncTask 与 Fragment(在屏幕旋转时不会被销毁)一起使用,或者使用 Loader 代替。(有关下一节中 Loaders 的链接,请参阅链接。)
参数类型
对于许多人来说,AsyncTask 最令人困惑的方面是在创建自己的类时参数的使用。如果你查看我们的类声明,AsyncTask 有三个参数;它们如下定义:
AsyncTask<Params, Progress, Result >
参数是泛型类型,如下使用:
-
Params:这是调用
doInBackground()的参数类型 -
Progress:这是用于发布更新的参数类型
-
Result:这是用于发布结果的参数类型
当你声明自己的类时,用你需要的变量类型替换参数。
这是 AsyncTask 的流程和如何使用先前参数的示例:
-
onPreExecute():在doInBackground()开始之前被调用 -
doInBackground(Params):在后台线程中执行 -
onProgressUpdate(Progress):在响应工作线程中调用的publishProgress(Progress)时(在 UI 线程上)被调用 -
onPostExecute(Result):当工作线程完成时(在 UI 线程上)被调用
取消任务
要取消任务,请按照以下方式在对象上调用 cancel 方法:
< AsyncTask>.cancel(true);
你将需要对象实例来访问 cancel() 方法。(在我们的前一个例子中我们没有保存对象。)在设置 cancel(true) 之后,doInBackground() 中的调用 isCancelled() 将返回 true,允许你退出循环。如果取消,将调用 onCancelled() 而不是 onPostExecute()。
参见
-
参考第六章 “使用数据” 中的 使用 Loader 在后台访问数据 菜谱,使用数据
-
开发者文档:AsyncTask 在
developer.android.com/reference/android/os/AsyncTask.html
将语音识别添加到你的应用程序中
Android 2.2(API 8)在 Android 中引入了语音识别,并且几乎在每个新的主要 Android 版本中都得到了改进。这个菜谱将演示如何使用 Google 语音服务将语音识别添加到你的应用程序中。
准备工作
在 Android Studio 中创建一个新的项目,并将其命名为 SpeechRecognition。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何做...
我们首先将在布局中添加一个“现在说话”(或麦克风)按钮,然后我们将添加必要的代码来调用语音识别器。打开 activity_main.xml 并按照以下步骤操作:
-
用以下 XML 替换现有的
TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" /> <ImageButton android:id="@+id/imageButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:src="img/ic_btn_speak_now" android:onClick="speakNow"/> -
定义
REQUEST_SPEECH常量:private final int REQUEST_SPEECH=1; -
将以下代码添加到现有的
onCreate()回调中:PackageManager pm = getPackageManager(); List<ResolveInfo> activities = pm.queryIntentActivities( new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); if (activities.size() == 0) { findViewById(R.id.imageButton).setEnabled(false); Toast.makeText(this, "Speech Recognition Not Supported", Toast.LENGTH_LONG).show(); } -
添加按钮点击方法:
public void speakNow(View view) { Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); startActivityForResult(intent, REQUEST_SPEECH); } -
将以下代码添加到重写
onActivityResult()回调:@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode==REQUEST_SPEECH && resultCode == RESULT_OK && data!=null) { ArrayList<String> result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); TextView textView = (TextView)findViewById(R.id.textView); if (result.size()>0){ textView.setText(""); for (String item : result ) { textView.append(item+"\n"); } } } } -
你已经准备好在设备或模拟器上运行应用程序了。
它是如何工作的...
这里的工作是由 Android 中的 Google 语音识别器完成的。为了确保服务在设备上可用,我们在 onCreate() 中调用 PackageManager。如果至少有一个活动注册来处理 RecognizerIntent.ACTION_RECOGNIZE_SPEECH 意图,那么我们知道它是可用的。如果没有活动可用,我们将显示一个 Toast 提示语音识别不可用,并禁用麦克风按钮。
按钮点击通过调用使用 RecognizerIntent.ACTION_RECOGNIZE_SPEECH 创建的意图来启动识别过程。
EXTRA_LANGUAGE_MODEL 参数是必需的,有两个选择:
-
LANGUAGE_MODEL_FREE_FORM -
LANGUAGE_MODEL_WEB_SEARCH
我们在 onActivityResult() 回调中获取结果。如果我们得到 RESULT_OK,那么我们应该有一个识别出的单词列表,我们可以使用 getStringArrayListExtra() 来检索它。数组列表将按识别置信度从高到低排序。
如果你想要检索置信度评分,请使用 EXTRA_CONFIDENCE_SCORES 检索浮点数组。以下是一个示例:
float[] confidence = data.getFloatArrayExtra(RecognizerIntent.EXTRA_CONFIDENCE_SCORES);
置信度评分是可选的,可能不存在。1.0 分表示最高置信度,而 0.0 分表示最低置信度。
更多内容...
使用意图是获取语音识别的一种快速简单的方法;然而,如果你不想使用默认的 Google 活动,你可以直接调用 SpeechRecognizer 类。以下是如何实例化类的示例:
SpeechRecognizer speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this);
你需要添加 RECORD_AUDIO 权限并实现 RecognitionListener 类来处理语音事件。(有关更多信息,请参阅以下链接。)
参见
-
开发者文档:RecognizerIntent 在
developer.android.com/reference/android/speech/RecognizerIntent.html -
开发者文档:SpeechRecognizer 在
developer.android.com/reference/android/speech/SpeechRecognizer.html -
开发者文档:RecognitionListener 在
developer.android.com/reference/android/speech/RecognitionListener.html
使用 GCM 的推送通知
GCM,即谷歌的推送通知版本,允许您的应用程序接收消息。其理念类似于短信,但更加灵活。GCM 有三个组成部分:
-
您的服务器(这是您启动消息的地方)
-
谷歌的 GCM 服务器
-
安卓设备(尽管 GCM 也适用于其他平台)
当用户启动您的应用程序时,您的代码需要连接到 GCM 服务器并获取设备令牌,然后将该令牌发送到您的服务器。您的服务器负责启动消息并将其传递给 GCM 服务器。您的服务器需要跟踪在启动消息时需要发送的设备令牌。(您的服务器告诉 GCM 服务器需要发送哪些设备令牌。)
您可以实施自己的服务器或选择使用众多可用的服务之一。下一章“后端服务选项”将探讨几个 BaaS 选项,其中许多也提供推送通知。(“简单测试选项”部分提供了一个验证您的代码是否正常工作的选项。)
本食谱将指导您使用当前(版本 8.3)的 Google Services 库添加 GCM 的步骤。在进入步骤之前,值得注意的是,GCM 一直支持到 API 8,只要用户有谷歌账户即可。从 Android 4.0.4 开始,不需要谷歌账户。
准备就绪
在 Android Studio 中创建一个新的项目,并将其命名为GCM。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
GCM 使用 Google Services 插件,该插件需要从谷歌开发者控制台获取的 Google Services 配置文件。要创建配置文件,您需要以下信息:
-
您的应用程序包名
-
当您拥有这些信息时,请登录到以下谷歌链接并按照向导操作以启用您的应用程序的 GCM:
developers.google.com/mobile/add
注意
如果您下载了源文件,在遵循前面的步骤时,您需要创建一个新的包名,因为现有的包名已经被注册。
如何操作...
在完成前面的“准备就绪”部分后,请按照以下步骤操作:
-
将在“准备就绪”部分下载的
google-services.json文件复制到您的应用程序文件夹中(<项目文件夹>\GCM\app`)。 -
打开项目 Gradle 构建文件:
build.gradle (Project: GCM),并将以下内容添加到buildscript dependencies部分:classpath 'com.google.gms:google-services:1.5.0-beta2' -
打开应用程序模块 Gradle 构建文件:
build.gradle (Module: app),并将以下语句添加到文件的开头(在android部分之上):apply plugin: 'com.google.gms.google-services' -
在与步骤 3 相同的模块构建文件中,将以下语句添加到依赖项部分:
compile 'com.google.android.gms:play-services-auth:8.3.0' -
打开 AndroidManifest.xml 文件并添加以下权限:
<uses-permission android:name="android.permission.WAKE_LOCK" /> <permission android:name="< packageName >.permission.C2D_MESSAGE" android:protectionLevel="signature" /> <uses-permission android:name="< packageName >.permission.C2D_MESSAGE" /> -
在
<application>元素内,添加以下<receiver>和<service>声明(这些应该与<activity>处于同一级别):<receiver android:name="com.google.android.gms.gcm.GcmReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND" > <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE" /> <category android:name="<packageName>" /> <action android:name="com.google.android.c2dm.intent.REGISTRATION" /> </intent-filter> </receiver> <service android:name=".GCMService" android:exported="false" > <intent-filter> <action android:name="com.google.android.c2dm.intent.GCM_RECEIVED_ACTION"/> <action android:name="com.google.android.c2dm.intent.RECEIVE" /> </intent-filter> </service> <service android:name=".GCMInstanceService" android:exported="false"> <intent-filter> <action android:name="com.google.android.gms.iid.InstanceID" /> </intent-filter> </service> <service android:name=".GCMRegistrationService" android:exported="false"> </service> -
创建一个名为
GCMRegistrationService的新 Java 类,它扩展了IntentService,如下所示:public class GCMRegistrationService extends IntentService { private final String SENT_TOKEN="SENT_TOKEN"; public GCMRegistrationService() { super("GCMRegistrationService"); } @Override protected void onHandleIntent(Intent intent) { super.onCreate(); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); try { InstanceID instanceID = InstanceID.getInstance(this); String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); Log.i("GCMRegistrationService", "GCM Registration Token: " + token); //sendTokenToServer(token); sharedPreferences.edit().putBoolean(SENT_TOKEN, true).apply(); } catch (Exception e) { sharedPreferences.edit().putBoolean(SENT_TOKEN, false).apply(); } } } -
创建一个名为
GCMInstanceService的新 Java 类,它扩展了InstanceIDListenerService,如下所示:public class GCMInstanceService extends InstanceIDListenerService { @Override public void onTokenRefresh() { Intent intent = new Intent(this, GCMRegistrationService.class); startService(intent); } } -
创建一个名为
GCMService的新 Java 类,它扩展了GcmListenerService,如下所示:public class GCMService extends GcmListenerService { @Override public void onMessageReceived(String from, Bundle data) { super.onMessageReceived(from, data); Log.i("GCMService", "onMessageReceived(): " + data.toString()); } } -
将以下代码添加到现有的
onCreate()回调中:Intent intent = new Intent(this, GCMRegistrationService.class); startService(intent); -
你现在可以开始在设备或模拟器上运行应用程序了。
它是如何工作的...
大部分实际的 GCM 代码都被封装在 Google API 中,简化了实现。我们只需设置项目以包含 Google 服务,并授予我们的应用程序所需的权限。
注意
重要!在步骤 5 和 6 中添加权限时,将 <packageName> 占位符替换为你的应用程序包名。
GCM 最复杂的部分可能是所需的多个服务。尽管每个服务中的代码量很少,但每个服务都有特定的任务。GCM 有两个主要方面:
-
将应用程序注册到 GCM 服务器
-
接收消息
这是向 GCM 服务器注册的代码:
String token = instanceID.getToken(getString(R.string.gcm_defaultSenderId), GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
我们不在 Activity 中调用 getToken(),因为它可能会阻塞 UI 线程。相反,我们调用 GCMRegistrationService,它在后台线程中处理调用。在收到设备令牌后,你需要将其发送到你的服务器,因为初始化消息时需要它。
接收 GCM 消息的过程由 GCMService 处理,它扩展了 GcmListenerService。由于 Google API 已经处理了大部分工作,我们只需响应 onMessageReceived() 回调。
还有更多...
为了便于输入,我们省略了一个重要的 Google 服务 API 验证,该验证应包含在任何生产应用程序中。我们不是直接调用 GCMRegistrationService,就像在前一节的 onCreate() 中所做的那样,而是首先检查 Google API 服务是否可用。以下是一个展示如何调用 isGooglePlayServicesAvailable() 方法的示例:
private boolean isGooglePlayServicesAvailable() {
GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance();
int resultCode = googleApiAvailability.isGooglePlayServicesAvailable(this);
if (resultCode != ConnectionResult.SUCCESS) {
if (googleApiAvailability.isUserResolvableError(resultCode)) {
googleApiAvailability.getErrorDialog(this, resultCode, PLAY_SERVICES_RESOLUTION_REQUEST).show();
} else {
Toast.makeText(MainActivity.this, "Unsupported Device", Toast.LENGTH_SHORT).show();
finish();
}
return false;
}
return true;
}
然后,将 onCreate() 代码更改为首先调用此方法:
if (isGooglePlayServicesAvailable()) {
Intent intent = new Intent(this, GCMRegistrationService.class);
startService(intent);
}
简单测试选项
为了帮助验证您的代码是否正确工作,已创建并发布了一个测试应用程序到 Google Play。此应用程序将在物理设备和模拟器上运行。Google Play 列表中还包括一个下载源代码并直接运行项目的链接,这使得输入所需字段变得更加容易。
小贴士
GCM (Push Notification) Tester: 请参考以下链接获取更多信息:
play.google.com/store/apps/details?id=com.eboyer.gcmtester
参见
-
参考谷歌云消息传递网页
developers.google.com/android/reference/com/google/android/gms/gcm/GoogleCloudMessaging -
参考关于 GCM 连接服务器网页
developers.google.com/cloud-messaging/server
如何将 Google 登录添加到您的应用
Google 登录允许您的用户使用他们的 Google 凭据登录您的应用程序。本指南将指导您将 Google 登录添加到您的应用程序中。以下是应用程序中 Google 登录按钮的截图,我们将按照本指南创建它:

准备就绪
在 Android Studio 中创建一个新的项目,并将其命名为 GoogleSignIn。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
Google 登录使用 Google 服务插件,这需要一个 Google 服务配置文件,该文件可以从 Google 开发者控制台获取。要创建配置文件,您需要以下信息:
-
您的应用程序包名
-
您签名证书的 SHA-1 哈希码(有关更多信息,请参阅本指南末尾的 验证您的客户端 链接)
当您有了信息后,登录此 Google 链接并按照向导启用登录:
developers.google.com/mobile/add
注意
如果您正在下载源文件,在遵循前面的步骤时需要创建一个新的包名,因为现有的包名已经被注册。
如何操作...
在完成前面的 准备就绪 部分后,按照以下步骤操作:
-
将在 准备就绪 部分下载的
google-services.json文件复制到您的应用文件夹中 (<project folder>\GoogleSignIn\app) -
打开项目 Gradle 构建文件:
build.gradle (Project: GoogleSignIn),并在buildscript dependencies部分添加以下内容:classpath 'com.google.gms:google-services:1.5.0-beta2' -
打开应用模块 Gradle 构建文件:
build.gradle (Module: app),并在文件开头(在android部分之上)添加以下语句:apply plugin: 'com.google.gms.google-services' -
在与第 3 步相同的模块构建文件中,在依赖项部分添加以下语句:
compile 'com.google.android.gms:play-services-auth:8.3.0' -
打开
activity_main.xml并用以下 XML 替换现有的TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" /> <com.google.android.gms.common.SignInButton android:id="@+id/signInButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> -
打开
MainActivity.java并添加以下全局声明:private final int REQUEST_SIGN_IN=1; GoogleApiClient mGoogleApiClient; -
添加以下
OnConnectionFailedListener:GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() { @Override public void onConnectionFailed(ConnectionResult connectionResult) { Toast.makeText(MainActivity.this, "connectionResult="+connectionResult.getErrorMessage(), Toast.LENGTH_SHORT).show(); } }; -
将以下代码添加到现有的
onCreate()中:GoogleSignInOptions googleSignInOptions = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestEmail() .build(); mGoogleApiClient = new GoogleApiClient.Builder(this) .addOnConnectionFailedListener(mOnConnectionFailedListener) .addConnectionCallbacks(mConnectionCallbacks) .addApi(Auth.GOOGLE_SIGN_IN_API, googleSignInOptions) .build(); findViewById(R.id.signInButton).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { signIn(); } }); -
如下创建
onActivityResult()回调的覆盖:@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_SIGN_IN) { GoogleSignInResult googleSignInResult = Auth.GoogleSignInApi.getSignInResultFromIntent(data); if (googleSignInResult.isSuccess()) { GoogleSignInAccount googleSignInAccount = googleSignInResult.getSignInAccount(); TextView textView = (TextView)findViewById(R.id.textView); textView.setText("Signed in: " + googleSignInAccount.getDisplayName()); findViewById(R.id.signInButton).setVisibility(View.GONE); } } } -
你现在可以运行应用程序在设备或模拟器上了。
它是如何工作的...
Google 通过他们的GoogleApiClient和GoogleSignInOptions API 使添加 Google 登录变得相对简单。首先,我们使用构建器创建一个GoogleSignInOptions对象。这是我们指定我们想要的登录选项的地方,例如请求电子邮件 ID。然后,我们将其传递给GoogleApiClient构建器。
当用户点击 Google 登录按钮(使用com.google.android.gms.common.SignInButton类创建)时,我们向处理程序发送一个GoogleSignInApi的 Intent。我们在onActivityResult()中处理结果。如果登录成功,我们可以获取账户详情。在我们的例子中,我们只获取电子邮件,但还有其他附加信息,如下所示:
-
getDisplayName(): 这是显示名称 -
getEmail(``): 电子邮件地址 -
getId(): Google 账户的唯一 ID -
getPhotoUrl(): 显示照片 -
getIdToken(): 这用于后端认证
在“另请参阅”部分查看GoogleSignInAccount链接,以获取完整列表。
更多内容...
如果你想让你的应用程序面向更广泛的受众,你需要考虑本地化。
本地化资源
Google 在 SDK 中提供了许多本地化字符串,位于此链接:<SDK 安装文件夹>/sdk/extras/google/google_play_services/libproject/google-play-services_lib/res/。
另请参阅
-
参考关于在
developers.google.com/android/guides/client-auth认证客户端的网页 -
访问
developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInAccount
第十五章. 后端即服务选项
在本章中,我们将涵盖以下主题:
-
App42
-
Backendless
-
Buddy
-
Firebase
-
Kinvey
简介
随着您的应用程序和用户基础的扩大,您可能会希望将您的应用程序连接到跨设备甚至跨用户,例如高分排行榜。您有两个选择:
-
创建和维护自己的服务器
-
使用后端即服务(BaaS)提供商
作为移动开发者,创建和维护一个网络服务器是一个耗时的工作,可能会使您偏离开发工作。
备注
如果您对 BaaS 提供商不熟悉,以下是一些背景信息:
维基百科 – 移动后端即服务:
我们将探讨几个针对 Android 开发者特定功能的 BaaS 提供商。仅包括提供原生 Android 支持和免费订阅的提供商。(仅提供免费试用或仅付费计划的提供商不包括在内。)随着您的应用程序超出免费层,所有这些提供商都提供不同月费的高级服务。
以下表格提供了每个提供商提供的每月免费服务的快速比较:
| 提供商 | 每月用户数 | API 调用 | 推送通知 | 文件存储 |
|---|---|---|---|---|
| Firebase | 无限制 | 100 SC | N/A | 1 GB |
| Buddy | * | 20/sec | 5 Million | 10 GB |
| App42 | * | 1 Million / month | 1 Million | 1 GB |
| Kinvey | 1000 | * | * | 30 GB |
| Backendless | 100 | 50/sec | 1 Million | 20 GB |
- = 未在其网站上发布
N/A = 功能不可用
SC = 同时连接数
备注
免责声明:上述表格和随后的食谱信息来源于它们的公共网站,并受其自行决定而变更。正如您所知,移动行业一直在不断变化;预计价格和服务将发生变化。请仅将此信息作为起点使用。
最后,这并不是 BaaS 提供商的详尽列表。希望本章能为您介绍 BaaS 能做什么以及如何为您的应用程序使用 BaaS 提供良好的入门。接下来的食谱将查看每个提供商,并指导您将它们的库添加到项目中。这将为您提供服务的直接比较。您将看到,一些服务比其他服务更容易使用,这可能是决定性因素。
App42
App42 是 ShepHertz 的 BaaS API 产品,ShepHertz 是一家提供多种服务的云服务提供商,包括游戏平台、平台即服务和营销分析。它们拥有非常丰富的功能集,包括许多特别适用于游戏的服务。
App42 Android SDK 支持以下功能:
-
用户服务
-
存储服务
-
定制代码服务
-
推送通知服务
-
事件服务
-
礼品管理服务
-
定时服务
-
社交服务
-
A/B 测试服务
-
Buddy 服务
-
头像服务
-
成就服务
-
排行榜服务
-
奖励服务
-
上传服务
-
相册服务
-
地理服务
-
会话服务
-
评论服务
-
购物车服务
-
目录服务
-
消息服务
-
推荐服务
-
邮件服务
-
日志服务
注意
要注册 App42/ShepHertz,请访问以下链接:
这是 App4 注册界面的截图:

准备工作
在 Android Studio 中创建一个新的项目,并将其命名为App42。使用默认的手机和平板选项,并在提示活动类型时选择空活动。
从以下链接下载并解压 App42 SDK:example.com
github.com/shephertz/App42_ANDROID_SDK/archive/master.zip
在创建您的 App42 账户(见前述链接)后,登录到 AppHQ 管理控制台,并注册您的应用程序。您将需要 ApiKey 和 SecretKey。
如何操作...
要将 App42 支持添加到您的项目中,首先打开 AndroidManifest,并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -
在您的文件浏览器中打开以下文件夹:
<项目文件夹>\App42\app\libs(如果libs文件夹不存在,请创建它),并将App42_ANDROID-CAMPAIGN_x.x.jar文件复制到app\libs文件夹中。 -
打开应用程序模块的 Gradle 构建文件:
build.gradle (Module: app),并在dependencies部分添加以下内容:compile files('libs/App42_ANDROID-CAMPAIGN_x.x.jar') -
打开
ActivityMain.java并添加以下导入:import com.shephertz.app42.paas.sdk.android.App42API; -
将以下代码添加到
onCreate()回调中:App42API.initialize(this, "YOUR_API_KEY", "YOUR_SECRET_KEY"); -
您现在可以在设备或模拟器上运行应用程序了。
它是如何工作的...
很遗憾,App42 不支持 Gradle 构建格式,因此您需要手动下载 JAR 文件并将其复制到\libs文件夹中。
在第 3 步中,将App42_ANDROID-CAMPAIGN_x.x.jar中的x.x替换为您下载的文件中的当前版本号。
在第 5 步中,将YOUR_API_KEY和YOUR_SECRET_KEY替换为您在 App42 注册应用程序时收到的凭据。
更多内容...
以下是一个使用 App42 API 注册用户的示例:
UserService userService = App42API.buildUserService();
userService.createUser("userName", "password", "email", new App42CallBack() {
public void onSuccess(Object response) {
User user = (User)response;
Log.i("UserService","userName is " + user.getUserName());
Log.i("UserService", "emailId is " + user.getEmail());
}
public void onException(Exception ex) {
System.out.println("Exception Message"+ex.getMessage());
}
});
参见
- 如需更多信息,请参阅 App42 网页
api.shephertz.com/
Backendless
除了MBaaS(移动后端作为服务,正如他们所称呼的),Backendless 还提供其他一些服务,如托管、API 服务和市场。他们的 MBaaS 功能包括:
-
用户管理
-
数据持久化
-
地理定位
-
媒体流
-
发布/订阅消息
-
推送通知
-
自定义业务逻辑
-
分析
-
移动代码生成
注意
要注册 Backendless,请遵循此链接:
develop.backendless.com/#registration
这是 Backendless 注册窗口的截图:

准备工作
在 Android Studio 中创建一个新的项目,命名为 Backendless。使用默认的 手机和平板 选项,并在提示 活动类型 时选择 空活动。
您需要一个 Backendless 账户(见前述链接),并且必须通过他们的 Backendless 控制台注册您的应用程序。一旦您获得了 App ID 和 Secret Key,开始以下步骤。
如何操作...
要将 Backendless 添加到您的项目,打开 AndroidManifest.xml 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -
打开应用模块的 Gradle 构建文件:
build.gradle (Module: app)并在dependencies部分添加以下内容:compile 'com.backendless:android:3.0.3' -
打开
ActivityMain.java并添加以下导入:import com.backendless.Backendless; -
将以下代码添加到
onCreate()回调中:String appVersion = "v1"; Backendless.initApp(this, YOUR_APP_ID, YOUR_SECRET_KEY, appVersion); -
您现在可以开始在设备或模拟器上运行应用程序了。
工作原理...
在第 4 步中将 YOUR_APP_ID 和 YOUR_SECRET_KEY 替换为您从 Backendless 控制台收到的凭证。
如果您更喜欢直接下载 SDK 而不是使用 Maven 依赖项,它在这里可用:backendless.com/sdk/java/3.0.0/backendless-sdk-android.zip。
更多内容...
以下是一个使用 BackendlessUser 对象注册用户的示例:
BackendlessUser user = new BackendlessUser();
user.setEmail("<user@email>");
user.setPassword("<password>");
Backendless.UserService.register(user, new BackendlessCallback<BackendlessUser>() {
@Override
public void handleResponse(BackendlessUser backendlessUser) {
Log.d("Registration", backendlessUser.getEmail() + " successfully registered");
}
} );
参见
- 更多信息,请参阅 Backendless 网页
backendless.com/
Buddy
与列表中的其他 BaaS 提供商相比,Buddy 有所不同,因为它们主要专注于连接设备和传感器。为了帮助维护隐私法规,Buddy 允许您选择将数据托管在美国或欧盟。
Buddy 支持常见的场景,如:
-
记录指标事件
-
发送推送通知
-
接收和安全存储遥测数据
-
存储和管理二进制文件
-
深度移动分析,了解客户如何使用应用程序
-
将设备或应用程序数据与您的公司 BI 系统集成
-
在您选择的地理位置的沙盒中,存储私有数据。
如果您想审查或贡献 Buddy SDK,源代码可以通过以下 Git 命令获取:
git clone https://github.com/BuddyPlatform/Buddy-Android-SDK.git
注意
要注册 Buddy,请点击此链接:
这是 Buddy 注册的截图:

准备工作
在 Android Studio 中创建一个新的项目,命名为 Buddy。使用默认的 手机和平板 选项,并在提示 活动类型 时选择 空活动。
您需要一个 Buddy 账户(见前述链接),并且必须通过他们的仪表板注册您的应用程序。一旦您获得了 App ID 和 App Key,开始以下步骤。
如何操作...
要将 Buddy 添加到您的项目,打开 AndroidManifest.xml 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -
打开应用模块的 Gradle 构建文件:
build.gradle (Module: app)并在dependencies部分添加以下内容:compile 'com.buddy:androidsdk:+' -
打开
ActivityMain.java并添加以下导入:import com.buddy.sdk.Buddy; -
在
onCreate()回调中添加以下代码:Buddy.init(myContext, "appId", "appKey"); -
您已准备好在设备或模拟器上运行应用程序。
工作原理...
在第 4 步中,将 appId 和 appKey 替换为从 Buddy 控制台收到的凭证。
与大多数其他 BaaS 提供商类似,我们只需在我们的 Gradle 构建中添加对 Maven 仓库的引用。然后,我们添加导入并开始调用 Buddy API。
更多内容...
以下是一个使用 Buddy 注册用户的示例:
Buddy.createUser("someUser", "somePassword", null, null, null, null, null, null, new BuddyCallback<User>(User.class) {
@Override
public void completed(BuddyResult<User> result) {
if (result.getIsSuccess()) {
Log.w(APP_LOG, "User created: " + result.getResult().userName);
}
}
});
参见
- 更多信息,请参考 Buddy 网页:
buddy.com/
Firebase
Firebase 是一个主要专注于数据库功能的 BaaS 提供商。虽然它们不像其他大多数 BaaS 提供商那样功能全面,但它们在数据库方面做得很好。它们是本列表中唯一提供自动同步数据库功能的提供商。
Firebase 服务包括:
-
Firebase 实时数据库
-
Firebase 身份验证
-
Firebase 主机
-
用户身份验证—电子邮件和密码、Facebook、Twitter、GitHub 和 Google
由于它们最近被 Google 收购,您可以期待与 Google Cloud 解决方案的进一步集成,正如您可以从此链接中看到:
cloud.google.com/solutions/mobile/firebase-app-engine-android-studio
注意
要使用 Firebase 注册,请访问此链接:
这是 Firebase 注册窗口的截图:

准备就绪
在 Android Studio 中创建一个新的项目,并将其命名为 Firebase。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
您将需要当您在 Firebase 上注册应用程序时提供的 Firebase URL。
如何操作...
要将 Firebase 添加到您的项目中,首先打开 AndroidManifest.xml 文件,并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> -
打开应用程序模块的 Gradle 构建文件:
build.gradle (Module: app)并在dependencies部分添加以下内容:compile 'com.firebase:firebase-client-android:2.5.0+' -
打开
ActivityMain.java并添加以下导入:import com.firebase.client.Firebase; -
在
onCreate()回调中添加以下代码:Firebase.setAndroidContext(this); Firebase firebase = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/"); -
您已准备好在设备或模拟器上运行应用程序。
工作原理...
将 Firebase 支持添加到您的应用程序相当直接。将 <YOUR-FIREBASE-APP> 占位符替换为 Firebase 在您注册应用程序时提供的链接。
更多内容...
以下是一个使用 Firebase 注册用户的示例:
firebase.createUser("bobtony@firebase.com", "correcthorsebatterystaple", new Firebase.ValueResultHandler<Map<String, Object>>() {
@Override
public void onSuccess(Map<String, Object> result) {
Log.i("Firebase", "Successfully created user account with uid: " + result.get("uid"));
}
@Override
public void onError(FirebaseError firebaseError) {
// there was an error
}
});
参见
- 更多信息,请参考 Firebase 网页
www.firebase.com/
Kinvey
Kinvey 是最早开始提供移动后端服务的提供商之一。它们的功能包括:
-
用户管理
-
数据存储
-
文件存储
-
推送通知
-
社交网络集成
-
位置服务
-
生命周期管理
-
版本控制
注意
在
console.kinvey.com/sign-up上注册 Kinvey。
这是 Kinvey 注册窗口的截图:

准备就绪
在 Android Studio 中创建一个新的项目,并将其命名为 Kinvey。使用默认的 手机和平板 选项,并在提示 活动类型 时选择 空活动。
从以下链接下载并解压 Kinvey SDK:download.kinvey.com/Android/kinvey-android-2.10.5.zip
您需要一个 Kinvey 账户(见前面的链接),并且必须通过他们的开发者控制台注册您的应用程序。一旦您获得了 App Key 和 App Secret,开始以下步骤。
如何做到这一点...
要将 Kinvey 添加到您的项目中,请按照以下步骤操作:
-
将以下权限添加到 AndroidManifest.xml 文件中:
<uses-permission android:name="android.permission.INTERNET"/> -
在您的文件浏览器中打开以下文件夹:
<项目文件夹>\Kinvey\app\libs(如果不存在libs文件夹,请创建它)并将 SDK 的lib和libJar文件夹中的所有文件复制到app\libs文件夹中。 -
打开应用模块的 Gradle 构建文件:
build.gradle (Module: app)并添加以下repositories和dependencies(保留任何现有的条目):repositories { flatDir { dirs 'libs' } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile(name:'kinvey-android-*', ext:'aar') } -
打开
MainActivity.java并添加以下导入:import com.kinvey.android.Client; -
将以下内容添加到类声明中:
final Client mKinveyClient = new mKinveyClient("your_app_key", "your_app_secret", this.getApplicationContext()).build(); -
您现在可以开始在设备或模拟器上运行应用程序了。
它是如何工作的...
由于 Kinvey 不提供简单的 Gradle 依赖项,因此它不是最容易设置的 BaaS 之一。相反,您需要像第 2 步中那样直接将它们的库添加到项目库中。
这些步骤将设置好 Kinvey 客户端,使其准备好开始添加您应用程序的附加功能。只需确保将 Kinvey 客户端构建器中的占位符替换为您的应用程序凭据。
还有更多...
为了验证您的设置是否正确工作,请在 onCreate() 方法或按钮点击时调用以下代码:
mKinveyClient.ping(new KinveyPingCallback() {
public void onFailure(Throwable t) {
Log.d("KinveyPingCallback", "Kinvey Ping Failed", t);
}
public void onSuccess(Boolean b) {
Log.d("KinveyPingCallback", "Kinvey Ping Success");
}
});
参见
- 更多信息,请参阅 Kinvey 网页
www.kinvey.com/






















浙公网安备 33010602011771号