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

102 阅读1小时+

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

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

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

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

本书涵盖内容

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

你需要这本书的内容

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

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

developer.android.com/sdk/index.html

Android SDK 和 Android Studio 都是免费提供的。

这本书的目标读者

本书假设你对编程概念和 Android 基础知识有一定的了解。否则,如果你是 Android 新手,并且通过直接进入代码学习效果最佳,这本书提供了最常见任务的广泛范围。

作为一本“食谱”,您可以轻松跳转到您感兴趣的主题,并尽快使代码在您自己的应用程序中运行。

部分

在这本书中,你会发现几个经常出现的标题(准备工作、如何操作、工作原理、还有更多、另请参阅)。

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

准备工作

本节告诉您对食谱的期望,并描述如何设置食谱所需的任何软件或初步设置。

如何操作…

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

工作原理…

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

还有更多…

本节包含关于食谱的额外信息,以使读者对食谱有更多了解。

另请参阅

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

约定

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

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入将如下显示:"使用JsonObjectRequest()请求 JSON 响应基本上与StringRequest()相同。"

代码块设置如下:

<activity
    android:name=".MainActivity"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>

新术语和重要词汇以粗体显示。例如,在菜单或对话框中看到的屏幕上的单词,将如下在文本中出现:"当提示选择活动类型时,使用默认的电话和平板电脑选项,并选择空活动。"

注意

警告或重要注意事项会像这样出现在一个框里。

提示

提示和技巧会像这样出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者的反馈对我们很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

如需发送一般反馈,只需将电子邮件发送至<feedback@packtpub.com>,并在邮件的主题中提及书籍的标题。

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

客户支持

既然您是 Packt 图书的骄傲拥有者,我们有许多方法可以帮助您充分利用您的购买。

下载示例代码

您可以从您的www.packtpub.com账户下载本书的示例代码文件。如果您在别处购买了这本书,可以访问www.packtpub.com/support注册,我们会直接将文件通过电子邮件发送给您。

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

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

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

  3. 点击代码下载与勘误

  4. 搜索框中输入书名。

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

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

  7. 点击代码下载

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

  • Windows 下的 WinRAR / 7-Zip

  • Mac 下的 Zipeg / iZip / UnRarX

  • Linux 下的 7-Zip / PeaZip

勘误

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

要查看之前提交的勘误信息,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书名。所需信息将显示在勘误部分下。

盗版

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

如果您发现疑似盗版材料,请通过 <copyright@packtpub.com> 联系我们,并提供链接。

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

问题

如果您对这本书的任何方面有疑问,可以联系 <questions@packtpub.com>,我们将尽力解决问题。

第一章:活动

本章节包括以下食谱:

  • 声明一个活动

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

  • 在活动之间切换

  • 向另一个活动传递数据

  • 从活动中返回结果

  • 保存活动状态

  • 存储持久活动数据

  • 理解活动生命周期

引言

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

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

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

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

注意

要开始开发 Android 应用程序,请访问Android Studio页面下载新的 Android Studio IDE 和Android SDK捆绑包:

developer.android.com/sdk/index.html

声明一个活动

活动和其他应用组件,如服务,是在AndroidManifest XML 文件中声明的。声明活动是我们告诉系统关于我们的活动以及如何请求它的方式。例如,一个应用通常会指出至少有一个活动应该作为桌面图标可见,并作为进入应用的主要入口。

准备就绪

Android Studio 是新的用于开发 Android 应用程序的工具,取代了现在已弃用的Eclipse ADT解决方案。本书将使用 Android Studio 展示所有食谱,因此如果你还没有安装它,请访问 Android Studio 网站(链接已提供)以安装 IDE 和 SDK 捆绑包。

如何操作...

在这个第一个示例中,我们将指导你创建一个新项目。Android Studio 提供了一个快速入门向导,使得这个过程非常简单。按照以下步骤开始:

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

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

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

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

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

  6. 由于我们之前选择了空白活动选项,所以会显示自定义活动对话框。你可以保留提供的默认设置,但注意默认的活动名称是MainActivity。点击完成

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

如果你查看MainActivity.java文件,你会发现它非常基础。这是因为我们选择了空白活动选项(在第 4 步)。现在看看AndroidManifest.xml文件。这里是我们实际声明活动的地方。在<application>元素内是<activity>元素:

<activity
    android:name=".MainActivity"
    android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity>

注意

在 Android Studio 中查看此xml时,你可能会注意到标签元素显示了strings.xml资源文件中定义的实际文本。这是新 IDE 中增强功能的一个小例子。

工作原理...

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

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

注意

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

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

使用意图对象启动新活动

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

准备就绪

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

如何操作...

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

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

  1. 打开MainActivity.java类,并添加以下函数:

    public void launchIntent(View view) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse("https://www.packtpub.com/"));
        startActivity(intent);
    }
    

    当你输入这段代码时,Android Studio 会对 View 和意图给出这个警告:无法解析符号'Intent'

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

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

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

  2. 打开activity_main.xml文件,并添加以下 XML 代码:

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Launch Browser"
        android:id="@+id/button"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:onClick="launchIntent"/>
    

    如何操作...

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

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

它是如何工作的...

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

注意

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

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

它是如何工作的...

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

还有更多内容...

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

另请参阅

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

在活动之间切换

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

准备工作

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

如何操作...

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

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

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

    public void onClickSwitchActivity(View view) {
        Intent intent = new Intent(this, SecondActivity.class);
        startActivity(intent);
    }
    
  4. 现在,打开位于\res\layout文件夹中的activity_main.xml文件,并添加以下 XML 代码来创建按钮:

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:text="Launch SecondActivity"
        android:onClick="onClickSwitchActivity"/>
    
  5. 实际上,您现在可以运行代码,并看到第二个活动出现。我们将会更进一步,在SecondActivity中添加一个按钮来关闭它,这将带我们回到第一个活动。打开SecondActivity.java文件,并添加此函数:

    public void onClickClose(View view) {
        finish();
    }
    
  6. 最后,在SecondActivity布局中添加关闭按钮。打开activity_second.xml文件,并在自动生成的<TextView>元素之后添加以下<Button>元素:

    <Button
        android:id="@+id/buttonClose"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Close"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:onClick="onClickClose"/>
    
  7. 在您的设备或模拟器上运行应用程序,并查看按钮的实际效果。

工作原理...

本练习的真正工作在于第 3 步中的 onClickSwitchActivity() 方法。在这里,我们使用 SecondActivity.class 为意图声明第二个活动。我们通过在第二个活动中添加关闭按钮,展示了常见的实际场景——启动一个新活动,然后关闭它,并返回到原始调用活动。这种行为是在 onClickClose() 函数中实现的。它所做的只是调用 finish(),但这告诉系统我们已经完成了活动。实际上,finish() 并没有返回到调用活动或任何特定的活动;它只是关闭当前活动并依赖于回退栈。如果我们想要一个特定的活动,可以再次使用意图对象(在创建意图时只需更改类名)。

这种活动切换并不能构成一个令人兴奋的应用程序。我们的活动除了演示如何从一个活动切换到另一个活动之外,什么也不做,这当然将成为我们开发几乎所有应用程序的基本方面。

如果我们手动创建活动,我们需要将它们添加到清单中。通过使用这些步骤,Android Studio 已经处理了 XML。要查看 Android Studio 的操作,请打开 AndroidManifest.xml 文件并查看 <application> 元素:

<activity
    android:name=".MainActivity"
    android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER/>
    </intent-filter>
</activity>
<activity
    android:name=".SecondActivity"
    android:label="@string/title_activity_second">
</activity>

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

另请参阅

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

将数据传递给另一个活动

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

准备工作

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

如何操作...

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

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

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

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

  2. 现在,打开 MainActivity.java 文件,并按如下所示更改 onClickSwitchActivity() 方法:

    public void onClickSwitchActivity(View view) {
        EditText editText = (EditText)findViewById(R.id.editTextData);
        String text = editText.getText().toString();
        Intent intent = new Intent(this, SecondActivity.class);
        intent.putExtra(Intent.EXTRA_TEXT,text);
        startActivity(intent);
    }
    
  3. 接下来,打开 activity_second.xml 文件,并修改 <TextView> 元素以包含 ID 属性:

    <TextView
        android:id="@+id/textViewText"
        android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    
  4. 最后一个改变是编辑第二个活动以查找这个新的数据并在屏幕上显示它。打开SecondActivity.java文件,并按以下方式编辑onCreate()

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        TextView textView = (TextView)findViewById(R.id.textViewText);
        if (getIntent()!=null && getIntent().hasExtra(Intent.EXTRA_TEXT)) {
            textView.setText(getIntent().getStringExtra(Intent.EXTRA_TEXT));
        }
    }
    
  5. 现在运行项目。在主活动中输入一些文本,然后按下启动第二个活动以查看它发送的数据。

它是如何工作的...

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

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

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

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

还有更多...

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

从活动中返回结果

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

准备工作

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

如何操作...

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

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

    public static final String REQUEST_RESULT="REQUEST_RESULT";
    
  2. 接下来,通过修改onClickSwitchActivity()方法以期待一个结果来改变调用意图的方式:

    public void onClickSwitchActivity(View view) {
        EditText editText = (EditText)findViewById(R.id.editTextData);
        String text = editText.getText().toString();
        Intent intent = new Intent(this, SecondActivity.class);
        intent.putExtra(Intent.EXTRA_TEXT,text);
        startActivityForResult(intent,1);
    }
    
  3. 然后,添加这个新方法以接收结果:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode==RESULT_OK) {
            Toast.makeText(this, Integer.toString(data.getIntExtra(REQUEST_RESULT, 0)), Toast.LENGTH_LONG).show();
        }
    }
    
  4. 最后,在SecondActivity.java中修改onClickClose以设置返回值,如下所示:

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

它是如何工作的...

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

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

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

还有更多...

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

startActivityForResult(Intent intent, int requestCode);

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

提示

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

另请参阅

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

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

保存活动状态

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

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

注意

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

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

准备工作

在 Android Studio 中创建一个新项目,并将其命名为 StateSaver。我们只需要一个活动,所以自动生成的 MainActivity 就足够了。但是,我们需要一些小部件,包括 EditTextButtonTextView。它们的布局(在 activity_main.xml 中)将如下所示:

<EditText
    android:id="@+id/editText"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_alignParentStart="true"/>

<Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:text="Count"
    android:onClick="onClickCounter"/>

<TextView
    android:id="@+id/textViewCounter"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@id/button"/>

如何操作...

执行以下步骤:

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

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

    public void onClickCounter(View view) {
        mCounter++;
        ((TextView)findViewById(R.id.textViewCounter)).setText("Counter: " + Integer.toString(mCounter));
    }
    
  3. 为了接收应用程序状态变化的通知,我们需要在应用程序中添加 onSaveInstanceState()onRestoreInstanceState() 方法。打开 MainActivity.java 文件,并添加以下内容:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(KEY_COUNTER,mCounter);
    }
    
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        mCounter=savedInstanceState.getInt(KEY_COUNTER);
    }
    
  4. 运行程序,尝试改变方向,看看它的表现(如果你使用模拟器,Ctrl + F11 将旋转设备)。

它是如何工作的...

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

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

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

提示

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

还有更多...

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

onCreate(Bundle savedInstanceState)

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

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

另请参阅

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

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

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

存储持久活动数据

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

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

准备工作

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

如何操作...

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

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

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

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

工作原理...

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

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

还有更多内容...

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

使用多个偏好文件

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

getSharedPreferences(String name, int mode)

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

另请参阅

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

理解活动生命周期

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

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

理解活动生命周期

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

准备工作

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

如何操作...

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

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

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

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

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

    @Override
    protected void onStart() {
        super.onStart();
        mTextViewState.append("onStart()\n");
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        mTextViewState.append("onResume()\n");
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        mTextViewState.append("onPause()\n");
    }
    
    @Override
    protected void onStop() {
        super.onStop();
        mTextViewState.append("onStop()\n");
    }
    
    @Override
    protected void onRestart() {
        super.onRestart();
        mTextViewState.append("onRestart()\n");
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mTextViewState.append("onDestroy()\n");
    }
    
  5. 运行应用程序,观察当活动被按下返回和主页键中断时会发生什么。尝试其他操作,比如任务切换,看看它们对应用程序的影响。

工作原理...

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

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

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

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

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

    @Override
      public void onPause() {
      super.onPause();
      mTextView.append("onPause()\n ");
      if (isFinishing()){
        mTextView.append(" ... finishing");
      }
    }
    

提示

在实现这些方法时,请在进行任何操作之前调用超类。

还有更多...

关闭一个活动

要关闭一个活动,直接调用它的 finish() 方法,这又会进而调用 onDestroy()。要从子活动执行相同操作,请使用 finishFromChild(Activity child),其中 child 是调用子活动。

了解活动是正在关闭还是仅仅暂停,通常很有用,isFinishing(boolean) 方法返回的值可以指示活动处于这两种状态中的哪一种。

第二章:布局

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

  • 定义和填充布局

  • 使用 RelativeLayout

  • 使用 LinearLayout

  • 创建表格 – TableLayout 和 GridLayout

  • 使用 ListView、GridView 和适配器

  • 在运行时更改布局属性

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

简介

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

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

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

定义和填充布局

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

定义和填充布局

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

准备工作

在 Android Studio 中创建一个新项目,并将其命名为InflateLayout。创建项目后,展开res/layout文件夹,以便我们可以编辑activity_main.xml文件。

如何操作...

  1. 编辑res/layout/activity_main.xml文件,使其包含如下定义的按钮:

    <Button
        android:id="@+id/buttonLeft"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Left Button"
        android:layout_centerVertical="true"
        android:layout_alignParentLeft="true"
        android:onClick="onClickLeft"/>
    
  2. 现在,复制activity_main.xml并将其命名为activity_main2.xml。更改按钮,使其与以下内容相匹配:

    <Button
        android:id="@+id/buttonRight"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Right Button"
        android:layout_centerVertical="true"
        android:layout_alignParentRight="true"
        android:onClick="onClickRight"/>
    
  3. 打开MainActivity.java,并添加以下两个方法来处理按钮点击:

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

工作原理...

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

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

还有更多...

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

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

参阅以下内容

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

使用 RelativeLayout

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

准备工作

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

如何操作...

  1. 打开res/layout/activity_main.xml文件,并按如下方式进行修改:

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Centered"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Below TextView1"
        android:layout_below="@+id/textView1"
        android:layout_toLeftOf="@id/textView1" />
    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bottom Right"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true" />
    
  2. 运行代码,或者在设计标签中查看布局

工作原理...

这是一个非常简单的练习,但它展示了RelativeLayout的几种选项:layout_centerVerticallayout_centerHorizontallayout_belowlayout_alignParentBottom等。

最常用的RelativeLayout布局属性包括:

  • layout_below:此视图应位于指定视图之下

  • layout_above:此视图应位于指定视图之上

  • layout_alignParentTop:将此视图与父视图的顶部边缘对齐

  • layout_alignParentBottom:将此视图与父视图的底边缘对齐

  • layout_alignParentLeft:将此视图与父视图的左边缘对齐

  • layout_alignParentRight:将此视图与父视图的右边缘对齐

  • layout_centerVertical:在父视图中垂直居中此视图

  • layout_centerHorizontal:在父视图中水平居中此视图

  • layout_center:在父视图中水平和垂直居中此视图

    注意

    要获取完整的RelativeLayout参数列表,请访问:developer.android.com/reference/android/widget/RelativeLayout.LayoutParams.html

还有更多...

与我们之前看到的相比,下面是仅使用LinearLayout来居中TextView的示例(创建与RelativeLayoutlayout_center参数相同的效果):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center" >
        <TextView
            android:id="@+id/imageButton_speak"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Centered" />
    </LinearLayout>
</LinearLayout>

请注意,此布局比等效的RelativeLayout(即嵌套在父LinearLayout中的LinearLayout)深一个层级。尽管这是一个简单的例子,但避免不必要的嵌套是一个好主意,因为它可能会影响性能,尤其是在重复膨胀布局时(如ListItem)。

另请参阅

  • 下一个食谱使用 LinearLayout将为您提供另一种布局选择

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

使用 LinearLayout

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

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

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

准备工作

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

如何操作...

  1. 打开res/layout/activity_main.xml文件,并按如下方式替换:

    <LinearLayout 
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <EditText
            android:id="@+id/editTextTo"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="To" />
        <EditText
            android:id="@+id/editTextSubject"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Subject" />
        <EditText
            android:id="@+id/editTextMessage"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:gravity="top"
            android:hint="Message" />
    </LinearLayout>
    
  2. 运行代码,或者在设计标签中查看布局。

工作原理...

当使用LinearLayout的垂直方向时,子视图会在单列中创建(一个叠在另一个上面)。前两个视图使用android:layout_height="wrap_content"属性,使它们各占一行。editTextMessage使用以下属性来指定高度:

android:layout_height="0dp"
android:layout_weight="1"

使用LinearLayout时,它会告诉 Android 根据权重计算高度。权重为 0(如果未指定,则为默认值)表示视图不应该扩展。在这个例子中,只有editTextMessage视图被定义了权重,因此它将独自扩展以填充父布局中的任何剩余空间。

提示

当使用水平方向时,指定android:layout_height="0dp"(连同权重),让 Android 计算宽度。

将权重属性视为百分比可能有助于理解。在这种情况下,定义的总权重是 1,所以这个视图获得了剩余空间的 100%。如果我们给另一个视图分配了 1 的权重,那么总权重将是 2,所以这个视图将获得 50%的空间。尝试给其他视图之一添加权重(确保也将高度更改为0dp)以查看效果。

如果你给其他视图之一(或两者)添加了权重,你注意到文本位置了吗?没有为gravity指定值时,文本只会保持在视图空间的中心。editTextMessage指定了:android:gravity="top",这将强制文本位于视图的顶部。

还有更多内容...

可以使用按位OR组合多个属性选项。(Java 使用管道字符(|)表示OR)。例如,我们可以结合两个重力选项,使其既沿着父视图的顶部对齐,又在可用空间内居中:

android:layout_gravity="top|center"

需要注意的是,layout_gravitygravity标签不是一回事。layout_gravity决定了视图在其父视图中的位置,而gravity控制视图内内容的位置——例如,按钮上文本的对齐方式。

另请参阅

  • 之前的食谱,使用 RelativeLayout

创建表格——TableLayout 和 GridLayout

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

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

准备好了

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

如何操作...

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

  2. 添加三个TableRows,每个TableRow包含三组TextViews,以创建一个 3x3 矩阵。为了演示目的,列被标记为 A-C,行被标记为 1-3,所以第一个TextViews行将是 A1,B1 和 C1。最终结果将如下所示:

    <TableLayout
    
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="A1"
                android:id="@+id/textView1" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="B1"
                android:id="@+id/textView2" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="C1"
                android:id="@+id/textView3" />
        </TableRow>
        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="A2"
                android:id="@+id/textView4" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="B2"
                android:id="@+id/textView5" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="C2"
                android:id="@+id/textView6" />
        </TableRow>
        <TableRow
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="A3"
                android:id="@+id/textView7" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="B3"
                android:id="@+id/textView8" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="C3"
                android:id="@+id/textView9" />
        </TableRow>
    </TableLayout>
    
  3. 现在,打开GridLayout项目以编辑activity_main.xml。将根布局更改为GridLayout。在GridLayout元素中添加columnCount=3rowCount=3属性。

  4. 现在,向GridLayout中添加九个TextViews。我们将使用与前面的TableLayout相同的文本以便进行一致的比较。由于GridView不使用TableRows,前三个TextViews位于第 1 行,接下来的三个位于第 2 行,依此类推。最终结果将如下所示:

    <GridLayout
    
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:columnCount="3"
        android:rowCount="3">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="A1"
            android:id="@+id/textView1" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="B1"
            android:id="@+id/textView2" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="C1"
            android:id="@+id/textView3" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="A2"
            android:id="@+id/textView4" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="B2"
            android:id="@+id/textView5" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="C2"
            android:id="@+id/textView6" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="A3"
            android:id="@+id/textView7" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="B3"
            android:id="@+id/textView8" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="C3"
            android:id="@+id/textView9" />
    </GridLayout>
    
  5. 您可以运行应用程序,或者使用设计标签来查看结果。

工作原理...

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

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

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

还有更多内容...

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

android:stretchColumns="1"

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

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

android:layout_columnWeight="1"

注意

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

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

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

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

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

提示

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

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

使用 ListViewGridView 和适配器

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

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

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

准备工作

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

如何操作...

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

    public class MainActivity extends ListActivity {
    
  2. 修改 onCreate() 使其与以下代码相匹配:

    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"};
    
      ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, countries);
      setListAdapter(countryAdapter);
    
        getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, Viewview, int position, long id) {
                String s = ((TextView) view).getText() + " " +position;
                Toast.makeText(getApplicationContext(), s, 
                    Toast.LENGTH_SHORT).show();
            }
        });
    }
    
  3. 现在,在模拟器或设备上运行应用程序,查看填充后的 ListView

它是如何工作的...

我们首先创建一个简单的国家名称数组,然后使用它来填充 ListAdapter。在这个例子中,我们在构造 ListAdapter 时使用了 ArrayAdapter,但 Android 还有其他几种适配器类型可用。例如,如果你的数据存储在数据库中,你可以使用 CursorAdapter。如果内置类型之一不符合你的需求,你总是可以使用 CustomAdapter

我们用以下这行代码创建适配器:

ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, countries);

在这里,我们使用我们的字符串数组(最后一个参数)实例化 ArrayAdapter。注意 android.R.layout.simple_list_item_1 参数吗?这定义了按钮的布局。这里,我们使用了 Android 提供的布局之一,但我们也可以创建自己的布局并传递我们的 ID。

一旦我们准备好适配器,只需通过 setListAdapter() 调用将其传递给底层的 ListView。(底层的 ListView 来自扩展 ListViewActivity。)最后,我们实现 setOnItemClickListener 以在用户按下列表中的按钮(代表一个国家)时显示一个 Toast。

ListViews 在 Android 中非常常见,因为它们通过滚动视图高效地利用屏幕空间,这对于小屏幕非常有用。ScrollView 布局提供了一种替代方法来创建类似的滚动效果。这两种方法的主要区别在于,ScrollView 布局在显示给用户之前会完全展开,而 ListView 只展开用户将看到的部分视图。对于有限的数据,这可能不是问题,但对于较大的数据集,在列表显示之前应用程序可能会耗尽内存。

由于 ListView 是由数据适配器驱动的,因此数据可以轻松更改。即使在我们的有限示例中,向屏幕添加一个新国家也只需将该名称添加到国家列表中。更重要的是,在用户使用应用程序时,列表可以在运行时更新(例如,从网站下载更新后的列表以显示实时选项)。

还有更多...

ListView 还支持通过 setChoiceMode() 方法设置多选模式。要查看其效果,请在 setListAdapter() 之后添加以下代码行:

getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);

然后,将 ListItem 布局从 android.R.layout.simple_list_item_1 更改为 android.R.layout.simple_list_item_checked

尽管大多数需要滚动列表的应用程序都使用 ListView,但 Android 也提供了 GridView。它们在功能上非常相似,甚至使用相同的数据适配器。主要区别在于视觉效果,它允许多列显示。为了更好地理解,让我们将 ListView 示例更改为 GridView

首先,我们需要将 MainActivity 修改为再次继承自 Activity,而不是 ListActivity。(这将撤销之前的第 1 步。)然后,用以下代码替换 onCreate()

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GridView gridView = new GridView(this);
    setContentView(gridView);
    String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"};
    ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, countries);
    gridView.setAdapter(countryAdapter);
    gridView.setNumColumns(2);
    gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                String s = ((TextView) view).getText() + " " + position;
            Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show();
        }
    });
}

如您所见,GridView 的设置代码比 ListView 的要多。onCreate() 方法创建了一个新的 GridView 并在 setContentView() 调用中传递它。(我们使用了在定义和填充布局中提到的 setContentView 的这种变体,而不是创建仅包含 GridView 的布局,但最终结果相同。)

ListViewActivity 基类处理了其中大部分工作,但 GridView 没有相应的活动类来继承。

在运行时更改布局属性

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

在上一个示例中,我们看到了一个从代码中修改布局的小例子,我们设置了 GridView 列的数量以在代码中显示。在这个示例中,我们将获取对 LayoutParams 对象的引用,以在运行时改变边距。

准备工作

在这里,我们将使用 XML 设置一个简单的布局,并使用 LinearLayout.LayoutParams 对象在运行时改变视图的边距。

如何操作...

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

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

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

    android:id="@+id/button"
    
  4. 打开 MainActivity.java 并在 onCreate() 方法中添加以下代码以设置 onClick 事件监听器:

    Button button = (Button)findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            ((TextView)findViewById(R.id.textView)).setText("Changed at runtime!");
            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)view.getLayoutParams();
            params.leftMargin += 5;
        }
    });
    
  5. 在设备或模拟器上运行程序。

工作原理...

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

使用层次查看器优化布局

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

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

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

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

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

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

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

准备工作

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

注意

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

如何操作...

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

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

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

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

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

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

工作原理...

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

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

还有更多...

Lint 是 Android SDK 中包含的另一个工具,Android Studio 提供了内置支持。默认情况下,您已经在使用 Lint 检查代码中的问题,例如不推荐使用的 API 调用、目标 API 级别不支持 API 调用、安全问题等。对于我们的优化布局关注点,Lint 将自动检查的一些条件包括以下内容:

  • 深层布局——默认最大层级为 10 级

  • 嵌套权重,这对性能不利

  • 无用的父节点

  • 无用的叶节点

如果在 Android Studio 中检查此布局的 Lint 警告,你会在第二个LinearLayout元素上看到以下警告:

还有更多...

ViewStub还可以用来优化布局。将ViewStub视为布局的“懒加载”。ViewStub中的布局在需要之前不会展开,这减少了需要展开的视图数量。布局将更快渲染并使用更少的内存。这是在需要时提供不常用功能(如打印功能)的一种好方法,但在不需要时不占用内存。以下是一个ViewStub的示例:

<ViewStub
    android:id="@+id/viewStubPrint"
    android:inflatedId="@id/print"
    android:layout="@layout/print"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

实际上有两种方法可以展开ViewStub

  • ViewStub的可见性参数设置为VISIBLE

    ((ViewStub) findViewById(R.id.viewStubPrint)).setVisibility(View.VISIBLE);
    
  • ViewStub上调用inflate()方法:

    View view = ((ViewStub) findViewById(R.id.viewStubPrint)).inflate();
    

一旦ViewStub被展开,ViewStub的 ID 将从布局中移除,并替换为展开后的 ID。

第三章:视图、控件和样式

在本章中,我们将介绍以下主题:

  • 在布局中插入小部件

  • 使用图形显示按钮状态

  • 在运行时创建控件

  • 创建自定义组件

  • 将样式应用于视图

  • 将样式转变为主题

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

介绍

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

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

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

介绍

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

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

在布局中插入控件

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

准备工作

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

如何操作...

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

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

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

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

  4. 现在,打开 MainActivity.java 文件以编辑代码。在 onCreate() 方法中添加以下代码以设置 onClickListener()

    Button button = (Button)findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(MainActivity.this,"Clicked",Toast.LENGTH_SHORT).show();
        }
    });
    
  5. 在设备或模拟器上运行应用程序。

工作原理...

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

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

android:id="@+id/button"

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

还有更多...

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

另请参阅

使用图形显示按钮状态

我们讨论了 Android 视图的灵活性以及行为和视觉外观如何定制。在本教程中,我们将创建一个可绘制的 状态选择器,这是一个在 XML 中定义的资源,它根据视图的状态指定要使用的可绘制资源。最常用的状态以及可能的值包括:

  • state_pressed=["true" | "false"]

  • state_focused=["true" | "false"]

  • state_selected=["true" | "false"]

  • state_checked=["true" | "false"]

  • state_enabled=["true" | "false"]

要定义状态选择器,请创建一个带有 <selector> 元素的 XML 文件,如下所示:

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

<selector> 元素内,我们定义一个 <item> 以根据指定的状态确定要使用的可绘制资源。以下是一个使用多个状态的 <item> 元素示例:

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

提示

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

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

准备工作

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

如何操作...

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

  1. res/drawable 文件夹中创建一个名为 state_selector.xml 的新 XML 文件。该文件应包含以下 XML 代码:

    <?xml version="1.0" encoding="utf-8"?>
    <selector >
        <item
            android:drawable="@android:color/darker_gray"
            android:state_checked="true"/>
        <item
            android:drawable="@android:color/white"
            android:state_checked="false"/>
    </selector>
    
  2. 现在,打开 activity_main.xml 文件,并按以下方式添加一个 ToggleButton

    <ToggleButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New ToggleButton"
        android:id="@+id/toggleButton"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:background="@drawable/state_selector" />
    
  3. 在设备或模拟器上运行应用程序。

工作原理...

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

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

创建好可绘制资源(第一步的 XML)后,我们只需告诉视图使用它。由于我们希望根据状态改变背景颜色,因此我们使用 android:background 属性。

state_selector.xml 是一个可传递给任何接受可绘制资源的属性的可绘制资源。例如,我们可以使用以下 XML 替换复选框中的按钮:

android:button="@drawable/state_selector"

还有更多...

如果我们想要实际的图片作为图形,而不仅仅是颜色变化呢?这就像更改项状态中引用的可绘制资源一样简单。

可以下载的源代码使用了两个图形图像,从pixabay.com/下载(选择这个网站是因为图像可以免费使用,且不需要登录。)

一旦你有了想要的图像,将它们放在res/drawable文件夹中。然后,在 XML 中更改状态项行以引用你的图像。以下是一个示例:

<item
    android:drawable="@drawable/checked_on"
    android:state_checked="true"/>

(将check_on更改为与您的图像资源名称匹配。)

使用指定文件夹进行屏幕特定资源

当 Android 遇到@drawable引用时,它会期望在res/drawable文件夹之一中找到目标。这些是为不同的屏幕密度设计的:ldpi(低每英寸点数)、mdpi(中等)、hdpi(高)和xhdpi(超高),它们允许我们为特定目标设备创建资源。当应用程序在特定设备上运行时,Android 将从与实际屏幕密度最接近的指定文件夹加载资源。

如果它发现这个文件夹是空的,它会尝试下一个最接近的匹配,以此类推,直到找到命名的资源。出于教程目的,不需要为每种可能的密度创建一组单独的文件,因此将我们的图像放在drawable文件夹中是在任何设备上运行练习的简单方法。

提示

要获取可用的资源标识符的完整列表,请访问developer.android.com/guide/topics/resources/providing-resources.html

另请参阅

有关在 Android 上选择资源的另一个示例,请参阅关于根据操作系统版本选择主题的食谱。

在运行时创建小部件

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

前一章中的 GridView 示例是在代码中创建的。但与 GridView 食谱不同,在这个食谱中,我们将向在activity_main.xml中定义的现有布局中添加一个视图。

准备工作

在 Android Studio 中创建一个新项目,并将其命名为RuntimeWidget。在选择活动类型时,选择空活动选项。

如何操作...

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

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

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

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

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

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

工作原理...

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

还有更多...

如果我们想完全从代码创建整个布局呢?尽管这可能不是最佳实践,但在某些时候,从代码创建布局肯定更容易(也更简单)。让我们看看如果我们不使用activity_main.xml中的布局,这个例子会是什么样子。以下是onCreate()的样子:

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

在这个例子中,其实并没有太大区别。如果你在代码中创建了一个视图,并且稍后想要引用它,你需要保留对对象的引用,或者给视图分配一个 ID 以使用findViewByID()。要给视图分配 ID,请使用setID()方法,传入View.generateViewId()(以生成唯一 ID)或在 xml 中使用****定义 ID。

创建自定义组件

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

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

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

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

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

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

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

  6. 实现自定义功能。

提示

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

准备就绪

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

如何操作...

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

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

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

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

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

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

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

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

工作原理...

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

super(context);

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

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

还有更多...

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

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

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

另请参阅

将样式应用于视图

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

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

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

准备工作

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

如何操作...

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

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

  2. 我们将通过在现有的AppTheme样式下方添加以下 XML,创建一个名为MyStyle的新样式:

    <style name="MyStyle">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:background">#000000</item>
        <item name="android:textColor">#AF0000</item>
        <item name="android:textSize">20sp</item>
        <item name="android:padding">8dp</item>
        <item name="android:gravity">center</item>
    </style>
    
  3. 现在告诉视图使用这个样式。打开activity_main.xml文件,并在现有的<TextView>元素中添加以下属性:

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

它是如何工作的...

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

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

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

还有更多...

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

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

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

提示

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

<style name="MyParent.MyStyle" >

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

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

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

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

另请参阅

Android 设计支持库位于:

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

将样式转变为主题

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

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

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

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

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

准备就绪

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

如何操作...

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

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

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

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

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

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

工作原理...

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

还有更多...

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

根据安卓版本选择主题

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

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

  • 主题 – Gingerbread 及更早版本

  • Theme.Holo – Honeycomb (API 11)

  • Theme.Material – Lollipop (API 21)

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

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

准备工作

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

如何操作...

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

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

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

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

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

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

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

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

  6. 现在,在每个新目录中创建一个styles.xml文件。(在values-v11目录上右键,选择新建 | 文件选项。)对于values-v11,使用以下样式来定义 Holo 主题:

    <resources>
        <style name="AutomaticTheme" parent="android:Theme.Holo.Light">
        </style>
    </resources>
    For the values-v21, use the following code to define the Material theme:
    <resources>
        <style name="AutomaticTheme" parent="android:Theme.Material.Light">
        </style>
    </resources>
    
  7. 最后一步是告诉应用使用我们的新主题。为此,请打开AndroidManifest.xml,并将android:theme属性更改为AutomaticTheme。它应如下所示:

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

工作原理...

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

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

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

还有更多内容…

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