精通安卓应用开发(一)

原文:zh.annas-archive.org/md5/23E2C896EDA56175BA900FB6F2E21CF8

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书是学习高级 Android 应用开发的实用指南。本书帮助掌握 Android 的核心概念,并在实际项目中快速应用知识。在整本书中,创建了一个应用,并在每一章中进化,以便读者可以轻松地跟随并吸收概念。

本书共分为十二章。前三章主要关注应用的设计,解释了设计的基本概念以及在 Android 中使用的编程模式。接下来的章节旨在改进应用,访问服务器端以下载应用中要显示的信息。应用功能完成后,将使用 Material Design 组件和其他第三方库进行改进。

在结束之前,为应用添加了额外服务,如位置服务、分析、崩溃报告和盈利化。最后,导出应用,解释不同的构建类型和证书,并将其上传到 Play 商店,准备进行分发。

本书内容涵盖

第一章,入门,解释了 Android 6 Marshmallow 的基础知识以及 Material Design 的重要概念。我们将设置开始开发所需的工具,并可选地安装一个比 Android 默认模拟器更快的超快模拟器,这将帮助我们在阅读本书的过程中测试我们的应用。

第二章,设计我们的应用,介绍了创建应用的第一步——设计导航——以及不同的导航模式。我们将应用标签页模式与滑动屏幕,解释并使用 Fragments,这是 Android 应用开发的一个关键组件。

第三章,从云端创建和访问内容,涵盖了在我们应用中显示来自互联网信息所需的一切。这些信息可以在外部服务器或 API 上。我们将使用 Parse 创建自己的服务器,并使用 Volley 和 OKHttp 进行高级网络请求来访问它,处理信息并使用 Gson 将其转换为可用的对象。

第四章,并发与软件设计模式,讨论了 Android 中的并发问题及处理它的不同机制,如 AsyncTask、服务、Loader 等。本章的第二部分讲述了在 Android 中最常见的编程模式。

第五章,列表和网格,讨论了列表和网格,从 ListViews 开始。它解释了这一组件如何在 RecyclerView 中演变,并以示例展示了如何创建带有不同类型元素的列表。

第六章,CardView 和材料设计,主要从用户界面角度出发,提升应用的设计感,并引入材料设计,讲解并实现如 CardView、Toolbar 和 CoordinatorLayout 等功能。

第七章,图像处理和内存管理,主要讨论如何在我们的应用中显示从互联网上下载的图片,使用不同的机制,例如 Volley 或 Picasso。它还涵盖了不同类型的图像,如矢量可绘制图像和 Nine patch。最后,它讨论了内存管理以及如何预防、检测和定位内存泄漏。

第八章,数据库和加载器,本质上是解释 Android 中数据库的工作原理,内容提供者是什么,以及如何使用 CursorLoaders 让数据库直接与视图通信。

第九章,推送通知和分析,讨论如何使用 Google Cloud Messaging 和 Parse 实现推送通知。章节的第二部分讨论了分析,这对于理解用户如何与我们的应用互动、捕获错误报告以及保持应用无 bug 至关重要。

第十章,位置服务,通过在应用中实现一个示例来介绍 MapView,从开发者控制台的初始设置到应用中最终展示位置标记的地图视图。

第十一章,安卓上的调试和测试,主要讨论测试。它涵盖了单元测试、集成测试和用户界面测试。还讨论了使用市场上不同的工具和最佳实践,通过自动化测试开发可维护的应用。

第十二章,营利化、构建过程和发布,展示了如何使应用盈利,并解释了广告盈利化的关键概念。它展示了如何导出具有不同构建类型的应用,并最终如何在 Google Play 商店上传和推广此应用。

阅读本书所需条件

您的系统需要安装以下软件以执行本书中提到的代码:

  • Android Studio 1.0 或更高版本

  • Java 1.7 或更高版本

  • Android 4.0 或更高版本

本书的目标读者

如果您是一位有 Gradle 和项目开发经验的 Java 开发者,并希望成为专家,那么这本书适合您。对 Gradle 的基本了解是必需的。

约定

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

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名和虚拟 URL 会以如下形式显示:“我们可以通过使用include指令包含其他上下文。”

代码块如下设置:

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 

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

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> 

新术语重要词汇会用粗体显示。你在屏幕上看到的词,例如菜单或对话框中的,会在文本中以这样的形式出现:“点击下一步按钮,你会进入下一个屏幕。”

注意

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

提示

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

读者反馈

我们始终欢迎读者的反馈。请告诉我们你对这本书的看法——你喜欢或不喜欢的地方。读者的反馈对我们很重要,因为它能帮助我们开发出你真正能从中获益的图书。

要发送给我们一般反馈,只需通过电子邮件<feedback@packtpub.com>联系我们,并在邮件的主题中提及书籍的标题。

如果你有一个擅长的主题,并且有兴趣撰写或参与编写一本书,请查看我们的作者指南:www.packtpub.com/authors

客户支持

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

下载示例代码

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

下载本书的彩色图像

我们还为你提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。彩色图像将帮助你更好地理解输出的变化。你可以从www.packtpub.com/sites/default/files/downloads/4221OS_ColorImages.pdf下载这个文件。

错误更正

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

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

盗版

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

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

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

问题咨询

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

第一章:开始

我们将以 Material Design 和 Android 6 Marshmallow 的概述开始本书。谷歌的新 Material Design 概念在应用的外观和感觉上带来了一场革命。

在本书的进行过程中,我们将构建一个名为 MasteringAndroidApp 的应用。在本章中,我们将解释这个应用的内容。在这个应用中,我们将实践每一章中讲解的所有概念和理论。在本书结束时,我们应该能拥有一个功能丰富的应用,可以轻松修改以创建你自己的版本,并上传到 Google Play 商店。

我们将确保拥有所有必要的工具,下载最新版本的 Android,并介绍Genymotion,这是本书强烈推荐的最快的 Android 模拟器。

  • Material Design

  • Android 6 Marshmallow 的关键点

  • 应用概述

  • 准备工具

    • Android Studio

    • SDK 管理器

  • Genymotion

介绍 Material Design

如前所述,Material Design 在应用的外观和体验上带来了一场革命。你可能之前已经听说过这个概念,但确切来说它是什么呢?

Material Design 是由谷歌创建的一种新的视觉语言,它适用于所有基于材质、有意义的过渡、动画和响应式交互的平台、网站和移动设备。

材质是可以在表面看到的一个元素的隐喻,它由可以具有不同高度和宽度的层组成,但它们的厚度始终是一个单位,就像纸张一样。我们可以将材质叠放在彼此之上,这为视图引入了深度元素,一个 Z 坐标。同样,我们可以在另一张纸上放置一张纸,投下阴影,定义视觉优先级。

内容呈现在材质上,但不会增加其厚度。内容可以以任何形状和颜色显示;可以是纯背景色、文本、视频,以及其他许多事物。内容被限制在材质的边界内。

材质可以扩展,内容也可以随之扩展,但内容永远不能超过材质的扩展。我们不能让两个材质处于同一 Z 位置。其中一个总是要在另一个下面或上面。如果我们与材质互动,我们总是在最顶层互动。例如,触摸事件将在顶层执行,不会穿透到底层。你可以改变材质的大小和形状,两个材质可以合并为一个,但它们不能弯曲或折叠。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是一个使用 Material Design 风格的应用示例;我们可以看到带有阴影的卡片、不同的内容以及带有导航抽屉的应用栏。所有这些组件都将在本书中进行讲解,我们将致力于构建一个使用相同风格的应用。

材料设计同时也带来了重要的 UI 元素,比如RecyclerView。这是一个视图,将替代早期 Android 中的ListView,用来创建任意类型的可滚动元素列表。我们将在第五章,列表和网格中处理这些组件,从ListView的基本版本开始,演进了解RecyclerView是如何诞生的,并以一个示例结束。

CardView是另一个引入的主要 UI 元素。我们可以在前面的图片中看到一个;这是一个带有背景和阴影的组件,可以自定义以适应我们想要的所有内容。我们将在第六章,CardView 和材料设计中处理它,同时也会介绍下一个组件——设计支持库。

设计支持库是一个包含动画、FAB浮动动作按钮)和导航抽屉的必备库。你可能已经在其他应用中见过从左侧滑出的滑动菜单。设计支持库为旧版 Android 提供了这些组件的支持,使我们能够在旧版本中使用材料设计特性。

所有以上内容都是从 UI 和编程的角度来看的特性,但材料设计同时也为我们的手机引入了不同的功能,比如具有不同优先级级别的新通知管理系统。例如,我们可以指定哪些通知是重要的,并设定一个时间段,在这个时间段内我们不想被打扰。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

另一个我们不能忽视的是这个版本在电池消耗上的改进,与之前的 Android 版本相比,它最多可以节省 90 分钟的电池寿命,这得益于一个新的 Android 运行时 ART。用非技术性的方式来解释,它是在应用安装时将应用翻译成一种可以更快被 Android 理解的语言。之前的运行时Dalvik是在执行我们应用时进行这种翻译,而不是只在安装时一次翻译。这帮助应用消耗更少的电池并运行得更快。

介绍 Android 6 Marshmallow

这个版本的主要变化之一与应用的权限有关。在 Android M 之前,我们习惯在下载应用前接受应用的权限;应用商店会向我们展示一个应用拥有的权限列表,我们需要接受它们才能下载和安装应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

运行时权限

这在引入运行时权限后发生了变化。这里的理念是在你需要时才接受权限。例如,WhatsApp 在你进行通话或留下语音消息之前可能不需要访问你的麦克风。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在开发应用程序时,我们需要考虑到这一点;这对开发者来说是一种改变,现在他们需要控制如果用户不接受权限,应该做什么。以前,在安装时,我们只需做全有或全无的选择;而现在,在运行时,我们必须考虑用户的决策。

提示

下载示例代码

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

省电优化

自从 Lollipop 版本以来,我们的手机电池寿命有了另外一项改进;这次,谷歌引入了两种新的状态:省电模式应用待机

省电模式提高了空闲设备的睡眠效率。如果我们关闭屏幕并且没有使用手机,就会进入空闲状态。以前,应用程序可以在后台进行网络操作并继续工作;现在,引入了省电模式后,系统会定期允许我们的应用程序在后台工作,并执行其他挂起操作的一段时间。在开发时,这也需要考虑;例如,在这种模式下,我们的应用程序无法访问网络。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

应用待机是一种针对长时间未使用且后台没有运行任何进程的应用程序引入的空闲模式。如果一个应用程序没有显示任何通知,并且用户没有明确要求它免除优化,那么它将进入待机模式。这种空闲模式防止应用程序访问网络和执行挂起的任务。当连接电源线时,所有处于待机状态的应用程序都会被释放,空闲限制也会被移除。

文本选择

在之前的版本中,当用户选择文本时,操作栏上会出现一组操作,如复制、剪切和粘贴。在这个版本中,我们可以在浮动的工具栏中显示这些操作以及更多内容,该工具栏将显示在选定内容的上方:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

指纹认证

在这个版本的 Android 中,我们可以验证指纹的使用。身份验证可以在设备级别进行,以解锁手机,不仅仅是为了解锁一个特定的应用程序;因此,我们可以基于用户最近解锁设备的情况,在应用程序中验证用户。

我们有一个新的可用对象,FingerprintManager,它将负责身份验证,并允许我们显示一个请求指纹的对话框。我们需要一个带有指纹传感器的设备才能使用这个功能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

直接分享

直接分享是一个新增加的功能,用于简化内容分享过程。以前,如果我们处在图库中,想要将图片分享给 WhatsApp 中的一个联系人,我们必须点击分享,在应用列表中找到 WhatsApp,然后在 WhatsApp 中找到联系人来分享内容。这个过程将被简化,直接显示一个你可以直接分享信息给的联系人群列表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这些是与 Android 6 Marshmallow 一起发布的主要新特性;完整列表可以在developer.android.com/preview/index.html查看。

创建 MasteringAndroidApp

既然我们已经了解了最新 Android 版本的主要特性,接下来可以介绍我们在本书中将要开发的应用程序。这个应用程序将包括这些特性中的大部分,但我们也会花时间研究在之前 Android 版本中广泛使用的组件。

要掌握 Android,我们应该准备好理解遗留代码;例如,我们可能需要在一个仍然使用ListView而不是新出的RecyclerView的应用上工作。我们不会总是用最新的组件从零开始创建应用,特别是如果我们是专业的 Android 开发者。同时,查看之前的组件将帮助我们理解这些组件的自然演变,从而更好地了解它们现在的样子。

我们将从零开始创建这个应用,从最初的设计开始,看看在 Android 中最常用的设计和导航模式,比如顶部标签,左侧的滑动菜单等。

我们将要开发的这个应用,MasteringAndroidApp,是一个与服务器端交互的应用。这个应用将展示存储在云中的信息,我们将创建云组件,使我们的应用能够与其通信。我们为这个应用选择的主题是职位公告板,我们将在服务器端创建职位信息,应用用户可以阅读这些信息并接收通知。

你可以轻松地自定义主题;这将是一个你可以更改信息并使用相同结构创建自己应用的例子。实际上,如果你有自己的想法会更好,因为我们将讨论如何在 Play 商店发布应用以及如何实现盈利;我们将添加广告,当用户点击广告时会产生收入。所以,如果你使用自己的想法应用所学内容,等到你完成这本书时,你将拥有一个准备发布的应用。

我们将开发这个应用,并解释在 Android 中最常用的编程模式,以及并发技术和连接到 REST API 或服务器的方法。

我们不仅关注后端,也关注 UI;通过高效地展示信息,使用列表和网格,从互联网上下载图片,以及使用最新的材料设计特性来自定义字体和视图。

我们将学习如何调试我们的应用程序,管理日志,并在学习如何识别和防止内存泄漏时考虑内存使用。

我们的应用程序将基于一个数据库的离线模式,我们将把云中的内容存储在这里。因此,如果手机失去连接,我们仍然可以显示上次在线时可用信息。

为了完成我们的应用程序,我们将添加额外的功能,如推送通知、崩溃报告和数据分析。

最后,我们将了解 Android 构建系统的工作原理,以不同版本导出我们的应用程序,并对代码进行混淆以保护它,防止被反编译。

我们压缩了大量的信息,这将帮助您在本书结束时掌握 Android;但是,在开始我们的应用程序之前,让我们先准备好工具。

准备工具

在本书中我们需要用到的工具是最新版本的 Android Studio 和更新到 Android M 或更高版本的 Android SDK。还建议您使用Genymotion,这是一个用于测试应用程序的模拟器。

注意

首先,我们需要下载并安装 Android Studio,这是在 Android 上进行开发的官方工具。可以从developer.android.com/sdk/index.html下载。

在网站的顶部,您可以根据您的操作系统版本找到一个下载链接。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

安装完成后,我们需要下载一个 Android M SDK,它将为特定 Android 版本的应用程序开发提供所有必要的类和资源。这是通过 SDK 管理器完成的,它是 Android Studio 内包含的一个工具。

我们可以点击工具 | Android | SDK 管理器,或者在 Android Studio 最上方的栏中找到快捷方式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

打开 SDK 管理器后,我们将看到一个可用 SDK 平台和 SDK 工具的列表。我们需要确保已安装最新可用的版本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这样,我们就有了我们开发应用程序所需的一切。为了测试它,理想情况下应该有 Genymotion,这是一个 Android 模拟器,它将帮助我们测试在不同设备上的应用程序。

我们使用这个模拟器而不是 Android 默认模拟器的主要原因是速度。在 Genymotion 上部署应用程序甚至比使用物理设备还要快。除此之外,我们还受益于其他功能,例如可调整大小的窗口、从我们的计算机复制和粘贴,以及使用默认模拟器时耗时的一些小细节。可以从www.genymotion.com下载。

我们需要做的就是安装它,一旦打开,我们就可以添加具有现有设备包含相同功能的模拟器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

总结

在本章中,我们回顾了最新版本的 Android 中的重要变化,重点介绍了 Android Marshmallow 和 Material Design。

我们说明了将在本书的学习过程中开发的应用程序中将执行的操作以及创建它所需的工具。

在下一章中,我们将研究 Android 中现有的设计模式并开始设计我们的应用程序。

第二章:设计我们的应用

在本章中,我们将为应用想出一个点子,并将这个点子转化为真实的应用,创建要在屏幕上显示的基本结构,并选择一个合适的导航模式在它们之间移动。

在查看最常用的导航模式之后,我们将继续实现由片段和ViewPager组成的标签模式。

在此期间,我们将回顾关于片段的知识,以便能够解释高级概念。我们还将讨论FragmentManager和片段后退栈的重要性。

最后,我们将在屏幕过渡中添加一些美观的动画。因此,本章我们将涵盖以下主题:

  • 选择应用导航模式

  • 精通片段

  • 实现标签和 ViewPager

  • 屏幕之间的动画过渡

选择应用导航模式

假设有一天你醒来时感到很有灵感;你有一个应用点子,你认为它可能比 WhatsApp 还要受欢迎。不要浪费时间,你会想要将这个应用点子变为现实!这就是为什么你需要学习如何设计应用并选择最合适的导航模式。不是要让你失去信心,但你会发现你的 99%的点子已经在 Google Play 商店里了。事实上,有数十万个应用可供选择,而且这个数字还在不断增加!所以,你可以选择改进已有的应用,或者继续头脑风暴,直到你有一个原创的点子。

为了将应用变为现实,第一步是在脑海中可视化应用;为此,我们需要确定基本组件。我们需要在屏幕上简化想法,并且需要在屏幕之间移动。

请记住,你正在为 Android 用户创建这个应用。这些用户习惯于使用如 Gmail、Facebook 和 Spotify 等应用中的滑动面板这样的导航模式。

我们将看看三种不同的常用导航模式,这些模式保证用户在我们的应用中不会迷失方向,并能立即理解应用结构。

基本结构

为了绘制我们的屏幕(请注意,我这里不是指活动或片段;所谓屏幕是指用户在我们的应用执行期间任何时候实际可以看到的内容),我们需要确定我们想法的关键点。我们需要用软件开发术语来建立用例。

让我们从确定本书学习过程中要构建的应用的形状开始:MasteringAndroidApp。一开始很难在脑海中想象出所有细节,所以我们将从确定我们肯定需要的组件开始,稍后再填补可能存在的空白。

我们从上一章知道,我们有一个演示屏幕,它会在需要时从互联网下载数据的同时显示应用的徽标几秒钟。

在这个应用程序中,我们还将有一个包含来自互联网的信息列表的屏幕,用户可以点击单个项目获取更详细的信息。

作为主要选项,我们将展示一个带有MapView显示我的位置和联系数据的联系人屏幕。

最后,我们需要一个偏好设置设置屏幕,在这里我们可以打开或关闭通知,以及禁用广告或购买额外内容。

现在,我们已经准备好创建一个草图。请看以下图片:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先,我们有应用程序的入口点,即启动屏幕。这里的导航很直接;我们可以直接导航到下一个屏幕,并且没有按钮或任何其他可能的流程。

在下一级,我们有一个项目列表的屏幕(即带有联系信息的屏幕)、一个地图视图和一个设置屏幕。这三个屏幕在我们的应用程序中处于同一级别,因此它们具有同等的重要性。

最后,我们有一个第三层导航,即列表项的详细视图。

我们打开这个屏幕的唯一方式是点击列表中的一个元素;因此,这个屏幕的入口点是列表屏幕。

既然我们已经建立了一个基本的结构和流程,接下来我们将研究各种广泛使用的导航模式,以决定哪一种最适合我们的应用程序。

注意

有关应用程序结构和有关材料设计类似信息的更多信息,请参考以下链接:

developer.android.com/design/patterns/app-structure.html

www.google.com/design/spec/patterns/app-structure.html#

仪表板模式

仪表板模式是 Android 中最早使用的模式之一。它由一组在主屏幕上以图标矩阵形式显示的元素组成。在以下图片中,我们可以看到左侧是 Facebook 应用程序的早期版本,右侧是 Motor Trend 对该模式的定制:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这种视图非常适合那些旨在清晰显示非常有限的选项的应用程序;每行不超过两个元素,行数适合屏幕显示。

这些图标清晰地展示了主要功能的符号,所有选项都在同一级别。这对于拥有大量目标受众的应用程序来说是一个理想的模式;它简单明了,一目了然,任何人都可以进行导航。

尽管这个设计看起来很古老,考虑到它曾在 Android 的第一个版本中被广泛使用,现在使用得较少,但它的使用取决于您的需求,因此不要因此而放弃。前面图片中显示的 Motor Trends 应用程序对这个模式有一个非常原始的实现。

如果元素不适合屏幕显示,我们需要滚动才能发现它们,那么我们需要重新考虑这个模式。当我们元素太少时也同样适用;这些情况下有更好的选择。在我们的具体示例中,我们有三个主要元素,因此我们将不使用此模式。

滑动面板

这种模式因如 Gmail 和 Facebook 等应用而广为人知。它在用户界面的顶层展示一个布局;当我们执行滑动手势或点击左上或右上按钮时,屏幕会从左或右滑出,这个按钮通常是一个显示三条水平线的图标,也被称为汉堡图标。

如果我们的应用在同一层级有大量选项,这个模式是完美的,并且它可以与其他模式结合使用,比如选项卡模式

此面板的实现可以通过DrawerLayout类完成,它由两个子视图组成:包含内容和导航抽屉的FrameLayout,导航抽屉可以是ListView或包含选项的任何其他自定义布局。

为此,执行以下代码:

<android.support.v4.widget.DrawerLayout    
   android:id="@+id/drawer_layout"   
   android:layout_width="match_parent"   
   android:layout_height="match_parent" >   

   <FrameLayout   
    android:id="@+id/frame_container"   
    android:layout_width="match_parent"   
    android:layout_height="match_parent" />   

   <ListView   
    android:id="@+id/drawer_list"   
    android:layout_width="240dp" 
    android:background="#fff"  
    android:layout_height="match_parent"   
    android:layout_gravity="start" />   

  </android.support.v4.widget.DrawerLayout>

一旦我们在侧面板中选择了一个元素,屏幕中间就会出现一个子元素;这个子元素可以帮助你导航到子子元素,但绝不能导航到主菜单的元素。子元素和子子元素的导航可以通过后退按钮或操作栏中的向上导航来管理。

我们可以通过点击一个条目来关闭面板,并通过设置一个抽屉监听器ActionBarDrawerToggle来知道面板是关闭还是打开,它包含onDrawerClosed(View drawerView)onDrawerOpened(View drawerView)方法。

确保你使用的是android.support.v7.app中的ActionBarDrawerToggle,v4 中的已弃用。

这种模式的另一个大优点是它允许通过菜单上的主项目进行分组导航,可以展开成子项目。正如以下示例所示,项目 4 在下拉菜单中有三个选项:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

抽屉布局的一个示例

这对我们的应用程序来说并不合适,因为我们没有足够的选项来充分利用这个模式。此外,由于这个模式可以与选项卡模式结合,从教育角度来看,用这个模式开发我们的示例更有意义。

选项卡

选项卡模式是一种你可能之前见过并使用过的模式。

它显示了一个具有同一层级组件的固定菜单。注意,当我们有选项卡时,菜单总是可见的,这在滑动和仪表板模式中不会发生。这看起来与网页界面非常相似,并且考虑到用户可能已经熟悉这个模式,它非常用户友好。

以下模式有两个变体:固定和滑动选项卡。如果我们只有少量可以适应屏幕的菜单项,第一个变体将是最合适的,因为它一次向用户展示所有项目。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当所有项目不适合屏幕或适合但知道将来会添加更多项目而无法容纳时,通常会使用滑动标签页。

这两个变体的实现略有不同,因此我们需要在决定变体时考虑未来的变化。在这里,我们可以看到一个滑动变体的实现:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

提示

记住,为了平台一致性,我们必须将标签放在屏幕顶部;否则,人们会认为你是 iOS 开发者!

以下是一些供你遵循的材料设计指南中的功能和格式规范:

  • 将标签作为单行呈现。如果需要,将标签文字换行到第二行,然后截断。

  • 不要在标签页内包含一组标签化内容。

  • 高亮显示与可见内容对应的标签页。

  • 按层次结构将标签页分组。将一组标签与其内容连接起来。

  • 保持标签与其内容相邻。这有助于减少两者之间的歧义,保持关系。

在以下图片中,我们可以看到一个带有子菜单的滚动/滑动标签页的例子:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意

设计标签时图形规格以及有关标签规范更多信息可以在www.google.com/design/spec/components/tabs.html#找到。

既然我们已经了解了应用导航的基础知识,我们可以探索实现这些模式所需的组件。正如你所知,主要组件是活动和片段。我们将实现一个带有三个片段的滑动标签页的例子。

片段

在本节中,我们将简要回顾片段的关键概念,以解释高级功能和组件,如片段管理器和片段回退栈。

在我们的例子中,我们将创建一个名为MainActivity的活动和四个片段:ListFragmentContactFragmentSettingsFragmentDetailsFragment。为此,你可以创建一个fragments包,双击该包装进入新建 | 片段 | 空白片段。看看以下对话框:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,你可以不使用片段工厂方法和接口回调来创建它们。我们将在本章后面介绍这些内容。

目前我们的项目在项目视图中应该看起来像这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

理解片段的重要性

片段代表活动中的行为或用户界面的一部分。你可以在单个活动中组合多个片段来构建多窗格 UI,并在多个活动中重用片段。你可以将片段视为活动的模块化部分,它有自己的生命周期并接收自己的输入事件,你可以在活动运行时添加或移除(有点像可以在不同活动中重用的子活动)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

片段的生命周期与活动的生命周期略有不同。我们注意到的第一个区别是使用了OnAttach()OnDetach()方法,它们将片段与活动连接起来。

onCreate()中使用,我们可以在OnCreateView()中创建视图;在这之后,我们可以在片段中调用getView(),它不会是 null。

onActivityCreated()方法告诉片段其活动在其自身的Activity.onCreate()中已完成。

有两种方法可以显示一个片段:

第一种方式是在我们的布局 XML 中拥有片段。这将创建我们的片段,当包含它的视图被填充时。执行以下代码:

<LinearLayout 
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <fragment android:name="com.example.android.MyFragment"
              android:id="@+id/headlines_fragment"
android:layout_width="match_parent"
              android:layout_height="match_parent" />
</LinearLayout>

第二种方式是程序化创建我们的片段,并告诉片段管理器在容器中显示它。为此,你可以使用以下代码:

<LinearLayout 
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

    <Framelayout android:id="@+id/fragment_container"
android:layout_width="match_parent"
             android:layout_height="match_parent" />

</LinearLayout>

之后,使用以下代码行填充一个FrameLayout容器,片段将被插入到其中:

Myfragment fragment = MyFragment.newInstance();
getSupportFragmentManager().beginTransaction()
                    .add(R.id.fragment_container, fragment).commit();

为了结束关键概念,解释为什么 Android 示例使用MyFragment.newInstance(params)工厂方法创建片段而不是使用默认的new MyFragment(params)构造函数是很重要的。请看以下代码:

public class MyFragment extends Fragment {

 // Static factory method that returns a new fragment
 // receiving a   parameter and initializing the fragment's arguments

    public static MyFragment newInstance(int param) {
        MyFragment fragment = new MyFragment();
        Bundle args = new Bundle();
        args.putInt("param", param);
        fragment.setArguments(args);
        return fragment;
    }
}

这种模式背后的原因是,Android 只使用默认构造函数重新创建片段;因此,如果我们有一个带参数的构造函数,它将被忽略,参数将丢失。

提示

请注意,我们将参数作为 bundle 中的参数传递,这样如果片段需要被重新创建(由于设备方向改变,我们使用后退导航),片段就可以检索参数。

片段管理器

片段管理器是一个接口,用于与活动内的片段交互。这意味着任何操作,如添加、替换、移除或查找片段,都必须通过它来完成。

要获取片段管理器,我们的Activity需要继承自FragmentActivity,这将允许我们调用getFragmentManager()getSupportFragmentManager(),优先选择后者因为它使用了Android.support.v4中包含的向后兼容的片段管理器。

如果我们想使用嵌套片段,可以用getChildFragmentManager()来管理它们。当布局包括<fragment>时,你不能将布局填充到片段中。只有当动态地将嵌套片段添加到片段中时,才支持嵌套片段。

现在,我们将讨论在使用片段时迟早会遇到的一些场景。设想我们有一个带有两个片段 A 和 B 的活动。

一个典型的情况是,我们处于一个片段中,并希望执行活动中的方法。在这种情况下,我们有两个选择;一个是 在MyActivity中实现一个public方法,例如doSomething(),这样我们可以将getActivity强转为我们的活动,并调用((MyActivity)getActivity).doSomething();方法。

第二种方法是让我们的活动实现碎片中定义的接口,并在onAttach(Activity)方法中将活动的实例设置为该接口的监听器。我们将在第四章 并发与软件设计模式中解释这种软件模式。反过来,如果要让活动与碎片通信(如果我们没有在活动的变量中实例化碎片 A),我们可以找到管理器中的碎片。我们将在下一节中查看如何使用容器 ID 或标签查找碎片:

FragmentManager fm = getSupportFragmentManger();
FragmentA fragmentA = fm.findFragmentById(R.id.fragment_container);
fragmentA.doSomething(params);

最后一种情况是在碎片 A 中与 B 对话;为此,我们只需从活动中获取管理器并查找碎片。运行以下代码:

FragmentManager fm = getActivity().getSupportFragmentManger();
FragmentA fragmentA = fm.findFragmentById(R.id.fragment_container);
fragmentA.doSomething(params);

碎片堆栈

我们一直在谈论在碎片管理器中查找碎片,这要归功于碎片管理器的碎片堆栈,我们可以在事务期间添加或移除碎片。

当我们想要动态显示一个碎片时,我们可以决定是否要将碎片添加到堆栈中。将碎片放在堆栈上允许我们导航回上一个碎片。

这对于我们的示例非常重要;如果用户在第一个标签页上,点击列表中的项目,我们希望他能看到详情屏幕,DetailsFragment。现在,如果用户在DetailsFragment上,点击返回按钮,我们不希望他离开应用;我们希望应用能导航回碎片堆栈。这就是为什么我们在添加DetailsFragment时,必须包含addToBackStack(String tag)选项。标签可以为 null,也可以是一个String类型,这将允许我们通过标签找到这个新碎片。它将类似于以下内容:

FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.simple_fragment, newFragment);
ft.addToBackStack(null);
ft.commit();

进一步说明,如果我们想在三个碎片之间导航,从 A 到 B 再到 C,然后返回,拥有一个堆栈将允许我们从C 到 B 再到 A。然而,如果我们不将碎片添加到返回堆栈,或者如果我们在同一个容器中添加或替换它们,从 A 到 B 再到 C,这将只留下 C 碎片,并且无法进行返回导航。

现在,要在DetailsFragment中实现返回导航,我们必须让活动知道,当我点击返回时,我想首先在碎片中导航回退,然后才退出应用,这是默认的行为。如果堆栈中有一个以上的碎片,可以通过覆盖onKeyDown并处理碎片导航来实现。运行以下命令:

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && getSupportFragmentManager.getBackStackEntryCount > 1) {
getSupportFragment.popBackStack();
return true;
}
return super.onKeyDown(keyCode, event);
}

视图翻页器

继续我们的示例,在MainActivity上有两种在碎片之间导航的方法:通过点击标签或通过在碎片间滑动。为了实现这一点,我们将使用ViewPager,包括其中的滑动标签,这是一个非常优雅的解决方案,代码量最少,并包括滑动与标签之间的同步。

ViewPager可用于滑动任何类型的视图。我们可以用ViewPager创建一个图片画廊;在一些应用首次运行时,常见到使用滑动屏幕来展示如何使用应用的教程,这是通过ViewPager实现的。要将ViewPager添加到MainActivity,我们可以简单复制并粘贴以下代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager

android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />

ViewPager部分最后,我们将了解如何使用不同的第三方库来改善标签页的体验,以及如果我们想要自定义解决方案,如何手动创建这些标签页。

适配器

ViewPager与适配器一起工作;适配器是负责创建我们滑动的每个页面的元素。在滑动片段的特殊情况下,我们可以使用Adapter类的扩展,称为FragmentPagerAdapterFragmentStatePagerAdapter

  • FragmentStatePagerAdapter保存页面的状态,在屏幕上不显示时销毁它,并在需要时重新创建,类似于ListView对其行所做的处理。

  • FragmentPagerAdapter将所有页面保存在内存中;因此,在滑动时没有与保存和恢复状态相关的计算成本。我们可以拥有的页面数量取决于内存。

根据元素的数量,我们可以选择其中一种。如果我们正在创建一个阅读新闻的应用,用户可以在带有图片和不同内容的新闻文章之间滑动,我们不会尝试将所有内容都保存在内存中。

我们有三个固定标签,因此我们将选择FragmentPagerAdapter。我们将创建一个包适配器,并创建一个扩展FragmentPagerAdapterMyPagerAdapter类。在扩展它时,我们需要覆盖getCount()getItem(int i)方法,这些方法返回项目的数量和给定位置的项目。

创建构造函数并完成方法后,我们的类将类似于以下代码:

public class MyPagerAdapter extends FragmentPagerAdapter {

    public MyPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int i) {
        switch (i) {
            case 0 :
                return new ListFragment();
            case 1 :
                return new ContactFragment();
            case 2 :
                return new SettingsFragment();
            default:
                return null;
        }
    }

    @Override
    public int getCount() {
        return 3;
    }
}

最后,我们需要在MainActivity中将适配器设置到页面。执行以下代码:

public class MainActivity extends FragmentActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
        ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
        viewPager.setAdapter(adapter);

    }

}

滑动标签

在我们的示例中,此时我们已经能够在片段之间滑动。现在,我们将使用PagerTabStripPagerTitleStrip添加标签。

有一种非常优雅的实现方式,即在ViewPager的 XML 标签中包含PageTabStrip。执行以下代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager

android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="wrap_content">

    <android.support.v4.view.PagerTabStrip
        android:id="@+id/pager_title_strip"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="top"
        android:background="#33b5e5"
        android:textColor="#fff"
        android:textSize="20dp"
        android:paddingTop="10dp"
        android:paddingBottom="10dp" />

</android.support.v4.view.ViewPager>

在这里,PagerTabStrip会找到页面的标题,并为每个页面显示一个标签。我们需要在MyPagerAdapter中添加getPageTitle方法,这将返回每个页面的字符串。在我们的案例中,这将是部分名称:列表、联系人及设置。为此,你可以使用以下代码:

@Override
public CharSequence getPageTitle(int position) {
  switch (position) {
    case 0 :
    return "LIST";
    case 1 :
    return "CONTACT";
    case 2 :
    return "SETTINGS";
    default:
    return null;
  }
}

运行应用,瞧!我们轻松实现了一个流畅的标签页和滑动导航支持,支持 Android 1.6(API 4):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

自定义标签

Android 中的标签有一段很长的历史;最初,标签是通过TabActivity实现的,但在 API 13 中被废弃,并演变成了FragmentTabHost

因此,我按照 Android 文档,开心地使用TabHost开发了一个应用,并意识到这必须改变。起初,我抱着侥幸心理,希望废弃不会影响我的应用,直到一些用户抱怨崩溃问题。然后,不可避免地,我必须移除废弃的TabHost并寻找新方法。

起初,FragmentTabHost似乎是拥有固定标签的好方法,但它不允许标签上带有图标。在遇到这个问题,并在 Stack Overflow 上发现其他人也有同样的问题时(一个我们可以提问和找到关于 Android 和其他主题答案的网站),我决定寻找其他方法。

在 API 11 中,出现了ActionBar.Tab的概念,这是一个允许我们向操作栏添加标签的类。最终,我找到了一种在应用中添加标签的方法,这让用户很开心!但这种喜悦并未持续太久;ActionBar.Tab又被废弃了!

这件事会让任何开发者的耐心耗尽;这让我创建了在LinearLayout中以按钮形式呈现的标签。在按钮上设置点击监听器,点击标签时,我会将ViewPager滑动到正确的页面,反之亦然,当检测到ViewPager页面滑动时,我会选择正确的标签。努力是值得的,因为它让我在设计标签时有完全的自由度,更重要的是,它给了我满足感,除非有一天LinearLayoutButton被废弃,否则它总是能工作!

你总可以将自己的实现作为最后的选择。如今,如果你不喜欢滑动标签的设计,还有第三方库的其他选择,比如ViewPagerIndicatorPagerSlidingTabStrip

注意

若要了解更多信息,你可以查看以下链接:

ViewPagerIndicator 的 GitHub 仓库

PagerSlidingTabStrip 的 GitHub 仓库

过渡效果

比如创建我们自己的屏幕过渡动画等小细节,这些都能让我们的应用更加精致,看起来更加专业。

我们的示例非常适合讨论过渡,因为我们有两种类型的屏幕过渡:

  • 第一个转换是活动之间的转换,从SplashActivityMainActivity

  • 第二个转换(尚未实现)是片段之间的转换,其中ListFragmentDetailsFragment替换。

对于活动之间的过渡,我们需要在启动新活动之前调用overridePendingTransition。该方法接收两个动画作为参数,这些动画可以是我们创建的 XML 文件中的,也可以是 Android 中已经创建的动画。运行以下命令:

overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);

在我们的示例中,我们不允许可返回导航到SplashActivity;然而,如果我们处于活动之间的过渡,并希望在点击返回时拥有相同的过渡效果,我们就必须重写返回键并在此处设置我们的过渡效果。为此,你可以运行以下命令:

@Override public void onBackPressed() {
   super.onBackPressed();        overridePendingTransition(android.R.anim.fade_in,  android.R.anim.fade_out); 
}

在片段的情况下,我们需要在FragmentTransaction对象中指定过渡。使用对象动画师,我们可以在两个文件中定义这一点:enter.xmlexit.xml。执行以下代码:

FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.setCustomAnimations(R.animator.enter, R.animator.exit);
transaction.replace(R.id.container, new DetailsFragment());
transaction.commit();

enter.xml

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

         android:duration="1000"
         android:propertyName="y"
         android:valueFrom="2000"
         android:valueTo="0"
         android:valueType="floatType" />
</set>

exit.xml 
<?xml version="1.0" encoding="utf-8"?>
<set>
    <objectAnimator

        android:duration="1000"
        android:propertyName="y"
        android:valueFrom="0"
        android:valueTo="-2000"
        android:valueType="floatType" />
</set>

对于 Android Lollipop 及其之后的版本,你可以直接为 Fragment 设置过渡。使用以下代码片段:

Fragment f = new MyFragment();
f.setEnterTransition(new Slide(Gravity.RIGHT));
f.setExitTransition(new Slide(Gravity.LEFT));

总结

在本章结束时,你应该了解基本的导航模式,并能将你心中应用程序的想法转化为 Android 应用程序的实际结构。Fragments 是 Android 开发中的关键概念,我们在本章中已经花费足够的时间通过复习 Fragment Manager 和片段回退栈来掌握它们,并学习如何面对诸如它们之间的通信等常见问题。我们考虑了一个带有PagerTabStripViewPager的工作示例,它将页面标题作为标签显示,你现在知道如果需要如何自定义它。我们有一个应用程序的框架;这个项目可以在这一阶段保存,并用作你未来开发的模板。我们已经准备好继续发展我们的应用程序。

在下一章中,我们将学习如何创建和访问将填充我们片段和ViewPager的内容,以使我们的应用程序生动起来。

第三章:从云端创建和访问内容

在本章中,我们将学习如何使用我们的应用程序从网络上获取内容;这些内容可能是一个 XML 或 JSON 文件中的项目列表(我们希望展示的内容),从互联网上获取。例如,如果我们正在构建一个显示当前天气状况的应用程序,我们需要联系外部 API 以获取所需的所有信息。

我们将在 Parse 中创建自己的云数据库,这项服务允许我们非常快速地完成这一操作,而无需创建和维护自己的服务器。除此之外,我们还将用要在MasteringAndroidApp中展示的信息填充数据库。

我们还将介绍与 Google Volley 网络请求相关的最佳实践,使用超快的 HTTP 库 OkHttp,以及使用 Gson 高效地解析请求的对象。我们将在本章中介绍以下主题:

  • 创建你自己的云数据库

  • 从 Parse 中消费内容

  • Google Volley 和 OkHttp

  • 使用 Gson 解析对象

创建你自己的云数据库

在项目的这个阶段,我们必须开始构建我们自己的版本的MasteringAndroidApp。你可以自由地开发自己的思路,并使用数据库存储自己的数据。以本例为指南,你不必严格按照我写的代码逐行复制。实际上,如果你在本书的最后开发出自己的示例,你将得到一个你可以使用的东西。例如,你可以创建一个供个人使用的应用程序,如任务提醒、旅行日记、个人照片画廊,或任何适合在云端存储的东西。

你也可以尝试将这个应用货币化;在这种情况下,你应该尝试为用户开发一些有趣的东西。例如,它可以是新闻源阅读器或食谱阅读器;它可以是任何可以提交内容到云端并通知用户新内容可用的应用。

在此过程中,我们将解释Application类的重要性,该类用于在我们的项目中设置 Parse。

Parse

如果你每秒的请求少于 30 次,Parse 是免费的。我猜想,如果你的应用有足够的用户每秒请求信息 30 次,即每分钟 1,800 次,你肯定能负担得起升级到付费账户,甚至构建自己的服务器!这项服务是一种非常简单且可靠的方法,可以为你的应用提供服务器端支持。此外,它还提供推送通知服务和分析,这也是它的一个优点。

我们将开始创建一个新账户;之后,我们需要在 Parse 中为我们的应用程序命名。在这里,我将使用MasteringAndroid。命名应用程序后,你将进入账户的主页。我们需要导航至数据服务 | 移动端 | Android | 原生 Java

下图展示了作为云的数据服务:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将 Parse SDK 添加到我们的项目中

要从我们的应用程序访问数据服务,我们需要安装 Parse SDK系统开发工具包)。为此,Parse 指引我们查看一个快速入门指南,其中包含所有代码,包括我们应用程序的 API 密钥,这些代码已准备好复制并粘贴到我们的项目中。

基本上,我们需要完成两个步骤:

  1. 第一步是下载一个 .jar 库文件,我们需要将其复制到项目中的 libs 文件夹内。复制后,我们需要告诉我们的构建系统在应用程序中包含这个库。为此,我们需要在 Application 文件夹内找到 build.gradle 文件(注意,我们的项目中有两个 build.gradle 文件),并添加以下几行:

    dependencies {
      compile 'com.parse.bolts:bolts-android:1.+'
      compile fileTree(dir: 'libs', include: 'Parse-*.jar')
    }
    
  2. 在下图中,你可以看到两个名为 build.gradle 的文件;被选中的是正确的文件:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 第二步是在我们的项目中初始化 Parse SDK;为此,我们可以直接导航到www.parse.com/apps/quickstart?app_id=masteringandroidapp。在链接中替换你自己的应用 ID,或者通过点击主页找到链接,如下面的截图所示:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  4. 点击 快速入门指南 后,转到 数据 | 移动 | Android | 原生 | 现有项目

  5. 如果尚未添加,系统会提示你在 AndroidManifest.xml 文件中添加 INTERNETACCESS_NETWORK_STATE 权限。

Android 的 Application 类

接下来我们要注意的一点是,我们需要将初始化 Parse 的代码添加到我们的 Application 类中;然而,我们的 Application 类在项目中默认并未创建。我们需要创建并了解 Application 类是什么以及它是如何工作的。

要创建一个 Application 类,我们将在包上右键点击并创建一个新的 Java 类,名为 MAApplication,继承 Application。一旦继承了 Application,我们就可以重写 onCreate 方法。然后,我们将在类内部右键点击 | 生成。 | 重写方法 | onCreate

这将重写 onCreate 方法,我们将准备好在那里实现我们自己的功能。onCreate 方法每次创建我们的 Application 时都会被调用;因此,它是初始化我们的库和第三方 SDK 的正确位置。现在,你可以按照快速入门指南中看到的 Parse 初始化行进行复制和粘贴。

提示

请注意,这是唯一的,对于你自己的账户,你应该有自己的密钥。

Parse.initialize(this, "yourKeyHere", "yourKeyHere");

最后,我们需要告诉我们的应用程序有一个新的 Application 类,并且我们想要使用这个类;如果我们不这样做,我们的 Application 类将不会被识别,onCreate 也不会被调用。

在我们的清单文件中,我们需要在 <application> 标签内设置属性名称以匹配我们自己的应用程序。执行以下代码:

<application
    android:name="MApplication "
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_newname"
>

应用程序类封装了我们应用中的所有内容;活动包含在应用程序中,随后,片段包含在活动中。如果我们需要在应用中访问所有活动/片段的全局变量,这将是一个合适的地方。在下一章中,我们将了解如何创建这个全局变量。以下图表是应用程序的图形结构:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

创建数据库

如我们所知,在本书中我们将创建一个示例应用,该应用将提供与 Android 相关的职位信息;因此,我们需要创建一个数据库来存储这些职位信息。

在开发过程中可以更改数据库(当应用发布并拥有用户时,这将变得更加困难)。但是,现在我们将从大局出发,创建整个系统,而不是拥有一个包含所有字段完成的最终版数据库。

要创建一个表,请点击如下截图所示的Core部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

首先,通过点击**+ 添加类按钮创建一个表,并将其命名为JobOffer**,包含以下属性,可以通过点击**Col+**按钮添加:

  • objectId: 这是默认创建的:String

  • title: 这是职位名称:String

  • description: 这是工作描述:String

  • salary: 这表示薪水或日薪:String

  • company: 这表示提供工作的公司:String

  • type: 这表示员工的类型,可以是永久、合同或自由职业者:String

  • imageLink: 这是公司的图片:String

  • Location: 这表示工作的地点:String

  • createdAtupdatedAt: 这是工作的日期;这些列是使用默认日期创建的

要向表中添加数据,请在左侧选择表并点击**+ 行**。我们只需要完成我们创建的列;默认创建的列,如 ID 或日期,将自动完成。到目前为止,我们的表应如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

随意添加更多详细信息,例如联系人、电子邮件和手机号码。你也可以添加更多表;例如,一个新的JobType表,包含工作类型和字段类型,而不是String,应为Relation<JobType>

我们已经拥有我们示例所需的内容;接下来要做的是使用我们的应用程序消费这些数据。

在 Parse 中存储和消费内容

Parse 是一个非常强大的工具,它不仅允许我们轻松地消费内容,还可以从我们的设备将内容存储在云数据库中,使用传统方法进行这项任务是相当繁琐的。

例如,如果我们想从设备上传图片到自定义服务器,我们就需要创建一个POST请求,并发送一个带有正确编码的表单,同时将图片作为FileBody对象附加在MultiPartEntity中,并导入 Apache HTTP 库:

HttpClient httpclient = new DefaultHttpClient();
HttpPost httppost = new HttpPost("URL TO SERVER");

MultipartEntity mpEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
File file = new File(filePath);
mpEntity.addPart("imgField", new FileBody(file, "application/octet"));

httppost.setEntity(mpEntity);
HttpResponse response = httpclient.execute(httppost);

现在,让我们看看 Parse 的替代方案:

ParseFile imgFile = new ParseFile ("img.png", ImgByteArray);

ParseObject myParseObject = new ParseObject ("ParseClass");    
 myParseObject.put("imageField", imgFile);
 myParseObject.saveInBackground();

让我们不要忘记在 Parse 上处理错误。你可以简单地这样写,非常优雅:

imageObj.saveInBackground(new SaveCallback() {
  @Override
  public void done(ParseException e) {
    if (e == null) {
      //Successful
    } else {
      //Error
    }
  }
});

存储内容

为了说明 Parse 的简便性,我们将从我们的应用程序将职位信息上传到我们的 Parse 云。

为了实现这一点,我们可以在联系人片段中创建一个按钮,在应用程序的最终版本中将其设置为不可见,因为我们不希望用户自己上传职位信息。

通过这个按钮,我们将创建一个类似于地图的ParseObject。我们将添加我们想要完成的字段,之后我们将调用saveInBackground()方法,这是上传对象的方法。执行以下代码:

view.findViewById(R.id.addJobOffer).setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {

    ParseObject jobOffer = new ParseObject("JobOffer");

    jobOffer.put("title", "Android Contract");
    jobOffer.put("description", "6 months rolling    contract. /n The client" +
    "is a worldwide known digital agency");
    jobOffer.put("type", "Contract");
    jobOffer.put("salary", "450 GBP/day");
    jobOffer.put("company", "Recruiters LTD");
    jobOffer.put("imageLink", "http://.....recruitersLTD_logo.png");
    jobOffer.put("location","Reading, UK");

    jobOffer.saveInBackground();
  }
});

如果在您自己的MasteringAndroidApp版本中,您希望用户上传内容,您可以显示一个带有EditText的对话框,让用户编写职位信息,按下上传按钮,然后您将发送带有用户编写字段的jobOffer对象。

运行应用程序,导航到联系人,并点击按钮。如果数据正确上传,在浏览器中打开 Parse 云数据库时,你应该能看到刚刚上传的职位信息额外的一行。

提示

记得在AndroidManifest.xml中添加权限,android.permission.ACCESS_NETWORK_STATEandroid.permission.INTERNET

消费内容

Parse 云中的对象默认有一个对象标识符,即objectId字段。让我们通过 ID 开始检索对象,之后,我们可以检索带有和没有过滤器的所有对象列表。运行以下代码:

ParseQuery<ParseObject> query = ParseQuery.getQuery("JobOffer");
query.getInBackground("yourObjectID", new GetCallback<ParseObject>() {
  public void done(ParseObject object, ParseException e) {
    if (e == null) {
      // object will be our job offer
    } else {
      // something went wrong
    }
  }
});

当网络请求完成时,ParseQuery对象会在网络上异步执行查询。回调中包含的方法done (the ParseObject object, ParseException e)将被执行。

检验结果的一个好方法是打印日志;在异常为null的情况下,意味着一切正常。

if (e == null) {
  Log.d("PARSE_TEST",object.getString("Title"));
} else {
  // something went wrong
}

我们可以从ParseObject中提取每个字段,并在我们的应用程序中创建一个JobOffer类,其构造函数的参数与对象的字段相匹配。使用以下代码片段:

JobOffer myJobOffer = new JobOffer(object.getString("title), object.getString("description"),);

然而,有一种更好的方法可以实现这一点。我们可以创建一个扩展了ParseObjectJobOffer类,这样所有的字段都会自动转换成我们类中的变量。这样,我们就可以非常方便地使用自己的类,而不是ParseObject

public void done(JobOffer jobOffer, ParseException e) 

不要忘记在类顶部添加@ParseClassName("Name")注解,以让 Parse 知道我们要实例化云中的哪个对象,并在MAApplication中初始化解析之前注册子类:

public class MAApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // Enable Local Datastore.
        Parse.enableLocalDatastore(this);

        ParseObject.registerSubclass(JobOffer.class);

        Parse.initialize(this, "KEY", "KEY");
    }

}

@ParseClassName("JobOffer")
public class JobOffer extends ParseObject {

    public JobOffer() {
        // A default constructor is required.
    }

    public String getTitle() {
        return getString("title");
    }

    public void setTitle(String title) {
        put("title", title);
    }

    public String getDescription() {
        return getString("description");
    }

    public void setDescription(String description) {
        put("description", description);
    }

    public String getType() {
        return getString("type");
    }

    public void setType(String type) {
        put("type", type);
    }
    //Continue with all the fields..

}

现在我们已经创建了自己的自定义类,获取所有职位列表就更加容易了。如果我们愿意,可以用一个参数来过滤它。例如,我可以用以下查询检索所有永久职位:

ParseQuery< JobOffer > query = ParseQuery.getQuery("JobOffer");
query.whereEqualTo("type", "Permanent");
query.findInBackground(new FindCallback<JobOffer>() {
    public void done(List<JobOffer> jobsList, ParseException e) {
        if (e == null) {
            Log.d("score", "Retrieved " + jobsList.size() + " jobs");
        } else {
            Log.d("score", "Error: " + e.getMessage());
        }
    }
});

显示内容

一旦检索到对象列表,就可以创建ListView和一个接收对象作为参数的Adapter。为了结束对 Parse 的使用,我们将使用另一个功能,它允许我们直接从查询结果创建适配器;这样,我们就不必自己创建一个Adapter类了。

在这两种情况下,我们需要创建ListView和列表行的视图。现在,只需显示标题和描述的第一行即可。我们将在第七章《图像处理和内存管理》中自定义此视图并添加一个图片。按照以下方式创建一个row_job_offer.xml布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp">

    <TextView
        android:id="@+id/rowJobOfferTitle"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Title"
        android:textColor="#555"
        android:textSize="18sp"
        />

    <TextView
        android:id="@+id/rowJobOfferDesc"
        android:layout_marginTop="5dp"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="Description"
        android:textColor="#999"
        android:textSize="16sp"
        android:maxLines="1"
        android:ellipsize="marquee"
        />

</LinearLayout>

现在我们准备创建ParseQueryAdapter并自定义getItemView()方法。这个适配器的一个巨大优势是我们不需要通过查询下载数据,因为它是自动完成的;基本上,我们可以从云中创建一个适配器来显示项目列表。从未如此简单!

要覆盖类中的方法——在这种情况下,我们想要覆盖getItemView——我们可以创建一个子类,一个扩展ParseQueryAdapterMyQueryAdapter类,并在该子类中覆盖方法。这是一个很好的解决方案,特别是如果我们想在应用程序中多次实例化对象。

然而,有一种方法可以在不扩展类的情况下覆盖方法;我们可以在对象实例化后添加{ }。例如,参考以下代码:

Object object = new Object() {

 //Override methods here

 }

使用这种方法,我可以创建一个新的ParseQueryAdapter并自定义getItemView,如下面的代码所示:

ParseQueryAdapter<JobOffer> parseQueryAdapter = new ParseQueryAdapter<JobOffer>(getActivity(),"JobOffer") {

  @Override
  public View getItemView(JobOffer jobOffer, View v, ViewGroup parent) {

    if (v == null) {
      v = View.inflate(getContext(), R.layout.row_job_offer, null);
    }

    super.getItemView(jobOffer, v, parent);

    TextView titleTextView = (TextView) v.findViewById(R.id.rowJobOfferTitle);
    titleTextView.setText(jobOffer.getTitle());
    TextView descTextView = (TextView) v.findViewById(R.id.rowJobOfferDesc);
    descTextView.setText(jobOffer.getDescription());

    return v;
  }

};

我们现在将在ListFragment的布局中创建ListView,在OnCreateView中找到这个视图,将适配器设置到列表中,这样就完成了。不再需要代码来检索项目并显示它们。如果您的列表为空,请确保在MyPagerAdapter中导入com.packtpub.masteringandroidapp.fragments.ListFragment;而不是android.support.v4.app.ListFragment;它们是不同的对象,使用后者将导致显示一个空的内置ListFragment

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

谷歌 Volley 和 OkHttp

要掌握 Android,我们不能依赖像 Parse 这样的解决方案。作为开发者,我们必须准备面对不同的服务器端解决方案。我们不能总是使用ParseObjects,因为我们需要能够进行 HTTP Post请求并消费 JSON 或 XML 格式的数据。然而,这并不意味着我们必须手动完成所有这些工作;我们可以使用谷歌的官方库来帮助我们解析数据和网络请求。

为此,我们将研究Google Volley,这是一个强大的库,用于管理我们的网络请求。我们还将讨论OkHttp,这是一个超快的 HTTP 客户端,并将两者结合以获得网络请求的惊人解决方案。

Google Volley

根据官方定义和功能列表来自developer.android.com/training/volley/index.html的说明,"Volley 是一个 HTTP 库,它使得 Android 应用的网络通信变得更加简单,最重要的是,更快”。

Volley 提供以下好处:

  • 网络请求的自动调度

  • 多个并发网络连接

  • 透明的磁盘和内存响应缓存,具有标准的 HTTP 缓存一致性

  • 支持请求优先级

  • 请求 API 的取消;这意味着您可以取消单个请求,或设置要取消的请求块或作用域

  • 易于定制;例如,用于重试和退避

  • 强大的排序功能,这使得可以轻松地用从网络异步获取的数据正确填充 UI

  • 调试和跟踪工具

在 Volley 诞生之前,在 Android 中管理网络请求是一项艰巨的任务。几乎每个应用程序都会执行网络请求。诸如定制重试(如果连接失败,我们需要再次尝试)以及管理并发网络连接等功能通常需要开发者手动实现。如今,我们习惯于这类库,但如果我们回想几年前的情景,Volley 是解决这一问题的绝佳方案。

在了解如何创建请求之前,我们需要理解 Volley 请求队列对象RequestQueue的概念。Volley 执行的每个请求都必须添加到此队列中,以便执行。这个想法是让我们的应用程序中有一个单一的请求队列,所有的网络请求都可以添加到其中,并且可以从应用程序的任何部分访问。我们将在第四章,并发与软件设计模式中看到如何拥有一个可以全局访问的对象实例。请看以下请求:

// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);

如果设备的 Android 版本晚于 Gingerbread,此请求队列将只使用以下HttpURLConnectionAndroidHttpClient方法;在 Gingerbread 之前的版本中,HttpURLConnection是不可靠的。

// If the device is running a version >= Gingerbread...
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
    // ...use HttpURLConnection for stack.
} else {
    // ...use AndroidHttpClient for stack.
}

当实例化请求队列时,我们只需向其中添加一个请求。例如,一个网络请求www.google.com,它会记录响应:

String url ="https://www.google.com";

// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
            new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        // Display the first 500 characters of the response string.
        Log.d("Volley","Response is: "+ response.substring(0,500));
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        Log.d("Volley","That didn't work!");
    }
});

// Add the request to the RequestQueue.
queue.add(stringRequest);

请求将被执行,并且在应用程序主线程(也称为 UI 线程)中调用onResponse(…)onErrorResponse(…)方法。我们将在第四章,并发与软件设计模式中更详细地解释 Android 中的线程。

OkHttp

OkHttp 是来自 Square 公司的 Android 和 Java 的 HTTP 和 SPDY 客户端。它不是 Volley 的替代品,因为它不包括请求队列。实际上,我们可以像下一节将看到的那样,使用 OkHttp 作为 Volley 的底层。

根据官方定义,“HTTP 是现代应用程序联网的方式。它是我们交换数据和媒体的方法。高效地使用 HTTP 可以使你的东西加载更快,节省带宽”。

如果我们不需要处理队列中的请求,优先处理请求或计划请求,我们可以直接在应用程序中使用 OkHttp;我们不一定需要 Volley。

例如,以下方法打印给定 URL 响应的内容:

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {

  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();

}

除了比使用AsyncTaskHttpUrlConnection进行请求更简单之外,让我们决定使用 OkHttp 的是 SPDY(快速)协议,它处理、标记化、简化和压缩 HTTP 请求。

极速网络

如果我们想保持 Volley 的特性,以便拥有灵活可管理的请求队列,并使用 SPDY 协议实现更快连接,我们可以将 Volley 和 OkHttp 结合起来使用。

这真的很简单;在实例化请求队列时,我们可以指定我们想要的HttpStack方法:

RequestQueue queue = Volley.newRequestQueue(this, new OkHttpStack());

在这里,OkHttpStack是一个我们自己通过扩展HurlStack创建的类,它将使用OkUrlFactory。这个OkUrlFactory将打开一个 URL 连接;这将在内部完成,无需重写createConnection方法:

/**
 * An HttpStack subclass
 * using OkHttp as transport layer.
 */
public class OkHttpStack extends HurlStack {

    private final OkUrlFactory mFactory;

    public OkHttpStack() {
        this(new OkHttpClient());
    }

    public OkHttpStack(OkHttpClient client) {
        if (client == null) {
            throw new NullPointerException("Null client.");
        }
        mFactory = new OkUrlFactory(client);
    }
}

JSON 和 Gson

作为一名 Android 开发者,迟早你将不得不处理 JSON 格式的网络请求。在某些情况下,你也可能会遇到 XML,这使得将其转换为对象更加繁琐。了解如何通过发送 JSON 格式的参数执行网络请求以及如何消费 JSON 格式的数据是非常重要的。

JSON 和 GSON 是两个不同的概念;我们需要了解它们之间的区别。JSON,即 JavaScript 对象表示法,是一种开放标准格式,它使用人类可读的文本来传输由属性-值对组成的数据对象。它主要用于在服务器和 Web 应用程序之间传输数据,作为 XML 的替代方案。下面是一个 JSON 文件的例子;如你所见,我们可以有不同的属性类型,我们还可以有嵌套的 JSON 结构:

{
  "firstName": "Antonio",
  "lastName": "Smith",
  "isDeveloper": true,
  "age": 25,
  "phoneNumbers": [
    {
      "type": "home",
      "number": "212 555-1234"
    },
    {
      "type": "office",
      "number": "646 555-4567"
    }
  ],
  "children": [],
  "spouse": null
}

下面是两个使用 JSON 格式发送带参数的网络请求的例子。这些例子涵盖了本章前面讨论过的 Volley 和 OkHttp:

//With Volley

public void post(String param1, String param2, String url) {

  Map<String, String> params = new HashMap<String, String>();
  params.put("param1",param1);
  params.put("param2",param2);

  JsonObjectRequest stringRequest = new  JsonObjectRequest(Request.Method.POST, url, new JSONObject(params),  new Response.Listener<JSONObject>() {

    @Override
    public void onResponse(JSONObject responseJSON) {

    }, new Response.ErrorListener() {

      @Override
      public void onErrorResponse(VolleyError error) {
      }
    });

    // Add the request to the RequestQueue.
    requestQueue.add(stringRequest);
  }

  //With OkHttp

  public static final MediaType JSON
  = MediaType.parse("application/json; charset=utf-8");

  String post(String url, String json) throws IOException {
    RequestBody body = RequestBody.create(JSON, json);
    Request request = new Request.Builder()
    .url(url)
    .post(body)
    .build();
    Response response = client.newCall(request).execute();
    return response.body().string();

  }

  //To create a JSONObject from a string

  JSONObject responseJSON = new JSONObject(String json);

GsonGoogle Gson)是一个开源的 Java 库,用于将 Java 对象序列化和反序列化为(或从)JSON。

如果我们从自定义服务器以 JSON 格式为我们的应用程序下载工作邀请,格式将如下所示:

{
  "title": "Senior Android developer",
  "description": "A developer is needed for…",
  "salary": "25.000 € per year",
  .
  .
  .
}

同样,我们不想手动创建一个新对象并从 JSON 中获取所有参数来设置;我们想要做的是从 JSON 创建一个JobOffer对象。这称为反序列化

要使用这个功能,我们需要在build.gradle中导入 GSON 库作为依赖:

dependencies {
compile 'com.google.code.gson:gson:2.2.4'
}

Gson 提供了fromJSONtoJSON方法来进行序列化和反序列化操作。fromJson方法接收要转换的 JSON 代码以及我们希望转换成的对象类作为输入。使用以下代码:

Gson gson = new Gson();
JobOffer offer = gson.fromJson(JSONString, JobOffer.class);

如果我们有一个列表而不是单个对象,这在请求数据时是典型场景,我们需要额外的步骤来获取类型:

Gson gson = new Gson();
Type listType = new TypeToken<List<JobOffer>>(){}.getType();
List<JobOffer> listOffers = gson.fromJson(JSONString, listType);

最后,如果我们希望类中的字段在反序列化时与 JSON 代码中的字段名称不同,可以使用如下注解:

import com.google.gson.annotations.SerializedName;

public class JobOffer extends ParseObject {

    @SerializedName("title")
    private String title;

    @SerializedName("description")
    private String desc;

    @SerializedName("salary")
    private String salary;

总结

在本章结束时,你应该能够自己在 Parse 中创建数据库并从应用程序中消费内容。你也应该拥有使用 Volley 和 OkHttp 进行网络请求的所有必要知识,特别是在执行网络请求和以 JSON 格式交换数据时。

在下一章中,我们将更详细地解释本章中使用的 HTTP 库的一些模式。例如,我们将了解回调是什么以及它遵循的模式,以及在 Android 中其他常用的软件模式。

第四章:并发与软件设计模式

作为开发者,你不仅要编写能工作的代码,而且要尽可能使用现有的解决方案,以便将来能更好地维护你的代码。如果其他开发者需要在你项目中工作,他们会很快理解你的做法。我们能够实现这一点,要归功于软件设计模式。

为了正确理解这些模式,我们需要了解 Android 中并发是如何工作的基本概述。我们将阐明 UI 线程是什么,并讨论在线程中延迟事件的不同机制。

我们将介绍 Android 中最常用的模式,这将帮助我们进一步理解 Android 功能和开发技术,并成为更好的开发者。

  • 并发

    • 处理器和线程

    • AsyncTask

    • Service

    • IntentService

    • Loader

  • Android 中的模式

    • 单例模式

    • 适配器和持有者模式

    • 观察者模式

Android 中的并发

如果你是一个 Android 用户,你可能对 ANR 消息有所了解。这可能不会让你立刻明白,所以请看以下图片:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当 UI 或主线程中的代码运行时间超过 5 秒,阻塞用户交互时,就会发生应用无响应ANR)。

在 Android 中,一个应用程序运行一个单一的线程,称为用户界面线程。我们将解释线程是什么,即使是没有编程背景的读者也能理解。我们可以将线程视为由 CPU 执行的指令或消息列。这些指令来自不同的地方;它们来自我们的应用程序以及操作系统。这个线程用于处理用户的响应、生命周期方法和系统回调。

CPU 逐个顺序处理消息;如果它很忙,消息将在队列中等待执行。因此,如果我们在应用程序中执行长时间的操作并向 CPU 发送许多消息,我们将不会让 UI 消息得到执行,这将导致用户感觉到手机无响应。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个问题的解决方案似乎很明显:如果一个线程不够用,我们可以使用多个。例如,如果我们发起一个网络请求,这个请求将在另一个线程中完成,并且当它结束时,会与主线程通信以显示所请求的数据。

只有主线程或 UI 线程可以访问 UI;因此,如果我们再另一个线程中进行任何后台计算,我们必须告诉主线程显示这些计算的结果,因为我们不能直接从那里做。

处理器和线程

我们之前描述的消息在一个称为MessageQueue的队列中运行,这个队列对于每个线程是唯一的。处理器可以发送消息到这个队列。当我们创建一个处理器时,它与创建它的线程的MessageQueue相关联。

处理器用于两种情况:

  • 向同一线程发送延迟消息

  • 向另一个线程发送消息

这就是为什么在我们的SplashActivity中,我们将使用以下代码:

new Handler().postDelayed(new Runnable() {
  @Override
  public void run() {

    Intent intent = new Intent(SplashActivity.this, MainActivity.class)

    startActivity(intent);
  }
},3000);

提示

当你创建一个新的Handler()方法时,确保导入Android.OS处理器。

在这里,我们使用了postDelayed(Runnable, time)方法来发送一个延迟的消息。在这种情况下,消息是一个可运行对象,代表可以执行的命令。

runOnUIThread()活动内部有一个方法允许我们向 UI 线程发送可运行对象时,你就不需要创建一个处理器来与它通信。当我们有活动的上下文并且想在 UI 上运行某些内容时,这非常有用,例如从在后台执行的任务中向 UI 发布更新。

如果我们查看 Android 中该方法的源代码,可以看到它只是简单地使用处理器在 UI 线程中发布可运行对象:

public final void runOnUiThread(Runnable action) {
  if (Thread.currentThread() != mUiThread) {
    mHandler.post(action);
  } else {
    action.run();
  }
}

通常,当我们想要在后台执行一个长时间的任务并希望管理并行线程的执行时,会手动创建线程。线程有一个run()方法,其中包含要执行的指令,并且必须在创建后启动以执行run()

Thread thread = new Thread(){

  @Override
  public void run() {
    super.run();
  }
};

thread.start();

创建线程和处理器以执行后台任务的缺点是它的手动处理,如果我们有多个任务,很容易导致应用程序的可读性变得极差。Android 还有其他机制来执行任务,如AsyncTask

介绍 AsyncTasks

这可能是你在初学阶段就已经了解的内容,但我们将从并发性的角度来审视它。Asynctask是基于线程和处理器的一个类,旨在提供一种在后台执行任务并更新 UI 的简便方法。

要使用AsyncTask,需要对其进行子类化,它有四个可以被重写的方法:onPreExecutedoInBackgroundonProgressUpdateonPostExecute

OnPreExecute方法是在后台执行任何工作之前被调用的;这意味着它仍然在 UI 线程中,用于在开始任务前初始化变量和进度。

doInBackground方法在后台线程中执行。在这里,你可以调用onProgressUpdate,它向 UI 线程发布一个更新,例如,通过增加ProgressBar的值来显示任务的进度。

最后一个方法onPostExecute是在后台任务完成且在 UI 线程中运行时被调用的。

以一个例子来说明:一个AsyncTask在后台需要x秒完成,并且每秒更新一次进度。进度条对象作为参数在构造函数中传递,秒数作为参数在execute方法中传递,并在doInBackground中获取。请注意,在以下代码中,<Integer,Integer,Void>类型分别指的是输入参数、进度更新和后执行的类型:

public class MyAsyncTask extends AsyncTask<Integer,Integer,Void> {

  ProgressBar pB;

  MyAsyncTask(ProgressBar pB) {
    this.pB = pB;
  }

  @Override
  protected void onPreExecute() {
    super.onPreExecute();
    pB.setProgress(0);
  }

  @Override
  protected void onProgressUpdate(Integer... values) {
    super.onProgressUpdate(values);
    pB.setProgress(values[0]);
  }

  @Override
  protected Void doInBackground(Integer... integers) {
    for (int i = 0; i < 10; i++){
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      onProgressUpdate(new Integer[]{i});
    }
    return null;
  }

  @Override
  protected void onPostExecute(Void o) {
    super.onPostExecute(o);
    Log.d("AsyncTask","Completed");
  }

}

创建了AsyncTask类后,以下是执行它的方法:

new MyAsyncTask( progressBar ).execute(new Integer[]{10});

如果我们同时执行多个 AsyncTask,从 Android 3.0 版本开始,默认情况下它们将会按顺序依次运行。如果我们希望它们并行运行,我们需要创建一个执行器,并通过 THREAD_POOL_EXECUTOR 参数调用 executeOnExecutor()

至于限制,我们应该提到 AsyncTask 必须始终从主线程执行,并且不能在同一个对象中两次调用 execute();因此,它们不能循环执行。

理解服务

当下载文件或执行任何需要任务完成时通知 UI 的短操作时,AsyncTask 是理想的。然而,在 Android 中,有时需要执行非常长时间的任务,这些任务可能不需要 UI 交互。例如,你可以有一个应用,它与服务器打开一个套接字(直接通道),用于收听广播应用中的音频流。

即使应用不在屏幕上显示,服务也会运行;它默认在后台运行,但使用主线程。因此,如果我们想要执行长时间的任务,我们需要在服务内创建一个线程。它必须在清单中声明,如果我们将其声明为公开,也可以从另一个应用中使用。

AsyncTask 相比,服务可以从任何线程触发;它们通过 onStartService() 方法触发,并通过 onStopService() 停止。

可选地,服务可以绑定到一个组件;一旦绑定组件,就会调用 onBind()。当绑定发生时,我们有一个接口供组件与服务交互。

一种服务类型 —— IntentService

IntentServiceservices 的一个子类,可以从一个意图触发。它创建一个线程并包含回调,以了解任务何时完成。

IntentService 的背后的想法是,如果你不需要并行运行任务,实现一个按顺序接收意图并完成任务后通知的服务更容易。

当我们调用 onStart 时,服务会持续运行;然而,IntentService 被创建后,只会在接收到意图并完成任务时,在短时间内运行。

以一个实际例子来说,我们可以考虑一个应用在屏幕不在显示时,在后台执行短任务的需求。比如一个新闻阅读应用,将新闻存储在您的设备上,以便您在没有网络的情况下也能离线阅读。这可能是来自一个每天发布文章的报纸的应用,允许用户在没有网络连接的情况下,如在飞机上或火车通勤时阅读文章。

这个想法是,当文章发布时,应用在后台运行时用户会收到推送通知。这个通知将触发一个下载文章的意图,这样用户下次打开应用时,无需任何额外操作,文章就已经在那里了。

下载文章是一个小而重复的任务,需要在应用在后台、在线程中运行时完成,无需并行处理,这正是IntentService的理想场景。

介绍加载器

为了结束并发部分,我们将快速概览Loader类。加载器的目的是更容易在活动中异步加载数据,因此在片段中也是如此。从 Android 3.0 开始,每个活动都有LoaderManager来管理在其中使用的加载器。在基于片段导航的应用程序中,即使在你切换片段时,也可以在活动级别执行后台操作。

加载器从数据源加载数据;当这个数据源发生变化时,它会自动刷新信息,这就是为什么加载器与数据库一起使用是完美的。例如,一旦我们将加载器连接到数据库,这个数据库就可以被修改,而加载器将捕获这些更改。这将允许我们立即刷新 UI,让用户看到这些变化。

在第八章《数据库 *和加载器》中,我们将实现CursorLoader来查询我们在MasteringAndroidApp中创建的数据库。

模式的重要性

当软件开发者需要开发具有特定功能特性的功能或组件时,通常可以用不同的方法完成;可以用不同的代码或不同的结构来完成。很可能同样的问题已经被其他开发者解决了无数次,以至于解决方案从具体实现中抽象出来,变成了一个模式。与其重新发明轮子,不如了解并实现这些模式。

在 Android 开发中,我们每天都在使用模式,即使我们没有意识到。大多数时候,我们使用的是 Android 内置的模式实现。例如,当我们想要执行按钮点击并设置OnClickListener时——换句话说,等待onClick()方法被调用——我们使用的是观察者模式的实现。如果我们创建一个弹出窗口AlertDialog,我们使用的是AlertDialog.Builder,它使用了构建器模式。这样的例子有很多,但我们希望的是能够将这些解决方案应用到我们自己的问题中。

有不同类型的模式分为四类,以下是在开发 Android 应用时我们可能会遇到的一些例子:

  • 创建型

    • 单例模式

    • 构建器模式

    • 工厂方法模式

  • 行为型

    • 观察者模式

    • 策略模式

    • 迭代器模式

  • 结构型

    • 适配器模式

    • 门面模式

    • 装饰器模式

  • 并发

    • 调度器

    • 读写锁

为了完成MasteringAndroidApp,我们需要实现前三组中的模式。关于第四组(并发),我们需要了解 Android 中的并发概念,但我们不会自己实现一个并发模式。

小贴士

模式通常由 UML 图表示。

根据维基百科(en.wikipedia.org/wiki/Class_diagram),“在软件工程中,统一建模语言(UML)中的类图是一种静态结构图,通过显示系统的类,它们的属性,操作(或方法)以及对象之间的关系来描述系统的结构”。

单例模式

软件设计模式中的单例模式(Singleton)限制了对象的创建,只允许有一个实例。其思想是全局访问这个单一对象。

该模式的实现是,如果之前没有创建对象,则创建对象,如果已创建,则返回现有实例。以下是 UML 图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在某些情况下,我们希望一个对象能够全局访问,并且希望它在我们的应用中是唯一的。例如,在使用 Volley 时,我们希望保持一个唯一的请求队列,以使所有请求都在同一个队列中,并且我们希望它能够全局访问,因为我们需要从任何片段或活动中添加请求。

这是一个单例实现的基本示例:

public class MySingleton {

    private static MySingleton sInstance;

    public static MySingleton getInstance(){
        if (sInstance == null) {
            sInstance = new MySingleton();
        }
        return sInstance;
    }
}

为了理解实现,请记住在 Java 中,静态变量与类相关联,而不是与对象相关。同样,静态方法可以在不创建类实例的情况下调用。

拥有一个静态方法意味着它可以在我们应用的任何地方被调用。我们可以调用MySingleton.getInstance(),它总是返回同一个实例。第一次调用时,它会创建并返回它;之后的调用,它会返回已创建的实例。

使用单例和测试框架有一个缺点;我们将在第十一章,安卓上的调试与测试中讨论这个问题。

应用类中的单例

我们可以将单例实现适配到 Android。考虑到在Application类中的onCreate方法在我们打开应用时只被调用一次,且Application对象不会被销毁,我们可以在应用中实现getInstance()方法。

应用这些更改后,我们的应用类将类似于以下结构:

public class MAApplication extends Application {

  private static MAApplication sInstance;

  @Override
  public void onCreate() {
    super.onCreate();

    sInstance = this;

    // Enable Local Datastore.
    Parse.enableLocalDatastore(this);

    ParseObject.registerSubclass(JobOffer.class);

    Parse.initialize(this, "KEy", "KEY");
  }

  private static MAApplication getInstance(){
    return sInstance;
  }
}

现在,我可以在应用的任何地方调用MAAplication.getInstance(),并在应用类中创建可以通过单例MAAplication对象全局访问的成员变量。例如,在 Volley 的情况下,我可以在OnCreate()中创建RequestQueue,然后随时从MAAplication对象中获取它。执行以下代码:

private RequestQueue mRequestQueue;

@Override
public void onCreate() {
  super.onCreate();

  sIntasnce = this;

  mRequestQueue = Volley.newRequestQueue(this);
  .
  .
  .
}

public RequestQueue getRequestQueue(){
  return mRequestQueue;
}

采用这种方法,我们有一个单例,即我们的Application类;其余的全局可访问对象都是成员变量。另一个选项是创建一个新的单例类来存储 Volley 请求队列,并为每个需要全局访问的对象创建一个新的请求单例。

提示

不要在Application类中使用此方法来持久化数据。例如,如果我们通过点击主页按钮进入后台,经过一段时间后,Android 可能需要内存,并会杀死该应用。因此,下次打开应用时,即使看起来像是返回到上一个实例,实际上会创建一个新的实例。如果你在onCreate中重新初始化所有变量,并且后来不改变它们的状态,这样做是没问题的。为了避免这个问题,请避免使用 setter 方法。

观察者模式

这种模式在 Android 中被广泛使用。我们讨论的大多数网络库都实现了这种模式,如果你是 Android 开发者,你肯定已经多次使用过它——我们需要实现它,甚至是为了检测按钮的点击。

观察者模式基于一个对象,即观察者,它注册其他对象来通知它们状态变化;在这里,监听状态变化的对象是观察者。这种模式可以用来创建一个发布/订阅系统:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以下是一个注册多个观察者的模式的实现:

public class MyObserved {

  public interface ObserverInterface{
    public void notifyListener();
  }

  List<ObserverInterface> observersList;

  public MyObserved(){
    observersList = new ArrayList<ObserverInterface>();
  }

  public void addObserver(ObserverInterface observer){
    observersList.add(observer);
  }

  public void removeObserver(ObserverInterface observer){
    observersList.remove(observer);
  }

  public void notifyAllObservers(){
    for (ObserverInterface observer : observersList){
      observer.notify();
    }
  }
}

public class MyObserver
implements MyObserved.ObserverInterface {

  @Override
  public void notify(){
    //Do something
  }
}

你会注意到,观察者可以是任何实现接口——ObserverInterface的对象。这个接口在观察对象中定义。

如果我们将这个与在 Android 中处理按钮点击的方式进行比较,我们会执行myButton.setOnClickListener(observer)。在这里,我们添加了一个等待点击的观察者;这个观察者实现了OnClick()方法,这是我们案例中通知的方法。

在 Volley 中,当我们创建一个网络请求时,我们必须指定两个作为参数的监听器:Response.ListenerResponse.ErrorListener。它们分别调用onResponse()onErrorResponse()。这是观察者模式的一个清晰实现。

我们将在第六章 CardView 和材料设计中实现观察者模式的一个变体,即发布/订阅模式的一个示例。

介绍适配器模式

适配器是在创建ListViewViewPager时在 Android 中使用的元素,但它也是一个著名的设计模式。我们将看看两者的定义及其关系。

一方面,适配器作为设计模式,是介于两个不兼容接口之间的桥梁。它允许两个不同的接口一起工作。这就像现实世界中的适配器,比如 SD 卡到 micro SD 卡的适配器,它允许两个不兼容的系统协同工作。如图所示,适配器被调用新的所需方法,但在内部,它调用来自被适配者的旧方法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

另一方面,来自android.widget.Adapter的适配器是一个对象,我们用它为列表的每一行或视图翻页的每一页创建视图。因此,它适配了一组数据和一组视图。

要实现一个适配器,我们必须扩展BaseAdapter并重写getView()getCount()方法。通过这两个方法,适配器将知道它必须创建多少个视图以及如何创建这些视图。

我们将在下一章更深入地探讨这个主题,同时会涉及到ListViews的使用,并讨论ViewHolder模式,这是在 Android 中处理适配器和列表时经常使用的一种特定模式。

总结

在本章结束时,你应该能够理解 Android 中的并发性以及与之相关的所有不同机制。你应该知道有一个主线程用于更新 UI,我们可以创建后台线程来执行其他任务。你还必须了解应用程序在后台(换句话说,不在屏幕上)执行任务与在后台线程中执行任务的区别。你也应该知道软件设计模式的重要性,并能够实现其中的一些。

在下一章中,我们将看看如何使用列表视图,我们将实现一个适配器,并发现一种新模式ViewHolder,它将是理解在 Android Lollipop 中引入的ListViewRecyclerView之间区别的关键。

第五章:列表和网格

在本章中,我们将处理列表和网格。几乎在市场上的每个应用中都可以找到列表或元素矩阵。了解如何在 Android 上显示元素列表是你在基础层面要学习的东西;然而,还有很多可以扩展和了解的内容。

了解我们可以在这里使用哪些模式很重要,如何回收视图,以及如何在同一个列表中用不同的视图显示不同类型的元素。

有了这个想法,我们将能够理解为什么RecyclerViewListView的继任者,并且我们将学习如何使用这个组件实现列表。因此,在本章中我们将介绍以下内容:

  • 从列表开始

    • ListView

    • 自定义适配器

    • 视图回收

    • 使用 ViewHolder 模式

  • 介绍 RecyclerView

    • 列表、网格或堆叠

    • 实现

  • OnItemClick

从列表开始

如果你听说过RecyclerView,你可能会想知道为什么我们要学习ListViewRecyclerView小部件是新的,它随着 Android Lollipop 一起出现,在显示项目列表时是一场革命;它可以垂直和水平显示,作为列表或网格,以及具有其他改进的精美动画。

尽管在某些场景下RecyclerView可能更高效、更灵活,但它需要额外的编码来实现相同的结果,因此仍然有使用ListView的理由。例如,RecyclerView中没有用于条目选择的onItemClickListener(),而且我们在点击条目时也没有视觉反馈。如果我们不需要定制和动画,例如对于一个简单的数据选择弹窗,这可能是一个只需选择一个国家的对话框。在这种情况下,使用ListView而不是RecyclerView是完全没问题的。

另一个从ListView开始的原因是RecyclerView解决了大多数在使用ListViews时遇到的问题。因此,通过从ListView开始并解决这些问题,我们将完全理解RecyclerView是如何工作的以及为什么这样实现。因此,我们将分别解释用于全局理解组件的模式。

下面是一个基本的AlertDialog示例,其目的是为了选择一个条目;在这种情况下,使用ListView是很有意义的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用内置视图的 ListViews

当你第一次实现ListView时,可能会觉得微不足道且简单;然而,当你花更多时间在 Android 上时,你会意识到它可能变得多么复杂。如果你有一个带有每行图片的大型元素列表,你很容易就能找到性能和内存问题。如果你尝试实现复杂的 UI,例如让同一个列表显示不同的条目,创建具有不同视图的不同行,或者甚至尝试在显示部分标题时组合某些条目,可能会很头疼。

让我们从实现列表的最简方式开始,使用前面讨论过的简单列表中使用的 Android 内置项目布局。为了显示列表,我们将它包含在AlertDialog中,当我们点击设置片段中的按钮时,会显示这个对话框。我会将按钮的文本设置为Lists Example

第一步是在settings_fragment.xml中创建按钮;创建后,我们可以为按钮设置点击监听器。现在,我们对软件模式有了更深入的了解,而不仅仅是以下这种方式设置点击监听器:

view.findViewById(R.id.settingsButtonListExample).setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    //Show the dialog here
  }
});

我们将以更有条理的方式来做这件事,特别是因为我们知道在设置屏幕上,将会有很多按钮,我们希望在同一地方处理所有点击事件。我们不会在方法调用内部创建onClickListener,而是通过将onClickListener设置为this,使Fragment实现OnClikListener。这里的this关键字指的是整个片段,因此片段将在onClick方法中监听点击,一旦Fragment实现了View.OnClickListener,这个方法是必须实现的。

OnClick()方法接收一个视图,即被点击的视图。如果我们将该视图的 ID 与按钮的 ID 进行比较,我们就会知道是按钮还是设置了clickListener的其他视图被点击了。

在定义类时只需键入implements View.OnClickListener,你就会被要求实现必填的方法:

/**
* Settings Fragment
*/
public class SettingsFragment extends Fragment implements View.OnClickListener {

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
  Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_settings, container, false);

    view.findViewById(R.id.settingsButtonListExample).setOnClickListener(this);

    view.findViewById(R.id.ViewX).setOnClickListener(this);

    view.findViewById(R.id.imageY).setOnClickListener(this);

    return view;
  }

  @Override
  public void onClick(View view) {
    switch (view.getId()){
      case (R.id.settingsButtonListExample) :
      showDialog();
      break;
      case (R.id.viewX) :
      //Example
      break;
      case (R.id.imageY) :
      //Example
      break;

      //...
    }
  }

  public void showListDialog(){
    //Show Dialog here
  }
}

你会注意到,我们还把显示列表对话框的逻辑移到了外部方法中,这样在onClick();中的结构易于阅读。

继续使用对话框,我们可以显示一个带有setAdapter()属性的AlertDialog,它会自动将内部项与ListView绑定。或者,我们可以为对话框创建一个带有ListView的视图,然后将适配器设置给该ListView

/**
*  Show a dialog with different options to choose from
*/
public void showListDialog(){

  AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());

  final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<String>(
  getActivity(),
  android.R.layout.select_dialog_singlechoice);
  arrayAdapter.add("Option 0");
  arrayAdapter.add("Option 1");
  arrayAdapter.add("Option 2");

  builder.setTitle("Choose an option");

  builder.setAdapter(arrayAdapter,
  new DialogInterface.OnClickListener() {
    @Override
    public void onClick(DialogInterface dialogInterface, int i) {
      Toast.makeText(getActivity(),"Option choosen "+i, Toast.LENGTH_SHORT).show();
      dialogInterface.dismiss();
    }
  });

  builder.show();
}

这个对话框将显示一条消息,指示点击的选项。我们使用了android.R.layout.select_dialog_singlechoice作为我们行的视图。

这些是列表内置布局的几个不同示例,它们将取决于我们应用程序的主题。例如,在 4.4 KitKat 和 5.0 Lollipop 中,对话框看起来是不同的,在android.R.layout.simple_list_item_1中,它看起来会是这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

android.R.layout.simple_list_item_2布局有两行,看起来会类似这样:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是一个android.R.layout.simpleListItemChecked的例子,我们可以将选择模式更改为多选或单选:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是android.R.layout.activityListItem,我们有一个图标和文本:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在创建布局时,我们可以访问这些内置的布局组件,以便进一步调整视图。这些组件的名称为android.resource.id.Text1android.resource.id.Text2android.resource.id.Icon等。

现在,我们知道了如何创建带有功能和视图的列表。是时候创建我们自己的适配器并手动实现功能和视图了。

创建自定义适配器

当你寻找工作时,除了查看职位信息,你还会向不同的软件公司或 IT 招聘公司提交你的简历,他们会为你找到一家公司。

在我们的联系人片段中,我们将创建一个按国家排序的列表,显示这些公司的联系人详细信息。将有两行不同的内容:一行用于国家头部,另一行用于公司详细信息。

我们可以在 Parse 数据库中创建另一个表,名为JobContact,包含以下字段:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们将从服务器请求工作联系人,并构建一个项目列表,该列表将发送到适配器以构建列表。在列表中,我们将发送两个不同的元素:公司和国家。我们可以生成一个项目列表并将这两个作为对象添加。我们的两个类将类似于以下内容:

@ParseClassName("JobContact")
public class JobContact extends ParseObject {

  public JobContact() {
    // A default constructor is required.
  }

  public String getName() {
    return getString("name");
  }

  public String getDescription() {
    return getString("description");
  }

  public String getCountry() {
    return getString("country");
  }

  public String getEmail() {
    return getString("email");
  }

}

public class Country {

  String countryCode;

  public Country(String countryCode) {
    this.countryCode = countryCode;
  }

}

一旦我们从www.parse.com按国家排序下载了信息,我们就可以通过遍历解析列表并检测到不同国家时添加一个国家头部来构建我们的项目列表。执行以下代码:

public void retrieveJobContacts(){
  ParseQuery<JobContact> query = ParseQuery.getQuery("JobContact");
  query.orderByAscending("country");
  query.findInBackground(new FindCallback<JobContact>() {
    @Override
    public void done(List<JobContact> jobContactsList, ParseException e) {
      mListItems = new ArrayList<Object>();
      String currentCountry = "";
      for (JobContact jobContact: jobContactsList) {
        if (!currentCountry.equals(jobContact.getCountry())){
          currentCountry = jobContact.getCountry();
          mListItems.add(new Country(currentCountry));
        }
        mListItems.add(jobContact);
      }
    }
  });
}

现在我们有了包含头部的列表,我们可以基于这个列表创建Adapter,它将在构造函数中作为参数发送。自定义Adapter的最佳方式是创建一个扩展BaseAdapter的子类。一旦我们这样做,我们将被要求实现以下方法:

public class JobContactsAdapter extends BaseAdapter {
  @Override
  public int getCount() {
    return 0;
  }

  @Override
  public Object getItem(int i) {
    return null;
  }

  @Override
  public long getItemId(int i) {
    return 0;
  }

  @Override
  public View getView(int i, View view, ViewGroup viewGroup) {
    return null;
  }
}

这些方法需要根据我们想要显示的数据来实现;例如,getCount()需要返回列表的大小。我们需要实现一个接收两个参数的构造函数:列表和上下文。上下文将是在getView()方法中膨胀列表所必需的。下面是没有实现getView()的适配器的外观:

public class JobContactsAdapter extends BaseAdapter {

  private List<Object> mItemsList;
  private Context mContext;

  public JobContactsAdapter(List<Object> list, Context context){
    mItemsList = list;
    mContext = context;
  }

  @Override
  public int getCount() {
    return mItemsList.size();
  }

  @Override
  public Object getItem(int i) {
    return mItemsList.get(i);
  }

  @Override
  public long getItemId(int i) {
    //Not needed
    return 0;
  }

  @Override
  public View getView(int i, View view, ViewGroup viewGroup) {
    return null;
  }
}

在我们的案例中,我们可以创建两个不同的视图;因此,除了必须实现的方法外,我们还需要实现两个额外的方法:

@Override
public int getItemViewType(int position) {
  return mItemsList.get(position) instanceof Country ? 0 : 1;
}

@Override
public int getViewTypeCount() {
  return 2;
}

getItemViewType方法将返回0如果元素是国家,或者1如果元素是公司。借助这个方法,我们可以实现getView()。如果是国家,我们膨胀row_job_country.xml,其中包含ImageViewTextView;如果是公司,我们膨胀row_job_contact.xml,其中包含三个文本视图:

@Override
public View getView(int i, View view, ViewGroup viewGroup) {

  View rowView = null;
  switch (getItemViewType(i)){

    case (0) :
    rowView = View.inflate(mContext, R.layout.row_job_country,null);
    Country country = (Country) mItemsList.get(i);
    ((TextView) rowView.findViewById(R.id.rowJobCountryTitle)).setText(country.getName());
    ((ImageView) rowView.findViewById(R.id.rowJobCountryImage)).setImageResource(country.getImageRes(mContext));
    break;

    case (1) :
    rowView = View.inflate(mContext, R.layout.row_job_contact,null);
    JobContact company = (JobContact) mItemsList.get(i);
    ((TextView) rowView.findViewById(R.id.rowJobContactName)).setText(company.getName());
    ((TextView) rowView.findViewById(R.id.rowJobContactEmail)).setText(company.getEmail());
    ((TextView) rowView.findViewById(R.id.rowJobContactDesc)).setText(company.getDescription());
  }

  return rowView;
}

最后,我们可以在contact_fragment.xml中创建ListView,并将适配器设置到这个列表。但是,我们将采取捷径,使用android.support.v4.ListFragment;这是一个已经通过ListView膨胀了视图并包含setListAdapter()方法的片段,该方法将适配器设置到内置的ListView中。从这段代码扩展,我们的ContactFragment类将类似于以下代码:

public class ContactFragment extends android.support.v4.app.ListFragment {

  List<Object> mListItems;

  public ContactFragment() {
    // Required empty public constructor
  }

  @Override
  public void onViewCreated(View view, Bundle bundle) {
    super.onViewCreated(view,bundle);
    retrieveJobContacts();
  }

  public void retrieveJobContacts(){
    ParseQuery<JobContact> query = ParseQuery.getQuery("JobContact");
    query.orderByAscending("country");
    query.findInBackground(new FindCallback<JobContact>() {
      @Override
      public void done(List<JobContact> jobContactsList, ParseException e) {
        mListItems = new ArrayList<Object>();
        String currentCountry = "";
        for (JobContact jobContact: jobContactsList) {
          if (!currentCountry.equals(jobContact.getCountry())){
            currentCountry = jobContact.getCountry();
            mListItems.add(new Country(currentCountry));
          }
          mListItems.add(jobContact);
        }
        setListAdapter(new JobContactsAdapter(mListItems,getActivity()));
      }
    });
  }
}

在视图创建后调用retrieveJobContacts()方法,我们实现了以下结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们显示的旗帜是drawable文件夹中的图片,其名称与国家代码相匹配,drawable/ “country_code” .png。我们可以通过将资源标识符设置为ImageView并在Country类内的以下方法中获取它们来显示它们:

public int getImageRes(Context ctx){
  return ctx.getResources().getIdentifier(countryCode, "drawable", ctx.getPackageName());
}

这是一个基本的ListView版本,包含两种不同类型的行。这个版本仍然远非完美;它的性能不佳,没有回收视图,并且每次创建行时都会查找小部件的 ID。我们将在下一节解释并解决这个问题。

视图回收

在使用ListView时,我们需要牢记行数是一个变量,即使我们尽可能快速地滚动,我们也希望列表能够流畅。幸运的是,Android 在这方面为我们提供了很大帮助。

当我们滚动ListView时,屏幕一侧不再可见的视图会被复用并在另一侧再次显示。这样,Android 节省了视图的膨胀;当它膨胀时,视图必须遍历 xml 节点,实例化每个组件。这种额外的计算可能是流畅列表和卡顿列表之间的区别。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

getView()方法接收一个待回收的视图作为参数,如果没有视图可以回收,则接收 null。

为了利用这种视图回收机制,我们需要停止每次都创建新视图,而是复用作为参数传入的视图。我们仍然需要在回收的视图中更改行内文本视图和小部件的值,因为它具有与其先前位置的初始值相对应的内容。在我们的示例中,有一个额外的复杂性;我们不能将国家的视图回收用于公司视图,因此我们只能回收相同视图类型的视图。然而,再次强调,Android 通过内部使用我们实现的getItemViewType方法为我们进行这个检查:

@Override
public View getView(int i, View view, ViewGroup viewGroup) {

  switch (getItemViewType(i)){

    case (0) :
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_country,null);
    }
    Country country = (Country) mItemsList.get(i);
    ((TextView) view.findViewById(R.id.rowJobCountryTitle)).setText(country.getName());
    ((ImageView) view.findViewById(R.id.rowJobCountryImage)).setImageResource(country.getImageRes(mContext));
    break;

    case (1) :
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_contact,null);
    }
    JobContact company = (JobContact) mItemsList.get(i);
    ((TextView) view.findViewById(R.id.rowJobContactName)).setText(company.getName());
    ((TextView) view.findViewById(R.id.rowJobContactEmail)).setText(company.getEmail());
    ((TextView) view.findViewById(R.id.rowJobContactDesc)).setText(company.getDescription());
  }

  return view;
}

应用 ViewHolder 模式

请注意,在getView()方法中,每次我们想要将文本设置到TextView时,都会使用findViewById()方法在行视图中搜索这个TextView;即使行被回收,我们仍然需要再次找到TextView以设置新值。

我们可以创建一个名为ViewHolder的类,它通过保存行内小部件搜索的计算来引用小部件。这个ViewHolder类将只包含对小部件的引用,我们可以通过setTag()方法在行与其ViewHolder类之间保持引用。View对象允许我们设置一个对象作为标签并在稍后检索它;我们可以通过指定这个标签的键来添加任意数量的标签:setTag(key)getTag(key)。如果没有指定键,我们可以保存和检索默认标签。

按照这种模式,在我们第一次创建视图时,我们将创建一个ViewHolder类并将其设置为视图的标签。如果视图已经创建并且我们正在回收利用它,我们只需简单地检索持有者。执行以下代码:

@Override
public View getView(int i, View view, ViewGroup viewGroup) {

  switch (getItemViewType(i)){

    case (0) :
    CountryViewHolder holderC;
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_country,null);
      holderC = new CountryViewHolder();
      holderC.name = (TextView) view.findViewById(R.id.rowJobCountryTitle);
      holderC.flag = (ImageView) view.findViewById(R.id.rowJobCountryImage);
      view.setTag(view);
    } else {
      holderC = (CountryViewHolder) view.getTag();
    }
    Country country = (Country) mItemsList.get(i);
    holderC.name.setText(country.getName());
    holderC.flag.setImageResource(country.getImageRes(mContext));
    break;
    case (1) :
    CompanyViewHolder holder;
    if (view == null){
      view = View.inflate(mContext, R.layout.row_job_contact,null);
      holder = new CompanyViewHolder();
      holder.name = (TextView) view.findViewById(R.id.rowJobContactName);
      holder.email = (TextView) view.findViewById(R.id.rowJobContactEmail);
      holder.desc = (TextView) view.findViewById(R.id.rowJobOfferDesc);
      view.setTag(holder);
    } else {
      holder = (CompanyViewHolder) view.getTag();
    }
    JobContact company = (JobContact) mItemsList.get(i);
    holder.name.setText(company.getName());
    holder.email.setText(company.getEmail());
    holder.desc.setText(company.getDescription());
  }

  return view;
}

private class CountryViewHolder{

  public TextView name;
  public ImageView flag;

}

private class CompanyViewHolder{

  public TextView name;
  public TextView email;
  public TextView desc;

}

为了简化这段代码,我们可以在每个持有者内部创建一个名为bindView()的方法;它将获取一个国家或公司对象并填充小部件:

CountryViewHolder holderC;
if (view == null){
  view = View.inflate(mContext, R.layout.row_job_country,null);
  holderC = new CountryViewHolder(view);
  view.setTag(view);
} else {
  holderC = (CountryViewHolder) view.getTag();
}
holderC.bindView((Country)mItemsList.get(i));
break;

private class CountryViewHolder{

  public TextView name;
  public ImageView flag;

  public CountryViewHolder(View view) {
    this.name = (TextView) view.findViewById(R.id.rowJobCountryTitle);
    this.flag = (ImageView) view.findViewById(R.id.rowJobCountryImage);
  }

  public void bindView(Country country){
    this.name.setText(country.getName());
    this.flag.setImageResource(country.getImageRes(mContext));
  }

}

我们现在将完成ListView性能改进列表。如果需要加载图像或长时间操作视图,我们需要在getView()中创建AsyncTask方法,以避免在滚动时进行繁重操作。例如,如果我们想在每一行显示从互联网下载的图像,我们会有一个LoadImageAsyncTask方法,我们将使用持有者和要下载图像的 URL 来执行它。当Asynctask方法完成后,它将拥有对持有者的引用,因此能够显示图像:

public View getView(int position, View convertView,
ViewGroup parent) {

  ...

  new LoadImageAsyncTask(list.get(position).getImageUrl, holder)
  .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);

  return convertView;
}

现在我们知道了所有不同的提高ListView性能的技术,我们准备引入RecyclerView。通过在实现中应用这些技术的大部分,我们将能够轻松识别它。

介绍 RecyclerView

RecyclerView在 Android 5.0 Lollipop 中引入,并被谷歌定义为比ListView更灵活和先进的版本。它基于一个类似于ListViewAdapter类,但强制使用ViewHolder类来提高性能和模块化,如我们前一部分所见。当我们把项目表示与组件分离,允许动画、项目装饰和布局管理器来完成工作时,灵活性就体现出来了。

RecyclerView通过RecyclerView.ItemAnimator处理添加和移除动画,我们可以通过子类化来自定义动画。如果你从数据源显示数据,或者数据发生变化,例如添加或移除项目,可以调用notifyItemInserted()notifyItemRemoved()来触发动画。

为了添加分隔线、分组项目或突出显示某个项目,我们可以使用RecyclerView.ItemDecoration

使用 ListView 的主要区别之一是使用布局管理器来定位项目。使用 ListView 时,我们知道我们的项目将始终垂直显示,如果我们想要网格,可以使用 GridView。布局管理器使我们的列表更加灵活,因为我们可以按需显示元素,甚至可以创建自己的布局管理器。

使用列表、网格或堆叠

默认情况下,我们有三个内置布局管理器:LinearLayoutManagerGridLayoutManagerStaggeredLayoutManager

LinearLayoutManager 以列表形式对齐显示项目,我们可以指定方向——垂直或水平。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

GridLayoutManager 以矩阵形式显示项目,我们可以指定列和行:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

StaggereGriddLayoutManager 以交错方式显示项目;这些项目可以有不同的宽度和高度,我们可以使用 setGapStrategy() 控制它们的显示方式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

实现 RecyclerView

继续使用 MasteringAndroidApp,我们将再次实现工作机会列表,移除 ParseQueryAdapter 并使用 RecyclerView 替代。我们仍然会从 Parse 查询数据,但这次,我们将做的是将项目列表保存在一个变量中,并使用它来构建 RecyclerView.Adapter,这将由 RecyclerView 使用。

RecyclerView 包含在 v7 支持库中;将依赖项添加到项目中的最佳方式是打开项目结构,点击依赖项标签,并搜索 RecyclerView。将展示如下截图所示的结果列表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这相当于在 build.gradle 依赖项中添加以下行:

dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  compile 'com.android.support:appcompat-v7:21.0.3'
  compile 'com.parse.bolts:bolts-android:1.+'
  compile fileTree(dir: 'libs', include: 'Parse-*.jar')
  compile 'com.mcxiaoke.volley:library-aar:1.0.1'
  compile 'com.android.support:recyclerview-v7:21.0.3'
}

添加完代码行后,我们将点击同步 Gradle 与项目文件来更新依赖项,并准备在 XML 中使用 RecyclerView

打开 fragment_list.xml 文件,将现有的 ListView 替换为 RecyclerView,如下所示:

<android.support.v7.widget.RecyclerView
  android:id="@+id/my_recycler_view"
  android:scrollbars="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent"/>

添加后如果没有错误,说明依赖项已正确添加。

下一步是创建适配器。这个适配器与我们为工作联系人创建的适配器略有不同;我们不会扩展 BaseAdapter,而是将扩展 RecyclerView.Adapter <RecyclerView.MyViewHolder>,在创建 JobOfferAdapter 适配器类后实现 ViewHolder 模式。但在扩展之前,我们必须创建一个内部类 MyViewHolder 继承 RecylcerView.ViewHolder。至此,我们有以下代码:

public class JobOffersAdapter  {

  public class MyViewHolder extends RecyclerView.ViewHolder{

    public TextView textViewName;
    public TextView textViewDescription;

    public  MyViewHolder(View v){
      super(v);
      textViewName = (TextView)v.findViewById(R.id.rowJobOfferTitle);
      textViewDescription = (TextView)v.findViewById(R.id.rowJobOfferDesc);
    }
  }
}

现在是扩展 JobOffersAdapter 类从 RecyclerView.Adapter<JobsOfferAdapter.MyViewHolder> 的时候了。系统将要求我们实现以下方法:

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  return null;
}

@Override
public void onBindViewHolder(MyViewHolder holder, int position) {

}

@Override
public int getItemCount() {
  return 0;
}

JobsContactsAdapter 中的方法相同,我们通过接收工作机会列表创建构造函数,并根据该列表实现适配器方法。

OnBindViewHolder 接收带有位置的持有者;我们需要做的就是获取列表中该位置的 job offer 并使用这些值更新持有者的文本视图。OnCreateViewHolder 将会填充视图;在这种情况下,我们只有一种类型,所以我们忽略 ViewType 参数。这里我们将展示一种替代的视图填充方法:使用作为参数传递的父级上下文。

最后,getItemCount 将返回工作机会的数量。完成上述所有任务后,我们新的适配器将使用以下代码创建:

public class JobOffersAdapter extends RecyclerView.Adapter<JobOffersAdapter.MyViewHolder>  {

  private  List<JobOffer> mOfferList;

  public JobOffersAdapter(List<JobOffer> offersList) {
    this.mOfferList = offersList;
  }

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

  @Override
  public void onBindViewHolder(MyViewHolder holder, int position) {
    holder.textViewName.setText(mOfferList.get(position).getTitle());
    holder.textViewDescription.setText(mOfferList.get(position).getDescription());
  }

  @Override
  public int getItemCount() {
    return mOfferList.size();
  }

  public class MyViewHolder extends RecyclerView.ViewHolder{

    public TextView textViewName;
    public TextView textViewDescription;

    public  MyViewHolder(View v){
      super(v);
      textViewName = (TextView)v.findViewById(R.id.rowJobOfferTitle);
      textViewDescription = (TextView)v.findViewById(R.id.rowJobOfferDesc);
    }
  }
}

这就是我们需要适配器完成的所有工作;现在,我们需要初始化 RecyclerView 并设置布局管理器以及适配器。适配器必须使用从 Parse 获取的对象列表实例化,就像我们在之前的适配器中获取工作联系人一样。首先,在 OnCreateView 中,我们将初始化 RecyclerView

public class ListFragment extends android.support.v4.app.Fragment {

  public List<JobOffer> mListItems;
  public RecyclerView mRecyclerView;

  public ListFragment() {
    // Required empty public constructor
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
  Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_list, container, false);

    mRecyclerView = (RecyclerView) view.findViewById(R.id.my_recycler_view);

    // use this setting to improve performance if you know that changes
    // in content do not change the layout size of the RecyclerView
    mRecyclerView.setHasFixedSize(true);

    // use a linear layout manager
    mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

    //Retrieve the list of offers
    retrieveJobOffers();

    return view;
  }

最后,我们将调用 retrieveOffers(),这是一个 async 操作。只有当从 Parse 获取结果后,我们才能创建适配器并将其设置到列表中:

public void retrieveJobOffers(){

  ParseQuery<JobOffer> query = ParseQuery.getQuery("JobOffer");
  query.findInBackground(new FindCallback<JobOffer>() {

    @Override
    public void done(List<JobOffer> jobOffersList, ParseException e) {
      mListItems = jobOffersList;
      JobOffersAdapter adapter = new JobOffersAdapter(mListItems);
      mRecyclerView.setAdapter(adapter);
    }

  });
}

检验我们工作成果的最佳方式是查看控制台是否有错误。如果一切运行正常,你应该能够看到如下截图所示的优惠列表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们故意添加了一个重复的工作机会,以便删除它以查看 RecyclerView 中默认包含的移除动画。我们将在 长按监听器 中实现这个功能。点击监听器仅用于在详情视图中打开优惠。我们将在下一节中看到如何操作。

点击 RecyclerView

ListView 中,检测项目点击相当简单;我们可以直接执行 ListView.setOnItemClickListersetOnItemLongClickListener 以处理长按点击。然而,这种实现方式在 RecyclerView 中并不那么快速,这种灵活性是有代价的。

这里有两种实现项目点击的方法:一种是通过创建一个实现 RecyclerView.OnItemTouchListener 的类,并调用 RecyclerView 的方法 addOnItemTouchListener,如下所示:

mrecyclerView.addOnItemTouchListener(new MyRecyclerItemClickListener(getActivity(), recyclerView, new MyRecyclerItemClickListener.OnItemClickListener() {

  @Override
  public void onItemClick(View view, int position){
    // ...
  }

  @Override
  public void onItemLongClick(View view, int position){
    // ...
  }
}));

public class MyRecyclerItemClickListener implements RecyclerView.OnItemTouchListener
{
  public static interface OnItemClickListener
  {
    public void onItemClick(View view, int position);
    public void onItemLongClick(View view, int position);
  }

  private OnItemClickListener mListener;
  private GestureDetector mGestureDetector;

  public MyRecyclerItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener)
  {
    mListener = listener;

    mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener()
    {
      @Override
      public boolean onSingleTapUp(MotionEvent e)
      {
        return true;
      }

      @Override
      public void onLongPress(MotionEvent e)
      {
        View child = recyclerView.findChildViewUnder(e.getX(), e.getY());

        if(child != null && mListener != null)
        {
          mListener.onItemLongClick(child, recyclerView.getChildPosition(child));
        }
      }
    });
  }

  @Override
  public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e)
  {
    View child = view.findChildViewUnder(e.getX(), e.getY());

    if(child != null && mListener != null && mGestureDetector.onTouchEvent(e))
    {
      mListener.onItemClick(child, view.getChildPosition(child));
    }

    return false;
  }

  @Override
  public void onTouchEvent(RecyclerView view, MotionEvent motionEvent){
    //Empty
  }
}
@Override
public void onRequestDisallowInterceptTouchEvent(RecyclerView view){
  //Empty
}

这种方法的好处在于,我们可以在每个活动或片段中定义 onClick 内应该执行的操作。点击逻辑不在视图上,一旦我们构建了这个组件,就可以在不同的应用中重复使用它。

第二种方法是设置和管理ViewHolder内部的点击事件。如果我们想要在应用程序的不同部分或在另一个应用程序中复用这个ViewHolder,这里就会出现问题,因为点击逻辑位于视图内部,我们可能希望在不同的片段或活动中有不同的逻辑。然而,这种方法使得在同一行内检测不同组件的点击变得更加容易。例如,如果我们在行内有一个小图标用于删除,另一个用于分享优惠,这种方法更有意义。这样,我们可以在每一行设置对工作名称的点击,并在整行上设置长按监听器:

public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener{

  public TextView textViewName;
  public TextView textViewDescription;

  public  MyViewHolder(View v){
    super(v);
    textViewName = (TextView)v.findViewById(R.id.rowJobOfferTitle);
    textViewDescription = (TextView)v.findViewById(R.id.rowJobOfferDesc);
    textViewName.setOnClickListener(this);
    v.setOnLongClickListener(this);
  }

  @Override
  public void onClick(View view) {
    switch (view.getId()){
      case R.id.rowJobOfferTitle :
      //Click
      break;
    }
  }

  @Override
  public boolean onLongClick(View view) {
    //Delete the element here
    return false;
  }
}

你应该能够判断在每种情况下应该使用哪种实现,并为其辩护。为了能够测试这一点,我们将在长按后删除一个元素(这里应该有一个确认对话框以避免误删,但我们将省略这部分内容)。元素将在本地被删除以显示移除动画。注意,我们没有从 Parse 中的源数据中删除这个元素;我们需要做的是从列表中删除元素并调用notifyItemRemoved来触发通知。我们可以通过getPosition()方法知道哪个条目被点击了。

@Override
public boolean onLongClick(View view) {
  mOfferList.remove(getPosition());
  notifyItemRemoved(getPosition());
  return true;
}

总结

在本章的最后,你将了解到如何实现一个适配器,如何在列表中处理不同类型的条目,以及我们如何以及为什么应用ViewHolder模式。你最早是在ListView类中学习这些内容,并手动实现了视图回收技术。因此,你将能够完全理解特性以及RecyclerView如何工作,以展示不同的条目显示方式和实现条目点击监听器。

在下一章,我们将探索在 Android 5.0 中与RecyclerView一起引入的一个新组件—CardView。我们将将其与RecyclerView结合使用,以获得灵活且专业外观的卡片列表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值