安卓-UI-开发-全-

安卓 UI 开发(全)

原文:zh.annas-archive.org/md5/0C4D876AAF9D190F8124849256569042

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

2007 年 1 月 9 日,苹果公司正式发布了 iPhone,用户界面设计的世界发生了转变。尽管平板电脑已经存在了一段时间,但 iPhone 是第一个为如此多人提供便携式触摸屏的设备,人们喜爱它。仅仅过了一年多,谷歌和开放手持设备联盟宣布推出 Android,它在许多方面是 iPhone 的直接竞争对手。

我们为什么喜欢触摸屏手机?答案很简单——反馈。触摸屏提供了一种直接操作屏幕对象的方式,而过去这必须通过键盘、鼠标、操纵杆或其他输入设备来驱动。这种直接操作的触摸屏模式对我们作为开发人员思考用户界面的方式产生了重大影响,也改变了用户对应用程序的期望。触摸屏设备要求我们停止以表单为中心的思考方式,开始考虑面向对象的用户界面。

Android 被广泛用作快速扩展的消费电子产品的主要操作系统,包括:

  • 智能手机

  • 准系统

  • 平板电脑

  • 一些桌面系统

尽管这些设备有不同的目的和规格,但它们都运行 Android 系统。这与许多其他操作环境不同,后者几乎总是有特定的用途。它们向开发者提供的服务和 API 通常反映了它们的目标硬件。而另一方面,Android 假设一个应用程序可能需要在许多不同类型的设备上运行,这些设备的硬件能力和规格可能大不相同,并尽可能简单优雅地让开发者处理这些设备之间的差异。

新的挑战

随着 Android 及其所支持的触摸屏设备变得越来越普及,它们将为用户界面设计和开发带来一系列新的挑战:

  • 您通常没有鼠标

  • 您可能有多个指点设备

  • 您通常没有键盘

  • 任何存在的键盘可能是软件键盘

  • 软件键盘可能会占用应用程序的部分屏幕空间

软件键盘减少了应用程序可用的屏幕空间,同样地,如果存在硬件键盘,它可能不会始终暴露给用户。因此,不同的 Android 设备不仅各不相同,而且它们在应用程序运行时可能会看似改变功能。

指尖法则

大多数 Android 设备都有触摸屏(尽管这不是必需的)。对任何触摸屏用户界面施加的第一个限制是人类食指的大小,这当然会因人而异。如果屏幕上的小部件太小,用户试图触摸的内容将不清楚。你会注意到,大多数 Android 小部件占用了大量的空间,并且周围有比正常更多的填充。在触摸屏设备上,你不能依赖像素级的精确。你需要确保当用户触摸一个小部件时,他们能接触到,并且不会意外地触摸到另一个小部件。

神奇的触摸

触摸屏对用户界面设计的另一个影响是,应用程序及其使用的所有小部件必须完全易于理解(甚至比通常更多)。我们经常用鼠标悬停或工具提示来替代良好的用户界面规划和设计,以指示小部件的功能。在触摸屏设备上,没有鼠标或指针设备。它与用户的第一次交互是当用户触摸它时,他们会期待发生一些事情。

提示

一个敏感的话题

大多数 Android 设备都有触摸屏,但这并非必需。不同设备间触摸屏的质量也大相径庭。触摸屏的种类及其功能也会因设备而异,这取决于设备的预期用途,以及常常是它的目标市场细分。

对世界的较小看法

大多数 Android 设备体积小巧,因此屏幕较小,通常像素比普通 PC 或笔记本电脑要少。这种尺寸的限制限制了小部件的大小。小部件必须足够大,以便安全触摸,但我们也需要在屏幕上尽可能多地展示信息。所以,不要给你的用户他们不需要的信息,同时避免索要你不需要的信息。

经典的用户界面原则

这里有一些每个用户界面都应该遵循的核心准则。这些准则将使你的用户感到满意,并确保你的应用程序成功。在本书的其余部分,我们将通过实际例子来讲解这些准则,展示用户界面可以做出的改进。

一致性

这是良好用户界面设计的基石。按钮应该看起来像按钮。确保每个屏幕的布局与应用程序中的每个其他屏幕都有关系。人们常将这一原则误认为是“遵循平台的外观和感觉”。外观和感觉很重要,但一致性主要适用于应用程序的布局和整体体验,而不是配色方案。

重用你的界面

维持一致用户界面的最简单方法,是尽可能多地复用界面元素。乍一看,这些建议看起来仅仅像是“好的面向对象”实践。然而,仔细观察将揭示出你未曾想到的复用图形小部件的方式。通过改变各种小部件的可见性,或者你可以复用编辑屏幕以查看预期类型的列表项。

简洁

这对于基于电话的应用程序尤为重要。通常,当用户遇到一个新应用程序时,是因为他们在寻找某样东西。他们可能没有时间(或者更经常是没有耐心)去学习一个新的用户界面。请确保你的应用程序尽可能少地请求信息,并以尽可能少的步骤引导用户获取他们想要的确切信息。

禅宗方法

通常,在使用移动设备时,你的时间有限。你也可能是在不太理想的环境中使用应用程序(比如,在火车上)。用户需要提供给应用程序的信息越少,需要从应用程序中吸收的信息越少,越好。剥离选项和信息也可以使学习曲线变短。

安卓的隐藏菜单

安卓一个非常实用的功能是隐藏的菜单结构。菜单仅在用户按下“菜单”按钮时可见,这通常意味着他们在寻找当前屏幕上没有的东西。通常,用户不应该需要打开菜单。然而,隐藏高级功能直到需要时,这是一种很好的方式。

反馈

反馈是触摸屏设备令人兴奋的原因。当你拖动一个对象时,它会跟随你的手指在屏幕上移动,直到你松开它。当用户将手指放在你的应用程序上时,他们期待某种反应。然而,你不希望挡他们的路——当他们的手指触摸一个按钮时,不要显示错误消息,而是禁用该按钮直到可以使用,或者根本不显示它。

位置与导航

当你来到一个以前从未去过的地方,很容易迷失方向或迷路。软件也是一样。仅仅因为应用程序对你这个开发者来说有意义,并不意味着对你的用户来说逻辑上说得通。添加过渡动画、面包屑和进度条可以帮助用户识别他们在应用程序中的位置以及正在发生的事情。

恢复之路

在桌面应用程序或网络上告诉用户出现问题的常见方式是打开错误对话框。在移动设备上,人们希望应用程序的使用更加流畅。在普通应用程序中,你可能会通知用户他们选择了一个无效选项,但在移动应用程序中,你通常希望确保他们根本无法选择该选项。同时,不要让他们浏览大量选项列表。相反,允许他们使用自动完成功能或类似方式过滤列表。

当出现问题时,要友好且有帮助——不要告诉用户,“我找不到任何符合你搜索的航班”。而应该告诉他们,“对于你的搜索,没有可用的航班,但如果你准备提前一天出发,以下是一份可用的航班列表”。一定要确保你的用户可以向前迈出一步,而不必返回(尽管返回的选项应该始终存在)。

安卓的方式

安卓平台在很多方面与为网页开发应用程序相似。有许多设备,由许多制造商制造,具有不同的能力和规格。然而,作为开发者,你将希望你的用户拥有尽可能一致的体验。与网页浏览器不同,Android 内置了应对这些差异的机制,甚至可以利用它们。

我们将从用户的角度来看待 Android,而不是纯粹以开发为中心的方法。我们将涵盖如下主题:

  • Android 提供了哪些用户界面元素

  • 安卓应用程序是如何组装的

  • 安卓的不同布局类型

  • 向用户展示各种类型的数据

  • 对现有安卓小部件进行定制

  • 保持用户界面美观的技巧和工具

  • 应用程序之间的集成

我们即将深入探索为安卓设备构建用户界面——所有安卓设备,从最高速的 CPU 到最小的屏幕。

这本书涵盖的内容

第一章,开发一个简单的活动介绍了构建 Android 应用程序的基础知识,从简单的用户界面开始。它还涵盖了在将你的设计实现为代码时,你可以使用的各种选项。

第二章,带适配器的视图展示了如何利用基于适配器的控件,这是 Android 对模型-视图-控制器(MVC)结构的回应。了解这些控件,以及它们最能为你服务的场景。

第三章,专门的安卓视图仔细查看了一些 Android 平台提供的更专业的控件,以及它们与普通控件的关系。这一章涵盖了诸如画廊和评分栏之类的控件,以及它们是如何被使用和定制的。

第四章,活动和意图更多地讨论了 Android 是如何运行你的应用程序的,以及从这个角度出发,如何最好地编写它的用户界面。这一章讲述了如何确保你的应用程序能够以用户期望的方式运行,而你所需付出的努力最小。

第五章,非线性布局探讨了 Android 提供的一些高级布局技术。它讲述了在考虑 Android 设备屏幕差异的同时,向用户呈现不同屏幕的最佳方式。

第六章,输入与验证提供了关于从用户那里接收输入的技巧,以及如何尽可能让这个过程轻松。本章探讨了 Android 提供的不同输入小部件,以及如何根据情况最佳地配置它们。同时,当其他方法都失败时,本章还讨论了如何最好地告知用户他们的操作是错误的。

第七章,动画化小部件和布局将告诉读者在哪里、何时、为何以及如何为你的 Android 用户界面添加动画。它还揭示了 Android 默认提供哪些类型的动画,如何将它们组合在一起,以及如何构建自己的动画。本章探讨了移动用户界面中动画的重要性,并展示了 Android 如何简化复杂动画的制作。

第八章,以内容为中心的设计详细介绍了如何设计屏幕布局,以便在屏幕上向用户展示信息。本章探讨了 Android 提供的一些不同显示技术的优缺点。

第九章,样式设计 Android 应用程序告诉我们如何保持整个应用程序的外观一致性,以使我们的应用程序更容易使用。

第十章,构建应用程序主题查看了设计过程,以及如何应用全局主题来使你的应用程序脱颖而出。

你需要为这本书准备什么

请查看 Android 开发者网站上提到的“系统要求”,链接为:developer.android.com/sdk/requirements.html

本书的代码在 Ubuntu Linux 10.04 和 Mac OS X 上进行了测试。

本书的目标读者

本书面向至少有一定 Java 经验,想要在 Android 平台上构建应用程序的开发者。对于那些已经在 Android 平台上开发应用程序,并希望获得关于用户界面设计额外知识的人来说,这本书也将非常有用。它还是 Android 平台提供的众多小部件和资源结构的宝贵参考资料。

这本书还对这些读者有帮助:

  • 学习 Android 开发的 Java 开发者

  • 想要拓宽技能范围的 MIDP 开发者

  • 想要将应用程序移植到 iPhone 的开发者

  • 想要拓宽用户基础的创业型 Android 开发者

约定

在这本书中,你会发现有几个标题经常出现。

为了清楚地说明如何完成一个过程或任务,我们使用:

行动时间——标题

  1. 在编辑器或 IDE 中打开res/layout/main.xml布局资源。

  2. 移除LinearLayout元素中的默认内容。

指令通常需要一些额外的解释,以便它们有意义,因此我们会接着提供:

刚才发生了什么?

这个标题解释了你刚刚完成的任务或指令的工作原理。

你还会在书中发现一些其他的学习辅助工具,包括:

小测验——标题

这些是简短的选择题,旨在帮助你测试自己的理解。

尝试英雄——标题

这些设置实际挑战,并为你提供实验所学内容的想法。

你还会发现一些区分不同类型信息的文本样式。以下是一些这些样式的例子,以及它们的含义解释。

文本中的代码字会如下所示:"我们将从创建一个选择器Activity和一个简单的NewsFeedActivity开始"。

一段代码会以下面的形式设置:

<activity
    android:name=".AskQuestionActivity"
    android:label="Ask Question">

    <intent-filter>
        <action android:name="questions.askQuestion"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

当我们希望引起你对代码块中某个特定部分的注意时,相关的行或项目会以粗体设置:

<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout
         android:layout_width="fill_parent"
         android:layout_height="fill_parent">

 <ViewStub android:id="@+id/review"
 android:inflatedId="@+id/inflated_review"
 android:layout="@layout/review"/>

 <ViewStub android:id="@+id/photos"
 android:inflatedId="@+id/inflated_photos"
 android:layout="@layout/photos"/>

 <ViewStub android:id="@+id/reservations"
 android:inflatedId="@+id/inflated_reservations"
 android:layout="@layout/reservations"/>
</FrameLayout>

任何命令行输入或输出都会以下面的形式编写:

android create project -n AnimationExamples -p Anima
tionExamples -k com.packtpub.animations -a AnimationSelector -t 3

新术语重要词汇会以粗体显示。你在屏幕上看到的词,比如菜单或对话框中的,会在文本中以这样的形式出现:"通常,如果用户选择购买音乐按钮而没有突然被带到网页浏览器,他们会更有信任感"。

注意

警告或重要注意事项会以这样的框出现。

提示

提示和技巧会以这样的形式出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢或可能不喜欢的地方。读者的反馈对我们开发能让你们充分利用的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果有一本书你需要的,并且希望看到我们出版,请通过www.packtpub.com上的建议一个标题表格给我们发送信息,或者发送电子邮件至<suggest@packtpub.com>

如果你在一个主题上有专业知识,并且有兴趣撰写或为书籍做贡献,请查看我们在www.packtpub.com/authors的作者指南。

客户支持

既然你现在拥有了 Packt 的一本书,我们有一些事情可以帮助你最大限度地利用你的购买。

提示

下载本书的示例代码

你可以从你在www.PacktPub.com的账户下载你所购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.PacktPub.com/support注册,我们会直接将文件通过电子邮件发送给你。

勘误

尽管我们已经竭尽全力确保内容的准确性,但错误仍然会发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——我们非常感激你能向我们报告。这样做,你可以让其他读者免受挫折,并帮助我们在后续版本中改进这本书。如果你发现任何勘误,请通过访问www.packtpub.com/support报告,选择你的书,点击勘误提交表单链接,并输入你的勘误详情。一旦你的勘误被验证,你的提交将被接受,勘误将在我们网站的相应标题下的勘误部分上传或添加到现有的勘误列表中。任何现有的勘误都可以通过在www.packtpub.com/support选择你的标题来查看。

盗版

网络上对版权材料的盗版行为是所有媒体持续面临的问题。在 Packt,我们非常重视对我们版权和许可的保护。如果你在互联网上以任何形式遇到我们作品的非法副本,请立即提供其位置地址或网站名称,以便我们可以寻求补救措施。

如果你怀疑有盗版材料,请通过<copyright@packtpub.com>联系我们,并提供相关链接。

我们感谢你帮助保护我们的作者,以及我们为你提供有价值内容的能力。

问题

如果你在这本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章:开发一个简单的 Activity

在 Android 的世界里,Activity是您与用户接触的点。这是一个您向用户捕捉和展示信息的屏幕。您可以通过使用以下方式构建您的Activity屏幕:XML 布局文件或硬编码的 Java。

为了开始我们的 Android 用户界面之旅,我们需要一个用户界面作为起点。在本章中,我们将从一个简单的Activity开始。我们将:

  • 创建一个新的 Android 项目

  • 在应用程序资源文件中构建Activity布局

  • 将资源文件与Activity类关联

  • 动态填充Activity一系列的多项选择题

开发我们的第一个示例

对于我们的第一个示例,我们将编写一个多项选择题和答案Activity。我们可以将其用于诸如“谁想成为百万富翁?”或“你是什么类型的猴子?”等应用程序。这个示例将通过提问来回答一个非常关键的问题:“我应该吃什么?”当用户回答问题时,这个应用程序将筛选出食物想法的数据库。用户可以在任何时候退出流程以查看建议的餐点列表,或者等到应用程序没有问题可问为止。

由于这是一个用户界面示例,我们将跳过构建筛选器和食谱数据库。我们只向用户询问与食物偏好相关的问题。对于每个问题,我们有一系列预设答案供用户选择(即多项选择题)。他们给出的每个答案都会让我们缩小合适的食谱列表。

创建项目结构

在我们开始编写代码之前,我们需要一个项目结构。一个 Android 项目远不止其 Java 代码——还有清单文件、资源、图标等等。为了简化事情,我们使用默认的 Android 工具集和项目结构。

您可以从developer.android.com为您的常用操作系统下载最新版本的 Android SDK。一个单一的 Android SDK 可以用来开发针对任何数量的目标 Android 版本。您需要遵循网站上的安装说明,在developer.android.com/sdk/installing.html安装最新的 SDK“入门包”和一个或多个平台目标。本书中的大多数示例将在 Android 1.5 及更高版本上运行。Android 网站还维护了一个非常有用的图表,您可以在上面看到最受欢迎的 Android 版本。

动手操作——设置 Android SDK

在为您的操作系统下载了 Android SDK 归档文件之后,您需要安装它,然后至少下载一个 Android 平台包。打开命令行或控制台,完成以下步骤:

  1. 解压 Android SDK 归档文件。

  2. 更改目录到未打包的 Android SDK 的根目录。

  3. 更改目录到 Android SDK 的 tools 目录。

  4. 通过运行以下命令更新 SDK:

    android update sdk
    
  5. 通过进入虚拟设备屏幕并点击新建按钮来创建一个新的虚拟设备。将新的虚拟设备命名为default

  6. 将其目标指定为 SDK 下载的最新版本的 Android。将 SD 卡的大小设置为4096 MiB。点击创建 AVD按钮。

刚才发生了什么?

上述命令告诉新的 Android SDK 安装程序查找可用的软件包并安装它们。这包括安装一个平台软件包。你安装的每个平台软件包都可以用来创建一个Android 虚拟设备AVD)。你创建的每个 AVD 都像购买了一个新的设备,可以在其上进行测试,每个设备都有自己的配置和数据。这些是虚拟机,当你要测试时,Android 模拟器将在上面运行你的软件。

开始一个新项目的行动时间——

Android SDK 提供了一个便捷的命令行工具,名为 android,可用于生成新项目的基本框架。你可以在你的 Android SDK 的 tools 目录下找到它。它能够创建基本的目录结构和一个 build.xml 文件(用于 Apache Ant),帮助你开始 Android 应用程序开发。你需要确保 tools 目录在你的可执行路径中,以便这个工具能够正常工作。打开命令行或控制台。

  1. 在你的主目录或桌面上创建一个名为 AndroidUIExamples 的新目录。你应该使用这个目录来存储本书中的每个示例。

  2. 更改目录到新的 AndroidUIExamples

  3. 运行以下命令:

    android create project -n KitchenDroid -p KitchenDroid -k com.packtpub.kitchendroid -a QuestionActivity -t 3
    

刚才发生了什么

我们刚刚创建了一个框架项目。在前面的命令行中,我们使用了以下选项来指定新项目的结构:

选项 描述
-n 给项目一个名字,在我们的例子中是 KitchenDroid。这实际上只是项目的内部标识符。
-p 指定项目的基目录。在这种情况下,使用与项目相同的名称。android工具将为你创建这个目录。
-k 指定应用程序的根 Java 包。这是一个相当重要的概念,因为它定义了我们在 Android 客户端设备上的唯一命名空间。
-a 为工具提供一个“主” Activity 类的名称。这个类将被填充一个基本的布局 XML,并作为构建你的应用程序的基础点。框架项目将预先配置为在启动时加载这个 Activity

如果你运行命令 android list targets,并且它提供了一个可能的空目标列表,那么你没有下载任何 Android 平台软件包。你通常可以单独运行 android 工具,并使用其图形界面下载和安装 Android 平台软件包。前面的示例使用 API 级别 3,对应于 Android 平台版本 1.5。

检查 Android 项目布局

一个典型的 Android 项目几乎拥有与企业级 Java 项目一样多的目录和文件。Android 既是一个框架,也是一个操作系统环境。在某种程度上,你可以将 Android 视为为在手机和其他有限设备上运行而设计的应用容器。

作为新项目结构的一部分,你将拥有以下重要文件和目录:

文件夹名称 描述
bin 编译器将把你的二进制文件放在这个目录中。
gen 由各种 Android 工具生成的源代码。
res 应用资源放在这里,将与你的应用一起编译和打包。
src 默认的 Java 源代码目录,build脚本将在这里查找要编译的源代码。
AndroidManifest.xml 你的应用描述符,类似于web.xml文件。

提示

资源类型和文件

大多数应用资源类型(位于res目录中)会受到 Android 应用打包器的特殊处理。这意味着这些文件占用的空间比它们通常情况下要少(因为 XML 会被编译成二进制格式,而不是保持纯文本形式)。你可以通过各种方式访问资源,但始终要通过 Android API(它会为你将这些资源解码成它们的原始形式)。

res的每个子目录表示不同的文件格式。因此,你不能直接将文件放入根res目录中,因为打包工具不知道如何处理它(你将得到一个编译错误)。如果你需要以原始状态访问一个文件,请将其放在res/raw目录中。raw目录中的文件会以字节为单位复制到你的应用程序包中。

动手操作时间——运行示例项目

android 工具为我们提供了一个最小的 Android 项目示例,基本上是一个“Hello World”应用。

  1. 在你的控制台或命令行中,切换到KitchenDroid目录。

  2. 要构建并签名项目,请运行:

    ant debug
    
  3. 你需要启动之前创建的default AVD:

    emulator -avd default
    
  4. 现在在模拟器中安装你的应用:

    ant install
    
  5. 在模拟器中,打开Android菜单,你应在菜单中看到一个名为QuestionActivity的图标。点击这个图标。动手操作时间——运行示例项目

刚才发生了什么?

Android 模拟器是一个完整的硬件模拟器,包括 ARM CPU,承载整个 Android 操作系统栈。这意味着在模拟器下运行软件将完全和在裸机硬件上运行一样(尽管速度可能会有所不同)。

当你使用 Ant 部署你的 Android 应用时,需要使用install Ant 目标。install Ant 目标会寻找正在运行的模拟器,然后将应用归档文件安装到它的虚拟内存中。需要注意的是,Ant 不会为你启动模拟器。相反,它会发出错误,并且构建会失败。

提示

应用签名

每个 Android 应用程序包都是数字签名的。签名用于将你标识为应用程序的开发者,并建立应用程序的权限。它还用于建立应用程序之间的权限。

通常你会使用自签名证书,因为 Android 并不要求你使用证书授权机构。然而,所有应用程序必须进行签名,以便它们能够被 Android 系统运行。

屏幕布局

虽然 Android 允许你通过 Java 代码或通过在 XML 文件中声明布局来创建屏幕布局,但我们将在 XML 文件中声明屏幕布局。这是一个重要的决定,原因有几个。首先,使用 Java 代码中的 Android 小部件需要为每个小部件编写多行代码(声明/构造行,调用 setter 的几行,最后将小部件添加到其父级),而在 XML 中声明的小部件只占用一个 XML 标签。

将布局保持为 XML 的第二个原因是,当它存储在 APK 文件中时,会被压缩成特殊的 Android XML 格式。因此,你的应用程序在设备上占用的空间更少,下载时间更短,由于需要加载的字节码更少,其内存大小也更小。XML 在编译期间还会由 Android 资源打包工具进行验证,因此具有与 Java 代码相同类型的安全性。

将布局保持为 XML 的第三个原因是,它们需要经过与其他所有外部资源相同的选择过程。这意味着布局可以根据任何定义的属性进行变化,例如语言、屏幕方向和大小,甚至是一天中的时间。这意味着你可以在未来简单通过添加新的 XML 文件,来添加对同一布局的新变体,而无需更改任何 Java 代码。

布局 XML 文件

为了让 Android 打包工具能够找到它们,所有的 XML 布局文件必须放在你的 Android 项目的/res/layout目录下。每个 XML 文件将生成一个同名的资源变量。例如,如果我们将文件命名为/res/layout/main.xml,那么我们可以在 Java 中通过R.layout.main访问它。

由于我们将屏幕布局构建为一个资源文件,它将由应用程序资源加载器加载(在资源编译器编译后)。资源需要经过选择过程,因此尽管应用程序只加载一个资源,但在应用程序包中可能有多个相同资源的可用版本。这个选择过程也是 Android 国际化的基础。

如果我们想为几种不同类型的触摸屏构建用户界面布局的不同版本,Android 为我们定义了三种不同的触摸屏属性:notouchstylusfinger。这大致相当于:没有触摸屏、电阻式触摸屏和电容式触摸屏。如果我们想为没有触摸屏的设备定义一个更依赖键盘的用户界面(notouch),我们可以编写一个新的布局 XML 文件,命名为/res/layout-notouch/main.xml。当我们在Activity代码中加载资源时,资源选择器会在我们运行的设备没有触摸屏时选择notouch版本的屏幕。

资源选择限定符

这里是一组常用的限定符(属性名),当 Android 选择要加载的资源文件时会考虑这些限定符。这个表格是按优先级排序的,最重要的属性在顶部。

名称 描述 示例 API 级别
MCC 和 MNC 移动国家代码(MCC)和移动网络代码(MNC)。这些可以用来确定设备中的 SIM 卡绑定的是哪个移动运营商和国家。移动网络代码可选地跟随移动国家代码,但单独使用是不被允许的(你必须首先指定国家代码)。 mcc505``mcc505-mnc03``mcc238``mcc238-mnc02``mcc238-mnc20 1
语言和地区代码 语言和地区代码可能是最常使用的资源属性。通常,这是你根据用户语言偏好本地化应用程序的方式。这些值是标准的 ISO 语言和地区代码,并且不区分大小写。你不能没有国家代码指定一个地区(类似于java.util.Locale)。 en``en-rUS``es``es-rCL``es-rMX 1

| 屏幕尺寸 | 这个属性只有三种变化:小、中、大。这个值基于可使用的屏幕空间量:

  • 小型:QVGA(320×240 像素)低密度类型的屏幕;

  • 中型:WQVGA 低密度,HVGA(480x360 像素)中密度,以及 WVGA 高密度类型的屏幕;

  • 大型:VGA(640x480 像素)或 WVGA 中密度类型的屏幕

small``medium``large 4
屏幕宽高比 这是基于设备“正常”使用方式的屏幕宽高比类型。这个值不会因为设备的方向改变而改变。
屏幕方向 用于确定设备当前是处于竖屏(port)还是横屏(land)模式。这只有在能检测到方向的设备上可用。
夜间模式 这个值简单地根据一天中的时间改变。

| 屏幕密度(DPI) | 设备屏幕的 DPI。这个属性有四个可能的值:

  • ldpi:低密度,大约 120dpi;

  • mdpi:中密度,大约 160dpi;

  • hdpi:高密度,大约 240dpi;

  • nodpi: 可用于不应该根据屏幕密度进行缩放的bitmap资源。

ldpi``mdpi``hdpi``nodpi 4
键盘状态 设备上可用的键盘类型是什么?这个属性不应该用来确定设备是否有硬件键盘,而应该用来确定键盘(或软件键盘)当前是否对用户可见。

动手操作时间——设置问题活动

为了开始,我们将使用 Android 最简单的布局,称为:LinearLayout。与 Java AWT 或 Swing 不同,Android 布局管理器被定义为特定的容器类型。因此,LinearLayout就像一个带有内置LayoutManagerPanel。如果您使用过 GWT,您会对这个概念非常熟悉。我们将以简单的从上到下结构(LinearLayout非常适合)来布局屏幕。

  1. 在您喜欢的 IDE 或文本编辑器中打开项目/res/layout目录下名为main.xml的文件。

  2. 删除任何模板 XML 代码。

  3. 将以下 XML 代码复制到文件中:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">
    
    </LinearLayout>
    

刚才发生了什么?

我们刚刚移除了“Hello World”示例,并放入了一个完全空的布局结构,这将成为我们构建剩余用户界面的平台。如您所见,Android 为其资源有一个特殊的 XML 命名空间。

注意

Android 中的所有资源类型都使用相同的 XML 命名空间。

我们将根元素声明为LinearLayout。这个元素直接对应于类android.widget.LinearLayout。每个带有 Android 命名空间前缀的元素或属性都对应于由 Android 资源编译器解释的属性。

AAPT(Android 资源打包工具)将生成一个R.java文件到您的根(或主要)包中。这个文件包含了用于引用各种应用资源的 Java 变量。在我们的例子中,我们有/res/layout目录中的main.xml包。这个文件变成了一个R.layout.main变量,并分配一个常数作为其标识。

填充ViewViewGroup

在 Android 中,一个控件被称为View,而一个容器(如LinearLayout)是ViewGroup。现在我们有一个空的ViewGroup,但我们需要开始填充它以构建我们的用户界面。虽然可以将ViewGroup嵌套在另一个ViewGroup对象中,但Activity只有一个根View——因此布局 XML 文件只能有一个根View

动手操作时间——提出问题

为了向用户提问,你需要将TextView添加到布局的顶部。TextView有点像LabelJLabel。它也是许多其他显示文本的 Android View小部件的基础类。我们希望它占用所有可用的水平空间,但只需足够的垂直空间让我们的问题适应。我们用请稍等...作为其默认文本填充TextView。稍后,我们将用动态选择的问题替换它。

  1. 回到你的main.xml文件。

  2. <LinearLayout...></LinearLayout>之间创建一个<TextView />元素,使用空元素/>语法结束,因为代表View对象的元素不允许有子元素。

  3. TextView元素设置一个 ID 属性:

    android:id="@+id/question"
    
  4. 将布局的宽度和高度属性分别更改为fill_parentwrap_content(与LinearLayout元素相同):

    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    
  5. TextView设置一些占位文本,以便我们可以在屏幕上看到它:

    android:text="Please wait..."
    
  6. 从项目根目录使用 Apache Ant 重新安装应用程序:

    ant install
    
  7. 再次在模拟器中运行应用程序,它应该看起来像以下截图:

动手时间——提出问题

TextView的代码最终看起来应该像这样:

<TextView android:id="@+id/question"
          android:text="Please wait..."
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"/>

刚才发生了什么

在前面的示例中,我们将fill_parentwrap_content用作布局宽度和高度属性的值。fill_parent的值是一个特殊值,始终等于父视图的大小。如果它用作android:layout_width属性的值(如我们的示例所示),那么它就是父视图的宽度。如果它在android:layout_height属性中使用,那么它将等于父视图的高度。

wrap_content的值在 Java AWT 或 Swing 中类似于首选大小。它告诉View对象,“占用你所需要的空间,但不要更多”。这些特殊属性值唯一有效的使用地方是android:layout_widthandroid:layout_height属性中。其他任何地方使用都会导致编译错误。

我们稍后需要在 Java 代码中访问这个TextView,以便调用其setText方法(该方法直接对应于我们用于占位文本的android:text属性)。通过为资源分配 ID,创建了对资源变量的 Java 引用。在这个例子中,ID 在这里声明为@+id/question。AAPT 将为id类型的每个资源生成一个int值作为标识符,作为你的R类的一部分。ID 属性还用于从另一个资源文件访问资源。

动手时间——添加答案的空间

向用户提问当然很好,但我们还需要给他们提供回答问题的方法。我们有几种选择:可以使用带有RadioButtonRadioGroup来表示每个可能的答案,或者使用带有每个答案项的ListView。然而,为了最小化所需的交互,并尽可能清晰,我们为每个可能的答案使用一个Button。但这稍微有些复杂,因为你在布局 XML 文件中不能声明可变数量的Button对象。相反,我们将声明一个新的LinearLayout,并在 Java 代码中使用Button对象填充它。

  1. 在我们提出问题的TextView下方,你需要添加一个<LinearLayout />元素。虽然这个元素通常会有子元素,但在我们的案例中,可能答案的数量是变化的,所以我们将其留为一个空元素。

  2. 默认情况下,LinearLayout会将它的子View对象水平排列。然而,我们希望每个子View垂直排列,因此你需要设置LinearLayoutorientation属性:

    android:orientation="vertical"
    
  3. 我们稍后需要在 Java 代码中填充新的ViewGroupLinearLayout),所以给它一个 ID:answers

    android:id="@+id/answers"
    
  4. 与我们的TextView和根LinearLayout一样,将宽度设置为fill_parent

    android:layout_width="fill_parent"
    
  5. 将高度设置为wrap_content,使其不会占用比所有按钮更多的空间:

    android:layout_height="wrap_content"
    

最终代码应如下所示:

<LinearLayout android:id="@+id/answers"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>

刚才发生了什么?

你可能已经注意到,对于这个例子,我们新的LinearLayout中没有内容。这可能看起来有些不寻常,但在这个案例中,我们希望用可变数量的按钮填充它——针对多项选择题的每个可能答案一个。然而,对于示例的下一部分,我们需要在这个LinearLayout中添加一些简单的内容Button小部件,以便我们可以看到整个屏幕布局的效果。在你的布局资源文件中使用以下代码,向LinearLayout添加Yes!No!Maybe? Button小部件:

<LinearLayout android:id="@+id/answers"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content">

    <Button android:id="@+id/yes"
            android:text="Yes!"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />

    <Button android:id="@+id/no"
            android:text="No!"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />

    <Button android:id="@+id/maybe"
            android:text="Maybe?"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
</LinearLayout>

在 Android XML 布局资源中,任何从ViewGroup类扩展的View类都被视为容器。向它们添加小部件就像将那些View元素嵌套在ViewGroup的元素内(而不是用没有子 XML 元素的闭合它)一样简单。

以下是前述Yes!No!Maybe?选项的屏幕截图:

发生了什么?

动手时间——添加更多按钮

我们还需要向屏幕布局添加两个额外的按钮。一个将允许用户跳过当前问题;另一个将允许他们查看到目前为止我们已过滤的简短餐单列表(基于他们已经回答的问题)。

  1. 首先,在我们答案ViewGroup <LinearLayout />下方(但仍在根LinearLayout元素内)创建一个空的<Button />元素。给它分配 ID skip,这样我们就可以在 Java 中引用它:

    android:id="@+id/skip"
    
  2. 使用边距为答案和新按钮之间创建一些填充:

    android:layout_marginTop="12sp"
    
  3. 给它显示标签 跳过问题

    android:text="Skip Question"
    
  4. 与所有之前的控件一样,宽度应为fill_parent,高度应为wrap_content

    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    
  5. 现在,在跳过问题按钮下方创建另一个空的 <Button /> 元素。

  6. 新按钮的 ID 应为 view

    android:id="@+id/view"
    
  7. 我们希望这个按钮显示文本:Feed Me!

    android:text="Feed Me!"
    
  8. 再次,在跳过问题按钮和新Feed Me!按钮之间放置一点空间:

    android:layout_marginTop="12sp"
    
  9. 最后,将Feed Me!按钮的宽度和高度设置为与我们迄今为止创建的其他元素一样:

    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    
    

完成这两个按钮后,你的布局 XML 文件现在应该以以下内容结束:

    <Button android:id="@+id/skip"
            android:text="Skip Question"
            android:layout_marginTop="12sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>

    <Button android:id="@+id/view"
            android:text="Feed Me!"
            android:layout_marginTop="12sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
</LinearLayout>

刚才发生了什么

将不相关的用户界面对象分开是用户界面设计的一个非常重要的部分。可以通过空白、边框或盒子将项目组分开。在我们的案例中,我们选择使用空白,因为空间也有助于让用户界面感觉更清洁。

我们通过在每个按钮上方使用边距来创建空白空间。边距和填充的工作方式与 CSS 中的(应该)完全一样。边距是控件外的空间,而填充是控件内的空间。在 Android 中,边距是ViewGroup的关注点,因此其属性名称以layout_为前缀。由于填充是View对象的责任,因此填充属性没有这样的前缀:

<Button android:id="@+id/view"
        android:text="Feed Me!"
        android:padding="25sp"
        android:layout_marginTop="12sp"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>

之前的代码会在Button的边缘和中间文本之间创建额外的空间,同时保留按钮上方的边距。

前一个示例中的所有测量单位均为sp,它是“与比例无关的像素”的缩写。与 CSS 类似,你可以在你指定的尺寸单位后缀上测量数字。Android 识别以下测量单位:

单位后缀 全名 描述和用途
px 像素 设备屏幕上的一个精确像素。这个单位在编写桌面应用程序时最常见,但随着手机屏幕尺寸的多样化,它变得较难使用。
in 英寸 一英寸(或最接近的近似值)。这是基于屏幕的物理尺寸。如果你需要与实际世界尺寸一起工作,这很棒,但由于设备屏幕尺寸的变异,它并不总是非常有用。
mm 毫米 另一个实际尺寸的测量,尽可能近似。这仅是英寸的公制版本:1 英寸等于 25.4 毫米。
pt 点的大小为 1/72 英寸。与毫米和英寸类似,它们对于与实际尺寸相对的大小调整非常有用。它们也常用于调整字体大小,因此相对于字体大小来说非常好用。
dpdip 密度独立像素 单个 DP 在 160 dpi 的屏幕上与单个像素大小相同。这个大小并不总是成比例的,也不总是精确的,但它是当前屏幕的最佳近似值。
sp 比例独立像素 dp单位类似,它是根据用户选择的字体大小缩放的像素。这可能是最佳的单位,因为它是基于用户选择的参数。用户可能因为觉得屏幕难以阅读而增加了字体大小。使用sp单位可以确保你的用户界面随之缩放。

定义通用尺寸

安卓还允许你定义自己的尺寸值作为资源常量(注意:是尺寸,不是测量)。当你想要多个view组件大小相同,或者定义一个通用的字体大小时,这会很有用。包含尺寸声明的文件放在项目的/res/values目录中。实际的文件名并不重要,常见的名称是dimens.xml。从技术上讲,尺寸可以与其他值类型(即字符串)一起包含,但这并不推荐,因为它使得在运行时追踪应用的尺寸变得更加困难。

将尺寸放在它们自己的文件中,而不是内联声明的一个优点是,你可以根据屏幕大小对它们进行本地化。这使得与屏幕分辨率相关的刻度(如像素)更加有用。例如,你可以将一个dimens.xml文件放入/res/values-320x240目录中,并带有不同的值,再将同一尺寸的另一个版本放入/res/values-640x480目录中。

尺寸资源文件是一个简单的值文件(类似于strings.xml),但是尺寸是通过<dimen>标签定义的:

<resources>
    <dimen name="half_width">160px</dimen>
</resources>

要在布局 XML 文件中作为大小访问,你可以使用资源引用(这与访问资源字符串的方式类似):

<TextView layout_width="@dimen/half_width" />

构建一个通用尺寸列表在构建复杂布局时会很有帮助,这些布局将在许多不同的屏幕上看起来都很好,因为它避免了需要构建几个不同的布局 XML 文件。

尝试改进样式的大侠——提升样式

现在我们有了这个用户界面最基本的结构,但它看起来并不太好看。除了答案按钮之间的边距,以及跳过问题给我提示!按钮之外,你几乎无法区分它们。我们需要让用户知道这些按钮各司其职。同时,我们也需要让问题更加突出,尤其是如果他们没有太多时间在屏幕上眯着眼看的时候。你可能需要安卓的文档,可以在网上找到,地址是developer.android.com/reference/

我们在屏幕顶部有一个问题,但正如你在之前的屏幕截图中看到的,它并不突出。因此,对于用户来说,他们需要做什么并不是非常清晰(尤其是第一次使用该应用程序时)。

尝试对屏幕顶部的题目TextView进行以下样式更改。这只需要你为其 XML 元素添加一些属性:

  1. 文本居中。

  2. 使文本加粗。

  3. 将文本大小改为24sp

  4. 在问题和答案按钮之间添加12sp的间距

喂我!按钮也非常重要。这是让用户访问应用程序根据他们的答案过滤出的建议食谱列表的按钮,所以它应该看起来不错。

以下样式应该有助于喂我!按钮很好地突出(提示:Button继承自TextView):

  1. 将文本大小设置为18sp

  2. 将文本颜色改为好看的红色#9d1111

  3. 将文本样式设置为加粗。

  4. 添加文本阴影:x=0y=-3radius=1.5color=white("#fff")。

当你完成屏幕样式的调整后,它应该看起来像以下截图:

尝试改进样式

布局 XML 格式的限制

布局 XML 格式最明显的限制之一是,你不能基于外部变量动态填充Activity的一部分——XML 文件中没有循环或方法。

在我们的示例中,这种限制以空LinearLayout的形式出现。因为每个问题都有任意数量的可能答案,我们需要在组内变动数量的按钮。对于我们的目的,我们将在 Java 代码中创建Button对象并将它们放入LinearLayout

XML 布局格式另一个失败的地方是动态引用外部资源。这可以在我们的示例中看到,我们在TextView元素上放置了占位符文本——questionandroid:text属性。我们可以使用以下语法引用外部字符串:

<TextView android:id="@+id/question"
          android:text="@string/question"
          android:gravity="center"
          android:textStyle="bold"
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"/>

这将有效地引用strings.xml文件中的静态变量。它不适合动态选择的问题,每次初始化Activity时都会改变。

突击测验

  1. 你有什么理由用 XML 而不是纯 Java 代码来编写你的布局?

    1. 安卓可以从外部读取布局文件以进行优化。

    2. 布局成为资源选择过程的一部分。

    3. 你的用户可以从 App Store 下载新的布局。

    4. 布局可以应用自定义主题。

  2. 我们如何使下一题按钮的文本加粗?

    1. 使用android:typeface属性。

    2. 创建一个自定义的Button实现。

    3. 添加一个 CSS 属性:style="font-weight: bold"

    4. 使用android:textStyle属性。

  3. 如果我们将LinearLayoutvertical方向改为horizontal方向,会发生什么?

    1. 布局会侧翻。

    2. 所有小部件在屏幕上会被挤压在一起。

    3. 只有问题的TextView会显示在屏幕上。

    4. 根据可用的像素数量,问题以及可能的其他View对象可能会显示在屏幕上。

    5. 布局将溢出,导致小部件紧挨着出现在多行上。

填充 QuestionActivity

我们有一个基本用户界面,但现在它是静态的。我们可能想要向用户提出许多不同的问题,每个问题都有不同的答案。我们还可能以某种方式改变我们提出的问题。简而言之,我们需要一些 Java 代码来填充布局,加入一个问题及一些可能的答案。我们的问题由两部分组成:

  • 问题

  • 可能答案的列表

在此示例中,我们将利用字符串数组资源来存储所有问题和答案数据。我们将使用一个字符串数组来列出问题标识符,然后为每个问题及其答案使用一个字符串数组。这种方法的优势与使用布局 XML 文件而不是硬编码的优势非常相似。你的项目的res/values目录中将有一个自动生成的strings.xml文件。这个文件包含了你希望应用程序使用的字符串和字符串数组资源。以下是我们strings.xml文件的开始部分,其中包含两个要问用户的问题:

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

<resources>
    <string name="app_name">Kitchen Droid</string>

    <string-array name="questions">
        <item>vegetarian</item>
        <item>size</item>
    </string-array>

    <string-array name="vegetarian">
        <item>Are you a Vegetarian?</item>
        <item>Yes</item>
        <item>No</item>
        <item>I\'m a vegan</item>
    </string-array>

    <string-array name="size">
        <item>How much do you feel like eating?</item>
        <item>A large meal</item>
        <item>Just a nice single serving of food</item>
        <item>Some finger foods</item>
        <item>Just a snack</item>
    </string-array>
</resources>

每个问题数组(vegetariansize)的第一个条目是问题本身,而随后的每个条目都是一个答案。

行动时间——编写更多的 Java 代码

  1. 打开编辑器或 IDE 中的QuestionActivity.java文件。

  2. 在包声明下方导入 Android 的Resources类:

    import android.content.res.Resources;
    
  3. 为了从你的strings.xml文件开始提问,你需要一个方法来查找questions <string-array>并找到包含当前问题的数组名称。这通常不是你在应用程序资源中需要做的事情——它们的标识符通常通过R类为你所知。但在此情况下,我们想要按照questions <string-array>中定义的顺序进行操作,这使得事情变得有些复杂:

    private int getQuestionID(Resources res, int index) {
    
  4. 现在我们可以查看questions字符串数组,它包含了每个问题的标识名称(我们的索引字符串数组):

    String[] questions = res.getStringArray(R.array.questions);
    
  5. 我们有一个问题数组,需要找到标识符值。这类似于对vegetarian问题使用R.array.vegetarian,只不过这是一个动态查找,因此比正常情况要慢得多。通常情况下,以下这行代码是不推荐的,但对我们来说非常有用:

    return res.getIdentifier(
            questions[index],
            "array",
            "com.packtpub.kitchendroid");
    
  6. QuestionActivity类将向用户展示几个问题。我们希望应用程序能够与手机及其环境"友好相处"。因此,每个问题都将在QuestionActivity的新实例中提出(允许设备控制我们Activity的显示)。然而,这种方法引发了一个重要问题:我们如何知道要向用户提出的问题的索引?答案是:我们的IntentActivity是通过一个Intent对象启动的,每个Intent对象可能携带任何数量的"额外"信息(类似于HttpServletRequest接口中的请求属性),供Activity使用,有点像main方法的参数。所以,Intent也像一个HashMap,包含供Activity使用的特殊数据。在我们的例子中,我们使用了一个名为KitchenDroid.Question的整型属性:

    private int getQuestionIndex() {
        return getIntent().getIntExtra("KitchenDroid.Question", 0);
    }
    

这两种方法构成了我们填充问题屏幕和按定义好的问题列表进行导航的基础。完成时,它们应该看起来像这样:

private static int getQuestionID(
        final Resources res,
        final int index) {

    final String[] questions = res.getStringArray(R.array.questions);

    return res.getIdentifier(
            questions[index],
            "array",
            "com.packtpub.kitchendroid");
}

private int getQuestionIndex() {
    return getIntent().getIntExtra("KitchenDroid.Question", 0);
}

刚才发生了什么

getQuestionID方法非常直接。在我们的代码中,我们使用R.array.questions来访问<string-array>,它标识了我们将要向用户提出的所有问题。每个问题都有一个String形式的名称,以及一个int形式的对应资源识别号。

getQuestionID方法中,我们使用了Resources.getIdentifier方法,该方法用于查找给定资源名称的资源标识符(整数值)。该方法的第二个参数是要查找的资源类型。这个参数通常是生成的R类的内部类。最后,我们传递了资源所在的基包。除了这三个参数,你也可以通过完整的资源名称来查找资源:

return res.getIdentifier(
        "com.packtpub.kitchendroid:array/" + questions[index],
        null,
        null);

getQuestionIndex方法告诉我们当前在questions <string-array>中的位置,从而确定要向用户提出哪个问题。这是基于触发ActivityIntent中的"额外"信息。getIntent()方法为你提供了访问触发你ActivityIntent的途径。每个Intent可以有任何数量的"额外"数据,这些数据可以是任何"原始"或"可序列化"的类型。这里我们从Intent中获取了KitchenDroid.Question额外的整数值,如果没有设置则替换为 0(即默认值)。如果用户点击菜单中的图标,Android 没有指定该值,那么我们从第一个问题开始。

动态创建小部件

到目前为止,我们只使用了布局 XML 文件来填充我们的屏幕。在某些情况下,这还不够。在这个简单的例子中,我们希望用户有一个按钮列表,他们可以点击来回答提出的问题。我们可以预先创建一些按钮并将它们命名为button1button2等,但这意味着限制了可能的答案数量。

为了从我们的 <string-array> 资源中创建按钮,我们需要在 Java 中进行操作。我们之前创建了一个 ViewGroup(以我们命名为 answersLinearLayout 的形式)。这就是我们将添加动态创建的按钮的地方。

是时候采取行动了——将问题显示在屏幕上。

你的应用程序现在知道去哪里找问题来询问,也知道应该询问哪个问题。现在它需要将问题显示在屏幕上,并允许用户选择答案。

  1. 在编辑器或 IDE 中打开 main.xml 文件。

  2. 从布局资源中移除 Yes!No!Maybe? Button 元素。

  3. 在编辑器或 IDE 中打开 QuestionActivity.java 文件。

  4. 我们需要一个新的类字段来保存动态创建的 Button 对象(作为引用):

    private Button[] buttons;
    
  5. 为了保持整洁,创建一个新的 private 方法来将问题显示在屏幕上:initQuestionScreen

    private void initQuestionScreen() {
    
  6. 在这个方法中,我们假设布局 XML 文件已经加载到 Activity 屏幕中(即,在 onCreatesetContentView 之后将被调用)。这意味着我们可以将布局的部分作为 Java 对象来查找。我们需要 TextView 名为 questionLinearLayout 名为 answers 的这两个对象:

    TextView question = (TextView)findViewById(R.id.question);
    ViewGroup answers = (ViewGroup)findViewById(R.id.answers);
    
  7. 这两个变量需要用问题和其可能的答案来填充。为此,我们需要 <string-array>(来自我们的 strings.xml 文件),其中包含这些数据,因此我们需要知道当前问题的资源标识符。然后我们可以获取实际的数据数组:

    int questionID = getQuestionID(resources, getQuestionIndex());
    String[] quesionData = resources.getStringArray(questionID);
    
  8. question 字符串数组的第一个元素是向用户提出的问题。接下来的 setText 调用与在布局 XML 文件中指定 android:text 属性完全相同:

    question.setText(quesionData[0]);
    
  9. 然后我们需要创建一个空数组来保存对我们 Button 对象的引用:

    int answerCount = quesionData.length – 1;
    buttons = new Button[answerCount];
    
  10. 现在我们准备填充屏幕了。根据我们的数组,对每个答案值进行 for 循环:

    for(int i = 0; i < answerCount; i++) {
    
  11. 从数组中获取每个答案,跳过索引为零的问题字符串:

    String answer = quesionData[i + 1];
    
  12. 为答案创建一个 Button 对象并设置其标签:

    Button button = new Button(this);
    button.setText(answer);
    
  13. 最后,我们将新的 Button 添加到我们的 answers 对象(ViewGroup)中,并在我们的 buttons 数组中引用它(我们稍后会需要它):

    answers.addView(button);
    buttons[i] = button;
    
  14. 做完这些之后,在 onCreate 中的 setContentView 调用之后,我们需要调用我们新的 initQuestionScreen 方法。

刚才发生了什么?

findViewById 方法遍历 View 对象的树,寻找特定的标识整数值。默认情况下,任何在资源文件中使用 android:id 属性声明的资源都将有一个关联的 ID。你也可以通过使用 View.setId 方法手动分配一个 ID。

与许多其他用户界面 API 不同,Android 用户界面 API 更倾向于 XML 开发而非纯 Java 开发。这一点的完美例证是View子类有三个不同的构造函数,其中两个是为与 XML 解析 API 配合使用而设计的。我们无法在构造函数中填充Button标签(像大多数其他 UI API 那样),而是被迫先构造对象,然后使用setText来定义其标签。

你传给每个View对象构造函数的是Context对象。在前面示例中,你将Activity对象作为this传递给答案Button对象的构造函数中。Activity类从Context类继承。Context对象被ViewViewGroup对象用来加载它们为了正确运行所需的应用程序资源和服务。

现在你可以尝试运行应用程序,在这种情况下,你会看到以下屏幕。你可能已经注意到这个截图中还有额外的样式。如果你没有这个,你可能需要回溯到之前的尝试一下英雄部分。

刚才发生了什么?

在 Android 中处理事件

Android 用户界面事件的工作方式与 Swing 事件监听器或 GWT 事件处理程序非常相似。根据你想接收的事件类型,实现一个接口并将一个实例传递给你希望从中接收事件的小部件。在我们的例子中,我们有Button小部件,当用户触摸时会触发点击事件。

事件监听接口在许多 Android 类中声明,因此没有一个单独的地方可以查找它们。而且,与大多数事件监听系统不同,许多小部件可能只有一个给定类型的事件监听器。你可以通过类名前缀为On来识别事件监听接口(类似于 HTML 事件属性)。为了监听小部件上的点击事件,你会使用View.setOnClickListener方法来设置其OnClickListener

下面的代码片段展示了如何向Button对象添加一个点击监听器来显示一个ToastToast是一个小型的弹出框,会短暂显示以向用户提供一些信息:

button.setOnClickListener(new View.OnClickListener() {
    public void onClick(View clicked) {
        Toast.makeText(this, "Button Clicked!", Toast.LENGTH_SHORT).
             show();
    }
});

前面的事件监听器被声明为一个匿名内部类,当你需要将类似的事件监听器传递给许多不同的组件时,这样做是可以的。然而,大多数情况下,你会在 XML 布局资源中声明的组件上监听事件。在这些情况下,最好让你的Activity类实现所需的接口,或者为不同的事件驱动操作创建专门的类。尽管 Android 设备非常强大,但与台式电脑或笔记本电脑相比,它们仍然有限制。因此,你应该避免创建不必要的对象,以节省内存。通过将尽可能多的事件监听器方法放在已经创建的对象中,你可以降低所需的资源开销。

小测验

  1. 当你在布局 XML 文件中声明一个对象时,你如何获取其 Java 对象?

    1. 对象将在R类中声明。

    2. 使用Activity.findViewById方法。

    3. 使用Resources.getLayout方法。

    4. 对象将被注入到Activity类中的一个字段中。

  2. 在 Android 应用程序中监听事件的“最佳”方式是什么?

    1. 将监听器声明为匿名内部类。

    2. 为每个Activity创建一个单独的事件监听器类。

    3. Activity类中实现事件监听接口。

  3. 为什么你要将this Activity传递给View对象(例如new Button(this))的构造函数中?

    1. 它定义了Activity屏幕,它们将在上面显示。

    2. 这是事件消息将被发送到的位置。

    3. 这是View将引用其操作环境的方式。

总结

Android 提供了一些出色的工具来创建和测试应用程序,即使你没有 Android 设备在身边。话虽如此,实际触摸你的应用程序是无法替代的。这是 Android 平台如此吸引人的部分原因,它的感觉和响应方式(而模拟器并不能传达这一点)。

Android 开发者工具库中最重要工具之一是资源选择系统。通过它,你可以构建高度动态的应用程序,这些程序能够响应设备的变化,从而响应用户环境的变化。根据设备的方向改变屏幕布局,或者当用户滑出手机的 QWERTY 键盘时,让他们知道你在构建应用程序时考虑了他们的偏好。

在 Android 中构建用户界面时,强烈建议至少在 XML 文件中构建布局结构。XML 布局文件不仅被视为应用程序资源,而且 Android 也强烈倾向于通过编写 XML 用户界面而不是 Java 代码来构建。然而,有时布局 XML 文件是不够的,你需要用 Java 构建用户界面的一部分。在这种情况下,最好至少定义一个 XML 的布局框架(如果可能的话),然后使用标记 ID 和容器将动态创建的View对象放入布局中(类似于在 JavaScript 中动态添加到 HTML 文档)。

在构建用户界面时,要仔细考虑最终的外观和感觉。在我们的示例中,我们使用Button对象作为问题的答案。我们本可以使用RadioButton对象,但那样用户就需要选择一个选项,然后触摸下一题按钮,需要两次触摸。我们也本可以使用List(它与需要动态填充的事实很好地交互),然而,List并不像Button那样向用户清楚地表示一个“动作”。

在编写布局代码时,要小心使用测量单位。强烈建议在大多数情况下使用sp——除非你可以使用特殊的fill_parentwrap_content值。其他值很大程度上取决于屏幕大小,并且不会响应用户偏好。你可以利用资源选择过程为小、中、大屏幕构建不同的屏幕设计。你也可以定义自己的测量单位,并基于屏幕大小进行设置。

时刻考虑你的用户将如何与应用程序互动,以及他们可能会花费多少时间(或很少的时间)在其中。保持每个屏幕简洁且响应迅速可以使你的用户感到满意。

既然我们已经学会了如何创建一个基本的 Android 项目和一个简单的Activiy,我们可以专注于 Android 用户界面设计的更微妙的问题和解决方案。在下一章中,我们将重点关注数据驱动小部件的工作。Android 有几个专门设计用于显示和选择更复杂数据结构的小部件。这些小部件构成了数据驱动应用程序(如地址簿或日历应用程序)的基础。

第二章:视图的数据展示

在第一章中,我们介绍了如何创建一个项目,以及如何构建一个简单的用户界面。我们为第一个Activity编写了足够的代码,以动态生成用户可以用来回答我们的多项选择题的按钮。

现在我们已经可以捕获一些数据了,但如何显示数据呢?软件的一大优势是它能快速并以易于阅读的格式呈现和筛选大量数据。在本章中,我们将介绍一系列专门用于展示数据的安卓控件。

大多数以数据为中心的安卓类都是建立在Adapter对象之上的,因此扩展了AdapterView。可以将Adapter视为 Swing Model 类和渲染器(或呈现器)之间的交叉。Adapter对象用于为软件需要向用户显示的数据对象创建View对象。这种模式允许软件维护并处理数据模型,并且只在需要时为每个数据对象创建图形View。这不仅有助于节省内存,而且从开发角度来看也更合逻辑。作为开发者,您处理自己的数据对象,而不是试图将数据保存在图形小部件中(这些小部件通常不是最健壮的结构)。

您最常遇到的AdapterView类有:ListViewSpinnerGridView。在本章中,我们将介绍ListView类和GridView,并探讨它们的各种使用方式和样式设置。

列表和选择数据

ListView类可能是显示数据列表的最常见方式。它由ListAdapter对象支持,后者负责保存数据并渲染数据对象在View中的显示。ListView内置了滚动功能,因此无需将其包裹在ScrollView中。

ListView 选择模式

ListView类支持三种基本的项选择模式,由其常量定义:CHOICE_MODE_NONECHOICE_MODE_SINGLECHOICE_MODE_MULTIPLE。可以通过在布局 XML 文件中使用android:choiceMode属性,或者在 Java 中使用ListView.setChoiceMode方法来设置ListView的选择模式。

注意

选择模式和项目

ListView的选择模式改变了ListView结构的行为方式,但不会改变其外观。ListView的外观主要由ListAdapter定义,后者为应该出现在ListView中的每个项目提供View对象。

无选择模式 - CHOICE_MODE_NONE

在桌面系统中,这种情况没有意义——一个不允许用户选择任何内容的列表?然而,这是 Android ListView 的默认模式。原因是当用户通过触摸导航时,这很有意义。ListView 的默认模式允许用户点击其中一个元素,并触发一个动作。这种行为的结果是,无需“下一步”按钮或类似的东西。因此,ListView 的默认模式是表现为一个菜单。以下截图显示了一个默认的 ListView 对象,它从一个默认的 ApiDemos 示例中的 String 数组 Java 对象中展示不同的字符串列表。

无选择模式 —— CHOICE_MODE_NONE

单选模式 —— CHOICE_MODE_SINGLE

在此模式下,ListView 更像是一个桌面 List 小部件。它有当前选择的概念,点击列表项仅仅会选中它,不会再有其他动作。这种行为对于配置或设置等操作很合适,用户希望应用程序记住他们当前的选择。单选列表在屏幕上有其他交互式小部件时也很有用。但是,要注意不要在单个 Activity 中放置太多信息。ListView 占据几乎整个屏幕是很常见的。

注意

单选模式:它不会直接改变你的列表项的外观。你的列表项的外观完全由 ListAdapter 对象定义。

然而,Android 确实在系统资源中提供了一系列合理的默认值。在 android 包中,你会发现一个 R 类。这是访问系统默认资源的编程方式。如果你想创建一个带有 <string-array> 颜色列表的单选 ListView,你可以使用以下代码:

list.setAdapter(new ArrayAdapter(
        this,
        android.R.layout.simple_list_item_single_choice,
        getResources().getStringArray(R.array.colors)));

在此情况下,我们使用了 android.widget 包中提供的 ArrayAdapter 类。在第二个参数中,我们引用了名为 simple_list_item_single_choice 的 Android 布局资源。这个资源被 Android 系统定义为在 CHOICE_MODE_SINGLE 模式下显示 ListView 项的默认方式。通常这是一个带有 RadioButton 的标签,对应 ListAdapter 中的每个对象。

单选模式 —— CHOICE_MODE_SINGLE

多选模式 —— CHOICE_MODE_MULTIPLE

在多选模式下,ListView 用普通的复选框替换单选模式中的单选按钮。这种设计结构在桌面和基于 Web 的系统中也经常使用。复选框容易被用户识别,也便于返回并关闭选项。如果你希望使用标准的 ListAdapter,Android 为你提供了 android.R.layout.simple_list_item_multiple_choice 资源作为有用的默认选项:每个 ListAdapter 中的对象都有一个带有 CheckBox 的标签。

多选模式 —— CHOICE_MODE_MULTIPLE

添加头部和底部控件

ListView中的头部和底部允许你在列表的顶部和底部放置额外的控件。默认情况下,头部和底部控件被视为列表中的项(就像它们来自你的ListAdapter一样)。这意味着你可以像选择List结构中的数据元素一样选择它们。一个简单的头部项示例可能是:

TextView header = new TextView(this);
header.setText("Header View");
list.addHeaderView(header);

通常你不想让ListView中的头部和底部成为列表项,而是一个标签或一组标签,标识ListView的各个部分,或提供其他信息。在这种情况下,你需要告诉ListView,你的头部或底部视图不是可选的列表项。这可以通过使用addHeaderViewaddFooterView的扩展实现来完成:

TextView footer = new TextView(this);
footer.setText("Footer View");
list.addFooterView(footer, null, false);

ListView类将头部和底部与列表结构紧密集成,因此你也可以提供一个Object,它将通过AdapterView.getItemAtPosition(index)方法返回。在我们之前的示例中,我们提供了null。每个头部项都会将后续视图的索引偏移一个(就像你向ListView添加新项一样)。第三个参数告诉ListView是否应将头部或底部视为可选择的列表项(在我们之前的示例中不应该)。

如果你习惯了桌面控件,那么 Android ListView上的头部和底部控件可能会让你有点惊讶。它们会随着列表中的其他项一起滚动,而不会固定在ListView对象的顶部和底部。

创建一个简单的 ListView

为了介绍ListView类,我们将开始一个新示例,该示例将通过本章的后续各个部分进行增强。我们将创建的第一个Activity将使用从<string-array>资源填充的简单ListView

动手操作——创建快餐菜单

为了继续我们的食物与饮食主题,让我们构建一个简单的应用程序,允许我们订购各种类型的快餐,并送到家!用户首先会选择他们想订购的餐厅,然后选择他们想吃的各种食物。

  1. 使用 Android 命令行工具创建一个新的android项目:

    android create project -n DeliveryDroid -p DeliveryDroid -k com.packtpub.deliverydroid -a SelectRestaurantActivity -t 3
    
  2. 使用你喜欢的编辑器或 IDE 打开/res/values/strings.xml文件。

  3. 创建一个字符串数组结构,列出用户可以订购的各种快餐餐厅:

    <string-array name="restaurants">
        <item>The Burger Place</item>
        <item>Mick's Pizza</item>
        <item>Four Buckets \'o Fruit</item>
        <item>Sam\'s Sushi</item>
    </string-array>
    
  4. 使用你喜欢的编辑器或 IDE 打开/res/layout/main.xml文件。

  5. 移除默认LinearLayout中的任何控件。

  6. 添加一个新的<ListView>元素。

  7. <ListView>元素的 ID 设置为restaurant

    <ListView android:id="@+id/restaurant"/>
    
  8. ListView的宽度和高度设置为fill_parent

    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    
  9. 由于我们有一个包含我们想要填充ListView内容的字符串数组资源,我们可以在布局 XML 文件中直接引用它:

    android:entries="@array/restaurants"
    
  10. 完成指定步骤后,你应该会得到一个看起来像下面的main.xml布局文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
     <ListView android:id="@+id/restaurant"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:entries="@array/restaurants"/>
    </LinearLayout>
    

刚才发生了什么

如果你将应用程序安装到模拟器中并运行它,你将看到一个屏幕,你可以从中选择在你字符串数组资源中指定的餐厅列表。请注意,ListView上的choiceMode设置为CHOICE_MODE_NONE,这使得它更像是一个直接菜单,用户选择餐厅后,可以立即跳转到该餐厅的菜单。

刚才发生了什么

在这个例子中,我们在布局 XML 文件中使用了android:entries属性,指定了一个引用字符串数组资源的引用,其中包含我们想要的列表项。通常,使用AdapterView需要你创建一个Adapter对象,为每个数据对象创建View对象。

使用android:entries属性允许你从布局资源中指定ListView的数据内容,而不是要求你编写与AdapterView相关的正常 Java 代码。然而,它有两个需要注意的缺点:

  • 由生成的ListAdapter创建的View对象将始终是系统指定的默认值,因此不容易进行主题设置。

  • 你不能定义将在ListView中表示的数据对象。由于字符串数组容易本地化,你的应用程序将依赖于项目索引位置来确定它们表示的内容。

你可能已经注意到截图顶部,标签Where should we order from?不是应用程序的默认设置。Activity的标签在AndroidManifest.xml文件中定义如下:

<activity
    android:name=".SelectRestaurantActivity"
    android:label="Where should we order from?">

设置标准 ListAdapter 的样式

标准的ListAdapter实现要求每个项目在TextView项中表示。默认的单选和多选项目是使用CheckedTextView构建的,尽管 Android 中有许多其他的TextView实现,但它确实限制了一些我们的选择。然而,标准的ListAdapter实现非常方便,并为最常见的列表需求提供了可靠的实现。

由于带有CHOICE_MODE_NONEListView与菜单非常相似,如果将项目改为Button对象而不是普通的TextView项,岂不是很好吗?从技术上讲,ListView可以包含任何扩展TextView的小部件。然而,有些实现比其他的更适合(例如,ToggleButtonView在用户触摸它时不会保持指定的文本值)。

定义标准尺寸

在这个例子中,我们将为应用程序创建各种菜单。为了保持一致的外观和感觉,我们应该定义一组标准尺寸,这些尺寸将用于我们的每个布局文件中。这样我们可以为不同类型的屏幕重新定义尺寸。对于用户来说,没有比只能看到部分项目更沮丧的了,因为它的尺寸比他们的屏幕还要大。

创建一个新的资源文件来包含尺寸。该文件应命名为res/values/dimens.xml。将以下代码复制到新的 XML 文件中:

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

<resources>
    <dimen name="item_outer_height">48sp</dimen>
    <dimen name="menu_item_height">52sp</dimen>
    <dimen name="item_inner_height">45sp</dimen>
    <dimen name="item_text_size">24sp</dimen>
    <dimen name="padding">15dp</dimen>
</resources>

我们为列表项声明了两个高度尺寸:item_outer_heightitem_inner_heightitem_outer_height将是列表项的高度,而item_inner_height是列表项中包含的任何View对象的高度。

文件末尾的padding尺寸用于定义两个视觉元素之间的标准空白量。这被定义为dp,因此它会根据屏幕的 DPI 保持不变(而不是根据用户的字体大小偏好进行缩放)。

提示

交互项目的大小调整

在这个样式设置中,你会注意到item_outer_heightmenu_item_height48sp52sp,这使得ListView中的项目相当大。Android 中列表视图项的标准大小是48sp。列表项的高度至关重要。如果你的用户手指较大,而你把项目设置得太小,他们将很难点击目标列表项。

这是一般针对安卓用户界面设计的“良好实践”。如果用户需要触摸操作,那么请把它设计得大一些。

行动时间——改善餐厅列表

我们之前整理的餐厅列表很棒,但它是一个菜单。为了进一步强调菜单,文本应该更加突出。为了使用标准ListAdapter实现来设置ListView的样式,你需要在你的 Java 代码中指定ListAdapter对象。

  1. res/layout目录中创建一个名为menu_item.xml的新文件。

  2. 将根 XML 元素创建为TextView

    <?xml version="1.0" encoding="UTF-8"?>
    <TextView />
    
  3. 导入 Android 资源 XML 命名空间:

  4. 通过设置TextView小部件的 gravity 属性来使文本居中:

    android:gravity="center|center_vertical"
    
  5. 我们将TextViewtextSize赋值为我们的标准item_text_size

    android:textSize="@dimen/item_text_size"
    
  6. TextView文本的默认颜色有点灰,我们希望它是白色:

    android:textColor="#ffffff"
    
  7. 我们希望TextView的宽度与包含它的ListView相同。因为这是我们的主菜单,所以它的高度是menu_item_height

    android:layout_width="fill_parent"
    android:layout_height="@dimen/menu_item_height"
    
  8. 现在我们有一个样式化的TextView资源,我们可以将它整合到我们的菜单中。打开SelectRestaurantActivity.java文件。

  9. onCreate方法中,使用setContentView之后,我们需要获取之前在main.xml中创建的ListView的引用:

    ListView restaurants = (ListView)findViewById(R.id.restaurant);
    
  10. 将餐厅的ListAdapter设置为一个包含我们在values.xml文件中创建的字符串数组的新ArrayAdapter

    restaurants.setAdapter(new ArrayAdapter<String>(
        this,
        R.layout.menu_item,
        getResources().getStringArray(R.array.restaurants)));
    

刚才发生了什么

我们首先创建了一个新的布局 XML 资源,其中包含我们想要用于餐厅ListView中每个列表项的样式化TextView。你编写的menu_item.xml文件应该包含以下代码:

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

<TextView 
              android:gravity="center|center_vertical"
              android:textSize="@dimen/item_text_size"
              android:textColor="#ffffff"
              android:layout_width="fill_parent"
              android:layout_height="@dimen/menu_item_height" />

与我们之前的布局资源不同,menu_item.xml不包含任何ViewGroup(如LinearLayout)。这是因为ArrayAdapter将尝试将menu_item.xml文件的根View转换为TextView。因此,如果我们以某种方式将TextView嵌套在ViewGroup中,我们将得到一个ClassCastException

我们还创建了一个ArrayAdapter实例,以引用我们之前创建的menu_item XML 资源以及餐厅字符串数组。这个操作消除了在main.xml布局 XML 资源中的ListView上使用android:entries属性。如果你愿意,可以删除该属性。现在,你在SelectRestaurantActivity中的onCreate方法应如下所示:

public void onCreate(final Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.main);

        final ListView restaurants = (ListView)
                findViewById(R.id.restaurant);

 restaurants.setAdapter(new ArrayAdapter<String>(
 this,
 R.layout.menu_item,
 getResources().getStringArray(R.array.restaurants)));
    }

尝试使用 Apache Ant 将应用程序重新安装到模拟器中,现在你将看到一个看起来更像菜单的屏幕:

刚才发生了什么

尝试英雄——开发一个多选题应用程序。

尝试回到我们在第一章中编写的多选题应用程序,Developing a Simple Activity。它使用LinearLayoutButton对象来显示问题的可能答案,但它也使用字符串数组作为答案。尝试修改应用程序以:

  • 使用ListView代替LinearLayout

  • 使用Button对象为ListView设置样式,就像我们使用TextView对象为餐厅菜单设置样式一样。

  • 确保在Button列表项之间有一定的间距,使它们不会过于紧密。

创建自定义适配器。

当我们想要点餐时,我们通常想要订购一个项目的多个数量。ListView的实现以及标准的ListAdapter实现允许我们选择一个Cheese Burger项目,但并不允许我们请求3 Cheese Burgers。为了显示用户可以以多个数量订购的不同食品的菜单,我们需要一个自定义的ListAdapter实现。

为“The Burger Place”创建菜单。

对于主菜单中的每家餐厅,我们将构建一个单独的Activity类。实际上,这不是一个好主意,但它允许我们研究组织和展示菜单数据的不同方式。我们的第一站是The Burger Place,我们向用户展示一个汉堡列表,让他们在屏幕上点击他们想要的汉堡。每次点击列表项时,他们都会再点一个汉堡。我们将在汉堡名称左侧以粗体显示他们正在订购的汉堡数量。对于他们没有订购的汉堡旁边,则不显示数字(这使得用户可以快速查看他们正在订购的内容)。

汉堡类。

为了显示菜单,我们需要一个简单的Burger数据对象。Burger类将保存要在菜单中显示的名称,以及用户正在订购的Burger数量。在项目的根包中创建一个Burger.java文件,并使用以下代码:

class Burger {
    final String name;
    int count = 0;

    public Burger(String name) {
        this.name = name;
    }
}

你会注意到,在前面的代码中没有 getter 和 setter 方法,而且namecount字段都声明为包保护的。在 Android 2.2 之前的版本中,与直接字段查找相比,方法会产生较大的开销。由于这个类将只是渲染过程的一小部分(我们将从中提取数据以显示),我们应该确保开销尽可能小。

动手时间——创建汉堡项布局

为了为汉堡店设计一个好看的菜单,首先要做的是设计菜单项。这与使用布局 XML 资源的餐厅列表样式设计非常相似。然而,由于这次我们将自己构建ListAdapter,因此不必使用单个TextView,而是可以构建更复杂的布局。

  1. res/layout目录中创建一个名为burger_item.xml的新 XML 文件。这个文件将用于ListView中的每个汉堡项。

  2. 将布局的根声明为horizontal LinearLayout(注意高度,这将是ListView中每个项目的高度):

    <LinearLayout 
    
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="@dimen/item_outer_height">
    
  3. 接下来,声明一个TextView,我们将用它作为订购汉堡数量的counter。我们稍后可以通过其 ID 访问这个TextView

    <TextView android:id="@+id/counter" />
    
  4. counter的文本大小与应用程序中所有其他列表项完全相同。然而,它应该是加粗的,这样就可以轻松识别和阅读:

    android:textSize="@dimen/item_text_size"
    android:textStyle="bold"
    
    
  5. 我们还希望counter是正方形的,因此将宽度和高度设置得完全相同:

    android:layout_width="@dimen/item_inner_height"
    android:layout_height="@dimen/item_inner_height"
    
  6. 我们还希望文本在counter内居中:

    android:gravity="center|center_vertical"
    
  7. 我们还需要一个文本空间来显示汉堡的名字:

    <TextView android:id="@+id/text" />
    
  8. 文本大小是标准的:

    android:textSize="@dimen/item_text_size"
    
  9. 我们希望countertext标签之间有一点空间:

    android:layout_marginLeft="@dimen/padding"
    
  10. 标签的宽度应填满ListView,但我们希望两个TextView对象的大小相同:

    android:layout_width="fill_parent"
    android:layout_height="@dimen/item_inner_height"
    
  11. 标签文本应垂直居中,以匹配counter的位置。然而,标签应该是左对齐的:

    android:gravity="left|center_vertical"
    

刚才发生了什么?

你刚刚构建了一个非常不错的LinearLayout ViewGroup,这将为我们从汉堡店销售的每个汉堡渲染。由于counter TextView与标签是分开的对象,因此可以独立地进行样式设计和管理。如果我们想独立地为它们应用额外的样式,这将使事情变得更加灵活。现在你的burger_item.xml文件应该如下所示:

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

<LinearLayout

    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="@dimen/item_outer_height">

 <TextView android:id="@+id/counter"
 android:textSize="@dimen/item_text_size"
 android:textStyle="bold"
 android:layout_width="@dimen/item_inner_height"
 android:layout_height="@dimen/item_inner_height"
 android:gravity="center|center_vertical" />

 <TextView android:id="@+id/text"
 android:textSize="@dimen/item_text_size"
 android:layout_marginLeft="@dimen/padding"
 android:layout_width="fill_parent"
 android:layout_height="@dimen/item_inner_height"
 android:gravity="left|center_vertical" />
</LinearLayout>

动手时间——展示汉堡对象

如果你的数据对象是字符串或者很容易表示为字符串,标准的ListAdapter类工作得很好。为了在屏幕上美观地显示我们的Burger对象,我们需要编写一个自定义的ListAdapter类。幸运的是,Android 为我们提供了一个名为BaseAdapter的很好的ListAdapter实现框架类。

  1. 创建一个名为BurgerAdapter的新类,并让它继承自android.widget.BaseAdapter类:

    class BurgerAdapter extends BaseAdapter {
    
  2. Adapter是表示层的一部分,但也是ListView的底层模型。在BurgerAdapter中,我们存储了一个Burger对象的数组,我们在构造函数中分配它:

    private final Burger[] burgers;
    BurgerAdapter(Burger... burgers) {
        this.burgers = burders;
    }
    
  3. 直接在Burger对象数组上实现Adapter.getCount()Adapter.getItem(int)方法:

    public int getCount() {
        return burgers.length;
    }
    
    public Object getItem(int index) {
        return burgers[index];
    }
    
  4. 还期望Adapter为各个项目提供标识符,我们将仅返回它们的索引:

    public long getItemId(int index) {
        return index;
    }
    
  5. Adapter被请求提供一个列表项的View时,它可能会接收到一个可复用的现有View对象。我们将实现一个简单的方法来处理这种情况,如果需要,将使用android.view包中的LayoutInflator类来填充我们之前编写的burger_item.xml文件:

    private ViewGroup getViewGroup(View reuse, ViewGroup parent) {
        if(reuse instanceof ViewGroup) {
            return (ViewGroup)reuse;
        }
        Context context = parent.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);
        ViewGroup item = (ViewGroup)inflater.inflate(
                R.layout.burger_item, null);
    return item;
    }
    
  6. BurgerAdapter中,对我们来说最重要的方法是getView方法。这是ListView请求我们提供一个View对象的地点,以表示它需要显示的每个列表项:

    public View getView(int index, View reuse, ViewGroup parent) {
    
  7. 为了获取给定项目的正确View,你首先需要使用getViewGroup方法以确保你有burger_item.xml ViewGroup来显示Burger项:

    ViewGroup item = getViewGroup(reuse, parent);
    TextView counter = (TextView)item.findViewById(R.id.counter);
    TextView label = (TextView)item.findViewById(R.id.text);
    
  8. 我们将使用请求的index位置上的Burger对象的数据来填充这两个TextView对象。如果当前的count为零,则需要从用户界面隐藏counter小部件:

    Burger burger = burgers[index];
    counter.setVisibility(
            burger.count == 0
            ? View.INVISIBLE
            : View.VISIBLE);
    counter.setText(Integer.toString(burger.count));
    label.setText(burger.name);
    return item;
    

刚才发生了什么?

我们刚刚编写了一个自定义的Adapter类,用于在ListView中向用户展示一系列Burger对象。当ListView调用Adapter.getView方法时,它会尝试传入之前调用Adapter.getView返回的View对象。将为ListView中的每个项目创建一个View对象。然而,当ListView显示的数据发生变化时,ListView将要求ListAdapter重用第一次生成的每个View对象。尽量遵循这一行为非常重要,因为它直接影响到应用程序的响应性。在我们之前的示例中,我们实现了getViewGroup方法,以便考虑到这一要求。

getViewGroup方法也用于加载我们编写的burger_item.xml文件。我们使用LayoutInflator对象来完成此操作,这正是Activity.setContentView(int)方法加载 XML 布局资源的方式。我们从parent ViewGroup获取的Context对象(通常是ListView)定义了我们将从哪里加载布局资源。如果用户没有选择“汉堡”,我们使用View.setVisibility方法隐藏计数器TextView。在 AWT 和 Swing 中,setVisible方法接受一个Boolean参数,而在 Android 中,setVisibility接受一个int值。这样做的原因是 Android 将可见性视为布局过程的一部分。在我们的例子中,我们希望counter消失,但仍然在布局中占据其空间,这将使text标签保持左对齐。如果我们希望计数器消失且不占用空间,我们可以使用:

counter.setVisibility(burger.count == 0
        ? View.GONE
        : View.VISIBLE);

ListView对象将自动处理选中项目的突出显示。这包括用户在项目上按住手指,以及他们使用轨迹板或方向键导航ListView时。当一个项目被突出显示时,其背景通常会根据标准的 UI 约定改变颜色。

然而,在ListView中使用某些直接捕获用户输入的小部件(例如,ButtonEditText)会导致ListView不再为该小部件显示选中高亮。实际上,这将阻止ListView完全注册OnItemClick事件。

提示

在 ListView 中自定义分隔符

如果重写ListAdapterisEnabled(int index)方法,你就可以策略性地禁用ListView中的指定项目。这种做法的一个常见用途是将某些项目设置为逻辑分隔符。例如,在按字母排序的列表中的部分分隔符,包含下一“部分”所有项目首字母。

创建 TheBurgerPlaceActivity 类

为了在屏幕上显示“汉堡”菜单,并允许用户订购项目,我们需要一个新的Activity类。我们需要知道用户何时触摸列表中的项目,为此我们将需要实现OnItemClickListener接口。当发生特定事件时(在本例中是用户在ListView中触摸特定项目),作为监听器注册的对象将调用与发生的事件相关的相应方法。Android 提供了一个简单的ListActivity类,为这种情况提供一些默认布局和实用方法。

动手实践——实现 TheBurgerPlaceActivity

为了使用 BurgerAdapter 类展示 Burger 对象的 ListView,我们将需要创建一个 The Burger PlaceActivity 实现。新的 Activity 还将负责监听 ListView 中项目的“触摸”或“点击”事件。当用户触摸其中一个项目时,我们需要更新模型和 ListView,以反映用户又订购了一个 Burger

  1. 在项目的根包中创建一个名为 TheBurgerPlaceActivity 的新类,并确保它继承自 ListActivity

    public class TheBurgerPlaceActivity extends ListActivity {
    
  2. 重写 Activity.onCreate 方法。

  3. 调用 super.onCreate 以允许正常的 Android 启动过程。

  4. 使用一些 Burger 对象创建 BurgerAdapter 的实例,并将其设置为 ListActivity 代码要使用的 ListAdapter

    setListAdapter(new BurgerAdapter(
            new Burger("Plain old Burger"),
            new Burger("Cheese Burger"),
            new Burger("Chicken Burger"),
            new Burger("Breakfast Burger"),
            new Burger("Hawaiian Burger"),
            new Burger("Fish Burger"),
            new Burger("Vegatarian Burger"),
            new Burger("Lamb Burger"),
            new Burger("Rare Tuna Steak Burger")));
    
  5. 最后,使用以下代码实现 onListItemClicked 方法:

    protected void onListItemClick(
            ListView parent,
            View item,
            int index,
            long id) {
    BurgerAdapter burgers = (BurgerAdapter)
                parent.getAdapter();
    Burger burger = (Burger)burgers.getItem(index);
        burger.count++;
        burgers.notifyDataSetInvalidated();
    }
    

刚才发生了什么?

这个 TheBurgerPlaceActivity 的实现有一个简单的硬编码 Burger 对象列表供用户显示,并创建了一个 BurgerAdapter 来将这些对象转换为之前创建的 burger_item View 对象。

当用户点击列表项时,我们在 onItemClick 方法中增加相关 Burger 对象的 count。然后我们调用 BurgerAdapter 上的 notifyDataSetInvalidated()。此方法将通知 ListView 底层数据已更改。当数据更改时,ListView 将重新调用 Adapter.getView 方法,针对 ListView 中的每个项目。

ListView 中的项目由实际上是静态的 View 对象表示。这意味着当数据模型更新时,适配器必须允许更新或重新创建该 View。一种常见的替代方法是获取表示你更新数据的 View,并直接更新它。

注册并启动 TheBurgerPlaceActivity

为了从我们的餐厅菜单启动新的 Activity 类,你需要在 AndroidManifest.xml 文件中注册它。首先,在编辑器或 IDE 中打开 AndroidManifest.xml 文件,并将以下 <activity> 代码复制到 <application>...</application> 块中:

<activity android:name=".TheBurgerPlaceActivity"
          android:label="The Burger Place\'s Menu">

    <intent-filter>
        <action android:name=
                "com.packtpub.deliverydroid.TheBurgerPlaceActivity"/>
    </intent-filter>
</activity>

为了启动 Activity,你将需要回到 SelectRestaurantActivity 并实现 OnItemClickListener 接口。在 restaurants ListView 上设置 Adapter 之后,将 SelectRestaurantActivity 设置为 restaurants ListViewOnItemClickListener。你可以在 onItemClick 方法中使用 Intent 对象启动 TheBurgerPlaceActivity。现在你的 SelectRestaurantActivity 类应该看起来像以下代码片段:

public class SelectRestaurantActivity extends Activity
 implements OnItemClickListener {

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.main);

        ListView restaurants = (ListView)
                findViewById(R.id.restaurant);

        restaurants.setAdapter(new ArrayAdapter<String>(
                this,
                R.layout.menu_item,
                getResources().getStringArray(R.array.restaurants)));

 restaurants.setOnItemClickListener(this);
    }

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

 switch(index) {
 case 0:
 startActivity(new Intent(
 this,
 TheBurgerPlaceActivity.class));
 break;
 }
 }
}

当你重新安装应用程序并在模拟器中启动它时,你将能够导航到 The Burger Place 并为汉堡包下订单。在 The Burger Place 菜单中按下硬件“返回”按钮将带你回到餐厅菜单。

注册并启动 TheBurgerPlaceActivity

小测验

  1. ListView 对象的选择模式设置为 CHOICE_MODE_SINGLE 将:

    1. 向每个项目添加一个RadioButton

    2. 不执行任何操作(这是默认行为)。

    3. 使ListView跟踪一个“选中”的项目。

  2. ListAdapter定义了ListView如何显示其项目。它将在何时被要求重用一个View来显示一个项目对象?

    1. 当数据模型无效或更改时。

    2. 在每个项目上,用于橡皮图章。

    3. ListView重新绘制自身时。

  3. ListView可以滚动时,头部和底部对象将被定位:

    1. 在滚动项目之上和之下。

    2. 水平并排显示,在滚动项目之上和之下。

    3. 与其他项目一起滚动。

使用ExpandableListView

ListView类非常适合显示中小量的数据,但有时它会向用户展示过多的信息。考虑一个电子邮件应用程序。如果你的用户是重度电子邮件用户,或者订阅了几个邮件列表,他们可能会在文件夹中有数百封电子邮件。即使他们可能不需要滚动超过前几封,看到滚动条缩小到只有几像素大小,对用户的心理影响并不好。

在桌面邮件客户端中,你经常会按时间将邮件列表分组:今天、昨天、本周、本月以及更早(或类似)。Android 提供了ExpandableListView以实现这种类型的分组。每个项目嵌套在一个组内,用户可以显示或隐藏组。这有点像树形视图,但始终只嵌套一个层级(你不能将项目显示在组外)。

提示

大量的ExpandableListView

有时即使是ExpandableListView也可能不足以将数据量保持在合理长度。在这些情况下,考虑为用户提供组中的前几个项目,并在最后添加一个特殊的查看更多项目。或者,对组使用ListView,对嵌套项目使用单独的Activity

创建ExpandableListAdapter实现

由于ExpandableList类包含两个详细级别,它不能与只处理单一级别的普通ListAdapter一起工作。相反,它包含了ExpandableListAdapter,后者使用两组方法:一组用于组级别,另一组用于项目级别。在实现自定义ExpandableListAdapter时,通常最简单的方法是让你的实现继承自BaseExpandableListAdapter,因为它提供了事件注册和触发的实现。

ExpandableListAdapter 会在每个组项的左侧放置一个箭头指针,以指示组是打开还是关闭(类似于下拉/组合框)。箭头是在由 ExpandableListAdapter 返回的组 View 对象上方渲染的。为了防止你的组标签被这个箭头部分遮挡,你需要为列表项 View 结构添加填充。列表项的默认填充可以通过主题参数 expandableListPreferredItemPaddingLeft 获取,你可以使用它:

android:paddingLeft=
    "?android:attr/expandableListPreferredItemPaddingLeft"

为了保持 ExpandableListView 的外观一致性,建议你为 ExpandableListView 的普通(子)项目添加相同数量的填充(以保持它们的文本与父组对齐),除非你在左侧放置一个项目,如图标或复选框。

尝试英雄 - 订购定制比萨

Mick's Pizza 示例中,我们将创建一个分类的比萨配料菜单。每个配料包括一个名称,以及它是否在比萨上('on' 或 'off'),或者需要'extra'(例如,额外芝士)。每个项目使用两个水平排列的 TextView 对象。右侧的 TextView 可以显示配料名称。当不包含配料时,左侧的 TextView 为空,包含配料时为 On,用户想要比通常更多配料时为 Extra

创建一个对象模型,包含 ToppingCatagory 对象,其中包含一个名称和 PizzaTopping 对象数组。你需要记录每个配料是否被点单以及数量。

你还需要实现一个 PizzaToppingAdapter 类,扩展 BaseExpandableListAdapter 类。为组标签使用默认的 Android simple_expandable_list_item_1 布局资源,为项目标签使用一个新的定制布局资源。

当用户点击一个比萨配料时,它的状态会在三个值之间变化:OffOn,和 Extra

注意

使用 ListView.getAdapter() 方法不会返回你的 ExpandableListAdapter 实现,而是一个包装器。要获取原始的 ExpandableListAdapter,你需要使用 getExpandableListAdapter() 方法。你还需要使用 ExpandableListView.OnChildClickListener 接口来接收点击事件。

当你的新 Activity 完成时,你应该有一个看起来像以下的屏幕:

尝试英雄 - 订购定制比萨

使用 GridView 类

GridView 是一个具有固定列数的 ListView,从左到右,从上到下排列。标准的(未定主题的)Android 应用程序菜单像 GridView 一样排列。GridView 类使用与 ListView 完全相同的 ListAdapter 格式。然而,由于其固定的列数,GridView 非常适合图标列表。

提示

有效使用 GridViews

ListView相比,GridView可以在单个屏幕上显示更多的信息,但代价是显示的文本信息较少。从可用性的角度来看,图标通常比文本更容易操作。由于它们的颜色,图标可以比文本更快地被识别。当您有可以使用图标表示的信息时,以这种方式显示它是一个好主意。但是,请记住,图标需要在单个屏幕内保持唯一性,最好是在整个应用程序内。

在下一个示例中,我们将使用GridView构建四桶水果菜单。GridView将为菜单上的每个项目提供一个图标,以及图标下方的项目名称。因此,完成后,它看起来将非常像标准的 Android 应用程序菜单。下一个示例将重点介绍ListAdapter的实现,因为它与我们为汉堡店构建的ListAdapter大致相同。

提示

触摸屏设备上的图标

在触摸屏设备上考虑图标非常重要。它们需要比平时更具自解释性,或者需要伴随一些文本。使用触摸屏很难提供像工具提示这样的上下文帮助。如果用户正在触摸对象,它通常会被他们的手指和/或手遮住,使得图标和工具提示不可见。

动手操作——创建水果图标的时间到了。

为了将各种类型的水果显示为图标,我们将需要创建一个布局 XML 文件。GridView中的每个图标都将作为此布局的一个实例表示,与ListView中表示列表项目的方式完全相同。我们为图标创建每个项目作为ImageView,并在其下方为标签创建一个TextView

  1. res/layout目录中创建一个名为fruit_item.xml的文件。

  2. 将图标的根元素声明为垂直的LinearLayout

    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  3. 创建将作为我们图标的ImageView元素:

    <ImageView android:id="@+id/icon"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    
  4. 接下来,创建将作为标签的TextView元素:

    <TextView android:id="@+id/text"
        android:textSize="@dimen/item_description_size"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center|center_vertical" />
    

刚才发生了什么?

fruit_item.xml文件是我们菜单图标的非常简单的布局,也可以用于许多其他类型的图标,表现为网格形式。ImageView对象默认会尝试将其内容缩放到其尺寸。在我们之前的示例中,根LinearLayout的宽度和高度定义为fill_parent。当在GridView中作为单个项目放置时,使用fill_parent作为大小将导致LinearLayout填充为该网格项目提供的空间(不是整个GridView)。

GridView中显示图标

我们需要一个对象模型和ListAdapter,以便在GridView中将水果显示给用户。此时,适配器相当直接。它是在一个项目类和为图标定义的布局 XML 之上构建的正常ListAdapter实现。

对于每种水果,我们需要一个同时保存水果名称和图标的对象。在根包中创建一个 FruitItem 类,并使用以下代码:

class FruitItem {
    final String name;
    final int image;

    FruitItem(String name, int image) {
        this.name = name;
        this.image = image;
    }
}

在前面的代码中,我们将水果的图标图像作为一个整数引用。在 Android 中引用应用程序资源和 ID 时,总是使用整数。在这个例子中,我们假设所有不同类型的水果都有一个作为应用程序资源的图标。另一个选项是每个 FruitItem 持有一个对 Bitmap 对象的引用。然而,这意味着当 FruitItem 可能不在屏幕上时,需要将完整的图像保存在内存中。

为了让 Android Asset Packaging Tool 识别并存储图标,你需要将它们放在 res/drawable 目录中。

提示

安卓图像资源

通常,在 Android 中,将位图图像存储为 PNG 文件被认为是一个好习惯。由于你将要从代码中访问这些文件,请确保它们具有 Java 友好的文件名。PNG 格式(与 JPG 不同)是无损的,可以具有不同的颜色深度,并且正确处理透明度。这使得它整体上成为一个很棒的图像格式。

是时候行动了——构建水果菜单

对于 四个水果桶菜单,我们需要一个 ListAdapter 实现,以将 FruitItem 对象渲染到 fruit_item.xml 布局资源中。我们还需要一个 GridView 的布局资源,我们将在新的 Activity 类中加载它。

  1. 在项目的根包中创建一个名为 FruitAdapter 的类,继承自 BaseAdapter

  2. FruitAdapter 需要保存并代表一个 FruitItem 对象数组。使用与 BurgerAdapter 相同的结构实现该类。

  3. ListAdapter.getView 方法中,按照 fruit_item.xml 布局资源中定义的标签和图标进行设置:

    FruitItem item = items[index];
    TextView text = ((TextView)view.findViewById(R.id.text));
    ImageView image = ((ImageView)view.findViewById(R.id.icon));
    text.setText(item.name);
    image.setImageResource(item.image);
    
  4. 创建一个新的布局资源,用于保存我们将用于 四个水果桶菜单GridView,并将其命名为 res/layout/four_buckets.xml

  5. 使用三列 GridView 填充新的布局资源:

    <GridView 
    
        android:numColumns="3"
        android:horizontalSpacing="5dip"
        android:verticalSpacing="5dip"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>
    

刚才发生了什么?

新的 four_buckets.xml 布局资源中只有一个 GridView。这与我们迄今为止编写的其他布局资源不同,尤其是 GridView 没有 ID。对于这个例子,水果菜单 Activity 将只包含 GridView,因此无需 ID 引用或布局结构。我们还指定了水平和垂直间距为 5dipGridView 对象的默认设置是在其单元格之间没有间距,这使得内容相当紧凑。为了使内容之间稍微有些间隔,我们要求在各个单元格之间有一些空白。

是时候行动了——创建 FourBucketsActivity

由于我们使用的是只有一个GridView的布局资源,并且没有 ID 引用,我们将逐步创建Activity。与之前的Activity实现不同,我们需要直接引用在four_buckets.xml中定义的GridView,这意味着需要手动加载它。

  1. 从在你的项目的根包中创建一个新类开始:

    public class FourBucketsActivity extends Activity {
    
  2. 重写onCreate方法,并调用父类实现:

    protected void onCreate(final Bundle istate) {
        super.onCreate(istate);
    
  3. 为你的Activity对象获取LayoutInflator实例:

    LayoutInflater inflater = getLayoutInflater();
    
  4. 充气four_buckets.xml资源,并将其内容直接转换为GridView对象:

    GridView view = (GridView)inflater.inflate(
            R.layout.four_buckets,
            null);
    
  5. view对象的ListAdapter设置为新FruitAdapter类的实例,并用一些FruitItem对象填充新的FruitAdapter

    view.setAdapter(new FruitAdapter(
            new FruitItem("Apple", R.drawable.apple),
            new FruitItem("Banana", R.drawable.banana),
            new FruitItem("Black Berries", R.drawable.blackberry),
            // and so on
    
  6. 使用setContentView使GridView成为你的根View对象:

    setContentView(view);
    
  7. 在你的AndroidManifest.xml中注册你的FourBucketsActivity类。

  8. SelectRestaurantActivity添加一个案例,当用户选择时启动新的FourBucketsActivity

刚才发生了什么?

你刚刚完成了四桶水果菜单。如果你将应用程序重新安装到你的模拟器中,你现在将能够去订购水果(只需小心准备好 16 吨的重量,以防送货员攻击你)。

如果你查看Activity文档,你会注意到虽然有一个setContentView方法,但没有相应的getContentView方法。仔细查看,你会注意到addContentView方法。Activity对象可以有任意数量的“内容”View对象附加到它上面。这使得任何有用的getContentView方法的实现变得不可能。

为了克服这个限制,我们自己充气了布局。使用的getLayoutInflator()方法只是LayoutInflator.from(this)的简写。我们没有使用 ID 和findViewById,而是直接将返回的View转换为GridView,因为我们的four_buckets.xml文件只包含这个(与ArrayAdapter类处理TextView对象的方式类似)。如果我们想要更抽象一点,我们可以将其转换为AdapterView<ListAdapter>,在这种情况下,我们可以将文件中的实现替换为ListView。然而,这对于这个例子来说并没有太大帮助。

如果你现在重新安装并运行应用程序,你的新FourBucketsActivity将展示一个类似以下的屏幕:

刚才发生了什么?

尝试英雄——山姆寿司

菜单上的最后一家餐厅是Sam's Sushi。尝试使用Spinner类和GridView创建一个复合寿司菜单。将下拉菜单放在屏幕顶部,提供不同类型寿司的选项:

  • 刺身

  • 麻辣卷

  • 寿司

  • 押寿司

  • 加州卷

  • 时尚三明治

  • 手卷

Spinner下方,使用GridView显示用户可以订购的每种不同类型鱼的图标。以下是一些建议:

  • 金枪鱼

  • 黄尾鱼

  • 鲷鱼

  • 鲑鱼

  • 鳗鱼

  • 海胆

  • 鱿鱼

Spinner 类使用了 SpinnerAdapter 而不是 ListAdapterSpinnerAdapter 包含了一个额外的 View 对象,它表示下拉菜单。这通常是指向 android.R.layout.simple_dropdown_item_1line 资源的引用。然而,对于这个例子,你或许可以使用 Spinner XML 元素上的 android:entries 属性。

概述

数据展示是移动应用程序最常见的要求之一,Android 有许多不同的选项可用。ListView 可能是标准 Android 套件中最常用的控件之一,对其样式进行设置可以使其用来显示不同数量的数据,从单行菜单项到多行的待办事项笔记。

GridView 实际上是 ListView 的表格版本,非常适合向用户展示图标视图。图标比文本有巨大的优势,因为用户可以更快地识别它们。图标还可以占用更少的空间,在 GridView 中,你可以在竖屏屏幕上轻松地放置四到六个图标,而不会让用户界面显得杂乱或难以操作。这也为其他项目显示释放了宝贵的屏幕空间。

构建自定义 Adapter 类不仅允许你完全控制 ListView 的样式,还可以决定数据来源以及如何加载数据。例如,你可以通过使用在 Web 服务响应实际数据之前生成虚拟 View 对象的 Adapter 直接从 Web 服务加载数据。仔细查看默认的 Adapter 实现,它们通常可以满足你的需求,尤其是与自定义布局资源结合使用时。

在下一章中,我们将看看 Android 提供的一些不那么通用、更加专业的 View 类。与 Android 中的几乎所有事物一样,默认值可能很具体,但它们可以通过多种方式定制,以适应一些非常特殊的需求。

第三章:使用专用 Android 控件进行开发

除了许多通用控件,如按钮、文本字段和复选框外,Android 还包括各种更专业的控件。虽然按钮相当通用,在许多情况下都有用途,但例如图库控件则更为针对性。在本章中,我们将开始研究更专业的 Android 控件,它们的出现位置以及最佳使用方法。

尽管这些是非常专业的View类,但它们非常重要。如前所述(这一点真的非常重要)良好用户界面设计的一个基石是一致性。例如DatePicker控件。它绝对不是世界上最漂亮的日期选择器。它不是一个日历控件,因此用户有时很难选择确切的日期(大多数人会想到“下周二”,而不是“17 号星期二”)。然而,DatePicker是标准的!所以用户确切知道如何使用它,他们不必使用一个有问题的日历实现。本章将使用 Android 更专业的View和布局类:

  • Tab布局

  • TextSwitcher

  • Gallery

  • DatePicker

  • TimePicker

  • RatingBar

这些类具有非常特殊的目的,其中一些在实现方式上略有不同。本章将探讨如何以及在何处使用这些控件,以及在使用它们的实现细节上需要小心。我们还将讨论如何将这些元素最佳地融入到应用程序和布局中。

创建一个餐厅评论应用程序

在上一章中,我们构建了一个外卖应用程序。在本章中,我们将要看看餐厅评论。该应用程序将允许用户查看其他人对餐厅的看法,一个餐厅照片的图库,以及最终在线预订的部分。我们将应用程序分为三个部分:

  • 回顾:此餐厅的评论和评分信息

  • 照片:餐厅的照片图库

  • 预订:向餐厅提出预订请求

当构建一个需要快速向用户展示这三个部分的应用程序时,最合理的选择是将每个部分放在屏幕上的一个标签页中。这样用户可以在三个部分之间切换,而无需同时将它们全部显示在屏幕上。这还节省了屏幕空间,为每个部分提供更多的空间。

回顾标签将包括人们对正在查看的餐厅的循环评论列表,以及餐厅的平均“星级”评分。

展示餐厅的照片是照片标签的工作。我们将在屏幕顶部为用户提供一个缩略图“轨道”,并使用剩余的屏幕空间显示所选图像。

对于预订标签,我们希望捕获用户的名字以及他们希望预订的时间(日期和时间)。最后,我们还需要知道预订将是为多少人。

动手时间——创建机器人评审项目结构

要开始这个示例,我们需要一个带有新的Activity的新项目。新的布局和Activity将与前两章的结构略有不同。为了构建标签式布局,我们需要使用FrameLayout类。因此,首先,我们将创建一个新的项目结构,并从一个框架开始,这个框架最终将成为我们的标签布局结构。这可以填充三个内容区域。

  1. 使用 Android 命令行工具创建一个新的 Android 项目:

    android create project -n RoboticReview -p RoboticReview -k com.packtpub.roboticreview -a ReviewActivity -t 3
    
    
  2. 在编辑器或 IDE 中打开res/layout/main.xml文件。

  3. 清除默认代码(保留 XML 头)。

  4. 创建一个根FrameLayout元素:

    <FrameLayout 
    
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  5. 在新的FrameLayout元素内,添加一个垂直 LinearLayout

    <LinearLayout android:id="@+id/review"
                  android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="wrap_content">
    </LinearLayout>
    
  6. LinearLayout之后,添加另一个空的LinearLayout元素:

    <LinearLayout android:id="@+id/photos"
                  android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="wrap_content">
    </LinearLayout>
    
  7. 然后,在第二个LinearLayout元素之后,添加一个空的ScrollView

    <ScrollView android:id="@+id/reservation"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">
    </ScrollView>
    

FrameLayout将被 Android 标签结构用作内容区域,每个子元素都将成为一个标签的内容。在上面的布局中,我们为评审照片部分添加了两个LinearLayout元素,并为预订标签添加了一个ScrollView

刚才发生了什么?

我们刚刚开始“餐厅评审”应用程序,为用户界面构建了一个框架。在继续示例之前,我们应该先浏览一下这个main.xml文件的几个关键部分。

首先,我们的根元素是一个FrameLayoutFrameLayout将其所有子元素锚定在自己的左上角。实际上,两个LinearLayoutScrollView将相互重叠。这种结构可以用来形成类似于 Java AWT CardLayout的东西,TabHost对象将使用它来在相应标签处于激活状态时显示这些对象。

其次,每个LinearLayoutScrollView都有一个 ID。为了将它们标识为标签根,我们需要能够从 Java 代码轻松访问它们。标签结构可能在 XML 中设计,但它们需要在 Java 中组合。

构建 TabActivity

为了继续,我们需要我们的Activity类来设置我们在main.xml文件中声明为标签的三个标签内容元素。按偏好,Android 中的所有标签都应该有一个图标。

以下是去掉图标的标签页的截图:

构建 TabActivity

以下是带有图标的标签页的截图:

构建 TabActivity

创建标签图标

安卓应用程序具有由系统提供的默认控件定义的特定外观和感觉。为了使所有应用程序对用户保持一致,应用开发者应遵循一系列的用户界面指南。虽然让应用程序脱颖而出很重要,但用户经常会因为应用程序不熟悉或看起来不协调而感到沮丧(这也是自动移植的应用程序通常非常不受欢迎的原因之一)。

安卓的标签和图标

在为应用程序选择标签图标时,最好实践是包含几个不同版本,以适应不同的屏幕大小和密度。在高密度屏幕上看起来很好的抗锯齿角,在低密度屏幕上看起来会很糟糕。对于非常小的屏幕,你也可以提供完全不同的图标,而不是丢失所有图标细节。当安卓标签被选中时,它们会显得凸起,而在未选中时则降低到背景中。安卓标签图标应该具有与它们所在标签相反的“雕刻”效果,即选中时降低,未选中时凸起。因此,图标主要有两种状态:选中状态和未选中状态。为了在这两种状态之间切换,标签图标通常由三个资源文件组成:

  • 选中图标的图像

  • 未选中图标的图像

  • 一个描述图标两种状态的 XML 文件

标签图标通常是简单的形状,而图像大小是正方形(通常最大为 32 x 32 像素)。对于不同像素密度的屏幕,应使用图像的不同变体(详见第一章,开发一个简单的活动关于“资源选择”的细节)。通常,对于选中状态,你会使用深色外凸图像,因为当标签被选中时,标签背景是浅色的。对于未选中的图标,正好相反,应该使用浅色内凹图像。

安卓应用程序中的位图图像应始终为 PNG 格式。我们将评论标签的选中图标命名为 res/drawable/ic_tab_selstar.png,未选中图标文件命名为 res/drawable/ic_tab_unselstar.png。为了自动在这两张图像之间切换状态,我们定义了一个特殊的 StateListDrawable 作为 XML 文件。因此,评论图标实际上在一个名为 res/drawable/review.xml 的文件中,其看起来像这样:

<selector 
          android:constantSize="true">

    <item
        android:drawable="@drawable/ic_tab_selstar"
        android:state_selected="false"/>

    <item
        android:drawable="@drawable/ic_tab_unselstar"
        android:state_selected="true"/>
</selector>

注意 <selector> 元素的 android:constantSize="true" 属性。默认情况下,安卓会假定 StateListDrawable 对象中的每个状态都会导致图像大小不同,进而可能导致用户界面重新运行布局计算。这可能会相当耗时,所以最好声明你的每个状态都是完全相同的大小。

在这个例子中,我们将使用三个标签图标,每个图标有两种状态。这些图标分别名为reviewphotosbook。每个图标都由三个文件组成:一个用于选中状态的 PNG 文件,一个用于未选中状态的 PNG 文件,以及一个定义状态选择器的 XML 文件。从我们的应用程序中,我们只需要直接使用状态选择器的 XML 文件,实际的 PNG 文件由 Android API 来加载。

实现 ReviewActivity

和往常一样,我们希望在我们的strings.xml文件中有本地化的文本。打开res/values/strings.xml文件,并复制以下代码到它里面:

<resources>
    <string name="app_name">Robotic Review</string>
    <string name="review">Review</string>
    <string name="gallery">Photos</string>
    <string name="reservation">Reservations</string>
</resources>

行动时刻——编写 ReviewActivity 类

如前所述,我们需要在 Java 代码中设置我们的标签布局结构。幸运的是,Android 提供了一个非常实用的TabActivity类,它为我们完成了大部分繁重的工作,提供了一个现成的TabHost对象,我们可以用这个对象构建Activity的标签结构。

  1. 打开之前生成的ReviewActivity.java文件,在编辑器或 IDE 中。

  2. 不要扩展Activity,将类改为继承TabActivity

    public class ReviewActivity extends TabActivity
    
  3. onCreate方法中,完全移除setContentView(R.layout.main)这一行(由android create project工具生成)。

  4. 首先,从你的父类中获取TabHost对象:

    TabHost tabs = getTabHost();
    
  5. 接下来,我们将布局 XML 文件加载到TabHost的内容视图中:

    getLayoutInflater().inflate(
            R.layout.main,
            tabs.getTabContentView(),
            true);
    
  6. 我们需要访问我们应用程序的其他资源:

    Resources resources = getResources();
    
  7. 现在我们为Review标签定义一个TabSpec

    TabHost.TabSpec details =
            tabs.newTabSpec("review").
            setContent(R.id.review).
            setIndicator(getString(R.string.review),
            resources.getDrawable(R.drawable.review));
    
  8. 使用前面的模式为PhotosReservation标签定义另外两个TabSpec变量。

  9. 将每个TabSpec对象添加到我们的TabHost中:

    tabs.addTab(details);
    tabs.addTab(gallery);
    tabs.addTab(reservation);
    

这就完成了ReviewActivity类的标签结构的创建。

刚才发生了什么?

我们为我们的新ReviewActivity构建了一个非常基本的标签布局。在使用标签时,我们并没有简单地使用Activity.setContentView方法,而是自己加载了布局 XML 文件。然后我们使用了TabActivity类提供的TabHost对象创建了三个TabSpec对象。TabSpec是一个构建器对象,它允许你构建你的标签内容,类似于使用StringBuilder构建文本的方式。

TabSpec的内容是将会附加到屏幕上标签的内容视图(通过setContent方法分配)。在这个例子中,我们选择了最简单的选项,在main.xml文件中定义了标签内容。也可以通过使用TabHost.TabContentFactory接口懒加载标签内容,或者甚至通过使用setContent(Intent)将外部Activity(如拨号器或浏览器)放入标签中。但是,为了这个例子的目的,我们使用了最简单的选项。

你会注意到TabSpec(类似于StringBuilder类)支持方法调用的链式操作,这使得以“单次设置”方法(如之前所做的)或分阶段构建TabSpec(即在从外部服务加载时)变得简单且灵活。

我们分配给TabSpecindicator是将在标签上显示的内容。在前一个案例中,是一段文本和我们的图标。从 API 级别 4(Android 版本 1.6)开始,可以使用View对象作为indicator,允许完全自定义标签的外观和感觉。为了保持示例简单(并与早期版本兼容),我们提供了一个String资源作为indicator

行动时间 - 创建评论布局

我们已经有了一个标签结构的框架,但里面还没有内容。第一个标签标题为评论,这就是我们将要开始的地方。我们已经完成了足够的 Java 代码以加载标签并将它们显示在屏幕上。现在我们回到main.xml布局文件,用一些提供用户评论信息的部件填充这个标签。

  1. 在编辑器或 IDE 中打开res/layout/main.xml

  2. 在我们命名为review<LayoutElement>内,添加一个新的TextView,它将包含餐厅的名称:

    <TextView android:id="@+id/name"
              android:textStyle="bold"
              android:textSize="25sp"
              android:textColor="#ffffffff"
              android:gravity="center|center_vertical"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  3. 在新的TextView下方,添加一个新的RatingBar,我们将在这里显示其他人对餐厅的评分:

    <RatingBar android:id="@+id/stars"
               android:numStars="5"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"/>
    
  4. 为了保持这个第一个标签简单,我们添加了一个TextSwitcher,我们可以在其中显示其他人对餐厅的评论:

    <TextSwitcher android:id="@+id/reviews"
                  android:inAnimation="@android:anim/fade_in"
                  android:outAnimation="@android:anim/fade_out"
                  android:layout_width="fill_parent"
                  android:layout_height="fill_parent"/>
    

在这个例子中,评论标签只有三个小部件,但可以轻松添加更多,让用户输入自己的评论。

刚才发生了什么

我们刚刚为第一个标签组合了布局。我们创建的RatingBar具有wrap_content的宽度,这非常重要。如果你使用fill_parent,则RatingBar中可见的星星数量将尽可能多地适应屏幕。如果你想控制RatingBar上显示的星星数量,请坚持使用wrap_content,但还要确保(至少在竖屏布局上)RatingBar有自己的水平线。如果你现在在模拟器中安装Activity,你将不会在TextViewTextSwitcher中看到任何内容。

TextSwitcher没有默认动画,因此我们将“进入”动画指定为android包提供的默认fade_in,而“退出”动画将是fade_out。这种语法用于访问可以在android.R类中找到的资源。

使用切换器类

我们已经放置的TextSwitcher用于在不同的TextView对象之间进行动画切换。它非常适合显示像股票价格变化、新闻标题或在我们的案例中,评论这样的内容。它继承自ViewSwitcher,后者可以用于在任意两个通用View对象之间进行动画切换。ViewSwitcher扩展了ViewAnimator,后者可以用作一种动画CardLayout

我们希望展示一系列来自过去客户的评论,并通过简短动画使它们之间渐变。TextSwitcher 需要两个 TextView 对象(它会要求我们动态创建),在我们的示例中。我们希望这些对象在资源文件中。

为了示例的下一部分,我们需要一些评论。而不是使用网络服务或类似的东西来获取真实的评论,这个示例将从其应用程序资源中加载一些评论。打开 res/values/strings.xml 文件,并添加带有一些可能评论的 <string-array name="comments">

<string-array name="comments">
    <item>Just Fantastic</item>
    <item>Amazing Food</item>
    <item>What rubbish, the food was too hairy</item>
    <item>Messy kitchen; call the health inspector.</item>
</string-array>

行动时间——开启 TextSwitcher

我们希望 TextSwitcher 每 5 秒钟显示下一个列出的评论。为此,我们将需要使用新的资源和一个 Handler 对象。Handler 是 Android 应用程序和服务之间在线程之间发布消息的方式,也可以用于在将来的某个时间点安排消息。它比 java.util.Timer 更受推荐的结构,因为 Handler 对象不会分配新的 Thread。在我们的情况下,Timer 过于复杂,因为只有一个任务我们想要安排。

  1. 在你的 res/layout 目录中创建一个名为 review_comment.xml 的新 XML 文件。

  2. 将以下代码复制到新的 review_comment.xml 文件中:

    <TextView 
    
    
        android:gravity="left|top"
        android:textStyle="italic"
        android:textSize="16sp"
        android:padding="5dip"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    
  3. 在编辑器或 IDE 中打开 ReviewActivity.java 文件。

  4. 我们需要能够加载 TextSwitcherreview_comment 资源,所以 ReviewActivity 需要实现 ViewSwitcher.ViewFactory 接口。

  5. 为了更新 TextSwitcher,我们需要与一个 Handler 交互,在这里最简单的方法是也实现 Runnable

  6. ReviewActivity 类的顶部,声明一个 Handler 对象:

    private final Handler switchCommentHandler = new Handler();
    
  7. 我们还希望在我们的 run() 方法中保留对 TextSwitcher 的引用,当我们切换评论时:

    private TextSwitcher switcher;
    
  8. 为了显示评论,我们将需要一个评论数组,以及一个索引来跟踪 TextSwitcher 正在显示哪个评论:

    private String[] comments;
    private int commentIndex = 0;
    
  9. 现在,在 onCreate 方法中,将 TabSpec 对象添加到 TabHost 之后,从 Resources 中读取 comments 字符串数组:

    comments = resources.getStringArray(R.array.comments);
    
  10. 接下来,找到 TextSwitcher 并将其分配给 switcher 字段:

    switcher = (TextSwitcher)findViewById(R.id.reviews);
    
  11. 告诉 TextSwitcherReviewActivity 对象将是它的 ViewFactory

    switcher.setFactory(this);
    
  12. 为了符合 ViewFactory 的规范,我们需要编写一个 makeView 方法。在我们的例子中这非常简单——只需膨胀 review_comment 资源:

    public View makeView() {
        return getLayoutInflater().inflate(
                R.layout.review_comment, null);
    }
    
  13. 重写 onStart 方法,以便我们可以发布之前声明的 Handler 对象上的第一个定时事件:

    protected void onStart() {
        super.onStart();
        switchCommentHandler.postDelayed(this, 5 * 1000l);
    }
    
  14. 类似地,重写 onStop 方法以取消任何未来的回调:

    protected void onStop() {
        super.onStop();
        switchCommentHandler.removeCallbacks(this);
    }
    
  15. 最后,run() 方法在 TextSwitcher 中交替评论,并在 finally 块中,在 5 秒后将自身重新发布到 Handler 队列中:

    public void run() {
        try {
            switcher.setText(comments[commentIndex++]);
            if(commentIndex >= comments.length) {
                commentIndex = 0;
            }
        } finally {
            switchCommentHandler.postDelayed(this, 5 * 1000l);
        }
    }
    

使用Handler对象而不是创建Thread对象意味着所有定时任务可以共享主用户界面线程,而不是各自分配一个单独的线程。这减少了应用程序在设备上占用的内存和 CPU 负载,对应用程序性能和电池寿命有直接影响。

刚才发生了什么?

我们刚刚构建了一个简单的定时器结构,用旋转的评论数组更新TextSwitcherHandler类是在两个应用程序线程之间发布消息和操作的一种便捷方式。在 Android 中,与 Swing 一样,用户界面不是线程安全的,因此线程间通信变得非常重要。Handler对象试图将自己绑定到创建它的线程(在前面的情况下,是main线程)。

创建Handler对象的线程必须有一个关联的Looper对象,这是前提条件。你可以在自己的线程中通过继承HandlerThread类或使用Looper.prepare()方法来设置这个。发送到Handler对象的消息将由与同一线程关联的Looper执行。通过将我们的ReviewActivity(实现了Runnable)发送到我们在main线程中创建的Handler对象,我们知道无论哪个线程发布它,ReviewActivity.run()方法都将在main线程上执行。

对于长时间运行的任务(例如获取网页或长时间的计算),Android 提供了一个与SwingWorker类惊人相似的类,名为AsyncTaskAsyncTask(与Handler一样)可以在android.os包中找到,你可以通过继承来使用它。AsyncTask用于允许后台任务与用户界面之间的交互(以更新进度条或类似需求)。

刚才发生了什么?

创建一个简单的照片画廊

Gallery这个词的使用有点误导人,它实际上是一个具有“单选项目”选择模型的水平行项目。在这个例子中,我们将使用Gallery类做它最擅长的事情,即显示缩略图。但是,正如你将看到的,它能够显示几乎任何内容的滚动列表。由于Gallery是一个微调器,你可以以与Spinner对象或ListView相同的方式使用它,即使用Adapter

动手时间——构建照片标签

在我们能够将图像添加到Gallery之前,我们需要在屏幕上有一个Gallery对象。为了开始这个练习,我们将向我们的标签中的FrameLayout添加一个Gallery对象和一个ImageView。这将在本章开始时创建的Photos标签下显示。我们将坚持使用相对传统的照片画廊模型,在屏幕顶部滑动缩略图,在下面显示选定图像的完整视图。

  1. 在你的编辑器或 IDE 中打开res/layout/main.xml

  2. 在第二个LinearLayout中,使用android:id="@+id/photos",添加一个新的Gallery元素以容纳缩略图:

    <Gallery android:id="@+id/gallery"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"/>
    
  3. 默认情况下,Gallery对象会将内容挤压在一起,这在我们的案例中看起来并不好。你可以通过使用Gallery类的spacing属性,在项目之间添加一点内边距:

    android:spacing="5dip"
    
  4. 我们在Gallery正上方也有标签页,并且在它下面会直接放置一个ImageView。同样,这里不会有任何内边距,所以我们需要使用外边距来添加一些空间:

    android:layout_marginTop="5dip"
    android:layout_marginBottom="5dip"
    
  5. 现在创建一个ImageView,我们可以用它来显示全尺寸的图片:

    <ImageView android:id="@+id/photo"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
    
  6. 为了确保全屏显示能正确缩放,我们需要在ImageView上指定scaleType

    android:scaleType="centerInside"
    

Gallery元素在屏幕顶部为我们提供了缩略图轨道。在Gallery中选择的图片将在ImageView小部件中以全尺寸显示。

刚才发生了什么?

我们刚刚用基本照片画廊所需的标准小部件填充了第二个标签页。这个结构非常通用,但用户也非常熟悉和理解。Gallery类将处理缩略图、滚动和选择。但是,你需要将选定的图片填充到主ImageView中,并提供Gallery对象要在屏幕上显示的缩略图小部件。

Gallery元素上的间距属性将添加一些空白,这作为缩略图之间的简单分隔符。你也可以在每个缩略图图像中添加边框,为返回的每个缩略图ImageView小部件添加边框,或者使用自定义小部件创建边框。

创建一个缩略图小部件

为了在Gallery对象中显示缩略图,我们需要为每个缩略图创建一个ImageView对象。我们可以在 Java 代码中轻松完成,但像往常一样,即使是最基本的小部件,也最好使用 XML 资源构建。在这种情况下,在res/layout目录中创建一个新的 XML 资源。将新文件命名为gallery_thn.xml,并将以下代码复制到其中:

<ImageView 
           android:scaleType="fitXY"/>

没错,它只有两行 XML,但重申一遍,这允许我们为许多不同的配置自定义此小部件,而无需编辑 Java 代码。虽然编辑代码可能看起来不是问题(资源需要重新编译),但你也同样不希望最终得到一系列长长的if语句来决定如何确切地创建ImageView对象。

实现一个 GalleryAdapter

为了简化问题,本例我们将继续使用应用资源。我们将有两个资源 ID 数组,一个是缩略图,另一个是完整尺寸的图片。Adapter实现期望为每个项目提供一个标识符。在下一个示例中,我们将提供完整尺寸图像的资源 ID 作为标识符,这样我们在Adapter实现之外的类中可以轻松访问完整尺寸的图像。虽然这是一个不寻常的约定,但它为我们提供了一种在已定义结构内传递图像资源的便捷方式。

为了显示你的图库,你需要一些图像进行展示(我的尺寸为 480 x 319 像素)。对于这些图像中的每一个,你都需要在Gallery对象中显示一个缩略图。通常,这些应该是实际图像的缩小版本(我的缩小到 128 x 84 像素)。

是时候行动了——GalleryAdapter

创建GalleryAdapter与我们在第二章中创建的ListAdapter类非常相似。但是,GalleryAdapter将使用ImageView对象而不是TextView对象。它还将两个资源列表绑定在一起,而不是使用对象模型。

  1. 在你的项目根包中创建一个新的 Java 类,名为GalleryAdapter。它应该扩展BaseAdapter类。

  2. 声明一个整数数组来保存缩略图资源的 ID:

    private final int[] thumbnails = new int[]{
        R.drawable.curry_view_thn,
        R.drawable.jai_thn,
        // your other thumbnails
    };
    
  3. 声明一个整数数组来保存完整尺寸图像资源的 ID:

    private final int[] images = new int[]{
        R.drawable.curry_view,
        R.drawable.jai,
        // your other full-size images
    };
    
  4. getCount()方法仅仅是thumbnails数组的长度:

    public int getCount() {
        return thumbnails.length;
    }
    
  5. getItem(int)方法返回完整尺寸图像资源的 ID:

    public Object getItem(int index) {
        return Integer.valueOf(images[index]);
    }
    
  6. 如前所述,getItemId(int)方法返回完整尺寸图像资源的 ID(几乎与getItem(int)完全一样):

    public long getItemId(int index) {
        return images[index];
    }
    
  7. 最后,getView(int, View, ViewGroup)方法使用LayoutInflater读取并填充我们在gallery_thn.xml布局资源中创建的ImageView

    public View getView(int index, View reuse, ViewGroup parent) {
        ImageView view = (reuse instanceof ImageView)
                ? (ImageView)reuse
                : (ImageView)LayoutInflater.
                             from(parent.getContext()).
                             inflate(R.layout.gallery_thn, null);
        view.setImageResource(thumbnails[index]);
        return view;
    }
    

Gallery类是AdapterView的子类,因此其功能与ListView对象相同。GalleryAdapter将为Gallery对象提供View对象以显示缩略图。

刚才发生了什么

与上一章构建的Adapter类类似,GalleryAdapter将尝试重用其getView方法中指定的任何View对象。然而,一个主要的区别是,这个GalleryAdapter是完全自包含的,并且总是显示相同的图像列表。

这个GalleryAdapter的示例非常简单。你也可以构建一个持有位图对象而不是资源 ID 引用的GalleryAdapter。然后你会使用ImageView.setImageBitmap方法,而不是ImageView.setImageResource

你也可以通过让ImageView将全尺寸图片缩放成缩略图来消除缩略图。这将只需要修改gallery_thn.xml资源文件,以指定每个缩略图所需的大小。

<ImageView 
           android:maxWidth="128dip"
           android:adjustViewBounds="true"
           android:scaleType="centerInside"/>

adjustViewBounds属性告诉ImageView调整自身大小,以保持其中图片的宽高比。我们还改变了scaleType属性为centerInside,当图片缩放时,这也会保持图片的宽高比。最后,我们为ImageView设置了最大宽度。使用标准的layout_widthlayout_height属性会被Gallery类忽略,因此我们改为向ImageView指定所需缩略图的大小(layout_widthlayout_height属性由Gallery处理,而maxWidthmaxHeightImageView处理)。

这将是一个标准的速度/大小权衡。拥有缩略图会占用更多的应用空间,但让ImageView执行缩放会使应用变慢。ImageView中的缩放算法也不会像 Adobe Photoshop 这样的图像处理应用中的缩放那样高质量。在大多数情况下这不会是问题,但如果你有高细节的图片,通常使用更简单的缩放算法会出现“缩放失真”。

是时候行动了——让图库工作起来

既然我们已经让GalleryAdapter工作起来了,我们需要将GalleryGalleryAdapterImageView连接起来,以便当选择了一个缩略图时,可以在ImageView对象中显示该图片的全视图。

  1. 在你的编辑器或 IDE 中打开ReviewActivity源代码。

  2. ReviewActivity实现的接口中添加AdapterView.OnItemSelectedListener

  3. TextSwitcher声明下方,声明一个对ImageView的引用,该ImageView将用于显示全尺寸的图片:

    private TextSwitcher switcher;
    private ImageView photo;
    
  4. onCreate方法的末尾,找到名为photoImageView并将其分配给你刚刚声明的引用:

    photo = ((ImageView)findViewById(R.id.photo));
    
  5. 现在,获取在main.xml布局资源中声明的Gallery对象:

    Gallery photos = ((Gallery)findViewById(R.id.gallery));
    
  6. 创建一个新的GalleryAdapter并将其设置在Gallery对象上:

    photos.setAdapter(new GalleryAdapter());
    
  7. Gallery对象的OnItemSelectedListener设置为this

    photos.setOnItemSelectedListener(this);
    
  8. ReviewActivity类的末尾,添加onItemSelected方法:

    public void onItemSelected(
            AdapterView<?> av, View view, int idx, long id) {
        photo.setImageResource((int)id);
    }
    
  9. OnItemSelectedListener还需要一个onNothingSelected方法,但对于这个例子,我们不需要它做任何事情。

GalleryAdapter通过id参数为ReviewActivity提供加载照片全视图所需的资源。如果图片位于远程服务器上,id参数也可以用作索引或标识符。

刚才发生了什么?

我们现在已经将Gallery对象连接到ImageView,我们将在其中显示全尺寸图片,而不是缩略图。我们使用了项目 ID 作为将全尺寸图片的资源 ID 直接发送到事件监听器的方式。这是一个相当奇怪的概念,因为你通常会使用对象模型。然而,在这个例子中,引入一个对象模型不仅仅会带来一个新类,它还需要在事件触发时从Adapter获取图片对象的另一个方法调用。

当你在像Gallery这样的AbsSpinner类上指定一个Adapter时,它会默认尝试选择从其新Adapter返回的第一个项目。这进而会通知已注册的OnItemSelectedListener对象。然而,由于 Android 用户界面对象使用的单线程模型,这个事件不会立即触发,而是在我们从onCreate方法返回后一段时间触发。当我们在Gallery对象上调用setAdapter(new GalleryAdapter())时,它会安排一个选择变更事件,然后我们收到这个事件。该事件导致ReviewActivity类显示GalleryAdapter对象中的第一张照片。

如果你现在在模拟器中重新安装应用程序,你将能够转到照片标签,浏览你用GalleryAdapter填充的所有图片的Gallery

发生了什么?

小测验

  1. 如果在前一个例子中,你将OnItemSelectedListener替换为OnItemClickListener(像在ListView示例中所做的那样),会发生什么?

    1. 全尺寸图片不再出现。

    2. 当触摸缩略图时,Gallery不会旋转它们。

    3. 只有当点击缩略图时,全尺寸照片才会出现。

  2. ScaleTypefitXYcenterInside之间的主要区别是什么?

    1. fitXY类型会将图片锚定到左上角,而centerInside会在ImageView中居中图片。

    2. fitXY会使图片扭曲到ImageView的大小,而centerInside将保持图片的宽高比。

    3. centerInside会使较大的轴被裁剪,以使图片适应ImageView,而fitXY会缩放图片,使较大轴的大小与ImageView相同。

  3. 当使用wrap_content属性时,什么决定了包含ImageView对象的Gallery对象的大小?

    1. ImageView对象的宽度和高度,由其内容图片的大小,或者它们的maxWidthmaxHeight参数决定。

    2. Gallery对象上的itemWidthitemHeight参数。

    3. 设置在ImageView对象上的LayoutParams(通过setLayoutParams方法,或者layout_width/layout_height属性)。

尝试英雄——动画和外部资源

既然你已经让基本示例运行起来,尝试稍微改善一下用户体验。当你触摸图像时,它们应该真正地动画显示,而不是立即改变。它们也应该来自外部资源,而不是应用程序资源。

  1. 将全尺寸图像的ImageView对象更改为ImageSwitcher,使用标准的 Android 淡入/淡出动画。

  2. 从项目中移除缩略图,并使用在gallery_thn.xml文件中声明的ImageView来缩放图像。

  3. 从应用程序资源 ID 列表更改为Uri对象列表,以便从外部网站下载图像。

构建预定标签

虽然这个例子的评论照片标签关注的是信息的展示,但预定标签将关注于捕获预定的详细信息。我们实际上只需要三部分信息:

  • 预定需要用到的名字

  • 预定的日期和时间

  • 预定的人数

在这个例子的这部分,我们将创建几个具有格式化标签的小部件。例如,人数:2,这将随着用户更改值而更新人数。为了简单地进行这个操作,我们指定小部件的文本(在布局文件中指定)将包含用于显示的格式。作为初始化过程的一部分,我们从View对象读取文本,并使用它来创建一个格式结构。一旦有了格式,我们就可以用它的初始值填充View

行动时间——实现预定布局

在我们的main.xml布局资源中,我们需要添加将形成预定标签的View对象。目前它仅包含一个空的ScrollView,如果整个用户界面不适合屏幕,这将使用户能够垂直滚动布局。

  1. 在编辑器或 IDE 中打开main.xml文件。

  2. 在我们之前为Reservation标签创建的<ScrollView>内。声明一个新的垂直LinearLayout元素:

    <LinearLayout android:orientation="vertical"
                  android:layout_width="fill_parent"
                  android:layout_height="wrap_content">
    
  3. 在新的LinearLayout元素内,创建一个TextView以询问用户预定应使用什么名字:

    <TextView android:text="Under What Name:"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  4. TextView标签后,创建一个EditText以允许用户输入预定的名字:

    <EditText android:id="@+id/name"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  5. 创建另一个TextView标签,询问用户将有多少人参加。这包括一个格式元素,我们将在其中放置数字:

    <TextView android:id="@+id/people_label"
              android:text="How Many People: %d"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  6. 添加一个SeekBar,用户可以通过它告诉我们将有多少人参加:

    <SeekBar android:id="@+id/people"
             android:max="20"
             android:progress="1"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"/>
    
  7. 使用另一个TextView询问用户预定将在哪一天:

    <TextView android:text="For What Date:"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  8. 添加一个Button以显示预定日期。当用户点击这个Button时,我们会请他选择一个新的日期:

    <Button android:id="@+id/date"
            android:text="dd - MMMM – yyyy"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
  9. 创建另一个TextView标签来询问预定时间:

    <TextView android:text="For What Time:"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  10. 添加另一个Button以显示时间,并允许用户更改它:

    <Button android:id="@+id/time"
            android:text="HH:mm"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
  11. 最后,添加一个Button以完成预订,并为表单中的其余输入添加一些边距:

    <Button android:id="@+id/reserve"
            android:text="Make Reservation"
            android:layout_marginTop="15dip"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    

前面的几个小部件包含了标签的格式而非标签文本,实际的标签将在 Java 代码中生成和设置。这是因为当用户更改日期、时间或预期预订的人数时,这些标签可能会发生变化。

刚才发生了什么?

预订标签中,我们询问用户预订的人数,为了获取他们的答案,我们使用了SeekBar对象。SeekBar的工作方式与 Swing 中的JSlider非常相似,并为用户提供了一种选择预订人数的方式,只要这个数字在我们定义的范围内即可。Android 中的SeekBar实际上是建立在ProgressBar类之上的,因此继承了其所有 XML 属性,有时这可能显得有些奇怪。不幸的是,与JSliderJProgressBar不同,SeekBar类没有最小值,由于你不能为 0 人预订,我们通过在显示前始终将SeekBar的选择值加 1 来解决这个问题。这意味着默认值是1(将显示的值设置为 2 人)。

注意

大多数人可能会为两个人预订餐厅,因此默认值为1

人数:标签中,我们加入了一个%d,这是一个printf标记,用于放置用户预订的人数。当用户操作SeekBar时,我们将使用String.format更新标签为用户选择的数字。在“日期”和“时间”Button标签中,我们希望显示当前为预订选择的日期和时间。我们在 XML 文件中设置了要显示此数据的格式,稍后我们将使用标准的java.text.SimpleDateFormat解析它。

我们之前的示例中的国际化怎么办?我们不应该把标签放在strings.xml文件中,这样布局就不需要改变吗?答案是:是的,如果你想国际化用户界面。稍后,请确保你的所有显示文本都在应用程序资源文件中。然而,我强烈建议直接从布局中获取格式字符串,因为它允许你将格式数据解耦到一个额外的层次。

在前面的布局中,你创建了用于显示日期和时间的Button小部件。为什么不直接使用DatePickerTimePicker对象呢?答案是:不幸的是,它们不适合正常的布局。它们占用了大量的垂直空间,并且不能水平缩放。如果我们在这个用户界面中内联放置一个DatePickerTimePicker,它看起来将像左边的截图,而实际的用户界面是右边的截图。

刚才发生了什么?

如你所见,Button对象提供了一个更整洁的用户界面。值得庆幸的是,Android 为我们提供了DatePickerDialogTimePickerDialog,正好适用于这种情况。当用户点击其中一个Button小部件时,我们会弹出适当的对话框,并在他确认后更新所选Button的标签。

尽管使用ButtonDialog至少增加了用户界面的两次触摸操作,但它极大地改善了应用程序的外观和感觉。如果界面没有正确对齐,用户会感到烦恼,即使他们无法说出为什么感到烦恼。用户觉得讨厌或烦恼的屏幕是他们将避免的,或者更糟的是——直接卸载。

行动时间——初始化预订标签

预订标签中我们使用了格式化的标签。这些标签不应直接显示给用户,但在让用户看到之前需要用数据填充它们。为此,我们需要再次回到 Java 代码中,构建一些功能来记住格式,并填充标签。

  1. 在编辑器或 IDE 中打开ReviewActivity的 Java 源文件。

  2. 在你迄今为止声明的所有字段下方,我们需要为预订标签添加一些内容。声明一个String来记住人数标签的格式:

    private String peopleLabelFormat;
    
  3. 然后声明一个对人数标签的引用:

    private TextView peopleLabel;
    
  4. date Button的格式声明一个SimpleDateFormat对象:

    private SimpleDateFormat dateFormat;
    
  5. 声明对date Button的引用:

    private Button date;
    
  6. time Button的格式添加另一个SimpleDateFormat

    private SimpleDateFormat timeFormat;
    
  7. 接下来,为time Button对象声明一个Button引用:

    private Button time;
    
  8. onCreate方法的末尾,我们需要初始化预订标签。首先使用TextView.getText()方法分配peopleLabel并获取peopleLabelFormat

    peopleLabel = (TextView)findViewById(R.id.people_label);
    peopleLabelFormat = peopleLabel.getText().toString();
    
  9. 然后获取date Button的引用及其标签格式:

    date = (Button)findViewById(R.id.date);
    dateFormat = new SimpleDateFormat(date.getText().toString());
    
  10. time Button及其标签格式做同样的操作:

    time = (Button)findViewById(R.id.time);
    timeFormat = new SimpleDateFormat(time.getText().toString());
    
  11. 现在,我们需要用默认日期和时间填充Button对象,为此我们需要一个Calendar对象:

    Calendar calendar = Calendar.getInstance();
    
  12. 如果现在是下午 4 点以后,那么预订很可能应该是在下一天,所以如果这种情况,我们会在Calendar中加一天:

    if(calendar.get(Calendar.HOUR_OF_DAY) >= 16) {
        calendar.add(Calendar.DATE, 1);
    }
    
  13. 现在我们设置Calendar对象上的预订默认时间:

    calendar.set(Calendar.HOUR_OF_DAY, 18);
    calendar.clear(Calendar.MINUTE);
    calendar.clear(Calendar.SECOND);
    calendar.clear(Calendar.MILLISECOND);
    
  14. Calendar对象设置datetime按钮的标签:

    Date reservationDate = calendar.getTime();
    date.setText(dateFormat.format(reservationDate));
    time.setText(timeFormat.format(reservationDate));
    
  15. 现在,我们需要SeekBar以便获取其默认值(如布局应用程序资源中声明的那样):

    SeekBar people = (SeekBar)findViewById(R.id.people);
    
  16. 然后,我们可以使用标签格式和SeekBar值来填充人数标签:

    peopleLabel.setText(String.format(
                peopleLabelFormat,
                people.getProgress() + 1));
    

现在我们有了标签需要显示在用户界面上的各种格式。这允许我们在用户更改预订参数时重新生成标签。

刚才发生了什么?

预订标签现在将用预订的默认数据填充,并且所有标签中的格式都已消失。你可能已经注意到在之前的代码中有许多对toString()的调用。Android 的View类通常接受任何CharSequence作为标签。这比String类允许更高级的内存管理,因为CharSequence可以是StringBuilder,或者可以是实际文本数据的SoftReference的门面。

然而,大多数传统的 Java API 期望得到一个String,而不是一个CharSequence,因此我们使用toString()方法以确保我们有一个String对象。如果底层的CharSequence是一个String对象,toString()方法就是一个简单的return this;(这将起到类型转换的作用)。

同样,为了解决SeekBar没有最小值的事实,我们在填充peopleLabel的最后一行时,将其当前值加1。虽然datetime格式被存储为SimpleDateFormat,但我们将peopleLabelFormat存储为String,并在需要更新标签时通过String.format运行它。

刚才发生了什么?

动手时间——监听 SeekBar

界面现在已用默认数据填充。但是,它根本不具备交互性。如果你拖动SeekBar人数:标签将保持在其默认值2。我们需要一个事件监听器,在SeekBar被使用时更新标签。

  1. 在编辑器或 IDE 中打开ReviewActivity的 Java 源文件。

  2. SeekBar.OnSeekBarChangeListener添加到ReviewActivity实现的接口中。

  3. onCreate中,使用findViewById获取SeekBar之后,将其OnSeekBarChangeListener设置为this

    SeekBar people = (SeekBar)findViewById(R.id.people);
    people.setOnSeekBarChangeListener(this);
    
  4. 实现onProgressChanged方法以更新peopleLabel

    public void onProgressChanged(
                SeekBar bar, int progress, boolean fromUser) {
        peopleLabel.setText(String.format(
                peopleLabelFormat, progress + 1));
    }
    
  5. 实现一个空的onStartTrackingTouch方法:

    public void onStartTrackingTouch(SeekBar bar) {}
    
  6. 实现一个空的onStopTrackingTouch方法:

    public void onStopTrackingTouch(SeekBar bar) {}
    

String.format方法是 Android 中在本地化字符串中放置参数的常用方法。虽然这与普通的java.text.MessageFormat类有所不同,但在 Android 中首选这种方法(尽管仍然支持MessageFormat)。

刚才发生了什么?

当你在模拟器中重新安装应用程序时,你现在可以使用SeekBar来选择预订的人数。尽管我们没有实现onStartTrackingTouchonStopTrackingTouch方法,但如果你默认隐藏实际状态值,它们会非常有用。例如,你可以使用一个包含人员图标的Dialog来告知用户预订的人数。当他们触摸SeekBar时——显示Dialog,然后当他们释放SeekBar时——再次隐藏Dialog

发生了什么?

动手时间——选择日期和时间

我们已经让SeekBar按预期工作,但datetime Button控件呢?当用户触摸它们时,他们希望能够为预订选择不同的日期或时间。为此,我们需要一个古老的OnClickListener,以及DatePickerDialogTimePickerDialog类。

  1. 再次在编辑器或 IDE 中打开ReviewActivity Java 源文件。

  2. View.OnClickListenerDatePickerDialog.OnDateSetListenerTimePickerDialog.OnTimeSetListener添加到ReviewActivity实现的接口中。你的类声明现在应该看起来像这样:

    public class ReviewActivity extends TabActivity
            implements ViewSwitcher.ViewFactory,
            Runnable,
            AdapterView.OnItemSelectedListener,
            SeekBar.OnSeekBarChangeListener,
            View.OnClickListener,
            DatePickerDialog.OnDateSetListener,
            TimePickerDialog.OnTimeSetListener {
    
  3. 实现一个实用方法,用指定的SimpleDateFormatCharSequence解析为Calendar对象:

    private Calendar parseCalendar(
            CharSequence text, SimpleDateFormat format) {
    
  4. 打开一个try块,以便在CharSequence不符合SimpleDateFormat格式时处理解析错误:

  5. CharSequence解析为Date对象:

    Date parsedDate = format.parse(text.toString());
    
  6. 然后创建一个新的Calendar对象:

    Calendar calendar = Calendar.getInstance();
    
  7. Calendar对象的时间设置为Date对象中的时间:

    calendar.setTime(parsedDate);
    
  8. 返回解析后的Calendar对象:

    return calendar;
    
  9. 在这个方法中,你需要捕获(ParseException)。我建议将其包装在RuntimeException中并重新抛出:

    catch(ParseException pe) {
        throw new RuntimeException(pe);
    }
    
  10. onCreate方法中,设置datetime Button控件的标签后,将它们的OnClickListener设置为this

    date.setText(dateFormat.format(reservationDate));
    time.setText(timeFormat.format(reservationDate));
    date.setOnClickListener(this);
    time.setOnClickListener(this);
    
    
  11. 实现onClick方法,以监听用户点击datetime Button的操作:

    public void onClick(View view) {
    
  12. 使用View参数确定点击的View是否是date Button

    if(view == date) {
    
  13. 如果是,使用parseCalendar方法解析date Button控件的标签当前值:

    Calendar calendar = parseCalendar(date.getText(), dateFormat);
    
  14. 创建一个DatePickerDialog并用Calendar中的日期填充它,然后显示()``DatePickerDialog

    new DatePickerDialog(
            this, // pass ReviewActivity as the current Context
            this, // pass ReviewActivity as an OnDateSetListener
            calendar.get(Calendar.YEAR),
            calendar.get(Calendar.MONTH),
            calendar.get(Calendar.DAY_OF_MONTH)).show();
    
  15. 现在检查用户是否点击了View Button而不是date

    else if(view == time) {
    
  16. 如果是,使用time Button控件的标签值解析一个Calendar

    Calendar calendar = parseCalendar(time.getText(), timeFormat);
    
  17. 现在创建一个以选定时间为准的TimePickerDialog,然后向用户显示()新的TimePickerDialog

    new TimePickerDialog(
            this, // pass ReviewActivity as the current Context
            this, // pass ReviewActivity as an OnTimeSetListener
            calendar.get(Calendar.HOUR_OF_DAY),
            calendar.get(Calendar.MINUTE),
            false) // we want an AM / PM view; true = a 24hour view
            .show();
    
  18. 现在实现onDateSet方法,以监听用户在选择新日期后接受DatePickerDialog的操作:

    public void onDateSet(
            DatePicker picker, int year, int month, int day)
    
  19. 创建一个新的Calendar实例来填充日期:

    Calendar calendar = Calendar.getInstance();
    
  20. Calendar上设置年、月和日:

    calendar.set(Calendar.YEAR, year);
    calendar.set(Calendar.MONTH, month);
    calendar.set(Calendar.DAY_OF_MONTH, day);
    
  21. date Button的标签设置为格式化的Calendar

    date.setText(dateFormat.format(calendar.getTime()));
    
  22. 实现onTimeSet方法,以监听用户在选择新时间后接受TimePickerDialog的操作:

    public void onTimeSet(TimePicker picker, int hour, int minute)
    
  23. 创建一个新的Calendar实例:

    Calendar calendar = Calendar.getInstance();
    
  24. 根据TimePickerDialog给出的参数设置Calendar对象的hourminute字段:

    calendar.set(Calendar.HOUR_OF_DAY, hour);
    calendar.set(Calendar.MINUTE, minute);
    
  25. 通过格式化Calendar对象来设置time Button的标签:

    time.setText(timeFormat.format(calendar.getTime()));
    

存储了datetime对象的格式后,我们现在可以在Button控件中显示用户选择的值。当用户选择新的日期或时间时,我们更新Button标签以反映新的选择。

刚才发生了什么

如果你是在模拟器中安装并运行应用程序,现在你可以点击datetime Button组件,你会看到一个模态Dialog,允许你选择一个新值。注意不要过度使用模态Dialog组件,因为它们会阻止访问应用程序的其他部分。你不应该使用它们来显示状态消息,因为它们在显示期间实际上会使应用程序的其他部分变得无用。如果你确实显示了模态Dialog,请确保用户有某种方式可以不进行任何其他交互就关闭Dialog(即一个取消按钮或类似的东西)。

使用DatePickerDialogTimePickerDialog的第一个优点在于,两者都包含设置取消按钮。这让用户可以操作DatePickerTimePicker,然后取消更改。如果你使用内联的DatePickerTimePicker组件,你可以提供一个重置按钮,但这会占用额外的屏幕空间,并且通常看起来不合适(直到实际需要它)。

DatePickerDialogDatePicker组件相比的另一个优点是,DatePickerDialog在其标题区域以长格式显示选定的日期。这种长格式的日期通常包括用户当前选择的星期几。从DatePicker组件中明显缺失的“星期几”字段,使得它出人意料地难以使用。大多数人会想到“下个星期四”,而不是“2010 年 8 月 2 日”。让星期几可见使得DatePickerDialog比内联的DatePicker更适合日期选择。

发生了什么

使用 Include、Merge 和 ViewStubs 创建复杂布局

在本章中,我们构建了一个包含三个不同标签的单个布局资源。因此,main.xml文件变得相当大,因此更难以管理。Android 提供了几种方法,你可以用这些方法将大布局文件(如这个)分解成更小的部分。

使用 Include 标签

include标签是最简单的操作标签。它直接将一个布局 XML 文件导入另一个。对于我们之前的示例,我们可以将每个标签分离到它自己的布局资源文件中,然后在main.xmlinclude每个文件。include标签只有一个必填属性:layout。这个属性指向要包含的布局资源。这个标签不是静态或编译时的标签,因此包含的布局文件将通过标准的资源选择过程来选择。这允许你有一个单一的main.xml文件,但随后可以添加一个特殊的reviews.xml文件(可能是西班牙语的)。

include标签上的layout属性带有android XML 命名空间前缀。如果你尝试将layout属性用为android:layout,你不会得到编译时错误,但你的应用程序将奇怪地无法运行。

include元素还可以用来分配或覆盖所包含根元素的多个属性。这些属性包括元素android:id以及任何android:layout属性。这允许你在应用程序的多个部分重用同一个布局文件,但具有不同的布局属性和不同的 ID。你甚至可以在同一屏幕上多次include同一个布局文件,但每个实例都有一个不同的 ID。如果我们更改main.xml文件以包含来自其他布局资源的每个标签,文件看起来会更像这样:

<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout 
             android:layout_width="fill_parent"
             android:layout_height="fill_parent">

    <include
        android:id="@+id/review"
        layout="@layout/review"/>

    <include
        android:id="@+id/photos"
        layout="@layout/photos"/>

    <includeandroid:id="@+id/reservation"
        layout="@layout/reservations"/>
</FrameLayout>

合并布局

当你想要将单个ViewViewGroup包含到更大的布局结构中时,include元素是非常好用的。但是,如果你想在不暗示所包含结构中需要根元素的情况下,将多个元素包含到更大的布局结构中呢?在我们的示例中,每个标签都需要一个单一的根View,以便每个标签携带单一且唯一的 ID 引用。

然而,仅仅为了include而增加一个额外的ViewGroup可能会对大型布局树的性能产生不利影响。在这种情况下,merge标签可以提供帮助。你可以将布局的根元素声明为<merge>,而不是声明为ViewGroup。在这种情况下,所包含布局 XML 中的每个View对象都会成为包含它们的ViewGroup的直接子项。例如,如果你有一个名为main.xml的布局资源文件,其中包含一个LinearLayout,该LinearLayout又包含了user_editor.xml布局资源,那么代码看起来会像这样:

<LinearLayout android:orientation="vertical">
 <include layout="@layout/user_editor"/>
    <Button android:id="@+id/save"
            android:text="Save User"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
</LinearLayout>

user_editor.xml的简单实现看起来像这样:

<LinearLayout

    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content">

    <TextView android:text="User Name:"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>

    <EditText android:id="@+id/user_name"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>

    <!-- the rest of the editor -->
</LinearLayout>

然而,当这个被包含进main.xml文件时,我们将user_editor.xmlLinearLayout嵌入到main.xmlLinearLayout中,导致有两个具有相同布局属性的LinearLayout对象。显然,直接将user_editor.xml中的TextViewEditView放入main.xmlLinearLayout元素中会更好。这正是<merge>标签的用途。如果我们现在使用<merge>标签而不是LinearLayout来重写user_editor.xml文件,它看起来会像这样:

<merge >
    <TextView android:text="User Name:"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>

    <EditText android:id="@+id/user_name"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>

    <!-- the rest of the editor -->
</merge>

注意我们不再有LinearLayout元素,取而代之的是TextViewEditView将直接添加到main.xml文件中的LinearLayout。要小心那些嵌套了过多ViewGroup对象的布局,因为它们几乎肯定会引起问题(超过大约十级嵌套很可能会导致你的应用程序崩溃!)。同时也要注意那些含有过多View对象的布局。同样,超过 30 个很可能会引起问题或使你的应用程序崩溃。

使用 ViewStub 类

当你加载包含另一个布局的布局资源时,资源加载器会立即将包含的布局加载到内存中,以便将其附加到你请求的布局中。当 main.xmlLayoutInflator 读取时,reviews.xmlphotos.xmlreservations.xml 文件也会被读取。在具有非常大型布局结构的情况下,这可能会消耗大量的应用程序内存,甚至可能导致应用程序崩溃。Android API 包含一个名为 ViewStub 的专用 View,它允许延迟加载布局资源。

默认情况下,ViewStub 是一个零大小(0x0)的空 View,当调用其专门的 inflate() 方法时,它会加载布局资源并替换为加载的 View 对象。这个过程允许一旦调用了 inflate() 方法,ViewStub 就可以被垃圾回收。

如果在我们的示例中使用 ViewStub,那么当用户选择一个标签页时,你需要延迟初始化该标签页的内容。这也意味着,在标签页被选中之前,该标签页中的任何 View 对象都不存在。虽然使用 ViewStub 比直接使用 include 要多做一些工作,但它可以让你处理比其他情况下更大的、更复杂的布局结构。

ViewStub 上设置的任何布局属性都将传递给其展开的 View 对象。你也可以为展开的布局分配一个单独的 ID。如果我们想在每个标签页中使用 ViewStub,那么 main.xml 文件看起来会像这样:

<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout

         android:layout_width="fill_parent"
         android:layout_height="fill_parent">

 <ViewStub android:id="@+id/review"
 android:inflatedId="@+id/inflated_review"
 android:layout="@layout/review"/>

 <ViewStub android:id="@+id/photos"
 android:inflatedId="@+id/inflated_photos"
 android:layout="@layout/photos"/>

 <ViewStub android:id="@+id/reservations"
 android:inflatedId="@+id/inflated_reservations"
 android:layout="@layout/reservations"/>
</FrameLayout>

注意,与 include 标签不同,ViewStub 需要使用 android XML 命名空间为其 layout 属性。当你对一个 ViewStub 对象执行 inflate() 操作后,它将不再可以通过原来的 android:id 引用访问。相反,你可以使用 android:inflatedId 引用来访问被展开的布局对象。

实战英雄——分离标签页

将每个标签页提取到自己的布局资源文件中,并使用 include 标签加载它们。这不需要对 Java 源代码进行任何更改。

为了更具挑战性,尝试使用 ViewStub 对象代替 include 标签。这将要求你分解 onCreate 方法,并监听标签页被点击的时候。为此,你需要使用 TabHost.OnTabChangeListener 来知道何时加载特定标签页的内容。

摘要

标签页是将 Activity 分割成不同工作区域的好方法。在屏幕空间有限的情况下,它们是使 Activity 对用户更具可访问性的好方法。由于一次只渲染一个标签页,它们也具有性能影响。

RatingBarSeekBar 是两种不同的捕获或向用户显示数值数据的方法。尽管它们密切相关,并且功能方式相同,但每个类用于处理不同类型的数据。在决定是否以及在哪里使用它们之前,要考虑到这两个类的局限性。

Gallery 类非常出色,允许用户查看大量不同的对象。尽管在这个例子中我们仅用它来显示缩略图,但它可以用作网页浏览器中标签的替代品,通过在浏览器视图上方显示页面缩略图列表。要自定义其功能,你所需要做的就是更改从 Adapter 实现中返回的 View 对象。

当涉及到日期和时间捕获时,尽量坚持使用 DatePickerDialogTimePickerDialog,而不是它们内联的对应物(除非你有充分的理由)。使用这些 Dialog 小部件可以帮助你节省屏幕空间并提升用户体验。当他们打开 DatePickerDialogTimePickerDialog 时,他们可以比你在用户界面中通常提供的编辑器更好地访问编辑器(特别是在屏幕较小的设备上)。

在下一章中,我们将更详细地了解 Intent 对象、活动堆栈以及 Android 应用程序的生命周期。我们将研究如何使用 Intent 对象和活动堆栈作为一种使应用程序更具可用性的方法。同时,我们也将学习如何提高 Activity 类的重用性。

第四章:利用活动和意图

在许多方面,Android 应用程序管理似乎受到 JavaScript 和网页浏览器的启发,这是有道理的!网页浏览器模型已经证明它是一个用户容易操作的机制。作为一个系统,Android 与网页浏览器有许多共同之处,其中一些是显而易见的,其他的则需要你更深入地了解。

活动堆栈与单向的网页浏览器历史类似。当你使用startActivity方法启动一个Activity时,实际上是将控制权交还给了 Android 系统。当用户在手机上按下硬件“返回”按钮时,默认操作是从堆栈中弹出顶部Activity,并显示下面的一个(不总是启动它的那个)。

在本章中,我们将探讨 Android 如何运行应用程序以及如何管理Activity实例。虽然这对于用户界面设计并非绝对必要,但了解其工作原理很重要。正确利用这些概念将帮助你确保用户界面体验的一致性。正如你将看到的,它还有助于提高应用程序的性能,并允许你重用更多的应用程序组件。

理解Activity是如何创建的(以及它何时被创建),以及 Android 如何决定创建哪个Activity也同样重要。我们还将讨论在构建Activity类时应遵循的一些良好实践,以及如何在 Android 应用程序的范围内良好地表现。

我们已经在第一章和第二章中遇到了“活动堆栈”,在那里我们构建了Intent对象来启动特定的Activity类。当你使用硬件“返回”按钮时,你会自动被带到上一个Activity实例,无需编写任何代码(就像网页浏览器一样)。在本章中,我们将要了解:

  • Activity对象的生命周期

  • 使用Bundle类维护应用程序状态

  • 探索IntentActivity之间的关系

  • 通过IntentActivity传递数据

探索活动类

Activity对象的生命周期更像 Java Applet而不是普通应用程序。它可能会被启动、暂停、恢复、再次暂停、被杀死,然后以看似随机的顺序重新激活。大多数 Android 设备的性能规格非常好。然而,与顶级设备相比,它们中的大多数似乎性能不足。对于那些规格好的设备,用户往往比便宜设备要求更多。在手机上,你永远无法摆脱这样一个事实:许多应用程序和服务正在共享非常有限的设备资源。

如果Activity对用户不可见,它可能会随时被垃圾回收。这意味着虽然你的应用程序可能在运行,但由于用户正在查看另一个Activity,任何不可见或后台的Activity对象可能会被关闭或垃圾回收以节省内存。默认情况下,Android API 会通过在关闭前存储它们的状态并在重新创建时恢复状态,优雅地处理这些关闭/启动周期。下面是一个包含两个Activity实例的应用程序生命周期的非常简单的图示。当"主 Activity"暂停时,它就有可能被系统垃圾回收。如果发生这种情况,它首先会在一个临时位置存储其状态,当它被带回前台时会恢复状态。

探索 Activity 类

提示

用户界面状态的存储

如果一个Activity被停止,所有分配了 ID 的View对象在可供垃圾回收之前都会尝试存储它们的状态。然而,这种状态只会在应用程序的生命周期内存储。当应用程序关闭时,这个状态就会丢失。

尽管可以一次又一次地使用setContentView方法来改变屏幕上的内容(就像你可能使用 AWT 的CardLayout对象构建向导界面一样),但这被认为是一个非常糟糕的做法。你实际上是在试图从 Android 手中夺走控制权,这总会给你带来问题。例如,如果你开发了一个只有一个Activity类的应用程序,并使用多个布局资源或自己的自定义ViewGroup对象来表示不同的屏幕,你还必须控制设备上的硬件"返回"按钮,以允许用户后退。你的应用程序在 Android 市场上发布,几个月后,一个手机制造商决定在其新手机上添加一个"前进"按钮(类似于网页浏览器中的"前进"按钮)。Android 系统会被打补丁以处理这个设备变化,但你的应用程序不会。因此,你的用户会对你的应用程序感到沮丧,因为"它不能正常工作"。

使用 Bundle 对象

Activity类的onCreate方法中,我们一直在接收一个名为saveInstanceStateBundle参数,如您所猜测的那样。它是在Activity的停止和启动之间存储状态信息的地方。尽管看起来是这样,但Bundle对象并不是一种持久化存储形式。当设备上下文的配置发生变化(例如,当用户选择了一种新语言,或从“纵向”改为“横向”模式)时,当前的Activity会被“重新启动”。为此,Android 请求Activity将其状态保存在一个Bundle对象中。然后它会关闭并销毁现有实例,并使用保存状态信息的Bundle创建Activity的新实例(带有新的配置参数)。

Bundle类实际上是一个Map<String, ?>,包含任意数量的值。由于Bundle对象用于存储短期状态(即用户正在输入的博客文章),它们主要用于存储View对象的状态。在这方面,它们相对于标准的 Java 序列化有两个主要优点:

  • 您必须手动实现对象存储。这需要考虑如何存储对象以及需要存储它的哪些部分。例如,在用户界面中,大多数时候您不需要存储布局信息,因为可以从布局文件重新创建它。

  • 由于Bundle是一个键值结构,它比序列化对象更面向未来且灵活。您可以省略设置为默认值的值,从而减少Bundle的大小。

Bundle对象也是一个类型安全的结构。如果您使用putString方法,那么只有getStringgetCharSequence可以用来检索对象。我强烈建议在使用Bundleget方法时,您应该总是提供一个默认值。

在 Android 系统暂停Activity之前,系统会请求它将任何状态信息保存在一个Bundle对象中。为此,系统会在Activity上调用onSaveInstanceState方法。这发生在onPause方法之前。为了恢复Activity的状态,系统会使用保存的状态Bundle调用onCreate方法。

提示

处理 Activity 崩溃

如果Activity类抛出一个未捕获的异常,用户将看到可怕的强制关闭对话框。Android 将尝试通过终止虚拟机并重新打开根活动来从这些错误中恢复,并提供一个带有从onSaveInstanceState获取的最后已知状态的Bundle对象。

View类也有一个onSaveInstanceState方法,以及相应的onRestoreInstanceState方法。如前所述,Activity类的默认功能将尝试在Bundle中保存每个带有 ID 的View对象。这是坚持使用 XML 布局而不是自己构建布局的另一个好理由。拥有对View对象的引用还不足以保存和恢复它,虽然你可以在 Java 代码中分配 ID,但这会使你的用户界面代码更加混乱。

行动时间 - 构建一个示例游戏:“猜数字”

我们想要构建一个简单的示例,它将从一个Bundle对象保存和恢复其状态。在这个示例中,我们有一个非常简单的“猜数字”游戏。Activity对象在 1 到 10 之间选择一个数字,并挑战用户猜测它。

这个示例的基本用户界面布局需要有一个标签告诉用户要做什么,一个输入区域供他们输入猜测,以及一个按钮告诉应用他们想要输入猜测。以下图表是用户界面应该如何构建的基本思路:

行动时间 - 构建一个示例游戏:“猜数字”

如果用户在玩这个游戏时收到短信,我们很可能会丢失他试图猜测的数字。因此,当系统要求我们保存状态时,我们将尝试猜测的数字存储在Bundle对象中。启动时我们还需要查找存储的数字。

  1. 从命令提示符中,创建一个名为GuessMyNumber的新项目:

    android create project -n GuessMyNumber -p GuessMyNumber -k com.packtpub.guessmynumber -a GuessActivity -t 3
    
    
  2. 在编辑器或 IDE 中打开默认的res/layout/main.xml文件。

  3. 移除LinearLayout元素中的默认内容。

  4. 添加一个新的TextView作为标签,告诉用户要做什么:

    <TextView android:text=
        "I'm thinking of a number between 1 and 10\. Can you guess what it is?"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    
  5. 创建一个新的EditText,用户将在其中输入他们的猜测。使用TextViewandroid:numeric属性来强制只输入integer(整数):

    <EditText
        android:id="@+id/number"
        android:numeric="integer"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    
  6. 添加一个用户可以点击提交猜测的Button

    <Button android:id="@+id/guess"
        android:text="Guess!"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    
  7. 现在在编辑器或 IDE 中打开GuessActivity.java文件。

  8. GuessActivity类实现OnClickListener

    public class GuessActivity
        extends Activity implements OnClickListener {
    
  9. 创建一个字段变量以存储用户应该猜测的数字:

    private int number;
    
  10. 创建一个实用方法以生成 1 到 10 之间的随机数:

    private static int random() {
        return (int)(Math.random() * 9) + 1;
    }
    
  11. onCreate方法中,在调用super.onCreate之后,检查以确保传递进来的Bundle不为null

    if(savedInstanceState != null) {
    
  12. 如果Bundle不为null,尝试从其中获取存储的Number

    number = savedInstanceState.getInt("Number", random());
    
  13. 如果Bundlenull,则Activity作为新实例运行 - 生成一个随机数:

    else {
        number = random();
    }
    
  14. 然后将setContentView设置为main.xml布局资源:

    setContentView(R.layout.main);
    
  15. main.xml布局资源中找到你声明的Button对象:

    Button button = (Button)findViewById(R.id.guess);
    
  16. Button对象的OnClickListener设置为GuessActivity对象:

    button.setOnClickListener(this);
    
  17. 现在重写onSaveInstanceState方法:

    protected void onSaveInstanceState(Bundle outState) {
    
  18. 首先确保允许默认的Activity行为:

    super.onSaveInstanceState(outState);
    
  19. 然后将number变量存储在Bundle中:

    outState.putInt("Number", number);
    
  20. 我们需要重写onClick方法来处理用户的猜测:

    public void onClick(View clicked) {
    
  21. 找到用户输入猜测数字的EditText

    EditText input = (EditText)findViewById(R.id.number);
    
  22. EditText的当前值解析为整数:

    int value = Integer.parseInt(input.getText().toString());
    
  23. 如果他们猜的数字太低,使用Toast告诉他们:

    if(value < number) {
        Toast.makeText(this, "Too low", Toast.LENGTH_SHORT).show();
    }
    
  24. 如果他们猜的数字太高,再次使用Toast告诉他们:

    else if(value > number) {
        Toast.makeText(this, "Too high", Toast.LENGTH_SHORT).show();
    }
    
  25. 如果他们成功猜对了数字,那么祝贺他们:

    else {
        Toast.makeText(
                this,
                "You got it! Try guess another one!",
                Toast.LENGTH_SHORT).show();
    
  26. 然后为用户生成一个新的猜测数字:

        number = random();
    }
    

在之前的代码中使用了Toast类来显示太高太低猜对了!的输出信息。Toast类是显示简短输出信息的完美机制,几秒钟后它们会自动消失。然而,它们不适合长消息,因为用户无法控制它们,也不能按命令打开或关闭消息,因为它们完全是非交互式的。

刚才发生了什么

在上一个示例中,我们监听onSaveInstanceState的调用,以记录用户应该猜测的数字。我们还有用户最近一次做出的猜测,以EditText的形式。由于我们在main.xml文件中为EditText分配了一个 ID 值,调用super.onSaveInstanceState将处理EditText小部件的确切状态存储(可能包括“选择”和“焦点”状态)。

onCreate方法中,示例首先检查以确保Bundle不为null。如果 Android 试图创建GuessActivity对象的新实例,它不会传递任何保存的状态。然而,如果我们有一个Bundle对象,我们会调用Bundle.getInt方法尝试获取我们之前存储的number值。我们还传递一个r andom()数作为第二个参数。如果Bundle对象(无论什么原因)没有存储Number,它将返回这个随机数,这样就无需我们检查这种情况。

顺便一提,示例使用了TextView类的android:numeric属性,以强制EditText对象接受整数输入。切换到数字视图可以阻止用户输入除了“有效”字符以外的任何内容。它还会影响软键盘。它不会显示全键盘,只会显示数字和符号。

刚才发生了什么

创建和使用意图:

Intent类是 Android 主要的“晚期绑定”方式。这是一种非常松散的耦合形式,允许你指定一个动作(以及一些参数数据),但不需要指定如何执行该动作。例如,你可以使用Intent指定浏览到www.packtpub.com/,但不需要指定 Android 如何执行此操作。它可能使用默认的“浏览器”应用,或者用户安装的其他网页浏览器,甚至可能询问用户他们确切想要如何访问www.packtpub.com/。有两种主要的Intent类型:

  • 显式 Intents

  • 隐式 Intents

到目前为止,我们只使用了显式Intent对象,我们指定了想要运行的的确切类。当从一个Activity切换到另一个时,这些非常重要,因为应用程序可能依赖于Activity的确切实现。隐式Intent是当我们不指定想要操作的确切类时,而是包含我们希望执行操作的抽象名称。通常,隐式Intent会包含更多信息,由于以下原因:

  • 为了让系统在选择与哪个组件交互时做出最佳选择。

  • Intent可能指向一个比我们自行构建的更通用的结构,而一个更通用的结构通常需要更多信息来明确其预期行为。

Intent对象是真正让 Android 与其他(更传统的)操作系统不同的地方。它们平衡了应用程序之间的竞争环境,并让用户在使用手机时有更多的选择。用户不仅可以安装一个新的网页浏览器,还可以安装新的菜单、桌面甚至拨号应用。

每个Activity实例都保存着启动它的Intent对象。第一章中,我们通过开发一个简单的活动用到了Activity.getIntent()方法,从Intent对象中获取一些参数,这些参数告诉我们应该向用户提出哪个问题。

定义 Intent 动作

在隐式Intent中首先要看的是它的动作。动作定义了Intent“做什么”,但不是“怎么做”或“对什么做”。Intent类定义了一系列常量,代表常见动作。这些常见动作总是有某种形式的支撑逻辑,通常由电话系统定义。因此,它们总是可供应用程序使用。

例如,如果你想向用户展示拨号应用,使他们可以拨打电话号码并进行通话,你会使用带有ACTION_DIALIntent

startIntent(new Intent(Intent.ACTION_DIAL));

Intent的动作值与Activity定义的一个动作匹配。一个Activity可能有多个它可以执行的动作,它们都作为应用程序AndroidManifest.xml文件的一部分被指定。例如,如果你想定义一个askQuestion动作并将其绑定到一个Activity,你的AndroidManifest.xml文件将包含一个Activity条目,看起来像这样:

<activity
    android:name=".AskQuestionActivity"
    android:label="Ask Question">

    <intent-filter>
        <action android:name="questions.askQuestion"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>

一个Activity可以有多个<intent-filter>元素,每个元素定义了一种不同类型的匹配要在Intent上执行。与任何给定的Intent最接近匹配的Activity被选中来执行Intent对象请求的动作。

在 Intent 中传递数据。

向用户展示拨号器应用程序,让他们拨打一个电话号码非常好,但如果实际上我们需要他们拨打一个电话号码呢?Intent类不仅仅通过使用动作来工作,它还为我们提供了一个默认的空间,告诉我们想要动作执行的对象。如果我们不能告诉浏览器要访问哪个 URL,那么打开网页浏览器不是非常有用,对吧?

Intent提供的默认数据作为一个Uri对象。Uri在技术上可以指向任何东西。对于我们之前的代码片段,我们启动了拨号器,让用户拨打一个电话号码。那么我们如何告诉拨号器:“拨打 555-1234”呢?很简单,看看以下代码:

startActivity(new Intent(
        Intent.ACTION_DIAL,
        Uri.parse("tel://5551234")));

向 Intent 添加额外数据

有时Uri不允许指定足够的数据。对于这些情况,Intent类为你提供了一个键值对的Map空间,称为"额外"数据。"额外"数据的访问方法与Bundle类中的方法相对应。在第一章《开发简单活动》中,我们使用了额外数据来跟踪我们向用户提出的问题。

在定义通用的Activity类(如文件查看器)时,查找操作数据时建立一个三阶段回退系统是一个好主意:

  • 任何自定义(非标准)参数都可以在额外字段中传递(而且它们都不应该是强制性的)。

  • 检查数据Uri以了解你应该处理哪些信息。

  • 如果没有指定数据Uri,优雅地回退到逻辑默认值,并为用户提供一些功能。

动手实践英雄——通用问题与答案

回顾一下第一章《开发简单活动》中的示例问题与答案应用程序。重写QuestionActivity类,使用数据Uri来指定问题 ID(通过名称),而不是额外的参数。

同时,允许使用"额外"参数传递完整问题——一个参数Question用于要问用户的问题文本,以及一个参数Answers,指定给定问题的可能答案的字符串数组。

使用高级 Intent 功能

Intent对象旨在指示用户请求的单个动作。它是一个自包含的请求,在某些方面与 HTTP 请求非常相似,既包含要执行的动作,也包含要执行动作的资源,以及可能需要的相关信息。

为了找到将处理IntentActivity(服务或广播接收器),系统使用了意图过滤器(我们之前简要讨论过)。每个意图过滤器指示了一个Activity可能执行的单个动作类型。当两个或更多的Activity实现匹配一个Intent时,系统会发送一个ACTION_PICK_ACTIVITY Intent,以允许用户(或某些自动化系统)选择哪个Activity实现应该用来处理Intent。默认行为是询问用户他们希望使用哪个Activity实现。

从 Intent 获取数据

Intent并不总是单向的结构,某些Intent动作会提供反馈。一个很好的例子就是Intent.ACTION_PICKIntent.ACTION_PICK动作是请求用户“挑选”或选择某种数据形式的方式(一个常见的用法是请求用户从他们的联系人列表中选择一个人或电话号码)。

当你需要从Intent获取信息时,应使用startActivityForResult方法,而不是普通的startActivity方法。startActivityForResult方法接受两个参数:要执行的Intent对象和一个有用的int值,该值将被传回给你。

如前所述,当另一个Activity可见而不是你的时,你的Activity会被暂停,甚至可能被停止并垃圾回收。因此,startActivityForResult方法会立即返回,并且通常可以假设在你从当前事件返回后(将控制权交还给系统),你的Activity将直接被暂停。

为了获取你触发的Intent中的信息,你需要重写onActivityResult方法。每次使用startActivityForResult启动的Intent返回数据时,都会调用onActivityResult方法。传回onActivityResult方法的第一参数是你传给startActivityForResult方法的相同整数值(允许你传回简单的参数)。

提示

向另一个 Activity 传递信息

如果你打算让一个Activity实现将信息传回给调用者,你可以使用Activity.setResult方法来传递一个结果码和带有你响应数据的Intent对象。

快速测验

  1. onCreate何时会接收到一个有效的Bundle对象?

    1. 每次创建Activity

    2. 当应用程序在之前的执行中在Bundle中存储了信息时

    3. 当由于配置更改或崩溃而重新启动 Activity 时

  2. onSaveInstanceState 方法何时被调用?

    1. onStop 方法之后

    2. onPause 方法之前

    3. Activity 正在被重新启动时

    4. onDestroy 方法之前

  3. Bundle 对象将被存储直到:

    1. 应用程序已关闭

    2. Activity 不再可见

    3. 应用已被卸载

动手时间——查看电话簿联系人

在这个例子中,我们将更深入地探讨 Android 系统的运作方式。我们将覆盖默认的“查看联系人”选项,提供我们自己的 Activity 来查看设备上电话簿中的联系人。当用户尝试打开一个联系人以发送电子邮件或拨打电话时,他们将有机会使用我们的 Activity 而不是默认的来查看联系人。

  1. 从命令行开始创建一个新项目:

    android create project -n ContactViewer -p ContactViewer -k com.packtpub.contactviewer -a ViewContactActivity -t 3
    
  2. 在编辑器或 IDE 中打开 res/layout/main.xml 布局资源。

  3. 移除 LinearLayout 元素中的默认内容。

  4. 添加一个新的 TextView 对象以包含联系人的显示名称:

    <TextView android:id="@+id/display_name"
              android:textSize="23sp"
              android:textStyle="bold"
              android:gravity="center"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  5. 然后添加一个 Button,该按钮将用于“拨打”显示联系人的默认电话号码:

    <Button android:id="@+id/phone_number"
            android:layout_marginTop="5sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
  6. 在编辑器或 IDE 中打开 ViewContactActivity.java 源文件。

  7. ViewContactActivity 实现 OnClickListener

    public class ViewContactActivity
            extends Activity implements OnClickListener {
    
  8. onCreate 方法中的 setContentView(R.layout.main) 之后,找到你创建的 TextView 对象,以显示联系人的名称:

    TextView name = (TextView)findViewById(R.id.display_name);
    
  9. 然后找到用于显示电话号码的 Button 控件:

    Button number = (Button)findViewById(R.id.phone_number);
    
  10. 现在,使用 Activity.managedQuery 方法查询联系人数据库,获取我们的 Intent 中指定的 data Uri

    Cursor c = managedQuery(
            getIntent().getData(),
            new String[]{
                People.NAME,
                People.NUMBER
            },
            null,
            null,
            null);
    
  11. try {} finally{} 代码块中,告诉 Cursor 执行 moveToNext() 并确保其这样做(这与 ResultSet.next() 的作用完全相同):

    if(c.moveToNext()) {
    
  12. Cursor 中获取并显示联系人显示名称:

    name.setText(c.getString(0));
    
  13. Cursor 中获取并显示联系人的默认电话号码:

    number.setText(c.getString(1));
    
  14. finally{} 代码块中,关闭 Cursor

    finally {
        c.close();
    }
    
  15. 现在,将 number ButtonOnClickListener 设置为 this

    number.setOnClickListener(this);
    
  16. 重写 onClick 方法:

    public void onClick(View clicked) {
    
  17. 我们知道点击的是 number Button(此时唯一带有事件监听器的 View)。将 View 参数转换为 Button,这样我们就可以使用它了:

    Button btn = (Button)clicked;
    
  18. 创建一个 Intent 对象以拨打选定的电话号码:

    Intent intent = new Intent(
            Intent.ACTION_DIAL,
            Uri.parse("tel://" + btn.getText()));
    
  19. 使用 startActivity 打开拨号器应用:

    startActivity(intent);
    
  20. 现在,在编辑器或 IDE 中打开 AndroidManifest.xml 文件。

  21. <application> 元素声明之前,我们需要读取联系人列表的权限:

    <uses-permission
         android:name="android.permission.READ_CONTACTS" />
    
  22. ViewContactActivity 的标签更改为 查看联系人

    <activity
        android:name=".ViewContactActivity"
        android:label="View Contact">
    
  23. 移除 <intent-filter> 元素内的所有默认内容。

  24. 为此 <intent-filter> 声明一个类型为 ACTION_VIEW<action>

    <action android:name="android.intent.action.VIEW"/>
    
  25. 将此 <intent-filter><catagory> 设置为 CATAGORY_DEFAULT

    <category android:name="android.intent.category.DEFAULT"/>
    
  26. 添加一个 <data> 元素以筛选 person 条目(这是一个 MIME 类型):

    <dataandroid:mimeType="vnd.android.cursor.item/person"
        android:host="contacts" />
    
  27. 添加另一个 <data> 元素以筛选 contact 条目:

    <data android:mimeType="vnd.android.cursor.item/contact"
          android:host="com.android.contacts" />
    

当在设备上安装时,前面的代码将成为用户打开通讯录中“联系人”的一个选项。正如你所见,替换 Android 标准框架的一部分非常简单,它允许应用程序与基础系统进行更加无缝的集成,这是更传统的应用程序架构所无法实现的。

刚刚发生了什么

如果你在这个模拟器上安装这个应用程序,你会注意到在启动器中没有图标来启动它。这是因为这个应用程序不像我们迄今为止编写的所有其他应用程序那样有一个主要的入口点。相反,如果你打开“联系人”应用程序,然后点击通讯录中的一个联系人,你会看到以下屏幕:

刚刚发生了什么

如果你选择第二个图标,你新的ViewContactActivity将被启动以查看选定的联系人。正如你所见,用户也有能力在默认情况下使用你的应用程序(只要你的应用程序在设备上可用)。

在开发新应用程序时,覆盖默认行为是一个非常重要的决定。Android 使得这变得非常简单,正如你所见,第三方应用程序可以几乎无缝地插入两个默认应用程序之间。在正常的操作系统环境中,你需要编写一个完整的“联系人管理器”,而在 Android 中,你只需要编写你感兴趣的部分。

这是你用户界面设计的一部分,因为它可以用来扩展系统的各种默认部分的 功能。例如,如果你编写了一个聊天应用程序,比如一个“Jabber”客户端,你可以将客户端嵌入到用户通讯录中每个与 Jabber ID 关联的联系人查看联系人Activity中。这将使用户可以直接从他们的通讯录与可用的联系人聊天,而无需打开你的应用程序。你的应用程序成为他们检查联系人状态的方式,甚至可能完全避免打电话。

总结

在正确的粒度上实现Activity是你用户界面设计过程的一个重要部分。尽管它不是直接与图形相关的一部分,但它定义了系统如何与你的应用程序交互,从而也定义了用户如何与它交互。

在构建Activity启动方式时,考虑到隐式意图是一个好主意。创建一个通用的Activity可以让其他应用程序与你自己的程序无缝集成,从而有效地将你的新应用程序转变为其他开发人员工作的平台。通过隐式方式启动的Activity可以被另一个应用程序替换或扩展,也可以在其他应用程序中被复用。在这两种情况下,用户可以像定制壁纸图像或主题一样自由地定制你的应用程序。

一定要尝试为用户可能要采取的每个动作提供一个单独的Activity实现,不要让一个Activity在同一个屏幕上做太多事情。一个很好的粒度例子就是“联系人”应用——它包含了联系人列表、联系人查看器、联系人编辑器和拨号应用。

当处理标签界面(正如我们在上一章所做的)时,可以将标签内容指定为Intent,实际上是将Activity嵌入到你的应用中。我强烈建议你在构建标签用户界面时考虑这样做,因为它可以让每个标签更容易地被你的应用重复使用,同时也允许第三方开发者一次创建一个标签来扩展你的界面。

迄今为止,我们主要使用了LinearLayout类,虽然这对于简单的用户界面来说是一个很好的基础,但几乎永远不够用。在下一章中,我们将探讨 Android 默认提供的许多其他类型的布局,研究每种布局的工作方式以及如何使用它们。

第五章:开发非线性布局

非线性布局通常是完全的用户界面设计的基础课题。然而,在屏幕较小的设备上(许多 Android 设备都是如此),这样做并不总是合理的。也就是说,Android 设备可以切换到横屏模式,突然之间你就有大量的水平空间,而垂直空间有限。在这些情况下(以及我们将要看到的其他许多情况下),你会想要使用除了我们至今为止使用的普通LinearLayout结构之外的布局。

Android 布局的真正强大之处与旧的 Java AWT LayoutManagers的强大之处相同——通过将不同的布局类相互组合。例如,将FrameLayout与其他ViewGroup实现相结合,允许你将用户界面的各个部分层层叠加。

考虑你的布局在不同大小的屏幕上的表现是很重要的。虽然 Android 允许你根据设备屏幕大小选择不同的布局,但这意味着你将不得不为不同的屏幕大小和密度维护多个布局,这些屏幕大小和密度将在野外遇到你的应用程序。尽可能使用 Android 提供的工具,并使用会根据各种View对象的大小进行缩放的布局。

在本章中,我们将探讨 Android 默认提供的各种其他布局样式,并研究每种布局的各种替代用途。我们还会更详细地了解如何为不同的布局指定参数,以及它们如何帮助提高可用性,而不仅仅是将你的小部件按特定顺序排列。

是时候行动了——创建一个布局示例项目

在我们逐一了解每种布局之前,我们需要一个公共项目,在其中展示每一种布局。

  1. 从命令提示符中,创建一个名为Layouts的新项目:

    android create project -n Layouts -p Layouts -k com.packtpub.layouts -a LayoutSelectorActivity -t 3
    
  2. 删除标准的res/layout/main.xml布局资源文件。

  3. 在编辑器或 IDE 中打开res/values/strings.xml文件。

  4. 在文件中添加一个名为layouts的新<string-array>

    <string-array name="layouts">
    
  5. 向新的<string-array>元素中添加以下项目:

    <item>Frame Layout</item>
    <item>Table Layout</item>
    <item>Custom Layout</item>
    <item>Relative Layout</item>
    <item>Sliding Drawer</item>
    
  6. 在你的编辑器或 IDE 中打开LayoutSelectorActivity源文件。

  7. 让类从ListActivity继承,而不是从Activity继承:

    public class LayoutSelectorActivity extends ListActivity {
    
  8. onCreate方法中,将你在strings.xml资源文件中声明的ListActivity的内容设置为你layouts数组:

    setListAdapter(new ArrayAdapter<String>(
            this,
            android.R.layout.simple_list_item_1, Have the class inherit from"
            getResources().getStringArray(R.array.layouts)));
    
  9. 重写onListItemClick方法:

    protected void onListItemClick(
            ListView l,
            View v,
            int position,
            long id) {
    
  10. position参数上创建一个switch语句:

    switch(position) {
    
  11. 添加一个default子句(目前唯一的一个),以让你知道你还没有为所选项目实现示例:

    default:
        Toast.makeText(
                this,
                "Example not yet implemented.",
                Toast.LENGTH_SHORT).show();
    

刚才发生了什么?

新项目将作为本章每个示例的基础。对于我们要探讨的每个布局,我们将构建一个新的Activity,这将成为这个应用程序的一部分。目前,该应用程序只包含一个菜单,用于访问每个布局示例。现在的想法是给每个示例填充一些有趣的内容。

在本章中,我们将不仅探讨基本布局,还会了解它们如何相互作用。

FrameLayout的使用

FrameLayout类将每个控件锚定在其自身的左上角。这意味着每个子控件都会在之前的控件上绘制。这可以通过使用View.setVisible来模拟 AWT 中的CardLayout,即显示一个子控件同时隐藏所有其他子控件(这正是TabHost的工作原理)。

由于FrameLayout实际上会绘制所有可见的子视图,因此可以用来将子控件层层叠加。在某些情况下,它会产生非常奇特的效果,而在其他情况下,它可能非常有用。例如,通过使用半透明的View对象和一个FrameLayout,可以实现除一个控件外所有控件变暗的效果。不活跃的控件是FrameLayout中的第一层,半透明的View对象是第二层,活跃的控件是第三层。

常见用途

FrameLayout最常见的用法可能是与TabHost结合使用——为每个标签页持有内容View对象。你也可以用它来模拟更像是桌面应用的感觉,通过将控件层层叠加。在游戏中也非常有效,可以用来显示游戏内的菜单,或者在游戏主菜单后面绘制动画背景。

通过将FrameLayout对象与占据整个屏幕的控件结合使用,可以利用gravity属性将对象更精确地放置在其他控件之上。为此,通常希望每个FrameLayout的子控件都是某种ViewGroup,因为除非特别指定,否则它们通常不会在背景中绘制(让下面的图层保持可见)。

FrameLayout还能够显示前景。虽然所有View对象都有背景属性,但FrameLayout包含一个前景(这也是一个可选的Drawable)。前景会在所有子控件之上绘制,允许显示一个“框架”。

动手实践时间——开发一个FrameLayout示例。

要真正理解FrameLayout的作用以及如何使用它,最好是通过一个示例来实践一下。在这个示例中,我们将使用FrameLayout将一些Button控件叠加在ImageView之上,并在点击其中一个按钮时显示和隐藏一个TextView消息。

为了使这个示例工作,你需要一张图片作为背景图。我将使用我朋友的一张照片。像往常一样,将你的图片放在res/drawable目录中,并尝试使用 PNG 文件。

  1. 创建一个名为res/layout/frame_layout.xml的新布局资源文件。

  2. 将根元素声明为占用所有可用空间的FrameLayout

    <FrameLayout
    
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  3. FrameLayout内部,创建一个ImageView作为背景图像。它应该缩放以填满所有可用空间:

    <ImageView android:src="img/jaipal"
               android:scaleType="centerCrop"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
    
  4. 现在创建一个垂直的LinearLayout,我们将在屏幕底部放置两个Button对象:

    <LinearLayout android:orientation="vertical"
                  android:gravity="bottom"
                  android:layout_width="fill_parent"
                  android:layout_height="fill_parent">
    
  5. 创建一个Button,我们将使用它来切换FrameLayout的一个子层(创建类似对话框的效果):

    <Button android:text="Display Overlay"
            android:id="@+id/overlay_button"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
  6. 创建另一个Button以退出演示并返回菜单:

    <Button android:text="Quit"
            android:id="@+id/quit"
            android:layout_marginTop="10sp"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
    
  7. </LinearLayout>之后,创建一个最终的TextView元素,我们将在点击第一个按钮时显示和隐藏它。默认情况下它是隐藏的:

    <TextView android:visibility="gone"
              android:id="@+id/overlay"
              android:textSize="18sp"
              android:textStyle="bold"
              android:textColor="#ffff843c"
              android:text="This is a text overlay."
              android:gravity="center|center_vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"/>
    
  8. 在项目的根包中创建一个新的FrameLayoutActivity Java 类,并在编辑器或 IDE 中打开源文件。新类需要从Activity继承并实现OnClickListener类(用于那两个Button小部件的事件):

    public class FrameLayoutActivity
            extends Activity implements OnClickListener {
    
  9. 重写onCreate方法:

    protected void onCreate(Bundle savedInstanceState) {
    
  10. 调用super.onCreate方法以使Activity代码工作:

    super.onCreate(savedInstanceState);
    
  11. 将内容布局设置为刚才创建的frame_layout资源:

    setContentView(R.layout.frame_layout);
    
  12. frame_layout资源文件中找到你声明的overlay Button小部件并创建一个引用:

    Button overlay = (Button)findViewById(R.id.overlay_button);
    
  13. 将其OnClickListener设置为新的FrameLayoutActivity对象:

    overlay.setOnClickListener(this);
    
  14. 查找你声明的quit Button小部件:

    Button quit = (Button)findViewById(R.id.quit);
    
  15. 然后将它的OnClickListener设置为FrameLayoutActivity对象:

    quit.setOnClickListener(this);
    
  16. OnClickListener接口要求我们实现一个具有以下签名的onClick方法:

    public void onClick(View view) {
    
  17. View参数的 ID 创建一个switch语句:

    switch(view.getId()) {
    
  18. 如果用户点击的Viewoverlay_button Button,则使用以下代码:

    case R.id.overlay_button:
    
  19. 从布局中获取overlay View对象:

    View display = findViewById(R.id.overlay);
    
  20. 根据当前状态切换其可见性,然后从switch语句中break

    display.setVisibility(
            display.getVisibility() != View.VISIBLE
            ? View.VISIBLE
            : View.GONE);
    break;
    
  21. 如果用户点击的Viewquit Button,则使用以下代码:

    case R.id.quit:
    
  22. 调用finish()方法,并从switch语句中break

    finish();
    break;
    
  23. 在编辑器或 IDE 中打开LayoutSelectorActivity Java 源文件。

  24. onListItemClick方法中,为switch语句创建一个新的case,用于position值为0的情况:

    case 0:
    
  25. 使用显式Intent启动FrameLayoutActivity

    startActivity(new Intent(this, FrameLayoutActivity.class));
    break;
    
  26. 在编辑器或 IDE 中打开AndroidManifest.xml文件。

  27. 将新的FrameLayoutActivity添加到清单文件中:

    <activity android:name=".FrameLayoutActivity"
              android:label="Frame Layout Example"/>
    

刚才发生了什么?

新的FrameLayoutActivity使用了一个简单三层FrameLayout。我们使用ImageView对象绘制一个漂亮的背景图像,在其上放置了两个按钮。尽管第三层(TextView小部件)在顶部按钮被点击之前是不可见的,但需要注意的是,顶部TextView的背景不仅透明,而且还将点击事件委托给技术上位于其下的控件(TextView有一个消耗整个FrameLayout的控件和高度)。即使TextView的背景是不透明的,这也会继续工作。这更多是因为TextView不是“可点击”的。如果你为overlay TextView对象添加了一个OnClickListener,那么它下面的按钮将停止工作。这意味着你需要小心如何在FrameLayout中分层控件(尽管只要一个控件不占用另一个控件的空间,这对你来说不会成为问题)。

在这个例子中,我们在布局中添加了一个退出按钮,并在点击Button时使用finish()方法关闭Activity。你会发现你通常不会直接使用finish()方法,因为用户通常会继续向前浏览你的应用程序。如果用户想要返回,他们通常会使用硬件“返回”按钮,或者按下硬件“主页”按钮完全退出你的应用程序。

关于上述示例的最后说明——在frame_layout.xml文件中,我们将overlay声明为一个TextView小部件。然而,在 Java 代码中,我们使用View类而不是TextView来访问它。这是一个简单的解耦例子。除非你正在处理一个以性能为中心的代码段,否则最好尽可能地将你的布局小部件引用到类树的高层。这样,你就可以在以后更快地修改用户界面。在这种情况下,你可以将简单的TextView更改为整个LinearLayout,而无需更改 Java 代码。

下面是FrameLayout示例的两张屏幕截图,分别是有和没有启用overlay TextView的情况。这种布局非常适合用于游戏菜单或类似结构中,在这些地方你需要将不同的控件层层叠加在一起。

发生了什么?

表格布局

Table Layout以 HTML 风格的网格排列其子项。它有点像 AWT 的Grid Layout类,但灵活性要大得多。与 Android 中的大多数其他布局类不同,Table Layout使用自己的专用直接子View类,名为Table RowTable Layout类也不允许你定义行数或列数(使其更像一个 HTML 的<table>元素)。相反,行数和列数是由Table Layout及其Table Row子项中的控件数量计算得出的。

Table Layout中的单元格可以占用任意数量的行和列,尽管默认情况下,放在Table Row中的View会正好占据一个单元格。但是,如果你直接将View作为Table Layout的子项,它将占用整行。

Table Layout也是一个相对布局结构,这在处理 Android 设备时至关重要。能够基于网格线对齐所有内容,使得用户界面可以从低分辨率的小手机扩展到 7 英寸平板电脑上的高密度屏幕。

android:gravity属性在Table Layout中的使用远比其他布局类更为频繁。在小屏幕上看起来很棒的效果在大屏幕上可能会完全不同,这并不是因为屏幕的大小,而是因为所使用字体的缩放。特别是在标签和控件的垂直对齐上要小心。最简单的方法是先将所有表格控件垂直居中,然后在此基础上进行调整。务必在多种屏幕分辨率和尺寸上测试基于表格的布局。

常见用途

在大多数情况下,你会发现自己使用Table Layout来排列输入表单。它也适用于布局复杂信息,特别是在让某些View对象跨越多行和多列时。Table Layout最重要的特点在于它以非常严格的方式对其单元格进行对齐,同时它是一个相对尺寸的布局。

Table Layout也可以用来实现类似于 AWT Border Layout类的效果。通常,在调整Table Layout以适应整个屏幕时,它变成了一个非常不同于简单网格的工具,允许你在控件中间放置一个Scroll View

通过在FrameLayout内使用Table Layout,你可以在内容View(如 Google Maps 中的控件)上方排列一个控制View。还要注意,与 AWT GridLayout不同,TableLayout内部的View尺寸并不附着在它所在的表格单元格尺寸上。通过使用gravity属性(可能还有布局边距),你可以在单元格内放置View对象,从而创建出更加用户友好的布局。

在记忆游戏中使用 TableLayout

为了演示TableLayout,我认为编写一个简单的记忆卡牌游戏会很有趣。你面对的是一个网格(以TableLayout的形式),你可以触摸它来有效地翻转“卡片”。然后你可以尝试匹配所有这些卡片上的内容(每次只允许翻转两张)。在这个例子中,你需要在卡片上放置一些图片(我这里复用了交付示例中的水果图标)。在这个应用中,我们还将创建一个简单的占位符图片,以 XML 文件的形式。

为了创建占位符图像,在res/drawable目录中创建一个新的 XML 资源,名为line.xml。这将是一个“形状”资源。形状资源对于创建简单、可伸缩的形状非常有用。此外,形状资源文件可以使用代码提供的任何颜色、纹理或渐变。

为了创建我们示例的简单占位符图像,将以下代码复制到line.xml文件中:

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

<shape 
       android:shape="line">

    <stroke android:width="3dp"
            android:color="#ff000000"/>

    <padding android:left="1dp"
             android:top="1dp"
             android:right="1dp"
             android:bottom="1dp"/>
</shape>

是时候行动了——开发一个简单的记忆游戏

与几乎所有之前的示例不同,在这个游戏中,我们将完全在 Java 代码中生成布局。这样做的主要原因是内容高度重复,每个单元格几乎包含完全相同的控件。我们使用TableLayout创建网格,并在ImageButton控件中显示“卡片”。为了封装单个卡片的行为,我们创建了一个MemoryCard内部类,它持有一个对它控制的ImageButton的引用。

  1. 在项目的根包中创建一个新的 Java 类,并将其命名为TableLayoutActivity

  2. 让新类继承Activity

    public class TableLayoutActivity extends Activity {
    Declare and array of all the icon resources to use as card images, there must be eight images resources declared in this array:private static final int[] CARD_RESOURCES = new int[]{
        R.drawable.apple,
        R.drawable.banana,
        R.drawable.blackberry,
        // …
    };
    
  3. 你需要一个定时器来将卡片翻回,因此声明一个Handler

    private final Handler handler = new Handler();
    
  4. 声明一个MemoryCard对象数组:

    private MemoryCard[] cards;
    
  5. 我们要跟踪的卡片有一张或两张被翻过来。声明第一个的占位符:

    private MemoryCard visible = null;
    
  6. 如果有两张卡片被翻过来,但它们不匹配,我们用一个简单的boolean开关禁用触摸(我们的事件监听器将检查这一点):

    private boolean touchEnabled = true;
    
  7. 现在声明一个名为MemoryCard的内部类,该类实现了OnClickListener接口:

    private class MemoryCard implements OnClickListener {
    
  8. MemoryCard类持有一个对ImageButton的引用:

    private ImageButton button;
    
  9. MemoryCard类还有一个值,它是卡片正面的图像资源的引用:

    private int faceImage;
    
  10. 最后,MemoryCard使用一个boolean值来记住其状态(是显示正面图像还是占位符图像):

    private boolean faceVisible = false;
    
  11. MemoryCard类声明一个构造函数,它只需要获取正面图像的资源标识符:

    MemoryCard(int faceImage) {
    
  12. 保存faceImage资源标识符以供以后使用:

    this.faceImage = faceImage;
    
  13. 使用TableLayoutActivity对象作为其ContextImageButton将使用它来加载图像)创建一个新的ImageButton对象:

    this.button = new ImageButton(TableLayoutActivity.this);
    
  14. ImageButton的大小设置为固定的 64x64 像素:

    this.button.setLayoutParams(new TableRow.LayoutParams(64, 64));
    
  15. 设置缩放类型,使图标适合ImageButton,然后将图像设置为占位符资源:

    this.button.setScaleType(ScaleType.FIT_XY);
    this.button.setImageResource(R.drawable.line);
    
  16. MemoryCard对象设置为ImageButton对象的OnClickListener

    this.button.setOnClickListener(this);
    
  17. 为了方便以后使用,MemoryCard需要一个setFaceVisible方法,该方法将在显示占位符和faceImage资源之间切换。

    void setFaceVisible(boolean faceVisible) {
        this.faceVisible = faceVisible;
        button.setImageResource(faceVisible
                ? faceImage
                : R.drawable.line);
    }
    
  18. MemoryCard类中实现onClick方法:

    public void onClick(View view) {
    
  19. 首先确保当前脸部不可见(即我们已经翻面朝下),并且触摸功能已启用(其他一些卡片不会再次被翻面朝下):

    if(!faceVisible && touchEnabled) {
    
  20. 如果满足这些条件,我们告诉TableLayoutActivity我们已被触摸并希望被翻到正面朝上:

    onMemoryCardUncovered(this);
    
  21. MemoryCell 内部类之后,在 TableLayoutActivity 中创建一个简单的工具方法,以特定大小创建有序的 MemoryCell 对象数组:

    private MemoryCard[] createMemoryCells(int count) {
    
  22. 当我们创建每个 MemoryCell 对象时,我们会成对创建它们,并且按照我们在图标资源数组中指定的顺序:

    MemoryCard[] array = new MemoryCard[count];
    for(int i = 0; i < count; i++) {
        array[i] = new MemoryCard(CARD_RESOURCES[i / 2]);
    }
    
  23. 完成后,返回新的 MemoryCell 对象数组:

    return array;
    
  24. 现在,重写 onCreate 方法:

    protected void onCreate(Bundle savedInstanceState) {
    
  25. 调用 Activity.onCreate 方法:

    super.onCreate(savedInstanceState);
    
  26. 现在,创建一个新的 TableLayout 对象,将其传递给 TableLayoutActivity 作为 Context 以加载样式和资源:

    TableLayout table = new TableLayout(this);
    
  27. 默认情况下,我们创建一个 4x4 的网格:

    int size = 4;
    cards = createMemoryCells(size * size);
    
  28. 然后,我们将其打乱以随机化顺序:

    Collections.shuffle(Arrays.asList(cards));
    
  29. 创建所需的每个 TableRow 对象,并用由 MemoryCard 对象在网格中创建的 ImageButtons 填充它:

    for(int y = 0; y < size; y++) {
        TableRow row = new TableRow(this);
        for(int x = 0; x < size; x++) {
            row.addView(cards[(y * size) + x].button);
        }
        table.addView(row);
    }
    
  30. Activity 内容视图设置为 TableLayout 对象:

    setContentView(table);
    
  31. 现在,我们编写 onMemoryCardUncovered 方法,它由 MemoryCard.onClick 实现调用:

    private void onMemoryCardUncovered(final MemoryCard cell) {
    
  32. 首先,检查当前是否有可见的 MemoryCard,如果没有,用户触摸的卡片将翻转到正面,并记住它:

    if(visible == null) {
        visible = cell;
        visible.setFaceVisible(true);
    }
    
  33. 如果已经有一张正面朝上的卡片,检查它们是否具有相同的图像。如果图像相同,禁用 ImageButton 小部件,以便我们忽略事件:

    else if(visible.faceImage == cell.faceImage) {
        cell.setFaceVisible(true);
        cell.button.setEnabled(false);
        visible.button.setEnabled(false);
        visible = null;
    }
    
  34. 最后,如果正面图像不匹配,我们将用户触摸的卡片翻转到正面,并切换我们的 touchEnabled 开关,使 MemoryCard 对象将忽略所有其他触摸事件一秒钟:

    else {
        cell.setFaceVisible(true);
        touchEnabled = false;
    
  35. 然后,我们在 Handler 上发布一个延迟的消息,它将再次翻转两张卡片并重新启用触摸事件:

    handler.postDelayed(new Runnable() {
        public void run() {
            cell.setFaceVisible(false);
            visible.setFaceVisible(false);
            visible = null;
            touchEnabled = true;
        }
    }, 1000); // one second before we flip back over again
    

刚才发生了什么

在上一个示例中,我们手动编写布局代码的原因应该很明了,如果用 XML 文件构建将会非常重复。你会注意到,代码创建了一个 TableRow 对象作为 TableLayout 的直接子项,就像我们在 XML 文件中一样。

MemoryCardonClick 方法使用 touchEnabled 开关来确定是否调用 onMemoryCardUncovered。然而,这既不能阻止用户按下 ImageButton 对象,也不能阻止对象对用户做出反应(尽管它们不会翻转)。为了提供更友好的用户体验,最好对每个启用的 ImageButton 对象使用 setClickable 方法,以完全阻止它们对用户的触摸做出反应。

当我们创建 ImageButton 对象时,会将它们预设为 64x64 像素大小。这对于大屏幕模拟器来说可能没问题,但有很多设备无法容纳屏幕上的 4x4 按钮网格。我建议你使用 XML 资源来创建 ImageButton 对象。

之前的代码使用setLayoutParams(new TableRow.LayoutParams(64, 64));来设置ImageButton对象的大小。需要注意的是,由于我们将ImageButton对象放入到TableRow中,它们的LayoutParams必须是TableRow.LayoutParams类型。如果你尝试改为通用的ViewGroup.LayoutParams,那么用户界面将不会布局(它会变成空白)。以下是应用程序运行的两个截图:

刚刚发生了什么

尝试一下英雄

TableLayout示例效果很好,但网格的位置不佳(在屏幕左上角),并且将其放在黑色背景上相当单调。是时候让它看起来很棒了!

首先,使用FrameLayout为游戏添加一个背景图像。这将通过添加更多色彩来增强游戏的整体吸引力。你也应该借此机会将网格在屏幕上居中。将其放在左上角不知为何会显得不平衡。

你还应该尝试移除touchEnabled开关,改为在每个ImageButton对象上使用setClickable。这将阻止它们在你将牌面朝下时提供视觉上的“按下和释放”反馈。

AbsoluteLayout/自定义布局

不要使用 AbsoluteLayout! AbsoluteLayout 已被弃用! 也就是说,有时使用AbsoluteLayout类是有意义的。那么你为什么不应该使用AbsoluteLayout类,你应该在什么时候使用它呢?第一个问题的答案很简单——AbsoluteLayout的所有子部件都有它们的确切位置,它们在不同屏幕上不会改变大小或位置。它还使你的布局几乎不可能被复用(例如,将其导入另一个布局,或嵌入到另一个应用程序中)。

如果你要使用AbsoluteLayout,你应该选择以下两种方法之一来接近它:

  1. 仔细为每种不同的屏幕尺寸构建一个单独的布局 XML。

  2. 在 Java 代码中编写你的布局数据,而不是在 XML 中。

第一种方法不切实际,除非你指定应用程序只能在特定设备上运行,而且该布局不能在你的应用程序之外使用。然而,第二种方法开启了“正确”的道路——编写自定义布局管理器。由于AbsoluteLayout需要严格的位置,并且不允许与子View对象的测量轻松交互,定义不适合任何其他布局类的布局的最佳方法是 在你自己的ViewGroup类中定义一个自定义布局。

开发你自己的布局

由于AbsoluteLayout已被弃用,但仍有很多人似乎坚持使用它,这个例子将展示如何编写自己的ViewGroup类定义一个新布局,以及将这个布局集成到布局 XML 资源中是多么容易。这将证明使用AbsoluteLayout并没有充分的理由(除非它真的有意义)。

行动时间——创建自定义布局

为了真正展示自定义布局的使用,你需要尝试构建一些不寻常的东西。在以下示例中,你将组合一个以美观的圆形排列其子项的ViewGroup。这并不是一个特别出色的布局,也不特别实用,但圆形看起来很美观,并且它将在屏幕中心提供有用的空白空间(可以使用FrameLayout填充)。

  1. 在项目的根包中创建一个名为CircleLayout.java的新 Java 源文件,并在编辑器或 IDE 中打开它。

  2. 声明CircleLayout扩展自ViewGroup类:

    public class CircleLayout extends ViewGroup
    
  3. 声明三个ViewGroup构造函数,并直接将它们委托给ViewGroup的默认构造函数:

    public CircleLayout(Context context) {
        super(context);
    }
    // ...
    
  4. 我们需要知道子View对象宽度占用的最大像素数,以及子View对象高度占用的最大像素数。为了避免不必要开销,我们借此机会也测量View对象。声明一个名为measureChildrenSizes的实用方法来执行这两个操作:

    private int[] measureChildrenSizes(int sw, int sh) {
    
  5. 声明一个int来保存我们找到的最大宽度和高度:

    int maxWidth = 0;
    int maxHeight = 0;
    
  6. 创建一个for循环,遍历此CircleLayout对象中的每个子View对象:

    for(int i = 0; i < getChildCount(); i++) {
    
  7. 声明一个对当前索引处View的引用:

    View child = getChildAt(i);
    
  8. 作为布局组件,你的类需要负责为其所有子组件设置显示大小。为了知道子组件期望的宽度和高度,你需要在ViewGroup类中使用measureChild方法:

    measureChild(child, sw, sh);
    
  9. 测试子View对象的宽度和高度,与你之前创建的最大宽度变量和高度变量进行比较:

    maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
    maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
    
  10. 在方法末尾,返回一个包含在过程中找到的最大宽度和高度的数组:

    return new int[]{maxWidth, maxHeight};
    
  11. 实现ViewGrouponLayout方法:

    protected void onLayout(boolean changed,
            int l, int t, int r, int b) {
    
  12. 计算我们可用空间的宽度和高度:

    int w = r – l;
    int h = b - t;
    
  13. 声明一个变量来保存子View对象的数量:

    int count = getChildCount();
    
  14. 对所有子View对象进行测量,以确定可用空间的大小:

    int[] max = measureChildrenSizes(w, h);
    
  15. 从可用空间中减去最大宽度和高度,以确保所有子View对象都能在屏幕上显示:

    w -= max[0];
    h -= max[1];
    
  16. 计算CircleLayout中的中心点:

    int cx = w / 2;
    int cy = h / 2;
    
  17. 创建一个for循环,再次遍历每个子View对象:

    for(int i = 0; i < count; i++) {
    
  18. 声明一个变量来保存当前的子View对象:

    View child = getChildAt(i);
    
  19. 计算子View对象的xy位置:

    double v = 2 * Math.PI * i / count;
    int x = l + (cx + (int)(Math.cos(v) * cx));
    int y = t + (cy + (int)(Math.sin(v) * cy));
    
  20. 使用计算出的圆中坐标调用子View对象的布局方法:

    child.layout(
            x, y,
            x + child.getMeasuredWidth(),
            y + child.getMeasuredHeight());
    

刚才发生了什么?

CircleLayout类是一个非常简单的ViewGroup实现。除了其子项请求的宽度和高度外,它没有可以在 XML 资源中使用的特殊属性。然而,它会注意到你为子项声明的尺寸,因此layout_widthlayout_height属性将正常工作。

需要注意的是,为了从布局 XML 资源中使用自定义ViewViewGroup,你需要重写所有三个默认构造函数。

注意

LayoutInflater将使用这些构造函数中的一个来创建你的类的实例。如果它想要使用的那个不存在,那么在尝试膨胀布局 XML 文件时,你会遇到可怕的强制关闭对话框。

CircleLayout有其自己的实用方法来处理其子View对象的测量。通常,ViewGroup会使用ViewGroup.measureChildren工具方法来确保其所有子View对象在执行实际布局之前都已被测量。然而,我们需要遍历子View对象列表以找到最大的宽度和高度,因此我们不是执行三次迭代,而是自己执行测量。

使用 CircleLayout

为了使用自定义ViewGroup实现,了解 Android 在 XML 布局资源方面为你提供了支持是很有帮助的。当你需要从 XML 布局资源中引用自定义ViewViewGroup类时,只需使用完整的类名而不是简单的类名。以下是使用CircleLayout的 XML 布局的一个简单示例:

<com.packtpub.layouts.CircleLayout

    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <Button android:text="Button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    <Button android:text="Button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    <!-- 10 Buttons in total works nicely 

</com.packtpub.layouts.CircleLayout>

动手实践时间——完成 CircleLayout 示例

我们已经有了CicleLayout的实现,但现在我们真的应该将其包含在“layouts”示例中。为此,我们需要一个布局资源 XML 文件,一个新的CircleLayoutActivity类。我们还需要在 Android(在清单文件中)和我们的LayoutSelectorActivity类(在其事件监听器中)注册新的Activity

  1. 将前面的 XML 布局复制到一个名为res/layout/circle_layout.xml的新文件中。最好添加大约十个小部件作为CircleLayout ViewGroup的子项。

  2. 在项目的根包中创建一个名为CircleLayoutActivity.java的新 Java 源文件。在编辑器或 IDE 中打开它。

  3. CircleLayoutActivity必须继承Activity类:

    public class CircleLayoutActivity extends Activity {
    
  4. 重写ActivityonCreate方法:

    protected void onCreate(Bundle savedInstanceState) {
    
  5. 调用父类:

    super.onCreate(savedInstanceState);
    
  6. 将内容视图设置为circle_layout布局资源:

    setContentView(R.layout.circle_layout);
    
  7. 在编辑器或 IDE 中打开AndroidManifest.xml文件。

  8. TableLayoutActivity声明之后,声明新的CircleLayoutActivity

    <activity android:name=".CircleLayoutActivity"
              android:label="Circle Layout Example"/>
    
  9. 在编辑器或 IDE 中打开LayoutSelectorActivity源文件。

  10. onListItemClick方法中,在default case之前,添加一个新的case语句来启动CircleLayoutActivity

    case 2:
        startActivity(new Intent(
            this, CircleLayoutActivity.class));
        break;
    

刚才发生了什么?

现在你有一个使用自定义ViewGroup实现的新Activity实现。自定义ViewGroup类不仅在标准ViewGroup实现无法很好地处理难以表达的布局时有用。当默认的ViewGroup实现对于你想要实现的具体结构来说太慢时,自定义ViewGroup也是一个选项。

你在本章中一直在构建的“布局”示例现在将拥有一个可用的自定义布局菜单项。点击它,你会看到以下截图。尝试添加除Button对象之外的控件,甚至可以尝试加入一个子ViewGroup看看会发生什么。

刚才发生了什么?

快速测验

  1. 布局通常分为两个阶段,第一个阶段叫什么?

    1. 预布局

    2. 计算

    3. 父布局

    4. 测量

  2. 布局方法的四个参数表示什么?

    1. x, y, 宽度, 高度。

    2. 左,上,右,下。

    3. ViewGroup的大小。

  3. 自定义ViewGroup实现如何读取布局 XML 属性?

    1. 它们通过LayoutInflator注入到 setter 方法中。

    2. 它们通过View.getAttribute方法加载。

    3. 它们从传递给ViewGroup构造函数的AttributeSet对象中读取。

RelativeLayout

RelativeLayout类可以说是 Android 提供的最强大的布局。它是一个相对布局,管理大小不一的控件,并使控件相互对齐,而不是与它们的父控件或网格线对齐。在某种程度上,RelativeLayout与 Swing 的GroupLayout类非常相似,尽管它远没有后者复杂。RelativeLayout中的每个控件都是相对于另一个控件或其父控件(即RelativeLayout本身)来定位的。

RelativeLayout通过单次循环计算每个子控件的位置,因此它非常依赖于你指定子控件的顺序。但这并不意味着你必须按照它们在屏幕上显示的顺序来指定控件。由于RelativeLayout的性质,子控件通常以不同的顺序声明和显示。这也要求任何用于对齐其他控件的用户界面元素必须分配一个 ID。这包括通常不需要 ID 的非交互式用户界面元素,现在也必须分配一个 ID,尽管它们永远不会在布局之外使用。

使用RelativeLayout非常灵活,但也可能需要一些仔细的规划。与任何用户界面一样,首先在纸上绘制布局会非常有帮助。一旦有了纸上的图表,你就可以开始根据RelativeLayout类的规则来规划如何构建布局了。

常见用途

RelativeLayout的用途与TableLayout非常相似。它非常适合绘制表单和内容视图。然而,RelativeLayout并不局限于TableLayout的网格模式,因此可以创建屏幕上物理位置相隔较远的控件之间的关联(即通过相互对齐)。

RelativeLayout 可以根据同一 RelativeLayout 中的其他组件以及/或者 RelativeLayout 边界来定位和设置组件的大小。这意味着某些组件可能被放置在屏幕顶部,而你可以将另一组组件对齐在屏幕底部,如下图所示。

常见用途

集成 RelativeLayout

面对联系人编辑器时,RelativeLayout 是制作易于使用用户界面的完美工具。在下一个示例中,我们构建了一个非常简单的联系人编辑用户界面,包括用户图像。

动手时间——创建一个联系人编辑器

本示例要求部分用户界面元素按非顺序声明(如之前讨论的)。我们还在屏幕底部包含了 保存取消 Button 组件。这个示例回到了在资源 XML 文件中声明用户界面,而不是在 Java 代码中编写。对于此示例,你需要一个用户联系人照片的占位图像。一个 64x64 像素的 PNG 文件是合适的大小(我使用了一个大大的笑脸图像)。

  1. 首先,创建一个新的 XML 布局文件,命名为 res/layout/relative_layout.xml。在你的编辑器或 IDE 中打开这个文件。

  2. 将根元素声明为全屏的 RelativeLayout

    <RelativeLayout
    
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  3. 创建一个带有用户图标的 ImageButtonImageButton 应该与屏幕左上角对齐,并包含一个占位图像:

    <ImageButton android:src="img/face"
                 android:id="@+id/photo"
                 android:layout_alignParentTop="true"
                 android:layout_alignParentLeft="true"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"/>
    
  4. 添加一个 EditText,用户可以在其中输入联系人的姓名。将其与 ImageButton 右下对齐:

    <EditText android:text="Unknown"
              android:id="@+id/contact_name"
              android:layout_alignBottom="@id/photo"
              android:layout_toRightOf="@id/photo"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  5. 现在添加一个 TextView 作为 EditText 组件的标签。我们将这个标签与 ImageButton 右对齐,但位于 EditText 之上:

    <TextView android:text="Contact Name:"
              android:id="@+id/contact_label"
              android:layout_above="@id/contact_name"
              android:layout_toRightOf="@id/photo"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"/>
    
  6. 我们需要一个 编辑 Button 以允许用户编辑联系人的电话号码列表。将此按钮放置在屏幕右侧,并位于 EditText 下方。我们在按钮顶部添加边距,以在用户界面中形成逻辑分隔:

    <Button android:id="@+id/edit_numbers"
            android:text="Edit"
            android:paddingLeft="20dp"
            android:paddingRight="20dp"
            android:layout_below="@id/contact_name"
            android:layout_alignParentRight="true"
            android:layout_marginTop="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
  7. 创建一个大的 TextView 作为电话号码的标签,我们将在新的 TextView编辑 Button 下方列出电话号码:

    <TextView android:text="Contact Numbers:"
              android:id="@+id/numbers_label"
              android:textSize="20sp"
              android:layout_alignBaseline="@id/edit_numbers"
              android:layout_alignParentLeft="true"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"/>
    
  8. 现在创建一个 TableLayout 以列出联系人电话号码,将这个 TableLayoutRelativeLayout 中居中对齐,并将其置于 Contact Numbers 标签下方,并留有微小边距:

    <TableLayout android:layout_below="@id/edit_numbers"
                 android:layout_marginTop="5dp"
                 android:layout_centerInParent="true"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content">
    
  9. TableLayout 添加两个带有一些示例内容的 TableRow 元素:

    <TableRow>
        <TextView android:text="Home"
                  android:layout_marginRight="20dp"/>
        <TextView android:text="555-987-5678"/>
    </TableRow>
    <TableRow>
        <TextView android:text="Mobile" 
                  android:layout_marginRight="20dp"/>
        <TextView android:text="555-345-7654"/>
    </TableRow>
    
  10. 创建一个位于屏幕左下角的保存 Button

    <Button android:text="Save"
            android:id="@+id/save"
            android:layout_alignParentLeft="true"
            android:layout_alignParentBottom="true"
            android:layout_width="100sp"
            android:layout_height="wrap_content"/>
    
  11. 创建一个位于屏幕右下角的取消 Button

    <Button android:text="Cancel"
            android:id="@+id/cancel"
            android:layout_alignParentRight="true"
            android:layout_alignParentBottom="true"
            android:layout_width="100sp"
            android:layout_height="wrap_content"/>
    

刚才发生了什么

在上一个示例中,许多用户界面元素是按照与逻辑布局顺序相反的顺序声明的,而其他元素则是相对于 RelativeLayout 本身定位的,因此可以放在 XML 文件的任何位置。

联系人姓名标签和编辑器相对于“联系人照片”定位,而“联系人照片”又相对于屏幕(或RelativeLayout)。然而,由于我们希望标签直接位于编辑器上方,因此我们需要在TextView元素之前声明并定位EditText元素。

联系人姓名EditText元素使用了fill_parent的宽度,在RelativeLayout中,这将简单地填充可用的水平空间(如果是用在控件的高度上则是垂直空间)。当你希望一个元素简单地占据“行”的剩余部分,或者横跨整个屏幕(例如,作为分割线)时,这是一个很有用的特性。在RelativeLayout中,你不能对同一个轴上的控件使用两个相互冲突的布局属性。例如,你不能在同一个View控件上同时使用layout_toRightOflayout_alignRight

行动时间——与布局示例集成

RelativeLayout示例的集成与之前编写的自定义CircleLayout示例的集成几乎相同。集成将需要一个新的Activity实现,然后我们需要将其注册到 Android 和LayoutSelectorActivity中。

  1. 在“layouts”示例项目的根包中创建一个新的 Java 源文件,命名为RelativeLayoutActivity.java。在你的编辑器或 IDE 中打开这个文件。

  2. 新的RelativeLayoutActivity需要扩展Activity类:

    public class RelativeLayoutActivity extends Activity {
    
  3. 重写onCreate方法:

    protected void onCreate(Bundle savedInstanceState) {
    
  4. 调用super类来设置其状态:

    super.onCreate(savedInstanceState);
    
  5. 将新的Activity的内容视图设置为之前创建的relative_layout XML 布局资源:

    setContentView(R.layout.relative_layout);
    
  6. 在你的编辑器或 IDE 中打开AndroidManifest.xml文件。

  7. CircleLayoutActivity之后注册RelativeLayoutActivity

    <activity android:name=".RelativeLayoutActivity"
              android:label="Relative Layout Example"/>
    
  8. 在你的编辑器或 IDE 中打开LayoutSelectorActivity的 Java 源代码。

  9. onListItemClick方法中,在default语句之前声明一个新的case语句并启动新的RelativeLayoutActivity

    case 3:
        startActivity(new Intent(
                this, RelativeLayoutActivity.class));
        break;
    

刚才发生了什么?

现在RelativeLayoutActivity已经与布局示例的其余部分集成在一起,你可以启动模拟器并查看你刚刚构建的屏幕。正如以下截图所示,这个设计比我们迄今为止构建的其他大多数设计都要用户友好。这主要是因为它能够以逻辑上相互关联的方式对控件进行分组和对其,而不是被迫局限于所选ViewGroup的要求。

然而,这种灵活性并非没有代价。RelativeLayout结构比其他ViewGroup实现更容易被破坏,在许多情况下,它不会为你提供太多的额外灵活性。在上述示例中,我们嵌入了一个TableLayout来显示联系人号码列表,而不是直接在RelativeLayout元素下显示它们。不仅TableLayout更适合这项任务,它还允许我们将号码作为一个组居中排列,而不是将它们对齐到RelativeLayout的左右两侧。

RelativeLayout与内嵌的ScrollViewFrameLayout结合使用,是提供以内容为中心的用户界面工具栏的绝佳方式。当你的用户界面以媒体为中心(如全屏地图、视频、照片或类似内容)时,使用RelativeLayout将工具按钮围绕屏幕边缘布局,并通过FrameLayout将实际内容置于其后,这在许多 Android 应用中都能看到,如谷歌地图或默认的浏览器应用。这种设计还允许你根据用户与应用的交互来显示或隐藏工具按钮,从而在用户不与工具集互动时,让他们更好地查看媒体内容。

发生了什么?

滑动抽屉

如果你使用过未主题化的 Android 安装(如在模拟器中),或大多数主题化的 Android 版本,那么你已经使用过SlidingDrawer。这是推动启动器菜单打开和关闭的控件。虽然它本身并不是一个布局,但SlidingDrawer允许你快速向用户展示大量较少使用的控件。在开发新用户界面时,这使得它成为一个重要的控件考虑因素。

通常,在使用菜单和SlidingDrawer之间需要做出选择。虽然菜单非常适合显示动作项,但SlidingDrawer可以显示你想要的任何内容。然而,SlidingDrawer对其使用也有一些限制。例如,它要求你将其放置在FrameLayoutRelativeLayout实例中(其中FrameLayout更为典型),以使其正确工作。

SlidingDrawer在某种程度上是一种揭示控件。它由一个手柄和内容部分组成。默认情况下,只有手柄在屏幕上是可见的,直到用户触摸或拉动手柄来打开SlidingDrawer并显示内容部分。

常见用途

SlidingDrawer类的打开/关闭内容特性使其成为 Android 中应用启动器的理想选择。默认情况下,它是隐藏的,因此桌面可见且可用,直到你点击手柄以查看可用的应用程序列表。

这也使得 SlidingDrawer 成为构建策略游戏等应用程序的绝佳工具。例如,不要为用户提供所有可用的构建选项,而是将默认屏幕视图限制为关键地图元素。当用户想要构建某物或检查某些状态信息时,他们可以从屏幕底部轻触或拖动打开 SlidingDrawer,从而显示所有构建/命令选项。

通常,当用户不需要经常与之交互的动作或信息时,SlidingDrawer 是一个展示它们的绝佳方式。当需要用户注意的关键事件发生时,它也可以从你的 Java 代码中打开和关闭。

SlidingDrawer 的 handle 元素也是一个完整的 ViewViewGroup,允许你在其中放置状态信息。slidingdrawer 控件的另一个常见用途是,大多数 Android 设备顶部的状态栏通常实现为 SlidingDrawer。当事件发生时,在 handle 上显示摘要,用户可以拖开内容以查看最近事件的完整详情。

创建一个 SlidingDrawer 示例

为了让 SlidingDrawer 示例保持简洁,我们将重用 CircleLayout 示例,并进行一个主要修改——背景颜色需要改变。如果 SlidingDrawer 的背景没有特别设置,背景将会是透明的。通常,这是不希望发生的,因为打开的 SlidingDrawer 控件背后的内容会变得可见,这会干扰 SlidingDrawer 的内容。

是时候行动了——创建一个 SlidingDrawer

在本例中,我们将在一张图片上方放置一个 SlidingDrawer 控件(我再次选择了一位朋友的照片作为背景)。SlidingDrawer 的 handle 将使用为 TableLayoutActivity 创建的线条可绘制 XML 文件。SlidingDrawer 的内容将使用 circle_layout 资源。

  1. 在你的编辑器或 IDE 中打开 res/layout/circle_layout.xml 文件。

  2. 在根元素声明中,将背景属性设置为黑色:

    <com.packtpub.layouts.CircleLayout
    
        android:background="#ff000000"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  3. 创建一个新的布局资源文件,命名为 sliding_drawer.xml,并在你的编辑器或 IDE 中打开这个文件。

  4. 将此布局的根元素声明为 FrameLayout

    <FrameLayout
    
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  5. FrameLayout 内部,创建一个 ImageView 以包含背景图像。记得设置缩放类型和大小,使图像充满屏幕:

    <ImageView android:src="img/jaipal"
               android:scaleType="centerCrop"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"/>
    
  6. 声明 SlidingDrawer 控件。由于 handle 和 content 控件尚未创建,你需要提前引用它们:

    <SlidingDrawer android:handle="@+id/handle"
                   android:content="@+id/content"
                   android:layout_width="fill_parent"
                   android:layout_height="fill_parent">
    
  7. SlidingDrawer 元素内部,创建一个 ImageView,使用之前为 TableLayoutActivity 创建的占位符 line 可绘制资源:

    <ImageView android:id="@id/handle"
               android:src="img/line"
               android:layout_width="fill_parent"
               android:layout_height="12dp"/>
    
  8. SlidingDrawer 元素内部,包含 circle_layout 布局资源,并将其 ID 分配为 "content":

    <include android:id="@id/content"
             layout="@layout/circle_layout"/>
    

刚才发生了什么?

你可能注意到了,在之前的例子中,SlidingDrawer为其手柄和内容小部件添加了 ID 引用,而小部件本身似乎访问这些 ID 而不是声明它们:

<SlidingDrawer android:handle="@+id/handle"
               android:content="@+id/content"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent">

这是SlidingDrawer类工作方式的一个副作用。它需要 ID 值,然后才需要小部件本身。这种技术非常类似于向前引用,不同之处在于对象在技术上并没有被创建。@+语法告诉资源编译器我们正在创建一个新的 id,但不是一个新对象。当我们后来使用@id/handle值作为其id声明ImageView元素时,实际上我们正在引用在声明SlidingDrawer时生成的值。

行动时间——滑动抽屉集成

现在是时候将SlidingDrawer示例插入到“layouts”示例中了。这与其他所有集成一样,涉及一个新的Activity,以及将新的Activity注册到 Android 和LayoutSelectorActivity中。

  1. 在“layouts”示例项目的根包中创建一个新的 Java 源文件,名为SlidingDrawerActivity.java。在你的编辑器或 IDE 中打开这个文件。

  2. 新的SlidingDrawerActivity需要扩展Activity类:

    public class SlidingDrawerActivity extends Activity {
    
  3. 重写onCreate方法:

    protected void onCreate(Bundle savedInstanceState) {
    
  4. 调用超类来设置其状态:

    super.onCreate(savedInstanceState);
    
  5. 将新Activity的内容视图设置为之前创建的sliding_drawer XML 布局资源:

    setContentView(R.layout.sliding_drawer);
    
  6. 在你的编辑器或 IDE 中打开AndroidManifest.xml文件。

  7. 在声明RelativeLayoutActivity之后注册SlidingDrawerActivity

    <activity android:name=".SlidingDrawerActivity"
              android:label="Sliding Drawer Example"/>
    
  8. 在你的编辑器或 IDE 中打开LayoutSelectorActivity Java 源代码。

  9. onListItemClick方法中,在default语句之前声明一个新的case语句,并启动新的SlidingDrawerActivity

    case 3:
        startActivity(new Intent(
                this, SlidingDrawerActivity.class));
        break;
    

刚才发生了什么?

你已经完成了本章中的所有布局示例。你的switch语句中的default条件不应该再次触发了!SlidingDrawer示例非常简单,但很好地展示了这个部件可以有多么灵活。如果这个例子是一个绘图应用程序,SlidingDrawer将是一个隐藏更多复杂绘图功能的完美地方。

这个SlidingDrawer示例的手柄是一个简单的ImageView,但它可以是任何ViewViewGroup(如果你愿意,可以是TableLayout)。然而,你希望避免手柄变得可交互(即,一个ButtonEditText小部件)。手柄中的交互式小部件会在用户触摸它时引起问题。尽管小部件完全功能正常,可以像手柄一样上下拖动,但触摸它以开始交互将导致SlidingDrawer打开或关闭。为了防止这种情况发生,你可以选择通过allowSingleTap属性关闭SlidingDrawer的“触摸切换”选项:

<SlidingDrawer android:handle="@+id/handle"
               android:content="@+id/content"
               android:allowSingleTap="false"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent">

也就是说,将EditText(或类似的控件)作为SlidingDrawer的把手几乎是没有意义的,这很可能会让你的用户感到非常恼火。尽可能确保你的SlidingDrawer小部件的把手看起来像是用户可以拖动的东西。启动器应用程序的默认把手就是一个很好的例子。

刚才发生了什么?

总结

通过本章示例的学习,应该能让你很好地了解 Android 默认提供的布局,以及它们是如何实现的(以及在需要时如何实现新的布局)。在大多数情况下,这些ViewGroup实现将满足你的任何布局需求,但在构建 Android 布局时,仍然需要牢记以下原则:

  • 不同的设备具有不同的大小和分辨率屏幕

  • 使用负空间(空白)和线条来分隔小部件组

  • 你几乎肯定需要在将来修改布局

在选择使用RelativeLayout类时,最后一点尤为重要。虽然它比其他实现方式提供了更多的功能,但一个组合得不好的RelativeLayout可能会非常难以维护,且耗时。

在接下来的章节中,我们将探讨如何捕获输入以及输入验证应当作为用户界面设计决策的一部分。我们还将通过一些示例来进行实践,这些示例可以作为未来用户界面开发的基础。

第六章:验证和处理输入数据

不幸的是,在应用程序中验证和处理输入通常在设计过程中是一个后顾之忧。这些应该在用户界面第二轮草稿中的首要考虑事项。触摸屏设备提供了许多机会来简化从用户那里捕获数据的流程,在许多情况下,无需进行数据清理或验证,同时大幅提升用户的应用体验。

安卓提供了一个优秀的工具集,以捕获用户的各种不同类型的数据,同时以Intent结构的形式为应用程序组件之间的松耦合提供支持。通过使用几个较小的Activity类来捕获数据,同时抽象出捕获不同类型输入的功能,你将能够更容易地重用输入捕获Activity类,不仅在应用程序内,也可以在其他应用程序中使用。此外,通过正确注册Activity,你将允许其他应用程序覆盖或使用你的Activity实现,让用户选择他们偏好的捕获机制。

处理不期望的输入

通常应用程序需要其用户输入特定类型的数据。应用程序从用户那里捕获输入是为了让用户告诉它关于世界的一些信息。这可以是任何东西,从用户正在寻找的内容(即一个搜索词),到关于用户自己的信息(即他们的年龄)。在大多数这些情况下,可以使用诸如自动完成框之类的机制引导用户输入。然而,如果用户可以给你“不期望”的输入,那么在某些环节中就会发生。

不期望的输入可以是任何从预期数字却输入文本,到搜索词没有结果的各种情况。在这两种情况下,你需要做三件事:

  1. 告诉用户你期望数据以何种格式输入

  2. 让他们知道他们输入了不期望的数据

  3. 让他们重新输入数据

正确标记输入

防止用户输入不期望数据的第一道防线是正确标记输入控件。这不仅仅意味着,有一个如下所示的标签:

出生日期(dd/mm/yy):

这意味着使用正确的控件来捕获数据。你的输入控件是一种标签,它们向用户指示你期望他们输入哪种类型的数据。在许多情况下,它们可以用来阻止用户输入无效数据,或者至少降低这种可能性。

注意

要牢记用户期望事物的工作方式,以及他们期望能够快速选择事物。如果你需要他们为你的应用程序提供一个国家的名字,不要使用Spinner并强迫他们浏览看似无尽的名称列表。

信号不期望的输入

如果用户确实输入了不希望或无用的内容,你需要迅速告诉他们!你越早让用户知道他们给了你无用的东西,他们就能越快地改正并回到使用你的应用程序。

一个常见的错误是在用户按下保存提交按钮时简单地Toast通知用户。虽然如果你只能在那时确定他们的错误,这样做是可以的,但你几乎总是可以提前弄清楚。

请记住,在触摸屏设备上,虽然你有一个“聚焦”的小部件,但它并不像在桌面系统上那样发挥作用,用户不会“跳转”离开小部件。这意味着,只要可能,你的用户界面就应该实时响应用户的操作,而不是等待他们做其他事情(即选择另一个小部件)后才给予反馈。如果他们做了使另一个表单元素无效的事情,就禁用它。如果他们做了使一组小部件无效的事情,就将整个组从他们那里隐藏或放在另一个屏幕上。

表示不希望的输入

使用颜色和图标是快速告诉用户他们做错了事情的好方法。当你意识到用户的某些输入是错误的时候,你可以采取额外的步骤,禁用任何保存下一步提交按钮。但是,如果你禁用这样的按钮,请确保清楚哪个表单元素上有不理想的数据,并确保它显示在屏幕上。一个很好的替代方法是当用户选择下一步按钮时Toast通知用户,并滚动到无效元素。

如果你需要检查用户的输入是否与某些远程服务相匹配,请使用后台(或异步)消息。这将允许你在用户使用应用程序时验证用户的内容。它还允许你在不阻止他们使用表单的其余部分的情况下,指出某些地方出了问题。他们总是可以回到无效字段并进行更正。

从不受欢迎的输入中恢复。

一定要确保用户纠正错误尽可能无痛。他们为了改正一个拼写错误(或类似的错误)而需要做的工作越多,他们停止使用应用程序的可能性就越大。从不受欢迎的输入中恢复(这与上述评论非常契合)的最简单方法是在用户有机会进入流程的另一部分之前告诉他们。然而,这并不总是可能的。

在某些流程中,你可能需要弹出一个请等待对话框,这通常会(作为副作用)验证用户的输入。在这种情况下,使用ProgressDialog是明智的,这样你就不会在这个阶段将用户从当前Activity中移开。这将带来两个重要的副作用:

  • 你不要向活动堆栈中添加不必要的层次。

  • 当你关闭ProgressDialog时,用户给出的输入仍然可用。

给用户提供直接反馈。

当接受用户输入文本或其他键盘输入时,最好在用户输入过程中向他们指示输入的有效性。一个常见的方法是在EditText组件右边使用一个ImageView,并通过更改图像内容来指示用户输入的是有效还是无效内容。ImageView中显示的图像可以根据输入当前是否有效来设置。这使用户能够实时查看验证过程。这种机制也适用于指示不同级别的验证(即输入不是严格的有效或无效,而是良好质量或不良质量),如在密码输入的情况下。

你可以使用图像图标,或者简单使用一个 Android 可绘制 XML 资源来表示有效性(即绿色表示有效,红色表示无效)。这也意味着你的图标会根据你在布局 XML 文件中指定的任何大小进行缩放。

提示

颜色和图标

通常使用非颜色指示器来区分图标是一个好主意。色盲的人可能很难或无法区分两个仅颜色不同的图标,除非你同时改变形状和颜色。将“有效”图标设为绿色圆形,而“无效”图标设为红色六边形,将使你的应用程序更具可用性。

为了避免屏幕上图标过多,你可能只想在用户当前操作的领域旁边显示验证图标。然而,使用INVISIBLE View状态而不是GONE是一个好主意,以避免用户改变用户界面焦点时改变布局。同时,请确保验证图标大小一致。

完全避免无效输入

请记住,在使用移动设备时,时间往往对用户是一种限制。因此(出于简单易用的原因),你通常应该尽力避免用户输入无效内容。Android 为你提供了多种机制来实现这一点,在每一个机会都利用它们是明智的。通常,你会想要使用那些避免验证需求的组件。在 Android 中这几乎总是一个选项,即使你的需求比简单的类型信息更复杂,你也可以通常自定义组件,以阻止用户违反你的验证规则。

捕获日期和时间

如我们已讨论的,在输入日期和时间时,你应该使用DatePickerTimePicker组件,或使用DatePickerDialogTimePickerDialog以避免基本组件引入的布局问题。

注意

除非你的应用程序有严格的要求,否则不要创建自己的日历小部件。你可能不喜欢DatePickerDialog的外观,但用户在其他 Android 应用程序中已经见过它们,并且知道如何使用。这些标准小部件还可能在未来的 Android 版本中得到改进,从而让你的应用程序在不做任何修改的情况下得到提升。

捕获日期和时间

你可能会发现,对于日期和时间输入,你需要额外的验证,特别是在捕获日期或时间范围时。例如,如果你要求用户输入出生日期,用户不应该能够输入晚于“今天”的任何时间(除非是预期的出生日期)。虽然DatePicker类有一个事件监听器,允许你监听对其数据的更改(DatePickerDialog实现此事件监听器),但你不能使用此事件监听器来取消更改事件。

因此,为了取消事件,你需要在事件执行期间将输入改回有效的值。这是 Android 中一个出奇简单的技巧。由于事件是在进行绘制的同一线程上执行的,这允许你在无效数据在屏幕上渲染之前更改值。以下是一个ValidatingDatePickerDialog的简单示例,你可以使用它来实现应用程序中简单的日期验证级别。如果你需要,也可以轻松地为TimePickerDialog编写类似的类。

public class ValidatingDatePickerDialog extends DatePickerDialog {

    private int lastValidYear;
    private int lastValidMonth;
    private int lastValidDay;
    private ValidationCallback callback = null;

    public ValidatingDatePickerDialog(
            final Context context,
            final OnDateSetListener callBack,
            final int year,
            final int monthOfYear,
            final int dayOfMonth) {

        super(context, callBack, year, monthOfYear, dayOfMonth);
        setValidData(year, monthOfYear, dayOfMonth);
    }

 protected void setValidData(
 final int year,
 final int monthOfYear,
 final int dayOfMonth) {

 lastValidYear = year;
 lastValidMonth = monthOfYear;
 lastValidDay = dayOfMonth;
 }

    @Override
    public void onDateChanged(
            final DatePicker view,
            final int year,
            final int month,
            final int day) {

        if(callback != null && !callback.isValid(year, month, day)) {
 view.updateDate(
 lastValidYear,
 lastValidMonth,
 lastValidDay);
        } else {
            super.onDateChanged(view, year, month, day);
            setValidData(year, month, day);
        }
    }

    public void setValidationCallback(
            final ValidationCallback callback) {
        this.callback = callback;
    }

    public ValidationCallback getValidationCallback() {
        return callback;
    }

    public interface ValidationCallback {
        boolean isValid(int year, int monthOfYear, int dayOfMonth);
    }
}

这种处理验证的方法适用于大多数不提供事件隐式验证的 Android 小部件,并且它比给用户一个带有文本 请输入一个有效的出生日期Toast 提供了更好的用户体验。它还避免了在应用程序中增加额外验证层的需要。

使用下拉菜单和列表视图进行选择

在应用程序中,用户经常需要从可能的值列表中选择某项。我们在第二章 视图的数据展示 中已经讨论了SpinnerListView小部件。然而,当涉及到验证时,它们提供的几个特性非常有用。它们是隐式验证的小部件,也就是说,由于输入的可能值是由应用程序定义的,用户不可能输入错误的数据。但是,当有效项目集基于其他用户输入或某些外部信息源改变时该怎么办呢?在这些情况下,你有几个选项可用。

更改数据集

阻止用户选择不再有效的值的简单方法是将其从数据集中移除。我们在BurgerAdapter中已经做过类似的事情,在第二章,为视图提供数据,当用户触摸某些项目时,我们修改了数据集。修改AdapterView的数据集是一个好主意,因为它“从菜单中移除了选项”。然而,它并不适用于Spinner类,因为如果项目从屏幕上移除,用户会想知道刚才还在那里的项目去哪了(可能会担心自己是否疯了)。

为了避免混淆或让用户感到沮丧,只有当某个项目可能不会重新添加到数据集中时,才应该从SpinnerListView数据集中移除项目。一个符合这一要求的好例子是可用的 Wi-Fi 网络列表或范围内的蓝牙设备列表。在这两种情况下,可用的项目列表由环境定义。用户会接受显示的选项并不总是对他们可用,而且新的项目可能会时不时出现。

禁用选择

一种替代的、通常对用户更友好的阻止某些项目被选中的方法是禁用它们。你可以通过覆盖ListAdapter类中的isEnabled(int)方法,让ListViewSpinner忽略项目。然而,这种方法只会在事件级别上禁用项目,项目仍然会显示为启用状态(它的主要目的是定义分隔视图)。

为了在视觉上禁用一个项目,你需要禁用显示该项目的View。这是告诉用户“你改变了某些东西,使得这个项目不可用”的一种非常有效的方式。图形化地禁用一个项目也让用户知道它将来可能会变得可用。

捕获文本输入

最难处理的输入是各种文本输入形式。我发现使用软键盘可能不如使用硬件键盘快,但从开发角度来看,它提供了硬件键盘所不具备的东西——灵活性。当我想要在字段中输入文本时,软键盘的状态将指示该字段有效的输入类型。如果我需要输入电话号码,键盘可以只显示数字,甚至变成拨号盘。这不仅告诉我应该做什么,还阻止我输入可能导致验证错误的内容。

安卓的TextView(以及EditText)控件为你提供了众多不同的选项和方法,通过这些你可以为文本输入定义复杂的验证规则。这些选项中的许多也被各种软键盘所理解,使得它们可以根据TextView控件的配置显示完整键盘的子集。即使软键盘不完全理解(或使用硬件键盘时),也必须遵守指定选项的规则。最简单的方法是使用inputType XML 属性来告诉EditText你希望它捕获的数据类型。

inputType的文档中,你可以看到其所有可能的值都是android.view.inputmethod.InputType接口中可用的位掩码的不同组合。inputType属性可用的选项将涵盖大多数需要捕获特定类型输入的情况。你也可以通过使用TextView.setRawInputTextView.setKeyboardListener方法创建自己的更复杂的输入类型。

提示

键盘监听器

尽可能地,你应该使用输入类型或标准的KeyListener来处理你的文本验证。编写一个KeyListener并非易事,在某些情况下,你可能需要实现一个自定义软键盘。在安卓中,如果一个软键盘存在,定义了除TYPE_NULL之外输入类型的KeyListener可能根本不会调用其监听事件(onKeyDownonKeyUponKeyOther)。KeyListener的按键事件仅用于接受或拒绝来自硬件键盘的事件。软件键盘使用TextView的输入类型属性来决定应向用户提供哪些功能。

自动完成文本输入

SpinnerListView控件是让用户从预定义选项列表中选择的好方法。然而,它们的主要缺点是不适合非常长的列表。尽管实现和性能都很好,用户只是不喜欢查看大量数据列表。解决这个问题的标准方法是提供一个自动完成的文本输入控件。

自动完成文本输入

带有自动完成功能的输入控件也常与用户过去提供的选项历史一起使用,或者建议用户可能想要“完成”输入的可能方式。安卓的AutoCompleteTextView控件是一个带有自动完成功能的EditText。它使用一个ListAdapter(也必须实现Filterable接口)来查找并显示可能的建议列表给用户。

然而,AutoCompleteTextView存在两个主要缺陷:

  • 它仍然是一个TextView,并且用户并不需要选择建议项之一,这意味着它的内容必须单独验证。

  • 提示列表直接显示在小部件下方,占用了相当大的屏幕空间。结合软键盘输入,用户界面可能会在小屏幕上变得杂乱无章或几乎无法使用。

通过谨慎和适度地使用AutoCompleteTextView类,可以解决这两个问题。当你需要一个搜索框、URL 输入或类似的东西时,它们非常有用,但它们通常不适合放在屏幕中间(最好放在顶部,这样它们有足够的空间显示提示列表)。

小测验

  1. KeyboardListener中的onKeyDown事件何时被调用?

    1. 当广播系统范围内的按键按下事件时

    2. 取决于系统是否有硬件键盘

    3. 当按下硬件键盘按键时

    4. 当按下硬件接口控制按钮之一时

  2. 你何时会使用Toast通知用户验证错误?

    1. 当他们犯了一个错误(也就是说,勾选了不应该勾选的复选框)

    2. 当他们从无效小部件上移开焦点后

    3. 在从外部服务接收到验证错误之后

  3. 在一个即时通讯(IM)应用中,如果用户的其中一个联系人下线了,你如何更新联系人ListView以反映这一变化?

    1. ListView中图形化地禁用用户图标,并将其移动到ListView底部

    2. ListView中移除用户

    3. ListView中禁用用户的图标

为结果构建活动

有时候,Android 中的默认小部件单独无法满足你的输入需求,你需要某种复合输入结构。在这种情况下,你可以创建一个Dialog小部件,或者构建一个新的Activity。当Dialog小部件的内容保持简短(最多两到三行小部件)时,它们非常有用,因为它们在视觉上保持在当前Activity的顶部。然而,这意味着它们会消耗额外的资源(因为它们的调用Activity不能被换到后台),并且由于它们有自己的装饰,它们没有像Activity那样多的可用屏幕空间。

在第四章,利用活动和意图中,我们讨论了Activity类将数据返回给调用者的概念。当你需要某种额外的验证形式或想要隔离特定的输入小部件(或小部件组)时,这是一个很好的技术。你可以在Activity.setResult方法中指定一些结果数据。通常,一个Activity只需指定成功或失败的结果(使用RESULT_OKRESULT_CANCELLED常量)。也可以通过填充Intent来返回数据:

Intent result = new Intent();
result.putExtra("paymentDetails", paymentDetails);
setResult(RESULT_OK, result);

当你调用finish()方法时,Intent数据会被传递给父Activity对象的onActivityResult方法,以及结果代码。

通用筛选搜索 Activity

正如本章前面所讨论的,有时你有一个预定义的对象列表,并希望用户选择其中一个。这个列表对于用户来说太大,无法滚动浏览(例如,世界上所有国家的列表),但它也是一个定义好的列表,所以你不希望他们能够选择自由文本。

在这种情况下,一个可过滤的ListView通常是最合适的选择。尽管ListView类具有过滤功能,但在没有硬件键盘的设备上,它工作得并不是很好(如果有的话)。因此,利用EditText小部件让用户过滤ListView的内容是明智的。

这种需求非常常见,因此在本节中,我们将研究构建一个几乎完全通用的Activity,用于过滤和选择数据。这个例子将为用户提供两种显示数据的方式。一种是通过Cursor,另一种是通过简单的Object数组。在这两种情况下,过滤ListView的任务都留给ListAdapter实现,使得实现相对简单。

动手时间——创建ListItemSelectionActivity

这是一个相当大且有些复杂的例子,因此我会将其分解成易于消化的部分,每个部分都有一个目标。我们首先想要的是一个具有美观布局的Activity类。我们将构建的布局是一个EditText在上,一个ListView在下,每个都有可以被Activity使用的 ID。

  1. 创建一个新项目来包含你的ListItemSelectionActivity类:

    android create project -n Selector -p Selector -k com.packtpub.selector -a ListItemSelectionActivity -t 3
    
  2. 在编辑器或 IDE 中打开res/layout/main.xml文件。

  3. 移除任何默认的布局代码。

  4. 确保根元素是一个在Activity中占用可用屏幕空间的LinearLayout

    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">"
    
  5. 在根元素内部,声明一个 ID 为inputinputTypetextFilterEditText,以表示它将过滤另一个小部件的内容:

    <EditText android:id="@+id/input"
              android:inputType="textFilter"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  6. EditText之后,我们声明一个ListView,它将占用剩余的空间:

    <ListView android:id="@+id/list"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"/>
    
  7. 在编辑器或 IDE 中打开ListItemSelectionActivity Java 源文件。

  8. 在类的顶部声明一个ListAdapter字段:

    private ListAdapter adapter;
    
  9. ListAdapter字段之后,声明一个Filter字段:

    private Filter filter;
    
  10. onCreate方法中,确保你将main.xml加载为ListItemSelectionActivity的内容视图:

    setContentView(R.layout.main);
    
  11. 然后获取在 XML 文件中声明的ListView,以供我们稍后使用:

    ListView list = (ListView)findViewById(R.id.list);
    
  12. 最后,获取在 XML 文件中声明的EditText,以供我们稍后使用:

    EditText input = (EditText)findViewById(R.id.input);
    

刚才发生了什么?

现在你已经得到了ListItemSelectionActivity类的框架。此时应用程序能够运行,向你展示一个空的ListView和一个EditText。稍后阶段将使用类顶部声明的ListAdapterFilter字段来保存列表信息,并过滤屏幕上可见的内容。

动手时间——创建一个ArrayAdapter

ListItemSelectionActivity类将接受来自两个不同来源的列表内容。你可以指定一个数据库查询Uri,用于从外部源选择两列,或者可以在Intent对象中指定一个Object数组作为额外数据。对于下一个任务,我们将编写一个私有实用方法,从Intent对象创建一个ArrayAdapter

  1. 在编辑器或 IDE 中打开ListItemSelectionActivity的 Java 源文件。

  2. 声明一个新的实用方法,用于为Intent创建ListAdapter

    private ListAdapter createArrayAdapter(Intent intent) {
    
  3. Intent的额外数据中获取Object数组:

    Object[] data = (Object[])intent.getSerializableExtra("data");
    
  4. 如果数组不为null且不为空,返回一个新的ArrayAdapter对象,该对象将显示数组内容在 Android 定义的标准列表项资源中:

    if(data != null && data.length > 0) {
    return new ArrayAdapter<Object>(
        this,
        android.R.layout.simple_list_item_1,
        data);
    
  5. 如果数组为null或为空,抛出IllegalArgumentException异常:

    else {
        throw new IllegalArgumentException(
                "no list data specified in Intent: "
                + intent);
    }
    

刚才发生了什么?

你刚刚编写了一个非常基本的实用方法,从Intent中提取Object数组并返回它。如果数组不存在或为空,该方法会抛出IllegalArgumentException。这是一个有效的响应,因为我们在查找数据库查询之后会查找数组。如果我们没有从外部获取任何数据,那么这个Activity无法执行。让用户从空白列表中选择项目是没有意义的。

注意

请记住,这个Activity旨在由另一个Activity启动,而不是通过应用程序菜单直接由用户启动。因此,当Activity的使用方式不符合预期时,我们希望给自己或其他开发者提供有用的反馈。

动手操作——创建CursorAdapter

CursorAdapter的设置比ArrayAdapter复杂得多。一方面,CursorAdapter提供的选项比ArrayAdapter多。我们的CursorAdapter可以根据指定一列或两列来显示单行或双行列表项。尽管ArrayAdapter包含一些默认的过滤逻辑,但我们需要为CursorAdapter提供更多的支持。

  1. 首先,我们允许使用两种不同的列命名约定,并附带一些默认值。声明一个实用方法,从Intent中查找预期的列名:

    private String getColumnName(
            final Intent intent,
            String primary,
            String secondary,
            String def) {
    
  2. 首先,尝试使用primary属性名获取列名:

    String col = intent.getStringExtra(primary);
    
  3. 如果列名为null,尝试使用secondary属性名:

    if(col == null) {
        col = intent.getStringExtra(secondary);
    }
    
  4. 如果列名仍然是null,使用默认值:

    if(col == null) {
        col = def;
    }
    
  5. 返回列名:

    return col;
    
  6. 现在,声明另一个实用方法,该方法将创建实际的CursorAdapter,以便在ListView中使用:

    private ListAdapter createCursorAdapter(Intent intent) {
    
  7. 查找要显示的第一列的名称:

    final String line1 = getColumnName(intent, "name", "line1", "name");
    
  8. 查找要显示的可选第二列的名称:

    String line2 = getColumnName(
            intent, "description", "line2", null);
    
  9. 现在我们有两种可能的路径——单行列表项或双行列表项。它们的构建非常相似,因此我们声明一些变量来保存这两条路径之间的不同值:

    int listItemResource;
    final String[] columns;
    String[] displayColumns;
    int[] textIds;
    
  10. 如果已指定line2列名,我们使用以下代码:

    if(line2 != null) {
    
  11. 我们将使用一个两行列表项资源:

    listItemResource = android.R.layout.two_line_list_item;
    
  12. 数据库查询需要选择_id列以及Intent中指定的两列:

    columns = new String[]{"_id", line1, line2};
    
  13. 然而,列表项将只显示两个指定的列:

    displayColumns = new String[]{line1, line2};
    
  14. CursorAdapter需要知道在two_line_list_item资源中声明的TextView小部件的资源 ID:

    textIds = new int[]{android.R.id.text1, android.R.id.text2};
    
  15. 如果在Intent中没有指定第二列的名称,则ListView应该有单行项目:

    else {
    listItemResource = android.R.layout.simple_list_item_1;
    
  16. 我们只需要请求_id列和单个列名:

    columns = new String[]{"_id", line1};
    
  17. 列表中的项目应该包含请求列的内容:

    displayColumns = new String[]{line1};
    
  18. 我们不需要告诉CursorAdapter在单行列表项资源中查找哪个小部件 ID:

    textIds = null;
    
  19. else子句之后,我们将拥有所需的变量填充。我们可以运行初始的数据库查询并获得数据的完整列表以展示给用户:

    Cursor cursor = managedQuery(
            intent.getData(),
            columns,
            null,
            null,
            line1);
    
  20. 我们现在可以创建CursorAdapter来包装数据库Cursor对象,供ListView使用。我们使用SimpleCursorAdapter的实现:

    CursorAdapter cursorAdapter = new SimpleCursorAdapter(
            this,
            listItemResource,
            cursor,
            displayColumns,
            textIds);
    
  21. 为了让用户过滤列表,我们需要给CursorAdapter一个FilterQueryProvider。将FilterQueryProvider声明为一个匿名内部类:

    cursorAdapter.setFilterQueryProvider(
            new FilterQueryProvider() {
    
  22. 在匿名FilterQueryProvider内部,声明runQuery方法,该方法将在用户每次按键时被调用:

    public Cursor runQuery(CharSequence constraint) {
    
  23. 我们可以返回一个managedQuery,它只对我们将在ListView中渲染的第一列执行 SQL LIKE操作:

    return managedQuery(
            intent.getData(),
            columns,
            line1 + " LIKE ?",
            new String[] {constraint.toString() + '%'},
            line1);
    
  24. 最后,createCursorAdapter方法可以返回CursorAdapter

    return cursorAdapter;
    

刚才发生了什么?

这个实用方法处理在Intent中指定了查询Uri时创建CursorAdapter的情况。这种结构允许对非常大的数据集进行过滤,因为它通常是建立在 SQL Lite 数据库之上的。其性能与它将查询的数据库表结构直接相关。

由于数据库查询可能非常大,CursorAdapter类本身不执行任何数据集过滤。相反,您需要实现FilterQueryProvider接口,为每次过滤更改创建并运行新的查询。在上述示例中,我们创建了一个与默认Cursor完全相同的Cursor,但我们为查询添加了selectionselectionArgs。这个LIKE子句将告诉 SQL Lite 只返回以用户输入的过滤条件开头的行。

动手时间——设置ListView

现在我们有了创建此Activity可以过滤的两种类型ListAdapter的实现。现在我们需要一个实用方法来确定使用哪一个并返回它;然后我们希望使用新的实用方法在ListView小部件上设置ListAdapter

  1. 声明一个新方法来创建所需的ListAdapter对象:

    protected ListAdapter createListAdapter() {
    
  2. 获取用于启动ActivityIntent对象:

    Intent intent = getIntent();
    
  3. 如果Intent中的数据Uri不为null,则返回给定IntentCursorAdapter。否则,返回给定IntentArrayAdapter

    if(intent.getData() != null) {
    return createCursorAdapter(intent);
    
    else {
        return createArrayAdapter(intent);
    }
    
  4. onCreate方法中,从布局中找到两个View对象之后,使用新的实用方法创建所需的ListAdapter

    adapter = createListAdapter();
    
  5. Filter字段分配给ListAdapter给出的Filter

    filter = ((Filterable)adapter).getFilter();
    
  6. ListView上设置ListAdapter

    list.setAdapter(adapter);
    

刚才发生了什么?

这段代码现在引用了创建的ListAdapter对象及其配合使用的Filter。如果你现在运行应用程序,会发现打开时会弹出强制关闭对话框。这是因为现在代码需要某种数据来填充ListView。虽然对于一个正常的应用程序来说这并不理想,但这实际上是一个可重用的组件,可以在多种情况下使用。

行动时间——过滤列表

尽管代码已经设置好了显示列表,甚至可以过滤它,但我们还没有将EditText框与ListView关联起来,因此在EditText中输入目前将完全不起作用。我们需要监听EditText框的变化,并根据输入的内容请求过滤ListView。这将涉及ListItemSelectionActivity类监听EditText上的事件,然后请求Filter对象缩小可用的项目集合。

  1. 应该让ListItemSelectionActivity实现TextWatcher接口:

    public class ListItemSelectionActivity extends Activity
            implements TextWatcher
    
  2. onCreate方法中在ListView上设置ListAdapter后,将ListItemSelectionActivity作为TextWatcher添加到EditText组件上:

    input.addTextChangedListener(this);
    
  3. 你需要声明beforeTextChangedonTextChanged方法的空实现,因为我们实际上并不关心这些事件:

    public void beforeTextChanged(
            CharSequence s,
            int start,
            int count,
            int after) {
    }
    
    public void onTextChanged(
            CharSequence s,
            int start,
            int count,
            int after) {
    }
    
  4. 然后声明我们感兴趣的afterTextChanged方法:

    public void afterTextChanged(Editable s) {
    
  5. afterTextChanged方法中,我们只需请求当前ListAdapterFilter过滤ListView

    filter.filter(s);
    

刚才发生了什么?

TextWatcher接口用于追踪TextView组件的变化。实现该接口可以监听到TextView实际内容的任何改变,无论这些改变来自何处。尽管OnKeyListenerKeyboardListener接口主要用于处理硬件键盘事件,但TextWatcher可以处理来自硬件键盘、软键盘甚至内部调用TextView.setText的变化。

行动时间——返回选择项

ListItemSelectionActivity现在可以用来显示可能的条目列表,并通过在ListView上方的EditText中输入来过滤它们。然而,我们还没有办法让用户从ListView中实际选择一个选项,以便将其传递回我们的父Activity。这只需要实现一个简单的OnItemClickListener接口。

  1. ListItemSelectionActivity类现在需要实现OnItemClickListener接口:

    public class ListItemSelectionActivity extends Activity
            implements TextWatcher, OnItemClickListener {
    
  2. onCreate方法中注册为TextWatcher之后,在ListView上注册为OnItemClickListener

    list.setOnItemClickListener(this);
    
  3. 重写onItemClick方法以监听用户的选择:

    public void onItemClick(
            AdapterView<?> parent,
            View clicked,
            int position,
            long id) {
    
  4. 创建一个空的Intent对象,以便传回我们的父Activity

    Intent data = new Intent();
    
  5. 如果ListAdapterCursorAdapter,传递给onItemClickid将是选择的数据的数据库_id列值。将这个值添加到Intent中:

    if(adapter instanceof CursorAdapter) {
    data.putExtra("selection", id);
    
  6. 如果ListAdapter不是CursorAdapter,我们将实际选择的Object添加到Intent中:

    else {
        data.putExtra(
                "selection",
                (Serializable)parent.getItemAtPosition(position));
    }
    
  7. 将结果代码设置为RESULT_OK,并将Intent传回:

    setResult(RESULT_OK, data);
    
  8. 用户已经做出了他们的选择,所以这部分我们已经完成了:

    finish();
    

刚才发生了什么?

ListItemSelectionActivity现在已完成并准备使用。它提供了与AutoCompleteTextView非常相似的功能,但作为一个独立的Activity,它为用户提供了更大的建议列表,并且用户必须从ListView中选择一个项目,而不是简单地输入他们的数据。

使用 ListItemSelectionActivity

您需要指定用户要从哪个数据中选择,这是启动ListItemSelectionActivityIntent的一部分。如已经讨论过的,实际上有两种路径:

  • 传入某种类型的数组(非常适合在您自己的应用程序中使用)

  • 提供一个数据库查询Uri以及您想要显示的列名(如果您想从另一个应用程序中使用它,这非常方便)

由于ListItemSelectionActivity返回其选择(如果它不这样做,那就没有多大用处),因此您需要使用startActivityForResult方法而不是正常的startActivity方法来启动它。如果您想传递一个String对象数组供选择,可以使用类似于以下意图的代码:new Intent(this, ListItemSelectionActivity.class)

intent.putExtra("data", new String[] {
    "Blue",
    "Green",
    "Red",// more colors    
});
startActivityForResult(intent, 101);

如果上述data数组中有足够的颜色,您将看到一个可以按用户所需颜色进行筛选的ListItemSelectionActivity屏幕。以下是结果屏幕外观的截图:

使用 ListItemSelectionActivity

为了从ListItemSelectionActivity接收结果,您需要在onActivityResult方法中监听结果(如第四章所述,利用活动和意图)。例如,如果您只是想Toast确认选择的结果,可以使用以下代码:

@Override
protected void onActivityResult(
        int requestCode,
        int resultCode,
        Intent data) {

    super.onActivityResult(requestCode, resultCode, data);

    if(requestCode == 101 && resultCode == RESULT_OK) {
        Object obj = data.getSerializableExtra("selection");
        Toast.makeText(
                this,
                String.valueOf(obj),
                Toast.LENGTH_LONG).show();
    }
}

最后,您会如何在ListItemSelectionActivity中使用数据库查询呢?这非常容易展示,可能是ListItemSelectionActivity最激动人心的功能。以下代码段将允许用户从他们的电话簿中选择一个联系人:

Intent intent = new Intent(
        this,
        ListItemSelectionActivity.class);

intent.setData(People.CONTENT_URI);
intent.putExtra("line1", People.NAME);
intent.putExtra("line2", People.NUMBER);

startActivityForResult(intent, 202);

动手试试吧!

ListItemSelectionActivity 可以过滤和选择几乎任何内容。尝试构建一个包含世界上所有国家(网上有许多这样的列表)的列表,然后创建一个 Activity,使用 ListItemSelectionActivity 让你选择其中一个。

总结

你如何接受用户的输入,以及如何验证这些输入,这在用户使用你的应用程序的整体体验中起着至关重要的作用。软件应该帮助用户,并在每一步告诉他们它需要什么。这不仅使应用程序更容易使用,而且还能让用户更快地操作。

使用 ListItemSelectionActivity 常常可以帮助用户浏览大量数据集,同时防止他们做出不想要或无效的选择。这是一种非常常用的控件类型,在许多不同的应用程序中以各种形式出现。目前,Android 没有一个通用的类能像这样轻松地执行这项任务。

在下一章中,我们将开始了解一种相当现代的用户反馈形式:动画。Android 不仅仅支持动画化用户界面的部分元素,还支持组合复杂的自定义动画。动画在用户享受应用程序的过程中起着至关重要的作用。这不仅仅因为它看起来很棒,还因为它提供了视觉线索,让用户了解应用程序当前正在做什么,以及他们的操作产生了什么效果。

第七章:动画小部件和布局

动画是现代应用程序用户界面设计的重要元素。然而,在设计中过度使用动画也是很容易的。在非游戏应用程序中使用动画的一般准则是——只对用户交互和通知进行动画处理,并保持动画时长简短,以免对用户体验产生负面影响。对于游戏来说,更多的动画通常是可以接受的(甚至可能是预期的)。

那么为什么动画要针对用户交互而不是(例如)应用程序的背景呢?一方面,动画化应用程序的背景是分散注意力的,如果你试图捕捉或向用户呈现重要信息,这是不专业的(无论它看起来多好)。关于通知,动画也非常重要。屏幕上的移动会吸引注意力,因此,通常需要一个大的弹出对话框可以被一个小型动画图标所替代。一个完美的例子就是在安卓设备的通知区域顶部左侧放置的“下载中”图标,当安卓市场应用程序正在下载新软件或更新时。

布局动画和过渡为用户提供了有用的状态信息。当使用屏幕过渡时,你告诉用户刚刚发生了什么,或者即将发生什么。不同的过渡对用户意味着不同的事件,了解每个不同活动应使用哪种过渡,将让用户知道即将采取哪种类型的动作。布局动画是用户反馈的重要组成部分,如果省略它们或在错误的地方使用错误的动画,可能会让用户感到烦躁或稍微有些困惑(“改变茫然”)。使用正确的动画将提升用户体验,甚至可以通过提供简短的提示,告诉用户接下来需要做什么,从而加快他们使用应用程序的速度。

在本章中,我们将重点介绍两种主要的动画类型——小部件动画和布局动画。我们将查看安卓提供的标准动画结构,并探讨如何创建新的动画类型和扩展现有类型。我们还将探讨动画的定时和“良好实践”使用,以及在不降低速度或分散注意力的前提下让用户保持愉悦。

使用标准的安卓动画

安卓中的任何ViewViewGroup对象都可以附加动画。动画通常在 XML 文件中定义为应用程序资源,安卓在android包中提供了一些有用的默认动画。同时,安卓还包含几个专门设计用来处理动画的View类。使用这些类时,你会发现它们具有布局属性,这些属性允许你为某些特定动作设置特定类型的动画。然而,通常在布局文件中并不指定动画,而是依赖 Java 代码来设置和启动Animation对象。

动画通常不作为布局 XML 的一部分来指定,原因非常简单——它们应该在何时运行?许多动画可以作为对用户输入的响应,让用户知道正在发生什么。大多数动画在某种程度上都会由用户的行为触发(除非它们用于通知)。因此,你需要指定两个内容:应该在哪个小部件上运行哪个动画,以及关于动画何时运行的信号。默认的 Android 动画会立即开始动画,而其他动画结构可能有一个预定延迟才会开始。

动手操作——动画新闻源

我们将从创建一个选择器Activity和一个简单的NewsFeedActivity开始。在新闻源中,我们将使用计时器使最新的新闻标题“进入和退出”。对于这个示例,我们将使用 Android 提供的一些默认动画,并通过布局资源主要驱动这个过程。

  1. 创建一个新项目,包含本章的动画示例,主Activity名为AnimationSelectionActivity

    android create project -n AnimationExamples -p AnimationExamples -k com.packtpub.animations -a AnimationSelector -t 3
    
  2. 在编辑器或 IDE 中打开res/layout/main.xml布局文件。

  3. 清除布局资源的默认内容。

  4. 声明一个消耗所有可用屏幕空间的垂直LinearLayout

    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  5. 创建一个标签为“新闻源”的Button,链接到第一个动画示例:

    <Button android:id="@+id/news_feed"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dip"
            android:text="News Feed"/>
    
  6. 创建一个名为news.xml的新布局资源文件。

  7. 声明一个垂直的LinearLayout,包含所有可用的屏幕空间:

    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">"
    
  8. LinearLayout添加一个TextSwitcher对象,指定默认的“滑动”动画作为“进入”和“退出”动画:

    <TextSwitcher
            android:id="@+id/news_feed"
            android:inAnimation="@android:anim/slide_in_left"
            android:outAnimation="@android:anim/slide_out_right"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:text=""/>
    
  9. 在编辑器或 IDE 中打开res/values/strings.xml文件。

  10. 声明一个名为headlines的字符串数组,包含一些模拟新闻标题的元素:

    <string-array name="headlines">
        <item>Pwnies found to inhabit Mars</item>
        <item>Geeks invent \"atoms\"</item>
        <item>Politician found not lying!</item>
        <!-- add some more items here if you like -->
    </string-array>
    
  11. 在生成的根包中,声明一个名为NewsFeedActivity.java的新 Java 源文件。

  12. 在你的AndroidManifest.xml文件中注册NewsFeedActivity类:

    <activity android:name=".NewsFeedActivity" android:label="News Feed" />
    
  13. 新类应继承Activity类并实现Runnable接口:

    public class NewsFeedActivity
            extends Activity implements Runnable {
    
  14. 声明一个Handler,用作改变标题的时间结构:

    private final Handler handler = new Handler();
    
  15. 我们需要引用TextSwitcher对象:

    private TextSwitcher newsFeed;
    
  16. 声明一个字符串数组,用于保存你添加到strings.xml文件中的模拟新闻标题:

    private String[] headlines;
    
  17. 你还需要跟踪当前正在显示的新闻标题:

    private int headlineIndex;
    
  18. 重写onCreate方法:

    protected void onCreate(final Bundle savedInstanceState) {
    
  19. 调用ActivityonCreate方法:

    super.onCreate(savedInstanceState);
    
  20. 将内容视图设置为news布局资源:

    setContentView(R.layout.news);
    
  21. strings.xml应用程序资源文件中存储对标题字符串数组的引用:

    headlines = getResources().getStringArray(R.array.headlines);
    
  22. 查找TextSwitcher小部件,并将其分配给之前声明的字段:

    newsFeed = (TextSwitcher)findViewById(R.id.news_feed);
    
  23. TextSwitcherViewFactory设置为一个新的匿名类,当被请求时创建TextView对象:

    newsFeed.setFactory(new ViewFactory() {
        public View makeView() {
            return new TextView(NewsFeedActivity.this);
        }
    });
    
  24. 重写onStart方法:

    protected void onStart() {
    
  25. 调用Activity类的onStart方法:

    super.onStart();
    
  26. 重置headlineIndex,以便我们从第一条新闻标题开始:

    headlineIndex = 0;
    
  27. 使用HandlerNewsFeedActivity作为延迟动作发布:

    handler.postDelayed(this, 3000);
    
  28. 重写onStop方法:

    protected void onStop() {
    
  29. 调用Activity类的onStop方法:

    super.onStop();
    
  30. 移除任何待处理的NewsFeedActivity调用:

    handler.removeCallbacks(this);
    
  31. 实现我们将用来切换到下一个标题的run方法:

    public void run() {
    
  32. 打开一个try块以交换内部标题:

  33. 使用TextSwitcher.setText方法切换到下一个标题:

    newsFeed.setText(headlines[headlineIndex++]);
    
  34. 如果headlineIndex超过了标题总数,将headlineIndex重置为零:

    if(headlineIndex >= headlines.length) {
        headlineIndex = 0;
    }
    
  35. 关闭try块,并添加一个finally块。在finally块中,将NewsFeedActivity重新发布到Handler队列中:

    finally {
        handler.postDelayed(this, 3000);
    }
    
  36. 在编辑器或 IDE 中打开自动生成的AnimationSelector Java 源文件。

  37. AnimationSelector类需要实现OnClickListener

    public class AnimationSelector
            extends Activity implements OnClickListener {
    
  38. onCreate方法中,确保将内容视图设置为之前创建的main布局资源:

    setContentView(R.layout.main);
    
  39. 找到声明的Button并将其OnClickListener设置为this

    ((Button)findViewById(R.id.news_feed)).
           setOnClickListener(this);
    
  40. 声明onClick方法:

    public void onClick(final View view) {
    
  41. 使用 switch 来判断点击了哪个View

    switch(view.getId()) {
    
  42. 如果是新闻源Button,则使用以下case

    case R.id.news_feed:
    
  43. 使用新的Intent启动NewsFeedActivity

    startActivity(new Intent(this, NewsFeedActivity.class));
    
  44. switch语句中断,从而完成onClick方法。

刚才发生了什么?

TextSwitcher是一个动画工具View的示例。在这种情况下,它是交换新闻标题的完美结构,一次显示一个标题并在每段文本之间动画过渡。TextSwitcher对象创建两个TextView对象(使用匿名ViewFactory类)。当你使用setText方法时,TextSwitcher会改变“离屏”TextView的文本,并在“在屏”TextView和“离屏”TextView之间动画过渡(显示新的文本内容)。

TextSwitcher类要求你为其指定两个动画资源以创建过渡效果:

  • 将文本动画移到屏幕上

  • 将文本动画移出屏幕

在前一个示例中,我们使用了默认的slide_in_leftslide_out_right动画。这两个都是基于平移动画的示例,因为它们实际上改变了TextView对象的“在屏”位置以产生效果。

使用 flipper 和 switcher 小部件

本章的第一个示例使用了TextSwitcher类,这是标准 Android API 中的一个动画View类。还有其他几个动画工具类,你可能之前遇到过(比如ImageSwitcher)。TextSwitcherImageSwitcher都是相关类,并且都继承自更通用的ViewSwitcher类。

ViewSwitcher类是一个通用的动画工具,并定义了我们在前一个示例中匿名实现的ViewFactory接口。ViewSwitcher是一个只包含两个子View对象的ViewGroup。一个在屏幕上显示,另一个隐藏。getNext实用方法允许你找出哪个是“离屏”的View对象。

虽然你通常使用ViewFactory来填充ViewSwitcher,但你也可以选择手动填充。例如,你可以通过继承自ViewGroupaddView方法,为TextSwitcher添加内容。

使用翻转和切换小部件

使用 ImageSwitcher 和 TextSwitcher 的实现

ImageSwitcherTextSwitcher类是ViewSwitcher的专业实现,它们了解所包含的View对象的类型。当你调用TextSwitcher对象的setText方法时,它类似于在包含两个TextView子项的ViewSwitcher上调用以下代码片段:

((TextView)switcher.getNext()).setText("Next text to display");
switcher.showNext();

TextSwitcher可用于显示内容,如(示例中的)新闻源,或像 Android 通知区域一样,显示不适合单行显示的文本内容。当动画使文本向上运行时,在TextSwitcher中显示多行特别有效,这会使文本看起来在TextSwitcher对象后面向上滚动。

ImageSwitcher通常用于画廊、幻灯片或类似结构中。你也可以使用ImageSwitcher让用户从一组小图片中选择,例如,选择登录头像的简短列表。

动手英雄 - 填充 TextSwitcher

在新闻源示例中,除了使用ViewFactory填充TextSwitcher外,还可以尝试在 XML 布局资源中填充。记住,它需要正好两个TextView子部件。如果做对了,尝试给两个TextView对象设置不同的字体颜色和样式。

动画布局小部件

使用如TextSwitcherImageSwitcher这样的动画工具小部件,可以让你随着时间的推移显示比一次能容纳在屏幕上的更多信息。通过LayoutAnimationController类,ViewGroup对象也可以在不进行重大修改的情况下进行动画处理。然而,在这种情况下,需要在你的 Java 代码中添加动画。

LayoutAnimationController最适合用于创建ViewGroup出现或即将从屏幕消失时的“进入”或“退出”效果。控制器只需在指定ViewGroup的每个View子项上启动一个指定的动画。然而,它不必同时进行,或按顺序进行。你可以轻松地配置LayoutAnimationController,使每个子部件动画开始之间有一小段延迟,从而产生交错效果。

如果正确应用于LinearLayout,你可以实现与以下图表类似的效果:

动画布局小部件

动手操作时间 - 动画化 GridView

GridView类拥有自己的LayoutAnimationController,专门设计用来以行和列的形式动画化它,可以实现比标准LayoutAnimationController更复杂的效果。在“动画”示例的下一部分,我们将使用GridView构建一个可爱的颜色选择器。当选择器首次出现在屏幕上时,每个颜色样本将从左上角开始淡入,直至右下角结束。

  1. 首先,在项目的根包中声明一个新的 Java 源文件,命名为ColorAdapter.java,它将为GridView生成颜色样本。

  2. ColorAdapter需要扩展BaseAdapter以处理Adapter的样板要求:

    public class ColorAdapter extends BaseAdapter {
    
  3. ColorAdapter将被创建,并指定行数和列数,这些数字将在GridView上显示:

    private final int rows;
    private final int cols;
    
    public ColorAdapter(int rows, int cols) {
        this.rows = rows;
        this.cols = cols;
    }
    
  4. ColorAdapter将提供的项目数是行数乘以列数:

    public int getCount()
        return rows * cols;
    }
    
  5. 颜色的 ID 是它所在的位置或索引:

    public long getItemId(int pos) {
        return pos;
    }
    
  6. 我们使用一个实用方法从“列表”中的索引组合颜色。对于这个函数,我们利用了 Android Color类中的HSVtoRGB方法:

    private int getColor(int pos) {
        float h = (float)pos / (float)getCount();
        return Color.HSVToColor(new float[]{h * 360f, 1f, 1f});
    }
    
  7. 适配器模型中索引处的项目作为其颜色值返回:

    public Object getItem(int pos) {
        return getColor(pos);
    }
    
  8. 为了创建颜色样本View对象,我们像平常一样实现AdaptergetView方法:

    public View getView(int pos, View reuse, ViewGroup parent) {
    
  9. 我们返回的View将是一个ImageView对象,因此我们要么复用父控件提供的对象,要么创建一个新的:

    ImageView view = reuse instanceof ImageView
            ? (ImageView)reuse
            : new ImageView(parent.getContext());
    
  10. 我们利用ColorDrawable类用我们的getColor实用方法指定的颜色填充ImageView

    view.setImageDrawable(new ColorDrawable(getColor(pos)));
    
  11. ImageView需要设置其android.widget.AbsListView.LayoutParams,然后才能返回给GridView进行显示:

    view.setLayoutParams(new LayoutParams(16, 16));
    return view;
    
  12. 创建一个新的 XML 布局资源文件,名为res/layout/colors.xml,以保存将作为颜色选择器的GridView的声明。

  13. colors.xml布局文件的内容仅包含一个GridView小部件:

    <GridView
    
        android:id="@+id/colors"
        android:verticalSpacing="5dip"
        android:horizontalSpacing="5dip"
        android:stretchMode="columnWidth"
        android:gravity="center"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />
    
  14. 在你的AnimationExamples项目的根包中定义另一个新的 Java 源文件。将这个命名为ColorSelectorActivity.java

  15. 新的类声明应该扩展Activity

    public class ColorSelectorActivity extends Activity {
    
  16. 正常重写onCreate方法,并将内容视图设置为刚刚编写的colors XML 布局资源:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.colors);
    
  17. 现在你可以使用android.view.animation包中的便捷AnimationUtils类加载默认的 Android“淡入”动画:

    Animation animation = AnimationUtils.loadAnimation(
            this, android.R.anim.fade_in);
    
  18. 为了正确地动画化GridView,你需要实例化一个新的GridLayoutAnimationController对象,并传递给它“淡入”动画:

    GridLayoutAnimationController animationController =
            new GridLayoutAnimationController(
            animation, 0.2f, 0.2f);
    
  19. 现在查找你在colors.xml文件中声明的GridView

    GridView view = (GridView)findViewById(R.id.colors);
    
  20. GridView中的列数设置为10(注意我们并没有在 XML 布局资源中这样做,尽管通常你会这样做):

    view.setNumColumns(10);
    
  21. 当你将GridView的适配器设置为ColorAdapter时,你还需要知道列数,最简单的方法是在 Java 中同时保持这两个值:

    view.setAdapter(new ColorAdapter(10, 10));
    
  22. 现在view对象已经准备好使用GridLayoutAnimationController了:

    view.setLayoutAnimation(animationController);
    
  23. 为了在屏幕显示时开始动画,我们重写了onStart方法。在这里,我们再次查找GridView并开始动画:

    protected void onStart() {
        super.onStart();
        ((GridView)findViewById(R.id.colors)).
                getLayoutAnimation().start();
    }
    
  24. 为了将这个新示例与其它动画示例整合,你需要在一个编辑器或 IDE 中打开res/layout/main.xml文件。

  25. LinearLayout的末尾添加一个新的Button,我们将使用它来启动颜色选择示例:

    <Button android:id="@+id/colors"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dip"
            android:text="Color Selector" />
    
  26. 在你的编辑器或 IDE 中打开AnimationSelector源文件。

  27. 设置了news_feed ButtonOnClickListener之后,以同样的方式找到并设置新的colors ButtonOnClickListener

    ((Button)findViewById(R.id.colors)).setOnClickListener(this);
    
  28. onClick方法中,在news_feed Buttonswitch case之后,为新的colors Button添加另一个switch case,并启动ColorSelectorActivity

    case R.id.colors:
        startActivity(new Intent(this, ColorSelectorActivity.class));
        break;
    
  29. 在你的编辑器或 IDE 中打开AndroidManifest.xml文件。

  30. <application>部分的底部,注册新的ColorSelectorActivity

    <activity android:name=".ColorSelectorActivity"
              android:label="Your Favorite Color" />
    

刚才发生了什么?

新示例使用了GridLayoutAnimationController,在上一动画开始后的几分之一秒内开始每个“淡入”动画。这创建了一个流畅的动画效果,颜色样本从屏幕左上角到右下角出现。

当你实例化一个GridLayoutAnimationController时,它需要你提供动画以及两个参数,这两个参数表示开始下一行或下一列动画之间的时间间隔。所给的延迟不是以“直接”时间格式指定,而是由给定动画完成所需的时间决定。在我们的例子中,如果动画需要一秒钟来完成,每个动画开始之间的延迟将是 200 毫秒,因为延迟被指定为0.2

我们在Activity一变为可见状态时对色块进行动画处理,实际上这成为了一个过渡动画,向用户介绍这个新屏幕。对于这类动画,尽可能缩短时间同时提供一个令人愉悦的介绍是至关重要的。当你运行这个新示例时,你应该会得到与以下图片中展示的动画相似的动画效果:

刚才发生了什么?

创建自定义动画

到目前为止,我们已经探索了使用 Android 的默认动画与普通小部件,但是如果是将自定义动画应用于一个不是为动画设计的小部件呢?Android 支持四种基本动画类型,可以应用于View对象:

  • 平移/移动

  • 旋转

  • 缩放

  • 透明度/Alpha

这些不同的动画结构可以单独应用,或者在一个动画集合中合并在一起,任意三种组合都可以同时运行。通过在动画开始前设置延迟时间,你可以通过简单的动画集合一个接一个地创建复杂的动画。

与 Android 中的许多事物一样,创建自定义动画的最简单方法是在资源 XML 文件中定义它。Android 使用的动画格式中的元素直接对应于android.animation.view包中的类。动画文件还可以引用其他动画资源中的动画,这使得组合复杂动画和复用简单动画变得更加容易。

动手实践——编写自定义动画

编写自定义动画非常简单,但并不完全直观。在本节中,你将定义一个自定义动画,它将使动画组件的大小增加五倍,同时逐渐淡出直至完全透明。

  1. 创建一个名为res/anim/vanish.xml的新 XML 资源文件,并在编辑器或 IDE 中打开它。

  2. 动画文件的根元素将是一个动画set元素:

    <set >
    
  3. <set>元素中,声明一个元素来定义缩放动画:

    <scale />
    
  4. 缩放动画的持续时间需要设置为300毫秒:

    android:duration="300"
    
  5. 动画从原始大小开始缩放:

    android:fromXScale="1.0"
    android:fromYScale="1.0"
    
  6. 缩放动画需要将大小增加5.0倍:

    android:toXScale="5.0"
    android:toYScale="5.0"
    
  7. 我们希望缩放效果从组件的中心向外扩展:

    android:pivotX="50%"
    android:pivotY="50%"
    
  8. <scale>元素的最后一部分定义了动画的加速曲线。在这里,我们希望缩放效果在运行时减速:

    android:interpolator="@android:anim/decelerate_interpolator"
    
  9. 接下来,定义一个新元素来处理动画的淡出部分:

    <alpha />
    
  10. 淡出动画的持续时间也是300毫秒:

    android:duration="300"
    
  11. 我们从没有透明度开始:

    android:fromAlpha="1.0"
    
  12. 淡出效果以组件完全不可见结束:

    android:toAlpha="0.0"
    
  13. 淡出效果应该随着运行而加速,因此我们使用了加速插值器:

    android:interpolator="@android:anim/accelerate_interpolator"
    

刚才发生了什么?

这是一个相对简单的动画集合,但其效果视觉效果令人满意。将动画保持在300毫秒内,足够快,不会干扰用户的交互,但又足够长,能让用户完全看到。

<set>元素中定义动画时,每个非集合子动画都需要定义其duration<set>元素没有它自己的duration的概念。然而,你可以为整个集合定义一个单一的interpolator来共享。

<scale>动画默认会使用左上角作为"轴心"点来缩放组件,导致组件向右和向下增长,而不是向左和向上。这会造成一边倒的动画效果,看起来并不吸引人。在上一个示例中,缩放动画以动画组件的中心作为轴心点运行。

刚才发生了什么?

动手实践——让一个按钮消失

那么我们如何将这个漂亮的光泽动画应用于 Button 对象呢?Button 对象没有动画属性,因此你不能直接从布局资源文件中引用它。我们想要的是当点击 Button 控件时运行动画。

  1. 创建一个名为 res/layout/vanish.xml 的新布局资源文件,并在编辑器或 IDE 中打开。

  2. 在新布局的根元素中,声明一个 RelativeLayout 元素:

    <RelativeLayout
    
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  3. Button 需要足够大,并在屏幕上居中。为此,我们给它一些内边距:

    <Button android:id="@+id/vanish"
            android:paddingTop="20dip"
            android:paddingBottom="20dip"
            android:paddingLeft="60dip"
            android:paddingRight="60dip"
            android:layout_centerInParent="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Vanish" />
    
  4. AnimationExamples 项目的根包中创建一个名为 VanishingButtonActivity.java 的新 Java 源文件。

  5. 新类需要扩展 Activity 并实现 OnClickListener 接口:

    public class VanishingButtonActivity extends Activity
            implements OnClickListener {
    
  6. 重写 onCreate 方法并调用 Activity.onCreate 方法以执行所需的 Android 设置:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
  7. 将内容视图设置为新的 vanish 布局资源:

    setContentView(R.layout.vanish);
    
  8. 在 XML 布局资源中找到声明的 Button 控件并设置其 OnClickListener

    Button button = (Button)findViewById(R.id.vanish);
    button.setOnClickListener(this);
    
  9. 实现 OnClickListeneronClick 方法:

    public void onClick(View clicked) {
    
  10. 从资源文件中加载 Animation

    Animation vanish = AnimationUtils.loadAnimation(
            this, R.anim.vanish);
    
  11. Button 对象上启动 Animation

    findViewById(R.id.vanish).startAnimation(vanish);
    
  12. 在编辑器或 IDE 中打开 AndroidManifest.xml 文件。

  13. <application> 部分的末尾,使用显示标签声明 VanishingButtonActivity

    <activity android:name=".VanishingButtonActivity"
              android:label="Vanishing Button" />
    
  14. 在编辑器或 IDE 中打开 res/layout/main.xml 布局资源。

  15. LinearLayout 的末尾添加一个新的 Button 以激活 VanishingButtonActivity

    <Button android:id="@+id/vanish"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dip"
            android:text="Vanishing Button" />
    
  16. 在编辑器或 IDE 中打开 AnimationSelector Java 源文件。

  17. onCreate 方法的末尾,从布局中获取新的 vanish Button 并设置其 OnClickListener

    ((Button)findViewById(R.id.vanish)).setOnClickListener(this);
    
  18. onClick 方法中,添加一个新的 switch case 以启动 VanishingButtonActivity

    case R.id.vanish:
        startActivity(new Intent(
            this, VanishingButtonActivity.class));
        break;
    

刚才发生了什么?

前述示例的添加将在屏幕中央显示一个单独的 Button。点击后,Button 将被 vanish 动画改变 300 毫秒。完成时,动画将不再对 Button 产生任何影响。这是动画的一个重要特点——当它们完成时,它们动画化的控件将返回到原始状态。

还需要注意到,被动画修改的不是控件本身,而是它所绘制的 Canvas 的状态。这与在 Java AWT 或 Swing 中修改 GraphicsGraphics2D 对象的状态的概念相同,在控件使用 Graphics 对象绘制自身之前。

在以下图片中,你可以看到当点击 Button 时动画对其产生的影响。实际上,Button 在动画的每一帧都会重新绘制,并且在那个时间保持完全活跃。

刚才发生了什么?

总结

在本章中,我们探讨了将动画应用于用户界面各个部分的各种方法。我们研究了某些小部件是如何设计来自我动画化的,布局可以为了进出Activity而进行动画过渡。

安卓资源中默认提供了一些简单的动画,但最终,手动创建自己的动画并将它们应用到用户界面上,无疑会为用户带来最视觉吸引人且用户友好的体验。

移动设备上的许多应用程序需要在屏幕上展示大量信息,并以易于吸收的方式呈现。在下一章中,我们将探讨关于向用户友好且有用地展示信息方面的用户界面设计。这使得用户能够以快速简便的方式尽可能快地访问他们所需的信息,同时不限制他们所能获取的信息量。

第八章:设计内容中心式活动

当您需要向用户展示大量数据,并且需要一个内容展示Activity时,通常这类 Activity 会变成以内容为中心的。内容中心式Activity的主要目的是在不过度压倒用户的情况下向用户提供尽可能多的信息。这是执行某种搜索或展示任何类型专业信息的应用程序的一个常见要求。

购物及相关电子商务应用是内容中心式应用的一个理想示例。在设计过程中,大部分努力都致力于展示有关在售产品的信息。如果用户无法找到他们想要的产品信息,他们就会去其他地方寻找。因此,产品展示不仅要吸引人、易于使用,还必须尽可能提供更多信息,同时避免晦涩难懂或杂乱无章。

另一个内容中心式布局的示例是社交网络应用中的用户个人资料页面。人们通常有很多关于自己的话要说,如果没有,其他人也经常会说很多关于他们的话。这些应用不仅需要向用户展示大量信息,而且信息的质量和相关性也大不相同。仅仅因为一个用户认为某件事很重要,并不意味着下一个人也会这么认为。在这些情况下,拥有一个可以根据用户偏好定制的界面(通常只需重新组织信息展示的顺序)也非常重要,同时还能吸引用户的注意力到他们可能感兴趣的新信息或区域。

一个吸引用户注意的好方法的绝佳示例可以在聊天应用程序中看到。如果用户向上滚动,他/她可能正在阅读几分钟前说过的内容。如果此时收到一条新消息,直接将他们滚动到新消息是非常不礼貌的,因为他们可能还在阅读旧消息。用音频提示通知他们有新消息是一种常见的选择,但这也会吸引其他人对用户的注意(毕竟这是移动设备)。最佳选择是在屏幕底部显示一个小型动画图标,可能通过颜色编码来告诉用户消息的相关性(如果有的话)。这样的图标也可以是交互式元素,允许用户点击它以自动滚动到最近发布的信息。这种思维方式在设计任何应用程序时都很重要,但在构建以内容为中心的Activity时,在设计上多花一些心思更为关键。

在本章中,我们将探讨在向用户展示内容时需要考虑的不同方面,以及内容屏幕可以开发的多种方式。具体来说,我们将探讨:

  • 设计 Android 内容展示时的思考过程

  • 用户如何使用和查看内容屏幕

  • 使用WebView类来显示内容

  • 构建用于显示内容的原生布局

  • 在 Android 中格式化和样式化文本

  • 引导用户注意屏幕的特定区域

在 Android 设备上显示内容时考虑设计选项

以内容为核心的Activity与网页非常相似,但在设计上有一些关键考虑因素,这些是人们在创建网页时不会考虑到的。例如,触摸屏设备通常没有软件指针,因此没有“悬停”的概念。然而,许多网页是利用光标悬停来驱动从链接高亮到菜单的一切操作。

在设计以内容为核心的Activity时,你需要仔细考虑设计的美观性。屏幕应避免杂乱,因为许多元素可能是可交互的,当用户触摸时会呈现附加信息。同时,你应尽量减少滚动的需要,尤其是水平滚动。保持信息简洁通常是使更多元素可交互的驱动力。如前几章所述,考虑在可能的地方使用图标代替文字,并按照对用户的重要性组织信息。

还要考虑到屏幕尺寸的变化。一些设备拥有大量像素(如各种 Android 平板电脑),而其他设备则只有 3.5 英寸的小屏幕。因此,考虑到一些人可以在一个屏幕上看到所有展示的信息,而其他人可能需要三个或四个屏幕来显示相同数量的内容,这是非常重要的。

当在 Android 应用程序中工作时,网页是快速轻松地构建以内容为中心的布局的好方法。它具有 WebKit 对 HTML 和 CSS 的出色支持以及与应用程序其他部分轻松集成的优势。它还可以由现有的网页设计师处理,或者如果应用程序连接到基于网页的系统,甚至只需显示一个网页。

然而,网页在某种程度上受到 HTML 和 CSS 布局结构的限制。虽然这些在一级上非常灵活,但如果你不习惯于构建基于网页的系统,即使是针对单一的渲染引擎(在 Android 的案例中是 WebKit),HTML 和 CSS 布局开发也可能是一个繁琐和令人沮丧的过程。当涉及到动画和类似结构时,你还会受到 HTML 渲染引擎性能的进一步限制,无论使用 JavaScript 还是 CSS3 动画。

考虑用户行为

与任何类型的用户界面一样,了解用户的行为以及他们如何与你提供的屏幕互动非常重要。在大量内容信息的情况下,了解哪些信息是重要的,以及用户如何阅读和吸收这些信息至关重要。

虽然你可能想要吸引用户注意某个选定的信息(如价格),但运行一个循环动画来改变该元素的颜色会分散用户对屏幕上其他信息的注意力。然而,简单地改变字体、将数据放在框内,或者改变文字颜色也可以达到预期的效果。同时,考虑用户如何与屏幕互动也很重要。在触摸屏设备上,用户几乎会触摸屏幕的每一个部分。他们还会拖动看起来可以移动的项,如果内容看起来超出了屏幕长度,他们也会使用滚动手势。

大多数人以相同的方式扫描信息。当用户第一次看到一个屏幕,或者屏幕上有大量信息时,他们阅读信息的方式大致相同。以下是用户在屏幕上寻找重要信息时眼睛会遵循的各种移动模式的说明。

考虑用户行为

你通常需要确保重要信息位于一个箭头与另一个箭头相遇的区域。最重要的区域是用户通常开始阅读的角落。对于大多数西方用户来说,这是屏幕的左上角,而亚洲和阿拉伯用户经常会从右上角开始。

注意事项

在设计内容屏幕时,可以考虑让这些区域的信息比正常情况下更加突出。这将产生一个“停留”时间,用户的眼光通常会在这个区域上比平时停留得更久一些。这就是为什么我们通常会在网页的左上角放置一个标志的原因。

吸引用户注意

几乎总是,某些信息比其他信息更重要。你希望用户能够尽可能快地识别出重要信息,并继续他们正在做的事情。一旦用户熟悉了你的应用,他们很可能会完全停止阅读细则。这是一件好事,你通过让用户继续他们的生活,帮助他们更好地使用你的应用。

当你需要吸引用户注意特定信息,如产品名称或价格时,利用TextView类提供的广泛选项是一个好主意。简单地改变一个项目的颜色就可以让用户注意到它。如果你需要更进一步,可以考虑添加阴影,或者将内容放在“高亮框”中。正如我们在第七章 动画小部件和布局中已经讨论过的,动画也可以用来吸引用户界面的特定区域。一个简单的“闪烁”动画(由淡出后紧跟淡入动画组成)可以用来吸引用户注意变化。

提示

一个更具体的例子:金钱

如果你向用户销售产品,并允许他们选择不同的运输方式和包装选项,那么根据他们的选择,总价会发生变化。确保通过加粗字体使总价突出显示。当价格更新时,通过一系列的“中间”价格循环显示,以便总价的图形“递增”或“递减”到新值。

仔细考虑你希望在用户界面中使用的控件。你可能会选择将通常为单一字段的文本放入 TextSwitcher(或类似控件)中,以便对单个单词或值进行动画处理,而不是使用常规的TextView

使用 WebView 类显示内容

WebView 类(位于 android.webkit 包中)通常是基于内容的设计逻辑选择,并且与构建用户界面和常规的 Android XML 布局资源相比,具有非常明显的优势。WebView 类提供了一个单独的入口,你可以在这里放置屏幕的所有内容,它自行处理历史记录和滚动,使得你的代码非常易于编写。

当显示需要复杂布局和/或大量文本内容(可能需要标记)的内容时,WebView 类是一个非常好的选择。它内置支持 HTML 和 CSS 标记,减少了屏幕上所需的控件数量。鉴于 Android 使用 Web-Kit 作为渲染引擎,你还可以使用许多 CSS3 结构(如 CSS 动画)。尽管 WebView 通常用于类似浏览器的网络应用,其中超链接非常重要,但你也可以轻松地为其提供不包含链接的本地内容。你还可以拦截链接请求,以允许导航到应用程序的其他部分。

通常在使用 WebView 结构时,你需要某种方法来生成你将要显示的内容。与在布局资源中构建用户界面不同,你可以简单地为需要注入动态内容的各种 View 对象分配 ID。也就是说,完整的模板引擎通常比 XML 布局和 Java 代码的混合更容易使用,尽管实施的难易程度强烈依赖于你拥有的技能以及需要在屏幕上显示的信息类型。

使用 WebView 对象

为了与 WebView 进行一些操作,并给出一个更具体的示例,说明如何使用它来呈现大量内容,我们将构建一个 Activity 来在屏幕上显示食谱。在这个例子中,我们将硬编码实际的食谱和布局代码以生成 HTML。实际上,你会希望使用如 Velocity/FreeMarker 或 XSLT 这样的模板引擎来生成 HTML 代码。

动手实践——创建食谱查看器应用

你会注意到,以下示例没有使用 XML 布局资源,而是完全在 Java 中创建了 Activity。在此示例中,我们使用 Recipe 对象生成 HTML 代码到 StringBuilder 以显示。这是一个简单但有效的实现。然而,如果需要更改食谱的外观和感觉,它要求修改 Java 代码。

  1. 创建一个新项目以包含食谱阅读器应用程序:

    android create project -n RecipeViewer -p RecipeViewer -k com.packtpub.viewrecipe -a ViewRecipeActivity -t 3
    
  2. 在新应用程序的根包中创建一个新的 Ingredient.java 源文件,以保存单个所需成分的信息,并在你的编辑器或 IDE 中打开这个新文件。

  3. 声明 nameamountunit 字段,这些字段对于食谱是必需的:

    private final String name;
    private final double amount;
    private final String unit;
    
  4. 创建一个构造函数以接收参数并将它们赋值给字段:

    public Ingredient(
            String name,
            double amount,
            String unit) {
        this.name = name;
        this.amount = amount;
        this.unit = unit;
    }
    
  5. 为每个字段创建一个获取器方法:

    public double getAmount() {
        return amount;
    }
    
    // . . .
    
  6. 在项目的根包中,创建一个名为 Recipe.java 的新源文件以包含一个单独的食谱,并在编辑器或 IDE 中打开它。

  7. 声明一个字段用于 Recipe 对象的名称:

    private final String name;
    
  8. 声明另一个字段以包含此 Recipe 所需的成分列表。我们将这些作为 Ingredient 对象的数组存储:

    private final Ingredient[] ingredients;
    
  9. 然后声明一个 String 对象数组,该数组将包含需要遵循的 Recipe 指令列表:

    private final String[] instructions;
    
  10. 创建一个构造函数以接受字段数据并将其赋值以存储:

    public Recipe(
            String name,
            Ingredient[] ingredients,
            String[] instructions) {
        this.name = name;
        this.ingredients = ingredients;
        this.instructions = instructions;
    }
    
  11. 为这三个字段创建一个获取器方法:

    public Ingredient[] getIngredients() {
        return ingredients;
    }
    
    // . . .
    
  12. 在此示例中,Recipe 类负责生成 HTML。声明一个名为 toHtml 的新方法:

    public String toHtml() {
    
  13. 创建一个 DecimalFormat 对象以处理体积的格式化:

    DecimalFormat format = new DecimalFormat("0.##");
    
  14. 创建一个新的 StringBuilder 对象以构建 HTML:

    StringBuilder s = new StringBuilder();
    
  15. 追加 HTML 标题:

    s.append("<html>").append("<body>");
    
  16. 追加一个一级标题元素,其中包含食谱的名称:

    s.append("<h1>").append(getName()).append("</h1>");
    
  17. 追加一个二级标题元素以打开 ingredients 部分:

    s.append("<h2>You will need:</h2>");
    
  18. 打开一个无序列表以列出食谱所需的成分:

    s.append("<ul class=\"ingredients\">");
    
  19. 对于每个 Ingredient 对象,为新的成分打开一个列表项:

    for(Ingredient i : getIngredients()) {
        s.append("<li>");
    
  20. 使用声明的 DecimalFormat 格式化后,将成分的量追加到 StringBuilder

    s.append(format.format(i.getAmount()));
    
  21. 然后追加成分的测量单位:

    s.append(i.getUnit());
    
  22. 现在将成分的名称追加到 StringBuilder,并关闭 ingredient 列表项:

    s.append(" - ").append(i.getName());
    s.append("</li>");
    
  23. 在关闭 for 循环后,关闭无序列表:

    s.append("</ul>");
    
  24. 创建一个二级标题,打开食谱的 Instructions 部分:

    s.append("<h2>Instructions:</h2>");
    
  25. 打开另一个无序列表以将食谱指令渲染其中:

    s.append("<ul class=\"instructions\">");
    
  26. 使用 for-each 循环遍历指令数组,将它们渲染成 StringBuilder 中的无序列表结构:

    for(String i : getInstructions()) {
        s.append("<li>").append(i).append("</li>");
    }
    
  27. 关闭无序列表和 HTML 标题,返回 StringBuilder 对象的 String 内容:

    s.append("</ul>");
    s.append("</body>").append("</html>");
    return s.toString();
    
  28. 在你的编辑器或 IDE 中打开 ViewRecipeActivity Java 源代码。

  29. onCreate 方法中,在调用 super.onCreate 之后,创建一个新的 WebView 对象,将 this 作为它的 Context 传递给它:

    WebView view = new WebView(this);
    
  30. WebView LayoutParams设置为占用所有可用的屏幕空间,因为WebView(与ListView类似)具有内置的滚动功能:

    view.setLayoutParams(new LayoutParams(
            LayoutParams.FILL_PARENT,
            LayoutParams.FILL_PARENT));
    
  31. 创建一个Recipe对象以在WebView中显示,完整的食谱在本示例部分末尾:

    Recipe recipe = new Recipe(
            "Microwave Fudge",
            // . . .
    
  32. 将由Recipe对象生成的 HTML 内容加载到WebView中:

    view.loadData(recipe.toHtml(), "text/html", "UTF-8");
    
  33. Activity的内容视图设置为创建的WebView对象:

    setContentView(view);
    

刚才发生了什么?

食谱查看器示例显示了一个简单的结构,可以通过多种不同的方式扩展,以易于使用的格式向用户呈现大量信息。由于WebView与 HTML 一起工作,使得呈现非交互式信息列表比使用ListView或类似结构更具吸引力。

之前使用的loadData方法有限制,它不允许页面轻松引用外部结构,如样式表或图片。你可以通过使用loadDataWithBaseURL方法来绕过这个限制,该方法与loadData类似,但会相对于指定的 URL 渲染页面,该 URL 可能是线上的或设备本地的。

Recipe对象被认为负责渲染其 HTML,这在纯 Java 情况下工作良好。你也可以将Recipe传递给模板引擎,或者使用访问者模式将Recipe对象渲染为 HTML 代码。上一个示例中Recipe对象的完整代码如下:

Recipe recipe = new Recipe(
    "Microwave Fudge",
    new Ingredient[]{
        new Ingredient("Condensed Milk", 385, "grams"),
        new Ingredient("Sugar", 500, "grams"),
        new Ingredient("Margarine", 125, "grams"),
        new Ingredient("Vanilla Essence", 5, "ml")
    },
    new String[]{
        "Combine the condensed milk, sugar and margarine "
        + "in a large microwave-proof bowl",
        "Microwave for 2 minutes on full power",
        "Remove from microwave and stir well",
        "Microwave for additional 5 minutes on full power",
        "Add the Vanilla essence and stir",
        "Pour into a greased dish",
        "Allow to cool",
        "Cut into small squares"
    });

使用WebView对象的 一个不利的副作用是它不符合其他小部件的外观和感觉。这就是当你将其与其他小部件放在同一屏幕上时,它不能很好地工作的原因。上一个示例的最终效果实际上是一个非交互式的网页,如下所示:

刚才发生了什么?

动手英雄——改进食谱查看器的观感

上一个示例生成了一个非常简单的 HTML 页面,并且没有包含任何样式。内联包含 CSS 是一个非常简单的操作,甚至可以通过从应用资源中读取样式内容来完成。创建一个 CSS,将其内联包含在 HTML 页面中,并包含如下规则:

  • 设置一级标题和二级标题元素背景颜色

  • 将一级标题和二级标题的字体颜色改为白色

  • 将头部元素的圆角设置为五个像素

  • 将列表项目符号从圆形改为方形

进一步使用 WebView

WebView类具有非常重要的功能,在处理内容屏幕时非常有用,例如,使用超链接为不太重要的内容提供一个显示/隐藏的披露部分。这需要 HTML 页面中使用 JavaScript,此时强烈建议你的应用程序使用模板引擎来生成 HTML 页面,而不是在 Java 代码中生成(因为 Java 代码将很快变得难以维护)。

WebView类还允许你的应用程序通过一种非常简单的机制与页面上的 JavaScript 代码交互,你可以通过这种方式将 Java 对象暴露给 JavaScript 代码。这是通过addJavascriptInterface方法实现的。这样,HTML 页面就可以调用你提供的 Java 对象上的动作,从而有效地允许页面控制你应用程序的一部分。如果你的内容屏幕需要执行诸如购买取消的业务动作,可以在 JavaScript 接口对象中公开所需的功能。当用户选择书籍HTML 元素时,页面中的 JavaScript 可以调用你定义的appInterface.buy();方法。

在考虑WebView类时,另一个重要的特性是“缩放”控件。当向用户展示大量信息时,用户可能需要放大或缩小以使某些元素更容易阅读。要启用WebView的内置缩放控件,你需要访问WebSettings对象:

webView.getWebSettings().setBuiltInZoomControls(true);

WebSettings对象可以用来启用和禁用 WebKit 浏览器组件中可用的许多其他功能,阅读可用的文档是非常值得的。

WebView类的主要问题是它的外观和感觉。默认主题的 Android 应用程序在黑色背景上是浅灰色,而WebView类在白色背景上是黑色,这使得由WebView驱动的屏幕在用户看来就像是一个单独的应用程序。

解决样式问题的最简单方法似乎是将 HTML 页面样式设计得与应用程序的其他部分一样。问题是,一些设备制造商有自己的 Android 应用程序样式,所以你无法确定应用程序的其余部分看起来会是什么样子。将 HTML 页面的背景和前景改为符合标准的 Android 主题,在制造商主题的设备上运行时,可能会使其与应用程序的其他部分形成鲜明对比。

小测验

  1. 渲染大型对象图以在WebView中显示的最佳方式是什么?

    1. 将其转换为 XML 并通过 XSLT 处理

    2. 将其发送到外部网络服务以进行渲染

    3. 硬编码 HTML 生成

    4. 使用简单的模板引擎

  2. 你如何通过WebView访问外部 CSS 和图片?

    1. 使用loadDataWithBaseURL方法

    2. 在 HTML 页面中指定完整的 URL 路径

    3. 生成包含内联数据的 HTML 代码

  3. Android 的WebView使用什么渲染引擎?

    1. Gecko

    2. MSIE/Trident

    3. KHTML

    4. WebKit

为内容显示创建相对布局

WebView提供了一种简单的方式,可以轻松地向用户展示大量内容,并以易于阅读的格式呈现。它还内置了许多专为查看内容而设计的功能。然而,它并不总是提供简单的解决方案,通常不允许使用其他小部件提供的现成功能。RelativeLayout类提供了与WebView类相同的布局功能。

正如我们刚刚讨论的,WebView几乎像一个独立的应用程序一样突出。使用RelativeLayout,你将使用标准的 Android 小部件来填充你的屏幕,这意味着从一屏切换到另一屏时,外观和感觉不会有任何变化。而WebView需要某种模板引擎(无论是 API 中的,还是在示例中简单的StringBuilder),RelativeLayout可以声明为应用程序资源中的 XML 文件。使用布局文件还意味着屏幕布局将通过资源选择过程进行选择,从而可以实现难以用WebView类和 HTML 代码实现的复杂自定义。

在某种意义上,使用RelativeLayout提供了一种模板引擎的形式。只需为需要用数据填充的View对象提供 ID,就可以通过将这些暴露的对象注入相关内容来填充屏幕。当我们构建基于 HTML 的视图时,我们需要为成分列表和说明列表创建标题元素,如果使用编码的布局结构,这些标题将从布局文件中加载,或从字符串束资源中加载。

在处理信息列表时,这是内容布局的常见要求,你可以以多种不同的方式提供数据。你可以使用ListView对象,或者你可以使用嵌入式LinearLayout作为列表。在使用它们中的任何一个时,建议有一个可以重复用于列表中每个项目的布局资源。使用ListView意味着你有了一个Adapter,通过它你可以将数据对象转换为可以在屏幕上显示的View对象。然而,ListView对象还有各种其他限制(如包含项目的大小),最好在它们显示的项目以某种方式交互时使用。如果你需要一个非交互式的项目列表(或网格),最好通过创建一个负责根据你的数据对象创建View对象的单独类来遵循Adapter机制。

充分利用 RelativeLayout

RelativeLayout结构的主要优势在于它们可以直接与你的应用程序的其余部分集成。它们比 HTML 页面更容易本地化。直接ViewGroup结构提供的事件结构比通过其专用的事件监听器和 JavaScript 的WebView对象提供的事件结构更为灵活。

XML 布局结构也提供了与模板引擎类似的效果,无需导入像 XSLT 引擎、Java 模板引擎这样的外部 API,或者硬编码 HTML 生成。标准的 Android Activity 类也内置了与 Android 动画结构工作的功能。虽然 WebView 类允许使用 CSS 动画或运行 JavaScript 动画,但这需要为动画的每一帧重新布局 HTML 结构。

一个实现了整个内容屏幕的 Android Activity 类还有个优点,那就是它可以从应用程序资源结构中加载外部资源。这不仅使得你能够更容易地本地化图像等资源,也意味着所有资源都会通过资源编译器处理,因此可以通过 Android 工具链进行优化。而使用 WebView 的话,你需要一个基本 URL 来加载这些资源,或者能够将它们内嵌编码在 HTML 页面中。

考虑到 Android 布局的限制

完全将内容视图开发为 Android 布局有一些缺点。从技能角度来看,只有开发者能够构建和维护用户界面。这也意味着任何针对单个小部件的样式设计都必须由开发者管理。而基于 WebView 的布局,布局的大部分创建工作可以由网页开发人员和图形设计师来处理。

注意

向屏幕上添加更多小部件会带来另一个问题——性能。不仅更大、更复杂的布局可能导致用户体验非常缓慢,还可能导致你的 Activity 完全崩溃。

屏幕上保持较少的小部件意味着用户一次需要吸收的信息量会减少,界面也将更容易操作。

过长或过深的布局会导致应用程序崩溃。如果你需要让句子中的一个单词动起来,你将不得不定义两个额外的 TextView 小部件,用来显示动画单词两侧的非动画文本。这增加了你的布局长度。如果你还需要一个水平 LinearLayout 来放置这三个 TextView 对象,你将增加布局结构的深度。考虑到这两个限制,你可以想象在布局渲染时,你很快就会耗尽内存或处理能力。每个小部件在渲染之前都必须进行布局测量。每次测量、布局步骤或渲染步骤都会通过递归调用方法来使用语言堆栈,以确保所有小部件在屏幕上的正确位置正确渲染(或者如果它们在屏幕外则不渲染)。Android 中的软件堆栈大小是有限的,每次方法调用都需要将其参数推送到堆栈上以进行调用。除此之外,所有测量信息都需要存储在堆空间中,这也是 Android 平台上另一个严重受限的资源(默认情况下,Dalvik VM 只分配了 8 MB 的堆空间开始)。

下图展示了布局结构的长度和深度的区别。左边的屏幕展示了一个长布局,而右边的屏幕展示了一个深布局:

考虑 Android 布局限制

设置 TextView 对象的样式

在这一点上,考虑如何让句子中的一个单词变粗体,或者给它加个阴影,这似乎令人担忧。在 WebView 中,只需添加一个带有特殊样式的 <span> 元素就很容易实现,但在原生布局中,难道你需要为文本的每个部分添加单独的 TextView 对象吗?如果是这样,你将极大地限制能够向用户显示的文本量,因为你将创建成千上万的几乎无用的对象。

幸运的是,Android 非常容易地对所有默认小部件中的文本进行标记。任何从 TextView 继承的类都可以处理带有样式信息或甚至图片的文本。通常,android.text.style 包中可用的类可以用来设置你想要显示的文本字符串的子片段的样式。

为了使用这些不同的样式结构,你需要使用一个SpannableString对象。SpannableString是 Android 字符串的一种特殊类型,它记录了一个需要显示的正常CharSequence文本的样式信息。还有其他一些类似的类(如SpannableStringBuilder),它们处理文本的简单修改,因此适合于将被编辑的文本。出于我们当前的目的,SpannableString是完美的,而且更简单易用。SpannableString有一个基于Spannable接口需要实现的方法——setSpansetSpan方法允许你向SpannableString添加标记结构,这些标记结构影响文本特定部分的渲染方式。

如果我们只想在屏幕上写下There is nothing to fear!这个文本,你通常会使用一个指定字符串的TextView对象。但如果我们想将字符串中的nothing划掉呢?现在的方法是使用StrikethroughSpan对象来处理第 9 到 16 个字符。在这种情况下,字符串不能只在布局文件中定义,需要在 Java 代码中创建一个SpannableString。以下是实现此操作的一个简单示例,以及结果TextView的外观:

TextView fear = new TextView(this);
SpannableString string = new SpannableString(
        "There is nothing to fear!");
string.setSpan(new StrikethroughSpan(), 9, 16, 0);
fear.setText(string);

这段 Java 代码的结果是一个TextView小部件,它显示的是样式化的内容,而不是普通的String,如下面的截图所示:

样式化 TextView 对象

如你所见,使用这种标记非常有效,而且实际上非常容易操作。与WebView渲染相比,这个示例的执行速度也非常快,因为它不包含任何形式的解析。

然而,这种机制存在一些问题。最重要的是索引处理。为了知道何时开始或结束标记渲染的Span,你需要指定需要用给定Span渲染的第一个和最后一个字符。除非你计划更改文本,甚至更糟——国际化它,否则这不是问题。

幸运的是,Android 已经有一个内置的解决方案,尽管这会牺牲一些性能。你可以将几乎任何 HTML 文本转换成一个Spannable对象,然后这个对象可以直接传递给任何TextView对象进行渲染。要使用的类是android.text.Html类,它包括用于将 HTML 代码解析为Spannable对象的实用方法,以及将Spannable对象转换为 HTML 代码的方法。

如果你需要国际化打算用额外样式属性渲染的字符串,Html类可能是唯一合理的做法。它还有一个额外的好处,即图片加载可以由你的应用程序处理(通过使用Html.ImageGetter接口)。此外,TextView仍然看起来和感觉像一个正常的 Android 小部件,这增强了用户的体验。

Html类处理大多数 HTML 标签,但并非所有。一方面,CSS 样式被忽略,因此颜色和边框不在考虑之列。然而,仍然可以实现很好的样式,至少你不需要在应用程序资源中记录字符索引值,以便所有样式对齐。

如果你想将Button标签中的文本设置为粗体,使用Html类可以轻松实现。直接将fromHtml方法的结果传递给TextView对象要快得多。例如,以下代码片段将生成一个Button对象,其中单词Hello会以斜体显示,而单词World则具有粗体权重:

Button button = new Button(this);
button.setText(Html.fromHtml("<i>Hello</i> <b>World!</b>"));

你还可以在布局资源 XML 文件中指定 HTML 内容,它将在传递给TextView对象的setText方法之前通过Html类进行解析。

上面的 Java 代码片段创建了一个Button小部件,其外观如下所示:

设置 TextView 对象的样式

HTML 标签也可以用于将迷你文档渲染到TextView对象中,尽管它们具有自己的样式,但也会遵循TextView对象的样式。这意味着,如果你需要一个比WebView更快速处理静态文本(且不含超链接)的解决方案,TextView实际上可以作为一个很好的替代品。例如,考虑以下代码片段:

TextView text = new TextView(this);
text.setTextColor(0xff000000);
text.setBackgroundColor(0xffffffff);
text.setText(Html.fromHtml(
        "<h1>Cows Love to Eat Grass</h1>"
        + "<p>Do not fear the Cow</p>"));

这将渲染一个带有第一级标题和单行段落元素的TextView。两者都将包含一些内边距,以便与屏幕上的其他元素保持距离。生成的图像应该看起来相当熟悉:

设置 TextView 对象的样式

如你所见,正确设置了样式的TextView可以成为WebView的优秀替代品,特别是当你将其与一系列原生小部件并列使用时。然而,黑底白字的样式确实带来了不一致的问题。因此,除非你的整个应用程序遵循这种模式,否则最好将样式保留为默认。

如果你打算使用TextView显示较长的内容,需要考虑一些额外的因素:

  • 确保如果文本长度超过用户屏幕尺寸,用户将能够滚动。这很容易做到,只需将TextView放置在ScrollView对象中。

  • 如果你的文本非常长,考虑对内容进行样式设计,要么使文本更亮白,要么使用黑底白字。虽然这与其他 Android 应用程序以及你自己的应用程序中的其他屏幕非常不一致,但它对眼睛来说要轻松得多,你的用户会为此感谢你。

  • 考虑允许用户通过长按或菜单更改字体大小。如果他们的屏幕是低密度的,或者他们视力不佳,你可能使他们的生活变得稍微轻松一些。

小测验

  1. 如果需要显示一个非交互式的项目符号列表,以下哪个更合适?

    1. 带有无序列表的WebView

    2. 一个特别样式的ListView对象

    3. 一个带有 HTML 内容的TextView对象

  2. 关于超链接,你可能使用WebView而不是TextView,因为:

    1. TextView不能处理超链接

    2. WebView中显示效果更佳

    3. WebView具有内置的历史管理功能

  3. 对于动画密集型应用,原生接口效果更好,因为:

    1. 你可以使用 Android 动画资源文件

    2. WebView类不处理动画

    3. HTML 动画运行成本更高

是时候采取行动了——开发专用内容视图

在许多情况下,你需要一种特定的交互逻辑,以便在应用程序的许多部分重复使用。在内容屏幕上,某些显示区域将需要更新,由显示的其他部分的变化来驱动。这通常是因为屏幕的一部分在向用户传递信息,而其他部分则在从用户那里捕获新数据。接下来,我们将构建一个简单的控件,负责向用户显示金额。它存在的主要原因是它不仅在变化之间进行动画处理,而且通过改变颜色来反馈给用户金额是上升还是下降。

  1. 创建一个名为AmountBox.java的新 Java 源文件用于新类,并在编辑器或 IDE 中打开新文件。

  2. 新类应扩展TextSwitcher类并实现ViewSwitcher.ViewFactory接口:

    public class AmountBox extends TextSwitcher
            implements ViewSwitcher.ViewFactory {
    
  3. 声明一个字段用于DecimalFormat,以便渲染金额:

    private DecimalFormat format = new DecimalFormat("0.##");
    private double amount;
    

    同时声明一个字段来存储当前显示的数值:

  4. 声明从TextSwitcher类提供的两个构造函数的副本,以允许LayoutInflator类从资源文件实例化AmountBox类:

    public AmountBox(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    public AmountBox(Context context) {
        super(context);
        init();
    }
    
  5. 声明init()方法以处理“常见构造函数”的要求:

    private void init() {
    
  6. 将“进入”和“退出”动画设置为 Android 提供的淡入淡出动画:

    setOutAnimation(getContext(), android.R.anim.fade_out);
    setInAnimation(getContext(), android.R.anim.fade_in);
    
  7. 接下来,将ViewFactory设置为AmountBox

    setFactory(this);
    
  8. 最后,调用setAmount(0)以确保显示的金额已指定:

    setAmount(0);
    
  9. 声明一个 setter 方法,以允许覆盖默认的DecimalFormat

    public void setFormat(DecimalFormat format) {
        this.format = format;
    }
    
  10. 声明一个 getter 方法,以便轻松访问当前数值:

    public double getAmount() {
        return amount;
    }
    
  11. 重写ViewFactorymakeView()方法:

    public View makeView() {
    
  12. 使用传递给此AmountBox的上下文创建一个新的TextView对象:

    TextView view = new TextView(getContext());
    
  13. 指定一个较大的文本大小,因为该数量将表示货币,然后返回TextView对象以显示:

    view.setTextSize(18);
    return view;
    
  14. 现在声明一个设置器方法,以允许更改金额值:

    public void setAmount(double value) {
    
  15. 这个方法将改变文本的颜色,因此声明一个变量来显示新的文本颜色

    int color;
    
  16. 首先检查我们应该将文本更改为哪种颜色

    if(value < amount) {
        color = 0xff00ff00;
    } else if(value > amount) {
        color = 0xffff0000;
    } else {
        return;
    }
    
  17. 获取屏幕外的TextView对象:

    TextView offscreen = (TextView)getNextView();
    
  18. 根据数值的变化设置字体颜色:

    offscreen.setTextColor(color);
    
  19. 在文本周围渲染阴影以产生“光晕”效果:

    offscreen.setShadowLayer(3, 0, 0, color);
    
  20. TextView的文本设置为新的值:

    offscreen.setText(format.format(value));
    
  21. 显示屏幕外的TextView并记住新值:

    showNext();
    amount = value;
    

刚才发生了什么?

AmountBox类是一个需要更新内容的小单元的很好例子。这个类向用户提供信息,同时也提供了一种反馈形式。当用户执行影响显示金额的操作时,AmountBox通过更新字体颜色来反映变化的方向——金额减少时为绿色,金额增加时为红色。

示例使用了第七章讨论的标准 Android 淡入淡出动画,即动画小部件和布局。动画的速度为两个金额之间的交叉淡入效果提供了很好的效果。注意在setAmount方法中,文本内容的更新和View对象的切换是手动处理的。

你可能可以用一个setText方法的调用替换offscreen.setTextshowNext方法的调用,但了解它内部的工作原理很有趣。此方法也不受未来实现变更的影响。

刚才发生了什么?

开发在线音乐商店

一个以内容为中心的布局的绝佳例子是嵌入媒体播放器应用程序中的音乐商店。直接从媒体播放器购买音乐的能力是一个极大提升用户体验的功能,并且与 Android 应用程序作为“连接”应用程序的行为而非纯粹的离线系统相得益彰。Android 还使得将商店真正集成到应用程序中变得非常简单,而不仅仅是提供到适当网站的链接。通常,如果用户点击购买音乐按钮而没有突然跳转到网页浏览器,他们会更有信任感。将应用程序的在线和离线部分正确集成,对于你的销售统计也能起到很大的作用。

在线购买音乐与在商店购买音乐非常不同。关于用户正在查看的歌曲、艺术家或专辑的附加信息是吸引人的部分。因此,一个针对移动设备的在线音乐商店必须精心设计,以提供尽可能多的信息,同时不使屏幕显得杂乱,也不偏离用户购买音乐的初衷。与应用程序的整合感也有助于建立用户信任,因此外观和感觉非常重要。在线购买音乐的另一个优点是,你只需为你想购买的内容付费。为此,用户界面需要允许用户选择他们想从专辑中购买的曲目,以及他们不想购买或计划以后购买的曲目。另外,他们如何知道哪些是他们喜欢的?他们还需要能够播放每首曲目的样本(无论是限时播放,还是只是低质量的)。

设计音乐商店

要真正说明以内容为中心的设计是如何结合在一起的,你需要构建一个。在这个例子中,我们将通过设计过程以及该设计的实现来工作。由于设计和实现是这里的重要部分,我们不会深入构建一个功能性的示例。它只是一个漂亮的屏幕。

首先,我们需要有一个基本的用户界面设计。我发现最好是从一块白板或一张纸和一支笔开始。尽管市面上有很多绘制模拟屏幕的工具,但没有一个能真正接近纸和笔的用户界面。首先,我们绘制一个高级线框,展示整个屏幕设计。这只是一系列告诉我们在屏幕的哪些部分显示什么类型信息的盒子。

设计音乐商店

在图表中,我们将用户界面分成了三个部分:

  • 专辑和艺术家信息区域:这一区域显示用户想要购买专辑的名称和封面艺术。

  • 曲目列表区域:在这个区域,用户可以试听样本,并选择他们想要购买的曲目。

  • 购买区域:这一区域显示用户将支付的总金额,以及一个购买选定曲目的按钮。

在上一个图表中,我遵循了屏幕的大小,但根据屏幕大小和可用的曲目数量,用户界面可能需要一个滚动条才能完全访问。

接下来的工作是对我们定义的用户界面的每个部分进行查看,并决定将哪些小部件放入它们中。首先,我们需要查看专辑和艺术家信息。专辑信息将作为专辑封面艺术和专辑名称显示。我们将包括一个用于艺术家标志的图像区域,并包括一个带有录音标签名称的文本块。

设计音乐商店

这样一个简单的块状图可以让你直观地考虑各种元素。它还允许你开始考虑诸如字体大小、边框和间距等因素。在上述图表中,我们希望右侧的三个元素大致与左侧的封面艺术大小相同。不幸的是,Android 的 RelativeLayout 类目前不允许我们直接规定这一点作为约定。接下来我们需要考虑的设计元素是音轨列表框。对于这个,我们不是在框中绘制所有内容,而是专注于单行外观及其包含的信息。

设计音乐商店

上述结构是一个非常简单的单行结构,用于显示单个音轨的详细信息。左侧的 CheckBox 可用于选择用户想要购买的音轨,而右侧的按钮可用于播放给定音轨的样本。两侧类似按钮的元素为中间的纯文本元素创建了一种框架。

最后,我们需要考虑我们打算如何让用户支付他们的钱。这是用户界面非常重要的部分,它需要清晰明了——他们预期要支付的金额。我们还需要让用户实际进行交易变得非常容易,所以需要一个单一的 购买购买选定音轨 按钮。

设计音乐商店

用户界面的最后一部分仅包含两个小部件,左侧用于购买,右侧显示用户预期支付的总金额。对于左侧的按钮,我们将使用一个简单的 Android Button 小部件,而在右侧,我们将使用本章前一部分编写的新的 AmountBox

开发音乐商店

我们将从构建一系列新的模型类开始新的示例,但首先你需要为我们的概念性媒体播放器创建一个新项目。为此,在命令行或控制台上运行以下命令:

android create project -n PacktTunes -p PacktTunes -k com.packtpub.packttunes -a ShopActivity -t 3

创建新项目后,将 AmountBox 源代码复制到新项目的根包中。然后,你需要创建一个类来包含单个音轨的数据。这只需存储音轨的名称和以秒为单位的音轨时长。我们还将包括一些实用方法,用于计算我们可以用来显示时长数据的分:秒值。

public class Track {
    private final String name;
    private final int length;

    public Track(final String name, final int length) {
        this.name = name;
        this.length = length;
    }

    public String getName() {
        return name;
    }

    public int getLength() {
        return length;
    }

    public int getMinutes() {
        return length / 60;
    }

    public int getSeconds() {
        return length % 60;
    }
}

Track 类是一个非常简单的结构,可以很容易地从 XML 解析或从二进制流反序列化。我们还需要另一个类来保存关于单个艺术家的信息。虽然以下类实际上不过是数据存储的一种形式,但很容易扩展以存储如需的生物信息:

public class Artist {
    private final Drawable logo;
    private final String description;

    public Artist(
            final Drawable logo,
            final String description) {

        this.logo = logo;
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public Drawable getLogo() {
        return logo;
    }
}

最后,在数据类方面,我们需要一个类来将前两个类连接到一个单一的专辑。这个类将被用作可以传递给Activity的单一点。将以下代码复制到项目根包中名为Album.java的新文件中:

public class Album {
    private final Drawable cover;
    private final String name;
    private final Artist artist;
    private final String label;
    private final Track[] tracks;

    public Album(
            final Drawable cover,
            final String name,
            final Artist artist,
            final String label,
            final Track... tracks) {

        this.cover = cover;
        this.name = name;
        this.artist = artist;
        this.label = label;
        this.tracks = tracks;
    }

    public Drawable getCover() {
        return cover;
    }

    public Artist getArtist() {
        return artist;
    }

    public String getLabel() {
        return label;
    }

    public String getName() {
        return name;
    }

    public Track[] getTracks() {
        return tracks;
    }
}

动手时间——构建一个轨道条目

要开始新的用户界面工作,你需要一些图片。在接下来的部分,你需要一个用于播放按钮的图片。播放图片应该是一个简单的“播放”箭头,我们将它放入的按钮会提供背景和边框。列表结构中的行将被放入一个TableLayout中,以便对齐所有子结构。

  1. 在项目的res/layouts目录中创建一个新的布局资源文件,并将新文件命名为track.xml

  2. 将新文件的根元素声明为一个TableRow元素,占用所有可用宽度和所需高度:

    <TableRowandroid:layout_width="fill_parent"android:layout_height="wrap_content">
    
  3. 作为TableRow的第一个元素,创建一个CheckBox,用户可以使用它来选择和取消选择他们想要购买的轨道:

    <CheckBox android:id="@+id/selected"
              android:checked="true"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"/>
    
  4. 声明一个TextView元素,以比通常更大的字体显示轨道名称,并使用纯白色字体颜色:

    <TextView android:id="@+id/track_name"
              android:textSize="16sp"
              android:textColor="#ffffff"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"/>
    
  5. TextView轨道名称后面跟随另一个右对齐的TextView对象,用于显示轨道的时长:

    <TextView android:id="@+id/track_time"
              android:gravity="right"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"/>
    
  6. 以一个ImageButton元素结束TableRow元素,用户可以使用它来在购买前试听轨道:

    <ImageButton android:id="@+id/play"
                 android:src="img/play"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"/>
    

刚才发生了什么

上面的布局资源文件将处理用户界面第二部分轨道列表项的布局。我们需要能够创建几个这样的结构,以处理专辑中所有可用的轨道。我们将它们包裹在一个TableRow元素中,当它被放入一个TableLayout对象时,会自动将其子元素与其他行中的元素对齐。

之后,在 Java 代码中,我们将使用LayoutInflator加载这个资源,用轨道的名称和时长填充它,然后将其添加到一个TableLayout对象中,这个对象我们将作为主用户界面的一部分进行声明。一旦这个新项目被填充了一些数据,它看起来将类似于以下的截图:

刚才发生了什么

动手时间——开发主用户界面布局

建立了后来将变成列表中轨道条目的布局资源文件后,我们现在需要定义这个用户界面的其余元素。虽然这个结构相对简单,但它也非常容易扩展,并且有一些小细节让它看起来非常棒。它还需要一些 Java 代码才能正确填充,但我们在完成资源文件后会涉及到这些内容。

动手时间——开发主用户界面布局

  1. 创建或打开新项目中的res/layout/main.xml文件。

  2. 为了处理主布局可能超出可用屏幕空间的情况,主布局的根元素需要是一个ScrollViewScrollView应占据所有可用屏幕空间:

    <ScrollView
    
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  3. 作为ScrollView的唯一元素,声明一个RelativeLayout,它占据可用宽度,但只有所需的高度。RelativeLayout应在顶部和底部包含一些内边距,以提供一些“呼吸空间”,使其内容不会显得过于拥挤:

    <RelativeLayout android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:paddingTop="10dip"
                    android:paddingBottom="10dip">
    
  4. RelativeLayout的第一个元素是专辑封面,这是一个固定大小的ImageView对象,它将适应可用空间中的专辑封面艺术:

    <ImageView android:id="@+id/artwork"
               android:scaleType="fitCenter"
               android:gravity="left"
               android:layout_alignParentTop="true"
               android:layout_alignParentLeft="true"
               android:layout_width="84dip"
               android:layout_height="84dip"/>
    
  5. 专辑封面之后的第二个元素是艺术家的标志图像,也是一个ImageView。这个元素需要将标志在可用空间中居中显示:

    <ImageView android:id="@+id/artist_logo"
               android:adjustViewBounds="true"
               android:scaleType="center"
               android:layout_alignParentTop="true"
               android:layout_toRightOf="@id/artwork"
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"/>
    
  6. 在艺术家标志之后,我们需要一个简单的TextView对象,并应用一些字体样式来显示我们试图销售的专辑名称。我们将按照之前看到的图像,在用户界面中将此放置在艺术家标志下方:

    <TextView android:id="@+id/album_label"
              android:gravity="center"
              android:textSize="22dip"
              android:textColor="#ffffff"
              android:textStyle="bold"
              android:layout_below="@id/artist_logo"
              android:layout_toRightOf="@id/artwork"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  7. 在带有专辑名称的TextView下方,我们有一个小的非样式的TextView来显示发行专辑的唱片公司名称:

    <TextView android:id="@+id/record_label"
              android:gravity="center"
              android:layout_below="@id/album_label"
              android:layout_toRightOf="@id/artwork"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  8. 如承诺的那样,在这些元素之后,我们使用一个TableLayout来保存可用的曲目信息。我们将TableLayout元素与专辑艺术相对齐,而不是与唱片公司TextView相对齐:

    <TableLayout android:id="@+id/track_listing"
                 android:stretchColumns="1"
                 android:layout_below="@id/artwork"
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"/>
    
  9. 在曲目列表下方,我们首先将购买选定曲目的按钮元素放置在屏幕左侧:

    <Button android:id="@+id/purchase"
            android:text="Buy Selected Tracks"
            android:layout_below="@id/track_listing"
            android:layout_alignParentLeft="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
  10. 最后,在屏幕右侧,我们添加了自定义的AmountBox小部件,在这里我们将告诉用户他们将支付多少费用:

    <com.packtpub.packttunes.AmountBox
        android:id="@+id/purchase_amount"
        android:layout_alignBaseline="@id/purchase"
        android:layout_alignParentRight="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content
    

刚才发生了什么?

在前面的布局中,每个指定的部件都通过提供信息给用户或从用户那里收集新信息来发挥重要作用。尽可能的,我们只给用户提供了对他们来说重要的信息。封面艺术和艺术家标志通常是人们识别特定专辑的第一方式,而名称可能是第二识别方式。专辑封面艺术中的颜色和形状通常比表明专辑名称的文本更快被人的大脑识别。

所有顶部元素:封面艺术、艺术家标志、专辑名称和唱片公司,都可以做成交互式元素,将用户带到包含所选元素更多信息屏幕。链接的信息可能包括评论、讨论论坛和评分小部件。另一个很好的补充是将所选专辑或艺术家的音乐视频链接过来(如果有)。

还要注意我们在购买区域的底部。AmountBox已经与“购买按钮”小部件的“基线”对齐。在这种情况下,它将这些两个小部件中的文本基线对齐,使它们相互看起来居中,尽管这是一种美学上的居中,而不是精确的计算。

动手实践——开发主要用户界面 Java 代码

为了将这个例子完整地组合在一起,并拥有一个以内容为中心的屏幕(尽管仅在示例意义上),我们需要一些 Java 代码。这段代码将处理用Album对象填充用户界面布局。对于接下来的这段代码,你需要封面艺术和艺术家标志的图片。

  1. 在编辑器或 IDE 中打开ShopActivity Java 源文件。

  2. onCreate方法中,确保将main.xml布局资源设置为ShopActivity的内容视图:

    setContentView(R.layout.main);
    
  3. 获取应用资源,并用你最喜欢的音乐专辑的内容调用一个新的setAlbum方法:

    Resources resources = getResources();
    setAlbum(new Album(
            resources.getDrawable(R.drawable.album_art),
            "The Android Quartet",
            new Artist(resources.getDrawable(R.drawable.sherlock),
            "Sherlock Peterson"),
            "Green Records",
            new Track("I was a robot", 208),
            new Track("Long is not enough time", 243),
            new Track("The rocket robot reel", 143),
            new Track("I love by bits", 188)));
    
  4. 声明setAlbum方法以接受一个Album对象:

    private void setAlbum(Album album) {
    
  5. 获取用户界面的track_listing部分,并使用新的addTrackView方法将每个音轨添加到显示中:

    ViewGroup tracks = (ViewGroup)findViewById(R.id.track_listing);
    for(Track t : album.getTracks()) {
        addTrackView(tracks, t);
    }
    
  6. 获取专辑封面艺术部件并设置其内容:

    ImageView albumArt = (ImageView)findViewById(R.id.artwork);
    albumArt.setImageDrawable(album.getCover());
    
  7. 获取艺术家的标志部件并设置其内容:

    ImageView artistLogo = (ImageView)findViewById(R.id.artist_logo);
    artistLogo.setImageDrawable(album.getArtist().getLogo());
    
  8. 获取专辑名称部件并设置其内容:

    TextView albumLabel = (TextView)findViewById(R.id.album_label);
    albumLabel.setText(album.getName());
    
  9. 获取唱片公司部件并设置其内容:

    TextView recordLabel =
            (TextView)findViewById(R.id.record_label);
    recordLabel.setText(album.getLabel());
    
  10. 获取AmountBox部件,并将其格式设置为货币格式,然后将其值设置为1.99乘以音轨的数量:

    AmountBox amount =
            (AmountBox)findViewById(R.id.purchase_amount);
    amount.setFormat(new DecimalFormat("$ 0.##"));
    
  11. 声明addTrackView方法,并像之前一样使用它:

    private void addTrackView(ViewGroup tracks, Track track) {
    
  12. 使用LayoutInflator来填充track布局资源:

    LayoutInflater inflater = getLayoutInflater();
    ViewGroup line = (ViewGroup)inflater.inflate(
            R.layout.track,
            tracks,
            false);
    
  13. 从新的ViewGroup中获取音轨名称部件,并设置其内容:

    TextView trackName =
            (TextView)line.findViewById(R.id.track_name);
    trackName.setText(track.getName());
    
  14. 从新的ViewGroup中获取音轨时长部件,并创建一个StringBuilder用来显示音轨时长:

    TextView trackTime =
            (TextView)line.findViewById(R.id.track_time);
    StringBuilder builder = new StringBuilder();
    
  15. 将分钟数和一个分隔符追加到StringBuilder中:

    builder.append(track.getMinutes());
    builder.append(':');
    
  16. 如果秒数小于10,我们需要一个前缀'0'字符:

    if(track.getSeconds() < 10) {
        builder.append('0');
    }
    
  17. 将时长中的秒数追加:

    builder.append(track.getSeconds());
    
  18. 设置时长部件的文本,并将新行添加到“音轨”列表中:

    trackTime.setText(builder.toString());
    tracks.addView(line);
    

刚才发生了什么?

前面的 Java 代码足以将Album对象中的数据复制到用户界面。一旦显示在屏幕上,它看起来像一个简单的音乐商店页面,但主题为 Android 应用程序。这提供了与网页在布局结构和易于维护方面的许多好处,同时完全集成到最终用户设备上可能存在的任何品牌和样式。一旦显示在屏幕上,之前的示例将呈现给你类似以下截图的东西:

刚才发生了什么?

动手英雄——更新总价

为了让之前的例子感觉更加真实,当用户从专辑列表中选择或取消选择音轨时,它需要更新屏幕底部的总金额。如果没有任何音轨被选择,它还应该禁用购买选定音轨按钮。

尝试为音轨布局中的每个CheckBox元素添加一个事件监听器,并跟踪哪些被选中。为了显示总金额,将1.99乘以被选中的音轨数量。

总结

在本章中,我们已经深入探讨了在向用户展示大量信息或内容时使用的许多重要领域和技术。在开始构建之前,仔细考虑你的界面是很重要的,但同时也不要在动手编码之前花费太多时间。有时,一个简单的用户界面运行起来能告诉你的东西,比你的图表和模型所能展示的要多得多。

我们已经使用WebView类完成了一个显示食谱给用户的示例,展示了在 Android 平台上使用 HTML 是多么简单。我们还通过构建一个在线音乐商店,使用RelativeLayout来显示内容,探讨了与 HTML 视图相对的原生替代方案。通过这两个示例,我们比较了两种机制之间的差异,并洞察了各自最佳使用场景。

在决定如何展示内容时,请务必考虑性能和用户体验。虽然WebView在某些方面可能更具灵活性,允许你根据显示的内容改变视图,但也可能导致不一致性,并让用户感到烦恼。RelativeLayout提供了更刚性的结构,并且还将确保代码库更加一致。

在下一章中,我们将更详细地探讨如何为你的 Android 应用程序添加更多样式。我们还将研究如何最佳地处理设备和配置的变化(例如语言变化或从竖屏模式切换到横屏模式)。

第九章:样式化安卓应用

到目前为止,我们一直在使用标准的 Android 主题和样式。从一致性的角度来看,这是一个非常好的事情,因为应用将与设备的主题(如果有的话)很好地融合。然而,有时候你需要能够定义自己的样式。这种样式可能只适用于单个小部件,也可能适用于整个应用。在这些情况下,你需要了解 Android 为你提供了哪些工具,以便决定如何最佳地解决问题。

样式设计不仅仅是让应用看起来好看。另外,你认为好看的,别人可能不喜欢。这也是关于让应用对用户更有用的问题。这可能涉及到确保无论用户选择哪种语言,应用看起来都是正确的。可能涉及到为某些选定的小部件添加额外的颜色,或者简单地实现某些关键屏幕的横屏布局。

在上一章中,我们探讨了在设计应用某些屏幕时可以做出的整体选择。该章节还介绍了使用WebView作为内容和窗口小部件容器的想法。使用WebView的一个优点是你可以使用 CSS。正如任何 Web 开发者都会告诉你的,使用 CSS 可以使高级样式设计变得非常容易。然而,Android 也内置了一系列样式工具,能够实现许多与 CSS 相同的效果,并且在某些情况下能做得更多。

让屏幕上的单个按钮看起来与众不同,使其与其他所有小部件区分开来。这有助于引起注意,使其与屏幕上的其他任何东西不同,它有特殊的作用。你可能还希望在两组小部件之间绘制一条线,以告知用户它们之间存在逻辑上的分隔。就像尝试理解别人的源代码一样,掌握一个新应用就是理解别人的逻辑。正确地样式化你的应用可以大大帮助用户理解你在构建应用时的思路,同时为他们提供关于预期操作的提示。如果你需要提供如何使用应用的说明,那么你在设计和样式化应用方面的努力就失败了。

在本章中,我们将探讨 Android 如何允许你为其提供的小部件设置样式,以及如何采用你自己的样式和主题。我们还将通过示例来展示自定义样式如何使用户更容易使用应用。我们将涵盖如下主题:

  • 定义样式资源

  • 可以用于样式设计的不同类型的图形资源

  • 创建和使用九宫格图片

  • 在运行时处理设备配置的变化

  • 定义可跨不同设备和屏幕移植的样式

使用样式资源

处理 Android 样式时的首要切入点是了解样式值是如何工作的。应用程序能够定义任意数量的样式,就像定义字符串和字符串数组资源一样。样式资源用于为某些用户界面元素定义一系列默认值,这与 CSS 规则定义样式属性的方式非常相似。主要区别在于,在 Android 中,样式可以覆盖为给定小部件类定义的任何 XML 属性。

下表快速比较了 Android 样式资源和 CSS 样式表。它们有许多共同特征,但行为却大相径庭。

Android 样式资源 CSS 样式表
可应用于任何 XML 属性 有一个目的明确的属性集,它们可以定义或更改
可以从父样式继承 按定义顺序级联形成复杂样式
必须明确应用于ViewActivityApplication 通过选择器与文档元素匹配
以普通 XML 定义 使用专用语法定义

Android 样式的级联方式与 CSS 规则类似。然而,这种级联的定义更多地归功于 Java 类层次结构。每个样式都可以声明一个父样式,从中继承参数。一旦继承,这些参数可能会被新样式选择性地覆盖。拥有一个父样式总是一个好主意,因为设备制造商可能已经修改了默认值,这样你就可以在创建自己的新样式的同时,与用户设备上安装的第一方软件保持一致。

样式声明不能简单地覆盖所有可用的TextView对象的样式。相反,你必须要在小部件声明中为特定小部件导入样式,或者在清单文件中引用样式作为主题,以应用于单个Activity或整个应用程序。首先,我们将重点放在构建样式并将其应用于单个小部件上。

样式与尺寸、字符串和字符串数组一样,都是值资源。创建样式元素时,可以将其放在res/values目录下的任何 XML 文件中(尽管最好是将资源分开,并将样式放在styles.xml文件中)。与values目录中的所有 XML 资源一样,根元素应为<resources>,之后你会列出你的<style>元素。以下是一个简单的样式,可用于将任何TextView设置为标题:

<resources>
    <style name="TitleStyle" parent="@android:style/TextAppearance">
        <item name="android:textSize">25dip</item>
        <item name="android:textColor">#ffffffff</item>
        <item name="android:textStyle">bold</item>
        <item name="android:gravity">center</item>
    </style>
</resources>

上面的<style>元素中的name属性是必填项,而parent属性可选,它决定了使用哪个样式作为默认项(在这种情况下,是TextView对象的默认外观)。以下代码片段声明了一个使用我们上面声明的TitleStyle作为其样式的TextView

<TextView
    style="@style/TitleStyle"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="Header"/>

注意在前一个例子中缺少了android命名空间前缀。实际上,在编译时,当资源被转换成二进制数据以便打包时,应用样式是有效的。当应用额外的属性时,任何在<style>元素上声明但应用样式的部件上不可用的项都会被忽略。理论上,这允许你创建更抽象的样式,并将它们应用于许多不同的部件。

应用了TitleStyleTextView将如下渲染:

使用样式资源

提示

谁覆盖了谁?

当对部件、活动或应用应用样式时,了解覆盖的顺序很重要。每个样式都会覆盖其父样式(如果有)的样式信息,同时每个部件将覆盖应用在它上面的任何样式信息。这意味着虽然你可以将android:text样式项应用于TextView对象,但这通常并不十分有用,因为TextView上的任何android:text属性都会覆盖样式中指定的值。

使用形状资源

能够改变部件中字体的大小和颜色当然很好,但如何从根本上改变该部件的渲染方式呢?我们已经使用过一些 XML 可绘制对象,但还可以用它们做更多的事情。

迄今为止,使用 XML 可绘制结构的工作仅限于为设计有图像的部件放置默认图片。然而,在 Android 中所有部件都被设计为可以拥有图像。View类的background属性允许你传入任何drawable资源,结合样式资源。这成为了一个非常强大的工具。当在 Java 代码中加载形状资源时,它会被返回为一个Drawable对象。

可供你使用的形状在android.graphics.drawable.shapes包中,除了Shape类,这是一个抽象类,该包中的其他类都继承自它。你通过在res/drawable目录中的 XML 文件引用这些类。然而,与布局 XML 资源不同,形状的使用更为有限:

  • 你不能直接访问类的属性

  • 你每个形状文件只能创建一个单一形状

  • 你不能绘制任意的路径(即对角线或贝塞尔曲线)

尽管有这些限制,形状非常有用且重要,因为:

  • 它们会缩放到所附加部件的尺寸

  • 这使得它们非常适合创建边框和/或背景结构

  • 它们还区分了形状的外框和填充

形状的行为

你可以定义的每个形状结构与其他形状略微不同,不仅在渲染方式上,而且在于哪些属性适用于它。由于形状资源的复杂性有限,它们的使用也相对有限。

渲染线条

在 Android 中,线条形状始终是居中于小部件内部的直线。之前我们在记忆游戏中将线条形状用作占位图像。然而,线条形状更常见的用法是作为垂直分隔符。线条形状在与ListView一起使用时很常见。线条形状不支持渐变填充,因此它总是实心颜色(默认为黑色)。但是,线条形状允许使用stroke元素中的所有属性。

一个简单的白色线条可以在几行代码中定义,通常可以用作ListView或类似结构中的分隔符。以下是一个线条定义的代码片段:

<shape 
       android:shape="line">

    <stroke android:width="1sp" android:color="#ffffffff"/>
</shape>

动手操作——绘制断线

Android 中定义的所有形状都允许你使用<stroke>元素来定义点线或虚线结构,但它在线元素上表现得最为出色。如果我们增加线条宽度并定义一个与间隔大小两倍的虚线模式,我们得到的线条看起来就像打印页面上的一条“切割”或“撕裂”线。这是在用户界面上制作更硬分隔线的好方法。

  1. res/drawable目录下创建一个新的形状资源 XML 文件,命名为line.xml,并在编辑器或 IDE 中打开这个文件。

  2. 将文件的根元素声明为line shape

    <shape 
           android:shape="line">
    
  3. 声明一个新的笔画元素,为新线条设置width3sp,颜色为白色,dashGap5sp,以及dashWidth10sp

        <stroke android:width="3sp"
                android:color="#ffffffff"
                android:dashGap="5sp"
                android:dashWidth="10sp" />
    
  4. 结束形状声明:

    </shape>
    

刚才发生了什么?

你刚才创建的shape资源将显示一个虚线。线中的虚线间距正好是虚线长度的一半。大小是相对于用户首选字体大小设置的,因此虚线会根据用户偏好增大或缩小。

以下是此线条在默认模拟器设置下运行的屏幕截图:

刚才发生了什么?

渲染矩形

矩形是使用最广泛的形状资源,因为View对象在屏幕上占据一个矩形空间(即使它们没有使用该空间的所有像素)。矩形形状包括拥有圆角的能力,每个角可以选择性地有不同的半径。

没有额外的样式信息,基本的矩形声明将渲染一个没有可见轮廓的填充黑色方块。然而,矩形更适合创建轮廓,可以用来单独吸引一个小部件的注意,或将一组小部件从屏幕上的其他所有小部件中隔离开来。一个简单的白色矩形边框可以通过将以下代码片段复制到名为res/drawable/border.xml的文件中构建:

<shape 
       android:shape="rectangle">

    <stroke android:width="2dip" android:color="#ffffffff" />
    <padding android:top="8dip"
             android:left="8dip"
             android:bottom="8dip"
             android:right="8dip" />

</shape>

这个形状中的填充元素将导致任何使用它的View对象将其填充大小增加8dip。这将阻止小部件的内容与形状资源渲染的边框相交。

动手时间——创建圆角边框

矩形形状也可能对其角进行圆滑处理,以形成一个圆角矩形。圆角矩形对于设置按钮样式或创建更干净的边框非常有用。

  1. res/drawable目录中创建一个名为rounded_border.xml的新形状资源 XML 文件,并在编辑器或 IDE 中打开此文件。

  2. 将文件的根元素声明为矩形形状

    <shape 
           android:shape="rectangle">
    
  3. 将矩形描边设置为2dip宽,颜色为白色:

    <stroke android:width="2dip" android:color="#ffffffff" />
    
  4. 使用8dip的空白空间填充矩形:

    <padding android:top="8dip"
                    android:left="8dip"
                    android:bottom="8dip"
                    android:right="8dip" />
    
  5. 将角落曲线半径设置为4dip

    <corners android:radius="4dip"/>
    
  6. 关闭形状声明:

    </shape>
    

刚才发生了什么?

要将你刚刚创建的圆角边框应用于View对象,你有几种不同的选项,最简单的是直接作为背景应用。为此,你可以像引用 drawable 目录中的任何其他图像文件一样引用该形状。之前,我们声明了一个TitleStyle并将其应用于包含单词HeaderTextView。如果你将新的rounded_border应用于这个TextView,布局资源中的TextView声明可能看起来更像这样:

<TextView
        style="@style/TitleStyle"
 android:background="@drawable/rounded_border"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Header"/>

另外,你也可以将此边框应用于TitleStyle,这样就会将新边框应用于分配了TitleStyle的每个小部件,这对于标题和标题小部件来说非常合适:

<style name="TitleStyle" parent="@android:style/TextAppearance">
    <item name="android:background">@drawable/rounded_border</item>
    <item name="android:textSize">25dip</item>
    <item name="android:textColor">#ffffffff</item>
    <item name="android:textStyle">bold</item>
    <item name="android:gravity">center</item>
</style>

这两种方法都会导致新小部件的渲染完全相同。实现的决定实际上取决于你试图达到的目标。样式是保持用于相同目的的不同小部件之间共性的最佳方式。

TextView上使用上述样式将得到一个看起来很不错的标题小部件,如下所示:

刚才发生了什么?

渲染椭圆

椭圆形正如其名所示——一个椭圆。椭圆形的使用比矩形更为受限,除非在其上绘制的组件最好由圆形或椭圆形边框,例如一个模拟时钟。也就是说,椭圆形,或者说圆形,在用户界面中作为图像使用非常有效。一个完美的例子就是通知用户他们是否连接到互联网,或者一个组件是否有效。出于这样的目的使用椭圆形与使用位图是相同的。然而,椭圆形可以根据用户的偏好进行缩放,而不会损失任何质量,而使用位图,你需要多个不同大小的位图图像来实现类似的效果(即便如此,一些位图可能还需要缩放)。

如果我们想要一个椭圆形来表示一个无效的组件(例如,当用户在选择密码时显示两个密码输入不匹配),那么最好是将椭圆形涂成红色。在以下代码片段中,我们以 XML 格式声明了一个带有灰色边框和红色填充的椭圆形:

<shape 
       android:shape="oval">

    <solid android:color="#ffff0000"/>
    <stroke android:width="1sp" android:color="#ffaaaaaa"/>
</shape>

在前面的例子中,我们使用 <solid> 元素以纯红色填充椭圆形,同时使用 <stroke> 元素为其围绕一个细的灰色轮廓。还要注意 shape 元素上没有尺寸设置。如之前所述,它们的尺寸是从它们被放置的宽度中继承的,可以作为背景,或者在 ImageView 的情况下,作为组件的内容。如果你想要将这个椭圆形放入 ImageView 中,你会在 src 属性中指定它,如下所示:

<ImageView
        android:src="img/oval"
        android:layout_width="8dip"
        android:layout_height="8dip"/>

之前的代码对于作为一个组件旁边的验证图标来说大小正合适,而将图标放大或缩小就像改变 ImageView 的宽度和高度一样简单。如果你使用 wrap_content 作为 ImageView 的大小,它将被设置为零像素乘零像素,实际上会从屏幕上消失。

下面是同一个椭圆形四种不同大小的截图,每一个都是前一个的两倍大小(从左边的 8x8 dip 开始):

渲染椭圆形

动手实践——给椭圆形应用渐变

之前的截图显示,虽然椭圆形看起来还可以,但当它被组成默认安卓工具包的渐变绘制组件包围时,它不会非常吸引人。为了让这个小椭圆形更好地融入,它需要看起来更像一个球,这需要应用一个简单的径向渐变。

  1. res/drawable 目录中创建一个新的形状资源 XML 文件,命名为 ball.xml,并在编辑器或 IDE 中打开这个文件。

  2. 将文件的根元素声明为 oval

    <shape 
           android:shape="oval">
    
  3. 与其声明一个 solid 颜色作为填充,不如声明一个从浅灰色开始到红色结束的 gradient 填充:

    <gradient android:type="radial"
                  android:centerX="0.5"
                  android:centerY="0.25"
                  android:startColor="#ffff9999"
                  android:endColor="#ffff0000"
                  android:gradientRadius="8" />
    
  4. stroke 元素中定义椭圆形的细浅灰色轮廓:

        <stroke android:width="1sp" android:color="#ffaaaaaa"/>
    
  5. 结束形状声明:

    </shape>
    

刚才发生了什么?

不幸的是,径向渐变的受影响半径不会随图像的其他部分一起缩放,当你将图像放大到较大尺寸时,渐变区域会变得非常小。在这种情况下,效果就是最小的图像看起来很棒,而较大的版本看起来则很糟糕。在撰写本书时,还没有直接的方法来解决这个限制。相反,如果你想要使用径向渐变,需要将椭圆形的大小与ImageView的大小绑定起来。

刚才发生了什么?

渲染环

ring形状在渲染上也是圆形的,但它与椭圆形形状的目的非常不同。虽然椭圆形形状的内容区域是轮廓空间内的所有内容,但环形状的内容区域是一个圆圈。

下图说明了两种形状之间的逻辑差异:

渲染环

ring形状也有两个轮廓,一个在外部,另一个在内部(如前图所示)。将这一点与填充环内容区域的能力结合起来,你就有了用于进度旋转器(默认的 Android 不确定进度旋转器就是用环构建的)的完美形状。

动手操作——渲染一个旋转环

默认情况下,形状会假设它作为LevelListDrawable的一部分被使用,除非你禁用这个行为,否则可能不会出现。你通过在形状元素上指定useLevel属性为false来实现这一点。如果你不禁用这个功能,环可能无法正确渲染,或者根本不会渲染。

  1. res/drawable目录中创建一个新的形状资源 XML 文件,命名为spinner.xml,并在编辑器或 IDE 中打开这个文件。

  2. 将文件的根元素作为ring shape开始:

    <shape 
           android:shape="ring"
    
  3. ring形状需要在shape声明中设置其相对厚度:

           android:innerRadiusRatio="3.2"
           android:thicknessRatio="5.333"
    
  4. 通过关闭useLevel功能来完成shape声明:

           android:useLevel="false">
    
  5. 声明一个在椭圆形中心居中的sweep渐变:

        <gradient android:type="sweep"
                  android:useLevel="false"
                  android:startColor="#ffaaffff"
                  android:centerColor="#ff0000ff"
                  android:centerY="0.50"
                  android:endColor="#ff0000ff"/>
    
  6. 用细白边框勾勒出ring

        <stroke android:width="1sp" android:color="#ffffffff"/>
    
  7. 结束shape声明:

    </shape>
    

刚才发生了什么

扫描渐变是径向渐变的另一种形式。它不是从图像中心向外扩展,而是像时钟的指针一样在圆圈中扫描。

左侧的图像是一个用sweep渐变填充的矩形;而右侧的图像是ring形状。如你所见,这两个效果非常不同。右侧的图像基于 Android 1.6 用于不确定旋转指示器的图像。

刚才发生了什么

定义图层

到目前为止,我们仅将形状定义为单一元素的图像。可以将这些形状组合成更复杂的图像。这些图像以图层的形式组合在一起,这是一种常用的图形结构。在 Android 中,这是通过layer-list结构完成的。layer-list不是一种形状类型,但它是一个Drawable结构,这意味着它可以替代普通的位图图像。

分层图像资源不仅限于与我们前面讨论过的形状等矢量Drawable结构一起使用。分层的Drawable对象也可能包括一些位图图像图层,或任何其他可以定义的Drawable类型。

对于layer-list中的每一层,你需要定义一个<item>元素。item元素用于声明可选的元信息,如图层的 ID(这可以在你的 Java 代码中用于检索该图层的Drawable对象)。你还可以在item元素中声明图层的位置偏移或内边距。虽然你可以将图层作为外部的Drawable资源引用,但你也可以在<item>元素内内联Drawable对象,从而允许你在单个文件中组合各种不同的Drawable结构。

提示

调整你的图层大小

layer-list中的第一个<item>将根据其所在的组件大小进行调整。所有其他图层将被调整为它们的“自然”大小。对于位图图像,这是它渲染的大小。对于<shape>元素,自然大小是 0x0。为了指定<shape>的“自然”大小,你需要为<shape>提供一个带有android:widthandroid:height属性的<size>子元素。

如果你想让一个双层图像充当一个大的绿色按钮,你可能会声明一个灰色圆角矩形的图层作为背景,再声明一个绿色椭圆形的图层,使其看起来像是在灰色背景上的一个光点或球体。这样的layer-list可能看起来类似于以下的代码片段:

<layer-list >
    <item>
        <shape android:shape="rectangle" android:useLevel="false">
            <stroke android:width="1dip" android:color="#ffffffff" />

            <gradient android:type="linear"
                      android:angle="90"
                      android:startColor="#ffaaaaaa"
                      android:endColor="#ffcdcdcd" />

            <padding android:top="8dip"
                     android:left="8dip"
                     android:bottom="8dip"
                     android:right="8dip" />

            <corners android:radius="4dip" />
        </shape>
    </item>
    <item>
        <shape android:shape="oval" android:useLevel="false">
            <size android:width="32dip" android:height="32dip" />
            <gradient android:type="radial"
              android:centerX="0.45"
              android:centerY="0.25"
              android:startColor="#ff1a4e1a"
              android:endColor="#ff1ad049"
              android:gradientRadius="32" />
        </shape>
    </item>
</layer-list>

在前面的代码片段中,只有shape图层,但你可以轻松地通过在<item>元素中引用位图资源,来添加一个位图图层,如下面的代码片段所示:

<item android:drawable="@drawable/checkmark"/>

使用九宫格图像进行拉伸

有时你想要一个比简单线条更复杂的边框,例如,如果你想添加阴影。在网页上,你通常会找到各种 HTML 技巧,将八张或九张图片插入一个盒子中,以便在保持边框完整的同时缩放内容。在 Android 中,这种技术称为“九宫格”图像,因为它由九个不同的部分组成。在 Android 中,当九宫格图像以大于其原始尺寸的大小渲染时,会特别处理。为了将这些图像标识为特殊的,它们有一个.9.png扩展名(必须是有效的PNG文件)。

九宫格图像将边框和背景结合在单一图像中。当内容变得过大而无法适应图像时,背景区域将会扩大,图像的边框区域也会被缩放,以避免留下“空洞”。

从概念上讲,你可以从以下图表所示的九宫格图像开始思考:

使用九宫格图像进行拉伸

图中的箭头指出了根据中心“内容”区域大小而变大的概念性“边界”区域。九宫格图像的角落将完全不受任何缩放的影响。

创建九宫格图像

要创建九宫格图像,你需要一个像样的图像编辑应用程序。我个人使用Gimp应用程序(在www.gimp.org免费提供),尽管你可能更喜欢使用其他应用程序。无论你使用什么应用程序,它都必须能够输出Portable Network Graphics (PNG)文件,并且还应该能够放大到相当高的程度。九宫格图像中的所有数据实际上都编码在图像文件中,这意味着不需要 XML 文件来告诉 Android 图像的哪些部分是边框区域,哪些部分在缩放时不应受到影响。

与网页上出现的 CSS 盒子不同,Android 中对九宫格图像的大小调整是通过最近邻缩放完成的。最近邻缩放并不试图以任何方式改善缩放图像的质量,像素只是变成了更大颜色块。这对于渐变内容背景来说效果很好(只要它们没有被强制变得过大),但它可能导致你的图像出现一些奇怪的艺术效果。由于当前在缩放过程中没有进行颜色插值,某些效果在缩放时可能看起来相当奇怪。缩放也比简单的图像复制耗时更长,因此在调整图像大小时请记住这一点,它可能需要比你想象的要大得多。然而,这也意味着九宫格图像比你在网上可能了解的图像要灵活得多。

下面的两张图像是同一 32x32 像素九宫格图像的放大版本:

创建九宫格图像

左侧的图像是原始的 PNG 文件,可用作九宫格图像。右侧的图像是同一图像的一部分被突出显示,以展示哪些区域将被缩放。顶部、底部左侧和右侧的区域将仅水平或垂直缩放,而中心区域将被拉伸以适应内容的大小。以下图像是作为TextView对象的背景使用的同一图像:

创建九宫格图像

那么,图像左侧和顶部上的黑色线条告诉安卓系统要缩放图像的哪些部分,但右侧和底部的线条表示什么呢?这两条线决定了如何放置小部件内容的位置,类似于<shape>资源中的<padding>元素。

为了了解你的九宫格图像将如何渲染以及可能的缩放方式,安卓系统在 Android SDK 安装的tools目录中提供了一个实用工具。draw9patch工具将你的九宫格图像渲染成各种形状和大小,并允许你在将图像用于应用程序之前有效地调试图像。

在安卓中使用位图图像

图像是塑造你的应用程序风格的重要组成部分。它们用于图标、边框、背景、标志等许多其他用途。安卓系统尽力确保你使用的资源图像能在安卓设备上的不同类型屏幕上尽可能好地渲染。

安卓系统对图片的自动处理远非完美。然而,有时你需要为应用程序提供同一图像的多种不同变体,以使其在各种不同的设备上看起来都正确。

处理不同的屏幕尺寸

在安卓中处理任何位图图像时,非常重要的一点是要考虑到你的应用程序将在各种不同大小和密度的屏幕上运行。在非常大的屏幕(如在笔记本电脑或平板电脑上找到的屏幕)上工作時,你需要使用比在非常小的屏幕上更大的图像。虽然九宫格图像在很大程度上简化了事情,但它们仍然使用最近邻算法进行缩放,这可能会在比你预期更大的屏幕和字体大小上开始显现。

您可以在资源目录中提供不同大小的图片。对于每种屏幕尺寸,你可以提供不同的drawable目录。资源加载工具会自动从与当前设备配置最接近的目录中选择文件。你不需要在每个目录中都有一份每种资源的副本,只需提供那些你希望有更合适替代品的资源。当尝试查找要加载的资源文件时,资源加载器会在匹配度较低的目录中回退查找。

安卓系统识别出与屏幕尺寸相关的五个重要参数。虽然你可以指定与屏幕上确切像素数相关的参数,但这不是一个好主意,因为你无法轻易地适应所有不同的屏幕尺寸。相反,最好坚持使用安卓系统提供的五个参数:

  • small

  • medium

  • large

  • long

  • notlong

前三个参数直接与屏幕尺寸相关,而后两个参数与屏幕是否为“传统”(如 VGA)格式或“宽屏”(如 WVGA)格式有关。这些参数可以以各种组合方式混合,例如:

  • /res/drawable-small/

  • /res/drawable-medium-long/

  • /res/drawable-large-notlong/

前面的例子都是有效的资源目录,可用于覆盖正常drawable目录中的文件。您不能组合相互矛盾的参数,例如:

  • /res/drawable-small-large/

  • /res/drawable-long-notlong/

在上述情况下,您将收到资源打包工具的错误信息。每当您处理位图图像时,考虑到这些尺寸参数很重要,因为有些设备的屏幕与默认模拟器显示的屏幕有很大不同。

处理不同的屏幕密度

屏幕密度通常指的是在给定物理空间内填充的像素数量(即每英寸点数或 DPI)。它还与屏幕上像素的大小有关。虽然大多数 Android 设备具有中等或高密度屏幕,但大量较便宜的设备使用相对低密度的屏幕。

这为什么会影响到九宫格和位图图像呢?同样的原因也影响到了字体渲染——密度越低,抗锯齿和阴影效果看起来越差。解释这个现象最好的方式是用图像来说明。在以下图片中,左边的是在高密度屏幕上显示的简单圆角矩形。右边的图片类似于在低密度屏幕上渲染的同一图像:

处理不同的屏幕密度

尽管这两张图片源自同一张图片,且以相同的物理尺寸渲染,但像素数量的减少使得在低密度屏幕上图像看起来变得块状。

以下两张图片是从右下角截取的,并放大以更详细地说明发生的情况:

处理不同的屏幕密度

同样,这些图片被配置为占据相同的物理空间。如果图像的尺寸以屏幕像素指定,那么在低密度屏幕上它将占据更多的物理空间。这就是为什么推荐在 Android 中使用“密度独立像素”(dpdip)单位而不是普通像素(px)单位来设置图像大小的一个原因。

与屏幕尺寸一样,Android 提供了一系列配置参数,可用于为不同屏幕密度的设备提供不同的资源。选择屏幕密度的参数可以与基于屏幕尺寸选择的参数混合使用。以下是 Android 提供的可用于根据当前设备的屏幕密度提供资源的参数列表:

  • ldpi:低密度屏幕(约 120dpi)

  • mdpi:中等密度屏幕(约 160dpi)

  • hdpi:高密度屏幕(约 260dpi)

  • nodpi:特殊情况

最后一个“特殊情况”可以在你有一个不希望根据设备密度缩放的九宫格图像或位图图像时使用。默认情况下,Android 会重新缩放图像,以尝试使图像的物理尺寸尽可能接近预期的尺寸。nodpi 目录中的图像不会被 Android 自动缩放,而是按像素对像素进行渲染。

提示

不同密度的图标

有时大尺寸的高分辨率图标并不能很好地缩小。在这些情况下,为低密度屏幕设计完全不同的图标通常是一个好主意。

处理配置变更

当你为 Android 提供与各种可能的硬件配置相关的不同资源目录时,资源加载器将尝试为运行你应用程序的设备匹配最佳的资源文件。然而,并非所有的配置参数都直接与硬件相关,而是描述设备状态或某些软件配置参数。这些参数的例子包括设备语言、网络 ID 和设备方向。这些参数可能会在应用程序运行时发生变化。最常见的例子就是设备方向。Android 有一个内置机制来为你处理这些变化,在大多数情况下,你不需要任何特殊的 Java 代码来处理这些变化。然而,至少为其中一些参数提供资源文件是非常可取的。

当配置参数发生变化时,Android 会将你的 Activity 状态存储在一个 Bundle 对象中,然后关闭 Activity。之后,系统会以新的配置参数启动 Activity 的新实例,并从 Bundle 对象中恢复状态。所有默认的 Android 控件都会在系统关闭你的 Activity 之前存储它们当前的状态。这意味着通常你不需要为配置变更执行任何特殊处理。

提供横屏布局

到目前为止,我们在这本书中只构建了竖屏布局。与桌面或网页系统不同,移动应用程序的默认方向是竖屏(因此配置参数是 longnotlong 而不是 widenarrow)。拥有 Android 平台的好处之一是它必须包含加速度计这一硬件,这意味着你的应用程序可以响应设备的方向。得益于 Android 的配置处理(如前所述),作为开发者的你除了提供替代的横屏布局资源外,不需要做任何事情,假设你没有在 Java 中构建用户界面的大部分内容。为了提供特定于竖屏或横屏方向的布局,你可以将布局的特定版本的 XML 资源放置在以下配置参数配置的目录中:

  • port:针对竖屏的布局

  • land:特定于横向的布局

当屏幕竖向比横向长(即肖像模式)时,使用一个简单的垂直方向的LinearLayout来布局一个输入表单是非常有意义的。你所使用的任何输入控件都会被放置在它们标签的下方,因此它们有更多的水平空间来显示数据。额外的水平空间使得标签可以包含更多信息。

下图展示了这两种布局概念之间的区别:

提供横向布局

右侧使用的布局方法在网页或桌面系统中非常常见,如果标签和输入控件的大小足够小,在移动设备上也会工作得很好。

当切换到横向模式时,水平空间的显著增加和垂直空间的巨大损失使得垂直LinearLayout成为一个糟糕的选择。如果你正在处理一个简单的输入表单,那么横向布局应该使用TableLayoutRelativeLayout来将标签放置在与它们相关的输入控件同一行上。

在横向布局上提供文本输入

在构建你的横向布局时,你需要仔细考虑用户界面的哪些部分最重要。如果屏幕被用来编写电子邮件或文档,你的横向布局可能与纵向布局几乎相同。然而,这样的布局有一个几乎隐藏的敌人:软件键盘。在纵向布局中,软件键盘会限制在屏幕底部,并占用相对较小的空间(大约四分之一到三分之一的可用屏幕空间)。然而,在横向布局中,软件键盘可能会占用你一半的垂直屏幕空间,使得构建以内容为中心的横向布局变得非常困难。如果你的布局是强烈以输入驱动的,那么在横屏模式下移除用户界面的一部分,或者重新设计用户界面,使得软件键盘不会妨碍,可能是合理的。

安卓提供了一系列的配置参数,可以告诉您关于运行您应用程序设备上的键盘信息。在构建应用程序时考虑所有可能性是一个好主意。以下是应用程序可能面临的一些可能的键盘情况简短列表:

  • 只有软件键盘

  • 硬件键盘

  • 硬件键盘可用;软件键盘在使用中

除了这些可能性,屏幕较小的设备通常会使用 12 键键盘而不是全 QWERTY 键盘。如果这是软件键盘(通常是这种情况),键盘可能占用高达 80%的屏幕空间。当用户激活文本输入框时,Android 通常会通过打开“文本输入”屏幕来处理这个问题。你可以通过以下配置参数确定键盘的可使用状态和使用的键盘类型:

  • nokeys:仅限软件键盘

  • qwerty:可以使用完整的硬件键盘

  • 12key:可以使用 12 键硬件手机键盘

  • keysexposed:用户可以看到键盘,无论是硬件还是软件的

  • keyshidden:当前没有任何键盘可见

  • keyssoft:用户将使用软件键盘(尽管它可能不可见)

在设计屏幕时,请考虑软件键盘可能占用你一半的垂直空间。确保内容区域可以滚动,而重要的控件将始终在屏幕上可见。如果一个聊天应用程序简单地被包裹在ScrollView中,当软件键盘可见时,输入EditView对象可能会变得不可见。考虑屏幕的外观不仅仅是如何,还要考虑它将如何应对用户可能带来的变化。最后,测试屏幕在有无软件键盘的情况下看起来和表现如何是至关重要的。

更改屏幕内容

Android XML 布局格式的一大优势是它提供的解耦。竖屏和横屏布局通常彼此差异很大,用户可能会分别找到一个更喜欢的方向来使用你的应用程序。在设计新布局时,一个不太常见但有用的技巧是能够从两个不同的布局中添加或删除“非功能性”元素。

在一个简单的例子中,你可能想要在竖屏布局中缩写标签文本,并包含一些图标作为图形提示,而在横屏布局中,你可能希望图标大小加倍并使用两行标签,所有这些都位于输入字段同一行。

下图阐述了这一概念:

更改屏幕内容

在前述图表的横屏布局中,你可以使用额外的TextView元素来显示标签的子文本。假设你的 Java 代码没有寻找额外的TextView对象,你的应用程序将完美运行。在设计Activity的替代布局时,能够更改用户界面的实际结构而不仅仅是布局,这是一个非常重要的考虑因素。

总结

应用程序的外观和感觉至关重要。对颜色或字体的一次更改就可能会影响屏幕的可用性。同时,过度设计应用程序可能会让它在使用者的设备上显得不协调。一个陌生的外观和感觉会将用户从该应用程序推向那些看起来和感觉更熟悉和舒适的应用程序。

Android 使用样式资源结构提供了一系列极其强大的功能。结合将你的图形放置在资源文件中并覆盖默认值的能力,你可以有效地重新设计任何小部件。使用样式也有助于维护你的应用程序,因为你只需要在样式资源中更改样式,而不是在每个特定样式的部件声明中进行更改。

将你的大部分小部件图形作为 <shape> 资源,将确保你的应用程序具有尽可能一致的外观和感觉。然而,这并不总是实用的。当你需要提供位图资源时,为用户可能使用的各种屏幕尺寸和密度提供不同的图像至关重要。

应用程序的风格设计还包括布局以及应用程序适应其运行设备的能力。拥有一个伟大的想法仅是应用程序吸引力的的一半,其风格和执行对它在“野外”的生存至关重要。关注细节是一个强大的工具,将吸引用户使用你的应用程序。那些“即开即用”的应用程序总是比那些需要时间和精力才能使用的应用程序更受欢迎。

利用 Android 模拟器提供的各种屏幕尺寸和密度,以确保你的应用程序能在尽可能多的设备上看起来良好。不要忘记,许多设备没有硬件键盘,而且软件键盘可能会占用你屏幕空间的一半。

在下一章中,我们将把这种样式知识扩展到应用程序的整体设计和主题。我们将构建一个具有许多提供布局的样式化应用程序,并进行相当广泛的样式设计。

第十章:构建应用程序主题

无论是否涉及图形样式,每个应用程序都有一个主题。应用程序的主题使其具有独特的外观和逻辑。

当用户使用移动应用程序(大多数安卓设备的情况)时,与台式机或笔记本电脑相比,他们的行为有一些根本性的不同:

  • 他们通常在应用程序上的时间更少,因此耐心也更小

  • 他们通常一次只专注于一个应用程序

  • 触摸屏设备鼓励用户进行近乎触觉的交互响应

安卓设备种类繁多,几乎兼容所有设备,包括常见的手机、平板、笔记本电脑,甚至一些桌面电脑。一个安卓应用程序预期在这些环境中都能良好运行,应用的主题应精心构建,以便用户在各种设备上获得最佳访问体验。

设备界面构成了你的应用程序主题的一部分。在台式机或笔记本电脑上使用鼠标时,仅考虑触摸屏的用户界面可能对用户来说会显得过大(因为所有控件都需要适合手指大小)。相反,为鼠标驱动的系统设计的应用程序通常会包含悬停效果,这在触摸屏设备上无法正常工作。确保你的应用程序在所有这些不同设备上都能正常工作的唯一方法是,在构建应用程序屏幕时考虑所有这些环境。

安卓自身定义了一种主题,尽可能的话,为安卓平台构建的应用程序应尝试符合或扩展这一主题,而不是重新定义。这并不意味着你的应用程序必须看起来和行为与其他所有安卓应用程序完全相同,但你的应用程序应该基于安卓所设定的基本原则。

注意

请记住,许多设备制造商对基本的安卓主题定义了额外的部分,你的应用程序也应如此。

在本章中,我们将探讨应用程序的构建,包括屏幕设计、构建和样式设计。我们还将研究此应用程序如何与各种不同设备交互,确保其外观和功能符合用户预期。我们将构建一个计算器应用程序,包含标准计算器和科学计算器功能。计算器将设计得更像物理计算器而非普通的安卓应用,并根据运行设备的性能调整其功能。总体而言,我们将定义一个具有自身一致主题的应用程序。

创建基本的计算器布局

要构建这个项目,我们首先需要一个标准的计算器的基本纵向布局。这个基本布局将作为用户首次启动应用程序时所看到的屏幕。鉴于计算器应用程序的性质以及用户对它的感知,屏幕简单且应用程序启动越快越好,这一点非常重要。

提示

计算器屏幕占据所有可用空间的功能性组件非常重要,以使其尽可能快地使用(更大的按钮等于更容易使用)。

小测验

  1. 布局资源何时变成 Java 类?

    1. 当运行资源处理器时

    2. 当应用程序包被构建时

    3. 当布局资源被加载时

    4. 从不

  2. 你如何引用那些在 Android 中默认未定义的小部件?

    1. 通过使用完整的类名作为元素名称

    2. 通过为 Java 包定义一个 XML 命名空间

    3. 目前不可能

    4. 通过在 android:package 属性中指定 Java 包名

  3. 一个 View 对象的默认宽度和高度是什么?

    1. 它内容的大小

    2. 零像素

    3. 它取决于它所在的 ViewGroup

    4. 它父级的宽度和内容的高度

  4. 你将布局资源写成 XML,它以什么格式存储?

    1. 作为原始 XML 文本

    2. Android 二进制 XML

    3. 布局特定的二进制格式

    4. Java 类

设计一个标准计算器

在开始构建计算器应用程序之前,最好先勾勒出它将是什么样子。这将帮助你决定如何确切地构建屏幕。由于计算器既是一个相当古老发明的东西,也是人们非常熟悉的东西,因此遵循最常见的设计非常重要。如果你推出的计算器对人们来说太陌生,他们可能没有耐心去“了解”你的应用程序。新想法是好的(即滑动键盘),但最成功的还是现有想法的延伸。同时,要向用户明确它们的工作方式。以下是我们将开始构建的标准计算器屏幕的区块图:

设计一个标准计算器

我们要最大化利用屏幕空间,因此我们会尽可能使按钮变大。同时,我们希望按钮之间稍微留点空隙,以避免用户不小心按下不想按的按钮。由于我们只有一个输出区域,我们会确保显示区域也足够大。

显示区域中的箭头将是一个图标,作为退格按钮,允许用户删除不需要的内容。给用户提供一种撤销操作的方法始终很重要。我们将使用与拨号应用中类似的图标,这将保持与系统的其他部分的整体一致性。这也有效地为我们提供了额外按钮的空间。这个用户界面不包括许多计算器所关联的常规“记忆”功能。基本屏幕设计得尽可能简单,我们将在开发应用程序时引入更多功能。

动手操作——构建标准计算器

计算器的第一个布局将由一系列正常的09的按钮组成,以及用于各种基本算术运算的按钮——加、减、乘、除。它还将包括等于号按钮和小数点按钮。尽管在 Java 代码中构建这样一个简单的屏幕非常容易,但我们将完全使用 XML 资源来构建这个示例。由于这个应用程序将具有相同屏幕的几种不同排列组合,使用不带 Java 代码的布局资源文件将使你的生活更加轻松。

  1. 首先,为计算器创建一个新项目:

    android create project -n Calculator -p Calculator -k com.packtpub.calculator -a CalculatorActivity -t 3
    
  2. 打开标准的主体布局文件/res/layout/main.xml

  3. 从文件中删除生成的布局结构。

  4. 首先,声明一个垂直的LinearLayout作为根元素,以占据屏幕上所有可用空间:

    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  5. 声明一个RelativeLayout,它将由显示和用户可以用来删除不需要输入的删除取消按钮组成:

    <RelativeLayout android:layout_width="fill_parent"
                    android:layout_height="wrap_content">
    
  6. RelativeLayout的右侧使用ImageView显示标准的 Android 输入删除图标:

    <ImageView android:id="@+id/delete"
               android:src="img/ic_input_delete"
               android:layout_centerInParent="true"
               android:layout_alignParentRight="true"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"/>
    
  7. RelativeLayout的左侧,创建一个TextView,它将实际显示计算器的数字状态:

    <TextView android:id="@+id/display"
              android:text="0"
              android:layout_alignParentTop="true"
              android:layout_toLeftOf="@id/delete"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  8. LinearLayout内部,声明一个TableLayout,用于包含简单计算器的按钮输入:

    <TableLayout android:id="@+id/standard_functions"
                 android:layout_width="fill_parent"
                 android:layout_height="fill_parent"
                 android:layout_margin="0px"
                 android:stretchColumns="0,1,2,3">
    
  9. TableLayout将由四个TableRow对象组成。声明第一个对象,无边距且layout_weight1

    <TableRow android:layout_margin="0px"  
              android:layout_weight="1">
    
  10. 右上角的Button对象需要是plus符号,我们也将其用作Button ID 的名称:

    <Button android:id="@+id/plus"
            android:text="+"/>
    
  11. 第一行接下来的三个Button对象将是数字123。这些也需要 ID:

    <Button android:id="@+id/one"
            android:text="1"/>
    <Button android:id="@+id/two"
            android:text="2"/>
    <Button android:id="@+id/three"
            android:text="3"/>
    
  12. 继续按块状图定义的顺序声明带有按钮的TableRow对象。

  13. 在编辑器或 IDE 中打开CalculatorActivity.java源文件。

  14. onCreate方法中,确保将Activity的内容视图设置为刚才定义的main布局:

    setContentView(R.layout.main);
    

刚才发生了什么?

现在你应该已经为计算器创建了一个基本用户界面;尽管它仍然看起来像一个非常通用的 Android 应用程序,但这至少是从基础层面开始的。用户界面需要做一些样式设计工作,包括着色和一些字体更改,但基本结构现在已经完成。使用RelativeLayout是为了确保我们可以正确地将删除图标定位在TextView的右侧,无论屏幕大小如何。

为了让按钮尽可能占用可用空间,我们告诉TableLayout拉伸其所有列。如果TableLayout不拉伸其列,那么它将只占用其子项所需的水平空间(实际上与wrap_content宽度相同)。尽管告诉TableLayout也占用所有垂直空间,但其子项将根据它们所需的空间进行大小调整,这就是为什么按钮没有占用所有可用屏幕空间的原因。以下图像是基本计算器在模拟器中运行时的截图:

刚才发生了什么?

构建计算器样式。

我们真的希望这个计算器看起来更像一个真正的计算器,为此我们需要应用一些样式。目前计算器的主题完全是标准的 Android 主题,尽管它看起来与 Android 系统的其他部分完全一样,但它并不真正适合这个应用程序。我们希望对按钮和应用程序的显示区域进行样式设计。我们将在资源文件中定义样式值,并在布局 XML 文件中引用这些样式。

首先,我们将定义一系列九宫格图像来创建我们自己的按钮设计。为此,我们需要三张不同的图片。第一张是按钮的“正常”状态,第二张是按钮的“按下”状态,最后是按钮的“聚焦”状态。

小测验。

  1. 九宫格图像边缘周围的黑色线条是做什么的?

    1. 提供给系统关于图像中哪些部分需要复制的提示。

    2. 指示图像中哪些部分需要缩放以及小部件内容放置的位置。

    3. 定义图像中包含元信息的内容部分。

  2. 九宫格图像可以存储为什么格式?

    1. JPEG、GIF 或 PNG 图像文件。

    2. 嵌入 TIFF 的 XML 文件。

    3. 可移植网络图形图像(Portable Network Graphic image)。

  3. draw9patch应用程序是做什么的?

    1. 在各种形状和大小中渲染九宫格图像。

    2. 这是一个用于绘制九宫格图像的应用程序。

    3. 为九宫格图像生成元数据作为 XML 文件。

动手操作——创建按钮图像。

为了在本节中构建按钮图像,你需要下载“GIMP”(可在www.gimp.org获取)。它非常适合这种图像创建或操作,而且它还有一个开源的优势。

  1. 打开“GIMP”,选择文件 | 新建以创建新图像。

  2. 将宽度和高度更改为38x38像素。

  3. 打开高级选项并将填充为选项更改为透明,这样就没有背景色了。

  4. 为了帮助调整大小,放大至大约800%

  5. 在工具箱左上角选择矩形工具(默认快捷键是R)。

  6. 启用圆角选项并将其设置为5

  7. 启用固定选项,并在下拉列表中选择大小

  8. 输入36x36作为矩形选择的固定大小。

  9. 将选择框放在图像画布中心,选择框和图像边缘之间应该有一个单像素的边界。

  10. 双击工具箱中的“前景色”(默认为黑色)。

  11. 在颜色选择器的十六进制表示框中输入444444

  12. 关闭颜色选择器对话框。

  13. 在工具箱中选择桶填充工具(默认快捷键是Shift-B)。

  14. 在选择框内部点击,用选定的颜色填充它。

  15. 使用选择菜单,点击选项以移除选择框。

  16. 选择滤镜 | 装饰 | 添加斜角

  17. 厚度选项更改为3

  18. 取消勾选在副本上工作选项,并点击确定按钮。

  19. 再次从工具箱中选择矩形工具。

  20. 取消勾选圆角固定选项。

  21. 使用选择工具在“按钮”形状内部选择一个单像素宽的垂直框,小心只选择按钮内容区域的一部分,避开斜角边框空间:行动时间 – 创建按钮图像

  22. 将光标放在选择框中间,将选择框水平拖动至图像画布边缘(在单像素边界内)。

  23. 再次双击“前景”矩形。

  24. 将颜色重置为纯黑色。

  25. 选择桶填充选项。

  26. 在选择框内部点击,创建一个单像素宽,黑色的垂直线条在图像左侧。

  27. 在图像右侧创建一个类似的垂直线条。

  28. 在图像的顶部和底部创建一个单像素高的水平黑色线条。

  29. 在你的res/drawable目录中将图像保存为button.9.png,保持 PNG 选项为默认值。

  30. 重复上述过程,将前景色444444更改为如步骤 11 中的c16400,并将新的图像保存为button_focus.9.png

使用翻转工具(默认快捷键Shift + F)翻转图像,你将创建button_down.9.png图像。

刚才发生了什么?

虽然构建图像有许多步骤,但使用正确的工具并进行一些实验,它们本质上非常容易创建。如果你只需要一个简单的按钮或类似的东西,那么找一些关于如何使用“GIMP”或类似工具的教程是很有价值的。以下链接有一些在线教程:

你在上一个部分保存的图像应该看起来像我为我的计算器应用程序创建的以下图像:

刚才发生了什么?

动手时间——美化计算器按钮

接下来我们需要做的是使用选择器列表和你刚刚创建的九宫格图像来设置计算器按钮的样式。我们还将定义按钮样式在资源文件中,这样我们就不必为每个按钮指定所有的样式。为了用我们创建的图像替换标准按钮,我们只需要用我们创建的背景替换它的背景。

  1. res/drawable目录中,创建一个名为button.xml的新 XML 文件,并在编辑器中打开它。

  2. 将文件的根元素定义为一个固定大小的选择器:

    <selector
    
        android:constantSize="true"
        android:variablePadding="false">
    
  3. 创建一个被按下的按钮状态,作为选择器的第一个子项:

    <item android:state_pressed="true" 
          android:drawable="@drawable/button_down"/>
    
  4. 选择器的第二个子项应该是获得焦点状态:

    <item android:state_focused="true"
          android:drawable="@drawable/button_focus"/>
    
  5. 选择器的最后一个子项是通用的,是正常状态:

    <item android:drawable="@drawable/button"/>
    
  6. res/values目录中创建一个名为styles.xml的新文件,并在编辑器中打开它。

  7. styles.xml文件的根元素应该是一个没有命名空间声明的资源元素(在这个文件中不需要):

    <resources>
    
  8. 在文件中定义第一个样式为CalculatorButton,其父样式为默认的 Android Button小部件样式:

    <style name="CalculatorButton"
           parent="@android:style/Widget.Button">
    
  9. 将文本大小设置为一种美观的大字体和浅灰色:

    <item name="android:textSize">30sp</item>
    <item name="android:textColor">#ffcacaca</item>
    
  10. 将样式的背景指定为新的button可绘制资源:

    <item name="android:background">@drawable/button</item>
    
  11. 在每个Button小部件周围创建一个两像素的边框,以创建一点间距:

    <item name="android:layout_margin">2dp</item>
    
  12. 确保让Button小部件消耗它们可用的所有垂直空间:

    <item name="android:layout_height">fill_parent</item>
    
  13. 在编辑器中打开main.xml布局资源。

  14. 在每个Button元素上,添加一个样式属性,以赋予你刚刚在styles.xml文件中定义的样式:

    <Button style="@style/CalculatorButton"
            android:id="@+id/multiply"
            android:text="*"/>
    

刚才发生了什么?

我们刚刚为计算器屏幕重新设计了Button对象。这个样式是标准 Android Button小部件的子样式。新的样式主要是通过将背景图像更改为我们之前创建的九宫格图像来驱动的。为了与新的背景图像一起工作,我们还指定了字体颜色和大小。运行时,新的计算器用户界面将如下截图所示:

刚才发生了什么?

在原始代码中,没有指定按钮周围的边距,但在新代码中,我们在自定义样式中添加了明确的边距。我们的九宫格图像在内容区域周围没有填充。

你会注意到我们在布局中为每个Button小部件设置样式。正如在前一章中提到的,样式属性不是 Android 资源命名空间的一部分。不幸的是,Android 目前不允许我们为特定类的所有小部件设置样式。相反,我们只能选择为每个小部件单独设置样式,或者在Activity或应用程序中为所有小部件设置相同的样式。作为新Button样式的一部分,我们声明了一个<selector>资源的可绘制资源。与标签结构一样,Button对象可以被样式化为使用不同的可绘制资源来表示它们的不同状态。在这种情况下,我们为Button被聚焦、按下或处于正常状态时指定背景图像。样式只适用于背景图像,因为新Button对象的背景是<selector>资源。

行动时间——设置显示样式

目前,数字显示看起来确实相当糟糕。这主要是因为我们还没有为其设置任何样式,现在它只是一个普通的TextView对象。我们希望样式能够同时涵盖TextView对象和ImageView。当前的显示效果如下截图所示:

行动时间——设置显示样式

为了修复这个显示,并将其样式与我们的新Button样式保持一致,我们将创建两种不同的样式。一种是在TextViewImageView对象周围创建边框和背景,另一种是用更合适的字体样式化TextView小部件。

  1. 创建一个名为display_background.xml的新可绘制资源文件,并在你的编辑器或 IDE 中打开它。

  2. 显示背景的根需要是一个矩形形状:

    <shape    
    
        android:shape="rectangle">
    
  3. 声明一些内边距来缩进文本和图像:

    <padding
        android:top="5sp"
        android:bottom="5sp"
        android:left="15sp"
        android:right="15sp"/>
    
  4. 为矩形创建一个纯灰背景色:

    <solid android:color="#ffcccccc"/>
    
  5. 指定描边大小,并将其颜色设置为白色:

    <stroke android:width="2px"
            android:color="#ffffffff"/>
    
  6. 在你的编辑器或 IDE 中打开res/values/styles.xml文件。

  7. 为显示包装器添加一个新的<style>项,并将新样式命名为没有父样式的CalculatorDisplay

    <style name="CalculatorDisplay">
    Set the background as the display_background:<item name="android:background">
        @drawable/display_background
    </item>
    
  8. 在显示包装器下方创建一个小边距:

    <item name="android:layout_marginBottom">25sp</item>
    
  9. 在显示上方添加一些内边距:

    <item name="android:layout_marginTop">50sp</item>
    
  10. 以名称CalculatorTextDisplay开始一个新的<style>元素,父样式应该是标准的TextView样式:

    <style name="CalculatorTextDisplay"
           parent="@android:style/TextAppearance">
    
  11. 在新样式中,将字体设置为45像素,黑色等宽字体:

    <item name="android:typeface">monospace</item>
    <item name="android:textSize">45sp</item>
    <item name="android:textColor">#ff000000</item>
    
  12. 计算器显示的文本应该是右对齐的,因此我们还将指定应用到TextView的重力属性:

    <item name="android:gravity">right</item>
    
  13. 在你的编辑器或 IDE 中打开res/layout/main.xml文件。

  14. RelativeLayout的样式指定为CalculatorDisplay

    <RelativeLayout style="@style/CalculatorDisplay"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content">
    
  15. 设置显示的TextView样式:

    <TextView android:id="@+id/display"
              style="@style/CalculatorTextDisplay"
              android:text="0"
              android:layout_alignParentTop="true"
              android:layout_toLeftOf="@id/delete"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    

刚才发生了什么?

新的样式适用于围绕TextView对象和ImageView对象的RelativeLayout。通过设置这个RelativeLayout的样式,你实际上将TextViewImageView作为一个单一的小部件合并在一起。如果你看以下截图,你会看到这是如何为你的用户工作的:

发生了什么?

TextView对象上下的边距会减少按钮可用的空间。在长垂直空间中,按钮通常会变长,看起来不成比例,因此通过为显示区域添加边距,我们可以帮助保持按钮更接近正方形。

尝试英雄——添加计算器逻辑

目前,我们有一个简单计算器的优秀用户界面。然而,它只是一个看起来很不错的用户界面。接下来要做的就是在其中添加一些逻辑。

以下是完成功能计算器所需的步骤:

  1. 实现OnClickListener接口,并将其注册到用户界面上的每个Button小部件。

  2. 创建一个新的Calculator类来处理实际的计算,并存储计算器的非用户界面状态。

  3. 使用StringBuilder类实现当前输入值的构建和显示。

  4. 使用double数据类型实现基本计算,以便处理带小数点的数字。

突击测验

  1. 当从布局中选取字符串时,字符串是如何被选中的?

    1. 直接从根目录values字符串资源

    2. 从与布局相同的目录中的strings.xml文件

    3. 从与当前配置最接近匹配且包含请求名称字符串的values目录

    4. 从具有与布局资源文件选择相同的限定符的values目录

  2. 放置样式资源的正确文件名是什么?

    1. values目录中的任何文件

    2. styles.xml

    3. values.xml

    4. theme.xml

  3. 在 Java 代码中选取资源与从 XML 资源文件中选取资源有何不同?

    1. Java 资源选择更快

    2. XML 资源只能引用具有相同配置限定符集合的其他资源。

    3. 没有显著差异

    4. XML 资源只能引用所有资源类型的一个子集。

科学景观布局

计算器的科学布局不仅仅是更多按钮的问题,因为当设备处于横屏方向时,我们希望使用此布局。这意味着我们可用的垂直空间大大减少,而标准布局占用了很多这样的空间。为了构建这个新的用户界面,我们不仅要定义一个新的布局资源,还要为新的布局添加额外的样式。

科学布局在其新按钮上也使用了更复杂的文本。一些数学函数,如平方根或反余弦,有特定的表示法应该被使用。在这些情况下,我们将需要使用 HTML 样式或特殊字符。幸运的是,Android 完全支持 UTF-8 字符集,在功能和字体渲染方面都支持,这使得这个过程变得容易得多。

为科学布局定义字符串资源

对于科学功能,我们将每个功能的字符串内容定义为一个资源字符串。这既是为了使它们成为资源选择过程的一个独立部分(这总是推荐的),同时也是为了让我们利用自动的 HTML 处理。如果你在字符串资源中使用 HTML,当使用 Resources.getText 方法访问时,资源处理器会自动解析该 HTML,而不是通常的 Resources.getString 方法。这正是 TextView 类加载其字符串资源的方式,使得将文本内容放在 values 资源文件中更具吸引力。

以下是我的 values 目录中 strings.xml 文件的内容。你会注意到这里的 HTML 标记是 HTML 3.2,而不是基于 HTML 4 的。这是因为 Android 的 Html 类不能处理 HTML 4 标记,而 Html 类实际上是用来加载包含标记的字符串资源的。在 res/values 目录中创建一个新的资源文件,命名为 strings.xml,并将以下代码片段复制到新文件中:

<resources>
    <string name="inverse">1/x</string>
    <string name="square">
        x<sup><font size="10">2</font></sup>
    </string>
    <string name="cube">
        x<sup><font size="10">3</font></sup>
    </string>
    <string name="pow">
        y<sup><font size="10">x</font></sup>
    </string>
    <string name="percent">%</string>

    <string name="cos">cos</string>
    <string name="sin">sin</string>
    <string name="tan">tan</string>
    <string name="log2">
        log<sub><font size="10">2</font></sub>
    </string>
    <string name="log10">
        log<sub><font size="10">10</font></sub>
    </string>

    <string name="acos">
        cos<sup><font size="10">-1</font></sup>
    </string>
    <string name="asin">
        sin<sup><font size="10">-1</font></sup>
    </string>
    <string name="atan">
        tan<sup><font size="10">-1</font></sup>
    </string>
    <string name="log">log</string>
    <string name="log1p">log1p</string>

    <string name="e"><i>e</i></string>
 <string name="pi">π</string>
    <string name="random">rnd</string>
 <string name="sqrt">√</string>
    <string name="hyp">hyp</string>
</resources>

pisqrt 字符串值中的 Unicode 十六进制值用于引用小写希腊字母 Pi 符号和标准的平方根符号。

设计科学布局的样式

标准计算器布局中使用的样式对于科学布局来说并不是很好。为了改变科学布局的样式,你可以将新样式添加到横屏布局的新 values 目录中。将以下代码片段复制到名为 res/values-land/styles.xml 的新文件中:

<resources>
    <style name="CalculatorDisplay">
        <item name="android:background">
            @drawable/display_background
        </item>
    </style>

    <style name="ScientificButton" parent="style/CalculatorButton">
        <item name="android:textSize">12sp</item>
    </style>
</resources>

前一个片段中的第一个样式资源用于计算器的显示区域。与标准计算器一样,我们使用本章前面编写的 display_background 形状。我们还为科学按钮定义了一种新样式。科学按钮将与标准计算器按钮完全相同,只是字体要小得多。由于科学按钮比标准按钮多得多,较小的字体使我们能够更舒适地在屏幕上容纳更多按钮。

构建科学布局

科学计算器布局包括屏幕右侧的标准计算器按钮,以及屏幕左侧的二十个附加按钮。这些附加按钮代表数学函数和常数,其中大部分可以在java.lang.Mathjava.lang.StrictMath类中找到。下图展示了我们想要布局的科学计算器样式:

构建科学计算器布局

新样式对横向布局的计算器显示效果将“移除”显示和按钮之间的边距。由于横向布局的垂直空间较少,这样的填充除了是浪费空间之外,什么也不是,应该用来给按钮以保持合理的大小。

动手时间——编写科学计算器布局代码

横向布局被分割成多个子布局,以便为两个独立的功能区域保持 ID:科学函数和标准函数。为它们分配自己的 ID 值可以更容易地从 Java 代码中检测到可用的功能。这样,Java 代码就可以使用findViewById并测试null来检查科学功能是否可用,而不是基于配置决定可用的功能。这和 JavaScript 中的“能力测试”(相对于检查)非常相似。

  1. 创建一个名为res/layout-land的新资源目录。

  2. layout-land目录中创建一个新的布局资源 XML 文件,名为main.xml,并在编辑器或 IDE 中打开此文件。

  3. 将新布局的根元素声明为一个垂直的LinearLayout,占据所有可用的屏幕空间:

    <LinearLayout
    
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
  4. 新布局的第一个元素是一个RelativeLayout,用来包裹作为计算器显示的TextViewImageView

    <RelativeLayout style="@style/CalculatorDisplay"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content">
    
  5. 从标准计算器布局(res/layout/main.xml)复制TextViewImageView元素,作为之前声明的RelativeLayout的两个子元素:

    <ImageView android:id="@+id/delete"
               android:src="img/ic_input_delete"
               android:layout_centerInParent="true"
               android:layout_alignParentRight="true"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"/>
    <TextView android:id="@+id/display"
              style="@style/CalculatorTextDisplay"
              android:text="0"
              android:layout_alignParentTop="true"
              android:layout_toLeftOf="@id/delete"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  6. LinearLayout的第二个子元素是一个水平方向的LinearLayout,占据屏幕剩余空间:

    <LinearLayout android:orientation="horizontal"
                  android:layout_width="fill_parent"
                  android:layout_height="fill_parent">
    
  7. 在新的LinearLayout子元素内,声明一个新的TableLayout来填充科学按钮:

    <TableLayout android:id="@+id/scientific_functions"
                 android:layout_width="wrap_content"
                 android:layout_height="fill_parent"
                 android:layout_marginRight="10dip">
    
  8. scientific_functions TableLayout内创建一个TableRow元素,以包含第一行科学Button元素:

    <TableRow android:layout_margin="0px"
              android:layout_weight="1">
    
  9. 在新的TableRow内声明前五个科学函数作为Button元素。Button的 ID 应与用作Button标签的资源字符串名称相同:

    <Button style="@style/ScientificButton"
            android:id="@+id/inverse"
            android:text="@string/inverse"/>
    
  10. 第一行科学Button小部件包含inversesquarecubepowpercent

  11. 创建一个TableRow,其中包含第二行科学Button小部件,包括cossintanlog2log10

  12. 第三行TableRow中的第三个科学Button小部件应为acosasinatanloglog1p

  13. 第四个也是最后一个包含Button小部件的TableRow应该是epirandomsqrthyp

  14. 这就是所有的科学函数,现在在LinearLayout子元素中为标准函数创建另一个TableLayout

    <TableLayout android:id="@+id/standard_functions"
                 android:layout_width="fill_parent"
                 android:layout_height="fill_parent"
                 android:layout_margin="0px"
                 android:stretchColumns="0,1,2,3">
    
  15. res/layout/main.xml中的standard_functions TableLayout内容复制到新的TableLayout元素中。

刚才发生了什么?

在前面的布局中,我们重用了在标准计算器布局中创建的大部分基础内容,并添加了一个新的TableLayout结构来包含科学函数。新的TableLayout被设置为wrap_content的宽度,并且只占用容纳所有Button小部件所需的水平空间。两个TableLayout元素之间的主要区别在于,科学表格没有拉伸其列,因为这实际上与将其设置为fill_parent相同,这样就没有空间放置标准函数了。

你还会注意到,在用于创建科学Button标签的字符串资源中,那些使用 HTML 标记的,没有使用 XML 转义实体(如&lt;&gt;)。这是告诉资源编译器一个字符串资源包含标记,并且应该以不同方式处理的主要指示。这种使用要求所有放入字符串资源中的 HTML 标记必须符合 HTML 3.2 规范,并且仍然是有效的 XML 内容。

为了测试新的横屏布局,你需要定义一个具有横屏大小的模拟器设备,或者在物理设备上运行应用程序。在模拟器中创建虚拟设备可以使用 Android SDK 安装目录中tools目录下的android应用程序,这个工具也用于创建项目框架。以下是新布局在物理 Android 设备上运行时的截图:

刚才发生了什么?

动手实践——在现有布局中使用 include

前面的布局重用了标准布局的几个元素。现在是把这些元素提取到它们自己的布局文件中的好时机,然后使用include元素将它们放置到两个特定的布局资源中。第五章 开发非线性布局 中介绍了布局包含的相关信息。

  1. 创建一个display.xml布局资源,包含带有计算器显示的RelativeLayout,并将其包含在main.xml布局资源文件中的适当位置。

  2. 创建一个standard_buttons.xml布局资源,包含名为standard_functionsTableLayout,并将其包含在main.xml布局资源文件中的适当位置。

处理活动重新启动

当设备改变方向时,屏幕上的CalculatorActivity对象会以新方向重新启动。在这个应用中,重新启动会导致一个严重的问题:计算器的状态丢失。正如第四章 利用活动和意图 中讨论的那样,有时你需要控制 Android 应用的状态——在关机前保存它,并在Activity再次启动时恢复它。

你需要重写Activity.onSaveInstanceState方法,以在提供的Bundle中存储计算器的当前状态。这个Bundle对象将在由于配置更改而重新启动时在onCreate方法中提供给你。在你的onCreate方法中,确保在从它恢复保存的参数之前,提供的Bundle对象非空。

尝试英雄——实现科学计算逻辑

目前计算器应该能够从标准计算按钮进行操作。然而,新的科学功能没有任何支持结构。此外,如果你重新调整设备方向以在科学和标准布局之间切换,任何“进行中”的计算都会丢失。

为了使科学计算按预期工作,需要完成以下步骤:

  1. 实现onSaveInstanceState以将计算状态保存到提供的Bundle对象。

  2. 实现onCreate方法,从提供的Bundle对象(假设有的话)恢复保存的状态。

  3. 向你之前编写的Calculator类中添加所需的功能,使科学Button小部件按预期工作。

支持硬件键盘

我们在这里开发的计算器现在是一个很棒的 Android 屏幕计算器应用程序,具有你所期望的简单和科学功能。然而,如果一个设备有硬件键盘,用户可能会期望能够使用它,目前他们做不到。此外,如果设备没有触摸屏,点击屏幕按钮会很快变得令人沮丧。我们需要为应用程序实现硬件键盘支持。

实现硬件键盘处理代码只有在你完成了“尝试英雄”部分并构建了一个Calculator类来执行所需功能时才有用。为了处理硬件键盘事件,你会使用KeyEvent.Callback接口中声明的方法。Activity类已经实现了KeyEvent.Callback接口,并为所有方法提供了默认处理。对于这些按键事件的处理,我们只需要覆盖onKeyDown方法。

对于这个onKeyDown实现,最好确保按键事件来自硬件键盘,方法是检查KeyEvent的标志。在自行处理之前,将其传递给父类也是一个好主意。最后,如果你在 Android 2.0(API 级别 5)或更高版本上工作,你应该在处理之前检查KeyEvent是否没有被取消(这也是KeyEvent标志之一)。以下是我的onKeyDown方法实现中的代码片段:

@Override
public boolean onKeyDown(
        final int keyCode,
        final KeyEvent event) {

    super.onKeyDown(keyCode, event);

    boolean handled = false;

    if((event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) == 0) {
 switch(keyCode) {
 case KeyEvent.KEYCODE_0:
 calculator.zero();
 handled = true;
 break;
 case KeyEvent.KEYCODE_1:
 calculator.one();
 handled = true;
 break;
 // Cases for each of the handles keys
 }

        display.setText(calculator.getCurrentDisplay());
    }

    return handled;
}

上述代码片段调用了每种可以在硬件键盘上按下的不同键的方法。

注意事项

如果你的 Android 设备没有硬件键盘,你可以使用模拟器测试这段代码——你的 PC 键盘和模拟器显示右侧的屏幕键盘都被模拟器归类为硬件键盘。

添加显示动画

目前,该应用程序具备成为一个优秀计算器应用程序的所有要素。然而,当前显示只是一个简单的TextView对象。为了提升用户体验,我们应该使用ViewSwitcher对象在计算器操作更改或按下“等于”按钮时替换TextView

动作时间——显示动画

为了为计算器显示构建一个漂亮的滑出滑入动画,我们需要定义自己的动画并将它们绑定到ViewSwitcher对象。这也需要我们修改 Java 代码以处理新的机制。由于我们不想在每次输入新数字时都让视图动画化,我们将直接更改当前屏幕上的TextView

  1. res/anim目录中创建一个名为slide_out_top.xml的新 XML 资源文件,并在编辑器或 IDE 中打开它。

  2. 在动画资源中声明一个从0%100%的 y 轴平移动画作为唯一的元素:

    <translate
    
        android:fromYDelta="0%"
        android:toYDelta="-100%"
        android:duration="300"/>
    
  3. res/anim目录中创建一个名为slide_in_bottom.xml的新 XML 资源文件,并在编辑器或 IDE 中打开这个文件。

  4. 在动画资源中声明一个从100%0%的 y 轴平移动画作为唯一的元素:

    <translate
    
        android:fromYDelta="100%"
        android:toYDelta="0%"
        android:duration="300"/>
    
  5. 打开你的display.xml文件,或者在你的编辑器或 IDE 中打开两个main.xml文件,具体打开哪一个取决于你是否完成了“尝试英雄——布局包含”部分。

  6. 在用于显示的RelativeLayout中,使用两个新的动画资源将名为displayTextView替换为ViewSwitcher元素:

    <ViewSwitcher android:id="@+id/display"
                  android:inAnimation="@anim/slide_in_bottom"
                  android:outAnimation="@anim/slide_out_top"
                  android:layout_alignParentTop="true"
                  android:layout_toLeftOf="@id/delete"
                  android:layout_width="fill_parent"
                  android:layout_height="wrap_content">
    
  7. 作为ViewSwitcher的子元素,声明两个具有CalculatorTextDisplay样式的TextView元素:

    <TextView style="@style/CalculatorTextDisplay"
              android:text="0"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"/>
    
  8. 两个TextView元素将彼此完全相同。

刚才发生了什么?

使用ViewSwitcher进行显示将导致现有 Java 代码崩溃,因为 Java 代码会期望该对象是某种TextView。你需要做的是使用ViewSwitcher.getCurrentView更新显示,而不是ViewSwitcher本身。

当使用操作Button时,例如乘或等于Button,你将希望将下一个显示内容放置在ViewSwitcher.getNextView小部件上,然后调用ViewSwitcher.showNext()方法。数字向上消失,新内容从显示底部出现的动画简单明了。这也是计算器应用程序中经常使用的,意味着用户通常会感到舒适。

在这个应用程序的案例中,动画更多的是视觉效果而非实用。然而,如果你在计算器中实现了一个历史栈,当用户按下“返回”Button时,动画可以反转。在计算器中,一个历史栈是一个非常实用的结构,因为它允许对同一计算进行轻微变化的反复运行。

动手英雄——圆角处理

在这一点上,这个计算器应用程序相当完整。它已经过样式设计,有一些不错的视觉效果,并且按预期工作。然而,它确实有一些注意事项——科学计算布局在小屏幕设备上工作得不是很好。以下截图是在小屏幕手机上以科学布局运行的应用程序:

动手英雄——圆角处理

前面的图片还展示了某些设备是如何为主题应用程序着色的。为了确保应用程序在所有设备上都能良好运行:

  1. 为小屏幕设备定义一个新的values目录。

  2. 在包含比默认样式边距和填充更小的样式的目录中创建一个新的styles.xml文件。

  3. 在具有横向取向的小屏幕设备上,减小display字体的大小。

这种圆角处理过程将遵循大多数成功的 Android 应用程序项目。这是关于在各种各样的模拟器配置和设备上尝试应用程序,然后利用资源加载器确保应用程序在尽可能多的设备上良好运行的问题。

总结

创建应用程序主题是新的应用程序成功的关键部分,无论运行在 Android、桌面还是 Web 上。我们已经探讨了如何利用 Android 提供的各种工具,以保持应用程序的一致性,从而使其对用户友好。

一个应用程序的主题及其外观和感觉远超出简单的样式设计。你个人使用应用程序的次数越多,你越会发现稍微不同的颜色或过渡动画会有所帮助的地方。每一个小的不同之处都使得应用程序真正对用户友好,因为它让应用程序看起来更加精致。

尽管运行在数百种截然不同的设备上,安卓让开发者能够轻松地保持应用程序运行,就像它们是为该硬件特别构建的一样。资源加载系统是安卓中最关键的结构之一,不利用它,对应用程序来说可能是自杀式的行为。

我强烈建议你熟悉现有的安卓应用程序,以及其他移动设备上的应用程序。了解如何使用像样的图像处理应用程序也会有很大帮助。在开始构建它们之前,为每个屏幕绘制一张图表,而铅笔和纸通常是了解用户界面想法的最佳方式,在你开始编码之前。

仔细考虑你在哪里可以使用现有的安卓图标和样式,以及你会在哪里想要替换或扩展它们。你总是希望保持应用程序的一致性,但添加一些炫目的视觉糖果往往能使应用程序从众多竞品中脱颖而出。

结合 XML 资源和 Java 语言,安卓是一个极具吸引力的设计和编码平台。它被广泛部署并拥有出色的开发者支持。有数十家硬件制造商在生产各种形状和大小的安卓设备,还有成千上万的开发者在开发应用程序。

在这本书中,我们致力于利用安卓平台构建以用户为中心、易于使用且界面美观的应用程序。安卓平台和安卓市场为新想法提供了固定的受众和巨大的曝光度。从现在开始,你应该能够为安卓生态系统添加你自己的独特想法和工作。任何已经完成的事情总是可以做得更好,而任何尚未完成的事情,都有人在等待。无论你是团队的一员,还是在夜晚的阁楼里努力开发下一个大项目,成功应用程序的关键在于一个出色的用户界面。

附录 A. 快速测验答案

第一章

布局作为 XML 文件

问题编号 答案
1 b
2 d
3 c

填充一个活动

问题编号 答案
1 b
2 c
3 c

第二章

列表视图和适配器

问题编号 答案
1 c
2 a
3 c

第三章

图库对象和 ImageViews

问题编号 答案
1 c
2 b
3 a

第四章

意图和活动

问题编号 答案
1 c
2 b
3 a

第五章:

自定义布局

问题编号 答案
1 d
2 b
3 c

第六章

文本输入

问题编号 答案
1 c
2 c
3 a

第八章

WebView 组件

问题编号 答案
1 d
2 b
3 d

WebView 与原生布局

问题编号 答案
1 a
2 c
3 c

第十章

布局资源

问题编号 答案
1 d 提示:(它们作为对象加载,而不是编译成类)
2 d
3 c

九宫格图片

问题编号 答案
1 b
2 c
3 a

安卓资源

问题编号 答案
1 c
2 a
3 c
posted @ 2024-05-23 11:07  绝不原创的飞龙  阅读(2)  评论(0编辑  收藏  举报