pAdTy_3 构建地理位置和地图的应用程序

2015.12.09 - 12.15
个人英文阅读练习笔记。原文地址:http://developer.android.com/training/building-location.html

2015.12.09
此节将描述如何添加用户地理位置并且映射信息到应用程序中。通过提供用户所在位置和他们周围的环境信息给用户让应用程序变得更加的有帮助性和有作用。

12.10

1. 让应用程序具有位置感知

描述如何通过获取用户当前位置增加位置感知的特性到应用程序中。

移动应用程序其中一个独特的特性就是位置感知。移动用户会随身携带移动设备,将位置感知添加到应用程序中可给用户提供更多的有联系的环境体验。Google Play服务工具中的可用位置APIs通过自动跟踪位置、地理围栏以及活动识别往应用程序中添加位置感知(百度地图Android SDK亦可)。

在往应用程序中增加位置感知功能时,Google Play 服务位置APIs比Android框架位置APIs(android.location)更优先被选择。如果当前正在使用Android框架位置APIs,那么还是尽可能的去使用Google Play服务位置APIs。

此节展示如何在应用程序中使用Google Play服务位置APIs以获取当前位置、获得定期的位置更新一集查询地址。此节中包含样例程序以及代码片段,它们都可以作为具地理位置感知程序的起点程序。

注:因为此节是基于Google Play服务客户端库,在使用样例和代码片段之前要确保安装最新的版本库。欲学习如何设置最新版本的客户端库,见Google Paly服务 Setup手册。

1.1 获取最新的已知位置

本节描述如何检索Android设备最新的可知位置(通常能够代表用户的当前位置)。

使用Google Play服务位置APIs的应用程序能够请求到用户设备最新的位置。在大多数情况下都会对用户当前位置感兴趣,这个位置一般都等同于设备的最新位置。

尤其,使用fused location provider来检索设备最新位置。fused location provider是Google Play服务的一种位置APIs。它管理底层的位置技术并提供简单的API来让应用程序根据需求指定级别(如高精度或低耗电)。它同时也优化设备的电池使用量。

此节展示如何使用fused location provider中的getLastLocation()方法来请求一次设备位置。

(1) 设置Google Play Services

欲访问fused location provider,应用程序工程必须要包含Google Play Services。通过SDK Manager下载并安装Google Play 服务组件并将库添加到工程之中。更多细节参见 Setting Up Google Play Services

(2) 指定应用程序权限

使用位置服务的应用程序必须请求位置权限。Android提供两个位置权限:ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION。所选择的权限决定API返回的位置精度。如果选择ACCESS_COARSE_LOCATION,API返回一个的地理精度大约等效于一个城市块。

这里申请一个粗略的地理位置。在应用程序的清单文件中用uses-permission元素请求权限,如以下的代码片段所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.gms.location.sample.basiclocationsample" >

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

(3) 连接Google Play Services

欲连接API,需要创建Google Play服务API客户端的实例。关于使用此客户端的更多细节,见Accessing Google APIs手册。

在活动的onCreate()方法中,使用GoogleApiClient.Builder类添加LocationServices API来创建Google API Client实例,如以下代码片段所示:

// Create an instance of GoogleAPIClient.
if (mGoogleApiClient == null) {
    mGoogleApiClient = new GoogleApiClient.Builder(this)
        .addConnectionCallbacks(this)
        .addOnConnectionFailedListener(this)
        .addApi(LocationServices.API)
        .build();
}

欲连接,在活动的onStart()方法中调用connect()方法。欲断开连接,在活动的onStop()方法中调用disconnect()方法。以下代码片段展示如何使用这两个方法。

protected void onStart() {
    mGoogleApiClient.connect();
    super.onStart();
}

protected void onStop() {
    mGoogleApiClient.disconnect();
    super.onStop();
}

(4) 获取最新的位置

一旦连接了Google Play服务和位置服务API,就可以获取用户设备的最新地理位置了。即当应用程序连接到二者后就可以使用fused location provider的getLastLocation()方法来检索设备的位置。此方法返回的地理位置精度取决于在应用程序清单文件中所声明的权限,如本节“指定应用程序权限”中的描述。

欲请求最新的位置,通过传递GoogleApiClient对象给getLastLocation()方法。在Google API Client中提供的onConnected()回调方法中做这个过程,当客户端就绪后onConnected()会被自动调用。以下代码片段演示了请求和对响应的一个简单处理:

public class MainActivity extends ActionBarActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    @Override
    public void onConnected(Bundle connectionHint) {
        mLastLocation = LocationServices.FusedLocationApi.getLastLocation(
                mGoogleApiClient);
        if (mLastLocation != null) {
            mLatitudeText.setText(String.valueOf(mLastLocation.getLatitude()));
            mLongitudeText.setText(String.valueOf(mLastLocation.getLongitude()));
        }
    }
}

getLastLocation()方法返回一个可以检索到地理位置的经纬度的Location对象。当位置不可用的这种较少的情况下,位置对象可能会返回null。

下一节“接收位置更新”将展示如何接收定期更新的位置。

12.14

1.2 接收位置更新

本节描述如何定期请求和接收位置更新。

如果应用程序能够持续的跟踪位置,那么应用程序能够传递更多的相关的信息给用户。例如,如果当用户在步行或者驾驶时应用程序能够帮助用户寻路或者能够帮助用户提供有利的位置信息,那么应用程序需要定期的获取设备的地理位置。除地理位置外(经纬度),可能还需要给用户更多的信息(诸如方位 - 旅行的水平方位,海拔高度或设备的速度)。这些所提及的以及更多的信息都在Location对象中,应用程序可以从fused location provider中检索到此对象。

如在“获取最新可知位置”一节中描述的那样,当用getLastLocation()方法获取设备位置时,还需要进一步根据fused location provider定期更新。作为响应,API会用基于诸如WiFi或GPS(全球定位系统,Global Positioning System)得到的当前可用位置返回给应用程序。位置的精准度取决于提供器、所请求的位置权限以及在位置请求中设置的选项。

此节描述如何使用fused locaiton provider中的requestLocationUpdates()方法来定期更新设备位置。

(1) 连接位置服务

应用程序的位置服务由Google Play服务和fused location provider提供。欲使用这些服务,用Google API连接然后请求位置更新。连接GoogleApiClient和请求当前位置的细节,按照 Getting the Last Known Location中的步骤做。

设备的最新可知位置为从哪里开始。确保应用程序在开始定期位置更新前有一个可知的位置提供了方便。“获取最新可知位置”一节描述了通过调用 getLastLocation()来获取最新可知位置。后续的代码片段假设应用程序已经检索到了最新位置信息并已将其保存在了一个Location全局对象mCurrentLocation中。

使用位置服务的应用程序必须请求位置权限。此节需要良好的位置检测,这样应用程序才能够根据可用的位置提供器尽可能的获取到精确的位置。在应用程序的清单文件中用uses-permission元素来请求此权限,如下例所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.gms.location.sample.locationupdates" >

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

(2) 建立位置请求

欲为请求fused location provider保存参数,创建一个LocationRequest.这个参数确定请求的精度级别。欲见位置请求中的可用参数的细节,见LocationRequest类参考。这里设置更新间隔时间、最快的更新间隔和优先级,如下描述:
更新间隔(Update interval)
setInterval() - 此方法以毫秒的速率设置应用程序接收位置更新。注意,当有另外一个应用程序以更快速率接收位置更新时,那么此应用程序的位置更新频率可能会比此处设置的更新速率快;反之,就可能比此处设置的频率慢;甚至没有更新速率(如设备没有连接)。

最快的更新间隔(Fastest update interval)
setFastestInterval() - 此方法以毫秒的速率设置应用程序能够处理的最快的位置更新速率。需要设置此速率,因为其它的应用程序的更新速率会影响此速率。Google Play服务位置APIs以所有应用程序中用setInterval()设置的最快的速率发送更新。如果此速率比应用程序所能处理的速率更快,那么就有可能会导致用户界面闪烁或数据溢出。欲防止此事,调用setFastestInterval()来设置最高速率限制。

优先权
setPriority() - 此方法设置请求的优先权,这样就会告知Google Play Services位置服务使用哪一个位置资源。它支持一下值:
- PRIORITY_BALANCED_POWER_ACCURACY - 以城市块范围的精度来设置请求,精度大约为100米。在只需粗略定位时使用该选项,该选项耗电可能会更少。使用此设置,位置服务可能使用WiFi和基站定位。然而需要注意,位置提供器的选择还是基于其它诸如哪一个资源可用这样的更多的因素。
- PRIORITY_HIGH_ACCURACY - 使用此设置来最大可能的请求最大准确精度的位置。使用该设置,位置服务会更大可能使用GPS来判断位置。
- PRIORITY_LOW_POWER - 使用该设置来请求城市级别的精确定位,它的精度大约在10千米。此选项适用于粗略精度的定位,它可能会消耗更少的电量。
- PRIORITY_NO_POWER - 当需要几乎不会耗电时但当可以是接收位置更新就使用此设置。使用此设置,应用程序不会触发任何位置更新,但能被其它的应用程序触发接收位置。

下代码片段示例创建位置请求并设置参数:

protected void createLocationRequest() {
    LocationRequest mLocationRequest = new LocationRequest();
    mLocationRequest.setInterval(10000);
    mLocationRequest.setFastestInterval(5000);
    mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
}

PRIRITY_HIGH_ACCURACY优先级结合在应用程序清单文件中定义的ACCESS_FINE_LOCATION权限、5000毫秒更新间隔使用,将会引起fused location provider在几步距离后就会返回位置更新。此设置适用于实时展示位置的应用程序。

性能提示:若应用程序在接收位置更新后会访问网络或做其它长时的工作,那么将快速的间隔调整至一个慢速的值。此调整能够阻止应用程序接收其不能接收的更新。一旦长时的工作完成,就可恢复快速的更新间隔。

(3) 请求位置更新

至此已经设置了为只请求,它包含在应用程序位置更新的请求之中,可以通过调用requestLocationUpdates()开始定期更新。在Google API客户端提供的 onConnected()回调函数中调用requestLocationUpdates(),当客户端就绪时,onConnected()会被自动调用。

基于请求的格式,fused location provider调用回调方法并传递Location对象或传递在PendingIntent的额外数据中包含位置给它。更新的精度和频率由请求的权限和在位置请求对象中所设置的选项决定。

此节描述如何使用LocationListener回调方法来获取更新。调用requestLocationUpdates(),并为其传递 GoogleApiClient、 LocationRequest对象以及 LocationListener。定义方法startLocationUpdates()并在 onConnected()回调方法中调用它,如下所示:

@Override
public void onConnected(Bundle connectionHint) {
    ...
    if (mRequestingLocationUpdates) {
        startLocationUpdates();
    }
}

protected void startLocationUpdates() {
    LocationServices.FusedLocationApi.requestLocationUpdates(
            mGoogleApiClient, mLocationRequest, this);
}

注意以上代码片段中所涉及到的一个布尔标志mRequestingLocationUpdates,它被用来跟踪用户是否开启位置更新。欲获取更多关于根据活动实例的此标志变量的值,见”保存活动的状态“。

(4) 定义位置更新回调函数

fused location provider会调用 LocationListener.onLocationChanged()回调方法。其参数是包含位置经纬度的Location对象。以下代码片段展示如何实现LocationListener接口并定义方法,然后获取位置更新的时间戳并展示纬度、精度以及时间戳到应用程序的用户界面上:

public class MainActivity extends ActionBarActivity implements
        ConnectionCallbacks, OnConnectionFailedListener, LocationListener {
    ...
    @Override
    public void onLocationChanged(Location location) {
        mCurrentLocation = location;
        mLastUpdateTime = DateFormat.getTimeInstance().format(new Date());
        updateUI();
    }

    private void updateUI() {
        mLatitudeTextView.setText(String.valueOf(mCurrentLocation.getLatitude()));
        mLongitudeTextView.setText(String.valueOf(mCurrentLocation.getLongitude()));
        mLastUpdateTimeTextView.setText(mLastUpdateTime);
    }
}

(5) 停止位置更新

当焦点不再当前活动上时考虑停止位置更新,如当用户切换到另外一个应用程序或本应用程序的另外一个活动时。这能够导致电量的消耗,当应用程序不再需要手机信息或运行在后台时。此节描述在活动的onPause()方法中如何停止更新。

欲停止位置更新,调用removeLocationUpdates(),传递 GoogleApiClient和 LocationListener的实例给此方法,如下面的代码示例:

@Override
protected void onPause() {
    super.onPause();
    stopLocationUpdates();
}

protected void stopLocationUpdates() {
    LocationServices.FusedLocationApi.removeLocationUpdates(
            mGoogleApiClient, this);
}

使用布尔变量mRequestingLocationUpdates来跟踪当前的位置跟新是否开启。在活动的onResume()方法中,检查位置更新是否是活跃的,若非活跃则使其活跃:

@Override
public void onResume() {
    super.onResume();
    if (mGoogleApiClient.isConnected() && !mRequestingLocationUpdates) {
        startLocationUpdates();
    }
}

(6) 保存活动的状态

设备配置的一点改变,如屏幕朝向或语言设置的改变,能够导致当前活动被销毁。因此应用程序必须存储活动恢复所需要的信息。实现保存信息的一种一个方式是通过在Bundle对象实例保存。

以下代码片段样例展示如何使用活动的onSaveInstanceState()回调方法来保存活动实例状态:

public void onSaveInstanceState(Bundle savedInstanceState) {
    savedInstanceState.putBoolean(REQUESTING_LOCATION_UPDATES_KEY,
            mRequestingLocationUpdates);
    savedInstanceState.putParcelable(LOCATION_KEY, mCurrentLocation);
    savedInstanceState.putString(LAST_UPDATED_TIME_STRING_KEY, mLastUpdateTime);
    super.onSaveInstanceState(savedInstanceState);
}

定义updateValuesFromBundle()方法来存储活动前一个实例的值,如果这些值可用。在活动的onCreate()中调用此方法,如下例所示:

@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    updateValuesFromBundle(savedInstanceState);
}

private void updateValuesFromBundle(Bundle savedInstanceState) {
    if (savedInstanceState != null) {
        // Update the value of mRequestingLocationUpdates from the Bundle, and
        // make sure that the Start Updates and Stop Updates buttons are
        // correctly enabled or disabled.
        if (savedInstanceState.keySet().contains(REQUESTING_LOCATION_UPDATES_KEY)) {
            mRequestingLocationUpdates = savedInstanceState.getBoolean(
                    REQUESTING_LOCATION_UPDATES_KEY);
            setButtonsEnabledState();
        }

        // Update the value of mCurrentLocation from the Bundle and update the
        // UI to show the correct latitude and longitude.
        if (savedInstanceState.keySet().contains(LOCATION_KEY)) {
            // Since LOCATION_KEY was found in the Bundle, we can be sure that
            // mCurrentLocationis not null.
            mCurrentLocation = savedInstanceState.getParcelable(LOCATION_KEY);
        }

        // Update the value of mLastUpdateTime from the Bundle and update the UI.
        if (savedInstanceState.keySet().contains(LAST_UPDATED_TIME_STRING_KEY)) {
            mLastUpdateTime = savedInstanceState.getString(
                    LAST_UPDATED_TIME_STRING_KEY);
        }
        updateUI();
    }
}

更多关于保存实例状态,见 Android Activity 类参考。

注:关于更持久的存储,可以将用户的偏好设置在应用程序中的 SharedPreferences中。将共享的偏好设置在活动的onPause()方法中,在onResume()中检索这些偏好。更多关于保存偏好的信息见 Saving Key-Value Sets

下一节“展示位置地址”描述如何展示一个给定位置的街道地址。

1.3 展示位置地址

本节描述如何将位置的纬度和经度转换为地址(反向地理编码)。

“获取最新可知位置”和“接收位置更新”两节描述了用包含经纬度坐标的Location对象的形式来获取用户位置。尽管经纬度对计算距离和展示地图位置有用,但在许多情况下,位置的地址会更加的有用。例如,如果欲告知用户的位置或他们在谁的附近,一个位置的街道地址恐比地理坐标(纬度或精度)会更加地有意义。

通过安卓框架位置APIs中的Geocoder类,可将地址转换为地理坐标。此过程称为地理编码。反过来,亦可以将地理位置转换为一个地址。这个过程称为反地理编码。

此节描述如何使用getFromLocation()方法将地理位置转换为一个地址。此方法根据给定的经纬度返回一个估计的街道地址。

(1) 获取地理位置

设备最新的可知位置是地址查询重要的开始点。“获取最新可知位置”一节描述了如何使用 fused location provider中的getLastLocation()方法来找到设备的最新地址。

欲访问fused location provider,需要创建Google Play 服务API客户端实例。欲知如何连接到客户端,见“连接Google Play Services”。

欲让fused location provider检索到精准的街道地址,在应用程序的清单文件中设置位置权限ACCESS_FINE_LOCATION,如以下代码所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.gms.location.sample.locationupdates" >

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

(2) 定义意图服务(Intent Service)来提取地址

由Geocoder类提供的getFromLocation()方法接收纬度和精度,返回一串地址信息。此方法是同步的,它会花较长的时间来完成这项工作,所以不应该在应用程序的主线程即用户界面线程中调用此方法。

IntentService类提供了在后台线程运行任务的结构。使用此类,可以操作一个长时运行的操作且不会影响用户界面的响应。注意AsyncTask类也允许在后台执行操作,但此类是为短时操作所设计的。如果活动重建时,AsyncTask不应该和用户界面保持关联,如设备屏幕旋转时。反之,当活动重建时,IntentService不必被取消。

定义一个扩展于IntentService的FetchAddressIntentService 类。此类是地址查询服务。意图服务在一个工作线程中异步的操控一个意图,当完成其工作后,它自身将会停止。意图额外提供服务所需的数据,包括将要被转换为地址的Location对象以及用来操纵地址查询结果的ResultReceiver对象。此服务使用Geocoder来提取位置地址,并将结果发往ResultReceiver。

[1] 在应用程序清单文件中定义意图服务

在应用程序的清单文件中增加条目来定义意图服务:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.gms.location.sample.locationaddress" >
    <application
        ...
        <service
            android:name=".FetchAddressIntentService"
            android:exported="false"/>
    </application>
    ...
</manifest>

注:清单文件中的service元素不需要包含意图过滤器,因为主活动会根据为意图指定的类名对应的类去创建准确的意图。

[2] 创建Geocoder

将地理位置转换为地址的过程称为地理编码。欲执行意图服务的主要工作,即反地理编码,在FetchAddressIntentService 类中实现onHandleIntent()。创建Geocoder来操作反地理编码。

本地区域代表一个指定的地理或语言区域。本地对象被用来调整信息的呈现,诸如数量或日期,以去适合由本地代表的方便的区域。传递Locale对象给Geocoder对象以确保结果地址对于用户地理区域来说是局部的。

@Override
protected void onHandleIntent(Intent intent) {
    Geocoder geocoder = new Geocoder(this, Locale.getDefault());
    ...
}
[3] 检索地道介质数据

下一步该根据地理编码检索街道地址了,需要操控每个可能发生的错误并将结果发送回请求该地址的活动中。欲报告地理编码处理的结果,需要两个表明成功或失败的数字常量。定义一个Constants类来包含这些值,如以下代码片段实例:

public final class Constants {
    public static final int SUCCESS_RESULT = 0;
    public static final int FAILURE_RESULT = 1;
    public static final String PACKAGE_NAME =
        "com.google.android.gms.location.sample.locationaddress";
    public static final String RECEIVER = PACKAGE_NAME + ".RECEIVER";
    public static final String RESULT_DATA_KEY = PACKAGE_NAME +
        ".RESULT_DATA_KEY";
    public static final String LOCATION_DATA_EXTRA = PACKAGE_NAME +
        ".LOCATION_DATA_EXTRA";
}

欲获得地理位置对应的街道地址,调用getFromLocation(),并给此方法传递位置对象的纬度和经度以及所需要返回地址的最大数量。在这种情况下,只需要一个地址。地理编码返回地址序列。如果没有地址匹配给定的位置,它将返回一个空序列。如果没有地理编码服务可用,地理编码将返回null。

在代码中检查以下所列举的错误情况。如果有一个错误发生,将相应的错误信息防止在errorMessage变量中,这样就可以将错误返回给请求检索的活动中了:
- 没有提供的位置数据(No location data provided) - 意图额外数据中没有包含反地理编码所需的Location对象。
- 使用的无效的纬度或精度(Invalid latitude or longitude used) - 在Location对象中提供过得纬度和/或精度值无效。
- 没有可用的地理编码(No geocoder available) - 由于网络错误或IO异常,后台地理编码服务不可用。
- 对不起,找不到地址(Sorry, no address found) - 根据给定过得纬度/精度没有找到相应的地址。

欲将结果发送到发生请求的活动中,调用deliverResultToReceiver()方法(定义在“返回地址给请求者中”)。结果又之前的数字常量(表明成功/失败)和一个字符串。在成功反向编码的情形下,字符串包含地址信息。在失败的情况下,字符串包含的错误信息,以上描述的代码示例如下:

@Override
protected void onHandleIntent(Intent intent) {
    String errorMessage = "";

    // Get the location passed to this service through an extra.
    Location location = intent.getParcelableExtra(
            Constants.LOCATION_DATA_EXTRA);

    ...

    List<Address> addresses = null;

    try {
        addresses = geocoder.getFromLocation(
                location.getLatitude(),
                location.getLongitude(),
                // In this sample, get just a single address.
                1);
    } catch (IOException ioException) {
        // Catch network or other I/O problems.
        errorMessage = getString(R.string.service_not_available);
        Log.e(TAG, errorMessage, ioException);
    } catch (IllegalArgumentException illegalArgumentException) {
        // Catch invalid latitude or longitude values.
        errorMessage = getString(R.string.invalid_lat_long_used);
        Log.e(TAG, errorMessage + ". " +
                "Latitude = " + location.getLatitude() +
                ", Longitude = " +
                location.getLongitude(), illegalArgumentException);
    }

    // Handle case where no address was found.
    if (addresses == null || addresses.size()  == 0) {
        if (errorMessage.isEmpty()) {
            errorMessage = getString(R.string.no_address_found);
            Log.e(TAG, errorMessage);
        }
        deliverResultToReceiver(Constants.FAILURE_RESULT, errorMessage);
    } else {
        Address address = addresses.get(0);
        ArrayList<String> addressFragments = new ArrayList<String>();

        // Fetch the address lines using getAddressLine,
        // join them, and send them to the thread.
        for(int i = 0; i < address.getMaxAddressLineIndex(); i++) {
            addressFragments.add(address.getAddressLine(i));
        }
        Log.i(TAG, getString(R.string.address_found));
        deliverResultToReceiver(Constants.SUCCESS_RESULT,
                TextUtils.join(System.getProperty("line.separator"),
                        addressFragments));
    }
}
[4] 返回地址给请求者

意图服务最后需要做的是在开启服务的活动中将地址返回给ResultReceiver。ResultReceiver类允许发送结果码和包含结果数据的信息。数字码用于报告地理编码请求成功或失败。地理编码请求成功时,字符串包含的地址信息。地理编码请求失败时,字符串包含的失败的原因的信息。

至此已经根据地理编码检索了地址,捕获了任何可能发生的错误并调用了deliverResultToReceiver()方法。现在还需要定义deliverResultToReceiver()方法来发送结果码和信息到结果接收器中。

对于结果码,使用传递给deliverResultToReceiver()方法中的resultCode参数中的值。欲构建信息束,在Constants类(定义在“检索街道地址数据”一节中)中连接RESULT_DATA_KEY常量和message参数中的值,将结合值传递给deliverResultToReceiver()方法,如下例所示:

public class FetchAddressIntentService extends IntentService {
    protected ResultReceiver mReceiver;
    ...
    private void deliverResultToReceiver(int resultCode, String message) {
        Bundle bundle = new Bundle();
        bundle.putString(Constants.RESULT_DATA_KEY, message);
        mReceiver.send(resultCode, bundle);
    }
}

(3) 开启意图服务

在上一节中定义的意图服务,运行于后台中且用于响应提取给定地理位置相应的地址。当开启此服务时,如果它还未运行,那么安卓框架将会实例化并开启此服务,如果有必要还会另创一个进程。如果此服务已经在运行,那么会保持它的运行。因为该服务扩展于IntentService,当所有的意图都被处理后它会被自动关闭。

在应用程序的主活动中开启该服务,并创建一个意图来传递数据给服务。这里需要一个显示的意图,因为这里只想让该服务来响应此意图。更多信息见 Intent Types

欲创建显示意图,指定该服务要使用类的类名:FetchAddressIntentService.class。为意图额外信息传递两点信息:
- 一个操控地址查询结果的ResultReceiver。
- 包含欲转换为地址的纬度精度的Location对象。

以下代码片段展示如何开启意图服务:

public class MainActivity extends ActionBarActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {

    protected Location mLastLocation;
    private AddressResultReceiver mResultReceiver;
    ...

    protected void startIntentService() {
        Intent intent = new Intent(this, FetchAddressIntentService.class);
        intent.putExtra(Constants.RECEIVER, mResultReceiver);
        intent.putExtra(Constants.LOCATION_DATA_EXTRA, mLastLocation);
        startService(intent);
    }
}

当用户采取了请求地理地址查询动作时调用以上的startIntentService()方法。例如,用户在应用程序界面按下“提取地址”按钮。在开启意图服务之前,需要检查与Google Play服务的连接是否正常。以下代码片段展示了以按钮来操控startIntentService()方法的调用:

public void fetchAddressButtonHandler(View view) {
    // Only start the service to fetch the address if GoogleApiClient is
    // connected.
    if (mGoogleApiClient.isConnected() && mLastLocation != null) {
        startIntentService();
    }
    // If GoogleApiClient isn't connected, process the user's request by
    // setting mAddressRequested to true. Later, when GoogleApiClient connects,
    // launch the service to fetch the address. As far as the user is
    // concerned, pressing the Fetch Address button
    // immediately kicks off the process of getting the address.
    mAddressRequested = true;
    updateUIWidgets();
}

如果用户已经在应用程序主界面点击了该按钮,当与Google Play服务的连接建立时开启意图服务。以下代码片段展示了在Google API客户端中的回调函数onConnected()回调方法中调用startIntentService()方法:

public class MainActivity extends ActionBarActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    @Override
    public void onConnected(Bundle connectionHint) {
        // Gets the best and most recent location currently available,
        // which may be null in rare cases when a location is not available.
        mLastLocation = LocationServices.FusedLocationApi.getLastLocation(
                mGoogleApiClient);

        if (mLastLocation != null) {
            // Determine whether a Geocoder is available.
            if (!Geocoder.isPresent()) {
                Toast.makeText(this, R.string.no_geocoder_available,
                        Toast.LENGTH_LONG).show();
                return;
            }

            if (mAddressRequested) {
                startIntentService();
            }
        }
    }
}

(4) 接收地理编码结果

意图服务已经操控了地理编码请求,并使用ResultReceiver将结果返回给做出请求的活动。在做出请求的活动中,定义一个扩展于ResultReceiver的AddressResultReceiver类来操控来自FetchAddressIntentService的响应。

结果同时包含了数字码(resultCode)和结果数据信息(resultData)。如果反向地理编码处理成功,resultData包含地址。如果失败,resultData包含失败的原因。更多关于可能错误的描述见“返回地址给请求者”一节。

重写onReceiveResult()方法来操控被发送给结果接收器的结果,如以下代码所示:

public class MainActivity extends ActionBarActivity implements
        ConnectionCallbacks, OnConnectionFailedListener {
    ...
    class AddressResultReceiver extends ResultReceiver {
        public AddressResultReceiver(Handler handler) {
            super(handler);
        }

        @Override
        protected void onReceiveResult(int resultCode, Bundle resultData) {

            // Display the address string
            // or an error message sent from the intent service.
            mAddressOutput = resultData.getString(Constants.RESULT_DATA_KEY);
            displayAddressOutput();

            // Show a toast message if an address was found.
            if (resultCode == Constants.SUCCESS_RESULT) {
                showToast(getString(R.string.address_found));
            }

        }
    }
}

12.15

1.4 创建和监控地理围栏(Geofences)

本节描述如何定义一个或多个感兴趣的地理区域(地理围栏,geofences),并检测用户什么时候接近或进入地理围栏区域。

地理围栏结合用户当前位置和用户感兴趣的临近位置的意识。欲标记一个感兴趣的位置,需要指定它的纬度和精度。欲调整到临近的位置,需要增加一个半径范围。纬度、精度以及半径在感兴趣的位置周围定义一个地理围栏,创建一个圆圈区域或围栏。

可以拥有多个活跃的地理围栏,每个设备用户最多可以拥有100个。对于每个地理围栏,可以通过请求位置服务来发送入口和出口事件,或在触发事件前可以在地理围栏区域中指定一段等待或停留时间。可以通过指定以毫秒为级别的过期时间来限制任何一个地理围栏的时间。在地理围栏过期后,位置服务会自动将其移除。

这里写图片描述

此节将描述如何增加和移除地理围栏,以及如何使用IntentService来监听地理围栏转变。

推荐升级已存在应用程序使用LocationServices类,此类包含GeofencingApi接口。 LocationServices类将替换 LocationClient类(过时了)。

(1) 设置地理围栏监控

请求地理围栏监控的第一步是请求必要的权限。欲使用地理围栏,必须在应用程序中请求ACCESS_FINE_LOCATION权限。欲请求此权限,将以下元素作为应用程序清单文件中manifest元素的子元素:

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

如果欲使用IntentService来监听地理围栏转变,需增加元素来指定服务名。此元素必须是application元素的子元素:

<application
   android:allowBackup="true">
   ...
   <service android:name=".GeofenceTransitionsIntentService"/>
<application/>

欲访问位置APIs,需要创建Google Play服务API客户端实例。欲知如何连接客户端,见“连接Google Play Services”。

(2) 创建并增加地理围栏

应用程序需要使用创建地理围栏对象的位置API来创建地理围栏,并使用便利的类来添加它们。同时,当地理围栏转变发生需操控由位置服务发送来的意图,这可以通过定义一个PendingIntent来实现(见后续内容)。

注:在单用户设备上,每个设备上的应用程序最多能有100个地理围栏。对于多用户设备,每个设备用户的应用程序都是100个地理围栏限制。

[1] 创建地理围栏对象

首先,使用Geofence.Builder来创建地理围栏,设置地理围栏的所需的半径、持续时间以及转变类型。如下例(填充名为mGeofenceList的对象):

mGeofenceList.add(new Geofence.Builder()
    // Set the request ID of the geofence. This is a string to identify this
    // geofence.
    .setRequestId(entry.getKey())

    .setCircularRegion(
            entry.getValue().latitude,
            entry.getValue().longitude,
            Constants.GEOFENCE_RADIUS_IN_METERS
    )
    .setExpirationDuration(Constants.GEOFENCE_EXPIRATION_IN_MILLISECONDS)
    .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER |
            Geofence.GEOFENCE_TRANSITION_EXIT)
    .build());

此例从一个常量文件中取数据。在实际操作中,应用程序可能会基于用户位置动态的创建地理围栏。

[2] 指定地理围栏和初始触发

以下代码片段使用GeofencingRequest类和其嵌套类GeofencingRequestBuilder来指定地理围栏监控和设置相应的地理围栏的触发事件:

private GeofencingRequest getGeofencingRequest() {
    GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
    builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
    builder.addGeofences(mGeofenceList);
    return builder.build();
}

此例描述了两个地理围栏触发。当设备进入一个地理围栏时GEOFENCE_TRANSITION_ENTER转变触发,当设备退出一个地理围栏时GEOFENCE_TRANSITION_EXIT转变被触发。指定INITIAL_TRIGGER_ENTER用来告诉位置服务若设备已经进入地理围栏则GEOFENCE_TRANSITION_ENTER应该被触发。

在许多情况下,可能会更优先使用INITIAL_TRIGGER_DWELL,当用户停止定义地理围栏内的持续时间时会触发事件。此种方法会减少当设备简单的进入和退出地理围栏时所导致的大量的“警告的垃圾邮件”。另外一种获取地理围栏最佳结果的策略时设置最小半径值100m。这会帮助在Wi-Fi网络下的位置精度,同时减少设备电量消耗。

[3] 定义地理围栏转变的意图

由位置服务发送的意图可以触发应用程序中的多种动作,但不应该让此去启动活动或碎片,因为组件只有在响应用户动作时才变得可见。在许多情况下,IntentService是操控意图的好选择。IntentService能够张贴通知、执行长期的后台工作、设置意图到其它的服务获发送广播意图。以下代码片段展示如何定义PendingIntent来启动IntentService:

public class MainActivity extends FragmentActivity {
    ...
    private PendingIntent getGeofencePendingIntent() {
        // Reuse the PendingIntent if we already have it.
        if (mGeofencePendingIntent != null) {
            return mGeofencePendingIntent;
        }
        Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);
        // We use FLAG_UPDATE_CURRENT so that we get the same pending intent back when
        // calling addGeofences() and removeGeofences().
        return PendingIntent.getService(this, 0, intent, PendingIntent.
                FLAG_UPDATE_CURRENT);
    }
[4] 增加地理围栏

欲增加地理围栏,使用GeoencingApi.addGeofences()方法。GeofencingRequest对象和PendingIntent提供Google API客户端。以下代码片段在onResult()中处理了结果,假设主活动实现了ResultCallback:

public class MainActivity extends FragmentActivity {
    ...
    LocationServices.GeofencingApi.addGeofences(
                mGoogleApiClient,
                getGeofencingRequest(),
                getGeofencePendingIntent()
        ).setResultCallback(this);

(3) 处理地理围栏转变

当位置服务检车到用户进入或退出地理围栏时,它会发送包含在请求添加地理围栏中的PendingIntent中的意图。此意图会被像GeofenceTransitionsIntentService这样的服务接收,此服务从意图中获取地理围栏事件、判断地理围栏转变的类型并判断哪一种地理围栏被触发。然后它将发送一个通知作为输出。

以下代码片段演示如何定义一个当地理围栏转变发生时张贴通知的IntentService。当用户点击此通知时,应用程序的主活动出现:

public class GeofenceTransitionsIntentService extends IntentService {
   ...
    protected void onHandleIntent(Intent intent) {
        GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);
        if (geofencingEvent.hasError()) {
            String errorMessage = GeofenceErrorMessages.getErrorString(this,
                    geofencingEvent.getErrorCode());
            Log.e(TAG, errorMessage);
            return;
        }

        // Get the transition type.
        int geofenceTransition = geofencingEvent.getGeofenceTransition();

        // Test that the reported transition was of interest.
        if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER ||
                geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) {

            // Get the geofences that were triggered. A single event can trigger
            // multiple geofences.
            List triggeringGeofences = geofencingEvent.getTriggeringGeofences();

            // Get the transition details as a String.
            String geofenceTransitionDetails = getGeofenceTransitionDetails(
                    this,
                    geofenceTransition,
                    triggeringGeofences
            );

            // Send notification and log the transition details.
            sendNotification(geofenceTransitionDetails);
            Log.i(TAG, geofenceTransitionDetails);
        } else {
            // Log the error.
            Log.e(TAG, getString(R.string.geofence_transition_invalid_type,
                    geofenceTransition));
        }
    }

通过PendingIntent检测到转变事件后,此IntentService获取地理围栏转换类型并检测此转变是否为应用程序中用来触发通知的事件 – 是否为GEOFENCE_TRANSITION_ENTER或GEOFENCE_TRANSITION_EXIT情况。此服务将发送一个通知并记录转变的细节。

(4) 停止地理围栏监控

在不再需要或渴求节约设备电量和CPU运行周期时停止地理围栏监控。可以在被用来增加和移除地理围栏的主活动中停止地理围栏监控;移除地理围栏并立即将其停止。API提供了方法来移除地理围栏,可以通过请求的IDs或通过给定的PendingIntent。

以下代码片段通过PendingIntent来移除地理围栏,当设备进入或退出之前添加的地理围栏时停止所有进一步的通知:

LocationServices.GeofencingApi.removeGeofences(
            mGoogleApiClient,
            // This is the same pending intent that was used in addGeofences().
            getGeofencePendingIntent()
    ).setResultCallback(this); // Result processed in onResult().
}

可以将地理围栏和其它的位置感知结合起来,诸如定期位置更新。欲见更多信息,见本文中的其它节。

(5) 使用实现地理围栏的最佳步骤

此节描述用Android位置APIs来使用地理围栏的框架/概要推荐。

[1] 减少电量消耗

在使用地理围栏的应用程序中可以使用以下技术来优化电量的使用:
- 设置通知响应为一个较高的值。通过增加地理围栏警报的延迟来降低电量消耗。如在应用程序中设置响应值为5分钟,那么只在每个5分钟才会检查进入或退出地理围栏。设置低的值也不绝对的意味着在时间过期时就能够通知用户(如设置5s,那么它将会话更长的时间才会接受到通知)。
- 在用户使用更大量时间时加大地理围栏半径,如用户在家或步行时。大的半径不会直接导致电量消耗变少,但它会减少应用程序检查进入或退出的频率,有效的降低了整体的电量消耗。

[2] 为地理围栏选择最优的半径

地理围栏的最小半径应该设置在100到150m之间为最佳。当Wi-Fi可用时,位置精度通常在20 - 50m。当室内位置可用时,精度范围能够小到5m。假设Wi-Fi位置精度约为50m,除非您知道地理围栏中的室内位置可用。

当Wi-Fi位置不可用(如驾驶在农村区域),那么位置精度将会降低。精度范围可大致几百米到几千米。在这种情况下,应该用大半径来创建地理围栏。

[3] 使用驻留转变类型来减少警报垃圾邮件

如果当驾驶时短暂的经过地理围栏时会受到大量的警报,减少警报的最好方式是使用GEOFENCE_TRANSITION_DWELL类型代替GEOFENCE_TRANSITION_ENTER类型。如此,只有当用户停留在地理围栏内给定的时间后驻留警报才会发出。可以通过设置 loitering delay来设置这个时间段。

[4] 只在需要时重新注册地理围栏

重新注册地理围栏被保存在com.google.android.gms包中的com.google.process.location进程中。在遇到以下事件时,应用程序不需要做任何处理,因为在这些事件后,系统将会存储地理微栏。
- Google Play服务升级。
- 由于资源限制系统杀死并重启了Google Play服务。
- 位置进程崩溃。

在遇到以下事件后若应用程序仍旧需要地理围栏则必须重新注册它,因为在以下情况中系统不能恢复地理围栏:
- 设备重启。应用程序应该监听设备的引导(boot)完成动作,然后重新注册所需的地理围栏。
- 应用程序被卸载后再被重新安装。
- 应用程序的数据被清除。
- Google Play服务数据被清除。
- 应用程序接收到了GEOFENCE_NOT_AVAILABLE警报。在NLP(安卓网络位置提供器,Android’s Network Location Provider)被关闭时常会发生。

(6) 解决地理围栏入口实践

如果当设备进入地理围栏中时地理围栏并没有被触发(GEOFENCE_TRANSITION_ENTER警报没有被触发),首先确保地理围栏是否按照本手册步骤被正确注册。

以下是一些警报没有按照期望被触发的原因:
- 在地理围栏中的精确位置不可用或地理围栏太小。在大多数设备上,地理围栏服务使用网络位置来供地理围栏触发。服务使用此种方法是因为网络位置消耗更少的电量,它花更少的时间获取独立的位置以及最重要的是它可使用的室内(indoors)。从Google Play服务3.2开始,地理围栏服务计算位置圆圈的重叠比率和地理围栏圆圈并在较大地理围栏情况下至少到达到85%的比率才产生进入警报(对于较小的地理围栏则比率达到75%)。对于退出警报,比率阀值在15%或25%。任何在这些阀值之间的比率都会让地理围栏服务标记地理围栏状态为INSIDE_LOW_CONFIDENCE或OUTSIDE_LOW_CONFIDENCE且无警报发送。
- 设备上的Wi-Fi被关闭。在有WiFi的情况下可以较大意义地提高位置精度,所以如果WiFi关闭,基于包括地理围栏的半径、设备模型或Android版本这些设置,应用程序可能永远都不会获得地理围栏警报。从Android 4.3(API level 18)开始,增加了“WiFi scan only mode”的性能,即允许在用户关闭WiFi的情况下还能够获得良好的网络位置。提示用户并提供一个快捷方式给用户来使能WiFi或Wi-Fi scan onlu mode是一个不错的实践方式(如果二者都被关闭)。使用SettingsApi来确保设备系统设置对于优化位置检测来说是合适的配置。
- 在地理围栏内无可用的网络连接。如果无可靠的数据连接,警报就可能不会产生。这是因为地理围栏服务基于需要数据连接的网络位置提供器。
- 警报延迟。地理围栏服务没有持续查询位置,所以对于接收警报来说会有些延迟。通常延迟都会少于2分钟,当设备在移动时延迟时间会更少。如果设备被较长时间地停留在某个地方,那么延迟可能会增加(增至6分钟)。

2. 增加地图

描述如何增加地图和映射信息到应用程序中。

用由谷歌提供的丰富的地图可以允许用户去探索世界。可用自定义的标记确认位置、用图像覆盖物增加地图数据、以碎片嵌入一个或更多地图以及更多。

Google Maps Android API允许在应用程序中包含地图和自定义映射信息。

这里写图片描述

(1) 主要开发特性(功能)

[1] 在应用程序中添加地图

通过Google Maps Android API v2,可通过简单的XML代码片段将地图作为碎片嵌入到活动中。新的地图提供诸如3D地图、室内、卫星、地形、混合地图、可高效缓存和绘制的基于矢量的瓦片层、动画转变以及更多等有趣的特性。增加地图对象(Add a map object)。

[2] 自定义地图

增加标记到地图上来表明用户所感兴趣的特定的点。可以自定义标记的颜色和图标以匹配应用程序的外表和感觉。欲进一步增强应用程序,可以绘制折线和多边形来表明路径或区域,或提供完整的图像层。绘制标记(Draw markers)。

[3] 控制用户的视图

通过控制旋转来给用户不同的世界视图,地图的倾斜、变焦、平面特性等这些以”相机“视角的特性。改变视图(Change the view

[4] 在应用程序中增加街道视图

嵌入街道视图到活动中并让用户通过360°的全貌视图探索世界。以编程方式控制街道视图的变焦和方向(瓦片和方位), 并在给定时间内动画移动。增加街道视图(Add Street View)。

(2) 开始(Get Started)

Google Maps Android API是Google Play服务平台的一部分。欲使用Google Maps,在应用程序工程开发工程中设置Google Play服务SDK。更多信息见Google Maps Android API的开始(Getting Started)手册。

[2015.12.09]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值