增强现实是移动计算领域相对较新的发展。尽管它还很年轻,但它已经是这个行业中发展最快的领域之一。公司正在投入大量资金开发使用增强现实的产品,其中最引人注目的是谷歌的谷歌眼镜项目。大多数人认为增强现实很难实现。那是一种误解。像任何好的应用一样,好的增强现实应用需要花费一些精力来编写。你所需要做的就是在潜水前保持开放的心态。
这本书的目标读者是那些想为谷歌的Android平台编写应用的人。这本书希望读者熟悉Java语言,并了解Android的基础知识。然而,已经做出努力来确保即使没有这种经验的人也能理解内容和代码。希望当你看完这本书的时候,你会知道如何使用增强现实的力量编写令人惊叹的丰富的Android应用。
这本书分为九章。我们从增强现实的基本介绍开始,并逐步介绍越来越复杂的功能。在第五章中,我们来看看如何处理增强现实应用中可能出现的常见错误。之后,我们有四个示例应用,展示如何使用日益复杂的增强现实应用。这里给出了更详细的结构:
本书包含一些相当高级的代码,并且假设您熟悉以下内容:
虽然具备所有这些先决条件并不是绝对的要求,但强烈建议这样做。你绝对需要一个Android设备来测试你的应用,因为应用中使用的许多功能在Android模拟器上是不可用的。
如果您在我们的代码中发现了一个bug,请在GitHub仓库中提出问题,或者通过下面给出的方式直接联系作者。
增强现实(AR)是一个相当新的、但仍然很大的领域。它没有很大的市场份额,而且它目前的大部分应用都只是原型。这使得AR成为一个非常令人期待且尚未开发的利基市场。目前Android市场上实现AR技术的应用非常少。这一章描述了AR在现实世界中的应用,给出了例子(在可能的情况下提供了图片),并讨论了现在是否有可能在Android平台上实现AR。
增强现实(AR)和虚拟现实(VR)是界限有点模糊的领域。换一种说法,你可以把VR看成AR的前身,两者有部分重叠。这两种技术的主要区别在于,VR不使用摄像头馈送。虚拟现实中显示的所有东西要么是动画,要么是预先录制的电影片段。
尽管这是一个相对较新的领域,但有足够多的AR应用可供我们进行分类。在这里,我们来看看在AR世界中已经实现了什么。
有数百个使用AR的应用旨在供普通人使用。它们有许多类型——例如,游戏、世界浏览器和导航应用。他们通常使用加速度计和GPS来获取设备的位置和物理状态。这些应用是用来享受和使用的。Android开发者挑战赛2的获奖应用之一是一款AR游戏:SpecTrek。游戏使用你的GPS找到你的位置,然后为你准备幽灵在周围地区狩猎。这款游戏还有一张地图,上面的鬼魂在谷歌地图上显示为标记。在游戏过程中,幽灵被添加到相机图像上。
另一方面,导航应用有识别道路和转弯的代码,并用箭头标出路线。这个过程并不像听起来那么简单,但今天却经常这样做。
休闲AR应用的最好例子是SpecTrek和Wikitude。总之,这些应用利用了几乎所有你可以用来在Android平台上制作AR应用的东西。我强烈建议您安装它们,并熟悉Android上ar的功能。
这个类别的大多数应用都可以在Android平台上实现。在一些情况下,他们甚至不使用所有的传感器。其中一些会变得相当复杂。图1-1和图1-2显示了来自SpecTrek的截图。
图1-1。SpecTrek的截图
图1-2。另一张SpecTrek的截图
军事和执法机构的使用要复杂得多,技术也更先进。它们的范围从AR护目镜到旨在帮助训练的全模拟器。军方和一些执法机构拥有利用AR技术的模拟器。房间或交通工具内的宽屏幕,屏幕上显示各种场景,受训者必须决定最佳行动方案。
一些先进的特种部队团队有基本的AR护目镜,随着视野中的土地,显示海拔高度,视角,光线强度等信息。这些信息是在现场用数学公式计算出来的,因为这些护目镜没有配备互联网连接。
专业的夜视镜也配备了AR技术。这些护目镜显示位置和其他信息,并试图填补夜视镜本身无法照亮的空白。
几乎所有的无人驾驶车辆都实现了AR。这些交通工具,尤其是空中交通工具,可能离它们的操作者有几千公里远。这些车辆的外部安装了一个或多个摄像头,将视频传输给操作人员。这些车辆中的大多数也配备了几个传感器。传感器数据与视频一起发送给操作员。该数据然后被处理并在视频上被增强。操作员系统上的算法处理视频,然后挑出并标记感兴趣的建筑物或物体。所有这些都以叠加的形式显示在视频上。
这些类型的应用很难在Android设备上实现,因为有两个主要问题:
最近,车辆已经开始实施AR技术。挡风玻璃已经被大而宽的高清显示屏所取代。通常车辆上有多个屏幕,每个屏幕显示一个特定的方向。如果只有一个屏幕和多个摄像头,车辆将自动切换馈送或让用户选择这样做。车辆外部有几个摄像头,面向多个方向。屏幕上的图像覆盖了有用的数据,如小地图、指南针、方向箭头、备用路线、天气预报等等。目前,这种技术在飞机和火车上最为常见。采用这种技术的智能汽车正在市场上接受测试。潜艇和船只也在使用这项技术。最近停止的航天飞机也有这种AR技术。
这些应用可以在Android平台上以一种混合的方式实现。因为大多数Android设备似乎缺乏正常车辆所具有的功能,所以没有实现同类功能。另一方面,可以编写应用,通过使用GPS获得位置来帮助导航;使用方向API来获取方向;并使用加速度计来帮助获取车辆的速度。Android设备提供AR电源,车辆提供车辆部分。
增强现实手术如今变得越来越普遍。以这种方式完成的手术出错率更低,因为计算机在手术中提供了有价值的输入,并使用这些信息来控制机器人执行部分或全部手术。计算机通常可以实时提供替代方案和指导,以改进手术。AR流以及其他数据也可以发送给远程医生,他们可以查看患者的信息,就好像患者就在他们面前一样。
AR技术还有其他的医疗应用。AR机器可以用来监测大量的病人,并确保他们的生命体征在任何时候都处于观察之中。
这种AR技术从未在Android平台上实现,原因有几个:
最重要的是,目前设计和构建这样一个应用非常困难和昂贵。允许在医疗领域进行实时增强现实工作所需的人工智能算法还没有出现。除此之外,你还需要一个非常优秀的开发团队,一个技术高超、经验丰富的医生团队,以及大量的资金。
在几家商店中,AR正在作为虚拟审判室进行试验。用户可以站在屏幕前,在某处安装一台摄像机。用户将看到自己显示在屏幕上。然后,用户使用鼠标或键盘等输入设备来选择任何可用的服装选项。然后,计算机会将该项目放大到用户的图像上,并显示在屏幕上。用户可以从各个角度观看自己。
原则上,这些应用可以为Android平台编写,但没有人这样做,因为缺乏兴趣,可能是因为不知道为什么有人会想要这样做。实际上这种类型的应用已经出现,但它们实际上是用于娱乐和修改人的面部特征。
这些应用的功能已经出现在世界各地的浏览器中,但显示的信息很少。还没有人实现过任何一个城市的完整版本,可以提供所需的信息。
有许多配有摄像头的机器可以从现有的结构中生成蓝图,或者在拟议的建筑工地上显示蓝图中的虚拟结构。这些加快了建筑工作,并有助于设计和检查建筑物。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和一些气味发射技术,整个体验可以变得栩栩如生,感觉完全真实。除此之外,穿上可以模仿触觉的连体衣会让它变得绝对且不可否认的真实。
如果它出现的话,这将很难在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应用的基础。*
现在,你对什么是增强现实(AR)、世界各地正在用它做什么以及你可以在Android设备上用它做什么有了一个基本的概念。本章将带你进入Android上的AR世界,并教你它的基础知识。为了帮助你理解本书中这里(和其他地方)所做的一切,我们将创建应用来演示我们正在学习的内容。本章将专注于制作一个基本的应用,它包含任何高级AR应用的四个主要部分:相机、GPS、加速度计和指南针。
这是一个非常简单的应用。它从GPS、指南针、照相机和加速度计接收的任何数据都没有覆盖和实际用途。在下一章,我们将在这个应用的基础上添加覆盖图。
首先,我们需要创建一个新项目。在包名中,我使用的是com.paar.ch2。您可以使用任何适合您的名称,但请确保更改这里代码中的任何引用,以匹配您的包名。该项目应设置为至少支持Android2.1。我正在针对Android4.0(冰淇淋三明治)构建项目,但您可以选择自己的目标。
每个AR应用中的第一件事是相机,它构成了AR中99%的现实(另外1%由3个基本传感器组成)。要在您的应用中使用相机,我们首先需要将权限请求和uses-feature行添加到我们的清单中。我们还必须告诉Android,我们希望我们的活动是风景,我们将自己处理某些配置更改。添加之后,清单应该类似于清单2-1:
清单2-1。更新舱单代码
我们也可以在
现在让我们来看看实际的摄像机代码。相机需要一个SurfaceView,在上面它会渲染它看到的东西。我们将使用SurfaceView创建一个XML布局,然后使用那个SurfaceView来显示相机预览。将您的XML文件(在本例中为main.xml)修改如下:
清单2-2。修改main.xml
这段代码没有什么突破性的东西。我们没有使用普通的布局,比如LinearLayout或RelativeLayout,而是简单地向XML文件添加了一个SurfaceView,其高度和宽度属性被设置为允许它填充整个可用屏幕。我们给它分配IDcameraPreview,这样我们就可以从代码中引用它。现在重要的一步是使用Android相机服务,并告诉它连接到我们的SurfaceView来显示来自相机的实际预览。
要实现这一点,需要做三件事:
清单2-3。进口和变量申报
`packagecom.paar.ch2;
importandroid.app.Activity;importandroid.hardware.Camera;importandroid.os.Bundle;importandroid.util.Log;importandroid.view.SurfaceHolder;importandroid.view.SurfaceView;
publicclassProAndroidAR2ActivityextendsActivity{SurfaceViewcameraPreview;SurfaceHolderpreviewHolder;Cameracamera;booleaninPreview;`
让我详细说明进口。第一个和第三个是显而易见的,但第二个是重要的,因为它是相机。确保从硬件包而不是图形包中导入Camera,因为那是一个不同的Camera类。SurfaceView和SurfaceHolder同样重要,但是没有两个选项可供选择。
关于变量。cameraPreview是一个SurfaceView变量,它将保存对XML布局中SurfaceView的引用(这将在onCreate()中完成)。previewHolder是管理SurfaceView的SurfaceHolder。camera是Camera对象,将处理所有相机的东西。最后,inPreview是我们的小布尔朋友,它将使用他的二元逻辑告诉我们一个预览是否是活动的,并给我们指示,以便我们可以正确地发布它。
现在我们继续为我们的小应用使用onCreate()方法:
清单2-4。onCreate()
`@OverridepublicvoidonCreate(BundlesavedInstanceState){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。
清单2-5。表面回调
SurfaceHolder.CallbacksurfaceCallback=newSurfaceHolder.Callback(){publicvoidsurfaceCreated(SurfaceHolderholder){try{camera.setPreviewDisplay(previewHolder);}catch(Throwablet){Log.e("ProAndroidAR2Activity","ExceptioninsetPreviewDisplay()",t);}}
现在,一旦Android对SurfaceView进行了设置和调整,我们需要将配置数据传递给Camera,这样它就知道应该绘制多大的预览。由于Android已经移植并安装在数百种不同的硬件设备上,因此没有办法安全地预先确定预览窗格的大小。等待我们的SurfaceHolder.Callback的surfaceChanged()方法被调用是非常简单的,因为这可以告诉我们SurfaceView的大小。然后我们可以将这些信息推入到一个Camera.Parameters对象中,用这些参数更新Camera,并让Camera通过startPreview()显示预览。现在我们可以回到编码:
清单2-6。sufaceChanged()
`publicvoidsurfaceChanged(SurfaceHolderholder,intformat,intwidth,intheight){Camera.Parametersparameters=camera.getParameters();Camera.Sizesize=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()
`@OverridepublicvoidonResume(){super.onResume();
camera=Camera.open();}
@OverridepublicvoidonPause(){if(inPreview){camera.stopPreview();}
camera.release();camera=null;inPreview=false;
super.onPause();}`
当活动被销毁时,您也可以这样做,如下所示,但我们不会这样做:
清单2-8。【地表摧毁】(
publicvoidsurfaceDestroyed(SurfaceHolderholder){camera.stopPreview();camera.release();camera=null;}
现在,我们的小演示应用应该可以编译并显示相机在屏幕上看到的漂亮预览。然而,我们还没有完全完成,因为我们还需要添加三个传感器。
这就把我们带到了应用的相机部分的末尾。这是到目前为止这个类的全部代码,包含了所有的内容。您应该将其更新为如下所示,以防遗漏某些内容:
清单2-9。完整代码清单
publicclassProAndroidAR2ActivityextendsActivity{SurfaceViewcameraPreview;SurfaceHolderpreviewHolder;Cameracamera;booleaninPreview;@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);
cameraPreview=(SurfaceView)findViewById(R.id.cameraPreview);previewHolder=cameraPreview.getHolder();previewHolder.addCallback(surfaceCallback);previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);}
@OverridepublicvoidonResume(){super.onResume();
super.onPause();}privateCamera.SizegetBestPreviewSize(intwidth,intheight,Camera.Parametersparameters){Camera.Sizeresult=null;
for(Camera.Sizesize:parameters.getSupportedPreviewSizes()){if(size.width<=width&&size.height<=height){if(result==null){result=size;}else{intresultArea=result.widthresult.height;intnewArea=size.widthsize.height;
if(newArea>resultArea){result=size;}}}}
return(result);}
SurfaceHolder.CallbacksurfaceCallback=newSurfaceHolder.Callback(){publicvoidsurfaceCreated(SurfaceHolderholder){try{camera.setPreviewDisplay(previewHolder);}catch(Throwablet){Log.e(TAG,"ExceptioninsetPreviewDisplay()",t);}}
publicvoidsurfaceChanged(SurfaceHolderholder,intformat,intwidth,intheight){Camera.Parametersparameters=camera.getParameters();Camera.Sizesize=getBestPreviewSize(width,height,parameters);
if(size!=null){parameters.setPreviewSize(size.width,size.height);camera.setParameters(parameters);camera.startPreview();inPreview=true;}}
publicvoidsurfaceDestroyed(SurfaceHolderholder){//notused}
};}`
方位传感器是磁场传感器和加速度传感器的组合。有了这两个传感器的数据和一点三角学,你就可以得到设备的pitch、roll和heading(azimuth)。如果你喜欢三角学,你会失望地知道Android为你做了所有的计算,你可以简单地从一个SensorEvent中取出数值。
注意:磁场罗盘在金属物体周围会变得有点疯狂。猜猜测试时什么大型金属物体可能会靠近您的设备?你的电脑!如果你的读数不是你所期望的,请记住这一点。
图2-1显示了方位传感器的轴。
图2-1。装置的斧子。
在我们开始从Android中获取这些价值并使用它们之前,让我们多了解一点它们到底是什么。
实际上有两种方法可以获得前面的数据。您可以直接查询方位传感器,或者分别获取加速度计和磁场传感器的读数,并计算方位。后者要慢几倍,但精度更高。在我们的应用中,我们将直接查询方向传感器。您可以从将下列变量添加到您的类开始:
`finalstaticStringTAG="PAAR";SensorManagersensorManager;
intorientationSensor;floatheadingAngle;floatpitchAngle;floatrollAngle;`
字符串TAG是一个常量,我们将在所有日志语句中使用它作为标签。sensorManager将用于获取我们所有的传感器数据,并管理我们的传感器。浮动headingAngle、pitchAngle和rollAngle将分别用于存储设备的航向、俯仰和横滚。
添加完上面给出的变量后,将下面几行添加到您的onCreate():
清单2-11。实现传感器管理器
sensorManager=(SensorManager)getSystemService(SENSOR_SERVICE);orientationSensor=Sensor.TYPE_ORIENTATION;sensorManager.registerListener(sensorEventListener,sensorManager.getDefaultSensor(orientationSensor),SensorManager.SENSOR_DELAY_NORMAL);
现在,sensorEventListener下面应该有一条红色下划线。这是因为到目前为止,我们实际上还没有创建侦听器;我们现在就去做:
清单2-12。sensorEventListener
`finalSensorEventListenersensorEventListener=newSensorEventListener(){publicvoidonSensorChanged(SensorEventsensorEvent){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));}}
publicvoidonAccuracyChanged(Sensorsenor,intaccuracy){//Notused}};`
我们创建并注册sensorEventListener作为新的SensorEventListener。然后,当传感器的值改变时,我们使用onSensorChanged()方法接收更新。因为onSensorChanged()接收所有传感器的更新,所以我们使用一个if语句来过滤掉除方位传感器之外的所有东西。然后,我们将来自传感器的值存储在变量中,并将它们打印到日志中。我们也可以在相机预览中叠加这些数据,但这超出了本章的范围。我们现在也有onAccuracyChanged()方法,我们现在不使用它。根据Eclipse的说法,它之所以存在,是因为您必须实现它。
现在,为了让我们的应用运行良好,并且不会耗尽用户的电池,我们将在onResume()和onPause()方法中注册和注销我们的传感器。将它们更新为以下内容:
清单2-13。onResume()和onPause()
`@OverridepublicvoidonResume(){super.onResume();sensorManager.registerListener(sensorEventListener,sensorManager.getDefaultSensor(orientationSensor),SensorManager.SENSOR_DELAY_NORMAL);camera=Camera.open();}
@OverridepublicvoidonPause(){if(inPreview){camera.stopPreview();}sensorManager.unregisterListener(sensorEventListener);camera.release();camera=null;inPreview=false;
这就完成了方向传感器的部分。我们现在来看看加速度计传感器。
加速度计测量沿三个方向轴的加速度:左右(横向(X))、前后(纵向(Y))和上下(垂直(Z))。这些值在值的浮点数组中传递。
图2-2显示了加速度计的轴线。
图2-2。加速度计轴
在我们的应用中,我们将接收加速度计值,并通过LogCat输出它们。在本书的后面,我们将使用加速度计来确定速度和其他东西。
让我们快速看一下加速度计的轴,以及它们到底测量什么。
现在开始编码工作吧。我们将使用与之前相同的加速度计SensorManager。我们只需要添加一些变量,获得加速度传感器,并在onSensorChanged()方法中添加另一个过滤if语句。让我们从变量开始:
清单2-14。加速度计变量
intaccelerometerSensor;floatxAxis;floatyAxis;floatzAxis;
accelerometerSensor将用于存储加速度计的常数,xAxis将存储传感器返回的X轴值,yAxis将存储传感器返回的Y轴值,zAxis将存储传感器返回的Z轴值。
清单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()
finalSensorEventListenersensorEventListener=newSensorEventListener(){publicvoidonSensorChanged(SensorEventsensorEvent){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));}
elseif(sensorEvent.sensor.getType()==Sensor.TYPE_ACCELEROMETER){xAxis=sensorEvent.values[0];yAxis=sensorEvent.values[1];zAxis=sensorEvent.values[2];
Log.d(TAG,"XAxis:"+String.valueOf(xAxis));Log.d(TAG,"YAxis:"+String.valueOf(yAxis));Log.d(TAG,"ZAxis:"+String.valueOf(zAxis));
}}`
同样,我们正在重复我们对方向传感器所做的,以监听加速度计传感器的变化。我们使用if语句来区分两个传感器,用新值更新适当的浮点数,并将新值打印到日志中。现在剩下的就是更新onResume()方法来再次注册加速度计:
清单2-17。修改onResume()
sensorManager.registerListener(sensorEventListener,sensorManager.getDefaultSensor(accelerometerSensor),SensorManager.SENSOR_DELAY_NORMAL);
camera=Camera.open();}`
这样,我们就到了两个传感器的终点。现在剩下的就是实现GPS来完成我们的应用。
全球定位系统(GPS)是一种定位系统,可以通过卫星给出极其精确的位置。这将是我们惊人的小演示应用的最后一部分。
首先,让我们简要了解一下GPS的历史及其工作原理。
GPS是一种基于空间的卫星导航系统。它由美国管理,任何拥有GPS接收器的人都可以使用,尽管它最初只打算用于军事用途。
最初,一个接收器可以与24颗卫星通信。多年来,该系统已经升级到31颗卫星,加上目前标记为备用的2颗旧卫星。在任何时候,从地面上至少可以看到九颗卫星,而其余的则看不到。
LocationManagerlocationManager;doublelatitude;doublelongitude;doublealtitude;
纬度是地球网格系统的一部分;它们是从北极到南极的假想圆圈。赤道是0线,是纬度中唯一一个大圆。所有的纬度都相互平行。每个纬度距离它的上一个和下一个纬度大约69英里,或111公里。确切的距离因地球的曲率而异。
图2-3显示了球体的概念。
图2-3。纬度的图示
经度也是地球网格系统的假想线。它们从北极运行到南极,在两极汇合。每个经度是一个大圆的一半。经度0°被称为本初子午线,穿过英国格林威治。两个经度之间的距离在赤道处最大,大约为69英里,或111公里,与两个纬度之间的距离大致相同。
图2-4显示了另一个球体上的概念。
图2-4。经度的图示
对纬度和经度有了新的理解后,我们可以继续从系统获取服务,并通过onCreate()方法请求位置更新:
清单2-19。在onCreate()中请求位置更新
locationManager=(LocationManager)getSystemService(LOCATION_SERVICE);locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,2000,2,locationListener);
现在,locationListener应该有红色下划线。那是因为我们还没有完全做到。让我们来解决这个问题:
清单2-20。位置监听器
`LocationListenerlocationListener=newLocationListener(){publicvoidonLocationChanged(Locationlocation){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));}
publicvoidonProviderDisabled(Stringarg0){//TODOAuto-generatedmethodstub
}
publicvoidonProviderEnabled(Stringarg0){//TODOAuto-generatedmethodstub
publicvoidonStatusChanged(Stringarg0,intarg1,Bundlearg2){//TODOAuto-generatedmethodstub
}};`
GPS是Android系统中电池最密集的部分之一,充满电的电池可能会在几个小时内耗尽。这就是为什么我们将在onPause()和onResume()方法中经历释放和获取GPS的整个过程:
清单2-21。onResume()和onPause()
`@OverridepublicvoidonResume(){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();}
@OverridepublicvoidonPause(){if(inPreview){camera.stopPreview();}locationManager.removeUpdates(locationListener);sensorManager.unregisterListener(sensorEventListener);camera.release();camera=null;inPreview=false;
这让我们结束了我们的演示应用。如果做得好,你应该在屏幕上看到相机预览,再加上一个快速移动的LogCat。现在,这里给出了在项目创建时从默认状态修改的所有文件,这样您就可以确保一切就绪。
清单2-22。ProAndroidAR2Activity.java全面上市
importandroid.app.Activity;importandroid.hardware.Camera;importandroid.hardware.Sensor;importandroid.hardware.SensorEvent;importandroid.hardware.SensorEventListener;importandroid.hardware.SensorManager;importandroid.location.Location;importandroid.location.LocationListener;importandroid.location.LocationManager;importandroid.os.Bundle;importandroid.util.Log;importandroid.view.SurfaceHolder;importandroid.view.SurfaceView;
publicclassProAndroidAR2ActivityextendsActivity{SurfaceViewcameraPreview;SurfaceHolderpreviewHolder;Cameracamera;booleaninPreview;
finalstaticStringTAG="PAAR";SensorManagersensorManager;
intorientationSensor;floatheadingAngle;floatpitchAngle;floatrollAngle;
LocationManagerlocationManager;doublelatitude;doublelongitude;doublealtitude;@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);
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);
LocationListenerlocationListener=newLocationListener(){publicvoidonLocationChanged(Locationlocation){latitude=location.getLatitude();longitude=location.getLongitude();altitude=location.getAltitude();
publicvoidonProviderDisabled(Stringarg0){//TODOAuto-generatedmethodstub}
}};
finalSensorEventListenersensorEventListener=newSensorEventListener(){publicvoidonSensorChanged(SensorEventsensorEvent){if(sensorEvent.sensor.getType()==Sensor.TYPE_ORIENTATION){headingAngle=sensorEvent.values[0];pitchAngle=sensorEvent.values[1];rollAngle=sensorEvent.values[2];
}}
publicvoidonAccuracyChanged(Sensorsenor,intaccuracy){//Notused}};
@OverridepublicvoidonResume(){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();}
super.onPause();}
privateCamera.SizegetBestPreviewSize(intwidth,intheight,Camera.Parametersparameters){Camera.Sizeresult=null;
清单2-23。Androidmanifest.XML的完整列表
清单2-24。main.XML的完整清单
写出应用后,使用顶部的RunAs按钮从Eclipse运行它。如果您在模拟器上运行它,您将一无所获,因为传感器没有被模拟。在一个设备上,你应该在屏幕上看到一个相机预览,再加上一个快速移动的LogCat,看起来有点像图2-5。
图2-5。应用运行时LogCat的截图。
如果你有一个清晰的天空视图,LogCat还将包括三条线,告诉你纬度,经度和海拔高度。
在本章中,您学习了如何使用摄像头,如何从加速度计和方位传感器读取值,以及如何使用GPS获取用户的位置。
你还学会了利用任何全功能增强现实应用的四个基本组件。你不会总是在你的应用中使用这四样东西。其实很少有app有这样的要求。
这一章应该会让你对AR有一个基本的了解,来自这个app的项目本质上是一个合适的ARapp的骨架。
下一章将讨论叠加,以及它们如何给用户一个真实的增强体验。
在第二章中,我们制作了一个基本的应用,显示设备摄像头的预览,通过GPS检索位置,获取加速度计读数,并检索方位传感器读数。在这一章中,我们将继续构建这个应用,并添加覆盖图。我们将添加正常的Android部件覆盖和实现标记识别。让我们从最简单的开始:小部件覆盖。
Android平台提供了一堆标准的小部件,比如TextViews、Buttons和Checkboxes。这些默认包含在Android操作系统中,可以由任何应用使用。它们可能是你可以在相机预览中叠加的最简单的东西。
首先,创建一个新的Android项目。本例中使用的插件名为ProAndroidAR3WidgetOverlay,构建于Android4之上,其minSdkVersion设置为7(Android2.1),包名为com.paar.ch3widgetoverlay(您可以根据自己的需求进行更改,但一定要更新这里给出的示例代码。)图3-1显示了项目设置屏幕。
图3-1。申请详情
复制完上一章的所有代码后,我们需要修改定义布局的XML文件,以允许小部件覆盖。之前整个布局是单个SurfaceView,显示相机预览。因此,我们目前不能在布局中添加其他小部件。因此,我们将修改我们的XML文件,使其包含一个RelativeLayout,并在这个RelativeLayout中包含SurfaceView和所有其他小部件。我们使用了一个RelativeLayout,因为它允许我们轻松地在SurfaceView上重叠窗口小部件。在本例中,我们将添加各种TextViews来显示传感器数据。因此,在我们开始布局编辑之前,我们需要向项目的strings.xml添加一些字符串资源:
清单3-1。字符串资源
这些字符串将为一些TextView提供标签。确切地说,是一半。另一半将用来自传感器的数据更新。在这之后,您应该更新您的main.xml文件,以便它有一个RelativeLayout。在我们进入实际代码之前,让我们快速看一下什么是RelativeLayout,以及它与其他布局相比如何。
【Android刚发布的时候,有第五种布局选项,叫做绝对布局。这种布局允许您使用精确的x和y坐标来指定元素的位置。这种布局现在已被弃用,因为它很难在不同的屏幕尺寸上使用。
框架布局是最简单的布局类型。它实际上是一个很大的空白空间,你可以在上面放置一个子对象,这个子对象将被固定在屏幕的左上角。在第一个对象之后添加的任何其他对象将直接绘制在它的上面。
表格布局将其子元素定位到行和列中。容器不显示它们的行、列或单元格的边框线。表格的列数与单元格最多的行数相同。表格可以将单元格留空,但单元格不能像HTML中那样跨列。TableRow对象是一个TableLayout的子视图(每个TableRow定义表格中的一行)。每行有零个或多个单元格,每个单元格都由任何类型的其他视图定义。所以一行的单元格可以由各种视图对象组成,比如ImageView或TextView。单元格也可以是一个ViewGroup对象(例如,您可以嵌套另一个TableLayout作为单元格)。
线性布局将所有子节点沿一个方向对齐—垂直或水平,这取决于您如何定义方向属性。所有的子元素都是一个接一个堆叠起来的,所以一个垂直的列表每一行只有一个子元素,不管它们有多宽;而一个水平列表只有一行高(最高的孩子的高度,加上填充)。ALinearLayout考虑子对象之间的边距和每个子对象的重心(右对齐、居中或左对齐)。
最后,相对布局允许子视图指定它们相对于父视图或彼此的位置(由ID指定)。因此,您可以通过右边框对齐两个元素,使一个元素位于另一个元素的下方,使其在屏幕上居中,使其向左居中,等等。元素是按照给定的顺序呈现的,所以如果第一个元素位于屏幕的中心,其他与该元素对齐的元素将相对于屏幕中心对齐。此外,由于这种排序,如果使用XML来指定这种布局,您将引用的元素(为了定位其他视图对象)必须在XML文件中列出,然后才能通过其引用ID从其他视图中引用它。
相对布局是唯一允许我们以应用需要的方式重叠视图的布局。由于它需要引用布局的其他部分来在屏幕上放置一个视图,你必须确保本书中的所有RelativeLayout都准确地复制到你的代码中;否则,你的整个布局看起来会非常混乱,视图会到处都是,除了你放置它们的地方。
在RelativeLayout里面是一个SurfaceView和18个TextView。小部件的顺序和id很重要,因为它是一个RelativeLayout。以下是布局文件:
清单3-2。相对布局
`
您可以通过查看id来了解每个TextView的用途。确保布局代码完全正确;否则,你的整个布局看起来就像是被推进了搅拌机(厨房里的那种,不是我能用这个软件做出很酷的图形的那种)。我们将只引用代码中id中有“Value”的TextView,因为其他的只是标签。这些TextView将用于显示我们的应用将接收的各种传感器值。
布局在项目中之后,我们可以开始从代码中引用所有那些TextView并用适当的数据更新它们。为了能够从代码中引用TextView,我们需要一些变量来存储这些引用。将以下九个变量添加到该类的顶部(名称不言自明):
TextViewxAxisValue;TextViewyAxisValue;TextViewzAxisValue;TextViewheadingValue;TextViewpitchValue;TextViewrollValue;TextViewaltitudeValue;TextViewlatitudeValue;TextViewlongitudeValue;
之后,将下面的代码添加到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
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));}
xAxisValue.setText(String.valueOf(xAxis));yAxisValue.setText(String.valueOf(yAxis));zAxisValue.setText(String.valueOf(zAxis));}}
现在传感器数据被写入日志和TextViews。因为传感器延迟被设置为SENSOR_DELAY_NORMAL,TextView将会以一种适中的速率更新。如果延迟被设置为SENSOR_DELAY_GAME,我们会让TextView的更新速度超过我们的视觉速度。那会对CPU造成很大的负担。即使是现在,在一些较慢的设备上,该应用可能会显得滞后。
注意:你可以通过将更新TextViews的代码转换成TimerTask或Handler来避免延迟。
既然数据来自方位和加速度传感器,我们应该为GPS做同样的事情。这或多或少是我们对SensorEventListener所做的重复,除了它是对LocationListener所做的:
清单3-6。修改了LocationListener
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));}
最后,您需要更改这个项目的AndroidManifest.xml:
清单3-7。修改后的AndroidManifest.xml
这些是在你的相机预览上覆盖标准Android部件的基础。确保小部件已经就位,并且您的所有id都已对齐。之后,在你的应用中使用小部件就和在任何其他应用中完全一样了。你将调用同样的方法,使用同样的函数,做同样的事情。这适用于Android框架中的所有小部件。
至此,我们结束了在你的相机预览上叠加标准Android小工具的工作。图3-2和3-3显示了应用完成后的样子。
图3-2。无GPS定位的app截图
图3-3。带GPS定位的应用截图
接下来,我们将看看如何在我们的应用中添加标记识别。
标记是AR应用用来知道在哪里放置叠层的视觉线索。您可以选择任何容易识别的图像(如白色背景上的黑色问号)。图像的一个副本保存在你的应用中,而另一个副本被打印出来并放在现实世界中的某个地方(或者如果你的手非常稳定,就画出来)。标记识别是人工智能领域正在进行的研究的一部分。
创建新项目。我这边的包名是com.paar.ch3marker,默认的活动名是Activity.java。
该应用将有四个它将识别的标记。对于每个标记,我们将提供一个.patt文件,AndAR可以用它来识别标记。这些文件以AndAR能够理解的方式描述了标记的外观。
如果您不喜欢提供的标记,或者感到无聊和喜欢冒险,您也可以创建和提供自己的标记。但是有一些限制:
标记可以是黑白的,也可以是彩色的。图3-4显示了一个标记的例子。
图3-4。样品安卓马克笔
先来编辑一下Activity.java,这是一个比较小的类。
清单3-8。改装的Activity.java
`publicclassActivityextendsAndARActivity{
privateARObjectsomeObject;privateARToolkitartoolkit;@OverridepublicvoidonCreate(BundlesavedInstanceState){
super.onCreate(savedInstanceState);CustomRendererrenderer=newCustomRenderer();setNonARRenderer(renderer);try{artoolkit=getArtoolkit();
someObject=newCustomObject1("test","marker_at16.patt",80.0,newdouble[]{0,0});artoolkit.registerARObject(someObject);
someObject=newCustomObject2("test","marker_peace16.patt",80.0,newdouble[]{0,0});artoolkit.registerARObject(someObject);
someObject=newCustomObject3("test","marker_rupee16.patt",80.0,newdouble[]{0,0});artoolkit.registerARObject(someObject);
someObject=newCustomObject4("test","marker_hand16.patt",80.0,newdouble[]{0,0});artoolkit.registerARObject(someObject);
}catch(AndARExceptionex){System.out.println("");}startPreview();}
publicvoiduncaughtException(Threadthread,Throwableex){Log.e("AndAREXCEPTION",ex.getMessage());finish();}}`
自定义对象基本上是4种不同颜色的3D盒子。它们根据标记的视图进行旋转等操作。图3-5显示了一个正在显示的立方体。
图3-5。四个自定义对象叠加之一
首先出场的是CustomObject1.java。
清单3-9。自定义对象1
`publicclassCustomObject1extendsARObject{
publicCustomObject1(Stringname,StringpatternName,doublemarkerWidth,double[]markerCenter){super(name,patternName,markerWidth,markerCenter);floatmat_ambientf[]={0f,1.0f,0f,1.0f};floatmat_flashf[]={0f,1.0f,0f,1.0f};floatmat_diffusef[]={0f,1.0f,0f,1.0f};floatmat_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);
}publicCustomObject1(Stringname,StringpatternName,doublemarkerWidth,double[]markerCenter,float[]customColor){super(name,patternName,markerWidth,markerCenter);floatmat_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);
privateSimpleBoxbox=newSimpleBox();privateFloatBuffermat_flash;privateFloatBuffermat_ambient;privateFloatBuffermat_flash_shiny;privateFloatBuffermat_diffuse;
@Overridepublicfinalvoiddraw(GL10gl){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);}@Overridepublicvoidinit(GL10gl){
我们开始为盒子设置各种灯光,并在构造函数中用它们创建FloatBuffer。然后我们直接从AndAR那里得到一个简单的盒子,这样我们就省去了制作它的麻烦。在draw()方法中,我们画出一切。在这种情况下,在draw()方法中完成的一切都将直接在标记上完成。
其他三个CustomObject类与CustomObject1完全相同,除了我们稍微改变了颜色。以下是你需要为CustomObject2做的改动。
清单3-10。自定义对象2
`publicCustomObject2(Stringname,StringpatternName,doublemarkerWidth,double[]markerCenter){super(name,patternName,markerWidth,markerCenter);floatmat_ambientf[]={1.0f,0f,0f,1.0f};floatmat_flashf[]={1.0f,0f,0f,1.0f};floatmat_diffusef[]={1.0f,0f,0f,1.0f};floatmat_flash_shinyf[]={50.0f};
//Samecodeeverywhereelse,exceptthedraw()method
gl.glColor4f(1.0f,0,0,1.0f);gl.glTranslatef(0.0f,0.0f,12.5f);
box.draw(gl);}`
以下是CustomObject3的变化。
清单3-11。自定义对象3
`publicCustomObject3(Stringname,StringpatternName,doublemarkerWidth,double[]markerCenter){super(name,patternName,markerWidth,markerCenter);floatmat_ambientf[]={0f,0f,1.0f,1.0f};floatmat_flashf[]={0f,0f,1.0f,1.0f};floatmat_diffusef[]={0f,0f,1.0f,1.0f};floatmat_flash_shinyf[]={50.0f};
gl.glColor4f(0f,0,1.0,1.0f);gl.glTranslatef(0.0f,0.0f,12.5f);
最后,CustomObject4的变化如下。
清单3-12。海关对象4
`publicCustomObject4(Stringname,StringpatternName,doublemarkerWidth,double[]markerCenter){super(name,patternName,markerWidth,markerCenter);floatmat_ambientf[]={1.0f,0f,1.0f,1.0f};floatmat_flashf[]={1.0f,0f,1.0f,1.0f};floatmat_diffusef[]={1.0f,0f,1.0f,1.0f};floatmat_flash_shinyf[]={50.0f};
gl.glColor4f(1.0f,0,1.0,1.0f);gl.glTranslatef(0.0f,0.0f,12.5f);
现在我们只有CustomRenderer.java要处理。这个类允许我们做任何非增强现实的事情以及设置OpenGL环境。
清单3-13。自定义渲染器
publicclassCustomRendererimplementsOpenGLRenderer{`privatefloat[]ambientlight1={.3f,.3f,.3f,1f};privatefloat[]diffuselight1={.7f,.7f,.7f,1f};privatefloat[]specularlight1={0.6f,0.6f,0.6f,1f};privatefloat[]lightposition1={20.0f,-40.0f,100.0f,1f};
privateFloatBufferlightPositionBuffer1=GraphicsUtil.makeFloatBuffer(lightposition1);privateFloatBufferspecularLightBuffer1=GraphicsUtil.makeFloatBuffer(specularlight1);privateFloatBufferdiffuseLightBuffer1=GraphicsUtil.makeFloatBuffer(diffuselight1);privateFloatBufferambientLightBuffer1=GraphicsUtil.makeFloatBuffer(ambientlight1);
publicfinalvoiddraw(GL10gl){}
publicfinalvoidsetupEnv(GL10gl){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);}
publicfinalvoidinitGL(GL10gl){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);}}`
最后,AndroidManifest.xml需要更新。
清单3-14。更新AndroidManifest.xml
这让我们结束了这个应用的编码。如果您还没有下载本章的源代码,请下载并使用。patt文件,并将它们放在您的项目/资产目录中。除了源代码,您还会找到一个名为“标记”的文件夹,其中包含本应用和本书后续部分中使用的标记。你可以打印出来供自己使用。
在这一章中,我们学习了如何在我们的应用中覆盖标准的Android小部件,以及如何使用标记来使我们的增强现实应用更具交互性。关于这一章还讨论了AndAR,一个适用于Android的开源AR工具包,它允许我们轻松快速地实现许多AR功能。
下一章讨论人工地平线,这是任何军事或导航应用的核心AR功能。
人工地平线被牛津英语词典定义为“一种回转仪或流体表面,通常是水银,用于在自然地平线被遮挡时为飞机飞行员提供水平参考平面以进行导航测量。”早在增强现实(AR)出现之前,人工地平线就已经用于导航目的,导航仍然是它们的主要用途。当平视显示器在飞机上,特别是军用飞机上大量使用时,它们变得突出起来。
人工地平线基本上是一条水平参考线,供导航员在自然地平线被遮挡时使用。对于我们所有痴迷于在应用中使用AR的人来说,这是一个需要熟悉的重要功能。在制作导航应用甚至游戏时,它会非常有用。
可能很难理解实际上并不存在的视界的概念,但必须用来进行各种计算,这些计算可能会以多种方式影响用户。为了解决这个问题,我们将制作一个小的示例应用,它不实现AR,但向您显示什么是人工地平线以及它是如何实现的。之后,我们将制作一个AR应用来使用人工地平线。
在这个应用中,我们将有一个内置人工地平线指示器的指南针。我将只为人工地平线代码提供一个解释,因为它的其余部分不属于本书的主题。
让我们先把这些小的XML文件去掉。我们将需要一个/res/layout/main.xml、一个/res/values/strings.xml和一个/res/values/colors.xml。
让我们从main.xml文件开始:
清单4-1。main.xml
这里没什么特别的。我们只需将Activity的视图设置为自定义视图,我们将在几分钟内着手制作。
现在让我们来看看strings.xml:
清单4-2。strings.xml
让我们继续讨论colours.xml:
清单4-3。colours.xml
#FF444444#FF323232#FF414141#FFFFFFFF
#FFA52A2A#FFFFC125#FF5F9EA0#FF00008B`
所有颜色在ARGB或AARRGGBB中指定。它们用于为我们的小演示应用增加一点视觉吸引力。“到”和“从”的颜色略有不同,因此我们可以在最终的演示中使用渐变。天空的颜色是蓝色,地面的颜色是橙色。
现在我们将创建我们在main.xml中提到的自定义视图。
在主包中创建一个名为HorizonView.java的Java文件(我这边是com.paar.ch4nonardemo)。向其中添加以下全局变量:
清单4-4。HorizonView.java全局变量
`publicclassHorizonViewextendsView{privateenumCompassDirection{N,NNE,NE,ENE,E,ESE,SE,SSE,S,SSW,SW,WSW,W,WNW,NW,NNW}
int[]borderGradientColors;float[]borderGradientPositions;
int[]glassGradientColors;float[]glassGradientPositions;
intskyHorizonColorFrom;intskyHorizonColorTo;intgroundHorizonColorFrom;intgroundHorizonColorTo;privatePaintmarkerPaint;privatePainttextPaint;privatePaintcirclePaint;privateinttextHeight;
privatefloatbearing;floatpitch=0;floatroll=0;`
变量的名字是对它们任务的合理的描述。CompassDirections提供我们将用来创建16点罗盘的弦。名称中带有渐变、颜色、颜料的用于绘制View,textHeight也是如此。
现在将以下方法添加到该类中:
清单4-5。方位、俯仰和滚转方式
`publicvoidsetBearing(float_bearing){bearing=_bearing;}publicfloatgetBearing(){returnbearing;}
publicfloatgetPitch(){returnpitch;}publicvoidsetPitch(floatpitch){this.pitch=pitch;}
publicfloatgetRoll(){returnroll;}publicvoidsetRoll(floatroll){this.roll=roll;}`
这些方法让我们获得并设置方位角、俯仰角和滚动角,这在以后被规范化并用于绘制我们的视图。
接下来,将以下三个构造函数添加到该类中:
清单4-6。【地平线】建造者
`publicHorizonView(Contextcontext){super(context);initCompassView();}
publicHorizonView(Contextcontext,AttributeSetattrs){super(context,attrs);initCompassView();}
publicHorizonView(Contextcontext,AttributeSetats,intdefaultStyle){super(context,ats,defaultStyle);initCompassView();}`
所有三个构造函数最终都调用了initCompassView(),它完成了这个类中的主要工作。
说到initCompassView(),下面是它的代码:
清单4-7。initCompassView()
`protectedvoidinitCompassView(){setFocusable(true);Resourcesr=this.getResources();
circlePaint=newPaint(Paint.ANTI_ALIAS_FLAG);circlePaint.setColor(R.color.background_color);circlePaint.setStrokeWidth(1);circlePaint.setStyle(Paint.Style.STROKE);
textPaint=newPaint(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=newPaint(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=newint[4];borderGradientPositions=newfloat[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=newint[5];glassGradientPositions=newfloat[5];
intglassColor=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()
`@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){
intmeasuredWidth=measure(widthMeasureSpec);intmeasuredHeight=measure(heightMeasureSpec);
intd=Math.min(measuredWidth,measuredHeight);
setMeasuredDimension(d,d);}
privateintmeasure(intmeasureSpec){intresult=0;
intspecMode=MeasureSpec.getMode(measureSpec);intspecSize=MeasureSpec.getSize(measureSpec);
if(specMode==MeasureSpec.UNSPECIFIED){result=200;}else{result=specSize;}returnresult;}`
这两种方法允许我们测量屏幕,并让我们决定我们希望我们的指南针有多大。
现在,最后将onDraw()方法添加到类中:
清单4-9。【onDraw()】
`@OverrideprotectedvoidonDraw(Canvascanvas){floatringWidth=textHeight+4;intheight=getMeasuredHeight();intwidth=getMeasuredWidth();
intpx=width/2;intpy=height/2;Pointcenter=newPoint(px,py);
intradius=Math.min(px,py)-2;RectFboundingBox=newRectF(center.x-radius,center.y-radius,center.x+radius,center.y+radius);
RectFinnerBoundingBox=newRectF(center.x-radius+ringWidth,center.y-radius+ringWidth,center.x+radius-ringWidth,center.y+radius-ringWidth);
floatinnerRadius=innerBoundingBox.height()/2;RadialGradientborderGradient=newRadialGradient(px,py,radius,borderGradientColors,borderGradientPositions,TileMode.CLAMP);
Paintpgb=newPaint();pgb.setShader(borderGradient);
PathouterRingPath=newPath();outerRingPath.addOval(boundingBox,Direction.CW);
canvas.drawPath(outerRingPath,pgb);LinearGradientskyShader=newLinearGradient(center.x,innerBoundingBox.top,center.x,innerBoundingBox.bottom,skyHorizonColorFrom,skyHorizonColorTo,TileMode.CLAMP);
PaintskyPaint=newPaint();skyPaint.setShader(skyShader);
LinearGradientgroundShader=newLinearGradient(center.x,innerBoundingBox.top,center.x,innerBoundingBox.bottom,groundHorizonColorFrom,groundHorizonColorTo,TileMode.CLAMP);
PaintgroundPaint=newPaint();groundPaint.setShader(groundShader);floattiltDegree=pitch;while(tiltDegree>90||tiltDegree<-90){if(tiltDegree>90)tiltDegree=-90+(tiltDegree-90);if(tiltDegree<-90)tiltDegree=90-(tiltDegree+90);}
floatrollDegree=roll;while(rollDegree>180||rollDegree<-180){if(rollDegree>180)rollDegree=-180+(rollDegree-180);if(rollDegree<-180)rollDegree=180-(rollDegree+180);}PathskyPath=newPath();skyPath.addArc(innerBoundingBox,-tiltDegree,(180+(2*tiltDegree)));canvas.rotate(-rollDegree,px,py);canvas.drawOval(innerBoundingBox,groundPaint);canvas.drawPath(skyPath,skyPaint);canvas.drawPath(skyPath,markerPaint);intmarkWidth=radius/3;intstartX=center.x-markWidth;intendX=center.x+markWidth;
doubleh=innerRadius*Math.cos(Math.toRadians(90-tiltDegree));doublejustTiltY=center.y-h;
floatpxPerDegree=(innerBoundingBox.height()/2)/45f;for(inti=90;i>=-90;i-=10){doubleypos=justTiltY+i*pxPerDegree;
if((ypos<(innerBoundingBox.top+textHeight))||(ypos>innerBoundingBox.bottom-textHeight))continue;
canvas.drawLine(startX,(float)ypos,endX,(float)ypos,markerPaint);intdisplayPos=(int)(tiltDegree-i);StringdisplayString=String.valueOf(displayPos);floatstringSizeWidth=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);
PathrollArrow=newPath();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);StringrollText=String.valueOf(rollDegree);doublerollTextWidth=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(inti=-180;i<180;i+=10){if(i%30==0){StringrollString=String.valueOf(i*-1);floatrollStringWidth=textPaint.measureText(rollString);PointFrollStringCenter=newPointF(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);
doubleincrement=22.5;
for(doublei=0;i<360;i+=increment){CompassDirectioncd=CompassDirection.values()[(int)(i/22.5)];StringheadString=cd.toString();
floatheadStringWidth=textPaint.measureText(headString);PointFheadStringCenter=newPointF(center.x-headStringWidth/2,boundingBox.top+1+textHeight);
if(i%increment==0)canvas.drawText(headString,headStringCenter.x,headStringCenter.y,textPaint);elsecanvas.drawLine(center.x,(int)boundingBox.top,center.x,(int)boundingBox.top+3,markerPaint);
canvas.rotate((int)increment,center.x,center.y);}canvas.restore();RadialGradientglassShader=newRadialGradient(px,py,(int)innerRadius,glassGradientColors,glassGradientPositions,TileMode.CLAMP);PaintglassPaint=newPaint();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
`publicclassAHActivityextendsActivity{float[]aValues=newfloat[3];float[]mValues=newfloat[3];HorizonViewhorizonView;SensorManagersensorManager;
@OverridepublicvoidonCreate(Bundleicicle){super.onCreate(icicle);setContentView(R.layout.main);
horizonView=(HorizonView)this.findViewById(R.id.horizonView);sensorManager=(SensorManager)getSystemService(Context.SENSOR_SERVICE);updateOrientation(newfloat[]{0,0,0});}
privatevoidupdateOrientation(float[]values){if(horizonView!=null){horizonView.setBearing(values[0]);horizonView.setPitch(values[1]);horizonView.setRoll(-values[2]);horizonView.invalidate();}}
privatefloat[]calculateOrientation(){float[]values=newfloat[3];float[]R=newfloat[9];float[]outR=newfloat[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]);
returnvalues;}
privatefinalSensorEventListenersensorEventListener=newSensorEventListener(){publicvoidonSensorChanged(SensorEventevent){if(event.sensor.getType()==Sensor.TYPE_ACCELEROMETER)aValues=event.values;if(event.sensor.getType()==Sensor.TYPE_MAGNETIC_FIELD)mValues=event.values;
updateOrientation(calculateOrientation());}
publicvoidonAccuracyChanged(Sensorsensor,intaccuracy){}};
@OverrideprotectedvoidonResume(){super.onResume();
Sensoraccelerometer=sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);SensormagField=sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);sensorManager.registerListener(sensorEventListener,accelerometer,SensorManager.SENSOR_DELAY_FASTEST);sensorManager.registerListener(sensorEventListener,magField,SensorManager.SENSOR_DELAY_FASTEST);}
@OverrideprotectedvoidonStop(){sensorManager.unregisterListener(sensorEventListener);super.onStop();}}`
这是实际工作发生的地方。在onCreate()方法中,我们将视图设置为main.xml,获取对horizonView的引用,注册一个SensorEventListener,并将方向更新为理想情况。updateOrientation()方法负责向我们的视图传递新的值,以便它可以适当地改变。calculateOrientation()使用SDK提供的一些方法,根据传感器提供的原始值精确计算方向。Android提供的这些方法为我们处理了很多复杂的数学问题。你应该很容易理解SensorEventListener、onResume()和onStop()。他们做着与前几章相同的工作。
最后,您应该将您的AndroidManifest更新为以下内容:
清单4-11。AndroidManifest.xml
如果你现在运行这个应用,你会对人工地平线有一个很好的了解。图4-1和4-2让你对完成的应用有个概念。
图4-1。设备直立时的app
图4-2。设备倒置时的app
在浏览并运行了前面的例子之后,您现在应该很好地理解了人工视界的概念。我们现在将设计一款应用,它具有以下功能:
在我们开始编码之前,有一些事情需要记住。由于用户几乎不可能保持设备完全静止,因此倾斜度将不断变化,这将导致5分钟内的高度也发生变化。为了解决这个问题,我们将添加一个按钮,允许用户随时更新高度。
注意:在一些设备上,这款应用可以顺时针和逆时针方向移动人工地平线,而不是像非AR演示中那样上下移动。所有值都是正确的,除了显示器有问题,可以通过在纵向模式下运行应用来修复。
首先,创建一个新项目。作为例子使用的包名为com.paar.ch4ardemo,目标是Android2.1。和往常一样,你可以把名字改成你喜欢的任何名字;只需确保更新示例代码中的所有引用。图4-3中的截图显示了项目详情。
图4-3。申请详情
创建新项目后,将非增强现实演示中的所有内容复制到这个项目中。我们将建立在以前的项目。确保在需要的地方更新文件中的包名。
首先,我们需要更新应用的XML。我们的应用目前只有四个XML文件:AndroidManifest.xml、main.xml、colours.xml和strings.xml。我们将只编辑从前面的例子中复制过来的,而不是从头开始构建新的。更新的和新的行以粗体显示。
清单4-12。更新AndroidManifest.xml
接下来,我们来看看strings.xml。我们将添加两个新的字符串,作为Button和TextView的标签。我们没有为另一个TextView的添加字符串,因为当用户点击按钮时,它将在运行时更新。在您的strings.xml文件中的任意位置添加以下两个字符串。
清单4-13。更新strings.xml
第二个字符串中的那个小\n告诉Android在新的一行上打印字符串的剩余部分。我们这样做是因为在较小的屏幕设备上,字符串可能会与按钮重叠。
现在来更新一下colours.xml。这一次,我们只需要两种颜色,其中只有一种是可见的颜色。在前面的例子中,我们为地面、天空等设置了不同的颜色。在这里这样做将导致仪表的表盘覆盖我们的相机预览。然而,使用ARGB色码,我们可以使除了文本以外的一切都透明。用下面的代码完全替换您的colours.xml文件的内容。
清单4-14。更新colours.xml
现在我们来看最后一个XML文件——也是变化最大的一个文件:main.xml。以前,main.xml只有一个LinearLayout,里面还有一个HorizonView。然而,考虑到我们的AR重叠,我们将用一个RelativeLayout替换LinearLayout,并在HorizonView之外添加两个TextView和一个Button。将main.xml更新为以下代码。
清单4-15。更新main.xml
在这种情况下,只有五行没有被修改。像往常一样,作为一个RelativeLayout,在id或位置上的任何错误都是致命的。
这负责我们应用的XML部分。现在我们必须转移到Java文件上。
Java文件比XML文件有更多的变化,有些变化一开始可能没有意义。我们将接受每个变更,一次一个代码块。
先从HorizonView.java说起吧。我们正在修改我们的代码,使表盘透明,并在风景模式下工作。先从修改initCompassView()开始。我们所做的唯一改变是用更新的颜色替换旧的颜色。已修改的行以粗体显示。
清单4-16。更新了initCompassView()
circlePaint=newPaint(Paint.ANTI_ALIAS_FLAG);circlePaint.setColor(R.color.transparent_color);circlePaint.setStrokeWidth(1);circlePaint.setStyle(Paint.Style.STROKE);
markerPaint=newPaint(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=newint[4];borderGradientPositions=newfloat[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;
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()
//CutHerePathskyPath=newPath();***skyPath.addArc(innerBoundingBox,-rollDegree,(180+(2*rollDegree)));******canvas.rotate(-tiltDegree,px,py);***canvas.drawOval(innerBoundingBox,groundPaint);canvas.drawPath(skyPath,skyPaint);canvas.drawPath(skyPath,markerPaint);intmarkWidth=radius/3;intstartX=center.x-markWidth;intendX=center.x+markWidth;`Log.d("PAARV","Roll"+String.valueOf(rollDegree));Log.d("PAARV","Pitch"+String.valueOf(tiltDegree));
doubleh=innerRadius*Math.cos(Math.toRadians(90-tiltDegree));doublejustTiltX=center.x-h;
floatpxPerDegree=(innerBoundingBox.height()/2)/45f;for(inti=90;i>=-90;i-=10){doubleypos=justTiltX+ipxPerDegree;*
canvas.drawLine(startX,(float)ypos,endX,(float)ypos,markerPaint);intdisplayPos=(int)(tiltDegree-i);StringdisplayString=String.valueOf(displayPos);floatstringSizeWidth=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);//CutHere`
这些变化使我们的应用看起来漂亮而透明。
现在,我们必须进入最后一个文件AHActivity.java。在这个文件中,我们将添加GPS代码、TextView和Button参考,稍微修改我们的传感器代码,最后放入一个小算法来计算我们5分钟后的高度。我们将使用三角学来寻找高度的变化,所以如果你的有点生疏,你可能想快速温习一下。
首先,将以下变量添加到类的顶部。
`LocationManagerlocationManager;
ButtonupdateAltitudeButton;TextViewaltitudeValue;
doublecurrentAltitude;doublepitch;doublenewAltitude;doublechangeInAltitude;doublethetaSin;`
将会是我们的区域经理。updateAltitudeButton和altitudeValue将保存对它们的XML对应物的引用,这样我们可以监听点击并更新它们。currentAltitude、newAltitude和changeInAltitude都将用于在我们的算法运行期间存储值。pitch变量将存储螺距,而thetaSin将存储螺距角的正弦值。
我们现在将更新我们的onCreate()方法以从Android获取位置服务,设置位置监听器,并为按钮设置OnClickListener。将其更新为以下代码。
清单4-19。更新onCreate()
`@OverridepublicvoidonCreate(Bundleicicle){super.onCreate(icicle);setContentView(R.layout.main);
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(newOnClickListener(){
publicvoidonClick(Viewarg0){updateAltitude();}});
horizonView=(HorizonView)this.findViewById(R.id.horizonView);sensorManager=(SensorManager)getSystemService(Context.SENSOR_SERVICE);updateOrientation(newfloat[]{0,0,0});}`
现在,Eclipse应该会告诉您updateAltitude()方法和locationListener不存在。我们将通过创建它们来解决这个问题。将下面的LocationListener添加到类的任何部分,方法之外。如果您想知道为什么我们有三个未使用的方法,那是因为一个LocationListener必须实现所有四个方法,即使它们没有被使用。移除它们会在编译时引发错误。
清单4-20。位置监听器
`LocationListenerlocationListener=newLocationListener(){publicvoidonLocationChanged(Locationlocation){currentAltitude=location.getAltitude();}
publicvoidonProviderDisabled(Stringarg0){//NotUsed}
publicvoidonProviderEnabled(Stringarg0){//NotUsed}
publicvoidonStatusChanged(Stringarg0,intarg1,Bundlearg2){//NotUsed}};`
在我们继续讨论updateAltitude()方法之前,我们将快速地在calculateOrientation()方法中添加一行,这样pitch变量就不会为空。在return语句之前添加以下内容。
清单4-21。确保calculateOrientation()中的俯仰变量不为空
pitch=values[1];
图4-4。算法的图形表示
以下是updateAltitude()的代码:
清单4-22。计算并显示高度
`publicvoidupdateAltitude(){inttime=300;floatspeed=4.5f;
doubledistanceMoved=(speedtime)0.3048;if(pitch!=0&¤tAltitude!=0){thetaSin=Math.sin(pitch);changeInAltitude=thetaSin*distanceMoved;newAltitude=currentAltitude+changeInAltitude;altitudeValue.setText(String.valueOf(newAltitude));}else{altitudeValue.setText("TryAgain");}}`
至此,我们已经完成了示例应用的AR版本。
看看图4-5和图4-6中的截图,看看这个应用是如何工作的。
图4-5。应用运行,向用户显示重试消息
图4-6。用户这次也看到了高度
本章探讨了人工视界的概念以及如何利用它们创建应用。我们设计了一个算法来发现海拔高度的变化,并在一个示例应用中实现了它。这里给出的应用只是你可以用人工地平线做什么的一个例子。它们广泛应用于军事,尤其是空军;而在城市中,自然地平线因高度而扭曲或被建筑物遮挡。
大多数AR应用在基本布局文件中使用相对布局。然后,RelativeLayout将所有小部件、表面视图和定制视图作为其子视图。这是首选的布局,因为它很容易让我们将UI元素一个接一个地叠加起来。
使用RelativeLayout时面临的一个最常见的问题是,布局最终看起来不像预期的那样。元素最终遍布整个位置,而不是按照你放置它们的顺序。最常见的原因是缺少ID或者没有定义某些布局规则。例如,如果你在文本视图的定义中漏掉了一个android:layout_*,文本视图没有设置布局选项,因为它不知道屏幕上的位置,所以布局最终看起来很混乱。此外,在对齐中使用上述TextView的任何其他小部件也将最终出现在屏幕上的随机位置。
清单5-1。一个例子TextView
在XML中定义了它之后,我们从Java代码中引用它,如下所示:
清单5-2。引用文本视图
TextViewtextView=(TextView)findViewById(R.id.textView1);
编译时,我们有时会在前面的代码中得到一个ClassCastException。在我们的XML文件发生大的变化后,通常会收到这个错误。当我们引用TextView时,我们使用View类中的方法findViewById()来获取与作为参数传递的ID相对应的视图。然后我们将由findViewById()返回的视图转换成一个文本视图。应用的所有R.*.*部分都存储在编译应用时生成的R.java文件中。有时这个文件没有正确更新,并且findViewById()返回一个不是我们正在寻找的视图(例如,一个按钮而不是一个文本视图)。然后,当我们试图将不正确的视图转换为TextView时,我们会得到一个ClassCastException,因为您不能将这两个视图相互转换。
这个错误的另一个原因是你实际上引用了一个不正确的视图,比如一个按钮而不是一个文本视图。要解决这个问题,您需要仔细检查您的Java代码,并确保您的id是正确的。
相机是任何AR应用不可或缺的一部分。如果你仔细想想,它增加了大部分的真实性。相机也是一个稍微不稳定的实现在一些设备上工作良好,但在其他设备上完全失败的部分。我们将查看最常见的Java错误,并在AndroidManifest部分处理单个清单错误。
在Android上使用相机时,最常见的错误可能是无法连接到相机服务。当您试图访问正在被应用(您自己的或其他)使用或已被一个或多个设备管理员完全禁用的摄像机时,会出现此错误。设备管理员是可以在运行Froyo和更高版本的设备中更改最小密码长度和相机使用等内容的应用。您可以通过使用android.app.admin中的DeviceManagerPolicy.getCameraDisabled()来检查管理员是否禁用了摄像头。您可以在检查时传递管理员的名称,或者传递null来检查所有管理员。
如果另一个应用正在使用相机,您将无能为力,但是您可以通过正确释放相机对象来确保您的应用不是导致问题的那个。这个的主代码一般在onPause()和surfaceDestroyed()里。您可以在应用中使用其中一种或两种。整本书,我们都在onPause()发行。两者的代码如下所示:
清单5-3。释放相机
@OverridepublicvoidsurfaceDestroyed(SurfaceHolderholder){`if(mCam!=null){mCam.stopPreview();mCam.setPreviewCallback(null);mCam.release();mCam=null;}}
@OverridepublicvoidonPause(){super.onPause();if(mCam!=null){mCam.stopPreview();mCam.setPreviewCallback(null);mCam.release();mCam=null;}}`
在前面的代码中,mCam是相机对象。这两种方法中都可能有额外的代码,这取决于您的应用的用途。
最常见的错误之一就是setParameters()的失败。出现这种情况有几个原因,但在大多数情况下,这是因为为预览提供了不正确的宽度或高度。
解决这个问题非常简单。你需要确保你传给Android的预览尺寸是受支持的。为了实现这一点,我们在本书的所有示例应用中使用了一个getBestPreviewSize()方法。该方法如清单5-4所示:
清单5-4。计算最佳预览尺寸
`privateCamera.SizegetBestPreviewSize(intwidth,intheight,Camera.Parametersparameters){Camera.Sizeresult=null;
for(Camera.Sizesize:parameters.getSupportedPreviewSizes()){if(size.width<=width&&size.height<=height){if(result==null){result=size;}else{intresultArea=result.widthresult.height;intnewArea=size.widthsize.height;if(newArea>resultArea){result=size;}}}}return(result);}`
要使用它,请执行以下操作:
清单5-5。调用最佳预览尺寸的计算器
surfaceChanged()是我们app的SurfaceHolder.Callback部分的一部分。
相机的另一个常见问题是在调用setPreviewDisplay()时出现异常。该方法用于告诉相机哪个表面视图用于实时预览,或者传递null来移除预览表面。如果向该方法传递了不合适的图面,该方法将引发IOException。
修复方法是确保传递的SurfaceView适用于相机。可以按如下方式创建合适的表面视图:
清单5-6。创建适合相机的表面视图
SurfaceViewpreviewHolder;previewHolder=cameraPreview.getHolder();previewHolder.addCallback(surfaceCallback);previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
我们将SurfaceView的类型改为SURFACE_TYPE_PUSH_BUFFERS,因为它告诉Android它将从其他地方接收位图。因为surfaceview的类型改变后可能不会立即准备好,所以您应该在surfaceCallback中完成其余的初始化工作。
在使用应用时,您很可能会遇到一些安全异常。如果您看一下LogCat,您会看到类似这样的内容:
04-0922:44:36.957:E/AndroidRuntime(13347):java.lang.RuntimeException:UnabletostartactivityComponentInfo{com.paar.ch2/com.paar.ch2.ProAndroidAR2Activity}:java.lang.SecurityException:ProvidergpsrequiresACCESS_FINE_LOCATIONpermission
表5-1列出了AR应用中的常用权限以及它们允许你做的事情。
在某些设备上,缺少相机权限也可能导致无法连接到相机服务错误。
清单5-7。将谷歌地图库添加到您的应用清单中
要包含这个库,您必须以GoogleAPIs为目标。在我们所有带有地图的示例应用中,我们的目标是Android2.1的GoogleAPIs。
虽然严格来说,丢失
android.hardware.cameraandroid.hardware.camera.autofocus
地图是许多AR应用的重要组成部分。但是,在使用它们时,会出现一些非常常见的错误。我们来看看他们两个。
GoogleMapsAPI用于提供本书中的地图,它要求每个应用获得一个调试证书的API密钥(Eclipse在调试时用它来签署应用)和另一个发布证书的API密钥(在发布前用它来签署您的.apk)。当从调试切换到生产时,开发人员通常会忘记更改密钥,或者根本忘记添加密钥。在这些情况下,您的地图工作正常,只是没有加载地图切片,并且您会得到一个带有网格的白色背景和一些叠加层(如果有)。
将这两个键作为注释保存在XML文件中是一个很好的实践,这样您就不必重复地在线生成它们。列表5-8显示了一个例子:
清单5-8。带有两个键的示例地图视图
`
要在您的应用中使用GoogleMapsAPI,显示地图的活动必须扩展com.google.android.maps.MapActivity而不是通常的活动。如果不这样做,您将会得到类似如下的错误:
04-0314:40:33.670:E/AndroidRuntime(414):Causedby:java.lang.IllegalArgumentException:MapViewscanonlybecreatedinsideinstancesofMapActivity.
publicclassexampleextendsActivity{
publicclassexampleextendsMapActivity{
本节讨论应用的调试。它将解释为了解决应用中的问题,您必须做些什么。
图5-1。一个logcat输出的例子
异常和错误将在LogCat中显示为红色块,长度大约为10-25行,这取决于具体的问题。大约在中间点,会有一行写着“起因于:……”。在这一行和这之后,你会在你的应用文件中找到导致错误的确切行号。
只有一种方法可以解决这样的问题:使用模拟器中的摄像头运行应用。模拟器不支持摄像机或任何其他传感器,除了通过模拟位置的GPS。当您尝试在模拟器中创建相机预览时,您会看到一个由黑白方块组成的网格,像棋盘一样排列。但是,所有覆盖都应该按预期显示。
有一些错误实际上不属于前面的任何类别。我们将在这个杂项部分讨论它们。
在测试或使用你的应用时,有时你的代码是完美的,但你仍然无法在你的应用中获得GPS定位。这可能是由以下任何原因造成的:
如今,许多增强现实应用都使用大多数Android设备中存在的指南针。指南针有助于导航应用,为观星设计的应用等等。
指南针经常会给出不正确的读数。这可能是由于以下原因之一:
如果您的指南针在尝试了之前给出的解决方案后仍未给出正确的读数,您可能应该将其送到服务中心进行检查,因为您的硬件可能有故障。
在下一章,我们将创建我们的第一个示例应用。
本章概述了如何制作一个非常简单的现实世界增强现实(AR)应用。本章结束时,你将拥有一个功能齐全的示例应用。
该应用将具有以下基本功能:
这个应用可以作为一个独立的应用,也可以扩展为一个增强现实导航系统,我们将在下一章中介绍。
首先,创建一个新的Android项目。该项目应针对谷歌API(API级别7,因为我们的目标是2.1及以上),以便我们可以使用Android的地图功能。本章通篇使用的项目有包名com.paar.ch06,项目名ProAndroidAR6:使用AR的简单App。您可以使用您想要的任何其他包和项目名称,只要您记得更改示例代码中的任何引用以匹配您的更改。
创建项目后,通过右键单击eclipse左侧栏中的包名并从New菜单中选择class,向您的项目添加另一个类(参见图6-1):
图6-1。菜单创建一个新的类。
清单6-1。更新AndroidManifest.xml
我们现在需要添加一些字符串,这些字符串将在应用的覆盖图和帮助对话框中使用。将您的strings.xml修改为以下内容:
清单6-2。更新strings.xml
您将创建两个菜单资源:一个用于摄像机预览Activity,另一个用于MapActivity。为此,在项目的/res目录中创建一个名为menu的新的子文件夹。在那个目录中创建两个XML文件,分别名为main_menu和map_toggle。在main_menu中,添加以下内容:
清单6-3。main_menu.xml
这基本上是主Activity中的帮助选项。现在在map_toggle中,我们将有三个选项,因此添加以下内容:
清单6-4。map_toggle.xml
第一个选项允许用户设置街道视图显示的地图种类,就像你在路线图上看到的一样。第二种选择允许他们在地图上使用卫星图像。第三种选择是在那个地方的卫星图像上叠加一张路线图。当然,这两个文件都只定义了用户界面的一部分,实际的工作将在Java文件中完成。
相机预览Activity布局文件是普通的main.xml,对其标准内容有一些改变:
清单6-5。摄像机预览布局文件
`android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignLeft="@+id/latitudeLabel"android:layout_below="@+id/latitudeLabel"android:text="@string/longitude"/> 同样,你需要确保所有的id都是有序的,并且你没有在任何地方打错字,因为这将影响整个布局。与第三章第一部分的布局唯一的主要区别是增加了一个帮助按钮,它将启动帮助对话框。“帮助”菜单选项会做同样的事情,但是最好有一个更容易看到的选项。 现在在/res/layout目录中创建另一个名为help.xml的XML文件。这将包含帮助对话框的布局设计,它有一个可滚动的TextView来显示实际的帮助文本和一个关闭对话框的按钮。将以下内容添加到help.xml文件中: 清单6-6。帮助对话框布局文件 如你所见,这是一个相对简单的RelativeLayout用于对话框布局。在文件的底部有一个ScrollView和一个TextView来保存帮助对话框的内容,还有一个Button用来关闭对话框。 现在我们需要创建最终的布局文件:地图布局。在您的/res/layout文件夹中创建一个map.xml,并将以下内容添加到其中: 清单6-7。地图布局文件 如果您的项目没有设置为基于GoogleAPIs目标构建,您将会得到一个错误。这里另一件重要的事情是API键。这是由谷歌以证书的形式分配给你的。它是从您的证书的MD5散列中生成的,您必须以在线形式提交该散列。Android使用数字证书来验证应用的安装文件。如果签名证书在已安装版本和新版本之间不匹配,Android将抛出一个安全异常,不允许您更新安装。映射API密钥对于每个证书都是唯一的。因此,如果您计划发布您的应用,您必须生成两个API密匙:一个用于您的调试证书(Eclipse在开发和测试过程中用它来签署您的应用),另一个用于您的发布证书(在将您的应用上传到在线市场(如AndroidMarket)之前用它来签署您的应用)。在不同的操作系统上,获取任何密钥的MD5的步骤是不同的。 为调试键: 调试密钥通常位于以下位置: 您需要运行以下命令来取出MD5。该命令使用Keytool工具: keytool-list-aliasandroiddebugkey-keystore 对于签名密钥: 签名密钥在系统中没有固定的位置。在创建过程中或创建后,无论您将它保存或移动到何处,它都会被保存。运行下面的命令获取MD5,用密钥的别名替换alias_name,用密钥的位置替换my-release-key: keytool-list-aliasalias_name-keystoremy-release-key.keystore 现在XML设置已经准备好了。所需要的只是标记图像和实际代码。让我们从标记图像开始。它叫做ic_maps_current_position_indicator.png,可以在这个项目源代码的drawable-mdpi和drawable-hdpi文件夹中找到。请确保将每个文件夹的图像复制到项目中的对应位置,不要错误地切换它们。 有了图像,我们就可以开始写代码了。我们将从主要的Activity开始。 清单6-8。主要活动进口和报关 `packagecom.paar.ch6; importandroid.app.Activity;importandroid.app.Dialog;importandroid.content.Intent;importandroid.hardware.Camera;importandroid.hardware.Sensor;importandroid.hardware.SensorEvent;importandroid.hardware.SensorEventListener;importandroid.hardware.SensorManager;importandroid.location.Location;importandroid.location.LocationListener;importandroid.location.LocationManager;importandroid.os.Bundle;importandroid.util.Log;importandroid.view.Menu;importandroid.view.MenuInflater;importandroid.view.MenuItem;importandroid.view.SurfaceHolder;importandroid.view.SurfaceView;importandroid.view.View;importandroid.view.View.OnClickListener;importandroid.widget.Button;importandroid.widget.TextView; publicclassASimpleAppUsingARActivityextendsActivity{SurfaceViewcameraPreview;SurfaceHolderpreviewHolder;Cameracamera;booleaninPreview; Buttonbutton;` app的第一个方法,onCreate(),做了很多事情。它将main.xml文件设置为Activity视图。然后它获得位置和传感器系统服务。它为加速计和方向传感器以及全球定位系统(GPS)注册监听器。然后,它执行相机初始化的一部分(其余部分稍后执行)。最后,它获得了对九个TextViews的引用,以便它们可以在应用中更新,并获得了对帮助按钮的引用,设置了它的onClickListener。此方法的代码如下: 清单6-9。主活动的onCreate() 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(newOnClickListener(){publicvoidonClick(Viewv){showHelp();}});}` 代码中的下一个是LocationListener,它监听来自定位服务(本例中是GPS)的位置更新。从GPS接收到更新后,它用新信息更新本地变量,将新信息打印到LogCat,并用新信息更新三个TextViews。它还包含应用中没有使用的方法的自动生成的方法存根。 清单6-10。locationlistener 接下来要解释的是launchFlatBack()方法。每当满足手机或多或少与地面平行的条件时,SensorEventListener就会调用该方法。然后,该方法启动地图。 清单6-11。launchFlatBack() publicvoidlaunchFlatBack(){IntentflatBackIntent=newIntent(this,FlatBack.class);startActivity(flatBackIntent);} 通过覆盖onCreateOptionsMenu()和onOptionsItemSelected()方法来创建和使用选项菜单。第一个从菜单资源(main_menu.xml)创建它,第二个监听菜单上的点击事件。如果单击了帮助项,它将调用适当的方法来显示帮助对话框。 清单6-12。onCreateOptionsMenu()和onOptionsItemSelected() `@OverridepublicbooleanonCreateOptionsMenu(Menumenu){MenuInflaterinflater=getMenuInflater();inflater.inflate(R.menu.main_menu,menu);returntrue;} publicbooleanonOptionsItemSelected(MenuItemitem){//Handleitemselectionswitch(item.getItemId()){caseR.id.help:showHelp();default:returnsuper.onOptionsItemSelected(item);}}` showHelp()是前面提到的合适方法。当单击“帮助”菜单项时,将调用该函数。 清单6-13。showHelp() `publicvoidshowHelp(){finalDialogdialog=newDialog(this);dialog.setContentView(R.layout.help);dialog.setTitle("Help");dialog.setCancelable(true);//therearealotofsettings,fordialog,checkthemallout! //setuptextTextViewtext=(TextView)dialog.findViewById(R.id.TextView01);text.setText(R.string.help);` //setupbuttonButtonbutton=(Button)dialog.findViewById(R.id.Button01);button.setOnClickListener(newOnClickListener(){publicvoidonClick(Viewv){dialog.cancel();}});//nowthatthedialogissetup,it'stimetoshowitdialog.show();} 现在我们来看SensorEventListener。有一个if陈述区分了方位传感器和加速度计。传感器的两个更新都被打印到LogCat和相应的TextViews。此外,代码中方位传感器部分的if语句决定了设备是否与地面平行。存在14度的偏差,因为任何人都不太可能将该设备与地面完全平行。 清单6-14。SensorEventListener 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();}}` `elseif(sensorEvent.sensor.getType()==Sensor.TYPE_ACCELEROMETER){xAxis=sensorEvent.values[0];yAxis=sensorEvent.values[1];zAxis=sensorEvent.values[2]; 我们覆盖了onResume()、onPause()和onDestroy()方法,这样我们就可以释放和重新获取SensorEventListener、LocationListener和Camera。当应用暂停(用户切换到另一个应用)或被破坏(Android终止该进程)时,我们会释放它们,以节省用户的电池和使用更少的系统资源。此外,一次只有一个应用可以使用Camera,所以通过释放它,我们可以让其他应用使用它。 清单6-15。onResume(),onPause()和onDestroy() `@OverridepublicvoidonResume(){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);//Cameracamera;} @OverridepublicvoidonPause(){if(inPreview){camera.stopPreview();}locationManager.removeUpdates(locationListener);sensorManager.unregisterListener(sensorEventListener);if(camera!=null){camera.release();camera=null;}inPreview=false; @OverridepublicvoidonDestroy(){camera.release();camera=null;}` 这最后四个方法处理管理SurfaceView、它的SurfaceHolder和Camera。 清单6-16。getBestPreviewSize()、surfaceCallback()、surfaceChanged()和surfaceDestroyed() SurfaceHolder.CallbacksurfaceCallback=newSurfaceHolder.Callback(){publicvoidsurfaceCreated(SurfaceHolderholder){if(camera==null){camera=Camera.open();}try{camera.setPreviewDisplay(previewHolder);}catch(Throwablet){Log.e(TAG,"ExceptioninsetPreviewDisplay()",t);}} publicvoidsurfaceDestroyed(SurfaceHolderholder){if(camera!=null){camera.stopPreview();camera.setPreviewCallback(null);camera.release();camera=null;}}` };} 这是第一个Java文件的结尾。该文件与GPS和传感器一起工作以获得更新,然后通过TextViews和LogCat输出显示它们。 现在我们来学习FlatBack.java.这个Activity在手机与地面平行时被调用,并在地图上显示你的当前位置。这个类现在没有多大意义,因为部分工作是在FixLocation完成的。 在这个Activity的onCreate()中,我们一如既往地在开头重复SensorManager的东西。这里我们需要传感器输入,因为当设备不再与地面平行时,我们希望切换回CameraView。之后,我们获取对MapView(XML布局中的那个)的引用,告诉Android我们不会实现自己的缩放控件,将MapView传递给FixLocation,将位置覆盖添加到MapView,告诉它更新,并调用自定义方法将它缩放到用户的位置。 importcom.google.android.maps.GeoPoint;importcom.google.android.maps.MapActivity;importcom.google.android.maps.MapView;importcom.google.android.maps.MyLocationOverlay; importandroid.hardware.Sensor;importandroid.hardware.SensorEvent;importandroid.hardware.SensorEventListener;importandroid.hardware.SensorManager;importandroid.os.Bundle;importandroid.util.Log;importandroid.view.Menu;importandroid.view.MenuInflater;importandroid.view.MenuItem;` `publicclassFlatBackextendsMapActivity{privateMapViewmapView;privateMyLocationOverlaymyLocationOverlay;finalstaticStringTAG="PAAR";SensorManagersensorManager; intorientationSensor;floatheadingAngle;floatpitchAngle;floatrollAngle;/**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); //main.xmlcontainsaMapViewsetContentView(R.layout.map); //extractMapViewfromlayoutmapView=(MapView)findViewById(R.id.mapView);mapView.setBuiltInZoomControls(true); //createanoverlaythatshowsourcurrentlocationmyLocationOverlay=newFixLocation(this,mapView); //addthisoverlaytotheMapViewandrefreshitmapView.getOverlays().add(myLocationOverlay);mapView.postInvalidate(); //callconveniencemethodthatzoomsmaponourlocationzoomToMyLocation();}` 清单6-18。onCreateOptionsMenu()和onOptionsItemSelected() `@OverridepublicbooleanonCreateOptionsMenu(Menumenu){MenuInflaterinflater=getMenuInflater();inflater.inflate(R.menu.map_toggle,menu);returntrue;} publicbooleanonOptionsItemSelected(MenuItemitem){//Handleitemselectionswitch(item.getItemId()){caseR.id.map:if(mapView.isSatellite()==true){mapView.setSatellite(false);mapView.setStreetView(true);}returntrue;caseR.id.sat:if(mapView.isSatellite()==false){mapView.setSatellite(true);mapView.setStreetView(false);}returntrue;caseR.id.both:mapView.setSatellite(true);mapView.setStreetView(true);default:returnsuper.onOptionsItemSelected(item);}}` 接下来是SensorEventListener,它与前面的类类似,只是它检查手机是否不再与地面平行,然后调用将带我们回到相机预览的自定义方法。 清单6-19。SensorEventListener `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();}}} publicvoidonAccuracyChanged(Sensorarg0,intarg1){//TODOAuto-generatedmethodstub launchCameraView()方法完成当前的activity,这样我们就可以毫无问题地进行相机预览。一个Intent被注释掉了,似乎做了同样的事情。我把它注释掉了,因为尽管它最终启动了摄像机预览,但它是通过创建那个activity的另一个实例来完成的,这将会产生一个错误,因为摄像机已经被活动的第一个实例使用了。因此,最好返回到以前的实例。 清单6-20。launchCameraView() publicvoidlaunchCameraView(){finish();//IntentcameraView=newIntent(this,ASimpleAppUsingARActivity.class);//startActivity(cameraView);} 然后是onResume()和onPause()方法,它们启用和禁用位置更新以节省资源。 清单6-21。onResume()和onPause() @OverrideprotectedvoidonResume(){super.onResume();`myLocationOverlay.enableMyLocation();} @OverrideprotectedvoidonPause(){super.onPause();myLocationOverlay.disableMyLocation();}` 这是自定义后的zoomToMyLocation()方法。此方法将缩放级别10应用于地图上的当前位置。 清单6-22。zoomToMyLocation() privatevoidzoomToMyLocation(){GeoPointmyLocationGeoPoint=myLocationOverlay.getMyLocation();if(myLocationGeoPoint!=null){mapView.getController().animateTo(myLocationGeoPoint);mapView.getController().setZoom(10);}} 最后是布尔方法isRouteDisplayed()。因为没有在app中使用,所以设置为false。 清单6-23。isRouteDisplayed() protectedbooleanisRouteDisplayed(){returnfalse;}} 这就把我们带到了FlatBack.java的结尾。注意,大多数实际的定位工作似乎是在FixLocation.java中完成的。在您厌倦Eclipse在其引用中给你错误之前,我们将继续编写那个类。 现在是时候了解FixLocation的用途了。在一些Android驱动的设备中,MyLocationOverlay类有严重的错误,其中最显著的是摩托罗拉droid。FixLocation试图使用标准的MyLocationOverlay,但是如果它不能正常工作,它会实现自己的版本,这将产生相同的结果。首先是源代码,然后是解释: 清单6-24。FixLocation.java importandroid.content.Context;importandroid.graphics.Canvas;importandroid.graphics.Paint;importandroid.graphics.Point;importandroid.graphics.Paint.Style;importandroid.graphics.drawable.Drawable;importandroid.location.Location; importcom.google.android.maps.GeoPoint;importcom.google.android.maps.MapView;importcom.google.android.maps.MyLocationOverlay;importcom.google.android.maps.Projection; publicclassFixLocationextendsMyLocationOverlay{ privatebooleanbugged=false; privateDrawabledrawable;privatePaintaccuracyPaint;privatePointcenter;privatePointleft;privateintwidth;privateintheight; publicFixLocation(Contextcontext,MapViewmapView){super(context,mapView);} @OverrideprotectedvoiddrawMyLocation(Canvascanvas,MapViewmapView,LocationlastFix,GeoPointmyLocation,longwhen){if(!bugged){try{super.drawMyLocation(canvas,mapView,lastFix,myLocation,when);}catch(Exceptione){//wefoundabuggyphone,drawthelocationiconsourselvesbugged=true;}}` `if(bugged){if(drawable==null){ accuracyPaint=newPaint();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=newPoint();left=newPoint();} Projectionprojection=mapView.getProjection();doublelatitude=lastFix.getLatitude();doublelongitude=lastFix.getLongitude();floataccuracy=lastFix.getAccuracy(); float[]result=newfloat[1]; Location.distanceBetween(latitude,longitude,latitude,longitude+1,result);floatlongitudeLineDistance=result[0]; GeoPointleftGeo=newGeoPoint((int)(latitude1e6),(int)((longitude-accuracy/longitudeLineDistance)1e6));projection.toPixels(leftGeo,left);projection.toPixels(myLocation,center);intradius=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所示。 图6-2。橙色预警三角 这个三角形仅仅意味着Eclipse无法确认GoogleAPIs是否安装在您的设备上。如果你的Android设备预装了AndroidMarket,你可以很确定它已经安装了GoogleAPIs。 当你运行这个应用时,你应该会看到类似于图6-3到图6-5的截图。 图6-3。app的增强现实视图 图6-4。app的帮助对话框 图6-5。地图显示当装置平行于地面时 LogCat看起来应该类似于图6-6。 图6-6。app的LogCat截图 以下是该应用的四个常见错误。对于其他任何事情,请向安卓开发者谷歌集团或stackoverflow.com寻求帮助。 这将我们带到本书中第一个示例应用的结尾,它演示了如何执行以下操作: 这个应用将在下一章中构建,作为一个简单的AR导航应用。 在第六章中,我们设计了一个简单的AR应用,如果设备与地面平行,它将在相机预览上显示传感器数据,并在地图上显示位置。在这一章中,我们将扩展这个应用,以便它可以用于基本的导航目的。 该应用的扩展版本将具有以下功能: 如果你想扩展它来添加指南针和做其他事情,这个应用会给你你需要的每一个计算。 现在,事不宜迟,让我们开始编码。 首先创建一个新项目。在这个例子中,包名是com.paar.ch7,构建目标是android2.1的GoogleAPIs。我们必须针对谷歌APIsSDK,因为我们正在使用谷歌地图。 首先,复制第六章的项目。把mainActivity(带有相机预览的那个)的名字改成你想要的任何名字,只要你记得更新它的清单。此外,因为这是一个新项目,您可能还需要另一个包名。 首先,我们需要更新一些XML文件。先说strings.xml: 清单7-1。更新strings.xml 这里的“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 `//Cuthere 鉴于我们添加的内容都遵循了前一章的模式,我希望这段代码是不言自明的。id中带有“label”的TextViews是实际值的标签。我们的Java代码不会引用这些。id中带有“value”的TextViews将从我们的Java代码中动态更新以显示值。 现在我们可以开始主要的Java代码了。我们三分之二的Java文件需要用新代码更新。 现在让我们转到下一个需要更新的文件:FlatBack.java: 清单7-4。更新了FlatBack.java的 `packagecom.paar.ch7;* importandroid.content.SharedPreferences;importandroid.hardware.Sensor;importandroid.hardware.SensorEvent;importandroid.hardware.SensorEventListener;importandroid.hardware.SensorManager;importandroid.os.Bundle;importandroid.util.Log;importandroid.view.Menu;importandroid.view.MenuInflater;importandroid.view.MenuItem;importandroid.view.MotionEvent;importandroid.view.View;importandroid.view.View.OnTouchListener; publicclassFlatBackextendsMapActivity{privateMapViewmapView;privateMyLocationOverlaymyLocationOverlay;finalstaticStringTAG="PAAR";SensorManagersensorManager; SharedPreferencesprefs;SharedPreferences.Editoreditor; intorientationSensor;floatheadingAngle;floatpitchAngle;floatrollAngle;StringenteredAddress;booleantapToSet;/**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); //main.xmlcontainsaMapViewsetContentView(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); //callconveniencemethodthatzoomsmaponourlocationzoomToMyLocation(); mapView.setOnTouchListener(newOnTouchListener(){` `publicbooleanonTouch(Viewarg0,MotionEventarg1){ if(tapToSet==true){GeoPointp=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));floatlat=(float)((float)p.getLatitudeE6()/1e6);floatlon=(float)((float)p.getLongitudeE6()/1e6);editor.putFloat("SetLatitude",lat);editor.putFloat("SetLongitude",lon);editor.commit();returntrue;}returnfalse; }); @OverridepublicbooleanonCreateOptionsMenu(Menumenu){MenuInflaterinflater=getMenuInflater();inflater.inflate(R.menu.map_toggle,menu);returntrue;} publicbooleanonOptionsItemSelected(MenuItemitem){//Handleitemselectionswitch(item.getItemId()){caseR.id.map:if(mapView.isSatellite()==true){mapView.setSatellite(false);mapView.setStreetView(true);}returntrue;caseR.id.sat:if(mapView.isSatellite()==false){mapView.setSatellite(true);mapView.setStreetView(false);}returntrue;caseR.id.both:mapView.setSatellite(true);mapView.setStreetView(true);caseR.id.toggleSetDestination:if(tapToSet==false){tapToSet=true;item.setTitle("DisableTaptoSet");}elseif(tapToSet==true){tapToSet=false;item.setTitle("EnableTaptoSet");mapView.invalidate();}default:returnsuper.onOptionsItemSelected(item);}} publicvoidonAccuracyChanged(Sensorarg0,intarg1){ publicvoidlaunchCameraView(){finish();} @OverrideprotectedvoidonResume(){super.onResume();myLocationOverlay.enableMyLocation();} @OverrideprotectedvoidonPause(){super.onPause();myLocationOverlay.disableMyLocation();} protectedbooleanisRouteDisplayed(){returnfalse;}}`**我们来看看有什么变化。首先,我们在顶部有一些新的变量: booleantapToSet;SharedPreferencesprefs;SharedPreferences.Editoreditor; 接下来,我们给我们的onCreate()方法添加了一些新的东西。这两行负责访问我们的SharedPreferences,并允许我们稍后编辑它们: prefs=getSharedPreferences("PAARCH7",0);editor=prefs.edit(); PAARCH7是我们偏好文件的名称,代表ProAndroidAugmentedRealityChapter7。如果你自己扩展这个应用,并从多个地方同时使用SharedPreferences,请记住,当编辑同一个偏好文件时,每个人都可以立即看到这些变化。第一次运行时,PAARCH7文件不存在,所以Android创建了它。逗号后面的小0告诉Android这个文件是私有的。下一行指定编辑器能够编辑我们的首选项。 现在我们的onCreate()方法有了更多的变化。我们给我们的MapView分配一个onTouchListener(): `mapView.setOnTouchListener(newOnTouchListener(){ publicbooleanonTouch(Viewarg0,MotionEventarg1){ });` 在这个onTouchListener(),中,我们过滤每个触摸。如果启用了点击设置模式,我们将捕获触摸事件并获得纬度和经度。然后我们把从touchedGeoPoint接收到的doubles转换成floats,这样我们就可以按照自己的喜好来写了,这正是我们所做的。我们把这两个浮点数都放在我们的首选项文件中,然后调用editor.commit()把它们写到文件中。如果我们捕捉到触摸,我们返回true,如果没有,我们返回false。通过返回false,我们允许MapView继续正常的滚动和放大缩小。 我们需要做的最后一件事是修改我们的onOptionsItemSelected()方法,以允许EnableTapToSet选项。 我们首先检查tapToSet是否是false。如果是,我们将其设置为true,并将标题更改为“禁用点击设置”如果是true,我们把它改成false,把标题改回“启用点击设置” 这份文件就包装好了。 现在我们只剩下主文件了。 我们将从查看新变量开始。 清单7-5。包申报、进口和新变量 `packagecom.paar.ch7; importandroid.app.Activity;importandroid.app.Dialog;importandroid.content.Intent;importandroid.content.SharedPreferences;` `importandroid.hardware.Camera; … doublebearing;doubledistance; floatlat;floatlon; LocationsetLoc;LocationlocationInUse; SharedPreferencesprefs; TextViewbearingValue;TextViewdistanceValue;` 当从文件中读取时,两个浮点数lat和lon将存储我们保存到MapActivity中的SharedPreferences中的值。位置setLoc将被传递前面提到的纬度和经度以创建一个新的Location。然后,我们将使用该位置来获取用户的方位。locationInUse是我们GPS定位的副本。这两个TextViews将显示我们的结果。doublebearing和distance将存储我们的结果。 现在我们需要对我们的onCreate()方法做一些改变。 清单7-6。更新onCreate() `@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);setLoc=newLocation(""); prefs=getSharedPreferences("PAARCH7",0); 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(newOnClickListener(){publicvoidonClick(Viewv){showHelp();}});}` 行prefs=getSharedPreferences("PAARCH7",0);让我们访问我们的SharedPreferences。接下来的新行(bearingValue=(TextView)findViewById(R.id.bearingValue);和distanceValue=(TextView)findViewById(R.id.distanceValue);)将引用我们的新TextViews,并允许我们稍后更新它们。 现在我们必须更新LocationListener,这样我们的计算就会随着位置的更新而更新。这个比较简单。 清单7-7。更新了LocationListener `LocationListenerlocationListener=newLocationListener(){publicvoidonLocationChanged(Locationlocation){locationInUse=location;latitude=location.getLatitude();longitude=location.getLongitude();altitude=location.getAltitude(); 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 `@OverridepublicvoidonResume(){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);//Cameracamera; 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("Unabletogetyourlocationreliably.");distanceValue.setText("Unabletogetyourlocationreliably.");}}` 几乎完全一样,除了如果我们不能得到位置来计算距离和方位,我们也会给出一个消息。 清单7-9。更新AndroidManifest.xml 图7-1–7-5显示了增强现实模式下的应用,打开了帮助对话框和地图。 图7-1。启动时的应用,没有GPS定位 图7-2。打开带有帮助对话框的app 图7-3。打开带有地图的应用,显示选项菜单 图7-4。显示用户当前位置的应用 图7-5。该app带有到达设定位置的方位和距离。地点定在中国中部,我面朝北。 你可以从这本书的apress.com页面或者GitHub库获得完整的源代码。 本章讨论了如何制作导航应用的基本框架。我们允许用户选择地图上的任何一点,然后我们计算用户需要移动的方向作为方位。将它转换为可发布的应用只需要你在增强现实视图中画一个箭头,为用户指出正确的方向。然而,在示例应用中添加这些内容会增加其复杂性,超出本章的范围。 在下一章,你将学习如何设计和实现一个基于标记的增强现实浏览器。* 在完成信息章节并浏览了前两个示例应用后,您现在应该对Android上的增强现实(ar)非常熟悉了。这是倒数第二个示例应用,也是普通非游戏应用中的最后一个,因为最后一个示例应用是使用AR构建的游戏。 这个应用使用标记功能,非常简单。启动时,它会向用户显示内置对象的列表,以显示在标记上,或者让他们选择从设备的内存中选择一个自定义对象。该应用接受波前中的对象。obj文件,以及它们的。mtl同行。如果你不熟悉这些格式和wavefront,我建议你在继续之前先阅读一下。 图8-1。正在展示的安卓模型。 以下是该应用的主要功能: 再次,从创建一个新项目开始。这个项目没有扩展任何以前的项目,所以我们将从零开始。我们将有22个Java文件、8个drawables、4个布局、1个strings.xml和31个资产文件。图8-2显示了项目的详细情况。 图8-2。本章中的项目的细节。 清单8-1。AndroidManifest.xml 让我们通过创建将成为我们主要活动的文件来开始Java代码。 在我的例子中,这个文件被称为ModelChooser.java,它在用户第一次启动应用时显示。它有一个可以显示的预加载模型列表,一个从设备内存加载外部用户提供的模型的选项,以及一个指向帮助文件的链接。 让我们通过对这个文件的onCreate()方法进行一些修改来开始它的编码。 清单8-2。ModelChooser.java的onCreate()方法 `@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);AssetManageram=getAssets();Vectormodels=newVector();Itemitem=newItem();item.text=getResources().getString(R.string.choose_a_model);item.type=Item.TYPE_HEADER;models.add(item); setListAdapter(newModelChooserListAdapter(models));}` 这段代码看起来有点复杂,但是它的任务非常简单。它检索我们的资产文件夹中所有模型的列表,并从中创建一个漂亮的列表。如果模型有相应的图像文件,它会在对象名称旁边以图标样式显示该图像;否则,它只是显示一个类似十字的图像。除了添加应用附带的模型之外,这段代码还添加了选择您自己的模型和访问帮助文件的选项。 接下来,我们需要一个方法来监听点击,并为每次点击做适当的工作。 清单8-3。onListItemClick()方法 @OverrideprotectedvoidonListItemClick(ListViewl,Viewv,intposition,longid){super.onListItemClick(l,v,position,id);Itemitem=(Item)this.getListAdapter().getItem(position);Stringstr=item.text;if(str.equals(getResources().getString(R.string.choose_custom_model))){//startoifilemanageractivityIntentintent=newIntent(ModelChooser.this,CheckFileManagerActivity.class);startActivity(intent);}elseif(str.equals(getResources().getString(R.string.instructions))){//showtheinstructionsactivitystartActivity(newIntent(ModelChooser.this,Instructions.class));}else{//loadtheselectedinternalfileIntentintent=newIntent(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。我们的适配器列表 classModelChooserListAdapterextendsBaseAdapter{`privateVectoritems; publicModelChooserListAdapter(Vectoritems){this.items=items;} publicintgetCount(){returnitems.size();} publicObjectgetItem(intposition){returnitems.get(position);} publiclonggetItemId(intposition){returnposition;} @OverridepublicintgetViewTypeCount(){//normalitems,andtheheaderreturn2;} @OverridepublicbooleanareAllItemsEnabled(){returnfalse;} @OverridepublicbooleanisEnabled(intposition){return!(items.get(position).type==Item.TYPE_HEADER);} @OverridepublicintgetItemViewType(intposition){returnitems.get(position).type;} publicViewgetView(intposition,ViewconvertView,ViewGroupparent){Viewv=convertView;Itemitem=items.get(position);if(v==null){LayoutInflatervi=(LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);switch(item.type){caseItem.TYPE_HEADER:v=vi.inflate(R.layout.list_header,null);break;caseItem.TYPE_ITEM:v=vi.inflate(R.layout.choose_model_row,null);break;}}if(item!=null){switch(item.type){caseItem.TYPE_HEADER:TextViewheaderText=(TextView)v.findViewById(R.id.list_header_title);if(headerText!=null){headerText.setText(item.text);}break;caseItem.TYPE_ITEM:ObjecticonImage=item.icon;ImageViewicon=(ImageView)v.findViewById(R.id.choose_model_row_icon);if(icon!=null){if(iconImageinstanceofInteger){ icon.setImageResource(((Integer)iconImage).intValue());}elseif(iconImageinstanceofBitmap){icon.setImageBitmap((Bitmap)iconImage);}}TextViewtext=(TextView)v.findViewById(R.id.choose_model_row_text);if(text!=null)text.setText(item.text);break;}}returnv;} }` 简而言之,这段代码负责实际提取图标图像、名称等等;然后创建一个列表。这没什么了不起的。在处理列表时,这或多或少是标准的Android代码。 下面是另一个非常小的内部类,处理我们的项目。 清单8-5。内类物品 classItem{privatestaticfinalintTYPE_ITEM=0;privatestaticfinalintTYPE_HEADER=1;privateinttype=TYPE_ITEM;privateObjecticon=newInteger(R.drawable.missingimage);privateStringtext;} 这五个变量用于设置列表中的每一行。TYPE_ITEM是一个常数,我们可以用它来表示包含模型的行,而不是使用整数。TYPE_HEADER与TYPE_ITEM相同,除了它是用于标题。type变量用于存储当前正在处理的项目的类型。默认情况下,它设置为TYPE_ITEM。icon变量用于表示当相应的图像不可用于模型时所使用的图标。text变量用于存储正在处理的当前项目的文本。 这将我们带到主ModelChooser类的末尾。不要忘记插入一个最后的“}”来结束整个外层类。 现在我们已经创建了我们的主活动,让我们按照字母顺序处理剩下的21个Java文件,以便于跟踪它们,并使这一切变得简单一点。 我们现在需要创建一个名为AssetsFileUtility的文件,它将负责读入我们存储在/assets文件夹中的数据。/assets文件夹是一个你可以存储任何你想要的文件的地方,然后以原始字节流的形式检索它。在存储原始文件的能力上,类似于/res/raw。然而,存储在/res/raw中的文件可以通过资源id(如R.raw.filename)进行本地化和访问。/assets文件夹不提供本地化或资源id访问。 清单8-6。AssetsFileUtility.java `publicclassAssetsFileUtilityextendsBaseFileUtil{privateAssetManageram; publicAssetsFileUtility(AssetManageram){this.am=am;} @OverridepublicBitmapgetBitmapFromName(Stringname){InputStreamis=getInputStreamFromName(name);return(isnull)null:BitmapFactory.decodeStream(is);}@OverridepublicBufferedReadergetReaderFromName(Stringname){InputStreamis=getInputStreamFromName(name);return(isnull)null:newBufferedReader(newInputStreamReader(is));} privateInputStreamgetInputStreamFromName(Stringname){InputStreamis;if(baseFolder!=null){try{is=am.open(baseFolder+name);}catch(IOExceptione){e.printStackTrace();returnnull;}}else{try{is=am.open(name);}catch(IOExceptione){e.printStackTrace();returnnull;}}returnis;} 这段代码帮助我们从/assets文件夹中检索一个文件。它处理大部分工作,比如创建InputStreamReaders等等。如果您试图读取的文件不存在或者在其他方面无效(例如,文件扩展名无效),您将得到一个IOException。 接下来是一个名为BaseFileUtil.java的微型类。该文件是其他文件(如AssetsFileUtility)的基础。它允许我们方便地更新正在查看的模型所在的文件夹。 清单8-7。BaseFileUtil.java `publicabstractclassBaseFileUtil{protectedStringbaseFolder=null; publicStringgetBaseFolder(){returnbaseFolder;}publicvoidsetBaseFolder(StringbaseFolder){this.baseFolder=baseFolder;} publicabstractBufferedReadergetReaderFromName(Stringname);publicabstractBitmapgetBitmapFromName(Stringname); 我们按字母顺序排列的下一个是CheckFileManagerActivity。,当用户想要提供他自己的对象以被应用增强时,调用这个函数。通过允许用户查看自己的模型,我们有效地将这个应用变成了一个成熟的3DAR查看器。例如,用户可以设计一把椅子,并在建造它之前看看它在他的房子里会是什么样子。这极大地扩展了我们应用的可用性。目前,该应用仅支持OIFilemanager来选择新文件,但您可以修改代码,以允许该应用与其他文件管理器一起工作。我选择OI作为默认的,因为它预装在很多设备上,如果没有的话,通常也会安装。 让我们一节一节的来看看CheckFileManagerActivity.java。 清单8-8。【CheckFileManagerActivity.java宣言】 `publicclassCheckFileManagerActivityextendsActivity{ privatefinalintPICK_FILE=1;privatefinalintVIEW_MODEL=2;publicstaticfinalintRESULT_ERROR=3; privatefinalintINSTALL_INTENT_DIALOG=1; privatePackageManagerpackageManager;privateResourcesres;privateTextViewinfoText; privatefinalintTOAST_TIMEOUT=3;` 我们做的第一件事是检查OI文件管理器是否安装。如果不是,我们要求用户安装它。如果文件管理器可用,我们允许用户选择一个文件。参见清单8-9。 清单8-9。onCreate()和onResume() `@OverridefinalpublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);Contextcontext=this;packageManager=context.getPackageManager();res=this.getResources();infoText=(TextView)findViewById(R.id.InfoText);if(isPickFileIntentAvailable()){selectFile();}else{installPickFileIntent();}} @OverrideprotectedvoidonResume(){super.onResume();}` 如果选择的文件不是一个有效的模型文件,我们会显示一个提示告诉用户,并要求他再次选择。如果选择的文件是一个有效的模型文件,我们将控制权传递给模型查看器,它将解析并显示该文件。如果用户取消操作,我们将应用返回到模型选择器屏幕。 清单8-10。onActivityResult() selectFile()方法允许用户选择一个模型文件。 清单8-11。selectFile() `/**LettheuserselectaFile.Theselectedfilewillbehandledin 在onCreate方法()中调用了isPickFileIntentAvailable()和installPickFileIntent()方法。 清单8-12。ispickfileattemptavailable()和installPickFileIntent() `privatebooleanisPickFileIntentAvailable(){returnpackageManager.queryIntentActivities(newIntent("org.openintents.action.PICK_FILE"),0).size()>0;} privatebooleaninstallPickFileIntent(){UrimarketUri=Uri.parse("market://searchq=pname:org.openintents.filemanager");IntentmarketIntent=newIntent(Intent.ACTION_VIEW).setData(marketUri);if(!(packageManager.queryIntentActivities(marketIntent,0).size()>0)){//noMarketavailable//showinfotouserandexitinfoText.setText(res.getText(R.string.android_markt_not_avail));returnfalse;}else{//notifyuserandstartAndroidmarket showDialog(INSTALL_INTENT_DIALOG);returntrue;}}` CheckFileManagerActivity.java中最后一个方法是onCreateDialog()。 清单8-13。onCreateDialog() `@OverrideprotectedDialogonCreateDialog(intid){Dialogdialog=null;switch(id){caseINSTALL_INTENT_DIALOG:AlertDialogalertDialog=newAlertDialog.Builder(this).create(); alertDialog.setMessage(res.getText(R.string.pickfile_intent_required));alertDialog.setButton("OK",newDialogInterface.OnClickListener(){publicvoidonClick(DialogInterfacedialog,intwhich){//launchandroidmarketUrimarketUri=Uri.parse("market://searchq=pname:org.openintents.filemanager");IntentmarketIntent=newIntent(Intent.ACTION_VIEW).setData(marketUri);startActivity(marketIntent);return;}});dialog=alertDialog;break;}returndialog;} 列表中的下一个是Config.java文件。这是你见过的最小的Java文件。除去包名,只有三行大小。 清单8-14。Config.java publicclassConfig{publicfinalstaticbooleanDEBUG=false;} 这个文件在技术上是配置文件,尽管它只有一个选项。将DEBUG设置为true会将应用置于调试模式。如果您决定扩展该应用,您可以在此添加其他配置选项,例如您要发布apk的市场的标志。 接下来是FixedPointUtilities类,它处理一些数学函数,主要是转换数组等。这对于保持我们的模型看起来是非常重要的。 清单8-15。FixedPointUtilities.java `publicclassFixedPointUtilities{publicstaticfinalintONE=0x10000; publicstaticinttoFixed(floatval){return(int)(val*65536F);} publicstaticint[]toFixed(float[]arr){int[]res=newint[arr.length];toFixed(arr,res);returnres;} publicstaticvoidtoFixed(float[]arr,int[]storage){for(inti=0;i publicstaticfloat[]toFloat(int[]arr){float[]res=newfloat[arr.length];toFloat(arr,res);returnres;} publicstaticvoidtoFloat(int[]arr,float[]storage){for(inti=0;i publicstaticintmultiply(intx,inty){longz=(long)x*(long)y;return((int)(z>>16));} publicstaticintdivide(intx,inty){longz=(((long)x)<<32);return(int)((z/y)>>16);} publicstaticintsqrt(intn){ints=(n+65536)>>1;for(inti=0;i<8;i++){s=(s+divide(n,s))>>1;}returns;}}` 现在让我们看看这个类中的方法。第一种方法将单浮点值转换为16.16定点值。 第二个方法做同样的事情,只是它是对一个浮点数组做的。 第三个方法由第二个方法调用以帮助其工作。 publicstaticvoidtoFixed(float[]arr,int[]storage){for(inti=0;i 第四种方法将单个定点值转换为浮点数。 publicstaticfloattoFloat(intval){return((float)val)/65536.0f;} 第五个方法对定点值数组做同样的事情,它调用第六个方法来帮助它。 `publicstaticfloat[]toFloat(int[]arr){float[]res=newfloat[arr.length];toFloat(arr,res);returnres;} publicstaticvoidtoFloat(int[]arr,float[]storage){for(inti=0;i 第七种方法是将两个定点值相乘,第八种方法是将两个定点值相除。 `publicstaticintmultiply(intx,inty){longz=(long)x*(long)y;return((int)(z>>16));} publicstaticintdivide(intx,inty){longz=(((long)x)<<32);return(int)((z/y)>>16);}` 第九个也是最后一个方法是求一个定点值的平方根。 publicstaticintsqrt(intn){ints=(n+65536)>>1;for(inti=0;i<8;i++){s=(s+divide(n,s))>>1;}returns;} 这些方法是从MatrixUtils.java调用的。我们的模型本质上是大量的顶点。当我们解析它们时,我们需要处理这些顶点,这有助于我们做到这一点。 接下来,我们有一个名为Group.java的类。此类主要是解析。obj文件和它们的。mtl对应方;并用它们制作合适的、用户友好的图形。这是我们对象解析中相对较小的一部分,但仍然很重要。 在OpenGL中,每个图形都是一组称为顶点的坐标。当这些顶点中有三个或三个以上被线连接起来时,它们被称为面。几个面孔经常被组合在一起。面可能有也可能没有纹理。纹理会改变特定面反射光线的方式。这个类处理组的创建,将每个组关联到一个材质,并设置它的纹理。 清单8-16。Group.java `publicclassGroupimplementsSerializable{privateStringmaterialName="default";privatetransientMaterialmaterial; privatebooleantextured=false;publictransientFloatBuffervertices=null;publictransientFloatBuffertexcoords=null;publictransientFloatBuffernormals=null;publicintvertexCount=0; publicArrayListgroupVertices=newArrayList(500);publicArrayListgroupNormals=newArrayList(500);publicArrayListgroupTexcoords=newArrayList(); publicGroup(){} publicvoidsetMaterialName(StringcurrMat){this.materialName=currMat;} publicStringgetMaterialName(){returnmaterialName;}publicMaterialgetMaterial(){returnmaterial;} publicvoidsetMaterial(Materialmaterial){if(texcoords!=null&&material!=null&&material.hasTexture()){textured=true;}if(material!=null)this.material=material;} publicbooleancontainsVertices(){if(groupVertices!=null)returngroupVertices.size()>0;elseif(vertices!=null)returnvertices.capacity()>0;elsereturnfalse;} publicvoidsetTextured(booleanb){textured=b;} publicbooleanisTextured(){returntextured;} publicvoidfinalize(){if(groupTexcoords.size()>0){textured=true;texcoords=MemUtil.makeFloatBuffer(groupTexcoords.size());for(Iteratoriterator=groupTexcoords.iterator();iterator.hasNext(){FloatcurVal=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;//threefloatspersvertexfor(Iteratoriterator=groupVertices.iterator();iterator.hasNext(){FloatcurVal=iterator.next();vertices.put(curVal.floatValue());}groupVertices=null;normals=MemUtil.makeFloatBuffer(groupNormals.size());for(Iteratoriterator=groupNormals.iterator();iterator.hasNext(){FloatcurVal=iterator.next();normals.put(curVal.floatValue());}groupNormals=null;vertices.position(0);normals.position(0);}}` 该代码主要处理添加纹理和“材料”到我们正在解析的图形。它设置用于图形和材料的纹理。当然,这种材料只是虚拟的,从技术上讲并不是真实的材料。 接下来是另一个非常简单的文件。这个文件叫做Instructions.java,包含Activity,它通过在WebView中显示一个位于/assets/help中的HTML文件来显示我们的应用的指令。 清单8-17。Instructions.java `publicclassInstructionsextendsActivity{ privateWebViewmWebView; @OverrideprotectedvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.instructions_layout);mWebView=(WebView)findViewById(R.id.instructions_webview); WebSettingswebSettings=mWebView.getSettings();webSettings.setSupportZoom(true);webSettings.setBuiltInZoomControls(true); WebChromeClientclient=newWebChromeClient();mWebView.setWebChromeClient(client);mWebView.loadUrl("file:///android_asset/help/"+getResources().getString(R.string.help_file));}}` 活动开始,单个WebView被设置为其视图。然后,WebView被传递一个HTML文件,该文件包含我们的帮助并存储在资产中。 现在我们来看一些更复杂的东西。我们的模型是使用OpenGL渲染的。为了让它们看起来更好,我们还采用了各种照明技术。对于这种照明,我们有一个名为LightingRenderer的类。 清单8-18。LightingRenderer.java `publicclassLightingRendererimplementsOpenGLRenderer{ privatefloat[]ambientlight0={.3f,.3f,.3f,1f};privatefloat[]diffuselight0={.7f,.7f,.7f,1f};privatefloat[]specularlight0={0.6f,0.6f,0.6f,1f};privatefloat[]lightposition0={100.0f,-200.0f,200.0f,0.0f}; privateFloatBufferlightPositionBuffer0=GraphicsUtil.makeFloatBuffer(lightposition0);privateFloatBufferspecularLightBuffer0=GraphicsUtil.makeFloatBuffer(specularlight0);privateFloatBufferdiffuseLightBuffer0=GraphicsUtil.makeFloatBuffer(diffuselight0);privateFloatBufferambientLightBuffer0=GraphicsUtil.makeFloatBuffer(ambientlight0); privatefloat[]ambientlight1={.3f,.3f,.3f,1f};privatefloat[]diffuselight1={.7f,.7f,.7f,1f};privatefloat[]specularlight1={0.6f,0.6f,0.6f,1f};privatefloat[]lightposition1={20.0f,-40.0f,100.0f,1f}; privateFloatBufferlightPositionBuffer1=GraphicsUtil.makeFloatBuffer(lightposition1);privateFloatBufferspecularLightBuffer1=GraphicsUtil.makeFloatBuffer(specularlight1);privateFloatBufferdiffuseLightBuffer1=GraphicsUtil.makeFloatBuffer(diffuselight1);privateFloatBufferambientLightBuffer1=GraphicsUtil.makeFloatBuffer(ambientlight1);privatefloat[]ambientlight2={.4f,.4f,.4f,1f};privatefloat[]diffuselight2={.7f,.7f,.7f,1f};privatefloat[]specularlight2={0.6f,0.6f,0.6f,1f};privatefloat[]lightposition2={5f,-3f,-20f,1.0f}; privateFloatBufferlightPositionBuffer2=GraphicsUtil.makeFloatBuffer(lightposition2);privateFloatBufferspecularLightBuffer2=GraphicsUtil.makeFloatBuffer(specularlight2);privateFloatBufferdiffuseLightBuffer2=GraphicsUtil.makeFloatBuffer(diffuselight2);privateFloatBufferambientLightBuffer2=GraphicsUtil.makeFloatBuffer(ambientlight2); privatefloat[]ambientlight3={.4f,.4f,.4f,1f};privatefloat[]diffuselight3={.4f,.4f,.4f,1f};privatefloat[]specularlight3={0.6f,0.6f,0.6f,1f};privatefloat[]lightposition3={0,0f,-1f,0.0f}; privateFloatBufferlightPositionBuffer3=GraphicsUtil.makeFloatBuffer(lightposition3);privateFloatBufferspecularLightBuffer3=GraphicsUtil.makeFloatBuffer(specularlight3);privateFloatBufferdiffuseLightBuffer3=GraphicsUtil.makeFloatBuffer(diffuselight3);privateFloatBufferambientLightBuffer3=GraphicsUtil.makeFloatBuffer(ambientlight3); publicfinalvoiddraw(GL10gl){ 我们创建浮动来存储不同部分和不同环境下的照明值,然后用它们来创建FloatBuffers。所有这些都通过setupEnv()方法应用到我们的应用中,最后通过initGL方法输出。这段代码比本章到目前为止看到的其他代码更接近AR,并且对于确保我们的图形的照明良好并且看起来真实非常重要。OpenGL总共支持八种不同的照明配置,我们创建了其中的四种(GL_LIGHT0-8)。我们有不同的环境光、高光和漫射光设置,这允许我们给模型四种不同的外观。所有的灯光都设置为GL_SMOOTH,这需要更多的计算能力,但会产生更真实的模型。 清单8-19。Material.java `publicclassMaterialimplementsSerializable{ privatefloat[]ambientlightArr={0.2f,0.2f,0.2f,1.0f};privatefloat[]diffuselightArr={0.8f,0.8f,0.8f,1.0f};privatefloat[]specularlightArr={0.0f,0.0f,0.0f,1.0f}; publictransientFloatBufferambientlight=MemUtil.makeFloatBuffer(4);publictransientFloatBufferdiffuselight=MemUtil.makeFloatBuffer(4);publictransientFloatBufferspecularlight=MemUtil.makeFloatBuffer(4);publicfloatshininess=0;publicintSTATE=STATE_DYNAMIC;publicstaticfinalintSTATE_DYNAMIC=0;publicstaticfinalintSTATE_FINALIZED=1; privatetransientBitmaptexture=null;privateStringbitmapFileName=null;privatetransientBaseFileUtilfileUtil=null; privateStringname="defaultMaterial"; publicMaterial(){ publicMaterial(Stringname){this.name=name;//fillwithdefaultvaluesambientlight.put(newfloat[]{0.2f,0.2f,0.2f,1.0f});ambientlight.position(0);diffuselight.put(newfloat[]{0.8f,0.8f,0.8f,1.0f});diffuselight.position(0);specularlight.put(newfloat[]{0.0f,0.0f,0.0f,1.0f});specularlight.position(0);} publicStringgetName(){returnname;} publicvoidsetName(Stringname){this.name=name;} publicvoidsetFileUtil(BaseFileUtilfileUtil){this.fileUtil=fileUtil;} publicStringgetBitmapFileName(){returnbitmapFileName;} publicvoidsetBitmapFileName(StringbitmapFileName){this.bitmapFileName=bitmapFileName;} publicvoidsetAmbient(float[]arr){ambientlightArr=arr;} publicvoidsetDiffuse(float[]arr){diffuselightArr=arr;} publicvoidsetSpecular(float[]arr){specularlightArr=arr;} publicvoidsetShininess(floatns){shininess=ns;} publicvoidsetAlpha(floatalpha){ambientlight.put(3,alpha);diffuselight.put(3,alpha);specularlight.put(3,alpha);} publicBitmapgetTexture(){returntexture;} publicvoidsetTexture(Bitmaptexture){this.texture=texture;} publicbooleanhasTexture(){if(STATE==STATE_DYNAMIC)returnthis.bitmapFileName!=null;elseif(STATE==STATE_FINALIZED)returnthis.texture!=null;elsereturnfalse;} publicvoidfinalize(){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班。 清单8-20。MemUtil.java `publicclassMemUtil{ publicstaticFloatBuffermakeFloatBufferFromArray(float[]arr){ByteBufferbb=ByteBuffer.allocateDirect(arr.length*4);bb.order(ByteOrder.nativeOrder());FloatBufferfb=bb.asFloatBuffer();fb.put(arr);fb.position(0);returnfb;} publicstaticFloatBuffermakeFloatBuffer(intsize){ByteBufferbb=ByteBuffer.allocateDirect(size*4);bb.order(ByteOrder.nativeOrder());FloatBufferfb=bb.asFloatBuffer();fb.position(0);returnfb;} publicstaticFloatBuffermakeFloatBuffer(float[]arr){ByteBufferbb=ByteBuffer.allocateDirect(arr.length*4);bb.order(ByteOrder.nativeOrder());FloatBufferfb=bb.asFloatBuffer();fb.put(arr);fb.position(0);returnfb;} 这个类非常简单,不需要太多解释,因为它是非常标准的Java。我们需要floatbuffer,因为OpenGL在其照明和材质实现中只接受floatbuffer参数。 现在我们有了一个对我们的应用非常重要的类,Model.java。 清单8-21。Model.java `publicclassModelimplementsSerializable{ publicfloatxrot=90;publicfloatyrot=0;publicfloatzrot=0;publicfloatxpos=0;publicfloatypos=0;publicfloatzpos=0;publicfloatscale=4f;publicintSTATE=STATE_DYNAMIC;publicstaticfinalintSTATE_DYNAMIC=0;publicstaticfinalintSTATE_FINALIZED=1; 这个课程为创建我们的模型做了大量的基础工作。让我们一个方法一个方法来看。方法是构造器,为我们的模型设置默认材质。addMaterial()方法为我们的应用添加了一个素材。addGroup()方法将另一个组添加到我们的组中,如果需要的话,还会完成它。setFileUtil()方法将一个BaseFileUtil作为参数,然后用它来设置我们所有材料的fileUtil。setScale()方法允许我们传递一个设置为刻度的浮点数。它还确保小数位数是非零的正值。该比例值用于缩放模型。setXrot()和setYrot()方法允许我们在模型上设置X轴和Y轴的旋转。setXpos()和setYpos()方法用于设置模型在X轴和Y轴上的位置。方法确定了一切,并使之不可改变。 我们列表中的下一个是Model3D.java,它负责我们模型的大量绘制。解释在代码之后。 清单8-22。Model3D.java `publicclassModel3DextendsARObjectimplementsSerializable{ privateModelmodel;privateGroup[]texturedGroups;privateGroup[]nonTexturedGroups;privateHashMap publicModel3D(Modelmodel,StringpatternName){super("model",patternName,80.0,newdouble[]{0,0});this.model=model;model.finalize(); Vectorgroups=model.getGroups();VectortexturedGroups=newVector();VectornonTexturedGroups=newVector();for(Iteratoriterator=groups.iterator();iterator.hasNext(){GroupcurrGroup=iterator.next();if(currGroup.isTextured()){texturedGroups.add(currGroup);}else{nonTexturedGroups.add(currGroup);}}this.texturedGroups=texturedGroups.toArray(newGroup[texturedGroups.size()]);this.nonTexturedGroups=nonTexturedGroups.toArray(newGroup[nonTexturedGroups.size()]);} @Overridepublicvoidinit(GL10gl){int[]tmpTextureID=newint[1]; IteratormaterialI=model.getMaterials().values().iterator();while(materialI.hasNext()){Materialmaterial=(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);}}} @Overridepublicvoiddraw(GL10gl){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);intcnt=nonTexturedGroups.length;for(inti=0;i gl.glEnable(GL10.GL_TEXTURE_2D);gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); cnt=texturedGroups.length;for(inti=0;i 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变量 `publicclassModelViewerextendsAndARActivityimplementsSurfaceHolder.Callback{publicstaticfinalintTYPE_INTERNAL=0;publicstaticfinalintTYPE_EXTERNAL=1;publicstaticfinalbooleanDEBUG=false;privatefinalintMENU_SCALE=0;privatefinalintMENU_ROTATE=1;privatefinalintMENU_TRANSLATE=2;privatefinalintMENU_SCREENSHOT=3; privateintmode=MENU_SCALE;privateModelmodel;privateModelmodel2;privateModelmodel3;privateModelmodel4;privateModelmodel5;privateModel3Dmodel3d;privateModel3Dmodel3d2;privateModel3Dmodel3d3;privateModel3Dmodel3d4;privateModel3Dmodel3d5;privateProgressDialogwaitDialog;privateResourcesres;ARToolkitartoolkit;` 这个类的构造函数如下所示。 清单8-24。ModelViewer.java建造师 publicModelViewer(){super(false);} 我们文件的onCreate()方法通过LightingRenderer.java设置照明,获取应用的资源,将ARToolkit的一个实例分配给artoolkit,为表面视图设置触摸事件监听器,并为表面视图添加回调。 清单8-25。onCreate()方法 @OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);super.setNonARRenderer(newLightingRenderer());res=getResources();artoolkit=getArtoolkit();getSurfaceView().setOnTouchListener(newTouchEventHandler());getSurfaceView().getHolder().addCallback(this);} 捕捉我们在别处没有显式捕捉到的任何异常。另外两种方法是非常常见的标准Android代码,用于创建菜单和监听用户在菜单上的活动。 清单8-26。捕捉异常并使用菜单 publicvoiduncaughtException(Threadthread,Throwableex){System.out.println("");}@OverridepublicbooleanonCreateOptionsMenu(Menumenu){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);returntrue;}publicbooleanonOptionsItemSelected(MenuItemitem){switch(item.getItemId()){caseMENU_SCALE:mode=MENU_SCALE;returntrue;caseMENU_ROTATE:mode=MENU_ROTATE;returntrue;caseMENU_TRANSLATE:mode=MENU_TRANSLATE;returntrue;caseMENU_SCREENSHOT:newTakeAsyncScreenshot().execute();returntrue;}returnfalse;} surfaceCreated()在创建SurfaceView时被调用,它在模型加载时显示一个进度对话框。 清单8-27。surfaceCreated() `@OverridepublicvoidsurfaceCreated(SurfaceHolderholder){super.surfaceCreated(holder); if(model==null){waitDialog=ProgressDialog.show(this,"",getResources().getText(R.string.loading),true);waitDialog.show();newModelLoader().execute();}}` 这个内部类截获我们活动中发生的每一个触摸事件。它接受这种事件,然后适当地缩放、平移或旋转模型。 清单8-28。内部类TouchEventHandler classTouchEventHandlerimplementsOnTouchListener{privatefloatlastX=0;privatefloatlastY=0;publicbooleanonTouch(Viewv,MotionEventevent){if(model!=null){switch(event.getAction()){default:caseMotionEvent.ACTION_DOWN:lastX=event.getX();lastY=event.getY();break;caseMotionEvent.ACTION_MOVE:floatdX=lastX-event.getX();floatdY=lastY-event.getY();lastX=event.getX();lastY=event.getY();if(model!=null){switch(mode){caseMENU_SCALE:model.setScale(dY/100.0f);break;caseMENU_ROTATE:model.setXrot(-1*dX);model.setYrot(-1*dY);break;caseMENU_TRANSLATE:model.setXpos(dY/10f);model.setYpos(dX/10f);break;}}break;caseMotionEvent.ACTION_CANCEL:caseMotionEvent.ACTION_UP:lastX=event.getX();lastY=event.getY();break;}}returntrue;}} InthisModelLoaderinnerclass,weuseaseriesofifelsestatementstodeterminethemodelthatweneedtoload.Wealsosetthedifferentmarkersrequiredforsomeoftheinbuiltmodels.Thedefaultmarkerforsomeoftheinbuiltmodels,andallexternalmodelsarecalledandroid.Ifthemodelisfromanexternalfile,wefirsttrimitbeforeloadingit.Ifitisaninbuiltmodel,weloaditdirectly.InonPostExecute(),weregisterallthemodels,anddismisstheprogressdialogbox. 清单8-29。模型加载器 privateclassModelLoaderextendsAsyncTask fileUtil.setBaseFolder(modelFile.getParentFile().getAbsolutePath());break;caseTYPE_INTERNAL:fileUtil=newAssetsFileUtility(getResources().getAssets());fileUtil.setBaseFolder("models/");break;}if(modelFileName.endsWith(".obj")){ObjParserparser=newObjParser(fileUtil);try{if(Config.DEBUG)Debug.startMethodTracing("AndObjViewer");if(type==TYPE_EXTERNAL){BufferedReadermodelFileReader=newBufferedReader(newFileReader(modelFile));Stringshebang=modelFileReader.readLine(); if(!shebang.equals("#trimmed")){FiletrimmedFile=newFile(modelFile.getAbsolutePath()+".tmp");BufferedWritertrimmedFileWriter=newBufferedWriter(newFileWriter(trimmedFile));Util.trim(modelFileReader,trimmedFileWriter);if(modelFile.delete()){trimmedFile.renameTo(modelFile);}}}if(fileUtil!=null){BufferedReaderfileReader=fileUtil.getReaderFromName(modelFileName);if(fileReader!=null){model=parser.parse("Model",fileReader);Log.w("ModelLoader","model3d=newModel3D(model,"+modelName2patternName(modelFileName)+".patt");model3d=newModel3D(model,modelName2patternName(modelFileName) try{if(model3d!=null){artoolkit.registerARObject(model3d);artoolkit.registerARObject(model3d2);artoolkit.registerARObject(model3d3);artoolkit.registerARObject(model3d4);artoolkit.registerARObject(model3d5);}}catch(AndARExceptione){e.printStackTrace();}startPreview();}}` 在TakeAsyncScreenshot内部类中,我们调用AndAR的内置截图功能。 清单8-30。takeasync截图 `classTakeAsyncScreenshotextendsAsyncTask privateStringerrorMsg=null; @OverrideprotectedVoiddoInBackground(Void...params){Bitmapbm=takeScreenshot();FileOutputStreamfos;try{fos=newFileOutputStream("/sdcard/AndARScreenshot"+newDate().getTime()+".png");bm.compress(CompressFormat.PNG,100,fos);fos.flush();fos.close();}catch(FileNotFoundExceptione){errorMsg=e.getMessage();e.printStackTrace();}catch(IOExceptione){errorMsg=e.getMessage();e.printStackTrace();}returnnull;} protectedvoidonPostExecute(Voidresult){if(errorMsg==null)Toast.makeText(ModelViewer.this,getResources().getText(R.string.screenshotsaved),Toast.LENGTH_SHORT).show();elseToast.makeText(ModelViewer.this,getResources().getText(R.string.screenshotfailed)+errorMsg,Toast.LENGTH_SHORT).show();};}}` 接下来我们有一个非常重要的课程。这个类负责解析。伴随。我们模型的obj文件。 清单8-31。MtlParser.java `publicclassMtlParser{ privateBaseFileUtilfileUtil;publicMtlParser(Modelmodel,BaseFileUtilfileUtil){this.fileUtil=fileUtil;} privatestaticfloat[]parseTriple(Stringstr){String[]colorVals=str.split("");float[]colorArr=newfloat[]{Float.parseFloat(colorVals[0]),Float.parseFloat(colorVals[1]),Float.parseFloat(colorVals[2])};returncolorArr;}}` 这个类不是很复杂。基本上,该类逐行读取整个文件,并通过查看文件的开头来处理每一行。绝对第一条件确保该行实际上是一行,而不是一个空行。之后,嵌套的ifelse语句就出现了。 从现在开始提到的所有条件都来自嵌套语句,除非另有说明。第一个这样的条件通过查看该行是否以“newmtl”开头来检查该行是否是第一行。 if(line.startsWith("newmtl")){StringmtlName=line.substring(7);curMat=newMaterial(mtlName);model.addMaterial(curMat); 下一个条件确保我们当前的材料不为空。 }elseif(curMat==null){ 第三个用于忽略注释,因为它们在。mtl文件。 }elseif(line.startsWith("#")){ 第四个条件查看该行是否为我们的模型指定了环境光,如果指定了,就设置它。 }elseif(line.startsWith("Ka")){StringendOfLine=line.substring(3);curMat.setAmbient(parseTriple(endOfLine)); 第五个条件是看这条线是否为我们的模型指定了漫射光,如果指定了,就设置它。 }elseif(line.startsWith("Kd")){StringendOfLine=line.substring(3);curMat.setDiffuse(parseTriple(endOfLine)); 第六个条件是看这条线是否为我们的模型指定了镜面反射光,如果是,就设置它。 }elseif(line.startsWith("Ks")){StringendOfLine=line.substring(3);curMat.setSpecular(parseTriple(endOfLine)); 第七个条件检查该行是否指定了模型的亮度,如果指定了,就设置它。 }elseif(line.startsWith("Ns")){StringendOfLine=line.substring(3);curMat.setShininess(Float.parseFloat(endOfLine)); 第八个和第九个条件检查该行是否为我们的模型指定了alpha值,如果指定了,就设置它。 }elseif(line.startsWith("Tr")){StringendOfLine=line.substring(3);curMat.setAlpha(Float.parseFloat(endOfLine));}elseif(line.startsWith("d")){StringendOfLine=line.substring(2);curMat.setAlpha(Float.parseFloat(endOfLine)); 第十个和第十一个条件检查该行是否为该模型指定了图像,如果是,就设置它。 }elseif(line.startsWith("map_Kd")){StringimageFileName=line.substring(7);curMat.setFileUtil(fileUtil);curMat.setBitmapFileName(imageFileName);}elseif(line.startsWith("mapKd")){StringimageFileName=line.substring(6);curMat.setFileUtil(fileUtil);curMat.setBitmapFileName(imageFileName); 方法末尾的catch语句捕捉到了IOException,这将由诸如文件未被找到或文件具有不利权限之类的情况触发。 }catch(IOExceptione){e.printStackTrace();} floatparseTriple()被反复调用来帮助解析文件。 privatestaticfloat[]parseTriple(Stringstr){String[]colorVals=str.split("");float[]colorArr=newfloat[]{Float.parseFloat(colorVals[0]),Float.parseFloat(colorVals[1]),Float.parseFloat(colorVals[2])};returncolorArr;} 接下来是另一个非常重要的类,ObjParser.java。它解析。obj文件,至少在一定程度上。它不支持完整的。obj规范。它支持以下内容: 这样的支持对于我们的车型来说已经足够了。 清单8-32。ObjParser.java `publicclassObjParser{privatefinalintVERTEX_DIMENSIONS=3;privatefinalintTEXTURE_COORD_DIMENSIONS=2; privateBaseFileUtilfileUtil; publicObjParser(BaseFileUtilfileUtil){this.fileUtil=fileUtil;} publicModelparse(StringmodelName,BufferedReaderis)throwsIOException,ParseException{ArrayList Modelmodel=newModel();GroupcurGroup=newGroup();MtlParsermtlParser=newMtlParser(model,fileUtil);SimpleTokenizerspaceTokenizer=newSimpleTokenizer();SimpleTokenizerslashTokenizer=newSimpleTokenizer();slashTokenizer.setDelimiter("/"); 这个文件通过。obj文件逐行。有一系列解析文件的ifelse块。每一行都会发生以下情况: 完成后,它返回一个模型。 接下来是ParseException.java类,也就是在ObjParser.java中反复抛出的ParseException。这是一个自定义的异常,我们编写它是为了让我们能够轻松地解决解析过程中出现的问题。 清单8-33。ParseException.java publicclassParseExceptionextendsException{publicParseException(Stringfile,intlineNumber,Stringmsg){super("Parseerrorinfile"+file+"online"+lineNumber+":"+msg);}} 很简单;它只是输出一条消息,通过参数填充消息的具体细节。 接下来是Renderer.java文件,它处理我们图形的大量绘图工作,包括一些复杂的3D内容。 清单8-34。Renderer.java `publicclassRendererimplementsGLSurfaceView.Renderer{ privatefinalVectormodels;privatefinalVector3DcameraPosition=newVector3D(0,3,50);longframe,time,timebase=0;publicRenderer(Vectormodels){this.models=models;}publicvoidaddModel(Model3Dmodel){if(!models.contains(model)){models.add(model);}}publicvoidonDrawFrame(GL10gl){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(Iteratoriterator=models.iterator();iterator.hasNext(){Model3Dmodel=iterator.next();model.draw(gl);}} publicvoidonSurfaceChanged(GL10gl,intwidth,intheight){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();} publicvoidonSurfaceCreated(GL10gl,EGLConfigconfig){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(Iteratoriterator=models.iterator();iterator.hasNext(){Model3Dmodel=iterator.next();model.init(gl);}}}` 接下来是SDCardFileUtil.java。这是BaseFileUtil的扩展,处理文件的读取。 清单8-35。SDCardFileUtil.java `publicclassSDCardFileUtilextendsBaseFileUtil{publicBufferedReadergetReaderFromName(Stringname){if(baseFolder!=null){try{returnnewBufferedReader(newFileReader(newFile(baseFolder,name)));}catch(FileNotFoundExceptione){returnnull;}}else{try{returnnewBufferedReader(newFileReader(newFile(name)));}catch(FileNotFoundExceptione){returnnull;}}} publicBitmapgetBitmapFromName(Stringname){if(baseFolder!=null){Stringpath=newFile(baseFolder,name).getAbsolutePath();returnBitmapFactory.decodeFile(path);}else{returnBitmapFactory.decodeFile(name);}}}` 第一个方法试图通过文件名获得一个BufferedReader,第二个方法试图通过文件名获得一个位图。 接下来是SimpleTokenizer.javaclass,which类在很多地方被用作Tokenizer来分隔字符串。 清单8-36。SimpleTokenizer.java publicclassSimpleTokenizer{Stringstr="";Stringdelimiter="";intdelimiterLength=delimiter.length();inti=0;intj=0;publicfinalStringgetStr(){returnstr;}publicfinalvoidsetStr(Stringstr){this.str=str;i=0;j=str.indexOf(delimiter);}publicfinalStringgetDelimiter(){returndelimiter;}publicfinalvoidsetDelimiter(Stringdelimiter){this.delimiter=delimiter;delimiterLength=delimiter.length();}publicfinalbooleanhasNext(){returnj>=0;}publicfinalStringnext(){if(j>=0){Stringresult=str.substring(i,j);i=j+1;j=str.indexOf(delimiter,i);`returnresult;}else{returnstr.substring(i);}}publicfinalStringlast(){returnstr.substring(i);} publicfinalintdelimOccurCount(){intresult=0;if(delimiterLength>0){intstart=str.indexOf(delimiter);while(start!=-1){result++;start=str.indexOf(delimiter,start+delimiterLength);}}returnresult;}}` 这是一个简单的类。所有东西都来自标准的Java包,不需要导入任何东西。严格来说,这个类没有使用AndroidAPI。你可以把它复制粘贴到一个普通的Java项目中,它将完美地工作。 接下来是Util.java。这个类优化了我们的。obj文件,以便下次可以更快地解析它们。 清单8-37。Util.java `publicclassUtil{privatestaticfinalPatterntrimWhiteSpaces=Pattern.compile("[\s]+");privatestaticfinalPatternremoveInlineComments=Pattern.compile("#");privatestaticfinalPatternsplitBySpace=Pattern.compile(""); publicstaticfinalStringgetCanonicalLine(Stringline){line=trimWhiteSpaces.matcher(line).replaceAll("");if(line.contains("#")){String[]parts=removeInlineComments.split(line);if(parts.length>0)line=parts[0];}returnline;}publicstaticString[]splitBySpace(Stringstr){returnsplitBySpace.split(str);} publicstaticvoidtrim(BufferedReaderin,BufferedWriterout)throwsIOException{Stringline;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();} publicfinalstaticListfastSplit(finalStringtext,charseparator,finalbooleanemptyStrings){finalListresult=newArrayList(); if(text!=null&&text.length()>0){intindex1=0;intindex2=text.indexOf(separator);while(index2>=0){Stringtoken=text.substring(index1,index2);result.add(token);index1=index2+1;index2=text.indexOf(separator,index1);} if(index1 returnresult;} 这又是标准Java。它只是删除空白,行内注释等。以便下次更快地解析。 现在我们来看最后一个Java文件,Vector3D.java。该文件使用三维矢量。这个类用于定位我们的虚拟OpenGL相机。这个摄像头和我们一直用的硬件摄像头很不一样。这是一个虚拟的摄像机,我们可以从中看到我们的模型。 清单8-38。Vector3D.java `publicclassVector3DimplementsSerializable{publicfloatx=0;publicfloaty=0;publicfloatz=0; publicVector3D(floatx,floaty,floatz){super();this.x=x;this.y=y;this.z=z;}publicfloatgetX(){returnx;} publicvoidsetX(floatx){this.x=x;} publicfloatgetY(){returny;} publicvoidsetY(floaty){this.y=y;} publicfloatgetZ(){returnz;} publicvoidsetZ(floatz){this.z=z;} 所有的方法都是获取或者设置x,y,或者z。仅此而已。 既然我们已经完成了所有的Java文件,我们可以继续处理XML文件了。 让我们从strings.xml开始。 清单8-39。strings.xml `PAARChapter8Selectamodelfile:Androidmarketisnotavailable,youneedinstalltoOIFilemanagermanually.YouneedtoinstalltheOIFileManagerinordertousethisapplication.Thisfiledoesn'texist!Unknownfiletype!rotatetranslatescale Loading.Pleasewait...AndARModelViewerallowsyoutoviewwavefrontobjmodelsonanAugmentedRealitymarker.TakeascreenshotScreenshotsaved!Failedtosavescreenshot:SelectamodelfileInstructionsSelectanobjmodelfile.(.obj)Chooseamodel:Help:Custommodel:index.html` 这个文件包含我们应用的所有预定义字符串。每个字符串的内容和名称应该为您提供它们的确切描述。 现在让我们看看布局文件。我们有choose_model_row.xml,用在ModelChooser里。 清单8-40。【choose_model_row.XML】 我们将图标的ImageView和名称的TextView放在一起。这就是我们行的全部布局。 接下来是instructions_layout.xml,它是我们的指令活动背后的XML文件。 清单8-41。说明_布局.xml 这里我们有一个完全由WebView填充的线性布局来显示指令HTML文件。 接下来我们有list_header.xml,顾名思义,它是我们列表的标题。 清单8-42。list_header.xml 最后我们有main.xml,用来显示信息。 清单8-43。main.xml 这就把我们带到了XML文件的末尾。现在剩下的就是一个HTML文件,也就是我们的指令文件。它位于/assets/help/中。 清单8-44。index.html 图8-3和8-4显示了应用的运行情况。 图8-3。app的开启画面。 图8-4。加载安卓模式。 在这一章中,我们使用AndAR框架创建了一个全功能的3DAR对象查看器。我们的应用有能力加载内部和外部模型;以3D方式显示它们;并且允许用户调整大小、旋转和重新定位它们。在下一章中,我们将构建一个AR应用,探索AR的社交和游戏功能。 图9-1显示应用正在运行,两个数据源的标记都可见。 图9-1。app运行时的截图。 为了完成这一切,我们将编写自己的迷你AR引擎,并使用两个免费资源来获取维基百科和Twitter数据。与第八章相比,这段代码不是很长,但有些是新的,尤其是移动叠加部分。事不宜迟,让我们开始编码吧。 这个应用中的xml只包含strings.xml和菜单的XML。我们将快速输入这些内容,然后转到Java代码。 清单9-1。strings.xml 字符串app_name仅仅存储我们的应用的名称。该名称显示在启动器中的图标下。 现在我们来看看menu.xml。 清单9-2。menu.xml 第一项是显示和隐藏雷达的切换,雷达将用于显示用户视野之外的对象的图标。第二项是切换显示和隐藏SeekBar小部件,允许用户调整推文和维基百科信息的半径。 有了这些XML,我们可以继续我们应用的Java代码了。 在这个应用中,我们将看看Java代码的格式,其中不同的类按功能分组。所以我们将依次查看所有的数据解析类,依此类推。 让我们从应用的基础部分开始。我们有一个SensorsActivity,扩展了标准的androidActivity。SensorsActivity没有用户界面。AugmentedActivity然后扩展这个SensorsActivity,这个【】又扩展了MainActivity,也就是最终显示给用户的Activity。所以我们先来看看SensorsActivity。 清单9-3。SensorsActivity.java全局变量 publicclassSensorsActivityextendsActivityimplementsSensorEventListener,LocationListener{privatestaticfinalStringTAG="SensorsActivity";privatestaticfinalAtomicBooleancomputing=newAtomicBoolean(false);`privatestaticfinalintMIN_TIME=30*1000;privatestaticfinalintMIN_DISTANCE=10; privatestaticfinalfloattemp[]=newfloat[9];privatestaticfinalfloatrotation[]=newfloat[9];privatestaticfinalfloatgrav[]=newfloat[3];privatestaticfinalfloatmag[]=newfloat[3]; privatestaticfinalMatrixworldCoord=newMatrix();privatestaticfinalMatrixmagneticCompensatedCoord=newMatrix();privatestaticfinalMatrixxAxisRotation=newMatrix();privatestaticfinalMatrixmagneticNorthCompensation=newMatrix(); privatestaticGeomagneticFieldgmf=null;privatestaticfloatsmooth[]=newfloat[3];privatestaticSensorManagersensorMgr=null;privatestaticListsensors=null;privatestaticSensorsensorGrav=null;privatestaticSensorsensorMag=null;privatestaticLocationManagerlocationMgr=null;` 现在让我们来看看我们的方法。在这个特定的Activity中,我们不需要在onCreate(),中做任何事情,所以我们只需要实现它的一个基本版本,这样扩展它的类就可以使用它。我们的主要工作是用onStart()方法完成的: 清单9-4。【SensorsActivity.java】onCreate()和onStart() `@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);} @OverridepublicvoidonStart(){super.onStart(); doubleangleX=Math.toRadians(-90);doubleangleY=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{Locationgps=locationMgr.getLastKnownLocation(LocationManager.GPS_PROVIDER);Locationnetwork=locationMgr.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);if(gps!=null){onLocationChanged(gps);}elseif(network!=null){onLocationChanged(network);}else{onLocationChanged(ARData.hardFix);}}catch(Exceptionex2){onLocationChanged(ARData.hardFix);} gmf=newGeomagneticField((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(Exceptionex){ex.printStackTrace();}}catch(Exceptionex1){try{if(sensorMgr!=null){sensorMgr.unregisterListener(this,sensorGrav);sensorMgr.unregisterListener(this,sensorMag);sensorMgr=null;}if(locationMgr!=null){locationMgr.removeUpdates(this);locationMgr=null;}}catch(Exceptionex2){ex2.printStackTrace();}}}` 这个文件中的下一个是我们的onStop()方法,它与本书前面使用的onResume()和onStop()方法相同。我们只是用它来放开传感器和GPS,以节省用户的电池寿命,并在不需要时停止收集数据。 清单9-5。SensorsActivity.java *`@OverrideprotectedvoidonStop(){super.onStop(); try{try{sensorMgr.unregisterListener(this,sensorGrav);sensorMgr.unregisterListener(this,sensorMag);}catch(Exceptionex){ex.printStackTrace();}sensorMgr=null; 清单9-6。SensorsActivity.java监听着传感器 `publicvoidonSensorChanged(SensorEventevt){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];}elseif(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);} publicvoidonProviderDisabled(Stringprovider){//NotUsed} publicvoidonProviderEnabled(Stringprovider){//NotUsed} publicvoidonStatusChanged(Stringprovider,intstatus,Bundleextras){//NotUsed} publicvoidonLocationChanged(Locationlocation){ARData.setCurrentLocation(location);gmf=newGeomagneticField((float)ARData.getCurrentLocation().getLatitude(),(float)ARData.getCurrentLocation().getLongitude(),(float)ARData.getCurrentLocation().getAltitude(),System.currentTimeMillis()); doubleangleY=Math.toRadians(-gmf.getDeclination()); synchronized(magneticNorthCompensation){magneticNorthCompensation.toIdentity(); magneticNorthCompensation.prod(xAxisRotation);}} publicvoidonAccuracyChanged(Sensorsensor,intaccuracy){if(sensornull)thrownewNullPointerException();if(sensor.getType()==Sensor.TYPE_MAGNETIC_FIELD&&accuracySensorManager.SENSOR_STATUS_UNRELIABLE){Log.e(TAG,"Compassdataunreliable");}}}` 这段代码中有六种方法。这些是onSensorChanged()、onProviderDisabled()、onProviderEnabled()、onStatusChanged、onLocationChanged()和onAccuracyChanged()。onProviderDisabled()、onProviderEnabled()和onStatusChanged()没有被使用,但是仍然在那里,因为它们必须被实现。 在onLocationChanged()中,我们首先更新ARData中的位置,用新数据重新计算gmf,然后执行与在onStart()中相同的代码。 在onAccuracyChanged()中,我们首先检查数据是否为null。如果是,则抛出一个NullPointerException。如果数据不是null,指南针似乎变得不可靠,我们给LogCat添加一条错误消息来说明这一点。 publicclassAugmentedViewextendsView{privatestaticfinalAtomicBooleandrawing=newAtomicBoolean(false);privatestaticfinalRadarradar=newRadar();privatestaticfinalfloat[]locationArray=newfloat[3];privatestaticfinalList 现在让我们看看它的构造函数和onDraw()方法。 清单9-8。onDraw()方法和AugmentedView的构造函数 `publicAugmentedView(Contextcontext){super(context);} @OverrideprotectedvoidonDraw(Canvascanvas){if(canvas==null)return; if(drawing.compareAndSet(false,true)){Listcollection=ARData.getMarkers(); cache.clear();for(Markerm:collection){m.update(canvas,0,0);if(m.isOnRadar())cache.add(m);}collection=cache; if(AugmentedActivity.useCollisionDetection)adjustForCollisions(canvas,collection); ListIteratoriter=collection.listIterator(collection.size());while(iter.hasPrevious()){Markermarker=iter.previous();marker.draw(canvas);}if(AugmentedActivity.showRadar)radar.draw(canvas);drawing.set(false);}}` 这个类的构造函数仅仅通过super()将它绑定到视图。在onDraw()方法中,我们首先将雷达上的所有标记添加到缓存变量中,然后将其复制到集合中。然后对标记进行碰撞调整(详见下一段代码清单),最后绘制所有标记,更新雷达。 现在让我们来看看调整碰撞标记的代码: 清单9-9。调整碰撞 `privatestaticvoidadjustForCollisions(Canvascanvas,Listcollection){updated.clear();for(Markermarker1:collection){if(updated.contains(marker1)||!marker1.isInView())continue; intcollisions=1;for(Markermarker2:collection){if(marker1.equals(marker2)||updated.contains(marker2)||!marker2.isInView())continue; if(marker1.isMarkerOnMarker(marker2)){marker2.getLocation().get(locationArray);floaty=locationArray[1];floath=collisions*COLLISION_ADJUSTMENT;locationArray[1]=y+h;marker2.getLocation().set(locationArray);marker2.update(canvas,0,0);collisions++;updated.add(marker2);}}updated.add(marker1);}}}//Closesclass` 我们使用此代码来检查一个或多个标记是否与另一个标记重叠,然后调整它们的位置数据,以便在绘制它们时,它们不会重叠。我们使用来自marker类的方法(在本章后面写)来检查标记是否重叠,然后适当地调整它们在locationArray中的位置。 现在我们已经写完了AugmentedView,我们可以开始写AugmentedActivity了。我们必须首先扩展视图类,因为我们将在AugmentedActivity中使用AugmentedView。让我们从类和全局变量开始。 `publicclassAugmentedActivityextendsSensorsActivityimplementsOnTouchListener{privatestaticfinalStringTAG="AugmentedActivity";privatestaticfinalDecimalFormatFORMAT=newDecimalFormat("#.##");privatestaticfinalintZOOMBAR_BACKGROUND_COLOR=Color.argb(125,55,55,55);privatestaticfinalStringEND_TEXT=FORMAT.format(AugmentedActivity.MAX_ZOOM)+"km";privatestaticfinalintEND_TEXT_COLOR=Color.WHITE; protectedstaticWakeLockwakeLock=null;protectedstaticCameraSurfacecamScreen=null;protectedstaticVerticalSeekBarmyZoomBar=null;protectedstaticTextViewendLabel=null;protectedstaticLinearLayoutzoomLayout=null;protectedstaticAugmentedViewaugmentedView=null; publicstaticfinalfloatMAX_ZOOM=100;//inKMpublicstaticfinalfloatONE_PERCENT=MAX_ZOOM/100f;publicstaticfinalfloatTEN_PERCENT=10fONE_PERCENT;publicstaticfinalfloatTWENTY_PERCENT=2fTEN_PERCENT;publicstaticfinalfloatEIGHTY_PERCENTY=4f*TWENTY_PERCENT; publicstaticbooleanuseCollisionDetection=true;publicstaticbooleanshowRadar=true;publicstaticbooleanshowZoomBar=true;` TAG在输出到LogCat时用作字符串常量。FORMAT用于在雷达上显示当前半径时格式化输出。ZOOMBAR_BACKGROUND_COLOR是我们用来允许用户改变半径的滑块背景颜色的ARGB_8888定义。END_TEXT是我们需要在雷达上显示的格式化文本。END_TEXT_COLOR是END_TEXT的颜色。wakeLock、camScreen、myZoomBar、endLabel、zoomLayout、augmentedView是我们需要的类的对象。它们目前都被赋予了一个null值,将在本章稍后进行初始化。MAX_ZOOM是我们的半径极限,以公里为单位。接下来的四个浮动是这个最大半径限制的不同百分比。useCollisionDetection是一个标志,允许我们启用或禁用标记的碰撞检测。showRadar是一个标志切换雷达的可见性。showZoomBar执行相同的切换,除了对控制半径的seekbar执行此操作。 现在我们来看看这个活动的onCreate()方法: 清单9-11。【增龄性onCreate() `@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); camScreen=newCameraSurface(this);setContentView(camScreen); augmentedView=newAugmentedView(this);augmentedView.setOnTouchListener(this);LayoutParamsaugLayout=newLayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);addContentView(augmentedView,augLayout); zoomLayout=newLinearLayout(this); zoomLayout.setVisibility((showZoomBar)LinearLayout.VISIBLE:LinearLayout.GONE);zoomLayout.setOrientation(LinearLayout.VERTICAL);zoomLayout.setPadding(5,5,5,5);zoomLayout.setBackgroundColor(ZOOMBAR_BACKGROUND_COLOR); endLabel=newTextView(this);endLabel.setText(END_TEXT);endLabel.setTextColor(END_TEXT_COLOR);LinearLayout.LayoutParamszoomTextParams=newLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);zoomLayout.addView(endLabel,zoomTextParams); myZoomBar=newVerticalSeekBar(this);myZoomBar.setMax(100);myZoomBar.setProgress(50);myZoomBar.setOnSeekBarChangeListener(myZoomBarOnSeekBarChangeListener);LinearLayout.LayoutParamszoomBarParams=newLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.FILL_PARENT);zoomBarParams.gravity=Gravity.CENTER_HORIZONTAL;zoomLayout.addView(myZoomBar,zoomBarParams); FrameLayout.LayoutParamsframeLayoutParams=newFrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.FILL_PARENT,Gravity.RIGHT);addContentView(zoomLayout,frameLayoutParams); updateDataOnZoom();PowerManagerpm=(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() wakeLock.acquire();} @OverridepublicvoidonPause(){super.onPause(); wakeLock.release();}` 现在我们有了我们的onSensorChanged()方法: 清单9-13。onSensorChanged() `@OverridepublicvoidonSensorChanged(SensorEventevt){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 `privateOnSeekBarChangeListenermyZoomBarOnSeekBarChangeListener=newOnSeekBarChangeListener(){publicvoidonProgressChanged(SeekBarseekBar,intprogress,booleanfromUser){updateDataOnZoom();camScreen.invalidate();} publicvoidonStartTrackingTouch(SeekBarseekBar){//Notused} publicvoidonStopTrackingTouch(SeekBarseekBar){updateDataOnZoom();camScreen.invalidate();}};` 在SeekBar被更改(onProgressChanged())时调用的方法中,在它停止被更改(onStopTrackingTouch())后,我们通过调用updateDataOnZoom()来更新我们的数据,然后使相机预览无效。 现在我们有一个方法来计算我们的应用的缩放级别。我们称之为缩放级别,但它实际上是我们显示数据的半径。只是缩放级别比半径级别更容易记忆和说出。 清单9-15。计算缩放级别 `privatestaticfloatcalcZoomLevel(){intmyZoomLevel=myZoomBar.getProgress();floatout=0; floatpercent=0;if(myZoomLevel<=25){percent=myZoomLevel/25f;out=ONE_PERCENTpercent;}elseif(myZoomLevel>25&&myZoomLevel<=50){percent=(myZoomLevel-25f)/25f;out=ONE_PERCENT+(TEN_PERCENTpercent);}elseif(myZoomLevel>50&&myZoomLevel<=75){percent=(myZoomLevel-50f)/25f;out=TEN_PERCENT+(TWENTY_PERCENTpercent);}else{percent=(myZoomLevel-75f)/25f;out=TWENTY_PERCENT+(EIGHTY_PERCENTYpercent);}returnout;}` 我们首先将搜索栏上的进度作为缩放级别。然后,我们创建floatout来存储最终结果,创建floatpercent来存储百分比。然后,我们有一些简单的数学来确定使用的半径。我们使用这些类型的计算,因为它允许用户设置半径,甚至以米为单位。你越往上走,半径设置就越不精确。最后,我们返回out作为当前缩放级别。 我们现在有了处理触摸和更新数据的方法。 清单9-16。更新数据和处理触摸 `protectedvoidupdateDataOnZoom(){floatzoomLevel=calcZoomLevel();ARData.setRadius(zoomLevel);ARData.setZoomLevel(FORMAT.format(zoomLevel));ARData.setZoomProgress(myZoomBar.getProgress());} publicbooleanonTouch(Viewview,MotionEventme){for(Markermarker:ARData.getMarkers()){if(marker.handleClick(me.getX(),me.getY())){if(me.getAction()==MotionEvent.ACTION_UP)markerTouched(marker);returntrue;}}returnsuper.onTouchEvent(me);}; protectedvoidmarkerTouched(Markermarker){Log.w(TAG,"markerTouched()notimplemented.");}}` 在updateDataOnZoom()中,我们获得缩放级别,将半径设置为新的缩放级别,并更新缩放级别和搜索栏进度的文本,所有这些都在ARData中完成。在onTouch()中,我们检查一个标记是否被触摸,并从那里调用markerTouched()。在那之后,markerTouched()向LogCat发出一条消息,说我们目前在markerTouched()中什么都不做。 这就把我们带到了AugmentedActivity的结尾。现在我们需要写我们最后的活动类:MainActivity。 publicclassMainActivityextendsAugmentedActivity{privatestaticfinalStringTAG="MainActivity";privatestaticfinalStringlocale="en";privatestaticfinalBlockingQueue 与我们之前编写的课程的目的相同。locale字符串将两个字母代码中的区域设置存储为英语。您也可以使用Locale.getDefault().getLanguage()来获取区域设置,但最好将其保留为“en”,因为我们使用它来获取附近的Twitter和Wikipedia数据,并且我们的数据源可能不支持所有语言。为了简化我们的线程,我们使用了作为BlockingQueue实例的queue变量。exeService是一个ThreadPoolExecutor,它的工作队列是queue。最后,我们有一个叫做sources的Map,它将存储数据源。 现在让我们看看这个类的onCreate()和onStart()方法: 清单9-18。onCreate()和onStart() `@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);LocalDataSourcelocalData=newLocalDataSource(this.getResources());ARData.addMarkers(localData.getMarkers()); NetworkDataSourcetwitter=newTwitterDataSource(this.getResources());sources.put("twitter",twitter);NetworkDataSourcewikipedia=newWikipediaDataSource(this.getResources());sources.put("wiki",wikipedia);} @OverridepublicvoidonStart(){super.onStart();Locationlast=ARData.getCurrentLocation();updateData(last.getLatitude(),last.getLongitude(),last.getAltitude());}` 在onCreate()中,我们首先创建一个LocalDataSource类的实例,并将其标记添加到ARData。然后我们为Twitter和Wikipedia创建一个NetworkDataSource,并将它们添加到sources地图中。在onStart()中,我们获得最后的位置数据,并用它更新我们的数据。 至此,我们现在可以进入代码的菜单部分了: 清单9-19。使用菜单 `@OverridepublicbooleanonCreateOptionsMenu(Menumenu){MenuInflaterinflater=getMenuInflater();inflater.inflate(R.menu.menu,menu);returntrue;} @OverridepublicbooleanonOptionsItemSelected(MenuItemitem){Log.v(TAG,"onOptionsItemSelected()item="+item);switch(item.getItemId()){caseR.id.showRadar:showRadar=!showRadar;item.setTitle(((showRadar)"Hide":"Show")+"Radar");break;caseR.id.showZoomBar:showZoomBar=!showZoomBar;item.setTitle(((showZoomBar)"Hide":"Show")+"ZoomBar"); zoomLayout.setVisibility((showZoomBar)LinearLayout.VISIBLE:LinearLayout.GONE);break;caseR.id.exit:finish();break;}returntrue;}` 这是标准的Android代码,我们以前已经使用过无数次了。我们简单地从XML菜单资源创建菜单,然后监听菜单上的点击。对于显示雷达/缩放栏选项,我们只需切换它们的显示,对于退出选项,我们退出。 现在让我们来看看位置更新和处理触摸。 清单9-20。位置改变和触摸输入 `@OverridepublicvoidonLocationChanged(Locationlocation){super.onLocationChanged(location); updateData(location.getLatitude(),location.getLongitude(),location.getAltitude());} @OverrideprotectedvoidmarkerTouched(Markermarker){Toastt=Toast.makeText(getApplicationContext(),marker.getName(),Toast.LENGTH_SHORT);t.setGravity(Gravity.CENTER,0,0);t.show();}` 当我们得到一个新的location对象时,我们用它来更新数据。我们覆盖了markerTouched()方法来显示一个包含被触摸的标记的详细信息的祝酒词。 现在让我们看看这个类对updateDataOnZoom()的实现: 清单9-21。updateDataOnZoom() @OverrideprotectedvoidupdateDataOnZoom(){super.updateDataOnZoom();Locationlast=ARData.getCurrentLocation();updateData(last.getLatitude(),last.getLongitude(),last.getAltitude());} 在updateDataOnZoom()的这个实现中,我们获取位置,然后调用updateData()并向其传递新的位置信息。 现在我们来看看updateData()方法: 清单9-22。updateData() `privatevoidupdateData(finaldoublelat,finaldoublelon,finaldoublealt){try{exeService.execute(newRunnable(){ publicvoidrun(){for(NetworkDataSourcesource:sources.values())download(source,lat,lon,alt);}});}catch(RejectedExecutionExceptionrej){Log.w(TAG,"NotrunningnewdownloadRunnable,queueisfull.");}catch(Exceptione){Log.e(TAG,"ExceptionrunningdownloadRunnable.",e);}}` 最后,我们有了download()方法: 清单9-23。下载() `privatestaticbooleandownload(NetworkDataSourcesource,doublelat,doublelon,doublealt){if(source==null)returnfalse; Stringurl=null;try{url=source.createRequestURL(lat,lon,alt,ARData.getRadius(),locale);}catch(NullPointerExceptione){returnfalse;} Listmarkers=null;try{markers=source.parse(url);}catch(NullPointerExceptione){returnfalse;} ARData.addMarkers(markers);returntrue;}}` 在download方法中,我们首先检查源是否是null。如果是,我们返回false。如果不是null,我们构造一个URL来获取数据。之后,我们解析来自URL的结果,并将获得的数据存储在列表标记中。该数据然后通过ARData.addMarkers添加到ARData。 为了获得数据,我们将创建五个类:基本的DataSource类,它将由LocalDataSource和NetworkDataSource扩展;TwitterDataSource和WikipediaDataSource将进一步延长NetworkDataSource。 先说DataSource。 DataSource是一个非常小的抽象类: 清单9-24。数据源 publicabstractclassDataSource{publicabstractList 这个类只有一个成员:。这个类是我们所有其他数据类的基础。 现在我们来看看LocalDataSource。 MainActivity使用LocalDataSource为ARData添加标记。班级很小。 清单9-25。本地数据源 `publicclassLocalDataSourceextendsDataSource{privateListcachedMarkers=newArrayList();privatestaticBitmapicon=null; publicLocalDataSource(Resourcesres){if(res==null)thrownewNullPointerException(); createIcon(res);} protectedvoidcreateIcon(Resourcesres){if(res==null)thrownewNullPointerException(); icon=BitmapFactory.decodeResource(res,R.drawable.ic_launcher);}publicListgetMarkers(){Markeratl=newIconMarker("ATL",39.931269,-75.051261,0,Color.DKGRAY,icon);cachedMarkers.add(atl); Markerhome=newMarker("MtLaurel",39.95,-74.9,0,Color.YELLOW);cachedMarkers.add(home); returncachedMarkers;}}` 这个类的构造函数接受一个Resource对象作为参数。然后它调用createIcon(),然后将我们应用的默认图标分配给icon位图。getMarkers(),嗯,拿马克笔。 这节课讲完了,我们来看看NetworkDataSource。 清单9-26。网络数据源 `publicabstractclassNetworkDataSourceextendsDataSource{protectedstaticfinalintMAX=1000;protectedstaticfinalintREAD_TIMEOUT=10000;protectedstaticfinalintCONNECT_TIMEOUT=10000; protectedListmarkersCache=null; publicabstractStringcreateRequestURL(doublelat,doublelon,doublealt,floatradius,Stringlocale); publicabstractListparse(JSONObjectroot);` MAX指定显示给用户的最大结果数。READ_TIMEOUT和CONNECT_TIMEOUT是连接的超时值,以毫秒为单位。markersCache是一个列表<标记>对象,我们将在这个类的后面使用。createRequestURL和parse是我们将在这个类的扩展中覆盖的存根。 现在让我们来看看getMarkers()和getHttpGETInputStream()方法: 清单9-27。getmarkers()和getHttpGETInputStream() `publicListgetMarkers(){returnmarkersCache;} protectedstaticInputStreamgetHttpGETInputStream(StringurlStr){if(urlStr==null)thrownewNullPointerException(); InputStreamis=null;URLConnectionconn=null; try{if(urlStr.startsWith("file://"))returnnewFileInputStream(urlStr.replace("file://","")); URLurl=newURL(urlStr);conn=url.openConnection();conn.setReadTimeout(READ_TIMEOUT);conn.setConnectTimeout(CONNECT_TIMEOUT); is=conn.getInputStream(); returnis;}catch(Exceptionex){try{is.close();}catch(Exceptione){//Ignore}try{if(conninstanceofHttpURLConnection)((HttpURLConnection)conn).disconnect();}catch(Exceptione){//Ignore}ex.printStackTrace();}returnnull;}` getMarkers()方法只是返回markersCache。getHttpGETInputStream()用于获取指定URL的InputStream,并作为String传递给它。 现在让我们来看看getHttpInputString()和parse()方法: 清单9-28。getHttpInputString()和parse() `protectedStringgetHttpInputString(InputStreamis){if(is==null)thrownewNullPointerException(); BufferedReaderreader=newBufferedReader(newInputStreamReader(is),8*1024);StringBuildersb=newStringBuilder(); try{Stringline;while((line=reader.readLine())!=null){sb.append(line+"\n");}}catch(IOExceptione){e.printStackTrace();}finally{try{is.close();}catch(IOExceptione){e.printStackTrace();}}returnsb.toString();} publicListparse(Stringurl){if(url==null)thrownewNullPointerException(); InputStreamstream=null;stream=getHttpGETInputStream(url);if(stream==null)thrownewNullPointerException(); Stringstring=null;string=getHttpInputString(stream);if(string==null)thrownewNullPointerException(); JSONObjectjson=null;try{json=newJSONObject(string);}catch(JSONExceptione){e.printStackTrace();}if(json==null)thrownewNullPointerException();returnparse(json);}}` 在getHttpInputString()中,我们获取InputStream的内容,并把它们放在一个String中。在parse()中,我们从数据源获取JSON对象,然后在其上调用另一个parse()方法。在这个类中,第二个parse()方法是一个存根,但是它在其他类中被覆盖和实现。 让我们写出扩展了NetworkDataSource:TwitterDataSource和WikipediaDataSource的两个类。 TwitterDataSource延伸NetworkDataSource。它负责从Twitter的服务器上获取附近推文的数据。TwitterDataSource只有两个全局变量。 字符串URL存储Twitter搜索URL的基础。我们将在createRequestURL()中动态构造参数。这个icon位图,现在是null,将在其中存储Twitter的标志。在显示推文时,我们会将这个标志显示为每个标记的图标。 现在让我们看看构造函数、createIcon()和createRequestURL()方法: 清单9-30。构造函数,createIcon(),和createRequestURL() `publicTwitterDataSource(Resourcesres){if(res==null)thrownewNullPointerException(); icon=BitmapFactory.decodeResource(res,R.drawable.twitter);} @OverridepublicStringcreateRequestURL(doublelat,doublelon,doublealt,floatradius,Stringlocale){returnURL+"geocode="+lat+"%2C"+lon+"%2C"+Math.max(radius,1.0)+"km";}` 在构造函数中,我们将一个Resource对象作为参数,然后将其传递给createIcon()。这和《》中的行为完全一样。在createIcon()中,我们再次做了和在NetworkDataSource中一样的事情,除了我们使用了不同的图标。这里我们将Twitterdrawable分配给了icon位图,而不是ic_launcher位图。在createRequestURL()中,我们为TwitterJSON搜索应用编程接口(API)制定了一个完整的请求URL。Twitter搜索API允许我们轻松匿名地搜索推文。我们在geocode参数中提供用户的位置,并从用户设置的范围中选择一个更大的半径限制,即一公里。 现在我们有两个parse()方法和一个processJSONObject()。 清单9-31。两个parse()方法和processJSONObject()方法 `@OverridepublicListparse(Stringurl){if(url==null)thrownewNullPointerException(); JSONObjectjson=null;try{json=newJSONObject(string);}catch(JSONExceptione){e.printStackTrace();}if(json==null)thrownewNullPointerException(); returnparse(json);} @OverridepublicListparse(JSONObjectroot){if(root==null)thrownewNullPointerException(); JSONObjectjo=null;JSONArraydataArray=null;Listmarkers=newArrayList(); try{if(root.has("results"))dataArray=root.getJSONArray("results");if(dataArray==null)returnmarkers;inttop=Math.min(MAX,dataArray.length());for(inti=0;i jo=dataArray.getJSONObject(i);Markerma=processJSONObject(jo);if(ma!=null)markers.add(ma);}}catch(JSONExceptione){e.printStackTrace();}returnmarkers;} privateMarkerprocessJSONObject(JSONObjectjo){if(jo==null)thrownewNullPointerException(); if(!jo.has("geo"))thrownewNullPointerException(); Markerma=null;try{Doublelat=null,lon=null; if(!jo.isNull("geo")){JSONObjectgeo=jo.getJSONObject("geo");JSONArraycoordinates=geo.getJSONArray("coordinates"); lat=Double.parseDouble(coordinates.getString(0)); lon=Double.parseDouble(coordinates.getString(1));}elseif(jo.has("location")){Patternpattern=Pattern.compile("\D*([0-9.]+),\s([0-9.]+)");Matchermatcher=pattern.matcher(jo.getString("location")); if(matcher.find()){lat=Double.parseDouble(matcher.group(1)); lon=Double.parseDouble(matcher.group(2));}}if(lat!=null){Stringuser=jo.getString("from_user"); ma=newIconMarker(user+":"+jo.getString("text"),lat,lon,0,Color.RED,icon);}}catch(Exceptione){e.printStackTrace();}returnma;}}` 第一个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。维基百科数据源 privatestaticBitmapicon=null; publicWikipediaDataSource(Resourcesres){if(res==null)thrownewNullPointerException(); icon=BitmapFactory.decodeResource(res,R.drawable.wikipedia);} @OverridepublicStringcreateRequestURL(doublelat,doublelon,doublealt,floatradius,Stringlocale){returnBASE_URL+"lat="+lat+"&lng="+lon+"&radius="+radius+"&maxRows=40"+"&lang="+locale; @OverridepublicListparse(JSONObjectroot){if(root==null)returnnull; try{if(root.has("geonames"))dataArray=root.getJSONArray("geonames");if(dataArray==null)returnmarkers;inttop=Math.min(MAX,dataArray.length());for(inti=0;i privateMarkerprocessJSONObject(JSONObjectjo){if(jo==null)returnnull; Markerma=null;if(jo.has("title")&&jo.has("lat")&&jo.has("lng")&&jo.has("elevation")){try{ma=newIconMarker(jo.getString("title"),jo.getDouble("lat"),jo.getDouble("lng"),jo.getDouble("elevation"),Color.WHITE,icon);}catch(JSONExceptione){e.printStackTrace();}}returnma;}}` 与Twitter不同,维基百科不提供官方搜索工具。然而,geonames.org为维基百科提供了基于JSON的搜索,我们将会使用它。与TwitterDataSource的下一个区别是图标。我们只是在创建图标时使用了不同的drawable。获取和解析JSONObject的基本代码是相同的;只是价值观不同。 我们现在将编写帮助我们在屏幕上和现实生活中定位标记的类。 这个类用来表示用户在真实世界中的三维位置。 清单9-33。PhysicalLocationUtility.java `publicclassPhysicalLocationUtility{privatedoublelatitude=0.0;privatedoublelongitude=0.0;privatedoublealtitude=0.0; privatestaticfloat[]x=newfloat[1];privatestaticdoubley=0.0d;privatestaticfloat[]z=newfloat[1]; publicPhysicalLocationUtility(){} publicPhysicalLocationUtility(PhysicalLocationUtilitypl){if(pl==null)thrownewNullPointerException(); set(pl.latitude,pl.longitude,pl.altitude);} publicvoidset(doublelatitude,doublelongitude,doublealtitude){this.latitude=latitude;this.longitude=longitude;this.altitude=altitude;} publicvoidsetLatitude(doublelatitude){this.latitude=latitude;} publicdoublegetLatitude(){returnlatitude;} publicvoidsetLongitude(doublelongitude){this.longitude=longitude;}publicdoublegetLongitude(){returnlongitude;} publicvoidsetAltitude(doublealtitude){this.altitude=altitude;} publicdoublegetAltitude(){returnaltitude;} publicstaticsynchronizedvoidconvLocationToVector(Locationorg,PhysicalLocationUtilitygp,Vectorv){if(orgnull||gpnull||v==null)thrownewNullPointerException("Location,PhysicalLocationUtility,andVectorcannotbeNULL."); Location.distanceBetween(org.getLatitude(),org.getLongitude(), gp.getLatitude(),org.getLongitude(),z); org.getLatitude(),gp.getLongitude(),x);y=gp.getAltitude()-org.getAltitude();if(org.getLatitude() v.set(x[0],(float)y,z[0]);} @OverridepublicStringtoString(){return"(lat="+latitude+",lng="+longitude+",alt="+altitude+")";}}` 顾名思义,前三个doubles分别用于存储latitude、longitude和altitude。x、y和z存储最终的三维位置数据。setLatitude()、setLongitude()和setAltitude()方法分别设置纬度、经度和高度。它们的get()对应物只是返回当前值。convLocationToVector()方法将位置转换为Vector。我们将在本章的后面写一个Vector类。toString()方法只是将纬度、经度和海拔高度编译成一个字符串,并将其返回给调用方法。 现在我们来看看ScreenPositionUtility。 ScreenPositionUtility用于显示雷达线。 清单9-34。ScreenPositionUtility.java `publicclassScreenPositionUtility{privatefloatx=0f;privatefloaty=0f; publicScreenPositionUtility(){set(0,0);} publicvoidset(floatx,floaty){this.x=x;this.y=y;} publicfloatgetX(){returnx;} publicvoidrotate(doublet){floatxp=(float)Math.cos(t)*x-(float)Math.sin(t)*y;floatyp=(float)Math.sin(t)*x+(float)Math.cos(t)*y; x=xp;y=yp;} publicvoidadd(floatx,floaty){this.x+=x;this.y+=y;} @OverridepublicStringtoString(){return"x="+x+"y="+y;}}` setX()和setY()方法分别为x和y变量设置浮点值。set()方法同时设置x和y的值。getX()和getY()方法只是返回x和y的值。rotate()方法围绕角度t旋转x和y值。add()方法将传递的值分别添加到x和y。最后,toString()方法返回一个字符串,其中包含了x和y的值。 现在我们来看看UI代码。 PaintableObject是用户界面所有自定义部分的基类。它的一些方法只是我们在它的子类中覆盖的存根。这个类包含了很多绘制特定对象的方法,比如线条、位图、点等。在给定的画布上。 清单9-35。PaintableObject.java `publicabstractclassPaintableObject{privatePaintpaint=newPaint(Paint.ANTI_ALIAS_FLAG); publicPaintableObject(){if(paint==null){paint=newPaint();paint.setTextSize(16);paint.setAntiAlias(true);paint.setColor(Color.BLUE);paint.setStyle(Paint.Style.STROKE);}}publicabstractfloatgetWidth(); publicabstractfloatgetHeight(); publicabstractvoidpaint(Canvascanvas); publicvoidsetFill(booleanfill){if(fill)paint.setStyle(Paint.Style.FILL);elsepaint.setStyle(Paint.Style.STROKE);} publicvoidsetColor(intc){paint.setColor(c);} publicvoidsetStrokeWidth(floatw){paint.setStrokeWidth(w);} publicfloatgetTextWidth(Stringtxt){if(txt==null)thrownewNullPointerException();returnpaint.measureText(txt);} publicfloatgetTextAsc(){return-paint.ascent();} publicfloatgetTextDesc(){returnpaint.descent();} publicvoidsetFontSize(floatsize){paint.setTextSize(size);} publicvoidpaintLine(Canvascanvas,floatx1,floaty1,floatx2,floaty2){if(canvas==null)thrownewNullPointerException(); canvas.drawLine(x1,y1,x2,y2,paint);} publicvoidpaintRect(Canvascanvas,floatx,floaty,floatwidth,floatheight){if(canvas==null)thrownewNullPointerException();canvas.drawRect(x,y,x+width,y+height,paint);} publicvoidpaintRoundedRect(Canvascanvas,floatx,floaty,floatwidth,floatheight){if(canvas==null)thrownewNullPointerException(); RectFrect=newRectF(x,y,x+width,y+height);canvas.drawRoundRect(rect,15F,15F,paint);} publicvoidpaintBitmap(Canvascanvas,Bitmapbitmap,Rectsrc,Rectdst){if(canvasnull||bitmapnull)thrownewNullPointerException(); canvas.drawBitmap(bitmap,src,dst,paint);} publicvoidpaintBitmap(Canvascanvas,Bitmapbitmap,floatleft,floattop){if(canvasnull||bitmapnull)thrownewNullPointerException(); canvas.drawBitmap(bitmap,left,top,paint);} publicvoidpaintCircle(Canvascanvas,floatx,floaty,floatradius){if(canvas==null)thrownewNullPointerException(); canvas.drawCircle(x,y,radius,paint);} publicvoidpaintText(Canvascanvas,floatx,floaty,Stringtext){if(canvasnull||textnull)thrownewNullPointerException(); canvas.drawText(text,x,y,paint);} publicvoidpaintObj(Canvascanvas,PaintableObjectobj,floatx,floaty,floatrotation,floatscale){if(canvasnull||objnull)thrownewNullPointerException(); 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();}publicvoidpaintPath(Canvascanvas,Pathpath,floatx,floaty,floatwidth,floatheight,floatrotation,floatscale){if(canvasnull||pathnull)thrownewNullPointerException(); 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.FILL或Paint.Style.STROKE。setColor()方法将颜料的颜色设置为与它作为参数的整数相对应的颜色。setStrokeWidth()允许我们设置笔画的宽度。getTextWidth()返回作为参数的文本宽度。getTextAsc()和getTextDesc()分别返回文本的上升和下降。setFontSize()允许我们设置绘画的字体大小。名称前带有paint前缀的所有其余方法绘制在所提供的canvas上的paint之后写入的对象。例如,paintLine()用提供的坐标在提供的canvas上画一条线。 现在让我们看看扩展了PaintableObject的类集。 类允许我们画出一个盒子的轮廓。这是一个简单的类,并且不是很大。 清单9-36。PaintableBox.java `publicclassPaintableBoxextendsPaintableObject{privatefloatwidth=0,height=0;privateintborderColor=Color.rgb(255,255,255);privateintbackgroundColor=Color.argb(128,0,0,0); publicPaintableBox(floatwidth,floatheight){this(width,height,Color.rgb(255,255,255),Color.argb(128,0,0,0));} publicPaintableBox(floatwidth,floatheight,intborderColor,intbgColor){set(width,height,borderColor,bgColor);} publicvoidset(floatwidth,floatheight){set(width,height,borderColor,backgroundColor);} publicvoidset(floatwidth,floatheight,intborderColor,intbgColor){this.width=width;this.height=height;this.borderColor=borderColor;this.backgroundColor=bgColor;} @Overridepublicvoidpaint(Canvascanvas){if(canvas==null)thrownewNullPointerException(); setFill(true);setColor(backgroundColor);paintRect(canvas,0,0,width,height); setFill(false);setColor(borderColor);paintRect(canvas,0,0,width,height);} @OverridepublicfloatgetWidth(){returnwidth;} @OverridepublicfloatgetHeight(){returnheight;}}` 这个类有两个构造函数,其中一个调用另一个。原因是其中一个构造函数只允许你设置盒子的宽度和高度,而第二个构造函数允许你设置它的颜色。当调用第一个时,它使用提供的宽度和高度来调用具有默认颜色的第二个。然后,第二个构造函数调用第二个set()方法来设置这些值。paint()方法只是在指定的canvas上画一个盒子。getWidth()和getHeight()只返回盒子的宽度和高度。 现在我们来看看PaintableBoxedText。 在画布上绘制文本,周围有一个方框。 清单9-37。PaintableBox.java `publicclassPaintableBoxedTextextendsPaintableObject{privatefloatwidth=0,height=0;privatefloatareaWidth=0,areaHeight=0;privateArrayListlineList=null;privateString[]lines=null;privatefloat[]lineWidths=null;privatefloatlineHeight=0;privatefloatmaxLineWidth=0;privatefloatpad=0; privateStringtxt=null;privatefloatfontSize=12;privateintborderColor=Color.rgb(255,255,255);privateintbackgroundColor=Color.argb(160,0,0,0);privateinttextColor=Color.rgb(255,255,255); publicPaintableBoxedText(StringtxtInit,floatfontSizeInit,floatmaxWidth){this(txtInit,fontSizeInit,maxWidth,Color.rgb(255,255,255),Color.argb(128,0,0,0),Color.rgb(255,255,255));} publicPaintableBoxedText(StringtxtInit,floatfontSizeInit,floatmaxWidth,intborderColor,intbgColor,inttextColor){set(txtInit,fontSizeInit,maxWidth,borderColor,bgColor,textColor);}publicvoidset(StringtxtInit,floatfontSizeInit,floatmaxWidth,intborderColor,intbgColor,inttextColor){if(txtInit==null)thrownewNullPointerException(); this.borderColor=borderColor;this.backgroundColor=bgColor;this.textColor=textColor;this.pad=getTextAsc(); set(txtInit,fontSizeInit,maxWidth);} publicvoidset(StringtxtInit,floatfontSizeInit,floatmaxWidth){if(txtInit==null)thrownewNullPointerException(); try{prepTxt(txtInit,fontSizeInit,maxWidth);}catch(Exceptionex){ex.printStackTrace();prepTxt("TEXTPARSEERROR",12,200);}} privatevoidprepTxt(StringtxtInit,floatfontSizeInit,floatmaxWidth){if(txtInit==null)thrownewNullPointerException(); setFontSize(fontSizeInit); txt=txtInit;fontSize=fontSizeInit;areaWidth=maxWidth-pad;lineHeight=getTextAsc()+getTextDesc(); if(lineList==null)lineList=newArrayList();elselineList.clear(); BreakIteratorboundary=BreakIterator.getWordInstance();boundary.setText(txt); intstart=boundary.first();intend=boundary.next();intprevEnd=start;while(end!=BreakIterator.DONE){Stringline=txt.substring(start,end);StringprevLine=txt.substring(start,prevEnd);floatlineWidth=getTextWidth(line); if(lineWidth>areaWidth){if(prevLine.length()>0)lineList.add(prevLine);start=prevEnd;} prevEnd=end;end=boundary.next();}Stringline=txt.substring(start,prevEnd);lineList.add(line); if(linesnull||lines.length!=lineList.size())lines=newString[lineList.size()];if(lineWidthsnull||lineWidths.length!=lineList.size())lineWidths=newfloat[lineList.size()];lineList.toArray(lines); maxLineWidth=0;for(inti=0;i width=areaWidth+pad*2;height=areaHeight+pad*2;} setFontSize(fontSize); setFill(true);setColor(backgroundColor);paintRoundedRect(canvas,0,0,width,height); setFill(false);setColor(borderColor);paintRoundedRect(canvas,0,0,width,height); for(inti=0;i 同样,有两个构造函数做与PaintableBox中相同的事情。与PaintableBox的第一个主要区别是新方法prepTxt()。prepTxt()通过将文本切割成不同大小的行以适合文本框来准备文本,而不是让一个长字符串愉快地离开文本框并溢出到外面。然后,paint()方法首先绘制基本的方框,然后使用一个for循环将每一行添加到其中。 现在我们来看看PaintableCircle。 PaintableCircle允许我们在提供的Canvas上画一个圆。这是一个简单的类,代码很简单: 清单9-38。PaintableCircle.java `publicclassPaintableCircleextendsPaintableObject{privateintcolor=0;privatefloatradius=0;privatebooleanfill=false; publicPaintableCircle(intcolor,floatradius,booleanfill){set(color,radius,fill);} publicvoidset(intcolor,floatradius,booleanfill){this.color=color;this.radius=radius;this.fill=fill;} @Overridepublicvoidpaint(Canvascanvas){if(canvas==null)thrownewNullPointerException();setFill(fill);setColor(color);paintCircle(canvas,0,0,radius);} @OverridepublicfloatgetWidth(){returnradius*2;} @OverridepublicfloatgetHeight(){returnradius*2;}}` 这一次,只有一个构造函数允许我们设置圆的半径和颜色。也只有一个set()方法,由构造函数调用。方法在给定的canvas上用指定的属性画一个圆。getWidth()和getHeight()方法返回直径,因为这是一个圆。 现在我们来看看PaintableGps。 PaintableGps很像PaintableCircle,除了它还允许我们设置正在绘制的圆的笔画宽度。 清单9-39。PaintableGps.java `publicclassPaintableGpsextendsPaintableObject{privatefloatradius=0;privatefloatstrokeWidth=0;privatebooleanfill=false;privateintcolor=0; publicPaintableGps(floatradius,floatstrokeWidth,booleanfill,intcolor){set(radius,strokeWidth,fill,color);} publicvoidset(floatradius,floatstrokeWidth,booleanfill,intcolor){this.radius=radius;this.strokeWidth=strokeWidth;this.fill=fill;this.color=color;} setStrokeWidth(strokeWidth);setFill(fill);setColor(color);paintCircle(canvas,0,0,radius);} 同样,只有一个构造函数,它调用set()方法来设置所画圆的颜色、大小和笔画的宽度。像往常一样,paint()方法在提供的Canvas上绘制具有指定属性的圆。再次,getWidth()和getHeight()返回圆的直径。 现在我们来看看PaintableIcon。 我们使用PaintableIcon来绘制Twitter和维基百科的图标。 清单9-40。PaintableIcon.java `publicClassPaintableIconExtendsPaintableObject{privateBitmapbitmap=null; publicPaintableIcon(Bitmapbitmap,intwidth,intheight){set(bitmap,width,height);} publicvoidset(Bitmapbitmap,intwidth,intheight){if(bitmap==null)thrownewNullPointerException(); this.bitmap=Bitmap.createScaledBitmap(bitmap,width,height,true);} @Overridepublicvoidpaint(Canvascanvas){if(canvasnull||bitmapnull)thrownewNullPointerException(); paintBitmap(canvas,bitmap,-(bitmap.getWidth()/2),-(bitmap.getHeight()/2));} @OverridepublicfloatgetWidth(){returnbitmap.getWidth();} @OverridepublicfloatgetHeight(){returnbitmap.getHeight();}}` 构造函数获取要绘制的位图,以及要绘制的宽度和高度,然后调用set()方法来设置它们。在set()方法中,Bitmap被缩放到指定的大小。然后,paint()方法将它绘制到提供的canvas上。getWidth()和getHeight()方法返回的是我们最后绘制的位图的宽度和高度,而不是构造函数中传递的位图。 现在我们来看看如何创建一个绘制线条的类。 PaintableLine允许我们在提供的Canvas上用指定的颜色画一条线。 清单9-41。PaintableLine.java `publicclassPaintableLineextendsPaintableObject{privateintcolor=0;privatefloatx=0;privatefloaty=0; publicPaintableLine(intcolor,floatx,floaty){set(color,x,y);} publicvoidset(intcolor,floatx,floaty){this.color=color;this.x=x;this.y=y;} setFill(false);setColor(color);paintLine(canvas,0,0,x,y);} @OverridepublicfloatgetWidth(){returnx;} @OverridepublicfloatgetHeight(){returny;}}` 构造函数获取线条的颜色以及X和Y点,并将它们传递给set()进行设置。paint()方法分别使用PaintableObject.getWidth()和getHeight()返回x和y在Canvas上绘制。 现在我们来看看PaintablePoint。 PaintablePoint用于在单个画布上绘制单个点。它被用来制造我们的雷达。 清单9-42。绘画点 `publicclassPaintablePointextendsPaintableObject{privatestaticintwidth=2;privatestaticintheight=2;privateintcolor=0;privatebooleanfill=false; publicPaintablePoint(intcolor,booleanfill){set(color,fill);} publicvoidset(intcolor,booleanfill){this.color=color;this.fill=fill;} setFill(fill);setColor(color);paintRect(canvas,-1,-1,width,height);} 正在画的点实际上不是一个点;它只是一个非常小的长方形。构造函数接受颜色并通过set()方法设置它们,然后paint()绘制一个非常小的矩形。getWidth()和getHeight()简单地返回正在绘制的矩形的宽度和高度。 现在我们来看看PaintablePosition。 PaintablePosition扩展了PaintableObject,增加了旋转和缩放被画物体的能力。 清单9-43。可涂装位置 `publicclassPaintablePositionextendsPaintableObject{privatefloatwidth=0,height=0;privatefloatobjX=0,objY=0,objRotation=0,objScale=0;privatePaintableObjectobj=null; publicPaintablePosition(PaintableObjectdrawObj,floatx,floaty,floatrotation,floatscale){set(drawObj,x,y,rotation,scale);}publicvoidset(PaintableObjectdrawObj,floatx,floaty,floatrotation,floatscale){if(drawObj==null)thrownewNullPointerException(); this.obj=drawObj;this.objX=x;this.objY=y;this.objRotation=rotation;this.objScale=scale;this.width=obj.getWidth();this.height=obj.getHeight();} publicvoidmove(floatx,floaty){objX=x;objY=y;} publicfloatgetObjectsX(){returnobjX;} publicfloatgetObjectsY(){returnobjY;} @Overridepublicvoidpaint(Canvascanvas){if(canvasnull||objnull)thrownewNullPointerException(); paintObj(canvas,obj,objX,objY,objRotation,objScale);} @OverridepublicfloatgetHeight(){returnheight;} @OverridepublicStringtoString(){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。漆面雷达点 `publicclassPaintableRadarPointsextendsPaintableObject{privatefinalfloat[]locationArray=newfloat[3];privatePaintablePointpaintablePoint=null;privatePaintablePositionpointContainer=null; floatrange=ARData.getRadius()*1000;floatscale=range/Radar.RADIUS;for(Markerpm:ARData.getMarkers()){pm.getLocation().get(locationArray);floatx=locationArray[0]/scale;floaty=locationArray[2]/scale;if((xx+yy)<(Radar.RADIUS*Radar.RADIUS)){if(paintablePoint==null)paintablePoint=newPaintablePoint(pm.getColor(),true);elsepaintablePoint.set(pm.getColor(),true); if(pointContainer==null)pointContainer=newPaintablePosition(paintablePoint,(x+Radar.RADIUS-1),(y+Radar.RADIUS-1),0,1);elsepointContainer.set(paintablePoint,(x+Radar.RADIUS-1),(y+Radar.RADIUS-1),0,1); pointContainer.paint(canvas);}}} @OverridepublicfloatgetWidth(){returnRadar.RADIUS*2;} @OverridepublicfloatgetHeight(){returnRadar.RADIUS*2;}}` 在这个类中,没有构造函数。相反,只有paint()、getWidth()和getHeight()方法存在。getWidth()和getHeight()返回我们绘制的代表标记的点的直径。在paint()方法中,我们使用一个for循环在雷达上为每个标记画一个点。 现在我们来看看PaintableText。 PaintableText是PaintableObject的扩展,用于绘制文本。我们用它在雷达上显示文本: 清单9-45。可绘画文字 `publicclassPaintableTextextendsPaintableObject{privatestaticfinalfloatWIDTH_PAD=4;privatestaticfinalfloatHEIGHT_PAD=2; privateStringtext=null;privateintcolor=0;privateintsize=0;privatefloatwidth=0;privatefloatheight=0;privatebooleanbg=false;publicPaintableText(Stringtext,intcolor,intsize,booleanpaintBackground){set(text,color,size,paintBackground);} publicvoidset(Stringtext,intcolor,intsize,booleanpaintBackground){if(text==null)thrownewNullPointerException(); 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;} @Overridepublicvoidpaint(Canvascanvas){if(canvasnull||textnull)thrownewNullPointerException(); 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);} 这个类的构造函数将文本、颜色、大小和背景色作为参数,然后将所有这些传递给要设置的set()方法。paint()方法绘制文本及其背景颜色。getWidth()和getHeight()再次返回宽度和高度。 现在,在我们进入主要的UI组件之前,比如Radar类、Marker类和IconMarker类,我们需要创建一些工具类。 在我们的应用中,我们有一些工具类。这些类处理向量和矩阵函数,实现低通滤波器,还处理诸如音调之类的值的计算。 首先,让我们看看全局变量和构造函数。 清单9-46。向量的构造函数和全局变量 `publicclassVector{privatefinalfloat[]matrixArray=newfloat[9]; privatevolatilefloatx=0f;privatevolatilefloaty=0f;privatevolatilefloatz=0f; publicVector(){this(0,0,0);} publicVector(floatx,floaty,floatz){set(x,y,z);}` matrixArray是我们在本课后面的prod()方法中使用的数组。浮点值x、y和z是任意给定Vector的三个值。第一个构造函数创建一个Vector,将x、y和z都设置为零。第二个构造函数将x、y和z设置为提供的值。 现在让我们看看这个类的getter和setter方法: 清单9-47。get()和set() `publicsynchronizedfloatgetX(){returnx;}publicsynchronizedvoidsetX(floatx){this.x=x;} publicsynchronizedfloatgetY(){returny;} publicsynchronizedvoidsetY(floaty){this.y=y;} publicsynchronizedfloatgetZ(){returnz;} publicsynchronizedvoidsetZ(floatz){this.z=z;} publicsynchronizedvoidget(float[]array){if(array==null||array.length!=3)thrownewIllegalArgumentException("get()arraymustbenon-NULLandsizeof3"); array[0]=this.x;array[1]=this.y;array[2]=this.z;} publicvoidset(Vectorv){if(v==null)return; set(v.x,v.y,v.z);} publicvoidset(float[]array){if(array==null||array.length!=3)thrownewIllegalArgumentException("get()arraymustbenon-NULLandsizeof3"); set(array[0],array[1],array[2]);} publicsynchronizedvoidset(floatx,floaty,floatz){this.x=x;this.y=y;this.z=z;}` getX()、getY()、getZ()分别向调用方法返回x、y和z的值,而它们的set()对应方更新所述值。采用浮点数组作为参数的get()方法将一次性给出x、y和z的值。在剩下的三个set()方法中,有两个最终调用了set(floatx,floaty,floatz),它设置了x、y和z的值。调用这个方法的另外两个set()方法只是允许我们使用数组或预先存在的向量来设置值,而不是总是必须为x、y和z传递单独的值。 现在我们将进入这节课的数学部分: 清单9-48。向量类的数学部分 `@Overridepublicsynchronizedbooleanequals(Objectobj){if(obj==null)returnfalse; Vectorv=(Vector)obj;return(v.x==this.x&&v.y==this.y&&v.z==this.z);} publicsynchronizedvoidadd(floatx,floaty,floatz){this.x+=x;this.y+=y;this.z+=z;} publicvoidadd(Vectorv){if(v==null)return; add(v.x,v.y,v.z);} publicvoidsub(Vectorv){if(v==null)return; add(-v.x,-v.y,-v.z);} publicsynchronizedvoidmult(floats){this.x*=s;this.y*=s;this.z*=s;}publicsynchronizedvoiddivide(floats){this.x/=s;this.y/=s;this.z/=s;} publicsynchronizedfloatlength(){return(float)Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z);} publicvoidnorm(){divide(length());} publicsynchronizedvoidcross(Vectoru,Vectorv){if(vnull||unull)return; floatx=u.y*v.z-u.z*v.y;floaty=u.z*v.x-u.x*v.z;floatz=u.x*v.y-u.y*v.x;this.x=x;this.y=y;this.z=z;} publicsynchronizedvoidprod(Matrixm){if(m==null)return; m.get(matrixArray);floatxTemp=matrixArray[0]*this.x+matrixArray[1]*this.y+matrixArray[2]*this.z;floatyTemp=matrixArray[3]*this.x+matrixArray[4]*this.y+matrixArray[5]*this.z;floatzTemp=matrixArray[6]*this.x+matrixArray[7]*this.y+matrixArray[8]*this.z; this.x=xTemp;this.y=yTemp;this.z=zTemp;} @OverridepublicsynchronizedStringtoString(){return"x="+this.x+",y="+this.y+",z="+this.z;}}` equals()方法将vector与给定的对象进行比较,看它们是否相等。add()和sub()方法分别在向量中加入和减去参数。mult()方法将所有值乘以传递的浮点数。divide()方法将所有的值除以传递的浮点数。length()方法返回向量的长度。norm()方法将向量除以其长度。方法将两个向量交叉相乘。prod()方法将向量与提供的矩阵相乘。toString()以人类可读的格式返回x、y和z的值。 接下来我们上Utilities课。 Utilities类包含一个单独的getAngle()方法,我们在计算像PitchAzimuthCalculator中的音高这样的东西时用它来获得角度。其中的数学是简单的三角学。 清单9-49。公用事业 `publicabstractclassUtilities{ privateUtilities(){} publicstaticfinalfloatgetAngle(floatcenter_x,floatcenter_y,floatpost_x,floatpost_y){floattmpv_x=post_x-center_x;floattmpv_y=post_y-center_y;floatd=(float)Math.sqrt(tmpv_x*tmpv_x+tmpv_y*tmpv_y);floatcos=tmpv_x/d;floatangle=(float)Math.toDegrees(Math.acos(cos)); angle=(tmpv_y<0)angle*-1:angle; returnangle;}}` 现在我们来看看那个PitchAzimuthCalculator。 PitchAzimuthCalculator是一个类,用于在给定矩阵的情况下计算俯仰和方位角: 清单9-50。【俯仰角计算器】 `publicclassPitchAzimuthCalculator{privatestaticfinalVectorlooking=newVector();privatestaticfinalfloat[]lookingArray=newfloat[3]; privatestaticvolatilefloatazimuth=0; privatestaticvolatilefloatpitch=0; privatePitchAzimuthCalculator(){}; publicstaticsynchronizedfloatgetAzimuth(){returnPitchAzimuthCalculator.azimuth;}publicstaticsynchronizedfloatgetPitch(){returnPitchAzimuthCalculator.pitch;} publicstaticsynchronizedvoidcalcPitchBearing(MatrixrotationM){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。lowpassfilter `publicclassLowPassFilter{ privatestaticfinalfloatALPHA_DEFAULT=0.333f;privatestaticfinalfloatALPHA_STEADY=0.001f;privatestaticfinalfloatALPHA_START_MOVING=0.6f;privatestaticfinalfloatALPHA_MOVING=0.9f; privateLowPassFilter(){} publicstaticfloat[]filter(floatlow,floathigh,float[]current,float[]previous){if(currentnull||previousnull)thrownewNullPointerException("Inputandprevfloatarraysmustbenon-NULL");if(current.length!=previous.length)thrownewIllegalArgumentException("Inputandprevmustbethesamelength"); floatalpha=computeAlpha(low,high,current,previous); for(inti=0;i privatestaticfinalfloatcomputeAlpha(floatlow,floathigh,float[]current,float[]previous){if(previous.length!=3||current.length!=3)returnALPHA_DEFAULT; floatx1=current[0],y1=current[1],z1=current[2]; floatx2=previous[0],y2=previous[1],z2=previous[2]; floatdistance=(float)(Math.sqrt(Math.pow((double)(x2-x1),2d)+Math.pow((double)(y2-y1),2d)+Math.pow((double)(z2-z1),2d))); if(distance 现在我们来看看Matrix类。 我们将像对待Vector类一样分解它: 清单9-52。矩阵的getters和setters,以及构造函数 `publicclassMatrix{privatestaticfinalMatrixtmp=newMatrix(); privatevolatilefloata1=0f,a2=0f,a3=0f;privatevolatilefloatb1=0f,b2=0f,b3=0f;privatevolatilefloatc1=0f,c2=0f,c3=0f; publicMatrix(){} publicsynchronizedfloatgetA1(){returna1;}publicsynchronizedvoidsetA1(floata1){this.a1=a1;} publicsynchronizedfloatgetA2(){returna2;}publicsynchronizedvoidsetA2(floata2){this.a2=a2;} publicsynchronizedfloatgetA3(){returna3;}publicsynchronizedvoidsetA3(floata3){this.a3=a3;} publicsynchronizedfloatgetB1(){returnb1;}publicsynchronizedvoidsetB1(floatb1){this.b1=b1;} publicsynchronizedfloatgetB2(){returnb2;}publicsynchronizedvoidsetB2(floatb2){this.b2=b2;} publicsynchronizedfloatgetB3(){returnb3;}publicsynchronizedvoidsetB3(floatb3){this.b3=b3;} publicsynchronizedfloatgetC1(){returnc1;}publicsynchronizedvoidsetC1(floatc1){this.c1=c1;} publicsynchronizedfloatgetC2(){returnc2;}publicsynchronizedvoidsetC2(floatc2){this.c2=c2;} publicsynchronizedfloatgetC3(){returnc3;}publicsynchronizedvoidsetC3(floatc3){this.c3=c3;} publicsynchronizedvoidget(float[]array){if(array==null||array.length!=9)thrownewIllegalArgumentException("get()arraymustbenon-NULLandsizeof9"); 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;} publicvoidset(Matrixm){if(m==null)thrownewNullPointerException(); set(m.a1,m.a2,m.a3,m.b1,m.b2,m.b3,m.c1,m.c2,m.c3);} publicsynchronizedvoidset(floata1,floata2,floata3,floatb1,floatb2,floatb3,floatc1,floatc2,floatc3){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。矩阵的数学函数 `publicvoidtoIdentity(){set(1,0,0,0,1,0,0,0,1);} publicsynchronizedvoidadj(){floata11=this.a1;floata12=this.a2;floata13=this.a3; floata21=this.b1;floata22=this.b2;floata23=this.b3; floata31=this.c1;floata32=this.c2;floata33=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);} publicvoidinvert(){floatdet=this.det(); adj();mult(1/det);} publicsynchronizedvoidtranspose(){floata11=this.a1;floata12=this.a2;floata13=this.a3; 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;}privatefloatdet2x2(floata,floatb,floatc,floatd){return(a*d)-(b*c);} publicsynchronizedfloatdet(){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 publicsynchronizedvoidmult(floatc){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;} publicsynchronizedvoidprod(Matrixn){if(n==null)thrownewNullPointerException(); 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);} @OverridepublicsynchronizedStringtoString(){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()将矩阵乘以提供的Matrix。toString()以人类可读的字符串格式返回所有值。 现在让我们为主要组件编写类,即Radar、Marker和IconMarker类。 这些类负责我们应用的主要组件,比如雷达和标记。标记组件分为两个类,IconMarker和Marker。 Radar类用于绘制我们的雷达,以及它的所有元素,如代表标记的线和点。 我们将从查看全局变量和构造函数开始: 清单9-54。雷达类的变量和构造函数 `publicclassRadar{publicstaticfinalfloatRADIUS=48;privatestaticfinalintLINE_COLOR=Color.argb(150,0,0,220);privatestaticfinalfloatPAD_X=10;privatestaticfinalfloatPAD_Y=20;privatestaticfinalintRADAR_COLOR=Color.argb(100,0,0,200);privatestaticfinalintTEXT_COLOR=Color.rgb(255,255,255);privatestaticfinalintTEXT_SIZE=12; privatestaticScreenPositionUtilityleftRadarLine=null;privatestaticScreenPositionUtilityrightRadarLine=null;privatestaticPaintablePositionleftLineContainer=null;privatestaticPaintablePositionrightLineContainer=null;privatestaticPaintablePositioncircleContainer=null; privatestaticPaintableRadarPointsradarPoints=null;privatestaticPaintablePositionpointsContainer=null;privatestaticPaintableTextpaintableText=null;privatestaticPaintablePositionpaintedContainer=null; publicRadar(){if(leftRadarLinenull)leftRadarLine=newScreenPositionUtility();if(rightRadarLinenull)rightRadarLine=newScreenPositionUtility();}` 前七个常量设置雷达的颜色、半径、文本颜色和填充的值。剩下的变量被创建为各种类的null对象,稍后将对其进行初始化。在构造函数中,我们检查是否已经创建了显示当前正在查看的区域的雷达线。如果它们还没有被创建,它们将被创建为ScreenPositionUtility的新实例。 现在让我们添加该类的实际方法: 清单9-55。雷达的方法 `publicvoiddraw(Canvascanvas){if(canvas==null)thrownewNullPointerException(); PitchAzimuthCalculator.calcPitchBearing(ARData.getRotationMatrix());ARData.setAzimuth(PitchAzimuthCalculator.getAzimuth());ARData.setPitch(PitchAzimuthCalculator.getPitch()); drawRadarCircle(canvas);drawRadarPoints(canvas);drawRadarLines(canvas);drawRadarText(canvas);} privatevoiddrawRadarCircle(Canvascanvas){if(canvas==null)thrownewNullPointerException(); if(circleContainer==null){PaintableCirclepaintableCircle=newPaintableCircle(RADAR_COLOR,RADIUS,true);circleContainer=newPaintablePosition(paintableCircle,PAD_X+RADIUS,PAD_Y+RADIUS,0,1);}circleContainer.paint(canvas);} privatevoiddrawRadarPoints(Canvascanvas){if(canvas==null)thrownewNullPointerException(); if(radarPoints==null)radarPoints=newPaintableRadarPoints(); if(pointsContainer==null)pointsContainer=newPaintablePosition(radarPoints,PAD_X,PAD_Y,-ARData.getAzimuth(),1);elsepointsContainer.set(radarPoints,PAD_X,PAD_Y,-ARData.getAzimuth(),1); pointsContainer.paint(canvas);} privatevoiddrawRadarLines(Canvascanvas){if(canvas==null)thrownewNullPointerException(); if(leftLineContainer==null){leftRadarLine.set(0,-RADIUS);leftRadarLine.rotate(-CameraModel.DEFAULT_VIEW_ANGLE/2);leftRadarLine.add(PAD_X+RADIUS,PAD_Y+RADIUS); floatleftX=leftRadarLine.getX()-(PAD_X+RADIUS);floatleftY=leftRadarLine.getY()-(PAD_Y+RADIUS);PaintableLineleftLine=newPaintableLine(LINE_COLOR,leftX,leftY);leftLineContainer=newPaintablePosition(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); floatrightX=rightRadarLine.getX()-(PAD_X+RADIUS);floatrightY=rightRadarLine.getY()-(PAD_Y+RADIUS);PaintableLinerightLine=newPaintableLine(LINE_COLOR,rightX,rightY);rightLineContainer=newPaintablePosition(rightLine,PAD_X+RADIUS,PAD_Y+RADIUS,0,1);}rightLineContainer.paint(canvas);} privatevoiddrawRadarText(Canvascanvas){if(canvas==null)thrownewNullPointerException();intrange=(int)(ARData.getAzimuth()/(360f/16f));StringdirTxt="";if(range==15||range==0)dirTxt="N";elseif(range==1||range==2)dirTxt="NE";elseif(range==3||range==4)dirTxt="E";elseif(range==5||range==6)dirTxt="SE";elseif(range==7||range==8)dirTxt="S";elseif(range==9||range==10)dirTxt="SW";elseif(range==11||range==12)dirTxt="W";elseif(range==13||range==14)dirTxt="NW";intbearing=(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);} privatevoidradarText(Canvascanvas,Stringtxt,floatx,floaty,booleanbg){if(canvasnull||txtnull)thrownewNullPointerException(); if(paintableText==null)paintableText=newPaintableText(txt,TEXT_COLOR,TEXT_SIZE,bg);elsepaintableText.set(txt,TEXT_COLOR,TEXT_SIZE,bg); if(paintedContainer==null)paintedContainer=newPaintablePosition(paintableText,x,y,0,1);elsepaintedContainer.set(paintableText,x,y,0,1); paintedContainer.paint(canvas);} privatestaticStringformatDist(floatmeters){if(meters<1000){return((int)meters)+"m";}elseif(meters<10000){returnformatDec(meters/1000f,1)+"km";}else{return((int)(meters/1000f))+"km";}} privatestaticStringformatDec(floatval,intdec){intfactor=(int)Math.pow(10,dec); intfront=(int)(val);intback=(int)Math.abs(val*(factor))%factor; returnfront+"."+back;}}` draw()方法通过获取俯仰和方位角,然后按照所需的顺序调用其他绘制方法来开始这个过程。drawRadarCircle()简单的给雷达画个基圆。drawRadarPoints()在雷达圈上画出所有表示标记的点。drawRadarLines()绘制两条线,显示哪些标记当前位于摄像机的可视区域内。drawRadarText()调用radarText()对文本进行格式化,然后将其绘制到雷达上。 这就把我们带到了Radar课的结尾。现在我们来看看Marker类。 我们将照常从全局变量开始: 清单9-56。全局变量 `publicclassMarkerimplementsComparable{privatestaticfinalDecimalFormatDECIMAL_FORMAT=newDecimalFormat("@#"); privatestaticfinalVectorsymbolVector=newVector(0,0,0);privatestaticfinalVectortextVector=newVector(0,1,0); privatefinalVectorscreenPositionVector=newVector();privatefinalVectortmpSymbolVector=newVector();privatefinalVectortmpVector=newVector();privatefinalVectortmpTextVector=newVector();privatefinalfloat[]distanceArray=newfloat[1];privatefinalfloat[]locationArray=newfloat[3];privatefinalfloat[]screenPositionArray=newfloat[3]; privatefloatinitialY=0.0f; privatevolatilestaticCameraModelcam=null; privatevolatilePaintableBoxedTexttextBox=null;privatevolatilePaintablePositiontextContainer=null; protectedfinalfloat[]symbolArray=newfloat[3];protectedfinalfloat[]textArray=newfloat[3]; protectedvolatilePaintableObjectgpsSymbol=null;protectedvolatilePaintablePositionsymbolContainer=null;protectedStringname=null;protectedvolatilePhysicalLocationUtilityphysicalLocation=newPhysicalLocationUtility();protectedvolatiledoubledistance=0.0;protectedvolatilebooleanisOnRadar=false;protectedvolatilebooleanisInView=false;protectedfinalVectorsymbolXyzRelativeToCameraView=newVector();protectedfinalVectortextXyzRelativeToCameraView=newVector();protectedfinalVectorlocationXyzRelativeToPhysicalLocation=newVector();protectedintcolor=Color.WHITE; privatestaticbooleandebugTouchZone=false;privatestaticPaintableBoxtouchBox=null;privatestaticPaintablePositiontouchPosition=null; privatestaticbooleandebugCollisionZone=false;privatestaticPaintableBoxcollisionBox=null;privatestaticPaintablePositioncollisionPosition=null;` DECIMAL_FORMAT用于格式化我们在雷达上显示的距离。 symbolVector和textVector用于查找文本和标记符号的位置。当使用旋转矩阵找到文本及其伴随符号的位置时,使用symbolVector和textVector。 接下来的四个向量和三个浮点数组用于定位和绘制标记符号及其附带的文本。 initialY是每个标记的初始Y轴位置。一开始它被设置为0,但是它的值对于每个标记都是不同的。 textBox、textContainer、cam分别是PaintableBoxedText、PaintablePostion和CameraModel的实例。我们还没有写CameraModel;我们将在完成应用的所有UI部分后这样做。 symbolArray和textArray在本课程稍后绘制符号和文本时使用。 debugTouchZone和debugCollisionZone是我们用来启用和禁用两个区域的调试的两个标志。touchBox、touchPosition、collisionBox、collisionPosition用于绘制不透明框,帮助我们调试app。 图9-1显示了在debugTouchZone和debugCollisionZone没有设置为false的情况下app运行;在图9.2中,它们被设置为true。 图9-2。禁用触摸碰撞调试运行的app 图9-3。启用触摸碰撞调试运行的app 现在我们来看看构造函数和set()方法: 清单9-57。构造函数和set()方法 `publicMarker(Stringname,doublelatitude,doublelongitude,doublealtitude,intcolor){set(name,latitude,longitude,altitude,color);}publicsynchronizedvoidset(Stringname,doublelatitude,doublelongitude,doublealtitude,intcolor){if(name==null)thrownewNullPointerException(); 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中描述和给出的变量。它还处理摄像机的一些基本初始化和屏幕上标记的位置。 现在让我们看看Marker类的各种get()方法: 清单9-58。get()方法 `publicsynchronizedStringgetName(){returnthis.name;} publicsynchronizedintgetColor(){returnthis.color;} publicsynchronizeddoublegetDistance(){returnthis.distance;} publicsynchronizedfloatgetInitialY(){returnthis.initialY;} publicsynchronizedbooleanisOnRadar(){returnthis.isOnRadar;} publicsynchronizedbooleanisInView(){returnthis.isInView;} publicsynchronizedVectorgetScreenPosition(){symbolXyzRelativeToCameraView.get(symbolArray);textXyzRelativeToCameraView.get(textArray);floatx=(symbolArray[0]+textArray[0])/2;floaty=(symbolArray[1]+textArray[1])/2;floatz=(symbolArray[2]+textArray[2])/2; if(textBox!=null)y+=(textBox.getHeight()/2); screenPositionVector.set(x,y,z);returnscreenPositionVector;} publicsynchronizedVectorgetLocation(){returnthis.locationXyzRelativeToPhysicalLocation;}publicsynchronizedfloatgetHeight(){if(symbolContainernull||textContainernull)return0f;returnsymbolContainer.getHeight()+textContainer.getHeight();} publicsynchronizedfloatgetWidth(){if(symbolContainernull||textContainernull)return0f;floatw1=textContainer.getWidth();floatw2=symbolContainer.getWidth();return(w1>w2)w1:w2;}` getName(),getColor(),getDistance(),getLocation(),isInView(),isOnRadar()和getInitialY()只是返回名称所指示的值。getHeight()将文本和符号图像的高度相加并返回。getWidth()检查并返回文本和符号图像之间的较大宽度。getScreenPosition()通过使用文本和符号的位置,计算标记在屏幕上相对于摄像机视图的位置。 现在让我们来看看update()和populateMatrices()方法: 清单9-59。更新()和填充矩阵() `publicsynchronizedvoidupdate(Canvascanvas,floataddX,floataddY){if(canvas==null)thrownewNullPointerException(); if(cam==null)cam=newCameraModel(canvas.getWidth(),canvas.getHeight(),true);cam.set(canvas.getWidth(),canvas.getHeight(),false);cam.setViewAngle(CameraModel.DEFAULT_VIEW_ANGLE);populateMatrices(cam,addX,addY);updateRadar();updateView();} privatesynchronizedvoidpopulateMatrices(CameraModelcam,floataddX,floataddY){if(cam==null)thrownewNullPointerException(); 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中得到的旋转矩阵,一个我们将在本章稍后编写的类。然后,我们使用这些数据将文本和符号投射到摄像机视图上。 现在让我们看看由update()调用的updateView()和updateRadar()方法。 清单9-60。更新雷达()和更新视图() `privatesynchronizedvoidupdateRadar(){isOnRadar=false; floatrange=ARData.getRadius()*1000;floatscale=range/Radar.RADIUS;locationXyzRelativeToPhysicalLocation.get(locationArray);floatx=locationArray[0]/scale;floaty=locationArray[2]/scale;//z==ySwitchedonpurposesymbolXyzRelativeToCameraView.get(symbolArray);if((symbolArray[2]<-1f)&&((xx+yy)<(Radar.RADIUS*Radar.RADIUS))){isOnRadar=true;}} privatesynchronizedvoidupdateView(){isInView=false; symbolXyzRelativeToCameraView.get(symbolArray);floatx1=symbolArray[0]+(getWidth()/2);floaty1=symbolArray[1]+(getHeight()/2);floatx2=symbolArray[0]-(getWidth()/2);floaty2=symbolArray[1]-(getHeight()/2);if(x1>=-1&&x2<=(cam.getWidth())&&y1>=-1&&y2<=(cam.getHeight())){isInView=true;}}` updateRadar()用于更新雷达上标记的位置。如果发现标记的位置应该显示在雷达上,它的OnRadar被更新为true。updateView()与updateRadar()做同样的事情,除了它检查标记当前是否可见。 现在让我们来看看calcRelativePosition()和updateDistance()方法: 清单9-61。calcRelativePosition()和updateDistance() `publicsynchronizedvoidcalcRelativePosition(Locationlocation){if(location==null)thrownewNullPointerException(); updateDistance(location); if(physicalLocation.getAltitude()==0.0)physicalLocation.setAltitude(location.getAltitude()); PhysicalLocationUtility.convLocationToVector(location,physicalLocation,locationXyzRelativeToPhysicalLocation);this.initialY=locationXyzRelativeToPhysicalLocation.getY();updateRadar();} privatesynchronizedvoidupdateDistance(Locationlocation){if(location==null)thrownewNullPointerException(); Location.distanceBetween(physicalLocation.getLatitude(),physicalLocation.getLongitude(),location.getLatitude(),location.getLongitude(),distanceArray);distance=distanceArray[0];}` 在calcRelativePosition()中,我们使用作为参数接收的位置计算新的相对位置。我们检查在physicalLocation中是否有标记的有效高度;如果没有,我们将它设置为用户当前的海拔高度。然后,我们使用这些数据创建一个向量,用这个向量更新initialY变量,最后我们调用updateRadar()用新的相对位置更新雷达。updateDistance()简单地计算标记的物理位置和用户位置之间的新距离。 现在让我们来看看我们是如何处理点击的,看看这个标记是否与另一个标记重叠: 清单9-62。检查点击和重叠 `publicsynchronizedbooleanhandleClick(floatx,floaty){if(!isOnRadar||!isInView)returnfalse;returnisPointOnMarker(x,y,this);} publicsynchronizedbooleanisMarkerOnMarker(Markermarker){returnisMarkerOnMarker(marker,true);} privatesynchronizedbooleanisMarkerOnMarker(Markermarker,booleanreflect){marker.getScreenPosition().get(screenPositionArray);floatx=screenPositionArray[0];floaty=screenPositionArray[1];booleanmiddleOfMarker=isPointOnMarker(x,y,this);if(middleOfMarker)returntrue; floathalfWidth=marker.getWidth()/2;floathalfHeight=marker.getHeight()/2; floatx1=x-halfWidth;floaty1=y-halfHeight;booleanupperLeftOfMarker=isPointOnMarker(x1,y1,this);if(upperLeftOfMarker)returntrue; floatx2=x+halfWidth;floaty2=y1;booleanupperRightOfMarker=isPointOnMarker(x2,y2,this);if(upperRightOfMarker)returntrue; floatx3=x1;floaty3=y+halfHeight;booleanlowerLeftOfMarker=isPointOnMarker(x3,y3,this);if(lowerLeftOfMarker)returntrue; floatx4=x2;floaty4=y3;booleanlowerRightOfMarker=isPointOnMarker(x4,y4,this);if(lowerRightOfMarker)returntrue; return(reflect)marker.isMarkerOnMarker(this,false):false;} privatesynchronizedbooleanisPointOnMarker(floatx,floaty,Markermarker){marker.getScreenPosition().get(screenPositionArray);floatmyX=screenPositionArray[0];floatmyY=screenPositionArray[1];floatadjWidth=marker.getWidth()/2;floatadjHeight=marker.getHeight()/2; floatx1=myX-adjWidth;floaty1=myY-adjHeight;floatx2=myX+adjWidth;floaty2=myY+adjHeight; if(x>=x1&&x<=x2&&y>=y1&&y<=y2)returntrue; returnfalse;}` handleClick()将点击的X和Y点作为参数。如果标记不在雷达上,也不在视图中,它返回false。否则,它会通过调用isPointOnMarker()返回找到的任何内容。 第一个isMarkerOnMarker()简单地返回第二个isMarkerOnMarker()方法发现的任何内容。第二个isMarkerOnMarker()方法包含我们用来确定作为参数接收的标记是否与当前标记重叠的所有代码。我们检查所有四个角和标记中心的重叠。如果其中任何一个是true,我们可以有把握地说标记是重叠的。 isPointOnMarker()检查传递的X和Y坐标是否位于标记上。 现在我们来看看draw()方法: 清单9-63。当先() `publicsynchronizedvoiddraw(Canvascanvas){if(canvas==null)thrownewNullPointerException(); if(!isOnRadar||!isInView)return; if(debugTouchZone)drawTouchZone(canvas);if(debugCollisionZone)drawCollisionZone(canvas);drawIcon(canvas);drawText(canvas);}` draw()方法很简单。它决定是否应该显示标记。如果要显示它,它会绘制它,并在需要时绘制调试框。这就是它的全部功能。 绘图的主要工作是在drawTouchZone()、drawCollisionZone()、drawIcon()和drawText()方法中完成的,我们现在来看看: 清单9-64。drawTouchZone()、drawCollisionZone()、drawIcon()和drawText() `protectedsynchronizedvoiddrawCollisionZone(Canvascanvas){if(canvas==null)thrownewNullPointerException(); getScreenPosition().get(screenPositionArray);floatx=screenPositionArray[0];floaty=screenPositionArray[1]; floatwidth=getWidth();floatheight=getHeight();floathalfWidth=width/2;floathalfHeight=height/2; floatx1=x-halfWidth;floaty1=y-halfHeight;floatx2=x+halfWidth;floaty2=y1; floatx3=x1;floaty3=y+halfHeight; floatx4=x2;floaty4=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=newPaintableBox(width,height,Color.WHITE,Color.RED);elsecollisionBox.set(width,height); floatcurrentAngle=Utilities.getAngle(symbolArray[0],symbolArray[1],textArray[0],textArray[1])+90; if(collisionPosition==null)collisionPosition=newPaintablePosition(collisionBox,x1,y1,currentAngle,1);elsecollisionPosition.set(collisionBox,x1,y1,currentAngle,1);collisionPosition.paint(canvas);} protectedsynchronizedvoiddrawTouchZone(Canvascanvas){if(canvas==null)thrownewNullPointerException(); if(gpsSymbol==null)return; symbolXyzRelativeToCameraView.get(symbolArray);textXyzRelativeToCameraView.get(textArray);floatx1=symbolArray[0];floaty1=symbolArray[1];floatx2=textArray[0];floaty2=textArray[1];floatwidth=getWidth();floatheight=getHeight();floatadjX=(x1+x2)/2;floatadjY=(y1+y2)/2;floatcurrentAngle=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=newPaintableBox(width,height,Color.WHITE,Color.GREEN);elsetouchBox.set(width,height);if(touchPositionnull)touchPosition=newPaintablePosition(touchBox,adjX,adjY,currentAngle,1);elsetouchPosition.set(touchBox,adjX,adjY,currentAngle,1);touchPosition.paint(canvas);} protectedsynchronizedvoiddrawIcon(Canvascanvas){if(canvas==null)thrownewNullPointerException(); if(gpsSymbol==null)gpsSymbol=newPaintableGps(36,36,true,getColor()); textXyzRelativeToCameraView.get(textArray);symbolXyzRelativeToCameraView.get(symbolArray); floatcurrentAngle=Utilities.getAngle(symbolArray[0],symbolArray[1],textArray[0],textArray[1]);floatangle=currentAngle+90; if(symbolContainer==null)symbolContainer=newPaintablePosition(gpsSymbol,symbolArray[0],symbolArray[1],angle,1);elsesymbolContainer.set(gpsSymbol,symbolArray[0],symbolArray[1],angle,1); symbolContainer.paint(canvas);} protectedsynchronizedvoiddrawText(Canvascanvas){if(canvas==null)thrownewNullPointerException(); StringtextStr=null;if(distance<1000.0){textStr=name+"("+DECIMAL_FORMAT.format(distance)+"m)";}else{doubled=distance/1000.0;textStr=name+"("+DECIMAL_FORMAT.format(d)+"km)";} floatmaxHeight=Math.round(canvas.getHeight()/10f)+1;if(textBox==null)textBox=newPaintableBoxedText(textStr,Math.round(maxHeight/2f)+1,300);elsetextBox.set(textStr,Math.round(maxHeight/2f)+1,300); floatcurrentAngle=Utilities.getAngle(symbolArray[0],symbolArray[1],textArray[0],textArray[1]);floatangle=currentAngle+90;floatx=textArray[0]-(textBox.getWidth()/2);floaty=textArray[1]+maxHeight; if(textContainer==null)textContainer=newPaintablePosition(textBox,x,y,angle,1);elsetextContainer.set(textBox,x,y,angle,1);textContainer.paint(canvas);}` 现在让我们看看最后两个方法,compareTo()和equals(): 清单9-65。compareTo()和equals() `publicsynchronizedintcompareTo(Markeranother){if(another==null)thrownewNullPointerException(); returnname.compareTo(another.getName());} @Overridepublicsynchronizedbooleanequals(Objectmarker){if(markernull||namenull)thrownewNullPointerException(); returnname.equals(((Marker)marker).getName());}}` compareTo()使用标准Java字符串函数比较两个标记的名称。equals()使用标准的Java字符串函数来检查一个标记与另一个标记的名称。 这将我们带到了Marker.java文件的末尾。现在我们来看看它唯一的子类,IconMarker.java。 将位图绘制为标记的图标,而不是保留默认设置。是Marker.java的延伸。 清单9-66。图标标记 `publicclassIconMarkerextendsMarker{privateBitmapbitmap=null; publicIconMarker(Stringname,doublelatitude,doublelongitude,doublealtitude,intcolor,Bitmapbitmap){super(name,latitude,longitude,altitude,color);this.bitmap=bitmap;} @OverridepublicvoiddrawIcon(Canvascanvas){if(canvasnull||bitmapnull)thrownewNullPointerException(); if(gpsSymbol==null)gpsSymbol=newPaintableIcon(bitmap,96,96); symbolContainer.paint(canvas);}}` 构造函数接受对Marker.java的super()调用所需的所有参数,以及一个标记位图的额外参数。drawIcon()然后使用来自Marker.java的数据绘制我们在构造函数中收到的位图,作为这个标记的图标。 这就把我们带到了UI组件的结尾。现在让我们来看看VerticalSeekBar.java,我们对AndroidSeekBar的定制扩展。 我们在应用中定制了一个标准的Android小部件。我们扩展了SeekBar来创建VerticalSeekBar。 VerticalSeekBar是Android的SeekBar实现的扩展。我们的附加代码允许它垂直工作,而不是水平工作。应用中使用的zoomBar是这个类的一个实例。 清单9-67。VerticalSeekBar.java `publicclassVerticalSeekBarextendsSeekBar{ publicVerticalSeekBar(Contextcontext){super(context);} publicVerticalSeekBar(Contextcontext,AttributeSetattrs,intdefStyle){super(context,attrs,defStyle);} publicVerticalSeekBar(Contextcontext,AttributeSetattrs){super(context,attrs);} @OverrideprotectedvoidonSizeChanged(intw,inth,intoldw,intoldh){super.onSizeChanged(h,w,oldh,oldw);} @OverrideprotectedsynchronizedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){super.onMeasure(heightMeasureSpec,widthMeasureSpec);setMeasuredDimension(getMeasuredHeight(),getMeasuredWidth());} @OverrideprotectedvoidonDraw(Canvasc){c.rotate(-90);c.translate(-getHeight(),0); super.onDraw(c);} @OverridepublicbooleanonTouchEvent(MotionEventevent){if(!isEnabled()){returnfalse;} switch(event.getAction()){caseMotionEvent.ACTION_DOWN:caseMotionEvent.ACTION_MOVE:caseMotionEvent.ACTION_UP:setProgress(getMax()-(int)(getMax()*event.getY()/getHeight()));onSizeChanged(getWidth(),getHeight(),0,0);break; caseMotionEvent.ACTION_CANCEL:break;}returntrue;}}` 我们的三个构造函数用于将它绑定到父类SeekBar。onSizeChanged()也可以追溯到SeekBar类。onMeasure()执行一个super()调用,并使用Android的View类提供的方法设置测量的高度和宽度。实际的修改是在onDraw()中完成的,在将画布传递给SeekBar之前,我们将画布旋转90度,这样绘图是垂直完成的,而不是水平完成的。在onTouchEvent()中,我们调用setProgress()和onSizeChanged()来允许我们旋转SeekBar以正常工作。 现在让我们来看看控制摄像机所需的三个类。 与任何AR应用一样,我们也必须在这个应用中使用摄像头。由于这个应用的性质,相机控制已经放在三个类中,我们现在将通过。 清单9-68。变量和构造函数 `publicclassCameraSurfaceextendsSurfaceViewimplementsSurfaceHolder.Callback{privatestaticSurfaceHolderholder=null;privatestaticCameracamera=null; publicCameraSurface(Contextcontext){super(context); try{holder=getHolder();holder.addCallback(this);holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);}catch(Exceptionex){ex.printStackTrace();}}` 现在我们来看看surfaceCreated()方法: 清单9-69。surfaceCreated() `publicvoidsurfaceCreated(SurfaceHolderholder){try{if(camera!=null){try{camera.stopPreview();}catch(Exceptionex){ex.printStackTrace();}try{camera.release();}catch(Exceptionex){ex.printStackTrace();}camera=null;} camera=Camera.open();camera.setPreviewDisplay(holder);}catch(Exceptionex){try{if(camera!=null){try{camera.stopPreview();}catch(Exceptionex1){ex.printStackTrace();}try{camera.release();}catch(Exceptionex2){ex.printStackTrace();}camera=null;}}catch(Exceptionex3){ex.printStackTrace();}}}` 我们使用surfaceCreated()来创建camera对象,如果它已经存在或者我们遇到了问题,也可以释放它。 现在让我们继续进行surfaceDestroyed()。 清单9-70。【地表摧毁】( publicvoidsurfaceDestroyed(SurfaceHolderholder){try{if(camera!=null){try{camera.stopPreview();}catch(Exceptionex){ex.printStackTrace();}try{camera.release();}catch(Exceptionex){ex.printStackTrace();}camera=null;}}catch(Exceptionex){ex.printStackTrace();}} 这是简单的代码,与我们的其他示例应用非常相似。我们停止使用相机并释放它,以便我们自己的或另一个第三方或系统应用可以访问它。 事不宜迟,我们来看看这个类的最后一个方法,surfaceChanged(): 清单9-71。surfaceChanged() publicvoidsurfaceChanged(SurfaceHolderholder,intformat,intw,inth){try{Camera.Parametersparameters=camera.getParameters();try{List floatff=(float)w/h; floatbff=0;intbestw=0;intbesth=0;Iterator while(itr.hasNext()){Camera.Sizeelement=itr.next();floatcff=(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(Exceptionex){parameters.setPreviewSize(480,320);} camera.setParameters(parameters);camera.startPreview();}catch(Exceptionex){ex.printStackTrace();}}}` 现在让我们继续剩下的两个类:CameraCompatibility和CameraModel。 允许我们的应用保持与所有版本Android的兼容性,并避开旧版本API的限制。它改编自Mixare项目,类似于Vector类。 清单9-72。【相机兼容性】 `publicclassCameraCompatibility{privatestaticMethodgetSupportedPreviewSizes=null;privatestaticMethodmDefaultDisplay_getRotation=null; static{initCompatibility();}; privatestaticvoidinitCompatibility(){try{getSupportedPreviewSizes=Camera.Parameters.class.getMethod("getSupportedPreviewSizes",newClass[]{});mDefaultDisplay_getRotation=Display.class.getMethod("getRotation",newClass[]{});}catch(NoSuchMethodExceptionnsme){}} publicstaticintgetRotation(Activityactivity){intresult=1;try{Displaydisplay=((WindowManager)activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();ObjectretObj=mDefaultDisplay_getRotation.invoke(display);if(retObj!=null)result=(Integer)retObj;}catch(Exceptionex){ex.printStackTrace();}returnresult;} publicstaticList try{ObjectretObj=getSupportedPreviewSizes.invoke(params);if(retObj!=null){retList=(List 在低于Android2.0的设备上失败,这允许我们优雅地将相机设置为480x320的默认预览大小。getRotation()允许我们检索设备的旋转。getSupportedPreviewSizes()返回设备上可用的预览尺寸列表。 现在我们来看看CameraModel,相机类的最后一个,也是这个app的倒数第二个类。 CameraModel代表摄像机及其视图,也允许我们投射点。这是另一个由Mixare改编的类。 清单9-73。相机模型 `publicclassCameraModel{privatestaticfinalfloat[]tmp1=newfloat[3];privatestaticfinalfloat[]tmp2=newfloat[3]; privateintwidth=0;privateintheight=0;privatefloatdistance=0F; publicstaticfinalfloatDEFAULT_VIEW_ANGLE=(float)Math.toRadians(45); publicCameraModel(intwidth,intheight,booleaninit){set(width,height,init);} publicvoidset(intwidth,intheight,booleaninit){this.width=width;this.height=height;} publicintgetWidth(){returnwidth;} publicintgetHeight(){returnheight;} publicvoidsetViewAngle(floatviewAngle){this.distance=(this.width/2)/(float)Math.tan(viewAngle/2);} publicvoidprojectPoint(VectororgPoint,VectorprjPoint,floataddX,floataddY){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作为全局控制和存储类;它存储整个应用中使用的数据,这些数据对于应用的运行至关重要。它使我们更容易将所有这些数据存储在一个地方,而不是分散在各处。 清单9-74。ardata的全局变量 `publicabstractclassARData{privatestaticfinalStringTAG="ARData";privatestaticfinalMap publicstaticfinalLocationhardFix=newLocation("ATL");static{hardFix.setLatitude(0);hardFix.setLongitude(0);hardFix.setAltitude(1);} privatestaticfinalObjectradiusLock=newObject();privatestaticfloatradius=newFloat(20);privatestaticStringzoomLevel=newString();privatestaticfinalObjectzoomProgressLock=newObject();privatestaticintzoomProgress=0;privatestaticLocationcurrentLocation=hardFix;privatestaticMatrixrotationMatrix=newMatrix();privatestaticfinalObjectazimuthLock=newObject();privatestaticfloatazimuth=0;privatestaticfinalObjectpitchLock=newObject();privatestaticfloatpitch=0;privatestaticfinalObjectrollLock=newObject();privatestaticfloatroll=0;` TAG是在LogCat中显示消息时使用的字符串。markerList是标记及其名称的散列表。cache是,嗯,一个贮藏处。dirty用于判断状态是否为脏。locationArray是位置数据的数组。hardFix是默认位置,与我们拥有的ATL标记相同。radius是雷达半径;zoomProgress是我们应用中的缩放进度。pitch、azimuth和roll分别保存俯仰、方位角和滚动值。前面名称中添加了Lock的变量是这些变量的同步块的锁对象。zoomLevel是目前的变焦水平。currentLocation存储当前位置,默认设置为hardFix。最后,rotationMatrix存储旋转矩阵。 现在让我们看看这个类的各种getter和setter方法: 清单9-75。ARData的获取器和设置器 publicstaticvoidsetZoomLevel(StringzoomLevel){`if(zoomLevel==null)thrownewNullPointerException(); synchronized(ARData.zoomLevel){ARData.zoomLevel=zoomLevel;}} publicstaticvoidsetZoomProgress(intzoomProgress){synchronized(ARData.zoomProgressLock){if(ARData.zoomProgress!=zoomProgress){ARData.zoomProgress=zoomProgress;if(dirty.compareAndSet(false,true)){Log.v(TAG,"SettingDIRTYflag!");cache.clear();}}}} publicstaticvoidsetRadius(floatradius){synchronized(ARData.radiusLock){ARData.radius=radius;}} publicstaticfloatgetRadius(){synchronized(ARData.radiusLock){returnARData.radius;}} publicstaticvoidsetCurrentLocation(LocationcurrentLocation){if(currentLocation==null)thrownewNullPointerException(); Log.d(TAG,"currentlocation.location="+currentLocation.toString());synchronized(currentLocation){ARData.currentLocation=currentLocation;}onLocationChanged(currentLocation);} publicstaticLocationgetCurrentLocation(){synchronized(ARData.currentLocation){returnARData.currentLocation;}} publicstaticvoidsetRotationMatrix(MatrixrotationMatrix){synchronized(ARData.rotationMatrix){ARData.rotationMatrix=rotationMatrix;}} publicstaticMatrixgetRotationMatrix(){synchronized(ARData.rotationMatrix){returnrotationMatrix;}} publicstaticListgetMarkers(){if(dirty.compareAndSet(true,false)){Log.v(TAG,"DIRTYflagfound,resettingallmarkerheightstozero.");for(Markerma:markerList.values()){ma.getLocation().get(locationArray);locationArray[1]=ma.getInitialY();ma.getLocation().set(locationArray);} Log.v(TAG,"Populatingthecache.");Listcopy=newArrayList();copy.addAll(markerList.values());Collections.sort(copy,comparator);cache.clear();cache.addAll(copy);}returnCollections.unmodifiableList(cache);} publicstaticvoidsetAzimuth(floatazimuth){synchronized(azimuthLock){ARData.azimuth=azimuth;}} publicstaticfloatgetAzimuth(){synchronized(azimuthLock){returnARData.azimuth;}} publicstaticvoidsetPitch(floatpitch){synchronized(pitchLock){ARData.pitch=pitch;}} publicstaticfloatgetPitch(){synchronized(pitchLock){returnARData.pitch;}} publicstaticvoidsetRoll(floatroll){synchronized(rollLock){ARData.roll=roll;}} publicstaticfloatgetRoll(){synchronized(rollLock){returnARData.roll;}}` 所有方法都只是简单地设置或获取它们名称中提到的变量,使用同步块来确保数据不会被应用的两个不同部分同时修改。在getMarkers()方法中,我们迭代标记以返回所有标记。现在我们来看看这个类的最后几个方法。 清单9-76。addMarkers()、comparator和onLocationChanged() `privatestaticfinalComparatorcomparator=newComparator(){publicintcompare(Markerarg0,Markerarg1){returnDouble.compare(arg0.getDistance(),arg1.getDistance());}}; publicstaticvoidaddMarkers(Collectionmarkers){if(markers==null)thrownewNullPointerException(); if(markers.size()<=0)return; Log.d(TAG,"Newmarkers,updatingmarkers.newmarkers="+markers.toString());for(Markermarker:markers){if(!markerList.containsKey(marker.getName())){marker.calcRelativePosition(ARData.getCurrentLocation());markerList.put(marker.getName(),marker);}} if(dirty.compareAndSet(false,true)){Log.v(TAG,"SettingDIRTYflag!");cache.clear();}} privatestaticvoidonLocationChanged(Locationlocation){Log.d(TAG,"Newlocation,updatingmarkers.location="+location.toString());for(Markerma:markerList.values()){ma.calcRelativePosition(location);} if(dirty.compareAndSet(false,true)){Log.v(TAG,"SettingDIRTYflag!");cache.clear();}}}` comparator用于比较一个标记到另一个标记的距离。addMarkers()用于从传递的集合中添加新的标记。onLocationChanged()用于更新标记相对于新位置的相对位置。 最后,我们必须创建如下的Android清单: 清单9-77。AndroidManifest.xml android:theme="@android:style/Theme.NoTitleBar.Fullscreen"> 运行应用时,您应该记住几件事。 如果没有互联网连接,将不会出现任何数据,因此不会显示任何标记。 一些标记可能比其他标记高。这是由于避免碰撞和位置的高度特性。 图9.4和9.5显示了运行中的应用。调试已被禁用。 图9-4。显示标记的应用 图9-5。由于指南针在一个金属物体旁边失控,几个标记汇聚在一起。 本章讲述了创建ar浏览器的过程并提供了代码,这是最流行的AR应用之一。随着GoogleGoggles的出现,它将在那些想使用类似Goggles的东西而不需要实际拥有一套的人中间变得更受欢迎。请务必从这本书的页面或GitHub库下载本章的源代码。您可以直接通过raghavsood@appaholics.in联系我,或者通过Twitter的@Appaholics16联系我。*