Android5 高级教程(六)
十九、探索地图和基于位置的服务
在本章中,我们将讨论地图和基于位置的服务。基于位置的服务是 Android SDK 最令人兴奋的部分之一。SDK 的这一部分提供了 API,让应用开发人员可以显示和操作地图,获取实时设备位置信息,并利用其他令人兴奋的功能。当谷歌推出了地图片段和谷歌地图 API 的第二版时,地图的使用发生了巨大的变化。本章将详细介绍创建和操作地图的新方法。
Android 中基于位置的服务设施基于两大支柱:地图和基于位置的 API。Android 中的地图 API 提供了显示和操作地图的工具。比如可以缩放、平移;您可以更改地图模式(例如,从卫星视图到交通视图);您可以向地图添加标记和自定义数据;诸如此类。另一端是全球定位系统(GPS)数据和位置信息,这两者都由位置包处理。
这些 API 通常通过 Google Play Services (设备上的本地 uber 应用)跨越互联网从谷歌服务器调用服务。因此,你通常需要有互联网连接,这些工作。此外,谷歌有一些服务条款,你必须同意这些条款,然后才能使用这些谷歌服务开发应用。仔细阅读条款;谷歌对你可以用服务数据做什么设置了一些限制。例如,您可以将位置信息用于用户的个人用途,但某些商业用途受到限制,例如涉及车辆自动控制的应用。当您注册 Maps API 密钥时,将会看到这些条款。
在本章中,我们将逐一介绍这些软件包。我们将从地图 API 开始,向您展示如何在您的应用中使用地图。正如你将看到的,除了与谷歌地图集成的映射 API 之外,Android 中的映射归结为使用 MapFragment 类。我们还将向您展示如何在您显示的地图上放置自定义数据,以及如何在地图上显示设备的当前位置。在讨论了地图之后,我们将深入研究基于位置的服务,它扩展了地图的概念。我们将向您展示如何使用 Android 地理编码器类和位置服务服务。我们还将触及使用这些 API 时出现的线程问题。
了解制图包
正如我们提到的,地图 API 是 Android 基于位置服务的组件之一。制图包几乎包含了在屏幕上显示地图、处理用户与地图的交互(如缩放)、在地图顶部显示自定义数据等所需的一切。在旧版本的 Android Maps 中,您的应用将直接与谷歌地图服务进行对话,处理所有与地图相关的事情。在新版本中,您的应用必须与 Google Play 服务对话,这是设备上的一个本地应用,作为操作系统的一部分提供。您的应用仍然可以通过互联网拨打电话获取数据,但如果设备上没有本地 Google Play 服务,您的地图将无法使用。如果你需要在没有 Google Play 服务的设备上使用地图,你需要探索一个适用于 Android 的其他地图包(例如 MapQuest)。
为了让您的应用与 Google Play 服务对话,您需要将 Google Play 服务库包含到您的应用中。Android Studio 的做法与 Eclipse 的 ADT 有所不同。请参阅下面的参考资料部分,获取在线说明的链接,了解最新的方法。在您的应用中包含 Google Play 服务库之前,您必须首先通过 SDK 管理器下载它。你会在附加项目下找到它。
您可能已经注意到,除了 Android SDK 平台之外,您的 Android SDK 管理器还显示了 Google API 包。以前,为了使用地图,你必须让你的应用基于 Google APIs 包,但现在不再是这样了。相反,地图 API 集成到 Google Play 服务中,因此您的应用可以基于常规的 Android 包。然而,要在模拟器中测试一个基于地图的应用,你需要将你的模拟器的 Android 虚拟设备(AVD) 基于一个 Google APIs 包。稍后更多关于测试应用的内容。
使用地图包的第一步是显示地图。为此,您将使用 MapFragment (或者 SupportMapFragment ,如果您想要向后兼容 API 12 之前的 Android 版本,也就是 Honeycomb 3.1)。然而,使用这个类需要一些准备工作。具体来说,在使用谷歌地图服务之前,你需要从谷歌获得一个地图 API 密钥。地图 API 键使 Android 能够与谷歌地图服务交互以获取地图数据。下一节将解释如何获取 Maps API 密钥。
从谷歌获取地图 API 密钥
谷歌希望能够识别连接到地图服务的应用。它使用应用包和用于签署应用的证书的组合来生成 Maps API 密钥,应用必须使用该密钥来请求服务。Maps API 密匙可以跨多对包和证书使用。这意味着您可以在开发和生产中使用相同的 Maps API 密钥;包是一样的,但是证书可能是不同的。理论上,您可以在多个应用中使用同一个密钥,但是不鼓励这样做。你无论如何都不想这么做,因为谷歌对地图 API 的使用有一定的限制,而且通过与多个应用共享一个地图 API 密钥,你可以更容易地超过限制。
要获取 Maps API 密钥,您需要用于对应用进行签名的证书(如果是应用的开发版本,则需要调试证书)。您将获得证书的 SHA-1 指纹,然后将它与您的应用包一起输入到 Google 的网站上,以生成一个相关的 Maps API 密钥。
首先,您必须找到由 Eclipse 生成和维护的调试证书。您可以使用 Eclipse IDE 找到确切的位置。如果您使用的是 Eclipse 之外的 IDE,那么您只需要找到保存证书的 keystore 文件。从 Eclipse 的 Preferences 菜单,进入 Android Build。调试证书的位置将显示在默认调试密钥库字段中,如图图 19-1 所示。
图 19-1 。调试证书的位置
要提取 SHA-1 的指纹,您可以使用–列表选项运行 keytool ,如下所示:
keytool -list -alias androiddebugkey -keystore
"FULL PATH OF YOUR debug.keystore FILE" -storepass android -keypass android
注意,您想要从调试存储中获得的别名是 androiddebugkey 。同样,密钥库密码是 android ,私钥密码也是 android 。运行该命令时, keytool 提供指纹(参见图 19-2 )。
图 19-2 。列表选项的 keytool 输出
你会注意到 keytool 命令显示的指纹与图 19-1 所示的首选项屏幕中显示的指纹是一样的,所以你可以从那个屏幕中获取指纹。但是现在您知道了为您的应用提取 SHA-1 指纹的两种方法。当您使用 keytool 提取生产证书的 SHA-1 指纹时,您将使用为生产证书设置的密钥库文件、别名和密码。
下一步是转到 Google 的开发者控制台添加您的应用,然后启用 Maps API。结果将是要包含在应用中的地图 API 密钥。开发者控制台就在这里,你需要一个谷歌账号才能进入:
[`console.developers.google.com`](https://console.developers.google.com)
您需要创建一个新项目。作为创建新项目的一部分,您需要提供项目名称和项目 ID。项目 ID 将预先填充一些奇怪的东西。你可以在这里放任何你想要的值,只要它是唯一的。但是,项目 ID 只是供 Google 开发者控制台使用;它与您的应用的源代码无关。请记住,您正在基于本章示例项目的代码创建一个示例项目,这样您就可以获得一个 Maps API 密钥来查看它的工作情况。
通读服务条款。如果您同意这些条款,请单击“创建”创建您的新项目。这与谷歌建立了一个项目的基本模板。接下来,您将启用您想要的 API。对于地图应用,您将选择谷歌地图 Android API v2。对于本章的示例应用,您还希望包含地理编码 API。你可能会看到一个名为“为你的应用名>配置安卓密钥”的弹出窗口。如果没有弹出窗口,可以在开发人员控制台中导航到项目的 API&authCredentials 部分,并在那里生成一个 API 密匙。在这里,您需要复制并粘贴应用签名证书的 SHA-1 指纹和应用的包名,用分号分隔。包名是源代码中的包名。请注意,您可以在多行中复制,因此如果您有来自生产应用签名证书的 SHA-1 指纹(它通常不同于开发中使用的 androiddebugkey),您可以为生产应用添加第二行。
一旦你按下这个屏幕上的创建按钮,你就会得到一个 API 密匙。这是您将包含在应用的 AndroidManifest.xml 文件中的内容。API 键立即激活,因此您可以开始使用它从 Google 获取地图数据。
将地图 API 密钥添加到您的应用中
要查看如何将 Maps API 键添加到清单文件中,请参见清单 19-1 的底部。
清单 19-1 。简单地图应用的 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
package="com.androidbook.maps.whereami"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="10" android:targetSdkVersion="19" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-feature
android:glEsVersion="0x00020000" android:required="true"/>
<application
android:allowBackup="true" android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.androidbook.maps.whereami.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="AIzaSyBDs1ZQgu9X2A4TG1a7fPl-Ge_MKlyviKM"/>
</application>
</manifest>
正如您无疑已经注意到的那样,清单文件中还有其他一些元素必须存在,地图应用才能正常工作。Maps API 键上方的 <元数据> 标记是必需的,靠近顶部的权限也是必需的。从技术上讲,显示地图不需要 ACCESS_FINE_LOCATION 权限;它在那里,所以定位功能(例如,GPS)将工作。GPS 通常用于地图应用。 ACCESS_NETWORK_STATE 和 INTERNET 权限,因此地图应用可以下载地图切片数据(即地图图形)并了解应用的网络连接类型和状态。有了 WRITE_EXTERNAL_STORAGE 权限,地图应用可以在设备的本地存储空间上创建地图切片文件的本地缓存。如果没有缓存,地图应用可能会花费大量时间反复下载地图切片,这不仅对您的应用来说效率低下,而且会给 Google 服务器带来不必要的负担,并且可能会消耗用户数据计划的很大一部分。最后, glEsVersion 特性之所以存在,是因为在屏幕上渲染地图使用了 OpenGL,因此通过要求该特性,应用可以避免安装在无法显示地图的设备上。
现在,让我们开始玩地图。
了解地图片段
地图应用的基础构件是地图片段。这是在 Honeycomb (Android 3.1)中引入的,取代了的 MapView 和的 MapActivity 功能。现在你可以在一个常规的 Android 活动中嵌入一个 MapFragment 。如果您希望您的应用在运行旧版本 Android 的设备上运行,您可以使用 SupportMapFragment 并将其嵌入到 fragmentation activity 中。 MapFragment 包含显示地图的地图视图,它处理用户手势来操作地图,并且它管理与 Google 服务对话以检索地图数据的后台线程。
MapFragment 是一个非常好的功能包,但它并不是你在设备上使用地图所需的全部。幸运的是,与 Google Play 服务的集成全部为您处理;您所需要做的就是在您的应用的 AndroidManifest.xml 文件中创建一个特殊的条目,您可以在上一节中看到它。
本章的第一个示例应用将简单地向用户显示地图,并让用户探索地图。
注意注意我们在本章末尾给了你一个 URL,你可以从本章下载项目。这将允许您将这些项目直接导入到 IDE 中。还要注意,如果您想用 Android 模拟器测试这些示例,请确保 Android 虚拟设备(AVD)是用 Google APIs 构建的。
请参考名为 WhereAmI 的示例项目。该应用由一个非常基本的 fragmentation activity,一个非常简单的布局,和一个 SupportMapFragment 组成。该示例使用了兼容性类,这意味着它可以在 Gingerbread 设备和最新型号的设备上运行。如果你的应用只需要在比 Honeycomb 3.0 更新的设备上运行,你可以使用一个常规活动和一个 MapFragment 来代替。
清单 19-2 显示了活动。所有需要做的就是设置布局,如果需要,创建 MapFragment 并将其插入到布局的容器中(一个框架布局)。
清单 19-2 。 显示地图的基本分片功能
public class MainActivity extends FragmentActivity {
private static final String MAPFRAGTAG = "MAPFRAGTAG";
private MyMapFragment myMapFrag;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if ((myMapFrag = (MyMapFragment) getSupportFragmentManager()
.findFragmentByTag(MAPFRAGTAG)) == null) {
myMapFrag = MyMapFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.add(R.id.container, myMapFrag, MAPFRAGTAG).commit();
}
}
}
例如,如果由于方向改变而重新创建活动,地图片段将仍然可用,并由 Android 自动附加到新活动。如果找不到地图片段,则表示这是第一次使用,或者地图片段已被破坏,因此创建一个新的地图片段并附加它。没有比这更简单的了。布局源如清单 19-3 所示。它只是一个框架布局,带有一个“容器的 id ,填充了可用的屏幕空间。
清单 19-3 。简单地图显示布局(activity_main.xml)
<FrameLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
xmlns:tools="[`schemas.android.com/tools`](http://schemas.android.com/tools)"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.androidbook.maps.whereami.MainActivity"
tools:ignore="MergeRootFrame" />
如果您将地图片段与其他项目一起包含在您的用户界面中,您可以简单地在您希望地图片段出现的地方使用框架布局,嵌入在其他布局中。唯一剩下的代码是 MapFragment 的代码,如清单 19-4 所示。图 19-3 显示了用户看到的内容。
清单 19-4 。地图片段的代码
public class MyMapFragment extends SupportMapFragment
implements OnMapReadyCallback {
private GoogleMap mMap = null;
public static MyMapFragment newInstance() {
MyMapFragment myMF = new MyMapFragment();
return myMF;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getMapAsync(this);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onResume() {
super.onResume();
doWhenMapIsReady();
}
@Override
public void onPause() {
super.onPause();
if(mMap != null)
mMap.setMyLocationEnabled(false);
}
@Override
public void onMapReady(GoogleMap arg0) {
mMap = arg0;
doWhenMapIsReady();
}
/* We have a race condition where the fragment could resume
* before or after the map is ready. So we put all our logic
* for initializing the map into a common method that is
* called when the fragment is resumed or resuming and the
* map is ready.
*/
void doWhenMapIsReady() {
if(mMap != null && isResumed())
mMap.setMyLocationEnabled(true);
}
}
图 19-3 。显示您位置的基本地图片段
地图应用编程接口的最新发展(2014 年 12 月)是使用回调来让应用知道地图何时可以被操作。使用 getMapAsync() 设置回调,当应用可以使用地图时,调用 onMapReady() 回调。在调用 getMapAsync() 和调用 onMapReady() 回调之间,Android 正在设置通信、线程等。,供图。这意味着当调用 onResume() 时,地图可能准备好了,也可能没有准备好,这告诉片段 UI 正在被显示。因此,应用需要一个单独的方法来处理地图,并且需要由 onResume() 和 onMapReady() 调用。对于这个示例应用, doWhenMapIsReady() 方法充当了这个角色。
应用希望向用户显示设备的当前位置,因此在 doWhenMapIsReady() 中调用了 setMyLocationEnabled() 方法。但是 doWhenMapIsReady() 需要检查映射是否存在以及片段是否正在恢复或者已经恢复。我们不知道哪一个会先发生,但在我们启用位置更新之前,两者都必须是真的。当片段离开视图时,当前位置更新被禁用(参见 onPause() )。另一个需要注意的代码行是 setRetainInstance() 方法调用。由于不需要为活动的配置更改而销毁和重新创建映射,因此保留片段并重用它以及线程和瓦片等是有意义的。您应该记住,配置更改将导致在配置更改期间调用 onPause() 和 onResume() 。这将正确禁用位置更新,并在 onResume() 期间重新启用它们。
地图控件:我的位置、缩放、平移
用户界面上有几个工件值得注意。首先是右上角的 MyLocation 按钮。当您第一次启动示例应用时,您将看到一个非常高级的世界视图。要显示当前位置,请轻按“我的位置”按钮。这将把地图重新定位到当前位置并放大。第二个是蓝点。蓝点代表应用认为你在哪里,圆圈代表它认为这个位置有多准确。该圆可以随着位置信息的改变而增大或缩小。
用户可以使用捏手势(即,将两个手指分开或合在一起)来放大或缩小。用户在地图上可以做的手势更多。通过滑动,用户可以平移地图;也就是说,他们可以移动地图来查看附近的区域。使用两个手指和一个旋转移动,用户可以旋转地图。简单地创建一个 MapFragment 就可以自动实现很多功能。
这些地图控件和更多控件包含在一个 UiSettings 类的对象中。您可以通过调用 GoogleMap 对象上的 getUiSettings() 来获得地图的 UiSettings (即示例应用中的 mMap )。然后,您可以通过编程方式修改这些设置。例如,您可以使指南针显示在地图上,或者您可以启用/禁用缩放加/减控件,使其显示或不显示。缩放加/减控件出现在右下角,允许用户通过分别点击加或减按钮来放大或缩小。
地图类型
默认地图类型为 MAP_TYPE _NORMAL 。这是在图 19-3 的中使用的类型。它显示了道路与土地的基本特征,如水在哪里,绿地在哪里,以及一些地方和建筑物。MAP_TYPE_SATELLITE 显示地面的摄影卫星视图,因此用户能够看到真实的建筑物、汽车,甚至人。 MAP_TYPE_HYBRID 是这两者的结合;MAP_TYPE_TERRAIN 类似于普通地图,但添加了地形特征,如山脉和峡谷。要真正看到 MAP_TYPE_TERRAIN 的效果,放大科罗拉多州博尔德这样的地方,将地图设置为地形。
您使用一个 GoogleMap 的 setMapType() 方法来改变类型。
添加流量图层
在之前版本的安卓地图中,交通就像地图的卫星和普通模式一样。在 API v2 中,流量是使用 GoogleMap 的 setTrafficEnabled() 方法单独启用的。
地图切片
当您的应用显示地图时,了解正在发生的事情会很有帮助。谷歌已经创建了数以百万计的底图来代表地球表面。在最低缩放级别(即,零),有一个平铺显示整个世界。在缩放级别 1,有四个 2x2 配置的单幅图块。在缩放级别 2,4x4 配置中有 16 个单幅图块。依此类推直到缩放级别 21。根据您想要显示世界的哪个部分以及缩放级别,GoogleMap 对象将获取并缓存适当的图块。平移到侧面,将获取并显示任何附加的图块。平移回您所在的位置,您的应用可以从缓存中检索地图切片,而不是往返于服务器之间。
有趣的是,普通类型地图的底图切片不是图像。谷歌想出了一种压缩的方式来描述瓷砖的形状和颜色,而不是只发送每个瓷砖的图像。因此,普通地图切片在缓存空间和网络带宽方面非常有效。另一方面,卫星图像块没有被压缩,因为它们是图像。
现在你可以理解为什么有时一个地图应用会显示一个灰色的网格图案,看起来可以工作,但不会显示街道和其他项目。GoogleMap 对象已经被实例化,它知道缩放级别以及应该在哪里显示地图,但是它无法检索并向用户呈现图块。这通常是由于无效的地图 API 键,或者 API 键设置不正确。但这也意味着很难到达谷歌地图服务器。但是,如果地图切片已经被缓存,即使 Google 的切片服务器不可达,这些切片也可以呈现给用户。地图切片缓存有两个不幸之处。首先,没有 API 调用来管理地图切片缓存,无论是强制缓存地图切片,还是更改缓存大小,或者从缓存中移除切片。你只需要相信谷歌会做正确的事情。第二个是每个应用都会缓存地图切片。因此,仅仅因为 Google Maps 应用可能缓存了切片,您的应用就无法访问这些切片。您的应用只能看到其缓存的缓存切片。
向地图添加标记
通常你会想要在地图上确定感兴趣的点,这是使用标记来完成的。这些点可以是静止的物体,如地址、地标或停车位。但它们也可能是移动的物体,如汽车、飞机、人、宠物、风暴等。您可以选择标记的外观以及它在地图上的位置。你可以同时拥有许多标记。我们将修改上面的示例程序,使其包含几个标记。您将看到如何放置它们,然后如何操作视图以确保用户看到标记。
现在使用名为 WhereAmIMarkers 的示例程序。您需要像以前一样修改 AndroidManifest.xml 文件,以使用您的 Maps API 键。MyMapFragment.java 的的源代码已经被修改,如清单 19-5 所示。屏幕将出现类似于图 19-4 的画面。
清单 19-5 。显示标记的地图片段代码
public class MyMapFragment extends SupportMapFragment
implements OnMapReadyCallback {
public static MyMapFragment newInstance() {
MyMapFragment myMF = new MyMapFragment();
return myMF;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getMapAsync(this);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
}
@Override
public void onMapReady(GoogleMap myMap) {
LatLng disneyMagicKingdom = new LatLng(28.418971, -81.581436);
LatLng disneySevenLagoon = new LatLng(28.410067, -81.583699);
// Add a marker
MarkerOptions markerOpt = new MarkerOptions()
.draggable(false)
.flat(false)
.position(disneyMagicKingdom)
.title("Magic Kingdom")
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE));
myMap.addMarker(markerOpt);
markerOpt.position(disneySevenLagoon)
.title("Seven Seas Lagoon");
myMap.addMarker(markerOpt);
// Derive a bounding box around the markers
LatLngBounds latLngBox = LatLngBounds.builder()
.include(disneyMagicKingdom)
.include(disneySevenLagoon)
.build();
// Move the camera to zoom in on our locations
myMap.moveCamera(CameraUpdateFactory.newLatLngBounds(latLngBox, 200, 200, 0));
}
}
图 19-4 。地图上的标记
再一次,一切从从 MapFragment 获取 GoogleMap 对象开始。一旦地图可用,您就可以创建标记,在这种情况下,标记是基于几个固定的锁定对象。不过你会注意到,你没有直接实例化一个标记对象。相反,您使用一个 MarkerOptions 对象来指定应该如何创建标记。在标记选项对象中,您可以决定位置、标题、标记形状、颜色等。虽然您可以实例化一个 Marker 对象,然后调用您想要的每个 setter,但是 MarkerOptions 使事情变得容易得多,尤其是如果您需要创建共享公共特性的多个标记。该示例仅使用了一些标记选项功能;请参阅参考文档以了解所有可用的选项。
接下来你可能要做的是向用户展示地图,这样所有的标记都可以同时看到。这需要做两件事:将地图放在标记的中间,将缩放级别设置得尽可能高,但不要太近,以至于无法将所有标记都放入视图中。幸运的是,有一个助手类可用于此目的。通过向其传递所有应该在视图内的倾斜点来创建倾斜对象,并计算包含所有这些点的最小盒子。在这个示例中,两个点都是同时传入的。您也可以使用一个循环来传递所有点,然后调用 build() 方法来返回边界框。
一旦你有了一个边界框,你需要调整地图的相机。在旧版本的谷歌地图中,只有地图的俯视图,就好像你在地图上方俯视一样。在 Maps API 第 2 版中,有一个摄像头的概念,它可以直接向下看,但也可以从一个角度看。如果你同时使用两个手指,从上到下滑动屏幕,你会看到视角的变化。您实际上已经旋转了摄像机,因此您不再直视下方。当相机倾斜时,它还可以向东、向南或任何其他方向看。你也可以转动两个手指来旋转地图。
所有这些摄像机角度、缩放级别等等都是使用地图的 animateCamera() 或 moveCamera() 方法来控制的。这些方法以一个 CameraUpdate 对象作为指令,而 CameraUpdateFactory 类生成这些指令。在该示例中,边界框被传递给 CameraUpdateFactory ,它返回一个适当的 CameraUpdate ,这样摄像机将被定位在最佳位置以查看所有标记。 CameraUpdateFactory 还有其他几种方法来适应其他定位摄像机的方式。你可以简单的用 zoomIn() 和 zoomOut() 来举例。您也可以创建一个 CameraPosition 对象并使用它。
总而言之,你会同意在地图上放置标记再简单不过了。或者可能吗?我们没有纬度/经度对的数据库,但是我们猜测我们将需要以某种方式使用真实地址创建一个或多个锁存对象。这时你可以使用地理编码器类,它是我们接下来要讨论的位置包的一部分。
了解位置包
android.location 包为基于位置的服务提供了便利。在这一节中,我们将讨论这个包的两个重要部分:地理编码器类和位置管理器服务。我们将从地理编码器开始。
使用 Android 进行地理编码
如果你要用地图做任何实际的事情,你可能必须将一个地址(或位置)转换成一个纬度/经度对。这个概念被称为地理编码,而 Android . location . geocoder 类提供了这个功能。事实上, Geocoder 类提供了向前和向后转换——它可以接受一个地址并返回一个纬度/经度对,它可以将一个纬度/经度对转换成一个地址列表。 该类提供了以下方法:
- 列表<地址> getFromLocation(双纬度,双经度,int maxResults)
- listgetfromlocation name(string location name,int maxResults,double lowerLeftLatitude,double lowerleftlongitude,double upperRightLatitude,double upper right length)
- listgetfromlocation name(string location name,int maxResults)
事实证明,计算地址并不是一门精确的科学,因为描述位置的方式多种多样。例如, getFromLocationName() 方法可以接受一个地点的名称、物理地址、机场代码,或者只是一个众所周知的地点名称。因此,这些方法返回地址列表,而不是单个地址。因为这些方法会返回一个列表,这个列表可能会很长(并且需要很长时间才能返回),所以建议您通过为 maxResults 提供一个范围在 1 和 5 之间的值来限制结果集。现在,让我们考虑一个例子。
清单 19-6 显示了图 19-5 中显示的活动和地图片段的 XML 布局和相应代码。要运行该示例,您需要用自己的 Maps API 密钥更新清单。
清单 19-6 。 使用 Android 地理编码器类
<!-- This is activity_main.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
xmlns:tools="[`schemas.android.com/tools`](http://schemas.android.com/tools)"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.androidbook.maps.whereami.MainActivity"
tools:ignore="MergeRootFrame" >
<EditText android:id="@+id/locationName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter location name"
android:inputType="text"
android:imeOptions="actionGo" />
<FrameLayout android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
/**
* This is from MainActivity.java
**/
public class MainActivity extends FragmentActivity {
private static final String MAPFRAGTAG = "MAPFRAGTAG";
MyMapFragment myMapFrag = null;
private Geocoder geocoder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if ((myMapFrag = (MyMapFragment) getSupportFragmentManager()
.findFragmentByTag(MAPFRAGTAG)) == null) {
myMapFrag = MyMapFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.add(R.id.container, myMapFrag, MAPFRAGTAG).commit();
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD
&& !Geocoder.isPresent()) {
Toast.makeText(this, "Geocoder is not available on this device",
Toast.LENGTH_LONG).show();
finish();
}
geocoder = new Geocoder(this);
EditText loc = (EditText)findViewById(R.id.locationName);
loc.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
String locationName = v.getText().toString();
try {
List<Address> addressList =
geocoder.getFromLocationName(locationName, 5);
if(addressList!=null && addressList.size()>0)
{
// Log.v(TAG, "Address: " + addressList.get(0).toString());
myMapFrag.gotoLocation(new LatLng(
addressList.get(0).getLatitude(),
addressList.get(0).getLongitude()),
locationName);
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
});
}
}
public class MyMapFragment extends SupportMapFragment
implements OnMapReadyCallback {
private GoogleMap mMap = null;
public static MyMapFragment newInstance() {
MyMapFragment myMF = new MyMapFragment();
return myMF;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getMapAsync(this);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
}
public void gotoLocation(LatLng latlng, String locString) {
if(mMap == null)
return;
// Add a marker for the given location
MarkerOptions markerOpt = new MarkerOptions()
.draggable(false)
.flat(false)
.position(latlng)
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
.title("You chose:")
.snippet(locString);
// See the onMarkerClicked callback for why we do this
mMap.addMarker(markerOpt);
// Move the camera to zoom in on our location
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latlng, 15));
}
@Override
public void onMapReady(GoogleMap arg0) {
mMap = arg0;
}
}
图 19-5 。地理编码到一个给定位置名称的点
要演示 Android 中地理编码的使用,请在编辑文本字段中键入一个位置的名称或地址,然后点击键盘上的 Go 按钮。为了找到一个位置的地址,我们调用地理编码器的 getfromclocationname()方法。该位置可以是地址或众所周知的名称,如“白宫”地理编码可能是一项耗时的操作,因此我们建议您将结果限制为五个,正如 Android 文档所建议的那样。
对 getFromLocationName() 的调用返回一个地址列表。示例应用获取地址列表,并处理找到的第一个地址。每个地址都有一个纬度和经度,您可以用它来创建一个标签。然后调用我们的 gotoLocation() 方法来导航到这个点。地图片段中的这个新方法创建了一个新标记,将其添加到地图中,并将相机移动到缩放级别为 15 的标记处。缩放级别可以设置为 1 到 21 之间的浮点数,包括 1 和 21。当您从 1 向 21 移动 1 时,缩放级别会增加 2 倍。如果我们愿意,我们可以显示一个对话框来显示多个找到的位置,但是现在,我们只显示返回给我们的第一个位置。
在我们的示例应用中,我们只读取返回的地址的纬度和经度。事实上,可以有大量关于地址 es 的数据返回给我们,包括该地点的通用名称、街道、城市、州、邮政编码、国家,甚至电话号码和网站 URL。
您应该了解与地理编码相关的几点:
- 虽然地理编码器类可能存在,但服务可能没有实现。如果设备是 Gingerbread 或更高版本,在尝试在您的应用中进行地理编码之前,您应该检查一下 Geocoder.isPresent() 。
- 返回的地址并不总是准确的地址。显然,因为返回的地址列表取决于输入的准确性,所以您需要尽一切努力向地理编码器提供准确的位置名称。
- 尽可能将 maxResults 参数设置为介于 1 和 5 之间的值。
- 您应该认真考虑在不同于 UI 线程的线程中执行地理编码操作。这有两个原因。第一个是显而易见的:操作很耗时,您不希望在进行地理编码时 UI 挂起,导致 Android 终止您的活动。第二个原因是,对于移动设备,您总是需要假设网络连接可能会丢失,并且连接很弱。因此,您需要适当地处理输入/输出(I/O)异常和超时。计算完地址后,您可以将结果发送到 UI 线程。请参阅附带的名为 WhereAmIGeocoder2 的示例应用,了解如何做到这一点。
了解定位服务
定位服务提供了两个主要功能:一个是让您获得设备地理位置的机制,另一个是在设备进入或退出指定地理位置时通知您(通过意向)的工具。后一种操作被称为地理围栏。
在本节中,您将学习如何找到设备的当前位置。要使用该服务,您必须首先获得对它的引用。清单 19-7 显示了服务的一个简单用法。这个示例项目叫做 wheremilocationapi。
清单 19-7 。使用位置提供者 API
public class MyMapFragment extends SupportMapFragment
implements GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
OnMapReadyCallback {
private Context mContext = null;
private GoogleMap mMap = null;
private GoogleApiClient mClient = null;
private LatLng mLatLng = null;
public static MyMapFragment newInstance() {
MyMapFragment myMF = new MyMapFragment();
return myMF;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getMapAsync(this);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(mClient == null) { // first time in, set up this fragment
setRetainInstance(true);
mContext = getActivity().getApplication();
mClient = new GoogleApiClient.Builder(mContext, this, this)
.addApi(LocationServices.API)
.build();
mClient.connect();
}
}
@Override
public void onConnectionFailed(ConnectionResult arg0) {
Toast.makeText(mContext, "Connection failed", Toast.LENGTH_LONG).show();
}
@Override
public void onConnected(Bundle arg0) {
// Figure out where we are (lat, long) as best as we can
// based on the user's selections for Location Settings
FusedLocationProviderApi locator = LocationServices.FusedLocationApi;
Location myLocation = locator.getLastLocation(mClient);
// if the services are not available, could get a null location
if(myLocation == null)
return;
double lat = myLocation.getLatitude();
double lng = myLocation.getLongitude();
mLatLng = new LatLng(lat, lng);
doWhenEverythingIsReady();
}
@Override
public void onConnectionSuspended(int arg0) {
Toast.makeText(mContext, "Connection suspended", Toast.LENGTH_LONG).show();
}
@Override
public void onMapReady(GoogleMap arg0) {
mMap = arg0;
doWhenEverythingIsReady();
}
private void doWhenEverythingIsReady() {
if(mMap == null || mLatLng == null)
return;
// Add a marker
MarkerOptions markerOpt = new MarkerOptions()
.draggable(false)
.flat(true)
.position(mLatLng)
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE));
mMap.addMarker(markerOpt);
// Move the camera to zoom in on our location
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(mLatLng, 15));
}
}
要获得位置服务,首先需要创建一个 Google API 客户端对象,它使您可以使用 Google Play 服务中的服务。这相对容易做到,一旦有了客户端对象,就需要调用它的 connect() 方法。这将在稍后异步调用 onConnected() 回调,让您的应用知道客户端已经连接,现在可以使用了。或者您的应用可能会得到 onConnectionFailed() 回调,在这种情况下,您应该采取适当的措施。对于这个示例,我们只是在连接尝试失败时显示一条 Toast 消息。稍后,您将看到如何更可靠地处理失败的连接。
当调用 onConnected() 回调时,现在您可以使用位置提供者 API 了。回想一下,在本章开始时,您在清单文件中设置了访问位置信息的权限。精确定位使用 GPS,而粗略定位使用手机信号塔和 WiFi 热点。使用融合的位置提供者 API 意味着您的应用不必担心启用了什么或设置了什么权限。API 调用是相同的。你只需询问位置,就能得到当时可用的最佳位置信息。
对于这个示例,我们调用 getLastLocation() 方法。运气好的话,返回的位置非常当前;但是,请注意,最后一次定位可能是在几分钟或几小时前。 Location 对象可以通过 getTime() 方法告诉您何时获得了这个定位。在决定使用它之前,您可以检查一下它是否足够新。从技术上讲, getLastLocation() 可能会返回 null,因此您也应该为这种情况做好准备。如果在“设置”中禁用了定位服务,就会发生这种情况。
您将很快看到如何更新位置。目前,该示例获取最后一个位置,并从中创建一个地图标记以显示给用户。您应该认识本章前面部分创建标记的代码。
如何启用定位服务
如果应用运行时没有打开定位服务,您可能会认为有一个简单的 API 来启用定位服务。不幸的是,事实并非如此。要打开定位服务,用户必须在设备的设置屏幕中打开。您的应用可以通过启动特定的设置屏幕来简化用户的操作。location settings source 屏幕实际上只是一个活动,这个活动是为了响应一个意图而设置的。
在刚刚介绍的示例应用中,您将在活动的 onCreate() 回调中看到来自清单 19-8 的代码。
清单 19-8 。检查定位服务是否开启
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if ((myMapFrag = (MyMapFragment) getSupportFragmentManager()
.findFragmentByTag(MAPFRAGTAG)) == null) {
myMapFrag = MyMapFragment.newInstance();
getSupportFragmentManager().beginTransaction()
.add(R.id.container, myMapFrag, MAPFRAGTAG).commit();
}
if(!isLocationEnabled(this)) {
// no location service providers are enabled
Toast.makeText(context, "Location Services appear to be turned off." +
" This app can't work without them. Please turn them on.",
Toast.LENGTH_LONG).show();
startActivityForResult(new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS), 0);
}
}
@SuppressWarnings("deprecation")
public boolean isLocationEnabled(Context context) {
int locationMode = Settings.Secure.LOCATION_MODE_OFF;
String locationProviders;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
try {
locationMode = Settings.Secure.getInt(
context.getContentResolver(),
Settings.Secure.LOCATION_MODE);
} catch (SettingNotFoundException e) {
e.printStackTrace();
}
return locationMode != Settings.Secure.LOCATION_MODE_OFF;
}else{
locationProviders = Settings.Secure.getString(
context.getContentResolver(),
Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
return !TextUtils.isEmpty(locationProviders);
}
}
Android 19 (KitKat)发生了一个变化,在静态设置中加入了新的设置值。安全级。这使得判断定位服务是否打开以及哪些服务打开变得更加容易,但用户仍然需要做一些工作来启用这些服务。这段代码中有两种方法来检查服务:使用其中一个新值,或者获取可用的位置提供者。清单 19-8 的第一部分检查 Android 版本是否是 KitKat 或更高版本,如果是,它寻找位置模式的新设置值。代码的第二部分(如果 Android 版本比 KitKat 旧)对允许的位置提供者进行获取。如果定位模式未关闭,或者至少有一个定位供应器可用,则定位服务正在运行。如果没有,此代码将启动位置设置屏幕。此时,在设置活动运行时,此活动将会暂停。设置活动完成后,我们的活动将继续。
如果您想要处理来自设置活动的响应(即,当该活动完成时得到通知,并且可能已经进行了设置更改),您必须在您的活动中实现 onActivityResult() 回调。还要记住,虽然你希望用户打开定位服务,但他们可能不会。您需要再次检查用户是否启用了定位服务,并根据结果采取适当的措施。我们将在后面的小节中向您展示如何完成所有这些工作。
位置供应器
您已经看到了 FusedLocationApi,但是您还应该知道更老的替代位置提供者。硬件就在设备上,用于获取位置信息,位置提供者会将它提供给你的应用。您将很快看到 FusedLocationApi 如何在比这些提供者更高的层次上处理您的位置需求。但是如果您需要深入了解细节,例如检查可用 GPS 卫星的状态,您会很高兴知道这些供应器的存在。谷歌建议大家改用 FusedLocationApi 但由于它依赖于 Google Play 服务,这意味着使用 FusedLocationApi 的应用将无法在非谷歌 Android 设备上运行。
location manager 服务是一个系统级服务。系统级服务是使用服务名从上下文中获得的服务;你不能直接实例化它们。 android.app.Activity 类提供了一个名为 getSystemService() 的实用方法,您可以使用它来获得系统级服务。您调用 getSystemService() 并传入您想要的服务的名称,在本例中是 Context。位置 _ 服务。你很快会在清单 19-9 中看到这一点。
LocationManager 服务通过使用位置提供者来提供地理位置细节。目前,有三种类型的位置供应器:
- GPS 供应商使用全球定位系统来获取位置信息。
- 网络供应器使用手机信号塔或 WiFi 网络来获取位置信息。
- 被动提供者就像一个位置更新嗅探器,它将其他应用请求的位置更新传递给你的应用,而你的应用不需要特别请求任何位置更新。当然,如果没有其他人要求位置更新,你也不会得到任何。
类似于 FusedLocationApi, LocationManager 类可以提供设备的最后已知位置,这次是通过 getLastKnownLocation() 方法。位置信息是从提供者处获得的,因此该方法将您想要使用的提供者的名称作为参数。提供者名称的有效值是 LocationManager。GPS_PROVIDER , LocationManager。网络供应器和位置管理器。被动 _ 提供者。请注意,对于融合提供者没有选项,因为这是一个单独的定位功能。
为了让您的应用成功获取位置信息,它必须在 AndroidManifest.xml 文件中拥有适当的权限。Android . permission . access _ FINE _ LOCATION 是 GPS 和被动供应器所必需的,而 Android . permission . access _ COARSE _ LOCATION 或 Android . permission . access _ FINE _ LOCATION 可用于网络供应器,具体取决于您的需求。例如,假设您的应用将使用 GPS 或网络数据进行位置更新。因为您需要 ACCESS_FINE_LOCATION 用于 GPS,您也已经满足了网络访问的权限,所以您不需要同时指定 ACCESS_COARSE_LOCATION 。如果您只打算使用网络提供者,那么您可以只使用清单文件中的 ACCESS_COARSE_LOCATION 来解决问题。
调用 getLastKnownLocation() 返回一个 Android . location . location 实例,如果没有可用的位置,则返回 null 。 Location 类提供位置的纬度和经度、计算位置的时间,还可能提供设备的高度、速度和方位。一个 Location 对象还可以使用 getProvider() 告诉你它来自哪个提供者,这个提供者可能是 GPS_PROVIDER 或 NETWORK_PROVIDER 。如果你通过 PASSIVE_PROVIDER 获取位置更新,请记住,你实际上只是嗅探位置更新,所以所有更新最终都来自 GPS 或网络。
因为 LocationManager 操作提供者,所以该类提供 API 来获取提供者。例如,您可以通过调用 getAllProviders() 获得所有已知的提供者。您可以通过调用 getProvider() ,将提供者的名称作为参数传递(例如 LocationManager)来获得特定的提供者。GPS_PROVIDER )。需要注意的一点是 getAllProviders() 将返回您可能无法访问或当前被禁用的提供程序。幸运的是,您可以使用其他方法来确定提供者的状态,例如 isProviderEnabled(String providerName)或 get providers(boolean enabled only),您可以使用值 true 来调用这些方法,以便只获取您可以立即使用的提供者。
还有另一种方法来获得合适的提供者,那就是使用 LocationManager 的 get providers(Criteria Criteria,boolean enabledOnly) 方法。通过指定位置更新的标准,并通过将 enabledOnly 设置为 true 以便您获得已启用并准备就绪的供应器,您可以获得返回给您的供应器名称列表,而不必知道您获得了哪个供应器的具体信息。这可能更便于携带,因为一个设备可能有一个自定义的 LocationProvider 来满足您的需求,而无需您事先了解它。标准对象可以设置参数,包括精度水平和对速度、方位、高度、成本和功率要求等信息的需求。如果没有供应器符合您的标准,将返回一个空列表,允许您退出或放宽标准并重试。
向您的应用发送位置更新
在进行开发测试时,您的应用需要位置信息,而模拟器无法访问 GPS 或手机信号塔。为了在模拟器中测试您的应用,您可以从 Eclipse 手动发送位置更新。清单 19-9 展示了一个简单的例子来说明如何做到这一点。这里我们将坚持使用 LocationManager 方法,稍后将展示 FusedLocationApi 方法。
清单 19-9 。注册位置更新
public class LocationUpdateDemoActivity extends Activity
{
LocationManager locMgr = null;
LocationListener locListener = null;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
locMgr = (LocationManager)
getSystemService(Context.LOCATION_SERVICE);
locListener = new LocationListener()
{
public void onLocationChanged(Location location)
{
if (location != null)
{
Toast.makeText(getBaseContext(),
"New location latitude [" +
location.getLatitude() +
"] longitude [" +
location.getLongitude()+"]",
Toast.LENGTH_SHORT).show();
}
}
public void onProviderDisabled(String provider)
{
}
public void onProviderEnabled(String provider)
{
}
public void onStatusChanged(String provider,
int status, Bundle extras)
{
} };
}
@Override
public void onResume() {
super.onResume();
locMgr.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
0, // minTime in ms
0, // minDistance in meters
locListener);
}
@Override
public void onPause() {
super.onPause();
locMgr.removeUpdates(locListener);
}
}
我们没有为这个例子显示用户界面,所以标准的初始布局 XML 文件就可以了,还有一个常规的活动。
服务的主要用途之一是接收设备位置的通知。清单 19-9 展示了如何注册一个监听器来接收位置更新事件。要注册一个侦听器,您需要调用 requestLocationUpdates() 方法,将提供者类型作为参数之一传递。当位置改变时, LocationManager 用新的位置调用监听器的 onLocationChanged() 方法。在适当的时候删除任何位置更新的注册非常重要。在我们的示例中,我们在 onResume() 中进行注册,并在 onPause() 中删除该注册。如果我们不能在位置更新上做任何事情,我们应该告诉供应器不要发送它们。我们的活动也有可能被破坏(例如,如果用户旋转他们的设备,我们的活动重新启动),在这种情况下,我们的旧活动可能仍然存在,接收更新,用 Toast 显示它们,并占用内存。
在我们的例子中,我们将最小时间和最小距离设置为零。这告诉 LocationManager 尽可能频繁地给我们发送更新。这些并不是您的生产应用或真实设备所需要的设置,但是我们在这里使用它们来使演示在模拟器中更好地运行。(在现实生活中,你不会希望硬件如此频繁地试图找出我们的当前位置,因为这会耗尽电池。)根据情况适当地设置这些值,尽量减少您真正需要得到位置变化通知的频率。谷歌通常推荐不小于 20 秒的值。
用模拟器测试定位应用
让我们在模拟器中测试这一点,使用 Eclipse 的 ADT 插件附带的 Dalvik Debug Monitor Service (DDMS)透视图。DDMS 用户界面提供了一个屏幕,让你将仿真器发送到一个新的位置(见图 19-6 )。
图 19-6 。使用 Eclipse 中的 DDMS UI 向模拟器发送位置数据
要在月食中到达 DDMS,使用窗口打开视角
DDMS。模拟器控件视图应该已经为你准备好了,但是如果没有,使用窗口
显示视图
其他
Android
模拟器控件使它在这个透视图中可见。您可能需要在模拟器控件中向下滚动,以找到位置控件。如图图 19-6 所示,DDMS 用户界面中的手动选项卡允许你发送一个新的 GPS 位置(纬度/经度对)到模拟器。发送一个新位置将触发监听器上的 onLocationChanged() 方法,这将导致向用户发送一条传达新位置的消息。
你可以使用其他几种技术向模拟器发送位置数据,如 DDMS 用户界面所示(见图 19-6 )。例如,DDMS 界面允许您提交 GPS 交换格式(GPX)文件或锁眼标记语言(KML)文件。您可以从以下站点获得 GPX 文件示例:
同样,您可以使用以下 KML 资源来获取或创建 KML 文件:
注意一些网站提供 KMZ 文件。这些是压缩的 KML 文件,所以只需解压它们就可以得到 KML 文件。一些 KML 文件需要修改它们的 XML 名称空间值才能在 DDMS 正常播放。如果您在使用某个 KML 文件时遇到问题,请确保该文件包含以下内容:
<KML font name 1 ">earth.google.com/kml/2.x
>。
您可以上传一个 GPX 或 KML 文件到模拟器,并设置模拟器回放文件的速度(见图 19-7 )。然后,模拟器将根据配置的速度向您的应用发送位置更新。如图图 19-7 所示,一个 GPX 文件包含点(显示在顶部)和路径(显示在底部)。你不能播放一个点,但是当你点击一个点时,它会被发送到模拟器。你点击一个路径,然后播放按钮将被激活,这样你就可以播放点。
图 19-7 。将 GPX 和 KML 的文件上传到模拟器进行回放
注意有报道称,并非所有的 GPX 文件都能被模拟器控件理解。如果您尝试加载 GPX 文件,但没有任何反应,请尝试从不同的源加载不同的文件。
清单 19-9 包括一些我们还没有提到的 LocationListener 的附加方法。分别是回调 onProviderDisabled() 、回调 onProviderEnabled() 和回调 onStatusChanged() 。对于我们的示例,我们没有做任何事情,但是在您的应用中,当用户禁用或启用某个位置提供者(如 gps )时,或者当某个位置提供者的状态发生变化时,您会收到通知。状态包括停用、暂时不可用和可用。即使启用了提供者,也不意味着它将发送任何位置更新,您可以使用状态来判断这一点。注意,如果为一个被禁用的提供者调用了 requestLocationUpdates(),那么 onProviderDisabled() 将被立即调用。
从模拟器控制台发送位置更新
Eclipse 有一些易于使用的工具可以将位置更新发送到您的应用,但是还有另外一种方法。您可以从工具窗口中使用以下命令启动模拟器控制台:
telnet localhost emulator_port_number
其中 emulator_port_number 是与已经运行的 AVD 实例相关联的编号,显示在模拟器窗口的标题栏中。如果您的工作站没有 telnet,您可能需要安装它。一旦你连接上了,你可以使用 geo fix 命令发送位置更新。要发送纬度/经度坐标和高度(高度是可选的),请使用以下形式的命令:
geo fix lon lat [ altitude ]
例如,以下命令将把佛罗里达州杰克逊维尔的位置发送到您的应用,该位置的海拔高度为 120 米。
geo fix -81.5625 30.334954 120
请仔细注意 geo fix 命令的参数顺序。经度是第一个自变量,纬度是第二个。
你能用一个位置做什么?
如前所述,位置 s 可以告诉你纬度和经度,何时计算出位置,计算这个位置的提供者,以及可选的高度、速度、方位和精度水平。根据位置来自的提供者,也可能有额外的信息。例如,如果位置来自 GPS 供应器,则有一个额外的捆绑包会告诉您使用了多少颗卫星来计算位置。可选值可能存在,也可能不存在,具体取决于提供程序。为了知道一个位置是否具有这些值之一,位置类提供了一组具有...()返回一个布尔值的方法,例如 hasAccuracy() 。在依赖 getAccuracy() 的返回值之前,先调用 hasAccuracy() 会比较明智。
Location 类还有一些其他有用的方法,包括一个静态方法 distanceBetween() ,它将返回两个 Location 之间的最短距离。另一个与距离相关的方法是 distanceTo() ,它将返回当前 Location 对象和传递给该方法的 Location 对象之间的最短距离。请注意,距离以米为单位,距离计算考虑了地球的曲率。但是也要知道,距离并不是以你必须开车去的距离来提供的。
如果你想得到驾驶方向或驾驶距离,你将需要有你的开始和结束地点 ??,但是为了进行计算,你可能需要使用谷歌方向 API。方向 API 将允许您的应用显示如何从起点到达终点。这是您可以为您的应用启用的另一个 Google API 客户端 API。
设置 Google Play 服务的位置更新
您已经看到了如何使用 LocationManager 获取位置更新,但是让我们返回到 FusedLocationProviderApi,看看如何从中获取位置更新。本节的示例项目是 FusedLocationApiUpdates。这一个有点棘手,因为我们正在处理 Google Play 服务,一个运行在设备上的独立服务。因此,您不能总是确保您具有有效的客户端连接,并且在请求位置更新时需要小心。因此,您的应用需要考虑状态。
在早期的示例程序(WhereAmILocationAPI)中,您检查了定位服务是否打开,但是代码假定 Google Play 服务可用并且准备好了。现在您将看到如何检查 Google Play 服务的存在,以及 GooglePlayServicesUtil 类如何帮助您。基本流程是检查每个位置更新发生的依赖关系,如果有纠正问题的方法,就帮助用户修复它。如果用户没有或者不能解决问题,应用就会退出。如果用户一直在解决问题,直到一切正常,然后位置更新被请求,应用通过 Toast 消息显示位置更新。
清单 19-10 显示了我们尝试连接的主要方法。您将在该方法中看到与前面的 WhereAmILocationAPI 示例应用相同的位置服务检查。将从活动的 onResume() 回调中调用 tryToConnect() 方法,以便每次恢复该活动时,都将建立一个新的客户端连接到 Google Play 服务。我们不想假设一个老客户仍然是有效的和活跃的。
清单 19-10 。检查进行位置更新的能力
private void tryToConnect() {
// Check that Google Play services is available
int resultCode = GooglePlayServicesUtil
.isGooglePlayServicesAvailable(this);
// If Google Play services is available, then we're good
if (resultCode == ConnectionResult.SUCCESS) {
Log.d(TAG, "Google Play services is available.");
if(!isLocationEnabled(this)) {
if(lastFix == FIX.LOCATION_SETTINGS) {
// Since we're coming through again, it means
// recovery didn't happen. Time to bail out.
Log.e(TAG, "Location settings didn't work");
finish();
}
else {
// no location service providers are enabled
Toast.makeText(this, "Location Services are off. " +
"Can't work without them. Please turn them on.",
Toast.LENGTH_LONG).show();
Log.i(TAG, "Location Services need to be on. " +
"Launching the Settings screen");
startActivityForResult(new Intent(
android.provider.Settings
.ACTION_LOCATION_SOURCE_SETTINGS),
LOCATION_SETTINGS_REQUEST);
lastFix = FIX.LOCATION_SETTINGS;
}
}
else {
client.connect();
Log.v(TAG, "Connecting to GoogleApiClient...");
}
}
// Google Play services was not available for some reason
// See if the user can do something about it
else if(GooglePlayServicesUtil
.isUserRecoverableError(resultCode)) {
if(lastFix == FIX.PLAY_SERVICES) {
// Since we're coming through again, it means
// recovery didn't happen. Time to bail out.
Log.e(TAG, "Recovery doesn't seem to work");
finish();
}
else {
Log.d(TAG, "Google Play services may be available. " +
"Asking user for help");
// This form of the dialog call will result in either a
// callback to onActivityResult, or a dialog onCancel.
GooglePlayServicesUtil.showErrorDialogFragment(resultCode,
this, PLAY_SERVICES_RECOVERY_REQUEST, this);
lastFix = FIX.PLAY_SERVICES;
}
} else {
// No hope left.
Log.e(TAG, "Google Play Services is/are not available." +
" No point in continuing");
finish();
}
}
GooglePlayServicesUtil 类有几个静态方法来帮助设置位置更新。第一个方法是 isGooglePlayServicesAvailable(),需要一个上下文。结果是一个整数值,该整数值或者是成功,或者是其他几个值中的一个,这些值可以指示例如服务缺失或者版本不合适。在大多数情况下,您真的不需要关心返回的其他值,您将会看到。
如果 Google Play 服务可用,您将检查位置服务(如前所述),如果它们正常,您可以在 GoogleApiClient 客户端上调用 connect() 方法。 connect() 调用是异步的,一个单独的回调将处理 connect 调用的结果。和以前一样,如果位置服务没有打开,您将启动位置设置活动,以便用户可以打开它们。在这个示例中,我们只是使用一条 Toast 消息来告诉用户为什么他们会被重定向到设置屏幕。在生产应用中,您可能希望在重定向到设置屏幕之前显示一个带有“确定”和“取消”按钮的警告对话框。
如果 Google Play 服务不可用,下一步检查是看用户是否可以使用 isUserRecoverableError() 方法解决问题。在这里,您传入先前检查的结果代码,它应该是除了成功之外的代码。这就是为什么您不需要关心返回了什么其他值。这个方法为您决定用户是否可以做一些事情。如果用户不能纠正这种情况(例如, isUserRecoverableError() 返回 false ),那么你真的没有别的办法,你可能会想退出。在这个示例应用中,会写入一条日志消息,然后活动结束。你可能想让你的退场更优雅些。
如果用户可以对 Google Play 服务的问题做些什么,那么 GooglePlayServicesUtil 类还有另一个静态方法可以使用:showerrodialogfragment()。这将向用户显示一个对话框,指出问题是什么以及他们可以做些什么。这个调用有一些变化,示例使用的是在监听对话框取消时弹出一个对话框片段的调用。对话框片段可以启动另一个活动,这将导致我们的 onActivityResult() 被调用。出于这个原因,您希望传入一个请求值(即 PLAY _ SERVICES _ RECOVERY _ REQUEST),该值稍后将被传递给 onActivityResult() 。这个方法也是异步的,您的应用将看到稍后调用的 onActivityResult() ,或者对话框的 onCancel() 。showerrodialogfragment()的第二个参数是上下文,最后一个参数是对话框的监听器。因为我们将“this”作为最后一个参数传递,为了表示此活动,示例活动必须实现 DialogInterface。OnCancelListener 并有一个 onCancel() 回调。
您很快就会看到 onActivityResult() 的代码,但是您应该知道,当一个结果被传递回您的活动时,您将不得不通过调用 tryToConnect() 再次进行这些检查。这就是为什么这个方法设置了一个 lastFix 值,来跟踪哪个问题正在被处理。如果同样的问题在用户有机会解决之后仍然存在,我们可以假设用户对解决问题不感兴趣,或者系统不能解决问题。我们不希望出现某种用户无法摆脱的无限循环。对于这个示例活动,如果 tryToConnect() 连续两次遇到相同的问题,它将退出,活动结束。您的应用可能希望采取替代措施,为用户提供更多选项来继续使用该应用。
回顾一下在 tryToConnect() 中发生的事情,您检查了 Google Play 服务和定位服务的存在和准备情况。如果一切看起来都很好,就在 GoogleClientApi 客户端上进行连接调用。如果用户能够纠正任何事情,就会激发一个合适的意图来启动一个活动来处理它。如果情况没有希望,活动就结束了。现在让我们看看这些操作可能导致的各种回调。
如果连接请求成功,将会触发 onConnected() 回调。清单 19-11 展示了这个样子。
清单 19-11 。客户端已连接,因此请求位置更新
@Override
public void onConnected(Bundle arg0) {
// Set up location updates
Log.v(TAG, "Connected!");
lastFix = FIX.NO_FAIL;
locator.requestLocationUpdates(client, locReq, this);
Log.v(TAG, "Requesting location updates (onConnected)...");
}
这个很简单。如果我们与 Google Play 服务连接良好,就开始向 FusedLocationProviderApi (定位器)请求位置更新。稍后您将看到更多关于 locReq 的内容,但是现在只知道它是一个 LocationRequest 对象,其参数定义了您的应用需要哪种位置更新。该方法还重置了一个状态变量(lastFix ),这将很快变得更有意义。
如果连接请求不成功,将触发 onConnectionFailed() 回调。清单 19-12 显示了这个回调。
清单 19-12 。处理失败的连接尝试
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
/*
* Google Play services can resolve some errors it detects.
* If the error has a resolution, try sending an Intent to
* start a Google Play services activity that can resolve
* the error.
*/
if (connectionResult.hasResolution()) {
Log.i(TAG, "Connection failed, trying to resolve it...");
if(lastFix == FIX.CONNECTION) {
// Since we're coming through again, it means
// recovery didn't happen. Time to bail out.
Log.e(TAG, "Connection retry didn't work");
finish();
}
try {
// Start an activity that tries to resolve the error
lastFix = FIX.CONNECTION;
connectionResult.startResolutionForResult(
this,
CONNECTION_FAILURE_RESOLUTION_REQUEST);
} catch (IntentSender.SendIntentException e) {
// Log the error
Log.e(TAG, "Could not resolve connection failure");
e.printStackTrace();
finish();
}
} else {
/*
* If no resolution is available, display error to the
* user.
*/
Log.e(TAG, "Connection failed, no resolutions available, "+
GooglePlayServicesUtil.getErrorString(
connectionResult.getErrorCode() ));
Toast.makeText(this, "Connection failed. Cannot continue",
Toast.LENGTH_LONG).show();
finish();
}
}
如果连接请求失败,仍然有可能纠正这种情况。同样,有一种方法可以判断是否有解决问题的方法。ConnectionResult 对象包含一个指示符(如果有解决方案),以及尝试解决这种情况的触发意图。在这种情况下,应用调用 startResolutionForResult()。与之前类似,一个意图将被触发,一些活动将被启动,您的应用将在 onActivityResult() 中返回一个结果。注意,这里的请求标签是连接失败解决请求。如果什么也做不了,显示一个错误并退出。
可能有几个启动的意图,每一个都应该触发你的 onActivityResult() 回调。清单 19-13 显示了这个回调的样子。请记住,可能有三个独立的意图来处理问题,所以这个回调必须期望三个中的任何一个。还要记住,意图导致活动运行,这意味着您的活动暂停了,它将在触发 onActivityResult() 之后立即恢复。这就是为什么 tryToConnect() 方法(如清单 19-10 所示)只能从活动的 onResume() 回调中调用的主要原因。每当恢复此活动时,它都会尝试建立到 Google Play 服务的新连接,并设置位置更新。当此活动暂停时,它会断开与 Google Play 服务的连接。重新连接比在不需要的时候试图抓住一个连接更容易。
清单 19-13 。从已发布的意向中获取消息
@Override
protected void onActivityResult(
int requestCode, int resultCode, Intent data) {
/* Decide what to do based on the original request code.
* Note that our activity got paused to launch the other
* activity, so after this callback runs, our activity's
* onResume() will run.
*/
switch (requestCode) {
case PLAY_SERVICES_RECOVERY_REQUEST :
Log.v(TAG, "Got a result for Play Services Recovery");
break;
case LOCATION_SETTINGS_REQUEST :
Log.v(TAG, "Got a result for Location Settings");
break;
case CONNECTION_FAILURE_RESOLUTION_REQUEST :
Log.v(TAG, "Got a result for connection failure");
break;
}
Log.v(TAG, "resultCode was " + resultCode);
Log.v(TAG, "End of onActivityResult");
}
因为 onActivityResult() 可能因为许多意图而被调用,所以 switch 语句被用来判断哪个意图被响应。通过将结果代码设置为活动,Google Play 服务纠正操作可能会说它成功了。结果 _OK 。这不一定意味着用户修复了问题,但它告诉你没有什么失败。如果对 Google Play 服务纠正措施的响应是活动。结果取消了,这可能意味着出现了某种故障。不管用户是否修复了问题,您都将从这个回调中返回,然后 onResume() 将运行,其中 tryToConnect() 将被再次调用。所以 resultCode 是什么真的不重要。实际上,即使正确设置了位置更新,您也可以看到 resultCode 设置为 RESULT _ cancelled。类似地,如果有对其他修复的响应,记录下来并继续,因为 onResume() 无论如何都会运行。
最后,回头参考清单 19-11 中的 onConnected() 回调,该回调调用 locator . requestlocationupdates(client,locReq,this) 。在这里 FusedLocationProviderApi 将被要求向该活动发回位置更新。Google Play 服务已启动并运行,位置服务也已正确设置。
一旦请求了位置更新,任何新的位置更新都将被发送到 onLocationChanged() 回调。在这个示例应用中,所发生的只是位置信息显示在 Toast 消息中。下一节将更详细地介绍如何请求位置更新。
到目前为止,活动中还有一些其他方法没有描述。 onPause() 回调在停止位置更新后断开客户端。您应该注意到,在调用方法之前,会检查客户端的连通性。 GoogleApiClient 类有一个名为 isConnected() 的方法,您将使用它来确保只有在有连接的客户端时才请求或删除位置更新。否则会得到一个 IllegalStateException 。设置菜单的两种方法是基本的菜单回调。该菜单用于允许用户在各种优先级值之间切换。当用户选择一个菜单项时,位置请求对象被更新并传回以改变位置更新过程。可以从弹出的错误对话框中调用 onCancel() 回调,如 tryToConnect 所示(参见清单 19-10 )。如果用户简单地关闭错误片段对话框,我们推断用户不想获得更新,应用退出。
使用 FusedLocationProviderApi 更新位置
使用 LocationManager ,您必须与特定的位置供应器打交道(即 GPS 或手机/WiFi)。使用 FusedLocationProviderApi ,您提交一个 LocationRequest ,Api 将为您选择哪一个提供者是最好的,不仅是最初,而且随着时间的推移。一般来说,在获取位置更新时,要在功耗和精度之间进行权衡。GPS 通常更精确,但耗电最多。另一方面,在室内时,GPS 可能不如手机/WiFi 精确,您可能希望自动切换到更精确的位置,同时消耗最少的电能。FusedLocationProviderApi 还可以利用陀螺仪或指南针等机载传感器。这个 API 向您隐藏了定位的复杂性。
您应该编写自己的代码,以便只在有意义的时候请求位置更新。如果您在地图上显示当前位置,并且地图不可见,则不需要请求更新。有些情况下,即使不显示当前位置,您也可能希望不断获得更新,我们将在下一节中讨论这一点。关键是位置更新会消耗大量的电池电量,所以只有在你真正需要的时候才请求更新。你不应该假设用户会“马上回来”,并因此不断获得更新。如果他们放下设备,一段时间内不会再看它,你最好不要耗尽电池。
清单 19-14 显示了示例应用如何设置 LocationRequest 对象来请求 FusedLocationProviderApi 的位置更新。这是在活动的 onCreate() 回调中完成的。
清单 19-14 。设置 LocationRequest 对象
locReq = LocationRequest.create()
.setPriority(
LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)
.setInterval(10000)
.setFastestInterval(5000);
使用静态的 create() 方法,然后调用适当的 setters 来填充请求对象。该对象将被传递给 FusedLocationProviderApi 的 requestLocationUpdates() 方法。与处理旧的位置提供者的一个很大的区别是,这个请求对象不引用任何特定的位置提供者。类似于寻找提供者的标准方法,这个请求对象最终选择更新的频率和功耗。
您可以使用 setInterval() 和 setFastestInterval() 指定所需的位置更新频率;两者都有一个表示毫秒数的长参数。前者是说你想要定期得到位置更新,每隔几毫秒。如果可以的话,系统会尽力做到这一点,但不能保证。您可以比预期的更频繁地获得更新,甚至更频繁。这就是第二种方法的用武之地。您可以指定接收位置更新的最快时间间隔。稍后会有更多的介绍。
请求的功率部分由 setPriority() 设置器处理。该参数目前有四个选项:
- PRIORITY_NO_POWER
- PRIORITY_LOW_POWER
- 优先级 _ 平衡 _ 功率 _ 精度
- 优先级 _ 高 _ 准确度
NO_POWER 选项相当于说您的应用将使用前面描述的被动提供者。不消耗任何功率的唯一方法是搭载另一个应用的位置更新。因此,位置的准确性可能不是非常准确或频繁;这完全取决于其他应用的请求。您刚刚了解到,您可以使用 setInterval() 和 setFastestInterval() 请求更新频率。如果您正在使用另一个应用,并且该应用每 5 秒接收一次位置更新,但是您不希望更新速度超过 20 秒,那么您应该使用 setFastestInterval(20000)这样您的应用就不会被更新淹没。同时,您可以使用 setInterval(60000) 请求每分钟更新一次的间隔。如果设备上发生的其他位置更新很少,你就不必担心将频率从 5 秒减少到 20 秒,但同时你可能也不会每分钟更新一次。您需要使用这两个设置器来表明您的应用想要什么,但这并不意味着您一定能得到您想要的。
低功率优先级通常意味着位置更新将仅通过蜂窝塔三角测量和 WiFi 热点位置信息来获得。这些都是确定位置的低功耗方法,精度会相应降低。您可以很容易地找到仅精确到 1500 米以内或更差的位置,但随后您可以获得精确到 10 米的位置。所有优先级都将利用被动提供者,因此,如果某个其他应用请求精确的位置更新,即使您的优先级设置为低功耗,您的应用也可以获得它。
平衡的优先级将尝试做一件体面的工作,以较低的功耗换取精度。它将考虑使用除 GPS 之外的所有可用的定位方法。
高精度优先级将潜在地使用所有可用的位置信息来源,包括 GPS。由于 GPS 无线电,这种优先权会消耗大量电池。
位置更新还取决于设备的位置模式。正如您之前看到的,KitKat 中的位置设置发生了变化,允许用户为他们的设备指定位置更新的模式。现在参考设置。安全类,定位模式设定值如下:
- 位置 _ 模式 _ 关闭
- 位置 _ 模式 _ 电池 _ 节能
- 位置 _ 模式 _ 高精度
- 位置 _ 模式 _ 传感器 _ 仅限于
使用清单 19-8 中的代码可以检索当前值。该模式由用户为整个器件设置,而不是由应用设置。但是,您的应用有机会请求一个优先级来补充用户所做的模式选择。如果设备的模式是高精度,而你的应用选择的优先级是低功耗,你的应用将不会耗尽电池,但仍然可以获得不错的位置更新。
然而,这种模式可能对你不利。如果用户选择了 SENSORS_ONLY 的模式,并且优先级被设置为 NO_POWER 、 LOW_POWER 甚至 BALANCED ,则位置更新将很少,无论您在位置请求中用 setInterval() 设置了什么。最有用的位置更新的首选模式是 HIGH_ACCURACY ,因为该模式将结合所有可能的位置信息来源,并提供最准确的结果。您的应用将能够在需要时获得高精度(希望这是一种罕见的需要),并在其余时间获得良好的精度。您的应用可以在需要时将优先级更改为高精度,但在其他时候平衡或低功耗。
使用 LocationRequest 的其他一些有趣的选项包括设置要接收的位置更新的具体数量,或者指定位置更新应该停止的时间限制。您还可以设置应用不希望更新的最小距离(以米为单位)。这是一种地理围栏,你告诉定位服务,如果设备从当前位置移动了一定距离,你只需要位置更新。这实际上是在当前位置周围建立地理围栏圈。稍后将详细介绍 geofences。
获取位置更新的替代方法
您已经看到了如何使用 LocationManager 的 requestLocationUpdates() 方法和 FusedLocationProviderApi 将位置更新发送到您的活动。这个方法实际上有几种不同的签名,包括使用 pending content 的签名。这使您能够将位置更新定向到服务或广播接收器。您还可以将位置更新指向其他 Looper 线程而不是主线程,为您的应用提供了很多灵活性,尽管其中一些方法仅在 Android 2.3 之后才可用。
使用邻近警报和地理围栏
地理围栏是移动应用的普遍要求。这意味着您的应用应该根据它的位置来改变它的行为。一个典型的使用案例是防止设备在特定位置之外工作。例如,当患者不在医院时,医院应用可以限制对患者数据的访问。或者,当设备在工作场所时,您的应用可能希望静音通知。LocationManager 有一个名为 proximity alerts 的机制,最近还有一个类似的 API,名为 GeofencingApi,用于更新的位置服务。我们将简要讨论第一个,然后详细讨论第二个。
我们之前提到过, LocationManager 可以在设备进入指定的地理位置时通知你。设置这个的方法是来自 LocationManager 类的 addProximityAlert() 。基本上,你告诉 LocationManager 你想要一个 Intent 在设备的位置进入或离开一个以纬度/经度位置为中心的特定半径的圆时被触发。意图可以触发 BroadcastReceiver 或者 Service 被调用,或者 Activity 被启动。警报还有一个可选的时间限制,所以它可以在意图开火之前超时。
在内部,该方法的代码为 GPS 和网络供应器注册监听器,并设置每秒一次的位置更新和 1 米的距离。您无法覆盖这种行为或设置参数。因此,如果您让它长时间运行,您可能会很快耗尽电池。如果屏幕进入睡眠状态,接近警报将每四分钟检查一次,但是同样,您无法控制持续时间。出于这些原因,我们在示例应用中包含了一个名为 ProximityAlertDemo 的演示应用,但我们不会深入讨论细节。相反,我们将把注意力转向位置服务方法,使用另一个名为 GeofencingApi 的示例应用。请注意,GeofencingApi 示例应用看起来与 FusedLocationProviderApi 示例应用相似,因为两者都共享 GoogleClientApi 的激活机制。
GeofencingApi API
在撰写本文时,地理围栏是一个具有纬度/经度中心的圆形区域,外加一些时间参数。在未来的某个时刻,这个区域可能不是圆形的,但现在是。一旦地理围栏构建完成,就可以将其传递给 GeofenceApi 进行监控。您的应用甚至可以消失,您的地理围栏也可以处于活动状态。除了一个地理围栏或一组地理围栏之外,您的应用还将传递一个 PendingIntent,目的是在地理围栏周围发生有趣的事情时被触发。三个当前事件是进入、退出和停留。进入和退出简单易懂;如果设备进入或离开圆形区域,将触发意图。停留事件在设备停留在圆形区域内一段时间后触发意图。这个待机延迟以毫秒为单位。这就是全部了。
请参见名为 GeofencingApiDemo 的示例应用。它设置了两个名为 home 和 work 的地理围栏,连接到位置服务,并注册了一个服务意图,当设备进入、退出或停留在这些地理围栏中的任何一个时,将被触发。触发时,该服务会为每个事件生成一个通知,以便您更容易看到结果。地理围栏通常在后台使用,因此服务在这里很有意义。也就是说,应用不需要在前台就可以拥有 geofences。事实上,地理围栏的基本思想是,如果设备进入或离开特定的地理区域,您希望您的应用被唤醒。
之前用于确保 Google Play 服务和位置服务可用且准备就绪的设置代码已从该示例应用中删除,以便于理解,但是您可能希望将该代码包含在生产应用中。清单 19-15 显示了主活动的 onCreate() 方法,其中创建了地理围栏和 pending content。
清单 19-15 。设置地理围栏
private GoogleApiClient mClient = null;
private List<Geofence> mGeofences = new ArrayList<Geofence>();
private PendingIntent pIntent = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final float radius = 0.5f * 1609.0f; // half mile times 1609 meters per mile
Geofence.Builder gb = new Geofence.Builder();
// Make a half mile geofence around your home
Geofence home = gb.setCircularRegion(28.993818, -81.383816, radius)
.setTransitionTypes(
Geofence.GEOFENCE_TRANSITION_ENTER |
Geofence.GEOFENCE_TRANSITION_EXIT |
Geofence.GEOFENCE_TRANSITION_DWELL )
.setExpirationDuration(
12 * 60 * 60 * 1000) // 12 hours
.setLoiteringDelay(300000) // 5 minutes
.setRequestId("home")
.setNotificationResponsiveness(5000) // 5 secs
.build();
mGeofences.add(home);
// Make another geofence around your work
Geofence work = gb.setCircularRegion(28.36631, -81.52120, radius)
.setRequestId("work")
.build();
mGeofences.add(work);
Intent intent = new Intent(this, ReceiveTransitionsIntentService.class);
pIntent = PendingIntent.getService(getApplicationContext(), 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
mClient = new GoogleApiClient.Builder(this, this, this)
.addApi(LocationServices.API)
.build();
Log.v(TAG, "Activity, client are created");
}
了解如何将地理围栏创建为围绕纬度/经度的圆圈,其中包含感兴趣的事件(在本例中为所有事件)和一些时间参数。在这个例子中,地理围栏将被激活 12 个小时,或者直到它们被移除(见 onDestroy() )。也可以将地理围栏设置为永不过期。5 分钟的游荡延迟意味着如果设备在地理围栏内停留至少 5 分钟,停留事件将触发。请求 ID 将与意向一起传回您的应用,以便您可以确定意向是针对哪个 geofence。5 秒的通知响应意味着 GeofencingApi 将尝试在事件发生后的 5 秒内发送意图。然而,没有人能保证这种意图会很快实现。这个值越大,对电池寿命越有利,因为 API 可以睡得更多,检查的次数更少。另一方面,如果该值很长,例如几分钟,那么如果设备快速通过地理围栏,您甚至可能会错过事件。通知响应的选择将取决于您的 geofences 有多大以及您希望您的应用如何运行。
类似于前面的示例应用,从 onResume() 尝试连接,并且清单 19-16 显示了当连接成功时运行的内容。
清单 19-16 。向 API 注册 Geofences
@Override
public void onConnected(Bundle arg0) {
// Set up geofences
Log.v(TAG, "Setting up geofences (onConnected)...");
PendingResult<Status> pResult = mFencer.addGeofences(mClient,
mGeofences, pIntent);
pResult.setResultCallback(this); // ResultCallback<Status> interface
}
@Override
public void onResult(Status status) {
Log.v(TAG, "Got a result from addGeofences("
+ status.getStatusCode() + "): "
+ status.getStatus().getStatusMessage());
}
GeofencingApi 通过 Api 客户端句柄、geofences 列表和 PendingIntent。返回是一个待定结果。如果想知道结果最终是否成功,需要使用 setResultCallback() 设置一个回调接收方。该活动已经实现了 result callback接口,因此将使用 addGeofences() 方法调用的结果来调用 onResult() 回调。对于这个示例,结果被简单地记录下来,但是如果结果不成功,您当然希望采取措施。这就是活动所做的一切。接下来是当有趣的事件发生时接收意图的服务。
清单 19-17 展示了 receivetransitionsinntentservice 的有趣回调和方法,这是该应用的一个 IntentService 。它主要报告收到的信息,无论是错误还是地理围栏事件。使用通知显示事件。这是为了您的安全,因为预计您将在家里启动该应用,然后开车去工作。我们不希望您在旅途中观看设备屏幕。相反,当您安全停止时,您将能够查看每个事件的所有通知。
清单 19-17 。从 GeofencingApi 接收意向
public void onCreate() {
super.onCreate();
notificationMgr = (NotificationManager)getSystemService(
NOTIFICATION_SERVICE);
}
@Override
protected void onHandleIntent(Intent intent) {
GeofencingEvent gfEvent = GeofencingEvent.fromIntent(intent);
// First check for errors
if (gfEvent.hasError()) {
// Get the error code with a static method
int errorCode = gfEvent.getErrorCode();
// Log the error
Log.e(TAG, "Location Services error: " +
Integer.toString(errorCode));
/*
* If there's no error, get the transition type and the IDs
* of the geofence or geofences that triggered the transition
*/
} else {
// Get the type of transition (entry or exit)
int transitionType =
gfEvent.getGeofenceTransition();
String tranTypeStr = "UNKNOWN(" + transitionType + ")";
switch(transitionType) {
case Geofence.GEOFENCE_TRANSITION_ENTER:
tranTypeStr = "ENTER";
break;
case Geofence.GEOFENCE_TRANSITION_EXIT:
tranTypeStr = "EXIT";
break;
case Geofence.GEOFENCE_TRANSITION_DWELL:
tranTypeStr = "DWELL";
break;
}
Log.v(TAG, "transitionType reported: " + tranTypeStr);
Location triggerLoc = gfEvent.getTriggeringLocation();
Log.v(TAG, "triggering location is " + triggerLoc);
List <Geofence> triggerList =
gfEvent.getTriggeringGeofences();
String[] triggerIds = new String[triggerList.size()];
for (int i = 0; i < triggerIds.length; i++) {
// Grab the Id of each geofence
triggerIds[i] = triggerList.get(i).getRequestId();
String msg = tranTypeStr + ": " + triggerLoc.getLatitude() +
", " + triggerLoc.getLongitude();
String title = triggerIds[i];
displayNotificationMessage(title, msg);
}
}
}
private void displayNotificationMessage(String title, String message)
{
int notif_id = (int) (System.currentTimeMillis() & 0xFFL);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle(title)
.setContentText(message)
.setSmallIcon(android.R.drawable.ic_menu_compass)
.setOngoing(false)
.build();
notificationMgr.notify(notif_id, notification);
}
当你在这个应用中替换家庭和工作的纬度和经度时,你在一个真实的设备上运行它,然后你移动这个设备,你将会看到如图图 19-8 中的通知。
图 19-8 。来自 GeofencingApi 事件的通知
第一个事件发生在下午 6:40,因为当应用启动时,设备已经在家庭区域内。下午 6:45 的第二个事件是停留事件,因为在 5 分钟的游荡延迟之后,设备仍然在归属区域内。如果设备在截屏被捕获之前离开了家庭区域,将会有离开家庭的事件。请注意,通知中的纬度和经度是设备的实际位置,不一定是该区域的中心。
参考
这里有一些有用的参考资料,您可能希望进一步研究。
- 。与本书相关的可下载项目列表。对于这一章,寻找一个名为 pro Android 5 _ Ch19 _ maps . zip 的 zip 文件。这个 zip 文件包含本章中的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,它准确地描述了如何将项目从这些 zip 文件之一导入到 IDE 中。这里有一些额外的示例应用,包括 WhereAmI4,它包含标记的自定义信息窗口。
developer . Android . com/guide/topics/location/index . html
。Android 位置和地图开发者指南。developer . Android . com/Google/play-services/index . html
。Google Play 服务文档,包括 FusedLocationProviderApi、GeofencingApi 和 GoogleMap。developer . Android . com/Google/play-services/setup . html
。将 Google Play 服务库纳入您的应用的说明。注意下拉菜单允许在 Android Studio 和 Eclipse with ADT 之间进行选择。- 。地图 API 文档独立于其他在线 Android 文档。
摘要
让我们通过快速列举到目前为止您已经了解的地图来结束本章:
- 如何从谷歌获取自己的地图 API 密钥?
- MapFragment ,所有地图的主要组件。
- 您需要对您的 AndroidManifest.xml 文件进行修改,以使地图应用工作。
- 定义包含地图的布局,以及如何实例化地图。
- 放大和缩小,平移和显示当前位置。
- 包括卫星、交通等不同模式。
- 如何使用地图框渲染地图。
- 向地图添加标记。
- 地图相机和设置适应特定标记集的缩放级别的方法。
- 地理编码器,以及它如何从地址转换为纬度/经度,或者从纬度/经度转换为地址和感兴趣的地点。
- 将地理编码器放入后台线程,以避免讨厌的应用不响应(ANR)弹出窗口。
- LocationServices 服务,使用 GPS 和/或网络塔来精确定位设备的位置。
- 选择位置提供者,以及如果期望的位置服务或提供者未被启用时该做什么。
- 使用模拟器的功能将位置事件发送到您的应用进行测试。这包括使用记录整个系列位置事件的特殊文件。
- 例如,使用 Location 类的方法来计算点之间的距离。
- 如何进行所有的检查和纠正措施来设置 Google Play 位置更新服务。
- 接近时发出警报—即设置一个接近度,当设备进入或离开该接近度时发出警报。
- 设置 geofences 来处理一个或多个区域的进入、退出和停留事件,同时节省电池寿命。
二十、了解媒体框架
现在我们将探索 Android SDK 的一个非常有趣的部分:媒体框架。我们将向您展示如何播放各种来源的音频和视频。我们还将在在线伴侣部分介绍如何用相机拍照以及录制音频和视频。
使用媒体 API
Android 支持播放 android.media 包下的音视频内容。在这一章中,我们将从这个包中探索媒体 API。
android.media 包的核心是 ?? Android . media . media player 类 ??。MediaPlayer 类负责播放音频和视频内容。本课程的内容可以来自以下来源:
- Web :您可以通过 URL 播放来自 Web 的内容。
- 。 apk 文件:你可以播放打包成你一部分的内容。apk 文件。您可以将媒体内容打包为资源或资产(在资产文件夹中)。
- Android KitKat 4.4 新增的存储访问框架,它提供了对存储在一系列供应器和互联网服务中的媒体文件的访问。
- SD 卡:您可以播放设备 SD 卡或模拟本地存储器上的内容。
MediaPlayer 能够解码多种不同的内容格式,包括第三代合作伙伴计划(3GPP、 .3gp )、MP3 ( .mp3 )、MIDI ( )。mid 等)、Ogg Vorbis ( )。ogg 、PCM/WAVE ( )。wav ),以及 MPEG-4 ( .mp4 )。RTSP、HTTP/HTTPS 直播和 M3U 播放列表也受支持,尽管包含 URL 的播放列表不受支持,至少在撰写本文时是如此。有关支持的媒体格式的完整列表,请访问developer . Android . com/guide/appendix/media-formats . html
。
SD 卡何去何从?
在我们深入媒体框架的核心之前,我们应该快速解决可移动存储的话题,尤其是 SD 卡。Android 设备的最新趋势是,一些制造商将它们从设备中删除,而其他制造商则继续包含它们。谷歌自己通过混淆 Android 中的底层文件系统,模糊了什么是移动存储,什么不是移动存储的界限。
不管你作为开发者的个人偏好如何,你的一些用户可能仍然拥有支持 SD 卡的设备,并且想要使用它们。我们将在这里讨论的许多例子同样适用于从 SD 卡获取媒体文件。然而,为了节省空间,并避免不必要的重复,我们在本书的网站上放置了一些额外的例子,这些例子涉及 SD 卡的细节和支持材料。一定要在www.androidbook.com检查一下。
播放媒体
首先,我们将向你展示如何构建一个简单的应用来播放网络上的 MP3 文件(见图 20-1 )。之后,我们将讨论如何使用 MediaPlayer 类的 setDataSource() 方法来播放来自的内容。apk 文件。 MediaPlayer 并不是播放音频的唯一方式,因此我们还将介绍 SoundPool 类,以及 JetPlayer 、 AsyncPlayer ,以及用于处理音频的最低级别的 AudioTrack 类。之后,我们将讨论 MediaPlayer 类的一些不足之处。最后,我们将了解如何播放视频内容。
播放音频内容
图 20-1 显示了我们第一个例子的用户界面。这个应用将演示 MediaPlayer 类的一些基本用法,比如启动、暂停、重启和停止媒体文件。查看应用用户界面的布局。
图 20-1 。媒体应用的用户界面
用户界面由一个带有四个按钮的 RelativeLayout 组成:一个启动播放器,一个暂停播放器,一个重启播放器,一个停止播放器。我们本可以使这变得简单,只需将我们的例子与做同样事情的媒体控制器小部件结合起来,但是我们想向您展示自己控制事物的内部工作原理。应用的代码和布局文件如清单 20-1 所示。对于这个示例,我们将假设您正在构建 Android 2.2 或更高版本,因为我们使用了环境类的 getexternalsragepublicdirectory()方法。如果你想在一个旧版本的 Android 上构建它,只需使用 getExternalStorageDirectory()并调整你放置媒体文件的位置,这样你的应用就能找到它们。
注意参见本章末尾的“参考”部分,从中可以直接将这些项目导入 Eclipse,而不是复制和粘贴代码。
清单 20-1 。媒体应用的布局和代码
<RelativeLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical" >
<Button android:id="@+id/startPlayerBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Playing Audio"
android:onClick="doClick" />
<Button android:id="@+id/pausePlayerBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause Player"
android:layout_below="@+id/startPlayerBtn"
android:onClick="doClick" />
<Button android:id="@+id/restartPlayerBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restart Player"
android:layout_below="@+id/pausePlayerBtn"
android:onClick="doClick" />
<Button android:id="@+id/stopPlayerBtn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop Player"
android:layout_below="@+id/restartPlayerBtn"
android:onClick="doClick" />
</RelativeLayout>
// This file is MainActivity.java
import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
public class MainActivity extends Activity implements OnPreparedListener
{
static final String AUDIO_PATH =
"[`www.androidbook.com/akc/filestorage/android/documentfiles/3389/play.mp3`](http://www.androidbook.com/akc/filestorage/android/documentfiles/3389/play.mp3)";
private MediaPlayer mediaPlayer;
private int playbackPosition=0;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
public void doClick(View view) {
switch(view.getId()) {
case R.id.startPlayerBtn:
try {
// Only have one of these play methods uncommented
playAudio(AUDIO_PATH);
// playLocalAudio();
// playLocalAudio_UsingDescriptor();
} catch (Exception e) {
e.printStackTrace();
}
break;
case R.id.pausePlayerBtn:
if(mediaPlayer != null && mediaPlayer.isPlaying()) {
playbackPosition = mediaPlayer.getCurrentPosition();
mediaPlayer.pause();
}
break;
case R.id.restartPlayerBtn:
if(mediaPlayer != null && !mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(playbackPosition);
mediaPlayer.start();
}
break;
case R.id.stopPlayerBtn:
if(mediaPlayer != null) {
mediaPlayer.stop();
playbackPosition = 0;
}
break;
}
}
private void playAudio(String url) throws Exception
{
killMediaPlayer();
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(url);
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.prepareAsync();
}
private void playLocalAudio() throws Exception
{
mediaPlayer = MediaPlayer.create(this, R.raw.music_file);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
// calling prepare() is not required in this case
mediaPlayer.start();
}
private void playLocalAudio_UsingDescriptor() throws Exception {
AssetFileDescriptor fileDesc = getResources().openRawResourceFd(
R.raw.music_file);
if (fileDesc != null) {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(fileDesc.getFileDescriptor(),
fileDesc.getStartOffset(), fileDesc.getLength());
fileDesc.close();
mediaPlayer.prepare();
mediaPlayer.start();
}
}
// This is called when the MediaPlayer is ready to start
public void onPrepared(MediaPlayer mp) {
mp.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
killMediaPlayer();
}
private void killMediaPlayer() {
if(mediaPlayer!=null) {
try {
mediaPlayer.release();
}
catch(Exception e) {
e.printStackTrace();
}
}
}
}
在第一个场景中,您正在从一个网址播放 MP3 文件。因此,您需要将 Android . permission . internet 添加到您的清单文件中。清单 20-1 显示了 MainActivity 类包含三个成员:一个指向 MP3 文件 URL 的 final 字符串,一个 MediaPlayer 实例,以及一个名为 playbackPosition 的整数成员。我们的 onCreate() 方法只是从我们的布局 XML 文件中设置用户界面。在按钮点击处理器中,当按下开始播放音频按钮时,调用 playAudio() 方法。在 playAudio() 方法中,创建了一个 MediaPlayer 的新实例,播放器的数据源设置为 MP3 文件的 URL。
然后调用播放器的 prepareAsync() 方法来准备 MediaPlayer 进行播放。我们在活动的主 UI 线程中,所以我们不想花太多时间来准备 MediaPlayer。在 MediaPlayer 上有一个 prepare() 方法,但是在准备完成之前它会阻塞。如果这需要很长时间,或者如果服务器需要一段时间来响应,用户可能会认为应用卡住了,或者更糟,得到一个错误消息。像进度对话框这样的东西可以帮助你的用户理解正在发生的事情。 prepareAsync() 方法立即返回,但是设置一个后台线程来处理 MediaPlayer 的 prepare() 方法。当准备完成时,我们的活动的 onPrepared() 回调被调用。这是我们最终开始播放媒体播放器的地方。我们必须告诉 MediaPlayer 谁是 onPrepared() 回调的侦听器,这就是为什么我们在调用 prepareAsync() 之前调用 setOnPreparedListener()。您不必将当前活动用作侦听器;我们在这里这样做是因为这对于本演示来说更简单。
现在看看暂停播放器和重启播放器按钮的代码。可以看到当暂停播放器按钮被选中时,通过调用 getCurrentPosition() 得到播放器的当前位置。然后通过调用 pause() 来暂停播放器。当播放器必须重启时,调用 seekTo() ,传入之前从 getCurrentPosition() 获得的位置,然后调用 start() 。
MediaPlayer 类还包含一个 stop() 方法。注意,如果您通过调用 stop() 来停止播放器,您需要在再次调用 start() 之前再次准备 MediaPlayer 。反过来,如果调用 pause() ,可以不用准备播放器,再次调用 start() 。此外,在使用完媒体播放器后,一定要调用它的 release() 方法。在本例中,您将此作为 killMediaPlayer() 方法的一部分。
在示例应用源代码中有第二个 URL 用于音频源,但它不是 MP3 文件,而是一个流音频提要(Radio-Mozart)。这也适用于 MediaPlayer,并再次显示了为什么您需要调用 prepareAsync() 而不是 prepare() 。准备用于回放的音频流可能需要一段时间,具体取决于服务器、网络流量等。
清单 20-1 展示了如何播放网络上的音频文件。 MediaPlayer 类也支持播放你的本地的媒体。apk 文件。清单 20-2 展示了如何从的 /res/raw 文件夹中引用并回放一个文件。apk 文件。继续添加 raw 文件夹到 /res 下,如果 Eclipse 项目中还没有的话。然后,将您选择的 MP3 文件复制到 /res/raw 中,文件名为 music_file.mp3 。还要注意原始代码中的注释,取消对 playLocalAudio() 的调用的注释,并注释掉 playAudio()。
清单 20-2 。 使用 MediaPlayer 播放应用本地文件
private void playLocalAudio()throws Exception
{
mediaPlayer = MediaPlayer.create(this, R.raw.music_file);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // calling prepare() is not required in this case
mediaPlayer.start();
}
如果您需要在应用中包含音频或视频文件,您应该将该文件放在 /res/raw 文件夹中。然后,您可以通过传入媒体文件的资源 ID 来获得资源的 MediaPlayer 实例。你可以通过调用静态的 create() 方法来实现,如清单 20-2 所示。请注意, MediaPlayer 类提供了一些其他静态的 create() 方法,您可以使用这些方法来获取 MediaPlayer 而不是自己实例化一个。在清单 20-2 中, create() 方法相当于调用构造函数 MediaPlayer(Context context,int resourceId) 后跟调用 prepare() 。只有当媒体源位于设备本地时,才应该使用 create() 方法,因为它总是使用 prepare() 而不是 prepareAsync() 。
了解 setDataSource 方法
在清单 20-2 中,我们调用了 create() 方法从原始资源加载音频文件。使用这种方法,您不需要调用 setDataSource() 。或者,如果您使用默认构造函数自己实例化了 MediaPlayer ,或者如果您的媒体内容不能通过资源 ID 或 URI 访问,您将需要调用 setDataSource() 。
setDataSource() 方法有重载版本,您可以根据自己的特定需求定制数据源。例如,清单 20-3 展示了如何使用 FileDescriptor 从原始资源中加载一个音频文件。
清单 20-3 。 使用文件描述符设置媒体播放器的数据源
private void playLocalAudio_UsingDescriptor() throws Exception {
AssetFileDescriptor fileDesc = getResources().openRawResourceFd(
R.raw.music_file);
if (fileDesc != null) {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDataSource(fileDesc.getFileDescriptor(),
fileDesc.getStartOffset(), fileDesc.getLength());
fileDesc.close();
mediaPlayer.prepare();
mediaPlayer.start();
}
}
清单 20-3 假设它在一个活动的上下文中。如图所示,调用 getResources() 方法获取应用的资源,然后使用 openRawResourceFd() 方法获取 /res/raw 文件夹中音频文件的文件描述符。然后使用 AssetFileDescriptor 调用 setDataSource() 方法,开始回放的起始位置和结束位置。如果您想回放音频文件的特定部分,也可以使用这个版本的 setDataSource() 。如果总是想播放整个文件,可以调用更简单的版本 set data source(file descriptor desc),不需要初始偏移量和长度。
在这种情况下,我们选择使用 prepare() 后跟 start() ,只是为了向您展示它可能的样子。我们应该能够逃脱,因为音频资源是本地的,但像以前一样使用 prepareAsync() 也无妨。
我们还有一个音频内容的来源可以谈论:SD 卡。有关处理 SD 卡及其文件系统内容的基础知识,请参考在线指南章节。在我们的例子中,我们使用 setDataSource() 通过传入一个 MP3 文件的 URL 来访问互联网上的内容。如果你在 SD 卡上有一个音频文件,你可以使用同样的 setDataSource() 方法,但是把你在 SD 卡上的音频文件的路径传给它。例如,音乐目录中名为 music_file.mp3 的文件可以用 AUDIO_PATH 变量来播放,如下所示:
static final String AUDIO_PATH =
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MUSIC) +
"/music_file.mp3";
您可能已经注意到,在我们的示例中,我们没有实现 onResume() 和 onPause() 。这意味着当我们的活动进入后台时,它会继续播放音频——至少,直到活动被终止,或者直到对音频源的访问被关闭。例如,如果我们不持有唤醒锁,CPU 可能会关闭,从而结束音乐的播放。许多人选择在服务中管理媒体播放来帮助解决这些问题。在我们当前的例子中,其他问题包括 MediaPlayer 是否正在通过 Wi-Fi 播放音频流,如果我们的活动没有锁定 Wi-Fi,Wi-Fi 可能会被关闭,我们将失去与该流的连接。 MediaPlayer 有一个名为 setWakeMode() 的方法,它允许我们设置一个 PARTIAL_WAKE_LOCK 来在播放时保持 CPU 活动。然而,为了锁定 Wi-Fi,我们需要分别通过 WifiManager 和 WifiManager 来实现。WifiLock 。
继续在后台播放音频的另一个方面是,我们需要知道什么时候不应该这样做,可能是因为有来电,或者是因为闹钟响了。Android 有一个 AudioManager 来帮助解决这个问题。调用的方法有 requestAudioFocus() 和 abandonAudioFocus() ,在接口 AudioManager 中有一个回调方法叫做 onAudioFocusChange() 。OnAudioFocusChangeListener 。有关更多信息,请参见 Android 开发人员指南中的媒体页面。
使用 SoundPool 进行同步音轨播放
MediaPlayer 是我们媒体工具箱中的一个重要工具,但它一次只能处理一个音频或视频文件。如果我们想同时播放多个音轨呢?一种方法是创建多个媒体播放器并同时使用它们。如果你只有少量的音频要播放,并且你想要快速的性能,Android 有 SoundPool 类来帮助你。在幕后, SoundPool 使用 MediaPlayer ,但是我们无法访问 MediaPlayer API,只能访问 SoundPool API。
MediaPlayer 和 SoundPool 的另一个区别是 SoundPool 被设计成只处理本地媒体文件。也就是说,您可以从资源文件、使用文件描述符的其他文件或使用路径名的文件载入音频。 SoundPool 还提供了其他几个不错的功能,比如循环播放音轨、暂停和恢复单个音轨,或者暂停和恢复所有音轨。
然而, SoundPool 也有一些缺点。对于所有的音轨, SoundPool 在内存中有一个总的音频缓冲区,只有 1MB。当您查看只有几千字节大小的 MP3 文件时,这可能看起来很大。但是 SoundPool 扩展了内存中的音频,使播放变得快速简单。内存中音频文件的大小取决于比特率、声道数(立体声与单声道)、采样速率和音频长度。如果你无法将声音加载到 SoundPool 中,你可以尝试使用源音频文件的这些参数来使音频在内存中变小。
我们的示例应用将加载并播放动物的声音。其中一种声音是蟋蟀的叫声,它不断地在背景中播放。其他声音以不同的时间间隔播放。有时候你听到的只有蟋蟀的叫声;其他时候,你会同时听到几种动物的声音。我们还将在用户界面中放置一个按钮,允许暂停和恢复。清单 20-4 显示了我们的布局 XML 文件和活动的 Java 代码。你最好的办法是从我们的网站上下载,以便获得声音文件和代码。有关如何找到可下载源代码的信息,请参见本章末尾的“参考”部分。
清单 20-4 。 用 SoundPool 播放音频
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
>
<ToggleButton android:id="@+id/button"
android:textOn="Pause" android:textOff="Resume"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:onClick="doClick" android:checked="true" />
</LinearLayout>
// This file is MainActivity.java
import java.io.IOException;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import android.widget.ToggleButton;
public class MainActivity extends Activity implements SoundPool.OnLoadCompleteListener {
private static final int SRC_QUALITY = 0;
private static final int PRIORITY = 1;
private SoundPool soundPool = null;
private AudioManager aMgr;
private int sid_background;
private int sid_roar;
private int sid_bark;
private int sid_chimp;
private int sid_rooster;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
@Override
protected void onResume() {
soundPool = new SoundPool(5, AudioManager.STREAM_MUSIC,
SRC_QUALITY);
soundPool.setOnLoadCompleteListener(this);
aMgr =
(AudioManager)this.getSystemService(Context.AUDIO_SERVICE);
sid_background = soundPool.load(this, R.raw.crickets, PRIORITY);
sid_chimp = soundPool.load(this, R.raw.chimp, PRIORITY);
sid_rooster = soundPool.load(this, R.raw.rooster, PRIORITY);
sid_roar = soundPool.load(this, R.raw.roar, PRIORITY);
try {
AssetFileDescriptor afd =
this.getAssets().openFd("dogbark.mp3");
sid_bark = soundPool.load(afd.getFileDescriptor(),
0, afd.getLength(), PRIORITY);
afd.close();
} catch (IOException e) {
e.printStackTrace();
}
//sid_bark = soundPool.load("/mnt/sdcard/dogbark.mp3", PRIORITY);
super.onResume();
}
public void doClick(View view) {
switch(view.getId()) {
case R.id.button:
if(((ToggleButton)view).isChecked()) {
soundPool.autoResume();
}
else {
soundPool.autoPause();
}
break;
}
}
@Override
protected void onPause() {
soundPool.release();
soundPool = null;
super.onPause();
}
@Override
public void onLoadComplete(SoundPool sPool, int sid, int status) {
Log.v("soundPool", "sid " + sid + " loaded with status " +
status);
final float currentVolume =
((float)aMgr.getStreamVolume(AudioManager.STREAM_MUSIC)) /
((float)aMgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC));
if(status != 0)
return;
if(sid == sid_background) {
if(sPool.play(sid, currentVolume, currentVolume,
PRIORITY, -1, 1.0f) == 0)
Log.v("soundPool", "Failed to start sound");
} else if(sid == sid_chimp) {
queueSound(sid, 5000, currentVolume);
} else if(sid == sid_rooster) {
queueSound(sid, 6000, currentVolume);
} else if(sid == sid_roar) {
queueSound(sid, 12000, currentVolume);
} else if(sid == sid_bark) {
queueSound(sid, 7000, currentVolume);
}
}
private void queueSound(final int sid, final long delay,
final float volume)
{
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if(soundPool == null) return;
if(soundPool.play(sid, volume, volume,
PRIORITY, 0, 1.0f) == 0)
Log.v("soundPool", "Failed to start sound (" + sid +
")");
queueSound(sid, delay, volume);
}}, delay);
}
}
这个例子的结构相当简单。我们有一个用户界面,上面只有一个切换按钮。我们将用它来暂停和恢复活动的音频流。当我们的应用启动时,我们创建我们的音池,并加载音频样本。当样本被正确加载后,我们开始播放它们。蟋蟀的声音无休止地循环播放;其他样本在延迟后播放,然后在相同的延迟后将自己设置为再次播放。通过选择不同的延迟,我们可以在声音上获得一种随机的效果。
创建一个声音池需要三个参数:
- 第一个是 SoundPool 将同时播放的最大样本数。这不是音池能容纳的样本数量。
- 第二个参数是样本将在哪个音频流上播放。典型值是 AudioManager。STREAM_MUSIC ,但是 SoundPool 可以用于闹铃或者铃声。请参见 AudioManager 参考页,了解完整的音频流列表。
- 在创建音池时 SRC_QUALITY 值应该设置为 0。
代码演示了 SoundPool 的几种不同的 load() 方法。最基本的是从 /res/raw 加载一个音频文件作为资源。我们对前四个音频文件使用这种方法。然后我们展示如何从应用的 /assets 目录中加载一个音频文件。这个 load() 方法还接受指定要加载的音频的偏移量和长度的参数。这将允许我们使用一个包含多个音频样本的文件,从中提取出我们想要使用的内容。最后,我们在注释中展示了如何从 SD 卡中访问音频文件。一直到 Android 4.0,优先级参数应该只是 1。
对于我们的例子,我们选择使用 Android 2.2 中引入的一些特性,特别是我们活动的 onLoadCompleteListener 接口,以及按钮回调中的 autoPause() 和 autoResume() 方法。
当将声音样本加载到 SoundPool 中时,我们必须等到它们被正确加载后才能开始播放它们。在我们的 onLoadComplete() 回调中,我们检查加载的状态,然后根据它是哪种声音,我们设置它进行播放。如果是蟋蟀的声音,我们会打开循环播放(第五个参数的值为 -1 )。对于其他人,我们会在一段时间后播放声音。时间值以毫秒为单位。注意音量的设置。Android 提供了 AudioManager 让我们知道当前的音量设置。我们还从 AudioManager 获得最大音量设置,因此我们可以计算出介于 0 和 1 之间的 play() 的音量值(作为一个浮点数)。 play() 方法实际上为左右声道取一个单独的音量值,但我们只是将两者都设置为当前音量。同样,优先级应该设置为 1。 play() 方法的最后一个参数用于设置回放速率。该值应介于 0.5 和 2.0 之间,1.0 为正常值。
我们的 queueSound() 方法使用一个处理器来基本上建立一个未来的事件。我们的 Runnable 将在延迟期过去后运行。我们检查以确保我们仍然有一个 SoundPool 可以播放,然后我们播放一次声音,并安排相同的声音在与之前相同的时间间隔后再次播放。因为我们用不同的声音 id 和不同的延迟来调用 queueSound() ,所以效果有点像动物声音的随机播放。
当您运行这个示例时,您将听到蟋蟀、黑猩猩、公鸡、狗和吼声(我们认为是熊)。当其他动物来来去去的时候,蟋蟀在不停地鸣叫。关于 SoundPool 的一个好处是它让我们可以同时播放多种声音,而不需要我们做任何实际的工作。此外,我们不会让设备负担太重,因为声音是在加载时解码的,我们只需要将声音比特馈送给硬件。
如果您点按该按钮,蟋蟀会停止鸣叫,当前播放的任何其他动物声音也会停止。然而, autoPause() 方法并不能阻止新声音的播放。你会在几秒钟内再次听到动物的声音(除了蟋蟀的叫声)。因为我们已经把声音排到了未来,我们仍然会听到那些声音。事实上, SoundPool 没有办法阻止现在和未来的所有声音。你需要自己停下来。只有我们再次点击按钮恢复声音,蟋蟀才会回来。但即使这样,我们也可能会失去蟋蟀,因为如果达到同时播放样本的最大数量,SoundPool 将丢弃最老的声音,为新的声音腾出空间。
使用 JetPlayer 播放声音
SoundPool 是一款不错的播放器,但内存限制可能会让它难以完成工作。当你需要播放同步声音时,另一个选择是 JetPlayer 。为游戏定制的 JetPlayer 是一个非常灵活的工具,可以播放大量的声音,并协调这些声音与用户动作。使用乐器数字接口(MIDI) 定义声音。
JetPlayer 的声音是用一种特殊的 JETCreator 工具制作的。这个工具是在 Android SDK 工具目录下提供的,尽管你也需要安装 Python 才能使用它,而且它仅限于 Mac OSX 和 Windows SDK 包。生成的 JET 文件可以读入您的应用,并设置声音进行播放。整个过程有些复杂,超出了本书的范围,所以我们将在本章末尾的“参考资料”部分为您提供更多信息。
用异步播放器播放背景声音
如果你想要的只是播放一些音频,并且不想占用当前线程,那么 AsyncPlayer 可能就是你要找的。音频源作为 URI 传递给该类,因此音频文件可以是本地的,也可以是网络上的远程文件。这个类自动创建一个后台线程来处理获取音频和开始播放。因为是异步的,所以你不会确切知道音频什么时候开始。你也不知道它什么时候结束,甚至不知道它是否还在播放。但是,您可以调用 stop() 来停止播放音频。如果您在之前的音频播放完毕之前再次调用 play() ,之前的音频将立即停止,新的音频将在未来的某个时间开始播放,此时一切都已设置好并已获取。这是一个非常简单的类,提供了一个自动后台线程。清单 20-5 显示了你的代码应该如何实现这一点。
清单 20-5 。?? 用 AsyncPlayer 播放音频
private static final String TAG = "AsyncPlayerDemo";
private AsyncPlayer mAsync = null;
[ ... ]
mAsync = new AsyncPlayer(TAG);
mAsync.play(this, Uri.parse("file://” + “/perry_ringtone.mp3"),
false, AudioManager.STREAM_MUSIC);
[ ... ]
@Override
protected void onPause() {
mAsync.stop();
super.onPause();
}
使用 AudioTrack 的低级音频回放
到目前为止,我们一直在处理来自文件的音频,无论是本地文件还是远程文件。如果您想深入到一个较低的层次,也许是从一个流中播放音频,您需要研究一下 AudioTrack 类。除了像 play() 和 pause() , AudioTrack 这样的常用方法,还提供了向音频硬件写入字节的方法。这个类给了你对音频回放的最大控制权,但是它比本章到目前为止讨论的音频类要复杂得多。我们的一个在线伙伴示例应用使用了 AudioRecord 类。 AudioRecord 类与 AudioTrack 类非常相似,因此为了更好地理解 AudioTrack 类,请参考后面的 AudioRecord 示例。
关于 MediaPlayer 的更多信息
一般来说,MediaPlayer 是非常系统化的,所以您需要以特定的顺序调用操作来正确地初始化 MediaPlayer 并为播放做准备。下面的列表总结了使用媒体 API 时应该知道的一些其他细节:
- 一旦设置了 MediaPlayer 的数据源,就不能轻易将其更改为另一个——您必须创建一个新的 MediaPlayer 或调用 reset() 方法来重新初始化播放器的状态。
- 调用 prepare() 后,可以调用 getCurrentPosition() 、 getDuration() 、 isPlaying() 来获取玩家的当前状态。也可以在调用 prepare() 后调用 setLooping() 和 setVolume() 方法。如果您使用了 prepareAsync() ,那么您应该等到 onPrepared() 被调用后再使用任何其他方法。
- 调用 start() 后,可以调用 pause() 、 stop() 、 seekTo() 。
- 你创建的每一个媒体播放器都会使用大量的资源,所以当你使用完媒体播放器时,一定要调用 release() 方法。在视频播放的情况下, VideoView 会处理这一点,但是如果你决定使用 MediaPlayer 而不是 VideoView ,你就必须手动处理。在接下来的章节中会有更多关于 VideoView 的内容。
- MediaPlayer 与几个监听器配合使用,您可以使用它们对用户体验进行额外的控制,包括 OnCompletionListener 、 OnErrorListener 和 OnInfoListener 。例如,如果您正在管理一个音频播放列表,当一个片段完成时,将调用 OnCompletionListener ,以便您可以将下一个片段排队。
我们关于播放音频内容的讨论到此结束。现在我们将注意力转向播放视频。正如您将看到的,引用视频内容类似于引用音频内容。
播放视频内容
在本节中,我们将讨论使用 Android SDK 播放视频。具体来说,我们将讨论从 web 服务器播放视频和从 SD 卡播放视频。可以想象,视频回放比音频回放要复杂一些。幸运的是,Android SDK 提供了一些额外的抽象来完成大部分繁重的工作。
注意在模拟器中回放视频不是很可靠。如果成功了,那太好了。但是如果不能,试着在一个设备上运行。因为模拟器必须只使用软件来运行视频,所以它可能很难跟上视频,并且您可能会得到意想不到的结果。
播放视频比播放音频需要更多的努力,因为除了音频之外,还有一个视觉组件要处理。为了消除一些痛苦,Android 提供了一个名为 android.widget.VideoView 的专门视图控件,它封装了 MediaPlayer 的创建和初始化。要播放视频,您需要在用户界面中创建一个 VideoView 小部件。然后设置视频的路径或 URI,并触发 start() 方法。清单 20-6 演示了 Android 中的视频播放。
清单 20-6 。 使用媒体 API 播放视频
<?xml version="1.0" encoding="utf-8"?>
<!-- This file is /res/layout/main.xml -->
<LinearLayout
android:layout_width="fill_parent" android:layout_height="fill_parent"
xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)">
<VideoView android:id="@+id/videoView"
android:layout_width="200px" android:layout_height="200px" />
</LinearLayout>
// This file is MainActivity.java
import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.widget.MediaController;
import android.widget.VideoView;
public class MainActivity extends Activity {
/** Called when the activity is first created. */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(R.layout.main);
VideoView videoView =
(VideoView)this.findViewById(R.id.videoView);
MediaController mc = new MediaController(this);
videoView.setMediaController(mc);
videoView.setVideoURI(Uri.parse(
"[`www.androidbook.com/akc/filestorage/android/`](http://www.androidbook.com/akc/filestorage/android/)" +
"documentfiles/3389/movie.mp4"));
/* videoView.setVideoPath(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES) +
"/movie.mp4");
*/
videoView.requestFocus();
videoView.start();
}
}
清单 20-6 演示了位于www . Android book . com/AKC/filestorage/Android/document files/3389/movie . MP4的文件的视频回放,这意味着运行代码的应用需要请求 Android . permission . internet 权限。所有回放功能都隐藏在 VideoView 类之后。事实上,你所要做的就是将视频内容传送给视频播放器。应用的用户界面如图 20-2 所示。
图 20-2 。启用了媒体控制的视频播放 UI
当这个应用运行时,您会在屏幕底部看到按钮控件大约三秒钟,然后它们会消失。您可以通过单击视频帧中的任意位置来找回它们。当我们回放音频内容时,我们只需要显示按钮控件来开始、暂停和重启音频。我们不需要音频本身的视图组件。当然,对于视频,我们需要按钮控件以及观看视频的东西。对于这个例子,我们使用一个 VideoView 组件来显示视频内容。但是我们没有创建自己的按钮控件(如果我们愿意,我们仍然可以这样做),而是创建了一个媒体控制器来为我们提供按钮。如图 20-2 和清单 20-6 所示,您可以通过调用 setMediaController() 来设置 VideoView 的媒体控制器,以启用播放、暂停和查找控件。如果您想用自己的按钮以编程方式操作视频,可以调用 start() 、 pause() 、 stopPlayback() 和 seekTo() 方法。
请记住,在这个例子中,我们仍然使用了一个 media player——只是我们没有看到它。事实上,你可以直接在 MediaPlayer 中“播放”视频。如果你回到清单 20-1 中的例子,在你的 SD 卡上放一个电影文件,并在 AUDIO_PATH 中插入电影的文件路径,你会发现即使你看不到视频,它也能很好地播放音频。
鉴于 MediaPlayer 有一个 setDataSource() 方法, VideoView 没有。 VideoView 改为使用 setVideoPath() 或 setVideoURI() 方法。假设您将一个电影文件放在您的 SD 卡上,您修改清单 20-6 中的代码,注释掉 setVideoURI() 调用,取消注释 setVideoPath() 调用,根据需要调整电影文件的路径。当您再次运行该应用时,您将在视频视图中听到并看到视频。从技术上讲,我们可以用下面的代码调用 setVideoURI() 来获得与 setVideoPath() 相同的效果:
videoView.setVideoURI(Uri.parse("file://" +
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES) + "/movie.mp4"));
你可能已经注意到 VideoView 没有像 MediaPlayer 那样从文件描述符中读取数据的方法。你可能也注意到了 MediaPlayer 有几个方法可以将 SurfaceHolder 添加到 MediaPlayer (一个 SurfaceHolder 就像一个图像或视频的视窗)。 MediaPlayer 方法之一是 create(Context context,Uri uri,surface holder),另一个是 set display(surface holder)。
关于录音和高级媒体的额外在线章节
现在,您已经掌握了媒体播放的许多方面,包括在应用中构建自己的音频和视频功能的各种方法,还有一些领域可以探索,这些领域本身几乎就是一本书的内容。因此,我们将它们放在另一个在线奖金章节中,探讨以下内容:
- 用 MediaRecorder 、 AudioRecord 等技术录音
- 从头开始录像
- 用于视频录制的相机和摄像机配置文件
- 使用 intents 和 MediaStore 类让其他应用帮你完成所有的录制工作!
看一下音频和视频记录奖励章节的在线材料。
参考
以下是一些对您可能希望进一步探索的主题有帮助的参考:
- :与本书相关的可下载项目列表。对于本章中的项目,寻找一个名为 proandroid 5 _ Ch20 _ media . zip 的 zip 文件。这个 zip 文件包含本章中的所有项目,列在单独的根目录中。还有一个自述。TXT 文件,它准确地描述了如何从这些 zip 文件之一将项目导入 Eclipse。
developer . Android . com/guide/topics/media/jet/jet creator _ manual . html
:jet creator 工具的用户手册。您可以使用它来创建一个 JET 声音文件,以便使用 JetPlayer 播放。JETCreator 仅适用于 Windows 和 Mac OS。要查看实际运行的 JetPlayer,请将 JetBoy 示例项目从 Android SDK 加载到 Eclipse 中,构建并运行它。请注意,发射按钮是中间的方向键。
摘要
以下是本媒体章节中关于音频和视频的主题摘要:
- 通过媒体播放器播放音频
- 为 MediaPlayer 提供音频的几种方式,从本地应用资源到文件,再到网络流
- 使用 MediaPlayer 正确播放音频的步骤
- SoundPool 及其同时播放多种声音的能力
- SoundPool 在处理音频量方面的限制
- AsyncPlayer 非常有用,因为声音通常需要在后台处理
- AudioTrack ,使用 VideoView 提供对音频播放视频的低级访问
二十一、主屏幕小部件
Android 中的主屏幕小部件在 Android 的主屏幕上呈现频繁变化的信息。主屏幕小部件是显示在主屏幕上的断开连接的视图。这些视图的数据内容由后台进程定期更新,或者仅作为静态视图保存。
例如,一个电子邮件主屏幕小部件可能会提醒您要阅读的未处理电子邮件的数量。小工具可能只显示电子邮件的数量,而不是邮件本身。单击电子邮件计数可能会将您带到显示实际电子邮件的活动。这些甚至可以是外部电子邮件源,如 Yahoo、Gmail 和 Hotmail,只要设备能够通过 HTTP 或其他连接机制访问计数。
在 Android SDK 中,小部件是声明式定义的。小部件定义包含以下内容:
- 要在主屏幕上显示的视图布局,以及它应该适合主页的大小。
- 指定更新频率的计时器。
- 一个名为 widget provider 的广播接收器 Java 类,它可以响应计时器更新,以便通过填充数据以某种方式改变视图。
- 一个 activity 类,负责收集进一步配置要显示的小部件所需的输入。
定时器、接收器和配置活动是可选的。一旦定义了小部件并提供了 Java 类,用户就可以将小部件拖到主页上。视图和相应的 Java 类是以这样一种方式构建的,它们相互之间是断开的。例如,任何 Android 服务或活动都可以使用其布局 id 检索视图,用数据填充视图(就像填充模板一样),并将其发送到主屏幕。一旦视图被发送到主屏幕,它就会从底层 Java 代码中移除。
在我们向您展示如何实现一个小部件之前,我们将首先向您概述一个最终用户是如何使用小部件的。
主屏幕小工具的用户体验
Android 中的 Home screen widget 功能允许你选择一个预编程的 widget 放在主屏幕上。放置后,如果需要,小部件将允许您使用活动(定义为小部件包的一部分)来配置它。在真正研究小部件是如何实现的细节之前,理解这种交互是很重要的。
我们将带您浏览我们为本章创建的名为“生日”的小部件。我们将在本章的后面给出它的源代码。首先,我们将使用这个小部件作为我们演练的示例。由于随后会有源代码,我们需要你仔细阅读图片,而不是在你的屏幕上寻找这个小部件。如果您遵循提供的图形和解释,您将了解生日小部件的性质和行为,这将在我们随后编写代码时使事情变得清楚。
让我们通过定位我们想要的小部件并在主屏幕上创建它的一个实例来开始这个旅程。访问可用小部件列表的方式因 Android 版本而异。不过,通常情况下,小工具列表与设备上可用的应用列表放在一起。图 21-1 中有一个来自 API 16(或 Android 的 Jellybean 版本)的例子。
图 21-1 。主屏幕小工具选择列表
在图 21-1 的小部件列表中,生日小部件 就是为这一章设计的。如果你选择这个小工具,Android 允许你把它拖到主屏幕的一个页面上。Android 将在主屏幕上创建一个相应的 widget 实例,看起来像图 21-2 中的生日 Widget 示例。
图 21-2 。生日小工具示例
图 21-2 中的生日小工具会在它的头部显示这个人的名字,这个人的生日还有几天,今年的出生日期是什么时候,以及一个购买礼物的链接。您可能想知道人名和出生日期是如何配置的。如果您想要这个小部件的两个实例,每个实例包含不同人的姓名和出生日期,该怎么办?这就是小部件配置活动发挥作用的地方,也是我们接下来要讨论的主题。
了解小部件配置活动
小部件定义可选地包括称为小部件配置活动的活动的规范。当您从主页小部件选择列表中选择一个小部件来创建小部件实例时,Android 会调用相应的小部件配置活动(如果为它定义了一个活动的话)。这个活动是你需要编码的东西。
如果是我们的 BirthdayWidget ,这个配置活动会提示你输入人名和即将到来的生日,如图图 21-3 所示。配置活动的责任是将该信息保存在一个持久的位置,以便当在窗口小部件提供者上调用更新时,窗口小部件提供者将能够定位该信息并更新离生日的天数。
图 21-3 。生日小工具配置活动
注意当用户选择在主屏幕上创建两个生日小部件实例时,配置活动将被调用两次(每个小部件实例调用一次)。
在内部,Android 通过分配唯一的 id 来跟踪小部件实例。这个惟一的小部件实例 ID 被传递给 Java 回调函数和 configurator Java 类,以便初始配置和更新可以指向主页上正确的小部件实例。在图 21-2 中,在字符串 satya:3 的后面部分, 3 是 widget 实例 ID。
了解小部件的生命周期
小部件的生命周期有以下几个阶段:
- 小部件定义
- 小部件实例创建
- onUpdate() (时间间隔到期时)
- 对点击的响应(在主屏幕的小部件视图上)
- 小工具删除(从主屏幕)
- 卸载
我们现在将详细介绍这些阶段。
理解小部件定义阶段
小部件定义从 Android 清单文件中的小部件提供者类的定义开始。清单 21-1 显示了我们为清单文件中的本章 BDayWidgetProvider 设计的 AppWidgetProviderT4 的定义。
清单 21-1 。Android 清单文件中的小部件定义
<!-- filename: AndroidManifest.xml, project: ProAndroid5_ch21_TestWidgets.zip -->
<manifest..>
<application>
....
<receiver android:name=".BDayWidgetProvider">
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/bday_appwidget_provider" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
...
<activity>
.....
</activity>
</application>
</manifest>
这个定义表明有一个名为 BDayWidgetProvider 的广播接收器 Java 类,它接收应用小部件广播更新消息。清单 21-1 中的 widget 类定义还指向一个 XML 文件@ XML/bday _ app widget _ provider,它是/RES/XML/bday _ app widget _ provider . XML。这个 XML 文件在清单 21-2 中。这个小部件定义文件有许多关于这个小部件的东西,比如它的布局资源文件、更新频率等等。
清单 21-2 。小部件提供者信息 XML 文件中的小部件视图定义
<!-- /res/xml/bday_appwidget_provider.xml(ProAndroid5_ch21_TestWidgets.zip) -->
<appwidget-provider xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:minWidth="150dp"
android:minHeight="120dp"
android:updatePeriodMillis="43200000"
android:initialLayout="@layout/bday_widget"
android:configure="com.androidbook.BDayWidget.ConfigureBDayWidgetActivity"
android:resizeMode="horizontal|vertical"
android:previewImage="@drawable/some_preview_image_icon"
>
</appwidget-provider>
这个 XML 文件称为应用小部件提供者信息文件。在内部,这被翻译成 Java 类 AppWidgetProviderInfo。该文件确定布局的宽度和高度分别为 150dp 和 120dp 。该定义文件还指示更新频率为 12 小时转换为毫秒。小部件定义还通过 initialLayout 属性指向一个布局文件。这个布局文件(见未来的清单 21-6 )产生了如图图 21-2 所示的部件外观。
了解调整大小模式属性
从 SDK 3.1 开始,用户能够调整放置在他们的一个图像上的小部件的大小。当用户长按小部件时,他们会看到调整大小手柄,然后可以使用这些手柄来调整大小。这种调整大小可以是水平的、垂直的或无。您可以组合水平和垂直来在两个维度上调整小部件的大小,如清单 21-2 所示。然而,为了利用这一点,您的小部件控件应该以这样一种方式进行布局,即它们可以使用它们的布局参数来扩展和收缩。没有回调来告诉你你的部件有多大。
了解预览图像属性
清单 21-2 中的预览图像属性指出了什么图像或图标用于显示可用部件列表中的部件。如果省略它,默认行为是显示应用包的主图标,这在清单文件中有所指示。
了解小部件布局:initialLayout 属性
小部件视图的布局被限制为只包含某些类型的视图元素。小部件布局中允许的视图通过一个名为 RemoteViews 的接口公开,并且只有某些视图可以组成这个布局。清单 21-3 中显示了一些允许的视图元素。注意它们的子类不被支持——只支持那些包含在清单 21-3 中的子类。
清单 21-3 。 RemoteViews 中允许的视图控件
FrameLayout
LinearLayout
RelativeLayout
GridLayout
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
这个列表可能会随着每个版本的发布而增加。限制远程视图中允许的内容的主要原因是,这些视图与实际控制它们的进程是断开的。这些窗口小部件视图由 Home 应用这样的应用托管。这些视图的控制器是由计时器调用的后台进程。因此,这些视图被称为远程视图。有一个相应的 Java 类叫做 RemoteViews ,允许访问这些视图。换句话说,程序员不能直接访问这些视图来调用它们的方法。您只能通过远程视图访问这些视图(就像看门人一样)。
当我们在下一个主要部分探索这个例子时,我们将会涉及到一个 RemoteViews 类的相关方法。现在,记住在小部件布局文件中只允许使用清单 21-3 中有限的一组视图。
了解配置属性
小部件定义(清单 21-2 )使用配置属性来指定用户创建小部件实例时需要调用的配置活动。清单 21-2 中的指定的配置活动是 ConfigureBDayWidgetActivity。这个活动(图 21-3 )和任何其他 Android 活动一样。此活动的表单字段用于收集小部件实例所需的信息。
了解小部件实例创建阶段
当用户选择一个小部件来创建一个小部件实例时,Android 调用配置活动(图 21-3 ),如果它是在小部件的配置 XML 文件中定义的。如果未定义此配置活动,则跳过此阶段,小部件直接显示在主页上。调用此配置活动时,它会执行以下操作:
- 从启动配置活动的调用意图接收小部件实例 ID。
- 通过表单字段提示用户收集特定于小部件实例的信息。
- 持久化小部件实例信息,以便对 AppWidgetProvider 的 onUpdate 方法的后续调用可以访问这些信息。
- 通过检索小部件视图布局准备第一次显示小部件视图,并用它创建一个 RemoteViews 对象。
- 调用 RemoteViews 对象上的方法来设置单个视图对象的值,比如文本和图像。
- 还使用 RemoteViews 对象来注册小部件的任何子视图上的任何 onClick 事件。
- 告诉 AppWidgetManager 使用小部件的实例 ID 在主屏幕上绘制 RemoteViews 。
- 返回小部件 ID,并关闭。
注意,在这种情况下,小部件的第一次填充是由配置活动完成的,而不是由 AppWidgetProvider 的 onUpdate() 方法完成的。
注意配置活动是可选的。如果没有指定配置活动,调用将直接转到 AppWidgetProvider 的 onUpdate() 方法。由 onUpdate() 来更新视图。
Android 将为用户创建的每个小部件实例执行这个过程。除了调用配置活动,Android 还调用 AppWidgetProvider 的 onEnabled 回调。让我们通过查看我们的 BDayWidgetProvider 的外壳来简要考虑一下 AppWidgetProvider 类上的回调(参见清单 21-4 )。我们将在后面的清单 21-10 中检查这个文件的完整清单。
清单 21-4 。小部件提供者外壳
// filename: BDayWidgetProvider.java(ProAndroid5_ch21_TestWidgets.zip)
public class BDayWidgetProvider extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds){}
public void onDeleted(Context context, int[] appWidgetIds){}
public void onEnabled(Context context){}
public void onDisabled(Context context) {}
}
one enabled()回调方法表示主屏幕上至少有一个小部件实例正在运行。这意味着用户必须至少将小部件放到主页上一次。在这个调用中,您将需要为这个广播接收器组件启用接收消息(您将在清单 21-10 中看到这一点)。SDK 基类 AppWidgetProvider 具有启用或禁用接收广播消息的功能。
当用户将小部件实例视图拖到垃圾桶时,调用 onDeleted() 回调方法。在这里,您需要删除为该小部件实例保存的任何持久值。
从主屏幕上移除最后一个小部件实例后,调用 onDisabled() 回调方法。当用户将小部件的最后一个实例拖到垃圾箱时,就会发生这种情况。您应该使用这个方法来取消对接收任何针对这个组件的广播消息的兴趣(您将在清单 21-9 中看到这个)。
每当清单 21-2 中的指定的定时器到期时,就会调用 onUpdate() 回调方法。如果没有配置活动,在第一次创建小部件实例时也会调用这个方法。如果存在配置活动,则在创建小部件实例时不会调用此方法。当计时器以指示的频率到期时,将随后调用此方法。
了解 onUpdate 阶段
一旦小部件实例出现在主屏幕上,下一个重要事件就是计时器到期。Android 将调用 onUpdate() 来响应那个定时器。因为 onUpdate() 是通过广播接收器调用的,所以相应的 Java 进程将被加载并保持活动状态,直到该调用结束。一旦调用返回,该进程将准备好被删除。
一旦有了更新 onUpdate() 方法中的小部件所需的数据,就可以调用 AppWidgetManager 来绘制远程视图。这表明 AppWidgetProvider 类是无状态的,甚至可能无法在调用之间维护静态变量。这是因为包含这个广播接收器类的 Java 进程可能会在两次调用之间被关闭和重建,从而导致静态变量的重新初始化。
因此,如果需要的话,您需要想出一个记住状态的方案。您可以将 widget 实例的状态保存在持久性存储中,如文件、共享首选项或 SQLite 数据库。在本章的例子中,我们使用共享参数作为持久性 API。
注意为了省电,谷歌建议更新的持续时间超过一个小时,这样设备就不会太频繁地被唤醒。从 2.0 API 开始,更新超时限制为 30 分钟或更长时间。
对于更短的持续时间,比如只有几秒钟,您需要使用 AlarmManager 类中的工具自己调用这个 onUpdate() 方法。当您使用 AlarmManager 时,您还可以选择不调用 onUpdate() ,而是在警报回调中执行 onUpdate() 的工作。参考第十七章中的使用警报管理器。
这是您在 onUpdate() 方法中通常需要做的事情:
- 确保配置器已经完成工作;否则,就返回。在 2.0 及更高版本中,这应该不是问题,因为预计持续时间会更长。否则,根据更新间隔(当它太小时),有可能在用户完成配置器中的小部件配置之前调用 onUpdate() 。
- 检索该小部件实例的持久化数据。
- 检索小部件视图布局,并用它创建一个 RemoteViews 对象。
- 调用 RemoteViews 上的方法来设置单个视图对象的值,比如文本和图像。
- 通过使用挂起的意图,在任何视图上注册任何 onClick 事件。
- 告诉 AppWidgetManager 使用实例 ID 绘制更新后的 RemoteViews 。
正如你所看到的,配置器最初做的和 onUpdate() 方法做的有很多重叠。您可能希望在两个地方之间重用该功能。
了解小部件视图鼠标点击事件回调
如上所述, onUpdate() 方法使小部件视图保持最新。小部件视图和该视图中的子元素可以为鼠标点击注册回调。通常, onUpdate() 方法使用一个挂起的意图来注册一个事件动作,比如鼠标点击。这个动作可以启动一个服务或者启动一个活动,比如打开一个浏览器。
如果需要,这个被调用的服务或活动可以使用小部件实例 ID 和 AppWidgetManager 与视图进行通信。因此,重要的是,挂起的意图带有小部件实例 ID。
删除小部件实例
小部件实例可能发生的另一个不同事件是它可能被删除。为此,用户必须长按主屏幕上的小工具。这将使垃圾桶显示在主屏幕上。然后,用户可以将小部件实例拖到垃圾桶,以便从屏幕上删除小部件实例。
这样做将调用小部件提供者的 onDelete() 方法。如果您保存了这个小部件实例的任何状态信息,您将需要在这个 onDelete 方法中删除该数据。
如果刚刚被删除的小部件实例是这种类型的最后一个小部件实例,Android 也会调用 onDisable() 。您将使用这个回调来清除为所有小部件实例存储的任何持久性属性,并从小部件 onUpdate() 广播中注销回调。
卸载 Widget 包
如果你打算卸载并安装你的的新版本,有必要清理小部件。包含这些小部件的 apk 文件。
建议您在尝试卸载软件包之前移除或删除所有小部件实例。按照“删除小部件实例”一节中的说明删除每个小部件实例,直到一个都没有。
然后,您可以卸载并安装新版本。如果您使用 Eclipse ADT 来开发小部件,这一点尤其重要,因为在开发期间,每次运行应用时,ADT 都会尝试这样做。因此,在两次运行之间,一定要删除小部件实例。
实现一个示例小部件应用
到目前为止,我们已经介绍了小部件背后的理论和方法。让我们创建一个样例小部件,它的行为被用作解释小部件架构的例子。我们将开发、测试和部署这个生日小部件。
每个生日小部件实例将显示一个名字、下一个生日的日期,以及从今天到生日还有多少天。它还会创建一个 onClick 区域,你可以在那里点击购买礼物。这个点击会打开一个浏览器,带你去 www.google.com 的。
完成的小部件的布局应该看起来像图 21-4 。
图 21-4 。生日小工具的外观
这个小部件的实现由以下与小部件相关的文件组成。整个项目也可以从本章“参考资料”一节中提到的 URL 下载。
基本档案有
- AndroidManifest.xml :定义 AppWidgetProvider 的地方(参见清单 21-5 )
- RES/XML/bday _ app Widget _ provider . XML:Widget 尺寸和布局(见清单 21-2 )
- RES/layout/bday _ widget . XML:小部件布局(参见清单 21-6 )
- res/drawable/box1.xml :为部件布局的部分提供框(参见清单 21-7 )
- src/.../bdaywidgetprovider . Java:AppWidgetProvider 类的实现(参见清单 21-10 )
这些文件实现了小部件配置活动:
- src/.../configurebdaywidgetactivity . Java:配置活动(参见清单 21-8 )
- Layout/edit _ bday _ widget . XML:取名字和生日的布局(见清单 21-9 )
这些文件使用首选项存储/检索小部件实例的状态:
- src/.../iwidgetmodelsavecontract . Java:用于保存和检索小部件数据的契约(参见可下载项目中的)
- src/.../APrefWidgetModel.java :抽象的基于首选项的小部件模型,将小部件数据保存在首选项中(参见可下载项目中的)
- src/.../BDayWidgetModel.java :保存小部件视图数据的小部件模型(参见可下载项目中的)
- src/.../Utils.java :一些工具类(参见可下载项目)
我们将浏览一些关键文件,并解释任何需要进一步考虑的其他概念。您可以从本章的可下载项目中获得其余的文件。
定义小部件提供者
对于生日小部件项目,清单文件在清单 21-5 中。它具有作为广播接收器的小部件提供者 BDayAppWidgetProvider 的声明,以及配置活动 ConfigureBDayWidgetActivity 的定义。注意小部件提供者定义是如何指向小部件定义 XML 文件@ XML/bday _ app widget _ provider。
清单 21-5 。 BDayWidget 示例应用的 Android 清单文件
<?xml version="1.0" encoding="utf-8"?>
<!-- file: AndroidManifest.xml(ProAndroid5_ch21_TestWidgets.zip) -->
<manifest xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
package="com.androidbook.BDayWidget"
android:versionCode="1"
android:versionName="1.0.0">
<application android:icon="@drawable/icon"
android:label="Birthday Widget">
<!--
**********************************************************************
* Birthday Widget Provider Receiver
**********************************************************************
-->
<receiver android:name=".BDayWidgetProvider">
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/bday_appwidget_provider"/>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
</receiver>
<!--
**********************************************************************
* Birthday Provider Configuration activity
**********************************************************************
-->
<activity android:name=".ConfigureBDayWidgetActivity"
android:label="Configure Birthday Widget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="3"/>
</manifest>
由下面一行中的“生日小工具”标识的应用标签
<application android:icon="@drawable/icon" android:label="Birthday Widget">
显示在主页的小部件选择列表中(见图 21-2 )。您还可以在小部件定义 XML 文件(清单 21-2 )中指定一个当小部件被列出时显示的替代图标(也称为预览)。配置活动定义类似于任何其他普通活动,只是它需要声明自己能够响应 Android . app widget . action . app widget _ CONFIGURE 操作。
参考清单 21-2 中的小部件定义文件@ XML/bday _ app widget _ provider,查看小部件大小和布局文件的路径是如何指定的。这个布局文件就像 Android 中任何其他视图的布局文件一样。清单 21-6 显示了我们用来生成图 21-4 所示的小部件布局的布局文件。
清单 21-6 。 BDayWidget 的 Widget 视图布局定义
<?xml version="1.0" encoding="utf-8"?>
<!-- res/layout/bday_widget.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:background="@drawable/box1">
<TextView
android:id="@+id/bdw_w_name"
android:layout_width="fill_parent" android:layout_height="40sp"
android:text="Anonymous" android:background="@drawable/box1"
android:gravity="center" android:layout_weight="0"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:layout_weight="1">
<TextView
android:id="@+id/bdw_w_days"
android:layout_width="wrap_content" android:layout_height="fill_parent"
android:gravity="center" android:layout_weight="50"
android:text="0" android:textSize="30sp" />
<TextView
android:id="@+id/bdw_w_button_buy"
android:layout_width="wrap_content" android:layout_height="fill_parent"
android:layout_weight="50" android:gravity="center"
android:textSize="20sp" android:text="Buy"
android:background="#FF6633"/>
</LinearLayout>
<TextView
android:id="@+id/bdw_w_date"
android:layout_width="fill_parent" android:layout_height="40sp"
android:gravity="center" android:layout_weight="0"
android:text="1/1/2000" android:background="@drawable/box1"/>
</LinearLayout>
一些控件还使用一个名为 box1.xml 的形状定义文件来定义边框。形状定义文件的代码如清单 21-7 所示。
清单 21-7 。边界框形状定义
<!-- res/drawable/box1.xml -->
<shape xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)">
<stroke android:width="4dp" android:color="#888888"/>
<padding android:left="2dp" android:top="2dp"
android:right="2dp" android:bottom="2dp"/>
<corners android:radius="4dp"/>
</shape>
实施小部件配置活动
以生日小部件为例,小部件职责的配置在 ConfigureBDayWidgetActivity 中实现。这个类的源代码在清单 21-8 中。
清单 21-8 。实施配置活动
// file: ConfigureBDayWidgetActivity.java(ProAndroid5_ch21_TestWidgets.zip)
public class ConfigureBDayWidgetActivity extends Activity
{
private static String tag = "ConfigureBDayWidgetActivity";
private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.edit_bday_widget);
setupButton(); //setup the save button
//Get the widget instanceid from the intent extra
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
}
private void setupButton(){
Button b = (Button)this.findViewById(R.id.bdw_button_update_bday_widget);
b.setOnClickListener(
new Button.OnClickListener(){
public void onClick(View v) {
saveConfiguration(v);
}
});
}
//Read name and date.
//Call updateAppWidgetLocal to save the values for this instance
//in that method also send the view to the homepage.
//Return the result of the configuration activity to the SDK
//finish the activity.
private void saveConfiguration(View v){
String name = this.getName();
String date = this.getDate();
if (Utils.validateDate(date) == false){
this.setDate("wrong date:" + date);
return;
}
if (this.mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID){
return;
}
updateAppWidgetLocal(name,date);
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
}
private String getName(){
EditText nameEdit =
(EditText)this.findViewById(R.id.bdw_bday_name_id);
String name = nameEdit.getText().toString();
return name;
}
private String getDate(){
EditText dateEdit = (EditText)this.findViewById(R.id.bdw_bday_date_id);
String dateString = dateEdit.getText().toString();
return dateString;
}
private void setDate(String errorDate){
EditText dateEdit = (EditText)this.findViewById(R.id.bdw_bday_date_id);
dateEdit.setText("error");
dateEdit.requestFocus();
}
private void updateAppWidgetLocal(String name, String dob){
//Create an object to hold the data: widgetid, name, and dob
BDayWidgetModel m = new BDayWidgetModel(mAppWidgetId,name,dob);
//Create the view and send it to the home screen
updateAppWidget(this,AppWidgetManager.getInstance(this),m);
//Use the data model object to save the id, name, and dob in prefs
m.savePreferences(this);
}
//A key method where a lot of magic happens
public static void updateAppWidget(Context context,
AppWidgetManager appWidgetManager,
BDayWidgetModel widgetModel)
{
//Construct a RemoteViews Object from the widget layout file
RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.bday_widget);
//Use the control ids in the layout to set values on them.
//Notice that these methods are limited and available on the
//on the RemoteViews object. In other words we are not using the
//TextView directly to set these values.
views.setTextViewText(R.id.bdw_w_name
, widgetModel.getName() + ":" + widgetModel.iid);
views.setTextViewText(R.id.bdw_w_date
, widgetModel.getBday());
//update the name
views.setTextViewText(R.id.bdw_w_days,
Long.toString(widgetModel.howManyDays()));
//Set intents to invoke other activities when widget is clicked on
Intent defineIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse("[`www.google.com`](http://www.google.com)"));
PendingIntent pendingIntent =
PendingIntent.getActivity(context,
0 /* no requestCode */,
defineIntent,
0 /* no flags */);
views.setOnClickPendingIntent(R.id.bdw_w_button_buy, pendingIntent);
// Tell the widget manager to paint the remote view
appWidgetManager.updateAppWidget(widgetModel.iid, views);
}
}
在我们介绍这段代码做什么之前,这个小部件配置活动使用的布局在清单 21-9 中。这种布局很简单。你也可以在图 21-3 中直观地看到这一点。
清单 21-9 。配置活动的布局定义
<?xml version="1.0" encoding="utf-8"?>
<!-- res/layout/edit_bday_widget.xml -->
<LinearLayout xmlns:android="[`schemas.android.com/apk/res/android`](http://schemas.android.com/apk/res/android)"
android:id="@+id/root_layout_id" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<TextView
android:id="@+id/bdw_text1" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="Name:" />
<EditText
android:id="@+id/bdw_bday_name_id" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="Anonymous" />
<TextView
android:id="@+id/bdw_text2" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="Birthday (9/1/2001):" />
<EditText
android:id="@+id/bdw_bday_date_id" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="ex: 10/1/2009" />
<Button
android:id="@+id/bdw_button_update_bday_widget" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="update"/>
</LinearLayout>
回到清单 21-8 中的配置活动代码,它完成以下任务:
- 从调用意图中读取小部件实例 ID
- 使用表单字段收集姓名和出生日期
- 通过加载 widget 布局文件获取 RemoteViews
- 在远程视图上设置文本值
- 通过 RemoteViews 注册待定意向
- 调用 AppWidgetManager 将 RemoteViews 发送给小部件
- 根据这个小部件实例 ID 在首选项中保存姓名和出生日期。这是通过类 BDayWidgetModel 完成的。我们很快会谈到这一点。
- 最后返回一个结果。
注意静态函数 udpateAppWidget 只要知道 widget ID 就可以从任何地方调用。这意味着您可以从设备上的任何地方和任何进程(可视和非可视)更新小部件。
注意我们是如何将小部件 ID 传递回这个配置活动的调用者的。这就是 AppWidgetManager 知道小部件实例的配置活动已经完成的方式。
让我们来谈谈通过清单 21-8 中的 BDayWidgetModel 对象来保存和检索 widget 实例状态。 BDayWidgetModel 对象的作用是存储和检索三个值:小部件实例 ID(主键)、名称和出生日期。这个类使用 preferences API 来保存和读回这些值。或者,您可以使用任何持久性机制来满足这一需求。我们不包括这个类的源代码,因为它实现起来非常简单。在本章的可下载项目中,我们有这个类的一个更广泛的实现,其中我们编写了一个可重用的框架来存储首选项中任何 java 对象的值。我们已经充分记录了源代码,这样您就可以按原样使用它来满足其他需求,或者进一步调整它并使用反射来进一步简化。最终,您将拥有一个非常可扩展的模型框架。由于这不是本章的主要目标,我们在这里没有深入讨论这些细节。对于本章来说,重要的是保存和检索这三个值,即实例 ID、名称和 dob。您可以根据 BDayWidgetModel 上的名称作为指南。
实现小部件提供者
现在让我们通过检查小部件提供者类来看看我们将如何响应小部件的生命周期事件。清单 21-10 实现了小部件提供者类。
清单 21-10 。样例小部件提供程序的源代码:BDayWidgetProvider
// file: BDayWidgetProvider.java(ProAndroid5_ch21_TestWidgets.zip)
public class BDayWidgetProvider extends AppWidgetProvider {
private static final String tag = "BDayWidgetProvider";
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
final int N = appWidgetIds.length;
for (int i=0; i<N; i++) {
int appWidgetId = appWidgetIds[i];
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
public void onDeleted(Context context, int[] appWidgetIds) {
final int N = appWidgetIds.length;
for (int i=0; i<N; i++) {
BDayWidgetModel bwm = BDayWidgetModel.retrieveModel(context, appWidgetIds[i]);
bwm.removePrefs(context);
}
}
public void onEnabled(Context context) {
BDayWidgetModel.clearAllPreferences(context);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName("com.androidbook.BDayWidget",
".BDayWidgetProvider"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
public void onDisabled(Context context) {
BDayWidgetModel.clearAllPreferences(context);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName("com.androidbook.BDayWidget",
".BDayWidgetProvider"),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
private void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
BDayWidgetModel bwm = BDayWidgetModel.retrieveModel(context, appWidgetId);
if (bwm == null) {return;}
ConfigureBDayWidgetActivity.updateAppWidget(context, appWidgetManager, bwm);
}
}
在“小部件的生命周期”一节中,我们讨论了这些方法的职责。对于生日小部件,所有这些方法都利用 BDayWidgetModel 来检索与小部件实例相关联的数据,回调函数被调用。在 BDayWidgetModel 上的这些方法有 removePrefs() 、 retrievePrefs() 和 clearAllPreferences() 。
为该小部件类型的所有小部件实例调用更新回调方法。这个方法必须更新所有的小部件实例。小部件实例作为 id 数组传入。对于每个 id,on update()方法将定位相应的小部件实例模型,并调用配置活动使用的相同方法(参见清单 21-8 )来显示检索到的小部件模型。
在 onDeleted() 方法中,我们实例化了一个 BDayWidgetModel ,然后要求它将自己从首选项持久性存储中删除。
在 one enabled()方法中,因为它在第一个实例出现时只被调用一次,所以我们已经清除了小部件模型的所有持久性,这样我们就可以从头开始了。我们在 onDisabled() 方法中也做了同样的事情,这样就不存在小部件实例的内存。
在 one enabled()方法中,我们启用小部件提供者组件,以便它可以接收广播消息。在 onDisabled() 方法中,我们禁用了组件,这样它就不会寻找任何广播消息。
基于集合的小部件
从 SDK 3.0 开始,Android 已经扩展了小部件,以包括基于集合的小部件。我们在这本书的印刷本中没有空间。我们将把上一版中关于收集部件的章节放在我们的在线网站上以供下载。
资源
以下是本章所涵盖主题的有用参考:
developer . Android . com/guide/topics/appwidgets/index . html
:关于 app widgets 的 Android SDK 官方文档。developer . Android . com/reference/Android/content/shared preferences . html
:shared preferences 管理状态的 API。developer . Android . com/reference/Android/content/shared preferences。Editor.html
:共享推荐。编辑器 API,与共享偏好相关。developer . Android . com/guide/practices/ui _ guidelines/widget _ Design . html
:设计令人愉悦的 widget 布局。developer . Android . com/reference/Android/widget/remote views . html
:remote viewsAPI,用于绘制和操纵 widget 视图。developer . Android . com/reference/Android/app widget/appwidgetmanager . html
:小部件本身由一个小部件管理器类管理。- :撰写本章时使用的研究笔记,包括摘要、研究日志、代码片段和有用的网址。
- :你可以使用这个网址下载关于列表小部件的详细章节。
- :本章可下载的测试项目。本章的 ZIP 文件的名称是 pro Android 5 _ ch21 _ test widgets . ZIP。
摘要
在 Android 中,小部件经常和你的应用一起使用。本章涵盖了创建和配置小部件所需的基本要素。在线提供了关于列表小部件的补充章节。