Android-AR-高级教程-全-
Android AR 高级教程(全)
零、简介
增强现实是移动计算领域相对较新的发展。尽管它还很年轻,但它已经是这个行业中发展最快的领域之一。公司正在投入大量资金开发使用增强现实的产品,其中最引人注目的是谷歌的谷歌眼镜项目。大多数人认为增强现实很难实现。那是一种误解。像任何好的应用一样,好的增强现实应用需要花费一些精力来编写。你所需要做的就是在潜水前保持开放的心态。
这本书是给谁的
这本书的目标读者是那些想为谷歌的 Android 平台编写应用的人。这本书希望读者熟悉 Java 语言,并了解 Android 的基础知识。然而,已经做出努力来确保即使没有这种经验的人也能理解内容和代码。希望当你看完这本书的时候,你会知道如何使用增强现实的力量编写令人惊叹的丰富的 Android 应用。
这本书的结构
这本书分为九章。我们从增强现实的基本介绍开始,并逐步介绍越来越复杂的功能。在第五章中,我们来看看如何处理增强现实应用中可能出现的常见错误。之后,我们有四个示例应用,展示如何使用日益复杂的增强现实应用。这里给出了更详细的结构:
- Chapter I : This chapter gives you a concept of what augmented reality is. It has several examples of how augmented reality can be used around the world, and a short list of potential future applications.
- Chapter 2 : This chapter guides you to write a simple augmented reality application, which consists of four main functions commonly used in augmented reality applications. By the end of this chapter, you will have a skeleton structure that can be extended to any augmented reality application.
- Chapter III : In this chapter, you will learn about some of the most important functions of augmented reality: superposition and marking. In two sample applications, we introduced the use of standard Android widgets as overlays and the use of open source AndAR library to add tag recognition to our applications.
- Chapter IV : Chapter IV introduces the concept of artificial horizon by using a non-augmented reality app. Then write a second application, and use the artificial horizon in the augmented reality application.
- Chapter V : This chapter describes the most common mistakes in making augmented reality applications and provides solutions. Besides errors, it also discusses other problems that will not cause errors, but it will still prevent your application from running as expected.
- Chapter 6 : In this chapter, we wrote the first of four sample applications. This is a very simple AR application, which can provide the basic information of the user's current location and draw it on the map.
- Chapter 7 : This chapter shows you how to expand the example application in Chapter 6 into an appropriate application that can be used to allow the user to navigate from his/her current location to the location set by the user on the map.
- Chapter 8 : This chapter shows you how to use AndAR library to write an augmented reality model viewer, which allows you to display 3D models on a tag.
- Chapter 9 : The last chapter of this book demonstrates how to write the most complicated application: an augmented reality browser that displays data from Wikipedia and Twitter around you.
先决条件
本书包含一些相当高级的代码,并且假设您熟悉以下内容:
- Java programming language
- Basic object-oriented concept
- Android platform (medium knowledge)
- Fundamentals of Eclipse IDE
虽然具备所有这些先决条件并不是绝对的要求,但强烈建议这样做。你绝对需要一个 Android 设备来测试你的应用,因为应用中使用的许多功能在 Android 模拟器上是不可用的。
下载代码
本书中所示示例的代码可在 Apress 网站[www.apress.com/9781430239451](http://www.apress.com/9781430239451)
上获得。在该书的信息页面上的源代码/下载选项卡下可以找到一个链接。该选项卡位于页面相关标题部分的下方。
你也可以在[
github.com/RaghavSood/ProAndroidAugmentedReality](http://github.com/RaghavSood/ProAndroidAugmentedReality)
从这本书的 GitHub 资源库获得源代码。
如果您在我们的代码中发现了一个 bug,请在 GitHub 仓库中提出问题,或者通过下面给出的方式直接联系作者。
联系作者
如果您有任何问题、评论或建议,甚至发现本书中的错误,请随时通过电子邮件或 Twitter @ app aholics 16 与作者联系。
一、增强现实的应用
增强现实 ( AR )是一个相当新的、但仍然很大的领域。它没有很大的市场份额,而且它目前的大部分应用都只是原型。这使得 AR 成为一个非常令人期待且尚未开发的利基市场。目前 Android 市场上实现 AR 技术的应用非常少。这一章描述了 AR 在现实世界中的应用,给出了例子(在可能的情况下提供了图片),并讨论了现在是否有可能在 Android 平台上实现 AR。
增强现实与虚拟现实
增强现实(AR)和虚拟现实(VR)是界限有点模糊的领域。换一种说法,你可以把 VR 看成 AR 的前身,两者有部分重叠。这两种技术的主要区别在于,VR 不使用摄像头馈送。虚拟现实中显示的所有东西要么是动画,要么是预先录制的电影片段。
当前用途
尽管这是一个相对较新的领域,但有足够多的 AR 应用可供我们进行分类。在这里,我们来看看在 AR 世界中已经实现了什么。
临时用户
有数百个使用 AR 的应用旨在供普通人使用。它们有许多类型——例如,游戏、世界浏览器和导航应用。他们通常使用加速度计和 GPS 来获取设备的位置和物理状态。这些应用是用来享受和使用的。Android 开发者挑战赛 2 的获奖应用之一是一款 AR 游戏: SpecTrek 。游戏使用你的 GPS 找到你的位置,然后为你准备幽灵在周围地区狩猎。这款游戏还有一张地图,上面的鬼魂在谷歌地图上显示为标记。在游戏过程中,幽灵被添加到相机图像上。
另一方面,导航应用有识别道路和转弯的代码,并用箭头标出路线。这个过程并不像听起来那么简单,但今天却经常这样做。
最后,世界浏览器可能是所有广泛使用的休闲应用中最复杂的。他们需要几个后端数据库,还需要来自几个传感器的大量现场信息。毕竟,浏览器还是要把所有东西放在一起,在屏幕上显示一组图标。几乎你在市面上看到的每一个 app,不管是不是 AR,第一眼看上去都很简单。但是如果你深入研究代码和后端,你会意识到它们中的大多数实际上非常非常复杂,需要很长时间来创建。
休闲 AR 应用的最好例子是 SpecTrek 和 Wikitude 。总之,这些应用利用了几乎所有你可以用来在 Android 平台上制作 AR 应用的东西。我强烈建议您安装它们,并熟悉 Android 上 ar 的功能。
这个类别的大多数应用都可以在 Android 平台上实现。在一些情况下,他们甚至不使用所有的传感器。其中一些会变得相当复杂。图 1-1 和图 1-2 显示了来自 SpecTrek 的截图。
图 1-1。SpecTrek 的截图
*
图 1-2。 另一张 SpecTrek 的截图
军事和执法
军事和执法机构的使用要复杂得多,技术也更先进。它们的范围从 AR 护目镜到旨在帮助训练的全模拟器。军方和一些执法机构拥有利用 AR 技术的模拟器。房间或交通工具内的宽屏幕,屏幕上显示各种场景,受训者必须决定最佳行动方案。
一些先进的特种部队团队有基本的 AR 护目镜,随着视野中的土地,显示海拔高度,视角,光线强度等信息。这些信息是在现场用数学公式计算出来的,因为这些护目镜没有配备互联网连接。
专业的夜视镜也配备了 AR 技术。这些护目镜显示位置和其他信息,并试图填补夜视镜本身无法照亮的空白。
几乎所有的无人驾驶车辆都实现了 AR。这些交通工具,尤其是空中交通工具,可能离它们的操作者有几千公里远。这些车辆的外部安装了一个或多个摄像头,将视频传输给操作人员。这些车辆中的大多数也配备了几个传感器。传感器数据与视频一起发送给操作员。该数据然后被处理并在视频上被增强。操作员系统上的算法处理视频,然后挑出并标记感兴趣的建筑物或物体。所有这些都以叠加的形式显示在视频上。
这些类型的应用很难在 Android 设备上实现,因为有两个主要问题:
- 低处理能力(尽管随着 2012 年 5 月发布的 HTC One X 和三星 Galaxy S3 四核手机的发布,这不是一个太大的问题。)
- 缺少更多的输入设备和传感器
车辆
最近,车辆已经开始实施 AR 技术。挡风玻璃已经被大而宽的高清显示屏所取代。通常车辆上有多个屏幕,每个屏幕显示一个特定的方向。如果只有一个屏幕和多个摄像头,车辆将自动切换馈送或让用户选择这样做。车辆外部有几个摄像头,面向多个方向。屏幕上的图像覆盖了有用的数据,如小地图、指南针、方向箭头、备用路线、天气预报等等。目前,这种技术在飞机和火车上最为常见。采用这种技术的智能汽车正在市场上接受测试。潜艇和船只也在使用这项技术。最近停止的航天飞机也有这种 AR 技术。
这些应用可以在 Android 平台上以一种混合的方式实现。因为大多数 Android 设备似乎缺乏正常车辆所具有的功能,所以没有实现同类功能。另一方面,可以编写应用,通过使用 GPS 获得位置来帮助导航;使用方向 API 来获取方向;并使用加速度计来帮助获取车辆的速度。Android 设备提供 AR 电源,车辆提供车辆部分。
医疗
增强现实手术如今变得越来越普遍。以这种方式完成的手术出错率更低,因为计算机在手术中提供了有价值的输入,并使用这些信息来控制机器人执行部分或全部手术。计算机通常可以实时提供替代方案和指导,以改进手术。AR 流以及其他数据也可以发送给远程医生,他们可以查看患者的信息,就好像患者就在他们面前一样。
AR 技术还有其他的医疗应用。AR 机器可以用来监测大量的病人,并确保他们的生命体征在任何时候都处于观察之中。
这种 AR 技术从未在 Android 平台上实现,原因有几个:
- 它需要设备上的大量信息,因为互联网连接还不够可靠,不足以让病人冒生命危险。
- 这些医疗任务中的一些所需的处理能力目前在设备上是不可用的。
- 在外科手术和帮助医疗任务方面,Android 设备没有很大的市场。
最重要的是,目前设计和构建这样一个应用非常困难和昂贵。允许在医疗领域进行实时增强现实工作所需的人工智能算法还没有出现。除此之外,你还需要一个非常优秀的开发团队,一个技术高超、经验丰富的医生团队,以及大量的资金。
审判室
在几家商店中,AR 正在作为虚拟审判室进行试验。用户可以站在屏幕前,在某处安装一台摄像机。用户将看到自己显示在屏幕上。然后,用户使用鼠标或键盘等输入设备来选择任何可用的服装选项。然后,计算机会将该项目放大到用户的图像上,并显示在屏幕上。用户可以从各个角度观看自己。
原则上,这些应用可以为 Android 平台编写,但没有人这样做,因为缺乏兴趣,可能是因为不知道为什么有人会想要这样做。实际上这种类型的应用已经出现,但它们实际上是用于娱乐和修改人的面部特征。
旅游
旅游业也受到了 AR 魔力的影响。在世界各地的几个著名景点,有组织的旅游现在提供了一个头戴式 AR 系统,当你查看它时,它会显示当前网站及其建筑的信息。通过 AR,游客可以重建过去存在的建筑、城市、景观和地形。旅游 AR 也是大多数世界浏览应用的内置部分,因为它们提供了著名古迹的标记。旅游 AR 不仅限于历史古迹。它可用于查找陌生城市的公园、餐馆、酒店和其他与旅游相关的地点和景点。虽然没有被广泛使用,但在过去的几年里,它已经呈指数级增长。
这些应用的功能已经出现在世界各地的浏览器中,但显示的信息很少。还没有人实现过任何一个城市的完整版本,可以提供所需的信息。
建筑
有许多配有摄像头的机器可以从现有的结构中生成蓝图,或者在拟议的建筑工地上显示蓝图中的虚拟结构。这些加快了建筑工作,并有助于设计和检查建筑物。AR 还可以模拟自然灾害条件,并显示建筑结构在这种压力下将如何反应。
这个细分市场的应用在一定程度上可以在 Android 上编写。那些从房间的角度创建蓝图的软件已经为 iOS 平台编写,也可以为 Android 编写。那些在建筑规模上显示虚拟模型的方法有点困难,但仍然可行,只要要增强的模型能够适应 Android 进程和设备 RAM 的大小限制。
装配线
AR 技术在各种装配线上都有很大帮助,无论你是组装汽车、飞机、手机还是其他任何东西。预编程的护目镜可以提供如何组装它的分步说明。
这些应用可以为 Android 编写,只要组装过程可以在每个需要增加指令的步骤中加入标记。在这种情况下,信息可以存储在远程后端。
电影/表演
AR 技术已被用于增强电影和戏剧,通过静态背景和覆盖其上的屏幕来产生图像和场景,否则需要昂贵和高度详细的布景。
这是一个真正可行的选择。你所需要做的就是获取表演的素材或背景信息,在适当的地方放置标记,并在需要时增加素材或背景。
娱乐
在世界各地的几个游乐园中,AR 技术正被用于制作适合单个房间的游乐设施,并设法给你一整个游乐设施的体验。你将被安排坐在一辆装有液压装置的汽车或其他交通工具上。你的四周都被巨大的屏幕包围着,屏幕上显示着整个场景。根据场景是来自现场摄像机还是动画,这可能属于 VR 和 AR。随着虚拟轨道的前进,车辆在空中移动。如果轨道向下,车辆将向下倾斜,你实际上会感觉好像在向下移动。为了提供更真实的体验,AR 技术还配有一些风扇或喷水设备。
在 Android 上实现这一点是可能的,但是有一些限制。为了获得完全身临其境的体验,你需要一个大屏幕。一些平板电脑可能会提供足够的空间来获得良好的体验,但在手机上实现这一点有点过于乐观。此外,液压安装的车辆用于实际的游乐设施,以提供完整的运动体验。作为补偿,你需要一些创新思维。
教育
AR 技术已经成功地用于各种教育机构,作为教科书材料的附加内容,或者本身作为虚拟的 3d 教科书。通常使用头戴式设备,AR 体验可以让学生“重温”已知发生的事件,而无需离开课堂。
这些应用可以在 Android 平台上实现,但你需要一些课程材料供应器的支持。像这样的应用也有可能将 AR 推到最前沿,因为它们有非常大的潜在用户群。
艺术
AR 技术可以并且已经被用于帮助创作绘画、模型和其他形式的艺术。它还帮助残疾人实现他们的创作天赋。AR 还被广泛用于试验一种特定的设计,然后实际用墨水写下来或用石头雕刻出来。例如,画可以被虚拟地画出来,看它们是如何产生的,被提炼直到艺术家对它们满意,然后最终被放到画布上。
这些类型的应用也是可能的。他们将需要有几个美术相关的功能,很可能很少使用传感器。理想情况下,该设备应该有一个高分辨率的屏幕,再加上一个高分辨率的摄像头。
翻译
在世界各地,支持 AR 的设备正被用于翻译多种语言的文本。这些设备具有 OCR 功能,或者在设备上有一个完整的跨语言词典,或者通过互联网翻译语言。
这些应用已经投入生产。您需要编写或使用现成的光学字符识别(OCR)库来将相机中的图像转换为文本。从图像中提取文本后,你可以使用设备上的翻译词典,它必须与应用捆绑在一起,或者通过互联网翻译并显示结果。
天气预报
几乎每个新闻频道都有气象预报员在他身后的世界地图上预报天气。事实上,这些应用大多数都是增强版的。预报员站在巨大的绿色背景前。录制时,绿色背景充当标记。记录完成后,用电脑添加地图并定位,以配合预报员的行动。如果将预报实时传输给查看者,则在传输预报时会添加地图。
电视
AR 也可以在日常生活中找到。许多游戏节目,尤其是有问题的节目,通过玩家的视频来增加这些信息。即使在现场直播的体育比赛中,比分和其他与比赛相关的信息也会在视频中增加并发送给观众。稍微烦人的广告也增加了。
许多提供体育比赛直播的应用目前都实现了这一功能。
天文学
有许多应用对天文学家很有用,对其他人也很有趣。这些应用可以在白天或有雾的夜晚显示星星和星座的位置,并(或多或少)实时显示。
其他
AR 还有很多很多的用途,不能这么容易归类。它们大多仍处于设计和规划阶段,但有潜力将 AR 技术推向日常小工具的前沿。
未来用途
正如上一节所讨论的,AR 非常有名,并且有足够多的应用可供使用,值得关注。然而,由于硬件和算法的限制,这项技术有一些令人惊讶的用途现在还不能实现。
虚拟体验
在未来,AR 技术可以用来创造虚拟体验。你可以有一个头戴式系统,可以将你当前的位置转换成完全不同的东西。例如,你可以通过佩戴这样的系统来体验电影,并看到电影在你身边发生。你可以把你的房子改造成中世纪的城堡或者国际空间站。加上听觉 AR 和一些气味发射技术,整个体验可以变得栩栩如生,感觉完全真实。除此之外,穿上可以模仿触觉的连体衣会让它变得绝对且不可否认的真实。
如果它出现的话,这将很难在 Android 上实现,因为 Android 缺乏实现这种事情所需的传感器和输入方法。它的视觉功能可以在一定程度上实现,但声音和感觉功能将遥不可及,除非有人在移植版本的 Android 上创建一个带有头戴式显示器和声音的紧身衣。
不可能的模拟
AR 技术可以做真正的硬件做不到的事情,至少目前是这样。你可以在屏幕上放一个普通的物体,比如一个立方体。然后你可以对这个立方体施加各种场景和力,看看结果如何。你不能用真实的硬件做到这一点,因为真实的硬件通常不被破坏就不能改变形状。你也可以用实验来测试理论,否则这将是极其昂贵或完全不可能的。
等到其他真实世界的模型开发出来的时候,这也许有可能在 Android 上实现,因为高端模拟的唯一硬性要求是数据和大量的处理能力。按照手机功能不断增强的速度,它们可能会快到足以运行此类应用。
全息图
AR 允许用户直接或间接观看世界,这可能使用户在他们面前拥有全息图。这些全息图可以是交互式的,或者仅仅是描述性的。他们可以展示任何东西。
即使在今天,使用标记显示模型的应用的高度修改版本也可以做到这一点。代替静态模型,应用可以显示动画或录音或直播。然而,这不会提供真正的全息图体验,因为它将只在设备的屏幕上。
视频会议
AR 可以允许多个人出现在同一个会议室,如果将会议室的视频传送给他们的话。人们可以使用网络摄像头和其他人一起“出现”在房间的座位上。这可以创造一个合作的环境,即使合作者相隔几千公里。
这个应用可以通过一些高级定位算法和高速互联网连接来实现。你需要算法,因为参加会议的人不可能一直呆在同一个地方。你需要一次又一次地定位他们,这样他们就不会和其他人重叠。
电影
AR 可以用来播放整部电影。剧院可以用电影的背景来代替,或者剧院可以只用演员来代替。在第一种方法中,演员可以被增强到背景上,而在第二种方法中,背景可以在演员后面被增强。这些可以提供更真实和有趣的电影,同时保持拍摄成本下降。
像这样的应用已经在生产中,但在质量、受欢迎程度和复杂程度上还不足以让我把它从未来的实现中拖出来。虽然这些应用并不容易制作,但它们也不是很难。
手势控制
AR 可以用来实现许多手势控制,例如眼睛拨号。相机可以跟踪用户的眼球运动来选择适当的数字键。选择所需的按键后,用户可以眨眼按下该数字,然后继续选择下一个按键。这同样可以用来控制音乐播放器、移动应用、电脑和其他形式的技术。
这类应用需要几样东西:
- 分辨率合理的前置摄像头
- 写得很好的算法来检测精细的眼球运动,并能够将它们与其他运动区分开来,例如检查侧视镜
AR 从一开始就已经走过了很长的路,还有很长的路要走。它的基本要求是摄像头、GPS、加速度计和指南针,几乎市场上的每一款 Android 设备都满足了这些要求。虽然使用 AR 技术的应用存在于 Android 平台,但与其他类型的应用相比,它们在数量上很少。现在是通过制作 AR 应用进入 Android 平台的好时机,因为竞争足够激烈,足以推动用户对这些应用产生兴趣,但还没有激烈到把你赶出这个行业。考虑到市场上相对较少的 AR 应用,如果你想出一个好的 AR 应用,它的竞争应用不会超过 3-5 个,这也是一个很好的机会,给你一个很大的优势。下一章讲解 Android 上 AR 应用的基础知识,开发一个基础应用。
总结
这就结束了我们对 AR 的当前和未来使用及其在 Android 平台上的实现(或可能的实现)的观察。下一章着眼于在 Android 上创建 AR 应用的基础。*
二、Android 平台上增强现实的基础知识
现在,你对什么是增强现实(AR)、世界各地正在用它做什么以及你可以在 Android 设备上用它做什么有了一个基本的概念。本章将带你进入 Android 上的 AR 世界,并教你它的基础知识。为了帮助你理解本书中这里(和其他地方)所做的一切,我们将创建应用来演示我们正在学习的内容。本章将专注于制作一个基本的应用,它包含任何高级 AR 应用的四个主要部分:相机、GPS、加速度计和指南针。
创建应用
这是一个非常简单的应用。它从 GPS、指南针、照相机和加速度计接收的任何数据都没有覆盖和实际用途。在下一章,我们将在这个应用的基础上添加覆盖图。
首先,我们需要创建一个新项目。在包名中,我使用的是 com.paar.ch2。您可以使用任何适合您的名称,但请确保更改这里代码中的任何引用,以匹配您的包名。该项目应设置为至少支持 Android 2.1。我正在针对 Android 4.0(冰淇淋三明治)构建项目,但您可以选择自己的目标。
照相机
每个 AR 应用中的第一件事是相机,它构成了 AR 中 99%的现实(另外 1%由 3 个基本传感器组成)。要在您的应用中使用相机,我们首先需要将权限请求和uses-feature
行添加到我们的清单中。我们还必须告诉 Android,我们希望我们的活动是风景,我们将自己处理某些配置更改。添加之后,清单应该类似于清单 2-1 :
清单 2-1。 更新舱单代码
`
<activity
android:label="@string/app_name"
android:name=".ProAndroidAR2Activity"
android:screenOrientation = "landscape"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:configChanges = "keyboardHidden|orientation">
我们也可以在<application>
元素开始之前添加权限;请确保它是清单的一部分,并且没有侵入任何其他元素。
现在让我们来看看实际的摄像机代码。相机需要一个SurfaceView
,在上面它会渲染它看到的东西。我们将使用SurfaceView
创建一个 XML 布局,然后使用那个SurfaceView
来显示相机预览。将您的 XML 文件(在本例中为main.xml
)修改如下:
清单 2-2。修改 main.xml
<?xml version="1.0" encoding="utf-8"?> <android.view.SurfaceView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/cameraPreview" android:layout_width="fill_parent" android:layout_height="fill_parent" > </android.view.SurfaceView>
这段代码没有什么突破性的东西。我们没有使用普通的布局,比如LinearLayout
或RelativeLayout
,而是简单地向 XML 文件添加了一个SurfaceView
,其高度和宽度属性被设置为允许它填充整个可用屏幕。我们给它分配 ID cameraPreview
,这样我们就可以从代码中引用它。现在重要的一步是使用 Android 相机服务,并告诉它连接到我们的SurfaceView
来显示来自相机的实际预览。
要实现这一点,需要做三件事:
-
我们创建一个
SurfaceView
,它在我们的 XML 布局中。 -
我们还需要一个
SurfaceHolder
,它控制我们的SurfaceView
的行为(例如,它的大小)。当发生变化时,例如当预览开始时,它也会得到通知。 -
We need a
Camera
, obtained from theopen()
static method on theCamera
class.要将所有这些串联起来,我们只需做以下事情:
-
通过
getHolder()
获取SurfaceView
的SurfaceHolder
。 -
注册一个
SurfaceHolder.Callback
,这样当我们的SurfaceView
准备好或改变时,我们会得到通知。 -
通过
SurfaceHolder
告诉SurfaceView
,它有SURFACE_TYPE_PUSH_BUFFERS
类型(使用setType()
)。这表明系统中的某些东西将更新SurfaceView
并提供位图数据来显示。
在你吸收和理解了所有这些之后,你就可以进行实际的编码工作了。首先,声明以下变量,并添加导入。完成后,您的类的顶部应该看起来像这样:
清单 2-3。 进口和变量申报
`package com.paar.ch2;
import android.app.Activity;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class ProAndroidAR2Activity extends Activity {
SurfaceView cameraPreview;
SurfaceHolder previewHolder;
Camera camera;
boolean inPreview;`
让我详细说明进口。第一个和第三个是显而易见的,但第二个是重要的,因为它是相机。确保从硬件包而不是图形包中导入Camera
,因为那是一个不同的Camera
类。SurfaceView
和SurfaceHolder
同样重要,但是没有两个选项可供选择。
关于变量。cameraPreview
是一个SurfaceView
变量,它将保存对 XML 布局中SurfaceView
的引用(这将在onCreate()
中完成)。previewHolder
是管理SurfaceView
的SurfaceHolder
。camera
是Camera
对象,将处理所有相机的东西。最后,inPreview
是我们的小布尔朋友,它将使用他的二元逻辑告诉我们一个预览是否是活动的,并给我们指示,以便我们可以正确地发布它。
现在我们继续为我们的小应用使用onCreate()
方法:
清单 2-4。 onCreate()
`@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
inPreview = false;
cameraPreview = (SurfaceView)findViewById(R.id.cameraPreview);
previewHolder = cameraPreview.getHolder();
previewHolder.addCallback(surfaceCallback);
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}`
我们将我们的视角设置为我们心爱的main.xml
,将inPreview
设置为false
(我们现在不显示相机的预览)。之后,我们从 XML 文件中找到我们的 SurfaceView
,并将其赋给cameraPreview
。然后我们运行getHolder()
方法,添加我们的回调(我们将在几分钟后进行这个回调;不用担心现在会弹出的错误),将previewHolder
的类型设置为SURFACE_TYPE_PUSH_BUFFERS
。
现在一个Camera
对象采用一个setPreviewDisplay()
方法,该方法采用一个SurfaceHolder
并安排摄像机预览显示在相关的SurfaceView
上。然而,SurfaceView
在转换到SURFACE_TYPE_PUSH_BUFFERS
模式后可能不会立即准备好。因此,虽然之前的设置工作可以在onCreate()
方法中完成,但是我们应该等到SurfaceHolder.Callback
调用了它的surfaceCreated()
方法之后再注册Camera
。有了这个小小的解释,我们可以回到编码:
清单 2-5。 表面回调
SurfaceHolder.Callback surfaceCallback=new SurfaceHolder.Callback() { public void surfaceCreated(SurfaceHolder holder) { try { camera.setPreviewDisplay(previewHolder); } catch (Throwable t) { Log.e("ProAndroidAR2Activity", "Exception in setPreviewDisplay()", t); } }
现在,一旦 Android 对SurfaceView
进行了设置和调整,我们需要将配置数据传递给Camera
,这样它就知道应该绘制多大的预览。由于 Android 已经移植并安装在数百种不同的硬件设备上,因此没有办法安全地预先确定预览窗格的大小。等待我们的SurfaceHolder.Callback
的surfaceChanged()
方法被调用是非常简单的,因为这可以告诉我们SurfaceView
的大小。然后我们可以将这些信息推入到一个Camera.Parameters
对象中,用这些参数更新Camera
,并让Camera
通过startPreview()
显示预览。现在我们可以回到编码:
清单 2-6。 sufaceChanged()
`public void surfaceChanged(SurfaceHolder holder, int format, int width, int
height) {
Camera.Parameters parameters=camera.getParameters();
Camera.Size size=getBestPreviewSize(width, height, parameters);
if (size!=null) {
parameters.setPreviewSize(size.width, size.height);
camera.setParameters(parameters);
camera.startPreview();
inPreview=true;
}
}`
最终,您会希望您的应用释放相机,并在需要时重新获取它。这将节省资源;而且很多设备只有一个物理摄像头,一次只能在一个活动中使用。有多种方法可以做到这一点,但是我们将使用onPause()
和onResume()
方法:
清单 2-7。 onResume()和 onPause()
`@Override
public void onResume() {
super.onResume();
camera=Camera.open();
}
@Override
public void onPause() {
if (inPreview) {
camera.stopPreview();
}
camera.release();
camera=null;
inPreview=false;
super.onPause();
}`
当活动被销毁时,您也可以这样做,如下所示,但我们不会这样做:
清单 2-8。 【地表摧毁】(
public void surfaceDestroyed(SurfaceHolder holder) { camera.stopPreview(); camera.release(); camera=null; }
现在,我们的小演示应用应该可以编译并显示相机在屏幕上看到的漂亮预览。然而,我们还没有完全完成,因为我们还需要添加三个传感器。
这就把我们带到了应用的相机部分的末尾。这是到目前为止这个类的全部代码,包含了所有的内容。您应该将其更新为如下所示,以防遗漏某些内容:
清单 2-9。 完整代码清单
`package com.paar.ch2;
import android.app.Activity;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class ProAndroidAR2Activity extends Activity {
SurfaceView cameraPreview;
SurfaceHolder previewHolder;
Camera camera;
boolean inPreview;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
inPreview = false;
cameraPreview = (SurfaceView)findViewById(R.id.cameraPreview);
previewHolder = cameraPreview.getHolder();
previewHolder.addCallback(surfaceCallback);
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
@Override
public void onResume() {
super.onResume();
camera=Camera.open();
}
@Override
public void onPause() {
if (inPreview) {
camera.stopPreview();
}
camera.release();
camera=null;
inPreview=false;
super.onPause();
}
private Camera.Size getBestPreviewSize(int width, int height,
Camera.Parameters parameters) {
Camera.Size result=null;
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
if (size.width<=width && size.height<=height) {
if (result==null) {
result=size;
}
else {
int resultArea=result.widthresult.height;
int newArea=size.widthsize.height;
if (newArea>resultArea) {
result=size;
}
}
}
}
return(result);
}
SurfaceHolder.Callback surfaceCallback=new SurfaceHolder.Callback() {
public void surfaceCreated(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(previewHolder);
}
catch (Throwable t) {
Log.e(TAG, "Exception in setPreviewDisplay()", t);
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
Camera.Parameters parameters=camera.getParameters();
Camera.Size size=getBestPreviewSize(width, height, parameters);
if (size!=null) {
parameters.setPreviewSize(size.width, size.height);
camera.setParameters(parameters);
camera.startPreview();
inPreview=true;
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
// not used
}
};
}`
方位传感器
方位传感器是磁场传感器和加速度传感器的组合。有了这两个传感器的数据和一点三角学,你就可以得到设备的pitch
、roll
和heading
( azimuth
)。如果你喜欢三角学,你会失望地知道 Android 为你做了所有的计算,你可以简单地从一个SensorEvent
中取出数值。
注意:磁场罗盘在金属物体周围会变得有点疯狂。猜猜测试时什么大型金属物体可能会靠近您的设备?你的电脑!如果你的读数不是你所期望的,请记住这一点。
图 2-1 显示了方位传感器的轴。
图 2-1。 装置的斧子。
在我们开始从 Android 中获取这些价值并使用它们之前,让我们多了解一点它们到底是什么。
- X 轴或航向:X 轴有点像指南针。它测量设备面向的方向,其中 0°或 360°为北,90°为东,180°为南,270°为西。
- Y 轴或俯仰:该轴测量设备的倾斜度。如果设备是平的,读数将是 0;如果顶部指向天花板,读数将是 90;如果设备是倒置的,读数将是 90。
- Z 轴或滚动:该轴测量设备的侧向倾斜。0°是平背,-90°是朝左,90°是屏幕朝右。
实际上有两种方法可以获得前面的数据。您可以直接查询方位传感器,或者分别获取加速度计和磁场传感器的读数,并计算方位。后者要慢几倍,但精度更高。在我们的应用中,我们将直接查询方向传感器。您可以从将下列变量添加到您的类开始:
清单 2-10。 新变量声明
`final static String TAG = "PAAR";
SensorManager sensorManager;
int orientationSensor;
float headingAngle;
float pitchAngle;
float rollAngle;`
字符串TAG
是一个常量,我们将在所有日志语句中使用它作为标签。sensorManager
将用于获取我们所有的传感器数据,并管理我们的传感器。浮动headingAngle
、pitchAngle
和rollAngle
将分别用于存储设备的航向、俯仰和横滚。
添加完上面给出的变量后,将下面几行添加到您的onCreate()
:
清单 2-11。 实现传感器管理器
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); orientationSensor = Sensor.TYPE_ORIENTATION; sensorManager.registerListener(sensorEventListener, sensorManager.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
是一个系统服务,我们在第一行中得到对它的引用。然后,我们将Sensor.TYPE_ORIENTATION
的常数值分配给orientationSensor
,该常数值基本上是给予方位传感器的常数。最后,我们为默认方位传感器注册我们的SensorEventListener
,带有正常延迟。SENSOR_DELAY_NORMAL
适合 UI 改动,SENSOR_DELAY_GAME
适合在游戏中使用,SENSOR_DELAY_UI
适合更新 UI 线程,SENSOR_DELAY_FASTEST
是硬件支持最快的。这些设置告诉 Android 您希望传感器更新的频率。Android 不会总是在指定的时间间隔给出它。它返回的值可能稍慢或稍快—通常更快。您应该只使用您需要的延迟,因为传感器消耗大量的 CPU 和电池寿命。
现在,sensorEventListener
下面应该有一条红色下划线。这是因为到目前为止,我们实际上还没有创建侦听器;我们现在就去做:
清单 2-12。 sensorEventListener
`final SensorEventListener sensorEventListener = new SensorEventListener() {
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION)
{
headingAngle = sensorEvent.values[0];
pitchAngle = sensorEvent.values[1];
rollAngle = sensorEvent.values[2];
Log.d(TAG, "Heading: " + String.valueOf(headingAngle));
Log.d(TAG, "Pitch: " + String.valueOf(pitchAngle));
Log.d(TAG, "Roll: " + String.valueOf(rollAngle));
}
}
public void onAccuracyChanged (Sensor senor, int accuracy) {
//Not used
}
};`
我们创建并注册sensorEventListener
作为新的SensorEventListener
。然后,当传感器的值改变时,我们使用onSensorChanged()
方法接收更新。因为onSensorChanged()
接收所有传感器的更新,所以我们使用一个if
语句来过滤掉除方位传感器之外的所有东西。然后,我们将来自传感器的值存储在变量中,并将它们打印到日志中。我们也可以在相机预览中叠加这些数据,但这超出了本章的范围。我们现在也有onAccuracyChanged()
方法,我们现在不使用它。根据 Eclipse 的说法,它之所以存在,是因为您必须实现它。
现在,为了让我们的应用运行良好,并且不会耗尽用户的电池,我们将在onResume()
和onPause()
方法中注册和注销我们的传感器。将它们更新为以下内容:
清单 2-13。 onResume()和 onPause()
`@Override
public void onResume() {
super.onResume();
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
camera=Camera.open();
}
@Override
public void onPause() {
if (inPreview) {
camera.stopPreview();
}
sensorManager.unregisterListener(sensorEventListener);
camera.release();
camera=null;
inPreview=false;
super.onPause();
}`
这就完成了方向传感器的部分。我们现在来看看加速度计传感器。
加速度计
加速度计测量沿三个方向轴的加速度:左右(横向(X))、前后(纵向(Y))和上下(垂直(Z))。这些值在值的浮点数组中传递。
图 2-2 显示了加速度计的轴线。
图 2-2。 加速度计轴
在我们的应用中,我们将接收加速度计值,并通过 LogCat 输出它们。在本书的后面,我们将使用加速度计来确定速度和其他东西。
让我们快速看一下加速度计的轴,以及它们到底测量什么。
- X 轴:在带有普通加速度计的普通设备上,X 轴测量横向加速度。即从左到右;从右到左。如果你把它移到右边,读数是正的,如果你把它移到左边,读数是负的。例如,一个设备平躺着,面朝上,在纵向上沿着你右边的表面移动,会在 X 轴上产生一个正读数。
- Y 轴:Y 轴的功能与 X 轴相同,只是它测量纵向加速度。当保持在 X 轴上描述的相同配置的设备沿其顶部方向移动时,记录正读数,如果沿相反方向移动,记录负读数。
- Z 轴:该轴测量上下运动的加速度,读数为正表示向上运动,读数为负表示向下运动。当静止时,由于重力的作用,你会得到大约-9.8 米/秒 2 的读数。在你的计算中,应该考虑到这一点。
现在开始编码工作吧。我们将使用与之前相同的加速度计SensorManager
。我们只需要添加一些变量,获得加速度传感器,并在onSensorChanged()
方法中添加另一个过滤if
语句。让我们从变量开始:
清单 2-14。 加速度计变量
int accelerometerSensor; float xAxis; float yAxis; float zAxis;
accelerometerSensor
将用于存储加速度计的常数,xAxis
将存储传感器返回的 X 轴值,yAxis
将存储传感器返回的 Y 轴值,zAxis
将存储传感器返回的 Z 轴值。
添加变量后,我们还需要在onCreate()
方法中更新与传感器相关的代码,这样我们就可以在onSensorChanged()
方法中使用和监听加速度计。将onCreate()
中的传感器代码修改如下:
清单 2-15。 修改 onCreate()
`sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
orientationSensor = Sensor.TYPE_ORIENTATION;
accelerometerSensor = Sensor.TYPE_ACCELEROMETER;
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL);`
我们只是简单地为加速度计重复了我们已经为方向传感器所做的,所以你应该可以理解这里发生了什么。现在我们必须更新sensorEventListener
来监听加速度计,方法是将代码改为如下:
清单 2-16。 修改了 sensorEventListener()
final SensorEventListener sensorEventListener = new SensorEventListener() { public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION) { headingAngle = sensorEvent.values[0]; pitchAngle = sensorEvent.values[1];
`rollAngle = sensorEvent.values[2];
Log.d(TAG, "Heading: " + String.valueOf(headingAngle));
Log.d(TAG, "Pitch: " + String.valueOf(pitchAngle));
Log.d(TAG, "Roll: " + String.valueOf(rollAngle));
}
else if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
{
xAxis = sensorEvent.values[0];
yAxis = sensorEvent.values[1];
zAxis = sensorEvent.values[2];
Log.d(TAG, "X Axis: " + String.valueOf(xAxis));
Log.d(TAG, "Y Axis: " + String.valueOf(yAxis));
Log.d(TAG, "Z Axis: " + String.valueOf(zAxis));
}
}`
同样,我们正在重复我们对方向传感器所做的,以监听加速度计传感器的变化。我们使用if
语句来区分两个传感器,用新值更新适当的浮点数,并将新值打印到日志中。现在剩下的就是更新onResume()
方法来再次注册加速度计:
清单 2-17。 修改 onResume()
`@Override
public void onResume() {
super.onResume();
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL);
camera=Camera.open();
}`
我们不需要改变onPause()
中的任何东西,因为我们在那里注销了整个监听器,包括所有相关的传感器。
这样,我们就到了两个传感器的终点。现在剩下的就是实现 GPS 来完成我们的应用。
全球定位系统
全球定位系统(GPS) 是一种定位系统,可以通过卫星给出极其精确的位置。这将是我们惊人的小演示应用的最后一部分。
首先,让我们简要了解一下 GPS 的历史及其工作原理。
GPS 是一种基于空间的卫星导航系统。它由美国管理,任何拥有 GPS 接收器的人都可以使用,尽管它最初只打算用于军事用途。
最初,一个接收器可以与 24 颗卫星通信。多年来,该系统已经升级到 31 颗卫星,加上目前标记为备用的 2 颗旧卫星。在任何时候,从地面上至少可以看到九颗卫星,而其余的则看不到。
为了获得定位,接收器必须与至少四颗卫星通信。卫星向接收器发送三条信息,然后这些信息被输入许多算法中的一种,以找到实际位置。这三部分是广播时间、特定卫星的轨道位置和所有其他卫星的粗略位置(系统健康或历书)。使用三角学计算位置。这可能会让你认为,在这种情况下,三颗卫星就足以获得定位,但通信中的定时误差乘以算法中使用的光速,会导致最终位置出现非常大的误差。
对于我们的传感器数据,我们使用了SensorManager
。然而,为了使用 GPS,我们将使用一个LocationManager
。虽然我们使用了一个SensorEventListener
传感器,我们将使用一个LocationListener
全球定位系统。首先,我们将声明将要使用的变量:
清单 2-18。 声明 GPS 变量
LocationManager locationManager; double latitude; double longitude; double altitude;
我们将只从我们的Location
对象获取纬度、经度和高度,但是如果你愿意,你也可以获取方位、时间等等。这完全取决于你希望你的应用做什么,以及你需要什么数据来做到这一点。在我们开始实际获取所有这些数据之前,让我们看一下纬度和经度是什么。
(经纬度)
纬度是地球网格系统的一部分;它们是从北极到南极的假想圆圈。赤道是 0 线,是纬度中唯一一个大圆。所有的纬度都相互平行。每个纬度距离它的上一个和下一个纬度大约 69 英里,或 111 公里。确切的距离因地球的曲率而异。
图 2-3 显示了球体的概念。
图 2-3。 纬度的图示
经度也是地球网格系统的假想线。它们从北极运行到南极,在两极汇合。每个经度是一个大圆的一半。经度 0°被称为本初子午线,穿过英国格林威治。两个经度之间的距离在赤道处最大,大约为 69 英里,或 111 公里,与两个纬度之间的距离大致相同。
图 2-4 显示了另一个球体上的概念。
图 2-4。 经度的图示
对纬度和经度有了新的理解后,我们可以继续从系统获取服务,并通过onCreate()
方法请求位置更新:
清单 2-19。 在 onCreate()中请求位置更新
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, locationListener);
首先,我们从 Android 获得位置服务。之后,我们使用requestLocationUpdates()
方法请求位置更新。第一个参数是我们想要使用的提供者的常量(在本例中是 GPS)。我们也可以使用手机网络。第二个参数是更新之间的时间间隔,以毫秒为单位,第三个参数是设备应该移动的最小距离,以米为单位,最后一个参数是应该通知的LocationListener
。
现在,locationListener
应该有红色下划线。那是因为我们还没有完全做到。让我们来解决这个问题:
清单 2-20。 位置监听器
`LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
latitude = location.getLatitude();
longitude = location.getLongitude();
altitude = location.getAltitude();
Log.d(TAG, "Latitude: " + String.valueOf(latitude));
Log.d(TAG, "Longitude: " + String.valueOf(longitude));
Log.d(TAG, "Altitude: " + String.valueOf(altitude));
}
public void onProviderDisabled(String arg0) {
// TODO Auto-generated method stub
}
public void onProviderEnabled(String arg0) {
// TODO Auto-generated method stub
}
public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
// TODO Auto-generated method stub
}
};`
每当您的最小时间间隔出现或设备移动您指定的最小距离或更大距离时,就会调用onLocationChanged()
方法。该方法接收到的Location
对象包含大量信息:纬度、经度、高度、方位等等。然而,在这个例子中,我们只提取和保存纬度、海拔和经度。Log.d
语句只是显示接收到的值。
GPS 是 Android 系统中电池最密集的部分之一,充满电的电池可能会在几个小时内耗尽。这就是为什么我们将在onPause()
和onResume()
方法中经历释放和获取 GPS 的整个过程:
清单 2-21。 onResume()和 onPause()
`@Override
public void onResume() {
super.onResume();
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000,
2, locationListener);
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL);
camera=Camera.open();
}
@Override
public void onPause() {
if (inPreview) {
camera.stopPreview();
}
locationManager.removeUpdates(locationListener);
sensorManager.unregisterListener(sensorEventListener);
camera.release();
camera=null;
inPreview=false;
super.onPause();
}`
这让我们结束了我们的演示应用。如果做得好,你应该在屏幕上看到相机预览,再加上一个快速移动的 LogCat。现在,这里给出了在项目创建时从默认状态修改的所有文件,这样您就可以确保一切就绪。
ProAndroidAR2Activity.java
清单 2-22。ProAndroidAR2Activity.java全面上市
`package com.paar.ch2;
import android.app.Activity;
import android.hardware.Camera;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class ProAndroidAR2Activity extends Activity{
SurfaceView cameraPreview;
SurfaceHolder previewHolder;
Camera camera;
boolean inPreview;
final static String TAG = "PAAR";
SensorManager sensorManager;
int orientationSensor;
float headingAngle;
float pitchAngle;
float rollAngle;
int accelerometerSensor;
float xAxis;
float yAxis;
float zAxis;
LocationManager locationManager;
double latitude;
double longitude;
double altitude;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
2000, 2, locationListener);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
orientationSensor = Sensor.TYPE_ORIENTATION;
accelerometerSensor = Sensor.TYPE_ACCELEROMETER;
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL);
inPreview = false;
cameraPreview = (SurfaceView)findViewById(R.id.cameraPreview);
previewHolder = cameraPreview.getHolder();
previewHolder.addCallback(surfaceCallback);
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
latitude = location.getLatitude();
longitude = location.getLongitude();
altitude = location.getAltitude();
Log.d(TAG, "Latitude: " + String.valueOf(latitude));
Log.d(TAG, "Longitude: " + String.valueOf(longitude));
Log.d(TAG, "Altitude: " + String.valueOf(altitude));
}
public void onProviderDisabled(String arg0) {
// TODO Auto-generated method stub
}
public void onProviderEnabled(String arg0) {
// TODO Auto-generated method stub
}
public void onStatusChanged(String arg0, int arg1, Bundle arg2)
{
// TODO Auto-generated method stub
}
};
final SensorEventListener sensorEventListener = new SensorEventListener() {
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.sensor.getType() ==
Sensor.TYPE_ORIENTATION)
{
headingAngle = sensorEvent.values[0];
pitchAngle = sensorEvent.values[1];
rollAngle = sensorEvent.values[2];
Log.d(TAG, "Heading: " + String.valueOf(headingAngle));
Log.d(TAG, "Pitch: " + String.valueOf(pitchAngle));
Log.d(TAG, "Roll: " + String.valueOf(rollAngle));
}
else if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
{
xAxis = sensorEvent.values[0];
yAxis = sensorEvent.values[1];
zAxis = sensorEvent.values[2];
Log.d(TAG, "X Axis: " + String.valueOf(xAxis));
Log.d(TAG, "Y Axis: " + String.valueOf(yAxis));
Log.d(TAG, "Z Axis: " + String.valueOf(zAxis));
}
}
public void onAccuracyChanged (Sensor senor, int accuracy) {
//Not used
}
};
@Override
public void onResume() {
super.onResume();
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000,
2, locationListener);
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL);
camera=Camera.open();
}
@Override
public void onPause() {
if (inPreview) {
camera.stopPreview();
}
locationManager.removeUpdates(locationListener);
sensorManager.unregisterListener(sensorEventListener);
camera.release();
camera=null;
inPreview=false;
super.onPause();
}
private Camera.Size getBestPreviewSize(int width,
int height, Camera.Parameters parameters) {
Camera.Size result=null;
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
if (size.width<=width && size.height<=height) {
if (result==null) {
result=size;
}
else {
int resultArea=result.widthresult.height;
int newArea=size.widthsize.height;
if (newArea>resultArea) {
result=size;
}
}
}
}
return(result);
}
SurfaceHolder.Callback surfaceCallback=new SurfaceHolder.Callback() {
public void surfaceCreated(SurfaceHolder holder) {
try {
camera.setPreviewDisplay(previewHolder);
}
catch (Throwable t) {
Log.e(TAG, "Exception in setPreviewDisplay()", t);
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Camera.Parameters parameters=camera.getParameters();
Camera.Size size=getBestPreviewSize(width, height, parameters);
if (size!=null) {
parameters.setPreviewSize(size.width, size.height);
camera.setParameters(parameters);
camera.startPreview();
inPreview=true;
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
// not used
}
};
}`
AndroidManifest.xml
清单 2-23。Android manifest . XML 的完整列表
`
main.xml
清单 2-24。main . XML 的完整清单
<?xml version="1.0" encoding="utf-8"?> <android.view.SurfaceView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/cameraPreview" android:layout_width="fill_parent" android:layout_height="fill_parent" > </android.view.SurfaceView>
样本 LogCat 输出
写出应用后,使用顶部的 Run As 按钮从 Eclipse 运行它。如果您在模拟器上运行它,您将一无所获,因为传感器没有被模拟。在一个设备上,你应该在屏幕上看到一个相机预览,再加上一个快速移动的 LogCat,看起来有点像图 2-5 。
图 2-5。 应用运行时 LogCat 的截图。
如果你有一个清晰的天空视图,LogCat 还将包括三条线,告诉你纬度,经度和海拔高度。
结论
在本章中,您学习了如何使用摄像头,如何从加速度计和方位传感器读取值,以及如何使用 GPS 获取用户的位置。
你还学会了利用任何全功能增强现实应用的四个基本组件。你不会总是在你的应用中使用这四样东西。其实很少有 app 有这样的要求。
这一章应该会让你对 AR 有一个基本的了解,来自这个 app 的项目本质上是一个合适的 AR app 的骨架。
下一章将讨论叠加,以及它们如何给用户一个真实的增强体验。
三、添加覆盖
如前所述,增强现实(AR)是与正在显示的直接或间接相机预览相关的数据的叠加。在大多数 AR 应用中,相机预览会首先扫描标记。在翻译类应用中,预览会扫描文本。而且在一些游戏 app 里,不做扫描;相反,字符、按钮、文本等会覆盖在预览上。
本章的所有源代码都可以从本书的页面[
www.apress.com](http://www.apress.com)
或 GitHub 库下载。
在第二章中,我们制作了一个基本的应用,显示设备摄像头的预览,通过 GPS 检索位置,获取加速度计读数,并检索方位传感器读数。在这一章中,我们将继续构建这个应用,并添加覆盖图。我们将添加正常的 Android 部件覆盖和实现标记识别。让我们从最简单的开始:小部件覆盖。
小工具覆盖
Android 平台提供了一堆标准的小部件,比如TextViews
、Buttons
和Checkboxes
。这些默认包含在 Android 操作系统中,可以由任何应用使用。它们可能是你可以在相机预览中叠加的最简单的东西。
首先,创建一个新的 Android 项目。本例中使用的插件名为 Pro Android AR 3 Widget Overlay,构建于 Android 4 之上,其 minSdkVersion 设置为 7 (Android 2.1),包名为 com.paar.ch3widgetoverlay(您可以根据自己的需求进行更改,但一定要更新这里给出的示例代码。)图 3-1 显示了项目设置屏幕。
图 3-1。 申请详情
从重复我们在上一章中所做的一切开始。你可以用手全部打出来,也可以复制粘贴。这是你的电话。一定要更新代码中的包名等等,这样它才能在新项目中工作。
复制完上一章的所有代码后,我们需要修改定义布局的 XML 文件,以允许小部件覆盖。之前整个布局是单个SurfaceView
,显示相机预览。因此,我们目前不能在布局中添加其他小部件。因此,我们将修改我们的 XML 文件,使其包含一个RelativeLayout
,并在这个RelativeLayout
中包含SurfaceView
和所有其他小部件。我们使用了一个RelativeLayout
,因为它允许我们轻松地在SurfaceView
上重叠窗口小部件。在本例中,我们将添加各种TextViews
来显示传感器数据。因此,在我们开始布局编辑之前,我们需要向项目的strings.xml
添加一些字符串资源:
清单 3-1。 字符串资源
<string name="xAxis">X Axis:</string> <string name="yAxis">Y Axis:</string> <string name="zAxis">Z Axis:</string> <string name="heading">Heading:</string> <string name="pitch">Pitch:</string> <string name="roll">Roll:</string> <string name="altitude">Altitude:</string> <string name="longitude">Longitude:</string> <string name="latitude">Latitude:</string> <string name="empty"></string>
这些字符串将为一些TextView
提供标签。确切地说,是一半。另一半将用来自传感器的数据更新。在这之后,您应该更新您的main.xml
文件,以便它有一个RelativeLayout
。在我们进入实际代码之前,让我们快速看一下什么是RelativeLayout
,以及它与其他布局相比如何。
布局选项
在 Android 中,有许多不同的根布局可用。这些布局定义了任何应用的用户界面。所有布局通常在位于/res/layout
的 XML 文件中定义。但是,布局及其元素可以在运行时通过 Java 代码动态创建。只有当应用需要动态添加小部件时,才会这样做。布局可以用 XML 声明,然后通过 Java 代码修改,就像我们在应用中经常做的那样。这是通过获取对布局的一部分(例如 TextView)的引用,并调用该类的各种方法来改变它来实现的。我们可以这样做,因为每个布局元素(包括布局)在 Android 框架中都有一个对应的 Java 类,它定义了修改它的方法。目前有四种不同的布局选项:
- 框架布局
- 表格布局
- 线性布局
- 相对布局
【Android 刚发布的时候,有第五种布局选项,叫做绝对布局。这种布局允许您使用精确的 x 和 y 坐标来指定元素的位置。这种布局现在已被弃用,因为它很难在不同的屏幕尺寸上使用。
框架布局
框架布局是最简单的布局类型。它实际上是一个很大的空白空间,你可以在上面放置一个子对象,这个子对象将被固定在屏幕的左上角。在第一个对象之后添加的任何其他对象将直接绘制在它的上面。
表格布局
表格布局将其子元素定位到行和列中。容器不显示它们的行、列或单元格的边框线。表格的列数与单元格最多的行数相同。表格可以将单元格留空,但单元格不能像 HTML 中那样跨列。TableRow
对象是一个TableLayout
的子视图(每个TableRow
定义表格中的一行)。每行有零个或多个单元格,每个单元格都由任何类型的其他视图定义。所以一行的单元格可以由各种视图对象组成,比如ImageView
或TextView
。单元格也可以是一个ViewGroup
对象(例如,您可以嵌套另一个TableLayout
作为单元格)。
线性布局
线性布局将所有子节点沿一个方向对齐—垂直或水平,这取决于您如何定义方向属性。所有的子元素都是一个接一个堆叠起来的,所以一个垂直的列表每一行只有一个子元素,不管它们有多宽;而一个水平列表只有一行高(最高的孩子的高度,加上填充)。A LinearLayout
考虑子对象之间的边距和每个子对象的重心(右对齐、居中或左对齐)。
相对布局
最后,相对布局允许子视图指定它们相对于父视图或彼此的位置(由 ID 指定)。因此,您可以通过右边框对齐两个元素,使一个元素位于另一个元素的下方,使其在屏幕上居中,使其向左居中,等等。元素是按照给定的顺序呈现的,所以如果第一个元素位于屏幕的中心,其他与该元素对齐的元素将相对于屏幕中心对齐。此外,由于这种排序,如果使用 XML 来指定这种布局,您将引用的元素(为了定位其他视图对象)必须在 XML 文件中列出,然后才能通过其引用 ID 从其他视图中引用它。
相对布局是唯一允许我们以应用需要的方式重叠视图的布局。由于它需要引用布局的其他部分来在屏幕上放置一个视图,你必须确保本书中的所有RelativeLayout
都准确地复制到你的代码中;否则,你的整个布局看起来会非常混乱,视图会到处都是,除了你放置它们的地方。
用相对布局更新 main.xml
在RelativeLayout
里面是一个SurfaceView
和 18 个TextView
。小部件的顺序和 id 很重要,因为它是一个RelativeLayout
。以下是布局文件:
清单 3-2。 相对布局
`
<TextView
android:id="@+id/yAxisLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/xAxisLabel"
android:layout_below="@+id/xAxisLabel"
android:text="@string/yAxis" />
<TextView
android:id="@+id/longitudeLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/latitudeLabel"
android:layout_below="@+id/latitudeLabel"
android:text="@string/longitude" />
<TextView
android:id="@+id/pitchValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/pitchLabel"
android:layout_alignBottom="@+id/pitchLabel"
android:layout_alignLeft="@+id/headingValue"
android:text="@string/empty" />
`
您可以通过查看 id 来了解每个TextView
的用途。确保布局代码完全正确;否则,你的整个布局看起来就像是被推进了搅拌机(厨房里的那种,不是我能用这个软件做出很酷的图形的那种)。我们将只引用代码中 id 中有“Value
”的TextView
,因为其他的只是标签。这些TextView
将用于显示我们的应用将接收的各种传感器值。
TextView 变量声明
布局在项目中之后,我们可以开始从代码中引用所有那些TextView
并用适当的数据更新它们。为了能够从代码中引用TextView
,我们需要一些变量来存储这些引用。将以下九个变量添加到该类的顶部(名称不言自明):
清单 3-3。 变量声明
TextView xAxisValue; TextView yAxisValue; TextView zAxisValue; TextView headingValue; TextView pitchValue; TextView rollValue; TextView altitudeValue; TextView latitudeValue; TextView longitudeValue;
更新 onCreate
之后,将下面的代码添加到onCreate()
方法中,这样每个TextView
对象都包含一个对 XML 中相应的TextView
的引用。
清单 3-4。 提供对 XML 文本视图的引用
xAxisValue = (TextView) findViewById(R.id.xAxisValue); yAxisValue = (TextView) findViewById(R.id.yAxisValue); zAxisValue = (TextView) findViewById(R.id.zAxisValue); headingValue = (TextView) findViewById(R.id.headingValue); pitchValue = (TextView) findViewById(R.id.pitchValue); rollValue = (TextView) findViewById(R.id.rollValue); altitudeValue = (TextView) findViewById(R.id.altitudeValue); longitudeValue = (TextView) findViewById(R.id.longitudeValue); latitudeValue = (TextView) findViewById(R.id.latitudeValue);
显示传感器的数据
现在我们有了一个引用,我们将使用我们的数据更新所有的TextView
,我们应该这样做。为了用正确的数据更新加速度计和方向传感器的数据,将SensorEventListener
修改为:
清单 3-5。 修改过的 SensorEventListener
`final SensorEventListener sensorEventListener = new SensorEventListener() {
public void onSensorChanged(SensorEvent sensorEvent) {
if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION)
{
headingAngle = sensorEvent.values[0];
pitchAngle = sensorEvent.values[1];
rollAngle = sensorEvent.values[2];
Log.d(TAG, "Heading: " + String.valueOf(headingAngle));
Log.d(TAG, "Pitch: " + String.valueOf(pitchAngle));
Log.d(TAG, "Roll: " + String.valueOf(rollAngle));
headingValue.setText(String.valueOf(headingAngle));
pitchValue.setText(String.valueOf(pitchAngle));
rollValue.setText(String.valueOf(rollAngle));
}
else if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
{
xAxis = sensorEvent.values[0];
yAxis = sensorEvent.values[1];
zAxis = sensorEvent.values[2];
Log.d(TAG, "X Axis: " + String.valueOf(xAxis));
Log.d(TAG, "Y Axis: " + String.valueOf(yAxis));
Log.d(TAG, "Z Axis: " + String.valueOf(zAxis));
xAxisValue.setText(String.valueOf(xAxis));
yAxisValue.setText(String.valueOf(yAxis));
zAxisValue.setText(String.valueOf(zAxis));
}
}
public void onAccuracyChanged (Sensor senor, int accuracy) {
//Not used
}
};`
现在传感器数据被写入日志和TextViews
。因为传感器延迟被设置为SENSOR_DELAY_NORMAL
,TextView
将会以一种适中的速率更新。如果延迟被设置为SENSOR_DELAY_GAME
,我们会让TextView
的更新速度超过我们的视觉速度。那会对 CPU 造成很大的负担。即使是现在,在一些较慢的设备上,该应用可能会显得滞后。
注意:你可以通过将更新TextViews
的代码转换成TimerTask
或Handler
来避免延迟。
既然数据来自方位和加速度传感器,我们应该为 GPS 做同样的事情。这或多或少是我们对SensorEventListener
所做的重复,除了它是对LocationListener
所做的:
清单 3-6。 修改了 LocationListener
`LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
latitude = location.getLatitude();
longitude = location.getLongitude();
altitude = location.getAltitude();
Log.d(TAG, "Latitude: " + String.valueOf(latitude));
Log.d(TAG, "Longitude: " + String.valueOf(longitude));
Log.d(TAG, "Altitude: " + String.valueOf(altitude));
latitudeValue.setText(String.valueOf(latitude));
longitudeValue.setText(String.valueOf(longitude));
altitudeValue.setText(String.valueOf(altitude));
}
public void onProviderDisabled(String arg0) {
// TODO Auto-generated method stub
}
public void onProviderEnabled(String arg0) {
// TODO Auto-generated method stub
}
public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
// TODO Auto-generated method stub
}
};`
数据再次被写入日志和TextView
中。如果您现在调试应用,您应该会在屏幕上看到一个摄像机预览和 18 个TextView
,其中 6 个应该会快速变化,而 3 个变化较慢。因为全球定位系统需要不间断的天空视图才能工作,而且可能需要一段时间来确定你的位置,所以与全球定位系统相关的字段可能需要一些时间来更新。
更新的 AndroidManifest.xml
最后,您需要更改这个项目的AndroidManifest.xml
:
清单 3-7。 修改后的 AndroidManifest.xml
`
<activity
android:label="@string/app_name"
android:name=".ProAndroidAR3Activity"
android:screenOrientation = "landscape"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:configChanges = "keyboardHidden|orientation">
这些是在你的相机预览上覆盖标准 Android 部件的基础。确保小部件已经就位,并且您的所有 id 都已对齐。之后,在你的应用中使用小部件就和在任何其他应用中完全一样了。你将调用同样的方法,使用同样的函数,做同样的事情。这适用于 Android 框架中的所有小部件。
测试 App
至此,我们结束了在你的相机预览上叠加标准 Android 小工具的工作。图 3-2 和 3-3 显示了应用完成后的样子。
图 3-2。 无 GPS 定位的 app 截图
图 3-3。 带 GPS 定位的应用截图
接下来,我们将看看如何在我们的应用中添加标记识别。
标记
标记是 AR 应用用来知道在哪里放置叠层的视觉线索。您可以选择任何容易识别的图像(如白色背景上的黑色问号)。图像的一个副本保存在你的应用中,而另一个副本被打印出来并放在现实世界中的某个地方(或者如果你的手非常稳定,就画出来)。标记识别是人工智能领域正在进行的研究的一部分。
我们将使用名为 AndAR 的开源 Android 库来帮助我们识别标记。AndAR 项目的详情可在[
code.google.com/p/andar](http://code.google.com/p/andar)
找到。
创建新项目。我这边的包名是 com.paar.ch3marker,默认的活动名是Activity.java
。
该应用将有四个它将识别的标记。对于每个标记,我们将提供一个. patt 文件,AndAR 可以用它来识别标记。这些文件以 AndAR 能够理解的方式描述了标记的外观。
如果您不喜欢提供的标记,或者感到无聊和喜欢冒险,您也可以创建和提供自己的标记。但是有一些限制:
- 标记必须是方形的。
- 边界必须对比鲜明。
- 边框必须是纯色。
标记可以是黑白的,也可以是彩色的。图 3-4 显示了一个标记的例子。
图 3-4。 样品安卓马克笔
您可以使用位于[
flash.tarotaro.org/blog/2009/07/12/mgo2/](http://flash.tarotaro.org/blog/2009/07/12/mgo2/)
的在线 flash 工具创建自己的标记。
Activity.java
先来编辑一下Activity.java
,这是一个比较小的类。
清单 3-8。 改装的 Activity.java
`public class Activity extends AndARActivity {
private ARObject someObject;
private ARToolkit artoolkit;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CustomRenderer renderer = new CustomRenderer();
setNonARRenderer(renderer);
try {
artoolkit = getArtoolkit();
someObject = new CustomObject1
("test", "marker_at16.patt", 80.0, new double[]{0,0});
artoolkit.registerARObject(someObject);
someObject = new CustomObject2
("test", "marker_peace16.patt", 80.0, new
double[]{0,0});
artoolkit.registerARObject(someObject);
someObject = new CustomObject3
("test", "marker_rupee16.patt", 80.0, new
double[]{0,0});
artoolkit.registerARObject(someObject);
someObject = new CustomObject4
("test", "marker_hand16.patt", 80.0, new double[]{0,0});
artoolkit.registerARObject(someObject);
} catch (AndARException ex){
System.out.println("");
}
startPreview();
}
public void uncaughtException(Thread thread, Throwable ex) {
Log.e("AndAR EXCEPTION", ex.getMessage());
finish();
}
}`
在onCreate()
方法中,我们首先得到savedInstanceState
。之后,我们创建一个对CustomRenderer
类的引用,几页后我们将创建这个类。然后我们设置非 AR 渲染器。现在是课程的主要部分。我们用 AndAR 和它们的相关对象注册所有四个标记。CustomObject1-4
是定义在每个标记上增加什么的类。最后,如果发生致命异常,使用uncaughtException()
方法干净地退出应用。
自定义对象覆盖
自定义对象基本上是 4 种不同颜色的 3D 盒子。它们根据标记的视图进行旋转等操作。图 3-5 显示了一个正在显示的立方体。
图 3-5。 四个自定义对象叠加之一
首先出场的是CustomObject1.java
。
清单 3-9。 自定义对象 1
`public class CustomObject1 extends ARObject {
public CustomObject1(String name, String patternName,
double markerWidth, double[] markerCenter) {
super(name, patternName, markerWidth, markerCenter);
float mat_ambientf[] = {0f, 1.0f, 0f, 1.0f};
float mat_flashf[] = {0f, 1.0f, 0f, 1.0f};
float mat_diffusef[] = {0f, 1.0f, 0f, 1.0f};
float mat_flash_shinyf[] = {50.0f};
mat_ambient = GraphicsUtil.makeFloatBuffer(mat_ambientf);
mat_flash = GraphicsUtil.makeFloatBuffer(mat_flashf);
mat_flash_shiny =
GraphicsUtil.makeFloatBuffer(mat_flash_shinyf);
mat_diffuse = GraphicsUtil.makeFloatBuffer(mat_diffusef);
}
public CustomObject1(String name, String patternName,
double markerWidth, double[] markerCenter, float[]
customColor) {
super(name, patternName, markerWidth, markerCenter);
float mat_flash_shinyf[] = {50.0f};
mat_ambient = GraphicsUtil.makeFloatBuffer(customColor);
mat_flash = GraphicsUtil.makeFloatBuffer(customColor);
mat_flash_shiny =
GraphicsUtil.makeFloatBuffer(mat_flash_shinyf);
mat_diffuse = GraphicsUtil.makeFloatBuffer(customColor);
}
private SimpleBox box = new SimpleBox();
private FloatBuffer mat_flash;
private FloatBuffer mat_ambient;
private FloatBuffer mat_flash_shiny;
private FloatBuffer mat_diffuse;
@Override
public final void draw(GL10 gl) {
super.draw(gl);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK,
GL10.GL_SPECULAR,mat_flash);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS,
mat_flash_shiny);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE,
mat_diffuse);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT,
mat_ambient);
gl.glColor4f(0, 1.0f, 0, 1.0f);
gl.glTranslatef( 0.0f, 0.0f, 12.5f );
box.draw(gl);
}
@Override
public void init(GL10 gl) {
}
}`
我们开始为盒子设置各种灯光,并在构造函数中用它们创建FloatBuffer
。然后我们直接从 AndAR 那里得到一个简单的盒子,这样我们就省去了制作它的麻烦。在draw()
方法中,我们画出一切。在这种情况下,在draw()
方法中完成的一切都将直接在标记上完成。
其他三个CustomObject
类与CustomObject1
完全相同,除了我们稍微改变了颜色。以下是你需要为CustomObject2
做的改动。
清单 3-10。 自定义对象 2
`public CustomObject2(String name, String patternName,
double markerWidth, double[] markerCenter) {
super(name, patternName, markerWidth, markerCenter);
float mat_ambientf[] = {1.0f, 0f, 0f, 1.0f};
float mat_flashf[] = {1.0f, 0f, 0f, 1.0f};
float mat_diffusef[] = {1.0f, 0f, 0f, 1.0f};
float mat_flash_shinyf[] = {50.0f};
mat_ambient = GraphicsUtil.makeFloatBuffer(mat_ambientf);
mat_flash = GraphicsUtil.makeFloatBuffer(mat_flashf);
mat_flash_shiny =
GraphicsUtil.makeFloatBuffer(mat_flash_shinyf);
mat_diffuse = GraphicsUtil.makeFloatBuffer(mat_diffusef);
}
//Same code everywhere else, except the draw() method
@Override
public final void draw(GL10 gl) {
super.draw(gl);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK,
GL10.GL_SPECULAR,mat_flash);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS,
mat_flash_shiny);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE,
mat_diffuse);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT,
mat_ambient);
gl.glColor4f(1.0f, 0, 0, 1.0f);
gl.glTranslatef( 0.0f, 0.0f, 12.5f );
box.draw(gl);
}`
以下是CustomObject3
的变化。
清单 3-11。 自定义对象 3
`public CustomObject3(String name, String patternName,
double markerWidth, double[] markerCenter) {
super(name, patternName, markerWidth, markerCenter);
float mat_ambientf[] = {0f, 0f, 1.0f, 1.0f};
float mat_flashf[] = {0f, 0f, 1.0f, 1.0f};
float mat_diffusef[] = {0f, 0f, 1.0f, 1.0f};
float mat_flash_shinyf[] = {50.0f};
mat_ambient = GraphicsUtil.makeFloatBuffer(mat_ambientf);
mat_flash = GraphicsUtil.makeFloatBuffer(mat_flashf);
mat_flash_shiny =
GraphicsUtil.makeFloatBuffer(mat_flash_shinyf);
mat_diffuse = GraphicsUtil.makeFloatBuffer(mat_diffusef);
}
//Same code everywhere else, except the draw() method
@Override
public final void draw(GL10 gl) {
super.draw(gl);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK,
GL10.GL_SPECULAR,mat_flash);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS,
mat_flash_shiny);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE,
mat_diffuse);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT,
mat_ambient);
gl.glColor4f(0f, 0, 1.0, 1.0f);
gl.glTranslatef( 0.0f, 0.0f, 12.5f );
box.draw(gl);
}`
最后,CustomObject4
的变化如下。
清单 3-12。海关对象 4
`public CustomObject4(String name, String patternName,
double markerWidth, double[] markerCenter) {
super(name, patternName, markerWidth, markerCenter);
float mat_ambientf[] = {1.0f, 0f, 1.0f, 1.0f};
float mat_flashf[] = {1.0f, 0f, 1.0f, 1.0f};
float mat_diffusef[] = {1.0f, 0f, 1.0f, 1.0f};
float mat_flash_shinyf[] = {50.0f};
mat_ambient = GraphicsUtil.makeFloatBuffer(mat_ambientf);
mat_flash = GraphicsUtil.makeFloatBuffer(mat_flashf);
mat_flash_shiny =
GraphicsUtil.makeFloatBuffer(mat_flash_shinyf);
mat_diffuse = GraphicsUtil.makeFloatBuffer(mat_diffusef);
}
//Same code everywhere else, except the draw() method
@Override
public final void draw(GL10 gl) {
super.draw(gl);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK,
GL10.GL_SPECULAR,mat_flash);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS,
mat_flash_shiny);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE,
mat_diffuse);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT,
mat_ambient);
gl.glColor4f(1.0f, 0, 1.0, 1.0f);
gl.glTranslatef( 0.0f, 0.0f, 12.5f );
box.draw(gl);
}`
自定义渲染器
现在我们只有CustomRenderer.java
要处理。这个类允许我们做任何非增强现实的事情以及设置 OpenGL 环境。
清单 3-13。 自定义渲染器
public class CustomRenderer implements OpenGLRenderer {
`private float[] ambientlight1 = {.3f, .3f, .3f, 1f};
private float[] diffuselight1 = {.7f, .7f, .7f, 1f};
private float[] specularlight1 = {0.6f, 0.6f, 0.6f, 1f};
private float[] lightposition1 = {20.0f,-40.0f,100.0f,1f};
private FloatBuffer lightPositionBuffer1 =
GraphicsUtil.makeFloatBuffer(lightposition1);
private FloatBuffer specularLightBuffer1 =
GraphicsUtil.makeFloatBuffer(specularlight1);
private FloatBuffer diffuseLightBuffer1 =
GraphicsUtil.makeFloatBuffer(diffuselight1);
private FloatBuffer ambientLightBuffer1 =
GraphicsUtil.makeFloatBuffer(ambientlight1);
public final void draw(GL10 gl) {
}
public final void setupEnv(GL10 gl) {
gl.glEnable(GL10.GL_LIGHTING);
gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_AMBIENT,
ambientLightBuffer1);
gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_DIFFUSE,
diffuseLightBuffer1);
gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_SPECULAR,
specularLightBuffer1);
gl.glLightfv(GL10.GL_LIGHT1, GL10.GL_POSITION,
lightPositionBuffer1);
gl.glEnable(GL10.GL_LIGHT1);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisable(GL10.GL_TEXTURE_2D);
initGL(gl);
}
public final void initGL(GL10 gl) {
gl.glDisable(GL10.GL_COLOR_MATERIAL);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glShadeModel(GL10.GL_SMOOTH);
gl.glDisable(GL10.GL_COLOR_MATERIAL);
gl.glEnable(GL10.GL_LIGHTING);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glEnable(GL10.GL_NORMALIZE);
}
}`
在变量声明中,我们指定不同类型的照明,并从中创建FloatBuffers
。在我们显示任何一个框之前,setupEnv()
被调用。它设置了灯光和其他特定于 OpenGL 的东西。创建表面时会调用一次initGL()
方法。
雄胺固定
最后,AndroidManifest.xml
需要更新。
清单 3-14。 更新 AndroidManifest.xml
`
<activity
android:label="@string/app_name"
android:name=".Activity"
android:clearTaskOnLaunch="true"
android:screenOrientation="landscape"
android:noHistory="true">
这让我们结束了这个应用的编码。如果您还没有下载本章的源代码,请下载并使用。patt 文件,并将它们放在您的项目/资产目录中。除了源代码,您还会找到一个名为“标记”的文件夹,其中包含本应用和本书后续部分中使用的标记。你可以打印出来供自己使用。
总结
在这一章中,我们学习了如何在我们的应用中覆盖标准的 Android 小部件,以及如何使用标记来使我们的增强现实应用更具交互性。关于这一章还讨论了 AndAR,一个适用于 Android 的开源 AR 工具包,它允许我们轻松快速地实现许多 AR 功能。
下一章讨论人工地平线,这是任何军事或导航应用的核心 AR 功能。
四、人工地平线
人工地平线被牛津英语词典定义为“一种回转仪或流体表面,通常是水银,用于在自然地平线被遮挡时为飞机飞行员提供水平参考平面以进行导航测量。”早在增强现实(AR)出现之前,人工地平线就已经用于导航目的,导航仍然是它们的主要用途。当平视显示器在飞机上,特别是军用飞机上大量使用时,它们变得突出起来。
人工地平线基本上是一条水平参考线,供导航员在自然地平线被遮挡时使用。对于我们所有痴迷于在应用中使用 AR 的人来说,这是一个需要熟悉的重要功能。在制作导航应用甚至游戏时,它会非常有用。
可能很难理解实际上并不存在的视界的概念,但必须用来进行各种计算,这些计算可能会以多种方式影响用户。为了解决这个问题,我们将制作一个小的示例应用,它不实现 AR,但向您显示什么是人工地平线以及它是如何实现的。之后,我们将制作一个 AR 应用来使用人工地平线。
至非空气演示应用
在这个应用中,我们将有一个内置人工地平线指示器的指南针。我将只为人工地平线代码提供一个解释,因为它的其余部分不属于本书的主题。
XML
让我们先把这些小的 XML 文件去掉。我们将需要一个/res/layout/main.xml
、一个/res/values/strings.xml
和一个/res/values/colors.xml
。
让我们从main.xml
文件开始:
清单 4-1。 main.xml
<?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"> <com.paar.ch4nonardemo.HorizonView android:id="@+id/horizonView" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
这里没什么特别的。我们只需将Activity
的视图设置为自定义视图,我们将在几分钟内着手制作。
现在让我们来看看strings.xml
:
清单 4-2。 strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Pro Android AR 4 Non AR Demo</string> <string name="cardinal_north">N</string> <string name="cardinal_east">E</string> <string name="cardinal_south">S</string> <string name="cardinal_west">W</string> </resources>
这个文件声明了四个枢机主教的字符串资源:N 对应北,E 对应东,S 对应南,W 对应西。
让我们继续讨论colours.xml
:
清单 4-3。 colours.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="text_color">#FFFF</color> <color name="background_color">#F000</color> <color name="marker_color">#FFFF</color>
`
`
所有颜色在ARGB
或AARRGGBB
中指定。它们用于为我们的小演示应用增加一点视觉吸引力。“到”和“从”的颜色略有不同,因此我们可以在最终的演示中使用渐变。天空的颜色是蓝色,地面的颜色是橙色。
爪哇
现在我们将创建我们在 main.xml 中提到的自定义视图。
创建视图
在主包中创建一个名为HorizonView.java
的 Java 文件(我这边是 com.paar.ch4nonardemo)。向其中添加以下全局变量:
清单 4-4。HorizonView.java全局变量
`public class HorizonView extends View {
private enum CompassDirection { N, NNE, NE, ENE,
E, ESE, SE, SSE,
S, SSW, SW, WSW,
W, WNW, NW, NNW }
int[] borderGradientColors;
float[] borderGradientPositions;
int[] glassGradientColors;
float[] glassGradientPositions;
int skyHorizonColorFrom;
int skyHorizonColorTo;
int groundHorizonColorFrom;
int groundHorizonColorTo;
private Paint markerPaint;
private Paint textPaint;
private Paint circlePaint;
private int textHeight;
private float bearing;
float pitch = 0;
float roll = 0;`
变量的名字是对它们任务的合理的描述。CompassDirections
提供我们将用来创建 16 点罗盘的弦。名称中带有渐变、颜色、颜料的用于绘制View
,textHeight
也是如此。
获取和设置轴承、俯仰和侧倾
现在将以下方法添加到该类中:
清单 4-5。 方位、俯仰和滚转方式
`public void setBearing(float _bearing) {
bearing = _bearing;
}
public float getBearing() {
return bearing;
}
public float getPitch() {
return pitch;
}
public void setPitch(float pitch) {
this.pitch = pitch;
}
public float getRoll() {
return roll;
}
public void setRoll(float roll) {
this.roll = roll;
}`
这些方法让我们获得并设置方位角、俯仰角和滚动角,这在以后被规范化并用于绘制我们的视图。
调用并初始化指南针
接下来,将以下三个构造函数添加到该类中:
清单 4-6。??【地平线】建造者
`public HorizonView(Context context) {
super(context);
initCompassView();
}
public HorizonView(Context context, AttributeSet attrs) {
super(context, attrs);
initCompassView();
}
public HorizonView(Context context,
AttributeSet ats,
int defaultStyle) {
super(context, ats, defaultStyle);
initCompassView();
}`
所有三个构造函数最终都调用了initCompassView()
,它完成了这个类中的主要工作。
说到initCompassView()
,下面是它的代码:
清单 4-7。 initCompassView()
`protected void initCompassView() {
setFocusable(true);
Resources r = this.getResources();
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setColor(R.color.background_color);
circlePaint.setStrokeWidth(1);
circlePaint.setStyle(Paint.Style.STROKE);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(r.getColor(R.color.text_color));
textPaint.setFakeBoldText(true);
textPaint.setSubpixelText(true);
textPaint.setTextAlign(Align.LEFT);
textHeight = (int)textPaint.measureText("yY");
markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
markerPaint.setColor(r.getColor(R.color.marker_color));
markerPaint.setAlpha(200);
markerPaint.setStrokeWidth(1);
markerPaint.setStyle(Paint.Style.STROKE);
markerPaint.setShadowLayer(2, 1, 1, r.getColor(R.color.shadow_color));
borderGradientColors = new int[4];
borderGradientPositions = new float[4];
borderGradientColors[3] = r.getColor(R.color.outer_border);
borderGradientColors[2] = r.getColor(R.color.inner_border_one);
borderGradientColors[1] = r.getColor(R.color.inner_border_two);
borderGradientColors[0] = r.getColor(R.color.inner_border);
borderGradientPositions[3] = 0.0f;
borderGradientPositions[2] = 1-0.03f;
borderGradientPositions[1] = 1-0.06f;
borderGradientPositions[0] = 1.0f;
glassGradientColors = new int[5];
glassGradientPositions = new float[5];
int glassColor = 245;
glassGradientColors[4] = Color.argb(65, glassColor,
glassColor, glassColor);
glassGradientColors[3] = Color.argb(100, glassColor,
glassColor, glassColor);
glassGradientColors[2] = Color.argb(50, glassColor,
glassColor, glassColor);
glassGradientColors[1] = Color.argb(0, glassColor,
glassColor, glassColor);
glassGradientColors[0] = Color.argb(0, glassColor,
glassColor, glassColor);
glassGradientPositions[4] = 1-0.0f;
glassGradientPositions[3] = 1-0.06f;
glassGradientPositions[2] = 1-0.10f;
glassGradientPositions[1] = 1-0.20f;
glassGradientPositions[0] = 1-1.0f;
skyHorizonColorFrom = r.getColor(R.color.horizon_sky_from);
skyHorizonColorTo = r.getColor(R.color.horizon_sky_to);
groundHorizonColorFrom = r.getColor(R.color.horizon_ground_from);
groundHorizonColorTo = r.getColor(R.color.horizon_ground_to);
}`
在这种方法中,我们使用颜色来形成合适的渐变。我们也给一些我们在开始时声明的变量赋值。
计算指南针的大小
现在向该类添加以下两个方法:
清单 4-8。 onMeasure()和 Measure()
`@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measuredWidth = measure(widthMeasureSpec);
int measuredHeight = measure(heightMeasureSpec);
int d = Math.min(measuredWidth, measuredHeight);
setMeasuredDimension(d, d);
}
private int measure(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.UNSPECIFIED) {
result = 200;
} else {
result = specSize;
}
return result;
}`
这两种方法允许我们测量屏幕,并让我们决定我们希望我们的指南针有多大。
画指南针
现在,最后将onDraw()
方法添加到类中:
清单 4-9。??【onDraw()】
`@Override
protected void onDraw(Canvas canvas) {
float ringWidth = textHeight + 4;
int height = getMeasuredHeight();
int width =getMeasuredWidth();
int px = width/2;
int py = height/2;
Point center = new Point(px, py);
int radius = Math.min(px, py)-2;
RectF boundingBox = new RectF(center.x - radius,
center.y - radius,
center.x + radius,
center.y + radius);
RectF innerBoundingBox = new RectF(center.x - radius + ringWidth,
center.y - radius + ringWidth,
center.x + radius - ringWidth,
center.y + radius - ringWidth);
float innerRadius = innerBoundingBox.height()/2;
RadialGradient borderGradient = new RadialGradient(px, py, radius,
borderGradientColors, borderGradientPositions, TileMode.CLAMP);
Paint pgb = new Paint();
pgb.setShader(borderGradient);
Path outerRingPath = new Path();
outerRingPath.addOval(boundingBox, Direction.CW);
canvas.drawPath(outerRingPath, pgb);
LinearGradient skyShader = new LinearGradient(center.x,
innerBoundingBox.top, center.x, innerBoundingBox.bottom,
skyHorizonColorFrom, skyHorizonColorTo, TileMode.CLAMP);
Paint skyPaint = new Paint();
skyPaint.setShader(skyShader);
LinearGradient groundShader = new LinearGradient(center.x,
innerBoundingBox.top, center.x, innerBoundingBox.bottom,
groundHorizonColorFrom, groundHorizonColorTo, TileMode.CLAMP);
Paint groundPaint = new Paint();
groundPaint.setShader(groundShader);
float tiltDegree = pitch;
while (tiltDegree > 90 || tiltDegree < -90) {
if (tiltDegree > 90) tiltDegree = -90 + (tiltDegree - 90);
if (tiltDegree < -90) tiltDegree = 90 - (tiltDegree + 90);
}
float rollDegree = roll;
while (rollDegree > 180 || rollDegree < -180) {
if (rollDegree > 180) rollDegree = -180 + (rollDegree - 180);
if (rollDegree < -180) rollDegree = 180 - (rollDegree + 180);
}
Path skyPath = new Path();
skyPath.addArc(innerBoundingBox, -tiltDegree, (180 + (2 * tiltDegree)));
canvas.rotate(-rollDegree, px, py);
canvas.drawOval(innerBoundingBox, groundPaint);
canvas.drawPath(skyPath, skyPaint);
canvas.drawPath(skyPath, markerPaint);
int markWidth = radius / 3;
int startX = center.x - markWidth;
int endX = center.x + markWidth;
double h = innerRadius*Math.cos(Math.toRadians(90-tiltDegree));
double justTiltY = center.y - h;
float pxPerDegree = (innerBoundingBox.height()/2)/45f;
for (int i = 90; i >= -90; i -= 10) {
double ypos = justTiltY + i*pxPerDegree;
if ((ypos < (innerBoundingBox.top + textHeight)) ||
(ypos > innerBoundingBox.bottom - textHeight))
continue;
canvas.drawLine(startX, (float)ypos,
endX, (float)ypos,
markerPaint);
int displayPos = (int)(tiltDegree - i);
String displayString = String.valueOf(displayPos);
float stringSizeWidth = textPaint.measureText(displayString);
canvas.drawText(displayString,
(int)(center.x-stringSizeWidth/2),
(int)(ypos)+1,
textPaint);
}
markerPaint.setStrokeWidth(2);
canvas.drawLine(center.x - radius / 2,
(float)justTiltY,
center.x + radius / 2,
(float)justTiltY,
markerPaint);
markerPaint.setStrokeWidth(1);
Path rollArrow = new Path();
rollArrow.moveTo(center.x - 3, (int)innerBoundingBox.top + 14);
rollArrow.lineTo(center.x, (int)innerBoundingBox.top + 10);
rollArrow.moveTo(center.x + 3, innerBoundingBox.top + 14);
rollArrow.lineTo(center.x, innerBoundingBox.top + 10);
canvas.drawPath(rollArrow, markerPaint);
String rollText = String.valueOf(rollDegree);
double rollTextWidth = textPaint.measureText(rollText);
canvas.drawText(rollText,
(float)(center.x - rollTextWidth / 2),
innerBoundingBox.top + textHeight + 2,
textPaint);
canvas.restore();
canvas.save();
canvas.rotate(180, center.x, center.y);
for (int i = -180; i < 180; i += 10) {
if (i % 30 == 0) {
String rollString = String.valueOf(i*-1);
float rollStringWidth = textPaint.measureText(rollString);
PointF rollStringCenter = new PointF(center.x-rollStringWidth /2,
innerBoundingBox.top+1+textHeight);
canvas.drawText(rollString,
rollStringCenter.x, rollStringCenter.y,
textPaint);
}
else {
canvas.drawLine(center.x, (int)innerBoundingBox.top,
center.x, (int)innerBoundingBox.top + 5,
markerPaint);
}
canvas.rotate(10, center.x, center.y);
}
canvas.restore();
canvas.save();
canvas.rotate(-1*(bearing), px, py);
double increment = 22.5;
for (double i = 0; i < 360; i += increment) {
CompassDirection cd = CompassDirection.values()
[(int)(i / 22.5)];
String headString = cd.toString();
float headStringWidth = textPaint.measureText(headString);
PointF headStringCenter = new PointF(center.x - headStringWidth / 2,
boundingBox.top + 1 + textHeight);
if (i % increment == 0)
canvas.drawText(headString,
headStringCenter.x, headStringCenter.y,
textPaint);
else
canvas.drawLine(center.x, (int)boundingBox.top,
center.x, (int)boundingBox.top + 3,
markerPaint);
canvas.rotate((int)increment, center.x, center.y);
}
canvas.restore();
RadialGradient glassShader = new RadialGradient(px, py, (int)innerRadius,
glassGradientColors, glassGradientPositions, TileMode.CLAMP);
Paint glassPaint = new Paint();
glassPaint.setShader(glassShader);
canvas.drawOval(innerBoundingBox, glassPaint);
canvas.drawOval(boundingBox, circlePaint);
circlePaint.setStrokeWidth(2);
canvas.drawOval(innerBoundingBox, circlePaint);
canvas.restore();
}
}`
onDraw()
方法绘制外圆,固定俯仰和横滚值,给圆着色,负责给圆添加指南针方向,在需要时旋转圆,并绘制实际的人工水平线并移动它们。
简而言之,我们用 N、NE 等标记以 30 度的间隔创建一个圆。在指南针内部,我们有一个类似高度计的视图,它给出了地平线相对于手机握持方式的位置。
更新活动
我们需要更新我们主要活动的整个展示。为此,我们需要更新AHActivity.java
:
清单 4-10。AHActivity.java??
`public class AHActivity extends Activity {
float[] aValues = new float[3];
float[] mValues = new float[3];
HorizonView horizonView;
SensorManager sensorManager;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
horizonView = (HorizonView)this.findViewById(R.id.horizonView);
sensorManager =
(SensorManager)getSystemService(Context.SENSOR_SERVICE);
updateOrientation(new float[] {0, 0, 0});
}
private void updateOrientation(float[] values) {
if (horizonView!= null) {
horizonView.setBearing(values[0]);
horizonView.setPitch(values[1]);
horizonView.setRoll(-values[2]);
horizonView.invalidate();
}
}
private float[] calculateOrientation() {
float[] values = new float[3];
float[] R = new float[9];
float[] outR = new float[9];
SensorManager.getRotationMatrix(R, null, aValues, mValues);
SensorManager.remapCoordinateSystem(R,
SensorManager.AXIS_X,
SensorManager.AXIS_Z,
outR);
SensorManager.getOrientation(outR, values);
values[0] = (float) Math.toDegrees(values[0]);
values[1] = (float) Math.toDegrees(values[1]);
values[2] = (float) Math.toDegrees(values[2]);
return values;
}
private final SensorEventListener sensorEventListener = new
SensorEventListener() {
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER)
aValues = event.values;
if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
mValues = event.values;
updateOrientation(calculateOrientation());
}
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
@Override
protected void onResume() {
super.onResume();
Sensor accelerometer =
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Sensor magField =
sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
sensorManager.registerListener(sensorEventListener,
accelerometer,
SensorManager.SENSOR_DELAY_FASTEST);
sensorManager.registerListener(sensorEventListener,
magField,
SensorManager.SENSOR_DELAY_FASTEST);
}
@Override
protected void onStop() {
sensorManager.unregisterListener(sensorEventListener);
super.onStop();
}
}`
这是实际工作发生的地方。在onCreate()
方法中,我们将视图设置为main.xml
,获取对horizonView
的引用,注册一个SensorEventListener
,并将方向更新为理想情况。updateOrientation()
方法负责向我们的视图传递新的值,以便它可以适当地改变。calculateOrientation()
使用 SDK 提供的一些方法,根据传感器提供的原始值精确计算方向。Android 提供的这些方法为我们处理了很多复杂的数学问题。你应该很容易理解SensorEventListener
、onResume()
和onStop()
。他们做着与前几章相同的工作。
安卓清单
最后,您应该将您的 AndroidManifest 更新为以下内容:
清单 4-11。 AndroidManifest.xml
`
<activity
android:label="@string/app_name"
android:name=".AHActivity"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >
`
测试完成的应用
如果你现在运行这个应用,你会对人工地平线有一个很好的了解。图 4-1 和 4-2 让你对完成的应用有个概念。
图 4-1。 设备直立时的 app
图 4-2。 设备倒置时的 app
在空中演示应用
在浏览并运行了前面的例子之后,您现在应该很好地理解了人工视界的概念。我们现在将设计一款应用,它具有以下功能:
- 显示实时摄像机预览
- 在相机预览上显示半透明版本的
HorizonView
,颜色类似于飞机中的电影版本 - 告诉你 5 分钟后你的高度,假设你继续保持当前的倾斜度
在我们开始编码之前,有一些事情需要记住。由于用户几乎不可能保持设备完全静止,因此倾斜度将不断变化,这将导致 5 分钟内的高度也发生变化。为了解决这个问题,我们将添加一个按钮,允许用户随时更新高度。
注意:在一些设备上,这款应用可以顺时针和逆时针方向移动人工地平线,而不是像非 AR 演示中那样上下移动。所有值都是正确的,除了显示器有问题,可以通过在纵向模式下运行应用来修复。
设置项目
首先,创建一个新项目。作为例子使用的包名为 com.paar.ch4ardemo,目标是 Android 2.1。和往常一样,你可以把名字改成你喜欢的任何名字;只需确保更新示例代码中的所有引用。图 4-3 中的截图显示了项目详情。
图 4-3。?? 申请详情
创建新项目后,将非增强现实演示中的所有内容复制到这个项目中。我们将建立在以前的项目。确保在需要的地方更新文件中的包名。
更新 XML
首先,我们需要更新应用的 XML。我们的应用目前只有四个 XML 文件:AndroidManifest.xml
、main.xml
、colours.xml
和strings.xml
。我们将只编辑从前面的例子中复制过来的,而不是从头开始构建新的。更新的和新的行以粗体显示。
更新 AndroidManifest.xml 以访问 GPS
先从AndroidManifest.xml
说起吧。因为我们更新的应用将需要用户的高度,我们将需要使用 GPS 来获取它。GPS 要求在清单中声明ACCESS_FINE_LOCATION
许可。除了新的权限之外,我们必须更新包名,并将Activity
的方向改为横向。
清单 4-12。 更新 AndroidManifest.xml
`
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.paar.ch4ardemo"
android:versionCode="1"
android:versionName="1.0" >
<activity
android:label="@string/app_name"
android:name=".AHActivity"
android:screenOrientation="landscape"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:configChanges = "keyboardHidden|orientation">
更新 strings.xml 以显示海拔高度
接下来,我们来看看strings.xml
。我们将添加两个新的字符串,作为Button
和TextView
的标签。我们没有为另一个TextView
的添加字符串,因为当用户点击按钮时,它将在运行时更新。在您的strings.xml
文件中的任意位置添加以下两个字符串。
清单 4-13。 更新 strings.xml
<string name="altitudeButtonLabel">Update Altitude</string> <string name="altitudeLabel">Altitude in \n 5 minutes</string>
第二个字符串中的那个小\n
告诉 Android 在新的一行上打印字符串的剩余部分。我们这样做是因为在较小的屏幕设备上,字符串可能会与按钮重叠。
更新 colours.xml 以提供双色显示
现在来更新一下colours.xml
。这一次,我们只需要两种颜色,其中只有一种是可见的颜色。在前面的例子中,我们为地面、天空等设置了不同的颜色。在这里这样做将导致仪表的表盘覆盖我们的相机预览。然而,使用 ARGB 色码,我们可以使除了文本以外的一切都透明。用下面的代码完全替换您的colours.xml
文件的内容。
清单 4-14。 更新 colours.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="text_color">#F0F0</color> <color name="transparent_color">#0000</color> </resources>
将 main.xml 更新为 RelativeLayout
现在我们来看最后一个 XML 文件——也是变化最大的一个文件:main.xml
。以前,main.xml
只有一个LinearLayout
,里面还有一个HorizonView
。然而,考虑到我们的 AR 重叠,我们将用一个RelativeLayout
替换LinearLayout
,并在HorizonView
之外添加两个TextView
和一个Button
。将main.xml
更新为以下代码。
清单 4-15。 更新 main.xml
<?xml version="1.0" encoding="utf-8"?> ***<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"*** ***android:orientation="vertical"*** ***android:layout_width="fill_parent"*** ***android:layout_height="fill_parent">***
***<SurfaceView*** ***android:id="@+id/cameraPreview"*** ***android:layout_width="fill_parent"*** ***android:layout_height="fill_parent" />*** ***<com.paar.ch4ardemo.HorizonView*** android:id="@+id/horizonView" android:layout_width="fill_parent" android:layout_height="fill_parent" /> ***<TextView*** ***android:id="@+id/altitudeLabel"*** ***android:layout_width="wrap_content"*** ***android:layout_height="wrap_content"*** ***android:layout_centerVertical="true"*** ***android:layout_toRightOf="@id/horizonView"*** ***android:text="@string/altitudeLabel"*** ***android:textColor="#00FF00">*** ***</TextView>*** ***<TextView*** ***android:id="@+id/altitudeValue"*** ***android:layout_width="wrap_content"*** ***android:layout_height="wrap_content"*** ***android:layout_centerVertical="true"*** ***android:layout_below="@id/altitudeLabel"*** ***android:layout_toRightOf="@id/horizonView"*** ***android:textColor="#00FF00">*** ***</TextView>*** ***<Button*** ***android:id="@+id/altitudeUpdateButton"*** ***android:layout_width="wrap_content"*** ***android:layout_height="wrap_content"*** ***android:text="@string/altitudeButtonLabel"*** ***android:layout_centerVertical="true"*** ***android:layout_alignParentRight="true">*** ***</Button>*** ***</RelativeLayout>***
在这种情况下,只有五行没有被修改。像往常一样,作为一个RelativeLayout
,在 id 或位置上的任何错误都是致命的。
这负责我们应用的 XML 部分。现在我们必须转移到 Java 文件上。
更新 Java 文件
Java 文件比 XML 文件有更多的变化,有些变化一开始可能没有意义。我们将接受每个变更,一次一个代码块。
更新 HorizonView.java 以使指南针透明
先从HorizonView.java
说起吧。我们正在修改我们的代码,使表盘透明,并在风景模式下工作。先从修改initCompassView()
开始。我们所做的唯一改变是用更新的颜色替换旧的颜色。已修改的行以粗体显示。
清单 4-16。 更新了 initCompassView()
`protected void initCompassView() {
setFocusable(true);
Resources r = this.getResources();
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setColor(R.color.transparent_color);
circlePaint.setStrokeWidth(1);
circlePaint.setStyle(Paint.Style.STROKE);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(r.getColor(R.color.text_color));
textPaint.setFakeBoldText(true);
textPaint.setSubpixelText(true);
textPaint.setTextAlign(Align.LEFT);
textHeight = (int)textPaint.measureText("yY");
markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
markerPaint.setColor(r.getColor(R.color.transparent_color));
markerPaint.setAlpha(200);
markerPaint.setStrokeWidth(1);
markerPaint.setStyle(Paint.Style.STROKE);
markerPaint.setShadowLayer(2, 1, 1, r.getColor(R.color.transparent_color));
borderGradientColors = new int[4];
borderGradientPositions = new float[4];
borderGradientColors[3] = r.getColor(R.color.transparent_color);
borderGradientColors[2] = r.getColor(R.color.transparent_color);
borderGradientColors[1] = r.getColor(R.color.transparent_color);
borderGradientColors[0] = r.getColor(R.color.transparent_color);
borderGradientPositions[3] = 0.0f;
borderGradientPositions[2] = 1-0.03f;
borderGradientPositions[1] = 1-0.06f;
borderGradientPositions[0] = 1.0f;
glassGradientColors = new int[5];
glassGradientPositions = new float[5];
int glassColor = 245;
glassGradientColors[4] = Color.argb(65, glassColor,
glassColor, glassColor);
glassGradientColors[3] = Color.argb(100, glassColor,
glassColor, glassColor);
glassGradientColors[2] = Color.argb(50, glassColor,
glassColor, glassColor);
glassGradientColors[1] = Color.argb(0, glassColor,
glassColor, glassColor);
glassGradientColors[0] = Color.argb(0, glassColor,
glassColor, glassColor);
glassGradientPositions[4] = 1-0.0f;
glassGradientPositions[3] = 1-0.06f;
glassGradientPositions[2] = 1-0.10f;
glassGradientPositions[1] = 1-0.20f;
glassGradientPositions[0] = 1-1.0f;
skyHorizonColorFrom = r.getColor(R.color.transparent_color);
skyHorizonColorTo = r.getColor(R.color.transparent_color);
groundHorizonColorFrom = r.getColor(R.color.transparent_color);
groundHorizonColorTo = r.getColor(R.color.transparent_color);
}`
接下来,我们需要更新onDraw()
方法来处理横向。因为第一部分的大部分内容没有改变,所以这里没有给出整个方法。我们在俯仰和滚转夹紧后立即更新代码。
清单 4-17。 更新了 onDraw()
//Cut Here Path skyPath = new Path(); ***skyPath.addArc(innerBoundingBox, -rollDegree, (180 + (2 * rollDegree)));*** ***canvas.rotate(-tiltDegree, px, py);*** canvas.drawOval(innerBoundingBox, groundPaint); canvas.drawPath(skyPath, skyPaint); canvas.drawPath(skyPath, markerPaint); int markWidth = radius / 3; int startX = center.x - markWidth; int endX = center.x + markWidth;
`Log.d("PAARV ", "Roll " + String.valueOf(rollDegree));
Log.d("PAARV ", "Pitch " + String.valueOf(tiltDegree));
double h = innerRadius*Math.cos(Math.toRadians(90-tiltDegree));
double justTiltX = center.x - h;
float pxPerDegree = (innerBoundingBox.height()/2)/45f;
for (int i = 90; i >= -90; i -= 10) {
double ypos = justTiltX + ipxPerDegree;*
if ((ypos < (innerBoundingBox.top + textHeight)) ||
(ypos > innerBoundingBox.bottom - textHeight))
continue;
canvas.drawLine(startX, (float)ypos,
endX, (float)ypos,
markerPaint);
int displayPos = (int)(tiltDegree - i);
String displayString = String.valueOf(displayPos);
float stringSizeWidth = textPaint.measureText(displayString);
canvas.drawText(displayString,
(int)(center.x-stringSizeWidth/2),
(int)(ypos)+1,
textPaint);
}
markerPaint.setStrokeWidth(2);
canvas.drawLine(center.x - radius / 2,
(float)justTiltX,
center.x + radius / 2,
(float)justTiltX,
markerPaint);
markerPaint.setStrokeWidth(1);
//Cut Here`
这些变化使我们的应用看起来漂亮而透明。
更新活动以访问 GPS,并找到和显示高度
现在,我们必须进入最后一个文件AHActivity.java
。在这个文件中,我们将添加 GPS 代码、TextView
和Button
参考,稍微修改我们的传感器代码,最后放入一个小算法来计算我们 5 分钟后的高度。我们将使用三角学来寻找高度的变化,所以如果你的有点生疏,你可能想快速温习一下。
首先,将以下变量添加到类的顶部。
清单 4-18。 新变量声明
`LocationManager locationManager;
Button updateAltitudeButton;
TextView altitudeValue;
double currentAltitude;
double pitch;
double newAltitude;
double changeInAltitude;
double thetaSin;`
将会是我们的区域经理。updateAltitudeButton
和altitudeValue
将保存对它们的 XML 对应物的引用,这样我们可以监听点击并更新它们。currentAltitude
、newAltitude
和changeInAltitude
都将用于在我们的算法运行期间存储值。pitch
变量将存储螺距,而thetaSin
将存储螺距角的正弦值。
我们现在将更新我们的onCreate()
方法以从 Android 获取位置服务,设置位置监听器,并为按钮设置OnClickListener
。将其更新为以下代码。
清单 4-19。 更新 onCreate()
`@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
inPreview = false;
cameraPreview = (SurfaceView)findViewById(R.id.cameraPreview);
previewHolder = cameraPreview.getHolder();
previewHolder.addCallback(surfaceCallback);
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
altitudeValue = (TextView) findViewById(R.id.altitudeValue);
updateAltitudeButton = (Button) findViewById(R.id.altitudeUpdateButton);
updateAltitudeButton.setOnClickListener(new OnClickListener() {
public void onClick(View arg0) {
updateAltitude();
}
});
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2,
locationListener);
horizonView = (HorizonView)this.findViewById(R.id.horizonView);
sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
updateOrientation(new float[] {0, 0, 0});
}`
现在,Eclipse 应该会告诉您updateAltitude()
方法和locationListener
不存在。我们将通过创建它们来解决这个问题。将下面的LocationListener
添加到类的任何部分,方法之外。如果您想知道为什么我们有三个未使用的方法,那是因为一个LocationListener
必须实现所有四个方法,即使它们没有被使用。移除它们会在编译时引发错误。
清单 4-20。 位置监听器
`LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
currentAltitude = location.getAltitude();
}
public void onProviderDisabled(String arg0) {
//Not Used
}
public void onProviderEnabled(String arg0) {
//Not Used
}
public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
//Not Used
}
};`
在我们继续讨论updateAltitude()
方法之前,我们将快速地在calculateOrientation()
方法中添加一行,这样 pitch 变量就不会为空。在 return 语句之前添加以下内容。
清单 4-21。 确保 calculateOrientation()中的俯仰变量不为空
pitch = values[1];
计算海拔高度
现在我们的音高有了一个值,让我们转到updateAltitude()
方法。这种方法实现了一种算法,可以在 5 分钟后找到一个人的高度,将当前的高度作为他们向上移动的角度。我们把行走速度取为 4.5 英尺/秒,这是一个成年人的平均速度。利用速度和时间,我们可以计算出 5 分钟内走过的距离。然后利用三角学,我们可以从行进的距离和倾斜的角度找出高度的变化。然后,我们将海拔高度的变化添加到旧的海拔高度,以获得更新的海拔高度,并在TextView
中显示出来。如果俯仰或当前高度为零,应用会要求用户再试一次。参见图 4-4 的概念图解。
图 4-4。 算法的图形表示
以下是updateAltitude()
的代码:
清单 4-22。 计算并显示高度
`public void updateAltitude() {
int time = 300;
float speed = 4.5f;
double distanceMoved = (speedtime)0.3048;
if(pitch != 0 && currentAltitude != 0)
{
thetaSin = Math.sin(pitch);
changeInAltitude = thetaSin * distanceMoved;
newAltitude = currentAltitude + changeInAltitude;
altitudeValue.setText(String.valueOf(newAltitude));
}
else
{
altitudeValue.setText("Try Again");
}
}`
至此,我们已经完成了示例应用的 AR 版本。
测试完成的增强现实应用
看看图 4-5 和图 4-6 中的截图,看看这个应用是如何工作的。
图 4-5。 应用运行,向用户显示重试消息
图 4-6。 用户这次也看到了高度
总结
本章探讨了人工视界的概念以及如何利用它们创建应用。我们设计了一个算法来发现海拔高度的变化,并在一个示例应用中实现了它。这里给出的应用只是你可以用人工地平线做什么的一个例子。它们广泛应用于军事,尤其是空军;而在城市中,自然地平线因高度而扭曲或被建筑物遮挡。
五、常见和不常见的错误和问题
本章处理你在编写本书或其他增强现实(AR)应用中的示例应用时可能遇到的错误和问题。我们将看看与布局、相机、清单和地图相关的错误。我们也有一个不属于这些类别的部分。
布局错误
首先,我们来看看布局中出现的错误。有许多这样的错误,但我们将只关注那些容易在 AR 应用中出现的错误。
用户界面对齐问题
大多数 AR 应用在基本布局文件中使用相对布局。然后,RelativeLayout 将所有小部件、表面视图和定制视图作为其子视图。这是首选的布局,因为它很容易让我们将 UI 元素一个接一个地叠加起来。
使用 RelativeLayout 时面临的一个最常见的问题是,布局最终看起来不像预期的那样。元素最终遍布整个位置,而不是按照你放置它们的顺序。最常见的原因是缺少 ID 或者没有定义某些布局规则。例如,如果你在文本视图的定义中漏掉了一个android:layout_*
,文本视图没有设置布局选项,因为它不知道屏幕上的位置,所以布局最终看起来很混乱。此外,在对齐中使用上述 TextView 的任何其他小部件也将最终出现在屏幕上的随机位置。
解决方法很简单。您只需要检查所有的对齐值,并修复任何拼写错误,或者添加任何缺失的值。一个简单的方法是使用 Eclipse 中内置的图形编辑器,或者从[
code.google.com/p/droiddraw/](http://code.google.com/p/droiddraw/)
获得的开源程序 DroidDraw。在这两种情况下,在图形布局中移动一个元素都会改变其对应的 XML 代码。它允许您轻松地检查和更正布局。
类种姓例外
使用布局时经常遇到的另一个问题是,当试图从 Java 代码中引用特定的小部件时,会出现 ClassCastException。假设我们有一个如下声明的 TextView:
清单 5-1。 一个例子 TextView
<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="@string/hello" />
在 XML 中定义了它之后,我们从 Java 代码中引用它,如下所示:
清单 5-2。 引用文本视图
TextView textView = (TextView) findViewById(R.id.textView1);
编译时,我们有时会在前面的代码中得到一个 ClassCastException。在我们的 XML 文件发生大的变化后,通常会收到这个错误。当我们引用 TextView 时,我们使用 View 类中的方法findViewById()
来获取与作为参数传递的 ID 相对应的视图。然后我们将由findViewById()
返回的视图转换成一个文本视图。应用的所有R.*.*
部分都存储在编译应用时生成的 R.java 文件中。有时这个文件没有正确更新,并且 findViewById()
返回一个不是我们正在寻找的视图(例如,一个按钮而不是一个文本视图)。然后,当我们试图将不正确的视图转换为 TextView 时,我们会得到一个 ClassCastException,因为您不能将这两个视图相互转换。
解决这个问题很简单。您只需在 Eclipse 中进入项目 清理或者从命令行运行
ant clean
来清理项目。
这个错误的另一个原因是你实际上引用了一个不正确的视图,比如一个按钮而不是一个文本视图。要解决这个问题,您需要仔细检查您的 Java 代码,并确保您的 id 是正确的。
相机误差
相机是任何 AR 应用不可或缺的一部分。如果你仔细想想,它增加了大部分的真实性。相机也是一个稍微不稳定的实现在一些设备上工作良好,但在其他设备上完全失败的部分。我们将查看最常见的 Java 错误,并在 AndroidManifest 部分处理单个清单错误。
无法连接到相机服务
在 Android 上使用相机时,最常见的错误可能是无法连接到相机服务。当您试图访问正在被应用(您自己的或其他)使用或已被一个或多个设备管理员完全禁用的摄像机时,会出现此错误。设备管理员是可以在运行 Froyo 和更高版本的设备中更改最小密码长度和相机使用等内容的应用。您可以通过使用 android.app.admin 中的DeviceManagerPolicy.getCameraDisabled()
来检查管理员是否禁用了摄像头。您可以在检查时传递管理员的名称,或者传递 null 来检查所有管理员。
如果另一个应用正在使用相机,您将无能为力,但是您可以通过正确释放相机对象来确保您的应用不是导致问题的那个。这个的主代码一般在onPause()
和surfaceDestroyed()
里。您可以在应用中使用其中一种或两种。整本书,我们都在onPause()
发行。两者的代码如下所示:
清单 5-3。 释放相机
@Override public void surfaceDestroyed(SurfaceHolder holder) {
`if (mCam != null) {
mCam.stopPreview();
mCam.setPreviewCallback(null);
mCam.release();
mCam = null;
}
}
@Override
public void onPause() {
super.onPause();
if (mCam != null) {
mCam.stopPreview();
mCam.setPreviewCallback(null);
mCam.release();
mCam = null;
}
}`
在前面的代码中,mCam
是相机对象。这两种方法中都可能有额外的代码,这取决于您的应用的用途。
Camera.setParameters()失败
最常见的错误之一就是setParameters()
的失败。出现这种情况有几个原因,但在大多数情况下,这是因为为预览提供了不正确的宽度或高度。
解决这个问题非常简单。你需要确保你传给 Android 的预览尺寸是受支持的。为了实现这一点,我们在本书的所有示例应用中使用了一个getBestPreviewSize()
方法。该方法如清单 5-4 所示:
清单 5-4。 计算最佳预览尺寸
`private Camera.Size getBestPreviewSize(int width, int height, Camera.Parameters
parameters) {
Camera.Size result=null;
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
if (size.width<=width && size.height<=height) {
if (result==null) {
result=size;
}
else {
int resultArea=result.widthresult.height;
int newArea=size.widthsize.height;
if (newArea>resultArea) {
result=size;
}
}
}
}
return(result);
}`
要使用它,请执行以下操作:
清单 5-5。 调用最佳预览尺寸的计算器
`public void surfaceChanged(SurfaceHolder holder, int format, int width, int
height) {
Camera.Parameters parameters=camera.getParameters();
Camera.Size size=getBestPreviewSize(width, height, parameters);
if (size!=null) {
parameters.setPreviewSize(size.width, size.height);
camera.setParameters(parameters);
camera.startPreview();
inPreview=true;
}
}`
surfaceChanged()
是我们 app 的SurfaceHolder.Callback
部分的一部分。
setPreviewDisplay()中的异常
相机的另一个常见问题是在调用setPreviewDisplay()
时出现异常。该方法用于告诉相机哪个表面视图用于实时预览,或者传递 null 来移除预览表面。如果向该方法传递了不合适的图面,该方法将引发 IOException。
修复方法是确保传递的 SurfaceView 适用于相机。可以按如下方式创建合适的表面视图:
清单 5-6。 创建适合相机的表面视图
SurfaceView previewHolder; previewHolder = cameraPreview.getHolder(); previewHolder.addCallback(surfaceCallback); previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
我们将 SurfaceView 的类型改为SURFACE_TYPE_PUSH_BUFFERS
,因为它告诉 Android 它将从其他地方接收位图。因为 surface view 的类型改变后可能不会立即准备好,所以您应该在surfaceCallback
中完成其余的初始化工作。
AndroidManifest 错误
任何 Android 项目中的一个主要错误来源是 AndroidManifest.xml。开发人员经常忘记更新它,或者将某些元素放在错误的部分。
安全异常
在使用应用时,您很可能会遇到一些安全异常。如果您看一下 LogCat,您会看到类似这样的内容:
04-09 22:44:36.957: E/AndroidRuntime(13347): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.paar.ch2/com.paar.ch2.ProAndroidAR2Activity}: java.lang.SecurityException: Provider gps requires ACCESS_FINE_LOCATION permission
在这种情况下,抛出异常是因为我没有在清单中声明android.permission.ACCESS_FINE_LOCATION
,并且试图使用 GPS,这需要许可。
即使您已经声明了权限,但它不在清单的正确部分,您也可能会得到相同的错误。一个常见的问题是当开发者把它放在<application>
或<activity>
元素中时,它应该在根<manifest>
元素中。此外,开发人员报告说,有时即使问题出现在正确的部分,也可以通过将其从<application>
元素之后移到之前来解决。
表 5-1 列出了 AR 应用中的常用权限以及它们允许你做的事情。
在某些设备上,缺少相机权限也可能导致无法连接到相机服务错误。
<用途——图书馆>
<uses-library>
元素放在 Android 清单的<application>
元素中。默认情况下,每个项目中都包含标准的 android 小部件等等。然而,有些库,比如谷歌地图库,需要通过清单的<uses-library>
部分显式包含。地图库是 AR 应用中最常用的一个。您可以将它包含在<application>
元素中,如下所示:
清单 5-7。 将谷歌地图库添加到您的应用清单中
<uses-library android:name="com.google.android.maps" />
要包含这个库,您必须以 Google APIs 为目标。在我们所有带有地图的示例应用中,我们的目标是 Android 2.1 的 Google APIs。
<用途-特性>
虽然严格来说,丢失<uses-feature>
不是一个实际的错误,但最好将它放在你的应用中,因为它被各种发布渠道用来查看你的应用是否能在特定设备上工作。增强现实应用中最常见的两个是:
android.hardware.camera android.hardware.camera.autofocus
与地图相关的错误
地图是许多 AR 应用的重要组成部分。但是,在使用它们时,会出现一些非常常见的错误。我们来看看他们两个。
钥匙
Google Maps API 用于提供本书中的地图,它要求每个应用获得一个调试证书的 API 密钥(Eclipse 在调试时用它来签署应用)和另一个发布证书的 API 密钥(在发布前用它来签署您的.apk
)。当从调试切换到生产时,开发人员通常会忘记更改密钥,或者根本忘记添加密钥。在这些情况下,您的地图工作正常,只是没有加载地图切片,并且您会得到一个带有网格的白色背景和一些叠加层(如果有)。
将这两个键作为注释保存在 XML 文件中是一个很好的实践,这样您就不必重复地在线生成它们。列表 5-8 显示了一个例子:
清单 5-8。 带有两个键的示例地图视图
`<com.google.android.maps.MapView
android:id="@+id/mapView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:apiKey="0nU9aMfHubxd2LIZ_dht3zDDRBb2IG6T3NrnvUA" />
不扩展 MapActivity
要在您的应用中使用 Google Maps API,显示地图的活动必须扩展com.google.android.maps.MapActivity
而不是通常的活动。如果不这样做,您将会得到类似如下的错误:
04-03 14:40:33.670: E/AndroidRuntime(414): Caused by: java.lang.IllegalArgumentException: MapViews can only be created inside instances of MapActivity.
要解决这个问题,只需将类声明修改如下:
正常活动
public class example extends Activity {
地图活动
public class example extends MapActivity {
要使用 MapActivity,您必须导入它,在清单中声明适当的<uses-library>
,并为 SDK 的 Google API 版本构建。
调试应用
本节讨论应用的调试。它将解释为了解决应用中的问题,您必须做些什么。
日志猫
我们先来看看 LogCat。在 Eclipse 的右上角有两个选项卡:Java 和 Debug。单击调试选项卡。你很可能会看到那里有两列。一个将有控制台和任务标签;另一个只有一个读 LogCat 的标签。如果 LogCat 选项卡不可见,进入窗口 显示视图
LogCat 。现在开始调试运行。通过 USB 插入设备后,如果启用了 USB 调试,您应该会在 LogCat 中看到类似图 5-1 的内容。
图 5-1。 一个 logcat 输出的例子
异常和错误将在 LogCat 中显示为红色块,长度大约为 10-25 行,这取决于具体的问题。大约在中间点,会有一行写着“起因于:……”。在这一行和这之后,你会在你的应用文件中找到导致错误的确切行号。
使用相机时的黑白方块
只有一种方法可以解决这样的问题:使用模拟器中的摄像头运行应用。模拟器不支持摄像机或任何其他传感器,除了通过模拟位置的 GPS。当您尝试在模拟器中创建相机预览时,您会看到一个由黑白方块组成的网格,像棋盘一样排列。但是,所有覆盖都应该按预期显示。
杂项
有一些错误实际上不属于前面的任何类别。我们将在这个杂项部分讨论它们。
无法从 GPS 获得定位
在测试或使用你的应用时,有时你的代码是完美的,但你仍然无法在你的应用中获得 GPS 定位。这可能是由以下任何原因造成的:
- 你在室内:GPS 需要一个清晰的天空视野来定位。试着站在开着的门或窗户附近,或者到外面去。
- 你在外面,但仍然没有定位:这种情况有时会发生在暴风雨或多云的天气。你唯一的选择是等天气稍微放晴后再试。
- 天气晴朗时没有 GPS 定位:这种情况通常发生在你忘记打开手机的 GPS 时。如果某个应用试图在它关闭时使用它,一些设备会自动打开它,但大多数设备都需要用户这样做。另一个简单的检查是打开另一个使用 GPS 的应用。你可以试试谷歌地图,因为它已经预装在几乎所有安卓设备上。如果连这个都不能解决,那么问题可能不在你的应用上。
指南针不工作
如今,许多增强现实应用都使用大多数 Android 设备中存在的指南针。指南针有助于导航应用,为观星设计的应用等等。
指南针经常会给出不正确的读数。这可能是由于以下原因之一:
- 指南针靠近金属/磁性物体或强电流:这些会产生强局部磁场,使硬件产生错误读数。试着转移到一个空旷的地方。
- 指南针没有校准:有时候硬件指南针没有校准到某个区域的磁场。在大多数设备中,用力翻转和摇动设备,或者以 8 字形挥动设备,可以重置指南针。
如果您的指南针在尝试了之前给出的解决方案后仍未给出正确的读数,您可能应该将其送到服务中心进行检查,因为您的硬件可能有故障。
总结
本章讨论您在编写 AR 应用时可能会遇到的常见错误以及如何解决它们。根据你正在编写的应用的类型,你无疑会面临许多其他逻辑标准 Java 和其他 Android 错误,这些错误与应用的 AR 部分并不真正相关。讨论你可能面临的每一个可能的错误本身就可以写满一整本书,所以我们在这里只讨论与 AR 相关的错误。
在下一章,我们将创建我们的第一个示例应用。
六、一个简单的基于位置的应用,使用增强现实和地图 API
本章概述了如何制作一个非常简单的现实世界增强现实(AR)应用。本章结束时,你将拥有一个功能齐全的示例应用。
该应用将具有以下基本功能:
- 该应用将启动,并在屏幕上显示一个实时相机预览。
- 相机预览将被传感器和位置数据覆盖,如在第三章微件覆盖示例应用中。
- 当手机与地面平行时,应用会切换到显示地图。我们将增加 7 的余量,因为用户不太可能将设备与地面完全平行。用户的当前位置将在应用上标记出来。该地图将有在卫星视图、街道视图和两者之间切换的选项。该地图将使用谷歌地图应用编程接口(API)提供。
- 当设备移动到与地面不平行的方向时,应用将切换回相机视图。
这个应用可以作为一个独立的应用,也可以扩展为一个增强现实导航系统,我们将在下一章中介绍。
首先,创建一个新的 Android 项目。该项目应针对谷歌 API(API 级别 7,因为我们的目标是 2.1 及以上),以便我们可以使用 Android 的地图功能。本章通篇使用的项目有包名com.paar.ch06
,项目名 Pro Android AR 6:使用 AR 的简单 App。您可以使用您想要的任何其他包和项目名称,只要您记得更改示例代码中的任何引用以匹配您的更改。
创建项目后,通过右键单击 eclipse 左侧栏中的包名并从 New 菜单中选择 class,向您的项目添加另一个类(参见图 6-1 ):
图 6-1。 菜单创建一个新的类。
将此类命名为FlatBack
。它将保存MapView
和相关的位置 API。然后创建另一个名为FixLocation
的类。在本章的后面你会学到更多关于这个类的知识。
编辑 XML
在创建了必要的类之后,我们就可以开始编码工作了。首先,编辑AndroidManifest.xml
来声明新的activity
,并要求必要的特性、库和权限。更新AndroidManifest.xml
如下:
清单 6-1 。更新 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
`package="com.paar.ch6"
android:versionCode="1"
android:versionName="1.0" >
<activity
android:label="@string/app_name"
android:name=".ASimpleAppUsingARActivity"
android:screenOrientation = "landscape"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:configChanges = "keyboardHidden|orientation">
确保FlatBack Activity
和之前声明的完全一样,确保<uses-library>
标签在<application>
标签内,所有的权限和特性请求都在<application>
标签外和<manifest>
标签内。这几乎是目前需要在AndroidManifest
中完成的所有事情。
我们现在需要添加一些字符串,这些字符串将在应用的覆盖图和帮助对话框中使用。将您的strings.xml
修改为以下内容:
清单 6-2。 更新 strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, ASimpleAppUsingARActivity!</string> <string name="app_name">A Simple App Using AR</string>
`
Android Augmented Reality. This app outlines some of the basic features of
Augmented Reality and how to implement them in real world applications.
`
创建菜单资源
您将创建两个菜单资源:一个用于摄像机预览Activity
,另一个用于MapActivity
。为此,在项目的/res
目录中创建一个名为menu
的新的子文件夹。在那个目录中创建两个 XML 文件,分别名为main_menu
和map_toggle
。在main_menu
中,添加以下内容:
清单 6-3。 main_menu.xml
`
`这基本上是主Activity
中的帮助选项。现在在map_toggle
中,我们将有三个选项,因此添加以下内容:
清单 6-4。 map_toggle.xml
`
`第一个选项允许用户设置街道视图显示的地图种类,就像你在路线图上看到的一样。第二种选择允许他们在地图上使用卫星图像。第三种选择是在那个地方的卫星图像上叠加一张路线图。当然,这两个文件都只定义了用户界面的一部分,实际的工作将在 Java 文件中完成。
布局文件
这个项目中有三个布局文件。一个用于主相机预览和相关叠加,一个用于帮助对话框,一个用于地图。
相机预览
相机预览Activity
布局文件是普通的main.xml
,对其标准内容有一些改变:
清单 6-5。 摄像机预览布局文件
`
<TextView
android:id="@+id/yAxisLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/xAxisLabel"
android:layout_below="@+id/xAxisLabel"
android:text="@string/yAxis" />
<TextView
android:id="@+id/longitudeLabel"`
`android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/latitudeLabel"
android:layout_below="@+id/latitudeLabel"
android:text="@string/longitude" />
`
<Button
android:id="@+id/helpButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/altitudeLabel"
android:layout_below="@+id/altitudeValue"
android:layout_marginTop="15dp"
android:text="@string/helpLabel" />
`
同样,你需要确保所有的 id 都是有序的,并且你没有在任何地方打错字,因为这将影响整个布局。与第三章第一部分的布局唯一的主要区别是增加了一个帮助按钮,它将启动帮助对话框。“帮助”菜单选项会做同样的事情,但是最好有一个更容易看到的选项。
帮助对话框
现在在/res/layout
目录中创建另一个名为help.xml
的 XML 文件。这将包含帮助对话框的布局设计,它有一个可滚动的TextView
来显示实际的帮助文本和一个关闭对话框的按钮。将以下内容添加到help.xml
文件中:
清单 6-6。 帮助对话框布局文件
`
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)