位置策略(Location Strategies)

让你的应用程序了解用户的位置,可以更加智能的给用户发送更好的信息。在开发一款Android版的位置感知应用程序时,你能够利用GPS和Android的网络位置提供器来获取用户的位置。尽管GPS是最准确的,但它只能在户外工作,并且会快速的消耗电池电量,同时返回的位置信息也没有用户想要的那样快。Android的网络位置提供器使用蜂窝基站和Wi-Fi信号来判定用户位置,使用这种方式,可以在屋内和户外来提供位置信息,它响应快,并且耗电很少。要在你的应用程序中获取用户的位置,你能够同时使用GPS和网络位置提供器,或其中一种。

在确定用户位置中所面临的挑战

从移动设备上获取用户位置可能是复杂的。有几个原因可能导致读取的位置(不管使用哪种方式)包含错误和不准确信息。以下是在用位置中所包括的一些错误源:

  • 众多的位置源

    GPS、Cell-ID和Wi-Fi都能够提供用户位置的线索。决定使用和信任哪条线索主要要权衡精度、速度和电池效率。

  • 用户的移动

    因为用户位置的改变,你必须要因此而经常的重新计算用户的位置。

  • 不同的精度

    因为位置估算来源与不同的位置探测源,所以它们的精度并不一致。从一个位置探测源10秒前获取的位置,可能比从另一个位置探测源获取的位置或同一个位置探测源所获取的位置要更准确。

这些问题使得获取一个稳定可靠的用户位置变的很困难。本文所提供的信息会帮助你在遇到这些问题的挑战时,来获取稳定可靠的位置信息。它还提供了一些建议,以便你能够在应用程序中提供准确的反应迅速的位置体验。

请求位置更新

在解决上述位置错误之前,首先要介绍如何在Android上获取用户位置。

通过回调的方式来获得在Android工作的用户位置。通过调用带有LocationListener对象的requestLocationUpdates()方法来指示你想要接收的来自LocationManager对象(位置管理器)的位置更新。你的locationListener接口必须实现位置管理器在用户位置改变或服务改变状态时所要调用的几个回调方法。

例如,下列代码显示了如何定义一个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()方法中的第一个参数是要使用的位置提供器的类型(在这个例子中,是基于蜂窝基站和Wi-Fi的网络位置提供器)。你能够用第二和第三个参数来控制你的监听器接收位置更新的频率---第二个参数是两个通知之间的最小间隔,第三个参数是两个通知之间位置改变的最小距离,把它们都设置为0,尽可能的提高位置通知的频率。最后一个参数是你的LocationListener,它用于接受位置更新的回调。

要请求来自GPS提供器的位置更新,用GPS_PROVIDER来代替NETWORK_PROVIDER。你还能够通过调用2~1次NETWORK_PROVIDER的requestLocationUpdates()方法和一次的GPS_PROVIDER的requestLocationUpdates()方法来请求来自GPS和网络位置提供器的位置更新。

申请用户权限

为了接收来自NETWORK_PROVIDER或GPS_PROVIDER的位置更新,你必须通过声明ACCESS_COARSE_LOCATION或ACCESS_FINE_LOCATION权限来分别的在你的Android清单中申请用户权限,如:

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

没有这些权限,你的应用程序会在请求位置更新时发生运行时错误。

注意:如果你同时使用了NETWORK_PROVIDER和GPS_PROVIDER,那么你只需要申请ACCESS_FINE_LOCATION权限,因为它包含这两个位置提供器的权限。(ACCESS_COARSE_LOCATION只包含了NETWORK_PROVIDER权限。)

定义最佳性能的模型

基于位置的应用程序目前是比较普遍的,但是由于精度不佳,需要使用多种方法来获取用户移动的位置,并且还希望能够达到省电的目的,因此获取用户位置是复杂的。要克服获取良好用户位置的障碍,同时保护电池的消耗,你就必须定义一个一致的模型来指定应用程序如何获取用户位置。这个模型包含了启动和停止监听更新的时机和使用被缓存的位置数据的时机。

获取用户位置的流程

以下是典型的获取用户位置的过程流程:

  • 启动应用程序;

  • 稍后,启动对期望的位置提供器的更新的监听;

  • 通过筛选新的位置,来维持一个当前最优的位置评估;

  • 停止对位置更新的监听;

  • 利用最后评估的最佳位置。

图1用一个可视的时间轴,演示了应用程序中所监听的位置更新,以及这些事件所发生的时机:

这个窗口模型在接收位置更新期间,你需要决定把基于位置的服务添加到你的应用程序中的时机。

确定启动监听位置更新的时机

你可能想要在应用程序一启动就要监听位置更新,或者是在用户激活某个特定功能之后才开始监听位置更新。要注意的时,长时间的监听位置的变化会消耗大量的电池电量,但是短期的监听可能不会提供足够的精度。

如上述Demo,通过调用requestLocationUpdates()方法能够开始监听位置的更新:

String locationProvider = LocationManager.NETWORK_PROVIDER;
// Or, use GPS location data:
// String 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);

保持当前的最佳估算值

你可能期望最近的位置修正是最精确的。但是,因为位置变化修正的精度,不总是最近的就是最好的。你应该包括基于几种标准的选择位置修正的逻辑。这些标准还要根据应用程序所使用的场景和区域来进行测试。

以下是验证位置修正的精度的几个步骤:

  • 检测新获取的位置是否比之前的更新;

  • 检测新获取的位置是否比之前的更精确或更差;

  • 检查新的位置是来源于哪个提供器,并判断它是否更加真实可信。

以下示例详细说明了这些检查的逻辑:

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

选择Window->ShowView->Other->Emulator Control
在模拟器的控制面板中,在位置控制下,键入GPS坐标来作为经纬度坐标,用一个GPX文件来作为路线回放,或者使用一个KML文件来进行多点标注。(必须要保证在设备面板中有一个被选中的设备---在Window->ShowView->Other->Devices中有效。)

使用DDMS

使用DDMS工具,你能够使用以下几种不同的方法来模拟位置数据:

  • 手动发送经纬度坐标给设备;

  • 使用GPX文件给设备描述一个路线;

  • 使用KML文件给设备描述一个顺序播放的独立的位置标注。

在模拟器控制台中使用“geo”命令

以下使用从命令行中发送模拟位置数据的方法:

  • 在Android模拟器中启动你的应用程序,并且打开你的SDK的/tools目录中的终端/控制台(terminal/console);

  • 连接到模拟器控制台:

telnetlocalhost <console-port>
  • 发送位置数据:

    • geo fix命令发送一个固定的地理位置。

      这个命令接受十进制的经纬度和一个可选的以米为单位的海拔高度,例如:
      geofix -121.45356 46.51119 5392

    • geo nmea命令发送一个NMEA0183语句

      这个命令接受一个单一的'$GPGGA'(修正数据)或'$GPRMC’(中转数据)类型的NMEA 语句。例如:
      geo nmea $GPRMC,081836,A,3751.65,S,14507.36,E,000.0,360.0,130998,011.3,E*62

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1