Xamarin-安卓移动应用开发第二版-全-
Xamarin 安卓移动应用开发第二版(全)
原文:
zh.annas-archive.org/md5/041d9a00d53204d93c498ec451f69c67译者:飞龙
前言
Xamarin 是基于 ECMA 标准的 .NET 框架的开源版本 Mono 的上层构建。Xamarin 为您提供了一套工具,包括其自己的 C# 编译器和公共语言运行时 (CLR)。Mono 框架源项目由位于旧金山的 Xamarin 公司(之前由 Novell 和最初由 Ximian 维护)维护。Mono 项目的首要目的是使 .NET 平台与 Linux 等其他非 Windows 平台兼容。
在 2011 年 4 月 Attachmate 收购 Novell 之后,Mono 平台的未来陷入了黑暗。几个月后,前 Novell 员工 Miguel de Icaza 创立了一家名为 Xamarin 的公司,并宣布将继续使用 Mono 平台进行商业软件开发。从那时起,Xamarin 赞助了 Mono 开源平台的发展,并为 iOS 和 Android 平台提供了商业 .NET 堆栈。iOS 的 .NET 被称为 MonoTouch 或 Xamarin.iOS,而 Android 的 .NET 被称为 Mono for Android 或 Xamarin.Android。
Xamarin 框架使开发者能够编写针对不同平台(包括 iOS、Android 和 Windows Phone)的跨平台移动应用程序。使用 Xamarin,您可以使用 C# 编程语言开发纯原生的 Android 或 iOS 应用程序,并在不同平台之间共享应用程序逻辑。这导致开发周期更快,开发者可以利用现有的 C# 和 .NET 编程技能,这有助于降低开发移动应用程序的学习曲线。
本书按照逻辑顺序组织,旨在帮助 C# 和 .NET 开发者从头开始构建 Xamarin.Android 应用程序。它解释了广泛使用的基调和高级 Android 概念,包括用户界面、数据存储、消费网络服务、地理位置、地图、摄像头以及构建和分发过程。
本书提供了对基本和高级 Xamarin.Android 概念的最全面解释;您可以通过实际的生活示例精确构建,以开发一个完整的工作应用。在本书的整个过程中,您将构建一个单一的应用程序,即 POIApp。通过这个应用程序,我们将涵盖所有 Xamarin.Android 的基础知识,以帮助您开始自己的应用程序开发。
本书涵盖的内容
第一章,Android 应用的解剖结构,概述了 Android 平台以及 Android 应用由什么组成。
第二章,Xamarin.Android 架构,概述了 Xamarin 平台并描述了 Mono 和 Android 运行时如何协同工作,以便开发者可以使用 C# 构建 Android 应用。
第三章,创建兴趣点应用,指导您如何设置开发环境,创建新的 Xamarin.Android 应用,并在 Android 模拟器中运行该应用。
第四章,添加列表视图,描述了 Android 的 AdapterView 架构,并指导你如何使用ListView和创建自定义适配器。本章还涵盖了如何从网络服务异步下载数据并在自定义ListView上显示响应。
第五章,添加详细信息视图,指导你如何创建详细信息视图以显示POIApp的详细信息,从列表视图中添加导航,并添加执行保存和删除网络服务操作的命令。
第六章,使你的应用方向感知,指导你如何检测设备方向并处理配置更改时的应用程序行为。
第七章,为多种屏幕尺寸设计,介绍了 Android 片段以及用于管理资源和支持多种屏幕尺寸(包括 Android 平板电脑)的不同技术。
第八章,创建数据存储机制,讨论了 Xamarin.Android 中可用的多种数据存储选项,并使用 SQLite 数据库引擎存储从网络服务获取的兴趣点列表,以便在设备离线时使列表可访问。
第九章,使 POIApp 具有位置感知功能,讨论了开发者为使他们的应用具有位置感知功能所拥有的各种选项,并介绍了如何添加逻辑以确定设备的地理位置、位置的地址以及在地图应用中显示位置。
第十章,添加相机应用集成,讨论了与设备相机集成的各种选项,以捕获POIApp的图片,并使用 HTTP 多部分表单上传将捕获的图片上传到网络服务。
第十一章,将应用发布到应用商店,讨论了分发 Android 应用的多种选项,并介绍了如何准备 Xamarin.Android 应用以进行分发。
你需要这本书的内容
本书中的所有示例都可以使用 Xamarin.Android 的 30 天试用版完成。这些示例是在 Mac OS X(Yosemite)、Xamarin Studio 5.9.3 和 Xamarin.Android 5.1.3(试用版)上开发的。只要它们是有效的 Xamarin 配置,任何后续版本都应该可以正常工作。你可以查看 Xamarin 网站以获取详细信息。
Xamarin.Android 也可以用于其他配置,包括 Windows 操作系统。在 Windows 操作系统上,你可以选择使用 Xamarin Studio 或 Visual Studio Xamarin 插件作为你选择的 IDE。使用与本书示例开发时不同的配置可能会导致书中描述的屏幕或步骤略有变化。
本书提供的示例使用了 Java JAX-RS 开发的 REST 网络服务。你可以在你的系统上部署网络服务代码以执行端到端测试,或者你可以使用代码包中提供的 Apiary 模拟数据 URL。要部署网络服务代码,你需要 MySQL 和 Apache Tomcat™应用程序服务器。
这本书面向的对象
这本书是为那些希望使用现有技能集开发 Android 应用的 C#和.NET 开发者编写的。本书包括使用 Xamarin 平台构建 Android 应用的逐步方法,无论你是经验丰富的移动开发者还是第一次尝试,都将非常有价值。
假设您在软件开发方面有一些经验,并且熟悉基本的面向对象开发概念和实践。理解 C#语法是必需的,并且对 C#有良好的实际知识是一个明显的优势,尽管这并不是严格必要的。
习惯用法
在本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称显示如下:“这些常量被放置在一个名为R.java的 Java 源文件中。”
代码块设置如下:
public override bool OnCreateOptionsMenu(IMenu menu)
{
MenuInflater.Inflate(Resource.Menu.POIListViewMenu, menu);
return base.OnCreateOptionsMenu(menu);
}
当我们希望将您的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:
public override Dialog OnCreateDialog (Bundle savedInstanceState)
{
<span class="strong"><strong> POIDetailFragment targetFragment = (POIDetailFragment) TargetFragment;</strong></span>
string poiName = Arguments.GetString(“name“);
新术语和重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,在文本中显示如下:“点击Step Over两次以查看执行进度。”
注意
警告或重要注意事项以如下框的形式出现。
小贴士
技巧和窍门如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要发送一般反馈,只需发送电子邮件至<<a class="email" href="mailto:feedback@packtpub.com">feedback@packtpub.com</a>>,并在邮件主题中提及本书的标题。
如果你在某个主题领域有专业知识,并且对撰写或为本书做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有一些东西可以帮助你从购买中获得最大收益。
下载示例代码
您可以从您的账户www.packtpub.com下载示例代码文件,适用于您购买的所有 Packt Publishing 图书。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/B04210_Graphics.pdf下载此文件。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<<a class="email" href="mailto:copyright@packtpub.com">copyright@packtpub.com</a>>与我们联系,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问答
如果您对本书的任何方面有问题,您可以通过<<a class="email" href="mailto:questions@packtpub.com">questions@packtpub.com</a>>联系我们,我们将尽力解决问题。
第一章:Android 应用程序的解剖结构
我们生活在一个技术正在发展和变得比以往任何时候都更容易获取的时代。移动计算平台的兴起将技术进化提升到了一个新的高度。手机和平板电脑正变得越来越智能,成为传统 PC 的替代品。在这个激烈竞争的移动计算世界中,从传统的 PC 制造商到小型初创公司都在竞相推出各种形态的设备。
在这本书中,我们将向您展示如何利用您现有的 C#技能来编写在 Android 设备上运行的应用程序。虽然这本书的大部分内容将专注于学习如何使用 C#和 Xamarin.Android 开发 Android 应用程序,但我们将从对 Android 的更一般性讨论开始。什么是 Android?Android 如何促进创建优秀的移动应用程序的任务?本章将通过提供以下主题的基础级理解来帮助您回答这些问题:
-
Android 平台概述
-
Android 平台版本和功能发布
-
Android 应用程序(构建块)
Android 平台
Android 平台是近年来开发的最强大、最进化、最先进的移动操作系统之一,它提供了各种服务和功能,帮助开发者构建丰富的移动应用程序。Android 是一个由 Google 开发和维护的开源操作系统。由于其开源性质,它拥有一个更大的开发者社区和设备制造商基础。
Android 操作系统最初主要是为低功耗计算手机设计的,但后来,其基础被扩展到各种形态,包括智能手机、平板电脑、Android TV 和可穿戴设备。
Android 版本
自 2007 年 11 月首次发布测试版以来,Android 操作系统经历了一系列频繁的更新。识别 Android 平台的版本可能会有些令人困惑;有一个版本号、API 级别和昵称,有时它们被交替使用。
版本号代表平台的发布。有时,创建一个新的发布是为了提供新的功能,而有时是为了修复错误。
API 级别是一个整数值,代表一组功能。随着 API 级别的提高,新的功能被提供给开发者。
以下表格按逆时间顺序列出了所有主要的 Android 平台发布版本:
平台版本
API 级别
发布日期
功能更新
5.1(棒棒糖)
22
03/09/2015
-
相比 Lollipop 的稳定性和性能改进。
-
添加了对多张 SIM 卡的支持。
5.0(棒棒糖)
21
11/12/2014
-
引入了新的运行时 ART,取代了 Dalvik。
-
完全的 UI 重设计和材料设计的介绍。
-
改进了锁屏通知。
-
更新了媒体 API,以实现更好的相机捕获和媒体播放。
-
添加了 Volta 项目以增加电池寿命。
4.4W, 4.4W.1, 4.4W.2 (奇巧可穿戴)
20
06/25/2014
-
Android Wear 平台智能手表的初始发布。
-
同一 Android 4.4 奇巧代码分支的分支,增加了可穿戴扩展。
4.4.x (奇巧)
19
10/31/2013
-
默认界面改为白色而不是蓝色。
-
添加了无线打印功能。
-
支持半透明导航栏和状态栏。
-
允许应用程序使用沉浸模式,在隐藏导航栏和状态栏的同时保持用户交互。
-
动作溢出菜单按钮始终可见,即使对于具有硬件菜单键的设备也是如此。
-
新的 UI 转换框架(属性动画)。
-
内置屏幕录制功能。
-
引入了新的实验性运行时环境 Android Runtime(ART)。
-
其他安全增强和错误修复。
4.3.x (蜂巢豆)
18
07/24/2013
-
支持新用户配置文件的受限访问模式。
-
引入了 Khronos OpenGL ES 3.0 的平台支持,为 2D 和 3D 图形渲染提供了更好的性能。
-
支持蓝牙低功耗。
-
优化了位置和传感器功能,包括硬件地理围栏优化。
-
许多安全增强、性能增强和错误修复。
4.2, 4.2.2 (蜂巢豆)
17
11/13/2012
-
改进了硬件加速的 2D 渲染器,使动画更平滑。
-
引入了名为 Daydream 的交互式屏保模式。
-
添加了演示窗口和外部显示支持。
-
完全支持从右到左(RTL)布局。
-
数量众多的错误修复。
4.1, 4.1.x (蜂巢豆)
16
07/09/2012
-
界面更快更平滑。
-
双向文本和其他语言支持。
-
引入了可展开的通知。
-
通过
ActivityOptions添加了新的活动启动动画。 -
改进了
WebView以提供更好的 HTML5 视频观看和画布动画。
4.0.3, 4.0.4 (冰淇淋三明治)
15
12/16/2011
-
错误修复和稳定性改进。
-
新的 API,包括联系人提供者的社交流 API。
-
相机性能更好。
-
屏幕旋转更平滑。
4.0, 4.0.1, 4.0.2 (冰淇淋三明治)
14
10/19/2011
-
使用新的 Roboto 字体家族进行了主要界面重整。
-
统一了 UI 框架,使其适用于手机、平板电脑等。
-
改进了锁屏,能够从锁屏访问应用程序。
-
引入了新的语音输入引擎。
-
改进了媒体流传输能力。
-
可以通过滑动从最近应用列表中关闭应用程序。
-
UI 的硬件加速。
3.2.x (蜂巢)
13
07/15/2011
-
为更广泛的平板电脑进行了优化。
-
添加了系统级同步功能,使 SD 卡文件可通过系统媒体存储对应用程序可用。
-
错误修复和其他小改进。
3.1 (蜂巢)
12
05/10/2011
-
USB 配件连接 API。
-
更新了各种 UI 框架。
-
可调整大小的主屏幕应用程序小部件。
-
支持每个连接的 Wi-Fi 接入点的 HTTP 代理。
-
高性能 Wi-Fi 锁定,在设备屏幕关闭时保持高性能 Wi-Fi 连接。
-
更新了动画框架类,增加了
ViewPropertyAnimator和背景颜色动画。
3.0(蜂巢)
11
02/22/2011
-
添加了新的用户界面,优化了平板电脑。
-
引入了一个操作栏,可在屏幕顶部访问上下文快速操作。
-
添加了片段,一个可以嵌入到活动中的自包含容器。它有自己的生命周期回调,用于设计平板电脑。
-
添加了系统级剪贴板。
-
改进了状态栏通知以支持更多内容丰富的通知。
-
添加了新的动画框架。
2.3.3、2.3.7(姜饼)
10
02/02/2011
- 改进和错误修复。
2.3、2.3.1、2.3.2(姜饼)
9
12/06/2010
-
更新了用户界面设计,以实现简洁和快速。
-
增加了支持近场通信(NFC)。
-
支持更大的屏幕尺寸和分辨率。
-
原生支持更多传感器,包括陀螺仪和气压计。
-
引入了并发垃圾回收,以改善应用程序响应性和更平滑的动画。
2.2.x(冻酸奶)
8
05/20/2010
-
改进了速度、内存和性能优化。
-
使用即时编译技术提高应用程序速度。
-
支持 Android 云到设备消息服务(C2DM)。
-
支持将应用程序安装到 SD 卡内存。
-
USB 网络共享和 Wi-Fi 热点功能。
-
错误修复和安全补丁更新。
2.1(clair)
7
01/12/2010
- 进行了小的 API 更改和错误修复。
2.0.1(clair)
6
12/03/2009
- 进行了小的 API 更改和错误修复。
2.0(甜甜圈)
5
10/26/2009
-
更新了相机功能,包括闪光灯、数码变焦、白平衡、色彩效果和场景模式。
-
优化硬件速度和用户界面全面升级。
-
MotionEvent类增强以跟踪多点触摸事件。 -
扩展账户同步功能,允许用户将多个账户添加到设备中。
1.6(甜甜圈)
4
09/15/2009
-
添加了多语言语音合成引擎,用于将文本转换为语音。
-
更新了对 CDMA/EVDO、802.1x、VPN 技术支持。
1.5(纸杯蛋糕)
3
04/27/2009
-
添加了带有文本预测和用户词典的第三方键盘支持。
-
支持 MPEG-4 和 3GP 格式的视频录制和播放。
-
在网页浏览器中添加了复制和粘贴功能。
-
动画屏幕转换。
-
支持主屏幕小部件。
1.1
2
02/09/2009
-
首次 Android 平台更新。
-
更新了地图应用程序。
-
在使用免提电话时,通话屏幕超时默认值现在更长。
-
添加了对从 MMS 保存附件的支持。
-
在布局中添加了对滚动公告的支持。
-
修复了各种错误。
1.0
1
09/23/2008
-
首个 Android 平台的商业版本。
-
包含了如 Android 市场、Gmail、相机、日历、联系人、Google Talk、地图、媒体播放器、图片、设置和浏览器等应用程序。
-
支持 Wi-Fi 和蓝牙。
-
支持即时通讯、短信和 MMS。
Android 平台由应用程序、操作系统、运行时、中间件、服务和库组成。以下图表提供了一个高级视图,说明了 Android 平台中每一层的组织方式,接下来的部分提供了每个主要组件的简要描述:

Linux 内核
Android 是一个基于 Linux 的操作系统,主要设计和定制用于移动设备,如智能手机和平板电脑。位于 Android 堆栈的底部,Linux 内核为设备硬件和 Android 软件层之间提供了接口。Android 的最新版本基于 Linux 内核版本 3.4 或更高(Android 4.0 之前的版本为 2.6)。
Linux 内核提供了一些核心系统服务,如内存管理、进程和任务管理、电源管理、网络堆栈以及各种设备驱动程序,以与设备硬件交互。
本地库
Android 提供了一套用 C/C++编写的本地库,提供各种类型的服务。这些库主要来自开源社区。
Android 运行时
Android 应用在Dalvik 虚拟机(Dalvik VM)中运行,类似于 Java VM,但已针对内存和处理能力有限的设备进行了优化。
Android 应用最初使用 Java 编译器编译成 Java 字节码,但它们有一个额外的编译步骤,使用称为即时编译(JIT)的过程将 Java 字节码转换为 Dalvik 字节码。JIT 编译器产生的输出适合在 Dalvik VM 中运行:

Dalvik 与 Android 核心库一起提供。这些库并不与特定的 Java 平台(JSE、JEE 或 JME)对齐,而更像是与 JSE 最接近的混合平台,但不包括以用户界面为中心的组件 AWT 和 Swing。Android 应用框架(AAF)提供了一种创建用户界面的替代方法。
虽然 Dalvik 运行得相当不错,但缺点是每次应用启动时都会有一个巨大的延迟。这就是新虚拟机 ART 出现的地方。
ART 是 Dalvik 的前身。它是 Android 4.4(KitKat)中引入的新应用运行时,作为新的实验性运行环境,并在 Android 5.0(Lollipop)中得到完全实现。这主要是为了性能和改进的应用启动时间。ART 与 Dalvik 的主要区别在于编译方法。虽然 Dalvik 使用 JIT,但 ART 采用了一种称为即时编译(AOT)的新概念。这意味着新应用在安装期间被编译,甚至在它们启动之前。要了解更多关于 ART 的信息,您可以参考source.android.com/devices/tech/dalvik/。
应用框架
应用程序框架是 Android 平台的一部分,这是开发者最熟悉的部分。它作为一组 Java 库提供,允许你构建用户界面,与设备功能(如相机或位置服务)交互,加载和处理各种类型的应用程序资源,并执行许多其他有用任务。以下是一些主要服务:
-
活动管理器: 该服务负责活动生命周期、状态管理和控制活动堆栈。稍后,在本章中,我们将了解更多关于活动生命周期的内容。
-
窗口管理器: 该服务负责管理屏幕的 z 顺序列表。每个活动都附加到一个窗口上,用于在屏幕上显示内容,该窗口由
WindowManager控制。 -
内容提供者: 这提供了一个接口,用于在应用程序之间发布和共享数据。
-
视图系统: 这提供了一套 UI 控件来构建应用程序用户界面。
-
通知管理器: 该服务管理应用程序警报和通知。
-
资源管理器: 该服务提供对资源(如用户界面布局、字符串、颜色、尺寸等)的访问。
-
包管理器: 这包含设备上所有已安装应用程序的元数据。
-
电话管理器: 这为应用程序提供有关设备上可用的电话服务的信息,例如状态和订阅者信息。
-
位置管理器: 这提供了对系统位置服务的访问。
应用程序层
在堆栈的顶部是谦逊的应用程序,这是实际向用户交付价值的组件。Android 自带一系列提供基本功能的应用程序,例如管理联系人、使用电话、检查电子邮件和浏览网页。Android 成功的关键是庞大的第三方应用程序库,用户可以通过安装这些应用程序来完成各种事情,例如直播体育赛事、编辑手机上捕获的电影、通过他们最喜欢的社交媒体网站与朋友互动,等等。
Android 应用程序的构建块
现在,让我们花些时间讨论应用程序——那些我们编写并提供给用户价值的东西。Android 应用程序由各种类型的类和资源组成。以下各节描述了应用程序可以由哪些不同的构建块组成。
Android 包(.apk)
应用程序以 Android 包格式交付安装。Android 包是在编译 Android 应用程序的结果中创建的,是一个具有.apk扩展名的存档文件。
Android 包包含运行单个应用程序所需的全部代码和支持文件,包括以下内容:
-
Dalvik 可执行文件(.dex 文件)
-
资源
-
原生库
-
应用程序清单
安卓包可以直接通过电子邮件、URL 或内存卡安装。它们也可以通过应用商店(如 Google Play)间接安装。
应用程序清单
所有安卓应用程序都有一个清单文件(AndroidManifest.xml),它告诉安卓平台运行应用程序所需知道的一切,包括以下内容:
-
应用程序所需的最低 API 级别
-
应用程序使用或要求的硬件/软件功能
-
应用程序所需的权限,如位置或摄像头
-
当应用程序启动时,要开始的初始屏幕(安卓活动)
-
在外部存储中安装应用程序的能力
-
应用程序所需的库(除 AAF 外)等
活动
安卓应用中最基本的组成部分之一是活动。活动代表一个用户界面屏幕,用户可以通过它与应用程序进行交互。一个应用程序由许多活动组成。例如,电话簿应用程序可以包含多个活动,代表不同的功能,如列出联系人、添加联系人、捕捉联系人照片等。
用户通过一个或多个视图与活动交互,这些视图将在本章后面进行描述。如果您熟悉模型-视图-控制器(MVC)模式,您会注意到活动扮演了控制器的角色。
活动的生命周期
活动具有一个定义良好的生命周期,可以用状态、转换和事件来描述。以下图表提供了活动生命周期的图形视图:

上图中描述的状态是派生的,这意味着活动上没有明确标识这些状态的State变量,但状态是隐含的,并且对讨论很有用。以下表格描述了活动基于其状态的行为:
状态
描述
运行中
活动已被创建和初始化,对用户可见并可交互。
暂停
活动视图被另一个活动部分遮挡。
已停止
活动对用户不再可见。活动尚未被销毁,状态被保留,但被放置在后台,不允许进行任何处理。
活动的各种事件
在状态之间的转换过程中,会在活动上调用一系列事件。这些事件为开发者提供了各种类型处理的平台。以下表格描述了不同的回调事件以及通常在每个回调期间在应用程序中执行的处理:
事件
被调用
典型处理
onCreate
当活动被创建时,通常是从用户选择启动应用程序开始的
-
这创建了视图
-
这初始化了变量
-
这分配了长期资源
onStart
在onCreate之后,在活动对用户可见之前
- 这会分配资源
onResume
在活动准备好与用户交互以及onStart回调之后立即
-
这会初始化用于查看的 UI 小部件
-
这会启动动画或视频
-
这会开始监听 GPS 更新
onPause
当一个活动的视图部分被遮挡且不是输入焦点时
-
这会提交未保存的更新
-
这会暂停动画或视频
-
这会停止监听 GPS 更新
onStop
当活动的视图对用户不再可见时
- 这会释放资源
onRestart
活动正在回到前台,通常是因为用户选择了返回按钮
- 这会分配资源
onDestroy
在活动被销毁之前
- 这会清理活动可能分配的资源
对于开发者和 Android 新手来说,框架处理设备方向变化的方式可能并不明显。默认情况下,当设备方向从纵向变为横向时,Android 会销毁并重新创建现有活动,以确保使用最适合当前设备方向的布局。
如果需要,此行为可以被覆盖,活动可以被保留。我们将在第六章“使您的应用方向感知”中讨论处理与此主题相关的状态和其他处理问题的特殊考虑。
片段
片段是一个可重用的用户界面组件,自 Android 3.0(API 级别 11)以来引入,主要用于构建适用于不同屏幕尺寸的动态和模块化用户界面。片段始终嵌入在活动中,并且像任何其他视图一样,它存在于视图层次结构中的ViewGroup(ViewGroups将在本章后面详细解释)中。像活动一样,片段定义自己的布局并有自己的生命周期回调。在设计支持多种形态的应用程序时,可以通过重用片段来优化基于可用屏幕空间的用户体验。
让我们通过以下示例来考察如何使用片段来开发模块化用户界面。
下图展示了新闻阅读器应用的线框图,该应用旨在在智能手机和平板设备上运行。由于平板电脑有更多的屏幕空间,新闻列表和详情以分割视图的形式在一个活动中呈现,而手机则使用两个不同的活动来处理此功能:

安卓智能手机使用两个活动:包含FragmentA的ActivityA用于显示新闻列表,包含FragmentB的ActivityB用于显示所选新闻的详情。在平板电脑上,我们有一个包含FragmentA和FragmentB的单个活动ActivityA。
正如你所看到的,FragmentA和FragmentB是相同的实现,并在不同的布局配置中被重用来在手机和平板电脑上提供不同的用户体验。
片段生命周期
与活动生命周期不同,理解片段生命周期可能有点棘手。在下一节中,我们将更深入地探讨片段的行为及其生命周期方法。
Android 片段有自己的生命周期方法,这与活动非常相似。它包含所有活动生命周期方法,并提供了额外的回调方法。片段始终嵌入在活动中,因此其回调直接受到宿主活动生命周期的直接影响。例如,如果宿主活动收到onStop(),所有附加的片段也会收到onStop()回调。
下面的图提供了片段生命周期的图形视图:

让我们看看每个被调用的片段生命周期事件:
-
onInflate: 仅当我们直接在活动布局中使用fragment标签定义片段,并且活动的内容视图正在填充时(通常在活动上调用setContentView()时),会调用此事件。此方法传递包含所有从fragment标签传递的片段属性的AttributeSet。这些属性可以存储以供以后使用。在这个阶段,片段甚至还没有与活动关联,因此我们无法执行任何与用户界面相关的任务。 -
onAttach: 当片段实例与活动关联时,会调用此方法。 -
onCreate: 在onAttach之后和onCreateView之前调用此事件;当片段实例被创建或重新创建时。在这个时候,持有此片段的基活动正处于创建过程中。在这个时候,你可以使用后台线程为片段获取数据。 -
onCreateView: 在这个阶段,片段实例化其用户界面并加载它包含的视图对象层次结构。此方法传递三个参数:LayoutInflater、ViewGroup和bundle。LayoutInflater参数可用于为片段填充任何布局。bundle指定片段是全新创建还是重新创建。如果是从之前的保存状态重新创建,则该 bundle 将非空。 -
onActivityCreated: 当包含片段的活动被创建,并且片段的视图层次结构被实例化时,会调用此方法。在这个时候,你可以使用findViewById()方法通过 ID 访问视图,并在它对用户可见之前进行任何更改。 -
onStart: 此方法与活动的onStart()回调相关联,当片段对用户可见时被调用。在这个时候,片段是可见的,但尚未准备好与用户交互。 -
onResume: 在片段准备好开始与用户交互之前调用此方法。在这个时候,片段被认为是正在运行,用户可以自由地对应用执行任何操作。 -
onPause: 此方法与活动的onPause()回调相关联,并在片段被移出前台时调用。 -
onStop: 此方法与活动的onStop()回调相关联,并在片段不可见时被调用。 -
onDestroyView: 此方法通知片段由onCreateView()创建的视图现在已从片段中分离。此回调在onStop()之后和onDestroy()方法之前被调用。 -
onDestroy: 当片段不再使用时,会调用此方法。此方法在onStop()之后和onDetach()之前被调用。 -
onDetach: 在onDestroy()之后调用此方法,并且当片段不再附加到活动上时。
服务
服务是运行在后台的应用组件,用于执行长时间运行的操作,且没有直接访问用户界面的权限。一个典型的长时间运行的任务可以是定期从互联网下载数据、在数据库中持久化多个记录、执行文件 I/O、获取电话联系人列表等。这些长时间运行的任务可以通过服务实现,以提供平滑的用户体验,允许用户在后台处理长时间运行的任务时与其他活动交互。
内容提供者
内容提供者管理对中央数据存储库(如联系人)的访问。它为你提供了一个标准接口,其他应用程序可以通过该接口访问和管理数据存储库。
广播接收器
广播接收器是响应系统级广播执行某些类型处理的组件。广播通常由系统为低电量、拍照或开启蓝牙等事件启动。应用程序也可以选择发送广播;当数据(如联系人)更新时,内容提供者可能会发送广播。虽然广播接收器没有用户界面,但它们可能会间接导致状态更新。
视图和 ViewGroup
在 Android 应用中,你所看到的一切都是视图;按钮、标签、文本框和单选按钮都是视图的例子。视图通过各种类型的 ViewGroup 组织成层次结构。ViewGroup 是一种特殊的视图,用于在屏幕上排列(布局)其他视图。
声明式与程序式视图创建
视图和 ViewGroup 可以使用两种不同的方法创建:程序式或声明式。当使用程序式方法时,开发者通过 API 调用创建和定位屏幕上的每个单独的视图。当使用声明式方法时,开发者创建 XML 布局文件,指定视图应该如何排列。声明式方法具有以下优点:
-
它提供了更好的应用视觉设计和处理逻辑之间的分离
-
它允许创建多个布局以支持单个代码库支持多个设备或设备配置
-
开发工具,如 Android Studio 和 Eclipse 及 Xamarin Studio 的 Android 插件,Android 设计师,允许您在构建用户界面时查看它,无需在每次更改后编译和执行应用程序
虽然大多数开发者更喜欢声明性方法创建 View;在实践中,通常需要程序性和声明性方法的某种组合。
用户界面小部件
Android 提供了一套全面的用户界面小部件,可用于构建丰富的用户体验。所有这些小部件都是 View 的子类型,可以使用各种类型的 ViewGroups 组织成复杂的布局。所有用户界面小部件都可以在应用程序框架中的android.widget包中找到。
以下截图展示了 Android 中的一些基本小部件:

常见布局
应用程序框架有几个ViewGroup的子类,每个子类都提供了一种独特且有用的组织内容的方式:

以下图表示例展示了 Android 中的一些常见布局管理器。布局管理器是作为容器来托管子视图或布局的ViewGroup类。每个标准布局管理器都提供了一种特定的策略来管理其子的大小和位置。例如,LinearLayout类将其子元素水平或垂直排列,一个视图紧邻另一个视图。
以下表格列出了 Android 中可用的不同类型的布局管理器:
布局
描述
场景
线性布局
此布局将子元素组织成单个水平或垂直行,并在需要时创建滚动条。
当小部件的位置水平或垂直流动时使用此布局。
相对布局
此布局将子对象相对于彼此或相对于父对象进行组织。
当小部件的位置最好描述为相对于另一个小部件(在左侧)或父级的边界区域(右侧,居中)时使用此布局。
表格布局
此组织将其子元素排列成行和列。
当小部件的位置自然适合于行和列时使用此布局。当需要多个列的输入和标签时,这非常出色。
对于复杂的布局场景,Android 允许嵌套布局。深度嵌套的布局可能会影响性能,如果可能的话应避免。
小贴士
下载示例代码
您可以从www.packtpub.com下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了此书,您可以访问www.packtpub.com/support并注册以将文件直接通过电子邮件发送给您。
适配器布局
对于由动态数据源驱动的布局,应用程序框架有一组从AdapterView派生的类:

上述图示描述了两种最常见的适配器布局:
-
ListView:这将从数据源组织内容到一个可滚动的单列列表中
-
GridView:这将从数据源组织内容到一个列和行的网格中
XML 布局文件
要使用声明性方法创建 UI,Android 提供了一个带有标签的 XML 词汇表,这些标签定义了可以组成视图的各种类型元素。Android XML 布局文件背后的概念与 HTML 标签用于定义网页或 Microsoft 的 XAML 标签用于定义Windows Presentation Foundation(WPF)用户界面的方式非常相似。以下示例展示了使用线性布局的简单视图,其中包含搜索输入字段和搜索按钮:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk
/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:text="Enter Search Criteria"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/searchCriteriaTextView" />
<Button
android:text="Search"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/searchButton" />
</LinearLayout>
元素和属性名称
已经注意将 XML 词汇表中的元素和属性名称与应用程序框架中的类和方法名称对齐。在前面的示例中,元素名称LinearLayout、TextView和Button对应于应用程序框架中的类名称。同样,在Button元素中,android:text属性对应于类的setText()设置器。
视图和布局标识符
每个视图都可以与一个唯一的整数 ID 相关联,并可用于在应用程序代码中引用视图。在 XML 文件中,ID 被指定为一个用户友好的文本名称。例如,考虑以下代码行:
android:id="@+id/searchButton"
在此示例中,@运算符告诉解析器将字符串的其余部分视为 ID 资源;+符号告诉解析器这是一个新的资源名称,应该添加到资源文件R.java中。资源文件定义了可以用于引用资源的整数常量。
在活动中使用 XML 布局
XML 布局可以在应用程序运行时轻松加载。这项任务通常是在活动的onCreate()方法中使用setContentView()方法来执行的。例如,考虑以下代码行:
setContentView(R.layout.main);
意图
意图是消息,可以发送到 Android 应用程序中的各种类型组件,以请求执行某种类型的操作。意图可以用于完成以下任何一项:
-
启动一个活动,并可选择接收结果
-
启动或停止服务
-
通知组件条件,例如低电量或时区更改
-
从另一个应用程序请求操作,例如请求地图应用程序显示位置或请求相机应用程序拍照并保存
资源
创建 Android 应用程序不仅仅是编写代码。一个丰富的移动应用程序需要诸如图像、音频文件、动画、菜单和样式等元素,仅举几例。应用程序框架提供了可以用于加载和利用各种类型资源的 API。
R.java 文件
资源通常通过在应用程序中使用一个整数常量来引用,该常量在资源被添加到项目并编译时自动分配。这些常量放置在一个名为R.java的 Java 源文件中。以下示例展示了来自一个简单应用程序的R.java类:
public final class R {
public static final class attr {
}
public static final class drawable {
public static final int icon=0x7f020000;
}
public static final class id {
public static final int myButton=0x7f050000;
public static final int searchButton=0x7f050002;
public static final int searchCriteriaTextView=0x7f050001;
}
public static final class layout {
public static final int main=0x7f030000;
public static final int search=0x7f030001;
}
public static final class string {
public static final int app_name=0x7f040001;
public static final int hello=0x7f040000;
}
}
摘要
在本章中,我们提供了一个简洁且充分的介绍,包括 Android 平台及其 Android 应用程序构建块。我们还了解了 Android 平台是如何随着每个平台版本的发布而不断进化,并增加了丰富的功能。
在下一章中,我们将关注 Xamarin.Android 及其提供的功能,这些功能允许使用.NET 和 C#进行 Android 开发。
第二章:Xamarin.Android 架构
现在我们已经了解了 Android 平台,让我们来谈谈 Xamarin。在本章中,我们将探讨 Xamarin.Android 的架构以及它是如何促进使用 C#和.NET 开发 Android 应用程序的。本章涵盖了以下主题:
-
采用 Xamarin.Android 的优缺点
-
什么是 Mono?
-
Mono 和 Android 运行时并排(对等对象)
-
Xamarin.Android 绑定库
-
开发 IDE 选择
开始使用 Xamarin
Xamarin 是一家位于加利福尼亚州旧金山的软件公司,它提供商业软件开发工具,利用 Mono 开源项目,以便你能够使用 C#和.NET 框架为 Android、iOS 和 Mac 开发应用程序。
Xamarin 通过一系列产品简化了跨平台方式下的移动应用程序开发。以下是一些 Xamarin 提供的产品:
-
Xamarin 平台: Xamarin 使用.NET 框架的开源实现,称为 Mono。Xamarin 框架实现包括其自己的用 C#编写的编译器和.NET 库。Xamarin 平台包括以下产品:
-
Xamarin.iOS: 这也被称为 MonoTouch。这是用于使用 C#和.NET 构建原生 iOS 应用程序的。
-
Xamarin.Android: 这也被称为 Mono for Android 或正式称为 MonoDroid。这是用于使用 C#和.NET 构建原生 Android 应用程序的。
-
Xamarin.Forms: 在 Xamarin.Android 和 Xamarin.iOS 中,我们无法构建纯跨平台应用程序。应用程序中平台无关的部分可以隔离并跨平台重用;然而,你仍然需要编写特定于平台的代码来设计应用程序的界面。这就是 Xamarin.Forms 的用武之地。Xamarin.Forms 允许你编写可以编译为 iPhone、Android 和 Windows Phone 的用户界面代码。
-
Xamarin.Mac: 这也被称为 Mac 上的 Mono。Xamarin.Mac 允许你使用 C#和.NET 开发完全本地的 Mac 应用程序。
-
Xamarin.Windows: 这也被称为 Windows 上的 Mono。Xamarin.Windows 允许你使用 C#和.NET 开发完全的 Windows 应用程序。
我们将在本书中使用 Xamarin.Android 来开发 Android 应用程序。
-
-
开发 IDE:除了框架之外,它还带来了所需的开发 IDE,例如 Xamarin Studio 和 Visual Studio 插件。Xamarin Studio 是一个完全集成的 IDE,与 Xamarin 包一起使用非常方便。Xamarin Studio 适用于 Windows 和 Mac 操作系统。Xamarin Studio 包括一些丰富的功能,包括代码补全、调试界面、Android 布局构建器和与 Xcode Interface Builder 的集成,用于 iOS 应用设计。然而,如果你熟悉 Visual Studio,你可以通过 Visual Studio 支持插件继续从前面提到的几乎所有好处中获益。
-
Xamarin Test Cloud:移动应用测试相当具有挑战性,因为我们必须考虑各种形态、设备密度、连接类型和不同的操作系统版本。在所有目标设备上测试您的应用程序几乎是不可能的。Xamarin Test Cloud 是解决这个问题的答案。Xamarin Test Cloud 使得在来自世界各地的真实设备集合上测试用任何语言编写的移动应用成为可能。您可以使用 Xamarin 测试框架编写测试脚本,并通过 CI 系统自动化您的应用测试。
为什么选择 Xamarin.Android?
在我们深入探讨 Xamarin.Android 的架构之前,让我们首先讨论一下为什么 Xamarin.Android 是我们的选择。像任何重大的平台决策一样,没有一种解决方案适合所有人,有许多事情需要考虑。以下两个列表列出了使用 Xamarin.Android 的一些关键优点和缺点。
使用 Xamarin.Android 的好处
-
它利用现有的 C#和.NET 技能:开发者投入了大量的时间和精力来掌握 C#语言的众多特性和.NET 框架的有效使用。是的,Java 和所有面向对象的语言有很多相似之处,但从 C#和.NET 的熟练程度过渡到 Java,确实存在一定的成本。那些在 C#和.NET 上投入了大量资金并需要开发 Android 应用的个人和团体,至少应该考虑 Xamarin.Android。
-
它可以在跨平台开发中重用:虽然 Xamarin 不会让您构建一个可以部署到 Android、iOS 和 Windows 的单个应用程序,但它确实赋予了您在所有这些平台上重用代码库大部分的能力。一般来说,用户界面代码和处理设备功能的代码通常是针对每个平台编写的,而像服务客户端逻辑、客户端验证、数据缓存和客户端数据存储这样的东西则可能被跨多个平台共享。这可以节省大量的时间和成本。
使用 Xamarin.Android 的缺点
-
许可要求:Xamarin.Android 以及 Xamarin.iOS 和 Xamarin.Mac 都是商业工具,必须获得许可,因此存在实际的入门成本。请访问 Xamarin 网站,了解当前定价。
-
等待更新:Android 平台的新版本与相应的 Xamarin.Android 版本之间存在一些滞后时间。然而,Xamarin 正在努力实现针对 Android 和 iOS 新版本的无缝支持。
-
分发大小:Xamarin.Android 应用程序必须与一些运行时库一起分发。我们将在最后一章讨论实际大小以及最小化分发大小的策略。
虽然缺点列表可能看起来很庞大;但在大多数情况下,每个缺点的影响都可以最小化。如果您是一个高度重视这些益处的团队或个人,您应该认真考虑使用 Xamarin.Android。
什么是 Mono?
Mono 是一个开源的跨平台 C# 编译器和 公共语言运行时(CLR)的实现,它与 Microsoft .NET 兼容。Mono CLR 已被移植到许多平台,包括 Android、大多数 Linux 发行版、BSD、OS X、Windows、Solaris,甚至一些游戏机,如 Wii 和 Xbox 360。此外,Mono 还提供了一种静态编译器,允许应用程序为 iOS 和 PS3 等环境编译。
Mono for Android 以原生方式运行,并提供几乎所有典型原生 Android 应用程序可以拥有的功能。它允许开发者在不牺牲主要性能的情况下重用更大比例的代码。
Mono 和 Dalvik 并行
如您从第一章,“Android 应用的解剖”中回忆的那样,Android 应用在 Dalvik 虚拟机中运行,我们现在知道 Mono 应用在 Mono CLR 中运行。那么 Xamarin.Android 应用程序是如何运行的?一个简单的答案是它同时使用 Mono CLR 和 Dalvik 虚拟机。以下图解展示了运行时是如何共存的:

Xamarin.Android 应用程序同时使用 Mono CLR 和 Dalvik 虚拟机,并在 Linux 内核之上运行。.Net API 作为 Mono CLR 的一部分存在,并提供了一组类(例如,System.Data、System.Net、System.IO 等)以访问各种设备操作系统功能。然而,使用 .Net API,您无法直接访问大多数设备特定功能,如音频、电话、OpenGL 等。它们作为 Android SDK 或 Java API 的一部分提供,并且可以通过 Android 绑定库访问。下一节将详细介绍 Android 绑定库。
自从 Android 5.0(Lollipop)发布以来,Dalvik 虚拟机被其继任者 Android Runtime(ART)所取代。这意味着现在 Xamarin.Android 应用程序与 ART 一起使用 Mono 虚拟机运行。这两个运行时都在 Linux 内核之上运行,并公开了一组类以访问设备功能。
那么,Mono CLR 和 Android 运行时(ART)在 Xamarin.Android 应用中是如何协同工作的呢?这个魔法是通过一个称为 JNI 的概念和框架实现的。
Java 原生接口
Java 原生接口(JNI)是一个框架,允许非 Java 代码(如 C++ 或 C#)在 JVM 内运行的 Java 代码中调用或被调用。如前图所示,JNI 是 Xamarin.Android 架构中的关键组件。
等价对象
等价对象是一对对象,包括一个位于 Mono CLR 中的托管对象和一个位于 Dalvik VM 中的 Java 对象,它们共同工作以执行 Xamarin.Android 应用程序的功能。
Xamarin.Android 随附一组称为 Android 绑定库的程序集。Android 绑定库中的类对应于 Android 应用框架中的 Java 类,绑定类中的方法作为包装器来调用 Java 类上的相应方法。绑定类被称为 托管可调用包装器(MCW)。每次你创建一个从这些绑定类继承的 C# 类时,都会在构建时生成一个相应的 Java 代理类。Java 代理包含为你的 C# 类中每个重写方法生成的重写,并作为包装器来调用 C# 类上的相应方法。
等价对象的创建可以由 Android 应用框架在 Dalvik VM 内启动,或者由你编写的重写方法在 Mono CLR 内启动。每个 MCW 实例都保留着两个等价对象之间的引用,可以通过 Android.Runtime.IJavaObject.Handle 属性访问。
下图描述了等价对象如何协作:

Xamarin.Android 应用程序打包
在第一章《Android 应用的解剖结构》中,我们讨论了 Android 包(.apk 文件)。Xamarin.Android 创建 .apk 文件,但还包括以下附加类型的文件:
-
C# 代码以程序集(包含 IL)的形式存储在存档的程序集文件夹中。
-
Mono 运行时被打包为 apk 内的本地库。Xamarin.Android 应用必须包含所需 Android 架构的本地库。如果没有包含所需的库,应用程序将无法在这些架构上运行。
Android 绑定设计
Xamarin.Android 的核心部分是对 Android API 的绑定。Xamarin 团队非常专注于开发一种一致的方法来创建绑定,以便 C# .NET 开发者在使用时感到舒适。这导致了一系列关键优势,如下所述:
-
Android API 对 C# .NET 开发者来说感觉非常自然,它允许开发者通过 IDE 内的代码补全和弹出文档来探索 API。
-
C#开发者可以利用大量的 Java/Android 示例和文档,这些示例和文档可以轻松转换为 C#和 Xamarin.Android 使用
设计原则
以下是一些 Xamarin.Android 绑定的关键设计原则。完整的设计原则集合可以在 Xamarin 网站上找到:
-
允许开发者从 Android 应用框架中继承 Java 类
-
公开强类型 API
-
将 JavaBean 属性公开为 C#属性
-
将 Java 事件监听器公开为 C#委托
C#属性
当适当的时候,将 JavaBean 属性、getter 和 setter 方法转换为 C#属性。以下规则用于确定何时创建属性:
-
为 getter 和 setter 方法对创建读写属性
-
为没有相应 setter 方法的 getters 创建只读属性
-
在仅存在 setter 方法的情况下,不会创建只写属性
-
当类型为数组时,不会创建属性
注意
如你所知,Java 没有属性构造,而是遵循 JavaBean 规范中定义的设计模式。为了定义一个属性,开发者只需创建公共的 getter 和 setter 方法,其中只提供 getter 方法的可读属性。
委托
Android API 遵循 Java 模式来定义和连接事件监听器。C#开发者更熟悉使用委托和事件,因此 Android 绑定尝试使用以下规则来简化这一过程:
-
当
listener回调有一个void返回时,基于EventHandler委托生成一个事件 -
当
listener回调没有void返回时,会生成一个支持适当签名的特定委托
这些事件或属性仅在以下条件下创建:
-
Android 事件处理方法有一个前缀,例如,
setOnClickListener -
Android 事件处理器的返回类型为
void -
Android 事件处理器有一个单一参数
常量到枚举
在 Android API 中,常见的方法接受或返回int类型,这些类型必须映射到常量以确定其含义。当可能时,Xamarin 团队创建.NET 枚举来替换这些常量,并调整相应的方法以与枚举一起工作。这通过在 IDE 中使用 IntelliSense 以及增强方法类型安全性,提供了显著的生产力提升。
开发环境
选择合适的 IDE 进行开发是绝对必要的,因为它可以极大地简化并加快你的开发速度,如果你选择了正确的 IDE。在 IDE 方面有两个选择:Xamarin Studio 或 Visual Studio。
对于在 Windows 机器上开发 iOS 应用,你可以使用 Xamarin Studio 或 Xamarin iOS Visual Studio 插件。然而,你无法在 Windows 操作系统上构建和运行 iOS 应用。你必须拥有一台 Mac 计算机。
Windows 用户在开发 Android 应用时有两个 IDE 选择。你可以使用 Xamarin Studio 或 Visual Studio。如果你在 Mac OS 上,那么你必须使用 Android Studio IDE。本书中的所有示例都是使用 Mac 上的 Xamarin Studio 开发的。
以下部分列出了 Xamarin Studio 和 Visual Studio IDE 的一些独特功能。
Xamarin Studio
Xamarin Studio 是 MonoDevelop IDE 的定制版本,可用于开发 Android、iOS 和 OS X 应用。Xamarin Studio 可在 OS X 和 Windows 上使用,并具有许多高级功能,如下所示:
-
代码补全
-
智能语法高亮
-
代码导航
-
代码工具提示
-
集成调试器,用于在模拟器或设备上运行的移动应用
-
Git 和 subversion 内置的源代码控制集成
-
Xamarin 组件存储库
-
NuGet 包浏览器
以下截图显示了在 Mac OS 上打开的 Xamarin Studio 的 Android 用户界面设计器:

Xamarin for Visual Studio
Xamarin for Visual Studio 是一个支持开发 Xamarin.Android 和 Xamarin.iOS 应用的插件。Xamarin 的 Visual Studio 插件至少需要一个商业或企业许可证。它不适用于基本的独立许可用户。如果你已经拥有 Visual Studio 许可证并且熟悉该环境,那么由于采用简单,插件可能比 Xamarin Studio 更吸引人。除了基本功能,如代码补全、语法高亮、智能导航和工具提示之外,Xamarin Visual Studio 插件还扩展了 IDE 功能,使移动开发变得轻松。以下是从 Xamarin for Visual Studio 中提供的某些专用功能,这些功能作为插件提供:
-
IntelliSense:这有助于开发者快速查看 iOS 和 Android API 的语言参考。
-
可视化设计器:使用可视化设计器时,你不必一定记住视图的所有属性,同时构建适用于多个分辨率的 UI 布局。这也集成了属性编辑器,可以轻松配置颜色、字体、大小、边距、视图 ID 等属性。
以下截图显示了打开 Android 用户界面设计器的 Visual Studio 2012:

IDE 比较
每个 IDE 都为开发者提供基本的核心功能,并有一些自己独特的功能,这并不奇怪。以下表格描述了两个不同 IDE 选择的优缺点,这些 IDE 可用于 Xamarin.Android 开发:
IDE
优点
缺点
Xamarin Studio
-
它包含 Xamarin.Android,无需额外许可证。它可以在 Windows 和 OS X 上运行。
-
Xamairn.Android 许可证在 Windows 和 Mac OS 上运行。
-
默认情况下,它不支持使用 TFS 进行源代码控制。
Visual Studio
-
大多数 C#开发者已经熟悉并习惯使用 Visual Studio。
-
当使用 TFS 进行源代码控制时,这在许多.NET 商店中很常见,这会很有用。在 Visual Studio 中使用 TFS 时,不需要额外的第三方工具或配置。
-
使用 Visual Studio 需要 Xamarin Android 的商业或企业许可证。
-
它仅在 Windows 上运行。
兼容性
由 Xamarin Studio 创建和更新的解决方案和项目文件与 Visual Studio 兼容,这使得在整个项目期间轻松地在两个环境之间切换。这也允许团队成员采用他们最舒适或在其首选平台上运行的工具。
摘要
在本章中,我们讨论了 Xamarin.Android 的架构以及它如何通过 C#和.NET 创建 Android 应用的神奇之处。我们还回顾了采用 Xamarin.Android 的好处和缺点。在下一章中,我们将安装 Xamarin.Android 并创建一个项目,我们将为本书的剩余部分构建该项目。
第三章:创建兴趣点应用
在本章中,我们将转向创建应用的实践方面,并介绍 Xamarin.Android 为开发者提供的创建、执行和调试应用程序的设施。本章涵盖了以下主题:
-
示例应用的概述
-
安装和配置 Xamarin.Android
-
创建示例应用
-
运行和调试应用
示例 POIApp
在本章中,我们将首先构建一个示例兴趣点(POIApp)应用,该应用将通过本书剩余章节完成。此应用将允许用户捕捉、保存和管理POIApp,并支持以下功能:
-
它捕捉关于
POIApp的信息,包括名称、描述、地址、纬度、经度和照片 -
它使用设备的定位功能捕捉
POIApp的地址、纬度和经度 -
它使用设备的相机捕捉并保存
POIApp的照片 -
它在云上保存
POIApp的详细信息 -
它从云中检索并显示
POIApp的列表 -
它存储/缓存
POIApp以供离线查看
安装 Xamarin.Android
在我们继续之前,我们需要安装 Xamarin.Android。本节将指导您在 Mac 操作系统上安装最新的 Xamarin 开发平台(Xamarin.Android 版本 5.1.3 和 Xamarin Studio 版本 5.9.3)。
小贴士
在撰写本书时,本章中提供的安装说明是准确的。然而,工具正在快速更新,因此这些说明在您阅读时可能已过时。您可以参考官方 Xamarin 网站以获取更新的安装说明。
您可以选择在 Windows 操作系统上安装 Xamarin.Android 并完成示例;在这种情况下,您将遇到一些方向上的微小偏差。如果您选择在 Windows 操作系统上安装,您可以参考官方 Xamarin 开发者门户上的安装指南,网址为developer.xamarin.com/guides/android/getting_started/installation/windows/。
要安装 Xamarin.Android,请执行以下步骤:
-
要使用 Xamarin 开发 Android 应用,Xamarin Studio IDE 和 Xamarin.Android 平台是必备条件。请访问
xamarin.com/的下载部分,填写您的个人信息,下载适用于您操作系统的统一安装程序,并启动它:![]()
-
点击安装程序页面和协议页面,直到到达产品选择页面。安装程序允许安装 Xamarin.Android 和 Xamarin.iOS,如下截图所示:
![]()
-
在本书的练习中不需要 Xamarin.iOS;您可以在安装程序窗口中取消选中 Xamarin.iOS,然后点击继续。
-
现在安装程序将继续配置安装目录。你可以更改计算机上的安装位置,或者继续安装:
![]()
-
接下来,将列出安装的先决条件,如下面的截图所示。它需要安装 Mono 框架、Android SDK、Xamarin Studio 和 Xamarin.Android 组件。点击 继续 以继续安装:
![]()
-
你现在将看到 Android SDK 协议页面。只需接受并点击 继续 以进行安装。将显示一个安装进度页面,描述组件安装的进度:
![]()
注意
在安装 Xamarin.Android 期间,如果你遇到 Android SDK 下载问题,请确保下载没有被你的企业网络或防火墙阻止。对于此类事件,你可以从
developer.android.com/sdk/index.html下载和安装独立的 Android SDK。一旦 Android SDK 安装在你的计算机上,你就可以继续使用 Xamarin 软件包安装程序。 -
随着每个组件的安装,将在组件旁边放置一个勾选标记,一旦所有项目都安装完毕,将显示一个最终的安装完成页面。
-
现在点击 启动 Xamarin Studio:
![]()
安装平台和工具
Android 平台安装附带 Android SDK 管理器工具包,允许你选择和下载构建 Android 应用程序所需的工具和平台。例如,当 Android 新版本发布时,你可以通过使用 SDK 管理器下载新捆绑包来测试你的应用程序与新的平台兼容性。
Android SDK 管理器可以通过在 Xamarin Studio 中导航到 工具 | 打开 Android SDK 管理器... 选项来启动:

注意到 Android SDK 管理器现在已经打开,并列出了以下选项供你安装或删除。
工具
工具部分是 Android 安装的核心部分之一。这是在设备上构建、安装和调试 Android 应用程序所必需的:

工具的简要描述如下:
-
Android SDK 工具: 这些是必须安装以编译 Xamarin.Android 应用程序的开发工具。当你安装 Xamarin 平台时,它将自动安装;然而,你可能需要保持其更新。
-
Android SDK 平台工具: SDK 平台工具是连接设备以部署构建和调试应用程序所必需的。你应该始终拥有最新版本的平台工具以确保与最新版本的 Android API 兼容;因此,也要保持其更新。
-
Android SDK 构建工具:构建工具用于将源代码编译成可在 Android 设备或模拟器上运行的应用程序。默认情况下,Xamarin Studio 安装了最新版本的构建工具。始终建议您安装最新的 SDK 构建工具版本。
更多信息,您可以访问以下官方 Android 文档:
Android 平台 API
Android 操作系统每个后续版本也包括 Android 框架 API,供开发者利用新包、类构建应用程序。平台 API 部分列出了所有 Android API 版本以及 API 级别:

以下是对各种平台的简要描述:
-
SDK 平台:给定 API 级别的 SDK 平台允许您针对该版本的 Android 进行编译。当发布新平台时,它需要更新的 SDK 平台工具和 SDK 工具;因此,您需要保持这些工具的最新状态。
-
SDK 示例:这些是使用 Java 开发的每个 API 级别的 Android 示例应用程序。Xamarin 开发者不需要这些示例。除非您有特定的需求或想要分析并将 Java 示例迁移到 C#,否则不需要安装这些示例。
-
系统镜像:系统镜像与 Android 虚拟设备(AVD)一起使用。所有最新的 Android 版本都包括 ARM 和 x86 系统镜像。x86 镜像运行速度明显更快,因此比 ARM 镜像更受欢迎。请注意,一些系统镜像以 Google APIs 为前缀。它们包括 Google Play 服务运行时,并且对于测试使用 Google Play 服务功能(如地图、应用内购买等)的应用程序非常有用。
Android 平台额外工具
额外工具部分包括一些在开发过程中可能需要的附加可选工具,如下面的截图所示:

以下部分将简要介绍 Android 平台的一些额外工具的重要部分:
-
Android 支持库:Android 支持库是一组代码库,用于为旧设备提供对新 API 功能的向后兼容性。在开发某些功能时使用支持库被视为最佳实践,因为它使得应用与旧版本设备兼容。
-
Google Play 服务:Google Play 服务运行时提供了一组 API,用于开发某些 Android 功能,如 Google Maps、与 Google+集成、Google Play 订阅等。所有 Android 设备都包含 Google Play 服务运行时。然而,Android 模拟器默认不包含 Google Play 服务运行时,但可以单独安装。
-
Google USB 驱动程序:如果您正在运行 Windows 操作系统,您将需要安装这些驱动程序以启用 Android 设备的 USB 调试。对于某些设备,您可能还需要安装设备制造商提供的特定设备驱动程序的软件。如果您使用 Mac OS X 进行开发,则不需要安装此驱动程序。
创建 Android 虚拟设备
Android 模拟器,也称为Android 虚拟设备(AVD),用于在没有设备的情况下测试 Android 应用程序。模拟器作为 Xamarin 安装的一部分进行安装。您可以为模拟的设备创建自己的模拟器或根据所需的设备配置自定义现有的模拟器。
为了创建或修改现有的模拟器,请执行以下步骤:
-
从主菜单栏导航到工具并打开Google 模拟器管理器。它将打开Android 虚拟设备管理器窗口:
![图片]()
-
要创建新的模拟器,请单击右侧面板上的创建按钮。提供配置,例如AVD 名称、设备、目标、内存选项等。
-
要编辑现有的模拟器,选择您要编辑的模拟器并单击编辑按钮:
![图片]()
-
注意目标设置;这指定了模拟器将使用的 Android 平台版本和 API 级别。
-
从设备字段下拉菜单中选择设备皮肤。在我的情况下,我选择了Nexus 5。
-
将目标字段设置为Android 4.4.2。如果需要,取消选中硬件键盘存在选项,然后单击确定。
-
选择使用主机 GPU选项。此选项使模拟器使用主机计算机的 OpenGL 实现,这使得渲染速度显著提高。
-
您可以选择快照选项来加速模拟器的启动时间。当此选项启用时,它将在第一次启动时保存其 RAM 的快照,并在未来的使用中从该快照恢复。您不能同时启用快照和使用主机 GPU选项。
克隆虚拟设备
有许多选项可以修改,以便模拟任何所需的设备和配置。Android 虚拟设备管理器对话框还有一个名为设备定义的选项卡,可以用来设置配置 AVD 时可用设备。以下截图显示了可以作为设备定义一部分进行配置的内容:

加速 Android 模拟器
随 Android 开发工具包一起提供的默认 Android 模拟器相当缓慢。在 Android 设备模拟器中测试应用程序常常令人失望且痛苦。对于开发者来说,更明智的选择是拥有真实的 Android 设备进行测试。然而,由于 Android 生态系统具有各种形态和设备制造商,实际上不可能购买每个目标设备来测试应用程序。您必须寻找一些成本效益高的解决方案来测试您的应用程序。
以下部分将指导您了解一些使您的 Android 模拟器更快以及可用于测试 Android 应用程序的替代选项的技巧。
使用 x86 模拟器
Android 提供了一个 x86 模拟器,可以显著加快开发速度,因为 AVD 的启动和执行时间更快。x86 模拟器不是基础 Xamarin 安装的一部分,但安装说明可以在 Xamarin 网站以及 Android 开发者网站上找到。可能需要非常具体的版本,尤其是如果您正在使用 OS X Mavericks,因此我们在此不重复说明。
安装完成后,您可以在编辑 AVD 配置时通过选择的 Intel Atom (x86) CPU/ABI 利用 x86 模拟器。
第三方模拟器解决方案
x86 解决方案应在具有良好内存的计算机上运行良好。除了原生 Android 模拟器之外,您还可以使用一些第三方工具,如 Xamarin Android Player 或 Genymotion。
Xamarin Android Player
Xamarin 最近宣布了自己的 Android 模拟器,名为 Xamarin Android Player。Xamarin Player 在 Android x86 硬件加速虚拟化和 OpenGL 2.0 上运行,以实现快速启动和流畅的用户界面。这适用于 Windows 和 Mac 平台。您的系统需要具有与 OpenGL 2.0 兼容的图形卡,至少 2 GB 的硬盘空间和至少 2 GB 的 RAM 来安装和运行 Xamarin Player。
以下截图显示了带有播放器设置面板的 Xamarin Android Player:

Xamarin Player 设置面板允许您模拟一些模拟器控件,例如电池寿命、地理位置、音量和电源控制。
Genymotion
Genymotion 是在更快的 Android 模拟器竞赛中又一个替代选择。Genymotion 基于开源项目 Android VM,由法国公司 Genymobile 开发。它也适用于所有主要平台,包括 Mac、Windows 和 Linux。Genymotion 的基本版本可以免费下载;然而,商业版本的功能远超免费版本。Genymotion 的付费版本可以模拟多点触控、摄像头、GPS、网络质量模拟、加速度计等等。这可以成为 Android AVD 的最佳替代选择。
以下截图显示了 Mac OS 上的 Xamarin Android Player:

创建 POI 应用程序
现在我们已经准备好了开发环境,让我们开始构建POIApp。
以下部分将指导您完成创建、构建并将 POI 应用程序部署到 Android 设备所需的几个步骤:
-
启动 Xamarin Studio。
-
从文件菜单中,导航到新建 | 解决方案。将显示新建解决方案视图,如下截图所示:
![图片]()
-
在屏幕左侧选择Android部分,导航到应用 | Android 应用,然后点击下一步。
-
将应用名称输入为
POIApp,并将应用程序标识符设置为com.packt.poiapp。 -
在兼容性选择中,选择最大兼容性以使您的应用程序在广泛的设备上兼容。
-
从主题下拉菜单中,您可以从提供的主题范围中选择,然后点击下一步:
![图片]()
-
检查项目位置,并根据需要调整。点击创建:
![图片]()
-
Xamarin Studio 将创建一个解决方案和所需的项目文件夹。项目文件夹将包含一个默认的
MainActivity.cs类和一个Main.axml布局文件。
Xamarin Studio IDE
创建POIApp后,项目将在环境中打开。
以下截图展示了创建项目后 Xamarin Studio 的状态:

与任何其他现代 IDE 一样,Xamarin Studio 通过屏幕顶部的菜单、下面的上下文相关工具栏和一系列可停靠的垫片来组织,用于查看和操作各种类型的内容。默认情况下,Xamarin Studio 配置了以下选项:
-
解决方案垫片停靠在左侧,允许您探索项目中的结构和内容。
-
编辑窗口位于中间,用于查看和操作文件内容。
-
任务特定垫片在右侧和底部折叠,可以通过悬停在图标和标题上展开。
可以通过导航到视图 | 垫片来访问额外的垫片。
项目选项视图
有许多可设置的选项,这些选项会影响应用程序的构建和执行方式。这些选项可以从项目选项视图中进行调整。以下部分将向您展示如何使用 Xamarin Studio 项目资源管理器中可用的不同选项来设置各种项目配置。
理解项目结构
项目是一个组织单元,它代表解决方案垫片中的完整 Xamarin Android 应用程序。它不仅包含源代码,还包含依赖库、资源和其他项目配置。在我们开始编写任何代码之前,我们必须了解 Xamarin.Android 项目结构和每个文件夹的重要性。以下截图展示了 Xamarin Android 应用程序由哪些不同组件组成:

Xamarin Studio 项目向导创建默认的项目结构,并将所需的文件和目录添加到 解决方案 面板中。Xamarin.Android 项目的最重要的构建块包括:
-
主要项目(
POIApp)是包含整个项目上下文的根目录。右键单击项目名称以获取各种选项,例如清理、构建、运行、项目配置选项等。 -
References目录包含应用程序中使用的基类库和程序集的引用。右键单击 编辑引用 以添加基类库或第三方程序集。 -
Components文件夹包含由社区开发者构建并共享在 Xamarin 组件商店中的可重用代码片段。组件使您能够快速将新的控件和功能添加到 Xamarin 应用程序中。例如,如果您的应用程序使用数据库操作,您可以通过编写几行代码快速集成 SQLite.Net 组件以执行 SQL 操作。我们将在第四章 添加列表视图 中讨论如何从 Xamarin 组件商店将组件添加到您的应用程序中。 -
Assets文件夹包含可以与应用程序捆绑的原始资产。它可以包含第三方.ttf字体、游戏纹理等文件。 -
Properties文件夹通常包含两个文件:AndroidManifest.xml和AssemblyInfo.cs文件。AndroidManifest.xml文件包含 Android 应用的元数据,而AssemblyInfo.cs文件则包含有关程序集的信息,例如名称、描述、版本等。 -
Resources文件夹是主要构建块,由图像(在 Android 中称为可绘制资源)、布局描述符、字符串、颜色、主题等组成。所有添加的资源都被分组到不同的文件夹中,并使用唯一的资源 ID 进行引用。Xamarin Studio 自动在
Resources目录下创建一个新的Resource.designer.cs文件。此文件包含每个资源的唯一 ID。这与在本地 Android 应用程序中自动创建的R.java文件类似。此文件由 Xamarin.Android 维护,并且每当应用程序的资源发生变化时,都会定期重新生成。以下是在
Resources目录内可以创建的一些子目录:-
Resources/drawable-xxx:可绘制文件夹用于存储图像,如
.png、.jpeg等。请注意,默认项目结构包含多个具有限定符的可绘制文件夹后缀,例如 hdpi、mdpi、xhdpi 等。限定符表示该目录中的资源仅在特定情况下使用。您可以创建另一个目录Resources/drawable来放置不需要针对不同设备配置的图像。在第七章“为多种屏幕尺寸设计”中,对资源限定符的更详细讨论被解释了。
-
资源/布局:这个文件夹包含 XML 布局描述符文件。在我们的例子中,创建了
Main.axml文件。 -
资源/值:这个文件夹包含用于声明应用中所有字符串的文件,例如
string.xml。这对于应用本地化很有帮助。 -
资源/菜单:菜单文件夹包含每个活动的基于 XML 的菜单规范。
-
-
应用程序源代码可以管理在多个不同的文件夹中。默认情况下,Xamarin Studio 项目模板是在
MainActivity.cs文件中创建的。 -
新版本的 Xamarin Studio 创建了
Xamarin.UITest项目,该项目用于自动化的 UI 验收测试。Xamarin.UITest框架基于 Calabash,使用它可以编写 C#和 NUnit 的测试用例,并在 Android 和 iOS 平台上执行。本书在第八章“创建数据存储机制”中介绍了使用 NUnitLite 进行 Android 单元测试。现在,如果你愿意,你可以安全地删除测试项目。
Xamarin Studio 布局设计器
从解决方案资源管理器中打开位于Resources/layout/Main.axml的Main.axml文件。注意图形布局编辑器将是默认布局编辑器。目前,Main.axml布局包含一个位于LinearLayout内的按钮。现在让我们继续了解 Xamarin Studio 布局设计器的不同选项。
设计内容布局
Android Studio 布局设计器包含两个按钮:内容和源,这两个按钮位于设计视图的底部。这些按钮允许你在布局的可视表示(内容)和布局的 XML 源代码视图之间切换。这对于直接在 XML 源视图中进行编辑的任务来说非常有用;然而,内容视图对于查看和排列小部件来说也很实用。
内容视图的实用性在一定程度上有限,仅因为大多数时候视图的一部分必须在运行时使用代码构建;然而,当视图可以完全在 XML 中指定时,内容视图就非常有用。在内容视图中,你会在窗口顶部注意到一组有用的工具,如下面的截图所示:

在右上角,你可以找到一组缩放控件。这些控件允许你根据你的显示器大小和想要查看的细节级别来放大或缩小布局。在页面顶部,你可以找到下拉菜单,这些菜单也允许你选择一些选项,例如要模拟的设备屏幕大小、设备的方向以及要模拟的 Android 平台版本。
文档大纲和属性面板
在 IDE 的右下角,你会注意到文档大纲和属性面板。
文档大纲面板提供了方便的导航和选择小部件的方法,尤其是在布局变得更加复杂时。选择listView1然后点击属性选项卡。激活文档大纲面板以查看您的布局大纲,列出其中包含的所有小部件、视图或视图组。
在设计窗口中选择任何视图并点击属性选项卡以激活属性窗口。属性窗口允许您编辑/选择所选视图的属性。Xamarin Studio 负责生成源代码以反映在设计视图中所做的更改。
工具箱
工具箱面板按照基本小部件在列表顶部,容器小部件如 ListView 等在列表下方。
默认情况下,工具箱面板显示在 Xamarin Studio IDE 的右上角。顶部有一个搜索框,允许您过滤列表中的小部件,并且搜索框右侧有两个按钮,允许您调整小部件的显示方式。
设置目标框架
目标框架设置确定在开发和测试期间可用的 API 级别。让我们选择自动设置;使用目标框架版本。在这种情况下,它自动选择了API 级别 19 Android 4.4。
最小 SDK 版本告诉您应用程序运行所需的最低 API 级别。根据最小 SDK 中指定的值,Google Play 商店会阻止用户安装 API 级别低于指定值的系统应用程序。
为了设置目标框架,请执行以下步骤:
-
在解决方案面板中,选择
POIApp解决方案下的POIApp项目。 -
右键单击它并点击选项。
-
导航到页面左侧的构建 | 常规:
![图片]()
-
在目标框架字段中,选择使用最新安装的平台并点击确定。
设置应用图标和包名
Xamarin.Android 为应用提供了默认的图标和包名。图标将在 Android 设备的首页抽屉中显示,与其他应用程序列表并列,以及在每个视图顶部的操作栏上。
为了调整这些设置的默认值,请执行以下步骤:
-
应用程序图标图像在代码包的
Assets文件夹中提供。 -
使用Finder/Windows 资源管理器,将
ic_launcher.png从您的计算机硬盘复制到相应的Resources\drawable文件夹。 -
从 Xamarin Studio 导航到
Resources\drawable,右键单击它,并点击添加文件或您也可以将启动器图像拖放到 Xamarin Studio 的Resource/drawable文件夹中。 -
您现在应该在解决方案面板的
drawable下看到ic_launcher.png。 -
选择
POIApp项目,右键单击它,并点击选项。 -
导航到构建 | Android 应用程序。
-
将应用程序名称更改为
POIApp。这将导致生成的 APK 文件被命名为POIApp.apk。 -
将应用程序图标选择更改为
@drawable/ic_launcher。 -
点击确定。
在我们准备应用程序部署的过程中,我们将在第十一章“将应用程序发布到应用商店”中介绍更多选项。
启动活动
当从 Android 设备的首页启动应用程序时,Android 操作系统会创建一个活动实例,该活动实例是你声明的启动活动。在用 Android SDK 进行开发时,这会在AndroidManifest.xml文件中指定。以下是从AndroidManifest.xml文件中摘录的代码,展示了如何指定一个活动为启动活动:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.paket.POIApp" >
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="POIApp"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
android:label="POIApp" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Xamarin.Android 通过使用.NET 属性提供了一种更方便的方法来指定这一点。这些.NET 属性在构建时用于构建ApplicationManifest.xml文件,因此你很少需要直接处理该文件。在指定要启动的初始活动的情况下,将MainLauncher设置为true即可完成工作,如下面的代码示例所示:
[Activity (Label = "POIApp", MainLauncher = true)]
public class MainActivity : Activity
{
...
}
运行和调试应用程序
你运行和调试应用程序的方式对开发者来说非常重要,因为它对生产力和时间表有重大影响。Xamarin Studio 和 Android 模拟器团队协作,使测试和调试周期尽可能无痛苦。让我们按照以下步骤进行:
-
通过点击任务栏左侧的播放按钮、按F5键或导航到运行 | 开始调试来启动调试会话:
![img/YGX0g1hq.jpg]()
-
从列表中选择Nexus 6(模拟器)并点击启动模拟器按钮。模拟器启动可能需要一些时间。让我们等待它完全加载。
-
在设备列表的顶部选择Nexus 6,然后点击确定。Xamarin Studio 会将编译后的应用程序部署到模拟器。部署进度可以通过工具栏中间的状态视图和 IDE 底部的应用程序输出面板进行监控。
-
切换到 Android 模拟器并解锁屏幕。POI 应用程序将显示在屏幕上:
![img/uoPqTLdR.jpg]()
注意
Android 模拟器在开发过程中用于测试 Android 应用程序。屏幕的左侧显示了设备上会看到的内容,右侧提供了复制设备硬件的按键。
-
点击Hello World按钮,应用程序将增加计数器并更新按钮的标题。
-
切换回 Xamarin Studio,通过点击工具栏最左侧的停止按钮来停止应用程序。
-
打开
MainActivity.cs文件,通过点击编辑器左侧的空白边缘(位于行号左侧)在第 21 行设置断点。 -
通过点击开始按钮重启应用。由于 Android 模拟器仍在运行,您不需要进行设备选择。应用将在之前设置的断点处停止!
![img/XlKlnUGv.jpg]()
-
您会注意到工具栏中有一组调试控件。有继续执行的控件,即跳过当前行、进入当前函数和退出当前函数:
![img/oJ0HE6fc.jpg]()
-
您还会注意到在 IDE 底部出现了一组与调试应用相关的新垫片。这些垫片允许您查看对象、断点、线程和调用堆栈!
![img/v1pXKN3w.jpg]()
-
点击单步执行两次以观察执行进度,然后点击继续以让应用开始。
如您从本节中看到的那样,Xamarin Studio 和 Android 模拟器提供了一种强大且直观的方式来执行和调试应用。
使用 Android 设备进行调试
应用可以在实际设备上以与使用模拟器相同的方式简单执行和调试。为了使用物理设备做准备,您需要执行以下步骤:
-
在设备上启用 USB 调试。
-
为设备安装适当的 USB 驱动程序(仅限 Windows)。
启用 USB 调试
为了在运行 Android 4.0 及更高版本的设备上启用 USB 调试,请执行以下步骤:
-
对于运行 Android 4.2 或更高版本的设备,有一个额外的步骤;“开发者选项”最初是隐藏的。导航到设置 | 关于手机并连续点击构建号七次。在某些配置中,确切的菜单结构可能不同。在我的运行 Android 4.3 的 HTC One 上,菜单是设置 | 关于 | 软件信息 | 更多。
-
导航到设置 | 开发者选项。
-
点击USB 调试。
安装 USB 驱动程序
Windows 用户需要安装设备制造商提供的 USB 驱动程序。您可以在“使用硬件设备”标题下的 Android 开发者网站上找到更多详细信息,或者咨询您的设备制造商。developer.android.com/tools/device.html。
OS X 用户应该可以顺利使用。
在设备上运行应用
完成前面的步骤后,只需使用 USB 线将设备连接到您的开发计算机,从 Xamarin Studio 启动应用,并在设备选择视图中选择实际硬件设备,而不是启动模拟器。
背后的事情
在这一点上,快速查看一些我们在第二章“Xamarin.Android 架构”中讨论过的幕后事情,是非常有趣的。
同伴对象
让我们从第二章“Xamarin.Android 架构”中讨论的同伴对象(代理对象)开始。在您的Finder/Windows Explorer窗口中导航到POIApp\POIApp\obj\Debug\android\src\poiapp目录代码包,使用记事本打开MainActivity.java。
以下代码片段展示了源文件的一些关键部分:
packagepoiapp;
public class MainActivity extends android.app.Activity implements
mono.android.IGCUserPeer
{
. . .
public void onCreate (android.os.Bundle p0)
{
n_onCreate (p0);
}
private native void n_onCreate (android.os.Bundle p0);
. . .
}
注意以下要点:
-
MainActivity类继承自android.app.Activity,正如你所期望的那样 -
创建了一个
onCreate()代理方法,该方法调用本地的n_onCreate()方法,该方法指向我们托管 C# 类中重写的OnCreate()方法 -
MainActivity类包含一个静态初始化块和一个构造函数,该构造函数建立了 Java 类与其管理的 C# 对等类之间的联系,包括初始化n_onCreate()
AndroidManifest.xml 文件
在代码包中导航到 POIApp\POIApp\obj\Debug\android,并打开 AndroidManifest.xml 文件。以下代码片段展示了清单文件的一部分:
<?xml version="1.0" encoding="utf-8"?>
<manifest android:versionCode="1" android:versionName="1.0" package="com.packt.poiapp">
<uses-sdk android:minSdkVersion="10" />
<application android:label="POIApp" android:name="mono.android.app.Application" android:debuggable="true">
<activity android:icon="@drawable/icon" android:label="POIApp" android:name="md56a0a1b7026a61848924491193f52dfa6.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider android:name="mono.MonoRuntimeProvider" android:exported="false" android:initOrder="2147483647" android:authorities="com.packt.poiapp.mono.MonoRuntimeProvider.__mono_init__" />
<receiver android:name="mono.android.Seppuku">
<intent-filter>
<action android:name="mono.android.intent.action.SEPPUKU" />
<category android:name="mono.android.intent.category.SEPPUKU.com.packt.poiapp" />
</intent-filter>
</receiver>
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>
注意以下要点:
-
在
<uses-sdk>元素中将最小 SDK 设置为15 -
初始活动是通过活动定义中的
<category>元素设置的
我们介绍了使用 Xamarin Studio 创建的 Xamarin.Android 项目的结构。我们使用 Mac 平台上的 Xamarin Studio 完成了本书中的所有示例。我们可以确认,之前提到的所有项目配置选项也都在 Visual Studio IDE 中可用。
摘要
在本章中,我们从一本书的剩余章节中完成的一个示例应用程序开始,并展示了我们执行和调试应用程序的设施。
在下一章中,我们将从创建基本布局和构建 ListView 以显示从服务器获取的 POIApp 开始。
第四章:添加列表视图
在本章中,我们终于到达了很多人一直在等待的部分,即开发用户界面。我们将向您介绍创建和填充ListView的相关活动,包括以下主题:
-
创建 POIApp 活动布局
-
创建自定义列表行项布局
-
ListView和ListAdapter类 -
将
BaseAdapter扩展以向ListView小部件提供数据 -
在 Xamarin.Android 中处理网络服务
-
与
ActionBar菜单选项一起工作 -
处理列表项点击事件
-
处理网络状态
创建 POI ListView 布局
从技术上讲,您可以使用 C#代码创建并将用户界面元素附加到您的活动上。然而,这会有些混乱。我们将采用最常见的方法,通过声明基于 XML 的布局。考虑到这一点,让我们从创建一个用于显示 POI 列表项的布局开始这一章。
当我们在上一章(第三章,创建兴趣点应用)中创建新的POIApp解决方案时,作为 Xamarin Studio 项目模板的一部分,创建了一个默认布局和活动。
而不是删除这些文件,让我们给它们更合适的名字并删除不必要的内客,如下所示:
-
在资源 | 布局中选择
Main.axml文件并将其重命名为POIList.axml。 -
双击
POIList.axml文件以在布局设计器窗口中打开它。目前,
POIList.axml文件包含的是作为默认 Xamarin Studio 模板的一部分创建的布局。根据我们的要求,我们需要添加一个占满整个屏幕宽度的ListView小部件和一个位于屏幕中间的ProgressBar。在从服务器下载数据时,将向用户显示不确定的进度条。一旦下载完成且数据准备就绪,不确定的进度条将在 POI 数据在列表视图中渲染之前被隐藏。 -
现在,在设计师窗口中打开文档大纲选项卡并删除按钮和
LinearLayout。 -
现在,在设计师工具箱中搜索
RelativeLayout并将其拖动到设计师布局预览窗口中。 -
在工具箱搜索框中搜索
ListView并将其拖动到布局设计器预览窗口中。或者,您也可以将其拖放到文档大纲选项卡中的RelativeLayout上。
我们刚刚将ListView小部件添加到了POIList.axml中。现在,在设计师窗口中打开属性面板视图并编辑一些属性:

如我们回忆第三章,创建兴趣点应用,属性面板允许您修改所选小部件的属性。面板顶部有五个按钮,用于切换正在编辑的属性集。@+id表示通知编译器需要创建一个新的资源 ID 来识别 API 调用中的小部件,而listView1标识常量的名称。现在,执行以下步骤:
-
将 ID 名称更改为
poiListView并保存更改。切换回文档大纲面板,并注意ListViewID 已更新。 -
再次,切换回属性面板,并点击布局按钮。
-
在布局属性的视图组部分,将宽度和高度属性都设置为
match_parent。 -
Height和Width属性的match_parent值告诉我们,ListView可以使用父级提供的整个内容区域,不包括指定的任何边距。在我们的例子中,父级将是顶级的RelativeLayout。提示
在 API 级别 8 之前,使用
fill_parent代替match_parent以实现相同的效果。在 API 级别 8 中,fill_parent被弃用,并替换为match_parent以提高清晰度。目前,这两个常量被定义为相同的值,因此它们具有完全相同的效果。然而,fill_parent可能会在未来版本的 API 中被移除;因此,从现在开始,应该使用match_parent。到目前为止,我们已经将
ListView添加到RelativeLayout中,现在让我们将进度条添加到屏幕的中心。 -
在工具箱搜索字段中搜索进度条。你会注意到将列出几种类型的进度条,包括水平、大、普通和小。将普通进度条拖到
RelativeLayout上。默认情况下,进度条小部件与其父布局的左上角对齐。要将它对齐到屏幕中心,请在文档大纲选项卡中选择进度条,切换到属性视图,并点击布局选项卡。现在选择在父级中居中复选框,你会注意到进度条已对齐到屏幕中心,并将出现在列表视图的顶部。
-
目前,进度条在屏幕中心可见。默认情况下,这可以在布局中隐藏,并且只有在数据正在下载时才会显示。
-
将进度条ID 更改为
progressBar并保存更改。 -
要从布局中隐藏进度条,请点击属性视图中的行为选项卡。从可见性中选择框,然后选择消失。
此行为也可以通过在任意视图中调用
setVisibility()并通过传递以下任何行为来实现控制。在本章的后面部分,我们将看到如何使用活动代码以编程方式隐藏视图。
View.Visibility属性允许您控制视图是否可见。它基于ViewStates枚举,该枚举定义了以下值:
值
描述
Gone
此值告诉父ViewGroup将视图视为不存在,因此不会在布局中分配空间
Invisible
此值告诉父ViewGroup隐藏视图的内容;然而,它仍然占据布局空间
Visible
此值告诉父ViewGroup显示视图的内容
点击源选项卡以将 IDE 上下文从可视化设计器切换到代码,查看我们迄今为止所构建的内容。注意以下代码是为POIList.axml布局生成的:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
p1:layout_width="match_parent"
p1:layout_height="match_parent"
p1:id="@+id/relativeLayout1">
<ListView
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="match_parent"
p1:layout_height="match_parent"
p1:id="@+id/poiListView" />
<ProgressBar
p1:layout_width="wrap_content"
p1:layout_height="wrap_content"
p1:id="@+id/progressBar"
p1:layout_centerInParent="true"
p1:visibility="gone" />
</RelativeLayout>
创建 POIListActivity
当我们创建POIApp解决方案时,除了默认布局外,还创建了一个默认活动(MainActivity.cs)。让我们将MainActivity.cs文件重命名为POIListActivity.cs:
-
从解决方案资源管理器中选择
MainActivity.cs文件并将其重命名为POIListActivity.cs。 -
在代码编辑器中打开
POIListActivity.cs文件并将类重命名为POIListActivity。 -
POIListActivity类目前包含在创建解决方案时自动创建的代码。我们将编写自己的活动代码,因此让我们从POIListActivity类中删除所有代码。 -
重写
OnCreate()活动生命周期回调方法。此方法将用于附加活动布局、实例化视图以及编写其他活动初始化逻辑。将以下代码块添加到POIListActivity类中:namespace POIApp { [Activity (Label = "POIApp", MainLauncher = true, Icon = "@ drawable/icon")] public class POIListActivity : Activity { protected override void OnCreate (Bundle savedInstanceState) { base.OnCreate (savedInstanceState); } } } -
现在让我们通过调用
SetContentView(layoutId)方法来设置活动内容布局。此方法将布局内容直接放置到活动的视图层次结构中。让我们提供之前步骤中创建的POIList布局的引用。在这一点上,
POIListActivity类看起来如下:namespace POIApp { [Activity (Label = "POIApp", MainLauncher = true, Icon = "@drawable/icon")] public class POIListActivity : Activity { protected override void OnCreate (Bundle savedInstanceState) { base.OnCreate (savedInstanceState); SetContentView (Resource.Layout.POIList); } } }
注意,在前面的代码片段中,POIListActivity类使用了[Activity]属性的一些属性,例如Label、MainLauncher和Icon。在构建过程中,Xamarin.Android 使用这些属性在AndroidManifest.xml文件中创建一个条目。正如我们在第一章“Android 应用的解剖结构”中学到的,AndroidManifest.xml文件是描述您的 Android 应用程序功能和要求的一种简单应用程序配置文件。Xamarin 通过允许使用属性设置所有 Manifest 属性,从而使其更容易,这样您就无需手动在AndroidManifest.xml中修改它们。
到目前为止,我们已经声明了一个活动并将布局附加到它。在此阶段,如果您在 Android 设备或模拟器上运行应用程序,您将注意到将显示一个空白屏幕。本章的后续部分将带您了解使POIListActivity活动完全功能化的魔法。
创建 POI 列表行布局
现在,我们将注意力转向ListView小部件中每一行的布局。Android 平台提供了一些默认布局,可以直接与ListView小部件一起使用:
布局
描述
SimpleListItem1
一行,包含一个标题字段
SimpleListItem2
两行布局,第一字段使用较大字体和较亮的文本颜色
TwoLineListItem
两行布局,两行字体大小相等,第一行文本颜色较亮
ActivityListItem
一行文本,包含一个图像视图
所有的前三个布局都提供了一个相当标准的布局,但为了更好地控制内容布局,也可以创建一个自定义布局,这正是poiListView所需要的。
要创建一个新的布局,请执行以下步骤:
-
在解决方案面板中,导航到资源 | 布局,右键单击它,然后导航到添加 | 新建文件。
-
从左侧的列表中选择Android,从模板列表中选择Android 布局,在名称列中输入
POIListItem,然后单击新建。
在我们开始为列表中每一行的项目布局设计之前,我们必须在纸上绘制并分析 UI 将如何呈现。在我们的例子中,POI 数据将组织如下:

实现这种布局有几种方法,但我们将使用RelativeLayout来实现相同的结果。这个图表中有很多内容。让我们如下分解:
-
RelativeLayout视图组用作顶级容器;它提供了一系列灵活的选项来定位相对内容、其边缘或其他内容。 -
使用
ImageView小部件来显示 POI 的照片,并将其锚定在RelativeLayout实用工具的左侧。 -
使用两个
TextView小部件来显示 POI 名称和地址信息。它们需要锚定在ImageView小部件的右侧,并在父RelativeLayout实用工具内居中。完成此操作的最简单方法是,将两个TextView类放置在另一个布局中;在这种情况下,一个方向设置为垂直的LinearLayout小部件。 -
使用一个额外的
TextView小部件来显示距离,并将其锚定在RelativeLayout视图组的右侧,并垂直居中。
现在,我们的任务是把这个定义放入POIListItem.axml中。接下来的几节将描述如何在可行时使用设计器的内容视图,在需要时使用源视图来完成这项任务。
添加 RelativeLayout 视图组
RelativeLayout布局管理器允许其子视图相对于彼此或相对于容器或另一个容器进行定位。在我们的案例中,为了构建行布局,如前图所示,我们可以使用RelativeLayout作为顶级视图组。当创建POIListItem.axml布局文件时,默认添加了一个顶级LinearLayout。首先,我们需要将顶级ViewGroup更改为RelativeLayout。以下部分将指导您完成 POI 列表行布局设计的步骤:
-
在内容模式下打开
POIListItem.axml,通过点击内容区域选择整个布局。您应该看到一个蓝色轮廓围绕边缘。按Delete键。LinearLayout视图组将被删除,您将看到一个指示布局为空的提示信息。 -
或者,您也可以从文档大纲选项卡中选择
LinearLayout视图组,然后按Delete键。 -
在工具箱中找到
RelativeLayout视图组,并将其拖放到布局中。 -
从文档大纲中选择
RelativeLayout视图组。打开属性面板,并更改以下属性:-
填充选项设置为
5dp -
布局高度选项设置为
wrap_content -
布局宽度选项设置为
match_parent
填充属性控制每个项目周围作为边距的空间量,而高度决定了每行列表的高度。将布局宽度选项设置为
match_parent将使POIListItem内容消耗整个屏幕的宽度,而将布局高度选项设置为wrap_content将使每行等于最长的控件。 -
-
切换到代码视图以查看已添加到布局中的内容。注意以下代码行已添加到
RelativeLayout中:<RelativeLayout p1:minWidth="25px" p1:minHeight="25px" p1:layout_width="match_parent" p1:layout_height="wrap_content" p1:id="@+id/relativeLayout1" p1:padding="5dp"/>提示
Android 运行在多种设备上,这些设备提供不同的屏幕尺寸和密度。在指定尺寸时,您可以使用多种不同的单位,包括像素(px)、英寸(in)和密度无关像素(dp)。密度无关像素是基于在 160 dpi 屏幕上 1 dp 等于 1 像素的抽象单位。在运行时,Android 将根据实际屏幕密度调整实际大小。使用密度无关像素指定尺寸是一种最佳实践。
添加 ImageView 小部件
Android 中的ImageView小部件用于显示来自不同来源的任意图像。在我们的案例中,我们将从服务器下载图像并在列表中显示它们。让我们在布局的左侧添加一个ImageView小部件,并设置以下配置:
-
在工具箱中找到
ImageView小部件,并将其拖放到RelativeLayout中。 -
在选择
ImageView小部件时,使用属性面板将 ID 设置为poiImageView。 -
现在,点击属性面板中的布局选项卡,并将高度和宽度值设置为
65 dp。 -
在名为
RelativeLayout的属性分组中,将 Center Vertical 设置为 true。简单地点击复选框似乎不起作用,但你可以点击右侧看起来像编辑框的小图标,然后输入true。如果其他方法都失败了,只需切换到 Source 视图并输入以下代码:p1:layout_centerVertical="true" -
在名为
ViewGroup的属性分组中,将 Margin Right 设置为5dp。这会在 POI 图像和 POI 名称之间留出一些空间。 -
切换到 Code 视图查看已添加到布局中的内容。注意以下行代码被添加到
ImageView:<ImageView p1:src="img/ic_menu_gallery" p1:layout_width="65dp" p1:layout_height="65dp" p1:layout_marginRight="5dp" p1:id="@+id/poiImageView" />
添加一个 LinearLayout 小部件
LinearLayout 是最基础的布局管理器之一,根据其 orientation 属性的值,水平或垂直地组织其子视图。让我们添加一个 LinearLayout 视图组,它将用于布局 POI 名称和地址数据,如下所示:
-
在工具箱中定位垂直的
LinearLayout视图组。 -
添加此小部件稍微有些复杂,因为我们希望它锚定到
ImageView小部件的右侧。 -
将
LinearLayout视图组拖动到ImageView小部件的右侧,直到边缘变成蓝色虚线,然后释放鼠标。它将与ImageView小部件的右侧对齐。 -
在 Layout 部分的名为
RelativeLayout的属性分组中,将 Center Vertical 设置为true。和之前一样,你需要在编辑框中输入true或手动将其添加到 Source 视图。 -
切换到 Code 视图查看已添加到布局中的内容。注意以下行代码被添加到
LinearLayout:<LinearLayout p1:orientation="vertical" p1:minWidth="25px" p1:minHeight="25px" p1:layout_width="wrap_content" p1:layout_height="wrap_content" p1:layout_toRightOf="@id/poiImageView" p1:id="@+id/linearLayout1" p1:layout_centerVertical="true" />
添加名称和地址的 TextView 类
添加 TextView 类以显示 POI 名称和地址:
-
在 Toolbox 中定位
TextView并将其添加到布局中。这个TextView需要添加到我们刚刚添加的LinearLayout视图组中,所以将TextView拖动到LinearLayout视图组上,直到它变成蓝色,然后释放鼠标。 -
将
TextView的 ID 命名为nameTextView并设置text size为20sp。文本大小可以在 Properties 面板的 Style 部分设置;你需要通过点击右侧的省略号(...)按钮来展开 Text Appearance 组。小贴士
Scale-independent pixels (sp)类似于 dp 单位,但它们也会根据用户的字体大小偏好进行缩放。Android 允许用户在设置中的可访问性部分选择字体大小。当使用 sp 指定字体大小时,Android 不仅会在缩放文本时考虑屏幕密度,还会考虑用户的可访问性设置。建议使用 sp 来指定字体大小。
-
使用相同的技术将另一个
TextView添加到LinearLayout视图组中,除了将新小部件拖动到nameTextView的底部边缘,直到它变成蓝色虚线,然后放下。这将导致第二个TextView添加到nameTextView下方。设置字体大小为14sp。 -
将新添加的
TextView的 ID 更改为addrTextView。 -
现在,将
nameTextView和addrTextView的示例文本更改为POI 名称、城市、州、邮政编码。 -
要编辑
TextView中显示的文本,只需在内容面板上双击小部件。这将启用一个小型编辑器,允许您直接输入文本。或者,您可以通过在属性面板的小部件部分的文本属性中输入值来更改文本。 -
在
Resources/values/string.xml文件中声明所有静态字符串是一种设计实践。通过在strings.xml文件中声明字符串,您可以轻松地将整个应用程序翻译成支持其他语言。让我们将以下字符串添加到string.xml文件中:<string name="poi_name_hint">POI Name</string> <string name="address_hint">City, State, Postal Code.</string> -
您现在可以通过选择属性面板的小部件部分的文本属性旁边的省略号(…)按钮来更改
nameTextView和addrTextView的文本属性。注意这将打开一个对话框窗口,列出了在string.xml文件中声明的所有字符串。为两个TextView对象选择适当的字符串。 -
现在,让我们切换到代码视图,看看布局中添加了什么。注意在
LinearLayout内部添加的以下代码行:<TextView p1:layout_width="match_parent" p1:layout_height="wrap_content" p1:id="@+id/nameTextView " p1:textSize="20sp" p1:text="@string/app_name" /> <TextView p1:text="@string/address_hint" p1:layout_width="match_parent" p1:layout_height="wrap_content" p1:id="@+id/addrTextView " p1:textSize="14sp" />
添加距离TextView
添加一个TextView来显示从 POI 的距离:
-
在工具箱中定位
TextView,并将一个TextView添加到布局中。这个TextView需要锚定到RelativeLayout视图组的右侧,但没有办法通过视觉方式完成;因此,我们将使用多步骤过程。最初,通过将其拖动到左侧,直到边缘变成虚线蓝色,然后将它放下,将TextView与LinearLayout视图组的右侧边缘对齐。 -
在属性面板的小部件部分,将小部件命名为
distanceTextView,并设置字体大小为14sp。 -
在属性面板的布局部分,将Align Parent Right设置为true,Center Vertical设置为true,并在To Right Of布局属性中清除
linearLayout1视图组名称。 -
将示例文本更改为204 英里。为此,让我们在
string.xml中添加一个新的字符串条目,并从属性面板的小部件部分的文本属性中设置文本属性。
以下截图显示了此时内容视图应该显示的内容:

切换回布局设计器中的源选项卡,注意为POIListItem.axml布局生成的以下代码:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="match_parent"
p1:layout_height="wrap_content"
p1:id="@+id/relativeLayout1"
p1:padding="5dp">
<ImageView
p1:src="img/ic_menu_gallery"
p1:layout_width="65dp"
p1:layout_height="65dp"
p1:layout_marginRight="5dp"
p1:id="@+id/poiImageView" />
<LinearLayout
p1:orientation="vertical"
p1:layout_width="wrap_content"
p1:layout_height="wrap_content"
p1:layout_toRightOf="@id/poiImageView"
p1:id="@+id/linearLayout1"
p1:layout_centerVertical="true">
<TextView
p1:layout_width="match_parent"
p1:layout_height="wrap_content"
p1:id="@+id/nameTextView "
p1:textSize="20sp"
p1:text="@string/app_name" />
<TextView
p1:text="@string/address_hint"
p1:layout_width="match_parent"
p1:layout_height="wrap_content"
p1:id="@+id/addrTextView "
p1:textSize="14sp" />
</LinearLayout>
<TextView
p1:text="@string/distance_hint"
p1:layout_width="wrap_content"
p1:layout_height="wrap_content"
p1:id="@+id/textView1"
p1:layout_centerVertical="true"
p1:layout_alignParentRight="true" />
</RelativeLayout>
创建 PointOfInterest 应用程序实体类
首先需要的类是代表应用程序主要焦点的 PointofInterest 类。POIApp 将允许 Point Of Interest 应用程序捕获以下属性:
-
ID -
名称 -
描述 -
地址 -
纬度 -
经度 -
图片
POI 实体类可能只是一个简单的 .NET 类,它包含这些属性。
要创建 POI 实体类,请执行以下步骤:
-
从 Xamarin Studio 的解决方案资源管理器中选择
POIApp项目。选择POIApp项目,而不是解决方案,它是 解决方案 选项卡中的顶级节点。 -
右键单击并选择 新建文件。
-
在 新建文件 对话框的左侧,选择 通用。
-
在模板列表的顶部,在对话框的中间,选择 空类 (C#)。
-
输入名称
PointOfInterest并单击 确定。该类将在POIApp项目文件夹中创建。 -
将类的可见性更改为公共,并根据之前确定的列表填写属性。
以下代码片段来自本书提供的代码包中的 \POIApp\POIApp\PointOfInterest.cs:
public class PointOfInterest
{
public int Id { get; set;}
public string Name { get; set; }
public string Description { get; set; }
public string Address { get; set; }
public string Image { get; set; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
}
注意,纬度和经度属性都被标记为可空。在纬度和经度的案例中,(0, 0) 实际上是一个有效的位置,因此空值表示这些属性从未被设置。
填充 ListView 项
所有适配器视图,如 ListView 和 GridView,都使用一个 Adapter 作为数据与视图之间的桥梁。Adapter 遍历内容并为列表中的每个数据项生成视图。
Android SDK 提供了三种不同的适配器实现,如 ArrayAdapter、CursorAdapter 和 SimpleAdapter。ArrayAdapter 期望输入一个数组或列表,而 CursorAdapter 接受 Cursor 的实例,SimpleAdapter 将资源中定义的静态数据映射。适合您应用程序的适配器类型纯粹基于输入数据类型。
BaseAdapter 是所有三种适配器类型的泛型实现,它实现了 IListAdapter、ISpinnerAdapter 和 IDisposable 接口。这意味着 BaseAdapter 可以用于 ListView、GridView 或 Spinners。
对于 POIApp,我们将创建 BaseAdapter<T> 的子类型,因为它满足我们的特定需求,在许多场景中表现良好,并允许使用我们的自定义布局。
创建 POIListViewAdapter
为了创建 POIListViewAdapter,我们将首先创建一个自定义适配器,如下所示:
-
创建一个名为
POIListViewAdapter的新类。 -
打开
POIListViewAdapter类文件,将类设为公共类,并指定它继承自BaseAdapter<PointOfInterest>。
现在适配器类已经创建,我们需要提供一个构造函数并实现四个抽象方法。
实现构造函数
让我们实现一个构造函数,它接受我们将需要用于填充列表的所有信息。
通常,你需要传递至少两个参数:一个活动实例,因为我们需要在访问标准公共资源时使用活动上下文,以及一个可以枚举的输入数据列表,用于填充 ListView。以下代码展示了代码包中的构造函数:
private readonly Activity context;
private List<PointOfInterest> poiListData;
public POIListViewAdapter (Activity _context, List<PointOfInterest> _poiListData)
:base()
{
this.context = _context;
this.poiListData = _poiListData;
}
实现 Count { get }
BaseAdapter<T> 类提供了一个只读 Count 属性的抽象定义。在我们的情况下,我们只需要提供 poiListData 中提供的 POI 的数量。以下代码示例展示了代码包中的实现:
public override int Count {
get {
return poiListData.Count;
}
}
实现 GetItemId()
BaseAdapter<T> 类提供了一个返回数据源中行长 ID 的方法的抽象定义。我们可以使用 position 参数访问列表中的 POI 对象并返回相应的 ID。以下代码示例展示了代码包中的实现:
public override long GetItemId (int position)
{
return position;
}
实现 index getter 方法
BaseAdapter<T> 类提供了一个基于索引参数返回类型对象的索引获取方法的抽象定义。我们可以使用位置参数从 poiListData 访问 POI 对象并返回一个实例。以下代码示例展示了从代码包中的实现:
public override PointOfInterest this[int index] {
get{
return poiListData [index];
}
}
实现 GetView()
BaseAdapter<T> 类提供了一个 GetView() 的抽象定义,它返回一个表示 ListView 项中单个行的视图实例。与其他场景一样,你可以选择完全在代码中构建视图,或者从布局文件中填充它。我们将使用之前创建的布局文件。以下代码示例展示了从布局文件中填充视图:
view = context.LayoutInflater.Inflate (Resource.Layout.POIListItem, null, false);
Inflate 的第一个参数是一个资源 ID,第二个参数是一个根 ViewGroup,在这个例子中可以留为 null,因为视图将在返回时被添加到 ListView 项中。
重复使用行视图
GetView() 方法会在源数据集的每一行上被调用。对于行数众多的数据集,比如数百行甚至数千行,为每一行创建一个单独的视图将需要大量的资源,而且由于在任何给定时间只有少数几行是可见的,这看起来似乎是浪费的。AdapterView 架构通过将行视图放入一个队列中来解决这个需求,这些视图在滚动出用户视野时可以被重复使用。GetView() 方法接受一个名为 convertView 的参数,其类型为 view。当一个视图可供重复使用时,convertView 将包含对该视图的引用;否则,它将是 null,此时应该创建一个新的视图。以下代码示例展示了如何使用 convertView 来促进行视图的重复使用:
var view = convertView;
if (view == null){
view = context.LayoutInflater.Inflate (Resource.Layout.POIListItem, null);
}
填充行视图
现在我们已经有一个视图实例,我们需要填充字段。View 类定义了一个命名的 FindViewById<T> 方法,它返回视图中包含的小部件的泛型实例。您通过传递在布局文件中定义的资源 ID 来指定您希望访问的控制。
以下代码返回对 nameTextView 的访问权限并设置 Text 属性:
PointOfInterest poi = this [position];
view.FindViewById<TextView>(Resource.Id.nameTextView).Text = poi.Name;
填充 addrTextView 稍微复杂一些,因为我们只想使用我们拥有的地址部分,并且当地址的任何组件都不存在时,我们想要隐藏 TextView。
View.Visibility 属性允许您控制视图的可见性属性。在我们的情况下,如果我们没有地址的任何组件,我们想要使用 ViewState.Gone 值。以下代码展示了 GetView 中的逻辑:
if (String.IsNullOrEmpty (poi.Address)) {
view.FindViewById<TextView> (Resource.Id.addrTextView).Visibility = ViewStates.Gone;
} else{
view.FindViewById<TextView>(Resource.Id.addrTextView).Text = poi.Address;
}
填充距离文本视图的值需要了解位置服务。我们需要进行一些计算,考虑到用户的当前位置与 POI 的纬度和经度。这部分将在第九章 使 POIApp 具有位置感知能力 中介绍。
填充列表缩略图图像
图像下载和处理是一个复杂的过程。您需要考虑各种方面,例如网络逻辑,从服务器下载图像,为了性能而缓存下载的图像,以及为了避免内存溢出条件而调整图像大小。我们不必为执行所有上述任务编写自己的逻辑,我们可以使用 UrlImageViewHelper,这是一个在 Xamarin 组件商店中可用的免费组件。
Xamarin 组件商店提供了一套可重用的组件,包括免费和付费组件,可以轻松地集成到任何基于 Xamarin 的应用程序中。
使用 UrlImageViewHelper
以下步骤将指导您从 Xamarin 组件商店添加组件的过程:
-
要在
POIApp中包含UrlImageViewHelper组件,您可以在 解决方案 面板中双击Components文件夹,或者右键单击并选择 编辑组件。 -
注意,组件管理器将加载已下载的组件以及一个 获取更多组件 按钮,允许您从窗口中打开 组件 商店。请注意,要访问组件管理器,您需要登录您的 Xamarin 账户!
![img/SwDRCFYC.jpg]()
-
在左侧窗格中可用的组件搜索框中搜索
UrlImageViewHelper。现在点击下载按钮以添加您的 Xamarin Studio 解决方案。 -
现在我们已经添加了
UrlImageViewHelper组件,让我们回到POIListViewAdapter类中的GetView()方法。让我们看一下以下代码段:var imageView = view.FindViewById<ImageView> (Resource.Id.poiImageView); if (!String.IsNullOrEmpty (poi.Address)) { Koush.UrlImageViewHelper.SetUrlDrawable (imageView, poi.Image, Resource.Drawable.ic_placeholder); }
让我们来看看前面的代码片段是如何工作的:
-
在
UrlImageViewHelper组件中定义的SetUrlDrawable()方法提供了一条使用单行代码下载图像的逻辑。它接受三个参数:一个imageView实例,图像下载后将在其中显示,图像源 URL 和占位图像。 -
将新的图像
ic_placeholder.png添加到drawable Resources目录。在图像下载期间,占位图像将在imageView上显示。 -
在网络上下载图像需要互联网权限。以下部分将指导您在
AndroidManifest.xml文件中定义权限的步骤。
添加互联网权限
在访问某些功能时,例如从互联网下载数据、在存储中保存图像等,Android 应用必须被授予权限。您必须在AndroidManifest.xml文件中指定应用所需的权限。这允许安装程序在安装时向潜在用户展示应用所需的权限集合。
要设置适当的权限,执行以下步骤:
-
双击Solution面板中Properties目录下的
AndroidManifest.xml。文件将在 manifest 编辑器中打开。屏幕底部有两个标签:Application和Source,可以用来在编辑文件的表单视图和原始 XML 之间切换,如下面的截图所示:![img/WwoNP5Zc.jpg]()
-
在Required permissions列表中,勾选Internet并导航到File | Save。
-
切换到Source视图以查看 XML,如下所示:
![img/TcyeJQCV.jpg]()
连接 POIListViewAdapter
我们已经准备好了列表布局和适配器;现在让我们继续将数据连接到poiListView。我们需要切换回POIListActivity类并添加以下更改:
-
在
POIListActivity类内部声明以下变量:private ListView poiListView; private ProgressBar progressBar; private List<PointOfInterest> poiListData; private POIListViewAdapter poiListAdapter; -
现在,在
OnCreate方法中,实例化ListView和ProgressBar:poiListView = FindViewById<ListView> (Resource.Id.poiListView); progressBar = FindViewById<ProgressBar> (Resource.Id.progressBar); -
目前,我们将创建一个
Async方法,该方法负责从服务器下载数据并在POIListActivity中显示。将以下方法添加到POIListActivity类中:public async void DownloadPoisListAsync(){ } -
从
OnCreate()活动生命周期回调中调用DownloadPoisListAsync()方法。 -
注意,本章使用 Android 设备的网络功能从 REST 网络服务下载数据。本章的以下部分将详细介绍如何进行网络请求从服务器获取数据。
现在为了测试目的,让我们添加以下方法,该方法提供了一个虚拟的 POI 列表对象。在本章的后面部分,我们将集成 REST 网络服务时移除此方法:
private List<PointOfInterest> GetPoisListTestData(){ List<PointOfInterest> listData = new List<PointOfInterest> (); for(int i=0; i<20; i++){ PointOfInterest poi = new PointOfInterest (); poi.Id = i; poi.Name = "Name " + i; poi.Address = "Address " + i; listData.Add (poi); } return listData; } -
让我们在
downloadPoisAsync()方法中添加以下逻辑,使我们的POIApp完全功能化:public async void DownloadPoisListAsync(){ progressBar.Visibility = ViewStates.Visible; poiListData = GetPoisListTestData(); progressBar.Visibility = ViewStates.Gone; poiListAdapter = new POIListViewAdapter (this, poiListData); poiListView.Adapter = poiListAdapter; }
注意以下代码中的内容:
-
下载开始时,向用户显示进度条。下载完成后,隐藏
progressBar。 -
目前,
GetPoiListTestData()方法模拟网络请求并提供 POI 对象列表。 -
数据下载完成后,通过传递下载的 POI 列表结果实例化
POIListViewAdapter类,然后将其设置为列表视图。
我们已经做了大量的工作!现在,让我们在 Android 模拟器或设备上构建并运行应用程序。您将看到以下截图所示的输出:

ListView与测试数据配合得很好。现在,我们需要担心从 REST 网络服务中消耗真实的 POI 数据。以下部分将指导您通过 Android 设备建立 HTTP 网络请求。
消耗互联网服务
到目前为止,我们已经为列表视图和列表适配器创建了布局;现在,我们需要担心如何消耗互联网服务以下载数据并将其连接到屏幕。以下部分将指导您如何从互联网服务异步下载数据。
互联网服务简介
互联网服务是万维网(WWW)基础设施的组成部分之一。它允许服务器应用程序通过 REST 和 SOAP 等网络协议以及 XML 和 JSON 等数据格式,与连接的客户端共享数据或逻辑。
互联网服务公开了一组应用程序编程接口(APIs),为客户端应用程序提供统一的数据访问机制。无论互联网服务是用哪种编程语言编写的,只要客户端应用程序遵循互联网服务 API 规范,无论它们使用的是不同的操作系统或不同的编程语言,都可以无缝地访问服务。例如,用 Java 编写并托管在 Apache Tomcat 上的互联网服务可以被.NET 网络表单、iOS 或 Android 应用程序使用。
SOAP 和 REST 是两个在行业中广泛使用的标准互联网服务架构。Microsoft 开发了 SOAP,而 REST 是由 W3C 技术架构组开发的。虽然 SOAP 带来了自己的协议和额外的安全层,但 REST 实现要容易得多。像 Google 和 Microsoft 这样的公司正在将它们的大部分现有服务迁移到 REST。哪个互联网服务架构更好是一个无休止的辩论,值得 Google 搜索;然而,选择适合您的架构纯粹是一个基于您需求的架构决策。
本书提供的POIApp示例代码将使用 REST 架构消耗用 Java(JAX-RS)开发的互联网服务。以下部分将指导您如何部署和设置您的系统以测试POIApp。
部署 POI 互联网服务
本书提供的代码包包括 POI Web 服务项目代码,这些代码将用于完成本书剩余章节的内容。代码包包括一个readme文件,描述了部署 POI Web 服务所需的步骤。以下部分将指导您完成 readme 文件中的步骤,并在您继续本章之前部署 Web 服务。
POI Web 服务示例应用程序提供了两个 API:一个用于获取服务器上可用的 POI 列表,另一个用于在服务器数据库中创建新的 POI 记录。
在本章中,我们将使用以下 API 规范从 Web 服务器获取 POI 列表:
-
请求方法:
GET -
资源 URI:
/com.packet.poiapp/api/poi/pois -
内容类型:
application/json -
接受类型:
application/json -
响应体:
{ "poi": [ { "description": "The London Eye is a giant Ferris wheel on the South Bank of the River Thames in London", "id": "1", "image": "http://<YOUR_IP>:8080/poiapp/api/poi/image.png", "latitude": "50.59938", "longitude": "80.8897", "address": "London SE17PB, UK", "name": "London Eye" }, { ... ... }, { ... ... } ] }
异步消费 REST Web 服务
在 Xamarin Android 应用程序中消费 REST Web 服务比看起来要简单。有各种框架类,如WebClient、WebRequest、HttpWebRequest、HttpClient,以及其他第三方库,如RestSharp和Service Stack,可供 Xamarin 消费 REST Web 服务。
在开发POIApp时,我们将讨论的重点放在HttpClient上。HttpClient是.NET 4.5 中新引入的,提供了一些高级功能,例如强类型头、共享缓存、缓存控制等。以下部分将指导您如何使用HttpClient从 Xamarin Android 应用程序中异步发起 Web 服务请求。
创建 POIService 类
现在,我们将创建一个标准的 C#类,该类抽象了所有用于消费 REST Web 服务的逻辑,从而使我们的活动看起来更加整洁。要创建新的POIService类,请执行以下步骤:
-
在 Xamarin Studio 的解决方案面板中选择
POIApp项目。 -
右键单击并选择新建文件。
-
在新建文件对话框的左侧选择通用。
-
在模板列表的顶部,在对话框的中间,选择空类(C#)。
-
输入名称
POIService并点击确定。 -
声明一个表示资源 URI 端点的字符串常量,用于访问 POI Web 服务以获取服务器上可用的 POI 列表。在此阶段,我假设 Web 服务代码托管在本地计算机的 8080 端口上。在实际应用中,您的应用程序将使用端点所在的主机域:
private const string GET_POIS = "http://<YOUR_IP>:8080/com.packet.poiapp/api/poi/pois";由于任何技术原因/限制,您无法完成 Web 服务安装,您仍然可以使用 Apiary 模拟数据继续测试您的应用程序。对于使用模拟数据测试,您可以使用以下数据源 URL:
private const string GET_POIS = "http://private-e451d-poilist.apiary-mock.com/com.packt.poiapp/api/poi/pois";
使用 async 和 await 进行异步编程
从服务器下载数据是一个长时间运行并阻塞的操作,对于移动应用来说,建议在主线程之外执行所有此类长时间运行的任务。为了实现响应和流畅的用户体验,移动应用需要为任何长时间运行的操作创建一个新的线程。自从 .NET 4.5 发布以来,async 和 await 关键字被用来轻松实现多线程,而不必在线程上动手。由 async 和 await 关键字定义的方法通常被称为 async 方法。
在实现 async 方法之前,你必须了解以下关键事项:
-
async关键字用于通知 .NET 语言 公共语言运行时 (CLR) 创建一个新的执行线程并异步执行任务。 -
await关键字会自动暂停调用线程,并在新线程上执行任务;一旦任务完成,控制权将返回。单个async方法可以有一个或多个await关键字。 -
async方法始终返回Task<T>,其中 T 代表执行后期望的结果的数据类型。当不需要结果时,可以使用 void 返回类型。 -
作为一种约定,Microsoft 建议将
async方法的名称后缀为async。然而,这并非强制性的,但它有助于提醒调用者使用await关键字。
现在我们已经了解了创建异步方法的基本知识,让我们在 POIService 类中定义一个方法,并将其命名为 GetPOIListAsync。GetPOIListAsync 方法将用于异步消费 REST 网络服务,反序列化 POI 列表集合中的响应,并将结果返回给活动以在列表中显示结果:
public async Task<List<PointOfInterest>> GetPOIListAsync() {
}
注意,GetPOIListAsync 方法的返回类型是 Task<List<PointOfInterest>>,而 List<PointOfInterest> 是在 POIListActivity 上渲染列表视图时期望的结果。
GetPOIListAsync 方法现在创建了一个 HttpClient 实例,将 HTTP 头部接受类型设置为 application/json,并通过传递网络服务 URL 调用 GetAsync() 方法。接受类型头部元数据告诉服务器客户端期望的响应媒体格式。GetAsync() 方法启动对指定端点的 GET 请求,并返回 HttpResponseMessage 实例。
如果响应状态码返回成功 ( 200OK ),我们可以继续获取内容。正如我们从网络服务规范中了解的那样,响应是一个结构化的 JSON 字符串;我们可以通过调用 GetStringAsync 方法来检索值。对于除成功状态码之外的状态码,我们可以放置处理错误情况的逻辑。为了简化这个示例,我们将在控制台上打印错误日志:
HttpClient httpClient = new HttpClient ();
httpClient.DefaultRequestHeaders.Accept.Add (new MediaTypeWithQualityHeaderValue ("application/json"));
HttpResponseMessage response = await httpClient.GetAsync (GET_POIS);
if (response != null || response.IsSuccessStatusCode) {
string content = await response.Content.ReadAsStringAsync ();
Console.Out.WriteLine ("Response Body: \r\n {0}", content);
} else {
Console.Out.WriteLine("Failed to fetch data. Try again later!");
return null;
}
注意,前面用于消费 REST 服务的 GetPOIListAsync 方法需要互联网权限。由于我们已经添加了互联网权限,在下载图片时使用 UrlImageViewHelper Xamarin 组件,我们不需要再次添加权限。
await 关键字期望下载结果继续进行,因此它等待当前的下载任务完成。一旦下载任务完成,POI 网络服务响应 JSON 字符串将被分配给一个内容变量。此时,字符串结果需要反序列化为 .NET 对象,并将结果返回给 POIListActivity。以下部分将指导您如何将内容字符串反序列化为 JSON 对象。
虽然 HttpClient 与 async 和 await 关键字结合提供了处理异步网络请求的本地 API 支持,但您也可以利用功能强大且流行的框架,如 Service Stack 或 Rest Sharp。以下是一些您可以参考的链接:
使用 Json.NET 进行序列化和反序列化
在序列化和反序列化 JSON 数据时,我们需要做出的另一个重要决定是如何将响应字符串转换为 .NET 对象,反之亦然。有多个选项可供选择,包括来自 .NET 的 DataContractJsonSerailzier。Json.NET 是由 James Newton-King 创建的开源组件库,这绝对值得考虑,原因如下:
-
它体积小、速度快、可靠
-
它作为免费组件在 Xamarin 组件商店和 NuGet 中可用
-
它使简单任务变得极其简单
考虑到这些特性,我们将通过将 Json.NET 组件添加到 POIApp 从 Xamarin 组件商店中继续操作。要添加 Json.NET 组件,请遵循我们添加 UrlImageViewHelper 组件时执行的相同步骤。
一旦将 Json.NET 组件添加到解决方案中,下一步是将响应字符串转换为 PointOfInterest 对象的列表。
-
在
POIListActivity.cs文件中包含以下命名空间指令:using Newtonsoft.Json.Linq; using System.Collections.Generic; using Newtonsoft.Json; using System.Linq; -
在
GetPOIListAsync()方法中声明一个列表集合以保存 POI 列表响应:private List<PointOfInterest> poiListData = null; -
在
downloadPoisAsync()方法中Console.Out.WriteLine语句之后初始化poiListData列表集合。放置以下代码片段:poiListData = new List<PointOfInterest> ();
现在,是时候将 JSON 字符串反序列化为 .NET 对象了。请注意,响应 JSON 字符串是一个具有 pois 键的对象,该键代表 PointOfInterest 对象的数组。将以下代码片段添加到 GetPOIListAsync() 方法中以将字符串反序列化为 .NET 对象。
以下行代码将完整的 JSON 响应字符串转换为 JObject:
JObject jsonResponse = JObject.Parse (content);
现在获取 pois 键的值并遍历它以转换为 .NET 列表:
IList<JToken> results = jsonResponse ["pois"].ToList ();
foreach (JToken token in results) {
PointOfInterest poi = token.ToObject<PointOfInterest>();
poiListData.Add (poi);
}
return poiListData;
JToken 是任何类型的 JSON 值的通用表示。它可以是一个字符串、对象、数组、属性等等。ToList() 方法返回 JToken 对象的集合。token.ToObject 方法将每个 poiJSON 标记转换为 PointOfInterest 对象类型,并将其添加到 poiListData 集合中。最后,我们将列表结果返回给调用者。
更新 POIListActivity
到目前为止,我们已经下载了数据,并且我们准备在 POIListActivity 中使用这些数据。让我们在 POIListActivity 中进行以下更改:
-
从
POIListActivity类中删除GetPoiListTestData()方法。 -
在
DownloadPoisListAsync方法中创建POIService的实例并调用GetPOIListAsync方法:public async void DownloadPoisListAsync(){ progressBar.Visibility = ViewStates.Visible; POIService service = new POIService (); poiListData = await service.GetPOIListAsync (); progressBar.Visibility = ViewStates.Gone; poiListAdapter = new POIListViewAdapter (this, poiListData); poiListView.Adapter = poiListAdapter; }
我们已经做了很多工作;现在是时候编译并运行应用程序了。使用我们在前几章中使用的程序编译并运行应用程序。
Bingo!你会注意到应用程序将从 POI 网络服务下载数据并在可滚动的列表视图中显示 POI 列表:

向 ActionBar 添加操作
从 Android 3.0(Honeycomb,API 级别 11)开始,Android 引入了一个统一标题,例如一个停靠在屏幕顶部的小部件,称为 ActionBar。它允许应用程序在设备屏幕顶部(状态栏下方)添加特定于活动的操作。我们将为 POIListActivity 类定义两个操作:新建,用于创建新的 POI,以及 刷新,用于刷新设备本地存储中的 POI 缓存。
Activity 类提供了以下虚拟方法,可以重写以添加操作:
虚拟方法
描述
OnCreateOptionsMenu
它允许通过 API 调用或通过填充 XML 定义来创建操作
OnOptionsItemSelected
当点击 ActionBar 中的操作时调用
定义菜单 XML 文件
可以在位于 Resources/menu 文件夹中的菜单 XML 文件中定义操作,或者可以通过 API 调用程序化创建。我们将在名为 POIListViewMenu.xml 的 XML 文件中定义 新建 和 刷新 操作。
要创建 POIListViewMenu.xml,请执行以下步骤:
-
在
POIApp中选择Resources文件夹,右键单击它,然后导航到 添加 | 新建文件夹。 -
将文件夹命名为菜单。
-
选择菜单文件夹,右键单击它,然后导航到 添加 | 新建文件。
-
导航到 XML | 空 XML 文件,输入
POIListViewMenu.xml作为名称,然后单击 新建。
您现在需要填写我们确定的两个操作的定义。不幸的是,Xamarin Studio 不包含菜单 XML 文件的模板,因此您必须从 Android 文档或在线示例中查找格式。以下代码包含了 actionNew 和 actionRefresh 的定义:
<menu >
<item android:id="@+id/actionNew"
android:icon="@drawable/ic_new"
android:title="New"
android:showAsAction="ifRoom" />
<item android:id="@+id/actionRefresh"
android:icon="@drawable/ic_refresh"
android:title="Refresh"
android:showAsAction="ifRoom" />
</menu>
注意,从菜单定义中,我们已经引用了两个新的可绘制资源:ic_new 和 ic_refresh。我们需要以与第三章“创建兴趣点应用”中为 ic_app 图标所做的方式相同,将这些图像添加到项目中。这些图像可以在 assets 位置下的 drawable 文件夹中找到。
在 OnCreateOptionsMenu() 中设置菜单
OnCreateOptionsMenu() 方法被调用,以给 Activity 参数一个定义 ActionBar 上的操作的机会。Activity 类提供了一个 MenuInflater 方法,它读取 XML 定义文件并将定义在 ActionBar 上的操作放置进去。以下代码显示了代码块中的实现:
public override bool OnCreateOptionsMenu(IMenu menu)
{
MenuInflater.Inflate(Resource.Menu.POIListViewMenu, menu);
return base.OnCreateOptionsMenu(menu);
}
在 OnOptionsItemSelected() 中处理选择
当点击 ActionBar 中的操作时,会调用 OnOptionsItemSelected() 方法,并传递一个 IMenuItem 实例。IMenuItem ItemId 实例对应于项目定义中指定的 ID,可用于确定点击了哪个操作。以下代码显示了代码块中 OnOptionsItemSelected() 的实现:
public override bool OnOptionsItemSelected (IMenuItem item)
{
switch (item.ItemId)
{
case Resource.Id.actionNew:
// place holder for creating new poi
return true;
case Resource.Id.actionRefresh:
DownloadPoisListAsync(url);
return true;
default :
return base.OnOptionsItemSelected(item);
}
}
注意,我们只是为 actionNew 创建了一个占位符,并为 actionRefresh 放置了两个方法调用。
调用 DownloadPoisListAsync() 方法来下载并刷新列表上的数据。
现在运行 POIApp 并注意 POI 列表活动标题上的两个按钮,添加 和 刷新:

处理 ListView 点击事件
当用户点击一行时,POI 应用将导航到一个详细视图,以便您查看和更新完整的信息集。我们将在下一章构建详细视图,但现在我们将讨论如何处理点击。
可以使用传统的事件处理器来处理点击。ListView 项目提供了一个 ItemClick 事件处理器,它接受一个 ListView.ItemClickEventArgs 参数。ListView.ItemClickEventArgs 参数提供了以下信息,可用于处理事件:
属性
描述
ID
它是点击的行的相关数据的 ID。这将是从 GetItemId() 返回的值。
位置
它是被点击行的 ListView 项中的位置。
视图
它是被点击行的相关视图。这将是从 GetView() 返回的视图。
父级
它是包含被点击行的 AdapterView 架构。在我们的例子中,它是 ListView。
在 POIListActivity 中创建一个事件处理器来处理 ListView 项的点击事件。由于我们尚未创建详细视图,所以我们现在只展示一个 Toast 消息。以下代码来自代码块:
protected void POIClicked(object sender, ListView.ItemClickEventArgs e)
{
// Fetching the object at user clicked position
PointOfInterest poi = result.poiListData[(int)e.Id];
Console.Out.WriteLine("POI Clicked: Name is {0}", poi.Name);
}
我们还需要连接事件处理器。将以下行代码添加到 OnCreate 方法的末尾:
poiListView.ItemClick += POIClicked;
运行 POIApp 项目并点击一个 POI;注意相应的行中的 POI 名称被打印在控制台上。
处理无网络条件
移动设备中的网络条件是不确定的。有时,用户会手动禁用网络连接,或者由于各种外部原因而不可用。对于使用网络数据的应用程序,您必须处理不同的网络状态。应用程序应通过向用户显示适当的消息来优雅地做出反应。
在 POIApp 中,在开始下载之前,我们必须确认网络数据连接的可用性。如果网络不可用,我们应该使用适当的消息通知用户,否则继续进行下载请求。
存在于 System.Net 包中的 ConnectivityManager 类可以用来查询设备网络连接的状态。此类还可以用来监控网络连接,并在网络状态发生变化时通知。在我们的案例中,我们只是查询网络信息,以便知道网络是可用的。
使用 ConnectivityManager 类访问网络状态需要在 AndroidManifest.xml 文件中请求 ACCESS_NETWORK_STATE 用户权限。按照我们之前添加互联网权限时的相同步骤进行,或者您可以直接将以下代码添加到 AndroidManifest.xml 源编辑器中:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
现在将以下实用方法添加到 POIService 类中,该方法读取网络信息,如果设备已连接则返回 true:
public bool isConnected(Context activity){
var connectivityManager = (ConnectivityManager)activity.GetSystemService (Context.ConnectivityService);
var activeConnection = connectivityManager.ActiveNetworkInfo;
return (null != activeConnection && activeConnection.IsConnected);
}
现在请对 POIService 类中的 DownloadPoisListAsync 方法进行以下更改:
public async void DownloadPoisListAsync(){
POIService service = new POIService ();
if (!service.isConnected (this)) {
Toast toast = Toast.MakeText (this, "Not conntected to internet. Please check your device network settings.", ToastLength.Short);
toast.Show ();
} else {
progressBar.Visibility = ViewStates.Visible;
poiListData = await service.GetPOIListAsync ();
progressBar.Visibility = ViewStates.Gone;
poiListAdapter = new POIListViewAdapter (this, poiListData);
poiListView.Adapter = poiListAdapter;
}
}
你一定会对 Toast 感到惊讶。嗯,我们将在下一节中学习它。
Toast
Toast 是一个非交互式、自动可丢弃的视图,用于在指定时间内显示一条简短的消息,并自行销毁。Android 推荐您仅使用 Toast 来通知用户,在这种情况下,用户的注意力不是强制性的。对于任何需要用户注意或交互的通知,请考虑使用对话框。
创建Toast很简单,您只需通过传递三个参数调用MakeText()静态方法:应用程序上下文、要显示的消息以及Toast显示的时间长度。时间长度是一个非负整数,单位为毫秒,但建议您使用在ToastLength枚举中定义的标准Long和Short常量。MakeText()方法使用给定的属性初始化Toast,并返回一个Toast实例,我们可以调用其Show()方法来显示Toast。
在前面的代码片段中,我们在设备离线时显示Toast消息。以下代码展示了调用MakeText()和Show()方法在Delete操作上显示Toast:
Toast toast = Toast.MakeText (this, "Not conntected to internet. Please check your device network settings.", ToastLength.Short);
toast.Show ();
摘要
在本章中,我们详细介绍了如何使用不同的布局管理器和控件(如TextView、ImageView、ProgressBar和ListView)来创建用户界面元素。
我们还介绍了如何使用 Xamarin.Android 的HttpClient类来消费 REST 网络服务,使用 Json.NET 组件反序列化 JSON 响应,并在屏幕上填充数据。
下一章将向您介绍更多视图组,并构建更复杂的用户界面。我们将继续使用POIApp,添加详细视图,并允许用户创建或删除兴趣点。
第五章。添加详情视图
在本章中,我们将向您介绍创建一个新活动以显示所选 POI 详情的过程。此活动还允许用户从服务器创建、更新和删除 POI。本章将涵盖以下主题:
-
创建用于显示 POI 详情的布局和活动
-
使用
LinearLayout、TableLayout和ScrollView来布局活动 -
使用 Intent 捆绑在活动之间传递数据
-
消耗 Web 服务以执行 HTTP
POST和DELETE操作 -
使用
EditText.Error属性执行EditText验证 -
显示确认提示
创建 POIDetail 布局
到目前为止,我们已经构建了POIApp来显示从服务器获取的 POI 列表。目前,列表视图仅显示有关 POI 的有限信息,例如名称、地址、图像和距离。现在让我们通过添加另一个活动来扩展POIApp,该活动显示所选 POI 的详细信息。当用户从列表中点击任何 POI 项或用户从导航栏中选择新建(+)操作时,将显示 POI 详情活动。此外,详情活动将允许用户创建新的 POI、更新或删除现有 POI。
考虑到所有前面的用例,让我们为 POI 详情创建一个新的布局:
-
在解决方案面板中选择
Resources/layout文件夹。 -
右键单击添加并选择新建文件。
-
在新建文件对话框中,点击Android并选择布局,在名称字段中输入 POI 详情,然后选择新建。
注意,已创建了一个新文件,其顶级容器为LinearLayout。POIDetail视图将包含多个字段,并且在小屏幕尺寸的设备上可能需要滚动。默认的 Android 布局管理器,如LinearLayout、RelativeLayout、FrameLayout或TableLayout,在内容增长且数据超出其实际屏幕大小时不提供自动滚动。
在我们之前的章节中,为了显示 POI 列表,布局为我们提供了免费滚动,因为我们使用了ListView;然而,在 POI 详情活动的案例中,我们需要使用ScrollView来使项目可滚动。
理解ScrollView
ScrollView是一种特殊的布局,用于容纳比其实际尺寸更大的视图。当子视图的大小超过ScrollView大小时,它会自动添加滚动条并可以垂直滚动。在使用ScrollView之前,你必须了解以下关键事项:
-
ScrollView最多可以容纳一个直接子项。这意味着如果您有一个具有多个子项的复杂布局,那么您必须将它们包含在另一个标准布局中,例如LinearLayout、TableLayout或RelativeLayout。 -
与任何其他标准布局管理器一样,可以使用
layout_height和layout_width属性来调整ScrollView的高度和宽度。 -
ScrollView对于需要滚动的屏幕来说很理想,但当滚动视图用于渲染大量数据时,它将增加开销。在这种情况下,您应考虑使用专门的适配器视图,如ListView和GridView。 -
永远不要在
ScrollView内放置ListView或GridView,因为它们都负责自己的垂直滚动。这样做的话,ListView子项将永远不会接收到手势,因为它将由父ScrollView处理。 -
ScrollView仅支持垂直滚动。对于水平滚动,可以使用HorizontalScrollView。 -
android:fillViewport属性定义了ScrollView是否应该将其内容拉伸以填充视口。您可以通过在ScrollView上调用setFillViewport(true)方法来设置相同的属性。
现在我们已经了解了ScrollView,让我们回到POIDetail布局,并添加一个ScrollView以支持小屏幕尺寸设备的垂直内容滚动:
-
在
Content视图中打开POIDetail.xaml文件,选择顶级LinearLayout并按Delete键。 -
在工具箱面板中,找到ScrollView小部件并将其拖动到内容视图。
-
在工具箱面板中,找到LinearLayout(垂直)小部件并将其拖动到
ScrollView内的内容视图中。 -
在选择
LinearLayout后,在属性面板的布局部分设置填充为5dp。
现在我们已经准备好向布局中添加标签和编辑控件。以下截图显示了我们试图实现的布局:

我们将使用简单的TextView小部件作为标签,并使用EditText小部件作为输入控件。EditText小部件包含一些可以用来自定义其行为的属性。其中一个属性名为InputType,它控制输入时使用的键盘类型(字母、数字等)以及允许的文本行数。工具箱面板在文本字段组名下提供了一些模板或预配置的EditText小部件。
以下截图显示了列表:

将一系列TextView和EditText控件添加到名称、描述和地址字段中。根据以下表格命名EditText小部件,并使用相应的工具箱小部件,以便应用适当的编辑特性:
名称
小部件工具箱名称
nameEditText
纯文本
descrEditText
多行文本
addrEditText
多行文本
现在我们准备处理纬度和经度字段,我们将使用一个新的布局管理器TableLayout。
使用 TableLayout 管理器
TableLayout管理器是LinearLayout的扩展。正如其名所示,TableLayout用于以行和列格式对子视图元素进行对齐。
TableLayout的概念与 HTML 表格类似。TableLayout由<table>标签组成,TableRow类似于<tr>元素。您可以在表格单元格内添加任何视图或视图组。
我们想在表格中添加纬度和经度字段。让我们添加一个两行两列的TableLayout,顶部行用于标签,底部行用于编辑字段。为了做到这一点,请执行以下步骤:
-
在工具箱面板中找到
TableLayout,将其拖放到addrEditText小部件下方的内容视图中,并放下。将创建一个三行三列的TableLayout。 -
在
TableLayout中选择一行,右键点击并选择删除行。 -
在
TableLayout中选择一列,右键点击并选择删除列。 -
选择第一列,右键点击并选择拉伸列。同样,对第二列也进行此操作。
您现在应该有一个TableLayout,其可见轮廓为两行,每行有两个列,如下面的截图所示:

我们现在需要添加第一行中两个标签的TextView小部件和用于纬度和经度编辑控制的数字(十进制)小部件,分别命名为latEditText和longEditText。
我们现在已经完成了POIDetail布局,您看到的内容视图应该与前面的截图相同。
使用 EditText 的 InputType
EditText元素提供了一个名为InputType的属性,该属性检查在输入数据时控件的行为。当添加描述和地址小部件时,我们从工具箱面板选择了多行文本。以下代码显示在这种情况下inputType被自动设置:
<EditText
android:inputType="textMultiLine"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/descrEditText" />
InputType 属性也可以在输入格式部分的Widget选项卡下的属性面板中设置或更改。可能不明显的是,inputType可以组合值,这在我们的情况下非常有用。以下表格显示了inputType的合理值集;请随意实验:
小部件
输入类型
nameEditText
inputType="textCapWords"
descrEditText
inputType="textMultiLine|textCapSentences"
addrEditText
inputType="textMultiLine"
latEditText
inputType="numberDecimal|numberSigned"
longEditText
inputType="numberDecimal|numberSigned"
切换到代码视图以查看已添加到布局的内容:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="match_parent"
p1:layout_height="match_parent"
p1:id="@+id/scrollView1">
<LinearLayout
p1:orientation="vertical"
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="fill_parent"
p1:layout_height="fill_parent"
p1:id="@+id/linearLayout1"
p1:padding="5dp">
<TextView
p1:text="Name"
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/textView10" />
<EditText
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/nameEditText"
p1:inputType="textCapWords" />
<TextView
p1:text="Description"
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/textView11" />
<EditText
p1:inputType="textMultiLine|textCapSentences"
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/descrEditText" />
<TextView
p1:text="Address"
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/textView12" />
<EditText
p1:inputType="textMultiLine"
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/addrEditText" />
<TableLayout
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/tableLayout1"
p1:stretchColumns="*">
<TableRow
p1:id="@+id/tableRow2">
<TextView
p1:text="Latitude"
p1:layout_column="0"
p1:id="@+id/textView21" />
<TextView
p1:text="Longitude"
p1:layout_column="1"
p1:id="@+id/textView22" />
</TableRow>
<TableRow
p1:id="@+id/tableRow3">
<EditText
p1:inputType="numberDecimal|numberSigned"
p1:layout_column="0"
p1:id="@+id/latEditText" />
<EditText
p1:inputType="numberDecimal|numberSigned"
p1:layout_column="1"
p1:id="@+id/longEditText" />
</TableRow>
</TableLayout>
</LinearLayout>
</ScrollView>
创建 POIDetailActivity
现在我们已经准备好了POIDetail布局,我们需要一个相应的活动。按照以下步骤创建一个新的 POI 详情活动:
-
在解决方案面板中选择
POIApp项目,右键点击并导航到添加 | 新建文件。 -
在 新建文件 对话框中,点击 Android 并选择 Activity,将 名称 输入为
POIDetailActivity,然后点击 新建。
如您从第三章中回忆起的,在创建活动时需要做的第一件事之一是设置布局内容,这是通过调用 SetContentView(layoutId) 来实现的。将以下行代码添加到 POIDetailActivity 的 OnCreate() 方法中:
SetContentView (Resource.Layout.POIDetail);
将导航添加到 POIDetailActivity
有两种情况需要从 POIListActivity 导航到 POIDetailActivity,要么是通过在导航栏上选择 新建 操作,要么是通过从列表中选择任何 POI。这两种情况的主要区别在于,当从列表中选择现有的 POI 时,我们必须将所选 POI 的详细信息传递给 POIDetailActivity,以便用户可以编辑和更新 POI 记录。在创建新的 POI 时,我们不传递任何数据。让我们先选择最简单的一种,并将导航添加到 新建 操作中。
Activity 类提供了一个名为 StartActivity() 的方法,用于启动新的活动。StartActivity() 方法也可以在许多不同的场景中使用。在 新建 操作的情况下,我们将使用其最基本的形式。要启动一个活动,我们只需要通过传递我们想要启动的活动类型来调用 StartActivity()。
以下代码片段演示了需要在 OnOptionsItemSelected() 方法中添加到 POIListActivity 中占位符的代码:
case Resource.Id.actionNew:
StartActivity (typeof(POIDetailActivity));
return true;
现在我们将应用程序运行在 Android 设备或模拟器上,看看我们到目前为止已经构建了什么。在 POIListActivity 中,点击导航栏上的 新建 按钮,注意新创建的 POIDetailActivity 已经打开:

在 POI 列表项点击时的导航
在第二种情况下,我们需要传递 POI 的详细信息以显示 POIDetailActivity。为了完成这个任务,我们将使用 Intent 类。Intent 类可以与 StartActivity() 结合使用,以便启动新的活动并传递有关要启动的活动的信息。我们将使用 Intent 类来启动 POIDetailActivity 并传递所选 POI 的详细信息。
首先,我们需要通过提供当前活动上下文和将要接收意图的目标活动类型来构造一个 Intent 实例;在我们的情况下,是 POIDetailActivity。以下代码演示了如何正确构造意图:
Intent poiDetailIntent = new Intent (this, typeof(POIDetailActivity));
Intent对象有一个Extras属性,用于将额外数据作为包从一个活动发送到另一个活动。Intent类提供了一系列重载的PutExtra()方法,允许你向Extras属性添加各种类型的键/值对。值可以是任何原始类型,如int、boolean、char、string、double、float、long等。例如,要添加字符串数据类型,可以使用以下语法:
intent.PutExtra ("YOUR_KEY", "SOME STRING VALUE HERE");
对于POIApp,我们需要添加所选PointOfInterest对象的全部属性。而不是使用不同的键传递每个属性,我们可以进一步简化此过程,使用Json.NET组件。它将PointOfInterest .NET 对象序列化为 JSON 字符串,可以通过具有不同键poi传递给POIDetailActivity,在接收端,我们将它反序列化回PointOfInterest对象。
除了使用Intent包外,还有各种其他选项可以在活动之间传递数据。对于复杂对象,使用 Json.NET 组件进行序列化并通过互联网传递数据包不是一个推荐选项。Android intent 包的大小限制大约为 1 MB,因此你应该始终注意你的数据限制。你可以考虑使用其他任何替代方案以获得更好的性能。
我的以下建议如下:
-
将数据写入文件,并只传递文件路径给第二个活动。第二个活动可以使用相同的文件路径访问内容。
-
使用 SQLite 将对象存储在表中。只传递唯一的 ID 或查询一个参数给第二个活动。第二个活动可以通过从数据库中读取来访问数据。
-
创建一个单例类来保存数据。第二个活动可以直接通过单例实例访问数据。
在我们的案例中,POI 对象很小。我们很高兴采用Intent包方法。以下代码语法使用JsonConvert类的SerializeObject()方法将选定的PointOfInterest对象转换为 JSON 字符串,并使用PutExtra()方法将其添加到Intent中:
string poiJson = JsonConvert.SerializeObject (poi);
poiDetailIntent.PutExtra("poi", poiJson);
最后一步是调用StartActivity(),传入我们在早期步骤中创建的Intent类:
StartActivity (poiDetailIntent);
因此,你的 POIListActivity 的OnListItemClick应该有如下代码:
protected void POIClicked(object sender, ListView.ItemClickEventArgs e)
{
PointOfInterest poi = poiListData[(int)e.Id];
Intent poiDetailIntent = new Intent(this, typeof(POIDetailActivity));
string poiJson = JsonConvert.SerializeObject(poi);
poiDetailIntent.PutExtra("poi", poiJson);
StartActivity(poiDetailIntent);
}
既然我们已经从POIListActivity传递了数据,现在让我们从POIDetailActivity类中访问 POI 对象。
在 POIDetailActivity 中接收数据
当我们到达POIDetailActivity的OnCreate()方法时,我们需要访问从POIListActivity发送的PointOfInterest对象。此对象将用于显示所选 POI 的详细信息。以下部分将指导您通过检索Intent中的额外包元数据的过程。
每个活动都有一个包含 intent 和启动活动时传递的相应信息的Intent属性。Intent类提供了一系列方法,通过提供相应的键来访问任何Extras包数据。在我们检索数据之前,我们可以通过调用HasExtra("poi")方法来确认指定键的值是否可用。HasExtra方法返回一个boolean值;如果它返回false,我们可以假设我们正在创建一个新的 POI;否则,我们需要检索额外的值。
Intent类有一系列GetXXExtra()方法,其中XX代表键/值对值的类型。在我们的情况下,我们可以使用GetStringExtra()方法在 intent 上获取从POIListActivity传递过来的poiJson字符串。GetStringExtra()方法接受一个字符串(这是最初在 intent 上设置的键/值对中的键),并返回与该键关联的值。
让我们声明一个PointOfIntrest变量来保存从POIListActivity接收到的 POI 对象:
private PointOfInterest _poi;
将以下列表添加到POIDetailActivity的OnCreate()方法中:
if (Intent.HasExtra ("poi")) {
string poiJson = Intent.GetStringExtra ("poi");
_poi = JsonConvert.DeserializeObject<PointOfInterest>(poiJson);
} else {
_poi = new PointOfInterest ();
}
将变量绑定到控件上
正如我们在上一章所学,我们需要手动将用户界面小部件绑定到内部程序引用,以便操作其内容、分配事件处理器等。为我们在布局中创建的每个输入小部件声明一组私有变量。以下列表来自源文件夹:
private EditText _nameEditText;
private EditText _descrEditText;
private EditText _addrEditText;
private EditText _latEditText;
private EditText _longEditText;
需要调用FindViewById<T>来将每个变量绑定到相应的用户界面小部件。以下列表描述了应该在SetContentView()调用之后添加到OnCreate()方法中的内容:
SetContentView (Resource.Layout.POIDetail);
_nameEditText = FindViewById<EditText> (Resource.Id.nameEditText);
_descrEditText = FindViewById<EditText> (Resource.Id.descrEditText);
_addrEditText = FindViewById<EditText> (Resource.Id.addrEditText);
_latEditText = FindViewById<EditText> (Resource.Id.latEditText);
_longEditText = FindViewById<EditText> (Resource.Id.longEditText);
填充用户界面小部件
到目前为止,我们已经有了一个PointOfInterest对象的引用,但我们还没有采取任何行动来填充 UI 上的内容。在 UI 上填充 POI 详情是一个相当直接的过程。
EditText小部件有一个名为Text的属性,我们可以将其设置为初始化小部件的内容。让我们创建一个名为UpdateUI()的简单方法,它负责在用户界面小部件上填充 POI 详情。
以下列表显示了UpdateUI()所需的内容:
protected void UpdateUI()
{
_nameEditText.Text = _poi.Name;
_descrEditText.Text = _poi.Description;
_addrEditText.Text = _poi.Address;
_latEditText.Text = _poi.Latitude.ToString ();
_longEditText.Text = _poi.Longitude.ToString ();
}
在OnCreate()回调的末尾调用UpdateUI()方法。
现在您应该能够运行POIApp,并通过点击POIListActivity中的任何一行来测试导航。请注意,POIDetailActivity将显示所选 POI 对象的详情:

添加保存和删除操作
使用POIDetailActivity,用户可以选择保存或删除 POI。相同的保存按钮适用于两种场景:当 POI 详情从POIListActivity传递过来时,它将更新 POI 详情;否则,它将创建一条新记录。
我们需要一种从用户界面完成这些任务的方法。让我们使用ActionBar并添加两个操作:Save和Delete。在Resources/menu目录下创建一个名为POIDetailMenu.xml的新文件来声明菜单布局。以下列表显示了POIDetailMenu.xml所需的内容:
<menu >
<item android:id="@+id/actionSave"
android:icon="@drawable/ic_save"
android:title="Save"
android:showAsAction="ifRoom" />
<item android:id="@+id/actionDelete"
android:icon="@drawable/ic_delete"
android:title="Delete"
android:showAsAction="ifRoom" />
</menu>
注意,每个菜单项都有一个指定的图标。这些图标可以在代码包的Assets文件夹中找到。
我们需要重写OnCreateOptionsMenu()和OnOptionsItemSelected()方法。这与我们在第四章添加 ListView中创建的非常相似。将以下代码片段添加到POIDetailActivity类中:
public override bool OnCreateOptionsMenu(IMenu menu)
{
MenuInflater.Inflate(Resource.Menu.POIDetailMenu, menu);
return base.OnCreateOptionsMenu(menu);
}
public override bool OnOptionsItemSelected (IMenuItem item)
{
switch (item.ItemId)
{
case Resource.Id.actionSave:
SavePOI ();
return true;
case Resource.Id.actionDelete:
DeletePOI ();
return true;
default :
return base.OnOptionsItemSelected(item);
}
}
您可能会注意到,在前面的代码片段中,我们引入了两个新方法:SavePOI()和DeletePOI()。这两个方法都用于保持OnOptionsItemSelected()方法简洁。SavePOI()和DeletePOI()方法封装了保存或删除 POI 对象所需的逻辑。
禁用删除操作
在POIDetailView中有一点不同,我们需要一个场景来禁用Delete操作。如果一个新 POI 正在创建,则不应允许删除操作。首先,我们需要在OnPrepareOptionsMenu()方法中获取菜单项的引用,然后我们可以通过传递您的偏好来调用SetEnabled(bool)方法以启用或禁用菜单操作。
IMenu类提供了一个FindItem()方法,可以用来获取特定IMenuItem的引用,该引用提供了用于启用和禁用操作的SetEnabled()方法。禁用菜单项会使按钮失效;然而,它仍然会显示在屏幕上。为了更好的用户体验,让我们通过调用SetVisible(false)方法完全隐藏Delete操作。
以下列表显示了在输入新 POI 时如何禁用Delete操作:
public override bool OnPrepareOptionsMenu (IMenu menu)
{
base.OnPrepareOptionsMenu (menu);
// Disable delete for a new POI
if (_poi.Id<=0) {
IMenuItem item = menu.FindItem (Resource.Id.actionDelete);
item.SetEnabled (false);
item.SetVisible(false);
}
return true;
}
在 POIService 中添加保存和删除操作
在第四章添加 ListView中,我们创建了专门的POIService类,该类使用HttpClient结合async和await关键字处理下载数据的逻辑。目前,POIService类只有一个方法GetPoisListAsync(),它处理从 REST 网络服务获取记录列表。让我们扩展其功能以创建新的或更新和删除 POI。让我们首先从创建 POI 开始。
消费网络服务以创建或更新 POI
到目前为止,我们已经在代码包中部署了提供的网络服务,并且POIApp已经消费了相同的网络服务来获取 POI 列表。在本节中,我们将使用相同的网络服务来创建新的 POI 或更新现有的 POI。
以下 API 规范用于创建新的或更新现有的 POI:
Request Method: POST
Resource Endpoint: /com.packet.poiapp/api/poi/pois
Content-type: application/json
Request Body:
{
"description": "The London Eye is a giant Ferris wheel on the South Bank ….",
"latitude": "50.59938",
"longitude": "80.8897",
"address": "London SE17PB, UK",
"name": "London Eye"
}
Response: Success/Failed
注意前面 API 规范中的关键点:请求方法是POST,Content-type 是application/json,POI JSON 对象作为请求体的一部分发送。POST方法指示服务器应用程序查找附加的请求内容体,Content-Type 描述了服务器即将接收的数据的 MIME 类型。在这种情况下,内容类型是application/json,这意味着服务器期望以 JSON 字符串的形式发送 POI 详细信息。
要创建一个新的 POI,我们需要在创建新的 POI 记录时发送 POI 的详细信息,如名称、描述、纬度、经度和地址。一旦 POI 被创建,将为每个 POI 记录创建并分配一个唯一的 ID。要更新现有的 POI 记录,我们必须在请求体中发送 POI ID 以及更新的 POI 详细信息。
将CreateOrUpdatePOIAsync方法添加到POIService
现在我们了解到,可以使用网络服务的详细信息来创建或更新一个 POI 记录,那么让我们在POIService类中创建一个新的async方法,命名为CreateOrUpdatePOIAsync(),并执行以下步骤:
-
创建一个新的
async方法CreateOrUpdatePOIAsync(),它接受PointOfInterest实例。这个实例包含了你需要发送到服务器以创建或更新操作的 POI 的详细信息。此方法返回Task<String>,因为创建/更新请求的结果以字符串形式返回:public async Task<String> CreateOrUpdatePOIAsync (PointOfInterest poi, Activity activity) { } -
声明一个表示创建或更新现有 POI 的 Web 服务端点的字符串常量:
private const string CREATE_POI = "http://<YOUR_SERVER_IP>:8080/com.packt.poiapp/api/poi/create";或者,如果你还没有设置 Web 服务器代码,可以使用以下 Apiary 模拟 API URL:
private const string CREATE_POI = "http://private-e451d-poilist.apiary-mock.com/com.packt.poiapp/api/poi/create"; -
正如你可能已经在网络服务 API 规范中注意到的,服务器期望 POI 详细信息以 JSON 字符串格式提供。因此,我们需要使用 Json.NET 组件将 POI 对象序列化为 JSON:
var poiJson = JsonConvert.SerializeObject(poi, Formatting.Indented);上述代码将 POI 对象转换为以下 JSON 格式:
{ "Name": "Googleplex " "Description": "Google HQ", "Latitude": "37.423441", "Longitude": "-102.083962", "Address": "1600 Amphitheater Parkway Mountain View, CA 94044", }前面的 JSON 字符串包含大写键,如
Name、Address,这些键对于特定的网络服务 API 规范是不兼容的。为此,我们可以使用 Json.NET 的ContractResolver类在序列化 POI 对象时提供自定义设置。 -
在
POIService内部声明一个名为POIContractResolver的内部类,并从DefaultContractResolver扩展它。重写ResolvePropertyName方法。你需要在POIService类中包含Newtonsoft.Json.Serialization命名空间指令:public class POIContractResolver : DefaultContractResolver { protected override string ResolvePropertyName(string key) { return key.ToLower(); } }上述代码片段是自我解释的。它将所有
PointOfInterest对象属性解析为小写 JSON 键。 -
现在我们可以使用以下代码片段将 POI 对象序列化为具有小写键的 JSON 字符串。将以下列表添加到
CreateOrUpdatePOIAsync方法中:var settings = new JsonSerializerSettings(); settings.ContractResolver = new POIContractResolver(); var poiJson = JsonConvert.SerializeObject(poi, Formatting.Indented, settings); -
现在我们将异步发送 POI 详细信息的 JSON 数据,使用
HttpClient类。HttpClient类提供了PostAsync()方法,该方法用于以异步操作向指定的 URI 发送 POST 请求。以下代码片段演示了使用HttpClient类向服务器发送数据:HttpClient httpClient = new HttpClient (); StringContent jsonContent = new StringContent (poiJson, Encoding.UTF8, "application/json"); HttpResponseMessage response = await httpClient.PostAsync (CREATE_POI, jsonContent); if (response != null || response.IsSuccessStatusCode) { string content = await response.Content.ReadAsStringAsync (); Console.Out.WriteLine ("{0} saved.", poi.Name); return content; } return null;
前面的代码块看起来与 GetPoisListAsync 方法相似,但有一些明显的区别。在这里,我们调用 PostAsync 而不是 GetAsync 方法来发送异步 POST 请求。PostAsync 方法接受两个参数:一个表示 Web 服务 URL 的字符串和一个表示 HTTP 实体主体的 HttpContent 实例。PostAsync 方法接受不同的 HTTP 实体主体格式,如 ByteArrayContent、MultipartContent、StreamContent 和 StringContent,代表请求体和内容头。在这里,在我们的情况下,我们发送 POI JSON 作为 StringContent。
目前,CreateOrUpdatePOIAsync() 方法会在控制台上打印此操作的结果。在本章的后面部分,我们将看到如何从 POIDetailActivity 使用 CreateOrUpdatePOIAsync () 方法来完成保存操作。
消费 Web 服务以删除 POI
要从服务器删除 POI,客户端需要发送关于要删除的 POI 的信息。由于所有 POI 记录都唯一分配了具有唯一 ID 属性的唯一标识符,我们只需传递要删除的 POI 的 ID。让我们了解以下用于删除 POI 的 API 规范:
Request Method: DELETE
Resource Endpoint: /com.packet.poiapp/api/poi/delete/{POI_ID}
Response: Success/Failed
从前面的 API 规范中,请求方法是 DELETE,Web 服务需要将 POI ID 传递到 URL 的末尾,以便服务器知道要删除哪个 POI。由于我们不需要向请求体发送任何数据,因此我们不需要指定 Content-Type 请求头。操作的结果是,此 API 会以 Success 或 Failure 纯文本消息的形式返回。
将 DeletePOIAsync 方法添加到 POIService
消费 Web 服务以删除 POI 与 GetPoisListAsync() 方法实现类似。以下步骤将帮助您创建一个新的 async 方法,并消费 Web 服务以删除 POI:
-
创建一个新的
async方法DeletePOIAsync(),该方法接受一个整数值poiId,它代表一个唯一的PointOfInterest对象:public async Task<String> DeletePOIAsync (int poiId) { } -
声明一个表示用于删除操作的 Web 服务 API URL 的字符串常量:
private const string DELETE_POI = "http://localhost:8080/com.packt.poiapp/api/poi/delete/{0}";注意,
DELETE_POI字符串期望poiId参数位于 URL 的末尾。这是要从服务器删除的 POI 的 ID。或者,您可以使用以下 Apiary 测试 URL:
private const string DELETE_POI = "http://private-e451d-poilist.apiary-mock.com/com.packt.poiapp/api/poi/delete"; -
现在我们将创建一个
HttpClient实例,并从服务器删除 POI。将以下代码片段添加到DeletePOIAsync()方法中:public async Task<String> DeletePOIAsync (int poiId) { HttpClient httpClient = new HttpClient (); String url = String.Format (DELETE_POI, poiId); HttpResponseMessage response = await httpClient.DeleteAsync (url); if (response != null || response.IsSuccessStatusCode) { string content = await response.Content.ReadAsStringAsync(); Console.Out.WriteLine ("One record deleted."); return content; } return null; }
在前面的代码片段中,DeleteAsync() 方法异步地向指定的统一资源标识符(URI)发送删除请求,并将此操作的结果打印在控制台上。
创建 SavePOI()
注意,在上一个步骤中,在添加保存和删除操作部分,我们从 OnOptionsItemSelected() 方法中调用了 SavePOI() 和 DeletePOI() 操作,但我们根本就没有声明它们。这两个方法将处理消耗网络服务以创建、更新和删除 POI。接下来的部分将向您介绍 SavePOI() 和 DeletePOI() 操作。
现在我们将在 POIDetailActivity 类中声明一个新的方法 SavePOI()。SavePOI() 方法可以避免在 OnOptionsItemSelected() 方法中放置过多的逻辑。此方法将验证用户输入并启动服务器请求以创建或更新 POI。
我们将在下一节中介绍字段验证,现在我们专注于从屏幕获取用户数据并启动创建/更新请求。以下列表显示了 SavePOI() 中应该包含的内容:
protected void SavePOI()
{
_poi.Name = _nameEditText.Text;
_poi.Description = _descrEditText.Text;
_poi.Address = _addrEditText.Text;
_poi.Latitude = tempLatitude;
_poi.Longitude = tempLongitude;
CreateOrUpdatePOIAsync (_poi);
}
注意,我们在 SavePOI() 中调用了一个新方法,即从 SavePOI() 调用的 CreateOrUpdatePOIAsync()。我们需要将 CreateOrUpdatePOIAsync() 添加到 POIDetailActivity 中。它是一个 async 方法,将负责初始化 POIService 类并启动保存 POI 的网络服务请求。
以下代码片段列表显示了 CreateOrUpdatePOIAsync() 方法中应该包含的内容:
private async void CreateOrUpdatePOIAsync(PointOfInterest poi){
POIService service = new POIService ();
if (!service.isConnected(this)) {
Toast toast = Toast.MakeText (this, "Not conntected to internet. Please check your device network settings.", ToastLength.Short);
toast.Show ();
return;
}
string response = await service.CreateOrUpdatePOIAsync (_poi);
if (!string.IsNullOrEmpty (response)) {
Toast toast = Toast.MakeText (this, String.Format ("{0} saved.", _poi.Name), ToastLength.Short);
toast.Show();
Finish ();
} else {
Toast toast = Toast.MakeText (this, "Something went Wrong!", ToastLength.Short);
toast.Show();
}
}
注意,前面提到的方法执行以下任务:
-
首先,它创建一个
POIService类的实例并通过调用isConnected()方法确认网络可用性。 -
如果设备未连接到互联网,它将向用户显示适当的
Toast消息。 -
如果网络可用,它将调用
CreateOrUpdatePOIAsync()方法,该方法定义在POIService类中。网络服务请求可能是一个长时间运行的阻塞操作,因此我们使用 async await 来使请求异步。 -
显示一个吐司消息来通知用户保存/更新操作的结果。
-
一旦保存请求成功,它将显示一个吐司消息并调用活动的
Finish()方法。Finish()方法导致POIDetailActivity活动关闭,并将堆栈中的上一个活动带到前台;在我们的例子中是POIListActivity。
创建 DeletePOI()
与 SavePOI() 类似,DeletePOI() 方法被创建是为了简化 OnOptionsItemSelected() 中的逻辑。在删除 POI 之前,我们必须通过显示对话框来要求用户重新确认。在本章的后面部分,我们将向您展示如何在启动删除请求之前显示确认提示。
我们在 POIDetailActivity 中创建了 CreateOrUpdatePOIAsync() 方法,现在让我们添加另一个新方法,名为 DeletePOIAsync()。此方法执行的任务与 POIDetailActivity 中的 CreateOrUpdatePOIAsync() 方法非常相似。它检查互联网连接的可用性,异步启动删除操作,并最终通过 toast 消息通知用户。
将以下 DeletePOIAsync 方法添加到你的 POIDetailActivity 类中:
public async void DeletePOIAsync(){
POIService service = new POIService ();
if (!service.isConnected(this)) {
Toast toast = Toast.MakeText (this, "Not conntected to internet. Please check your device network settings.", ToastLength.Short);
toast.Show ();
return;
}
string response = await service.DeletePOIAsync (_poi.id);
if (!string.IsNullOrEmpty (response)) {
Toast toast = Toast.MakeText (this, String.Format ("{0} deleted.", _poi.Name), ToastLength.Short);
toast.Show();
Finish ();
} else {
Toast toast = Toast.MakeText (this, "Something went Wrong!", ToastLength.Short);
toast.Show();
}
}
以下列表显示了 DeletePOI() 方法中应该存在的内容:
protected void DeletePOI()
{
DeletePOIAsync();
}
现在让我们构建并运行这个应用。你现在应该能够执行添加、更新和删除操作:

备注
如果你正在使用 Apiary 模拟数据 URL 进行保存和删除操作,则不会删除或保存任何 POI。Apiary 仅用于测试目的。你必须部署本书中提供的网络服务代码包,以便 保存 和 删除 操作对服务器数据进行生效。
添加验证
任何非平凡的应用都会有一定程度的验证需求。POIApp 应用相对简单,但我们有一组需要强制执行的规则,这将有助于讨论:
属性
规则
名称
这不能为空或为空值
纬度
这包含一个在 -90 和 90 之间的有效十进制数
经度
这包含一个在 -180 和 180 之间的有效十进制数
使用 EditText.Error 属性
EditText 小部件有一个名为 Error 的字符串属性,它简化了向用户显示错误的工作,尤其是如果你想一次性显示所有带有错误的字段时。以下截图显示了因留空 名称 字段而接收到的错误:

要使用此功能,只需将属性设置为错误消息,当不存在错误时清除属性。以下示例演示了为 名称 属性实现规则的实现:
bool errors = false;
if (String.IsNullOrEmpty (_nameEditText.Text)) {
_nameEditText.Error = "Name cannot be empty";
errors = true;
}
else
_nameEditText.Error = null;
注意名为 errors 的局部布尔变量,它用于跟踪是否找到任何错误。对于 纬度 和 经度 的编辑稍微复杂一些,因为你需要将文本转换为 double 值,并允许指定 null 值。
以下代码演示了实现编辑的一种方法:
double? tempLatitude = null;
if (!String.IsNullOrEmpty(_latEditText.Text)) {
try {
tempLatitude = Double.Parse(_latEditText.Text);
if ((tempLatitude > 90) | (tempLatitude < -90)) {
_latEditText.Error = "Latitude must be a decimal value between -90 and 90";
errors = true;
}
else
_latEditText.Error = null;
}
catch
{
_latEditText.Error = "Latitude must be valid decimal number";
errors = true;
}
}
使用 EditText.Error 属性在 SavePOI() 方法中实现本节开头确定的规则。
只有当所有编辑都通过时,你才能更新和保存 POI 属性。以下列表显示了结构化逻辑的一种方法:
if (errors) {
return;
}
_poi.Name = _nameEditText.Text;
_poi.Description = _descrEditText.Text;
_poi.Address = _addrEditText.Text;
_poi.Latitude = tempLatitude;
_poi.Longitude = tempLongitude;
CreateOrUpdatePOIAsync ();
运行 POIApp 并确认验证是否正确工作。
添加删除确认提示
对于应用来说,在执行任何类型的破坏性更新之前提供确认是一个最佳实践,尤其是如果无法撤销的话。因此,我们需要为 删除 操作提供确认。幸运的是,Android 通过 AlertDialog 和 AlertDialog.Builder 类使这相对容易。
AlertDialog类允许您显示一个模态确认对话框。AlertDialog.Builder类是一个嵌套类,它帮助您构建一个AlertDialog实例的方法;您可以将其视为一个工厂类。步骤如下:
-
创建一个
AlertDialog.Builder实例。 -
在构建器实例上设置各种属性,如消息、按钮文本、当按钮被点击时调用事件处理器等。
-
在
AlertDialog.Builder的实例上调用Show()以创建并显示AlertDialog实例。在我们的案例中,我们想要一个包含简单消息以及OK和Cancel按钮的
AlertDialog类。当我们点击Cancel时,我们只需关闭对话框并什么都不做。当用户选择OK时,我们需要启动删除 POI 操作。 -
创建一个事件处理器,当您点击OK按钮时将被调用。此方法现在将调用
DeletePOIAsync()以执行删除操作。以下列表展示了这些更改:protected void ConfirmDelete(object sender, EventArgs e) { DeletePOIAsync (); } -
将构建
AlertDialog类的逻辑添加到现有的DeletePOI()方法中。以下列表展示了这个逻辑:protected void DeletePOI() { AlertDialog.Builder alertConfirm = new AlertDialog.Builder(this); alertConfirm.SetTitle("Confirm delete"); alertConfirm.SetCancelable(false); alertConfirm.SetPositiveButton("OK", ConfirmDelete); alertConfirm.SetNegativeButton("Cancel", delegate {}); alertConfirm.SetMessage(String.Format("Are you sure you want to delete {0}?", _poi.Name)); alertConfirm.Show(); }
SetPositiveButton()和SetNegativeButton()方法允许指定按钮标题和事件处理器。在取消的消极按钮的情况下,我们提供一个空的事件处理器,因为没有要做的事情;Android 将负责关闭对话框。AlertDialog还提供了一个中立按钮。
小贴士
在 Honeycomb 之前的设备上,按钮顺序(从左到右)是积极 - 中立 - 消极。在较新的设备上,使用 Holo 主题,按钮顺序(从左到右)是消极 - 中立 - 积极。
运行POIApp并验证删除确认是否正常工作。以下截图显示了用户点击删除操作时的删除确认对话框:

刷新 POIListActivity
我们在POIDetailActivity上采取的操作,如Save和Delete,会影响POIListActivity中显示的数据。我们需要确保当POIListActivity再次变得活跃时,ListView被刷新并显示更新的 POI 列表。为了实现这一点,我们必须依赖于活动生命周期回调方法。
如您从第一章“Android 应用的解剖结构”中可能记得的,当一个活动由于新活动的启动而被移动到后台时,会调用OnPause()方法。当POIDetailActivity启动时,这就会发生在POIListActivity上。一旦POIDetailActivity通过调用Finish()方法或按设备返回按钮完成,POIListActivity将回到前台,并调用OnResume()方法。
让我们在OnCreate()方法中移除对DownloadPoisListAsync()的调用,并添加以下片段以刷新POIListActivity:
protected override void OnResume (){
base.OnResume ();
DownloadPoisListAsync ();
}
在本章中,我们涵盖了大量内容。我们完成了 POI 详细信息活动,以执行添加、更新或删除 POI 的操作。如果您有任何意外的偏差,您可以参考代码包。
摘要
在本章中,我们通过使用不同的布局管理器,如LinearLayout、TableLayout和ScrollView,以及使用EditText来验证表单数据,创建了大量的复杂布局。
现在在POIApp中添加了一个新的活动,用于显示 POI 的详细信息,并允许用户执行添加、更新或删除 POI 操作。StartActivity方法与Intent结合使用,用于在活动之间传递数据包。
我们还通过添加执行网络服务POST和DELETE操作的方法,扩展了POIService类。
下一章将指导您处理设备方向改变时应用程序的行为。
第六章:使您的应用程序方向感知
本章将向您介绍处理 Android 应用程序在设备配置变化时的行为的过程。在本章中,我们将涵盖理论概念的全面细节,但不会对POIApp进行任何重大更改。本章将涵盖以下主题:
-
Android 在配置变化时的行为
-
锁定 Android 应用程序方向
-
保存活动状态以应对配置变化
-
添加备用资源
-
手动处理方向行为
Android 在配置变化时的行为
所有现代智能手机和平板电脑都会根据用户旋转设备的方式在纵向和横向模式之间切换。Android 应用程序应响应配置变化并显示适合当前设备配置的适当布局。Android 设备配置可以在运行时以多种形式更改,例如设备方向变化、设备语言更改、设备字体更新、设备连接到外部显示器、设备连接到坞站等。在所有这些早期情况下,Android 都会重新启动正在运行的活动,如果可用,则加载备用资源,以正确加载给定配置的应用程序。活动会经历一系列生命周期方法,例如OnDestroy(),然后是OnCreate()来处理活动重启行为。
例如,如果您在 Nexus 5 设备上以 1080 x 1920 分辨率在纵向模式下运行应用程序,并且当方向变为横向时,应用程序必须适当地响应以适应 1920 x 1080 维度的布局及其子视图。您可能会问为什么不在布局中调整视图以适应适当的大小?为什么活动需要重新启动?嗯,Android 这样做是为了保持简单。然而,在内部,它做了很多事情,并提供了一些高级功能,以使配置更改更加平滑。
如果设备配置发生变化,Android 只会销毁并重新启动前台的活动,但应用程序实例保持不变。当活动重新启动时,将加载该配置的适当布局。对于此类事件,确保您的应用程序必须恢复活动状态和用户在 UI 上输入的数据非常重要。这可以通过使用OnSaveInstanceState()和OnRestoreInstanceState()回调方法来实现。
本章探讨了构建平滑、响应和方向感知应用程序的一些关键点。
锁定 Android 应用程序方向
根据谷歌设计指南,Android 应用必须响应用户设备的方向,并为给定的方向显示适当的布局。然而,某些类型的应用,如游戏、视频播放器等,旨在仅限制方向为横幅或纵向。对于原生 Android 应用,这可以通过在 AndroidManifest.xml 描述文件中的 <activity> 声明中使用 android:screenOrientation 属性来实现。Xamarin 使这一过程更加简化,并建议你任何时候都不要手动编辑 AndroidManifest.xml 文件,而是允许你使用 [Activity] 属性自定义属性来设置活动声明中的所有应用程序配置元数据。
如我们从第三章,创建兴趣点应用 回忆的那样,我们已经在使用 [Activity] 属性的一些属性,例如 Label、MainLauncher 和 Icon。现在为了锁定活动方向行为,我们可以在活动类声明中使用 ScreenOrientation 属性。ScreenOrientation 属性需要静态地指定给每个需要控制方向锁定的活动。它不能在应用程序的全局范围内进行控制。
ScreenOrientation 属性期望 Android.Content.PM.ScreenOrientation 枚举中定义的可能常量之一。ScreenOrientation 枚举定义了所有设备配置常量,如 FullSensor、FullUser、Landscape、Locked、Nosensor、Portrait 等。
要查看 ScreenOrientation 常量的完整集合,请访问官方 Xamarin.Android 文档:developer.xamarin.com/api/type/Android.Content.PM.ScreenOrientation/。
注意,在这本书中,我们将构建 POIApp 以响应设备方向和其他不同配置更改事件。以下代码块仅用于演示目的,并且不会与 POIApp 示例代码一起继续。你可以添加以下属性来锁定 POIListActivity 的方向为仅横幅:
namespace POIApp
{
[Activity (Label = "POI List", ScreenOrientation = ScreenOrientation.Landscape)]
public class POIListActivity : Activity
{
……
………
}
}
将前面的更改应用到 POIListActivity 并运行应用。你会注意到 POI 列表屏幕覆盖了设备方向锁定设置,并且它始终以横幅模式显示。
动态请求方向
你也可以通过设置 RequestedOrientation 属性到你的活动来动态更改所需的活动方向。这允许在需要时随时动态更改方向。在任何地方添加以下代码片段以限制当前运行的活动方向为仅横幅:
RequestedOrientation = ScreenOrientation.Landscape;
对 RequestedOrientation 属性的更改会影响前台活动,并立即通过使当前活动重新启动来请求指定的方向。您可以使用相同的属性来获取当前活动生效的配置。
为配置更改保存活动状态
Android 设备的运行时配置更改会导致前台活动经历重启过程。这种行为旨在简化活动使用给定配置的替代资源重新初始化的过程。在活动经历重新创建过程时,您可能会丢失用户输入的数据或活动的当前状态。对于任何此类事件,所有 Android 应用都必须快速且低成本地保留活动状态,以避免不良的用户体验。以下部分将帮助您深入了解保留活动状态的过程。
在配置更改的情况下,活动会经历一系列生命周期方法,例如 OnDestroy() 后跟 OnCreate()。在这个过程中,它在销毁活动之前调用 OnSaveInstanceState(),以便您可以保存活动状态数据。应用程序状态可以在 onCreate() 或 OnRestoreInstanceState() 回调方法中保留。为了理解整个过程,我们必须回忆起第一章“Android 应用解剖”中描述的活动生命周期方法。以下图展示了活动生命周期以及 OnSaveInstanceState 和 OnRestoreInstanceState() 方法的调用流程及其相应的配置更改:

方向的改变会调用 OnPause()、OnSaveInstanceState()、OnStop() 和 OnDestroy() 方法,然后是 OnCreate()、OnStart()、OnRestoreInstanceState() 和 OnResume()。请记住,这个顺序并不总是正确的。例如,当用户按下设备返回按钮或调用 Finish() 方法时,您不需要保存活动状态;因此,它只会调用 OnPause()、OnStop() 和 OnDestroy(),但 OnSaveInstanceState() 方法永远不会被调用。
OnSaveInstanceState() 和 OnRestoreInstanceState() 的默认实现负责保存和保留所有与布局关联并具有 id 属性的 Android 视图输入小部件(例如,EditText、CheckBox、RadioButton 等)上的数据。这意味着 Android SDK 在每个视图控件上实现了 OnSaveInstanceState() 和 OnRestoreInstanceState() 方法。
基于两个因素,Android 决定是否在方向更改时保留附加到视图组的视图的状态。一个因素是id属性,另一个是基于为view.SaveEnabled属性设置的值。SaveEnabled属性检查是否会在该视图中调用onSaveInstanceState()方法。SaveEnabled的默认值是true。
除了SaveEnabled属性外,Android 还提供了一个有趣的方法,允许您控制视图层次结构下整个保存状态的行为。例如,如果您在LinearLayout下有五个不同的视图,并且您希望不保存包括LinearLayout在内的这五个视图的状态,您只需将LinearLayout的SaveFromParentEnabled属性设置为false即可。
对于使用自定义或复合视图的应用程序,您必须手动处理状态恢复。您需要在自定义视图实现中重写OnSaveInstanceState()和OnRestoreInstanceState()方法以存储您视图的状态。
手动保存活动状态
如果您需要手动保存活动状态,您必须在您的活动中重写以下方法,并编写自己的逻辑来在数据包中保存和恢复活动状态。OnSaveInstanceState()方法提供了一个Bundle实例,我们可以将其用于数据转储,而OnRestoreInstanceState()方法返回之前保存的Bundle。
Bundle是一种特殊的容器,它提供了异构值的键/值映射。Bundle通常用于从一个活动解析数据到另一个活动或保存和检索视图状态。Bundle类提供了一组重载的PutXXX()和GetXXX()方法来存储和检索值。
以下代码片段描述了OnSaveInstanceState()和OnRestoreInstanceState()方法的原型:
protected override void OnSaveInstanceState (Bundle outState)
{
base.OnSaveInstanceState (outState);
// Place your logic to save activity state
}
protected override void OnRestoreInstanceState (Bundle savedInstanceState)
{
base.OnRestoreInstanceState (savedInstanceState);
// Place your logic to restore activity state
}
保留 POI 列表滚动位置
到目前为止,我们已经对配置更改时的活动生命周期行为有了很多了解。现在让我们将其实现到POIApp中。当 POI 列表增长时,它允许用户垂直滚动以查看所有列表项。假设,用户已经滚动到列表中的第 10 个元素,同时设备方向发生了变化。由于设备配置更改请求,活动重新启动,导致列表从顶部出现。保留列表滚动位置以获得更好的用户体验是一个好主意。
当设备配置更改时,要保留 POI 列表滚动位置,请执行以下步骤:
-
在
OnSaveInstanceState()方法中获取第一个可见列表项的索引并将其保存到Bundle中:protected override void OnSaveInstanceState (Bundle outState) { base.OnSaveInstanceState (outState); int currentPosition = poiListView.FirstVisiblePosition; outState.PutInt ("scroll_position", currentPosition); }在前面的代码片段中,字符串
scroll_position用作键,用于在方向更改时保存ListView的当前滚动位置。在从OnRestoreInstanceState()回调检索数据时,应使用相同的键。 -
在
OnRestoreInstanceState()中恢复保存的列表滚动位置。请注意,我们必须使用与保存当前滚动位置相同的键:int scrollPosition; protected override void OnRestoreInstanceState (Bundle savedInstanceState) { base.OnRestoreInstanceState (savedInstanceState); scrollPosition = savedInstanceState.GetInt ("scroll_position"); } -
将以下代码片段添加到
POIListActivity类的DownloadPoisListAsync()方法中,并将滚动 POI 列表到最后一个保存的滚动位置的队列消息:poiListView.Post(() => { poiListView.SetSelection(scrollPosition); });
现在运行应用程序,通过改变方向来测试 POI 应用程序;注意滚动位置被保留。
构建方向感知布局
Android 允许您添加多个竞争版本的资源,以使应用程序与不同的设备配置兼容。在为给定配置选择正确资源时,Android 系统非常有用。您不需要编写任何代码来查找当前配置并选择适当的资源。您需要做的只是添加多个版本的资源,例如string.xml、.png可绘制图像或布局 XML 文件,以便在不同情况下选择最佳的资源版本。
假设您的应用程序最初主要针对美国市场,但现在我们期待着针对俄罗斯市场,并支持俄语。在这种情况下,可以添加多个版本的string.xml以支持俄语。或者想象一下,用户正在竖屏模式下在平板电脑上运行应用程序,现在设备旋转到横屏方向。屏幕宽度加倍,提供了显示更多信息的机会。在这种情况下,您将需要添加不同方向的活动布局的多个版本。让我们屏住呼吸,期待下一章了解更多关于如何为 Android 平板电脑构建方向感知布局的信息。
添加到应用程序的所有资源都将添加到Resources目录下的各个子目录中。默认资源目录结构如下所示:

当没有可用的替代最佳匹配资源时,将作为默认资源添加的资源将被使用。替代资源是为特定配置设计的。
要添加替代资源,您只需使用特定的配置限定符。您可以为设备屏幕尺寸使用的配置限定符有:小、正常、大和超大。例如,对于超大屏幕的布局,如平板电脑布局,应放在layout-xlarge目录下。自 Android 3.2 以来,Android 建议您使用sw<N>dp配置限定符来为平板电脑定义超大布局。例如,如果您的多窗格平板电脑布局至少需要 600dp 的屏幕宽度,您应将其放置在layout-sw600dp目录下。
要为俄语用户提供strings.xml文件的翻译版本,您必须将string.xml文件放置在Resources/values-ru/目录下。
下表显示了允许您为不同屏幕配置提供特殊资源的配置限定符列表:
特征
限定符
描述
屏幕尺寸
小号、正常、大号和超大号
为小号、正常、大号和超大号屏幕尺寸的资源。
l dpi
为低密度(l dpi)屏幕的资源(约 120 dpi)。您现在可以忽略这个资源集,因为市场上没有新的 Android 设备具有 ldpi 密度。
mdpi
为中等密度(mdpi)屏幕的资源(约 160 dpi)。(这是基准密度。)
hdpi
为高密度(hdpi)屏幕的资源(约 240 dpi)。
xhdpi
为超高密度(xhdpi)屏幕的资源(约 320 dpi)。
xxhdpi
为超高密度(xxhdpi)屏幕的资源(约 480 dpi)。
xxxhdpi
为超高超高超高密度(xxxhdpi)屏幕的资源使用(约 640 dpi)。仅用于启动器图标;参见前面的说明。
nodpi
为所有密度的资源。这些是密度无关的资源。系统不会缩放带有此限定符的资源。
设备方向
land
为横向方向的屏幕使用的资源。
port
为纵向方向的屏幕使用的资源。
宽高比
long
为具有比基准屏幕配置显著更高或更宽宽高比(在纵向或横向方向时)的屏幕使用的资源。
不长
为具有与基准屏幕配置相似宽高比的屏幕使用的资源。
在接下来的第七章“为多种屏幕尺寸设计”中,我们将使用替代布局资源来使应用程序兼容 Android 平板电脑。
手动处理方向行为
如所述,Android 系统会自动处理配置更改时更新适当的资源。然而,有时出于性能原因,您可能希望限制活动重启并编写自己的逻辑来更新给定配置的适当资源。但请记住,Google 指南不推荐这样做;如果您正在为您的应用程序实现此功能,请自行承担风险。
以下步骤将指导您手动处理应用所需的配置:
-
将
ConfigurationChanges属性添加到活动声明中。这允许您声明您想要自己处理的所有在Android.Content.PM.ConfigChanges中定义的可能配置值。在运行时,这将在AndroidManifest.xml文件中的活动声明中添加android:configChanges属性:[Activity (Label = "POI List", ConfigurationChanges= ConfigChanges.Orientation | ConfigChanges.KeyboardHidden)] public class POIListActivity : Activity { --- }在前面的代码语法中,我们说明了我们将手动处理设备方向和滑动键盘状态变化的配置。请注意,前面的声明绕过了整个活动销毁过程,并简单地返回一个回调以通知您有关变化。
-
在你的活动中重写
OnConfigurationChanged()。当配置变化之一(如ConfigurationChanges属性声明中列出)发生时,将调用此方法:public override void OnConfigurationChanged (Android.Content.Res.Configuration newConfig) { base.OnConfigurationChanged (newConfig); //update UI to reflect the orientation change }
在这里,在OnConfigurationChanged()方法中,你需要更新 UI,以便它反映方向变化。对于我们的POIApp,我们允许系统自行处理配置。请注意,本章中使用的所有代码片段仅用于演示目的,我们不会继续使用这些更改来构建本书其余章节中的POIApp。
摘要
在本章中,我们详细介绍了当设备配置发生变化时,活动行为的变化,迫使活动显示指定的方向并保存和恢复活动状态。下一章将指导你处理为各种设备形态设计应用程序,例如 Android 平板电脑。
第七章:为多种屏幕尺寸设计
2011 年初,安卓蜂巢 3.0(API 级别 11)发布,专门用于支持更大屏幕尺寸的平板电脑。从那时起,安卓生态系统爆炸式增长,所有后续的安卓版本都旨在提供更多屏幕尺寸,包括智能手机、平板电脑和谷歌电视。
本章将指导您处理多种屏幕尺寸并使您的应用程序兼容智能手机和平板电脑。在本章的过程中,我们将扩展我们一直在开发的POIApp并为其优化以适应安卓平板电脑。本章将涵盖以下主题:
-
安卓平板电脑生态系统简介
-
创建和管理片段
-
使
POIApp与安卓平板电脑兼容 -
处理
ListFragment以显示 POI 列表 -
为平板电脑创建多面板布局
-
为安卓平板电脑添加替代布局
-
使用
DialogFragment显示对话框 -
为旧版安卓设备使用片段
-
在设备配置更改时保留片段状态
安卓平板电脑简介
第一款安卓平板电脑于 2011 年发布,搭载安卓版本 3.0。然而,所有后续的安卓版本都旨在支持一系列屏幕尺寸,包括小、中和大尺寸。由于其开放性,安卓生态系统拥有各种设备制造商,因此存在不同屏幕尺寸和密度的设备。
为安卓智能手机构建的应用程序可以在不进行任何更改的情况下在平板电脑上运行。然而,如果 UI 没有优化,它将给您带来不愉快的用户体验。开发者必须将额外空间视为机会,并有效地利用它来设计一个可以在更大屏幕上显示更多信息的 UI。例如,安卓手机中的 Gmail 应用程序将有两个活动来显示最近的电子邮件列表和所选电子邮件的详细信息。然而,同一应用程序在平板电脑上使用多面板分割视图布局在同一页面上显示电子邮件列表和详细信息。
设计安卓应用程序用户界面时需要考虑的关键因素如下:
-
屏幕尺寸:这是设备的实际物理屏幕尺寸;通常分为小、正常、大和超大。尽管没有官方确认最小和最大屏幕尺寸,但它们通常在 2.55 到 10.1 英寸之间。
-
屏幕密度:这是屏幕物理区域中像素的数量,表示为 dpi(每英寸点数)。高密度屏幕比低密度屏幕有更多的像素。
-
分辨率:这是设备屏幕上像素的数量。它通常定义为
宽度 x 高度。例如,Nexus 5 设备的分辨率为1080 x 1920。 -
屏幕方向:设备的方向可以是横屏或竖屏模式。
创建安卓平板电脑模拟器
到目前为止,我们已经构建了 POIApp 来显示从服务器获取的 POI 列表,并在 Android 智能手机上进行了测试。在我们开始扩展 POIApp 以支持 Android 平板电脑之前,让我们为 Android 平板电脑创建一个新的模拟器实例并运行到目前为止构建的现有 POIApp。
为了创建平板电脑的模拟器,执行以下步骤:
-
从主菜单栏导航到 工具 并打开 Google 模拟器管理器。这将打开 AVD 管理器窗口。
-
要创建一个新的模拟器,点击右侧面板中的 创建 按钮。
-
将 AVD 名称 字段设置为
Nexus10,从 设备 下拉菜单中选择 Nexus 10,将 目标 字段设置为Android 4.4.x,如果需要,取消选中 硬件键盘存在 选项,然后点击 确定。 -
注意,目标 设置指定了模拟器将使用的 Android 平台版本和 API 级别。
-
提供如以下截图所示的配置,例如 AVD 名称、设备、皮肤、目标、内存选项等:
![]()
如果您正在使用第三方模拟器解决方案,例如 Xamarin Android Player 或 Genymotion,您可以在平板电脑配置中下载 Nexus 7 或 Nexus 10 模拟器进行测试。
现在启动之前步骤中创建的 Nexus 10 模拟器,并运行到目前为止构建的现有 POIApp。以下截图显示了未针对大屏幕优化的平板电脑上的 POIApp 用户界面:

利用额外的屏幕空间,UI 元素可以组织在各个位置,使应用看起来更美观。目前,POIApp 使用两个活动来显示 POI 列表和 POI 详细信息。对于平板电脑,我们可以使用多面板分割布局,在同一个活动中通过共享相同的屏幕空间来显示 POI 列表和详细信息。Android 活动旨在用于单一目的,并且不允许在另一个活动内部嵌入活动。Android 片段可以用来解决这个问题。片段是一个可重用的用户界面组件,用于为不同屏幕尺寸构建动态和模块化的用户界面。一个活动可以包含一个片段或多个片段。
片段简介
片段是用户界面组件的一部分,提供了对应用程序设计的灵活性。正如您在第一章“Android 应用解剖”中学到的,片段是可重用的迷你活动,如 UI 组件,可以管理它们自己的生命周期。片段总是旨在在没有依赖另一个活动或片段的情况下工作。与 Activity 类一样,片段类需要从 Fragment 类扩展。为了使事情更加简单,Android 提供了一些额外的专用片段子类,例如 ListFragment、DialogFragment 和 PreferenceFragment。
下表显示了片段子类及其用途列表。您还可以扩展以下任何片段子类以创建自己的片段。
ListFragment
这显示了来自不同来源的数据项列表,例如数组、游标等。
DialogFragment
这显示了一个作为浮动对话框窗口的片段。
PreferenceFragment
这显示了应用程序的首层偏好设置列表。当用户进行任何更改时,偏好设置将被保存。Nexus 设备设置屏幕设计遵循PreferenceFragment模式。
创建一个新的片段
片段可以被视为活动的一个模块化组件,它维护自己的生命周期,处理用户事件,并且可以被添加到或从运行的活动中移除。创建片段的过程与创建活动非常相似。
在 Android 中创建和添加片段的步骤如下:
-
创建片段的第一步是定义其布局。与活动布局类似,您可以使用 Xamarin Studio 中可用的拖放界面构建器,或者您可以使用 XML 代码编辑器来创建布局。
-
创建一个直接扩展
Fragment类或其子类的新的类。以下代码片段扩展了Fragment基类:public class MyFragment : Fragment { ----- ----- } -
现在实现必要的片段方法。首先,让我们重写
OnCreateView()方法。此方法将片段添加到视图层次结构,并返回一个表示片段的View实例。片段的 UI 可以通过两种方式创建:通过在
OnCreateView()中声明 XML 布局并填充片段布局,或者通过动态创建所有接口。为了简单起见,总是首选 XML 声明方法:public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for your fragment View view = inflater.Inflate(Resource.Layout.MyFragmentLayout, container, false); // Initialize other view controls return view; }可选地,您可以覆盖其他片段生命周期,如
OnPause()、OnResume()等,以控制其他生命周期事件回调。 -
最后,让我们将片段添加到活动中。这可以通过两种方式完成:要么通过声明性添加活动布局,要么通过编程添加。
使用以下代码片段通过在活动布局中声明<fragment>标签来添加片段:
<fragment class="MyApp.MyFragment"
android:id="@+id/myFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
要动态添加片段,您需要首先创建一个片段占位符容器布局并为其分配一个 ID:
<FrameLayout
android:id="@+id/myFragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
现在您已经指定了片段将动态添加到的ViewGroup,您可以使用FragmentTransaction实例来添加、删除和替换事务。使用以下代码片段来获取FragmentTransaction实例:
MyFragment myFragment = new MyFragment();
FragmentTransaction ft = FragmentManager.BeginTransaction();
然后,您可以通过传递片段实例和片段将要添加到的View的 ID 来使用Add()、Remove()和Replace()方法。一旦片段事务完成,您必须调用Commit()方法以使更改生效:
ft.Add(Resource.Id.myFragmentContainer, myFragment);
ft.Commit();
到目前为止,你对如何使用片段有了相当的了解;现在让我们继续让 POIApp 与片段一起工作,并重用片段来优化平板电脑的布局。
使 POIApp 兼容 Android 平板
在第一章 Android App 的解剖结构 中,我们已经覆盖了很多关于片段基础和生命周期的内容。如果你还没有阅读这些概念,我建议你阅读一下。现在让我们通过以下步骤来创建和管理片段,并构建 POIApp 以支持多栏平板电脑布局。
目前,POIApp 正在使用两个活动:POIListActivity 用于显示 POI 列表,POIDetailsActivity 用于显示详情。现在我们将创建两个新的片段:POIListFragment 和 POIDetailFragment,这两个片段将被重用于智能手机和多栏平板电脑布局:

注意以下截图中的要点,这些将在本章其余部分中实现:
-
手机布局包含两个活动;
POIListActivity和POIDetailsActivity,分别托管POIListFragment和POIDetailFragment。在这里,活动将像占位符一样工作,以容纳片段。大部分的应用逻辑都应该从活动移动到片段中。 -
POIListFragment将包含初始化列表视图的逻辑,使用在POIService类中声明的方法下载数据,并准备用户界面。目前,相同的逻辑放置在POIListActivity中。 -
POIDetailFragment将包含显示所选 POI 详情的逻辑,以及创建、更新和删除 POI 的能力。 -
平板电脑的可用空间相对较大,因此其布局可以在单个活动中同时容纳
POIListFragment和POIDetailFragment。这使我们能够重用片段,并允许在不同屏幕尺寸上提供不同的用户体验。
本章的以下部分将指导你如何使用片段构建 POIApp,使用 Android 平板的分栏视图布局。
使用片段显示 POIDetails
为了使事情简单,我们将逐步进行。首先,让我们创建一个新的片段来显示 POI 的详情,这将使你能够编辑、更新和删除 POI。目前,POIDetailActivity 活动包含相同的逻辑。在这个阶段,让我们保持简单,不要通过考虑多栏平板电脑布局来使事情复杂化。
创建 POIDetailsFragment 布局
让我们从为 POI 详情片段创建一个新的布局文件开始,并将其命名为 POIDetailFragment.axml:
-
在 Solution 面板中选择
Resources/Layout文件夹。 -
右键单击 Add 并选择 New File。
-
在 New File 对话框中,点击 Android 并选择 Layout,在 Name 字段中输入
POIDetailFragment,并选择 New。 -
注意,我们不会对 POI 详细信息屏幕布局进行任何更改。所以,我们只需将
POIDetail.axml中的布局 XML 源代码复制并粘贴到新创建的POIDetailFragment.axml文件中。
创建 POIDetailFragment
现在我们已经准备好了 POI 详细信息片段布局,我们需要相应的片段。要创建POIDetailFragment,请执行以下步骤:
-
在解决方案面板中选择
POIApp项目,右键单击它,然后导航到添加 | 新建文件。 -
从新建文件对话框中,点击Android并选择Fragment,在名称字段中输入
POIDetailFragment,然后点击新建。在前一步骤中创建的
POIDetailFragment将包含显示 POI 详细信息并启用各种操作的逻辑,例如创建、更新和删除 POI。目前,业务逻辑在POIDetailsActivity中。 -
为我们在布局中创建的每个输入小部件声明以下私有变量。将以下列表添加到您的
POIDetailFragment类中:PointOfInterest _poi; EditText _nameEditText; EditText _descrEditText; EditText _addrEditText; EditText _latEditText; EditText _longEditText; -
重写
OnAttach()方法并保存activity实例的引用:private Activity activity; public override void OnAttach (Activity activity) { base.OnAttach (activity); this.activity = activity; } -
重写
OnCreateView()方法,填充片段布局,并通过调用FindViewById<T>方法和UpdateUI()方法将每个变量绑定到相应的用户界面小部件:public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.Inflate(Resource.Layout.POIDetailFragment, container, false); _nameEditText = view.FindViewById<EditText>(Resource.Id.nameEditText); _descrEditText = view.FindViewById<EditText> (Resource.Id.descrEditText); _addrEditText = view.FindViewById<EditText> (Resource.Id.addrEditText); _latEditText = view.FindViewById<EditText> (Resource.Id.latEditText); _longEditText = view.FindViewById<EditText> (Resource.Id.longEditText); UpdateUI(); return view; } -
将
UpdateUI()、SavePOI()、DeletePOI()和ConfirmDelete()方法从POIDetailsActivity类复制并粘贴到POIDetailFragment类中。您将在使用此关键字的地方遇到编译错误。将其替换为活动实例。 -
现在将
CreateOrUpdatePOIAsync()和DeletePOIAsync()方法从POIDetailsActivity复制到POIDetailFragment类中。在这里,你将再次注意到Finish()方法和Toast初始化附近出现编译错误。对于Toast实例化,将其替换为活动实例,并将Finish()替换为activity.Finish()。 -
现在让我们重写
OnCreate()方法。在这里,我们将检索发送到POIDetailFragment的 POI 详细信息。每个片段都有一个Arguments属性,其中包含数据包。与活动一样,我们可以使用一系列GetXXX()方法,其中XXX代表名称/值对的值的类型:public override void OnCreate (Bundle savedInstanceState) { base.OnCreate (savedInstanceState); if (Arguments!=null && Arguments.ContainsKey("poi")) { string poiJson = Arguments.GetString ("poi"); _poi = JsonConvert.DeserializeObject<PointOfInterest>(poiJson); } else { _poi = new PointOfInterest (); } }
操作保存和删除操作
POIDetailFragment向用户显示所选 POI 的详细信息,并保留保存新 POI 或删除现有 POI 的能力。保存和删除操作需要添加到POIDetailFragment中的操作栏。
以下步骤将指导您了解在POIDetailFragment中添加操作所需的内容:
-
要使片段中的动作栏按钮工作,您需要首先通过调用
SetHasOptionsMenu()方法并传递bool值true来启用此选项。bool值表示片段是否希望贡献以填充菜单项。在OnCreateView()中的return语句之前添加以下代码块:SetHasOptionsMenu (true); -
在第五章“添加详情视图”中,我们已经将菜单声明添加到了
POIDetailMenu.xml文件。POIDetailFragment将继续使用相同的菜单 XML 声明。 -
在
POIDetailFragment中重写OnCreateOptionsMenu()和OnOptionsItemSelected()方法的片段版本,并添加以下代码块:public override void OnCreateOptionsMenu (IMenu menu, MenuInflater inflater) { inflater.Inflate(Resource.Menu.POIDetailMenu, menu); base.OnCreateOptionsMenu (menu, inflater); } public override bool OnOptionsItemSelected (IMenuItem item) { switch (item.ItemId) { case Resource.Id.actionSave: SavePOI (); return true; case Resource.Id.actionDelete: DeletePOI (); return true; default: return base.OnOptionsItemSelected(item); } }注意,在之前的代码块中,我们在填充
save和delete操作时没有对逻辑进行任何更改。这只是相同代码的副本,它被用于POIDetailActivity类。 -
在
POIDetailFragment中重写OnPrepareOptionsMenu()方法,以在创建新 POI 时禁用删除操作。
以下列表展示了如何在新 POI 创建时禁用删除操作:
public override void OnPrepareOptionsMenu (IMenu menu)
{
base.OnPrepareOptionsMenu (menu);
if (_poi.Id <= 0) {
IMenuItem item = menu.FindItem (Resource.Id.actionDelete);
item.SetEnabled (false);
item.SetVisible(false);
}
}
将 POIDetailFragment 添加到 POIDetailActivity
现在我们已经准备好了POIDetailFragment,我们可以将其添加到POIDetailActivity中使其完全功能化。正如本章前面所讨论的,一个片段可以通过两种方式添加到活动中:使用声明性方法,或者通过动态填充布局。对于POIDetailActivity,我们将动态地添加片段。
以下章节将带您了解将POIDetailFragment添加到POIDetailActivity活动中的代码重构过程。
修改 POI 详情活动布局
现在我们已经将整个详情视图布局放置在POIDetailFragment布局中,POIDetail活动布局将有一个简单的容器,可以容纳片段。为此,我们可以使用FrameLayout。
FrameLayout是一种特殊的布局管理器,通常用于显示单个项目。当添加多个视图时,它们被放置在一个堆栈中,最近添加的子视图位于顶部。
让我们对 POI 详情活动布局进行以下更改,并添加一个占位符视图以容纳片段:
-
打开
Resources/POIDetail.axml文件,在文档大纲视图中点击ScrollView,然后点击删除。注意布局将变为空。 -
在工具箱中搜索FrameLayout,并将其拖到模拟器窗口中作为根元素添加。
-
在文档大纲视图中点击FrameLayout,然后点击属性窗口。将视图 ID 设置为
poiDetailLayout。 -
在布局编辑器的源选项卡中点击,注意以下代码被生成:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout p1:minWidth="25px" p1:minHeight="25px" p1:layout_width="match_parent" p1:layout_height="match_parent" p1:id="@+id/poiDetailLayout " />
重构 POIDetailActivity 以添加 POIDetailFragment
目前,POIDetailFragment包含我们在POIDetailActivity中编写的逻辑,以执行添加、更新或删除操作。现在让我们重构POIDetailActivity,简化逻辑,并仅将其添加到其中:
-
从解决方案资源管理器中打开
POIDetailActivity类。 -
选择所有代码并删除除基本活动模板之外的所有内容。为了简化,你可以用以下代码替换整个
POIDetailActivity类:using Android.App; namespace POIApp { [Activity (Label = "POIDetailActivity")] public class POIDetailActivity : Activity { PointOfInterest _poi; protected override void OnCreate (Bundle bundle) { base.OnCreate (bundle); SetContentView (Resource.Layout.POIDetail); } } } -
现在是时候将
POIDetailFragment添加到Activity中。让我们首先初始化POIDetailFragment,并传递从POIListActivity接收到的 POI 详细信息数据。要显示所选 POI 的详细信息,
POIDetailFragment期望将 POI 数据传递给它。像任何其他活动一样,可以使用其Arguments属性将数据包传递给片段。Arguments属性有一组自己的PutXX()和GetXX()方法(XX代表数据类型,如字符串、双精度浮点数等),用于从片段发送和检索数据。在
SetContentView()方法之后立即将以下代码片段添加到OnCreate()回调中:var detailFragment = new POIDetailFragment(); detailFragment.Arguments = new Bundle (); if (Intent.HasExtra ("poi")) { string poiJson = Intent.GetStringExtra ("poi"); detailFragment.Arguments.PutString("poi", poiJson);} -
POIDetailActivity布局包含一个占位符布局,其中将添加POIDetailFragment。FragmentTransaction类可用于执行任何片段事务,例如添加、替换或删除片段。实例化片段事务以将POIDetailFragment添加到POIDetailActivity视图层次结构中:FragmentTransaction ft = FragmentManager.BeginTransaction(); ft.Add(Resource.Id.poiDetailLayout, detailFragment); ft.Commit(); -
现在让我们在模拟器中构建并运行应用程序。注意,你将看到我们在第五章“添加详细信息视图”中构建的相同输出。现在我们正在使用
POIDetailFragment,它包含添加、更新和删除 POI 的逻辑。
在创建平板电脑的多面板分割视图布局时,将重用相同的POIDetailFragment。
使用列表片段显示 POI 列表
到目前为止,我们已经使用Fragment子类创建了一个片段来显示兴趣点的详细信息。现在让我们使用专门的ListFragment类来创建一个新的片段,用于显示从服务器获取的 POI 列表。
理解列表片段
在我们开始创建POIListFragment之前,让我们了解ListFragment的以下关键概念:
-
ListFragment是一个用于显示来自不同数据源(如数组或游标)的项目列表的专门Fragment子类,这些数据源包含查询结果。 -
ListFragment提供了一个默认布局,其中包含一个单独的列表视图。但是,可以使用自己的自定义布局进行自定义。 -
在为
ListFragment使用自定义布局时,你的布局必须包含一个 ID 为@android:id/list的ListView对象。 -
与正常的
ListView一样,列表片段需要适配器的实例来操作。它公开了一个ListAdapter属性来设置列表适配器。 -
如果
ListFragment允许检测用户对列表项的点击事件,你可以重写OnListItemClick()。
创建 POIListFragment 布局
在我们的示例中,POIDetailsFragment将包含一个占满父视图宽度和高度的ListView和一个位于屏幕中心的ProgressBar。我们现在将为ListFragment创建一个自定义布局。
让我们从创建用于 POI 列表片段的新布局文件开始,并将其命名为POIListFragment.axml。
-
在解决方案面板中选择
Resources/Layout文件夹。 -
右键单击添加并选择新建文件。
-
在新建文件对话框中,点击Android并选择Android 布局,在名称字段中输入
POIListFragment,然后选择新建。 -
将
POIList.axml中的布局 XML 源代码复制并粘贴到新创建的POIListFragment.axml文件中。 -
注意,我们正在声明
ListFragment的自定义布局。我们必须将列表视图 ID 更改为@android:id/list。
POILsitFragment.axml文件将包含以下代码片段:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:minWidth="25px"
android:minHeight="25px">
<ListView
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@android:id/list" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/progressBar"
android:layout_centerInParent="true"
android:visibility="gone" />
</RelativeLayout>
使用 POIListFragment 创建用于显示 POI 列表的片段
现在我们已经为POIListFragment准备好了布局,让我们继续创建一个新的片段来显示 POI 列表。注意,我们没有对我们的POIListActivity逻辑进行任何重大的修改。相反,我们将重构相同的逻辑以使其与片段一起工作。
执行以下步骤以创建POIListFragment并使其完全可用:
-
在解决方案面板中选择
POIApp,右键单击添加并选择新建文件。 -
在新建文件对话框中,点击Android并选择新建文件,在名称字段中输入
POIListFragment,然后选择新建。 -
从
ListFragment扩展它并实现OnCreateView()以填充 POI 列表片段布局:namespace POIApp { public class POIListFragment: ListFragment { public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.Inflate(Resource.Layout.POIListFragment, container, false); return view; } } }注意,具有自定义布局的列表片段需要声明一个 ID 为
@android:id/list的ListView对象。 -
在下载完成后,声明以下一组私有变量以保存列表适配器实例、进度条和 POI 列表:
private ProgressBar progressBar; private List<PointOfInterest> poiListData; private POIListViewAdapter poiListAdapter; -
重写
OnAttach()方法并保存activity实例的引用:private Activity activity; public override void OnAttach (Activity activity) { base.OnAttach (activity); this.activity = activity; } -
在
OnCreateView()方法中的return语句之前添加以下代码块以初始化进度条:progressBar = view.FindViewById<ProgressBar> (Resource.Id.progressBar); -
创建一个名为
DownloadPoisListAsync的新方法(与在POIListActivity中声明的相同)。我们将使用之前为POIListActivity编写的相同逻辑,并进行以下小的修改。你会在使用this关键字的地方得到编译错误。将this替换为activity实例:public async void DownloadPoisListAsync(){ POIService service = new POIService (); if (!service.isConnected (activity)) { Toast toast = Toast.MakeText (activity, "Not conntected to internet. Please check your device network settings.", ToastLength.Short); toast.Show (); } else { progressBar.Visibility = ViewStates.Visible; poiListData = await service.GetPOIListAsync (); progressBar.Visibility = ViewStates.Gone; poiListAdapter = new POIListViewAdapter (activity, poiListData); this.ListAdapter = poiListAdapter; } }注意,在前面的代码块中,我们使用了在
ListFragment类中定义的ListAdapter属性来设置适配器实例。 -
重写
OnResume()生命周期方法并调用DownloadPoisListAsync()以在片段恢复时开始下载:public override void OnResume () { DownloadPoisListAsync (); base.OnResume (); } -
注意,
POIListFragment通过在OnCreateView()方法中添加以下语句来为片段添加动作栏菜单项,并在return语句之前启用此选项:SetHasOptionsMenu (true); -
重写片段版本的
OnCreateOptionsMenu和OnOptionsItemSelected方法,并粘贴以下代码。我们只是重用了POIListActivity中的相同逻辑:public override void OnCreateOptionsMenu (IMenu menu, MenuInflater inflater) { <span class="strong"><strong>inflater.Inflate(Resource.Menu.POIListViewMenu, menu);</strong></span> base.OnCreateOptionsMenu (menu, inflater); } public override bool OnOptionsItemSelected (IMenuItem item) { switch (item.ItemId) { case Resource.Id.actionNew: <span class="strong"><strong> Intent intent = new Intent (activity, typeof(POIDetailActivity));</strong></span> StartActivity (intent); return true; case Resource.Id.actionRefresh: DownloadPoisListAsync (); return true; default : return base.OnOptionsItemSelected(item); } } -
重写
OnListItemClick()方法,从ListFragment中检测列表行点击动作。以下代码块是我们为POIListActivity中的POIClicked()方法编写的相同逻辑的副本:public override void OnListItemClick (ListView l, View v, int position, long id) { <span class="strong"><strong>PointOfInterest poi = poiListData[position]; </strong></span> Intent poiDetailIntent = new Intent(activity, typeof(POIDetailActivity)); string poiJson = JsonConvert.SerializeObject(poi); poiDetailIntent.PutExtra("poi", poiJson); StartActivity(poiDetailIntent); }
到目前为止,列表片段将显示 POI 列表,并准备好添加到POIListActivity。
将POIListFragment添加到POIListActivity
为了使POIListFragment能够工作,它必须被添加到POIListActivity中。在前一节中,你学习了如何动态添加片段。现在,让我们使用声明性方法添加POIListFragment。
修改 POI 列表活动布局
可以使用布局中的<fragment>标签声明将片段添加到活动视图层次结构。<fragment>标签的layout_width和layout_height属性用于控制将片段的视图附加到活动布局时提供的LayoutParams。
对 POI 列表活动布局(POIList.axml)进行以下更改,以静态地添加POIListFragment:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="25px"
android:minHeight="25px"
android:orientation="horizontal">
<fragment
class="POIApp.POIListFragment"
android:id="@+id/listFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<fragment>属性的值应该是完全限定的片段类名。class属性表示指定的片段类将被附加到活动的内容布局。
将POIListFragment添加到POIListActivity
现在我们已经准备好了POIListFragment,并且已经将其附加到 POI 列表活动内容布局,我们不需要在POIListActivity内部做太多事情。使用以下代码块更新现有的POIListActivity类:
using Android.App;
using Android.Os;
namespace POIApp
{
[Activity (Label = "POI List")]
public class POIListActivity : Activity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
SetContentView (Resource.Layout.POIList);
}
}
}
到目前为止,我们已经通过创建两个片段来处理下载和显示 POI 详情的逻辑,完成了很多工作。现在,是时候编译并运行应用了。在 Android 模拟器上编译并运行应用:

注意,该应用将从 POI 网络服务下载数据,并在可滚动列表视图中显示 POI 列表。它的工作方式与我们在第五章“添加详情视图”中构建它时完全相同,但现在它使用的是片段。
为平板电脑创建多面板布局
到现在为止,你对片段的工作方式有了相当的了解,我们已经重构了现有的POIApp以使用片段。以下章节将指导你完成构建多面板布局和使应用兼容 Android 平板电脑的关键步骤。
在第六章“使应用方向感知”中,我们讨论了在 Android 中添加替代布局时需要考虑的各种配置限定符。其中一个限定符是设备的屏幕尺寸。Android 设备的尺寸范围从小型、正常、大型、超大到布局-xxlarge。自 Android 3.2 以来,Android 建议您使用sw<N>dp配置限定符来定义平板电脑的额外大型布局。
假设我们需要至少600dp的屏幕宽度来为POIApp构建多窗格布局。为此,我们需要在Resources目录下添加一个新的layout-sw600dp子目录。如果设备配置匹配600dp的宽度,Android 运行时会选择此目录下放置的布局。
对于平板电脑配置,POIListActivity布局将在单个活动中同时托管POIListFragment和POIDetailFragment。POI 列表片段将放置在屏幕左侧,覆盖总宽度的 40%,而详情片段则停靠在右侧,覆盖剩余的 60%总宽度。
Xamarin Studio 提供了一个易于使用的布局设计器,允许您轻松地为各种设备配置添加布局,而无需任何麻烦。您无需记住或手动添加不同配置限定符的不同文件夹名称。设计器负责为不同配置创建、编辑和删除替代布局。
以下部分展示了 Xamarin Studio 布局设计器的用法,用于为 Android 平板电脑添加替代布局:
-
打开
POIList.axml布局文件,并点击内容选项卡以打开布局设计器。 -
点击位于左上角的替代布局按钮。这启用了一个特殊编辑器来管理替代布局。以下截图展示了 Xamarin Studio 提供的特殊编辑器,用于编辑和管理不同配置的替代布局:
![img/zOfxWkZc.jpg]()
-
点击新建版本按钮,为给定配置添加布局的另一个版本。
-
这将打开一个对话框,要求输入不同的配置参数。目前,我们将平板电脑布局的最小宽度视为
600dp。输入最小的屏幕宽度值600,然后点击添加:![img/mW606wsD.jpg]()
-
注意,将创建一个名为
layout-sw600dp的新文件夹,并在项目资源管理器中创建POIList布局的新版本。 -
选择
sw600dp布局进行编辑。 -
在工具箱中搜索FrameLayout,并将其拖到模拟器窗口中,作为根元素添加。这将用于在用户从列表中选择任何 POI 时动态添加
POIDetailFragment。 -
在文档大纲视图中点击FrameLayout,然后点击属性窗口。将视图 ID 设置为
poiDetailLayout。 -
现在我们已经在屏幕上有一个列表片段和一个帧布局。让我们为列表分配 40%的屏幕宽度以显示,为详情布局分配 60%的宽度。这可以通过使用
weight属性来完成。从文档大纲视图中选择
listFragment,并将权重属性更改为2以及layout_width属性更改为0dp。这允许权重属性决定视图的宽度。现在,从文档大纲视图中选择detailsLayout,并将权重属性更改为3以及layout_width属性更改为0dp。 -
将
listFragment和detailsLayout的Padding left和Padding right属性设置为20dp。以下截图展示了到目前为止构建的布局:
![图片]()
-
点击源选项卡,注意以下代码被生成:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:minWidth="25px" android:minHeight="25px" android:orientation="horizontal"> <fragment class="POIApp.POIListFragment" android:id="@+id/listFragment" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" android:paddingLeft="20dp" android:paddingRight="20dp" /> <FrameLayout android:minWidth="25px" android:minHeight="25px" android:layout_width="0dp" android:layout_height="match_parent" android:id="@+id/poiDetailLayout" android:layout_weight="3" android:paddingLeft="20dp" android:paddingRight="20dp" /> </LinearLayout>
将 POIApp 更新为支持多面板分割布局
在 Android 平板电脑中,POI 活动布局被声明为通过共享相同的屏幕空间来容纳 POI 列表和 POI 详情片段。POIListFragment被静态添加到布局中,当用户从列表中选择任何 POI 项时,POIDetailFragment将被动态添加。具有detailsLayout ID 的FrameLayout用作占位符以容纳POIDetailFragment。然而,在移动设备中,当用户点击任何 POI 列表项时,它将继续启动POIDetailActivity。
以下步骤将指导您进行必要的更改,以使POIApp在 Android 平板电脑上的多面板布局中工作:
-
首先,我们需要找出设备是否正在多面板模式下运行。这有助于了解您是想在新的活动中启动还是更新同一活动中的片段内容。让我们声明一个静态布尔变量,用于保存应用程序是否在双模式下的信息:
public static bool isDualMode = false; -
如果
detailsLayout不为null且可见,则我们可以假设设备正在多面板视图模式下运行。在POIListActivity的OnCreate()方法中添加以下代码块以初始化isDualMode变量:var detailsLayout = FindViewById (Resource.Id.poiDetailLayout); if (detailsLayout != null && detailsLayout.Visibility == ViewStates.Visible) { isDualMode = true; }else{ isDualMode = false; } -
目前,
POIListFragment中的新操作会将用户带到POIDetailActivity。现在对于平板电脑,而不是调用另一个活动,POIDetailFragment将显示在同一活动的右侧。将以下代码块添加到
OnOptionsItemSelected()方法下的新操作:case Resource.Id.actionNew: if (POIListActivity.isDualMode) { var detailFragment = new POIDetailFragment(); FragmentTransaction ft = FragmentManager.BeginTransaction (); ft.Replace (Resource.Id.poiDetailLayout, detailFragment); ft.Commit (); } else { Intent intent = new Intent (activity, typeof(POIDetailActivity)); StartActivity (intent); } return true;注意,由于我们没有传递任何 POI 的详细信息,
POIDetailsFragment将显示空字段并允许用户添加新的 POI。 -
如前所述,我们还需要在
OnListItemClick()方法中实现相同的逻辑。当用户点击 POI 列表项时,而不是将他们带到另一个活动,POI 详情将在同一活动的右侧详情面板中显示。使用以下代码片段更新
OnListItemClick()方法:public override void OnListItemClick (ListView l, View v, int position, long id) { PointOfInterest poi = poiListData[position]; if (POIListActivity.isDualMode) { var detailFragment = new POIDetailFragment(); detailFragment.Arguments = new Bundle (); detailFragment.Arguments.PutString("poi", JsonConvert.SerializeObject(poi)); FragmentTransaction ft = FragmentManager.BeginTransaction (); ft.Replace (Resource.Id.poiDetailLayout, detailFragment); ft.Commit (); } else { Intent poiDetailIntent = new Intent(activity, typeof(POIDetailActivity)); poiDetailIntent.PutExtra("poi", JsonConvert.SerializeObject(poi)); StartActivity(poiDetailIntent); } }
我们几乎完成了!现在让我们在 Android 平板模拟器中运行 POIApp。你会注意到 POI 应用显示了多窗格布局,如下面的截图所示:

与 DialogFragment 一起工作
在本章中,我们已经介绍了如何使用片段和 ListFragment 来优化 Android 平板的布局。现在让我们讨论另一个专门的片段子类,DialogFragment。POIApp 的当前实现使用 AlertDialog 在删除 POI 之前向用户显示确认对话框。在本节中,我们将用 DialogFragment 替换 AlertDialog 实现。
DialogFragment 用于显示一个作为浮动对话框窗口的片段,该窗口会出现在当前窗口的顶部。DialogFragment 是片段的子类,并在 Android 3.0 API 级别 11 中引入。谷歌建议你使用 DialogFragment 来实现对话框,原因如下:
-
与普通片段一样,
DialogFragment管理自己的生命周期。例如,用户按下设备的返回按钮或旋转屏幕等事件都在DialogFragment中处理。 -
DialogFragmentUI 可以被重用并嵌入到另一个活动内部。例如,如果你希望你的对话框在不同屏幕尺寸上显示不同,你可以在活动布局中嵌入DialogFragment,这与普通片段类似。 -
虽然
DialogFragment是在 Android 3.0 中引入的,但谷歌发布了 Android 支持库,通过它你可以使用DialogFragment类在运行 Android 1.6 或更高版本的设备上。接下来的部分(适用于旧版 Android 设备的片段)将描述如何将 Android 支持库添加到 Xamarin Studio 解决方案中。
在牢记前面的要点的基础上,我们将替换 AlertDialog 并在 POIDetailFragment 类中使用 DialogFragment。创建对话框片段的过程与创建普通片段类似。你需要首先创建一个继承自 DialogFragment 的类,并重写以下方法之一来为你的对话框提供视图层次结构:
-
OnCreateView(): 这个方法用于填充对话框的布局。它与Fragment.OnCreateView()回调相同。如果你想使你的片段 UI 可重用/嵌入到另一个视图内部,你必须重写OnCreateView()并填充布局。 -
OnCreateDialog(): 这个方法返回一个Dialog实例。在创建对话框布局时,这个方法会自动被调用。由于我们已经在POIApp中使用了AlertDialog;在这个例子中,我们将重写OnCreateDialog()来创建DialogFragment。
以下步骤将展示如何使用 DialogFragment 类向用户显示 删除 确认对话框:
-
创建一个新的片段类,并将其命名为
DeleteDialogFragment。 -
从
DialogFragment类继承DeleteDialogFragment类:public class DeleteDialogFragment : DialogFragment { } -
重写
OnCreateDialog()方法并添加以下代码片段。在OnCreateDialog()方法中使用的代码块与我们之前在POIDetailFragment的DeletePOI()方法中使用的代码块类似:public override Dialog OnCreateDialog (Bundle savedInstanceState) { AlertDialog.Builder alertConfirm = new AlertDialog.Builder(this.Activity); alertConfirm.SetTitle("Confirm delete"); alertConfirm.SetCancelable(false); alertConfirm.SetPositiveButton("OK", delegate {}); alertConfirm.SetNegativeButton("Cancel", delegate {}); alertConfirm.SetMessage("Are you sure you want to delete?"); return alertConfirm.Create (); } -
我们已经构建了一个基本的
DialogFragment版本。现在我们需要初始化片段并使用FragmentTransaction来显示对话框。将以下代码片段添加到POIDetailFragment DeletePOI()方法中:FragmentTransaction ft = FragmentManager.BeginTransaction(); DeleteDialogFragment dialogFragment = new DeleteDialogFragment (); dialogFragment.Show(ft, "dialog");任何其他片段的操作都是通过片段事务来完成的。使用
Show()方法将对话框片段添加到活动视图层次结构中,然后提交事务。这需要两个参数:一个片段事务的实例和一个标签。字符串值标签可以在以后用于从片段管理器检索片段实例。 -
现在运行并测试
POIApp。注意,对话框片段工作得很好。然而,它没有显示用户想要删除的 POI 的名称。 -
要在删除确认对话框中显示 POI 的名称,我们需要将 POI 名称传递给
DeleteDialogFragment。这可以通过使用片段的Arguments属性来实现。在片段初始化之后和调用
dialogFragment.Show()之前,将以下代码片段添加到DeletePOI()方法中:Bundle bundle = new Bundle(); bundle.PutString("name", _poi.Name); dialogFragment.Arguments = bundle; -
现在我们需要在
DeleteDialogFragment类的OnCreateDialog()方法中进行以下更改以检索和显示 POI 的名称:public override Dialog OnCreateDialog (Bundle savedInstanceState) { <span class="strong"><strong>string poiName = Arguments.GetString("name");</strong></span> AlertDialog.Builder alertConfirm = new AlertDialog.Builder(this.Activity); ... .... <span class="strong"><strong>alertConfirm.SetMessage(String.Format("Are you sure you want to delete {0}?", poiName));</strong></span> return alertConfirm.Create (); } -
运行
POIApp并注意,删除确认对话框现在显示了要删除的 POI 的名称。然而,当你点击确定按钮时,对话框被关闭,但删除操作没有执行。我们将从片段对话框传递事件回调到POIDetailFragment。这可以通过使用TargetFragment属性轻松实现。在添加对话框片段时,我们需要通过调用
SetTargetFragment()方法向对话框提供目标片段信息。此方法接受两个参数:一个片段实例,它是对话框的目标,以及一个可选的整型请求代码。以下代码片段显示了在
DeletePOI()方法中所需的更改:protected void DeletePOI() { FragmentTransaction ft = FragmentManager.BeginTransaction(); DeleteDialogFragment dialogFragment = new DeleteDialogFragment(); <span class="strong"><strong>dialogFragment.SetTargetFragment (this,0);</strong></span> Bundle bundle = new Bundle(); bundle.PutString("name", _poi.Name); dialogFragment.Arguments = bundle; dialogFragment.Show(ft, "dialog"); } -
现在,我们可以通过使用
TargetFragment属性从对话框片段访问POIDetailsFragment的实例,并调用DeletePOIAsync()方法来启动删除 POI 的 Web 服务请求。以下代码展示了
DeleteDialogFragment类中应该包含的内容:public class DeleteDialogFragment : DialogFragment { public override Dialog OnCreateDialog (Bundle savedInstanceState) { <span class="strong"><strong>POIDetailFragment targetFragment = (POIDetailFragment) TargetFragment;</strong></span> string poiName = Arguments.GetString("name"); AlertDialog.Builder alertConfirm = new AlertDialog.Builder(this.Activity); alertConfirm.SetTitle("Confirm delete"); alertConfirm.SetCancelable(false); alertConfirm.SetPositiveButton("OK", (sender, e) => { <span class="strong"><strong>targetFragment.DeletePOIAsync();</strong></span> }); alertConfirm.SetNegativeButton("Cancel", delegate {}); alertConfirm.SetMessage(String.Format("Are you sure you want to delete {0}?", poiName)); return alertConfirm.Create (); } }
现在运行并测试POIApp。注意,DeletePOI与FragmentDialog一起完全功能正常。
适用于旧版 Android 设备的片段
如我们之前讨论的,Fragment API 是在 Android 3.0 API 级别 11 中添加的。在迄今为止讨论的POIApp示例中,我假设测试我的应用程序的用户将使用 Android 3.0 或更高版本。然而,如果你的业务需求要求你支持旧设备上的应用程序,那么你仍然可以利用新的 Fragment API 类。
由于市场上仍有一部分在使用 3.0 版本,谷歌提供了支持库,允许你在保持向后兼容的同时实现片段。Fragment 功能在 V4 支持库中可用,且在 Android 1.6 API 级别 4 上运行。
你不能直接使用我们在本章中使用的Fragment类。你必须将支持包添加到你的解决方案中。对于 Xamarin Studio 5.9.x 或更高版本的用户,在创建新解决方案时,你可以为目标平台选择选择最大兼容性以将支持包包含到你的项目中。
或者,你可以通过导航到包 | 添加包来添加 Android 支持包。这将打开 NuGet 包浏览器,如下面的截图所示。你可以搜索支持包,然后点击添加包按钮将选定的包添加到你的项目中:

在将 Android 支持包添加到项目后,你可以开始为旧版 Android 设备实现片段功能。支持包的片段类基本上与原生 Android 3.0 兼容的片段类相似。然而,以下是一些你的应用程序在使用支持兼容 API 时需要进行的更改:
-
将要承载支持片段的活动必须现在扩展并从
Support.V4.App.FragmentActivity继承。 -
使用
Support.V4.App.Fragment而不是Android.App.Fragment -
使用
SupportFragmentManager而不是FragmentManager
使用片段处理配置更改
在第六章使你的应用方向感知中,我们讨论了 Android 活动如何响应设备配置更改。在本节中,我们将探讨在配置更改事件中片段的行为。
与活动类似,片段提供了OnSaveInstanceState()方法,可以用来保存你的片段状态。这基本上与它的活动对应方法相同。OnSaveInstanceState()方法提供了一个Bundle实例,我们可以将其中的数据包导出。保存的片段实例可以从OnCreate()、OnActivityCreated()或OnCreateView()回调中检索。
以下步骤将指导你如何在设备配置更改时保存和保留 POI 列表的滚动位置:
-
在
OnSaveInstanceState()方法中获取第一个可见列表项的索引并将其保存到 bundle 中:public override void OnSaveInstanceState (Bundle outState) { base.OnSaveInstanceState (outState); int currentPosition = ListView.FirstVisiblePosition; outState.PutInt ("scroll_position", currentPosition); }在前面的代码片段中,字符串
scroll_position被用作键来保存ListView的当前滚动位置。 -
在
OnCreate()中恢复保存的列表的滚动位置。注意我们必须使用我们保存时使用的相同键:public override void OnCreate (Bundle savedInstanceState) { base.OnCreate (savedInstanceState); if (null != savedInstanceState) { scrollPosition = savedInstanceState.GetInt ("scroll_position"); } } -
将以下代码片段添加到
POIListFragment类中的DownloadPoisListAsync()方法。这将队列一个消息以将 POI 列表视图滚动到之前保存的滚动位置:public async void DownloadPoisListAsync(){ POIService service = new POIService (); if (!service.isConnected (activity)) { ... ... } else { ... ... this.ListAdapter = poiListAdapter; ListView.Post(() => { ListView.SetSelection(scrollPosition); }); } }
在 Android 设备或模拟器上构建和运行应用程序。更改您的设备方向并注意现在列表视图的滚动位置已保留。
摘要
在本章中,我们涵盖了创建和管理片段的许多内容,包括以下主题:
-
Fragment类及其功能,以及创建和管理片段 -
如何使用声明性方法通过
<fragment>标签将片段添加到活动中 -
使用
ListFragment和ListAdapter来填充数据 -
如何从 Xamarin Studio 布局设计器添加替代布局资源
-
如何重用片段以创建适用于 Android 平板电脑的多窗格分割视图布局
-
如何使用
DialogFragment创建对话框,并使用OnCreateDialog()方法创建其视图层次结构 -
优化
POIApp布局以支持大屏幕 Android 平板电脑 -
处理配置更改以保存和保留片段状态
下一章将指导你使用 SQLite 在 Android 中处理数据持久化。
让我们看看以下参考:
-
android-developers.blogspot.in/2012/11/designing-for-tablets-were-here-to-help.html -
developer.android.com/distribute/essentials/quality/tablets.html
第八章. 创建数据存储机制
我们现在转向数据存储需求。我们需要一种方法来存储从服务器获取的 兴趣点(POI)数据列表,并在设备离线时使列表可访问。本章将展示如何使用内置的 SQLite 数据库引擎以跨平台的方式存储和检索 POI 数据。本章将涵盖以下主题:
-
数据存储解决方案的方法
-
Android 中不同的应用程序存储选项
-
使用首选项存储键值对
-
在 Xamarin.Android 中使用 SQLite 数据库存储
-
使用 SQLite.NET ORM 组件
-
执行数据库 CRUD 操作
-
使用 NUnitLite 对 Android 应用进行单元测试
-
在
POIApp中实现缓存
数据存储解决方案主要分为两大类:使用网络服务在云端存储或使用设备的本地存储。在云端存储数据相对于本地数据存储选项提供了巨大的优势,但在某些情况下,例如运行离线应用程序或游戏,通常需要将数据存储在设备的存储中。
当前的 POIApp 使用网络服务将 POI 列表存储在服务器上,而移动应用程序通过发出 REST API 调用来检索列表。到目前为止,它运行得很好。然而,它始终需要互联网连接来获取和显示 POI 列表。让我们通过在本地存储 POI 列表并即使在设备离线时也能访问它们来克服这个问题。
Android 中的数据存储
Android 支持多种用于本地持久化数据的解决方案。即使应用程序关闭或设备重启,持久化的数据仍然可以访问。此类数据的例子包括设备设置、联系人列表、浏览器书签或任何此类特定应用程序的数据。
下表展示了 Android 平台可用的不同数据存储选项:
存储选项
描述
共享首选项
这是您的应用程序的私有数据存储,仅持久化原始键值数据对。当用户卸载应用程序时,此数据将被删除。
内部存储
这将数据存储在设备的内部内存中,直到应用程序从设备中卸载,数据都是可用的。这些数据仅对您的应用程序是私有的,其他应用程序无法访问它。
外部文件存储
这将数据存储在共享的外部存储中,如外部 SD 卡。存储的数据是公开的。其他应用程序或用户可以通过将设备连接到计算机来访问这些文件。
SQLite 存储
这是一个结构化的私有数据存储。从应用程序创建的 SQLite 数据库只能由同一应用程序访问。
本章简要介绍了共享首选项和 SQLite 存储选项。您可以从官方 Xamarin 网站自行研究内部和外部存储选项。
共享首选项
共享偏好是持久化的 键/值 数据对,用于存储原始数据对,例如 bool、float、int、string 和 long。在 Android 偏好中保存的数据在不同的应用程序会话之间持久化,并且对创建它的应用程序是私有的。任何其他应用程序都无法访问它。
要使用共享偏好保存数据对,您首先需要获取 ISharedPreferences 接口的实例。共享偏好可以特定于一个活动,也可以使应用程序中所有活动的共享偏好全局化。如果您想创建一个特定于活动的单个偏好文件,可以使用 Activity.GetPreferences 来获取 ISharedPreferences 接口的实例,或者您可以通过传递偏好名称和操作模式到应用程序上下文中调用 GetSharedPreferences 方法来获取应用程序级别的偏好:
ISharedPreferences prefs = Application.Context.GetSharedPreferences ("PREF_NAME", FileCreationMode.Private);
现在,让我们调用 Edit() 方法来获取 ISharedPreferencesEditor 的实例。这将匹配对共享偏好中值所做的所有更改,并且只有在调用 Commit() 或 Apply() 时才会保存:
ISharedPreferencesEditor editor = prefs.Edit();
editor.PutInt("your_key1" ,10);
editor.PutString("your_key2", "Xamarin Example");
editor.Apply();
要从共享偏好中读取值,我们可以通过提供保存数据时使用的相同键来使用 GetXX() 方法,其中 XX 代表支持的原始类型。以下代码片段检索了之前步骤中存储的值:
var value1 = prefs.GetInt ("your_key1", 0);
var value2 = prefs.GetString ("your_key2", null);
SQLite 数据库存储
SQLite 是一个开源、轻量级且支持事务的数据库引擎,它随移动平台(包括 Android、iOS 和 Windows 手机)一起提供。根据官方文档,Android 正在使用 SQLite 版本 3.4.0。它是一个广泛使用的、独立的、关系型数据库引擎,不需要单独的服务器进程。
以下是一些使 SQLite 数据库引擎成为当今最广泛使用的数据库引擎之一的优点:
-
它是一个开源项目,一个庞大的开源社区正在积极为其工作。
-
它没有服务器,因此不需要服务器基础设施。
-
它是一个轻量级引擎,提供了一套用于执行数据库事务的类。
-
完整的数据库是一个存储在设备内存中的单个文件,该文件对您的应用程序是私有的。SQLite 数据库的隐私性归结为平台文件系统的隐私性。
因此,我们决定使用 SQLite 来缓存 POI 列表。当列表成功下载后,我们将把 POI 保存到数据库中,并在下载成功时定期更新它。当设备离线或无法从服务器获取更新列表时,缓存的 POI 列表可以在屏幕上显示。
虽然 SQLite 内置在 Android 中,并提供了一套 API 来执行所有数据库 CRUD(创建、读取、更新和删除)操作,但我们将使用组件存储库中的 SQLite.ORM 组件。下一节将指导您使用 SQLite.ORM 组件执行不同的数据库操作。
使用 SQLite.net ORM 组件
SQLite.NET 对象关系映射(ORM)是一个开源库,允许 .NET 和 Mono 应用程序将数据对象存储在 SQLite 数据库中。它是一个轻量级且易于使用的组件,允许你保存和检索数据对象,无需担心编写任何 SQL 查询。这是一个在 Xamarin 组件商店中可用的免费组件。它最初是为 iPhone 上的 MonoTouch 设计的,但后来扩展到支持其他平台,包括 Android、Windows 和 Silverlight 平台。感谢 Frank A. Krueger 开发和维护此组件。
有三种方法可以将 SQLite.NET ORM 添加到 Xamarin 应用程序中。你可以通过从 GitHub 开源代码仓库、NuGet 或 Xamarin 组件商店下载所需文件来实现:
-
SQLite.NET 是一个非常薄的库,只有一个文件。从 GitHub 下载
SQLite.cs文件并将其添加到你的应用程序中。SQLite.NET 库直接绑定到每个平台的 SQLite 数据库引擎。 -
要从 Xamarin 组件商店添加,你需要遵循我们在第四章 添加列表视图 中添加
UrlImageViewHelper组件时使用的相同步骤。 -
要将其作为 NuGet 包捆绑添加,你可以从你的解决方案资源管理器导航到 包 | 添加包 以打开 NuGet 画廊窗口。搜索
SQlite.NET并选择它以将其添加到你的项目中。
一旦将 SQlite.NET ORM 组件的引用添加到项目中,我们就可以专注于执行数据库操作。
使用属性标记 POIApp 以进行持久化
SQLite.NET ORM 带来一组属性,允许你标记要持久化到数据库的类和字段。你可以应用以下任何属性来控制表的构建:
属性
用法
Table
默认情况下,类名称用作表名称。如果你想要指定自己的表名称,可以使用此属性。这是一个应用于类的可选属性。
Column
默认情况下,属性名称用作列名称。此属性提供你指定自己的列名称的控制权。
PrimaryKey
此属性是表的键。仅支持单列主键。
AutoIncrement
此属性在插入时由数据库自动生成。属性类型应为整数,并且还应标记为 PrimaryKey 属性。
Indexed
应为此属性创建一个索引。
Unique
此属性将在表中是唯一的。
MaxLength
此属性指定 varchar 的最大长度。默认最大长度为 140。
Ignore
此属性将不会在表中。
NotNull
此属性不能为空。
对于 POIApp,我们可以使用 PointOfInterest 类前述列表中的某些属性:
using SQLite;
namespace POIApp
{
[Table("POITable")]
public class PointOfInterest
{
[PrimaryKey, AutoIncrement, Column("_id")]
public int Id { get; set;}
[NotNull]
public string Name { get; set; }
[MaxLength(1000)]
public string Description { get; set; }
[MaxLength(150)]
public string Address { get; set; }
public string Image { get; set; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
}
}
在前面的代码片段中,请注意我们声明了表名将是 POITable,Id 字段被标记为主键,并且 Name 字段不能为空。
添加数据库辅助类
现在我们需要创建一个标准类,该类将定义一组方法以允许基本的 CRUD 操作。此类可能被多次访问;因此,我们将此类标记为单例类以防止创建相同类的多个实例。
要创建数据库辅助实现类,请执行以下步骤:
-
创建一个名为
DBManager的类。 -
要使
DBManager类成为单例类,首先声明默认构造函数为私有:public class DBManager { private DBManager() { } }
现在声明一个 DBManager 类的私有静态实例并执行早期初始化。我们需要添加一个静态获取方法来使 DBManager 类可访问。将以下代码片段添加到 DBManager 类中:
private static readonly DBManager instance = new DBManager();
public static DBManager Instance
{
get
{
return instance;
}
}
注意,创建单例类的方法有很多种。我选择使用 早期初始化 方法来使用单例类。有关单例类实现的更多信息,请访问 MSDN 开发者网站。
在保存数据之前,我们首先需要创建一个数据库。让我们通过传递文件路径到 SQLiteConnection 类构造函数来创建一个空数据库或打开一个现有的数据库。将以下 CreateTable() 方法添加到您的 DBManager 类中:
SQLiteConnection dbConn;
private const string DB_NAME = "PointOfInterest_DB.db3";
public void CreateTable()
{
var path = System.Environment.GetFolderPath (System.Environment.SpecialFolder.Personal);
dbConn = new SQLiteConnection (System.IO.Path.Combine (path, DB_NAME));
dbConn.CreateTable<PointOfInterest> ();
}
注意以下代码:
-
DB_NAME常量定义了数据库的名称。数据库文件将使用此名称创建,并保存到路径变量指定的位置。 -
SQLite 数据库文件路径可能因平台而异。Android 和 iOS 都使用环境类来构建有效的路径。
-
SQLiteConnection类构造函数如果已存在则使用指定名称打开数据库,否则创建一个新的数据库。 -
CreateTable()方法如果存在则打开表,否则创建一个新的表。
创建或更新 POI 记录
一旦您的数据库连接打开,我们就可以执行不同的数据库操作。首先,让我们创建一个名为 SavePOI() 的方法来保存数据库中的 POI 对象。SQLiteConnection 类提供了如 Insert、InsertOrReplace、InsertAll、Update 和 UpdateAll 等方法,用于在数据库中创建或更新记录。
我们将使用 InsertOrReplace 方法,因为它对我们来说很方便。此方法在数据库中查找相同的记录,如果它已经存在,则更新它,否则插入一个新的记录。为了使 InsertOrReplace() 方法正常工作,您的表必须有一个主键。
让我们在 SavePOI 方法中添加以下代码片段:
public int SavePOI(PointOfInterest poi)
{
int result = dbConn.InsertOrReplace (poi);
Console.WriteLine ("{0} record updated!", result);
return result;
}
从数据库中读取 POI 详细信息
当设备处于离线模式运行,或者如果应用未能从服务器下载 POI 列表数据时,POIApp会读取 POI 对象的列表。将以下GetPOIListFromCache()方法添加到数据库中检索所有记录:
public List<PointOfInterest> GetPOIListFromCache()
{
var poiListData = new List<PointOfInterest> ();
IEnumerable<PointOfInterest> table = dbConn.Table<PointOfInterest> ();
foreach (PointOfInterest poi in table)
{
poiListData.Add (poi);
}
return poiListData;
}
GetPOIListFromCache方法返回数据库中所有可用的 POI 列表。如果你正在通过 ID 查找特定的 POI,以下方法将帮助你:
public PointOfInterest GetPOI(int poiId)
{
PointOfInterest poi = dbConn.Table<PointOfInterest>().Where(a => a.Id.Equals(poiId)).FirstOrDefault();
return poi;
}
从数据库中删除 POI 数据
就像从数据库中读取记录一样,我们可以通过 POI ID 逐个删除记录,或者清除所有数据库记录。将以下方法添加到DBManager类中。它们简单直接,相当直观:
public int DeletePOI(int poiId)
{
int result = dbConn.Delete<PointOfInterest>(poiId);
Console.WriteLine("{0} record effected!", result);
return result;
}
public int ClearPOICache()
{
int result = dbConn.DeleteAll<PointOfInterest>();
Console.WriteLine("{0} records effected!", result);
return result;
}
现在我们已经定义了执行不同数据库操作的所有方法。下一步是在使用POIApp之前,通过编写单元测试用例来验证每个方法。
使用 Xamarin.Android NUnitLite
你可能熟悉一个叫做测试驱动开发(TDD)的过程。从高层次来看,这种方法建议你创建自动化的单元测试用例来测试你的软件需要支持的功能,并使用这些测试用例来驱动开发和单元测试周期。
本章不会详细介绍测试驱动开发背后的概念,但我们将介绍 Xamarin.Android 提供的一个功能,该功能支持使用 TDD 的团队。这个功能是NUnitLite。NUnitLite 是一个轻量级、开源的测试框架,其理念与NUnit相同。它被设计为使用最少的资源,非常适合嵌入式和移动软件开发。
当使用 NUnitLite 时,你创建名为测试固定点的类。这些类包含用于测试测试目标的各个方面的测试方法;在我们的案例中,是DBManager类。为了将一个类指定为测试固定点或方法指定为测试方法,NUnitLite 使用.NET 属性。一旦创建了测试固定点和测试方法,Xamarin.Android 提供了一个用户界面,允许在 Android 模拟器或设备上执行测试。
要开始使用 NUnitLite,我们需要在我们一直在使用的解决方案面板中创建一个测试项目。
要创建一个测试项目,执行以下步骤:
-
从 Xamarin Studio 的解决方案面板中选择
POIApp解决方案。 -
右键单击并选择添加新项目。
-
在新项目对话框的左侧,转到C# | Android。
-
在对话框的模板列表中,选择对话框中间的Android 单元测试项目。
-
输入
POITestApp作为名称,然后点击确定。新的单元测试项目被创建并添加到POIApp解决方案中。 -
前往新项目的选项对话框,将包名设置为
POITestApp,并确认目标框架设置为最新的可用 Android SDK 框架。
你会注意到新的单元测试项目有以下文件:
-
MainActivity.cs:此活动继承自TestSuiteActivity,并在我们运行测试时提供测试套件用户界面。基本上,它允许我们运行测试并查看结果。 -
TestsSample.cs:此类充当测试固定装置,并允许我们添加测试方法,这些方法将测试DBManager提供的功能。
现在,我们需要创建测试方法来测试 DBManager 类执行的数据操作功能。
准备测试
NUnitLite 提供了一个执行可能需要的任何初始化代码的地方。在我们的情况下,我们需要创建 DBManager 类的实例,测试方法稍后会与其交互。Setup() 方法是完美的选择,因为它将在每个测试之前被调用。以下步骤将帮助你在 Xamarin Studio 中设置一个新的测试用例:
-
将
TestsSample.cs文件重命名为POITestFixture.cs。同时,将文件内的相应类也重命名。 -
在
POITestApp中,选择 引用,右键单击它,然后选择 编辑引用。在 编辑引用 对话框中,选择 项目 选项卡,勾选POIApp项目,然后单击 确定。POITestApp需要引用POIApp,以便它可以与DBManager类一起工作。小贴士
一些版本的 Xamarin Studio 存在一个错误,即使将项目引用添加到测试项目中,引用也不会链接。在这种情况下,您需要转到 编辑引用 对话框中的 .NET 程序集选项卡,并手动浏览到
POIApp/bin/Debug/POIApp.dll文件。 -
打开
POITestFixture类并删除除Setup()之外的所有其他方法。 -
在
Setup()方法中,从DBManager类调用CreateTable()方法:[TestFixture] public class POITestFixture { [SetUp] public void Setup () { DBManager.Instance.CreateTable (); } }
创建测试方法
现在真正的任务开始了;我们需要创建测试方法来测试每个重要的场景。在数据服务的情况下,我们需要确保我们涵盖了以下主题:
-
创建一个新的 POI
-
更新现有的 POI
-
删除现有的 POI
我们可以选择测试的情景还有很多,但前面的小集合应该有助于验证我们数据库逻辑的基本功能。
CreatePOI 测试
我们将首先开始的第一个测试方法是 CreatePOI(),正如其名称所暗示的,我们将测试创建和保存新 POI 的过程。为了完成这个任务,我们需要执行以下步骤:
-
创建
PointOfInterest的新实例并填写一些属性。 -
在
DBManager类上调用SavePOI()方法。 -
save方法返回一个整数,表示更新的记录数。确保它返回值1。 -
根据保存的 ID 调用
GetPOI()来检索 POI。 -
使用
Assert类来确认检索到的 POI(引用不是 null)以及 POI 的名称符合预期。
以下代码展示了 CreatePOI() 的实现:
[Test]
public void CreatePOI ()
{
int testId = 1091;
PointOfInterest newPOI = new PointOfInterest ();
newPOI.Id = testId;
newPOI.Name = "New POI";
newPOI.Description = "POI to test creating a new POI";
newPOI.Address = "100 Main Street\nAnywhere, TX 75069";
//Saving poi record
int recordsUpdated = DBManager.Instance.SavePOI (newPOI);
//Check if the number of records updated are same as expected
Assert.AreEqual (1, recordsUpdated);
// verify if the newly create POI exists
PointOfInterest poi = DBManager.Instance.GetPOI (testId);
Assert.NotNull (poi);
Assert.AreEqual (poi.Name, "New POI");
}
删除 POI 测试
接下来,我们将实现DeletePOI()。同样,我们希望DeletePOI()独立于其他测试,因此我们首先需要创建一个稍后将被删除的 POI。
在调用DeletePOI()时,将执行以下步骤:
-
创建一个新的
PointOfInterest实例并填写一些属性。 -
在
DBManager类上调用SavePOI()方法。 -
使用
GetPOI()根据保存的 ID 检索 POI。 -
使用
DeletePOI()来删除 POI 文件并将其从数据库中删除。 -
使用
GetPOI()根据保存的 ID 检索 POI。 -
使用
Assert类来确保找不到 POI(引用为 null)。
以下代码展示了DeletePOI()的实现:
[Test]
public void DeletePOI ()
{
int testId = 1019;
PointOfInterest testPOI = new PointOfInterest ();
testPOI.Id = testId;
testPOI.Name = "Delete POI";
testPOI.Description = "POI being saved so we can test delete";
testPOI.Address = "100 Main Street\nAnywhere, TX 75069";
DBManager.Instance.SavePOI (testPOI);
PointOfInterest deletePOI = DBManager.Instance.GetPOI (testId);
Assert.NotNull (deletePOI);
DBManager.Instance.DeletePOI(testId);
PointOfInterest poi = DBManager.Instance.GetPOI (testId);
Assert.Null (poi);
}
此外,我们将实现ClearCache()以验证对ClearPOICache()的调用是否清除了所有数据库记录。以下步骤将在ClearCache测试方法中执行:
-
在
DBManager类上调用ClearPOICache()以从数据库中删除所有记录。 -
在
DBManager类上调用GetPOIListFromCache以从数据库中获取记录列表。 -
使用
Assert类来确保从服务器检索的记录数为0。
以下代码展示了ClearCache()的实现:
[Test]
public void ClearCache ()
{
DBManager.Instance.ClearPOICache ();
List<PointOfInterest> poiList = DBManager.Instance.GetPOIListFromCache ();
Assert.AreEqual (0, poiList.Count);
}
执行测试
现在测试已经开发完毕,我们准备执行它们。为此,我们只需使用 Android 模拟器或物理设备运行测试应用。要在模拟器中执行测试,请执行以下步骤:
-
使用 Android 模拟器运行
POITestApp。请注意,POITestApp没有被设置为启动项目,所以当你选择运行时,你需要选择项目。你可以通过选择它,右键单击它,并选择设置为启动项目来将POITestApp设置为启动项目。一旦POITestApp开始运行,你应该会看到以下屏幕,当应用已部署并启动时:![]()
-
通过点击运行测试标签来执行测试。你应该会看到一个绿色的消息标签,表示所有测试都已通过。
-
如果测试用例失败,消息将以红色显示,你可以进一步深入测试以查看失败详情。
到目前为止,我们已经创建了DBManager类,并有一套自动化测试来测试 CRUD 方法。现在是时候专注于填充逻辑,以便POIApp可以从数据库中保存和检索数据。
NUnitLite 是一个优秀的框架,用于实现 Android 应用开发的单元测试策略,但它不仅仅关于测试。你需要实现某种 UI 自动化测试框架来测试大多数应用组件,包括用户界面。
Xamarin 带来了另一个平台;Xamarin Test Cloud 使得可以在全球各地的真实设备上测试用任何语言编写的移动应用。您可以使用 Xamarin 测试框架编写测试脚本,并从 CI 系统中自动化应用测试。本书不涵盖 UI 自动化框架和 Xamarin Test Cloud 服务。您可以访问官方xamarin.com/网站获取更多信息。
实现缓存逻辑到 POIApp
DBManager类现在经过测试并可以直接在POIApp中使用。当下载完成时,POIApp将保存 POI 记录,当应用无法从服务器获取更新列表时,稍后将从列表中检索。执行以下步骤以将DBManager类集成到POIApp中:
-
在
POIListActivity类上调用CreateTable()方法以初始化数据库:DBManager.Instance.CreateTable(); -
打开
POIListFragment类并进入DownloadPoisListAsync()方法。我们已使用此方法从服务器下载 POI 列表。以下是需要在此处进行的更新:-
当从服务器成功下载 POI 时清除数据库缓存。
-
将新获取的 POI 数据保存到数据库中。
-
当设备未连接到网络时,从数据库返回缓存数据。
-
以下代码片段展示了更新后的DownloadPoisListAsync()方法:
public async void DownloadPoisListAsync(){
POIService service = new POIService ();
if (!service.isConnected (activity)) {
Toast toast = Toast.MakeText (activity, "Not conntected to internet. Please check your device network settings.", ToastLength.Short);
toast.Show ();
poiListData = DBManager.Instance.GetPOIListFromCache ();
} else {
progressBar.Visibility = ViewStates.Visible;
poiListData = await service.GetPOIListAsync ();
//Clear cached data
DBManager.Instance.ClearPOICache ();
//Save updated POI data
DBManager.Instance.InsertAll (poiListData); progressBar.Visibility = ViewStates.Gone;
}
poiListAdapter = new POIListViewAdapter (activity, poiListData);
this.ListAdapter = poiListAdapter;
ListView.Post(() => {
ListView.SetSelection(scrollPosition);
});
}
在前面的代码块中,我们保存了从服务器接收到的所有 POI 列表数据。然而,当用户编辑并保存任何 POI 数据时,我们需要更新该 POI 的本地数据库。
要做到这一点,让我们在CreateOrUpdatePOIAsync()方法中从POIDetailFragment类调用SavePOI()方法:
private async void CreateOrUpdatePOIAsync(PointOfInterest poi){
.....
.....
if (!string.IsNullOrEmpty (response)) {
Toast toast = Toast.MakeText (activity, String.Format ("{0} saved.", _poi.Name), ToastLength.Short);
toast.Show();
DBManager.Instance.SavePOI (poi);
if(!POIListActivity.isDualMode)
activity.Finish ();
} else {
Toast toast = Toast.MakeText (activity, "Something went Wrong!", ToastLength.Short);
toast.Show();
}
}
在POIDetailFragment类中从服务器删除 POI 记录时,在DeletePOIAsync()方法中我们需要调用DeletePOI()方法来从本地设备数据库中删除相同的 POI。
以下代码片段展示了更新后的DeletePOIAsync()方法:
public async void DeletePOIAsync(){
.....
....
string response = await service.DeletePOIAsync (_poi.Id);
if (!string.IsNullOrEmpty (response)) {
Toast toast = Toast.MakeText (activity, String.Format ("{0} deleted.", _poi.Name), ToastLength.Short);
toast.Show();
DBManager.Instance.DeletePOI (poi);
if(!POIListActivity.isDualMode)
activity.Finish ();
} else {
Toast toast = Toast.MakeText (activity, "Something went Wrong!", ToastLength.Short);
toast.Show();
}
}
现在我们已经实现了POIApp的离线数据库逻辑。运行应用;当连接到网络时,它将下载并缓存 POI 列表。现在在设备离线状态下重新启动应用,注意屏幕上显示的是相同的旧缓存 POI 列表。
摘要
在本章中,我们使用了 SQLite.ORM 将 POI 记录保存到数据库中,并创建了一系列单元测试来验证数据库操作是否正常工作。
在下一章中,我们将通过添加相机支持来捕获和保存 POI 图像,继续集成设备功能。
第九章:使 POIApp 具备位置感知功能
移动开发中最有趣的一个方面是与设备功能交互,例如运动传感器、摄像头和位置传感器。这些功能对于大多数应用来说是上下文相关的,并且对用户具有很高的价值。在本章中,我们将向您展示如何将位置感知添加到 POIApp 中。我们将涵盖以下主题:
-
设置应用程序权限
-
获取当前经纬度
-
获取经纬度的地址
-
计算两个地理点之间的距离
-
在地图应用中显示 POI
使用位置服务工作
在 Android 平台上集成位置服务比看起来要复杂。
您需要考虑不同的位置提供者、位置精度、用户移动,以及最重要的是设备的额定电池功耗。在 Android 平台上使用位置服务时,您将主要与LocationManager的一个实例一起工作。LocationManager类为您提供了获取设备地理位置周期性更新的能力,或者当设备进入特定地理位置的邻近区域时触发事件。
Android 设备通常提供两种不同的确定位置的方法:GPS和网络。在请求位置变化通知时,您可以指定希望接收更新的提供者。Android 平台定义了一组字符串常量,用于以下提供者:
提供者名称
描述
GPS_PROVIDER(GPS)
此提供者使用卫星确定位置。根据条件,此提供者可能需要一段时间才能返回位置修正。这需要ACCESS_FINE_LOCATION权限。
NETWORK_PROVIDER(网络)
此提供者根据蜂窝塔和 Wi-Fi 接入点的可用性确定位置。其结果通过网络查找获取。这需要ACCESS_COARSE_LOCATION权限。
PASSIVE_PROVIDER(被动)
此提供者可以在其他应用程序或服务请求位置更新时被动接收位置更新,而无需您自己实际请求位置。它需要ACCESS_FINE_LOCATION权限。如果 GPS 未启用,此提供者可能只能返回粗略的位置。
将位置服务集成到 Android 应用程序中的过程包括以下步骤:
-
获取
LocationManager实例的引用。 -
使用
LocationManager实例来请求位置变化通知,无论是持续通知还是单个通知。 -
处理
LocationListener回调方法。这些方法仅在通过RequestLocationUpdates(string, long, float, ILocationListener)方法请求位置时才会触发。
访问 Android 应用程序中的位置服务需要添加特定的权限,具体取决于您想使用的提供者。
设置应用权限
要在 Android 中访问位置服务,您必须向AndroidManifest.xml文件提供权限。Android 应用程序使用两个权限来访问位置 API:ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION。ACCESS_FINE_LOCATION包括对GPS_PROVIDER和NETWORK_PROVIDER提供者的权限。ACCESS_COARSE_LOCATION权限仅包括对NETWORK_PROVIDER的权限。
要将适当的权限添加到您的应用程序描述符中,请执行以下步骤:
-
双击属性/
AndroidManifest.xml在解决方案面板中。文件将在清单编辑器中打开。屏幕底部有两个标签页,应用程序和源,可以用来在查看用于编辑文件的表单或原始 XML 之间切换。 -
在所需权限列表中,勾选AccessCoarseLocation、AccessFineLocation和Internet。导航到文件 | 保存:
![]()
-
切换到源视图以查看以下内容:
![]()
配置模拟器
要使用模拟器进行开发,本章将需要配置模拟器以使用 Google APIs,以便地址查找和将应用映射到地图上。
要安装和配置Google APIs,请执行以下步骤:
-
从主菜单导航到工具并打开Android SDK Manager。
-
选择您正在使用的平台版本,勾选Google APIs,然后单击安装 1 个包...,如图所示:
![]()
-
安装完成后,关闭 Android SDK Manager,然后从主菜单导航到工具 | 打开Android 模拟器管理器。
-
选择您想要配置的模拟器并单击编辑。
-
在目标中,选择您想要工作的 API 级别的Google APIs条目。
-
点击确定以保存。
获取LocationManager实例
LocationManager类是一个系统服务,它提供对设备位置和方位的访问,如果设备支持这些服务。您不需要显式创建LocationManager实例;相反,您可以使用GetSystemService()方法从一个Context对象请求一个实例。在大多数情况下,Context对象是 activity 的子类型。以下代码展示了声明一个LocationManager类的引用并请求一个实例:
LocationManager locMgr;
. . .
locMgr = (LocationManager) GetSystemService (Context.LocationService);
请求位置变更通知
LocationManager类提供了一系列重载方法,可用于请求位置更新通知。如果您只需要单个更新,可以调用RequestSingleUpdate();要接收持续更新,请调用RequestLocationUpdate()。
在请求位置更新之前,您必须确定应使用的位置提供者。在我们的例子中,我们只想使用当时可用的最精确的提供者。这可以通过使用Android.Location.Criteria实例指定所需提供者的标准来实现。以下代码示例显示了如何指定最小标准:
Criteria criteria = new Criteria();
criteria.Accuracy = Accuracy.NoRequirement;
criteria.PowerRequirement = Power.NoRequirement;
现在我们有了标准,我们准备按照以下方式请求更新:
locMgr.RequestSingleUpdate (criteria, this, null);
实现ILocationListener
您会注意到RequestSingleUpdate()的第二个参数必须是一个实现ILocationListener的对象,该对象定义了以下方法:
void OnLocationChanged (Location location);
void OnProviderDisabled (string provider);
void OnProviderEnabled (string provider);
void OnStatusChanged (string provider, Availability status, Bundle extras);
在大多数情况下,我们将为所有方法创建空白存根,除了OnLocationChanged()。在编写更复杂的应用程序时,为其他一些方法提供实现将很有用。例如,您可能调用RequestLocationUpdate()以开始接收更新,然后通过OnProviderEnabled()接收通知,表明首选提供者现在可用,在这种情况下,您可能想要停止更新并再次使用首选提供者启动它们。
将位置服务添加到 POIApp
在POIApp中,我们有两个不同的场景用于请求位置更新:
-
在 POI 列表中,我们需要计算每个列表中 POI 的距离。在这种情况下,我们希望持续请求位置变化通知,并使用最新的位置来计算距离。
-
在
POIDetailFragment中,我们希望在添加新的 POI 时请求当前位置。在这种情况下,我们希望请求一个单一的位置变化通知。
将位置服务添加到 POI 列表
现在我们已经了解了如何向应用程序添加位置服务,让我们按照以下方式将位置服务添加到POIListFragment:
-
在
OnCreateView()方法中声明LocationManager的私有实例并获取引用,如下所示:LocationManager locMgr; ... public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.Inflate(Resource.Layout.POIListFragment, container, false); progressBar = view.FindViewById<ProgressBar> (Resource.Id.progressBar); SetHasOptionsMenu (true); locMgr = (LocationManager) Activity.GetSystemService (Context.LocationService); return view; } -
在
POIListFragment中包含Android.Locations命名空间并实现ILocationListener接口:public class POIListFragment: ListFragment, ILocationListener { ... } -
在代码编辑器中,右键单击
ILocationListener接口,选择重构 | 实现接口以实现存根方法。删除任何默认放置在存根方法中的代码;我们将提供OnLocationChange()的逻辑。 -
在
OnResume()中,获取最佳位置提供者并调用RequestLocationUpdates()以开始接收更新,如下所示:public override void OnResume () { base.OnResume (); DownloadPoisListAsync (); Criteria criteria = new Criteria (); criteria.Accuracy = Accuracy.NoRequirement; criteria.PowerRequirement = Power.NoRequirement; string provider = locMgr.GetBestProvider (criteria, true); locMgr.RequestLocationUpdates (provider, 2000, 100, this); } -
在
POIListFragment类中重写OnPause()方法并添加对RemoveUpdates()的调用。这样,当POIListFragment类不可见时,可以消除对位置变化的不必要处理,如下面的代码所示:protected override void OnPause () { base.OnPause (); locMgr.RemoveUpdates (this); } -
将
CurrentLocation属性添加到POIListViewAdapter中。POIListFragment类将使用此属性将位置变化通知适配器:public Location CurrentLocation { get; set; } -
在
OnLocationChanged()中添加逻辑,当接收到位置变化时在POIListViewAdapter上设置CurrentLocation,并调用NotifyDataSetChange()以使ListView刷新,如下所示:public void OnLocationChanged (Location location) { (this.ListAdapter as POIListViewAdapter).CurrentLocation = location;this.ListView.InvalidateViews (); } -
在
POIListViewAdapter的GetView()方法中添加逻辑,以计算CurrentLocation和 POI 的位置属性之间的距离,并将结果更新到distanceTextView。只有在CurrentLocation不是null,并且要添加到ListView的 POI 的Latitude和Longitude属性不是null时,才进行计算。如果这些值中的任何一个为null,则在距离字段中简单地放置??以指示此时无法计算,如下所示:var distanceTextView = view.FindViewById<TextView> (Resource.Id.distanceTextView); if ((CurrentLocation != null) && (poi.Latitude.HasValue) && (poi.Longitude.HasValue)) { Location poiLocation = new Location (""); poiLocation.Latitude = poi.Latitude.Value; poiLocation.Longitude = poi.Longitude.Value; float distance = CurrentLocation.DistanceTo (poiLocation) * 0.000621371F; distanceTextView.Text = String.Format("{0:0,0.00} miles", distance); } else { distanceTextView.Text = "??"; }
现在,运行 POIApp 并在 POIListView 中查看结果。
将位置服务添加到 POI 详细信息
将位置服务添加到 POIDetailFragment 的步骤将与上一节非常相似,但会稍微简单一些。
更新用户界面
在添加逻辑之前,我们需要在我们的应用程序中添加几个按钮;一个用于获取我们的位置,一个用于导航到地图,我们将在本章后面介绍。我们可以将这些按钮作为 POIDetailFragment.axml 底部的一行添加,如下面的截图所示:

使用 ImageButton 允许指定 drawable 类。要添加 ImageButton 小部件,请执行以下步骤:
-
在
POIDetailFragment.axml的底部添加一个LinearLayout实例,位于用于排列纬度和经度内容的TableLayout下方。方向应该是horizontal。 -
在
LinearLayout实例上,方向应该是horizontal,内容应该被包裹(高度和宽度),并且它应该在父元素中水平居中。可以使用布局重力在父元素内水平居中内容。10dp的顶部和底部填充将为按钮提供良好的间距。 -
在
LinearLayout实例中添加两个ImageButton小部件:locationImageButton和mapImageButton。这些按钮的图片可以在assets位置的drawable文件夹中找到。 -
以下 XML 代码显示了结果:
.. . </TableLayout> <LinearLayout p1:orientation="horizontal" p1:layout_width="wrap_content" p1:layout_height="wrap_content" p1:layout_gravity="center_horizontal" p1:minWidth="25px" p1:minHeight="25px" p1:layout_marginTop="10dp" p1:layout_marginBottom="10dp"> <ImageButton p1:src="img/ic_locate" p1:layout_width="wrap_content" p1:layout_height="wrap_content" p1:id="@+id/locationImageButton" /> <ImageButton p1:src="img/ic_map" p1:layout_width="wrap_content" p1:layout_height="wrap_content" p1:id="@+id/mapImageButton" /> </LinearLayout>
添加代码
现在我们已经在 UI 上有了按钮,我们可以添加以下代码来获取位置:
-
声明一个
LocationManager的私有实例,并在OnCreate()中以与上一节中POIListView相同的方式获取引用:locMgr = (LocationManager) Activity.GetSystemService (Context.LocationService); -
添加
GetLocationClicked事件处理程序并将其连接到ImageButton,如下所示:_locationImageButton = FindViewById<ImageButton> (Resource.Id.locationImageButton); _locationImageButton.Click += GetLocationClicked; -
在
GetLocationClicked()中添加对RequestSingleUpdate()的调用。RequestSingleUpdate()方法允许传入一个Criteria对象,这样我们就不需要单独调用GetBestProvider(),如下所示:protected void GetLocationClicked(object sender, EventArgs e) { Criteria criteria = new Criteria(); criteria.Accuracy = Accuracy.NoRequirement; criteria.PowerRequirement = Power.NoRequirement; locMgr.RequestSingleUpdate (criteria, this, null); } -
指定
POIDetailFragment实现Android.Locations。使用 Refactor | Implement 接口实现ILocationListener的存根方法。删除存根方法中放置的任何代码;我们将提供OnLocationChange()的逻辑。 -
在
OnLocationChange()中添加逻辑以更新位置字段,如下所示:public void OnLocationChanged (Location location) { _latEditText.Text = location.Latitude.ToString(); _longEditText.Text = location.Longitude.ToString (); }
模拟位置数据以进行测试
运行 POIApp 并尝试添加一个新的 POI 并获取位置。在模拟器中运行应用时,您会注意到当您点击位置按钮时似乎没有任何反应。实际上,应用正在等待从位置管理器来的 OnLocationChanged() 回调。要触发此回调,您必须使用 Android Device Monitor (ADM)来模拟位置数据。
要触发 OnLocationChanged(),执行以下步骤:
-
启动 ADM 并选择屏幕左侧的模拟器实例。
-
点击屏幕右侧的 Emulator Control 标签。如果 Emulator Control 标签不存在,导航到 Window | Show View 来显示该标签。注意,在面板底部有一个嵌套的标签,标题为 Location Controls,如下截图所示:
![]()
-
选择 Manual 标签,根据需要调整 经度 和 纬度,然后点击 Send。这将导致在
POIDetailFragment上触发OnLocationChanged()方法。
注意 Location Controls 下的其他两个标签:GPX 和 KML。这些标签可以用来从文件中加载一系列位置更新并回放到您的应用中,以测试更复杂的场景。
获取位置的地址
Android 平台提供的另一个有用功能称为 地理编码。这是从已知地址以纬度和经度表示位置的过程。Android 还支持反向地理编码,正如其名称所暗示的,从已知位置获取地址。
Android.Locations.Geocoder 类是用来执行地理编码和反向地理编码的类。使用它非常简单,如下步骤所示:
-
创建
Android.Locations.Geocoder的实例。 -
通过传递您想要查找地址的位置来调用
GetFromLocation()。 -
处理返回的
IList<Address>集合。从GetFromLocation()返回的地址集合在具体细节上有所不同,这意味着一些是具体的街道地址,一些指定了城市、国家等。第一个地址总是最具体的,因此我们将使用以下代码自动选择它:public void OnLocationChanged (Location location) { _latEditText.Text = location.Latitude.ToString(); _longEditText.Text = location.Longitude.ToString (); Geocoder geocdr = new Geocoder(activity); IList<Address> addresses = geocdr.GetFromLocation (location.Latitude, location.Longitude, 5); if (addresses.Any()) { UpdateAddressFields (addresses.First ()); } }
注意,在 GetFromLocation() 方法调用中的数字 5 参数表示已知可以描述围绕纬度和经度的区域的最多结果数量。
你可以看到我们选择调用一个方法来格式化地址信息。FeatureName属性可能包含一个标题,例如金门大桥或帝国大厦。很多时候,FeatureName将简单地包含街道号码。地址包含一系列地址行,我们将它们组合并放置在_addrEditText中,如下所示:
protected void UpdateAddressFields(Address addr)
{
if (String.IsNullOrEmpty(_nameEditText.Text))
_nameEditText.Text = addr.FeatureName;
if (String.IsNullOrEmpty(_addrEditText.Text))
{
for (int i = 0; i < addr.MaxAddressLineIndex; i++) {
if (!String.IsNullOrEmpty(_addrEditText.Text))
_addrEditText.Text += System.Environment.NewLine;
_addrEditText.Text += addr.GetAddressLine (i);
}
}
}
现在,运行POIApp并尝试添加一个新的 POI 和获取位置的地址。
保持用户知情
在使用获取位置按钮后,你会注意到请求位置信息需要一些时间来处理;通常,几秒钟或更长时间。最好让用户知道正在处理,这样他们就不会不断地点击按钮。
这可以通过两种方式实现。你可以直接将ProgressBar附加到详情片段布局中,就像我们在POIListFragment中所做的那样,或者我们可以使用ProgressDialog类。ProgressDialog类提供了一个简单的方法来显示一个带有旋转进度小部件和描述正在进行的进程的文本描述的对话框。自从DialogFragment类被添加到 Android 以来,谷歌推荐使用DialogFragment类而不是ProgressDialog。在这里,在这个例子中,我们将创建一个DialogFragment类,它显示的加载进度与DeleteDialogFragment类类似。
以下步骤将指导你添加一个对话框片段以显示加载进度:
-
让我们添加一个名为
ProgressDialogFragment的新片段,并从DialogFragment类扩展它。 -
重写
OnCreateDialog()方法并添加以下代码块:public class ProgressDialogFragment : DialogFragment { public override Dialog OnCreateDialog (Android.OS.Bundle savedInstanceState) { Cancelable = false; ProgressDialog _progressDialog = new ProgressDialog (Activity); _progressDialog.SetMessage ("Getting location..."); _progressDialog.Indeterminate = true; _progressDialog.SetProgressStyle (ProgressDialogStyle.Spinner); return _progressDialog; } } -
我们还没有准备好
ProgressDialogFragment片段。让我们将以下代码片段添加到GetLocationClicked()方法中,以便在用户从POIDetailFragment点击位置按钮时显示加载进度:FragmentTransaction ft = FragmentManager.BeginTransaction(); var dialogFragment = new ProgressDialogFragment (); dialogFragment.Show(ft, "progress_dialog"); -
现在我们需要从位置管理器检索到位置后,移除加载进度对话框。将以下代码片段添加到
OnLocationChanged()回调中,以移除进度对话框:FragmentTransaction ft = FragmentManager.BeginTransaction(); ProgressDialogFragment dialogFragment = (ProgressDialogFragment) FragmentManager.FindFragmentByTag("progress_dialog"); if (dialogFragment != null) { ft.Remove (dialogFragment).Commit(); }
现在,运行POIApp并检查新的进度对话框,如图所示:

添加地图集成
地图是移动计算中真正酷的一部分。它们提供了一种导航方式,在区域内查找兴趣点,以及支持许多其他有用的场景。
从应用程序与地图交互有两种基本方法如下:
-
导航到设备上已安装的现有 Android 地图应用程序以显示兴趣点。大多数最新的 Android 设备都预装了 Google 地图应用程序。但是,这并不保证。
-
集成 Google Maps API。这种方法直接使用 Google Play 服务将地图视图集成到你的应用程序中。你必须在 Google 开发者控制台中创建一个应用程序,并获取你应用的 API 密钥副本。
第一个选项更容易实现,而第二个选项允许更紧密的集成和控制地图,但代价是更多的代码和复杂性。第二个选项需要与 Google Play 库对应的非常具体的 Xamarin.Android 绑定库版本。我们选择第一个选项在POIApp示例中,以下是一些原因:
-
在模拟器中实现第二个选项非常困难,这意味着您可能需要在实际设备上测试和查看代码的结果,这可能不是所有读者都有的选择
-
我们需要比本章中可用的更多时间来设置第二个选项
Xamarin 的官方网站包含所有必要的详细信息,以实现第二个选项。
导航到地图应用程序
要导航到地图应用程序,我们将依赖本书前面使用过的Intent类;然而,我们不会指定我们想要启动的Activity类,而是将指定我们想要通过 URI 查看的信息类型。Android 包含一个可以显示不同类型信息的应用程序注册表,并将启动最合适的应用程序。
Android 平台定义了一系列可以用于在 Android 设备上启动 Google 应用程序的Intent类。以下表格总结了与位置相关的Intent类:
URI
动作
geo:latitude,longitude
此操作将在指定的纬度或经度上打开地图应用程序
geo:latitude,longitude?z=zoom
此操作将在指定的纬度或经度上打开地图应用程序,并放大到指定级别
geo:0,0?q=my+street+address
此操作将在地图应用程序中打开街道地址的位置
geo:0,0?q=business+near+city
此操作将在地图应用程序中打开并显示标注的搜索结果
在我们的案例中,我们有一个街道地址、纬度和经度,或者两者都有。如果存在街道地址,我们应该使用它构建Intent类,因为这会使街道地址在地图应用程序中显示,使其更用户友好。如果不存在街道地址,我们将使用纬度和经度构建Intent类。以下代码显示了构建Intent类的逻辑:
Android.Net.Uri geoUri;
if (String.IsNullOrEmpty (_addrEditText.Text)) {
geoUri = Android.Net.Uri.Parse (String.Format("geo:{0},{1}", _poi.Latitude, _poi.Longitude));
}
else {
geoUri = Android.Net.Uri.Parse (String.Format("geo:0,0?q={0}", _addrEditText.Text));
}
Intent mapIntent = new Intent (Intent.ActionView, geoUri);
在启动Intent类之前,我们需要确保有一个可以处理Intent类的应用程序;否则,我们可能会在StartActivity()中遇到未处理的异常。
检查已注册的地图应用程序
应用程序在其清单文件中通过 <intent-filter/> 元素提供有关它们提供的任何功能(Intent 类)的信息。由于我们依赖于外部地图应用来显示我们的位置,因此我们应该检查我们正在运行的设备上是否存在此类应用。我们可以通过调用 PackageManager 类的几个方法来完成此操作。PackageManager 类允许您检索有关设备上安装的应用程序包的各种类型的信息。QueryIntentActivities() 方法允许您检查是否有任何应用程序可以处理特定的 Intent 类。以下代码演示了 QueryIntentActivities() 的使用:
PackageManager packageManager = Activity.PackageManager;
IList<ResolveInfo> activities = packageManager.QueryIntentActivities(mapIntent, 0);
if (activities.Count == 0) {
Toast.MakeText (activity, "No map app available.", ToastLength.Short).Show ();
}
else
{
StartActivity (mapIntent);
}
创建一个 MapClicked() 事件处理程序并将其附加到 _mapImageButton。以下代码片段表示用于打开地图应用的完整 MapClicked() 代码:
protected void MapClicked(object sender, EventArgs e){
Android.Net.Uri geoUri;
if (String.IsNullOrEmpty (_addrEditText.Text)) {
geoUri = Android.Net.Uri.Parse (String.Format("geo:{0},{1}", _poi.Latitude, _poi.Longitude));
}
else {
geoUri = Android.Net.Uri.Parse (String.Format("geo:0,0?q={0}", _addrEditText.Text));
}
Intent mapIntent = new Intent (Intent.ActionView, geoUri);
PackageManager packageManager = Activity.PackageManager;
IList<ResolveInfo> activities = packageManager.QueryIntentActivities(mapIntent, 0);
if (activities.Count == 0) {
Toast.MakeText (activity, "No map app available.", ToastLength.Short).Show ();
}
else
{
StartActivity (mapIntent);
}
}
运行 POIApp 并从 POI 详细页面点击 地图 按钮。你会注意到地图应用会以 POI 位置打开。你可以选择从当前位置导航到那里。
摘要
在本章中,我们看到了如何使用设备位置服务来查找当前位置。我们还集成了 POIApp 与原生设备地图,以便在地图上定位 POI。在下一章中,我们将继续通过添加与摄像头的集成来扩展设备功能。
第十章:添加相机应用集成
移动计算的一个令人兴奋的特性是,大多数 Android 设备都有一些类型的相机,可以用来捕捉照片和/或视频。本章将指导您完成添加捕捉和上传 POI 图片功能所需的步骤,并将包括以下主题:
-
与设备相机集成的方法
-
相机权限和功能
-
捕获和显示照片
-
使用 HTTP 多部分表单上传上传图片
选择集成方法
Android 平台提供了两种不同的方式来集成设备相机功能到您的应用中:
-
使用现有的相机应用通过
Intent方法进行集成 -
创建自己的自定义活动,直接使用 Android API 与相机交互
第二种方法允许对相机视图如何呈现给用户以及用户如何与视图交互有很高的控制度。然而,第一种方法实现起来非常直接,因为它重用了现有的设备相机应用来捕捉图片。我们将采用Intent方法,因为它代表了一种非常实用的添加相机集成的方式。
权限和功能
在深入讨论集成设备相机功能的具体细节之前,我们将更详细地讨论与相机相关的通用权限和功能。以下表格包含了可能需要的各种权限。在我们的案例中,我们不需要指定这些权限,因为我们使用的是Intent方法,外部相机应用会为我们捕捉图片。外部相机应用需要指定所需的相机权限:
权限
描述
CAMERA
这是请求使用设备相机权限的应用所需的;如果您使用的是Intent方法,则不需要此权限
WRITE_EXTERNAL_STORAGE
此权限是必需的,以便将图像或视频保存到设备的外部存储(SD 卡)
RECORD_AUDIO
如果您的应用在视频捕获时记录音频
可以使用应用清单文件中的<uses-feature>元素来设置应用特定的功能。<uses-feature>声明用于通知关于应用所依赖的硬件和软件功能集合。您可以指定required=true来声明应用没有声明的功能将无法运行。功能声明仅用于信息目的。在安装应用之前,Android 系统不会进行验证。
以下表格展示了您可以在应用程序清单声明中定义的一组功能:
功能
描述
android.hardware.camera
应用使用设备的相机。如果设备支持多个相机,应用将使用面向屏幕背面的相机。
android.hardware.camera.autofocus
子功能。应用使用设备相机的自动对焦功能。
android.hardware.camera.flash
子功能。应用程序使用设备摄像头的闪光灯。
android.hardware.camera.front
子功能。应用程序使用设备的前置摄像头。
android.hardware.camera.any
应用程序至少使用一个可以朝任何方向的摄像头。如果不需要后置摄像头,请优先使用此功能android.hardware.camera。
在我们的案例中,我们不会指定任何功能作为要求,但在运行时,我们将检查是否有外部应用程序可用于捕获照片。
配置模拟器
如果您正在使用模拟器进行开发,您需要配置它以拥有摄像头。如果您使用的计算机有网络摄像头,模拟器可以使用它作为摄像头;否则,您可以选择使用模拟摄像头。
要配置模拟器以使用摄像头,请执行以下步骤:
-
从主菜单导航到工具并打开Android 模拟器管理器。
-
选择您一直在使用的模拟器并选择编辑。
-
在编辑 AVD 对话框的中间,您将看到两个下拉菜单;一个用于前置摄像头,一个用于后置摄像头。做出您的选择并点击确定:
![]()
扩展数据服务
由于我们已决定使用外部摄像头应用程序来捕获图片,它将在捕获图片后负责保存图片。我们将必须提供图像将保存的存储路径。为了保存 POI 图像,我们将使用类似于poiimage<poi id>.jpg的命名方案。
现在让我们扩展POIService类,添加以下附加方法。
实现 GetFileName()
让我们在POIService.cs中实现GetFileName()方法,该方法将负责提供在设备内存中保存图像的绝对路径。绝对路径包括位置和文件名。图像文件将被命名为poiimage<poi id>.jpg。以下列表显示了如何构造文件名:
public static string GetFileName (int poiId)
{
String storagePath = System.IO.Path.Combine (Android.OS.Environment.ExternalStorageDirectory.Path, "POIApp");
String path = System.IO.Path.Combine (storagePath, "poiimage" + poiId + ".jpg");
return path;
}
实现 GetImage()
如前几节所述,图像的保存可以通过相机意图完成。但是,我们需要编写两个辅助方法来从设备存储中读取和删除图像。
让我们添加一个GetImage()辅助方法,它将被用来从设备内存位置读取 POI 图像,该位置是之前保存的。将以下列表添加到POIService.cs类中:
public static Bitmap GetImage(int poiId)
{
string filename = GetFileName (poiId);
if (File.Exists (filename)) {
Java.IO.File imageFile = new Java.IO.File (filename);
return BitmapFactory.DecodeFile (imageFile.Path);
}
return null;
}
实现 DeleteImage()
用户通过点击删除操作栏按钮删除poi对象后,目前 POI 将从服务器删除。一旦从服务器端删除成功,删除设备内存中的相应 POI 图像是一个好主意。
以下辅助方法用于根据其 POI ID 删除图像:
public void DeleteImage (int poiId)
{
String filePath = GetFileName (poiId);
if (File.Exists (filePath)) {
File.Delete (filePath);
}
}
在本章中,我们不处理删除 POI;然而,为了实现前面描述的更改,您需要在DeletePOIAsync()中调用DeleteImage()方法:
....
if (response != null || response.IsSuccessStatusCode){
DeleteImage (poi.Id.Value);
string content = await response.Content.ReadAsStringAsync();
return content;
}
....
从 POIDetailFragment 捕获图像
我们现在准备承担拍照的任务。这涉及到以下任务:
-
添加新的用户界面小部件以启动拍照并显示照片
-
构建一个导航到外部相机应用以拍照的拍照 Intent
-
处理拍照 Intent 的结果并在成功捕获照片后显示照片
以下部分描述了每个步骤的详细信息。
添加 UI 元素
我们需要添加一些新的 UI 元素来支持图像捕获;我们需要一个 ImageButton 元素来启动捕获图像的过程,还需要一个 ImageView 元素来显示捕获的 POI 图像。ImageButton 小部件可以添加到位置和地图按钮旁边,而 ImageView 元素可以放置在 名称 字段上方的第一个小部件。
以下列表显示了 ImageView 的定义,它应该放置在 LinearLayout 中 POI 名称文本字段之前:
<LinearLayout
--- >
<ImageView
p1:src="img/ic_placeholder"
p1:layout_width="wrap_content"
p1:layout_height="200dp"
p1:id="@+id/poiImageView"
p1:layout_gravity="center_horizontal"
p1:scaleType="centerCrop"
p1:layout_marginBottom="10dp" />
<TextView
p1:text="Name"
p1:layout_width="fill_parent"
p1:layout_height="wrap_content"
p1:id="@+id/textView10" />
---
</LinearLayout>
在 POIDetailFragment 中创建一个私有引用对象,并在 OnCreateView() 中分配引用:
ImageView _poiImageView;
...
_poiImageView = view.FindViewById<ImageView> (Resource.Id.poiImageView);
现在,我们需要一个启动相机的按钮。我们将首先将 ic_new_picture.png 图标从 assets 文件夹复制到项目的 drawable 文件夹,并以与之前章节相同的方式将其添加到项目中。
将以下按钮定义添加到包含其他两个按钮的 LinearLayout 中:
<ImageButton
android:src="img/ic_new_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/photoImageButton" />
前述布局更改的输出应类似于以下截图:

在 POIDetailFragment 中创建一个私有引用对象,并按如下方式在 OnCreateView() 中分配引用:
ImageButton _photoImageButton;
...
_photoImageButton = view.FindViewById<ImageButton> (
Resource.Id.photoImageButton);
_photoImageButton.Click += NewPhotoClicked;
注意,在前述代码片段中,我们将 NewPhotoClicked() 事件监听器分配给 photoImageButton 按钮。我们将在本章接下来的部分完成 NewPhotoClicked() 方法的实现。
创建相机 Intent
要启动外部相机应用以拍照,我们再次依赖 Intent 类,这次结合了一个动作。以下列表展示了创建具有图像捕获动作的 Intent 类:
Intent cameraIntent = new Intent(MediaStore.ActionImageCapture);
MediaStore 类包含内部和外部存储设备上所有可用媒体的元数据。MediaStore.ActionImageCapture 动作告诉 Android 平台,您想要捕获照片,并且愿意使用任何提供这些功能的现有应用。
检查已注册的相机应用
在第九章“使 POIApp 具有位置感知能力”中,我们使用 PackageManager 检查是否安装了可以处理我们 intent 的地图应用。我们现在需要执行相同的检查以确定可以处理我们的 ActionImageCapture intent 的应用。以下列表显示了我们需要执行的逻辑:
PackageManager packageManager = PackageManager;
IList<ResolveInfo> activities = packageManager.QueryIntentActivities(cameraIntent, 0);
if (activities.Count == 0) {
//display alert indicating there are no camera apps
}
else {
//launch the camera Intent
}
通过 Intent 提供附加信息
在我们启动意图之前,我们需要向处理我们请求的相机应用提供一些信息;具体来说,是一个文件名和位置,以及结果照片的最大尺寸。你必须小心提供最大尺寸的值;这可能会引起内存溢出异常的潜在威胁。我们通过向意图添加Extras来完成此操作。MediaStore类定义了多个标准Extras,可以将它们添加到意图中,以控制外部应用如何满足意图。
提供文件名和位置
MediaStore.ExtraOutput额外可以用来控制外部相机应用在捕获图像时应使用的文件名和位置。我们之前在POIService类中添加了GetFileName()方法来提供文件路径信息。然而,相机应用期望文件路径是一个Android.Net.Uri实例;因此,我们需要将字符串路径转换为Android.Net.Uri实例。
这是一个两步过程。首先,我们使用来自数据服务的字符串路径创建一个Java.IO.File对象,然后创建一个Android.Net.Uri对象。以下列表展示了如何完成 URI 的构建和设置MediaStore.ExtraOutput额外:
Java.IO.File imageFile = new Java.IO.File(POIData.Service.GetFilename(_poi.Id.Value));
Android.Net.Uri imageUri = Android.Net.Uri.FromFile (imageFile);
cameraIntent.PutExtra (MediaStore.ExtraOutput, imageUri);
提供大小限制
MediaStore.ExtraSizeLimit额外限制图像大小。如所示,设置起来更为直接:
cameraIntent.PutExtra (MediaStore.ExtraSizeLimit, 1.5 * 1024);
启动意图
现在我们已经准备好通过调用StartActivity()方法启动相机应用。与地图应用不同,在相机意图的情况下,我们期望从活动中返回结果。我们期望相机应用提供一张照片或用户取消拍照的通知。这可以通过传递意图调用StartActivityForResult()来实现。StartActivityForResults()方法与OnActivityResult()活动回调协同工作,以通信意图的结果。
以下列表展示了调用StartActivityForResult()的方法:
const int CAPTURE_PHOTO = 0;
.. .
StartActivityForResult(cameraIntent, CAPTURE_PHOTO);
注意StartActivityForResult()的第二个参数。它是一个名为requestCode的整数值,它将在回调中的OnActivityResult()作为参数返回,并帮助您识别启动意图的原始原因。最佳实践是为每个可能引起OnActivityResult()被调用的requestCode定义一个常量值来传递。
注意,当我们从片段中调用StartActivityForResult()方法时,结果将始终返回到托管POIDetailsFragment的活动OnActivityResult()方法。然而,在这种情况下,我们正在POIDetailsFragment本身中寻找结果。为此,我们需要覆盖所有托管活动的OnActivityResult()方法,并调用base.OnActivityResult来触发回调到片段上声明的OnActivityResult()方法。
完成 NewPhotoClicked()方法
我们以某种碎片化的方式覆盖了许多与启动相机应用相关的主题。以下列表是NewPhotoClicked()的完整实现:
int CAPTURE_PHOTO = 100;
void NewPhotoClicked (object sender, EventArgs e)
{
if (_poi.Id <= 0){
Toast.MakeText (activity, "You must save the POI before attaching a photo.", ToastLength.Short).Show ();
return;
}
Intent cameraIntent = new Intent (MediaStore.ActionImageCapture);
PackageManager packageManager = Activity.PackageManager;
IList<ResolveInfo> activities = packageManager.QueryIntentActivities (cameraIntent, 0);
if (activities.Count == 0) {
Toast.MakeText (activity, "No camera app available.", ToastLength.Short).Show ();
} else {
string path = POIService.GetFileName (_poi.Id);
Java.IO.File imageFile = new Java.IO.File (path);
Android.Net.Uri imageUri = Android.Net.Uri.FromFile (imageFile);
cameraIntent.PutExtra (MediaStore.ExtraOutput, imageUri);
cameraIntent.PutExtra (MediaStore.ExtraSizeLimit, 1 * 1024 * 1024);
StartActivityForResult (cameraIntent, CAPTURE_PHOTO);
}
}
处理 Intent 的结果
启动活动通过OnActivityResult()回调方法通知 intent 的结果。让我们向POIListActivity和POIDetailActivity添加以下回调以触发回调到POIDetailFragment:
protected override void OnActivityResult (int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult (requestCode, resultCode, data);
}
现在,让我们在POIDetailFragment类中重写OnActivityResult方法。以下列表显示了OnActivityResult()方法的签名:
public override void OnActivityResult (int requestCode, Result resultCode, Intent data)
我们在上一节讨论了requestCode。resultCode参数表示启动的 intent 的结果,其类型为Result,可以有以下值:
值
意义
RESULT_OK
活动成功完成了请求
REQUEST_CANCELED
活动通常由用户操作取消
REQUEST_FIRST_USER
可以用于自定义意义的第一个值
第三个参数data是Intent类型,可以用来从启动的活动传递额外的信息。在我们的案例中,我们只关心requestCode和resultCode。以下列表显示了POIDetailFragment中OnActivityResult()的实现:
public override void OnActivityResult (int requestCode, Result resultCode, Intent data)
{
if (requestCode == CAPTURE_PHOTO) {
if (resultCode == Result.Ok) {
Bitmap bitmap = POIService.GetImage (_poi.Id.Value);
_poiImageView.SetImageBitmap (bitmap);
if (bitmap != null) {
bitmap.Dispose ();
bitmap = null;
}
} else {
Toast.MakeText (Activity, "No picture captured.", ToastLength.Short).show();
}
} else {
base.OnActivityResult (requestCode, resultCode, data);
}
}
注意,当resultCode是RESULT_OK时,我们将捕获的图片加载到 Bitmap 对象中,然后设置给_poiImageView。这导致图片在POIDetailFragment布局的顶部显示。如果resultCode不是RESULT_OK,我们向用户显示一个 toast 消息,指示操作已取消。
我们正在使用GetImage()在POIService中从内存中检索图片。这是一个简单的实用方法,它接受一个 POI ID 并使用 Android 实用类BitmapFactory加载Android.Graphics.Bitmap。
到目前为止,我们已经添加了很多代码。运行POIApp,从POIDetails页面点击添加图片按钮以调用相机。注意,捕获的图片将在POIDetails屏幕上显示。
使用 HTTP 多部分上传上传图片
我们在POIApp示例应用程序中要实现的最后一项功能是将捕获的 POI 图片上传到服务器。目前,我们只将 POI 详情保存在云上,POI 图片存储在用户的设备内存中。将图片上传到服务器会很好,这样我们就不必担心当用户从设备上本地删除它们时丢失图片。
如果用户已经捕获了 POI 的图片并且它可在设备内存中本地使用,那么保存操作将使用多部分表单上传将图片和数据一起发布。否则,它将只发布用于保存操作的 POI JSON 数据。
在我们创建多部分表单数据请求之前,让我们首先了解 Web 服务 API 规范。以下块描述了多部分表单上传的 Web 服务 API 要求:
Endpoint: com.packt.poiapp/api/poi/upload
Request-Type: multipart/form-data POST
-----BOUNDARY
Content-Disposition: form-data; name="file"; filename="poiimage1.jpg"
Content-Type: application/octet-stream
/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL===
-----BOUNDARY
-----BOUNDARY
Content-Disposition: form-data; name="poi";
Content type = application/json, Encoding:UTF8
Body= {"id":"1","latitude":"51.5033","longitude":"0.1197","name":"London Eye","description":"...","image":""}
-----BOUNDARY
注意,前面的 API 规范使用了multipart/form-data编码类型。在这本书的早期部分,我们使用 HTTP POST方法上传 POI JSON 对象,但现在我们需要发送 POI 数据和位图流,这些数据不能使用POST方法上传。我们需要使用multipart/form-data编码类型,通过它可以附加图像以及其他消息内容。
一个名为boundary的标准分隔符用于分隔多部分消息的每一部分。消息的每一部分都可以定义自己的标准头,例如Content-Type和Content-Disposition,提供包含其值的文件名。多部分分隔符和头字段始终是 7 位 ASCII,不区分大小写。图像内容位于Content-Type头下方,接着是 POI JSON 字符串的值。
想要获取关于多部分Content-Type的更详细信息,请搜索RFC 1341(MIME)并访问www.w3.org/Protocols/rfc1341/7_2_Multipart.html。
现在我们已经了解了上传图像和 POI 数据的 API 规范,让我们继续实施:
-
在
POIService类中为CreateOrUpdatePOIAsync()方法创建一个重载版本,该方法接受两个参数:PointOfInterest对象和 POI 图像bitmap:public async Task<String> CreateOrUpdatePOIAsync(PointOfInterest poi, Bitmap bitmap) { ... } -
添加以下代码片段以将 POI 对象转换为 JSON 对象,然后转换为
StringContent。由于我们已经在发送 POI 数据时做过类似的事情,因此不需要解释:var settings = new JsonSerializerSettings (); settings.ContractResolver = new POIContractResolver (); var poiJson = JsonConvert.SerializeObject (poi, Formatting.None, settings); var stringContent = new StringContent(poiJson); -
现在将位图图像转换为字节数组,以便通过多部分表单上传发送
ByteArrayContent:byte[] bitmapData; var stream = new MemoryStream(); bitmap.Compress(Bitmap.CompressFormat.Jpeg, 0, stream); bitmapData = stream.ToArray(); var fileContent = new ByteArrayContent(bitmapData);注意,
bitmap.Compress()方法将位图的压缩版本写入指定的流。第二个整数参数表示压缩质量,范围从0到100,其中0表示低质量,而100是最大值(最低质量)。 -
添加以下附加内容头,如媒体内容类型和内容处置。POI 图像名称以
poiimage<poid>.jpg格式发送。将图像保存到数据库的服务器将使用此名称:fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse ("application/octet-stream"); fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "file", FileName = "poiimage" + poi.Id.ToString () + ".jpg" }; -
现在我们已经准备好了 POI 数据和图像内容,可以将这两个块添加到
MultipartFormDataContent中。boundary是一个随机字符串,用作分隔符,以分隔消息体的每一部分:string boundary = "---8d0f01e6b3b5daf"; MultipartFormDataContent multipartContent= new MultipartFormDataContent (boundary); multipartContent.Add (fileContent); multipartContent.Add(stringContent, "poi"); -
声明一个表示上传 POI 图像的 Web 服务端点的字符串常量,使用多部分:
private const string UPLOAD_POI = "http://YOUR_IP:8080/com.packt.poiapp/api/poi/upload"; -
现在我们继续使用
HttpClient类中的PostAsync()方法上传表单内容到服务器。以下代码片段演示了如何使用HttpClient类向服务器发送数据:HttpClient httpClient = new HttpClient (); HttpResponseMessage response = await httpClient.PostAsync (UPLOAD_POI, multipartContent); if (response.IsSuccessStatusCode) { string content = await response.Content.ReadAsStringAsync(); return content; } return null;
上传请求的结果会发送回POIDetailFragment。
现在我们有了上传图像以及 POI JSON 数据的方法,让我们改变POIDetailFragment类中的SavePOI()方法中的逻辑。目前,保存操作验证用户输入并通过传递 POI 对象调用CreateOrUpdatePOIAsync()。
现在如果用户使用摄像头捕获了 POI 图像,让我们改为调用新创建的覆盖版本的CreateOrUpdatePOIAsync(),并传递 Bitmap、POI 和活动实例。将以下逻辑添加到SavePOI()方法中:
POIService service = new POIService ();
Bitmap bitmap = null;
if (_poi.Id>0) {
bitmap = POIService.GetImage (_poi.Id);
}
string response;
if (bitmap != null) {
response = service.CreateOrUpdatePOIAsync (_poi, bitmap);
} else {
response = service.CreateOrUpdatePOIAsync (_poi);
}
为了使我们的应用程序内存效率更高,在你完成使用 Bitmap 实例后销毁它是好主意。将以下代码添加到销毁 Bitmap 内存中:
if (bitmap != null) {
bitmap.Dispose ();
bitmap = null;
}
我们现在已经完成了POIApp应用程序,该应用程序练习了您在开发专业应用程序时需要利用的许多 Xamarin.Android 功能。虽然该应用程序在本质上相对简单,但我们希望它能提供一个良好的 Android 应用程序开发的入门指南。祝您 Xamarin.Android 开发顺利!编码愉快!
摘要
在本章中,我们通过添加与摄像头的集成完成了POIApp。现在我们有一个演示了 Android 平台许多功能的 App。
在上一章中,我们将讨论各种应用分发渠道以及准备应用程序部署的步骤。
第十一章。将应用发布到应用商店
如果每个人都能享受它,那么一个应用就是有用的,这意味着要找到一种方法使其对大众可用。在本章中,我们将探讨使您的应用准备好部署的各个方面,并讨论将 Android 应用发布到市场的各种选项。在本章中,我们将涵盖以下主题:
-
准备应用发布
-
发布用于上传的已签名 APK
-
不同的应用分发选项
-
在 Google Play 上发布
准备应用发布
当您的应用完全开发完成后,您投入大量精力验证每个关键功能模块的时刻至关重要。许多开发者更喜欢使用不同的测试自动化框架(如 Robotium、Appium、Xamarin Test Cloud 等)来测试应用,或者一些开发者使用手动测试来验证目标设备上的应用。一旦您确信应用不包含任何明显的错误并且运行得像预期的那样顺畅,您就可以准备将应用部署到应用商店。
以下部分讨论了在生成发布版 APK 之前需要考虑的各个方面。
禁用调试模式
在应用开发过程中,Xamarin Studio 支持使用 Java 调试 Wire Protocol(JDWP)进行调试。这对于开发目的来说很棒,但对于已部署的应用来说存在安全风险,因此需要在发布的应用中禁用。有两种不同的方法可以实现这一点:
-
在
AndroidManifest.xml文件中设置应用的android:debuggable属性。 -
使用
AssemblyInfo.cs条件指令。
要使用 Android 的 AndroidManifest.xml 描述符来移除调试模式,您需要将 android:debuggable 属性的值设置为 false。以下列表显示了如何从清单文件中关闭 JDWP 调试:
<application ...
android:debuggable="false" ...
</application>
禁用调试模式的另一种方法是使用 AssemblyInfo.cs 文件中的条件指令。以下列表显示了如何使用条件指令根据所选配置关闭或打开 JDWP 调试。这种方法的优势在于它基于当前所选配置:
#if RELEASE
[assembly: Application(Debuggable=false)]
#else
[assembly: Application(Debuggable=true)]
#endif
配置链接选项
默认情况下,发布模式会关闭共享运行时并开启链接,以便您的分发 APK 只包含应用所需的 Xamarin.Android 运行时部分。链接器通过执行对编译代码的静态分析来确定哪些程序集、类型和类型成员被应用使用。所有未使用的程序集、类型和成员都被移除,以减少应用的整体大小。
链接选项可以在 Android 构建部分的项目选项对话框中查看和设置:

当查看和调整链接器选项时,请确保首先从配置下拉框中选择发布。Xamarin.Android 提供以下链接行为:
-
不要链接:这将关闭链接器;不会执行任何链接操作。
-
仅链接 SDK 组件:这将仅链接 Xamarin.Android 所需的组件;其他组件将不会链接,它们将以单独的组件形式分发。
-
链接所有组件:这将链接应用程序所需的全部组件,而不仅仅是 Xamarin.Android 所需的组件。
链接的副作用
在某些情况下,链接可能会产生一些意外的副作用,包括需要类型和成员被意外丢弃。对于在发布模式下编译和链接的应用程序,进行彻底的测试周期非常重要,以确保应用程序不会受到这种副作用的影响。实际上,在大多数情况下,测试应超出初始开发者的测试,并使用 APK 文件在发布模式下进行生成。
如果您遇到与缺失类型相关的运行时异常或构建失败问题,或者难以定位特定方法,您可能需要提供一个自定义链接器文件,该文件向链接器提供有关特定类型或成员的明确指令。
以下列表是一个自定义链接文件的示例,该文件指示链接器始终包含特定类型及其特定成员集:
<?xml version="1.0" encoding="UTF-8" ?>
<linker>
<assembly fullname="Mono.Android">
<type fullname="Android.Widget.AdapterView" >
<method name="GetGetAdapterHandler" />
<method name="GetSetAdapter_Landroid_widget_Adapter_Handler" />
</type>
</assembly>
</linker>
可以将自定义链接文件添加到项目中作为简单的 XML 文件。将文件添加到项目后,选择该文件,打开属性面板,并从构建操作菜单中选择链接描述,如图下所示:

选择支持的 ABIs
Android 支持多种不同的 CPU 架构。Android 平台定义了一组应用程序二进制接口(ABIs),对应不同的 CPU 架构。默认情况下,Xamarin.Android 假定在大多数情况下armeabi-v7a是合适的。如果您需要支持额外的架构,那么您必须检查每个适用的架构。这将导致构建过程生成将在所有目标 ABIs 上运行的代码,以及包括适用于每个架构的本地库。
支持的 ABIs 可以在项目选项对话框的Android 构建部分指定:

我遇到需要指定附加 ABIs 的场景之一是在测试过程中。我多次与一组测试人员合作;其中一些拥有物理设备,而另一些则使用模拟器。为了支持 x86 模拟器的使用,您需要在支持的 ABIs 列表中包含 x86。
验证包名、图标和应用程序版本
在准备发布构建以上传到 Android 市场之前,您需要验证几个额外的清单属性。本节将带您了解这些关键项目。
包名
包名是您应用程序在 Google Play 中的唯一标识符。应用程序包名在AndroidManifest.xml文件中定义。一旦您的应用程序使用此名称部署到市场,以后就不能更改它。更改应用程序包名意味着它将被视为一个全新的应用程序。
Android 应用程序包名应该是唯一的,并且名称可以包含大写或小写字母('A'到'Z')、数字和下划线('_')。通常,使用您的互联网域名所有权作为包名的基础是一种最佳实践。例如,由 Yahoo 发布的应用程序将以com.yahoo.<app id>开头。
大多数应用程序设置可以通过直接编辑AndroidManifest.xml文件或在项目选项窗口中进行控制。要修改AndroidManifest.xml中的包名,您需要使用package属性,如下面的代码片段所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.packt.poiapp">
...
</manifest>
在 Xamarin Solution Explorer 中右键单击您的项目,然后选择选项以打开项目选项窗口。以下截图展示了 Xamarin Studio 中的项目选项窗口:

应用程序名称和图标
制作一个在设备上工作的应用程序并不足够。您还需要提供一个代表您品牌或产品的优质应用程序图标。毕竟,应用程序图标是用户在设备上安装应用程序后首先注意到的东西。您必须为 mdpi、hdpi、xhdpi 和 xxhdpi 密度的设备准备应用程序启动器图标。
一旦启动器图标准备就绪,请将它们添加到相应屏幕密度的 drawable 资源文件夹中。您可以通过将android:label属性设置为AndroidManifest.xml文件中的<application>元素来设置应用程序名称,并为设置应用程序图标,可以使用android:icon属性:
<manifest ……. >
<application
android:label="POIApp"
android:icon="@drawable/ic_launcher">
</application>
</manifest>
您可以参考官方 Google 应用程序图标设计指南:www.google.com/design/spec/style/icons.html#icons-product-icons。
应用程序版本控制方案
应用程序版本控制方案用于跟踪不同的构建版本。这也帮助在应用程序有可下载的升级版本时通知用户。Android 使用以下两个不同的清单属性来定义构建版本信息:
-
android:versionCode:这是一个表示应用程序当前构建版本的整数值。内部,Google Play 使用此属性来处理构建更新过程,例如在应用程序的新版本可用时通知用户。您必须为每个后续版本增加versionCode的值。 -
android:versionName:这是一个表示应用程序发布版本的字符串。此版本代码将在 Google Play 上向用户公开显示。请注意,此字符串在 Google Play 上不是强制性的,也不在内部使用,但它只是向用户显示。
这两个属性可以从项目选项窗口或应用程序清单文件中设置:
<manifest
android:versionCode="1"
android:versionName="1.0"
package="com.packt.POIApp">
....
....
</manifest>
审查用户权限
安卓应用程序必须声明它需要访问应用程序中某些功能的权限列表。安卓应用程序权限在AndroidManifest.xml文件中使用<uses-permission>标签声明。当应用程序正在用户设备上安装时,安卓系统读取清单文件并向用户显示列表。用户必须决定允许权限以完成安装。以下截图显示了下载 Facebook 移动应用程序时的应用程序权限对话框:

在准备应用程序的发布版本构建时,请确保您的应用程序清单文件仅定义了应用程序所需的权限。例如,一个请求读取通话记录权限的相机应用程序可能会引起用户的不满,并且有很高的可能性用户不会下载您的应用程序。
发布用于上传的已签名 APK
完成所有之前的步骤后,我们将进入最后一步,准备一个用于在不同应用商店发布的已签名 APK。以下部分将讨论在 Xamarin Studio 内部生成已签名 APK 的步骤。
Android 密钥库
密钥库是由 Java SDK 中的keytool程序创建和管理的安全证书数据库。密钥库是创建安卓应用程序发布版本的重要方面。安卓设备无法运行未经数字签名的应用程序。这可能有些令人惊讶,因为我们已经运行了我们的应用程序一段时间了。在开发过程中,Xamarin.Android 使用调试密钥库在从 IDE 运行应用程序时对构建进行签名,并且因此运行在设备上的应用程序。这个密钥库仅用于调试目的,但不会被视为发布应用程序的分发有效密钥库。
密钥库必须得到妥善保管,因为发布应用程序所有未来版本都需要相同的密钥库密钥。如果密钥库丢失,将无法在 Google Play 上发布应用程序的更新。唯一的解决方案是创建一个新的密钥库,并将新版本作为全新的应用程序发布。
可以使用 Java JDK 中提供的keytool和jarsigner命令行工具创建密钥库。以下命令用于使用 Java keytool 工具创建密钥库:

注意,前面的命令提示用户提供密码和其他必需的详细信息以生成release.keystore密钥。
虽然可以直接使用命令行 keytool 工具创建和管理 keystore,但 Xamarin.Android 提供了一个用户界面来访问此工具,该界面集成到发布过程中。以下部分将指导您创建 keystore 并为发布准备签名 Android 构建。
从 Xamarin.Android 发布
以下步骤将指导您在创建签名.apk文件的过程中创建新的 keystore:
-
在运行配置下拉框中,选择发布选项:
![运行配置]()
-
在 Xamarin Studio 工具栏中导航到构建|发布准备选项。您将看到最新的
POIApp构建列表,如下截图所示。如果您从未构建过代码,它将为您构建并生成一个:![构建列表]()
-
从列表中选择最新构建,然后单击底部的右下角的签名和分发…按钮。您将看到两个选项:临时或Google Play。如果您选择临时选项,它将创建一个构建并将其保存在您的磁盘上。您需要手动将应用发布到 Google Play。然而,Google Play选项允许您直接从 Xamarin Studio 将应用发布到 Google Play。
然而,始终创建一个签名构建并将其保存在您的计算机上是一个好主意,这样您可以在将其发布到 Google Play 之前验证所有功能:
![创建新密钥]()
![导入现有密钥]()
-
让我们选择临时并单击下一步。注意,它将打开Android 签名身份向导,带有两个按钮:创建新密钥和导入现有密钥。如果您之前使用 Java keytool 命令行工具创建了 keystore,您需要直接选择导入现有密钥。
在此示例中,我已选择创建新密钥按钮来演示 Xamarin Studio 如何简化创建新 keystore 的过程。
-
选择创建新密钥按钮。
-
填写所有必需的详细信息,例如别名、密码、全名、有效期等,然后选择确定以确认:
![填写详细信息]()
-
它将创建一个新密钥并显示Android 签名身份对话框。选择密钥并单击下一步。
-
注意,它将显示一个确认对话框,如下截图所示:
![确认对话框]()
-
选择发布。它将提示您选择保存 apk 文件的存储位置。按照提示,在生成签名 APK 时,您需要提供 keystore 密码:
![生成签名 APK]()
生成的 APK 已准备好进行最终测试和潜在的分发。
重新发布应用
在部署应用程序的第一个版本时创建的密钥库非常重要,应该保持安全。密钥库、别名和密码应该保持安全,以确保只有有权发布应用程序新版本的授权人员才能访问它们。
不同的应用分发选项
Android 开发者有几种分发应用程序的选项,包括以下内容:
-
通过私有云或网站链接自发布
-
通过电子邮件附件分发
-
在 Google Play 上发布
-
在第三方应用商店发布
通过网站链接或电子邮件自发布
网站链接和电子邮件附件相当直接,易于完成,可能适合一些公司内部使用或由一小群合作伙伴使用的应用程序。
在从网站链接或电子邮件附件安装应用程序之前,你必须首先更新你的设备安全设置,以允许从未知来源安装应用程序。让我们看看以下截图:

启用此选项后,当你选择电子邮件附件或包含 APK 的网页链接时,将提示你安装应用程序。
注意,对于普通消费者来说,网站链接和电子邮件作为分发手段并不理想。像 Google Play 和 Amazon Appstore 这样的市场与自发布方法相比提供了显著的优势。以下是通过应用商店分发应用程序的一些优势:
-
大多数应用商店都提供审查流程,以确保应用程序不是恶意的。谷歌最近推出了一种应用商店审查流程,该流程验证了违反 Android 开发者政策、构建大小、权限等问题。审查流程加强了 Android 社区,并有助于建立消费者的信任。
-
通过在 App Store 上分发应用程序提供强大的基础设施,以接触数百万消费者。
-
推广应用程序并允许消费者进行内容评级。
-
处理使用订阅或购买的应用程序的财务结算。
在 Google Play 上发布
Google Play 是一个理想的市场,可以上传和分发你的应用程序给更广泛的受众。这是一个单一的平台,允许应用发布者分发、广告、通过销售你的应用程序赚钱,并分析应用程序的使用和统计数据。另一方面,每台 Android 设备都预装了 Google Play Android 应用程序,通过这个应用程序,你的发布的应用程序将被发现并下载给用户。
在你的应用程序推向市场之前,从用户那里获得一些真实的反馈总是很有价值的。开发者倾向于将应用程序的 alpha/beta 版本分发给一组用户并收集反馈。Beta 测试可以帮助你从真实用户那里获得早期反馈,并在进入生产阶段之前解决这些问题。
Google Play 允许您设置和分发您应用的 alpha 和 beta 阶段发布版本。在准备并上传到 beta 阶段的应用程序发布版本后,您可以邀请一组您希望分发应用的测试者。收到邀请的测试者可以下载应用,提供反馈并评分您的应用。本书不包括使用 Google Play 测试计划的详细步骤。如需更多详情,您可以访问 developer.android.com/distribute/tools/launch-checklist.html。
要将您的应用程序发布到 Google Play,您需要遵循以下描述的一些具体步骤:
-
要在 Google Play 上分发应用,您需要一个开发者账户。要注册为开发者,需要一次性注册费 25 美元。您可以根据屏幕上的说明注册为开发者,注册链接为
play.google.com/apps/publish/signup/。 -
准备在上传应用程序时所需的宣传资产,如图形、视频和宣传信息。以下是一份列表:
-
准备宣传信息,包括应用程序标题、简短描述和完整描述
-
高分辨率的应用程序图标,尺寸为 512 w x 512 h
-
特征图形,尺寸为 1024 w x 500 h
-
宣传图形,尺寸为 180 w x 120 h
-
展示您应用的可选宣传 YouTube 视频
-
-
一旦准备好 APK 的发布版本和宣传内容,应用程序就可以上传到 Google Play。这是通过登录 Google Play 发布者仪表板完成的:
![img/S2d52OQ0.jpg]()
-
点击添加新应用按钮以帮助您上传 APK 的过程:
![img/iRs16e1G.jpg]()
-
为您的应用在 Google Play 上显示的标题提供标题,并点击上传 APK。
-
一旦上传了 APK,您需要通过上传所需的截图和其他宣传材料来完成商店列表的详细信息。
-
在上传过程中,所有应用都必须根据 Google Play 评分系统进行评分。要了解更多关于新的内容评分系统,您可以参考
support.google.com/googleplay/android-developer/answer/188189。 -
仪表板上的定价和分发选项卡允许您选择应用可用的地理位置。
-
一旦提供所有必要的详细信息,您现在可以点击发布应用按钮,使您的应用可供下载。
第三方应用商店
Google Play 并非是您将应用程序分发到全球的唯一选择。您应该考虑使用其他分发渠道,例如 Mall.Soc.io (soc.io/apps)、GetJar (www.getjar.com/)、Amazon Appstore 等等。Google 没有对在其他市场重新分发相同的应用程序施加任何限制。考虑所有可用的选项,以充分利用您的努力。
摘要
在本章中,我们简要讨论了准备和签名 APK 发布版本的过程。我们还讨论了开发者可用的不同应用程序分发渠道。
我们现在已经完成了POIApp应用程序,该应用程序练习了您在开发专业 Android 应用程序时将需要的许多 Xamarin.Android 功能。然而,本书中还有许多伟大的功能尚未被发现,包括音频、视频、面部识别、蓝牙、NFC 等等。
在本入门指南的背景下,我们简单地没有足够的空间涵盖 Android 支持的所有功能。请确保您访问developer.android.com/index.html和developer.xamarin.com/以增强您对其他有趣 Android 功能的了解。
我祝愿你在 Xamain.Android 开发中好运!快乐编码!








































浙公网安备 33010602011771号