Android开发指南:获取用户位置

知道用户的位置可以让你的应用程序更加智能并能够传递更好的信息给用户。当开始一个位置感知的Android程序时,你可以利用GPS或Android的网络位置提供者(Location Provider)来获取用户位置。虽然GPS最为精确,但它只能在户外使用,比较耗电,而且不能快速获取到用户的位置。Android的网络位置提供者(Location Provider)利用基站和WIFI判断用户的位置,这种方式的定位在室内室外都可以,而且速度快、耗电少。在应用程序中可以利用GPS和网络提供者,或两者选一个,来获取用户的位置。

确定用户位置的难点

利用一个移动设置来获取用户位置可能有点复杂。有很多原因会导致定位到的位置(无论是哪种方式的定位)是错误或不精确的。一些引起用户位置错误的根源如下:

1、 多位置源。GPS、基站ID和WIFI都能为用户位置提供根据。确定使用和依赖哪一个信息源需要折衷考虑精度、速度和耗电情况。

2、 用户移动。因为用户位置一直在变,所以你必须频繁重新估算以记录用户的移动。

3、 多变的精度。来自于各种不同位置信息源的位置估算并没有一个连贯的精度。10秒前从一个信息源得到的位置可能比一个从另一信息源或同一信息得到的最新位置要精度。

这些问题会使得获取一个可依赖的用户位置显得非常困难。这个文档提供了一些信息帮助你去解决这些获取用户位置的这些难点。同时提供一些建议,让你能在你的程序中为用户提供一个精确、灵敏的地理定位体验。

请求位置更新

在讨论上面提到的定位错误之前,这里先介绍在Android中如何获取用户的位置。

在Android中获取用户位置是通过回调的方法。你可以通过调用requestLocationUpdates()并传递进一个LocationListener来指出你想要从LocationManager中收到的位置更新。你的LocationListener必须实现几个方法,这些方法是在用户位置改变或服务状态改变时被位置管理器(Location Manager)调用的。

下面给出如何定义一个LocationListener和请求位置的代码:

// Acquire a reference to the system Location Manager 
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE); 
// Define a listener that responds to location updates 
LocationListener locationListener = new LocationListener() { 
    public void onLocationChanged(Location location) { 
      // Called when a new location is found by the network location provider. 
      makeUseOfNewLocation(location); 
    } 
    public void onStatusChanged(String provider, int status, Bundle extras) {} 
    public void onProviderEnabled(String provider) {} 
    public void onProviderDisabled(String provider) {} 
  }; 
// Register the listener with the Location Manager to receive location updates 
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, locationListener);

requestLocationUpdates()的第一个参数是要使用到的位置提供器类型,在这个例子中使用了基站网络位置提供器和基于位置的WIFI。你可以用第二个和第三个参数来控制你的监听器接收更新信息的频率,第二个参数表示两次通知之间的最小间隔时间,第三个参数表示两次通知之间最小的位置更改距离,如果都设置为0表示尽可能快地去请求位置通知。最后一个参数是就你定义的接收位置更新回调的LocationListener。

如果要通过GPS提供器请求用户位置更新就要用GPS_PROVIDER代替NETWORK_PROVIDER。你也可以通过调用两次requestLocationUpdates()并分别传入NETWORK_PROVIDER 和GPS_PROVIDER来同时使用GPS和网络定位提供器请求位置更新。

请求用户权限

为了能从NETWORK_PROVIDER 和GPS_PROVIDER接收到位置更新,你必须在manifest文件中定义ACCESS_COARSE_LOCATION 或ACCESS_FINE_LOCATION权限来请求用户权限。代码如下:

<manifest ... > 
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> 
    ... 
</manifest>

如果没有申请这些权限,你的应用程序将无法在运行时请求位置更新。

注:如果你需要通过NETWORK_PROVIDER 和GPS_PROVIDER两种方式同时定位,那么你只需要申请一个ACCESS_FINE_LOCATION权限就行,因为这个权限对于这两种定位方式都适应,而ACCESS_COARSE_LOCATION权限只是对于NETWORK_PROVIDER方式适应。

定义一个最佳执行模型

基于位置的应用程序目前已经相当普遍了,但由于缺乏最佳精度、用户移动、获取位置方式的多样性和节省电池等等原因,使得获取用户位置显得很复杂。在节省电池的情况下,为了克服获取一个准确用户位置的难点,你必须定义一个能指定你的应用程序如何获取用户位置的稳定模型。这个模型包括了何时开始和停止监听更新信息,以及何时使用获取到的位置数据。

获取用户位置的流程

下面是一个典型的获取用户位置流程:

1、 启动应用程序

2、 一段时间后,开始监听从指定位置提供器(location providers)得到的位置更新信息

3、 通过过滤掉一个新的但相对不精确的定位,保存一个“当前最佳估值”

4、 停止监听位置更新信息

5、 利用最新的一个最佳位置估值

图1中,在一个表示应用程序监听位置更新周期的时间轴上,演示了这个模型,也给出了这个过程中会被激发的事件。

这个在其期间可以接收到位置更新信息的时间窗模型支撑了所有你在向应用程序中添加基于位置服务时所需要做的所有事情。

决定何时开始启动更新监听

你可能需要在你的程序运行时就启动更新监听器,或在用户触发了某个属性后启动。需要注意的是,一个长的定位监听时间窗可能会消耗很多电量,而一个周期很短的监听可能无法得到足够的精度。

如上所述,你可以通过调用requestLocationUpdates()方法启动位置更新的监听。

LocationProvider locationProvider = LocationManager.NETWORK_PROVIDER; 
// Or, use GPS location data: 
// LocationProvider locationProvider = LocationManager.GPS_PROVIDER; 
locationManager.requestLocationUpdates(locationProvider, 0, 0, locationListener);

通过最后一个已知位置快速定位

你的位置监听器获取到第一个定位信息可能需要花较长时间,这样用户就需要等待。在你从位置监听器得到一个更为精确的位置前,你应该通过调用getLastKnownLocation(String)方法使用一个缓存中的位置。

LocationProvider locationProvider = LocationManager.NETWORK_PROVIDER; 
// Or use LocationManager.GPS_PROVIDER 
Location lastKnownLocation = locationManager.getLastKnownLocation(locationProvider);

确定何时停止更新监听

逻辑上决定什么时候不需要新的定位信息可能很简单也可能很复杂,这完全取决于你的应用程序要求。在获取位置信息和使用位置信息间有一定间隔时间用来可以改进估值的精确度。一定要记住长时间监听会消耗大量的电量,因此只要你得到你需要的信息后就需要通过调用removeUpdates(PendingIntent)停止更新监听器,如下:

// Remove the listener you previously added 
locationManager.removeUpdates(locationListener);

保存当前最佳估值

可能你已经预计最新的位置信息就是最精确的,然而,因为定位精确的多变性,所以最新的定位信息不一定是最好的。你需要有一个基于多个判断标准的逻辑来选择定位信息。这些判断标准也是随着应用程序的使用案例和现场试验的变化而变化。

以下是你可以来确定定位信息精度有效性的一些步骤:

1、``检查重新获取到的位置信息是否比之前的估值更新;

2、``检查位置要求精度比之前的估值好还是差;

3、``检查新的位置来源于哪个提供器,以决定是否是值得依赖。

关于这个逻辑的一个详细例子如下代码:

private static final int TWO_MINUTES = 1000 * 60 * 2; 
/** Determines whether one Location reading is better than the current Location fix 
  * @param location  The new Location that you want to evaluate 
  * @param currentBestLocation  The current Location fix, to which you want to compare the new one 
  */
protected boolean isBetterLocation(Location location, Location currentBestLocation) { 
    if (currentBestLocation == null) { 
        // A new location is always better than no location 
        return true; 
    } 
    // Check whether the new location fix is newer or older 
    long timeDelta = location.getTime() - currentBestLocation.getTime(); 
    boolean isSignificantlyNewer = timeDelta > TWO_MINUTES; 
    boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES; 
    boolean isNewer = timeDelta > 0; 
    // If it's been more than two minutes since the current location, use the new location 
    // because the user has likely moved 
    if (isSignificantlyNewer) { 
        return true; 
    // If the new location is more than two minutes older, it must be worse 
    } else if (isSignificantlyOlder) { 
        return false; 
    } 
    // Check whether the new location fix is more or less accurate 
    int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); 
    boolean isLessAccurate = accuracyDelta > 0; 
    boolean isMoreAccurate = accuracyDelta < 0; 
    boolean isSignificantlyLessAccurate = accuracyDelta > 200; 
    // Check if the old and new location are from the same provider 
    boolean isFromSameProvider = isSameProvider(location.getProvider(), 
            currentBestLocation.getProvider()); 
    // Determine location quality using a combination of timeliness and accuracy 
    if (isMoreAccurate) { 
        return true; 
    } else if (isNewer && !isLessAccurate) { 
        return true; 
    } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) { 
        return true; 
    } 
    return false; 
} 
/** Checks whether two providers are the same */
private boolean isSameProvider(String provider1, String provider2) { 
    if (provider1 == null) { 
      return provider2 == null; 
    } 
    return provider1.equals(provider2); 
}

修正模型以节省电量和减少数据交换

正如你测试程序一样,你可能会发现一个用来提供合适定位和良好操作的模型也需要进行一些修正。以下是一些选择模型过程中需要修改的一些东西:

降低选择窗的大小

一个更小的监听位置选择窗意味着与GPS和网络位置服务器进行更少的交互,这也减少了电量的消耗。但它也会更少的位置供选出最好的位置估值。

设置一个返回更新信息频率更低的位置提供器

减少选择窗中新的位置更新频率同样能提高电池的利用有效性,但同时会以牺牲精度为代价。这个折衷的数据完全取决于你应用程序的需要。如果你想降低更新的频率,你可以通过提高requestLocationUpdates()方法两个用于表示监听的间隔时间和最短距离的参数。

限制一组提供者

根据你的应用程序使用的环境或期望的精度水平,你可以只使用网络定位提供者或GPS定位,而不需要两者都使用。只使用一种定位服务可以降低电池的使用,也可能会以牺牲精度为代价。

通用应用程序案例

可能会有很多原因使得你需要在程序中获取用户的位置。下面是两个你可以利用用户位置来充实你程序的方案。每一个方案都描述了何时开启和关闭位置监听的经验,这样就可以得到一个好的感知并帮助保护电池的寿命。

用位置来标志用户创建内容

你可能正在做一个用位置来标志用户创建内容的程序。想像一下,用户通过共享他们的位置信息评论了一个餐饮或记录一些补充当前位置信息的内容。一个关于如何进行这种交互的模型(关于这个位置服务)如图2所示:

这个跟之前那个关于如何在代码上获取用户位置的模型是一致的(图1)。为了得到一个最精确的位置,你应该在用户开始创建内容或甚至程序开始运行就开始监听位置更新,并在内容可以被提交或被记录时停止监听。你需要考虑这样一个创建内容的任务会持续多长时间,并判断这个过程的时间是否足够来得到一个位置估值的有效集合。

帮助用户确定何去何从

你可能会创建一个应用程序来为用户提供一系统备选地点,以让用户决定何去何从。例如,希望提供一个周围的餐馆、商店和娱乐的列表,并根据用户位置的不同而改变这个推荐列表的排序。

为了提供这样一个流程,你可能需要改变以下内容:

当获取到一个新的最佳位置估值时重排推荐列表;

当推荐列表顺序确定后停止位置更新监听。

这个模型见图3所示

提供模拟位置数据

当你在开始你的应用程序时,你肯定需要测试获取用户位置模型的性能如何。最简单的方法就是使用一个Android系统的设备进行测试。但是即使你没有真机,你也可以通过Android模拟器提供的模拟位置数据来测试程序中基于位置的属性。有三种方式可以向你的应用程序发送模拟位置数据:用Eclipse、DDMS或在模拟器控制台使用geo命令。

注:提供模拟位置数据是以GPS位置数据的方式输入数据,因此你必须以GPS_PROVIDER的方式请求位置更新,才能正常使用模拟位置数据。

使用Eclipse(由于Eclicpse大部分是英文版,所以这些涉及菜单界面的单词不翻译**@Sam****)**

选择Window > Show View > Other> Emulator Control。

在Emulator Control panel中,输入GPS数据作为经纬度数据,在GPX文件下作为远程回放,在KML文件标识多个地点。(当然需要确认你已经选择在设备面板中选中了一个设备——可以通过Window > Show View > Other > Devices选择)

使用****DDMS

通过使用DDMS工具,你可以用一些不同的方式模拟位置数据:

1、 手动向设备发送一个有效的经纬度

2、 Use a GPX file describing a route for playback to the device

3、 Use a KML file describing individual place marks for sequenced playback to the device. 

在模拟器控制台使用geo命令行

用控制命令行发送模拟位置数据:

1、 在Android模拟器中加载你的应用程序,并打开SDK 的tool目录下的控制台(终端)

2、 连接模拟控制台,如下:

telnet localhost <console-port>

3、 发送位置数据:

geo fix命令发送一个定位经纬度信息

这个命令允许十进制方式表示经纬度,可以米为单位表示海拔高度,如:

geo fix -121.45356 46.51119 4392

geo nmea命令发送NMEA 0138语句

这个命令行允许一个单独的如'$GPGGA' (fix data) 或'$GPRMC' (transit data).的NMEA语句,如下:

       geo nmea $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62