安卓应用开发秘籍第二版-全-

安卓应用开发秘籍第二版(全)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

Android 最初在 2007 年被谷歌公司收购后发布。起初,Android 主要应用于手机。Android 3.0 增加了利用不断增长的平板市场的功能。

2014 年,谷歌宣布 Android 活跃用户超过 10 亿!在谷歌应用商店有超过 100 万的应用程序,现在正是加入 Android 社区的最佳时机!

随着我们在 2016 年的开始,我们迎来了最近发布的 Android 6.0,为用户和开发者带来了激动人心的新功能。

本书涵盖内容

第一章,活动,讨论了活动,这是大多数应用程序的基本构建块。查看最常见的任务示例,例如创建活动以及从一个活动传递控制到另一个活动。

第二章,布局,讨论了布局选项;虽然活动是 UI 的基础,但布局实际上定义了用户在屏幕上看到的内容。了解可用的主要布局选项和最佳实践。

第三章,视图、小部件和样式,探索了所有布局构建的基本 UI 对象。小部件包括从按钮和文本框到更复杂的 NumberPicker 和日历对话框等一切。

第四章,菜单,教你如何在 Android 中使用菜单。学习如何创建菜单以及如何在运行时控制它们的行为。

第五章,探索片段、应用小部件和系统 UI,展示了如何通过重用 UI 组件的片段创建更灵活的用户界面。利用新的操作系统功能,如半透明系统栏,甚至使用沉浸模式完全隐藏系统 UI。

第六章,处理数据,帮助你发现 Android 提供的多种持久化数据的方法,并了解何时使用每种选项最佳。Loader 类示例展示了一种无需绑定 UI 线程的高效数据呈现解决方案。

第七章,警报和通知,展示了向用户显示通知的多种选项。选项包括应用内的警报、使用系统通知和浮动通知

第八章,使用触摸屏和传感器,帮助你学习处理标准用户交互的事件,例如按钮点击、长按和手势。访问设备硬件传感器以确定方向变化、设备移动和指南针方位。

第九章,图形和动画,帮助你通过动画让你的应用程序生动起来!利用 Android 提供的多种创建动画的选项,从简单的位图到自定义属性动画。

第十章,初识 OpenGL ES,讨论了 OpenGL;当需要高性能的 2D 和 3D 图形时,可以转向开放图形库。Android 支持跨平台的 OpenGL 图形 API。

第十一章,多媒体,利用硬件特性播放音频。使用 Android 意图调用默认的相机应用程序,或者深入研究相机 API 以直接控制相机。

第十二章,电信、网络和互联网,介绍了如何使用电信功能发起电话通话和监听来电事件。了解如何发送和接收短信(文本)消息。在你的应用程序中使用 WebView 显示网页,并学习如何使用 Volley 直接与网络服务通信。

第十三章,获取位置和使用地理围栏,指导你如何确定用户的位置,以及最佳实践以防止你的应用程序耗尽电池。使用新的位置 API 接收位置更新并创建地理围栏。

第十四章,为 Play 商店准备你的应用,帮助你为 Play 商店润色你的应用程序,并了解如何实现更高级的功能,如闹钟和 AsyncTask 进行后台处理。了解如何将 Google 云消息(推送通知)添加到你的应用程序,并利用 Google 登录。

第十五章,后端即服务选项,探讨了后端即服务提供商可以为你的应用程序提供什么。比较几个提供原生 Android 支持和免费订阅选项的顶级提供商。

你需要这本书的内容

开发 Android 应用程序需要 Android SDK,它支持多个平台,包括 Windows、Mac 和 Linux。

尽管不是必须的,但本书使用了 Android Studio,这是官方的 Android IDE。如果你是 Android 开发新手,请访问以下链接查看当前的系统要求,并下载适用于你平台的带有 SDK 捆绑包的 Android Studio:

developer.android.com/sdk/index.html

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注册,我们会直接将文件通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的支持标签上。

  3. 点击代码下载与勘误

  4. 搜索框中输入书名。

  5. 选择您要下载代码文件的书。

  6. 从下拉菜单中选择您购买此书的地点。

  7. 点击代码下载

文件下载后,请确保您使用最新版本的以下软件解压或提取文件夹:

  • Windows 下的 WinRAR / 7-Zip

  • Mac 下的 Zipeg / iZip / UnRarX

  • Linux 下的 7-Zip / PeaZip

勘误

尽管我们已经尽力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——我们非常感激您能向我们报告。这样做,您可以避免其他读者感到沮丧,并帮助我们改进该书的后续版本。如果您发现任何勘误,请访问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 提供了一个快速入门向导,使得这个过程非常简单。按照以下步骤开始:

  1. 启动 Android Studio,会出现欢迎使用 Android Studio对话框。

  2. 点击开始一个新的 Android Studio 项目选项。

  3. 输入应用程序名称;对于此示例,我们使用了DeclareAnActivity。点击下一步

  4. 将活动添加到移动设备对话框中,点击空白活动按钮,然后点击下一步

  5. 目标 Android 设备对话框中,选择Android 6.0(API 23)作为最低 SDK(对于此示例,你选择哪个 API 级别其实并不重要,因为自从 API 级别 1 以来就已经存在活动,但选择最新版本被认为是最佳实践)。点击下一步

  6. 由于我们之前选择了空白活动选项,所以会显示自定义活动对话框。你可以保留提供的默认设置,但注意默认的活动名称是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>元素添加到Android Manifest中,我们是在表明意图将此组件包含在应用程序内。任何未在清单中声明的活动(或其他任何组件)都不会被包含在应用程序中。尝试访问或使用未声明的组件将在运行时抛出异常。

在前面的代码中,还有一个属性—android:label。此属性表示屏幕上显示的标题以及如果这是启动器活动的话图标标签。

注意

要查看可用应用程序属性的全部列表,请查看以下资源:

developer.android.com/guide/topics/manifest/activity-element.html

使用意图对象启动新活动

Android 应用程序模型可以看作是一种面向服务的模型,活动作为组件,意图作为它们之间传递的消息。这里,一个意图用于启动显示用户通话记录的活动,但意图可以用作很多事情,我们将在本书中多次遇到它们。

准备就绪

为了简化事情,我们将使用一个意图对象来启动 Android 的一个内置应用程序,而不是创建一个新的应用程序。这只需要一个非常基础的应用程序,因此用 Android Studio 启动一个新的 Android 项目,并将其命名为ActivityStarter

如何操作...

为了让示例简单,以便我们专注于手头的任务,我们将创建一个函数来展示一个意图操作,并从活动中的按钮调用这个函数。

在 Android Studio 中创建新项目后,请按照以下步骤操作:

  1. 打开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 会对 View 和意图给出这个警告:无法解析符号'Intent'

    这意味着你需要将库引用添加到项目中。你可以通过在import部分手动输入以下代码来实现这一点:

    import android.view.View;
    
    import android.content.Intent;
    

    或者,只需点击红色字体的文字,按Alt + Enter,让 Android Studio 为你添加库引用。

  2. 打开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"/>
    

    如何操作...

  3. 现在是运行应用程序并查看意图操作的时候了。你需要创建一个 Android 模拟器(在 Android Studio 中,转到工具 | Android | AVD 管理器)或者将一个物理设备连接到你的电脑。

  4. 当你按下启动浏览器按钮时,你会看到默认的网页浏览器打开,并显示指定的 URL。

它是如何工作的...

尽管这个应用很简单,但它展示了 Android 操作系统背后的许多强大功能。意图对象只是一个消息对象。意图可以用于在应用程序的组件之间(如服务和广播接收器)以及与其他设备上的应用程序进行通信(正如本例中所做的那样)。

注意

要在物理设备上测试,你可能需要为你的设备安装驱动程序(驱动程序针对硬件制造商是特定的)。你还需要在设备上启用开发者模式。启用开发者模式根据 Android 操作系统版本而有所不同。如果你在设备设置中看不到开发者模式选项,打开关于手机选项,并开始点击构建号。点击三次后,你应该会看到一个Toast消息,告诉你正在成为开发者的路上。再点击四次将启用该选项。

在本例中,我们通过指定ACTION_VIEW作为我们想要执行的操作(我们的意图)来创建一个意图对象。你可能已经注意到,当你输入Intent然后输入句点时,Android Studio 提供了一个弹出式可能性的列表(这是自动完成功能),如下所示:

它是如何工作的...

ACTION_VIEW与数据中的 URL 一起,表示意图是查看网站,因此会启动默认浏览器(不同的数据可能会启动不同的应用)。在这个例子中,我们的意图只是查看 URL,所以我们仅使用startActivity()方法调用意图。根据我们的需求,还有其他调用意图的方法。在从活动中返回结果的食谱中,我们将使用startActivityForResult()方法。

还有更多内容...

对于 Android 用户来说,下载他们喜欢的网页浏览、拍照、发短信等应用是非常常见的。使用意图,您可以允许您的应用利用用户喜欢的应用,而不是试图重新发明所有这些功能。

另请参阅

若要从菜单选择启动一个活动,请参考第四章中的处理菜单选择部分,菜单

在活动之间切换

通常我们会在一个活动内部激活另一个活动。尽管这不是一个困难的任务,但它需要比之前的食谱更多的设置,因为它需要两个活动。我们将创建两个活动类,并在清单中声明它们。我们还将创建一个按钮,就像在之前的食谱中所做的那样,以切换到活动。

准备工作

我们将在 Android Studio 中创建一个新项目,就像在之前的食谱中所做的那样,并将这个项目命名为ActivitySwitcher。Android Studio 将创建第一个活动ActivityMain,并在清单中自动声明。

如何操作...

  1. 由于 Android Studio 新项目向导已经创建了第一个活动,我们只需要创建第二个活动。打开ActivitySwitcher项目,并按照此截图所示导航至文件 | 新建 | 活动 | 空白活动如何操作...

  2. 自定义活动对话框中,您可以保留默认的活动名称,即Main2Activity,或者像这里显示的那样更改为SecondActivity如何操作...

  3. 打开MainActivity.java文件,并添加以下函数:

    public void onClickSwitchActivity(View view) {
        Intent intent = new Intent(this, SecondActivity.class);
        startActivity(intent);
    }
    
  4. 现在,打开位于\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"/>
    
  5. 实际上,您现在可以运行代码,并看到第二个活动出现。我们将会更进一步,在SecondActivity中添加一个按钮来关闭它,这将带我们回到第一个活动。打开SecondActivity.java文件,并添加此函数:

    public void onClickClose(View view) {
        finish();
    }
    
  6. 最后,在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"/>
    
  7. 在您的设备或模拟器上运行应用程序,并查看按钮的实际效果。

工作原理...

本练习的真正工作在于第 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> 元素。主活动通常是在启动应用程序时的入口点。这就是为什么定义了 MAINLAUNCHER ——以便系统知道在应用程序启动时应该启动哪个活动。

另请参阅

  • 要了解有关嵌入如 Button 之类的控件的更多信息,请访问 第三章,视图、控件和样式

将数据传递给另一个活动

意图对象被定义为消息对象。作为消息对象,其目的是与应用程序的其他组件进行通信。在这个食谱中,我们将向您展示如何使用意图传递信息以及如何再次获取它。

准备工作

这个食谱将从上一个食谱结束的地方开始。我们将这个项目称为 SendData

如何操作...

由于此食谱基于上一个食谱,因此大部分工作已经完成。我们将在主活动中添加一个 EditText 元素,以便我们有一些内容发送到 SecondActivity。我们将使用(自动生成的)TextView 视图来显示消息。以下是完整的步骤:

  1. 打开 activity_main.xml,移除现有的 <TextView> 元素,并添加以下 <EditText> 元素:

    <EditText
        android:id="@+id/editTextData"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    

    我们在上一个食谱中创建的 <Button> 元素没有变化。

  2. 现在,打开 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);
    }
    
  3. 接下来,打开 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"/>
    
  4. 最后一个改变是编辑第二个活动以查找这个新的数据并在屏幕上显示它。打开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));
        }
    }
    
  5. 现在运行项目。在主活动中输入一些文本,然后按下启动第二个活动以查看它发送的数据。

它是如何工作的...

如预期的那样,意图对象正在完成所有工作。我们像在之前的食谱中一样创建了一个意图,然后添加了一些额外的数据。你注意到putExtra()方法调用了吗?在我们的例子中,我们使用了已经定义的Intent.EXTRA_TEXT作为标识符,但我们并不一定要这么做。我们可以使用我们想要的任何键(如果你熟悉名称/值对,你之前应该已经见过这个概念)。

使用名称/值对的关键点在于,你必须使用相同的名称来获取数据。这就是为什么我们在使用getStringExtra()读取额外数据时使用相同的键标识符。

第二个活动是用我们创建的意图启动的,所以只需获取意图并检查随它发送的数据。我们在onCreate()中进行这项操作:

textView.setText(getIntent().getStringExtra(Intent.EXTRA_TEXT));

还有更多...

我们不仅限于发送String数据。意图对象非常灵活,并且已经支持基本数据类型。回到 Android Studio,点击putExtra方法。然后按下Ctrl空格键。Android Studio 将会弹出自动完成列表,这样你就可以看到你可以存储的不同数据类型了。

从活动中返回结果

能够从一个活动启动另一个活动是很好的,但我们经常需要知道被调用的活动在任务中的表现,甚至需要知道哪个活动被调用了。startActivityForResult()方法提供了这个解决方案。

准备工作

从活动中返回结果与我们在之前的食谱中调用活动的方式并没有太大不同。你可以使用之前食谱中的项目,或者开始一个新项目并将其命名为GettingResults。无论如何,一旦你有一个带有两个活动以及调用第二个活动所需代码的项目,你就可以开始了。

如何操作...

获取结果只需要进行少量更改:

  1. 首先,打开MainActivity.java并将以下常量添加到类中:

    public static final String REQUEST_RESULT="REQUEST_RESULT";
    
  2. 接下来,通过修改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);
        startActivityForResult(intent,1);
    }
    
  3. 然后,添加这个新方法以接收结果:

    @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();
        }
    }
    
  4. 最后,在SecondActivity.java中修改onClickClose以设置返回值,如下所示:

    public void onClickClose(View view) {
        Intent returnIntent = new Intent();
        returnIntent.putExtra(MainActivity.REQUEST_RESULT,42);
        setResult(RESULT_OK, returnIntent);
        finish();
    }
    

它是如何工作的...

如你所见,获取结果回来相对简单。我们只需使用startActivityForResult调用意图,这样它就知道我们想要一个结果。我们设置onActivityResult()回调处理程序以接收结果。最后,我们确保在关闭活动之前,第二个活动使用setResult()返回一个结果。在这个例子中,我们只是用静态值设置一个结果。我们仅显示我们收到的内容以演示这个概念。

检查结果码以确保用户没有取消操作是一个好习惯。它从技术上来说是一个整数,但系统将其作为布尔值使用。检查RESULT_OKRESULT_CANCEL并根据情况进行处理。在我们的示例中,第二个活动没有取消按钮,那么为什么要检查呢?如果用户点击了返回按钮怎么办?系统会将结果码设置为RESULT_CANCEL,并将意图设置为 null,这将导致我们的代码抛出异常。

我们使用了Toast对象,这是一种便捷的弹出式消息,可以用来不打扰地通知用户。它还作为一种方便的调试方法,因为它不需要特殊的布局或屏幕空间。

还有更多...

除了结果码,onActivityResults()还包括一个请求码。你可能想知道这是从哪里来的?它只是与startActivityForResult()调用一起传递的整数值,形式如下:

startActivityForResult(Intent intent, int requestCode);

我们没有检查请求码,因为我们知道只有一个结果需要处理——但在有多个活动的小型应用程序中,这个值可以用来识别请求的来源。

提示

如果使用负请求码调用startActivityForResult(),它将表现得就像调用startActivity()一样——也就是说,它不会返回结果。

另请参阅

  • 要了解有关创建新的活动类的更多信息,请参考在活动之间切换的食谱。

  • 想了解更多关于 Toasts 的信息,请查看第七章中的制作 Toast食谱,警报和通知

保存活动状态

移动环境非常动态,用户更换任务比在桌面上更频繁。由于移动设备通常资源较少,可以预期你的应用程序在某个时刻会被中断。系统完全关闭你的应用程序以提供更多资源给当前任务也是非常可能的。这是移动设备的天性。

用户可能会在你的应用中开始输入内容,被电话呼叫打断,或者切换到另一个应用发送短信,等到他们回到你的应用时,系统可能已经完全关闭它以释放内存。为了提供最佳用户体验,你需要预期这种行为,并让用户更容易从离开的地方继续。好消息是,Android 操作系统通过提供回调来通知你的应用程序状态变化,从而简化了这一过程。

注意

只需旋转设备,操作系统就会销毁并重新创建你的活动。这可能看起来有些过激,但这样做是有原因的——通常需要为纵向和横向提供不同的布局,这样可以确保你的应用程序使用正确的资源。

在这个教程中,你将看到如何处理 onSaveInstanceState()onRestoreInstanceState() 回调来保存应用程序的状态。我们将通过创建一个计数器变量,并在每次按下 计数 按钮时增加它来演示这一点。我们还将有一个 EditText 和一个 TextView 小部件,以观察它们默认的行为。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 StateSaver。我们只需要一个活动,所以自动生成的 MainActivity 就足够了。但是,我们需要一些小部件,包括 EditTextButtonTextView。它们的布局(在 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"/>

如何操作...

执行以下步骤:

  1. 为了跟踪计数器,我们需要在项目中添加一个全局变量,以及用于保存和恢复的键。在 MainActivity.java 类中添加以下代码:

    static final String KEY_COUNTER = "COUNTER";
    private int mCounter=0;
    
  2. 然后添加处理按钮按下的代码;它增加计数器并在 TextView 小部件中显示结果:

    public void onClickCounter(View view) {
        mCounter++;
        ((TextView)findViewById(R.id.textViewCounter)).setText("Counter: " + Integer.toString(mCounter));
    }
    
  3. 为了接收应用程序状态变化的通知,我们需要在应用程序中添加 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);
    }
    
  4. 运行程序,尝试改变方向,看看它的表现(如果你使用模拟器,Ctrl + F11 将旋转设备)。

它是如何工作的...

所有活动在其生命周期中都会经历多个状态。通过设置回调来处理事件,我们可以在活动被销毁之前让代码保存重要信息。

第 3 步是实际保存和恢复状态发生的地方。系统会向这些方法发送一个 Bundle(一个数据对象,也使用名称/值对)。我们使用 onSaveInstanceState() 回调来保存数据,并在 onRestoreInstanceState() 回调中取出。

但是等等!你在旋转设备之前尝试在 EditText 视图中输入文本了吗?如果是,你会注意到文本也被恢复了,但我们没有任何代码来处理这个视图。默认情况下,系统会自动保存状态,前提是它有一个唯一的 ID(并非所有视图都会自动保存状态,比如 TextView,但如果我们想要,可以手动保存)。

提示

请注意,如果你希望 Android 自动保存和恢复视图的状态,该视图必须有一个唯一的 ID(在布局中使用 android:id= 属性指定)。注意,并非所有视图类型都会自动保存和恢复视图的状态。

还有更多...

onRestoreInstanceState() 回调不是唯一可以恢复状态的地方。看看 onCreate() 的签名:

onCreate(Bundle savedInstanceState)

这两个方法接收同一个名为savedInstanceStateBundle实例。你可以将恢复代码移动到onCreate()方法中,效果是一样的。但需要注意的是,如果没有数据,比如在活动初次创建时,savedInstanceState包将为空。如果你想从onRestoreInstanceState()回调中移动代码,只需确保数据不是空的,如下所示:

if (savedInstanceState!=null) {
    mCounter = savedInstanceState.getInt(KEY_COUNTER);
}

另请参阅

  • 存储持久活动数据的菜谱将介绍持久存储。

  • 请查看第六章,数据处理,了解更多关于 Android 活动的例子。

  • 了解活动生命周期的菜谱解释了 Android 活动的生命周期。

存储持久活动数据

能够在临时基础上存储关于我们活动的信息非常有用,但通常我们希望应用程序能够跨多个会话记住信息。

Android 支持 SQLite,但对于简单的数据来说,这可能会带来很多开销,比如用户的名字或高分。幸运的是,Android 还提供了SharedPreferences这样的轻量级选项,适用于这些场景。

准备工作

你可以使用上一个菜谱的项目,或者启动一个新项目并称之为PersistentData(在实际应用中,你可能无论如何都会这样做)。在之前的菜谱中,我们将mCounter保存在会话状态中。在这个菜谱中,我们将添加一个新方法来处理onPause()并将mCounter保存到SharedPreferences中。我们将在onCreate()中恢复该值。

如何操作...

我们只需做两个更改,都在MainActivity.java文件中:

  1. 在活动关闭之前,添加以下onPause()方法以保存数据:

    @Override
    protected void onPause() {
        super.onPause();
    
        SharedPreferences settings = getPreferences(MODE_PRIVATE);
        SharedPreferences.Editor editor = settings.edit();
        editor.putInt(KEY_COUNTER, mCounter);
        editor.commit();
    }
    
  2. 然后在onCreate()的最后添加以下代码以恢复计数器:

    SharedPreferences settings = getPreferences(MODE_PRIVATE);
    
    int defaultCounter = 0;
    mCounter = settings.getInt(KEY_COUNTER, defaultCounter);
    
  3. 运行程序并尝试一下。

工作原理...

如你所见,这与保存状态数据非常相似,因为它也使用名称/值对。这里,我们只存储了一个int,但我们同样可以轻松地存储其他基本数据类型。每种数据类型都有相应的获取器和设置器,例如,SharedPreferences.getBoolean()SharedPreferences.setString()

保存我们的数据需要SharedPreferences.Editor的服务。这是通过edit()调用的,接受remove()clear()过程以及如putInt()的设置器。请注意,我们必须在这里用commit()语句结束任何存储操作。

还有更多内容...

getPreferences()访问器的稍微复杂一点的变体是getSharedPreferences()。它可以用来存储多个偏好设置。

使用多个偏好文件

使用getSharedPreferences()与使用其对应的方法没有区别,但它允许使用不止一个偏好文件。它的形式如下:

getSharedPreferences(String name, int mode)

在这里,name 是文件。mode 可以是 MODE_PRIVATEMODE_WORLD_READABLEMODE_WORLD_WRITABLE,描述了文件的访问级别。

另请参阅

  • 第六章,数据处理,更多关于数据存储的示例

理解活动生命周期

对于一个活动来说,Android 操作系统是一个充满危险的地方。系统对电池供电平台上的资源需求管理非常严格。当内存不足时,我们的活动可能会被从内存中清除,不会有任何预兆,同时包含的任何数据也会丢失。因此,理解活动生命周期至关重要。

下图显示了活动在其生命周期内经历的各个阶段:

理解活动生命周期

除了阶段,图表还显示了可以覆盖的方法。如您所见,在之前的食谱中我们已经利用了这些方法中的大部分。希望了解全局情况将有助于您的理解。

准备工作

在 Android Studio 中创建一个带有 空白活动 的新项目,并将其命名为 ActivityLifecycle。我们将使用(自动生成)的 TextView 方法来显示状态信息。

如何操作...

为了观察应用程序经历各个阶段的过程,我们将为所有阶段创建方法:

  1. 打开 activity_main.xml 并为自动生成的 TextView 添加一个 ID:

    android:id="@+id/textViewState"
    
  2. 剩下的步骤将在 MainActivity.java 中进行。添加以下全局声明:

    private TextView mTextViewState;
    
  3. 修改 onCreate() 方法以保存 TextView 并设置初始文本:

    mTextViewState = (TextView)findViewById(R.id.textViewState);
    mTextViewState.setText("onCreate()\n");
    
  4. 添加以下方法来处理剩余的事件:

    @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");
    }
    
  5. 运行应用程序,观察当活动被按下返回和主页键中断时会发生什么。尝试其他操作,比如任务切换,看看它们对应用程序的影响。

工作原理...

我们的活动可以处于这三种状态之一:activepausedstopped。还有一种第四状态,destroyed,但我们可以安全地忽略它:

  • 当活动的界面可供用户使用时,活动处于 active 状态。它从 onResume() 持续到 onPause(),这是当另一个活动进入前台时触发的。如果这个新活动没有完全遮盖我们的活动,那么我们的活动将保持 paused 状态,直到新活动完成或消失。然后它会立即调用 onResume() 并继续。

  • 当新启动的活动填满屏幕或使我们的活动不可见时,我们的活动将进入 stopped 状态,恢复时总会调用 onRestart()

  • 当活动处于 pausedstopped 状态时,操作系统可以在内存不足或其他应用程序需要时将其从内存中移除。

  • 值得注意的是,我们实际上从未看到 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 和适配器

  • 在运行时更改布局属性

  • 使用层次结构查看器优化布局

简介

在 Android 中,用户界面是在一个布局中定义的。布局可以在 XML 中声明,或者在代码中动态创建。(建议在 XML 中声明布局,以保持表现层与实现层的分离。)布局可以定义一个单独的ListItem、一个片段,甚至是整个 Activity。布局文件存储在 /res/layout 文件夹中,并在代码中使用以下标识符引用:R.layout.<文件名不带扩展名>

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文件。

如何操作...

  1. 编辑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"/>
    
  2. 现在,复制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"/>
    
  3. 打开MainActivity.java,并添加以下两个方法来处理按钮点击:

    public void onClickLeft(View view) {
        setContentView(R.layout.activity_main2);
    }
    
    public void onClickRight(View view) {
        setContentView(R.layout.activity_main);
    }
    
  4. 在设备或模拟器上运行此应用程序,查看效果。

工作原理...

这里的关键是调用setContentView(),我们在之前自动生成的onCreate()代码中遇到过。只需将布局 ID 传递给setContentView(),它就会自动膨胀布局。

此代码旨在让概念易于理解,但对于仅更改按钮属性(在这个例子中,我们只需在按钮点击时更改对齐方式)来说可能过于复杂。通常在onCreate()方法中只需要对布局进行一次膨胀,但有时你可能需要像我们这里一样手动膨胀一个布局。(如果你要手动处理方向变化,这将是一个很好的例子。)

还有更多...

除了像我们在这里用资源 ID 标识布局,setContentView()还可以接受一个视图作为参数,例如:

findViewById(R.id.myView)
setContentView(myView);

参阅以下内容

  • 如前所述,在第五章,探索片段、应用微件和系统 UI片段主题中,查看更改屏幕布局的替代方法。

使用 RelativeLayout

引言中所述,RelativeLayout允许视图相对于彼此和父视图定位。RelativeLayout特别有用,因为它可以减少嵌套布局的数量,这对于降低内存和处理要求非常重要。

准备工作

创建一个名为RelativeLayout的新项目。默认布局使用RelativeLayout,我们将用它来水平和垂直对齐视图。

如何操作...

  1. 打开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" />
    
  2. 运行代码,或者在设计标签中查看布局

工作原理...

这是一个非常简单的练习,但它展示了RelativeLayout的几种选项:layout_centerVerticallayout_centerHorizontallayout_belowlayout_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的示例(创建与RelativeLayoutlayout_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将为您提供另一种布局选择

  • 有关高效布局设计的更多信息,请参阅使用层次结构查看器优化布局的食谱。

使用 LinearLayout

另一个常见的布局选项是LinearLayout,它根据指定的方向,将子视图排列在单列或单行中。默认方向(如果未指定)是垂直,将视图对齐在单列中。

LinearLayout有一个RelativeLayout没有的关键特性——权重属性。在定义视图时,我们可以指定一个layout_weight参数,让视图根据可用空间动态调整大小。选项包括让视图填充所有剩余空间(如果视图具有更高的权重),让多个视图在给定空间内适应(如果所有视图权重相同),或者按权重比例分配视图空间。

我们将创建一个包含三个EditText视图的LinearLayout,以演示权重属性如何使用。在这个例子中,我们将使用三个EditText视图——一个用于输入收件人地址参数,另一个用于输入主题,第三个用于输入消息收件人主题视图各占一行,剩余空间留给消息视图。

准备工作

创建一个新项目,将其命名为LinearLayout。我们将用LinearLayout替换在activity_main.xml中创建的默认RelativeLayout

如何操作...

  1. 打开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>
    
  2. 运行代码,或者在设计标签中查看布局。

工作原理...

当使用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_gravitygravity标签不是一回事。layout_gravity决定了视图在其父视图中的位置,而gravity控制视图内内容的位置——例如,按钮上文本的对齐方式。

另请参阅

  • 之前的食谱,使用 RelativeLayout

创建表格——TableLayout 和 GridLayout

当你在 UI 中需要创建一个表格时,Android 提供了两种方便的布局选项:TableLayout(以及TableRow)和GridLayout(在 API 14 中添加)。这两种布局选项可以创建看起来相似的表格,但每个都使用不同的方法。使用TableLayout时,行和列是动态添加的,随着表格的构建而添加。使用GridLayout时,行和列的大小在布局定义中定义。

这两种布局没有绝对的好坏,只是根据你的需求选择最适合的布局。我们将使用每种布局创建一个 3x3 网格以进行比较,因为你可以轻易地在同一个应用程序中使用这两种布局。

准备好了

为了专注于布局并提供更简单的比较,我们将为这个食谱创建两个独立的应用程序。创建两个新的 Android 项目,第一个名为TableLayout,另一个名为GridLayout

如何操作...

  1. TableLayout项目开始,打开activity_main.xml。将根布局更改为TableLayout

  2. 添加三个TableRows,每个TableRow包含三组TextViews,以创建一个 3x3 矩阵。为了演示目的,列被标记为 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>
    
  3. 现在,打开GridLayout项目以编辑activity_main.xml。将根布局更改为GridLayout。在GridLayout元素中添加columnCount=3rowCount=3属性。

  4. 现在,向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>
    
  5. 您可以运行应用程序,或者使用设计标签来查看结果。

工作原理...

如您在查看创建的表格时所见到的那样,屏幕上的表格基本看起来是一样的。主要区别在于创建它们的代码。

TableLayout的 XML 中,每一行都是通过TableRow添加到表格中的。每个视图都成为一个列。这不是必须的,因为单元格可以跳过或留空。(在下一节中了解如何在TableRow中指定单元格位置。)

GridLayout使用相反的方法。在创建表格时指定行数和列数。我们不必指定行或列的信息(尽管我们可以,下面会讨论)。Android 会自动按顺序将每个视图添加到单元格中。

还有更多内容...

首先,让我们看看两种布局之间的更多相似之处。这两种布局都有能力拉伸列以使用剩余的屏幕空间。对于TableLayout,在 xml 声明中添加以下属性:

android:stretchColumns="1"

stretchColumns指定了需要拉伸的列的(基于零的)索引。(android:shrinkColumns是一个可以收缩的列的基于零的索引,以便表格可以适应屏幕。)

为了在GridLayout中实现相同的效果,请在 B 列中的所有视图上添加以下属性(textView2textView5textView8):

android:layout_columnWeight="1"

注意

给定列中的所有单元格必须定义权重,否则它不会拉伸。

现在,让我们来看一下它们之间的不同之处,因为这确实是决定针对特定任务使用哪种布局的关键。首先要注意的是列和行是如何定义的。在TableLayout中,行是明确定义的,使用TableRow。(Android 会根据拥有最多单元格的行来确定表格中的列数。)在定义视图时,使用android:layoutColumn属性来指定列。

相比之下,在GridLayout中,在定义表格时(如前所示使用columnCountrowCount)指定行数和列数。

在前面的示例中,我们只是将TextViews添加到GridLayout中,并让系统自动定位它们。我们可以通过在定义视图时指定行和列的位置来更改此行为,例如:

android:layout_row="2"
android:layout_column="2"

提示

在添加每个视图后,Android 会自动增加单元格计数器,因此下一个视图也应该指定行和列,否则,您可能无法得到预期的结果。

LinearLayout 配方中显示的 LinearLayout 一样,GridLayout 也提供了支持水平和垂直(默认)方向的 orientation 属性。方向决定了单元格的放置方式。(水平方向首先填充列,然后向下移动到下一行。垂直方向则先填充每行的第一列,然后移动到下一列。)

使用 ListViewGridView 和适配器

ListViewGridView 都是 ViewGroup 的后代,但它们更像是一个 View,因为它们是由数据驱动的。换句话说,在设计时,你不需要定义可能填充 ListView(或 GridView)的所有可能的 View,而是从传递给 View 的数据动态创建内容。(ListItem 的布局可以在设计时创建,以控制数据在运行时的外观。)

例如,如果你需要向用户展示一个国家列表,你可以创建一个 LinearLayout 并为每个国家添加一个按钮。这种方法有几个问题:确定可用国家、保持按钮列表更新、有足够的屏幕空间来容纳所有国家等等。否则,你可以创建一个国家列表来填充 ListView,它将为每个条目创建一个按钮。

我们将使用第二种方法创建一个示例,从一个国家名称数组中填充 ListView

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 ListView。默认的 ActivityMain 类扩展了 Activity 类。我们将改为让它扩展 ListActivity 类。然后,我们将创建一个简单的字符串列表并将其绑定到 ListView 上,以在运行时派生按钮。

如何操作...

  1. 打开 MainActivity.java 文件,并更改基本声明,使其扩展 ListActivity 而不是 Activity 类:

    public class MainActivity extends ListActivity {
    
  2. 修改 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();
            }
        });
    }
    
  3. 现在,在模拟器或设备上运行应用程序,查看填充后的 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。

ListViews 在 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 对象在运行时改变视图的边距。

如何操作...

  1. 打开 activity_main.xml 文件,将布局从 RelativeLayout 更改为 LinearLayout。它将如下所示:

    <LinearLayout 
    
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </LinearLayout>
    
  2. 添加一个 TextView 并包含一个 ID,如下所示:

    android:id="@+id/textView"
    
  3. 添加 Button 并包含一个 ID,如下所示:

    android:id="@+id/button"
    
  4. 打开 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;
        }
    });
    
  5. 在设备或模拟器上运行程序。

工作原理...

每个视图(因此也包括 ViewGroup)都有一组与其关联的布局参数。特别是,所有视图都有参数来告诉它们的父视图期望的高度和宽度。这些通过 layout_heightlayout_width 参数定义。我们可以使用 getLayoutParams() 方法从代码中访问此布局信息。布局信息包括布局高度、宽度、边距以及任何类特定的参数。在这个例子中,我们通过获取按钮的 LayoutParams 并改变边距,在每次点击时移动按钮。

使用层次查看器优化布局

在开始优化你的布局之前,了解 Android 布局过程是有帮助的。布局的膨胀开始于活动首次显示时。发生以下三个步骤:

  • 测量(Measure):这里视图确定它们的大小,从父视图开始,遍历所有子视图。父视图可能需要多次调用子视图以确定最终大小。

  • 布局(Layout):这里父视图确定其子视图的位置

  • 绘制(Draw):这里视图实际上被渲染。

这个过程从父视图开始,然后遍历其所有子视图。这些子视图再遍历它们的子视图。这样就创建了布局树(Layout Tree),父视图成为树中的根节点。

层次查看器(Hierarchy Viewer)Android SDK 中包含的一个用于检查布局的工具。它以图形化的方式显示布局树,并附带了每个视图/节点的计时结果。通过检查树状布局和计时,你可以查找低效的设计和瓶颈。拥有这些信息,你就可以优化你的布局了。

对于这个示例,我们将使用层次查看器(Hierarchy Viewer)检查在 使用 RelativeLayout 示例中给出的布局。

准备工作

Using RelativeLayout 配方中的 There's more… 部分展示了一个 LinearLayout 示例,以突出显示布局之间的差异。评论指出 LinearLayout 需要一个嵌套布局。我们将使用示例 LinearLayout 创建一个名为 OptimizingLayouts 的新项目。然后,我们将使用层次结构查看器检查布局。为此,我们需要一个已获得 root 权限的 Android 设备或模拟器。

注意

层次结构查看器只能连接到已获得 root 权限的设备,例如模拟器。

如何操作...

  1. 在 Android Studio 中打开 OptimizingLayouts 项目。在已获得 root 权限的设备(或模拟器)上运行项目,并确保屏幕可见(如需解锁则解锁)。

  2. 在 Android Studio 中,通过以下菜单选项启动 Android Device Monitor:Tools | Android | Android Device Monitor

  3. 在 Android Device Monitor 中,通过转到 Window | Open Perspective… 切换到层次视图视角,这将弹出以下对话框:如何操作...

  4. 现在点击 Hierarchy Viewer 然后点击 OK

  5. 在左侧的 Windows 部分列出了带有运行进程的设备列表。点击 OptimizingLayouts 进程以检查布局。如何操作...

  6. TreeView 部分查看此活动的图形表示(位于层次查看器视角的中心窗格,占据了大部分空间)。如何操作...

工作原理...

树状布局部分显示了组成此布局的视图的图形层次结构以及布局时间。(不幸的是,对于此演示,渲染时间太快,无法进行视觉颜色编码参考。)对于此示例重要的是之前展示的嵌套 LinearLayouts。(花时间探索构成此布局的其他视图是值得的,这样您可以看到 Android 在幕后为我们做了什么。)

如已在 RelativeLayout 示例中提到,解决方案是使用 RelativeLayout 重新设计此布局。理想情况下,我们希望有一个更宽、更扁平的布局,而不是深层嵌套的布局,以减少在调整大小步骤中所需迭代次数。从时间上讲,这显然是一个微不足道的例子,但即使这个例子也可能产生影响。想象一下,用户基于这种低效布局在 ListView 中快速浏览成千上万的条目。如果在滚动时遇到卡顿,您的优化步骤可以从在层次结构查看器中检查布局开始。

还有更多...

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 中可以指代几个不同的概念。当大多数人谈论控件时,他们指的是应用控件,通常出现在主屏幕上。应用控件本身就像迷你应用程序,因为它们通常提供基于它们主要应用程序的功能子集。(通常,大多数应用控件随应用程序一起安装,但这不是必需的。它们可以是独立的应用,以控件格式存在。)一个常见的应用控件示例是提供多种不同主屏幕控件的风 weather 应用程序。第五章,探索片段、应用控件和系统 UI,将讨论主屏幕应用控件并提供创建你自己的食谱。

在为 Android 开发时,控件一词通常指的是在布局文件中放置的专用视图,如 Button、TextView、CheckBox 等。在本章中,我们将专注于应用开发中的控件。

要查看Android SDK 提供的控件列表,请在 Android Studio 中打开一个布局文件,并点击设计标签。在设计视图的左侧,你会在布局部分下方看到控件部分,如下面的屏幕截图所示:

介绍

如你所见,Android SDK 提供了许多有用的控件——从简单的 TextView、Button 或 Checkbox 到更复杂的控件,如 Clock、DatePicker 和 Calendar。内置控件虽然很有用,但扩展 SDK 提供的内容也非常容易。我们可以扩展现有控件来自定义其功能,或者通过扩展基础的 View 类来从头创建我们自己的控件。(我们将在后面的创建自定义组件食谱中提供一个示例。)

控件的视觉外观也可以自定义。这些设置可以用来创建样式,进而用来创建主题。就像在其他开发环境中一样,创建主题可以轻松地改变我们整个应用程序的外观,而无需付出太多努力。最后,Android SDK 还提供了许多内置主题和变体,如来自 Android 3/4 的 Holo 主题和来自 Android 5 的 Material 主题。(Android 6.0 没有发布新主题。)

在布局中插入控件

如您在之前的食谱中所见,小部件 在布局文件中声明,或者在代码中创建。对于这个食谱,我们将逐步使用 Android Studio Designer 添加一个按钮。(对于后续的食谱,我们只展示从 TextView 的布局 XML。)创建按钮后,我们将创建一个 onClickListener()

准备工作

在 Android Studio 中开始一个新项目,并将其命名为 InsertWidget。为创建电话和平板项目选择默认选项,并在提示 Activity 类型时选择 Empty Activity。您可以删除默认的 TextView(或者保留它),因为对于这个食谱来说不需要。

如何操作...

要将小部件插入到布局中,请按照以下步骤操作:

  1. 在 Android Studio 中打开 activity_main.xml 文件并点击 设计 选项卡。如何操作...

  2. 在小部件列表中找到 Button 并将其拖到右侧活动屏幕的中央。Android 会根据按钮放置的位置自动设置布局参数。如果您像截图那样将按钮居中,Android Studio 会在 XML 中设置这些参数。如何操作...

  3. 要查看创建的 xml,请点击如下截图所示的 文本 选项卡。看看按钮是如何使用 RelativeLayout 参数居中的。同时注意默认的 ID,因为下一步我们会需要它。如何操作...

  4. 现在,打开 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();
        }
    });
    
  5. 在设备或模拟器上运行应用程序。

工作原理...

使用 Android Studio 创建 UI 就像拖放 Views 一样简单。您还可以直接在 设计 选项卡中编辑 Views 的属性。切换到 XML 代码只需点击 文本 选项卡。

这里我们所做的是在 Android 开发中非常常见的操作——在 XML 中创建 UI,然后在 Java 代码中将 UI 组件(Views)连接起来。要从代码中引用一个 View,它必须有一个与之关联的资源标识符。这是通过使用 id 参数完成的:

android:id="@+id/button"

我们的 onClickListener 函数会在按钮被按下时在屏幕上显示一个名为 Toast 的弹出消息。

还有更多...

再次看看我们之前创建的标识符格式,@+id/button@ 表示这是一个资源,而 + 符号表示新资源。(如果我们忘记包含加号,将会在编译时出现错误,提示 No resource matched the indicated name(没有资源与指定的名称匹配))。

另请参阅

使用图形显示按钮状态

我们讨论了 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> 以根据指定的状态确定要使用的可绘制资源。以下是一个使用多个状态的 <item> 元素示例:

<item
    android:drawable="@android:color/darker_gray"
    android:state_checked="true"
    android:state_selected="false"/>

提示

需要记住,文件是从上往下读取的,因此第一个符合状态要求的项将被使用。一个默认的可绘制资源,没有包含状态的,应该放在最后。

对于本教程,我们将使用状态选择器根据 ToggleButton 的状态改变背景颜色。

准备工作

在 Android Studio 中创建一个名为 StateSelector 的新项目,使用默认的 手机 & 平板 选项。当提示选择 活动类型 时,选择 空活动。为了便于输入本教程的代码,我们将使用颜色作为表示按钮状态的图形。

如何操作...

我们将从创建状态选择器开始,这是一个用 XML 代码定义的资源文件。然后我们将设置按钮使用新的状态选择器。以下是步骤:

  1. res/drawable 文件夹中创建一个名为 state_selector.xml 的新 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>
    
  2. 现在,打开 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" />
    
  3. 在设备或模拟器上运行应用程序。

工作原理...

这里需要理解的主要概念是 Android 状态选择器。如第二步所示,我们创建了一个资源文件,根据 state_checked 指定了一个 可绘制资源(在这种情况下是颜色)。

除了选中状态,Android 还支持许多其他状态条件。在输入 android:state 时,查看自动完成下拉列表以查看其他选项。

创建好可绘制资源(第一步的 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。在选择活动类型时,选择空活动选项。

如何操作...

我们将从为现有布局添加 ID 属性开始,这样我们就可以在代码中访问布局。一旦我们在代码中有了对布局的引用,我们就可以向现有布局中添加新视图。以下是步骤:

  1. 打开res/layout/activity_main.xml,并为主要的RelativeLayout添加 ID 属性,如下所示:

    android:id="@+id/layout"
    
  2. 完全移除默认的<TextView>元素。

  3. 打开MainActivity.java文件,以便我们可以在onCreate()方法中添加代码。在setContentView()之后添加以下代码,以获取对RelativeLayout的引用:

    RelativeLayout layout = (RelativeLayout)findViewById(R.id.layout);
    
  4. 使用以下代码创建一个 DatePicker 并将其添加到布局中:

    DatePicker datePicker = new DatePicker(this);
    layout.addView(datePicker);
    
  5. 在设备或模拟器上运行程序。

工作原理...

这段代码应该非常直观。首先,我们使用findViewById获取父布局的引用。我们在第一步中为现有的 RelativeLayout 添加了 ID,以便更容易引用。我们在代码中创建一个 DatePicker,并使用addView()方法将其添加到布局中。

还有更多...

如果我们想完全从代码创建整个布局呢?尽管这可能不是最佳实践,但在某些时候,从代码创建布局肯定更容易(也更简单)。让我们看看如果我们不使用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 中使用定义 ID。

创建自定义组件

正如我们在之前的教程中所看到的,Android SDK 提供了广泛的组件。但是当你找不到符合你独特需求的预建组件时会发生什么呢?你可以随时创建自己的组件!

在本教程中,我们将介绍如何创建一个自定义组件,该组件从 View 类派生,就像内置小部件一样。以下是一个高级概述:

  1. 创建一个扩展自 View 的新类。

  2. 创建自定义构造函数。

  3. 重写onMeasure(),默认实现返回 100 x 100 的大小。

  4. 重写onDraw(),默认实现不绘制任何内容。

  5. 定义自定义方法和监听器(例如 on<Event>())。

  6. 实现自定义功能。

提示

虽然重写onMeasure()onDraw()不是严格要求的,但默认行为很可能不是你想要的。

准备就绪

在 Android Studio 中开始一个新项目,并将其命名为CustomView。使用默认的向导选项,包括Phone & Tablet SDK,并在提示选择 Activity 类型时选择Empty Activity。一旦项目文件在 Android Studio 中创建并打开,你就可以开始了。

如何操作...

我们将为自定义组件创建一个新类,从 Android View 类派生。我们的自定义组件可以是现有类的子类,比如 Activity,但我们将在一个单独的文件中创建它,以便更容易维护。以下是步骤:

  1. 首先创建一个新的 Java 类,并将其命名为CustomView。这里我们将实现自定义组件,如引言中所描述的。

  2. 修改类构造函数,使其继承自 View。它应该如下所示:

    public class CustomView extends View {
    
  3. 为类定义一个Paint对象,将在onDraw()中使用:

    final Paint mPaint = new Paint();
    
  4. 创建一个默认构造函数,需要活动Context,这样我们就可以加载视图。我们也将在这里设置画笔属性。构造函数应该如下所示:

    public CustomView(Context context) {
        super(context);
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(30);
    }
    
  5. 按如下方式重写onDraw()方法:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        setBackgroundColor(Color.CYAN);
        canvas.drawText("Custom Text", 100, 100, mPaint);
        invalidate();
    }
    
  6. 最后,在MainActivity.java中通过将setContentView()替换为我们的视图来扩展自定义视图,如下所示:

    setContentView(new CustomView(this));
    
  7. 在设备或模拟器上运行应用程序以查看实际效果。

工作原理...

我们首先扩展了 View 类,正如内置组件所做的。接下来,我们创建默认构造函数。这很重要,因为我们需要将上下文传递给超类,我们通过以下调用实现:

super(context);

我们需要重写onDraw(),否则,如引言中所述,我们的自定义视图将不会显示任何内容。当调用onDraw()时,系统会传递一个画布对象。画布是我们视图的屏幕区域。(因为我们没有重写onMeasure(),我们的视图将是 100 x 100,但由于我们的整个活动仅包含这个视图,因此我们的整个屏幕都是我们的画布。)

我们在类级别创建了Paint对象,并作为final,以更有效地分配内存。(onDraw()应该尽可能高效,因为它每秒可能会被调用多次。)从运行程序中可以看出,我们的 onDraw()实现只是将背景色设置为青色,并使用drawText()将文本打印到屏幕上。

还有更多...

实际上,还有很多内容。我们只是触及了自定义组件能做什么的皮毛。幸运的是,从本例中可以看出,实现基本功能并不需要太多代码。我们可以很容易地用一整章来讨论诸如将布局参数传递给视图、添加监听器回调、重写onMeasure()、在 IDE 中使用我们的视图等主题。这些功能都可以根据你的需求添加。

尽管自定义组件应该能够处理任何解决方案,但可能还有其他编码量更少的选项。扩展现有小部件通常足以满足需求,无需从头开始自定义组件的开销。如果你需要的解决方案包含多个小部件,还有复合控件。复合控件(如组合框)只是将两个或多个控件组合在一起作为一个单独的小部件。

复合控件通常会从布局扩展而来,而不是从视图,因为你将添加多个小部件。你可能不需要重写 onDraw()和 onMeasure(),因为每个小部件都会在其各自的方法中处理绘制。

另请参阅

将样式应用于视图

样式是一组属性设置的集合,用于定义视图的外观。正如在定义布局时你已经看到的,一个视图提供了许多设置以决定它的外观及功能。我们已经设置了视图的高度、宽度、背景颜色和内边距,还有更多的设置,比如文字颜色、字体、文字大小、边距等等。创建样式就像把这些设置从布局中提取出来,然后放入一个样式资源中一样简单。

在这个食谱中,我们将通过创建样式并将其连接到视图的步骤。

与级联样式表类似,Android 样式允许你将设计设置与 UI 代码分开指定。

准备工作

创建一个新的 Android Studio 项目,命名为Styles。使用默认的向导选项创建一个 Phone & Tablet 项目,并在提示选择 Activity 时选择 Empty Activity。默认情况下,向导还会创建一个styles.xml文件,我们将在这个食谱中使用它。

如何操作...

我们将创建自己的样式资源,以改变TextView的外观。我们可以按照以下步骤将新的样式添加到 Android Studio 创建的styles.xml资源中:

  1. 打开位于res/values中的默认styles.xml文件,如下所示:如何操作...

  2. 我们将通过在现有的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>
    
  3. 现在告诉视图使用这个样式。打开activity_main.xml文件,并在现有的<TextView>元素中添加以下属性:

    style="@style/MyStyle"
    
  4. 运行应用程序或在设计标签中查看结果。

它是如何工作的...

样式是一个资源,通过在 xml 文件的<resources>元素中嵌套<style>元素来定义。我们使用了现有的styles.xml文件,但这不是必须的,因为我们可以使用我们想要的任何文件名。正如这个食谱所示,一个 xml 文件中可以包含多个<style>元素。

一旦创建了样式,你可以轻松地将其应用到任意数量的其他视图上。如果你想拥有一个具有相同风格的按钮怎么办?只需在布局中放置一个按钮,并分配相同的样式。

如果我们创建了一个新的按钮,但希望按钮能扩展到视图的完整宽度怎么办?我们如何只为那个视图覆盖样式?很简单,就像你一直做的那样,在布局中指定属性。局部属性将优先于样式中的属性。

还有更多...

样式还有一个特性:继承。在定义样式时指定一个父样式,我们可以让样式相互构建,形成一个样式层次结构。如果你查看styles.xml中的默认样式AppTheme,你会看到以下这一行:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

AppTheme继承自 Android SDK 中定义的主题。

提示

如果你想要从自己创建的样式中继承,有一个快捷方法。你可以先指定父样式的名称,然后是句点,接着是新名称,例如:

<style name="MyParent.MyStyle" >

你已经看到了如何为视图指定样式,但如果我们希望应用中的所有 TextView 都使用特定样式呢?我们不得不回到每个 TextView 并指定样式。但还有另一种方法。我们可以在样式中包含一个textViewStyle项,以自动为所有 TextView 分配样式。(每种小部件类型都有一个样式,因此你可以对按钮、切换按钮、文本视图等进行此操作。)

要为所有 TextView 设置样式,请在AppTheme样式中添加以下行:

<item name="android:textViewStyle">@style/MyStyle</item>

由于我们应用的主题已经使用了AppTheme,因此只需在AppTheme中添加那一行,我们所有的 TextView 就可以使用自定义的MyStyle样式。

另请参阅

Android 设计支持库位于:

android-developers.blogspot.de/2015/05/android-design-support-library.html

将样式转变为主题

主题是一种应用于活动或整个应用的样式。要设置主题,请在AndroidManifest.xml文件中使用android:theme属性。theme属性适用于<Application>元素以及<Activity>元素。该元素内的所有视图都将使用指定的主题样式。

通常设置应用主题,但随后会用不同的主题覆盖特定的活动。

在上一个示例中,我们使用向导自动创建的AppTheme样式设置了textViewStyle。在本示例中,你将学习如何设置应用和活动的主题。

除了我们已经探讨过的样式设置外,还有一些我们没有讨论的样式选项,因为它们不适用于视图,而是适用于整个窗口。例如隐藏应用标题或操作栏以及设置窗口背景等设置,这些适用于窗口,因此必须设置为主题。

对于这个示例,我们将基于自动生成的AppTheme创建一个新主题。我们的新主题将修改窗口外观,使其成为一个对话框。我们还将查看AndroidManifest.xml中的theme设置。

准备就绪

在 Android Studio 中开始一个新项目,并将其命名为Themes。使用默认的向导选项,并在提示活动类型时选择空活动

如何操作...

我们首先在现有的styles.xml文件中添加一个新主题,使我们的活动看起来像一个对话框。以下是创建新主题并设置活动使用新主题的步骤:

  1. 由于主题是在与样式相同的资源中定义的,打开位于 res/values 目录下的 styles.xml 文件,并创建一个新的样式。我们将基于已提供的 AppTheme 创建一个新样式,并设置 windowIsFloating。XML 将如下所示:

    <style name="AppTheme.MyDialog">
        <item name="android:windowIsFloating">true</item>
    </style>
    
  2. 接下来,设置 Activity 使用这个新的对话框主题。打开 AndroidManifest.xml 文件,并在 Activity 元素中添加一个 theme 属性,如下所示:

    <activity android:name=".MainActivity"
        android:theme="@style/AppTheme.MyDialog">
    

    请注意,现在 Application 和 Activity 都将指定一个主题。

  3. 现在,在设备或模拟器上运行应用程序,以查看对话框主题的实际效果。

工作原理...

我们的新主题 MyDialog 使用替代的父主题声明继承基础的 AppTheme,因为 AppTheme 是在我们的代码中定义的(而不是系统主题)。如引言所述,某些设置适用于整个窗口,这就是我们看到 windowIsFloating 设置的原因。一旦声明了我们的新主题,我们就在 AndroidManifest 文件中将主题分配给活动。

还有更多...

你可能已经注意到,我们只需将 windowIsFloating 添加到现有的 AppTheme 中就可以完成。由于此应用只有一个 Activity,最终结果将是相同的,但是,任何新的活动也将显示为对话框。

根据安卓版本选择主题

大多数用户更愿意看到应用使用 Android 提供的最新主题。现在支持 Material 主题,对于升级到 Android Lollipop 的应用来说很常见。为了与市场上许多其他应用竞争,你可能也希望升级你的应用,但那些仍在运行较旧版本 Android 的用户怎么办呢?通过正确设置我们的资源,我们可以使用 Android 中的资源选择,根据用户运行的 Android 操作系统版本自动定义父主题。

首先,让我们探讨 Android 中可用的三个主要主题:

  • 主题 – Gingerbread 及更早版本

  • Theme.Holo – Honeycomb (API 11)

  • Theme.Material – Lollipop (API 21)

(截至目前,Android 6.0 似乎还没有新的主题。)

本指南将展示如何为 Android 正确设置资源目录,以便根据应用运行的 API 版本使用最合适的主题。

准备工作

在 Android Studio 中启动一个新项目,并将其命名为 AutomaticThemeSelector。使用默认向导选项创建一个 Phone & Tablet 项目。在选择 Activity 类型时,选择空活动

如何操作...

根据选择的 API 版本,Android Studio 可能会使用 App 兼容性库。由于我们想要明确设置使用哪个主题,所以在这个项目中我们不希望使用这些库。我们将从确保扩展自通用的 Activity 类开始,然后我们可以添加新的样式资源,根据 API 选择主题。以下是步骤:

  1. 我们需要确保 MainActivityActivity 而不是 AppCompatActivity 扩展。打开 ActivityMain.java 文件,如果需要,将其更改为以下内容:

    public class MainActivity extends Activity {
    
  2. 打开activity_main.xml,并添加两个视图:一个 Button 和一个 Checkbox。

  3. 打开styles.xml,移除AppTheme,因为它将不再使用。添加我们的新主题,使文件内容如下所示:

    <resources>
        <style name="AutomaticTheme" parent="android:Theme.Light">
        </style>
    </resources>
    
  4. 我们需要为 API 11 和 21 创建两个新的 values 文件夹。为此,我们需要将 Android Studio 更改为使用项目视图而不是 Android 视图。(否则,在下一步中我们看不到新文件夹。)在项目窗口顶部,显示Android,将其更改为项目以获取项目视图。请参阅以下截图:如何操作...

  5. 通过在res文件夹上右键并导航到新建 | 目录来创建新目录,如下面的截图所示:如何操作...

    为第一个目录使用以下名称:values-v11

    对第二个目录重复此操作,使用values-v21

  6. 现在,在每个新目录中创建一个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>
    
  7. 最后一步是告诉应用使用我们的新主题。为此,请打开AndroidManifest.xml,并将android:theme属性更改为AutomaticTheme。它应如下所示:

    android:theme="@style/AutomaticTheme"
    
  8. 现在,在物理设备或模拟器上运行应用程序。如果你想查看三个不同的主题,你需要有一个运行不同版本 Android 的设备或模拟器。

工作原理...

在此食谱中,我们使用 Android 资源选择过程来根据 API 版本分配适当的主题(主题也是一种资源)。由于我们需要根据其发布的操作系统版本选择主题,因此我们创建了两个指定 API 版本的新 values 文件夹。这样,我们总共有三个styles.xml文件:默认样式,values-v11目录中的样式,以及values-v21目录中的样式。

注意,相同的主题名称在所有三个styles.xml文件中都有定义。这就是资源选择的工作方式。Android 将使用最适合我们值的目录中的资源。这里我们使用 API 级别,但也有其他标准可供使用。根据其他标准(如屏幕大小、屏幕密度,甚至是方向)定义单独的资源是非常常见的。

上一步是在 Android Manifest 中将我们的新主题指定为应用主题。

还有更多内容…

有关资源选择的更多信息,请参阅前一个食谱“使用图像显示按钮状态”中的“使用针对屏幕特定资源的指定文件夹”主题。

第四章:菜单

在本章中,我们将涵盖以下主题:

  • 创建选项菜单

  • 在运行时修改菜单和菜单项

  • 为视图启用上下文操作模式

  • 在 ListView 中使用上下文批量模式

  • 创建弹出菜单

引言

Android 操作系统是一个不断变化的环境。最早的 Android 设备(在 Android 3.0 之前)需要有一个硬件菜单按钮。尽管现在不再需要硬件按钮,但菜单的重要性并未降低。实际上,Menu 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:这表示要显示的文本

  • icon:这是一个可绘制的资源

  • showAsAction:这已经如下解释(见下一段

  • enabled:默认情况下是启用的

让我们更详细地看一下showAsAction

showAsAction属性控制菜单项的显示方式。选项包括以下内容:

  • ifRoom:如果空间足够,此菜单项应包含在操作栏中

  • withText:表示应显示标题和图标

  • never:表示菜单项不应包含在操作栏中;始终在溢出菜单中显示。

  • always:表示菜单项应始终包含在操作栏中(空间有限,请谨慎使用)。

    注意

    可以使用管道符(|)分隔组合多个选项,例如 showAsAction="ifRoom|withText"

了解了菜单资源的基础知识后,我们现在准备创建一个标准的选项菜单并充气它。

准备就绪

使用 Android Studio 创建一个名为 OptionsMenu 的新项目。选择默认的 Phone & Tablet 选项,并在提示 Activity 类型时选择 Empty Activity。由于向导默认不创建 res/menu 文件夹,在继续之前,请导航至 File | New | Directory 来创建它。

如何操作...

按照前一部分描述创建新项目后,您就可以创建菜单了。但是,首先,我们将在 strings.xml 文件中为菜单标题添加一个字符串资源。在创建菜单的 XML 时,我们将使用新的字符串作为菜单标题。以下是步骤:

  1. 首先打开 strings.xml 文件,并向 <resources> 元素中添加以下 <string> 元素:

    <string name="menu_settings">Settings</string>
    
  2. res/menu 目录中创建一个新文件,并将其命名为 menu_main.xml

  3. 打开 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>
    
  4. 创建好菜单后,我们只需在 ActivityMain.java 中重写 onCreateOptionsMenu() 方法来充气菜单:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
    
  5. 在设备或模拟器上运行程序,以查看操作栏中的菜单。

工作原理...

这里有两个基本步骤:

  1. 在 XML 中定义菜单。

  2. 在 Activity 创建时充气菜单。

作为良好的编程习惯,我们在 strings.xml 文件中定义字符串,而不是在 XML 中硬编码。然后在步骤 3 中使用标准的 Android 字符串标识符为菜单设置标题。由于这是一个“设置”菜单项,我们不希望它在操作栏中显示。为确保它从不显示,请使用 showAsAction="never"

定义好菜单后,我们将在步骤 4 中使用菜单解析器在 Activity 创建期间加载菜单。注意 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>

另请参阅

在运行时修改菜单和菜单项

尽管已经多次提到,但在 XML 中创建 UI 而非在 Java 中被认为是“最佳”的编程实践。仍然有些时候你可能需要在代码中完成这一操作。特别是如果你想根据某些外部条件让菜单项可见(或可用)时。菜单可以包含在资源文件夹中,但有时候你需要代码来执行逻辑。例如,如果你想仅在用户登录应用时提供上传菜单项。

在此食谱中,我们将仅通过代码创建和修改菜单。

准备就绪

在 Android Studio 中创建一个新项目,将其命名为RuntimeMenu,使用默认的手机和平板选项。当提示添加活动时,选择空活动选项。由于我们将完全在代码中创建和修改菜单,因此我们不需要创建res/menu目录。

如何操作...

首先,我们将为我们的菜单项添加字符串资源以及一个切换菜单可见性的按钮。打开res/strings.xml文件并按照以下步骤操作:

  1. 在现有的<resources>元素中添加以下两个字符串:

    <string name="menu_download">Download</string>
    <string name="menu_settings">Settings</string>
    
  2. 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"/>
    
  3. 打开ActivityMain.java文件,在类声明下方添加以下三行代码:

    private final int MENU_DOWNLOAD = 1;
    private final int MENU_SETTINGS = 2;
    private boolean showDownloadMenu = false;
    
  4. 添加以下方法供按钮调用:

    public void toggleMenu(View view) {
        showDownloadMenu=!showDownloadMenu;
    }
    
  5. 当活动首次创建时,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;
    }
    
  6. 为了最佳编程实践,不要使用onCreateOptionsMenu()来更新或更改你的菜单;而是使用onPrepareOptionsMenu()。以下是根据我们的标志更改下载菜单项可见性的代码:

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MenuItem menuItem = menu.findItem(MENU_DOWNLOAD);
        menuItem.setVisible(showDownloadMenu);
        return true;
    }
    
  7. 虽然从技术上讲这个菜谱不需要,但这段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;
    }
    
  8. 在设备或模拟器上运行程序以查看菜单更改。

工作原理...

我们为onCreateOptionsMenu()创建了一个重写,就像在之前的菜谱创建选项菜单中所做的那样。但我们没有膨胀现有的菜单资源,而是使用Menu.add()方法创建菜单。由于我们希望在以后修改菜单项以及响应菜单项事件,因此我们定义了自己的菜单 ID 并将它们传递给add()方法。

onOptionsItemSelected()被所有菜单项调用,因此我们获取菜单 ID 并根据我们创建的 ID 使用switch语句。如果我们正在处理菜单事件,则返回true;否则,我们将事件传递给超类。

菜单的更改发生在onPrepareOptionsMenu()方法中。为了模拟外部事件,我们创建了一个按钮来切换布尔标志。下载菜单的可见性由该标志决定。这就是你需要根据你设定的条件创建自定义代码的地方。你的标志可以使用当前玩家等级来设置,或者当有新等级准备发布时;你发送一个推送消息,从而启用菜单项。

还有更多...

如果我们希望这个下载选项容易被注意到以指示其是否可用呢?我们可以在onPrepareOptionsMenu()中添加以下代码(在返回语句之前),告诉 Android 我们希望操作栏中显示菜单:

menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);

现在如果你运行代码,你将在操作栏中看到下载菜单项,但行为并不正确。

之前,当我们没有在操作栏中显示菜单项时,每次打开溢出菜单,安卓都会调用 onPrepareOptionsMenu() 以更新可见性。为了纠正这种行为,请在由按钮调用的 toggleMenu() 方法中添加以下代码行:

invalidateOptionsMenu();

invalidateOptionsMenu() 调用告诉安卓我们的选项菜单不再有效,这将强制调用 onPrepareOptionsMenu(),从而实现我们预期的行为。

注意

安卓将菜单视为始终开启状态,如果菜单项显示在操作栏中。

为视图启用上下文操作模式

上下文菜单为特定视图提供附加选项,这与在桌面上右键点击的概念相同。安卓目前支持两种不同的方法:浮动的上下文菜单和上下文模式。上下文操作模式是在安卓 3.0 中引入的。较旧的浮动上下文菜单可能导致混淆,因为没有当前选定项的指示,并且不支持对多个项目进行操作,例如一次选择多个电子邮件进行删除。

创建浮动上下文菜单

如果你需要使用旧式的上下文菜单,例如,支持低于安卓 3.0 的设备,它与选项菜单 API 非常相似,只是方法名称不同。要创建菜单,请使用 onCreateContextMenu() 而不是 onCreateOptionsMenu()。要处理菜单项的选择,请使用 onContextItemSelected() 而不是 onOptionsItemSelected()。最后,调用 registerForContextMenu() 以便系统知道你希望为视图处理上下文菜单事件。

由于上下文模式被认为是显示上下文选项的首选方式,本教程将重点介绍新的 API。上下文模式提供了浮动上下文菜单的所有功能,但通过在批量模式下允许选择多个项目,还增加了额外的功能。

本教程将演示如何在单个视图中设置上下文模式。一旦激活,通过长按,一个上下文操作栏CAB)将替代操作栏,直到上下文模式结束。

注意

上下文操作栏与操作栏不同,你的活动不需要包含操作栏。

准备就绪

使用 Android Studio 创建一个新项目,命名为 ContextualMode。选择默认的手机 & 平板选项,并在提示添加活动时选择空活动。创建一个菜单目录(res/menu),就像在第一个教程中创建选项菜单时所做的那样,用于存储上下文菜单的 XML。

如何操作...

我们将创建一个 ImageView 作为初始化上下文模式的宿主视图。由于上下文模式通常是通过长按触发的,我们将在 onCreate() 中为 ImageView 设置一个长按监听器。当被调用时,我们将启动上下文模式,并传递一个 ActionMode 回调来处理上下文模式事件。以下是步骤:

  1. 我们将从添加两个新的字符串资源开始。打开 strings.xml 文件,并添加以下内容:

    <string name="menu_cast">Cast</string>
    <string name="menu_print">Print</string>
    
  2. 字符串创建后,我们现在可以通过在 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>
    
  3. 现在,向 activity_main.xml 添加一个 ImageView,作为启动上下文模式的源头。以下是 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"/>
    
  4. 界面设置好后,我们可以添加上下文模式的代码。首先,我们需要一个全局变量来存储调用 startActionMode() 时返回的 ActionMode 实例。在 MainActivity.java 的类构造函数下方添加以下代码行:

    ActionMode mActionMode;
    
  5. 接下来,创建一个 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;
        }
    };
    
  6. 创建了 ActionMode 回调后,我们只需调用 startActionMode() 来开始上下文模式。在 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;
        }
    });
    
  7. 在设备或模拟器上运行程序,以查看 CAB 的实际效果。

工作原理...

正如你在第二步所看到的,我们使用了相同的菜单 XML 来定义上下文菜单,以及其他的菜单。

需要理解的主要代码是 ActionMode 回调。这里是我们处理上下文模式事件的地方:初始化菜单、处理菜单项选择以及清理。我们在长按事件中通过调用 startActionMode() 并传入在第五步创建的 ActionMode 回调来开始上下文模式。

当触发动作模式时,系统会调用 onCreateActionMode() 回调,该回调会填充菜单并在上下文操作栏中显示。用户可以通过按返回箭头或返回键来关闭上下文操作栏。当用户进行菜单选择时,CAB 也会关闭。我们显示一个 Toast 以对此食谱提供视觉反馈,但这里是你实现功能的地方。

还有更多...

在此示例中,我们从 startActionMode() 调用中存储返回的 ActionMode。我们使用它来防止在动作模式已经激活时创建新实例。我们还可以使用此实例对上下文操作栏本身进行更改,例如,使用以下方式更改标题:

mActionMode.setTitle("New Title");

当处理多个项目选择时,这特别有用,我们将在下一个食谱中看到。

另请参阅

  • 请参阅下一个食谱,在 ListView 中使用上下文批量模式,以处理多个项目选择

在 ListView 中使用上下文批量模式

如前一个食谱所述,上下文模式支持两种使用形式:单一视图模式(如所示)和多重选择(或批量)模式。批量模式是上下文模式优于旧的上下文菜单的地方,因为旧菜单不支持多选。

如果你曾经使用过像 Gmail 这样的电子邮件应用或文件浏览器,你可能在选择多个项目时见过上下文模式。以下是 Solid Explorer 的截图,它展示了材料和上下文模式的优秀实现:

在 ListView 中使用上下文批量模式

在此食谱中,我们将创建一个填充有多个国家名称的ListView,以演示多项选择或批量模式。此示例将使用正常的长按事件以及项目点击事件来启动上下文模式。

准备工作

在 Android Studio 中创建一个新项目,将其命名为ContextualBatchMode。选择默认的手机和平板选项,并在提示添加活动时选择空活动。为上下文菜单创建一个菜单目录(res/menu)。

如何操作...

与之前的食谱类似,我们首先在 XML 中创建一个菜单,以便在开始上下文模式时展开。我们需要定义MultiChoiceModeListener来处理ListView的批量模式。然后设置ListView以允许多项选择,并传入MultiChoiceModeListener。以下是步骤:

  1. 打开strings.xml文件,并按照以下方式添加两个新的字符串资源用于菜单项:

    <string name="menu_move">Move</string>
    <string name="menu_delete">Delete</string>
    
  2. 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>
    
  3. 由于我们需要一个ListView,我们将改变MainActivity使其从ListActivity继承,如下所示:

    public class MainActivity extends ListActivity
    
  4. 创建一个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) {
        }
    };
    
  5. 接下来,我们将改变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);
            }
        });
    }
    
  6. 在设备或模拟器上运行程序,以查看 CAB 的实际操作。

它是如何工作的...

使操作模式在批量模式下工作的三个关键元素是:

  1. 创建一个上下文菜单以展开

  2. 定义MultiChoiceModeListener以传递给setMultiChoiceModeListener()

  3. ListViewChoiceMode设置为CHOICE_MODE_MULTIPLE_MODAL

MultiChoiceModeListener与单视图上下文模式中使用的ActionMode回调相同,实际上实现了ActionMode.Callback。与ActionMode.Callback一样,当MultiChoiceModeListener调用onCreateActionMode()时,菜单会被展开。

默认情况下,通过在ListView中的项目上长按来启动上下文模式。我们将更进一步,当使用onItemClick()事件选中项目时启动上下文模式。如果我们不这样做,启动上下文模式的唯一方式就是通过长按,这可能会让许多用户不知道有额外的功能。

还有更多...

正如本章引言中提到的,你的活动不需要包含操作栏就可以使用上下文操作栏。如果你确实有一个操作栏并且它可见,它将被 CAB 覆盖。如果你没有操作栏作为此食谱的默认设置,布局将被重绘以包含 CAB(当 CAB 消失时再次重绘)。如果你希望操作栏可见,可以更改活动的主题或更改基类,并手动设置ListView

另请参阅

  • 有关ListView的更多信息,请参考第二章,布局

创建弹出菜单

弹出菜单附加到一个类似于下拉菜单的视图上。弹出菜单的想法是提供额外的选项来完成一个动作。一个常见的例子可能是电子邮件应用中的回复按钮。按下时,会显示几个回复选项,如:回复回复所有人转发

下面是食谱中的弹出菜单示例:

创建弹出菜单

如果有空间,Android 将在锚视图下方显示菜单选项;否则,它将在视图上方显示。

提示

弹出菜单不是用来影响视图本身的。这是上下文菜单的目的。相反,请参考为视图启用上下文操作模式食谱中描述的浮动菜单/上下文模式。

在这个食谱中,我们将使用一个ImageButton作为锚视图,创建前面显示的弹出菜单。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为PopupMenu。使用默认的手机 & 平板选项,在选择添加活动时选择空活动。像以前一样,创建一个菜单目录(res/menu)来存储菜单 XML。

如何操作...

我们首先创建在按钮按下时充气的 XML 菜单。在充气弹出菜单之后,我们通过传递回调来处理菜单项选择的回调来调用setOnMenuItemClickListener()。以下是步骤:

  1. strings.xml添加以下字符串:

    <string name="menu_reply">Reply</string>
    <string name="menu_reply_all">Reply All</string>
    <string name="menu_forward">Forward</string>
    
  2. 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>
    
  3. 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"/>
    
  4. 打开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;
            }
        }
    };
    
  5. 最后的代码是处理按钮onClick()事件,如下所示:

    public void showPopupMenu(View view) {
        PopupMenu popupMenu = new PopupMenu(MainActivity.this,view);
        popupMenu.inflate(R.menu.menu_popup);
        popupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener);
        popupMenu.show();
    }
    
  6. 在设备或模拟器上运行程序,以查看弹出菜单。

它是如何工作的...

如果你读过前面的菜单食谱,这可能看起来非常熟悉。基本上,我们只是在ImageButton被按下时充气弹出菜单。我们设置一个菜单项监听器来响应菜单选择。

关键是要了解 Android 中可用的每个菜单选项,这样你就可以为特定场景使用正确的菜单类型。这将帮助你的应用程序提供一致的用户体验,并降低学习曲线。

第五章:探索片段、应用小部件和系统界面

在本章中,我们将涵盖以下主题:

  • 创建和使用片段

  • 运行时添加和移除片段

  • 在片段间传递数据

  • 在主屏幕上创建快捷方式

  • 在主屏幕上创建小部件

  • 向操作栏添加搜索

  • 让你的应用全屏显示

引言

通过第二章对布局的深入了解,布局,我们将进一步探讨使用片段的用户界面开发。片段是将你的用户界面分割成更小部分的一种方式,这些部分可以轻松复用。将片段视为迷你活动,它们有自己的类、布局和生命周期。你不需要在一个活动布局中设计整个屏幕,可能在多个布局中重复功能,你可以将屏幕分解成更小、逻辑上的部分,并将它们转换为片段。然后,你的活动布局可以根据需要引用一个或多个片段。前三个食谱将深入探讨片段。

了解片段后,我们准备扩展关于小部件的讨论。在第三章中,视图、小部件和样式,我们讨论了如何向你的应用添加小部件。现在,我们将看看如何创建一个应用小部件,以便用户可以将他们的应用放在主屏幕上。

本章最后的食谱将探讨系统界面选项。我们有一个食谱,介绍如何使用 Android SearchManager API 在操作栏中添加 Search 选项。最后一个食谱展示了全屏模式以及几种改变系统界面的额外变体。

创建和使用片段

安卓并非一直支持片段。早期的安卓版本是为手机设计的,当时屏幕相对较小。直到安卓开始被用在平板上,才需要将屏幕分割成更小的部分。安卓 3.0 引入了 Fragments 类和片段管理器。

随着新类的出现,也引入了片段生命周期。片段生命周期与第一章中介绍的活动生命周期相似,活动,因为大多数事件与活动生命周期平行。

下面是主要回调函数的简要概述:

  • onAttach(): 当片段与活动关联时调用。

  • onCreate(): 当片段首次被创建时调用。

  • onCreateView(): 当片段即将第一次显示时调用。

  • onActivityCreated(): 当关联的活动被创建时调用。

  • onStart(): 当片段将要对用户可见时调用。

  • onResume(): 在片段显示之前调用。

  • onPause(): 当片段首次被暂停时调用。用户可能会返回到片段,但这里是你应该保存任何用户数据的地方。

  • onStop(): 当片段对用户不再可见时调用。

  • onDestroyView():它被调用以允许最后的清理。

  • onDetach():当片段不再与活动关联时调用。

在我们的第一个练习中,我们将创建一个从标准Fragment类派生的新片段。但我们还可以从其他几个Fragment类派生,包括:

  • DialogFragment:用于创建一个浮动的对话框

  • ListFragment:它在片段中创建一个ListView,类似于ListActivity

  • PreferenceFragment:它创建一个偏好设置对象列表,通常用于设置页面

在本教程中,我们将通过创建一个基于Fragment类的简单片段,并将其包含在活动布局中来逐步操作。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为CreateFragment。使用默认的Phone & Tablet选项,在选择活动类型时选择Empty Activity选项。

如何操作...

在本教程中,我们将创建一个带有伴随布局文件的新Fragment类。然后,我们将片段添加到活动布局中,以便在活动启动时能够看到它。以下是创建和显示新片段的步骤:

  1. 使用以下 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>
    
  2. 创建一个名为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);
        }
    }
    
  3. 打开main_activity.xml文件,用以下<fragment>元素替换现有的<TextView>元素:

      <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" />
    
  4. 在设备或模拟器上运行程序。

它是如何工作的...

我们首先像创建活动一样创建一个新类。在本教程中,我们只创建了一个onCreateView()方法的覆盖,以加载我们的片段布局。但是,与活动事件一样,我们可以根据需要覆盖其他事件。创建新的片段后,我们将其添加到活动布局中。由于Activity类是在Fragments存在之前创建的,所以它们不支持Fragments。如果我们使用纯框架类,我们会希望使用FragmentActivity。如果你使用了 Android Studio 的新项目向导,那么默认情况下MainActivity扩展了AppCompatActivity,这已经包括了片段的支持。

还有更多...

在此教程中,我们仅创建一个简单的片段来教授片段的基础知识。但现在是指出片段强大功能的好时机。如果我们正在创建多个片段(通常是这样,因为使用片段的目的就在于此),在步骤 4 中创建活动布局时,我们可以使用 Android 资源文件夹创建不同的布局配置。竖屏布局可能只有一个片段,而横屏可能有多个片段。

在运行时添加和移除片段

在布局中定义一个 Fragment,就像我们在上一个配方中所做的那样,这称为静态 Fragment,在运行时无法更改。我们将创建一个容器来保存 Fragment,而不是使用<fragment>元素,然后在 Activity 的onCreate()方法中动态创建 Fragment。

FragmentManager提供了在运行时使用FragmentTransaction添加、移除和更改 Fragments 的 API。一个 Fragment 事务包括:

  • 开始一个事务

  • 执行一个或多个动作

  • 提交事务

这个配方将展示通过在运行时添加和移除 Fragments 来演示FragmentManager

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为RuntimeFragments。使用默认的Phone & Tablet选项,在选择Activity Type时选择Empty Activity

如何操作...

为了演示添加和移除 Fragments,我们首先需要创建 Fragments,这可以通过扩展Fragment类来完成。创建新的 Fragments 后,我们需要更改主活动的布局以包含Fragment容器。从那里,我们只需添加处理 Fragment 事务的代码。以下是步骤:

  1. 创建一个名为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>
    
  2. 第二个名为fragment_two.xml的布局文件几乎相同,唯一的区别是文本:

    android:text="Fragment Two"
    
  3. 创建一个名为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
    
  4. 创建第二个名为FragmentTwo的 Java 文件,并包含以下代码:

    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
    
  5. 现在我们需要在主活动布局中添加一个容器和一个按钮。如下更改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>
    
  6. 创建了 Fragments 并且将容器添加到布局中后,我们现在准备编写操作 Fragments 的代码。打开MainActivity.java并在类构造函数下面添加以下代码:

    FragmentOne mFragmentOne;
    FragmentTwo mFragmentTwo;
    int showingFragment=0;
    
  7. 在现有的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
    
  8. 我们需要添加的最后一段代码处理按钮触发的 Fragment 切换:

    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();
    }
    
  9. 在设备或模拟器上运行程序。

工作原理...

这个配方的多数步骤涉及设置 Fragments。一旦声明了 Fragments,我们将在onCreate()方法中创建它们。尽管代码可以压缩成单行,但它以长形式展示,这样更容易阅读和理解。

首先,我们获取FragmentManager以便开始一个FragmentTransaction。一旦有了FragmentTransaction,我们通过beginTransaction()开始事务。在事务中可以发生多个动作,但这里我们只需要add()我们的初始 Fragment。我们调用commit()方法来最终确定事务。

现在您理解了 Fragment 事务,这是onCreate()的简洁版本:

getFragmentManager().beginTransaction().add(R.id.framLayout, mFragmentOne).commit();

switchFragment基本上执行相同类型的 Fragment 事务。我们不是调用add()方法,而是使用replace()方法替换现有 Fragment。我们通过showingFragment变量跟踪当前 Fragment,这样我们就知道接下来要显示哪个 Fragment。我们不仅限于在两个 Fragment 之间切换。如果我们需要额外的 Fragment,只需创建它们即可。

还有更多...

在第一章《活动》中的活动间切换一节,我们讨论了返回栈。大多数用户会期望按返回键可以向后穿过“屏幕”,他们不知道或不在乎这些屏幕是活动还是 Fragment。幸运的是,Android 通过在调用commit()之前添加对addToBackStack()的调用,非常容易地将 Fragment 添加到返回栈中。

提示

如果在没有将 Fragment 添加到返回栈的情况下移除或替换它,它将被立即销毁。如果添加到返回栈中,它会被停止,如果用户返回到该 Fragment,它将被重新启动,而不是重新创建。

在 Fragment 之间传递数据

通常,需要在 Fragment 之间传递信息。电子邮件应用程序就是一个典型的例子。通常在一个 Fragment 中显示电子邮件列表,在另一个 Fragment 中显示电子邮件详情(这通常被称为 Master/Detail 模式)。由于我们只需要为每个 Fragment 编写一次代码,然后就可以将它们包含在不同的布局中,所以 Fragment 使得创建这种模式变得更容易。我们可以在纵向布局中轻松地拥有一个单独的 Fragment,并在选择电子邮件时,用详情 Fragment 替换主 Fragment。我们还可以创建一个双面板布局,其中列表和详情 Fragment 并排显示。无论哪种方式,当用户点击列表中的电子邮件时,电子邮件都会在详情面板中打开。这就是我们需要在两个 Fragment 之间进行通信的时候。

由于 Fragment 的一个主要目标是完全自包含,因此不建议直接在 Fragment 之间进行通信,这有充分的理由。如果 Fragment 必须依赖其他 Fragment,当布局更改且只有一个 Fragment 可用时,你的代码很可能会出问题。幸运的是,在这种情况下也不需要直接通信。所有 Fragment 的通信都应该通过宿主活动进行。宿主活动负责管理 Fragment,并且可以正确地传递消息。

现在的问题变成了:Fragment 是如何与活动通信的?答案是:通过一个接口。你可能已经熟悉了接口,因为视图就是通过接口将事件回传给活动的。按钮点击就是一个常见的例子。

在此食谱中,我们将创建两个片段,以展示通过宿主活动从一个片段向另一个片段传递数据。我们将在上一个食谱的基础上,包括两种不同的活动布局——一种用于竖屏,一种用于横屏。在竖屏模式下,活动将根据需要交换片段。以下是应用程序首次在竖屏模式下运行时的截图:

在片段间传递数据

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

在片段间传递数据

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

在片段间传递数据

由于主/详模式通常涉及主列表,我们将利用 ListFragment(在创建和使用片段介绍中提到)。当列表中的项目被选中时,项目文本(在我们的示例中是国家名称)将通过宿主 Activity 发送到详情片段。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 Fragmentcommunication。使用默认的 Phone & Tablet 选项,在选择 Activity Type 时选择 Empty Activity

如何操作...

为了完全展示工作的片段,我们需要创建两个片段。第一个片段将继承自 ListFragment,因此不需要布局。我们还将进一步创建活动和横屏模式下的两种布局。对于竖屏模式,我们将交换片段,对于横屏模式,我们将并排显示两个片段。

注意

在输入这段代码时,Android Studio 会提供两个不同的库导入选项。由于新项目向导会自动引用 AppCompat 库,我们需要使用支持库 API 而非框架 API。尽管它们非常相似,以下代码使用了支持片段 API。

下面是从第一个片段开始的步骤:

  1. 创建一个名为 MasterFragment 的新 Java 类,并将其更改为继承 ListFragment,如下所示:

    public class MasterFragment extends ListFragment
    

    从以下库导入:

    android.support.v4.app.ListFragment
    
  2. MasterFragment 类内部创建以下 interface

    public interface OnMasterSelectedListener {
        public void onItemSelected(String countryName);
    }
    
  3. 使用以下代码设置接口回调监听器:

    private OnMasterSelectedListener mOnMasterSelectedListener=null;
    
    public void setOnMasterSelectedListener(OnMasterSelectedListener listener) {
        mOnMasterSelectedListener=listener;
    }
    
  4. 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());
                }
            }
        });
    }
    
  5. 接下来我们需要创建 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>
    
  6. 创建一个名为 DetailFragment 的新 Java 类,继承自 Fragment,如下所示:

    public class DetailFragment extends Fragment
    

    从以下库导入:

    android.support.v4.app.Fragment
    
  7. 将以下常量添加到类中:

    public static String KEY_COUNTRY_NAME="KEY_COUNTRY_NAME";
    
  8. 按如下方式覆盖 onCreateView()

    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_detail, container, false);
    }
    
  9. 按如下方式编写 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));
        }
    }
    
  10. 对于这个 Fragment 的最后一个步骤是在接收到选定的国家名称时更新 TextView。将以下方法添加到类中:

    public void showSelectedCountry(String countryName) {
        ((TextView)getView().findViewById(R.id.textViewCountryName)).setText(countryName);
    }
    
  11. 现有的activity_main.xml布局将处理竖屏模式的布局。移除现有的<TextView>并替换为以下<FrameLayout>

    <FrameLayout
        android:id="@+id/frameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    
  12. res文件夹中为横屏布局创建一个新目录,如:res/layout-land

    提示

    如果你没有看到新的res/layout-land目录,从Android视图更改为Project视图。

  13. 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>
    
  14. 最后的步骤是设置MainActivity以处理 Fragments。打开MainActivity.java文件,并添加以下类变量以跟踪单/双面板:

    boolean dualPane;
    
  15. 接下来,按如下方式更改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);
            }
        });
    }
    
  16. 需要添加的最后一段代码是处理将国家名称发送给DetailFragmentsendCountryName()方法:

    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();
        }
    }
    
  17. 在设备或模拟器上运行程序。

工作原理...

我们首先创建MasterFragment。在我们使用的 Master/Detail 模式中,这通常代表一个列表,因此我们通过扩展ListFragment来创建一个列表。ListFragmentListActivity对应的 Fragment 版本。除了从 Fragment 扩展而来,基本上是相同的。

如菜谱介绍中所说,我们不应该尝试直接与其他 Fragments 进行通信。

为了提供一种方法来通知列表项的选择,我们暴露了接口:OnMasterSelectedListener。每次在列表中选择一个项目时,我们都会调用onItemSelected()

在 Fragments 之间传递数据的大部分工作由宿主活动完成,但最终,接收 Fragment 需要一种接收数据的方法。DetailFragment通过以下两种方式支持这一点:

  • 在创建时通过参数包传递国家名称。

  • 一个供活动直接调用的公共方法。

当活动创建 Fragment 时,它还会创建一个bundle来保存我们想要发送的数据。这里我们使用在步骤 7 中定义的KEY_COUNTRY_NAME添加国家名称。我们在onViewCreated()中使用getArguments()获取这个包。如果包中找到了键,则提取并使用showSelectedCountry()方法显示。如果 Fragment 已经可见(在双面板布局中),活动将直接调用此方法。

本菜谱的大部分工作都在活动中完成。我们创建了两个布局:一个用于竖屏,一个用于横屏。Android 将使用在步骤 12中创建的res/layout-land目录选择横屏布局。这两个布局都使用类似于之前练习的<FrameLayout>占位符。我们在onCreate()sendCountryName()中管理两种 Fragments。

onCreate()中,我们通过检查当前布局是否包含frameLayout视图来设置dualPane标志。如果找到了frameLayout(它不会为空),那么我们只有一个面板,因为frameLayout的 ID 只存在于竖屏布局中。如果没有找到 frameLayout,那么我们将有两个<FrameLayout>元素:一个用于MasterFragment,另一个用于DetailFragment

onCreate()中我们最后要做的事情是设置MasterFragment的监听器,通过创建一个匿名回调函数,将国家名称传递给sendCountryName()

sendCountryName()是实际将数据传递给DetailFragment的地方。如果我们处于竖屏(或单面板)模式,我们需要创建一个DetailFragment并替换现有的MasterFragment。在这里,我们创建带有国家名称的包,并调用setArguments()。注意我们在提交事务之前是如何调用addToBackStack()的吗?这使得按下返回键可以将用户带回列表(MasterFragment)。如果我们处于横屏模式,DetailFragment已经可见,因此我们直接调用showSelectedCountry()公共方法。

还有更多...

MasterFragment中,在发送onItemSelected()事件之前,我们通过以下代码检查以确保监听器不为空:

if (mOnMasterSelectedListener != null)

虽然活动负责设置回调以接收事件,但我们不希望如果没有监听器这段代码崩溃。另一种方法是在 Fragment 的onAttach()回调中验证活动是否扩展了我们的接口。

另请参阅

  • 有关 ListViews 的更多信息,请参见第二章中的使用 ListView、GridView 和适配器布局

  • 有关资源目录的更多信息,请参见第三章中的根据 Android 版本选择主题视图、小部件和样式

在主屏幕上创建快捷方式

本食谱解释了如何在用户的主屏幕上为你的应用创建链接或快捷方式。为了避免过于突兀,通常最好让用户在设置中自行选择启动这一选项。

下面是一张截图,展示了我们在主屏幕上的快捷方式:

在主屏幕上创建快捷方式

如你所见,这只是个快捷方式,但在下一个食谱中我们将探讨创建主屏幕(AppWidget)的方法。

准备工作

在 Android Studio 中创建一个新项目,将其命名为:HomescreenShortcut。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动选项。

如何操作...

第一步是添加适当的权限。以下是步骤:

  1. 打开AndroidManifest文件,并添加以下权限:

    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
    
  2. 接下来,打开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"/>
    
  3. 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);
    }
    
  4. 在设备或模拟器上运行程序。注意,每次你按按钮,应用将在主屏幕上创建一个快捷方式。

工作原理...

设置了适当的权限后,这个任务相当直接。当按钮被点击时,代码创建了一个名为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是基于广播接收器的辅助类,因此它使用<receiver>元素在清单中声明。以下是一个示例清单条目:

<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 Widgets 时显示的图片

  • configure(可选):用于启动配置设置的 activity

  • resizeMode(可选):标志表示调整大小选项 - horizontal(水平)、vertical(垂直)、none(无)

  • minResizeWidth(可选):调整大小时允许的最小宽度

  • minResizeHeight(可选):调整大小时允许的最小高度

  • widgetCategory(可选):Android 5+仅支持主屏幕小部件

AppWidgetProvider扩展了BroadcastReceiver类,这就是在清单中声明 App Widget 时使用<receiver>的原因。由于它是BroadcastReceiver,该类仍然接收操作系统的广播事件,但辅助类将这些事件过滤为适用于 App Widget 的事件。AppWidgetProvider类公开以下方法:

  • onUpdate():在最初创建时以及指定的时间间隔调用。

  • onAppWidgetOptionsChanged():在最初创建时以及任何大小更改时调用。

  • onDeleted():每当小部件被移除时调用。

  • onEnabled(): 当小部件首次被放置时调用(在添加第二个及以后的小部件时不会调用)。

  • onDisabled(): 当最后一个部件被移除时调用。

  • onReceive(): 在接收到每个事件时调用,包括前面的那些事件。通常不重写,因为默认实现只发送适用的事件。

最后一个必需的组件是布局。Remote Views 只支持可用布局的一个子集。由于 App Widget 是 Remote View,因此只支持以下布局:

  • FrameLayout

  • LinearLayout

  • RelativeLayout

  • GridLayout

以及以下小部件:

  • AnalogClock

  • Button

  • Chronometer

  • ImageButton

  • ImageView

  • ProgressBar

  • TextView

  • ViewFlipper

  • ListView

  • GridView

  • StackView

  • AdapterViewFlipper

了解了 App Widget 的基础知识后,现在开始编码。我们的示例将涵盖基础知识,以便你可以根据需要扩展功能。这个示例使用了一个带有时钟的视图,按下时,会打开我们的活动。

这张截图显示了在添加到主屏幕时小部件在部件列表中的样子:

创建主屏幕小部件

注意

小部件列表的外观因启动器而异。

这是一张添加到主屏幕后的小部件截图:

创建主屏幕小部件

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:AppWidget。使用默认的Phone & Tablet选项,在选择Activity Type时选择Empty Activity

如何操作...

我们将从创建小部件布局开始,该布局位于标准布局资源目录中。然后我们将创建 xml 资源目录以存储AppWidgetProviderInfo文件。我们将添加一个新的 Java 类并扩展AppWidgetProvider,它处理小部件的onUpdate()调用。创建接收器后,我们可以将其添加到 Android 清单中。

以下是详细的步骤:

  1. 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>
    
  2. 在资源目录中创建一个名为xml的新目录。最终结果将是:res/xml

  3. 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切换到Project

  4. 创建一个名为HomescreenWidgetProvider的新 Java 类,继承自AppWidgetProvider

  5. HomescreenWidgetProvider类中添加以下onUpdate()方法:

    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);
        }
    }
    
  6. AndroidManifest中使用以下 XML 声明将HomescreenWidgetProvider添加到<application>元素中:

    <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>
    
  7. 在设备或模拟器上运行程序。首次运行应用程序后,小部件就可以添加到主屏幕上了。

工作原理...

我们的第一步是创建小部件的布局文件。这是一个标准的布局资源,受到 App Widget 作为远程视图的限制,如菜谱介绍中所述。尽管我们的示例使用了模拟时钟小部件,但这是根据你的应用程序需求扩展功能的地方。

XML 资源目录用于存储AppWidgetProviderInfo,它定义了默认的小部件设置。配置设置决定了在最初浏览可用小部件时如何显示小部件。本例中我们使用非常基础的设置,但它们可以轻松扩展,以包含如预览图像显示功能正常的小部件和大小选项等附加功能。updatePeriodMillis属性设置了更新频率。由于更新将唤醒设备,这是保持数据最新与电池寿命之间的权衡。(这时可选的设置活动就很有用,可以让用户决定。)

AppWidgetProvider类是我们处理由updatePeriodMillis轮询触发的onUpdate()事件的地方。我们的示例不需要任何更新,因此我们将轮询设置为 0。尽管如此,在最初放置小部件时仍会调用更新。在onUpdate()中,我们设置了当按下时钟时打开我们应用的待定意图。

由于onUpdate()方法可能是 AppWidgets 最复杂的方面,我们将详细解释这一点。首先需要注意的是,onUpdate()在每个轮询间隔内只发生一次,对于由此提供者创建的所有小部件。(创建第一个之后的小部件将遵循第一个小部件的周期。)这就解释了for循环的必要性,我们需要它来遍历所有现有的小部件。在这里,我们创建了一个待定意图,当按下时钟时调用我们的应用。如前所述,AppWidget 是一个远程视图。因此,为了获取布局,我们使用带有完全限定包名和布局 ID 的RemoteViews()。一旦我们有了布局,就可以使用setOnClickPendingIntent()将待定意图附加到时钟视图上。我们调用名为updateAppWidget()AppWidgetManager来启动我们做出的更改。

要使所有这些工作正常,最后一步是在 Android Manifest 中声明小部件。我们使用<intent-filter>标识我们要处理的行为。大多数应用小部件可能都想处理更新事件,就像我们的例子一样。声明中需要注意的另一项是这一行:

<meta-data android:name="android.appwidget.provider"
    android:resource="@xml/appwidget_info" />

这告诉系统在哪里找到我们的配置文件。

还有更多...

添加 App Widget 配置活动可以使你的小部件更加灵活。你不仅可以提供轮询选项,还可以提供不同的布局、点击行为等。用户往往非常重视灵活的 App Widgets。

添加配置 Activity 需要几个额外的步骤。Activity 需要像往常一样在 Manifest 中声明,但需要包含 APPWIDGET_CONFIGURE 动作,如下例所示:

<activity android:name=".AppWidgetConfigureActivity">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter>
</activity>

Activity 还需要在 AppWidgetProviderInfo 文件中使用 configure 属性指定,如下例所示:

android:configure="com.packtpub.androidcookbook.appwidget.AppWidgetConfigureActivity"

configure 属性需要完全限定的包名,因为此 Activity 将从您的应用外部调用。

提示

请记住,使用配置 Activity 时不会调用 onUpdate() 方法。如果需要,配置 Activity 负责处理任何初始设置。

另请参阅

将搜索添加到操作栏

除了操作栏,Android 3.0 还引入了 SearchView 小部件,创建菜单时可以作为菜单项包含。这是现在推荐使用以提供一致用户体验的 UI 模式。

以下截图展示了搜索图标在操作栏中的初始外观:

将搜索添加到操作栏

此截图展示了按下搜索选项时如何展开:

将搜索添加到操作栏

如果你想在应用中添加搜索功能,本指南将引导你完成设置用户界面和正确配置搜索管理器 API 的步骤。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为 SearchView。使用默认的 Phone & Tablet 选项,在选择 Activity 类型时选择 Empty Activity

如何操作...

要设置搜索 UI 模式,我们需要创建一个搜索菜单项和一个名为 searchable 的资源。我们将创建第二个 Activity 以接收搜索查询。然后我们将在 AndroidManifest 文件中连接所有内容。首先,打开 res/values 中的 strings.xml 文件,并按照以下步骤操作:

  1. 添加以下字符串资源:

    <string name="search_title">Search</string>
    <string name="search_hint">Enter text to search</string>
    
  2. 创建菜单目录:res/menu

  3. 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>
    
  4. 重写 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;
    }
    
  5. 创建一个新的 xml 资源目录:res/xml

  6. res/xml 中创建一个名为 searchable.xml 的新文件,使用以下 xml:

    <?xml version="1.0" encoding="utf-8"?>
    <searchable 
        android:label="@string/app_name"
        android:hint="@string/search_hint" />
    
  7. 使用此 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>
    
  8. 创建一个名为 SearchResultActivity 的新 Activity。

  9. 向类中添加以下变量:

    TextView mTextViewSearchResult;
    
  10. 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));
    }
    
  11. 添加以下方法来处理搜索:

    private void handleSearch(String searchQuery) {
        mTextViewSearchResult.setText(searchQuery);
    }
    
  12. 界面和代码现在已完成,我们只需要在 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>
    
  13. 在设备或模拟器上运行应用程序。输入搜索查询并点击搜索按钮(或按回车键)。SearchResultActivity将显示,并展示输入的搜索查询。

工作原理...

由于新建项目向导使用了AppCompat库,我们的示例使用了支持库 API。使用支持库可以提供最大的设备兼容性,因为它允许在旧版本的 Android OS 上使用现代功能(如操作栏)。这有时可能会带来额外的挑战,因为官方文档通常关注的是框架 API。尽管支持库通常紧跟框架 API,但它们并不总是可以互换的。搜索 UI 模式就是这样的情况,因此值得对之前概述的步骤给予更多关注。

我们从为searchable创建字符串资源开始,如第 6 步中声明的那样。

在第 3 步中,我们创建菜单资源,就像我们之前多次做的那样。一个不同点是,我们为showAsActionactionViewClass属性使用app命名空间。早期版本的 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" />

请注意,这是在应用程序元素中,而不是在任一<activity>元素中。

我们在SearchResultActivity <meta-data>元素中指定可搜索的资源:

<meta-data android:name="android.app.searchable" android:resource="@xml/searchable" />

我们还需要为SearchResultActivity设置意图过滤器,就像这里做的那样:

<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 模式完成后,如何处理搜索取决于你的应用需求。根据你的应用,你可能要搜索本地数据库,或者可能是网络服务。

另请参阅

如果要在互联网上进行搜索,请查看第十二章中的互联网查询电信、网络和 Web

显示应用全屏

Android 4.4(API 19)引入了一个名为沉浸模式的 UI 特性。与之前的全屏标志不同,应用在沉浸模式下接收所有触摸事件。这种模式非常适合某些活动,如阅读书籍和新闻、全屏绘图、游戏或观看视频。实现全屏有几种不同的方法,每种方法都有最佳使用场景:

  • 阅读书籍/文章等:带有便捷访问系统 UI 的沉浸模式

  • 游戏/绘图应用:沉浸模式用于全屏使用但系统 UI 最小化

  • 观看视频:全屏和正常系统 UI

这两种模式的主要区别在于系统 UI 的响应方式。在前两种场景中,应用期望用户交互,因此隐藏系统 UI 以便用户更容易操作(例如,在玩游戏时不会误按返回按钮)。而在使用带有正常系统 UI 的全屏观看视频时,你不会期望用户操作屏幕,所以当用户操作时,系统 UI 应该正常响应。在所有模式下,用户可以通过在隐藏的系统栏内向内滑动来调出系统 UI。

由于观看视频不需要新的沉浸模式,可以使用两个标志SYSTEM_UI_FLAG_FULLSCREENSYSTEM_UI_FLAG_HIDE_NAVIGATION实现全屏模式,这两个标志自 Android 4.0(API 14)起可用。

我们的教程将演示如何设置沉浸模式。我们还将添加通过在屏幕上轻敲来切换系统 UI 的功能。

准备就绪

在 Android Studio 中创建一个新项目,名为ImmersiveMode。使用默认的手机 & 平板选项,在选择活动类型时选择空活动。在选择最低 API 级别时,选择API 19或更高。

如何操作...

我们将创建两个处理系统 UI 可见性的函数,然后我们将创建一个手势监听器来检测屏幕轻敲。这个食谱的所有步骤都是向MainActivity.java添加代码,所以打开文件,让我们开始吧:

  1. 添加以下方法以隐藏系统 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);
    }
    
  2. 添加以下方法以显示系统 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);
    }
    
  3. 添加以下类变量:

    private GestureDetectorCompat mGestureDetector;
    
  4. 在类级别的先前类变量下方,添加以下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;
        }
    }
    
  5. 使用以下内容重写onTouchEvent()回调:

    public boolean onTouchEvent(MotionEvent event){
        mGestureDetector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }
    
  6. onCreate()方法中添加以下代码,以设置GestureListener和隐藏系统 UI:

    mGestureDetector = new GestureDetectorCompat(this, new GestureListener());
    hideSystemUi();
    
  7. 在设备或模拟器上运行应用。向内滑动隐藏的系统栏将显示系统界面。轻触屏幕将切换系统界面。

工作原理...

我们通过在应用窗口上使用setSystemUiVisibility()来创建showSystemUI()hideSystemUI()方法。我们设置的标志(以及不设置的标志)控制着哪些是可见的,哪些是隐藏的。当我们不带有SYSTEM_UI_FLAG_IMMERSIVE标志设置可见性时,实际上,我们禁用了沉浸模式。

如果我们只想隐藏系统界面,我们可以在onCreate()中添加hideSystemUI()就完成了。问题是它不会保持隐藏。一旦用户退出沉浸模式,它将保持常规显示模式。这就是为什么我们创建了GestureListener。(我们将在第八章,使用触摸屏和传感器中再次讨论手势。)由于我们只想响应onSingleTapUp()手势,所以我们没有实现全套手势。当检测到onSingleTapUp时,我们会切换系统界面。

还有更多...

让我们看看一些其他重要的任务:

粘性沉浸

如果我们希望系统界面能自动保持隐藏,还有另一种选择。我们可以不使用SYSTEM_UI_FLAG_IMMERSIVE来隐藏界面,而是使用SYSTEM_UI_FLAG_IMMERSIVE_STICKY

淡化系统界面

如果你只需要减少导航栏的可见性,还可以使用SYSTEM_UI_FLAG_LOW_PROFILE来淡化界面。

使用这个标志与沉浸模式标志相同的setSystemUiVisibility()调用:

getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);

使用setSystemUiVisibility()并传入 0 来清除所有标志:

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 数据库

  • 使用加载器在后台访问数据

简介

由于几乎任何大小应用都需要保存某种类型的数据,Android 提供了许多选项。从保存一个简单值到使用 SQLite 创建完整的数据库,存储选项包括以下内容:

  • 共享偏好设置:简单的名称/值对

  • 内部存储:私有存储中的数据文件

  • 外部存储:在私有或公共存储中的数据文件

  • SQLite 数据库:私有数据可以通过内容提供者暴露数据

  • 云存储:私有服务器或服务提供商

使用内部和外部存储有其优点和权衡。我们将在这里列出一些差异,以帮助你决定是使用内部存储还是外部存储:

  • 内部存储

    • 与外部存储不同,内部存储始终可用,但通常可用空间较少

    • 文件对用户不可见(除非设备拥有 root 权限)

    • 当你的应用被卸载时,文件会自动删除(或者在应用管理器中使用清除缓存/清理文件选项)

  • 外部存储

    • 设备可能没有外部存储,或者可能无法访问(例如连接到计算机时)

    • 文件对用户(和其他应用)可见,无需 root 权限

    • 当你的应用被卸载时,文件不会被删除(除非你使用getExternalFilesDir()获取特定于应用的公共存储)

在本章中,我们将演示如何使用共享偏好设置、内部和外部存储以及 SQLite 数据库。对于云存储,请查看第十二章中的互联网食谱,电信、网络和互联网以及第十五章中的在线服务提供商,后端即服务选项

存储简单数据

存储简单数据是一个常见需求,Android 使用偏好设置 API 使其变得简单。不仅限于用户偏好;你可以使用名称/值对存储任何原始数据类型。

我们将演示如何从EditText保存一个名字,并在应用启动时显示它。以下屏幕截图显示了应用首次启动时没有保存名字的样子,以及在保存名字后启动时的样子:

存储简单数据

准备工作

在 Android Studio 中创建一个新项目,并将其命名为:Preferences。使用默认的手机 & 平板选项,在选择活动类型时选择空活动

如何操作...

我们将使用现有的TextView显示欢迎回来的消息,并创建一个新的EditText按钮来保存名字。首先打开activity_main.xml文件:

  1. 替换现有的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"/>
    
  2. 打开ActivityMain.java文件,并添加以下全局声明:

    private final String NAME="NAME";
    private EditText mEditTextName;
    
  3. 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);
    
  4. 添加以下saveName()方法:

    public void saveName(View view) {
        SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit();
        editor.putString(NAME, mEditTextName.getText().toString());
        editor.commit();
    }
    
  5. 在设备或模拟器上运行程序。由于我们要演示持久化数据,所以在onCreate()期间会加载名称,因此保存一个名称并重新启动程序以查看加载过程。

它是如何工作的...

为了加载名称,我们首先获取对SharedPreference的引用,这样就可以调用getString()方法。我们传入名称/值对的关键字,以及如果找不到关键字时要返回的默认值。

为了保存首选项,我们首先需要获取对首选项编辑器的引用。我们使用putString()然后调用commit()。如果没有commit(),更改将不会被保存。

还有更多...

我们的示例将所有首选项存储在单个文件中。我们还可以使用getSharedPreferences()并传递名称,在不同文件中存储首选项。如果你想要为多个用户设置不同的配置文件,可以使用这个选项。

在内部存储中读写文本文件

当简单的名称/值对不够用时,Android 还支持常规文件操作,包括处理文本和二进制数据。

以下示例展示了如何将文件读取和写入内部或私有存储。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为InternalStorageFile。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

如何操作...

为了演示读取和写入文本,我们需要一个带有EditText和两个按钮的布局。首先打开main_activity.xml文件,并按照以下步骤操作:

  1. 用以下视图替换现有的<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"/>
    
  2. 现在打开ActivityMain.java文件,并添加以下全局变量:

    private final String FILENAME="testfile.txt";
    EditText mEditText;
    
  3. setContentView()方法后,向onCreate()方法中添加以下内容:

    mEditText = (EditText)findViewById(R.id.editText);
    
  4. 添加以下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();
        }
    }
    
  5. 现在添加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);
    }
    
  6. 在设备或模拟器上运行程序。

它是如何工作的...

我们使用InputStreamFileOutputStream类分别进行读取和写入操作。将文件写入操作简化为从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 步布局,读取和写入内部存储的文本文件

如何操作...

如之前在准备工作部分提到的,我们将使用之前的食谱中的布局。布局文件完成后,第一步将是添加访问外部存储的写入权限。以下是步骤:

  1. 打开 Android Manifest 并添加以下权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
  2. 接下来,打开ActivityMain.java并添加以下全局变量:

    private final String FILENAME="testfile.txt";
    EditText mEditText;
    
  3. onCreate()方法中,在setContentView()之后添加以下内容:

    mEditText = (EditText)findViewById(R.id.editText);
    
  4. 添加以下两种方法来检查存储状态:

    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;
    }
    
  5. 添加以下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();
        }
    }
    
  6. 添加以下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();
        }
    }
    
  7. 在具有外部存储的设备或模拟器上运行程序。

工作原理...

对于内部和外部存储,读取和写入文件基本上是相同的。主要的区别在于,在尝试访问它之前,我们应该检查外部存储的可用性,这是通过isExternalStorageWritable()isExternalStorageReadable()方法完成的。在检查存储状态时,MEDIA_MOUNTED意味着我们可以读取和写入它。

与内部存储示例不同,我们请求工作路径,就像在这行代码中所做的那样:

File textFile = new File(Environment.getExternalStorageDirectory(), FILENAME);

实际的读写操作是由相同的类完成的,因为只是位置不同。

提示

硬编码外部文件夹路径是不安全的。该路径可能会因操作系统的版本不同而有所差异,尤其是在不同硬件制造商之间。最佳的做法是调用getExternalStorageDirectory(),如所示。

还有更多...

以下是一些额外的信息讨论。

获取公共文件夹

getExternalStorageDirectory()方法返回外部存储的根目录。如果你想获取特定的公共文件夹,比如MusicRingtone文件夹,请使用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()(复数形式),它也会创建父目录。)有关完整列表,请参见以下链接。

防止文件被包含在图库中

安卓使用了一个媒体扫描器,它会自动将声音、视频和图像文件包含在系统集合中,比如图片库。要排除你的目录,请在你要排除的文件所在的同一目录中创建一个名为.nomedia的空文件(注意前面的句点)。

另请参阅

在项目中包含资源文件

Android 为您的项目提供了两种包含文件的方式:raw 文件夹和 Assets 文件夹。您使用哪种选项取决于您的需求。首先,我们将简要概述每种选项,帮助您决定何时使用每种选项:

  • 原始文件

    • 包含在资源目录中:/res/raw

    • 作为资源,通过原始标识符访问:R.raw.<资源名>

    • 存储媒体文件(如 MP3、MP4 和 OOG 文件)的好地方

  • 资产文件

    • 在您的 APK 中编译文件系统(提供资源 ID)

    • 通过文件名访问文件,通常使得它们更容易与动态创建的名称一起使用。

    • 某些 API 不支持资源标识符,因此需要作为资产包含

通常,raw 文件更容易处理,因为它们是通过资源标识符访问的。正如我们将在本食谱中演示的,主要区别在于您如何访问文件。在这个例子中,我们将加载一个 raw 文本文件和一个 asset 文本文件,并显示其内容。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为:ReadingResourceFiles。使用默认的 手机 & 平板 选项,并在提示 活动类型 时选择 空活动

如何操作...

为了演示从两个资源位置读取内容,我们将创建一个分割布局。我们还需要创建这两个资源文件夹,因为它们不包括在默认的 Android 项目中。以下是步骤:

  1. 打开 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>
    
  2. 在 res 文件夹中创建 raw 资源文件夹。它将被读取为:res/raw

  3. raw 文件夹上右键点击,选择 新建 | 文件 创建一个新文本文件。将文件命名为 raw_text.txt,并在文件中输入一些文本。(运行应用程序时将显示此文本。)

  4. 创建 asset 文件夹。由于位置的原因,asset 文件夹更难以处理。幸运的是,Android Studio 提供了一个菜单选项,使得创建它变得非常简单。转到 文件 菜单(或者在 app 节点上右键点击),然后选择 新建 | 文件夹 | 资产文件夹,如下截图所示:如何操作...

  5. 在 asset 文件夹中创建另一个名为 asset_text.txt 的文本文件。同样,您在这里输入的任何文本在运行应用时都会显示。以下是创建两个文本文件后的最终结果应该看起来像这样:如何操作...

  6. 现在是编写代码的时候了。打开 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();
    }
    
  7. 最后,在 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();
    }
    
  8. 在设备或模拟器上运行程序。

工作原理...

总结一下,唯一的区别在于我们如何获取对每个文件的引用。这行代码读取 raw 资源:

this.getResources().openRawResource(R.raw.raw_text)

而这段代码读取 asset 文件:

this.getAssets().open("asset_text.txt")

这两个调用都返回一个 InputStreamgetText() 方法使用它来读取文件内容。值得注意的是,打开 asset 文本文件的调用需要一个额外的 try/catch。正如菜谱介绍中所提到的,资源是经过索引的,因此我们有编译时验证,而 asset 文件夹没有。

还有更多...

一种常见的方法是将资源包含在 APK 中,但在新资源可用时下载它们。(请参阅 第十二章中的网络通信,电信、网络和互联网。)如果新资源不可用,你总是可以退回到 APK 中的资源。

另请参阅

  • 第十二章中的网络通信菜谱,电信、网络和互联网

创建和使用 SQLite 数据库

在这个菜谱中,我们将演示如何使用 SQLite 数据库。如果你已经熟悉来自其他平台的 SQL 数据库,那么你所知道的大部分内容都将适用。如果你是 SQLite 的新手,请查看“另请参阅”部分中的参考链接,因为此菜谱假设你具有数据库概念的基本理解,包括模式、表、游标和原始 SQL。

为了让你快速开始使用 SQLite 数据库,我们的示例实现了基本的 CRUD 操作。通常,在 Android 中创建数据库时,你会创建一个扩展 SQLiteOpenHelper 的类,这是实现数据库功能的地方。以下是为每个基本操作提供功能的函数列表:

  • 创建:insert()

  • 读取:query()rawQuery()

  • 更新:update()

  • 删除:delete()

为了演示一个完全工作的数据库,我们将创建一个简单的 Dictionary 数据库,以便我们可以存储单词及其定义。我们将通过允许添加新单词(及其定义)和更新现有单词定义来演示 CRUD 操作。我们将使用游标在 ListView 中显示单词。点击 ListView 中的单词将从数据库中读取定义并在 Toast 消息中显示。长按将删除单词。

准备就绪

在 Android Studio 中创建一个新项目,命名为 SQLiteDatabase。使用默认的 Phone & Tablet 选项,并在提示选择 Activity Type 时选择 Empty Activity

如何操作...

首先,我们将创建一个 UI,它包括两个 EditText 字段,一个按钮,和一个 ListView。当我们向数据库添加单词时,它们将填充 ListView。开始时,打开 activity_main.xml 并按照以下步骤操作:

  1. 用以下新视图替换现有的 <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" />
    
  2. 向项目中添加一个名为 DictionaryDatabase 的新 Java 类。这个类从 SQLiteOpenHelper 扩展而来,处理所有的 SQLite 函数。以下是类声明:

    public class DictionaryDatabase extends SQLiteOpenHelper {
    
  3. 在声明下方,添加以下常量:

    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;
    
  4. 添加以下构造函数,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
    }
    
  5. 以下方法负责创建、更新和删除记录:

    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)});
    }
    
  6. 而这些方法处理从数据库读取信息:

    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);
    }
    
  7. 数据库类完成后,打开MainActivity.java。在类声明下面添加以下全局变量:

    EditText mEditTextWord;
    EditText mEditTextDefinition;
    DictionaryDatabase mDB;
    ListView mListView;
    
  8. 添加以下方法以在点击按钮时保存字段:

    private void saveRecord() {
        mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString());
        mEditTextWord.setText("");
        mEditTextDefinition.setText("");
        updateWordList();
    }
    
  9. 添加这个方法来填充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);
    }
    
  10. 最后,在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();
    
  11. 在设备或模拟器上运行程序并尝试一下。

它的工作原理是...

我们将从解释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.javaonCreate()方法进行了我们之前见过的标准初始化,并使用以下代码行创建数据库实例:

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。

另请参阅

在后台使用 Loader 访问数据

任何可能长时间运行的操作都不应该在 UI 线程上执行,因为这可能导致应用程序变慢或无响应。当应用程序无响应时,Android OS 会弹出 应用程序无响应 (ANR) 对话框。

由于查询数据库可能很耗时,Android 在 Android 3.0 中引入了 Loader API。Loader 在后台线程上处理查询,并在完成后通知 UI 线程。

Loaders 的两个主要优点包括:

  • 数据库查询操作(自动)在后台线程中处理

  • 查询(在使用内容提供者数据源时)会自动更新

为了演示 Loader,我们将修改之前的 SQLite 数据库示例,使用 CursorLoader 填充 ListView

准备工作

我们将使用上一个示例中的项目,创建和使用 SQLite 数据库,作为这个示例的基础。在 Android Studio 中创建一个新项目,将其命名为 Loader。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。复制上一个示例中的 DictionaryDatabase 类和布局。尽管我们将使用之前 ActivityMain.java 代码的部分内容,但在这个示例中我们将从头开始,以便更容易跟随。

如何操作...

按照之前的描述设置项目后,我们将从创建两个新的 Java 类开始,然后在 ActivityMain.java 中将所有内容整合在一起。以下是步骤:

  1. 创建一个名为 DictionaryAdapter 的新 Java 类,该类继承自 CursorAdapter。这个类替代了我们在上一个示例中使用的 SimpleCursorAdapter。以下是完整代码:

    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")));
        }
    }
    
  2. 接下来,创建另一个新的 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();
        }
    }
    
  3. 接下来,打开 ActivityMain.java。我们需要将声明更改为实现 LoaderManager.LoaderCallbacks<Cursor> 接口,如下所示:

    public class MainActivity extends AppCompatActivity implements {
    
  4. 将适配器添加到全局声明中。完整的列表如下:

    EditText mEditTextWord;
    EditText mEditTextDefinition;
    DictionaryDatabase mDB;
    ListView mListView;
    DictionaryAdapter mAdapter;
    
  5. 修改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);
    }
    
  6. 我们不再有updateWordList()方法,因此按照以下方式更改saveRecord()

    private void saveRecord() {
        mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString());
        mEditTextWord.setText("");
        mEditTextDefinition.setText("");
        getSupportLoaderManager().restartLoader(0, null, MainActivity.this);
    }
    
  7. 最后,为加载器接口实现以下三个方法:

    @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);
    }
    
  8. 在设备或模拟器上运行程序。

工作原理...

默认的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 数据库作为数据源,对于严肃的应用程序,建议使用。请参阅以下内容提供者链接以开始操作。

另请参阅

第七章:警报和通知

在本章中,我们将涵盖以下主题:

  • 灯光、动作和声音——吸引用户的注意!

  • 使用自定义布局创建 Toast

  • 使用 AlertDialog 显示消息框

  • 显示进度对话框

  • 使用通知重新实现灯光、动作和声音

  • 创建媒体播放器通知

  • 使用抬头通知制作手电筒

简介

Android 提供了多种方式来通知用户——从非视觉方法,包括声音、灯光和振动,到视觉方法,包括 Toast、对话框和状态栏通知。

请记住,通知会分散用户的注意力,因此在使用任何通知时都应该非常谨慎。用户喜欢控制他们的设备(毕竟这是他们的设备),所以给他们启用和禁用通知的选择。否则,用户可能会感到烦恼,并完全卸载你的应用。

我们将从以下基于非 UI 的通知选项开始回顾:

  • 闪烁 LED

  • 振动手机

  • 播放铃声

然后我们将继续讨论视觉通知,包括:

  • Toasts

  • AlertDialog

  • ProgressDialog

  • 状态栏通知

接下来的食谱将向你展示如何在你的应用程序中实现这些功能。阅读以下链接以了解使用通知时的“最佳实践”是非常值得的:

提示

请参考Android 通知设计指南,网址为:developer.android.com/design/patterns/notifications.html

灯光、动作和声音——吸引用户的注意!

本章中的大部分食谱使用 Notification 对象来提醒用户,所以这个食谱将展示当你实际上不需要通知时的替代方法。

如标题所示,我们将使用灯光、动作和声音:

  • 灯光:通常,你会使用 LED 设备,但这仅通过 Notification 对象才可用,我们将在本章后面演示。相反,我们将借此机会使用setTorchMode()(在 API 23—Android 6.0 中添加),使用相机闪光灯作为手电筒。(注意:正如你在代码中看到的,这个功能只会在带有相机闪光灯的 Android 6.0 设备上工作。)

  • 动作:我们将使手机振动。

  • 声音:我们将使用RingtoneManager播放默认通知声音。

如你所见,这些的代码都非常简单。

如以下 使用通知的 Lights, Action, 和 Sound Redux 配方所示,LED、振动和声音这三个选项都可以通过 Notification 对象使用。当用户没有积极使用你的应用时,Notification 对象当然是最合适的方法来提供警报和提醒。但是,当你想在用户使用你的应用时提供反馈时,这些选项是可用的。振动选项就是一个很好的例子;如果你想对按钮按下提供触觉反馈(键盘应用中很常见),可以直接调用振动方法。

准备工作

在 Android Studio 中创建一个新项目,命名为 LightsActionSound。当提示选择 API 级别时,我们需要 API 21 或更高版本来编译项目。在选择 Activity 类型 时,选择 Empty Activity

如何操作...

我们将使用三个按钮来启动每个操作,首先打开 activity_main.xml 并按照以下步骤操作:

  1. 用以下三个按钮替换现有的 <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"/>
    
  2. 向 Android Manifest 添加以下权限:

    <uses-permission android:name="android.permission.VIBRATE"></uses-permission>
    
  3. 打开 ActivityMain.java 并添加以下全局变量:

    private CameraManager mCameraManager;
    private String mCameraId=null;
    private ToggleButton mButtonLights;
    
  4. 添加以下方法以获取相机 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;
    }
    
  5. 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);
    }
    
  6. 现在添加处理每个按钮点击的代码:

    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();
    }
    
  7. 你已经准备好在物理设备上运行应用程序了。这里提供的代码需要 Android 6.0(或更高版本)才能使用手电筒选项。

工作原理...

如前文所述,大部分代码都是关于查找并打开摄像头以使用闪光灯功能。setTorchMode() 在 API 23 中引入,这就是为什么我们要进行 API 版本检查:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){}

这个应用展示了使用在 Lollipop (API 21) 中引入的新的 camera2 库。vibrateringtone 方法自 API 1 以来都已可用。

getCameraId() 方法是我们检查摄像头的位置。我们想要一个带闪光灯的外向摄像头。如果找到,则返回其 ID,否则为 null。如果摄像头 ID 为 null,我们将禁用按钮。

为了播放声音,我们使用来自 RingtoneManagerRingtone 对象。除了实现相对简单之外,这种方法的好处是我们可以使用默认通知声音,通过以下代码获取:

Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

这样,如果用户更改了他们首选的通知声音,我们会自动使用它。

最后是调用手机振动的部分。这是最简单的代码使用,但它确实需要权限,我们已经将其添加到 Manifest 中:

<uses-permission android:name="android.permission.VIBRATE"></uses-permission>

还有更多...

在一个生产级别的应用中,如果你不必这样做,你不会想要简单地禁用按钮。在这种情况下,还有其他方法可以使用相机闪光灯作为手电筒。查看多媒体章节,了解更多关于使用摄像头的示例,我们将会再次看到 getCameraId() 的使用。

另请参阅

  • 在本章后面的用通知的灯光、动作和声音 Redux食谱中,可以看到使用通知对象的等效功能。

  • 有关使用新相机 API 和其他声音选项的示例,请参考第十一章,多媒体

使用自定义布局创建 Toast

在前面的章节中,我们已经大量使用了 Toast,因为它们提供了一种快速简便的方式来显示信息——既适用于用户,也适用于我们调试时。

前面的例子都使用了简单的一行语法,但 Toast 并不限于此。与 Android 中的大多数组件一样,Toast 也可以自定义,我们将在本节中演示这一点。

Android Studio 为制作简单的 Toast 语句提供了快捷方式。当你开始输入 Toast 命令时,按下Ctrl + Spacebar,你会看到以下内容:

使用自定义布局创建 Toast

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

使用自定义布局创建 Toast

当你再次按下Enter键时,它会自动完成以下内容:

Toast.makeText(MainActivity.this, "", Toast.LENGTH_SHORT).show();

在本节中,我们将使用 Toast Builder 来更改默认布局和定位,以创建一个自定义的 Toast,如以下屏幕截图所示:

使用自定义布局创建 Toast

准备就绪

在 Android Studio 中创建一个新项目,将其命名为CustomToast。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

如何操作...

我们将改变 Toast 的形状为正方形,并创建一个自定义布局来显示图像和文本信息。首先打开activity_main.xml并按照以下步骤操作:

  1. 使用以下内容替换现有的<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"/>
    
  2. 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>
    
  3. 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>
    
  4. 现在,打开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();
    }
    
  5. 在设备或模拟器上运行程序。

工作原理...

这个自定义的 Toast 更改了默认的定位、形状,并添加了图像,只是展示“这是可以做到的”。

第一步是创建一个新的 Toast 布局,我们通过膨胀我们的custom_toast布局来实现。一旦我们有了新的布局,我们需要获取TextView,这样我们就可以设置我们的信息,我们使用标准的setText()方法来完成这个操作。完成这些后,我们创建一个 Toast 对象并设置各个属性。我们使用setGravity()方法设置 Toast 的定位。定位决定了我们的 Toast 在屏幕上的显示位置。我们通过setView()方法调用指定我们的自定义布局。与单行版本一样,我们使用show()方法显示 Toast。

使用 AlertDialog 显示消息框

在第四章,菜单中,我们创建了一个主题,使活动看起来像一个对话框。在这个菜谱中,我们将演示如何使用AlertDialog类创建对话框。AlertDialog提供了标题,最多三个按钮,以及一个列表或自定义布局区域,如下例所示:

使用 AlertDialog 显示消息框

注意

按钮的位置可能会根据操作系统版本而有所不同。

准备就绪

在 Android Studio 中创建一个新项目,将其命名为:AlertDialog。使用默认的手机 & 平板选项,在选择活动类型时选择空活动选项。

如何操作...

为了演示,我们将创建一个确认删除对话框,在用户按下删除按钮后提示用户确认。首先打开main_activity.xml布局文件,并按照以下步骤操作:

  1. 添加以下<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"/>
    
  2. 添加由按钮调用的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();
    }
    
  3. 在设备或模拟器上运行应用程序。

作用机理...

这个对话框旨在作为一个简单的确认对话框——例如确认删除操作。基本上,只需创建一个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。使用默认的手机和平板电脑选项,并在提示活动类型时选择空活动

如何操作...

  1. 由于这只是一个使用ProgressDialog的演示,我们将创建一个按钮来显示对话框。为了模拟等待服务器响应,我们将使用一个延迟消息来关闭对话框。首先,打开activity_main.xml并按照以下步骤操作:

  2. <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"/>
    
  3. 打开MainActivity.java并添加以下两个全局变量:

    private ProgressDialog mDialog;
    final int THIRTY_SECONDS=30*1000;
    
  4. 添加由按钮点击引用的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);
    
  5. 在设备或模拟器上运行程序。当你按下显示对话框按钮时,你会看到与简介中屏幕显示的对话框一样的内容。

工作原理...

我们使用ProgressDialog类来显示我们的对话框。这些选项应该是自解释的,但这个设置值得注意:

mDialog.setCancelable(false);

通常,可以通过按下返回键来取消对话框,但当这被设置为 false 时,用户将停留在对话框上,直到从代码中隐藏/关闭它。为了模拟服务器的延迟响应,我们使用了一个HandlerpostDelayed()方法。在指定的毫秒数(在本例中是 30,000,代表 30 秒)之后,将调用run()方法,该方法将关闭我们的对话框。

还有更多...

在这个示例中,我们使用了默认的ProgressDialog设置,创建了一个不确定的对话框指示器,例如,连续旋转的圆圈。如果你可以衡量当前的任务,比如加载文件,你可以使用确定样式代替。添加并运行这行代码:

mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);

你将得到以下对话框样式作为前一行代码的输出:

还有更多...

使用通知重新实现灯光、动作和声音

你可能已经对通知(Notifications)很熟悉了,因为它们已经成为一个突出的功能(甚至已经应用到桌面环境中),而且有充分的理由。它们为向用户发送信息提供了极好的方式。与其他可用的警告和通知选项相比,它们提供了最小侵扰性的选择。

正如我们在第一个食谱“灯光、动作和声音——吸引用户的注意!”中所看到的,灯光、振动和声音都是吸引用户注意力的非常有用的方法。这就是为什么通知对象包括支持这三种方式的原因,我们将在本食谱中展示这一点。鉴于这种吸引用户注意力的能力,仍然应该注意不要滥用用户。否则,他们很可能会卸载你的应用。通常来说,给用户选择启用/禁用通知甚至如何显示通知是一个好主意——带声音或不带声音等。

准备就绪

在 Android Studio 中创建一个新项目,命名为:LightsActionSoundRedux。使用默认的手机 & 平板选项,并在提示选择活动类型时选择空活动

如何操作...

我们需要获得使用振动功能的权限,因此首先打开 Android Manifest 文件,并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.VIBRATE"/>
    
  2. 打开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"/>
    
  3. 现在打开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());
    }
    
  4. 在设备或模拟器上运行程序。

工作原理...

首先,我们将三个动作合并为一个通知,仅仅是因为我们可以这样做。你不必使用所有三个额外的通知选项,甚至一个也不用。以下内容是必需的:

.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()方法添加一个按钮(最多三个)。下面是一个带有一个操作按钮的通知的示例:

使用 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)

按钮操作可能需要更多信息,因为它应该引导用户到应用中的特定项目。你也应该创建一个应用的后退栈以获得最佳用户体验。查看以下链接中的话题"启动活动时保持导航":

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());
    

    展开式通知

  • BigTextStyle

    NotificationCompat.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的示例使用通知的灯光、动作和声音重做不同,这个示例没有使用,因为这种样式在支持库中不可用。

下面是通知显示方式的截图:

创建媒体播放器通知

这张截图展示了锁定屏幕上媒体播放器通知的一个示例:

创建媒体播放器通知

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:MediaPlayerNotification。当提示选择 API 级别时,我们需要为这个项目选择 API 21(或更高)。在选择活动类型时,选择空活动

如何操作...

我们只需要一个按钮来调用我们的代码发送通知。打开activity_main.xml并按照以下步骤操作:

  1. 用以下按钮代码替换现有的<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"/>
    
  2. 打开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);
    }
    
  3. 在设备或模拟器上运行程序。

工作原理...

首先要注意的细节是,我们对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()创建了三个动作来处理媒体播放器的功能。由于我们实际上并没有一个正在运行的媒体播放器,所以所有动作我们都使用了相同的意图,但在你的应用程序中,你应创建独立的意图。

为了让通知在锁定屏幕上可见,我们需要将可见性级别设置为VISIBILITY_PUBLIC,我们通过以下调用实现:

.setVisibility(Notification.VISIBILITY_PUBLIC)

这个调用值得注意:

.setShowActionsInCompactView(1)

正如方法名称所暗示的,这设置了在通知以简化布局显示时展示的动作。(请参阅菜谱介绍中的锁定屏幕图片。)

还有更多...

在这个示例中,我们只创建了视觉通知。如果我们正在创建一个实际的媒体播放器,我们可以实例化一个MediaSession类,并通过以下调用传递会话令牌:

.setMediaSession(mMediaSession.getSessionToken())

这将允许系统识别媒体内容并做出相应的反应,例如在锁定屏幕上用当前专辑封面进行更新。

另请参阅

使用抬头通知制作手电筒

安卓 5.0—棒棒糖(API 21)引入了一种新的通知类型,称为抬头通知。很多人不喜欢这种新通知,因为它可能会非常侵入式,强制出现在其他应用之上。(请看以下截图。)在使用这种类型的通知时要记住这一点。我们将通过一个手电筒来演示抬头通知,因为这展示了一个好的使用场景。

下面是一张稍后我们将要创建的抬头通知的截图:

使用抬头通知制作手电筒

如果你有一个运行安卓 6.0 的设备,你可能已经注意到了新的手电筒设置选项。作为演示,我们将在本食谱中创建类似的东西。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为FlashlightWithHeadsUp。当被提示选择 API 级别时,我们需要为这个项目选择 API 23(或更高)。在选择活动类型时,选择空活动

如何操作...

我们的活动布局将仅包含一个ToggleButton来控制手电筒模式。我们将使用与之前提供的灯光、动作和声音——吸引用户的注意!食谱相同的setTorchMode()代码,并添加一个抬头通知。我们需要使用振动选项的权限,因此首先打开 Android 清单,并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.VIBRATE"/>
    
  2. 通过向<MainActivity>元素添加android:launchMode="singleInstance"来指定我们只希望有一个MainActivity的实例。它将如下所示:

    <activity android:name=".MainActivity"
        android:launchMode="singleInstance">
    
  3. AndroidManifest的更改完成后,打开activity_main.xml布局,并将现有的<TextView>元素替换为此<ToggleButton>代码:

    <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"/>
    
  4. 现在,打开ActivityMain.java并添加以下全局变量:

    private static final String ACTION_STOP="STOP";
    private CameraManager mCameraManager;
    private String mCameraId=null;
    private ToggleButton mButtonLight;
    
  5. 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);
    }
    
  6. 添加以下方法来处理用户按下通知时的响应:

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        if (ACTION_STOP.equals(intent.getAction())) {
            setFlashlight(false);
        }
    }
    
  7. 添加获取相机 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;
    }
    
  8. 添加这两个方法来处理手电筒模式:

    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();
        }
    }
    
  9. 最后,添加这个方法来创建通知:

    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());
    }
    
  10. 你已经准备好在物理设备上运行应用程序了。如前所述,你需要一个运行安卓 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 关闭闪光灯。

还有更多内容...

就像之前在使用自定义布局创建 Toast 的方法中一样,我们也可以在通知中使用自定义布局。在构建器上使用以下方法来指定布局:

headsupContentView()

另请参阅

  • 请参考 灯光、动作和声音 —— 获取用户的注意! 的方法

第八章:使用触摸屏和传感器

在本章中,我们将涵盖以下主题:

  • 监听点击和长按事件

  • 识别轻敲和其他常见手势

  • 多指触控下的捏合缩放手势

  • 滑动刷新

  • 列出可用的传感器——安卓传感器框架简介

  • 读取传感器数据——使用安卓传感器框架事件

  • 读取设备方向

引言

如今,移动设备配备了众多传感器,通常包括陀螺仪、磁力、重力、压力和/或温度传感器,更不用说触摸屏了。这为与用户互动提供了许多新颖而激动人心的选项。通过传感器,您可以确定三维设备位置以及设备本身的使用方式,如摇晃、旋转、倾斜等。即使是触摸屏也提供了许多新的输入方法,从简单的点击到手势和多指触控。

我们将从探索触摸屏交互开始本章内容,先从简单的点击和长按开始,然后使用SimpleOnGestureListener类来检测常见的手势。接下来,我们将通过ScaleGestureDetector来探讨多指触控下的捏合缩放手势。

本书旨在为您的应用添加特性和功能提供快速指南。因此,它专注于所需的代码。强烈建议您花些时间阅读设计指南。

提示

请参考谷歌手势设计指南

在本章的后面部分,我们将探讨安卓中的传感器能力,使用安卓传感器框架。我们将演示如何获取所有可用传感器的列表,以及如何检查特定传感器。一旦获取到传感器,我们将演示如何设置一个监听器来读取传感器数据。最后,我们将通过确定设备方向来结束本章的演示。

监听点击和长按事件

几乎每个应用都需要识别并响应基本的点击和长按事件。这是如此基础,在大多数菜谱中,我们使用 XML 的onClick属性,但对于更高级的监听器,需要通过代码设置。

安卓提供了一个事件监听器接口,当发生某些特定动作时会收到通知,如下所示:

  • onClick(): 当视图被按下时调用

  • onLongClick(): 当视图被长按时调用

  • onFocusChange(): 当用户导航到或离开视图时调用

  • onKey(): 当硬件按键被按下或释放时调用

  • onTouch(): 当触摸事件发生时调用

本节将演示如何响应用户的点击事件以及长按事件。

准备就绪

在 Android Studio 中创建一个新项目,命名为PressEvents。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动

如何操作...

接收基本视图事件设置非常简单。首先我们将创建一个视图;我们的示例将使用按钮,然后在活动的onCreate()方法中设置事件监听器。以下是步骤:

  1. 打开activity_main.xml,并用以下Button替换现有的TextView

    <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" />
    
  2. 现在打开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;
        }
    });
    
  3. 在设备或模拟器上运行应用程序,尝试常规点击和长按。

工作原理...

在本书中使用的大多数示例中,我们使用以下属性在 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 + 空格键查看列表。

识别轻敲和其他常见手势

与前一个食谱中描述的事件监听器不同,手势识别需要两个步骤的过程:

  • 收集移动数据

  • 分析数据以确定它是否与已知的手势匹配

第一步是当用户触摸屏幕时开始,此时会触发onTouchEvent()回调,并通过MotionEvent对象发送移动数据。幸运的是,Android 通过GestureDetector类使第二步(分析数据)变得更容易,该类可以检测以下手势:

  • onTouchEvent()

  • onDown()

  • onFling()

  • onLongPress()

  • onScroll()

  • onShowPress()

  • onDoubleTap()

  • onDoubleTapEvent()

  • onSingleTapConfirmed()

本食谱将演示使用GestureDetector.SimpleOnGestureListener来识别触摸和双击手势。

准备工作

在 Android Studio 中创建一个新项目,命名为CommonGestureDetector。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动

如何操作...

我们将使用活动本身来检测手势,因此不需要向布局中添加任何视图。打开MainActivity.java并按照以下步骤操作:

  1. 添加以下全局变量:

    private GestureDetectorCompat mGestureDetector;
    
  2. 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);
        }
    }
    
  3. 如下重写onTouchEvent()

    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        return super.onTouchEvent(event);
    }
    
  4. 最后,在onCreate()中添加以下代码行:

    mGestureDetector = new GestureDetectorCompat(this, new GestureListener());
    
  5. 在设备或模拟器上运行此应用程序。

工作原理...

我们使用了来自支持库的GestureDetectorCompat,它使得 Android 1.6 及以上版本的设备能够支持手势操作。

如食谱介绍中所述,检测手势是一个两步的过程。为了收集运动或手势数据,我们从触摸事件开始追踪运动。每次调用onTouchEvent()时,我们将这些数据发送给GestureDetectorGestureDetector处理第二步,即分析数据。一旦检测到手势,就会进行适当的回调。我们的示例处理了单点和双击手势。

还有更多...

您的应用程序只需通过重写适当的回调,就可以轻松添加对GestureDetector检测到的其余手势的支持。

另请参阅

  • 有关多指手势,请参阅下一个食谱,使用多指手势进行捏合缩放

使用多指手势进行捏合缩放

之前的食谱使用了SimpleOnGestureListener来提供简单的一指手势的检测。在本食谱中,我们将使用SimpleOnScaleGestureListener类来演示常见的捏合缩放多指手势。

下面的截图展示了使用以下食谱创建的应用程序中图标缩小后的效果:

使用多指手势进行捏合缩放

下面的截图展示了图标放大后的效果:

使用多指手势进行捏合缩放

准备工作

在 Android Studio 中创建一个新项目,命名为MultiTouchZoom。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

如何操作...

为了提供捏合缩放的视觉提示,我们将使用一个带有应用图标的ImageView。打开activity_main.xml并按照以下步骤操作:

  1. 使用以下ImageView替换现有的TextView

    <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" />
    
  2. 现在打开MainActivity.java,并向类中添加以下全局变量:

    private ScaleGestureDetector mScaleGestureDetector;
    private float mScaleFactor = 1.0f;
    private ImageView mImageView;
    
  3. 如下重写onTouchEvent()

    public boolean onTouchEvent(MotionEvent motionEvent) {
        mScaleGestureDetector.onTouchEvent(motionEvent);
        return true;
    }
    
  4. MainActivity类中添加以下ScaleListener类:

    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;
        }
    }
    
  5. 在现有的onCreate()方法中添加以下代码:

    mImageView=(ImageView)findViewById(R.id.imageView);
    mScaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener());
    
  6. 要实验捏合缩放功能,请在带有触摸屏的设备上运行应用程序。

工作原理...

ScaleGestureDetector通过分析手势数据并最终通过onScale()回调报告缩放因子来完成所有工作。我们通过在ScaleGestureDetector上调用getScaleFactor()来获取实际的缩放因子。

我们使用一个带有应用图标的ImageView,通过设置ImageView的缩放比例来提供视觉上的缩放表示,该比例来自ScaleGestureDetector返回的缩放因子。为了防止缩放过大或过小,我们添加了以下检查:

mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));

滑动刷新

通过下拉列表来表示手动刷新的手势称为 Swipe-to-Refresh(下拉刷新)。这是一个如此常见的功能,以至于这个功能已经被封装在一个名为SwipeRefreshLayout的单个小部件中。

本食谱将展示如何使用小部件为ListView添加 Swipe-to-Refresh(下拉刷新)功能。以下屏幕截图显示了刷新动作:

Swipe-to-Refresh

准备就绪

在 Android Studio 中创建一个新项目,命名为SwipeToRefresh。选择默认的Phone & Tablet(手机和平板)选项,并在提示Activity Type(活动类型)时选择Empty Activity(空活动)。

如何操作...

首先,我们需要将SwipeRefreshLayout小部件和ListView添加到活动布局中,然后我们将在 java 代码中实现刷新监听器。以下是详细步骤:

  1. 打开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>
    
  2. 现在打开MainActivity.java文件,并向类中添加以下全局变量:

    SwipeRefreshLayout mSwipeRefreshLayout;
    ListView mListView;
    List mArrayList = new ArrayList<>();
    private int mRefreshCount=0;
    
  3. 添加以下方法以处理刷新:

    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);
    }
    
  4. 在现有的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);
    
  5. 在设备或模拟器上运行应用程序。

工作原理...

本食谱的大部分代码是为了在每次调用刷新方法时向ListView添加项以模拟刷新。实现 Swipe-to-Refresh(下拉刷新)的主要步骤包括:

  1. 添加SwipeRefreshLayout小部件。

  2. SwipeRefreshLayout中包含ListView

  3. 添加OnRefreshListener以调用你的刷新方法。

  4. 更新完成后调用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 传感器框架简介

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",否则应用程序将无法通过 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_ORIENTATIONTYPE_TEMPERATURE,它们已被弃用,因为它们已被新型传感器所取代。

本教程将演示如何获取可用传感器的列表。以下是物理设备上的屏幕截图:

列出可用传感器 – Android 传感器框架简介

准备工作

在 Android Studio 中创建一个新项目,命名为:ListDeviceSensors。选择默认的手机 & 平板选项,并在提示活动类型时选择空活动

如何操作...

首先,我们将查询可用的传感器列表,然后在ListView中显示结果。以下是详细步骤:

  1. 打开activity_main.xml,并用以下内容替换现有的TextView

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
  2. 接下来,打开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);
    
  3. 在设备或模拟器上运行程序。

工作原理...

以下代码行负责获取可用传感器的列表;其余代码用于填充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 传感器框架事件

前一个菜谱,列出可用传感器——Android 传感器框架简介,提供了对 Android 传感器框架的介绍。现在,我们将使用SensorEventListener来读取传感器数据。SensorEventListener接口只有两个回调:

  • onSensorChanged()

  • onAccuracyChanged()

当传感器有新数据要报告时,它会使用SensorEvent对象调用onSensorChanged()。本示例将演示如何读取光线传感器,但由于所有传感器都使用相同的框架,因此很容易将此示例适配到其他任何传感器。(请参阅前一个菜谱介绍中可用的传感器类型列表。)

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:ReadingSensorData。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动

如何操作...

我们将在活动布局中添加一个TextView以显示传感器数据,然后我们将SensorEventListener添加到 java 代码中。我们将使用onResume()onPause()事件来启动和停止我们的事件监听器。要开始,请打开activity_main.xml并按照以下步骤操作:

  1. 按照以下方式修改现有的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"/>
    
  2. 现在,打开MainActivity.java并添加以下全局变量声明:

    private SensorManager mSensorManager;
    private Sensor mSensor;
    private TextView mTextView;
    
  3. 按照以下方式将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
        }
    };
    
  4. 我们将在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);
    }
    
  5. onCreate()中添加以下代码:

    mTextView = (TextView)findViewById(R.id.textView);
    mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
    mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
    
  6. 现在,你可以在物理设备上运行应用程序,以查看光传感器的原始数据。

工作原理...

使用 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

除了TYPE_MAGNETIC_FIELD_UNCALIBRATED发送六个值外,这些传感器在onSensorChanged()事件中返回三个值。

第三个传感器,方向传感器,已被弃用,现在建议使用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_MOTIONTYPE_STEP_DETECTOR表示事件,而TYPE_STEP_COUNTER返回自上次启动以来(传感器处于活动状态时)的步数。

另请参阅

  • 列举可用传感器——介绍 Android 传感器框架的菜谱

  • 第九章,图形和动画中的使用传感器数据和 RotateAnimation 创建指南针的菜谱

  • 关于设备方向,请参阅读取设备方向的菜谱

  • 第十三章,获取位置和使用地理围栏,涵盖了GPS 和位置的菜谱

读取设备方向

尽管 Android 框架会在方向变化时自动加载新资源(如布局),但有时您可能希望禁用此行为。如果您希望被通知方向变化而不是让 Android 自动处理,请在 Android Manifest 中的 Activity 添加以下属性:

android:configChanges="keyboardHidden|orientation|screenSize"

当以下配置更改发生时,系统将通过onConfigurationChanged()方法通知你,而不是自动处理:

  • keyboardHidden

  • orientation

  • screenSize

onConfigurationChanged()的签名如下:

onConfigurationChanged (Configuration newConfig)

你可以在newConfig.orientation中找到新的方向。

提示

禁用自动配置更改(这会导致布局重新加载并重置状态信息)不应用于替代正确保存状态信息。你的应用程序仍然可能在任何时候被中断或完全停止,并被系统杀死。(请参阅第一章中的保存活动状态,了解如何正确保存状态。)

本教程将演示如何确定当前设备方向。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:GetDeviceOrientation。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动

如何操作...

我们将在布局中添加一个按钮,以便按需检查方向。首先打开activity_main.xml并按照以下步骤操作:

  1. 使用以下Button替换现有的TextView

    <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"/>
    
  2. 添加以下方法来处理按钮点击:

    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;
        }
    }
    
  3. 在设备或模拟器上运行应用程序。

    提示

    使用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_90ROTATION_270

另请参阅

第九章:图形和动画

在本章中,我们将涵盖以下主题:

  • 缩小大图片以避免内存溢出异常

  • 过渡动画——定义场景并应用过渡

  • 使用传感器数据和 RotateAnimation 创建指南针

  • 使用 ViewPager 创建幻灯片展示

  • 使用片段创建卡片翻转动画

  • 使用自定义过渡创建缩放动画

引言

动画既可以是视觉上吸引人的,也可以是功能性的,简单的按钮按下操作就证明了这一点。按钮按下的图形表示使应用生动起来,同时通过为用户的事件提供视觉反馈,提供了功能性价值。

Android 框架提供了几种动画系统,以便在您的应用程序中更容易地包含动画。它们包括以下内容:

  • 视图动画:(原始动画系统。)它通常需要的代码较少,但动画选项有限

  • 属性动画:这是一个更灵活的系统,允许对任何对象的任何属性进行动画处理

  • Drawable 动画:它使用 drawable 资源逐帧创建动画(像电影一样)

属性动画系统在 Android 3.0 中引入,通常由于其灵活性而比视图动画更受欢迎。视图动画的主要缺点包括:

  • 可以动画化的方面有限——例如缩放和旋转

  • 只能对视图的内容进行动画处理——它不能改变视图在屏幕上的绘制位置(因此它不能动画化球在屏幕上的移动)

  • 只能对 View 对象进行动画处理

这是一个简单的示例,演示了使用视图动画使视图“闪烁”(模拟按钮按下的简单操作):

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>

如您所见,创建这种动画非常简单,所以如果视图动画实现了您的目标,请使用它。当它不符合您的需求时,转向属性动画系统。我们将在使用片段创建卡片翻转动画使用自定义过渡创建缩放动画的菜谱中展示属性动画,使用新的objectAnimator

过渡动画——定义场景并应用过渡的菜谱将提供有关 Android Transition Framework 的额外信息,我们将在许多菜谱中使用它。

注意

插值器是定义动画变化速率的函数。

插值器将在本章的几个菜谱和前面的闪烁示例中提到。插值器定义了过渡是如何计算的。线性插值器会在设定的持续时间内均匀计算变化,而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 大小。)

如何操作...

如在准备工作中所述,我们需要一张大图片来演示缩放。一旦你有了图片,按照以下步骤操作:

  1. 将图片复制到res/drawable目录下,并命名为image_large.jpg(如果你选择了不同的文件类型,请使用适当的扩展名)。

  2. 打开activity_main.xml文件,将现有的TextView替换为以下ImageView

    <ImageView
        android:id="@+id/imageViewThumbnail"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerInParent="true" />
    
  3. 现在打开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);
    }
    
  4. 在现有的onCreate()方法中添加以下代码:

    ImageView imageView = (ImageView)findViewById(R.id.imageViewThumbnail);
    imageView.setImageBitmap(loadSampledResource(R.drawable.image_large, 100, 100));
    
  5. 在设备或模拟器上运行应用程序。

工作原理...

loadSampledResource()方法的目的是加载一张较小的图片,以减少图片的内存消耗。如果我们尝试加载从www.Pixabay.Com选择的完整图片(见前面的准备部分),应用需要超过 3MB 的 RAM 来加载。这对于大多数设备来说(至少目前是这样)是过多的内存,即使能够完全加载,对于我们的缩略图视图也没有视觉上的好处。

为了避免出现内存不足的情况,我们使用BitmapFactory.OptionsinSampleSize属性来减少或子采样图像。(如果我们设置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 Universal Image Loader:用于加载、缓存和显示图片的强大而灵活的库

还有更多...

需要注意的是,我们传递给loadSampledResource()方法的targetHeighttargetWidth参数实际上并不设置图片大小。如果你使用我们使用的相同大小的图片运行应用程序,那么样本大小将为 32,导致加载的图片大小为 187 x 125。

如果你的布局需要特定大小的图片,可以在布局文件中设置大小,或者可以直接使用 Bitmap 类修改图片大小。

另请参阅

转换动画——定义场景并应用转换

安卓转换框架提供了以下内容:

  • 组级别动画: 动画适用于层次结构中的所有视图

  • 基于转换的动画: 基于开始和结束属性变化的动画

  • 内置动画: 一些常见的过渡效果,如淡入/淡出和移动

  • 资源文件支持: 将动画值保存到资源(XML)文件中,以便在运行时加载

  • 生命周期回调: 动画期间接收回调通知

转换动画包括以下内容:

  • 起始场景: 动画开始时的视图(或ViewGroup

  • Transition: 变化类型(稍后介绍)

  • 结束场景: 动画结束时的视图(或ViewGroup

  • Transitions: 安卓为以下三种转换提供了内置支持:

    • AutoTransition(默认转换): 淡出,移动和调整大小,然后淡入(按此顺序)

    • Fade: 淡入,淡出(默认),或两者都有(指定顺序)

    • ChangeBounds: 移动和调整大小

转换框架将自动创建从开始场景到结束场景所需的帧。

以下是在使用以下类时转换框架的一些已知限制:

  • SurfaceView: 由于SurfaceView动画是在非 UI 线程上执行的,因此动画可能不会正确显示,因此它们可能与应用程序不同步

  • TextView: 动态文本大小变化可能不会正确工作,导致文本直接跳转到最终状态

  • AdapterView: 扩展了AdapterView的类,如ListViewGridView可能会挂起

  • TextureView: 某些转换可能不起作用

本教程提供了一个关于使用过渡动画系统的快速指南。我们将从定义场景和过渡资源开始,然后应用过渡,创建动画。以下步骤将指导您使用 XML 创建资源,这是通常推荐的。也可以通过代码创建资源,我们将在更多内容部分讨论这一点。

准备工作

在 Android Studio 中创建一个新项目,命名为:TransitionAnimation。在目标 Android 设备对话框中,选择手机和平板选项,并为最低 SDK选择 API 19(或更高)。当提示选择活动类型时,选择空活动

如何操作...

以下是创建资源文件并应用过渡动画的步骤:

  1. 按照以下方式更改现有的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>
    
  2. 使用以下 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>
    
  3. 新建一个过渡资源目录(文件 | 新建 | Android 资源目录,并选择过渡作为资源类型)。

  4. res/transition文件夹中创建一个名为transition_move.xml的新文件,使用以下 XML:

    <?xml version="1.0" encoding="utf-8"?>
    <changeBounds  />
    
  5. 使用以下代码添加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);
    }
    
  6. 您已经准备好在设备或模拟器上运行应用程序。

工作原理...

您可能会发现代码本身相当简单。正如简介中所概述的,我们只需要创建开始和结束场景并设置过渡类型。以下是代码的详细分解。

创建开始场景

运行以下代码行将创建开始场景:

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);

我们仍然需要开始和结束场景以及过渡;唯一的区别在于我们如何创建资源。在前面的代码中,我们使用当前布局创建了开始场景。

在我们开始通过代码修改布局之前,我们使用TransitionManagerbeginDelayedTransition()方法以及过渡类型。TransitionManager将跟踪结束场景的变化。当我们调用go()方法时,TransitionManager会自动对变化进行动画处理。

另请参阅

使用传感器数据和 RotateAnimation 创建罗盘。

在上一章中,我们演示了从物理设备传感器读取传感器数据。在那个教程中,我们使用了光线传感器,因为环境传感器数据通常不需要额外的处理。尽管获取磁感应强度数据很容易,但这些数字本身并没有太多意义,当然也不能创建吸引人的显示效果。

在本教程中,我们将演示如何获取磁感应数据以及加速度计数据来计算磁北。我们将使用SensorManager.getRotationMatrix来响应设备移动时对指南针进行动画处理。下面是在物理设备上我们指南针应用程序的截图:

使用传感器数据和 RotateAnimation 创建指南针

准备工作

在 Android Studio 中创建一个新项目,命名为Compass。使用默认的手机和平板选项,在选择活动类型时选择空活动

我们需要一个图像作为指南针指示器。同样,我们可以从www.Pixabay.Com获取图像。我们使用了以下这张图片:

pixabay.com/en/geography-map-compass-rose-plot-42608/

尽管不是必须的,但这张图片具有透明背景,在旋转图像时看起来更好。

如何操作...

如前文准备工作部分所述,我们需要一个指南针的图像。你可以下载之前链接的那个,或者使用你喜欢的任何图像,然后按照以下步骤操作:

  1. 将你的图像复制到res/drawable文件夹中,并将其命名为compass.png

  2. 打开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"/>
    
  3. 现在打开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;
    
  4. MainActivity类中添加以下SensorEventListener类:

    private SensorEventListener mSensorListener = new SensorEventListener() {
        @Override
        public void onSensorChanged(SensorEvent event) {
            calculateCompassDirection(event);
        }
        @Override
        public void onAccuracyChanged(Sensor sensor, int accuracy) {
            //Nothing to do
        }
    };
    
  5. 按如下方式覆盖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);
    }
    
  6. 在现有的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);
    
  7. 最终代码执行实际计算和动画:

    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;
        }
    
  8. 你已经准备好运行应用程序了。尽管你可以在模拟器上运行这个应用程序,但如果没有加速度计和磁力计,你是看不到指南针移动的。

工作原理...

由于我们已经在上一章的读取传感器数据——使用 Android 传感器框架中涵盖了读取传感器数据,所以我们不会重复解释传感器框架,而是直接跳到calculateCompassDirection()方法。

我们直接从onSensorChanged()回调中调用这个方法。由于我们使用了相同的类来处理磁力计和加速度计的传感器回调,因此我们首先检查SensorEvent中报告的是哪个传感器。然后我们调用SensorManager.getRotationMatrix(),传入最后的传感器数据。如果计算成功,它将返回一个RotationMatrix,我们使用它来调用SensorManager.getOrientation()方法。getOrientation()将在orientationValues数组中返回以下数据:

  • Azimuth: value [0]

  • Pitch: value [1]

  • Roll: 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()来创建快速动画。你也可以尝试使用更慢的传感器更新和更慢的动画,并比较结果。

另请参阅

使用 ViewPager 创建幻灯片放映

本教程将向您展示如何使用ViewPager类创建幻灯片。以下是显示从一张图片过渡到另一张图片的屏幕截图:

使用 ViewPager 创建幻灯片

准备就绪

在 Android Studio 中创建一个新项目,名为SlideShow。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

我们需要几幅图像用于幻灯片。为了演示目的,我们从www.Pixabay.com下载了四张图片包含在项目源文件中,但你可以使用任何图片。

如何操作...

我们将创建一个 Fragment 来显示幻灯片中的每张图片,然后在主活动中设置ViewPager。以下是步骤:

  1. 将四张图片复制到/res/drawable文件夹中,并将它们命名为slide_0slide_3,保持其原始文件扩展名。

  2. 使用以下 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>
    
  3. 现在创建一个名为SlideFragment的新 Java 类。它将如下扩展Fragment

    public class SlideFragment extends Fragment {
    

    使用以下导入:

    import android.support.v4.app.Fragment;
    
  4. 添加以下全局声明:

    private int mImageResourceID;
    
  5. 添加以下空的默认片段构造函数:

    public SlideFragment() {}
    
  6. 添加以下方法以保存图像资源 ID:

    public void setImage(int resourceID) {
        mImageResourceID=resourceID;
    }
    
  7. 如下重写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;
    }
    
  8. 我们的主活动将只显示一个ViewPager。打开activity_main.xml,并按以下方式替换文件内容:

    <android.support.v4.view.ViewPager
    
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
  9. 现在打开MainActivity.java,将MainActivity更改为扩展FragmentActivity,如下所示:

    public class MainActivity extends FragmentActivity {
    

    使用以下导入:

    import android.support.v4.app.FragmentActivity;
    
  10. 添加以下全局声明:

    private final int PAGE_COUNT=4;
    private ViewPager mViewPager;
    private PagerAdapter mPagerAdapter;
    

    使用以下导入:

    import android.support.v4.view.PagerAdapter;
    import android.support.v4.view.ViewPager;
    
  11. 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;
    
  12. 如下重写onBackPressed()

    @Override
    public void onBackPressed() {
        if (mViewPager.getCurrentItem() == 0) {
            super.onBackPressed();
        } else {
            mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1);
        }
    }
    
  13. onCreate()方法中添加以下代码:

    mViewPager = (ViewPager) findViewById(R.id.viewPager);
    mPagerAdapter = new SlideAdapter(getSupportFragmentManager());
    mViewPager.setAdapter(mPagerAdapter);
    
  14. 在设备或模拟器上运行应用程序。

工作原理...

第一步是创建一个片段。由于我们要做幻灯片,我们只需要一个ImageViewer。我们还更改MainActivity以扩展FragmentActivity,以便将片段加载到ViewPager中。

ViewPager使用FragmentStatePagerAdapter作为片段过渡的来源。我们创建SlideAdapter来处理FragmentStatePagerAdapter类中的两个回调:

  • getCount()

  • getItem()

getCount()简单地返回我们幻灯片中的页面数。getItem()返回实际要显示的片段。这里我们指定要显示的图像。如您所见,添加或更改幻灯片会非常容易。

处理返回键不是ViewPager的要求,但它确实提供了更好的用户体验。onBackPressed()递减当前页面,直到达到第一页,然后它将返回键发送给超类,退出应用程序。

还有更多...

如示例所示,ViewPager处理了大部分工作,包括过渡动画的处理。如果我们需要,可以通过实现ViewPager.PageTransformer接口上的transformPage()回调来自定义过渡。(有关自定义动画,请参见下一个食谱。)

创建设置向导

ViewPager还可以用于创建设置向导。而不是创建一个用于显示图片的单个碎片,为向导的每个步骤创建一个碎片,并在getItem()回调中返回适当的碎片。

另请参阅

  • 请参考Android ViewPager 文档

  • 要创建自定义动画,请参考创建自定义缩放动画食谱中的示例。

使用碎片创建卡牌翻转动画

牌面翻转是一个常见的动画,我们将使用碎片过渡来演示。我们将使用两张不同的图片——一张用于正面,一张用于背面,以创建牌面翻转效果。我们需要四个动画资源:两个用于正面过渡,两个用于背面过渡,我们将使用objectAnimator在 XML 中定义它们。

这是我们将要构建的应用程序的屏幕截图,展示了卡牌翻转动画的实际效果:

使用碎片创建卡牌翻转动画

准备工作

在 Android Studio 中创建一个新项目,命名为CardFlip。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

对于扑克牌的正反面图片,我们在www.Pixabay.com上找到了以下图片:

如何操作...

我们将需要两个碎片——一个用于牌的正面,另一个用于背面。每个碎片将定义牌的图片。然后我们需要四个动画文件以实现完整的牌面翻转效果。以下是正确设置项目结构并创建所需资源的步骤:

  1. 一旦有了牌的正反面图片,将它们复制到res/drawable文件夹中,分别命名为card_front.jpgcard_back.jpg(如果图片原始文件扩展名不同,请保持原样)。

  2. 创建一个动画资源目录:res/animator。(在 Android Studio 中,转到File | New | Android resource directory。当显示New Android Resource对话框时,在Resource Type下拉菜单中选择animator。)

  3. 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>
    
  4. 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>
    
  5. 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>
    
  6. 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>
    
  7. 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>
    
  8. 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" />
    
  9. 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" />
    
  10. 使用以下代码创建一个名为 CardFrontFragment 的新 Java 类:

    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);
        }
    }
    
  11. 使用以下代码创建一个名为 CardBackFragment 的新 Java 类:

    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);
        }
    }
    
  12. 使用以下 XML 替换现有的 activity_main.xml 文件:

    <FrameLayout
    
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    
  13. 打开 MainActivity.java 并添加以下全局声明:

    boolean mShowingBack = false;
    
  14. 在现有的 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();
    }
    
  15. 添加以下方法,该方法处理实际的片段过渡:

    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();
        }
    }
    
  16. 现在你可以在设备或模拟器上运行应用程序了。

工作原理...

创建卡牌翻转的大部分工作在于设置资源。由于我们需要卡牌的正面和背面视图,因此我们创建了两个带有适当图片的片段。当点击卡牌时,我们调用 flipCard() 方法。实际的动画由 setCustomAnimations() 处理。在这里,我们传递了在 XML 中定义的四个动画资源。正如你所看到的,Android 使这一过程变得非常简单。

需要注意的是,我们没有使用 Support Library Fragment Manager,因为支持库不支持 objectAnimator。如果你想要支持低于 Android 3.0 的版本,你需要包含旧的 anim 资源并在运行时检查操作系统版本,或者在代码中创建动画资源。(请参阅下一个示例。)

参见

使用自定义过渡创建缩放动画

前一个示例,使用片段创建卡牌翻转动画,演示了使用动画资源文件的过渡动画。在本示例中,我们将使用代码中创建的动画资源来创建缩放效果。应用程序显示一个缩略图,当点击时会放大显示完整图片。

下图包含了三个屏幕截图,展示了缩放动画的实际效果:

使用自定义过渡创建缩放动画

准备工作

在 Android Studio 中创建一个新项目,命名为 <项目名称>。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity

为了这个示例所需的图片,我们从 www.Pixabay.com 下载了一张图片并包含在项目源文件中,但你可以使用任何图片。

如何操作...

按照之前所述准备好你的图片后,执行以下步骤:

  1. 将你的图片复制到res/drawable文件夹中,并将其命名为image.jpg(如果不是 jpeg 格式,保持原始文件扩展名)。

  2. 现在,打开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>
    
  3. 现在,打开MainActivity.java文件,并声明以下全局变量:

    private Animator mCurrentAnimator;
    private ImageView mImageViewExpanded;
    
  4. 添加我们在缩放大型图片以避免内存溢出异常食谱中创建的loadSampledResource()方法来缩放图片:

    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));
    }
    
  5. 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);
        }
    });
    
  6. 添加以下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;
    }
    
  7. 在设备或模拟器上运行应用程序。

它是如何工作的...

首先,看看我们使用的布局文件。它有两部分——带有缩略图ImageViewLinearLayout,以及展开的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()控制如何进行转换。(在引言中提到了插值器,并在后面提供了链接。)使用start()方法启动动画后,我们将当前动画保存到mCurrentAnimator变量中,以便在需要时可以取消动画。我们创建了一个AnimatorListenerAdapter以响应动画事件,清除mCurrentAnimator变量。

还有更多...

当用户点击展开的图片时,应用只是隐藏了展开的ImageView并将缩略图设置为可见。我们可以在mImageViewExpanded的点击事件中使用扩展边界作为起点,返回到缩略图边界来创建反向缩放动画。(在zoomFromThumbnail()中创建mImageViewExpanded事件可能更容易,以避免再次重复计算开始和结束边界。)

获取默认动画持续时间

我们的代码在调用setDuration()设置持续时间时使用了 1000 毫秒。我们故意使用较长的持续时间,以便更容易查看动画。我们可以使用以下代码获取默认的 Android 动画持续时间:

getResources().getInteger(android.R.integer.config_shortAnimTime)

另请参阅

第十章:初识 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的步骤开始。与画布类似,GLSurfaceView是你执行 OpenGL 绘图的地方。由于这是起点,其他教程在需要创建GLSurfaceView时会将这个教程作为基本步骤引用。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:SetupOpenGL。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动

如何操作...

我们将从在 Android Manifest 中指明应用程序对 OpenGL 的使用开始,然后将 OpenGL 类添加到活动中。以下是步骤:

  1. 打开 Android Manifest 并添加以下 XML:

    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
    
  2. 打开MainActivity.java并添加以下全局变量:

    private GLSurfaceView mGLSurfaceView;
    
  3. MainActivity类添加以下内部类:

    class CustomGLSurfaceView extends GLSurfaceView {
    
        private final GLRenderer mGLRenderer;
    
        public CustomGLSurfaceView(Context context){
            super(context);
    		setEGLContextClientVersion(2);
            mGLRenderer = new GLRenderer();
            setRenderer(mGLRenderer);
        }
    }
    
  4. 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);}
    }
    
  5. 在现有的onCreate()方法中添加以下代码:

    mGLSurfaceView = new CustomGLSurfaceView(this);
    setContentView(mGLSurfaceView);
    
  6. 你现在可以在设备或模拟器上运行这个应用程序了。

工作原理...

如果你运行了前面的应用程序,你会看到活动创建并且背景设置为灰色。由于这些是设置 OpenGL 的基本步骤,你将在这个章节的其他食谱中重用这段代码。以下是详细解释的过程:

在 Android Manifest 中声明 OpenGL

我们首先在 Android Manifest 中通过这行代码声明我们要求使用 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 绘制提供了一个表面,类似于CanvasSurfaceView对象。实际的绘制在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轴直接从屏幕前或屏幕后出来。

下面是一个展示xyz轴的图示:

在 GLSurfaceView 上绘制形状

我们将创建一个Triangle类,因为它是基本形状。在 OpenGL 中,你通常会使用一系列三角形来创建对象。要使用 OpenGL 绘制形状,我们需要定义以下内容:

  • 顶点着色器:这是为了绘制形状

  • 片元着色器:这是为了给形状上色

  • 程序:这是前面着色器的 OpenGL ES 对象

着色器使用OpenGL 着色语言GLSL)定义,然后编译并添加到 OpenGL 程序对象中。

这有两张屏幕截图,展示了三角形在纵向和横向的显示效果:

在 GLSurfaceView 上绘制形状在 GLSurfaceView 上绘制形状

准备工作

在 Android Studio 中创建一个新项目,并将其命名为:ShapesWithOpenGL。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动

本食谱使用了前一个食谱设置 OpenGL 环境中创建的 OpenGL 环境。如果你还没有完成那些步骤,请参考前一个食谱。

如何操作...

如前所述,我们将使用前一个食谱中创建的 OpenGL 环境。以下步骤将指导你创建一个三角形形状的类并在 GLSurfaceView 上绘制它:

  1. 创建一个名为Triangle的新 Java 类。

  2. 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;
    
  3. Triangle类中添加以下loadShader()方法:

    public int loadShader(int type, String shaderCode){
        int shader = GLES20.glCreateShader(type);
        GLES20.glShaderSource(shader, shaderCode);
        GLES20.glCompileShader(shader);
        return shader;
    }
    
  4. 添加如下所示的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);
    }
    
  5. 添加如下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);
    }
    
  6. 现在打开MainActivity.java,并向GLRenderer类中添加一个Triangle变量,如下所示:

    private Triangle mTriangle;
    
  7. onSurfaceCreated()回调中初始化Triangle变量,如下所示:

    mTriangle = new Triangle();
    
  8. onDrawFrame()回调中调用draw()方法:

    mTriangle.draw();
    
  9. 你现在可以在设备或模拟器上运行应用程序了。

工作原理...

如引言中所述,要使用 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()方法是实际使用 GLES20 库调用进行绘制的位置,它从onDrawFrame()回调中被调用。

还有更多...

你可能已经从引言中的屏幕截图注意到,纵向和横向的三角形看起来是相同的。从代码中可以看出,在绘制时我们没有区分方向。我们将在下一个食谱中解释为什么会这样,并展示如何纠正这个问题。

另请参阅

有关 OpenGL 着色语言的更多信息,请参考以下链接:

www.opengl.org/documentation/glsl/

在绘制时应用投影和摄像机视角

正如在前一个食谱中我们所看到的,当我们把形状绘制到屏幕上时,形状会被屏幕方向扭曲。之所以会发生这种情况,是因为 OpenGL 默认假设屏幕是完美正方形。我们之前提到过,默认的屏幕坐标右上角是(1,1,0),左下角是(-1,-1,0)。

由于大多数设备屏幕都不是完美正方形,我们需要将显示坐标映射到与我们的物理设备相匹配。在 OpenGL 中,我们通过投影来实现这一点。这个食谱将展示如何使用投影将 GLSurfaceView 坐标与设备坐标相匹配。除了投影,我们还将展示如何设置摄像机视角。以下是显示最终结果的屏幕截图:

在绘制时应用投影和摄像机视角

准备工作

在 Android Studio 中创建一个新项目,并将其命名为:ProjectionAndCamera。使用默认的手机 & 平板选项,在选择活动类型时选择空活动

这个食谱基于之前的食谱在 GLSurfaceView 上绘制形状。如果你还没有输入之前的食谱,请在开始这些步骤之前完成它。

如何操作...

如前所述,这个食谱将基于前一个食谱,所以在开始之前请完成那些步骤。我们将修改之前的代码,在绘图计算中添加投影和摄像机视角。以下是步骤:

  1. 打开Triangle类,并在现有声明中添加以下全局声明:

    private int mMVPMatrixHandle;
    
  2. vertexShaderCode中添加一个矩阵变量,并在位置计算中使用它。以下是最终结果:

    private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
        "uniform mat4 uMVPMatrix;" +
        "void main() {" +
        "  gl_Position = uMVPMatrix * vPosition;" +
        "}";
    
  3. 改变draw()方法,按以下方式传入一个矩阵参数:

    public void draw(float[] mvpMatrix) {
    
  4. 为了使用变换矩阵,在draw()方法中,在GLES20.glDrawArrays()方法之前添加以下代码:

    mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
    
  5. 打开MainActivity.java文件,在GLRenderer类中添加以下类变量:

    private final float[] mMVPMatrix = new float[16];
    private final float[] mProjectionMatrix = new float[16];
    private final float[] mViewMatrix = new float[16];
    
  6. 修改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);
    }
    
  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);
    }
    
  8. 你现在可以在设备或模拟器上运行应用程序了。

工作原理...

首先,我们修改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。在选择活动类型时,使用默认的手机 & 平板选项,并选择空活动

本教程基于之前的教程在绘制时应用投影和相机视图。如果你还没有输入之前的教程,请在继续之前完成。

如何操作...

由于我们是从上一个教程继续,所以需要做的工作非常少。打开 MainActivity.java 并按照以下步骤操作:

  1. GLRendered 类中添加一个矩阵:

    private float[] mRotationMatrix = new float[16];
    
  2. 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);
    
  3. 你已经准备好在设备或模拟器上运行应用程序了。

工作原理...

我们使用 Matrix.setRotateM() 方法来根据我们传入的角度计算新的旋转矩阵。在这个例子中,我们使用系统运行时间来计算一个角度。我们可以使用任何我们想要的方法来推导一个角度,比如传感器读数或触摸事件。

还有更多...

使用系统时钟提供了创建连续运动的额外好处,这对于演示目的来说肯定看起来更好。下一个教程将展示如何使用用户输入来导出一个旋转三角形的角。

渲染模式

OpenGL 提供了一个 setRenderMode() 选项,只有在视图变脏时才绘制。通过在 setRenderer() 调用下面的 CustomGLSurfaceView() 构造函数中添加以下代码,可以启用此功能:

setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

这将导致显示更新一次,然后等待我们通过 requestRender() 请求更新。

使用用户输入旋转三角形

上一个示例演示了基于系统时钟旋转三角形。这创建了一个根据我们使用的渲染模式连续旋转的三角形。但是,如果你想要响应用户的输入呢?

在这个教程中,我们将通过覆盖 GLSurfaceViewonTouchEvent() 回调来展示如何响应用户输入。我们将仍然使用 Matrix.setRotateM() 方法来旋转三角形,但不是从系统时间导出角度,而是根据触摸位置计算角度。

这是一张在物理设备上运行此食谱的截图(为了突出触摸,启用了显示触摸的开发者选项):

使用用户输入旋转三角形

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为RotateWithUserInput。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动

本食谱展示了与上一个食谱不同的方法,因此将基于绘制时应用投影和摄像机视图(与上一个食谱相同的起点)。

如何操作...

如前所述,我们将继续从绘制时应用投影和摄像机视图的食谱开始,而不是从上一个食谱。打开MainActivity.java并按照以下步骤操作:

  1. MainActivity类中添加以下全局变量:

    private float mCenterX=0;
    private float mCenterY=0;
    
  2. GLRendered类中添加以下代码:

    private float[] mRotationMatrix = new float[16];
    public volatile float mAngle;
    public void setAngle(float angle) {
        mAngle = angle;
    }
    
    
  3. 在同一类中,通过替换现有的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);
    
  4. onSurfaceChanged()回调中添加以下代码:

    mCenterX=width/2;
    mCenterY=height/2;
    
  5. CustomGLSurfaceView构造函数中添加以下代码,位于setRenderer()下方:

    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    
  6. CustomGLSurfaceView类中添加以下onTouchEvent()

    @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;
    }
    
  7. 你已经准备好在设备或模拟器上运行应用程序了。

它是如何工作的...

本示例与上一个食谱最明显的区别在于我们如何导出传递给Matrix.setRotateM()调用的角度。我们还通过使用setRenderMode()更改了GLSurfaceView的渲染模式,仅在请求时绘制。在onTouchEvent()回调中计算出新角度后,我们使用requestRender()发出请求。

我们还证明了派生我们自己的GLSurfaceView类的重要性。如果没有我们的CustomGLSurfaceView类,我们将无法重写onTouchEvent回调,也无法重写来自GLSurfaceView的其他任何回调。

还有更多...

这结束了 OpenGL ES 的食谱,但我们只是触及了 OpenGL 的强大功能。如果你认真想要学习 OpenGL,请查看下一节中的链接,并阅读关于 OpenGL 的众多书籍之一。

检查可用的众多框架之一,例如 Unreal Engine,也是值得的:

提示

Unreal Engine 4 是由游戏开发者为游戏开发者制作的一套完整的游戏开发工具。

www.unrealengine.com/what-is-unreal-engine-4

另请参阅

第十一章:多媒体

在本章中,我们将涵盖以下主题:

  • 使用 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。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

为了演示同时播放声音,我们至少需要在项目中包含两个音频文件。我们访问了 SoundBible.com(免费版权声音),并找到了两个免费版权的公共领域声音,包含在下载项目文件中:

第一个声音是较长的播放声音:

水声效果

第二个声音较短:

金属掉落声

如何操作...

如前所述,我们需要在项目中包含两个音频文件。准备好您的声音文件后,请按照以下步骤操作:

  1. 创建一个新的 raw 文件夹(文件 | 新建 | Android 资源目录),并在 资源类型 下拉菜单中选择 raw

  2. 将你的声音文件复制到 res/raw 作为 sound_1sound_2。(保留它们的原始扩展名。)

  3. 打开 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"/>
    
  4. 现在,打开 ActivityMain.java 并添加以下全局变量:

    HashMap<Integer, Integer> mHashMap= null;
    SoundPool mSoundPool;
    
  5. 修改现有的 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));
    
  6. 添加 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();
    }
    
  7. 添加 createSoundPooolOld() 方法:

    @SuppressWarnings("deprecation")
    private void createSoundPooolOld(){
        mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);
    }
    
  8. 添加按钮 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);
    }
    
  9. 按如下方式重写 onStop() 回调:

    protected void onStop() {
        super.onStop();
        mSoundPool.release();
    }
    
  10. 在设备或模拟器上运行应用程序。

工作原理...

首先要注意的是我们如何构建这个对象本身。正如我们在引言中提到的,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 指定一个声音;你不能使用自己的声音文件。

另请参阅

使用 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。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity

我们这个示例还需要一个声音文件,将使用上一个示例中的相同长音效“水声”。

第一个声音是一个较长的音效:水声

如何操作...

如前所述,我们需要在项目中包含一个声音文件。准备好声音文件后,请按照以下步骤操作:

  1. 创建一个新的原始资源文件夹(文件 | 新建 | Android 资源目录),并在 资源类型 下拉菜单中选择 raw

  2. 将您的声音文件复制到 res/raw 目录下,命名为 sound_1。(保留原始扩展名。)

  3. 打开 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"/>
    
  4. 现在,打开 ActivityMain.java 文件,并添加以下全局变量:

    MediaPlayer mMediaPlayer;
    
  5. 添加 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();
        }
    }
    
  6. 添加 buttonPause() 方法:

    public void buttonPause(View view){
        if (mMediaPlayer!=null && mMediaPlayer.isPlaying()) {
            mMediaPlayer.pause();
        }
    }
    
  7. 添加 buttonStop() 方法:

    public void buttonStop(View view){
        if (mMediaPlayer!=null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }
    
  8. 最后,用以下代码重写 onStop() 回调方法:

    protected void onStop() {
        super.onStop();
        if (mMediaPlayer!=null) {
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }
    
  9. 现在,您可以在设备或模拟器上运行应用程序了。

工作原理...

这里的代码非常直观。我们创建一个带有声音的 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 线程,否则,你需要自己执行线程操作。(有关线程和选项的更多信息,请参见第十四章,让你的应用准备好上架 Play 商店。)

使用硬件音量键控制你的应用的音频音量

如果你希望音量控制能控制你应用中的音量,请使用setVolumeControlStream()方法来指定应用程序的音频流,如下所示:

setVolumeControlStream(AudioManager.STREAM_MUSIC);

有关其他流选项,请参见以下AudioManager链接。

另请参阅

在你的应用中响应硬件媒体控制

让你的应用响应用户的媒体控制,如播放、暂停、跳过等,是一个用户会非常欣赏的贴心功能。

安卓通过媒体库使这成为可能。与之前使用 SoundPool 播放声音效果的食谱一样,Lollipop 版本改变了解决这个问题的方式。与SoundPool示例不同,这个食谱能够利用另一种方法——兼容性库。

本示例将展示如何设置 MediaSession 以响应硬件按钮,这将适用于 Lollipop 及以上版本,以及使用 MediaSessionCompat 库的早期 Lollilop 版本。(兼容性库将自动处理检查操作系统版本并使用正确的 API 调用。)

准备工作。

在 Android Studio 中创建一个新项目,并将其命名为 HardwareMediaControls。使用默认的 Phone & Tablet 选项,并在提示选择 Activity Type 时选择 Empty Activity

如何操作...

我们将仅使用 Toast 消息来响应硬件事件,因此无需对活动布局进行任何更改。要开始,请打开 ActivityMain.java 并按照以下步骤操作:

  1. 创建以下 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();
        }
    };
    
  2. 在现有的 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);
    
  3. 在带有媒体控制功能(如耳机)的设备或模拟器上运行应用程序,以查看 Toast 消息。

工作原理...

设置此功能共有四个步骤:

  1. 创建一个 MediaSession.Callback 并将其附加到 MediaSession。

  2. 设置 MediaSession 标志,以表示我们希望使用媒体按钮。

  3. SessionState 设置为 active

  4. 使用我们将要处理的操作来设置 PlayBackState

步骤 4 和步骤 1 一起工作,因为回调只会接收到在 PlayBackState 中设置的事件。

由于在本示例中我们实际上并未控制任何播放,只是演示如何响应硬件事件。你需要在 PlayBackState 中实现实际功能,并在 setActions() 调用后包含一个 setState() 的调用。

这是一个很好的示例,展示了 API 的变化如何使事情变得更容易。由于新的 MediaSessionPlaybackState 被整合到兼容性库中,我们可以在旧版本的操作系统上利用这些新的 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?
}

另请参阅

使用默认相机应用程序拍照

如果你的应用程序需要来自相机的图像,但不是相机的替代应用,那么允许“默认”相机应用拍照可能更好。这也尊重用户选择的首选相机应用程序。

当你拍照时,除非它仅适用于你的应用程序,否则最好将照片公开。 (这允许它包含在用户的照片库中。)这个方法将演示如何使用默认的照片应用程序拍照,将其保存到公共文件夹,并显示图像。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:UsingTheDefaultCameraApp。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity

如何操作...

我们将创建一个带有 ImageView 和按钮的布局。按钮将创建一个 Intent 来启动默认的相机应用。当相机应用完成时,我们的应用将得到一个回调。首先打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE" />
    
  2. 打开 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"/>
    
  3. 打开 MainActivity.java 并将以下全局变量添加到 MainActivity 类中:

    final int PHOTO_RESULT=1;
    private Uri mLastPhotoURI=null;
    
  4. 添加以下方法来创建照片的 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));
    }
    
  5. 添加以下方法来处理按钮点击:

    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);
        }
    }
    
  6. 添加一个新的方法来重写 onActivityResult(),如下所示:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == PHOTO_RESULT && resultCode == RESULT_OK ) {
            mImageView.setImageBitmap(BitmapFactory.decodeFile(mLastPhotoURI.getPath()));
        }
    }
    
  7. 你可以准备在设备或模拟器上运行应用程序了。

它是如何工作的...

使用默认相机应用程序有两个部分。第一部分是设置意图来启动应用程序。我们使用 MediaStore.ACTION_IMAGE_CAPTURE 创建 Intent,表示我们想要一个拍照应用。我们通过检查 resolveActivity() 的结果来验证默认应用是否存在。只要它不是 null,我们就知道有一个应用程序可以处理这个意图。(否则,我们的应用会崩溃。)我们创建一个文件名,并将其添加到意图中:putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI)

当我们在 onActivityResult() 中得到回调时,我们首先确保它是 PHOTO_RESULTRESULT_OK(用户可能已取消),然后在 ImageView 中加载照片。

还有更多...

如果你不在意图片存储在哪里,可以在不使用 MediaStore.EXTRA_OUTPUT 额外参数的情况下调用意图。如果你没有指定输出文件,onActivityResult() 将在 data 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 takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);

你可以在 onActivityResult() 中获取到视频的 URI,如下所示:

Uri videoUri = intent.getData();

另请参阅

  • 第九章《图形和动画》中的缩小大图像以避免内存溢出异常食谱。

使用(旧的)Camera API 拍照

之前的食谱演示了如何使用意图调用默认照片应用程序。如果你只需要快速拍照,意图可能是理想的解决方案。如果不是,并且你需要更多控制相机,这个食谱将向你展示如何直接使用 Camera API。

实际上有两个使用 Camera API 的食谱——一个是针对在 Android 1.0(API 1)中发布的原始 Camera API,另一个是 Camera2 API,在 Android 5.0(API 21)中发布。我们将介绍新旧 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(或更高)。当提示选择活动类型时,选择空活动

如何操作...

首先,打开 Android 清单文件并按照以下步骤操作:

  1. 添加以下两个权限:

    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
  2. 现在打开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"/>
    
  3. 打开MainActivity.java文件,并将MainActivity类声明修改为实现SurfaceTextureListener,如下所示:

    public class MainActivity extends AppCompatActivity
            implements TextureView.SurfaceTextureListener {
    
  4. MainActivity添加以下全局声明:

    @Deprecated
    private Camera mCamera;
    private TextureView mTextureView;
    
  5. 创建以下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();
            }
        }
    };
    
  6. 向现有的onCreate()回调添加以下代码:

    mTextureView = (TextureView)findViewById(R.id.textureView);
    mTextureView.setSurfaceTextureListener(this);
    
  7. 添加以下方法来实现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
    }
    
  8. 添加以下方法来处理按钮点击:

    public void takePicture(View view) {
        if (mCamera!=null) {
            mCamera.takePicture(null, null, pictureCallback);
        }
    }
    
  9. 在带有相机的设备或模拟器上运行应用程序。

工作原理...

首先要注意的是,在 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的表面准备就绪时,我们会收到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 拍照

现在我们已经了解了旧的 Camera API,是时候学习新的 Camera2 API 了。不幸的是,由于 API 的异步性质,它有点复杂。幸运的是,总体概念与之前的 Camera API 相同。

准备就绪

在 Android Studio 中创建一个新项目,命名为Camera2API。在Target Android Devices对话框中,选择Phone & Tablet选项,并将Minimum SDK设置为 API 21(或更高)。当提示选择Activity Type时,选择Empty Activity

如何操作...

你会看到,这个配方有很多代码。首先打开 Android Manifest 文件,并按照以下步骤操作:

  1. 添加以下两个权限:

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
  2. 现在,打开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"/>
    
  3. 现在,打开MainActivity.java文件,并在MainActivity类中添加以下全局变量:

    private CameraDevice mCameraDevice = null;
    private CaptureRequest.Builder mCaptureRequestBuilder = null;
    private CameraCaptureSession mCameraCaptureSession  = null;
    private TextureView mTextureView = null;
    private Size mPreviewSize = null;
    
  4. 添加以下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());
        }
    }
    
  5. 添加以下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) {}
    };
    
  6. 添加以下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();
        }
    };
    
  7. 添加以下CameraCaptureSession.StateCallback

    private CameraCaptureSession.StateCallback mPreviewStateCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(CameraCaptureSession session) {
            startPreview(session);
        }
    
        @Override
        public void onConfigureFailed(CameraCaptureSession session) {}
    };
    
  8. 在现有的onCreate()回调中添加以下代码:

    mTextureView = (TextureView) findViewById(R.id.textureView);
    mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
    
  9. 添加以下方法以覆盖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);
        }
    }
    
  10. 添加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();
        }
    }
    
  11. 添加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();
        }
    }
    
  12. 添加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);
    }
    
  13. 添加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();
        }
    }
    
  14. 在带有摄像头的设备或模拟器上运行应用程序。

工作原理...

由于我们在上一个配方中了解了 TextureView,我们可以跳转到新的 Camera2 API 信息。

尽管涉及更多类,但与旧的 Camera API 一样,有两个基本步骤:

  • 设置预览

  • 捕获图像

设置预览

下面是代码如何设置预览的概要:

  1. 首先,我们在onCreate()中使用setSurfaceTextureListener()方法设置TextureView.SurfaceTextureListener

  2. 当我们收到onSurfaceTextureAvailable()回调时,我们打开相机。

  3. 我们将我们的CameraDevice.StateCallback类传递给openCamera()方法,该方法最终调用onOpened()回调。

  4. onOpened()通过调用getSurfaceTexture()获取预览的表面,并通过调用createCaptureSession()将其传递给 CameraDevice。

  5. 最后,当调用CameraCaptureSession.StateCallback onConfigured()时,我们使用setRepeatingRequest()方法开始预览。

捕获图像

尽管takePicture()方法看起来可能是程序性的,但捕获图像也涉及到几个类,并且依赖于回调。以下是代码如何拍照的分解说明:

  1. 用户点击拍照按钮。

  2. 然后查询相机以找到最大的可用图像尺寸。

  3. 然后,创建一个ImageReader

  4. 然后,设置OnImageAvailableListener,并在onImageAvailable()回调中保存图像。

  5. 然后,创建CaptureRequest.Builder并包含ImageReader表面。

  6. 接下来,创建CameraCaptureSession.CaptureCallback,它定义了onCaptureCompleted()回调。当捕获完成时,它会重新启动预览。

  7. 然后,调用createCaptureSession()方法,创建一个CameraCaptureSession.StateCallback。在这里,会调用capture()方法,并传入之前创建的CameraCaptureSession.CaptureCallback

还有更多...

与之前的 Camera 示例一样,我们刚刚创建了基础代码以展示一个工作的摄像头应用程序。同样,还有改进的空间。首先,你应该处理设备方向,既要考虑预览,也要在保存图片时考虑。(请参阅上一个食谱中的链接。)另外,随着 Android 6.0(API 23)的推出,现在是一个很好的时机来开始使用新的权限模型。我们不应该只在openCamera()方法中检查异常,而应该检查所需的权限。

另请参阅

第十二章:电信、网络与互联网

本章将涵盖以下主题:

  • 如何拨打电话

  • 监控电话通话事件

  • 如何发送短信(文本消息)

  • 接收短信

  • 在你的应用中显示网页

  • 检查在线状态和连接类型

  • Volley 网络请求入门

  • 取消 Volley 请求

  • 使用 Volley 请求 JSON 响应

  • 使用 Volley 请求图像

  • 使用 Volley 的 NetworkImageView 和 ImageLoader

引言

我们将通过《如何拨打电话》一节开始本章,了解电话功能。探索了如何拨打电话之后,我们将通过《监控电话通话事件》了解如何监控电话通话。然后,我们将通过《如何发送短信》转到短信通讯,接着通过《接收短信》介绍接收短信。

我们将探索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方法。从打开 Android Manifest 开始,按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>
    
  2. 打开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"/>
    
  3. 添加此方法,检查你的应用是否被授予了CALL_PHONE权限:

    private boolean checkPermission(String permission) {
        int permissionCheck = ContextCompat.checkSelfPermission(
                this, permission);
        return (permissionCheck == PackageManager.PERMISSION_GRANTED);
    }
    
  4. 添加拨号的代码:

    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);
        }
    }
    
  5. 在你的设备上运行此操作之前,请确保将0123456789替换为有效的电话号码。

工作原理...

正如我们在引言中的代码所看到的,当调用默认拨号应用时,我们不需要任何权限。但如果我们想直接拨号,我们需要添加CALL_PHONE权限。从 Android 6.0 Marshmallow(API 23)开始,权限不再在安装时授予,因此,在尝试拨号之前,我们会检查应用是否拥有权限。

参见

  • 想获取更多信息,请参阅第十四章中的新的运行时权限模型食谱,准备应用上架 Play 商店

监控电话呼叫事件

在上一个食谱中,我们演示了如何拨打电话,既通过意图调用默认应用程序,也可以直接拨号,无需 UI。

如果你希望在通话结束时得到通知,这就有点复杂了,因为你需要监听电话事件并跟踪电话状态。在本食谱中,我们将演示如何创建一个PhoneStateListener来读取电话状态事件。

准备就绪

在 Android Studio 中创建一个新项目,命名为PhoneStateListener。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

尽管这不是必须的,但你可以使用上一个食谱来发起电话呼叫以查看事件。否则,使用默认拨号盘和/或观察来电事件。(下载文件中提供的示例代码包括上一个食谱,以便更容易查看事件。)

如何操作...

我们只需要在布局中添加一个TextView来显示事件信息。无论你是接着上一个食谱继续操作,还是开始一个新食谱,打开activity_main.xml文件,并按照以下步骤操作:

  1. 按照以下方式添加或修改TextView

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    
  2. 向 Android Manifest 中添加以下权限:

    <uses-permission android:name="android.permission.READ_PHONE_STATE">
    </uses-permission>
    
  3. 打开MainActivity.java,并在MainActivity类中添加以下PhoneStateListener类:

    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);
        }
    };
    
  4. 修改onCreate()以设置监听器:

    final TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
    telephonyManager.listen(mPhoneStateListener,PhoneStateListener.LISTEN_CALL_STATE);
    
  5. 在设备上运行应用程序,并发起和/或接收电话以查看事件。

工作原理...

为了演示如何使用监听器,我们在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);

参见

如何发送短信(文本消息)

由于你可能已经熟悉短信(或文本消息),我们不会花时间解释它们是什么或为什么重要。(如果你不熟悉短信或想了解更多信息,请查看本示例中另请参阅部分提供的链接。)本示例将演示如何发送短信。(下一个示例将演示如何接收新消息的通知以及如何读取现有消息。)

准备就绪

在 Android Studio 中创建一个新项目,命名为SendSMS。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

如何操作...

首先,我们将添加发送短信所需的权限。然后,我们将创建一个带有Phone NumberMessage字段以及Send按钮的布局。点击发送按钮时,我们将创建并发送短信。以下是步骤:

  1. 打开 Android Manifest 并添加以下权限:

    <uses-permission android:name="android.permission.SEND_SMS"/>
    
  2. 打开activity_main.xml,用以下 XML 替换现有的TextView

    <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"/>
    
  3. 打开MainActivity.java并添加以下全局变量:

    final int SEND_SMS_PERMISSION_REQUEST_CODE=1;
    Button mButtonSend;
    
  4. 在现有的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);
    }
    
  5. 添加以下方法以检查权限:

    private boolean checkPermission(String permission) {
        int permissionCheck = ContextCompat.checkSelfPermission(this,permission);
        return (permissionCheck == PackageManager.PERMISSION_GRANTED);
    }
    
  6. 重写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;
            }
        }
    }
    
  7. 最后,添加实际发送短信的方法:

    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();
        }
    }
    
  8. 你已经准备好在设备或模拟器上运行应用程序了。(向另一个模拟器发送时,使用模拟器设备号,如 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)

你可以包含一个待定意图,以便在发送状态和/或投递状态时得到通知。收到你的待定意图后,它将包含一个结果代码,如果发送成功,则为 Activity.RESULT_OK,或者如 SMSManager 文档(在以下另请参阅部分提到的链接)中定义的错误代码:

  • RESULT_ERROR_GENERIC_FAILURE:一般失败原因

  • RESULT_ERROR_NO_SERVICE:由于服务当前不可用而失败

  • RESULT_ERROR_NULL_PDU:由于没有提供 PDU 而失败

  • RESULT_ERROR_RADIO_OFF:由于无线电被明确关闭而失败

另请参阅

接收短信消息

本教程将演示如何设置一个广播接收器,以便在接收到新短信时通知你。值得注意的是,你的应用无需运行即可接收短信意图。Android 会启动你的服务来处理短信。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 ReceiveSMS。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity

如何操作...

在本演示中,我们将不使用布局,因为所有工作都将在广播接收器中进行。我们将使用 Toast 来显示传入的短信。打开 Android 清单文件并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.RECEIVE_SMS" />
    
  2. <application> 元素添加以下广播接收器的声明:

    <receiver android:name=".SMSBroadcastReceiver">
        <intent-filter>
            <action android:name="android.provider.Telephony.SMS_RECEIVED">
            </action>
        </intent-filter>
    </receiver>
    
  3. 打开 MainActivity.java 并添加以下方法:

    private boolean checkPermission(String permission) {
        int permissionCheck = ContextCompat.checkSelfPermission(
                this, permission);
        return (permissionCheck == PackageManager.PERMISSION_GRANTED);
    }
    
  4. 修改现有的 onCreate() 回调以检查权限:

    if (!checkPermission(Manifest.permission.RECEIVE_SMS)) {
        ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.RECEIVE_SMS}, 0);
    }
    
  5. 使用以下代码向项目中添加一个名为 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();
                    }
                }
            }
        }
    }
    
  6. 现在你可以在设备或模拟器上运行应用程序了。

工作原理...

就像在前一个关于发送短信的教程中一样,我们首先需要检查应用是否有权限。(在 Android 6.0 之前的设备上,清单声明将自动提供权限,但对于棉花糖及以后的版本,我们需要像这里一样提示用户。)

如你所见,广播接收器接收新短信的通知。我们使用此代码在 Android 清单文件中告诉系统我们希望接收新的短信接收广播:

<receiver android:name=".SMSBroadcastReceiver">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED"></action>
    </intent-filter>
</receiver>

通知通过标准的 onRecieve() 回调传入,因此我们使用此代码检查动作:

if (SMS_RECEIVED.equals(intent.getAction())) {}

这可能是本演示中最复杂的代码行:

messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);

基本上,它调用 SmsMessage 库从 PDU 创建一个 SMSMessage 对象。(PDU,即协议数据单元,是短信的二进制数据格式。)如果你不熟悉 PDU 格式,你不需要了解。SmsMessage 库会为你处理并返回一个 SMSMessage 对象。

提示

如果你的应用没有收到短信广播消息,可能是其他应用阻止了你的应用。你可以尝试像这样增加 intent-filter 中的优先级值,或者禁用/卸载其他应用:

<intent-filter android:priority="100">
    <action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>

还有更多...

本指南演示了接收到短信时如何显示短信,但如何读取现有消息呢?

阅读现有短信

首先,要读取现有消息,你需要以下权限:

<uses-permission android:name="android.permission.READ_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");
}

在撰写本文时,短信内容提供者有超过 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 的一部分,可能会在未经通知的情况下更改。

另请参阅

在应用中显示网页

当你想要在网页上显示 HTML 内容时,你有两个选择:调用默认浏览器或在你的应用内显示。如果你只想调用默认浏览器,可以使用如下 Intent:

Uri uri = Uri.parse("https://www.packtpub.com/");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);

如果你需要在自己的应用内显示内容,可以使用 WebView。本指南将展示如何在你的应用中显示网页,如图所示:

在应用中显示网页

准备工作

在 Android Studio 中创建一个新项目,命名为 WebView。选择默认的手机和平板选项,并在提示活动类型时选择空活动

如何操作...

我们将通过代码创建 WebView,因此不会修改布局。我们将从打开 Android Manifest 开始,并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    
  2. 修改现有的 onCreate() 以包含以下代码:

    WebView webview = new WebView(this);
    setContentView(webview);
    webview.loadUrl("https://www.packtpub.com/");
    
  3. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

我们创建一个 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

我们可以通过 WebViewWebSettings 进行许多其他自定义设置。如果你想启用 JavaScript,获取 WebViewWebSettings 并调用 setJavaScriptEnabled(),如下所示:

WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);

启用内置缩放

另一个 webSetting 选项是 setBuiltInZoomControls()。从前面的代码继续,只需添加:

webSettings.setBuiltInZoomControls(true);

在下一节中查看 webSetting 链接,以获取大量附加选项。

另请参阅

检查在线状态和连接类型

这是一个简单的食谱,但非常常见,可能会包含在你构建的每个互联网应用程序中:检查在线状态。在检查在线状态时,我们还可以检查连接类型:WIFI 或 MOBILE。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为 isOnline。在选择 Activity Type 时,使用默认的 Phone & Tablet 选项,并选择 Empty Activity

如何操作...

首先,我们需要添加必要的权限来访问网络。然后,我们将创建一个带有 ButtonTextView 的简单布局。要开始,请打开 Android Manifest 文件,并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
  2. 打开 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"/>
    
  3. 添加以下方法来报告连接状态:

    private boolean isOnline() {
        ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }
    
  4. 添加以下方法来处理按钮点击:

    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");
        }
    }
    
  5. 你现在可以在设备或模拟器上运行应用程序了。

工作原理...

我们创建了 isOnline() 方法,以方便重用此代码。

为了检查状态,我们获取 ConnectivityManager 的实例来读取 NetworkInfo 的状态。如果它报告我们已连接,我们可以通过调用 getType() 来获取活动网络的名字,这将返回以下常量之一:

  • TYPE_MOBILE

  • TYPE_WIFI

  • TYPE_WIMAX

  • TYPE_ETHERNET

  • TYPE_BLUETOOTH

另外,请参阅后面的ConnectivityManager链接,了解其他常量。为了显示目的,我们调用getTypeName()。我们也可以调用getType()来获取数值常量。

还有更多...

让我们看看ConnectivityManager的一些其他常量。

监控网络状态变化

如果你的应用需要响应网络状态的变化,请查看ConnectivityManager中的CONNECTIVITY_ACTION。你需要创建一个广播接收器,然后注册该事件。以下是如何通过 Android 清单在接收器的意图过滤器中包含动作的示例:

<receiver android:name="com.vcs.timetrac.VCSBroadcastReceiver">
    <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
    </intent-filter>
</receiver>

使用 Android 清单时要小心,因为它会在每次网络状态变化时通知你的应用,即使你的应用没有被使用。这可能会导致不必要的电池消耗。如果你的应用只需要在用户实际使用你的应用时响应网络变化,请在代码中创建监听器。

另请参阅

开始使用 Volley 进行网络请求

Android 提供了多个用于互联网查询的库,包括 Apache 的HttpClientHttpURLConnection。在 Android 2.3 Gingerbread(API 9)之前,Apache HttpClient是推荐的库。在 Android 2.3 Gingerbread(API 9)中,对HttpURLConnection库进行了许多改进,使其成为了推荐的库,至今仍然如此。随着 Android 6.0 的发布,Apache HttpClient已完全从 SDK 中移除,HttpURLConnection库成为了推荐的替代品。

尽管HttpURLConnection库仍然可用并有其用途,但也有其缺点:如果你是初次编写网络请求,它不是最易于使用的库,并且需要编写很多重复的样板代码。幸运的是,来自 Google Play 团队的谷歌开发者Ficus Kirkpatrick提供了一个新选择。他发布了一个名为 Volley 的库,该库提供了一个简化的封装器。(默认使用HttpURLConnection库,也可以与其他库一起使用。)

注意

你可以在这里观看他的 Google I/O 演讲:

观看此视频

使用 Volley 而不是HttpURLConnection的几个原因包括以下:

  • 线程池(默认为四个线程)

  • 透明磁盘缓存

  • 队列优先级设置

还有其他的好处,但这三点就足以让你想要了解 Volley。第四个好处,如果你曾经使用过HttpURLConnection,就会很明显,那就是它减少了模板代码。你不需要围绕许多调用编写一堆标准的try/catch代码,库将在内部处理检查,让你更专注于手头的具体任务。

Volley 内置支持以下请求类型:

  • 字符串

  • JSON

  • 图像

  • 自定义

虽然 Volley 擅长处理多个小型请求调用(例如在ListView中滚动时),但不适合大文件下载,因为返回的对象是在内存中解析的。对于大文件下载,请查看DownloadManager(请参考食谱末尾的链接)。同样,由于这个原因,它也不适合流式内容;对于流式内容,请参考HttpURLConnection

由于 Volley 目前不在 Android SDK 中,我们需要下载代码并将其添加到我们的项目中。这个食谱将指导你完成将 Volley 添加到你的应用程序项目并发出一个简单请求的步骤。

准备就绪

在创建你的新项目之前,使用以下 Git 命令从Android Open Source ProjectAOSP)网站下载 Volley 项目文件:

git clone https://android.googlesource.com/platform/frameworks/volley

如果你不太熟悉 Git,请查看本食谱末尾的 Git(软件)链接,获取更多信息以及帮助你找到适合你平台的 Git 客户端。Git 是一种在许多平台上使用的版本控制软件VCS)。(安装后,你还可以在 Android Studio 中集成 Git VCS。)

在 Android Studio 中创建一个新项目,并将其命名为SetupVolley。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

如何操作...

在开始以下步骤之前,请确保你已经按照先前的描述下载了 Volley 项目。下面我们将从将 Volley 添加到我们的项目开始,进行一个简单的互联网调用。我们将在布局中使用一个按钮来发起请求,并使用TextView来显示结果。以下是步骤:

  1. 打开 Android Manifest 并添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    
  2. 通过选择File | New | Import Module导入Volley模块(请参考以下截图),并按照向导操作。如何操作...

  3. New Module导入向导的第二页(请参考以下截图),你需要指定 Volley 文件的位置并分配Module name。这是下一步我们需要用到的名称:如何操作...

  4. Gradle Scripts部分,打开build.gradle (Module: app)文件。请参考以下截图:如何操作...

  5. dependencies部分添加/验证以下声明:

    compile project(":Volley")
    

    注意

    括号内的值需要与你上一步指定的模块名称相匹配。

  6. Gradle Scripts下,打开settings.gradle文件,并按以下内容验证:

    include ':app', ':Volley'
    
  7. 打开activity_main.xml文件,用以下TextViewButton元素替换现有的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_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"/>
    
  8. 添加由按钮点击调用的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);
    }
    
  9. 您已经准备好在设备或模拟器上运行应用程序。

工作原理...

了解在 Volley 中,网络事务被称为请求可能很有帮助。要执行请求,请将其添加到队列中。为此,我们首先创建一个 Volley RequestQueue的实例,然后创建一个StringRequest并将其添加到队列中。StringRequest顾名思义;我们请求的是字符串响应。

对于这个食谱,我们只需调用 Packt Publishing 网站,并将页面作为字符串响应获取。由于这只是为了说明,我们只显示前 500 个字符。

还有更多...

现在您已经正确设置了 Volley 并进行了网络请求,这个食谱将是后续 Volley 食谱的基础。

另请参阅

取消 Volley 请求

在上一个食谱中,我们演示了如何将请求添加到 Volley 队列中。如果您不再需要响应会怎样?这可能发生在用户通过ListView滚动时,您通过从网上获取信息来更新ListItems。如果允许请求完成,知道您将丢弃响应,这将是带宽、电力和 CPU 周期的浪费。

如果您之前使用的是HTTPURLConnection库,您需要手动跟踪并取消所有请求。这个食谱将向您展示如何在 Volley 中轻松取消请求。

准备工作

如果您还没有完成之前的食谱,即开始使用 Volley 进行网络请求,您需要按照步骤 1-5 将 Volley 模块添加到您的应用程序中。

在 Android Studio 中创建一个新项目,并将其命名为CancelVolleyRequest。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

如何操作...

如果您还没有将 Volley 模块添加到您的应用程序中,请回顾之前的章节。在您的项目中添加了 Volley 之后,请按照以下步骤操作:

  1. 打开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_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"/>
    
  2. 打开MainActivity.java并添加以下全局变量:

    RequestQueue mRequestQueue;
    
  3. 编辑现有的onCreate()以初始化RequestQueue

    mRequestQueue = Volley.newRequestQueue(this);
    
  4. 添加以下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();
    }
    
  5. 添加关闭按钮的onClick方法:

    public void close(View view){
        finish();
    }
    
  6. onStop()回调创建以下覆盖方法:

    @Override
    protected void onStop() {
        super.onStop();
        mRequestQueue.cancelAll(this);
    }
    
  7. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

要取消请求,我们可以调用RequestQueuecancelAll()方法,并传入我们的标签。在这个例子中,我们使用活动this作为我们的标签,但我们可以使用任何对象作为标签。这允许你为请求创建所需的任何分组。

还有更多...

我们不仅仅是在展示如何轻松取消请求,同时也在演示一种防御性编程策略。通过确保取消所有请求,我们无需在响应中添加检查 null 活动的代码,因为 Volley 保证在请求被取消后,我们将不会收到任何响应。

使用 Volley 请求 JSON 响应。

由于 JavaScript Object Notation(JSON)可能是最常见的数据交换格式,你可能需要调用一个 JSON 网络服务。(如果你不熟悉 JSON,请查看本食谱末尾的链接。)本食谱将演示如何使用 Volley 进行 JSON 请求。

准备工作。

在 Android Studio 中创建一个新项目,名为JSONRequest。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

本食谱将使用如Volley 入门进行网络请求中所述的 Volley 设置。按照步骤 1-5 将 Volley 添加到你的新项目中。

如何操作...

按照前面的描述将 Volley 添加到你的项目中,然后执行以下步骤:

  1. 打开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_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"/>
    
  2. 添加以下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);
    }
    
  3. 在运行此应用程序之前,请替换代码中的url字符串。

工作原理...

使用JsonObjectRequest()请求 JSON 响应基本上与StringRequest()相同。不同之处在于响应,它返回一个JSONObject

要运行此代码,你需要将url参数替换为你的网络服务 URL。如果你没有可测试的网络服务,你可以尝试来自 JSON Test 网站(www.jsontest.com/)的链接。

还有更多...

在前面的示例中,我们使用JsonObjectRequest请求了JSONObject。我们还可以使用JsonArrayRequest请求JSONARray

另请参阅。

使用 Volley 请求图像。

一旦你按照前一个菜谱中的演示进行了 JSON 请求,接下来最可能进行的调用就是获取一个图片。本示例将演示如何请求一个图片来更新一个 ImageView

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 ImageRequest。使用默认的 手机 & 平板 选项,并在提示选择 活动类型 时选择 空活动

本示例将使用在 Volley 网络请求入门 示例中描述的设置。按照步骤 1-5 在你的新项目中添加 Volley。

如何操作...

按照之前的描述将 Volley 添加到你的项目中,然后遵循以下步骤:

  1. 打开 activity_main.xml 并用以下 XML 替换现有的 TextView

    <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"/>
    
  2. 添加以下 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);
    }
    
  3. 在设备或模拟器上运行应用程序。

工作原理...

本示例基本上与前两个 Volley 请求的方式相同。在这个示例中,我们传递一个图片的 URL 并在响应中加载 ImageView

我们现在已经涵盖了三种基本请求类型:String,JSON 和 Image。

还有更多内容...

尽管基本类型可能覆盖了你的大部分需求,但 Volley 是可扩展的,你也可以通过扩展 Request<T> 来实现自定义响应。

本示例演示了我们的示例代码中存在的问题。如果你改变设备的方向,你会看到活动重新创建时图像闪烁。

创建一个 Volley 单例

建议将 Volley 实例化为单例。(另一种方法是在应用类中创建队列。)要在 Android Studio 中创建单例类,请转到 新建 | 文件 | 单例 并为其提供一个类名,例如 VolleySingleton

将创建请求队列的代码移动到单例类中。如果你按照如下方式创建一个方法:

public <T> void addToRequestQueue(Request<T> req) {
    mRequestQueue.add(req);
}

然后,你可以使用以下代码从任何地方向你的队列中添加请求:

VolleySingleton.getInstance(this).addToRequestQueue(stringRequest);

正确实现这一点的关键在于始终通过在传入的上下文中调用 getApplicationContext() 来使用应用上下文(而不是活动或广播接收器的上下文)。

另请参阅

使用 Volley 的 NetworkImageView 和 ImageLoader

我们关于 Volley 的最后一个菜谱不是一个请求本身,而是对 ImageView 的替换。请求图片来填充 ImageView 是如此常见的任务;Volley 将这一功能组合到一个名为 NetworkImageView 的新视图中。本示例将演示如何使用 NetworkImageView

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 NetworkImageView。使用默认的 手机 & 平板 选项,并在提示选择 活动类型 时选择 空活动

本示例将使用在 Volley 网络请求入门 示例中描述的设置。按照步骤 1-5 在你的新项目中添加 Volley。

如何操作...

按照之前描述的方式将 Volley 添加到你的项目中,然后执行以下步骤:

  1. 打开 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" />
    
  2. 将以下代码添加到现有的 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);
    
  3. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

这个示例与之前的 Volley 示例非常不同。我们没有创建请求对象,而是创建了一个 ImageLoaderImageLoader 类允许我们覆盖默认的缓存行为,比如位图的数量或大小计算方式(我们可以将缓存改为基于总内存而不是图片数量)。有关更多信息,请参阅后面的 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());

另请参阅

第十三章:获取位置和使用地理围栏

在本章中,我们将涵盖以下主题:

  • 如何获取最后一次位置

  • 解决使用 GoogleApiClient OnConnectionFailedListener 报告的问题

  • 如何接收位置更新

  • 创建并监控地理围栏

引言

位置感知为应用带来了许多好处,实际上,好处如此之多,以至于现在连桌面应用也尝试获取用户的位置信息。位置信息的用途包括逐向导航、"查找最近"的应用程序、基于位置的提醒,以及现在甚至有了让你用设备去探索的基于位置的游戏。

Google API 提供了丰富的功能,用于创建具有位置感知的应用程序和地图功能。我们的第一个方法如何获取最后一次位置将查看设备上存储的最后一次已知位置。如果你的应用不是位置密集型的,这可能提供了一种获取用户位置而不需要大量资源开销的理想方式。如果你需要持续更新,那么请转向如何接收位置更新的方法。尽管持续的位置更新需要更多资源,但用户可能会理解你为他们提供逐向导航时的情况。如果你请求位置更新是为了邻近位置,请查看使用地理围栏选项,在创建和监控地理围栏的方法中。

本章中的所有方法都使用 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。使用默认的手机 & 平板选项,在选择活动类型时,选择空活动

如何操作...

首先,我们将在 Android Manifest 中添加必要的权限,然后创建一个带有ButtonTextView元素的布局。最后,我们将创建一个GoogleAPIClient API 来访问最后一次位置。打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    
  2. Gradle Scripts部分下,打开build.gradle (Module: app)文件,如下截图所示:如何操作...

  3. dependencies部分添加以下声明:

    compile 'com.google.android.gms:play-services:8.4.0'
    
  4. 打开activity_main.xml文件,用以下 XML 替换现有的TextView

    <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"/>
    
  5. 打开MainActivity.java文件,并添加以下全局变量:

    GoogleApiClient mGoogleApiClient;
    TextView mTextView;
    Button mButton;
    
  6. 添加ConnectionCallbacks的类:

    GoogleApiClient.ConnectionCallbacks mConnectionCallbacks = new GoogleApiClient.ConnectionCallbacks() {
        @Override
        public void onConnected(Bundle bundle) {
            mButton.setEnabled(true);
        }
        @Override
        public void onConnectionSuspended(int i) {}
    };
    
  7. 添加处理OnConnectionFailedListener回调的类:

    GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() {
        @Override
        public void onConnectionFailed(ConnectionResult connectionResult) {
            Toast.makeText(MainActivity.this, connectionResult.toString(), Toast.LENGTH_LONG).show();
        }
    };
    
  8. 将以下代码添加到现有的onCreate()方法中:

    mTextView = (TextView) findViewById(R.id.textView);
    mButton = (Button) findViewById(R.id.button);
    mButton.setEnabled(false);
    setupGoogleApiClient();
    
  9. 添加设置GoogleAPIClient的方法:

    protected synchronized void setupGoogleApiClient() {
        mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addConnectionCallbacks(mConnectionCallbacks)
            .addOnConnectionFailedListener(mOnConnectionFailedListener)
            .addApi(LocationServices.API)
            .build();
        mGoogleApiClient.connect();
    }
    
  10. 为按钮点击添加以下方法:

    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();}
    }
    
  11. 您已准备好在设备或模拟器上运行应用程序。

工作原理...

在我们调用getLastLocation()方法之前,需要设置GoogleApiClient。我们在setupGoogleApiClient()方法中调用GoogleApiClient.Builder方法,然后连接到库。当库准备就绪时,它会调用我们的ConnectionCallbacks.onConnected()方法。出于演示目的,这里是我们启用按钮的地方。(在后续的食谱中,我们将使用此回调启动附加功能。)

我们使用了按钮来显示我们可以按需调用getLastLocation();这不是一次性的调用。系统负责更新位置,并在重复调用时可能返回相同的最后位置。(这可以在时间戳中看到——它是位置时间戳,而不是按下按钮时的时间戳。)

这种按需调用位置的方法在您只需要在应用程序中发生某些事情时获取位置(例如对对象进行地理编码)的情况下可能很有用。由于系统负责位置更新,因此您的应用程序不会因为位置更新而导致电池耗尽。

我们收到的位置对象的精确度基于我们的权限设置。我们使用了ACCESS_COARSE_LOCATION,但如果我们想要更高的精确度,可以改为请求ACCESS_FINE_LOCATION,并使用以下权限:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

最后,为了使代码专注于GoogleApiClient,我们只需用SecurityException包装getLastLocation()。在生产应用程序中,您应该检查并请求上一章显示的权限。(请参阅新的运行时权限模型。)

还有更多...

如果在连接到GoogleApiClient时出现问题,将调用OnConnectionFailedListener。在这个例子中,我们显示了一个 Toast。下一个食谱,解决 GoogleApiClient OnConnectionFailedListener 报告的问题,将展示一种更健壮的方式来处理这种情况。

测试位置可能是一个挑战,因为在测试和调试时实际上移动设备是困难的。幸运的是,我们有能力用模拟器模拟 GPS 数据。(也可以在物理设备上创建模拟位置,但这并不容易。)

模拟位置

有三种方法可以在模拟器中模拟位置:

  • Android Studio

  • DDMS

  • 通过 Telnet 的Geo命令

若要在 Android Studio 中设置模拟位置,请按照以下步骤操作:

  1. 导航至工具 | Android | Android Device Monitor

  2. 在设备窗口中选择Emulator Control标签页。

  3. Location Controls下输入 GPS 坐标。

下面是一个显示Location Controls的截图:

模拟位置

提示

请注意,模拟位置是通过发送 GPS 数据来实现的。因此,为了让你的应用接收模拟位置,它需要接收 GPS 数据。测试lastLocation()可能不会发送模拟 GPS 数据,因为它并不完全依赖 GPS 来确定设备位置。尝试使用教程如何接收位置更新设置模拟位置,在那里我们可以请求优先级。(我们无法强制系统使用任何特定的位置传感器,我们只能提出请求。系统会选择最佳方案来提供结果。)

另请参阅

解决使用 GoogleApiClient OnConnectionFailedListener 报告的问题

鉴于 Google API 的不断变化,你的用户可能会尝试使用你的应用程序,但由于文件过时而无法使用。在之前的示例中,我们只是显示了一个 Toast,但我们还可以做得更好。我们可以使用GoogleApiAvailability库来显示一个对话框,帮助用户解决问题。

我们将继续之前的教程,并在onConnectionFailed()回调中添加代码。我们将使用错误结果向用户显示更多信息,以解决他们的问题。

准备工作

本教程将从之前的教程如何获取最后的位置继续。如果你是从下载的源文件中加载项目,它被称为HandleGoogleAPIError

如何操作...

由于我们是从上一个教程继续,我们只涵盖更新之前代码所需的步骤。打开ActivityMain.java并按照以下步骤操作:

  1. 在全局类变量中添加以下行:

    private final int REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR=1;
    boolean mResolvingError;
    
  2. 添加以下方法以显示 Google API 错误对话框:

    private void showGoogleAPIErrorDialog(int errorCode) {
      GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance();
      Dialog errorDialog = googleApiAvailability.getErrorDialog(this, errorCode, REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR);
      errorDialog.show();
    }
    
  3. 添加以下代码以覆盖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();
        }
      }
    }
    
  4. 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());
    }
    
  5. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

与之前使用 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");

另请参阅

如何接收位置更新

如果你的应用需要频繁的位置更新,你的应用可以请求周期性的更新。本示例将使用 GoogleApiClientrequestLocationUpdates() 方法来演示这一点。

准备工作

在 Android Studio 中创建一个新项目,命名为 LocationUpdates。使用默认的 Phone & Tablet 选项,并在提示选择 Activity Type 时选择 Empty Activity

如何操作...

由于我们将从系统接收更新,因此这个示例不需要按钮。我们的布局将只包含 TextView 以查看位置数据。打开 Android 清单文件并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    
  2. 打开 build.gradle (Module: app) 文件,并在 dependencies 部分添加以下声明:

    compile 'com.google.android.gms:play-services:8.4.0'
    
  3. 打开 activity_main.xml 文件,并用以下 XML 替换现有的 TextView

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    
  4. 打开 MainActivity.java 文件,并添加以下全局变量:

    GoogleApiClient mGoogleApiClient;
    LocationRequest mLocationRequest;
    TextView mTextView;
    
  5. 创建以下 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());
            }
        }
    };
    
  6. 创建一个 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) {}
    };
    
  7. 创建一个 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());
        }
    };
    
  8. 在现有的 onCreate() 回调中添加以下代码:

    mTextView = (TextView) findViewById(R.id.textView);
    setupLocationRequest();
    
  9. 创建 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();
    }
    
  10. 你可以准备在设备或模拟器上运行应用程序了。

工作原理...

本示例与 如何获取最后位置 的示例相似,因为我们需要像之前一样设置 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);

通常,当您的应用程序不再处于前台时,您可能希望关闭更新,但这取决于您的具体应用程序需求。如果您的应用程序需要持续更新,可能更希望创建一个后台服务来处理回调。

另请参阅

创建并监控一个地理围栏

如果您的应用程序需要知道用户进入某个特定位置的时间,有一种替代方法可以避免不断检查用户位置:地理围栏。地理围栏是一个带有半径的地理位置(纬度和经度)。您可以创建一个地理围栏,当用户进入您指定的位置范围时,系统会通知您。(Android 目前允许每个用户最多设置 100 个地理围栏。)

地理围栏属性包括:

  • 位置:经度和纬度

  • 半径:圆的大小(以米为单位)

  • 逗留延迟:用户在发送通知前可以在半径内停留的时间

  • 过期时间:地理围栏自动过期的时长

  • 过渡 类型:以下列出了这些类型:

    • GEOFENCE_TRANSITION_ENTER

    • GEOFENCE_TRANSITION_EXIT

    • INITIAL_TRIGGER_DWELL

本指南将向您展示如何创建一个地理围栏对象,并使用它来创建一个GeofencingRequest实例。

准备就绪

在 Android Studio 中创建一个新项目,命名为Geofence。使用默认的手机 & 平板选项,在选择活动类型时,选择空活动

如何操作...

由于我们将使用提示信息和通知与用户互动,因此本指南不需要布局。我们需要为IntentService创建一个额外的 Java 类,用于处理地理围栏警报。打开 Android Manifest 文件,按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    
  2. 打开文件build.gradle (Module: app)并在dependencies部分添加以下声明:

    compile 'com.google.android.gms:play-services:8.4.0'
    
  3. 创建一个名为GeofenceIntentService的新 Java 类并继承IntentService类。声明如下所示:

    public class GeofenceIntentService extends IntentService {
    
  4. 添加以下构造函数:

    public GeofenceIntentService() {
        super("GeofenceIntentService");
    }
    
  5. 添加onHandleIntent()以接收 Geofence 警告:

    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();
        }
    }
    
  6. 添加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());
    }
    
  7. 打开 Android 清单并在与<activity>元素同一级别的<application>元素内添加以下内容:

    <service android:name=".GeofenceIntentService"/>
    
  8. 打开MainActivity.java并添加以下全局变量:

    private final int MINIMUM_RECOMENDED_RADIUS=100;
    GoogleApiClient mGoogleApiClient;
    PendingIntent mGeofencePendingIntent;
    
  9. 创建以下ResultCallback类:

    ResultCallback mResultCallback = new ResultCallback() {
        @Override
        public void onResult(Result result) {
            Log.i("onResult()", "result: " + result.getStatus().toString());
        }
    };
    
  10. 创建一个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) {}
    };
    
  11. 创建一个OnConnectionFailedListener类:

    GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() {
        @Override
        public void onConnectionFailed(ConnectionResult connectionResult) {
            Log.i("onConnectionFailed()", "connectionResult: " +connectionResult.toString());
        }
    };
    
  12. 在现有的onCreate()回调中添加以下代码:

      setupGoogleApiClient();
    
  13. 添加设置GoogleAPIClient的方法:

    protected synchronized void setupGoogleApiClient() {
        mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addConnectionCallbacks(mConnectionCallbacks)
            .addOnConnectionFailedListener(mOnConnectionFailedListener)
            .addApi(LocationServices.API)
            .build();
        mGoogleApiClient.connect();
    }
    
  14. 创建setupGoogleApiClient()方法:

    protected synchronized void setupGoogleApiClient() {
        mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addConnectionCallbacks(mConnectionCallbacks)
            .addOnConnectionFailedListener(mOnConnectionFailedListener)
            .addApi(LocationServices.API)
            .build();
        mGoogleApiClient.connect();
    }
    
  15. 使用以下方法创建一个待定意图:

    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);
    }
    
  16. 创建一个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;
    }
    
  17. 按如下方式创建createGeofencingRequest()方法:

    private GeofencingRequest createGeofencingRequest() {
        GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
        builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL);
        builder.addGeofences(createGeofenceList());
        return builder.build();
    }
    
  18. 你现在可以在设备或模拟器上运行应用程序了。

工作原理...

首先,我们添加ACCESS_FINE_LOCATION权限,因为这是 Geofencing 所必需的。我们像在以前的食谱中一样设置GoogleApiClient,并等待onConnected()被调用以设置GeofencingApi

在我们可以调用GeofencingApi.addGeofences()方法之前,我们必须准备三个对象:

  • GoogleApiClient

  • Geofence 请求

  • 待定意图

我们已经创建了GoogleApiClient,我们将其保存在mGoogleApiClient中。

要创建 Geofence 请求,我们使用GeofencingRequest.Builder。构建器需要 Geofence 对象列表,这些对象在createGeofenceList()方法中创建。(即使我们只创建了一个 Geofence 对象,构建器也需要一个列表,所以我们只需将单个 Geofence 添加到ArrayList中。)以下是设置 Geofence 属性的地方:

.setRequestId("GeofenceLocation")
.setCircularRegion(
        37.422006, //Latitude
        -122.084095, //Longitude
        MINIMUM_RECOMENDED_RADIUS)
.setLoiteringDelay(30000)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL)

只有 Loitering 延迟是可选的,但我们需要它,因为我们使用了DWELL转换。在调用setTransitionTypes()时,我们可以使用管道显示的OR运算符组合多个转换类型。以下是使用ENTEREXIT的示例:

.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)

在这个例子中,我们使用了与模拟器相同的默认纬度和经度。根据需要更改这些值。

我们调用Geofence.Builder()创建 Geofence 对象。准备好 Geofence 列表后,我们调用GeofencingRequest.Builder并将我们的初始触发器设置为INITIAL_TRIGGER_DWELL。(如果你更改前面的转换类型,你可能还需要更改初始触发器。)

我们需要的最后一个对象是 Pending Intent,这是系统在满足 Geofence 条件时通知我们应用的途径。我们创建了 GeofenceIntentService 来处理 Geofence 意图,通过向用户发送通知。(有关通知的更多信息,请参阅第七章中的 使用通知的灯光、动作和声音 Redux 配方,警报和通知。)

创建了所有三个对象后,我们只需调用 LocationServices.GeofencingApi.addGeofences() 并等待通知的到来。

还有更多...

若要停止接收 Geofence 通知,可以调用 removeGeofences() 方法,并使用 RequestID 参数或 PendingIntent。以下示例使用了与通知相同的 PendingIntent 方法:

LocationServices.GeofencingApi.removeGeofences(
    mGoogleApiClient,
    getGeofencePendingIntent()
).setResultCallback(mResultCallback);

另请参阅

第十四章:为 Play 商店准备你的应用

在本章中,我们将涵盖以下主题:

  • 新的 Android 6.0 运行时权限模型

  • 如何安排闹钟

  • 接收设备启动通知

  • 使用 AsyncTask 进行后台工作

  • 将语音识别添加到你的应用

  • 使用 Google 云消息推送通知

  • 如何将谷歌登录添加到你的应用

引言

当我们接近这本书的结尾时,是时候在发布到 Play 商店之前为你的应用添加最后的润色了。本章的食谱涵盖了可能决定用户保留还是卸载你的应用的主题。

我们的第一部分,新的 Android 6.0 运行时权限模型,无疑是一个重要的话题,可能是 Android 从版本 5.x 升级到版本 6 的主要原因!对 Android 权限模型的更改已经被请求了一段时间,所以这个新模型是一个受欢迎的改变,至少对用户来说是这样。

接下来,我们将看看在 Android 中使用闹钟。闹钟的主要优点之一是操作系统负责维护闹钟,即使你的应用没有运行。由于闹钟在设备重启后不会保留,我们还将看看如何在接收设备启动通知中检测设备重启,以便你可以重新创建你的闹钟。

几乎任何严肃的 Android 应用都需要一种方法在主线程之外执行可能阻塞的任务。否则,你的应用可能会被视为反应迟钝,或者更糟,完全无响应。AsyncTask旨在使创建后台工作线程更容易,我们将在使用 AsyncTask 进行后台工作这一部分中演示。

如果你希望你的应用能够从免提输入或语音识别中受益,请查看一下将语音识别添加到你的应用这一部分,我们将探讨谷歌语音 API。

与你的用户沟通的最有趣的功能之一可能是推送通知或谷歌所谓的Google Cloud Messaging (GCM)。使用 Google 云消息推送通知这一部分将指导你将 GCM 添加到你的应用程序,并解释更大的图景。

最后,我们将在如何将谷歌登录添加到你的应用这一部分结束本章,展示如何让你的应用更加舒适并鼓励用户登录。

新的 Android 6.0 运行时权限模型

旧的安全模型是 Android 中许多人的痛点。评论中经常看到对应用所需权限的评论是很常见的。有时,权限是过分的(比如一个手电筒应用需要网络权限),但其他时候,开发者请求某些权限是有充分理由的。主要问题是这是一个全有或全无的前景。

这最终在 Android 6 Marshmallow(API 23)版本中得到了改变。新的权限模型仍然像以前一样在清单中声明权限,但用户可以选择性地接受或拒绝每个权限。用户甚至可以撤销之前授予的权限。

尽管这对于许多人是受欢迎的改变;然而,对于开发者来说,这可能会破坏之前正常工作的代码。我们之前讨论过这个权限变化,因为它具有深远的影响。这个菜谱将汇总所有信息,以便在您自己的应用中实施此更改时作为单一参考点。

需要记住的一个重要点是,这个变化只影响 Android 6.0(API 23)及以上版本的用户。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为 RuntimePermission。对于 Activity 类型,使用默认的 Phone & Tablet 选项并选择 Empty Activity

示例源代码将最低 API 设置为 23,但这并不是必须的。如果您的 compileSdkVersion 是 API 23 或以上,编译器将针对新的安全模型标记您的代码。

如何操作...

我们需要先在清单中添加我们所需的权限,然后我们将添加一个按钮来调用我们的检查权限代码。打开 Android 清单并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.SEND_SMS"/>
    
  2. 打开 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"/>
    
  3. 打开 MainActivity.java 并向类中添加以下常量:

    private final int REQUEST_PERMISSION_SEND_SMS=1;
    
  4. 添加此方法来检查权限:

    private boolean checkPermission(String permission) {
        int permissionCheck = ContextCompat.checkSelfPermission(
                this, permission);
        return (permissionCheck == PackageManager.PERMISSION_GRANTED);
    }
    
  5. 添加此方法以显示解释对话框:

    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();
    }
    
  6. 添加此方法来请求权限:

    private void requestPermission(String permissionName, int permissionRequestCode) {
        ActivityCompat.requestPermissions(this, new String[]{permissionName}, permissionRequestCode);
    }
    
  7. 添加按钮点击的方法:

    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();
        }
    }
    
  8. 如下重写 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;
            }
        }
    }
    
  9. 现在,你可以在设备或模拟器上运行应用程序了。

它是如何工作的...

使用新的运行时权限模型涉及以下内容:

  1. 检查你是否拥有所需的权限。

  2. 如果没有,检查我们是否应该显示理由(意味着,之前的请求被拒绝了)。

  3. 请求权限;只有操作系统可以显示权限请求。

  4. 处理请求响应。

以下是相应的方法:

  • ContextCompat.checkSelfPermission

  • ActivityCompat.requestPermissions

  • ActivityCompat.shouldShowRequestPermissionRationale

  • onRequestPermissionsResult

    注意

    尽管你在运行时请求权限,但所需的权限必须在 Android 清单中列出。如果未指定权限,操作系统将自动拒绝请求。

还有更多...

你可以通过 ADB 使用以下命令来授权/撤销权限:

adb shell pm [grant|revoke] <package> <permission-name>

这是一个示例,为我们的测试应用授予 SEND_SMS 权限:

adb shell pm grant com.packtpub.androidcookbook.runtimepermissions android.permission.SEND_SMS

另请参阅

如何计划一个闹钟

Android 提供了 AlarmManager 来创建和计划闹钟。闹钟提供以下功能:

  • 计划在设定的时间或间隔触发报警

  • 由操作系统维护,而不是您的应用程序,因此即使您的应用程序没有运行,或者设备在休眠,也会触发报警

  • 可用于触发周期性任务(例如每小时新闻更新),即使应用程序没有运行

  • 您的应用程序不使用资源(如计时器或后台服务),因为操作系统管理调度

如果您在应用程序运行时只需要一个简单的延迟,例如,一个 UI 事件的短暂延迟,报警并不是最佳解决方案。对于短暂延迟,使用处理程序更容易、更高效,正如我们在之前的几个食谱中所做的那样。

使用报警时,请记住以下最佳实践:

  • 尽可能使用不频繁的报警时间

  • 避免唤醒设备

  • 尽可能使用不精确的时间——时间越精确,所需的资源越多

  • 避免基于时钟时间设置报警(例如 12:00);如果可能,添加随机调整以避免服务器拥堵(尤其是在检查新内容,如天气或新闻时尤为重要)

报警有三个属性,如下所示:

  • 报警类型(见以下列表)

  • 触发时间(如果时间已经过去,则立即触发报警)

  • 待定意图

重复报警具有相同的三个属性,以及一个间隔:

  • 报警类型(见以下列表)

  • 触发时间(如果时间已经过去,则立即触发)

  • 间隔

  • 待定意图

有四种报警类型:

  • RTC实时时钟):这是基于墙钟时间。它不会唤醒设备。

  • RTC_WAKEUP:这是基于墙钟时间。如果设备在休眠,它会唤醒设备。

  • ELAPSED_REALTIME:这是基于设备自启动以来的时间。它不会唤醒设备。

  • ELAPSED_REALTIME_WAKEUP:这是基于设备自启动以来的时间。如果设备在休眠,它会唤醒设备。

Elapsed Real Time 更适合时间间隔报警——例如每 30 分钟。

提示

设备重启后,报警不会保留。设备关闭时,所有报警都会被取消,因此,在设备启动时重置报警是您应用程序的责任。(更多信息请参见接收设备启动通知。)

以下食谱将演示如何使用 AlarmManager 创建报警。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为:Alarms。选择默认的手机和平板选项,并在提示活动类型时选择空活动

如何操作...

设置报警需要一个待定意图,当触发报警时,Android 会发送该意图。因此,我们需要设置一个广播接收器来捕获报警意图。我们的用户界面将仅包含一个简单按钮来设置报警。首先,打开 Android 清单文件并按照以下步骤操作:

  1. 在与现有 <activity> 元素同一级别的 <application> 元素中添加以下 <receiver>

    <receiver android:name=".AlarmBroadcastReceiver">
        <intent-filter>
            <action android:name="com.packtpub.androidcookbook.alarms.ACTION_ALARM" />
        </intent-filter>
    </receiver>
    
  2. 打开 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"/>
    
  3. 使用以下代码创建一个名为 AlarmBroadcastReceiver 的新 Java 类:

    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();
            }
        }
    }
    
  4. 打开 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);
    }
    
  5. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

创建闹钟是通过以下这行代码完成的:

alarmManager.set(AlarmManager.ELAPSED_REALTIME, thirtyMinutes, alarmIntent);

这是方法的签名:

set(AlarmType, Time, PendingIntent);

注意

在 Android 4.4 KitKat(API 19)之前,这是请求确切时间的方法。从 Android 4.4 开始,出于效率考虑,这被视为一个非确切时间,但不会在请求的时间之前传递意图。(如果你需要确切时间,请参考下面的 setExact() 方法。)

为了设置闹钟,我们创建了一个带有之前定义的闹钟动作的待定意图:

public static final String ACTION_ALARM="com.packtpub.androidcookbook.alarms.ACTION_ALARM";

(这是一个任意字符串,可以是任何我们想要的内容,但它需要是唯一的,因此我们在前面加上我们的包名。)我们在广播接收器的 onReceive() 回调中检查这个动作。

还有更多...

如果你点击 设置闹钟 按钮,等待三十分钟,当闹钟触发时你会看到 Toast。如果你在第一个闹钟触发之前就迫不及待地再次点击 设置闹钟 按钮,你不会得到两个闹钟。相反,操作系统将用新的闹钟替换第一个闹钟,因为它们都使用相同的待定意图。(如果你需要多个闹钟,你需要创建不同的待定意图,比如使用不同的动作。)

取消闹钟

如果你想要取消闹钟,通过传递用于创建闹钟的相同待定意图来调用 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

另请参阅

接收设备启动通知

安卓在其生命周期中发送许多意图。最早发送的意图之一是 ACTION_BOOT_COMPLETED。如果你的应用程序需要知道设备何时启动,你需要捕获这个意图。

本指南将引导你完成在设备启动时接收通知所需的步骤。

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为 DeviceBoot。在选择 Activity Type 时,使用默认的Phone & Tablet选项并选择Empty Activity

如何操作...

首先,打开 Android Manifest 文件并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    
  2. <application> 元素中添加以下 <receiver>,与现有的 <activity> 元素同一级别:

    <receiver android:name=".BootBroadcastReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED"/>
        </intent-filter>
    </receiver>
    
  3. 使用以下代码创建一个名为 BootBroadcastReceiver 的新 Java 类:

    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();
            }
        }
    }
    
  4. 重启设备以查看提示消息。

工作原理...

当设备启动时,Android 会发送BOOT_COMPLETED意图。只要我们的应用程序有接收意图的权限,我们就会在广播接收器中收到通知。

要实现这一功能,需要考虑以下三个方面:

  • RECEIVE_BOOT_COMPLETED的权限

  • BOOT_COMPLETED添加到接收意图过滤器中

  • 在广播接收器中检查BOOT_COMPLETED动作

显然,你希望用你自己的代码替换提示消息,比如重新创建你可能需要的任何闹钟。

还有更多...

如果你按照前面的步骤操作,那么你已经有一个广播接收器了。不需要为每个动作分别创建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
    }
}

另请参阅

使用 AsyncTask 进行后台工作

在整本书中,我们提到了不要阻塞主线程的重要性。在主线程上执行长时间运行的操作可能会导致你的应用程序看起来反应迟钝,甚至挂起。如果你的应用程序在约 5 秒内没有响应,系统可能会显示应用程序无响应ANR)对话框,并给出终止你应用程序的选项。(这是你要避免的事情,因为这会导致你的应用程序被卸载。)

Android 应用程序使用单线程模型,有两个简单的规则,如下:

  • 不要阻塞主线程

  • 所有 UI 操作应该在主线程上执行

当 Android 启动你的应用程序时,它会自动创建主线程(或 UI 线程)。所有 UI 操作都必须从这条线程中调用。第一条规则是“不要阻塞主线程”。这意味着你需要为任何长时间运行或可能阻塞的任务创建一个后台线程或工作线程。这就是为什么所有基于网络的任务都应该在主线程之外执行。

Android 在处理后台线程时提供以下选项:

  • Activity.runOnUiThread()

  • View.post()

  • View.postDelayed()

  • Handler

  • AsyncTask

本教程将探讨AsyncTask类;由于它之前已经创建过,你无需直接使用 Handler 或 post 方法。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为:AsyncTask。选择默认的手机 & 平板选项,并在提示活动类型时选择空活动

如何操作...

这个示例我们只需要一个按钮。打开activity_main.xml并按照以下步骤操作:

  1. 使用以下按钮替换现有的 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" />
    
  2. 打开MainActivity.java并添加以下全局变量:

    Button mButtonStart;
    
  3. 添加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);
        }
    }
    
  4. onCreate()中添加以下代码以初始化按钮:

    mButtonStart=(Button)findViewById(R.id.buttonStart);
    
  5. 添加按钮点击的方法:

    public void start(View view){
        mButtonStart.setEnabled(false);
        new CountingTask().execute(10000000);
    }
    
  6. 你已经准备好在设备或模拟器上运行应用程序了。

它是如何工作的...

这是一个非常简单的AsyncTask示例,只是为了让它工作。从技术上讲,只有doInBackground()是必需的,但通常,你可能希望在它完成时通过onPostExecute()接收通知。

AsyncTask通过为doInBackground()方法创建一个工作线程来工作,然后在 UI 线程的onPostExecute()回调中响应。

注意我们是如何等到onPostExecute()被调用之后才进行任何 UI 操作,比如启用按钮。如果我们尝试在工作线程中修改 UI,它要么无法编译,要么会抛出运行时异常。你还应该注意,我们是如何在每个按钮点击时实例化一个新的CountingTask对象。这是因为AsyncTask只能执行一次。尝试再次调用 execute 将会抛出异常。

还有更多...

AsyncTask可以非常简单,但如果你需要,它仍然具有很多灵活性选项。当与 Activity 一起使用AsyncTask时,了解 Activity 是否被销毁和重新创建(如在屏幕方向改变时)或AsyncTask是否继续运行非常重要。这可能导致你的AsyncTask成为孤儿,并且可能对已销毁的活动做出响应(导致NullPointer异常)。因此,通常使用与 Fragment 一起的AysncTask(在屏幕旋转时不会销毁),或者使用 Loader 代替。(有关 Loader 的链接请参见下一节。)

参数类型

对于许多人来说,创建自己的类时AsyncTask最令人困惑的方面是参数。如果你看我们的类声明,AsyncTask有三个参数;它们定义如下:

AsyncTask<Params, Progress, Result >

这些参数是泛型类型,使用方法如下:

  • 参数: 这是调用doInBackground()的参数类型。

  • 进度: 这是发布更新的参数类型。

  • 结果: 这是发布结果的参数类型。

当你声明自己的类时,请将参数替换为你需要的变量类型。

这是AsyncTask的流程以及上述参数的使用方法:

  • onPreExecute(): 这在doInBackground()开始之前被调用。

  • doInBackground(Params): 这在后台线程中执行。

  • onProgressUpdate(Progress): 这在 UI 线程中响应工作线程中的publishProgress(Progress)调用。

  • onPostExecute(Result): 当工作线程完成时,在 UI 线程中调用。

取消任务

要取消任务,请按照以下方式在对象上调用 cancel 方法:

< AsyncTask>.cancel(true);

你需要拥有对象实例来访问cancel()方法。(在我们的上一个示例中,我们没有保存该对象。)在设置cancel(true)之后,在doInBackground()中调用isCancelled()将返回true,这样你就可以退出循环了。如果取消,将调用onCancelled()而不是onPostExecute()

另请参阅

在你的应用中添加语音识别

Android 2.2(API 8)在 Android 中引入了语音识别功能,并且几乎在每一个新的主要 Android 版本发布时都会进行改进。本教程将演示如何使用谷歌语音服务在你的应用中添加语音识别功能。

准备工作

在 Android Studio 中创建一个新项目,将其命名为SpeechRecognition。使用默认的手机 & 平板选项,在选择活动类型时选择空活动

如何操作...

我们首先在布局中添加一个“立即说话”(或麦克风)按钮,然后添加必要的代码来调用语音识别器。打开activity_main.xml并按照以下步骤操作:

  1. 使用以下 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"/>
    
  2. 定义REQUEST_SPEECH常量:

    private final int REQUEST_SPEECH=1;
    
  3. 在现有的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();
    }
    
  4. 添加按钮点击方法:

    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);
    }
    
  5. 添加以下代码以覆盖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");
                }
            }
        }
    }
    
  6. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

这里的工作由 Android 中包含的谷歌语音识别器完成。为了确保设备上可用该服务,我们在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 表示最低信心。

还有更多...

使用意图是获取语音识别的快速简便方法;然而,如果你不想使用默认的谷歌活动,可以直接调用SpeechRecognizer类。以下是实例化该类的一个例子:

SpeechRecognizer speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this);

你需要添加RECORD_AUDIO权限并实现RecognitionListener类来处理语音事件。(更多信息请参见以下链接。)

另请参阅

使用 GCM 的推送通知

GCM,谷歌版的推送通知,可以让你的应用程序接收消息。这个想法与短信类似,但更加灵活。GCM 有三个组成部分:

  • 你的服务器(这是你发起消息的地方)

  • 谷歌的 GCM 服务器

  • 安卓设备(尽管 GCM 也适用于其他平台)

当用户启动你的应用程序时,你的代码需要连接到 GCM 服务器并获取一个设备令牌,然后将该令牌发送到你的服务器。你的服务器负责发起消息并将其传递给 GCM 服务器。你的服务器需要跟踪在发起消息时需要发送的设备令牌。(你的服务器告诉 GCM 服务器需要发送哪些设备令牌。)

你可以实施自己的服务器,或者选择使用许多可用的服务之一。下一章,后端服务选项,将查看几个 BaaS 选项,其中许多也提供推送通知。(简单测试选项部分提供了一个选项来验证你的代码是否正常工作。)

本指南将带你通过使用当前(版本 8.3)的谷歌服务库添加 GCM 的步骤。在开始步骤之前,值得注意的是 GCM 支持回溯到 API 8,只要用户有谷歌账户即可。在安卓 4.0.4 之后,不再需要谷歌账户。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为GCM。使用默认的手机和平板选项,并在提示活动类型时选择空活动

GCM 使用谷歌服务插件,该插件需要从谷歌开发者控制台获取谷歌服务配置文件。要创建配置文件,你需要以下信息:

注意

如果你下载了源文件,按照前面的步骤操作时,你需要创建一个新的包名,因为现有的包名已经被注册了。

如何操作...

完成前面的准备工作后,按照以下步骤操作:

  1. 将你在准备工作部分下载的google-services.json文件复制到你的应用文件夹(<项目文件夹>`GCM\app`)。

  2. 打开项目 Gradle 构建文件:build.gradle (Project: GCM),并向buildscript dependencies部分添加以下内容:

    classpath 'com.google.gms:google-services:1.5.0-beta2'
    
  3. 打开 app 模块的 Gradle 构建文件:build.gradle (Module: app),并在文件开头(android部分之上)添加以下声明:

    apply plugin: 'com.google.gms.google-services'
    
  4. 在第 3 步的同一模块构建文件中,向依赖项部分添加以下声明:

    compile 'com.google.android.gms:play-services-auth:8.3.0'
    
  5. 打开 Android Manifest 文件,并添加以下权限:

    <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" />
    
  6. <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>
    
  7. 创建一个名为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();
            }
        }
    }
    
  8. 创建一个名为GCMInstanceService的新 Java 类,它扩展了InstanceIDListenerService,如下所示:

    public class GCMInstanceService extends InstanceIDListenerService {
        @Override
             public void onTokenRefresh() {
            Intent intent = new Intent(this, GCMRegistrationService.class);
            startService(intent);
        }
    }
    
  9. 创建一个名为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());
        }
    }
    
  10. 在现有的onCreate()回调中添加以下代码:

    Intent intent = new Intent(this, GCMRegistrationService.class);
    startService(intent);
    
  11. 你现在可以在设备或模拟器上运行应用程序了。

工作原理...

大部分实际的 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()回调。

还有更多...

为了便于输入,我们省略了一个重要的谷歌服务 API 验证,这个验证在生产应用中应当被包含。我们在前面的部分中的onCreate()直接调用了GCMRegistrationService,而不是首先检查 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 (推送通知) 测试器:更多信息请参考以下链接:

play.google.com/store/apps/details?id=com.eboyer.gcmtester

参阅

如何在应用中添加 Google 登录

Google 登录允许你的用户使用他们的 Google 凭据登录你的应用。本教程将指导你如何在应用中添加 Google 登录。以下是将在教程中创建的应用中显示的 Google 登录按钮的截图:

如何在应用中添加 Google 登录

准备工作

在 Android Studio 中创建一个新项目,命名为 GoogleSignIn。选择默认的手机 & 平板选项,并在提示活动类型时选择空活动

Google 登录使用 Google 服务插件,该插件需要一个 Google 服务配置文件,你可以在 Google 开发者控制台获取。要创建配置文件,你需要以下信息:

  • 您的应用程序包名

  • 你的签名证书的 SHA-1 哈希码(有关更多信息,请参阅教程末尾的验证客户端链接)

当你拥有这些信息后,登录此 Google 链接,并按照向导启用登录:

developers.google.com/mobile/add

注意

如果你正在下载源文件,按照前面的步骤操作时,你需要创建一个新的包名,因为现有的包名已经被注册。

如何操作...

完成前面的准备工作部分后,请遵循以下步骤:

  1. 将在准备工作部分下载的 google-services.json 文件复制到你的应用文件夹(<项目文件夹>\GoogleSignIn\app

  2. 打开项目 Gradle 构建文件:build.gradle (项目:GoogleSignIn),并在 buildscript dependencies 部分添加以下内容:

    classpath 'com.google.gms:google-services:1.5.0-beta2'
    
  3. 打开应用模块 Gradle 构建文件:build.gradle (模块:app),并在文件开头(android 部分之上)添加以下声明:

    apply plugin: 'com.google.gms.google-services'
    
  4. 在步骤 3 的同一模块构建文件中,将以下声明添加到依赖项部分:

    compile 'com.google.android.gms:play-services-auth:8.3.0'
    
  5. 打开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" />
    
  6. 打开MainActivity.java并添加以下全局声明:

    private final int REQUEST_SIGN_IN=1;
    GoogleApiClient mGoogleApiClient;
    
  7. 添加以下OnConnectionFailedListener

    GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = new GoogleApiClient.OnConnectionFailedListener() {
        @Override
        public void onConnectionFailed(ConnectionResult connectionResult) {
            Toast.makeText(MainActivity.this, "connectionResult="+connectionResult.getErrorMessage(), Toast.LENGTH_SHORT).show();
        }
    };
    
  8. 在现有的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();
        }
    });
    
  9. 按照以下步骤为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);
            }
        }
    }
    
  10. 现在你可以将应用程序运行在设备或模拟器上了。

工作原理...

Google 使用他们的GoogleApiClientGoogleSignInOptions API 相对简单地添加了 Google 登录。首先,我们使用构建器创建一个GoogleSignInOptions对象。在这里,我们指定所需的登录选项,例如请求电子邮件 ID。然后,我们将其传递给GoogleApiClient构建器。

当用户点击使用com.google.android.gms.common.SignInButton类创建的 Google 登录按钮时,我们会向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/

另请参阅

第十五章:后端即服务选项

在本章中,我们将涵盖以下主题:

  • App42

  • Backendless

  • Buddy

  • Firebase

  • Kinvey

简介

当您的应用程序和用户基础增长时,您可能希望将应用程序跨设备甚至用户连接起来,例如高分排行榜。您有两个选择:

  • 创建和维护您自己的服务器

  • 使用后端即服务BaaS)提供商

作为移动开发者,创建和维护一个网站服务器是一个耗时的任务,可能会让您偏离开发工作。

备注

如果您不熟悉 BaaS 提供商,以下是一些背景信息:

维基百科——移动后端即服务:

en.wikipedia.org/wiki/Mobile_backend_as_a_service

我们将研究几个针对 Android 开发者的特定功能的 BaaS 提供商。仅包括提供原生 Android 支持和免费订阅的提供商。(仅提供免费试用或付费计划的提供商不包括在内。)当您的应用程序超出免费层级时,所有这些提供商都提供更高层级的服务,月费用各不相同。

下表提供了每个提供商每月免费服务的快速比较:

提供商 月用户数 API 调用 推送通知 文件存储
Firebase 无限制 100 SC N/A 1 GB
Buddy * 20/秒 500 万 10 GB
App42 * 每月 100 万 100 万 1 GB
Kinvey 1000 * * 30 GB
Backendless 100 50/秒 100 万 20 GB
  • = 他们的网站上没有发布

N/A = 功能不可用

SC = 同时连接数

备注

免责声明:前述表格和以下食谱的信息是从它们的公共网站获取的,可能会在它们的意愿下进行更改。如您所知,移动行业一直在变化;预计价格和服务会有所变动。请仅将此信息作为起点。

最后,这并不是一个详尽的 BaaS 提供商列表。希望这一章能提供一个关于 BaaS 能做什么以及如何为您的应用程序使用 BaaS 的好介绍。接下来的食谱将研究每个提供商,并带您了解将它们的库添加到您的项目的步骤。这将直接比较这些服务。正如您将看到的,一些服务比其他服务更容易使用,这可能是决定性因素。

App42

App42 是 ShepHertz 公司的 BaaS API 产品,提供包括游戏平台、平台即服务、营销分析在内的多种云服务。它们具有非常丰富的功能集,包括许多特别适用于游戏的服务。

App42 Android SDK 支持以下功能:

  • 用户服务

  • 存储服务

  • 自定义代码服务

  • 推送通知服务

  • 事件服务

  • 礼品管理服务

  • 定时服务

  • 社交服务

  • A/B 测试服务

  • Buddy 服务

  • 头像服务

  • 成就服务

  • 排行榜服务

  • 奖励服务

  • 上传服务

  • 图库服务

  • 地理服务

  • 会话服务

  • 评论服务

  • 购物车服务

  • 目录服务

  • 消息服务

  • 推荐服务

  • 邮件服务

  • 日志服务

注意

要注册 App42/ShepHertz,请访问以下链接:

apphq.shephertz.com/register

这是 App4 注册屏幕的截图:

App42

准备就绪

在 Android Studio 中创建一个名为 App42 的新项目。使用默认的 Phone & Tablet 选项,在选择 Activity Type 时选择 Empty Activity

从以下链接下载并解压 App42 SDK:

github.com/shephertz/App42_ANDROID_SDK/archive/master.zip

创建 App42 账户后(见前一个链接),登录到 AppHQ 管理控制台,并注册你的应用。你需要 ApiKey 和 SecretKey。

如何操作...

要向你的项目添加对 App42 的支持,首先打开 Android Manifest 文件,并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
  2. 在你的文件浏览器中打开以下文件夹:<project folder>\App42\app\libs(如果 libs 文件夹不存在,请创建它),并将 App42_ANDROID-CAMPAIGN_x.x.jar 文件复制到 app\libs 文件夹。

  3. 打开 app 模块的 Gradle 构建文件:build.gradle (Module: app),并在 dependencies 部分添加以下内容:

    compile files('libs/App42_ANDROID-CAMPAIGN_x.x.jar')
    
  4. 打开 ActivityMain.java 并添加以下导入:

    import com.shephertz.app42.paas.sdk.android.App42API;
    
  5. onCreate() 回调中添加以下代码:

    App42API.initialize(this, "YOUR_API_KEY", "YOUR_SECRET_KEY");
    
  6. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

不幸的是,App42 不支持 Gradle 构建格式,因此你需要下载 JAR 文件并将其手动复制到 \libs 文件夹。

在第 3 步中,将 App42_ANDROID-CAMPAIGN_x.x.jar 中的 x.x 替换为你下载文件中的当前版本号。

在第 5 步中,用你注册 App42 应用时收到的凭证替换 YOUR_API_KEYYOUR_SECRET_KEY

还有更多...

下面是使用 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());
    }
});

另请参阅

Backendless

除了MBaaS(即他们所说的移动后端即服务),Backendless 还提供其他服务,如托管、API 服务和市场。它们的 MBaaS 功能包括:

  • 用户管理

  • 数据持久化

  • 地理定位

  • 媒体流

  • 发布/订阅消息传递

  • 推送通知

  • 自定义业务逻辑

  • 分析

  • 移动代码生成

注意

要注册 Backendless,请点击此链接:

develop.backendless.com/#registration

这是 Backendless 注册窗口的截图:

Backendless

准备就绪

在 Android Studio 中创建一个新项目,命名为Backendless。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

你需要一个Backendless账户(见前一个链接),并通过他们的Backendless控制台注册你的应用程序。获取到你的 App ID 和 Secret Key 后,开始以下步骤。

如何操作...

要将Backendless添加到你的项目中,打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
  2. 打开 app 模块的 Gradle 构建文件:build.gradle (Module: app),并在dependencies部分添加以下内容:

    compile 'com.backendless:android:3.0.3'
    
  3. 打开ActivityMain.java并添加以下导入:

    import com.backendless.Backendless;
    
  4. onCreate()回调中添加以下代码:

    String appVersion = "v1";
    Backendless.initApp(this, YOUR_APP_ID, YOUR_SECRET_KEY, appVersion);
    
  5. 你准备好在设备或模拟器上运行应用程序。

工作原理...

在步骤 4 中,将YOUR_APP_IDYOUR_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");
    }
} );

另请参阅

Buddy

Buddy 与其他 BaaS 提供商略有不同,因为他们主要专注于连接设备和传感器。为了帮助维护隐私法规,Buddy 允许你选择将数据托管在美国或欧盟。

Buddy 支持以下常见场景:

  • 记录度量事件

  • 发送推送通知

  • 接收并安全存储遥测数据

  • 存储和管理二进制文件

  • 深入了解客户如何使用应用程序的移动分析

  • 将设备或应用程序数据与公司的 BI 系统整合

  • 在你选择的地理位置的沙盒私有数据。

如果你想要查看或贡献 Buddy SDK 的源代码,可以使用以下 Git 命令获取:

git clone https://github.com/BuddyPlatform/Buddy-Android-SDK.git

注意

要注册 Buddy,请点击以下链接:

www.buddyplatform.com/Signup

这是 Buddy 注册的截图:

Buddy

准备就绪

在 Android Studio 中创建一个新项目,命名为Buddy。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity

你需要一个 Buddy 账户(见前一个链接),并通过他们的仪表板注册你的应用程序。获取到你的 App ID 和 App Key 后,开始以下步骤。

如何操作...

要将 Buddy 添加到你的项目中,打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
  2. 打开 app 模块的 Gradle 构建文件:build.gradle (Module: app),并在dependencies部分添加以下内容:

    compile 'com.buddy:androidsdk:+'
    
  3. 打开ActivityMain.java并添加以下导入:

    import com.buddy.sdk.Buddy;
    
  4. onCreate()回调中添加以下代码:

    Buddy.init(myContext, "appId", "appKey");
    
  5. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

在第 4 步中,将appIdappKey替换为你在 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

由于它们最近被谷歌收购,你可以期待与谷歌云解决方案的进一步整合,正如你在这个链接上看到的:

cloud.google.com/solutions/mobile/firebase-app-engine-android-studio

注意

要使用 Firebase 注册,请访问此链接:

www.firebase.com/login/

这是 Firebase 注册窗口的截图:

Firebase

准备就绪

在 Android Studio 中创建一个新项目,并将其命名为Firebase。使用默认的Phone & Tablet选项,在选择Activity Type时选择Empty Activity

你需要在你用 Firebase 注册应用程序时提供的 Firebase URL。

如何操作...

要将 Firebase 添加到你的项目中,首先打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    
  2. 打开 app 模块的 Gradle 构建文件:build.gradle (Module: app),并在dependencies部分添加以下内容:

    compile 'com.firebase:firebase-client-android:2.5.0+'
    
  3. 打开ActivityMain.java并添加以下导入:

    import com.firebase.client.Firebase;
    
  4. onCreate()回调中添加以下代码:

    Firebase.setAndroidContext(this);
    Firebase firebase = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/");
    
  5. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

将 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
    }
});

另请参阅

Kinvey

Kinvey 是最早开始提供移动后端服务的提供商之一。他们的功能包括:

  • 用户管理

  • 数据存储

  • 文件存储

  • 推送通知

  • 社交网络集成

  • 位置服务

  • 生命周期管理

  • 版本控制

    注意

    console.kinvey.com/sign-up注册 Kinvey。

这是 Kinvey 注册窗口的截图:

Kinvey

准备工作

在 Android Studio 中创建一个新项目,并将其命名为Kinvey。使用默认的手机和平板选项,并在提示活动类型时选择空活动

从以下链接下载并解压 Kinvey SDK:download.kinvey.com/Android/kinvey-android-2.10.5.zip

你需要一个 Kinvey 账户(见前一个链接),并通过他们的开发者控制台注册你的应用程序。一旦你有 App Key 和 App Secret,就可以开始以下步骤。

如何操作...

要将 Kinvey 添加到你的项目中,请按照以下步骤操作:

  1. 在 Android Manifest 中添加以下权限:

    <uses-permission android:name="android.permission.INTERNET"/>
    
  2. 在你的文件浏览器中打开以下文件夹:<项目文件夹>\Kinvey\app\libs(如果libs文件夹不存在,请创建它),并将 SDK 的liblibJar文件夹中的所有文件复制到app\libs文件夹中。

  3. 打开 app 模块的 Gradle 构建文件:build.gradle (Module: app),并添加以下repositoriesdependencies(保留任何现有的条目):

    repositories {
        flatDir {
            dirs 'libs'
        }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile(name:'kinvey-android-*', ext:'aar')
    }
    
  4. 打开MainActivity.java并添加以下导入:

    import com.kinvey.android.Client;
    
  5. 在类声明中添加以下内容:

    final Client mKinveyClient = new mKinveyClient("your_app_key", "your_app_secret", this.getApplicationContext()).build();
    
  6. 你现在可以在设备或模拟器上运行应用程序了。

工作原理...

Kinvey 不是最容易设置的 BaaS,因为它不提供简单的 Gradle 依赖项。相反,你需要像在第 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");
    }
});

另请参阅

posted @ 2024-05-23 11:08  绝不原创的飞龙  阅读(5)  评论(0编辑  收藏  举报