Android-Studio-学习手册-全-

Android Studio 学习手册(全)

原文:Learn Android Studio

协议:CC BY-NC-SA 4.0

一、Android Studio 简介

本章将引导您安装和设置您的开发环境,以便您可以遵循本书中的示例和实验。首先,您将安装一个名为 Java 开发工具包(JDK)的必备组件。然后,您将下载并安装 Android Studio 以及 Android 软件开发工具包(SDK),这是构建 Android 应用所需的一套软件工具。我们将向您展示如何使用 New Project 向导来创建一个名为 HelloWorld 的简单项目。最后,我们将向您展示如何建立到 Android 虚拟设备(AVD)和物理 Android 设备的连接。本章结束时,你将拥有在 Android Studio 中开始开发应用所需的一切。

在 Windows 上安装 Java 开发工具包

本节适用于 Windows 用户。如果您是 Mac 用户,请跳到“在 Mac 上安装 Java 开发工具包”一节 Android Studio 使用 Java 工具链进行构建,因此在开始使用 Android Studio 之前,您需要确保您的计算机上安装了 Java 开发工具包(JDK)。很有可能你的电脑上已经安装了 JDK,尤其是如果你是一个经验丰富的 Android 或 Java 开发人员。如果您的计算机上已经安装了 JDK,并且运行的是 1.6 版或更高版本,那么您可以跳过这一部分。但是,您可能还是想下载、安装和配置最新的 JDK。您可以从以下 Oracle 站点下载 JDK:

www.oracle.com/technetwork/java/javase/downloads/index.html

当你登陆这个页面时,点击 Java 下载按钮,如图 1-1 所示。

A978-1-4302-6602-0_1_Fig1_HTML.jpg

图 1-1。

The Java Download button on the Java Downloads page

在 Windows 上下载 JDK

如图 1-2 所示,安装的下一步要求您通过单击 Accept License Agreement 单选按钮接受许可协议。然后,您必须为您的操作系统选择合适的 JDK。如果你运行的是 Windows 7 或 Windows 8,你应该点击 Windows x64 标签右边的文件链接,也如图 1-2 所示。Oracle 经常对 JDK 进行版本更新。到本书付印时,几乎可以肯定的是,JDK 的新版本将会推出,所以请务必下载最新版本。等待安装文件下载。这个文件通常在 125MB 左右,所以下载不会花很长时间。

A978-1-4302-6602-0_1_Fig2_HTML.jpg

图 1-2。

Accept the license agreement and click the appropriate link for Windows

在 Windows 上执行 JDK 向导

在安装 JDK 之前,在您的C:驱动器的根目录下创建一个名为Java的目录。这个目录的名字是任意的,尽管我们称它为Java,因为我们要在这里安装的许多工具都与 Java 相关,包括 JDK、Android Studio 和 Android SDK。将与 Android Studio 相关的工具一致地安装在C:\Java目录中也可以让您的开发环境保持有序。

导航到浏览器下载安装文件的位置,双击该文件执行安装。安装开始后,会出现安装向导,如图 1-3 所示。在 Windows 中,JDK 安装程序默认为C:\Program Files\Java\。要更改安装目录位置,请单击“更改”按钮。我们建议将您的 JDK 安装在C:\Java目录中,因为它的路径名中不包含空格,而且容易记忆。见图 1-4 。

A978-1-4302-6602-0_1_Fig4_HTML.jpg

图 1-4。

Select the JDK installation directory

A978-1-4302-6602-0_1_Fig3_HTML.jpg

图 1-3。

Installation Wizard for the JDK on Windows

记下您安装 JDK 的位置。按照提示操作,直到安装完成。如果提示安装 Java Runtime Edition (JRE ),请选择安装 JDK 的目录。

在 Windows 上配置环境变量

本节将向您展示如何配置 Windows,以便 Android Studio 可以找到 JDK。在运行 Windows 的计算机上,按住 Windows 键并按下 Pause 键打开系统窗口。点击高级系统设置选项,如图 1-5 所示。

A978-1-4302-6602-0_1_Fig5_HTML.jpg

图 1-5。

Windows System window

点击环境变量按钮,如图 1-6 所示。在底部的系统变量列表中,如图 1-7 所示,导航到JAVA_HOME项。如果JAVA_HOME项不存在,单击新建创建它。否则,单击编辑。

A978-1-4302-6602-0_1_Fig7_HTML.jpg

图 1-7。

Environmental variables

A978-1-4302-6602-0_1_Fig6_HTML.jpg

图 1-6。

System properties

点击新建或编辑显示类似图 1-8 的对话框。确保在变量名字段中输入JAVA_HOME。在变量值字段中,输入之前安装 JDK 的位置(去掉任何尾随斜线),如图 1-4 所示。现在点击确定。

A978-1-4302-6602-0_1_Fig8_HTML.jpg

图 1-8。

Edit the JAVA_HOME environmental variable

正如您对JAVA_HOME环境变量所做的一样,您将需要编辑PATH环境变量。见图 1-9 。将光标放在变量值字段的末尾,并键入以下内容:

A978-1-4302-6602-0_1_Fig9_HTML.jpg

图 1-9。

Edit the PATH environmental variable

;%JAVA_HOME%\bin

现在,单击确定、确定、确定接受这些更改并退出系统属性。

要测试新的 JDK 是否已正确安装,请单击开始按钮,键入 cmd,然后按 Enter 键。

在命令行窗口中,发出以下命令并按 Enter 键:

java –version

如果您得到如图 1-10 所示的响应,那么恭喜您。您刚刚正确安装了 JDK。

A978-1-4302-6602-0_1_Fig10_HTML.jpg

图 1-10。

Confirm the proper JDK installation

在 Mac 上安装 Java 开发工具包

安装 Mac 版和 Windows 版 JDK 的前两步是相同的。将您的浏览器指向以下站点:

www.oracle.com/technetwork/java/javase/downloads/index.html

当你登陆这个页面时,点击 Java 下载按钮,如图 1-11 所示。

A978-1-4302-6602-0_1_Fig11_HTML.jpg

图 1-11。

The Java Download button on the Java Downloads page

在 Mac 上下载 JDK

点击接受许可协议单选按钮,接受许可协议,如图 1-12 所示。然后,您必须为您的操作系统选择合适的 JDK。如果你运行的是 64 位版本的 OS X,你应该点击 Mac OS X64 标签右边的文件链接,也如图 1-12 所示。Oracle 经常对 JDK 进行版本更新。到本书付印时,几乎可以肯定的是,JDK 的新版本将会推出,所以请务必下载最新版本。等待安装文件下载。

A978-1-4302-6602-0_1_Fig12_HTML.jpg

图 1-12。

Accept the license agreement and click the appropriate link for Mac

在 Mac 上执行 JDK 向导

双击.dmg文件执行它。现在点击.pkg文件开始向导,根据需要点击继续,如图 1-13 到 1-15 所示。

A978-1-4302-6602-0_1_Fig15_HTML.jpg

图 1-15。

Installation success

A978-1-4302-6602-0_1_Fig14_HTML.jpg

图 1-14。

Installation Wizard

A978-1-4302-6602-0_1_Fig13_HTML.jpg

图 1-13。

JDK 8 Update 25.pkg

在 Mac 上配置 JDK 版本

要配置 Mac 以便 Android Studio 可以找到正确的 JDK,请打开 Finder 窗口并选取“应用”“➤实用工具”。在那里,打开 Java Preferences,并按照指示将新版本拖到列表的顶部,以便它被识别为首选版本。

安装 Android Studio

在开始下载 Android Studio 之前,为您将在本书中创建的 Lab 创建一个 Lab 父目录。我们在整本书中使用C:\androidBook\作为我们 Lab 的父目录,但是你可以选择或创建任何你认为合适的目录。因此,我们简单地称它为 labs 父目录。

下载 Android Studio 很简单。将您的浏览器指向此站点:

developer.android.com/sdk/installing/studio.html

现在点击绿色的下载 Android Studio for your OS 按钮,如图 1-16 所示。接下来,选中标记为“我已阅读并同意上述条款和条件”的复选框。再次单击 Download Android Studio for your OS,您的安装文件应该开始下载。下载完成后,执行刚刚下载的文件。

A978-1-4302-6602-0_1_Fig16_HTML.jpg

图 1-16。

Download Android Studio

安装向导开始后,通过单击“下一步”按钮浏览其屏幕,直到到达“选择组件”屏幕。在那里,选中所有组件复选框,如图 1-17 所示。然后单击下一步。再次同意条款和条件。当你进入配置设置:安装位置屏幕,如图 1-18 所示,选择 Android Studio 和 Android SDK 的位置。为了保持一致,我们选择在C:\Java\astudio\安装 Android Studio,在C:\Java\asdk\安装 Android SDK。

A978-1-4302-6602-0_1_Fig18_HTML.jpg

图 1-18。

Select locations for Android Studio and the SDK

A978-1-4302-6602-0_1_Fig17_HTML.jpg

图 1-17。

Choose components

安装 Android Studio 和 Android SDK 时,点击几个后续按钮。你应该最终到达完成 Android Studio 设置屏幕,如图 1-19 所示。“启动 Android Studio”复选框使 Android Studio 能够在您单击“完成”后启动。确保复选框被选中,然后点击 Finish,Android Studio 将启动。请注意,从现在开始,您需要导航到桌面图标或开始菜单来启动 Android Studio。

A978-1-4302-6602-0_1_Fig19_HTML.jpg

图 1-19。

Completing the Android Studio setup

当 Android Studio 第一次启动时,如图 1-20 所示,设置向导将分析您的系统,寻找现有的 JDK(如您之前安装的那个),以及 Android SDK 的位置。设置向导应该下载你在 Android Studio 中开始开发应用所需的一切。单击“完成”按钮退出安装向导。

A978-1-4302-6602-0_1_Fig20_HTML.jpg

图 1-20。

Setup Wizard – Downloading Components

创建您的第一个项目:HelloWorld

一旦设置向导完成,欢迎使用 Android Studio 对话框出现,如图 1-21 所示。单击开始新的 Android 项目选项。

A978-1-4302-6602-0_1_Fig21_HTML.jpg

图 1-21。

Welcome to Android Studio

在出现的新建项目向导中(见图 1-22 ,在应用名称字段中键入 HelloWorld,在公司域字段中键入 gerber.apress.com。请注意,包名是反向公司域加上项目名。将 HelloWorld 项目安装在 labs 父目录的根目录下。如前所述,如果你运行 Windows,我们使用C:\androidBook\。如果您运行的是 Mac 或 Linux,您的 Lab 父目录名称不会以字母开头,而是以正斜杠开头。

A978-1-4302-6602-0_1_Fig22_HTML.jpg

图 1-22。

Configure your new project

Android 操作系统可以在许多平台上运行,包括游戏机、电视、手表、眼镜、智能手机和平板电脑。默认情况下,手机和平板电脑复选框将被选中,API-8 将被选为最低 SDK。接受这些设置,点击下一步,如图 1-23 所示。

A978-1-4302-6602-0_1_Fig23_HTML.jpg

图 1-23。

Select the form factors your app will run on

新建项目向导中的后续屏幕提示您选择布局。选择空白活动,然后单击下一步按钮。接受默认名称,如图 1-24 所示。它们应该如下:

  • 活动名称:主活动
  • 布局名称:活动 _ 主要
  • 标题:MainActivity
  • 菜单资源名称:menu_main

A978-1-4302-6602-0_1_Fig24_HTML.jpg

图 1-24。

Choose options for your new file

使用 Android 虚拟设备管理器

Android 虚拟设备管理器允许您创建 Android 虚拟设备(avd),然后您可以运行它来模拟您计算机上的设备。模拟和仿真之间有一个重要但微妙的区别。模拟意味着虚拟设备只是模拟实际物理设备行为的一个门面,但并不运行目标操作系统。iOS 开发环境使用模拟,考虑到该平台可用的设备数量有限,这可能是 iOS 的一个好选择。

但是,使用仿真时,您的计算机会留出一块内存来再现仿真器正在仿真的设备上的环境。Android Studio 使用模拟,这意味着 Android 虚拟设备管理器启动 Linux 内核和整个 Android 堆栈的沙盒版本,以便模拟物理 Android 设备上的环境。尽管仿真提供了一个比模拟更可靠的环境来测试你的应用,但是启动 AVD 可能需要几分钟,这取决于你的电脑速度。好消息是,当您的模拟器在内存中激活后,它仍然可以响应。然而,如果你有安卓手机或平板电脑,我们建议使用物理设备来测试你的应用,而不是使用 AVD。也就是说,让我们首先使用 Android 虚拟设备管理器设置一个 AVD,在本章的后面,我们将向您展示如何连接您的物理设备,如果您有物理设备的话。

点击图 1-25 中圈出的 Android 虚拟设备管理器图标。在 Andriod 虚拟设备管理器向导的第一个屏幕上,单击创建虚拟设备按钮。在下一个屏幕上,如图 1-26 所示,选择 Galaxy Nexus 并点击下一步。下一个屏幕如图 1-27 所示,允许您选择系统图像。为 ABI 为 x86_64 的 Lollipop(或最新的 API)选择第一个选项。单击下一步。在下一个屏幕上,单击完成按钮验证您的 AVD 设置。恭喜你,你刚刚创建了一个新的 AVD。

A978-1-4302-6602-0_1_Fig27_HTML.jpg

图 1-27。

Select the x86_64 system image

A978-1-4302-6602-0_1_Fig26_HTML.jpg

图 1-26。

Select the Galaxy Nexus hardware

A978-1-4302-6602-0_1_Fig25_HTML.jpg

图 1-25。

AVD icon Note

x86_64 版本需要英特尔硬件加速,它只能在有限数量的英特尔芯片组上运行。如果您尝试安装 x86_64 并且失败,请尝试 armeabi-vxx 版本。

Tip

如果您想要为 Android Studio 还没有设备定义的设备创建 AVD,我们建议您前往 phonearena.com 并搜索您的型号。在那里你可以找到技术规范,你可以用它来创建一个新的设备定义。创建新的设备定义后,使用相同的步骤创建新的 AVD。

市面上有一款优秀的第三方 Android 模拟器,叫做 Genymotion。Genymotion 模拟器是免费的,用于非商业目的,性能非常好。解释如何设置和使用 Genymotion 超出了本书的范围,但是你可以从 genymotion.com 下载 Genymotion 模拟器。

在 AVD 上运行 HelloWorld

要在新创建的 AVD 上运行你的 HelloWorld 应用,点击工具栏上绿色的 run 按钮,如图 1-28 所示。

A978-1-4302-6602-0_1_Fig28_HTML.jpg

图 1-28。

Run button

确保选择了 Launch Emulator 单选按钮,然后在组合框中选择 Galaxy Nexus API 21。点击确定,如图 1-29 所示。请耐心等待,因为启动 AVD 可能需要几分钟时间。你现在应该看到你的 HelloWorld 应用在你电脑的一个窗口中运行,如图 1-30 所示。

A978-1-4302-6602-0_1_Fig30_HTML.jpg

图 1-30。

Emulator screenshot

A978-1-4302-6602-0_1_Fig29_HTML.jpg

图 1-29。

Choosing a device and launching the emulator

在 Android 设备上运行 HelloWorld

如前所述,虽然 avd 对于模拟特定设备很有用,尤其是那些你手头没有的设备,但在物理 Android 设备上开发应用更受欢迎。如果当您通过 USB 电缆将 Android 设备连接到您的电脑时,您的电脑无法识别您的 Android 设备,您可能需要 USB 驱动程序。如果您的计算机最初识别您的 Android 设备,您可能应该放弃安装不同或更新版本的 USB 驱动程序,因为这可能会导致 USB 连接失败。

Note

Mac 和 Linux 用户通常不需要下载 USB 驱动程序来在其 Android 设备和计算机之间建立 USB 连接。

您可以使用developer.android.com/tools/extras/oem-usb.html#Drivers处的表格来查找合适的 USB 驱动程序,或者使用您喜欢的搜索引擎来查找适用于您的型号的 USB 驱动程序。下载驱动程序并安装在您的计算机上。在 Android 设备上,点击设置,然后点击开发者选项。确保选中了 USB 调试复选框。有些设备(如 Samsung 设备)需要密码才能启用 USB 调试,因此您可能希望使用您最喜欢的搜索引擎来研究如何在您的设备上启用 USB 调试。如果这个过程不是很明显的话,YouTube 也是一个很好的视频来源,可以教你如何在你的特定设备上启用 USB 调试。

大多数 Android 设备都配有一根电缆,一端有一个 USB 插头,另一端有一个微型 USB 插头。使用此电缆将您的 Android 设备连接到电脑。点击图 1-31 中圈出的 Android 设备监控按钮。如果驱动程序安装正确,你应该看到设备在那里列出并连接,如图 1-32 所示。

A978-1-4302-6602-0_1_Fig32_HTML.jpg

图 1-32。

Android Device Monitor screen showing the connected physical device

A978-1-4302-6602-0_1_Fig31_HTML.jpg

图 1-31。

Android Device Monitor button Note

请记住,您的计算机和 Android 设备之间的连接是通过使用一个名为 Android Debug Bridge (ADB)的服务器建立的。如果看不到该设备,请单击 IDE 左下角的终端按钮,并发出以下命令:

adb start-server

如果重新启动 ADB 服务器后,您仍然看不到该设备,虽然可能性不大,但 USB 驱动程序需要重新启动系统才能生效。

现在点击绿色的运行按钮(如之前图 1-28 所示)。选择连接的 Android 设备。在图 1-33 中,连接的设备是 HTC One X Android 智能手机。点击 OK,等待几秒钟,你会看到 HelloWorld 在你的 Android 设备上运行。

A978-1-4302-6602-0_1_Fig33_HTML.jpg

图 1-33。

The Choose Device screen with the physical Android device listed

摘要

在这一章中,你安装了 Java 开发工具包,或 JDK,你也安装了 Android Studio 和 Android SDK。您使用新建项目向导创建了一个名为 HelloWorld 的简单应用。然后你创建了一个 Android 虚拟设备,或 AVD。我们向您展示了如何安装任何所需的 USB 驱动程序。最后,我们向您展示了如何在 AVD 和物理 Android 设备上启动 HelloWorld。现在,您应该已经拥有了在 Android Studio 中开发 Android 应用所需的所有软件。

二、导航 Android Studio

Android Studio 是一个窗口环境。为了最大限度地利用有限的屏幕空间,并防止你不知所措,Android Studio 在任何时候都只显示一小部分可用窗口。其中一些窗口是上下文相关的,只有在上下文合适时才会出现,而其他窗口则保持隐藏,直到您决定显示它们,或者相反,保持可见,直到您决定隐藏它们。为了充分利用 Android Studio,您需要了解这些窗口的功能,以及如何和何时显示它们。在这一章中,我们将向你展示如何在 Android Studio 中管理窗口。

任何集成开发环境(IDE)的基本功能之一就是导航。Android 项目通常由许多包、目录和文件组成,一个中等复杂程度的 Android 项目可能包含数百个这样的素材。您使用 Android Studio 的效率将在很大程度上取决于您在这些素材中导航的舒适程度。在本章中,我们还将向您展示如何在 Android Studio 中导航。

最后,我们将向您展示如何使用 Android Studio 中的帮助系统。为了充分利用本章,打开我们在第一章中创建的 HelloWorld 项目。如果这个项目已经在 Android Studio 中打开,您就可以开始了。当我们讨论以下导航功能时,请参考图 2-1 。

A978-1-4302-6602-0_2_Fig1_HTML.gif

图 2-1。

Android Studio’s integrated development environment

编辑

任何 IDE 的主要目的都是编辑文件。正如所料,允许用户在 Android Studio 中编辑文件的窗口位于 IDE 的中央窗格。编辑器窗口在窗口中是唯一的,因为它总是可见的,并且总是位于中心窗格。事实上,编辑器窗口是 Android Studio 的一个非常普遍的特性,从现在开始,我们将它简称为编辑器。Android Studio 中的所有其他窗口被称为工具窗口,聚集在编辑器周围的侧窗格(左侧、底部和右侧)中。

编辑器是一个选项卡式窗口,在这方面,它类似于当代的 web 浏览器。当您从其中一个工具窗口、键盘快捷方式或上下文菜单中打开文件时,该文件将显示为编辑器的一个选项卡。正如您在构建第一个项目 HelloWorld 时已经发现的那样,MainActivity.javaactivity_main.xml文件会作为标签自动加载到编辑器中。Android Studio 试图预测您可能会开始编辑哪些文件,然后在完成新建项目向导后,自动在编辑器中以标签形式打开它们。几乎任何文件都可以在编辑器中打开,尽管原始图像和声音文件还不能在 Android Studio 中编辑。您也可以将文件从工具窗口拖放到编辑器上;这样做可以在编辑器中将文件作为选项卡打开。

编辑器顶部是编辑器标签。沿着编辑器的左边界是装订线,沿着编辑器的右边界是标记栏。让我们依次检查每一个。

编辑器选项卡

要在 Android Studio 的编辑器选项卡中导航,请使用 Alt+右箭头键| Ctrl+右箭头键或 Alt+左箭头键| Ctrl+左箭头键。当然,你可以用鼠标选择一个编辑器标签。编辑器选项卡的选项位于➤编辑器选项卡窗口的主菜单栏中。从该菜单中选择的任何操作都适用于当前选定的选项卡。将鼠标滚动到MainActivity.java选项卡上,右键单击(在 Mac 上按住 Ctrl 键单击)它。在产生的上下文菜单中,如图 2-2 所示,您会注意到许多与您在窗口➤编辑器选项卡中发现的选项相同的选项。从该上下文菜单中,选择选项卡放置子菜单。菜单选项“上”、“下”、“左”和“右”允许您移动标签栏。向右或向左移动标签栏可以容纳更多可见的标签,尽管这是以牺牲屏幕空间为代价的。

A978-1-4302-6602-0_2_Fig2_HTML.jpg

图 2-2。

Editor tab context menu

编辑器选项卡上下文菜单的关闭和关闭所有操作非常简单。当您想要关闭除活动标签之外的所有标签时,可以使用“关闭其他标签”操作。“垂直拆分”和“水平拆分”操作用于将编辑器细分为多个窗格。如果您想并排比较两个文件,垂直拆分特别有用。您可以无限分割窗格,尽管这种嵌套分割的效用会很快减少。您还可以将文件从其他窗口拖放到编辑器的任何窗格,或者从一个窗格拖放到另一个窗格。关闭窗格的最后一个选项卡会导致整个窗格消失。

阴沟

装订线用于传达关于代码的信息。也许 gutter 最明显的特征是小色板和图像图标显示在引用这些视觉资源的相应代码行旁边。装订线还用于设置断点、方便代码折叠和显示范围指示器。所有这些特性将在随后的章节中详细介绍。

标记栏

编辑器右侧是标记栏。标记栏用于指示源文件中重要行的位置。例如,标记栏突出显示 Java 或 XML 文件中的警告和编译时错误。标记栏还显示未提交的更改、搜索结果和书签的位置。

标记栏不会像装订线那样滚动;相反,标记栏上的彩色记号是相对于文件的长度定位的。单击标记栏中的彩色勾号会立即跳转到文件中的该位置。现在,通过单击标记栏上的一些彩色标记来练习使用标记栏。

工具按钮

您已经看到了默认情况下显示在左侧窗格中的项目工具窗口。要查看所有工具窗口的列表,请从主菜单栏中选择查看➤工具窗口。现在仔细观察 IDE 的左、右和下边缘。在那里,您可以找到与许多工具窗口相对应的工具按钮。请注意,其中一些工具按钮还标有数字,该数字与 Alt(Mac 上的 Cmd)键结合使用,以打开/关闭该工具按钮对应的工具窗口。现在尝试单击工具按钮来练习这项技能。还要练习使用快捷键 Alt+1 | Cmd+1、Alt+2 | Cmd+2、Alt+3 | Cmd+3 等等来切换工具窗口的打开/关闭。

当工具窗口打开时,相应的工具按钮是暗灰色的,表示它被按下。请注意,工具按钮位于页边距的角上。例如,“项目”工具按钮的默认位置在左边距的右上角,而“个人收藏”工具按钮默认位于左边距的左下角。

侧窗格(左侧、底部和右侧)一次最多可以由两个工具窗口共享。要查看如何共享侧窗格,请同时打开收藏夹和项目工具窗口。请注意,“个人收藏”和“项目”工具按钮位于同一页边空白的对角。如果两个工具窗口的对应工具按钮位于同一角落,则尝试在这两个工具窗口之间共享侧窗格将不起作用。例如,项目和结构工具窗口不能同时显示——至少在 Android Studio 的默认配置中不能。

默认布局

不要混淆 Android Studio 中的默认布局和 Android SDK 中的布局。默认布局是聚集在编辑器周围的一组特定的工具窗口。Android Studio 配置为开箱即用,默认布局在左侧窗格中显示项目工具窗口。这是之前在图 2-1 中显示的布局。

让我们来看看主菜单栏中的窗口菜单。前两个菜单项是将当前布局存储为默认布局和恢复默认布局。“恢复默认布局”操作通常在 IDE 变得过于拥挤时使用,或者您只想清除白板并返回到熟悉的布局。您也可以自定义默认布局,方法是打开和关闭您喜欢的任何工具窗口,调整它们的大小和/或位置,然后通过选择“将当前布局存储为默认值”将该新布局设置为默认布局。

Repositioning Tool Buttons

如上所述,项目和结构工具窗口不能同时显示,因为它们对应的工具按钮位于同一角落。但是,您可以将任何工具按钮移动到您想要的任何角落。将“结构”工具按钮拖放到左边距的左下角。现在,通过使用键盘快捷键 Alt+1 | Cmd+1 和 Alt+7 | Cmd+7 或通过单击它们的工具按钮来切换项目和结构工具窗口打开。因为我们将它们的工具按钮移动到对角,所以项目和结构工具窗口现在可以共享同一个侧窗格并同时显示。

导航工具窗口

本节讨论专门用于导航的工具窗口:项目、结构、收藏夹、待办事项和命令。表 2-1 列出了每个导航工具窗口的功能。后续章节涵盖了许多其他工具窗口。

表 2-1。

Navigation Tool Windows

| 工具窗口 | 电脑钥匙 | Mac 密钥 | 功能 | | --- | --- | --- | --- | | 项目 | Alt+1 | Cmd+1 | 允许您在项目中导航文件和资源 | | 收藏夹 | Alt+2 | Cmd+2 | 显示收藏夹、书签和断点 | | 结构 | Alt+7 | Cmd+7 | 显示当前文件中对象或元素的分层树 | | 指挥官 |   |   | 类似于项目工具窗口,但允许轻松管理文件 | | 待办事项 |   |   | 显示项目中所有活动待办事项的列表 |

项目工具窗口

我们发现项目工具窗口是最有用的导航工具窗口,因为它结合了广泛的范围和相对容易的访问。要了解项目工具窗口的功能和范围,您可能需要将窗口的模式设定为项目。有三种模式;项目、包和 Android。默认情况下,Android Studio 会将模式设置为 Android。Android 和 Project 是最有用的模式,尽管 Android 模式可能会对你隐藏某些目录。模式设置组合框位于 90 度角,靠近 IDE 左上角的项目工具按钮。项目工具窗口提供了一个简单的树状界面,其中包含可以切换的文件和嵌套目录。“项目工具”窗口提供了项目中所有包、目录和文件的概述。如果在“项目工具”窗口中右键单击(在 Mac 上按住 Ctrl 键单击)某个文件,将会出现一个上下文菜单。此上下文菜单中有三个重要的菜单项:复制路径、文件路径和在资源管理器中显示。单击复制路径将操作系统到该文件的绝对路径复制到剪贴板。单击文件路径会将路径显示为一堆目录,文件位于顶部,单击这些目录中的任何一个都会在操作系统中打开它们。单击“在资源管理器中显示”会在操作系统的新窗口中显示该文件。参见图 2-3 。

A978-1-4302-6602-0_2_Fig3_HTML.jpg

图 2-3。

The Project tool window

结构工具窗口

“结构工具”窗口显示文件中元素的层次结构。当编辑器显示一个 Java 源文件如MainActivity.java时,结构工具窗口显示一个元素树,如字段、方法和内部类。当编辑器显示一个 XML 文件(如activity_main.xml)时,结构工具窗口会显示一个 XML 元素树。单击结构工具窗口中的任何元素都会立即将光标移动到编辑器中的该元素。结构工具窗口对于在大型源文件中的元素间导航特别有用。通过打开结构工具窗口并在MainActivity.javaactivity_main.xml的元素间导航来练习这项技能。见图 2-4 。

A978-1-4302-6602-0_2_Fig4_HTML.jpg

图 2-4。

The Structure tool window

收藏夹工具窗口

在 Android 中开发一个特性(或调试一个 bug)时,您可能会创建或修改几个相关的文件。中等复杂程度的 Android 项目可能包含数百个单独的文件,因此将相关文件分组的能力确实很有用。“收藏夹”工具窗口包含收藏夹,这些收藏夹允许您对相关文件的引用进行逻辑分组,否则这些文件可能位于项目中完全不同的部分。

确保MainActivity.javaactivity_main.xml文件都作为标签加载到编辑器中。现在右键单击(在 Mac 上按住 Ctrl 键)编辑器中的任一选项卡,并从上下文菜单中选择“全部添加到收藏夹”。在输入新的收藏夹列表名称字段中,键入 main,然后按确定。如果收藏夹工具窗口未打开,请通过切换 Alt+2 | Cmd+2 来激活它。展开名为 main 的收藏夹项目,双击其中列出的一个文件将其打开/激活。

就像“收藏夹”窗口允许您立即导航到任何特定的文件或文件组一样,书签允许您立即导航到文件中的任何特定行。将光标定位在MainActivity.java中的任意一行。现在按 F11(在 Mac 上按 F3)。这项动作会在任何来源档案(包括 XML 档案)中建立或移除书签。请注意槽中的勾号和标记栏中指示新书签的黑色勾号。要查看刚刚创建的书签,请在收藏夹工具窗口中切换打开书签。

Note

在 PC 上,如果 F11 似乎没有响应,请检查以确保键盘上的 F-Lock 键已被激活。

断点用于调试。与可以在任何文件中设置的书签不同,您需要在 Java 源文件中设置断点。打开MainActivity.java并点击下一行代码旁边的空白处:

setContentView(R.layout.activity_main);

您会注意到一个红色的圆圈现在占据了檐槽,并且这条线也被突出显示为红色。只能在可执行代码行上设置断点;例如,试图在注释行上设置断点是行不通的。若要查看新创建的断点,请在收藏夹工具窗口中切换打开断点树。你可以用断点做几件更有趣的事情,我们在专门讨论调试的第十二章中详细讨论了断点。

待办事项工具窗口

TODO 的意思当然是做。TODOs 本质上是向程序员和他们的合作者表明还有工作要做的注释。TODO 的写法类似于注释,以两个正斜杠、全大写的 TODO 和一个空格开始。例如:

//TODO inflate the layout here.

MainActivity.java中创建一个待办事项,打开待办事项工具窗口查看。单击 TODO 工具窗口中的 TODO 会立即跳转到源代码中的该 TODO。

管理器工具窗口

“管理器工具”窗口是一个导航辅助工具,有左右两个窗格。这些窗格的功能与项目和结构工具窗口非常相似。Commander 工具窗口与其他导航窗口的不同之处在于,它一次只显示一个目录级别,而不是显示嵌套的目录树。如果您更喜欢 Windows 风格的导航,或者您发现项目工具窗口太大,那么 Commander 工具窗口可能是一个不错的导航选择。

主菜单栏

主菜单栏是 Android Studio 中最上面的栏,你可以通过浏览它的菜单和子菜单来执行几乎任何操作。与 Android Studio 中的其他栏不同,主菜单栏无法隐藏。不要被主菜单栏及其子菜单中包含的众多操作所淹没。即使是最有经验的 Android 开发人员每天也只会使用这些操作中的一小部分,而且大多数操作都有相应的键盘快捷键和/或上下文菜单项。我们将在后续章节中讨论主菜单栏中包含的许多操作。

工具栏

工具栏包含常用文本操作的按钮,如剪切、复制、粘贴、撤消和重做。正如你已经在第一章中看到的,工具栏还包含了 Android Studio 中各种管理器的按钮,包括 SDK 管理器和 Android 虚拟设备管理器。工具栏还有用于设置和帮助的按钮,以及用于运行和调试应用的按钮。工具栏中的所有按钮都有相应的菜单项和键盘快捷键。高级用户可能希望通过取消选中“查看➤工具栏”菜单项来隐藏工具栏,以节省屏幕空间。

导航栏

导航栏显示一系列水平箭头框,代表从项目根目录(左侧)到编辑器中当前选定选项卡(右侧)的路径。导航栏可用于导航项目资源,而无需求助于项目或管理器工具窗口。

状态栏

状态栏如图 2-5 (之前在图 2-1 中)所示,显示相关的和上下文敏感的反馈,例如关于任何运行的进程或者你的项目的 Git 库的状态的信息。现在让我们更详细地研究一下状态栏。

A978-1-4302-6602-0_2_Fig5_HTML.gif

图 2-5。

Status bar

状态栏的最左边是“切换边距”按钮。单击此按钮可切换隐藏和显示页边距。此外,当您将鼠标悬停在此按钮上时,会出现一个上下文菜单,允许您激活任何工具窗口。

消息区域用于提供反馈并显示有关并发运行进程的任何信息。当您将鼠标滚动到 UI 元素(如工具栏中的菜单项或按钮)上时,该区域也会显示提示。单击此区域将打开事件日志。

编辑器光标位置以行:列格式显示光标在编辑器中的位置。单击此区域会激活一个对话框,允许您直接导航到代码中的特定行。

行分隔符区域显示文本文件中使用的回车格式。在 Windows 上,默认为 CRLF,代表回车换行。LF 是 Unix 和 Mac 机器上使用的标准格式,在 Git 中也是如此。如果您在 Windows 计算机上进行开发,当您将代码提交到存储库时,Git 通常会从 CRLF 转换到 LF。

文本格式区域描述用于源文件的文本编码。缺省值是 UTF-8,它是 ASCII 的超集,包含大多数西方字母,包括标准 Java 或 XML 文件中的任何字符。

文件访问指示器区域允许您在读/写和只读之间切换。未锁定的图标意味着编辑器中的当前文件具有读/写权限。锁图标表示编辑器中的当前文件是只读的。您可以通过单击指示器图标来切换这些设置。

“突出显示级别”按钮激活一个带有滑块的对话框,该对话框允许您设置希望在代码中看到的突出显示级别。

默认设置是 Inspections,它对应于皱眉的检查经理的图标。这个设置表明你应该准备好一些严厉的爱,因为检查经理将严格识别语法错误和代码中可能的问题,称为警告。您可以在标记栏中看到由 Inspections Manager 生成的一些黄色标记。

滑块上的下一个设置是语法,它对应于配置文件中检查管理器的图标。对于这种设置,检查经理对警告视而不见。语法模式没有检查模式严格,但仍然会突出显示会阻止代码编译的语法问题。

滑块上的最后一个突出显示模式是 None,它对应于一个微笑的检查经理的图标。这个图标让我觉得检查经理很开心,他根本不关心你的代码。在这种模式下,即使是最严重的语法错误也会被忽略,尽管当您试图构建时,编译器仍然会被它们卡住。我建议把重点放在检查上,并学会欣赏检查经理的严厉的爱。

常见操作

本节回顾 Android Studio 中使用的各种常见操作。如果您使用过像 Microsoft Word 这样的文本编辑器,您可能会对本节中介绍的功能很熟悉。

选择文本

正如您对任何优秀的文本编辑器所期望的那样,双击源文件中的任何单词都会选中它。此外,在字母或单词上单击并拖动光标会选择这些文本元素。将光标放在源文件中的任意位置,然后按 Shift+下箭头键或 Shift+上箭头键选择从光标处开始的文本行。在一行文本的任意位置连按三次会选择整行。按 Ctrl+A | Cmd+A 选择文件中的所有文本。

如果将光标放在任何单词内并按 Ctrl+W | Alt+向上键,则整个单词都将被选中。如果您继续按 Ctrl+W | Alt+向上箭头键,所选内容将无限扩大,以包括相邻的文本。如果您现在按 Ctrl+Shift+W | Alt+向下箭头键,选择范围将缩小。这种增长/收缩选择功能在 Android Studio 中被称为结构化选择。

使用撤消和重做

“撤消”和“重做”命令对于回滚和前滚有限数量的编辑操作非常有用。更改由特定的 UI 事件来限定,例如按 Enter 键或重新定位光标。撤消和恢复的键盘快捷键分别是 Ctrl+Z | Cmd+Z 和 Ctrl+Shift+Z | Cmd+Shift+Z。在工具栏的左边有紫色的左右箭头也可以做到这一点。Android Studio 的默认设置是记住你上次保存的所有步骤,或者最多 300 步。撤销和重做一次只应用于一个文件,所以回滚更改最有效的方法是使用 Git,这在第七章中讨论。

查找最近的文件

Android Studio 最好的特性之一是它能记住你最近处理过的所有文件。要激活此命令,请选择“查看➤最近的文件”或按 Ctrl+E | Cmd+E。出现的对话框允许您选择任何最近的文件,并在编辑器中将其作为选项卡打开。默认限制是最多记忆 50 个以前的文件。您可以通过导航到文件➤设置➤限制➤编辑器➤最近的文件限制来更改这些限制。

遍历最近的导航操作

Android Studio 还会记住你最近的导航操作。导航操作包括光标移动、选项卡更改和文件激活。若要遍历导航操作历史记录,请按 Ctrl+Alt+左箭头键| Cmd+Alt+左箭头键或 Ctrl+Alt+右箭头键| Cmd+Alt+右箭头键。请记住,导航操作不同于编辑操作;如果你想遍历你的编辑操作,你应该使用撤销和重做。

剪切、复制和粘贴

如果你使用过任何文本编辑器或文字处理器,你应该熟悉剪切、复制和粘贴。表 2-2 列出了这些基本命令,以及一些扩展的剪贴板命令。

表 2-2。

Cut, Copy, and Paste

| 命令 | 电脑钥匙 | Mac 密钥 | | --- | --- | --- | | 切口 | Ctrl+X | Cmd+X | | 复制 | Ctrl+C | Cmd+C | | 粘贴 | Ctrl+V | Cmd+V | | 扩展粘贴 | Ctrl+Shift+V | Cmd+Shift+V | | 复制路径 | Ctrl+Shift+C | Cmd+Shift+C | | 复制参考 | Ctrl+Alt+Shift+C | Cmd+Alt+Shift+C |

除了操作系统剪贴板提供的简单剪切、复制和粘贴功能,Android Studio 还有一个扩展的剪贴板,可以记住最近五次剪切和复制操作。当你从 Android Studio 中剪切或复制文本时——或者在 Android Studio 运行时从几乎任何其他应用中剪切或复制文本时——Android Studio 会将这些文本放到一个堆栈中。要查看扩展的剪贴板堆栈,请按 Ctrl+Shift+V | Cmd+Shift+V。出现的对话框允许您选择要粘贴的项目。参见图 2-6 。

A978-1-4302-6602-0_2_Fig6_HTML.jpg

图 2-6。

Extended clipboard

您还可以通过导航到文件➤设置➤限制➤编辑器➤剪贴板中保留的最大内容数来更改扩展剪贴板堆栈的大小。您也可以通过右键单击所选内容并选择“与剪贴板比较”菜单项,将任何当前选定的文本与扩展剪贴板中最新元素的文本进行比较。

“复制路径”命令 Ctrl+Shift+C | Cmd+Shift+C 复制在“项目”或“管理器”工具窗口或编辑器的任何选项卡中选择的任何文件或目录的完全限定的操作系统路径。复制路径对于终端会话中的操作特别有用。

使用复制引用 Ctrl+Alt+Shift+C | Cmd+Alt+Shift+C,Android Studio 允许您复制对方法、变量或类的逻辑引用。当您将这个引用粘贴到另一个源文件中时,Android Studio 会自动包含任何必需的包限定符和导入。您还可以在“项目”和“管理器”工具窗口中对包、目录和文件使用通用的剪切、复制和粘贴来代替鼠标操作(如拖放),以便重新组织资源在项目中的位置。

上下文菜单

在 IDE 上右键单击(在 Mac 上按住 Ctrl 键单击)可以激活许多上下文菜单。您已经在上一节中探索了编辑器选项卡上下文菜单。Android Studio 中的大多数窗格、图标和栏都会生成一个上下文菜单,如果你右击它(在 Mac 上是 Ctrl-click)。Android Studio 最大的特点之一是动作可以用多种方式执行。这种冗余意味着你可以根据自己的喜好自由发展自己的技能和习惯。我发现对最频繁的操作使用键盘快捷键,对不太频繁的操作使用菜单和上下文菜单操作是与 Android Studio 交互的最有效方式。现在,通过右键单击(在 Mac 上按住 Ctrl 键单击)IDE 中的栏、选项卡、窗格和文件来浏览上下文菜单。

获得帮助

Android Studio 中的帮助菜单有几个有用的菜单项。find Action(Ctrl+Shift+A | Cmd+Shift+A)是你在 Android Studio 中获取帮助最常用的命令。该命令会激活一个对话框,允许您在 Android Studio 中搜索任何功能。按 Ctrl+Shift+A | Cmd+Shift+A 并在搜索框中键入显示行号。现在使用箭头键选择设置,并按下回车键。在设置窗口中,选择编辑器➤外观。您应该看到“显示行号”复选框。

选择帮助➤在线文档是你的所有 Android Studio 技术规范的来源。这是 Android Studio 最全面的文档。此外,帮助➤默认键映射参考菜单项是一个有用的参考。当你学习使用 Android Studio 时,你可以考虑打印这个 PDF 文件并把它放在附近。

使用键盘导航

键盘可能是在 Android Studio 中导航的最强大的方式。从主菜单栏中选择导航菜单以检查其内容。本节讨论导航菜单中最重要的菜单项(如表 2-3 所示)及其相应的键盘快捷键。后续章节讨论其他菜单项。

表 2-3。

Keyboard Navigation

| 命令 | 电脑钥匙 | Mac 密钥 | | --- | --- | --- | | 在中选择 | Alt+F1 | Alt+F1 | | 班级 | Ctrl+N | Cmd+O | | 文件 | Ctrl+Shift+N | Cmd+Shift+O | | 线条 | Ctrl+G | Cmd+L | | 相关文件 | Ctrl+Alt+Home | alt+Cmd+向上箭头 | | 上次编辑位置 | ctrl+Shift+退格键 | cmd+Shift+退格键 | | 类型层次结构 | Ctrl+H | Ctrl+H | | 申报 | Ctrl+B | Cmd+B |

在中选择

Android Studio 最好的一个特性就是导航是双向的。您已经看到了如何从各种工具窗口中打开/激活作为编辑器选项卡的文件。现在,您将学习如何从编辑器导航到各种工具窗口。

按 Alt+F1。这将激活“在上下文中选择”菜单,该菜单包含几个菜单项,包括“项目视图”、“收藏夹”和“文件结构”。单击“项目视图”选项。项目工具窗口被激活,对应于编辑器的活动选项卡的文件被突出显示,并且该文件的任何父目录被切换打开。Android 项目往往有很多文件素材;因此,在中使用 Select 是您将掌握的最重要的技能之一。

班级

类操作允许您导航到特定的 Java 类。值得注意的是,这个操作只搜索 Java 源文件,或者 Java 源文件的内部类。按 Ctrl+N | Cmd+O,开始输入 act。Android Studio 已经为你的所有文件建立了索引,所以它会为你提供一个可能匹配的列表,最可能匹配的会高亮显示。你所需要做的就是按回车键打开MainActivity.java

文件

文件操作允许您导航到项目中的任何文件。如果您正在项目中寻找一个 XML 文件,这就是您想要使用的操作。按 Ctrl+Shift+N | Cmd+Shift+O,开始输入 act。我们特意使用了相同的搜索术语 act 来说明“导航➤”文件的更大范围。注意,搜索结果包括 Java 源文件MainActivity.java以及任何其他文件,比如activity_main.xml。使用箭头键选择activity_main.xml并按 Enter 键打开它。

线条

行操作 Ctrl+G | Cmd+L 激活一个对话框,允许您导航到源文件的特定行:列。如果你在弹出的“转到第一行”对话框中输入一个简单的整数,然后按“确定”, Android Studio 会跳到那一行,而不考虑第几列。

相关文件

相关的文件动作 Ctrl+Alt+Home | Alt+Cmd+向上箭头是 Android Studio 中最有用的命令之一。Android 项目通常有很多相关文件。例如,一个简单的 Android 活动通常至少有一个相应的 XML 布局文件来呈现活动的布局,还有一个相应的 XML 菜单文件来呈现活动的菜单。当您处理片段时,这种复杂性只会增加。您已经看到了如何使用收藏夹将相关文件分组在一起。通过导航➤相关文件,你可以查询 Android Studio 来显示相关文件。激活MainActivity.java标签后,按 Ctrl+Alt+Home | Alt+Cmd+上箭头键。您应该会看到activity_main.xml列在那里。使用箭头键选择它,然后按 Enter 键。

上次编辑位置

最后一次编辑位置操作 Ctrl+Shift+back space | Cmd+Shift+back space 允许您导航到最后一次编辑。如果您继续激活此命令,光标将移动到您上次编辑的文件/位置,依此类推。

类型层次结构

Android 使用 Java,一种面向对象的编程语言。任何面向对象语言的标志之一是继承,这有利于代码重用和多态。当编辑器中的MainActivity.java文件处于活动状态时,按 Ctrl+H 切换打开层次工具窗口。在那里你会看到一系列层叠的对象,所有这些对象都可以追溯到 Java 中所有对象的祖先Object。请记住,只有当编辑器中的活动选项卡是 Java 源文件时,导航➤类型层次结构操作才有效。

申报

声明操作允许您跳转到方法、变量和资源的原始定义。激活此操作的另一种方法是按住 Ctrl|Cmd 键,同时将鼠标滚动到文件中的方法、变量或资源上。如果元素带有下划线,您可以通过在按住 Ctrl|Cmd 键的同时左键单击该元素来导航到它的声明。在MainActivity.java中,点击方法setContentView(...)中的任意位置,然后按 Ctrl+B | Cmd+B。您将立即被带到这个方法的声明,它位于MainActivity的一个超类ActionBarActivity.java中。

查找和替换文本

查找和替换文本是编程的一个重要部分,Android Studio 有一套强大的工具来帮助你做到这一点。本节涵盖了一些最重要的工具。表 2-4 为你列出。

表 2-4。

Find and Replace

| 命令 | 电脑钥匙 | Mac 密钥 | | --- | --- | --- | | 发现 | Ctrl+F | Cmd+F | | 在路径中查找 | Ctrl+Shift+F | Cmd+Shift+F | | 替换 | Ctrl+R | Cmd+R | | 在路径中替换 | Ctrl+Shift+R | Cmd+Shift+R |

发现

Find 动作用于查找单个文件中出现的文本。在MainActivity.java中,按 Ctrl+F | Cmd+F 调出一个出现在编辑器顶部的搜索栏。在搜索栏的搜索框中键入 action。您会注意到action在整个文件中立即以黄色突出显示。您还会注意到标记栏中的绿色小记号,指示找到的文本的位置。将鼠标滚动到查找栏上的双右箭头上会显示高级搜索选项。

在路径中查找

“在路径中查找”动作允许您在比前面描述的“查找”动作更大的范围内进行搜索。您也可以使用正则表达式,并用文件掩码分隔结果。按 Ctrl+Shift+F | Cmd+Shift+F,并在编辑器顶部搜索栏的搜索框中键入 hello。默认情况下,“查找路径”中的搜索范围设置为“整个项目”,尽管您可以将搜索范围限制为特定的目录或模块。接受缺省值“整个项目”,然后单击“查找”按钮。结果显示在“查找工具”窗口中。在“查找工具”窗口中单击一个条目会立即将包含的文件作为编辑器的一个新选项卡打开,并跳转到该事件。

替换

替换操作 Ctrl+R | Cmd+R 用于替换单个文件中的文本,替换功能是查找的超集。替换文本的更安全的方法是使用重构➤重命名命令,我们将在后面介绍。

在路径中替换

“在路径中替换”操作 Ctrl+Shift+R | Cmd+Shift+R 是“在路径中查找”的超集。然而,使用重构➤重命名几乎总是比在路径中使用替换更好,所以使用这个命令要非常小心,因为你可能会引入错误。

摘要

在本章中,我们已经讨论了编辑器和聚集在编辑器周围的工具窗口。我们已经讨论了如何使用工具按钮和重新定位它们。我们还讨论了用于导航的工具窗口和 IDE 的主要 UI 元素,包括主菜单栏、工具栏、状态栏、装订线和标记栏。我们还讨论了如何使用菜单和键盘快捷键,以及使用查找和替换来搜索和导航。最后,我们讨论了如何使用 Android Studio 中的帮助系统。最重要的是,我们已经在 Android Studio 中建立了一个 UI 元素词典,我们将在后续章节中引用它。

三、在 Android Studio 中编程

本章讲述了如何在 Android Studio 中编写和生成代码。Android Studio 利用其面向对象编程的知识来生成极其相关且格式良好的代码。本章涵盖的功能包括覆盖方法、用 Java 块包围语句、使用模板插入代码、使用自动完成、注释代码和移动代码。如果你阅读这本书的目标是掌握 Android Studio,你会想要特别关注这一章,因为这里描述的工具和技术将对你的编程效率产生最大的影响。

我们开始吧。如果您在第一章中创建的 HelloWorld 应用尚未打开,请立即打开它。

使用代码折叠

代码折叠是节省编辑器屏幕空间的一种方式。代码折叠允许您隐藏特定的代码块,以便您可以专注于您感兴趣的那些代码块。如果MainActivity.java没有打开,按 Ctrl+N | Cmd+O 并键入 Main 将其打开。按 Enter 键打开MainActivity.java类,如图 3-1 所示。

A978-1-4302-6602-0_3_Fig1_HTML.jpg

图 3-1。

Use the Enter Class Name dialog box to open MainActivity.java

如果默认情况下不显示行号,请导航以帮助➤找到操作。键入显示行号并选择显示行号活动编辑器选项,如图 3-2 所示。

A978-1-4302-6602-0_3_Fig2_HTML.jpg

图 3-2。

Use the Enter Action or Option Name dialog box to show line numbers

当你观察MainActivity.java中的行号时,你会注意到一些奇怪的事情:行号不是连续的。在图 3-3 中,行号从 1、2、3 开始,然后跳到 7、8、9。仔细看图 3-3 中的第 3 行。您会注意到在 import 语句的左边有一个加号,后面有一个省略号。如果您仔细查看自己的代码,您还会注意到省略号以淡绿色突出显示。所有这些视觉元素都在告诉你,Android Studio 隐藏了一段已经折叠的代码。

A978-1-4302-6602-0_3_Fig3_HTML.jpg

图 3-3。

Folded block of code at the import statement

一条称为折叠轮廓的细虚线位于左边距的长度方向,在灰色装订线和白色编辑器之间。折叠轮廓可以包含三个图标:包围在方框中的加号图标(如图 3-3 的第 3 行)和上下箭头,上下箭头内有水平线(见图 3-3 的第 12 行和第 15 行)。向下箭头表示可折叠代码块的开始,而向上箭头表示可折叠代码块的结束。如上所述,加号框表示代码块已经被折叠。单击这些图标中的任何一个都可以将相应的块切换到折叠或展开状态。表 3-1 包含所有代码折叠操作的描述和键盘快捷键。

表 3-1。

Code-Folding Options

| [计]选项 | 电脑钥匙 | Mac 密钥 | 描述 | | --- | --- | --- | --- | | 发展 | ctrl+数字加 | cmd+数字加 | 展开光标所在的折叠块 | | 倒塌 | ctrl+数字-减号 | cmd+数字-减号 | 折叠光标所在的展开块 | | 全部展开 | ctrl+Shift+数字加 | cmd+Shift+数字加 | 展开窗口中的所有代码 | | 全部折叠 | ctrl+Shift+数字-减号 | cmd+Shift+数字-减号 | 折叠窗口中的所有代码 | | 切换折叠 | ctrl+句点 | cmd+句点 | 折叠/展开光标所在的块 |

将光标放在MainActivity.javaonCreate()方法内的任何地方。现在按几次 Ctrl+句点| Cmd+句点来切换此块的展开和折叠。还可以尝试使用展开键盘快捷键 Ctrl+Numeric-Plus | Cmd+Numeric-Plus 和折叠键盘快捷键 Ctrl+Numeric-Minus | Cmd+Numeric-Minus。

最后,通过单击折叠轮廓中的代码折叠图标,使用鼠标切换折叠和展开的块。请记住,为了节省屏幕空间,折叠单个块、多个块甚至文件中的所有块只是将它们从视图中删除。然而,编译器仍然会在您构建时尝试编译它们。同样,折叠包含有问题或错误代码的块不会从标记栏中删除任何警告或错误。您可以通过选择菜单选项设置➤编辑器➤代码折叠来更改代码折叠选项。

执行代码完成

大多数当代 ide 都提供某种形式的代码完成,Android Studio 也不例外。Android Studio 随时准备提供帮助,即使你没有主动寻求帮助。在实践中,这意味着 Android Studio 会在你输入的时候默认建议各种选项来完成你的代码。Android Studio 生成的建议列表并不总是完美的,但是这些建议是根据最佳实践排序的,并且它们通常符合适当的命名约定。Android Studio 对 Android SDK 和 Java 编程语言都非常了解;事实上,它可能比你更了解这些学科。如果你带着谦逊和渴望学习的态度来使用这个工具,不管你以前的编程经验如何,你最终都会看起来像一个摇滚明星。

代码完成特性是上下文相关的,因为根据光标的范围,提供给你的建议会有所不同。如果您在类范围内键入代码,代码完成建议将与您在方法范围内键入的建议不同。即使您选择不接受代码补全建议,出于上述原因,您也应该注意它们。

表 3-2 列出了 Android Studio 中的四种代码补全:

表 3-2。

Code-Completion Options

| [计]选项 | 电脑钥匙 | Mac 密钥 | 描述 | | --- | --- | --- | --- | | 默认 | 没有人 | 没有人 | 默认代码完成行为。Android Studio 会在您输入时在光标旁边显示一个建议列表。您可以使用上下箭头键在建议列表中的条目间导航,并使用 Enter 键选择一个条目。 | | 基础 | ctrl+空格 | ctrl+空格 | 基本代码完成功能类似于默认代码完成,但也在当前选定的条目旁边显示 Javadoc 窗口。单击 Javadoc 窗口中的向上箭头图标会显示详细的文档。 | | 智能类型 | ctrl+Shift+空格键 | ctrl+Shift+空格键 | SmartType 代码完成功能类似于 Basic,但会生成一个更具选择性和相关性的建议列表。 | | 循环扩展字 | Alt+/ | Alt+/ | 提供文档中已经使用过的单词。循环往复。 | | 循环扩展字(向后) | Alt+Shift+/ | Alt+Shift+? | 提供文档中已经使用过的单词。循环下降。 |
  • 一旦您开始键入,默认代码自动完成。
  • 基本代码完成的行为类似于默认代码完成,但还会在建议列表中当前所选项的旁边显示一个 Javadoc 窗口。
  • SmartType 代码完成也显示 Javadoc,但也生成更具选择性和相关性的建议列表。
  • 循环展开单词循环显示源文档中已经使用的单词,并允许您选择它们。

让我们开始编码来演示代码完成是如何工作的。右键单击(在 Mac 上按住 Ctrl 键)包com.apress.gerber.helloworld并选择新建➤ Java 类,弹出新建类对话框,如图 3-4 所示。将该类命名为 Sandbox,然后单击“确定”。

A978-1-4302-6602-0_3_Fig4_HTML.jpg

图 3-4。

Create New Class dialog box

Sandox.javaSandbox类的括号内,通过输入 private Li 开始定义成员,如图 3-5 所示。将出现一个代码完成菜单,其中列出了可供您完成代码的可能选项。使用上下箭头键导航代码完成菜单。用向下箭头键选择List<E>选项,然后按回车键。

A978-1-4302-6602-0_3_Fig5_HTML.jpg

图 3-5。

A code-completion menu appears when you start typing

Android Studio 中的默认行为是在开始输入时显示代码完成建议列表。您不需要激活任何键盘快捷键来调用默认代码完成—它会自动发生。你现在应该有一行代码显示为private List,如图 3-6 所示。直接在单词List后面,键入用于在 Java 中定义泛型的左尖括号(<)。请注意,Android Studio 用一个右尖括号来结束括号子句,并将光标放在括号内。

A978-1-4302-6602-0_3_Fig6_HTML.jpg

图 3-6。

Code completion of a list with String as the generic

在尖括号内键入 Str,然后按 Ctrl+Space 调用基本代码完成。您会注意到,在建议列表中当前选定的项目(String)旁边会出现一个 String Javadoc 窗口的文档。滚动 Javadoc 窗口查看String的 Javadoc 文档。单击 Javadoc 窗口中的向上箭头,在默认浏览器中显示String的详细 API 文档。返回 Android Studio,选择String作为通用类,通过按回车键定义List<String>时使用。

Android Studio 最好的特性之一就是它会为你建议变量名。在private List<String>后直接输入一个空格,按 Ctrl+Space 激活基本代码补全。Android Studio 生成了一个建议列表,但是没有一个变量名具有足够的描述性,所以改为键入 mGreetings。小写的m代表成员(也称为字段),在类成员名前面加上m是 Android 中的命名约定。同样,静态类成员的前缀是小写的s。您不需要遵循这种命名约定,但是如果您这样做,您的代码将更容易被他人理解。请记住,局部(方法作用域)变量不遵循ms前缀命名约定。

修改您的代码行,使其现在显示为private List<String> mGreetings = new。通过按 Ctrl+Shift+Space 调用 SmartType 代码完成。选择ArrayList<>()完成该语句,包括终止分号,如图 3-7 所示。SmartType 代码完成类似于基本代码完成,只是在建议列表中生成项目时,它考虑的变量范围比默认和基本代码完成更广。例如,在赋值操作符的右侧使用 SmartType 代码完成时,建议列表通常会包含相关的工厂方法。

A978-1-4302-6602-0_3_Fig7_HTML.jpg

图 3-7。

SmartType code completion Note

如果您在 Android Studio 中的 JDK 设置为 7 或更高,那么代码完成生成的代码可能会使用菱形符号。例如,ArrayList<String>可能以ArrayList<>的形式出现在使用泛型的赋值语句的声明的右侧,如图 3-7 所示。

循环展开词有个花里胡哨的名字,其实很简单。按住 Alt 键,同时按几次正斜杠,调用循环展开 Word。提供给您的单词与您的文档中出现的单词相同。当你循环阅读单词时,注意黄色的高亮部分。现在通过按住 Alt 和 Shift 键同时按下正斜杠(Mac 上的问号)几次来调用循环向后展开单词。请注意,提供/突出显示的单词现在向下循环并远离光标,而不是向上循环并远离光标。

注释代码

如果你曾经做过任何编程,你就会知道注释是被编译器忽略的代码行,但是它包含了对编码者和他们的合作者来说很重要的信息和元数据。注释可以是以两个正斜杠开始的行注释,也可以是以一个正斜杠和一个星号开始并以一个星号和一个正斜杠结束的块注释。从主菜单中,您可以通过选择代码➤注释来激活注释。然而,激活注释的最佳方式是使用表 3-3 中列出的键盘快捷键。

表 3-3。

Commenting Options

| [计]选项 | 电脑钥匙 | Mac 密钥 | 描述 | | --- | --- | --- | --- | | 切换注释行 | Ctrl+/ | Cmd+/ | 使用 Java 行注释样式(例如// …)在注释或取消注释之间切换行。您可以通过选择这些行将此操作应用于多行。 | | 切换注释块 | Ctrl+Shift+/ | Alt+Cmd+/ | 使用 Java 块注释样式(如/* … */)在注释块和未注释块之间切换选定的文本。将注释块应用到所选文本将包括注释块中的所有所选文本。 |

mGreetings声明上方键入 refactor initial ization to constructor。按 Ctrl+/ | Cmd+/将此文本转换为注释,如图 3-7 所示。使用快捷键 Ctrl+/ | Cmd+/,尝试打开和关闭此注释。

使用代码生成

如果使用得当,代码生成将会为您节省最多的时间。代码生成有能力为你生成各种各样的方法,包括构造器、getters、setters、equals()hashCode()toString(),等等。

在使用代码生成之前,让我们验证 Android Studio 是否被正确配置为忽略成员名前缀ms。单击文件➤设置➤代码样式➤ Java ➤代码生成打开设置对话框,其中代码生成选项卡处于选中状态。如果字段和静态字段文本框没有分别包含 m 和 s,现在在那里输入,点击应用,然后点击确定,如图 3-8 所示。

A978-1-4302-6602-0_3_Fig8_HTML.jpg

图 3-8。

Adding m and s to Field and Static Field in the Code Generation tab

构造器

将光标放在Sandbox.java的类范围内。要在 Android Studio 中生成构造函数,按 Alt+Insert | Cmd+N,选择构造函数。如图 3-9 所示,选择由构造函数初始化的字段对话框允许你选择类成员作为参数。我们需要一个无参数的构造函数,所以单击“不选”按钮。在 Java 中,重载构造函数以接受不同类型和数量的参数是很常见的。例如,您可以再次调用这个对话框,生成一个构造函数,它将一个List<String>作为参数,并将这个参数赋给我们的成员mGreetings:List<String>

A978-1-4302-6602-0_3_Fig9_HTML.jpg

图 3-9。

Choose Fields to Initialize by Constructor dialog box

getter/setter

Java 类通常是封装的,这意味着类成员通常被声明为私有的,这些成员的公共接口是通过公共访问器(getter)和公共赋值器(setter)方法提供的。单击将光标放在Sandbox.java的类范围内,然后按 Alt+Insert | Cmd+N。您会注意到,有一个 Getter 选项,一个 Setter 选项,以及一个 Getter 和 Setter 选项。Getter 和 setter 方法通常是成对出现的,所以除非你有充分的理由省略其中的一个,否则最好同时生成两个方法。从列表中选择 Getter 和 Setter,如图 3-10 所示。在随后的“选择字段以生成 Getters 和 Setters”对话框中,从列表中选择mGreetings:List<String>并单击“确定”。你的类现在有了一个mGreetings的 getter 和 setter,如图 3-11 所示。注意,在生成方法名时,生成的代码忽略了前缀m,因为您在之前的设置中声明了前缀ms

A978-1-4302-6602-0_3_Fig11_HTML.jpg

图 3-11。

Generated getter and setter methods

A978-1-4302-6602-0_3_Fig10_HTML.jpg

图 3-10。

Generating the getter and setter

覆盖方法

代码生成理解类的层次结构,因此您可以覆盖任何超类或实现的接口中包含的方法。Sandbox.java是一个简单明了的老式 Java 对象(POJO)。现在修改Sandbox类,使其扩展RectShape。当您键入 extends RectShape 时,单词RectShape可能会以红色突出显示。如果是这种情况,按 Alt+Enter 导入RectShape类,如图 3-12 所示。

A978-1-4302-6602-0_3_Fig12_HTML.jpg

图 3-12。

Extending the superclass

如果您通过按 Ctrl+H 调用层次视图,您将看到以RectShapeShapeObject为其祖先的Sandbox的类层次,正如您通过检查图 3-13 所看到的。现在按 Alt+Insert | Cmd+N 并选择覆盖方法。让我们从Shape开始覆盖hasAlpha()方法,如图 3-14 所示。从版本 Java 5 开始的惯例是用@Override注释被覆盖的方法,所以让我们保持 Insert @Override 复选框处于选中状态。@Override注释告诉编译器验证方法的名称和签名,以确保该方法确实被覆盖。修改hasAlpha()的返回语句,使其总是返回true

A978-1-4302-6602-0_3_Fig14_HTML.jpg

图 3-14。

Modifying the hasAlpha( ) method

A978-1-4302-6602-0_3_Fig13_HTML.jpg

图 3-13。

Selecting methods to override/Implement with RectShape

toString()方法

Android Studio 可以为你生成toString()方法。让我们为Sandbox创建一个toString()方法,并包含mGreetings成员。按 Alt+Insert | Cmd+N,选择toString()。选择您唯一的成员mGreetings,然后单击确定。Android Studio 生成一个返回字符串如"Sandbox{" + "mGreetings=" + mGreetings + '}',如图 3-15 。如果在我们的类中有多个成员并选择了它们,它们也会被附加到这个方法的返回字符串中。当然,toString()生成的代码并不是一成不变的;您可以随意更改这个方法,只要它返回一个String

A978-1-4302-6602-0_3_Fig15_HTML.jpg

图 3-15。

Generate the toString( ) method

委托方法

Android Studio 知道您的类成员,因此允许您将行为从类中定义的代理方法委托给类成员的方法。这听起来很复杂,但很简单。为了向您展示委托方法选项是如何工作的,让我们直接进入代码。

Sandbox.java中,将光标放在类范围内。按 Alt+Insert | Cmd+N,然后选择委托方法。选择mGreetings:List<String>并按下 OK。List 接口有许多方法,您可以将行为委托给这些方法。为简单起见,选择add(object:E):boolean,如图 3-16 所示。如果您想要委托多个方法,请在选择这些方法时按住 Ctrl 键(Mac 上为 Cmd 键)。单击确定。

A978-1-4302-6602-0_3_Fig16_HTML.jpg

图 3-16。

Selecting methods to generate delegates

生成的add() methodin Sandbox.java现在是一个代理,将行为委托给mGreetings成员的add()方法,如图 3-17 所示。注意,add()方法的参数被定义为一个String,以匹配mGreetings的通用定义List<String>。委托方法不是被覆盖的方法,所以你可以把你的代理方法重命名为任何你想要的名字,但是名字add()很有意义,所以继续保持这个名字。

A978-1-4302-6602-0_3_Fig17_HTML.jpg

图 3-17。

Generated add( ) method

插入实时模板

Android Studio 附带了许多模板,允许您将预定义的代码直接插入到源文件中。在许多 ide 中,生成的代码只是从一个模板粘贴过来的,不考虑作用域;然而,Android Studio 的模板是范围感知的,也可以集成可变数据。

在您开始使用 Android Studio 中的实时模板之前,让我们探索现有的实时模板并创建一个我们自己的模板。导航到文件➤设置➤实时模板。选择普通模板组。现在单击右上角的绿色加号按钮,并选择 Live Template。填充缩写、描述和模板文本字段,如图 3-18 所示。在应用此模板之前,您必须单击 Define 按钮,该按钮看起来像窗口底部的蓝色超文本链接。现在选择 Java 并选择所有的作用域(语句、表达式、声明等等)。单击应用。

A978-1-4302-6602-0_3_Fig18_HTML.jpg

图 3-18。

Create a live template called cb (comment block)

您刚刚创建了一个名为cb的定制 live 模板,它在任何 Java 源文件和任何范围内编码时都是可用的。图 3-18 中的红色字$SELECTION$为变量。您将很快看到这个变量的作用。现场模板选项在表 3-4 中描述。

表 3-4。

Live Template Options

| [计]选项 | 电脑钥匙 | Mac 密钥 | 描述 | | --- | --- | --- | --- | | 插入实时模板 | Ctrl+J | Cmd+J | 调用范围敏感的实时模板列表。将在您的文档中插入模板代码。 | | 用实时模板包围 | Ctrl+Alt+J | Cmd+Alt+J | 使用实时模板列表调用范围敏感的包围。将用一个范围敏感的实时模板包围所选内容。 |

在离开实时模板的设置页面之前,快速浏览一下位于普通模板组中的一个现有实时模板,其缩写为 psfs。单击 psfs 检查其内容。您会注意到这个模板生成了一个带有public static final StringString常量,并且它只在 Java 和 Groovy 声明范围内可用。单击“确定”返回编辑器。

Sandbox.java的声明部分,在mGreetings的定义下,键入 psfs,然后通过按 Ctrl+J | Cmd+J 调用实时模板,然后按 Enter。通过给该常量一个名称和赋值来完成该语句,如下所示:public static final String HELLO = " HELLO Sandbox ";。

Note

在 Java 中,常量的命名约定是全部大写。

在构造函数上方,键入单词 CONSTRUCTORS。现在将这个单词转换成一个注释块,以引起其他程序员的注意。选择整个单词,“构造函数”,然后按 Ctrl+Alt+J | Cmd+Alt+J 调用“用实时模板包围”。在 Live Templates 列表中选择 cb,按回车键,如图 3-19 所示。您刚刚应用了之前创建的实时模板。

A978-1-4302-6602-0_3_Fig19_HTML.jpg

图 3-19。

Apply the live template called cb (comment block)

移动您的代码

Android Studio 了解代码块是如何分隔的,因此移动代码行或代码块既简单又直观。Move 语句和 Move Line 的区别在于,Move 语句同时考虑边界和范围,而 Move Line 则两者都不考虑。如果选择用 move 语句移动代码语句,该语句将保留在其封闭块范围的边界内。如果你用 Move Line 移动同一个语句,Android Studio 会把这个语句当作一个简单的文本行,并把它移动到你想要它去的任何地方。

您也可以移动整个代码块。使用 Move 语句,您只需将光标放在要移动的块的起始行(带大括号的行)上的任意位置,然后按 Ctrl+Shift+Down | Cmd+Shift+Down 或 Ctrl+Shift+Up | Cmd+Shift+Up。整个区块将一起移动,同时尊重其他区块的边界,并保持在其封闭范围的边界内。“移动行”不理解范围或边界,但在应用“上移行”或“下移行”操作之前,您仍然可以通过先选择它们来移动多行,这两种操作在 PC 和 Mac 上分别是 Alt+Shift+上移和 Alt+Shift+下移。

要理解 Android Studio 中的 move 操作,最好直接去做。让我们从在我们的add()方法中创建一个语句开始。在显示return mGreetings.add(object);的那一行之后,按 Enter 键开始新的一行,并键入 soutm。然后按 Ctrl+J | Cmd+J 调用 Live Template,产生System.out.println("Sandbox.add");。您可能已经注意到,您的新代码行将不会到达,因为 return 语句在它上面,如图 3-20 所示。让我们用 move 语句上移这个语句。按住 Ctrl|Cmd 和 Shift 的同时,多次按向上箭头键。Android Studio 重新定位该语句,但不会让您意外地将该语句移动到可能没有任何意义的范围内。使用移动线(Alt+Shift+Up)再次尝试此操作,并再次观察其行为。

A978-1-4302-6602-0_3_Fig20_HTML.jpg

图 3-20。

Move Statement and Move Line

让我们尝试另一个例子,通过将你的构造函数移到类的底部来展示 Move 语句的强大功能。确保在Sandbox()声明和它上面的注释块之间没有空行。现在,将光标放在Sandbox()的声明行上的任意位置,通过按住 Ctrl|Cmd 和 Shift 键并重复按下向下箭头键来调用 Move Statement Down,直到您的构造函数是类中的最后一个方法。请注意,包括注释在内的整个块都跳到了类的底部,同时避开了其他方法。表 3-5 描述了移动代码操作及其键盘快捷键。

表 3-5。

Move Code Options

| [计]选项 | 电脑钥匙 | Mac 密钥 | 描述 | | --- | --- | --- | --- | | 下移语句 | ctrl+Shift+向下 | Cmd+Shift+Down | 在范围边界内向下移动一条或多条语句。如果移动一个块,整个块将一起跳到下一个语法正确的位置。 | | 将语句上移 | ctrl+Shift+向上 | cmd+Shift+向上 | 与下移语句相同,但上移。 | | 下移一行 | Alt+Shift+Down | Alt+Shift+Down | 向下移动语句或行。不遵守范围边界或语法。 | | 向上移动一行 | Alt+Shift+Up | Alt+Shift+Up | 与下移一行相同,但上移。 |

设计您的代码

代码风格约定不断发展。对于在方法后面应该放置多少空格,或者左大括号应该出现在方法签名的同一行还是紧接在方法签名的下面,没有严格的规则。组织倾向于定义他们自己的代码风格,但是代码风格也因程序员而异;您也可能有一种自己喜欢的代码风格。幸运的是,Android Studio 使得设计和组织代码变得简单。在开始设计代码样式之前,让我们检查一下代码样式的设置。选择文件➤设置➤码样式,弹出设置对话框,如图 3-21 所示。Java 和 XML 是我们对 Android 最感兴趣的语言。在左窗格中切换打开代码样式,选择 Java,并检查设置窗口中的每个选项卡。

A978-1-4302-6602-0_3_Fig21_HTML.jpg

图 3-21。

Settings dialog box with Code Style ➤ Java selected and showing the Wrapping and Braces tab

通过选择/取消选择各个选项卡中间窗格中的复选框来试验这些设置,并注意右窗格中的示例类如何相应地改变以适应您的风格。单击顶部的“管理”按钮,定义一个新方案。现在点击另存为,给你的方案起个名字,比如 android,然后点击确定。如果您对已保存的方案做了进一步的更改,请单击“应用”按钮来应用这些更改。当您使用 Ctrl+Alt+L | Cmd+Alt+L 设置代码格式时,将应用您在代码样式选项卡中选择的设置。表 3-6 中描述了代码组织选项。

表 3-6。

Code-Organizing Options

| [计]选项 | 电脑钥匙 | Mac 密钥 | 描述 | | --- | --- | --- | --- | | 自动缩进行 | Ctrl+Alt+I | Ctrl+Alt+I | 根据方案设置对当前选定的一行或多行应用缩进。 | | 优化导入 | Ctrl+Alt+O | Ctrl+Alt+O | 从 import 语句中删除任何未使用的导入。Android Studio 非常注意保持导入的干净和相关性,这个命令实际上是多余的。 | | 重新排列代码 | 没有人 |   | 根据排列设置中建立的规则重新排列代码元素的顺序。 | | 重新格式化代码 | Ctrl+Alt+L | Cmd+Alt+L | 为特定方案应用代码样式设置。 |

自动缩进行

自动缩进行对于在编码时保持行的正确缩进很有用。Java 中控制制表符和缩进的规则可以通过文件➤设置➤代码样式➤ Java ➤制表符和缩进来访问。自动缩进行应用于当前行,或者如果您选择了多行,则应用于所有选定的行。

Sandbox.java中,选择整个方法代码块并按 Tab 键。该块应该向右移动一个制表符的距离。现在将光标放在该块的第一行,并在 PC 和 Mac 上按 Ctrl+Alt+I。您会注意到,自动缩进会将该行重新定位到适当的缩进位置,尽管方法块的其余部分不受影响。现在,通过按 Ctrl+A | Cmd+A 选择该类中的所有代码,并再次按 Ctrl+Alt+I。这一次,将对整个文件应用适当的缩进。

重新排列代码

排列决定了代码中元素的顺序。例如,大多数人喜欢将类成员声明放在类的顶部,然后是构造函数,最后是 getters 和 setters,等等。您可以通过文件➤设置➤代码风格➤ Java ➤排列访问排列选项卡,编辑排列设置。

在上一节中,您将构造函数移到了类的底部。这通常不属于它的位置。从主菜单中选择代码➤重排代码。你会注意到你的构造函数已经回到了它的预期位置,在声明部分的下面。重排代码根据排列设置中的规则执行此重排操作。

重新格式化代码

重新格式化代码是最强大的代码样式操作,因为它为您提供了应用代码样式设置中定义的所有代码样式选项的选项。正如你已经看到的,代码风格设置可以从主菜单通过文件➤设置➤代码风格。此外,重新格式化代码允许您重新格式化当前选定的文件,或相同类型和目录的每个文件。此外,重新格式化代码允许你链接重排条目(这将在 Java 文件上应用重排代码),并优化导入到命令中,如图 3-22 所示。尝试通过按 Ctrl+Alt+L | Cmd+Alt+L 来重新格式化Sandbox.java

A978-1-4302-6602-0_3_Fig22_HTML.jpg

图 3-22。

Reformat Code dialog box with Rearrange Entries selected

围绕着

Surround With(Ctrl+Alt+T | Cmd+Alt+T)是 Live Template(Ctrl+Alt+J | Cmd+Alt+J)中功能的超集。但是,Surround With 还包括用 Java 块包围所选语句的选项,例如if/elsefortry/catch等等。虽然你的沙箱类中的简单代码不会威胁抛出任何检查过的异常,但是用try/catch块包围威胁抛出异常的语句是最好的环绕应用之一;这可能就是为什么快捷键 Ctrl+Alt+T | Cmd+Alt+T 中包含一个 T 的原因。环绕操作如表 3-7 所示。

表 3-7。

Surround With Options

| [计]选项 | 电脑钥匙 | Mac 密钥 | 描述 | | --- | --- | --- | --- | | 用...包围 | Ctrl+Alt+T | Cmd+Alt+T | 用 Java 代码块将所选语句括起来,如 if/else、for、try/catch 等。 | | 展开/移除 | Ctrl+Shift+Delete | Cmd+Shift+Delete | 从选定的一条或多条语句中解开代码块。 |

Sandbox.javaadd()方法中,您希望确保没有重复。如图 3-23 所示,用if/else块将返回的mGreetings.add(object);围住。选择整行并按 Ctrl+Alt+T | Cmd+Alt+T 激活环绕。现在从菜单中选择if/else。在if语句的括号中,键入!mGreetings.contains(object)并且在 else 块类型中返回 false。

A978-1-4302-6602-0_3_Fig23_HTML.jpg

图 3-23。

Wrapping and unwrapping blocks of code with Surround With

假设您的业务规则已经改变,您不再关心mGreetings中的重复条目。使用展开/移除来移除您刚刚创建的if/else块。将光标放在return mGreetings.add(object);语句的任意位置,按 Ctrl+Shift+Delete | Cmd+Shift+Delete,选择unwrap if。该方法现在看起来应该和您修改它之前一样。

Surround With 的另一个伟大应用是迭代集合。在上一节中,您自动生成了一个toString()方法。现在修改这个方法,以便迭代mGreetings集合。从toString()方法中删除 return 语句,这样toString()方法的主体就是空的。现在键入 mGreetings,然后按 Ctrl+Alt+T | Cmd+Alt+T。从列表中选择Iterate Iterable,或者按 I 键。再次按回车键接受greeting作为单个元素的名称。产生的代码是一个for-each循环。注意,Android Studio 理解mGreetings包含Strings,并且它还生成了一个名为greeting的局部变量,其单数形式为mGreetings减去m。根据图 3-24 进一步修改add()方法。

A978-1-4302-6602-0_3_Fig24_HTML.jpg

图 3-24。

Using Surround With to iterate an iterable

摘要

本章介绍了 Android Studio 最重要的代码生成特性。我们建议您返回文件➤设置➤代码样式➤ Java 和文件➤设置➤代码样式➤,并花几分钟时间探索那里的各种设置。Android Studio 为编码提供了很多键盘快捷键,但你不必全部记住。如果你不知所措,你可以使用这本书作为参考,或导航到代码菜单,并探索其菜单项和子菜单作为参考。

四、重构代码

你在 Android Studio 中开发的解决方案从设计到完成不会总是遵循一条直线。要成为一名高效的 Android 程序员,你需要在开发、调试和测试的过程中保持灵活性并重构代码。在前一章中,你学习了 Android Studio 如何生成代码;在这一章中,你将看到 Android Studio 如何重构你的代码。重构代码的最大风险是你可能会引入意想不到的错误。Android Studio 通过分析某些有风险的重构操作的后果来降低这些风险,然后激活查找工具窗口,在提交之前,您可以在其中预览您的更改(标记有任何错误或冲突)。

本章介绍的许多重构操作也可以在没有 Android Studio 重构工具的情况下执行。然而,您应该避免强力重构(例如,借助全局查找替换选项),因为 Android Studio 并不总是能够避免您在这些情况下引入错误。相比之下,如果 Android Studio 检测到你正在尝试重构操作,它会试图阻止你犯任何愚蠢的错误。例如,在“项目工具”窗口中,将 Java 源文件从一个包拖到另一个包将强制执行重构➤移动操作,该操作分析移动操作的结果,允许您预览更改,然后将整个项目中该类的任何导入语句优雅地更改为新的完全限定包名。

大多数重构操作都局限于一个方法或一个类,因此不太可能在项目中引入错误。有风险的重构操作是那些涉及两个或更多素材的操作。如果重构操作引入了编译错误,检查管理器将在编辑器中用红色标签标记受影响的素材。此时,您可以尝试修复它们,或者通过按 Ctrl+Z | Cmd+Z 来简单地撤消整个重构操作。如果重构操作成功且没有编译错误,但仍然涉及大量素材,您仍然应该运行您的测试来验证您没有引入任何运行时错误。第十一章涵盖了测试。

Tip

您应该将任何重要的重构更改作为单个 Git 提交,以便您可以在以后轻松地恢复该提交。第七章涵盖了 Git。

这一章主要关注最有用的重构操作。在我们开始讨论单独的重构操作之前,我们想指出 Android Studio 有一个非常方便的重构操作,叫做重构➤重构这个。选择这个选项会显示一个上下文菜单,如图 4-1 所示,它聚集了最有用的重构操作。这个操作的键盘快捷键是 Ctrl+Alt+Shift+T | Ctrl+T,在 PC 上你可以通过它方便记忆的首字母缩略词:CAST 来记住它。

A978-1-4302-6602-0_4_Fig1_HTML.jpg

图 4-1。

The Refactor This menu with the most useful refactoring operations

在开始学习本章中的例子之前,修改第三章中的Sandbox.java文件,使其不扩展任何内容,既不包含方法也不包含成员,如下所示:

public class Sandbox {

}

重新命名

从项目工具窗口选择Sandbox,然后导航到重构➤重命名或按 Shift+F6。产生的对话框允许您重命名您的类,并重命名注释、测试用例以及继承的类中出现的该名称。将Sandbox重命名为 Playpen,点击重构按钮,如图 4-2 所示。您应该会在项目中看到重命名操作的结果。现在通过按 Ctrl+Z | Cmd+Z 撤消重命名操作。

A978-1-4302-6602-0_4_Fig2_HTML.jpg

图 4-2。

Rename Sandbox to Playpen

更改签名

更改签名操作使您能够更改方法的以下属性:可见性、名称、返回类型、参数和引发的异常。在Sandbox.java中创建一个方法,如下面的代码片段所示:

public String``greetings

return "Hello " + message;

}

将光标放在单词greetings(以粗体突出显示)上的任意位置,然后按 Ctrl+F6 | Cmd+F6,或者导航到重构➤更改签名。出现的对话框允许你修改方法的签名,如图 4-3 所示。

A978-1-4302-6602-0_4_Fig3_HTML.jpg

图 4-3。

The Change Signature dialog box

在参数选项卡中,点击String信息项。将String参数的名称从message改为问候语,如图 4-3 所示。绿色加号和红色减号图标分别允许您向方法中添加参数或从中减去参数;您可以在列表中编辑它们的类型和名称。除了修改当前方法之外,您还可以通过重载方法单选按钮来选择委托。选择这个单选按钮将使您的原始方法不受影响,但是会用您定义的新签名生成另一个方法。在 Java 中,如果一组方法具有相同的名称,但是参数顺序和/或参数类型不同,则可以认为它们是重载的。但是,我们所做的更改并没有使这个方法适合重载。如果您愿意,可以在提交更改前通过单击预览按钮来预览更改。若要完成操作并关闭对话框,请单击“重构”按钮。

类型迁移

顾名思义,类型迁移允许您从一种 Java 类型迁移到另一种类型。让我们假设您创建了一个Person类。在进一步的开发中,您发现Person过于一般化,因此您创建了一个扩展PersonManager类。如果您想将Person的所有实例迁移到Manager,您可以通过类型迁移轻松完成。

将光标放在greetings方法的String声明上(在下面的代码片段中以粗体突出显示),然后按 Ctrl+Shift+F6 | Cmd+Shift+F6,或者选择“重构➤类型迁移”。出现的对话框如图 4-4 所示。

A978-1-4302-6602-0_4_Fig4_HTML.jpg

图 4-4。

Type migration from string to date

public String greetings(``String

return "Hello " + greet;

}

java.lang.String改为 java.util.Date,如图 4-4 所示。从选择范围下拉列表中选择打开文件。与大多数重构操作一样,您可以通过单击预览按钮来预览您的更改。单击重构按钮。

移动

您可以通过以下三种方式之一移动源文件:

  • 通过在项目工具窗口中将源文件从一个包拖到另一个包
  • 通过选择源文件并从主菜单导航到重构➤移动
  • 通过在项目工具窗口中选择文件并按下 F6

右键单击(在 Mac 上按住 Ctrl 键单击)这个com.apress.gerber.helloworld包,然后选择“新建➤包”。将包命名为 refactor。从项目工具窗口中,将Sandbox类拖放到refactor包中,当出现如图 4-5 所示的对话框时,点击 OK。您在“项目工具”窗口中执行的任何拖放操作都会自动强制执行重构➤移动操作,这允许您将类从一个包安全地移动到另一个包。

A978-1-4302-6602-0_4_Fig5_HTML.jpg

图 4-5。

The Move dialog box, resulting from drag-and-drop

除了移动类,您还可以移动成员。在您的Sandbox类中,定义一个新成员,如下所示:

public static final String HELLO = "Hello Android Studio";

将光标放在这行代码上,然后按 F6。出现的对话框允许你将成员从一个类移动到另一个类,如图 4-6 所示。单击“取消”按钮取消此操作。

A978-1-4302-6602-0_4_Fig6_HTML.jpg

图 4-6。

The Move Members dialog box

复制

“复制”与“移动”类似,可通过按键盘快捷键 F5 或从主菜单中选择“重构➤复制”来访问。在项目工具窗口中,选择refactor包中的Sandbox类,然后按 F5。在目的包下拉菜单中选择com.apress.gerber.helloworld包,点击确定,如图 4-7 所示。像我们在这里所做的那样不加选择地复制 Java 源文件并不是一个好主意,因为解析是不明确的,因此容易产生潜在的错误。

A978-1-4302-6602-0_4_Fig7_HTML.jpg

图 4-7。

The Copy Class dialog box

安全删除

让我们删除我们创建的复制类。您可以在 Android Studio 中删除文件和资源,方法是在项目工具窗口中选择它们,然后按下 delete 键。点击refactor包中的Sandbox文件,按 Delete 键。出现的对话框允许您通过选中安全删除复选框来使用安全删除选项。使用安全删除的好处是,我们可以在执行删除之前搜索素材上任何可能被破坏的依赖关系,如图 4-8 所示。如果在您的项目中发现此素材的任何依赖项,您将可以选择查看它们,或者通过单击“仍然删除”来强制执行删除操作。

A978-1-4302-6602-0_4_Fig8_HTML.jpg

图 4-8。

The Safe Delete dialog box

提取

提取不只是一个操作,而是几个操作。本节涵盖了一些更重要的提取操作:提取变量、提取常数、提取字段、提取参数和提取方法。在Sandbox类中,让我们删除所有成员和方法,从头开始:

public class Sandbox {

}

提取变量

在您的Sandbox.java类中,定义一个方法,如下所示:

private String saySomething(){

return "``Something

}

将光标放在硬编码的Something值(以粗体突出显示)上的任意位置,并选择重构➤提取➤变量,或者按 Ctrl+Alt+V | Cmd+Alt+V,然后在不选中声明最终值复选框的情况下按 Enter。Android Studio 提取一个局部变量,按照硬编码的String命名。您应该会得到这样的结果:

private String saySomething(){

String something = "Something";

return something;

}

提取常数

当你在 Android 中开发应用时,你会发现自己使用了大量的Strings作为键——例如,在MapsBundles中。所以提取常量会节省你很多时间。

定义一个方法,如下面的代码片段所示。将光标放在name_key字符串上的任意位置,然后按 Ctrl+Alt+C | Cmd+Alt+C。出现的对话框应该如图 4-9 所示。在这里,Android Studio 为名字提供了几个建议。按照惯例,Java 中的常量都是大写的。选择NAME_KEY并按下回车键。

A978-1-4302-6602-0_4_Fig9_HTML.jpg

图 4-9。

Extract Constant NAME_KEY Note

您将需要import android.os.Bundle来创建没有编译时错误的 proceding 方法。

private void addName(String name, Bundle bundle ){

bundle.putString("``name_key

}

您最终应该得到一个名为NAME_KEY的常数,它应该这样定义:

public static final String NAME_KEY = "name_key";

提取字段

提取字段将局部变量转换为类的字段(也称为成员)。

Note

为了创建没有编译时错误的 proceding 方法,您需要导入java.util.Date

在您的Sandbox类中定义一个方法:

private Date getDate(){

return new``Date

}

将光标放在Date(粗体突出显示)的任意位置,按 Ctrl+Alt+F | Cmd+Alt+F。您将看到如图 4-10 所示的对话框。在 Android 中,命名约定是在字段(也称为成员)前加一个 m。您还会注意到一个下拉菜单,允许您在当前方法、字段声明或构造函数中初始化字段。选择字段声明并按回车键。

A978-1-4302-6602-0_4_Fig10_HTML.jpg

图 4-10。

The Extract Field dialog box

您应该会得到这样的结果:

private final Date mDate = new Date();

...

private Date getDate(){

return mDate;

}

删除final关键字,使声明行看起来像下面的代码片段:

private Date mDate = new Date();

提取参数

Extract Parameter 允许您提取一个变量,并将其作为封闭方法的参数。在您的Sandbox类中定义一个方法:

private void setDate(){

mDate = new``Date()

}

将光标放在Date()上的任意位置(以粗体突出显示),按 Ctrl+Alt+P | Cmd+Alt+P,然后按 Enter。结果方法应该类似于下面的代码片段:

private void setDate(Date date){

mDate = date;

}

提取方法

“提取方法”允许您选择一行或多行连续的代码,并将它们放在单独的方法中。你想这么做有两个原因。第一个原因是你有一个太复杂的方法。将一个算法分成大约 10-20 行的离散块,比一个有 100 行代码的方法更容易阅读,也更不容易出错。

重复代码块几乎从来都不是一个好主意,所以如果您发现重复的代码块,最好提取一个方法并调用该方法来代替重复的代码块。通过提取一个方法并在您之前使用重复代码块的地方调用它,您可以在一个地方维护您的方法,并且如果您需要修改它,您只需要修改它一次。在你的Sandbox类中重新创建以下两个方法,如清单 4-1 所示。随意复制粘贴。

Listing 4-1. Exract Method Code

private String methodHello (){

String greet = "Hello";

StringBuilder stringBuilder = new StringBuilder();

for(int nC = 0; nC < 10; nC++){

stringBuilder.append(greet + nC);

}

return  stringBuilder.toString();

}

private String methodGoodbye (){

String greet = "Goodbye";

StringBuilder stringBuilder = new StringBuilder();

for(int nC = 0; nC < 10; nC++){

stringBuilder.append(greet + nC);

}

return stringBuilder.toString();

}

正如我们已经提到的,任何时候你发现自己重复代码块或者复制粘贴代码块,你应该考虑使用 Extract 方法。选择清单 4-1 中所有粗体突出显示的行。现在按 Ctrl+Alt+M | Cmd+Alt+M 提取方法。您将看到一个显示方法签名的对话框。将该方法重命名为 getGreet,如图 4-11 所示,点击确定。

A978-1-4302-6602-0_4_Fig11_HTML.jpg

图 4-11。

The Extract Method dialog box

Android Studio 扫描您的文件,发现您有另一个完全相同的代码块实例。点击【是】接受流程复制对话框中的建议,如图 4-12 所示。

A978-1-4302-6602-0_4_Fig12_HTML.jpg

图 4-12。

The Process Duplicates dialog box

您应该得到类似清单 4-2 的结果。结果方法更容易维护,因为它保存在一个地方。

Listing 4-2. Code Resulting from Extract Method Operation

private String methodHello (){

String greet = "Hello";

return  getGreet(greet);

}

private String methodGoodbye (){

String greet = "Goodbye";

return  getGreet(greet);

}

private String getGreet (String greet){

StringBuilder stringBuilder = new StringBuilder();

for(int nC = 0; nC < 10; nC++){

stringBuilder.append(greet + nC);

}

return  stringBuilder.toString();

}

高级重构

本章剩余部分介绍的重构操作是高级的。如果你只是对快速使用 Android Studio 感兴趣,你已经有足够的知识来有效地使用重构操作,你可以跳过这一节。然而,如果你很好地理解了 Java,并且想要深入一些更高级的重构操作,请继续阅读。

通过删除Sandbox.java中的所有方法和成员,从头开始:

public class Sandbox {

}

在项目工具窗口中右键单击(在 Mac 上按住 Ctrl 键单击)该com.apress.gerber.helloworld包,然后选择“新建➤ Java 类”。把你的班级命名为迷你箱。更改Minibox的定义,使其继承自Sandbox,并拥有一个名为mShovel的成员,如下所示:

public class Minibox extends Sandbox {

private String mShovel;

}

向下推动构件,向上拉动构件

向下推成员和向上拉成员与继承一起使用。注意,我们已经在Minibox类中定义了mShovel成员。假设我们后来决定mShovel可能对其他扩展Sandbox的类有用。为此,打开Minibox类并选择 Refactor ➤向上拉成员。出现的对话框如图 4-13 所示。

A978-1-4302-6602-0_4_Fig13_HTML.jpg

图 4-13。

The Pull Members Up dialog box

由于SandboxMinibox的超类,默认情况下mShovel成员被选中,下拉成员组合框被设置为com.apress.gerber.helloworld.Sandbox类。单击重构。如果您现在检查SandboxMinibox,您会注意到mShovel成员属于Sandbox,不再出现在Minibox中。一般来说,如果您认为某个成员可能对其他扩展类有用,您应该将这些成员提升到层次结构中。要向下推动成员,可以遵循类似的步骤。

用委派替换继承

右键单击(在 Mac 上按住 Ctrl 键单击)这个com.apress.gerber.helloword包,然后选择“新建➤ Java 类”。将您的类命名为 Patio,并将其扩展为Sandbox:

public class Patio extends Sandbox {

}

经过进一步分析,我们决定Patio不是一个Sandbox,而是有一个Sandbox。要更改这种关系,导航到重构➤用委托替换继承。在弹出的对话框中,点击为委托组件生成 Getter 复选框,如图 4-14 所示。

A978-1-4302-6602-0_4_Fig14_HTML.jpg

图 4-14。

The Replace Inheritance with Delegation dialog box

您的Patio类现在应该有一个Sandbox成员,如下面的代码片段所示:

public class Patio {

private final Sandbox mSandbox = new Sandbox();

public Sandbox getSandbox() {

return mSandbox;

}

}

封装字段

封装是一种面向对象的策略,它通过将类成员的访问级别设为私有来隐藏类成员,然后通过公共 getter/setter 方法向这些成员提供公共接口。重构➤封装字段类似于代码➤生成➤ Getter 和 Setter,尽管当您选择重构➤封装字段时有更多的选择。打开您的Sandbox类并定义一个名为mChildren的新成员,在下一段代码中以粗体突出显示。从主菜单中,选择重构➤封装字段。

public class Sandbox {

private String mShovel;

private int mChildren;

}

出现的对话框允许您准确选择字段的封装方式以及它们应该具有的访问级别。一个真正封装的字段将具有公共访问器(getter)和赋值器(setter)方法的私有可见性。点击 Refactor 按钮,如图 4-15 所示,注意 Android Studio 已经在我们的Sandbox.java类中为我们生成了 getters 和 setters。

A978-1-4302-6602-0_4_Fig15_HTML.jpg

图 4-15。

The Encapsulate Fields dialog box

包装方法返回值

当您需要返回一个对象而不是一个原语时,包装一个返回值可能是有用的(尽管在其他情况下您可能希望包装一个返回值)。将光标放在getChildren()方法上,导航到重构➤包装方法返回值。选中使用现有类复选框,键入 java.lang.Integer 作为名称,键入 value 作为包装字段,如图 4-16 所示。现在单击 Refactor,注意您的getChildren()方法返回了一个Integer对象,而不是一个原语int

A978-1-4302-6602-0_4_Fig16_HTML.jpg

图 4-16。

The Wrap Return Value dialog box

用工厂方法替换构造函数

将光标放在Sandbox类定义的括号内。按 Alt+Insert | Cmd+N 并选择构造函数以生成新的构造函数。选择两个成员,如图 4-17 所示,点击确定。

A978-1-4302-6602-0_4_Fig17_HTML.jpg

图 4-17。

The Choose Fields to Initialize by Constructor dialog box

将光标放在新定义的构造函数中的任意位置,如下面的代码片段所示,然后导航到重构➤用工厂方法替换构造函数。出现的对话框如图 4-18 所示。单击“重构”以生成工厂方法。

A978-1-4302-6602-0_4_Fig18_HTML.jpg

图 4-18。

The Replace Constructor with Factory Method dialog box

public Sandbox(String shovel, int children) {

mShovel = shovel;

mChildren = children;

}

注意,构造函数现在是私有的,一个新的静态方法返回了一个Sandbox类的实例,如下面的代码片段所示。如果您正在创建单例,此操作特别有用。

public static Sandbox createSandbox(String shovel, int children) {

return new Sandbox(shovel, children);

}

将匿名转换为内部

Sandbox类的构造函数中,添加下面一行:

new Thread(new Runnable()).start();

将光标放在Runnable()上,按 Alt+Enter 调用代码完成操作。然后选择实现方法。选择run方法并点击确定。您的代码应该类似于下面的代码片段:

new Thread(new Runnable() {

@Override

public void run() {

//do something

}

}).start();

将光标放在Runnable()上,导航到重构➤将匿名转换为内部。Android Studio 给你建议MyRunnable作为类名,如图 4-19 。取消选中“使类成为静态”复选框,然后单击“确定”。注意,现在在Sandbox.java中有了一个名为MyRunnable的私有内部类,它实现了Runnable接口。这个例子没做多少;但是,在委派Views的行为时,您可能有机会使用此操作。

A978-1-4302-6602-0_4_Fig19_HTML.jpg

图 4-19。

The Convert Anonymous to Inner dialog box

摘要

本章讨论了 Android Studio 中许多可用的重构操作。重构代码是任何编程项目的必要组成部分,Android Studio 中的重构工具就是其中的佼佼者。Android Studio 通过分析结果来降低执行某些重构操作的风险,并允许您在提交操作之前在 Find tool 窗口中预览结果。最重要的重构操作可从“重构➤重构此对话框”中获得,该对话框通过使用键盘快捷键 Ctrl+Alt+Shift+T | Ctrl+T 来调用

五、提醒 Lab:第一部分

到目前为止,您已经熟悉了创建新项目、编程和重构的基础知识。是时候创建一个 Android 应用了,也就是所谓的 app。本章介绍四个实验项目中的第一个。这些实验旨在让您熟悉在开发应用的背景下使用 Android Studio。在这个项目中,您将开发一个应用来管理您想要记住的项目列表。核心功能将允许您创建和删除提醒,并将某些提醒标记为重要。提醒文本左侧的橙色标签会强调一个重要项目。该应用将包含一个动作栏菜单,上下文菜单,一个持久化的本地数据库,以及支持多重选择的设备上的多重选择。

图 5-1 展示了在仿真器上运行的完整应用。这个例子向您介绍了 Android 的基础知识,您还将学习如何通过使用内置的 SQLite 数据库来持久化数据。如果有些题目不熟悉也不用担心;后面的章节将更详细地介绍这些主题。

A978-1-4302-6602-0_5_Fig1_HTML.jpg

图 5-1。

The completed app interface Note

我们邀请您使用 Git 克隆这个项目,以便跟进,尽管您将从头开始使用它自己的 Git 库重新创建这个项目。如果你的电脑上没有安装 Git,参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端)并导航到C:\androidBook\reference\(如果您没有参考目录,请创建一个。在 Mac 上,导航到/your-labs-parent-dir/reference/)并发出以下 git 命令:git clone https://bitbucket.org/csgerber/reminders.git Reminders。

要操作提醒应用,您可以使用操作栏的溢出菜单。点击溢出按钮,它看起来像菜单栏右侧的三个垂直点,打开一个有两个选项的菜单,如图 5-2 所示:新提醒和退出。点击新建提醒打开如图 5-3 所示的对话框。在对话框中,您可以为新提醒添加文本,然后点击提交将其添加到列表中。轻按“退出”即可退出应用。

A978-1-4302-6602-0_5_Fig3_HTML.jpg

图 5-3。

New Reminder dialog box

A978-1-4302-6602-0_5_Fig2_HTML.jpg

图 5-2。

App interface with overflow menu activated

点击列表中的任一提醒会打开一个带有两个选项的上下文菜单,如图 5-4 所示:编辑提醒和删除提醒。点击上下文菜单中的编辑提醒打开编辑提醒弹出对话框,如图 5-5 所示,在此可以更改提醒的文本。点击上下文菜单中的删除提醒可从列表中删除提醒。

A978-1-4302-6602-0_5_Fig5_HTML.jpg

图 5-5。

Edit Reminder dialog box

A978-1-4302-6602-0_5_Fig4_HTML.jpg

图 5-4。

Context menu

开始一个新项目

按照第一章中的说明,使用新建项目向导在 Android Studio 中启动一个新项目。输入提醒作为应用名称,设置公司域为 gerber.apress.com ,选择空白活动模板。将该项目保存在路径C:\androidBook\Reminders下。为了与我们的示例保持一致,最好将您的所有 Lab 项目保存在一个公共文件夹中,例如C:\androidBook(或者对于 Mac/Linux 使用∼/androidBook)。在向导的下一页,选择 Phone and Tablet,将最低 SDK 设置为 API 8: Android 2.2 (Froyo)。通过将你的最小 API 级别设置为 8,你的应用可以被超过 99%的 Android 市场使用。单击 Next 按钮,从可用模板中选择空白活动,然后再次单击 Next。将活动名称设置为RemindersActivity,然后点击完成,如图 5-6 所示。

A978-1-4302-6602-0_5_Fig6_HTML.jpg

图 5-6。

Entering an activity name

Android Studio 在设计模式下显示activity_reminders.xmlactivity_reminders.xml文件是你的主活动的布局,如图 5-7 所示。正如在第一章中所讨论的,项目应该在模拟器或者设备上运行。请随意连接您的设备或启动您的仿真器,并运行该项目进行测试。

A978-1-4302-6602-0_5_Fig7_HTML.jpg

图 5-7。

Design mode for activity_reminders

初始化 Git 储存库

创建新项目后的第一步应该是使用版本控制来管理源代码。这本书的所有实验都使用 Git,这是一个流行的版本控制系统,可以与 Android Studio 无缝协作,并且可以在网上免费获得。第七章更彻底地探讨了 Git 和版本控制。

如果您的计算机上尚未安装 Git,请参考第七章中标题为安装 Git 的章节。从主菜单中选择 VCS ➤导入到版本控制➤创建 Git 存储库。(在 Apple OS 中,选择 VCS ➤ VCS Operations ➤创建 Git 存储库。)图 5-8 和 5-9 展示了这一流程。

A978-1-4302-6602-0_5_Fig9_HTML.jpg

图 5-9。

Selecting the root directory for the Git repository

A978-1-4302-6602-0_5_Fig8_HTML.jpg

图 5-8。

Creating the Git repository

当提示为 Git init 选择目录时,确保 Git 项目将在根项目目录中初始化(在本例中也称为Reminders)。单击确定。

您会注意到位于项目工具窗口中的大多数文件都变成了棕色,这意味着它们正在被 Git 跟踪,但是还没有被添加到 Git 存储库中,也没有被计划添加。一旦你的项目在 Git 的控制之下,Android Studio 就使用一种颜色方案来指示文件在创建、修改或删除时的状态。随着我们的进展,我们会更详细地解释这个着色方案,不过你可以在这里更详细地研究这个主题:jetbrains.com/idea/help/file-status-highlights.html.

单击位于底部空白处的“更改工具”按钮,切换打开“更改工具”窗口,并展开标记为“未版本化的文件”的叶。这将显示所有被跟踪的文件。要添加它们,请选择未版本化文件叶,然后按 Ctrl+Alt+A | Cmd+Alt+A,或者右键单击未版本化文件叶,然后选择“Git ➤添加”。棕色文件应该已经变成绿色,这意味着它们已经在 Git 中暂存,现在可以提交了。

按 Ctrl+K | Cmd+K 调用提交更改对话框。提交文件是将项目变更记录到 Git 版本控制系统的过程。如图 5-10 所示,作者下拉菜单用于覆盖当前默认提交者。您应该将 Author 字段留空,Android Studio 将简单地使用您在 Git 安装期间最初设置的默认值。取消选择“提交前”部分中的所有复选框选项。将以下消息放入提交消息字段:使用新建项目向导进行初始提交。单击提交按钮,并从下拉项目中再次选择提交。

A978-1-4302-6602-0_5_Fig10_HTML.jpg

图 5-10。

Committing changes to Git

默认情况下,项目工具窗口应处于打开状态。“项目工具”窗口以不同的方式组织您的项目,这取决于在窗口顶部的“模式”下拉菜单中选择的视图。默认情况下,下拉菜单设置为 Android view,它根据文件的用途来组织文件,与文件在计算机操作系统上的组织方式无关。在浏览项目工具窗口时,您会注意到 app 文件夹下有三个文件夹:manifestsjavares。在manifests文件夹中可以找到您的 Android 清单文件。在java文件夹中可以找到你的 Java 源文件。res文件夹保存了你所有的 Android 资源文件。位于res目录下的资源可能是 XML 文件、图像、声音和其他有助于定义应用外观和 UI 体验的资源。一旦你有机会探索 Android 视图,我们建议切换到更直观的项目视图,因为它直接映射到您计算机上的文件结构。

Note

如果您使用过其他 ide 或旧的 Android Studio 测试版本,您会注意到自从 Android Studio 发布以来,在项目工具窗口中引入了 Android 和包视图。

构建用户界面

默认情况下,Android Studio 在编辑器的新选项卡中打开与主活动相关联的 XML 布局文件,并将其模式设置为 Design,因此可视化设计器通常是您在新项目中看到的第一个东西。可视化设计器允许您编辑应用的可视化布局。屏幕中间是预览窗格。预览窗格显示 Android 设备的可视化表示,同时呈现您当前编辑的布局的结果。这种表示可以通过使用屏幕顶部的预览布局控件来控制。这些控件可以调整预览,并可用于选择不同(或多种)风格的 Android 设备,从智能手机到平板电脑或可穿戴设备。您还可以更改与布局描述相关联的主题。在屏幕的左侧,您会发现控制面板。它包含各种控件和小部件,可以拖放到舞台上,舞台是设备的可视化表示。ide 的右侧包含一个组件树,显示布局中描述的组件的层次结构。布局使用 XML。当您在可视设计器中进行更改时,这些更改会在 XML 中更新。您可以单击“设计”和“文本”选项卡,在可视和文本编辑模式之间切换。图 5-11 指出了视觉设计者的几个关键领域。

A978-1-4302-6602-0_5_Fig11_HTML.jpg

图 5-11。

The Visual Designer layout

使用可视化设计器

让我们从创建一个提醒列表开始。单击舞台上的 Hello World TextView控件,然后按 Delete 键删除它。在调色板中找到ListView控件,并将其拖到舞台上。拖动时,IDE 将显示各种度量和对齐参考线,以帮助您定位控件,当您拖动控件靠近边缘时,这些参考线往往会与边缘对齐。放下ListView,使其与屏幕顶部对齐。您可以将它放置在左上角或中上方。定位后,在编辑器的右下方找到 Properties 视图。将id属性设置为reminders_list_viewid属性是一个可以赋予控件的名称,允许您在 Java 代码中以编程方式引用它们;这就是我们以后修改 Java 源代码时引用ListView的方式。在属性窗口中更改layout:width属性,并将其设置为match_parent。这将扩展控件,使其占据与其所在的父控件一样多的空间。你将在第八章中了解更多关于设计布局的细节。现在,你的布局应该如图 5-12 所示。

A978-1-4302-6602-0_5_Fig12_HTML.jpg

图 5-12。

The activity_reminders layout with a ListView

在 Android 中,一个活动定义了控制用户与应用交互的逻辑。当第一次学习 Android 时,将一个活动想象成应用中的一个屏幕会有所帮助,尽管活动可能比这更复杂。这些活动通常会扩大一个布局,该布局定义了事物在屏幕上出现的位置。布局文件被定义为 XML,但可以使用可视化设计器进行可视化编辑,如前所述。

编辑布局的原始 XML

单击底部的文本选项卡,从可视编辑切换到文本编辑。这将显示布局的原始 XML 视图,以及右侧的实时预览。对 XML 所做的更改会立即反映在预览窗格中。通过在显示android:layout_height="match_parent"的行下面插入android:background="#181818",将相对布局的背景颜色更改为深灰色。颜色以十六进制值表示。关于十六进制颜色值的更多信息,参见第九章。请注意,现在有一个深灰色样本出现在您插入的设置根视图组背景颜色的行旁边的 gutter 中。如果您切换回设计模式,您会发现整个布局现在是深灰色的。

直接在 XML 布局文件中硬编码颜色值不是最好的方法。更好的选择是在 values resource 文件夹下定义一个colors.xml文件,并在那里定义您的颜色。我们将值外部化到 XML 文件(如 colors.xml)的原因是,这些资源保存在一个地方并可以在一个地方编辑,在整个项目中可以很容易地引用它们。

选择十六进制值#181818并使用 Ctrl+X | Cmd+X 或选择编辑➤剪切将其剪切到剪贴板。在其位置键入@color/dark_grey。这个值使用特殊的语法来引用名为dark_grey的 Android 颜色值。这个值应该在一个名为colors.xml的 Android 资源文件中定义,但是因为这个文件在你的项目中还不存在,Android Studio 用红色突出显示了这个错误。按 Alt+Enter,系统会提示您改正错误的选项。选择第二个选项,创建颜色值资源dark_grey,然后将该值粘贴到出现的下一个对话框的资源值:字段中,并单击确定。

新的颜色值资源对话框将创建 Android 资源文件colors.xml并用十六进制值填充它。单击“确定”,然后在“将文件添加到 Git”对话框中单击“确定”,将此新文件添加到版本控制中,并确保选中“记住,不要再询问”复选框,这样您就不会再受到此消息的困扰。图 5-13 展示了这一流程。

A978-1-4302-6602-0_5_Fig13a_HTML.jpg A978-1-4302-6602-0_5_Fig13b_HTML.jpg

图 5-13。

Extracting the hard-coded color value as a resource value

预览模式中的ListView包含的行布局与我们选择的背景颜色没有足够的对比度。要更改这些项目的显示方式,您需要在单独的布局文件中为该行定义一个布局。右键单击res文件夹下的layout文件夹,选择新建➤布局资源文件。在“新建资源文件”对话框中输入 reminders_row。使用LinearLayout作为根视图组,保留其余的默认设置,如图 5-14 所示。

A978-1-4302-6602-0_5_Fig14_HTML.jpg

图 5-14。

New Resource File dialog box

现在,您将为单个列表项行创建布局。LinearLayout根视图组是布局中最外面的元素。使用预览窗格顶部工具栏中的控件将其方向设置为vertical。使用此控件时要小心,因为水平线表示垂直方向,反之亦然。图 5-15 突出显示了改变方向按钮。

A978-1-4302-6602-0_5_Fig15_HTML.jpg

图 5-15。

Change Orientation button

在预览窗格的右下角找到属性视图。找到layout:height属性并将其设置为50dp。此属性控制控件的高度,dp 后缀是指与密度无关的像素度量。这是 Android 使用的一个指标,允许布局适当缩放,而不管呈现它们的屏幕密度如何。您可以单击此视图中的任何属性,并开始键入内容以增量方式搜索属性,然后按向上或向下箭头继续搜索。

将一个水平LinearLayout拖放到垂直LinearLayout内。将一个CustomView控件拖放到水平LinearLayout中,并将其 class 属性设置为android.view.View,以创建一个通用的空视图,并赋予它一个row_tabid属性。在撰写本文时,Android Studio 中有一个限制,不允许您从面板中拖动通用视图。一旦你点击CustomView,你会得到一个有不同选项的对话框,没有一个选项包含通用视图类。从对话框中选择任意一个类,并将其放置在布局中。使用 properties 窗格右侧的 properties 窗口找到刚才放置的视图的 class 属性,并将其更改为android.view.View以解决此限制。参考清单 5-1 来查看这是如何完成的。

您将使用此通用视图选项卡将某些提醒标记为重要。在编辑模式仍然设置为文本的情况下,将自定义视图的layout:width属性更改为10dp,将其layout:height属性设置为match_parent。在这里使用match_parent值将使这个View控件与其父容器一样高。切换到设计模式,在组件树的水平LinearLayout中拖放一个Large Text控件,并将其widthheight属性设置为match_parent。验证大文本组件是否位于自定义视图控件的右侧。在组件树中,标记为 textView 的组件应该嵌套在 LinearLayout (horizontal)组件中,并位于 View 组件之下。如果“文本视图”出现在“视图”上方,请用鼠标向下拖动它,使它吸附到第二个(也是最后一个)位置。给你的 TextView 控件一个idrow_text,并将其textSize属性设置为18sp。sp 后缀指的是与比例无关的像素测量,其表现类似于dp,但也尊重用户的文本大小设置,例如,如果用户视力不好,并希望其手机上的文本显示较大,sp会尊重此设置,而dp则不会。因此,对textSize使用sp总是一个好主意。你将在第八章中了解更多关于屏幕尺寸的信息。

最后,将 TextView 控件的text属性设置为Reminder Text。切换到文本模式,对 XML 进行额外的修改,使您的代码类似于清单 5-1 。

Listing 5-1. The reminders_row Layout XML Code

<?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="50dp"

android:orientation="vertical">

<LinearLayout

android:orientation="horizontal"

android:layout_width="match_parent"

android:layout_height="48dp">

<view

android:layout_width="10dp"

android:layout_height="match_parent"

class="android.view.View"

android:id="@+id/row_tab" />

<TextView

android:layout_width="match_parent"

android:layout_height="50dp"

android:textAppearance="?

android:attr/textAppearanceLarge"

android:text="Reminder Text"

android:id="@+id/row_text"

android:textSize="18sp" />

</LinearLayout>

</LinearLayout>

现在,您将创建一些自定义颜色。切换到设计模式。在组件树中选择根 LinearLayout (vertical)。将其 android:background 属性设置为@color/dark_grey以重用我们之前定义的颜色。在组件树中选择 row_tab 组件,并将其android:background属性设置为@color/green。选择 row_text 组件,并将其android:textColor属性设置为@color/white。与之前一样,这些颜色没有在colors.xml文件中定义,您需要使用与之前相同的过程来定义它们。切换到文本模式。重复按 F2 键在这两个附加错误之间来回跳转,然后按 Alt+Enter 键调出 IntelliSense 建议。在两种情况下选择第二个建议,并在弹出的对话框中填入值#ffffff以固定白色,填入值#003300以固定绿色。在使用建议对话框修复这些错误后,您可以按住 Ctrl 键并左键单击这些颜色中的任何一种,这将把您带到colors.xml文件,看起来应该如清单 5-2 所示。

Listing 5-2. The colors.xml File

<resources>

<color name="dark_grey">#181818</color>

<color name="white">#ffffff</color>

<color name="green">#003300</color>

</resources>

返回到activity_reminders.xml布局文件。现在你可以将新的reminders_row布局连接到这个布局中的ListView。切换到文本模式,给ListView元素添加如下属性:tools:listitem="@layout/reminders_row",如图 5-16 所示。

A978-1-4302-6602-0_5_Fig16_HTML.jpg

图 5-16。

The preview pane is now rendering a custom ListView layout

添加此属性不会更改布局运行时的呈现方式;它只是改变了预览窗格对列表视图中每个项目的使用。要使用新的布局,您必须使用 Java 代码来扩展它,我们将在后续步骤中向您展示如何操作。

添加视觉增强

您已经完成了 ListView 行的自定义布局,但是您不应该就此止步。添加一些视觉增强会让你的应用与众不同。看看文本是如何在屏幕上呈现的。细心的人会发现它稍微偏离了中心,碰到了左边的绿色标签。打开reminders_row布局进行一些小的调整。您希望文本向行的垂直中心移动,并提供一些填充,以便从侧边提供一些视觉分离。用清单 5-3 中的代码替换您的TextView元素。

Listing 5-3. TextView Additional Attributes

<TextView

android:layout_width="match_parent"

android:layout_height="50dp"

android:text="Reminder Text"

android:id="@+id/row_text"

android:textColor="@color/white"

android:textSize="18sp"

android:gravity="center_vertical"

android:padding="10dp"

android:ellipsize="end"

android:maxLines="1"

/>

附加的ellipsize属性将截断太长的文本,使其不适合末尾带有省略号的行,而maxLines属性将每行的行数限制为 1。最后,从清单 5-4 中添加两个通用视图对象,在内部LinearLayout之后,外部LinearLayout的结束标记之前,在该行下面创建一个水平标尺。外LinearLayout设置为高度50dp,内LinearLayout设置为高度48dp。两个通用视图对象将占据布局内剩余的垂直 2dp,从而创建一个斜边。这显示在清单 5-4 中。

Listing 5-4. Extra Generic Views for beveled edge

</LinearLayout>

<view

class="android.view.View"

android:layout_width="fill_parent"

android:layout_height="1dp"

android:background="#000"/>

<view

class="android.view.View"

android:layout_width="fill_parent"

android:layout_height="1dp"

android:background="#333"/>

</LinearLayout>

向 ListView 添加项目

现在,您将对使用您刚刚修改的布局的活动进行更改。打开项目工具窗口,在您的java源文件夹下找到RemindersActivity文件。它将位于com.apress.gerber.reminders包下。在这个文件中找到onCreate()方法。它应该是您的类中定义的第一个方法。声明一个名为 mListView 的 ListView 成员,并将onCreate()方法修改成清单 5-5 中的代码。您将需要解析导入 ListView 和 ArrayAdapter。

Listing 5-5. Add List Items to the ListView

public class RemindersActivity extends ActionBarActivity {

private ListView mListView;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_reminders);

mListView = (ListView) findViewById(R.id.reminders_list_view);

//The arrayAdatper is the controller in our

//model-view-controller relationship. (controller)

ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(

//context

this,

//layout (view)

R.layout.reminders_row,

//row (view)

R.id.row_text,

//data (model) with bogus data to test our listview

new String[]{"first record", "second record", "third record"});

mListView.setAdapter(arrayAdapter);

}

//Remainder of the class listing omitted for brevity

}

这段代码通过使用您之前定义的id属性来查找ListView,并删除默认的列表分隔线,以便我们之前创建的定制斜面分隔线能够正确呈现。该代码还创建了一个带有几个示例列表项的适配器。Adapter是一个特殊的 Java 类,被定义为 Android SDK 的一部分,在 SQLite 数据库(模型)、ListView(视图)和适配器(控制器)之间的模型-视图-控制器关系中充当控制器。适配器将模型绑定到视图,并处理更新和刷新。Adapter 是 ArrayAdapter 的超类,它将数组的元素绑定到视图。在我们的例子中,这个视图是一个 ListView。ArrayAdapter在其三参数构造函数中接受三个参数。第一个参数是由当前活动表示的一个Context对象。适配器还需要知道应该使用哪个布局和布局中的哪个或哪些字段来显示行数据。为了满足这个需求,您传递布局和布局中的TextView项的 id。最后一个参数是用于列表中每一项的字符串数组。如果此时运行项目,您将会看到列表视图中显示的ArrayAdapter构造函数中给出的值,如图 5-17 所示。

A978-1-4302-6602-0_5_Fig17_HTML.jpg

图 5-17。

ListView example

按 Ctrl+K | Cmd+K 提交对 Git 的更改,并使用带有自定义颜色的 Adds ListView 作为提交消息。当您完成一个项目时,最好对 Git 执行增量提交,同时使用提交消息来描述每次提交添加/删除/更改的特性。保持这种习惯使得识别单个提交和为将来的合作者和用户构建发布说明变得容易。

设置操作栏溢出菜单

Android 使用一种常见的视觉元素,称为动作栏。操作栏是许多应用定位导航和其他允许用户执行重要任务的选项的地方。此时运行应用时,您可能会注意到一个看起来像三个垂直点的菜单图标。这些点被称为溢出菜单。单击溢出菜单图标会产生一个菜单,其中有一个名为settings的菜单项。该菜单项作为新建项目向导模板的一部分放在那里,本质上是一个不执行任何操作的占位符。RemindersActivity加载menu_reminders.xml文件,该文件位于res/menu文件夹下。对这个文件进行修改,向活动添加新的菜单项,如清单 5-6 所示。

Listing 5-6. New Menu Items

<menu xmlns:android="``http://schemas.android.com/apk/res/android

xmlns:app="``http://schemas.android.com/apk/res-auto

xmlns:tools="``http://schemas.android.com/tools

tools:context="com.apress.gerber.reminders.app.RemindersActivity" >

<item android:id="@+id/action_new"

android:title="new Reminder"

android:orderInCategory="100"

app:showAsAction="never" />

<item android:id="@+id/action_exit"

android:title="exit"

android:orderInCategory="200"

app:showAsAction="never" />

</menu>

在前面的代码清单中,title属性对应于菜单项中显示的文本。由于我们已经对这些属性进行了硬编码,Android Studio 会将这些值标记为警告。按 F2 在这些警告之间跳转,按 Alt+Enter 调出 IntelliSense 建议。您只需按 Enter 键接受第一个建议,为新的字符串资源键入一个名称,当对话框弹出时,再次按 Enter 键接受命名的资源。使用new_reminder作为第一个项目的名称,使用exit作为第二个项目的名称。

打开RemindersActivity并用清单 5-7 中的文本替换onOptionsItemSelected()方法。您需要解决日志类的导入问题。当你点击应用中的一个菜单项时,运行时会调用这个方法,传入一个被点击的MenuItem的引用。switch语句接受MenuItemitemId,并根据点击的项目执行日志语句或终止活动。这个例子使用了将文本写入 Android 调试日志的Log.d()方法。如果您的应用包含多个活动,并且这些活动是在当前活动之前查看的,那么调用finish()将简单地从 backstack 中弹出当前活动,控制权将传递给下一个底层活动。因为 RemindersActivity 是这个应用中唯一的活动,finish()方法从 backstack 中弹出唯一的活动,并导致您的应用终止。

Listing 5-7. onOptionsItemSelected( ) Method Definition

@Override

public boolean onOptionsItemSelected(MenuItem item) {

switch (item.getItemId()) {

case R.id.action_new:

//create new Reminder

Log.d(getLocalClassName(),"create new Reminder");

return true;

case R.id.action_exit:

finish();

return true;

default:

return false;

}

}

运行应用并测试新的菜单选项。轻按新的提醒菜单选项,并观看 Android 日志以查看消息出现。在仿真器或设备上运行应用时,将会打开 Android DDMS(Dalvik 调试监控服务)窗口,您需要选择日志级别下的调试选项来查看调试日志。运行您的应用并与菜单项交互。点击新的提醒菜单项时,请注意 Android DDMS 窗口中的日志。最后,按 Ctrl+K | Cmd+K,使用添加新的提醒和退出菜单选项作为提交消息,将代码提交给 Git。

持续提醒

因为提醒应用需要维护一个提醒列表,所以你需要一个持久性策略。Android SDK 和运行时提供了一个名为 SQLite 的嵌入式数据库引擎,该引擎旨在在内存受限的环境中运行,非常适合移动设备。本节介绍 SQLite 数据库,并探讨如何维护提醒列表。我们的策略将包括一个数据模型、一个数据库代理类和一个CursorAdapter。该模型将保存从数据库读取和写入数据库的数据。代理将是一个适配器类,它将把来自应用的简单调用转换成对 SQLite 数据库的 API 调用。最后,CursorAdapter将扩展一个标准的 Android 类,以抽象的方式处理数据访问。

数据模型

让我们从创建数据模型开始。右键单击 com.apress.gerber.reminders 包并选择 New ➤ Java Class。命名您的课堂提醒,然后按回车键。用清单 5-8 中的代码装饰你的类。这个类是一个简单的 POJO (Plain Old Java Object ),它定义了一些实例变量和相应的 getter 和 setter 方法。Reminder类包括一个整数 ID、一个字符串值和一个数字重要性值。ID 是用于识别每个提醒的唯一号码。字符串值保存提醒的文本。重要性值是一个数字指示器,将单个提醒标记为重要(1 =重要,0 =不重要)。我们在这里使用了int而不是boolean,因为 SQLite 数据库没有boolean数据类型。

Listing 5-8. Reminder Class Definition

public class Reminder {

private int mId;

private String mContent;

private int mImportant;

public Reminder(int id, String content, int important) {

mId = id;

mImportant = important;

mContent = content;

}

public int getId() {

return mId;

}

public void setId(int id) {

mId = id;

}

public int getImportant() {

return mImportant;

}

public void setImportant(int important) {

mImportant = important;

}

public String getContent() {

return mContent;

}

public void setContent(String content) {

mContent = content;

}

}

现在,您将创建一个数据库代理。同样,这个代理将把简单的应用调用转换成较低级别的 SQLite API 调用。在 com.apress.gerber.reminders 包中创建一个名为RemindersDbAdapter的新类。将清单 5-9 中的代码直接放在新创建的 RemindersDbAdapter 类中。当您解析导入时,您会注意到在 Android SDK 中找不到 DatabaseHelper。我们将在后续步骤中定义 DatabaseHelper 类。此代码定义了列名和索引;一个TAG用于测井;两个数据库 API 对象;数据库名称、版本和主表名称的一些常量;上下文对象;和用于创建数据库的 SQL 语句。

Listing 5-9. Code to be placed inside the RemindersDbAdapter class

//these are the column names

public static final String COL_ID = "_id";

public static final String COL_CONTENT = "content";

public static final String COL_IMPORTANT = "important";

//these are the corresponding indices

public static final int INDEX_ID = 0;

public static final int INDEX_CONTENT = INDEX_ID + 1;

public static final int INDEX_IMPORTANT = INDEX_ID + 2;

//used for logging

private static final String TAG = "RemindersDbAdapter";

private DatabaseHelper mDbHelper;

private SQLiteDatabase mDb;

private static final String DATABASE_NAME = "dba_remdrs";

private static final String TABLE_NAME = "tbl_remdrs";

private static final int DATABASE_VERSION = 1;

private final Context mCtx;

//SQL statement used to create the database

private static final String DATABASE_CREATE =

"CREATE TABLE if not exists " + TABLE_NAME + " ( " +

COL_ID + " INTEGER PRIMARY KEY autoincrement, " +

COL_CONTENT + " TEXT, " +

COL_IMPORTANT + " INTEGER );";

SQLite API

DatabaseHelper是一个 SQLite API 类,用于打开和关闭数据库。它使用了Context,这是一个抽象的 Android 类,提供对 Android 操作系统的访问。DatabaseHelper是一个自定义类,必须由你定义。使用清单 5-10 中的代码将DatabaseHelper实现为RemindersDbAdapter的内部类。将此代码放在 RemindersDbAdapters 的末尾,但仍在 RemindersDbAdapters 的大括号内。

Listing 5-10. RemindersDbAdapter

private static class DatabaseHelper extends SQLiteOpenHelper {

DatabaseHelper(Context context) {

super(context, DATABASE_NAME, null, DATABASE_VERSION);

}

@Override

public void onCreate(SQLiteDatabase db) {

Log.w(TAG, DATABASE_CREATE);

db.execSQL(DATABASE_CREATE);

}

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

Log.w(TAG, "Upgrading database from version " + oldVersion + " to "

+ newVersion + ", which will destroy all old data");

db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);

onCreate(db);

}

}

DatabaseHelper扩展了SQLiteOpenHelper,这有助于用特殊的回调方法维护数据库。回调方法是运行时环境将在应用的整个生命周期中调用的方法,它们使用提供的SQLiteDatabase db变量来执行 SQL 命令。构造函数是初始化数据库的地方。构造函数将数据库名称和版本传递给它的超类;然后超类做建立数据库的艰苦工作。当运行时需要创建数据库时,会自动调用onCreate()方法。此操作仅运行一次,即应用首次启动且数据库尚未创建时。每当数据库需要升级时,就会调用onUpgrade()方法,例如,如果开发人员更改了模式。如果您确实更改了数据库模式,请确保将DATABASE_VERSION加 1,然后onUpgrade()将管理剩余的部分。如果你忘记增加DATABASE_VERSION,即使在调试构建模式下,你的程序也会崩溃。在前面的代码中,我们运行一个 SQL 命令来删除数据库中唯一的一个表,然后运行onCreate()方法来重新创建该表。

清单 5-11 中的代码演示了如何使用DatabaseHelper打开和关闭数据库。构造函数保存了一个Context的实例,该实例被传递给DatabaseHelperopen()方法初始化助手并使用它来获取数据库的实例,而close()方法使用助手来关闭数据库。在 RemindersDbAdapter 类中所有成员变量定义之后和DatabaseHelper内部类定义之前添加此代码。当您解析导入时,使用android.database.SQLException类。

Listing 5-11. Database Open and Close Methods

public RemindersDbAdapter(Context ctx) {

this.mCtx = ctx;

}

//open

public void open() throws SQLException {

mDbHelper = new DatabaseHelper(mCtx);

mDb = mDbHelper.getWritableDatabase();

}

//close

public void close() {

if (mDbHelper != null) {

mDbHelper.close();

}

}

清单 5-12 包含了处理tbl_remdrs表中Reminder对象的创建、读取、更新和删除的所有逻辑。这些通常被称为 CRUD 操作;CRUD 代表创建、读取、更新和删除。在 RemindersDbAdapter 类中的close()方法之后添加下面的代码。

Listing 5-12. Database CRUD Operations

//CREATE

//note that the id will be created for you automatically

public void createReminder(String name, boolean important) {

ContentValues values = new ContentValues();

values.put(COL_CONTENT, name);

values.put(COL_IMPORTANT, important ? 1 : 0);

mDb.insert(TABLE_NAME, null, values);

}

//overloaded to take a reminder

public long createReminder(Reminder reminder) {

ContentValues values = new ContentValues();

values.put(COL_CONTENT, reminder.getContent()); // Contact Name

values.put(COL_IMPORTANT, reminder.getImportant()); // Contact Phone Number

// Inserting Row

return mDb.insert(TABLE_NAME, null, values);

}

//READ

public Reminder fetchReminderById(int id) {

Cursor cursor = mDb.query(TABLE_NAME, new String[]{COL_ID,

COL_CONTENT, COL_IMPORTANT}, COL_ID + "=?",

new String[]{String.valueOf(id)}, null, null, null, null

);

if (cursor != null)

cursor.moveToFirst();

return new Reminder(

cursor.getInt(INDEX_ID),

cursor.getString(INDEX_CONTENT),

cursor.getInt(INDEX_IMPORTANT)

);

}

public Cursor fetchAllReminders() {

Cursor mCursor = mDb.query(TABLE_NAME, new String[]{COL_ID,

COL_CONTENT, COL_IMPORTANT},

null, null, null, null, null

);

if (mCursor != null) {

mCursor.moveToFirst();

}

return mCursor;

}

//UPDATE

public void updateReminder(Reminder reminder) {

ContentValues values = new ContentValues();

values.put(COL_CONTENT, reminder.getContent());

values.put(COL_IMPORTANT, reminder.getImportant());

mDb.update(TABLE_NAME, values,

COL_ID + "=?", new String[]{String.valueOf(reminder.getId())});

}

//DELETE

public void deleteReminderById(int nId) {

mDb.delete(TABLE_NAME, COL_ID + "=?", new String[]{String.valueOf(nId)});

}

public void deleteAllReminders() {

mDb.delete(TABLE_NAME, null, null);

}

这些方法都使用SQLiteDatabase mDb变量来生成和执行 SQL 语句。如果您熟悉 SQL,您可能会猜测这些 SQL 语句将采用INSERTSELECTUPDATEDELETE的形式。

这两个创建方法使用一个特殊的ContentValues对象,它是一个数据穿梭器,用于将数据值传递给数据库对象的insert方法。数据库最终会将这些对象转换成 SQL insert语句并执行它们。有两个read方法,一个用于获取单个提醒,另一个用于获取光标来迭代所有提醒。您将在稍后的特殊Adapter课程中使用Cursor

update方法类似于第二个创建方法。然而,这个方法调用了低级数据库对象上的一个update方法,该方法将生成并执行一个update SQL 语句,而不是一个insert

最后,有两个delete方法。第一个使用一个id参数,并使用数据库对象为特定的提醒生成并执行一个delete语句。第二种方法要求数据库生成并执行一个delete语句,从表中删除所有提醒。

此时,您需要一种方法将提醒从数据库中取出并放入ListView。清单 5-13 展示了通过扩展您之前看到的特殊的Adapter Android 类将数据库值绑定到单个行对象的必要逻辑。在 com.apress.gerber.reminders 包中创建一个名为 RemindersSimpleCursorAdapter 的新类,并用下面的代码修饰它。在解析导入时,使用 Android . support . v4 . widget . simplecursoradapter 类。

Listing 5-13. RemindersSimpleCursorAdapter Code

public class RemindersSimpleCursorAdapter extends SimpleCursorAdapter {

public RemindersSimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags) {

super(context, layout, c, from, to, flags);

}

//to use a viewholder, you must override the following two methods and define a ViewHolder class

@Override

public View newView(Context context, Cursor cursor, ViewGroup parent) {

return super.newView(context, cursor, parent);

}

@Override

public void bindView(View view, Context context, Cursor cursor) {

super.bindView(view, context, cursor);

ViewHolder holder = (ViewHolder) view.getTag();

if (holder == null) {

holder = new ViewHolder();

holder.colImp = cursor.getColumnIndexOrThrow(RemindersDbAdapter.COL_IMPORTANT);

holder.listTab =  view.findViewById(R.id.row_tab);

view.setTag(holder);

}

if (cursor.getInt(holder.colImp) > 0) {

holder.listTab.setBackgroundColor(context.getResources().getColor(R.color.orange));

} else {

holder.listTab.setBackgroundColor(context.getResources().getColor(R.color.green));

}

}

static class ViewHolder {

//store the column index

int colImp;

//store the view

View listTab;

}

}

我们用ListView注册Adapter来填充提醒。在运行时,当用户加载和滚动列表时,ListView将重复调用Adapter上的bindView()方法和屏幕上的View对象。用列表项填充这些视图是Adapter的工作。在这个代码示例中,我们使用了名为SimpleCursorAdapterAdapter的子类。这个类使用一个Cursor对象,它跟踪表中的行。

这里你可以看到一个ViewHolder模式的例子。这是一个众所周知的 Android 模式,其中一个小的ViewHolder对象作为标签附加在每个视图上。这个对象通过使用来自数据源的值为列表中的View对象添加装饰,在这个例子中是CursorViewHolder被定义为具有两个实例变量的静态内部类,一个用于重要表列的索引,另一个用于您在布局中定义的row_tab视图。

bindView()方法首先调用超类方法,该方法将光标的值映射到视图中的元素。然后,它检查是否有一个持有者被附加到标签上,如果需要,创建一个新的持有者。然后,bindView()方法通过使用重要的列索引和您之前定义的row_tab来配置 holder 的实例变量。找到或配置持有者后,它使用当前提醒中的COL_IMPORTANT常量的值来决定对row_tab使用哪种颜色。该示例使用了新的橙色,您需要将它添加到您的colors.xml: <color name="orange">#ffff381a</color>中。

之前您使用了一个ArrayAdapter来管理模型和视图之间的关系。SimpleCursorAdapter遵循相同的模式,尽管它的模型是 SQLite 数据库。对清单 5-14 进行更改,以使用新的 RemindersDbAdapter 和 RemindersSimpleCursorAdapter。

Listing 5-14. RemindersActivity Code

public class RemindersActivity extends ActionBarActivity {

private ListView mListView;

private RemindersDbAdapter mDbAdapter;

private RemindersSimpleCursorAdapter mCursorAdapter;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_reminders);

mListView = (ListView) findViewById(R.id.reminders_list_view);

mListView.setDivider(null);

mDbAdapter = new RemindersDbAdapter(this);

mDbAdapter.open();

Cursor cursor = mDbAdapter.fetchAllReminders();

//from columns defined in the db

String[] from = new String[]{

RemindersDbAdapter.COL_CONTENT

};

//to the ids of views in the layout

int[] to = new int[]{

R.id.row_text

};

mCursorAdapter = new RemindersSimpleCursorAdapter(

//context

RemindersActivity.this,

//the layout of the row

R.layout.reminders_row,

//cursor

cursor,

//from columns defined in the db

from,

//to the ids of views in the layout

to,

//flag - not used

0);

//the cursorAdapter (controller) is now updating the listView (view)

//with data from the db (model)

mListView.setAdapter(mCursorAdapter);

}

//Abbreviated for brevity

}

如果此时运行应用,您将不会在列表中看到任何内容;该屏幕将完全是空的,因为您的最后一次更改插入了 SQLite 功能来代替示例数据。按 Ctrl+K | Cmd+K 并提交您的更改,同时显示消息“为提醒添加 SQLite 数据库持久性并为重要提醒添加新颜色”。作为一项挑战,您可能会尝试找出如何通过使用新的RemindersDbAdapter来添加示例项。这将在下一章讨论,所以你可以向前看并检查你的工作。

摘要

至此,你有了一个成熟的 Android 应用。在这一章中,你学习了如何建立你的第一个 Android 项目,并使用 Git 控制它的源代码。您还探索了如何在设计和文本模式下编辑 Android 布局。您已经看到了在操作栏中创建溢出菜单的演示。本章最后探讨了ListViewsAdapters,并将数据绑定到内置的 SQLite 数据库。在下一章中,您将通过添加创建和编辑提醒的功能来完善应用。

六、提醒 Lab:第二部分

本章介绍如何通过使用自定义对话框来捕获用户输入。我们还将继续演示适配器和 SQLite 数据库的使用。在本章中,我们将完成从第五章开始的实验。

添加/删除提醒

第五章中的例子让屏幕空着,没有任何提醒。要查看带有提醒列表的应用布局,在应用启动时添加一些示例提醒很有用。如果您试图对前一章的挑战提出一个解决方案,将您的代码与清单 6-1 中的变化进行比较。清单 6-1 中的代码检查实例是否有任何保存的状态,如果没有,它继续设置示例数据。为此,代码调用了DatabaseAdapter上的一些方法;一个用于清除所有提醒,另一个用于插入一些提醒。

Listing 6-1. Add Some Example Reminders

public class RemindersActivity extends ActionBarActivity {

private ListView mListView;

private RemindersDbAdapter mDbAdapter;

private RemindersSimpleCursorAdapter mCursorAdapter;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_reminders);

mListView = (ListView) findViewById(R.id.reminders_list_view);

mListView.setDivider(null);

mDbAdapter = new RemindersDbAdapter(this);

mDbAdapter.open();

if (savedInstanceState == null) {

//Clear all data

mDbAdapter.deleteAllReminders();

//Add some data

mDbAdapter.createReminder("Buy Learn Android Studio", true);

mDbAdapter.createReminder("Send Dad birthday gift", false);

mDbAdapter.createReminder("Dinner at the Gage on Friday", false);

mDbAdapter.createReminder("String squash racket", false);

mDbAdapter.createReminder("Shovel and salt walkways", false);

mDbAdapter.createReminder("Prepare Advanced Android syllabus", true);

mDbAdapter.createReminder("Buy new office chair", false);

mDbAdapter.createReminder("Call Auto-body shop for quote", false);

mDbAdapter.createReminder("Renew membership to club", false);

mDbAdapter.createReminder("Buy new Galaxy Android phone", true);

mDbAdapter.createReminder("Sell old Android phone - auction", false);

mDbAdapter.createReminder("Buy new paddles for kayaks", false);

mDbAdapter.createReminder("Call accountant about tax returns", false);

mDbAdapter.createReminder("Buy 300,000 shares of Google", false);

mDbAdapter.createReminder("Call the Dalai Lama back", true);

}

//Removed remaining method code for brevity...

}

//Removed remaining method code for brevity...

}

有几个对createReminder()方法的调用,每个调用都接受一个带有提醒文本的字符串值和一个将提醒标记为重要的布尔值。我们为true设置了几个值,以提供良好的视觉效果。在所有的createReminder()调用周围单击并拖动选择,然后按 Ctrl+Alt+M | Cmd+Alt+M,弹出提取方法对话框,如图 6-1 所示。这是通过“重构”菜单和快捷键组合可用的许多重构操作之一。输入 insertSomeReminders 作为新方法的名称,然后按 Enter 键。RemindersActivity中的代码将被替换为对您在提取方法对话框中命名的新方法的调用,并且代码将被移动到该方法的主体中。

A978-1-4302-6602-0_6_Fig1_HTML.jpg

图 6-1。

Extract Method dialog box, create insertSomeReminders( ) method

运行应用,查看界面的外观和使用示例提醒的行为。你的应用应该看起来像图 6-2 中的截图。一些提醒应该用绿色的行标签显示,而那些标记为重要的会用橙色标签显示。使用消息“添加示例提醒”提交您的更改。

A978-1-4302-6602-0_6_Fig2_HTML.jpg

图 6-2。

Runtime with example reminders inserted

响应用户交互

任何应用都没有多大用处,除非它对输入做出响应。在本节中,您将添加逻辑来响应触摸事件,并最终允许用户编辑各个提醒。应用中的主要组件是 Android View对象的子类ListView。到目前为止,除了将View对象放置在布局中,你还没有对它们做什么。android.view.View对象是所有绘制到屏幕上的组件的超类。

将清单 6-2 中的代码添加到RemindersActivityonCreate()方法的底部,就在右花括号之前,然后解析 imports。这是一个匿名的内部类实现OnItemClickListener,它只有一个方法onItemClicked()。当您与它所附加的ListView组件交互时,运行时将使用这个对象。每当你点击ListView时,匿名内部类的onCreate()方法将被调用。我们定义的方法使用了Toast,Android SDK 中的一个类。对Toast.makeText()的调用会在屏幕上显示一个小弹出窗口,显示传递给该方法的任何文本。您可以使用Toast作为方法被正确调用的快速指示器,如清单 6-2 所示。

Note

某些设备可能会隐藏信息。另一种方法是使用 Android logger 记录消息,这在第十二章中有详细介绍。

Listing 6-2. Set an OnItemClickListener with a Toast

//when we click an individual item in the listview

mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

@Override

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

Toast.makeText(RemindersActivity.this, "clicked " + position,

Toast.LENGTH_SHORT).show();

}

});

单击列表中的第一项会调用onItemClick()方法,该方法的位置值为 0,因为列表中的元素从 0 开始进行索引。然后逻辑弹出一个带有点击文本和位置的祝酒词,如图 6-3 所示。

A978-1-4302-6602-0_6_Fig3_HTML.jpg

图 6-3。

Toast message after tapping the first reminder

用户对话框

熟悉了触摸事件之后,您现在可以增强 click listener 来显示对话框。用清单 6-3 中的代码替换整个onItemClick()方法。当你解析导入时,请使用android.support.v7.app.AlertDialog类。

Listing 6-3. onItemClick( ) Modifications to Allow Edit/Delete

public void onItemClick(AdapterView<?> parent, View view, final int masterListPosition, long id) {

AlertDialog.Builder builder = new AlertDialog.Builder(RemindersActivity.this);

ListView modeListView = new ListView(RemindersActivity.this);

String[] modes = new String[] { "Edit Reminder", "Delete Reminder" };

ArrayAdapter<String> modeAdapter = new ArrayAdapter<>(RemindersActivity.this,

android.R.layout.simple_list_item_1, android.R.id.text1, modes);

modeListView.setAdapter(modeAdapter);

builder.setView(modeListView);

final Dialog dialog = builder.create();

dialog.show();

modeListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

@Override

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

//edit reminder

if (position == 0) {

Toast.makeText(RemindersActivity.this, "edit "

+ masterListPosition, Toast.LENGTH_SHORT).show();

//delete reminder

} else {

Toast.makeText(RemindersActivity.this, "delete "

+ masterListPosition, Toast.LENGTH_SHORT).show();

}

dialog.dismiss();

}

});

}

在前面的代码中,您看到了另一个正在工作的 Android 类,AlertDialog.Builder。类Builder是嵌套在AlertDialog类中的静态类,它用于构建AlertDialog

到目前为止,这个 Lab 中的代码创建了一个ListView和一个ArrayAdapter来向一个ListView提供项目。你可能还记得第五章中的这个模式。在传递给ListView之前,该适配器由两个潜在选项组成的数组创建,编辑提醒和删除提醒,然后传递给AlertDialog.Builder。然后使用生成器创建并显示一个带有选项列表的对话框。

请特别注意清单 6-3 中的最后一段代码。它类似于前面添加的OnItemClickListener()代码;然而,我们在当前的OnItemClickListener中创建的modeListView上附加了一个点击监听器。您看到的是一个带有OnItemClickListenerListView,它创建了另一个modeListView和另一个嵌套的OnItemClickListener来响应modeListView的点击事件。

嵌套的 click 侦听器弹出一条 toast 消息,指示是否点击了编辑或删除项。它还将外层OnItemClickListener的位置参数重命名为masterListPosition,以区别于嵌套OnItemClickListener中的position参数。这个主位置在 toast 中用于指示哪个提醒可能被编辑或删除。最后,从点击监听器中调用dialog.dismiss()方法,这将完全删除对话框。

通过在您的设备或仿真器上运行来测试图 6-4 中所示的新功能。点击提醒事项,然后在新弹出对话框中点击编辑提醒事项或删除提醒事项。如果 toast 中报告的提醒位置与您点击的提醒不匹配,请仔细检查您是否将masterListPosition值附加到了 toast 中的文本,并且没有使用position。按 Ctrl+K | Cmd+K 提交此逻辑,并使用消息为单个列表项添加一个 ListView 对话框。

A978-1-4302-6602-0_6_Fig4_HTML.jpg

图 6-4。

Simulating the deletion of a reminder

提供多选上下文菜单

随着应用开始成形,你现在将攻击一个允许在一次操作中编辑多个提醒的功能。此功能仅在运行 API 11 和更高版本的设备上可用。您将通过使用资源加载约定,使该特性在应用中有条件地可用。这一过程将在本章稍后解释,并在第八章中详细说明。您还需要在运行时进行检查,以决定是否启用该特性。

首先为提醒行项目创建一个备用布局。打开项目工具窗口,右键单击res文件夹,弹出上下文菜单。从菜单中选择新建 Android 资源文件,在对话框中输入 reminders_row 作为名称,如图 6-5 所示。

A978-1-4302-6602-0_6_Fig5a_HTML.jpg A978-1-4302-6602-0_6_Fig5b_HTML.jpg

图 6-5。

New resource file for reminders_row

选择 Layout 作为资源类型,这将自动将目录名更改为layout。在可用限定词部分下选择版本,然后单击双 v 形(> >)按钮将版本添加到所选限定词列表中。输入 11 作为平台 API 级别,注意目录名已经更新,以反映所选的限定符。这些被称为资源限定符,它们在运行时被询问,以允许您为特定的设备和平台版本定制用户界面。按 Enter 键(或单击 OK)接受这个新的资源限定目录并继续。如果你打开项目工具窗口,并将其视图设置为 Android,如图 6-6 所示,你会看到两个reminders_row布局文件被分组在layout文件夹下。同样,项目窗口的 Android 视图将相关文件分组在一起,让您可以有效地管理它们。

A978-1-4302-6602-0_6_Fig6_HTML.jpg

图 6-6。

Grouped layouts

复制整个原始reminders_row布局并粘贴到版本 11 新创建的布局中。现在使用下面的代码改变内部水平LinearLayoutbackground属性:

android:background="?android:attr/activatedBackgroundIndicator"

这个赋给背景属性的值以?android:attr/为前缀,它指的是 Android SDK 中定义的一种样式。Android SDK 提供了许多这样的预定义属性,你可以在你的应用中使用它们。属性使用系统定义的颜色作为多选模式下激活的项目的背景。

针对早期的 SDK

现在您将学习如何引入一个平台相关的特性。打开项目工具窗口,并打开 Gradle 脚本部分下的app模块的build.gradle文件(这将是第二个条目)。这些 Gradle 文件包含编译和打包应用的构建逻辑。所有关于你的应用支持哪些平台的配置都位于这些特殊的文件中(第十三章深入探讨了 Gradle build 系统)。请注意,minSdkVersion 设置为 8,这允许您的应用在 99%以上的 Android 设备上运行。我们将要创建的特性需要最低版本为 11 的 SDK (aka API)。我们在本节中介绍的代码和特性将允许运行 SDK 11 或更高版本的用户利用一个称为上下文动作模式的特性。此外,那些运行 SDK 版本低于 11 的人将不会看到这个功能,但更重要的是,他们的应用不会崩溃。

添加上下文操作模式

下一个特性在多选模式下引入了一个上下文动作菜单,这是一个动作列表,可以应用于所有选中项目的上下文。通过右键单击 res/menu 目录并选择 New ➤菜单资源文件来添加一个新的菜单资源,并将其命名为cam_menu。用下面的代码来修饰它:

<?xml version="1.0" encoding="utf-8"?>

<menu xmlns:android="``http://schemas.android.com/apk/res/android

<item android:id="@+id/menu_item_delete_reminder"

android:icon="@android:drawable/ic_menu_delete"

android:title="delete" />

</menu>

这个资源文件为上下文菜单定义了一个单独的delete动作项。这里您也使用了稍微不同的属性值。这些特殊值类似于您之前在background属性中使用的值,因为它们允许您访问内置的 Android 默认值。然而,?android:attr/前缀仅在引用样式属性时使用。这些属性中使用的语法遵循稍微不同的形式。使用 at 符号(@)触发资源值的名称空间查找。您可以通过这种方式访问各种名称空间。android名称空间是所有内置 Android 值所在的位置。在这个名称空间中有各种资源位置,例如drawablestringlayout。当您使用特殊的@+id前缀时,它会在项目的 R.java 文件中创建一个新的 ID,当您使用@id前缀时,它会在 Android SDK 的 R.java 文件中查找一个现有的 ID。这个例子定义了一个新的 ID 名menu_item_delete_reminder,它与菜单选项相关联。它还从名称空间android:drawable中取出一个图标,用作它的图标。

使用新的上下文菜单和运行 API 11 或更高版本的设备的备用布局,您可以添加一个复选标记,以有条件地启用带有上下文操作菜单的多选模式。打开RemindersActivity并在onCreate方法的末尾添加以下if块:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {

}

Build类是从android.os包中导入的,它让您可以访问一组常数值,这些常数值可以用来匹配具有特定 API 级别的设备。在这种情况下,您希望 API 级别等于或高于包含整数值 11 的HONEYCOMB。将清单 6-4 中的代码插入刚刚定义的 if 块中。if 块保护运行低于 Honeycomb 的操作系统的设备,如果没有 Honeycomb,应用将会崩溃。

Listing 6-4. MultiChoiceModeListener Example

mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);

mListView.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {

@Override

public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { }

@Override

public boolean onCreateActionMode(ActionMode mode, Menu menu) {

MenuInflater inflater = mode.getMenuInflater();

inflater.inflate(R.menu.cam_menu, menu);

return true;

}

@Override

public boolean onPrepareActionMode(ActionMode mode, Menu menu) {

return false;

}

@Override

public boolean onActionItemClicked(ActionMode mode, MenuItem item) {

switch (item.getItemId()) {

case R.id.menu_item_delete_reminder:

for (int nC = mCursorAdapter.getCount() - 1; nC >= 0; nC--) {

if (mListView.isItemChecked(nC)) {

mDbAdapter.deleteReminderById(getIdFromPosition(nC));

}

}

mode.finish();

mCursorAdapter.changeCursor(mDbAdapter.fetchAllReminders());

return true;

}

return false;

}

@Override

public void onDestroyActionMode(ActionMode mode) { }

});

解决任何导入。您会注意到 getIdFromPositon()没有被定义,并且被标记为红色。将光标放在方法上,按 Alt+Enter 调用IntelliSense并选择创建方法。选择 RemindersActivity 作为目标类。选择int作为返回值。装饰方法如清单 6-5 所示。

Listing 6-5. getIdFromPosition() method

private int getIdFromPosition(int nC) {

return (int)mCursorAdapter.getItemId(nC);

}

前面的逻辑定义了一个MultiChoiceModeListener并将它附加到ListView上。每当您长按ListView中的一个项目时,运行时就会调用MultiChoiceModeListener上的onCreateActionMode()方法。如果方法返回布尔值true,则进入多选操作模式。在这种模式下,这里被重写的方法中的逻辑会展开一个显示在操作栏中的上下文菜单。使用多选操作模式的好处是可以选择多行。一次点击选择项目,随后的点击取消选择项目。当您点击上下文菜单中的每一项时,运行时将使用被点击的菜单项调用onActionItemClicked()方法。

在这个方法中,通过比较添加到菜单项的删除元素的itemIdid来检查删除项是否被点击。(有关删除项 ID 的描述,请参见本节开头的 XML 清单。)如果项目被选中,您循环遍历每个列表项目,并请求mDbAdapter删除它们。删除选中的项目后,逻辑调用ActionMode对象上的finish(),这将禁用多选操作模式,并将ListView返回到正常状态。接下来,调用fetchAllReminders()从数据库中重新加载所有提醒,并将调用返回的光标传递给mCursorAdapter对象上的changeCursor方法。最后,该方法返回true来指示该动作已经被正确处理。在不处理逻辑的所有其他情况下,该方法返回false,表明其他一些事件侦听器可以处理 tap 事件。

Android Studio 将突出显示几个错误语句,因为您使用的 API 在比 Honeycomb 更老的平台上不可用。这个错误是由 Lint 生成的,Lint 是一个内置于 Android SDK 中的静态分析工具,并完全集成到 Android Studio 中。您需要在@Override注释的上方或下方向RemindersActivity.onCreate()方法添加以下注释,并解析 TargetApi 的导入:

@TargetApi(Build.VERSION_CODES.HONEYCOMB)

这个特殊的注释告诉 Lint,无论构建配置指定什么,都要将方法调用视为针对所提供的 API 级别。将您的更改提交给 Git,并显示消息“使用上下文操作菜单添加上下文操作模式”。图 6-7 描绘了当您构建并运行应用来测试新功能时可能会看到的情况。

A978-1-4302-6602-0_6_Fig7_HTML.jpg

图 6-7。

Multichoice mode enabled

实现添加、编辑和删除

到目前为止,您已经添加了从列表中删除提醒的逻辑。该逻辑仅在上下文动作模式下可用。您目前无法插入新的提醒或修改现有的提醒。但是,现在您将创建一个自定义对话框来添加提醒,并创建另一个对话框来编辑现有的提醒。最终,您会将这些对话框绑定到RemindersDbAdapter

在继续之前,您需要定义一些额外的颜色。将以下颜色定义添加到您的colors.xml文件中:

<color name="light_grey">#bababa</color>

<color name="black">#000000</color>

<color name="blue">#ff1118ff</color>

Note

通常,你的应用会有一个整体的颜色主题,这将确保所有屏幕和对话框之间的一致性。然而,颜色主题超出了这个简单实验的范围。

规划自定义对话框

要养成的一个好习惯是,在实现 UI 之前,先用简单的工具画出草图。这样做可以让您在编写任何代码之前,直观地了解元素在屏幕上的位置。你可以使用跨平台的编辑器,比如 Inkscape,或者你可以使用像笔记本纸和铅笔这样简单的东西。在移动业务中,这些草图被称为线框。

图 6-8 是用 Inkscape 完成的自定义对话框的图示。线框是有意非正式的,强调组件的位置,而不是特定的外观和感觉。

A978-1-4302-6602-0_6_Fig8_HTML.jpg

图 6-8。

Wireframe sketch of the custom dialog box Note

本书中的一些自定义插图和线框是使用多平台矢量图形编辑器 Inkscape 创建的。可在 www.inkscape.org 免费获取。

有了线框,你可以开始计划如何在屏幕上排列组件。因为大多数组件从上到下流动,所以对最外层的容器使用垂直的LinearLayout是一个明显的选择。但是,底部的两个按钮是并排的。对于这些你可以使用一个水平的LinearLayout并将其嵌套在包含垂直的LinearLayout中。图 6-9 向图纸添加注释并突出显示该嵌套组件。

A978-1-4302-6602-0_6_Fig9_HTML.jpg

图 6-9。

Wireframe sketch with widget labels

从计划到代码

有了这些线框之后,尝试使用可视化设计器来设计布局。首先右键单击项目工具窗口中的res目录,选择创建新的 Android 资源文件选项,将资源文件命名为 dialog_custom,然后选择 Layout 作为资源类型。使用LinearLayout作为根元素完成对话框。为了重现我们的线框,从面板拖放视图到舞台上。清单 6-6 包含了完整的布局 XML 定义,以及您将在 Java 代码中使用的 ID 值。

Listing 6-6. Completed dialog_custom.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:id="@+id/custom_root_layout"

android:layout_width="300dp"

android:layout_height="fill_parent"

android:background="@color/green"

android:orientation="vertical"

>

<TextView

android:id="@+id/custom_title"

android:layout_width="fill_parent"

android:layout_height="60dp"

android:gravity="center_vertical"

android:padding="10dp"

android:text="New Reminder:"

android:textColor="@color/white"

android:textSize="24sp" />

<EditText

android:id="@+id/custom_edit_reminder"

android:layout_width="fill_parent"

android:layout_height="100dp"

android:layout_margin="4dp"

android:background="@color/light_grey"

android:gravity="start"

android:textColor="@color/black">

<requestFocus />

</EditText>

<CheckBox

android:id="@+id/custom_check_box"

android:layout_width="fill_parent"

android:layout_height="30dp"

android:layout_margin="4dp"

android:background="@color/black"

android:paddingLeft="32dp"

android:text="Important"

android:textColor="@color/white" />

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:orientation="horizontal">

<Button

android:id="@+id/custom_button_cancel"

android:layout_width="0dp"

android:layout_height="60dp"

android:layout_weight="50"

android:text="Cancel"

android:textColor="@color/white"

/>

<Button

android:id="@+id/custom_button_commit"

android:layout_width="0dp"

android:layout_height="60dp"

android:layout_weight="50"

android:text="Commit"

android:textColor="@color/white"

/>

</LinearLayout>

</LinearLayout>

创建自定义对话框

您现在将使用RemindersActivity中已完成的对话框布局。清单 6-7 是一个新fireCustomDialog()方法的实现。将这段代码放在RemindersActivity.java文件中,就在onCreateOptionsMenu()方法的上面,并解析导入。

Listing 6-7. The fireCustomDialog( ) Method

private void fireCustomDialog(final Reminder reminder){

// custom dialog

final Dialog dialog = new Dialog(this);

dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);

dialog.setContentView(R.layout.dialog_custom);

TextView titleView = (TextView) dialog.findViewById(R.id.custom_title);

final EditText editCustom = (EditText) dialog.findViewById(R.id.custom_edit_reminder);

Button commitButton = (Button) dialog.findViewById(R.id.custom_button_commit);

final CheckBox checkBox = (CheckBox) dialog.findViewById(R.id.custom_check_box);

LinearLayout rootLayout = (LinearLayout) dialog.findViewById(R.id.custom_root_layout);

final boolean isEditOperation = (reminder != null);

//this is for an edit

if (isEditOperation){

titleView.setText("Edit Reminder");

checkBox.setChecked(reminder.getImportant() == 1);

editCustom.setText(reminder.getContent());

rootLayout.setBackgroundColor(getResources().getColor(R.color.blue));

}

commitButton.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

String reminderText = editCustom.getText().toString();

if (isEditOperation) {

Reminder reminderEdited = new Reminder(reminder.getId(),

reminderText, checkBox.isChecked() ? 1 : 0);

mDbAdapter.updateReminder(reminderEdited);

//this is for new reminder

} else {

mDbAdapter.createReminder(reminderText, checkBox.isChecked());

}

mCursorAdapter.changeCursor(mDbAdapter.fetchAllReminders());

dialog.dismiss();

}

});

Button buttonCancel = (Button) dialog.findViewById(R.id.custom_button_cancel);

buttonCancel.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

dialog.dismiss();

}

});

dialog.show();

}

fireCustomDialog()方法将用于插入和编辑,因为这两种操作之间几乎没有区别。该方法的前三行创建了一个没有标题的 Android 对话框,并扩展了清单 6-6 中的布局。然后,fireCustomDialog()方法从这个布局中找到所有重要的元素,并将它们存储在局部变量中。然后,该方法通过检查提醒参数是否为空来设置一个isEditOperation布尔变量。如果有一个提醒被传入(或者如果值不为空),该方法假定这不是一个编辑操作,并且变量被设置为false;否则,设置为true。如果对 fireCustomDialog()的调用是一个编辑操作,则标题被设置为编辑提醒,而CheckBoxEditText则使用来自提醒参数的值进行设置。该方法还将最外层容器布局的背景设置为蓝色,以便在视觉上区分编辑对话框和插入对话框。

接下来的几行代码组成了一个代码块,它为提交按钮设置并定义了一个OnClickListener。这个侦听器通过更新数据库来响应提交按钮上的 click 事件。再次检查isEditOperation(),如果正在进行编辑操作,则使用来自提醒参数的 ID 和来自EditText的值以及屏幕上的复选框值创建一个新的提醒。这个提醒通过使用updateReminder()方法传递给mDbAdapter

如果没有进行编辑,逻辑会要求mDbAdapter使用来自EditText的值和屏幕复选框值在数据库中创建一个新的提醒。在更新或创建调用被调用后,通过使用mCursorAdapter.changeCursor()方法重新加载提醒。这个逻辑类似于您之前在清单 6-5 中添加的逻辑。提醒重新加载后,点击监听器关闭对话框。

在配置了“提交”按钮的单击行为之后,该示例为“取消”按钮设置了另一个单击侦听器。这个监听器简单地关闭对话框。指定了这两个按钮的行为后,该示例以显示自定义对话框结束。

现在你可以在OnItemClickListener中使用这个新方法来代替onCreate()方法中的modeListView。找到这个监听器的onItemClick()方法,并用下面的代码替换整个方法:

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

//edit reminder

if (position == 0) {

int nId = getIdFromPosition(masterListPosition);

Reminder reminder = mDbAdapter.fetchReminderById(nId);

fireCustomDialog(reminder);

//delete reminder

} else {

mDbAdapter.deleteReminderById(getIdFromPosition(masterListPosition));

mCursorAdapter.changeCursor(mDbAdapter.fetchAllReminders());

}

dialog.dismiss();

}

要编辑一个提醒,您可以使用ListView位置,用一个查找提醒的调用替换Toast.makeText()调用。这个提醒然后被传递给fireCustomDialog()方法来触发编辑行为。要删除提醒,您可以使用与在多选模式下添加到清单 6-5 中的逻辑相同的逻辑。同样,mDbAdapter.deleteReminderById()用于删除提醒,changeCursor()方法用于从mDbAdapter.fetchAllReminders()调用返回的光标。

RemindersActivity.java文件的最底部找到onOptionsItemSelected()方法,并修改它,看起来像清单 6-8 。

Listing 6-8. onOptionsItemSelected Definition

public boolean onOptionsItemSelected(MenuItem item) {

switch (item.getItemId()) {

case R.id.action_new:

//create new Reminder

fireCustomDialog(null);

return true;

case R.id.action_exit:

finish();

return true;

default:

return false;

}

}

这里,当选择的菜单项是action_new item时,您只需添加对fireCustomDialog()的调用。您将null传递给该方法,因为前面的逻辑会检查 null 值并将isEditOperation设置为false,从而调用一个新的提醒对话框。运行应用并测试新功能。您应该能够看到新的自定义对话框。创建提醒时会看到绿色对话框,编辑提醒时会看到蓝色对话框,分别如图 6-10 和图 6-11 所示。测试菜单项以确保创建和删除操作正常运行。使用 Commit 消息将您的更改提交到 Git,该消息通过自定义对话框添加了数据库创建、读取、更新和删除支持。

A978-1-4302-6602-0_6_Fig11_HTML.jpg

图 6-11。

Edit Reminder dialog box

A978-1-4302-6602-0_6_Fig10_HTML.jpg

图 6-10。

New Reminder dialog box

添加自定义图标

有了所有的功能,您可以添加一个自定义图标作为点睛之笔。您可以使用任何图像编辑器来创建图标,或者,如果您不喜欢图形,可以在网上找到一些免版税的剪贴画。我们的示例用在 Inkscape 中创建的自定义图稿替换了ic_launcher图标。打开项目工具窗口,右键单击 res/mipmap 目录。现在选择新的➤图像素材。你会看到如图 6-12 所示的对话框。单击位于Image file:字段最右侧的省略号按钮,导航至您创建的图像素材的位置。保留其余的设置,如图 6-13 所示。现在单击下一步,并在随后的对话框中单击完成。

A978-1-4302-6602-0_6_Fig13_HTML.jpg

图 6-13。

Custom icon in action bar

A978-1-4302-6602-0_6_Fig12_HTML.jpg

图 6-12。

New Image Asset dialog box

有许多名为mipmap的文件夹。这些文件夹都有指定为屏幕大小限定符的后缀。Android 运行时将从特定的文件夹中提取资源,这取决于运行该应用的设备的屏幕分辨率。资源文件夹及其后缀在第八章中有更详细的介绍。

将以下代码行插入 RemindersActivity 的 onCreate()方法中,在展开布局的代码行之后,setContentView(R.layout.activity_reminders);。这段代码在您的操作栏中显示一个自定义图标:

ActionBar actionBar = getSupportActionBar();

actionBar.setHomeButtonEnabled(true);

actionBar.setDisplayShowHomeEnabled(true);

actionBar.setIcon(R.mipmap.ic_launcher);

当您运行代码时,您将在操作栏中看到您的自定义图标。图 6-13 显示了使用自定义图标运行的应用示例。

按 Ctrl+K | Cmd+K 并提交您的更改,同时显示消息“添加自定义图标”。

摘要

恭喜你!您已经使用 Android Studio 实现了您的第一个 Android 应用。在此过程中,您学习了如何使用可视化设计器编辑 XML 布局。您还了解了如何使用文本模式编辑原始 XML。本章向您展示了如何在支持该特性的平台上有条件地实现上下文动作模式。最后,您看到了如何为各种屏幕密度添加自定义图标。

七、Git 简介

Git 版本控制系统(VCS)正在迅速成为事实上的标准,不仅在 Android 应用开发中,而且在一般的软件编程中。与需要使用中央服务器的早期版本控制系统不同,Git 是分布式的,这意味着存储库的每个副本都包含项目的整个历史,任何贡献者都没有特权。Git 是由 Linux fame 的 Linus Torvalds 开发的,目的是管理 Linux 操作系统的开发。就像开源运动本身一样,Git 是系统化的非层级结构,鼓励协作。

虽然 Git 从命令行提供了丰富的特性,但本章主要关注在 Android Studio 中使用 Git。多年来,支撑 Android Studio 的 IntelliJ 平台为包括 Git 在内的几个 VCS 系统提供了出色的支持。不同支持系统的一致性以一种使新手和专业人员都容易精通的方式呈现。然而,理解从 Android Studio 内部使用 Git 和从命令行使用 Git 之间的区别是很重要的。本章非常详细地解释了你开始使用 Git 所需要的一切。您将重用在前面章节中开始的 Reminders 应用来学习提交、分支、推送和获取等重要命令的基础知识。您将使用本地和远程 Git 存储库,并了解如何在协作环境中使用 Git 和 Android Studio。

打开你在第一章中创建的 HelloWorld 项目。如果您跳过了这一章,从头开始创建一个名为 HelloWorld 的新项目。在向导过程中,使用所有默认设置。您将简要地使用这个项目来理解 Git 设置的基础。

安装 Git

在开始使用 Git 之前,您需要安装它。将浏览器指向 http://git-scm.com/downloads 。点击您的操作系统的下载按钮,如图 7-1 所示。

A978-1-4302-6602-0_7_Fig1_HTML.jpg

图 7-1。

Git download page

我们建议将 Git 安装在 Windows 上的C:\java\ directory目录中,或者 Mac 或 Linux 上的∼/java目录中。无论您决定在哪里安装它,都要确保整个路径没有空格。例如,不要在C:\Program Files目录中安装 Git,因为在ProgramFiles之间有一个空格。像 Git 这样的面向命令行的工具可能会遇到名称中有空格的目录的问题。一旦安装完成,您必须确保C:\java\git\bin\目录是您的PATH环境变量的一部分。关于如何给PATH环境变量添加路径的详细说明,参见第一章。

通过单击 Git Bash 图标启动 Git Bash 终端。如果你运行的是 Mac 或 Linux,只需打开一个终端。您需要用您的姓名和电子邮件配置 Git,以便您的提交将有一个作者。从 Git Bash 发出以下命令,用您自己的名字和电子邮件地址替换 John Doe 的名字和电子邮件地址。图 7-2 显示了一个例子。

A978-1-4302-6602-0_7_Fig2_HTML.jpg

图 7-2。

Adding your name and e-mail to Git

$ git config --global user.name "John Doe"

$ git config --global user.email johndoe@example.com

返回 Android Studio 继续设置 Git 与 Android Studio 的集成。导航到文件➤设置,然后在左窗格的版本控制部分找到 Git。单击省略号按钮并导航到刚刚安装的 Git 二进制文件。单击 Test 按钮以确保您的 Git 环境是可操作的。您应该看到一个弹出窗口,表明 Git 执行成功,以及您安装的 Git 版本。

导航到 VCS ➤导入到版本控制➤创建 Git 存储库。当对话框提示您选择将创建新 Git 存储库的目录时,确保您选择了项目根目录HelloWorld。你可以选择在目录选择器对话框中点击小小的 Android Studio 图标。该图标将导航到项目的根目录,如图 7-3 所示。单击 OK 按钮,您的本地 Git 存储库将被创建。

A978-1-4302-6602-0_7_Fig3_HTML.jpg

图 7-3。

Selecting the directory for your Git repository

您会注意到项目工具窗口中的大多数文件名都变成了棕色。这意味着这些文件被 Git 在本地识别,但是没有被 Git 跟踪,也没有被计划添加。Git 以两阶段的方式管理提交(这不同于 Subversion 和 Perforce 等其他 VCS 工具使用的方式)。登台区是 Git 在提交之前组织更改的地方。进行中的变更、临时区域变更和提交的变更之间的差异是显著的,可能会使新用户不知所措。因此,Android Studio 不会暴露这些差异。取而代之的是一个简单的 changes 界面,允许您轻松地管理和提交修改后的文件。

忽略文件

当您创建本地存储库时,Android Studio 会生成特殊的.gitignore文件,防止某些路径被跟踪。除非您另外指定,否则 Git 将继续跟踪这个目录及其子目录中的所有文件。然而,.gitignore文件可以告诉 Git 忽略某些文件或整个目录。

通常,根目录有一个.gitingore文件,每个项目有一个.gitignore文件。在 HelloWorld 中,一个.gitignore位于 HelloWorld 的根目录,一个.gitignore位于 app 文件夹的根目录。打开位于 HelloWorld 根目录下的.gitignore文件并检查其内容。图 7-4 展示了项目根目录下生成的.gitignore文件。默认情况下,Android Studio 会将某些文件从您的 Git 存储库中排除。该列表包括由项目生成或特定于本地计算机的控制设置生成的文件。例如,/.idea/workspace.xml文件控制 Android Studio 本地配置的设置。虽然可以在 Git 中跟踪这一点,但它不一定是您正在构建的项目的一部分,事实上可能会造成问题,因为该文件对于每个工作空间(例如计算机)都是唯一的。注意.gitignore中的一个条目是/local.properties。像 workspace.xml 一样,local.properties 对于每台计算机都是唯一的。

A978-1-4302-6602-0_7_Fig4_HTML.jpg

图 7-4。

The root .gitignore file contents

注意列表中的/build条目。在第十三章中深入讨论的 Android Studio 构建系统 Gradle,在你编译和运行你的项目时,把它所有的输出放在这里。因为该文件夹将包含来自。类文件到.dex文件再到最终的可安装 Android 包,而且因为它的内容是不断变化的,所以用 Git 跟踪它意义不大。在项目工具窗口中找到local.properties文件。你会注意到它是黑色的,而其他文件是棕色的。

Android Studio 使用一种配色方案,允许您在工作时轻松识别您的版本控制系统将会看到什么。正如我们已经说过的,brown 表示 Git 在本地识别了 a 文件,但是 Git 没有跟踪它,也没有计划添加它。蓝色表示 Git 正在跟踪的文件已经被更改。绿色用于 Git 正在跟踪的全新文件。黑色表示文件未被更改或未被跟踪。Android Studio 不断跟踪添加到项目中的文件,并在必要时提示您将这些文件与 Git 保持同步。

添加文件

打开屏幕底部的更改视图。它包括两个部分:默认和未版本化的文件。默认部分最初为空,表示活动的变更列表。当您修改和创建文件时,它们将属于此部分,因为它保存了准备提交到您的 VCS 的文件。未版本化文件部分包含所有不被 VCS 跟踪的内容。

因为尚未跟踪所有项目文件,所以它们属于未版本化文件部分。您需要将这些添加到您的存储库中。“变更”视图的左侧是两列图标。在右栏中,单击顶部第三个图标(文件夹图标);参见图 7-5 中的圆圈图标。这是一种切换方式,使您可以按文件夹对文件进行分组,以便更好地了解它们在项目中的相对位置。右键单击未版本化文件部分标题,然后从上下文菜单中单击添加到 VCS,将这些文件添加到 Git 索引中。或者,您可以单击并拖动整个部分到粗体默认部分。

A978-1-4302-6602-0_7_Fig5_HTML.jpg

图 7-5。

Group files by folders

添加完所有文件后,单击带有向上绿色箭头的 VCS 图标。这将打开你在第五章中开始使用的熟悉的提交对话框。单击 Commit 记录您的更改,默认部分最终会清空。您也可以按 Ctrl+K | Cmd+K 来执行相同的操作。从现在开始,你在 Android Studio 中接触的每个文件都将在 Git 下被跟踪。

克隆参考应用:提醒

本节扩展了您在第 5 和 6 章节中创建的提醒应用。我们邀请您使用 Git 克隆这个项目,以便跟进,尽管您将使用从第 5 和 6 章中使用的资源库派生的新 Git 资源库重新创建这个项目。如果您的计算机上没有安装 Git,请参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端)并导航到 C:\androidBook\reference(如果没有参考目录,请创建一个。在 Mac 上,导航到/your-labs-parent-dir/reference/)并发出以下 git 命令:git clonehttps://bitbucket.org/csgerber/reminders-git.gitreminders git。您将使用 Git 特性来修改项目,就像您在一个团队中工作一样。通过这个过程,您将学习如何派生和克隆一个项目,并在开发特性时设置和维护分支。在开始本练习之前,将您在第六章中完成的提醒项目重命名为 RemindersChapter6,因为您将很快重新创建该文件夹。在 windows 中,您可以右键单击资源管理器中的文件夹,然后选择重命名。在 Linux 或 Mac 上运行以下命令:mv∽/Android book/Reminders∽/Android book/Reminders chapter 6。

分叉和克隆

派生远程存储库包括在单个 web 托管服务上从一个远程帐户/分区克隆到另一个远程帐户/分区。Fork 不是 Git 命令;这是一个网站托管服务的操作,如 Bitbucket 或 GitHub。据我们所知,两个更受欢迎的网络托管服务,Bitbucket 和 GitHub,不允许它们的服务器之间有分叉。派生一个项目是将一个项目从它原来的远程存储库复制到您自己的远程 Git 存储库的过程,目的是为了改变它或者制作衍生作品。

从历史上看,分叉具有某种负面的含义,因为它通常是不同的最终目标或项目成员之间的分歧的结果。这些差异经常导致来自多个团队的看似相同的软件的替代版本,并且没有用户社区可以依赖的明确的官方版本。然而,现在由于 Git 的出现,分叉得到了强烈的鼓励。分叉现在是协作的自然组成部分。许多开源项目使用 forks 作为改进整个源代码库的手段。成员鼓励其他人派生并改进代码。这些改进通过拉请求的方式被拉回到原始项目中,或者通过个人的请求将 bug 修复或特性拉回到主线中。因为 Git 的合并和分支非常灵活,所以您可以将任何东西放入您的存储库,从单个提交到整个分支。

本章并没有涵盖拉请求和开源协作的全部,但是涵盖了推动这种强大协作形式的特性。登录您的 Bitbucket 帐户,在 Bitbucket 上查找案例研究。如果你还没有一个比特币账户,请在浏览器中导航到 Bitbucket 并注册。注册大约需要 30 秒。登录 Bitbucket 后,您可以使用 Bitbucket web 界面右上角的搜索框找到提醒存储库。在搜索框中,键入 csgerber/reminders。同样,不要将其与您之前作为参考克隆的 reminders-git 存储库混淆。要分叉这个项目,点击左边的分叉按钮,如图 7-6 所示。当随后的窗口提示时,接受默认设置并点击如图 7-7 所示的 Fork repository 按钮。

A978-1-4302-6602-0_7_Fig7_HTML.jpg

图 7-7。

Click the Fork repository button

A978-1-4302-6602-0_7_Fig6_HTML.jpg

图 7-6。

Click Fork in the Reminders repository left margin controls

现在,我们将克隆您刚刚分叉的存储库。在 Git 中,克隆是从另一个位置复制整个 Git 项目的过程,通常是从远程复制到本地。找到项目的分支并复制 URL。您可以通过在 Bitbucket web 界面的搜索框中键入 your-bit bucket-username/reminders 来完成此操作。在搜索框的正下方,沿着 Bitbucket web 界面的右上方,您会发现克隆框,其中会有一个类似于git@bitbucket.org:csgerber/reminders.githttps://your-bitbucket-username@bitbucket.org/your-bitbucket-username/reminders.git的 URL。如果你没有 http URL,那么点击 URL 旁边的按钮,它应该被标记为 SSH,如图 7-8 所示。这将显示一个下拉菜单,允许您选择一个 http URL。从版本控制> Git 导航到 VCS > Checkout。图 7-9 所示的对话框打开,提示您输入 VCS 存储库 URL、父目录和目录名。VCS 存储库 URL 是前面克隆框中的 URL,父目录和目录名的组合是您希望在本地计算机上放置副本的位置。默认情况下,目录名称中的项目名称是小写的。我们建议您用大写字母命名您的项目,因此请根据图 7-9 进行更改。

A978-1-4302-6602-0_7_Fig9_HTML.jpg

图 7-9。

Cloning the repository with the Git GUI

A978-1-4302-6602-0_7_Fig8_HTML.jpg

图 7-8。

The Bitbucket Share URL

点击克隆,源代码将被复制到本地。

使用 Git 日志

Git 日志是一个强大的特性,它让您能够探索项目的提交历史。通过单击工具按钮或按 Alt+9 | Cmd+9 打开“更改”工具窗口,然后选择“日志”选项卡以显示日志。图 7-10 说明了在第六章结束时通过最终提交的提醒项目的历史。这个视图显示了与存储库中各个分支相关联的时间线。

A978-1-4302-6602-0_7_Fig10_HTML.jpg

图 7-10。

Exploring the Git log

单击时间轴中的单个条目会在右侧显示更改列表中的文件;这些是在提交过程中更改的文件。单击来自任何特定提交的文件,然后按 Ctrl+D | Cmd+D(或者简单地双击它们)来获得可视文本 diff,这是一个并排的比较,突出显示对文件的更改。您可以使用更改列表上方的工具栏按钮来编辑源文件,打开文件的存储库版本,或者恢复选定的更改。您还可以使用日志下面的窗口来查看提交作者、日期、时间和哈希代码 ID。这些散列码是唯一的 id,在使用 Git 的一些更高级的特性时,可以用来识别单个提交。

分支

到目前为止,您已经在一个名为master的分支上进行了所有的提交,这是默认的分支名称。但是,你不需要停留在master上。Git 允许您创建任意多的分支,分支在 Git 中有多种用途。这里有一个可能的场景。假设您正在与一个开发团队一起工作,并且在一个开发周期中,您每个人都被分配了特定的任务。这些任务中有些是特性,有些是错误修复。处理这项工作的一个合乎逻辑的方法是将每个任务变成一个分支。开发人员都同意,当一个任务完成并测试后,开发人员会将该任务分支合并成一个名为dev的分支,然后删除该任务分支。在开发周期结束时,dev分支由 QA 团队测试,QA 团队要么拒绝变更并将项目踢回给开发团队,要么签署周期并将dev合并到master。这个过程被称为 Git 流,这是用 Git 在团队中开发软件的推荐方式。你可以在这里阅读更多关于 Git 流的内容:

https://guides.github.com/introduction/flow/index.html

Git Flow 非常适合大型团队,但是如果你是单独开发或者只和一两个其他开发人员一起工作,你可能需要同意一个不同的工作流程。无论您的工作流是什么样的,Git 中的分支功能都是灵活的,它将允许您调整您的工作流以适应 Git。在本节中,我们将假设您正在进行一个团队项目,并被分配了在提醒应用中添加一个功能的任务,该功能允许用户在一天中的特定时间安排提醒。

在树枝上生长

选择“文件”“➤导入项目”,打开之前克隆的 Reminders-Git 项目。右键单击项目视图中的 Reminders-Git 根文件夹,并选择 Git ➤存储库➤分支,以打开分支提示窗口。这个提示允许您浏览所有可用的分支。从提示中单击新建分支。将您的分支命名为 ScheduledReminders,如图 7-11 所示。

A978-1-4302-6602-0_7_Fig11_HTML.jpg

图 7-11。

Creating a new branch with Git

新的分支将被创建并签出,供您使用。打开 Changes 视图并点击绿色加号按钮来创建一个新的 changelist。将其命名为 ScheduledReminders,就像您的新分支一样,因为下一轮的更改将引入安排提醒的功能。确保选中激活该变更列表复选框,如图 7-12 所示。

A978-1-4302-6602-0_7_Fig12_HTML.jpg

图 7-12。

Creating a new changelist for the branch work

要开始您的新功能,您需要向对话框添加一个新选项,当单击提醒时会显示该选项。打开RemindersActivity.java并转到附加到 mListViewvariable 的第一个OnItemClickListener嵌套类中的onItemClick()方法的顶部。在String数组中添加日程提醒作为第三个条目,构建可点击选项,如图 7-13 的第 92 行所示。接下来,您需要允许用户设置当您的新选项被点击时的提醒时间。找到第二个嵌套的OnItemClickListener,它附加到modeListView上,当单个提醒被点击时,它会创建一个对话框。这将发生在 dialog.show()方法调用之后。查看其onItemClick()方法内部,如第 101 行所示,并进行如图 7-13 所示的更改。您需要解决日期类的导入问题。

A978-1-4302-6602-0_7_Fig13_HTML.jpg

图 7-13。

Changes for scheduled reminders

这里,您将删除提醒的else块更改为else if块,它检查索引 1 处的位置。您添加了一个在第三个新选项被点击时运行的else块。这个块创建一个新的代表今天的Date,并用它来构建一个TimePickerDialog。立即运行应用,测试新选项。图 7-14 显示了新功能的运行情况。

A978-1-4302-6602-0_7_Fig14_HTML.jpg

图 7-14。

Trying the Schedule Reminder option

现在,新功能的一部分已经开始工作,按 Ctrl+K | Cmd+K 确认消息添加新的计划时间选取器选项。回到 IDE,将找到提醒的两行移到position==0条件之外。将reminder变量标记为final。示例见图 7-15 。

A978-1-4302-6602-0_7_Fig15_HTML.jpg

图 7-15。

Move the reminder variable outside the if block

接下来转到您刚刚添加的else块,在那里您构造并显示时间选择器对话框。在显示与图 7-13 中第 113 行对应的对话框的行前添加以下代码:

final Date today = new Date();

TimePickerDialog.OnTimeSetListener listener = new TimePickerDialog.OnTimeSetListener() {

@Override

public void onTimeSet(TimePicker timePicker, int hour, int minute) {

Date alarm = new Date(today.getYear(), today.getMonth(), today.getDate(), hour, minute);

scheduleReminder(alarm.getTime(), reminder.getContent());

}

};

这将为时间选取器对话框创建一个侦听器。在这个监听器中,您使用今天的日期作为闹钟的基准时间。然后包括从对话框中选择的小时和分钟,为您的提醒创建闹钟日期变量。你在一个新的scheduleReminder()方法中使用了闹钟时间和提醒的内容。Android Studio 会将 TimePicker 标记为未解析的类,并将 scheduleReminder()方法标记为未解析的方法。按 Alt+Enter 解决 TimePicker 类的导入问题。再次按 F2 和 Alt+Enter 打开智能感知对话框,然后按 Enter 让 Android Studio 为您生成方法,如图 7-16 所示。

A978-1-4302-6602-0_7_Fig16_HTML.jpg

图 7-16。

Generate method using IntelliSense

选择RemindersActivity类,如图 7-17 所示。

A978-1-4302-6602-0_7_Fig17_HTML.jpg

图 7-17。

Selecting the RemindersActivity as the target class

将以下代码添加到新的方法体中:

AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);

Intent alarmIntent = new Intent(this, ReminderAlarmReceiver.class);

alarmIntent.putExtra(ReminderAlarmReceiver.REMINDER_TEXT, content);

PendingIntent broadcast = PendingIntent.getBroadcast(this, 0, alarmIntent, 0);

alarmManager.set(AlarmManager.RTC_WAKEUP, time, broadcast);

同样,Android Studio 会为代码中缺失的导入标记一系列错误。按 F2,然后按 Alt+Enter 打开快速修复提示并修复每个错误。快速修复选项最终会提示您ReminderAlarmReceiver不存在。按 Alt+Enter 并选择第一个选项来生成类。在第一个弹出对话框中按 Enter 键使用建议的包,然后在第二个弹出对话框中再次按 Enter 键将这个新的类文件添加到 Git。让类扩展BroadcastReceiver并实现onReceive()方法。您的ReminderReceiver.java文件应该如下所示:

package com.apress.gerber.reminders;

import android.content.BroadcastReceiver;

import android.content.Context;

import android.content.Intent;

public class ReminderAlarmReceiver extends BroadcastReceiver {

@Override

public void onReceive(Context context, Intent intent) {

}

}

Tip

重复按 F2(下一个突出显示的错误)和 Alt+Enter(快速修复)来让 Android Studio 修复许多从清单中复制代码时出现的错误,如图 7-16 。它将添加缺失的导入,并为未定义的方法、常量和类生成代码。

返回到RemindersActivity.java文件。通过按 F2 然后 Alt+Enter 找到并修复最后一个错误,并选择第二个建议来编码生成一个String常量,如图 7-18 所示。将此文本的值设置为“REMINDER_TEXT”。

A978-1-4302-6602-0_7_Fig18_HTML.jpg

图 7-18。

Generate a Constant field .

最后,打开你的 AndroidManifest.xml 文件,添加一个 receiver 标签来定义新的BroadcastReceiver,如图 7-19 所示。

A978-1-4302-6602-0_7_Fig19_HTML.jpg

图 7-19。

BroadcastReceiver manifest entry

运行应用进行测试。您应该能够点击一个提醒,选择日程提醒,并设置它的触发时间。选择时间不会有任何作用,因为我们还没有涉及到BroadcastReceivers的细节。现在按 Ctrl+K | Cmd+K 调用提交更改对话框。请花些时间在“提交更改”对话框中确认您到目前为止所做的更改。请注意,该对话框保留了您之前提交的消息,您应该更新该消息,如图 7-20 所示。

A978-1-4302-6602-0_7_Fig20_HTML.jpg

图 7-20。

Git’s Commit Changes dialog box

选择RemindersActivity后,点击显示差异按钮(如图 7-20 所示)以显示所有变更的并排差异。点按左上角的向上和向下箭头,或者按下 F7 键,在文件的早期和晚期差异之间移动。这些控制出现在图 7-21 中。使用向下箭头移动到您的onItemClickListener中有趣的变化。

A978-1-4302-6602-0_7_Fig21_HTML.jpg

图 7-21。

Visual text diff view

到目前为止,您已经设法包含了一个当前没有使用的OnTimeSetListener。(listener变量的浅灰色表示它不在代码中使用。)当您在这个视图中移动代码时,不仅会提醒您已经做出的更改,还会提醒您可能错过的更改,这给了您在提交之前修复问题的另一个机会。diff 视图也是一个具有一些语法感知特性的编辑器。如果您选择进行小的调整,您可以利用诸如自动完成之类的东西。

按 Escape 键关闭 diff 视图,并在提交更改之前更改提交消息。点击提交,让 Android Studio 有机会执行代码分析。您将看到另一个对话框,告诉您某些文件包含问题。对话框将提示代码中有警告。此时,您可以单击 Review 按钮取消提交,并生成所有潜在问题的列表。尽管忽略警告不是好的做法,但是您可以有意地暂时不理会这些警告,继续下一步。

Git 提交和分支

分支上提交的 Git 风格是相似的,但是如果您来自使用 Perforce 或 Subversion 等工具的传统 VCS 背景,可能会感觉与您所习惯的有些不同。您将希望理解 Git 如何管理提交和分支的细微差别。这些差异可能会让新手感到困惑,但它们是 Git 强大和灵活的核心。

Git 中的提交被视为项目历史中的一级实体,可以通过特殊的提交哈希代码来识别。虽然您不需要了解 Git 如何实现单个提交和版本控制的细节,但是将提交视为存在于代表存储库整个状态的历史时间线中的对象或实体是很重要的。提交作为一个原子工作单元存在,它发生在 Git 历史中的某一时刻,用描述工作的提交消息进行注释。每个提交都有一个父提交,它前面有一个或多个提交。您可以将分支视为历史中指向单个提交的标签。当您创建一个分支时,会在历史记录中的该点创建一个标签,当您对该分支进行提交时,标签会跟随提交的历史记录。下图从图 7-22 开始,展示了 Git 当前看到的 Reminders 项目历史。

A978-1-4302-6602-0_7_Fig22_HTML.gif

图 7-22。

Git history showing ScheduledReminders branch Note

Android Studio 提交日志是自下而上的,而我们的图是自上而下的。

主分支由指向来自克隆项目的最后一个 commit A 的灰色箭头表示。(与 Git 日志视图相比,您将会注意到还有其他提交在进行,但是为了简洁起见,我们省略了它们。)ScheduledReminders 分支是绿色箭头,指向实现新特性的一系列提交 B 和 C 中的最新一个。为了简单起见,我们使用单个字母作为标签,但是 Git 使用提交哈希代码,其中包括更长的十六进制名称,例如c04ee425eb5068e95c1e5758f6b36c6bb96f6938。您可以通过仅使用其散列的前几个字符来引用特定的提交,只要它们是唯一的或者不与任何其他散列的前几个字母相似。

Revert 在哪里?

当人们第一次尝试 Git 时,最大的障碍之一就是适应 Git reverts,因为他们不能像其他 VCS 客户那样工作。Git revert 是一个撤销早期提交的提交(工作单元)。理解的最好方法是看它的实际操作。让我们做一个修改,修复您在RemindersActivity.java中的反对警告。引入Calendar物体,移除Date物体,如图 7-23 所示。

A978-1-4302-6602-0_7_Fig23_HTML.jpg

图 7-23。

Fix the deprecation warnings

构建并运行代码以验证它是否有效,然后提交此更改并显示消息 Fixes deprecation warnings。注意,对于未使用的变量,仍然会有一个警告,这将在后面的“重定基时解决冲突”一节中解决。Android Studio 中的 revert 命令与 Git revert 命令有很大不同。在这里,您将使用命令行 git revert 命令来理解其中的区别。找到“修复不推荐使用的警告”提交在“更改工具”窗口的 Git 历史记录中,右键单击它并选择“复制哈希”,将提交哈希代码复制到您的系统剪贴板。现在,通过单击底部空白处的终端窗口按钮打开终端,输入 git revert 并粘贴提交散列作为命令的最后一部分。你的命令应该如图 7-24 所示。按 enter 键,Git 将在您的终端中启动一个提交消息编辑会话,如图 7-25 所示。键入“:q”退出编辑会话,该会话保存默认提交消息并执行提交。

A978-1-4302-6602-0_7_Fig25_HTML.jpg

图 7-25。

Commit message edit. Exit by typing :q.

A978-1-4302-6602-0_7_Fig24_HTML.jpg

图 7-24。

Issuing the git revert command from the terminal

git 恢复会导致执行新的提交,从而取消之前的提交。切换回 Android Studio,看看有什么变化。所有的弃用警告都随着展开的更改返回。您的 Git 历史将反映提交。Git 应用前一次提交的所有更改的反转,并立即执行一次带有这些更改的提交,并显示一条与前一次提交相同的带有 Revert 前缀的消息。相比之下,其他工具会跟踪您对文件的本地修改,并允许您在提交之前撤消修改。尽管这种撤销更改的新风格有所不同,但 Android Studio 为您提供了一个与经典的、更熟悉的版本控制工具一致的恢复界面。在撰写本文时,还没有 IDE 命令或菜单动作触发 Git revert 的等效操作。但是,一个内置选项允许您在本地应用来自本地历史、Git 历史甚至补丁文件的反向更改。Git revert 自动化了应用反向更改和执行提交这两个步骤。图 7-26 展示了 Git 的历史,提交 D 引入了修改弃用的变更,提交 D 代表了恢复弃用的对Date对象的调用的撤销变更。

A978-1-4302-6602-0_7_Fig26_HTML.gif

图 7-26。

Git history after revert

撤销已提交更改的另一种方法是使用 reset 命令,它的工作方式类似于 revert,但有一个细微的区别。将图 7-23 中的更改添加回源代码,并再次提交。你的 Git 历史将在-D 后面有一个额外的 E 提交,如图 7-27 所示。这次选择风投➤ Git ➤重置头。在弹出的对话框中输入 HEAD 1,如图 7-28 所示,点击复位。

A978-1-4302-6602-0_7_Fig28_HTML.jpg

图 7-28。

The Git Reset Head dialog box

A978-1-4302-6602-0_7_Fig27_HTML.gif

图 7-27。

Git history after reapplying the deprecation fix

Git 会在您最后一次提交之前将您的存储库与提交同步,这相当于对该提交的撤销——使您的历史看起来如图 7-26 所示。Android Studio 通过使用当前的更改列表重新应用您的更改来增强 Git 重置。这为您提供了第二次机会,以便在意外执行重置时收回提交。在大多数情况下,您会希望在重置后完全放弃更改。单击更改工具窗口中的恢复更改按钮以完全放弃更改。在图 7-29 中,恢复更改按钮被圈起来。

A978-1-4302-6602-0_7_Fig29_HTML.jpg

图 7-29。

Click the revert changes button

让我们更进一步地重置,以删除您在不赞成使用的方法调用上的所有工作痕迹。选择 VCS ➤ Git ➤重置头。然后在弹出的对话框中输入 HEAD 2,如图 7-28 所示,点击 Reset。记得之后点按“还原更改”按钮。这将成为你每次在 Android Studio 中使用 Git Reset 的习惯。你的历史将会反映出图 7-22 的历史。

Revert Vs. Reset

还原和重置之间的区别是微妙但重要的。revert 添加了一个提交,该提交反转了上次提交的更改,而 reset 取消了一个提交。重置本质上是通过给定数量的提交来备份您的分支标签。如果您不小心提交了某个东西,您通常会想要撤销或删除某个提交。在这种情况下使用重置是合理的,因为这是最简单的选择,并且不会增加您的历史记录。但是,在某些情况下,您可能希望您的历史记录能够反映撤销提交的工作—例如,如果您从项目中提取一个特性,并且希望向用户社区记录该特性的删除。revert 的另一个重要用途是远程存储库,我们将在本章后面讨论。因为您只能向远程存储库添加提交,所以删除或撤销远程存储库上的提交的唯一方法是使用 revert,它将反转的更改作为提交追加。

合并

合并是将两个独立分支的工作结合起来的一种方式。历史上,由于分支之间的冲突,合并需要额外的努力。由于 Git 实现了更改,合并变得不那么痛苦了。

您将从在主分支上为极端拖延者添加一个新特性开始。这个新功能会将所有提醒的默认值设置为重要,因为我们知道你会忽略除了最重要的提醒之外的任何事情。单击文件➤ VCS ➤ Git ➤分支机构调出分支机构列表。选择主分支,然后选择签出。请注意,基础源代码已经更改,支持新功能的所有更改都已删除,并且您的项目已恢复到您开始处理计划提醒之前的状态。创建一个名为的新变更列表,并将其设置为活动。出现提示时,删除空的 ScheduledReminders 更改列表。图 7-30 和 7-31 演示了该流程。

A978-1-4302-6602-0_7_Fig31_HTML.jpg

图 7-31。

A confirmation dialog box appears when deleting the old changelist

A978-1-4302-6602-0_7_Fig30_HTML.jpg

图 7-30。

New changelist dialog box

查看fireCustomDialog()方法,找到从对话框布局中检索复选框的行。新增一行调用checkBox.setChecked(true),将设置新的默认值,如图 7-32 第 200 行所示。

A978-1-4302-6602-0_7_Fig32_HTML.jpg

图 7-32。

Set the check box default to checked

构建并运行应用以测试新功能,然后使用 Ctrl+K | Cmd+K 提交。Git 将看到图 7-33 中记录的历史,它代表您在分支的初始克隆之后的最新提交。

A978-1-4302-6602-0_7_Fig33_HTML.gif

图 7-33。

Commit history after adding a feature to the master branch

在这里,你把你的头转向主,并作出了 D 的承诺。此最新提交遵循与 ScheduledReminders 功能的提交不同的历史路径,因为此提交不在同一分支上。

Note

如果您在 Git 日志视图中跟踪历史,您会注意到有另一个 origin/master 分支指向我们没有显示的提交。这是稍后讨论的远程分支。

您已经在 master 分支上做了一些工作,并提交了一些内容,以便在 ScheduledReminders 分支上添加一个新特性,所以现在您将把这些更改一起放到主线或 master 分支中,以便其他人可以看到它们。再次单击文件➤ VCS ➤ Git ➤分支机构,调出分支机构列表。选择 ScheduledReminders 分支,然后单击“合并”。该分支的所有更改和历史记录都将合并到主分支中。构建并运行应用来测试这两个特性。从“选项”菜单中单击“新建提醒”将打开一个选中“重要提示”复选框的“新建提醒”对话框,而单击列表中的任何提醒将提供在特定时间安排提醒的选项。图 7-34 展示了 Git 是如何管理你的变更的。

A978-1-4302-6602-0_7_Fig34_HTML.gif

图 7-34。

Commit history after merging the ScheduledReminders feature

自动执行新的 E 提交,包括来自 C 和 D (E 的父代)的更改。还要注意,HEAD 指向包含最新提交的主分支的头。

Git 重置更改历史

如果你想把你的重要提醒功能作为一个分支呢?您从未为此功能创建分支。相反,你就在主分支的顶端发展。您可以强制您的主分支进行备份,并指向您的 D commit,所以我们现在就开始吧。单击文件➤ VCS ➤ Git,然后单击重置头。“提交”字段将被设置为“标题”。设置为 HEAD∾1 点击重置按钮如图 7-35 再次重置你的主分支,更像是一个标签。记得恢复从 Git 重置保存的更改。然后它将指向先前的提交。Git 现在将会看到如图 7-33 中之前的图表所示的存储库。

A978-1-4302-6602-0_7_Fig35_HTML.jpg

图 7-35。

Git reset dialog box

由于最后一次提交包含了合并的更改,重置使得合并从未发生,您现在位于提交之上,这引入了 ImportantReminders 特性。这让您可以自由地改变历史,使它看起来好像这个新特性是在一个分支上开发的。单击文件➤ VCS ➤ Git,然后单击分支打开分支对话框。单击新建分支。将该分支命名为 ImportantReminders,然后单击 OK 创建它。你现在有了图 7-36 中描述的历史。

A978-1-4302-6602-0_7_Fig36_HTML.gif

图 7-36。

Git history showing the new branch

master 和 ImportantReminders 分支都指向同一个提交。使用“分支”对话框检查主分支,可以通过单击状态栏右上角的“分支”部分或选择“文件”“➤VCS”“➤git”“➤分支”来调用该对话框。再次重置该分支,使其指向最初从 Bitbucket 克隆项目的位置,然后签出 ImportantReminders 分支。历史现在反映了仍在开发中的两个实验性特性分支,而工作副本(您在 IDE 中看到的)反映了项目在您第一次克隆它时的存在。你可以在图 7-37 中看到这一点。

A978-1-4302-6602-0_7_Fig37_HTML.gif

图 7-37。

Git history after resetting master to the beginning

现在,您想要进一步更改历史并重新排序您的特性提交,以便它们看起来像是连续开发的,并且在开发期间没有使用分支。在此之前,签出 master 分支,并将其与 ImportantReminders 分支合并。合并将导致一个特殊的快进操作:Git 只是在历史中将主分支向前移动到由 ImportantReminders 分支共享的同一个提交。这与前面的合并分支示例不同,因为一个分支是另一个分支的后代。如果您足够仔细地观察,您会注意到创建一个将来自 ImportantReminders 分支的更改合并到主服务器上的提交与这个分支已经指向的 D 提交是相同的。因此,Git 优化了操作,只是将主分支向前移动,这将您带回到类似于图 7-36 所示的历史。不同之处在于,您签出的是 master 而不是 ImportantReminders 分支。

现在你会让你的历史更有趣。您将在应用中添加一个“关于”对话框,以便您的用户了解更多关于开发者的信息。“关于”对话框也是放置所用技术和艺术作品属性的好地方。你的会相对简单。如果尚未删除 ImportantReminders 更改列表,请将其删除,并使用标题为 AboutScreen 的新更改列表。在appsrcmainreslayout下新建一个名为 dialog_about.xml 的资源 XML 文件,并用清单 7-1 中的代码填充。

Listing 7-1. dialog_about.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:layout_width="match_parent" android:layout_height="match_parent">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="Reminders!"

android:id="@+id/textView2"

android:layout_alignParentTop="true"

android:layout_centerHorizontal="true" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceMedium"

android:text="Version 1.0\nAll rights reserved\nDeveloped by yours truly!"

android:id="@+id/textView3"

android:layout_marginTop="34dp"

android:layout_below="@+id/imageView"

android:layout_centerHorizontal="true"

android:gravity="center" />

<ImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/imageView"

android:layout_below="@+id/textView2"

android:layout_centerHorizontal="true"

android:src="@drawable/ic_launcher" />

</RelativeLayout>

此布局定义了一个“关于”对话框,其中包含标题的文本视图、正文的文本视图,并在两者之间放置提醒启动图标。您需要一个新的菜单项来触发对话框。打开menu_reminders.xml并在第一个和第二个项目标签之间添加以下 XML 片段:

<item android:id="@+id/action_about"

android:title="About"

android:orderInCategory="200"

app:showAsAction="never" />

将退出菜单项的orderInCategory从 200 更改为 300,以便它可以在新的“关于”项之后排序。

现在打开RemindersActivity.java,为调用新fireAboutDialog方法的新菜单项添加一个用例,如图 7-38 所示。

A978-1-4302-6602-0_7_Fig38_HTML.jpg

图 7-38。

Add an About screen

方法使用你的新布局构建一个对话框并显示它。构建并运行新特性来测试它。最后,按 Ctrl+K | Cmd+K 并提交消息“添加一个关于屏幕”。Git 历史现在在重要的提醒特性之后多了一个 commit,它现在由一个分支指向。图 7-39 中您最新的 E commit 包含了关于对话框功能。

A978-1-4302-6602-0_7_Fig39_HTML.gif

图 7-39。

Git history after adding the About screen

吉特·福克斯

重设基础是一种基于另一个分支或一系列提交创建分支的方法。它类似于合并,因为它合并了分支之间的更改,但它以一种没有多个父级的方式创建提交历史。最好用现在的历史作为例子。单击文件➤ VCS ➤ Git ➤重设基础以打开重设基础分支对话框。告诉该对话框,您想通过从“到”下拉菜单中选择主分支来将主分支重置到 ScheduledReminders 分支,如图 7-40 所示。保持选择交互式选项,这样您可以更好地控制组合内容。

A978-1-4302-6602-0_7_Fig40_HTML.jpg

图 7-40。

The Git Rebase branch dialog box

这将带您进入交互式重设基础模式,显示图 7-41 中的对话框。交互式重定基础是 Git 更强大的特性之一。在这里,您可以删除和更改提交历史中的单个提交。“重设提交基准”对话框列出了所选分支历史记录中发生的所有提交,直到您“基于”的分支的第一个共同祖先。首先要注意的是每个提交的 Action 列下的选项。该对话框提供了拾取、编辑、跳过或挤压选项。然而,Android Studio 默认选择每个提交。

A978-1-4302-6602-0_7_Fig41_HTML.jpg

图 7-41。

The Git Rebase commits dialog box

假设您不再需要这个分支的 ImportantReminders 特性,但是您仍然对 About 屏幕感兴趣。选择 Skip 操作从列表中删除这个提交,当您完成 rebase 和合并分支时,这些更改都不会出现。单击开始重置基础选项以完成操作。你的 Git 历史现在看起来如图 7-42 所示。

A978-1-4302-6602-0_7_Fig42_HTML.gif

图 7-42。

After rebasing and skipping the ImportantReminders branch

分离头

让我们假设当您最初克隆这个项目时,有另一个开发人员正在开发一个报警功能。再进一步说,你想最终融合在这个作品里。要模拟这种情况,您需要在历史中回到 A commit 并启动新特性。到目前为止,您一直在针对一个特定的分支进行工作和提交。这可以是自定义命名的分支,也可以是初始导入时创建的主分支。

我们现在将演示 Git 中的另一种工作方式,即分离头模式。如果您签出一个特定的提交而不是一个分支,那么这个头将从您正在处理的分支中分离出来并被暴露。首先,您需要检查 ImportantReminders 分支的父提交。为此,打开分支对话框,点击签出标签或修订,如图 7-43 所示。

A978-1-4302-6602-0_7_Fig43a_HTML.jpg A978-1-4302-6602-0_7_Fig43b_HTML.jpg

图 7-43。

Checking out the change prior to the last change in the ImportantReminders branch

在结帐提示中输入 important reminders 1。您现在将处于分离模式,您的头分支和项目状态将反映您最初克隆项目时所做的最后一次提交,如图 7-44 所示。

A978-1-4302-6602-0_7_Fig44_HTML.gif

图 7-44。

git_diagram8

请注意,Git 现在公开了一个新的 HEAD,它与开发过程中创建的任何分支都是分离的。负责人已经正式跟踪了你检查过的任何一个分支。当您提交时,签出的分支将随着头部移动到最近的提交。您输入的 important reminders 1 文本是您希望从何处开始结帐的相对参考。您可以给大多数需要分支或提交散列的操作一个相对引用。相对引用使用以下两种格式之一:

BranchOrCommitHash^

BranchOrCommitHash∼NumberOfStepsBack

单插入符号形式从左侧指定的分支或提交在历史中后退一步,而波浪号形式在历史中后退的步数等于波浪号右侧给出的步数。

相对引用

相对引用是 Git 表达式,用于引用 Git 历史中的特定点。他们使用一个起点或参考点,以及一个目标,该目标是从参考点开始的步数。虽然引用经常作为 HEAD 给出,但它也可以是分支的名称或特定提交的哈希代码(或缩写的哈希代码)。您可以使用相对引用来完成任务,比如将分支移动到 Git 历史中的任意位置,选择特定的提交,或者将头移动到历史中的特定点。在任何可以给出分支名或提交散列的地方,相对引用都可以作为参数给出。虽然我们已经看到了一些在 IDE 中使用它们的例子,但是它们最好在命令行中与 Git 一起使用。

创建一个新的分支来开始下一个特性,并将其命名为 SetAlarm。为新分支创建一个变更列表,并删除任何旧的空变更列表。在com.apress.gerber.reminders包中添加一个名为 RemindersAlarmReceiver 的新类,并用下面的代码填充它:

public class ReminderAlarmReceiver extends BroadcastReceiver{

public static final String REMINDER_TEXT = "REMINDER TEXT";

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)

@Override

public void onReceive(Context context, Intent intent) {

String reminderText = intent.getStringExtra(REMINDER_TEXT);

Intent intentAction = new Intent(context, RemindersActivity.class);

PendingIntent pi = PendingIntent.getActivity(context, 0, intentAction, 0);

Notification notification = new Notification.Builder(context)

.setSmallIcon(R.drawable.ic_launcher)

.setTicker("Reminder!")

.setWhen(new Date().getTime())

.setContentText(reminderText)

.setContentIntent(pi)

.build();

NotificationManager notificationManager = (NotificationManager)

context.getSystemService(Context.NOTIFICATION_SERVICE);

notificationManager.notify(1, notification);

}

}

这里我们有一个BroadcastReceiver,它期望REMINDER_TEXT作为额外的意图给出。它使用该文本并创建一个动作意图,用它来构建一个通知以发布到通知栏中。接下来,在AndroidManifest.xml中,在activity标签之后,在结束的application标签之前添加以下条目,以定义BroadcastReceiver:

<receiver android:name="com.apress.gerber.reminders.ReminderAlarmReceiver"/>

按 Ctrl+K | Cmd+K 并提交带有消息“添加警报广播接收器”的设置警报更改列表。您的 Git 历史将类似于图 7-45 ,其中第三次提交在您的初始起点 a 处挂起。

A978-1-4302-6602-0_7_Fig45_HTML.jpg

图 7-45。

Git history after committing to the SetAlarm branch

这个特性本身不会做太多事情。它需要与 ScheduledReminders 特性合并,后者存在于它自己的分支上。为了完成您的工作,您需要将这两个特性结合起来,并将它们推送到您的远程 Bitbucket 主机,但是您希望以一种方式完成这项工作,使它看起来像是由一个人或一个团队在主分支上完成的,并清理您的所有其他分支。前面您已经看到了 Git 合并如何创建一个提交,其中包含来自合并所涉及的两个分支的两个父提交。您还了解了 Git rebase 通过单个父提交以线性方式组合了两个分支,这正是您所需要的。打开“分支”对话框并签出主分支。点击文件➤ VCS ➤ Git ➤ Rebase。选择 SetAlarm 作为您所基于的分支,并取消选择 Interactive 复选框,因为您现在想要包含来自主干的所有更改。单击开始重置基础。您应该会看到如图 7-46 所示的错误弹出窗口。

A978-1-4302-6602-0_7_Fig46_HTML.gif

图 7-46。

Rebase conflict pop-up

重置基础时解决冲突

这个弹出窗口不应该让您惊慌,因为它指出 Git 已经发现了一些冲突。Git 用冲突状态标记它不能自动合并的文件。在重定基数可以继续之前,由您来解决这些冲突。传统上,解决冲突是许多合作努力的祸根。当你遇到错误或冲突时,感到不舒服是很自然的,尤其是在合并期间。然而,让自己熟悉不那么愉快的合作之路,让合并和冲突解决成为一种习惯,会增加你协调团队和个人之间变化的能力。此外,Android Studio 使解决这种冲突变得不那么痛苦。记住,作为 ScheduledReminders 特性的一部分,您在主分支中启动了BroadcastReceiver。这种冲突是由包含相似或相同更改的两个分支中的代码引起的。通过查找红色突出显示的文件,在变更视图中找到冲突,如图 7-47 所示。

A978-1-4302-6602-0_7_Fig47_HTML.jpg

图 7-47。

Merge conflicts in the Changes view

右键单击并从上下文菜单中选择 Git ➤解决冲突,如图 7-48 所示。这将启动“合并有冲突的文件”对话框。传统上,解决冲突需要双方提供两种输入来源;您的本地更改或您的更改,以及他们的传入更改或他们的更改。

A978-1-4302-6602-0_7_Fig48_HTML.jpg

图 7-48。

Select the Resolve Conflicts option

图 7-49 所示的 Android Studio 文件合并冲突对话框是一个强大的合并工具,用于执行三向文件合并和解决文本冲突。它从传统的合并场景中借用了你的和他们的术语,引导你完成合并。合并工具将您正在重置的 SetAlarm 分支视为他们的分支,或者接收服务器发生了变化。您重新设置基础的主分支被认为是您的,或者是本地工作副本。“合并有冲突的文件”对话框首先出现一个对话框,允许您接受自己的文件、接受他们的文件或进行合并。“接受您的”选项完全忽略来自您正在重置基础的分支的传入服务器文件更新,而支持来自您正在重置基础的本地工作副本分支的更改,并将文件标记为已解决。“接受他们的”选项将当前分支的本地工作副本完全替换为来自分支的传入服务器文件更新,同时将文件标记为已解析。Merge 选项将带您进入三向合并编辑器,在这里您可以将来自传入服务器和工作副本的单个行更改拉入基本合并副本,同时仅自定义合并您需要的内容。基本合并副本是合并的输出或结果。

A978-1-4302-6602-0_7_Fig49_HTML.jpg

图 7-49。

Merge the ReminderAlarmReceiver

点按“合并”按钮来查看这是如何工作的。图 7-50 所示的合并编辑器打开。

A978-1-4302-6602-0_7_Fig50_HTML.jpg

图 7-50。

The Merge editor

合并编辑器将您的工作副本和传入副本排列在合并结果的两侧,这是屏幕的可编辑部分。它支持语法和导入,这意味着在编辑本地副本时,可以使用自动完成、快速修复和其他键盘快捷键。这为您提供了某些外部 VCS 合并工具所没有的优势。编辑器显示了本地工作副本和传入的更新,后者被标记为来自服务器的更改。这些是您要重置基础的 SetAlarm 分支的更改。沿着边栏,您会看到在更改的行旁边有小的双 v 形和 x。单击任一侧的双 v 形图标将在合并结果中包含该特定更改。单击 X 将忽略该特定更改。这些更改也用颜色编码,红色表示冲突,绿色表示附加行,蓝色表示更改的行。在这种情况下,文件的大部分是相互冲突的。

由于您在左侧的本地副本中只有一个类的存根,所以从右侧传入的更改中接受整个完整的实现更有意义。单击 Cancel 并对询问您是否要退出而不应用更改的提示回答 Yes。在“合并有冲突的文件”对话框中,单击“接受他们的”以接受所有传入的服务器更改。接下来,对话框将排列最主要的文件。如果您单击“合并”,您将看到本地工作副本与传入的服务器副本具有完全相同的修改,因此您可以选择您的或他们的。单击本地工作副本中的双 v 形符号接受您的更改,单击传入副本窗格中的 X 拒绝他们的更改。在弹出的提示中单击 Save and Finish 完成合并。对于 Git,这两个文件都将被标记为冲突已解决。如果您在更改工具窗口中查看,您会在默认的更改列表中看到您合并的文件。Git 在向 ScheduleAlarm 分支重放一系列更改的过程中暂停了,正在等待您继续。

进入主菜单,找到风险投资公司➤➤git continue rebase 选项,如图 7-51 所示。注意,您还可以选择中止重设基础或者在重设基础时跳过此提交。如果您正在进行一个复杂的合并,并意识到有些事情是灾难性的错误,您可以单击 Abort Rebasing 并将所有内容恢复到开始 rebase 之前的状态。如果您不小心包含了一个有几个冲突的提交,您还可以选择跳过。单击“继续重设基础”以完成重设基础。

A978-1-4302-6602-0_7_Fig51_HTML.jpg

图 7-51。

Click the Continue Rebasing menu option

重置将完成,并执行新的提交。Git 历史将反映时间线中 SetAlarm 提交之后主服务器的所有更改的副本。如图 7-52 所示。

A978-1-4302-6602-0_7_Fig52_HTML.gif

图 7-52。

Git history after rebasing and fixing conflicts

主包含提交 B 和 C,支持 ScheduledReminders 提交 E,增加了关于屏幕;并从 SetAlarm 分支提交 F。您还决定不再需要之前的 rebase 中的 ImportantReminders 特性。

设置闹钟和实现实际的BroadcastReceiver的任务是在一个单独的分支上完成的,但现在它看起来像是你的时间线中的一个标记或里程碑。为了完成您的特性,您需要将您的工作从 ScheduleReminders 分支绑定到 SetAlarm 分支中的实际BroadcastReceiver。进行以下更改,将调用BroadcastReceiver的监听器连接到TimePickerDialog。您将在 else 块的末尾、我们用于编辑提醒的对话框之前插入以下代码片段。

new TimePickerDialog(RemindersActivity.this,listener,today.getHours(),today.getMinutes(),false).show();

在设备上运行您的项目,并验证该功能是否有效。现在,当您安排提醒时,您应该会收到设备通知,如图 7-53 所示。

A978-1-4302-6602-0_7_Fig53_HTML.jpg

图 7-53。

Notification from a reminder

现在,您可以将主分支推送到远程 Bitbucket 主机。从“文件”菜单中,选择“VCS ➤ Git ➤推送”。图 7-54 中的对话框打开,您可以将更改从本地主分支推送到 Bitbucket 存储库的远程主分支。单击推送按钮执行推送。

A978-1-4302-6602-0_7_Fig54_HTML.jpg

图 7-54。

Push your changes to Bitbucket

由于您已经完成了 ScheduledReminders 和 ImportantReminders 分支,因此可以删除它们。打开“分支”对话框,依次选择这两个分支;单击删除以删除它们。

Git Remotes

Git 远程只是存储在远程服务器上的 Git 存储库的副本,可以通过网络访问。虽然您可以像使用 Subversion 这样的传统客户端/服务器模型的 VCS 一样使用它们,但是最好将它们视为您的作品的可公开访问的副本。在 Git 工作流中,您没有提交到共享的中央服务器;相反,你通过拉请求来分享你的工作。

pull 请求是来自一个开发人员的请求,从该开发人员概要文件下的公共存储库中获取变更。其他人可以根据自己的判断自由地包含个人提交或您的整个工作。您通常会发现一个主分支,有一个或多个主要开发人员负责用最新的和最有价值的特性和提交来更新该分支。领导通过使用 Git 的整个特性集从不同的贡献者那里引入变更,Git 允许选择、删除、重新排序、合并、压缩和修改单个提交。

然而,拉请求是针对高级 Git 用户的。人们开始使用 Git 最常见的方式是从 Git 托管服务器克隆一个项目——下载 Git 存储库的完整副本以便在本地使用。您继续进行更改并在本地提交它们,然后最终将这些更改推回到远程存储库。您还可以获取和合并其他人上传到远程的更改。

另一种选择是从本地的一个空存储库开始,然后构建一个项目。然后,您将项目推送到一个 Git 托管服务(如 Bitbucket 或 GitHub ),并公布与他人共享,或者您可以将其设为私有,并自行邀请他人。开发像普通方法一样继续,本地提交推送到远程。最终,当您工作时,贡献者会分支并添加到他们的远程项目副本中,您将获取并合并这些变更。

拉模式与推模式

传统的 VCS 系统依赖于一种推送模式,在这种模式下,功能由几个开发人员开发,最终被推送到一个中央服务器。虽然这种模型已经工作了多年,但是它受到主分支的单个副本被破坏的限制,因为贡献者试图通过使用 diffs 和补丁文件来合并他们的更改。补丁文件是更改源文件所采取的单个操作的文本表示;例如,指示添加这些行、移除这些行或改变这些行。大多数遵循这种模型的 VCS 系统随着时间的推移,通过应用一系列差异来管理变更。

Git 遵循分布式拉模型,将项目视为共享实体。因为 Git 允许主分支的分布式副本,所以任何人都可以在任何时候提交和更新本地副本,这降低了贡献者之间合并工作的复杂性。Git 还提升了单个提交的重要性,将它们视为存储库随时间变化的快照。这使得该工具更擅长管理变更。它还增加了分别管理对单个源文件的多个更改的灵活性。合并更加精确和易于管理,合并工作的复杂性大大降低。例如,一个项目负责人可以将您在多个分支之间通过大约 10 次提交实现的功能提取出来,将它们全部压缩到一个分支中,修改消息,并在主分支中的其他提交之前将其组织到该负责人的个人历史中,最后在与项目相关联的远程设备上推送并公布它。

摘要

这涵盖了在 Android Studio 中使用 Git 的基础知识。在本章中,你已经看到了如何安装 Git 并使用它来跟踪变化。我们演示了如何将源代码添加到 Git 中,并使用 Git 日志特性来查看提交历史的摘要。您已经看到了分支如何像指向单个提交的标签一样工作的深入示例。可以通过使用相对引用在提交之间移动分支,甚至完全删除分支。我们已经展示了 Git history 如何修改并行提交的更改,并按顺序排列它们。我们展示了几个涉及多个分支同时成熟的协作场景。

八、设计布局

充分利用你的应用通常意味着给它合适的视觉吸引力来取悦你的目标受众。虽然 Android 使得启动和运行各种模板项目变得很简单,但有时您可能需要对应用的外观和感觉有更多的控制。也许你想调整一个单选按钮在另一个控件旁边的位置,或者你需要创建你自己的自定义控件。本章介绍了设计布局和组织控件的基础知识,以便它们能在各种 Android 设备上正确显示。

Android 布局设计基于三个核心 Android 类,ViewsViewGroupsActivities。当画屏幕时,这些是你的基本构件。虽然用户界面包有更多的类,但大多数都是这些核心类的子类,利用这些核心类,或者是这些核心类的组件。另一个重要的组件 fragment 是在 Android 3.0 蜂巢(API 11)中引入的。片段解决了设计用户界面的模块化部分的关键需求,允许跨多种形式的重用,特别是平板电脑。本章从核心用户界面类开始,然后在后面的章节中继续讨论。

活动

Android activity 代表一个用户可以与之交互的屏幕。Activity类本身不画任何东西;相反,它是根容器,负责编排绘制的每个组件。任何被绘制到屏幕上的组件都存在于活动的边界内。Activity类也用于响应用户输入。当用户在屏幕之间导航时,一个活动可以转换到另一个活动。活动有一个众所周知的生命周期,详见表 8-1 。我们将在本章后面提到活动生命周期。

表 8-1。

Activity Life-Cycle Methods

| 方法 | 描述 | 之后杀死 | 然后 | | --- | --- | --- | --- | | `onCreate()` | 这在最初创建活动时被调用。它负责构造视图、将数据绑定到控件,以及管理或恢复给定包的状态。 | 不 | `onStart()` | | `onRestart()` | 该方法在活动停止后、再次启动前调用。这种情况发生在一些情况下,比如在打完电话后继续或者将应用带回前台。 | 不 | `onStart()` | | `onStart()` | 在屏幕上显示活动之前,立即调用此方法。如果活动被置于前台,则随后调用`onResume()`,如果活动被隐藏,则调用`onStop()`。 | 不 | `onResume()`或`onStop()` | | `onResume()` | 当活动被创建、启动并准备好接收用户输入时,触发`onResume()`方法。该活动将在此方法完成后运行。 | 不 | `onPause()` | | `onPause()` | 每当系统准备好恢复活动时,就会触发此方法。当系统准备转换到另一个活动时,可以在当前活动正在执行时调用它,或者当当前活动被中断并发送到后台时调用它。 | 是 | `onStop()`或`onResume()` | | `onStop()` | 当活动不可见时,调用此方法。 | 是 | `onRestart()``or` | | `onDestroy()` | 活动就在被销毁之前得到这个调用。这通常是从活动内部显式调用`finish()`的结果,或者是因为`WatchDog`需要终止活动来回收内存或者因为它变得没有响应。这是活动将收到的最后一个呼叫。 | 是 | 不适用的 |

视图和视图组

尽管活动是根组件,但它通常包含几个ViewViewGroup对象的集合。视图是屏幕上所有可见元素的超类,包括view-group。这些元素包括按钮、文本字段、文本输入控件、复选框等等。一个视图通常包含在一个或多个视图组中。视图组代表一个或多个视图对象的集合。一个视图组可以嵌套在 n 层的其他视图组中,以创建复杂的布局。视图组的主要职责是控制一个或多个嵌套的ViewViewGroup对象的布局。各种类型的专用视图组控制它们的子组件如何定位。这些是布局容器对象。每个布局对象的行为不同,并使用唯一的位置属性。LinearLayoutRelativeLayoutFrameLayoutTableLayoutGridLayout为核心布局容器。

为了更好地理解各个布局是如何工作的,让我们来看几个例子。使用新建项目向导启动一个名为 SimpleLayouts 的新项目。选择至少符合 API 14 (IceCreamSandwich)标准的手机和平板电脑外形,并使用空白活动模板。保留默认的活动名称MainActivity和布局名称字段的名称activity_main.xml,然后继续创建项目。你应该进入主活动布局的编辑模式,如图 8-1 所示。

A978-1-4302-6602-0_8_Fig1_HTML.jpg

图 8-1。

Starting with the main activity’s layout

预览窗格

对于新项目,您将在文本编辑模式下开始主活动的布局 XML。如果您的项目不在此模式下,请按 Ctrl+Shift+N | Cmd+Shift+O 打开文件搜索对话框,并键入名称activity_main以找到您的主布局。Android Studio 支持文本和设计两种模式来设计布局,你应该熟悉这两种模式。可以使用编辑器窗口左下角的选项卡来切换这些模式。默认情况下,文本模式允许您像编辑任何其他源文件一样直接编辑 XML 文件。

编辑器右侧的预览面板在您进行更改时,为您提供布局外观的实时预览。您还可以通过选择“配置渲染”菜单下的“预览所有屏幕尺寸”选项来预览您的布局在多个设备上的外观。虚拟设备下拉菜单中有一个相同的选项。这两个菜单都位于预览窗格的左上角。您可以打开和关闭预览选项,看看它是如何工作的。

预览窗格顶部有几个控件,允许您更改预览的呈现方式。您可以在定义了 AVD 的任何特定设备中渲染预览。您可以同时在多台设备上预览。您还可以更改用于渲染预览的 API 级别和主题。表 8-2 描述了图 8-2 中突出显示的预览窗格的注释部分。

表 8-2。

Description of the Preview Pane

| 部分 | 描述 | | --- | --- | | 答:预览切换 | 这是一个预览开关。它可以选择特定的 Android 版本或选择所有的屏幕尺寸。它可用于根据当前布局快速创建特定屏幕尺寸的布局。 | | B: AVD 渲染 | 此菜单允许您在特定设备上预览布局。它也可以用来切换所有屏幕尺寸作为优先菜单。 | | C: UI 模式 | 在这里,您可以找到在横向、纵向和各种 UI 模式之间切换预览器的选项,以及汽车、桌面和电视对接模式。它还包括电器模式和夜间模式。 | | 主题控制 | 主题切换允许您预览带有特定主题的布局。它默认为 AppTheme,但是您可以从 SDK 中的各种主题中选择,或者从您的项目中选择任何主题。 | | 活动协会 | 活动关联菜单允许您将当前布局与特定活动相关联。 | | f:本地控制 | 此菜单将预览设置为使用特定的翻译。 | | g:安卓版本 | API 菜单允许您将预览设置为特定的 API 级别。您可以使用它来查看您的布局如何响应各种 API 级别。 |

A978-1-4302-6602-0_8_Fig2_HTML.jpg

图 8-2。

The Preview pane in detail

在文本模式下,选择RelativeLayout标签并将其开始和结束标签更改为FrameLayout。请注意预览窗格中没有任何变化,因为您只更改了根布局标记,还没有触及其中的任何内容。稍后您将了解到这些布局之间的更多区别。

选择嵌套的TextView中的Hello World文本,它会自动展开为@string/hello_world,这是对外部strings.xml文件中文本的引用。Android Studio 的代码折叠功能默认隐藏外部字符串引用。按 Ctrl+- | Cmd+-将属性收拢或折叠回其呈现形式,然后按 Ctrl+= | Cmd+=将其展开以查看实际属性值。在 Android 中,将字符串值硬编码到你的布局中被认为是不好的做法,因为它们作为字符串引用会更好地处理。在一个简单的例子中,比如我们在这里创建的例子,硬编码字符串并不重要,但一个商业应用可能需要以几种语言推出,外部化的字符串使这个过程变得非常简单。所以,养成字符串外化的习惯是个好主意。

引用是在资源文件中编码的特殊属性值,它引用在别处定义的实际值。在这种情况下,特殊字符串“@string/hello_world”指的是在strings.xml资源文件中定义的值。Ctrl+click | Cmd+click 文本导航到"Hello World"字符串定义,如下所示:

<string name="hello_world">Hello world!</string>

将值更改为“Hello Android Studio!”按 Ctrl+Alt+左箭头键| Cmd+Alt+左箭头键导航回布局,并在预览窗格中查看更新的新值。现在将文本改为一个随机的硬编码值,比如“再见,拉斯维加斯!”,预览将再次更新,但在这种情况下,您已经直接覆盖了字符串。当您更改TextView时,预览窗格将会更新。

宽度和高度

文本视图是您可以添加到布局中的许多视图之一。每个视图都有控制其大小的宽度和高度属性。您可以设置绝对像素值,如250px或使用各种相对值之一,如250dp。最好使用一个带有dp后缀的相对值,因为这使得组件能够根据呈现它的设备的像素密度来调整大小。相对尺寸将在后面的“覆盖各种显示尺寸”一节中解释。将TextView标签改为Button tag,然后将android:layout_width属性改为match_parent。文本视图将变成一个按钮,横跨整个屏幕。将android:layout_height属性更改为match_parent。该按钮将占据整个屏幕。将android:layout_width属性更改为wrap_content,按钮宽度会变窄,但仍然占据屏幕的整个高度。match_parent值是一个特殊的相对值,它根据父容器调整视图的大小。图 8-3 描述了使用match_parent测量部件宽度和/或高度的可能变化。wrap_content是另一个广泛使用的相对值,它以一种紧密围绕其内容的方式来调整视图的大小。将Button标签改回TextView标签,将其宽度和高度设置为match_parent,并向我们的布局添加几个其他组件,如ButtonCheckBox,如清单 8-1 中所定义。

A978-1-4302-6602-0_8_Fig3_HTML.jpg

图 8-3。

Variations of the match_parent size value Listing 8-1. Add More Components to the Layout

<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android

xmlns:tools="``http://schemas.android.com/tools

android:layout_width="match_parent"

android:layout_height="match_parent"

android:paddingLeft="@dimen/activity_horizontal_margin"

android:paddingRight="@dimen/activity_horizontal_margin"

android:paddingTop="@dimen/activity_vertical_margin"

android:paddingBottom="@dimen/activity_vertical_margin"

tools:context=".MainActivity">

<TextView

android:text="Goodbye, Las Vegas!"

android:layout_width="wrap_content"

android:layout_height="wrap_content" />

<Button

android:text="Push Me"

android:layout_width="wrap_content"

android:layout_height="wrap_content" />

<CheckBox

android:text="Click Me"

android:layout_width="wrap_content"

android:layout_height="wrap_content" />

</FrameLayout>

请注意这些组件是如何一个接一个地绘制的。图 8-4 说明了这个问题。FrameLayout的行为是按照定义的顺序堆叠组件。暂时删除额外堆叠的组件,以便您可以探索设计器模式并了解如何可视化地布局组件。

A978-1-4302-6602-0_8_Fig4_HTML.jpg

图 8-4。

Widgets are stacked on top of one another

让我们检查一下FrameLayout容器标签。这个标签定义了两个属性,android:layout_widthandroid:layout_height,它们都指定了match_parent。这意味着框架的宽度和高度将与其包含的父级的宽度和高度相匹配。因为FrameLayout是最外层的元素,或者说根元素,所以它是所有其他组件的父元素。因此,它的宽度和高度将覆盖设备屏幕的整个可视区域。

设计师模式

点击编辑器左下方的设计选项卡(如图 8-5 所示)调出设计模式。在本节中,您将探索如何使用可视化设计器来定位控件。

A978-1-4302-6602-0_8_Fig5_HTML.jpg

图 8-5。

The designer and text view tabs

设计模式与文本模式具有相同的实时预览窗格,但添加了一个小部件调色板。在可视化设计布局时,您可以将组件从组件面板拖放到预览窗格中。可视化设计器为您生成 XML,同时允许您专注于布局的外观。设计模式还在右上角显示一个组件树窗格,在其下方显示一个属性窗格。组件树提供了当前布局中所有视图和视图组组件的分层视图。顶部是根组件,在我们的例子中是FrameLayout

框架布局

正如您所看到的,FrameLayout按照定义的顺序堆叠组件。但是,它也将您的屏幕分成九个特殊部分。单击TextView in the component tree并按 Delete 键将其删除。执行相同的操作来移除复选框和按钮小部件,并完全清除显示。在左侧面板中找到Button小部件,并单击它。在预览窗格中移动鼠标,注意鼠标移动时显示的突出显示部分。屏幕被分成由每个特殊FrameLayout部分指示的区域(参见图 8-6 )。单击左上部分以放下按钮。双击按钮并将其文本更改为左上角以指示其位置。继续拖放其他八个部分中的小部件,并相应地标记它们。当您拖放每个按钮时,在文本模式和设计模式之间来回切换,以查看 XML 是如何生成的。当你完成时,你应该有类似图 8-7 的东西。参见清单 8-2 中创建该布局的代码。

A978-1-4302-6602-0_8_Fig7_HTML.jpg

图 8-7。

Layout demonstrating FrameLayout

A978-1-4302-6602-0_8_Fig6_HTML.jpg

图 8-6。

Preview pane is divided into nine drop sections Listing 8-2. Code That Creates the Figure 8-7 Layout

<FrameLayout

android:layout_width="fill_parent"

android:layout_height="fill_parent"

xmlns:android="``http://schemas.android.com/apk/res/android

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Center"

android:id="@+id/button"

android:layout_gravity="center" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Top Left"

android:id="@+id/button2"

android:layout_gravity="left|top" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Top Center"

android:id="@+id/button3"

android:layout_gravity="center_horizontal|top" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Top Right"

android:id="@+id/button4"

android:layout_gravity="right|top" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Center Left"

android:id="@+id/button5"

android:layout_gravity="center|left" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Center Right"

android:id="@+id/button6"

android:layout_gravity="center|right" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Bottom Left"

android:id="@+id/button7"

android:layout_gravity="bottom|left" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Bottom Center"

android:id="@+id/button8"

android:layout_gravity="bottom|center" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Bottom Right"

android:id="@+id/button9"

android:layout_gravity="bottom|right" />

</FrameLayout>

设计者生成这个 XML,它以一个FrameLayout标签开始。它的宽度和高度被设置为占据屏幕的整个可视区域。每个嵌套的按钮都指定了一个layout_gravity,它决定了按钮落入屏幕的哪个区域。

线性布局

LinearLayout水平或垂直地组织相邻的子节点。打开左侧的项目窗格。在res文件夹下找到layout文件夹,右键打开右键菜单。单击“新建➤ XML ➤ XML 布局文件”创建一个新的布局资源文件,并将其命名为 three_button。单击并将三个按钮放置到预览中,每个按钮都位于“上一个”按钮的下方。你的布局应该看起来像图 8-8 的左侧。在预览的左上方,点按“转换方向”按钮(在第二行按钮中)。屏幕上的按钮会从垂直对齐切换到水平对齐,如图 8-8 右图所示。

A978-1-4302-6602-0_8_Fig8_HTML.jpg

图 8-8。

Vertical LinearLayout vs. a Horizontal LinearLayout

下面的 XML(如清单 8-3 所示)以一个LinearLayout根标签开始,它指定了一个方向属性。方向可以设置为垂直或水平。嵌套在LinearLayout中的Button标签根据方向从上到下或从左到右排列。

Listing 8-3. A Three-Button LinearLayout Example

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:orientation="horizontal" android:layout_width="match_parent"

android:layout_height="match_parent">

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="New Button"

android:id="@+id/button1" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="New Button"

android:id="@+id/button2" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="New Button"

android:id="@+id/button3" />

</LinearLayout>

相对布局

通过使用相对属性来组织它的子节点。使用这些类型的布局时,您可以创建更复杂的设计,因为您可以更好地控制每个子视图的放置位置。在本例中,您将假装创建一个个人资料视图,类似于您在社交网络应用中找到的内容。

创建一个名为 relative_example 的新布局 XML 文件,并将RelativeLayout指定为根元素。将一个ImageView拖放到预览的左上角。拖动时您会看到辅助线,它应该会吸附到左上角。不要惊慌,这个控件在放下时会消失,因为我们没有给它维度或内容。在屏幕右侧的属性窗格中找到src属性,并单击省略号以打开资源对话框。(您可能需要滚动属性才能找到src。)选择系统页签,然后选择名为sym_def_app_icon的资源,如图 8-9 所示。

A978-1-4302-6602-0_8_Fig9_HTML.jpg

图 8-9。

Select the sym_def_app_icon

图标将呈现在添加到预览窗格的ImageView中。从调色板中点击PlainTextView,然后点击ImageView的右上方,将PlainTextView放置在相对于该组件的右侧,并与其父组件的顶部对齐。当您在图像的右边缘移动鼠标时,会出现一个工具提示,指示当前的放置位置。操纵直至刀尖同时提示toRightOf=imageViewalignParentTop,如图 8-10 所示。

A978-1-4302-6602-0_8_Fig10_HTML.jpg

图 8-10。

Tool tips show as you move around the view

将另外两个PlainTextView组件拖到预览上,将每个组件排在前面组件的下方和ImageView的右侧。使用指南来帮助你。双击顶部的TextView,并更改其文本以包含一个名称。更改中间TextView的文本,以包含一个著名的城市。最后,改变底部TextView的文本以包含一个网站。当您在 designer 视图中工作时,来回切换到 text 视图以查看生成的 XML。你应该有类似图 8-11 的东西。请参见清单 8-4 了解该布局背后的代码。

A978-1-4302-6602-0_8_Fig11_HTML.jpg

图 8-11。

The relative layout for the profile Listing 8-4. The Code Behind the Layout in Figure 8-11

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:layout_width="match_parent" android:layout_height="match_parent">

<ImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/imageView"

android:layout_alignParentTop="true"

android:layout_alignParentLeft="true"

android:layout_alignParentStart="true"

android:src="@android:drawable/sym_def_app_icon" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Clifton Craig"

android:id="@+id/textView1"

android:layout_alignParentTop="true"

android:layout_toRightOf="@+id/imageView" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="California"

android:id="@+id/textView2"

android:layout_below="@+id/textView1"

android:layout_toRightOf="@+id/imageView" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="``http://codeforfun.wordpress.com

android:id="@+id/textView3"

android:layout_below="@+id/textView2"

android:layout_toRightOf="@+id/imageView" />

</RelativeLayout>

生成的 XML 包含作为根元素的RelativeLayout。这个布局包含一个具有两个属性的ImageViewlayout_alignParentToplayout_alignParentLeft。这些属性使ImageView固定在布局的左上方。layout_alignParentStart属性用于支持从右到左的语言,不会产生歧义。ImageView也指定了我们之前研究过的高度和宽度属性。最后,它定义了一个指向sym_def_app_icon资源的src属性,这是一个由 Android 运行时预定义的内置资源。

每个小部件都包含一个值以@+id/开始的android:id属性。这些 ID 属性是在运行时定位单个小部件的一种方式。对于RelativeLayouts,它们变得尤其重要,因为它们被用来指定一个小部件相对于另一个小部件的位置。注意剩余的TextView组件如何使用layout_belowlayout_toRightOf属性中的这些值。它们各自指定了layout_toRightOf=@+id/imageView,这将它们直接放置在图像视图的右边缘。最后两个TextView小部件指定了一个layout_below属性,该属性指向紧接在它前面的TextView

嵌套布局

布局可以相互嵌套,以创建复杂的设计。如果您想改进前面的概要视图,您可以利用在您的RelativeLayout中嵌套一个LinearLayout的优势。该布局可以包括在线状态标签和描述字段。

在调色板中单击垂直的LinearLayout,并在预览窗格中单击ImageView的正下方以放置它。确保刀尖指示alignParentLeftbelow=imageView。在调色板中点击Plain TextView,然后在新添加的LinearLayout内点击放置该组件。这将是您的在线状态指示器。接下来找到Large Text小部件;在组件面板中点击它,这一次在右边的组件树中找到另一个新的TextView,试着在它下面点击来放置组件。将鼠标悬停在LinearLayout中的TextView下方,会出现一个粗下划线的拖放目标指示器,如图 8-12 所示。

A978-1-4302-6602-0_8_Fig12_HTML.jpg

图 8-12。

Mouse under the TextView to see a drop-target indicator, and click to add the widget

使用 properties 窗格,将第一个TextView的 text 属性更改为 online,并向它下面的TextView的 text 属性添加一个伪描述。接下来单击预览中的任意位置,并按 Ctrl+A | Cmd+A 选择所有组件。找到layout:margin属性,展开,设置 all 为5dp,给每个组件一个 5 像素的边距,如图 8-13 所示。

A978-1-4302-6602-0_8_Fig13_HTML.jpg

图 8-13。

Give all widgets a 5-pixel margin

边距控制组件边缘和任何相邻组件之间的间距。为组件提供边距是减少界面混乱的好方法。尽管我们在所有组件的所有边上都设置了相同的边距,但是您可以尝试在某些边上设置不同的边距。

layout:margin分组包含四个边的设置:左、上、右和下。再次选择所有组件,展开layout:margin设置,找到全部选项。删除5dp值,改为将5dp值设置到左侧设置。组件将紧密地组合在一起,但是左边距在水平边缘之间留出足够的空间。选择在线的TextView,设置它的上边距为5dp,让它和上面的图片之间有更多的空间。图 8-14 显示了此时的结果。清单 8-5 显示了这个布局背后的代码。

A978-1-4302-6602-0_8_Fig14_HTML.jpg

图 8-14。

The results of adding left and top margins Listing 8-5. The Code for relative_example.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:layout_width="match_parent"

android:layout_height="match_parent">

<ImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/imageView"

android:layout_alignParentTop="true"

android:layout_alignParentLeft="true"

android:layout_alignParentStart="true"

android:src="@android:drawable/sym_def_app_icon"

android:layout_marginLeft="5dp" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Clifton Craig"

android:id="@+id/textView1"

android:layout_alignParentTop="true"

android:layout_toRightOf="@+id/imageView"

android:layout_marginLeft="5dp" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="California"

android:id="@+id/textView2"

android:layout_below="@+id/textView1"

android:layout_toRightOf="@+id/imageView"

android:layout_marginLeft="5dp" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="``http://codeforfun.wordpress.com

android:id="@+id/textView3"

android:layout_below="@+id/textView2"

android:layout_toRightOf="@+id/imageView"

android:layout_marginLeft="5dp" />

<LinearLayout

android:orientation="vertical"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:layout_below="@+id/imageView"

android:layout_alignParentLeft="true"

android:layout_alignParentStart="true"

android:layout_marginLeft="5dp">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Online"

android:id="@+id/textView"

android:layout_marginLeft="5dp"

android:layout_marginTop="5dp" />

<EditText

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:id="@+id/editText"

android:text="Likes biking, reads tech manuals and loves to code in Java"

android:layout_marginLeft="5dp" />

</LinearLayout>

</RelativeLayout>

嵌套布局的另一种方法是用 includes 间接引用它们。找到LinearLayout,更改其属性,使其包含一个值为detailsid属性,并确保其高度设置为wrap_content。同时更改设置layout_below属性,使其属于textView3。这显示在以下代码中:

<LinearLayout

android:id="@+id/details"

android:orientation="vertical"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_below="@+id/textView3"

android:layout_alignParentLeft="true"

android:layout_alignParentStart="true"

android:layout_marginLeft="5dp">

接下来,在最后一个TextView标签下,但就在结束LinearLayout标签之前,添加下面的include标签:

<include layout="@layout/three_button"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_below="@id/details"/>

特殊的include标签将任何预定义的布局添加到当前布局。在前面的示例中,您在当前布局中包含了我们之前的三按钮示例。您将宽度声明为match_parent,它扩展了布局的整个宽度,并将高度设置为wrap_content。您还将按钮布局设置在details组件的下面,这是相对布局的名称。

按住 Ctrl 键并单击| Cmd 键并单击布局属性的值@layout/three_button,导航到其定义。在定义中,您将更改每个按钮的文本,以反映社交网络应用中可用的典型操作。更改每个按钮的文本属性,依次添加好友、关注和消息。您可以在文本或设计模式下完成此操作。图 8-15 展示了这在设计模式下的样子。

A978-1-4302-6602-0_8_Fig15_HTML.jpg

图 8-15。

Add labels to the buttons

完成后,导航回relative_example.xml查看集成按钮。图 8-16 显示了完成的结果。

A978-1-4302-6602-0_8_Fig16_HTML.jpg

图 8-16。

The relative_example.xml with integrated buttons

列表视图

ListView小部件是一个容器控件,它提供了一个项目列表,每个项目都是可操作的。这些列表项被组织在一个位于可滚动视图中的布局中。单个列表项的内容由适配器以编程方式提供,适配器从数据源提取内容。适配器将数据映射到布局中的各个视图。在这个例子中,您将探索一个ListView组件的简单用法。

在 res ➤布局文件夹下创建一个名为list_view的新布局。指定FrameLayout作为根元素。在FrameLayout的中心添加一个ListView。预览窗格将显示使用默认布局的ListView,称为简单的 2 行列表项。切换到文本编辑模式,向根元素标签添加一个xmlns:tools属性。将其值设置为 http://schemas.android.com/tools 。这使得 tools:前缀属性可用,其中一个属性将用于更改预览渲染的方式。向ListView标签添加一个tools:listitem属性,并将其值设置为"@android:layout/simple_list_item_1"。如下面的代码片段所示:

<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:layout_width="match_parent" android:layout_height="match_parent"

xmlns:tools="``http://schemas.android.com/tools

>

<ListView

android:layout_width="wrap_content"

android:layout_height="match_parent"

android:id="@+id/listView"

android:layout_gravity="center"

tools:listitem="@android:layout/simple_list_item_1"

/>

</FrameLayout>

在 Android Studio 的早期版本中,可以在设计模式下右键单击预览窗格中的ListView,从菜单中选择预览列表内容➤简单列表项,如图 8-17 所示。1.0 版本中删除了此功能。

A978-1-4302-6602-0_8_Fig17_HTML.jpg

图 8-17。

List Preview Layout option feature from Android Studio 0.8 beta

打开MainActivity类,将其改为扩展ListActivity,然后在onCreate()方法中输入以下内容:

public class MainActivity extends ListActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.list_view);

String[] listItems = new String[]{"Mary","Joseph","Leah", "Mark"};

setListAdapter(new ArrayAdapter<String>(this,

android.R.layout.simple_list_item_1,

listItems));

}

//...

}

ListActivity是一个特殊的基类,旨在提供处理ListView的通用功能。在我们的例子中,我们使用提供的setListAdapter方法,它将一个适配器与列表视图关联起来。我们创建一个ArrayAdapter,并给它一个上下文(当前正在执行的活动)、一个列表项布局和一个填充ListView的项目数组。现在构建并运行应用,它会崩溃!这是因为ListActivity的常见误用。这个特殊的活动寻找一个 id 为@android:id/listListView。这些是由系统定义的特殊的 Android ids,这个特殊的 id 让ListActivity找到它的ListView,并自动将其连接到给定的ListAdapter。按如下方式更改list_view布局中的ListView标签:

<ListView

android:layout_width="wrap_content"

android:layout_height="match_parent"

android:id="@android:id/list"

android:layout_gravity="center"

tools:listitem="@android:layout/simple_list_item_1"

/>

构建并测试应用,您应该会看到如图 8-18 所示的名称列表。

A978-1-4302-6602-0_8_Fig18_HTML.jpg

图 8-18。

Screenshot of a simple ListView

通过为列表项提供自定义布局,可以进一步自定义列表视图的外观。要想知道最终结果会是什么样子,打开list_view.xml。右键单击预览窗格中的ListView,将其预览列表内容设置回简单的两行列表项。这种布局使用一个大文本视图和一个小文本视图来显示多个值。切换到文本视图查看生成的 XML,如清单 8-6 所示。

Listing 8-6. Custom Layout for list items

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android

xmlns:tools="``http://schemas.android.com/tools

android:layout_width="match_parent" android:layout_height="match_parent">

<ListView

android:layout_width="wrap_content"

android:layout_height="match_parent"

android:id="@android:id/list"

android:layout_gravity="center"

tools:listitem="@android:layout/simple_list_item_2" />

</FrameLayout>

ListView元素中添加了一个特殊的tools:listitem属性来控制预览窗格中的布局。这个属性是在 tools XML 名称空间中定义的,它被添加到了FrameLayout根元素中。ctrl+click | Cmd+单击listitem属性的值,导航到其定义。该布局包括两个子视图,其id值为@android:id/text1@android:id/text2。我们之前的例子包括一个数组适配器,它知道如何向simple_list_item_1布局添加值。使用这种新布局,您需要定制逻辑来为这两个子视图设置值。回到MainActivity类。在最顶层定义一个内部的Person类,为列表中的每个人保存一个额外的 web 站点值,并更改onCreate()方法,如清单 8-7 所示。

Listing 8-7. Create Person Class and Modify onCreate( )

public class MainActivity extends ListActivity {

class Person {

public String name;

public String website;

public Person(String name, String website) {

this.name = name;

this.website = website;

}

}

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.list_view);

Person[] listItems = new Person[]{

new Person("Mary", "www.allmybuddies.com/mary

new Person("Joseph", "``www.allmybuddies.com/joeseph

new Person("Leah", "``www.allmybuddies.com/leah

new Person("Mark", "``www.allmybuddies.com/mark

};

setListAdapter(new PersonAdapter(this,

android.R.layout.simple_expandable_list_item_2,

listItems)

);

}

//...

}

在这些修订中,您创建了一个由Person对象组成的数组,每个对象都在构造函数中接受名称和网站字符串值。这些值缓存在公共变量中。(尽管我们强烈主张在常规实践中使用 getters 和 setters 而不是 public 变量,但为了简洁起见,我们在我们设计的示例中使用了后者。)然后你将列表和相同的simple_expandable_list_item_2布局传递给一个自定义的PersonAdapter,我们还没有定义它。按 Alt+Enter 激活 IntelliSense,这将给你机会为PersonAdapter创建一个存根内部类。见图 8-19 。

A978-1-4302-6602-0_8_Fig19_HTML.jpg

图 8-19。

Add PesonAdapter inside onCreate( ) method

选择 Create Inner Class 选项,将在当前类中为您生成一个类存根。使用 Tab 键浏览构造函数参数。随着您的推进,将每个构造函数参数更改为Context contextint layoutPerson[] listItems respectively。让这个类扩展BaseAdapter而不是实现ListItem,然后使用清单 8-8 中的代码完成它的定义。因为我们在 PersonAdapter 中使用了Person类,所以需要将它移到 MainActivity 之外。将光标放在Person类定义上,按下 F6 将其移动到更高的级别。您将看到如图 8-20 所示的对话框。单击“重构”移动该类。

A978-1-4302-6602-0_8_Fig20_HTML.jpg

图 8-20。

Add PesonAdapter inside onCreate( ) method Listing 8-8. PersonAdapter Class

public class PersonAdapter extends BaseAdapter {

private final Context context;

private final int layout;

private Person[] listItems;

public PersonAdapter(Context context, int layout, Person[] listItems) {

this.context = context;

this.layout = layout;

this.listItems = listItems;

}

@Override

public int getCount() {

return listItems.length;

}

@Override

public Object getItem(int i) {

return listItems[i];

}

@Override

public long getItemId(int i) {

return i;

}

@Override

public View getView(int position, View convertView, ViewGroup parent) {

View view = convertView;

if (view==null) {

LayoutInflater inflater = (LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

view = inflater.inflate(layout, parent, false);

}

TextView text1 = (TextView) view.findViewById(android.R.id.text1);

TextView text2 = (TextView) view.findViewById(android.R.id.text2);

text1.setText(listItems[position].name);

text2.setText(listItems[position].website);

return view;

}

}

这个基本示例说明了适配器如何创建单个列表项进行显示。定义从缓存上下文、布局资源 ID 和列表项作为成员变量开始,这些变量稍后用于创建单独的列表项视图。扩展BaseAdapter为您提供了适配器接口中一些方法的默认实现。否则,这些方法将被要求显式定义。然而,您有义务为getCount()getItem()getItemId()getView()抽象方法提供一个实现。运行时调用getCount()方法,以便知道需要呈现多少视图。对于需要在给定位置检索项目的调用者来说,getItem()是必需的。getItemId()必须返回给定位置的项目的唯一编号。在我们的示例中,您可以只返回作为参数给出的位置,因为它是唯一的。最后,getView()包含了组装每个列表项视图的所有逻辑。它用一个位置、一个可能为空也可能不为空的convertView()以及包含它的ViewGroup父对象被重复调用。如果convertView()为空,您必须使用从构造函数缓存的布局 ID 和父视图组作为其目的地,来扩展一个新视图以保存列表项细节。您使用LAYOUT_INFLATER_SERVICE系统服务进行膨胀。展开视图后,找到text1text2子视图,并分别用给定位置的人的姓名和 web 站点值填充它们。运行这个示例,看看每个 person 对象是如何映射到新的布局的。图 8-21 显示了您的屏幕外观。

A978-1-4302-6602-0_8_Fig21_HTML.jpg

图 8-21。

List showing new list item layout and use of PersonAdapter

布局设计指南

市场上有如此多的 Android 设备,每一个都有不同的屏幕尺寸和密度,布局设计可能具有挑战性。设计布局时,你需要注意几点。你也可以遵循一些规则来跟上快速发展的形势。一般来说,你要注意屏幕分辨率和像素密度。

屏幕分辨率是屏幕在水平和垂直方向上可以容纳的像素总数,以二维数字的形式给出。分辨率通常以标准 VGA 测量值给出。VGA 代表视频图形阵列,是 640×480 的台式机和笔记本电脑的标准。这意味着 640 像素宽和 480 像素高。这年头能找到半 VGA (HVGA)、480×320 等移动变种;四分之一 VGA (QVGA),320×240;宽 VGA (WVGA),800×480;扩展图形阵列(XGA);宽 XGA(WXGA);以及更多。这些仅仅是一些可能的解决方案。

像素密度表示在给定的测量单位内可以压缩的像素总数。这种测量与屏幕大小无关,尽管它会受到屏幕大小的影响。例如,想象一下,20 英寸显示器的分辨率为 1024×768 像素,而 5 英寸显示器的分辨率为 1024×768 像素。两种情况下使用的像素数量相同,但后一种屏幕将这些像素压缩到一个更小的区域,从而增加了它们的密度。像素密度以每英寸点数(dpi)来衡量,它表示 1 英寸区域中可以容纳的点数或像素。在 Android 屏幕上,密度通常以一种称为密度无关像素(dp)的单位来测量。它是基于 160dpi 屏幕上相当于 1 个像素的基线测量。使用差压作为测量单位可使您的布局在不同密度的设备上适当缩放。

Android 包括了另一种隔离不同屏幕尺寸的方法:资源限定符。在我们之前的例子中,我们将一个图像复制到了drawable文件夹中,这是任何可绘制资源被提取的默认位置。可绘制资源通常是图像,但也可以包括定义形状定义、轮廓和边框的资源 XML 文件。为了定位可绘制资源,Android 运行时首先考虑当前设备的屏幕尺寸。如果它属于主要类别列表中的一个,运行时将在带有资源限定符后缀的drawable文件夹下查找。这些是后缀,比如ldpimdpihdpixhdpildpi后缀是低密度屏幕,大约 120dpi(每英寸 120 点)。中密度屏幕,160dpi,使用mdpi后缀。高密度屏幕,320dpi,使用hdpi后缀。超高密度屏幕使用xhdpi后缀。这不是一个详尽的列表,但它代表了更常见的后缀。当您在 Android Studio 中启动一个项目时,会在res文件夹下创建无数特定于分辨率的子文件夹。在下一个例子中,您将研究如何以实用的方式使用这些文件夹。

覆盖各种显示器尺寸

在本练习中,您将找到一个 200×200 像素的配置文件图像,并将其交换到您已经构建的RelativeLayout示例中。您可以选择使用本书源代码下载中的图片。这将是您在最高分辨率显示器上使用的图像。

将图像命名为 my_profile.png,并将其保存到硬盘上。打开项目窗口,展开res文件夹。您的项目应该有带有mdpihdpixhdpixxhdpi后缀的drawable文件夹。您需要为不同的屏幕尺寸创建原始图像的缩小版本。您将遵循 3:4:6:8 的缩放比例进行调整。您可以使用 Microsoft Paint 或任何其他工具来调整大小。(一个名为 Image Resizer for Windows 的开源项目可在imageresizer.codeplex.com获得,它可以使这项任务变得简单,并与 Windows 资源管理器很好地集成。)参考表 8-3 了解如何按照我们的比例指南在单个文件夹中创建缩放尺寸。将图像的每个版本保存在表格所示的文件夹中,并对每个版本使用相同的my_profile.png名称。

表 8-3。

Various Image Asset Sizes and Descriptions

| 文件夹 | 原始大小 | 比例 | 缩放尺寸 | 图像 | | --- | --- | --- | --- | --- | | `drawable-xxhdpi` | 200×200 | 不适用的 | 200×200 | ![A978-1-4302-6602-0_8_Figa_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/learn-as/img/A978-1-4302-6602-0_8_Figa_HTML.jpg) | | `drawable-xhdpi` | 200×200 | 3:4 | 150×150 | ![A978-1-4302-6602-0_8_Figb_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/learn-as/img/A978-1-4302-6602-0_8_Figb_HTML.jpg) | | `drawable-hdpi` | 150×150 | 4:6 | 100×100 | ![A978-1-4302-6602-0_8_Figc_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/learn-as/img/A978-1-4302-6602-0_8_Figc_HTML.jpg) | | `drawable-mdpi` | 100×100 | 6:8 | 75×75 | ![A978-1-4302-6602-0_8_Figd_HTML.jpg](https://gitee.com/OpenDocCN/vkdoc-android-zh/raw/master/docs/learn-as/img/A978-1-4302-6602-0_8_Figd_HTML.jpg) |

添加这些图像后,在设计器模式下打开relative_example.xml布局并找到图像视图。点击该组件的src属性旁边的省略号,在资源对话框中找到my_profile图像,如图 8-22 所示。

A978-1-4302-6602-0_8_Fig22_HTML.jpg

图 8-22。

Resources dialog box with image of Clifton Craig

更新图片后,点击预览窗口中的 Android 虚拟设备按钮,尝试各种屏幕渲染选项,如图 8-23 所示。选择 Preview All Screen Sizes(预览所有屏幕尺寸),可以同时在多个设备上查看模拟轮廓,如图 8-24 所示。

A978-1-4302-6602-0_8_Fig24_HTML.jpg

图 8-24。

Layout previewed on various devices

A978-1-4302-6602-0_8_Fig23_HTML.jpg

图 8-23。

Preview All Screen Sizes from Design mode in Visual Designer

把这一切放在一起

现在,您将使用 Java 加载布局,并探索如何在运行时进行细微的更改。在开始之前,您需要向将要使用的组件添加描述性 id。在设计模式下打开relative_example.xml布局,并将以下 id 添加到这些嵌套在LinearLayout中的组件中:

  • imageView: profile_image
  • textView1: name
  • textView2: location
  • textView3: website
  • textView4: online_status
  • editText: description

通过单击每个小部件,然后在右窗格的属性编辑器中更改其id属性来进行这些更改。当您进行更改时,您将看到一个弹出对话框,要求更新使用情况。见图 8-25 。

A978-1-4302-6602-0_8_Fig25_HTML.jpg

图 8-25。

Android Studio will update usages while you work

选中复选框并单击是,允许 Android Studio 在您工作时更新每个小部件的所有用法。切换到文本模式查看最终结果,如清单 8-9 所示。

Listing 8-9. New Layout with Components Placed Inside

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:layout_width="match_parent"

android:layout_height="match_parent">

<ImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/profile_image"

android:layout_alignParentTop="true"

android:layout_alignParentLeft="true"

android:layout_alignParentStart="true"

android:src="@drawable/my_profile"

android:layout_marginLeft="5dp" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Clifton Craig"

android:id="@+id/name"

android:layout_alignParentTop="true"

android:layout_toRightOf="@+id/profile_image"

android:layout_marginLeft="5dp" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="California"

android:id="@+id/location"

android:layout_below="@+id/name"

android:layout_toRightOf="@+id/profile_image"

android:layout_marginLeft="5dp" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="``http://codeforfun.wordpress.com

android:id="@+id/website"

android:layout_below="@+id/location"

android:layout_toRightOf="@+id/profile_image"

android:layout_marginLeft="5dp" />

<LinearLayout

android:id="@+id/details"

android:orientation="vertical"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_below="@+id/profile_image"

android:layout_alignParentLeft="true"

android:layout_alignParentStart="true"

android:layout_marginLeft="5dp">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="Online"

android:id="@+id/online_status"

android:layout_marginLeft="5dp"

android:layout_marginTop="5dp" />

<EditText

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:id="@+id/description"

android:text="Likes biking, reads tech manuals and loves to code in Java"

android:layout_marginLeft="5dp" />

</LinearLayout>

<include

android:id="@+id/buttons"

layout="@layout/three_button"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_below="@id/details"/>

</RelativeLayout>

注意 Android Studio 不仅更新了 id 定义,还更新了每个 id 的每次使用,以保持组件彼此相邻对齐,就像以前一样。创建一个名为ProfileActivity的新类,并将其修改为清单 8-10 。

Listing 8-10. ProfileActivity Class

public class ProfileActivity extends Activity {

private TextView name;

private TextView location;

private TextView website;

private TextView onlineStatus;

private EditText description;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.relative_example);

name = (TextView) findViewById(R.id.name);

location = (TextView) findViewById(R.id.location);

website = (TextView) findViewById(R.id.website);

onlineStatus = (TextView) findViewById(R.id.online_status);

description = (EditText) findViewById(R.id.description);

View parent = (View) name.getParent();

parent.setBackgroundColor(getResources().getColor(android.R.color.holo_blue_light));

name.setTextAppearance(this,android.R.style.TextAppearance_DeviceDefault_Large);

location.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Medium);

location.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Inverse);

website.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Inverse);

onlineStatus.setTextAppearance(this, android.R.style.TextAppearance_DeviceDefault_Inverse);

description.setEnabled(false);

description.setBackgroundColor(getResources().getColor(android.R.color.white));

description.setTextColor(getResources().getColor(android.R.color.black));

}

}

这里您已经为每个TextViewEditText组件添加了成员字段。onCreate()方法首先找到每个视图组件,并将它们保存在单独的成员变量中。接下来,您找到名称标签的父标签,并将其背景颜色更改为浅蓝色。Android Studio 有一个独特的功能,用一个正方形装饰左边的槽,说明这一行引用的颜色。这些方块也出现在引用颜色资源的其他行上。然后更改每个TextView的文本外观,使名称以大的外观突出出来。您正在使用来自android.R类的预定义样式,该类包含对 Android SDK 中所有可用资源的引用。每个剩余的TextView也被更新以使用中等或相反的外观。最后,禁用描述EditText以防止修改其内容。您还可以将其背景设置为白色,同时将文本颜色更改为黑色。

要尝试我们新的ProfileActivity和布局,你必须在AndroidManifest.xml中定义它,并将其链接到MainActivity。打开清单,在MainActivity定义下为我们的ProfileActivity添加一个标签:

<activity

android:name=".ProfileActivity"

android:label="@string/app_name" />

接下来返回到MainActivity,用下面的代码覆盖onListItemClick()方法,围绕ProfileActivity类创建一个新的意图,并开始活动。运行该示例,并尝试单击任何列表项以显示其配置文件。参见图 8-26 。

A978-1-4302-6602-0_8_Fig26_HTML.jpg

图 8-26。

New layout with buttons and EditText

@Override

protected void onListItemClick(ListView l, View v, int position, long id) {

super.onListItemClick(l, v, position, id);

Intent intent = new Intent(this, ProfileActivity.class);

startActivity(intent);

}

现在您将学习如何将值从列表视图带入下一个活动。使用清单 8-11 中的代码更改MainActivity类中的onCreate()方法。

Listing 8-11. Modifications to MainActivity

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.list_view);

Person[] listItems = new Person[]{

new Person(R.drawable.mary, "Mary", "New York",

"``www.allmybuddies.com/mary

"Avid cook, writes poetry."),

new Person(R.drawable.joseph,"Joseph", "Virginia",

"``www.allmybuddies.com/joeseph

"Author of several novels."),

new Person(R.drawable.leah, "Leah", "North Carolina",

"``www.allmybuddies.com/leah

"Basketball superstar. Rock climber."),

new Person(R.drawable.mark, "Mark", "Denver",

"``www.allmybuddies.com/mark

"Established chemical scientist with several patents.")

};

setListAdapter(new PersonAdapter(this,

android.R.layout.simple_expandable_list_item_2,

listItems)

);

}

您正在向构造函数调用添加名称、位置和描述字段。现在使用清单 8-12 中的代码将 Person 类更改为接受并保存这些新值。

Listing 8-12. Modifications to the Person Class

class Person {

public int image;

public String name;

public String location;

public String website;

public String descr;

Person(int image, String name, String location, String website, String descr) {

this.image = image;

this.name = name;

this.location = location;

this.website = website;

this.descr = descr;

}

}

Next change the onListItemClick() as follows:

@Override

protected void onListItemClick(ListView l, View v, int position, long id) {

super.onListItemClick(l, v, position, id);

Person person = (Person) l.getItemAtPosition(position);

Intent intent = new Intent(this, ProfileActivity.class);

intent.putExtra(ProfileActivity.IMAGE, person.image);

intent.putExtra(ProfileActivity.NAME, person.name);

intent.putExtra(ProfileActivity.LOCATION, person.location);

intent.putExtra(ProfileActivity.WEBSITE, person.website);

intent.putExtra(ProfileActivity.DESCRIPTION, person.descr);

startActivity(intent);

}

在这里,您检索被单击的Person对象,并将它的每个成员变量作为额外的值传递给下一个活动。这些额外的值被映射到ProfileActivity常量,我们在ProfileActivity类的顶部定义了这些常量:

public class ProfileActivity extends Activity {

public static final String IMAGE = "IMAGE";

public static final String NAME = "NAME";

public static final String LOCATION = "LOCATION";

public static final String WEBSITE = "WEBSITE";

public static final String DESCRIPTION = "DESCRIPTION";

//...

}

现在对清单 8-13 和ProfileActivity进行如下修改,以定义一个profileImage ImageView成员变量,并将所有额外的意图读入缓存的视图组件。

Listing 8-13. Modifications to the PersonActivity Class

private ImageView proflieImage;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.relative_example);

name = (TextView) findViewById(R.id.name);

location = (TextView) findViewById(R.id.location);

website = (TextView) findViewById(R.id.website);

onlineStatus = (TextView) findViewById(R.id.online_status);

description = (EditText) findViewById(R.id.description);

proflieImage = (ImageView) findViewById(R.id.profile_image);

int profileImageId = getIntent().getIntExtra(IMAGE, -1);

proflieImage.setImageDrawable(getResources().getDrawable(profileImageId));

name.setText(getIntent().getStringExtra(NAME));

location.setText(getIntent().getStringExtra(LOCATION));

website.setText(getIntent().getStringExtra(WEBSITE));

description.setText(getIntent().getStringExtra(DESCRIPTION));

运行应用,尝试点击列表视图中的项目,调出相应的配置文件。您可以点击后退键导航回列表视图并选择不同的项目。参见图 8-27 。

A978-1-4302-6602-0_8_Fig27_HTML.jpg

图 8-27。

Layout with ImageView

碎片

片段是您之前检查的活动和可包含文件之间的一个步骤。片段是可重用的 XML 片段,类似于包含布局。然而,像活动一样,它们有包含业务逻辑的额外好处。片段用于使你的用户界面适应不同的外形。考虑一下我们之前的例子,我们开发的时候考虑到了智能手机,在 10 英寸的平板电脑上会是什么样子。更大的显示屏所提供的额外空间会让一个简单列表视图的屏幕看起来很笨拙。使用片段,您可以智能地组合两个屏幕,这样您的显示就像当前在较小的屏幕上一样,但在较大的屏幕上包含列表和细节视图。要完成这项任务,您必须将所有的 UI 更新逻辑从活动中移出,放到新的片段类中。从MainActivity中的ListView逻辑开始,您需要将嵌套类作为外部顶级类取出。点击MainActivity顶部的Person类,然后按 F6。弹出的 Move Refactor 对话框询问您希望将类移动到哪个包和目录中。您可以在这里接受默认值。对底层的PersonAdapter类做同样的事情。

创建一个名为BuddyListFragment的新类,它扩展了ListFragment并包含了MainActivity中的ListView的初始化,如清单 8-14 所示。

Listing 8-14. BuddyListFragment Class Which Extends ListFragment

import android.app.Activity;

import android.os.Bundle;

import android.support.v4.app.ListFragment;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.ListView;

public class BuddyListFragment extends ListFragment {

private OnListItemSelectedListener onListItemSelectedListener;

public interface OnListItemSelectedListener {

void onListItemSelected(Person selectedPerson);

}

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

Person[] listItems = new Person[]{

new Person(R.drawable.mary, "Mary",

"``www.allmybuddies.com/mary

"New York","Avid cook, writes poetry."),

new Person(R.drawable.joseph, "Joseph",

"``www.allmybuddies.com/joeseph

"Virginia","Author of several novels"),

new Person(R.drawable.leah, "Leah",

"``www.allmybuddies.com/leah

"North Carolina",

"Basketball superstar. Rock climber."),

new Person(R.drawable.mark,"Mark",

"``www.allmybuddies.com/mark

"Denver",

"Established chemical scientist with several patents.")

};

setListAdapter(new PersonAdapter(getActivity(),

android.R.layout.simple_expandable_list_item_2,

listItems)

);

}

@Override

public void onAttach(Activity activity) {

super.onAttach(activity);

if(!(activity instanceof OnListItemSelectedListener)) {

throw new ClassCastException(

"Activity should implement OnListItemSelectedListener");

}

//Save the attached activity as an onListItemSelectedListener

this.onListItemSelectedListener = (OnListItemSelectedListener) activity;

}

@Override

public void onListItemClick(ListView l, View v, int position, long id) {

Person selectedPerson = (Person) l.getItemAtPosition(position);

this.onListItemSelectedListener.onListItemSelected(selectedPerson);

}

@Override

public View onCreateView(LayoutInflater inflater, ViewGroup container,

Bundle savedInstanceState) {

return inflater.inflate(R.layout.list_view, container, false);

}

}

这个片段镜像了MainActivity中的onCreate()方法,但是增加了两个生命周期方法。onAttach()方法捕获必须实现在类顶部声明的OnListItemSelectedListener()的附加活动。ListFragment超类定义了一个在这里被覆盖的onListItemClick()回调方法。在我们的自定义版本中,您可以引用缓存的onListItemSelectedListener()并将选择的人传递到它上面。最后,您覆盖了扩展我们的list_view布局的onCreateView()生命周期方法,并将其返回到运行时。

创建一个扩展FragmentBuddyDetailFragment类,并用清单 8-15 中所示的代码填充它。

Listing 8-15. BuddyDetailFragment Code

import android.os.Bundle;

import android.support.v4.app.Fragment;

import android.support.v4.app.FragmentActivity;

import android.view.LayoutInflater;

import android.view.View;

import android.view.ViewGroup;

import android.widget.EditText;

import android.widget.ImageView;

import android.widget.TextView;

public class BuddyDetailFragment extends Fragment {

public static final String IMAGE = "IMAGE";

public static final String NAME = "NAME";

public static final String LOCATION = "LOCATION";

public static final String WEBSITE = "WEBSITE";

public static final String DESCRIPTION = "DESCRIPTION";

private Person person;

@Override

public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {

updatePersonDetail(bundle);

return inflater.inflate(R.layout.relative_example, container, false);

}

@Override

public void onStart() {

super.onStart();

updatePersonDetail(getArguments());

}

private void updatePersonDetail(Bundle bundle) {

//if bundle arguments were passed, we use them

if (bundle != null) {

this.person = new Person(

bundle.getInt(IMAGE),

bundle.getString(NAME),

bundle.getString(LOCATION),

bundle.getString(WEBSITE),

bundle.getString(DESCRIPTION)

);

}

//if we have a valid person from the bundle

//or from restored state then update the screen

if(this.person !=null){

updateDetailView(this.person);

}

}

public void updateDetailView(Person person) {

FragmentActivity activity = getActivity();

ImageView profileImage = (ImageView) activity.findViewById(R.id.profile_image);

TextView name = (TextView) activity.findViewById(R.id.name);

TextView location = (TextView) activity.findViewById(R.id.location);

TextView website = (TextView) activity.findViewById(R.id.website);

EditText description = (EditText) activity.findViewById(R.id.description);

profileImage.setImageDrawable(getResources().getDrawable(person.image));

name.setText(person.name);

location.setText(person.location);

website.setText(person.website);

description.setText(person.descr);

}

}

这个思路和之前创造的ProfileActivity差不多。但是,现在您有了一个内部的Person成员变量,用于保存包值。您这样做是因为您现在从两个地方读取包中的值,onCreate()onStart()。您还创建了一个公共方法,允许外部调用者用给定的人更新片段。另一件要注意的事情是,您覆盖了onCreateView()生命周期方法,并要求充气机通过使用资源 ID 来膨胀适当的视图。

我们的主屏幕将被更改以反映单个片段,它将像以前一样具有列表视图。简化activity_main布局,由一个FrameLayout组成:

<FrameLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:id="@+id/empty_fragment_container"

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:paddingBottom="@dimen/activity_vertical_margin"

>

</FrameLayout>

这个空布局有一个适当命名的 ID,值为empty_fragment_container。稍后,您将向该布局动态添加一个片段。重新访问您的res目录,使用特殊资源限定符large创建另一个布局文件。右键单击res文件夹,选择新建➤ Android 资源文件,添加新的资源文件。将名称设置为activity_main,与之前使用的名称相同。将资源类型设置为布局。从可用限定符列表中选择大小;从屏幕尺寸下拉菜单中选择大。参见图 8-28 进行指导。这与我们前面的例子相似,在前面的例子中,您为不同的屏幕密度添加了图像。布局文件将位于layout-large目录中。此文件夹中的布局将在分类为大型的设备上选择,例如 7 英寸及以上的平板电脑。

A978-1-4302-6602-0_8_Fig28_HTML.jpg

图 8-28。

Select layout-large from New Resource Directory

打开新创建的activity_main布局,切换到文本模式并输入以下 XML:

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:orientation="horizontal"

android:layout_width="match_parent"

android:layout_height="match_parent">

<fragment android:name="com.apress.gerber.simplelayouts.BuddyListFragment"

android:id="@+id/list_fragment"

android:layout_width="wrap_content"

android:layout_height="match_parent"

/>

<fragment android:name="com.apress.gerber.simplelayouts.BuddyDetailFragment"

android:id="@+id/detail_fragment"

android:layout_width="wrap_content"

android:layout_height="match_parent"

/>

</LinearLayout>

请注意,它在平板电脑 AVD 上呈现布局。预览窗格会抱怨在设计时没有足够的信息来呈现您的布局。它将建议几个布局与预览相关联,并给你一个选项来选择你的项目布局。使用“选择布局”超链接为您的项目选择布局。选择第一个片段的list_view布局,第二个片段的relative_example布局,如图 8-29 所示。

A978-1-4302-6602-0_8_Fig29_HTML.jpg

图 8-29。

Linear layout containing fragments

此时,列表视图将占据整个屏幕。您需要稍微调整宽度和重量,以便为查看两个片段留出空间。诀窍是将宽度设置为 0dp,并使用 weight 属性来适当调整小部件的大小。将两个片段的宽度都更改为 0dp。将BuddyListFragment重量设置为 1,将BuddyDetailFragment重量设置为 2。weight 属性允许您根据可用空间的比率来调整组件的大小。系统将布局中所有组件的重量相加,并用该总和除以可用空间。每个组件占据的空间部分相当于其重量。在我们的例子中,细节片段将占据屏幕的 2/3,而列表将占据 1/3。您的更改应该类似于清单 8-16 。

Listing 8-16. Linear Layout Containing Fragments with Changes

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android

xmlns:tools="``http://schemas.android.com/tools

android:orientation="horizontal"

android:layout_width="match_parent"

android:layout_height="match_parent">

<fragment android:name="com.apress.gerber.simplelayouts.BuddyListFragment"

android:id="@+id/list_fragment"

android:layout_weight="1"

android:layout_width="0dp"

android:layout_height="match_parent"

tools:layout="@layout/list_view" />

<fragment android:name="com.apress.gerber.simplelayouts.BuddyDetailFragment"

android:id="@+id/detail_fragment"

android:layout_weight="2"

android:layout_width="0dp"

android:layout_height="match_parent"

tools:layout="@layout/relative_example" />

</LinearLayout>

尝试预览窗格。使用工具栏中的控制将方向更改为横向或选取不同的 avd。图 8-30 展示了 Nexus 10 的横向布局。

A978-1-4302-6602-0_8_Fig30_HTML.jpg

图 8-30。

View the fragment in landscape on a Nexus 10

有了这两个片段,你就可以打开MainActivity并简化它了。Make it extend FragmentActivity. FragmentActivity是一个特殊的类,它允许您在视图层次结构中查找片段,并通过一个FragmentManager类执行片段事务。您将在我们的示例中使用事务来添加和替换屏幕上的片段。在小屏幕设备上,运行时将使用empty_fragment_container选择布局。您将使用FragmentManager将我们的BuddyListFragment添加到屏幕上。您还将在用一个片段替换另一个片段时创建一个事务,并将其添加到后台堆栈,以便用户可以通过单击 back 按钮来展开操作。

简化清单 8-17 中所示的onCreate()方法。

Listing 8-17. Simplified onCreate( ) Method

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

if(findViewById(R.id.empty_fragment_container)!=null) {

// We should return if we're being restored from a previous state

// to avoid overlapping fragments.

if (savedInstanceState != null) {

return;

}

BuddyListFragment buddyListFragment = new BuddyListFragment();

// Pass any Intent extras to the fragment as arguments

buddyListFragment.setArguments(getIntent().getExtras());

FragmentTransaction transaction =

getSupportFragmentManager().beginTransaction();

transaction.add(R.id.empty_fragment_container, buddyListFragment);

transaction.commit();

}

}

通过将内容视图设置为activity_main布局来开始这个方法,这是您简化的。然后,检查如果应用正在从先前的状态恢复,是否应该提前返回。然后实例化BuddyListFragment,并将任何额外的意图作为参数传递给它。接下来创建一个FragmentTransaction,向其中添加片段并提交事务。您只有在找到empty_fragment_container的情况下才执行此操作。

更改类声明,使其也实现BuddyListFragment.OnListItemSelectedListener接口。它应该如下所示:

public class MainActivity extends FragmentActivity

implements BuddyListFragment.OnListItemSelectedListener {

IntelliSense 将标记错误的类,因为它没有定义所需的方法。按 Alt+Enter 并选择提示来生成方法存根。使用清单 8-18 中所示的示例进行填写。

Listing 8-18. Code Showing FragmentManager Transaction

@Override

public void onListItemSelected(Person selectedPerson) {

BuddyDetailFragment buddyDetailFragment = (BuddyDetailFragment)

getSupportFragmentManager().findFragmentById(R.id.detail_fragment);

if (buddyDetailFragment != null) {

buddyDetailFragment.updateDetailView(selectedPerson);

} else {

buddyDetailFragment = new BuddyDetailFragment();

Bundle args = new Bundle();

args.putInt(BuddyDetailFragment.IMAGE, selectedPerson.image);

args.putString(BuddyDetailFragment.NAME, selectedPerson.name);

args.putString(BuddyDetailFragment.LOCATION, selectedPerson.location);

args.putString(BuddyDetailFragment.WEBSITE, selectedPerson.website);

args.putString(BuddyDetailFragment.DESCRIPTION, selectedPerson.descr);

buddyDetailFragment.setArguments(args);

//Start a fragment transaction to record changes in the fragments.

FragmentTransaction transaction =

getSupportFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,

// and add the transaction to the back stack so the user can navigate back

transaction.replace(R.id.empty_fragment_container, buddyDetailFragment);

transaction.addToBackStack(null);

// Commit the transaction

transaction.commit();

}

}

移除onListItemSelected()方法,因为这段代码替换了它。在这里,您检查buddyDetailFragment是否已经在视图层次结构中。如果有,你找到它并更新它。否则,您可以重新创建它,并通过使用您在BuddyDetailFragment中定义的键,将所选的人作为一个包中的单个值传递进来。最后,创建并提交一个片段事务,用细节片段替换列表片段,并将事务添加到后台堆栈。在平板电脑和智能手机上运行代码(分别为图 8-31 和图 8-32 ),查看不同的行为。您可以创建一个 Nexus 10 平板电脑 AVD,用于大屏幕测试。

A978-1-4302-6602-0_8_Fig32_HTML.jpg

图 8-32。

Layout rendered on a phone

A978-1-4302-6602-0_8_Fig31_HTML.jpg

图 8-31。

Layout rendered on a tablet

摘要

在本章中,你学习了在 Android Studio 中设计用户界面的基础知识。您使用了可视化设计器和文本编辑器来创建和修改布局。您了解了如何使用各种容器和属性来对齐用户界面中的元素,以及如何嵌套容器来创建复杂的界面。我们解释了如何针对不同的屏幕尺寸和设备类型调整布局中的元素,并举例说明了如何在多个设备上同时查看布局。我们谈到了碎片。每个主题都有更多的细节。Android 包括丰富的定制,允许您构建和调整用户界面来满足您的需求。参见 https://developer.android.com 了解更多可用的高级特性和 API。

九、货币 Lab:第一部分

这一章,以及下一章,将向你展示如何在构建一个名为货币的应用的环境中使用 Android Studio。货币的目的是提供一种在外币和用户本国货币之间进行转换的便捷方式。典型的用例是用户在国外旅行,需要用外币兑换货币或购买一些东西。货币汇率总是在波动,甚至可能一分钟一分钟地变化,所以用户能够获得最新的数据是很重要的。货币应用从由openexchangerates.org托管的网络服务中获取最新的汇率。

不仅货币会波动,交易所上市的活跃货币代码也会变化。例如,比特币(BTC)最近被添加到openexchangerates.org的交易货币列表中。如果我们不久前开发了货币应用并硬编码了活跃的货币代码,我们可能会错过比特币,或者更糟的是,为用户提供了从不再交易的失败国家选择货币的选项。为了解决这个问题,我们需要在填充主活动布局中使用的微调器之前,获取由openexchangerates.org发布的活动货币代码。如果你把浏览器指向 openexchangerates.org/api/currencies.json ,你就能看到 JSON 格式的现行货币代码,谢天谢地,这些代码是机器可读和人工可读的。货币应用涵盖的 Android 功能和技术包括高级布局、素材、共享偏好、风格、web 服务、并发性和对话框。

Note

我们邀请您使用 Git 克隆这个项目,以便跟进,尽管您将从头开始使用它自己的 Git 库重新创建这个项目。如果你的电脑上没有安装 Git,参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端),发出以下 Git 命令:git clone https://bitbucket.org/csgerber/currencies.git_Currencies

货币规格

为了解决前面描述的活动货币代码问题,我们将使用一个典型的 Android 惯例,称为闪屏。当应用 firsts 启动时(见图 9-1 ,用户会看到一个活动,其中只包含一张各种货币的照片。当这个闪屏活动可见时,后台线程获取活动货币代码。当后台线程成功终止时,闪屏活动调用主活动并将活动货币代码传递给它。然后,主活动使用活动货币代码来填充其微调器。即使假设连接性相对较差,闪屏活动也只能在大约一秒钟内可见。

A978-1-4302-6602-0_9_Fig1_HTML.jpg

图 9-1。

Currency splash screen

如果用户先前选择了本币和外币,则从用户的共享首选项中获取这些值,并将适当的值应用于微调器(参见图 9-2 )。例如,如果上次使用的货币组合是 HKD 作为外币,美元作为本币,那么下次用户启动应用时,这些相同的值将应用于微调器。在极端情况下,存储在共享首选项中的本币和/或外币值中的一个或两个不再交易。在这种情况下,受影响的微调器将只显示列出的第一个货币代码。

A978-1-4302-6602-0_9_Fig2_HTML.jpg

图 9-2。

The input currency amount

一旦主活动可见,焦点就被设置到位于主活动顶层的EditText控件上。此EditText控件仅接受数字数据,并代表要转换的外币金额。在从微调器中选择外币和本币,并输入想要转换的金额之后,用户单击 Calculate 按钮,这将触发一个后台线程来获取当前汇率。当后台线程激活时,用户看到一个对话框显示“请稍等”(见图 9-3);此对话框允许用户通过单击取消按钮来中止操作。一旦后台线程成功终止,就会从openexchangerates.org返回一个 JSON 对象,其中包含所有活动货币代码相对于美元的汇率。然后提取适当的值,并计算结果。结果格式化为五位小数,并显示在主活动底层的TextView控件中,如图 9-4 所示。

A978-1-4302-6602-0_9_Fig4_HTML.jpg

图 9-4。

Returning the result

A978-1-4302-6602-0_9_Fig3_HTML.jpg

图 9-3。

Calculating the result

货币应用的操作栏有一个带有三个选项的菜单:查看活动代码、反转代码和退出(见图 9-5 )。查看活动代码选项启动浏览器并指向 openexchangerates.org/api/currencies.json 。“反转代码”选项将微调器中显示的值转换为本币和外币。例如,如果外币是 CNY,本币是美元,则在激活反转代码菜单选项后,外币将是美元,本币将是 CNY。退出选项只是退出应用。图 9-4 和 9-5 中获得的结果(72.39314 美元和 72.44116 美元)略有不同,尽管我们使用了相同的输入值 450。这种差异的有趣原因是openexchangerates.org上的汇率每分钟都在波动,我们计算这两张截图的结果只相差几分钟。

A978-1-4302-6602-0_9_Fig5_HTML.jpg

图 9-5。

The Options menuUsing the New Project Wizard

现在,您已经了解了货币应用应该如何运行,让我们通过选择文件➤新建项目来创建一个新项目。(新项目向导及其屏幕包含在第一章中。)说出你的应用货币。我们选择使用 gerber.apress.com 作为域名,但是你可以输入你喜欢的域名。Android(和 Java)中的约定是反域名为包名。您会注意到包名是反向域名加上全部小写字母的项目。与本书中的其他实验和练习一样,您可以将该实验存储在C:\androidBook\Currencies目录中。如果您运行的是 Mac,请将货币应用放在 labs 的父目录中。单击下一步。

向导的下一步是选择一个目标 API 级别。在使你的应用兼容尽可能多的设备(通过将你的目标 API 设置得较低)和增加你作为开发者可用的特性数量(通过将你的目标 API 设置得较高)之间有一个权衡。然而,这种权衡严重偏向于将您的目标 API 级别设置得较低,因为 Google 提供了优秀的兼容性库,这些库提供了您将在以后的 API 中找到的大多数功能。在 Android 中开发商业应用的最佳实践是选择最高的目标 API 级别,该级别仍然允许您的应用在大约 100%的设备上运行。目前,目标 API 级别是 API 8。请注意,Android Studio 会自动为您导入适当的兼容性库(也称为支持库)。API 8:默认应该选择 Android 2.2 (Froyo)。如果尚未选择,请选择 API 8: Android 2.2 (Froyo),然后单击下一步。

向导的下一步是选择将为您生成的活动类型。选择空白活动,然后单击下一步。如果默认值与图 9-6 中显示的不同,则按此设置。点击 Finish,Android Studio 会为你生成一个新的项目。grad le(Android Studio 附带的构建工具,在第十三章中有介绍)将开始下载任何依赖项,比如兼容性库。请留意状态栏以查看这些进程的状态。一旦这些过程完成,您应该有一个没有错误的新项目。

A978-1-4302-6602-0_9_Fig6_HTML.jpg

图 9-6。

The Create New Project dialog box

初始化 Git 储存库

Git 已经成为 Android 应用开发不可或缺的工具,这向您展示了如何为您的 Android 项目初始化 Git 存储库。关于如何使用 Git 的更全面的教程,请参见第七章。选择 VCS ➤导入到版本控制➤创建 Git 资源库,如图 9-7 所示。当提示选择将创建新 Git 存储库的目录时,确保 Git 项目将在根项目目录中初始化,该目录名为Currencies,在本例中位于C:\androidBook\Currencies,如图 9-8 所示。如果你运行的是 Mac 电脑,在你的 Lab 父目录中选择Currencies目录。单击确定。

A978-1-4302-6602-0_9_Fig8_HTML.jpg

图 9-8。

Selecting the directory for Git initialization

A978-1-4302-6602-0_9_Fig7_HTML.jpg

图 9-7。

Initializing the Git repository

请确保将项目工具窗口切换到项目视图。视图组合框位于项目工具窗口的顶部,默认设置为 Android。如果您在 Project tool 窗口中检查这些文件,您将会注意到这些文件中的大部分都变成了棕色,这意味着它们正在被 Git 跟踪,但是没有被计划添加到存储库中。要添加它们,在项目工具窗口中选择Currencies目录并按 Ctrl+Alt+A | Cmd+Alt+A 或选择 Git ➤添加。棕色文件应该变成绿色,这意味着它们已经被添加到 Git 中的 staging 索引中,现在可以提交了。如果这个添加然后转移素材的过程看起来很乏味,请记住,您只需要这样做一次;从现在开始,Android Studio 将自动为您管理文件的添加和升级。

按 Ctrl+K | Cmd+K 调用提交修改对话框,如图 9-9 所示。作者组合框用于覆盖当前的默认提交者。您应该将作者组合框留空,Android Studio 将简单地使用您在 Git 安装期间最初设置的默认值。在“提交前”部分,取消选择所有复选框选项。在提交消息字段中键入以下消息:使用新建项目向导进行初始提交。单击提交按钮两次。通过按 Alt+9 | Cmd+9 检查“更改”工具窗口,以查看您的提交。

A978-1-4302-6602-0_9_Fig9_HTML.jpg

图 9-9。

Committing initial changes with the Commit Changes dialog box

修改主活动的布局

在本节中,我们将修改MainActivity的布局。新建项目向导为我们创建了一个名为activity_main.xml的文件。打开该文件,参考图 9-2 (之前显示)和清单 9-1 。图 9-2 中的视图是垂直排列的,所以垂直方向的LinearLayout对于我们的根ViewGroup来说似乎是个不错的选择。我们视图的宽度将填充父视图ViewGroup,所以只要有可能layout_width就应该设置为fill_parentfill_parentmatch_parent设置可以互换使用。为了在我们的布局中表达视图的高度,我们希望尽可能避免硬编码dp(与密度无关的像素)值。相反,我们将使用一个名为layout_weight的属性来指示 Android Studio 以其父视图ViewGroup的百分比来呈现视图的高度。

layout_weight属性被计算为任何给定父ViewGroup的子视图的layout_weight值总和的一部分。例如,假设我们有一个TextView和一个Button嵌套在一个方向为垂直的LinearLayout中。如果TextViewlayout_weight为 30,而Buttonlayout_weight为 70,那么TextView将占据其父布局高度的 30%,而Button将占据其父布局高度的 70%。为了使我们的任务更容易,让我们假设 100 为layout_weight总和,这样每个layout_weight值将表示为百分比。使用这种技术的唯一问题是layout_height是 Android 视图中的一个必需属性,所以我们必须将layout_height的值设置为0dp。通过将layout_height设置为0dp,你实际上是在告诉 Android 忽略layout_height而使用layout_weight

当您检查这个布局中包含的视图时,请注意其中一些视图有 ID,而另一些没有。只有在 Java 代码中引用视图时,为视图分配 ID 才有用。如果一个视图在整个用户体验中保持静态,就没有理由给它分配一个 ID。当你从清单 9-1 重新创建布局时,注意id的使用,以及layout_weightlayout_height的使用。选择 activity_main.xml 选项卡后,您会在底部看到另外两个选项卡,Design 和 Text。点击文本选项卡,然后键入清单 9-1 中包含的代码,或者如果你正在阅读这本书的电子版,复制并粘贴。确保完全替换activity_main.xml中任何现有的 XML 代码。

Listing 9-1. activity_main.xml Code

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="``http://schemas.android.com/apk/res/android

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:background="#000"

android:orientation="vertical">

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_weight="20"

android:orientation="vertical">

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="30"

android:gravity="bottom"

android:text="Foreign Currency"

android:textColor="#ff22e9ff"/>

<Spinner

android:id="@+id/spn_for"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="55"

android:gravity="top"/>

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="15"

android:gravity="bottom"

android:text="Enter foreign currency amount here:"

android:textColor="#666"

android:textSize="12sp"/>

</LinearLayout>

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="20"

android:background="#222">

<EditText

android:id="@+id/edt_amount"

android:layout_width="fill_parent"

android:layout_height="50dp"

android:layout_gravity="center_vertical"

android:layout_marginLeft="5dp"

android:layout_marginRight="5dp"

android:background="#111"

android:digits="0123456789."

android:gravity="center_vertical"

android:inputType="numberDecimal"

android:textColor="#FFF"

android:textSize="30sp">

<requestFocus/>

</EditText>

</LinearLayout>

<Button

android:id="@+id/btn_calc"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="10"

android:text="Calculate"

android:textColor="#AAA"/>

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_weight="20"

android:orientation="vertical">

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="30"

android:gravity="bottom"

android:text="Home Currency"

android:textColor="#ff22e9ff"/>

<Spinner

android:id="@+id/spn_hom"

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="55"

android:gravity="top"/>

<TextView

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="15"

android:gravity="bottom"

android:text="Calculated result in home currency:"

android:textColor="#666"

android:textSize="12sp"/>

</LinearLayout>

<LinearLayout

android:layout_width="fill_parent"

android:layout_height="0dp"

android:layout_marginLeft="10dp"

android:layout_marginRight="10dp"

android:layout_weight="20"

android:background="#222">

<TextView

android:id="@+id/txt_converted"

android:layout_width="fill_parent"

android:layout_height="50dp"

android:layout_gravity="center_vertical"

android:layout_marginLeft="5dp"

android:layout_marginRight="5dp"

android:background="#333"

android:gravity="center_vertical"

android:textSize="30sp"

android:typeface="normal"/>

</LinearLayout>

</LinearLayout>

一旦创建了这个布局,按 Ctrl+K | Cmd+K 并提交一条修改 activity_main 布局的消息。

定义颜色

当您检查清单 9-1 中的 XML 源代码时,请注意我们已经硬编码了像textColorbackground这样的属性。将颜色值外部化到资源文件是一个好主意,特别是当颜色重复时。一旦您将一种颜色具体化,您就可以通过简单地改变资源文件中的一个值来改变整个应用中的颜色。在第五章中,我们展示了如何使用IntelliSense创建颜色定义。这里,我们将从颜色定义开始,替换硬编码的值。使用对你来说最容易的方法。右键单击(在 Mac 上按住 Ctrl 键)目录并选择“新➤值资源文件”。命名文件颜色,然后单击确定。如果提示将文件添加到 Git,选择记住,不要再问复选框,并选择是。修改清单 9-2 中的colors文件。

Listing 9-2. Define Some Colors in colors.xml

<?xml version="1.0" encoding="utf-8"?>

<resources>

<color name="white">#FFF</color>

<color name="black">#000</color>

<color name="grey_very_dark">#111</color>

<color name="grey_dark">#222</color>

<color name="grey_med_dark">#333</color>

<color name="grey_med">#666</color>

<color name="grey_light">#AAA</color>

<color name="turquoise">#ff22e9ff</color>

<color name="flat_blue">#ff1a51f4</color>

</resources>

在 Android 中,颜色用十六进制数字表示。十六进制数字可以使用以下字母数字值:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E 和 F。从 0 到 9 的十进制和十六进制数字是相同的,但要用十六进制表示 10、11、12、13、14 和 15,则分别使用 A、B、C、D、E 和 F。十六进制数字不区分大小写,所以 F 与 F 相同。

在 Android 中,你可以用四种格式中的一种来表达颜色:#ARGB、#RGB、# AARRGGBB 或# RRGGBB 每个字母是一个十六进制数字。#ARGB 格式代表 Alpha,红色,绿色,蓝色通道,Alpha 是透明通道。该配色方案中可能的颜色数量是 16 个可能的透明度值乘以 16×16×16 个可能的颜色组合。#RGB 格式代表红色、绿色、蓝色,Alpha 通道自动设置为完全不透明。# AARRGGBB 和#RRGGBB 格式使用 8 位通道,而不是#ARGB 和#RGB 格式中使用的 4 位通道。# AARRGGBB 格式中可能的颜色组合数量是 256 个可能的透明度级别乘以 256×256×256 个可能的颜色组合。#RRGGBB 格式类似于前者,只是透明度级别自动设置为完全不透明。

我们的colors.xml文件中的<color name="grey_med">#666</color>条目使用#RGB 格式。显然,具有等量红色、绿色和蓝色的颜色将是灰色。我们的colors.xml文件中的<color name="turquoise">#ff22e9ff</color>条目使用# AARRGGBB 格式。我们可以看到,我们的绿松石被定义为大量的蓝色和绿色,很少红色。如果我们在任何 XML 文件的装订线中单击任何颜色样本,我们可以看到一个对话框,允许我们定义我们想要的任何颜色,尽管从“选择颜色”对话框返回的字符串总是采用最精确的格式#AARRGGBB。见图 9-10 。一旦你定义了你的颜色,按 Ctrl+K | Cmd+K 提交一条定义一些颜色的消息。

A978-1-4302-6602-0_9_Fig10_HTML.jpg

图 9-10。

The Choose Color dialog box

将颜色应用于布局

现在您已经在colors.xml文件中定义了您的颜色,您可以将它们应用到您的布局中。一种方法是使用 Android Studio 的查找/替换功能。参见第五章了解创建颜色值的另一种方法。在编辑器中将activity_main.xml和 colors.xml 文件作为选项卡打开。右键单击(在 Mac 上按住 Ctrl 键)?? 标签并选择向右移动,这样你就可以并排看到两个文件。将光标置于 activity_main.xml 选项卡中,然后按 Ctrl+R | Cmd+R。在查找字段中键入#FFF,在替换字段中键入@color/white。选中单词复选框,然后单击全部替换。对我们定义的所有颜色重复这个步骤,除了flat_blue,我们稍后会用到它。你可以在图 9-11 中看到这个过程。一旦你应用了你的颜色,按 Ctrl+K | Cmd+K 确认并显示一条消息“应用颜色到布局”。然后关闭colors.xml选项卡。

A978-1-4302-6602-0_9_Fig11_HTML.jpg

图 9-11。

Replacing hard-coded color values with named references in the colors.xml file

创建和应用样式

风格可以大大提高你的生产力。短期内对创建风格的少量投资可能会在长期内为您节省大量时间,并且提供很大的灵活性。在这一节中,我们将为activity_main.xml布局中的一些视图创建样式,并向您展示如何应用它们。

我们使用的布局适合样式,因为许多属性在视图中是重复的。例如,两个蓝绿色的TextView控件共享除文本之外的所有相同属性。我们可以将这些复制的属性提取到一个样式中,然后将该样式应用于适当的TextView元素。如果我们以后想改变样式,我们只需简单地改变一次样式,所有使用该样式的视图也会改变。样式是有用的,但是没有理由对样式感到高兴,并把样式应用到你所有的视图中。例如,为 Calculate 按钮创建一个样式没有多大意义,因为只有一个样式。

我们的第一个任务是为在activity_main.xml布局中使用的标签(TextViews)创建样式。将您的光标放在我们的第一个TextView的定义内的任何地方,这个定义具有文本属性Foreign Currency。从主菜单中,选择重构➤提取➤风格。

在提取 Android 样式对话框中,选择如图 9-12 所示的复选框。在样式名称字段中输入 label。确保选中启动复选框,然后单击确定。在随后的可能使用样式对话框中,如图 9-13 所示,选择文件单选按钮,然后点击确定。现在,在“查找工具”窗口中单击“执行重构”(位于 IDE 底部),将该样式应用于共享这些属性的其他三个视图。

A978-1-4302-6602-0_9_Fig13_HTML.jpg

图 9-13。

The Use Style Where Possible dialog box

A978-1-4302-6602-0_9_Fig12_HTML.jpg

图 9-12。

Extracting the style called label

样式最好的属性之一是它们可以从你或 Android SDK 定义的父样式继承。将光标放在同一个TextView控件的同一个定义中,再次选择重构➤提取➤风格。

您会注意到提供给您的样式名称以label.开头。label后面的圆点表示这种新样式将继承其父样式label。将样式命名为 label.curr,如图 9-14 所示,点击确定。再次单击“执行重构”。

A978-1-4302-6602-0_9_Fig14_HTML.jpg

图 9-14。

Extracting the style called label.curr

activity_main.xml文件中,导航到标签为Enter foreign currency amount here:TextView。将光标放在该视图定义的括号内的任意位置,并从主菜单中选择“重构➤提取➤样式”。Android Studio 足够智能,能够意识到文本可能不会重复,并从提取 Android 样式对话框中将其忽略。将该样式重命名为 label.desc,点击确定,如图 9-15 所示。再次单击 IDE 底部的 Do Refactor,将样式应用于第二次出现的这个TextView

A978-1-4302-6602-0_9_Fig15_HTML.jpg

图 9-15。

Extracting the style called label.desc

让我们为布局再创建一种样式,为输入字段和输出字段提供灰色背景。将光标放在背景为@color/grey_darkLinearLayout的定义内的任何地方。从主菜单中,选择重构➤提取➤风格。调用你的新样式 layout_back,如图 9-16 所示,点击确定。

A978-1-4302-6602-0_9_Fig16_HTML.jpg

图 9-16。

Extracting the style called layout_back

从“尽可能使用样式”对话框中选择“文件”单选按钮,然后单击“确定”。现在单击 Do Refactor 将样式应用到第二次出现的布局。

按 Ctrl+Shift+N | Cmd+Shift+O,键入 styles,选择res/values/styles.xml文件,在编辑器中将其作为选项卡打开。你应该会得到一些看起来很像图 9-17 的东西。按 Ctrl-K | Cmd+K 提交,并显示创建样式并将样式应用于布局的消息。

A978-1-4302-6602-0_9_Fig17_HTML.jpg

图 9-17。

Styles created automatically for you in the styles.xml file

创建 JSONParser 类

为了从openexchangerates.org web 服务中读取数据,我们需要一种解析 JSON 的方法。JSON 代表 JavaScript 对象符号,已经成为 web 服务事实上的标准格式。我们已经创建了自己的 JSON 解析器,名为JSONParser。这个类使用DefaultHttpClient填充InputStream,使用BufferedReader解析数据,使用JSONObject构造并返回JSONObject。虽然这听起来很复杂,但却非常简单。顺便说一下,我们不是唯一提出 JSON 解析器的人;如果在您最喜欢的搜索引擎中搜索 JSON parser,您会发现这种基本模式的许多实现。

详细解释JSONParser如何工作超出了本书的范围。尽管如此,请将这个类添加到您的项目中,因为我们将需要它的全部功能。右键单击(在 Mac 上按住 Ctrl 键单击)这个com.apress.gerber.currencies包,然后选择 New ➤ Java Class。将您的类命名为 JSONParser。将清单 9-3 中的代码键入(或者复制粘贴)到这个类中。

Listing 9-3. The JSONParser.java Code

package com.apress.gerber.currencies;

import android.util.Log;

import org.apache.http.HttpEntity;

import org.apache.http.HttpResponse;

import org.apache.http.client.ClientProtocolException;

import org.apache.http.client.methods.HttpPost;

import org.apache.http.impl.client.DefaultHttpClient;

import org.json.JSONException;

import org.json.JSONObject;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.io.UnsupportedEncodingException;

public class JSONParser {

static InputStream sInputStream = null;

static JSONObject sReturnJsonObject = null;

static String sRawJsonString = "";

public JSONParser() {}

public JSONObject getJSONFromUrl(String url) {

//attempt to get response from server

try {

DefaultHttpClient httpClient = new DefaultHttpClient();

HttpPost httpPost = new HttpPost(url);

HttpResponse httpResponse = httpClient.execute(httpPost);

HttpEntity httpEntity = httpResponse.getEntity();

sInputStream = httpEntity.getContent();

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

} catch (ClientProtocolException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

//read stream into string-builder

try {

BufferedReader reader = new BufferedReader(new InputStreamReader(

sInputStream, "iso-8859-1"), 8);

StringBuilder stringBuilder = new StringBuilder();

String line = null;

while ((line = reader.readLine()) != null) {

stringBuilder.append(line + "\n");

}

sInputStream.close();

sRawJsonString = stringBuilder.toString();

} catch (Exception e) {

Log.e("Error reading from Buffer: " + e.toString(), this.getClass().getSimpleName());

}

try {

sReturnJsonObject = new JSONObject(sRawJsonString);

} catch (JSONException e) {

Log.e("Parser", "Error when parsing data " + e.toString());

}

//return json object

return sReturnJsonObject;

}

}

在您键入或粘贴了前面的代码之后,按 Ctrl+K | Cmd+K 提交您的更改,并显示一条“创建 JSONParser 类”的提交消息。

创建 Splash 活动

在本节中,我们将创建 splash 活动。这项活动的功能是为我们争取大约一秒钟的时间,以便获取有效的货币代码。当后台线程正在工作时,我们将显示一张货币的照片。如果这是一个商业应用,我们可能会显示一个带有公司标志的图像,也许还有应用的名称。

右键单击(在 Mac 上按住 Ctrl 并单击)该com.apress.gerber.currencies包,然后选择“新建➤活动”“➤空白活动”。将您的Activity命名为 SplashActivity,并选中启动器活动复选框,如图 9-18 所示。

A978-1-4302-6602-0_9_Fig18_HTML.jpg

图 9-18。

New ➤ Activity ➤ Blank Activity to create SplashActivity

在新创建的SplashActivity.java file中,修改类定义,使SplashActivity扩展Activity而不是ActionBarActivity。同样在onCreate()方法中插入this.requestWindowFeature(Window.FEATURE_NO_TITLE);,如清单 9-4 所示,并解析导入。

Listing 9-4. Modify the SplashActivity Class to Extend Activity and Remove the Title Bar

public class SplashActivity extends``Activity

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

this.requestWindowFeature(Window.FEATURE_NO_TITLE);

setContentView(R.layout.activity_splash);

}

...

按 Ctrl+Shift+N | Cmd+Shift+O,然后键入和。选择并打开app/src/main/AndroidManifest.xml文件。修改文件,使其看起来如清单 9-5 所示。

Listing 9-5. Modified AndroidManifest.xml File

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="``http://schemas.android.com/apk/res/android

package="com.apress.gerber.currencies" >

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

<application

android:allowBackup="true"

android:icon="@android:color/transparent"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:icon="@mipmap/ic_launcher"

android:name=".MainActivity"

android:label="@string/app_name" >

</activity>

<activity

android:name=".SplashActivity"

android:label="@string/title_activity_splash">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

我们添加到AndroidManifest.xml文件中的uses-permission行允许应用访问互联网。此外,我们已经将应用本身的icon属性设置为transparent,以确保在SplashActivity之前不显示任何内容。注意SplashActivity现在包含了主/启动器意图过滤器,而不是MainActivity。main/launcher intent-filter 告诉 Android OS 哪个活动将首先启动。

我们需要一些免版税的作品来显示在我们的闪屏上。将浏览器指向 google.com/advanced_image_search 。在所有这些单词字段中,键入货币。在“使用权”字段中,选择“免费使用、共享或修改,甚至商业使用”。单击高级搜索。找到一张你喜欢的图片并下载下来。将图像命名为 world_currencies.jpg(如果文件是 png,则命名为 world_currencies.png)。将world_currencies.jpg复制并粘贴到项目工具窗口中的res/drawable目录中。修改activity_splash.xml文件,结果如清单 9-6 所示。

Listing 9-6. Modified activity_splash.xml File to Display world_currencies as Background

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:background="@drawable/world_currencies"

android:orientation="vertical">

</LinearLayout>

按 Ctrl+K | Cmd+K 提交一条创建 splash 活动的消息,并使其成为已启动的活动。

以 JSON 形式提取活动货币代码

在上一节中,您使SplashActivity成为首先启动的活动,并修改了它的布局以显示world_currencies图像。在本节中,您将修改SplashActivity来触发一个后台线程,以便从 openexchangerates.org/api/currencies.json 获取活动货币代码。

按 Ctrl+N | Cmd+O,键入 Spl,选择SplashActivity.java文件。在我们的SplashActivity中没有菜单,所以我们可以移除那些引用菜单的方法。移除onCreateOptionsMenu()onOptionsItemSelected()方法。

我们需要创建一个AsyncTask,我们称之为FetchCodesTask,因为SplashActivity.java. AsyncTask的私有内部类是一个专门设计用于在 Android 中促进并发(线程)操作的类。我们在第十章的中讨论了AsyncTask的架构,所以在此期间只要相信AsyncTask是可行的。

首先将FetchCodesTask定义为onCreate()方法下SplashActivity.java类的私有内部类,如下所示:

private class FetchCodesTask extends AsyncTask<String, Void, JSONObject> {

}

通过将光标放在红色文本上,然后按 Alt+Enter 并选择 Import Class 来解决任何导入,如图 9-19 所示。

A978-1-4302-6602-0_9_Fig19_HTML.jpg

图 9-19。

Resolving JSONObject and AsyncTask imports

即使在解决了这些导入之后,类定义也应该用红色下划线标出,表明存在编译时错误。将光标放在这个新的内部类定义中,按 Alt+Insert | Cmd+N,然后选择 Override Methods。在弹出的对话框中,按住 Ctrl 键(Mac 上为 Cmd 键)并点击 OK,选择doInBackground()onPostExecute()两种方法,如图 9-20 所示。

A978-1-4302-6602-0_9_Fig20_HTML.jpg

图 9-20。

Selecting doInBackground and onPostExecute methods

请注意,您的方法参数是根据包含在内部类定义中的泛型定义的。修改您的SplashActivity.java类,使其最终如清单 9-7 所示,并解析任何导入。

Listing 9-7. Modify the SplashActivity.java file

public class SplashActivity extends Activity {

//url to currency codes used in this application

public static final String URL_CODES = "``http://openexchangerates.org/api/currencies.json

//ArrayList of currencies that will be fetched and passed into MainActivity

private ArrayList<String> mCurrencies;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

this.requestWindowFeature(Window.FEATURE_NO_TITLE);

setContentView(R.layout.activity_splash);

new FetchCodesTask().execute(URL_CODES);

}

private class FetchCodesTask extends AsyncTask<String, Void, JSONObject> {

@Override

protected JSONObject doInBackground(String... params) {

return new JSONParser().getJSONFromUrl(params[0]);

}

@Override

protected void onPostExecute(JSONObject jsonObject) {

try {

if (jsonObject == null) {

throw new JSONException("no data available.");

}

Iterator iterator = jsonObject.keys();

String key = "";

mCurrencies = new ArrayList<String>();

while (iterator.hasNext()) {

key = (String)iterator.next();

mCurrencies.add(key + " | " + jsonObject.getString(key));

}

finish();

} catch (JSONException e) {

Toast.makeText(

SplashActivity.this,

"There's been a JSON exception: " + e.getMessage(),

Toast.LENGTH_LONG

).show();

e.printStackTrace();

finish();

}

}

}

}

通过点击该行旁边的装订线,在第mCurrencies.add(key + " | " + jsonObject.getString(key));行设置断点。点按工具栏中的“调试”按钮(看起来像 bug 的按钮)。等待项目在调试器中生成和加载。当遇到断点时,单击几次 Resume 按钮(调试工具窗口中的绿色右箭头)。如果您在调试窗口中打开mCurrencies,您会注意到这些值并没有按照特定的顺序获取。见图 9-21 。在调试器窗口中,单击看起来像红色正方形的“停止”按钮。既然我们对正确获取值感到满意,那么按 Ctrl+K | Cmd+K 提交一条从 openexchangerates.org 获取 json 代码的消息。

A978-1-4302-6602-0_9_Fig21_HTML.jpg

图 9-21。

Debug window inspecting mCurrencies frame by frame

启动主活动

在上一节中,您通过使用一个AsyncTask成功地获取了活动货币代码。现在您需要启动MainActivity并向它传递当前的货币代码。

Android 的软件架构极其开放和模块化。模块化是一种福气,因为我们可以将任意数量的第三方应用集成到我们自己的应用中。然而,模块化也是一个诅咒,因为这些其他应用不共享相同的内存空间,因此我们不能简单地传递对象引用。Android 通过在每个活动周围创建一堵中国墙来加强这种模块化,任何对象引用都不能通过这堵墙。仅按值传递规则同样适用于应用间通信和应用内通信。即使我们的SplashActivityMainActivity位于同一个应用的同一个包中,我们仍然必须序列化这两个组件之间的任何通信,就好像每个活动位于不同的服务器上一样;这是我们为开发一个开放的模块化软件架构所付出的代价。

通过使用 Android 中一个名为Intent的专门类,可以方便地通过值传递数据。意图是发送给 Android 操作系统的消息。您不能将意图从一个活动直接发送到另一个活动;Android 操作系统必须总是调解活动之间的通信,这就是为什么您的活动必须总是列在您的AndroidManifest.xml文件中。一个意向也可能有一个称为 bundle 的有效负载。bundle 是键/值对的映射,其中键是字符串,值是 Java 原语或序列化对象。一旦 intent 的包完全装载了数据,就可以将 intent 分派给 Android OS,后者将 intent 及其有效负载传递给目的地活动。

我们想要从SplashActivity传递到MainActivity的数据只是一个字符串列表。幸运的是,ArrayList<String>已经实现了Serializable接口,所以我们可以将mCurrencies对象放入目的地为MainActivity的意图的bundle中,并将该意图分派给 Android 操作系统。打开SplashActivity.java文件。在while循环块之后,放置如图 9-22 所示的三行代码。

A978-1-4302-6602-0_9_Fig22_HTML.jpg

图 9-22。

Create and dispatch Intent

根据需要解决导入问题。在图 9-22 的第一行新代码中,我们正在构建一个意图,并传递一个上下文(SplashActivity.this)和一个目的地活动(MainActivity.class)。下一行用关键字"key_arraylist"mCurrencies对象添加到我们的 intent 包中。最后一行,startActivity(mainIntent);,将意图发送给 Android 操作系统,后者负责找到目的地并交付有效载荷。

将光标放在key_arraylist上,按 Ctrl+Alt+C | Cmd+Alt+C 提取一个常数。选择SplashActivity作为将要定义常量的类,如图 9-23 所示,从建议列表中选择KEY_ARRAYLIST,按回车键在该类中创建一个常量。

A978-1-4302-6602-0_9_Fig23_HTML.jpg

图 9-23。

Select SplashActivity to be the class in which constant will be defined

按 Ctrl+K | Cmd+K 并提交一条带有意图的 Fires-up MainActivity 消息,然后将 ArrayList 传递给 Bundle。

摘要

在这一章中,我们描述了货币应用规范,并着手实现它的一些特性。我们定义了布局,提取了颜色,创建并应用了样式。我们还介绍了 JSON,并创建了一个闪屏来获取活动货币代码,这些代码是填充主活动的微调器所必需的。我们引入了AsyncTask并从 web 服务中获取 JSON 数据。我们还使用意图在活动之间进行交流。在下一章中,我们将完成货币应用。

十、货币 Lab:第二部分

在前一章中,您通过在SplashActivity中使用一个AsyncTask来获取活动货币代码。您将货币代码加载到一个包中,并将该包附加到目的地为MainActivity的意向中。最后,您将意向发送到 Android 操作系统。

在本章中,您将继续开发货币应用,并专注于MainActivity的功能来完成应用。您将使用一个ArrayAdapter将一个字符串数组绑定到微调器。您将使用 Android Studio 将视图行为的处理委托给封装活动。您还将了解如何使用共享偏好设置以及资源。您将了解 Android 中的并发性,尤其是如何使用AsyncTask。最后,您将修改布局并使用 Android Studio 生成可绘制的资源。

定义主活动的成员

让我们从定义对应于activity_main.xml布局文件中视图的MainActivity类中的引用开始,然后给它们分配对象。打开MainActivity.javaactivity_main.xml文件,这样你可以参考这两个文件。右键单击activity_main.xml选项卡,选择右移,将activity_main.xml的模式改为文本。修改您的MainActivity.java文件,使其看起来如图 10-1 所示,并根据需要通过按 Alt+Enter 来解决任何导入。

A978-1-4302-6602-0_10_Fig1_HTML.jpg

图 10-1。

Define members and assign references to these members

注意,我们在MainActivity中只为那些在activity_main.xml中的视图定义了引用,我们之前已经为这些视图分配了一个 ID。setContentView (R.layout.activity_main);陈述夸大了activity_main.xml中包含的观点。在 Android 中,world inflate 意味着当 Android 遍历activity_main.xml布局中定义的视图时,Android 会将每个视图实例化为堆中的一个 Java 对象。如果那个View对象有一个 id,Android 会将那个对象的内存位置与其 ID 关联起来。这种关联可以在一个名为R.java的自动生成文件中找到,它在您的资源和 Java 源文件之间起到了桥梁的作用。

一旦布局和它的所有视图都被放入内存空间,我们就可以通过调用findViewById()方法并传递一个 ID 来将这些对象分配给我们之前定义的引用。findViewById()方法返回一个View对象,它是 Android 中所有ViewsViewGroups的层次祖先;这就是为什么我们需要将每个对findViewById()的调用转换到适当的View子类。按 Ctrl+K | Cmd+K 并提交,同时显示获取对布局中定义的视图的引用的消息。

从捆绑包中解包货币代码

在前一章中,我们将一个StringsArrayList传递到用于启动MainActivity的 intent 包中。虽然 Android OS 已经成功交付了它的有效载荷,但我们仍然需要拆开它。我们在SplashActivity中使用的数据结构是一个向量(ArrayList<String>,这意味着它可以根据需要增长和收缩。我们将在MainActivity中用来存储活动货币代码的数据结构将是一个长度固定的简单字符串数组。改变数据结构的原因是我们将使用ArrayAdapter作为微调器的控制器,而ArrayAdapter使用数组,而不是ArrayLists。修改MainActivity类,使其看起来如图 10-2 所示,并根据需要解析任何导入。

A978-1-4302-6602-0_10_Fig2_HTML.jpg

图 10-2。

Unpack currency codes from ArrayList

语句ArrayList<String> arrayList = ((ArrayList<String>) getIntent().getSerializableExtra(SplashActivity.KEY_ARRAYLIST));ArrayList<String>从用于启动此活动的意向包中解包。注意,我们使用同一个公共常量作为键(SplashActivity.KEY_ARRAYLIST)来对MainActivity中的ArrayList<String>进行解包,我们之前使用这个常量来对SplashActivity中的ArrayList<String>进行打包。还要注意,我们使用了Collections接口对数据进行排序,然后我们将ArrayList<String>转换成一个字符串数组。按 Ctrl+K | Cmd+K 并提交一条从包中解包货币代码的消息。

创建选项菜单

新建项目向导为我们创建了一个名为menu_main.xml的菜单。按 Ctrl+Shift+N | Cmd+Shift+O,键入 main,选择res/menu/menu_main.xml打开。修改menu_main.xml,使其看起来像清单 10-1 。

Listing 10-1. Modify the menu_main.xml File

<menu xmlns:android="``http://schemas.android.com/apk/res/android

xmlns:app="``http://schemas.android.com/apk/res-auto

xmlns:tools="``http://schemas.android.com/tools

tools:context=".MainActivity">

<item

android:id="@+id/mnu_codes"

android:orderInCategory="100"

app:showAsAction="never"

android:title="search active codes"/>

<item

android:id="@+id/mnu_invert"

android:orderInCategory="200"

app:showAsAction="never"

android:title="invert codes"/>

<item

android:id="@+id/mnu_exit"

android:orderInCategory="300"

app:showAsAction="never"

android:title="exit"/>

</menu>

app:showAsAction属性决定了菜单项的位置。将该属性设置为never意味着该菜单项永远不会出现在动作栏上,而总是出现在溢出菜单中。溢出菜单由操作栏右侧的三个垂直圆点表示。

android:orderInCategory用于设置菜单项的顺序。Android 中的惯例是使用 100 的倍数,例如,我们可以使用 250 在 200 和 300 之间插入一个新的菜单项,使用 225 在 200 和 250 之间插入一个新的菜单项。orderInCategory属性值必须是一个整数,所以如果我们从 2 和 3 这样的连续值开始,将没有空间插入中间值,我们将不得不对整个集合重新排序。

请注意,我们为每个菜单项分配了一个 ID,这样我们以后就可以在 Java 代码中引用这些对象。打开MainActivity.java,改变onOptionsItemSelected()方法,如清单 10-2 所示。

Listing 10-2. Modify the onOptionsItemSelected( ) Method

public boolean onOptionsItemSelected(MenuItem item) {

int id = item.getItemId();

switch (id){

case R.id.mnu_invert:

//TODO define behavior here

break;

case R.id.mnu_codes:

//TODO define behavior here

break;

case R.id.mnu_exit:

finish();

break;

}

return true;

}

注意,除了 Exit 菜单项之外,我们用 TODOs 代替了实现代码。我们将在下一步实现其余选项菜单项的功能。按 Ctrl+K | Cmd+K 并提交一条创建选项菜单的消息。

实施选项菜单行为

在本节中,我们将编写需要权限的代码。如果你是一个 Android 用户,那么你可能很熟悉在安装一个应用之前你必须同意的一系列权限。一些应用比其他应用需要更多权限,但大多数应用至少需要一个权限。在前面的步骤中,我们请求用户允许我们访问互联网。在这一步中,我们将向用户请求访问设备网络状态的权限。很容易忽略权限,尤其是如果你是一个新手 Android 程序员。幸运的是,如果您忘记包含适当的权限,与这个问题相关的异常是简单明了的。

要打开AndroidManifest.xml文件,按 Ctrl+Shift+N | Cmd+Shift+O,键入和,按 Enter 选择AndroidManifest.xml打开。修改AndroidManifest.xml插入图 10-3 中高亮显示的线。

A978-1-4302-6602-0_10_Fig3_HTML.jpg

图 10-3。

Add permission to access the network state in the AndroidManifest.xml file

打开MainActivity.java类。定义清单 10-3 中的三种方法。isOnline()方法检查用户是否有互联网连接。这个方法使用的是 Android ConnectivityManager,这就是为什么我们需要将android.permission.ACCESS_NETWORK_STATE添加到AndroidManifest.xml文件中。launchBrowser()方法接受一个表示统一资源标识符(URI)的字符串。URI 是统一资源定位器(URL)的超集,因此任何定义为有效 HTTP 或 HTTPS 地址的字符串都可以作为参数使用。launchBrowser()方法启动设备上的默认浏览器,并打开我们传递给它的 URI。invertCurrencies()方法简单地交换本币和外币微调器的值。当然,如果包含计算结果的TextView先前已经填充了数据,我们也需要清除它以避免任何混淆。把你的新方法放在onCreate()方法下面。

Listing 10-3. Create Three Methods in MainActivity.java

public boolean isOnline() {

ConnectivityManager cm =

(ConnectivityManager)

getSystemService(Context.CONNECTIVITY_SERVICE);

NetworkInfo networkInfo = cm.getActiveNetworkInfo();

if (networkInfo != null && networkInfo.isConnectedOrConnecting()) {

return true;

}

return false;

}

private void launchBrowser(String strUri) {

if (isOnline()) {

Uri uri = Uri.parse(strUri);

//call an implicit intent

Intent intent = new Intent(Intent.ACTION_VIEW, uri);

startActivity(intent);

}

}

private void invertCurrencies() {

int nFor = mForSpinner.getSelectedItemPosition();

int nHom = mHomSpinner.getSelectedItemPosition();

mForSpinner.setSelection(nHom);

mHomSpinner.setSelection(nFor);

mConvertedTextView.setText("");

}

按照清单 10-4 用方法调用替换MainActivity.java文件的onOptionsItemSelected()方法中的 TODOs。按 Ctrl+K | Cmd+K 并提交一条实现选项菜单行为和修改清单文件的消息。

Listing 10-4. Replace TODOs in onOptionsItemSelected( ) Method with Calls to the Methods We Just Defined

case R.id.mnu_invert:

invertCurrencies();

break;

case R.id.mnu_codes:

launchBrowser(SplashActivity.URL_CODES);

break;

创建 spinner_closed 布局

当微调器处于关闭状态时,为其创建布局。右键单击(在 Mac 上按住 Ctrl 键)目录并选择“新建➤布局资源文件”。将文件命名为 spinner_closed,点击确定,如图 10-4 所示。

A978-1-4302-6602-0_10_Fig4_HTML.jpg

图 10-4。

Define the spinner_closed layout resource file

修改spinner_closed.xml文件,如清单 10-5 所示。

Listing 10-5. Definition of spinner_closed.xml

<TextView xmlns:android="``http://schemas.android.com/apk/res/android

android:id="@android:id/text1"

android:background="@color/grey_very_dark"

android:textColor="@color/grey_light"

android:singleLine="true"

android:textSize="18sp"

android:layout_width="match_parent"

android:layout_height="fill_parent"

android:gravity="center_vertical"

android:ellipsize="marquee"

/>

将货币绑定到旋转器

本币微调器和外币微调器将显示相同的项目。我们需要将mCurrencies数组绑定到两个微调器。为此,我们将使用一个名为ArrayAdapter的类。在MainActivity.javaonCreate()方法中,添加如图 10-5 所示的代码,解析导入。

A978-1-4302-6602-0_10_Fig5_HTML.jpg

图 10-5。

Bind mCurrencies to spinners

ArrayAdapter构造函数有三个参数:一个上下文、一个布局和一个数组。ArrayAdapter充当模型-视图-控制器设计模式中的控制器,并协调模型和视图之间的关系。在我们的例子中,模型是名为mCurrencies的字符串数组。mCurrencies中的每个元素都包含一个货币代码、一个提供视觉分隔的管道字符和一个货币描述。微调器有两个视图:一个视图在微调器打开时显示,另一个视图在微调器关闭时显示。最后两条语句将新构建的arrayAdapter对象分配给微调器。按 Ctrl+K | Cmd+K 并提交一条将数据绑定到微调器的消息。通过按下 Shift+F10 | Ctrl+R 运行您的应用,并与两个微调器进行交互,以确保它们正常工作。

将微调器行为委托给 MainActivity

Java 事件模型非常灵活。我们可以将事件的处理委托给任何实现适当侦听器接口的对象。如果一个视图是惟一的,比如 Calculate 按钮,那么将其行为的处理委托给一个匿名的内部类是有意义的。然而,如果我们的布局包含相同类型的多个视图,比如货币应用中的两个或更多微调器,那么通常更容易将这些视图行为的处理委托给封闭类。将两行代码添加到MainActivity.javaonCreate()方法的末尾,如图 10-6 所示。单词this将以红色下划线标出,表示编译时错误。

A978-1-4302-6602-0_10_Fig6_HTML.jpg

图 10-6。

Delegate the behavior of spinners to MainActivity

将光标放在任一单词this上的任意位置,按 Alt+Enter 调用 IntelliSense 代码完成。选择第二个选项(使' MainActivity '实现' Android . widget . adapter view . onitemselectedlistener '),如图 10-7 所示。在如图 10-8 所示的选择要实现的方法对话框中选择两种方法,然后点击确定。如果你向上滚动到类的顶部,你会注意到MainActivity现在实现了AdapterView.OnItemSelectedListener

A978-1-4302-6602-0_10_Fig8_HTML.jpg

图 10-8。

Select Methods to Implement dialog box

A978-1-4302-6602-0_10_Fig7_HTML.jpg

图 10-7。

Make ‘MainActivity’ implement OnItemSelectedListener

OnItemSelectedListener接口有两个任何实现类都必须覆盖的收缩方法:onItemSelected()onNothingSelected()。我们不会在onNothingSelected()方法的主体中提供任何实现代码。虽然onNothingSelected()是一个鼻涕虫,但是它必须出现在MainActivity里面才能满足接口契约。

onItemSelected()方法中,我们需要通过检查parent.getId()来确定选择了哪个微调器,然后添加一些条件逻辑来编程所选微调器的行为。修改onItemSelected()方法,如图 10-9 所示。

A978-1-4302-6602-0_10_Fig9_HTML.jpg

图 10-9。

Modify the onItemSelected( ) method

请注意,我们将占位符注释(//define behavior here)放在了我们期望实现代码所在的位置。我们将在后续步骤中实现 spinners 的行为。按 Ctrl+K | Cmd+K 并提交一条消息,将 spinners 行为的处理委托给 MainActivity。

创建首选项管理器

共享偏好设置提供了一种在应用退出时保留用户偏好设置的方法。如果我们试图将用户的偏好存储在内存中,这些数据将在用户退出应用后被刷新,应用的内存将被 Android 操作系统回收。为了解决这个问题,可以将共享偏好存储在用户设备上的文件中。这个文件是一个带有键/值对的序列化哈希映射,每个应用可能都有自己的共享首选项。

可以存储在共享首选项中的值的类型仅限于 Java 原语、字符串、序列化对象和序列化对象数组。与向 SQLite 数据库读写数据相比,共享首选项速度较慢。因此,您不应该考虑使用共享首选项作为记录管理的替代方案;您应该始终使用 SQLite 数据库进行记录管理,就像您在 Reminders 实验中看到的那样。然而,共享偏好是保持用户偏好的一个好方法。

我们希望保留本币和外币微调器中显示的货币代码。这是一个典型的场景。假设一个美国用户正在伊斯坦布尔度假,在露天市场使用货币应用争论一些珍贵的拜占庭古董。用户退出应用并返回酒店。第二天早上,他在当地一家餐馆吃早餐,然后启动货币应用查看账单。如果我们的用户在执行另一个计算之前不得不在微调器中重新选择 TRY 和 USD,这将是非常令人沮丧的。相反,微调器应该自动填充先前为本币和外币选择的代码。

我们将创建一个实用程序类,让我们能够访问共享的首选项。我们的实用程序类将具有公共静态方法,允许我们获取和设置用户为本国货币和外国货币选择的货币代码。右键单击(在 Mac 上按住 Ctrl 键)这个com.apress.gerber.currencies包并选择 New Java Class。将您的类命名为 PrefsMgr,并插入如图 10-10 所示的代码。

A978-1-4302-6602-0_10_Fig10_HTML.jpg

图 10-10。

Create the PrefsMgr class

setString()方法为国内或国外的特定地区设置货币代码。getString()方法将返回为特定地区存储的货币代码值,如果没有找到代码,那么默认情况下将返回null。按 Ctrl+K | Cmd+K 并提交一条创建我们自己的首选项管理器的消息。

查找给定代码的位置

微调器使用从零开始的整数来表示其当前位置的值。要将微调器设置为特定代码,我们需要找到元素的适当位置或索引。由于mCurrencies被用作微调器的模型,我们可以简单地将货币代码与存储在mCurrencies中的聚合字符串的前三个字符进行比较。如果找到匹配,我们返回索引位置。如果没有找到匹配,我们返回到零,这对应于微调器的第一个位置。ISO 4217 货币代码标准规定货币代码的长度总是三个字母。让我们编写一个方法,从包含货币代码、管道字符和货币描述的聚合字符串中提取三个字母的货币代码。我们知道这个聚合字符串的前三个字符是货币代码,所以我们可以使用 string 的 substring()方法来提取它。打开MainActivity.java,在 invertCurrencies()方法下定义 findPositionGivenCode()方法,如图 10-11 所示。按 Ctrl+K | Cmd+K 并提交一条创建查找位置给定代码方法的消息。

A978-1-4302-6602-0_10_Fig11_HTML.jpg

图 10-11。

Create the findPositionGivenCode() method

从货币中提取代码

从存储在mCurrencies的每个元素中的聚合字符串中提取三个字母的货币代码将不限于findPositionGivenCode()方法。与其在别处复制这些代码,不如提取一个方法,然后在需要它的功能时调用这个方法。在MainActivity.java中,高亮显示如图 10-12 所示的代码,按 Ctrl+Alt+M | Cmd+Alt+M 提取一个方法,选择第一个选项。

A978-1-4302-6602-0_10_Fig12_HTML.jpg

图 10-12。

Select the code that will be extracted as a method

在提取方法对话框中,将方法名称改为 extractCodeFromCurrency,如图 10-13 所示,点击确定。你最终应该得到类似图 10-14 的东西。按 Ctrl+K | Cmd+K 并提交名为 extractCodeFromCurrency 的消息提取方法。

A978-1-4302-6602-0_10_Fig14_HTML.jpg

图 10-14。

Resulting code from the extract method operation

A978-1-4302-6602-0_10_Fig13_HTML.jpg

图 10-13。

Create extractCodeFromCurrency( ) in the Extract Method dialog box

实施共享偏好设置

shared preferences 中的数据存储在一个 hash-map 中,其中的键总是字符串,因此这是一个将键定义为String常量的绝佳机会。打开MainActivity.java,定义如图 10-15 所示的两个String常量。

A978-1-4302-6602-0_10_Fig15_HTML.jpg

图 10-15。

Define two constants that will be used as keys

在你的MainActivity类的onCreate()方法的末尾插入如图 10-16 所示的if/else块。在前面的步骤中,我们对PrefsMgr类进行了编程,以便在找不到键的情况下返回nullif块检查本币和外币密钥是否都不存在。这种独特的情况只会发生一次,即首次在用户设备上使用该应用时,微调器将分别设置为 CNY 和美元作为外币和本币。如果不满足该唯一条件,微调器将被设置为存储在用户共享首选项中的值。

A978-1-4302-6602-0_10_Fig16_HTML.jpg

图 10-16。

Create the if/else block

使用共享偏好设置会对性能造成轻微影响,我们希望尽可能避免这种影响。我们在我们的if语句的括号内包含了savedInstanceState == null &&,以便在MainActivity简单地从中断或配置改变中恢复的情况下,这个块将会短路。

导航到我们之前定义的onItemSelected()方法。修改此方法,以便我们每次在一个微调器中选择一个项目时都设置共享首选项。此外,我们将清除mConvertedTextView以避免任何混淆。修改MainActivity.java如图 10-17 所示。

A978-1-4302-6602-0_10_Fig17_HTML.jpg

图 10-17。

Apply shared preferences to the onItemSelected method

最后,我们需要确保当用户从 options 菜单中选择 Invert Currencies 菜单项时,正确设置了共享的首选项。将如图 10-18 所示的两行代码添加到invertCurrencies()方法的末尾。按 Ctrl+K | Cmd+K 并提交一条实现共享首选项的消息。

A978-1-4302-6602-0_10_Fig18_HTML.jpg

图 10-18。

Apply shared preferences to the invertCurrencies method

按钮点击行为

我们的 app 里只有一个按钮。因此,将按钮行为的处理委托给一个匿名的内部类,而不是委托给封闭活动,这是有意义的,就像我们前面对两个 spinners 所做的那样。

onCreate()方法的末尾,但仍在它的大括号内,键入 mcalcbutton . setonclicklistener();现在将光标放在该方法的括号内,并键入 new On。如有必要,使用向下箭头键从代码完成提供给您的建议中选择onClickListener{...}选项,然后按 Enter。在onClick()方法中添加一些占位符文本如//define behavior,如图 10-19 所示。按 Ctrl+K | Cmd+K 并提交一条消息,提示创建一个内部类来处理按钮行为。

A978-1-4302-6602-0_10_Fig19_HTML.jpg

图 10-19。

Create an anonymous inner class to handle button click behavior

存储开发者密钥

在项目工具窗口中右键单击app,选择新建➤文件夹➤素材文件夹。在随后的对话框中,默认情况下,目标源集选项应该是 main。单击完成。

在项目工具窗口中右键单击新创建的assets目录,并选择新建➤文件。命名新的文件密钥。属性,如图 10-20 所示。

A978-1-4302-6602-0_10_Fig20_HTML.jpg

图 10-20。

Create the keys.properties file

将下面一行添加到keys.properties文件中:

open_key=9a894f5f4f5742e2897d20bdcac7706a

您需要通过将浏览器导航到 https://openexchangerates.org/signup/free 来注册您自己的免费密钥。这个过程很简单,大约需要 30 秒。用你自己的有效密钥代替我们在这里提供的假密钥。参见图 10-21 。按 Ctrl+K | Cmd+K 并提交一条定义 openexchangerates.org 键的消息。

A978-1-4302-6602-0_10_Fig21_HTML.jpg

图 10-21。

Define open_key in the keys.properties file. The key provided here is a placeholder and will not work Note

我们提供的密钥 9a 894 F5 F4 f 5742 e 2897d 20 bdcac 7706 a 无法工作;它只是一个占位符。您需要在浏览器中导航到 https://openexchangerates.org/signup/free 来注册您自己的密钥,然后用您自己的有效密钥替换假密钥。

获取开发者密钥

MainActivity.java中定义一个方法,在extractCodeFromCurrency()方法下获取存储在keys.properties中名为getKey()的密钥。注意,我们使用AssetManagerkeys.properties读取一个密钥。您将需要根据需要解析导入。见图 10-22 。

A978-1-4302-6602-0_10_Fig22_HTML.jpg

图 10-22。

Define the getKey( ) method

文件 I/O 是一项开销很大的操作。我们在上一步中定义的getKey()方法包含这样的操作,所以我们希望尽可能少地调用getKey()。我们将在onCreate()中调用一次这个调用,然后将这个值存储在MainActivity的一个名为mKey的成员中,而不是每次我们想从openexchangerates.org中获取利率时都调用getKey()。定义你的MainActivity类的成员,如图 10-23 所示。

A978-1-4302-6602-0_10_Fig23_HTML.jpg

图 10-23。

Define members to facilitate fetching key and formatting results

onCreate()方法的末尾,但仍在它的括号内,给mKey赋值,如下所示:mKey = getKey("open_key");。参见图 10-24 。按 Ctrl+K | Cmd+K 并提交一条获取键、定义成员和常量的消息。

A978-1-4302-6602-0_10_Fig24_HTML.jpg

图 10-24。

Assign the key as the last statement of the onCreate( ) method

CurrencyConverterTask

线程是一个轻量级进程,它可以与同一应用中的其他线程并发运行。Android 并发性的第一条规则是你不能阻塞 UI 线程,它也称为主线程。UI 线程是默认情况下在应用启动期间产生的线程,它驱动用户界面。如果 UI 线程被阻塞超过 5000 毫秒,Android 操作系统将显示应用不响应(ANR)错误,您的应用将崩溃。阻塞 UI 线程不仅会导致 ANR 错误,而且当 UI 线程被阻塞时,用户界面将完全没有响应。因此,如果一个操作可能需要几毫秒以上的时间,那么它可能会阻塞 UI 线程,应该在后台线程上完成。例如,尝试从远程服务器获取数据可能会持续几毫秒以上,应该在后台线程上完成。当在 Android 环境中使用时,术语后台线程是指除 UI 线程之外的任何线程。

Note

UI 线程有时被称为主线程。

Android 并发的第二个规则是 UI 线程是唯一有权限与用户界面交互的线程。如果你试图从后台线程更新任何视图,你的应用将立即崩溃!违反 Android 并发规则中的一个或两个将导致糟糕的用户体验。

没有什么可以阻止你在你的 Android 应用中生成优秀的 Java 线程,但是一个名为AsyncTask的类是专门为解决本节描述的问题而设计的,因此它是 Android 并发的首选实现。如果你正确实现了AsyncTask,遵循 Android 并发的两条规则就没有问题。

在本节中,我们将创建一个名为CurrencyConverterTask的内部类,它将用于获取openexchangerates.org. CurrencyConverterTask上引用的汇率。它是抽象类AsyncTask. AsyncTask的一个具体实现,有一个名为doInBackground()的抽象方法,所有具体类都需要覆盖它。此外,您还可以覆盖其他一些方法,包括onPreExecute()onProgressUpdate()onPostExecute()等等。AsyncTask的神奇之处在于doInBackground()方法是在后台线程上执行的,而AsyncTask的其余方法是在 UI 线程上执行的。如果我们不接触doInBackground()方法中的任何视图,AsyncTask使用起来是完全安全的。

CurrencyConverterTask定义为私有内部类,在MainActivity.java的末尾,但仍在MainActivity的括号内。除了扩展AsyncTask,还必须定义三个通用对象参数,如图 10-25 所示。解决任何导入。即使在您解析了导入之后,您的类定义仍将带有红色下划线,表明存在编译时错误。暂时忽略这个。

A978-1-4302-6602-0_10_Fig25_HTML.jpg

图 10-25。

Define CurrencyConverterTask

将光标放在CurrencyConverterTask类定义的花括号内,按 Alt+Insert | Cmd+N,并选择 Override Methods。选择doInBackground()onPreExecute()onPostExecute()方法,点击确定,如图 10-26 所示。注意,返回值以及参数doInBackground()onPostExecute()是根据通用参数<String, Void, JSONObject>定义的。第一个参数(String)用作doInBackground()方法的输入,第二个参数(Void)用于向onProgressUpdate()方法发送进度更新,第三个参数(JSONObject)是doInBackground()的返回值,也是onPostExecute()方法的输入参数。整个获取操作大约需要一秒钟,因此用户几乎察觉不到进度更新;这就是为什么我们省略了onProgressUpdate()方法并使用Void作为第二个参数。

A978-1-4302-6602-0_10_Fig26_HTML.jpg

图 10-26。

Select methods to override/implement

让我们重新排列我们的方法,使它们按照被触发的顺序出现。选择整个onPreExecute()块,包括@Override注释,按 Ctrl+Shift+Up | Cmd+Shift+Up 将onPreExecute()方法移动到doInBackground()方法之上。你的CurrencyConverterTask现在应该看起来如图 10-27 所示。

A978-1-4302-6602-0_10_Fig27_HTML.jpg

图 10-27。

Results after overriding methods in CurrencyConverterTask and moving onPreExecute() up

再次修改CurrencyConverterTask,使其看起来像清单 10-6 并解析任何导入。让我们依次讨论一下CurrencyConverterTask的三个被覆盖的方法。

Listing 10-6. Modify the CurrencyConverterTask

private class CurrencyConverterTask extends AsyncTask<String, Void, JSONObject> {

private ProgressDialog progressDialog;

@Override

protected void onPreExecute() {

progressDialog = new ProgressDialog(MainActivity.this);

progressDialog.setTitle("Calculating Result...");

progressDialog.setMessage("One moment please...");

progressDialog.setCancelable(true);

progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE,

"Cancel", new DialogInterface.OnClickListener() {

@Override

public void onClick(DialogInterface dialog, int which) {

CurrencyConverterTask.this.cancel(true);

progressDialog.dismiss();

}

});

progressDialog.show();

}

@Override

protected JSONObject doInBackground(String... params) {

return new JSONParser().getJSONFromUrl(params[0]);

}

@Override

protected void onPostExecute(JSONObject jsonObject) {

double dCalculated = 0.0;

String strForCode =

extractCodeFromCurrency(mCurrencies[mForSpinner.getSelectedItemPosition()]);

String strHomCode = extractCodeFromCurrency(mCurrencies[mHomSpinner.getSelectedItemPosition()]);

String strAmount = mAmountEditText.getText().toString();

try {

if (jsonObject == null){

throw  new JSONException("no data available.");

}

JSONObject jsonRates = jsonObject.getJSONObject(RATES);

if (strHomCode.equalsIgnoreCase("USD")){

dCalculated = Double.parseDouble(strAmount) / jsonRates.getDouble(strForCode);

} else if (strForCode.equalsIgnoreCase("USD")) {

dCalculated = Double.parseDouble(strAmount)  * jsonRates.getDouble(strHomCode) ;

}

else {

dCalculated = Double.parseDouble(strAmount) *  jsonRates.getDouble(strHomCode)

/ jsonRates.getDouble(strForCode)   ;

}

} catch (JSONException e) {

Toast.makeText(

MainActivity.this,

"There's been a JSON exception: " + e.getMessage(),

Toast.LENGTH_LONG

).show();

mConvertedTextView.setText("");

e.printStackTrace();

}

mConvertedTextView.setText(DECIMAL_FORMAT.format(dCalculated) + " " + strHomCode);

progressDialog.dismiss();

}

}

onPreExecute()

在触发doInBackground()方法之前,在 UI 线程上执行onPreExecute()方法。由于我们可能不会从后台线程接触 UI 中的任何视图,onPreExecute()方法代表了在doInBackground()被触发之前修改 UI 的机会。当onPreExecute()被调用时,会出现一个ProgressDialog,用户可以选择按下取消按钮并终止操作。

doInBackground()

doInBackground()方法是AsyncTaskexecute()方法的代理。例如,调用CurrencyConverterTask的最简单方法是实例化一个新的引用匿名对象,并像这样调用它的execute()方法:

new CurrencyConverterTask().execute("url_to_web_service");

您传递给execute()的参数将依次传递给doInBackground(),但不是在执行onPreExecute()之前。我们doInBackground()的全称是protected JSONObject doInBackground(String... params)doInBackground()的参数被定义为 varargs,因此我们可以向execute()中传递尽可能多的逗号分隔的类型为String的参数,尽管在这个简单的应用中我们只传递一个——URL 的字符串表示。在doInBackground()方法中,params被视为一个字符串数组。为了引用第一个(也是唯一的)元素,我们使用params[0]

doInBackground()的体内,我们称之为return new JSONParser().getJSONFromUrl(params[0]);getJSONFromUrl()方法从 web 服务中获取一个JSONObject。因为这个操作需要用户设备和远程服务器之间的通信——因此可能需要几毫秒以上的时间——我们将getJSONFromUrl()放在了doInBackground()方法中。getJSONFromUrl()方法返回一个JSONObject,它是为doInBackground()定义的返回值。如前所述,doInBackground()是 AsyncTask 唯一运行在后台线程上的方法,其他所有方法都运行在 UI 线程上。注意,我们在doInBackground()方法中没有触及任何视图。

onPostExecute()

onPreExecute()一样,onPostExecute()方法运行在 UI 线程上。doInBackground()的返回值被定义为JSONObject。这个相同的对象将作为参数传递给onPostExecute()方法,该方法的完整签名被定义为protected void onPostExecute(JSONObject jsonObject)。当我们进入onPostExecute()方法时,doInBackground()方法的后台线程已经终止,我们现在可以用从doInBackground()获取的JSONObject数据安全地更新 UI。最后,我们进行一些计算,并将格式化的结果分配给mConvertedTextView

在运行我们的应用之前,我们需要对代码进行最后一次修改,以便执行CurrencyConverterTask。根据图 10-28 修改mCalcButtononClick()方法。

A978-1-4302-6602-0_10_Fig28_HTML.jpg

图 10-28。

Fire the new CurrencyConverterTask in the mCalcButton onClick method

按 Ctrl+K | Cmd+K 并提交一条实现 CurrencyConverterTask 的消息。按 Shift+F10 | Ctrl+R 运行应用。在此处输入外币金额字段中输入金额,然后单击计算按钮。您应该从服务器返回一个结果,这个结果应该显示在以本币计算的结果字段中。如果您的应用未能返回结果,请验证您是否拥有来自openexchangerates.org的有效开发者密钥。

按钮选择器

当您运行您的货币应用时,您可能已经注意到在mConvertedTextView中显示的文本是黑色的,这不能提供足够的对比度。打开activity_main.xml文件,插入图 10-29 中高亮显示的行,修改txt_converted TextView的定义。

A978-1-4302-6602-0_10_Fig29_HTML.jpg

图 10-29。

Insert the textColor attribute of txt_converted and set to @color/white in activity_main.xml

右键单击(在 Mac 上按住 Ctrl 键单击)可绘制目录,并选择“新建➤可绘制资源文件”。将资源命名为 button_selector,如图 10-30 所示。修改 XML,使其看起来如图 10-31 所示。根据图 10-32 更改activity_main.xmlbtn_calc的定义。

A978-1-4302-6602-0_10_Fig32_HTML.jpg

图 10-32。

Modify the btn_calc in activity_main.xml

A978-1-4302-6602-0_10_Fig31_HTML.jpg

图 10-31。

Modify the button_selector resource file

A978-1-4302-6602-0_10_Fig30_HTML.jpg

图 10-30。

Create the button_selector resource file

按 Ctrl+K | Cmd+K 并提交一条创建按钮选择器的消息。

启动器图标

我们将定义自己的图标,而不是使用普通的 Android 图标作为启动图标。我冒昧地使用先进的谷歌图片搜索找到了一枚一欧元硬币的免版税图片,这是流通中最好的硬币之一。你可以在这里找到这个形象: http://pixabay.com/static/uploads/photo/2013/07/13/01/21/coin-155597_640.png

下载这张图片,并将其命名为 coin.png。将您的项目工具窗口切换到 Android 视图。右键单击(在 Mac 上按住 Ctrl 键单击)res/mipmap 目录,然后选择“新建➤图像素材”。在随后的对话框中,选择可绘制目录作为目标目录。使用图 10-33 中的设置为每个分辨率创建ic_launcher.png文件,然后点击下一步并完成。将以下代码行插入到MainActivityonCreate()方法中,在展开布局的代码行setContentView(R.layout.activity_main)之后;。这段代码在您的操作栏中显示一个自定义图标:

A978-1-4302-6602-0_10_Fig33_HTML.jpg

图 10-33。

Create ic_launcher icons

ActionBar actionBar = getSupportActionBar();

actionBar.setHomeButtonEnabled(true);

actionBar.setDisplayShowHomeEnabled(true);

actionBar.setIcon(R.mipmap.ic_launcher);

这个应用的图标现在将是一个一欧元硬币,而不是一个标准的 Android 图标。按 Ctrl+K | Cmd+K 并提交一条创建启动器图标的消息。

摘要

本章展示了 Android 如何扩大视图,以及R.java文件如何充当资源和 Java 源文件之间的桥梁。您学习了如何从包中解包一个值,并实现了菜单和编码它们的行为。您使用了一个ArrayAdapter将一个字符串数组绑定到微调器。您还了解了如何使用 Android Studio 将视图事件的处理委托给封闭活动。您学习了如何使用共享的偏好设置和资源。您学习了 Android 中的并发性——特别是关于AsyncTask的方法。您还实现了自己的CurrencyConverterTask,它从openexchangerates.org web 服务中获取货币汇率。最后,您使用 Android Studio 生成图像资源,并创建了一个按钮选择器。

我们已经完成了上一章开始的货币应用。通过按下 Shift+F10 | Ctrl+R 运行您的应用,并确保其正常运行。如果你是一个经验丰富的 Android 开发人员,或者只是一个特别好奇的 UI 测试人员,你可能会注意到有一个角落情况会导致应用崩溃。我们将把这个 bug 留在原处,并在第十一章中修复它,这一章专门用于分析和测试。

十一、测试和分析

测试是任何软件开发生命周期中的关键阶段。在一些商店中,质量保证团队负责编写和维护测试,而在其他商店中,开发团队必须执行这项任务。在这两种情况下,随着应用变得越来越复杂,测试的需求也变得越来越重要。测试允许团队成员识别应用的功能问题,这样他们就可以放心地继续工作,因为他们知道他们在源代码中所做的任何更改都不会导致运行时错误、错误的输出和意外的行为。当然,即使是最彻底的测试也不能消除所有的错误,但是测试是软件开发团队的第一道防线。

测试在软件开发人员中是一个有争议的问题。所有开发人员可能都会同意需要进行一些测试。然而,有些人认为测试是如此重要,以至于它应该在开发阶段之前(一种被称为测试驱动开发的方法),而在其他商店,特别是初创公司,有些人试图创建一个最小可行的产品,因此认为测试是一种潜在的浪费,只能有节制地进行。无论你对测试的看法如何,我们鼓励你熟悉本章所涉及的技术,包括android.test库中的类,以及 Android Studio 和 Android SDK 附带的工具。

我们选择了那些我们认为对 Android 开发者最有用的工具。在这一章中,我们介绍仪器测试;然后向您展示 Monkey,它是 Android SDK 附带的一个优秀工具,可以生成随机 UI 事件,用于对您的应用进行压力测试;最后,我们将向您展示 Android Studio 中的一些分析工具。

Tip

有一个很好的第三方测试框架叫做 Roboelectric。虽然 Roboelectric 并没有为我们在这里讨论的 Android SDK 测试框架提供任何明显的好处,但它仍然受到 Android 开发人员的欢迎。你可以在这里找到更多关于 Roboelectric 的信息:robo electric。org

创建新的检测测试

仪器测试允许您在设备上执行操作,就像人类用户在操作它一样。在本节中,您将通过扩展android.test.ActivityInstrumentationTestCase2类来创建一个插装测试。

从第十章中打开货币项目,将你的项目工具窗口切换到 Android 视图。在项目工具窗口中,右键单击(在 Mac 上按住 Ctrl 键并单击)该com.apress.gerber.currencies(androidTest)包,然后选择“新建➤ Java 类”。将您的类命名为 MainActivityTest,扩展ActivityInstrumentationTestCase2<MainActivity>.定义一个构造函数,如图 11-1 所示。您会注意到ActivityInstrumentationTestCase2<>的泛型参数是MainActivity,这是这里测试的活动。

A978-1-4302-6602-0_11_Fig1_HTML.jpg

图 11-1。

Define a class called MainActivityTest, which extends ActivityInstrumentationTestCase2

定义 SetUp()和 TearDown()方法

将光标放在MainActivityTest的类范围内,再次按 Alt+Insert | Cmd+N 调用生成上下文菜单,如图 11-2 所示。选择设置方法,然后按 Enter 键。对拆卸方法重复此过程。框架代码应该如图 11-3 所示。setUp()tearDown()方法是该仪器测试的生命周期方法。setUp()方法为您提供了一个机会,可以连接到任何需要的资源,通过一个包传递任何数据,或者在运行测试之前分配引用。在测试方法运行之后,tearDown()方法可以用来关闭任何连接和清理任何资源。

A978-1-4302-6602-0_11_Fig3_HTML.jpg

图 11-3。

SetUp and TearDown skeleton code

A978-1-4302-6602-0_11_Fig2_HTML.jpg

图 11-2。

Generate SetUp and TearDown methods

打开MainActivity.java文件,这是我们将要测试的活动,并检查onCreate()方法。在每一项活动中—MainActivity也不例外—onCreate()生命周期法是你获得膨胀视图参考的机会。例如,在MainActivity中,第mCalcButton = (Button) findViewById(R.id.btn_calc);行将找到在堆上实例化并由R.id.bnt_calc ID 标识的视图,将其转换为Button,并将该引用分配给mCalcButton

MainActivityTest中,我们将以几乎完全相同的方式引用MainActivity的视图。然而,由于findViewById()Activity的一个方法,而不是ActivityInstrumentationTestCase2,为了做到这一点,我们需要引用MainActivity。在MainActivityTest中定义一个名为MainActivity mActivity;的引用,以及其他引用,如图 11-4 所示。ActivityInstrumentationTestCase2<MainActivity>类有一个名为getActivity()的方法,该方法返回对MainActivity的引用。在将MainActivity引用传递给MainActivityTest的构造函数时,MainActivity中的视图已经被展开。一旦我们有了这个引用,我们就可以调用mActivity.findViewById()来获取我们的引用,如图 11-4 所示。

A978-1-4302-6602-0_11_Fig4_HTML.jpg

图 11-4。

Define the members and body of the setUp( ) method

按 Ctrl+K | Cmd+K 并提交,同时显示一条消息,提示获取对 MainActivity 中展开视图的引用。请记住,在正常情况下,MainActivity是从SplashActivity启动的,它获取有效货币代码并将其存储在一个ArrayList<String>中,然后将该ArrayList<String>打包成一个包,然后通过一个意向将该包穿梭到MainActivity。我们可以不借助SplashActivity来模拟这一切。重新创建代码,如图 11-5 所示。在第setActivityIntent(intent)行中,我们向MainActivity输入了测试数据——如果MainActivity在正常情况下被SplashActivity调用,它将会得到相同类型的数据。

A978-1-4302-6602-0_11_Fig5_HTML.jpg

图 11-5。

Simulate the work of SplashActivity by passing a loaded intent into MainActivity

在 MainActivity 中定义回调

在大多数情况下,您的插装测试将在 UI 线程上进行,而不需要修改测试中的活动。然而,在我们的例子中,我们希望在CurrencyConverterTask在后台线程上完成工作后测试应用的状态。为此,我们需要在MainActivity中定义一个回调。

打开MainActivity.java,定义实例、接口和设置器,如图 11-6 所示。同样,在CurrencyConverterTaskonPostExecute()方法的最后,根据图 11-7 添加代码。按 Ctrl+K | Cmd+K 并在 MainActivity 中提交定义回调的消息。

A978-1-4302-6602-0_11_Fig7_HTML.jpg

图 11-7。

Add an if block of code to the end of CurrencyConverterTask

A978-1-4302-6602-0_11_Fig6_HTML.jpg

图 11-6。

Define an interface in the MainActivity.java class

定义一些测试方法

返回到MainActivityTest.java。将光标放在类范围内。重新创建名为proxyCurrencyConverterTask()convertToDouble()的方法,如清单 11-1 所示。您需要解决一些导入问题。proxyCurrencyConverterTask()方法允许您用数据填充微调器,模拟单击 Calculate 按钮,并在测试从服务器返回的数据是否准确之前等待来自服务器的响应。

Listing 11-1. Create Method to Simulate CurrencyConverterTask and Wait for Termination

public void proxyCurrencyConverterTask (final String str) throws Throwable {

final CountDownLatch latch = new CountDownLatch(1);

mActivity.setCurrencyTaskCallback(new MainActivity.CurrencyTaskCallback() {

@Override

public void executionDone() {

latch.countDown();

assertEquals(convertToDouble(mConvertedTextView.getText().toString().substring(0, 5)),convertToDouble( str));

}

});

runTestOnUiThread(new Runnable() {

@Override

public void run() {

mAmountEditText.setText(str);

mForSpinner.setSelection(0);

mHomSpinner.setSelection(0);

mCalcButton.performClick();

}

});

latch.await(30, TimeUnit.SECONDS);

}

private double convertToDouble(String str) throws NumberFormatException{

double dReturn = 0;

try {

dReturn = Double.parseDouble(str);

} catch (NumberFormatException e) {

throw e;

}

return dReturn;

}

再次将光标放在类范围内的proxyCurrencyConverterTask()方法下面,然后按 Alt+Insert | Cmd+N 调用生成上下文菜单。选择测试方法并按 Enter 键。将您的方法命名为testInteger()并重新创建如图 11-8 所示的方法,包括将Exception替换为Throwable。对名为testFloat()的测试方法重复这些步骤。

A978-1-4302-6602-0_11_Fig8_HTML.jpg

图 11-8。

Create test methods. Pass a nonnumeric value such as “12..3” or “12,,3” into proxyCurrencyConverterTask( )

在这两种测试方法中,我们将大部分行为委托给了proxyCurrencyConverterTask()方法。请记住,为了让您的测试方法被ActivityInstrumentationTestCase2识别,它必须以小写的test开头。

testInteger()中,我们用整数 12 的字符串表示填充mAmountEditText,并用对应于EUR|Euro的货币数组索引设置mForSpinnermHomSpinner。然后我们通过调用performClick()方法来模拟点击mCalculateButton。我们使用一种叫做CountDownLatch的机制,它被设置为在我们从服务器获取汇率时暂停当前线程。一旦MainActivityCurrencyConverterTask的线程终止,CurrencyConverterTask将调用executionDone(),释放挂起的CountDownLatch,允许ActivityInstrumentationTestCase2继续调用assertEquals()。由于本币和外币都被设置为EUR,输出应该与输入相同。我们在这里创建的插装测试使用了 JUnit 框架;因此,如果assertEquals()方法评估为true,我们的测试将通过。

testFloat()方法中,我们模拟与前面描述的相同的过程,尽管我们用非数字数据填充mAmountEditText( 12..3).尽管我们通过将mAmountEditText的软键盘设置为仅允许数字输入来约束用户,但是我们的用户仍然有可能连续输入两个小数点,这就是我们在这里测试的场景。按 Ctrl+K | Cmd+K,并提交一条创建代理方法的消息。

Note

在某些语言中,逗号被用来代替小数点后面的句号。如果您的设备的默认语言设置为这种语言,您的软键盘将显示逗号而不是句号。您可以简单地测试(12,,3)而不是(12..3).

运行仪器测试

在项目工具窗口中右键单击(在 Mac 上按住 Ctrl 键单击)该MainActivityTest类,并从上下文菜单中选择 Run。您也可以从工具栏中 Run 按钮左侧的组合框中选择 MainActivityTest,然后按 Run 按钮。Android Studio 会显示运行工具窗口,控制台会显示你的进度。你的testFloat()方法应该会失败,你会看到一个红色的进度条,如图 11-9 所示。注意,抛出的异常被称为java.lang.NumberFormatException。将该值从 12..3 到 12.3(或者,如果您的语言使用逗号而不是句点来表示小数点,则从 12,,3 到 12,3),然后再次运行它。您的测试现在应该成功了,您应该会看到一个绿色的进度条,如图 11-10 所示。按 Ctrl+K | Cmd+K 并提交,同时显示一条创建检测测试的消息。

A978-1-4302-6602-0_11_Fig10_HTML.jpg

图 11-10。

All tests succeeded

A978-1-4302-6602-0_11_Fig9_HTML.jpg

图 11-9。

Failed testFloat( ) method

修复错误

您刚刚运行的失败测试突出了您的代码中的一个问题。即使键盘被设置为只接受数值,小数点也可能被输入多次,这将导致当 Android 试图将诸如"12..3"的字符串值转换为 double 时出现NumberFormatException。在调用CurrencyConverterTask之前,您需要验证用户输入的数据是数字。在MainActivity.java中,创建名为isNumeric()的方法,如清单 11-2 所示。

Listing 11-2. The isNumeric() Method to Be Used to Verify Input from the User

public static boolean isNumeric(String str)

{

try{

double dub = Double.parseDouble(str);

}

catch(NumberFormatException nfe) {

return false;

}

return true;

}

修改mCalcButtononClick()方法,使我们在执行CurrencyConverterTask之前验证输入数据是数值,如图 11-11 所示。

A978-1-4302-6602-0_11_Fig11_HTML.jpg

图 11-11。

Modify the onClick( ) method so that we verify the input value of mAmountEditText with isNumeric( )

祝贺您——您刚刚创建了一个插装测试,用它来识别一个 bug,然后在源代码中修复了这个 bug。按 Ctrl+K | Cmd+K,并通过验证输入是数字来提交一条修复 bug 的消息。

使用猴子

Android SDK 附带了一个非常好的工具,叫做 Monkey,也称为 UI/应用练习器 Monkey。这个工具允许你在你的应用上生成随机的 UI 事件,就像一只猴子在使用它一样。Monkey 对你的应用进行压力测试很有用。Monkey 的文档可以在developer.android.com/tools/help/monkey.html找到。

Note

除了 Monkey 之外,一个名为 MonkeyRunner 的工具允许您创建和运行 Python 脚本来自动化您的应用进行测试。MonkeyRunner 和猴子没有血缘关系。此外,MonkeyRunner 要求您知道如何使用 Python 编写脚本,这超出了本书的范围。如果您有兴趣了解更多关于 MonkeyRunner 的信息,请参阅位于developer.android.com/tools/help/monkeyrunner_concepts.html的文档。

首先在 Android Studio 中打开一个终端会话,方法是按下位于 IDE 底部边缘的终端窗口按钮。在工具栏的组合框中选择应用,然后单击绿色的运行按钮,启动货币应用。一旦应用运行并空闲,向终端会话发出以下命令,然后按 Enter 键,如图 11-12 所示:

A978-1-4302-6602-0_11_Fig12_HTML.jpg

图 11-12。

Open a terminal session, type the monkey command, and then press Enter

adb shell monkey -p com.apress.gerber.currencies -v 2000

从这个命令中您会注意到的第一件事是 Monkey 正在使用adb,即 Android Debug Bridge,它允许您与运行设备的操作系统外壳进行交互。如果您在发出此命令之前忘记启动您的应用,Monkey 将不会工作。-p开关告诉 Monkey 将其随机 UI 事件约束到com.apress.gerber.currencies包中。-v开关告诉 Monkey 以详细的方式报告事件和异常;如果 Monkey 抛出了一个异常,如果报告很详细,那么跟踪这个异常会更容易。最后一个参数(2000)是事件的数量。两千个随机的 UI 事件应该可以暴露出 UI 的任何问题,您可以根据需要随时运行这个命令。

Caution

当运行 Monkey 时,即使将 Monkey 的 UI 事件约束到一个特定的包中,您也有可能意外地更改设备的默认设置。例如,Monkey 翻转你的 Wi-Fi 或更改手机默认语言的情况并不少见。

使用分析工具

Android SDK 附带的分析工具叫做 Lint。不久前,开发人员还被要求从命令行调用这个工具。幸运的是,Lint 现在已经完全集成到 Android Studio 中。Lint 将分析您的源代码、XML 文件和其他素材,以寻找潜在的错误、未使用的资源、低效的布局、硬编码的文本和其他与 Android 相关的潜在问题。更重要的是,Android Studio 有自己的分析工具,可以对 Java 和 Android 语法执行类似的操作,甚至比 Lint 更强大。总之,这个完全集成的工具套件将保持您的代码整洁,希望没有错误。您可以从主菜单栏中的分析菜单访问 Android Studio 的分析工具。

检查代码

检查代码操作是最有用和最全面的分析操作。导航至分析➤检查代码以运行此操作。在弹出的对话框中,选择整个项目单选按钮,点击确定,如图 11-13 所示。等待几秒钟,让 Android Studio 分析你的整个项目,并在检查工具窗口中显示结果,如图 11-14 所示。您会注意到,首先列出了 Android Lint 检查的目录,然后进一步列出了 Android Studio 自己检查的几个目录。

A978-1-4302-6602-0_11_Fig14_HTML.jpg

图 11-14。

Inspection tool window showing results of the Inspect Code operation

A978-1-4302-6602-0_11_Fig13_HTML.jpg

图 11-13。

Select the Whole Project option from the Specify Inspection Scope dialog box

请记住,由 Inspect 代码操作识别的问题可能根本不是严重的问题。因此,不要觉得有义务去解决每一个问题。此外,在极少数情况下,建议的解决方案可能会破坏您的代码或违背您最初的良好意图。所以,你应该把 Lint 和 Android Studio 的分析工具识别出的问题当作建议。

切换打开检查工具窗口中的目录,直到您能够看到单个行项目。当您检查这些行项目时,请注意检查工具窗口右窗格中每个可能问题的摘要。详细信息包括名称、位置、问题概要、问题解决和抑制,如图 11-14 所示。修复一个潜在的问题就像直接点击问题解决标题下的蓝色超文本一样简单;Android Studio 会完成剩下的工作。避免试图修复由 Inspect 代码操作识别的每一个问题。如果你解决了这些问题中的一个,请谨慎操作并测试你的应用,以确保你没有引入新的错误。

分析依赖关系

“分析依赖项”操作同样位于主菜单栏的“分析”菜单中。分析依赖项将检查您的源代码,并自动为您识别任何依赖项。您可以通过检查项目中每个 Java 源文件的 import 语句来手动执行这个操作,但是这很繁琐。Analyze Dependencies 操作为您节省了这种繁琐的工作,并且还识别了每个依赖项的位置。

Android 中的依赖项可能来自各种来源,包括 Java JDK、Android SDK、第三方 JAR 库(如 Apache Commons)和库项目(如脸书)。如果协作开发人员无法编译和运行项目,主要怀疑是缺少依赖项,您可以使用分析依赖项操作来确定可能缺少哪些依赖项。在 Gradle 之前,管理依赖关系是一件大事。自从 Gradle 出现以来,大多数依赖项都是自动为您下载的,Gradle 使得管理依赖项变得容易和可移植。

从主菜单栏中选择分析➤分析相关性。等待 Android Studio 执行操作,在依赖查看器工具窗口查看结果,如图 11-15 所示。浏览左窗格和右窗格中的各个行项目,注意底部窗格突出显示了 Java 源文件中每个依赖项的位置。

A978-1-4302-6602-0_11_Fig15_HTML.jpg

图 11-15。

Analyze Dependencies tool window showing dependency on org.apache.http.HttpEntity.class

分析堆栈跟踪

假设您没有处于调试模式,并且抛出了一个异常,那么追踪它的最好方法是检查 logcat,它是 Android 的日志工具。Logcat 太好了,也太啰嗦了,很容易让你不知所措,这就是为什么你应该使用 Analyze Stacktrace。撤销我们之前做的错误修复。如果您熟悉 Git,您可以恢复最后一次提交。否则,注释掉修复这个错误的代码,如清单 11-3 所示。

Listing 11-3. Comment Out the Bug Fix

mCalcButton.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

// if (isNumeric(String.valueOf(mAmountEditText.getText()))){

new CurrencyConverterTask().execute(URL_BASE + mKey);

//  } else {

//  Toast.makeText(MainActivity.this, "Not a numeric value, try again.", //  Toast.LENGTH_LONG).show();

//  }

}

});

按下主工具栏中的绿色运行按钮,运行货币应用。一旦货币应用启动并准备就绪,输入 12..在mAmountEditText中输入 3(或者 12,3,如果您的语言使用逗号而不是句点),然后按下计算按钮。应用将崩溃,因为 12..3 不是数值。

按 Alt+6 | Cmd+6 激活 Android DDMS 工具窗口。单击 logcat 选项卡,这是 Android DDMS 工具窗口中最左侧的选项卡。按 Ctrl+A | Cmd+A 选择 logcat 窗口中的所有文本,然后按 Ctrl+C | Cmd+C 复制所有文本,如图 11-16 所示。

A978-1-4302-6602-0_11_Fig16_HTML.jpg

图 11-16。

Logcat window with verbose logs and stack trace

选择分析➤分析堆栈跟踪以调用分析堆栈跟踪操作。任何设置到剪贴板的文本现在都将出现在“分析堆栈跟踪”对话框中。点击正常化按钮,然后点击确定,如图 11-17 所示。运行工具窗口将被激活,堆栈跟踪将可见(不包括任何多余的日志)以及显示异常来源的超链接文本,如图 11-18 所示。Analyze Stacktrace 很好地解析和显示了相关的堆栈跟踪,现在可以轻松地对其进行分析了。

A978-1-4302-6602-0_11_Fig18_HTML.jpg

图 11-18。

The Stacktrace window showing only the relevant stack trace and hyperlinks to the exception’s source

A978-1-4302-6602-0_11_Fig17_HTML.jpg

图 11-17。

Analyze Stacktrace dialog box with contents of entire clipboard

您可以使用 Git 来恢复最后一次提交,或者取消对 bug 修复的注释,如清单 11-4 所示。

Listing 11-4. Uncomment the Bug Fix

mCalcButton.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

if (isNumeric(String.valueOf(mAmountEditText.getText()))){

new CurrencyConverterTask().execute(URL_BASE + mKey);

} else {

Toast.makeText(MainActivity.this, "Not a numeric value, try again.", Toast.LENGTH_LONG).show();

}

}

});

摘要

在这一章中,我们已经向你展示了如何使用 Android Studio 中的一些测试和分析工具。我们还向您展示了如何使用测试工具来识别 bug,然后我们继续修复 bug。最后,我们讨论了仪器测试、Monkey、Lint 和 Android Studio 自己的分析工具。

十二、排除故障

你的应用变得越复杂,就越有可能包含错误。没有什么比一个应用崩溃、在特定条件下无法运行或者妨碍了它本来要完成的任务更让用户沮丧的了。一种天真的开发方法是假设您的代码将总是沿着您定义的路径执行。这有时被称为快乐之路。

理解代码在哪里会偏离快乐的道路对于成为一名优秀的软件开发人员至关重要。因为您无法预测开发过程中所有潜在的不愉快路径,所以了解 Android 开发中涉及的各种诊断工具和技术会有所帮助。第十一章涵盖了任何工具;本章详细探讨了调试器,并回顾了其他一些分析工具,您不仅可以使用它们来修复错误,还可以在工作中洞察潜在的弱点。

记录

许多开发人员在 Android 中使用的第一个工具是 Android 登录系统。日志记录是将变量值或程序状态打印到系统控制台的一种方式,可以在程序运行时读取。如果你有编程的背景,你可能对这种技术很熟悉。然而,日志记录在 Android 中的形式与在其他平台上略有不同。第一个差异是您可能习惯于在普通 Java 平台上使用的函数或方法调用。Android 应用在一台机器上开发,但在另一台机器上执行,因此打印输出被藏在运行代码的设备上。

Android 上负责日志消息的框架叫做logger。它捕获各种事件的输出,不限于您的应用,并将该输出存储在一系列循环缓冲区中。循环缓冲区是一种类似链表的数据结构,但除了以串行方式链接其元素之外,它还将其最后一个元素链接到其第一个元素。这些缓冲区包括radio,它包含无线电和电话相关的消息;events,包含系统事件消息,如服务创建和销毁的通知;和main,包含主日志输出。SDK 提供了一组用于检查这些日志消息的编程和命令行工具。查看所有这些事件的日志就像切断消防水管喝一口水一样。因此,您可以使用各种操作和标志来减少输出。

使用 Logcat

从命令行,您可以使用 Logcat,它连接到一个附加设备,并将这些循环缓冲区的内容转发到您的开发控制台。它有多种选项,调用它的语法在表 12-1 中给出。

表 12-1。

Logcat Options and Filters

| `Log Options and Filters` | 描述 | | --- | --- | | `-c` | 清除或刷新日志。 | | `-d` | 将日志转储到控制台。 | | `-f ` | 将日志写入``。 | | `-g` | 显示给定日志缓冲区的大小。 | | `-n ` | 设置旋转日志的数量。默认值为 4。该选项需要`-r`选项。 | | `-r ` | 针对给定的每千字节数旋转日志文件。缺省值是 16,这个选项需要`-f`选项。 | | `-s` | 将默认过滤器设置为静音。 | | `-v ` | 将输出格式设置为以下格式之一:`brief`显示发出消息的进程的优先级、标签和 PID。`process`仅显示 PID。`tag`只显示优先级和标签。`raw`显示原始日志消息,没有任何其他字段。`time`显示发出消息的进程的日期、调用时间、优先级、标签和 PID。`threadtime`:显示每条消息线程的日期、调用时间、优先级、标签、PID 和线程 ID (TID)。`long`显示所有字段,并用空行分隔消息。 | | -b | 显示给定缓冲区的日志输出。缓冲区可以是下列之一:`radio`包含与无线电/电话相关的消息。`events`包含事件相关消息。`main`是主日志缓冲区(默认)。 |

adb logcat [option] ... [filter] ...

日志中的每条消息都有一个标签。标签是一个短字符串,通常代表发出消息的组件。该组件可以是一个View、一个CustomErrorDialog或应用中定义的任何小部件。每条消息还具有相关联的优先级,该优先级决定了该消息的重要性。优先事项如下:

  • V:详细(最低优先级)
  • D:调试
  • I:信息
  • W:警告
  • E:错误
  • F:致命
  • S:静默(最高优先级,日志中的所有内容都被忽略)

您可以通过使用过滤器表达式来控制 Logcat 的输出。使用正确的标志组合将有助于您关注与您的调查相关的输出。过滤表达式采用tag:priority的形式。例如,MyBroadcastReceiver:D将只包含来自MyBroadcastReceiver组件的日志消息,这些消息被标记为调试优先级。

Android Studio 包括一个内置的设备 Logcat 查看器,它通过使用图形控件来处理命令行的细节。插入您的设备或启动模拟器,然后单击 IDE 底部的数字 6 选项卡打开 DDMS 浏览器。如果尚未选择,选择Devices | Logcat选项卡。你的屏幕应该如图 12-1 所示。

A978-1-4302-6602-0_12_Fig1_HTML.jpg

图 12-1。

The Android DDMS tool window

在该视图的右上角,您将看到三个重要的过滤器控件。日志级别下拉列表按优先级控制过滤。在图 12-1 中,该选项设置为 Verbose,记录所有消息。将日志级别设置为 Debug 将包括 Debug 优先级或更高优先级的所有消息。该下拉列表旁边是一个手动文本输入控件,它将消息限制为只包含您在此处键入的文本。清除条目会清除过滤器。下一个下拉列表包括一组预设过滤器和一个编辑或更改这些预设的选项。单击编辑过滤器配置以打开创建新的 Logcat 过滤器对话框。该对话框如图 12-2 所示,包括修改任何预设过滤器的控件。

A978-1-4302-6602-0_12_Fig2_HTML.jpg

图 12-2。

The Create New Logcat Filter dialog box

您还可以添加、更改或删除任何自定义过滤器。这些预设可以通过标签、包名、进程 ID (PID)和/或日志级别进行过滤。

写入 Android 日志

当你的应用运行时,你可能想知道一个方法实际上正在执行,这个执行使得它通过了方法中的某个点,或者某些变量的值。SDK 在一个名为android.util.Log的类上定义了静态方法,您可以用它来写入日志。这些方法使用短名称— vdIwef—对应于详细、调试、信息、警告、错误和致命优先级。每个方法都有一个标签、一个消息字符串和一个可选的 throwable。您选择的方法决定了与您提供的消息相关联的优先级。例如,下面的片段是您可能在活动中找到的日志。它将记录带有调试优先级的文本onCreate(),同时使用类名作为标记:

protected void onCreate(Bundle savedInstanceState) {

Log.d(this.getClass().getSimpleName(), "onCreate()");

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

捕虫游戏。

大多数开发人员主要关注于编写有效的软件。本节向您介绍一个不工作的应用!它是故意带着问题写的,作为调试的练习。这个简单的数学测试应用有几个文本输入字段,用于输入任意数字。运算符下拉列表允许您选择加法、减法、乘法和除法。在底部的文本输入字段中,您可以尝试回答您构建的数学问题。复选按钮使您能够检查答案。通读清单 12-1 中的代码,看看它是如何工作的。

Listing 12-1. The DebugMe App

<FrameLayout

android:layout_width="fill_parent"

android:layout_height="fill_parent"

xmlns:android="``http://schemas.android.com/apk/res/android

android:background="@android:color/black">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="Math Test"

android:id="@+id/txtTitle"

android:layout_gravity="center_horizontal|top"

android:layout_marginTop="10dp"

android:textColor="@android:color/white" />

<RelativeLayout

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:layout_gravity="center">

<EditText

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/editItem1"

android:text="25"

android:layout_above="@+id/editItem2"

android:layout_centerHorizontal="true"

android:layout_alignStart="@+id/editItem2"

android:textColor="@android:color/white" />

<Spinner

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/spinOperator"

android:layout_centerVertical="true"

android:layout_toLeftOf="@+id/editItem2"

android:layout_alignBottom="@+id/editItem2"

android:spinnerMode="dropdown" />

<EditText

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/editItem2"

android:text="50"

android:layout_centerVertical="true"

android:layout_centerHorizontal="true"

android:layout_margin="25dp"

android:textColor="@android:color/white" />

<EditText

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="\???"

android:id="@+id/editAnswer"

android:layout_below="@+id/editItem2"

android:layout_centerHorizontal="true"

android:layout_marginLeft="25dp"

android:textColor="@android:color/white" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="="

android:id="@+id/textView"

android:layout_below="@+id/editItem2"

android:layout_toLeftOf="@+id/editAnswer"

android:textColor="@android:color/white" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginLeft="25dp"

android:text="Check"

android:onClick="checkAnswer"

android:layout_toRightOf="@id/editAnswer"

android:layout_alignBottom="@id/editAnswer"

android:textColor="@android:color/white" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="The answer is:\nXXX"

android:id="@+id/txtAnswer"

android:layout_below="@+id/editAnswer"

android:layout_centerHorizontal="true"

android:textColor="@android:color/holo_red_light"

/>

</RelativeLayout>

</FrameLayout>

public class MainActivity extends Activity {

private static final int SECONDS = 1000;//millis

private Spinner operators;

private TextView answerMessage;

@Override

protected void onCreate(Bundle savedInstanceState) {

Log.d(this.getClass().getSimpleName(), "onCreate()");

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

answerMessage = (TextView) findViewById(R.id.txtAnswer);

answerMessage.setVisibility(View.INVISIBLE);

operators = (Spinner) findViewById(R.id.spinOperator);

final ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,

R.array.operators_array, android.R.layout.simple_spinner_item);

adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

operators.setAdapter(adapter);

}

public void checkanswer(View sender) {

InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);

imm.hideSoftInputFromWindow(findViewById(R.id.editAnswer).getWindowToken(), 0);

checkAnswer(sender);

}

public void checkAnswer(View sender) {

String givenAnswer = ((EditText) findViewById(R.id.editAnswer)).getText().toString();

int answer = calculateAnswer((EditText) findViewById(R.id.editItem1),

(EditText) findViewById(R.id.editItem2));

final String message = "The answer is:\n" + answer;

if(Integer.parseInt(givenAnswer) == answer) {

showAnswer(true, message);

} else {

showAnswer(false, message);

}

eventuallyHideAnswer();

}

private int calculateAnswer(EditText item1, EditText item2) {

int number1 = Integer.parseInt(item1.getText().toString());

int number2 = Integer.parseInt(item2.getText().toString());

int answer = 0;

switch(((Spinner) findViewById(R.id.spinOperator)).getSelectedItemPosition()) {

case 0:

answer = number1 + number2;

break;

case 1:

answer = number1 - number2;

break;

case 2:

answer = number1 * number2;

break;

case 3:

answer = number1 / number2;

break;

}

return answer;

}

private void showAnswer(final boolean isCorrect, final String message) {

if (isCorrect) {

answerMessage.setText("Correct! " + message);

answerMessage.setTextColor(getResources().getColor(android.R.color.holo_green_light));

} else {

answerMessage.setText("Incorrect! " + message);

answerMessage.setTextColor(getResources().getColor(android.R.color.holo_red_light));

}

answerMessage.setVisibility(View.VISIBLE);

}

private void eventuallyHideAnswer() {

final Runnable hideAnswer = new Runnable() {

@Override

public void run() {

answerMessage.setVisibility(View.INVISIBLE);

}

};

answerMessage.postDelayed(hideAnswer,10 * SECONDS);

}

}

我们有一个活动,允许用户尝试解决一个简单的数学问题。onCreate()方法将所有视图组件保存在实例变量中,并将基本操作符(加、减、乘、除)插入到ArrayAdapter中。在调用被覆盖的checkAnswer()方法之前,checkanswer()方法隐藏了键盘,该方法执行检查我们答案的实际工作。这个被覆盖的checkAnswer()方法调用一个calculateAnswer()方法来寻找实际的答案。然后,checkAnswer()方法将答案与给定的答案进行比较,并构建一个答案消息。如果答案与给定的答案匹配,则调用showAnswer(),其中true值指示成功;否则,用false调用showAnswer()。最后,checkAnswer()方法通过调用eventuallyHideAnswer()方法最终隐藏了应答消息,这将发布一个Runnable代码块在 10 秒钟后执行。

当你开始使用这个应用时,你可能不会注意到这些错误,但它们很快就会出现。如果您通读了示例代码,并在运行它之前自己输入了它,您可能会对它明显的弱点很敏感。保留默认答案或尝试回答并点击“检查”按钮。应用会立即崩溃。试着再运行一次。这一次,输入数学题的错误答案,然后点击“检查”按钮。没有可见的反馈告诉你答案是否正确!您可能认为您知道崩溃的根源在哪里,但是我们不会猜测假设的问题,而是尝试使用调试器来正确地隔离 bug。

使用交互式调试器

Android Studio 包括一个交互式调试器,允许您设置断点。您可以通过单击编辑器左侧空白处您想要检查的行来设置断点。请记住,断点必须设置在包含可执行语句的行上;例如,您不能在包含注释的行上设置断点。设置断点时,Android Studio 会在装订线中添加一个粉色圆圈图标,并以粉色突出显示整行。当在调试模式下运行应用,并且程序执行到达断点时,槽中的圆圈变为红色,该行突出显示,执行暂停并进入交互式调试模式。在交互调试模式下,许多应用状态都显示在调试工具窗口中,包括变量和线程。程序的状态可以被详细检查或者甚至被改变。

要开始调试,可以通过单击顶部工具栏中的 bug 图标以调试模式启动程序,也可以单击 bug 图标右侧的图标。这将在程序运行时将调试器附加到程序上(见图 12-3 )。您选择的方法取决于您试图捕捉的问题。您的 bug 可能会在真实世界的条件下出现,因此您需要将设备带到特定的位置或以特定的方式使用它。在这种情况下,将设备连接到电脑可能会不方便。在这些情况下,让您的设备进入 bug 开始显现的状态,然后将设备连接到您的计算机以启动调试器是有意义的。然而,如果 bug 发生在应用启动的早期,以调试模式启动可能是有意义的,这样应用启动时可以立即暂停执行。在第三种方法中,你可以从 Android 设备设置中将一个应用设置为可调试的,并让它等待调试器连接。当您试图发现应用启动时出现的问题,但不想上传和替换设备上已安装的实际应用时,这很有帮助。

A978-1-4302-6602-0_12_Fig3_HTML.jpg

图 12-3。

Attach the debugger while running

我们将从在MainActivity中每个方法的第一行添加断点开始。当您不确定问题的确切位置并且没有太多方法时,这种方法非常有效。但是,它不会随着应用复杂性的增加而扩展。单击左边距中的装订线,在每个方法的第一行添加断点。你应该会看到类似于图 12-4 的东西。

A978-1-4302-6602-0_12_Fig4_HTML.jpg

图 12-4。

Add breakpoints to each method in the MainActivity class

单击运行➤调试应用,等待 Android Studio 在设备上构建并启动您的应用。当应用启动时,您会看到一个简短的对话框,指示 adb (Android Debug Bridge)正在等待调试器连接,然后 IDE 才会建立连接。然后 Android Studio 最终会高亮显示(蓝色)在onCreate()方法行的第一个断点,如图 12-5 所示。调试工具窗口将会打开,如果您在等待断点时碰巧正在运行另一个程序,IDE 甚至会请求焦点并跳到屏幕的前面。这可能很方便,但如果你碰巧使用社交网络或聊天应用,会造成干扰,因为你的击键可能会进入编辑器并破坏你的代码,所以要小心!

A978-1-4302-6602-0_12_Fig5_HTML.jpg

图 12-5。

Execution stops at a breakpoint and highlights it blue

当第一个断点显示为蓝色时,“调试工具”窗口从底部窗格打开,此时您可以开始检查程序的状态。“调试工具”窗口具有一些功能,您可以使用这些功能深入到执行和控件的不同区域,您可以使用这些功能单步执行、单步退出和单步跳过方法。当前行恰好是对Log.d()方法的调用,该方法向 Logcat 发送一行文本。单击 Logcat 选项卡显示日志,然后单击 Step Over 按钮A978-1-4302-6602-0_12_Figf_HTML.jpg执行 log 语句。Logcat 显示日志信息,执行移动到下一行,如图 12-6 所示

A978-1-4302-6602-0_12_Fig6_HTML.jpg

图 12-6。

The Logcat view shows the log message after stepping over

单击调试器选项卡以显示变量视图。在这个视图下,您应该看到三个变量:thissavedInstanceanswerMessage。点击this变量旁边的三角形,展开与this对象相关的所有变量。this对象总是表示正在执行的当前类,所以当前文件中的所有实例变量在您深入查看时都是可见的。您还会看到许多其他实例变量,每个都是从父类派生的。筛选如此多的变量可能有些乏味,但它有助于理解您当前正在调试的类的结构。折叠this变量,再点击两次单步执行,将执行点移动到answerMessage的赋值处。请注意,在 variables 视图中突然出现了操作符的实例变量。当执行点接近实例变量的赋值时,它们开始显示在 variables 视图中。

评估表达式

在运行将设置answerMessage变量的赋值语句之前,您可以分解这一行,看看在它发生之前会有什么赋值。单击并拖动选择到findViewById(R.id.txtAnswer)表达式上,然后按 Alt+F8。你会看到一个类似于图 12-7 的对话框。

A978-1-4302-6602-0_12_Fig7_HTML.jpg

图 12-7。

Using the Evaluate Expression dialog box

该表达式将被复制到“表达式求值”对话框中,并且可以独立于该行的其余部分执行。该对话框接受任何 Java 代码片段,并显示其评估结果。单击“求值”(或按 Enter 键,因为默认情况下选择了“求值”)来求值并执行表达式。对话框最终会被表达式的结果填充,你可以看到代表TextView的对象,它保存了答案文本。当你检查答案时,你最终会看到同样的TextView。结果是一个以扩展形式显示的对象,它提供了关于TextView状态的大量信息。您可以检查内部的mText属性、文本颜色、布局参数等等。在表达式后面追加一个getVisibility()方法调用,如图 12-8 所示。

A978-1-4302-6602-0_12_Fig8_HTML.jpg

图 12-8。

Examine the answer EditText’s visibility in the Evaluate Expression dialog box

findViewById(R.id.txtAnswer).getVisibility()表达式的结果为 0,等于View.VISIBLE常数。记住常量的值可能很困难,但您可以在表达式计算器中使用任何表达式。这意味着通过使用如下表达式,您可以直接询问 Android Studio,“我的视图可见吗?”

findViewById(R.id.txtAnswer) == View.Visible

前面一行代码的结果将是true;但是,尝试跳过接下来的两行,执行将视图设置为不可见的那一行。按 Alt+F8 再次调出“表达式计算器”对话框,并使用向下箭头键在您计算过的早期表达式中循环,找到“我的视图可见吗?”表情。此时,结果应该是false,在意料之中。这个想法是隐藏答案,直到点击检查按钮。逐行单步执行语句可以让您了解实际发生了什么,而使用表达式计算器可以让您在程序运行时确认变量或表达式的值。

单击调试器左侧控制面板中的运行按钮,恢复正常执行。app 会继续完成onCreate()方法,以正常速度运行,直到到达另一个断点。在onCreate()完成之后,用户界面应该在您的设备或模拟器上呈现。至此,我们可以开始解决实际问题了。当你试图检查数学问题的给定答案时,第一个问题出现了。键盘永远不会隐藏,答案永远不会透露。点击问号激活答案字段TextEdit控件,清除它,并键入任何数字。接下来点击检查按钮。即使在第一个checkanswer()方法的开头有一个断点,执行也会在第一行的checkAnswer()方法处暂停。这里的意图是第一个checkanswer()方法应该在有逻辑隐藏键盘的地方被调用。这个方法然后调用第二个checkAnswer()方法来完成验证输入的实际工作。因为没有调用第一个方法,所以键盘保持可见!

现在您已经知道了这个问题的原因,让我们检查一下代码的其他部分,看看为什么这个方法没有被调用。我们的例子使用activity_main布局文件中的onClick属性将按钮连接到方法。打开activity_main布局文件,你会找到根本原因。Check 按钮的onClick属性被设置为checkAnswer(使用大小写混合版本),而你确实希望onClick属性调用checkanswer(全小写版本)。忽略使用仅大小写不同的两个方法名的明显不良模式,修复在android:onClick属性中的调用,将其设置为checkanswer。现在,单击左侧控制窗格中的调试器停止按钮。这将分离调试器,并允许程序恢复正常执行。构建并再次运行应用以查看结果。您应该会看到类似于图 12-9 的内容。

A978-1-4302-6602-0_12_Fig9_HTML.jpg

图 12-9。

The keypad is dismissed, and the answer’s TextView is visible

使用堆栈跟踪

您通过使用交互式调试器发现并修复了两个错误。然而,更多的问题存在。如果您再次启动应用并立即点击检查按钮,应用将会崩溃。您可以使用交互式调试器来查找根本原因,也可以跟踪堆栈跟踪。堆栈跟踪是崩溃时堆栈上每个方法的转储,包括行号。堆栈引用一系列方法,每个方法都由它之前的方法调用。Java 将程序错误表示为ExceptionThrowable对象。这些特殊的对象携带关于错误原因的元数据以及错误发生时的程序状态。异常沿着程序堆栈向上传播到调用方法及其父调用方,直到它们被捕获和处理。如果它们没有被发现和处理,它们会一直传播到操作系统,使你的应用崩溃。要想清楚,最好看一个例子。触发崩溃,然后立即在 Android DDMS 工具窗口下的 logcat 窗口中查找堆栈跟踪。

Listing 12-2. The Stack Trace Produced When Check Is Tapped

03-08 20:10:56.660    9602-9602/com.apress.gerber.debugme E/AndroidRuntime: FATAL EXCEPTION: main

Process: com.apress.gerber.debugme, PID: 9602

java.lang.IllegalStateException: Could not execute method of the activity

at android.view.View$1.onClick(View.java:3841)

at android.view.View.performClick(View.java:4456)

at android.view.View$PerformClick.run(View.java:18465)

at android.os.Handler.handleCallback(Handler.java:733)

at android.os.Handler.dispatchMessage(Handler.java:95)

at android.os.Looper.loop(Looper.java:136)

at android.app.ActivityThread.main(ActivityThread.java:5086)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)

at dalvik.system.NativeStart.main(Native Method)

Caused by: java.lang.reflect.InvocationTargetException

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at android.view.View$1.onClick(View.java:3836)

at android.view.View.performClick(View.java:4456)

at android.view.View$PerformClick.run(View.java:18465)

at android.os.Handler.handleCallback(Handler.java:733)

at android.os.Handler.dispatchMessage(Handler.java:95)

at android.os.Looper.loop(Looper.java:136)

at android.app.ActivityThread.main(ActivityThread.java:5086)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)

at dalvik.system.NativeStart.main(Native Method)

Caused by: java.lang.NumberFormatException: Invalid int: "???"

at java.lang.Integer.invalidInt(Integer.java:137)

at java.lang.Integer.parse(Integer.java:374)

at java.lang.Integer.parseInt(Integer.java:365)

at java.lang.Integer.parseInt(Integer.java:331)

at com.apress.gerber.debugme.MainActivity.checkAnswer(MainActivity.java:46)

at com.apress.gerber.debugme.MainActivity.checkanswer(MainActivity.java:39)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at android.view.View$1.onClick(View.java:3836)

at android.view.View.performClick(View.java:4456)

at android.view.View$PerformClick.run(View.java:18465)

at android.os.Handler.handleCallback(Handler.java:733)

at android.os.Handler.dispatchMessage(Handler.java:95)

at android.os.Looper.loop(Looper.java:136)

at android.app.ActivityThread.main(ActivityThread.java:5086)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)

at dalvik.system.NativeStart.main(Native Method)

堆栈跟踪可能会很长,这取决于你的应用的复杂性,但是学习导航它们是一项有价值的技能,可以添加到你的武器库中。在前面的堆栈跟踪中,您将看到各种方法名称和行号一起列出。第一个列出的方法View$1.onClick被认为是堆栈的顶部,是最近调用的方法。方法名旁边是一个行号,指向发生异常的源代码的实际行。因为这个类不是我们作为示例的一部分编写的代码,所以您必须深入查看堆栈。当您向下查看堆栈时,您会看到以Caused By开头的条目。阅读的方式如下:你有一个异常,这个异常是由一个异常引起的,以此类推。如果你读了最后一个原因,你会发现实际的问题,Invalid int: "???"。系统抱怨您向Integer.java中的InvalidInt方法传递了一个无效的整数值,一系列问号。这是 Android 运行时的一部分,不受你的控制。但是,如果您继续阅读,您会看到invalidInt被更多的 Java 运行时方法调用,这些方法实际上是由checkAnswer调用的,这些方法在MainActivity.java中。您可以单击 Logcat 视图中的行号,这将直接跳转到下面代码片段中指示的位置:

if(Integer.parseInt(givenAnswer) == answer) {

showAnswer(true, message);

} else {

showAnswer(false, message);

}

eventuallyHideAnswer();

此时,在点击 Check 之后,我们将把givenAnswer变量传递给Integer.parseInt方法。在同一方法的前几行,您将看到以下初始化 givenAswer 变量的代码:

String givenAnswer = ((EditText) findViewById(R.id.editAnswer)).getText().toString();

来自EditText控件的文本值存储在givenAnswer字符串变量中。在将值转换为数字之前,您应该检查它是否确实是一个数字,以防止系统崩溃。将调用Integer.parseIntif块改为使用以下if / else if逻辑:

if(! isNumeric(givenAnswer)) {

showAnswer(false, "Please enter only numbers!");

} else if(Integer.parseInt(givenAnswer) == answer) {

showAnswer(true, message);

} else {

showAnswer(false, message);

}

接下来定义isNumeric方法如下:

private boolean isNumeric(String givenAnswer) {

String numbers = "1234567890";

for(int i =0; i < givenAnswer.length(); i++){

if(!numbers.contains(givenAnswer.substring(i,i+1))){

return false;

}

}

return true;

}

isNumeric()方法根据所有数字的列表测试每个字符。如果该方法返回false,那么修改后的if块将调用showAnswer(),并提示用户只输入数字。准备就绪后,再次尝试运行该应用。轻按“检查”按钮,不更改带问号的默认答案。崩溃行为应该被重点关注。代码中还有一个故意放置的错误,这可能会导致崩溃。我们稍后解释。使用该应用来解决一些数学问题与一些其他运营商除了揭露崩溃。花一点时间用你所学的来看看你是否能找到它。

本节介绍了调试的基础知识。现在,您将深入探索交互式调试器,并访问它的更多特性。在第十一章中,我们讨论了分析堆栈跟踪工具,它可以帮助你解析长堆栈跟踪。

探索交互式调试器的工具窗口

调试器工具窗口包括跟踪执行时逐句进入代码行的控件。默认情况下,“框架”选项卡显示调用堆栈。调用堆栈是被调用以到达当前断点的方法调用的堆栈。在这些堆栈中,最后调用的方法在顶部,而调用它的方法就在下面。属于 Android 运行时的方法被涂成黄色,以区别于项目中定义的方法,后者被涂成白色。

图 12-10 描述了一个调用栈,并关注两个项目方法。在这个例子中,checkAnswer()方法调用了calculateAnswer()方法,所以calculateAnswer()方法在栈顶。

A978-1-4302-6602-0_12_Fig10_HTML.jpg

图 12-10。

Use the frames view to examine the call stack

跳过按钮A978-1-4302-6602-0_12_Figa_HTML.jpg跳过当前行,到下一行。当前行上的所有指令,包括任何方法调用,都被立即执行。当它到达下一行时,应用将暂停。

单步执行按钮A978-1-4302-6602-0_12_Figb_HTML.jpg执行当前行上的所有指令,直到该行上的第一个方法调用。执行在第一个方法调用的第一行暂停。如果一行中出现多个方法调用,则遵循 Java 定义的正常操作顺序:从左向右执行,嵌套方法首先执行。不考虑在项目外部的类中定义的方法(比如第三方 JAR 文件,以及内置的 Java 和 Android API 方法)。这些方法的执行步骤。

Force Step Into 按钮A978-1-4302-6602-0_12_Figc_HTML.jpg的行为类似于 Step Into 按钮,除了外部定义的方法,如 Android SDK 中定义的方法,也被单步执行。

步出按钮A978-1-4302-6602-0_12_Figd_HTML.jpg完成当前方法中所有指令的执行,并步出该方法,到调用堆栈中的前一个调用方法。执行在调用方法后的下一行代码处暂停。

Show Execution Point A978-1-4302-6602-0_12_Fige_HTML.jpg按钮将您导航到执行当前暂停的位置。有时,您可能会在调试时远离断点并深入代码。通过使用导航中介绍的一些高级功能,您可以进入各种方法调用或探索类的调用者。这种探索可能会导致您丢失最初跟踪的方法的上下文。此选项允许您快速重新校准并从开始的地方开始。

使用断点浏览器

点击运行查看断点打开断点对话框,如图 12-11 所示。此对话框概述了您在应用中创建的所有断点。如果双击列表中的任何断点,IDE 将跳转到源代码的该行。选择任何断点都会在右侧视图中显示其详细信息。详细视图使您能够禁用断点,并控制当执行到达断点时应用暂停的方式和时间。这个视图充满了强大的选项,允许您微调断点的行为。你有能力运行任意程序语句,有条件地在感兴趣的点暂停应用,甚至控制其他断点的执行。

A978-1-4302-6602-0_12_Fig11_HTML.jpg

图 12-11。

Set breakpoint properties with the Breakpoints dialog box

视图中的第一个复选框启用和禁用断点。“挂起”复选框控制到达断点时的执行行为。如果未选中此复选框,断点将被完全禁用,并且在运行时对应用没有影响。当与其他一些选项(如 Log Evaluated Expression 选项)结合使用时,此功能特别有用。Suspend 选项旁边的单选按钮将导致断点分别挂起整个应用或当前线程。这是一项高级功能,有助于调试行为难以遵循的多线程应用。

Condition 选项允许您指定断点处于活动状态的条件。该下拉列表接受任何计算结果为布尔值的有效 Java Android 代码表达式。表达式中使用的代码在定义断点的方法的上下文中执行。因此,代码可以访问在定义它的方法中可见的任何变量。它遵循 Java 语法规则来确定作用域,您可以参考这些规则来获得关于变量可见性的更多细节。当条件为false时,断点被忽略。当条件为true时,执行将在到达断点时暂停。

每次到达断点时,“将消息记录到控制台”选项都会向调试控制台发出一条通用日志消息。这个通用消息包括方法的完全限定名和行号的可点击引用。若要查看实际效果,请检查当前在“断点”对话框中设置的每个断点。取消选中“暂停”复选框,并为每个选项选中“将消息记录到控制台”复选框。在应用运行的情况下,点击 Check 按钮触发对checkanswer()的呼叫。激活调试器工具窗口中的“控制台”选项卡,以查找来自调试器的日志消息。

Log Evaluated Expression 选项包括一个文本输入字段,它接受任何有效的 Java 代码语句。每当到达断点时,就会执行下拉列表中的代码,并将代码评估结果写入调试控制台。与 Condition 选项非常相似,这段代码在定义它的方法的上下文中运行。代码遵循与 Condition 选项相同的变量可见性规则。通常,您会指定一个计算结果为字符串的 Java 表达式,但是要知道任何 Java 语句都可以被计算,甚至是 Java 赋值语句。这使您能够在应用运行时插入代码,甚至改变行为!

Remove Once Hit 选项允许您定义自毁断点。当在一个紧密的循环中使用时,这是很有用的,在这种循环中,多次点击会使你试图看到的东西变得模糊。

“命中选定断点前禁用”选项允许您将一个断点连接到另一个断点。此选项使当前断点保持禁用状态,直到执行到达此处指定的断点。假设您有一个方法foo,它重复调用另一个方法bar,您正试图调试这个方法。当foo调用bar时,您正试图跟踪它的行为。让事情变得复杂的是,假设其他几种方法也调用bar。您可以在foobar中放置一个断点,然后选择bar的断点并配置此选项以禁用bar,直到到达foo中的断点。

早些时候,我们认为应用中还有一个错误会导致崩溃。这场崩溃可能很明显,也可能不明显。如果输入类似图 12-12 的表达式,就会触发 bug。您可以使用本章介绍的任何功能来调试崩溃。查看堆栈跟踪可以直接找到问题的根源。

A978-1-4302-6602-0_12_Fig12_HTML.jpg

图 12-12。

Try a division problem to find a crash!

switch / case块中的算术表达式需要防止被零除。使用以下代码片段解决崩溃问题:

switch(((Spinner) findViewById(R.id.spinOperator)).getSelectedItemPosition()) {

case 0:

answer = number1 + number2;

break;

case 1:

answer = number1 - number2;

break;

case 2:

answer = number1 * number2;

break;

case 3:

if(number2 != 0) {

answer = number1 / number2;

}

break;

}

条件断点

调试中比较乏味的练习之一是跟踪重复方法调用和循环之间的错误行为。根据逻辑的复杂程度,您可能会花费宝贵的时间逐句通过代码行,等待逻辑出现异常的特定情况。为了节省时间,Android Studio 支持条件断点。这些断点仅在给定的条件下是活动的。为了演示,假设您想要支持数学测试的指数特性。给arrays.xml中的operators_array添加一个指数运算符,如下所示:

<resources>

<string-array name="operators_array">

<item>+</item>

<item>-</item>

<item>x</item>

<item>/</item>

<item>exp</item>

</string-array>

</resources>

因为您已经在数组的索引 4 处添加了 exp,所以您必须向calculateAnswer()方法添加另一个 case 块,如下所示:

case 4:

if (number2!=0) {

answer = 1;

for(int i=0; i <=number2; i++) {

answer = answer * number1;

}

}

break;

您添加的是一个简单的循环,使用第二个数字作为循环计数器,将第一个数字乘以自身。在这一点上,故意的错误对您来说可能是显而易见的,也可能是不明显的。构建并运行应用,尝试解决一个 2 的 8 次方的数学问题。图 12-13 展示了这些变化会给你带来什么。

A978-1-4302-6602-0_12_Fig13_HTML.jpg

图 12-13。

The exponent answer is correct, but the app gives an error

应用错误地将答案计算为 512。您将使用交互式调试器来查找问题。首先,清除所有断点以避免任何不必要的暂停。单击“附加调试器”图标进入交互式调试模式并附加您的调试器。现在,您可以在刚刚添加的 for 循环中间放置一个断点,遍历 8 个循环,看看为什么会得到错误的结果。或者,您可以使用条件断点来查看最后一次迭代中发生了什么。单击装订线以在该行添加断点:

answer = answer * number1;

接下来右击断点,在条件字段中输入表达式 i==8(如图 12-14 所示)。

A978-1-4302-6602-0_12_Fig14_HTML.jpg

图 12-14。

Set a condition for the breakpoint

单击完成关闭弹出窗口,然后点击设备或模拟器上的检查按钮。执行将在断点处暂停,但只有在i计数器增加到 8 之后。在调试工具窗口的变量视图中查看所有变量的状态。number1变量设为 2,number2变量设为 8,答案是 256。但是,此时单击“单步执行”会导致发生额外的乘法运算,从而改变值。预期的行为是循环在第 8 个周期后终止,但事实并非如此。如果您仔细观察 for 循环中的条件,您会看到i如何初始化为 0,以及对i<=number2的检查。你需要检查i<number2,因为i是从 0 开始的。进行更改,然后构建并正常运行应用来测试它。图 12-15 显示了更改后运行的应用。

A978-1-4302-6602-0_12_Fig15_HTML.jpg

图 12-15。

Figure 12-15.

摘要

在本节中,您学习了如何使用 Android Studio 中的各种工具和功能进行调试。您了解了如何在各种级别使用日志记录,以及如何直接在 IDE 中检查 Android logcat。您探索了交互式调试器并研究了它的高级特性。您还在一个损坏的应用中进行了代码潜水,并使用调试工具来查找和修复崩溃。通过代码示例,您已经熟悉了从 stacktraces 导航和设置常规断点和条件断点。本章只讲述了在 Android Studio 中调试的基础知识。您可以创造性地组合调试器中的许多功能,以定制您的体验。您还可以在调试会话中结合 Android Logcat,以便更深入地了解您的应用。

十三、级别

当 Android 最初发布时,Google 开发了一个基于 Apache Ant 的构建系统,作为 SDK 的一部分。Ant 是一项成熟的技术,经过多年的改进,拥有一个庞大的贡献者社区。多年来,出现了其他构建系统,其中一些在蓬勃发展的社区中变得流行起来。在这些构建系统中,Gradle 成为 Java 开发的下一个发展阶段。本章探讨了 Gradle,并举例说明了如何最好地使用它来开发和维护您的 Android 应用。

在阐述 Gradle 之前,本章解释了什么是构建系统,以及为什么你可能需要对现有的构建系统进行改进。历史上,创建应用或任何软件的过程都涉及用特定的编程语言编写代码,然后将代码编译成可执行的形式。

Note

本章后面会有一个实验,解释如何在多模块项目中使用 Gradle。我们邀请您使用 Git 克隆这个项目的 Lab,以便跟进,尽管您将从头开始使用它自己的 Git 库重新创建这个项目。如果您的计算机上没有安装 Git,请参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端)并导航到 C:\androidBook\reference(如果没有参考目录,请创建一个。在 Mac 上,导航到/your-labs-parent-dir/reference/)并发出以下 git 命令:git clonehttps://bitbucket.org/csgerber/gradleweather.gitgradle weather。

现代软件开发不仅包括链接和编译,还包括测试、打包和最终产品的发布。构建系统通过提供完成这些任务的必要工具来满足这些紧急需求。考虑许多开发人员今天面临的紧急需求列表:支持最终产品的变体(调试版本、发布版本、付费版本和免费版本),管理作为产品一部分的第三方软件库和组件,以及基于外部因素向整个过程添加条件。

Android 构建系统最初是用 Ant 编写的。它基于一个相当扁平的项目结构,没有为诸如构建变化、依赖性管理和将项目的输出发布到中央存储库之类的事情提供太多的支持。Ant 有一个简单且可扩展的基于 XML 标记的编程模型,尽管许多开发人员觉得它很麻烦。此外,Ant 使用声明性模型。尽管 Ant 遵循函数式编程的一些原则,但许多开发人员对大多数现代编程语言中常见的命令式模型并不陌生。简而言之,不直接支持诸如循环结构、条件分支和可重分配属性(变量的 Ant 等价物)之类的东西。

Gradle build 是用 Groovy 编程语言编写的,它构建在 Java 的核心运行时和 API 之上。Groovy 松散地遵循 Java 的语法,当与 Java 的语法结合时,降低了学习曲线。这增加了 Groovy 的吸引力,因为它非常接近 Java 语言,您可以将大部分 Java 代码移植到 Groovy,只需做很少的更改。这也增加了 Gradle 的优势,因为您可以在 Gradle 构建的任何时候添加 Groovy 代码。由于 Groovy 语法与 Java 如此接近,您实际上可以在 Gradle 构建脚本的中间添加 Java 语法,以达到您想要的效果。Groovy 还在 Java 的语法中加入了闭包。闭包是用花括号括起来的代码块,可以赋给变量或传递给方法。闭包是 Gradle 构建系统的核心部分,稍后您将了解更多。

gradle 语法

Gradle 构建脚本实际上是遵循某些约定的 Groovy 脚本文件。因此,您可以在构建中包含任何有效的 Groovy 语句。然而,大多数都是由遵循基于块的简单语法的语句组成的。Gradle 构建脚本的基本结构包括配置和任务块。任务块定义了在构建过程中不同时间点执行的代码。配置块是特殊的 Groovy 闭包,它在运行时向底层对象添加属性和方法。您可以在 Gradle 构建脚本中包含其他类型的块,但这超出了本书的范围。您将主要使用配置块,因为 Gradle Android 构建中涉及的任务已经定义好了。配置块采用以下形式:

label {

//Configuration code...

}

其中label是特定对象的名称,花括号定义了该对象的配置块。配置块中的代码采用以下形式:

{

stringProperty "value"

numericProperty 123.456

buildTimeProperty anObject.someMethod()

objectProperty {

//nested configuration block

}

}

该块可以访问对象的各个属性,并为它们赋值。这些属性可以是字符串、数字或对象本身。字符串属性可以接受文字值或从 Groovy 方法调用返回的值。文字值遵循类似于 Java 的规则。但是,字符串可以用双引号、单引号或 Groovy 用来表示字符串的任何其他方式来表示。对象特性使用嵌套块来设置它们各自的特性。

Gradle 构建脚本遵循一定的标准。在这个标准下,构建脚本的顶部是您声明 Gradle 插件的地方。这些是用 Groovy/Gradle 编写的组件,它们增加或扩展了 Gradle 的特性。插件声明遵循apply plugin: 'plugin.id'的形式,其中plugin.id是您希望使用的 Gradle 插件的标识符。

Gradle 任务和配置块以任何顺序遵循插件定义。习惯上声明 Android 插件,它是通过android属性在构建脚本中可用的对象。项目的依赖项通常遵循 Android 配置。依赖项列出了支持项目使用的任何外部 API、声明的插件或组件的所有库。下面是一个 Gradle 构建脚本的例子。稍后你会了解到更多的细节。

Listing 13-1. A Gradle Build Script Example

apply plugin: 'com.android.application'

android {

compileSdkVersion 20

buildToolsVersion '20.0.0'

defaultConfig {

applicationId "com.company.package.name"

minSdkVersion 14

targetSdkVersion 20

versionCode 1

versionName "1.0"

}

buildTypes {

release {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android.txt'), A978-1-4302-6602-0_13_Figa_HTML.jpg

'proguard-rules.pro'

}

}

}

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile 'com.android.support:support-v4:20.+'

}

IntelliJ 核心构建系统

Android Studio 基于 IntelliJ IDEA 平台构建,继承了 IntelliJ 核心的大部分功能。它以插件的方式向内核添加了更多 Android 特有的功能。插件是一种软件组件,可以从 IntelliJ 插件库中下载,并以可插拔的方式安装或删除,就像乐高积木一样。这些插件用于增强 IntelliJ 的功能,每个插件都可以通过使用设置窗口来启用或禁用。IntelliJ Gradle 插件将 IntelliJ 的核心构建系统融合到 Gradle 构建系统中。当输出通过 IntelliJ 核心反馈并以 IntelliJ 熟悉的方式格式化时,通常触发应用构建的动作改为调用 Gradle。

Gradle 构建概念

Gradle build 系统是一个通用工具,用于从源文件集合中构建软件包。它定义了一些构建软件的高级概念,这些概念对于大多数项目都是一致的。最常见的概念包括项目、源集、构建工件、依赖工件和存储库。项目是硬盘上的一个位置,包含所有项目源代码的集合。一个 Gradle build 将有一组源文件,它们被表示为源集。它将有一个可选的依赖列表。这些依赖项是软件工件,可以包括从 JAR 或 ZIP 存档到文本文件,再到预编译的二进制文件的任何东西。工件从存储库中取出。存储库是以特殊方式组织的工件的集合,以允许构建系统找到给定的工件。它可以是硬盘上的一个位置,也可以是一个按照标准惯例组织工件的特殊网站。每个工件可以有选择地包括它自己的依赖集,这些依赖集可以包含在构建中。构建将源集与依赖工件结合起来,生成构建工件。构建者可以有选择地将这些工件发布到存储库中,以便其他开发人员或团队可以使用它们。

Gradle Android 结构

Gradle Android 项目有一个分层结构,将子项目或模块嵌套在项目根下的各个文件夹中。这类似于 Android Studio 的 IntelliJ 基础传统上管理项目的方式。使用 Gradle 和 IntelliJ 环境,一个简单的项目可以包含一个名为app的模块,以及一些其他的文件夹和文件,或者它可以包含多个不同名称的模块。相似之处到此为止,因为 Gradle 允许模块的无限嵌套。换句话说,一个项目可以包含一个模块,该模块也包含嵌套模块。因此,Android Studio 构建系统在幕后运行 Gradle。下面的列表简要描述了一个典型的 Gradle Android 项目中包含的各个文件和文件夹。此列表主要关注您可能会考虑更改的文件:

  • 临时的 Gradle 输出、缓存和其他支持元数据都存储在这个文件夹下。
  • app:单个模块按照名字嵌套在根目录下的文件夹中。每个模块文件夹包含一个 Gradle 项目文件,该文件生成主项目使用的输出。最简单的 Android 项目将包括一个生成 APK 工件的 Gradle 项目。
  • 这个文件夹包含了 Gradle 包装器。Gradle 包装器是一个 JAR 文件,包含与当前项目兼容的 Gradle 运行时版本。
  • build.gradle:整个项目构建逻辑存在于这个文件中。它负责包含任何必需的子项目,并触发每个子项目的构建。
  • gradle.properties : Gradle 和 JVM 属性存储在这个文件中。您可以使用它来配置 Gradle 守护进程,并管理 Gradle 在构建期间如何生成 JVM 进程。你也可以使用这个文件来帮助 Gradle 在网络上通过 web 代理进行通信。
  • 这些文件是操作系统特有的文件,用于通过包装器执行 Gradle。如果您的系统上没有安装 Gradle,或者您没有与您的构建兼容的版本,那么建议使用这些文件中的一个来调用 Gradle。
  • local.properties:这个文件用来定义本地机器特有的属性,比如 Android SDK 或者 NDK 的位置。
  • settings.gradle:多项目构建或任何定义子项目的项目都需要这个文件。它定义了整体构建中包含哪些子项目。
  • Project.iml.idea.gitignore:在 Android Studio 中创建新项目时,您可能会注意到根目录下的这些文件。虽然这些文件(除了在第七章中讨论的.gitignore)不是 Gradle 构建系统的一部分,但是它们会随着你对 Gradle 文件的修改而不断更新。它们会影响 Android Studio“看待”您的项目的方式。
  • 所有的 Gradle 构建输出都在这个文件夹下。这包括生成的源。Gradle 有组织有意识地将所有输出保存在一个文件夹中。这简化了项目,因为要从版本控制中排除的内容列表不那么令人生畏,而清理只是删除一个文件夹。

项目相关性

Gradle 简化了依赖项管理,使得跨多个项目使用和重用代码变得容易,与平台无关。当一个项目变得越来越复杂时,将它分成单独的部分是有意义的,这在 Android 中被称为库。这些库可以在单独的 Gradle 项目中独立开发,也可以在 Android Studio 的多模块项目中共同开发。因为 Android Studio 将模块作为 Gradle 项目来处理,所以界限可能会变得模糊,这为代码共享带来了强大的可能性。调用全球另一个团队开发的代码中的对象与调用本地单独模块中的对象几乎是一样的!当您的项目中的代码需要调用另一个 Gradle 项目或另一个 Android Studio 模块中的代码时,您只需在主项目中声明一个依赖项,就可以将代码绑定在一起。最终的结果是将独立的部分无缝地缝合在一起,形成一个有凝聚力的应用。

考虑一个简单的例子,您的应用需要调用外部类Foo中的方法bar。使用传统的 Android 工具,你必须找到定义类Foo的项目。这可能包括从网上下载,或者如果你不太确定项目的位置或主页,甚至是费力的网上搜索。然后,您必须执行以下操作:

  • 将下载的项目保存到开发计算机上
  • 可能从源代码构建它
  • 找到它的输出 JAR 文件,并将其复制或移动到项目的libs文件夹中
  • 可能将其签入源代码管理
  • 如果您的 IDE 或工具集不能自动完成这一工作,请将它添加到您的库构建路径中
  • 编写调用该方法的代码

所有这些步骤都容易出错,如果项目使用其他项目的 jar 或代码,许多步骤都需要重复。此外,项目的不同版本有时可能位于不同的位置,或者与您已经包含在应用中的其他项目不兼容。如果项目由您公司的另一个团队维护,您可能会遇到缺少预构建 JAR 的问题,这意味着您需要将另一个团队的构建文件与您的构建文件相结合,这可能会大大增加构建应用的时间和复杂性!

有了 Android Studio 和 Gradle,你可以跳过所有的混乱。您只需要在构建文件中将项目声明为依赖项,然后编写代码来调用该方法。为了理解依赖项是如何声明的,回想一下本章前面介绍的 Gradle 构建文件示例,它包括以下块:

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile 'com.android.support:support-v4:20.+'

}

第一个compile行指示 Gradle 获取libs文件夹下的所有 JAR 文件,作为编译步骤的一部分。这类似于经典的 Ant 构建脚本处理依赖项的方式,主要是为了与旧项目兼容。第二个compile行告诉 Gradle 从资源库中找到由com.android.support组组织的support-v4库的版本 20 或更高版本,并使其在项目中可用。请记住,存储库是一个抽象的位置,包含一组预构建的工件。Gradle 将根据需要从互联网上下载依赖工件,并使它们在编译器的类路径中可用,并将它们与您生成的应用打包在一起。

案例研究:Gradle 气象项目

在这一节中,您将研究一个项目 Gradle Weather,它将逐步公开各种类型的 Gradle 构建。这个项目显示天气预报。虽然一些实现使用了中等程度的高级功能,但我们将主要关注将应用缝合在一起并截断许多源列表的构建文件。演练的每一步都有分支。这个项目的 Git 存储库标记了这个研究中各个步骤的分支。您可以通过逐个检查这些步骤或者通过查看 Git 日志中与它们相关联的变更列表来参考本章中的这些步骤。请随意深入探究其来源。

我们从一个呈现假天气预报的极简实现开始 Gradle Weather。打开 Git 日志,找到名为 Step1 的分支。右键单击该分支,从上下文菜单中选择新建分支,创建一个新分支,如图 13-4 所示。将这个分支命名为 mylocal。随着您的继续,您将对这个分支进行提交。Gradle Weather 建立在全屏活动模板的基础上,使用作为该模板的一部分生成的SystemUiHider逻辑。它启动时有一个闪屏,运行在一个 5 秒钟的计时器上,并通过从一个名为TemperatureData的硬编码的普通 Java 对象中提取数据来模拟天气预报的加载。这个TemperatureData对象被赋予一个Adapter类来填充一个充满预测的列表视图。(ListView组件将在第八章的中深入讨论。)TemperatureData使用一个TemperatureItem类来描述给定一天的天气预报。该项目的构建脚本代码遵循之前定义的相同的标准 Gradle Android 项目结构。首先,您将检查负责 Gradle 构建的根文件夹中的文件。图 13-1 和清单 13-2 到 13-5 详细说明了控制构建的核心文件背后的代码。

A978-1-4302-6602-0_13_Fig1_HTML.jpg

图 13-1。

Create a new branch from the Step1 branch Listing 13-2. Settings.gradle

include ':app'

Listing 13-3. Root build.gradle

buildscript {

repositories {

jcenter()

}

dependencies {

classpath 'com.android.tools.build:gradle:0.12.+'

// NOTE: Do not place your application dependencies here; they belong

// in the individual module build.gradle files

}

}

allprojects {

repositories {

jcenter()

}

}

Listing 13-4. local.properties

sdk.dir=C\:\\Android\\android-studio\\sdk

Listing 13-5. app\build.gradle

apply plugin: 'com.android.application'

android {

compileSdkVersion 20

buildToolsVersion '20.0.0'

defaultConfig {

applicationId "com.apress.gerber.gradleweather"

minSdkVersion 14

targetSdkVersion 20

versionCode 1

versionName "1.0"

}

buildTypes {

release {

minifyEnabled true

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

}

}

}

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile 'com.android.support:support-v4:20.+'

}

settings.gradle文件只定义了app子项目的路径。接下来是build.gradle,包括一个buildscript { ... }区块。buildscript块配置当前的构建文件。它包括应用中唯一的子项目app。接下来,build.gradle文件定义了适用于所有子项目的所有构建设置。它定义了一个包含 JCenter 存储库的buildscript块。这个可通过互联网访问的 Maven 资源库包含许多 Android 依赖项和开源项目。然后,该文件设置对 Gradle 0.12 或更高版本的依赖。最后,它将所有子项目设置为使用同一个 JCenter 存储库。

local.properties文件只包含 Android SDK 位置的设置。最后我们有app\build.gradle。这包括我们应用的所有构建配置和逻辑。第一行使用 Android Gradle 插件,用于当前构建。然后,它在android { ... }块中应用 Android 特有的配置。在这个块中,我们设置 SDK 版本和构建工具版本。SDK 指的是您希望编译的 Android SDK APIs 的版本,而构建工具版本指的是用于 Dalvik 可执行文件转换(dx步骤)、ZIP 对齐等等的构建工具的版本。defaultConfig { ... }块定义了应用 ID(当您提交到谷歌 Play 商店时使用)、您的应用兼容的最低 SDK 版本、您的目标 SDK、应用版本和版本名称。

buildTypes { ... }块控制构建的输出。它允许您重写控制生成输出的不同配置。使用此块,您可以定义发布到谷歌 Play 商店的特定配置。

dependencies { ... }块定义了应用的所有依赖关系。第一个依赖项是一个本地依赖项,它使用一个特殊的fileTree方法调用,该方法调用包含了libs子文件夹中的所有 JAR 文件。第二行声明了一个外部依赖项,它将从远程存储库中获取。使用一种特殊的语法,通过给定的字符串来声明外部依赖关系。该字符串由冒号分隔成几个部分。第一部分是组 ID,它标识了创建工件的公司或组织。第二部分是工件名称。最后一部分是您的模块所依赖的工件的特定版本。

Gradle Weather 定义了一个MainActivity类和另外三个负责建模和显示天气数据的类。清单 13-6 显示了这个活动的代码。这些级别包括TemperatureAdapterTemperatureDataTemperatureItem。在这个应用的最初版本中,天气仅仅是一个虚构的数据集,硬编码在TemperatureData类中。

Listing 13-6. MainActivity.java

public class MainActivity extends ListActivity implements Runnable{

private Handler handler;

private TemperatureAdapter temperatureAdapter;

private TemperatureData temperatureData;

private Dialog splashDialog;

String [] weekdays = { "Sunday","Monday","Tuesday",

"Wednesday","Thursday","Friday","Saturday" };

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

temperatureAdapter = new TemperatureAdapter(this);

setListAdapter(temperatureAdapter);

showSplashScreen();

handler = new Handler();

AsyncTask.execute(this);

}

private void showSplashScreen() {

splashDialog = new Dialog(this, R.style.splash_screen);

splashDialog.setContentView(R.layout.activity_splash);

splashDialog.setCancelable(false);

splashDialog.show();

}

private void onDataLoaded() {

((TextView) findViewById(R.id.currentDayOfWeek)).setText(

weekdays[Calendar.getInstance().get(Calendar.DAY_OF_WEEK)-1]);

((TextView) findViewById(R.id.currentTemperature)).setText(

temperatureData.getCurrentConditions().get(TemperatureData.CURRENT));

((TextView) findViewById(R.id.currentDewPoint)).setText(

temperatureData.getCurrentConditions().get(TemperatureData.DEW_POINT));

((TextView) findViewById(R.id.currentHigh)).setText(

temperatureData.getCurrentConditions().get(TemperatureData.HIGH));

((TextView) findViewById(R.id.currentLow)).setText(

temperatureData.getCurrentConditions().get(TemperatureData.LOW));

if (splashDialog!=null) {

splashDialog.dismiss();

splashDialog = null;

}

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

// Inflate the menu; this adds items to the action bar if it is present.

getMenuInflater().inflate(R.menu.main, menu);

return true;

}

@Override

public void run() {

temperatureData = new TemperatureData(this);

temperatureAdapter.setTemperatureData(temperatureData);

// Set Runnable to remove splash screen just in case

handler.postDelayed(new Runnable() {

@Override

public void run() {

onDataLoaded();

}

}, 5000);

}

}

MainActivity.java假装加载天气数据时,暂时显示闪屏。(这样做是为了计划项目的后续修订,这将引入实际的数据负载。)然后,它通过使用TemperatureData类将数据加载到屏幕上的各个视图中。TemperatureData类包含一组假想的预测数据,如下面的部分代码片段所示:

protected List<TemperatureItem> getTemperatureItems() {

List<TemperatureItem>items = new ArrayList<TemperatureItem>();

items.add(new TemperatureItem(drawable(R.drawable.early_sunny),

"Today", "Sunny",

"Sunny, with a high near 81\. North northwest wind 3 to 8 mph."));

items.add(new TemperatureItem(drawable(R.drawable.night_clear),

"Tonight", "Clear",

"Clear, with a low around 59\. North wind 5 to 10 mph becoming A978-1-4302-6602-0_13_Figb_HTML.jpg

light northeast  in the evening."));

items.add(new TemperatureItem(drawable(R.drawable.sunny_icon),

"Wednesday", "Sunny",

"Sunny, with a high near 82\. North wind 3 to 8 mph."));

//example truncated for brevity...

return items;

}

public Map<String, String> getCurrentConditions() {

Map<String, String> currentConditions = new HashMap<String, String>();

currentConditions.put(CURRENT,"63");

currentConditions.put(LOW,"59");

currentConditions.put(HIGH,"81");

currentConditions.put(DEW_POINT,"56");

return currentConditions;

}

主活动的布局包括一个由清单 13-7 中所示的TemperatureAdapter类填充的ListView。这个类接受一个TemperatureData对象,用它来拉一个TemperatureItems列表。它使用图 13-2 所示的temperature_summary布局为每个TemperatureItem创建一个视图。清单 13-8 中详述的每个TemperatureItem仅仅是一个数据容器对象,带有重要数据字段的 getters。这些总结包含在活动的主布局中,如图 13-3 所示。

A978-1-4302-6602-0_13_Fig3_HTML.jpg

图 13-3。

The activity_main layout

A978-1-4302-6602-0_13_Fig2_HTML.jpg

图 13-2。

The temperature_summary layout Listing 13-7. TemperatureAdapter.java

public class TemperatureAdapter extends BaseAdapter {

private final Context context;

List<TemperatureItem>items;

//This example is truncated for brevity...

@Override

public View getView(int position, View convertView, ViewGroup parent) {

View view = convertView != null ? convertView : createView(parent);

TemperatureItem temperatureItem = items.get(position);

((ImageView) view.findViewById(R.id.imageIcon)).setImageDrawable(temperatureItem. A978-1-4302-6602-0_13_Figc_HTML.jpg

getImageDrawable());

((TextView) view.findViewById(R.id.dayTextView)).setText(

temperatureItem.getDay());

((TextView) view.findViewById(R.id.briefForecast)).setText(

temperatureItem.getForecast());

((TextView) view.findViewById(R.id.description)).setText(

temperatureItem.getDescription());

return view;

}

private View createView(ViewGroup parent) {

LayoutInflater inflater = (LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

return inflater.inflate(R.layout.temperature_summary, parent, false);

}

public void setTemperatureData(TemperatureData temperatureData) {

items = temperatureData.getTemperatureItems();

notifyDataSetChanged();

}

}

Listing 13-8. TemperatureItem.java

class TemperatureItem {

private final Drawable image;

private final String day;

private final String forecast;

private final String description;

public TemperatureItem(Drawable image, String day, String forecast, A978-1-4302-6602-0_13_Figd_HTML.jpg

String description) {

this.image = image;

this.day = day;

this.forecast = forecast;

this.description = description;

}

public String getDay() {

return day;

}

public String getForecast() {

return forecast;

}

public String getDescription() {

return description;

}

public Drawable getImageDrawable() {

return image;

}

}

Android 库依赖项

虽然一个微不足道的 Android 应用可能包含由单个团队开发的代码,但随着时间的推移,该应用最终会成熟,以包含由其他开发人员或团队实现的功能。这些可以在 Android 库中对外提供。Android 库是一种特殊类型的 Android 项目,您可以在其中开发一个软件组件或一系列组件,为您的应用提供一些行为——无论是像两个数字相乘这样简单的事情,还是像提供一个列出朋友和活动的社交网络门户这样复杂的事情。Android 库以一种允许你即插即用而没有太多麻烦的方式将特性具体化。Gradle 强大的存储库系统允许您轻松定位和使用来自其他公司、开源库或您自己组织中其他人的库的代码。在本节中,您将使用一个 Android 库依赖项来改进我们的应用,该依赖项通过网络请求天气数据。这种改变对于里程碑版本来说是不够的,因为它不会以有意义的方式呈现网络数据。然而,它足以演示如何在现有的 Android 应用中使用库项目中的代码。您将做进一步的修改来呈现数据。

添加 Android 库的流程类似于从头开始创建 Android 应用。选择文件➤新建模块打开新建模块向导,如图 13-4 所示。然后在第一个对话框中选择 Android 库。在第二个对话框中,输入WeatherRequest作为模块名称,并选择符合您的 app 要求的最低 SDK 设置,如图 13-5 所示。

A978-1-4302-6602-0_13_Fig5_HTML.jpg

图 13-5。

Set the library module’s name and SDK levels

A978-1-4302-6602-0_13_Fig4_HTML.jpg

图 13-4。

Add a library module

在向导的下一页选择不添加活动,如图 13-6 所示。单击“完成”按钮,将库模块添加到项目中。

A978-1-4302-6602-0_13_Fig6_HTML.jpg

图 13-6。

Choose the Add No Activity option

克隆的存储库中的步骤 2 有新的模块,您可以将其用作参考。您的新模块将包含以下build.gradle文件:

apply plugin: 'com.android.library'

android {

compileSdkVersion 20

buildToolsVersion "20.0.0"

defaultConfig {

minSdkVersion 14

targetSdkVersion 14

versionCode 1

versionName "1.0"

}

buildTypes {

release {

minifyEnabled false

proguardFiles getDefaultProguardFile('proguard-android.txt'), A978-1-4302-6602-0_13_Fige_HTML.jpg

'proguard-rules.pro'

}

}

}

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

}

这个版本和应用主版本的主要区别是使用了 Android 库插件。这个插件从模块源生成一个特殊的 Android 归档文件格式,AAR。AAR 格式是 Android 新增的增强功能之一,它允许代码以库的形式在项目间共享。这些库可以通过使用新的 Gradle 构建系统发布到工件存储库中。您还可以声明对任何拥有已发布的 AAR 工件的项目的依赖,并在您的项目中使用它。典型的 AAR 文件仅仅是一个扩展名为.aar的 ZIP 文件。它具有以下结构:

  • /AndroidManifest.xml(必需)
  • /classes.jar(必需)
  • /res/(必需)
  • /R.txt(必需)
  • /assets/(可选)
  • /libs/*.jar(可选)
  • /jni/<abi>/*.so(可选)
  • /proguard.txt(可选)
  • /lint.jar(可选)

AndroidManifest.xml描述了归档文件的内容,而classes.jar包含了编译后的 Java 代码。资源可以在res目录下找到。R.txt文件包含aapt工具的文本输出。

Android AAR 文件允许您随意捆绑素材、本地库和/或 JAR 依赖项,这在 SDK 的早期版本中是不可能的。

在我们例子中的存储库的步骤 3 分支中,我们已经向项目添加了一个WeatherRequest模块,并更改了主应用模块,以将该模块作为依赖项包含进来。这个新模块包含一个类NationalWeatherRequest,它代表主应用与国家气象局建立网络连接。这是一个返回任何地点的天气信息的服务。位置以经度和纬度的形式给出,响应是 XML 格式的。研究清单 13-9 中的代码会有更好的理解。

Listing 13-9. NationalWeatherRequest.java

public class NationalWeatherRequest {

public static final String NATIONAL_WEATHER_SERVICE =

"``http://forecast.weather.gov/MapClick.php?lat=%f&lon=%f&FcstType=dwml

public NationalWeatherRequest(Location location) {

URL url;

try {

url = new URL(String.format(NATIONAL_WEATHER_SERVICE,

location.getLatitude(), location.getLongitude()));

} catch (MalformedURLException e) {

throw new IllegalArgumentException(

"Invalid URL for National Weather Service: " +

NATIONAL_WEATHER_SERVICE);

}

InputStream inputStream;

try {

inputStream = url.openStream();

} catch (IOException e) {

log("Exception opening Nat'l weather URL " + e);

e.printStackTrace();

return;

}

log("Dumping weather data...");

BufferedReader weatherReader = new BufferedReader(

new InputStreamReader(inputStream));

try {

for(String eachLine = weatherReader.readLine(); eachLine!=null;

eachLine = weatherReader.readLine()) {

log(eachLine);

}

} catch (IOException e) {

log("Exception reading data from Nat'l weather site " + e);

e.printStackTrace();

}

}

private int log(String eachLine) {

return Log.d(getClass().getName(), eachLine);

}

}

这个新类检索天气数据并将其转储到 Android 日志中,作为使用 Android 库的一个基本示例。要在我们的项目中包含新模块,必须编辑 app 模块中的build.gradle文件。找到 dependencies 块并对其进行更改,如下所示:

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile 'com.android.support:support-v4:20.+'

compile project(':WeatherRequest')

}

compile project()行引入了一个项目依赖。项目位置是作为参数给定给project()方法的相对路径,这个位置使用冒号作为路径分隔符。前面的例子是在主项目文件夹GradleWeather中名为WeatherRequest的文件夹中定位一个项目。Gradle 将项目依赖视为主构建中的附加工作。在构建应用模块之前,Gradle 将运行WeatherRequest依赖项目,然后查看这个项目,在build/outputs文件夹下找到它的输出。WeatherRequest项目输出一个 AAR 文件作为它的主要输出,由 app 模块中的构建使用。AAR ZIP 文件在 app 模块的build/intermediates文件夹下展开,其内容包含在其编译输出中。您通常不需要了解哪个项目文件包含在哪里的细节。只是在你的主模块的dependencies块中引用另一个模块是告诉 Gradle 把它作为你的应用的一部分的高级方式。对您的本地分支进行相同的更改,并提交给 get。

Java 库依赖性

我们项目的下一个版本,包括在第 4 步中,包含了一个纯 Java 依赖。这展示了 Android 和 Gradle build 系统的灵活性,因为它为包含大量预先存在的代码打开了大门。选择文件➤新建模块打开新建模块向导,如图 13-7 所示。然后在第一个对话框中选择 Java 库。在第二个对话框中,输入 WeatherParse 作为库名,点击完成,如图 13-8 所示。

A978-1-4302-6602-0_13_Fig8_HTML.jpg

图 13-8。

Name the new JAR library

A978-1-4302-6602-0_13_Fig7_HTML.jpg

图 13-7。

Add a new JAR library

如你所见,添加 Java 库模块类似于添加 Android 模块。主要区别在第二个对话框中很明显,它的选项较少。这是因为 Java 模块通常只包含编译过的 Java 类文件,其输出是一个 jar 文件。与输出 aar 文件的 Android 库模块相比,AAR 文件可以包括布局、原生 C/C++ 代码、素材、布局文件等等。

这就引出了一个问题,为什么会有人想要使用 Java 模块而不是 Android 库呢?一开始优势并不明显,但是有了 Java 模块,你就有机会在 Android 平台之外重用你的 Java 代码。这在很多情况下都会让你受益。考虑一个服务器端 web 解决方案,它定义了一个复杂的图像处理算法来匹配相似的人脸。这种算法可以单独定义为一个 Gradle 项目,并直接在您的 Android 应用中使用,以添加相同的功能。Java 模块也可以与普通的 JUnit 测试用例集成。虽然 Android 包括 JUnit 框架的衍生产品,但这些测试用例必须在设备或仿真器上部署和执行,这在几个周期后很快成为一个繁琐的过程。使用 pure JUnit 来测试 Java 代码,这样只需点击一下按钮,测试就可以直接在 IDE 中运行。这些测试的运行速度通常比 Android JUnit 的同类产品快一个数量级。

我们的示例项目将包含一些复杂的 XML 解析逻辑,以理解来自国家气象局的 XML 响应。我们的WeatherParse使用开源的 kXML 库来解析响应。这是与 Android 运行时捆绑在一起的同一个库。挑战在于在 kXML 所在的 Android 运行时之外编译我们的解析器。虽然我们可以为 kXML 设置一个依赖项,但是我们还需要在设备上分发和使用我们的 Java 库,而不包括多余的 kXML API 副本。我们稍后将解决这个问题。现在,让我们看看添加的 Java 依赖关系的build.gradle文件:

apply plugin: 'java'

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile 'kxml:kxml:2.2.2'

testCompile 'junit:junit:4.11'

}

processTestResources << {

ant.copy(todir:sourceSets['test'].output.classesDir) {

fileset(dir:sourceSets['test'].output.resourcesDir)

}

}

除了 Java 插件的声明之外,这里没有太多内容。Java 插件配置 Gradle 生成一个 JAR 文件作为输出,同时设置编译、测试和打包类文件所需的构建步骤。dependencies { ... }块为 kXML 解析器和 JUnit 定义了编译时依赖关系。Gradle 将生成一个 Java JAR 文件,其中只包含项目中已编译的类。该项目还包括两个 Java 类文件(一个调用解析器,一个处理解析器事件)以及一个单元测试 Java 类。该测试将来自服务的典型天气 XML 响应的副本提供给解析器,并验证解析器可以提取天气信息。响应的副本保存在 resources 文件夹下。参见清单 13-10 中简短的单元测试代码片段。

Listing 13-10. WeatherParseTest.java

public class WeatherParseTest extends TestCase {

private WeatherParser weather;

private String asString(InputStream inputStream) throws IOException {

BufferedReader reader = new BufferedReader(

new InputStreamReader(inputStream));

StringBuilder builder = new StringBuilder();

for(String eachLine = reader.readLine(); eachLine != null;

eachLine = reader.readLine()) {

builder.append(eachLine);

}

return builder.toString();

}

public void setUp() throws IOException, XmlPullParserException {

URL weatherXml = getClass().getResource("/weather.xml");

assertNotNull("Test requires weather.xml as a resource at the CP root.",

weatherXml);

String givenXml = asString(weatherXml.openStream());

this.weather = new WeatherParser();

weather.parse(new StringReader(givenXml.replaceAll("<br>", "<br/>")));

}

public void testCanSeeCurrentTemp() {

assertEquals(weather.getCurrent("apparent"), "63");

assertEquals(weather.getCurrent("minimum"), "59");

assertEquals(weather.getCurrent("maximum"), "81");

assertEquals(weather.getCurrent("dew point"), "56");

}

public void testCanSeeCurrentLocation() {

assertEquals("Should see the location in XML", weather.getLocation(),

"Sunnyvale, CA");

}

}

任何单元测试都可以通过右击测试方法名并单击上下文菜单中的 run 选项来运行。反馈是即时的,因为测试直接在 IDE 中运行,没有启动或选择设备、上传 APK 文件和启动的开销。当您从 Android Studio 中的 Java 库运行单元测试时,Gradle 会在后台被调用,并将资源从 resources 文件夹复制到测试要定位的输出文件夹中。测试用例中的setUp方法利用复制的weather.xml文件,并使用定制的asString方法将它作为字符串读入。(另外,XML 包含 HTML <br>标签,需要使用 Java 的String replaceAll()方法正确终止这些标签,以防止 XML 解析异常。)方法setUp()继续创建一个WeatherParser对象,同时要求它解析 XML。前面代码中包含的两个测试方法演示了如何使用天气解析器从响应中找到当前温度和当前位置。

有了天气解析 Java 库,你可以自由地改变我们的天气请求 Android 库来使用它。要做到这一点,你需要做两件事。首先,您要确保 Java 库包含在GradleWeather项目根目录下的顶层settings.gradle文件中。接下来,您在WeatherRequest gradle build 中设置一个依赖项来获取WeatherParse项目输出。同样,WeatherParse项目是一个输出单个 JAR 文件的 Java 库,但是有一个微妙的细节需要注意。我们的 Java 库包括对 kXML 的依赖,它被认为是可传递的。我们可以在WeatherRequest模块中声明依赖关系如下:

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile project(':WeatherParse')

}

但是,这将导致以下编译器错误:

Output:

UNEXPECTED TOP-LEVEL EXCEPTION:

com.android.dex.DexException: A978-1-4302-6602-0_13_Figf_HTML.jpg

Multiple dex files define Lorg/xmlpull/v1/XmlPullParser;

许多开发人员感到沮丧的一个常见原因是,你的 APK 中包含了不止一个相同的文件。在这种情况下,例外来自于 Android,它已经将 kXML API 中定义的XmlPullParser作为 SDK 的一部分。Android SDK 使这些和其他 API 在任何 Android 应用或库项目的编译过程中可用。我们在构建WeatherParse模块时不会出错的原因是它被定义为一个 Java 库。Java 库项目是用 Java SDK 编译的,编译过程中不包含任何 Android APIs。为了解决这个错误,我们需要从WeatherRequest模块中考虑的依赖项列表中排除这个可传递的依赖项。我们将图 13-9 所示的代码添加到WeatherRequest模块的 Gradle 构建文件中,以消除错误。

A978-1-4302-6602-0_13_Fig9_HTML.jpg

图 13-9。

Exclude the kXML dependency

该项目现在被更新为解析 XML 天气响应,并通过使用来自 XML 的链接下载图像。NationalWeatherRequest对象将 URL 对象缓存为成员变量,并添加一个getWeatherXml方法来使用 URL,如清单 13-11 所示。

Listing 13-11. NationalWeatherRequest.java

public class NationalWeatherRequest {

public static final String NATIONAL_WEATHER_SERVICE =

"``http://forecast.weather.gov/MapClick.php?lat=%f&lon=%f&FcstType=dwml

private final URL url;

//...

public String getWeatherXml() {

InputStream inputStream = getInputStream(url);

return readWeatherXml(inputStream);

}

private String readWeatherXml(InputStream inputStream) {

StringBuilder builder = new StringBuilder();

if (inputStream!=null) {

BufferedReader weatherReader = new BufferedReader(

new InputStreamReader(inputStream));

try {

for(String eachLine = weatherReader.readLine(); eachLine!=null;

eachLine = weatherReader.readLine()) {

builder.append(eachLine);

}

} catch (IOException e) {

log("Exception reading data from Nat'l weather site " + e);

e.printStackTrace();

}

}

String weatherXml = builder.toString();

log("Weather data " + weatherXml);

return weatherXml;

}

private InputStream getInputStream(URL url) {

InputStream inputStream = null;

try {

inputStream = url.openStream();

} catch (IOException e) {

log("Exception opening Nat'l weather URL " + e);

e.printStackTrace();

}

return inputStream;

}

清单 13-12 详述了如何更新NationalWeatherRequestData对象以使用新的getWeatherXML方法,并将其结果提供给新的WeatherParse Java 组件。

Listing 13-12. NationalWeatherRequestData.java

public NationalWeatherRequestData(Context context) {

this.context = context;

Location location = getLocation(context);

weatherParser = new WeatherParser();

String weatherXml = new NationalWeatherRequest(location).getWeatherXml();

//National weather service returns XML data with embedded HTML <br> tags

//These will choke the XML parser as they don't have closing syntax.

String validXml = asValidXml(weatherXml);

try {

weatherParser.parse(new StringReader(validXml));

} catch (XmlPullParserException e) {

e.printStackTrace();

} catch (IOException e) {

e.printStackTrace();

}

}

public String asValidXml(String weatherXml) {

return weatherXml.replaceAll("<br>","<br/>");

}

@Override

public List<TemperatureItem> getTemperatureItems() {

ArrayList<TemperatureItem> temperatureItems =

new ArrayList<TemperatureItem>();

List<Map<String, String>> forecast = weatherParser.getLastForecast();

if (forecast!=null) {

for(Map<String,String> eachEntry : forecast) {

temperatureItems.add(new TemperatureItem(

context.getResources().getDrawable(R.drawable.progress),

eachEntry.get("iconLink"),

eachEntry.get("day"),

eachEntry.get("shortDescription"),

eachEntry.get("description")

));

}

}

return temperatureItems;

}

TemperatureAdapter级经历了一次大修,变得相当复杂。它使用来自WeatherRequest的图像链接在后台下载图像。参见清单 13-13 中的定义。

Listing 13-13. TemperatureAdapter.java

public class TemperatureAdapter extends BaseAdapter {

private final Context context;

List<TemperatureItem>items;

public TemperatureAdapter(Context context) {

this.context = context;

this.items = new ArrayList<TemperatureItem>();

}

@Override

public int getCount() {

return items.size();

}

@Override

public Object getItem(int position) {

return items.get(position);

}

@Override

public long getItemId(int position) {

return position;

}

@Override

public View getView(int position, View convertView, ViewGroup parent) {

View view = convertView != null ? convertView : createView(parent);

TemperatureItem temperatureItem = items.get(position);

ImageView imageView = (ImageView) view.findViewById(R.id.imageIcon);

imageView.setImageDrawable(temperatureItem.getImageDrawable());

if(temperatureItem.getIconLink()!=null){

Animation animation = AnimationUtils.loadAnimation(

context, R.anim.progress_animation);

animation.setInterpolator(new LinearInterpolator());

imageView.startAnimation(animation);

((ViewHolder) view.getTag()).setIconLink(temperatureItem.getIconLink());

}

((TextView) view.findViewById(R.id.dayTextView)).setText(

temperatureItem.getDay());

((TextView) view.findViewById(R.id.briefForecast)).setText(

temperatureItem.getForecast());

((TextView) view.findViewById(R.id.description)).setText(

temperatureItem.getDescription());

return view;

}

class ViewHolder {

private final View view;

private String iconLink;

private AsyncTask<String, Integer, Bitmap> asyncTask;

public ViewHolder(View view) {

this.view = view;

}

public void setIconLink(String iconLink) {

if(this.iconLink != null && this.iconLink.equals(iconLink)) return;

else this.iconLink = iconLink;

if(asyncTask != null) {

asyncTask.cancel(true);

}

asyncTask = new AsyncTask<String,Integer,Bitmap>() {

@Override

protected Bitmap doInBackground(String... url) {

InputStream imageStream;

try {

imageStream = new URL(url[0]).openStream();

} catch (IOException e) {

e.printStackTrace();

return null;

}

return BitmapFactory.decodeStream(imageStream);

}

@Override

protected void onPostExecute(final Bitmap bitmap) {

if (bitmap == null) {

return;

}

new Handler(context.getMainLooper()).post(new Runnable() {

@Override

public void run() {

ImageView imageView = (ImageView) view

.findViewById(R.id.imageIcon);

imageView.clearAnimation();

imageView.setImageBitmap(bitmap);

}

});

asyncTask = null;

}

};

asyncTask.execute(iconLink);

}

}

private View createView(ViewGroup parent) {

LayoutInflater inflater = (LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

View inflatedView = inflater.inflate(R.layout.temperature_summary,

parent, false);

inflatedView.setTag(new ViewHolder(inflatedView));

return inflatedView;

}

public void setTemperatureData(TemperatureData temperatureData) {

items = temperatureData.getTemperatureItems();

notifyDataSetChanged();

}

}

每个ImageViews都与一个ViewHolder相关联,并用一个微调图标和一个模拟无限进度微调的旋转动画来初始化。大部分工作都是用ViewHoldersetIconLink方法完成的。该方法触发后台天气图标的下载。下载完成后,ImageView会根据下载的图像进行更新。并且旋转动画被取消。同样,这个类文件非常复杂,只是为了处理图像的加载。简化不是更好吗?

第三方库

有时你没有实现一个复杂逻辑的能力或专业知识。第三方库经常被用来解决 Android 开发中的这些和其他棘手的问题。如前所述,调用由地球上其他地方的另一个开发人员或团队开发的代码与调用项目中另一个模块的代码几乎是一样的。我们继续 Step5 分支,在这里我们将演示如何将开源组件用于 Gradle Weather 项目。我们的应用下载了一系列图片,每张图片代表某一天的情况。我们从图 13-10 所示的 app 模块下的 Gradle build 的极简添加开始。

A978-1-4302-6602-0_13_Fig10_HTML.jpg

图 13-10。

Add the universal image loader

就这样!您将立即看到一个黄色提示,指示 Gradle 文件已更改,以及一个超链接文本按钮,使项目同步开始。点击图 13-11 所示的链接,允许 Android Studio 将底层 IntelliJ 项目文件与依赖项同步。格雷尔会在后台下载它们。

A978-1-4302-6602-0_13_Fig11_HTML.jpg

图 13-11。

Gradle files need to be synced

项目同步和下载完成后,可以更改代码来调用 API。回顾之前的内容,我们可以体会到在后台下载天气图标是多么容易:

private final ImageLoader imageLoader;

List<TemperatureItem>items;

public TemperatureAdapter(Context context, ImageLoader imageLoader) {

this.context = context;

this.imageLoader = imageLoader;

this.items = new ArrayList<TemperatureItem>();

}

public void setIconLink(String iconLink) {

final ImageView imageView = (ImageView) view.findViewById(

R.id.imageIcon);

imageLoader.displayImage(iconLink, imageView,

new SimpleImageLoadingListener(){

@Override

public void onLoadingComplete(String imageUri, View view,

Bitmap loadedImage) {

imageView.clearAnimation();

super.onLoadingComplete(imageUri, view, loadedImage);

}

});

}

构造函数被更新以获取一个ImageLoader对象并将其存储在一个实例变量中。setIconLink方法只是将iconLink交给了ImageLoader,后者完成了所有繁重的工作。

打开旧项目

Android Studio 现在包括强大的导入工具,用于将旧项目迁移到新的 Gradle build 系统中。这种支持几乎是透明的,并且会在您打开旧项目时自动发生。在 Android Studio 的早期测试阶段,许多人在打开这些旧项目时会感到恼火。令人沮丧的部分是 Gradle 的快速更新周期,这可能导致旧版本有时无法工作。当你在旧版本上使用新版本的 Gradle 时会发生这种情况。在导入旧项目时使用 Gradle 包装器应该可以在某种程度上减轻这种痛苦,但有时这并不可行或有效。当您在更新版本的 Android Studio 中打开一个旧项目时,例如,从 0.8x 版本迁移到 1.x 版本,您可能会看到如图 13-12 所示的不支持的 Android Gradle 插件错误。

A978-1-4302-6602-0_13_Fig12_HTML.jpg

图 13-12。

Unsupported version error

你可以点击修复插件版本并重新导入项目链接,但是你会看到图 13-13 中的错误,它抱怨一个丢失的 DSL 方法runProGuard()。有了 Gradle 的新知识,您可以推测 DSL 方法是什么,并且您现在知道打开应用的build.gradle文件来找到这个错误的方法调用。1.x 版本不赞成这个调用,而支持minifyEnabled

A978-1-4302-6602-0_13_Fig13_HTML.jpg

图 13-13。

DSL method not found error

摘要

您已经探索了 Gradle 构建系统的基础。我们展示了一个具有不同依赖类型的多模块 Android 项目。您还看到了如何在 Android 项目中将常规 Java 代码与 JUnit 测试结合起来。最后,您学习了如何使用 Android Studio 内置的导入功能来打开旧项目。您演练了如何修复这些旧项目导入的一些常见问题。Gradle 还包括一个强大的依赖管理系统,允许您轻松地在项目间重用代码。这一章仅仅触及了 Android Studio 中 Gradle 的一些皮毛。请自行探索,并进一步增强示例项目。

十四、更多 SDK 工具

Android Studio 是 IntelliJ IDEA 的一个特殊构建,它包含了面向 Android 开发的工具。本章探讨了您可以使用的各种工具。其中许多被嵌入到各种工具窗口中,其他的只需轻轻一击。

Android 设备监视器

Android 设备监视器(ADM)是 SDK 中最强大的工具之一。它允许您从多个角度监控您的设备,并检查诸如内存、CPU、网络利用率等内容。要开始使用 ADM,请从 Android Studio 菜单中选择工具➤ Android ➤ Android 设备监视器。打开的窗口左侧有一个设备视图。

在这个视图中,您应该看到连接到开发计算机的所有设备,以及在每个设备上运行的进程列表。如果您的应用没有运行,请启动它,然后在进程列表中找到它。该名称应该遵循通常的包命名约定。如果读进程名有困难,您可以在设备视图中调整各个列的大小。点击你的应用将其选中,它将成为 ADM 中各种工具的焦点,在这些示例中,你将分析 Gradle Weather 应用。图 14-1 显示了选择 Gradle Weather 应用的 ADM 窗口。

A978-1-4302-6602-0_14_Fig1_HTML.jpg

图 14-1。

The Android Device Monitor screen

线程监视器

选中您的应用后,您可以通过单击启用 ADM 中的特性来开始探索其执行的各种特征。线程活动是比较容易监控的事情之一。单击 Update Threads 按钮,用活动线程列表以及 id、状态和名称填充右侧视图。当您执行更新时,单击右视图中的任何线程将显示其活动的更多细节。其他详细信息将作为堆栈跟踪显示在“线程”选项卡下的窗格中。例如,单击名为 main 的线程。您可能会看到类似如下的堆栈跟踪:

at android.os.MessageQueue.nativePollOnce(Native Method)

at android.os.MessageQueue.next(MessageQueue.java:138)

at android.os.Looper.loop(Looper.java:123)

at android.app.ActivityThread.main(ActivityThread.java:5086)

at java.lang.reflect.Method.invokeNative(Native Method)

at java.lang.reflect.Method.invoke(Method.java:515)

at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:785)

at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:601)

at dalvik.system.NativeStart.main(Native Method)

主线程通常迭代android.os.MessageQueue,寻找用户交互。当在屏幕上做手势、敲键盘或发生其他交互时,系统会将活动记录为消息并填充MessageQueue。系统调用nativePollOnce()来检索这些消息,然后将它们作为事件发送到你的应用。这个调用是从由主循环调用的MessageQueue.next()方法调用的,主循环由ActivityThread.main()方法调用。沿着堆栈往下看,您可以看到主线程是由Zygote.Init()启动的,它是您启动设备时首先启动的进程之一。您可以单击堆栈跟踪上方的刷新按钮来更新它。

探索应用中其他线程的堆栈跟踪,了解它们在做什么。在图 14-2 中,我们探索了 Gradle Weather 项目中众多通用图像加载器线程之一,同时更新了堆栈跟踪。堆栈跟踪揭示了从网络流中读取图像并将其解码为位图所涉及的工作。

A978-1-4302-6602-0_14_Fig2_HTML.jpg

图 14-2。

The thread monitor

堆监视器

堆监视器允许您在应用运行时检查在堆上分配的对象。单击 ADM 窗口右侧 Threads 选项卡旁边的 Heap 选项卡,将 heap monitor 置于前台。保持您的应用在设备窗格中处于选中状态,单击更新堆按钮以启用堆更新,如图 14-3 所示。每次垃圾收集器在设备上运行时,都会发生堆更新;每次执行时,描述堆的新数据都被发送到 ADM 用户界面。在偶然的用例下与你的应用的交互可能最终触发垃圾收集器的执行。您还可以通过单击看起来像垃圾桶的垃圾收集图标,随时强制执行。

A978-1-4302-6602-0_14_Fig3_HTML.jpg

图 14-3。

The heap monitor

Heap 选项卡中充满了标识堆上各个对象的类型和数量以及每种类型的最小和最大大小的详细信息。选择单个类型允许您深入查看该特定类型的分配计数。在我们的示例中,我们深入研究了 11,212 个 2 字节数组对象,它们占据了堆中最大的总空间。堆详细信息下面的图表显示,有超过 2,500 个 2 字节数组正好是 32 字节长。这些数组可能是用于图标的分配,因为 32 字节是管理图像数据的最佳大小。

分配跟踪器

分配跟踪器也可以用来跟踪你的应用中内存的使用情况。您可以通过“分配跟踪器”选项卡访问分配跟踪器,该选项卡位于“堆”选项卡旁边,有两个按钮:开始跟踪和获取分配。单击开始跟踪按钮开始跟踪分配。单击获取分配按钮,在用户分配视图中加载捕获的数据。跟踪器运行时,“开始”按钮变成“停止”按钮。您可以随时单击停止跟踪来终止跟踪器。

在捕获时,视图将显示顺序、大小、类、线程 ID 以及每个分配的类和方法。该列表最初按大小降序排序,但您可以单击任何列标题来更改排序顺序。重复单击列标题可在升序和降序之间切换排序顺序。单击视图中的任何条目都会加载发生分配的堆栈跟踪。同样,这个例子使用了 Gradle Weather 应用,您可以滚动列表。该应用将加载不同日子的图标,同时跟踪分配。图 14-4 说明了结果。

A978-1-4302-6602-0_14_Fig4_HTML.jpg

图 14-4。

The allocations tracker results from Gradle Weather

作为从网络下载图标数据的一部分,您可以看到几个 32KB byte数组的分配。如果您的应用遇到内存不足的问题,这可能是优化的目标。除非遇到内存不足的情况,否则不应该优化代码,理解这一点很重要。过早地优化代码会导致不必要的复杂性,并且可能与您的性能优化目标背道而驰。

网络统计

网络统计选项卡能够监控网络流量。这个工具和其他工具一样容易使用。图 14-5 显示了开始网络统计数据捕获之前的选项卡。单击网络统计选项卡上的开始按钮,开始捕获网络流量。“开始”按钮变成了“停止”按钮,单击它可以停止采集。

A978-1-4302-6602-0_14_Fig5_HTML.jpg

图 14-5。

Tracking network statistics

该视图将显示一个图表,绘制应用运行时的传入和传出流量。图表顶部的 RX 部分代表响应数据,而 TX 部分代表传输数据。在我们的示例中,我们捕获了 1MB 的响应数据,这些数据是在 Gradle Weather 应用中滚动列表视图以下载图像数据时发生的。设备已经发送了总计 52KB 的请求数据。

层次结构查看器

通常,您可能很难正确渲染布局。您的活动中可能有逻辑,根据用户交互有条件地定位视图或设置可见性。当事情变得复杂时,转储 ADM 中的视图层次结构会有所帮助。单击屏幕截图中的元素会在屏幕截图右侧的窗格中显示细分。细分以树形结构给出,节点代表ViewGroup对象。图 14-6 显示了 Gradle Weather 应用的层级转储。您可以浏览这些节点,查看它们各自的布局属性。您还可以钻取到任何节点以浏览其子对象。

A978-1-4302-6602-0_14_Fig6_HTML.jpg

图 14-6。

Exploring the Gradle Weather UI by using a hierarchy ump

单击右窗格中的单个节点将在屏幕截图中找到相应的视图对象,同时在其周围绘制一个红色矩形高亮显示。任何选定节点的属性都显示在树视图窗格下的窗格中。这些属性表明视图是否可见、聚焦、可点击、被选择等等。您还可以检查视图边界、资源 ID 和内容描述。如果您遇到一个应该可见但没有的视图,您可以选择包含的ViewGroup布局并钻取以找到该视图。

对视图的一个常见误解是View.INVISIBLEView.GONE常量属性值之间的差异。标记为View.GONE的视图不会出现在层次结构中。标记为View.INVISIBLE的视图将出现在层次结构中,但不会被绘制到屏幕上。另一个常见的问题是理解如何在ViewGroup布局或容器上使用wrap_content属性为视图保留空间,即使它们是不可见的。如果视图被标记为View.GONE,容器将不会保留空间,并将缩小尺寸以容纳任何剩余的内容。

Note

Android 设备监视器基于 Eclipse 工具,让您能够通过切换视角来调整用户界面。如果您不熟悉 Eclipse,请理解透视图代表了一个特定的工作流,并且选项卡和视图以一种最适合该工作流的方式定位。Eclipse 工具通常有几个预配置的透视图,同时允许您创建自己的透视图。因为 ADM 中的许多工具都被嵌入到 Android Studio IDE 中,所以本节只讨论 ADM 专用工具的一个子集。

点击窗口➤打开透视图,查看监视器可用的工作流程,如图 14-7 所示。

A978-1-4302-6602-0_14_Fig7_HTML.jpg

图 14-7。

Switching the perspective in ADM

单击层次视图选项打开层次视图透视图。层次查看器与层次转储工具的不同之处在于,它仅适用于模拟器或根设备。要使用级别查看器,请启动模拟器并在模拟器中启动您的应用。单击刷新按钮,然后在 Windows 选项卡的设备列表中找到您的模拟器。您的屏幕应该类似于图 14-8 。在设备列表中找到代表您的应用的进程,然后单击加载按钮,从您的应用加载当前屏幕的视图层次。层次视图提供了当前在屏幕上呈现的布局的大而深的树形视图。

A978-1-4302-6602-0_14_Fig8_HTML.jpg

图 14-8。

Exploring the Gradle Weather UI using the Hierarchy viewer

ADM 窗口左侧的“视图属性”选项卡包含一个全面的属性列表,而中间的窗格则显示层次结构的放大视图。您可以在窗口的右下角找到布局视图选项卡,它显示了当前屏幕的类似线框的摘要。单击这些选项卡中的元素会选择其他选项卡中的等效元素,因为它们都保持同步。

Android 显示器集成

Android Studio 在 IDE 底部的 Android DDMS 视图中捆绑了来自 ADM 的一些更常用的工具。这些工具允许您生成系统信息转储、执行垃圾收集、终止应用、分析堆以及执行方法跟踪。随着你的应用越来越复杂,这些工具将会成为你宝库中的无价之宝。在 Android DDMS 视图中,在进程列表中选择您的应用。可以在 Android 视图中的设备➤日志目录选项卡下找到进程列表。如果它还不在最前面,请单击此选项卡使其成为焦点。选择运行应用的流程后,将会启用附加工具按钮。

内存监视器

内存监视器显示当前调试的应用消耗的内存的图形图表。它可用于轻松识别一般内存趋势。单击屏幕右下角的内存监视器按钮,该按钮位于事件日志和 Gradle 控制台按钮旁边。这将打开监视器工具窗口。试验您的应用,并在监视器运行时观察图表。在图 14-9 中,我们运行 Gradle Weather 应用,同时滚动预测列表以查看内存影响。您还可以使用 Initiate GC 按钮在任何时间点触发垃圾收集,并查看回收了多少内存。如果图形中使用的内存在启动垃圾收集器后没有恢复到合理的水平,您的应用可能会泄漏内存。

A978-1-4302-6602-0_14_Fig9_HTML.jpg

图 14-9。

Memory consumption of Gradle Weather while scrolling the list

方法跟踪工具

方法跟踪工具可以帮助您找到需要大量 CPU 周期来执行的方法。CPU 周期是一种宝贵的资源,方法应该这样对待它们。当一个或多个方法在 CPU 上运行得太舒服时,应用就会变慢。如果您的应用速度变慢,或者您只想更好地了解典型用例中 CPU 的使用情况,您可以使用方法跟踪工具来记录在任何给定场景下使用应用时的活动。

方法跟踪工具使用起来很简单。准备好你的应用,或者让它进入你想要检查的状态。从流程列表中选择您的应用后,单击启动方法跟踪图标开始跟踪。使用您的应用练习您感兴趣的任何方法,然后再次单击按钮以完成方法跟踪。在图 14-10 中,我们在滚动列表时从 Gradle Weather 捕捉到了活动。

A978-1-4302-6602-0_14_Fig10_HTML.jpg

图 14-10。

The Method Trace tool

在本例中,我们运行了 Gradle Weather 应用,并在录制时滚动了天气条目列表。当您最初完成方法跟踪时,视图将默认为主线程。每个方法调用都以可视化方式表示,为调用绘制条形。这些条根据它们的独占时间来着色,独占时间是仅在该方法中花费的时间,不包括在它调用的方法中花费的时间。“线程”下拉列表可用于切换其他线程的视图,以便您可以看到它们遇到的活动。该图探索了发生在后台线程(不是主线程)中的图像加载和解码。虽然在主线程上做大量的工作是 UI 迟缓的常见原因,但是您永远不能排除在其他线程上做的工作。只要注意到有多少额外的线程正在运行以及它们正在执行什么工作,许多问题就会浮出水面。

使用鼠标上的滚轮可以放大和缩小跟踪视图。围绕鼠标光标在屏幕上的位置进行缩放。需要一段时间来习惯探索跟踪视图,因为您可能已经习惯了典型的左/右滚动行为,这在查看器中是不存在的。要找到一个位图加载方法调用的细节,您可以在查看器中找到它,然后用鼠标指向它。然后向下滚动,放大你需要的视觉细节。缩放时,查看器会包含更多细节,堆栈中较低的方法调用会被显示和标记。稍后,要查看之前发生的方法调用,您可以向上滚动鼠标滚轮以缩小并查看更多的跟踪。然后,您将指向先前的方法调用,并重复该过程。

可视化查看器下面的表格显示了所有方法调用的细目分类。该细分包括名称、调用计数以及包含和排除时间。所有这些计时都与记录跟踪所花费的时间相关。如果你花 4 秒钟记录一个轨迹,50%的读数相当于 2 秒钟。您可以将鼠标悬停在查看器中的任何方法调用上,等待 2 秒钟,工具提示就会出现,并以毫秒为单位给出准确的时间。

分配跟踪器

分配跟踪器现在内置在 Android Studio 中。它的工作原理与 ADM 类似。单击 Android DDMS 工具窗口下左侧工具栏中的内存跟踪器,开始跟踪分配。在应用运行时与其互动,然后再次点按该按钮以停止跟踪分配。编辑器中将打开一个新的选项卡,显示跟踪的结果。如图 14-11 所示。

A978-1-4302-6602-0_14_Fig11_HTML.jpg

图 14-11。

The built-in allocation tracker

屏幕捕获

Android DDMS 窗口包含几个选项,允许你在使用应用时捕捉屏幕。屏幕捕获按钮立即捕获设备的当前屏幕,并在预览对话框中加载图像,您可以选择将其保存到磁盘。图 14-12 显示了该对话框。“屏幕截图”对话框还允许您通过使用手机或平板电脑设计的框架来给图像加框。有放大和缩小屏幕的缩放控件。您可以启用投影、屏幕眩光,甚至在保存之前旋转图像。单击“重新加载”按钮,用当前屏幕渲染的图像刷新对话框。

A978-1-4302-6602-0_14_Fig12_HTML.jpg

图 14-12。

Using the Screen Capture tool

屏幕录制按钮允许您在与应用交互时录制屏幕视频。点击该按钮,会出现如图 14-13 所示的对话框,提示您选择录制比特率和分辨率。单击开始录制按钮开始录制并使用您的应用。完成后,单击“停止录制”以生成包含录制的交互的视频文件。另一个对话框将提示您保存记录。使用任何文件名,并将其保存到系统中容易找到的位置。Windows 用户可能需要安装替代编解码器或软件,因为文件以 MP4 格式保存。图 14-14 展示了使用 Windows 上流行的 VLC 播放器回放与 Gradle Weather 应用的交互。

A978-1-4302-6602-0_14_Fig14_HTML.jpg

图 14-14。

Playback of a screen recording

A978-1-4302-6602-0_14_Fig13_HTML.jpg

图 14-13。

Starting the Screen Recorder tool

导航编辑器

导航编辑器是 Android Studio 中的一个全新功能。虽然在撰写本文时它已经可以使用了,但它仍在大量开发中。该编辑器允许您在进出特定活动和片段的编辑模式时,快速构建应用的高级流程原型。如果你对一个应用有一个粗略的想法,并想想象用户如何在屏幕之间移动,导航编辑器是理想的工具。它还可以发现现有应用中屏幕之间的现有流程和连接。随着时间的推移,看到这个工具成熟将是令人兴奋的。

让自己熟悉它的最好方法就是一个全新的项目。想象一下,你想设计一个新的购物应用,允许用户通过他们喜欢的社交网络凭证快速注册,并随意浏览商品列表。找到一件商品后,用户可以在决定购买前点击它以获得更多细节。要设计这样的流程,您可以使用手绘草图、白板或其他工具,这些工具提供了与您的 IDE 的有限集成。将您的粗略想法转化为功能性应用的过程可能是一个艰巨的过程,外部工具会在您工作时管理多个设计师程序时增加额外的麻烦。在使用 IDE 时,人们经常使用线框或图表工具,如 OmniGraffle、Lucidchart 等。在这些程序之间转换以实现一个工作应用的过程并不总是简单明了的。导航编辑器为您提供了一种方法,可以在您的 IDE 中轻松地构建原型和绘制流程。在本节中,您将使用该工具探索我们的购物应用。

设计用户界面

使用新建项目向导和空白活动模板,创建一个名为 Navigate 的项目。项目加载后,您应该在设计模式下开始编辑activity_main xml 布局。移除 Hello World 标签,拖出一个下面有三个按钮的大文本标签。将标签的文本更改为 Mini-Shopper,并更改按钮上的文本以反映三个虚构的社交网络服务。图 14-15 中的例子使用了 FaceBox、Twiggler 和 G++,但是你可以随心所欲地发挥创造力。

A978-1-4302-6602-0_14_Fig15a_HTML.jpg A978-1-4302-6602-0_14_Fig15b_HTML.jpg

图 14-15。

Designing the FaceBox UI

导航编辑器的第一步

接下来,从主菜单中单击工具 Android ➤导航编辑器。您的屏幕将类似于图 14-16 。

A978-1-4302-6602-0_14_Fig16_HTML.jpg

图 14-16。

Opening the Navigation Editor

Android Studio 将创建一个main.nvg.xml文件,并将其呈现在导航编辑器中。它会直观地显示你的活动及其相关的 Android 上下文菜单。(空白活动模板会自动创建此上下文菜单。)该编辑器允许您快速创建新的活动,并将这些活动与现有活动上的控件相关联,以创建转换。它还允许你连接到 Android 系统上下文菜单中的项目。您可以在编辑器中单击并拖动项目,如上下文菜单。

右键单击编辑器中的任意位置,打开带有单个新活动选项的编辑器上下文菜单,如图 14-17 所示。单击此选项打开新建活动向导。选择空白活动模板,并将新活动命名为 FaceBoxLoginActivity。您将返回到导航视图,该视图现在显示这两个活动。

A978-1-4302-6602-0_14_Fig17_HTML.jpg

图 14-17。

Create a new activity with the Navigation Editor

连接活动

重新定位新活动,使其与原始活动相邻。你需要在它们之间建立联系。在编辑器中工作时,可以随意重新定位上下文菜单。按住 Shift 键,同时单击 FaceBox 按钮并拖动到新的FaceBoxLoginActivity。编辑器将在它们之间绘制一条连接线,一个粉红色的点代表位于线中间的过渡。单击此点查看过渡的定义。过渡通过按压手势将源MainActivity连接到目的FaceBoxLoginActivity,如图 14-18 所示。

A978-1-4302-6602-0_14_Fig18_HTML.jpg

图 14-18。

Connecting activities with the Navigation Editor

现在打开MainActivity.java源文件。您应该看到一个点击监听器连接到启动FaceBoxLoginActivity的按钮:

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

MainActivity.this.startActivity(new Intent(MainActivity.this,

FaceBoxLoginActivity.class));

}

});

}

这段代码是通过在编辑器中简单的点击和拖动操作生成的。返回导航编辑器,双击FaceBoxLoginActivity。您将被设置到这个活动的图形编辑视图,在这里您可以拖放以用更多的控件和选项来装饰它。创建一个极简的登录界面,有两个TextView标签,两个用于用户名和密码的EditText输入框,最后是一个登录按钮。图 14-19 显示了假装 FaceBox 登录屏幕。

A978-1-4302-6602-0_14_Fig19_HTML.jpg

图 14-19。

Designing the FaceBox login screen

编辑菜单

返回导航编辑器,这将反映出FaceBoxLogin布局的变化。您可以运行应用来测试过渡和新的FaceBoxLogin布局更改。在导航编辑器中,双击与登录活动关联的上下文菜单。menu_facebox_login.xml文件将被打开,并在右侧显示一个即时预览窗口。更改菜单中的单个项目,将其 ID 设为@+id/action_back,标题设为@string/action_back。按 Alt+Enter 弹出意图对话框,提示创建新的字符串值资源的动作,如图 14-20 所示。按回车键执行此操作。

A978-1-4302-6602-0_14_Fig20_HTML.jpg

图 14-20。

Editing the FaceBox menu

在资源对话框中键入 back 作为新字符串的值,然后按 Enter 键继续。返回导航编辑器。现在,您将从新菜单项建立到MainActivity的连接。像以前一样,按住 Shift 键,从后退菜单项单击并拖动到MainActivity。当您建立新的连接时,编辑器将在MainActivity中生成代码。打开MainActivity.java文件,查看下面生成的代码:

@Override

public boolean onPrepareOptionsMenu(Menu menu) {

boolean result = super.onPrepareOptionsMenu(menu);

menu.findItem(R.id.action_back).setOnMenuItemClickListener(new

MenuItem.OnMenuItemClickListener() {

@Override

public boolean onMenuItemClick(MenuItem menuItem) {

FaceBoxLoginActivity.this.startActivity(new Intent(FaceBoxLoginActivity.this,

MainActivity.class));

return true;

}

});

return result;

}

在建立这些连接时,构建并运行应用,以测试过渡是如何进行的。此时,您应该能够从主活动转换到FaceBoxLogin活动,然后通过使用新的上下文菜单项返回到主活动。

现在您已经对导航编辑器的基本用法有了一些了解,试着为应用再创建两个活动,一个用于显示项目列表,一个用于查看项目细节。

末端的

也许您的工具箱中需要的最实用的插件是终端。单击 IDE 底部的“终端”选项卡,打开一个终端窗口,您可以在其中输入操作系统命令。您可以单击绿色加号按钮在单独的选项卡中开始新的会话。当您找不到或记不起等效的 IDE 时,命令窗口可以帮助您完成任务。也许你需要了解的终端中最重要的工具是 ADB,Android 调试桥。此工具使您可以直接控制连接的设备或仿真器。该命令采用adb {device-options} sub-command {sub-command-options}的形式。设备选项如下:-d指向唯一连接的设备,-e指向唯一连接的仿真器,或者-s deviceID指向具有给定 ID 的特定设备。

打开您的终端,研究本节其余部分描述的命令。

查询设备

adb devices

devices子命令列出每个连接设备的名称和设备 id。模拟器将列出一个设备 ID,格式为emulator-<port>

安装 APK

adb install /path/to/app.apk

install命令会将一个 Android APK 推送到设备上并安装它。只需提供开发机器上 APK 文件的路径。

下载文件

adb pull /path/to/device/file.ext /path/to/local/destination/

pull命令将任意文件从设备下载到您的开发机器上。

上传文件

adb push /path/to/local/file.ext /path/to/device/destination/

push命令将任意文件从您的开发机器上传到设备。

左舷向前

adb forward local-port remote-port

forward命令将把开发机器上的网络连接重定向到设备。这是一种在高级场景中使用的技术,例如调试 Chrome web 浏览器中运行的代码或连接到设备上运行的网络服务器。

谷歌云工具

之前,您探索了一个使用网络服务收集天气预报的 Android 应用。在本节中,您将探索如何使用 Google Cloud tools 开发和部署您自己的后端。首先,您将设计前端,它将与任意 bean 通信以构建问候。稍后,您将构建后端并在本地运行它。最后,您将发布到 Google 的云服务,并对项目进行端到端的测试。首先,您需要使用您的 Google 帐户登录 Google,如图 14-21 所示。

A978-1-4302-6602-0_14_Fig21a_HTML.jpg A978-1-4302-6602-0_14_Fig21b_HTML.jpg

图 14-21。

Sign into Google

创建 HelloCloud 前端

使用空白活动模板创建一个新的 Android 项目,并将其命名为 HelloCloud。将空白活动命名为 MainActivity,然后单击 Finish 开始您的项目。对您的MainActivity使用清单 14-1 中的代码,对您的activity_main.xml布局使用清单 14-2 中的 XML。

Listing 14-1. The MainActivity for the HelloCloud Front End

public class MainActivity extends Activity {

private SimpleCloudBean cloudBean;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setCloudBean(new SimpleCloudBean());

setContentView(R.layout.activity_main);

}

public void onGoClick(View sender) {

final TextView txtResponse = (TextView) findViewById(R.id.txtResponse);

txtResponse.setText(getCloudBean().getResponse());

txtResponse.setVisibility(View.VISIBLE);

}

public SimpleCloudBean getCloudBean() {

return cloudBean;

}

public void setCloudBean(SimpleCloudBean cloudBean) {

this.cloudBean = cloudBean;

}

public class SimpleCloudBean {

public CharSequence getResponse() {

return "This response is from " + getClass().getSimpleName();

}

}

}

Listing 14-2. The activity_main.xml for the HelloCloud Front End

<RelativeLayout xmlns:android="``http://schemas.android.com/apk/res/android

xmlns:tools="``http://schemas.android.com/tools

android:layout_width="match_parent"

android:layout_height="match_parent"

android:paddingLeft="@dimen/activity_horizontal_margin"

android:paddingRight="@dimen/activity_horizontal_margin"

android:paddingTop="@dimen/activity_vertical_margin"

android:paddingBottom="@dimen/activity_vertical_margin"

tools:context=".MainActivity">

<TextView

android:text="@string/greeting_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:id="@+id/txtGreeting" />

<Button

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="go!"

android:id="@+id/button"

android:layout_below="@+id/txtGreeting"

android:layout_alignParentRight="true"

android:layout_alignParentEnd="true"

android:layout_marginRight="42dp"

android:layout_marginTop="72dp"

android:onClick="onGoClick" />

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:textAppearance="?android:attr/textAppearanceLarge"

android:text="Response Shows Here"

android:id="@+id/txtResponse"

android:layout_below="@+id/txtGreeting"

android:layout_alignParentLeft="true"

android:layout_alignParentStart="true"

android:layout_marginTop="34dp"

android:visibility="invisible" />

</RelativeLayout>

这段代码调用一个简单的本地 bean,该 bean 返回对活动的响应。响应在一个隐藏的TextView组件中更新,然后被设置为View.Visible

创建 Java 端点后端模块

现在,您可以向项目中添加一个新的后端模块。这个后端模块将包含在 web 服务器上运行的代码。点击文件➤新建模块,选择谷歌云模块,如图 14-22 所示。

A978-1-4302-6602-0_14_Fig22_HTML.jpg

图 14-22。

Create an App Engine module

命名你的模块后端,保留其他选项的默认设置,如图 14-23 所示。点击 Finish,Android Studio 将生成一个基本的 Java servlet 项目,并带有一个随时可用的 Google Cloud 端点。Gradle 将开始用新模块同步您的项目。

A978-1-4302-6602-0_14_Fig23_HTML.jpg

图 14-23。

Select App Engine Java Endpoints Module

同步完成后,右键单击项目窗口中的后端模块,然后选择“使模块成为后端”选项。接下来,在运行配置列表中找到后端选项,并单击 run 按钮启动它。Android Studio 将把 servlet 代码包装在本地运行的 Jetty web servlet 引擎的一个实例中,供您探索。控制台会提示如何使用 web 浏览器与终端进行交互。启动您的浏览器并将其指向http://localhost:8080/以查看运行中的端点。您将看到如图 14-24 所示的页面。

A978-1-4302-6602-0_14_Fig24_HTML.jpg

图 14-24。

Running your Google Cloud Endpoint

连接零件

在验证端点正在运行之后,您可以让 Android Studio 生成并安装可以在您的 Android 应用中使用的客户端库。在右侧的 Gradle 构建工具窗口中找到后端模块的构建,并运行appengineEndpointsInstallClientLibs任务。如图 14-25 所示。

A978-1-4302-6602-0_14_Fig25_HTML.jpg

图 14-25。

Install the client libs for your endpoint

Android Studio 早期版本的菜单中有一个选项,最近从版本 1.0.1 中删除了。在 0.8.x 版本中,您可以单击工具➤谷歌云工具➤安装客户端库。图 14-26 显示了之前的菜单。

A978-1-4302-6602-0_14_Fig26_HTML.jpg

图 14-26。

Earlier versions of Android Studio had the task baked into the menu

Android Studio 触发了一个特殊的 Gradle build,它将生成一个可安装的客户端库,作为 Android 客户端和后端 web 服务器之间的代理。Gradle 构建完成后,您可以在后端模块的构建文件夹中的client-libs文件夹下找到作为 ZIP 文件的客户端库。ZIP 文件包含一个readme.html文件,其中包含所有关于如何使用它的说明。寻找编译时依赖项,这些依赖项需要复制到使用端点的模块中。您可以忽略解释如何安装客户端库的额外说明,因为 IDE 会在生成过程中执行此步骤。

在应用模块的build.gradle文件中添加编译时依赖项后,您的dependencies块应该如下所示:

dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])

compile ([group: 'com.apress.gerber.cloud.backend', name: 'myApi',

version: 'v1-1.19.0-SNAPSHOT'])

compile([group: 'com.google.api-client', name: 'google-api-client-android',

version: '1.19.0'])

//    compile project(path: ':backend', <- remove this line

//      configuration: 'android-endpoints') <- remove this line

}

当我们向项目中添加新模块时,我们示例中被注释掉的依赖项是自动添加的。应该删除它,因为您不希望应用直接链接到 servlet 代码;相反,它使用客户端库来代理请求。您还必须确保已经将本地 Maven 存储库添加到项目中。打开顶层build.gradle文件并将其添加到allprojects部分:

allprojects {

repositories {

jcenter()

mavenLocal()

}

}

在添加了依赖项和mavenLocal存储库之后,您应该将您的项目与 Gradle build 同步,以使 API 可用。在您的应用模块中添加一个新类来使用它。调用这个类 RemoteCloudBeanAsyncTask 并使其扩展AsyncTask。声明一个MyApi类型的静态变量。应该提示您导入该类,它现在应该在类路径中可用。如果您没有导入它的选项,请仔细检查您的依赖项并重新构建模块,以确保您已经正确地包含了生成的客户端库。清单 14-3 定义了这个新类。

Listing 14-3. The RemoteCloudBeanAsyncTask Class Definition

class RemoteCloudBeanAsyncTask extends AsyncTask<String, Void, String> {

public static final String RESULT = "result";

private static MyApi apiService = null;

private final Handler handler;

public RemoteCloudBeanAsyncTask(Handler handler) {

this.handler = handler;

}

@Override

protected String doInBackground(String... params) {

String name = params[0];

try {

return getMyApi().sayHi(name).execute().getData();

} catch (IOException e) {

return e.getMessage();

}

}

private MyApi getMyApi() {

//Lazily initialize the API service

if(apiService == null) {

MyApi.Builder builder = new MyApi.Builder(AndroidHttp.newCompatibleTransport(),

new AndroidJsonFactory(), null)

// The special 10.0.2.2 IP points to the local machine's IP address

// in the emulator

.setRootUrl("``http://10.0.2.2:8080/_ah/api/

.setGoogleClientRequestInitializer(new

GoogleClientRequestInitializer() {

@Override

public void initialize(AbstractGoogleClientRequest<?>

abstractGoogleClientRequest) throws IOException {

abstractGoogleClientRequest.setDisableGZipContent(true);

}

});

apiService = builder.build();

}

return apiService;

}

@Override

protected void onPostExecute(String result) {

final Message message = new Message();

final Bundle data = new Bundle();

data.putString(RESULT, result);

message.setData(data);

handler.sendMessage(message);

}

}

记住使用AsyncTask很重要,因为初始化服务和进行网络调用需要一些时间。这个对象是用 Android 处理程序实例化的,稍后将在逻辑中使用。我们在doInBackground方法中检索对 API 的引用。返回引用的方法创建并延迟初始化它。获得 API 引用后,调用 web 端点,并返回调用结果。然后,一条消息被发送到onPostExecute方法中的处理程序。

通过修改onGoClick方法将此AsyncTask插入MainActivity;

public void onGoClick(View sender) {

final RemoteCloudBeanAsyncTask remoteCloudBeanAsyncTask =

new RemoteCloudBeanAsyncTask(new Handler() {

@Override

public void handleMessage(Message msg) {

super.handleMessage(msg);

final String result = msg.getData().getString(RemoteCloudBeanAsyncTask.RESULT);

final TextView txtResponse = (TextView) findViewById(R.id.txtResponse);

txtResponse.setText(result);

txtResponse.setVisibility(View.VISIBLE);

}

});

remoteCloudBeanAsyncTask.execute("Developers");

}

这里我们创建了RemoteCloudBeanAsyncTask并给它一个处理程序,将消息传递给隐藏的文本视图并使其可见。当后端服务器仍然在您的开发机器上运行时,在模拟器上构建并运行这个示例。点击 Go 按钮,您应该会看到来自 Google Cloud 端点的返回消息,如图 14-27 所示。如果您收到指示超时的消息,请仔细检查您的服务器是否仍在运行,并且可以使用 web 浏览器访问。确保您已经在清单中声明了 Internet 权限。您可能还需要更改或禁用已启用的任何主动防火墙设置。

A978-1-4302-6602-0_14_Fig27_HTML.jpg

图 14-27。

Running the app on the emulator against the endpoint

部署到应用引擎

既然这项服务已经在本地运行并产生了结果,你可以部署到谷歌的云服务器上。部署到云很简单。如果后端正在本地运行,请停止它。使用您的 Google 帐户在 https://console.developers.google.com/project 登录开发者控制台。单击创建项目按钮,在 Google 的云服务中创建新的端点。给项目起一个名字,比如 MyBackend,然后将生成的项目 ID 复制并保存到一个可以访问的地方。示例见图 14-28 。点击创建,您将看到如图 14-29 所示的进度指示器。给它一点时间让谷歌服务完成这个过程。返回 Android Studio,找到appengine-web.xml文件,并将保存的项目 id 复制到application标签中。这个文件在srcmainwebappWEB-INF下。

A978-1-4302-6602-0_14_Fig29_HTML.jpg

图 14-29。

Google Developers Console will spin momentarily while it works

A978-1-4302-6602-0_14_Fig28_HTML.jpg

图 14-28。

Creating a new Java Endpoints project with Google Developers Console

从顶部菜单中,选择构建➤将模块部署到应用引擎。单击“部署到”下拉列表,并选择您的项目 ID。首次部署时,您需要登录 Google 进行选择。图 14-30 显示了点击部署到下拉菜单后的登录屏幕。

A978-1-4302-6602-0_14_Fig30_HTML.jpg

图 14-30。

Sign into Google Developers Console

登录提示打开浏览器窗口,如图 14-31 所示。单击“接受”以授予必要的权限。

A978-1-4302-6602-0_14_Fig31_HTML.jpg

图 14-31。

Google Developers Console permission prompt

后端发布后,切换到之前创建的AsyncTask并更新加载 API 的方法:

private MyApi getMyApiRemote() {

//Lazily initialize the API service

if(apiService == null) {

MyApi.Builder builder = new MyApi.Builder(

AndroidHttp.newCompatibleTransport(), new AndroidJsonFactory(), null)

.setRootUrl("https://{your-project-id}.appspot.com/_ah/api/");

apiService = builder.build();

}

return apiService;

}

用您在线创建的项目的项目 ID 替换{your-project-id}。在您的设备或模拟器上构建并运行该应用,您应该会得到相同的结果。

摘要

本章探讨了用于分析和设计应用的各种工具。它着眼于从不同方面探索应用性能的许多可用选项。您学会了使用新的导航编辑器快速构建想法的原型,这些想法可以在以后构建到成熟的应用中。最后,您深入研究了 Google 的云服务,了解了如何使用 Google 提供的强大计算引擎来构建、测试和部署客户机服务器应用。这些工具都为您提供了强大的控制和洞察力,可用于构建健壮的应用。

十五、Android Wear Lab

Android Wear 是谷歌的最新技术创新之一,为更亲密的用户体验创造了机会。在撰写本文时,只有少数设备支持 Android Wear,但这个列表正在增长。目前只支持手表,但随着技术的成熟,可穿戴设备可以包括从项链到实际衣服的任何东西。这些设备中有来自三大制造商的手表:三星、摩托罗拉和索尼。在本章中,您将学习如何构建一个可穿戴的应用,它可以从 Android Studio 以有线和无线方式部署和运行。

Note

我们邀请您使用 Git 克隆这个项目,以便跟进,尽管您将从头开始使用它自己的 Git 库重新创建这个项目。如果您的计算机上没有安装 Git,请参见第七章。在 Windows 中打开一个 Git-bash 会话(或者在 Mac 或 Linux 中打开一个终端)并导航到 C:\androidBook\reference(如果没有参考目录,请创建一个。在 Mac 上,导航到/your-labs-parent-dir/reference/)并发出以下 git 命令:git clone https://bitbucket.org/csgerber/megadroid MegaDroid。

设置您的可穿戴环境

在你开始开发可穿戴应用之前,你需要采取一些步骤来准备你的工作环境。虽然可以只使用模拟器进行开发,但最好手边有一个实际的穿戴设备。确保您的设备运行的是最新版本的操作系统,如果您使用的是 Windows PC,请下载并安装所有必要的驱动程序。连接您的可穿戴设备,并在 Android DDMS 工具窗口的设备列表中查找它。如果出现,请跳过下一部分。

安装设备驱动程序

在 Windows 上,如果您计划通过 USB 部署应用,可能需要为某些设备安装驱动程序。请注意,只有在连接设备时设备未被识别时,才安装驱动程序。如果您计划使用蓝牙部署应用,可以跳过这一部分。第一次连接设备时,Windows 将尝试自动安装驱动程序,但会失败。打开 Windows 设备管理器,在列表中的“其他设备”下找到您的设备。图 15-1 说明了你所看到的。

A978-1-4302-6602-0_15_Fig1_HTML.jpg

图 15-1。

The Samsung Gear Live as listed in the Device Manager without drivers

右键单击已卸载的设备,然后从上下文菜单中单击更新驱动程序软件。在弹出窗口中选择浏览我的电脑中的驱动软件,如图 15-2 所示。

A978-1-4302-6602-0_15_Fig2_HTML.jpg

图 15-2。

Browse your computer for the driver

点击“让我从我电脑的设备驱动列表中选择”,如图 15-3 所示。

A978-1-4302-6602-0_15_Fig3_HTML.jpg

图 15-3。

Click the “Let me pick from a list” option

点击 Android 设备,如图 15-4 所示。

A978-1-4302-6602-0_15_Fig4_HTML.jpg

图 15-4。

Select Android Device

从下一个弹出窗口中选择复合驱动程序,然后单击下一步。您的驱动程序的供应商会有所不同,可能与图 15-5 中的驱动程序不匹配。您可以安全地使用任何供应商提供的复合驱动程序。驱动程序将会安装,您将一切就绪。

A978-1-4302-6602-0_15_Fig5_HTML.jpg

图 15-5。

Select the Composite ADB Interface from any vendor

设置您的 SDK 工具

在开始开发之前,下载并安装 SDK 平台 5.0.1 或更高版本,并将 SDK 工具更新到 24.0.2 或更高版本。Android Wear 支持从平台版本 4.4W.2 和 SDK tools 23.0 开始提供。然而,本章中使用的示例使用了在更高版本的 SDK 平台中发现的特性。您还需要在 Extras 下安装更新的 SDK 平台和 Google 存储库的示例。

设置穿戴虚拟设备

使用工具栏按钮或通过单击工具➤安卓➤ AVD 管理器启动 AVD 管理器,然后单击创建虚拟设备。在左侧的类别窗格中选择 Wear,在可用硬件配置文件列表中选择 Android Wear Square 或 Android Wear Round,如图 15-6 所示。选择 5.0.1 或更高的 API 级别,因为本章中的示例需要 API 5.0.1。根据开发计算机的功能,您可能希望选择一个 x86 系统映像。这些映像使用较少的 CPU 周期,通常运行速度更快,因为它们不必模拟 CPU。但是,这些系统映像需要安装 HAXM,这是英特尔开发的硬件加速执行管理器。HAXM 是从 SDK 管理器安装的。HAXM 依赖于英特尔虚拟化技术(VT)支持,而该技术在某些机器上可能不可用。单击“下一步”按钮,并在向导的最后一页保留默认值。确保选择“使用主机 GPU”以获得最佳速度。

A978-1-4302-6602-0_15_Fig6_HTML.jpg

图 15-6。

Select the Wear category

选择最新的 API 级别(撰写本文时是 Lollipop)。然后点击下一步,如图 15-7 所示。

A978-1-4302-6602-0_15_Fig7_HTML.jpg

图 15-7。

Select Lollipop for the system image

在下一个屏幕中给你的 AVD 命名为 Android Wear Square API 21,如图 15-8 所示。

A978-1-4302-6602-0_15_Fig8_HTML.jpg

图 15-8。

Give your AVD the name Android Wear Square API 21

单击 Finish 构建 AVD。一旦建立,你可以点击列表中 Wear AVD 旁边的下拉箭头,复制它,如图 15-9 所示。如果您创建了方形 AVD,请将您的副本更改为使用圆形。否则,将其更改为使用方形外形。您希望使用这两种类型的外形来测试您的应用是否能在尽可能多的变化中正常工作。

A978-1-4302-6602-0_15_Fig9_HTML.jpg

图 15-9。

Duplicate your AVD to create Android Wear Round API 21

设置您的 Android Wear 硬件

如果你有一个可穿戴设备,你需要设置它以允许开发。可穿戴应用是通过 Android 智能手机或平板电脑部署和管理的,因此您将需要其中一个来实现可穿戴应用的开发。在 Android 智能手机上,在 Google Play 上安装 Android Wear 应用。启动应用,使用它将智能手机与可穿戴设备配对。

有两种方法可以将应用部署到可穿戴设备上:有线或蓝牙。有线是最简单的选择,但是如果你缺少 USB 端口或者你不想和设备驱动程序较劲,蓝牙是一个不错的选择。当您支持智能手机、平板电脑以及只有几个端口的电脑上的 Wear 时,可能会出现这种情况。

启用开发者模式

如果您从未启用开发人员模式,或者您的设备是全新的,请遵循以下步骤,您将能够设置许多选项,如始终开启模式、蓝牙调试、调试布局等:

Open the Settings app on your wearable device by pressing and holding the button on the side for 2 seconds.   Scroll to the bottom and tap the About option.   When the About screen opens, tap the build number seven times. Afterward, you will find the Developer option under the About option in the Settings list.   Open the developer options and enable ADB debugging.

使用蓝牙调试

如果您希望无线工作,请在开发者选项屏幕中启用蓝牙调试。接下来,打开命令终端并运行以下两个 ADB 命令:

adb forward tcp:4444 localabstract:/adb-hub

adb connect localhost:4444

观察移动设备上运行的 Android Wear 应用的状态。它应该更改为以下内容:

Host: connected

Target: connected

此时,您的可穿戴设备就可以安装应用了。

创建巨型机器人项目

这一节演示了如何基于一个名为 MegaDroid 的虚拟视频游戏角色创建一个定制的 watch-face 项目。你可以把 MegaDroid 想象成两个流行的 80 年代电子游戏角色的混搭物(这个名字将保持不变)。表盘将体现一个用双剑与敌人战斗的太空战士的形象。该应用将作为一个额外的实际游戏部署。图 15-10 展示了练习的最终结果。你可以用这个例子作为秘诀,让你的品牌深入你的目标受众的手腕。对自定义手表表面的支持是 Lollipop 中引入的新特性。这一功能使您的应用能够作为设备的实际表面运行,并为新型用户体验创造了机会。您的应用可以显示各种来源的信息,包括但不限于互联网、GPS、配对移动设备的日历或联系人列表等。由于 watch face 是一个完整的、持续运行的 Android 应用,因此它可以作为您的应用的整体扩展。这个示例只涵盖了绘制用户界面和从运行时接收更新以推进时间的基础知识。

A978-1-4302-6602-0_15_Fig10_HTML.jpg

图 15-10。

The final result of the MegaDroid watch face

使用第一章中描述的新建项目向导开始您的新 Android Wear 项目。在向导的第二页,选中磨损复选框,选择 SDK 5.0 或更高版本,如图 15-11 所示。保留其余屏幕的默认值,为移动应用选择一个空白活动,为磨损组件选择一个空白磨损活动。完成最后一页上的向导,等待项目生成。点击运行按钮,在可穿戴设备或 AVD 上测试您的项目。

A978-1-4302-6602-0_15_Fig11_HTML.jpg

图 15-11。

Select Wear in the New Project Wizard

为穿戴设备设计应用时,掌握 Android 布局和设计至关重要。对于一个最佳的产品来说,你最初开发的大部分时间最好花在你选择的图形编辑器上,主要关注设计方法、尺寸、颜色等等。每个表盘都是独一无二的,你的方法会根据你想要完成的目标而有所不同。设计一个简单的数字钟面需要不同的方法,比设计一个模拟的花费更少。Android 网站上的在线开发者文档可能会让一些没有太多设计经验的人感到有些害怕。一般来说,该网站建议您应该为方形和圆形模型进行设计,决定如何或是否集成附加数据,允许系统 UI 元素保持可见,并支持不同的显示模式。这些显示模式将在后面的章节中解释。您还可以考虑提供一个配置屏幕。这个例子试图简化这个过程,并有意忽略其中的一些考虑因素。

针对屏幕技术进行优化

Wear 运行时将在两种显示模式下执行您的应用:环境模式和交互模式。观看或使用应用时,手表会在这些模式之间切换。环境模式由系统自动启用,以节省电池寿命。因此,您的 Wear 应用应该会检测到这种模式,并通过将其显示输出更改为使用暗淡的颜色来做出相应的响应。在这种模式下,更新每分钟发送一次,因此降低屏幕绘制的次数也是有意义的。此示例将在此模式下移除秒针,并将绘制速度从每秒更改为每分钟一次。

某些设备支持低位环境模式。在这种模式下,设备屏幕退回到有限的调色板。这有助于减少电池使用并防止屏幕老化。您可以检测此模式,并将图形调整为仅使用黑色、白色、蓝色、红色、洋红色、绿色、青色和黄色。使用你的画的轮廓而不是整个图像也是好的。在低位模式下,你的背景应该以黑色为主。非黑色像素不应超过总像素的 10 %,而彩色像素不应超过屏幕的 5%。这适用于支持这种特殊绘图模式的设备。在此模式下绘图时,还应该禁用抗锯齿功能。抗锯齿是一种模糊绘图边缘的技术,使它们看起来不那么像素化,如图 15-12 和 15-13 所示。

A978-1-4302-6602-0_15_Fig13_HTML.gif

图 15-13。

An anti-aliased image

A978-1-4302-6602-0_15_Fig12_HTML.gif

图 15-12。

An image without anti-aliasing

在我们的例子中,为了简单起见,我们将在环境模式下使用灰度版本的图形,如图 15-14 所示。将参考克隆中的所有图像复制到当前项目中。在 windows 上,导航到 C:\ Android book \ reference \ mega droid \ wear \ src \ main 文件夹,右键单击,然后复制 res 目录。导航到 C:\ Android book \ mega droid \ wear \ src \ main \ res 文件夹,然后右键单击并将复制的文件夹粘贴到现有的 RES 文件夹上。在 Mac 或 Linux 上,从终端运行以下命令:

A978-1-4302-6602-0_15_Fig14_HTML.jpg

图 15-14。

Grayscale artwork

CP-r≤Android book/reference/mega droid/wear/src/main/RES≤Android book/mega droid/wear/src/main/

构建 WatchFace 服务

一个表盘服务负责创建一个WatchFaceService.Engine,它是表盘的核心。WatchFaceService.Engine响应系统回调,负责更新时间和绘制人脸。创建一个新的MegaDroidWatchFaceService类来扩展CanvasWatchFaceService类。用清单 15-1 中的代码填充。

Listing 15-1. The MegaDroidWatchFaceService Class

public class MegaDroidWatchFaceService extends CanvasWatchFaceService {

private static final String TAG = "MegaDroidWatchSvc";

@Override

public Engine onCreateEngine() {

// create and return the watch face engine

return new MegaDroidEngine(this);

}

/* implement service callback methods */

private class MegaDroidEngine extends CanvasWatchFaceService.Engine {

private final Service service;

public MegaDroidEngine(Service service) {

this.service = service;

}

/**

* initialize your watch face

*/

@Override

public void onCreate(SurfaceHolder holder) {

super.onCreate(holder);

}

/**

* called when system properties are changed

* use this to capture low-bit ambient.

*/

@ Override

public void onPropertiesChanged(Bundle properties) {

super.onPropertiesChanged(properties);

}

/**

* This is called by the runtime on every minute tick

*/

@Override

public void onTimeTick() {

super.onTimeTick();

}

/**

* Called when there's a switched in/out of ambient mode

*/

@Override

public void onAmbientModeChanged(boolean inAmbientMode) {

super.onAmbientModeChanged(inAmbientMode);

}

@Override

public void onDraw(Canvas canvas, Rect bounds) {

//Draw the watch face here

}

/**

* Called when the watch face becomes visible or invisible

*/

@Override

public void onVisibilityChanged(boolean visible) {

super.onVisibilityChanged(visible);

}

}

}

注册服务

将以下内容添加到 Android 清单中的结束标签application之前:

<service

android:name=".MegaDroidWatchFaceService"

android:label="@string/mega_droid_service_name"

android:allowEmbedded="true"

android:taskAffinity=""

android:permission="android.permission.BIND_WALLPAPER" >

<meta-data

android:name="android.service.wallpaper"

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

<meta-data

android:name="com.google.android.wearable.watchface.preview"

android:resource="@drawable/preview_analog" />

<meta-data

android:name="com.google.android.wearable.watchface.preview_circular"

android:resource="@drawable/preview_analog_circular" />

<intent-filter>

<action android:name="android.service.wallpaper.WallpaperService" />

<category

android:name=

"com.google.android.wearable.watchface.category.WATCH_FACE" />

</intent-filter>

</service>

按 F2 键浏览错误。按 Alt+Enter 可以显示建议来逐个修复错误。第一个建议将让您为新服务生成一个名称。这将是显示在您的设备上的手表面孔图片和姓名列表中的姓名。下一个建议是为壁纸元数据标签生成一个 XML 描述符。创建xml/watch_face.xml并用以下代码填充它:

<?xml version="1.0" encoding="utf-8"?>

<wallpaper xmlns:android="``http://schemas.android.com/apk/res/android

IntelliJ 建议不应该修复接下来的两个错误。这些是对可绘制资源的引用,这些资源将成为面选取器中您的手表面的预览。您将需要使用图形编辑器来创建它们。或者,你可以截取应用的截图,但这有点本末倒置的感觉。如果你不熟悉图像编辑器或图形设计程序,你可以在这里临时添加一些虚拟图像来让应用运行。然后你可以在你的应用看起来已经完成后返回,截取手表运行的截图,并使用这些截图。

初始化可绘制资源和样式

onCreate()方法中,添加以下逻辑来设置样式并初始化可绘制资源:

public void onCreate(SurfaceHolder holder) {

super.onCreate(holder);

setWatchFaceStyle(new WatchFaceStyle.Builder(service)

.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)

.setStatusBarGravity(Gravity.RIGHT | Gravity.TOP)

.setHotwordIndicatorGravity(Gravity.LEFT | Gravity.TOP)

.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)

.setShowSystemUiTime(false)

.build());

Resources resources = service.getResources();

Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg);

this.backgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();

this.character = ((BitmapDrawable) resources.getDrawable(

R.drawable.character_standing)).getBitmap();

this.logo = ((BitmapDrawable) resources.getDrawable(

R.drawable.megadroid_logo)).getBitmap();

this.minuteHand = ((BitmapDrawable) resources.getDrawable(

R.drawable.minute_hand)).getBitmap();

this.hourHand = ((BitmapDrawable) resources.getDrawable(

R.drawable.hour_hand)).getBitmap();

this.secondPaint = new Paint();

secondPaint.setARGB(255, 255, 0, 0);

secondPaint.setStrokeWidth(2.f);

secondPaint.setAntiAlias(true);

secondPaint.setStrokeCap(Paint.Cap.ROUND);

this.time = new Time();

}

管理观察更新

将以下两个静态字段添加到封闭的 MegaDroid 中:

private static final long INTERACTIVE_UPDATE_RATE_MS =

TimeUnit.SECONDS.toMillis(1);

private static final int MSG_UPDATE_TIME = 0;

现在定义一个更新处理程序,它将基于前面定义的INTERACTIVE_UPDATE_RATE_MS常量触发观察器更新:

/** Handler to update the time once a second in interactive mode. */

final Handler mUpdateTimeHandler = new Handler() {

@Override

public void handleMessage(Message message) {

switch (message.what) {

case MSG_UPDATE_TIME:

if (Log.isLoggable(TAG, Log.VERBOSE)) {

Log.v(TAG, "updating time");

}

invalidate();

if (shouldTimerBeRunning()) {

long timeMs = System.currentTimeMillis();

long delayMs = INTERACTIVE_UPDATE_RATE_MS

- (timeMs % INTERACTIVE_UPDATE_RATE_MS);

mUpdateTimeHandler.sendEmptyMessageDelayed(

MSG_UPDATE_TIME, delayMs);

}

break;

}

}

};

private boolean shouldTimerBeRunning() {

return isVisible() && !isInAmbientMode();

}

该处理程序将在最初被调用后继续根据更新间隔安排更新。它只是使显示无效,这触发了一个隐式的onDraw调用。然后,它检查表盘是否可见,并在重新安排未来更新之前处于环境模式。当服务被运行时垃圾收集时,实现如下的onDestroy方法来清理更新处理程序:

@Override

public void onDestroy() {

mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);

super.onDestroy();

}

实现onPropertiesChanged方法来跟踪lowBitAmbient模式。设置一个布尔成员变量来跟踪环境模式是否正在运行。这就是你决定何时降低拉伸速率的方法。在实现中使用以下代码:

public void onPropertiesChanged(Bundle properties) {

super.onPropertiesChanged(properties);

this.lowBitAmbient = properties.getBoolean(

PROPERTY_LOW_BIT_AMBIENT, false);

if (Log.isLoggable(TAG, Log.DEBUG)) {

Log.d(TAG, "onPropertiesChanged: low-bit ambient = " + lowBitAmbient);

}

}

您还需要定义lowBitAmbient成员字段:

private boolean lowBitAmbient;

onTimeTick方法中添加对 invalidate 的调用。这里您调用了invalidate,它触发了屏幕的重绘:

@Override

public void onTimeTick() {

super.onTimeTick();

if (Log.isLoggable(TAG, Log.DEBUG)) {

Log.d(TAG, "onTimeTick: ambient = " + isInAmbientMode());

}

invalidate();

}

现在实现onAmbientModeChanged回调。这里你在环境模式下使用黑白插图。作为一种优化,您还将关闭秒针的反走样绘制,这在前面已经解释过了。

public void onAmbientModeChanged(boolean inAmbientMode) {

super.onAmbientModeChanged(inAmbientMode);

if (Log.isLoggable(TAG, Log.DEBUG)) {

Log.d(TAG, "onAmbientModeChanged: " + inAmbientMode);

}

if(inAmbientMode) {

character = ((BitmapDrawable) service.getResources().getDrawable(

R.drawable.character_standing_greyscale)).getBitmap();

logo = ((BitmapDrawable) service.getResources().getDrawable(

R.drawable.megadroid_logo_bw)).getBitmap();

hourHand = ((BitmapDrawable) service.getResources().getDrawable(

R.drawable.hour_hand_bw)).getBitmap();

minuteHand = ((BitmapDrawable) service.getResources()

.getDrawable(R.drawable.minute_hand_bw)).getBitmap();

} else {

character = ((BitmapDrawable) service.getResources()

.getDrawable(R.drawable.character_standing)).getBitmap();

logo = ((BitmapDrawable) service.getResources()

.getDrawable(R.drawable.megadroid_logo)).getBitmap();

hourHand = ((BitmapDrawable) service.getResources()

.getDrawable(R.drawable.hour_hand)).getBitmap();

minuteHand = ((BitmapDrawable) service.getResources()

.getDrawable(R.drawable.minute_hand)).getBitmap();

}

if (lowBitAmbient) {

boolean antiAlias = !inAmbientMode;

secondPaint.setAntiAlias(antiAlias);

}

invalidate();

// Whether the timer should be running depends on whether

//we're in ambient mode (as well

// as whether we're visible), so we may need to start

//or stop the timer.

updateTimer();

}

这调用了updateTimer方法,您将在接下来定义它。updateTimer方法将向mUpdateTimeHandler发送空的更新消息。您希望仅在表盘可见且不处于环境模式时发送更新。使用以下代码片段作为您的实现:

private void updateTimer() {

if (Log.isLoggable(TAG, Log.DEBUG)) {

Log.d(TAG, "updateTimer");

}

mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);

if (shouldTimerBeRunning()) {

mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);

}

}

使用清单 15-2 定义一个广播接收器,以响应时区的变化。register 和 unregister 方法将用于使接收器能够听到时区更改事件。

Listing 15-2. The Time-Zone BroadcastReceiver

final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

time.clear(intent.getStringExtra("time-zone"));

time.setToNow();

}

};

boolean mRegisteredTimeZoneReceiver = false;

private void registerReceiver() {

if (mRegisteredTimeZoneReceiver) {

return;

}

mRegisteredTimeZoneReceiver = true;

IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);

service.registerReceiver(mTimeZoneReceiver, filter);

}

private void unregisterReceiver() {

if (!mRegisteredTimeZoneReceiver) {

return;

}

mRegisteredTimeZoneReceiver = false;

service.unregisterReceiver(mTimeZoneReceiver);

}

每当接收到消息时,接收器将更新时间。它用一个IntentFilter注册,这个IntentFilterACTION_TIMEZONE_CHANGED动作相关联。使用IntentFilter是一种将活动或BroadcastReceiver绑定到特定类型意图动作的编程方式。

现在定义onVisibilityChanged回调来注册接收者并启动更新处理程序:

@Override

public void onVisibilityChanged(boolean visible) {

super.onVisibilityChanged(visible);

if (Log.isLoggable(TAG, Log.DEBUG)) {

Log.d(TAG, "onVisibilityChanged: " + visible);

}

if (visible) {

registerReceiver();

// Update time zone in case it changed while we weren't visible.

time.clear(TimeZone.getDefault().getID());

time.setToNow();

} else {

unregisterReceiver();

}

// Whether the timer should be running depends on whether

//we're visible (as well as

// whether we're in ambient mode), so we may need to start

//or stop the timer.

updateTimer();

}

画脸

绘制表盘是你的大部分逻辑会发生的地方。对invalidate的调用最终导致对onDraw方法的调用。在这个方法中,您将集合用于渲染面部的各种图形和艺术品。每次更新都会根据经过的小时、分钟和秒来旋转和绘制表针。覆盖onDraw方法,使用清单 15-3 中的代码。

Listing 15-3. The Full onDraw Method Implementation

public void onDraw(Canvas canvas, Rect bounds) {

time.setToNow();

int width = bounds.width();

int height = bounds.height();

// Draw the background, scaled to fit.

if (backgroundScaledBitmap == null

|| backgroundScaledBitmap.getWidth() != width

|| backgroundScaledBitmap.getHeight() != height) {

backgroundScaledBitmap = Bitmap.createScaledBitmap(backgroundBitmap,

width, height, true /* filter */);

}

canvas.drawBitmap(backgroundScaledBitmap, 0, 0, null);

canvas.drawBitmap(character, (width- character.getWidth())/2,

((height- character.getHeight())/2)+ 20, null);

canvas.drawBitmap(logo, (width- logo.getWidth())/2,

(logo.getHeight()*2), null);

float secRot = time.second / 30f * (float) Math.PI;

int minutes = time.minute;

float minRot = minutes / 30f * (float) Math.PI;

float hrRot = ((time.hour + (minutes / 60f)) / 6f ) * (float) Math.PI;

// Find the center. Ignore the window insets so that, on round

//watches with a "chin", the watch face is centered on the

//entire screen, not just the usable portion.

float centerX = width / 2f;

float centerY = height / 2f;

Matrix matrix = new Matrix();

int minuteHandX = ((width - minuteHand.getWidth()) / 2)

- (minuteHand.getWidth() / 2);

int minuteHandY = (height - minuteHand.getHeight()) / 2;

matrix.setTranslate(minuteHandX-20, minuteHandY);

float degrees = minRot * (float) (180.0 / Math.PI);

matrix.postRotate(degrees+90, centerX,centerY);

canvas.drawBitmap(minuteHand, matrix, null);

matrix = new Matrix();

int rightArmX = ((width - hourHand.getWidth()) / 2)

+ (hourHand.getWidth() / 2);

int rightArmY = (height - hourHand.getHeight()) / 2;

matrix.setTranslate(rightArmX + 20, rightArmY);

degrees = hrRot * (float) (180.0 / Math.PI);

matrix.postRotate(degrees-90, centerX,centerY);

canvas.drawBitmap(hourHand, matrix, null);

float secLength = centerX - 20;

if (!isInAmbientMode()) {

float secX = (float) Math.sin(secRot) * secLength;

float secY = (float) -Math.cos(secRot) * secLength;

canvas.drawLine(centerX, centerY, centerX + secX,

centerY + secY, secondPaint);

}

}

一次看一部分代码将有助于您理解整个流程。首先捕获传递给方法的bounds对象的宽度和高度:

int width = bounds.width();

int height = bounds.height();

接下来,您检查背景的缩放版本,并将其绘制到屏幕上。背景需要缩放到给定的宽度和高度。您只需要缩放一次,因为每次重绘时边界都是相同的。

// Draw the background, scaled to fit.

if (backgroundScaledBitmap == null

|| backgroundScaledBitmap.getWidth() != width

|| backgroundScaledBitmap.getHeight() != height) {

backgroundScaledBitmap = Bitmap

.createScaledBitmap(backgroundBitmap,

width, height, true /* filter */);

}

canvas.drawBitmap(backgroundScaledBitmap, 0, 0, null);

现在你画一个字符,后面跟着一个标志。一个数学扭曲被用来水平居中字符,但是给他一个从垂直中心 20 像素的偏移量。要居中,取边界宽度和字符宽度之差,并将其一分为二。高度也是如此,但偏移增加了 20 个像素:

canvas.drawBitmap(character,(width- character.getWidth())/2,

((height- character.getHeight())/2)+ 20, null);

canvas.drawBitmap(logo, (width- logo.getWidth())/2,

(logo.getHeight()*2), null);

下一节使用一点几何学来寻找分针、时针和秒针的旋转角度。它使用一个公式将经过的秒数或分钟数除以 30π。对于小时,这是一个更复杂的问题。在加上经过分钟的微小偏移后,将小时除以 6。然后将结果乘以π。分钟偏移量是通过将经过的分钟分成 60 个小片段来计算的,因为每一分钟是一个小时的 1/60。偏移量是可选的。如果您希望时针直接与当前小时对齐而不逐渐前进,可以省略这部分计算:

float secRot = time.second / 30f * (float) Math.PI;

int minutes = time.minute;

float minRot = minutes / 30f * (float) Math.PI;

float hrRot = ((time.hour + (minutes / 60f)) / 6f ) * (float) Math.PI;

接下来,您找到屏幕的中心来定位时针、分针和秒针:

float centerX = width / 2f;

float centerY = height / 2f;

使用中心点,在绘制分针之前,围绕屏幕中心执行平移和旋转。所用角度的计算方法是将分钟乘以 180/π。示例中使用的图形直接指向 9 点钟方向,因此需要将旋转角度增加 90 度:

Matrix matrix = new Matrix();

int minuteHandX = ((width - minuteHand.getWidth()) / 2)

- (minuteHand.getWidth() / 2);

int minuteHandY = (height - minuteHand.getHeight()) / 2;

matrix.setTranslate(minuteHandX-20, minuteHandY);

float degrees = minRot * (float) (180.0 / Math.PI);

matrix.postRotate(degrees+90, centerX,centerY);

canvas.drawBitmap(minuteHand, matrix, null);

时针使用几乎相同的操作,但由于图形指向分针的对面,您必须从旋转角度减去 90 °:

matrix = new Matrix();

int rightArmX = ((width - hourHand.getWidth()) / 2) + (hourHand.getWidth() / 2);

int rightArmY = (height - hourHand.getHeight()) / 2;

matrix.setTranslate(rightArmX + 20, rightArmY);

degrees = hrRot * (float) (180.0 / Math.PI);

matrix.postRotate(degrees-90, centerX,centerY);

canvas.drawBitmap(hourHand, matrix, null);

最后,你画秒针,它只是一条从屏幕中心延伸出来的红线。你通过从中心减去 20 个像素来计算秒针的长度。您将绘制逻辑放在一个条件块中,当设备处于环境模式时,该条件块不会触发。终点 x 坐标由旋转角度的正弦值乘以长度确定。y 坐标是通过取旋转角度余弦的倒数并乘以长度来确定的。使用这些坐标,您在画布上调用drawLine方法,向其传递中心 X 和 Y,并将计算出的secXsecY添加到中心,以确定线条的终点。我们使用在onCreate方法中创建的 paint 对象将它们联系在一起:

float secLength = centerX - 20;

if (!isInAmbientMode()) {

float secX = (float) Math.sin(secRot) * secLength;

float secY = (float) -Math.cos(secRot) * secLength;

canvas.drawLine(centerX, centerY, centerX + secX,

centerY + secY, secondPaint);

}

现在构建并运行您的 watch 服务,并将其部署到设备上。图 15-15 显示了我们在 Galaxy Gear 实时手表上运行的示例。

A978-1-4302-6602-0_15_Fig15_HTML.jpg

图 15-15。

MegaDroid watch face running on the device

摘要

通过本练习,您学习了如何设计定制的表盘。您了解了如何通过 USB 和蓝牙部署可穿戴应用。您学习了如何响应环境模式来优化电池。您发现了设计一个表盘所涉及的各种组件,包括服务和引擎,以及控制重绘速率的自定义计时器。虽然这一章仅仅是作为一个介绍,但在可穿戴应用的世界中存在着一些机会。Watch faces 可以完全访问系统服务,并可以检索日历条目以进行自定义显示、地址簿联系人、电池寿命信息等。

十六、定制 Android Studio

Android Studio 所基于的 IntelliJ IDEA 已经发展了很多年。这种演变的一部分是随着每个软件版本而激增的许多定制特性。这些众多的可定制特性,加上数百个第三方插件,使得 IntelliJ,以及现在的 Android Studio,成为市场上最可定制、最灵活的 ide 之一。事实上,几乎所有你能想到的在 IDE 中定制的东西都很有可能在 Android Studio 中定制。Android Studio 中的可定制功能如此之多,以至于我们无法一一涵盖。在本书中,我们已经讨论了 Android Studio 的一些最重要的可定制功能,包括工具按钮和默认布局(第二章),以及实时模板、代码生成和代码样式(第三章)。

本章展示了那些我们认为对 Android 开发者最有用的可定制特性的平衡。为了利用 Android Studio 中的可定制功能,您应该熟悉设置键盘快捷命令(Ctrl+Alt+S | Cmd+逗号)。此操作也可从主菜单的“文件”“➤设置”中找到(如果您使用的是 Mac,请选择“文件”“➤偏好设置”)。键盘快捷键和菜单操作都会激活“设置”对话框。

在设置对话框中,你可以找到 Android Studio 中大多数可定制的特性,我们将在本章中向你展示如何导航它的许多标签和子面板。当我们讨论设置对话框的功能时,请参考图 16-1 。左窗格包含可定制功能的列表。该列表分为两部分:项目设置和 IDE 设置。您对前者中的项目所做的任何更改可能会应用于您当前的项目或所有项目,而您对后者所做的任何更改将应用于现在和将来的所有项目。“设置”对话框的左上角还包含一个过滤栏。当您在过滤栏中键入文本时,下面的列表将只显示与文本匹配的条目。

A978-1-4302-6602-0_16_Fig1_HTML.jpg

图 16-1。

The Settings dialog box showing Java ➤ Code Generation

代码风格

尽管第三章提到了代码风格,我们还是会在这里更详细地讨论这个重要的话题。通过按 Ctrl+Alt+S | Cmd+逗号激活设置对话框。在左窗格中切换打开代码样式目录,并选择 Java。然后在右窗格中选择代码生成选项卡。

如果您按照第三章中的步骤操作,您应该在字段和静态字段文本区域看到小写的 m 和 s。如果您没有看到这些字母,请现在将它们键入各自的字段中。假设您遵循 Android 中的命名约定,即命名您的类成员时以 m(代表成员—例如,mCurrencies)或 s(代表静态成员—例如,sName)为前缀,这些前缀允许 Android Studio 在您自动生成 getters、setters、constructors 和其他代码时生成有意义的方法名。我们强烈建议您遵循这个命名约定;因此,这个特殊的设置是您可以设置的最重要的设置之一。

选择位于代码生成页签左侧的排列页签,如图 16-3 所示。“排列”选项卡的作用是对源文件中的代码元素进行排序。Java 软件开发人员希望成员声明首先出现,然后是构造函数,然后是方法,最后是内部类,依此类推。“排列”选项卡允许您设置代码元素的顺序。作为一名软件开发人员,你应该维护一个整洁有序的代码库。然而,您不需要关心以正确的顺序插入代码元素,因为 Android Studio 会自动为您重新排列它们。若要应用排列设置,请键入 Ctrl+Alt+L | Cmd+Alt+L,并确保选中“重新排列条目”复选框。然后点击运行,如图 16-2 所示。生成的源文件元素将根据您在“排列”标签中选择的顺序重新排列。您也可以通过从主菜单中选择代码➤重排代码来执行相同的功能。

A978-1-4302-6602-0_16_Fig3_HTML.jpg

图 16-3。

Reformat Code dialog box with Rearrange Entries selected

A978-1-4302-6602-0_16_Fig2_HTML.jpg

图 16-2。

The Settings dialog box showing Java ➤ Arrangement

选择包装和支撑标签,如图 16-4 所示。此选项卡的目的是允许您设置代码包装的方式和时间。关于包装 Java 代码,没有一成不变的规则。我们更喜欢在代码块的第一个语句的末尾看到左花括号,而其他人更喜欢垂直对齐的左花括号和右花括号的对称性。如果你是喜欢对齐花括号的人,你可以在图 16-4 中突出显示的括号放置部分改变这个设置(以及许多其他设置),只需将设置从行尾改为下一行。当您更改设置时,请注意右边子面板上的示例代码是如何变化的,以反映这些设置。

A978-1-4302-6602-0_16_Fig4_HTML.jpg

图 16-4。

The Settings dialog box showing Java ➤ Wrapping and Braces

花点时间探索代码样式➤ Java 窗格的其他选项卡,并根据您自己的偏好定制代码样式。您不仅可以对 Java 应用代码样式更改,还可以对 HTML、Groovy 和 XML 的代码样式应用类似的更改。XML 设置对 Android 布局特别有用。对代码样式设置感到满意后,可以通过单击位于代码样式窗格顶部的管理按钮来保存它们。在出现的名为代码风格模式的对话框中,如图 16-5 所示,点击另存为按钮,给你的代码风格起一个名字,如 android1。如果将代码样式更改应用于默认方案,这些设置将成为所有项目的默认设置。如果您将代码样式的更改应用到项目方案中,那么只有当前项目会受到影响。如你所见,Android Studio 在配置代码风格时给了你很大的灵活性。

A978-1-4302-6602-0_16_Fig5_HTML.jpg

图 16-5。

The Code Style Schemes dialog box enables you to save your code styles

外观、颜色和字体

许多程序员喜欢反转 IDE 的颜色主题,使背景为深色。有证据表明反转(深色背景)的颜色主题可以减少眼睛疲劳,但是需要一段时间来适应反转的主题,特别是如果你已经在白色背景上写了一段时间的代码。Android Studio 附带了三个预打包的主题:IntelliJ、Darcula 和 Windows。IntelliJ 是默认的浅色背景主题;Darcula 是暗背景主题;而 Windows 是复古的 Windows 主题。你也可以从 ideacolorthemes.org 下载更多的主题。

通过调用 Ctrl+Alt+S | Cmd+逗号打开设置对话框。在过滤器栏中键入 appearance,并在列表中选择外观的第一个实例。将主题字段更改为 Darcula。然后点击应用和确定,如图 16-6 所示。Android Studio 将请求重启自己,您应该允许。一旦 Android Studio 重启,你的 IDE 应该类似于图 16-7 。

A978-1-4302-6602-0_16_Fig7_HTML.jpg

图 16-7。

The Android Studio IDE with the Darcula theme applied

A978-1-4302-6602-0_16_Fig6_HTML.jpg

图 16-6。

Settings dialog box with Appearance Theme set to Darcula

使用 Android Studio 自带的预打包主题是改变颜色主题最简单的方法,但是如果你倾向于进一步定制颜色和字体,你可以通过调整编辑器➤颜色和字体下的设置来实现,如图 16-8 所示。如果您想保存您的新配色方案,您可以通过单击另存为按钮并为您的配色方案命名来完成。

A978-1-4302-6602-0_16_Fig8_HTML.jpg

图 16-8。

Settings ➤ Colors and Fonts ➤ Java

键盘映射

如果您对任何键盘快捷键不确定,您可以通过导航到“帮助➤默认键盘映射参考”来参考快速参考。该命令在位于 JetBrains 服务器上的浏览器中打开一个 PDF 文件。

如果您想修改默认的键映射,您可以通过激活设置对话框(Ctrl+Alt+S | Cmd+逗号)然后在过滤栏中键入键映射来完成。按键映射条目按菜单组织,如图 16-9 所示。在开始自定义键映射之前,单击子面板顶部的复制按钮,将默认的所有键映射设置复制到一个新的键映射模式中,现在您可以随意命名该模式。双击任何条目都可以让您修改或添加任何您喜欢的键盘或鼠标快捷键,只要这些新命令不与任何现有命令冲突。如果您经常使用某些没有快捷键的命令,请为它们创建一个键盘或鼠标快捷键。

A978-1-4302-6602-0_16_Fig9_HTML.jpg

图 16-9。

From Settings ➤ Keymap, double-click entries to modify or create keyboard shortcuts

宏指令

当您考虑前面的“Keymap”一节中提供的现有命令时,您可能想知道是否有一种方法可以自定义命令本身,而不仅仅是激活该命令的键盘或鼠标快捷键。嗯,有——它叫做宏。宏让你有机会在 Android Studio 中记录任何事件或事件序列,并随意回放。

让我们创建一个发出 Monkey 命令的宏。通过单击 IDE 底部空白处的“终端工具”按钮,打开一个终端会话。导航到编辑➤宏➤开始宏记录。在终端会话中键入 ADB shell monkey–v 2000。沿着状态栏的右下角,您会看到一个绿色的消息框,上面写着“宏记录已开始”,如图 16-10 所示。单击左侧的红色停止按钮。在弹出的对话框中,输入 monkey,点击 OK,如图 16-11 所示。您现在可以通过导航到编辑➤宏➤猴子选择这个宏。您还可以按照上一节中的说明为该宏指定键盘或鼠标快捷键。

A978-1-4302-6602-0_16_Fig11_HTML.jpg

图 16-11。

Enter monkey as the macro name

A978-1-4302-6602-0_16_Fig10_HTML.jpg

图 16-10。

Macro recording status in the status bar

文件和代码模板

文件和代码模板对于创建经常使用的文件或代码块非常有用。每当您在 Android Studio 中创建新的活动时,都会使用一个文件模板来生成代码。在本节中,您将基于您在第九章中完成的代码创建您自己的名为 CurrencyLayout 的定制代码模板。

通过按 Ctrl+Alt+S | Cmd+逗号激活设置对话框。在过滤栏中键入文件和代码模板。单击模板选项卡,然后单击文件和代码模板子面板顶部的绿色加号箭头。将清单 9-1 (在第九章的中)中的代码复制并粘贴到右边的子面板中,或者如果你正在阅读这本书的印刷版,将清单 9-1 中的代码键入到右边的子面板中,如图 16-12 所示。在 Name 字段中将模板命名为 CurrencyLayout,并在 Extension 字段中键入 xml。Description 字段描述了如何在文件和代码模板中使用变量,尽管在这个简单的例子中我们不会使用任何变量。单击应用,然后单击确定。在项目工具窗口中,右击res/layout目录,选择【新➤当前布局】,如图 16-13 所示。在出现的对话框中,将文件命名为 activity_currency,然后单击 OK。Android Studio 将为您生成一个名为activity_currency.xml的新 XML 布局文件,其中包含您在文件模板定义中使用的代码。CurrencyLayout文件模板提供了一个很好的框架,从中我们可以创建全新的 XML 布局。您不局限于 XML 文件;您也可以轻松地为 Java 或 HTML 文件创建文件模板。

A978-1-4302-6602-0_16_Fig13_HTML.jpg

图 16-13。

Right-click the res/layout directory and choose New ➤ CurrencyLayout

A978-1-4302-6602-0_16_Fig12_HTML.jpg

图 16-12。

From Settings ➤ File and Code Templates, create the CurrencyLayout file template

菜单和工具栏

Android Studio 中很少有超出定制的内容。如果你不喜欢菜单和工具栏,你也可以修改它们。在本节中,您将向 Analyze 菜单添加 Monkey 命令。

通过按 Ctrl+Alt+S | Cmd+逗号激活设置对话框。在筛选器栏中键入菜单和工具栏,并在列表中选择菜单和工具栏项目。从主菜单中,选择分析➤分析操作以切换打开目录。选择分析模块依赖,点击添加后按钮,如图 16-14 所示。在出现的对话框中,导航到所有操作➤主菜单➤编辑➤宏➤猴子。如图 16-15 所示,点击确定退出该对话框,再次点击确定保存更改。要查看并激活您刚才所做的更改,请打开终端会话并导航至分析➤猴子。

A978-1-4302-6602-0_16_Fig15_HTML.jpg

图 16-15。

Select Monkey from the Macros options

A978-1-4302-6602-0_16_Fig14_HTML.jpg

图 16-14。

From Settings ➤ Menus and Toolbars, create the Monkey action item in the Analyze menu

外挂程式

许多第三方插件可用于 Android Studio。在本节中,您将安装 Bitbucket 插件,它是 Android Studio 数百个插件中的一个。要查看插件的完整列表,请将浏览器指向:plugins.jetbrains.com/?androidstudio

通过按 Ctrl+Alt+S | Cmd+逗号激活设置对话框。在过滤栏中键入插件,然后在下面的列表中选择插件项目。点击插件子面板底部的浏览存储库按钮,如图 16-16 所示。在弹出的对话框顶部的搜索栏中输入 bitbucket,如图 16-17 所示。点击安装按钮,Android Studio 将安装 Bitbucket 插件。点击关闭按钮,Android Studio 将请求重启,您应该允许。如果您从版本控制菜单浏览 VCS ➤结帐,以及 VCS ➤导入到版本控制菜单,您会注意到一些与 Bitbucket 相关的新菜单项。Bitbucket 插件有助于 Git 存储库的远程管理。参见第七章对 Git 的全面讨论。

A978-1-4302-6602-0_16_Fig17_HTML.jpg

图 16-17。

Search for the Bitbucket plug-in and install it

A978-1-4302-6602-0_16_Fig16_HTML.jpg

图 16-16。

From Settings ➤ Plugins, select Browse Repositories

摘要

本章讲述了设置对话框。您还回顾了代码风格,这在第三章中有介绍。您学习了如何保存新的代码样式方案,如何更改 Android Studio 的外观,以及调整现有的主题并保存该主题。您还学习了如何修改键映射并保存您自己的键映射模式。您创建了一个宏,然后将该宏插入到菜单栏中。您还使用了文件和代码模板,修改了菜单和工具栏,并最终学会了管理插件。

别忘了我们已经讨论过定制工具按钮(第二章)、默认布局(第二章)和动态模板(第三章)。特别是,动态模板是 Android Studio 中最重要的定制特性之一。在 Android Studio 中,比我们在本书中介绍的更多的定制是可能的,并且许多定制特性有助于自我发现。重新访问“设置”对话框(Ctrl+Alt+S | Cmd+逗号),并探索其中可用的许多自定义功能。

posted @ 2024-08-13 14:02  绝不原创的飞龙  阅读(527)  评论(0)    收藏  举报