Android-AR-高级教程-全-

Android AR 高级教程(全)

原文:Pro Android Augmented Reality

协议:CC BY-NC-SA 4.0

零、简介

增强现实是移动计算领域相对较新的发展。尽管它还很年轻,但它已经是这个行业中发展最快的领域之一。公司正在投入大量资金开发使用增强现实的产品,其中最引人注目的是谷歌的谷歌眼镜项目。大多数人认为增强现实很难实现。那是一种误解。像任何好的应用一样,好的增强现实应用需要花费一些精力来编写。你所需要做的就是在潜水前保持开放的心态。

这本书是给谁的

这本书的目标读者是那些想为谷歌的 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 应用的最好例子是 SpecTrekWikitude 。总之,这些应用利用了几乎所有你可以用来在 Android 平台上制作 AR 应用的东西。我强烈建议您安装它们,并熟悉 Android 上 ar 的功能。

这个类别的大多数应用都可以在 Android 平台上实现。在一些情况下,他们甚至不使用所有的传感器。其中一些会变得相当复杂。图 1-1 和图 1-2 显示了来自 SpecTrek 的截图。

Image

图 1-1。SpecTrek 的截图

*Image

图 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>

这段代码没有什么突破性的东西。我们没有使用普通的布局,比如LinearLayoutRelativeLayout,而是简单地向 XML 文件添加了一个SurfaceView,其高度和宽度属性被设置为允许它填充整个可用屏幕。我们给它分配 ID cameraPreview,这样我们就可以从代码中引用它。现在重要的一步是使用 Android 相机服务,并告诉它连接到我们的SurfaceView来显示来自相机的实际预览。

要实现这一点,需要做三件事:

  1. 我们创建一个SurfaceView,它在我们的 XML 布局中。

  2. 我们还需要一个SurfaceHolder,它控制我们的SurfaceView的行为(例如,它的大小)。当发生变化时,例如当预览开始时,它也会得到通知。

  3. We need a Camera, obtained from the open() static method on the Camera class.

    要将所有这些串联起来,我们只需做以下事情:

  4. 通过getHolder()获取SurfaceViewSurfaceHolder

  5. 注册一个SurfaceHolder.Callback,这样当我们的SurfaceView准备好或改变时,我们会得到通知。

  6. 通过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类。SurfaceViewSurfaceHolder同样重要,但是没有两个选项可供选择。

关于变量。cameraPreview是一个SurfaceView变量,它将保存对 XML 布局中SurfaceView的引用(这将在onCreate()中完成)。previewHolder是管理SurfaceViewSurfaceHoldercameraCamera对象,将处理所有相机的东西。最后,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.CallbacksurfaceChanged()方法被调用是非常简单的,因为这可以告诉我们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.width
size.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 }

};
}`

方位传感器

方位传感器是磁场传感器和加速度传感器的组合。有了这两个传感器的数据和一点三角学,你就可以得到设备的pitchrollheading ( azimuth)。如果你喜欢三角学,你会失望地知道 Android 为你做了所有的计算,你可以简单地从一个SensorEvent中取出数值。

注意:磁场罗盘在金属物体周围会变得有点疯狂。猜猜测试时什么大型金属物体可能会靠近您的设备?你的电脑!如果你的读数不是你所期望的,请记住这一点。

图 2-1 显示了方位传感器的轴。

Image

图 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将用于获取我们所有的传感器数据,并管理我们的传感器。浮动headingAnglepitchAnglerollAngle将分别用于存储设备的航向、俯仰和横滚。

添加完上面给出的变量后,将下面几行添加到您的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 显示了加速度计的轴线。

Image

图 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 显示了球体的概念。

Image

图 2-3。 纬度的图示

经度也是地球网格系统的假想线。它们从北极运行到南极,在两极汇合。每个经度是一个大圆的一半。经度 0°被称为本初子午线,穿过英国格林威治。两个经度之间的距离在赤道处最大,大约为 69 英里,或 111 公里,与两个纬度之间的距离大致相同。

图 2-4 显示了另一个球体上的概念。

Image

图 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.width
size.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 。

Image

图 2-5。 应用运行时 LogCat 的截图。

如果你有一个清晰的天空视图,LogCat 还将包括三条线,告诉你纬度,经度和海拔高度。

结论

在本章中,您学习了如何使用摄像头,如何从加速度计和方位传感器读取值,以及如何使用 GPS 获取用户的位置。

你还学会了利用任何全功能增强现实应用的四个基本组件。你不会总是在你的应用中使用这四样东西。其实很少有 app 有这样的要求。

这一章应该会让你对 AR 有一个基本的了解,来自这个 app 的项目本质上是一个合适的 AR app 的骨架。

下一章将讨论叠加,以及它们如何给用户一个真实的增强体验。

三、添加覆盖

如前所述,增强现实(AR)是与正在显示的直接或间接相机预览相关的数据的叠加。在大多数 AR 应用中,相机预览会首先扫描标记。在翻译类应用中,预览会扫描文本。而且在一些游戏 app 里,不做扫描;相反,字符、按钮、文本等会覆盖在预览上。

本章的所有源代码都可以从本书的页面[www.apress.com](http://www.apress.com)或 GitHub 库下载。

在第二章中,我们制作了一个基本的应用,显示设备摄像头的预览,通过 GPS 检索位置,获取加速度计读数,并检索方位传感器读数。在这一章中,我们将继续构建这个应用,并添加覆盖图。我们将添加正常的 Android 部件覆盖和实现标记识别。让我们从最简单的开始:小部件覆盖。

小工具覆盖

Android 平台提供了一堆标准的小部件,比如TextViewsButtonsCheckboxes。这些默认包含在 Android 操作系统中,可以由任何应用使用。它们可能是你可以在相机预览中叠加的最简单的东西。

首先,创建一个新的 Android 项目。本例中使用的插件名为 Pro Android AR 3 Widget Overlay,构建于 Android 4 之上,其 minSdkVersion 设置为 7 (Android 2.1),包名为 com.paar.ch3widgetoverlay(您可以根据自己的需求进行更改,但一定要更新这里给出的示例代码。)图 3-1 显示了项目设置屏幕。

Image

图 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定义表格中的一行)。每行有零个或多个单元格,每个单元格都由任何类型的其他视图定义。所以一行的单元格可以由各种视图对象组成,比如ImageViewTextView。单元格也可以是一个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_NORMALTextView将会以一种适中的速率更新。如果延迟被设置为SENSOR_DELAY_GAME,我们会让TextView的更新速度超过我们的视觉速度。那会对 CPU 造成很大的负担。即使是现在,在一些较慢的设备上,该应用可能会显得滞后。

注意:你可以通过将更新TextViews的代码转换成TimerTaskHandler来避免延迟。

既然数据来自方位和加速度传感器,我们应该为 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 显示了应用完成后的样子。

Image

图 3-2。 无 GPS 定位的 app 截图

Image

图 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 显示了一个标记的例子。

Image

图 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 显示了一个正在显示的立方体。

Image

图 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> `#7AAA

#FF444444
#FF323232
#FF414141
#FFFFFFFF

#FFA52A2A
#FFFFC125
#FF5F9EA0
#FF00008B
`

所有颜色在ARGBAARRGGBB中指定。它们用于为我们的小演示应用增加一点视觉吸引力。“到”和“从”的颜色略有不同,因此我们可以在最终的演示中使用渐变。天空的颜色是蓝色,地面的颜色是橙色。

爪哇

现在我们将创建我们在 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 点罗盘的弦。名称中带有渐变颜色颜料的用于绘制ViewtextHeight也是如此。

获取和设置轴承、俯仰和侧倾

现在将以下方法添加到该类中:

清单 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 提供的这些方法为我们处理了很多复杂的数学问题。你应该很容易理解SensorEventListeneronResume()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 让你对完成的应用有个概念。

Image

图 4-1。 设备直立时的 app

Image

图 4-2。 设备倒置时的 app

在空中演示应用

在浏览并运行了前面的例子之后,您现在应该很好地理解了人工视界的概念。我们现在将设计一款应用,它具有以下功能:

  • 显示实时摄像机预览
  • 在相机预览上显示半透明版本的HorizonView,颜色类似于飞机中的电影版本
  • 告诉你 5 分钟后你的高度,假设你继续保持当前的倾斜度

在我们开始编码之前,有一些事情需要记住。由于用户几乎不可能保持设备完全静止,因此倾斜度将不断变化,这将导致 5 分钟内的高度也发生变化。为了解决这个问题,我们将添加一个按钮,允许用户随时更新高度。

注意:在一些设备上,这款应用可以顺时针和逆时针方向移动人工地平线,而不是像非 AR 演示中那样上下移动。所有值都是正确的,除了显示器有问题,可以通过在纵向模式下运行应用来修复。

设置项目

首先,创建一个新项目。作为例子使用的包名为 com.paar.ch4ardemo,目标是 Android 2.1。和往常一样,你可以把名字改成你喜欢的任何名字;只需确保更新示例代码中的所有引用。图 4-3 中的截图显示了项目详情。

Image

图 4-3。?? 申请详情

创建新项目后,将非增强现实演示中的所有内容复制到这个项目中。我们将建立在以前的项目。确保在需要的地方更新文件中的包名。

更新 XML

首先,我们需要更新应用的 XML。我们的应用目前只有四个 XML 文件:AndroidManifest.xmlmain.xmlcolours.xmlstrings.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。我们将添加两个新的字符串,作为ButtonTextView的标签。我们没有为另一个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 代码、TextViewButton参考,稍微修改我们的传感器代码,最后放入一个小算法来计算我们 5 分钟后的高度。我们将使用三角学来寻找高度的变化,所以如果你的有点生疏,你可能想快速温习一下。

首先,将以下变量添加到类的顶部。

清单 4-18。 新变量声明

`LocationManager locationManager;

Button updateAltitudeButton;
TextView altitudeValue;

double currentAltitude;
double pitch;
double newAltitude;
double changeInAltitude;
double thetaSin;`

将会是我们的区域经理。updateAltitudeButtonaltitudeValue将保存对它们的 XML 对应物的引用,这样我们可以监听点击并更新它们。currentAltitudenewAltitudechangeInAltitude都将用于在我们的算法运行期间存储值。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,Image
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 的概念图解。

Image

图 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 中的截图,看看这个应用是如何工作的。

Image

图 4-5。 应用运行,向用户显示重试消息

Image

图 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 中进入项目 Image 清理或者从命令行运行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.width
size.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 应用中的常用权限以及它们允许你做的事情。

Image

在某些设备上,缺少相机权限也可能导致无法连接到相机服务错误。

<用途——图书馆>

<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 选项卡不可见,进入窗口 Image 显示视图 Image LogCat 。现在开始调试运行。通过 USB 插入设备后,如果启用了 USB 调试,您应该会在 LogCat 中看到类似图 5-1 的内容。

Image

图 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 ):

Image

图 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>
`X Axis:
Y Axis:
Z Axis:
Heading:
Pitch:
Roll:
Altitude:
Longitude:
Latitude:

This is the example app from Chapter 6 of Pro
Android Augmented Reality. This app outlines some of the basic features of
Augmented Reality and how to implement them in real world applications.

Help

`

创建菜单资源

您将创建两个菜单资源:一个用于摄像机预览Activity,另一个用于MapActivity。为此,在项目的/res目录中创建一个名为menu的新的子文件夹。在那个目录中创建两个 XML 文件,分别名为main_menumap_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。 帮助对话框布局文件

`

现在我们需要创建最终的布局文件:地图布局。在您的/res/layout文件夹中创建一个map.xml,并将以下内容添加到其中:

清单 6-7。 地图布局文件

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <com.google.android.maps.MapView android:id="@+id/mapView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:clickable="true" android:apiKey="<your_key_here>" /> </LinearLayout>

获取 API 密钥

如果您的项目没有设置为基于 Google APIs 目标构建,您将会得到一个错误。这里另一件重要的事情是 API 键。这是由谷歌以证书的形式分配给你的。它是从您的证书的 MD5 散列中生成的,您必须以在线形式提交该散列。Android 使用数字证书来验证应用的安装文件。如果签名证书在已安装版本和新版本之间不匹配,Android 将抛出一个安全异常,不允许您更新安装。映射 API 密钥对于每个证书都是唯一的。因此,如果您计划发布您的应用,您必须生成两个 API 密匙:一个用于您的调试证书(Eclipse 在开发和测试过程中用它来签署您的应用),另一个用于您的发布证书(在将您的应用上传到在线市场(如 Android Market)之前用它来签署您的应用)。在不同的操作系统上,获取任何密钥的 MD5 的步骤是不同的。

获取密钥的 MD5

为调试键:

调试密钥通常位于以下位置:

  • Mac/Linux: ~/。android/debug.keystore
  • Windows Vista/7: C:\Users\ \。android\debug.keystore
  • windows XP:C:\ Documents and Settings <user>\。android\debug.keystore

您需要运行以下命令来取出 MD5。该命令使用 Keytool 工具:

keytool -list -alias androiddebugkey -keystore <path_to_debug_keystore>.keystore -storepass android -keypass android

对于签名密钥:

签名密钥在系统中没有固定的位置。在创建过程中或创建后,无论您将它保存或移动到何处,它都会被保存。运行下面的命令获取 MD5,用密钥的别名替换alias_name,用密钥的位置替换my-release-key:

keytool -list -alias alias_name -keystore my-release-key.keystore

在您提取了您想要的任何密钥 MD5 之后,使用您最喜欢的 web 浏览器导航到[code.google.com/android/maps-api-signup.html](http://code.google.com/android/maps-api-signup.html)。输入 MD5 并完成要求您做的任何其他事情。提交表单后,您将看到应用运行所需的 API 密钥。

Java 代码

现在 XML 设置已经准备好了。所需要的只是标记图像和实际代码。让我们从标记图像开始。它叫做ic_maps_current_position_indicator.png,可以在这个项目源代码的drawable-mdpidrawable-hdpi文件夹中找到。请确保将每个文件夹的图像复制到项目中的对应位置,不要错误地切换它们。

主要活动

有了图像,我们就可以开始写代码了。我们将从主要的Activity开始。

导入和变量声明

首先,我们来看看导入、类声明和变量声明:

清单 6-8。 主要活动进口和报关

`package com.paar.ch6;

import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
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.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class ASimpleAppUsingARActivity 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;

TextView xAxisValue;
TextView yAxisValue;
TextView zAxisValue;
TextView headingValue;
TextView pitchValue;
TextView rollValue;
TextView altitudeValue;
TextView latitudeValue;
TextView longitudeValue;

Button button;`

导入语句和类声明是标准的 Java,并且变量已经被命名来描述它们的功能。现在让我们继续讨论课堂上的不同方法。

onCreate()方法

app 的第一个方法,onCreate(),做了很多事情。它将main.xml文件设置为Activity视图。然后它获得位置和传感器系统服务。它为加速计和方向传感器以及全球定位系统(GPS)注册监听器。然后,它执行相机初始化的一部分(其余部分稍后执行)。最后,它获得了对九个TextViews的引用,以便它们可以在应用中更新,并获得了对帮助按钮的引用,设置了它的onClickListener。此方法的代码如下:

清单 6-9。 主活动的 onCreate()

`@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);

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);
button = (Button) findViewById(R.id.helpButton);
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
showHelp();
}
});
}`

位置监听器

代码中的下一个是LocationListener,它监听来自定位服务(本例中是 GPS)的位置更新。从 GPS 接收到更新后,它用新信息更新本地变量,将新信息打印到LogCat,并用新信息更新三个TextViews。它还包含应用中没有使用的方法的自动生成的方法存根。

清单 6-10。location listener

`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

}
};`

启动地图

接下来要解释的是launchFlatBack()方法。每当满足手机或多或少与地面平行的条件时,SensorEventListener就会调用该方法。然后,该方法启动地图。

清单 6-11。 launchFlatBack()

public void launchFlatBack() { Intent flatBackIntent = new Intent(this, FlatBack.class); startActivity(flatBackIntent); }

选项菜单

通过覆盖onCreateOptionsMenu()onOptionsItemSelected()方法来创建和使用选项菜单。第一个从菜单资源(main_menu.xml)创建它,第二个监听菜单上的点击事件。如果单击了帮助项,它将调用适当的方法来显示帮助对话框。

清单 6-12。 onCreateOptionsMenu()和 onOptionsItemSelected()

`@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu);
return true;
}

public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.help:
showHelp();
default:
return super.onOptionsItemSelected(item);
}
}`

显示帮助对话框

showHelp()是前面提到的合适方法。当单击“帮助”菜单项时,将调用该函数。

清单 6-13。 showHelp()

`public void showHelp() {
final Dialog dialog = new Dialog(this);
dialog.setContentView(R.layout.help);
dialog.setTitle("Help");
dialog.setCancelable(true);
//there are a lot of settings, for dialog, check them all out!

//set up text
TextView text = (TextView) dialog.findViewById(R.id.TextView01);
text.setText(R.string.help);`

//set up button Button button = (Button) dialog.findViewById(R.id.Button01); button.setOnClickListener(new OnClickListener() { public void onClick(View v) { dialog.cancel(); } }); //now that the dialog is set up, it's time to show it dialog.show(); }

监听传感器

现在我们来看SensorEventListener。有一个if陈述区分了方位传感器和加速度计。传感器的两个更新都被打印到LogCat和相应的TextViews。此外,代码中方位传感器部分的if语句决定了设备是否与地面平行。存在 14 度的偏差,因为任何人都不太可能将该设备与地面完全平行。

清单 6-14。 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));

if (pitchAngle < 7 && pitchAngle > -7 && rollAngle < 7 &&
rollAngle > -7)
{
launchFlatBack();
}
}`

`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
}
};`

onResume()、onPause()和 onDestroy()方法

我们覆盖了onResume()onPause()onDestroy()方法,这样我们就可以释放和重新获取SensorEventListenerLocationListenerCamera。当应用暂停(用户切换到另一个应用)或被破坏(Android 终止该进程)时,我们会释放它们,以节省用户的电池和使用更少的系统资源。此外,一次只有一个应用可以使用Camera,所以通过释放它,我们可以让其他应用使用它。

清单 6-15。 onResume(),onPause()和 onDestroy()

`@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;
}

@Override
public void onPause() {
if (inPreview) {
camera.stopPreview(); }
locationManager.removeUpdates(locationListener);
sensorManager.unregisterListener(sensorEventListener);
if (camera != null)
{
camera.release();
camera=null;
}
inPreview=false;

super.onPause();
}

@Override
public void onDestroy() {
camera.release();
camera=null;
}`

管理表面视图和相机

这最后四个方法处理管理SurfaceView、它的SurfaceHolderCamera

  • getBestPreviewSize()方法获得可用预览尺寸的列表,并选择最佳尺寸。
  • SurfaceView准备好时,调用surfaceCallback。相机在那里设置并打开。
  • 如果 Android 对SurfaceView做了任何更改,就会调用surfaceChanged()方法(例如,在方向改变之后)。
  • SurfaceView被销毁时,调用surfaceDestroyed()方法。

清单 6-16。 getBestPreviewSize()、surfaceCallback()、surfaceChanged()和 surfaceDestroyed()

`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.width
size.height;

if (newArea>resultArea) {
result=size;
}
}
}
}

return(result);
}

SurfaceHolder.Callback surfaceCallback=new SurfaceHolder.Callback() {
public void surfaceCreated(SurfaceHolder holder) {
if (camera == null) {
camera = Camera.open();
}
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) {
if (camera != null) {
camera.stopPreview();
camera.setPreviewCallback(null);
camera.release();
camera = null;
}
}`

}; }

这是第一个 Java 文件的结尾。该文件与 GPS 和传感器一起工作以获得更新,然后通过TextViewsLogCat输出显示它们。

平反. java

现在我们来学习FlatBack.java.这个Activity在手机与地面平行时被调用,并在地图上显示你的当前位置。这个类现在没有多大意义,因为部分工作是在FixLocation完成的。

导入、变量声明和 onCreate()方法

在这个ActivityonCreate()中,我们一如既往地在开头重复SensorManager的东西。这里我们需要传感器输入,因为当设备不再与地面平行时,我们希望切换回CameraView。之后,我们获取对MapView(XML 布局中的那个)的引用,告诉 Android 我们不会实现自己的缩放控件,将MapView传递给FixLocation,将位置覆盖添加到MapView,告诉它更新,并调用自定义方法将它缩放到用户的位置。

清单 6-17。 Flatback.java 的导入、声明和 onCreate()

`package com.paar.ch6;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import com.google.android.maps.MyLocationOverlay;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;`

`public class FlatBack extends MapActivity{
private MapView mapView;
private MyLocationOverlay myLocationOverlay;
final static String TAG = "PAAR";
SensorManager sensorManager;

int orientationSensor;
float headingAngle;
float pitchAngle;
float rollAngle;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// main.xml contains a MapView
setContentView(R.layout.map);

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
orientationSensor = Sensor.TYPE_ORIENTATION;
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);

// extract MapView from layout
mapView = (MapView) findViewById(R.id.mapView);
mapView.setBuiltInZoomControls(true);

// create an overlay that shows our current location
myLocationOverlay = new FixLocation(this, mapView);

// add this overlay to the MapView and refresh it
mapView.getOverlays().add(myLocationOverlay);
mapView.postInvalidate();

// call convenience method that zooms map on our location
zoomToMyLocation();
}`

onCreateOptionsMenu()和 onOptionsItemSelected()方法

接下来是两个与选项菜单相关的方法,它们创建选项菜单,观察点击,区分哪个选项被点击,并在地图上执行适当的操作。

清单 6-18。 onCreateOptionsMenu()和 onOptionsItemSelected()

`@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.map_toggle, menu);
return true;
}

public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.map:
if (mapView.isSatellite() == true) {
mapView.setSatellite(false);
mapView.setStreetView(true);
}
return true;
case R.id.sat:
if (mapView.isSatellite()==false){
mapView.setSatellite(true);
mapView.setStreetView(false);
}
return true;
case R.id.both:
mapView.setSatellite(true);
mapView.setStreetView(true);
default:
return super.onOptionsItemSelected(item);
}
}`

SensorEventListener

接下来是SensorEventListener,它与前面的类类似,只是它检查手机是否不再与地面平行,然后调用将带我们回到相机预览的自定义方法。

清单 6-19。 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));

if (pitchAngle > 7 || pitchAngle < -7 || rollAngle > 7
|| rollAngle < -7)
{
launchCameraView();
}
}
}

public void onAccuracyChanged(Sensor arg0, int arg1) {
// TODO Auto-generated method stub

}
};`

launchCameraView()方法

launchCameraView()方法完成当前的activity,这样我们就可以毫无问题地进行相机预览。一个Intent被注释掉了,似乎做了同样的事情。我把它注释掉了,因为尽管它最终启动了摄像机预览,但它是通过创建那个activity的另一个实例来完成的,这将会产生一个错误,因为摄像机已经被活动的第一个实例使用了。因此,最好返回到以前的实例。

清单 6-20。 launchCameraView()

public void launchCameraView() { finish(); //Intent cameraView = new Intent(this, ASimpleAppUsingARActivity.class); //startActivity(cameraView); }

onResume()和 onPause()方法

然后是onResume()onPause()方法,它们启用和禁用位置更新以节省资源。

清单 6-21。 onResume()和 onPause()

@Override protected void onResume() { super.onResume();
`myLocationOverlay.enableMyLocation();
}

@Override
protected void onPause() {
super.onPause();
myLocationOverlay.disableMyLocation();
}`

zoomToMyLocation()方法

这是自定义后的zoomToMyLocation()方法。此方法将缩放级别 10 应用于地图上的当前位置。

清单 6-22。 zoomToMyLocation()

private void zoomToMyLocation() { GeoPoint myLocationGeoPoint = myLocationOverlay.getMyLocation(); if(myLocationGeoPoint != null) { mapView.getController().animateTo(myLocationGeoPoint); mapView.getController().setZoom(10); } }

isRouteDisplayed()方法

最后是布尔方法isRouteDisplayed()。因为没有在 app 中使用,所以设置为 false。

清单 6-23。 isRouteDisplayed()

protected boolean isRouteDisplayed() { return false; } }

这就把我们带到了FlatBack.java的结尾。注意,大多数实际的定位工作似乎是在FixLocation.java中完成的。在您厌倦 Eclipse 在其引用中给你错误之前,我们将继续编写那个类。

FixLocation.java

现在是时候了解FixLocation的用途了。在一些 Android 驱动的设备中,MyLocationOverlay类有严重的错误,其中最显著的是摩托罗拉 droid。FixLocation试图使用标准的 MyLocationOverlay,但是如果它不能正常工作,它会实现自己的版本,这将产生相同的结果。首先是源代码,然后是解释:

清单 6-24。FixLocation.java??

`package com.paar.ch6;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Paint.Style;
import android.graphics.drawable.Drawable;
import android.location.Location;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.MyLocationOverlay;
import com.google.android.maps.Projection;

public class FixLocation extends MyLocationOverlay {

private boolean bugged = false;

private Drawable drawable;
private Paint accuracyPaint;
private Point center;
private Point left;
private int width;
private int height;

public FixLocation(Context context, MapView mapView) {
super(context, mapView);
}

@Override
protected void drawMyLocation(Canvas canvas, MapView mapView,
Location lastFix, GeoPoint myLocation, long when) {
if(!bugged) {
try {
super.drawMyLocation(canvas, mapView, lastFix,
myLocation, when);
} catch (Exception e) {
// we found a buggy phone, draw the location
icons ourselves
bugged = true;
}
}`

`if(bugged) {
if(drawable == null) {

accuracyPaint = new Paint();
accuracyPaint.setAntiAlias(true);
accuracyPaint.setStrokeWidth(2.0f);

drawable = mapView.getContext()
.getResources().getDrawable(R.drawable.ic_maps_indicator_current_position);
width = drawable.getIntrinsicWidth();
height = drawable.getIntrinsicHeight();
center = new Point();
left = new Point();
}

Projection projection = mapView.getProjection();
double latitude = lastFix.getLatitude();
double longitude = lastFix.getLongitude();
float accuracy = lastFix.getAccuracy();

float[] result = new float[1];

Location.distanceBetween(latitude, longitude, latitude,
longitude + 1, result);
float longitudeLineDistance = result[0];

GeoPoint leftGeo = new GeoPoint((int)(latitude1e6),
(int)((longitude-accuracy/longitudeLineDistance)
1e6));
projection.toPixels(leftGeo, left);
projection.toPixels(myLocation, center);
int radius = center.x - left.x;

accuracyPaint.setColor(0xff6666ff);
accuracyPaint.setStyle(Style.STROKE);
canvas.drawCircle(center.x, center.y, radius,
accuracyPaint);

accuracyPaint.setColor(0x186666ff);
accuracyPaint.setStyle(Style.FILL);
canvas.drawCircle(center.x, center.y, radius,
accuracyPaint);

drawable.setBounds(center.x - width/2, center.y -
height/2, center.x + width/2, center.y + height/2);
drawable.draw(canvas);
}
}
}`

首先,我们有接收来自FlatBack的调用的方法。然后我们覆盖drawMyLocation()方法。在实现中,我们检查它是否被窃听。我们试图让它正常运行,但是如果我们得到一个异常,我们将 bugged 设置为 true,然后继续执行我们自己的工作实现。

如果它确实被窃听,我们设置油漆,得到一个可画的参考,得到位置,计算精度,然后在地图上画标记,随着精度圈。准确度圆圈意味着位置不是 100%准确,你在圆圈内的某个地方。

这个示例应用到此结束。现在快速看一下如何运行该应用,并查看一些截图。

运行应用

该应用应该编译没有任何错误或警告。如果您确实遇到了错误,请阅读下面的常见错误部分。

在设备上调试时,你可能会看到一个橙色的三角形,如图 6-2 所示。

Image

图 6-2。 橙色预警三角

这个三角形仅仅意味着 Eclipse 无法确认 Google APIs 是否安装在您的设备上。如果你的 Android 设备预装了 Android Market,你可以很确定它已经安装了 Google APIs。

当你运行这个应用时,你应该会看到类似于图 6-3 到图 6-5 的截图。

Image

图 6-3。app 的增强现实视图

Image

图 6-4。app 的帮助对话框

Image

图 6-5。 地图显示当装置平行于地面时

LogCat看起来应该类似于图 6-6 。

Image

图 6-6。app的 LogCat 截图

常见错误

以下是该应用的四个常见错误。对于其他任何事情,请向安卓开发者谷歌集团或 stackoverflow.com 寻求帮助。

  • 未能连接到相机服务:我唯一一次看到这个错误是在其他东西已经在使用相机的时候。这个错误可以用几种方法解决,stackoverflow.com 应该能给你答案。
  • 任何看起来与地图相关的东西:这很可能是因为你没有针对 Google APIs 进行构建,或者因为你忘记在AndroidManifest中声明<uses-library>,或者使用了不正确的 API 键。
  • 任何看起来与 R.something 有关的东西:这个错误很可能是由于 XML 文件中的错误或不匹配,或者缺少 drawable。您可以通过检查 XML 文件来修复它。如果你确定它们是正确的,并且你的可绘制标记已经就位,试着通过删除/bin 目录后编译或者使用 Project - > Clean 从头开始构建。
  • 安全异常:这些很可能是由于您的AndroidManifest中缺少许可。

总结

这将我们带到本书中第一个示例应用的结尾,它演示了如何执行以下操作:

  • 使用标准的 Android SDK,通过实时摄像头预览增加传感器信息
  • 当以特定方式握住设备时,启动Activity,在这种情况下,与地面平行
  • 使用 Google Maps APIs 在地图上显示用户的当前位置
  • 在设备上的地图 API 被破坏的情况下实施修复

这个应用将在下一章中构建,作为一个简单的 AR 导航应用。

七、使用增强现实、GPS 和地图的基本导航应用

在第六章中,我们设计了一个简单的 AR 应用,如果设备与地面平行,它将在相机预览上显示传感器数据,并在地图上显示位置。在这一章中,我们将扩展这个应用,以便它可以用于基本的导航目的。

新的应用

该应用的扩展版本将具有以下功能:

  • 当与地面不平行时,该应用将显示一个覆盖有各种数据的相机预览。
  • 当与地面平行时,地图将会展开。用户可以在地图上定位期望的位置,并使 Tap 能够设置模式。启用该模式后,用户可以点击所需的位置。该位置已保存。
  • 再次显示相机预览时,根据 GPS 数据计算出目标位置的方位以及两个位置之间的距离。
  • 每次接收到新的定位时,方位和距离都会更新。

如果你想扩展它来添加指南针和做其他事情,这个应用会给你你需要的每一个计算。

现在,事不宜迟,让我们开始编码。

首先创建一个新项目。在这个例子中,包名是 com.paar.ch7,构建目标是 android 2.1 的 Google APIs。我们必须针对谷歌 APIs SDK,因为我们正在使用谷歌地图。

首先,复制第六章的项目。把 main Activity(带有相机预览的那个)的名字改成你想要的任何名字,只要你记得更新它的清单。此外,因为这是一个新项目,您可能还需要另一个包名。

更新的 XML 文件

首先,我们需要更新一些 XML 文件。先说strings.xml:

清单 7-1。 更新 strings.xml

<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, ASimpleAppUsingARActivity!</string> <***string name="app_name">A Slightly More Complex AR App</string>*** <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> ***<string name="help">This is the example app from Chapter 7 of Pro Android*** ***Augmented Reality. This app outlines some of the basic features of Augmented*** ***Reality and how to implement them in real world applications. This app includes*** ***a basic system that can be used for navigation. To make use of this system, put*** ***the app in the map mode by holding the device flat. Then enable \"Enable tap to*** ***set\" from the menu option after you have located the place you want to go to.*** ***After that, switch back to camera mode. If a reliable GPS fix is available, you***
***will be given your current bearing to that location. The bearing will be updated*** ***every time a new location fix is received.</string>*** <string name="helpLabel">Help</string> ***<string name="go">Go</string>*** ***<string name="bearingLabel">Bearing:</string>*** ***<string name="distanceLabel">Distance:</string>*** </resources>

这里的“Distance:”将作为标签,告诉用户从他/她的当前位置到所选位置的直线距离。乌鸦路径是从 A 点到 b 点的直线距离。它不显示通过道路或任何其他路径的距离。如果你还记得高中物理的话,这很像位移。它是从 A 点到 B 点的最短距离,不管这段距离实际上是否可以穿越。

您会注意到一些新的字符串和帮助字符串大小的增加。除此之外,我们的strings.xml大体相同。接下来,我们需要从/res/menu文件夹中更新我们的map_toggle.xml。我们需要添加一个新的选项来允许用户设置位置。

清单 7-2。 更新后的 map_toggle.xml

`

****** ****** `

我们的新菜单选项是“启用点击设置”此选项将用于允许用户启用和禁用点击来设置我们的应用的功能。如果我们不添加检查,每次用户移动地图或试图缩放时,都会设置一个新的位置。为了避免这种情况,我们设置了一个启用/禁用选项。

现在是我们最大的 XML 文件 main.xml 的最后一个变化,我们需要添加两个TextViews并稍微移动我们的帮助按钮。下面的代码只显示了更新的部分。此处未给出的内容与上一章完全相同。

清单 7-3。 更新 main.xml

`// Cut here

<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/altitudeLabel"
android:layout_below="@+id/altitudeLabel"
android:text="@string/bearingLabel" />

<TextView
android:id="@+id/bearingValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/altitudeValue"
android:layout_below="@+id/altitudeValue"
android:text="@string/empty" />

<Button
android:id="@+id/helpButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="@string/helpLabel" />

<TextView
android:id="@+id/distanceLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView1"
android:layout_below="@+id/textView1"
android:text="@string/distanceLabel" />

<TextView android:id="@+id/distanceValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/distanceLabel"
android:layout_alignLeft="@+id/bearingValue"
android:text="@string/empty" />

`

鉴于我们添加的内容都遵循了前一章的模式,我希望这段代码是不言自明的。id 中带有“label”的TextViews是实际值的标签。我们的 Java 代码不会引用这些。id 中带有“value”的TextViews将从我们的 Java 代码中动态更新以显示值。

更新的 Java 文件

现在我们可以开始主要的 Java 代码了。我们三分之二的 Java 文件需要用新代码更新。

FixLocation.java中,您需要更新包声明以匹配新的声明。那是那份文件中唯一的变化。

FlatBack.java 更新

现在让我们转到下一个需要更新的文件:FlatBack.java:

清单 7-4。更新了 FlatBack.java 的

`package com.paar.ch7;*

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapView;
import com.google.android.maps.MyLocationOverlay;

import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

public class FlatBack extends MapActivity{
private MapView mapView;
private MyLocationOverlay myLocationOverlay;
final static String TAG = "PAAR";
SensorManager sensorManager;

SharedPreferences prefs;
SharedPreferences.Editor editor;

int orientationSensor;
float headingAngle;
float pitchAngle;
float rollAngle;
String enteredAddress;
boolean tapToSet;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// main.xml contains a MapView
setContentView(R.layout.map);
prefs = getSharedPreferences("PAARCH7", 0);
editor = prefs.edit();
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
orientationSensor = Sensor.TYPE_ORIENTATION;
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);

// extract MapView from layout
mapView = (MapView) findViewById(R.id.mapView);
mapView.setBuiltInZoomControls(true);

// create an overlay that shows our current location
myLocationOverlay = new FixLocation(this, mapView);

// add this overlay to the MapView and refresh it
mapView.getOverlays().add(myLocationOverlay);
mapView.postInvalidate();

// call convenience method that zooms map on our location
zoomToMyLocation();

mapView.setOnTouchListener(new OnTouchListener() {`

`public boolean onTouch(View arg0, MotionEvent arg1) {

if(tapToSet == true)
{
GeoPoint p = mapView.getProjection().fromPixels((int)
arg1.getX(), (int) arg1.getY());

Log.d(TAG,"Latitude:" + String.valueOf(p.getLatitudeE6()/1e6));
Log.d(TAG,"Longitude:" +
String.valueOf(p.getLongitudeE6()/1e6));
float lat =(float) ((float) p.getLatitudeE6()/1e6);
float lon = (float) ((float) p.getLongitudeE6()/1e6);
editor.putFloat("SetLatitude", lat);
editor.putFloat("SetLongitude", lon);
editor.commit();
return true;
}
return false;

}

});

}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.map_toggle, menu);
return true;
}

public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.map:
if (mapView.isSatellite() == true) {
mapView.setSatellite(false);
mapView.setStreetView(true);
}
return true;
case R.id.sat:
if (mapView.isSatellite()==false){
mapView.setSatellite(true);
mapView.setStreetView(false);
}
return true;
case R.id.both:
mapView.setSatellite(true); mapView.setStreetView(true);
case R.id.toggleSetDestination:
if(tapToSet == false)
{
tapToSet = true;
item.setTitle("Disable Tap to Set");
}
else if(tapToSet == true)
{
tapToSet = false;
item.setTitle("Enable Tap to Set");
mapView.invalidate();
}
default:
return super.onOptionsItemSelected(item);
}
}

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));

if (pitchAngle > 7 || pitchAngle < -7 || rollAngle > 7 || rollAngle
< -7)
{
launchCameraView();
}
}
}

public void onAccuracyChanged(Sensor arg0, int arg1) {

}
};

public void launchCameraView() {
finish();
}

@Override
protected void onResume() {
super.onResume(); myLocationOverlay.enableMyLocation();
}

@Override
protected void onPause() {
super.onPause();
myLocationOverlay.disableMyLocation();
}

private void zoomToMyLocation() {
GeoPoint myLocationGeoPoint = myLocationOverlay.getMyLocation();
if(myLocationGeoPoint != null) {
mapView.getController().animateTo(myLocationGeoPoint);
mapView.getController().setZoom(10);
}
}

protected boolean isRouteDisplayed() {
return false;
}
}`* *我们来看看有什么变化。首先,我们在顶部有一些新的变量:

boolean tapToSet; SharedPreferences prefs; SharedPreferences.Editor editor;

boolean tapToSet将告诉我们点击设置模式是否启用。另外两个是SharedPreferences相关变量。我们将使用SharedPreferences来存储用户的设置值,因为我们将从我们类的两个活动中访问它。当然,我们可以在启动MapActivity时使用startActivityForResult(),并以这种方式获取用户的设置值,但通过使用SharedPreferences,我们也可以保留用户最后使用的位置,以防应用稍后启动时没有设置新位置。

接下来,我们给我们的onCreate()方法添加了一些新的东西。这两行负责访问我们的SharedPreferences,并允许我们稍后编辑它们:

prefs = getSharedPreferences("PAARCH7", 0); editor = prefs.edit();

PAARCH7 是我们偏好文件的名称,代表ProAn droidAugmentedRealityChapter7。如果你自己扩展这个应用,并从多个地方同时使用SharedPreferences,请记住,当编辑同一个偏好文件时,每个人都可以立即看到这些变化。第一次运行时,PAARCH7 文件不存在,所以 Android 创建了它。逗号后面的小 0 告诉 Android 这个文件是私有的。下一行指定编辑器能够编辑我们的首选项。

现在我们的onCreate()方法有了更多的变化。我们给我们的MapView分配一个onTouchListener():

`mapView.setOnTouchListener(new OnTouchListener() {

public boolean onTouch(View arg0, MotionEvent arg1) {

if(tapToSet == true)
{
GeoPoint p =
mapView.getProjection().fromPixels((int)arg1.getX(),Image
(int) arg1.getY());

Log.d(TAG,"Latitude:" +String.valueOf(p.getLatitudeE6()/1e6));
Log.d(TAG,"Longitude:" +String.valueOf(p.getLongitudeE6()/1e6));
float lat =(float) ((float) p.getLatitudeE6()/1e6);
float lon = (float) ((float) p.getLongitudeE6()/1e6);
editor.putFloat("SetLatitude", lat);
editor.putFloat("SetLongitude", lon);
editor.commit();
return true;
}
return false;

}

});`

在这个onTouchListener(),中,我们过滤每个触摸。如果启用了点击设置模式,我们将捕获触摸事件并获得纬度和经度。然后我们把从 touched GeoPoint接收到的 doubles 转换成 floats,这样我们就可以按照自己的喜好来写了,这正是我们所做的。我们把这两个浮点数都放在我们的首选项文件中,然后调用editor.commit()把它们写到文件中。如果我们捕捉到触摸,我们返回true,如果没有,我们返回false。通过返回false,我们允许MapView继续正常的滚动和放大缩小。

我们需要做的最后一件事是修改我们的onOptionsItemSelected()方法,以允许 Enable Tap To Set 选项。

public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { case R.id.map: if (mapView.isSatellite() == true) { mapView.setSatellite(false);
mapView.setStreetView(true); } return true; case R.id.sat: if (mapView.isSatellite()==false){ mapView.setSatellite(true); mapView.setStreetView(false); } return true; case R.id.both: mapView.setSatellite(true); mapView.setStreetView(true); case R.id.toggleSetDestination: if(tapToSet == false) { tapToSet = true; item.setTitle("Disable Tap to Set"); } else if(tapToSet == true) { tapToSet = false; item.setTitle("Enable Tap to Set"); mapView.invalidate(); } default: return super.onOptionsItemSelected(item); } }

我们首先检查tapToSet是否是false。如果是,我们将其设置为true,并将标题更改为“禁用点击设置”如果是true,我们把它改成false,把标题改回“启用点击设置”

这份文件就包装好了。

主活动文件

现在我们只剩下主文件了。

我们将从查看新变量开始。

清单 7-5。 包申报、进口和新变量

`package com.paar.ch7;

import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.content.SharedPreferences;`

`import android.hardware.Camera;

double bearing;
double distance;

float lat;
float lon;

Location setLoc;
Location locationInUse;

SharedPreferences prefs;

TextView bearingValue;
TextView distanceValue;`

当从文件中读取时,两个浮点数latlon将存储我们保存到MapActivity中的SharedPreferences中的值。位置setLoc将被传递前面提到的纬度和经度以创建一个新的Location。然后,我们将使用该位置来获取用户的方位。locationInUse是我们 GPS 定位的副本。这两个TextViews将显示我们的结果。doublebearingdistance将存储我们的结果。

现在我们需要对我们的onCreate()方法做一些改变。

清单 7-6。 更新 onCreate()

`@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
setLoc = new Location("");

prefs = getSharedPreferences("PAARCH7", 0);

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);

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);
bearingValue = (TextView) findViewById(R.id.bearingValue);
distanceValue = (TextView) findViewById(R.id.distanceValue);
button = (Button) findViewById(R.id.helpButton);
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
showHelp();
}
});
}`

prefs = getSharedPreferences("PAARCH7", 0);让我们访问我们的SharedPreferences。接下来的新行(bearingValue = (TextView) findViewById(R.id.bearingValue);distanceValue = (TextView) findViewById(R.id.distanceValue);)将引用我们的新TextViews,并允许我们稍后更新它们。

现在我们必须更新LocationListener,这样我们的计算就会随着位置的更新而更新。这个比较简单。

清单 7-7。 更新了 LocationListener

`LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
locationInUse = 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));

lat = prefs.getFloat("SetLatitude", 0.0f);
lon = prefs.getFloat("SetLongitude", 0.0f);

setLoc.setLatitude(lat);
setLoc.setLongitude(lon);
if(locationInUse != null)
{
bearing = locationInUse.bearingTo(setLoc);
distance = locationInUse.distanceTo(setLoc);
bearingValue.setText(String.valueOf(bearing));
distanceValue.setText(String.valueOf(distance));
}
}`

我们的修改包括从SharedPreferences获取值,并检查我们是否有一个有效的位置;如果有一个有效的位置,我们计算并显示方位和距离。如果没有,我们什么也不做。

我们需要在onResume()中重复一些相同的事情。这是因为当我们切换到MapActivity并设置位置时,我们将回到相机预览。这意味着onResume()将被调用,从而使它成为更新我们的位置和计算的最佳位置。

清单 7-8。 更新于 Resume

`@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;

lat = prefs.getFloat("SetLatitude", 0.0f);
lon = prefs.getFloat("SetLongitude", 0.0f);

setLoc.setLatitude(lat);
setLoc.setLongitude(lon);
if(locationInUse != null) {
bearing = locationInUse.bearingTo(setLoc);
distance = locationInUse.distanceTo(setLoc);
bearingValue.setText(String.valueOf(bearing));
distanceValue.setText(String.valueOf(distance));
}
else
{
bearingValue.setText("Unable to get your location reliably.");
distanceValue.setText("Unable to get your location reliably.");
}
}`

几乎完全一样,除了如果我们不能得到位置来计算距离和方位,我们也会给出一个消息。

更新的 Android 清单

这基本上结束了这个示例应用。此处未给出的所有文件与第六章中的完全相同。最后一次更新是对AndroidManifest.xml的更新,其中Activity声明已被编辑:

清单 7-9。 更新 AndroidManifest.xml

`
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.paar.ch7"
android:versionCode="1"
android:versionName="1.0" >

`

完成的应用

图 7-1–7-5 显示了增强现实模式下的应用,打开了帮助对话框和地图。

Image

图 7-1。 启动时的应用,没有 GPS 定位

Image

图 7-2。 打开带有帮助对话框的 app

Image

图 7-3。 打开带有地图的应用,显示选项菜单

Image

图 7-4。 显示用户当前位置的应用

Image

图 7-5。 该 app 带有到达设定位置的方位和距离。地点定在中国中部,我面朝北。

你可以从这本书的 apress.com 页面或者 GitHub 库获得完整的源代码。

摘要

本章讨论了如何制作导航应用的基本框架。我们允许用户选择地图上的任何一点,然后我们计算用户需要移动的方向作为方位。将它转换为可发布的应用只需要你在增强现实视图中画一个箭头,为用户指出正确的方向。然而,在示例应用中添加这些内容会增加其复杂性,超出本章的范围。

在下一章,你将学习如何设计和实现一个基于标记的增强现实浏览器。*

八、3D 增强现实模型查看器

在完成信息章节并浏览了前两个示例应用后,您现在应该对 Android 上的增强现实(ar)非常熟悉了。这是倒数第二个示例应用,也是普通非游戏应用中的最后一个,因为最后一个示例应用是使用 AR 构建的游戏。

这个应用使用标记功能,非常简单。启动时,它会向用户显示内置对象的列表,以显示在标记上,或者让他们选择从设备的内存中选择一个自定义对象。该应用接受波前中的对象。obj 文件,以及它们的。mtl 同行。如果你不熟悉这些格式和 wavefront,我建议你在继续之前先阅读一下。

Image

图 8-1。 正在展示的安卓模型。

这款应用的主要特点

以下是该应用的主要功能:

  • Allows the user to view any pre-installed model on the marker.
  • Allows users to view the external model located on SD card, and locate and select it by using OI file manager.
  • Show all 3D models on the marker

再次,从创建一个新项目开始。这个项目没有扩展任何以前的项目,所以我们将从零开始。我们将有 22 个 Java 文件、8 个 drawables、4 个布局、1 个 strings.xml 和 31 个资产文件。图 8-2 显示了项目的详细情况。

Image

图 8-2。本章中的项目的细节。

这个项目将使用 AndAR 作为外部库,使我们的 AR 任务更容易。标记识别算法很难实现,这个库是一个工作实现,我们可以在本书的范围内安全地使用。你可以从code.google.com/p/andar/downloads/list获得 AndAR 库的副本,但是如果你从这个项目在 GitHub.com 或 Apress.com 的源代码下载它会更好,因为将来或旧版本的 AndAR 可能不会以这个项目中使用的方式实现。

载货单

首先,这里是声明应用中所有权限和活动的AndroidManifest.xml

清单 8-1。 AndroidManifest.xml

`










` ` `

在这个文件中,我们声明了应用中的四个活动:ModelChooser、ModelViewer、Instructions 和 CheckFileManagerActivity。然后,我们告诉 Android,我们将使用外部存储和摄像头,并请求许可。我们告诉 Android,我们将使用相机功能和自动对焦功能。最后,我们声明我们的应用支持的屏幕尺寸。

Java 文件

让我们通过创建将成为我们主要活动的文件来开始 Java 代码。

主要活动

在我的例子中,这个文件被称为ModelChooser.java,它在用户第一次启动应用时显示。它有一个可以显示的预加载模型列表,一个从设备内存加载外部用户提供的模型的选项,以及一个指向帮助文件的链接。

onCreate()方法

让我们通过对这个文件的onCreate()方法进行一些修改来开始它的编码。

清单 8-2。ModelChooser.java 的 onCreate()方法

`@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AssetManager am = getAssets();
Vector models = new Vector();
Item item = new Item();
item.text = getResources().getString(R.string.choose_a_model);
item.type = Item.TYPE_HEADER;
models.add(item);

try {
String[] modelFiles = am.list("models");
List modelFilesList = Arrays.asList(modelFiles); for (int i = 0; i < modelFiles.length; i++) {
String currFileName = modelFiles[i];
if(currFileName.endsWith(".obj")) {
item = new Item();
String trimmedFileName =
currFileName.substring(0,currFileName.lastIndexOf(".obj"));
item.text = trimmedFileName;
models.add(item);
if(modelFilesList.contains(trimmedFileName+".jpg")) {
InputStream is = am.open("models/"+trimmedFileName+".jpg");
item.icon=(BitmapFactory.decodeStream(is));
} else if(modelFilesList.contains(trimmedFileName+".png")) {
InputStream is = am.open("models/"+trimmedFileName+".png");
item.icon=(BitmapFactory.decodeStream(is));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
item = new Item();
item.text = getResources().getString(R.string.custom_model);
item.type = Item.TYPE_HEADER;
models.add(item);
item = new Item();
item.text = getResources().getString(R.string.choose_custom_model);
item.icon = new Integer(R.drawable.open);
models.add(item);
item = new Item();
item.text = getResources().getString(R.string.help);
item.type = Item.TYPE_HEADER;
models.add(item);
item = new Item();
item.text = getResources().getString(R.string.instructions);
item.icon = new Integer(R.drawable.help);
models.add(item);

setListAdapter(new ModelChooserListAdapter(models));
}`

这段代码看起来有点复杂,但是它的任务非常简单。它检索我们的资产文件夹中所有模型的列表,并从中创建一个漂亮的列表。如果模型有相应的图像文件,它会在对象名称旁边以图标样式显示该图像;否则,它只是显示一个类似十字的图像。除了添加应用附带的模型之外,这段代码还添加了选择您自己的模型和访问帮助文件的选项。

倾听滴答声

接下来,我们需要一个方法来监听点击,并为每次点击做适当的工作。

清单 8-3。 onListItemClick()方法

@Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); Item item = (Item) this.getListAdapter().getItem(position); String str = item.text; if(str.equals(getResources().getString(R.string.choose_custom_model))) { //start oi file manager activity Intent intent = new Intent(ModelChooser.this, CheckFileManagerActivity.class); startActivity(intent); } else if(str.equals(getResources().getString(R.string.instructions))) { //show the instructions activity startActivity(new Intent(ModelChooser.this, Instructions.class)); } else { //load the selected internal file Intent intent = new Intent(ModelChooser.this, ModelViewer.class); intent.putExtra("name", str+".obj"); intent.putExtra("type", ModelViewer.TYPE_INTERNAL); intent.setAction(Intent.ACTION_VIEW); startActivity(intent); } }

这段代码监听任何列表项上的点击。当检测到单击时,它会检查哪个项目被单击。如果用户想要选择一个外部模型,我们使用 intent 来检查并启动 OI 文件管理器。如果用户想要查看指令,我们启动指令活动。如果选择了一个内部模型,我们启动模型查看器,同时将它的动作设置为ACTION_VIEW,并将模型的名称作为额外信息发送出去。

列表适配器

如果你一直在仔细观察onCreate中的代码,你会看到一个错误,我们正在为我们的列表设置适配器。我们现在将通过创建一个内部类作为列表的适配器来解决这个问题。

清单 8-4。 我们的适配器列表

class ModelChooserListAdapter extends BaseAdapter{ `private Vector items;

public ModelChooserListAdapter(Vector items) {
this.items = items;
}

public int getCount() {
return items.size();
}

public Object getItem(int position) {
return items.get(position);
}

public long getItemId(int position) {
return position;
}

@Override
public int getViewTypeCount() {
//normal items, and the header
return 2;
}

@Override
public boolean areAllItemsEnabled() {
return false;
}

@Override
public boolean isEnabled(int position) {
return !(items.get(position).type==Item.TYPE_HEADER);
}

@Override
public int getItemViewType(int position) {
return items.get(position).type;
}

public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
Item item = items.get(position);
if (v == null) {
LayoutInflater vi =
(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
switch(item.type) {
case Item.TYPE_HEADER:
v = vi.inflate(R.layout.list_header, null);
break; case Item.TYPE_ITEM:
v = vi.inflate(R.layout.choose_model_row, null);
break;
}
}
if(item != null) {
switch(item.type) {
case Item.TYPE_HEADER:
TextView headerText = (TextView)
v.findViewById(R.id.list_header_title);
if(headerText != null) {
headerText.setText(item.text);
}
break;
case Item.TYPE_ITEM:
Object iconImage = item.icon;
ImageView icon = (ImageView)
v.findViewById(R.id.choose_model_row_icon);
if(icon!=null) {
if(iconImage instanceof Integer) {

icon.setImageResource(((Integer)iconImage).intValue());
} else if(iconImage instanceof Bitmap) {
icon.setImageBitmap((Bitmap)iconImage);
}
}
TextView text = (TextView)
v.findViewById(R.id.choose_model_row_text);
if(text!=null)
text.setText(item.text);
break;
}
}
return v;
}

}`

简而言之,这段代码负责实际提取图标图像、名称等等;然后创建一个列表。这没什么了不起的。在处理列表时,这或多或少是标准的 Android 代码。

下面是另一个非常小的内部类,处理我们的项目。

清单 8-5。 内类物品

class Item { private static final int TYPE_ITEM=0; private static final int TYPE_HEADER=1; private int type = TYPE_ITEM; private Object icon = new Integer(R.drawable.missingimage); private String text; }

这五个变量用于设置列表中的每一行。TYPE_ITEM是一个常数,我们可以用它来表示包含模型的行,而不是使用整数。TYPE_HEADERTYPE_ITEM相同,除了它是用于标题。type变量用于存储当前正在处理的项目的类型。默认情况下,它设置为 TYPE_ITEM。icon 变量用于表示当相应的图像不可用于模型时所使用的图标。text 变量用于存储正在处理的当前项目的文本。

这将我们带到主 ModelChooser 类的末尾。不要忘记插入一个最后的“}”来结束整个外层类。

现在我们已经创建了我们的主活动,让我们按照字母顺序处理剩下的 21 个 Java 文件,以便于跟踪它们,并使这一切变得简单一点。

AssetsFileUtility.java

我们现在需要创建一个名为AssetsFileUtility的文件,它将负责读入我们存储在/assets文件夹中的数据。/assets文件夹是一个你可以存储任何你想要的文件的地方,然后以原始字节流的形式检索它。在存储原始文件的能力上,类似于/res/raw。然而,存储在/res/raw中的文件可以通过资源 id(如R.raw.filename)进行本地化和访问。/assets文件夹不提供本地化或资源 id 访问。

清单 8-6。AssetsFileUtility.java??

`public class AssetsFileUtility extends BaseFileUtil {
private AssetManager am;

public AssetsFileUtility(AssetManager am) {
this.am = am;
}

@Override
public Bitmap getBitmapFromName(String name) {
InputStream is = getInputStreamFromName(name);
return (isnull)?null:BitmapFactory.decodeStream(is);
} @Override
public BufferedReader getReaderFromName(String name) {
InputStream is = getInputStreamFromName(name);
return (is
null)?null:new BufferedReader(new InputStreamReader(is));
}

private InputStream getInputStreamFromName(String name) {
InputStream is;
if(baseFolder != null) {
try {
is = am.open(baseFolder+name);
} catch (IOException e) {
e.printStackTrace();
return null;
}
} else {
try {
is = am.open(name);
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return is;
}

}`

这段代码帮助我们从/assets文件夹中检索一个文件。它处理大部分工作,比如创建InputStreamReaders等等。如果您试图读取的文件不存在或者在其他方面无效(例如,文件扩展名无效),您将得到一个IOException

BaseFileUtil.java

接下来是一个名为BaseFileUtil.java的微型类。该文件是其他文件(如 AssetsFileUtility)的基础。它允许我们方便地更新正在查看的模型所在的文件夹。

清单 8-7。BaseFileUtil.java??

`public abstract class BaseFileUtil {
protected String baseFolder = null;

public String getBaseFolder() {
return baseFolder;
} public void setBaseFolder(String baseFolder) {
this.baseFolder = baseFolder;
}

public abstract BufferedReader getReaderFromName(String name);
public abstract Bitmap getBitmapFromName(String name);

}`

CheckFileManagerActivity.java

我们按字母顺序排列的下一个是CheckFileManagerActivity。,当用户想要提供他自己的对象以被应用增强时,调用这个函数。通过允许用户查看自己的模型,我们有效地将这个应用变成了一个成熟的 3D AR 查看器。例如,用户可以设计一把椅子,并在建造它之前看看它在他的房子里会是什么样子。这极大地扩展了我们应用的可用性。目前,该应用仅支持 OI Filemanager 来选择新文件,但您可以修改代码,以允许该应用与其他文件管理器一起工作。我选择 OI 作为默认的,因为它预装在很多设备上,如果没有的话,通常也会安装。

代码列表

让我们一节一节的来看看CheckFileManagerActivity.java

声明

首先是这个类中需要的声明。

清单 8-8。【CheckFileManagerActivity.java 宣言】??

`public class CheckFileManagerActivity extends Activity {

private final int PICK_FILE = 1;
private final int VIEW_MODEL = 2;
public static final int RESULT_ERROR = 3;

private final int INSTALL_INTENT_DIALOG=1;

private PackageManager packageManager;
private Resources res;
private TextView infoText;

private final int TOAST_TIMEOUT = 3;`

onCreate()和 onResume()

紧随声明之后的是onCreate()onResume()方法。

我们做的第一件事是检查 OI 文件管理器是否安装。如果不是,我们要求用户安装它。如果文件管理器可用,我们允许用户选择一个文件。参见清单 8-9 。

清单 8-9。 onCreate()和 onResume()

`@Override
final public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Context context = this;
packageManager= context.getPackageManager();
res = this.getResources();
infoText = (TextView) findViewById(R.id.InfoText);
if (isPickFileIntentAvailable()) {
selectFile();
} else {
installPickFileIntent();
}
}

@Override
protected void onResume() {
super.onResume();
}`

onActivityResult()

如果选择的文件不是一个有效的模型文件,我们会显示一个提示告诉用户,并要求他再次选择。如果选择的文件是一个有效的模型文件,我们将控制权传递给模型查看器,它将解析并显示该文件。如果用户取消操作,我们将应用返回到模型选择器屏幕。

清单 8-10。 onActivityResult()

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { default: case PICK_FILE: switch(resultCode) { case Activity.RESULT_OK: //does file exist?? File file = new File(URI.create(data.getDataString())); if (!file.exists()) { //notify user that this file doesn't exist Toast.makeText(this, res.getText(R.string.file_doesnt_exist), TOAST_TIMEOUT).show(); selectFile(); } else { String fileName = data.getDataString(); if(!fileName.endsWith(".obj")) { Toast.makeText(this, res.getText(R.string.wrong_file), TOAST_TIMEOUT).show(); selectFile(); } else { //hand over control to the model viewer Intent intent = new Intent(CheckFileManagerActivity.this, ModelViewer.class); intent.putExtra("name", data.getDataString()); intent.putExtra("type", ModelViewer.TYPE_EXTERNAL); intent.setAction(Intent.ACTION_VIEW); startActivityForResult(intent, VIEW_MODEL); } } break; default: case Activity.RESULT_CANCELED: //back to the main activity Intent intent = new Intent(CheckFileManagerActivity.this, ModelChooser.class); startActivity(intent); break; } break; case VIEW_MODEL: switch(resultCode) { case Activity.RESULT_OK: //model viewer returned...let the user view a new file selectFile(); break; case Activity.RESULT_CANCELED: selectFile(); break; case RESULT_ERROR: //something went wrong ... notify the user if(data != null) { Bundle extras = data.getExtras(); String errorMessage = extras.getString("error_message"); if(errorMessage != null) Toast.makeText(this, extras.getString("error_message"), TOAST_TIMEOUT).show(); } selectFile(); break; } } }

选择文件()

selectFile()方法允许用户选择一个模型文件。

清单 8-11。 selectFile()

`/** Let the user select a File. The selected file will be handled in

  • {@link
    edu.dhbw.andobjviewer.CheckFileManagerActivity#onActivityResult(int, int,
    Intent)} */
    private void selectFile() {
    //let the user select a model file
    Intent intent = new Intent("org.openintents.action.PICK_FILE");
    intent.setData(Uri.parse("file:///sdcard/"));
    intent.putExtra("org.openintents.extra.TITLE", res.getText(
    R.string.select_model_file));
    startActivityForResult(intent, PICK_FILE);
    }`
ispickfileintentavailable()和 installPickFileIntent()

在 onCreate 方法()中调用了isPickFileIntentAvailable()installPickFileIntent()方法。

清单 8-12。ispickfileattemptavailable()和 installPickFileIntent()

`private boolean isPickFileIntentAvailable() {
return packageManager.queryIntentActivities(
new Intent("org.openintents.action.PICK_FILE"), 0).size() > 0;
}

private boolean installPickFileIntent() {
Uri marketUri =
Uri.parse("market://search?q=pname:org.openintents.filemanager");
Intent marketIntent = new Intent(Intent.ACTION_VIEW).setData(marketUri);
if (!(packageManager
.queryIntentActivities(marketIntent, 0).size() > 0)) {
//no Market available
//show info to user and exit infoText.setText(res.getText(R.string.android_markt_not_avail));
return false;
} else {
//notify user and start Android market

showDialog(INSTALL_INTENT_DIALOG);
return true;
}
}`

onCreateDialog()

CheckFileManagerActivity.java中最后一个方法是onCreateDialog()

清单 8-13。 onCreateDialog()

`@Override
protected Dialog onCreateDialog(int id) {
Dialog dialog = null;
switch(id){
case INSTALL_INTENT_DIALOG:
AlertDialog alertDialog = new
AlertDialog.Builder(this).create();

alertDialog.setMessage(res.getText(R.string.pickfile_intent_required));
alertDialog.setButton("OK", new
DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
//launch android market
Uri marketUri =
Uri.parse("market://search?q=pname:org.openintents.filemanager");
Intent marketIntent = new
Intent(Intent.ACTION_VIEW).setData(marketUri);
startActivity(marketIntent);
return;
} });
dialog = alertDialog;
break;
}
return dialog;
}

}`

配置文件

列表中的下一个是Config.java文件。这是你见过的最小的 Java 文件。除去包名,只有三行大小。

清单 8-14。Config.java??

public class Config { public final static boolean DEBUG = false; }

这个文件在技术上是配置文件,尽管它只有一个选项。将DEBUG设置为true会将应用置于调试模式。如果您决定扩展该应用,您可以在此添加其他配置选项,例如您要发布 apk 的市场的标志。

与数字打交道

接下来是FixedPointUtilities类,它处理一些数学函数,主要是转换数组等。这对于保持我们的模型看起来是非常重要的。

清单 8-15。FixedPointUtilities.java??

`public class FixedPointUtilities {
public static final int ONE = 0x10000;

public static int toFixed(float val) {
return (int)(val * 65536F);
}

public static int[] toFixed(float[] arr) {
int[] res = new int[arr.length];
toFixed(arr, res);
return res;
}

public static void toFixed(float[] arr, int[] storage)
{
for (int i=0;i<storage.length;i++) {
storage[i] = toFixed(arr[i]);
}
} public static float toFloat(int val) {
return ((float)val)/65536.0f;
}

public static float[] toFloat(int[] arr) {
float[] res = new float[arr.length];
toFloat(arr, res);
return res;
}

public static void toFloat(int[] arr, float[] storage)
{
for (int i=0;i<storage.length;i++) {
storage[i] = toFloat(arr[i]);
}
}

public static int multiply (int x, int y) {
long z = (long) x * (long) y;
return ((int) (z >> 16));
}

public static int divide (int x, int y) {
long z = (((long) x) << 32);
return (int) ((z / y) >> 16);
}

public static int sqrt (int n) {
int s = (n + 65536) >> 1;
for (int i = 0; i < 8; i++) {
s = (s + divide(n, s)) >> 1;
}
return s;
}
}`

现在让我们看看这个类中的方法。第一种方法将单浮点值转换为 16.16 定点值。

public static int toFixed(float val) { return (int)(val * 65536F); }

第二个方法做同样的事情,只是它是对一个浮点数组做的。

public static int[] toFixed(float[] arr) { int[] res = new int[arr.length]; toFixed(arr, res); return res; }

第三个方法由第二个方法调用以帮助其工作。

public static void toFixed(float[] arr, int[] storage) { for (int i=0;i<storage.length;i++) { storage[i] = toFixed(arr[i]); } }

第四种方法将单个定点值转换为浮点数。

public static float toFloat(int val) { return ((float)val)/65536.0f; }

第五个方法对定点值数组做同样的事情,它调用第六个方法来帮助它。

`public static float[] toFloat(int[] arr) {
float[] res = new float[arr.length];
toFloat(arr, res);
return res;
}

public static void toFloat(int[] arr, float[] storage)
{
for (int i=0;i<storage.length;i++) {
storage[i] = toFloat(arr[i]);
}
}`

第七种方法是将两个定点值相乘,第八种方法是将两个定点值相除。

`public static int multiply (int x, int y) {
long z = (long) x * (long) y;
return ((int) (z >> 16));
}

public static int divide (int x, int y) {
long z = (((long) x) << 32);
return (int) ((z / y) >> 16);
}`

第九个也是最后一个方法是求一个定点值的平方根。

public static int sqrt (int n) { int s = (n + 65536) >> 1; for (int i = 0; i < 8; i++) { s = (s + divide(n, s)) >> 1; } return s; }

这些方法是从 MatrixUtils.java 调用的。我们的模型本质上是大量的顶点。当我们解析它们时,我们需要处理这些顶点,这有助于我们做到这一点。

Group.java

接下来,我们有一个名为Group.java的类。此类主要是解析。obj 文件和它们的。mtl 对应方;并用它们制作合适的、用户友好的图形。这是我们对象解析中相对较小的一部分,但仍然很重要。

在 OpenGL 中,每个图形都是一组称为顶点的坐标。当这些顶点中有三个或三个以上被线连接起来时,它们被称为面。几个面孔经常被组合在一起。面可能有也可能没有纹理。纹理会改变特定面反射光线的方式。这个类处理组的创建,将每个组关联到一个材质,并设置它的纹理。

清单 8-16。Group.java??

`public class Group implements Serializable {
private String materialName = "default";
private transient Material material;

private boolean textured = false;
public transient FloatBuffer vertices = null;
public transient FloatBuffer texcoords = null;
public transient FloatBuffer normals = null;
public int vertexCount = 0;

public ArrayList groupVertices = new ArrayList(500);
public ArrayList groupNormals = new ArrayList(500);
public ArrayList groupTexcoords = new ArrayList();

public Group() {
}

public void setMaterialName(String currMat) {
this.materialName = currMat;
}

public String getMaterialName() {
return materialName;
} public Material getMaterial() {
return material;
}

public void setMaterial(Material material) {
if(texcoords != null && material != null && material.hasTexture()) {
textured = true;
}
if(material != null)
this.material = material;
}

public boolean containsVertices() {
if(groupVertices != null)
return groupVertices.size()>0;
else if(vertices != null)
return vertices.capacity()>0;
else
return false;
}

public void setTextured(boolean b) {
textured = b;
}

public boolean isTextured() {
return textured;
}

public void finalize() {
if (groupTexcoords.size() > 0) {
textured = true;
texcoords = MemUtil.makeFloatBuffer(groupTexcoords.size());
for (Iterator iterator = groupTexcoords.iterator();
iterator.hasNext()😉 { Float curVal = iterator.next();
texcoords.put(curVal.floatValue());
}
texcoords.position(0);
if(material != null && material.hasTexture()) {
textured = true;
} else {
textured = false;
}
}
groupTexcoords = null;
vertices = MemUtil.makeFloatBuffer(groupVertices.size());
vertexCount = groupVertices.size()/3;//three floats pers vertex
for (Iterator iterator = groupVertices.iterator();
iterator.hasNext()😉 { Float curVal = iterator.next();
vertices.put(curVal.floatValue());
}
groupVertices = null;
normals = MemUtil.makeFloatBuffer(groupNormals.size());
for (Iterator iterator = groupNormals.iterator();
iterator.hasNext()😉 {
Float curVal = iterator.next();
normals.put(curVal.floatValue());
}
groupNormals = null;
vertices.position(0);
normals.position(0);
}
}`

该代码主要处理添加纹理和“材料”到我们正在解析的图形。它设置用于图形和材料的纹理。当然,这种材料只是虚拟的,从技术上讲并不是真实的材料。

Instructions.java

接下来是另一个非常简单的文件。这个文件叫做Instructions.java,包含Activity,它通过在WebView中显示一个位于/assets/help中的 HTML 文件来显示我们的应用的指令。

清单 8-17。Instructions.java??

`public class Instructions extends Activity {

private WebView mWebView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.instructions_layout);
mWebView = (WebView) findViewById(R.id.instructions_webview);

WebSettings webSettings = mWebView.getSettings();
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);

WebChromeClient client = new WebChromeClient();
mWebView.setWebChromeClient(client); mWebView.loadUrl("file:///android_asset/help/"+getResources().getString(R.string
.help_file));
}
}`

活动开始,单个 WebView 被设置为其视图。然后,WebView 被传递一个 HTML 文件,该文件包含我们的帮助并存储在资产中。

与光一起工作

现在我们来看一些更复杂的东西。我们的模型是使用 OpenGL 渲染的。为了让它们看起来更好,我们还采用了各种照明技术。对于这种照明,我们有一个名为LightingRenderer的类。

清单 8-18。LightingRenderer.java??

`public class LightingRenderer implements OpenGLRenderer {

private float[] ambientlight0 = {.3f, .3f, .3f, 1f};
private float[] diffuselight0 = {.7f, .7f, .7f, 1f};
private float[] specularlight0 = {0.6f, 0.6f, 0.6f, 1f};
private float[] lightposition0 = {100.0f,-200.0f,200.0f,0.0f};

private FloatBuffer lightPositionBuffer0 =
GraphicsUtil.makeFloatBuffer(lightposition0);
private FloatBuffer specularLightBuffer0 =
GraphicsUtil.makeFloatBuffer(specularlight0);
private FloatBuffer diffuseLightBuffer0 =
GraphicsUtil.makeFloatBuffer(diffuselight0);
private FloatBuffer ambientLightBuffer0 =
GraphicsUtil.makeFloatBuffer(ambientlight0);

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); private float[] ambientlight2 = {.4f, .4f, .4f, 1f};
private float[] diffuselight2 = {.7f, .7f, .7f, 1f};
private float[] specularlight2 = {0.6f, 0.6f, 0.6f, 1f};
private float[] lightposition2 = {5f,-3f,-20f,1.0f};

private FloatBuffer lightPositionBuffer2 =
GraphicsUtil.makeFloatBuffer(lightposition2);
private FloatBuffer specularLightBuffer2 =
GraphicsUtil.makeFloatBuffer(specularlight2);
private FloatBuffer diffuseLightBuffer2 =
GraphicsUtil.makeFloatBuffer(diffuselight2);
private FloatBuffer ambientLightBuffer2 =
GraphicsUtil.makeFloatBuffer(ambientlight2);

private float[] ambientlight3 = {.4f, .4f, .4f, 1f};
private float[] diffuselight3 = {.4f, .4f, .4f, 1f};
private float[] specularlight3 = {0.6f, 0.6f, 0.6f, 1f};
private float[] lightposition3 = {0,0f,-1f,0.0f};

private FloatBuffer lightPositionBuffer3 =
GraphicsUtil.makeFloatBuffer(lightposition3);
private FloatBuffer specularLightBuffer3 =
GraphicsUtil.makeFloatBuffer(specularlight3);
private FloatBuffer diffuseLightBuffer3 =
GraphicsUtil.makeFloatBuffer(diffuselight3);
private FloatBuffer ambientLightBuffer3 =
GraphicsUtil.makeFloatBuffer(ambientlight3);

public final void draw(GL10 gl) {

}

public final void setupEnv(GL10 gl) {
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT, ambientLightBuffer0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, diffuseLightBuffer0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR, specularLightBuffer0);
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, lightPositionBuffer0);
gl.glEnable(GL10.GL_LIGHT0);
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.glLightfv(GL10.GL_LIGHT2, GL10.GL_AMBIENT, ambientLightBuffer2);
gl.glLightfv(GL10.GL_LIGHT2, GL10.GL_DIFFUSE, diffuseLightBuffer2);
gl.glLightfv(GL10.GL_LIGHT2, GL10.GL_SPECULAR, specularLightBuffer2);
gl.glLightfv(GL10.GL_LIGHT2, GL10.GL_POSITION, lightPositionBuffer2);
gl.glEnable(GL10.GL_LIGHT2);
gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_AMBIENT, ambientLightBuffer3);
gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_DIFFUSE, diffuseLightBuffer3);
gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_SPECULAR, specularLightBuffer3);
gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_POSITION, lightPositionBuffer3);
gl.glEnable(GL10.GL_LIGHT3);
initGL(gl);
}

public final void initGL(GL10 gl) {
gl.glDisable(GL10.GL_COLOR_MATERIAL);
gl.glShadeModel(GL10.GL_SMOOTH);
gl.glEnable(GL10.GL_LIGHTING);
//gl.glEnable(GL10.GL_CULL_FACE);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glEnable(GL10.GL_NORMALIZE);
gl.glEnable(GL10.GL_RESCALE_NORMAL);
}
}`

我们创建浮动来存储不同部分和不同环境下的照明值,然后用它们来创建FloatBuffers。所有这些都通过setupEnv()方法应用到我们的应用中,最后通过initGL方法输出。这段代码比本章到目前为止看到的其他代码更接近 AR,并且对于确保我们的图形的照明良好并且看起来真实非常重要。OpenGL 总共支持八种不同的照明配置,我们创建了其中的四种(GL_LIGHT0-8)。我们有不同的环境光、高光和漫射光设置,这允许我们给模型四种不同的外观。所有的灯光都设置为GL_SMOOTH,这需要更多的计算能力,但会产生更真实的模型。

创建材料

现在我们来上我们的课。这个类是前面提到的在Group类中使用的材料。在现实世界中,光被物体的材料反射。有些材质反射绿色,有些反射红色,有些反射蓝色,等等。同样,在 OpenGL 中,我们创建所谓的物质对象,然后反过来构成我们的最终模型。每个材质对象都被设置为反射特定的光线阴影。当这与我们的灯光效果相结合时,我们得到了两种灯光的组合。例如,红色材质球在蓝色光源下会呈现黑色,因为红色材质不会反射蓝色阴影;它只会反射出一点红色。这个类处理所有与材质相关的 OpenGL 代码。

清单 8-19。Material.java??

`public class Material implements Serializable {

private float[] ambientlightArr = {0.2f, 0.2f, 0.2f, 1.0f}; private float[] diffuselightArr = {0.8f, 0.8f, 0.8f, 1.0f};
private float[] specularlightArr = {0.0f, 0.0f, 0.0f, 1.0f};

public transient FloatBuffer ambientlight = MemUtil.makeFloatBuffer(4);
public transient FloatBuffer diffuselight = MemUtil.makeFloatBuffer(4);
public transient FloatBuffer specularlight = MemUtil.makeFloatBuffer(4);
public float shininess = 0;
public int STATE = STATE_DYNAMIC;
public static final int STATE_DYNAMIC = 0;
public static final int STATE_FINALIZED = 1;

private transient Bitmap texture = null;
private String bitmapFileName = null;
private transient BaseFileUtil fileUtil = null;

private String name = "defaultMaterial";

public Material() {

}

public Material(String name) {
this.name = name;
//fill with default values
ambientlight.put(new float[]{0.2f, 0.2f, 0.2f, 1.0f});
ambientlight.position(0);
diffuselight.put(new float[]{0.8f, 0.8f, 0.8f, 1.0f});
diffuselight.position(0);
specularlight.put(new float[]{0.0f, 0.0f, 0.0f, 1.0f});
specularlight.position(0);
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void setFileUtil(BaseFileUtil fileUtil) {
this.fileUtil = fileUtil;
}

public String getBitmapFileName() {
return bitmapFileName;
}

public void setBitmapFileName(String bitmapFileName) {
this.bitmapFileName = bitmapFileName; }

public void setAmbient(float[] arr) {
ambientlightArr = arr;
}

public void setDiffuse(float[] arr) {
diffuselightArr = arr;
}

public void setSpecular(float[] arr) {
specularlightArr = arr;
}

public void setShininess(float ns) {
shininess = ns;
}

public void setAlpha(float alpha) {
ambientlight.put(3, alpha);
diffuselight.put(3, alpha);
specularlight.put(3, alpha);
}

public Bitmap getTexture() {
return texture;
}

public void setTexture(Bitmap texture) {
this.texture = texture;
}

public boolean hasTexture() {
if(STATE == STATE_DYNAMIC)
return this.bitmapFileName != null;
else if(STATE == STATE_FINALIZED)
return this.texture != null;
else
return false;
}

public void finalize() {
ambientlight = MemUtil.makeFloatBuffer(ambientlightArr);
diffuselight = MemUtil.makeFloatBuffer(diffuselightArr);
specularlight = MemUtil.makeFloatBuffer(specularlightArr);
ambientlightArr = null;
diffuselightArr = null;
specularlightArr = null;
if(fileUtil != null && bitmapFileName != null) {
texture = fileUtil.getBitmapFromName(bitmapFileName); }
}
}`

这门课帮助我们在需要的时候创造新的材料。有一个指定的默认材质,但是 setter 方法如setAmbient()setDiffuse()setSpecular()setShininess()允许我们指定新数组的反射值,以及它的环境光,等等。finalize 方法将光照转换为 FloatBuffers,并为纹理赋值。

MemUtil.java

接下来我们有一个很小的类,用来创建浮动缓冲区。这是我们的MemUtil班。

清单 8-20。MemUtil.java??

`public class MemUtil {

public static FloatBuffer makeFloatBufferFromArray(float[] arr) {
ByteBuffer bb = ByteBuffer.allocateDirect(arr.length*4);
bb.order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.put(arr);
fb.position(0);
return fb;
}

public static FloatBuffer makeFloatBuffer(int size) {
ByteBuffer bb = ByteBuffer.allocateDirect(size*4);
bb.order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.position(0);
return fb;
}

public static FloatBuffer makeFloatBuffer(float[] arr) {
ByteBuffer bb = ByteBuffer.allocateDirect(arr.length*4);
bb.order(ByteOrder.nativeOrder());
FloatBuffer fb = bb.asFloatBuffer();
fb.put(arr);
fb.position(0);
return fb;
}

}`

这个类非常简单,不需要太多解释,因为它是非常标准的 Java。我们需要 floatbuffer,因为 OpenGL 在其照明和材质实现中只接受 float buffer 参数。

Model.java

现在我们有了一个对我们的应用非常重要的类,Model.java

清单 8-21。Model.java??

`public class Model implements Serializable{

public float xrot = 90;
public float yrot = 0;
public float zrot = 0;
public float xpos = 0;
public float ypos = 0;
public float zpos = 0;
public float scale = 4f;
public int STATE = STATE_DYNAMIC;
public static final int STATE_DYNAMIC = 0;
public static final int STATE_FINALIZED = 1;

private Vector groups = new Vector();
protected HashMap<String, Material> materials = new HashMap<String,
Material>();
public Model() {
materials.put("default",new Material("default"));
}
public void addMaterial(Material mat) {
materials.put(mat.getName(), mat);
}
public Material getMaterial(String name) {
return materials.get(name);
}
public void addGroup(Group grp) {
if(STATE == STATE_FINALIZED)
grp.finalize();
groups.add(grp);
}
public Vector getGroups() {
return groups;
}
public void setFileUtil(BaseFileUtil fileUtil) {
for (Iterator iterator = materials.values().iterator();
iterator.hasNext()😉 { Material mat = (Material) iterator.next();
mat.setFileUtil(fileUtil);
}
}
public HashMap<String, Material> getMaterials() {
return materials;
}
public void setScale(float f) {
this.scale += f;
if(this.scale < 0.0001f)
this.scale = 0.0001f;
}
public void setXrot(float dY) {
this.xrot += dY;
}
public void setYrot(float dX) {
this.yrot += dX;
}
public void setXpos(float f) {
this.xpos += f;
}
public void setYpos(float f) {
this.ypos += f;
}
public void finalize() {
if(STATE != STATE_FINALIZED) {
STATE = STATE_FINALIZED;
for (Iterator iterator = groups.iterator();
iterator.hasNext()😉 {
Group grp = (Group) iterator.next();
grp.finalize();
grp.setMaterial(materials.get(grp.getMaterialName()));
}
for (Iterator iterator = materials.values().iterator(); iterator.hasNext()😉 {
Material mtl = iterator.next();
mtl.finalize();
}
}
}
}`

这个课程为创建我们的模型做了大量的基础工作。让我们一个方法一个方法来看。方法是构造器,为我们的模型设置默认材质。addMaterial()方法为我们的应用添加了一个素材。addGroup()方法将另一个组添加到我们的组中,如果需要的话,还会完成它。setFileUtil()方法将一个BaseFileUtil作为参数,然后用它来设置我们所有材料的 fileUtil。setScale()方法允许我们传递一个设置为刻度的浮点数。它还确保小数位数是非零的正值。该比例值用于缩放模型。setXrot()setYrot()方法允许我们在模型上设置 X 轴和 Y 轴的旋转。setXpos()setYpos()方法用于设置模型在 X 轴和 Y 轴上的位置。方法确定了一切,并使之不可改变。

Model3D.java

我们列表中的下一个是Model3D.java,它负责我们模型的大量绘制。解释在代码之后。

清单 8-22。Model3D.java??

`public class Model3D extends ARObject implements Serializable{

private Model model;
private Group[] texturedGroups;
private Group[] nonTexturedGroups;
private HashMap<Material, Integer> textureIDs = new HashMap<Material,
Integer>();

public Model3D(Model model, String patternName) {
super("model", patternName, 80.0, new double[]{0,0});
this.model = model;
model.finalize();

Vector groups = model.getGroups();
Vector texturedGroups = new Vector();
Vector nonTexturedGroups = new Vector();
for (Iterator iterator = groups.iterator(); iterator.hasNext()😉 {
Group currGroup = iterator.next();
if(currGroup.isTextured()) {
texturedGroups.add(currGroup);
} else {
nonTexturedGroups.add(currGroup);
}
}
this.texturedGroups = texturedGroups.toArray(new
Group[texturedGroups.size()]);
this.nonTexturedGroups = nonTexturedGroups.toArray(new
Group[nonTexturedGroups.size()]);
}

@Override
public void init(GL10 gl){
int[] tmpTextureID = new int[1];

Iterator materialI = model.getMaterials().values().iterator();
while (materialI.hasNext()) { Material material = (Material) materialI.next();
if(material.hasTexture()) {

gl.glGenTextures(1, tmpTextureID, 0);
gl.glBindTexture(GL10.GL_TEXTURE_2D, tmpTextureID[0]);
textureIDs.put(material, tmpTextureID[0]);
GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, material.getTexture(),0);
material.getTexture().recycle();
gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
GL10.GL_LINEAR);
gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
GL10.GL_LINEAR);
}
}
}

@Override
public void draw(GL10 gl) {
super.draw(gl);

gl.glScalef(model.scale, model.scale, model.scale);
gl.glTranslatef(model.xpos, model.ypos, model.zpos);
gl.glRotatef(model.xrot, 1, 0, 0);
gl.glRotatef(model.yrot, 0, 1, 0);
gl.glRotatef(model.zrot, 0, 0, 1);

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);

gl.glDisable(GL10.GL_TEXTURE_2D);
int cnt = nonTexturedGroups.length;
for (int i = 0; i < cnt; i++) {
Group group = nonTexturedGroups[i];
Material mat = group.getMaterial();
if(mat != null) {
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR,
mat.specularlight);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT,
mat.ambientlight);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE,
mat.diffuselight);
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS,
mat.shininess);
}
gl.glVertexPointer(3,GL10.GL_FLOAT, 0, group.vertices);
gl.glNormalPointer(GL10.GL_FLOAT,0, group.normals);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, group.vertexCount);
}

gl.glEnable(GL10.GL_TEXTURE_2D); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

cnt = texturedGroups.length;
for (int i = 0; i < cnt; i++) {
Group group = texturedGroups[i];
Material mat = group.getMaterial();
if(mat != null) {
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_SPECULAR,
mat.specularlight);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_AMBIENT,
mat.ambientlight);
gl.glMaterialfv(GL10.GL_FRONT_AND_BACK, GL10.GL_DIFFUSE,
mat.diffuselight);
gl.glMaterialf(GL10.GL_FRONT_AND_BACK, GL10.GL_SHININESS,
mat.shininess);
if(mat.hasTexture()) {
gl.glTexCoordPointer(2,GL10.GL_FLOAT, 0, group.texcoords);
gl.glBindTexture(GL10.GL_TEXTURE_2D,
textureIDs.get(mat).intValue());
}
}
gl.glVertexPointer(3,GL10.GL_FLOAT, 0, group.vertices);
gl.glNormalPointer(GL10.GL_FLOAT,0, group.normals);
gl.glDrawArrays(GL10.GL_TRIANGLES, 0, group.vertexCount);
}

gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
}
}`

在我们的构造函数中,我们得到我们的模型,然后从非纹理组中分离纹理组以获得更好的性能。方法为我们所有的材质加载纹理。这个类中最主要的是draw()方法。超级语句后的前七个 gl 语句做我们模型的定位。接下来的两条语句和for循环画出了所有没有纹理的部分。该方法的其余部分绘制模型的纹理部分。

查看模型

我们名单上的下一个是ModelViewer.java。这个类负责加载和显示用户选择的模型,无论是从我们提供的模型还是从 SD 卡。这是一个很大的类,相当复杂。

变量声明

全局变量用于存储文件的位置类型(内置或外部)、菜单选项、每个模型的模型和模型 3D 实例、进度对话框和 ARToolkit 实例。

清单 8-23。ModelViewer.java变量

`public class ModelViewer extends AndARActivity implements SurfaceHolder.Callback
{
public static final int TYPE_INTERNAL = 0;
public static final int TYPE_EXTERNAL = 1;
public static final boolean DEBUG = false;
private final int MENU_SCALE = 0;
private final int MENU_ROTATE = 1;
private final int MENU_TRANSLATE = 2;
private final int MENU_SCREENSHOT = 3;

private int mode = MENU_SCALE;
private Model model;
private Model model2;
private Model model3;
private Model model4;
private Model model5;
private Model3D model3d;
private Model3D model3d2;
private Model3D model3d3;
private Model3D model3d4;
private Model3D model3d5;
private ProgressDialog waitDialog;
private Resources res;
ARToolkit artoolkit;`

构造器

这个类的构造函数如下所示。

清单 8-24。ModelViewer.java建造师

public ModelViewer() { super(false); }

onCreate()方法

我们文件的onCreate()方法通过 LightingRenderer.java 设置照明,获取应用的资源,将 ARToolkit 的一个实例分配给artoolkit,为表面视图设置触摸事件监听器,并为表面视图添加回调。

清单 8-25。onCreate()方法

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setNonARRenderer(new LightingRenderer()); res=getResources(); artoolkit = getArtoolkit(); getSurfaceView().setOnTouchListener(new TouchEventHandler()); getSurfaceView().getHolder().addCallback(this); }

捕捉异常和处理菜单选项

捕捉我们在别处没有显式捕捉到的任何异常。另外两种方法是非常常见的标准 Android 代码,用于创建菜单和监听用户在菜单上的活动。

清单 8-26。 捕捉异常并使用菜单

public void uncaughtException(Thread thread, Throwable ex) { System.out.println(""); } @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, MENU_TRANSLATE, 0, res.getText(R.string.translate)) .setIcon(R.drawable.translate); menu.add(0, MENU_ROTATE, 0, res.getText(R.string.rotate)) .setIcon(R.drawable.rotate); menu.add(0, MENU_SCALE, 0, res.getText(R.string.scale)) .setIcon(R.drawable.scale); menu.add(0, MENU_SCREENSHOT, 0, res.getText(R.string.take_screenshot)) .setIcon(R.drawable.screenshoticon); return true; } public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_SCALE: mode = MENU_SCALE; return true; case MENU_ROTATE: mode = MENU_ROTATE; return true; case MENU_TRANSLATE: mode = MENU_TRANSLATE; return true; case MENU_SCREENSHOT: new TakeAsyncScreenshot().execute(); return true; } return false; }

surfaceCreated()

surfaceCreated()在创建 SurfaceView 时被调用,它在模型加载时显示一个进度对话框。

清单 8-27。 surfaceCreated()

`@Override
public void surfaceCreated(SurfaceHolder holder) {
super.surfaceCreated(holder);

if(model == null) {
waitDialog = ProgressDialog.show(this, "",
getResources().getText(R.string.loading), true);
waitDialog.show();
new ModelLoader().execute();
}
}`

TouchEventHandler 内部类

这个内部类截获我们活动中发生的每一个触摸事件。它接受这种事件,然后适当地缩放、平移或旋转模型。

清单 8-28。 内部类 TouchEventHandler

class TouchEventHandler implements OnTouchListener { private float lastX=0; private float lastY=0; public boolean onTouch(View v, MotionEvent event) { if(model!=null) { switch(event.getAction()) { default: case MotionEvent.ACTION_DOWN: lastX = event.getX(); lastY = event.getY(); break; case MotionEvent.ACTION_MOVE: float dX = lastX - event.getX(); float dY = lastY - event.getY(); lastX = event.getX(); lastY = event.getY(); if(model != null) { switch(mode) { case MENU_SCALE: model.setScale(dY/100.0f); break; case MENU_ROTATE: model.setXrot(-1*dX); model.setYrot(-1*dY); break; case MENU_TRANSLATE: model.setXpos(dY/10f); model.setYpos(dX/10f); break; } } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: lastX = event.getX(); lastY = event.getY(); break; } } return true; } }

模型加载器内部类

In this ModelLoader inner class, we use a series of if else statements to determine the model that we need to load. We also set the different markers required for some of the inbuilt models. The default marker for some of the inbuilt models, and all external models are called android. If the model is from an external file, we first trim it before loading it. If it is an inbuilt model, we load it directly. In onPostExecute(), we register all the models, and dismiss the progress dialog box.

清单 8-29。 模型加载器

private class ModelLoader extends AsyncTask<Void, Void, Void> { private String modelName2patternName (String modelName) { String patternName = "android"; if (modelName.equals("plant.obj")) { patternName = "marker_rupee16"; `} else if (modelName.equals("chair.obj")) {
patternName = "marker_fisch16";
} else if (modelName.equals("tower.obj")) {
patternName = "marker_peace16";
} else if (modelName.equals("bench.obj")) {
patternName = "marker_at16";
} else if (modelName.equals("towergreen.obj")) {
patternName = "marker_hand16";
}
return patternName;
}
@Override
protected Void doInBackground(Void... params) {
Intent intent = getIntent();
Bundle data = intent.getExtras();
int type = data.getInt("type");
String modelFileName = data.getString("name");
BaseFileUtil fileUtil= null;
File modelFile=null;
switch(type) {
case TYPE_EXTERNAL:
fileUtil = new SDCardFileUtil();
modelFile = new File(URI.create(modelFileName));
modelFileName = modelFile.getName();

fileUtil.setBaseFolder(modelFile.getParentFile().getAbsolutePath());
break;
case TYPE_INTERNAL:
fileUtil = new AssetsFileUtility(getResources().getAssets());
fileUtil.setBaseFolder("models/");
break;
}
if(modelFileName.endsWith(".obj")) {
ObjParser parser = new ObjParser(fileUtil);
try {
if(Config.DEBUG)
Debug.startMethodTracing("AndObjViewer");
if(type == TYPE_EXTERNAL) {
BufferedReader modelFileReader = new BufferedReader(new
FileReader(modelFile));
String shebang = modelFileReader.readLine();

if(!shebang.equals("#trimmed")) {
File trimmedFile = new File(modelFile.getAbsolutePath()+".tmp");
BufferedWriter trimmedFileWriter = new BufferedWriter(new
FileWriter(trimmedFile));
Util.trim(modelFileReader, trimmedFileWriter);
if(modelFile.delete()) {
trimmedFile.renameTo(modelFile);
} }
}
if(fileUtil != null) {
BufferedReader fileReader =
fileUtil.getReaderFromName(modelFileName);
if(fileReader != null) {
model = parser.parse("Model", fileReader);
Log.w("ModelLoader", "model3d = new Model3D(model, " +
modelName2patternName(modelFileName) + ".patt");
model3d = new Model3D(model, modelName2patternName(modelFileName)

  • ".patt");
    }
    String modelFileName2 = "chair.obj";
    BufferedReader fileReader2 =
    fileUtil.getReaderFromName(modelFileName2);
    if(fileReader2 != null) {
    model2 = parser.parse("Chair", fileReader2);
    Log.w("ModelLoader", "model3d = new Model3D(model2, " +
    modelName2patternName(modelFileName2) + ".patt");
    model3d2 = new Model3D(model2,
    modelName2patternName(modelFileName2) + ".patt");
    } else {
    Log.w("ModelLoader", "no file reader");
    }
    String modelFileName3 = "towergreen.obj";
    BufferedReader fileReader3 =
    fileUtil.getReaderFromName(modelFileName3);
    if(fileReader3 != null) {
    model3 = parser.parse("towergreen", fileReader3);
    Log.w("ModelLoader", "model3d = new Model3D(model3, " +
    modelName2patternName(modelFileName3) + ".patt");
    model3d3 = new Model3D(model3,
    modelName2patternName(modelFileName3) + ".patt");
    } else {
    Log.w("ModelLoader", "no file reader");
    }
    String modelFileName4 = "tower.obj";
    BufferedReader fileReader4 =
    fileUtil.getReaderFromName(modelFileName4);
    if(fileReader4 != null) {
    model4 = parser.parse("tower", fileReader4);
    Log.w("ModelLoader", "model3d = new Model3D(model4, " +
    modelName2patternName(modelFileName4) + ".patt");
    model3d4 = new Model3D(model4,
    modelName2patternName(modelFileName4) + ".patt");
    } else {
    Log.w("ModelLoader", "no file reader");
    }
    String modelFileName5 = "plant.obj"; BufferedReader fileReader5 =
    fileUtil.getReaderFromName(modelFileName5);
    if(fileReader5 != null) {
    model5 = parser.parse("Plant", fileReader5);
    Log.w("ModelLoader", "model3d = new Model3D(model5, " +
    modelName2patternName(modelFileName5) + ".patt");
    model3d5 = new Model3D(model5,
    modelName2patternName(modelFileName5) + ".patt");
    } else {
    Log.w("ModelLoader", "no file reader");
    }
    }
    if(Config.DEBUG)
    Debug.stopMethodTracing();
    } catch (IOException e) {
    e.printStackTrace();
    } catch (ParseException e) {
    e.printStackTrace();
    }
    }
    return null;
    }
    @Override
    protected void onPostExecute(Void result) {
    super.onPostExecute(result);
    waitDialog.dismiss();

try {
if(model3d!=null) {
artoolkit.registerARObject(model3d);
artoolkit.registerARObject(model3d2);
artoolkit.registerARObject(model3d3);
artoolkit.registerARObject(model3d4);
artoolkit.registerARObject(model3d5);
}
} catch (AndARException e) {
e.printStackTrace();
}
startPreview();
}
}`

TakeAsyncScreenshot 内部类

TakeAsyncScreenshot内部类中,我们调用 AndAR 的内置截图功能。

清单 8-30。takeasync 截图

`class TakeAsyncScreenshot extends AsyncTask<Void, Void, Void> {

private String errorMsg = null;

@Override
protected Void doInBackground(Void... params) {
Bitmap bm = takeScreenshot();
FileOutputStream fos;
try {
fos = new FileOutputStream("/sdcard/AndARScreenshot"+new
Date().getTime()+".png");
bm.compress(CompressFormat.PNG, 100, fos);
fos.flush();
fos.close();
} catch (FileNotFoundException e) {
errorMsg = e.getMessage();
e.printStackTrace();
} catch (IOException e) {
errorMsg = e.getMessage();
e.printStackTrace();
}
return null;
}

protected void onPostExecute(Void result) {
if(errorMsg == null)
Toast.makeText(ModelViewer.this,
getResources().getText(R.string.screenshotsaved), Toast.LENGTH_SHORT ).show();
else
Toast.makeText(ModelViewer.this,
getResources().getText(R.string.screenshotfailed)+errorMsg, Toast.LENGTH_SHORT
).show();
};
}
}`

解析。mtl 文件

接下来我们有一个非常重要的课程。这个类负责解析。伴随。我们模型的 obj 文件。

清单 8-31。MtlParser.java??

`public class MtlParser {

private BaseFileUtil fileUtil; public MtlParser(Model model, BaseFileUtil fileUtil) {
this.fileUtil = fileUtil;
}

public void parse(Model model, BufferedReader is) {
Material curMat = null;
int lineNum = 1;
String line;
try {
for (line = is.readLine();
line != null;
line = is.readLine(), lineNum++)
{
line = Util.getCanonicalLine(line).trim();
if (line.length() > 0) {
if (line.startsWith("newmtl ")) {
String mtlName = line.substring(7);
curMat = new Material(mtlName);
model.addMaterial(curMat);
} else if(curMat == null) {
} else if (line.startsWith("# ")) {
} else if (line.startsWith("Ka ")) {
String endOfLine = line.substring(3);
curMat.setAmbient(parseTriple(endOfLine));
} else if (line.startsWith("Kd ")) {
String endOfLine = line.substring(3);
curMat.setDiffuse(parseTriple(endOfLine));
} else if (line.startsWith("Ks ")) {
String endOfLine = line.substring(3);
curMat.setSpecular(parseTriple(endOfLine));
} else if (line.startsWith("Ns ")) {
String endOfLine = line.substring(3);
curMat.setShininess(Float.parseFloat(endOfLine));
} else if (line.startsWith("Tr ")) {
String endOfLine = line.substring(3);
curMat.setAlpha(Float.parseFloat(endOfLine));
} else if (line.startsWith("d ")) {
String endOfLine = line.substring(2);
curMat.setAlpha(Float.parseFloat(endOfLine));
} else if(line.startsWith("map_Kd ")) {
String imageFileName = line.substring(7);
curMat.setFileUtil(fileUtil);
curMat.setBitmapFileName(imageFileName);
} else if(line.startsWith("mapKd ")) {
String imageFileName = line.substring(6);
curMat.setFileUtil(fileUtil);
curMat.setBitmapFileName(imageFileName);
}
} }
} catch (IOException e) {
e.printStackTrace();
}
}

private static float[] parseTriple(String str) {
String[] colorVals = str.split(" ");
float[] colorArr = new float[]{
Float.parseFloat(colorVals[0]),
Float.parseFloat(colorVals[1]),
Float.parseFloat(colorVals[2])};
return colorArr;
}
}`

这个类不是很复杂。基本上,该类逐行读取整个文件,并通过查看文件的开头来处理每一行。绝对第一条件确保该行实际上是一行,而不是一个空行。之后,嵌套的if else语句就出现了。

从现在开始提到的所有条件都来自嵌套语句,除非另有说明。第一个这样的条件通过查看该行是否以“newmtl”开头来检查该行是否是第一行。

if (line.startsWith("newmtl ")) { String mtlName = line.substring(7); curMat = new Material(mtlName); model.addMaterial(curMat);

下一个条件确保我们当前的材料不为空。

} else if(curMat == null) {

第三个用于忽略注释,因为它们在。mtl 文件。

} else if (line.startsWith("# ")) {

第四个条件查看该行是否为我们的模型指定了环境光,如果指定了,就设置它。

} else if (line.startsWith("Ka ")) { String endOfLine = line.substring(3); curMat.setAmbient(parseTriple(endOfLine));

第五个条件是看这条线是否为我们的模型指定了漫射光,如果指定了,就设置它。

} else if (line.startsWith("Kd ")) { String endOfLine = line.substring(3); curMat.setDiffuse(parseTriple(endOfLine));

第六个条件是看这条线是否为我们的模型指定了镜面反射光,如果是,就设置它。

} else if (line.startsWith("Ks ")) { String endOfLine = line.substring(3); curMat.setSpecular(parseTriple(endOfLine));

第七个条件检查该行是否指定了模型的亮度,如果指定了,就设置它。

} else if (line.startsWith("Ns ")) { String endOfLine = line.substring(3); curMat.setShininess(Float.parseFloat(endOfLine));

第八个和第九个条件检查该行是否为我们的模型指定了 alpha 值,如果指定了,就设置它。

} else if (line.startsWith("Tr ")) { String endOfLine = line.substring(3); curMat.setAlpha(Float.parseFloat(endOfLine)); } else if (line.startsWith("d ")) { String endOfLine = line.substring(2); curMat.setAlpha(Float.parseFloat(endOfLine));

第十个和第十一个条件检查该行是否为该模型指定了图像,如果是,就设置它。

} else if(line.startsWith("map_Kd ")) { String imageFileName = line.substring(7); curMat.setFileUtil(fileUtil); curMat.setBitmapFileName(imageFileName); } else if(line.startsWith("mapKd ")) { String imageFileName = line.substring(6); curMat.setFileUtil(fileUtil); curMat.setBitmapFileName(imageFileName);

方法末尾的catch语句捕捉到了IOException,这将由诸如文件未被找到或文件具有不利权限之类的情况触发。

} catch (IOException e) { e.printStackTrace(); }

floatparseTriple()被反复调用来帮助解析文件。

private static float[] parseTriple(String str) { String[] colorVals = str.split(" "); float[] colorArr = new float[]{ Float.parseFloat(colorVals[0]), Float.parseFloat(colorVals[1]), Float.parseFloat(colorVals[2])}; return colorArr; }

解析着。对象文件

接下来是另一个非常重要的类,ObjParser.java。它解析。obj 文件,至少在一定程度上。它不支持完整的。obj 规范。它支持以下内容:

  • 头顶
  • 法线顶点
  • 纹理坐标
  • 基本材料
  • 有限的纹理支持(通过 map_Kd,没有选项)
  • 面(不能省略面法线)

这样的支持对于我们的车型来说已经足够了。

清单 8-32。ObjParser.java??

`public class ObjParser {
private final int VERTEX_DIMENSIONS = 3;
private final int TEXTURE_COORD_DIMENSIONS = 2;

private BaseFileUtil fileUtil;

public ObjParser(BaseFileUtil fileUtil) {
this.fileUtil = fileUtil;
}

public Model parse(String modelName, BufferedReader is) throws IOException,
ParseException {
ArrayList<float[]> vertices = new ArrayList<float[]>(1000);
ArrayList<float[]> normals = new ArrayList<float[]>(1000);
ArrayList<float[]> texcoords = new ArrayList<float[]>();

Model model = new Model(); Group curGroup = new Group();
MtlParser mtlParser = new MtlParser(model,fileUtil);
SimpleTokenizer spaceTokenizer = new SimpleTokenizer();
SimpleTokenizer slashTokenizer = new SimpleTokenizer();
slashTokenizer.setDelimiter("/");

String line;
int lineNum = 1;
for (line = is.readLine();
line != null;
line = is.readLine(), lineNum++)
{
if (line.length() > 0) {
if (line.startsWith("#")) {
} else if (line.startsWith("v ")) {
String endOfLine = line.substring(2);
spaceTokenizer.setStr(endOfLine);
vertices.add(new float[]{
Float.parseFloat(spaceTokenizer.next()),
Float.parseFloat(spaceTokenizer.next()),
Float.parseFloat(spaceTokenizer.next())});
}
else if (line.startsWith("vt ")) {
String endOfLine = line.substring(3);
spaceTokenizer.setStr(endOfLine);
texcoords.add(new float[]{
Float.parseFloat(spaceTokenizer.next()),
Float.parseFloat(spaceTokenizer.next())});
}
else if (line.startsWith("f ")) {
String endOfLine = line.substring(2);
spaceTokenizer.setStr(endOfLine);
int faces = spaceTokenizer.delimOccurCount()+1;
if(faces != 3) {
throw new ParseException(modelName, lineNum, "only triangle faces
are supported");
}
for (int i = 0; i < 3; i++) {
String face = spaceTokenizer.next();
slashTokenizer.setStr(face);
int vertexCount = slashTokenizer.delimOccurCount()+1;
int vertexID=0;
int textureID=-1;
int normalID=0;
if(vertexCount == 2) {
vertexID = Integer.parseInt(slashTokenizer.next())-1;
normalID = Integer.parseInt(slashTokenizer.next())-1;
throw new ParseException(modelName, lineNum, "vertex normal
needed.");
} else if(vertexCount == 3) { vertexID = Integer.parseInt(slashTokenizer.next())-1;
String texCoord = slashTokenizer.next();
if(!texCoord.equals("")) {
textureID = Integer.parseInt(texCoord)-1;
}
normalID = Integer.parseInt(slashTokenizer.next())-1;
} else {
throw new ParseException(modelName, lineNum, "a faces needs
reference a vertex, a normal vertex and optionally a texture coordinate per
vertex.");
}
float[] vec;
try {
vec = vertices.get(vertexID);
} catch (IndexOutOfBoundsException ex) {
throw new ParseException(modelName, lineNum, "non existing vertex
referenced.");
}
if(vecnull)
throw new ParseException(modelName, lineNum, "non existing vertex
referenced.");
for (int j = 0; j < VERTEX_DIMENSIONS; j++)
curGroup.groupVertices.add(vec[j]);
if(textureID != -1) {
try {
vec = texcoords.get(textureID);
} catch (IndexOutOfBoundsException ex) {
throw new ParseException(modelName, lineNum, "non existing texture
coord referenced.");
}
if(vec
null)
throw new ParseException(modelName, lineNum, "non existing texture
coordinate referenced.");
for (int j = 0; j < TEXTURE_COORD_DIMENSIONS; j++)
curGroup.groupTexcoords.add(vec[j]);
}
try {
vec = normals.get(normalID);
} catch (IndexOutOfBoundsException ex) {
throw new ParseException(modelName, lineNum, "non existing normal
vertex referenced.");
}
if(vec==null)
throw new ParseException(modelName, lineNum, "non existing normal
vertex referenced.");
for (int j = 0; j < VERTEX_DIMENSIONS; j++)
curGroup.groupNormals.add(vec[j]);
}
}
else if (line.startsWith("vn ")) { String endOfLine = line.substring(3);
spaceTokenizer.setStr(endOfLine);
normals.add(new float[]{
Float.parseFloat(spaceTokenizer.next()),
Float.parseFloat(spaceTokenizer.next()),
Float.parseFloat(spaceTokenizer.next())});
} else if (line.startsWith("mtllib ")) {
String filename = line.substring(7);
String[] files = Util.splitBySpace(filename);
for (int i = 0; i < files.length; i++) {
BufferedReader mtlFile = fileUtil.getReaderFromName(files[i]);
if(mtlFile != null)
mtlParser.parse(model, mtlFile);
}
} else if(line.startsWith("usemtl ")) {
if(curGroup.groupVertices.size()>0) {
model.addGroup(curGroup);
curGroup = new Group();
}
curGroup.setMaterialName(line.substring(7));
} else if(line.startsWith("g ")) {
if(curGroup.groupVertices.size()>0) {
model.addGroup(curGroup);
curGroup = new Group();
}
}
}
}
if(curGroup.groupVertices.size()>0) {
model.addGroup(curGroup);
}
Iterator groupIt = model.getGroups().iterator();
while (groupIt.hasNext()) {
Group group = (Group) groupIt.next();
group.setMaterial(model.getMaterial(group.getMaterialName()));
}
return model;
}
}`

这个文件通过。obj 文件逐行。有一系列解析文件的 if else 块。每一行都会发生以下情况:

  1. 注释(以#开头)被忽略
  2. 顶点(从v开始)被添加到顶点ArrayList.
  3. 纹理坐标(从vt开始)被添加到纹理坐标ArrayList.
  4. 面(以f开始)被添加到组中。
  5. 法线(从vn开始)被添加到法线ArrayList.
  6. 对应。解析 mtl 文件(以mtllib开头)。
  7. 添加新材料并创建相应的组(从usemtl开始)。
  8. 创建新组(从g开始)。

完成后,它返回一个模型。

剖析异常

接下来是ParseException.java类,也就是在ObjParser.java中反复抛出的ParseException。这是一个自定义的异常,我们编写它是为了让我们能够轻松地解决解析过程中出现的问题。

清单 8-33。ParseException.java??

public class ParseException extends Exception { public ParseException(String file,int lineNumber, String msg) { super("Parse error in file "+file+"on line "+lineNumber+":"+msg); } }

很简单;它只是输出一条消息,通过参数填充消息的具体细节。

渲染

接下来是Renderer.java文件,它处理我们图形的大量绘图工作,包括一些复杂的 3D 内容。

清单 8-34。Renderer.java??

`public class Renderer implements GLSurfaceView.Renderer {

private final Vector models;
private final Vector3D cameraPosition = new Vector3D(0, 3, 50);
long frame,time,timebase=0;
public Renderer(Vector models) {
this.models = models;
}
public void addModel(Model3D model) {
if(!models.contains(model)) {
models.add(model);
} }
public void onDrawFrame(GL10 gl) {
if(ModelViewer.DEBUG) {
frame++;
time=System.currentTimeMillis();
if (time - timebase > 1000) {
Log.d("fps: ", String.valueOf(frame*1000.0f/(time-timebase)));
timebase = time;
frame = 0;
}
}
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity();
GLU.gluLookAt(gl, cameraPosition.x, cameraPosition.y, cameraPosition.z,
0, 0, 0, 0, 1, 0);
for (Iterator iterator = models.iterator(); iterator.hasNext()😉
{
Model3D model = iterator.next();
model.draw(gl);
}
}

public void onSurfaceChanged(GL10 gl, int width, int height) {
gl.glViewport(0,0,width,height);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadIdentity();
GLU.gluPerspective(gl, 45.0f, ((float)width)/height, 0.11f, 100f);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadIdentity();
}

public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glClearColor(1,1,1,1);

gl.glClearDepthf(1.0f);
gl.glEnable(GL10.GL_DEPTH_TEST);
gl.glDepthFunc(GL10.GL_LEQUAL);

gl.glEnable(GL10.GL_TEXTURE_2D);

gl.glShadeModel(GL10.GL_SMOOTH);
gl.glDisable(GL10.GL_COLOR_MATERIAL);
gl.glEnable(GL10.GL_BLEND);
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
gl.glEnable(GL10.GL_LIGHTING);
float[] ambientlight = {.6f, .6f, .6f, 1f};
float[] diffuselight = {1f, 1f, 1f, 1f};
float[] specularlight = {1f, 1f, 1f, 1f};
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_AMBIENT,
MemUtil.makeFloatBuffer(ambientlight)); gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE,
MemUtil.makeFloatBuffer(diffuselight));
gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_SPECULAR,
MemUtil.makeFloatBuffer(specularlight));
gl.glEnable(GL10.GL_LIGHT0);

for (Iterator iterator = models.iterator(); iterator.hasNext()😉
{
Model3D model = iterator.next();
model.init(gl);
}
}
}`

构造函数存储本地传递给它的模型。addModel()方法向我们的列表中添加了一个模型。如果应用设置为调试模式,则onDrawFrame()记录帧速率。不管应用的模式如何,onDrawFrame()方法也会更新显示给用户的内容。每当表面发生变化并应用与新的宽度和高度相关的变化时,就会调用onSurfaceChanged()方法。第一次创建表面时,onSurfaceCreated()方法进行初始设置。

SDCardFileUtil.java

接下来是SDCardFileUtil.java。这是BaseFileUtil的扩展,处理文件的读取。

清单 8-35。SDCardFileUtil.java??

`public class SDCardFileUtil extends BaseFileUtil {
public BufferedReader getReaderFromName(String name) {
if (baseFolder != null) {
try {
return new BufferedReader(new FileReader(new File(baseFolder, name)));
} catch (FileNotFoundException e) {
return null;
}
} else {
try {
return new BufferedReader(new FileReader(new File(name)));
} catch (FileNotFoundException e) {
return null;
}
}
}

public Bitmap getBitmapFromName(String name) { if (baseFolder != null) {
String path = new File(baseFolder,name).getAbsolutePath();
return BitmapFactory.decodeFile(path);
} else {
return BitmapFactory.decodeFile(name);
}
}
}`

第一个方法试图通过文件名获得一个BufferedReader,第二个方法试图通过文件名获得一个位图。

SimpleTokenizer.java

接下来是SimpleTokenizer.java class, which类在很多地方被用作Tokenizer来分隔字符串。

清单 8-36。SimpleTokenizer.java??

public class SimpleTokenizer { String str = ""; String delimiter = " "; int delimiterLength = delimiter.length(); int i =0; int j =0; public final String getStr() { return str; } public final void setStr(String str) { this.str = str; i =0; j =str.indexOf(delimiter); } public final String getDelimiter() { return delimiter; } public final void setDelimiter(String delimiter) { this.delimiter = delimiter; delimiterLength = delimiter.length(); } public final boolean hasNext() { return j >= 0; } public final String next() { if(j >= 0) { String result = str.substring(i,j); i = j + 1; j = str.indexOf(delimiter, i); `return result;
} else {
return str.substring(i);
}
}
public final String last() {
return str.substring(i);
}

public final int delimOccurCount() {
int result = 0;
if (delimiterLength > 0) {
int start = str.indexOf(delimiter);
while (start != -1) {
result++;
start = str.indexOf(delimiter, start + delimiterLength);
}
}
return result;
}
}`

这是一个简单的类。所有东西都来自标准的 Java 包,不需要导入任何东西。严格来说,这个类没有使用 Android API。你可以把它复制粘贴到一个普通的 Java 项目中,它将完美地工作。

Util.java

接下来是Util.java。这个类优化了我们的。obj 文件,以便下次可以更快地解析它们。

清单 8-37。Util.java??

`public class Util {
private static final Pattern trimWhiteSpaces = Pattern.compile("[\s]+");
private static final Pattern removeInlineComments = Pattern.compile("#");
private static final Pattern splitBySpace = Pattern.compile(" ");

public static final String getCanonicalLine(String line) {
line = trimWhiteSpaces.matcher(line).replaceAll(" ");
if(line.contains("#")) {
String[] parts = removeInlineComments.split(line);
if(parts.length > 0)
line = parts[0];
}
return line;
} public static String[] splitBySpace(String str) {
return splitBySpace.split(str);
}

public static void trim(BufferedReader in, BufferedWriter out) throws
IOException {
String line;
out.write("#trimmed\n");
for (line = in.readLine();
line != null;
line = in.readLine()) {
line = getCanonicalLine(line);
if(line.length()>0) {
out.write(line.trim());
out.write('\n');
}
}
in.close();
out.close();
}

public final static List fastSplit(final String text, char
separator, final boolean emptyStrings) {
final List result = new ArrayList();

if (text != null && text.length() > 0) {
int index1 = 0;
int index2 = text.indexOf(separator);
while (index2 >= 0) {
String token = text.substring(index1, index2);
result.add(token);
index1 = index2 + 1;
index2 = text.indexOf(separator, index1);
}

if (index1 < text.length() - 1) {
result.add(text.substring(index1));
}
}

return result;
}

}`

这又是标准 Java。它只是删除空白,行内注释等。以便下次更快地解析。

3D 矢量

现在我们来看最后一个 Java 文件,Vector3D.java。该文件使用三维矢量。这个类用于定位我们的虚拟 OpenGL 相机。这个摄像头和我们一直用的硬件摄像头很不一样。这是一个虚拟的摄像机,我们可以从中看到我们的模型。

清单 8-38。Vector3D.java??

`public class Vector3D implements Serializable {
public float x=0;
public float y=0;
public float z=0;

public Vector3D(float x, float y, float z) {
super();
this.x = x;
this.y = y;
this.z = z;
}
public float getX() {
return x;
}

public void setX(float x) {
this.x = x;
}

public float getY() {
return y;
}

public void setY(float y) {
this.y = y;
}

public float getZ() {
return z;
}

public void setZ(float z) {
this.z = z;
}

}`

所有的方法都是获取或者设置 x,y,或者 z。仅此而已。

XML 文件

既然我们已经完成了所有的 Java 文件,我们可以继续处理 XML 文件了。

Strings.xml

让我们从strings.xml开始。

清单 8-39。 strings.xml

`

PAAR Chapter 8
Select a model file:
Android market is not available, you need
install to OI File manager manually.

You need to install the OI File Manager
in order to use this application.

This file doesn't exist!
Unknown file type!
rotate
translate
scale

Loading. Please wait...
AndAR Model Viewer allows you to view wavefront
obj models on an Augmented Reality marker.

Take a screenshotScreenshot saved!Failed to save screenshot:
Select a model file
Instructions
Select an obj model file. (.obj)
Choose a model:
Help:
Custom model:
index.html
`

这个文件包含我们应用的所有预定义字符串。每个字符串的内容和名称应该为您提供它们的确切描述。

为行布局

现在让我们看看布局文件。我们有choose_model_row.xml,用在ModelChooser里。

清单 8-40。??【choose _ model _ row . XML】

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:padding="6dip"> <ImageView android:id="@+id/choose_model_row_icon" android:layout_width="wrap_content" android:layout_height="fill_parent" android:layout_marginRight="6dip" android:src="@drawable/ic_launcher" /> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/choose_model_row_text" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" android:gravity="center_vertical" android:paddingLeft="6dip" android:minHeight="?android:attr/listPreferredItemHeight" /> </LinearLayout>

我们将图标的 ImageView 和名称的 TextView 放在一起。这就是我们行的全部布局。

说明 _ 布局. xml

接下来是instructions_layout.xml,它是我们的指令活动背后的 XML 文件。

清单 8-41。 说明 _ 布局. 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"


`

这里我们有一个完全由 WebView 填充的线性布局来显示指令 HTML 文件。

列表标题

接下来我们有list_header.xml,顾名思义,它是我们列表的标题。

清单 8-42。 list_header.xml

<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/list_header_title" android:layout_width="fill_parent" android:layout_height="wrap_content" android:paddingTop="2dip" android:paddingBottom="2dip" android:paddingLeft="5dip" style="?android:attr/listSeparatorTextViewStyle" />

main.xml

最后我们有main.xml,用来显示信息。

清单 8-43。 main.xml

`
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"



`

HTML 帮助文件

这就把我们带到了 XML 文件的末尾。现在剩下的就是一个 HTML 文件,也就是我们的指令文件。它位于/assets/help/中。

清单 8-44。index.html??

`

<head> <style type="text/css"> h1 {font-size:20px;} h2 {font-size:16px;} </style> </head> <body> <p>This is the 3rd example application from the book Pro Android Augmented Reality by Raghav Sood, published by Apress. It projects models on a marker. You may either use the internal models or supply your own models in the <a href="http://en.wikipedia.org/wiki/Obj">wavefront obj format.</a></p> <ol> <li><a href="#firststeps">First steps</a></li> <li><a href="#screenshot">Taking a screenshot</a></li> <li><a href="#transforming">Transforming the model</a></li> <li><a href="#custommodels">Custom models</a></li> </ol> <h2><a name="firststeps">First steps</a></h2> <ul> <li>First print out the marker, upon which the models will be projected. The marker is located in the assets folder of the project source.</li> </ul> <ul> <li>Select one of the internal models.</li> <li>The app will start loading the model.</li> <li>After it has finished, you will see a live video stream from the camera.</li> <li>Now point the camera to the marker, you should see the model you selected.</li> </ul> <h2><a name="screenshot">Taking a screenshot</a></h2> <ul> <li>First press the menu key.</li> <li>Next press the button "Take a screenshot".</li> <li>The application will now process the image. It will notfiy you, when it's finished.</li>` `<li>The screenshot you just took can be found in the root folder of your sd-card. It will be named something like <i>AndARScreenshot1234.png</i></li> </ul> <h2><a name="transforming">Transforming the model</a></h2> <ul> <li>Press the menu key and select the desired transformation mode. You may either scale, rotate or translate the model.</li> <li>Scale: Slide you finger up and down the touch screen. This will enlarge and shorten the model, respectively.</li> <li>Rotate: Slide your finger horizontally and vertically, this will rotate your model correspondingly. </li> <li>Translate: Slide your finger horizontally and vertically, this will translate your model correspondingly. </li> </ul> <h2><a name="custommodels">Custom models</a></h2> The application is capable of showing custom wavefront obj models. Most 3d modelling software out there can export this format(e.g. 3ds max, Blender). There are currently some restrictions to the models: <ul> <li>Every face must have normals specified</li> <li>The object must be triangulated, this means exactly 3 vertices per face.</li> <li>Basic materials and textures are supported.</li> </ul> E.g. when exporting a model from blender make sure you check <i>Normals</i> and <i>Triangulate</i>. <h2>Attribution</h2> <ul> <li>This app contains code developed by the Android Open Source Project, released under the Apache License.</li> <li>This app contains models from <a href="http://resources.blogscopia.com/modelos_en.html">resources.blogscopia.com< /a> released under the <a href="http://creativecommons.org/licenses/by/3.0/">Creative Commons 3.0 Unported license</a>, see also: <a href="http://www.scopia.es">www.scopia.es</a>.</li> <li>This product includes software developed by the <a href="http://mij.oltrelinux.com/devel/simclist/">SimCList project</a>.</li> <li>This project includes code from the <a href=http://code.google.com/p/andar/AndAR</a> project.</li> </ul> </body> </html>`

已完成的 App

这就是你需要的可以在书中显示的所有文件!从这本书的页面下载这本书的源代码,或者从 http://github.com/RaghavSood/ProAndroidAugmentedReality 的 GitHub repo 获取图片。帕特和。obj +。项目顺利运行所需的 mtl 文件。

图 8-3 和 8-4 显示了应用的运行情况。

Image

图 8-3。app 的开启画面。

Image

图 8-4。 加载安卓模式。

结论

在这一章中,我们使用 AndAR 框架创建了一个全功能的 3D AR 对象查看器。我们的应用有能力加载内部和外部模型;以 3D 方式显示它们;并且允许用户调整大小、旋转和重新定位它们。在下一章中,我们将构建一个 AR 应用,探索 AR 的社交和游戏功能。

九、增强现实浏览器

欢迎来到第九章。这是本书的最后一章,涵盖了增强现实(AR)的不同方面,从制作基本应用,使用标记,覆盖小部件,到制作导航应用。这最后一章讨论了一个现实世界的 AR 浏览器的示例应用。这款浏览器类似于非常流行的 Wikitude 和 Layar 应用,但没有那么广泛。Wikitude 和 Layar 是经过长时间开发的非常强大的工具,提供了许多许多功能。我们的 AR 浏览器将相对简陋,但仍然非常非常强大和有用:

  • 它将有一个现场摄像机预览
  • 位于附近的 Twitter 帖子和维基百科文章的主题将显示在这个预览上
  • 将有一个小雷达可见,允许用户看到是否有任何其他覆盖在他们的视野之外
  • 当用户移动和旋转时,覆盖将被移入和移出视图
  • 用户可以设置数据采集半径,范围从 0 米到 100,000 米(100 公里)

图 9-1 显示应用正在运行,两个数据源的标记都可见。

Image

图 9-1。app 运行时的截图。

为了完成这一切,我们将编写自己的迷你 AR 引擎,并使用两个免费资源来获取维基百科和 Twitter 数据。与第八章相比,这段代码不是很长,但有些是新的,尤其是移动叠加部分。事不宜迟,让我们开始编码吧。

XML

这个应用中的 xml 只包含 strings.xml 和菜单的 XML。我们将快速输入这些内容,然后转到 Java 代码。

strings.xml

清单 9-1。 strings.xml

<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Pro Android AR Chapter 9</string> </resources>

字符串app_name仅仅存储我们的应用的名称。该名称显示在启动器中的图标下。

现在我们来看看 menu.xml。

清单 9-2。 menu.xml

`







`

第一项是显示和隐藏雷达的切换,雷达将用于显示用户视野之外的对象的图标。第二项是切换显示和隐藏 SeekBar 小部件,允许用户调整推文和维基百科信息的半径。

有了这些 XML,我们可以继续我们应用的 Java 代码了。

Java 代码

在这个应用中,我们将看看 Java 代码的格式,其中不同的类按功能分组。所以我们将依次查看所有的数据解析类,依此类推。

活动和增强视图

感官活性

让我们从应用的基础部分开始。我们有一个SensorsActivity,扩展了标准的 android ActivitySensorsActivity没有用户界面。AugmentedActivity然后扩展这个SensorsActivity,这个【】又扩展了MainActivity,也就是最终显示给用户的Activity。所以我们先来看看SensorsActivity

清单 9-3。SensorsActivity.java全局变量

public class SensorsActivity extends Activity implements SensorEventListener, LocationListener { private static final String TAG = "SensorsActivity"; private static final AtomicBoolean computing = new AtomicBoolean(false); `private static final int MIN_TIME = 30*1000;
private static final int MIN_DISTANCE = 10;

private static final float temp[] = new float[9];
private static final float rotation[] = new float[9];
private static final float grav[] = new float[3];
private static final float mag[] = new float[3];

private static final Matrix worldCoord = new Matrix();
private static final Matrix magneticCompensatedCoord = new Matrix();
private static final Matrix xAxisRotation = new Matrix();
private static final Matrix magneticNorthCompensation = new Matrix();

private static GeomagneticField gmf = null;
private static float smooth[] = new float[3];
private static SensorManager sensorMgr = null;
private static List sensors = null;
private static Sensor sensorGrav = null;
private static Sensor sensorMag = null;
private static LocationManager locationMgr = null;`

第一个变量只是一个包含我们的类名的TAG常量。第二个是computing,类似于一个标志,用于检查任务当前是否正在进行。MIN_TIMEMIN_DISTANCE指定位置更新之间的最小时间和距离。在上面的四个浮点数中,第一个是旋转时使用的临时数组,第二个存储最终旋转的矩阵,grav存储重力数字,mag存储磁场数字。在其后的四个矩阵中,worldCoord存储设备在世界上的位置,magneticCompensatedCoordmagneticNorthCompensation用于补偿地理北极和磁北极之间的差异,而xAxisRotation用于存储沿 X 轴旋转 90 度后的矩阵。之后,gmf被用来存储GeomagneticField的一个实例。当对来自gravmag的值使用低通滤波器时,使用smooth阵列。sensorMgr是一个SensorManager对象;sensors是传感器列表。sensorGravsensorMag将在设备上存储默认的重力(加速度计)和磁性(指南针)传感器,以防设备有多个传感器。locationMgrLocationManager的一个实例。

现在让我们来看看我们的方法。在这个特定的Activity中,我们不需要在onCreate(),中做任何事情,所以我们只需要实现它的一个基本版本,这样扩展它的类就可以使用它。我们的主要工作是用onStart()方法完成的:

清单 9-4。【SensorsActivity.java】??onCreate()和 onStart()

`@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

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

double angleX = Math.toRadians(-90);
double angleY = Math.toRadians(-90);

xAxisRotation.set( 1f,
0f,
0f,
0f,
(float) Math.cos(angleX),
(float) -Math.sin(angleX),
0f,
(float) Math.sin(angleX),
(float) Math.cos(angleX));

try {
sensorMgr = (SensorManager) getSystemService(SENSOR_SERVICE);

sensors = sensorMgr.getSensorList(Sensor.TYPE_ACCELEROMETER);

if (sensors.size() > 0) sensorGrav = sensors.get(0);

sensors = sensorMgr.getSensorList(Sensor.TYPE_MAGNETIC_FIELD);

if (sensors.size() > 0) sensorMag = sensors.get(0);

sensorMgr.registerListener(this, sensorGrav, SensorManager.SENSOR_DELAY_NORMAL);
sensorMgr.registerListener(this, sensorMag, SensorManager.SENSOR_DELAY_NORMAL);

locationMgr = (LocationManager)
getSystemService(Context.LOCATION_SERVICE);
locationMgr.requestLocationUpdates(LocationManager.GPS_PROVIDER, MIN_TIME, MIN_DISTANCE, this);

try {

try {
Location gps=locationMgr.getLastKnownLocation(LocationManager.GPS_PROVIDER); Location
network=locationMgr.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
if(gps!=null)
{
onLocationChanged(gps);
}
else if (network!=null)
{
onLocationChanged(network);
}
else
{
onLocationChanged(ARData.hardFix);
}
} catch (Exception ex2) {
onLocationChanged(ARData.hardFix);
}

gmf = new GeomagneticField((float)
ARData.getCurrentLocation().getLatitude(),
(float)
ARData.getCurrentLocation().getLongitude(),
(float)
ARData.getCurrentLocation().getAltitude(),
System.currentTimeMillis());
angleY = Math.toRadians(-gmf.getDeclination());

synchronized (magneticNorthCompensation) {

magneticNorthCompensation.toIdentity();

magneticNorthCompensation.set( (float) Math.cos(angleY),
0f,
(float) Math.sin(angleY),
0f,
1f,
0f,
(float) -Math.sin(angleY),
0f,
(float) Math.cos(angleY));

magneticNorthCompensation.prod(xAxisRotation);
}
} catch (Exception ex) {
ex.printStackTrace();
}
} catch (Exception ex1) {
try {
if (sensorMgr != null) {
sensorMgr.unregisterListener(this, sensorGrav); sensorMgr.unregisterListener(this, sensorMag);
sensorMgr = null;
}
if (locationMgr != null) {
locationMgr.removeUpdates(this);
locationMgr = null;
}
} catch (Exception ex2) {
ex2.printStackTrace();
}
}
}`

正如代码前面提到的,onCreate()方法只是一个默认的实现。另一方面,onStart()方法包含大量传感器和位置相关代码。我们从设置xAxisRotation矩阵的值开始。然后我们设置sensorMagsensorGrav的值,然后注册这两个传感器。然后我们给gmf赋值,并将angleY的值重新分配给负磁偏角,负磁偏角是真北(北极)和磁北(目前位于格陵兰海岸附近,以大约 40 英里/年的速度向西伯利亚移动)之差gmf的弧度。在重新分配之后,我们在同步块中有了一些代码。该代码用于首先设置magneticNorthCompensation的值,然后乘以xAxisRotation。在这之后,我们有一个catch块,接着是另一个catch块,其中有一个try块。这个try块的代码只是试图注销传感器和位置监听器。

这个文件中的下一个是我们的onStop()方法,它与本书前面使用的onResume()onStop()方法相同。我们只是用它来放开传感器和 GPS,以节省用户的电池寿命,并在不需要时停止收集数据。

清单 9-5。SensorsActivity.java

*`@Override
protected void onStop() {
super.onStop();

try {
try {
sensorMgr.unregisterListener(this, sensorGrav);
sensorMgr.unregisterListener(this, sensorMag);
} catch (Exception ex) {
ex.printStackTrace();
}
sensorMgr = null;

try { locationMgr.removeUpdates(this);
} catch (Exception ex) {
ex.printStackTrace();
}
locationMgr = null;
} catch (Exception ex) {
ex.printStackTrace();
}
}* *在onStop()方法之后,我们有了从三个传感器获取数据的方法。如果你看一下之前给出的类声明,你会注意到这次我们为整个类实现了SensorEventListenerLocationListener`,而不是像我们在之前的应用中那样为它们编写小代码块。我们这样做是为了让任何扩展这个类的类都可以容易地覆盖与传感器相关的方法。

清单 9-6。SensorsActivity.java 监听着传感器

`public void onSensorChanged(SensorEvent evt) {
if (!computing.compareAndSet(false, true)) return;

if (evt.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
smooth = LowPassFilter.filter(0.5f, 1.0f, evt.values, grav);
grav[0] = smooth[0];
grav[1] = smooth[1];
grav[2] = smooth[2];
} else if (evt.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
smooth = LowPassFilter.filter(2.0f, 4.0f, evt.values, mag);
mag[0] = smooth[0];
mag[1] = smooth[1];
mag[2] = smooth[2];
}

SensorManager.getRotationMatrix(temp, null, grav, mag);

SensorManager.remapCoordinateSystem(temp, SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X, rotation);

worldCoord.set(rotation[0], rotation[1], rotation[2], rotation[3], rotation[4], rotation[5], rotation[6], rotation[7], rotation[8]);

magneticCompensatedCoord.toIdentity();

synchronized (magneticNorthCompensation) {
magneticCompensatedCoord.prod(magneticNorthCompensation);
}

magneticCompensatedCoord.prod(worldCoord); magneticCompensatedCoord.invert();

ARData.setRotationMatrix(magneticCompensatedCoord);

computing.set(false);
}

public void onProviderDisabled(String provider) {
//Not Used
}

public void onProviderEnabled(String provider) {
//Not Used
}

public void onStatusChanged(String provider, int status, Bundle extras) {
//Not Used
}

public void onLocationChanged(Location location) {
ARData.setCurrentLocation(location);
gmf = new GeomagneticField((float)
ARData.getCurrentLocation().getLatitude(),
(float) ARData.getCurrentLocation().getLongitude(),
(float) ARData.getCurrentLocation().getAltitude(),
System.currentTimeMillis());

double angleY = Math.toRadians(-gmf.getDeclination());

synchronized (magneticNorthCompensation) {
magneticNorthCompensation.toIdentity();

magneticNorthCompensation.set((float) Math.cos(angleY),
0f,
(float) Math.sin(angleY),
0f,
1f,
0f,
(float) -Math.sin(angleY),
0f,
(float) Math.cos(angleY));

magneticNorthCompensation.prod(xAxisRotation);
}
}

public void onAccuracyChanged(Sensor sensor, int accuracy) {
if (sensornull) throw new NullPointerException(); if(sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD && accuracySensorManager.SENSOR_STATUS_UNRELIABLE) {
Log.e(TAG, "Compass data unreliable");
}
}
}`

这段代码中有六种方法。这些是onSensorChanged()onProviderDisabled()onProviderEnabled()onStatusChangedonLocationChanged()onAccuracyChanged()onProviderDisabled()onProviderEnabled()onStatusChanged()没有被使用,但是仍然在那里,因为它们必须被实现。

现在让我们来看一下使用的三个。在onSensorChanged()中,我们首先从罗盘和加速度计中获取传感器值,并在存储之前通过低通滤波器。低通滤波器将在本章稍后编写代码时详细讨论和解释。存储这些值后,我们找出真实世界的坐标,并将它们存储在临时数组中。之后,我们立即重新映射坐标,以便在横向模式下使用 Android 设备,并将其存储在旋转数组中。然后,我们通过将数据转移到worldCoord矩阵中,将旋转数组转换为矩阵。然后我们将magneticCompensatedCoord乘以magneticNorthCompensation,再将magneticCompensatedCoord乘以worldCoord。然后magneticCompensatedCoord被反转,并设置为ARData的旋转矩阵。这个旋转矩阵将用于将我们的推文和维基百科文章的纬度和经度转换为 X 和 Y 坐标,以便显示。

onLocationChanged()中,我们首先更新ARData中的位置,用新数据重新计算gmf,然后执行与在onStart()中相同的代码。

onAccuracyChanged()中,我们首先检查数据是否为null。如果是,则抛出一个NullPointerException。如果数据不是null,指南针似乎变得不可靠,我们给LogCat添加一条错误消息来说明这一点。

增强视图

在我们继续讨论AugmentedActivity之前,我们需要创建AugmentedView,这是我们在 Android 框架中找到的View类的自定义扩展。它被设计用来绘制雷达,控制数据半径的缩放栏和在我们的相机预览上显示数据的标记。让我们从类和全局变量声明开始。

清单 9-7。 声明增强视图及其变量

public class AugmentedView extends View { private static final AtomicBoolean drawing = new AtomicBoolean(false); private static final Radar radar = new Radar(); private static final float[] locationArray = new float[3]; private static final List<Marker> cache = new ArrayList<Marker>(); private static final TreeSet<Marker> updated = new TreeSet<Marker>(); private static final int COLLISION_ADJUSTMENT = 100;

AtomicBoolean绘图是检查绘图过程当前是否正在进行的标志。radarRadar类的一个实例,在我们完成所有活动后,我们将在本书的后面写它。locationArray用于存储我们稍后工作的标记的位置。cache是用来,嗯,画画的时候临时缓存。updated用于更新我们从维基百科文章和推文中的数据源获得的数据。COLLISION_ADJUSTMENT用于调整屏幕上标记的位置,使它们不会相互重叠。

现在让我们看看它的构造函数和onDraw()方法。

清单 9-8。onDraw()方法和 AugmentedView 的构造函数

`public AugmentedView(Context context) {
super(context);
}

@Override
protected void onDraw(Canvas canvas) {
if (canvas==null) return;

if (drawing.compareAndSet(false, true)) {
List collection = ARData.getMarkers();

cache.clear();
for (Marker m : collection) {
m.update(canvas, 0, 0);
if (m.isOnRadar()) cache.add(m);
}
collection = cache;

if (AugmentedActivity.useCollisionDetection)
adjustForCollisions(canvas,collection);

ListIterator iter =
collection.listIterator(collection.size());
while (iter.hasPrevious()) {
Marker marker = iter.previous();
marker.draw(canvas); }
if (AugmentedActivity.showRadar) radar.draw(canvas);
drawing.set(false);
}
}`

这个类的构造函数仅仅通过super()将它绑定到视图。在onDraw()方法中,我们首先将雷达上的所有标记添加到缓存变量中,然后将其复制到集合中。然后对标记进行碰撞调整(详见下一段代码清单),最后绘制所有标记,更新雷达。

现在让我们来看看调整碰撞标记的代码:

清单 9-9。 调整碰撞

`private static void adjustForCollisions(Canvas canvas, List collection)
{
updated.clear();
for (Marker marker1 : collection) {
if (updated.contains(marker1) || !marker1.isInView()) continue;

int collisions = 1;
for (Marker marker2 : collection) {
if (marker1.equals(marker2) || updated.contains(marker2) ||
!marker2.isInView()) continue;

if (marker1.isMarkerOnMarker(marker2)) {
marker2.getLocation().get(locationArray);
float y = locationArray[1];
float h = collisions*COLLISION_ADJUSTMENT;
locationArray[1] = y+h;
marker2.getLocation().set(locationArray);
marker2.update(canvas, 0, 0);
collisions++;
updated.add(marker2);
}
}
updated.add(marker1);
}
}
} //Closes class`

我们使用此代码来检查一个或多个标记是否与另一个标记重叠,然后调整它们的位置数据,以便在绘制它们时,它们不会重叠。我们使用来自marker类的方法(在本章后面写)来检查标记是否重叠,然后适当地调整它们在locationArray中的位置。

增加活力

现在我们已经写完了AugmentedView,我们可以开始写AugmentedActivity了。我们必须首先扩展视图类,因为我们将在AugmentedActivity中使用AugmentedView。让我们从类和全局变量开始。

清单 9-10。 声明 AugmentedActivity 及其全局变量

`public class AugmentedActivity extends SensorsActivity implements
OnTouchListener {
private static final String TAG = "AugmentedActivity";
private static final DecimalFormat FORMAT = new DecimalFormat("#.##");
private static final int ZOOMBAR_BACKGROUND_COLOR =
Color.argb(125,55,55,55);
private static final String END_TEXT =
FORMAT.format(AugmentedActivity.MAX_ZOOM)+" km";
private static final int END_TEXT_COLOR = Color.WHITE;

protected static WakeLock wakeLock = null;
protected static CameraSurface camScreen = null;
protected static VerticalSeekBar myZoomBar = null;
protected static TextView endLabel = null;
protected static LinearLayout zoomLayout = null;
protected static AugmentedView augmentedView = null;

public static final float MAX_ZOOM = 100; //in KM
public static final float ONE_PERCENT = MAX_ZOOM/100f;
public static final float TEN_PERCENT = 10fONE_PERCENT;
public static final float TWENTY_PERCENT = 2f
TEN_PERCENT;
public static final float EIGHTY_PERCENTY = 4f*TWENTY_PERCENT;

public static boolean useCollisionDetection = true;
public static boolean showRadar = true;
public static boolean showZoomBar = true;`

TAG在输出到 LogCat 时用作字符串常量。FORMAT用于在雷达上显示当前半径时格式化输出。ZOOMBAR_BACKGROUND_COLOR是我们用来允许用户改变半径的滑块背景颜色的ARGB_8888定义。END_TEXT是我们需要在雷达上显示的格式化文本。END_TEXT_COLOREND_TEXT的颜色。wakeLockcamScreenmyZoomBarendLabelzoomLayoutaugmentedView是我们需要的类的对象。它们目前都被赋予了一个null值,将在本章稍后进行初始化。MAX_ZOOM是我们的半径极限,以公里为单位。接下来的四个浮动是这个最大半径限制的不同百分比。useCollisionDetection是一个标志,允许我们启用或禁用标记的碰撞检测。showRadar是一个标志切换雷达的可见性。showZoomBar执行相同的切换,除了对控制半径的 seekbar 执行此操作。

现在我们来看看这个活动的onCreate()方法:

清单 9-11。 【增龄性 onCreate()

`@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

camScreen = new CameraSurface(this);
setContentView(camScreen);

augmentedView = new AugmentedView(this);
augmentedView.setOnTouchListener(this);
LayoutParams augLayout = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
addContentView(augmentedView,augLayout);

zoomLayout = new LinearLayout(this);

zoomLayout.setVisibility((showZoomBar)?LinearLayout.VISIBLE:LinearLayout.GONE);
zoomLayout.setOrientation(LinearLayout.VERTICAL);
zoomLayout.setPadding(5, 5, 5, 5);
zoomLayout.setBackgroundColor(ZOOMBAR_BACKGROUND_COLOR);

endLabel = new TextView(this);
endLabel.setText(END_TEXT);
endLabel.setTextColor(END_TEXT_COLOR);
LinearLayout.LayoutParams zoomTextParams = new
LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
zoomLayout.addView(endLabel, zoomTextParams);

myZoomBar = new VerticalSeekBar(this);
myZoomBar.setMax(100);
myZoomBar.setProgress(50);
myZoomBar.setOnSeekBarChangeListener(myZoomBarOnSeekBarChangeListener);
LinearLayout.LayoutParams zoomBarParams = new
LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT);
zoomBarParams.gravity = Gravity.CENTER_HORIZONTAL;
zoomLayout.addView(myZoomBar, zoomBarParams);

FrameLayout.LayoutParams frameLayoutParams = new
FrameLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.FILL_PARENT, Gravity.RIGHT);
addContentView(zoomLayout,frameLayoutParams);

updateDataOnZoom(); PowerManager pm = (PowerManager)
getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "DimScreen");
}`

我们首先将CameraSurface的一个实例分配给camScreen。我们将在本章稍后写CameraSurface,但基本上它是关于相机表面视图的设置,就像我们在前面的章节中多次做的那样。然后,我们将基本内容视图设置为这个CameraSurface。之后,我们创建一个AugmentedView的实例,将其布局参数设置为WRAP_CONTENT,并将其添加到屏幕上。然后我们将 SeekBar 和END_TEXT的基本布局添加到屏幕上。然后,我们将 SeekBar 添加到屏幕上,并调用一个方法来更新数据。最后,我们使用PowerManager来获取一个WakeLock来保持屏幕打开,但是如果不使用的话就把它调暗。

然后我们有onPause(onResume()方法,在其中我们简单地释放和重新获取WakeLock:

清单 9-12。 onPause()和 onResume()

`@Override
public void onResume() {
super.onResume();

wakeLock.acquire();
}

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

wakeLock.release();
}`

现在我们有了我们的onSensorChanged()方法:

清单 9-13。 onSensorChanged()

`@Override
public void onSensorChanged(SensorEvent evt) {
super.onSensorChanged(evt);

if (evt.sensor.getType() == Sensor.TYPE_ACCELEROMETER ||
evt.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD)
{
augmentedView.postInvalidate();
}
}`

我们用这种方法监听罗盘和加速计传感器上的变化。如果这些传感器中的任何一个发生变化,我们通过调用postInvalidate()来使augmentedView无效。它自动调用invalidate(),后者调用视图的onDraw()

然后我们有了处理 SeekBar 变化的方法:

清单 9-14。 处理 SeekBar

`private OnSeekBarChangeListener myZoomBarOnSeekBarChangeListener = new OnSeekBarChangeListener() {
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
updateDataOnZoom();
camScreen.invalidate();
}

public void onStartTrackingTouch(SeekBar seekBar) {
//Not used
}

public void onStopTrackingTouch(SeekBar seekBar) {
updateDataOnZoom();
camScreen.invalidate();
}
};`

在 SeekBar 被更改(onProgressChanged())时调用的方法中,在它停止被更改(onStopTrackingTouch())后,我们通过调用updateDataOnZoom()来更新我们的数据,然后使相机预览无效。

现在我们有一个方法来计算我们的应用的缩放级别。我们称之为缩放级别,但它实际上是我们显示数据的半径。只是缩放级别比半径级别更容易记忆和说出。

清单 9-15。 计算缩放级别

`private static float calcZoomLevel(){
int myZoomLevel = myZoomBar.getProgress();
float out = 0;

float percent = 0;
if (myZoomLevel <= 25) {
percent = myZoomLevel/25f;
out = ONE_PERCENTpercent;
} else if (myZoomLevel > 25 && myZoomLevel <= 50) {
percent = (myZoomLevel-25f)/25f;
out = ONE_PERCENT+(TEN_PERCENT
percent); } else if (myZoomLevel > 50 && myZoomLevel <= 75) {
percent = (myZoomLevel-50f)/25f;
out = TEN_PERCENT+(TWENTY_PERCENTpercent);
} else {
percent = (myZoomLevel-75f)/25f;
out = TWENTY_PERCENT+(EIGHTY_PERCENTY
percent);
}
return out;
}`

我们首先将搜索栏上的进度作为缩放级别。然后,我们创建 float out来存储最终结果,创建 float percent来存储百分比。然后,我们有一些简单的数学来确定使用的半径。我们使用这些类型的计算,因为它允许用户设置半径,甚至以米为单位。你越往上走,半径设置就越不精确。最后,我们返回out作为当前缩放级别。

我们现在有了处理触摸和更新数据的方法。

清单 9-16。 更新数据和处理触摸

`protected void updateDataOnZoom() {
float zoomLevel = calcZoomLevel();
ARData.setRadius(zoomLevel);
ARData.setZoomLevel(FORMAT.format(zoomLevel));
ARData.setZoomProgress(myZoomBar.getProgress());
}

public boolean onTouch(View view, MotionEvent me) {
for (Marker marker : ARData.getMarkers()) {
if (marker.handleClick(me.getX(), me.getY())) {
if (me.getAction() == MotionEvent.ACTION_UP)
markerTouched(marker);
return true;
}
}
return super.onTouchEvent(me);
};

protected void markerTouched(Marker marker) {
Log.w(TAG, "markerTouched() not implemented.");
}
}`

updateDataOnZoom()中,我们获得缩放级别,将半径设置为新的缩放级别,并更新缩放级别和搜索栏进度的文本,所有这些都在ARData中完成。在onTouch()中,我们检查一个标记是否被触摸,并从那里调用markerTouched()。在那之后,markerTouched()向 LogCat 发出一条消息,说我们目前在markerTouched()中什么都不做。

这就把我们带到了AugmentedActivity的结尾。现在我们需要写我们最后的活动类:MainActivity

主要活动

让我们再次从类和全局变量声明开始:

清单 9-17。 声明类和全局变量

public class MainActivity extends AugmentedActivity { private static final String TAG = "MainActivity"; private static final String locale = "en"; private static final BlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(1); private static final ThreadPoolExecutor exeService = new ThreadPoolExecutor(1, 1, 20, TimeUnit.SECONDS, queue); private static final Map<String,NetworkDataSource> sources = new ConcurrentHashMap<String,NetworkDataSource>();

与我们之前编写的课程的目的相同。locale字符串将两个字母代码中的区域设置存储为英语。您也可以使用Locale.getDefault().getLanguage()来获取区域设置,但最好将其保留为“en”,因为我们使用它来获取附近的 Twitter 和 Wikipedia 数据,并且我们的数据源可能不支持所有语言。为了简化我们的线程,我们使用了作为BlockingQueue实例的queue变量。exeService是一个ThreadPoolExecutor,它的工作队列是queue。最后,我们有一个叫做sourcesMap,它将存储数据源。

现在让我们看看这个类的onCreate()onStart()方法:

清单 9-18。 onCreate()和 onStart()

`@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LocalDataSource localData = new LocalDataSource(this.getResources());
ARData.addMarkers(localData.getMarkers());

NetworkDataSource twitter = new TwitterDataSource(this.getResources());
sources.put("twitter", twitter);
NetworkDataSource wikipedia = new
WikipediaDataSource(this.getResources());
sources.put("wiki", wikipedia);
}

@Override
public void onStart() {
super.onStart(); Location last = ARData.getCurrentLocation();
updateData(last.getLatitude(), last.getLongitude(), last.getAltitude());
}`

onCreate()中,我们首先创建一个LocalDataSource类的实例,并将其标记添加到ARData。然后我们为 Twitter 和 Wikipedia 创建一个NetworkDataSource,并将它们添加到 sources 地图中。在onStart()中,我们获得最后的位置数据,并用它更新我们的数据。

至此,我们现在可以进入代码的菜单部分了:

清单 9-19。 使用菜单

`@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
Log.v(TAG, "onOptionsItemSelected() item="+item);
switch (item.getItemId()) {
case R.id.showRadar:
showRadar = !showRadar;
item.setTitle(((showRadar)? "Hide" : "Show")+" Radar");
break;
case R.id.showZoomBar:
showZoomBar = !showZoomBar;
item.setTitle(((showZoomBar)? "Hide" : "Show")+" Zoom Bar");

zoomLayout.setVisibility((showZoomBar)?LinearLayout.VISIBLE:LinearLayout.GONE);
break;
case R.id.exit:
finish();
break;
}
return true;
}`

这是标准的 Android 代码,我们以前已经使用过无数次了。我们简单地从 XML 菜单资源创建菜单,然后监听菜单上的点击。对于显示雷达/缩放栏选项,我们只需切换它们的显示,对于退出选项,我们退出。

现在让我们来看看位置更新和处理触摸。

清单 9-20。 位置改变和触摸输入

`@Override
public void onLocationChanged(Location location) {
super.onLocationChanged(location);

updateData(location.getLatitude(), location.getLongitude(), location.getAltitude());
}

@Override
protected void markerTouched(Marker marker) {
Toast t = Toast.makeText(getApplicationContext(), marker.getName(), Toast.LENGTH_SHORT);
t.setGravity(Gravity.CENTER, 0, 0);
t.show();
}`

当我们得到一个新的location对象时,我们用它来更新数据。我们覆盖了markerTouched()方法来显示一个包含被触摸的标记的详细信息的祝酒词。

现在让我们看看这个类对updateDataOnZoom()的实现:

清单 9-21。 updateDataOnZoom()

@Override protected void updateDataOnZoom() { super.updateDataOnZoom(); Location last = ARData.getCurrentLocation(); updateData(last.getLatitude(),last.getLongitude(),last.getAltitude()); }

updateDataOnZoom()的这个实现中,我们获取位置,然后调用updateData()并向其传递新的位置信息。

现在我们来看看updateData()方法:

清单 9-22。 updateData()

`private void updateData(final double lat, final double lon, final double alt) {
try {
exeService.execute(
new Runnable() {

public void run() {
for (NetworkDataSource source : sources.values())
download(source, lat, lon, alt);
}
} );
} catch (RejectedExecutionException rej) {
Log.w(TAG, "Not running new download Runnable, queue is full.");
} catch (Exception e) {
Log.e(TAG, "Exception running download Runnable.",e);
}
}`

在这个方法中,我们试图使用一个Runnable来下载我们需要的数据,以显示 Twitter 帖子和 Wikipedia 文章。如果遇到一个RejectedExecutionException,就会向 LogCat 发送一条消息,告诉它队列已满,现在无法下载数据。如果遇到任何其他异常,Logcat 中会显示另一条消息。

最后,我们有了download()方法:

清单 9-23。 下载()

`private static boolean download(NetworkDataSource source, double lat, double
lon, double alt) {
if (source==null) return false;

String url = null;
try {
url = source.createRequestURL(lat, lon, alt,
ARData.getRadius(), locale);
} catch (NullPointerException e) {
return false;
}

List markers = null;
try {
markers = source.parse(url);
} catch (NullPointerException e) {
return false;
}

ARData.addMarkers(markers);
return true;
}
}`

download方法中,我们首先检查源是否是null。如果是,我们返回false。如果不是null,我们构造一个 URL 来获取数据。之后,我们解析来自 URL 的结果,并将获得的数据存储在列表标记中。该数据然后通过ARData.addMarkers添加到ARData

这使我们结束了所有的活动。我们现在将编写代码来获取 Twitter 帖子和 Wikipedia 文章的数据。

获取数据

为了获得数据,我们将创建五个类:基本的DataSource类,它将由LocalDataSourceNetworkDataSource扩展;TwitterDataSourceWikipediaDataSource将进一步延长NetworkDataSource

先说DataSource

数据源

DataSource是一个非常小的抽象类:

清单 9-24。 数据源

public abstract class DataSource { public abstract List<Marker> getMarkers(); }

这个类只有一个成员:??。这个类是我们所有其他数据类的基础。

现在我们来看看LocalDataSource

本地数据源

MainActivity使用LocalDataSourceARData添加标记。班级很小。

清单 9-25。 本地数据源

`public class LocalDataSource extends DataSource{
private List cachedMarkers = new ArrayList();
private static Bitmap icon = null;

public LocalDataSource(Resources res) {
if (res==null) throw new NullPointerException();

createIcon(res);
}

protected void createIcon(Resources res) {
if (res==null) throw new NullPointerException();

icon=BitmapFactory.decodeResource(res, R.drawable.ic_launcher);
} public List getMarkers() {
Marker atl = new IconMarker("ATL", 39.931269, -75.051261, 0,
Color.DKGRAY, icon);
cachedMarkers.add(atl);

Marker home = new Marker("Mt Laurel", 39.95, -74.9, 0, Color.YELLOW);
cachedMarkers.add(home);

return cachedMarkers;
}
}`

这个类的构造函数接受一个Resource对象作为参数。然后它调用createIcon(),然后将我们应用的默认图标分配给icon位图。getMarkers(),嗯,拿马克笔。

这节课讲完了,我们来看看NetworkDataSource

网络数据源

NetworkDataSource包含从我们的 Twitter 和 Wikipedia 来源获取数据的基本设置。

让我们从类和全局变量声明开始:

清单 9-26。 网络数据源

`public abstract class NetworkDataSource extends DataSource {
protected static final int MAX = 1000;
protected static final int READ_TIMEOUT = 10000;
protected static final int CONNECT_TIMEOUT = 10000;

protected List markersCache = null;

public abstract String createRequestURL(double lat, double lon, double alt,
float radius, String locale);

public abstract List parse(JSONObject root);`

MAX指定显示给用户的最大结果数。READ_TIMEOUTCONNECT_TIMEOUT是连接的超时值,以毫秒为单位。markersCache是一个列表<标记>对象,我们将在这个类的后面使用。createRequestURLparse是我们将在这个类的扩展中覆盖的存根。

现在让我们来看看getMarkers()getHttpGETInputStream()方法:

清单 9-27。get markers()和 getHttpGETInputStream()

`public List getMarkers() {
return markersCache;
}

protected static InputStream getHttpGETInputStream(String urlStr) {
if (urlStr == null)
throw new NullPointerException();

InputStream is = null;
URLConnection conn = null;

try {
if (urlStr.startsWith("file://"))
return new FileInputStream(urlStr.replace("file://", ""));

URL url = new URL(urlStr);
conn = url.openConnection();
conn.setReadTimeout(READ_TIMEOUT);
conn.setConnectTimeout(CONNECT_TIMEOUT);

is = conn.getInputStream();

return is;
} catch (Exception ex) {
try {
is.close();
} catch (Exception e) {
// Ignore
}
try {
if (conn instanceof HttpURLConnection)
((HttpURLConnection) conn).disconnect();
} catch (Exception e) {
// Ignore
}
ex.printStackTrace();
}
return null;
}`

getMarkers()方法只是返回markersCachegetHttpGETInputStream()用于获取指定 URL 的InputStream,并作为String传递给它。

现在让我们来看看getHttpInputString()parse()方法:

清单 9-28。getHttpInputString()和 parse()

`protected String getHttpInputString(InputStream is) {
if (is == null)
throw new NullPointerException();

BufferedReader reader = new BufferedReader(new InputStreamReader(is),
8 * 1024);
StringBuilder sb = new StringBuilder();

try {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line + "\n");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}

public List parse(String url) {
if (url == null)
throw new NullPointerException();

InputStream stream = null;
stream = getHttpGETInputStream(url);
if (stream == null)
throw new NullPointerException();

String string = null;
string = getHttpInputString(stream);
if (string == null)
throw new NullPointerException();

JSONObject json = null;
try {
json = new JSONObject(string);
} catch (JSONException e) {
e.printStackTrace();
}
if (json == null)
throw new NullPointerException(); return parse(json);
}
}`

getHttpInputString()中,我们获取InputStream的内容,并把它们放在一个String中。在parse()中,我们从数据源获取 JSON 对象,然后在其上调用另一个parse()方法。在这个类中,第二个parse()方法是一个存根,但是它在其他类中被覆盖和实现。

让我们写出扩展了NetworkDataSource : TwitterDataSourceWikipediaDataSource的两个类。

推特数据源

TwitterDataSource延伸NetworkDataSource。它负责从 Twitter 的服务器上获取附近推文的数据。TwitterDataSource只有两个全局变量。

清单 9-29。 声明 TwitterDataSource 及其全局变量

public class TwitterDataSource extends NetworkDataSource { private static final String URL = "http://search.twitter.com/search.json"; private static Bitmap icon = null;

字符串URL存储 Twitter 搜索 URL 的基础。我们将在createRequestURL()中动态构造参数。这个icon位图,现在是null,将在其中存储 Twitter 的标志。在显示推文时,我们会将这个标志显示为每个标记的图标。

现在让我们看看构造函数、createIcon()createRequestURL()方法:

清单 9-30。 构造函数,createIcon(),和 createRequestURL()

`public TwitterDataSource(Resources res) {
if (res==null) throw new NullPointerException();

createIcon(res);
}

protected void createIcon(Resources res) {
if (res==null) throw new NullPointerException();

icon=BitmapFactory.decodeResource(res, R.drawable.twitter);
}

@Override public String createRequestURL(double lat, double lon, double alt, float radius, String locale) {
return URL+"?geocode=" + lat + "%2C" + lon + "%2C" + Math.max(radius,
1.0) + "km";
}`

在构造函数中,我们将一个Resource对象作为参数,然后将其传递给createIcon()。这和《??》中的行为完全一样。在createIcon()中,我们再次做了和在NetworkDataSource中一样的事情,除了我们使用了不同的图标。这里我们将 Twitter drawable 分配给了icon位图,而不是ic_launcher位图。在createRequestURL()中,我们为 Twitter JSON 搜索应用编程接口(API)制定了一个完整的请求 URL。Twitter 搜索 API 允许我们轻松匿名地搜索推文。我们在 geocode 参数中提供用户的位置,并从用户设置的范围中选择一个更大的半径限制,即一公里。

现在我们有两个parse()方法和一个processJSONObject()

清单 9-31。 两个 parse()方法和 processJSONObject()方法

`@Override
public List parse(String url) {
if (url==null) throw new NullPointerException();

InputStream stream = null;
stream = getHttpGETInputStream(url);
if (stream==null) throw new NullPointerException();

String string = null;
string = getHttpInputString(stream);
if (string==null) throw new NullPointerException();

JSONObject json = null;
try {
json = new JSONObject(string);
} catch (JSONException e) {
e.printStackTrace();
}
if (json==null) throw new NullPointerException();

return parse(json);
}

@Override
public List parse(JSONObject root) {
if (root==null) throw new NullPointerException();

JSONObject jo = null; JSONArray dataArray = null;
List markers=new ArrayList();

try {
if(root.has("results")) dataArray =
root.getJSONArray("results");
if (dataArray == null) return markers;
int top = Math.min(MAX, dataArray.length());
for (int i = 0; i < top; i++) {

jo = dataArray.getJSONObject(i);
Marker ma = processJSONObject(jo);
if(ma!=null) markers.add(ma);
}
} catch (JSONException e) {
e.printStackTrace();
}
return markers;
}

private Marker processJSONObject(JSONObject jo) {
if (jo==null) throw new NullPointerException();

if (!jo.has("geo")) throw new NullPointerException();

Marker ma = null;
try {
Double lat=null, lon=null;

if(!jo.isNull("geo")) {
JSONObject geo = jo.getJSONObject("geo");
JSONArray coordinates =
geo.getJSONArray("coordinates");

lat=Double.parseDouble(coordinates.getString(0));

lon=Double.parseDouble(coordinates.getString(1));
} else if(jo.has("location")) {
Pattern pattern = Pattern.compile("\D*([0-
9.]+),\s?([0-9.]+)");
Matcher matcher =
pattern.matcher(jo.getString("location"));

if(matcher.find()){
lat=Double.parseDouble(matcher.group(1));

lon=Double.parseDouble(matcher.group(2));
}
} if(lat!=null) {
String user=jo.getString("from_user");

ma = new IconMarker(
user+": "+jo.getString("text"),
lat,
lon,
0,
Color.RED,
icon);
}
} catch (Exception e) {
e.printStackTrace();
}
return ma;
}
}`

第一个parse()方法采用字符串参数形式的 URL。然后 URL 通过getHttpGETInputStream()方法,产生的输入流被传递给getHttpInputString()方法。最后,从结果字符串中创建一个新的JSONObject,并将其传递给第二个parse()方法。

在第二个parse()方法中,我们接收来自前一个方法的JSONObject作为参数。然后我们首先确定我们收到的对象不是null。然后,我们将数据从对象转移到一个JSONArray中,如果它有任何结果的话。在此之后,我们需要遍历数组,为每个结果创建一个标记。为了确保不超出数组索引,我们找到了最大标记数和结果数之间的较小值。然后我们遍历数组,调用processJSONObject()来创建每个标记。

最后,我们来过一遍processJSONObject()。我们再次通过检查传递的JSONObject是否是null来开始该方法。之后,我们检查对象中的数据是否包含geo数据。如果没有,我们就不使用它,因为geo数据对于将它显示在屏幕和雷达上很重要。然后,我们通过JSONObject获取 tweet、用户和内容的坐标。所有这些数据被编译成一个标记,然后返回给第二个parse()方法。第二个parse方法将其添加到它的markers列表中。一旦创建了所有这样的标记,整个列表被返回给第一个parse()方法,该方法进一步将它返回给它在MainActivity中的调用者。

现在我们来看看最后一个数据源类,WikipediaDataSource

维基百科全书资料来源

WikipediaDataSource在结构和逻辑上与TwitterDataSource非常相似。唯一的主要区别在于对JSONObject的解析。

清单 9-32。 维基百科数据源

`public class WikipediaDataSource extends NetworkDataSource {
private static final String BASE_URL = "http://ws.geonames.org/findNearbyWikipediaJSON";

private static Bitmap icon = null;

public WikipediaDataSource(Resources res) {
if (res==null) throw new NullPointerException();

createIcon(res);
}

protected void createIcon(Resources res) {
if (res==null) throw new NullPointerException();

icon=BitmapFactory.decodeResource(res, R.drawable.wikipedia);
}

@Override
public String createRequestURL(double lat, double lon, double alt, float radius, String locale) {
return BASE_URL+
"?lat=" + lat +
"&lng=" + lon +
"&radius="+ radius +
"&maxRows=40" +
"&lang=" + locale;

}

@Override
public List parse(JSONObject root) {
if (root==null) return null;

JSONObject jo = null;
JSONArray dataArray = null;
List markers=new ArrayList();

try {
if(root.has("geonames")) dataArray =
root.getJSONArray("geonames");
if (dataArray == null) return markers; int top = Math.min(MAX, dataArray.length());
for (int i = 0; i < top; i++) {

jo = dataArray.getJSONObject(i);
Marker ma = processJSONObject(jo);
if(ma!=null) markers.add(ma);
}
} catch (JSONException e) {
e.printStackTrace();
}
return markers;
}

private Marker processJSONObject(JSONObject jo) {
if (jo==null) return null;

Marker ma = null;
if ( jo.has("title") &&
jo.has("lat") &&
jo.has("lng") &&
jo.has("elevation")
) {
try {
ma = new IconMarker(
jo.getString("title"),
jo.getDouble("lat"),
jo.getDouble("lng"),
jo.getDouble("elevation"),
Color.WHITE,
icon);
} catch (JSONException e) {
e.printStackTrace();
}
}
return ma;
}
}`

与 Twitter 不同,维基百科不提供官方搜索工具。然而,geonames.org 为维基百科提供了基于 JSON 的搜索,我们将会使用它。与TwitterDataSource的下一个区别是图标。我们只是在创建图标时使用了不同的drawable。获取和解析JSONObject的基本代码是相同的;只是价值观不同。

我们现在将编写帮助我们在屏幕上和现实生活中定位标记的类。

定位类

我们需要一组为我们的应用处理职位相关工作的类。这些类处理用户的物理位置,以及对象在屏幕上的位置。

物理分配工具

这个类用来表示用户在真实世界中的三维位置。

清单 9-33。PhysicalLocationUtility.java??

`public class PhysicalLocationUtility {
private double latitude = 0.0;
private double longitude = 0.0;
private double altitude = 0.0;

private static float[] x = new float[1];
private static double y = 0.0d;
private static float[] z = new float[1];

public PhysicalLocationUtility() { }

public PhysicalLocationUtility(PhysicalLocationUtility pl) {
if (pl==null) throw new NullPointerException();

set(pl.latitude, pl.longitude, pl.altitude);
}

public void set(double latitude, double longitude, double altitude) {
this.latitude = latitude;
this.longitude = longitude;
this.altitude = altitude;
}

public void setLatitude(double latitude) {
this.latitude = latitude;
}

public double getLatitude() {
return latitude;
}

public void setLongitude(double longitude) {
this.longitude = longitude;
} public double getLongitude() {
return longitude;
}

public void setAltitude(double altitude) {
this.altitude = altitude;
}

public double getAltitude() {
return altitude;
}

public static synchronized void convLocationToVector(Location org, PhysicalLocationUtility gp, Vector v) {
if (orgnull || gpnull || v==null)
throw new NullPointerException("Location,
PhysicalLocationUtility, and Vector cannot be NULL.");

Location.distanceBetween( org.getLatitude(),
org.getLongitude(),

gp.getLatitude(), org.getLongitude(),
z);

Location.distanceBetween( org.getLatitude(),
org.getLongitude(),

org.getLatitude(), gp.getLongitude(),
x);
y = gp.getAltitude() - org.getAltitude();
if (org.getLatitude() < gp.getLatitude())
z[0] *= -1;
if (org.getLongitude() > gp.getLongitude())
x[0] *= -1;

v.set(x[0], (float) y, z[0]);
}

@Override
public String toString() {
return "(lat=" + latitude + ", lng=" + longitude + ", alt=" +
altitude + ")";
}
}`

顾名思义,前三个 doubles 分别用于存储latitudelongitudealtitudexyz存储最终的三维位置数据。setLatitude()setLongitude()setAltitude()方法分别设置纬度、经度和高度。它们的get()对应物只是返回当前值。convLocationToVector()方法将位置转换为Vector。我们将在本章的后面写一个Vector类。toString()方法只是将纬度、经度和海拔高度编译成一个字符串,并将其返回给调用方法。

现在我们来看看ScreenPositionUtility

屏幕定位工具

ScreenPositionUtility用于显示雷达线。

清单 9-34。ScreenPositionUtility.java??

`public class ScreenPositionUtility {
private float x = 0f;
private float y = 0f;

public ScreenPositionUtility() {
set(0, 0);
}

public void set(float x, float y) {
this.x = x;
this.y = y;
}

public float getX() {
return x;
}

public void setX(float x) {
this.x = x;
}

public float getY() {
return y;
}

public void setY(float y) {
this.y = y;
}

public void rotate(double t) {
float xp = (float) Math.cos(t) * x - (float) Math.sin(t) * y;
float yp = (float) Math.sin(t) * x + (float) Math.cos(t) * y;

x = xp; y = yp;
}

public void add(float x, float y) {
this.x += x;
this.y += y;
}

@Override
public String toString() {
return "x="+x+" y="+y;
}
}`

setX()setY()方法分别为xy变量设置浮点值。set()方法同时设置xy的值。getX()getY()方法只是返回xy的值。rotate()方法围绕角度t旋转xy值。add()方法将传递的值分别添加到xy。最后,toString()方法返回一个字符串,其中包含了xy的值。

现在我们来看看 UI 代码。

用户界面工作正常

可涂装对象

PaintableObject是用户界面所有自定义部分的基类。它的一些方法只是我们在它的子类中覆盖的存根。这个类包含了很多绘制特定对象的方法,比如线条、位图、点等。在给定的画布上。

清单 9-35。PaintableObject.java??

`public abstract class PaintableObject {
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

public PaintableObject() {
if (paint==null) {
paint = new Paint();
paint.setTextSize(16);
paint.setAntiAlias(true);
paint.setColor(Color.BLUE);
paint.setStyle(Paint.Style.STROKE);
}
} public abstract float getWidth();

public abstract float getHeight();

public abstract void paint(Canvas canvas);

public void setFill(boolean fill) {
if (fill)
paint.setStyle(Paint.Style.FILL);
else
paint.setStyle(Paint.Style.STROKE);
}

public void setColor(int c) {
paint.setColor(c);
}

public void setStrokeWidth(float w) {
paint.setStrokeWidth(w);
}

public float getTextWidth(String txt) {
if (txt==null) throw new NullPointerException();
return paint.measureText(txt);
}

public float getTextAsc() {
return -paint.ascent();
}

public float getTextDesc() {
return paint.descent();
}

public void setFontSize(float size) {
paint.setTextSize(size);
}

public void paintLine(Canvas canvas, float x1, float y1, float x2, float y2)
{
if (canvas==null) throw new NullPointerException();

canvas.drawLine(x1, y1, x2, y2, paint);
}

public void paintRect(Canvas canvas, float x, float y, float width, float
height) {
if (canvas==null) throw new NullPointerException(); canvas.drawRect(x, y, x + width, y + height, paint);
}

public void paintRoundedRect(Canvas canvas, float x, float y, float width,
float height) {
if (canvas==null) throw new NullPointerException();

RectF rect = new RectF(x, y, x + width, y + height);
canvas.drawRoundRect(rect, 15F, 15F, paint);
}

public void paintBitmap(Canvas canvas, Bitmap bitmap, Rect src, Rect dst) {
if (canvasnull || bitmapnull) throw new NullPointerException();

canvas.drawBitmap(bitmap, src, dst, paint);
}

public void paintBitmap(Canvas canvas, Bitmap bitmap, float left, float top)
{
if (canvasnull || bitmapnull) throw new NullPointerException();

canvas.drawBitmap(bitmap, left, top, paint);
}

public void paintCircle(Canvas canvas, float x, float y, float radius) {
if (canvas==null) throw new NullPointerException();

canvas.drawCircle(x, y, radius, paint);
}

public void paintText(Canvas canvas, float x, float y, String text) {
if (canvasnull || textnull) throw new NullPointerException();

canvas.drawText(text, x, y, paint);
}

public void paintObj( Canvas canvas, PaintableObject obj,
float x, float y,
float rotation, float scale)
{
if (canvasnull || objnull) throw new NullPointerException();

canvas.save();
canvas.translate(x+obj.getWidth()/2, y+obj.getHeight()/2);
canvas.rotate(rotation);
canvas.scale(scale,scale);
canvas.translate(-(obj.getWidth()/2), -(obj.getHeight()/2));
obj.paint(canvas);
canvas.restore();
} public void paintPath( Canvas canvas, Path path,
float x, float y, float width,
float height, float rotation,
float scale)
{
if (canvasnull || pathnull) throw new NullPointerException();

canvas.save();
canvas.translate(x + width / 2, y + height / 2);
canvas.rotate(rotation);
canvas.scale(scale, scale);
canvas.translate(-(width / 2), -(height / 2));
canvas.drawPath(path, paint);
canvas.restore();
}
}`

整个类只有一个全局变量:一个启用了抗锯齿的paint对象。消除锯齿会平滑正在绘制的对象的线条。构造函数通过将文本大小设置为 16、启用抗锯齿、将绘画颜色设置为蓝色、将绘画风格设置为Paint.Style.STROKE来初始化paint对象。

下面的三个方法——getWidth()getHeight()paint()——被保留为方法存根,如果需要的话可以被覆盖。

setFill()方法允许我们改变画风为Paint.Style.FILLPaint.Style.STROKEsetColor()方法将颜料的颜色设置为与它作为参数的整数相对应的颜色。setStrokeWidth()允许我们设置笔画的宽度。getTextWidth()返回作为参数的文本宽度。getTextAsc()getTextDesc()分别返回文本的上升和下降。setFontSize()允许我们设置绘画的字体大小。名称前带有 paint 前缀的所有其余方法绘制在所提供的canvas上的 paint 之后写入的对象。例如,paintLine()用提供的坐标在提供的canvas上画一条线。

现在让我们看看扩展了PaintableObject的类集。

彩绘箱

类允许我们画出一个盒子的轮廓。这是一个简单的类,并且不是很大。

清单 9-36。PaintableBox.java??

`public class PaintableBox extends PaintableObject {
private float width=0, height=0;
private int borderColor = Color.rgb(255, 255, 255);
private int backgroundColor = Color.argb(128, 0, 0, 0);

public PaintableBox(float width, float height) {
this(width, height, Color.rgb(255, 255, 255), Color.argb(128, 0,
0, 0));
}

public PaintableBox(float width, float height, int borderColor, int
bgColor) {
set(width, height, borderColor, bgColor);
}

public void set(float width, float height) {
set(width, height, borderColor, backgroundColor);
}

public void set(float width, float height, int borderColor, int bgColor)
{
this.width = width;
this.height = height;
this.borderColor = borderColor;
this.backgroundColor = bgColor;
}

@Override
public void paint(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

setFill(true);
setColor(backgroundColor);
paintRect(canvas, 0, 0, width, height);

setFill(false);
setColor(borderColor);
paintRect(canvas, 0, 0, width, height);
}

@Override
public float getWidth() {
return width;
}

@Override
public float getHeight() {
return height; }
}`

这个类有两个构造函数,其中一个调用另一个。原因是其中一个构造函数只允许你设置盒子的宽度和高度,而第二个构造函数允许你设置它的颜色。当调用第一个时,它使用提供的宽度和高度来调用具有默认颜色的第二个。然后,第二个构造函数调用第二个set()方法来设置这些值。paint()方法只是在指定的canvas上画一个盒子。getWidth()getHeight()只返回盒子的宽度和高度。

现在我们来看看PaintableBoxedText

可画拳击文本

在画布上绘制文本,周围有一个方框。

清单 9-37。PaintableBox.java??

`public class PaintableBoxedText extends PaintableObject {
private float width=0, height=0;
private float areaWidth=0, areaHeight=0;
private ArrayList lineList = null;
private String[] lines = null;
private float[] lineWidths = null;
private float lineHeight = 0;
private float maxLineWidth = 0;
private float pad = 0;

private String txt = null;
private float fontSize = 12;
private int borderColor = Color.rgb(255, 255, 255);
private int backgroundColor = Color.argb(160, 0, 0, 0);
private int textColor = Color.rgb(255, 255, 255);

public PaintableBoxedText(String txtInit, float fontSizeInit, float
maxWidth) {
this(txtInit, fontSizeInit, maxWidth, Color.rgb(255, 255, 255),
Color.argb(128, 0, 0, 0), Color.rgb(255, 255, 255));
}

public PaintableBoxedText(String txtInit, float fontSizeInit, float
maxWidth, int borderColor, int bgColor, int textColor) {
set(txtInit, fontSizeInit, maxWidth, borderColor, bgColor,
textColor);
} public void set(String txtInit, float fontSizeInit, float maxWidth, int
borderColor, int bgColor, int textColor) {
if (txtInit==null) throw new NullPointerException();

this.borderColor = borderColor;
this.backgroundColor = bgColor;
this.textColor = textColor;
this.pad = getTextAsc();

set(txtInit, fontSizeInit, maxWidth);
}

public void set(String txtInit, float fontSizeInit, float maxWidth) {
if (txtInit==null) throw new NullPointerException();

try {
prepTxt(txtInit, fontSizeInit, maxWidth);
} catch (Exception ex) {
ex.printStackTrace();
prepTxt("TEXT PARSE ERROR", 12, 200);
}
}

private void prepTxt(String txtInit, float fontSizeInit, float maxWidth)
{
if (txtInit==null) throw new NullPointerException();

setFontSize(fontSizeInit);

txt = txtInit;
fontSize = fontSizeInit;
areaWidth = maxWidth - pad;
lineHeight = getTextAsc() + getTextDesc();

if (lineList==null) lineList = new ArrayList();
else lineList.clear();

BreakIterator boundary = BreakIterator.getWordInstance();
boundary.setText(txt);

int start = boundary.first();
int end = boundary.next();
int prevEnd = start;
while (end != BreakIterator.DONE) {
String line = txt.substring(start, end);
String prevLine = txt.substring(start, prevEnd);
float lineWidth = getTextWidth(line);

if (lineWidth > areaWidth) {
if(prevLine.length()>0) lineList.add(prevLine); start = prevEnd;
}

prevEnd = end;
end = boundary.next();
}
String line = txt.substring(start, prevEnd);
lineList.add(line);

if (linesnull || lines.length!=lineList.size()) lines = new
String[lineList.size()];
if (lineWidths
null || lineWidths.length!=lineList.size())
lineWidths = new float[lineList.size()];
lineList.toArray(lines);

maxLineWidth = 0;
for (int i = 0; i < lines.length; i++) {
lineWidths[i] = getTextWidth(lines[i]);
if (maxLineWidth < lineWidths[i])
maxLineWidth = lineWidths[i];
}
areaWidth = maxLineWidth;
areaHeight = lineHeight * lines.length;

width = areaWidth + pad * 2;
height = areaHeight + pad * 2;
}

@Override
public void paint(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

setFontSize(fontSize);

setFill(true);
setColor(backgroundColor);
paintRoundedRect(canvas, 0, 0, width, height);

setFill(false);
setColor(borderColor);
paintRoundedRect(canvas, 0, 0, width, height);

for (int i = 0; i < lines.length; i++) {
String line = lines[i];
setFill(true);
setStrokeWidth(0);
setColor(textColor);
paintText(canvas, pad, pad + lineHeight * i +
getTextAsc(), line); }
}

@Override
public float getWidth() {
return width;
}

@Override
public float getHeight() {
return height;
}
}`

同样,有两个构造函数做与PaintableBox中相同的事情。与PaintableBox的第一个主要区别是新方法prepTxt()prepTxt()通过将文本切割成不同大小的行以适合文本框来准备文本,而不是让一个长字符串愉快地离开文本框并溢出到外面。然后,paint()方法首先绘制基本的方框,然后使用一个for循环将每一行添加到其中。

现在我们来看看PaintableCircle

绘画圈

PaintableCircle允许我们在提供的Canvas上画一个圆。这是一个简单的类,代码很简单:

清单 9-38。PaintableCircle.java??

`public class PaintableCircle extends PaintableObject {
private int color = 0;
private float radius = 0;
private boolean fill = false;

public PaintableCircle(int color, float radius, boolean fill) {
set(color, radius, fill);
}

public void set(int color, float radius, boolean fill) {
this.color = color;
this.radius = radius;
this.fill = fill;
}

@Override
public void paint(Canvas canvas) {
if (canvas==null) throw new NullPointerException(); setFill(fill);
setColor(color);
paintCircle(canvas, 0, 0, radius);
}

@Override
public float getWidth() {
return radius*2;
}

@Override
public float getHeight() {
return radius*2;
}
}`

这一次,只有一个构造函数允许我们设置圆的半径和颜色。也只有一个set()方法,由构造函数调用。方法在给定的canvas上用指定的属性画一个圆。getWidth()getHeight()方法返回直径,因为这是一个圆。

现在我们来看看PaintableGps

可绘图 Gps

PaintableGps很像PaintableCircle,除了它还允许我们设置正在绘制的圆的笔画宽度。

清单 9-39。PaintableGps.java??

`public class PaintableGps extends PaintableObject {
private float radius = 0;
private float strokeWidth = 0;
private boolean fill = false;
private int color = 0;

public PaintableGps(float radius, float strokeWidth, boolean fill, int
color) {
set(radius, strokeWidth, fill, color);
}

public void set(float radius, float strokeWidth, boolean fill, int color) {
this.radius = radius;
this.strokeWidth = strokeWidth;
this.fill = fill;
this.color = color; }

@Override
public void paint(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

setStrokeWidth(strokeWidth);
setFill(fill);
setColor(color);
paintCircle(canvas, 0, 0, radius);
}

@Override
public float getWidth() {
return radius*2;
}

@Override
public float getHeight() {
return radius*2;
}
}`

同样,只有一个构造函数,它调用set()方法来设置所画圆的颜色、大小和笔画的宽度。像往常一样,paint()方法在提供的Canvas上绘制具有指定属性的圆。再次,getWidth()getHeight()返回圆的直径。

现在我们来看看PaintableIcon

・T0️可涂装图标

我们使用PaintableIcon来绘制 Twitter 和维基百科的图标。

清单 9-40。PaintableIcon.java??

`public Class PaintableIcon Extends PaintableObject {
private Bitmap bitmap=null;

public PaintableIcon(Bitmap bitmap, int width, int height) {
set(bitmap,width,height);
}

public void set(Bitmap bitmap, int width, int height) {
if (bitmap==null) throw new NullPointerException();

this.bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); }

@Override
public void paint(Canvas canvas) {
if (canvasnull || bitmapnull) throw new NullPointerException();

paintBitmap(canvas, bitmap, -(bitmap.getWidth()/2), -
(bitmap.getHeight()/2));
}

@Override
public float getWidth() {
return bitmap.getWidth();
}

@Override
public float getHeight() {
return bitmap.getHeight();
}
}`

构造函数获取要绘制的位图,以及要绘制的宽度和高度,然后调用set()方法来设置它们。在set()方法中,Bitmap被缩放到指定的大小。然后,paint()方法将它绘制到提供的canvas上。getWidth()getHeight()方法返回的是我们最后绘制的位图的宽度和高度,而不是构造函数中传递的位图。

现在我们来看看如何创建一个绘制线条的类。

印刷机

PaintableLine允许我们在提供的Canvas上用指定的颜色画一条线。

清单 9-41。PaintableLine.java??

`public class PaintableLine extends PaintableObject {
private int color = 0;
private float x = 0;
private float y = 0;

public PaintableLine(int color, float x, float y) {
set(color, x, y);
}

public void set(int color, float x, float y) {
this.color = color; this.x = x;
this.y = y;
}

@Override
public void paint(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

setFill(false);
setColor(color);
paintLine(canvas, 0, 0, x, y);
}

@Override
public float getWidth() {
return x;
}

@Override
public float getHeight() {
return y;
}
}`

构造函数获取线条的颜色以及 X 和 Y 点,并将它们传递给set()进行设置。paint()方法分别使用PaintableObject. getWidth()getHeight()返回xyCanvas上绘制。

现在我们来看看PaintablePoint

压力表点

PaintablePoint用于在单个画布上绘制单个点。它被用来制造我们的雷达。

清单 9-42。 绘画点

`public class PaintablePoint extends PaintableObject {
private static int width=2;
private static int height=2;
private int color = 0;
private boolean fill = false;

public PaintablePoint(int color, boolean fill) {
set(color, fill);
}

public void set(int color, boolean fill) { this.color = color;
this.fill = fill;
}

@Override
public void paint(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

setFill(fill);
setColor(color);
paintRect(canvas, -1, -1, width, height);
}

@Override
public float getWidth() {
return width;
}

@Override
public float getHeight() {
return height;
}
}`

正在画的点实际上不是一个点;它只是一个非常小的长方形。构造函数接受颜色并通过set()方法设置它们,然后paint()绘制一个非常小的矩形。getWidth()getHeight()简单地返回正在绘制的矩形的宽度和高度。

现在我们来看看PaintablePosition

可喷漆位置

PaintablePosition扩展了PaintableObject,增加了旋转和缩放被画物体的能力。

清单 9-43。 可涂装位置

`public class PaintablePosition extends PaintableObject {
private float width=0, height=0;
private float objX=0, objY=0, objRotation=0, objScale=0;
private PaintableObject obj = null;

public PaintablePosition(PaintableObject drawObj, float x, float y, float
rotation, float scale) {
set(drawObj, x, y, rotation, scale);
} public void set(PaintableObject drawObj, float x, float y, float rotation,
float scale) {
if (drawObj==null) throw new NullPointerException();

this.obj = drawObj;
this.objX = x;
this.objY = y;
this.objRotation = rotation;
this.objScale = scale;
this.width = obj.getWidth();
this.height = obj.getHeight();
}

public void move(float x, float y) {
objX = x;
objY = y;
}

public float getObjectsX() {
return objX;
}

public float getObjectsY() {
return objY;
}

@Override
public void paint(Canvas canvas) {
if (canvasnull || objnull) throw new NullPointerException();

paintObj(canvas, obj, objX, objY, objRotation, objScale);
}

@Override
public float getWidth() {
return width;
}

@Override
public float getHeight() {
return height;
}

@Override
public String toString() {
return "objX="+objX+" objY="+objY+" width="+width+" height="+height;
}
}`

构造函数获取PaintableObject类的实例、位置的 X 和 Y 坐标、旋转角度和缩放量。然后,构造函数将所有这些数据传递给set()方法,由它来设置值。我们现在有三个新方法,比其他已经扩展了PaintableObject的类多了:move()getObjectsX()getObjectsY()getObjectsX()getObjectsY()分别返回传递给构造函数的 x 和 y 值。move()允许我们将对象移动到新的 X 和 Y 坐标。paint()方法再次在提供的canvas上绘制对象。getWidth()getHeight()返回对象的宽度和高度。toString()在单个字符串中返回对象的 X 坐标、Y 坐标、宽度和高度。

现在我们来看看PaintableRadarPoints

推拉雷达测点

PaintableRadarPoints用于绘制雷达上所有标记的相对位置。

清单 9-44。 漆面雷达点

`public class PaintableRadarPoints extends PaintableObject {
private final float[] locationArray = new float[3];
private PaintablePoint paintablePoint = null;
private PaintablePosition pointContainer = null;

@Override
public void paint(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

float range = ARData.getRadius() * 1000;
float scale = range / Radar.RADIUS;
for (Marker pm : ARData.getMarkers()) {
pm.getLocation().get(locationArray);
float x = locationArray[0] / scale;
float y = locationArray[2] / scale;
if ((xx+yy)<(Radar.RADIUS*Radar.RADIUS)) {
if (paintablePoint==null) paintablePoint = new
PaintablePoint(pm.getColor(),true);
else paintablePoint.set(pm.getColor(),true);

if (pointContainer==null) pointContainer = new
PaintablePosition( paintablePoint,
(x+Radar.RADIUS-1),
(y+Radar.RADIUS-1),
0, 1);
else pointContainer.set(paintablePoint,
(x+Radar.RADIUS-1),
(y+Radar.RADIUS-1),
0,
1);

pointContainer.paint(canvas);
}
}
}

@Override
public float getWidth() {
return Radar.RADIUS * 2;
}

@Override
public float getHeight() {
return Radar.RADIUS * 2;
}
}`

在这个类中,没有构造函数。相反,只有paint()getWidth()getHeight()方法存在。getWidth()getHeight()返回我们绘制的代表标记的点的直径。在paint()方法中,我们使用一个for循环在雷达上为每个标记画一个点。

现在我们来看看PaintableText

彩绘板文字

PaintableTextPaintableObject的扩展,用于绘制文本。我们用它在雷达上显示文本:

清单 9-45。 可绘画文字

`public class PaintableText extends PaintableObject {
private static final float WIDTH_PAD = 4;
private static final float HEIGHT_PAD = 2;

private String text = null;
private int color = 0;
private int size = 0;
private float width = 0;
private float height = 0;
private boolean bg = false; public PaintableText(String text, int color, int size, boolean
paintBackground) {
set(text, color, size, paintBackground);
}

public void set(String text, int color, int size, boolean paintBackground) {
if (text==null) throw new NullPointerException();

this.text = text;
this.bg = paintBackground;
this.color = color;
this.size = size;
this.width = getTextWidth(text) + WIDTH_PAD * 2;
this.height = getTextAsc() + getTextDesc() + HEIGHT_PAD * 2;
}

@Override
public void paint(Canvas canvas) {
if (canvasnull || textnull) throw new NullPointerException();

setColor(color);
setFontSize(size);
if (bg) {
setColor(Color.rgb(0, 0, 0));
setFill(true);
paintRect(canvas, -(width/2), -(height/2), width, height);
setColor(Color.rgb(255, 255, 255));
setFill(false);
paintRect(canvas, -(width/2), -(height/2), width, height);
}
paintText(canvas, (WIDTH_PAD - width/2), (HEIGHT_PAD + getTextAsc() -
height/2), text);
}

@Override
public float getWidth() {
return width;
}

@Override
public float getHeight() {
return height;
}
}`

这个类的构造函数将文本、颜色、大小和背景色作为参数,然后将所有这些传递给要设置的set()方法。paint()方法绘制文本及其背景颜色。getWidth()getHeight()再次返回宽度和高度。

现在,在我们进入主要的 UI 组件之前,比如Radar类、Marker类和IconMarker类,我们需要创建一些工具类。

公用事业类

在我们的应用中,我们有一些工具类。这些类处理向量和矩阵函数,实现低通滤波器,还处理诸如音调之类的值的计算。

矢量

我们的第一个工具类是Vector类。这个类处理Vectors后面的数学。在前一章我们有一个类似的叫做Vector3D的类,但是这一个更全面。所有这些都是纯 Java,没有任何 Android 特有的东西。代码改编自免费开源 Mixare 框架的Vector类(http://www.mixare.org/)。我们将看看按类型分组的方法,比如数学函数、设置值等等。

首先,让我们看看全局变量和构造函数。

清单 9-46。 向量的构造函数和全局变量

`public class Vector {
private final float[] matrixArray = new float[9];

private volatile float x = 0f;
private volatile float y = 0f;
private volatile float z = 0f;

public Vector() {
this(0, 0, 0);
}

public Vector(float x, float y, float z) {
set(x, y, z);
}`

matrixArray是我们在本课后面的prod()方法中使用的数组。浮点值xyz是任意给定Vector的三个值。第一个构造函数创建一个Vector,将xyz都设置为零。第二个构造函数将xyz设置为提供的值。

现在让我们看看这个类的 getter 和 setter 方法:

清单 9-47。 get()和 set()

`public synchronized float getX() {
return x;
}
public synchronized void setX(float x) {
this.x = x;
}

public synchronized float getY() {
return y;
}

public synchronized void setY(float y) {
this.y = y;
}

public synchronized float getZ() {
return z;
}

public synchronized void setZ(float z) {
this.z = z;
}

public synchronized void get(float[] array) {
if (array==null || array.length!=3)
throw new IllegalArgumentException("get() array must be non-NULL and
size of 3");

array[0] = this.x;
array[1] = this.y;
array[2] = this.z;
}

public void set(Vector v) {
if (v==null) return;

set(v.x, v.y, v.z);
}

public void set(float[] array) {
if (array==null || array.length!=3)
throw new IllegalArgumentException("get() array must be non-NULL and
size of 3");

set(array[0], array[1], array[2]);
}

public synchronized void set(float x, float y, float z) { this.x = x;
this.y = y;
this.z = z;
}`

getX()getY()getZ()分别向调用方法返回xyz的值,而它们的set()对应方更新所述值。采用浮点数组作为参数的get()方法将一次性给出xyz的值。在剩下的三个set()方法中,有两个最终调用了set(float x, float y, float z),它设置了xyz的值。调用这个方法的另外两个set()方法只是允许我们使用数组或预先存在的向量来设置值,而不是总是必须为xyz传递单独的值。

现在我们将进入这节课的数学部分:

清单 9-48。 向量类的数学部分

`@Override
public synchronized boolean equals(Object obj) {
if (obj==null) return false;

Vector v = (Vector) obj;
return (v.x == this.x && v.y == this.y && v.z == this.z);
}

public synchronized void add(float x, float y, float z) {
this.x += x;
this.y += y;
this.z += z;
}

public void add(Vector v) {
if (v==null) return;

add(v.x, v.y, v.z);
}

public void sub(Vector v) {
if (v==null) return;

add(-v.x, -v.y, -v.z);
}

public synchronized void mult(float s) {
this.x *= s;
this.y *= s;
this.z *= s;
} public synchronized void divide(float s) {
this.x /= s;
this.y /= s;
this.z /= s;
}

public synchronized float length() {
return (float) Math.sqrt(this.x * this.x + this.y * this.y +
this.z * this.z);
}

public void norm() {
divide(length());
}

public synchronized void cross(Vector u, Vector v) {
if (vnull || unull) return;

float x = u.y * v.z - u.z * v.y;
float y = u.z * v.x - u.x * v.z;
float z = u.x * v.y - u.y * v.x;
this.x = x;
this.y = y;
this.z = z;
}

public synchronized void prod(Matrix m) {
if (m==null) return;

m.get(matrixArray);
float xTemp = matrixArray[0] * this.x + matrixArray[1] * this.y +
matrixArray[2] * this.z;
float yTemp = matrixArray[3] * this.x + matrixArray[4] * this.y +
matrixArray[5] * this.z;
float zTemp = matrixArray[6] * this.x + matrixArray[7] * this.y +
matrixArray[8] * this.z;

this.x = xTemp;
this.y = yTemp;
this.z = zTemp;
}

@Override
public synchronized String toString() {
return "x = " + this.x + ", y = " + this.y + ", z = " + this.z;
}
}`

equals()方法将 vector 与给定的对象进行比较,看它们是否相等。add()sub()方法分别在向量中加入和减去参数。mult()方法将所有值乘以传递的浮点数。divide()方法将所有的值除以传递的浮点数。length()方法返回向量的长度。norm()方法将向量除以其长度。方法将两个向量交叉相乘。prod()方法将向量与提供的矩阵相乘。toString()以人类可读的格式返回xyz的值。

接下来我们上Utilities课。

公用事业

Utilities类包含一个单独的getAngle()方法,我们在计算像PitchAzimuthCalculator中的音高这样的东西时用它来获得角度。其中的数学是简单的三角学。

清单 9-49。 公用事业

`public abstract class Utilities {

private Utilities() { }

public static final float getAngle(float center_x, float center_y, float
post_x, float post_y) {
float tmpv_x = post_x - center_x;
float tmpv_y = post_y - center_y;
float d = (float) Math.sqrt(tmpv_x * tmpv_x + tmpv_y * tmpv_y);
float cos = tmpv_x / d;
float angle = (float) Math.toDegrees(Math.acos(cos));

angle = (tmpv_y < 0) ? angle * -1 : angle;

return angle;
}
}`

现在我们来看看那个PitchAzimuthCalculator

俯仰方位计算器

PitchAzimuthCalculator是一个类,用于在给定矩阵的情况下计算俯仰和方位角:

清单 9-50。??【俯仰角计算器】??

`public class PitchAzimuthCalculator {
private static final Vector looking = new Vector();
private static final float[] lookingArray = new float[3];

private static volatile float azimuth = 0;

private static volatile float pitch = 0;

private PitchAzimuthCalculator() {};

public static synchronized float getAzimuth() {
return PitchAzimuthCalculator.azimuth;
}
public static synchronized float getPitch() {
return PitchAzimuthCalculator.pitch;
}

public static synchronized void calcPitchBearing(Matrix rotationM) {
if (rotationM==null) return;

looking.set(0, 0, 0);
rotationM.transpose();
looking.set(1, 0, 0);
looking.prod(rotationM);
looking.get(lookingArray);
PitchAzimuthCalculator.azimuth = ((Utilities.getAngle(0, 0,
lookingArray[0], lookingArray[2]) + 360 ) % 360);

rotationM.transpose();
looking.set(0, 1, 0);
looking.prod(rotationM);
looking.get(lookingArray);
PitchAzimuthCalculator.pitch = -Utilities.getAngle(0, 0,
lookingArray[1], lookingArray[2]);
}
}`

现在我们来看看LowPassFilter

低通滤波器

低通滤波器是一种电子滤波器,它通过低频信号,但衰减频率高于截止频率的信号(降低其振幅)。每个频率的实际衰减量因滤波器而异。在音频应用中,它有时被称为高频截止滤波器高音截止滤波器

清单 9-51。low pass filter

`public class LowPassFilter {

private static final float ALPHA_DEFAULT = 0.333f;
private static final float ALPHA_STEADY = 0.001f;
private static final float ALPHA_START_MOVING = 0.6f;
private static final float ALPHA_MOVING = 0.9f;

private LowPassFilter() { }

public static float[] filter(float low, float high, float[] current, float[]
previous) {
if (currentnull || previousnull)
throw new NullPointerException("Input and prev float arrays must be
non-NULL");
if (current.length!=previous.length)
throw new IllegalArgumentException("Input and prev must be the same
length");

float alpha = computeAlpha(low,high,current,previous);

for ( int i=0; i<current.length; i++ ) {
previous[i] = previous[i] + alpha * (current[i] - previous[i]);
}
return previous;
}

private static final float computeAlpha(float low, float high, float[]
current, float[] previous) {
if(previous.length != 3 || current.length != 3) return ALPHA_DEFAULT;

float x1 = current[0],
y1 = current[1],
z1 = current[2];

float x2 = previous[0],
y2 = previous[1],
z2 = previous[2];

float distance = (float)(Math.sqrt( Math.pow((double)(x2 - x1), 2d) +
Math.pow((double)(y2 - y1), 2d) +
Math.pow((double)(z2 - z1), 2d))
);

if(distance < low) {
return ALPHA_STEADY;
} else if(distance >= low || distance < high) {
return ALPHA_START_MOVING;
} return ALPHA_MOVING;
}
}`

现在我们来看看Matrix类。

矩阵

Matrix类处理与矩阵相关的函数,就像 Vector 类处理向量一样。同样,这个类是从 Mixare 框架改编而来的。

我们将像对待Vector类一样分解它:

清单 9-52。 矩阵的 getters 和 setters,以及构造函数

`public class Matrix {
private static final Matrix tmp = new Matrix();

private volatile float a1=0f, a2=0f, a3=0f;
private volatile float b1=0f, b2=0f, b3=0f;
private volatile float c1=0f, c2=0f, c3=0f;

public Matrix() { }

public synchronized float getA1() {
return a1;
}
public synchronized void setA1(float a1) {
this.a1 = a1;
}

public synchronized float getA2() {
return a2;
}
public synchronized void setA2(float a2) {
this.a2 = a2;
}

public synchronized float getA3() {
return a3;
}
public synchronized void setA3(float a3) {
this.a3 = a3;
}

public synchronized float getB1() {
return b1;
} public synchronized void setB1(float b1) {
this.b1 = b1;
}

public synchronized float getB2() {
return b2;
}
public synchronized void setB2(float b2) {
this.b2 = b2;
}

public synchronized float getB3() {
return b3;
}
public synchronized void setB3(float b3) {
this.b3 = b3;
}

public synchronized float getC1() {
return c1;
}
public synchronized void setC1(float c1) {
this.c1 = c1;
}

public synchronized float getC2() {
return c2;
}
public synchronized void setC2(float c2) {
this.c2 = c2;
}

public synchronized float getC3() {
return c3;
}
public synchronized void setC3(float c3) {
this.c3 = c3;
}

public synchronized void get(float[] array) {
if (array==null || array.length!=9)
throw new IllegalArgumentException("get() array must be non-NULL and
size of 9");

array[0] = this.a1;
array[1] = this.a2;
array[2] = this.a3;

array[3] = this.b1;
array[4] = this.b2; array[5] = this.b3;

array[6] = this.c1;
array[7] = this.c2;
array[8] = this.c3;
}

public void set(Matrix m) {
if (m==null) throw new NullPointerException();

set(m.a1,m. a2, m.a3, m.b1, m.b2, m.b3, m.c1, m.c2, m.c3);
}

public synchronized void set(float a1, float a2, float a3, float b1, float
b2, float b3, float c1, float c2, float c3) {
this.a1 = a1;
this.a2 = a2;
this.a3 = a3;

this.b1 = b1;
this.b2 = b2; this.b3 = b3;

this.c1 = c1;
this.c2 = c2;
this.c3 = c3;
}`

getA1()getA2()等方法。返回矩阵特定部分的值,而它们的set()对应部分更新它。另一个get()方法用来自矩阵的所有九个值填充传递的数组。剩下的set()方法将矩阵的值设置为所提供的矩阵值或所提供的浮点值。

现在让我们来看看这个类的数学函数:

清单 9-53。 矩阵的数学函数

`public void toIdentity() {
set(1, 0, 0, 0, 1, 0, 0, 0, 1);
}

public synchronized void adj() {
float a11 = this.a1;
float a12 = this.a2;
float a13 = this.a3;

float a21 = this.b1;
float a22 = this.b2;
float a23 = this.b3;

float a31 = this.c1;
float a32 = this.c2;
float a33 = this.c3;

this.a1 = det2x2(a22, a23, a32, a33);
this.a2 = det2x2(a13, a12, a33, a32);
this.a3 = det2x2(a12, a13, a22, a23);

this.b1 = det2x2(a23, a21, a33, a31);
this.b2 = det2x2(a11, a13, a31, a33);
this.b3 = det2x2(a13, a11, a23, a21);

this.c1 = det2x2(a21, a22, a31, a32);
this.c2 = det2x2(a12, a11, a32, a31);
this.c3 = det2x2(a11, a12, a21, a22);
}

public void invert() {
float det = this.det();

adj();
mult(1 / det);
}

public synchronized void transpose() {
float a11 = this.a1;
float a12 = this.a2;
float a13 = this.a3;

float a21 = this.b1;
float a22 = this.b2;
float a23 = this.b3;

float a31 = this.c1;
float a32 = this.c2;
float a33 = this.c3;

this.b1 = a12;
this.a2 = a21;
this.b3 = a32;
this.c2 = a23;
this.c1 = a13;
this.a3 = a31;

this.a1 = a11;
this.b2 = a22;
this.c3 = a33;
} private float det2x2(float a, float b, float c, float d) {
return (a * d) - (b * c);
}

public synchronized float det() {
return (this.a1 * this.b2 * this.c3) - (this.a1 * this.b3 * this.c2) -
(this.a2 * this.b1 * this.c3) +
(this.a2 * this.b3 * this.c1) + (this.a3 * this.b1 * this.c2) - (this.a3

  • this.b2 * this.c1);
    }

public synchronized void mult(float c) {
this.a1 = this.a1 * c;
this.a2 = this.a2 * c;
this.a3 = this.a3 * c;

this.b1 = this.b1 * c;
this.b2 = this.b2 * c;
this.b3 = this.b3 * c;

this.c1 = this.c1 * c;
this.c2 = this.c2 * c;
this.c3 = this.c3 * c;
}

public synchronized void prod(Matrix n) {
if (n==null) throw new NullPointerException();

tmp.set(this);
this.a1 = (tmp.a1 * n.a1) + (tmp.a2 * n.b1) + (tmp.a3 * n.c1);
this.a2 = (tmp.a1 * n.a2) + (tmp.a2 * n.b2) + (tmp.a3 * n.c2);
this.a3 = (tmp.a1 * n.a3) + (tmp.a2 * n.b3) + (tmp.a3 * n.c3);

this.b1 = (tmp.b1 * n.a1) + (tmp.b2 * n.b1) + (tmp.b3 * n.c1);
this.b2 = (tmp.b1 * n.a2) + (tmp.b2 * n.b2) + (tmp.b3 * n.c2);
this.b3 = (tmp.b1 * n.a3) + (tmp.b2 * n.b3) + (tmp.b3 * n.c3);

this.c1 = (tmp.c1 * n.a1) + (tmp.c2 * n.b1) + (tmp.c3 * n.c1);
this.c2 = (tmp.c1 * n.a2) + (tmp.c2 * n.b2) + (tmp.c3 * n.c2);
this.c3 = (tmp.c1 * n.a3) + (tmp.c2 * n.b3) + (tmp.c3 * n.c3);
}

@Override
public synchronized String toString() {
return "(" + this.a1 + "," + this.a2 + "," + this.a3 + ")"+
" (" + this.b1 + "," + this.b2 + "," + this.b3 + ")"+
" (" + this.c1 + "," + this.c2 + "," + this.c3 + ")";
}
}`

toIdentity()将矩阵的值设置为 1,0,0,0,1,0,0,0,1。adj()求矩阵的伴随矩阵。invert()通过调用adj()然后除以通过调用det()方法找到的行列式来反转矩阵。transpose()方法转置矩阵。det2x2()方法为提供的值寻找行列式,而det()方法为整个矩阵寻找行列式。mult()将矩阵中的每个值乘以提供的浮点数,而prod()将矩阵乘以提供的MatrixtoString()以人类可读的字符串格式返回所有值。

现在让我们为主要组件编写类,即RadarMarkerIconMarker类。

组件

这些类负责我们应用的主要组件,比如雷达和标记。标记组件分为两个类,IconMarker 和 Marker。

雷达

Radar类用于绘制我们的雷达,以及它的所有元素,如代表标记的线和点。

我们将从查看全局变量和构造函数开始:

清单 9-54。 雷达类的变量和构造函数

`public class Radar {
public static final float RADIUS = 48;
private static final int LINE_COLOR = Color.argb(150,0,0,220);
private static final float PAD_X = 10;
private static final float PAD_Y = 20;
private static final int RADAR_COLOR = Color.argb(100, 0, 0, 200);
private static final int TEXT_COLOR = Color.rgb(255,255,255);
private static final int TEXT_SIZE = 12;

private static ScreenPositionUtility leftRadarLine = null;
private static ScreenPositionUtility rightRadarLine = null;
private static PaintablePosition leftLineContainer = null;
private static PaintablePosition rightLineContainer = null;
private static PaintablePosition circleContainer = null;

private static PaintableRadarPoints radarPoints = null;
private static PaintablePosition pointsContainer = null; private static PaintableText paintableText = null;
private static PaintablePosition paintedContainer = null;

public Radar() {
if (leftRadarLinenull) leftRadarLine = new ScreenPositionUtility();
if (rightRadarLine
null) rightRadarLine = new ScreenPositionUtility();
}`

前七个常量设置雷达的颜色、半径、文本颜色和填充的值。剩下的变量被创建为各种类的null对象,稍后将对其进行初始化。在构造函数中,我们检查是否已经创建了显示当前正在查看的区域的雷达线。如果它们还没有被创建,它们将被创建为ScreenPositionUtility的新实例。

现在让我们添加该类的实际方法:

清单 9-55。 雷达的方法

`public void draw(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

PitchAzimuthCalculator.calcPitchBearing(ARData.getRotationMatrix());
ARData.setAzimuth(PitchAzimuthCalculator.getAzimuth());
ARData.setPitch(PitchAzimuthCalculator.getPitch());

drawRadarCircle(canvas);
drawRadarPoints(canvas);
drawRadarLines(canvas);
drawRadarText(canvas);
}

private void drawRadarCircle(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

if (circleContainer==null) {
PaintableCircle paintableCircle = new
PaintableCircle(RADAR_COLOR,RADIUS,true);
circleContainer = new
PaintablePosition(paintableCircle,PAD_X+RADIUS,PAD_Y+RADIUS,0,1);
}
circleContainer.paint(canvas);
}

private void drawRadarPoints(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

if (radarPoints==null) radarPoints = new PaintableRadarPoints();

if (pointsContainer==null) pointsContainer = new PaintablePosition( radarPoints,
PAD_X,
PAD_Y,
-ARData.getAzimuth(),
1);
else
pointsContainer.set(radarPoints,
PAD_X,
PAD_Y,
-ARData.getAzimuth(),
1);

pointsContainer.paint(canvas);
}

private void drawRadarLines(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

if (leftLineContainer==null) {
leftRadarLine.set(0, -RADIUS);
leftRadarLine.rotate(-CameraModel.DEFAULT_VIEW_ANGLE / 2);
leftRadarLine.add(PAD_X+RADIUS, PAD_Y+RADIUS);

float leftX = leftRadarLine.getX()-(PAD_X+RADIUS);
float leftY = leftRadarLine.getY()-(PAD_Y+RADIUS);
PaintableLine leftLine = new PaintableLine(LINE_COLOR, leftX,
leftY);
leftLineContainer = new PaintablePosition( leftLine,
PAD_X+RADIUS,
PAD_Y+RADIUS,
0,
1);
}
leftLineContainer.paint(canvas);

if (rightLineContainer==null) {
rightRadarLine.set(0, -RADIUS);
rightRadarLine.rotate(CameraModel.DEFAULT_VIEW_ANGLE / 2);
rightRadarLine.add(PAD_X+RADIUS, PAD_Y+RADIUS);

float rightX = rightRadarLine.getX()-(PAD_X+RADIUS);
float rightY = rightRadarLine.getY()-(PAD_Y+RADIUS);
PaintableLine rightLine = new PaintableLine(LINE_COLOR, rightX,
rightY);
rightLineContainer = new PaintablePosition( rightLine,
PAD_X+RADIUS,
PAD_Y+RADIUS,
0,
1);
} rightLineContainer.paint(canvas);
}

private void drawRadarText(Canvas canvas) {
if (canvas==null) throw new NullPointerException();
int range = (int) (ARData.getAzimuth() / (360f / 16f));
String dirTxt = "";
if (range == 15 || range == 0) dirTxt = "N";
else if (range == 1 || range == 2) dirTxt = "NE";
else if (range == 3 || range == 4) dirTxt = "E";
else if (range == 5 || range == 6) dirTxt = "SE";
else if (range == 7 || range == 8) dirTxt= "S";
else if (range == 9 || range == 10) dirTxt = "SW";
else if (range == 11 || range == 12) dirTxt = "W";
else if (range == 13 || range == 14) dirTxt = "NW";
int bearing = (int) ARData.getAzimuth();
radarText( canvas,
""+bearing+((char)176)+" "+dirTxt,
(PAD_X + RADIUS),
(PAD_Y - 5),
true
);

radarText( canvas,
formatDist(ARData.getRadius() * 1000),
(PAD_X + RADIUS),
(PAD_Y + RADIUS*2 -10),
false
);
}

private void radarText(Canvas canvas, String txt, float x, float y, boolean
bg) {
if (canvasnull || txtnull) throw new NullPointerException();

if (paintableText==null) paintableText = new
PaintableText(txt,TEXT_COLOR,TEXT_SIZE,bg);
else paintableText.set(txt,TEXT_COLOR,TEXT_SIZE,bg);

if (paintedContainer==null) paintedContainer = new
PaintablePosition(paintableText,x,y,0,1);
else paintedContainer.set(paintableText,x,y,0,1);

paintedContainer.paint(canvas);
}

private static String formatDist(float meters) {
if (meters < 1000) {
return ((int) meters) + "m";
} else if (meters < 10000) { return formatDec(meters / 1000f, 1) + "km";
} else {
return ((int) (meters / 1000f)) + "km";
}
}

private static String formatDec(float val, int dec) {
int factor = (int) Math.pow(10, dec);

int front = (int) (val);
int back = (int) Math.abs(val * (factor) ) % factor;

return front + "." + back;
}
}`

draw()方法通过获取俯仰和方位角,然后按照所需的顺序调用其他绘制方法来开始这个过程。drawRadarCircle()简单的给雷达画个基圆。drawRadarPoints()在雷达圈上画出所有表示标记的点。drawRadarLines()绘制两条线,显示哪些标记当前位于摄像机的可视区域内。drawRadarText()调用radarText()对文本进行格式化,然后将其绘制到雷达上。

这就把我们带到了Radar课的结尾。现在我们来看看Marker类。

选择

Marker类处理与我们显示的标记相关的大部分编码。它计算标记是否应该在屏幕上可见,并相应地绘制图像和文本。

全局变量

我们将照常从全局变量开始:

清单 9-56。 全局变量

`public class Marker implements Comparable {
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("@#");

private static final Vector symbolVector = new Vector(0, 0, 0);
private static final Vector textVector = new Vector(0, 1, 0);

private final Vector screenPositionVector = new Vector(); private final Vector tmpSymbolVector = new Vector();
private final Vector tmpVector = new Vector();
private final Vector tmpTextVector = new Vector();
private final float[] distanceArray = new float[1];
private final float[] locationArray = new float[3];
private final float[] screenPositionArray = new float[3];

private float initialY = 0.0f;

private volatile static CameraModel cam = null;

private volatile PaintableBoxedText textBox = null;
private volatile PaintablePosition textContainer = null;

protected final float[] symbolArray = new float[3];
protected final float[] textArray = new float[3];

protected volatile PaintableObject gpsSymbol = null;
protected volatile PaintablePosition symbolContainer = null;
protected String name = null;
protected volatile PhysicalLocationUtility physicalLocation = new
PhysicalLocationUtility();
protected volatile double distance = 0.0;
protected volatile boolean isOnRadar = false;
protected volatile boolean isInView = false;
protected final Vector symbolXyzRelativeToCameraView = new Vector();
protected final Vector textXyzRelativeToCameraView = new Vector();
protected final Vector locationXyzRelativeToPhysicalLocation = new Vector();
protected int color = Color.WHITE;

private static boolean debugTouchZone = false;
private static PaintableBox touchBox = null;
private static PaintablePosition touchPosition = null;

private static boolean debugCollisionZone = false;
private static PaintableBox collisionBox = null;
private static PaintablePosition collisionPosition = null;`

DECIMAL_FORMAT用于格式化我们在雷达上显示的距离。

symbolVectortextVector用于查找文本和标记符号的位置。当使用旋转矩阵找到文本及其伴随符号的位置时,使用symbolVectortextVector

接下来的四个向量和三个浮点数组用于定位和绘制标记符号及其附带的文本。

initialY是每个标记的初始 Y 轴位置。一开始它被设置为 0,但是它的值对于每个标记都是不同的。

textBoxtextContainercam分别是PaintableBoxedTextPaintablePostionCameraModel的实例。我们还没有写CameraModel;我们将在完成应用的所有 UI 部分后这样做。

symbolArraytextArray在本课程稍后绘制符号和文本时使用。

gpsSymbol是,嗯,GPS 符号。symbolContainer是 GPS 符号的容器。name是每个标记的唯一标识符,使用维基百科文章的文章标题和推文的用户名设置。physicalLocation是标记的物理位置(真实位置)。distance以米为单位存储用户到physicalLocation的距离。isOnRadarisInView用作标记,以跟踪标记的可见性。symbolXyzRelativeToCameraViewtextXyzRelativeToCameraViewlocationXyzRelativeToPhysicalLocation分别用于确定标记符号和文本相对于摄像机视图的位置,以及用户相对于物理位置的位置。x是向上/向下;y是左/右;而z不是用来,而是用来完成矢量的。颜色int是标记的默认颜色,设置为白色。

debugTouchZonedebugCollisionZone是我们用来启用和禁用两个区域的调试的两个标志。touchBoxtouchPositioncollisionBoxcollisionPosition用于绘制不透明框,帮助我们调试 app。

图 9-1 显示了在debugTouchZonedebugCollisionZone没有设置为false的情况下 app 运行;在图 9.2 中,它们被设置为true

Image

图 9-2。 禁用触摸碰撞调试运行的 app

Image

图 9-3。 启用触摸碰撞调试运行的 app

构造函数和 set()方法

现在我们来看看构造函数和set()方法:

清单 9-57。 构造函数和 set()方法

`public Marker(String name, double latitude, double longitude, double
altitude, int color) {
set(name, latitude, longitude, altitude, color);
}
public synchronized void set(String name, double latitude, double
longitude, double altitude, int color) {
if (name==null) throw new NullPointerException();

this.name = name;
this.physicalLocation.set(latitude,longitude,altitude);
this.color = color;
this.isOnRadar = false;
this.isInView = false;
this.symbolXyzRelativeToCameraView.set(0, 0, 0);
this.textXyzRelativeToCameraView.set(0, 0, 0);
this.locationXyzRelativeToPhysicalLocation.set(0, 0, 0);
this.initialY = 0.0f;
}`

构造函数采用标记的名称;其纬度、经度和高度为PhysicalLocation;和颜色作为参数,然后将它们传递给set()方法。set()方法将这些值设置为在清单 9-56 中描述和给出的变量。它还处理摄像机的一些基本初始化和屏幕上标记的位置。

get()方法

现在让我们看看Marker类的各种get()方法:

清单 9-58。get()方法

`public synchronized String getName(){
return this.name;
}

public synchronized int getColor() {
return this.color;
}

public synchronized double getDistance() {
return this.distance;
}

public synchronized float getInitialY() {
return this.initialY;
}

public synchronized boolean isOnRadar() {
return this.isOnRadar;
}

public synchronized boolean isInView() {
return this.isInView;
}

public synchronized Vector getScreenPosition() {
symbolXyzRelativeToCameraView.get(symbolArray);
textXyzRelativeToCameraView.get(textArray);
float x = (symbolArray[0] + textArray[0])/2;
float y = (symbolArray[1] + textArray[1])/2;
float z = (symbolArray[2] + textArray[2])/2;

if (textBox!=null) y += (textBox.getHeight()/2);

screenPositionVector.set(x, y, z);
return screenPositionVector;
}

public synchronized Vector getLocation() {
return this.locationXyzRelativeToPhysicalLocation;
} public synchronized float getHeight() {
if (symbolContainernull || textContainernull) return 0f;
return symbolContainer.getHeight()+textContainer.getHeight();
}

public synchronized float getWidth() {
if (symbolContainernull || textContainernull) return 0f;
float w1 = textContainer.getWidth();
float w2 = symbolContainer.getWidth();
return (w1>w2)?w1:w2;
}`

getName(), getColor(), getDistance(), getLocation(), isInView(), isOnRadar()getInitialY()只是返回名称所指示的值。getHeight()将文本和符号图像的高度相加并返回。getWidth()检查并返回文本和符号图像之间的较大宽度。getScreenPosition()通过使用文本和符号的位置,计算标记在屏幕上相对于摄像机视图的位置。

update()和 populateMatrices()方法

现在让我们来看看update()populateMatrices()方法:

清单 9-59。 更新()和填充矩阵()

`public synchronized void update(Canvas canvas, float addX, float addY) {
if (canvas==null) throw new NullPointerException();

if (cam==null) cam = new CameraModel(canvas.getWidth(),
canvas.getHeight(), true);
cam.set(canvas.getWidth(), canvas.getHeight(), false);
cam.setViewAngle(CameraModel.DEFAULT_VIEW_ANGLE);
populateMatrices(cam, addX, addY);
updateRadar();
updateView();
}

private synchronized void populateMatrices(CameraModel cam, float addX,
float addY) {
if (cam==null) throw new NullPointerException();

tmpSymbolVector.set(symbolVector);
tmpSymbolVector.add(locationXyzRelativeToPhysicalLocation);
tmpSymbolVector.prod(ARData.getRotationMatrix());

tmpTextVector.set(textVector); tmpTextVector.add(locationXyzRelativeToPhysicalLocation);
tmpTextVector.prod(ARData.getRotationMatrix());

cam.projectPoint(tmpSymbolVector, tmpVector, addX, addY);
symbolXyzRelativeToCameraView.set(tmpVector);
cam.projectPoint(tmpTextVector, tmpVector, addX, addY);
textXyzRelativeToCameraView.set(tmpVector);
}`

update()方法用于更新视图和填充矩阵。我们首先确保canvas不是一个null值,如果cam还没有初始化,就初始化它。然后我们更新cam的属性,使之与正在使用的canvas相匹配,并设置它的视角。视角是在CameraModel中定义的,这个类我们会在本章后面写。然后它调用populateMatrices()方法,传递cam对象,以及要作为参数添加到标记的 X 和 Y 位置的值。在那之后,update()进一步调用updateRadar()updateView().populateMatrices(),中,我们找到文本的位置和标记的符号,给定我们从ARData中得到的旋转矩阵,一个我们将在本章稍后编写的类。然后,我们使用这些数据将文本和符号投射到摄像机视图上。

updateView()和 updateRadar()方法

现在让我们看看由update()调用的updateView()updateRadar()方法。

清单 9-60。 更新雷达()和更新视图()

`private synchronized void updateRadar() {
isOnRadar = false;

float range = ARData.getRadius() * 1000;
float scale = range / Radar.RADIUS;
locationXyzRelativeToPhysicalLocation.get(locationArray);
float x = locationArray[0] / scale;
float y = locationArray[2] / scale; // z==y Switched on purpose
symbolXyzRelativeToCameraView.get(symbolArray);
if ((symbolArray[2] < -1f) &&
((xx+yy)<(Radar.RADIUS*Radar.RADIUS))) {
isOnRadar = true;
}
}

private synchronized void updateView() {
isInView = false;

symbolXyzRelativeToCameraView.get(symbolArray); float x1 = symbolArray[0] + (getWidth()/2);
float y1 = symbolArray[1] + (getHeight()/2);
float x2 = symbolArray[0] - (getWidth()/2);
float y2 = symbolArray[1] - (getHeight()/2);
if (x1>=-1 && x2<=(cam.getWidth())
&&
y1>=-1 && y2<=(cam.getHeight())
) {
isInView = true;
}
}`

updateRadar()用于更新雷达上标记的位置。如果发现标记的位置应该显示在雷达上,它的OnRadar被更新为trueupdateView()updateRadar()做同样的事情,除了它检查标记当前是否可见。

calcRelativePosition()和 updateDistance()方法

现在让我们来看看calcRelativePosition()updateDistance()方法:

清单 9-61。 calcRelativePosition()和 updateDistance()

`public synchronized void calcRelativePosition(Location location) {
if (location==null) throw new NullPointerException();

updateDistance(location);

if (physicalLocation.getAltitude()==0.0) physicalLocation.setAltitude(location.getAltitude());

PhysicalLocationUtility.convLocationToVector(location,
physicalLocation, locationXyzRelativeToPhysicalLocation);
this.initialY = locationXyzRelativeToPhysicalLocation.getY();
updateRadar();
}

private synchronized void updateDistance(Location location) {
if (location==null) throw new NullPointerException();

Location.distanceBetween(physicalLocation.getLatitude(),
physicalLocation.getLongitude(), location.getLatitude(),
location.getLongitude(), distanceArray);
distance = distanceArray[0];
}`

calcRelativePosition()中,我们使用作为参数接收的位置计算新的相对位置。我们检查在physicalLocation中是否有标记的有效高度;如果没有,我们将它设置为用户当前的海拔高度。然后,我们使用这些数据创建一个向量,用这个向量更新initialY变量,最后我们调用updateRadar()用新的相对位置更新雷达。updateDistance()简单地计算标记的物理位置和用户位置之间的新距离。

handleClick()、isMarkerOnMarker()和 isPointOnMarker()方法

现在让我们来看看我们是如何处理点击的,看看这个标记是否与另一个标记重叠:

清单 9-62。 检查点击和重叠

`public synchronized boolean handleClick(float x, float y) {
if (!isOnRadar || !isInView) return false;
return isPointOnMarker(x,y,this);
}

public synchronized boolean isMarkerOnMarker(Marker marker) {
return isMarkerOnMarker(marker,true);
}

private synchronized boolean isMarkerOnMarker(Marker marker, boolean
reflect) {
marker.getScreenPosition().get(screenPositionArray);
float x = screenPositionArray[0];
float y = screenPositionArray[1];
boolean middleOfMarker = isPointOnMarker(x,y,this);
if (middleOfMarker) return true;

float halfWidth = marker.getWidth()/2;
float halfHeight = marker.getHeight()/2;

float x1 = x - halfWidth;
float y1 = y - halfHeight;
boolean upperLeftOfMarker = isPointOnMarker(x1,y1,this);
if (upperLeftOfMarker) return true;

float x2 = x + halfWidth;
float y2 = y1;
boolean upperRightOfMarker = isPointOnMarker(x2,y2,this);
if (upperRightOfMarker) return true;

float x3 = x1; float y3 = y + halfHeight;
boolean lowerLeftOfMarker = isPointOnMarker(x3,y3,this);
if (lowerLeftOfMarker) return true;

float x4 = x2;
float y4 = y3;
boolean lowerRightOfMarker = isPointOnMarker(x4,y4,this);
if (lowerRightOfMarker) return true;

return (reflect)?marker.isMarkerOnMarker(this,false):false;
}

private synchronized boolean isPointOnMarker(float x, float y, Marker
marker) {
marker.getScreenPosition().get(screenPositionArray);
float myX = screenPositionArray[0];
float myY = screenPositionArray[1];
float adjWidth = marker.getWidth()/2;
float adjHeight = marker.getHeight()/2;

float x1 = myX-adjWidth;
float y1 = myY-adjHeight;
float x2 = myX+adjWidth;
float y2 = myY+adjHeight;

if (x>=x1 && x<=x2 && y>=y1 && y<=y2) return true;

return false;
}`

handleClick()将点击的 X 和 Y 点作为参数。如果标记不在雷达上,也不在视图中,它返回false。否则,它会通过调用isPointOnMarker()返回找到的任何内容。

第一个isMarkerOnMarker()简单地返回第二个isMarkerOnMarker()方法发现的任何内容。第二个isMarkerOnMarker()方法包含我们用来确定作为参数接收的标记是否与当前标记重叠的所有代码。我们检查所有四个角和标记中心的重叠。如果其中任何一个是true,我们可以有把握地说标记是重叠的。

isPointOnMarker()检查传递的 X 和 Y 坐标是否位于标记上。

draw()方法

现在我们来看看draw()方法:

清单 9-63。 当先()

`public synchronized void draw(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

if (!isOnRadar || !isInView) return;

if (debugTouchZone) drawTouchZone(canvas);
if (debugCollisionZone) drawCollisionZone(canvas);
drawIcon(canvas);
drawText(canvas);
}`

draw()方法很简单。它决定是否应该显示标记。如果要显示它,它会绘制它,并在需要时绘制调试框。这就是它的全部功能。

drawTouchZone()、drawCollisionZone()、drawIcon()和 drawText()方法

绘图的主要工作是在drawTouchZone()drawCollisionZone()drawIcon()drawText()方法中完成的,我们现在来看看:

清单 9-64。 drawTouchZone()、drawCollisionZone()、drawIcon()和 drawText()

`protected synchronized void drawCollisionZone(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

getScreenPosition().get(screenPositionArray);
float x = screenPositionArray[0];
float y = screenPositionArray[1];

float width = getWidth();
float height = getHeight();
float halfWidth = width/2;
float halfHeight = height/2;

float x1 = x - halfWidth;
float y1 = y - halfHeight; float x2 = x + halfWidth;
float y2 = y1;

float x3 = x1;
float y3 = y + halfHeight;

float x4 = x2;
float y4 = y3;

Log.w("collisionBox", "ul (x="+x1+" y="+y1+")");
Log.w("collisionBox", "ur (x="+x2+" y="+y2+")");
Log.w("collisionBox", "ll (x="+x3+" y="+y3+")");
Log.w("collisionBox", "lr (x="+x4+" y="+y4+")");

if (collisionBox==null) collisionBox = new
PaintableBox(width,height,Color.WHITE,Color.RED);
else collisionBox.set(width,height);

float currentAngle = Utilities.getAngle(symbolArray[0], symbolArray[1],
textArray[0], textArray[1])+90;

if (collisionPosition==null) collisionPosition = new
PaintablePosition(collisionBox, x1, y1, currentAngle, 1);
else collisionPosition.set(collisionBox, x1, y1, currentAngle, 1);
collisionPosition.paint(canvas);
}

protected synchronized void drawTouchZone(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

if (gpsSymbol==null) return;

symbolXyzRelativeToCameraView.get(symbolArray);
textXyzRelativeToCameraView.get(textArray);
float x1 = symbolArray[0];
float y1 = symbolArray[1];
float x2 = textArray[0];
float y2 = textArray[1];
float width = getWidth();
float height = getHeight();
float adjX = (x1 + x2)/2;
float adjY = (y1 + y2)/2;
float currentAngle = Utilities.getAngle(symbolArray[0], symbolArray[1],
textArray[0], textArray[1])+90;
adjX -= (width/2);
adjY -= (gpsSymbol.getHeight()/2);

Log.w("touchBox", "ul (x="+(adjX)+" y="+(adjY)+")");
Log.w("touchBox", "ur (x="+(adjX+width)+" y="+(adjY)+")");
Log.w("touchBox", "ll (x="+(adjX)+" y="+(adjY+height)+")");
Log.w("touchBox", "lr (x="+(adjX+width)+" y="+(adjY+height)+")");

if (touchBoxnull) touchBox = new
PaintableBox(width,height,Color.WHITE,Color.GREEN);
else touchBox.set(width,height); if (touchPosition
null) touchPosition = new PaintablePosition(touchBox,
adjX, adjY, currentAngle, 1);
else touchPosition.set(touchBox, adjX, adjY, currentAngle, 1);
touchPosition.paint(canvas);
}

protected synchronized void drawIcon(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

if (gpsSymbol==null) gpsSymbol = new PaintableGps(36, 36, true,
getColor());

textXyzRelativeToCameraView.get(textArray);
symbolXyzRelativeToCameraView.get(symbolArray);

float currentAngle = Utilities.getAngle(symbolArray[0], symbolArray[1],
textArray[0], textArray[1]);
float angle = currentAngle + 90;

if (symbolContainer==null) symbolContainer = new
PaintablePosition(gpsSymbol, symbolArray[0], symbolArray[1], angle, 1);
else symbolContainer.set(gpsSymbol, symbolArray[0], symbolArray[1],
angle, 1);

symbolContainer.paint(canvas);
}

protected synchronized void drawText(Canvas canvas) {
if (canvas==null) throw new NullPointerException();

String textStr = null;
if (distance<1000.0) {
textStr = name + " ("+ DECIMAL_FORMAT.format(distance) + "m)";
} else {
double d=distance/1000.0;
textStr = name + " (" + DECIMAL_FORMAT.format(d) + "km)";
}

textXyzRelativeToCameraView.get(textArray);
symbolXyzRelativeToCameraView.get(symbolArray);

float maxHeight = Math.round(canvas.getHeight() / 10f) + 1;
if (textBox==null) textBox = new PaintableBoxedText(textStr,
Math.round(maxHeight / 2f) + 1, 300);
else textBox.set(textStr, Math.round(maxHeight / 2f) + 1, 300);

float currentAngle = Utilities.getAngle(symbolArray[0],
symbolArray[1], textArray[0], textArray[1]);
float angle = currentAngle + 90; float x = textArray[0] - (textBox.getWidth() / 2);
float y = textArray[1] + maxHeight;

if (textContainer==null) textContainer = new
PaintablePosition(textBox, x, y, angle, 1);
else textContainer.set(textBox, x, y, angle, 1);
textContainer.paint(canvas);
}`

drawCollisionZone()方法绘制两个标记之间的碰撞区域(如果有一个要绘制的话)。drawTouchZone()在我们听标记上的触摸的区域上画一个红色的矩形。drawIcon()方法绘制图标,drawText()方法绘制相关文本。

compareTo()和 equals()方法

现在让我们看看最后两个方法,compareTo()equals():

清单 9-65。 compareTo()和 equals()

`public synchronized int compareTo(Marker another) {
if (another==null) throw new NullPointerException();

return name.compareTo(another.getName());
}

@Override
public synchronized boolean equals(Object marker) {
if(markernull || namenull) throw new NullPointerException();

return name.equals(((Marker)marker).getName());
}
}`

compareTo()使用标准 Java 字符串函数比较两个标记的名称。equals()使用标准的 Java 字符串函数来检查一个标记与另一个标记的名称。

这将我们带到了Marker.java文件的末尾。现在我们来看看它唯一的子类,IconMarker.java

IconMarker.java

将位图绘制为标记的图标,而不是保留默认设置。是Marker.java的延伸。

清单 9-66。图标标记

`public class IconMarker extends Marker {
private Bitmap bitmap = null;

public IconMarker(String name, double latitude, double longitude, double
altitude, int color, Bitmap bitmap) {
super(name, latitude, longitude, altitude, color);
this.bitmap = bitmap;
}

@Override
public void drawIcon(Canvas canvas) {
if (canvasnull || bitmapnull) throw new NullPointerException();

if (gpsSymbol==null) gpsSymbol = new PaintableIcon(bitmap,96,96);

textXyzRelativeToCameraView.get(textArray);
symbolXyzRelativeToCameraView.get(symbolArray);

float currentAngle = Utilities.getAngle(symbolArray[0], symbolArray[1],
textArray[0], textArray[1]);
float angle = currentAngle + 90;

if (symbolContainer==null) symbolContainer = new
PaintablePosition(gpsSymbol, symbolArray[0], symbolArray[1], angle, 1);
else symbolContainer.set(gpsSymbol, symbolArray[0], symbolArray[1],
angle, 1);

symbolContainer.paint(canvas);
}
}`

构造函数接受对Marker.javasuper()调用所需的所有参数,以及一个标记位图的额外参数。drawIcon()然后使用来自Marker.java的数据绘制我们在构造函数中收到的位图,作为这个标记的图标。

这就把我们带到了 UI 组件的结尾。现在让我们来看看VerticalSeekBar.java,我们对 Android SeekBar 的定制扩展。

定制小工具

我们在应用中定制了一个标准的 Android 小部件。我们扩展了SeekBar来创建VerticalSeekBar

垂直杆. java

VerticalSeekBar是 Android 的SeekBar实现的扩展。我们的附加代码允许它垂直工作,而不是水平工作。应用中使用的zoomBar是这个类的一个实例。

清单 9-67。VerticalSeekBar.java??

`public class VerticalSeekBar extends SeekBar {

public VerticalSeekBar(Context context) {
super(context);
}

public VerticalSeekBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

public VerticalSeekBar(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(h, w, oldh, oldw);
}

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int
heightMeasureSpec) {
super.onMeasure(heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
}

@Override
protected void onDraw(Canvas c) {
c.rotate(-90);
c.translate(-getHeight(), 0);

super.onDraw(c);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}

switch (event.getAction()) { case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
setProgress(getMax() - (int) (getMax() * event.getY() /
getHeight()));
onSizeChanged(getWidth(), getHeight(), 0, 0);
break;

case MotionEvent.ACTION_CANCEL:
break;
}
return true;
}
}`

我们的三个构造函数用于将它绑定到父类SeekBaronSizeChanged()也可以追溯到SeekBar类。onMeasure()执行一个super()调用,并使用 Android 的View类提供的方法设置测量的高度和宽度。实际的修改是在onDraw()中完成的,在将画布传递给SeekBar之前,我们将画布旋转 90 度,这样绘图是垂直完成的,而不是水平完成的。在onTouchEvent()中,我们调用setProgress()onSizeChanged()来允许我们旋转SeekBar以正常工作。

现在让我们来看看控制摄像机所需的三个类。

控制相机

与任何 AR 应用一样,我们也必须在这个应用中使用摄像头。由于这个应用的性质,相机控制已经放在三个类中,我们现在将通过。

CameraSurface.java

我们要看的第一个类是CameraSurface。这个类处理所有摄像机的SurfaceView相关代码:

清单 9-68。 变量和构造函数

`public class CameraSurface extends SurfaceView implements SurfaceHolder.Callback
{
private static SurfaceHolder holder = null;
private static Camera camera = null;

public CameraSurface(Context context) { super(context);

try {
holder = getHolder();
holder.addCallback(this);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
} catch (Exception ex) {
ex.printStackTrace();
}
}`

我们一开始就声明了SurfaceHoldercamera。然后,我们使用构造函数初始化SurfaceHolder,并将其类型设置为SURFACE_TYPE_PUSH_BUFFERS,这允许它从摄像机接收数据。

现在我们来看看surfaceCreated()方法:

清单 9-69。 surfaceCreated()

`public void surfaceCreated(SurfaceHolder holder) {
try {
if (camera != null) {
try {
camera.stopPreview();
} catch (Exception ex) {
ex.printStackTrace();
}
try {
camera.release();
} catch (Exception ex) {
ex.printStackTrace();
}
camera = null;
}

camera = Camera.open();
camera.setPreviewDisplay(holder);
} catch (Exception ex) {
try {
if (camera != null) {
try {
camera.stopPreview();
} catch (Exception ex1) {
ex.printStackTrace();
}
try {
camera.release();
} catch (Exception ex2) {
ex.printStackTrace();
} camera = null;
}
} catch (Exception ex3) {
ex.printStackTrace();
}
}
}`

我们使用surfaceCreated()来创建camera对象,如果它已经存在或者我们遇到了问题,也可以释放它。

现在让我们继续进行surfaceDestroyed()

清单 9-70。 【地表摧毁】(

public void surfaceDestroyed(SurfaceHolder holder) { try { if (camera != null) { try { camera.stopPreview(); } catch (Exception ex) { ex.printStackTrace(); } try { camera.release(); } catch (Exception ex) { ex.printStackTrace(); } camera = null; } } catch (Exception ex) { ex.printStackTrace(); } }

这是简单的代码,与我们的其他示例应用非常相似。我们停止使用相机并释放它,以便我们自己的或另一个第三方或系统应用可以访问它。

事不宜迟,我们来看看这个类的最后一个方法,surfaceChanged():

清单 9-71。 surfaceChanged()

public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { try { Camera.Parameters parameters = camera.getParameters(); try { List<Camera.Size> supportedSizes = null; `supportedSizes =
CameraCompatibility.getSupportedPreviewSizes(parameters);

float ff = (float)w/h;

float bff = 0;
int bestw = 0;
int besth = 0;
Iterator<Camera.Size> itr = supportedSizes.iterator();

while(itr.hasNext()) {
Camera.Size element = itr.next();
float cff = (float)element.width/element.height;

if ((ff-cff <= ff-bff) && (element.width <= w) &&
(element.width >= bestw)) {
bff=cff;
bestw = element.width;
besth = element.height;
}
}

if ((bestw == 0) || (besth == 0)){
bestw = 480;
besth = 320;
}
parameters.setPreviewSize(bestw, besth);
} catch (Exception ex) {
parameters.setPreviewSize(480, 320);
}

camera.setParameters(parameters);
camera.startPreview();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}`

我们使用surfaceChanged()来计算预览尺寸,从而得到最接近设备屏幕宽高比(ff)的宽高比(形状系数,存储在bff)。我们还确保预览尺寸不大于屏幕尺寸,因为 HTC Hero 等一些手机报告的尺寸较大,当应用使用它们时会崩溃。我们在结尾还有一个if声明,以防止宽度和高度的值为 0,这可能发生在一些三星手机上。

现在让我们继续剩下的两个类:CameraCompatibilityCameraModel

相机兼容性

允许我们的应用保持与所有版本 Android 的兼容性,并避开旧版本 API 的限制。它改编自 Mixare 项目,类似于Vector类。

清单 9-72。??【相机兼容性】

`public class CameraCompatibility {
private static Method getSupportedPreviewSizes = null;
private static Method mDefaultDisplay_getRotation = null;

static {
initCompatibility();
};

private static void initCompatibility() {
try {
getSupportedPreviewSizes =
Camera.Parameters.class.getMethod("getSupportedPreviewSizes", new Class[] { } );
mDefaultDisplay_getRotation =
Display.class.getMethod("getRotation", new Class[] { } );
} catch (NoSuchMethodException nsme) {
}
}

public static int getRotation(Activity activity) {
int result = 1;
try {
Display display = ((WindowManager)
activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
Object retObj = mDefaultDisplay_getRotation.invoke(display);
if(retObj != null) result = (Integer) retObj;
} catch (Exception ex) {
ex.printStackTrace();
}
return result;
}

public static List<Camera.Size>
getSupportedPreviewSizes(Camera.Parameters params) {
List<Camera.Size> retList = null;

try {
Object retObj = getSupportedPreviewSizes.invoke(params);
if (retObj != null) {
retList = (List<Camera.Size>)retObj;
}
} catch (InvocationTargetException ite) { Throwable cause = ite.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
} else {
throw new RuntimeException(ite);
}
} catch (IllegalAccessException ie) {
ie.printStackTrace();
}
return retList;
}
}`

在低于 Android 2.0 的设备上失败,这允许我们优雅地将相机设置为 480 x320 的默认预览大小。getRotation()允许我们检索设备的旋转。getSupportedPreviewSizes()返回设备上可用的预览尺寸列表。

现在我们来看看CameraModel,相机类的最后一个,也是这个 app 的倒数第二个类。

照相机模型

CameraModel代表摄像机及其视图,也允许我们投射点。这是另一个由 Mixare 改编的类。

清单 9-73。 相机模型

`public class CameraModel {
private static final float[] tmp1 = new float[3];
private static final float[] tmp2 = new float[3];

private int width = 0;
private int height = 0;
private float distance = 0F;

public static final float DEFAULT_VIEW_ANGLE = (float)
Math.toRadians(45);

public CameraModel(int width, int height, boolean init) {
set(width, height, init);
}

public void set(int width, int height, boolean init) {
this.width = width; this.height = height;
}

public int getWidth() {
return width;
}

public int getHeight() {
return height;
}

public void setViewAngle(float viewAngle) {
this.distance = (this.width / 2) / (float) Math.tan(viewAngle /
2);
}

public void projectPoint(Vector orgPoint, Vector prjPoint, float addX,
float addY) {
orgPoint.get(tmp1);
tmp2[0]=(distance * tmp1[0] / -tmp1[2]);
tmp2[1]=(distance * tmp1[1] / -tmp1[2]);
tmp2[2]=(tmp1[2]);
tmp2[0]=(tmp2[0] + addX + width / 2);
tmp2[1]=(-tmp2[1] + addY + height / 2);
prjPoint.set(tmp2);
}
}`

构造函数设置类的宽度和高度。getWidth()getHeight()分别返回宽度和高度。setViewAngle()用新的视角更新距离。projectPoint()使用原点向量、投影向量以及 X 和 y 坐标的加法来投影一个点。

全球阶级

现在让我们来看看我们的最后一节课,ARData

ARData.java

ARData作为全局控制和存储类;它存储整个应用中使用的数据,这些数据对于应用的运行至关重要。它使我们更容易将所有这些数据存储在一个地方,而不是分散在各处。

清单 9-74。ar data 的全局变量

`public abstract class ARData {
private static final String TAG = "ARData";
private static final Map<String,Marker> markerList = new ConcurrentHashMap<String,Marker>();
private static final List cache = new CopyOnWriteArrayList();
private static final AtomicBoolean dirty = new AtomicBoolean(false);
private static final float[] locationArray = new float[3];

public static final Location hardFix = new Location("ATL");
static {
hardFix.setLatitude(0);
hardFix.setLongitude(0);
hardFix.setAltitude(1);
}

private static final Object radiusLock = new Object();
private static float radius = new Float(20);
private static String zoomLevel = new String();
private static final Object zoomProgressLock = new Object();
private static int zoomProgress = 0;
private static Location currentLocation = hardFix;
private static Matrix rotationMatrix = new Matrix();
private static final Object azimuthLock = new Object();
private static float azimuth = 0;
private static final Object pitchLock = new Object();
private static float pitch = 0;
private static final Object rollLock = new Object();
private static float roll = 0;`

TAG是在 LogCat 中显示消息时使用的字符串。markerList是标记及其名称的散列表。cache是,嗯,一个贮藏处。dirty用于判断状态是否为脏。locationArray是位置数据的数组。hardFix是默认位置,与我们拥有的 ATL 标记相同。radius是雷达半径;zoomProgress是我们应用中的缩放进度。pitchazimuthroll分别保存俯仰、方位角和滚动值。前面名称中添加了Lock的变量是这些变量的同步块的锁对象。zoomLevel是目前的变焦水平。currentLocation存储当前位置,默认设置为hardFix。最后,rotationMatrix存储旋转矩阵。

现在让我们看看这个类的各种 getter 和 setter 方法:

清单 9-75。ARData 的获取器和设置器

public static void setZoomLevel(String zoomLevel) { `if (zoomLevel==null) throw new NullPointerException();

synchronized (ARData.zoomLevel) {
ARData.zoomLevel = zoomLevel;
}
}

public static void setZoomProgress(int zoomProgress) {
synchronized (ARData.zoomProgressLock) {
if (ARData.zoomProgress != zoomProgress) {
ARData.zoomProgress = zoomProgress;
if (dirty.compareAndSet(false, true)) {
Log.v(TAG, "Setting DIRTY flag!");
cache.clear();
}
}
}
}

public static void setRadius(float radius) {
synchronized (ARData.radiusLock) {
ARData.radius = radius;
}
}

public static float getRadius() {
synchronized (ARData.radiusLock) {
return ARData.radius;
}
}

public static void setCurrentLocation(Location currentLocation) {
if (currentLocation==null) throw new NullPointerException();

Log.d(TAG, "current location. location="+currentLocation.toString());
synchronized (currentLocation) {
ARData.currentLocation = currentLocation;
}
onLocationChanged(currentLocation);
}

public static Location getCurrentLocation() {
synchronized (ARData.currentLocation) {
return ARData.currentLocation;
}
}

public static void setRotationMatrix(Matrix rotationMatrix) {
synchronized (ARData.rotationMatrix) {
ARData.rotationMatrix = rotationMatrix; }
}

public static Matrix getRotationMatrix() {
synchronized (ARData.rotationMatrix) {
return rotationMatrix;
}
}

public static List getMarkers() {
if (dirty.compareAndSet(true, false)) {
Log.v(TAG, "DIRTY flag found, resetting all marker heights to
zero.");
for(Marker ma : markerList.values()) {
ma.getLocation().get(locationArray);
locationArray[1]=ma.getInitialY();
ma.getLocation().set(locationArray);
}

Log.v(TAG, "Populating the cache.");
List copy = new ArrayList();
copy.addAll(markerList.values());
Collections.sort(copy,comparator);
cache.clear();
cache.addAll(copy);
}
return Collections.unmodifiableList(cache);
}

public static void setAzimuth(float azimuth) {
synchronized (azimuthLock) {
ARData.azimuth = azimuth;
}
}

public static float getAzimuth() {
synchronized (azimuthLock) {
return ARData.azimuth;
}
}

public static void setPitch(float pitch) {
synchronized (pitchLock) {
ARData.pitch = pitch;
}
}

public static float getPitch() {
synchronized (pitchLock) {
return ARData.pitch; }
}

public static void setRoll(float roll) {
synchronized (rollLock) {
ARData.roll = roll;
}
}

public static float getRoll() {
synchronized (rollLock) {
return ARData.roll;
}
}`

所有方法都只是简单地设置或获取它们名称中提到的变量,使用同步块来确保数据不会被应用的两个不同部分同时修改。在getMarkers()方法中,我们迭代标记以返回所有标记。现在我们来看看这个类的最后几个方法。

清单 9-76。 addMarkers()、comparator 和 onLocationChanged()

`private static final Comparator comparator = new Comparator() {
public int compare(Marker arg0, Marker arg1) {
return Double.compare(arg0.getDistance(),arg1.getDistance());
}
};

public static void addMarkers(Collection markers) {
if (markers==null) throw new NullPointerException();

if (markers.size()<=0) return;

Log.d(TAG, "New markers, updating markers. new
markers="+markers.toString());
for(Marker marker : markers) {
if (!markerList.containsKey(marker.getName())) {
marker.calcRelativePosition(ARData.getCurrentLocation());
markerList.put(marker.getName(),marker);
}
}

if (dirty.compareAndSet(false, true)) {
Log.v(TAG, "Setting DIRTY flag!");
cache.clear();
}
}

private static void onLocationChanged(Location location) { Log.d(TAG, "New location, updating markers.
location="+location.toString());
for(Marker ma: markerList.values()) {
ma.calcRelativePosition(location);
}

if (dirty.compareAndSet(false, true)) {
Log.v(TAG, "Setting DIRTY flag!");
cache.clear();
}
}
}`

comparator用于比较一个标记到另一个标记的距离。addMarkers()用于从传递的集合中添加新的标记。onLocationChanged()用于更新标记相对于新位置的相对位置。

AndroidManifest.xml

最后,我们必须创建如下的 Android 清单:

清单 9-77。 AndroidManifest.xml

`

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"

android:theme="@android:style/Theme.NoTitleBar.Fullscreen">


`

运行应用

运行应用时,您应该记住几件事。

如果没有互联网连接,将不会出现任何数据,因此不会显示任何标记。

有些地方找不到推文和维基百科文章的数据。这更有可能是推文。此外,在晚上的某些时候,推文的数量可能会很少。

一些标记可能比其他标记高。这是由于避免碰撞和位置的高度特性。

图 9.4 和 9.5 显示了运行中的应用。调试已被禁用。

Image

图 9-4。 显示标记的应用

Image

图 9-5。 由于指南针在一个金属物体旁边失控,几个标记汇聚在一起。

总结

本章讲述了创建 ar 浏览器的过程并提供了代码,这是最流行的 AR 应用之一。随着 Google Goggles 的出现,它将在那些想使用类似 Goggles 的东西而不需要实际拥有一套的人中间变得更受欢迎。请务必从这本书的页面或 GitHub 库下载本章的源代码。您可以直接通过raghavsood@appaholics.in联系我,或者通过 Twitter 的@Appaholics16联系我。*

posted @   绝不原创的飞龙  阅读(85)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示