首页 > 技术文章 > 【Android Developers Training】 104. 接受地点更新

jdneo 2014-05-14 09:42 原文

注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。

原文链接:http://developer.android.com/training/location/receive-location-updates.html


如果你的应用有导航的功能,你可能会希望可以定期获取用户的地理位置。虽然你可以通过LocationClient.getLastLocation()做到这一点,但是一个更加直接的方法是向定位服务申请定期更新。作为响应,定位服务会自动用最佳的地理位置信息(基于当前激活的可以提供位置信息的传感器,如WiFi或者GPS)更新到你的应用。

要从定位服务定期获取地理位置更新,你使用定位客户端发送一个请求。根据请求的形式,定位服务或是激活一个回调函数,并把一个Location对象传递给该函数,或是发送一个Intent,在其数据部分包含了地理位置信息。有两方面因素会影响精度和频率,一个是你的应用申请的定位权限,一个是你在请求中传递给定位服务的参数。


一). 指定应用权限

使用位置服务的应用必须请求定位权限。Android有两个定位权限:ACCESS_COARSE_LOCATION(粗定位)和ACCESS_FINE_LOCATION(精定位)。你所选择的权限决定了定位的精度。如果你只请求粗定位,位置服务所范围的地点信息大致会精确到一个城市街区。

如果请求ACCESS_FINE_LOCATION,它也暗含了ACCESS_COARSE_LOCATION的权限。

例如,要添加ACCESS_COARSE_LOCATION,将下面的代码作为<manifest>元素的子元素:

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

二). 检查Google Play服务

位置服务是Google Play服务APK的其中一部分。由于用户设备的状态时难以预料的,你应该一直在你尝试连接定位服务之前,检查APK是否已经安装。要检查APK是否安装,可以调用GooglePlayServicesUtil.isGooglePlayServicesAvailable(),它会返回一个整形的结果码,其含义可以参阅:ConnectionResult。如果你遇到了一个错误,可以调用GooglePlayServicesUtil.getErrorDialog(),来获取一个本地的对话框,引导用户执行正确地行为,之后将这一对话框显示在一个DialogFragment上。这一对话框可能允许用户解决当前的问题,此时Google Play服务会发回一个结果到你的activity中。要处理这一结果,需要覆写onActivityResult()方法。

Note:

要使你的应用可以兼容1.6及以后版本的系统,显示DialogFragment的activity必须是FragmentActivity的子类,而非Activity。使用FragmentActivity还可以允许你调用getSupportFragmentManager()方法来显示DialogFragment

由于你一直需要在你的代码多个地方检查Google Play服务,所以应该定义一个方法将检查行为进行封装,之后在每次连接尝试之前进行检查。下面的代码片段包含了检查Google Play服务所需要的代码:

public class MainActivity extends FragmentActivity {
    ...
    // Global constants
    /*
     * Define a request code to send to Google Play services
     * This code is returned in Activity.onActivityResult
     */
    private final static int
            CONNECTION_FAILURE_RESOLUTION_REQUEST = 9000;
    ...
    // Define a DialogFragment that displays the error dialog
    public static class ErrorDialogFragment extends DialogFragment {
        // Global field to contain the error dialog
        private Dialog mDialog;
        // Default constructor. Sets the dialog field to null
        public ErrorDialogFragment() {
            super();
            mDialog = null;
        }
        // Set the dialog to display
        public void setDialog(Dialog dialog) {
            mDialog = dialog;
        }
        // Return a Dialog to the DialogFragment.
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            return mDialog;
        }
    }
    ...
    /*
     * Handle results returned to the FragmentActivity
     * by Google Play services
     */
    @Override
    protected void onActivityResult(
            int requestCode, int resultCode, Intent data) {
        // Decide what to do based on the original request code
        switch (requestCode) {
            ...
            case CONNECTION_FAILURE_RESOLUTION_REQUEST :
            /*
             * If the result code is Activity.RESULT_OK, try
             * to connect again
             */
                switch (resultCode) {
                    case Activity.RESULT_OK :
                    /*
                     * Try the request again
                     */
                    ...
                    break;
                }
            ...
        }
        ...
    }
    ...
    private boolean servicesConnected() {
        // Check that Google Play services is available
        int resultCode =
                GooglePlayServicesUtil.
                        isGooglePlayServicesAvailable(this);
        // If Google Play services is available
        if (ConnectionResult.SUCCESS == resultCode) {
            // In debug mode, log the status
            Log.d("Location Updates",
                    "Google Play services is available.");
            // Continue
            return true;
        // Google Play services was not available for some reason
        } else {
            // Get the error code
            int errorCode = connectionResult.getErrorCode();
            // Get the error dialog from Google Play services
            Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(
                    errorCode,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
            // If Google Play services can provide an error dialog
            if (errorDialog != null) {
                // Create a new DialogFragment for the error dialog
                ErrorDialogFragment errorFragment =
                        new ErrorDialogFragment();
                // Set the dialog in the DialogFragment
                errorFragment.setDialog(errorDialog);
                // Show the error dialog in the DialogFragment
                errorFragment.show(
                        getSupportFragmentManager(),
                        "Location Updates");
            }
        }
    }
    ...
}

在后续章节的代码片段中,都会调用这一方法来验证是否可获取Google Play服务。


三). 定义位置服务回调函数

在你创建定位客户端之前,实现定位服务的接口,以和你的应用进行交互:

ConnectionCallbacks

指定当定位连接上或者没有连接上时,定位服务调用的方法。

OnConnectionFailedListener

指定当尝试连接到定位客户端时,如果出现了错误,定位服务调用的方法。这一方法使用之前定义的showErrorDialog方法来显示一个错误对话框,它尝试使用Google Play服务来解决这一问题。

下面的样例代码展示了如何指定接口和定义相关的函数:

public class MainActivity extends FragmentActivity implements
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener {
    ...
    /*
     * Called by Location Services when the request to connect the
     * client finishes successfully. At this point, you can
     * request the current location or start periodic updates
     */
    @Override
    public void onConnected(Bundle dataBundle) {
        // Display the connection status
        Toast.makeText(this, "Connected", Toast.LENGTH_SHORT).show();
    }
    ...
    /*
     * Called by Location Services if the connection to the
     * location client drops because of an error.
     */
    @Override
    public void onDisconnected() {
        // Display the connection status
        Toast.makeText(this, "Disconnected. Please re-connect.",
                Toast.LENGTH_SHORT).show();
    }
    ...
    /*
     * Called by Location Services if the attempt to
     * Location Services fails.
     */
    @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
         * error.
         */
        if (connectionResult.hasResolution()) {
            try {
                // Start an Activity that tries to resolve the error
                connectionResult.startResolutionForResult(
                        this,
                        CONNECTION_FAILURE_RESOLUTION_REQUEST);
                /*
                * Thrown if Google Play services canceled the original
                * PendingIntent
                */
            } catch (IntentSender.SendIntentException e) {
                // Log the error
                e.printStackTrace();
            }
        } else {
            /*
             * If no resolution is available, display a dialog to the
             * user with the error.
             */
            showErrorDialog(connectionResult.getErrorCode());
        }
    }
    ...
}

定义地理位置更新回调函数

定位服务或是以一个Intent的形式,或者以一个参数的形式将为之更新传递给一个你定义的回调函数。这节课将会讲解如何使用一个回调函数来获取更新,课程中使用的代码基本可以用于任何应用场景。如果你想要以一个Intent的形式接收位置更新,可以阅读:Recognizing the User's Current Activity。它提供了类似的可以参考的模板。

位置服务所调用的将为之更新发送给你的应用的回调函数是在LocationListener接口的onLocationChanged()方法中指定的。传入的参数是一个Location对象,包含了地点的经纬度。下面的代码片段展示了如何指定接口和定义方法:

public class MainActivity extends FragmentActivity implements
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationListener {
    ...
    // Define the callback method that receives location updates
    @Override
    public void onLocationChanged(Location location) {
        // Report to the UI that the location was updated
        String msg = "Updated Location: " +
                Double.toString(location.getLatitude()) + "," +
                Double.toString(location.getLongitude());
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
    }
    ...
}

现在你已经有了回调函数,你可以配置位置更新的请求了。首先第一步是指定控制更新的参数。


四). 指定更新参数

定位服务允许你控制更新之间的时间间隔以及你期望的位置精确度,通过设置LocationRequest对象中的值,再将这一对象作为你的请求的一部分发出以开始更新。

首先,设置下列间隔参数:

更新间隔:

通过LocationRequest.setInterval()设置。这一方法以毫秒为单位设置你的应用接收更新的事件间隔。如果没有其它应用从定位服务接收更新,那么你的应用将会以这一频率接收更新。

最快更新间隔:

通过LocationRequest.setFastestInterval()设置。这一方法设置的是你的应用能处理更新的最快间隔时间,以毫秒为单位。你需要设置这个频率是因为其它应用也会影响位置更新非频率。定位服务会以所有应用通过LocationRequest.setInterval()设置的最快的间隔时间来发送更新。如果这一频率比你的应用能够处理的频率要快,那么你可能会遇到UI闪烁或数据溢出等问题。为了避免这一情况发生,应该调用LocationRequest.setFastestInterval()这一方法设置更新频率的最高限额。

调用LocationRequest.setFastestInterval()方法还可以节省电量。当你通过LocationRequest.setInterval()请求了一个更新间隔后,又用LocationRequest.setFastestInterval()请求了一个最大速率后,你的应用会以正常速率进行更新。如果其它应用使用了一个更快的更新速率,那么你的更新频率也会加快。如果没有其它应用申请了更快的更新速率,那么你的应用会以LocationRequest.setInterval()中所设置的速率进行更新。

接下来,设置精度参数。在一个前台应用程序中,你需要以高频率更新地理位置,所以使用LocationRequest.PRIORITY_HIGH_ACCURACY设置精度。

下面的代码片段展示课如何设置更新间隔和精度:

public class MainActivity extends FragmentActivity implements
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationListener {
    ...
    // Global constants
    ...
    // Milliseconds per second
    private static final int MILLISECONDS_PER_SECOND = 1000;
    // Update frequency in seconds
    public static final int UPDATE_INTERVAL_IN_SECONDS = 5;
    // Update frequency in milliseconds
    private static final long UPDATE_INTERVAL =
            MILLISECONDS_PER_SECOND * UPDATE_INTERVAL_IN_SECONDS;
    // The fastest update frequency, in seconds
    private static final int FASTEST_INTERVAL_IN_SECONDS = 1;
    // A fast frequency ceiling in milliseconds
    private static final long FASTEST_INTERVAL =
            MILLISECONDS_PER_SECOND * FASTEST_INTERVAL_IN_SECONDS;
    ...
    // Define an object that holds accuracy and frequency parameters
    LocationRequest mLocationRequest;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Create the LocationRequest object
        mLocationRequest = LocationRequest.create();
        // Use high accuracy
        mLocationRequest.setPriority(
                LocationRequest.PRIORITY_HIGH_ACCURACY);
        // Set the update interval to 5 seconds
        mLocationRequest.setInterval(UPDATE_INTERVAL);
        // Set the fastest update interval to 1 second
        mLocationRequest.setFastestInterval(FASTEST_INTERVAL);
        ...
    }
    ...
}

Note:

如果你的应用要访问网络或者在接收到更新后需要做其它长期的任务,那么应该调整更新频率到一个比较慢的值。这可以避免你的应用接收到太多它来不及处理的更新数据。一旦长期处理的任务结束了,可以再通过设置最快更新频率到一个较快的值。


五). 开始位置更新

要发送位置更新请求,在onCreate()创建一个定位客户端,之后连接它,并通过requestLocationUpdates()发起请求。因为你的客户端必须连接以后你的应用才能收到更新,所以你应该在onStart()方法中连接到客户端。这能保证当你的应用可见时,你都能获取一个已连接的有效的客户端。因为你需要在发出请求前先进行连接,所以在ConnectionCallbacks.onConnected()发出更新请求。

另外要记住用户可能会有各种各样的原因希望关闭位置更新。你应该为用户提供一个这样做的方法,并且你应该保证当更新关闭了之后,你不会在onStart()中启动更新。为了记录用户的设置,在onPause()方法中保存应用的SharedPreferences,并在onResume()方法中获取它。

下面的代码片段展示了如何在onCreate()方法中设置客户端,以及如何在onStart()方法中连接并发出更新请求:

public class MainActivity extends FragmentActivity implements
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationListener {
    ...
    // Global variables
    ...
    LocationClient mLocationClient;
    boolean mUpdatesRequested;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        // Open the shared preferences
        mPrefs = getSharedPreferences("SharedPreferences",
                Context.MODE_PRIVATE);
        // Get a SharedPreferences editor
        mEditor = mPrefs.edit();
        /*
         * Create a new location client, using the enclosing class to
         * handle callbacks.
         */
        mLocationClient = new LocationClient(this, this, this);
        // Start with updates turned off
        mUpdatesRequested = false;
        ...
    }
    ...
    @Override
    protected void onPause() {
        // Save the current setting for updates
        mEditor.putBoolean("KEY_UPDATES_ON", mUpdatesRequested);
        mEditor.commit();
        super.onPause();
    }
    ...
    @Override
    protected void onStart() {
        ...
        mLocationClient.connect();
    }
    ...
    @Override
    protected void onResume() {
        /*
         * Get any previous setting for location updates
         * Gets "false" if an error occurs
         */
        if (mPrefs.contains("KEY_UPDATES_ON")) {
            mUpdatesRequested =
                    mPrefs.getBoolean("KEY_UPDATES_ON", false);

        // Otherwise, turn off location updates
        } else {
            mEditor.putBoolean("KEY_UPDATES_ON", false);
            mEditor.commit();
        }
    }
    ...
    /*
     * Called by Location Services when the request to connect the
     * client finishes successfully. At this point, you can
     * request the current location or start periodic updates
     */
    @Override
    public void onConnected(Bundle dataBundle) {
        // Display the connection status
        Toast.makeText(this, "Connected", Toast.LENGTH_SHORT).show();
        // If already requested, start periodic updates
        if (mUpdatesRequested) {
            mLocationClient.requestLocationUpdates(mLocationRequest, this);
        }
    }
    ...
}

更多关于保存配置信息的知识,可以查看:Saving Key-Value Sets


六). 停止位置更新

要停止位置更新,在onPause()方法中保存更新标识的状态,并在onStop()方法中通过调用removeLocationUpdates(LocationListener)来停止更新,例如:

public class MainActivity extends FragmentActivity implements
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationListener {
    ...
    /*
     * Called when the Activity is no longer visible at all.
     * Stop updates and disconnect.
     */
    @Override
    protected void onStop() {
        // If the client is connected
        if (mLocationClient.isConnected()) {
            /*
             * Remove location updates for a listener.
             * The current Activity is the listener, so
             * the argument is "this".
             */
            removeLocationUpdates(this);
        }
        /*
         * After disconnect() is called, the client is
         * considered "dead".
         */
        mLocationClient.disconnect();
        super.onStop();
    }
    ...
}

现在你已经有了请求并接收定期位置更新的基本应用框架。你可以将这节课中所讲的东西结合到导航,行为识别,反地址解析等等场景中。

下一节课中,我们将会讲解如何使用当前地点显示现在的街道地址。

推荐阅读