显示位置地址

获取最近一次的已知位置接收位置信息更新课程介绍了如何以 Location 对象的形式获取用户包含经纬度坐标的位置信息。虽然纬度和经度有助于计算距离或显示在地图上的位置,但在很多情况下,位置的地址更为实用。例如,如果您想要用户知道他们在哪里或者附近有什么,那么该位置所在的街道地址要比地理坐标(纬度/经度)更有意义。

您可以使用 Android 框架地理位置 API 中的 Geocoder 类,将地址转换为相应的地理坐标。这一过程称为地理编码。或者,您可以将地理位置转换为地址。地址查询功能也称为逆向地理编码。

本课介绍了如何使用 getFromLocation() 方法将地理位置转换为地址。该方法会返回与给定纬度和经度相对应的大概街道地址。

获取地理位置

设备最近一次的已知位置有助于开始使用地址查询功能。获取最近一次的已知位置一课介绍了如何使用一体化位置信息提供程序提供的 getLastLocation() 方法来查找设备最近一次的位置。

要访问一体化位置信息提供程序,请创建一个 FusedLocationProviderClient 实例。要了解如何创建客户端,请参阅创建位置信息服务客户端

要启用一体化位置信息提供程序来检索准确的街道地址,请在应用清单中将位置权限设置为 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>
    

定义 Intent 服务以获取地址

Geocoder 类提供的 getFromLocation() 方法接受纬度和经度,并会返回地址列表。这是同步方法,可能需要很长时间才能完成相应操作,因此不要通过应用的主界面线程调用该方法。

IntentService 类提供在后台线程上运行任务的结构。使用此类,您可以处理长时间运行的操作,而不会影响界面的响应速度。

定义扩展 IntentServiceFetchAddressIntentService 类。此类是您的地址查询服务。这项 Intent 服务会在工作线程上异步处理 Intent,并在处理完成时自行停止。这些 Intent extra 提供该服务所需的数据,其中包括一个 Location 对象(用于转换为地址)和一个 ResultReceiver 对象(用于处理地址查询的结果)。该服务使用 Geocoder 来获取相应位置的地址,并将结果发送给 ResultReceiver

在应用清单中定义 Intent 服务

在您的应用清单中添加定义 Intent 服务的条目,如下所示:

    <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> 元素不需要包含 Intent 过滤器,因为您的主 Activity 会通过指定用于 Intent 的类名称来创建显式 Intent。

创建 Geocoder

将地理位置转换为地址的过程称为逆向地理编码。要执行 Intent 服务的主要工作(您的逆向地理编码请求),请在 FetchAddressIntentService 类中实现 onHandleIntent()。创建 Geocoder 对象可处理逆向地理编码。

语言区域表示特定的地理或语言区域。语言区域对象会调整数字或日期等信息的表示方式,以适应该语言区域所代表地区的惯例。将 Locale 对象传递给 Geocoder 对象可确保生成的地址已本地化为用户的地理区域,如下例所示:

Kotlin

    override fun onHandleIntent(intent: Intent?) {
        val geocoder = Geocoder(this, Locale.getDefault())
        // ...
    }
    

Java

    @Override
    protected void onHandleIntent(Intent intent) {
        Geocoder geocoder = new Geocoder(this, Locale.getDefault());
        // ...
    }
    

检索街道地址

现在,您可以通过该地理编码器检索街道地址,处理可能发生的任何错误,并将结果发回给请求该地址的 Activity。要报告地理编码过程的结果,您需要两个分别表示成功和失败的数字常量。定义常量以包含值,如以下代码段所示:

Kotlin

    object Constants {
        const val SUCCESS_RESULT = 0
        const val FAILURE_RESULT = 1
        const val PACKAGE_NAME = "com.google.android.gms.location.sample.locationaddress"
        const val RECEIVER = "$PACKAGE_NAME.RECEIVER"
        const val RESULT_DATA_KEY = "${PACKAGE_NAME}.RESULT_DATA_KEY"
        const val LOCATION_DATA_EXTRA = "${PACKAGE_NAME}.LOCATION_DATA_EXTRA"
    }
    

Java

    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。

检查是否出现以下错误,如下面的代码示例所示:

  • 未提供位置数据 - Intent extra 不包含逆向地理编码所需的 Location 对象。
  • 使用的纬度或经度无效 - Location 对象中提供的纬度和/或经度值无效。
  • 地理编码器不可用 – 由于网络连接错误或者 IO 异常,后台地理编码服务不可用。
  • 抱歉,未找到地址 - 地理编码器无法找到给定纬度/经度的地址。

如果出现错误,请将相应的错误消息放入 errorMessage 变量中,这样您就可以将其发回给请求的 Activity。

要获取地址对象的各行,请使用 Address 类提供的 getAddressLine() 方法。将这些行添加到准备返回到请求该地址的 Activity 的地址 Fragment 列表中。

要将结果发回给请求的 Activity,请调用 deliverResultToReceiver() 方法(在将地址返回给请求者中定义)。结果中包含前面提到的数字成功/失败代码以及一个字符串。如果逆向地理编码成功,则该字符串中会包含相应地址。如果失败,该字符串中会包含错误消息,如以下代码示例所示:

Kotlin

    protected fun onHandleIntent(intent: Intent?) {
        intent ?: return

        var errorMessage = ""

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

        // ...

        var addresses: List<Address> = emptyList()

        try {
            addresses = geocoder.getFromLocation(
                    location.latitude,
                    location.longitude,
                    // In this sample, we 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.latitude , " +
                    "Longitude =  $location.longitude", illegalArgumentException)
        }

        // Handle case where no address was found.
        if (addresses.isEmpty()) {
            if (errorMessage.isEmpty()) {
                errorMessage = getString(R.string.no_address_found)
                Log.e(TAG, errorMessage)
            }
            deliverResultToReceiver(Constants.FAILURE_RESULT, errorMessage)
        } else {
            val address = addresses[0]
            // Fetch the address lines using getAddressLine,
            // join them, and send them to the thread.
            val addressFragments = with(address) {
                (0..maxAddressLineIndex).map { getAddressLine(it) }
            }
            Log.i(TAG, getString(R.string.address_found))
            deliverResultToReceiver(Constants.SUCCESS_RESULT,
                    addressFragments.joinToString(separator = "\n"))
        }
    }
    

Java

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent == null) {
            return;
        }
        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));
        }
    }
    

将地址返回给请求者

Intent 服务必须完成的最后一项操作是将地址发回给启动该服务的 Activity 中的 ResultReceiver。通过 ResultReceiver 类,您可以发送一个数字结果代码以及一条包含结果数据的消息。该数字代码可用于报告地理编码请求成功与否。如果逆向地理编码成功,则该消息中会包含相应地址。如果失败,则该消息中会包含描述失败原因的文本。

您已通过地理编码器检索到了地址,捕获了可能发生的任何错误,并调用了 deliverResultToReceiver() 方法,因此,您现在必须定义 deliverResultToReceiver() 方法,将结果代码以及消息包发送给结果接收器。

对于结果代码,请使用 resultCode 参数中传递给 deliverResultToReceiver() 方法的值。要构建消息包,请将 Constants 类(在检索街道地址数据中定义)中的 RESULT_DATA_KEY 常量与 message 参数中传递给 deliverResultToReceiver() 方法的值连接起来,如下例所示:

Kotlin

    class FetchAddressIntentService : IntentService() {
        private var receiver: ResultReceiver? = null

        // ...

        private fun deliverResultToReceiver(resultCode: Int, message: String) {
            val bundle = Bundle().apply { putString(Constants.RESULT_DATA_KEY, message) }
            receiver?.send(resultCode, bundle)
        }

    }
    

Java

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

启动 Intent 服务

根据上一节的定义,Intent 服务在后台运行,并获取与给定地理位置相对应的地址。当您启动该服务时,Android 框架会实例化并启动该服务(如果尚未运行的话),并根据需要创建一个进程。如果该服务已在运行,则继续保持运行状态。由于该服务扩展了 IntentService,因此它会在处理完所有 Intent 后自动关闭。

从应用的主 Activity 启动该服务,并创建 Intent 以将数据传递给该服务。您需要一个显式 Intent,因为您只想自己的服务响应该 Intent。如需了解详情,请参阅 Intent 类型

要创建显式 Intent,请指定服务所使用的类的名称:FetchAddressIntentService.class。将这些信息传递到 Intent extra 中:

  • 一个 ResultReceiver,用于处理地址查询结果。
  • 一个 Location 对象,其中包含要转换为地址的纬度和经度。

以下代码示例展示了如何启动 Intent 服务:

Kotlin

    class MainActivity : AppCompatActivity(), ConnectionCallbacks, OnConnectionFailedListener {

        private var lastLocation: Location? = null
        private lateinit var resultReceiver: AddressResultReceiver

        // ...

        private fun startIntentService() {

            val intent = Intent(this, FetchAddressIntentService::class.java).apply {
                putExtra(Constants.RECEIVER, resultReceiver)
                putExtra(Constants.LOCATION_DATA_EXTRA, lastLocation)
            }
            startService(intent)
        }
    }
    

Java

    public class MainActivity extends AppCompatActivity implements
            ConnectionCallbacks, OnConnectionFailedListener {

        protected Location lastLocation;
        private AddressResultReceiver resultReceiver;

        // ...

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

注意:为确保您的应用安全无虞,请务必在启动 Service 时使用显式 Intent,并且不要为您的服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定响应该 Intent 的服务,并且用户无法看到哪项服务会启动。

当用户执行需要查询地理编码地址的操作时,调用上述 startIntentService() 方法。例如,用户可以按下应用界面上的“获取地址”按钮。以下代码段展示了对按钮处理程序中 startIntentService() 方法的调用:

Kotlin

    fun fetchAddressButtonHander(view: View) {
        fusedLocationClient?.lastLocation?.addOnSuccessListener { location: Location? ->
            lastKnownLocation = location

            if (lastKnownLocation == null) return@addOnSuccessListener

            if (!Geocoder.isPresent()) {
                Toast.makeText(this@MainActivity,
                        R.string.no_geocoder_available,
                        Toast.LENGTH_LONG).show()
                return@addOnSuccessListener
            }

            // Start service and update UI to reflect new location
            startIntentService()
            updateUI()
        }
    }
    

Java

    private void fetchAddressButtonHander(View view) {
        fusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, new OnSuccessListener<Location>() {
                    @Override
                    public void onSuccess(Location location) {
                        lastKnownLocation = location;

                        // In some rare cases the location returned can be null
                        if (lastKnownLocation == null) {
                            return;
                        }

                        if (!Geocoder.isPresent()) {
                            Toast.makeText(MainActivity.this,
                                    R.string.no_geocoder_available,
                                    Toast.LENGTH_LONG).show();
                            return;
                        }

                        // Start service and update UI to reflect new location
                        startIntentService();
                        updateUI();
                    }
                });
        }
    

接收地理编码结果

在 Intent 服务处理地理编码请求后,它会使用 ResultReceiver 将结果返回给发出请求的 Activity。在发出请求的 Activity 中,定义一个扩展 ResultReceiverAddressResultReceiver 以处理来自 FetchAddressIntentService 的响应。

该结果包括一个数字结果代码 (resultCode) 以及一条包含结果数据 (resultData) 的消息。如果逆向地理编码过程成功,则 resultData 中会包含相应地址。如果失败,则 resultData 中会包含描述失败原因的文本。如需详细了解可能的错误,请参阅将地址返回给请求者

替换 onReceiveResult() 方法以处理传递给结果接收器的结果,如以下代码示例所示:

Kotlin

    class MainActivity : AppCompatActivity() {
        // ...
        internal inner class AddressResultReceiver(handler: Handler) : ResultReceiver(handler) {

            override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {

                // Display the address string
                // or an error message sent from the intent service.
                addressOutput = 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))
                }

            }
        }
    }
    

Java

    public class MainActivity extends AppCompatActivity {

        // ...

        class AddressResultReceiver extends ResultReceiver {
            public AddressResultReceiver(Handler handler) {
                super(handler);
            }

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

                if (resultData == null) {
                    return;
                }

                // Display the address string
                // or an error message sent from the intent service.
                addressOutput = resultData.getString(Constants.RESULT_DATA_KEY);
                if (addressOutput == null) {
                    addressOutput = "";
                }
                displayAddressOutput();

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

            }
        }
    }