Android AR 高级教程(二)
五、常见和不常见的错误和问题
本章处理你在编写本书或其他增强现实(AR)应用中的示例应用时可能遇到的错误和问题。我们将看看与布局、相机、清单和地图相关的错误。我们也有一个不属于这些类别的部分。
布局错误
首先,我们来看看布局中出现的错误。有许多这样的错误,但我们将只关注那些容易在 AR 应用中出现的错误。
用户界面对齐问题
大多数 AR 应用在基本布局文件中使用相对布局。然后,RelativeLayout 将所有小部件、表面视图和定制视图作为其子视图。这是首选的布局,因为它很容易让我们将 UI 元素一个接一个地叠加起来。
使用 RelativeLayout 时面临的一个最常见的问题是,布局最终看起来不像预期的那样。元素最终遍布整个位置,而不是按照你放置它们的顺序。最常见的原因是缺少 ID 或者没有定义某些布局规则。例如,如果你在文本视图的定义中漏掉了一个android:layout_*,文本视图没有设置布局选项,因为它不知道屏幕上的位置,所以布局最终看起来很混乱。此外,在对齐中使用上述 TextView 的任何其他小部件也将最终出现在屏幕上的随机位置。
解决方法很简单。您只需要检查所有的对齐值,并修复任何拼写错误,或者添加任何缺失的值。一个简单的方法是使用 Eclipse 中内置的图形编辑器,或者从[code.google.com/p/droiddraw/](http://code.google.com/p/droiddraw/)获得的开源程序 DroidDraw。在这两种情况下,在图形布局中移动一个元素都会改变其对应的 XML 代码。它允许您轻松地检查和更正布局。
类种姓例外
使用布局时经常遇到的另一个问题是,当试图从 Java 代码中引用特定的小部件时,会出现 ClassCastException。假设我们有一个如下声明的 TextView:
清单 5-1。 一个例子 TextView
<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:text="@string/hello" />
在 XML 中定义了它之后,我们从 Java 代码中引用它,如下所示:
清单 5-2。 引用文本视图
TextView textView = (TextView) findViewById(R.id.textView1);
编译时,我们有时会在前面的代码中得到一个 ClassCastException。在我们的 XML 文件发生大的变化后,通常会收到这个错误。当我们引用 TextView 时,我们使用 View 类中的方法findViewById()来获取与作为参数传递的 ID 相对应的视图。然后我们将由findViewById()返回的视图转换成一个文本视图。应用的所有R.*.*部分都存储在编译应用时生成的 R.java 文件中。有时这个文件没有正确更新,并且 findViewById()返回一个不是我们正在寻找的视图(例如,一个按钮而不是一个文本视图)。然后,当我们试图将不正确的视图转换为 TextView 时,我们会得到一个 ClassCastException,因为您不能将这两个视图相互转换。
解决这个问题很简单。您只需在 Eclipse 中进入项目 清理或者从命令行运行
ant clean来清理项目。
这个错误的另一个原因是你实际上引用了一个不正确的视图,比如一个按钮而不是一个文本视图。要解决这个问题,您需要仔细检查您的 Java 代码,并确保您的 id 是正确的。
相机误差
相机是任何 AR 应用不可或缺的一部分。如果你仔细想想,它增加了大部分的真实性。相机也是一个稍微不稳定的实现在一些设备上工作良好,但在其他设备上完全失败的部分。我们将查看最常见的 Java 错误,并在 AndroidManifest 部分处理单个清单错误。
无法连接到相机服务
在 Android 上使用相机时,最常见的错误可能是无法连接到相机服务。当您试图访问正在被应用(您自己的或其他)使用或已被一个或多个设备管理员完全禁用的摄像机时,会出现此错误。设备管理员是可以在运行 Froyo 和更高版本的设备中更改最小密码长度和相机使用等内容的应用。您可以通过使用 android.app.admin 中的DeviceManagerPolicy.getCameraDisabled()来检查管理员是否禁用了摄像头。您可以在检查时传递管理员的名称,或者传递 null 来检查所有管理员。
如果另一个应用正在使用相机,您将无能为力,但是您可以通过正确释放相机对象来确保您的应用不是导致问题的那个。这个的主代码一般在onPause()和surfaceDestroyed()里。您可以在应用中使用其中一种或两种。整本书,我们都在onPause()发行。两者的代码如下所示:
清单 5-3。 释放相机
@Override public void surfaceDestroyed(SurfaceHolder holder) { `if (mCam != null) {
mCam.stopPreview();
mCam.setPreviewCallback(null);
mCam.release();
mCam = null;
}
}
@Override public void onPause() { super.onPause(); if (mCam != null) { mCam.stopPreview(); mCam.setPreviewCallback(null); mCam.release(); mCam = null; } }`
在前面的代码中,mCam是相机对象。这两种方法中都可能有额外的代码,这取决于您的应用的用途。
Camera.setParameters()失败
最常见的错误之一就是setParameters()的失败。出现这种情况有几个原因,但在大多数情况下,这是因为为预览提供了不正确的宽度或高度。
解决这个问题非常简单。你需要确保你传给 Android 的预览尺寸是受支持的。为了实现这一点,我们在本书的所有示例应用中使用了一个getBestPreviewSize()方法。该方法如清单 5-4 所示:
清单 5-4。 计算最佳预览尺寸
`private Camera.Size getBestPreviewSize(int width, int height, Camera.Parameters parameters) { Camera.Size result=null;
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
if (size.width<=width && size.height<=height) {
if (result==null) {
result=size;
}
else {
int resultArea=result.widthresult.height;
int newArea=size.widthsize.height; if (newArea>resultArea) {
result=size;
}
}
}
}
return(result);
}`
要使用它,请执行以下操作:
清单 5-5。 调用最佳预览尺寸的计算器
`public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Camera.Parameters parameters=camera.getParameters(); Camera.Size size=getBestPreviewSize(width, height, parameters);
if (size!=null) { parameters.setPreviewSize(size.width, size.height); camera.setParameters(parameters); camera.startPreview(); inPreview=true; } }`
surfaceChanged()是我们 app 的SurfaceHolder.Callback部分的一部分。
setPreviewDisplay()中的异常
相机的另一个常见问题是在调用setPreviewDisplay()时出现异常。该方法用于告诉相机哪个表面视图用于实时预览,或者传递 null 来移除预览表面。如果向该方法传递了不合适的图面,该方法将引发 IOException。
修复方法是确保传递的 SurfaceView 适用于相机。可以按如下方式创建合适的表面视图:
清单 5-6。 创建适合相机的表面视图
SurfaceView previewHolder; previewHolder = cameraPreview.getHolder(); previewHolder.addCallback(surfaceCallback); previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
我们将 SurfaceView 的类型改为SURFACE_TYPE_PUSH_BUFFERS,因为它告诉 Android 它将从其他地方接收位图。因为 surface view 的类型改变后可能不会立即准备好,所以您应该在surfaceCallback中完成其余的初始化工作。
AndroidManifest 错误
任何 Android 项目中的一个主要错误来源是 AndroidManifest.xml。开发人员经常忘记更新它,或者将某些元素放在错误的部分。
安全异常
在使用应用时,您很可能会遇到一些安全异常。如果您看一下 LogCat,您会看到类似这样的内容:
04-09 22:44:36.957: E/AndroidRuntime(13347): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.paar.ch2/com.paar.ch2.ProAndroidAR2Activity}: java.lang.SecurityException: Provider gps requires ACCESS_FINE_LOCATION permission
在这种情况下,抛出异常是因为我没有在清单中声明android.permission.ACCESS_FINE_LOCATION,并且试图使用 GPS,这需要许可。
即使您已经声明了权限,但它不在清单的正确部分,您也可能会得到相同的错误。一个常见的问题是当开发者把它放在<application>或<activity>元素中时,它应该在根<manifest>元素中。此外,开发人员报告说,有时即使问题出现在正确的部分,也可以通过将其从<application>元素之后移到之前来解决。
表 5-1 列出了 AR 应用中的常用权限以及它们允许你做的事情。
在某些设备上,缺少相机权限也可能导致无法连接到相机服务错误。
<用途——图书馆>
<uses-library>元素放在 Android 清单的<application>元素中。默认情况下,每个项目中都包含标准的 android 小部件等等。然而,有些库,比如谷歌地图库,需要通过清单的<uses-library>部分显式包含。地图库是 AR 应用中最常用的一个。您可以将它包含在<application>元素中,如下所示:
清单 5-7。 将谷歌地图库添加到您的应用清单中
<uses-library android:name="com.google.android.maps" />
要包含这个库,您必须以 Google APIs 为目标。在我们所有带有地图的示例应用中,我们的目标是 Android 2.1 的 Google APIs。
<用途-特性>
虽然严格来说,丢失<uses-feature>不是一个实际的错误,但最好将它放在你的应用中,因为它被各种发布渠道用来查看你的应用是否能在特定设备上工作。增强现实应用中最常见的两个是:
android.hardware.camera android.hardware.camera.autofocus
与地图相关的错误
地图是许多 AR 应用的重要组成部分。但是,在使用它们时,会出现一些非常常见的错误。我们来看看他们两个。
钥匙
Google Maps API 用于提供本书中的地图,它要求每个应用获得一个调试证书的 API 密钥(Eclipse 在调试时用它来签署应用)和另一个发布证书的 API 密钥(在发布前用它来签署您的.apk)。当从调试切换到生产时,开发人员通常会忘记更改密钥,或者根本忘记添加密钥。在这些情况下,您的地图工作正常,只是没有加载地图切片,并且您会得到一个带有网格的白色背景和一些叠加层(如果有)。
将这两个键作为注释保存在 XML 文件中是一个很好的实践,这样您就不必重复地在线生成它们。列表 5-8 显示了一个例子:
清单 5-8。 带有两个键的示例地图视图
`<com.google.android.maps.MapView android:id="@+id/mapView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:clickable="true" android:apiKey="0nU9aMfHubxd2LIZ_dht3zDDRBb2IG6T3NrnvUA" />
`不扩展 MapActivity
要在您的应用中使用 Google Maps API,显示地图的活动必须扩展com.google.android.maps.MapActivity而不是通常的活动。如果不这样做,您将会得到类似如下的错误:
04-03 14:40:33.670: E/AndroidRuntime(414): Caused by: java.lang.IllegalArgumentException: MapViews can only be created inside instances of MapActivity.
要解决这个问题,只需将类声明修改如下:
正常活动
public class example extends Activity {
地图活动
public class example extends MapActivity {
要使用 MapActivity,您必须导入它,在清单中声明适当的<uses-library>,并为 SDK 的 Google API 版本构建。
调试应用
本节讨论应用的调试。它将解释为了解决应用中的问题,您必须做些什么。
日志猫
我们先来看看 LogCat。在 Eclipse 的右上角有两个选项卡:Java 和 Debug。单击调试选项卡。你很可能会看到那里有两列。一个将有控制台和任务标签;另一个只有一个读 LogCat 的标签。如果 LogCat 选项卡不可见,进入窗口 显示视图
LogCat 。现在开始调试运行。通过 USB 插入设备后,如果启用了 USB 调试,您应该会在 LogCat 中看到类似图 5-1 的内容。
图 5-1。 一个 logcat 输出的例子
异常和错误将在 LogCat 中显示为红色块,长度大约为 10-25 行,这取决于具体的问题。大约在中间点,会有一行写着“起因于:……”。在这一行和这之后,你会在你的应用文件中找到导致错误的确切行号。
使用相机时的黑白方块
只有一种方法可以解决这样的问题:使用模拟器中的摄像头运行应用。模拟器不支持摄像机或任何其他传感器,除了通过模拟位置的 GPS。当您尝试在模拟器中创建相机预览时,您会看到一个由黑白方块组成的网格,像棋盘一样排列。但是,所有覆盖都应该按预期显示。
杂项
有一些错误实际上不属于前面的任何类别。我们将在这个杂项部分讨论它们。
无法从 GPS 获得定位
在测试或使用你的应用时,有时你的代码是完美的,但你仍然无法在你的应用中获得 GPS 定位。这可能是由以下任何原因造成的:
- **你在室内:**GPS 需要一个清晰的天空视野来定位。试着站在开着的门或窗户附近,或者到外面去。
- **你在外面,但仍然没有定位:**这种情况有时会发生在暴风雨或多云的天气。你唯一的选择是等天气稍微放晴后再试。
- **天气晴朗时没有 GPS 定位:**这种情况通常发生在你忘记打开手机的 GPS 时。如果某个应用试图在它关闭时使用它,一些设备会自动打开它,但大多数设备都需要用户这样做。另一个简单的检查是打开另一个使用 GPS 的应用。你可以试试谷歌地图,因为它已经预装在几乎所有安卓设备上。如果连这个都不能解决,那么问题可能不在你的应用上。
指南针不工作
如今,许多增强现实应用都使用大多数 Android 设备中存在的指南针。指南针有助于导航应用,为观星设计的应用等等。
指南针经常会给出不正确的读数。这可能是由于以下原因之一:
- **指南针靠近金属/磁性物体或强电流:**这些会产生强局部磁场,使硬件产生错误读数。试着转移到一个空旷的地方。
- **指南针没有校准:**有时候硬件指南针没有校准到某个区域的磁场。在大多数设备中,用力翻转和摇动设备,或者以 8 字形挥动设备,可以重置指南针。
如果您的指南针在尝试了之前给出的解决方案后仍未给出正确的读数,您可能应该将其送到服务中心进行检查,因为您的硬件可能有故障。
总结
本章讨论您在编写 AR 应用时可能会遇到的常见错误以及如何解决它们。根据你正在编写的应用的类型,你无疑会面临许多其他逻辑标准 Java 和其他 Android 错误,这些错误与应用的 AR 部分并不真正相关。讨论你可能面临的每一个可能的错误本身就可以写满一整本书,所以我们在这里只讨论与 AR 相关的错误。
在下一章,我们将创建我们的第一个示例应用。
六、一个简单的基于位置的应用,使用增强现实和地图 API
本章概述了如何制作一个非常简单的现实世界增强现实(AR)应用。本章结束时,你将拥有一个功能齐全的示例应用。
该应用将具有以下基本功能:
- 该应用将启动,并在屏幕上显示一个实时相机预览。
- 相机预览将被传感器和位置数据覆盖,如在第三章微件覆盖示例应用中。
- 当手机与地面平行时,应用会切换到显示地图。我们将增加 7 的余量,因为用户不太可能将设备与地面完全平行。用户的当前位置将在应用上标记出来。该地图将有在卫星视图、街道视图和两者之间切换的选项。该地图将使用谷歌地图应用编程接口(API)提供。
- 当设备移动到与地面不平行的方向时,应用将切换回相机视图。
这个应用可以作为一个独立的应用,也可以扩展为一个增强现实导航系统,我们将在下一章中介绍。
首先,创建一个新的 Android 项目。该项目应针对谷歌 API(API 级别 7,因为我们的目标是 2.1 及以上),以便我们可以使用 Android 的地图功能。本章通篇使用的项目有包名com.paar.ch06,项目名 Pro Android AR 6:使用 AR 的简单 App。您可以使用您想要的任何其他包和项目名称,只要您记得更改示例代码中的任何引用以匹配您的更改。
创建项目后,通过右键单击 eclipse 左侧栏中的包名并从 New 菜单中选择 class,向您的项目添加另一个类(参见图 6-1 ):
图 6-1。 菜单创建一个新的类。
将此类命名为FlatBack。它将保存MapView和相关的位置 API。然后创建另一个名为FixLocation的类。在本章的后面你会学到更多关于这个类的知识。
编辑 XML
在创建了必要的类之后,我们就可以开始编码工作了。首先,编辑AndroidManifest.xml来声明新的activity,并要求必要的特性、库和权限。更新AndroidManifest.xml如下:
清单 6-1 。更新 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
`package="com.paar.ch6"
android:versionCode="1"
android:versionName="1.0" >
<activity android:label="@string/app_name" android:name=".ASimpleAppUsingARActivity" android:screenOrientation = "landscape" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:configChanges = "keyboardHidden|orientation">
****** ****** ****** ****** ****** ****** `确保FlatBack Activity和之前声明的完全一样,确保<uses-library>标签在<application>标签内,所有的权限和特性请求都在<application>标签外和<manifest>标签内。这几乎是目前需要在AndroidManifest中完成的所有事情。
我们现在需要添加一些字符串,这些字符串将在应用的覆盖图和帮助对话框中使用。将您的strings.xml修改为以下内容:
清单 6-2。 更新 strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, ASimpleAppUsingARActivity!</string> <string name="app_name">A Simple App Using AR</string>
`X Axis:
Y Axis:
Z Axis:
Heading:
Pitch:
Roll:
Altitude:
Longitude:
Latitude:
This is the example app from Chapter 6 of Pro
Android Augmented Reality. This app outlines some of the basic features of
Augmented Reality and how to implement them in real world applications.
Help
`
创建菜单资源
您将创建两个菜单资源:一个用于摄像机预览Activity,另一个用于MapActivity。为此,在项目的/res目录中创建一个名为menu的新的子文件夹。在那个目录中创建两个 XML 文件,分别名为main_menu和map_toggle。在main_menu中,添加以下内容:
清单 6-3。 main_menu.xml
`
`这基本上是主Activity中的帮助选项。现在在map_toggle中,我们将有三个选项,因此添加以下内容:
清单 6-4。 map_toggle.xml
`
` ` `第一个选项允许用户设置街道视图显示的地图种类,就像你在路线图上看到的一样。第二种选择允许他们在地图上使用卫星图像。第三种选择是在那个地方的卫星图像上叠加一张路线图。当然,这两个文件都只定义了用户界面的一部分,实际的工作将在 Java 文件中完成。
布局文件
这个项目中有三个布局文件。一个用于主相机预览和相关叠加,一个用于帮助对话框,一个用于地图。
相机预览
相机预览Activity布局文件是普通的main.xml,对其标准内容有一些改变:
清单 6-5。 摄像机预览布局文件
`
<TextView
android:id="@+id/yAxisLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_alignLeft="@+id/xAxisLabel"
android:layout_below="@+id/xAxisLabel"
android:text="@string/yAxis" />
<TextView android:id="@+id/longitudeLabel"`
`android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/latitudeLabel" android:layout_below="@+id/latitudeLabel" android:text="@string/longitude" />
`
`
<Button
android:id="@+id/helpButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/altitudeLabel"
android:layout_below="@+id/altitudeValue"
android:layout_marginTop="15dp" android:text="@string/helpLabel" />
`
同样,你需要确保所有的 id 都是有序的,并且你没有在任何地方打错字,因为这将影响整个布局。与第三章第一部分的布局唯一的主要区别是增加了一个帮助按钮,它将启动帮助对话框。“帮助”菜单选项会做同样的事情,但是最好有一个更容易看到的选项。
帮助对话框
现在在/res/layout目录中创建另一个名为help.xml的 XML 文件。这将包含帮助对话框的布局设计,它有一个可滚动的TextView来显示实际的帮助文本和一个关闭对话框的按钮。将以下内容添加到help.xml文件中:
清单 6-6。 帮助对话框布局文件
`
`
如你所见,这是一个相对简单的RelativeLayout用于对话框布局。在文件的底部有一个ScrollView和一个TextView来保存帮助对话框的内容,还有一个Button用来关闭对话框。
地图布局
现在我们需要创建最终的布局文件:地图布局。在您的/res/layout文件夹中创建一个map.xml,并将以下内容添加到其中:
清单 6-7。 地图布局文件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <com.google.android.maps.MapView android:id="@+id/mapView" android:layout_width="fill_parent" android:layout_height="fill_parent" android:clickable="true" android:apiKey="<your_key_here>" /> </LinearLayout>
获取 API 密钥
如果您的项目没有设置为基于 Google APIs 目标构建,您将会得到一个错误。这里另一件重要的事情是 API 键。这是由谷歌以证书的形式分配给你的。它是从您的证书的 MD5 散列中生成的,您必须以在线形式提交该散列。Android 使用数字证书来验证应用的安装文件。如果签名证书在已安装版本和新版本之间不匹配,Android 将抛出一个安全异常,不允许您更新安装。映射 API 密钥对于每个证书都是唯一的。因此,如果您计划发布您的应用,您必须生成两个 API 密匙:一个用于您的调试证书(Eclipse 在开发和测试过程中用它来签署您的应用),另一个用于您的发布证书(在将您的应用上传到在线市场(如 Android Market)之前用它来签署您的应用)。在不同的操作系统上,获取任何密钥的 MD5 的步骤是不同的。
获取密钥的 MD5
为调试键:
调试密钥通常位于以下位置:
- Mac/Linux: ~/。android/debug.keystore
- Windows Vista/7: C:\Users\ \。android\debug.keystore
- windows XP:C:\ Documents and Settings <user>\。android\debug.keystore
您需要运行以下命令来取出 MD5。该命令使用 Keytool 工具:
keytool -list -alias androiddebugkey -keystore <path_to_debug_keystore>.keystore -storepass android -keypass android
对于签名密钥:
签名密钥在系统中没有固定的位置。在创建过程中或创建后,无论您将它保存或移动到何处,它都会被保存。运行下面的命令获取 MD5,用密钥的别名替换alias_name,用密钥的位置替换my-release-key:
keytool -list -alias alias_name -keystore my-release-key.keystore
在您提取了您想要的任何密钥 MD5 之后,使用您最喜欢的 web 浏览器导航到[code.google.com/android/maps-api-signup.html](http://code.google.com/android/maps-api-signup.html)。输入 MD5 并完成要求您做的任何其他事情。提交表单后,您将看到应用运行所需的 API 密钥。
Java 代码
现在 XML 设置已经准备好了。所需要的只是标记图像和实际代码。让我们从标记图像开始。它叫做ic_maps_current_position_indicator.png,可以在这个项目源代码的drawable-mdpi和drawable-hdpi文件夹中找到。请确保将每个文件夹的图像复制到项目中的对应位置,不要错误地切换它们。
主要活动
有了图像,我们就可以开始写代码了。我们将从主要的Activity开始。
导入和变量声明
首先,我们来看看导入、类声明和变量声明:
清单 6-8。 主要活动进口和报关
`package com.paar.ch6;
import android.app.Activity; import android.app.Dialog; import android.content.Intent; import android.hardware.Camera; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView;
public class ASimpleAppUsingARActivity extends Activity { SurfaceView cameraPreview; SurfaceHolder previewHolder; Camera camera; boolean inPreview;
final static String TAG = "PAAR"; SensorManager sensorManager;
int orientationSensor;
float headingAngle; float pitchAngle;
float rollAngle;
int accelerometerSensor; float xAxis; float yAxis; float zAxis;
LocationManager locationManager; double latitude; double longitude; double altitude;
TextView xAxisValue; TextView yAxisValue; TextView zAxisValue; TextView headingValue; TextView pitchValue; TextView rollValue; TextView altitudeValue; TextView latitudeValue; TextView longitudeValue;
Button button;`
导入语句和类声明是标准的 Java,并且变量已经被命名来描述它们的功能。现在让我们继续讨论课堂上的不同方法。
onCreate()方法
app 的第一个方法,onCreate(),做了很多事情。它将main.xml文件设置为Activity视图。然后它获得位置和传感器系统服务。它为加速计和方向传感器以及全球定位系统(GPS)注册监听器。然后,它执行相机初始化的一部分(其余部分稍后执行)。最后,它获得了对九个TextViews的引用,以便它们可以在应用中更新,并获得了对帮助按钮的引用,设置了它的onClickListener。此方法的代码如下:
清单 6-9。 主活动的 onCreate()
`@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000,
2, locationListener);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); orientationSensor = Sensor.TYPE_ORIENTATION; accelerometerSensor = Sensor.TYPE_ACCELEROMETER; sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL); sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL);
inPreview = false;
cameraPreview = (SurfaceView)findViewById(R.id.cameraPreview); previewHolder = cameraPreview.getHolder(); previewHolder.addCallback(surfaceCallback); previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
xAxisValue = (TextView) findViewById(R.id.xAxisValue); yAxisValue = (TextView) findViewById(R.id.yAxisValue); zAxisValue = (TextView) findViewById(R.id.zAxisValue); headingValue = (TextView) findViewById(R.id.headingValue); pitchValue = (TextView) findViewById(R.id.pitchValue); rollValue = (TextView) findViewById(R.id.rollValue); altitudeValue = (TextView) findViewById(R.id.altitudeValue); longitudeValue = (TextView) findViewById(R.id.longitudeValue); latitudeValue = (TextView) findViewById(R.id.latitudeValue); button = (Button) findViewById(R.id.helpButton); button.setOnClickListener(new OnClickListener() { public void onClick(View v) { showHelp(); } }); }`
位置监听器
代码中的下一个是LocationListener,它监听来自定位服务(本例中是 GPS)的位置更新。从 GPS 接收到更新后,它用新信息更新本地变量,将新信息打印到LogCat,并用新信息更新三个TextViews。它还包含应用中没有使用的方法的自动生成的方法存根。
**清单 6-10。**location listener
`LocationListener locationListener = new LocationListener() { public void onLocationChanged(Location location) { latitude = location.getLatitude(); longitude = location.getLongitude(); altitude = location.getAltitude();
Log.d(TAG, "Latitude: " + String.valueOf(latitude)); Log.d(TAG, "Longitude: " + String.valueOf(longitude)); Log.d(TAG, "Altitude: " + String.valueOf(altitude));
latitudeValue.setText(String.valueOf(latitude)); longitudeValue.setText(String.valueOf(longitude)); altitudeValue.setText(String.valueOf(altitude)); }
public void onProviderDisabled(String arg0) { // TODO Auto-generated method stub
}
public void onProviderEnabled(String arg0) { // TODO Auto-generated method stub
}
public void onStatusChanged(String arg0, int arg1, Bundle arg2) { // TODO Auto-generated method stub
} };`
启动地图
接下来要解释的是launchFlatBack()方法。每当满足手机或多或少与地面平行的条件时,SensorEventListener就会调用该方法。然后,该方法启动地图。
清单 6-11。 launchFlatBack()
public void launchFlatBack() { Intent flatBackIntent = new Intent(this, FlatBack.class); startActivity(flatBackIntent); }
选项菜单
通过覆盖onCreateOptionsMenu()和onOptionsItemSelected()方法来创建和使用选项菜单。第一个从菜单资源(main_menu.xml)创建它,第二个监听菜单上的点击事件。如果单击了帮助项,它将调用适当的方法来显示帮助对话框。
清单 6-12。 onCreateOptionsMenu()和 onOptionsItemSelected()
`@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_menu, menu); return true; }
public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { case R.id.help: showHelp(); default: return super.onOptionsItemSelected(item); } }`
显示帮助对话框
showHelp()是前面提到的合适方法。当单击“帮助”菜单项时,将调用该函数。
清单 6-13。 showHelp()
`public void showHelp() { final Dialog dialog = new Dialog(this); dialog.setContentView(R.layout.help); dialog.setTitle("Help"); dialog.setCancelable(true); //there are a lot of settings, for dialog, check them all out!
//set up text TextView text = (TextView) dialog.findViewById(R.id.TextView01); text.setText(R.string.help);`
//set up button Button button = (Button) dialog.findViewById(R.id.Button01); button.setOnClickListener(new OnClickListener() { public void onClick(View v) { dialog.cancel(); } }); //now that the dialog is set up, it's time to show it dialog.show(); }
监听传感器
现在我们来看SensorEventListener。有一个if陈述区分了方位传感器和加速度计。传感器的两个更新都被打印到LogCat和相应的TextViews。此外,代码中方位传感器部分的if语句决定了设备是否与地面平行。存在 14 度的偏差,因为任何人都不太可能将该设备与地面完全平行。
清单 6-14。 SensorEventListener
`final SensorEventListener sensorEventListener = new SensorEventListener() { public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION) { headingAngle = sensorEvent.values[0]; pitchAngle = sensorEvent.values[1]; rollAngle = sensorEvent.values[2];
Log.d(TAG, "Heading: " + String.valueOf(headingAngle)); Log.d(TAG, "Pitch: " + String.valueOf(pitchAngle)); Log.d(TAG, "Roll: " + String.valueOf(rollAngle));
headingValue.setText(String.valueOf(headingAngle)); pitchValue.setText(String.valueOf(pitchAngle)); rollValue.setText(String.valueOf(rollAngle));
if (pitchAngle < 7 && pitchAngle > -7 && rollAngle < 7 && rollAngle > -7) { launchFlatBack(); } }`
`else if (sensorEvent.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { xAxis = sensorEvent.values[0]; yAxis = sensorEvent.values[1]; zAxis = sensorEvent.values[2];
Log.d(TAG, "X Axis: " + String.valueOf(xAxis)); Log.d(TAG, "Y Axis: " + String.valueOf(yAxis)); Log.d(TAG, "Z Axis: " + String.valueOf(zAxis));
xAxisValue.setText(String.valueOf(xAxis)); yAxisValue.setText(String.valueOf(yAxis)); zAxisValue.setText(String.valueOf(zAxis)); } }
public void onAccuracyChanged (Sensor senor, int accuracy) { //Not used } };`
onResume()、onPause()和 onDestroy()方法
我们覆盖了onResume()、onPause()和onDestroy()方法,这样我们就可以释放和重新获取SensorEventListener、LocationListener和Camera。当应用暂停(用户切换到另一个应用)或被破坏(Android 终止该进程)时,我们会释放它们,以节省用户的电池和使用更少的系统资源。此外,一次只有一个应用可以使用Camera,所以通过释放它,我们可以让其他应用使用它。
清单 6-15。 onResume(),onPause()和 onDestroy()
`@Override public void onResume() { super.onResume(); locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, locationListener); sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL); sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL); //Camera camera; }
@Override
public void onPause() {
if (inPreview) {
camera.stopPreview(); }
locationManager.removeUpdates(locationListener);
sensorManager.unregisterListener(sensorEventListener);
if (camera != null)
{
camera.release();
camera=null;
}
inPreview=false;
super.onPause(); }
@Override public void onDestroy() { camera.release(); camera=null; }`
管理表面视图和相机
这最后四个方法处理管理SurfaceView、它的SurfaceHolder和Camera。
getBestPreviewSize()方法获得可用预览尺寸的列表,并选择最佳尺寸。- 当
SurfaceView准备好时,调用surfaceCallback。相机在那里设置并打开。 - 如果 Android 对
SurfaceView做了任何更改,就会调用surfaceChanged()方法(例如,在方向改变之后)。 - 当
SurfaceView被销毁时,调用surfaceDestroyed()方法。
清单 6-16。 getBestPreviewSize()、surfaceCallback()、surfaceChanged()和 surfaceDestroyed()
`private Camera.Size getBestPreviewSize(int width, int height, Camera.Parameters parameters) { Camera.Size result=null;
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
if (size.width<=width && size.height<=height) {
if (result==null) {
result=size;
} else {
int resultArea=result.widthresult.height;
int newArea=size.widthsize.height;
if (newArea>resultArea) { result=size; } } } }
return(result); }
SurfaceHolder.Callback surfaceCallback=new SurfaceHolder.Callback() { public void surfaceCreated(SurfaceHolder holder) { if (camera == null) { camera = Camera.open(); } try { camera.setPreviewDisplay(previewHolder); } catch (Throwable t) { Log.e(TAG, "Exception in setPreviewDisplay()", t); } }
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Camera.Parameters parameters=camera.getParameters(); Camera.Size size=getBestPreviewSize(width, height, parameters);
if (size!=null) { parameters.setPreviewSize(size.width, size.height); camera.setParameters(parameters); camera.startPreview(); inPreview=true; } }
public void surfaceDestroyed(SurfaceHolder holder) { if (camera != null) { camera.stopPreview(); camera.setPreviewCallback(null); camera.release(); camera = null; } }`
}; }
这是第一个 Java 文件的结尾。该文件与 GPS 和传感器一起工作以获得更新,然后通过TextViews和LogCat输出显示它们。
平反. java
现在我们来学习FlatBack.java.这个Activity在手机与地面平行时被调用,并在地图上显示你的当前位置。这个类现在没有多大意义,因为部分工作是在FixLocation完成的。
导入、变量声明和 onCreate()方法
在这个Activity的onCreate()中,我们一如既往地在开头重复SensorManager的东西。这里我们需要传感器输入,因为当设备不再与地面平行时,我们希望切换回CameraView。之后,我们获取对MapView(XML 布局中的那个)的引用,告诉 Android 我们不会实现自己的缩放控件,将MapView传递给FixLocation,将位置覆盖添加到MapView,告诉它更新,并调用自定义方法将它缩放到用户的位置。
清单 6-17。 Flatback.java 的导入、声明和 onCreate()
`package com.paar.ch6;
import com.google.android.maps.GeoPoint; import com.google.android.maps.MapActivity; import com.google.android.maps.MapView; import com.google.android.maps.MyLocationOverlay;
import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem;`
`public class FlatBack extends MapActivity{ private MapView mapView; private MyLocationOverlay myLocationOverlay; final static String TAG = "PAAR"; SensorManager sensorManager;
int orientationSensor; float headingAngle; float pitchAngle; float rollAngle; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// main.xml contains a MapView setContentView(R.layout.map);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); orientationSensor = Sensor.TYPE_ORIENTATION; sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
// extract MapView from layout mapView = (MapView) findViewById(R.id.mapView); mapView.setBuiltInZoomControls(true);
// create an overlay that shows our current location myLocationOverlay = new FixLocation(this, mapView);
// add this overlay to the MapView and refresh it mapView.getOverlays().add(myLocationOverlay); mapView.postInvalidate();
// call convenience method that zooms map on our location zoomToMyLocation(); }`
onCreateOptionsMenu()和 onOptionsItemSelected()方法
接下来是两个与选项菜单相关的方法,它们创建选项菜单,观察点击,区分哪个选项被点击,并在地图上执行适当的操作。
清单 6-18。 onCreateOptionsMenu()和 onOptionsItemSelected()
`@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.map_toggle, menu); return true; }
public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { case R.id.map: if (mapView.isSatellite() == true) { mapView.setSatellite(false); mapView.setStreetView(true); } return true; case R.id.sat: if (mapView.isSatellite()==false){ mapView.setSatellite(true); mapView.setStreetView(false); } return true; case R.id.both: mapView.setSatellite(true); mapView.setStreetView(true); default: return super.onOptionsItemSelected(item); } }`
SensorEventListener
接下来是SensorEventListener,它与前面的类类似,只是它检查手机是否不再与地面平行,然后调用将带我们回到相机预览的自定义方法。
清单 6-19。 SensorEventListener
final SensorEventListener sensorEventListener = new SensorEventListener() { public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION) { headingAngle = sensorEvent.values[0]; pitchAngle = sensorEvent.values[1]; rollAngle = sensorEvent.values[2];
`Log.d(TAG, "Heading: " + String.valueOf(headingAngle)); Log.d(TAG, "Pitch: " + String.valueOf(pitchAngle)); Log.d(TAG, "Roll: " + String.valueOf(rollAngle));
if (pitchAngle > 7 || pitchAngle < -7 || rollAngle > 7 || rollAngle < -7) { launchCameraView(); } } }
public void onAccuracyChanged(Sensor arg0, int arg1) { // TODO Auto-generated method stub
} };`
launchCameraView()方法
launchCameraView()方法完成当前的activity,这样我们就可以毫无问题地进行相机预览。一个Intent被注释掉了,似乎做了同样的事情。我把它注释掉了,因为尽管它最终启动了摄像机预览,但它是通过创建那个activity的另一个实例来完成的,这将会产生一个错误,因为摄像机已经被活动的第一个实例使用了。因此,最好返回到以前的实例。
清单 6-20。 launchCameraView()
public void launchCameraView() { finish(); //Intent cameraView = new Intent(this, ASimpleAppUsingARActivity.class); //startActivity(cameraView); }
onResume()和 onPause()方法
然后是onResume()和onPause()方法,它们启用和禁用位置更新以节省资源。
清单 6-21。 onResume()和 onPause()
@Override protected void onResume() { super.onResume();
`myLocationOverlay.enableMyLocation();
}
@Override protected void onPause() { super.onPause(); myLocationOverlay.disableMyLocation(); }`
zoomToMyLocation()方法
这是自定义后的zoomToMyLocation()方法。此方法将缩放级别 10 应用于地图上的当前位置。
清单 6-22。 zoomToMyLocation()
private void zoomToMyLocation() { GeoPoint myLocationGeoPoint = myLocationOverlay.getMyLocation(); if(myLocationGeoPoint != null) { mapView.getController().animateTo(myLocationGeoPoint); mapView.getController().setZoom(10); } }
isRouteDisplayed()方法
最后是布尔方法isRouteDisplayed()。因为没有在 app 中使用,所以设置为 false。
清单 6-23。 isRouteDisplayed()
protected boolean isRouteDisplayed() { return false; } }
这就把我们带到了FlatBack.java的结尾。注意,大多数实际的定位工作似乎是在FixLocation.java中完成的。在您厌倦 Eclipse 在其引用中给你错误之前,我们将继续编写那个类。
FixLocation.java
现在是时候了解FixLocation的用途了。在一些 Android 驱动的设备中,MyLocationOverlay类有严重的错误,其中最显著的是摩托罗拉 droid。FixLocation试图使用标准的 MyLocationOverlay,但是如果它不能正常工作,它会实现自己的版本,这将产生相同的结果。首先是源代码,然后是解释:
清单 6-24。FixLocation.java??
`package com.paar.ch6;
import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Paint.Style; import android.graphics.drawable.Drawable; import android.location.Location;
import com.google.android.maps.GeoPoint; import com.google.android.maps.MapView; import com.google.android.maps.MyLocationOverlay; import com.google.android.maps.Projection;
public class FixLocation extends MyLocationOverlay {
private boolean bugged = false;
private Drawable drawable; private Paint accuracyPaint; private Point center; private Point left; private int width; private int height;
public FixLocation(Context context, MapView mapView) { super(context, mapView); }
@Override protected void drawMyLocation(Canvas canvas, MapView mapView, Location lastFix, GeoPoint myLocation, long when) { if(!bugged) { try { super.drawMyLocation(canvas, mapView, lastFix, myLocation, when); } catch (Exception e) { // we found a buggy phone, draw the location icons ourselves bugged = true; } }`
`if(bugged) { if(drawable == null) {
accuracyPaint = new Paint(); accuracyPaint.setAntiAlias(true); accuracyPaint.setStrokeWidth(2.0f);
drawable = mapView.getContext() .getResources().getDrawable(R.drawable.ic_maps_indicator_current_position); width = drawable.getIntrinsicWidth(); height = drawable.getIntrinsicHeight(); center = new Point(); left = new Point(); }
Projection projection = mapView.getProjection(); double latitude = lastFix.getLatitude(); double longitude = lastFix.getLongitude(); float accuracy = lastFix.getAccuracy();
float[] result = new float[1];
Location.distanceBetween(latitude, longitude, latitude, longitude + 1, result); float longitudeLineDistance = result[0];
GeoPoint leftGeo = new GeoPoint((int)(latitude*1e6), (int)((longitude-accuracy/longitudeLineDistance)*1e6)); projection.toPixels(leftGeo, left); projection.toPixels(myLocation, center); int radius = center.x - left.x;
accuracyPaint.setColor(0xff6666ff); accuracyPaint.setStyle(Style.STROKE); canvas.drawCircle(center.x, center.y, radius, accuracyPaint);
accuracyPaint.setColor(0x186666ff); accuracyPaint.setStyle(Style.FILL); canvas.drawCircle(center.x, center.y, radius, accuracyPaint);
drawable.setBounds(center.x - width/2, center.y - height/2, center.x + width/2, center.y + height/2); drawable.draw(canvas); } } }`
首先,我们有接收来自FlatBack的调用的方法。然后我们覆盖drawMyLocation()方法。在实现中,我们检查它是否被窃听。我们试图让它正常运行,但是如果我们得到一个异常,我们将 bugged 设置为 true,然后继续执行我们自己的工作实现。
如果它确实被窃听,我们设置油漆,得到一个可画的参考,得到位置,计算精度,然后在地图上画标记,随着精度圈。准确度圆圈意味着位置不是 100%准确,你在圆圈内的某个地方。
这个示例应用到此结束。现在快速看一下如何运行该应用,并查看一些截图。
运行应用
该应用应该编译没有任何错误或警告。如果您确实遇到了错误,请阅读下面的常见错误部分。
在设备上调试时,你可能会看到一个橙色的三角形,如图 6-2 所示。
图 6-2。 橙色预警三角
这个三角形仅仅意味着 Eclipse 无法确认 Google APIs 是否安装在您的设备上。如果你的 Android 设备预装了 Android Market,你可以很确定它已经安装了 Google APIs。
当你运行这个应用时,你应该会看到类似于图 6-3 到图 6-5 的截图。
**图 6-3。**app 的增强现实视图
**图 6-4。**app 的帮助对话框
图 6-5。 地图显示当装置平行于地面时
LogCat看起来应该类似于图 6-6 。
**图 6-6。**app的 LogCat 截图
常见错误
以下是该应用的四个常见错误。对于其他任何事情,请向安卓开发者谷歌集团或 stackoverflow.com 寻求帮助。
- **未能连接到相机服务:**我唯一一次看到这个错误是在其他东西已经在使用相机的时候。这个错误可以用几种方法解决,stackoverflow.com 应该能给你答案。
- **任何看起来与地图相关的东西:**这很可能是因为你没有针对 Google APIs 进行构建,或者因为你忘记在
AndroidManifest中声明<uses-library>,或者使用了不正确的 API 键。 - **任何看起来与 R.something 有关的东西:**这个错误很可能是由于 XML 文件中的错误或不匹配,或者缺少 drawable。您可以通过检查 XML 文件来修复它。如果你确定它们是正确的,并且你的可绘制标记已经就位,试着通过删除/bin 目录后编译或者使用 Project - > Clean 从头开始构建。
- **安全异常:**这些很可能是由于您的
AndroidManifest中缺少许可。
总结
这将我们带到本书中第一个示例应用的结尾,它演示了如何执行以下操作:
- 使用标准的 Android SDK,通过实时摄像头预览增加传感器信息
- 当以特定方式握住设备时,启动
Activity,在这种情况下,与地面平行 - 使用 Google Maps APIs 在地图上显示用户的当前位置
- 在设备上的地图 API 被破坏的情况下实施修复
这个应用将在下一章中构建,作为一个简单的 AR 导航应用。
七、使用增强现实、GPS 和地图的基本导航应用
在第六章中,我们设计了一个简单的 AR 应用,如果设备与地面平行,它将在相机预览上显示传感器数据,并在地图上显示位置。在这一章中,我们将扩展这个应用,以便它可以用于基本的导航目的。
新的应用
该应用的扩展版本将具有以下功能:
- 当与地面不平行时,该应用将显示一个覆盖有各种数据的相机预览。
- 当与地面平行时,地图将会展开。用户可以在地图上定位期望的位置,并使 Tap 能够设置模式。启用该模式后,用户可以点击所需的位置。该位置已保存。
- 再次显示相机预览时,根据 GPS 数据计算出目标位置的方位以及两个位置之间的距离。
- 每次接收到新的定位时,方位和距离都会更新。
如果你想扩展它来添加指南针和做其他事情,这个应用会给你你需要的每一个计算。
现在,事不宜迟,让我们开始编码。
首先创建一个新项目。在这个例子中,包名是 com.paar.ch7,构建目标是 android 2.1 的 Google APIs。我们必须针对谷歌 APIs SDK,因为我们正在使用谷歌地图。
首先,复制第六章的项目。把 main Activity(带有相机预览的那个)的名字改成你想要的任何名字,只要你记得更新它的清单。此外,因为这是一个新项目,您可能还需要另一个包名。
更新的 XML 文件
首先,我们需要更新一些 XML 文件。先说strings.xml:
清单 7-1。 更新 strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Hello World, ASimpleAppUsingARActivity!</string> <***string name="app_name">A Slightly More Complex AR App</string>*** <string name="xAxis">X Axis:</string> <string name="yAxis">Y Axis:</string> <string name="zAxis">Z Axis:</string> <string name="heading">Heading:</string> <string name="pitch">Pitch:</string> <string name="roll">Roll:</string> <string name="altitude">Altitude:</string> <string name="longitude">Longitude:</string> <string name="latitude">Latitude:</string> <string name="empty"></string> ***<string name="help">This is the example app from Chapter 7 of Pro Android*** ***Augmented Reality. This app outlines some of the basic features of Augmented*** ***Reality and how to implement them in real world applications. This app includes*** ***a basic system that can be used for navigation. To make use of this system, put*** ***the app in the map mode by holding the device flat. Then enable \"Enable tap to*** ***set\" from the menu option after you have located the place you want to go to.*** ***After that, switch back to camera mode. If a reliable GPS fix is available, you***
***will be given your current bearing to that location. The bearing will be updated*** ***every time a new location fix is received.</string>*** <string name="helpLabel">Help</string> ***<string name="go">Go</string>*** ***<string name="bearingLabel">Bearing:</string>*** ***<string name="distanceLabel">Distance:</string>*** </resources>
这里的“Distance:”将作为标签,告诉用户从他/她的当前位置到所选位置的直线距离。乌鸦路径是从 A 点到 b 点的直线距离。它不显示通过道路或任何其他路径的距离。如果你还记得高中物理的话,这很像位移。它是从 A 点到 B 点的最短距离,不管这段距离实际上是否可以穿越。
您会注意到一些新的字符串和帮助字符串大小的增加。除此之外,我们的strings.xml大体相同。接下来,我们需要从/res/menu文件夹中更新我们的map_toggle.xml。我们需要添加一个新的选项来允许用户设置位置。
清单 7-2。 更新后的 map_toggle.xml
`
****** ****** `我们的新菜单选项是“启用点击设置”此选项将用于允许用户启用和禁用点击来设置我们的应用的功能。如果我们不添加检查,每次用户移动地图或试图缩放时,都会设置一个新的位置。为了避免这种情况,我们设置了一个启用/禁用选项。
现在是我们最大的 XML 文件 main.xml 的最后一个变化,我们需要添加两个TextViews并稍微移动我们的帮助按钮。下面的代码只显示了更新的部分。此处未给出的内容与上一章完全相同。
清单 7-3。 更新 main.xml
`// Cut here
<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/altitudeLabel" android:layout_below="@+id/altitudeLabel" android:text="@string/bearingLabel" />
<TextView android:id="@+id/bearingValue" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/altitudeValue" android:layout_below="@+id/altitudeValue" android:text="@string/empty" />
<Button android:id="@+id/helpButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:text="@string/helpLabel" />
<TextView android:id="@+id/distanceLabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@+id/textView1" android:layout_below="@+id/textView1" android:text="@string/distanceLabel" />
<TextView android:id="@+id/distanceValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/distanceLabel"
android:layout_alignLeft="@+id/bearingValue"
android:text="@string/empty" />
`
鉴于我们添加的内容都遵循了前一章的模式,我希望这段代码是不言自明的。id 中带有“label”的TextViews是实际值的标签。我们的 Java 代码不会引用这些。id 中带有“value”的TextViews将从我们的 Java 代码中动态更新以显示值。
更新的 Java 文件
现在我们可以开始主要的 Java 代码了。我们三分之二的 Java 文件需要用新代码更新。
在FixLocation.java中,您需要更新包声明以匹配新的声明。那是那份文件中唯一的变化。
FlatBack.java 更新
现在让我们转到下一个需要更新的文件:FlatBack.java:
**清单 7-4。**更新了 FlatBack.java 的
*`package com.paar.ch7;
import com.google.android.maps.GeoPoint; import com.google.android.maps.MapActivity; import com.google.android.maps.MapView; import com.google.android.maps.MyLocationOverlay;
import android.content.SharedPreferences;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
public class FlatBack extends MapActivity{ private MapView mapView; private MyLocationOverlay myLocationOverlay; final static String TAG = "PAAR"; SensorManager sensorManager;
SharedPreferences prefs; SharedPreferences.Editor editor;
int orientationSensor; float headingAngle; float pitchAngle; float rollAngle; String enteredAddress; boolean tapToSet; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// main.xml contains a MapView setContentView(R.layout.map); prefs = getSharedPreferences("PAARCH7", 0); editor = prefs.edit(); sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); orientationSensor = Sensor.TYPE_ORIENTATION; sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL);
// extract MapView from layout mapView = (MapView) findViewById(R.id.mapView); mapView.setBuiltInZoomControls(true);
// create an overlay that shows our current location myLocationOverlay = new FixLocation(this, mapView);
// add this overlay to the MapView and refresh it mapView.getOverlays().add(myLocationOverlay); mapView.postInvalidate();
// call convenience method that zooms map on our location zoomToMyLocation();
mapView.setOnTouchListener(new OnTouchListener() {`
`public boolean onTouch(View arg0, MotionEvent arg1) {
if(tapToSet == true) { GeoPoint p = mapView.getProjection().fromPixels((int) arg1.getX(), (int) arg1.getY());
Log.d(TAG,"Latitude:" + String.valueOf(p.getLatitudeE6()/1e6)); Log.d(TAG,"Longitude:" + String.valueOf(p.getLongitudeE6()/1e6)); float lat =(float) ((float) p.getLatitudeE6()/1e6); float lon = (float) ((float) p.getLongitudeE6()/1e6); editor.putFloat("SetLatitude", lat); editor.putFloat("SetLongitude", lon); editor.commit(); return true; } return false;
}
});
}
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.map_toggle, menu); return true; }
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.map:
if (mapView.isSatellite() == true) {
mapView.setSatellite(false);
mapView.setStreetView(true);
}
return true;
case R.id.sat:
if (mapView.isSatellite()==false){
mapView.setSatellite(true);
mapView.setStreetView(false);
}
return true;
case R.id.both:
mapView.setSatellite(true); mapView.setStreetView(true);
case R.id.toggleSetDestination:
if(tapToSet == false)
{
tapToSet = true;
item.setTitle("Disable Tap to Set");
}
else if(tapToSet == true)
{
tapToSet = false;
item.setTitle("Enable Tap to Set");
mapView.invalidate();
}
default:
return super.onOptionsItemSelected(item);
}
}
final SensorEventListener sensorEventListener = new SensorEventListener() { public void onSensorChanged(SensorEvent sensorEvent) { if (sensorEvent.sensor.getType() == Sensor.TYPE_ORIENTATION) { headingAngle = sensorEvent.values[0]; pitchAngle = sensorEvent.values[1]; rollAngle = sensorEvent.values[2];
Log.d(TAG, "Heading: " + String.valueOf(headingAngle)); Log.d(TAG, "Pitch: " + String.valueOf(pitchAngle)); Log.d(TAG, "Roll: " + String.valueOf(rollAngle));
if (pitchAngle > 7 || pitchAngle < -7 || rollAngle > 7 || rollAngle < -7) { launchCameraView(); } } }
public void onAccuracyChanged(Sensor arg0, int arg1) {
} };
public void launchCameraView() { finish(); }
@Override
protected void onResume() {
super.onResume(); myLocationOverlay.enableMyLocation();
}
@Override protected void onPause() { super.onPause(); myLocationOverlay.disableMyLocation(); }
private void zoomToMyLocation() { GeoPoint myLocationGeoPoint = myLocationOverlay.getMyLocation(); if(myLocationGeoPoint != null) { mapView.getController().animateTo(myLocationGeoPoint); mapView.getController().setZoom(10); } }
protected boolean isRouteDisplayed() { return false; } }`* *我们来看看有什么变化。首先,我们在顶部有一些新的变量:
boolean tapToSet; SharedPreferences prefs; SharedPreferences.Editor editor;
boolean tapToSet将告诉我们点击设置模式是否启用。另外两个是SharedPreferences相关变量。我们将使用SharedPreferences来存储用户的设置值,因为我们将从我们类的两个活动中访问它。当然,我们可以在启动MapActivity时使用startActivityForResult(),并以这种方式获取用户的设置值,但通过使用SharedPreferences,我们也可以保留用户最后使用的位置,以防应用稍后启动时没有设置新位置。
接下来,我们给我们的onCreate()方法添加了一些新的东西。这两行负责访问我们的SharedPreferences,并允许我们稍后编辑它们:
prefs = getSharedPreferences("PAARCH7", 0); editor = prefs.edit();
PAARCH7 是我们偏好文件的名称,代表ProAn droidAugmentedRealityChapter7。如果你自己扩展这个应用,并从多个地方同时使用SharedPreferences,请记住,当编辑同一个偏好文件时,每个人都可以立即看到这些变化。第一次运行时,PAARCH7 文件不存在,所以 Android 创建了它。逗号后面的小 0 告诉 Android 这个文件是私有的。下一行指定编辑器能够编辑我们的首选项。
现在我们的onCreate()方法有了更多的变化。我们给我们的MapView分配一个onTouchListener():
`mapView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View arg0, MotionEvent arg1) {
if(tapToSet == true)
{
GeoPoint p =
mapView.getProjection().fromPixels((int)arg1.getX(),
(int) arg1.getY());
Log.d(TAG,"Latitude:" +String.valueOf(p.getLatitudeE6()/1e6)); Log.d(TAG,"Longitude:" +String.valueOf(p.getLongitudeE6()/1e6)); float lat =(float) ((float) p.getLatitudeE6()/1e6); float lon = (float) ((float) p.getLongitudeE6()/1e6); editor.putFloat("SetLatitude", lat); editor.putFloat("SetLongitude", lon); editor.commit(); return true; } return false;
}
});`
在这个onTouchListener(),中,我们过滤每个触摸。如果启用了点击设置模式,我们将捕获触摸事件并获得纬度和经度。然后我们把从 touched GeoPoint接收到的 doubles 转换成 floats,这样我们就可以按照自己的喜好来写了,这正是我们所做的。我们把这两个浮点数都放在我们的首选项文件中,然后调用editor.commit()把它们写到文件中。如果我们捕捉到触摸,我们返回true,如果没有,我们返回false。通过返回false,我们允许MapView继续正常的滚动和放大缩小。
我们需要做的最后一件事是修改我们的onOptionsItemSelected()方法,以允许 Enable Tap To Set 选项。
public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { case R.id.map: if (mapView.isSatellite() == true) { mapView.setSatellite(false);
mapView.setStreetView(true); } return true; case R.id.sat: if (mapView.isSatellite()==false){ mapView.setSatellite(true); mapView.setStreetView(false); } return true; case R.id.both: mapView.setSatellite(true); mapView.setStreetView(true); case R.id.toggleSetDestination: if(tapToSet == false) { tapToSet = true; item.setTitle("Disable Tap to Set"); } else if(tapToSet == true) { tapToSet = false; item.setTitle("Enable Tap to Set"); mapView.invalidate(); } default: return super.onOptionsItemSelected(item); } }
我们首先检查tapToSet是否是false。如果是,我们将其设置为true,并将标题更改为“禁用点击设置”如果是true,我们把它改成false,把标题改回“启用点击设置”
这份文件就包装好了。
主活动文件
现在我们只剩下主文件了。
我们将从查看新变量开始。
清单 7-5。 包申报、进口和新变量
`package com.paar.ch7;
import android.app.Activity; import android.app.Dialog; import android.content.Intent; import android.content.SharedPreferences;`
`import android.hardware.Camera;
…
double bearing; double distance;
float lat; float lon;
Location setLoc; Location locationInUse;
SharedPreferences prefs;
…
TextView bearingValue; TextView distanceValue;`
当从文件中读取时,两个浮点数lat和lon将存储我们保存到MapActivity中的SharedPreferences中的值。位置setLoc将被传递前面提到的纬度和经度以创建一个新的Location。然后,我们将使用该位置来获取用户的方位。locationInUse是我们 GPS 定位的副本。这两个TextViews将显示我们的结果。doublebearing和distance将存储我们的结果。
现在我们需要对我们的onCreate()方法做一些改变。
清单 7-6。 更新 onCreate()
`@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); setLoc = new Location("");
prefs = getSharedPreferences("PAARCH7", 0);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, locationListener);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
orientationSensor = Sensor.TYPE_ORIENTATION;
accelerometerSensor = Sensor.TYPE_ACCELEROMETER;
sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL); sensorManager.registerListener(sensorEventListener, sensorManager
.getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL);
inPreview = false;
cameraPreview = (SurfaceView)findViewById(R.id.cameraPreview); previewHolder = cameraPreview.getHolder(); previewHolder.addCallback(surfaceCallback); previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
xAxisValue = (TextView) findViewById(R.id.xAxisValue); yAxisValue = (TextView) findViewById(R.id.yAxisValue); zAxisValue = (TextView) findViewById(R.id.zAxisValue); headingValue = (TextView) findViewById(R.id.headingValue); pitchValue = (TextView) findViewById(R.id.pitchValue); rollValue = (TextView) findViewById(R.id.rollValue); altitudeValue = (TextView) findViewById(R.id.altitudeValue); longitudeValue = (TextView) findViewById(R.id.longitudeValue); latitudeValue = (TextView) findViewById(R.id.latitudeValue); bearingValue = (TextView) findViewById(R.id.bearingValue); distanceValue = (TextView) findViewById(R.id.distanceValue); button = (Button) findViewById(R.id.helpButton); button.setOnClickListener(new OnClickListener() { public void onClick(View v) { showHelp(); } }); }`
行prefs = getSharedPreferences("PAARCH7", 0);让我们访问我们的SharedPreferences。接下来的新行(bearingValue = (TextView) findViewById(R.id.bearingValue);和distanceValue = (TextView) findViewById(R.id.distanceValue);)将引用我们的新TextViews,并允许我们稍后更新它们。
现在我们必须更新LocationListener,这样我们的计算就会随着位置的更新而更新。这个比较简单。
清单 7-7。 更新了 LocationListener
`LocationListener locationListener = new LocationListener() { public void onLocationChanged(Location location) { locationInUse = location; latitude = location.getLatitude(); longitude = location.getLongitude(); altitude = location.getAltitude();
Log.d(TAG, "Latitude: " + String.valueOf(latitude));
Log.d(TAG, "Longitude: " + String.valueOf(longitude)); Log.d(TAG, "Altitude: " + String.valueOf(altitude));
latitudeValue.setText(String.valueOf(latitude)); longitudeValue.setText(String.valueOf(longitude)); altitudeValue.setText(String.valueOf(altitude));
lat = prefs.getFloat("SetLatitude", 0.0f); lon = prefs.getFloat("SetLongitude", 0.0f);
setLoc.setLatitude(lat); setLoc.setLongitude(lon); if(locationInUse != null) { bearing = locationInUse.bearingTo(setLoc); distance = locationInUse.distanceTo(setLoc); bearingValue.setText(String.valueOf(bearing)); distanceValue.setText(String.valueOf(distance)); } }`
我们的修改包括从SharedPreferences获取值,并检查我们是否有一个有效的位置;如果有一个有效的位置,我们计算并显示方位和距离。如果没有,我们什么也不做。
我们需要在onResume()中重复一些相同的事情。这是因为当我们切换到MapActivity并设置位置时,我们将回到相机预览。这意味着onResume()将被调用,从而使它成为更新我们的位置和计算的最佳位置。
清单 7-8。 更新于 Resume
`@Override public void onResume() { super.onResume(); locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 2, locationListener); sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(orientationSensor), SensorManager.SENSOR_DELAY_NORMAL); sensorManager.registerListener(sensorEventListener, sensorManager .getDefaultSensor(accelerometerSensor), SensorManager.SENSOR_DELAY_NORMAL); //Camera camera;
lat = prefs.getFloat("SetLatitude", 0.0f); lon = prefs.getFloat("SetLongitude", 0.0f);
setLoc.setLatitude(lat);
setLoc.setLongitude(lon);
if(locationInUse != null) {
bearing = locationInUse.bearingTo(setLoc);
distance = locationInUse.distanceTo(setLoc);
bearingValue.setText(String.valueOf(bearing));
distanceValue.setText(String.valueOf(distance));
}
else
{
bearingValue.setText("Unable to get your location reliably.");
distanceValue.setText("Unable to get your location reliably.");
}
}`
几乎完全一样,除了如果我们不能得到位置来计算距离和方位,我们也会给出一个消息。
更新的 Android 清单
这基本上结束了这个示例应用。此处未给出的所有文件与第六章中的完全相同。最后一次更新是对AndroidManifest.xml的更新,其中Activity声明已被编辑:
清单 7-9。 更新 AndroidManifest.xml
` <manifest xmlns:android="schemas.android.com/apk/res/and…" package="com.paar.ch7" android:versionCode="1" android:versionName="1.0" >
`
完成的应用
图 7-1–7-5 显示了增强现实模式下的应用,打开了帮助对话框和地图。
图 7-1。 启动时的应用,没有 GPS 定位
图 7-2。 打开带有帮助对话框的 app
图 7-3。 打开带有地图的应用,显示选项菜单
图 7-4。 显示用户当前位置的应用
图 7-5。 该 app 带有到达设定位置的方位和距离。地点定在中国中部,我面朝北。
你可以从这本书的 apress.com 页面或者 GitHub 库获得完整的源代码。
摘要
本章讨论了如何制作导航应用的基本框架。我们允许用户选择地图上的任何一点,然后我们计算用户需要移动的方向作为方位。将它转换为可发布的应用只需要你在增强现实视图中画一个箭头,为用户指出正确的方向。然而,在示例应用中添加这些内容会增加其复杂性,超出本章的范围。
在下一章,你将学习如何设计和实现一个基于标记的增强现实浏览器。*