安卓-9-应用开发秘籍-全-

安卓 9 应用开发秘籍(全)

原文:zh.annas-archive.org/md5/761e836d28b1f1fe745c316947a6f1fe

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

安卓系统首次于 2007 年发布,当时被谷歌公司收购。最初,安卓主要应用于手机。安卓 3.0 增加了功能,以利用不断增长的平板电脑市场。

2014 年,谷歌宣布安卓拥有超过 10 亿活跃用户!在谷歌玩应用商店上有超过 100 万的应用程序,现在是加入安卓社区最激动人心的时刻!

今年,2018 年,对安卓来说是一个重要的里程碑——自首款安卓手机发布以来已有 10 周年!因此,我们也迎来了一个新的操作系统版本发布——安卓派。在本版书中,我们将涵盖跨多个章节的几个新主题中为平台发布的特性,以及现有热门主题的更新,以涵盖 SDK 的变化。像往常一样,安卓平台一直在不断变化!

本书面向对象

本书假设读者对编程概念和安卓基础知识有基本的了解。或者,如果您是安卓的新手,并且通过直接编写代码来学习效果最好,本书提供了大量最常见任务的广泛范围。如果您是安卓的新手,可以从本书的开头开始,按照它们建立在前一知识之上的顺序来学习各个主题。

作为一本烹饪书,主题被设计为独立(有例外情况),以便您可以快速跳转到特定主题,并在自己的应用程序中尽快使代码生效。

本书涵盖内容

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

第二章,布局,虽然活动对 UI 至关重要,但布局实际上定义了用户在屏幕上看到的内容。了解可用的主要布局选项以及最佳使用案例。

第三章,视图、小部件和样式,探讨了基本 UI 对象,所有布局都是基于此构建的。本章首先探讨了视图和小部件——任何应用程序的基本构建块,然后继续讨论小部件的样式以及如何将这些样式转换为主题。

第四章,菜单和操作模式,教您如何在安卓中使用菜单。了解如何创建菜单以及如何在运行时控制它们的行为,包括操作模式。

第五章,片段,展示了如何通过重用 UI 组件来创建更灵活的用户界面。

第六章,主屏幕小部件、搜索和系统 UI,将我们带到应用程序之外的主题,例如如何为主屏幕创建小部件,向您的应用程序添加搜索功能 UI,以及以全屏模式运行您的应用程序。

第七章,数据存储,比较了 Android 提供的多种持久化数据的方法,以及何时使用每个选项最佳。

第八章,警报和通知,展示了向用户显示通知的多种选项。选项包括在您的应用中显示警报、使用系统通知和“抬头显示通知”。

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

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

第十一章,OpenGL ES 初探,当您需要高性能的 2D 和 3D 图形时,转向开放图形库。Android 支持 OpenGL,这是一个跨平台的图形 API。

第十二章,多媒体 - 声音和摄像头,利用硬件功能播放音频。使用 Android 意图调用默认的摄像头应用或深入摄像头 API 直接控制摄像头。

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

第十四章,位置和地理围栏的使用,展示了如何确定用户的位置以及最佳实践,以确保您的应用不会耗尽电池。使用新的位置 API 接收位置更新并创建地理围栏。

第十五章,为 Play 商店准备您的应用,当您为 Play 商店打磨您的应用时,学习如何实现更高级的功能,例如闹钟、后台处理的 AsynchTask 以及将 Google Sign-In 添加到您的应用中。

第十六章,Kotlin 入门,为您提供了对这款新 Android 语言的初步了解以及一些入门主题。

为了充分利用这本书

  1. 您应该了解基本的编程基础。本书假设读者理解基本的编程语法和概念。如if/thenfor nexttry/catch等语言特性应该已经熟悉并理解。

  2. 下载并安装官方的 Android 开发环境 - Android Studio。有关详细信息,请参阅硬件-软件列表部分。

下载示例代码文件

您可以从www.packt.com上的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

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

  1. www.packt.com 登录或注册。

  2. 选择 SUPPORT 选项卡。

  3. 点击代码下载与勘误表。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

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

  • Windows 上的 WinRAR/7-Zip

  • Mac 上的 Zipeg/iZip/UnRarX

  • Linux 上的 7-Zip/PeaZip

本书代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Android-9-Development-Cookbook。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还从我们丰富的图书和视频目录中提供了其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781788991216_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”

代码块设置如下:

<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>

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

警告或重要注意事项看起来像这样。

小贴士和技巧看起来像这样。

部分

在本书中,您将找到一些经常出现的标题(准备就绪如何操作...它是如何工作的...还有更多...也看看)。

为了清楚地说明如何完成食谱,请按照以下方式使用这些部分:

准备就绪

本节告诉您在食谱中可以期待什么,并描述了如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

它是如何工作的…

本节通常包含对上一节发生情况的详细解释。

更多内容…

本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。

参见

本节提供了其他有用的信息链接,以帮助您了解食谱。

联系我们

我们欢迎读者的反馈。

一般反馈: 如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并给我们发送邮件至 customercare@packtpub.com

勘误: 尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packt.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版: 如果您在互联网上发现我们作品的任何非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至 copyright@packt.com 与我们联系。

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com.

评论

请留下评论。一旦您阅读并使用了这本书,为何不在您购买书籍的网站上留下评论呢?潜在读者可以看到并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问 packt.com

第一章:活动

本章涵盖了以下食谱:

  • 声明一个活动

  • 使用意图对象启动新的活动

  • 在活动之间切换

  • 将数据传递给另一个活动

  • 从活动返回结果

  • 保存活动的状态

  • 存储持久活动数据

  • 理解活动生命周期

简介

Android SDK 提供了一个强大的工具来编程移动设备,而掌握这样一个工具的最佳方式就是直接上手。虽然您可以从头到尾阅读这本书,因为它是一本食谱书,但它特别设计成允许您跳转到特定的任务并立即获得结果。

活动是大多数 Android 应用程序的基本构建块,因为活动类提供了应用程序和屏幕之间的接口。大多数 Android 应用程序至少将有一个活动,如果不是几个(但不是必需的)。一个没有用户界面的后台服务应用程序可能不需要活动。

本章解释了如何在应用程序中 声明启动 活动,以及如何通过在它们之间共享数据、从它们请求结果以及在另一个活动内部调用它们来同时管理多个活动。

本章还简要探讨了 意图 对象,该对象通常与活动一起使用。意图可以用来在您的应用程序中的活动之间以及在外部应用程序(例如 Android 操作系统包含的应用程序)之间传输数据(一个常见的例子是使用意图启动默认的网页浏览器)。

要开始开发 Android 应用程序,请转到 Android Studio 页面下载新的 Android Studio IDE 和 Android SDK 套件:

developer.android.com/sdk/index.html.

声明一个活动

活动和其他应用程序组件(如 服务)在 AndroidManifest.xml 文件中声明。声明活动节点是我们告诉操作系统关于我们的 Activity 类以及如何请求它的方式。例如,应用程序通常会指示至少有一个活动应作为桌面图标可见,并作为应用程序的主要入口点。

准备工作

Android Studio,现在版本为 3.2,用于本书中展示的所有代码示例。如果您尚未安装它,请访问 Android Studio 网站(参见前面的提示)以安装 IDE 和 SDK 套件。

如何做到...

对于这个第一个示例,我们将引导您创建一个新的项目。Android Studio 提供了一个快速入门向导,这使得整个过程变得极其简单。按照以下步骤开始:

  1. 启动 Android Studio,将弹出“欢迎使用 Android Studio”对话框:

  1. 点击“启动新的 Android Studio 项目”选项。

  2. 输入应用程序名称;在这个例子中,我们使用了 DeclareAnActivity。点击“下一步”:

图片

  1. 在“目标 Android 设备”对话框中,您可以保留默认的“手机”和“平板电脑”复选框,并选择最低 SDK 的默认 API 21:Android 5.0(Lollipop)。点击“下一步”:

图片

  1. 在“添加活动到移动设备”对话框中,选择“空活动”选项。点击“下一步”:

图片

  1. 在“配置活动”对话框中,您可以保留默认值,但请注意默认活动名称是 MainActivity。点击“完成”:

图片

完成向导后,Android Studio 将创建项目文件。对于这个示例,我们将检查的两个文件是 MainActivity.java(对应于第 6 步中提到的活动名称)和 AndroidManifest.xml

如果您查看 MainActivity.java 文件,您会意识到它非常基础。这是因为我们选择了“空活动”选项(在第 5 步中)。现在,查看 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 资源文件中定义的实际文本(在这种情况下为 DeclareAnActivity)。

它是如何工作的...

声明活动是一个简单的过程,只需声明 <activity> 元素并使用 android:name 属性指定活动类的名称。通过将 <activity> 元素添加到 AndroidManifest,我们指定了将此组件包含到我们的应用程序中的意图。任何未在清单中声明的活动(或任何其他组件)将不可用给应用程序。尝试访问或使用未声明的组件将在运行时抛出异常。

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

要查看可用的活动属性完整列表,请参阅此资源:

Android 开发者指南 - Activity 元素.

使用意图对象启动新活动

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 将在视图和意图上给出以下警告:无法解析符号'Intent'。

  • 这意味着您需要将库引用添加到项目中。您可以通过在import部分输入以下代码手动完成此操作:

        import android.content.Intent;
        import android.net.Uri;
        import android.support.v7.app.AppCompatActivity;
        import android.os.Bundle;
        import android.view.View;

或者,让 Android Studio 为您添加库引用:只需单击用红色字体突出显示的代码并按Alt+Enter

  1. 打开activity_main.xml文件并将<TextView />块替换为以下 XML:
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Launch Browser"
    android:id="@+id/button"
    android:onClick="launchIntent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

  1. 现在,是时候运行应用程序并看到意图的作用了。您需要创建一个 Android 模拟器(在 Android Studio 中,转到工具 | Android | AVDManager)或将物理设备连接到您的计算机。

  2. 当您点击启动浏览器按钮时,您将看到默认的网页浏览器打开并显示指定的 URL。

它是如何工作的...

虽然简单,但这个应用展示了 Android 操作系统背后的强大功能。意图是一个消息对象。意图可以用来在您的应用程序组件(如服务和广播接收器)之间以及与设备上的其他应用程序之间进行通信。在这个菜谱中,我们要求操作系统启动任何可以处理我们使用setData()方法指定的数据的应用程序。(如果用户安装了多个浏览器但没有默认设置,操作系统将显示一个应用程序列表供用户选择。)

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

在这个菜谱中,我们创建了一个意图对象,使用ACTION_VIEW作为我们想要执行的操作(我们的意图)。您可能已经注意到,当您输入Intent和点号时,Android Studio 提供了一个弹出列表的可能选项(这是自动完成功能),如下所示:

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

更多内容...

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

参见

要从菜单选择开始一个活动,请参考第四章中的处理菜单选择配方,菜单和动作模式

在活动之间切换

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

准备工作

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

如何做到这一点...

  1. 由于 Android Studio 新建项目向导已经创建了第一个活动,我们只需创建第二个活动。打开 ActivitySwitcher 项目,导航到文件 | 新建 | 活动 | 空活动,如图所示:

图片

  1. 在“新建 Android 活动”对话框中,您可以保留默认的活动名称不变,或者将其更改为SecondActivity,如下所示:

图片

  1. 打开MainActivity.java文件,并添加以下函数:
    public void onClickSwitchActivity(View view) { 
        Intent intent = new Intent(this, SecondActivity.class); 
        startActivity(intent); 
    }
  1. 现在,打开位于res/layout文件夹中的activity_main.xml文件,并用以下 XML 替换<TextView />以创建按钮:
    <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 Second Activity"
        android:onClick="onClickSwitchActivity"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
  1. 您现在可以运行代码并看到第二个活动打开。我们将进一步添加一个按钮到SecondActivity以关闭它,这将带我们回到第一个活动。打开SecondActivity.java文件并

    添加此功能:

    public void onClickClose(View view) { 
        finish(); 
    } 
  1. 最后,将关闭按钮添加到SecondActivity布局中。打开activity_second.xml文件,并将以下<Button>元素添加到自动生成的ConstraintLayout中:
    <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"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
  1. 在您的设备或模拟器上运行应用程序,并查看按钮的实际效果。

它是如何工作的...

这个练习的真正工作是在第 3 步中的 onClickSwitchActivity() 方法。这是我们使用 SecondActivity.class 声明第二个活动的地方。我们更进一步,向第二个活动添加了关闭按钮,以展示一个常见的现实世界情况:启动一个新的活动,然后返回到原始调用活动。这种行为是在 onClickClose() 函数中实现的。它所做的只是调用 finish(),但这告诉操作系统我们已经完成了这个活动。finish() 并不会实际上带我们回到调用活动(或任何特定的活动);它只是关闭当前活动,并依赖于应用程序的 返回栈 来显示最后一个活动。如果我们想要特定的活动,我们还可以再次使用 Intent 对象,并在创建 Intent 时指定活动类名。

这种活动切换并不会使应用程序变得非常吸引人。我们的活动什么也不做,只是演示了如何从一个活动切换到另一个活动,这当然将是几乎所有我们开发的应用程序的基本方面之一。

如果我们手动创建了活动,我们需要将它们添加到清单中。使用“新建 Android 活动”向导将自动将必要的元素添加到 Android Manifest 文件中。要查看 Android Studio 为你做了什么,请打开 AndroidManifest.xml 文件并查看 <application> 元素:

<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=".SecondActivity"></activity>

在前面自动生成的代码中需要注意的一点是,第二个活动没有 <intent-filter> 元素。主活动通常是启动应用程序时的入口点。这就是为什么定义了 MAINLAUNCHER,以便系统知道在应用程序启动时启动哪个活动。

参见

  • 要了解更多关于嵌入小部件(如按钮)的信息,请访问第二章,视图、小部件和样式

将数据传递给另一个活动

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

准备工作

这个菜谱将从上一个菜谱结束的地方继续。我们将把这个项目命名为 SendData

如何做...

由于这个菜谱是在上一个菜谱的基础上构建的,所以大部分工作已经完成。我们将向主活动添加一个 EditText 元素,以便我们有东西可以发送到 SecondActivity。我们将使用(自动生成的)TextView 视图来显示消息。以下是完全的步骤:

  1. 打开 activity_main.xml 并在按钮上方添加以下 <EditText> 元素:
<EditText
    android:id="@+id/editTextData"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@+id/button" />

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

  1. 现在,打开 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); 
}
  1. 接下来,打开 activity_second.xml 文件并添加以下 <TextView> 元素:
<TextView
    android:id="@+id/textViewText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@id/buttonClose"/>
  1. 最后一个更改是编辑第二个活动以查找新数据并在屏幕上显示它。打开 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));
    }
}
  1. 现在,运行项目。在主活动中输入一些文本,然后按“启动第二个活动”以查看它发送数据。

它是如何工作的...

如预期的那样,Intent 对象正在做所有的工作。我们创建了一个意图,就像在先前的食谱中一样,然后添加了一些额外数据。您注意到 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"; 
  1. 接下来,通过修改 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);
}
  1. 然后,添加此新方法以接收结果:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode==RESULT_OK) {
        Toast.makeText(this, Integer.toString(data.getIntExtra(REQUEST_RESULT, 
                0)), Toast.LENGTH_LONG).show();
    }
}
  1. 最后,修改 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() 返回一个结果。在这个例子中,我们只是使用一个静态值设置结果。我们使用一个简单的 Toast 将结果显示给用户。

检查结果码是一个好习惯,以确保用户没有取消操作。技术上它是一个整数,但系统将其用作布尔值。检查RESULT_OKRESULT_CANCEL并根据情况相应处理。在我们的例子中,第二个活动没有取消按钮,为什么还要检查?如果用户点击返回按钮怎么办?Android 会将结果码设置为RESULT_CANCEL并将 intent 设置为 null,如果我们尝试访问 null 结果,这会导致我们的代码抛出异常。

我们使用了Toast对象,它显示一个方便的弹出消息来不引人注目地通知用户。它还作为一个方便的调试方法,因为它不需要特殊的布局或屏幕空间。

还有更多...

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

startActivityForResult(Intent intent, int requestCode); 

我们没有检查请求码,因为我们知道我们只有一个结果要处理,但在具有多个活动的非平凡应用中,这个值可以用来识别哪个活动正在返回结果。

如果startActivityForResult()方法被调用时带有负请求码,它的行为将与使用startActivity()相同,即它不会返回结果。

参见

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

  • 更多关于 Toast 的信息,请参阅第八章的制作 Toast食谱,警报和通知

保存活动状态

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

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

简单地旋转设备将导致操作系统销毁并重新创建你的活动。这可能会显得有些过于强硬,但这样做是有好理由的:在纵向和横向布局不同的情况下非常常见,这确保了你的应用正在使用正确的资源。

在这个菜谱中,您将了解如何处理 onSaveInstanceState()onRestoreInstanceState() 回调以保存应用程序的状态。我们将通过创建一个计数变量并在每次计数时增加它来演示这一点。

当按钮被按下时,我们将还有一个 EditTextTextView 小部件来查看它们的默认行为。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 StateSaver。我们只需要一个活动,因此自动生成的主活动就足够了。但是,我们需要一些小部件,包括 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"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@+id/button"/>

<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"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"/>

<TextView
    android:id="@+id/textViewCounter"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/button"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/button"
    app:layout_constraintBottom_toBottomOf="parent"/>

如何做到这一点...

执行以下步骤:

  1. 为了跟踪计数器,我们需要向项目中添加一个全局变量,以及一个用于保存和恢复的键。将以下代码添加到 MainActivity.java 类中:
static final String KEY_COUNTER = "COUNTER"; 
private int mCounter=0; 
  1. 然后,添加处理按钮点击所需的代码;它增加计数器并在 TextView 小部件中显示结果:
public void onClickCounter(View view) {
    mCounter++;
    ((TextView)findViewById(R.id.textViewCounter))
            .setText("Counter: " + Integer.toString(mCounter));
}
  1. 要接收应用程序状态更改的通知,我们需要将 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); 
} 
  1. 运行程序并尝试更改方向以查看其行为(如果您使用的是模拟器,Ctrl + F11 将旋转设备)。

它是如何工作的...

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

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

但等等!您在旋转设备之前尝试在 EditText 视图中输入文本了吗?如果是这样,您会注意到文本也被恢复了,但我们没有处理该视图的代码。默认情况下,如果系统有一个唯一的 ID,它将自动保存状态。

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

还有更多...

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

onCreate(Bundle savedInstanceState) 

这两种方法都接收同一个名为 savedInstanceStateBundle 实例。您可以将恢复代码移动到 onCreate() 方法,并且它将按相同的方式工作。但有一个要注意的是,如果没有数据,例如在活动的初始创建期间,savedInstanceState 包将是一个空值。如果您想从 onRestoreInstanceState() 回调中移动代码,只需确保数据不是空值。以下是该代码的示例:

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

相关内容

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

  • 查看第七章数据存储,了解更多关于如何持久化数据的示例

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

存储持久活动数据

能够暂时存储关于我们活动的信息非常有用,但往往我们希望我们的应用程序能够在多个会话中记住信息。

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();
}
  1. 然后,在onCreate()的末尾添加以下代码以恢复计数器:
SharedPreferences settings = getPreferences(MODE_PRIVATE);
int defaultCounter = 0;
mCounter = settings.getInt(KEY_COUNTER, defaultCounter);
((TextView)findViewById(R.id.textViewCounter))
        .setText("Counter: " + Integer.toString(mCounter));
  1. 运行程序并尝试使用它。

工作原理...

如您所见,这与保存状态数据非常相似,因为它也使用名称/值对。在这里,我们只存储了一个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 应用程序生命周期就变得非常重要。

下面的图示显示了活动在其生命周期中经过的阶段:

除了阶段之外,该图还显示了可以重写的方法。如您所见,我们已经在先前的菜谱中使用了这些方法中的大部分。希望了解整体情况能帮助您理解。

准备工作

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

如何实现...

为了看到应用程序通过各个阶段,我们将为所有

阶段:

  1. 打开 activity_main.xml 并为自动生成的 TextView 添加一个 ID:
android:id="@+id/textViewState" 
  1. 剩余步骤将在 MainActivity.java 中进行。修改 onCreate() 方法以设置初始文本:
((TextView)findViewById(R.id.textViewState)).setText("onCreate()n");

  1. 添加以下方法来处理剩余的事件:
@Override
protected void onStart() {
    super.onStart();
    ((TextView)findViewById(R.id.textViewState)).append("onStart()\n");
}

@Override
protected void onResume() {
    super.onResume();
    ((TextView)findViewById(R.id.textViewState)).append("onResume()\n");
}

@Override
protected void onPause() {
    super.onPause();
    ((TextView)findViewById(R.id.textViewState)).append("onPause()\n");
}

@Override
protected void onStop() {
    super.onStop();
    ((TextView)findViewById(R.id.textViewState)).append("onStop()\n");
}

@Override
protected void onRestart() {
    super.onRestart();
    ((TextView)findViewById(R.id.textViewState)).append("onRestart()\n");
}

@Override
protected void onDestroy() {
    super.onDestroy();
    ((TextView)findViewById(R.id.textViewState)).append("onDestroy()\n");
}
  1. 运行应用程序,并观察当按下返回键和主页键中断活动时会发生什么。尝试其他操作,如任务切换,以查看它们如何影响您的应用程序。

它是如何工作的...

我们的活动可以存在于以下三种状态之一:活跃暂停停止。还有一个

第四种状态,销毁(但无法保证操作系统会调用它):

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

  • 当一个新启动的活动填满屏幕或使我们的活动不可见时,我们的活动将进入 停止 状态,并且恢复将始终调用 onRestart()

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

  • 值得注意的是,我们实际上从未看到onDestroy()方法的结果,因为到这时活动已经被移除。如果你想进一步探索这些方法,那么使用Activity.isFinishing()来查看在onDestroy()执行之前活动是否真的正在结束是非常有价值的,如下面的代码片段所示:

@Override
public void onPause() {
    super.onPause();
    ((TextView)findViewById(R.id.textViewState)).append("onPause()\n");
    if (isFinishing()){
        ((TextView)findViewById(R.id.textViewState)).append(" ... finishing");
    }
}

在实现这些方法时,始终在执行任何工作之前调用超类。

还有更多...

要关闭一个活动,直接调用其finish()方法,该方法会依次调用onDestroy()。要从子活动中执行相同操作,使用finishFromChild(Activity child),其中child是调用子活动。

有时知道活动是正在关闭还是仅仅暂停是有用的,isFinishing(boolean)方法返回一个值,表示活动处于这两种状态中的哪一种。

第二章:布局

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

  • 定义和展开布局

  • 使用 RelativeLayout

  • 使用 LinearLayout

  • 创建表格——TableLayoutGridLayout

  • RecyclerView 替代 ListView

  • 在运行时更改布局属性

简介

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

Android 提供了多种有用的 Layout 类,用于包含和组织活动(如按钮、复选框和其他视图)的各个元素。ViewGroup 对象是一个容器对象,作为 Android Layout 类家族的基类。放置在布局中的视图形成一个层次结构,最顶层的布局是父布局。

Android 提供了多种内置布局类型,专为特定目的设计,例如 RelativeLayout,它允许视图相对于其他元素定位。LinearLayout 可以堆叠视图或根据指定的方向水平对齐视图。TableLayout 可以用于布局视图网格。在各种布局中,我们还可以使用 Gravity 来对齐视图,并使用 Weight 控制提供比例大小。布局和 ViewGroups 可以嵌套在彼此内部以创建复杂的配置。提供了十几种不同的布局对象来管理小部件、列表、表格、画廊和其他显示格式,并且您还可以从基类派生来自定义布局。

Google 发布了一种名为 ConstraintLayout 的新布局。这个布局与 RelativeLayout 类似,因为视图是相对于彼此和父视图定位的,还有一个新元素称为指南。布局的重点是尽可能保持布局本身尽可能平坦(深度嵌套的布局可能导致性能问题)以及视觉布局编辑器。在保持编辑器与底层类同步的同时提供最佳的视觉编辑体验,这对 Google 来说是如此重要,以至于同一个团队开发了这两个。ConstraintLayout 现在是使用 Android Studio 创建的默认布局,也是本书中大多数示例的基础。(其他布局仍然可用,并在它们的布局提供最干净的 XML 时使用。)以下是 ConstraintLayout 类的链接,但为了获得最佳体验,建议使用 Android Studio 中的视觉编辑器:developer.android.com/reference/android/support/constraint/ConstraintLayout

定义和填充布局

当使用 Android Studio 向导创建新项目时,它会自动创建res/layout/activity_main.xml文件(如下面的截图所示)。然后,它使用setContentView(R.layout.activity_main)onCreate()回调中填充 XML 文件:

图片

对于这个食谱,我们将创建两个略有不同的布局,并通过按钮在它们之间切换。

准备工作

在 Android Studio 中创建一个新项目并命名为InflateLayout。一旦项目创建完成,展开res/layout文件夹,以便我们可以编辑activity_main.xml文件。在Target Android devices上使用默认的Phone & Tablet设置,并在Add an Activity to Mobile对话框中选择Empty Activity

如何操作...

  1. 编辑res/layout/activity_main.xml文件,使其包含以下按钮定义:
<Button
    android:id="@+id/buttonLeft"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_centerVertical="true"
    android:onClick="onClickLeft"
    android:text="Left Button"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintHorizontal_bias="0.0"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  1. 现在将activity_main.xml的副本复制并命名为activity_main2.xml。更改按钮以匹配以下内容:
<Button
    android:id="@+id/buttonLeft"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_centerVertical="true"
    android:onClick="onClickRight"
    android:text="Right Button"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintHorizontal_bias="1.0"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  1. 打开MainActivity.java并添加以下两个方法来处理按钮点击:
public void onClickLeft(View view) { 
    setContentView(R.layout.activity_main2); 
} 

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

它是如何工作的...

关键在于对setContentView()的调用,我们之前在自动生成的onCreate()代码中已经遇到过。只需将布局 ID 传递给setContentView(),它就会自动填充布局。

这段代码的目的是使概念易于理解,但仅用于更改按钮属性(在这个例子中,我们可以在按钮点击时更改对齐方式)可能过于冗余。通常,在onCreate()方法中只需要填充布局一次,但有时你可能需要手动填充布局,就像我们在这里做的那样。(如果你手动处理方向变化,这将是一个很好的例子。)

更多内容...

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

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

参见

如前所述,在第五章中了解有关片段的信息,用于为你的活动创建可重用的屏幕组件。

使用 RelativeLayout

如同在简介部分所述,RelativeLayout允许视图相对于彼此和父视图进行定位。RelativeLayout特别适用于减少嵌套布局的数量,这对于减少内存和处理需求非常重要。

准备工作

创建一个新项目并命名为RelativeLayout。Android Studio 默认使用ConstraintLayout,但在这个例子中,我们将用RelativeLayout替换它。在Target Android devices上使用默认的Phone & Tablet设置,并在Add an Activity to Mobile对话框中选择Empty Activity

如何操作...

  1. 打开res/layout/activity_main.xml文件并按以下方式更改它:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <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 Left"
        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_alignParentRight="true" />
</RelativeLayout>
  1. 运行代码,或在设计选项卡中查看布局

它是如何工作的...

这是一个非常直接的练习,但它演示了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中没有的关键特性——weight属性。在定义视图时,我们可以指定layout_weight参数,以允许视图根据可用空间动态调整大小。选项包括让视图填充所有剩余空间(如果视图具有更高的权重),让多个视图适应给定空间(如果所有视图都具有相同的权重),或者按权重成比例地分配视图间距。

我们将创建包含三个EditText视图的LinearLayout来演示如何使用weight属性。对于此示例,我们将使用三个EditText视图——一个用于输入To Address参数,另一个用于输入Subject,第三个用于输入MessageToSubject视图将各占一行,剩余空间将分配给Message视图。

准备工作

创建一个新的项目,并将其命名为LinearLayout。我们将用LinearLayout替换在activity_main.xml中创建的默认RelativeLayout。使用默认的“手机和平板”设置在“目标 Android 设备”上,并在“添加活动到移动设备”对话框中选择“空活动”。

如何操作...

  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> 
  1. 运行代码,或在“设计”选项卡中查看布局。

它是如何工作的...

当使用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时,行和列的大小在布局定义中定义。

两种布局都没有更好,这只是使用最适合您需求的最佳布局。我们将使用每种布局创建一个 3 x 3 网格以进行比较,因为您可能会很容易地在同一应用程序中使用这两种布局。

准备工作

为了专注于布局并提供更易于比较的体验,我们将为这个食谱创建两个独立的应用程序。创建两个新的 Android 项目,第一个称为 TableLayout,另一个称为 GridLayout。在 Target Android devices 上使用默认的 Phone & Tablet 设置,并在 Add an Activity to Mobile 对话框中选择 Empty Activity

如何做到这一点...

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

  2. 向每个 TableRow 添加三个 TextView 对象的三个集合,以创建一个 3 x 3 矩阵。为了演示目的,列被标记为 A-C,行标记为 1-3,因此 TextView 对象的第一行将是 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> 
  1. 现在,打开 GridLayout 项目以编辑 activity_main.xml。将根布局更改为 GridLayout。向 GridLayout 元素添加 columnCount=3rowCount=3 属性。

  2. 现在,向 GridLayout 添加九个 TextView 对象。我们将使用与前面的 TableLayout 相同的文本,以便进行一致的比较。由于 GridView 不使用 TableRow 对象,前三个 TextView 对象位于第 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> 
  1. 您可以运行应用程序或使用设计选项卡来查看结果。

它是如何工作的...

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

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)指定行和列的数量。

在前面的例子中,我们只是向 GridLayout 添加了 TextView 对象,并让系统自动定位它们。我们可以通过在定义视图时指定行和列位置来改变这种行为,如下所示:

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

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

与在 使用 LinearLayout 烹饪法中展示的 LinearLayout 一样,GridLayout 也提供了支持水平(默认)和垂直(Using GridLayout)方向的定位属性。方向决定了单元格的放置方式。(水平首先填充列,然后移动到下一行。垂直首先填充每行的第一列,然后移动到下一列。)

RecyclerView 替代 ListView

如其名所示,ListView 是为显示信息列表而设计的。如果你在 Android 上有先前的经验,你可能之前已经遇到过 ListView 和可能 GridView 控件。如果不是在编码时,你很可能已经作为应用程序使用过它,因为它是可用控件中最常用的之一。对于大多数应用程序和用户来说,旧的 ListView 可能已经足够,并且没有引起任何问题。例如,大多数用户可能能够在他们的收件箱中无任何问题地看到他们的电子邮件列表。但对于一些人来说,他们收件箱中的电子邮件可能如此之多,以至于在滚动列表时,他们的设备会卡顿(滚动时的轻微暂停)。不幸的是,ListView 有许多这样的性能问题。

ListView 的最显著性能问题是由在滚动时为每个项目创建新项目对象引起的。尽管通过正确实现数据适配器可以消除大部分性能问题,但实现是可选的。正如其名所示,RecyclerView 基于回收列表项(在 ListView 适配器中是可选的部分)。控制还有其他变化。ListView 有许多内置功能,而 RecyclerView 非常基础,依赖于额外的辅助类来实现相同的功能。对于一些人来说,这感觉像是新控制的一个倒退,但这种设计使得它更容易扩展。

RecyclerView 真正发光的地方在于其扩展性和动画的灵活性。我们这里的示例使用了一个静态列表,因此没有展示内置的动画,但使用动态数据时,你的列表将利用到 Material Design 的外观和感觉。尽管 ListView 并未官方弃用,但推荐对于新项目使用 RecyclerView。开始时可能需要做更多的工作,但这个食谱将为你提供所有设置所需的代码。

准备工作

在 Android Studio 中创建一个名为 RecyclerView 的新项目。在 Target Android devices 上使用默认的 Phone & Tablet 设置,并在“Add an Activity to Mobile”对话框中选择 Empty Activity。

如何操作...

创建 RecyclerView 与在屏幕上放置控件一样简单。大部分工作都与适配器有关,我们将从一个静态列表创建适配器。RecyclerView 是作为一个单独的库分发的,因此需要将其作为依赖项添加到项目中。步骤如下:

  1. 你可以通过 Android Studio UI 添加依赖项,或者将以下代码添加到 build.gradle (Module: app) 文件的 dependencies 部分中:

    实现 'com.android.support:recyclerview-v7:27.1'

注意:v7:27.1 在撰写本文时是当前的版本,但应更新到最新版本。(如果你的 IDE 没有使用最新版本,可能会给出警告。)

  1. 打开 activity_main.xml 并将现有的 <TextView /> 块替换为以下 RecyclerView 小部件:
<android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 我们需要为适配器创建列表中的单个项的另一个布局。为此,在 res\layout 文件夹中创建一个名为 item.xml 的新文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView" />
</LinearLayout>
  1. 现在,来到 RecyclerView 的核心——适配器。创建一个名为 MyAdapter.java 的新 Java 文件。我们的新类将扩展 RecyclerView.Adapter 类,因此我们需要重写几个关键方法。我们将在稍后讨论这个类的细节,但完整的代码如下:
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private List<String> nameList;

    public MyAdapter(List<String> list) {
        nameList = list;
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View view = inflater.inflate(R.layout.item, parent, false);
        MyViewHolder myViewHolder = new MyViewHolder(view);
        return myViewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, final int position) {
        final String name = nameList.get(position);
        holder.textView.setText(name);
    }

    @Override
    public int getItemCount() {
        if (nameList==null) {
            return 0;
        } else {
            return nameList.size();
        }
    }

    public class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView textView;

        public MyViewHolder(View itemVieww) {
            super(itemVieww);
            textView = itemView.findViewById(R.id.textView);
        }
    }
}
  1. 在所有组件设置完毕后,最后一步是将它们全部组合起来。打开 MainActivity.java 文件,并将以下代码添加到现有的 onCreate() 方法中:
List<String> list = new ArrayList<>();
list.add("China");
list.add("France");
list.add("Germany");
list.add("India");
list.add("Russia");
list.add("United Kingdom");
list.add("United States");

RecyclerView recyclerView = findViewById(R.id.recyclerView);

recyclerView.setHasFixedSize(true);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);

MyAdapter myAdapter = new MyAdapter(list);
recyclerView.setAdapter(myAdapter);

它是如何工作的...

我们故意保持这个食谱的基础性,但正如你所见,即使是这个基本实现也有许多步骤。好消息是,有了这个基础,你可以轻松地根据需要扩展和修改 RecyclerView。想要你的列表水平滚动吗?你可以通过在 setOrientation() 调用中使用 LinearLayoutManager.HORIZONTAL 实现这一点。

如果你之前曾经使用过 Android ListView,那么前面的步骤看起来会非常熟悉。概念是相同的:我们创建一个适配器来持有项目列表。步骤 1 和 2 在活动中设置了RecyclerView。在步骤 3 中,我们指定了视觉布局并将其传递给适配器。在步骤 4 中,我们通过扩展RecyclerView.Adapter类创建了适配器。从代码中可以看出,我们需要重写三个方法:onCreateViewHolder()onBindViewHolder()getItemCount()RecyclerView背后的关键概念是回收或重用项目视图。这意味着,当你有一个非常大的项目列表时,你不需要为每个项目创建一个新的视图对象(这在性能和内存使用方面成本很高),而是重用项目视图。所以当用户滚动浏览长列表时,当一个视图离开屏幕,它将被重用于下一个显示的项目。即使我们把我们列表中的所有国家都添加进去,也不会有足够的项目来看到性能差异,但当你处理包含数千个项目的列表时,尤其是如果这些项目包括图片,滚动时的性能差异将是明显的。

现在你已经理解了RecyclerView背后的概念,希望我们需要的重写方法都是不言自明的。适配器只调用onCreateViewHolder()来创建足够多的项目以显示在屏幕上(以及一些额外的项目用于滚动),而onBindViewHolder()则会在每个项目显示时被调用。

还有更多...

如果你运行了代码,那么你看到它是一个非常简单的应用。实际上,它并没有做任何比在可滚动的容器中显示列表更多的事情。大多数应用都需要与列表进行一些交互,那么我们如何响应点击事件?与较老的ListView不同,RecyclerView没有内置任何点击事件。这取决于你,程序员,来创建你需要的事件。(对于像我们例子中的基本项目,这可能看起来对程序员来说工作量更大,但当你处理带有按钮和其他交互控件复杂列表项时,ListView经常会阻碍你,你仍然需要实现自定义事件。)

要响应项目点击,请将以下代码添加到MyAdapter类中:

private void remove(int position) {
    nameList.remove(position);
    notifyItemRemoved(position);
}

然后在步骤 4 中创建的onBindViewHolder()方法中添加以下代码:

holder.itemView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        remove(position);
    }
});

现在,当你运行代码时,应用将响应点击事件,通过移除被点击的项目。你也许还会注意到移除项目时的平滑动画。通过调用RecyclerViewnotifyItemRemoved()notifyItemInserted()方法,我们可以利用小部件内置的 Material Design 动画。

在运行时更改布局属性

在 Android 开发中,通常首选的做法是用 XML 定义 UI,用 Java 定义应用程序代码,将用户界面代码与应用程序代码分开。有时,从 Java 代码中更改(甚至构建)UI 要容易得多或更高效。幸运的是,这在 Android 中很容易实现。

在这个菜谱中,我们将获取LayoutParams对象的引用,以便在运行时更改边距。

准备工作

在这里,我们将使用 XML 设置一个简单的布局,并使用LinearLayout.LayoutParams对象在运行时更改视图的边距。使用带有名为RuntimeProperties的空活动的项目。在目标 Android 设备上使用默认的手机和平板设置,并在添加活动到移动设备对话框中选择空活动

如何实现...

我们可以通过代码创建或操作任何标准布局或控件。在这个例子中,我们将使用LinearLayout

  1. 打开activity_main.xml文件,并按如下方式更改布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

</LinearLayout>
  1. 添加一个 ID 值为textViewTextView,如下所示:
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="TextView" />
  1. 添加一个 ID 值为buttonButton,如下所示:
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button" />
  1. 打开MainActivity.java文件,并在onCreate()方法中添加以下代码以响应用户点击:
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;
    }
});
  1. 在设备或模拟器上运行程序。

它是如何工作的...

每个视图(以及因此ViewGroup)都有一组与之关联的布局参数。特别是,所有视图都有参数来通知其父视图它们期望的高度和宽度。这些参数由layout_heightlayout_width参数定义。我们可以通过getLayoutParams()方法从代码中访问这些布局信息。布局信息包括布局高度、宽度、边距以及任何类特定的参数。

在这个例子中,我们通过获取当前按钮的LayoutParams并增加边距来在每次点击时移动按钮。

第三章:视图、小部件和样式

本章将涵盖以下主题:

  • 在布局中插入小部件

  • 使用图形显示按钮状态

  • 在运行时创建小部件

  • 创建自定义组件

  • 将样式应用到视图

  • 将样式转换为主题

  • 根据 Android 操作系统版本选择主题

简介

在 Android 中,术语小部件可以指几个不同的概念。当大多数人谈论小部件时,他们指的是应用小部件,通常在主屏幕上看到。应用小部件本身就像迷你应用程序,因为它们通常提供基于其主要应用程序的功能子集。(通常,大多数应用小部件与应用程序一起安装,但这不是必需的。它们可以作为独立的应用程序以小部件格式存在。)一个常见的小部件应用示例是提供几个不同小部件的主屏幕天气应用程序。第六章,超越您的应用 - 主屏幕小部件、搜索和系统 UI,将讨论主屏幕应用小部件并提供创建自己的菜谱。

在为 Android 开发时,术语小部件通常指的是放置在布局文件中的专用视图,如 Button、TextView、CheckBox 等。本章将专注于屏幕布局的小部件。

要查看Android SDK提供的部件列表,请在 Android Studio 中打开一个布局文件,并点击设计标签。在设计视图的左侧,您将看到可以放置在布局中的项目列表:常用、文本、按钮、小部件、布局、容器、Google 和遗留。尽管许多项目不在小部件类别中,但根据定义,它们仍然是小部件。如图所示,小部件类别将更复杂的控件分组:

图片

如列表所示,Android SDK 提供了许多有用的部件——从简单的 TextView、Button 或 Checkbox,到更复杂的部件,如 WebView、ProgressBar 和 SearchView。尽管内置部件很有用,但也很容易在 SDK 提供的内容上扩展。我们可以扩展现有部件以自定义其功能,或者我们可以通过扩展基本 View 类从头创建自己的部件。(我们将在后面的创建自定义组件菜谱中提供一个例子。)

小部件的视觉外观也可以自定义。这些设置可以用来创建样式,进而可以用来创建主题。就像在其他开发环境中一样,创建主题的好处是可以通过最小的努力轻松地更改整个应用程序的外观。最后,Android SDK 还提供了许多内置主题和变体,例如在 Android 5 中引入的 Material 主题以及之后的 Material Design 2.0。

在布局中插入小部件

如您从之前的菜谱中看到的,小部件是在布局文件中声明的,或者是在代码中创建的。在这个菜谱中,我们将一步一步地使用 Android Studio 设计器添加一个按钮。(对于后续的菜谱,我们只会展示布局 XML。)创建按钮后,我们将创建一个方法来接收按钮点击事件,使用 onClickListener()

准备工作

在 Android Studio 中启动一个新项目,并将其命名为 InsertWidget。使用创建手机和平板项目的默认选项,并在提示活动类型时选择空活动。你可以删除默认的 TextView(或者保留它),因为在这个菜谱中不需要它。

如何做到这一点...

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

  1. 在 Android Studio 中打开 activity_main.xml 文件并点击设计标签。如您所见,默认情况下,Android Studio 会向布局添加一个 TextView。选择 TextView 并删除它:

图片

  1. 在小部件列表中找到按钮并将其拖到活动屏幕右边的中心:

图片

  1. 虽然我们将按钮放置在屏幕中心,但在运行应用程序时按钮实际上并不会居中。如果我们想让它居中,我们需要相应地设置布局属性。(目前,按钮只是在设计工具中居中,以便更容易工作,但这在应用程序运行时没有任何影响。)要使按钮居中,首先在设计视图中选择按钮。当它被选中时,你会看到边缘节点。将每个边缘节点拖动到屏幕相应边缘,如图所示:

图片

  1. 要查看创建的 xml,请点击文本标签,如图所示。看看按钮是如何使用 ConstraintLayout 参数居中的。还要注意默认 ID,因为我们将在下一步需要它:

图片

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

它是如何工作的...

使用 Android Studio 创建 UI 与拖放视图一样简单。你还可以直接在设计标签页中编辑视图的属性。切换到 XML 代码就像点击文本标签一样简单。

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

android:id="@+id/button" 

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

还有更多...

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

相关内容

使用图形来显示按钮状态

我们已经讨论了 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文件夹中创建一个新的 Drawable 资源文件,并将其命名为:state_selector.xml。该文件应包含以下代码:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@android:color/darker_gray"
        android:state_checked="true"/>
    <item
        android:drawable="@android:color/white"
        android:state_checked="false"/>
</selector>
  1. 现在打开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" /> 
  1. 在设备或模拟器上运行应用程序。

它是如何工作的...

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

Android 支持除已检查之外的其他许多状态条件。当在android:state中键入时,查看自动完成下拉菜单以查看其他选项列表。

一旦我们创建了可绘制资源(步骤 1 中的 XML),我们只需告诉视图使用它即可。由于我们希望背景颜色根据状态改变,我们使用 android:background 属性。state_selector.xml 是一个可绘制资源,可以传递给任何接受可绘制资源的属性。例如,我们可以用以下 XML 替换复选框的勾选图像:

<CheckBox
    android:id="@+id/checkBox"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:button="@drawable/state_selector"
    android:text="CheckBox" />

更多...

如果我们想要实际的图像而不是仅仅改变颜色呢?这就像更改项目状态中引用的可绘制资源一样简单。可用于下载的源代码使用了两个图形,下载自: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 资源选择的另一个示例,请参阅 根据 Android 版本选择主题 菜谱

在运行时创建小部件

如前所述,通常 UI 在 XML 文件中声明,然后在运行时通过 Java 代码进行修改。虽然可以在 Java 代码中完全创建 UI,但对于复杂的布局,通常不会将其视为最佳实践。

在这个菜谱中,我们将向在 activity_main.xml 中定义的现有布局中添加一个视图。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 RuntimeWidget。当提示选择 Activity 类型时,选择 Empty Activity 选项。

如何操作...

我们将首先向现有布局添加一个 ID 属性,以便在代码中访问该布局。一旦我们在代码中有了布局的引用,我们就可以向现有布局添加新视图。以下是步骤:

  1. 打开 res/layout/activity_main.xml 并向根 ConstraintLayout 添加一个 ID 属性,如下所示:
android:id="@+id/layout" 
  1. 完全移除默认的 <TextView> 元素。

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

ConstraintLayout layout = findViewById(R.id.layout);
  1. 创建 DatePicker 并使用以下代码将其添加到布局中:
DatePicker datePicker = new DatePicker(this); 
layout.addView(datePicker); 
  1. 在设备或模拟器上运行程序。

它是如何工作的...

这段代码希望非常直观。首先,我们使用 findViewById 获取父布局的引用。我们在现有的 ConstraintLayout(在第 1 步中)中添加了 ID 以获取引用。我们通过 addView() 方法在代码中创建一个 DatePicker 并将其添加到布局中。

还有更多...

如果我们想从代码中创建整个布局呢?虽然这可能不被认为是最佳实践,但在某些情况下,从代码中创建整个布局确实更容易(且更简单)。让我们看看如果我们没有使用 activity_main.xml 中的布局,这个例子会是什么样子。以下是 onCreate() 的样子:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ConstraintLayout layout = new ConstraintLayout(this);
    DatePicker datePicker = new DatePicker(this);
    layout.addView(datePicker);
    setContentView(layout);
}

在这个例子中,实际上并没有太大的不同。如果你在代码中创建了一个视图并希望稍后引用它,你需要保留对对象的引用,或者给视图分配一个 ID 以使用 findViewByID()。要给视图分配一个 ID,请使用 setID() 方法并通过传递 View.generateViewId()(以生成一个唯一的 ID)或使用 XML 中的 <resources> 定义 ID。

创建自定义组件

正如我们在之前的菜谱中看到的,Android SDK 提供了广泛的各种组件。但是,当你找不到适合你独特需求的预构建组件时会发生什么?你总是可以创建自己的!

在这个菜谱中,我们将逐步创建一个从 View 类派生的自定义组件,就像内置小部件一样。以下是高级概述:

  1. 创建一个新的类,它扩展了 View 类。

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

  3. 覆盖 onMeasure(),因为默认实现返回 100 x 100 的大小。

  4. 覆盖 onDraw(),因为默认实现不绘制任何内容。

  5. 定义自定义方法和监听器(例如 onClick() 事件)。

  6. 实现自定义功能。

覆盖 onMeasure()onDraw() 不是严格要求的,但默认行为可能不是你想要的。

准备工作

在 Android Studio 中开始一个新的项目,并将其命名为 CustomView。使用默认向导选项,包括手机和平板 SDK,并在被提示活动类型时选择空活动。一旦项目文件创建并打开在 Android Studio 中,你就可以开始工作了。

如何做到这一点...

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

  1. 首先,创建一个新的 Java 类,并将其命名为CustomView。这就是我们将实现自定义组件的地方,如简介中所述。

  2. 将类构造函数更改为扩展View。它应如下所示:

public class CustomView extends View {
  1. 为类定义一个Paint对象,它将在onDraw()中使用:
final Paint mPaint = new Paint();
  1. 创建一个默认构造函数,它需要一个活动Context,这样我们就可以填充视图。我们还将在这里设置画笔属性。构造函数应如下所示:
public CustomView(Context context) { 
    super(context); 
    mPaint.setColor(Color.BLACK); 
    mPaint.setTextSize(30); 
} 
  1. 按如下方式重写onDraw()方法:
@Override 
protected void onDraw(Canvas canvas) { 
    super.onDraw(canvas); 
    setBackgroundColor(Color.CYAN); 
    canvas.drawText("Custom Text", 100, 100, mPaint); 
    invalidate(); 
} 
  1. 最后,在MainActivity.java中通过将onCreate()方法中的setContentView()替换为我们的视图来填充我们的自定义视图,如下所示:
setContentView(new CustomView(this)); 
  1. 在设备或模拟器上运行应用程序,以查看其实际效果。

它是如何工作的...

我们首先扩展View类,就像内置组件一样。接下来,我们创建默认构造函数。这很重要,因为我们需要将上下文传递给super类,我们通过以下调用来实现:

super(context);

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

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

还有更多...

实际上,还有很多。我们只是触及了使用自定义组件可以做的事情的表面。幸运的是,正如您从这个示例中看到的,要获得基本功能并不需要很多代码。我们很容易就能用整个章节来讨论诸如将布局参数传递给视图、添加监听器回调、重写onMeasure()、在 IDE 中使用我们的视图等问题。这些都是您根据需要可以添加的功能。

虽然自定义组件始终是一个选项,但可能还有其他选项可能需要更少的编码。扩展现有小部件通常足以避免从头创建自定义组件的开销。如果您需要的是具有多个小部件的解决方案,还有复合控件。复合控件,如组合框,只是将两个或更多控件组合在一起作为一个小部件。

由于您将添加多个小部件,复合控件通常从布局扩展,而不是从View扩展。您可能不需要重写onDraw()onMeasure(),因为每个小部件都会在其相应的方法中处理绘制。

参见

将样式应用于视图

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

在本食谱中,我们将通过创建样式并将其连接到视图的步骤进行操作。

与层叠样式表(Cascading Style Sheets)类似,Android 样式允许你将设计设置与 UI 代码分开指定。

准备工作

创建一个新的 Android Studio 项目,并将其命名为 Styles。使用默认向导选项创建一个手机和平板项目,并在提示 Activity 类型 时选择空活动。我们之前没有看过它,但默认情况下,向导还会创建一个 styles.xml 文件,我们将使用它来完成本食谱。

如何做到这一点...

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

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

  1. 我们将通过在现有 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> 
  1. 现在告诉视图使用这个样式。打开 activity_main.xml 文件,并将以下属性添加到现有的 <TextView> 元素中:
style="@style/MyStyle" 
  1. 要么运行应用程序,要么在“设计”选项卡中查看结果。

它是如何工作的...

样式是一种资源,通过在 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 对象。(每种小部件类型都有一个样式,因此你可以为 ButtonToggleButtonTextView 等做同样的事情。)

要为所有 TextView 对象设置样式,请将以下行添加到 AppTheme 样式中:

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

由于我们的应用程序主题已经使用了 AppTheme,我们只需将那一行添加到 AppTheme 中,就可以让所有的 TextView 对象都使用我们的自定义 MyStyle 进行样式化。

参见

Android 设计支持库位于 www.google.com/design/spec/material-design/introduction.html

将样式转换为主题

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

设置应用程序主题很常见,但通常会覆盖特定的活动以使用不同的主题。

在上一个菜谱中,我们使用 AppTheme 样式(由向导自动创建)设置了 textViewStyle。在这个菜谱中,你将学习如何设置应用程序和活动主题。

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

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

准备工作

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

如何做到这一点...

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

  1. 由于主题和样式定义在相同的资源中,请打开位于res/valuesstyles.xml文件,并创建一个新的样式。我们将基于已提供的AppTheme创建一个新的样式,并设置windowIsFloating。XML 将如下所示:
<style name="AppTheme.MyDialog"> 
    <item name="android:windowIsFloating">true</item> 
</style> 
  1. 接下来,设置活动使用这个新的对话框主题。打开AndroidManifest.xml文件,并将theme属性添加到活动元素中,如下所示:
<activity android:name=".MainActivity" 
    android:theme="@style/AppTheme.MyDialog"> 

注意,现在应用和活动都将指定一个主题。

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

它是如何工作的...

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

更多内容...

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

根据 Android 版本选择主题

大多数用户更喜欢看到使用 Android 提供的最新主题的应用。为了在市场上与其他众多应用竞争,你可能也想升级你的应用,但那些仍在运行较旧 Android 版本的用户怎么办?通过正确设置我们的资源,我们可以使用 Android 中的资源选择来根据用户运行的 Android OS 版本自动定义父主题。

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

  • Theme - Gingerbread 和更早版本

  • Theme.Holo - Honeycomb (API 11)

  • Theme.Material - Lollipop (API 21)

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

准备工作

在 Android Studio 中启动一个新项目,并将其命名为AutomaticThemeSelector。使用默认向导选项创建一个手机和平板项目。当被提示活动类型时,选择空活动。在配置活动对话框中,取消选择向后兼容性(AppCompat)复选框。

如何做到这一点...

通常,我们在创建项目时使用 AppCompat 选项,但在前面的准备工作部分,我们取消选择了此选项,因为我们需要显式手动设置我们的资源。我们将验证我们是否扩展了通用的Activity类,然后我们可以添加我们的新样式资源来根据 API 选择主题。以下是步骤:

  1. 我们需要确保MainActivity继承自Activity而不是AppCompatActivity。打开ActivityMain.java文件,如果需要,将其修改为如下所示:
public class MainActivity extends Activity { 
  1. 打开activity_main.xml文件,添加两个视图:ButtonCheckbox

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

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

  1. 通过在res文件夹上右键单击并导航到新建 | 目录,创建一个新的目录,如下截图所示:

首个目录使用以下名称:values-v11

使用相同的方法为第二个目录使用values-v21

  1. 现在在每个新目录中创建一个styles.xml文件。(在values-v11目录上右键单击并选择新建 | 文件选项。)对于values-v11,使用以下样式定义 Holo 主题:
<resources> <style name="AutomaticTheme"
 parent="android:Theme.Holo.Light"> </style>
</resources>

对于values-v21,使用以下代码定义 Material 主题:

<resources> <style name="AutomaticTheme"
    parent="android:Theme.Material.Light"> </style>
</resources>
  1. 最后一步是告诉应用程序使用我们新的主题。为此,打开AndroidManifest.xml文件,将应用程序的android:theme属性更改为AutomaticTheme。它应该如下所示:
android:theme="@style/AutomaticTheme"
  1. 现在在物理设备或模拟器上运行应用程序。如果您想看到三种不同的主题,您将需要一个运行不同版本 Android 的设备或模拟器。

它是如何工作的...

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

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

最后一步是指定我们的新主题作为应用程序主题,我们在 AndroidManifest 中已经做到了这一点。

还有更多...

关于资源选择更多信息,请参阅使用指定文件夹为屏幕特定资源部分以及使用图形显示按钮状态食谱*。

第四章:菜单和动作模式

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

  • 创建选项菜单

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

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

  • 使用 RecyclerView 的上下文批量模式

  • 创建弹出式菜单

简介

Android 操作系统是一个不断变化的环境。最早的 Android 设备(在 Android 3.0 之前),需要有一个硬件菜单按钮。尽管硬件按钮不再需要,但菜单同样重要。事实上,菜单 API 已经扩展到现在支持三种不同类型的菜单:

  • 选项菜单和操作栏:这是标准菜单,用于应用程序的全局选项。用于搜索、设置等附加功能。

  • 上下文模式上下文动作模式):这通常通过长按激活。(想想这就像桌面上的右键点击。)这用于对按下的项执行操作,例如回复电子邮件或删除文件。

  • 弹出式菜单:这为附加操作提供了一个弹出式选择(类似于旋转框)。菜单选项不旨在影响按下的项;相反,使用前面描述的上下文模式。一个例子是点击分享按钮并获取额外的分享选项列表。

菜单资源与其他 Android UI 组件类似;它们通常在 XML 中创建,但也可以在代码中创建。我们将在下一节中展示的第一个菜谱将展示 XML 菜单格式以及如何展开它。

创建选项菜单

在我们实际创建和显示菜单之前,让我们看看一个菜单以查看最终结果。以下是一个显示 Chrome 应用程序菜单部分的屏幕截图:

最明显的特点是菜单将根据屏幕大小而有所不同。默认情况下,菜单项将被添加到溢出菜单中——这就是你按下最右边边缘的三个点时看到的菜单。

菜单通常使用 XML(像许多其他 Android 资源)在 res/menu 目录中创建,尽管它们也可以在代码中创建。要创建菜单资源,请使用如下所示的 <menu> 元素:

<menu > 
</menu> 

<item> 元素定义了每个单独的菜单项,并包含在 <menu> 元素中。一个基本的菜单项看起来如下:

<item  
    android:id="@+id/settings" 
    android:title="@string/settings" /> 

最常见的 <item> 属性如下:

  • id:这是标准资源标识符

  • title:这表示要显示的文本

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

  • showAsAction:这将在以下段落中解释

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

让我们更详细地看看 showAsAction

showAsAction 属性控制菜单项的显示方式。选项包括

以下内容:

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

  • withText:这表示标题和图标都应该显示

  • never:这表示菜单项永远不会包含在操作栏中;它始终显示在溢出菜单中

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

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

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

准备工作

使用 Android Studio 创建一个名为OptionsMenu的新项目。使用默认的“手机和平板”选项,并在提示活动类型时选择“空活动”选项。Android Studio 向导默认不会创建res/menu文件夹。您可以通过使用文件 | 新建 | 目录手动创建它,或者使用 Android 资源目录向导创建它。

这里是使用向导的步骤:

  1. 首先,在res文件夹上右键单击,并选择如这里所示的“新建 | Android 资源目录”:

图片

  1. 在“新建资源目录”对话框中,选择资源类型下拉菜单并选择菜单选项:

图片

如何做...

如前所述创建的新项目,您现在可以创建一个菜单。首先,我们将向strings.xml添加一个字符串资源。当创建菜单的 XML 时,我们将使用这个新字符串作为菜单标题。以下是步骤:

  1. 首先打开strings.xml文件,并在<resources>元素中添加以下<string>元素:
    <string name="menu_settings">Settings</string> 
  1. res/menu目录下创建一个新文件,并将其命名为menu_main.xml

  2. 打开menu_main.xml文件,并添加以下 XML 以定义菜单:

    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
        <item android:id="@+id/menu_settings"
            android:title="@string/menu_settings"
            app:showAsAction="never">
        </item>
    </menu>
  1. 现在菜单已在 XML 中定义,我们只需在ActivityMain.java中重写onCreateOptionsMenu()方法来填充菜单:
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
  1. 在设备或模拟器上运行程序,以查看操作栏中的菜单。

它是如何工作的...

这里有两个基本步骤:

  1. 在 XML 中定义菜单

  2. 在活动创建时填充菜单

作为良好的编程习惯,我们应在strings.xml文件中定义字符串,而不是在 XML 中硬编码它。然后,我们在第 3 步使用标准的 Android 字符串标识符来设置菜单的标题。由于这是一个设置菜单项,我们使用了showAsAction="never"选项,这样它就不会在操作栏中作为一个单独的菜单选项显示。

菜单定义后,我们将在第 4 步使用菜单填充器在活动创建时加载菜单。注意R.menu.menu_main菜单资源语法?这就是为什么我们在res/menu目录中创建 XML 的原因——这样系统就会知道这是一个菜单资源。

在第 4 步,我们使用了app:showAsAction而不是 Android 的android:showAsAction。这是因为我们正在使用AppCompat库(也称为 Android 支持库)。默认情况下,Android Studio 的新项目向导会将支持库包含在项目中。

还有更多...

如果你在第 5 步中运行了程序,那么当你按下菜单溢出按钮时,你必须已经看到了设置菜单项。但仅此而已。没有其他发生。显然,如果应用程序没有对这些菜单项做出响应,菜单项就没什么用了。通过 onOptionsItemSelected() 回调来响应选项菜单。

将以下方法添加到应用程序中,当选择设置菜单时显示 Toast:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == R.id.menu_settings) {
        Toast.makeText(this, "Settings", Toast.LENGTH_LONG).show();
    } else {
        return super.onContextItemSelected(item);
    }
    return true;
}

就这样。你现在有一个可以工作的菜单了!

如前例所示,当处理回调时返回 true;否则,如 else 语句所示,调用超类。

使用菜单项启动活动

在上述示例中,我们展示了在菜单点击时显示的 Toast;然而,如果需要,我们也可以轻松地启动一个新的活动。要启动一个活动,创建一个 Intent 并使用 startActivity() 调用它,如第一章中“使用 Intent 对象启动新活动”菜谱所示,活动

创建子菜单

子菜单的创建和访问几乎与其他菜单元素完全相同。它们可以放置在任何提供的菜单中,但不能放置在其他子菜单中。要定义子菜单,请在 <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 xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <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>

相关内容

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

尽管已经多次声明,但创建 UI 的最佳编程实践是在 XML 中而不是在 Java 中进行,但仍然有使用代码是更好的选择的时候。这尤其适用于你希望菜单项根据某些外部标准可见(或启用)的情况。菜单也可以包含在资源文件夹中,但有时你需要代码来执行选择哪个资源的逻辑。一个例子可能是,如果你只想在用户登录你的应用程序时提供上传菜单项。

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

准备工作

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

res/menu 目录。

如何操作...

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

  1. 将以下两个字符串添加到现有的 <resources> 元素中:
    <string name="menu_download">Download</string> 
    <string name="menu_settings">Settings</string> 
  1. 删除现有的 TextView 并在 activity_main.xml 中添加一个按钮,将其 onClick() 设置为 toggleMenu,如下所示:
    <Button
        android:id="@+id/buttonToggleMenu"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Toggle Menu"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
  1. 打开 ActivityMain.java 并在类声明下方添加以下三行代码:
    private final int MENU_DOWNLOAD = 1; 
    private final int MENU_SETTINGS = 2; 
    private boolean showDownloadMenu = false; 
  1. 为按钮点击回调添加以下方法:
    public void toggleMenu(View view) { 
        showDownloadMenu=!showDownloadMenu; 
    } 
  1. 当活动首次创建时,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;
    }
  1. 为了最佳编程实践,不要使用 onCreateOptionsMenu() 来更新或更改您的菜单;相反,使用 onPrepareOptionsMenu()。以下是根据我们的标志更改下载菜单项可见性的代码:
    @Override 
    public boolean onPrepareOptionsMenu(Menu menu) { 
        MenuItem menuItem = menu.findItem(MENU_DOWNLOAD); 
        menuItem.setVisible(showDownloadMenu); 
        return true; 
    } 
  1. 虽然此配方中技术上不需要此 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;
    }
  1. 在设备或模拟器上运行程序以查看菜单更改。

它是如何工作的...

我们为 onCreateOptionsMenu() 方法创建了一个覆盖,就像我们在之前的配方中做的那样,创建一个选项菜单。但是,我们不是展开现有的菜单资源,而是使用 Menu.add() 方法创建菜单。由于我们希望在以后也修改菜单项以及响应菜单项事件,我们定义了自己的菜单 ID 并将它们传递给 add() 方法。onOptionsItemSelected() 对象会为所有菜单项调用,因此我们获取菜单 ID 并根据 ID 使用 switch 语句。如果我们正在处理菜单事件,则返回 true,否则将事件传递给父类。

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

更多内容...

如果我们希望当下载选项可用时使其突出显示,我们可以通过在 onPrepareOptionsMenu() 方法(在返回语句之前)添加以下代码来告诉 Android 我们希望在操作栏中显示菜单:

menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 

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

早期,当我们动作栏中没有菜单项时,每次打开溢出菜单,Android 都会调用onPrepareOptionsMenu()来确保可见性总是更新。为了纠正这种行为,请将以下代码行添加到toggleMenu()方法中:

invalidateOptionsMenu(); 

invalidateOptionsMenu()调用告诉 Android 我们的选项菜单不再有效,这会强制调用onPrepareOptionsMenu(),从而得到我们期望的行为。

如果动作栏中显示菜单项,Android 认为菜单始终是打开的。

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

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

创建浮动上下文菜单

如果您需要使用旧式上下文菜单,例如为了支持 Android 3.0 之前的设备,它与选项菜单 API 非常相似,只是方法名不同。要创建菜单,请使用onCreateContextMenu()而不是onCreateOptionsMenu()。要处理菜单项选择,请使用onContextItemSelected()而不是onOptionsItemSelected()。最后,调用registerForContextMenu()以让系统知道您想要上下文菜单事件。

由于上下文模式被认为是显示上下文选项的首选方式,本食谱将重点关注较新的 API。上下文模式提供了与浮动上下文菜单相同的特性,但通过允许在批量模式下进行多项选择,还增加了额外的功能。

本食谱将演示为单个视图设置上下文模式。一旦激活,在我们的示例中,通过长按,一个上下文操作栏CAB)将替换动作栏,直到上下文模式完成。

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> 
  1. 创建了字符串后,我们现在可以通过在 res/menu 中创建一个名为 context_menu.xml 的新文件来创建菜单,如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
        <item android:id="@+id/menu_cast"
            android:title="@string/menu_cast" />
        <item android:id="@+id/menu_print"
            android:title="@string/menu_print" />
    </menu>
  1. 现在将 ImageView 添加到 activity_main.xml 中,作为启动上下文模式的来源。以下是 ImageView 的 XML:
    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@mipmap/ic_launcher" />
  1. 现在 UI 已经设置好了,我们可以添加上下文模式的代码。首先,我们需要一个全局变量来存储在调用 startActionMode() 时返回的 ActionMode 实例。将以下行代码添加到 MainActivity.java 中的类构造函数下方:
    ActionMode mActionMode;
  1. 接下来,创建一个 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;
        }
    };
  1. 创建了 ActionMode 回调后,我们只需调用 startActionMode() 以开始上下文模式。将以下代码添加到 onCreate() 方法中以设置长按监听器:
    ImageView imageView = findViewById(R.id.imageView);
    imageView.setOnLongClickListener(new View.OnLongClickListener() {
        public boolean onLongClick(View view) {
            if (mActionMode != null) return false;
            mActionMode = startSupportActionMode(mActionModeCallback);
            return true;
        }
    });
  1. 在设备或模拟器上运行程序,以查看 CAB 的实际效果。

它是如何工作的...

如你在第 2 步中看到的,我们使用了相同的菜单 XML 来定义上下文菜单和其他菜单。

需要理解的主要代码片段是 ActionMode 回调。这是我们处理上下文模式事件的地方:初始化菜单、处理菜单项选择和清理。我们在长按事件中使用 startActionMode() 调用并传入第 5 步中创建的 ActionMode 回调来启动上下文模式。

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

更多内容...

在这个例子中,我们存储了从 startActionMode() 调用返回的 ActionMode。我们使用它来防止在 Action Mode 已经激活时创建新的实例。我们也可以使用这个实例来对 CAB 本身进行更改,例如使用以下方式更改标题:

mActionMode.setTitle("New Title"); 

这在处理多个项目选择时特别有用,正如我们在下一道菜谱中将要看到的。

另请参阅

  • 查看下一道菜谱,使用 RecyclerView 的上下文批量模式,以处理多个项目选择

使用 RecyclerView 的上下文批量模式

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

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

图片

当我们在第二章布局中介绍RecyclerView时,我们讨论了旧ListView中的许多功能并未包含在新RecyclerView中。多项选择是最受欢迎的功能之一。在本菜谱中,我们将使用RecyclerView和 Action Mode 演示多项选择。

准备工作

我们将使用第二章布局中创建的RecyclerView示例作为本菜谱的基础。如果您还没有这样做,请回到该章节中的RecyclerView 替换 ListView菜谱,然后添加之前演示的上下文菜单目录(res/menu)。从这一点开始,您可以执行以下步骤以将多项选择添加到RecyclerView。项目将被称为RecyclerViewActionMode

如何做到这一点...

我们将结合之前菜谱中已经学到的几个概念,以使用RecyclerView实现多项选择。我们将首先添加菜单和相关代码,然后修改RecyclerView项以显示状态选择。最后,我们将修改RecyclerView适配器以支持点击通知,这将启动 Action Mode。以下是步骤:

  1. 打开strings.xml文件,并添加两个新的字符串资源用于菜单项,如下所示:
    <string name="delete_all">Delete All</string>
  1. res/menu文件夹中创建一个名为contextual_menu.xml的新文件,内容如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android" >
        <item android:id="@+id/delete_all"
            android:title="@string/delete_all" />
    </menu>
  1. 接下来,在res/drawable文件夹中添加一个名为item_selector.xml的新文件,内容如下所示:
    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android" >
        <item android:id="@+id/delete_all"
        android:title="@string/delete_all" />
    </menu>
  1. 打开res/layout中的item.xml文件,并在LinearLayout中添加以下行:
    android:background="@drawable/item_selector"
  1. 接下来,创建一个名为SelectMode的新 Java 文件,作为点击事件接口。代码如下:
    public interface SelectMode {
        void onSelect();
    }
  1. 现在打开MyAdapter文件,并将implements SelectMode添加到类中。最终结果如下:
    public class MyAdapter extends 
    RecyclerView.Adapter<MyAdapter.MyViewHolder>
        implements SelectMode { 
  1. 使用以下代码向类中添加onSelect方法:
    @Override
    public void onSelect() {
        if (mListener!=null) {
            mListener.onSelect();
        }
    }
  1. 向类中添加以下声明以保存所选项目的列表:
    private SparseArray<Boolean> selectedList = new SparseArray<>();
  1. 我们将在适配器中添加另一个方法来处理从 Action Mode 调用的实际delete方法:
public void deleteAllSelected() {
    if (selectedList.size()==0) { return; }
    for (int index = nameList.size()-1; index >=0; index--) {
        if (selectedList.get(index,false)) {
            remove(index);
        }
    }
    selectedList.clear();
}
  1. MyAdapter类的最后修改是替换现有的onClick()。最终代码如下:
    @Override
    public void onClick(View v) {
        holder.itemView.setSelected(!holder.itemView.isSelected());
        if (holder.itemView.isSelected()) {
           selectedList.put(position, true);
        } else {
            selectedList.remove(position);
        }
        onSelect();
    }
  1. 现在我们已经创建了菜单并更新了适配器,我们需要在MainActivity类中将它们全部连接起来。首先,修改MainActivity声明以实现SelectMode接口。最终代码如下:
    public class MainActivity extends AppCompatActivity
    implements SelectMode {
  1. 在类声明下方,添加以下两个变量声明:
    MyAdapter myAdapter;
    ActionMode mActionMode;
  1. 然后添加ActionMode回调声明:
    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. delete_all:
                    myAdapter.deleteAllSelected();
                    mode.finish();
                    return true;
                default:
                    return false;
            }
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            mActionMode = null;
        }
    };
  1. 我们需要存储MyAdapter引用,以便可以从ActionMode中调用它。为此,修改onCreate()方法中myAdapter实例化调用如下:
    myAdapter = new MyAdapter(list, this);
  1. 最终代码是实现onSelect方法以将适配器回调连接到 Action Mode。向MainActivity类中添加以下方法:
    @Override
    public void onSelect() {
        if (mActionMode != null) return;
        mActionMode = startSupportActionMode(mActionModeCallback);
    }
  1. 在设备或模拟器上运行程序以查看 CAB 的实际效果。

它是如何工作的...

如本菜谱的简介中提到的,多项目选择是RecyclerView最常被遗漏的功能,也是收到最多问题的一个。正如这个例子所示,即使是基本实现也需要许多步骤,但最终结果可以是满足你任务所需的精确实现。由于你将自行创建它,所以你不会局限于现有的功能集。

这个菜谱结合了从先前菜谱中学到的几个概念,包括以下内容:

  • RecyclerView

  • RecyclerView 适配器

  • 上下文菜单

  • 动作模式回调

为了将所有内容结合起来,我们创建了一个自定义接口,以便适配器能够在项目被选中时通知。MainActivity接收onSelect()事件来触发ActionMode。当用户点击删除所有菜单项并关闭 CAB 时,ActionMode菜单项会调用适配器。

这只是ActionMode可能实现的一种方式。我们可以通过长按、项目上的复选框或可能是一个菜单项来启动ActionMode。选择权在你。

还有更多...

如果你使用前面显示的代码运行了应用程序,一切都会如你所预期的那样工作。但是有一个问题。我们的例子中列表只有几个项目——可能甚至不足以允许滚动。然而,RecyclerView的目的在于在滚动时高效地处理许多项目。如果你向列表中添加更多的项目,足够多以至于可以滚动一两个屏幕,你就会看到问题。RecyclerView确实做了它所说的:回收视图。如果你选中了第一个项目,然后向下滚动,你会看到问题——未选中的项目被选中了。

发生的事情是一个常见问题,并让许多新接触RecyclerView的开发者感到困惑。因为视图正在被重用,所以它显示了上一个项目的状态。解决方案很简单:只需在绑定新项目时适当地设置状态。我们只需在MyAdapter类的onBindViewHolder()调用中设置初始状态即可修复前面的问题。向MyAdapter类的onBindViewHolder()方法中添加以下代码行:

holder.itemView.setSelected(selectedList.get(position,false));

正如你所见,我们通过检查列表中是否选中了项目来设置初始状态。

参见

创建弹出菜单

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

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

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

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

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

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为PopupMenu。使用默认的 Phone & Tablet 选项,并在添加活动到移动对话框中选择 Empty Activity。如本章第一项练习中详细说明的,创建一个菜单目录(res/menu)来存储菜单 XML。

如何实现...

我们首先创建一个 XML 菜单,在按钮按下时展开。展开弹出菜单后,我们通过传递回调来调用setOnMenuItemClickListener()处理菜单项选择。首先打开位于res/values文件夹中的strings.xml文件,然后按照以下步骤操作:

  1. 添加以下字符串:
    <string name="menu_reply">Reply</string> 
    <string name="menu_reply_all">Reply All</string> 
    <string name="menu_forward">Forward</string> 
  1. res/menu目录下创建一个名为menu_popup.xml的新文件,使用以下 XML:
    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
        <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>
  1. activity_main.xml中创建ImageButton以提供弹出菜单的锚视图。按照以下 XML 代码创建它:
    <ImageButton
        android:id="@+id/imageButtonReply"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@android:drawable/ic_menu_revert"
        android:onClick="showPopupMenu"/>
  1. 打开MainActivity.java并在类构造函数下方添加以下OnMenuItemClickListener
    private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener  
    = new
            PopupMenu.OnMenuItemClickListener() {
                @Override
                public boolean onMenuItemClick(MenuItem item) {
                    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;
                    }
                }
            };
  1. 最后的代码是处理按钮onClick()事件,如下所示:
    public void showPopupMenu(View view) {
        PopupMenu popupMenu = new PopupMenu(MainActivity.this,view);
        popupMenu.inflate(R.menu.menu_popup);
        popupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener);
        popupMenu.show();
    }
  1. 在设备或模拟器上运行程序以查看弹出菜单。

它是如何工作的...

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

关键是要理解 Android 中可用的每个菜单选项,以便你可以为特定场景选择正确的菜单类型。这将通过提供一致的用户体验并减少用户的学习曲线来帮助你的应用程序,因为用户已经熟悉了标准的操作方式。

第五章:Fragments

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

  • 创建和使用 Fragment

  • 在运行时添加和移除 Fragment

  • 在 Fragment 之间传递数据

  • 处理 Fragment 回退栈

简介

在对第二章,布局中的布局有了一定的理解之后,我们将更深入地探讨使用 Fragment 进行 UI 开发。Fragment 是将 UI 分割成更小部分以便于重用的一种方式。将 Fragment 视为具有自己的类、布局和生命周期的迷你活动。您不必在一个 Activity 布局中设计整个屏幕,可能还会在多个布局中重复功能,而是可以将屏幕分割成更小的、逻辑上合理的部分,并将它们转换为 Fragment。然后,您的 Activity 布局可以按需引用一个或多个 Fragment。

创建和使用 Fragment

Android 并不总是支持 Fragment。Android 的早期版本是为手机设计的,当时屏幕相对较小。直到 Android 开始在平板电脑上使用时,才需要将屏幕分割成更小的部分。Android 3.0 引入了Fragments类和 Fragment 管理器。

随着新类的出现,也出现了 Fragment 生命周期。Fragment 生命周期与在第一章,活动中引入的活动生命周期相似,因为大多数事件都与活动生命周期并行。

这里是对主要回调的简要概述:

  • onAttach(): 当 Fragment 与 Activity 关联时调用。

  • onCreate(): 当 Fragment 首次创建时调用。

  • onCreateView(): 当 Fragment 即将首次显示时调用。

  • onActivityCreated(): 当相关 Activity 被创建时调用。

  • onStart(): 当 Fragment 将变为用户可见时调用。

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

  • onPause(): 当 Fragment 首次暂停时调用。用户可能会返回到 Fragment,但这是您应该持久化任何用户数据的地方。

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

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

  • onDetach(): 当 Fragment 不再与 Activity 关联时调用。

对于我们的第一个练习,我们将创建一个新的由标准Fragment类派生的 Fragment。但我们可以从以下几个其他 Fragment 类中派生,包括以下内容:

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

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

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

在这个菜谱中,我们将通过创建一个由Fragment类派生的基本 Fragment,并将其包含在 Activity 布局中来进行操作。

准备工作

在 Android Studio 中创建一个新的项目,命名为 CreateFragment。使用默认的 Phone & Tablet 选项,并在 Add an Activity to Mobile 对话框中选择 Empty Activity

如何操作...

在此配方中,我们将创建一个新的 Fragment 类及其相应的布局文件。然后,我们将片段添加到 Activity 布局中,以便在活动启动时可见。

创建和显示新片段的步骤如下:

  1. 使用以下 XML 创建一个新的布局文件 fragment_one.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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> 
  1. 创建一个名为 FragmentOne.java 的新 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);
    }
}
  1. 打开 activity_main.xml 文件,并用以下 <fragment> 元素替换现有的 <TextView> 元素:
<fragment
    android:name="com.packtpub.createfragment.FragmentOne"
    android:id="@+id/fragment"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerVertical="true"
    android:layout_centerHorizontal="true" 
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 在设备或模拟器上运行程序。

它是如何工作的...

我们首先创建一个新的类,与 Activity 类似。在此配方中,我们只创建对 onCreateView() 方法的覆盖,以加载我们的片段布局。但是,就像 Activity 事件一样,我们可以根据需要覆盖其他事件。一旦创建了新的片段,我们就将其添加到 Activity 布局中。由于原始 Activity 类是在片段存在之前创建的,因此它们不支持片段。这就是为什么,除非另有说明,本书的所有示例都扩展自 AppCompatActivity。(如果您使用了 Android Studio 新项目向导,则默认情况下 MainActivity 扩展自 AppCompatActivity。)

还有更多...

在此配方中,我们只创建了一个简单的片段来教授片段的基本原理。但这是一个指出片段强大功能的好时机。如果我们正在创建多个片段(通常我们是这样做的,因为使用片段的目的),在创建活动布局时(如步骤 4 所示),我们可以使用 Android 资源文件夹创建不同的布局配置。纵向布局可能只有一个片段,而横向布局可能有两个或更多。Master/Detail 布局通常使用片段,因此只需要为每个屏幕部分设计一次并编码,然后根据需要将其包含在布局中。

相关内容

  • 关于 Master/Detail 模式的更多信息,请参阅本章后面的 在片段之间传递数据 配方。

在运行时添加和删除片段

在布局中定义片段,就像我们在前面的配方中所做的那样,称为静态片段,它不允许在运行时更改片段。而不是使用 <fragment> 元素,我们将创建一个容器来容纳片段,然后在 Activity 的 onCreate() 方法中动态创建片段。

FragmentManager 提供了在运行时使用 FragmentTransaction 添加、删除和更改片段的 API。一个 Fragment 事务包括以下步骤:

  1. 开始事务

  2. 执行一个或多个操作

  3. 提交事务

此配方将通过在运行时添加和删除片段来演示片段管理器。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为:RuntimeFragments。使用默认的 Phone & Tablet 选项,并在 Add an Activity to Mobile 对话框中选择 Empty Activity。

如何做到这一点...

为了演示添加和删除片段,我们首先需要创建片段,我们将通过扩展 Fragment 类来实现。在创建了新的片段之后,我们需要修改主活动的布局以包含片段容器。从那里,我们只需添加处理片段事务的代码。以下是步骤:

  1. 创建一个名为 fragment_one.xml 的新布局文件,并包含以下 XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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> 
  1. 第二个布局文件 fragment_two.xml 几乎相同,唯一的区别是文本:
android:text="Fragment Two" 
  1. 创建一个名为 FragmentOne.java 的新 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);
    }
} 
  • 按照以下方式从支持库中导入:
import android.support.v4.app.Fragment;
  1. 创建第二个 Java 类 FragmentTwo,并包含以下代码:
public class FragmentTwo extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_two,
                container, false);
    }
}
  • 如前所述,从支持库中导入:
import android.support.v4.app.Fragment;
  1. 现在我们需要在主活动布局中添加一个容器和一个按钮。按照以下方式更改 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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>
  1. 在创建了片段并将容器添加到布局中后,我们现在可以编写操作片段的代码。打开 MainActivity.java 并在类构造函数下方添加以下代码:
FragmentOne mFragmentOne;
FragmentTwo mFragmentTwo;
int showingFragment=0;
  1. 在现有的 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;
  • 从支持库中导入:
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
  1. 最后需要添加的代码处理片段切换,由按钮调用:
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();
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

此菜谱的大多数步骤都涉及设置片段。一旦声明了片段,我们就在 onCreate() 方法中创建它们。虽然代码可以压缩成一行,但为了便于阅读和理解,这里以长形式展示。

首先,我们获取 FragmentManager 以开始 FragmentTransaction。一旦我们有了 FragmentTransaction,我们就使用 beginTransaction() 开始事务。事务中可以发生多个操作,但这里我们只需要 add() 我们初始的片段。我们调用 commit() 方法来最终化事务。

现在您已经了解了片段事务,以下是 onCreate() 的简洁版本:

getSupportFragmentManager().beginTransaction().add(R.id.frameLayout, mFragmentOne).commit();

我们的 switchFragment() 方法基本上执行相同的片段事务。我们不是调用 add() 方法,而是调用带有现有片段的 replace() 方法。我们通过 showingFragment 变量跟踪当前片段,以便知道下一个要显示的片段。我们也不限于在两个片段之间切换。如果我们需要额外的片段,我们只需创建它们即可。

还有更多...

在第一章的切换活动食谱中,我们讨论了返回栈。大多数用户都期望返回键可以向后移动通过“屏幕”,他们不知道或不在乎那些屏幕是活动还是片段。幸运的是,Android 通过在调用commit()之前添加对addToBackStack()的调用,使得向返回栈添加片段变得非常简单。

当一个片段被移除或替换而没有添加到返回栈时,它将被立即销毁。如果它被添加到返回栈,它将被停止,如果用户返回到该片段,它将被重新启动,而不是重新创建。

参见

  • 关于管理片段返回栈的更多信息,请参阅本章后面的处理片段返回栈食谱。

在片段之间传递数据

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

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

现在问题变成了:片段如何与活动通信?答案是使用接口。你可能已经熟悉接口,因为这是视图如何将事件回传给活动的方式。最常见的一个例子是按钮的onClick()接口。

在这个食谱中,我们将创建两个片段来演示通过宿主活动从一个片段向另一个片段传递数据。我们还将基于之前食谱中学到的知识,包括两个不同的活动布局——一个用于纵向,一个用于横向。在纵向模式下,活动将根据需要交换片段。以下是应用程序首次在纵向模式下运行的截图:

截图

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

图片

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

图片

由于 Master/Detail 模式通常涉及一个主列表,我们将利用 ListFragment(在 创建和使用 Fragment 部分中提到)。当列表中的项目被选中时,项目文本(在我们的例子中是国家名称)将通过宿主活动发送到详细 Fragment。

准备工作

在 Android Studio 中创建一个新的项目,命名为 FragmentCommunication。使用默认的 Phone & Tablet 选项,并在 Add an Activity to Mobile 对话框中选择 Empty Activity。

如何做到这一点...

为了完全展示工作的 Fragment,我们需要创建两个 Fragment。第一个 Fragment 将扩展 ListFragment,因此它不需要布局。我们将更进一步,为我们的 Activity 创建纵向和横向布局。对于纵向模式,我们将交换 Fragment,对于横向模式,我们将同时显示两个 Fragment。

当输入此代码时,Android Studio 将提供两种不同的库导入选项。由于新项目向导自动引用了 AppCompat 库,我们需要使用支持库 API 而不是框架 API。尽管非常相似,以下代码使用了支持 Fragment API。

这里是步骤,从第一个 Fragment 开始:

  1. 创建一个名为 MasterFragment 的新 Java 类,并修改它使其扩展 ListFragment,如下所示:
public class MasterFragment extends ListFragment 
  • 从以下库中导入:
android.support.v4.app.ListFragment 
  1. MasterFragment 类中创建以下接口:
public interface OnMasterSelectedListener {
    public void onItemSelected(String countryName);
}
  1. 使用以下代码设置接口回调监听器:
private OnMasterSelectedListener mOnMasterSelectedListener=null;

public void setOnMasterSelectedListener(OnMasterSelectedListener listener) {
    mOnMasterSelectedListener=listener;
}
  1. 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());
            }
        }
    });
}
  1. 接下来,我们需要创建 DetailFragment,从布局开始。创建一个名为 fragment_detail.xml 的新布局文件,其 XML 如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    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>
  1. 创建一个名为 DetailFragment 的新 Java 类,它扩展自 Fragment,如下所示:
public class DetailFragment extends Fragment 
  • 从以下库中导入:
android.support.v4.app.Fragment 
  1. 将以下常量添加到类中:
public static String KEY_COUNTRY_NAME="KEY_COUNTRY_NAME"; 
  1. 如下重写 onCreateView() 方法:
@Override
public View onCreateView(LayoutInflater inflater, 
                         ViewGroup container, 
                         Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_detail, container, false);
}
  1. 编写 onViewCreated() 如下:
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    Bundle bundle = getArguments();

    if (bundle != null && bundle.containsKey(KEY_COUNTRY_NAME)) {
        showSelectedCountry(bundle.getString(KEY_COUNTRY_NAME));
    }
}
  1. 对于此 Fragment 的最后一步,当接收到选中的国家名称时更新 TextView。向类中添加以下方法:
public void showSelectedCountry(String countryName) {
    ((TextView)getView().findViewById(R.id.textViewCountryName)).setText(countryName);
}
  1. 现有的 activity_main.xml 布局将处理纵向模式布局。删除现有的 <TextView> 并替换为以下 <FrameLayout>
<FrameLayout
    android:id="@+id/frameLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="8dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 对于横向布局,在 res 文件夹中创建一个名为 layout-land 的新目录。最终结果将是 res/layout-land

如果您看不到新的 res/layout-land 目录,请从 Android 视图切换到项目视图。

  1. res/layout-land中创建一个新的activity_main.xml布局,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="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>
  1. 最后的步骤是将MainActivity设置起来以处理 Fragment。打开MainActivity.java文件,并添加以下类变量以跟踪单/双面板:
boolean mDualPane;
  1. 接下来,按照以下方式修改onCreate()
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    MasterFragment masterFragment = null;
    FrameLayout frameLayout = findViewById(R.id.frameLayout);
    if (frameLayout != null) {
        mDualPane = 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 {
        mDualPane = 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);
        }
    });
}
  1. 最后要添加的代码是sendCountryName()方法,它处理将国家名称发送到DetailFragment
private void sendCountryName(String countryName) {
    DetailFragment detailFragment;
    if (mDualPane) {
        //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();
    }
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

我们首先创建MasterFragment。在我们使用的 Master/Detail 模式中,这通常代表一个列表,所以我们通过扩展ListFragment来创建一个列表。ListFragmentListActivity的 Fragment 等价物。除了扩展自 Fragment 之外,它基本上是相同的。

如菜谱介绍中所述,我们不应尝试直接与其他 Fragment 通信。

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

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

  • 在创建时传递国家名称到参数包中

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

当活动创建 Fragment 时,它也会创建一个包含我们想要发送的数据的 bundle。在这里,我们使用在第 7 步中定义的KEY_COUNTRY_NAME添加国家名称。我们在onViewCreated()中使用getArguments()检索这个 bundle。如果在 bundle 中找到该键,它将通过showSelectedCountry()方法提取并显示。这是活动如果 Fragment 已经可见(在双面板布局中)将直接调用的相同方法。

这个菜谱的大部分工作都在活动中。我们创建了两个布局:一个用于纵向,一个用于横向。当处于横向方向时,Android 将选择在第 12 步中创建的res/layout-land目录中的横向布局。这两个布局都使用一个<FrameLayout>占位符,类似于之前的练习。我们在onCreate()sendCountryName()中管理 Fragment。

onCreate()中,我们通过检查当前布局是否包含frameLayout视图来设置mDualPane标志。如果找到frameLayout(意味着它不是 null),那么我们只有一个面板,因为frameLayout仅在纵向布局中定义。如果没有找到frameLayout,那么我们有两个<FrameLayout>元素:一个用于MasterFragment,另一个用于DetailFragment

onCreate() 方法中,我们最后要做的事情是通过创建匿名回调来设置 MasterFragment 监听器,该回调将国家名称传递给 sendCountryName() 方法。sendCountryName() 方法是数据实际上传递给 DetailFragment 的地方。如果我们处于纵向(或单面板)模式,我们需要创建 DetailFragment 并替换现有的 MasterFragment。这就是我们创建包含国家名称的 bundle 并调用 setArguments() 的地方。注意我们在提交事务之前调用 addToBackStack()?这允许返回键将用户带回列表(MasterFragment)。如果我们处于横向模式,DetailFragment 已经可见,所以我们直接调用 howSelectedCountry() 公共方法。

还有更多...

MasterFragment 中,在发送 onItemSelected() 事件之前,我们使用以下代码检查监听器是否为空:

if (mOnMasterSelectedListener != null) 

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

本配方的目标是演示在片段之间通信的正确模式(通过使用接口)以及如何传递数据。我们使用了 ListView 片段,因为它使编写此示例更容易,但在实际应用中,可能最好使用 RecyclerViewRecyclerView 没有预制的 Fragment 类(或 Activity 类),因此你需要自己实现,但这与前面章节中显示的示例没有区别。

参见

  • 对于 RecyclerView 示例,请参阅 第二章 的 RecyclerView replaces ListView 部分,布局,以及 第四章 的 Using Contextual Batch Mode with RecyclerView 部分,菜单和操作模式

  • 有关资源目录的更多信息,请参阅 第三章 的 Selecting themes based on the Android version 部分,视图、小部件和样式

处理 Fragment 返回栈

在之前的几个配方中,提到你应该在 Fragment 事务中调用 addToBackStack() 方法,以便 Android 能够维护一个 Fragment 返回栈。这是第一步,但可能不足以提供丰富的用户体验。在这个配方中,我们将探索另外两个回调:onBackPressed()onBackStackChanged()。正如你将看到的,通过实现这些回调,你的应用程序可以为 Fragment 返回栈提供特定的行为。onBackPressed() 回调允许应用程序检查返回栈状态并提供自定义行为,例如在适当的时候关闭应用程序。

当实际的返回栈发生变化时(例如,当从返回栈中弹出 Fragment 时),会调用 onBackStackChanged() 回调。通过重写此回调,您的应用可以检查当前 Fragment 并根据需要更新 UI(例如,主页 键的返回箭头)。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 FragmentBackStack。使用默认的 Phone & Tablet 选项,并在添加活动到移动对话框中选择 Empty Activity。

如何实现...

为了演示处理 Fragment 返回栈,我们将创建两个带有 Next 按钮的 Fragment 来创建返回栈。有了这个设置,我们将实现 onBackPressed() 回调,当用户到达顶部 Fragment 时退出应用。我们将使用支持库中的 Fragment Manager,所以当提示导入库时,请确保选择支持库版本。我们需要两个布局文件——每个 Fragment 一个——以及两个 Fragment 类。以下是详细步骤:

  1. 创建一个新的布局文件 fragment_one.xml,其 XML 如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Fragment One"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>
  1. 创建第二个 Fragment 布局文件 fragment_two.xml,其 XML 与上面相同,但更改以下文本属性:
android:text="Fragment Two"
  1. 在创建布局文件后,是时候创建片段的类了。创建一个新的 Java 类 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);
    }
} 
  1. 创建第二个名为 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);
    }
}
  1. 现在,我们需要将容器和按钮添加到 MainActivity 布局中。按如下方式更改 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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/buttonNext"
        android:layout_alignParentTop="true">
    </FrameLayout>
    <Button
        android:id="@+id/buttonNext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Next"
        android:layout_alignParentBottom="true"
        android:layout_centerInParent="true"/>
</RelativeLayout>
  1. 在创建 Fragment 并将容器添加到布局后,我们现在可以编写操作 Fragment 的代码。打开 MainActivity.java 并在类构造函数下方添加以下代码:
Button mButtonNext;
  1. 将以下代码添加到现有的 onCreate() 方法中,在 setContentView() 下方:
mButtonNext = findViewById(R.id.buttonNext);
mButtonNext.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction =
                fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.frameLayout,  new FragmentTwo());
        fragmentTransaction.addToBackStack(null);
        fragmentTransaction.commit();
        mButtonNext.setVisibility(View.INVISIBLE);
    }
});

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction =
        fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.frameLayout,  new FragmentOne());
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
  1. 最后要实现的方法是 onBackPressed() 回调:
@Override
public void onBackPressed() {
    if(getSupportFragmentManager().getBackStackEntryCount() == 2 ) {
        super.onBackPressed();
        mButtonNext.setVisibility(View.VISIBLE);
    } else {
        finish();
    }
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

大多数步骤与之前讨论的 在运行时添加和删除 Fragment 的食谱 相似,直到第 8 步。前七步只是设置应用以创建用于演示的 Fragment。在第 8 步中,我们实现了 onBackPressed() 回调。这就是我们为特定情况编写代码的地方。对于这个示例,我们只需要再次使 Next 按钮可见。

还有更多...

在处理返回栈的基本知识覆盖后,现在是时候讨论另一个回调:onBackStackChanged()。这是您可以在栈发生变化时实现自定义行为的地方。一个常见的例子是将主页图标更改为返回箭头。当我们设置父属性(在 AndroidManifest 中)时,我们自动获得 Activity 的这种行为,但 Android 并不会为 Fragment 做这件事。如果我们想在 FragmentTwo 上有一个返回箭头,请将此行代码添加到 NextButton 的 onClick() 中:

getSupportActionBar().setDisplayHomeAsUpEnabled(true);

如果你现在运行应用,当你进入FragmentTwo时,你会看到返回箭头。问题是,返回箭头实际上并没有做任何事情。你可能注意到的下一个问题是,如果你使用返回键,当你返回到FragmentOne时,你仍然会看到返回箭头。

为了使返回箭头生效,将以下代码添加到MainActivity中:

@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
    if (menuItem.getItemId() == android.R.id.home) {
            onBackPressed();
            return true;
    } else {
        return super.onOptionsItemSelected(menuItem);
    }
}

现在应用将响应返回箭头,并将其与返回键同等对待。那么第二个问题呢?主页图标仍然显示返回箭头。这就是我们可以使用onBackStackChanged()回调的地方。我们不需要像之前那样修改 NextButton 的onClick()方法,我们可以将所有代码放入onBackStackChanged()中。

要实现这个功能,我们需要在类定义中实现OnBackStackChangedListener接口。将MainActivity的声明修改如下:

public class MainActivity extends AppCompatActivity
        implements FragmentManager.OnBackStackChangedListener {

然后将此行代码添加到onCreate()方法中(在setContentView()下方)以添加监听器:

getSupportFragmentManager().addOnBackStackChangedListener(this);

现在,我们可以实现onBackStackChanged()回调函数:

@Override
public void onBackStackChanged() {
    Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.frameLayout);
    if (fragment instanceof FragmentOne) {
        getSupportActionBar().setDisplayHomeAsUpEnabled(false);
    } else if (fragment instanceof FragmentTwo) {
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    }
}

现在当你运行应用并进入FragmentTwo时,你会看到返回箭头。你可以点击返回箭头图标或使用返回键返回到第一个屏幕。多亏了onBackStackChanged()回调,当你处于FragmentOne时,你不会看到返回箭头。

第六章:主屏幕小部件、搜索和系统 UI

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

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

  • 创建主屏幕小部件

  • 将搜索添加到操作栏

  • 显示你的应用全屏

  • 锁屏快捷方式

简介

在上一章中了解了片段之后,我们准备扩展对小部件的讨论。在第三章,“视图、小部件和样式”中,我们讨论了如何将小部件添加到自己的应用中。现在,我们将探讨如何创建一个 App Widget,以便用户可以在他们的主屏幕上添加应用。

本章剩余的食谱将探讨系统 UI 选项。有一个食谱是使用 Android SearchManager API 将搜索选项添加到操作栏。另一个食谱将探讨全屏模式以及改变系统 UI 的几种额外变体。最后一个食谱将展示 Android O(API 26)中引入的新锁屏快捷方式。

在主屏幕上创建快捷方式

这个食谱解释了如何在用户的主屏幕上创建链接或创建快捷方式。为了不过于突兀,通常最好将其作为用户可以启动的选项,例如在设置中。

以下是一个显示我们主屏幕快捷方式的屏幕截图:

如您所见,这只是一个到应用的快捷方式。下一个食谱将通过创建主屏幕(AppWidget)来进一步深入。

准备工作

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

如何做到...

为了创建快捷方式,应用必须具有INSTALL_SHORTCUT权限。有了适当的权限,只需调用一个带有应用属性的 intent 即可。以下步骤:

  1. 打开AndroidManifest文件并添加以下权限:
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
  1. 接下来,打开activity_main.xml并将现有的 TextView 替换为以下按钮:
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Create Shortcut"
    android:onClick="createShortcut"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 将以下方法添加到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);
}
  1. 在设备或模拟器上运行程序。注意,每次你按下按钮,应用都会在主屏幕上创建一个快捷方式。

它是如何工作的...

一旦设置了适当的权限,这便是一个相当直接的任务。当按钮被点击时,代码会创建两个 intent。第一个 intent 向操作系统广播你想要创建快捷方式。第二个 intent 是当图标被按下时启动应用的 intent。一个需要记住的重要考虑因素是,主屏幕各不相同,可能不支持INSTALL_SHORTCUT intent。

更多...

如果你还想删除快捷方式,你需要以下权限:

<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />

而不是使用INSTALL_SHORTCUT动作,你可以设置以下动作:

com.android.launcher.action.UNINSTALL_SHORTCUT 

创建主屏幕小部件

在深入到创建 App Widget 的代码之前,让我们先了解基础知识。有三个必需组件和一个可选组件:

  • AppWidgetProviderInfo文件:这是一个 XML 资源(稍后描述)

  • AppWidgetProvider类:这是一个 Java 类

  • View layout文件:这是一个标准的布局 XML 文件,有一些限制(稍后解释)

  • App Widget 配置 Activity(可选):这是一个操作系统在放置小部件时启动的活动,以提供配置选项

AppWidgetProvider必须在AndroidManifest文件中声明。由于AppWidgetProvider是基于广播接收器的辅助类,它使用<receiver>元素在 Manifest 中声明。以下是一个 Manifest 条目示例:

<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>

元数据指向放置在res/xml目录中的AppWidgetProviderInfo文件。以下是一个示例AppWidgetProviderInfo.xml文件:

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="0"
    android:initialLayout="@layout/widget"
    android:resizeMode="none"
    android:widgetCategory="home_screen">
</appwidget-provider>

以下是对可用属性的简要概述:

  • minWidth: 放置在主屏幕上的默认宽度

  • minHeight:放置在主屏幕上的默认高度

  • updatePeriodMillis:它是onUpdate()轮询间隔的一部分(以毫秒为单位)

  • initialLayout:App Widget 布局

  • previewImage(可选):浏览 App Widget 时显示的图像

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

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

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

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

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

AppWidgetProvider类扩展了BroadcastReceiver类,这就是为什么在 Manifest 中声明 AppWidget 时使用<receiver>元素。由于它是BroadcastReceiver,该类仍然接收操作系统广播事件,但辅助类会过滤这些事件,只保留适用于 App Widget 的事件。AppWidgetProvider类公开以下方法:

  • onUpdate():当首次创建和指定的时间间隔时调用。

  • onAppWidgetOptionsChanged(): 当首次创建和任何时间大小改变时调用。

  • onDeleted(): 任何时间移除小部件时都会调用。

  • onEnabled(): 当第一个小部件被放置时调用(添加第二个和后续小部件时不会调用)。

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

  • onReceive(): 每当接收到事件时都会调用此方法,包括前一个事件。通常不需要重写,因为默认实现只发送适用的事件。

最后一个必需组件是布局。App Widget 使用 Remote View,它只支持可用布局的子集:

  • AdapterViewFlipper

  • FrameLayout

  • GridLayout

  • GridView

  • LinearLayout

  • ListView

  • RelativeLayout

  • StackView

  • ViewFlipper

它支持以下小部件:

  • AnalogClock

  • Button

  • Chronometer

  • ImageButton

  • ImageView

  • ProgressBar

  • TextClock

  • TextView

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

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

图片

图片的目的是展示如何将小部件添加到主屏幕

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

以下是添加到主屏幕后小部件的截图:

图片

准备工作

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

如何操作...

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

这里是详细的步骤:

  1. res/layout中创建一个名为widget.xml的新文件,使用以下 XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <AnalogClock
        android:id="@+id/analogClock"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>
  1. 在资源目录中创建一个名为XML的新目录。最终结果将是res/xml

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

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="40dp"
    android:minHeight="40dp"
    android:updatePeriodMillis="0"
    android:initialLayout="@layout/widget"
    android:resizeMode="none"
    android:widgetCategory="home_screen">
</appwidget-provider>

如果您看不到新的 XML 目录,请从项目面板下拉菜单中切换到 Android 视图。

  1. 创建一个名为HomescreenWidgetProvider的新 Java 类,并从AppWidgetProvider扩展。

  2. 将以下onUpdate()方法添加到HomescreenWidgetProvider类中:

@Override
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);
    }
}
  1. 使用以下 XML 声明在<application>元素中向 AndroidManifest 添加HomescreenWidgetProvider
<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>
  1. 在设备或模拟器上运行程序。在首次运行应用程序后,小部件将可供添加到主屏幕。

工作原理...

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

XML 资源目录用于存储AppWidgetProviderInfo,它定义了默认的小部件设置。配置设置决定了在浏览可用小部件时小部件的显示方式。我们在这个配方中使用非常基本的设置,但它们可以很容易地扩展以包括其他功能,例如显示预览图像

一个功能正常的 widget 和尺寸选项。updatePeriodMillis属性设置更新频率。由于更新会唤醒设备,这需要在最新的数据和电池寿命之间做出权衡。(这就是可选的设置 Activity 有用的地方,因为它允许用户做出决定。)

AppWidgetProvider类是我们处理由updatePeriodMillis轮询触发的onUpdate()事件的场所。我们的示例不需要任何更新,所以我们把轮询设置为零。当最初放置 widget 时,更新仍然会被调用。onUpdate()是我们设置挂起 intent 以在按下时钟时打开我们的 app 的地方。

由于onUpdate()方法可能是 AppWidgets 中最复杂的一部分,我们将对此进行详细解释。首先,值得注意的是,onUpdate()方法将在每个由该提供程序创建的 widget 的轮询间隔内只发生一次。(所有额外创建的 widget 将使用与第一个创建的 widget 相同的周期。)这解释了 for 循环,因为我们需要它来遍历所有现有的 widget。这就是我们创建挂起 intent 的地方,当时钟 widget 被按下时,它会调用我们的 app。如前所述,AppWidget 是一个远程视图。因此,为了获取布局,我们使用我们的完全限定包名和布局 ID 调用RemoteViews()。一旦我们有了布局,我们就可以使用setOnClickPendingIntent()将挂起 intent 附加到时钟视图。我们调用名为updateAppWidget()AppWidgetManager来启动我们所做的更改。

使所有这些工作完成的最后一步是在 Android Manifest 中声明 widget。我们使用<intent-filter>标识我们想要处理的操作。大多数 App Widgets 可能希望处理更新事件,正如我们的那样。声明中需要注意的另一个项目是以下行:

<meta-data android:name="android.appwidget.provider" 

    android:resource="@xml/appwidget_info" />

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

还有更多...

添加 App Widget 配置 Activity 可以让你的 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.appwidget.AppWidgetConfigureActivity"

configure属性需要完全限定的包名,因为这个 Activity 将从应用程序外部被调用。

记住,当使用配置 Activity 时,onUpdate()方法不会被调用。配置 Activity 负责处理任何所需的初始设置。

另请参阅

将搜索添加到动作栏

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

以下截图显示了动作栏中搜索图标的初始外观:

图片

以下截图显示了按下搜索选项时的展开情况:

图片

如果你想在你的应用程序中添加搜索功能,这个菜谱将指导你设置用户界面并正确配置搜索管理器 API 的步骤。

准备工作

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

如何操作...

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

  1. 添加以下字符串资源:
<string name="search_title">Search</string>
<string name="search_hint">Enter text to search</string>
  1. 创建菜单目录:res/menu

  2. res/menu中创建一个名为menu_search.xml的新菜单资源,使用以下 XML:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <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>
  1. 打开ActivityMain并添加以下onCreateOptionsMenu()以填充菜单并设置搜索管理器:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.menu_search, menu);
    SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    MenuItem searchItem = menu.findItem(R.id.menu_search);
    SearchView searchView = (SearchView) searchItem.getActionView();
    searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
    return true;
}
  1. 创建一个新的 XML 资源目录:res/xml

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

<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_name"
    android:hint="@string/search_hint" />
  1. 使用以下 XML 创建一个名为activity_search_result.xml的新布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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> 
  1. 在项目中添加一个新的空活动,命名为SearchResultActivity

  2. 在类中添加以下变量:

TextView mTextViewSearchResult; 
  1. onCreate()改为加载我们的布局,设置 TextView,并检查 QUERY 动作:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_search_result);
    mTextViewSearchResult = findViewById(R.id.textViewSearchResult);
    if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
        handleSearch(getIntent().getStringExtra(SearchManager.QUERY));
    }
}
  1. 添加以下方法来处理搜索:
private void handleSearch(String searchQuery) {
    mTextViewSearchResult.setText(searchQuery);
}
  1. 用户界面和代码现在已经完成,我们只需要在AndroidManifest中正确连接一切。以下是完整的清单,包括两个活动:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.packtpub.searchview">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <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>
  1. 在设备或模拟器上运行应用程序。输入搜索查询并点击搜索按钮(或按Enter)。将显示SearchResultActivity,显示输入的搜索查询。

它是如何工作的...

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

我们首先为搜索视图(在步骤 6 中声明)创建字符串资源。

在第 3 步中,我们创建菜单资源,就像我们之前多次做的那样。一个不同之处在于我们使用app命名空间为showAsActionactionViewClass属性。Android OS 的早期版本不包括 Android 命名空间中的这些属性,这就是我们创建app命名空间的原因。这为将新功能引入较旧的 Android OS 版本提供了一种方式。

在第 4 步中,我们使用支持库 API 设置SearchManager

第 6 步是我们定义可搜索的 XML 资源,该资源由搜索管理器使用。唯一必需的属性是标签,但建议添加一个提示,以便用户知道应该在字段中输入什么。

android:label必须与应用程序名称或活动名称匹配,并且必须使用字符串资源(因为它不适用于硬编码的字符串)。

第 7 步到第 11 步是为SearchResultActivity的。调用第二个活动不是SearchManager的要求,但通常这样做是为了提供一个活动来处理应用程序中启动的所有搜索。

如果在这个时候运行应用程序,你会看到搜索图标,但什么都不会工作。第 12 步是我们将所有内容整合到AndroidManifest文件中的地方。首先要注意的是以下内容:

<meta-data android:name="android.app.default_searchable"
    android:value=".SearchResultActivity" />

注意这一点是在<application>元素中,而不是在任何一个<activity>元素中。通过在<application>级别定义它,它将自动应用于所有<activities>。如果我们将其移动到MainActivity元素,它在我们这个例子中的行为将完全相同。

你可以在<application>节点中为你的应用程序定义样式,同时仍然可以在<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 模式的完成,您对搜索结果的处理将具体取决于您的应用需求。根据您的应用,您可能需要搜索本地数据库或可能是一个网络服务。

参见

要将您的搜索扩展到互联网,请参阅第十三章中的网络查询,电话、网络和互联网

显示您的应用全屏

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。使用默认的Phone & Tablet选项,并在提示活动类型时选择Empty Activity。在选择最小 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);
}
  1. 添加以下方法以显示系统 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);
}
  1. 添加以下类变量:
private GestureDetectorCompat mGestureDetector;
  1. 在类级别添加以下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;
    }
}
  1. 用以下代码覆盖onTouchEvent()回调:
@Override
public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    return super.onTouchEvent(event);
}
  1. 将以下代码添加到onCreate()方法中,以设置GestureListener并隐藏系统 UI:
mGestureDetector = new GestureDetectorCompat(this, new GestureListener());
hideSystemUi();
  1. 在设备或模拟器上运行应用程序。轻触屏幕将切换系统 UI。根据你的 Android OS 版本,你可以从底部向上滑动或从顶部向下滑动以显示系统 UI。

它是如何工作的...

我们在 showSystemUI()hideSystemUI() 方法中使用适当的标志调用 setSystemUiVisibility() 来设置应用程序窗口状态。我们设置的(和未设置的)标志控制着哪些内容可见,哪些内容隐藏。当我们设置可见性而不使用 SYSTEM_UI_FLAG_IMMERSIVE 标志时,实际上我们禁用了沉浸模式。

如果我们只想隐藏系统 UI,我们只需将 hideSystemUI() 添加到 onCreate() 中即可完成。问题是它不会保持隐藏。一旦用户离开沉浸模式,它将保持在常规显示模式。这就是我们创建 GestureListener 的原因。(我们将在第九章,使用触摸屏和传感器中再次讨论手势。)由于我们只想响应 onSingleTapUp() 手势,所以我们不实现完整的手势范围。当检测到 onSingleTapUp 时,我们切换系统 UI。

还有更多...

让我们看看一些其他可以执行的重要任务。

粘性沉浸

如果我们想要系统 UI 自动保持隐藏,还有一个选项可以使用。我们不是使用 SYSTEM_UI_FLAG_IMMERSIVE 来隐藏 UI,而是使用 SYSTEM_UI_FLAG_IMMERSIVE_STICKY

降低系统 UI 的亮度

如果你只需要减少导航栏的可见性,还可以使用 SYSTEM_UI_FLAG_LOW_PROFILE 来降低 UI 的亮度。

使用此标志与 Immersive 模式标志相同的 setSystemUiVisibility() 调用:

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

使用 0 作为参数调用 setSystemUiVisibility() 以清除所有标志:

getWindow().getDecorView().setSystemUiVisibility(0); 

将操作栏设置为叠加层

如果你只需要隐藏或显示操作栏,请使用以下方法:

getActionBar().hide(); 

getActionBar().show(); 

这种方法的一个问题是,每次调用任一方法时系统都会调整布局的大小。相反,你可能希望考虑使用主题选项来使系统 UI 表现为一个叠加层。要启用叠加模式,请将以下内容添加到主题中:

<item name="android:windowActionBarOverlay">true</item> 

透明系统栏

以下两个主题可以启用透明设置:

Theme.Holo.NoActionBar.TranslucentDecor 

Theme.Holo.Light.NoActionBar.TranslucentDecor 

如果你正在创建自己的主题,请使用以下主题设置:

<item name="android:windowTranslucentNavigation">true</item> 

<item name="android:windowTranslucentStatus">true</item> 

参见

更多关于处理手势的信息,请参阅第九章,使用触摸屏和传感器

第七章:数据存储

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

  • 存储简单数据

  • 将文本文件读取和写入到内部存储

  • 将文本文件读取和写入到外部存储

  • 在你的项目中包含资源文件

  • 创建和使用 SQLite 数据库

  • 使用 Loader 在后台访问数据

  • 使用作用域目录访问外部存储

简介

由于大多数应用程序,无论大小,都需要保存数据——从默认用户选择到用户账户——Android 提供了许多选项。从保存一个简单的值到使用 SQLite 创建完整数据库,存储选项包括以下内容:

  • 共享首选项:简单的名称/值对

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

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

  • SQLite 数据库:私有数据(可以通过内容提供者使其公开)

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

使用内部和外部存储都有利弊。我们将在此列出一些差异,以帮助您决定哪个选项最适合您的需求:

内部存储

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

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

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

外部存储

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

  • 文件对用户(和其他应用程序)是可访问的,无需 root 访问权限

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

在本章中,我们将演示如何使用共享首选项、内部和外部存储以及 SQLite 数据库。对于云存储,请参阅第十二章的互联网食谱,Telephony, Networks

存储简单数据

存储简单数据是一个常见需求,Android 通过使用首选项 API 使其变得简单。它不仅限于用户首选项;您可以使用名称/值对存储任何原始数据类型。

我们将演示如何从EditText保存一个名字并在应用程序启动时显示它。以下截图显示了应用程序第一次启动且没有保存名字时的样子:

这是保存名字后的样子示例:

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为Preferences。使用默认的Phone & Tablet选项,并在Add an Activity to Mobile对话框中选择Empty Activity

如何做到这一点...

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

  1. 用以下新视图替换现有的TextView
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<EditText
    android:id="@+id/editTextName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:hint="Enter your name"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Save"
    app:layout_constraintTop_toBottomOf="@+id/editTextName"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    android:onClick="saveName"/>

  1. 打开ActivityMain.java并添加以下全局声明:
private final String NAME="NAME"; 
private EditText mEditTextName; 
  1. 将以下代码添加到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 = findViewById(R.id.editTextName);
  1. 添加以下saveName()方法:
public void saveName(View view) {
    SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit();
    editor.putString(NAME, mEditTextName.getText().toString());
    editor.commit();
}
  1. 在设备或模拟器上运行程序。由于我们正在演示持久化数据,它将在onCreate()期间加载名称,因此保存一个名称并重新启动程序以查看它如何加载。

工作原理...

要加载名称,我们首先获取对SharedPreference的引用并调用getString()方法。我们传入我们的名称/值对的键(我们创建了一个名为NAME的常量)以及如果找不到键则返回的默认值。

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

更多...

我们的示例将所有首选项存储在一个单独的文件中。我们也可以使用getSharedPreferences()和传入名称来在不同的文件中存储首选项。一个可以使用此选项的例子是在多用户应用程序中拥有单独的配置文件。

将文本文件读取和写入内部存储

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

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

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为InternalStorageFile。使用默认的Phone & Tablet选项,并在Add an Activity to Mobile对话框中选择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"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@+id/buttonRead"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent" />
<Button
    android:id="@+id/buttonRead"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Read"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintBottom_toTopOf="@+id/buttonWrite"
    android:onClick="readFile"/>
<Button
    android:id="@+id/buttonWrite"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Write"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    android:onClick="writeFile"/>
  1. 现在,打开ActivityMain.java并添加以下全局变量:
private final String FILENAME="testfile.txt"; 
EditText mEditText; 
  1. onCreate()方法中setContentView()之后添加以下内容:
mEditText = (EditText)findViewById(R.id.editText); 
  1. 添加以下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();
    }
}
  1. 现在,添加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);
}
  1. 在设备或模拟器上运行程序。

工作原理...

我们使用InputStreamFileOutputStream类分别进行读取和写入。写入文件就像从EditText获取文本并调用write()方法一样简单。

读取内容稍微复杂一些。我们可以使用FileInputStream类进行读取,但在处理文本时,辅助类使操作更简单。在我们的示例中,我们使用openFileInput()打开文件,它返回一个InputStream对象。然后我们使用InputStream获取一个BufferedReader,它提供了ReadLine()方法。我们遍历文件中的每一行并将其追加到我们的StringBuilder中。当我们完成文件读取后,我们将文本分配给EditText

更多...

之前的示例使用了私有存储来保存文件。以下是使用缓存文件夹的方法。

缓存文件

如果您只需要临时存储数据,您也可以使用缓存文件夹。以下方法返回缓存文件夹作为一个File对象(下一个配方将演示如何与File对象一起工作):

getCacheDir() 

缓存文件夹的主要好处是,当存储空间不足时,系统可以清除缓存。(用户也可以在设置中的应用管理中清除缓存文件夹。)

例如,如果您的应用下载新闻文章,您可以将这些文章存储在缓存中。当您的应用启动时,您可以显示已下载的新闻。这些文件不是使您的应用工作所必需的。如果系统资源不足,缓存可以被清除,而不会对您的应用产生不利影响。(即使系统可能会清除缓存,但仍然建议您的应用也删除旧文件。)

参见

  • 下一个配方,读取和写入外部存储中的文本文件

读取和写入外部存储中的文本文件

将文件读取和写入外部存储的过程基本上与使用内部存储相同。区别在于,在尝试访问之前,最好检查存储的可用性。如简介中所述,外部存储可能不可用,因此最好在尝试访问之前进行检查。

此配方将读取和写入一个文本文件,就像我们在上一个配方中所做的那样。我们还将演示在访问它之前如何检查外部存储状态。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为ExternalStorageFile。使用默认的Phone & Tablet选项,并在Add an Activity to Mobile对话框中选择Empty Activity。我们将使用与上一个配方相同的布局,所以如果您已经输入了它,可以直接复制粘贴。否则,使用上一个配方中步骤 1 的布局,读取和写入内部存储中的文本文件

如何操作...

如前文在准备就绪部分所述,我们将使用上一个配方中的布局。布局文件完成后,第一步将是添加访问外部存储写入权限。以下是步骤:

  1. 打开 AndroidManifest 文件并添加以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  1. 接下来,打开ActivityMain.java并添加以下全局变量:
private final String FILENAME="testfile.txt";
private final String[] PERMISSIONS_STORAGE = {
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
};
EditText mEditText;
  1. onCreate()方法中,在setContentView()之后添加以下内容:
mEditText = (EditText)findViewById(R.id.editText); 
  1. 添加以下两种方法来检查存储状态:
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;
}
  1. 添加以下方法以验证应用是否有权限访问外部存储:
public void checkStoragePermission() {
    int permission = ActivityCompat.checkSelfPermission(this, 
            Manifest.permission.WRITE_EXTERNAL_STORAGE);

    if (permission != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE,101);
    }
}
  1. 添加以下writeFile()方法:
public void writeFile(View view) {
    if (isExternalStorageWritable()) {
        checkStoragePermission();
        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();
    }
}
  1. 添加以下readFile()方法:
public void readFile(View view) {
    if (isExternalStorageReadable()) {
        checkStoragePermission();
        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();
    }
  1. 在具有外部存储的设备或模拟器上运行程序。

工作原理...

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

与内部存储示例不同,我们请求工作路径,这是我们在这一行代码中做的:

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

实际的读取和写入使用相同的类,因为只是位置不同。

将外部文件夹路径硬编码是不安全的。路径可能在操作系统版本之间以及特别是在硬件制造商之间有所不同。始终最好按照所示调用getExternalStorageDirectory()

更多...

你可能已经注意到了第 5 步中提到的checkStoragePermission()函数没有提及。这是因为权限不仅限于存储,而且应用程序访问各种设备功能也需要这些权限。与之前使用本地应用程序存储的配方不同,“外部”存储被认为对用户来说是危险的。(如果任何应用程序都可以浏览用户的私人文件,那就不好了。)因此,应用程序必须做出额外努力来检查它是否有访问存储所需的权限。如果没有,用户将被提示。请注意,这个额外的对话框来自操作系统,而不是应用程序本身。

当你第一次运行应用程序时,如果你被提示请求权限但仍然在写入时出错,请退出应用程序并重新启动。有关对新 Android 权限模型的深入解释和处理,请参阅另请参阅...部分。

获取公共文件夹

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()(复数)的方法,它将创建父文件夹。)

请参阅另请参阅部分中的链接以获取完整列表。

防止文件包含在图库中

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

另请参阅

在项目中包含资源文件

Android 为包含项目中的文件提供了两种选项:raw 文件夹和 assets 文件夹。您使用哪种选项取决于您的需求。首先,我们将简要概述每个选项,以帮助您决定最佳用途:

  • 原始文件

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

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

    • 存储媒体文件的好地方,例如 MP3、MP4 和 OGG 文件

  • 资产文件

    • 在您的 APK 中创建一个编译后的文件(不提供资源 ID)

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

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

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

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 ReadingResourceFiles。使用默认的“电话和平板电脑”选项,并在“添加活动到移动设备”对话框中选择“空活动”。

如何操作...

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

  1. 打开 activity_main.xml 并将内容替换为以下布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <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> 
  1. res 文件夹中创建 raw 资源文件夹。它将如下所示:res/raw。您可以轻松手动创建它,或者让 Android Studio 帮您创建,方法是在 res 文件夹上右键单击并选择“新建 | Android 资源目录”。当打开“选择资源目录”对话框时,选择raw作为“资源类型”,如图所示:

图片

  1. 通过右键单击 raw 文件夹并选择“新建 | 文件”来创建一个新的文本文件。将文件命名为 raw_text.txt 并在文件中输入一些文本。(运行应用程序时将显示此文本。)

  2. 创建 asset 文件夹。手动创建 asset 文件夹比较困难,因为它需要位于正确的文件夹级别。幸运的是,Android Studio 提供了一个菜单选项,可以轻松创建它。转到文件菜单(或在应用节点上右键单击)并选择“新建 | 文件夹 | 资源文件夹”,如图所示:

图片

  1. 在资产文件夹中创建一个名为 asset_text.txt 的文本文件。同样,你在这里输入的任何文本都会在运行应用程序时显示出来。以下是创建两个文本文件后的最终结果:

图片

  1. 现在,是时候编写代码了。打开 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();
}
  1. 最后,将以下代码添加到 onCreate() 方法中:
TextView textViewRaw = findViewById(R.id.textViewRaw);
textViewRaw.setText(getText(this.getResources().openRawResource(R.raw.raw_text)));
TextView textViewAsset = findViewById(R.id.textViewAsset);
try {
    textViewAsset.setText(getText(this.getAssets().open("asset_text.txt")));
} catch (IOException e) {
    e.printStackTrace();
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

总结来说,唯一的区别在于我们获取每个文件的引用方式。此行代码读取 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 的类,这是实现数据库功能的地方。以下是 CRUD(创建、读取、更新和删除)函数的列表:

  • 创建:insert()

  • 读取:query()rawQuery()

  • 更新:update()

  • 删除:delete()

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

准备工作

在 Android Studio 中创建一个新的项目,命名为 SQLiteDatabase。使用默认的 Phone & Tablet 选项,并在添加到移动对话框中选择 Empty Activity。

如何做...

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

  1. 用以下内容替换默认的 XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <EditText
        android:id="@+id/editTextWord"
        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/editTextDefinition"
        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:id="@+id/buttonAddUpdate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Save"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true" />
    <ListView
        android:id="@+id/listView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/et_definition"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true" />
</LinearLayout>
  1. 向项目中添加一个名为 DictionaryDatabase 的新 Java 类。此类从 SQLiteOpenHelper 继承,并处理所有 SQLite 功能。以下是类声明:
public class DictionaryDatabase extends SQLiteOpenHelper { 
  1. 在声明下方添加以下常量:
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;
  1. 添加以下构造函数、OnCreate()onUpgrade() 方法:
DictionaryDatabase(Context context) {
    super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
    db.execSQL("CREATE TABLE " + TABLE_DICTIONARY +
            "(_id integer PRIMARY KEY," +
            FIELD_WORD + " TEXT, " +
            FIELD_DEFINITION + " TEXT);");
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    //Handle database upgrade as needed
}
  1. 以下方法负责创建、更新和删除记录:
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)});
}
  1. 而这些方法处理从数据库读取信息:
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});
    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);
}
  1. 数据库和类完成后,打开 MainActivity.java。在类声明下方添加以下全局变量:
EditText mEditTextWord; 
EditText mEditTextDefinition; 
DictionaryDatabase mDB; 
ListView mListView; 
  1. 添加以下方法以在按钮点击时保存字段:
private void saveRecord() {
    mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString());
    mEditTextWord.setText("");
    mEditTextDefinition.setText("");
    updateWordList();
}
  1. 将此方法添加到填充 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);
}
  1. 最后,将以下代码添加到 onCreate()
mDB = new DictionaryDatabase(this);
mEditTextWord = findViewById(R.id.editTextWord);
mEditTextDefinition = findViewById(R.id.editTextDefinition);
Button buttonAddUpdate = findViewById(R.id.buttonAddUpdate);
buttonAddUpdate.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        saveRecord();
    }
});

mListView = 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();
  1. 在设备或模拟器上运行程序并尝试它。

它是如何工作的...

我们将首先解释 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 示例,但它仍然只是基础知识。有许多关于 SQLite 的书籍专门针对 Android,值得一看。

升级数据库

正如我们之前提到的,当我们增加数据库版本时,onUpgrade()方法将被调用。你在这里所做的是依赖于对数据库所做的更改。如果你更改了一个现有的表,理想情况下,你将希望通过查询现有数据并将其插入到新格式中来迁移用户数据。请注意,没有保证用户会按顺序升级,他们可能会从版本 1 跳到版本 4,例如。

参见

使用 Loader 在后台访问数据

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

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

Loaders 的两个主要好处如下:

  • 查询数据库是在后台线程上(自动)处理的

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

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

准备工作

我们将使用前一个示例中的项目,创建和使用 SQLite 数据库,作为本菜谱的基础。在 Android Studio 中创建一个新的项目,命名为 Loader。使用默认的 Phone & Tablet 选项,并在“添加一个活动到移动”对话框中选择 Empty Activity。从上一个菜谱复制 DictionaryDatabase 类和布局。虽然我们将使用前一个 ActivityMain.java 代码的一部分,但在这个菜谱中我们将从头开始,以便更容易理解。

如何实现...

按照在 准备就绪 中描述的设置项目,我们将继续创建两个新的 Java 类,然后在 ActivityMain.java 中将它们全部结合起来。以下是步骤:

  1. 创建一个新的 Java 类,命名为 DictionaryAdapter,并扩展 CursorAdapter 类。这个类替换了上一个菜谱中使用的 SimpleCursorAdapater。以下是完整的代码:
public class DictionaryAdapter extends CursorAdapter {
    public DictionaryAdapter(Context context, Cursor c, int flags) {
        super(context, c, flags);
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return LayoutInflater.from(context)
                .inflate(android.R.layout.simple_list_item_1,parent, false);
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        TextView textView = view.findViewById(android.R.id.text1);
        textView.setText(cursor.getString(getCursor().getColumnIndex("word")));
    }
}
  1. 接下来,创建另一个新的 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();
    }
} 
  1. 接下来,打开 ActivityMain.java。我们需要将声明改为实现 LoaderManager.LoaderCallbacks<Cursor> 接口,如下所示:
public class MainActivity extends AppCompatActivity 
    implements LoaderManager.LoaderCallbacks<Cursor> {
  1. 将适配器添加到全局声明中。完整的列表如下:
EditText mEditTextWord; 
EditText mEditTextDefinition; 
DictionaryDatabase mDB; 
ListView mListView; 
DictionaryAdapter mAdapter; 
  1. onCreate() 改为使用新的适配器,并在删除记录后添加一个调用更新 Loader 的调用。最终的 onCreate() 方法应如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mDB = new DictionaryDatabase(this);

    mEditTextWord = findViewById(R.id.editTextWord);
    mEditTextDefinition = findViewById(R.id.editTextDefinition);    
    Button buttonAddUpdate = findViewById(R.id.buttonAddUpdate);
    buttonAddUpdate.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            saveRecord();
        }
    });

    mListView = 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);
}
  1. 我们不再有 updateWordList() 方法,因此将 saveRecord() 方法

    如下所示:

private void saveRecord() {
    mDB.saveRecord(mEditTextWord.getText().toString(), mEditTextDefinition.getText().toString());
    mEditTextWord.setText("");
    mEditTextDefinition.setText("");
    getSupportLoaderManager().restartLoader(0, null, MainActivity.this);
}
  1. 最后,实现 Loader 接口这三个方法:
@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);
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

默认的 CursorAdapter 需要一个 Content Provider URI。由于我们直接访问 SQLite 数据库(而不是通过 Content Provider),我们没有 URI 可以传递,因此我们创建了一个自定义适配器,通过扩展 CursorAdapter 类来实现。DictionaryAdapter 仍然执行与上一个菜谱中的 SimpleCursorAdapter 相同的功能,即从游标映射数据到项目布局。

我们添加的下一个类是 DictionaryLoader,它负责填充适配器。正如你所见,它实际上非常简单。它所做的只是从 getWordList() 返回游标。关键在于这个查询是在后台线程中处理的,并在完成后调用 onLoadFinished() 回调(在 MainActivity.java 中)。幸运的是,大部分繁重的工作都在基类中处理。

这将带我们到 ActivityMain.java,在那里我们实现了 LoaderManager.LoaderCallbacks 接口中的以下三个回调:

  • onCreateLoader(): 它最初在 onCreate() 中通过 initLoader() 调用。在更改数据库后,它会在 restartLoader() 调用后再次被调用。

  • onLoadFinished(): 当 LoaderloadInBackground() 方法完成时被调用。

  • onLoaderReset():当 Loader 正在被重新创建时(例如使用restart()方法)会被调用。我们设置旧的游标为null,因为它将被无效化,我们不希望保留引用。

还有更多...

正如您在之前的示例中看到的,我们需要手动通知 Loader 使用restartLoader()重新查询数据库。使用 Loader 的一个好处是它可以自动更新,但它需要一个 Content Provider 作为数据源。Content Provider 支持使用 SQLite 数据库作为数据源,并且对于严肃的应用程序是推荐的。(请参阅以下 Content Provider 链接以开始。)

参见

在 Android N 中使用范围目录访问外部存储

随着安全意识的提高,用户对允许应用程序拥有不必要的权限变得更加怀疑。Android N 引入了一个名为范围目录访问的新选项,允许您的应用程序仅请求所需的权限,而不是对所有文件夹的通用访问。

如果您的应用程序请求READ_EXTERNAL_STORAGE和/或WRITE_EXTERNAL_STORAGE权限,但只需要访问特定目录,则可以使用范围目录访问。本配方将演示如何请求访问特定目录,即本例中的Music文件夹。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为ScopedDirectoryAccess。在“目标 Android 设备”对话框中,确保选择 API 24:Android 7.0(Nougat)或更高版本,对于“手机和平板”选项。在“添加移动活动”对话框中选择“Empty Activity”。

如何做到这一点...

要启动用户访问请求,我们将在布局中添加一个按钮。首先打开activity_main.xml,然后按照以下步骤操作:

  1. 用此按钮 XML 替换现有的TextView
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Request Access"
    android:onClick="onAccessClick"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 现在,打开MainActivity.java,并将以下代码行添加到类中:
private final int REQUEST_FOLDER_MUSIC=101;
  1. 添加处理按钮点击的方法:
public void onAccessClick(View view) {
    StorageManager storageManager = (StorageManager)getSystemService(Context.STORAGE_SERVICE);
    StorageVolume storageVolume = storageManager.getPrimaryStorageVolume();
    Intent intent = storageVolume.createAccessIntent(Environment.DIRECTORY_MUSIC);
    startActivityForResult(intent, REQUEST_FOLDER_MUSIC);
}
  1. 如下重写onActivityResult()方法:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case REQUEST_FOLDER_MUSIC:
            if (resultCode == Activity.RESULT_OK) {
                getContentResolver().takePersistableUriPermission(data.getData(), 0);
            }
            break;
    }
}
  1. 您现在可以在设备或模拟器上运行应用程序。

它是如何工作的...

访问请求由操作系统处理,而不是由应用程序处理。要请求访问,我们需要调用createAccessIntent(),我们通过以下代码行来完成:

Intent intent = storageVolume.createAccessIntent(Environment.DIRECTORY_MUSIC);

我们使用startActivityForResult()方法调用 Intent,这是我们之前使用过的。由于我们正在寻找一个返回的结果,我们需要传递一个唯一的标识符,以便知道返回的结果回调是否来自我们的请求。(onActivityResult()回调方法可以接收多个请求的回调。)如果请求代码与我们的请求匹配,我们然后检查结果代码是否等于Activity.RESULT_OK,这意味着用户已授予权限请求。我们将结果传递给takePersistableUriPermission(),这样我们就不需要在下次需要访问同一目录时提示用户。

对目录的访问也包括对所有子目录的访问。

还有更多...

为了获得最佳用户体验,请遵循以下最佳实践:

  1. 确保在用户授予权限后持久化 URI,以避免重复请求相同的权限(就像我们使用takePersistableUriPermission()那样)

  2. 如果用户拒绝权限请求,不要通过不断询问来烦扰用户

参见

第八章:警报和通知

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

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

  • 使用自定义布局创建 Toast

  • 使用 AlertDialog 显示消息框

  • 显示进度对话框

  • 使用通知的“光、动作和声音 Redux”

  • 创建媒体播放器通知

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

  • 允许直接回复的通知

简介

Android 提供了许多通知用户的方法,包括视觉和非视觉方法。请记住,通知会分散用户的注意力,因此在使用任何通知时都应非常谨慎。用户喜欢控制自己的设备(毕竟,这是他们的),因此请给他们提供按需启用和禁用通知的选项。否则,您的用户可能会感到烦恼,甚至完全卸载您的应用程序。

我们将首先回顾以下基于非 UI 的通知选项:

  • 闪烁 LED

  • 振动手机

  • 播放铃声

然后,我们将转向视觉通知,包括以下内容:

  • Toasts

  • AlertDialog

  • 进度对话框

  • 状态栏通知

下面的食谱将向您展示如何在您的应用程序中实现这些通知中的每一个。阅读以下链接了解使用通知的最佳实践是值得的:

请参阅Android 通知设计指南

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

本章中的大多数食谱都使用Notification对象来提醒用户,因此这个食谱将展示在您实际上不需要通知时的替代方法。

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

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

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

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

正如您将看到的,这些代码相当简单。

如以下使用通知的“光、动作和声音 Redux”食谱所示,所有三个选项,LED、振动和声音,都可通过Notification对象获得。当用户没有积极使用您的应用程序时,Notification对象无疑是提供警报和提醒的最合适方法。但有时您想在用户使用应用程序时提供反馈,这些选项是可用的。振动选项是一个很好的例子;如果您想为按钮按下提供触觉反馈(常见于键盘应用程序),请直接调用振动方法。

准备工作

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

如何实现...

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

  1. 用以下布局替换现有的布局 XML:
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"&gt;
    &lt;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" /&gt;
    &lt;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"/&gt;
    &lt;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"/&gt;
&lt;/RelativeLayout&gt;
  1. 在 AndroidManifest.xml 中添加以下权限:
&lt;uses-permission android:name="android.permission.VIBRATE" /&gt;
  1. 打开 ActivityMain.java 并添加以下全局变量:
private CameraManager mCameraManager;
private String mCameraId=null;
private ToggleButton mButtonLights;
  1. 添加以下方法以获取摄像头 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;
}
  1. onCreate() 方法中添加以下代码:
mButtonLights = findViewById(R.id.buttonLights);
if (Build.VERSION.SDK_INT &gt;= 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);
}
  1. 现在,添加代码来处理每个按钮点击:
public void clickLights(View view) {
    if (Build.VERSION.SDK_INT &gt;= 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();
}
  1. 你已经准备好在物理设备上运行应用程序了。这里展示的代码需要 Android 6.0(或更高版本)才能使用手电筒选项。

它是如何工作的...

如前几段所示,大部分代码都与查找和打开摄像头以使用闪光灯功能相关。setTorchMode() 是在 API 23 中引入的,这就是为什么我们需要进行 API 版本检查的原因:

if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.M){} 

此应用程序演示了使用在 Lollipop(API 21)中引入的新 camera2 库。vibrateringtone 方法自 API 1 以来一直可用。

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

要播放声音,我们使用 RingtoneManager 中的 Ringtone 对象。除了相对容易实现之外,这种方法的好处是我们可以使用默认的通知声音,我们通过以下代码获取它:

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

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

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

&lt;uses-permission android:name="android.permission.VIBRATE" /&gt;

还有更多...

在一个生产级别的应用程序中,如果你不需要禁用按钮,你不会想要简单地禁用按钮。在这种情况下,有其他方法可以使用摄像头闪光灯作为手电筒。有关使用摄像头的其他示例,请参阅 第十二章,多媒体,我们将再次看到 getCameraId() 的使用。

参见

  • 参考本章后面的 使用通知的“灯光、动作和声音 Redux” 菜单,以查看使用 Notification 对象的等效功能。

  • 参考第十二章 多媒体,以了解使用新摄像头 API 和其他声音选项的示例。

创建一个带有自定义布局的 Toast

我们已经在之前的章节中多次使用 Toast,因为它们提供了一种快速且简单的方式来显示信息,无论是用于用户通知还是用于调试时的自我提醒。

之前的例子都使用了简单的单行语法,但 Toast 并不局限于这一点。Toast,就像 Android 中的大多数组件一样,可以进行自定义,正如我们将在本食谱中展示的那样。

Android Studio 提供了一个快捷键来创建简单的 Toast 语句。当你开始输入 Toast 命令时,你会看到以下内容:

图片

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

图片

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

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

在本食谱中,我们将使用 Toast 构建器来更改默认布局,并使用重力创建一个自定义 Toast,如图下截图所示:

图片

准备工作

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

如何操作...

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

  1. 将现有的&lt;TextView&gt;元素替换为以下内容的&lt;Button&gt;
&lt;Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Show Toast"
    android:onClick="showToast"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" /&gt;
  1. res/drawable文件夹中创建一个新的可绘制资源文件,命名为border_square.xml,使用以下代码:
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;layer-list xmlns:android="http://schemas.android.com/apk/res/android"&gt;
    &lt;item
        android:left="4px"
        android:top="4px"
        android:right="4px"
        android:bottom="4px"&gt;
        &lt;shape android:shape="rectangle" &gt;
            &lt;solid android:color="@android:color/black" /&gt;
            &lt;stroke android:width="5px" android:color="@android:color/white"/&gt;
        &lt;/shape&gt;
    &lt;/item&gt;
&lt;/layer-list&gt;
  1. res/layout文件夹中创建一个新的布局资源文件,命名为toast_custom.xml,使用以下代码:
&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toast_layout_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:background="@drawable/border_square"&gt;
    &lt;ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/imageView"
        android:layout_weight="1"
        android:src="img/ic_launcher" /&gt;
    &lt;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" /&gt;
&lt;/LinearLayout&gt;
  1. 现在,打开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();
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

这个自定义 Toast 改变了默认的重力和形状,并添加了一个图像,仅为了展示“它可以做到”。

第一步是创建一个新的 Toast 布局,我们通过填充我们的custom_toast布局来实现。一旦我们有了新的布局,我们需要获取TextView以便我们可以设置我们的消息,我们使用标准的setText()方法来完成。完成此操作后,我们创建一个 Toast 对象并设置单个属性。我们使用setGravity()方法设置 Toast 的重力。重力决定了 Toast 将在屏幕上的哪个位置显示。我们使用setView()方法调用指定我们的自定义布局。就像在单行变体中一样,我们使用show()方法显示 Toast。

相关内容

  • 对于 Kotlin 版本,请参阅第十六章中的Creating a Toast in Kotlin食谱,Kotlin 入门

显示带有 AlertDialog 的消息框

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

图片

按钮排列可能因操作系统版本而异。

准备工作

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

如何做到这一点...

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

  1. 添加以下&lt;Button&gt;
&lt;Button
    android:id="@+id/buttonDelete"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Delete"
    android:onClick="confirmDelete"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" /&gt;
  1. confirmDelete()方法添加到ActivityMain.java中;这个方法是由按钮调用的:
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();
}
  1. 在设备或模拟器上运行应用程序。

它是如何工作的...

此对话框旨在作为简单的确认对话框,例如确认删除操作。基本上,只需创建一个AlertDialog.Builder对象并按需设置属性。我们使用 Toast 消息来指示用户选择。我们甚至不需要关闭对话框;这由基类处理。

更多...

如食谱介绍截图所示,AlertDialog还有一个第三个按钮,称为中性按钮,可以使用以下方法设置:

builder.setNeutralButton() 

添加图标

要向对话框添加图标,请使用setIcon()方法。以下是一个示例:

.setIcon(R.mipmap.ic_launcher) 

Android 4.3 中引入的 mipmap 文件夹是一个用于存储不应在 APK 优化期间修改/转换的位图的 drawable 文件夹。这是存储应用图标的首选位置,以便启动器在显示应用图标时可以显示最佳图像。

使用列表

我们还可以使用各种列表设置方法创建一个可供选择的项列表,包括以下方法:

.setItems() 
.setAdapter() 
.setSingleChoiceItems() 
.setMultiChoiceItems() 

如您所见,还有用于单选(使用单选按钮)和多选列表(使用复选框)的方法。

您不能同时使用消息和列表,因为setMessage()将具有优先级。

自定义布局

最后,我们还可以创建一个自定义布局,并使用以下方法设置它:

.setView() 

如果您使用自定义布局并替换标准按钮,您也负责关闭对话框。如果您计划重用对话框,请使用hide();完成时使用dismiss()来释放资源。

显示进度对话框

ProgressDialog自 API 1 以来一直可用,并且被广泛使用。正如我们将在本食谱中展示的那样,它很简单,但请记住这条信息(发布在 Android 对话框指南网站上developer.android.com/guide/topics/ui/dialogs.html):

Android 还包括另一个名为ProgressDialog的对话框类,它显示带有进度条的对话框。然而,如果您需要指示加载或不确定的进度,您应遵循进度和活动的设计指南,并在布局中使用进度条。

这条消息并不意味着 ProgressDialog 已被弃用或代码不好。它只是建议应该避免使用 ProgressDialog,因为用户在对话框显示时无法与你的应用交互。如果可能的话,使用包含进度条的布局(这样其他视图仍然可用),而不是使用 ProgressDialog 停止一切。

Google Play 应用提供了一个很好的例子。当添加下载项目时,Google Play 会显示一个进度条,但它不是一个对话框,因此用户可以继续与应用交互,甚至可以添加更多要下载的项目。如果可能的话,请使用那种方法。

有时候你可能没有这样的奢侈;例如,在下单后,用户会期待订单确认。(即使使用 Google Play,在购买应用时仍然会看到确认对话框。)所以,如果可能的话,请避免使用进度对话框。但是,对于那些必须在继续之前完成的事情,这个配方提供了一个如何使用 ProgressDialog 的示例。以下截图显示了配方中的 ProgressDialog

图片

准备工作

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

如何操作...

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

  2. <TextView> 替换为以下 <Button>

&lt;Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Show Dialog"
    android:onClick="startProgress"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" /&gt;
  1. 打开 MainActivity.java 并添加以下两个全局变量:
private ProgressDialog mDialog; final int THIRTY_SECONDS=30*1000; 
  1. 添加由按钮点击引用的 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);
}
  1. 在设备或模拟器上运行程序。当你按下“显示对话框”按钮时,你将看到屏幕上从介绍部分显示的对话框。

工作原理...

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

mDialog.setCancelable(false); 

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

还有更多...

我们为这个配方使用了默认的 ProgressDialog 设置,它创建了一个不确定的对话框指示器,例如,持续旋转的圆圈。如果你可以测量手头的任务,例如加载文件,你可以使用确定样式。

添加并运行以下代码行:

mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 

使用 STYLE_HORIZONTAL,你将看到这里显示的百分比对话框:

图片

使用通知重置灯光、动作和声音

你可能已经熟悉通知了,因为它们已经成为一个突出的功能(甚至进入桌面环境),而且有很好的理由。它们为用户提供了一种极好的方式来传达信息。它们提供了所有可用警报和通知选项中最不干扰的选项。

正如我们在第一个菜谱中看到的,灯光、动作和声音 - 引起用户的注意! 灯光、振动和声音都是吸引用户注意的非常有用的手段。这就是为什么 Notification 对象包含了所有三种选项的支持,正如我们将在本菜谱中展示的那样。鉴于这种吸引用户注意的能力,我们仍然应该注意不要滥用用户。否则,他们可能会卸载你的应用。通常,给用户选择启用/禁用通知以及如何显示通知(带声音或不带声音等)的选项是一个好主意。

准备工作

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

如何做到...

我们需要权限来使用振动选项,所以首先打开 Android Manifest 文件,然后按照以下剩余步骤操作:

  1. 添加以下权限:
&lt;uses-permission android:name="android.permission.VIBRATE"/&gt;
  1. 打开 activity_main.xml 并将现有的 <TextView> 替换为以下按钮:
&lt;Button
    android:id="@+id/buttonSound"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Lights, Action, and Sound"
    android:onClick="clickLightsActionSound"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" /&gt;
  1. 现在,打开 MainActivity.java 并将以下声明添加到类中:
final String CHANNEL_ID="notifications";
  1. 接下来,添加处理按钮点击的方法:
public void clickLightsActionSound(View view) {
    Uri notificationSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {
        AudioAttributes audioAttributes = new AudioAttributes.Builder()
                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
                .build();
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID, 
                "Notifications", NotificationManager.IMPORTANCE_HIGH);
        channel.setDescription("All app notifications");
        channel.setSound(notificationSoundUri,audioAttributes);
        channel.setLightColor(Color.BLUE);
        channel.enableLights(true);
        channel.enableVibration(true);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }

    NotificationCompat.Builder notificationBuilder = new
            NotificationCompat.Builder(this,CHANNEL_ID)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle(getString(R.string.app_name))
            .setContentText("Lights, Action & Sound")
            .setSound(notificationSoundUri)
            .setLights(Color.BLUE, 500, 500)
            .setVibrate(new long[]{250,500,250,500,250,500})
            .setDefaults(Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE);
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    notificationManager.notify(0, notificationBuilder.build());
}
  1. 在物理设备上运行程序以体验所有通知效果。

它是如何工作的...

我们将所有三种动作组合成一个单一的通知,仅仅是因为我们可以这样做。你不必使用所有三个额外的通知选项,甚至不需要任何。以下是需要的内容:

.setSmallIcon() 
.setContentText() 

如果你没有设置图标和文本,通知将不会显示。

我们使用了 NotificationCompat 来构建我们的通知。这来自支持库,使得与较旧 OS 版本向后兼容变得更容易。如果我们请求一个用户 OS 版本上不可用的通知功能,它将被简单地忽略。

这三条代码行生成了我们的额外通知选项:

.setSound(notificationSoundUri) 
.setLights(Color.BLUE, 500, 500) 
.setVibrate(new long[]{250,500,250,500,250,500}); 

值得注意的是,我们使用与之前 灯光、动作和声音 菜谱中的 RingtoneManager 相同的声音 URI 来创建通知。振动功能也要求相同的振动权限,但请注意我们发送的值是不同的。我们不是只发送振动的持续时间,而是发送一个振动模式。第一个值代表 off 持续时间(以毫秒为单位);下一个值代表振动 on 持续时间,并重复。

如以下代码行所示:

if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O)

如果应用正在运行 Android 8 Oreo(API 26)或更高版本,创建通知有两个部分:通知本身以及通知通道(或用户在设置中看到的“类别”)。通知“类别”功能是在 Android 8 中添加的,以便用户更容易管理由应用显示的许多通知。在此功能添加之前,通知对于应用要么开启要么关闭。用户没有方法来允许仅某些通知类型。

如果用户正在运行 Android 8 或更高版本,我们需要创建通道和通道特性。请注意,一旦通道创建,其属性就不能更改。例如,如果你在首次创建通道时没有启用声音,之后更改它将没有任何效果。(这也适用于应用重启后。)

在具有 LED 通知的设备上,当屏幕处于活动状态时,你不会看到 LED 通知。

还有更多...

这个菜谱展示了通知的基本知识,但像 Android 上的许多功能一样,选项在后续的操作系统版本中有所扩展。(请注意,以下 Toast 的外观可能会根据操作系统版本和制造商而有所不同。)

使用addAction()向通知添加按钮

在添加操作按钮时,你应该考虑几个设计因素,这些因素在章节引言中提到的通知指南链接中有详细说明。你可以在通知构建器上使用addAction()方法添加一个按钮(最多三个)。以下是一个包含一个操作按钮的通知示例:

图片

这是创建此通知的代码:

NotificationCompat.Builder notificationBuilder = new
        NotificationCompat.Builder(this, CHANNEL_ID)
        .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);

一个Action需要三个参数:图像、文本和PendingIntent。前两项用于视觉显示,而第三项PendingIntent在用户按下按钮时被调用。

以下代码创建了一个非常简单的PendingIntent;它只是启动应用。这可能是通知中最常见的意图,通常在用户按下通知时使用。要设置通知意图,请使用以下代码:

.setContentIntent(pendingIntent) 

按钮操作可能需要更多信息,因为它应该将用户带到应用中的特定项目。你还应该创建一个应用程序回退栈以获得最佳用户体验。

扩展通知

扩展通知是在 Android 4.1(API 16)中引入的,可以通过在通知构建器上使用setStyle()方法来使用。如果用户的操作系统不支持扩展通知,通知将显示为正常通知。

目前在NotificationCompat库中可用的三种扩展样式包括以下内容:

  • InboxStyle:包含字符串列表的大格式通知

  • BigPictureStyle:包含大图像附件的大格式通知

  • BigTextStyle:包含大量文本的大格式通知

下面是每种通知样式的示例以及创建示例所使用的代码:

  1. InboxStyle: 包含字符串列表的大格式通知

下面是此样式的代码:

NotificationCompat.Builder notificationBuilder =
        new NotificationCompat.Builder(this, CHANNEL_ID)
                .setSmallIcon(R.mipmap.ic_launcher);
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
inboxStyle.setBigContentTitle("InboxStyle - Big Content Title")
        .addLine("Line 1")
        .addLine("Line 2");
notificationBuilder.setStyle(inboxStyle);
  1. BigPictureStyle: 包含大图像附件的大格式通知

查看此样式的代码:

NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("LightsActionSoundRedux")
        .setContentText("BigPictureStyle");
NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
bigPictureStyle.bigPicture(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
notificationBuilder.setStyle(bigPictureStyle);
  1. BigTextStyle : 包含大量文本的大格式通知

下面是这个样式的代码示例。

NotificationCompat.Builder notificationBuilder = 
        new NotificationCompat.Builder(this, CHANNEL_ID)
        .setSmallIcon(R.mipmap.ic_launcher)
        .setContentTitle("LightsActionSoundRedux");
NotificationCompat.BigTextStyle BigTextStyle = new NotificationCompat.BigTextStyle();
BigTextStyle.bigText("This is an example of the BigTextStyle expanded notification.");
notificationBuilder.setStyle(BigTextStyle);

锁屏通知

Android 5.0(API 21)及以上版本可以根据用户的锁屏可见性显示通知。使用 setVisibility() 方法通过以下值指定通知可见性:

  • VISIBILITY_PUBLIC: 可以显示所有内容。

  • VISIBILITY_SECRET: 不应显示任何内容。

  • VISIBILITY_PRIVATE: 显示基本内容(标题和图标),其余内容隐藏。

参见

  • 参见 创建媒体播放器通知制作手电筒

    Android 5.0 的 Heads-Up Notification 食谱,用于提供额外的通知选项

    (API 21)及以上。

创建媒体播放器通知

本食谱将探讨 Android 5.0(API 21)中引入的新媒体播放器样式。与之前的食谱不同,使用通知的“灯光、动作和声音重制”,该食谱使用了 NotificationCompat,而本食谱没有使用,因为此样式在支持库中不可用。

下面是通知将如何显示的截图:

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

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 MediaPlayerNotification。在“目标 Android 设备”对话框中,选择 API 21:Android 5.0(Lollipop)或更高版本,为此项目选择。在“添加活动到移动设备”对话框中选择“空活动”。

如何做到这一点...

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

  1. 将现有的 &lt;TextView&gt; 替换为以下按钮代码:
&lt;Button
    android:id="@+id/button"
    android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:text="Show Notification" 
    android:onClick="showNotification"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" /&gt;
  1. 打开 MainActivity.java 并添加 showNotification() 方法:
@SuppressWarnings("deprecated")
public void showNotification(View view) {
    Intent activityIntent = new Intent(this,MainActivity.class);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, activityIntent, 0);

    Log.i(this.getClass().getSimpleName(),"showNotification()" );
    Notification.Builder notificationBuilder;
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.M) {
        notificationBuilder = 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));
    } else {
        notificationBuilder = 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));
    }
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {
        notificationBuilder.setChannelId(createChannel());
    }
    NotificationManager notificationManager =
            (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
    notificationManager.notify(0, notificationBuilder.build());
}
  1. 添加以下方法以创建 Android O 及更高版本的通道:
private String createChannel() {
    final String channelId = "mediaplayer";
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {
        NotificationChannel channel = new NotificationChannel(channelId, "Notifications",
                NotificationManager.IMPORTANCE_HIGH);
        channel.setDescription("All app notifications");
        channel.enableVibration(true);
        NotificationManager notificationManager =
                getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }
    return channelId;
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

首先要注意的是,我们用以下方式装饰我们的 showNotification() 方法:

@SuppressWarnings("deprecated")

这告诉编译器我们知道我们正在使用已弃用的调用。(如果没有这个,编译器将标记代码。)我们随后进行 API 检查,使用以下调用:

if (Build.VERSION.SDK_INT &gt;= 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()) 

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

相关内容

创建带有抬头通知的手电筒

Android 5.0-Lollipop(API 21)引入了一种新的通知类型,称为抬头通知。许多人不喜欢这种新的通知,因为它可能非常侵扰性,因为它强迫其方式覆盖其他应用。(参见以下截图。)在使用此类通知时请记住这一点。我们将通过使用手电筒来演示抬头通知,因为这演示了一个良好的用例场景。

这里是一个截图,显示了我们将要创建的抬头通知:

如果您的设备正在运行 Android 6.0,您可能已经注意到了新的手电筒设置选项。作为演示,我们将在本菜谱中创建类似的内容。

准备工作

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

如何操作...

我们的活动布局将只包含一个 ToggleButton 来控制手电筒模式。我们将使用之前在 灯光、动作和声音 - 引起用户的注意! 菜单中展示的相同的 setTorchMode() 代码,并添加一个抬头通知。我们需要权限来使用振动选项,所以首先打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:
&lt;uses-permission android:name="android.permission.VIBRATE"/&gt;
  1. &lt;MainActivity&gt; 元素中添加 android:launchMode="singleInstance" 以指定我们只想有一个 MainActivity 的实例。它将看起来如下:
&lt;activity android:name=".MainActivity" 
    android:launchMode="singleInstance"&gt; 
  1. 在完成对 AndroidManifest 的修改后,打开 activity_main.xml 布局,并用以下 &lt;ToggleButton&gt; 代码替换现有的 &lt;TextView&gt; 元素:
&lt;ToggleButton
    android:id="@+id/buttonLight"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Flashlight"
    android:onClick="clickLight"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" /&gt;
  1. 现在,打开 ActivityMain.java 并添加以下全局变量:
private static final String ACTION_STOP="STOP"; 
private CameraManager mCameraManager; 
private String mCameraId=null; 
private ToggleButton mButtonLight; 
  1. onCreate() 中添加以下代码以设置相机:
mButtonLight = findViewById(R.id.buttonLight);
mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
mCameraId = getCameraId();
if (mCameraId==null) {
    mButtonLight.setEnabled(false);
} else {
    mButtonLight.setEnabled(true);
}
  1. 添加以下方法来处理用户按下通知时的响应:
@Override 
protected void onNewIntent(Intent intent) { 
    super.onNewIntent(intent); 
    if (ACTION_STOP.equals(intent.getAction())) { 
        setFlashlight(false); 
    } 
} 
  1. 添加获取相机 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;
}
  1. 添加以下两个方法来处理手电筒模式:
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();
    }
}
  1. 最后,添加以下方法来创建通知:
private void showNotification() {
    final String CHANNEL_ID = "flashlight";
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {
        NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
                "Notifications", NotificationManager.IMPORTANCE_HIGH);
        channel.setDescription("All app notifications");
        channel.enableVibration(true);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }

    Intent activityIntent = new Intent(this, MainActivity.class);
    activityIntent.setAction(ACTION_STOP);
    PendingIntent pendingIntent = 
            PendingIntent.getActivity(this, 0, activityIntent, 0);
    final NotificationCompat.Builder notificationBuilder = 
            new NotificationCompat.Builder(this, CHANNEL_ID)
            .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)
            .setAutoCancel(true);
    NotificationManager notificationManager = (NotificationManager) 
 this.getSystemService(Context.NOTIFICATION_SERVICE);
 notificationManager.notify(0, notificationBuilder.build());
}
  1. 你现在可以运行应用程序在物理设备上了。如前所述,你需要一个 Android 6.0(或更高版本)的设备,并且有一个外置的相机闪光灯。

它是如何工作的...

由于这个菜谱使用了与 灯光、动作和声音 - 引起用户的注意! 相同的手电筒代码,我们将跳转到 showNotification() 方法。大多数通知构建器调用与之前的示例相同,但有两大显著差异:

.setVibrate() 
.setPriority(PRIORITY_MAX) 

除非优先级设置为 HIGH(或更高),并且使用振动或声音,否则通知不会被提升为抬头通知。

注意开发者文档中的以下内容 developer.android.com/reference/android/app/Notification.html#headsUpContentView

"系统用户界面可以自行决定是否将其显示为抬头通知。"

我们像之前一样创建了一个 PendingIntent,但在这里我们使用以下方式设置动作:

activityIntent.setAction(ACTION_STOP); 

我们在 AndroidManifest 文件中将应用设置为只允许单个实例,因为我们不希望在用户按下通知时启动应用的新实例。我们创建的 PendingIntent 设置了动作,我们在 onNewIntent() 回调中检查这个动作。如果用户在没有按下通知的情况下打开应用,他们仍然可以使用 ToggleButton 禁用手电筒。

还有更多...

你可能已经注意到了以下代码行:

.setAutoCancel(true);

.setAutoCancel() 告诉操作系统在用户点击通知时自动移除通知。如果用户按下通知来关闭灯光,这很好,但如果他们使用切换按钮会发生什么呢?灯光会像预期的那样关闭,但他们将留下一个无用的通知。为了解决这个问题,我们可以添加一个新的方法来取消通知:

private void cancelNotification() {
    NotificationManager notificationManager = (NotificationManager)
            this.getSystemService(Context.NOTIFICATION_SERVICE);
    notificationManager.cancelAll();
}

然后在他们按下按钮时调用它。以下是clickLight()将看起来如何:

public void clickLight(View view) {
    setFlashlight(mButtonLight.isChecked());
    if (mButtonLight.isChecked()) {
        showNotification();
    } else {
        cancelNotification();
    }
}

参见

  • 参考之前关于使用灯光、动作和声音 - 吸引用户注意!的食谱,以获取有关火炬 API 的更多信息

  • 参考之前关于使用通知的灯光、动作和声音重置的食谱,以获取更多通知示例

支持直接回复的通知

Android N 中引入的最令人兴奋的新功能之一是内联回复,称为直接回复。使用直接回复,用户可以在不离开通知栏的情况下进行回复!

在这个食谱中,我们将通过将 RemoteInput 传递给addRemoteInput()方法来添加创建内联回复的能力。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为DirectReply。在目标 Android 设备对话框中,选择手机和平板选项,并选择 API 24:Android Nougat 7.0(或更高版本)作为最小 SDK。当提示活动类型时,选择空活动。

如何操作...

我们的应用将包含主屏幕上的单个按钮,用于启动初始通知。首先打开activity_main.xml并按照以下步骤操作:

  1. 将现有的TextView替换为按钮 XML:
&lt;Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Send Notification"
    android:id="@+id/buttonSend"
    android:onClick="onClickSend"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" /&gt;
  1. 现在,打开MainActivity.java并将以下代码添加到类中:
private final String KEY_REPLY_TEXT = "KEY_REPLY_TEXT";
private final int NOTIFICATION_ID = 1;
  1. 将以下代码添加到现有的onCreate()方法中:
if (getIntent()!=null) {
    Toast.makeText(MainActivity.this, getReplyText(getIntent()), Toast.LENGTH_SHORT).show();
}
  1. 如下重写onNewIntent()方法:
@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    Toast.makeText(MainActivity.this, getReplyText(intent), Toast.LENGTH_SHORT).show();
}
  1. 添加以下方法以处理按钮点击:
public void onClickSend(View view){
    Intent activityIntent = new Intent(this,MainActivity.class);
    PendingIntent pendingIntent =
            PendingIntent.getActivity(this,0,activityIntent,0);

    RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY_TEXT)
            .setLabel("Reply")
            .build();

    NotificationCompat.Action action = 
            new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_revert, 
                    "Reply", pendingIntent)
                    .addRemoteInput(remoteInput)
                    .build();

    NotificationCompat.Builder notificationBuilder = 
            new NotificationCompat.Builder(this,getChannelId())
                    .setSmallIcon(android.R.drawable.ic_dialog_email)
                    .setContentTitle("Reply")
                    .setContentText("Content")
                    .addAction(action);

    NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
    notificationManager.notify(0, notificationBuilder.build());
}

private String getChannelId() {
    final String channelId = "directreply";
    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.O) {
        NotificationChannel channel = new NotificationChannel(channelId,
                "Notifications", NotificationManager.IMPORTANCE_DEFAULT);
        channel.setDescription("All app notifications");
        channel.enableVibration(true);
        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        notificationManager.createNotificationChannel(channel);
    }
    return channelId;
}
  1. 添加getReplyText()方法:
private CharSequence getReplyText(Intent intent) {
    Bundle notificationReply = RemoteInput.getResultsFromIntent(intent);
    if (notificationReply != null) {
        return notificationReply.getCharSequence(KEY_REPLY_TEXT);
    }
    return null;
}
  1. 您现在可以开始在设备或模拟器上运行应用程序了。

工作原理...

将直接回复选项添加到通知实际上非常简单。我们从一个通知对象开始,就像之前食谱中做的那样。(我们使用支持库中的 NotifcationCompat 以提供更好的向后兼容性。)在创建动作时,调用addRemoteInput()方法,传入一个 RemoteInput。RemoteInput 是定义检索用户输入文本键的地方。在用户输入回复后,操作系统调用 PendingIntent,通过 Intent 将数据传回您的应用。使用RemoteInput.getResultsFromIntent()来检索用户文本,就像我们在getReplyText()方法中所做的那样。

参见

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

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

  • 监听点击和长按事件

  • 识别点击和其他常见手势

  • 使用多指手势进行捏合缩放

  • 滑动刷新

  • 列出可用传感器——Android 传感器框架简介

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

  • 读取设备方向

简介

现在,移动设备配备了各种传感器,通常包括陀螺仪、磁性、重力、压力和/或温度传感器,更不用说触摸屏了。这为与用户交互提供了许多新的和令人兴奋的选项。通过传感器,您可以确定三维设备位置以及设备的使用方式,例如摇晃、旋转、倾斜等。甚至触摸屏也提供了从简单的点击到手势和多指的许多新的输入方法。

我们将从这个章节开始探索触摸屏交互,从简单的点击和长按开始,然后继续使用 SimpleOnGestureListener 类检测常见的手势。接下来,我们将查看多指使用

使用 ScaleGestureDetector 进行捏合缩放手势。

本书旨在为您提供快速指南,以添加您自己的应用程序的功能和功能。因此,重点在于所需的代码,但强烈建议您也熟悉设计指南。

查看谷歌手势设计指南:www.google.com/design/spec/patterns/gestures.html

在本章的后半部分,我们将探讨 Android 中的传感器功能,使用 Android 传感器框架。我们将演示如何获取所有可用传感器的列表,以及如何检查特定传感器。一旦我们识别出传感器,我们将演示如何设置监听器以读取传感器数据。最后,我们将以演示如何确定设备方向结束本章。

监听点击和长按事件

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

Android 提供了一个事件监听器接口,用于在发生某些操作时接收单个通知,如下列所示:

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

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

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

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

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

本食谱将演示如何响应用击事件以及长按事件。

准备工作

在 Android Studio 中创建一个新的项目,命名为 PressEvents。使用默认的 Phone & Tablet 选项,并在 Add an Activity to Mobile 对话框中选择 Empty Activity。

如何实现...

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

  1. 打开 activity_main.xml 并将现有的 TextView 替换为以下 Button
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 现在打开 MainActivy.java 并将以下代码添加到现有的 onCreate() 方法中:
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;
    }
});
  1. 在设备或模拟器上运行应用程序,并尝试常规点击和长按。

它是如何工作的...

在本书中使用的多数示例中,我们使用以下属性在 XML 中设置 onClick 监听器:

android:onClick="" 

你可能会注意到 XML onClick() 方法回调需要与 setOnClickListener.onClick() 回调相同的方法签名。

public void onClick(View v) {} 

这是因为当我们使用 XML onClick 属性时,Android 会自动为我们设置回调。此示例还演示了我们可以在一个视图上拥有多个监听器。

最后一点要注意的是,onLongClick() 方法返回一个布尔值,正如大多数其他事件监听器一样。返回 true 表示事件已被处理。

还有更多...

尽管按钮通常用于指示用户应按下的位置,但我们可以在任何视图(甚至 TextView)上使用 setOnClickListener()setOnLongClickListener()

如介绍中所述,还有其他事件监听器。你可以通过输入以下内容使用 Android Studio 的自动完成功能来列出可用监听器:

button.setOn 

当你开始输入时,你会在 Android Studio 的自动完成列表中看到可用选项的列表。

识别点击和其他常见手势

与前一个食谱中描述的事件监听器不同,手势需要一个两步过程:

  1. 收集运动数据

  2. 分析数据以确定它是否匹配已知的手势

第一步在用户触摸屏幕时开始,这会触发带有在 MotionEvent 对象中发送的运动数据的 onTouchEvent() 回调。幸运的是,Android 使用 GestureDetector 类使第二步,即数据分析,变得更容易,该类可以检测以下手势:

  • onTouchEvent()

  • onDown()

  • onFling()

  • onLongPress()

  • onScroll()

  • onShowPress()

  • onDoubleTap()

  • onDoubleTapEvent()

  • onSingleTapConfirmed()

本食谱将演示使用 GestureDetector.SimpleOnGestureListener 来识别触摸和双击手势。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 CommonGestureDetector。使用默认的“电话”和“平板电脑”选项,并在提示活动类型时选择“空活动”。

如何实现...

我们将使用活动本身来检测手势,因此我们不需要在布局中添加任何视图。打开 MainActivity.java 并执行以下步骤:

  1. MainActivity 类中添加以下全局变量:
private GestureDetectorCompat mGestureDetector; 
  1. 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);
    }
}
  1. 将以下 onTouchEvent() 方法添加到 MainActivity 类中,以处理触摸事件通知:
public boolean onTouchEvent(MotionEvent event) {
    mGestureDetector.onTouchEvent(event);
    return super.onTouchEvent(event);
}
  1. 最后,将以下行代码添加到 onCreate() 中:
mGestureDetector = new GestureDetectorCompat(this, new  GestureListener());
  1. 在设备或模拟器上运行此应用程序。

工作原理...

我们使用 GestureDetectorCompat,这是支持库的一部分,它允许在运行 Android 1.6 及以上版本的设备上支持手势。

如配方介绍中所述,检测手势是一个两步过程。为了收集运动或手势数据,我们开始通过触摸事件跟踪运动。每次调用 onTouchEvent() 时,我们都将那些数据发送到 GestureDetectorGestureDetector 处理第二步,即分析数据。一旦检测到手势,就会调用适当的回调。我们的示例处理单次和双击手势。

还有更多...

您的应用程序可以通过简单地重写适当的回调来轻松添加对 GestureDetector 检测到的剩余手势的支持。

相关内容

  • 请参阅下一配方,使用多指手势进行缩放,了解多指手势

使用多指手势进行缩放

之前的配方使用了 SimpleOnGestureListener 来提供对简单、单指手势的检测。在这个配方中,我们将使用 SimpleOnScaleGestureListener 类来检测常见的多指手势“缩放手势”。

这里是本配方中我们将创建的应用程序的两个截图。第一个显示了图标被缩小:

这第二个截图显示了图标被放大:

准备工作

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

如何实现...

为了提供缩放手势的视觉指示,我们将使用带有应用程序图标的 ImageView。打开 activity_main.xml 并按照以下步骤操作:

  1. 用以下 ImageView 替换现有的 TextView
<android.support.v7.widget.AppCompatImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="img/ic_launcher"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 现在,打开 MainActivity.java 并将以下全局变量添加到类中:
private ScaleGestureDetector mScaleGestureDetector;
private float mScaleFactor = 1.0f;
private AppCompatImageView mImageView;
  1. 将以下 onTouchEvent() 实现添加到 MainActivity 类中:
public boolean onTouchEvent(MotionEvent motionEvent) { 
    mScaleGestureDetector.onTouchEvent(motionEvent); 
    return true; 
} 
  1. 将以下 ScaleListener 类添加到 MainActivity 类中:
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
    @Override
    public boolean onScale(ScaleGestureDetector scaleGestureDetector) {
        mScaleFactor *= scaleGestureDetector.getScaleFactor();
        mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));
        mImageView.setScaleX(mScaleFactor);
        mImageView.setScaleY(mScaleFactor);
        return true;
    }
} 
  1. 将以下代码添加到现有的 onCreate() 方法中:
mImageView=findViewById(R.id.imageView);
mScaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener());
  1. 要实验缩放手势功能,请在带触摸屏的设备上运行应用程序。

工作原理...

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 功能。以下截图显示了刷新操作:

图片

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为SwipeToRefresh。使用默认的“手机和平板”选项,并在“添加活动到移动”对话框中选择“空活动”。

如何做到这一点...

首先,我们需要将SwipeRefreshLayout小部件和ListView添加到活动布局中,然后我们将在 Java 代码中实现刷新监听器。以下是详细步骤:

  1. 打开activity_main.xml并用以下内容替换现有的约束布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <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>
</RelativeLayout>
  1. 现在打开MainActivity.java并为该类添加以下全局变量:
SwipeRefreshLayout mSwipeRefreshLayout;
ListView mListView;
List mArrayList = new ArrayList<>();
private int mRefreshCount=0;
  1. 将以下方法添加到MainActivity类中,以处理刷新:
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);
}
  1. 将以下代码添加到现有的onCreate()方法中:
mSwipeRefreshLayout = findViewById(R.id.swipeRefresh);
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
    @Override
    public void onRefresh() {
        refreshList();
    }
});
mListView = findViewById(android.R.id.list);
final String[] countries = new String[]{"China", "France", "Germany", "India",
        "Russia", "United Kingdom", "United States"};
mArrayList = new ArrayList<>(Arrays.asList(countries));
ListAdapter countryAdapter = new ArrayAdapter<String>(this,
        android.R.layout.simple_list_item_1, mArrayList);
mListView.setAdapter(countryAdapter);
  1. 在设备或模拟器上运行应用程序。

它是如何工作的...

此食谱的大部分代码通过在每次调用刷新方法时向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 传感器框架支持硬件传感器。该框架包括以下类和接口:

  • SensorManager

  • Sensor

  • SensorEventListener

  • SensorEvent

大多数 Android 设备都包括硬件传感器,但它们在不同制造商和型号之间差异很大。如果您的应用程序使用传感器,您有两个选择:

  • 在 AndroidManifest 中指定传感器

  • 在运行时检查传感器

要指定您的应用程序使用传感器,请在 AndroidManifest 中包含<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,已经被弃用,因为它们已被新的传感器所取代。

本教程将演示如何检索可用传感器的列表。以下是 Pixel 2 模拟器的截图:

准备工作

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

如何操作...

首先,我们将查询可用的传感器列表,然后将在ListView中显示结果。以下是详细步骤:

  1. 打开activity_main.xml,将现有的TextView替换为以下内容:
<ListView
    android:id="@+id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 接下来,打开ActivityMain.java,并将以下代码添加到现有的onCreate()方法中:
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);
  1. 在设备或模拟器上运行程序。

它是如何工作的...

以下代码行负责获取可用传感器的列表;其余代码将填充ListView

List<Sensor> sensors = ((SensorManager) getSystemService(
     Context.SENSOR_SERVICE)).getSensorList(Sensor.TYPE_ALL);

注意我们返回了一个Sensor对象列表。我们只获取传感器名称以在ListView中显示,但还有其他属性可用。请参阅另请参阅部分提供的完整列表。

更多内容...

重要的是要注意,一个设备可以有多种相同类型的传感器。如果你正在寻找特定的传感器,你可以传递介绍中显示的表中的一个常量。在这种情况下,如果你想查看所有可用的加速度计传感器,你可以使用这个调用:

List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER); 

如果你不是在寻找传感器列表,而是需要与特定传感器一起工作,你可以使用以下代码检查默认传感器:

SensorManager sensorManager =  ((SensorManager) getSystemService(Context.SENSOR_SERVICE));
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。使用默认的 Phone & Tablet 选项,并在提示活动类型时选择 Empty Activity。

如何做...

我们将在活动布局中添加一个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:text="0"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 现在,打开MainActivity.java并添加以下全局变量声明:
private SensorManager mSensorManager; 
private Sensor mSensor; 
private TextView mTextView; 
  1. MainActivity类中实现SensorListener类,如下所示:
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
    }
};
  1. 我们将在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);
}
  1. 将以下代码添加到onCreate()中:
mTextView = (TextView)findViewById(R.id.textView);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
  1. 你现在可以在物理设备上运行应用程序,以查看来自光传感器的原始数据。

它是如何工作的...

使用 Android 传感器框架首先是从获取传感器开始,我们在onCreate()中这样做。在这里,我们调用getDefaultSensor(),请求TYPE_LIGHT。我们在onResume()中注册监听器,并在onPause()中再次取消注册以减少电池消耗。当我们调用registerListener()时,我们传递我们的mSensorListener对象。

在我们的案例中,我们只寻找传感器数据,这些数据在onSensorChanged()回调中发送。当传感器发生变化时,我们使用传感器数据更新TextView

更多...

现在你已经使用过一个传感器了,你知道如何使用所有传感器,因为它们都使用相同的框架。当然,你对数据的处理将因你所读取的数据类型而大不相同。环境传感器,如这里所示,返回一个单一值,但位置和运动传感器也可以返回额外的元素,如下所示。

环境传感器

Android 支持以下四种环境传感器:

  • 湿度

  • 压力

  • 温度

环境传感器通常更容易处理,因为返回的数据是单个元素,通常不需要校准或过滤。我们在这个菜谱中使用了光传感器(Sensor.TYPE_LIGHT),因为大多数设备都包含光传感器来控制屏幕亮度。

位置传感器

位置传感器包括:

  • 地磁场

  • 距离

以下传感器类型使用地磁场:

  • TYPE_GAME_ROTATION_VECTOR

  • TYPE_GEOMAGNETIC_ROTATION_VECTOR

  • TYPE_MAGNETIC_FIELD

  • TYPE_MAGNETIC_FIELD_UNCALIBRATED

这些传感器在onSensorChanged()事件中返回三个值,除了TYPE_MAGNETIC_FIELD_UNCALIBRATED,它发送六个值。

第三个传感器,方向传感器,已被弃用,现在建议使用getRotation()getRotationMatrix()来计算方向变化。(有关设备方向,如纵向和横向模式,请参阅下一道菜谱:读取设备方向。)

运动传感器

运动传感器包括以下:

  • 加速度计

  • 惯性仪

  • 重力

  • 线性加速度

  • 旋转向量

这些包括以下传感器类型:

  • TYPE_ACCELEROMETE

  • TYPE_GRAVITY

  • TYPE_GYROSCOPE

  • TYPE_GYROSCOPE_UNCALIBRATED

  • TYPE_LINEAR_ACCELERATION

  • TYPE_ROTATION_VECTOR

  • TYPE_SIGNIFICANT_MOTION

  • TYPE_STEP_COUNTER

  • TYPE_STEP_DETECTOR

这些传感器也包括三个数据元素,除了最后三个。TYPE_SIGNIFICANT_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。使用默认的 Phone & Tablet 选项,并在提示活动类型时选择 Empty Activity。

如何操作...

我们将在布局中添加一个按钮以按需检查方向。首先打开 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:onClick="checkOrientation"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 添加以下方法来处理按钮点击:
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;
    }
}
  1. 在设备或模拟器上运行应用程序。

使用 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 创建幻灯片

  • 使用片段创建卡片翻转动画

  • 使用自定义过渡创建缩放动画

  • 使用新的 ImageDecoder 库显示动画图像(GIF/WebP)

  • 使用新的 ImageDecoder 创建圆形图像

简介

动画可以既吸引视觉,又具有功能性,如简单的按钮按下所示。按钮按下的图形表示使应用程序生动起来,同时它通过向用户提供对事件的视觉响应来提供功能性价值。

Android 框架提供了几个动画系统,以简化在您的应用程序中包含动画的过程。它们包括以下内容:

  • 视图动画(原始动画系统):它通常需要更少的代码,但动画选项有限

  • 属性动画:这是一个更灵活的系统,允许对任何对象的任何属性进行动画处理

  • Drawable 动画:它使用可绘制资源创建逐帧动画(如电影)

属性动画系统是在 Android 3.0 中引入的,通常比视图动画更受欢迎,因为其灵活性。视图动画的主要缺点包括以下内容:

  • 可以动画化的方面有限,例如缩放和旋转

  • 只能对视图的内容进行动画处理;它不能改变视图在屏幕上的绘制位置(因此不能对在屏幕上移动的球进行动画处理)

  • 只能对视图对象进行动画处理

这里有一个简单的示例,演示了如何使用视图动画来“闪烁”视图(按钮按下的简单模拟):

Animation blink =AnimationUtils.loadAnimation(this,R.anim.blink); 
view.startAnimation(blink); 

以下是位于 res/anim 文件夹中的 blink.xml 资源文件的目录内容:

<?xml version="1.0" encoding="utf-8"?> 
<set > 
    <alpha android:fromAlpha="1.0" 
        android:toAlpha="0.0" 
        android:background="#000000" 
        android:interpolator="@android:anim/linear_interpolator" 
        android:duration="100" 
        android:repeatMode="restart" 
        android:repeatCount="0"/> 
</set> 

如您所见,创建此动画非常简单,因此如果视图动画实现了您的目标,请使用它。当它不符合您的需求时,转向属性动画系统。我们将通过在 使用片段创建卡片翻转动画使用自定义过渡创建缩放动画 菜谱中使用新的 objectAnimator 来演示属性动画。

动画过渡动画 - 定义场景和应用过渡 菜单将提供有关 Android 过渡框架的额外信息,我们将在许多菜谱中使用该框架。

插值器是一个定义动画变化速率的函数。

Interpolators 将在本章的几个菜谱中以及在前面的闪烁示例中提到。插值器定义了过渡的计算方式。线性插值器将在设定的时间内均匀地计算变化,而 AccelerateInterpolator 函数将创建在持续时间内的更快移动。以下是可用的完整 Interpolators 列表,以及相应的 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 选项,并在提示活动类型时选择 Empty Activity。

对于这个菜谱,我们需要一张大图。我们转向 Unsplash.com 下载了一张免费图片,(unsplash.com),尽管任何大(多兆字节)的图片都适用。

如何操作...

如同在 准备工作 中提到的,我们需要一张大图来演示缩放。一旦你有了这张图,请按照以下步骤操作:

  1. 将图片复制到你的 res/drawable 文件夹。

  2. 打开 activity_main.xml 并将现有的 TextView 替换为以下 ImageView

<android.support.v7.widget.AppCompatImageView
    android:id="@+id/imageViewThumbnail"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 现在,打开 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);
}
  1. 将以下代码添加到现有的 onCreate() 方法中:
AppCompatImageView imageView = findViewById(R.id.imageViewThumbnail);
imageView.setImageBitmap(
        loadSampledResource(R.drawable.miguel_henriques_789508_unsplash, 100, 100));
  1. 在设备或模拟器上运行应用程序。

它是如何工作的...

loadSampledResource() 方法的目的是加载一个较小的图像,以减少图像的内存消耗。如果我们尝试加载原始的全尺寸图像(参见之前的 准备工作 部分),应用程序将需要超过 3 MB 的 RAM 来加载。这比大多数设备能处理的内存要多(至少目前是这样),即使它能完全加载,也不会为我们的缩略图视图提供任何视觉上的好处。

为了避免Out of Memory情况,我们使用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类可用于执行后台网络处理,但还有许多其他库可用(食谱末尾的链接)。

更多内容...

重要的是要注意,我们传递给loadSampledResource()方法的targetHeighttargetWidth参数实际上并没有设置图像大小。如果您使用与我们相同的图像大小(4,000 x 6,000)运行应用程序,样本大小将为 32,导致加载的图像大小为 187 x 125。

如果您的布局需要特定大小的图像,您可以在布局文件中设置大小,或者您可以直接使用 Bitmap 类修改图像大小。

参考以下内容

过渡动画 – 定义场景并应用过渡

Android 过渡框架提供了以下功能:

  • 组级动画:动画应用于层次结构中的所有视图

  • 基于过渡的动画:基于起始和结束属性变化的动画

  • 内置动画:一些常见的过渡效果,如淡入/淡出

    以及移动

  • 资源文件支持:将动画值保存到资源(XML)文件中以便加载

    在运行时

  • 生命周期回调:在动画期间接收回调通知

过渡动画由以下组成:

  • 起始场景:动画开始时的视图(或ViewGroup

  • Transition:更改类型(见后文)

  • 结束场景:结束视图(或ViewGroup

  • Transitions:Android 为以下三种过渡提供了内置支持:

    • 自动过渡(默认过渡):淡出,移动,调整大小,然后淡入(按此顺序)

    • 淡入:淡入,淡出(默认),或两者(指定顺序)

    • ChangeBounds:移动和调整大小

过渡框架将自动创建从起始场景到结束场景所需的帧以进行动画。

以下是在使用以下类时,过渡框架的一些已知限制:

  • SurfaceView:由于SurfaceView动画是在非 UI 线程上执行的,因此动画可能不正确,可能与应用程序不同步

  • TextView:动画文本大小更改可能不会正确工作,导致文本跳转到最终状态

  • AdapterView:扩展AdapterView的类,如ListViewGridView,可能会挂起

  • TextureView:某些过渡可能无法工作

本教程提供了一个关于使用过渡动画系统的快速教程。我们将从定义场景和过渡资源开始,然后应用过渡,从而创建动画。以下步骤将指导您在 XML 中创建资源,因为它们通常是推荐的。资源也可以通过代码创建,我们将在

更多内容部分讨论。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为TransitionAnimation。在目标 Android 设备对话框中,选择手机和平板选项,并将最小 SDK 选择为 API 19(或更高)。当被提示选择活动类型时,选择空活动。

如何操作...

下面是创建资源文件并应用过渡动画的步骤:

  1. 将现有的activity.main.xml布局替换为以下 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> 
  1. 使用以下 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> 
  1. 创建一个新的过渡资源目录(文件 | 新建 | Android 资源目录,并将过渡作为资源类型选择)。

  2. res/transition文件夹中创建一个名为transition_move.xml的新文件,使用以下 XML:

<?xml version="1.0" encoding="utf-8"?> 
<changeBounds xmlns:android=
     "http://schemas.android.com/apk/res/android" />
  1. 使用以下代码添加goAnimate()方法:
public void goAnimate(View view) {
    ViewGroup root = 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);
}
  1. 你已经准备好在设备或模拟器上运行应用程序了。

它是如何工作的...

你可能觉得代码本身相当简单。正如在菜谱介绍中概述的,我们只需要创建起始场景和结束场景,并设置过渡类型。以下是代码的详细分解:

  • 创建起始场景:以下代码行将加载起始场景:
ViewGroup root = 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 = findViewById(R.id.layout);
Scene scene = new Scene(root);

Transition transition = new ChangeBounds();
TransitionManager.beginDelayedTransition(root,transition);

TextView textViewTop = 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 = 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来在响应设备移动时动画化指南针。以下是我们在物理设备上的指南针应用程序的截图:

图片

准备工作

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

我们需要一个用于指南针指示器的图像。在 www.Pixabay.Com 上有一个图像,可以通过此链接为我们工作:

pixabay.com/en/geography-map-compass-rose-plot-42608/

虽然不是必需的,但此图像具有透明背景,在旋转图像时看起来更好。

如何操作...

如前文准备就绪部分所述,我们需要一个指南针的图像。您可以下载之前链接的图像,或者使用您喜欢的任何图像,然后按照以下步骤操作:

  1. 将您的图像复制到res/drawable文件夹,并将其命名为compass.png

  2. 打开activity_main.xml,将现有的TextView替换为以下ImageView

<android.support.v7.widget.AppCompatImageView
    android:id="@+id/imageViewCompass"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:src="img/compass"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 现在,打开MainActivity.java,添加以下全局变量声明:
private SensorManager mSensorManager;
private Sensor mMagnetometer;
private Sensor mAccelerometer;
private AppCompatImageView mImageViewCompass;
private float[] mGravityValues=new float[3];
private float[] mAccelerationValues=new float[3];
private float[] mRotationMatrix=new float[9];
private float mLastDirectionInDegrees = 0f;
  1. 将以下SensorEventListener类添加到MainActivity类中:
private SensorEventListener mSensorListener = new SensorEventListener() {
    @Override
    public void onSensorChanged(SensorEvent event) {
        calculateCompassDirection(event);
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int
            accuracy) {
        //Nothing to do 
    }
};
  1. 如下重写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);
}
  1. 将以下代码添加到现有的onCreate()方法中:
mImageViewCompass = findViewById(R.id.imageViewCompass);
mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  1. 最终代码执行实际的计算和动画:
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;
    }
}
  1. 您已准备好运行应用程序。虽然您可以在模拟器上运行此应用程序,但没有加速度计和磁力计,您将看不到指南针移动。

它是如何工作的...

由于我们已经在读取传感器数据 - 使用 Android 传感器框架部分(在前一章)中介绍了读取传感器数据,因此我们不会重复解释传感器框架,而是直接跳到calculateCompassDirection()方法。

我们直接从onSensorChanged()回调中调用此方法。由于我们使用同一个类来处理磁力计和加速度计的传感器回调,我们首先检查在SensorEvent中报告的是哪个传感器。然后,我们调用SensorManager.getRotationMatrix(),传入最后一批传感器数据。如果计算成功,它将返回RotationMatrix,我们使用它来调用SensorManager.getOrientation()方法。请注意,getOrientation()将在orientationValues数组中返回以下数据:

  • 方位角value [0]

  • 俯仰value [1]

  • 倾斜value [2]

方位角以弧度为单位报告,方向相反,因此我们反转符号并使用Math.toDegrees()将其转换为度数。方位角表示北方的方向,因此我们在RotateAnimation中使用它。

使用SensorManager已经完成的数学计算,实际的指南针动画非常简单。我们使用前一个方向和新的方向创建RotateAnimation。我们使用Animation.RELATIVE_TO_SELF标志和 0.5f(或 50%)将图像的中心设置为旋转点。在调用startAnimation()更新指南针之前,我们使用setDuration()setFillAfter(true)设置动画持续时间。(使用true表示我们希望在动画完成后将图像保留“原样”;否则,图像将重置回原始图像。)最后,我们保存下一次传感器更新时的方位角。

还有更多...

值得花些时间来实验 RotationAnimation 设置和传感器更新时间。在我们的注册传感器监听器调用中,我们使用 SensorManager.SENSOR_DELAY_FASTEST 以及 50 毫秒的 setDuration() 来创建快速动画。你也可以尝试使用较慢的传感器更新和较慢的动画,并比较结果。

相关内容

使用 ViewPager 创建幻灯片放映

此配方将向您展示如何使用 ViewPager 类创建幻灯片放映。以下是显示从一张图片切换到另一张图片的截图:

准备工作

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

我们需要四个图像用于幻灯片放映。

如何做到这一点...

我们将为幻灯片放映创建一个显示每张图像的片段,然后在主活动中设置 ViewPager。以下是步骤:

  1. 将四个图像复制到 /res/drawable 文件夹,并命名为 slide_0slide_3,保持原始文件扩展名。

  2. 使用以下 XML 创建一个新的布局文件 fragment_slide.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v7.widget.AppCompatImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/imageView"
        android:layout_gravity="center_horizontal" />
</LinearLayout>
  1. 现在,创建一个新的名为 SlideFragment.java 的 Java 类。它将扩展 Fragment 如下:
public class SlideFragment extends Fragment { 
  • 从支持库导入,结果如下导入:
import android.support.v4.app.Fragment; 
  1. 添加以下全局声明:
private int mImageResourceID; 
  1. 添加以下空的默认片段构造函数:
public SlideFragment() {}
  1. 添加以下方法以保存图像资源 ID:
public void setImage(int resourceID) { 
    mImageResourceID=resourceID; 
} 
  1. 如下重写 onCreateView()
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
    ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_slide, container, false);
    AppCompatImageView imageView = rootView.findViewById(R.id.imageView);
    imageView.setImageResource(mImageResourceID);
    return rootView;
}
  1. 我们的主要活动将只显示一个 ViewPager。打开 activity_main.xml 并将文件内容替换为以下内容:
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  1. 现在,打开 MainActivity.java 并添加以下全局声明:
private final int PAGE_COUNT=4; 
private ViewPager mViewPager; 
private PagerAdapter mPagerAdapter; 
  • 使用以下导入:
import android.support.v4.view.PagerAdapter; 
import android.support.v4.view.ViewPager; 
  1. 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; 
  1. MainActivity 类中重写 onBackPressed() 如下:
@Override
public void onBackPressed() {
    if (mViewPager.getCurrentItem() == 0) {
        super.onBackPressed();
    } else {
        mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1);
    }
}
  1. 将以下代码添加到 onCreate() 方法中:
mViewPager = findViewById(R.id.viewPager);
mPagerAdapter = new SlideAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mPagerAdapter);
  1. 在设备或模拟器上运行应用程序。

它是如何工作的...

第一步是创建一个片段。由于我们正在进行幻灯片放映,我们只需要 ImageViewer。我们还把 MainActivity 改为扩展 FragmentActivity,以便将片段加载到 ViewPager 中。

ViewPager 使用 FragmentStatePagerAdapter 作为片段过渡的来源。我们创建 SlideAdapter 来处理 FragmentStatePagerAdapter 类的两个回调:

  • getCount()

  • getItem()

此外,getCount() 简单地返回我们幻灯片中页面的数量,而 getItem() 返回要显示的实际片段。这是我们指定要显示的图片的地方。如您所见,很容易添加或更改幻灯片。

处理 退格 键不是 ViewPager 的要求,但它确实提供了更好的用户体验。然而,onBackPressed() 会递减当前页面,直到它达到第一页,然后它将 退 键发送到超类,从而退出应用程序。

还有更多...

如示例所示,ViewPager 负责大部分工作,包括处理过渡动画。如果我们想自定义过渡动画,可以通过在 ViewPager.PageTransformer 接口上实现 transformPage() 回调来实现。(有关自定义动画的示例,请参阅下一道菜谱。)

创建设置向导

ViewPager 也可以用来创建设置向导。不是创建一个用于显示图片的单个片段,而是为向导的每个步骤创建一个片段,并在 getItem() 回调中返回适当的片段。

参见

使用片段创建卡片翻转动画

卡片翻转是一种常见的动画,我们将通过使用片段转换来演示。我们将使用两张不同的图片,一张用于正面,一张用于背面,以创建卡片翻转效果。我们需要四个动画资源,两个用于正面过渡,两个用于背面过渡,我们将使用 objectAnimator 在 XML 中定义这些资源。

这是我们将构建的应用程序的截图,展示了卡片翻转动画的实际效果:

准备工作

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

对于玩牌的前后图像,我们在 www.pixabay.com 找到了以下图像:

如何操作...

我们需要两个片段:一个用于卡片的前面,另一个用于背面。每个片段将定义卡片的图像。然后,我们需要四个动画文件来实现完整的卡片翻页效果。以下是正确设置项目结构和创建所需资源的步骤:

  1. 一旦你有了卡片的正反两面图像,将它们复制到res/drawable文件夹中,分别命名为card_front.jpgcard_back.jpg(如果图像的原始文件扩展名不同,请保持原始文件扩展名)。

  2. 创建一个动画资源目录:res/animator。(在 Android Studio 中,转到文件 | 新建 | Android 资源目录。当新建 Android 资源对话框显示时,在资源类型下拉列表中选择animator。)

  3. res/animator中使用以下 XML 创建card_flip_left_enter.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <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>
  1. res/animator中使用以下 XML 创建card_flip_left_exit.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <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>
  1. res/animator中使用以下 XML 创建card_flip_right_enter.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <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>
  1. res/animator中使用以下 XML 创建card_flip_right_exit.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <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>
  1. 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>
  1. res/layout中使用以下 XML 创建一个新的文件,命名为fragment_card_front.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.AppCompatImageView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="img/card_front"
    android:scaleType="centerCrop" />
  1. res/layout中使用以下 XML 创建一个新的文件,命名为fragment_card_back.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.AppCompatImageView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="img/card_back"
    android:scaleType="centerCrop" /> 
  1. 使用以下代码创建一个新的 Java 类CardFrontFragment
public class CardFrontFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_card_front, container, false);
    }
}
  1. 使用以下代码创建一个新的 Java 类CardBackFragment
public class CardBackFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, 
                             Bundle savedInstanceState) {        
        return inflater.inflate(R.layout.fragment_card_back, container, false);
    }
} 
  1. 将现有的activity_main.xml文件替换为以下 XML:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  1. 打开MainActivity.java并添加以下全局声明:
boolean mShowingBack = false;
  1. 将以下代码添加到现有的onCreate()方法中:
FrameLayout frameLayout = findViewById(R.id.container);
frameLayout.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        flipCard();
    }
});

if (savedInstanceState == null) {
    getSupportFragmentManager()
            .beginTransaction()
            .add(R.id.container, new CardFrontFragment())
            .commit();
}
  1. 添加以下方法,它处理实际的片段转换:
void flipCard() {
    if (mShowingBack) {
        mShowingBack = false;
        getSupportFragmentManager().popBackStack();
    } else {
        mShowingBack = true;
        getSupportFragmentManager()
                .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.container, new CardBackFragment())
                .addToBackStack(null)
                .commit();
    }
}
  1. 你已经准备好在设备或模拟器上运行应用程序了。

它是如何工作的...

创建卡片翻页的大部分工作在于设置资源。由于我们想要卡片的正反两面视图,我们创建了两个包含适当图像的片段。当卡片被按下时,我们调用flipCard()方法。实际的动画由setCustomAnimations()处理。这就是我们传入在 XML 中定义的四个动画资源的地方。正如你所见,Android 使这变得非常简单。

需要注意的是,我们没有使用 Support Library Fragment Manager,因为支持库不支持objectAnimator。如果你想支持 Android 3.0 之前的版本,你需要包含旧的动画资源,并在运行时检查操作系统版本,或者直接在代码中创建动画资源。(见下一道菜谱。)

参见

  • 请参阅下一道菜谱,使用自定义过渡创建缩放动画,以查看在代码中创建的动画资源示例。

  • 请参考整数资源类型网页

使用自定义过渡创建缩放动画

之前的菜谱,“使用片段创建卡片翻转动画”演示了使用动画资源文件实现的过渡动画。在本菜谱中,我们将使用代码中创建的动画资源创建缩放效果。应用程序显示缩略图图像,然后按下时扩展到放大图像。

以下图像包含三个截图,展示了缩放动画的实际效果:

准备工作

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

对于本菜谱所需的图像,我们从www.pixabay.com下载了一张图片并将其包含在项目源文件中,但你可以使用任何图像。

如何实现...

一旦您的图像准备就绪,请按照以下步骤操作:

  1. 将您的图像复制到res/drawable文件夹,并命名为image.jpg(如果不是 JPEG 图像,则保留原始文件扩展名)。

  2. 现在,打开activity_main.xml并用以下内容替换现有的 XML:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    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">
        <android.support.v7.widget.AppCompatImageButton
            android:id="@+id/imageViewThumbnail"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:scaleType="centerCrop"
            android:background="@android:color/transparent"/>
    </LinearLayout>
    <android.support.v7.widget.AppCompatImageView
        android:id="@+id/imageViewExpanded"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="invisible" />
</FrameLayout>
  1. 现在,打开MainActivity.java并声明以下全局变量:
private Animator mCurrentAnimator;
private AppCompatImageView mImageViewExpanded;
  1. 将我们在“缩小大图像以避免内存不足异常”菜谱中创建的loadSampledResource()方法添加到缩放图像中:
private 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));
}
  1. 将以下代码添加到onCreate()方法中:
final AppCompatImageButton imageViewThumbnail = findViewById(R.id.imageViewThumbnail);
imageViewThumbnail.setImageBitmap(loadSampledResource(R.drawable.image, 100, 100));
imageViewThumbnail.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        zoomFromThumbnail(imageViewThumbnail);
    }
});
mImageViewExpanded = 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);
    }
});
  1. 添加以下zoomFromThumbnail()方法,它处理实际的动画,稍后解释:
private void zoomFromThumbnail(final AppCompatImageButton 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;
}
  1. 在设备或模拟器上运行应用程序。

它是如何工作的...

首先,看看我们使用的布局文件。有两个部分:包含ImageView缩略图的LinearLayout和扩展的ImageView。我们控制这两个视图的可见性,当图像被点击时。我们使用与“缩小大图像以避免内存不足异常”菜谱中讨论的相同的loadSampledResource()设置起始缩略图图像。

然而,zoomFromThumbnail()是本演示中真正工作的地方。有很多代码,分解如下。

首先,我们将当前动画存储在mCurrentAnimator中,这样我们可以在动画正在运行时取消它。

接下来,我们使用getGlobalVisibleRect()方法获取图像的起始位置。此方法返回视图的屏幕位置。当我们获取扩展ImageView的可见边界时,我们也会获取视图的GlobalOffset,以便将坐标从应用坐标转换为屏幕坐标。

设置起始边界后,下一步是计算结束边界。我们希望保持最终图像的相同宽高比,以防止其变形。我们需要计算边界需要如何调整,以保持宽高比在扩展的ImageView内。介绍中显示的截图显示了如何调整此图像的大小,但这一点会因图像和设备而异。

在计算了起始和结束边界之后,我们现在可以创建动画。实际上,在这种情况下有四个动画,每个矩形的一个点对应一个动画,如以下代码所示:

animatorSet.play(ObjectAnimator.ofFloat(mImageViewExpanded, View.X,
        startBounds.left, finalBounds.left))
        .with(ObjectAnimator.ofFloat(mImageViewExpanded, View.Y,
                startBounds.top, finalBounds.top))
        .with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_X, startScale, 1f))
        .with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_Y, startScale, 1f));

这两行代码控制动画时间:

animatorSet.setDuration(1000); 
animatorSet.setInterpolator(new AccelerateInterpolator()); 

setDuration()方法告诉动画对象之前设置的平移动画需要多长时间。然而,setInterpolator()控制平移的方式。(在简介中提到了 Interpolator,并在本食谱的另请参阅部分提供了链接。)在用start()方法开始动画后,我们将当前动画保存到mCurrentAnimator变量中,以便在需要时取消动画。我们创建一个AnimatorListenerAdapter来响应动画事件,并在动画完成后清除mCurrentAnimator变量。

还有更多...

当用户按下展开的图像时,应用程序只是隐藏展开的ImageView并将缩略图设置为可见。我们可以在mImageViewExpanded点击事件中使用展开的边界作为起点创建反向缩放动画,然后返回到缩略图边界。(在zoomFromThumbnail()中创建mImageViewExpanded事件可能更容易,这样可以避免再次计算起始和结束边界。)

获取默认动画持续时间。

我们在设置持续时间时使用了 1,000 毫秒。我们故意使用较长的持续时间以便更容易查看动画。我们可以使用以下代码获取默认的 Android 动画持续时间:

getResources().getInteger(android.R.integer.config_shortAnimTime)

另请参阅

使用新的 ImageDecoder 库显示动画图像(GIF/WebP)。

Android P(API 28)引入了一个名为 ImageDecoder 的新库,它将弃用 BitmapFactory 类。这个新的图像库承诺将使处理位图更容易,同时还能处理旧 BitmapFactory 类不支持的其他文件格式,例如 GIF 和 WebP 动画图像。

在撰写本文时,它仅适用于运行 Android P(或更高版本)的设备,且不支持在支持库中,但根据谷歌问题跟踪器上的此问题,有计划将 ImageDecoder 添加到支持库中:issuetracker.google.com/issues/78041382

当这种情况发生时,或如果发生这种情况,之前的示例将更新为使用这个新库。现在,我们将查看新功能,那就是对显示 GIF 图像的原生支持。

准备工作

在 Android Studio 中创建一个新的项目,命名为AnimatedImage。在“目标 Android 设备”对话框中,确保为“手机和平板”选项选择 API 28(或更高版本)。当被提示选择“活动类型”时,选择“空活动”。在“配置活动”对话框(如图所示)中,取消选择“向后兼容性”选项,因为此功能在支持库中尚不可用:

我们还需要一个 GIF 图像。我们转向 Giphy.com 寻找免费版权的图像,您可以在可下载的项目文件中看到。

如何操作...

一旦您有了 GIF 图像,请按照以下步骤操作:

  1. 将您的图像复制到res/drawable文件夹。我们的文件名为giphy.gif,但您可以使用自己的文件名。

  2. 打开activity_main.xml,将现有的TextView替换为以下ImageView

<ImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 打开MainActivity.java,并将以下代码行添加到现有的onCreate()方法中:
loadGif();
  1. 最后,按照以下方式添加 loadGif 方法:
private void loadGif() {
    try {
        ImageDecoder.Source source = ImageDecoder.createSource(getResources(),
                R.drawable.giphy);
        Drawable decodedAnimation = ImageDecoder.decodeDrawable(source);

        ImageView imageView = findViewById(R.id.imageView);
        imageView.setImageDrawable(decodedAnimation);

        if (decodedAnimation instanceof AnimatedImageDrawable) {
            ((AnimatedImageDrawable) decodedAnimation).start();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  1. 在至少运行 Android P 的设备或模拟器上运行您的应用程序。

如果在运行此代码时看不到动画图像,请尝试在 AndroidManifest 中禁用硬件加速。将以下内容添加到<application>节点或<activity>节点中:

android:hardwareAccelerated="false"

它是如何工作的...

如前述代码所示,ImageDecoder库使得显示 GIF 变得非常简单。首先,您必须定义您的源图像。目前,createSource()方法可以从以下来源读取图像:

  • 资源(drawable)文件夹

  • 资产文件夹

  • ContentResolver (URI)

  • 字节缓冲区

  • 文件

(这可能在最终的 Android P 版本中有所改变。)

在我们的代码中,我们将图片复制到了 drawable 文件夹。如果我们将其复制到 assets 文件夹,代码将如下所示:

ImageDecoder.Source source = ImageDecoder.createSource(getAssets(), "giphy.gif");

在定义了图像源之后,我们只需调用decodeDrawable()来解码图像并设置 ImageView 的 drawable。一旦设置了 drawable,动画图像的最后一个关键步骤是调用start()方法。如果解码的图像是AnimatedDrawable类型(如果我们加载了一个有效的 GIF,它将是这种类型),我们就调用 start 方法来激活动画。

参见

使用新的 ImageDecoder 创建圆形图像

如前所述,ImageDecoder 库是 Android P 中引入的新库,它承诺了许多之前使用 BitmapFactory 类不可用的新功能和令人兴奋的功能。其中之一是使用后处理器应用图像效果的能力。后处理器是一个新的辅助类,允许您在图像加载后添加自定义处理(或操作)。自定义处理可能包括向图像添加色调、在图像上绘制(如邮票)、添加边框,或者在我们的例子中使图像圆形。

在我们的例子中,我们从矩形图像(从 Pixabay.com 下载,您可以在以下链接中看到:pixabay.com/en/wallpaper-background-eclipse-1492818/)开始。然后我们应用后处理器来创建圆形图像,正如您可以在下面的屏幕截图中所见:

这是 ImageDecoder 库中可用的另一个令人兴奋的新功能,因为到目前为止,开发者通常转向第三方库。尽管这些库中的许多仍然非常有用,尤其是在处理列表中的图像加载时;对于像创建圆形图像这样简单的事情,比如头像,现在有一个简单的本地解决方案。

准备工作

在 Android Studio 中创建一个新的项目,命名为 CircleImage。在“目标 Android 设备”对话框中,确保为“手机和平板”选项选择 API 28(或更高版本)。当提示选择“活动类型”时,选择“空活动”。在“配置活动”对话框(如下所示)中,取消选择“向后兼容”选项,因为此功能尚不支持在支持库中。

如何操作...

一旦您有了 GIF 图像,请按照以下步骤操作:

  1. 将图片复制到 res/drawable 文件夹中。(本例使用名为 stars.jpg 的图片。请使用您的图片名称。)如果它比我们在这里创建的圆的大小小,您需要使用更小的半径。

  2. 打开 activity_main.xml 并将现有的 TextView 替换为以下 ImageView

<ImageView
    android:id="@+id/imageView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 打开 MainActivity.java 并将以下代码添加到类声明中:
PostProcessor mCirclePostProcessor = new PostProcessor() {
    @Override
    public int onPostProcess(Canvas canvas) {
        Path path = new Path();
        path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
        int width = canvas.getWidth();
        int height = canvas.getHeight();
        path.addCircle(width/2,height/2,600, Path.Direction.CW);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.TRANSPARENT);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
        canvas.drawPath(path, paint);
        return PixelFormat.TRANSLUCENT;
    }
};
  1. 将以下代码行添加到现有的 onCreate() 方法中:
loadImage();
  1. 需要添加的最后一段代码是以下 loadImage() 方法:
private void loadImage() {
    ImageDecoder.Source source = ImageDecoder.createSource(getResources(),
            R.drawable.stars);

    ImageDecoder.OnHeaderDecodedListener listener = 
            new ImageDecoder.OnHeaderDecodedListener() {
        public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
                                    ImageDecoder.Source source) {
            decoder.setPostProcessor(mCirclePostProcessor);
        }
    };
    try {
        Drawable drawable = ImageDecoder.decodeDrawable(source, listener);
        ImageView imageView = findViewById(R.id.imageView);
        imageView.setImageDrawable(drawable);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  1. 在至少运行 Android P 的设备或模拟器上运行应用程序。

它是如何工作的...

我们从与之前食谱相同的 XML 布局开始。如果我们省略了添加后处理器的步骤,我们会得到一个标准的矩形图像。为了自己看看,请在 OnHeaderDecodedListener 中注释掉以下行代码:

decoder.setPostProcessor(mCirclePostProcessor);

这里正在进行的这项工作的核心是在第 3 步中创建的 PostProcessor。尽管有几行代码,但所做的工作非常简单。它只是创建一个圆(使用我们指定的尺寸)并清除(通过将颜色设置为 TRANSPARENT)不在我们圆内的所有内容。

关键是设置后处理器,这只能在onHeaderDecoded()回调中完成。这就是我们首先创建OnHeaderDecodedListener的原因,这样我们就可以获取解码器的引用。

还有更多...

如果你想使用圆角而不是圆形图像呢?通过在后处理器的Path创建中进行一个简单的更改,你就可以实现这种效果。在创建Path时,不要使用addCircle()调用,而是使用以下代码行代替:

path.addRoundRect(0, 0, width, height, 250, 250, Path.Direction.CW);

使用 250 的值创建一个非常圆滑的角落,因此请进行实验以获得所需的圆滑程度。查看“也见”部分中的参考链接以获取有关后处理器和Path的更多信息。

也见

第十一章:初探 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。使用默认的 Phone & Tablet 选项,并在提示活动类型时选择 Empty Activity。

如何做到这一点...

我们首先在 Android Manifest 中指出应用程序使用 OpenGL,然后我们将 OpenGL 类添加到活动中。以下是步骤:

  1. 打开 Android Manifest 并添加以下 XML:
<uses-feature android:glEsVersion="0x00020000" android:required="true" /> 
  1. 打开 MainActivity.java 并添加以下全局变量:
private GLSurfaceView mGLSurfaceView;
  1. 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);
    }
}
  1. MainActivity 类中添加另一个内部类:
class CustomGLSurfaceView extends GLSurfaceView {

    private final GLRenderer mGLRenderer;

    public CustomGLSurfaceView(Context context){
        super(context);
        setEGLContextClientVersion(2);
        mGLRenderer = new GLRenderer();
        setRenderer(mGLRenderer);
    }
}
  1. 修改现有的 onCreate() 方法如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mGLSurfaceView = new CustomGLSurfaceView(this);
    setContentView(mGLSurfaceView);
}
  1. 你现在可以开始在设备或模拟器上运行应用程序了。

它是如何工作的...

如果你运行了前面的应用程序,你会看到活动创建并且背景设置为灰色。由于这些是设置 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 轴直接从屏幕或直接在屏幕后面出来。

我们将创建一个Triangle类,因为它是基础形状。在 OpenGL 中,您通常使用三角形的集合来创建对象。要使用 OpenGL 绘制形状,我们需要定义以下内容:

  • 顶点着色器:这是为了绘制形状

  • 片段着色器:这是为了给形状上色

  • 程序:这是前面着色器的 OpenGL ES 对象

着色器使用OpenGL 着色语言GLSL)定义,然后编译并添加到 OpenGL 程序对象中。

以下是两个截图,显示了三角形在竖直方向上的样子:

当方向旋转为横幅时,这里是相同的图像:

准备工作

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

本配方使用上一配方中创建的 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;
  1. 将以下loadShader()方法添加到Triangle类中:
public int loadShader(int type, String shaderCode){
     int shader = GLES20.glCreateShader(type);
     GLES20.glShaderSource(shader, shaderCode);
     GLES20.glCompileShader(shader);
     return shader; 
} 
  1. 添加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); 
} 
  1. 按照以下方式添加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); 
} 
  1. 现在,打开MainActivity.java并在GLRenderer类中添加一个Triangle变量,如下所示:
private Triangle mTriangle; 
  1. onSurfaceCreated()回调中初始化Triangle变量,如下所示:
mTriangle = new Triangle();
  1. onDrawFrame()回调中,在调用glClear之后调用Triangledraw()方法:
mTriangle.draw(); 
  1. 您已准备好在设备或模拟器上运行应用程序。

工作原理...

如介绍中所述,要使用 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 中,我们使用projection*来完成这个操作。这个食谱将展示如何使用投影来匹配 GLSurfaceView 坐标与设备坐标。除了投影,我们还将展示如何设置相机视图。以下是显示最终结果的截图:

图片

准备工作

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

这个食谱基于之前的食谱,在 GLSurfaceView 上绘制形状。如果你还没有之前的食谱,请在开始这些步骤之前先从那里开始。

如何操作...

如前所述,这个食谱将在之前的食谱基础上构建,因此在开始之前请完成这些步骤。我们将修改之前的代码以添加投影和相机视图到绘图计算中。以下是步骤:

  1. 打开Triangle类,并在现有声明中添加以下全局声明:
private int mMVPMatrixHandle; 
  1. vertexShaderCode中添加一个矩阵变量并在位置计算中使用它。以下是最终结果:
private final String vertexShaderCode = "attribute vec4 vPosition;" +
                "uniform mat4 uMVPMatrix;" +
                "void main() {" +
                "  gl_Position = uMVPMatrix * vPosition;" +
                "}";
  1. draw()方法修改为传递一个矩阵参数,如下所示:
public void draw(float[] mvpMatrix) {
  1. 要使用变换矩阵,请在draw()方法中在GLES20.glDrawArrays()方法之前添加以下代码:
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); 
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
  1. 打开MainActivity.java并将以下类变量添加到GLRenderer类中:
private final float[] mMVPMatrix = new float[16]; 
private final float[] mProjectionMatrix = new float[16]; 
private final float[] mViewMatrix = new float[16]; 
  1. 修改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); 
} 
  1. 修改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); 
} 
  1. 你已经准备好在设备或模拟器上运行应用程序。

它是如何工作的...

首先,我们修改vertexShaderCode以包含一个矩阵变量。我们使用传入的参数高度和宽度在onSurfaceChanged()回调中计算矩阵。我们将变换矩阵传递给draw()方法,以便在计算绘制位置时使用。

在调用draw()方法之前,我们计算相机视图。这两行代码计算了相机视图:

Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
 Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

没有这段代码,实际上不会绘制三角形,因为相机视角不会“看到”我们的顶点。(这回到了我们讨论顶点顺序如何决定图像的前后。)

当你运行程序时,你会看到 简介 中所示的输出。注意,我们现在有一个等边三角形(所有边相等),即使显示被旋转。

还有更多...

在下一个食谱中,我们将开始展示 OpenGL 旋转三角形的强大功能。

通过旋转移动三角形

我们到目前为止用 OpenGL 演示的内容可能使用传统的画布或可绘制对象会更简单。这个食谱将通过旋转三角形展示 OpenGL 的一些强大功能。不是我们不能用其他绘图方法创建运动,而是我们用 OpenGL 来做这件事有多容易?

这个食谱将演示如何旋转三角形,如下面的截图所示:

准备工作

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

这个食谱基于之前的食谱,在绘图时应用投影和相机视图。如果你还没有完成那些步骤,请参考之前的食谱。

如何做到这一点...

由于我们是从上一个食谱继续的,所以我们几乎没有工作要做。打开 MainActivity.java 并按照以下步骤操作:

  1. GLRendered 类添加一个矩阵:
private float[] mRotationMatrix = new float[16]; 
  1. 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); 
  1. 你现在可以运行应用程序在设备或模拟器上。

它是如何工作的...

我们使用 Matrix.setRotateM() 方法根据我们传递的角度计算一个新的旋转矩阵。在这个例子中,我们使用系统运行时间来计算一个角度。我们可以使用任何我们想要的方法来得到一个角度,比如传感器读取或触摸事件。

还有更多...

使用系统时钟提供了创建连续运动的额外好处,这对于演示目的当然看起来更好。下一个食谱将演示如何使用用户输入来得到旋转三角形的角。

渲染模式

OpenGL 提供了一个 setRenderMode() 选项,只有在视图变脏时才绘制。这可以通过在 setRenderer() 调用下方添加以下代码到 CustomGLSurfaceView() 构造函数中来实现:

setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 

这将导致显示只更新一次,然后等待我们使用 requestRender() 请求更新。

通过用户输入旋转三角形

之前的例子演示了根据系统时钟旋转三角形。这创建了一个持续旋转的三角形,这取决于我们使用的渲染模式。但如果你想要响应用户的输入呢?

在这个食谱中,我们将展示如何通过重写 GLSurfaceViewonTouchEvent() 回调来响应用户输入。我们仍然会使用 Matrix.setRotateM() 方法来旋转三角形,但不是从系统时间中推导角度,而是基于触摸位置计算角度。

下面是一张显示此配方在物理设备上运行的截图(为了突出触摸,已启用 Show touches 开发者选项):

准备工作

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

这个配方演示了与之前配方不同的方法,因此将基于 在绘制时应用投影和相机视图(与之前的配方相同的起点。)

如何操作...

如前所述,我们将继续,不是从之前的配方开始,而是从 在绘制时应用投影和相机视图 配方开始。打开 MainActivity.java 并按照以下步骤操作:

  1. 将以下全局变量添加到 MainActivity 类中:
private float mCenterX=0; 
private float mCenterY=0; 
  1. 将以下代码添加到 GLRendered 类中:
private float[] mRotationMatrix = new float[16];
public volatile float mAngle;
public void setAngle(float angle) {
    mAngle = angle;
}
  1. 在同一类中,通过将现有的 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);
  1. 将以下代码添加到 onSurfaceChanged() 回调中:
mCenterX=width/2; 
mCenterY=height/2; 
  1. 将以下代码添加到 CustomGLSurfaceView 构造函数中,该函数位于 setRenderer() 下方:
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); 
  1. 将以下 onTouchEvent() 添加到 CustomGLSurfaceView 类中:
@Override
public boolean onTouchEvent(MotionEvent e) {
    float x = e.getX();
    float y = e.getY();
    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:
            double angleRadians = Math.atan2(y-mCenterY,x-mCenterX);
            mGLRenderer.setAngle((float)Math.toDegrees
                    (-angleRadians));
            requestRender();
    }
    return true;
}
  1. 你现在可以运行应用程序在设备或模拟器上。

它是如何工作的...

与之前的配方相比,这个例子明显的区别在于我们如何推导出传递给 Matrix.setRotateM() 调用的角度。我们还使用 setRenderMode() 改变了 GLSurfaceView 的渲染模式,使其仅在请求时绘制。我们在 onTouchEvent() 回调中计算了一个新角度后,使用 requestRender() 发出了请求。

我们还展示了自定义 GLSurfaceView 类的重要性。如果没有我们的 CustomGLSurfaceView 类,我们就无法覆盖 onTouchEvent 回调或其他来自 GLSurfaceView 的回调。

还有更多...

这完成了 OpenGL ES 配方,但我们只是刚刚触及了 OpenGL 的强大功能。如果你认真想学习 OpenGL,请查看下一节中的链接,并查看许多关于 OpenGL 编写的书籍之一。还有许多框架可用于图形和游戏开发,包括 2D 和 3D。

相关内容

第十二章:多媒体

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

  • 使用 SoundPool 播放声音效果

  • 使用 MediaPlayer 播放音频

  • 在您的应用中响应用户硬件媒体控件

  • 使用默认相机应用拍照

  • 使用 Camera2 API 拍照

简介

在前几章中我们已经探讨了图形和动画,现在是时候看看 Android 中可用的声音选项了。播放声音最常用的两个选项如下:

  • SoundPool:这是用于短声音片段的

  • MediaPlayer:这是为较大的声音文件(如音乐)和视频文件设计的

前两个教程将探讨如何使用这些库。我们还将探讨如何使用与声音相关的硬件,例如音量控制和媒体播放控制(播放、暂停、下一曲和上一曲,通常在耳机上可用)。

本章的其余部分将专注于使用相机,无论是通过 Intents(将相机请求传递到默认相机应用)还是直接使用相机 API。我们将展示一个使用 Android 5.0 Lollipop(API 21)发布的 Camera2 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 选项,并在提示活动类型时选择 Empty Activity。

为了演示同时播放声音,项目中至少需要两个音频文件。我们访问了 SoundBible.com (soundbible.com/royalty-free-sounds-5.html) 并找到了两个免费、公有领域的声音,以包含在下载的项目文件中。

第一个声音是一个较长的播放声音:soundbible.com/2032-Water.html

第二个声音较短:soundbible.com/1615-Metal-Drop.html

如何做到这一点...

如前所述,我们需要在项目中包含两个音频文件。一旦你的音频文件准备就绪,请按照以下步骤操作:

  1. 创建一个新的原始文件夹(文件 | 新建 | Android 资源目录)并在资源类型下拉菜单中选择原始。

  2. 将你的音频文件复制到res/raw作为sound_1sound_2。 (保留它们的原始扩展名。)

  3. 打开activity_main.xml并用以下按钮替换现有的TextView

<Button
    android:id="@+id/button1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    android:onClick="playSound1"/>
<Button
    android:id="@+id/button2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button"
    android:onClick="playSound2"
    app:layout_constraintTop_toBottomOf="@+id/button1"/>
  1. 现在,打开ActivityMain.java并添加以下全局变量:
HashMap<Integer, Integer> mHashMap= null;
SoundPool mSoundPool;
  1. 修改现有的onCreate()方法如下:
final Button button1 = findViewById(R.id.button1);
button1.setEnabled(false);
final Button button2 = findViewById(R.id.button2);
button2.setEnabled(false);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    createSoundPoolNew();
} else {
    createSoundPoolOld();
}
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));
  1. 添加createSoundPoolNew()方法:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void createSoundPoolNew() {
    AudioAttributes audioAttributes = new AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
            .build();
    mSoundPool = new SoundPool.Builder()
            .setAudioAttributes(audioAttributes)
            .setMaxStreams(2)
            .build();
}
  1. 添加createSoundPoolOld()方法:
@SuppressWarnings("deprecation")
private void createSoundPoolOld(){
    mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0);
}
  1. 添加按钮的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);
}
  1. 如此覆盖onStop()回调:
@Override
protected void onStop() {
    mSoundPool.release();
    super.onStop();
}
  1. 在设备或模拟器上运行应用程序。

它是如何工作的...

首先要注意的细节是如何构建对象本身。正如我们在介绍中提到的,SoundPool构造函数在 Lollipop(API 21)中发生了变化。旧的构造函数已被弃用,转而使用SoundPool.Builder()。在像 Android 这样的不断变化的环境中,API 的变化非常常见,因此学习如何处理这些变化是个好主意。正如你所看到的,在这种情况下并不困难。我们只需检查当前的操作系统版本并调用相应的方法。值得注意的是这两个方法注解。第一个指定了目标 API:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)

并且第二个抑制了弃用警告:

@SuppressWarnings("deprecation")

创建SoundPool后,我们设置一个setOnLoadCompleteListener()监听器。启用按钮主要是为了演示目的,以说明SoundPool在可用之前需要加载音效资源。

使用SoundPool的最后一个要点是调用play()。我们需要传递soundID,这是我们在使用load()加载声音时返回的。play()给我们一些选项,包括音量(左右)、循环次数和播放速率。为了展示其灵活性,我们将第一个声音(较长的声音)以较低的音量播放,以创造更多的背景效果。第二个声音以较高的音量播放,并且我们播放它两次。

还有更多...

如果你只需要一个基本的音效,例如点击声,你可以使用AudioManagerplaySoundEffect()方法。以下是一个示例:

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 选项,并在提示活动类型时选择 Empty Activity。

我们还需要为这个食谱准备一个声音,并将使用之前食谱中使用的相同较长的“水”声音:

soundbible.com/2032-Water.html

如何操作...

如前所述,我们需要一个声音文件包含在项目中。一旦您准备好了声音文件,请按照以下步骤操作:

  1. 创建一个新的 raw 文件夹(文件 | 新 | Android 资源目录),并在资源类型下拉菜单中选择 raw。

  2. 将您的声音文件复制到 res/raw 目录下,命名为 sound_1。(保留原始扩展名。)

  3. 打开 activity_main.xml 并将现有的 TextView 替换为以下按钮:

<Button
    android:id="@+id/buttonPlay"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:text="Play"
    android:onClick="buttonPlay" />
<Button
    android:text="Pause"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:id="@+id/buttonPause"
    android:onClick="buttonPause"
    app:layout_constraintTop_toBottomOf="@+id/buttonPlay"/>
<Button
    android:text="Stop"
    android:layout_width="100dp"
    android:layout_height="wrap_content"
    android:id="@+id/buttonStop"
    android:onClick="buttonStop"
    app:layout_constraintTop_toBottomOf="@+id/buttonPause"/>
  1. 现在,打开 ActivityMain.java 并添加以下全局变量:
MediaPlayer mMediaPlayer;
  1. 添加 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();
    }
}
  1. 添加 buttonPause() 方法:
public void buttonPause(View view){
    if (mMediaPlayer!=null && mMediaPlayer.isPlaying()) {
        mMediaPlayer.pause();
    }
}
  1. 添加 buttonStop() 方法:
public void buttonStop(View view){
    if (mMediaPlayer!=null) {
        mMediaPlayer.stop();
        mMediaPlayer.release();
        mMediaPlayer = null;
    }
}
  1. 最后,使用以下代码覆盖 onStop() 回调:
@Override
protected void onStop() {
    super.onStop();
    if (mMediaPlayer!=null) {
        mMediaPlayer.release();
        mMediaPlayer = null;
    }
}
  1. 您现在可以运行应用程序在设备或模拟器上。

工作原理...

这里的代码相当简单。我们使用我们的声音创建 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 而不是活动。你将以相同的方式使用 MediaPlayer 库;你只需要从 UI 传递信息(如声音选择)到你的服务。

注意,由于服务在同一个 UI 线程上运行,因此你仍然不希望在服务中执行可能阻塞的操作。MediaPlayer 可以处理后台线程以防止阻塞你的 UI 线程;否则,你可能需要自己进行线程处理。(有关线程和选项的更多信息,请参阅第十五章,为应用商店准备你的应用。)

使用硬件音量键控制你应用的音频音量

如果你希望音量控制可以控制你应用中的音量,请使用 setVolumeControlStream() 方法指定你的应用程序的音频流,如下所示:

setVolumeControlStream(AudioManager.STREAM_MUSIC);

参见下方的 AudioManager 链接以获取其他流选项。

参见

在你的应用中响应用户硬件媒体控制

让你的应用响应用户媒体控制(如耳机上的控制),例如播放、暂停、跳过等,这是一个用户会喜欢的贴心功能。Android 通过媒体库实现了这一点。与之前提到的 使用 SoundPool 播放声音效果 的配方一样,Lollipop 版本改变了这种操作方式。与 SoundPool 示例不同,这个配方能够利用另一种方法,即兼容性库。

本配方将向你展示如何设置 MediaSession 以响应用户硬件按钮,这将适用于 Lollipop 及更高版本,以及使用 MediaSessionCompat 库的早期 Lollipop 版本。(兼容性库将负责检查操作系统版本并自动使用正确的 API 调用。)

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 HardwareMediaControls。使用默认的 Phone & Tablet 选项,并在“添加活动到移动”对话框中选择 Empty Activity。

如何操作...

我们将只使用 Toast 消息来响应硬件事件,因此不需要对活动布局进行任何更改。第一步是将 V13 支持库添加到项目中。首先打开 build.gradle (Module: app) 并执行以下步骤:

  1. 在依赖关系部分添加以下库:
implementation 'com.android.support:support-v13:28.0.0-rc02'
  1. 接下来,打开 ActivityMain.java 并将以下 mMediaSessionCallback 添加到类声明中:
MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() {
    @Override
    public void onPlay() {
        super.onPlay();
        Toast.makeText(MainActivity.this, "onPlay()", Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onPause() {
        super.onPause();
        Toast.makeText(MainActivity.this, "onPause()", Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onSkipToNext() {
        super.onSkipToNext();
        Toast.makeText(MainActivity.this, "onSkipToNext()", Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onSkipToPrevious() {
        super.onSkipToPrevious();
        Toast.makeText(MainActivity.this, "onSkipToPrevious()", Toast.LENGTH_SHORT).show();
    }
};
  1. 将以下代码添加到现有的 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);
  1. 在具有媒体控制(如耳机)的设备或模拟器上运行应用程序以查看 Toast 消息。

它是如何工作的...

设置此功能有四个步骤:

  1. 创建一个 MediaSession.Callback 并将其附加到 MediaSession

  2. 设置 MediaSession 标志以指示我们想要媒体按钮

  3. SessionState 设置为活动状态

  4. 使用我们将要处理的操作设置 PlayBackState

步骤 4 和步骤 1 一起工作,因为回调函数只会获取在 PlayBackState 中设置的 events

由于我们在这个菜谱中实际上并没有控制任何播放,所以我们只是演示如何响应硬件事件。你将需要在 PlayBackState 中实现实际的功能,并在调用 setActions() 之后调用 setState()

这是一个很好的例子,说明了 API 的更改可以使事情变得更容易。由于新的 MediaSessionPlaybackState 被整合到 Compatibility 库中,我们可以在旧版本的操作系统上利用这些新 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 选项,并在“添加到移动设备”对话框中选择 Empty Activity。

如何操作...

我们将创建一个包含 ImageView 和按钮的布局。按钮将创建一个 Intent 以启动默认的相机应用。当相机应用完成后,我们的应用将收到回调。我们将检查结果并在有照片的情况下显示图片。首先打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  1. 打开 activity_main.xml 并用以下视图替换现有的 TextView:
<android.support.v7.widget.AppCompatImageView
 android:id="@+id/imageView"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:src="img/ic_launcher"
 app:layout_constraintTop_toTopOf="parent"
 app:layout_constraintLeft_toLeftOf="parent"
 app:layout_constraintRight_toRightOf="parent" />
<android.support.v7.widget.AppCompatButton
 android:id="@+id/button"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="Take Picture"
 android:onClick="takePicture"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintLeft_toLeftOf="parent"
 app:layout_constraintRight_toRightOf="parent"/>
  1. 打开 MainActivity.java 并将以下全局变量添加到 MainActivity 类中:
final int PHOTO_RESULT=1;
private Uri mLastPhotoURI=null;
  1. 添加以下方法以创建照片的 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));
}
  1. 添加以下方法以处理按钮点击:
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);
    }
}
  1. 添加一个新方法以覆盖 onActivityResult(),如下所示:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == PHOTO_RESULT && resultCode == RESULT_OK ) {
        AppCompatImageView imageView = findViewById(R.id.imageView);
        imageView.setImageBitmap(BitmapFactory.decodeFile(mLastPhotoURI.getPath()));
    }
}
  1. 将以下代码添加到现有的 onCreate() 方法末尾:
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) 
        != PackageManager.PERMISSION_GRANTED ) {
    ActivityCompat.requestPermissions(this, 
            new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},0);
}
  1. 你可以准备在设备或模拟器上运行应用程序了。

它是如何工作的...

与默认相机应用一起工作有两个部分。第一部分是设置启动应用的 Intent。我们使用 MediaStore.ACTION_IMAGE_CAPTURE 创建 Intent 以指示我们想要一个照片应用。我们通过检查 resolveActivity() 的结果来验证是否存在默认应用。只要它不为 null,我们就知道有一个应用可以处理该 Intent。(否则,我们的应用会崩溃。)我们创建一个文件名并将其添加到 Intent 中,使用 putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI)

当我们在 onActivityResult() 中收到回调时,我们首先确保它是 PHOTO_RESULTRESULT_OK(用户可能已取消),然后我们在 ImageView 中加载照片。

你可能想知道 onCreate() 中的 StrictMode 调用有什么用。基本上,这些代码行禁用了操作系统进行的额外安全检查。如果我们不禁用 StrictMode,当创建文件 URI 时应用会因 FileUriExposedException 异常而崩溃。对于生产应用,一个解决方案是创建一个 FileProvider,就像我们在第七章 数据存储使用有作用域目录访问外部存储 食谱中所做的那样。有关其他选项,请参阅 另请参阅 部分。

还有更多...

如果你不在乎图片存储在哪里,你可以不使用 MediaStore.EXTRA_OUTPUT 额外信息调用 Intent。如果你不指定输出文件,onActivityResult() 将在数据 Intent 中包含图像的缩略图。以下是如何显示缩略图的方法:

if (data != null) {
    imageView.setImageBitmap((Bitmap) data.getExtras().get(“data”));
}

这里是加载全分辨率图像的代码,使用在 data Intent 中返回的 URI:

if (data != null) {
    try {
        imageView.setImageBitmap(
            MediaStore.Images.Media. getBitmap(getContentResolver(),
            Uri.parse(data.toUri(Intent.URI_ALLOW_UNSAFE))));
    } catch (IOException e) {
        e.printStackTrace();
    }
}

调用默认视频应用程序

如果你想调用默认的视频捕获应用程序,过程是相同的。只需在步骤 5 中更改 Intent,如下所示:

Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);

你可以在 onActivityResult() 中获取视频的 URI,如下所示:

Uri videoUri = intent.getData();

参见

  • 在 第十章 的 将大图像缩小以避免内存不足异常 配方中,图形和动画

  • 在 第七章 的 使用作用域目录访问外部存储 配方中,数据存储

使用 Camera2 API 拍照

之前的配方演示了如何使用 Intent 调用默认的拍照应用程序。如果你只需要快速拍照,Intent 可能是理想的解决方案。如果不是,并且你需要对相机有更多控制,这个配方将展示如何直接使用 Camera2 API 使用相机。

由于现在 85% 的设备都在使用 Android 5.0 或更高版本,这个配方只关注 Camera2 API。 (Google 已经弃用了原始的 Camera API。)

准备中

在 Android Studio 中创建一个新的项目,并将其命名为 Camera2API。在目标 Android 设备对话框中,选择手机和平板选项,并选择 API 21:Android 5.0 (Lollipop) 或更高版本作为最小 SDK。在“添加到移动”对话框中选择“Empty Activity”。

如何做到这一点...

如您将看到的,这个配方有很多代码。首先打开 AndroidManifest.xml 并按照以下步骤操作:

  1. 添加以下两个权限:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  1. 现在,打开 activity_main.xml 并将现有的 TextView 替换为以下视图:
<TextureView
    android:id="@+id/textureView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toTopOf="@+id/button"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent" />
<android.support.v7.widget.AppCompatButton
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Take Picture"
    android:onClick="takePictureClick"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"/>
  1. 现在,打开 MainActivity.java 并将以下全局变量添加到 MainActivity 类中:
private CameraDevice mCameraDevice = null;
private CaptureRequest.Builder mCaptureRequestBuilder = null;
private CameraCaptureSession mCameraCaptureSession  = null;
private TextureView mTextureView = null;
private Size mPreviewSize = null;
  1. 将以下 Comparator 类添加到 MainActivity 类中:
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());
    }
}
  1. 添加以下 CameraCaptureSession.StateCallback
private CameraCaptureSession.StateCallback mPreviewStateCallback = new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(CameraCaptureSession session) {
        startPreview(session);
    }
    @Override
    public void onConfigureFailed(CameraCaptureSession session) {}
};
  1. 添加以下 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();
            }
        };
  1. 添加 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) {}
};
  1. 添加以下 CaptureCallback 类以接收捕获完成事件:
final CameraCaptureSession.CaptureCallback mCaptureCallback = 
        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);
    }
};
  1. 将以下代码添加到现有的 onCreate() 回调中:
mTextureView = findViewById(R.id.textureView);
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);

if(ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) 
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1);
}
  1. 将以下方法添加以重写 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);
    }
}
  1. 添加 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();
    }
}
  1. 添加 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();
    }
}
  1. 添加 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);
}
  1. 添加以下方法以保存图像文件:
private void saveImage(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(getPictureFile());
        output.write(bytes);
        output.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (image != null) {
            image.close();
        }
    }
}
  1. 添加以下方法以处理按钮点击:
public void takePictureClick(View view) {
    if (null == mCameraDevice) {
        return;
    }
    takePicture();
}
  1. 添加最终代码以实际设置相机并拍照:
private void takePicture() {
    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<>(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) {
                saveImage(reader);
            }
        };
        HandlerThread thread = new HandlerThread("CameraPicture");
        thread.start();
        final Handler backgroundHandler = new 
        Handler(thread.getLooper());
        reader.setOnImageAvailableListener(readerListener, 
        backgroundHandler);
        mCameraDevice.createCaptureSession(outputSurfaces,
                new CameraCaptureSession.StateCallback() {
                    @Override
                    public void onConfigured(CameraCaptureSession 
                    session) {
                        try {
                            session.capture(captureBuilder.build(),
                                    mCaptureCallback, 
                       backgroundHandler);
                        } catch (CameraAccessException e) {
                            e.printStackTrace();
                        }
                    }
                    @Override
                    public void onConfigureFailed(CameraCaptureSession 
                   session) { }
                }, backgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
  1. 在带有相机的设备或模拟器上运行应用程序。

它是如何工作的...

如您所见,这个配方有很多步骤,但从高层次来看,它相当简单:

  • 设置相机预览

  • 捕获图像

现在,我们将逐一详细说明。

设置相机预览

下面是代码如何设置预览的概述:

  1. 首先,我们使用 setSurfaceTextureListener() 方法在 onCreate() 中设置 TextureView.SurfaceTextureListener

  2. 当我们收到 onSurfaceTextureAvailable() 回调时,我们打开相机

  3. 我们将我们的CameraDevice.StateCallback类传递给openCamera()方法,它最终会调用onOpened()回调

  4. onOpened()通过调用getSurfaceTexture()获取预览的表面,并通过调用createCaptureSession()将其传递给CameraDevice

  5. 最后,当CameraCaptureSession.StateCallbackonConfigured()被调用时,我们使用setRepeatingRequest()方法开始预览

捕获图片

尽管takePicture()方法可能看起来是过程性的,捕获图片也涉及到几个类并依赖于回调。以下是代码工作原理的分解:

  1. 当点击拍照按钮时,这个过程开始。

  2. 然后代码查询相机以找到最大的可用图像大小

  3. 然后创建一个 ImageReader。

  4. 接下来,代码设置OnImageAvailableListener,并在onImageAvailable()回调中保存图片。

  5. 然后它创建CaptureRequest.Builder并包含ImageReader表面。

  6. 接下来,它创建CameraCaptureSession.CaptureCallback,这定义了

    onCaptureCompleted()回调。当捕获完成后,它重新开始预览。

  7. 最后,调用createCaptureSession()方法,创建一个CameraCaptureSession.StateCallback。这是调用之前创建的capture()方法的地方,传递CameraCaptureSession.CaptureCallback

还有更多...

我们刚刚创建了基础代码来演示一个可工作的相机应用程序。有许多改进的地方。首先,你应该处理设备的方向,无论是预览还是保存图片时。(见以下链接。)此外,由于 Android 6.0(API 23)拥有超过 60%的市场份额,你的应用程序应该已经使用新的权限模型。而不是像我们在openCamera()方法中那样只是检查异常,最好是检查所需的权限。

参见

第十三章:电信、网络和互联网

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

  • 如何拨打电话

  • 监控电话呼叫事件

  • 如何发送短信(文本)消息

  • 接收短信消息

  • 在您的应用中显示网页

  • 检查在线状态和连接类型

  • 电话号码屏蔽 API

简介

我们将首先通过查看“如何拨打电话”来探讨电信功能。在探索了如何拨打电话之后,我们将查看如何通过监控电话呼叫事件来监控电话呼叫。然后,在“如何发送短信消息”部分,我们将介绍短信消息的接收。

然后,我们将探索WebView以向您的应用添加浏览器功能。在基本层面上,WebView是一个基本的 HTML 查看器。我们将展示如何扩展WebViewClient类并通过WebSettings修改设置来创建完整的浏览器功能,包括 JavaScript 和缩放功能。

本章的最后一个食谱将探讨一个新 API(在 Android 7.0 Nougat 中添加),该 API 可以在操作系统级别屏蔽电话号码。

如何拨打电话

如前文所述,我们可以通过使用 Intent 简单地调用默认应用。对于电话呼叫,有两个 Intent:

  • ACTION_DIAL: 使用默认的电话应用拨打电话(无需权限)

  • CALL_PHONE: 跳过 UI 直接拨打电话(需要权限)

下面是设置并调用 Intent 以使用默认电话应用的代码:

Intent intent = new Intent(Intent.ACTION_DIAL); 
intent.setData(Uri.parse("tel:" + number)); 
startActivity(intent); 

由于您的应用不执行拨号,并且用户必须按下拨号按钮,因此您的应用不需要任何拨号权限。接下来的食谱将向您展示如何直接拨打电话,绕过拨号器应用。

准备工作

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

如何做...

首先,我们需要添加适当的权限来拨打电话。然后,我们需要添加一个按钮来调用我们的拨号方法。首先打开 Android Manifest,并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.CALL_PHONE"/>
  1. 打开activity_main.xml并将现有的TextView替换为以下按钮:
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Dial"
    android:onClick="dialPhone"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 添加此方法,它将检查您的应用是否已被授予CALL_PHONE权限:
private boolean checkPermission(String permission) {
    int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
    return (permissionCheck == PackageManager.PERMISSION_GRANTED);
}
  1. 添加拨打电话的代码:
public void dialPhone(View view){
    if (checkPermission(Manifest.permission.CALL_PHONE)) {
        Intent intent = new Intent(Intent.ACTION_CALL);
        intent.setData(Uri.parse("tel:0123456789"));
        startActivity(intent);
    } else {
        ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.CALL_PHONE},1);
    }
}
  1. 在您的设备上运行之前,请务必将 0123456789 替换为有效的号码。

它是如何工作的...

如介绍中所述,使用CALL_PHONE Intent 需要适当的权限。我们在第一步中将所需的权限添加到清单中,并在第四步实际调用 Intent 之前,在第三步中使用该方法来验证权限。从 Android 6.0 Marshmallow(API 23)开始,权限不再在安装期间授予。因此,我们在尝试拨打电话之前检查应用程序是否有权限。

参见

  • 参考第十五章中的The Android 6.0 Runtime Permission Model配方,为 Play 商店准备您的应用程序,以获取有关新运行时权限的更多信息。

监控电话呼叫事件

在之前的配方中,我们演示了如何进行电话呼叫,无论是通过 Intent 调用默认应用程序,还是直接拨打电话而不显示 UI。

如果您想在电话结束时收到通知怎么办?这会变得稍微复杂一些,因为您需要监控 Telephony 事件并跟踪电话状态。在这个配方中,我们将演示如何创建一个PhoneStateListener来读取电话状态事件。

准备工作

在 Android Studio 中创建一个新的项目,命名为PhoneStateListener。使用默认的“电话和平板”选项,并在“添加到移动”对话框中选择“Empty Activity”。

虽然不是必需的,但您可以使用之前的配方来发起电话。否则,使用默认拨号器并/或监视来电事件。

如何操作...

我们只需要在布局中添加一个TextView来显示事件信息。打开activity_main.xml文件并按照以下步骤操作:

  1. 添加或修改TextView如下:
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 将以下权限添加到 AndroidManifest.xml 中:
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
  1. 打开MainActivity.java并将以下PhoneStateListener类添加到MainActivity类中:
PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
    @Override
    public void onCallStateChanged(int state, String number) {
        String phoneState = number;
        switch (state) {
            case TelephonyManager.CALL_STATE_IDLE:
                phoneState += "CALL_STATE_IDLE\n";
                break;
            case TelephonyManager.CALL_STATE_RINGING:
                phoneState += "CALL_STATE_RINGING\n";
                break;
            case TelephonyManager.CALL_STATE_OFFHOOK:
                phoneState += "CALL_STATE_OFFHOOK\n";
                break;
        }
        TextView textView = findViewById(R.id.textView);
        textView.append(phoneState);
    }
};
  1. 修改onCreate()以设置监听器:
final TelephonyManager telephonyManager = 
        (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
  1. 在设备上运行应用程序并发起和/或接收电话。返回此应用程序后,您将看到事件列表。

它是如何工作的...

为了演示使用监听器,我们在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 选项,并在“添加一个活动到移动”对话框中选择 Empty Activity。

如何做到这一点...

首先,我们需要添加发送短信所需的必要权限。然后,我们将创建一个包含电话号码和消息字段以及发送按钮的布局。当点击发送按钮时,我们将创建并发送短信。以下是步骤:

  1. 打开 AndroidManifest 并添加以下权限:
<uses-permission android:name="android.permission.SEND_SMS"/>
  1. 打开activity_main.xml并用以下 XML 替换现有的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <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"/>
</RelativeLayout>
  1. 打开MainActivity.java并添加以下全局变量:
final int SEND_SMS_PERMISSION_REQUEST_CODE=1; 
Button mButtonSend; 
  1. 将以下代码添加到现有的onCreate()回调中:
mButtonSend = findViewById(R.id.buttonSend);
mButtonSend.setEnabled(false);

if (checkPermission(Manifest.permission.SEND_SMS)) {
    mButtonSend.setEnabled(true);
} else {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SEND_SMS}, 
            SEND_SMS_PERMISSION_REQUEST_CODE);
}
  1. 添加以下方法来检查权限:
private boolean checkPermission(String permission) {
    int permissionCheck = ContextCompat.checkSelfPermission(this,permission);
    return (permissionCheck == PackageManager.PERMISSION_GRANTED);
}
  1. 重写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;
        }
    }
}
  1. 最后,添加实际发送短信的方法:
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();
    }
}
  1. 你现在可以运行应用程序在设备或模拟器上。(在发送到另一个模拟器时使用模拟器设备号码。第一个模拟器是 5554;第二个是 5556,每个额外的模拟器递增 2。)

它是如何工作的...

发送短信的代码只有两行,如下所示:

SmsManager smsManager = SmsManager.getDefault(); 
smsManager.sendTextMessage(phoneNumber, null, msg, null, null); 

sendTextMessage()方法执行实际的发送。这个菜谱的大多数代码是用来检查和获取所需的权限。

还有更多...

虽然发送短信消息很简单,但我们还有一些其他选项。

多部分消息

尽管这取决于运营商,但通常每个短信允许的最大字符数是 160 个。你可以修改前面的代码来检查消息是否超过 160 个字符,如果是的话,你可以调用 SMSManager 的 divideMessage()方法。该方法返回ArrayList,你可以将其发送到sendMultipartTextMessage()。以下是一个示例:

ArrayList<String> messages=smsManager.divideMessage(msg);
smsManager.sendMultipartTextMessage(phoneNumber, null, messages, null, null);

注意,使用sendMultipartTextMessage()发送的消息在模拟器上可能无法正确工作,因此请确保在真实设备上进行测试。

投递状态通知

如果你希望被通知消息的状态,有两个可选字段你可以使用。以下是 SMSManager 文档中定义的sendTextMessage()方法:

sendTextMessage(String destinationAddress, String scAddress, String text, 
        PendingIntent sentIntent, PendingIntent deliveryIntent)

你可以包含一个待处理的 Intent 来通知发送状态和/或投递状态。在你收到待处理的 Intent 时,它将包含一个结果代码,要么是 Activity 的RESULT_OK,如果发送成功,或者是一个在 SMSManager 文档中定义的错误代码(见以下链接):

  • RESULT_ERROR_GENERIC_FAILURE:通用失败原因

  • RESULT_ERROR_NO_SERVICE:由于服务当前不可用而失败

  • RESULT_ERROR_NULL_PDU:由于没有提供 PDU 而失败

  • RESULT_ERROR_RADIO_OFF:由于无线电被明确关闭而失败

参见

接收短信消息

这个食谱将演示如何设置一个广播接收器来通知你新的短信消息。值得注意的是,你的应用不需要运行就可以接收短信意图。Android 将启动你的服务来处理短信。

准备工作

在 Android Studio 中创建一个新的项目,命名为 ReceiveSMS。使用默认的 Phone & Tablet 选项,并在“添加一个活动到移动”对话框中选择 Empty Activity。

如何操作...

在这个演示中,我们不会使用布局,因为所有的工作都将放在广播接收器中。我们将使用 Toast 显示传入的短信消息。打开 AndroidManifest 并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.RECEIVE_SMS" />
  1. 将以下广播接收器声明添加到应用程序元素中:
<receiver android:name=".SMSBroadcastReceiver">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
    </intent-filter>
</receiver>
  1. 打开 MainActivity.java 并添加以下方法:
private boolean checkPermission(String permission) {
    int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
    return (permissionCheck == PackageManager.PERMISSION_GRANTED);
}
  1. 修改现有的 onCreate() 回调以检查权限:
if (!checkPermission(Manifest.permission.RECEIVE_SMS)) {
    ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECEIVE_SMS}, 0);
}
  1. 使用以下代码在项目中添加一个新的 Java 类,命名为 SMSBroadcastReceiver
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();
                }
            }
        }
    }
}
  1. 你现在可以在设备或模拟器上运行应用程序了。

它是如何工作的...

就像在之前的发送短信食谱中一样,我们首先需要检查应用是否有权限。(在 Android 6.0 之前的设备上,清单声明将自动提供权限,但对于 Marshmallow 及以后的版本,我们需要像这里一样提示用户。)

如你所见,广播接收器接收新短信消息的通知。我们通过在 AndroidManifest 中使用以下代码告诉系统我们想要接收新的短信接收广播:

<receiver android:name=".SMSBroadcastReceiver">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
    </intent-filter>
</receiver>

通知通过标准的 onReceive() 回调传入,因此我们使用以下代码检查操作:

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" /> 

下面是使用 SMS 内容提供程序获取游标的示例:

Cursor cursor = getContentResolver().query(
        Uri.parse("content://sms/"), null, null, null, null);
while (cursor.moveToNext()) {
    textView.append("From :" + cursor.getString(1) + " : " + cursor.getString(11)+"\n");
}

在撰写本文时,SMS 内容提供程序有超过 30 列。以下是前 12 列,它们是最有用的(记住,列数从零开始):

  1. _id

  2. thread_id

  3. address

  4. person

  5. date

  6. protocol

  7. read

  8. status

  9. type

  10. reply_path_present

  11. subject

  12. body

相关内容

在你的应用程序中显示网页

当你想显示一个网页时,你有两个选择:调用默认浏览器或在你的应用程序中显示内容。如果你只想调用默认浏览器,使用以下 Intent:

Uri uri = Uri.parse("https://www.packtpub.com/"); 
Intent intent = new Intent(Intent.ACTION_VIEW, uri); 
startActivity(intent); 

如果你需要在你的应用程序中显示内容,你可以使用 WebView。这个示例将展示如何在你的应用程序中显示一个网页,如截图所示:

准备工作

在 Android Studio 中创建一个新的项目,命名为 WebView。使用默认的 Phone & Tablet 选项,并在“添加一个活动到移动”对话框中选择 Empty Activity。

如何操作...

我们将通过代码创建 WebView,因此不会修改布局。我们首先打开 Android Manifest 并按照以下步骤进行:

  1. 添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/>
  1. 修改现有的 onCreate() 方法以包含以下代码:
WebView webview = new WebView(this);
setContentView(webview);
webview.loadUrl("https://www.packtpub.com/");
  1. 你现在可以运行应用程序在设备或模拟器上。

它是如何工作的...

我们创建一个 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 
        }
    }
}

然后,使用以下代码设置客户端:

webview.setWebViewClient(new mWebViewClient());

如何启用 JavaScript

我们可以通过 WebSetting 来自定义许多其他的 WebView 选项。如果你想要启用 JavaScript,从 WebView 中获取 webSettings 并调用 setJavaScriptEnabled(),如下所示:

WebSettings webSettings = webview.getSettings(); 
webSettings.setJavaScriptEnabled(true); 

启用内置缩放

另一个 webSettings 选项是 setBuiltInZoomControls()。从前面的代码继续,只需添加以下内容:

webSettings.setBuiltInZoomControls(true); 

在下一节中查看 webSettings 链接,以获取大量其他选项。

相关内容

检查在线状态和连接类型

这是一个简单的配方,但也是一个非常常见的配方,可能被包含在你构建的每一个互联网应用中:检查在线状态。在检查在线状态时,我们还可以检查连接类型:WIFIMOBILE

准备工作

在 Android Studio 中创建一个新的项目,命名为 isOnline。使用默认的 Phone & Tablet 选项,并在“添加活动到移动”对话框中选择 Empty Activity。

如何做...

首先,我们需要添加必要的权限来访问网络。然后,我们将创建一个简单的布局,包含 ButtonTextView。要开始,打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  1. 打开 activity_main.xml 文件,并用以下内容替换现有的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <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="getStatus"/>
</RelativeLayout>
  1. 将此方法添加到检查连接状态:
private boolean isOnline() {
    ConnectivityManager connectivityManager = (ConnectivityManager)
                    getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
    return (networkInfo != null && networkInfo.isConnected());
}
  1. 添加以下方法来处理按钮点击:
public void getStatus(View view) {
    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");
    }
}
  1. 你现在可以运行应用在设备或模拟器上。

它是如何工作的...

我们创建了 isOnline() 方法,使其易于重用此代码。

要检查状态,我们通过调用 getType() 来获取 ConnectivityManager 的实例,读取 NetworkInfo 状态。如果它报告我们已连接,我们将通过调用 getType() 获取活动网络的名称,它返回以下常量之一:

  • TYPE_MOBILE

  • TYPE_WIFI

  • TYPE_WIMAX

  • TYPE_ETHERNET

  • TYPE_BLUETOOTH

还可以查看后面的 ConnectivityManager 链接以获取其他常量。为了显示目的,我们调用 getTypeName()。我们也可以调用 getType() 来获取一个数字常量。

还有更多...

我们还可以设置应用在网络状态变化时被通知。

监控网络状态变化

如果你的应用需要响应网络状态的变化,请查看 ConnectivityManager 中的 CONNECTIVITY_ACTION。设置过滤器以通知连接状态变化事件有两种方式:

  • 通过 Android Manifest

  • 通过代码

下面是一个如何在 Android Manifest 中通过接收器意图过滤器包含动作的示例:

<receiver android:name=".MyBroadcastReceiver"> 
    <intent-filter> 
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> 
    </intent-filter> 
</receiver> 

使用 Android Manifest 时要小心,因为它会在网络状态改变时通知你的应用,即使你的应用没有被使用。这可能导致不必要的电池消耗。

针对 Android 7.0 及以后的版本的应用将不再在 Manifest 中接收 CONNECTIVITY_CHANGE。(这是为了防止不必要的电池消耗)。相反,通过代码注册意图过滤器,如下所示。

更好的解决方案(并且对于 Android 7.0 及以后的版本是必需的)是通过代码注册你的意图过滤器。以下是一个示例:

registerReceiver(mReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

查看文件下载中的食谱示例,了解如何记录CONNECTIVITY_CHANGE事件。

参见

电话号码阻止 API

Android Nougat(API 24)引入的新功能是在操作系统级别处理阻止电话号码的能力。这为用户在多个设备上提供了一致的用户体验:

  • 被阻止的号码会阻止所有来电和短信

  • 被阻止的号码可以使用备份和还原功能进行备份

  • 设备上的所有应用程序共享相同的被阻止号码列表

在本食谱中,我们将查看如何添加一个号码到阻止列表、移除号码以及检查号码是否已被阻止的代码。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为BlockedCallList。在Target Android Devices对话框中,选择Phone & Tablet选项,并将最小 SDK 选择为 API 24:Android 7.0 Nougat(或更高)。在Add an Activity to Mobile对话框中选择Empty Activity

如何操作...

我们将首先创建一个带有EditText输入电话号码和三个按钮(BlockUnblockisBlocked)的用户界面。首先,打开activity_main.xml并按照以下步骤操作:

  1. 用以下 XML 代码替换现有的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <EditText
        android:id="@+id/editTextNumber"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="phone"
        android:ems="10"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="36dp" />
    <Button
        android:id="@+id/buttonblock"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Block"
        android:layout_above="@+id/buttonUnblock"
        android:layout_centerHorizontal="true"
        android:onClick="onClickBlock"/>
    <Button
        android:id="@+id/buttonUnblock"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Block"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:onClick="onClickUnblock"/>
    <Button
        android:id="@+id/buttonIsBlocked"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="isBlocked"
        android:layout_below="@+id/buttonUnblock"
        android:layout_centerHorizontal="true"
        android:onClick="onClickIsBlocked"/>
</RelativeLayout>
  1. 打开MainActivity.java并在类声明中添加以下代码:
private EditText mEditTextNumber;
  1. 将以下代码行添加到onCreate()方法的末尾:
mEditTextNumber=findViewById(R.id.editTextNumber);
  1. 添加处理按钮点击的三个方法:
public void onClickBlock(View view) {
    String number = mEditTextNumber.getText().toString();
    if (number!=null && number.length()>0) {
        blockNumber(number);
    }
}
public void onClickUnblock(View view) {
    String number = mEditTextNumber.getText().toString();
    if (number!=null && number.length()>0) {
        unblockNumber(number);
    }
}
public void onClickIsBlocked(View view) {
    String number = mEditTextNumber.getText().toString();
    if (number!=null && number.length()>0) {
        isBlocked(number);
    }
}
  1. 添加以下函数以阻止号码:
private void blockNumber(String number) {
    if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
        ContentValues values = new ContentValues();
        values.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number);
        getContentResolver().insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, values);
    }
}
  1. 添加以下函数以取消阻止号码:
private void unblockNumber(String number) {
    if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
        ContentValues values = new ContentValues();
        values.put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number);
        Uri uri = getContentResolver()
                .insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, values);
        getContentResolver().delete(uri, null, null);
    }
}
  1. 添加以下函数以检查号码是否被阻止:
public void isBlocked(String number) {
    if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {
        boolean blocked = BlockedNumberContract.isBlocked(this,number);
        Toast.makeText(MainActivity.this, number + "blocked: " + blocked, 
                Toast.LENGTH_SHORT).show();
    } else {
        Toast.makeText(MainActivity.this, "User cannot perform this operation", 
                Toast.LENGTH_SHORT).show();
    }
}
  1. 你已经准备好在至少运行 Android 7.0 的设备或模拟器上运行应用程序了。

它是如何工作的...

在我们调用BlockedNumberContract API 之前,我们通过调用canCurrentUserBlockNumbers()来检查我们是否有权限,就像以下代码所示:

if (BlockedNumberContract.canCurrentUserBlockNumbers(this)) {

如果为真,我们进行实际的 API 调用。

重要:只有以下应用程序可以读取和写入BlockedNumber提供者:默认短信应用程序、默认电话应用程序和运营商应用程序。用户可以选择他们的默认短信和电话应用程序。

BlockedNumber列表中添加和删除号码使用标准的 Service Provider 格式。

更新方法不受支持;请使用AddDelete方法代替。

要检查一个号码是否已在阻止列表中,调用isBlocked()方法,传入当前上下文和要检查的号码,就像我们在以下代码中所做的那样:

boolean blocked = BlockedNumberContract.isBlocked(this,number);

更多内容...

要获取所有当前被阻止号码的列表,使用以下代码创建一个带有列表的游标:

Cursor cursor = getContentResolver().query(
        BlockedNumberContract.BlockedNumbers.CONTENT_URI,
        new String[]{BlockedNumberContract.BlockedNumbers.COLUMN_ID,
                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER,
                BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER},
        null, null, null);

参见

如需更多信息,请参阅BlockedNumberContract参考文档:developer.android.com/reference/android/provider/BlockedNumberContract

第十四章:位置和地理围栏的使用

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

  • 如何获取设备位置

  • 解决GoogleApiClient OnConnectionFailedListener报告的问题

  • 创建和监控地理围栏

简介

位置感知为应用程序提供了许多好处,实际上如此之多,以至于甚至桌面应用程序现在也试图获取用户的位置。位置的使用范围从路线导航,“查找最近的”应用程序,基于位置的通知,现在甚至有基于位置的游戏,让你用设备探索。

Google API 提供了许多丰富的功能,用于创建具有位置感知的应用程序和地图功能。我们的第一个菜谱将探讨如何获取设备上的最后已知位置,并在位置变化时接收更新。如果你正在请求接近位置的位置更新,请查看创建和监控地理围栏菜谱中使用的 Geofence 选项。

本章中的所有菜谱都使用 Google 库。如果你还没有下载 SDK 包,请按照 Google 的说明操作。

developer.android.com/sdk/installing/adding-packages.html添加 SDK 包。

现在你已经获得了位置,很可能你还会想将其映射出来。这是 Google 在 Android 上使用 Google Maps API 使这一过程变得非常简单的一个领域。当与 Google Maps 一起工作时,在 Android Studio 中创建新项目时,请查看 Google Maps Activity 选项。不要选择我们通常用于这些菜谱的 Empty Activity,而是选择 Google Maps Activity,如本截图所示:

图片

如何获取设备位置

这个第一个菜谱将向你展示如何获取最后已知位置。如果你以前使用过 Google Location API,那么你可能注意到事情已经发生了变化。这个菜谱展示了获取最后位置和位置变化时更新的最新 API。

准备工作

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

如何操作...

首先,我们将向 AndroidManifest 添加必要的权限,然后我们将修改TextView元素以包含一个 ID。最后,我们将添加一个方法来接收最后已知位置回调。打开 AndroidManifest 并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  1. 在 Gradle 脚本部分,打开 build.gradle (Module: app)文件,如本截图所示:

图片

  1. dependencies部分添加以下语句:
implementation 'com.google.android.gms:play-services:12.0.1'
  1. 打开activity_main.xml,并使用以下 XML 更新现有的TextView
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 将以下代码添加到现有的onCreate()方法中:
if (ActivityCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION)
        == PackageManager.PERMISSION_GRANTED) {
    getLocation();
} else {
    ActivityCompat.requestPermissions(this, new String[] {ACCESS_COARSE_LOCATION},1);
}
  1. 创建getLocation()方法如下:
private void getLocation() throws SecurityException {
    LocationServices.getFusedLocationProviderClient(this).getLastLocation()
            .addOnSuccessListener(this, new OnSuccessListener<Location>() {
                @Override
                public void onSuccess(Location location) {
                    final TextView textView = findViewById(R.id.textView);
                    if (location != null) {
                        textView.setText(DateFormat.getTimeInstance()
                                .format(location.getTime()) + "\n"
                                + "Latitude=" + location.getLatitude() + "\n"
                                + "Longitude=" + location.getLongitude());
                    } else {
                        Toast.makeText(MainActivity.this, "Location null", Toast.LENGTH_LONG)
                                .show();
                    }
                }
            });
}
  1. 您已准备好在设备或模拟器上运行应用程序。

它是如何工作的...

此代码示例使用了 Google Play 服务getLastLocation()方法的最新版本(12.0.1,截至本文撰写时)。如果您以前使用过它,您可能会注意到 API 工作方式的一些重大变化。实际上,现在它变得更简单了,因为我们只需要调用getFusedLocationProviderClient()并传递我们的监听器。确保我们在回调中检查位置,以确保它不是 null。(有几种情况会导致 null 位置,例如设备尚未定位,用户禁用了位置功能,或者进行了出厂重置。)

我们收到的位置对象精度基于我们的权限设置。我们使用了ACCESS_COARSE_LOCATION,但如果我们想要更高的精度,我们可以请求ACCESS_FINE_LOCATION,以下是需要此权限:

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

确保在checkSelfPermission()调用中检查适当的权限。

最后,为了使代码专注于位置功能,我们只进行简单的权限检查。在生产应用中,您应该像在第十五章“为 Play 商店准备您的应用”中的“Android 6.0 运行时权限模型”配方中所示那样检查和请求权限。

还有更多...

测试位置可能是一个挑战,因为在测试和调试时很难实际移动设备。幸运的是,我们有能力使用模拟器模拟 GPS 数据。(在物理设备上创建模拟位置也是可能的,但这并不容易。)

模拟位置

使用模拟器模拟位置有几种方法:

  • 通过模拟器设置位置设置

  • 通过 ADB shell 的Geo命令

要在模拟器中设置模拟位置,请按照以下步骤操作:

  1. 点击更多选项按钮(位于模拟器控制选项底部的...)

  2. 在设备窗口中选择位置标签页

  3. 在经度和纬度框中输入 GPS 坐标

下面是一个显示位置标签页的截图:

注意,模拟位置是通过发送 GPS 数据来工作的。因此,为了让您的应用接收到模拟位置,它需要接收 GPS 数据。测试lastLocation()可能不会发送模拟 GPS 数据,因为它并不完全依赖于 GPS 来确定设备位置。尝试使用“如何获取设备位置”配方中的模拟位置,我们可以请求优先级。(我们无法强制系统使用任何特定的位置传感器,我们只能提出请求。系统将选择最佳解决方案来提供结果。)

参见

解决 GoogleApiClient OnConnectionFailedListener 报告的问题

由于 Google API 的不断变化,用户可能会尝试使用您的应用程序,但由于他们的文件过时而无法使用。我们可以使用 GoogleApiAvailability 库来显示对话框,以帮助用户解决问题。

我们将继续使用之前的配方,并向 onConnectionFailed() 回调中添加代码。我们将使用错误结果向用户显示额外信息以解决问题。

准备工作

在 Android Studio 中创建一个新的项目,并将其命名为 HandleGoogleAPIError。使用默认的 Phone & Tablet 选项,并在提示活动类型时选择 Empty Activity。一旦创建项目,将 Google Play 库引用添加到项目依赖项中。(参见之前的配方步骤。)

如何操作...

此配方的第一步是将 Google Play Services 库添加到项目中。从那里,我们将创建处理 Google 客户端回调的类,并使用吐司来提供反馈。首先,打开 build.gradle (Module: app) 文件,并按照以下步骤操作(如果您不确定要打开哪个文件,请参见之前的配方步骤截图):

  1. 将以下语句添加到 dependencies 部分:
implementation 'com.google.android.gms:play-services:12.0.1'
  1. 打开 ActivityMain.java 并将以下行添加到全局类变量中:
private final int REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR=1;
boolean mResolvingError;
GoogleApiClient mGoogleApiClient;
  1. 添加以下两个类来处理回调:
GoogleApiClient.ConnectionCallbacks mConnectionCallbacks =
        new GoogleApiClient.ConnectionCallbacks() {
    @Override
    public void onConnected(Bundle bundle) {
        Toast.makeText(MainActivity.this, "onConnected()", Toast.LENGTH_LONG).show();
    }
    @Override
    public void onConnectionSuspended(int i) {}
};

GoogleApiClient.OnConnectionFailedListener mOnConnectionFailedListener = 
        new GoogleApiClient.OnConnectionFailedListener() {
    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Toast.makeText(MainActivity.this, connectionResult.toString(), Toast.LENGTH_LONG).show();
        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());
        }
    }
};
  1. 将以下方法添加到 MainActivity 类中,以显示 Google API 错误对话框:
private void showGoogleAPIErrorDialog(int errorCode) {
    GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance();
    Dialog errorDialog = googleApiAvailability.getErrorDialog(
            this, errorCode, REQUEST_RESOLVE_GOOGLE_CLIENT_ERROR);
    errorDialog.show();
}
  1. 添加以下代码以覆盖 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();
        }
    }
}
  1. 将以下方法添加以设置 GoogleApiClient
protected void setupGoogleApiClient() {
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addConnectionCallbacks(mConnectionCallbacks)
            .addOnConnectionFailedListener(mOnConnectionFailedListener)
            .addApi(LocationServices.API)
            .build();
    mGoogleApiClient.connect();
}
  1. 最后,将以下行代码添加到现有的 onCreate() 方法末尾:
setupGoogleApiClient();
  1. 你已经准备好在设备或模拟器上运行应用程序。

工作原理...

这里的大部分代码是 GoogleApiClient 的标准设置,主要新增了设置 OnConnectionFailedListener 回调。这是应用程序从简单地失败到实际上帮助最终用户使其工作的地方。幸运的是,Google 通过检查导致其失败的条件以及向用户展示 UI 来为我们做了大部分工作。我们只需确保检查 Google 向我们报告的状态。

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

参见

创建和监控 Geofence

如果您的应用程序需要知道用户何时进入或离开某个特定位置,有一种替代方案是持续检查用户的位置:Geofencing。Geofence 是一个位置(纬度和经度)以及一个半径。您可以创建一个 Geofence,并让系统在用户进入您指定的位置附近时通知您。(Android 目前允许每个用户最多创建 100 个 Geofence。)

Geofence 属性包括:

  • 位置:经度和纬度

  • 半径:圆的大小(以米为单位)

  • 派待延迟:在发送通知之前用户可以在半径内停留多长时间

  • 过期时间:Geofence 自动过期前的时间

  • 过渡 类型

    • GEOFENCE_TRANSITION_ENTER

    • GEOFENCE_TRANSITION_EXIT

    • INITIAL_TRIGGER_DWELL

本教程将向您展示如何创建 Geofence 对象,并使用它来创建 GeofencingRequest 的实例。

准备工作

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

如何操作...

我们不需要为这个教程创建布局,因为我们将会使用 Toasts 和通知与用户进行交互。我们还需要创建一个额外的 Java 类用于 IntentService,该类处理 Geofence 警报。打开 Android 清单文件,并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
  1. 打开 build.gradle (Module: app) 文件,并在 dependencies 部分添加以下语句:
implementation 'com.google.android.gms:play-services:12.0.1'
  1. 创建一个名为 GeofenceIntentService 的新 Java 类,并扩展 IntentService 类。声明如下:
public class GeofenceIntentService extends IntentService { 
  1. 添加以下构造函数:
public GeofenceIntentService() { 
    super("GeofenceIntentService"); 
} 
  1. 添加 onHandleIntent() 以接收 Geofence 警报:
@Override
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) {
        Toast.makeText(getApplicationContext(), "GEOFENCE_TRANSITION_DWELL",
                Toast.LENGTH_SHORT).show();
    }
}
  1. 打开 Android 清单文件,并在 <application> 元素内添加以下内容,与 <activity> 元素处于同一级别:
<service android:name=".GeofenceIntentService"/> 
  1. 打开 MainActivity.java 并添加以下全局变量:
private final int MINIMUM_RECOMENDED_RADIUS=100;
  1. 使用以下方法创建一个 PendingIntent
private PendingIntent createGeofencePendingIntent() {
    Intent intent = new Intent(this, GeofenceIntentService.class);
    return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
  1. 使用以下方法创建 Geofence 项目:
private List createGeofenceList() {
    List<Geofence> geofenceList = new ArrayList<>();
    geofenceList.add(new Geofence.Builder()
            .setRequestId("GeofenceLocation")
            .setCircularRegion(
                    47.6062,  //Latitude
                    122.3321, //Longitude
                    MINIMUM_RECOMENDED_RADIUS)
            .setLoiteringDelay(30000)
            .setExpirationDuration(Geofence.NEVER_EXPIRE)
            .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL)
            .build());
    return geofenceList;
}
  1. 使用以下方法创建 Geofence 请求:
private GeofencingRequest createGeofencingRequest() {
    GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
    builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_DWELL);
    builder.addGeofences(createGeofenceList());
    return builder.build();
}
  1. 将以下代码添加到现有的 onCreate() 回调中:
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
    GeofencingClient geofencingClient = LocationServices.getGeofencingClient(this);
    geofencingClient.addGeofences(createGeofencingRequest(), createGeofencePendingIntent())
            .addOnSuccessListener(this, new OnSuccessListener<Void>() {
                @Override
                public void onSuccess(Void aVoid) {
                    Toast.makeText(MainActivity.this, "onSuccess()", Toast.LENGTH_SHORT).show();
                }
            })
            .addOnFailureListener(this, new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    Toast.makeText(MainActivity.this,
                            "onFailure(): " + e.getMessage(), Toast.LENGTH_SHORT).show();
                }
            });
} else {
    ActivityCompat.requestPermissions(this, 
            new String[] {android.Manifest.permission.ACCESS_FINE_LOCATION},1);
}
  1. 您现在可以开始在设备或模拟器上运行应用程序了。

它是如何工作的...

首先,我们添加ACCESS_FINE_LOCATION权限,因为这对于地理围栏是必需的。

在我们可以调用GeofencingApi.addGeofences()方法之前,我们必须准备两个对象:

  • 地理围栏请求

  • 地理围栏挂起 Intent

要创建地理围栏请求,我们使用GeofencingRequest.Builder。构建器需要地理围栏对象的列表,这些对象是在createGeofenceList()方法中创建的。(即使我们只创建了一个地理围栏对象,构建器也需要一个列表,所以我们只需将我们的单个地理围栏添加到ArrayList中。)这就是我们设置地理围栏属性的地方:

.setRequestId("GeofenceLocation")
.setCircularRegion(
        47.6062,  //Latitude
        122.3321, //Longitude
        MINIMUM_RECOMENDED_RADIUS)
.setLoiteringDelay(30000)
.setExpirationDuration(Geofence.NEVER_EXPIRE)
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL)

只有逗留延迟是可选的,但我们需要它,因为我们正在使用DWELL转换。当调用setTransitionTypes()时,我们可以使用OR运算符(使用管道字符)组合多个转换类型。以下是一个使用ENTEREXIT的示例:

.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)

对于这个示例,我们使用了与模拟器相同的默认纬度和经度。根据需要更改这些值。

我们对Geofence.Builder()的调用创建了地理围栏对象。在准备好地理围栏列表后,我们调用GeofencingRequest.Builder并设置初始触发器为INITIAL_TRIGGER_DWELL。(如果您更改了前面的转换类型,您可能还需要更改初始触发器,否则我们的地理围栏创建可能会失败。)

我们需要的第二个对象是一个挂起 Intent,这是系统在地理围栏条件满足时通知我们的应用的方式。(严格来说,Intent 服务不是必需的,如果您的应用在后台监控地理围栏响应,您甚至可能不需要它。)我们的示例在地理围栏触发时显示一个吐司,但您可以在您的应用中自定义响应。

在创建了这两个对象之后,我们在检查了适当的权限后获得对GeofencingClient的引用。我们的示例仅检查必要的权限,因此您需要手动通过应用设置启用位置权限。生产应用应根据需要提示用户。(有关完整示例,请参阅第十五章中的The Android 6.0 Runtime Permission Model配方,Getting Your App Ready for the Play Store。)

更多内容...

要停止接收地理围栏通知,您可以使用RequestID参数或PendingIntent调用removeGeofences()方法。以下示例使用了我们用于通知的相同PendingIntent方法:

geofencingClient.removeGeofences(createGeofencePendingIntent())
        .addOnSuccessListener(this, new OnSuccessListener<Void>() {
            @Override
            public void onSuccess(Void aVoid) {
                //Success
            }
        })
        .addOnFailureListener(this, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                //Failuare
            }
        });

参见

第十五章:为应用准备 Play 商店

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

  • Android 6.0 运行时权限模型

  • 如何设置闹钟

  • 接收设备启动通知

  • 使用 AsyncTask 进行后台工作

  • 将语音识别添加到您的应用中

  • 如何将谷歌登录添加到您的应用中

简介

当我们接近这本书的结尾时,是时候在将应用发布到 Play 商店之前为您的应用添加一些最后的修饰了。本章中的配方涵盖了可以在用户保留或删除您的应用之间产生差异的主题。

我们的第一道菜,Android 6.0 运行时权限模型,无疑是一个重要的话题,可能是 Android 从 5.x 版本升级到 6.x 版本的主要原因!对 Android 权限模型的更改已经请求了一段时间,所以这个新模型是一个受欢迎的改变,至少对于用户来说是这样。

接下来,我们将查看如何设置闹钟中的闹钟。闹钟的主要好处之一是操作系统负责维护闹钟,即使您的应用没有运行。由于闹钟在设备重启后不会持续存在,我们还将查看如何检测设备重启,以便您可以在接收设备启动通知中重新创建您的闹钟。

几乎任何严肃的 Android 应用都需要一种方式来在主线程之外执行可能阻塞的任务。否则,您的应用可能会被认为运行缓慢,或者更糟,完全无响应。AsyncTask被设计用来简化创建后台工作任务的难度,正如我们将在使用 AsyncTask 进行后台工作配方中展示的那样。

如果您希望您的应用受益于免提输入或语音识别,请查看将语音识别添加到您的应用中配方,我们将探索 Google 语音 API。

最后,我们将以如何将谷歌登录添加到您的应用中配方来结束本章,展示如何使您的应用更加舒适并鼓励用户登录。

Android 6.0 运行时权限模型

旧的安全模型是许多 Android 用户的痛点。经常看到评论提到应用所需的权限。有时,权限是不切实际的(例如,手电筒应用需要互联网权限),但有时开发者有很好的理由请求某些权限。主要问题是这是一个全有或全无的选择。

这最终随着 Android 6 Marshmallow(API 23)的发布而改变。新的权限模型仍然像以前一样在清单中声明权限,但用户可以选择性地接受或拒绝每个权限。用户甚至可以撤销之前授予的权限。

虽然这对许多人来说是一个受欢迎的变革,但对于开发者来说,它有可能破坏之前正常工作的代码。我们已经在之前的菜谱中讨论了这一权限变更,因为它具有深远的影响。这个菜谱将把所有内容整合在一起,以便在实现你自己的应用中的这一变更时作为一个单一的参考点。

Google 现在要求应用的目标为 Android 6.0(API 23)或更高版本才能包含在 Play Store 中。如果你还没有更新你的应用,未更新的应用将在年底(2018 年)被移除。

准备工作

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

示例源代码将最小 API 设置为 23,但这不是必需的。如果你的compileSdkVersion是 API 23 或更高,编译器将标记你的代码以使用新的安全模型。

如何操作...

我们需要首先将所需的权限添加到清单中,然后我们将添加一个按钮来调用我们的检查权限代码。打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.SEND_SMS"/>
  1. 打开activity_main.xml并用此按钮替换现有的TextView
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Do Something"
    android:onClick="doSomething"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 打开MainActivity.java并将以下常量添加到类中:
private final int REQUEST_PERMISSION_SEND_SMS=1; 
  1. 添加此方法进行权限检查:
private boolean checkPermission(String permission) { 
    int permissionCheck = 
         ContextCompat.checkSelfPermission( 
            this, permission); 
    return (permissionCheck == 
         PackageManager.PERMISSION_GRANTED); 
} 
  1. 添加此方法来请求权限:
private void requestPermission(String permissionName, int permissionRequestCode) {    
    ActivityCompat.requestPermissions(this, new String[]{permissionName}, 
            permissionRequestCode);
}
  1. 添加此方法来显示解释对话框:
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();
}
  1. 添加此方法来处理按钮点击:
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();
    }
}
  1. 如下重写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, "Granted!", Toast.LENGTH_SHORT)
                        .show();
            } else {
                Toast.makeText(MainActivity.this, "Denied!", Toast.LENGTH_SHORT)
                        .show();
            }
            return;
        }
    }
}
  1. 现在,你可以在设备或模拟器上运行应用程序了。

它是如何工作的...

使用新的运行时权限模型涉及以下步骤:

  1. 检查你是否拥有所需的权限

  2. 如果不是,检查是否应该显示理由(意味着请求之前已被拒绝)

  3. 请求权限;只有操作系统可以显示权限请求

  4. 处理请求响应

这里是相应的函数:

  • ContextCompat.checkSelfPermission

  • ActivityCompat.requestPermissions

  • ActivityCompat.shouldShowRequestPermissionRationale

  • onRequestPermissionsResult

即使你在运行时请求权限,所需的权限也必须在 Android Manifest 中列出。如果没有指定权限,操作系统将自动拒绝请求。

更多内容...

你可以通过 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 事件的短暂延迟),则 Alarms 不是最佳解决方案。对于短延迟,使用 Handler 更容易且更高效,正如我们在几个先前的菜谱中所做的那样。

在使用闹钟时,请记住以下最佳实践:

  • 尽可能使用不频繁的闹钟时间

  • 避免唤醒设备

  • 尽可能使用不精确的时间;时间越精确,所需的资源越多

  • 避免根据时钟时间设置闹钟时间(例如 12:00);如果可能,添加随机调整以避免服务器拥堵(特别是在检查新内容,如天气或新闻时尤为重要)

闹钟有三个属性,如下所示:

  • 闹钟类型(见以下列表)

  • 触发时间(如果时间已经过去,则闹钟立即触发)

  • Pending Intent

重复闹钟具有相同的三个属性,加上一个间隔:

  • 闹钟类型(见以下列表)

  • 触发时间(如果时间已经过去,则立即触发)

  • 间隔

  • Pending Intent

有四种闹钟类型:

  • RTC实时时钟):这是基于墙上的时钟时间。这不会唤醒设备。

    设备。

  • RTC_WAKEUP:这是基于墙上的时钟时间。如果设备处于睡眠状态,这将唤醒设备。

    设备处于睡眠状态。

  • ELAPSED_REALTIME:这是基于设备启动以来经过的时间。

    这不会唤醒设备。

  • ELAPSED_REALTIME_WAKEUP:这是基于设备启动以来经过的时间。

    设备启动。如果设备处于睡眠状态,这将唤醒设备。

Elapsed Real Time 对于时间间隔闹钟(如每 30 分钟)更好。

闹钟在设备重启后不会持续存在。当设备关闭时,所有闹钟都会被取消,因此您需要在设备启动时负责重置闹钟。(有关更多信息,请参阅 Receive notification of device boot 菜谱。)

以下菜谱将演示如何使用 AlarmManager 创建闹钟。

准备中

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

如何做...

设置闹钟需要一个挂起意图,当闹钟被触发时,Android 会发送这个意图。因此,我们需要设置一个广播接收器来捕获闹钟意图。我们的 UI 将仅包含一个简单的按钮来设置闹钟。首先,打开 AndroidManifest 文件,按照以下步骤操作:

  1. 在与现有<activity>元素同一级别的<application>元素中添加以下<receiver>
<receiver android:name=".AlarmBroadcastReceiver">
    <intent-filter>
        <action android:name="com.packtpub.alarms.ACTION_ALARM" />
    </intent-filter>
</receiver>
  1. 打开activity_main.xml并将现有的 TextView 替换为

    以下按钮:

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Set Alarm"
    android:onClick="setAlarm"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 使用以下代码创建一个新的 Java 类AlarmBroadcastReceiver
public class AlarmBroadcastReceiver extends BroadcastReceiver {    
    public static final String ACTION_ALARM= "com.packtpub.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();
        }
    }
} 
  1. 打开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)getSystemService(Context.ALARM_SERVICE);
    long thirtyMinutes=SystemClock.elapsedRealtime() + 30 * 1000;
    alarmManager.set(AlarmManager.ELAPSED_REALTIME, thirtyMinutes, alarmIntent);
}
  1. 您现在可以开始在设备或模拟器上运行应用程序了。

它是如何工作的...

创建闹钟是通过以下代码行完成的:

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.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

参见

接收设备启动通知

Android 在其生命周期中会发送许多意图。其中第一个发送的意图是ACTION_BOOT_COMPLETED。如果您的应用程序需要知道设备何时启动,您需要捕获这个意图。

此菜谱将指导您完成在设备启动时接收通知所需的步骤。

准备工作

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

如何操作...

首先,打开 AndroidManifest 并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  1. 将以下<receiver>添加到<application>元素中,与现有的<activity>元素处于同一级别:
<receiver android:name=".BootBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</receiver>
  1. 使用以下代码创建一个新的 Java 类BootBroadcastReceiver
public class BootBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(
                "android.intent.action.BOOT_COMPLETED")) {            
            Toast.makeText(context, "BOOT_COMPLETED", Toast.LENGTH_SHORT).show();
        }
    }
}
  1. 重启设备以查看 Toast。

它是如何工作的...

当设备启动时,Android 会发送BOOT_COMPLETED意图。只要我们的应用程序有接收该意图的权限,我们就会在 Broadcast Receiver 中收到通知。

要使这一切工作,有三个方面需要考虑:

  • RECEIVE_BOOT_COMPLETED权限

  • 在接收器意图过滤器中添加BOOT_COMPLETEDDEFAULT

  • 在 Broadcast Receiver 中检查BOOT_COMPLETED动作

显然,你将想要用你自己的代码替换 Toast 消息,例如重新创建你可能需要的任何闹钟。

还有更多...

如果你遵循了前面的食谱,那么你已经有了一个 Broadcast Receiver。你不需要为每个动作创建单独的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();
    } 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。使用默认的 Phone & Tablet 选项,并在提示 Activity 类型时选择 Empty Activity。

如何做到这一点...

我们只需要一个按钮来演示这个例子。打开 activity_main.xml 并按照

这些步骤:

  1. 用以下按钮替换现有的 TextView:
<Button
    android:id="@+id/buttonStart"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Start"
    android:onClick="start"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 打开 MainActivity.java 并添加以下全局变量:
Button mButtonStart; 
  1. 添加 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++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return count;
    }
    @Override
    protected void onPostExecute(Integer returnVal) {
        super.onPostExecute(returnVal);
        mButtonStart.setEnabled(true);
    }
}
  1. 将以下代码添加到 onCreate() 以初始化按钮:
mButtonStart=findViewById(R.id.buttonStart);
  1. 添加按钮点击的方法:
public void start(View view){
    mButtonStart.setEnabled(false);
    new CountingTask().execute(10);
}
  1. 你现在可以运行应用程序在设备或模拟器上了。

它是如何工作的...

这是一个非常简单的 AsyncTask 示例,只是为了展示它的工作原理。技术上,只需要 doInBackground(),但通常,你希望在它完成时收到通知,这是通过 onPostExecute() 实现的。

AsyncTask 通过为 doInBackground() 方法创建一个工作线程来工作,然后在 onPostExecute() 回调中在 UI 线程上响应。我们的示例使用 Thread.Sleep() 方法使线程休眠指定的时间(在我们的示例中是 1000 毫秒)。由于我们用值 10 调用 CountingTask,后台任务将花费 10 秒钟。这个例子说明了实际上任务是在后台执行的,因为否则,Android 将在 5 秒后显示 ANR 对话框。

也很重要的是要注意,我们在执行任何 UI 操作(如在我们的示例中启用按钮)之前,等待 onPostExecute() 被调用。如果我们尝试在工作线程中修改 UI,代码将无法编译或抛出运行时异常。你也应该注意,我们如何在每次按钮点击时实例化一个新的 CountingTask 对象。这是因为 AsyncTask 只能执行一次。再次尝试调用 execute 也会抛出异常。

还有更多...

在其最简单的情况下,AsyncTask 可以非常简单,但它仍然非常灵活,如果你需要的话,还有更多选项可用。当使用 AsyncTask 与 Activity 时,了解 Activity 在何时被销毁和重新创建(例如,在方向改变期间)很重要,AsyncTask 会继续运行。这可能会使你的 AsyncTask 成为一个孤儿,并且它可能会对现在已销毁的活动做出响应(导致 NullPointer 异常)。因此,通常使用 AsyncTask 与 Fragment(在屏幕旋转时不会被销毁)一起使用。

参数类型

对于许多人来说,AsyncTask 最令人困惑的方面是在创建自己的类时参数。如果你查看我们的类声明,AsyncTask 有三个参数;它们定义如下:

AsyncTask<Params, Progress, Result > 

参数是泛型类型,并按以下方式使用:

  • 参数:这是用于调用 doInBackground() 的参数类型

  • 进度:这是用于发布更新的参数类型

  • 结果:这是用于发布结果的参数类型

当你声明自己的类时,用你需要的变量类型替换参数。

这是 AsyncTask 的流程以及如何使用前面的参数的示例:

  • onPreExecute(): 在 doInBackground() 开始之前调用

  • doInBackground(Params): 这个方法在后台线程中执行

  • onProgressUpdate(Progress): 当 doInBackground() 开始之前,这个方法会在 UI 线程中被调用。

    到工作线程中调用 publishProgress(Progress)

  • onPostExecute(Result): 当工作线程中的 publishProgress(Progress) 被调用后,这个方法会在 UI 线程中被调用。

    线程结束

取消任务

取消任务,请按照以下方式在对象上调用 cancel 方法:

< AsyncTask>.cancel(true); 

您需要拥有对象实例来访问 cancel() 方法。(在我们的上一个示例中,我们没有保存对象。)在设置 cancel(true) 之后,在 doInBackground() 中调用 isCancelled() 将返回 true,允许您退出循环。如果被取消,将调用 onCancelled() 而不是 onPostExecute()

参见

将语音识别添加到您的应用程序中

Android 2.2 (API 8) 在 Android 中引入了语音识别功能,并且几乎在每次新的主要 Android 版本发布中都得到了改进。本食谱将演示如何使用 Google 语音服务将语音识别添加到您的应用程序中。

准备工作

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

如何实现...

我们首先将在布局中添加一个“现在说话”(或麦克风)按钮,然后添加调用语音识别器所需的代码。打开 activity_main.xml 并按照以下步骤操作:

  1. 将现有的 TextView 替换为以下 XML:
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<ImageButton
    android:id="@+id/imageButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="img/ic_btn_speak_now"
    android:onClick="speakNow"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent" />
  1. 定义 REQUEST_SPEECH 常量:
private final int REQUEST_SPEECH=1; 
  1. 将以下代码添加到现有的 onCreate() 回调中:
PackageManager pm = getPackageManager();
List<ResolveInfo> activities = pm
        .queryIntentActivities(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
if (activities.isEmpty()) {
    findViewById(R.id.imageButton).setEnabled(false);
    Toast.makeText(this, "Speech Recognition Not Supported", Toast.LENGTH_LONG).show();
}
  1. 添加按钮点击方法:
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);
}
  1. 将以下代码添加到重写的 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 = findViewById(R.id.textView);
        if (!result.isEmpty()){
            textView.setText("");
            for (String item : result ) {
                textView.append(item+"\n");
            }
        }
    }
}
  1. 您现在可以开始在设备或模拟器上运行应用程序了。

它是如何工作的...

这里的工作是由 Android 中包含的 Google 语音识别器完成的。为了确保服务在设备上可用,我们在 onCreate() 中调用 PackageManager。如果至少有一个活动注册来处理 RecognizerIntent.ACTION_RECOGNIZE_SPEECH intent,那么我们知道它是可用的。如果没有可用的活动,我们将显示一个 Toast,指示语音识别不可用,并禁用麦克风按钮。

按钮点击通过调用使用 RecognizerIntent.ACTION_RECOGNIZE_SPEECH 创建的 intent 来启动识别过程。EXTRA_LANGUAGE_MODEL 参数是必需的,并且有以下两个选项:

  • LANGUAGE_MODEL_FREE_FORM

  • LANGUAGE_MODEL_WEB_SEARCH

我们在 onActivityResult() 回调中获取结果。如果结果等于 RESULT_OK,则我们应该有一个识别出的单词列表,我们可以使用 getStringArrayListExtra() 来检索它。数组列表将按识别置信度从高到低排序。

如果你想获取置信度评分,请使用 EXTRA_CONFIDENCE_SCORES 获取浮点数组。以下是一个示例:

float[] confidence = data.getFloatArrayExtra(RecognizerIntent.EXTRA_CONFIDENCE_SCORES);

置信度评分是可选的,可能不存在。1.0 分表示最高置信度,而 0.0 分表示最低置信度。

更多内容...

使用意图是一个快速简单的方法来获取语音识别;然而,如果你不想使用默认的 Google 活动,你可以直接调用 SpeechRecognizer 类。以下是如何实例化类的示例:

SpeechRecognizer speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this);

你需要添加 RECORD_AUDIO 权限并实现 RecognitionListener 类来处理语音事件。(有关更多信息,请参阅以下链接。)

相关内容

如何将 Google 登录添加到你的应用

Google 登录 允许你的用户使用他们的 Google 凭据登录到你的应用程序。此选项为你的用户提供以下优势:

  • 置信度,因为他们使用 Google

  • 方便之处在于他们可以使用现有的账户

对于你,作为开发者,也有一些优势:

  • 不需要编写自己的身份验证服务器的便利性

  • 更多用户登录到你的应用

本食谱将指导你将 Google 登录添加到你的应用程序中。以下是一个截图,显示了我们将要在食谱中创建的应用程序中的 "GoogleSignin" 按钮:

图片

准备工作

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

Google 登录使用 Google 服务插件,这需要一个 Google 服务配置文件,该文件可以从 Google 开发者控制台获取。要创建配置文件,你需要以下信息:

  • 你的应用程序包名

  • 你的签名证书的 SHA-1 哈希码(有关更多信息,请参阅食谱末尾的 Authenticating Your Client 链接)

当你有了信息,登录到这个 Google 链接,并按照向导启用登录:

developers.google.com/identity/sign-in/android/start-integrating?refresh=1#configure_a_console_name_project

如果您正在下载源文件,您需要在遵循前面的步骤时创建一个新的包名,因为现有的包名已经被注册。

如何操作...

在完成前面的 准备就绪 部分后,按照以下步骤操作:

  1. 将在 准备就绪 部分下载的 google-services.json 文件复制到您的应用文件夹中(<项目文件夹>\GoogleSignIn\app

  2. 打开应用模块的 Gradle 构建文件,build.gradle (Module: app),并在依赖项部分添加以下语句:

implementation 'com.google.android.gms:play-services-auth:16.0.0'
  1. 打开 activity_main.xml 并将现有的 TextView 替换为以下 XML:
<com.google.android.gms.common.SignInButton
    android:id="@+id/signInButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 打开 MainActivity.java 并添加以下全局声明:
private final int REQUEST_SIGN_IN=1;
GoogleSignInClient mGoogleSignInClient;
  1. 将以下代码添加到现有的 onCreate() 中:
findViewById(R.id.signInButton).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        signIn();
    }
});
GoogleSignInOptions googleSignInOptions = new GoogleSignInOptions
        .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        .requestEmail()
        .build();
mGoogleSignInClient = GoogleSignIn.getClient(this, googleSignInOptions);
  1. 添加 signIn() 方法:
private void signIn() {
    Intent signInIntent = mGoogleSignInClient.getSignInIntent();
    startActivityForResult(signInIntent, REQUEST_SIGN_IN);
}
  1. 如下创建 onActivityResult() 回调的覆盖:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == REQUEST_SIGN_IN) {
        Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
        try {
            GoogleSignInAccount account = task.getResult(ApiException.class);
            findViewById(R.id.signInButton).setVisibility(View.GONE);
            Toast.makeText(this, "Logged in:"+account.getDisplayName(), Toast.LENGTH_SHORT)
                    .show();
        } catch (ApiException e) {
            e.printStackTrace();
            Toast.makeText(this, "Sign in failed:"+e.getLocalizedMessage(), Toast.LENGTH_SHORT)
                    .show();
        }
    }
}
  1. 您现在可以运行应用程序在设备或模拟器上。

它是如何工作的...

Google 通过他们的 GoogleSignInClientGoogleSignInOptions API 使添加 Google 登录变得相对简单。首先,我们使用构建器创建一个 GoogleSignInOptions 对象。这是我们指定我们想要的登录选项的地方,例如请求电子邮件 ID。然后,调用 GoogleSignIn.getClient() 方法来获取 GoogleSignInClient

当用户点击 Google 登录按钮(使用 com.google.android.gms.common.SignInButton 类创建)时,我们向处理程序发送一个 GoogleSignInApi 的 Intent。我们在 onActivityResult() 中处理结果。如果登录成功,我们可以获取账户详情。在我们的示例中,我们只获取电子邮件,但还有其他附加信息,例如以下内容:

  • getDisplayName(): 这是显示名称

  • getEmail(): 电子邮件地址

  • getId(): Google 账户的唯一 ID

  • getPhotoUrl(): 显示照片

  • getIdToken(): 这用于后端身份验证

参考在“另请参阅”部分中的 GoogleSignInAccount 链接以获取完整列表。

更多内容...

如果你想检查用户是否已经登录过?

GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(this);

如果账户不为空,那么您就有最后一次登录的详细信息。

另请参阅

第十六章:Kotlin 入门

本章涵盖了以下食谱:

  • 如何使用 Kotlin 创建 Android 项目

  • 在 Kotlin 中创建 Toast

  • Kotlin 中的运行时权限

简介

Kotlin 可能是近年来 Android 开发中最大的变化,至少从 Eclipse 到 Android Studio 的变化来看。Kotlin 于 2011 年 7 月由 JetBrains 宣布,并于 2012 年 2 月作为开源软件发布。1.0 版本于 2016 年 2 月发布,Google 在 2017 年的 Google I/O 上宣布了对该语言的一级支持。Android Studio 3.0 随着对 Kotlin 的全面支持而发布(并且是以下食谱的最低要求。)

为什么选择 Kotlin?

在已有如此多的语言可供选择的情况下,为什么 JetBrains 还要创建另一种语言呢?根据他们自己的公告,他们正在寻找 Java 的替代品。由于他们现有的超过 70% 的代码已经是 Java 编写的,从头开始并不是一个选择。他们需要一个与现代 Java 兼容的现代语言。在比较了许多选项并发现没有任何一种能满足所有需求后,他们决定创建 Kotlin。Kotlin 的一个有趣之处在于,它是通过使用该语言的开发者创建的,而不是学者。以下是 Kotlin 为 Android 开发带来的某些特性:

  • 代码更简洁

  • 完全支持 JVM,可以在任何使用 Java 的地方使用

  • IDE 中已包含全面支持,特别是鉴于 JetBrains 既是 Kotlin 语言的创造者,也是 Android Studio 的创造者

  • 更安全的代码:语言内置了空值检查

  • 激增的受欢迎程度:许多大型公司正在采用 Kotlin

  • 现代语言:提供了最新语言提供中的许多特性

  • 更有趣味性:许多调查发现 Kotlin 具有最高的满意度评分

希望这些原因足以让您至少了解一下 Kotlin,尤其是对于 Java 开发者来说,通常可以轻松地跟随代码。正如您在本章的第一个示例中所看到的,将 Kotlin 支持添加到 Android 项目中非常简单。

如何使用 Kotlin 创建 Android 项目

在 Kotlin 中开发再简单不过了!正如您从下面的简单步骤中看到的,完整的 Kotlin 支持已经内置到 Android Studio IDE 中。

准备工作

Kotlin 支持需要 Android Studio 3.0 或更高版本,因此对于本食谱或本章中的任何食谱都没有额外的要求。

如何做...

实际上,将 Kotlin 支持添加到 Android 项目中非常简单,您可能已经注意到了复选框。在创建新项目时,Android Studio 会为您提供选项。实际上,它如此明显,您可能甚至没有注意到它,所以我们从开始处开始,展示一个截图。首先,启动 Android Studio 并点击“开始新 Android 项目”:

  1. 在“创建 Android 项目”对话框中,勾选“包含 Kotlin 支持”复选框,如图所示:

  1. 就这样!点击剩余的对话框,您将拥有您的第一个 Android Kotlin 项目。

它是如何工作的...

IDE 已经处理了您开始使用 Kotlin 开发所需的所有操作。甚至第一个 Activity 现在也是使用 Kotlin 代码创建的,正如您在打开 MainActivity.kt 文件时可以看到的那样:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

如您所见,这与 Java 代码非常相似。Java 开发者可能会能够阅读并至少理解 Kotlin 代码。如果您是第一次接触 Kotlin,以下是一些值得注意的事项:分号不是行终止符所必需的。另一个值得注意的点是,变量类型位于变量名称之后,用冒号分隔。关于 Bundle 后面的那个问号又是怎么回事呢?这表示变量可能为 null。

还有更多...

如果您已经有一个现有的项目并且想要添加 Kotlin 代码,可以通过 File | New | Kotlin File/Class 菜单选项来完成,如图所示:

Android Studio(3.0 及以上版本)提供了两种轻松将 Java 代码转换为 Kotlin 的选项:

  1. 打开一个 Java 文件,并选择 Code | Convert to Kotlin 菜单项。

  2. 在 Android Studio 中,将您的 Java 代码复制到剪贴板,然后将代码粘贴到您的 Kotlin 文件中。当您看到以下对话框询问是否要转换代码时,请选择是:

相关链接

这里有一些资源,可以帮助您开始 Kotlin 开发:

在 Kotlin 中创建 Toast

Toast 是开发应用程序时非常有用的工具,尤其是在学习新语言时,因此我们将重新审视 Toast。这个配方将向您展示显示非常熟悉的 Toast 的 Kotlin 方法,如图所示:

准备工作

在 Android Studio 中创建一个新的项目,命名为 KotlinToast。使用默认的 Phone & Tablet 选项,并在提示 Activity 类型时选择 Empty Activity。请记住,在创建 Android 项目对话框中勾选 Include Kotlin support 复选框。

如何操作...

我们将使用默认的 Toast 布局来保持简单,并专注于 Kotlin 代码。首先打开 activity_main.xml 并按照以下步骤操作:

  1. 将现有的 <TextView> 元素替换为 <Button>,如下所示:
<Button
 android:id="@+id/button"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="Show Toast"
 android:onClick="showToast"
 app:layout_constraintLeft_toLeftOf="parent"
 app:layout_constraintRight_toRightOf="parent"
 app:layout_constraintTop_toTopOf="parent" />
  1. 现在,打开 ActivityMain.kt 并将以下代码添加到现有的 onCreate() 方法中:
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
    Toast.makeText(this, "First Toast in Kotlin", Toast.LENGTH_LONG).show()
}
  1. 在设备或模拟器上运行程序。

它是如何工作的...

显示 Toast 实际上只有两个部分:创建事件监听器和调用 Toast 本身。我们使用setOnClickListener创建事件监听器。这与 Java 中的概念相同,只是代码语法更简洁。在大括号中,我们有将被调用的代码。在我们的例子中,它是 Toast。这基本上看起来是一样的,因为它调用的是完全相同的库,正如你将在导入语句中看到的那样:

import android.widget.Toast

如果你之前在 Java 中使用过 Toast,或者已经通过第八章中的使用自定义布局创建 Toast配方进行过操作,警报和通知,那么你会注意到这看起来非常相似。确实如此。但你也会注意到它要简单得多,代码也更干净。这是 Kotlin 的一个大吸引力。如果你想创建一个自定义布局,就像之前的 Java 示例一样?基本上,它们是相同的,因为资源(布局 XML 和可绘制资源)不是 Kotlin 或 Java 特定的;它们是 Android 特定的。所以,使用与之前示例相同的资源。

参见

  • 使用自定义布局创建 Toast的配方在第八章,警报和通知

Kotlin 中的运行时权限

尽管运行时权限模型早在 Android 6.0(API 23)时就已经发布,但这个话题仍然收到很多查询。由于这基本上是所有未来应用的基本要求,你很可能也需要在 Kotlin 中实现这一点。查看之前的配方(见以下链接)以获取有关 API 的信息,以及这个配方以获取 Kotlin 代码。

准备工作

在 Android Studio 中创建一个新的项目并命名为KotlinRuntimePermission。使用默认的 Phone & Tablet 选项,当被提示选择 Activity 类型时,选择 Empty Activity,并记得勾选 Include Kotlin support 复选框。

示例源代码将最小 API 设置为 23,但这不是必需的。如果你的compileSdkVersion是 API 23 或更高,编译器将标记你的代码以使用新的安全模型。

如何实现...

我们需要首先将所需的权限添加到清单中,然后我们将添加一个按钮来调用我们的检查权限代码。打开 Android Manifest 并按照以下步骤操作:

  1. 添加以下权限:
<uses-permission android:name="android.permission.SEND_SMS"/>
  1. 打开activity_main.xml并将现有的TextView替换为以下按钮:
<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Do Something"
    android:onClick="doSomething"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  1. 打开MainActivity.kt并在 MainActivity 类上方(外部)添加以下常量:
private const val REQUEST_PERMISSION = 1
  1. 添加此方法以进行权限检查:
private fun checkPermission(permission: String): Boolean {
    val permissionCheck = ContextCompat.checkSelfPermission(this, permission)
    return permissionCheck == PackageManager.PERMISSION_GRANTED
}
  1. 添加此方法以请求权限:
private fun requestPermission(permissionName: String, permissionRequestCode: Int) {
    ActivityCompat.requestPermissions(this, arrayOf(permissionName),
            permissionRequestCode)
}
  1. 添加此方法以显示解释对话框:
private fun showExplanation(title: String, message: String,
                            permission: String,
                            permissionRequestCode: Int) {
    val builder = AlertDialog.Builder(this)
    builder.setTitle(title)
            .setMessage(message)
            .setPositiveButton(android.R.string.ok
            ) { dialog, id -> requestPermission(permission, permissionRequestCode) }
    builder.create().show()
}
  1. 添加此方法以处理按钮点击:
fun 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)
        } else {
            requestPermission(Manifest.permission.SEND_SMS,
                    REQUEST_PERMISSION)
        }
    } else {
        Toast.makeText(this@MainActivity, "Permission (already) Granted!", Toast.LENGTH_SHORT)
                .show()
    }
}
  1. 如下重写onRequestPermissionsResult()
override fun onRequestPermissionsResult(requestCode: Int,
                                        permissions: Array<String>,
                                        grantResults: IntArray) {
    when (requestCode) {
        REQUEST_PERMISSION -> {
            if (grantResults.isNotEmpty() && grantResults[0] == 
                    PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this@MainActivity, "Granted!", Toast.LENGTH_SHORT)
                        .show()
            } else {
                Toast.makeText(this@MainActivity, "Denied!", Toast.LENGTH_SHORT)
                        .show()
            }
            return
        }
    }
}
  1. 现在,你可以在设备或模拟器上运行应用程序了。

工作原理...

使用新的运行时权限模型包括以下内容:

  1. 检查你是否拥有所需的权限

  2. 如果不是,检查我们是否应该显示理由(意味着请求之前已被拒绝)

  3. 请求权限;只有操作系统才能显示权限请求

  4. 处理请求响应

这里是相应的函数:

  • ContextCompat.checkSelfPermission

  • ActivityCompat.requestPermissions

  • ActivityCompat.shouldShowRequestPermissionRationale

  • onRequestPermissionsResult

即使您在运行时请求权限,所需的权限也必须在 AndroidManifest.xml 中列出。如果未指定权限,操作系统将自动拒绝请求。

参见

  • 对于 Java 版本,请参阅第十五章中的《Android 6.0 运行时权限模型》配方,在为 Play 商店准备您的应用
posted @ 2025-10-25 10:41  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报