Android-Studio-秘籍-全-

Android Studio 秘籍(全)

原文:zh.annas-archive.org/md5/4884403F3172F01088859FB8C5497CF5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Android Studio 是开发 Android 应用程序的最佳 IDE,任何想要开发专业 Android 应用程序的人都可以免费使用。

现在有了 Android Studio,我们有了一个稳定和更快的 IDE,它带来了很多很酷的东西,比如 Gradle、更好的重构方法和更好的布局编辑器。如果您曾经使用过 Eclipse,那么您一定会喜欢这个 IDE。

简而言之,Android Studio 真的带回了移动开发的乐趣,在这本书中,我们将看到如何做到这一点。

本书涵盖的内容

第一章,欢迎来到 Android Studio,演示了如何配置 Android Studio 和 Genymotion,这是一个非常快速的模拟器。

第二章,具有基于云的后端的应用程序,解释了如何使用 Parse 在很短的时间内开发使用基于云的后端的应用程序。

第三章,Material Design,解释了材料设计的概念以及如何使用 RecycleViews、CardViews 和过渡来实现它。

第四章,Android Wear,涵盖了 Android Wear API 以及如何开发自己的手表表盘或其他在智能手表上运行的应用程序。

第五章,Size Does Matter,演示了如何使用片段和其他资源来帮助您创建能够在手机、平板电脑、平板电脑甚至电视上运行的应用程序。我们将即时连接到 YouTube API,使示例更有趣。

第六章,Capture and Share,是关于使用新的 Camera2 API 捕获和预览图像的深入教程。它还告诉您如何在 Facebook 上分享捕获的图像。

第七章,内容提供程序和观察者,解释了如何从使用内容提供程序来显示和观察持久数据中受益。

第八章,Improving Quality,详细介绍了应用模式、单元测试和代码分析工具。

第九章,Improving Performance,介绍了如何使用设备监视器来优化应用程序的内存管理,以及如何使用手机上的开发者选项来检测过度绘制和其他性能问题。

第十章,Beta Testing Your Apps,指导您完成一些最后步骤,例如使用构建变体(类型和风味)和在 Google Play 商店上进行测试版分发。除此之外,它还涵盖了 Android Marshmallow(6.0)提供的运行时权限与安装权限的不同之处。

您需要为本书准备什么

对于这本书,您需要下载并设置 Android Studio 和最新的 SDK。Android Studio 是免费的,适用于 Windows、OSX 和 Linux。

强烈建议至少拥有一部手机、平板电脑或平板电脑,但在第一章,欢迎来到 Android Studio 中,我们将向您介绍 Genymotion,一个非常快速的模拟器,在大多数情况下可以代替真实设备使用。

最后,对于一些示例,您需要拥有 Google 开发者帐户。如果您还没有,请尽快获取一个。毕竟,您需要一个才能将您的应用程序放入 Play 商店。

这本书是为谁准备的

这本书适合任何已经熟悉 Java 语法并可能已经开发了一些 Android 应用程序的人,例如使用 Eclipse IDE。

这本书特别解释了使用 Android Studio 进行 Android 开发的概念。为了演示这些概念,提供了真实世界的示例。而且,通过真实世界的应用程序,我指的是连接到后端并与 Google Play 服务或 Facebook 等进行通信的应用程序。

部分

在本书中,您会经常看到几个标题(准备工作、如何做、它是如何工作的、还有更多和另请参阅)。

为了清晰地说明如何完成食谱,我们使用以下这些部分:

准备工作

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

如何做…

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

它是如何工作的…

本节通常包括对前一节发生的事情的详细解释。

还有更多…

本节包括有关食谱的额外信息,以使读者更加了解食谱。

另请参阅

本节提供了有关食谱的其他有用信息的有用链接。

约定

所有针对 Android Studio 的屏幕截图、快捷方式和其他元素都基于 OSX 上的 Android Studio。

OSX 被使用的主要原因是因为它允许我们在同一台机器上为 Android 和 iOS 开发应用程序。除此之外,选择特定操作系统的原因除了个人(或公司)的偏好之外没有其他原因。

虽然屏幕截图是基于 OSX 上的 Android Studio,但如果您的操作系统是 Windows 或 Linux,您也不难弄清楚事情。

在需要时,还会提及 Windows 的快捷键。

在本书中,您会发现一些区分不同类型信息的文本样式。以下是一些这些样式的示例及其含义的解释。

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄都会以这种方式显示:"我们可以通过使用 include 指令包含其他上下文"。

代码块设置如下:

public void onSectionAttached(int number) {
    switch (number) {
        case 0:
            mTitle = getString(  
             R.string.title_section_daily_notes);
            break;

        case 1:
            mTitle = getString( 
             R.string.title_section_note_list);
             break;
    }
}

新术语重要单词以粗体显示。例如,屏幕上看到的菜单或对话框中的单词会以这种方式出现在文本中:"单击下一步按钮将您移至下一个屏幕"。

警告或重要提示会以这种方式显示在一个框中。

提示

技巧和窍门会以这种方式出现。

第一章:欢迎来到 Android Studio

在本章中,我们将涵盖与 Android Studio 相关的一些基本任务。在阅读本章和其他章节时,您将学习如何有效地使用 Android Studio。

在本章中,您将学习以下配方:

  • 创建您的第一个名为Hello Android Studio的应用程序。

  • 使用 Gradle 构建脚本

  • 使用名为 Genymotion 的模拟器测试您的应用程序

  • 重构您的代码

介绍

本章是对 Android Studio 的介绍,并提供了这个集成开发环境(IDE)所配备的不同工具的概览。除此之外,还将在这里讨论一些其他重要的工具,比如 Genymotion,我强烈建议您使用它来测试您的应用在不同类型的设备上。

使用 Android Studio,您可以创建任何您喜欢的应用程序。手机应用程序、平板电脑应用程序、手表和其他可穿戴设备应用程序、谷歌眼镜、电视应用程序,甚至汽车应用程序。

如果您已经有移动编程经验,甚至以前使用过 Android 应用程序和 Eclipse,并且想要了解如何创建拍照、播放媒体、在任何设备上工作、连接到云端或者您能想到的其他任何功能的应用程序,那么这本书就适合您!

本书中描述的所有配方都是基于 Mac 上的 Android Studio;但是,如果您使用的是 Windows 或 Linux 上的 Android Studio,这一点完全没有问题。所有平台的术语都是相同的。只是每个配方提供的截图可能看起来有点不同,但我相信您可以通过一点努力找出来。如果 Windows 有任何重大差异,我会告诉您的。

为什么我们应该使用 Android Studio

Android Studio 是开发 Android 应用程序的推荐 IDE,对于任何开发专业 Android 应用程序的人来说都是免费的。Android Studio 基于 JetBrains IntelliJ IDEA 软件,这可能解释了为什么即使 Android Studio 的预览和测试版本都比 Eclipse 更好,以及为什么许多 Android 开发人员从一开始就使用它作为他们的 IDE。

Android Studio 的第一个稳定版本于 2014 年 12 月发布,取代了 Eclipse(带有 Android 开发工具)成为 Android 开发的主要 IDE。现在,有了 Android Studio,我们不仅拥有了一个更稳定和更快的 IDE,而且还有了一些很酷的东西,比如 Gradle、更好的重构方法和更好的布局编辑器,仅举几例。

好吧,我偶尔还是会遇到一些奇怪的问题(我想这就是作为移动开发人员有时会遇到的情况),但我肯定不会像在使用 Eclipse 时那样感到沮丧。如果您只是使用 Eclipse 进行普通的 Java 开发,我想那也还好;但是,它与 Android 不兼容。如果您以前在 Java 开发任务中使用过 IntelliJ IDEA,那么 Android Studio 对您来说会看起来非常熟悉。

Android Studio 真的让移动开发变得有趣起来。如果您目前正在使用 Eclipse,那么您应该立即切换到 Android Studio!要亲自看看,请从developer.android.com/sdk/index.html获取它,并立即开始使用 Android Studio 构建酷炫的应用程序。

碎片化

留下的是 Android 开发中需要处理的碎片化挑战。有许多设备运行在许多 Android 版本和版本上。

有很多 Android 版本,导致了碎片化。因此,您不能期望所有设备都能运行在最新的 Android 版本上。事实上,大多数设备都没有。许多设备仍在运行 Android 4.x(甚至更旧的版本)。

在这里,您可以看到一个包含所有相关 Android 版本和分发数字的表格。这个表格中的数字表明,如果您决定支持 Android 4.0 及以后的版本,您将能够触及 88.7%的所有 Android 用户。在这个例子中,显示了 2015 年第二季度的数字,这解释了为什么Android Marshmallow (6.0)在这里没有列出。如果您在 Android Studio 中创建一个新项目,您可以通过在创建新项目向导对话框中点击帮助我选择链接来获取实际的数字,这将在接下来的章节中找出。

让我们来看一下下面的屏幕截图,描述了不同 Android 平台版本的累积分布以及它们的 API 级别:

碎片化

除了软件碎片化之外,还有许多硬件碎片化需要注意。编写 Android 应用程序并不难,但编写一个能够在任何 Android 设备上正常运行的应用程序确实很难。

一个好的应用程序应该能够在尽可能多的不同设备上运行。例如,想象一个拍照的应用程序。Android 设备可能有一个摄像头,多个摄像头,或者根本没有摄像头。根据您的应用程序提供的其他功能,您可能还需要担心其他事情,比如设备是否能够录制声音等。

我可以想象你想要触及尽可能多的受众,所以你应该始终问自己,你的应用功能需求中哪些是必须的,哪些是不必要的。如果设备没有摄像头,用户可能无法拍照,但这真的是不允许用户使用应用的理由吗?

在 Android Marshmallow (6.0)中引入运行时权限使您更加重视在应用程序中提供某种后备功能。至少您需要解释为什么某个特定功能在您的应用程序中不可用。例如,用户设备不支持它或用户没有为其授予权限。

这本书将帮助您处理 Android 碎片化和其他问题。

创建您的第一个名为 Hello Android Studio 的应用程序

下载 Android Studio 后,安装并按照设置向导进行操作。向导会检查一些要求,例如Java 开发工具包(JDK)是否可用,以及其他重要的元素,安装向导会引导您完成这些操作。

安装完成后,是时候使用 Android Studio 开发您的第一个 Android 应用程序了,只是为了检查一切是否已正确安装并且正常运行。这可能不会让人感到意外,这就是 Hello Android Studio 教程的用武之地。

准备就绪

要完成这个教程,你需要一个运行中的 Android Studio IDE,一个 Android 软件开发工具包(SDK)和一个 Android 设备。不需要其他先决条件。

如何做...

让我们使用 Android Studio 创建我们的第一个 Android 应用程序,以检查一切是否正常运行,以下是帮助的步骤:

  1. 启动 Android Studio。几秒钟后,欢迎使用 Android Studio对话框将显示给您。

  2. 选择开始一个新的 Android Studio 项目选项。然后,配置您的新项目对话框将出现。

  3. 对于应用程序名称,输入HelloAndroidStudio;对于公司域字段,输入packtpub.com(或者如果您愿意,可以使用您自己公司的域名)。

  4. 建议使用packtpub.comhelloandroidstudio等包名称,并在输入时进行更新。如果愿意,可以在单击“下一步”按钮之前编辑项目位置

  5. 目标 Android 设备对话框中,选中手机和平板电脑选项。不要选择其他选项。稍后我们将创建一些其他有趣的目标,比如 Android Wear 应用程序。对于最低 SDK,请选择API 14。如果该选项尚未(尚)可用,请单击其他可用的 SDK。单击下一步按钮继续。

  6. 在下一个对话框中为移动设备添加活动,选择空白活动选项,然后单击下一步按钮。

  7. 在此之后将显示最终对话框自定义活动。保持所有值不变,然后单击完成按钮。

  8. Android Studio 现在将为您创建这个新应用程序。过一会儿,项目视图、一个MainActivity类和一个activity_main.xml布局将显示出来。如果您通过单击左侧 Android Studio 上显示绿色小安卓人和文字为Android的按钮,将项目视图的视角从Android更改为项目,布局看起来会更像您习惯的样子,也就是说,如果您以前使用过 Eclipse。

  9. 双击app文件夹以展开它。您会注意到一个名为build.gradle的文件(请注意,此文件也存在于根级别)。

  10. 双击build.gradle文件以打开它,并查看compileSdkVersionminSdkVersiontargetSdkVersion的值。默认情况下,compileSdkVersion的值始终与最新(可用)的 SDK 相关。minSdkVersion的值是您在目标 Android 设备对话框中选择的值。

注意

如果您希望使用不同的 SDK 进行编译,您必须更改compileSdkVersion的值。您选择的版本可能需要先安装。如果您对当前的配置满意,请立即转到第 14 步。

  1. 如果您想要检查已安装了哪些 SDK,请从主菜单中选择工具选项,然后从SDK Manager子菜单中选择Android如何操作...

  2. Android SDK Manager对话框显示了已安装的 SDK。如果您需要安装其他 SDK,您可以选中所需的元素,然后单击安装 n 个软件包...按钮。

  3. 安装所需的 SDK 并配置好您的build.gradle文件后,您现在可以运行您的应用程序了。

  4. 如果您要用物理设备进行 Android 开发,您需要先解锁开发者选项。在您的设备上,启动设置应用程序,然后转到设备信息选项。(此选项可能位于常规选项卡或部分,或者根据您的设备运行的 Android 版本和风格而在其他位置)。

注意

如果您没有真实设备,我强烈建议您尽快获取一个。您现在可以使用模拟器。您可以使用 Android SDK 附带的模拟器,或者您可以先阅读有关 Genymotion 的教程,了解如何使用模拟设备。

  1. 设备信息视图中,向下滚动直到看到构建号选项。现在,点击七(7)次构建号以解锁(启用)开发者模式。(不,这不是一个玩笑)。您现在已经解锁了开发者菜单。

注意

在较旧的 Android 版本(4.2 以下),此步骤可能会被跳过,或者如果开发者选项已经作为设置应用程序中的菜单项可用,此步骤可能会被跳过。

  1. 现在您的设置应用程序中有一个名为开发者选项的新选项,请单击它并查看。重要的是您在此菜单中启用USB 调试选项。此外,您可能希望启用或禁用其他一些选项。

  2. 通过单击 Android Studio 旁边的下拉框中读取应用程序的绿色三角形,连接您的设备并从 Android Studio 运行您的应用程序。或者,从运行菜单中选择运行...选项。然后,选择设备对话框将出现。您的设备现在应该出现在选择运行设备选项的列表中。(如果您的设备未出现在列表中,请重新连接您的设备。)

  3. 点击确定按钮。(对于 Windows,在您能够连接设备之前,通常需要先安装驱动程序。)

  4. 在您的设备上,可能会弹出一个对话框,要求您接受指纹。选择允许以继续。

应用程序现在正在部署到您的设备上。如果一切顺利,您的新应用程序现在将显示在您的设备上,上面写着Hello world!万岁!我承认这并不是一个非常令人兴奋的应用程序,但至少我们现在知道 Android Studio 和您的设备已经正确配置。

它是如何工作的...

Android Studio 将处理应用程序设置的基本部分。您只需要选择应用程序的目标和最低 SDK。目前使用 API 级别 14(Android 4.0)是最佳选择,因为这将使您的应用程序能够在大多数 Android 设备上运行。

应用程序将由 Android Studio 针对所选择的(编译)SDK 进行编译。

应用程序将被复制到您的设备上。启用USB 调试选项将有助于您解决任何问题,我们稍后将会发现。

Gradle 构建脚本的使用

Android Studio 使用 Gradle 构建脚本。它是一个项目自动化工具,使用领域特定语言DSL)而不是更常见的 XML 形式来创建项目的配置。

项目附带一个顶级构建文件和每个模块的构建文件。这些文件称为build.gradle。大多数情况下,只有应用程序模块的构建文件需要您的注意。

注意

您可能会注意到,以前在 Android 清单文件中找到的一些属性,例如目标 SDK 和版本属性,现在在构建文件中定义,并且应该仅驻留在构建文件中。

典型的build.gradle文件可能如下所示:

applylugin: 'com.android.application'
android {
  compileSdkVersion 21
  buildToolsVersion "21.0.0"
  defaultConfig {
  minSdkVersion 8
  targetSdkVersion 21
  versionCode 1
  versionName "0.1"
  } 
}
dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
}

Gradle 构建系统现在不是您需要过多担心的东西。在以后的教程中,我们将看到它的真正威力。该系统还设计用于支持在创建 Android 应用程序时可能面临的复杂场景,例如处理为各种客户定制的相同应用程序的版本(构建风格)或为不同设备类型或不同 Android OS 版本创建多个 APK 文件。

目前,只需知道这是我们将定义compileSdkVersiontargetSdkVersionminSdkVersion的地方,就像您之前在使用 Eclipse 时在清单文件中所做的那样。

此外,这也是我们定义versionCodeversionName的地方,这反映了您的应用程序的版本,如果有人要更新您编写的应用程序,这将非常有用。

Gradle 功能的另一个有趣的关键元素是依赖关系。依赖关系可以是本地或远程库和 JAR 文件。项目依赖于它们以便能够编译和运行。在您将在上一个文件夹中找到的app文件夹中的build.gradle文件中,您将找到库所在的定义的存储库。jCenter是默认存储库。

例如,如果您希望添加Parse功能,这是我们将在下一章的教程中做的事情,以下依赖声明将向您的项目添加本地 Parse 库:

dependencies {
compile fileTree(dir: 'libs', include: 'Parse-*.jar')compile project(':Parse-1.9.1')
}

使用外部库变得更加容易。例如,如果你想添加UniversalImageLoader,一个用于从互联网加载图像的知名库,或者如果你想要使用Gson库的功能,它基本上是 JSON 数据的对象包装器,那么以下依赖声明将使这些库可用于项目:

dependencies {
compile 'com.google.code.gson:gson:2.3+'
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3'
}

还有更多...

下一章的食谱中将解释一些其他 Gradle 概念。Gradle 是一个可以写一本书的话题,如果你想了解更多关于它的信息,你可以在互联网上找到许多有趣的深入教程。

另请参阅

  • 有关 Gradle 构建脚本的更多信息,请参阅第二章,带有基于云的后端的应用程序

使用名为 Genymotion 的模拟器测试你的应用程序

测试你的应用程序的最佳方法是使用真实设备。Android 模拟器非常慢,而且没有提供真实设备所具有的所有功能,比如相机和各种传感器。

我可以想象你可能只有一个或几个设备。有成千上万的 Android 设备可用,许多品牌和型号都在定制的(例如三星设备)或纯净的(如 Nexus 设备)Android OS 版本上运行,而且你能想到的任何 Android 版本上进行真机测试都会变得非常昂贵。

例如,如果你正在创建一个应该在 Android 2.3、Android 4.x 和 Android 5.x 上运行良好的应用程序,使用模拟设备可能会很方便。不幸的是,默认的模拟器非常慢。在模拟器上启动 Android 需要很长时间,调试也可能非常慢。为了让模拟器快一点,你可以尝试安装硬件加速执行管理器HAXM)。有一些关于如何做到这一点的主题在互联网上,然而,有一个更好的解决方案,那就是 Genymotion。

Genymotion 是一个真实、快速、易于使用的模拟器,并且具有许多真实设备配置。你可以在其网站www.genymotion.com上了解更多关于 Genymotion 的信息。它有免费和付费版本可用。免费版本对于起步来说是可以的。

准备工作

确保你有互联网访问权限和足够的硬盘空间。我们需要下载 VirtualBox 和 Genymotion。之后,你就可以准备创建你的第一个虚拟设备了。让魔法开始吧。

如何做...

让我们安装 Genymotion 以准备 Android Studio 与运行流畅的模拟设备一起使用:

  1. 需要安装 Oracle 的 VirtualBox 和 Genymotion 应用程序。这是因为 Genymotion 在后台使用Oracle 虚拟机VM)VirtualBox 的虚拟化技术来虚拟化各种 Android 操作系统。如果你的计算机上还没有安装 Oracle VM VirtualBox(或者你的 VirtualBox 版本低于 4.1.1,不兼容 Genymotion),你需要先安装它。

从 VirtualBox 下载页面下载适用于 OS X 主机(或 Windows)的 VirtualBox,网址为www.virtualbox.org/wiki/Downloads

安装 VirtualBox,然后重新启动计算机。

从 Genymotion 的网页www.genymotion.com/#!/download下载 Genymotion。

  1. 现在,打开并安装下载的文件。

  2. 运行 Genymotion。然后会有一个对话框询问你是否要创建一个新设备。点击按钮来创建。之后,你可以通过在主屏幕上点击+(加号)按钮来创建额外的设备。

  3. 在对话框的左侧下拉列表中选择 Android OS 版本。

  4. 从中心的下拉列表中选择一个虚拟设备(品牌和型号),然后点击下一步按钮。

  5. 给您的设备命名。建议您在设备名称中包括设备和操作系统版本,以便在以后使用时可以轻松识别您正在测试的内容。

  6. 单击下一步按钮确认名称。您的虚拟设备将被创建,并出现在 Genymotion 主屏幕的列表中。根据需要创建多个虚拟设备。

  7. 要运行虚拟设备,请选择它,然后单击播放按钮。它将启动 Genymotion 模拟器,以便您可以与 Android Studio 一起使用。启动后,您可以解锁设备,使其准备好使用。

  8. 如果您再次点击 Android Studio 中的运行按钮,您会注意到正在运行的虚拟设备显示在选择设备对话框中的可用设备列表中。只需单击确定按钮,魔法就会开始。您的 Android 应用程序将在模拟器上启动。

它运行得又快又顺畅!相当酷,不是吗?

以下是 Genymotion 主屏幕的示例,列出了已创建的一些虚拟设备:

如何做...

还有更多...

Genymotion 配备了模拟的前端和/或后端摄像头,具体取决于所选择的配置。要启用它们,请单击相机图标。一个新的对话框出现,您可以在其中将滑块更改为打开,并为虚拟设备的前端和后端摄像头选择一个真实相机。

选择相机后,您可以关闭对话框。相机按钮旁边现在会出现一个绿色复选框。现在,每当应用程序需要使用相机时,它将使用所选的相机,我这里是笔记本电脑上的网络摄像头。要检查这是否有效,请在虚拟设备上选择相机应用程序。

Genymotion 的付费版本提供了额外的功能,包括模拟传感器,如 GPS 和加速计。如果愿意,您可以在www.genymotion.com/#!/store上查看差异。

请记住,虽然使用虚拟设备进行测试目的在 Genymotion 上非常出色,但始终重要的是在多个真实设备上进行测试。一些问题,特别是与内存管理相关的问题,稍后在本书中我们将看到,很容易在真实设备上重现,但在虚拟设备上可能会更难一些。

除此之外,真实设备更加像素完美,一些问题可能只会出现在特定设备上,因此在查看艺术品外观时,您将需要一些设备。

当您的应用程序几乎完成时,您可能会对 Testdroid 的(付费)服务感兴趣,这是一个基于云的服务,允许在许多真实设备上运行(自动化)测试。访问www.testdroid.com了解更多关于这项伟大服务的信息!

以下截图提供了一个示例,显示了在 Genymotion 上运行的虚拟 Nexus 5 设备上运行的 Hello Android Studio 应用程序:

还有更多...

重构您的代码

优秀的软件工程师会不断地重构他们的工作。方法和成员的名称应始终指示它们正在做什么。由于业务需求在开发过程中经常发生变化,特别是在采用敏捷方法时,您的应用程序也会发生变化。

如果您选择正确的名称,并遵守方法长度必须限制在最多一页滚动以查看整个方法的规则,通常您不需要许多注释来解释您的代码在做什么。如果很难为特定方法想出一个好的名称,那么它可能做得太多了。

由于更改名称可能令人恐惧,因为它可能破坏你的代码,开发人员通常选择不这样做。或者,他们决定以后再做。提前这样做可以节省几分钟。如果其他人查看你的代码,或者一年后再看你的代码,你的代码可能很难理解。查找方法的功能可能非常耗时。方法的描述性名称可以解决这个问题。

好消息是,使用 Android Studio,重构是轻松而相当容易的。只需高亮显示成员或方法的名称,右键单击它,然后从弹出的上下文菜单中选择重构项目。

在选择重构项目时出现的重构子菜单中,你会发现许多有趣的选项。在这里你将使用的一个选项,也是你将经常使用的选项是重命名…选项。

操作步骤…

以下步骤描述了如何在“重构”子菜单中重命名方法:

  1. 高亮显示您想要重命名的方法的名称。

  2. 从上下文菜单中选择重构

  3. 从子菜单中选择重命名 (或使用快捷键Shift + F6)。

  4. 现在,你可以就地重命名你的方法或成员,并通过按下Enter按钮应用更改。Android Studio 会为你提供一些建议,你可以接受这些建议,或者你可以输入你想要的名称。

提示

如果重复步骤 2 和 3,将会出现一个对话框,你可以在其中编辑名称。(或者使用快捷键Shift + F6两次)。

  1. 单击预览按钮,查看重命名的效果。

  2. 在屏幕底部,会出现一个新视图,显示重命名在每个文件(类、资源或其他)中的影响。

  3. 在该视图中单击执行重构按钮以应用所有更改。

以下截图显示了就地重构(重命名)的示例。

操作步骤…

它是如何工作的…

Android Studio 会负责在整个项目中重命名方法或成员以及对它的任何引用。这包括 Java 类、布局、可绘制对象,以及你能想到的任何其他东西。

重构菜单中还有许多其他有趣的选项可供使用。其中一些将在下一章的示例中讨论,它们将会派上用场。

现在,让我们继续下一章,构建一个真正的应用程序,好吗?

另请参阅

  • 有关重构代码的更多信息,请参阅第八章, 提高质量

第二章:具有基于云的后端的应用程序

本章将教您如何构建一个不需要自己的后端但使用基于云的解决方案的应用程序。

在本章中,您将学习以下食谱:

  • 设置 Parse

  • 从云端获取数据

  • 将数据提交到云端

介绍

许多应用程序需要后端解决方案,允许用户与服务器或彼此通信,例如在社交应用程序中,哪个应用程序今天不是社交应用程序呢?您还可以考虑业务应用程序,例如用于物流目的的应用程序。

当然,我们可以编写自己的 API,在某个地方托管它,并编写一些 Android 代码与之通信,包括查询、缓存和应用程序需要支持的所有其他功能。不幸的是,开发所有这些可能是一个非常耗时的过程,而且由于这通常是最有价值的资产,必须有另一种方法来做到这一点。

好消息是,您不必自己做所有这些事情。互联网上有几种现成的移动后端解决方案,例如 QuickBlox、Firebase、Google 应用引擎和 Parse 等,只是其中最知名的几种。

这些解决方案各自擅长特定事情;尽管如此,一个解决方案可能比另一个更合适。例如,以 QuickBlox 为例,它提供了设置事物的最快方式,但需要付出代价。它也不像其他选项那样灵活。Firebase,最近被 Google 收购,是一个非常好的解决方案,特别是如果您需要实时支持;例如,用于聊天应用程序。Parse,被 Facebook 收购,没有实时选项,但更灵活,并且有一些有趣的第三方集成可供选择。

当选择特定解决方案时,当然还有其他考虑因素。提供这种解决方案的各方(Facebook 和 Google)可能可以访问您存储在云中的数据,包括您的用户群,这不一定是坏事,但可能会对您选择的策略产生影响。还要考虑诸如可扩展性和数据锁定等问题,这两者都是奢侈问题,但当您的应用程序变得更受欢迎时,仍然可能成为问题。

Parse 是我最喜欢的,因为它目前是大多数用途最灵活的解决方案。它没有数据锁定(所有数据都可以导出),但它是可扩展的(如果您选择付费计划而不是免费计划),它适用于所有相关的移动平台,甚至允许我们创建云模块(在云中运行的方法,可以定期安排,并且/或者可以被应用程序访问)。在所有可用的热门服务中,这个服务提供了将后端附加到移动应用程序的最简单方法。

注意

将来可能会发生变化,特别是对于 Android 开发人员,如果 Google 应用引擎(顺便说一句,也可以用于 iOS 应用程序)与 Android Studio 的集成得到进一步改进。您已经可以在构建菜单中找到部署模块到应用引擎选项。

设置 Parse

想象一个这样的场景:在一个中心点,订单正在被收集并将被准备运输。商品需要被送达,客户收到他们订购的商品后需要在应用程序中签名。每个司机都有一个移动设备和一个应用程序来在数字化过程中支持这个过程。

这是我们将提供接下来的三个食谱的过程,我们将使用 Parse 进行,因为它是我们将要创建的解决方案最合适的后端。

即将介绍的食谱描述了如何设置 Parse,如何从 Parse 中获取数据到您的 Android 应用程序,以及如何发送数据,例如应用程序中的签名,到 Parse。

准备就绪

要完成这个食谱,您需要运行 Android Studio 并具有互联网访问权限。就是这样。

如何做...

让我们首先创建一个连接到 Parse 后端的应用程序,这样我们就有了一个可以构建应用程序的基础。让我们将应用程序命名为CloudOrder。接下来的步骤如下:

  1. 启动 Android Studio 并开始一个新的 Android Studio 项目。将应用程序命名为CloudOrder,并为公司域字段输入packtpub.com或适合您或您公司的任何其他名称。然后,单击下一步按钮。

  2. 选择手机和平板电脑选项,可选择更改最低 SDK字段。在我的情况下,这将是 API 14(Android 4.x),这在撰写时是尽可能覆盖尽可能多的受众并从我们需要的 SDK 功能中受益的最佳选择。确保至少将目标定位到 API 级别 9,因为 Parse 不支持低于此级别的级别。单击下一步按钮继续。

  3. 接下来,选择空白活动,然后单击下一步按钮。在下一页上,只需单击完成按钮。Android Studio 将为您设置新项目。

  4. 现在,让我们转到www.parse.com创建一个新帐户和一个应用程序。使用www.Parse.com注册。输入您的姓名、电子邮件地址和选择的密码,然后单击注册按钮。

  5. www.Parse.com上的下一页是开始页面。在显示有关您的应用程序名称的字段中输入CloudOrder或类似的内容。选择适合您情况的公司类型的值,并根据所选择的值完成任何其他字段。完成后,单击开始使用 Parse按钮。选择数据作为您要开始使用的产品。选择移动作为您的环境。接下来,选择一个平台。选择Android,在下一个视图中,选择本机(Java)选项。

  6. 选择现有项目选项。我们正在创建一个新项目;但是,为了知道这里发生了什么,我们将自己做以下事情。

  7. 现在,下载 SDK。在下载时,切换到 Android Studio,并将项目视图透视从Android更改为项目。然后,展开app文件夹。请注意,其中一个基础文件夹称为libs

  8. Parse-x.x.x.jar文件(其中x.x.x表示版本号)拖放到 Android Studio 中的libs文件夹中。如果出现非项目文件访问对话框,只需单击确定按钮。这样做后,Parse-x.x.x.jar将出现在libs文件夹下。

  9. 在第一章中,欢迎来到 Android Studio,我们需要告诉 Gradle 关于这个 Parse 库。双击打开apps文件夹中的build.gradle文件。在依赖项部分,我们需要添加两行,所以它看起来就像下面的例子一样。在已经存在的两行之后,添加boltsparse库的依赖项:

dependencies {
    compile 'com.android.support:appcompat-v7:22.0.0'
    compile 'com.parse.bolts:bolts-android:1.+'
    compile fileTree(dir: 'libs', include: 'Parse-*.jar')
}

注意

除了通过步骤 6 到 8 中描述的使用本地 JAR 文件,我们还可以使用类似于此的依赖项:

dependencies {
…
    compile 'com.parse:android:1.8.2'}
  1. AndroidManifest.xml文件中,添加访问互联网所需的权限。Manifest文件将位于/app/src/main文件夹中。双击打开它。按照下面的例子添加访问互联网和访问网络状态的权限。还要为包名+CloudOrderApplication应用程序定义名称:
<?xml version="1.0" encoding="utf-8"?>
<manifest 
    package="com.packtpub.cloudorder" >
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name= "android.permission.ACCESS_NETWORK_STATE" />
<application
    android:name="com.packtpub.cloudorder.CloudOrderApplication"
  1. 选择并展开src/main/java/com.packt.cloudorder文件夹。右键单击此文件夹。在弹出的上下文菜单中,选择新建,在子菜单中选择Java 类。在显示的对话框中,将CloudOrderApplication输入到名称字段中。然后,单击确定按钮。

  2. 使新类成为Application类的子类,并重写onCreate方法。在onCreate方法中,在super.OnCreate()之后,添加 Parse 的初始化,如 Parse 使用以下代码所示:

Parse.initialize(this, "your application Id", "your client Id");
  1. Android Studio 还不太满意。您会注意到 Android Studio IDE 中代码中的 Parse 部分被标记为红色。这是因为您的应用程序不知道这个类。每当您更改gradle文件时,您的项目都需要进行同步。要这样做,请单击带有工具提示“与 Gradle 文件同步项目”的按钮。您会在导航栏上找到这个按钮。或者,您也可以单击立即同步链接。如何做...

  2. 同步之后,Android Studio 将了解 Parse 类,但您仍然需要为此添加一个导入子句。如果您将鼠标悬停在代码中读取Parse的部分上,您会注意到 Android Studio 建议这可能是指com.parse.Parse。按下Alt + Enter接受此建议,或者自己添加import com.parse.Parse行。最后,您的类将如下所示:

package com.packt.cloudorder; 
import android.app.Application;
import com.parse.Parse;
public class CloudOrderApplication extends Application{
    @Override
    public void onCreate(){
        super.onCreate();
        Parse.enableLocalDatastore(this);
        Parse.initialize(this, "your application Id", "your client Id");
    }
}
  1. 我们几乎完成了配置基于 Parse 的应用程序。打开MainActivity文件,并将以下行添加到您的onCreate方法中:
ParseObject testObject = new ParseObject("CloudOrder");
testObject.put("customer", "Packt Publishing Ltd");
testObject.saveInBackground();
  1. 不要忘记添加适当的导入语句。运行您的应用程序。如果一切设置成功,CloudOrder类的新对象将被发送到 Parse 并在 Parse 中创建。

  2. 在 parse 网页上,点击导航栏顶部的Core按钮。查看网页左侧的Data部分。CloudOrder应该出现在那里,如果您点击它,您将看到包含您刚刚发送的属性(字段)的条目(行)。

这是www.Parse.com上的数据部分的样子:

如何做...

如果这个测试成功,删除你在MainActivityonCreate方法中添加的三行代码,因为我们不再需要它们了。

干得好!你刚刚创建了你的第一个 Parse 应用!让我们继续看看如何扩展CloudOrder应用程序!

它是如何工作的...

Parse SDK 将负责检索或发送数据。使用ParseObject类、Query和其他 Parse 类,所有数据通信都将自动进行。

还有更多...

www.parse.com上,您将找到有关缓存策略、将数据保存到云端和其他有趣功能的其他信息。

从云端获取数据

我们的基于 Parse 的应用程序已经启动运行。现在,让我们看看如何从 Parse 获取订单并在列表中显示它们。

准备工作

要完成本教程,您需要先前的教程正在运行,互联网访问以及一些咖啡,尽管我必须承认最后一个不是绝对必要的。茶也可以。

如何做...

让我们看看如何通过使用以下步骤从 Parse 后端获取订单并使用列表视图显示它们来扩展我们的CloudOrder应用程序:

  1. 设置 Parse步骤的最后一步中,我们正在查看新创建的 Parse 实体和其中的数据。实体可以像我们所做的那样在应用程序中即时创建或扩展,但我们也可以在网页上定义列并在这里添加数据。点击+Col按钮,将新列添加到CargoOrder实体中。

  2. 在模态中,显示添加列,从选择类型中选择字符串,并将新列命名为address。然后,点击创建列按钮。新列将被添加到已经可用的行中(您可能需要向右滚动以查看此内容)。

  3. 添加另一列。从类型下拉框中选择文件,并将此字段命名为signature。最后,添加一个带有数字类型和Status名称的最后一列。现在,我们为每个CargoOrder行添加了三个新的自定义列。

  4. 点击地址列并输入一个地址;例如,假设订单的送货地址应该是1600 Amphitheatre Pkwy, Mountain View, CA 94043, United States(这是谷歌总部的地址,但你当然可以在这里输入任何地址)。

  5. 点击+行按钮创建一个新的Cargo Order行,并为customeraddress字段输入其他值。重复几次以确保我们的应用程序中有一些数据可供使用。

  6. 要从CargoOrder条目中检索行,我们首先需要创建一个表示订单的模型。在MainActivityCloudOrderApplication类所在的位置创建一个新类。右键单击包名,选择新建Java 类。命名你的新类为CloudOrder,然后点击确定按钮。将你的模型设置为ParseObject类的子类,并指示该类映射到哪个实体。你的类应该是这样的:

package com.packt.cloudorder; 
import com.parse.ParseClassName;
import com.parse.ParseObject;
@ParseClassName("CloudOrder")
public class CloudOrder extends ParseObject {...
  1. 使用以下代码为我们在 Parse 中创建的列添加获取器和设置器:
public void setCustomer (String value) {
    put("customer", value);
}
public String getCustomer (){
    return getString("customer");
}
public void setAddress (String value) {
    put("address", value);
}
public String getAddress (){
    return getString("address");
}
  1. 现在,告诉 Parse 关于这个新类。在CloudOrderApplication类中,在Parse.Initialize行之前添加这一行:
ParseObject.registerSubclass(CloudOrder.class); 
  1. 为了在我们的应用程序中获取云订单,我们需要定义一个查询,指示我们究竟在寻找什么。在其最基本的形式中,查询看起来像以下代码片段。将其添加到MainActivityonCreate方法中:
ParseQuery<ParseObject> query = ParseQuery.getQuery("CloudOrder");
  1. 我们将使用findInBackground方法告诉 Parse 我们要异步执行这个查询。添加以下行来实现:
query.findInBackground(new FindCallback<ParseObject>() {
    public void done(List<ParseObject> items, ParseException e) {
        if (e==null){
            Log.i("TEST", String.format("%d objects found", items.size()));
        }
    }
});
  1. 运行应用程序并检查LogCat(使用快捷键Cmd + 6)。它会显示已找到的对象数量。这应该返回你在www.parse.comCargoOrder创建的行数。

  2. 太好了!现在,如果我们有一个适配器可以让这些项目在列表视图中可用就好了。创建一个新类,命名为CloudOrderAdapter。将其设置为CloudOrder类型的数组适配器子类:

public class CloudOrderAdapter extends ArrayAdapter<CloudOrder> { …
  1. 实现构造函数,创建一个视图持有者,并为所有需要被重写的方法添加实现。最终,你的适配器将是这样的:
public class CloudOrderAdapter extends ArrayAdapter<CloudOrder> {
    private Context mContext;
    private int mAdapterResourceId;
    public ArrayList<CloudOrder> mItems = null;
    static class ViewHolder{
        TextView customer;
        TextView address;
    }
    @Override	
    public int getCount(){
        super.getCount();
        int count = mItems !=null ? mItems.size() : 0;
        return count;
    }
    public CloudOrderAdapter (Context context, int adapterResourceId, ArrayList<CloudOrder>items) {
        super(context, adapterResourceId, items);
        this.mItems = items;
        this.mContext = context;
        this.mAdapterResourceId = adapterResourceId;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        View v = null;
        v = convertView;
        if (v == null){
            LayoutInflater vi = (LayoutInflater)this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            v = vi.inflate(mAdapterResourceId, null);
            ViewHolder holder = new ViewHolder();
            holder.customer = (TextView) v.findViewById(R.id.adapter_main_customer);
            holder.address = (TextView)v.findViewById(R.id.adapter_main_address);
            v.setTag(holder);
        }
        final CloudOrder item = mItems.get(position);
        if(item != null){
            final ViewHolder holder = (ViewHolder)v.getTag();
            holder.customer.setText(item.getCustomer());
            holder.address.setText(item.getAddress());
        }
        return v;
    }
}
  1. 返回MainActivity类,并修改查询回调的代码,以便我们可以在那里用结果来填充我们新创建的适配器,如下所示:
ParseQuery<ParseObject> query = ParseQuery.getQuery("CloudOrder");
query.findInBackground(new FindCallback<ParseObject>(){
    public void done(List<ParseObject> items, ParseException e) {
        Object result = items;
        if (e == null){
            ArrayList<CloudOrder> orders = (ArrayList<CloudOrder>) result;
            Log.i("TEST", String.format("%d objects found", orders.size()));
            CloudOrderAdapter adapter = new CloudOrderAdapter(getApplicationContext(), R.layout.adapter_main, orders);
            ListView listView = (ListView)findViewById(R.id.main_list_orders);
            listView.setAdapter(adapter);;
        }
    }
});
  1. 为了在我们的应用程序中显示订单,我们必须为其创建一个布局。展开layout文件夹,双击activity_main.xml文件以打开它。默认情况下,会显示布局的预览。通过点击 Android Studio 底部的Text选项卡,将布局显示为 XML。

  2. 删除显示Hello worldTextView小部件,并添加一个列表视图:

<ListView
android:id="@+id/main_list_orders"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
  1. 再次选择layout文件夹,右键单击它。从菜单中选择新建,然后从子菜单中选择布局资源。选择adapter_main作为文件名,然后点击确定按钮。将创建一个新的布局文件。将视角从设计更改为文本。

  2. 在布局中添加两个文本视图,以便我们可以显示客户姓名和地址,并添加一些格式,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:orientation="vertical" android:layout_width="match_parent"
    android:padding="8dp" android:layout_height="match_parent">
    <TextView
        android:text="(Customer)"
        android:textStyle="bold"
        android:textSize="20sp"
        android:textColor="@android:color/black"
        android:id="@+id/adapter_main_customer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:text="(Address)"
        android:textSize="16sp"
        android:textColor="@android:color/darker_gray"
        android:id="@+id/adapter_main_address"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
  1. 你已经完成了。运行你的应用程序。如果一切顺利,你会看到一个输出,就像下面的截图所示,这就是你从www.parse.com获取订单后列表视图的样子:操作步骤...

  2. 如果你遇到class exception error,再看一下第 8 步。你是否注册了ParseOrder子类?如果你遇到其他错误,请仔细重复每一步,检查是否有任何遗漏或不匹配的地方。

还有更多...

这个示例只是对 Parse 的简要介绍。在www.parse.com上,您将找到更多关于如何从云中检索数据的信息,包括在查询中使用whereorder by语句。它还为您提供了创建关系或复合查询所需的信息。

提交数据到云

现在我们已经完成了之前的示例,并且将使用我们的CloudOrder应用程序的司机知道去哪里获取特定订单,如果一旦货物交付,司机将能够选择该订单并要求客户在设备上签名。

在这个最后的示例中,我们将实现代码,让客户在设备上签名。签名将作为图像发送到 Parse,并且CloudOrder记录将被更新。

准备工作

要完成这个示例,您需要先运行之前的示例。

如何做…

  1. 创建一个新的类,命名为SignatureActivity

  2. 创建一个新的布局,命名为activity_signature.xml

  3. 切换布局为文本。将TextViewButton小部件添加到布局中。确保布局看起来像这样:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:orientation="vertical" android:layout_width="match_parent"
    android:padding="8dp" android:layout_height="match_parent">
    <TextView
        android:id="@+id/signature_text"
        android:text=" Please sign here:"
        android:textSize="24sp"
        android:textColor="@android:color/black"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/signature_button"
        android:text="Send signature"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
  1. 为了让客户签名,我们需要创建一个自定义小部件。

  2. com.packt.cloudorder包的下面,创建一个新包,命名为widget

  3. 在这个新包中,创建一个新类,命名为SignatureView

  4. 使SignatureView类从View类继承,并覆盖onDraw方法,以在屏幕上放置手指或触控笔时绘制路径。覆盖onTouch方法以创建路径。创建路径的代码段如下所示:

package com.packt.cloudorder.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public class SignatureView extends View {
    private Paint paint = new Paint();
    private Path path = new Path();
    public SignatureView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint.setAntiAlias(true);
        paint.setStrokeWidth(3f);
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeJoin(Paint.Join.ROUND);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawPath(path, paint);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float eventX = event.getX();
        float eventY = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                path.moveTo(eventX, eventY);
                return true;
            case MotionEvent.ACTION_MOVE:
                path.lineTo(eventX, eventY);
                break;
            case MotionEvent.ACTION_UP: 
                break;
            default:
                return false;
        }
        invalidate();
        return true;
    } 
  1. getSignatureBitmap方法添加到SignatureView类中,以便我们可以从Signature view小部件获取签名作为位图:
public Bitmap getSignatureBitmap() {
        Bitmap result = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Drawable bgDrawable =getBackground();
        if (bgDrawable!=null) {
            bgDrawable.draw(canvas);
        }else {
            canvas.drawColor(Color.WHITE);
            draw(canvas);
        }
        return result;
    }
} 
  1. 返回signature_activity布局,并在文本视图和按钮之间添加签名视图:
<com.packt.cloudorder.widget.SignatureView
    android:id="@+id/signature_view"
    android:layout_width="match_parent"
	android:layout_height="200dp"
	android:layout_marginLeft="3dp"
	android:layout_marginTop="3dp"
	android:layout_marginRight="0dp"
	android:layout_marginBottom="18dp"/>
  1. 构建项目。它应该消除任何渲染问题。

  2. 实现SignatureActivity类。首先,将其设置为Activity的子类,并覆盖onCreate方法。将内容视图设置为我们刚刚创建的布局,并在布局中的按钮上添加一个onClick实现,如下所示:

public class SignatureActivity  extends Activity {
    @Override
	protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_signature);
        findViewById(R.id.signature_button).setOnClickListener(new View.OnClickListener(){
            @Override
			public void onClick(View v) {
            }
        });
    }
}
  1. MainActivity声明之后,将活动添加到清单文件中,如下所示:
<activity android:name=".SignatureActivity"/>
  1. 如果司机选择了任何订单,我们需要显示签名活动,然后需要知道选择了哪个订单。转到MainActivity类,并在OnCreate方法的末尾,在Query.findInBackground调用之后,添加OnItemClickListener到列表视图上:
((ListView)findViewById(R.id.main_list_orders)).setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
	public void onItemClick(AdapterView<?> parent, View view, int position, long id) {        }
});
  1. onItemClick事件中,使用以下代码段找出选择了哪个订单:
ListView listView = (ListView)findViewById(R.id.main_list_orders);
CloudOrder order = (CloudOrder)listView.getAdapter().getItem(position);
gotoSignatureActivity(order);
  1. gotoSignatureActivity方法中,我们希望使用意图启动Signature活动,并将从MainActivity传递到SignatureActivity的选择订单,使用如下所示的捆绑:
private void gotoSignatureActivity(CloudOrder order){
    Intent intent = new Intent(this, SignatureActivity.class);
    Bundle extras = new Bundle();
    extras.putString("orderId", order.getObjectId());
    intent.putExtras(extras);
    this.startActivity(intent);
}
  1. SignatureActivity类中,将以下内容添加到按钮的OnClick实现中:
sendSignature();  
  1. 对于sendSignature方法的实现,我们将创建一个新的ParseFile对象,并将来自签名视图的位图数据传递给它。我们将使用saveInBackground方法将文件发送到 Parse:
private void sendSignature() {
    final Activity activity = this; 
    SignatureView signatureView = (SignatureView)findViewById(R.id.signature_view); 
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    signatureView.getSignatureBitmap().compress(Bitmap.CompressFormat.PNG, 100, stream);
    byte[] data = stream.toByteArray();
    final ParseFile file = new ParseFile("signature.jpg", data); 
    file.saveInBackground(new SaveCallback() {
        @Override
		public void done(com.parse.ParseException e) {
        }
    });
}
  1. 保存完成后,我们希望更新订单的信息,例如我们创建的文件和状态,例如10,这可能表示订单已完成或类似的状态。这里实际的值并不重要。

  2. 如果在保存过程中没有发生错误,我们使用ParseObject类的createWithoutData方法,这样我们就可以传递正确的对象 ID 和我们想要更新的字段。我们也会保存这些更改,以便在 Parse 上更新记录。(为了简单起见,我们使用这种方法;尽管我们也可以使用CloudOrder对象来完成相同的事情)完成回调的实现如下:

if (e == null) {
 Bundle extras = getIntent().getExtras();
ParseObject order = ParseObject.createWithoutData("CloudOrder", extras.getString("orderId"));
                order.put("signature", file);
                order.put("status", 10);
order.saveInBackground(new SaveCallback() {
                    @Override
                    public void done(ParseException e) {
                        if (e==null){
                            Toast.makeText(activity, "Signature has been sent!", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
  1. 运行应用程序,选择一个订单,签名,然后单击发送签名按钮。如果一切顺利,将显示一个 toast,指示签名已发送。

这是顾客签名后签名的样子:

操作步骤…

  1. 自己去www.parse.com看看。刷新Cloud order的视图。注意,在应用程序中选择的订单中,signature.jpg文件出现在签名列中。双击它以查看其内容。在向其提交签名图像后,您的数据行可能如下所示:操作步骤…

实际上,您应该使用字符串资源而不是硬编码的值。通过重用字符串资源(或常量值),不仅可以用于类和字段名称,还可以用于其他文本,从而减少由拼写错误引起的错误数量。这将提高您的应用程序的质量。它也将使以后本地化应用程序变得更加容易。(在最后三章中,我们将更多地关注这些内容,但现在就开始使用这些好的实践。)以下步骤使用了字符串资源:

  1. 查看strings.xml文件。它位于res/values文件夹中。想象一下,如果我们在步骤 19 中显示的 toast 中包含了文本。您的strings.xml文件可能如下所示:
<?xml version="1.0" encoding="utf-8"?>
<resources>
…<string name="app_name">Cloud order</string><string name="parse_class_cargo_order">CargoOrder</string>
    <string name="signature_send">Your signature has been sent.</string>
  1. 在您的代码中,您可以使用getString方法引用字符串资源。例如,您可以用字符串引用替换步骤 19 中显示的 toast 的硬编码字符串,如下所示:
Toast.makeText(activity, getString(R.string.signature_send), Toast.LENGTH_SHORT).show();
  1. 在您的布局文件中,您也可以引用这个字符串资源,例如,在一个文本视图中:
<TextView
    android:text="@string/signature_send"
	android:layout_width="wrap_content"
	android:layout_height="match_parent" />

我们将在以后深入介绍如何使用字符串、颜色、尺寸和其他类型的资源,但您可以通过用字符串资源引用替换本教程中的所有硬编码字符串,或在适用的情况下使用常量值来熟悉这些概念。

通过实现这个步骤,我们已经完成了我们的CloudOrder应用程序。随意进行进一步的定制,并在需要的地方进行增强。

工作原理...

自定义小部件在视图上绘制路径,然后将创建一个位图。使用ParseFile对象,位图数据将被发送到 Parse(然后将文件存储在 Amazon 并保留对文件的引用)。

如果成功,我们将更新适用于签名的CloudOrder行,指明signature列中的图像指向哪个文件。

还有更多...

请查看www.parse.com上的文档。那里有一些有趣的功能可用,包括saveEventually方法和云代码选项。

如果没有可用的互联网连接,saveEventually方法将在本地存储更新,这对于移动应用程序是常见的情况。一旦恢复了互联网连接,这个方法将开始发送已排队等待发送到云端的数据。这个选项将为您节省大量麻烦和时间。

还要查看其他功能,比如云代码和各种可用的第三方集成,比如 Twilio,如果您想发送文本或语音消息(这对于入职流程中的确认目的可能很方便),以及 SendGrid,这是一个用于电子邮件传递的工具。

在本章的示例中,我们只需付出少许努力就实现了一些非常有趣的功能,这真的很棒!然而,该应用程序目前还不够吸引人。通过应用下一章将解释的材料设计概念,我们可以使应用程序看起来更加出色,并且更加直观易用。

另请参阅

  • 有关更多信息,请参阅第三章 材料设计

第三章:Material Design

这一章将教你什么是 Material Design,为什么它是一个很大的改进,以及为什么你应该在你的应用中使用它。

在这一章中,你将学到:

  • 回收视图和卡片视图

  • 涟漪和高程

  • 出色的过渡

介绍

随着 Material Design 的引入,Android 应用的外观终于成熟了。它们可以与 iOS 设计很好地竞争。Android Material 应用具有扁平设计,但也有一些有趣的区别,比如高程。例如考虑下面的图:

Introduction

把它想象成多张纸片。它是基于,嗯,材料的。每张纸片都有特定的高程。所以,环境实际上是一个有光和阴影等效果的 3D 世界。任何动作都应该具有真实世界的行为,就好像移动的元素是真实的物体一样。动画是 Material Design 的另一个重要元素。

首先看一下www.google.co.in/design/spec/material-design/introduction.html来了解 Material Design 的全部内容。当然,对设计师来说有很多有趣的东西,而你可能只对所有这些美丽的东西的实现感兴趣;然而,这个链接为你提供了更多关于 Material Design 的背景信息。

长时间以来,大多数 Android 应用都受到糟糕的设计的困扰,或者在早期根本没有设计。或者,它们看起来与为 iPhone 制作的应用非常相似,包括所有 iOS 典型的元素。

看一下下一个应用的截图:

Introduction

使用 Material Design,这就是现在大多数谷歌应用的外观。

现在许多谷歌的 Android 应用都使用 Material Design。它们都遵循相同的交互和设计准则。界面是极简主义的,正如人们对谷歌所期望的那样。此外,界面变得更加统一,使得更容易理解和使用。

以前,响应性是你自己要注意的事情。Material Design 带来了涟漪和其他效果,做着同样的事情,即提供用户输入的反馈,但它更容易实现,更加优雅。

至于组件,Material Design 规定了例如特定情况下按钮应该是什么样子。想想用于操作的浮动按钮,或者用于对话框中的扁平按钮。它还用RecyclerView替换了ListView,这样可以更灵活地显示列表。CardViews是常见的元素,你可以经常在谷歌应用中看到它们的使用。各种动画提供了更自然的过渡,比如用于导航或滚动目的的动画。

Material Design 不仅适用于最新和最好的。虽然它随 Android Lollipop(5.0)和更高版本一起发布,但大多数 Material Design 功能可以通过v7 支持库在 Android 2.1 及更高版本中使用,这使我们能够应用 Material Design 并仍然支持几乎所有的 Android 设备。

总的来说,Material Design 为你的应用美化提供了很多。人们也想变得更美丽。健康应用因此而蓬勃发展。找出健康的饮食,建议多喝水,以及建议跑步或健身锻炼是这类应用的常见目标。为了展示 Material Design 的美丽,我们将创建一个可以帮助人们变得更健康的应用。

那么,喝水并自拍应用怎么样?人们需要更经常地喝水,如果他们这样做,他们就能看到效果。美丽的人们应该有一个美丽的应用。这是有道理的,不是吗?

回收视图和卡片视图

RecyclerView取代了传统的列表视图。它提供了更多的灵活性,可以以网格形式或水平或垂直项目的形式显示列表的元素。现在,我们可以选择在合适的地方显示卡片,而不是行。

在我们的应用中,每个卡片应该显示有关条目的一些文本和我们拍摄的图片的缩略图。这就是本教程的全部内容。

准备工作

要完成本教程,您需要运行 Android。还要确保您已安装了最新的 SDK。(您可以通过打开 SDK 管理器来检查是否安装了最新的 SDK)。为此,打开工具菜单,选择Android,然后选择SDK 管理器选项。

如何做...

让我们使用以下步骤来调查如何使用recycler view和卡片:

  1. 启动 Android Studio 并开始一个新项目。将应用程序命名为WaterApp,并在公司域字段中输入packtpub.com。然后,点击下一步按钮。

  2. 在下一个对话框中选择空白活动,然后点击下一步按钮。

  3. 在下一个对话框中,点击完成按钮。

  4. app文件夹中的build.gradle文件中,如下所示,在dependencies部分添加recycler view的依赖项:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.1.1'
    compile 'com.android.support:recyclerview-v7:+'
}
  1. build.gradle文件中将minSdkVersion更改为至少21

注意

这不一定是最小所需的版本,但由于用于向后兼容目的的支持库不包含所有的 Material 设计功能,我选择在这里选择 API 级别 21,以确保安全。

  1. 通过单击build.gradle文件编辑后出现的黄色条上的立即同步标签来同步您的项目,或者如果没有出现,请单击工具栏上的同步项目与 Gradle 文件按钮。

  2. 打开activity_main.xml布局文件,删除Hello World TextView,并向布局中添加一个RecyclerView标签,如下所示:

<android.support.v7.widget.RecyclerView
    android:id="@+id/main_recycler_view"
    android:scrollbars="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
  1. 在您的MainActivity类中,在setContentView之后的onCreate方法中添加以下内容:
RecyclerView recyclerView = (RecyclerView) 
 findViewById(R.id.main_recycler_view);
  1. RecyclerView类还不是一个已知的类。使用Alt + Enter快捷键添加正确的导入语句,或者自己添加以下行:
import android.support.v7.widget.RecyclerView;
  1. 我们将在这个教程中使用线性布局管理器。在第 9 步中添加的行后添加以下行:
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
  1. 创建一个新的包并将其命名为models,在该包中创建一个新的Drink类,如下所示:
package com.packt.waterapp.models;import java.util.Date;
public class Drink {
    public Date dateAndTime;
    public String comments;
    public String imageUri;
}

这里,Date类指的是java.util.Date包(这是指定的,因为还有一个同名的与 SQL 相关的类)。

  1. 让我们创建一个布局来显示这些项目。在项目树中的layout包上右键单击,创建一个新的资源文件。为此,从菜单中选择新建新建布局资源文件。将其命名为adapter_main.xml,然后点击确定按钮。

  2. 将布局切换到文本模式,将LinearLayout的方向从垂直改为水平,为其添加一些填充,并向其添加一个图像视图,如下面的代码片段所示。我们还将添加一个默认图像,以便我们有东西可以查看:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
android:orientation="horizontal" android:layout_width="match_parent"
android:padding="8dp" android:layout_height="match_parent">
<ImageView android:id="@+id/main_image_view"
android:src="img/ic_menu_camera"
android:scaleType="center"
android:layout_width="90dp"
android:layout_height="90dp" />
</LinearLayout>
  1. 在图像旁边,我们想要使用两个TextView小部件显示日期和时间以及评论,这两个小部件包裹在另一个LinearLayout小部件中。在ImageView标签之后添加这些:
<LinearLayoutandroid:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:id="@+id/main_date_time_textview"
		android:layout_marginTop="8dp"
		android:textSize="12sp"
		android:textColor="@color/material_blue_grey_800"
		android:layout_width="match_parent"
		android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/main_comment_textview"
		android:layout_marginTop="16dp"
		android:maxLines="3"
		android:textSize="16sp"
		android:textColor="@color/material_deep_teal_500"
		android:layout_width="match_parent"
		android:layout_height="wrap_content" />
	</LinearLayout>
  1. 创建另一个包并将其命名为adapters。在该包中创建MainAdapter类,该类将使用ViewHolder类,帮助我们将数据显示在我们想要的位置。我们还包括所有需要被重写的方法,比如onBindViewHolder方法和getItemCount方法:
public class MainAdapter extends RecyclerView.Adapter<MainAdapter.ViewHolder> {
    private ArrayList<Drink> mDrinks;private Context mContext;public static class ViewHolder extends        RecyclerView.ViewHolder {
        public TextView mCommentTextView;
        public TextView mDateTimeTextView;
        public ImageView mImageView;
        public ViewHolder(View v) {
            super(v);
        }
    }
    public MainAdapter(Context context, 
      ArrayList<Drink> drinks) {
        mDrinks = drinks;
        mContext = context;
    }
    @Override
    public MainAdapter.ViewHolder  
     onCreateViewHolder(ViewGroup parent,  int viewType) {
        View v = LayoutInflater.from(
         parent.getContext()).inflate(
          R.layout.adapter_main, parent, false);
        ViewHolder viewHolder = new ViewHolder(v);
        viewHolder.mDateTimeTextView =  
         (TextView)v.findViewById(
          R.id.main_date_time_textview);
        viewHolder.mCommentTextView =  
         (TextView)v.findViewById(
          R.id.main_comment_textview);
        viewHolder.mImageView = 
         (ImageView)v.findViewById(
          R.id.main_image_view);
        return viewHolder;
    }
    @Override
    public int getItemCount() {
        return mDrinks.size();
    }
}
  1. 我们还有更多的事情要做。添加onBindViewHolder方法,并添加实现以将数据实际绑定到正确的小部件上:
@Override
public void onBindViewHolder(ViewHolder holder,int position) {
    Drink currentDrink = mDrinks.get(position);
    holder.mCommentTextView.setText(
     currentDrink.comments);
    holder.mDateTimeTextView.setText(
     currentDrink.dateAndTime.toString());
    if (currentDrink.imageUri != null){
        holder.mImageView.setImageURI(
         Uri.parse(currentDrink.imageUri));
    }
}
  1. MainActivity文件中,我们需要有一个适配器的实例和一些要显示的数据。添加一个私有适配器和一个包含Drink项目的私有数组列表:
private MainAdapter mAdapter;private ArrayList<Drink> mDrinks;
  1. onCreate方法的末尾,告诉recyclerView使用哪个适配器,并告诉适配器使用哪个数据集:
mAdapter = new MainAdapter(this, mDrinks);
recyclerView.setAdapter(mAdapter);

  1. MainActivity文件中,我们想添加一些虚拟数据,以便我们对事情将会是什么样子有一些想法。在我们创建MainAdapter类之前的onCreate方法中添加以下内容:
mDrinks = new ArrayList<Drink>();
Drink firstDrink = new Drink();
firstDrink.comments = "I like water with bubbles most of the time...";
firstDrink.dateAndTime = new Date();
mDrinks.add(firstDrink);Drink secondDrink = new Drink();
secondDrink.comments = "I also like water without bubbles. It depends on my mood I guess ;-)";
secondDrink.dateAndTime = new Date();
mDrinks.add(secondDrink);

使用Alt + enter快捷键导入所需的包。

运行您的应用程序以验证到目前为止一切都进行得很顺利。您的应用程序将显示两个包含我们在上一步中创建的示例数据的条目。

使用卡片视图

应用程序看起来还可以,但我不想称其为美丽。让我们看看是否可以稍微改进一下。以下步骤将帮助我们使用卡片视图创建应用程序:

  1. app文件夹中的build.gradle文件中添加一个CardView依赖项,就在对recycler view的依赖项之后:
compile 'com.android.support:cardview-v7:+'

然后再次同步您的项目。

注意

顺便说一句,如果这个应用是真的,那么避免不愉快的惊喜,通过指定确切的版本而不是在版本号中使用+号来解决任何依赖项可能存在的问题。目前,对于这个特定的依赖项,版本号是21.0.0,但在您阅读本文时,可能已经有了新版本。

  1. 如果出现错误提示 Gradle 无法解析卡片视图依赖项,则单击安装存储库并同步项目链接,接受许可证,并单击下一步按钮。等待一段时间,直到下载完成并安装完成。完成后,单击完成按钮。再次同步您的项目。

  2. 创建一个新的布局并命名为adapter_main_card_view.xml。在LinearLayout标签中添加一些填充,在LinearLayout标签内部添加一个CardView

<?xml version="1.0" encoding="utf-8"?><LinearLayout 
    android:orientation="vertical"   
    android:layout_width="match_parent"
    android:padding="4dp"  
    android:layout_height="match_parent">
    <android.support.v7.widget.CardView
        xmlns:card_view=
        "http://schemas.android.com/apk/res-auto"android:id="@+id/card_view"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"card_view:cardCornerRadius="4dp"></android.support.v7.widget.CardView>
</LinearLayout>
  1. 从先前的布局adapter_main.xml文件中,复制ImageView和两个TextView小部件(但不包括包含这两个TextView小部件的LinearLayout),并将它们粘贴到您已添加到adapter_main_card_view.xml文件中的CardView中。

  2. 因为CardView的行为就像FrameLayout,所以您需要为文本标签设置边距。为两个文本视图添加左边距。还修改TextView评论的顶部边距:

<TextView
    android:id="@+id/main_date_time_textview"
	android:layout_marginTop="8dp"
	android:layout_marginLeft="100dp"
	android:textSize="12sp"
	android:textColor="@color/material_blue_grey_800"
	android:layout_width="match_parent"
	android:layout_height="wrap_content" />
<TextView
    android:id="@+id/main_comment_textview"
	android:layout_marginTop="32dp"
	android:layout_marginLeft="100dp"
	android:maxLines="3"
	android:textSize="16sp"
	android:textColor="@color/material_deep_teal_500"
	android:layout_width="match_parent"
	android:layout_height="wrap_content" />
  1. 现在,通过更改onCreateViewHolder方法中的布局 ID,告诉MainAdapter类使用这个布局:
View v = LayoutInflater.from(parent.getContext()). inflate(R.layout.adapter_main_card_view, parent, false);

再次运行应用程序,我们将看到这次它会是什么样子:

使用卡片视图

  1. 在下一个教程中,我们将添加一个提升的浮动按钮,并创建一个新的活动,允许我们的应用程序的用户添加饮料、评论和自拍。

还有更多...

有很多关于材料设计的文档。浏览各种网站上提供的各种示例,比如www.materialup.commaterialdesignblog.commaterial-design.tumblr.com

或者,下载一些在 Play 商店中可用的材料设计应用程序,例如 Inbox、Google+、Wunderlist、Evernote、LocalCast 和 SoundCast 应用程序。

涟漪和高程

尽管高程和涟漪并不一定会使人们更加美丽,但将这些和其他材料设计原则应用到我们的应用程序中肯定会有助于美化它。

在上一个教程中,我们创建了一个列表来显示所有已登录的饮料。在这个教程中,我们将添加一个提升的按钮来添加新条目。此外,我们将创建一个新的活动。

对于每个条目,用户可以描述一些关于他喝的东西的想法。当然,用户必须能够每次都自拍,以便以后他可以检查喝那么多水或绿茶(或者啤酒)是否确实对他的健康和外貌产生了积极的影响。

准备工作

对于这个指南,如果您已经完成了上一个指南,那将是很好的,因为这将建立在我们以前的成就之上。

如何做...

让我们添加一个浮动按钮,并创建一个新的活动来编辑新条目:

  1. res/drawable文件夹中添加一个新的可绘制资源文件,命名为button_round_teal_bg.xml,然后点击OK按钮。

  2. 使用 XML,我们将为按钮创建一个圆形椭圆形状。首先删除选择器标签(如果有)。将其包装在ripple标签中。ripple在按钮被按下时提供可见反馈;我选择了一种蓝绿色的材料设计变体作为颜色,但您当然可以选择任何您喜欢的颜色。作为灵感,您可以查看www.google.com/design/spec/style/color.html。文件的内容如下例所示:

<ripple android:color="#009789">
    <item>
        <shape android:shape="oval">
            <solid android:color="?android:colorAccent"/>
        </shape>
    </item>
</ripple>

提示

如果遇到任何错误,请检查build.gradle文件中的minSdkVersion。有关更多信息,请参考第一条指南的第 5 步。

  1. activity_main.xml布局文件中的循环视图后添加一个按钮:
<ImageButton
    android:id="@+id/main_button_add"
	android:elevation="1dp"
	android:layout_width="48dp"
    android:layout_height="48dp"
    android:layout_alignParentBottom="true"
    android:layout_alignParentRight="true"
    android:layout_margin="16dp"
    android:tint="@android:color/white"
    android:background="@drawable/button_round_teal_bg"
    android:src="img/ic_input_add"/>

注意

颜色应该在单独的颜色资源文件中定义。此外,高程和边距应该放在尺寸资源文件中。由于这超出了本指南的范围,我建议您稍后再做这些。

  1. 接下来,我们希望有一些阴影,还希望在按钮被按下或释放时改变高程。在res文件夹中创建一个新的目录,命名为anim。在此文件夹中,创建一个新的动画资源文件。将文件命名为button_elevation.xml,然后点击OK按钮:
<selector >
    <item android:state_pressed="true">
        <objectAnimator
            android:propertyName="translationZ"android:duration="@android:integer/config_shortAnimTime"
            android:valueFrom="1dp"
            android:valueTo="4dp"android:valueType="floatType"/></item>
    <item>
        <objectAnimator
            android:propertyName="translationZ"android:duration="@android:integer/config_shortAnimTime"
            android:valueFrom="4dp"
            android:valueTo="1dp"
            android:valueType="floatType"/>
    </item>
</selector>
  1. 通知图像按钮有关这个新的资源文件。在您的activity_main.xml布局中,为图像按钮添加以下行:
android:stateListAnimator="@anim/button_elevation"
  1. 在 MainActivity 类的 onCreate 方法末尾,为我们刚刚创建的按钮添加一个OnClickListener,并调用showEntry方法,我们将在一两分钟内创建:
findViewById(R.id.main_button_add).setOnClickListener(new  
 View.OnClickListener() {
    @Override
    public void onClick(View v) {
        showEntry();}
});
  1. 创建一个新的布局资源文件,命名为activity_entry.xml,并将FrameLayout用作根元素。然后点击OK按钮。

  2. 为评论添加一个EditText小部件,一个拍照按钮和另一个保存条目的按钮。然后将这些元素包装在CardView小部件中。在CardView小部件之后添加一个ImageView小部件,就像这样:

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android= 
 "http://schemas.android.com/apk/res/android"
    android:padding="8dp" android:layout_width="match_parent"   
    android:layout_height="match_parent">
    <android.support.v7.widget.CardView 
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        card_view:cardCornerRadius="4dp">
    <EditText                                                                                                  android:id="@+id/entry_edit_text_comment"android:lines="6"android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginRight="60dp"/>
    <ImageButton 
	    android:id="@+id/entry_image_button_camera"
        android:src="img/ic_menu_camera"
        android:layout_gravity="right"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button 
	    android:id="@+id/entry_button_add"
        android:layout_gravity="bottom"
        android:text="Add entry"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    </android.support.v7.widget.CardView>
    <ImageView
        android:id="@+id/entry_image_view_preview"
        android:scaleType="fitCenter"
        android:layout_marginTop="210dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>
  1. 创建一个新的类,命名为EntryActivity,然后点击OK按钮。

  2. 使您的类从Activity继承,重写onCreate方法,并将内容视图设置为您刚刚创建的布局:

public class EntryActivity extends Activity {
    @Override
	protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_entry);
    }
}
  1. 不要忘记在AndroidManifest.xml文件中添加新的活动:
<activity android:name=".EntryActivity"/>
  1. MainActivity类中,添加showEntry方法和显示新活动所需的实现。我们将在这里使用startActivityForResult方法,因为这将允许EntryActivity稍后返回数据:
private int REQUEST_NEW_ENTRY = 1;
private void showEntry(){
    Intent intent = new Intent(this, EntryActivity.class);
    startActivityForResult(intent, REQUEST_NEW_ENTRY);
}

现在,如果您运行应用程序并按下按钮,您将注意到视觉反馈。为了正确看到效果,您可能需要使用触控笔或放大按钮的大小。如果您释放按钮,您将看到条目布局。在布局中,如果您按住添加条目按钮(或相机按钮),您将注意到涟漪效果。我们不必为此做任何特殊处理。随着 Lollipop 的推出(以及之前的描述),这是按钮的默认行为。但是,这些按钮看起来有点无聊,就像您在浮动按钮中看到的那样,有很多自定义选项可用。让我们按照下一步操作:

  1. EntryActivity类中,为相机按钮设置OnClickListener,并对add按钮执行相同的操作:
findViewById(R.id.entry_image_button_camera).setOnClickListener( 
 new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        takePicture();
    }
});
findViewById(R.id.entry_button_add).setOnClickListener(new 
 View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
}
);
  1. 添加一个私有成员,用于包含我们将要拍摄的照片的 URI:
private Uri mUri;
  1. 创建一个takePicture方法并为其添加实现。我们将使用时间戳提前创建一个带有唯一图像名称的文件,并告诉图像捕获意图使用Uri来访问该文件:
private int REQUEST_IMAGE_CAPTURE = 1;
private void takePicture(){
    File  filePhoto = new  
    File(Environment.getExternalStorageDirectory(),String.valueOf(new Date().getTime())+"selfie.jpg");
    mUri = Uri.fromFile(filePhoto);
    Intent intent = new   
     Intent("android.media.action.IMAGE_CAPTURE");
    intent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);
    startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
  1. 重写onActivityResult方法,一旦拍照就会触发。如果一切顺利,我们需要创建刚刚拍摄的文件的位图,并显示其预览:
@Override
    protected void onActivityResult(int requestCode, int resultCode,Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_IMAGE_CAPTURE &&
        resultCode == RESULT_OK){
        Bitmap bitmap = getBitmapFromUri();
        ImageView preview = (ImageView)  
          findViewById(R.id.entry_image_view_preview);
        preview.setImageBitmap(bitmap);}
}
  1. 接下来,实现getBitmapFromUri方法:
public Bitmap getBitmapFromUri() {
    getContentResolver().notifyChange(mUri, null);
    ContentResolver resolver = getContentResolver();
    Bitmap bitmap;
    try {
        bitmap = android.provider.MediaStore.Images.Media.getBitmap(  
         resolver, mUri);
        return bitmap;
    } 
    catch (Exception e) {
        Toast.makeText(this, e.getMessage(),  
         Toast.LENGTH_SHORT).show();
       return null;}
}
  1. AndroidManifest.xml文件中添加适当的权限和功能:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission  
  android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.hardware.camera" />
  1. 现在让我们实现submitEntry方法。我们将返回评论和图片的uri,然后结束活动:
private void submitEntry(){
    EditText editComment =  (EditText)
      findViewById(R.id.entry_edit_text_comment);
    Intent intent = new Intent();
    intent.putExtra("comments", editComment.getText().toString());
    if (mUri != null) {
        intent.putExtra("uri", "file://" +   
          mUri.getPath().toString());}
    setResult(Activity.RESULT_OK, intent);
    finish();
}
  1. add按钮的onClick事件添加实现。只需调用submitEntry方法:
findViewById(R.id.entry_button_add).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        submitEntry();
    }
});
  1. MainActivity类中,我们将通过重写onActivityResult方法来处理返回的结果。将创建一个新的饮料并添加到饮料列表中。最后,我们将通过添加以下片段通知适配器需要显示更新:
@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_NEW_ENTRY && 
        resultCode == RESULT_OK) {
        Bundle bundle = data.getExtras();
        Drink newDrink = new Drink();
        newDrink.comments = bundle.getString("comments");
        newDrink.imageUri = bundle.getString("uri");
        newDrink.dateAndTime = new Date();
        mDrinks.add(newDrink);
        mAdapter.notifyDataSetChanged();
}
  1. MainAdapter类中,我们需要做一些工作来显示每个图像的缩略图。将以下内容添加到onBindViewHolder方法的末尾:
if (currentDrink.imageUri != null){
    Bitmap bitmap =    
     getBitmapFromUri(Uri.parse(currentDrink.imageUri));
    holder.mImageView.setImageBitmap(bitmap);
}
  1. 如果已知项目的Uri,我们需要为其显示缩略图。我们将在MainAdapter中以稍有不同的方式实现getBitmapFromUri。方法如下:
public Bitmap getBitmapFromUri(Uri uri) {
    mContext.getContentResolver().notifyChange(uri, null);
    ContentResolver cr = mContext.getContentResolver();
    try {
       Bitmap bitmap =   
android.provider.MediaStore.Images.Media.getBitmap(cr, uri);
       return bitmap;
    }
    catch (Exception e) {
        Toast.makeText(mContext, e.getMessage(),  
         Toast.LENGTH_SHORT).show();
        return null;
    }
}

现在,运行应用程序。您可以使用真实设备或 Genymotion。如果您使用 Genymotion,您需要启用相机,如第一章欢迎使用 Android Studio中所述。单击添加按钮,喝一杯水,输入一些评论,然后自拍。点击添加条目按钮,使其出现在列表中。

太棒了!您现在已经完成了。该应用程序远非完美,但我们已经做出了一些有趣的举措。美化需要时间。在下一个示例中,我们将通过添加过渡来实现一些令人惊叹的东西。

注意

在某些设备上,但不是所有设备,图片可能会被旋转。这是 Android 开发面临的挑战之一,我们将在第六章捕获和分享中涵盖这个主题。

还有更多…

除了在应用程序的生命周期内,条目列表尚未持久化。如果需要,可以通过将条目存储在 SQLite 数据库中或最终使用 Parse 来使条目持久化,这在第二章具有基于云的后端的应用程序中讨论。由于持久性不是本示例的目标,这里不会进一步讨论。在第七章内容提供程序和观察者中,将讨论 SQLite 和内容提供程序。

注意

自 API 级别 23 以来,有一个可用的 FloatingActionButton 小部件。它有两种大小:默认和迷你。

另请参阅

  • 第二章具有基于云的后端的应用程序

  • 第六章捕获和分享

  • 第七章内容提供程序和观察者

出色的过渡

如果单击任何卡片,它将再次显示条目视图,其中包括评论和我们之前拍摄的图片的预览。

我们不仅希望从列表视图转到详细视图。Material design 还负责出色的自然过渡。这个示例将应用这一点。

准备就绪

要完成这个示例,您需要先运行之前的示例。这个示例将为其添加一些动画。

如何做…

以下步骤将帮助我们为应用程序添加动画:

  1. MainAdapter类的ViewHolder中添加一个mDrink成员:
public Drink mDrink;
  1. onBindViewHolder方法中的同一文件中,在currentDrink初始化后,通知view holder有关实际饮料的信息:
Drink currentDrink = mDrinks.get(position);
holder.mDrink = currentDrink;
  1. onCreateViewHolder方法中,添加一个OnClickListener到末尾:
v.setTag(viewHolder);
v.setOnClickListener(new View.OnClickListener() {
    @Override
	    public void onClick(View view) {
        ViewHolder holder = (ViewHolder) view.getTag();
        if (view.getId() == holder.itemView.getId()) 
        {
        }
    }
});
  1. 如果视图被点击,我们希望EntryActivity类显示所选的饮料条目。为了能够通知条目有关选择,我们需要将Drink模型设为parcelable类:
public class Drink implements Parcelable
  1. 我们需要实现一些方法:
@Override
public int describeContents() {
    return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
    out.writeLong(dateAndTime.getTime());
    out.writeString(comments);
    out.writeString(imageUri);
}
public static final Parcelable.Creator<Drink> CREATOR = new 
 Parcelable.Creator<Drink>() {
    public Drink createFromParcel(Parcel in) {
        return new Drink(in);
    }
    public Drink[] newArray(int size) {
        return new Drink[size];
    }
};
  1. Drink类添加两个构造函数——一个默认的和一个带有 parcel 的,这样我们就可以重新创建对象并用适当的值填充它:
public Drink(){
}
public Drink(Parcel in) {
    dateAndTime = new Date(in.readLong());
    comments = in.readString();
    imageUri = in.readString();
}
  1. MainAdapter类中,添加一个用于请求的私有变量。这种方法使您的代码更易读:
private int REQUEST_EDIT_ENTRY = 2;

提示

所谓的魔术数字很容易被误解,应尽量避免使用。这些和其他的示例仅用于演示目的,但在现实世界中,您应尽可能使用自解释的常量。在这里,REQUEST_EDIT_ENTRY比在代码中的某个地方只放置数字2更有意义。

  1. 现在,在MainAdapteronCreateViewHolder方法中我们之前创建的onClick方法中,我们可以启动一个新的条目活动并将所选的饮料作为参数传递。onClick方法的实现现在将如下所示:
v.setOnClickListener(new View.OnClickListener() {
    @Override
	    public void onClick(View view) {
        ViewHolder holder = (ViewHolder) view.getTag();
        if (view.getId() == holder.itemView.getId()) {
            Intent intent = new Intent(mContext,    
             EntryActivity.class);
            intent.putExtra("edit_drink", holder.mDrink);
    ((Activity)mContext).startActivityForResult(intent,  
              REQUEST_EDIT_ENTRY); }
    }
});
  1. EntryActivity类的onCreate方法中,我们将检索并显示所选饮料的属性。将此实现添加到方法的末尾:
Intent intent = getIntent();
if (intent.hasExtra("edit_drink")) {
    Drink editableDrink = intent.getParcelableExtra("edit_drink");
    EditText editComment =    
     (EditText)findViewById(R.id.entry_edit_text_comment);
    editComment.setText(editableDrink.comments);
    if (editableDrink.imageUri != null) {
        mUri = Uri.parse(editableDrink.imageUri);
        Bitmap bitmap = getBitmapFromUri();
        ImageView preview = (ImageView) 
         findViewById(R.id.entry_image_view_preview);
        preview.setImageBitmap(bitmap);
    }
}

评论的 EditText 将填充评论,以便用户可以编辑它们。如果饮料条目附有图像,它将显示在预览图像视图中。现在,如果我们有一种简单而酷的方法将图像的缩略图动画到预览中:

  1. 惊喜!有。在res/values文件夹中的strings.xml文件中添加一个新的字符串资源:
<string name="transition_preview">transition_preview 
  </string>
  1. MainAdapter类的onCreateViewHolder方法中,在onClick实现中,并且在startActivityForResult方法之前,我们将使用ActivityOptionsCompat类来创建从缩略图(holder 的mImageView成员)到条目活动布局中预览图像的过渡:
ActivityOptionsCompat options =  
 ActivityOptionsCompat.makeSceneTransitionAnimation(
  ((Activity)mContext), holder.mImageView,    
   mContext.getString (R.string.transition_preview));
  1. 通过用这个实现替换下一行上的startActivityForResult调用来提供这些选项:
ActivityCompat.startActivityForResult(((Activity) mContext),  
 intent, REQUEST_EDIT_ENTRY, options.toBundle());
  1. 打开adapter_main_card_view.xml布局文件,并将此行添加到图像视图(具有main_image_viewID 的小部件):
android:transitionName="@string/transition_preview"
  1. activity_entry.xml布局中,也将此行添加到ImageView小部件(具有entry_image_view_previewID 的小部件)。这样 Android 就知道缩略图到更大的预览图像的过渡应该去哪里)。

注意

使用字符串资源是一个好的实践。我们可以在这里使用这些资源,以确保我们在代码的任何地方都在谈论相同的过渡,但这也对本地化目的非常有用。

现在,如果您运行您的应用程序并点击MainActivity类中的任何卡片,您将看到缩略图被放大并适合于EntryActivity类的布局中预览图像的占位符。如果选择返回按钮,则显示反向过渡。在以前的版本中,我们不能只用几行代码来做到这一点!

主题

作为奖励,让我们按照以下步骤进行一些主题设置:

  1. 访问www.materialpalette.com并选择两种颜色。主题设置出现了一个颜色集,我们可以将其用作主题,如下截图所示:Theming

  2. res/values文件夹中创建一个color.xml文件,并添加建议的颜色名称和值。我在网站上选择了蓝色和靛蓝色,所以我的颜色资源文件看起来像这样:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="primary_dark">#1976d2</color><color name="primary">#2193f3</color>
    <color name="light_primary">#bbdefb</color>
    <color name="text">#ffffff</color>
    <color name="accent">#536dfe</color>
    <color name="primary_text">#212121</color>
    <color name="secondary_text">#727272</color>
    <color name="divider_color">#b6b6b6</color>
</resources>
  1. 编辑res/values文件夹中的styles.xml文件,并使其看起来像这样:
<resources><style name="AppTheme" parent="Theme.AppCompat.Light">
      <item name="android:colorPrimary">@color/primary</item>
      <item name="android:colorPrimaryDark">@color/primary_dark 
      /item>
      <item name="android:colorAccent">@color/accent</item>
      <item name="android:textColor">@color/text</item>
      <item name="android:textColorPrimary">@color/primary_text 
      </item>
     <item name="android:textColorSecondary">
        @color/secondary_text
      </item>
  </style></resources>

上述代码的输出如下截图所示:

Theming

  1. 修改您的布局文件并更改文本视图和其他元素,以便它可以反映颜色方案。运行应用程序。

它是如何工作的...

Android 的活动转换将处理一切。我们只需要告诉它什么,哪里以及如何。只需几行代码,API 就可以让您在活动之间创建有意义的转换,这将大大改善应用程序的用户体验。

每一步都让你的应用程序看起来越来越好!不幸的是,这就是材料设计介绍的结束。无论你想要在哪里进行改进,都可以随意尝试并享受乐趣!动画,用户体验和布局是高质量应用程序的重要元素。

对于可穿戴应用程序来说,这可能更加重要,正如我们将在下一章中看到的那样。但是,我们如何在如此小的屏幕上实现出色的用户体验呢?

还有更多...

我们只看到了材料设计的一些方面。还有很多东西等待我们去发现。

进一步改善应用程序的外观和用户体验,将实现添加到MainActivity类中以处理您添加的饮料条目的数据,并在需要时进行增强。或者,您可以查看现有的应用程序,看看如何将它们实现。

第四章:Android Wear

本章将向您介绍 Android Wear 以及它如何作为手表和其他设备实现的现象。

在本章中,您将学习以下内容:

  • 可穿戴设备

  • 全屏可穿戴应用

  • 表盘

  • 通知

可穿戴设备

Android Wear 是许多可穿戴设备运行的系统。您可能自己有一块智能手表。可穿戴设备会成为继手机、平板电脑之后的下一个热潮吗?还是智能手表会成为更大事物的一部分,比如物联网IoT)?

Android Wear 是 Android SDK 的一个特殊版本,专门用于通常在硬件和可用传感器方面更受限制、屏幕更小的可穿戴设备。可穿戴设备可能出现为手表、眼镜,或者将来可能会出现为隐形眼镜、纹身或服装。

目前,我们看到可穿戴设备主要出现在手表上,但您可以想到还有许多其他可穿戴设备。然而,人们需要一些时间来接受这项新技术。例如,想想谷歌眼镜项目。这是一个很棒的解决方案,但主要是因为内置摄像头,人们对它有严重的反对意见。在旧金山,他们甚至为此创造了一个词:glass hole。嗯。这真的不太讨人喜欢,对吧?让我们看看以下设备:

可穿戴设备

设备不一定要是可穿戴的。当讨论 IOT 时,项目 Brillo 就会浮现在脑海中。它将 Android 平台扩展到您能想到的任何连接设备上。未来,Brillo 和 Android Wear 甚至可能会合并。

想象一个炎热的夏日;冰箱通知我们即将用完气泡水(还是啤酒?)。酷!学习型恒温器在您回家前一小时将温度设定为 18°C。更酷!客厅的灯光自动调暗,因为现在是晚上;您正在播放一些浪漫的音乐,系统知道您刚刚打开了一瓶葡萄酒-嗯。奇怪。这是一个完全不同的故事,Brillo 现在也是如此。

相反,让我们找出我们可以为智能手表构建哪些应用,比如全新的表盘或健康应用程序,不时显示通知。在接下来的步骤中,我们将看到为此需要做些什么。

首先,让我们看看我们是否可以在可穿戴设备上运行起来。在前两个步骤中,您不需要拥有真正的智能手表。我们将在第一个步骤中创建一个虚拟的智能手表。

全屏可穿戴应用

可穿戴全屏应用程序确实有手机(或其他手持设备)和可穿戴组件。用户在手机上安装手持应用程序,可穿戴组件会自动推送到配对的可穿戴设备上。

这是探索为可穿戴设备开发应用程序的有趣世界的一个很好的开始,因为它们基本上与 Android 手机应用程序相同。然而,谷歌鼓励您将应用程序与 Android Wear 的上下文流集成在一起。这个上下文流包含各种有趣的信息。可以将它们视为收件箱中的新邮件、天气、今天走的步数或心率。我们将在有关通知的食谱中了解更多信息。

准备就绪

要完成这个食谱,您需要确保 Android Studio 已经运行起来。还要确保您已安装了最新的 SDK,包括 Android Wear SDK。当您打开 SDK 管理器时,可以检查是否已经安装了这些(导航到工具菜单,Android SDK 管理器),如下截图所示:

准备就绪

如何做...

让我们看看如何创建我们自己的可穿戴应用,并通过以下步骤在虚拟设备上运行它:

  1. 开始一个新的 Android Studio 项目。将应用命名为WatchApp,并在公司域字段中输入packtpub.com。然后,点击下一步按钮。

  2. 在下一个对话框中,勾选手机和平板电脑。还要勾选可穿戴设备选项。

  3. 对于这两个选项,选择API 21或更高版本,然后点击下一步按钮。

  4. 添加到 wear 的活动对话框中,选择空白 wear 活动,然后点击下一步按钮。

  5. 选择空白活动,然后点击下一步按钮。

  6. 将您的新活动命名为PhoneActivity,然后点击下一步按钮。

  7. 选择空白 wear 活动,然后点击下一步按钮,如下一个截图所示:如何做...

  8. 将您的新 wear 活动命名为WatchActivity,然后点击完成按钮。

  9. Android Studio 将创建两个模块:mobilewear。移动模块在智能手机(或平板电脑或平板电脑)上运行。wear 应用程序将被推送到配对的可穿戴设备,例如您的智能手表。项目视图现在看起来像这样:如何做...

  10. 让我们看看默认情况下它在智能手机上的样子。为此,我们将创建一个可穿戴虚拟设备。从工具菜单中,选择Android选项,然后选择AVD Manager选项。

  11. 然后,点击创建虚拟设备按钮。

  12. 在弹出的对话框中,在类别列表中选择Wear。在旁边的列表中选择Android Wear Round设备,然后点击下一步按钮,如下一个截图所示:如何做...

  13. 在下一个对话框中,选择一个系统镜像,例如棒棒糖API 级别 21x86(如果可用,也可以选择更高级别。您可能需要先点击下载链接)。然后,点击下一步按钮继续。

  14. 给您的虚拟设备起一个好听的名字,然后点击完成按钮。您的新 Android wear 设备现在将出现在列表中,如下一个截图所示:如何做...

  15. 点击播放图标启动设备。

  16. 虚拟设备启动后,将配置更改为wear,然后点击工具栏旁边的运行按钮。如何做...

应用程序安装完成后,将如下所示:

如何做...

如果Hello Round World!消息没有立即出现,那么该应用程序可能已安装,但可能尚不可见。多次滑动屏幕以检查是否存在。

如果您的应用程序已经运行,那么现在是时候探索更有趣的东西了。让我们在下一个教程中创建一个表盘。

还有更多...

在撰写本文时,Genymotion 尚不支持可穿戴设备。这就是为什么在本教程中我们使用默认的模拟器。

但那个太慢了!您可能会说。这是真的,但通过安装 HAXM,您可以使它快一点。关于这个主题有一些有趣的信息,可以在developer.android.com/tools/devices/emulator.html找到。

如果您确实有真实设备,当然也可以在智能手表上部署您的应用程序。如果要这样做,您还需要在手持设备上安装 Android wear 配套应用程序,因为您无法直接在其上安装和测试可穿戴应用程序。

您可以从 Google Play 获取这个配套应用。下载应用程序,安装它,并通过 USB 连接您的手持设备。

另请参阅

  • 参考第一章中的使用名为 Genymotion 的模拟器测试您的应用程序部分,欢迎使用 Android Studio

表盘

您的 Android 智能手表默认配备了各种表盘,还有许多其他表盘可供下载。它们以任何形状或类型提供:方形和圆形,模拟和数字。实际上,甚至还有另一种形状 - 所谓的平坦轮胎形状 - 就像 Moto 360 设备上看到的那样。

有许多自定义选项,但所有的表盘都是为了以简单的方式显示时间和日期信息。这首先是手表的用途,不是吗?

他们应该注意到即将到来的通知,还需要为系统指示器腾出空间,例如电池寿命图标和Ok Google文本。有关更多信息,请访问developer.android.com/design/wear/watchfaces.html

在即将创建的示例中,我们将创建一个手表表盘,告诉你时间,例如七点半十点五分钟

准备工作

要完成本示例,您需要运行 Android Studio。还要确保已安装了最新的 SDK,包括 Android Wear SDK。您可以通过打开 SDK 管理器来检查是否已安装,该管理器可在工具菜单下的Android中找到,该菜单项位于SDK Manager下。

操作步骤

让我们按以下步骤创建一个手表表盘应用的新 Android 项目:

  1. 创建一个新的 Android Studio 项目。

  2. 将应用命名为HelloTime,或者您想要的应用名称。在公司域字段中输入packtpub.com,然后单击下一步按钮。

  3. 在下一个对话框中,勾选手机和平板。还要勾选Wear选项。

  4. 对于这两个选项,选择API 21或更高版本,然后单击下一步按钮。

  5. 选择空白活动,并单击下一步按钮。

  6. 将新的活动命名为PhoneActivity,并单击下一步按钮。

  7. 选择表盘,并单击下一步按钮。

  8. 将表盘命名为HelloTimeWatchFace,并选择数字作为样式。之后,点击完成按钮。

  9. Android Studio 将为手机或平板和可穿戴设备创建必要的模块。

  10. 在项目视图中,打开wear模块的HelloTimeWatchFace类。

  11. wear模块的res/values文件夹中打开strings.xml文件,并将my_digital_name的字符串更改为Hello Time!

  12. 让我们看看我们到目前为止得到了什么。启动虚拟(或真实的)可穿戴设备。如果你不知道如何创建虚拟可穿戴设备,请参考上一个示例。

  13. 虚拟设备启动后,将配置更改为Wear,并单击工具栏旁边的运行按钮,如下图所示:操作步骤…

  14. 在可穿戴设备上,滑动查看设置图标并点击它。

  15. 向下滑动到更改表盘,并点击它。

  16. 向右滑动,直到看到Hello Time!表盘,然后点击它。

  17. 您现在将看到 Android Studio 为您创建的数字表盘。

让我们稍微检查一下这段代码。为你创建的HelloTimeWatchFace类扩展了CanvasWatchFaceService,并添加了一个内部的Engine类。引擎有一个处理程序,以便可以更新时间。它还有一个广播接收器,将处理用户在旅行时移动到另一个时区的情况。

Engine类有一些有趣的方法。onCreate方法分配了两个Paint对象:一个用于背景,一个用于前景(文本)。onVisibilityChanged方法将在用户显示或隐藏表盘时调用。onApplyWindowInSets方法用于确定应用是否在圆形或方形屏幕上运行。

接下来是onPropertiesChanged方法,一旦可穿戴设备的硬件属性已知,例如是否支持低位环境模式,就会调用该方法。onAmbientModeChanged方法非常重要,因为它可以节省电池。它还可以用于应用防烧屏保护。在这里,您可能想要更改背景或前景的颜色。

让我们改变时间的显示方式:

  1. 添加一个以口语语言返回当前时间的方法,类似于这样:
private String[] getFullTextTime(){
    String time = "";Calendar cal = Calendar.getInstance();
   int minute = cal.get(Calendar.MINUTE);
   int hour = cal.get(Calendar.HOUR);
   if (minute<=7){
        time = String.format("%s o'clock",   getTextDigit(hour));
    }
   else if (minute<=15){
        time = String.format("ten past %s",    getTextDigit(hour));
    }
   else if (minute<=25){
       time = String.format("Quarter past %s", getTextDigit(hour));
    }
   else if (minute<=40){
       time = String.format("Half past %s", getTextDigit(hour));
   }
  else if (minute<53){
       time = String.format("Quarter to %s",  
       getTextDigit(hour));
  }
  else {
       time = String.format("Almost %d o'clock", (hour<=11)? hour+1: 1);
  }
  return time.split(" ");
}
  1. 添加此方法以将数字转换为文本:
private String getTextDigit(int digit){
    String[] texts ={ "twelve", "one", "two", "three",  
     "four", "five", "six", "seven", "eight", "nine",       
       "eleven"};
     return texts[digit];
  1. onDraw方法中,用这里显示的行替换canvas.DrawText部分。此方法显示口语语言中当前时间的多行:
String[] timeTextArray = getFullTextTime();
float y = mYOffset;
for (String timeText : timeTextArray){
    canvas.drawText(timeText, mXOffset, y, mTextPaint);
    y+=65;
}

注意

魔术并不总是很酷...

等等!在上一步中那个魔术数字是在做什么?65 实际上并没有意义。这是什么意思?它是做什么的?在您的类中的某个地方创建一个常量值,并使用该变量名称(在这里最好将值放在尺寸资源文件中,但我们稍后会看到这一点,所以现在让我们暂时忘记它):

private static final int ROW_HEIGHT  = 65;
y+= ROW_HEIGHT;
  1. 转到onCreate方法,并添加此行以使文本以漂亮的绿色显示(是的,GREEN也是一个常量):
mTextPaint.setColor(Color.GREEN);

再次运行您的应用程序。它会看起来像这样:

如何做...

为了以后准备好 Play 商店的手表表盘,您需要在完成后拍摄屏幕截图。您需要为方形和圆形手表提供屏幕截图。在res/drawable文件夹中,您会找到 Android Studio 为您创建的默认预览图像。

目前,您只是以最基本的形式创建了您的第一个手表表盘应用程序。在下一个食谱中,我们将看到通知到来时会发生什么。

还有更多...

本食谱中的手表表盘应用程序远非完美。文本未对齐;它没有正确响应环境模式的更改,您可能希望将其本地化以以您自己的语言显示时间。

要了解这可能会发展成什么样,您可以查看 Play 商店中已经可用的许多手表表盘。

通知

Android Wear 与在手机或平板上运行的应用程序有所不同。 Android Wear 使用卡片,而不是图标和列表,这是我们在介绍材料设计基本概念的食谱中已经看到的东西。

根据上下文,并且只在相关时刻,一旦新通知到达,就会向卡片流中添加一张卡片。这被称为上下文流,其中包含各种有趣的信息。将它们视为收件箱中的电子邮件,天气,今天走的步数,心率,以及其他事件或提醒。

还记得上一章的饮水应用程序吗?例如,我们可以创建一个提醒我们更频繁饮水并为其添加新卡片的通知。这将是一个很好的功能。

准备工作

此食谱需要安装 Android Studio 和最新的 SDK,包括 wear SDK。有关更多信息,请查看上一个食谱。

您还需要一台运行 Android Lollipop 或更高版本的手持设备,该设备已安装Android Wear应用程序,并且通过蓝牙连接到您的手持设备的可穿戴设备。

如何做...

让我们看看如何触发通知以及如何在智能手表上漂亮地显示它们:

  1. 在 Android Studio 中创建一个新项目。将其命名为WaterNowNotification,然后单击下一步按钮。

  2. 选择手机和平板电脑作为智能手表平台。不要选择Wear选项。然后单击下一步按钮。

  3. 选择空白活动,然后单击下一步按钮。

  4. 将您的活动命名为WaterNowActivity,然后单击完成按钮。

  5. 在您的应用中打开build.gradle文件。将其添加到依赖项部分并应用适当的版本:

compile 'com.android.support:support-v4:22.0+'
  1. 单击工具栏上可以找到的与 Gradle 文件同步项目按钮。

  2. 打开activity_water_now.xml文件,并使用 Android Studio 底部的选项卡将其更改为文本模式。

  3. 创建一个带有按钮的布局,我们将用它来发送测试通知:

<LinearLayout

android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".WaterNowActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Drink water now!"
android:id="@+id/water_now_button"
android:layout_gravity="center" />
</LinearLayout>
  1. WaterNowActivity类的onCreate方法中,添加一个onClick处理程序,用于刚刚创建的按钮。根据需要使用Alt + Enter快捷键添加导入语句:
Button waterNowButton = (Button)findViewById(R.id.water_now_button);
waterNowButton.setOnClickListener(new View.OnClickListener() {
@Override
        public void onClick(View v) {
        sendNotification();   }
});
  1. 创建sendNotification方法:
private void sendNotification(){
    NotificationCompat.Builder notificationBuilder =
    new NotificationCompat.Builder(   
      WaterNowActivity.this)
      .setContentTitle("Water app!")
      .setSmallIcon(R.drawable.icon)
      .setContentText("Hey there! Drink water now!");
    NotificationManagerCompat notificationManager =NotificationManagerCompat.from(  
      WaterNowActivity.this);
    notificationManager.notify(1 ,   
     notificationBuilder.build());
}
  1. 通知确实需要一个图标,所以在res/drawable文件夹中创建一个。创建一个 drawable icon.xml文件,并添加实现以创建一个漂亮的蓝色圆圈:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android= "http://schemas.android.com/apk/res/android"android:shape="oval">
<corners android:radius="10dip"/>
<stroke android:color="#0000FF" android:width="15dip"/>
<solid android:color="#0000FF"/>
</shape>
  1. 连接你的手持设备;确保可穿戴设备已连接(使用Android wear应用来检查),然后运行应用。你会看到类似以下截图的输出:操作步骤...

  2. 点击应用内的现在喝水按钮。

  3. 手机上会显示类似以下截图的通知。如果通知没有立刻出现,屏幕顶部会有一些指示。在这种情况下,打开通知中心查看。操作步骤...

  4. 如果一切正常并且配置正确,同样的通知会出现在可穿戴设备上,如下所示:操作步骤...

  5. 如果通知在你的手机上显示,但在你的可穿戴设备上没有出现,那么请验证通知访问设置。打开设置应用,选择声音和消息。接下来,选择通知访问,并检查Android Wear选项是否已被选中。

对于其他 Android 版本或特定品牌(定制的 Android 版本),你要找的设置可能在其他地方,或者可能有不同的名称。

还有更多...

接下来怎么办?你可以将这个通知配方与第三章中的 Water 应用配方相结合,创造出更酷的东西,或者你可以检查是否可以找到一种自定义通知的方法。

智能手表、手机、平板手机和平板电脑都配备了各种尺寸和形状的屏幕。我们如何从更大的屏幕中受益,或者如何为较小的屏幕提供智能导航,并在一个应用中保持相同的功能和代码?

不同 Android 版本的不同布局?多个布局与多个片段的结合正是我们需要的。这就是下一章中的配方发挥作用的地方。

另请参阅

  • 参考第三章中的RecyclerViewCardView部分,材料设计

  • 参考第五章, 尺寸很重要

第五章:大小确实重要

本章是关于构建可以在各种设备上运行的应用程序:手机、平板、平板手机和电视。我们将连接到 YouTube 获取一些数据和视频来显示。

大小和上下文确实很重要。当然,我们可以将所有内容都放大,但这并不能真正使应用程序变得更好。平板提供的空间比手机更多,而在用户交互方面,电视与智能手机有所不同。我们如何使布局在每台设备上都能按比例缩放并看起来流畅?我们如何为每种类型的设备找到正确的方法?

在本章中,您将学习以下内容:

  • 大小和上下文

  • 手机、平板和平板手机布局

  • 媒体播放

  • 电视和媒体中心

大小和上下文

手机、平板、平板手机和电视等设备都配备了各种大小和形状的屏幕。我们如何从更大的屏幕中受益,或者如何为较小的屏幕提供智能导航,并在一个应用程序中保持相同的功能和代码?这就是这个第一个配方要解决的问题。

各种设备怎么样?随着可穿戴设备的推出,我们发现这些类型设备的用户行为有很大不同。同样的情况也适用于电视。一如既往,让我们先做第一步。让我们检查一个可以在手机和平板上运行的应用程序。

手机、平板和平板手机

手机上一个众所周知的模式是列表或回收视图,当您点击任何行时,它会显示一些详细信息。在小屏幕上,应用程序会将您导航到不同的视图。这种模式之所以存在,是因为手机屏幕上的空间不足。如果您在具有足够空间的设备上运行相同的应用程序,我们可以在屏幕的左侧显示列表,右侧显示详细信息。

多个布局是我们需要的,再加上多个片段。如果我们这样做,我们可以减少需要编写的代码量。我们不想重复自己,对吧?

片段是 Android 开发中功能强大但也经常被误解的组件。片段是(小)功能块,大多数时间都有自己的布局。使用片段容器,片段可以存在于多个位置和多个与活动相关的布局中。这就是我们如何重用功能和布局的方法。

不过,片段应该谨慎使用。如果没有合适的策略,使用片段的应用程序可能会给您带来很多麻烦。片段中的代码经常会引用活动。虽然这些代码可能仍在运行,但片段可能在中间被从活动中分离出来(例如,因为用户按下了返回按钮)。这可能导致您的应用程序崩溃。

准备工作

要完成此配方,您需要安装并运行 Android Studio,并且需要一部手机、平板和/或平板手机设备(一如既往,建议使用实体设备;但是您也可以使用 Genymotion 创建虚拟设备)。

由于我们将使用 YouTube Android API,您需要在设备上安装最新的 YouTube Android 应用程序。检查您的设备上是否有该应用程序,如果没有或者有更新的话,可以使用 Google Play 应用程序进行安装或更新。

最后,您需要一个开发者账户。如果您还没有一个,您需要首先从developer.android.com/distribute/googleplay/start.html创建一个。

除了购买本书之外,为自己购买一个开发者账户是一个非常好的投资,我强烈建议您这样做。无论如何,您都需要一个才能将您的应用程序提交到 Google Play 商店中!

如何做...

让我们看看如何创建我们自己的可穿戴应用程序并在设备上运行:

  1. 开始一个新的 Android Studio 项目。将您的应用程序命名为YouTubeMediaApp,并在公司域字段中输入packt.com。然后点击下一步按钮。

  2. 在接下来的对话框中,只选中手机和平板电脑选项,然后单击下一步按钮。

  3. 在下一个对话框中,选择空白活动,然后单击下一步按钮。

  4. 自定义活动对话框中,单击完成按钮。

  5. Android Studio 将为您创建新项目。在 Android Studio 左侧的项目视图中,找到app文件夹中的build.gradle并打开它。

  6. app文件夹中的build.gradle文件中添加一个依赖项到dependencies部分,以使用 YouTube 服务 API。我们将使用此 API 在 YouTube 上搜索视频:

compile 'com.google.apis:google-api-services-youtube:v3-rev120-1.19.0'
  1. 同步项目(单击立即同步链接或使用工具栏中的同步项目与 Gradle 文件按钮)。

  2. 打开activity_main.xml布局。创建一个框架布局,它将作为我们稍后要在此处显示的片段的容器。出于演示目的,我们将为其选择一个漂亮的背景颜色。让我们选择橙色:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android=
  "http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="@android:color/holo_orange_light"
   android:id="@+id/main_container_for_list_fragment">
</FrameLayout>
  1. 添加一个新布局并命名为fragment_list.xml。在容器内创建一个列表视图。此列表将包含我们在 YouTube 上找到的视频的标题和其他信息:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 
    android:orientation="vertical"     
    android:layout_width="match_parent"
    android:layout_height="match_parent">
<ListView
    android:id="@+id/main_video_list_view"
	android:visibility="visible"
	android:padding="6dp"
	android:layout_marginTop="0dp"
	android:layout_width="match_parent"
	android:layout_height="match_parent">
	</ListView>
</FrameLayout>
  1. 添加一个新的 Java 类,命名为ListFragment,然后单击确定按钮继续。

  2. 将新类设置为Fragment的子类,并重写onCreate方法。为列表视图创建一个私有成员,并按照以下代码在布局中添加对列表视图的引用:

public class ListFragment extends Fragment {
  private ListView mListView;
  @Override
  public View onCreateView(LayoutInflater inflater,    
   ViewGroup container, Bundle savedInstanceState) 
    final View view= inflater.inflate(  
      R.layout.fragment_list, container, false);
    mListView = (ListView)view.findViewById(
     R.id.main_video_list_view);
    return view;
  }
}

注意

除了ListActivity之外,还有一个ListFragment类,您可以从中继承。出于演示目的,我们将在这里从Fragment类继承并自行处理一些事情。

  1. 在添加正确的导入语句(使用Alt + Enter快捷键或其他方式)时,您将能够选择要导入的包。您可以在android.app.Fragmentandroid.support.v4.app.Fragment包之间进行选择。后者仅用于向后兼容。由于我们将为我们的应用程序使用最新的 SDK,请在被询问时选择此导入语句:
import android.app.Fragment;
  1. 为 YouTube 添加另一个私有成员和一个 YouTube 列表,并创建一个名为loadVideos的方法。首先,我们将初始化 YouTube 成员:
private YouTube mYoutube;
private YouTube.Search.List mYouTubeList;
private void loadVideos(String queryString){
 mYoutube = new YouTube.Builder(new NetHttpTransport(),
  new JacksonFactory(), new HttpRequestInitializer() {
   @Override
   public void initialize(HttpRequest hr) throws  
    IOException {}
 }).setApplicationName( 
  getString(R.string.app_name)).build();
}
  1. 接下来,我们将告诉 YouTube 我们要寻找什么以及我们希望 API 返回什么信息。我们需要在loadVideos方法的末尾添加 try catch 结构,因为我们事先不知道是否能连接到 YouTube。将以下内容添加到loadVideos方法的末尾:
try{
 mYouTubeList = mYoutube.search().list("id,snippet");      
 mYouTubeList.setType("video");
 mYouTubeList.setFields( 
  "items(id/videoId,snippet/title,snippet/   
      description,snippet/thumbnails/default/url)");
}
catch (IOException e) {
  Log.d(this.getClass().toString(), "Could not 
    initialize: " + e);
}
  1. 要使用 YouTube API,您必须首先注册您的应用程序。要这样做,请将浏览器导航到console.developers.google.com/project

  2. 单击创建项目按钮。输入YouTubeApp作为项目名称,然后单击创建按钮。

  3. 项目创建后,仪表板将显示在网页上。在左侧,展开API 和身份验证,然后单击API

  4. 在页面的右侧,单击 YouTube 数据 API。单击启用 API按钮。

  5. 再次在左侧,单击 API 之后的凭据。在公共 API 访问下,单击创建新密钥按钮。

  6. 创建新密钥弹出对话框中,单击Android 密钥按钮。

  7. 由于此应用仅用于演示目的,我们不需要查找所请求的SHA1值。只需单击创建按钮。

  8. 现在,将为您创建一个 API 密钥。复制 API 密钥的值。

  9. AndroidManifest.xml文件中,添加一个访问互联网的权限:

android:name="android.permission.INTERNET"/>

将其粘合在一起!

  1. 现在回到ListFragment类,告诉 API 关于您的密钥,该密钥就在 YouTube 对象的search调用旁边:
mYouTubeList.setKey("Your API key goes here");
  1. 创建一个新的VideoItem类,并添加成员以保存每个视频的请求信息。请注意,我们在这里使用 getter 和 setter:
private String title;
private String description;
private String thumbnailURL;
private String id;
public String getId() {
 return id;
}
public void setId(String id) {
 this.id = id;
}
public String getTitle() {
 return title;
}
public void setTitle(String title) {
 this.title = title;
}
public String getDescription() {
 return description;
}
public void setDescription(String description) {
 this.description = description;
}
public String getThumbnailURL() {
 return thumbnailURL;
}
public void setThumbnailURL(String thumbnail) {
 this.thumbnailURL = thumbnail;
}
  1. 创建一个新布局并命名为adapter_video.xml。然后,添加文本视图以显示视频信息:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical"
   xmlns:android= 
    "http://schemas.android.com/apk/res/android"
  android:padding="6dp">
<TextView
  android:id="@+id/adapter_video_id"android:textSize="14sp"android:textStyle="bold"android:layout_width="match_parent"android:layout_height="wrap_content" />
<TextView
  android:id="@+id/adapter_video_title"android:textSize="20sp"android:layout_marginTop="2dp"android:layout_width="match_parent"android:layout_height="wrap_content" /></LinearLayout>
  1. 创建一个新的VideoAdapter类,并使其成为ArrayAdapter的子类,用于保存VideoItem类型的条目。一个视图持有者将帮助我们用列出的VideoItem对象的属性填充文本视图:
public class VideoAdapter extends ArrayAdapter<VideoItem> {
 private Context mContext;
 private int mAdapterResourceId;
 public ArrayList<VideoItem>mVideos = null;
 static class ViewHolder{
        TextView videoId;
        TextView videoTitle;
    }
@Override
 public int getCount(){
 super.getCount();
 int count = mVideos !=null ? mVideos.size() : 0;
 return count;
}
public VideoAdapter (Context context, int  
 adapterResourceId, ArrayList<VideoItem> items)
{
 super(context, adapterResourceId, items);
 this.mVideos = items;
 this.mContext = context;
 this.mAdapterResourceId = adapterResourceId; 
}
@Override
public View getView(int position, View convertView, ViewGroup parent)
{
 View v = convertView;
if (v == null){LayoutInflater vi =   
     (LayoutInflater)this.getContext().getSystemService(
      Context.LAYOUT_INFLATER_SERVICE);
    v = vi.inflate(mAdapterResourceId, null);
    ViewHolder holder = new ViewHolder();
    holder.videoId = (TextView)  
     v.findViewById(R.id.adapter_video_id);
    holder.videoTitle = (TextView) 
     v.findViewById(R.id.adapter_video_title);     
    v.setTag(holder);
 }
 final VideoItem item = mVideos.get(position);
 if(item != null){
  final ViewHolder holder = (ViewHolder)v.getTag();
  holder.videoId.setText(item.getId());
  holder.videoTitle.setText( item.getTitle());
 }
 return v;
}
  1. 现在回到ListFragment类。在其中再添加两个私有成员,一个用于我们找到的视频列表,一个用于我们刚刚创建的适配器:
private List<VideoItem>mVideos;
private VideoAdapter mAdapter;
  1. ListFragment类中添加一个search方法:
public List<VideoItem> search(String keywords){
 mYouTubeList.setQ(keywords);
try{
   SearchListResponse response = mYouTubeList.execute();
   List<SearchResult> results = response.getItems();
   List<VideoItem>  items = new ArrayList<VideoItem>();
    for(SearchResult result:results){

    VideoItem item = new VideoItem();
    item.setTitle(result.getSnippet().getTitle());
    item.setDescription(result.getSnippet().
     getDescription());

    item.setThumbnailURL(result.getSnippet().
     getThumbnails().getDefault().getUrl());
    item.setId(result.getId().getVideoId());
    items.add(item);
  }
  return items;
 }
catch(IOException e){
  Log.d("TEST", "Could not search: " + e);
 }
}
  1. loadVideos方法的末尾,添加调用search方法和初始化适配器的实现:
mVideos =search(queryString§);
mAdapter = new VideoAdapter(getActivity(), R.layout.adapter_video, (ArrayList<VideoItem>) mVideos);
  1. 告诉列表视图关于适配器,并调用适配器的notifyDataSetChanged方法,通知有新条目可供显示。为此,我们将使用一个在 UI 线程上运行的Runnable实例:
getActivity().runOnUiThread(new Runnable() {
public void run() {
   mListView.setAdapter(mAdapter);
   mAdapter.notifyDataSetChanged();
 }
});
  1. 现在我们将异步加载视频信息,因为我们希望应用在从互联网获取数据时能够响应。创建一个新线程,并在run方法内调用loadVideos。假设我们想要查看Android 开发视频:
@Override
 public void onActivityCreated(Bundle bundle){
 super.onActivityCreated(bundle);
 new Thread(new Runnable() {
   public void run(){
      loadVideos("Android development");
   }
}).start();
}
  1. 创建一个新的布局并命名为fragment_details.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">
<Button
android:id="@+id/detail_button_play"android:text="@string/play"android:layout_width="match_parent"android:layout_height="wrap_content" />
<ImageView
android:id="@+id/detail_image"android:layout_width="match_parent"android:layout_height="wrap_content"android:src="img/gallery_thumb"/>
<TextView
android:layout_marginTop="16dp"android:id="@+id/detail_text"android:minHeight="200dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
  1. 创建DetailsFragment类:
public class DetailsFragment  extends Fragment {
  @Override
  public View onCreateView(LayoutInflater inflater,
   ViewGroup container, Bundle savedInstanceState) {
    final View view= inflater.inflate(
      R.layout.fragment_details, container, false);
     return view;
  }
}
  1. DetailsFragment类中添加showDetails私有方法。在此方法中,我们将设置描述的文本,并创建一个新的 runnable 实例来加载视频的缩略图。还添加setVideo方法并重写onResume方法:
private void showDetails(){
if (getView()!=null &&mVideo != null)
 {
   TextView tv = (TextView) 
    getView().findViewById(R.id.detail_text);
   final ImageView iv = (ImageView)    
    getView().findViewById(R.id.detail_image);
   tv.setText(mVideo.getDescription());
  new Thread(new Runnable() {
   public void run() {
      loadThumbnail(mVideo, iv);
    }
   }).start();
  }
}
public void setVideo(VideoItem video)
{
  mVideo = video;
  showDetails();
}
@Override
  public void onResume(){
  super.onResume();
  showDetails();
}
  1. 现在,在DetailsFragment类中添加loadThumbnail方法和从给定 URL 加载缩略图图像的实现:
private void loadThumbnail(VideoItem video,final  
 ImageView iv){
try 
 {
    URL url = new URL(video.getThumbnailURL());
   final Bitmap bmp = BitmapFactory.decodeStream(   
    url.openConnection().getInputStream());

   getActivity().runOnUiThread(new Runnable() {
    public void run() {
      iv.setImageBitmap(bmp);
     }
    });
 }
 catch (Exception ex){
    Log.d(this.getClass().toString(), ex.getMessage());
 }
}
  1. 如果用户在ListFragment类的列表视图中选择了一个项目,我们需要告诉DetailFragment显示相应的详情。在ListFragment类的onCreateView方法中,添加onItemClick处理程序:
mListView.setOnItemClickListener(new 
 AdapterView.OnItemClickListener() 
{
  @Override
  public void onItemClick(AdapterView<?> adapterView,    
    View view, int i, long l) 
    {
        VideoItem video = mVideos.get(i);
        onVideoClicked(video);
    }
});
return view;
  1. MainActivity类中,添加两个静态成员,它们将代表ListFragmentDetailsFragment类的标签:
public static String TAG_LIST_FRAGMENT = "LIST";
public static String TAG_DETAILS_FRAGMENT = "DETAILS";

ListFragment类中创建onVideoClicked方法。如果DetailsFragment存在(有一个带有DETAILS标签的片段),它将调用DetailsFragmentshowDetails方法:

private void onVideoClicked(VideoItem video) {  
  DetailFragment detailsFragment = (DetailFragment)   
   getFragmentManager().findFragmentByTag(   
    MainActivity.TAG_DETAILS_FRAGMENT);
if (detailsFragment != null) { 
  detailsFragment.setVideo(video);}
}
  1. 我们快要完成了。在activity_main.xml布局中,我们为片段创建了一个容器。现在我们将添加一些代码,以在该容器中显示ListFragment的内容。在MainActivity类中,为两个片段添加两个私有成员:
private DetailFragment mDetailsFragment;
private ListFragment mListFragment;
  1. 创建ListFragment并将其添加到容器中:
mListFragment = new ListFragment();
FragmentTransaction ft =  
 getFragmentManager().beginTransaction();
ft.add(R.id.main_container_for_list_fragment, 
 mListFragment, TAG_LIST_FRAGMENT);
ft.commit();
  1. 让我们为主活动创建另一个布局,但这次是为大屏幕,比如平板电脑。在res文件夹中,通过右键单击res项目,添加一个新的 Android 资源目录。选择layout作为资源类型,将目录命名为layout-large,然后单击 To 按钮。

  2. 在新的layout-large目录中,添加一个新的布局并命名为activity_main。平板设备足够大,可以容纳我们的两个片段,因此对于此布局,我们将创建两个容器:一个用于列表,一个用于详情:

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android=  
 "http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/main_container">
<FrameLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:background="@android:color/holo_orange_light"
android:id="@+id/main_container_for_list_fragment">
</FrameLayout>
<FrameLayout
android:id="@+id/main_container_for_detail_fragment"android:background="@android:color/holo_blue_light"
android:layout_marginLeft="300dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
</FrameLayout>
  1. 修改MainActivityonCreate实现。如果容器可用,我们也将加载详情片段。将commit调用移到最后:
mListFragment = new ListFragment();
FragmentTransaction ft =  
 getFragmentManager().beginTransaction();
ft.add(R.id.main_container_for_list_fragment,  mListFragment, TAG_LIST_FRAGMENT);
if (findViewById(  
 R.id.main_container_for_detail_fragment)!= null){
  mDetailsFragment = new DetailFragment();ft.add(R.id.main_container_for_detail_fragment,  
  mDetailsFragment, TAG_DETAILS_FRAGMENT);
}
ft.commit();
  1. 还有一件事,如果你允许我解释。嗯,实际上有几件事。如果应用正在手机上运行,我们需要从列表片段视图导航到详情片段视图。修改MainActivity文件中的onVideoClicked方法,以便在那里创建详情片段:
private void onVideoClicked(VideoItem video) {
  DetailFragment detailsFragment = (DetailFragment)    
   getFragmentManager().findFragmentByTag(  
    MainActivity.TAG_DETAILS_FRAGMENT);
 if (detailsFragment != null) {
   detailsFragment.setVideo(video);
 }
 else
 {
   FragmentTransaction ft =  getFragmentManager().beginTransaction();
   detailsFragment = new DetailFragment();
   ft.add(R.id.main_container_for_list_fragment,  
    detailsFragment, MainActivity.TAG_DETAILS_FRAGMENT);
   ft.addToBackStack(MainActivity.TAG_DETAILS_FRAGMENT); 
   ft.commit();
   detailsFragment.setVideo(video);
 }
}
  1. 我们在上一步中添加的addToBackStack调用通知片段管理器所有片段都在堆栈上,因此我们可以提供导航方式。我们需要告诉我们的活动在按下返回按钮时如何行为:我们想离开活动还是我们想从堆栈中弹出一个片段?我们将覆盖MainActivityonBackPressed方法,就像这样:
@Override 
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount()>0){
        getFragmentManager().popBackStack();
    }
else {
this.finish();
    }
}

我们完成了!我们有一些工作要做,但现在我们有一个可以在具有导航的手机上运行并且如果有足够的空间将显示两个片段的应用程序,就像平板电脑一样。

为了查看差异,请在智能手机和平板电脑上运行应用程序。在手机上,它将类似于以下屏幕截图。在平板电脑上(如果您没有可用的平板电脑,可以使用 Genymotion),列表和详细信息都显示在单个视图中:

粘合在一起!

还有更多…

下一个教程将展示如何实现允许我们观看刚刚找到的视频的功能。毕竟,播放视频是我们想要的!

媒体播放

在上一个教程中,我们从 YouTube 检索了搜索结果,并在列表和详细片段中显示了它们。找到的条目代表视频,因此如果我们能够在应用程序中播放它们,那将是很好的。让我们找到一种方法来做到这一点。

由于我们知道视频 ID,因此很容易为其组合 URL 并在 web 视图中加载它们;但是,Google 为此提供了更简单的解决方案,并为此提供了 YouTube Android Player API。它有一些限制,但足够有趣。

准备工作

要完成本教程,您需要完成上一个教程,因为本教程从上一个教程结束的地方开始。虽然我建议您在物理手机和平板电脑上测试应用程序,但您当然也可以使用 Genymotion。

如果您使用虚拟设备,那么谷歌应用程序(以及 API 和播放器所依赖的 YouTube 应用程序)将丢失,并且该应用程序将因此失败。您需要首先在虚拟设备上下载并安装它们。

如何做…

让我们看看如何通过以下步骤扩展应用程序,以便为我们播放视频:

  1. developers.google.com/youtube/android/player/downloads下载 YouTube Player API。

  2. 在下载的文件中,在libs文件夹中找到YouTubeAndroidPlayerApi.jar文件并复制它。

  3. 从上一个教程中打开项目。

  4. app模块中的libs文件夹中找到libs文件夹,并粘贴YouTubeAndroidPlayerApi.jar文件。

  5. build.gradle文件中的依赖项可能已经准备好包括lib文件中的任何文件;但是如果没有,添加依赖项:

compile fileTree(dir: 'libs', include: ['YouTubeAndroidPlayerApi.jar'])
  1. 单击立即同步链接,或者如果它没有出现,请单击工具栏上的使用 Gradle 文件同步项目按钮。

  2. MainActivity类中,添加一个用于将要创建的播放器片段的静态标签。还添加YouTubePlayerFragment的私有成员和一个公共成员来存储 YouTube 播放器,如果初始化成功的话:

public static String TAG_PLAYER_FRAGMENT = "PLAYER";
private YouTubePlayerFragment mPlayerFragment;
public YouTubePlayer mYouTubePlayer = null;
  1. layout-large目录中打开activity_main.xml,将详细片段的高度更改为300dp,并将YouTubePlayerFragment添加到其中。预览可能会抱怨,因为它不知道应该如何渲染,但只要包被识别,这并不是真正的问题,如果您已成功完成步骤 5 和 6,那么包将被识别:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/main_container">
<FrameLayout
android:layout_width="300dp"android:layout_height="match_parent"android:background="@android:color/holo_orange_light"android:id="@+id/main_container_for_list_fragment"></FrameLayout>
<FrameLayout
android:id="@+id/main_container_for_detail_fragment"android:background="@android:color/holo_blue_light"android:layout_marginLeft="300dp"android:layout_width="match_parent"android:layout_height="300dp"></FrameLayout>
<fragment
android:name="com.google.android.youtube.player.YouTubePlayerFragment"
android:id="@+id/main_youtube_player_fragment"android:layout_marginTop="300dp"android:layout_marginLeft="300dp"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_weight="3"/>
</FrameLayout>
  1. onCreateView中,在ft.commit之前,找到播放器片段的容器并初始化YouTuberPlayer
mPlayerFragment = (YouTubePlayerFragment)  
 getFragmentManager().findFragmentById(
  R.id.main_youtube_player_fragment);if (mPlayerFragment != null) {
  ft.add(mPlayerFragment, TAG_PLAYER_FRAGMENT);  mPlayerFragment.initialize("Your API key", new 
   YouTubePlayer.OnInitializedListener() 
  {
   @Override 
     public void onInitializationSuccess( YouTubePlayer.Provider   
     provider, YouTubePlayer youTubePlayer, boolean isRestored) 
   {
     mYouTubePlayer = youTubePlayer;}
   @Override 
    public void onInitializationFailure(YouTubePlayer.Provider    
    provider, YouTubeInitializationResult 
     youTubeInitializationResult) {
      Log.d(this.getClass().toString(),   
       youTubeInitializationResult.toString()); 
 });
}
  1. DetailFragment中,在onCreateView方法中为播放按钮添加一个点击处理程序,就在返回视图对象之前:
view.findViewById(R.id.detail_button_play).setOnClickListener(
 new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    playVideo();}
});
  1. DetailFragment中创建playVideo方法。如果播放器片段存在(在大屏幕设备上),并且已经初始化,它将播放视频;如果不存在(在小屏幕设备上),我们将创建一个播放器片段,初始化它,并将其添加到堆栈中:
private void playVideo(){
if (getActivity() != null && 
 ((MainActivity)getActivity()).mYouTubePlayer != null){
    ((MainActivity)getActivity()  
     ).mYouTubePlayer.cueVideo(mVideo.getId());
 }
 else {
    FragmentTransaction ft =  
     getFragmentManager().beginTransaction();
    YouTubePlayerFragment playerFragment = new 
    YouTubePlayerFragment();
   ft.add(R.id.main_container_for_list_fragment,   
    playerFragment, MainActivity.TAG_DETAILS_FRAGMENT);
   ft.addToBackStack(MainActivity.TAG_PLAYER_FRAGMENT);
   ft.commit();
   playerFragment.initialize("Your API key", new 
    YouTubePlayer.OnInitializedListener() {
      @Override
     public void onInitializationSuccess(YouTubePlayer.Provider 
       provider, YouTubePlayer youTubePlayer, boolean 
       isRestored) {
         if (!isRestored) {
             youTubePlayer.cueVideo(mVideo.getId());
          }
      }
      @Override
	   public void onInitializationFailure(YouTubePlayer.Provider 
       provider, YouTubeInitializationResult 
        youTubeInitializationResult) {
        Log.d(this.getClass().toString(),   
         youTubeInitializationResult.toString()); 
      }
   });
 }
}

通过这样,我们已经添加了一个简单但完全功能的实现来播放所选视频。

还有更多...

有许多选项可用于播放视频,例如全屏或原位播放,带按钮或不带按钮等。使用 Chrome Cast,媒体也可以发送到您的电视上,或者正如我们将在最后的食谱中看到的那样,我们可以为 Android TV 创建一个应用程序。

电视和媒体中心

无聊!电视上又没有什么好看的!至少没有什么看起来足够有趣的东西。运行在 Android 上的智能电视为开发者创造了一个全新有趣的世界。最终,我们得到了我们应得的屏幕尺寸!

然而,它也拥有不同类型的受众。用户与他们的手机和平板电脑的互动程度非常高。当涉及观看电视时,焦点更多地放在消费上。

好吧,电视上有什么?泡杯茶,开始观看节目。偶尔,用户可能对一些互动感兴趣(这种现象大多出现在第二屏应用程序中,因为并非每个人都拥有智能电视),但大多数时候,电视观众只是想靠在椅子上放松。

准备工作

这个食谱需要 Android Studio 正常运行和安装最新的 SDK。在这个食谱中,我们将为您提供一个关于电视应用程序的简要介绍。只需几个步骤,我们就可以创建一个媒体中心应用程序。不用担心,您不需要拥有 Android 电视。我们将创建一个虚拟的电视。

如何做...

让我们看看开发 Android TV 应用程序需要做什么:

  1. 在 Android Studio 中创建一个新项目。将其命名为PersonalTeeVee,然后点击“下一步”按钮。

  2. 选择电视选项,然后点击“下一步”按钮。

  3. 选择 Android TV Activity,然后点击下一步。

  4. 在“Activity Name”字段中输入TeeVeeActivity,在“Title”字段中输入Personal Tee Vee,然后点击“完成”按钮。

  5. Android Studio 为您创建了一个手机和一个电视模块。将配置更改为电视。您将看到如下图所示的内容:如何做...

  6. 查看电视模块中的AndroidManifest.xml文件。注意“lean back”功能要求(告诉我们这是一个全屏体验的电视应用程序,没有任何重型互动,基本上是关于消费内容,比如观看视频)。还要注意我们不需要触摸屏。电视屏幕离得太远了,无法触摸。此外,没有人喜欢电视屏幕上的污渍:

<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="true" />
  1. 要测试电视应用程序,我们需要有一个虚拟电视设备。从“工具”|“Android”菜单中打开“AVD 管理器”选项。

  2. 点击“创建虚拟设备”按钮。

  3. 从类别列表中选择电视,并选择一个电视设备(1080p 或更高)。然后点击“下一步”按钮。

  4. 选择一个系统镜像。例如,我选择了API 级别 22 x86。点击“下一步”。

  5. 修改 AVD 的名称为您认为最合适的名称,然后点击“完成”按钮。将为您创建一个新的虚拟电视设备。

  6. 点击播放按钮启动您的电视设备。如果它说Google Play 服务已停止,您现在可以忽略这条消息(尽管如果您想播放视频,您将需要它)。

  7. 一旦设备启动,从 Android Studio 运行您的电视应用程序。默认情况下,它看起来像这样:如何做...

哇,这已经是一个完全功能的媒体中心应用程序了!

这只是一个简短的介绍,介绍了如何构建 Android TV 应用程序。玩玩它,调整一下。

还有更多...

虽然这个食谱中的应用程序专门用于电视,但我认为您没有理由不能将其制作成任何类型的设备的应用程序:手机、平板电脑和电视。如果您愿意,您可以将本章中的所有食谱合并为一个单一的应用程序。这是一个不错的挑战,不是吗?

除了 YouTube 之外,还有一些有趣的与媒体相关的 API 可以调查。例如,在www.programmableweb.com上,你可以找到一些有趣的 API。以下是其中一些:

API 导航
YouTube http://www.programmableweb.com/api/youtube-live-streaming
Vimeo http://www.programmableweb.com/api/vimeo
Hey! Spread http://www.programmableweb.com/api/heyspread
Pirateplay http://www.programmableweb.com/api/pirateplay
Tinysong http://www.programmableweb.com/api/tinysong
TwitVid http://www.programmableweb.com/api/twitvid

现在我们知道从哪里获取媒体项目,如何播放它们,以及如何自动创建媒体中心应用程序了。

接下来:让我们通过捕捉一些图像来创建一些媒体。下一章见!

另请参阅

  • 第六章,捕捉和分享

第六章:捕捉和分享

我们喜欢与他人分享我们生活的世界,所以我们将使用我们的智能手机拍摄我们关心的所有事物和所有人的图像或视频。在 Android 上,这相当容易。

在本章中,你将学习以下内容:

  • 以简单的方式捕捉图像

  • 使用 Camera2 API 进行图像捕捉

  • 图像分享

  • 方向问题

介绍

作为开发者,你可以启动一个意图,获取数据,并对其进行任何你想要的操作。

如果你想自己处理图像或视频捕捉,事情会变得有点复杂。那么,为什么有人要这样做呢?这给了我们更多的灵活性,以处理相机的预览、过滤或处理方式。

从 Android Lollipop 开始,我们一直在使用的旧相机 API 已被 Camera2 API 取代,这被证明是一个巨大的改进。不幸的是,一些方向问题仍然存在,主要是由于 Android 硬件和软件的大碎片化。在一些设备上,捕获的图像似乎被旋转了 90 度。为什么会这样?你将在本章的最后一个配方中找到答案。

以简单的方式捕捉图像

当然,在 Android 上有许多拍照或录像的方式。捕捉图像的最简单方式是使用意图启动相机应用程序,并在拍摄完成后获取结果。

准备工作

对于这个配方,你只需要运行 Android Studio。

如何做...

启动相机意图通常是这样的:

  1. 在 Android Studio 中,创建一个新项目。

  2. activity_main.xml布局中,添加一个新按钮和一个图像视图。将图像视图命名为image

  3. 为该按钮创建一个点击处理程序。

  4. 从事件处理程序实现中调用takePicture方法。

  5. 实现takePicture方法。如果设备支持,启动捕捉意图:

static final int REQUEST_IMAGE_CAPTURE = 1;
private void takePicture() {
  Intent captureIntent = new  
    Intent(MediaStore.ACTION_IMAGE_CAPTURE);
  if (captureIntent.resolveActivity(  
   getPackageManager()) != null) {
    startActivityForResult(captureIntent,   
       REQUEST_IMAGE_CAPTURE);
   }
}
  1. 重写onActivityResult方法。你将从返回的数据中获取缩略图,并在图像视图中显示结果:
@Override 
  protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
   if (requestCode == REQUEST_IMAGE_CAPTURE &&resultCode == RESULT_OK) {     
        Bundle extras = data.getExtras();
        Bitmap thumbBitmap = (Bitmap)  
         extras.get("data");");
         ((ImageView)findViewById(R.id.image) 
         ).setImageBitmap(thumbBitmap);
    }
}

这是捕捉图像的最简单方式,也许你以前已经这样做过了。

还有更多...

如果你想在自己的应用程序中预览图像,还有更多工作要做。Camera2 API 可用于预览、捕捉和编码。

在 Camera2 API 中,你会找到诸如CameraManagerCameraDeviceCaptureRequestCameraCaptureSession之类的组件。

以下是最重要的 Camera2 API 类:

目标
CameraManager 选择相机,创建相机设备
CameraDevice 创建 CaptureRequestCameraCaptureSession
CaptureRequest, CameraBuilder 链接到表面视图(预览)
CameraCaptureSession 捕捉图像并在表面视图上显示

我们将在下一个配方“图像捕捉”中调查的示例可能一开始看起来有点令人困惑。这主要是因为设置过程需要许多步骤,大部分将以异步方式执行。但不要担心,我们将逐步调查它。

使用 Camera2 API 进行图像捕捉

让我们与我们所爱的人分享我们周围的世界。一切都始于预览和捕捉。这就是这个配方的全部内容。我们还将回到那些旧日的照片是棕褐色调的好日子。

有许多应用程序,比如 Instagram,提供了添加滤镜或效果到你的照片的选项。如果棕褐色是过滤和分享照片的唯一选项,会发生什么?也许我们可以设置一个趋势。#每个人都喜欢棕褐色!

我们将使用 Camera2 API 来捕捉图像,基于 Google 在 GitHub 上提供的 Camera2 Basic 示例。作为配方步骤的参考,你可以查看以下类图。它将清楚地显示我们正在处理的类以及它们之间的交互方式:

使用 Camera2 API 进行图像捕捉

我们将调查其中的具体情况,一旦您找出了问题所在,我们将通过使预览和捕获的图像呈现为棕褐色(或者,如果您愿意,可以选择其他效果)来为其添加一些我们自己的东西。

准备工作

对于这个示例,我们将使用 Camera2 API。由于我们将使用此 API,您需要使用运行 Android 5.0 或更高版本(推荐)的真实设备,或者您需要创建一个虚拟设备。

操作步骤...

让我们看看如何快速上手。Google 已经为我们准备了一个整洁的示例:

  1. 在 Android Studio 中,从启动向导中选择导入 Android 代码示例,或者在文件菜单上选择导入示例

  2. 在下一个对话框中,您将看到许多有趣的示例应用程序,展示了各种 Android 功能。选择Camera2 Basic示例,然后点击Next按钮:操作步骤...

  3. 将项目命名为EverybodyLovesSepia,然后点击Finish按钮。

注意

如果点击按钮后什么都没有发生(由于 Android Studio 的某些版本中存在的错误),请再试一次,但这次保持项目名称不变。

  1. Android Studio 将为您从 GitHub 获取示例项目。您可以在github.com/googlesamples/android-Camera2Basic找到它。

  2. 在设备上或虚拟设备上运行应用程序。

注意

如果您正在使用 Genymotion 上运行的虚拟设备,请首先通过单击右侧的相机图标,打开相机开关,并选择(网络)相机来启用相机。

在应用程序中,您将看到相机的预览,如下截图所示:

操作步骤...

许多事情又自动发生了!这个 Camera2 API 示例中有什么?需要什么来捕获图像?实际上,需要相当多的东西。打开Camera2BasicFragment类。这就是大部分魔术发生的地方。

折叠所有方法

为了创建一个不那么压倒性的视图,折叠所有方法:

  1. 您可以通过从Code菜单中选择Folding选项来做到这一点。在子菜单中,选择Collapse all

  2. 您还会在此子菜单中找到其他选项;例如,展开所有方法或展开(仅展开所选方法)。

提示

使用快捷键Cmd后跟+Cmd后跟(或者Ctrl后跟+Ctrl后跟对于 Windows)来展开或折叠一个方法。使用快捷键Cmd + Shift后跟+Cmd + Shift后跟Ctrl + Shift+Shift + Ctrl对于 Windows)来展开或折叠类中的所有方法。

  1. 展开onViewCreated方法。在这里,我们看到了mTextureView的初始化,它是对自定义小部件AutoFitTextureView的引用。它将显示相机预览。

  2. 接下来,展开onResume方法。最初,这是设置SurfaceTextureListener类的地方。正如示例中的注释已经建议的那样,这允许我们在尝试打开相机之前等待表面准备就绪。双击mSurfaceTextureListener,使用快捷键Cmd + B(对于 Windows,是Ctrl + B)跳转到其声明,看看这是怎么回事。

  3. 完全展开mSurfaceTextureListener的初始化。就像活动一样,纹理视图也有一个生命周期。事件在这里被处理。目前,这里最有趣的是onSurfaceTextureAvailable事件。一旦表面可用,将调用openCamera方法。双击它并跳转到它。

  4. openCamera方法中发生了许多事情。调用了setUpCameraOutputs方法。此方法将通过设置私有成员mCameraId和图像的(预览)大小来处理要使用的相机(如果有多个)。这对于每种类型的设备可能是不同的。它还会处理宽高比。几乎任何设备都支持 4:3 的宽高比,但许多设备也支持 16:9 或其他宽高比。

注意

大多数设备都有一到两个摄像头。有些只有一个后置摄像头,有些只有一个前置摄像头。前置摄像头通常支持较少的图像尺寸和宽高比。

另外,随着 Android Marshmallow(Android 6.0)带来的新权限策略,您的应用程序可能根本不被允许使用任何摄像头。这意味着您始终需要测试您的应用程序是否可以使用摄像头功能。如果不能,您将需要通过显示对话框或 toast 向用户提供一些反馈。

  1. 接下来,让我们看一下openCamera方法中的以下行。它说要打开setCameraOutputs方法为我们选择的相机:
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
  1. 它还提供了一个mStateCallback参数。如果您双击它并跳转到它,您可以看到它的声明。这里的事情再次是异步发生的。

  2. 一旦相机被打开,预览会话将会开始。让我们跳转到createCameraPreviewSession方法。

  3. 看一下mCameraDevice.createCaptureSession。进入该方法的一个参数是捕获会话状态回调。它用于确定会话是否成功配置,以便可以显示预览。

  4. 现在,需要做什么来拍照?找到onClick方法。您会注意到调用takePicture方法。跳转到它。takePicture方法又调用lockFocus方法。跳转到它。

  5. 拍照涉及几个步骤。相机的焦点必须被锁定。接下来,需要创建一个新的捕获请求并调用capture方法:

mCaptureSession.capture(mPreviewRequestBuilder.build(),  
 mCaptureCallback, mBackgroundHandler);
  1. 进入capture方法的一个参数是mCaptureCallback。使用Cmd + B(或 Windows 的Ctrl + B)跳转到它的声明。

  2. 您会注意到两个方法:onCaptureProgressedonCaptureCompleted。它们都调用私有方法process并将结果或部分结果传递给它。

  3. process方法将根据各种可能的状态而有所不同。最后,它将调用captureStillPicture方法。使用Cmd + B(或 Windows 的Ctrl + B)跳转到它的声明。

  4. captureStillPicture方法初始化了一个CaptureRequest.Builder类,用于拍照并以正确的属性存储照片,例如方向信息。一旦捕获完成并且文件已保存,相机焦点将被解锁,并通过 toast 通知用户:

CameraCaptureSession.CaptureCallback CaptureCallback= new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureCompleted 
     (CameraCaptureSession session, 
         CaptureRequest request, TotalCaptureResult  
          result) {
           showToast("Saved: " + mFile);
          unlockFocus();
       }
};

前面的步骤向您展示了基本的 Camera2 示例应用程序的亮点。为了在您的应用程序中拍照,需要做相当多的工作!如果您不需要在应用程序中进行预览,您可能希望考虑使用意图来拍照。但是,拥有自己的预览可以为您提供更多的控制和效果的灵活性。

添加深褐色效果

我们将在预览中添加一个深褐色效果,只是因为它看起来很酷(当然,一切在早期都更好),使用以下步骤:

  1. 转到createCameraPreviewSession方法,并在相机捕获会话状态回调实现的onConfigured类内部,在设置autofocus参数之前添加这一行:
mPreviewRequestBuilder.set(
 CaptureRequest.CONTROL_EFFECT_MODE,  
  CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);
  1. 如果您现在运行您的应用程序,您的预览将是深褐色。但是,如果您按下按钮来捕获图像,它将不会产生这种效果。在onCaptureStillPicture方法中,您将不得不做同样的事情。在设置autofocus参数的行的上面添加这一行:
captureBuilder.set(   
 CaptureRequest.CONTROL_EFFECT_MODE,  
  CaptureRequest.CONTROL_EFFECT_MODE_SEPIA);

再次运行您的应用程序,捕捉一张图像,并使用 Astro 应用程序(或其他文件浏览器应用程序)找到捕捉的文件。您可以在Android/data/com.example.android.camera2basic找到它(显然,如果您接受了建议的包名称,否则路径将包括您提供的包名称)。它是泛黄的!

如果您愿意,您还可以尝试一些其他可用效果的负面实验,这也很有趣,至少有一段时间。

目前就是这样。我们还没有做太多的编程,但我们已经看了一些有趣的代码片段。在下一个教程中,我们将在 Facebook 上分享我们捕捉的图像。

还有更多...

欲了解更多信息,请访问 GitHub github.com/googlesamples/android-Camera2Basic 和 Google Camera2 API 参考 developer.android.com/reference/android/hardware/camera2/package-summary.html

您可以在github.com/ChristianBecker/Camera2Basic找到一个有趣的 Camera2 API 示例的分支,支持 QR 码扫描。

图像分享

图像捕捉如果没有分享图像的能力就不好玩;例如,在 Facebook 上。我们将使用 Facebook SDK 来实现这一点。

挑战!如果您正在构建一个在 Parse 后端上运行的应用程序,就像我们在第二章中所做的那样,云端后端的应用程序,那就没有必要了,因为 Facebook SDK 已经在其中了。如果您愿意,您可以将第二章的教程与本教程结合起来,快速创建一个真正酷的应用程序!

准备工作

对于这个教程,您需要成功完成上一个教程,并且需要有一个真正的 Android 设备(或虚拟设备,但这将需要一些额外的步骤)。

您还需要一个 Facebook 账户,或者您可以只为测试目的创建一个。

操作步骤...

让我们看看如何在 Facebook 上分享我们的泛黄捕捉的图像:

  1. 从上一个教程中获取代码。打开app文件夹中的build.gradle文件。在dependencies部分添加一个新的依赖项,并在添加了这行代码后点击立即同步链接:
compile 'com.facebook.android:facebook-android-sdk:4.1.0'

  1. 要获取 Facebook 应用程序 ID,请浏览developers.facebook.com(是的,这需要一个 Facebook 账户)。从MyApps菜单中,选择添加新应用,选择Android作为您的平台,输入您的应用名称,然后点击创建新的 Facebook 应用程序 ID。选择一个类别-例如,娱乐-然后点击创建应用程序 ID

  2. 您的应用程序将被创建,并显示一个快速入门页面。向下滚动到告诉我们关于您的 Android 项目部分。在包名称默认活动类名称字段中输入详细信息,然后点击下一步按钮。

  3. 将显示一个弹出警告。您可以放心地忽略警告,然后点击使用此包名称按钮。Facebook 将开始思考,一段时间后添加您的开发和发布密钥哈希部分将出现。

  4. 要获取开发密钥哈希,打开终端应用程序(在 Windows 中,启动命令提示符)并输入以下内容:

keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore | openssl sha1 -binary | openssl base64

提示

如果提示输入密钥库密码,请输入android,这应该就可以了 - 除非您之前已更改了密码。

  1. 点击Enter,复制显示的值,并粘贴到 Facebook 网页的开发密钥哈希中。点击下一步按钮继续。

  2. 下一步部分,点击跳转到开发者仪表板按钮。它会直接带你到你需要的信息,即应用 ID。复制应用 ID字段中的值:操作步骤...

  3. 接下来,初始化 Facebook SDK。打开CameraActivity类,在onCreate方法中,在super.OnCreate行后添加以下行。使用Alt + Enter快捷键导入所需的包com.facebook.FacebookSdk

FacebookSdk.sdkInitialize(getApplicationContext());
  1. 现在我们需要告诉应用关于 Facebook 应用 ID 的信息。打开res/values文件夹中的strings.xml文件。添加一个包含你的 Facebook 应用 ID 的新字符串:
<string name="facebook_app_id">Your facebook app id</string>
  1. 打开AndroidManifest.xml文件。

  2. application元素中添加一个元数据元素:

<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
  1. manifest文件中添加一个FacebookActivity声明:
<activity android:name="com.facebook.FacebookActivity"android:configChanges="keyboard|keyboardHidden|screenLayout|   
   screenSize|orientation"
  android:theme="@android:style/Theme.Translucent.
   NoTitleBar"
  android:label="@string/app_name" />
  1. Camera2BasicFragment类中,找到captureStillPicture方法。在onCaptureCompleted回调实现的末尾添加一个新的调用,就在unlockFocus类后面:
sharePictureOnFacebook();
  1. 最后,在manifest文件中的application部分添加一个提供者,这将允许你在 Facebook 上分享图片。下一章将讨论内容提供者。现在只需在authoritiesFaceBookContentProvider末尾添加你的应用 ID,替换示例中的零:
<provider android:authorities="com.facebook.app. 
  FacebookContentProvider000000000000"android:name="com.facebook.FacebookContentProvider"android:exported="true" />
  1. 实现sharePictureOnFacebook方法。我们将从文件中加载位图。在真实的应用中,我们需要计算inSampleSize的所需值,但为了简单起见,我们在这里只使用固定的inSampleSize设置为4。在大多数设备上,这将足以避免其他情况下可能发生的任何OutOfMemory异常。此外,我们将在拍照后显示的share对话框中添加照片:
private void sharePictureOnFacebook(){
    final BitmapFactory.Options options = new  
     BitmapFactory.Options();
    options.inJustDecodeBounds = false;
    options.inSampleSize = 4;
    Bitmap bitmap =  
     BitmapFactory.decodeFile(mFile.getPath(), options); 
    SharePhoto photo = new  
    SharePhoto.Builder().setBitmap(bitmap).build();
    SharePhotoContent content = new  
    SharePhotoContent.Builder().addPhoto(photo).build();
    ShareDialog.show(getActivity(), content);
}
  1. 为了安全起见,我们希望为每张图片创建一个唯一的文件名。修改onActivityCreated方法以实现这一点:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mFile = new 
    File(getActivity().getExternalFilesDir(null),  
      "pic"+ new Date().getTime()+".jpg");
}
  1. 在你的 Facebook 时间轴上,页面会显示如下。这里是用荷兰语显示的:操作步骤...

  2. 运行应用程序,在你自己的 Facebook 时间轴上分享一些棕褐色的图片!

我们的应用已经完全可用,尽管可能需要一些调整。在我的三星设备上,我以竖屏模式拍摄的所有图像都旋转了 90 度。这有点太艺术了。让我们在下一个示例中修复它!

方向问题

在一些设备上(如三星设备),以竖屏模式捕获的图像会旋转 90 度;而在其他设备上(如 Nexus 设备),情况似乎很好。例如,如果你使用 Astro 应用查看文件,你可能不会注意到这一点,但如果你在 Facebook 的share对话框中预览,你就会注意到。

这是许多 Android 开发者都面临的一个众所周知的挑战。图像可能包含有关旋转角度的元数据,但显然并不是每个应用都尊重这些元数据。最好的解决方案是什么?每次显示图像时都应该旋转图像吗?应该旋转位图本身,这可能非常耗时和占用处理器吗?

做好准备

对于这个示例,你需要成功完成之前的示例。最好如果你有多个 Android 设备来测试你的应用。否则,如果你至少有一台三星设备可用,那就太好了,因为这个品牌的大多数(如果不是全部)型号都可以重现方向问题。

操作步骤

让我们看看如果出现这个方向问题,你如何解决它:

  1. 在 Facebook 的share对话框中,预览图像会旋转 90 度(在一些设备上),如下所示:操作步骤...

  2. 这看起来不像我生活的世界。在我的三星 Galaxy Note 3 设备上是这样的,但在我的 Nexus 5 设备上不是。显然,三星将图片存储为从横向角度看的样子,然后向其中添加元数据以指示图像已经旋转(与默认方向相比)。然而,如果你想在 Facebook 上分享它,事情就会出错,因为元数据中的方向信息没有得到尊重。

  3. 因此,我们需要检查元数据,并找出其中是否有旋转信息。添加getRotationFromMetaData方法:

private int getRotationFromMetaData(){
   try {
      ExifInterface exif = new 
      ExifInterface(mFile.getAbsolutePath());
      int orientation = exif.getAttributeInt(
       ExifInterface.TAG_ORIENTATION,
        ExifInterface.ORIENTATION_NORMAL);
      switch (orientation) {
		  case ExifInterface.ORIENTATION_ROTATE_270:
                return 270;
          case ExifInterface.ORIENTATION_ROTATE_180:
                return 180;case ExifInterface.ORIENTATION_ROTATE_90:
                return 90;
          default:
                return 0;
      }
   }
   catch (IOException ex){
       return 0;
   }
}
  1. 如果需要,您必须在显示共享预览之前旋转位图。这就是rotateCaptureImageIfNeeded方法的用处。

在这里,我们可以安全地在内存中旋转位图,因为inSampleSet值为4。如果旋转原始全尺寸位图,很可能会耗尽内存。无论哪种方式,都会耗费时间,并导致捕获图像和显示共享预览对话框之间的延迟:

private Bitmap rotateCapturedImageIfNeeded(Bitmap bitmap){
    int rotate = getRotationFromMetaData();
    Matrix matrix = new Matrix();
    matrix.postRotate(rotate);
    bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
     bitmap.getHeight(), matrix, true);
    Bitmap mutableBitmap = bitmap.copy(Bitmap.Config.ARGB_8888,  
     true);
   return mutableBitmap;
}
  1. 然后,在sharePictureOnFacebook方法中,在使用BitmapFactory类检索位图后,调用onRotateCaptureImageIfNeeded方法,并将位图作为参数传递:
bitmap = rotateCapturedImageIfNeeded(bitmap);

  1. 如果再次运行应用程序,您会发现在纵向模式下一切都很好:如何做...

这些东西很容易实现,并且会提高您的应用程序的质量,尽管有时它们也会让您感到困惑,让您想知道为什么一个解决方案不能在任何设备上都正常工作。现在一切看起来都很好,但在平板电脑或华为、LG 或 HTC 设备上会是什么样子呢?没有什么是不能解决的,但由于您没有一堆 Android 设备(或者也许您有),测试是困难的。

尽可能在尽可能多的设备上测试您的应用程序总是一件好事。考虑使用远程测试服务,例如 TestDroid。您可以在www.testdroid.com找到他们的网站。在第八章中,将讨论这些和其他主题,但首先我们将在即将到来的章节中看一下可观察对象和内容提供程序。

还有更多...

拍摄视频更有趣。还有一个用于视频捕获的 Camera2 API 示例可用。您也可以通过导入示例选项来检查示例项目。

另请参阅

  • 第八章, 提高质量

第七章:内容提供程序和观察者

在大多数应用程序中,我们需要持久化数据,并经常使用 SQLite 来实现这一目的。

非常常见的情况是列表和详细视图。通过使用内容提供程序,我们不仅提供了应用程序之间的通信方式,还在我们自己的应用程序中节省了大量工作。

在本章中,您将学习:

  • 内容提供程序

  • 使用内容提供程序消耗和更新数据

  • 将投影更改为在您的应用程序中显示关键绩效指标KPIs

  • 使用内容提供程序与其他应用程序进行通信

介绍

如果我们想要创建一个新的行,或者想要编辑数据库中的一行,应用程序将显示包含详细信息的片段或活动,用户可以在那里输入或修改一些文本和其他值。一旦记录被插入或更新,列表需要知道这些变化。告诉列表活动或片段有关这些变化并不难做到,但有一种更优雅的方法可以实现这一点。为此,以及其他我们将在以后了解的原因,我们将研究内容提供程序的内容。

Android 内容提供程序框架允许我们为应用程序创建更好的设计。其中一个特点是它允许我们注意到某些数据已经发生了变化。即使在不同的应用程序之间也可以工作。

内容提供程序

构建内容提供程序是一件非常聪明的事情。内容提供程序 API 具有一个有趣的功能,允许应用程序观察数据集的变化。

内容提供程序将一个进程中的数据与另一个进程中运行的代码连接起来,甚至可以在两个完全不同的应用程序之间进行连接。如果您曾经编写过从 Gallery 应用中选择图像的代码,您可能已经经历过这种行为。某些组件操作其他组件依赖的持久数据集。内容提供程序可以使用许多不同的方式来存储数据,可以存储在数据库中,文件中,甚至可以通过网络进行存储。

数据集由唯一的 URI 标识,因此可以要求在某个 URI 发生变化时进行通知。这就是观察者模式的应用之处。

观察者模式是一种常见的软件设计模式,其中一个对象(主题)具有一个或多个依赖对象(观察者,也称为监听器),它们将自动被通知任何状态更改。

还有更多...

设计模式

要了解更多关于这个和其他面向对象OO)设计模式,您可以查看www.oodesign.com/observer-pattern.html

RxJava

RxJava 是一个非常有趣的库,也可以在 Android 版本中使用。响应式编程与观察者模式有主要相似之处。响应式代码的基本构建块也是可观察对象和订阅者。

要了解更多关于 Rx 和 RxJava,您可以访问这些网站:

另请参阅

  • 第八章 ,提高质量

使用内容提供程序消耗和更新数据 - 每日想法

为了演示如何创建和使用内容提供程序,我们将创建一个应用程序,用于存储您每天的想法和快乐程度。

是的,有一些应用程序正在这样做;但是,如果您想创建一个应用程序来记录体育笔记和分数,可以随意修改代码,因为它基本上涉及相同的功能。

在这个示例中,我们将使用内容提供程序存储新的想法并检索它们。对于应用程序的各个元素,我们将使用片段,因为它们将清楚地展示观察者模式的效果。

准备工作

对于这个配方,您只需要运行 Android Studio 并拥有一个物理或虚拟的 Android 设备。

如何做...

让我们看看如何使用内容提供程序设置项目。我们将使用导航抽屉模板:

  1. 在 Android Studio 中创建一个名为DailyThoughts的新项目。点击下一步按钮。

  2. 选择手机和平板电脑选项,然后点击下一步按钮。

  3. 选择导航抽屉活动,然后点击下一步按钮。

  4. 接受自定义活动页面上的所有值,然后点击完成按钮。

  5. 打开res/values文件夹中的strings.xml文件。修改以title_section开头的条目的字符串。用我们应用程序所需的菜单项替换它们。还替换action_sample字符串:

<string name="title_section_daily_notes">Daily  
 thoughts</string><string name="title_section_note_list">Thoughts 
 list</string>
<string name="action_add">Add thought</string>
  1. 打开NavigationDrawerFragment文件,在onCreate方法中,相应地修改适配器的字符串:
mDrawerListView.setAdapter(new ArrayAdapter<String>(
        getActionBar().getThemedContext(),
        android.R.layout.simple_list_item_activated_1,
        android.R.id.text1,
        new String[]{
                getString(R.string.title_section_daily_notes),
                getString(R.string.title_section_note_list)
        }));
  1. 在同一个类中,在onOptionsItemSelected方法中,删除显示 toast 的第二个if语句。我们不需要它。

  2. res/menu文件夹中打开main.xml。删除设置项,并修改第一项,使其使用action_add字符串。还重命名它的 ID 并为其添加一个漂亮的图标:

<menu xmlns:android= 
 "http://schemas.android.com/apk/res/android"  
   tools:context=".MainActivity">
<item android:id="@+id/action_add"  
 android:title="@string/action_add"android:icon="@android:drawable/ic_input_add"android:showAsAction="withText|ifRoom" />
</menu>
  1. MainActivity文件中,在onSectionAttached部分,为不同的选项应用正确的字符串:
public void onSectionAttached(int number) {
    switch (number) {
        case 0:
            mTitle = getString(  
             R.string.title_section_daily_notes);
            break;
        case 1:
            mTitle = getString( 
             R.string.title_section_note_list);
             break;
    }
}
  1. 创建一个名为db的新包。在这个包中,创建一个名为DatabaseHelper的新类,它继承SQLiteOpenHelper类。它将帮助我们为我们的应用程序创建一个新的数据库。它将只包含一个表:thoughts。每个Thought table将有一个 id,一个名称和一个幸福评分:
public class DatabaseHelper extends SQLiteOpenHelper {
    public static final String DATABASE_NAME = 
     "DAILY_THOUGHTS";
    public static final String THOUGHTS_TABLE_NAME =   
     "thoughts";
    static final int DATABASE_VERSION = 1;
    static final String CREATE_DB_TABLE =
      " CREATE TABLE " + THOUGHTS_TABLE_NAME +
      " (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +" name TEXT NOT NULL, " +" happiness INT NOT NULL);";public DatabaseHelper(Context context){
        super(context, DATABASE_NAME, null, 
         DATABASE_VERSION);}
    @Override
    public void onCreate(SQLiteDatabase db)
    {
        db.execSQL(CREATE_DB_TABLE);
    }
    @Override 
	 public void onUpgrade(SQLiteDatabase db, int 
     oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " +  
         THOUGHTS_TABLE_NAME);
        onCreate(db);}
}
  1. 创建另一个包并命名为providers。在这个包中,创建一个名为ThoughtsProvider的新类。这将是我们所有日常想法的内容提供程序。将其作为ContentProvider类的后代。

  2. 代码菜单中,选择实现方法选项。在出现的对话框中,所有可用的方法都被选中。接受这个建议,然后点击确定按钮。您的新类将扩展这些方法。

  3. 在类的顶部,我们将创建一些静态变量:

static final String PROVIDER_NAME =  
 "com.packt.dailythoughts";
static final String URL = "content://" + PROVIDER_NAME +  
 "/thoughts";
public static final Uri CONTENT_URI = Uri.parse(URL);
public static final String THOUGHTS_ID = "_id";
public static final String THOUGHTS_NAME = "name";
public static final String THOUGHTS_HAPPINESS = 
 "happiness";
static final int THOUGHTS = 1;
static final int THOUGHT_ID = 2;
static final UriMatcher uriMatcher;
static{
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(PROVIDER_NAME, "thoughts", 
     THOUGHTS);
    uriMatcher.addURI(PROVIDER_NAME, "thoughts/#",   
     THOUGHT_ID);
}
  1. 添加一个私有成员db,引用SQLiteDatabase类,并修改onCreate方法。我们创建一个新的数据库助手:
private SQLiteDatabase db;
@Override 
   public boolean onCreate() {
    Context context = getContext();
    DatabaseHelper dbHelper = new DatabaseHelper(context);
    db = dbHelper.getWritableDatabase();
    return (db == null)? false:true;
}

查询

接下来,实现query方法。查询返回一个游标对象。游标表示查询的结果,并指向查询结果中的一个,因此结果可以被高效地缓冲,因为它不需要将数据加载到内存中:

private static HashMap<String, String> 
 THOUGHTS_PROJECTION; 
@Override 
public Cursor query(Uri uri, String[] projection, 
 String selection, String[] selectionArgs, String 
  sortOrder) {
   SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
   builder.setTables( 
    DatabaseHelper.THOUGHTS_TABLE_NAME);
   switch (uriMatcher.match(uri)) {
      case THOUGHTS:
        builder.setProjectionMap(
         THOUGHTS_PROJECTION);
         break;
      case THOUGHT_ID:
        builder.appendWhere( THOUGHTS_ID + "=" + uri.getPathSegments().get(1));
        break;
      default:
        throw new IllegalArgumentException(
         "Unknown URI: " + uri);
    }
    if (sortOrder == null || sortOrder == ""){
        sortOrder = THOUGHTS_NAME;
    }
    Cursor c = builder.query(db, projection,selection, selectionArgs,null, null, sortOrder);
    c.setNotificationUri(    
     getContext().getContentResolver(), uri);
    return c;
}

注意

setNotificationUri调用注册指令以监视内容 URI 的更改。

我们将使用以下步骤实现其他方法:

  1. 实现getType方法。dir目录表示我们想要获取所有的想法记录。item术语表示我们正在寻找特定的想法:
@Override 
public String getType(Uri uri) {
    switch (uriMatcher.match(uri)){
      case THOUGHTS:
        return "vnd.android.cursor.dir/vnd.df.thoughts";
     case THOUGHT_ID:
       return "vnd.android.cursor.item/vnd.df.thoughts";
     default:
       throw new IllegalArgumentException(
        "Unsupported URI: " + uri);
    }
}
  1. 实现insert方法。它将基于提供的值创建一个新记录,如果成功,我们将收到通知:
@Override
public Uri insert(Uri uri, ContentValues values) {
   long rowID = db.insert(  
    DatabaseHelper.THOUGHTS_TABLE_NAME , "", values);
   if (rowID > 0)
   {
      Uri _uri = ContentUris.withAppendedId(CONTENT_URI, 
       rowID);
      getContext().getContentResolver().notifyChange( _uri, 
       null);
      return _uri;
    }
    throw new SQLException("Failed to add record: " + uri);
}
  1. deleteupdate方法超出了本配方的范围,所以我们现在不会实现它们。挑战:在这里添加您自己的实现。

  2. 打开AndroidManifest.xml文件,并在application标签内添加provider标签:

<providerandroid:name=".providers.ThoughtsProvider"android:authorities="com.packt.dailythoughts"android:readPermission=  
     "com.packt.dailythoughts.READ_DATABASE"android:exported="true" />

注意

出于安全原因,在大多数情况下,您应该将导出属性的值设置为false。我们将此属性的值设置为true的原因是,稍后我们将创建另一个应用程序,该应用程序将能够从此应用程序中读取内容。

  1. 添加其他应用程序读取数据的权限。我们将在最后一个配方中使用它。将其添加到application标签之外:
<permission   
 android:name="com.packt.dailythoughts.READ_DATABASE"android:protectionLevel="normal"/>
  1. 打开strings.xml文件并向其中添加新的字符串:
<string name="my_thoughts">My thoughts</string>
<string name="save">Save</string>
<string name="average_happiness">Average 
  happiness</string>
  1. 创建两个新的布局文件:fragment_thoughts.xml用于我们的想法列表和fragment_thoughts_detail用于输入新的想法。

  2. fragment_thoughts.xml定义布局。 一个ListView小部件很适合显示所有的想法:

<?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"
    android:orientation="vertical" >
    <ListView
        android:id="@+id/thoughts_list"android:layout_width="match_parent"android:layout_height="wrap_content" ></ListView>
</LinearLayout> 
  1. fragment_thoughts_detail.xml的布局将包含EditTextRatingBar小部件,以便我们可以输入我们的想法和我们当前的幸福程度:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=
  "http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_gravity="center"android:layout_margin="32dp"android:padding="16dp"android:layout_width="match_parent"android:background="@android:color/holo_green_light"android:layout_height="wrap_content">
    <TextView
        android:layout_margin="8dp"android:textSize="16sp"android:text="@string/my_thoughts"
     android:layout_width="match_parent"android:layout_height="wrap_content" />
    <EditText
        android:id="@+id/thoughts_edit_thoughts"android:layout_margin="8dp"android:layout_width="match_parent"android:layout_height="wrap_content" />
    <RatingBar
        android:id="@+id/thoughs_rating_bar_happy"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal"android:clickable="true"android:numStars="5"android:rating="0" />
    <Button
        android:id="@+id/thoughts_detail_button"android:text="@string/save"          
        android:layout_width="match_parent"android:layout_height="wrap_content" />
</LinearLayout>
  1. 还要为想法列表中的行创建布局。将其命名为adapter_thought.xml。添加文本视图以显示 ID、标题或名称和评分:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android=
  "http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_gravity="center"
    android:layout_margin="32dp"
    android:padding="16dp"
    android:layout_width="match_parent"
    android:background=
     "@android:color/holo_green_light"
    android:layout_height="wrap_content">
    <TextView
        android:layout_margin="8dp"
        android:textSize="16sp"
        android:text="@string/my_thoughts"
     android:layout_width="match_parent"
  android:layout_height="wrap_content" />
    <EditText
        android:id="@+id/thoughts_edit_thoughts"
        android:layout_margin="8dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <RatingBar
        android:id="@+id/thoughs_rating_bar_happy"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:clickable="true"
        android:numStars="5"
        android:rating="0" />
    <Button
        android:id="@+id/thoughts_detail_button"
        android:text="@string/save"          
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

  1. 创建一个新的包,命名为:fragments,并向其中添加两个新的类:ThoughtsDetailFragmentThoughtsFragment,它们都将是Fragment类的子类。

  2. ThoughtsFragment类中,添加LoaderCallBack的实现:

public class ThoughtsFragment extends Fragment   
  implementsLoaderManager.LoaderCallbacks<Cursor>{
  1. 代码菜单中选择实现方法,接受建议的方法,并单击确定按钮。它将创建onCreateLoaderonLoadFinishedonLoaderReset的实现。

  2. 添加两个私有成员,它们将保存列表视图和适配器:

private ListView mListView;private SimpleCursorAdapter mAdapter;
  1. 重写onCreateView方法,在其中我们将填充布局并获取对列表视图的引用。从这里,我们还将调用getData方法:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    final View view = inflater.inflate( 
     R.layout.fragment_thoughts, container, false);
    mListView = (ListView)view.findViewById( 
     R.id.thoughts_list);
    getData();
    return view;
}

加载程序管理器

以下步骤将帮助我们向应用程序添加加载程序管理器:

  1. 实现getData方法。我们将使用loaderManagerinitLoader方法。投影定义了我们想要检索的字段,目标是adapter_thought_title布局中的 ID 数组,这将节省我们使用SimpleCursorAdapter类的一些工作。
private void getData(){String[] projection = new String[] { 
     ThoughtsProvider.THOUGHTS_ID,   
     ThoughtsProvider.THOUGHTS_NAME, 
     ThoughtsProvider.THOUGHTS_HAPPINESS};
    int[] target = new int[] {    
     R.id.adapter_thought_id,  
     R.id.adapter_thought_title,  
     R.id.adapter_thought_rating };
    getLoaderManager().initLoader(0, null, this);
    mAdapter = new SimpleCursorAdapter(getActivity(),   
     R.layout.adapter_thought, null, projection,  
      target, 0);
    mListView.setAdapter(mAdapter); 
}
  1. initLoader调用之后,需要创建一个新的加载程序。为此,我们将不得不实现onLoadFinished方法。我们将使用与适配器相同的投影,并使用我们在前面步骤中创建的ThoughtsProvideruri内容创建CursorLoader类。我们将按 ID(降序)对结果进行排序:
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        String[] projection = new String[] { 
     ThoughtsProvider.THOUGHTS_ID,   
     ThoughtsProvider.THOUGHTS_NAME, 
     ThoughtsProvider.THOUGHTS_HAPPINESS};
    String sortBy = "_id DESC";CursorLoader cursorLoader = new 
    CursorLoader(getActivity(), 
    ThoughtsProvider.CONTENT_URI, projection, null, 
     null, sortBy);
    return cursorLoader;
}
  1. onLoadFinished中,通知适配器加载了数据:
mAdapter.swapCursor(data);
  1. 最后,让我们为onLoaderReset方法添加实现。在这种情况下,数据不再可用,因此我们可以删除引用。
mAdapter.swapCursor(null);
  1. 让我们来看看ThoughtsDetailFragment方法。重写onCreateView方法,填充布局,并为布局中的保存按钮添加点击监听器:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    final View view = inflater.inflate( 
     R.layout.fragment_thoughts_detail, container,  
      false); 
   view.findViewById( 
    R.id.thoughts_detail_button).setOnClickListener( 
     new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            addThought();
        }
    });
    return view;
}
  1. 添加addThought方法。我们将根据通过EditTextRatingBar字段输入创建新的内容值。我们将根据提供的 URI 使用内容解析器的insert方法。插入新记录后,我们将清除输入:
private void addThought(){
    EditText thoughtsEdit = 
     (EditText)getView().findViewById(    
      R.id.thoughts_edit_thoughts);
    RatingBar happinessRatingBar =            
     (RatingBar)getView().findViewById(
      R.id.thoughs_rating_bar_happy);
    ContentValues values = new ContentValues();
    values.put(ThoughtsProvider.THOUGHTS_NAME, 
     thoughtsEdit.getText().toString());
    values.put(ThoughtsProvider.THOUGHTS_HAPPINESS,    
     happinessRatingBar.getRating());
    getActivity().getContentResolver().insert( 
     ThoughtsProvider.CONTENT_URI, values);
    thoughtsEdit.setText("");
    happinessRatingBar.setRating(0);
}
  1. 再次是将事物粘合在一起的时候了。打开MainActivity类,并添加两个私有成员,它们将引用我们创建的片段,如下所示:
private ThoughtsFragment mThoughtsFragment;
private ThoughtsDetailFragment mThoughtsDetailFragment;
  1. 添加两个私有成员,如果需要,将它们初始化,并返回实例:
private ThoughtsFragment getThoughtsFragment(){
    if (mThoughtsFragment==null) {
        mThoughtsFragment = new ThoughtsFragment();
    }
    return mThoughtsFragment;
}
private ThoughtsDetailFragment 
getThoughtDetailFragment() {
   if (mThoughtsDetailFragment==null){
    mThoughtsDetailFragment = new ThoughtsDetailFragment();
    }
    return mThoughtsDetailFragment;
}
  1. 删除onNavigationDrawerItemSelected的实现,并添加一个新的来显示想法列表。我们稍后将实现 KPI 选项:
@Override
  public void onNavigationDrawerItemSelected(int  
  position) {
   FragmentManager fragmentManager =    
    getFragmentManager();
   if (position==1) {
        fragmentManager.beginTransaction().   
         replace(R.id.container, 
          getThoughtsFragment()).commit();
    }
}
  1. onOptionsItemSelected方法中,测试 id 是否为action_add,如果是,则显示详细片段。在获取 id 的行后立即添加实现:
if (id== R.id.action_add){FragmentManager fragmentManager = 
     getFragmentManager();
    fragmentManager.beginTransaction().add( 
     R.id.container, getThoughtDetailFragment()  
      ).commit();
}

注意

这里使用add而不是replace。我们希望详细片段出现在堆栈的顶部。

  1. 保存详细信息后,片段必须再次被移除。再次打开ThoughtsDetailFragment。在addThought方法的末尾,添加以下内容以完成操作:
getActivity().getFragmentManager().beginTransaction().
 remove(this).commit();
  1. 然而,最好让活动处理片段的显示,因为它们旨在成为活动的辅助程序。相反,我们将为onSave事件创建一个监听器。在类的顶部,添加一个DetailFragmentListener接口。还创建一个私有成员和一个 setter:
public interface DetailFragmentListener {
    void onSave();
}
private DetailFragmentListener 
 mDetailFragmentListener; 
public void setDetailFragmentListener(  
 DetailFragmentListener listener){
    mDetailFragmentListener = listener;
}
  1. addThought成员的末尾添加这些行,以便让监听器知道已保存事物:
if (mDetailFragmentListener != null){
    mDetailFragmentListener.onSave();
}
  1. 返回MainActivity类,并为其添加一个监听器实现。如果需要,您可以使用代码菜单中的实现方法选项:
public class MainActivity extends Activityimplements NavigationDrawerFragment. 
   NavigationDrawerCallbacks, 
    ThoughtsDetailFragment.DetailFragmentListener {
@Override 
 public void onSave() {      
  getFragmentManager().beginTransaction().remove(
   mThoughtsDetailFragment).commit();
}
  1. 要告诉详细片段主活动正在监听,请滚动到getThoughtDetailFragment类并在创建新详细片段后立即调用setListener方法:
mThoughtsDetailFragment.setDetailFragmentListener(this);

现在运行应用程序,从导航抽屉中选择Thoughts list,然后单击加号添加新的想法。以下截图显示了添加想法的示例:

加载程序管理器

我们不需要告诉包含列表的片段有关我们在详细片段中创建的新想法。使用具有观察者的内容提供程序,列表将自动更新。

这样我们就可以完成更多,写更少容易出错的功能,从而写更少的代码,这正是我们想要的。它使我们能够提高代码的质量。

另请参阅

  • 参见第五章, 大小很重要

  • 参见第八章, 提高质量

更改投影以在应用程序中显示 KPI

我们可以使用不同的投影和相同的观察者模式来显示一些 KPI。实际上,这很容易,正如我们将在本示例中看到的那样。

准备工作

对于这个示例,您需要成功完成上一个示例。

如何做...

我们将继续在上一个示例中的应用程序上工作,并添加一个新视图来显示 KPI:

  1. 打开您在上一个示例中工作的项目。

  2. 添加一个新的布局,fragment_thoughts_kpi.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:gravity="center_horizontal"   
  android:padding="16dp"
  android:layout_height="match_parent">
  <TextView
        android:id="@+id/thoughts_kpi_count"          
        android:textSize="32sp"
        android:layout_margin="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/thoughts_kpi_avg_happiness"
        android:text= "@string/average_happiness"
        android:textSize="32sp"
        android:layout_margin="16dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <RatingBar
        android:id="@+id/thoughts_rating_bar_happy"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:clickable="false"
        android:numStars="5"
        android:rating="0" />
</LinearLayout>

  1. 添加一个新的片段并命名为ThoughtsKpiFragment。它是从Fragment类继承的。我们将在这里使用LoaderManager,所以它基本上看起来像这样:
public class ThoughtsKpiFragment extends Fragment    
 implements LoaderManager.LoaderCallbacks<Cursor> {
   @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {return null;
    }
    @Override
	public void onLoadFinished(Loader<Cursor> loader, Cursordata) {
    }
    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
    }
}
  1. 因为我们将使用两个加载程序来显示两个不同的 KPI,所以我们首先要添加两个常量值:
public static int LOADER_COUNT_THOUGHTS = 1;
public static int LOADER_AVG_RATING = 2;
  1. 覆盖onCreate方法:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    final View view = inflater.inflate( 
     R.layout.fragment_thoughts_kpi, container, false);
    getKpis();
    return view;
}
  1. 创建getKpis方法(在这里我们为不同目的两次初始化加载程序):
private void getKpis(){
    getLoaderManager().initLoader(LOADER_COUNT_THOUGHTS, null, 
     this);
    getLoaderManager().initLoader(LOADER_AVG_RATING, null, 
     this); 
}
  1. 添加onCreateLoader方法的实现。这次投影取决于加载程序的 ID。投影就像您期望的那样,如果它是普通的 SQL。我们正在计算行数,并计算平均幸福指数:
@Override 
 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    if (id == LOADER_COUNT_THOUGHTS) {
      String[] projection = new String[] {"COUNT(*) AS kpi"};
      android.content.CursorLoader cursorLoader = new android.content.CursorLoader(getActivity(),  
        ThoughtsProvider.CONTENT_URI, projection, null, null, 
         null);
      return cursorLoader;
    }
    else {
      String[] projection = new String[]
         {"AVG(happiness) AS kpi"};
      android.content.CursorLoader cursorLoader = new 
      android.content.CursorLoader(getActivity(), 
       ThoughtsProvider.CONTENT_URI, projection, null, null, 
        null);
      return cursorLoader;}
}
  1. 一旦数据到达,我们将到达onLoadFinished方法,并调用方法显示数据(如果有的话):
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    if (data == null || !data.moveToNext()) {
        return;
    }
    if (loader.getId() == LOADER_COUNT_THOUGHTS) {
        setCountedThoughts(data.getInt(0)); 
    }
    else{
        setAvgHappiness(data.getFloat(0));
    }
}
  1. 添加setCountedThoughtssetAvgHappiness方法。如果片段仍附加到活动中,我们将更新文本视图或评分栏:
private void setCountedThoughts(final int counted){
    if (getActivity()==null){
        return;
    }
    getActivity().runOnUiThread(new Runnable() {
        @Override
        public void run() {
          TextView countText = (TextView)getView().findViewById(
             R.id.thoughts_kpi_count);
          countText.setText(String.valueOf(counted));
        }
    });
}
private void setAvgHappiness(final float avg){
    if (getActivity()==null){
        return;
    }
    getActivity().runOnUiThread(new Runnable() {
        @Override
		public void run() {
            RatingBar ratingBar =        
             (RatingBar)getView().findViewById(
              R.id.thoughts_rating_bar_happy);
            ratingBar.setRating(avg);}
    });
}
  1. MainActivity文件中,添加一个 KPI 片段的私有成员:
private ThoughtsKpiFragment mThoughtsKpiFragment;
  1. 创建一个getKpiFragment方法:
private ThoughtsKpiFragment getKpiFragment(){
    if (mThoughtsKpiFragment==null){
        mThoughtsKpiFragment = new ThoughtsKpiFragment();
    }
    return mThoughtsKpiFragment;
}
  1. 找到onNavigationDraweItemSelected方法,并将其添加到if语句中:
… 
else if (position==0){ 
    fragmentManager.beginTransaction()
            .replace(R.id.container, getKpiFragment())
            .commit();
}

运行您的应用程序。现在我们的想法应用程序中有一些整洁的统计数据:

如何做...

在这个和上一个示例中,我们已经看到了一旦掌握了内容提供程序的概念,处理数据变得多么容易。

到目前为止,我们在同一个应用程序中完成了所有这些工作;然而,由于我们已经准备好导出内容提供程序,让我们找出如何在不同的应用程序中读取我们的想法。现在就让我们来做吧。

另请参阅

参见第五章, 大小很重要

参见第八章, 提高质量

使用内容提供程序与其他应用程序通信

如果您阅读谷歌关于内容提供程序的文档,您会注意到内容提供程序基本上是为了在请求时向其他应用程序提供数据。这些请求由ContentResolver类的方法处理。

我们将创建一个新的应用程序,它将从另一个应用程序中读取我们的日常想法。

准备工作

对于这个示例,您需要成功完成上一个示例。确保您也向应用程序添加了一些想法,否则将没有东西可读,正如显而易见的船长所告诉我们的那样。

如何做...

首先我们将创建一个新的应用程序。它将读取我们的想法。这是肯定的!

  1. 在 Android Studio 中创建一个新项目,命名为DailyAnalytics,然后点击确定按钮。

  2. 选择手机和平板电脑,然后点击下一步按钮。

  3. 选择空白活动,然后点击下一步按钮。

  4. 接受自定义活动视图中的所有值,然后点击完成按钮。

  5. 打开AndroidManifest.xml文件,并添加与DailyThought应用程序通信所需的权限:

<uses-permission android:name=  
 "com.packt.dailythoughts.READ_DATABASE"/>
  1. 打开activity_main.xml布局,并将TextView应用程序的id更改为main_kpi_count
<TextView
    android:id="@+id/main_kpi_count"android:text="@string/hello_world"  
    android:layout_width="wrap_content"android:layout_height="wrap_content" />
  1. MainActivity类中,添加LoaderCallBack实现:
public class MainActivity extends Activity  implementsLoaderManager.LoaderCallbacks<Cursor>
  1. onCreate方法的末尾调用initLoader
getLoaderManager().initLoader(0, null, this);
  1. onCreateLoader方法添加一个实现。它的工作方式基本与应用程序的内容提供程序相同:
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    Uri uri = Uri.parse(  
     "content://com.packt.dailythoughts/thoughts");
    String[] projection = new String[] { "_id", "name", 
     "happiness"};
    String sortBy = "name";
    CursorLoader cursorLoader = new  
    android.content.CursorLoader(
     this,uri, projection, null, null, null);
    return cursorLoader;
}
  1. onLoadFinished方法中,我们可以根据您在其他应用程序中输入的内容显示一些分析:
@Override
public void onLoadFinished(Loader<Cursor> loader, 
 Cursor data) {
   final StringBuilder builder = new StringBuilder();
    builder.append(
     "I know what you are thinking of... \n\n");
   while ( (data.moveToNext())){
       String onYourMind = data.getString(1);
       builder.append("You think of "+
         onYourMind+". ");
       if (data.getInt(2) <= 2){
           builder.append(
            "You are sad about this...");
        }
        if (data.getInt(2) >= 4) {
           builder.append("That makes you happy!");
        }
        builder.append("\n");
    }
    builder.append("\n Well, am I close? ;-)");
    runOnUiThread(new Runnable() {
        @Override
		public void run() {TextView countText = (TextView) 
           findViewById(R.id.main_kpi_count);
          countText.setText(String.valueOf(
           builder.toString()));}});}

运行应用程序,看到所有你的想法出现在这里,如下所示:

如何做...

可怕,不是吗?使用内容提供程序,很容易在不同的应用程序之间共享数据。这就是许多应用程序如联系人或画廊的工作方式。

还有更多...

我们已经学习了内容提供程序的工作原理,并且已经偷偷看了一眼观察者模式。使用这个和其他模式可以提高我们应用程序的质量。

现在事情将变得非常严肃。避免潜在错误,减少需要编写的代码量,并使其在任何 Android 设备上运行!我们将在下一章中找出如何做到这一点。

另请参阅

  • 参考第八章, 提高质量

第八章:提高质量

您刚刚完成了应用的编码。现在呢?尽快将其放到 Play 商店上!

不要等待,您还没有完成!您是否正确测试了您的应用?它是否适用于任何 Android 版本?在任何设备上?在任何情况下?

在本章中,我们将重点关注:

  • 模式和支持注释

  • 使用 Robolectrics 进行单元测试

  • 代码分析

介绍

有一些常见的陷阱要避免,以及一些模式,您可能希望应用以提高应用程序的质量。您已经在之前的章节中看到了其中一些。此外,还有一些有趣的工具可以用来测试和分析您的代码。

在接下来的路线图中,您会注意到在将应用上线之前,您需要完成不同的阶段:

介绍

您的代码结构、健壮性、可维护性以及其与功能要求的符合程度是关键因素。

功能质量通过软件测试来衡量,因此我们需要将应用分发给我们的测试人员。我们将在第十章中讨论这一点,测试您的应用程序

通过运行单元测试和手动代码检查(同行审查)或使用诸如 Android Lint 之类的工具来评估结构质量,您将在本章的最后一个配方中了解更多有关它。现在的问题是代码架构是否满足良好软件工程的要求?

总的来说,有一些有趣的原则将帮助您提高代码的质量。其中一些列在这里:

  • 学习活动生命周期,并以正确的方式使用片段。

  • 如果可以避免,就不要分配内存。

  • 避免过于沉重的片段和活动。

  • 考虑模型视图控制器MVC)方法。应用正确的模式。

  • 在一个地方解决一次问题。不要重复自己DRY)。

  • 不要做不需要做的工作(尚未)。也被称为:你不会需要它YAGNI)。

下一个配方将让您了解模式是什么,以及为什么您会想要应用它们。

模式和支持注释

质量是一项严肃的业务,因此我们将把它与一些乐趣结合起来。在即将到来的配方中,我们将创建一个测验应用。我们将使用 Google Play 服务进行此操作,并且我们将研究可以应用于我们的应用的模式,特别是 MVC 和模型视图控制器MVP)方法。

那么设计模式实际上是什么?设计模式是常见问题的解决方案。我们可以在任何地方重用这样的模式。没有必要重新发明轮子(除非您当然可以想到更好的轮子),也没有必要重复自己。

模式是我们可以信任的最佳实践。它们可以帮助我们加快开发过程,包括测试。

一些模式包括:

  • MVC

  • MVP

  • 可观察的

  • 工厂

  • 单例

  • 支持注释

  • Google Play 服务

MVC

MVC 最适合较大的项目。这种模式的好处是关注点的分离。我们可以将 UI 代码与业务逻辑分开。控制器将负责显示哪个视图。它将从另一层获取数据,一个类似存储库的类,该类将从某处获取其数据,并通过模型(或模型列表)将数据传递给 UI。控制器不知道数据来自何处以及如何显示。这些是存储库类和 UI 的任务,分别。

MVP

在大多数情况下,MVP 是与 Android 应用程序开发一起使用的更合适的模式,因为活动和片段的性质。使用 MVP 模式,一个 Presenter 包含视图的 UI 逻辑。视图的所有调用都直接委托给它。Presenter 将通过接口与视图通信,允许我们稍后使用模拟数据创建单元测试。

观察者模式

我们在第七章中已经看到了这种模式,内容提供者和观察者。观察者观察另一个对象的变化。

工厂模式

这种模式有助于创建对象。我们之前使用过的位图工厂(并且我们将在本教程中再次使用)是工厂模式的一个很好的例子。

单例

单例模式将防止我们拥有对象的多个实例。通常,它是一个(类)方法,返回一个实例。如果它不存在,它将被创建,否则它将返回先前创建的实例。应用程序类就是单例模式的一个例子。

支持注释

支持注释可以帮助我们向代码检查工具(如 lint)提供提示。它们可以帮助您通过添加元数据标签并运行代码检查来检测问题,例如空指针异常和资源类型冲突。支持库本身已经用这些注释进行了注释。是的,他们自己也在使用注释,这证明使用注释是正确的方法。

基本上有三种我们可以使用的注释类型:空值注释、资源类型注释和 IntDef \ StringDef 注释。例如,我们可以使用@NonNull注释来指示给定参数不能为空,或者我们可以使用@Nullable注释来指示返回值可以为空。

Google Play 服务

Play Games SDK 提供跨平台的 Google Play 游戏服务,让您可以轻松地在平板电脑和移动设备游戏中集成流行的游戏功能,例如成就、排行榜、保存的游戏和实时多人游戏(在 Android 上)选项。

现在理论已经足够了!让我们创建我们的测验应用程序,并应用我们在这里讨论过的一些理论。

准备工作

对于本教程,您需要拥有最新版本的 Android Studio 和已安装 Google Play 服务的真实设备,这对大多数设备来说都是成立的。或者,您可以在虚拟 Genymotion 设备上安装它们,但这将需要一些额外的准备工作。

此外,您需要拥有(或创建)一个 Google 开发者帐户。

如何做...

然后开始。启动 Android Studio 并执行以下步骤,因为我们将要构建一些伟大的东西:

  1. 在 Android Studio 中创建一个新项目。命名为GetItRight,然后点击下一步按钮。

  2. 选择手机和平板电脑选项,然后点击下一步按钮。

  3. 为移动设备添加活动视图中,选择Google Play 服务,然后点击下一步按钮。

  4. 接受活动名称标题字段,然后点击完成按钮。

  5. 将您的网络浏览器指向 Google 开发者控制台,如果您还没有帐户,请登录或注册。您可以在以下网址找到它:console.developers.google.com

  6. 在开发者控制台中,点击游戏选项卡(网页左侧的游戏图标)。

  7. 如果被要求,接受服务条款。

  8. 点击设置 Google Play 服务按钮。

  9. 输入应用程序名称Get It Right Sample,选择一个类别:问答,然后点击继续按钮。

  10. 在游戏详情视图中,输入描述,然后点击保存按钮。

  11. 接下来,您需要生成一个 Oauth2 客户端 ID。要这样做,请点击关联应用链接。

  12. 选择Android作为您的操作系统,输入packt.com.getitright作为包名称,保持其他设置不变,然后点击保存并继续按钮。

  13. 在第 2 步中,点击立即授权您的应用按钮。在品牌信息弹出对话框中,点击继续按钮。

  14. 客户端 ID对话框出现。输入packt.com.getitright作为包名称。要获取签名证书指纹,打开终端应用程序(对于 Windows:命令提示符)并输入:

keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore  -list –v

  1. 如果要求keystore密码,默认的调试 keystore 密码是android

  2. 复制并粘贴指纹(SHA1),然后点击创建客户端按钮。

  3. 点击返回列表按钮,然后点击继续下一步按钮。

  4. Android 应用程序详细信息视图中,您将看到应用程序 ID(如果向下滚动一点),我们稍后将需要它。复制其值。

排行榜

按照提供的步骤为应用程序添加排行榜:

  1. 在网页的左侧,选择排行榜,然后点击添加新排行榜按钮。将新排行榜命名为GetItRight Leaderboard,然后点击保存按钮。注意排行榜ID。我们稍后会用到它:排行榜

  2. 打开项目app目录内的build.gradle文件,并添加 Google Play 服务的依赖项:

compile 'com.google.android.gms:play-services:7.5.0'
  1. 同步您的项目。如果无法解析 Google Play 服务,将生成一个错误,其中包含一个链接,上面写着安装存储库并同步项目。点击此链接进行操作。

  2. 打开AndroidManifest.xml文件,并向应用程序标签添加元数据标记:

<meta-data 
 android:name="com.google.android.gms.games.APP_ID"android:value="@string/app_id" />
  1. 此外,将app_id添加到strings.xml文件中:
<resources><string name="app_name">GetItRight</string><string name="app_id">your app id</string>
  1. GooglePlayServicesActivity类的onConnected方法的第一行设置断点。对于onConnectionFailed方法的第一行也是如此。使用 Google Play 服务模板和提供的应用 ID,您应该已经能够连接到 Google Play 服务。运行应用程序(调试模式)以查看是否成功。

  2. 创建一个新的 Android 资源目录,并选择layout作为资源类型;在该目录中创建一个新的布局资源文件,并命名为activity_google_play_services.xml

  3. strings.xml资源文件添加一些新的字符串:

<string name="incorrect_answer">That is incorrect</string><string name="correct_answer">That is the correct 
 answer!</string><string name="leader_board">LEADER BOARD</string>
  1. activity_google_play_service资源文件创建布局:
<?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:padding="16dp"
    android:background="@android:color/holo_blue_dark"
    android:layout_height="match_parent">
    <ScrollView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"><LinearLayout
         android:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content">
          <ImageView
            android:id="@+id/image"android:src= 
             "@android:drawable/ic_popup_sync"android:layout_width="match_parent"android:layout_height="300px" />
          <TextView
            android:id="@+id/text"android:textColor="@android:color/white"android:text="Question"android:textSize="24sp"android:layout_width="match_parent"android:layout_height="wrap_content" />
          <LinearLayout
            android:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content">
          <Button
            android:id="@+id/button_1"            
            android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_vertical|left" />
          <Button
            android:id="@+id/button_2"android:layout_width="match_parent"android:layout_height="wrap_content"                     
            android:gravity="center_vertical|left" />
          <Button
           android:id="@+id/button_3"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_vertical|left" />
          <Button
           android:id="@+id/button_4"           
           android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_vertical|left" />
          <Button
           android:id="@+id/button_test"android:text="@string/leader_board"android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center_vertical|left" />
          </LinearLayout>
        </LinearLayout>
    </ScrollView>
</LinearLayout>
  1. 打开GooglePlayServicesActivity文件。在onCreate方法中,加载布局并为所有按钮设置点击监听器:
setContentView(R.layout.activity_google_play_services); 
findViewById(R.id.button_1).setOnClickListener(this);
findViewById(R.id.button_2).setOnClickListener(this);
findViewById(R.id.button_3).setOnClickListener(this);
findViewById(R.id.button_4).setOnClickListener(this); 
findViewById(R.id.button_test).setOnClickListener(this);
  1. GooglePlayServicesActivity文件实现onClickListener方法。Android Studio 将建议一个实现,您可以接受此建议,也可以自己添加实现:
public class GooglePlayServicesActivity extends Activity implements GoogleApiClient.ConnectionCallbacks,GoogleApiClient.OnConnectionFailedListener, 
   View.OnClickListener { 
@Override
public void onClick(View v) {
}
  1. 添加两个私有成员,一个用于我们的排行榜请求,另一个用于保存您的排行榜 ID:
private int REQUEST_LEADERBOARD = 1;
private String LEADERBOARD_ID = "<your leaderboard id>";
  1. onClick方法创建实现。我们正在准备用户点击多项选择选项的情况。对于排行榜(测试)按钮,我们可以立即添加实现:
@Override
public void onClick(View v) {
    switch (v.getId()){
        case R.id.button_1:
        case R.id.button_2:
        case R.id.button_3:
        case R.id.button_4: 
            break;
        case R.id.button_test:
         startActivityForResult( 
          Games.Leaderboards.getLeaderboardIntent(  
           mGoogleApiClient, LEADERBOARD_ID),  
            REQUEST_LEADERBOARD);
         break;
    }
}
  1. 创建一个新的包并命名为models。创建AnswerQuestionQuiz类:

要添加Answer类,您需要以下代码:

public class Answer {
    private String mId;
    private String mText;
    public String getId() {
        return mId;
    }
    public String getText() {
       return mText;
    }
    public Answer (String id, String text) {
        mId = id;
        mText = text;
    }
}

要添加Question类,请使用以下代码:

public class Question {
    private String mText;
    private String mUri;
    private String mCorrectAnswer;
    private String mAnswer;
    private ArrayList<Answer> mPossibleAnswers;
    public String getText(){
        return mText;
    }
    public String getUri(){
        return mUri;}
    public String getCorrectAnswer(){
        return mCorrectAnswer;
    }
    public String getAnswer(){
        return mAnswer;
    }
    public Question (String text, String uri, String 
     correctAnswer){
        mText = text;
        mUri = uri;
        mCorrectAnswer = correctAnswer;
    }
    public Answer addAnswer(String id, String text){
        if (mPossibleAnswers==null){
            mPossibleAnswers = new ArrayList<Answer>();
        }
        Answer answer = new Answer(id,text);
        mPossibleAnswers.add(answer);
        return answer;
    }
    public ArrayList<Answer> getPossibleAnswers(){
        return mPossibleAnswers;
    }
}

要添加Quiz类,请使用以下代码:

public class Quiz {
    private ArrayList<Question> mQuestions;
    public ArrayList<Question> getQuestions(){
        return mQuestions;
    }
    public Question addQuestion(String text, String uri, String 
     correctAnswer){
        if (mQuestions==null){
            mQuestions = new ArrayList<Question>();
        }
        Question question = new Question( 
         text,uri,correctAnswer);
        mQuestions.add(question);
        return question;
    }
}
  1. 创建一个新的包并命名为repositories。创建一个新的类并命名为QuizRepository。向测验添加一些问题。您可以使用以下示例中的问题,但如果愿意,也可以自己创建一些问题。在真实的应用程序中,问题和答案当然不会是硬编码的,而是从数据库或后端检索的(请注意,我们随时可以更改此行为,而无需修改除此类之外的任何内容):
public class QuizRepository {
    public Quiz getQuiz(){
      Quiz quiz = new Quiz();
      Question q1 = quiz.addQuestion(
      "1\. What is the largest city in the world?",  
       "http://cdn.acidcow.com/pics/20100923/
        skylines_of_large_cities_05.jpg" , "tokyo");
        q1.addAnswer("delhi" , "Delhi, India");
        q1.addAnswer("tokyo" , "Tokyo, Japan");
        q1.addAnswer("saopaulo" , "Sao Paulo, Brazil");
        q1.addAnswer("nyc" , "New York, USA");
        Question q2 = quiz.addQuestion("2\. What is the largest animal in the world?","http://www.onekind.org/uploads/a-z/az_aardvark.jpg" , "blue_whale");
        q2.addAnswer("african_elephant" , "African Elephant");
       q2.addAnswer("brown_bear" , "Brown Bear");
        q2.addAnswer("giraffe" , "Giraffe");
        q2.addAnswer("blue_whale" , "Blue whale");
        Question q3 = quiz.addQuestion("3\. What is the highest mountain in the world?","http://images.summitpost.org/medium/ 815426.jpg", "mount_everest");
        q3.addAnswer("mont_blanc" , "Mont Blanc");
        q3.addAnswer("pico_bolivar" , "Pico Bolívar");
        q3.addAnswer("mount_everest" , "Mount Everest");
        q3.addAnswer("kilimanjaro" , "Mount Kilimanjaro");
        return quiz;
    }
}
  1. GamePlayServicesActivity类中,添加这三个私有成员:
private Quiz mQuiz;
private int mScore;
private int mQuestionIndex=0;
  1. newGame方法添加实现。我们将通过向存储库请求来获取Quiz对象。重置分数和问题索引后,我们调用displayQuestion方法,该方法通过实际显示问题、可能的答案和漂亮的图片来实现 UI 逻辑:
private void newGame(){
    mQuiz = new QuizRepository().getQuiz();
    mScore = 0;
    mQuestionIndex = 0;
    displayQuestion(mQuiz.getQuestions().get(mQuestionIndex));
private void displayQuestion(Question question){ 
    TextView questionText = (TextView)findViewById(R.id.text); 
    displayImage(question); 
    questionText.setText(question.getText());
    ArrayList<Answer> answers = question.getPossibleAnswers();
    setPossibleAnswer(findViewById(R.id.button_1), 
     answers.get(0));
    setPossibleAnswer(findViewById(R.id.button_2), 
     answers.get(1));
    setPossibleAnswer(findViewById(R.id.button_3), answers.get(2));
    setPossibleAnswer(findViewById(R.id.button_4), answers.get(3));
}
private void setPossibleAnswer(View v, Answer answer){
    if (v instanceof Button) {
        ((Button) v).setText(answer.getText());
        v.setTag(answer);
    }
}
private void displayImage(final Question question){ 
    new Thread(new Runnable() {
        public void run(){
            try {
              URL url = new URL(question.getUri());
              final Bitmap image = BitmapFactory.decodeStream(url.openConnection().getInputStream());
               runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        ImageView imageView = (ImageView) 
                          findViewById(R.id.image);
                        imageView.setImageBitmap(image);
                    }
                });
            }
            catch (Exception ex){
                Log.d(getClass().toString(), ex.getMessage());
            }
        }
    }).start();
}

让游戏开始!

以下步骤可用于添加新游戏的方法:

  1. onCreate方法的末尾,我们将调用newGame方法:
newGame();
  1. 修改onClick方法,这样当用户点击任何按钮时我们可以做出响应。如果点击了任何一个多选按钮,我们将调用checkAnswer方法。我们选择的是正确答案吗?多么令人兴奋:
@Override
public void onClick(View v) {
    switch (v.getId()){
        case R.id.button_1:
        case R.id.button_2:
        case R.id.button_3:
        case R.id.button_4:
            checkAnswer(v);
            break;
        case R.id.button_test: startActivityForResult( 
         Games.Leaderboards.getLeaderboardIntent(
          mGoogleApiClient, LEADERBOARD_ID), REQUEST_LEADERBOARD);
         break;
}

   }
  1. 添加checkAnswer方法。我们将比较给定答案与问题的正确答案,根据结果,我们将调用onGoodAnsweronWrongAnswer方法。根据答案,你的进展将被决定:如果答案错误,游戏结束,我们将显示排行榜。

  2. 如果没有更多的问题,我们将提交用户的分数并显示排行榜。排行榜本身将处理所有相关逻辑。提交的分数是否足够高,使你的名字出现在榜单的顶部?通过以下片段来检查:

private void checkAnswer(View v){ 
    if (v instanceof Button){
        Answer answer = (Answer)((Button)v).getTag();
        if (mQuiz.getQuestions().get(mQuestionIndex).  
         getCorrectAnswer().equalsIgnoreCase( 
          answer.getId())){
            onGoodAnswer();
        }
        else{
            onWrongAnswer();
        }
    }
}
private void onWrongAnswer(){
    Toast.makeText(this, getString( 
     R.string.incorrect_answer), Toast.LENGTH_SHORT).show();
    startActivityForResult(
     Games.Leaderboards.getLeaderboardIntent( 
     mGoogleApiClient, LEADERBOARD_ID), 
      REQUEST_LEADERBOARD);
}
private void onGoodAnswer(){
    mScore+= 1000;
    Games.Leaderboards.submitScore(mGoogleApiClient, 
     LEADERBOARD_ID, mScore);
    Toast.makeText(this, getString(R.string.correct_answer), 
     Toast.LENGTH_SHORT).show();
    mQuestionIndex++;
    if (mQuestionIndex < mQuiz.getQuestions().size()){
        displayQuestion(mQuiz.getQuestions().get( 
         mQuestionIndex));
    }
    else{
        startActivityForResult( 
         Games.Leaderboards.getLeaderboardIntent( 
          mGoogleApiClient, LEADERBOARD_ID), 
           REQUEST_LEADERBOARD);
	}
}
  1. 为了做好单元测试和代码检查,让我们添加注释支持。在app文件夹中打开build.gradle文件并添加依赖项。在修改文件后,点击出现的立即同步链接:
compile 'com.android.support:support-annotations:22.2.0'
  1. 如果出现“无法解析支持注释”的错误,则点击出现的安装存储库并同步项目链接。

  2. 如果一切顺利,我们可以添加注释,例如在CheckAnswer方法的参数上:

private void checkAnswer(@NonNull View v){
  1. Question类中,我们可以为getPossibleAnswers方法添加@Nullable注释,如果我们没有为问题提供任何多选选项的话,这可能是情况:
@Nullable
public ArrayList<Answer> getPossibleAnswers(){
    return mPossibleAnswers;
}
  1. 稍后,如果我们进行一些分析,这将导致GooglePlayServiceActivity出现警告,我们将在代码分析中更仔细地查看这一点:
Method invocation 'answers.get(0)' may produce  'java.lang.NullPointerException' 

如果你喜欢,你可以玩这个游戏并添加一些注释。只是不要花太多时间。我们来玩游戏吧!

运行你的应用程序,并成为排行榜上的第一名。因为目前你是唯一的测试玩家,我猜这不会太难。

你刚刚创建了自己的测验应用程序,如果你愿意,可以添加一些其他具有挑战性的问题,如下面的屏幕截图所示:

让游戏开始吧!

我们已经调查了 Google Play 服务,并且我们一直在为我们的应用使用 MVC 方法。此外,我们还研究了如何使用注释,在进行一些代码分析后,这些注释可以帮助我们改进代码。

还有更多...

我们只是匆匆一瞥了一下模式以及如何应用它们。查看互联网或获取一些优秀的书籍,以了解更多关于模式的知识。另外,请参阅www.google.com/design/spec/patterns/app-structure.html

确保你也阅读了有关支持注释的文档。使用它们有很多更多的可能性。在tools.android.com/tech-docs/support-annotations中查看文档。

此外,我们只使用了 Google Play 服务的一小部分。我们只知道如何登录和如何使用排行榜。如果你愿意,你可以查看其他选项。有关此内容,请参阅developers.google.com/games/services/android/quickstart

另请参阅

  • 参见第七章,内容提供者和观察者

使用 Robolectric 进行单元测试

单元测试是一种测试方法,其中测试代码的各个单元。例如,可以测试视图或存储库,以检查它是否满足要求。与大多数其他测试不同,这些测试通常由软件开发人员开发和运行。

理想情况下,一个测试用例完全独立于其他用例和其他单元。由于类通常依赖于其他替代品,例如需要使用模拟对象。在上一个教程中,QuizRepository类提供了硬编码的测验数据(存根或模拟数据),但正如建议的那样,意图是测验数据应该从后端获取。

我们将准备好我们在上一个教程中创建的应用程序进行单元测试,并且我们将自己创建一些测试。Robolectric将帮助我们完成这些。尽管自 Android Studio 1.2 版本发布以来,基于 JUnit 的单元测试设置变得更加容易,但它仍然不如 Robolectric 强大。

Robolectric 不需要额外的模拟框架,它也可以在模拟器之外运行,这使我们能够将单元测试与持续集成环境结合起来,就像我们将在第十章中所做的那样,测试你的应用程序

准备工作

对于这个教程,最理想的情况是上一个教程已经成功完成。如果你选择跳过本章的这一部分,当然可以打开你自己的项目,并以更或多或少相同的方式设置单元测试。这取决于你。

如何做...

那么我们要做些什么来创建和运行一些单元测试呢?让我们找出来:

  1. 打开我们在上一个教程中创建的项目。

  2. app文件夹中打开build.gradle文件,并为 Robolectric 添加一个依赖项:

testCompile 'org.robolectric:robolectric:3.0'
  1. src文件夹中的androidTest文件夹重命名为test

  2. Run菜单中选择Edit configurations选项。

  3. 在 Run\Debug Configuration 窗口的左侧,选择DefaultsJUnit。在右侧将Working directory的内容更改为$MODULE_DIR$,然后点击OK按钮。

  4. ApplicationTest类重命名为QuizRepositoryTest

  5. QuizRepositoryTest类添加一些测试。我们将使用 Robolectric 进行这项工作。正如你所注意到的,我们将在这里使用注解,就像我们在上一个教程中所做的那样:

@Config(constants = BuildConfig.class, sdk = 21)
@RunWith(RobolectricGradleTestRunner.class)
public class QuizRepositoryTest {
    private QuizRepository mRepository; 
    @Beforepublic void setup() throws Exception {
       mRepository = new QuizRepository();
        assertNotNull("QuizRepository is not 
        instantiated", mRepository);
    }
    @Test
    public void quizHasQuestions() throws Exception {
        Quiz quiz = mRepository.getQuiz();
        ArrayList<Question> questions = quiz.getQuestions();
        assertNotNull("quiz could not be created", quiz);

        assertNotNull("quiz contains no questions",       
         questions);
        assertTrue("quiz contains no questions", 
         questions.size()>0);
    }
    @Test
    public void quizHasSufficientQuestions() throws 
     Exception {
        Quiz quiz = mRepository.getQuiz();
        ArrayList<Question> questions = quiz.getQuestions();
        assertNotNull("quiz could not be created", quiz);
        assertNotNull("quiz contains no questions", 
         questions);
        assertTrue("quiz contains insufficient questions", questions.size()>=10);
    }
}
  1. 创建另一个测试类,以便我们可以测试该活动。将新类命名为GooglePlayServicesActivityTest。在这个测试中,我们也可以进行一些布局测试:
@Config(constants = BuildConfig.class, sdk = 21)
@RunWith(RobolectricGradleTestRunner.class)
public class GooglePlayServicesActivityTest {
    private GooglePlayServicesActivity activity;
    @Before
    public void setup() throws Exception {
       activity = Robolectric.setupActivity( 
        GooglePlayServicesActivity.class);
        assertNotNull("GooglePlayServicesActivity is not instantiated", activity);
    }
    @Test
    public void testButtonExistsAndHasCorrectText() throwsException {
        Button testButton = (Button) activity.findViewById( 
         R.id.button_test); 
        assertNotNull("testButton could not be found",testButton); 
}
  1. 打开build variants窗格,并选择Unit tests而不是Instrumentation tests

现在test包中的所有内容都将被突出显示为绿色(你可能需要先进行重建)。如果你右键单击packt.com.getitright包名或者你创建的任何测试类,你将在上下文菜单中找到一个选项Run tests in packt.com.getrightRun QuizRepositoryTest。例如,选择运行QuizRepositoryTest。如果选择此选项,Gradle 会开始思考一会儿。一段时间后,结果会显示出来。

默认情况下只显示失败的测试。要查看成功的测试,点击左侧显示测试树上方的Hide passed按钮。

你会看到quizHasQuestions测试已经通过。然而,quizHasSufficientQuestions测试失败了。这是有道理的,因为我们的测试要求我们的测验至少有 10 个问题,而我们只添加了三个问题到测验中,如下图所示:

如何做...

QuizRepository中为Quiz添加七个问题,以便做对。当然,你也可以作弊,通过修改测试来达到目的,但我们就说这是一个业务需求吧。

重新运行测试。每个单元测试都成功了。万岁!创建一些你能想到的其他单元测试。

单元测试是一个非常有趣的选择,因为我们也可以将其用于持续集成的目的。想象一下,每次您将源代码提交(和推送)到 GitHub 或 BitBucket 等中央存储库时,我们都运行单元测试的情景。如果编译和所有单元测试都成功,我们可以自动创建一个新的(临时的)发布,或者被通知编译或任何测试失败。

还有更多...

还有很多其他工具和方法可用于移动测试目的。

除了单元测试,我们还希望测试用户界面UI),例如使用 Espresso。

Espresso

Espresso 适用于编写简洁可靠的 Android UI 测试。测试通常包含点击、文本输入和检查。编写测试实际上非常简单。以下是使用 Espresso 的测试示例:

@Test
public void testLogin() {
   onView(withId(R.id.login)).perform(
    typeText("mike@test.com"));
   onView(withId(R.id.greet_button)).perform(click());
}

引用网站上的话:

Espresso 测试清楚地陈述期望、交互和断言,而不受到样板内容、自定义基础设施或混乱的实现细节的干扰”。

有关更多信息,请参阅code.google.com/p/android-test-kit/wiki/Espresso

方法

在测试方面,有不同的方法可以考虑。其中一种方法是测试驱动开发TDD)。如果功能和所有要求都已知,我们可以在开发应用程序之前定义我们的测试。当然,所有测试最初都会失败,但这实际上是件好事。它将概述需要做的事情,并集中精力做正确的事情。如果您开始开发得越来越多,测试将成功,剩下的工作量也会减少。

另一种更近期的方法是行为驱动开发BDD)。这种测试方法是基于功能的,其中一个功能是从特定的角度表达的一系列故事。

BDD 工具可以作为单元测试的一种风格,例如Rspec,也可以作为更高级别的验收测试风格:Cucumber

Cucumber、Gherkin 和 Calabash

不,这不是突然出现在这里的蔬菜店广告。Cucumber是一种以 BDD 风格编写的自动化验收测试的工具。它允许执行以业务面向文本编写的功能文档。

以下是使用Gherkin的功能文件的示例。它有两个目的:文档和自动化测试:

Scenario: Login
  Given I am on the Login Screen
  Then I touch the "Email" input field
  Then I use the keyboard and type "test@packt.com"
  Then I touch the "Password" input field
  Then I use the keyboard and type "verysecretpassword"
  Then I touch "LOG IN"
  Then I should see "Hello world"

Gherkin是一种可读性强的领域特定语言,它可以让您描述软件的行为,而不详细说明该行为是如何实现的。因此,非开发团队成员也可以编写这些测试。

需要一些粘合代码来使事情发生。在 Cucumber 中,这个过程是在步骤定义中定义的。Cucumber 通常让您用 Ruby 语言编写这些步骤定义。

通过 Calabash 框架,您可以使用 Cucumber 为 Android 和 iOS 创建测试。它使您能够定义和执行自动化验收测试。Calabash 的另一个很棒的地方是,它允许您在云上运行自动化测试,例如使用 TestDroid 的服务。

首先要做的事情!

要了解有关 Cucumber 的更多信息,请访问cucumber.io

您可以在calaba.sh找到 Calabash 框架。

还可以查看www.testdroid.com了解有关使用 TestDroid 云测试环境在尽可能多的设备上进行测试的更多信息。

最后,要在时间、质量和金钱之间找到一个良好的平衡。测试应用程序的方法取决于您(或您的公司或您的客户)认为这些元素中的每个元素有多有价值。至少创建单元测试和 UI 测试。还要不要忘了性能测试,但这是下一章将讨论的一个话题!

另请参阅

  • 参考第九章,性能改进

  • 参考第十章,测试您的应用程序的 Beta 版

代码分析

代码分析工具,如 Android Lint,可以帮助你检测潜在的错误,以及如何优化你的应用程序的安全性、可用性和性能。

Android Lint 随 Android Studio 一起提供,但也有其他可用的工具,如:Check Style,项目 Mess DetectorPMD)和 Find Bugs。在这个示例中,我们只会看一下 Android Lint。

准备工作

  • 最理想的情况是,你已经完成了本章的前两个示例,所以我们现在将检查应用的结果。但是,你也可以在任何项目上使用Android Lint(或其他工具)来查看哪里可以改进。

注意

第一个示例的支持注解影响了显示的结果。是的,没错,我们引起了这些警告。

操作步骤...

我们不需要安装任何东西来获取 Android Lint 报告,因为它已经在 Android Studio 中了。只需按照下一步骤来使用它:

  1. 打开你在之前示例中创建的项目。或者,打开你自己的项目。

  2. 分析菜单中选择代码检查。检查范围是整个项目。单击确定按钮继续。

  3. 检查结果将以树形视图呈现。展开并选择项目以查看每个项目的内容,如下面的快照所示:操作步骤...

  4. 这里看起来很严重,但实际上并不是那么糟糕。有一些问题根本不是致命错误,但修复它们可以极大地改进你的代码,这正是我们目前的目标。

  5. 例如,查看声明冗余 | 声明访问可以更弱 | 可以是私有问题。导航到它。双击它跳转到问题出现的代码。右键单击它。上下文菜单立即提供了解决方案。选择使字段私有选项应用正确的解决方案。如果这样做,此项目将被标记为已完成(划掉)。

  6. 现在看看硬编码文本。如果你双击与此问题相关的任何项目,你会看到问题所在。

  7. 为了方便起见,我们放置了一个临时文本(如Text View中的Question)。如果这是真的,我们应该使用一个字符串资源。在这里,我们可以安全地删除这个文本。如果你重新运行代码检查,问题将消失:

<TextView
    android:id="@+id/text"android:textColor="@android:color/white"android:textSize="24sp"android:layout_width="match_parent"
    android:layout_height="wrap_content" />
  1. 接下来,看看常量条件和异常下的可能的错误。对于GooglePlayServicesActivity文件,它说:
Method invocation 'answers.get(0)' may produce 'java.lang.NullPointerException'
  1. 如果你双击这条消息,你会发现问题所在:
setPossibleAnswer(findViewById(R.id.button_1), answers.get(0));
  1. 这一行可能会产生Null Pointer Exception。为什么?如果你通过选择并按下Cmd + B(对于 Windows:Ctrl + B)来查看getPossibleAnswers方法的声明,你就会找到原因:
@Nullable
public ArrayList<Answer> getPossibleAnswers(){return mPossibleAnswers;}

啊对了!我们在第一个示例中自己添加了这个注解,以提醒我们以后(或其他开发人员)返回的答案可能为空。有几种方法可以解决这个问题。

  1. 我们可以在这里删除@Nullable注解,但那样做是不好的,因为答案实际上可能是空的。我们也可以选择忽略这个警告。

  2. 最好的解决方案是在执行任何操作之前实际测试getAnswers方法的结果。就像这样:

ArrayList<Answer> answers = question.getPossibleAnswers();
if (answers == null){
    return;
}
  1. 展开声明冗余 | 方法可以是 void | 问题。它说:
Return value of the method is never used 

  1. 双击问题跳转到代码。嗯,那个警告是正确的,但假设我确实想要返回答案,因为我相当确定(你能有多确定?)我以后会使用它。在这种情况下,你可以右键单击问题,选择对成员进行抑制选项。你将不会再被这个问题打扰,因为它会在你的代码中添加SuppressWarnings注释:
@SuppressWarnings("UnusedReturnValue")public Answer addAnswer(String id, String text){
  1. 最后,看看拼写警告。展开拼写和底层的拼写错误应用项目。就在那里。一个拼写错误
Typo: In word 'getitright' 

我们现在没有getitright,是吗?由于这是我们应用程序的名称,也是包名称的一部分,我相当确定我们可以安全地忽略这个警告。这一次,我们右键单击类型,选择保存到字典选项:

  1. 警告列表似乎是无穷无尽的,但所有这些项目有多严重呢?在 Android Studio 的左侧,你会找到一个带有按严重性分组工具提示的按钮。点击它。

  2. 现在树视图包含一个错误节点(如果有的话),一个警告节点和一个拼写错误节点。如果你只专注于错误和警告,并了解每个项目是关于什么,那么你将改进你的代码,并且实际上会学到很多,因为每个问题都附带了问题的描述和如何修复的建议。

很好,你今天学到了一些很酷的东西!并且通过应用模式、运行单元测试以及修复Android Lint报告的问题来编写更好的代码。

我们现在知道我们的应用程序做了它应该做的事情,并且在一些重构之后它结构良好。

接下来要想的是,如果我们从互联网加载的图像是现在的 10 倍大小会发生什么?如果我们有 1000 个问题呢?不真实?也许。

我们的测验应用在低端设备上的表现如何?在下一章中,我们将寻找这些和其他问题的答案。

另请参阅

  • 参考第九章, 性能

  • 参考第十章, 测试您的应用程序的 Beta 版

第九章:改善性能

性能很重要,因为它会影响您的应用在 Google Play 商店上的评价。我们想要一个五星级的应用!在高端设备上,您的应用可能会顺利运行,没有任何问题,但在用户的低端设备上,情况可能会有所不同。它可能运行缓慢或者内存不足,导致应用崩溃。

改善性能

在本章中,您将学习以下配方:

  • 内存分析器和性能工具

  • 糟糕的应用程序-性能改进

  • 过度绘制问题

介绍

我们如何检测我们的应用是否会有性能问题?Android 应用程序中常见的问题是什么?我们如何解决这些问题?

在性能方面,可能会出现一些问题,如下所示:

  • 内存泄漏:尽管 Android 配备了自己的内存管理系统,但可能会发生内存泄漏。

  • 内存不足异常:您的应用程序可能会很容易耗尽内存,导致应用程序崩溃。例如,在低端设备上处理大图像时会出现这种情况。

  • 过度绘制:过度绘制是指视图上的像素被绘制多次的现象。它可能导致用户界面无响应或延迟。

在接下来的示例中,我们将检查这里列出的问题。Android SDK 和 Android Studio 都配备了一些很好的工具来检查您的应用。

内存分析器和性能工具

您的应用程序可能会受到内存泄漏或分配过多内存的影响。

垃圾收集器GC)负责清理我们不再需要使用的任何东西,这是一个很好的帮手,但不幸的是,它并不完美。它只能删除被识别为不可达的对象。未清理的对象会一直占用空间。过一段时间,如果创建了越来越多的对象,就可能会发生OutOfMemoryError,就像尝试加载一些大图像时会发生的情况,这是许多 Android 应用程序常见的崩溃场景。

内存泄漏有些难以发现。幸运的是,Android Studio 配备了内存监视器。它可以为您提供应用程序内存使用情况的概述,并提供一些关于内存泄漏的线索。

我们将使用这个内存监视器来找出是否不需要的 GC 事件模式导致了性能问题。除此之外,我们将使用分配跟踪器来确定代码中可能存在的问题所在。

准备工作

对于这个配方,如果您已经完成了前几章中的任何一个配方,那将是很好的。如果可能的话,它应该是从互联网获取数据(文本和图像)的配方,例如第二章中的应用程序,具有基于云的后端的应用程序。当然,任何其他应用程序都可以,因为我们将检查工具来检查我们的应用程序以改进它。

如何做...

让我们看看我们的应用程序的性能如何!

  1. 启动 Android Studio 并打开您选择的应用程序。

  2. 在设备上运行您的应用程序(或使用虚拟 Genymotion 设备)。

  3. 内存监视器位于内存选项卡上,您可以在Android选项卡上找到它。

  4. 如果没有显示,请使用Cmd + 6(对于 Windows:Alt + 6)快捷键使其出现。

  5. 运行您的应用程序,查看内存监视器记录您的应用程序的内存使用情况。在下面的示例中,我运行了一个从 FourSquare API 加载了 200 个场馆(包含文本和图片)的应用程序。每次我按下按钮时,我会请求 200 个更多的场馆,导致图表中显示的峰值。请给我更多附近的咖啡店:如何做...

  6. 应用的内存使用显示为深蓝色。未分配的内存显示为浅蓝色。当您的应用开始运行时,分配的内存会增长,直到没有更多的内存,或者当 GC 到达并完成其工作时,它会减少。

  7. 这些都是常见的事件,最终,您可以通过单击左侧窗口上方的“内存”选项卡上的“启动 GC”图标(Initiate GC)来自己调用 GC。

  8. 只有在短时间内分配了大量内存或 GC 事件更频繁时才会引起怀疑。您的应用程序可能存在内存泄漏。

  9. 同样,您可以监视 CPU 使用情况。您可以在Android面板的CPU选项卡上找到它。如果您在这里注意到非常高的峰值,那么您的应用程序可能做得太多了。在下面的截图中,一切看起来都很好:如何操作...

  10. 要了解更多关于内存问题的信息,我们可以使用另一个工具。从“工具”菜单中,选择“Android”和“Android 设备监视器”选项。该工具带有堆视图、内存监视器和分配跟踪器,这些都是提供有关应用程序使用的内存的见解的工具。

  11. 如果尚未选择,请单击顶部导航栏上出现的“Dalvik 调试监视器服务器”(DDMS)按钮。DDMS是一个提供线程和堆信息以及其他一些内容的调试工具。

  12. 选择“堆”选项卡。在窗口的右侧,选择应用程序,应该会出现在设备名称的下方。如果找不到您的应用程序,可能需要重新运行您的应用程序。如何操作...

  13. 内存请求将通过从内存池中分配部分来处理,这称为堆。在任何给定时间,堆的某些部分正在使用,而某些部分未使用,因此可供将来分配使用。

  14. 选项卡可以帮助您诊断内存泄漏,显示系统为您的应用程序分配了多少内存。在这里,您可以确定意外或不必要地分配的对象类型。如果分配的内存不断增加,那么这是您的应用程序存在内存泄漏的强烈迹象。

注意

如果未启用堆更新,请查看“设备”选项卡上的按钮。单击“更新堆”按钮(截图左侧第二个按钮)。

  1. 堆输出仅在 GC 事件之后显示。在堆选项卡上,找到“Cause GC”按钮并单击它以强制 GC 执行其工作。之后,“堆”选项卡将看起来有点像这样:如何操作...

  2. 在上面的截图中显示了关于应用程序堆使用情况的大量信息。单击表中的任何项目以获取更多信息。这里显示的信息可以帮助您确定应用程序的哪些部分导致了太多的分配。也许,您需要减少分配的数量或更早释放内存。

  3. 为了更好地了解您的应用程序的关键部分以及确切导致问题的堆栈跟踪,您可以单击“分配跟踪器”选项卡。

  4. 在该选项卡上,单击“开始跟踪”按钮。

  5. 以某种方式与您的应用程序进行交互,例如刷新列表,转到详细视图或您的应用程序所做的任何操作,并且您想要测量。

  6. 单击“获取分配”按钮以更新分配列表。

  7. 作为您为应用程序启动的操作的结果,您将在此处看到所有最近的分配。

  8. 要查看堆栈跟踪,请单击任何分配。在下一个示例中,我们正在调查在表行中加载图像。跟踪显示了在哪个线程中分配了什么类型的对象以及在哪里。如何操作...

如果愿意,您可以玩一下,以更多了解 Android 设备监视器。既然您已经看到了一些测量结果的工具,让我们更仔细地看看如何处理它们以及如何避免内存问题。我们下一节再见!

还有更多...

Android 设备监视器和 Android Studio 附带的内存工具都有许多其他选项可供您探索。这些选项将帮助您提高应用程序的质量和性能。这将使您和您的应用程序用户感到满意!

另请参阅

  • 第二章, 具有基于云的后端的应用程序

  • 第八章, 提高质量

  • 第十章, 测试您的应用程序

这里是一个糟糕的应用程序 - 性能改进

在 Android 应用程序开发中,有哪些应该做和不应该做的事情,以避免性能问题,即使这些问题可能不会在您自己的设备上出现?测试 Android 应用程序很困难,因为有这么多的设备。谨慎写代码总比抱憾写代码要好。

有人说编写高效代码有两个基本规则:不要做不需要做的工作(因此来自第八章的 DRY 和 YAGNI 原则,提高质量),如果可以避免的话,不要分配内存。除此之外,还有一点很有趣,那就是有各种可用的库,它们不仅可以节省您的时间,而且还可以证明非常高效。当然,重新发明轮子也可能出现错误。

例如,考虑RetroFit库,它将使编写用于消耗网络服务的代码变得更加容易,或者考虑Picasso,这是一个图像加载库,它将通过一行代码从 URL 加载图像,而无需过多担心诸如线程、图像大小调整、转换或内存管理等问题。

总的来说,一些良好的做法如下:

  • 优化位图内存使用。

  • 在隐藏用户界面时释放内存。

  • 不要在布局中使用太多嵌套视图。

  • 不要创建不必要的对象、类或内部类。

  • 在可能的情况下,使用原始类型而不是对象。

  • 如果您不需要对象的任何成员,最好使用静态方法而不是虚拟方法。静态调用会更快。

  • 尽量避免使用内部的 getter 和 setter,因为在 Android 中直接访问字段要快得多。

  • 如果整数可以胜任,就不要使用浮点数。

  • 如果注册了监听器,那么一定要确保取消注册。在活动生命周期的相应对中注册和取消注册。例如,在onCreate方法中注册,然后在onDestroy方法中取消注册。或者,在onResume方法中注册,然后在onPause方法中取消注册。

  • 如果某个操作花费的时间超过几秒钟,请向用户提供反馈。让用户知道您的应用程序并没有死机,而是在忙着处理!通过显示进度指示器来显示正在进行的操作。

  • 始终进行测量。使用性能工具来了解您的应用程序的表现如何。

提示

Android Studio 提示

您在寻找什么吗?按两次Shift键,然后开始输入您要搜索的内容。或者,要显示所有最近的文件,请使用Cmd + E(对于 Windows:Ctrl + E)快捷键。

准备就绪

对于这个示例,您只需要安装并运行 Android Studio,最好还有一个具有互联网访问权限的真实设备。

如何做...

让我们创建一个真正糟糕的应用程序,这样我们就有东西可以修复。我们不会优化位图内存使用。我们会大量使用嵌套视图,做一些其他非常糟糕的事情,对于这个示例,我们将显示有史以来最糟糕的电影列表。这就是糟糕的应用程序:

  1. 在 Android Studio 中创建一个新项目。

  2. 将其命名为BadApp,然后单击下一步按钮。

  3. 勾选手机和平板电脑选项,然后单击下一步按钮。

  4. 选择空白活动,然后单击下一步按钮。

  5. 接受名称并单击完成按钮。

  6. 打开activity_main.xml布局,并用一个具有漂亮背景颜色的列表视图替换内容,该列表视图位于具有另一个漂亮背景颜色的相对布局中。我们这样做是因为我们想在下一个示例中演示概述问题:

<RelativeLayout xmlns:android=  
  "http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:background="@android:color/holo_orange_dark"
    android:paddingBottom="@dimen/activity_vertical_margin" 
    tools:context=".MainActivity">
   <ListView
       android:id="@+id/main_list"
       android:background="@android:color/holo_blue_bright"
       android:layout_width="match_parent"
       android:layout_height="match_parent"></ListView>
</RelativeLayout>
  1. 创建一个新的布局文件,命名为adapter.xml。让我们有一些嵌套视图和许多背景颜色。都是为了糟糕的应用程序。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android= 
 "http://schemas.android.com/apk/res/android"
    android:orientation="vertical"    
    android:layout_width="match_parent"
    android:background="@android:color/holo_green_light"
    android:padding="8dp"android:layout_height="match_parent">
    <ImageView
        android:id="@+id/main_image"
        android:src="img/ic_media_play"android:layout_marginTop="8dp"android:layout_width="80dp"android:scaleType="fitCenter"android:layout_height="60dp" />
    <TableLayout
        android:layout_marginTop="8dp"android:layout_marginLeft="90dp"android:layout_width="match_parent"android:layout_height="wrap_content"><TableRow android:background=  
          "@android:color/holo_purple">
            <TextView android:layout_width="match_parent"
                android:id="@+id/main_text_title"
                android:layout_marginTop="8dp"
                android:textSize="24sp"
                android:layout_height="wrap_content"
                android:textColor="@android:color/white"/>
    </TableRow>
        <TableRow android:background=
           "@android:color/holo_blue_light">
             <TextView android:layout_width="match_parent"android:id="@+id/main_text_year"android:layout_height="wrap_content"android:textSize="20sp"android:layout_marginTop="8dp"android:textColor="@android:color/white"/></TableRow>
        <TableRow android:background= 
           "@android:color/holo_green_dark">
           <LinearLayout
               android:orientation="vertical"android:layout_height="wrap_content"android:layout_width="match_parent"android:layout_marginTop="16dp">
               <TextView android:layout_width="match_parent"android:id="@+id/main_text_genre"android:layout_height="wrap_content"android:textSize="16sp"android:layout_marginTop="8dp"android:background=   "@android:color/holo_green_dark"android:textColor="@android:color/white"/>
                <TextView android:layout_width="match_parent"android:id="@+id/main_text_director"android:layout_height="wrap_content"android:textSize="16sp"android:layout_marginTop="8dp"android:background=
                    "@android:color/holo_green_light"android:textColor="@android:color/white"/>
               <TextView android:layout_width="match_parent"android:id="@+id/main_text_actors"android:layout_height="wrap_content"android:textSize="16sp"android:layout_marginTop="8dp"android:background=  "@android:color/holo_green_dark"android:textColor="@android:color/white"/></LinearLayout>
        </TableRow>
    </TableLayout>
</FrameLayout>
  1. 打开AndroidManifest.xml文件,并添加对互联网访问的权限:
<uses-permission android:name="android.permission.INTERNET" />
  1. 创建一个新类,命名为BadMovie
public class BadMovie {
    public String title;
    public String genre;
    public String year;
    public String director;
    public String actors;
    public String imageUrl;
    public BadMovie(String title, String genre, String 
     year, String director, String actors, String 
      imageUrl){
        this.title = title;
        this.genre = genre;
        this.year =year;
        this.director = director;
        this.actors = actors;
        this.imageUrl = imageUrl;
    }
}
  1. 创建一个适配器类,命名为MainAdapter。我们将使用ViewHolder类,并创建一个单独的线程从网络加载每个电影图像:
public class MainAdapter  extends ArrayAdapter<BadMovie> {
    private Context mContext;
    private int mAdapterResourceId;
    public List<BadMovie> Items = null;
    static class ViewHolder
        TextView title;
        TextView genre;
        ImageView image;
        TextView actors;
        TextView director;
        TextView year;
    }
    @Override
    public int getCount() {
        super.getCount();
        int count = Items != null ? Items.size() : 0;
        return count;
    }
    public MainAdapter(Context context, int adapterResourceId, 
     List<BadMovie> items) {
        super(context, adapterResourceId, items);
        this.Items = items;
        this.mContext = context;
        this.mAdapterResourceId = adapterResourceId;
    }
    @Override
	public View getView(int position, View convertView, 
     ViewGroup parent) {
        View v = null;
        v = convertView;
        if (v == null) {
            LayoutInflater vi = (LayoutInflater)    
            this.getContext().getSystemService(
             Context.LAYOUT_INFLATER_SERVICE);
            v = vi.inflate(mAdapterResourceId, null);
            ViewHolder holder = new ViewHolder();
            holder.title = (TextView) v.findViewById(
             R.id.main_text_title);
            holder.actors = (TextView) v.findViewById(
             R.id.main_text_actors);
            holder.image = (ImageView)       
             v.findViewById(R.id.main_image);
            holder.genre = (TextView)   
             v.findViewById(R.id.main_text_genre);
            holder.director = (TextView) 
             v.findViewById(R.id.main_text_director);
            holder.year = (TextView) 
             v.findViewById(R.id.main_text_year);
            v.setTag(holder);
        }

        final BadMovie item = Items.get(position); 
        if (item != null) {final ViewHolder holder = (ViewHolder) v.getTag();
           holder.director.setText(item.director);
           holder.actors.setText(item.actors);
           holder.genre.setText(item.genre);
           holder.year.setText(item.year);
           holder.title.setText(item.title);
           new Thread(new Runnable() {
            public void run(){
             try {
              final Bitmap bitmap = 
               BitmapFactory.decodeStream((
                InputStream) new  
               URL(item.imageUrl).getContent());
              ((Activity)getContext()).runOnUiThread(new  
              Runnable() {
                  @Override
                  public void run() {                    
                     holder.image.setImageBitmap(bitmap);
                   }
                });
             } 
             catch (Exception e) {
               e.printStackTrace();
             }
            }
          }).start();}
        return v;
    }
}
  1. MainActivity文件中,添加一个包含所有电影的私有成员:
private ArrayList<BadMovie> mBadMovies;
  1. onCreate方法中添加实现,以添加几千部糟糕的电影,为它们创建一个适配器,并告诉列表视图相关信息:
mBadMovies = new ArrayList<BadMovie>();
for (int iRepeat=0;iRepeat<=20000;iRepeat++) {
    mBadMovies.add(new BadMovie("Popstar", "Comedy", "2000", "Paulo Segio de Almeida", "Xuxa Meneghel,Luighi Baricelli", "https://coversblog.files.wordpress.com/2009/03/xuxa-popstar.jpg"));
    mBadMovies.add(new BadMovie("Bimbos in Time", "Comedy","1993", "Todd Sheets", "Jenny Admire, Deric Bernier","http://i.ytimg.com/vi/bCHdQ1MB1D4/maxresdefault.jpg"));
    mBadMovies.add(new BadMovie("Chocolat", "Comedy", "2013", "Unknown", "Blue Cheng-Lung Lan, MasamiNagasawa", "http://i.ytimg.com/vi/EPlbiYD1MmM/maxresdefault.jpg"));
    mBadMovies.add(new BadMovie("La boda o la vida", "1974", "year", "Rafael Romero Marchent", "Manola Codeso, La Polaca", "http://monedasycolecciones.com/10655-thickbox_default/la-boda-o-la-vida.jpg"));
    mBadMovies.add(new BadMovie("Spudnuts", "Comedy", "2005", "Eric Hurt", "Brian Ashworth, Dave Brown, Mendy St. Ours", "http://lipponhomes.com/wp-content/uploads/2014/03/DSCN0461.jpg"));}

//source: www.imdb.com
MainAdapter adapter = new MainAdapter(this, R.layout.adapter, mBadMovies);
((ListView)findViewById(R.id.main_list)).setAdapter(adapter);
  1. 现在运行您的应用程序。根据互联网电影数据库IMDB)的用户,这些是有史以来最糟糕的喜剧电影。我们故意多次添加了这些电影,以创建一个巨大的列表,其中每一行都使用了从互联网加载缩略图的原始方法,如下图所示:操作步骤...

  2. 根据您测试应用程序的设备,您可能需要滚动一段时间,或者错误可能会立即出现。

  3. 这是迟早会出现在LogCat中的。在应用程序崩溃后,检查日志。使用Cmd + 6快捷键(对于 Windows:Alt + 6)显示LogCat。它会显示类似于这样的内容:

packt.com.thebad E/AndroidRuntime﹕ FATAL EXCEPTION: Thread-3529
java.lang.OutOfMemoryError: Failed to allocate a 7238412 byte allocation with 53228 free bytes and 51KB until OOM
  1. 这就是发生的地方:
At packt.com.thebad.MainAdapter$1.run(MainAdapter.java:82)
  1. 还要查看内存和 CPU 监视器。您的设备很难受。如果您滚动列表,就会出现这种情况。

以下屏幕截图提供了内存报告:

操作步骤...

以下屏幕截图提供了CPU报告:

操作步骤...

  1. 如果您想多次加载全尺寸图像,就会得到这样的结果。由于我们无论如何都显示缩略图,因此没有必要这样做,而且您的设备无法处理。让我们来解决这个问题。

注意

我们还存在线程问题,因为错误的图像可能出现在行上。

  1. 尽管最好的解决方案是让服务器返回缩略图而不是大图像,但我们并不总能控制这一点,特别是在处理第三方来源时。因此,解决内存问题的一种方法是在MainAdapter类中加载位图时为BitmapFactory Options设置inSampleSize属性,就像我们在前几章的示例中所做的那样。

  2. 但是,在这里使用Picasso库将更加高效。Picasso是一个流行的图像库,将简化我们的流程。除其他功能外,它将在单独的线程中从互联网加载图像,并将其缩小到其容器的大小,这里是适配器布局中的图像视图。

  3. 打开app文件夹中的build.gradle文件,并添加Picasso的依赖项:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.squareup.picasso:picasso:2.3.3'
}
  1. 保存文件并单击出现的立即同步链接。

  2. 打开MainAdapter类,并用一行代码替换加载图像的线程(以及其中的任何内容)。使用Alt + Enter快捷键添加Picasso导入:

Picasso.with(getContext()).load(item.imageUrl).resize(80,
  60).into(holder.image);
  1. 就是这样。Picasso将负责下载和调整图像的大小。

  2. 现在再次运行应用程序,并随意滚动列表。内存和线程问题都已解决。列表视图可以平滑滚动。

  3. 如果查看Android面板的内存CPU选项卡,您将了解到这样做的区别。

以下屏幕截图提供了内存报告:

操作步骤...

以下屏幕截图提供了CPU报告:

操作步骤...

我们刚刚修复了我们的应用程序,现在能够显示一大堆糟糕的电影。在下一个教程中,我们将检查应用程序是否存在过度绘制问题。在旧的或性能较差的设备上,可能会出现这些问题。

还有更多...

Picasso还有一些其他有趣的功能,比如创建圆形图像,旋转图像,或者自动显示错误或占位图像。

Picasso的替代方案是Universal Image Loader库。

RetroFit是一个强烈推荐的用于 API 通信的库。它是 Android 和 Java 的 REST 客户端,可以节省大量时间和头疼。

注意

Android Studio 提示

想要重构你的代码吗?使用快捷键Ctrl + T(对于 Windows:Ctrl + Alt + Shift + T)来查看你有哪些选项。例如,你可以重命名一个类或方法,或者从一个方法中提取代码。

过度绘制问题

你的应用程序的界面需要快速渲染,例如,滚动列表时的交互应该运行顺畅。特别是旧的或低端设备经常很难做到这些。无响应或缓慢的用户界面可能是结果,这通常是由所谓的过度绘制引起的。

过度绘制是指视图上的像素被绘制多次的现象。一个带有另一个背景颜色的视图的彩色背景就是过度绘制的一个例子(像素被绘制两次),但这并不是真正的问题。然而,过度绘制过多会影响应用程序的性能。

准备就绪

你需要有一个真实的设备,并且需要完成前一个教程中的The Bad应用程序,以演示过度绘制问题,但如果愿意,你也可以检查任何其他应用程序。

如何做...

你的设备包含一些有趣的开发者选项。其中之一是调试 GPU 过度绘制选项,可以通过以下步骤获得:

  1. 在你的设备上,打开设置应用程序。

  2. 选择开发者选项

注意

如果你的设备上没有开发者选项项目,你需要先进入关于设备,然后点击版本号七次。完成后,返回。现在列表中会出现一个名为开发者选项的新选项。

  1. 找到调试 GPU 过度绘制选项并点击它:如何做...

  2. 在弹出的对话框中,选择显示过度绘制区域。

  3. 现在,你的设备看起来有点像没有相应眼镜的 3D 电影,但实际上显示的是:颜色表示过度绘制的数量,没有颜色表示没有过度绘制(像素只被绘制一次),蓝色表示过度绘制 1 次,绿色表示过度绘制 2 次,浅红色表示过度绘制 3 次,深红色表示过度绘制 4 次甚至更多。

提示

最多过度绘制 2 次是可以接受的,所以让我们集中在红色部分。

  1. 运行你想要检查的应用程序。在这个教程中,我选择了前一个教程中的The Bad应用程序进行检查,如下所示:如何做...

  2. 是的,情况非常糟糕。每个视图都有自己的背景颜色,导致过度绘制。

  3. Android 足够智能,可以减少一些过度绘制的情况,但对于复杂的应用程序,你需要自己解决。当你查看前一个教程中的活动和适配器的布局时,这并不难。

  4. 首先,打开activity_main.xml布局文件。删除列表视图中的background属性,因为它根本没有被使用。同时,也从RelativeLayout文件中删除背景属性,因为我不喜欢橙色,至少不适合应用程序。

  5. main_text_genremain_text_directormain_text_actors文本视图中删除background属性。同时,从它们的父视图中删除background属性,即出现在TableLayout中的最后一个TableRow

  6. 如果重新运行应用程序,应用程序不仅会布局得更好一些,而且你还会注意到过度绘制的迹象减少了。

  7. 让我们检查一下是否可以进一步改进。将根部的FrameLayout更改为RelativeLayout。摆脱TableLayout并相对定位文本视图:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android=
  "http://schemas.android.com/apk/res/android"
    android:orientation="vertical"    
    android:layout_width="match_parent"
    android:background="@android:color/holo_green_light"
    android:padding="8dp"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/main_image"android:src="img/ic_media_play"android:layout_marginTop="8dp"android:layout_width="80dp"android:scaleType="fitCenter"android:layout_height="60dp" />
    <TextView android:layout_width="match_parent"android:id="@+id/main_text_title"android:layout_marginTop="8dp"android:layout_toRightOf="@+id/main_image"android:background="@android:color/holo_purple"android:textSize="24sp"android:layout_height="wrap_content"android:textColor="@android:color/white"android:text="Line 1"/>
    <TextView android:layout_width="match_parent"android:id="@+id/main_text_year"android:layout_height="wrap_content"android:layout_toRightOf="@+id/main_image"android:layout_below="@+id/main_text_title"android:background=
         "@android:color/holo_blue_light"android:textSize="20sp"android:layout_marginTop="8dp"android:textColor="@android:color/white"android:text="Line 2"/>
    <TextView android:layout_width="match_parent"android:id="@+id/main_text_genre"android:layout_height="wrap_content"android:layout_toRightOf="@+id/main_image"android:layout_below="@+id/main_text_year"android:textSize="16sp"android:layout_marginTop="8dp"android:textColor="@android:color/white"android:text="Sub  1"/>
    <TextView android:layout_width="match_parent"android:id="@+id/main_text_director"android:layout_height="wrap_content"android:layout_toRightOf="@+id/main_image"android:layout_below="@+id/main_text_genre"android:textSize="16sp"android:layout_marginTop="8dp"android:textColor="@android:color/white"android:text="Sub 2"/>
    <TextView android:layout_width="match_parent"android:id="@+id/main_text_actors"android:layout_height="wrap_content"android:layout_toRightOf="@+id/main_image"android:layout_below="@+id/main_text_director"android:textSize="16sp"android:layout_marginTop="8dp"android:textColor="@android:color/white"android:text="Sub 3"/>
</RelativeLayout>
  1. 再次运行您的应用程序。它变得越来越好了,不是吗?

  2. 要进一步改进您的应用程序,请删除所有text属性。它们只是用来检查我们是否在使用layout_toRightOflayout_below属性时做对了。

在这个示例中,我们通过优化布局进一步改进了我们的糟糕应用程序。而且,它不再难看。实际上,它变得相当不错。

使用哪种布局类型?

使用RelativeLayoutLinearLayout更有效,但不幸的是,如果,例如,您想要移动或删除另一个视图引用的文本视图,则对开发人员不太友好。

FrameLayout要简单得多,但它没有这个问题,而且似乎表现和RelativeLayout一样好。

另一方面,它并不打算包含许多子部件。请注意,最终重要的是最小数量的嵌套布局视图,因此您应该选择适合您的需求并且性能最佳的容器。

太棒了!我们的应用程序在所有设备上都运行流畅。我们不再期望出现任何奇怪的错误。

现在让我们将其发送给我们的 Beta 用户,看看他们对此的看法。一旦我们完成最后一章,我们将讨论临时分发,我们就会知道了。

还有更多...

还有更多有趣的工具,也许您想检查以改进应用程序的质量和性能。

我们之前提到过EspressoRobotium是另一个用于 UI 测试的 Android 测试自动化框架。您可以在robotium.com找到它。

另请参阅

  • 第八章, 提高质量

  • 第十章, 测试您的应用程序的 Beta 版

第十章:测试您的应用程序

您已经尽力确保应用程序的质量和性能。现在是时候将应用程序发布到测试版用户,看看他们对此的看法了。

提示

在发布应用程序之前,您应该先查看 Crashlytics。您可以在try.crashlytics.com找到它。

Crashlytics 可以为您提供实时崩溃报告信息,不仅在测试版测试期间,还在您的应用程序发布到 Play 商店后。迟早,您的应用程序会在您没有测试过的设备上运行,并在其上崩溃。Crashlytics 可以帮助您找到这一原因。

只需下载他们的 SDK,向您的应用程序添加几行代码,然后您就可以开始了。

在将应用程序发布到 Play 商店上向大众公开之前,先分发您的应用程序并进行测试。从他们的反馈中学习并改进您的应用程序。

最后,您可以将这个标志放在您的网站上:

测试您的应用程序

在本章中,您将学习以下内容:

  • 构建变体

  • 运行时权限

  • Play 商店测试版分发

介绍

典型的软件发布周期是这样的,尽管不一定必须经过每个阶段:

Alpha -> 封闭测试版 -> 公开测试版 -> 发布。

您可以直接在 Google Play 商店上发布您的应用程序,但至少进行一轮测试是明智的。收集反馈并进行进一步改进可以使您的应用程序变得更好。

我们将看看如何为您的应用程序设置多个不同的风味,以及如何为其定义不同的构建类型。例如,您的发布应用程序很可能会使用不同的 API 端点,而不是您用于调试和测试的端点,至少我希望如此。

您选择的最低 API 级别、所需功能和所请求的权限将影响您的应用程序在 Play 商店中可用的设备数量。此外,我们将预览 Android Marshmallow 提供的运行时权限需要不同的方法。

最后,我们将找出在 Google Play 商店上分发应用程序的测试版或 Alpha 版本需要做什么。

构建变体

Android Studio 支持应用程序的不同配置。例如,您的应用程序可能会在调试时使用不同的 API 端点。为此,我们将使用构建类型。

除此之外,您可能会有不同版本的应用程序。一个项目可以有多个定制版本的应用程序。如果这些变化很小,例如只是改变了应用程序的外观,那么使用风味是一个好方法。

构建变体是构建类型和特定风味的组合。接下来的教程将演示如何使用这些。

准备工作

对于这个教程,您只需要一个最新版本的 Android Studio。

如何做...

我们将构建一个简单的消息应用程序,该应用程序使用不同的构建类型和构建风味:

  1. 在 Android Studio 中创建一个新项目,命名为WhiteLabelMessenger,在公司域字段中输入公司名称,然后单击确定按钮。

  2. 接下来,选择手机和平板电脑,然后单击下一步按钮。

  3. 选择空白活动,然后单击下一步按钮。

  4. 接受建议的值,然后单击完成按钮。

  5. 打开strings.xml文件并添加一些额外的字符串。它们应该看起来像这样:

<resources>
    <string name="app_name">WhiteLabelMessenger</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>
    <string name="button_send">SEND YEAH!</string>
    <string name="phone_number">Your phone number</string>
    <string name="yeah">Y-E-A-H</string>
    <string name="really_send_sms">YES</string>
</resources>
  1. res/drawable文件夹中创建一个icon.xml和一个background.xml资源文件。

  2. res/drawable文件夹中,创建一个名为icon.xml的新文件。它将绘制一个蓝色的圆圈:

<?xml version="1.0" encoding="utf-8"?>
<shape    
    android:shape="oval">
    <solid
        android:color="@android:color/holo_blue_bright"/>
    <size
        android:width="120dp"
        android:height="120dp"/>
</shape>
  1. res/drawable文件夹中,创建一个名为background.xml的新文件。它定义了一个渐变蓝色背景:
<?xml version="1.0" encoding="utf-8"?>
<selector >
    <item>
        <shape>
            <gradient
                android:angle="90"
                android:startColor="@android:color/holo_blue_light"android:endColor="@android:color/holo_blue_bright"android:type="linear" />
        </shape>
    </item>
</selector>
  1. 打开activity_main.xml文件并修改它,使其看起来像这样:
<FrameLayout xmlns:android=
  "http://schemas.android.com/apk/res/android"
      android:layout_width="match_parent"
     android:layout_height="match_parent"    android:paddingLeft="@dimen/activity_horizontal_margin"       
     android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"android:background="@drawable/background"       
     android:paddingBottom= "@dimen/activity_vertical_margin" 
     tools:context=".MainActivity">
    <EditText
        android:id="@+id/main_edit_phone_number"
        android:layout_marginTop="38dp"
        android:textSize="32sp"
        android:gravity="center"
        android:hint="@string/phone_number"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/main_button_send"android:background="@drawable/icon"android:layout_gravity="center"android:layout_width="200dp"android:layout_height="200dp" />
    <TextView
        android:text="@string/button_send"android:textSize="32sp"android:gravity="center"android:layout_gravity="bottom"android:textColor="@android:color/white"android:layout_width="match_parent"android:layout_height="wrap_content" />
</FrameLayout>
  1. 打开androidmanifest.xml文件并添加一个发送短信的权限:
<uses-permission 
 android:name="android.permission.SEND_SMS"/>
  1. 修改MainActivity文件的onCreate方法。您可以按两次Shift键来显示搜索面板。在搜索面板上输入onCreate,并选择MainActivity类的onCreate方法:
findViewById(R.id.main_button_send).setOnClickListener(this);
  1. MainActivity类上添加一个点击监听器,并实现onClick方法:
public class MainActivity extends Activity implements View.OnClickListener{
@Override
public void onClick(View v) {
    String phoneNumber = ((EditText)findViewById( 
     R.id.main_edit_phone_number)).getText().toString();
    SmsManager sms = SmsManager.getDefault();
    String message = getString(R.string.yeah);
    if (getString(R.string.really_send_sms)  == "YES"){
     Toast.makeText(this, String.format(
      "TEST Send %s to %s", message, phoneNumber), Toast.LENGTH_SHORT).show();
    }
    else {
      sms.sendTextMessage(phoneNumber, null, message, null, 
       null);

      Toast.makeText(this, String.format(
       "Send %s to %s", message, phoneNumber), Toast.LENGTH_SHORT).show();
    }
}
  1. 选择app文件夹。然后,从构建菜单中选择编辑风味

  2. 列表中只包含一个 defaultConfig。单击+按钮添加一个新的风味。将其命名为blueFlavor,并与defaultConfig相同的值作为min sdk versiontarget sdk version

  3. 对于application id字段,使用包名+扩展名.blue

  4. 为该风味输入版本代码版本名称,然后单击确定按钮。

  5. 为另一个风味重复步骤 14 到 16。将该风味命名为greenFlavor

  6. 现在您的build.gradle文件应该包含如下风味:

productFlavors {
    blueFlavor {
        minSdkVersion 21
        applicationId 'packt.com.whitelabelmessenger.blue'targetSdkVersion 21
        versionCode 1
        versionName '1.0'
    }
    greenFlavor {
        minSdkVersion 21
        applicationId 'packt.com.whitelabelmessenger.green'targetSdkVersion 21versionCode 1
        versionName '1.0'
    }
}
  1. 项目面板中,选择app文件夹下的src文件夹。然后,创建一个新文件夹,并命名为blueFlavor。在该文件夹中,您可以保持与main文件夹相同的结构。对于本教程,只需添加一个res文件夹,在该文件夹中再添加一个名为drawable的文件夹即可。

  2. greenFlavor构建的风味执行相同的操作。项目结构现在如下所示:如何操作...

  3. /main/res/drawable文件夹中复制background.xmlicon.xml文件,并将它们粘贴到blueFlavor/res/drawable文件夹中。

  4. greenFlavor重复此操作,并在greenFlavor/res/drawable文件夹中打开background.xml文件。修改其内容。对于绿色风味,我们将使用渐变绿色:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android=
  "http://schemas.android.com/apk/res/android">
    <item>
        <shape>
            <gradient
            android:angle="90"
            android:startColor= 
             "@android:color/holo_green_light"                   
            android:endColor=  
             "@android:color/holo_green_dark"
            android:type="linear" />
        </shape>
    </item>
</selector>
  1. 现在,在同一文件夹中,打开icon.xml文件,并将drawable文件夹也显示为绿色:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android=
   "http://schemas.android.com/apk/res/android"
     android:shape="oval">
    <solidandroid:color="@android:color/holo_green_dark"/>
    <size
        android:width="120dp"android:height="120dp"/>
</shape>
  1. 可以使用相同的方法来为调试和发布构建类型使用不同的值(或类或布局)。在app/src文件夹中创建一个debug文件夹。

  2. 在该文件夹中,创建一个res文件夹,然后在其中创建一个values文件夹。

  3. strings.xml文件从main/res/values文件夹复制并粘贴到debug/res/values文件夹中。

  4. 打开strings.xml文件,并修改really_send_sms字符串资源:

<string name="really_send_sms">NO</string>

提示

当然,为了简单起见,我们将修改字符串资源,而更好的方法当然是使用一个定义不同值的常量类。

构建变体

选择app文件夹,并从构建菜单中选择选择构建变体。它将显示如下截图所示的构建变体面板:

构建变体

构建变体中按照以下步骤进行:

  1. 选择greenFlavorDebug构建变体,并运行应用程序。

  2. 如果一切顺利,应用程序将呈现绿色外观,并且表现得好像正在进行调试。

  3. 现在将构建变体更改为blueFlavorDebug,然后再次运行应用程序。确实,现在它看起来是蓝色的。

构建类型

调试和发布构建类型也基本相同;但是,这次不是外观,而是行为或数据(或者端点)发生了变化。

注意

发布应用程序需要签名,这是我们将在将应用程序分发到 Play 商店时执行的操作,这在上一篇教程中已经描述过了。

构建类型

这基本上就是构建变体的全部内容。大多数理想的构建类型和风味只包含少量修改。如果您的应用程序的各种风味之间的差异不仅仅是在布局、可绘制对象或常量值上进行一些微调,那么您将不得不考虑采用不同的方法。

还有更多...

Android Studio 还提供了一些其他很棒的功能来完成您的应用程序。其中之一是自动生成技术文档。只需向类或方法添加一些注释,就像这样:

/*** This is the main activity where all things are happening*/
public class MainActivity extends Activity implements View.OnClickListener{

现在,如果您从工具菜单中选择生成 JavaDoc,并在出现的对话框中定义输出目录字段的路径,您只需要点击确定按钮,所有文档都将被生成为 HTML 文件。结果将显示在您的浏览器中,如下所示:

更多内容...

注意

Android Studio 提示

您经常需要返回到代码中的特定位置吗?使用Cmd + F3(对于 Windows:F11)快捷键创建书签。

要显示书签列表并从中选择,请使用快捷键Cmd + F3(对于 Windows:Shift + F11)。

运行时权限

您的应用程序将针对不同类型的设备取决于功能要求(需要权限)和您所针对的市场(通过明确选择特定国家或提供特定语言的应用程序)的数量。

例如,如果您的应用程序需要前置摄像头和后置摄像头,那么您将针对较少数量的设备,就像您只需要后置摄像头一样。

通常,在安装应用程序时,用户会被要求接受(或拒绝)所有所需的权限,就像在应用程序的AndroidManifest文件中定义的那样。

随着 Android 6(Marshmallow)的推出,用户被要求特定权限的方式发生了变化。只有在需要某种类型的权限时,用户才会被提示,以便他可以允许或拒绝该权限。

有了这个机会,应用程序可以解释为什么需要这个权限。之后,整个过程对用户来说就更有意义了。这些所谓的运行时权限需要一种稍微不同的开发方法。

对于这个示例,我们将修改之前发送短信的应用程序。现在,我们需要在用户点击按钮后请求用户的权限,以便发送短信。

准备工作

要测试运行时权限,您需要有一个运行 Android 6.0 或更高版本的设备,或者您需要有一个运行 Android Marshmallow 或更高版本的虚拟设备。

还要确保您已经下载了 Android 6.x SDK(API 级别 23 或更高)。

操作步骤...

那么,这些运行时权限是什么样的,我们如何处理它们?可以通过以下步骤来检查:

  1. 从上一个示例中打开项目。

  2. 打开AndroidManifest文件,并添加权限(根据新模型)以发送短信:

<uses-permission-sdk- 
 android:name="android.permission.SEND_SMS"/>
  1. app文件夹中打开build.gradle文件,并将compileSdkVersion的值设置为最新可用版本。还要将每个minSdkVersiontargetSdkVersion的值更改为23或更高。

  2. 修改onClick方法:

@Override
public void onClick(View v) {
    String phoneNumber = ((EditText) findViewById( 
     R.id.main_edit_phone_number)).getText().toString();
    String message = getString(R.string.yeah);
    if (Constants.isTestSMS) {
      Toast.makeText(this, String.format(
       "TEST Send %s to %s", message, phoneNumber), 
       Toast.LENGTH_SHORT).show();
    } 
    else {
      if (checkSelfPermission(Manifest.permission.SEND_SMS)   
       != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{  
              Manifest.permission.SEND_SMS},
                 REQUEST_PERMISSION_SEND_SMS);
        }
    }
}
  1. 添加一个常量值,以便以后我们将知道权限请求的权限结果是指哪个权限请求:
private final int REQUEST_PERMISSION_SEND_SMS = 1;
  1. 实现sendSms方法。我们将使用SmsManager方法将Y-E-A-H文本发送到用户输入的电话号码。一旦消息发送成功,将显示一个 toast:
private void sendSms(){
    String phoneNumber = ((EditText) findViewById( 
     R.id.main_edit_phone_number)).getText().toString();
    String message = getString(R.string.yeah);
    SmsManager sms = SmsManager.getDefault();
    sms.sendTextMessage(phoneNumber, null, 
     getString(R.string.yeah), null, null);
    Toast.makeText(this, String.format("Send %s to %s", getString(R.string.yeah), phoneNumber), Toast.LENGTH_SHORT).show();
}
  1. 最后,实现onRequestPermissionsResult方法。如果授予的权限是短信权限,则调用sendSms方法。如果权限被拒绝,则会显示一个 toast,并且发送按钮和输入电话号码的编辑文本将被禁用:
@Override
public void onRequestPermissionsResult(int requestCode,  String permissions[], int[] grantResults) {
    switch (requestCode) {
        case REQUEST_PERMISSION_SEND_SMS: {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                sendSms();
            }
            else {                
              findViewById(
               R.id.main_edit_phone_number).setEnabled(false); 
              findViewById(  
               R.id.main_button_send).setEnabled(false);
                Toast.makeText(this, 
                 getString(R.string.no_sms_permission), Toast.LENGTH_SHORT).show();
            }
            return;
        }
    }
}
  1. 运行您的应用程序。使用运行 Android 6.0 或更高版本的设备,或者创建一个运行 API 级别 23 或更高版本的虚拟设备。

  2. 现在,发送短信的权限不会被事先要求(也就是说,如果用户安装了应用程序)。相反,一旦您点击发送按钮,就会弹出一个请求权限的对话框。

  3. 如果您同意请求权限,短信将被发送。如果您拒绝了请求的权限,编辑框和按钮将被禁用,并且将显示一个 toast 以提供反馈:操作步骤...

这个示例演示了运行时权限的基本概念。

更多内容...

要了解何时以及如何请求权限,或者何时以及如何提供有关不可用特定功能的反馈意见,您可以在www.google.com/design/spec/patterns/permissions.html上查看 Google 的指南。

注意

Android Studio 提示

您可以轻松地从变得太大的方法中提取代码。只需标记您想要移动的代码,然后使用快捷键Cmd + Alt + M(对于 Windows:Ctrl + Alt + M)。

Play 商店 beta 分发

好了,我们将把我们的应用程序上传到 Play 商店作为 beta 分发。很激动人心,不是吗?

准备工作

对于这个食谱,我们将使用第一个食谱中的应用程序;尽管如此,任何您认为已准备好进行 beta 发布的应用程序都可以。

确保您也有一些艺术作品,例如图标和截图。别担心,对于这个食谱,您也可以从<www.packtpub.com>下载这些项目。此外,考虑您应用程序的元数据,例如标题、描述和类别。

最重要的是您必须拥有开发者帐户,并且可以访问 Google Play 开发者控制台。如果您没有帐户,您需要首先通过developer.android.com/distribute/googleplay/start.html注册。

如何做...

将您的应用程序放入 Play 商店并不难。只是需要一些时间来正确设置事物:

  1. 登录到您的Google Play 开发者控制台网页,或者如果需要的话,首先注册。

  2. 在仪表板上,点击添加新应用程序按钮。

  3. 在对话框中,输入应用程序的标题“蓝色信使”,然后点击立即上传 APK按钮。

  4. 您会注意到productionbetaalpha选项卡。理想情况下,您应该从 alpha 测试开始,但出于演示目的,我们将立即选择beta选项卡。在那里,将显示将第一个 APK 上传到 beta按钮。点击该按钮。

  5. 在 Android Studio 中,打开我们为第一个(或第二个)食谱创建的应用程序,然后从构建菜单中选择生成已签名的 APK选项。

  6. 选择app模块,然后点击下一步按钮。

  7. 输入密钥库的路径。如果没有,请点击创建新...按钮,找到一个适合您的密钥库文件(带有.jks扩展名)的好地方。为其输入一个密码,重复密码,并输入名字的合适值。然后,点击确定按钮。

  8. 输入密钥库密码,创建一个新的密钥别名,并将其命名为whitelabelmessenger。为密钥输入一个密码,然后点击下一步按钮。

  9. 如果需要,输入主密码,然后点击确定按钮。

  10. 如果需要,修改目标路径,然后选择构建类型风味。选择发布blueFlavor,然后点击确定按钮。

  11. 一个新的对话框通知我们,如果一切顺利,已成功创建了一个新的已签名 APK。点击在 Finder 中显示(或者在 Windows 中使用 Windows 资源管理器找到)按钮,以查看刚刚创建的 APK 文件。

  12. 在浏览器中上传此 APK 文件。一旦 APK 文件上传完成,版本将显示在beta选项卡上;您可以选择测试方法并查看受支持设备的数量,这将取决于您选择的 API 级别以及带有短信权限的必需功能(例如,这将立即排除许多平板电脑)。

  13. 对于测试方法,点击设置封闭式 beta 测试按钮。

  14. 点击创建列表按钮创建一个列表。给列表取一个名字,例如内部测试,然后添加测试人员的电子邮件地址(或者只是为了练习,输入您自己的)。完成后,点击保存按钮。

  15. 将您自己的电子邮件地址输入为反馈渠道,然后点击保存草稿按钮。

  16. 尽管我们尚未在商店上发布任何内容,但您需要为商店列表部分输入一些值,这是您可以从网页左侧的菜单中选择的选项:如何做…

  17. 输入标题、简短和长描述。还要添加两张截图、一个高分辨率图标和一个特色图像。您可以从<www.packtpub.com>下载这些资源,或者您可以通过从您的应用中截取截图并使用某种绘图程序进行一些有趣的操作,以使它们具有正确的宽度和高度。

  18. 分类中,选择应用程序作为应用程序类型,并选择社交通讯作为类别

  19. 输入您的联系方式,并选择目前不提交隐私政策(除非您确实希望这样做)。

  20. 点击保存草稿按钮,然后从屏幕左侧的菜单中选择内容评级部分,继续进行。

为您的应用评分

点击继续按钮,输入您的电子邮件地址,并回答有关您的应用是否具有任何暴力、色情或其他潜在危险内容或功能的问题。最后,点击保存问卷按钮:

  1. 现在,您可以点击计算评级按钮。之后将显示您的评级。点击应用评级按钮,然后您就完成了。

  2. 接下来是定价和分发部分。从页面左侧的菜单中选择此选项。

  3. 通过点击免费按钮,使其成为免费应用,并选择所有国家(或者如果您愿意,可以指定特定国家)。之后,点击保存草稿按钮。

  4. 到目前为止,发布应用按钮应该已经启用。点击它。如果它没有启用,您可以点击我无法发布?链接,找出缺少哪些信息。

  5. 在这里,“发布”这个词有点令人困惑。实际上,在这种情况下,它意味着该应用将被发布给您刚刚创建的测试用户名单上的用户。不用担心。在您将应用程序推广到生产环境之前,Play 商店中将不会有任何内容,尽管“发布”这个词似乎暗示了这一点。

  6. 当您的应用状态显示为待发布时,您可以调查一些其他选项,比如您的应用支持的设备列表、所需功能和权限以及用于分析目的的选项,包括功能分割测试(A/B 测试)。

休息一下

待发布状态可能需要几个小时(甚至更长时间),因为自 2015 年 4 月以来,谷歌宣布将事先审查应用程序(以半手动半自动的方式),即使是 alpha 和 beta 版本的分发也是如此。

  1. 吃一个棉花糖,喝点咖啡,或者在公园里散散步。几个小时后回来检查您的应用状态是否已更改为已发布。可能需要一些时间,但会成功的。

注意

您的测试人员可能需要更改其(安全)设置,以允许在 Google Play 商店之外安装应用程序

  1. 还有一些其他看起来令人困惑的事情。在包名称后面,会有一个链接,上面写着在 Play 商店中查看…,还有一个提示说 alpha 和 beta 应用程序不会在 Play 商店中列出。

  2. 在网页左侧的菜单中点击APK项目。通过链接,您将在Beta选项卡上找到Opt In Url,您的测试用户可以通过该链接下载并安装 beta 应用程序:休息一下

太棒了!您的第一个 beta 分发已经准备好进行测试。您可能需要多次迭代才能做到完美,或者也许只需要一个 beta 版本就足以发现您的应用已经准备好进入Play 商店

要在 Play 商店上发布你的应用,点击推广到生产按钮,如果你敢的话…

就到这里吧。还有很多关于 Android 开发的东西要讲和学习,比如服务、Android Pay、近场通讯(NFC)和蓝牙等等;然而,通过阅读这本书,你已经看到了 Android Studio IDE 的大部分元素,这也是我们的目标。

就是这样了。谢谢你的阅读,祝你编码愉快!

还有更多…

你应该意识到,除了技术,方法论同样重要。开发一个不仅在技术上完美,而且有很多用户对你的应用和其流程、可用性和外观都非常满意,给你应得的五星评价的应用是很难的。

我假设你不想花几个月甚至几年的时间开发一个应用,最后发现其实没有人在乎。在早期阶段找出是什么让人们真正想使用你的应用,你应该考虑精益创业方法论来开发你的应用。

构建-测量-学习

精益创业方法论是一种开发企业和产品(或服务)的方法。其理念是基于假设的实验、验证学习和迭代产品发布会导致更短的产品开发周期。

精益创业方法论的最重要的关键元素是:

  • 最小可行产品(MVP)

  • 分割测试和可操作指标

  • 持续部署

简而言之,MVP 是产品的一个版本,需要最小的努力来测试特定的假设。

要了解更多关于精益创业方法论的信息,可以查看网站theleanstartup.com,阅读 Eric Ries 的书,或者从www.leanstartupcircle.com找到一个靠近你的精益创业活动。

Play 商店开发者控制台提供了分割测试和测量应用程序使用情况的选项。谷歌分析可以帮助你做到这一点,因为这是获得可操作指标的最简单方法,你需要收集这些指标以便通过学习改进你的应用程序。

持续部署很好地融入了精益创业方法论。它可以提高应用程序开发的质量和速度。

你可能会想知道持续部署是什么。完全解释这个概念需要另一本书,但这里是对持续集成和持续交付的简要介绍,如果结合起来,就是持续部署的内容。

持续集成(CI)是开发人员提交他们的更改并将结果合并到源代码存储库的过程。构建服务器观察代码存储库的更改,拉取和编译代码。服务器还运行自动化测试。

持续交付是自动创建可部署版本的过程,例如,通过在 Play 商店发布 alpha 或 beta 应用。因此,提交和验证的代码始终处于可部署状态是很重要的。

设置持续部署需要一些前期工作,但最终会导致更小更快的开发周期。

对于 Android 应用程序的持续部署,JenkinsTeamCity都是合适的。Teamcity经常被推荐,并且使用插件可以与 Android Studio 集成。

要了解如何设置TeamCity服务器或找到更多信息,你可以查看 Packt Publishing 的网站,那里有一些很好的书来解释持续集成和TeamCity的概念。

posted @ 2024-05-22 15:08  绝不原创的飞龙  阅读(19)  评论(0编辑  收藏  举报