在地图上选择当前地点并显示详细信息

本教程介绍了如何查找 Android 设备的当前位置,并显示该位置上相应地点(商家或其他地图注点)的详细信息。请按照本教程中的说明操作,使用 Maps SDK for AndroidPlaces SDK for Android 以及 Google Play Services Location API 中的一体化位置信息提供方构建一个 Android 应用。

获取代码

从 GitHub 克隆或下载 Google Maps Android API v2 示例代码库

设置您的开发项目

按照以下步骤在 Android Studio 中创建教程项目。

  1. 下载安装 Android Studio。
  2. Google Play 服务软件包添加到 Android Studio。
  3. 克隆或下载 Google Maps Android API v2 示例代码库(如果您在开始阅读本教程之前尚未执行此操作)。
  4. 导入教程项目:

    • 在 Android Studio 中,依次选择 File > New > Import Project
    • 转到已下载的 Google Maps Android API v2 示例代码库的保存位置。
    • 在以下位置找到 CurrentPlaceDetailsOnMap 项目:
      PATH-TO-SAVED-REPO/android-samples/tutorials/CurrentPlaceDetailsOnMap
    • 选择项目目录,然后点击 OK。Android Studio 现在将使用 Gradle 构建工具构建您的项目。

获取 API 密钥并启用必要的 API

如需完成本教程,您需要一个已获得 Maps SDK for Android 和 Places SDK for Android 使用授权的 Google API 密钥。

点击下面的按钮以获取密钥并激活 API。

开始使用

如需了解详情,请参阅有关获取 API 密钥的完整指南。

向您的应用添加 API 密钥

  1. 修改项目的 gradle.properties 文件。
  2. 将您的 API 密钥粘贴到 GOOGLE_MAPS_API_KEY 属性的值中:

    GOOGLE_MAPS_API_KEY=PASTE-YOUR-API-KEY-HERE

    在您构建应用时,Gradle 将 API 密钥复制到应用的 Android 清单中。应用的 build.gradle 文件包含将清单中的字符串 google_maps_key 映射到 gradle 属性 GOOGLE_MAPS_API_KEY 的以下行:

    resValue "string", "google_maps_key",
            (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
    

构建并运行您的应用

  1. 将 Android 设备连接到您的计算机。按照说明在您的 Android 设备上启用开发者选项,并配置您的系统以检测设备。(或者,您可以使用 Android 虚拟设备 (AVD) 管理器来配置虚拟设备。选择模拟器时,请务必选择一个包含 Google API 的映像。如需了解详情,请参阅入门指南。)
  2. 在 Android Studio 中,点击 Run 菜单选项(或 play 按钮图标)。按提示选择设备。

Android Studio 会调用 Gradle 来构建应用,然后在设备或模拟器上运行该应用。您将看到一张地图,围绕着您当前所在位置显示许多标记,与本页上的图像类似。

问题排查:

  • 如果您没有看到地图,请检查您是否已获取 API 密钥,以及是否已将其添加到应用,如上文所述。在 Android Studio 的 Android Monitor 中查看日志,获取有关 API 密钥的错误消息。
  • 如果地图上只显示一个位于悉尼海港大桥(应用中指定的默认位置)的标记,请检查您是否已向应用授予位置权限。应用会在运行时按照 Android 权限指南中描述的模式提示您提供位置权限。请注意,您还可以通过选择 Settings > Apps > 应用的名称 > Permissions > Location 直接在设备上设置权限。如需详细了解如何在您的代码中处理权限,请参阅下面有关在您的应用中请求位置权限的指南。
  • 使用 Android Studio 调试工具查看日志并调试应用。

了解代码

本部分教程介绍了 CurrentPlaceDetailsOnMap 应用最重要的部分,以帮助您了解如何构建类似的应用。

实例化 Places API 客户端

以下接口提供了 Places SDK for Android 的主要入口点:

  • GeoDataClient,可访问包含本地地点和商家信息的 Google 数据库。
  • PlaceDetectionClient,可快速访问设备的当前地点,并且让用户有机会报告位于特定地点的设备的位置。

LocationServices 接口是 Android 位置信息服务的主要入口点。

如需使用这些 API,请在 Fragment 或 Activity 的 onCreate() 方法中实例化 GeoDataClientPlaceDetectionClientFusedLocationProviderClient,如以下代码示例所示:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // Construct a GeoDataClient.
    mGeoDataClient = Places.getGeoDataClient(this, null);

    // Construct a PlaceDetectionClient.
    mPlaceDetectionClient = Places.getPlaceDetectionClient(this, null);

    // Construct a FusedLocationProviderClient.
    mFusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);
}

请求位置权限

您的应用必须请求位置权限才能确定设备的位置,并允许用户点按地图上的“我的位置”按钮。

本教程提供了请求精确位置权限所需的代码。如需了解详情,请参阅有关 Android 权限的指南。

  1. 在 Android 清单中,添加此权限作为 <manifest> 元素的子元素:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.example.currentplacedetailsonmap">
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    </manifest>
    
  2. 在应用中请求运行时权限,让用户有机会允许或拒绝位置权限。以下代码会检查用户是否已授予精确位置权限。如果未授予,它将请求此权限:

    private void getLocationPermission() {
        /*
         * Request location permission, so that we can get the location of the
         * device. The result of the permission request is handled by a callback,
         * onRequestPermissionsResult.
         */
        if (ContextCompat.checkSelfPermission(this.getApplicationContext(),
                android.Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            mLocationPermissionGranted = true;
        } else {
            ActivityCompat.requestPermissions(this,
                    new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION},
                    PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION);
        }
    }
    
  3. 替换 onRequestPermissionsResult() 回调以处理权限请求的结果:

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        mLocationPermissionGranted = false;
        switch (requestCode) {
            case PERMISSIONS_REQUEST_ACCESS_FINE_LOCATION: {
                // If request is cancelled, the result arrays are empty.
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    mLocationPermissionGranted = true;
                }
            }
        }
        updateLocationUI();
    }
    

    本教程后面的部分介绍了 updateLocationUI() 方法。

添加地图

使用 Maps SDK for Android 显示地图。

  1. 向 Activity 的布局文件 activity_maps.xml 添加 <fragment> 元素。此元素定义一个 SupportMapFragment 以充当地图的容器,并提供 GoogleMap 对象的访问权限。本教程使用 Android 支持库版本的地图 Fragment,以确保与较早版本的 Android 框架向后兼容。

    <fragment xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/map"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.example.currentplacedetailsonmap.MapsActivityCurrentPlace" />
    
    
  2. 在您的 Activity 的 onCreate() 方法中,将布局文件设置为内容视图:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_maps);
    }
    
  3. 实现 OnMapReadyCallback 接口并替换 onMapReady() 方法,以便在 GoogleMap 对象可用时设置地图:

    public void onMapReady(GoogleMap map) {
        mMap = map;
    
        // Do other setup activities here too, as described elsewhere in this tutorial.
    
        // Turn on the My Location layer and the related control on the map.
        updateLocationUI();
    
        // Get the current location of the device and set the position of the map.
        getDeviceLocation();
    }
    
  4. 在您的 Activity 的 onCreate() 方法中,通过调用 FragmentManager.findFragmentById() 来获取地图 Fragment 的句柄。然后使用 getMapAsync() 进行注册以执行地图回调:

    SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
            .findFragmentById(R.id.map);
    mapFragment.getMapAsync(this);
    
  5. 编写 updateLocationUI() 方法以在地图上设置位置控件。如果用户已授予位置权限,则在地图上启用“我的位置”图层和相关控件,否则停用它们并将当前位置设为 null:

    private void updateLocationUI() {
        if (mMap == null) {
            return;
        }
        try {
            if (mLocationPermissionGranted) {
                mMap.setMyLocationEnabled(true);
                mMap.getUiSettings().setMyLocationButtonEnabled(true);
            } else {
                mMap.setMyLocationEnabled(false);
                mMap.getUiSettings().setMyLocationButtonEnabled(false);
                mLastKnownLocation = null;
                getLocationPermission();
            }
        } catch (SecurityException e)  {
            Log.e("Exception: %s", e.getMessage());
        }
    }
    

获取 Android 设备的位置并在地图上定位

使用一体化位置信息提供方查找设备的最后已知位置,然后使用该位置在地图上定位。本教程提供了您所需的代码。如需详细了解如何获取设备的位置信息,请参阅有关 Google Play Services Location API 中一体化位置信息提供方的指南。

private void getDeviceLocation() {
    /*
     * Get the best and most recent location of the device, which may be null in rare
     * cases when a location is not available.
     */
    try {
        if (mLocationPermissionGranted) {
            Task locationResult = mFusedLocationProviderClient.getLastLocation();
            locationResult.addOnCompleteListener(this, new OnCompleteListener() {
                @Override
                public void onComplete(@NonNull Task task) {
                    if (task.isSuccessful()) {
                        // Set the map's camera position to the current location of the device.
                        mLastKnownLocation = task.getResult();
                        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
                                new LatLng(mLastKnownLocation.getLatitude(),
                                        mLastKnownLocation.getLongitude()), DEFAULT_ZOOM));
                    } else {
                        Log.d(TAG, "Current location is null. Using defaults.");
                        Log.e(TAG, "Exception: %s", task.getException());
                        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(mDefaultLocation, DEFAULT_ZOOM));
                        mMap.getUiSettings().setMyLocationButtonEnabled(false);
                    }
                }
            });
        }
    } catch(SecurityException e)  {
        Log.e("Exception: %s", e.getMessage());
    }
}

获取当前地点

使用 Places SDK for Android 获取设备当前位置可能存在的地点的列表。在这种情况下,地点是指商家或其他地图注点。

当用户点击获取地点按钮时,本教程构建的应用就会获取当前地点。它将以列表形式显示可能的地点,让用户进行选择,然后在地图上为所选地点的位置添加标记。本教程提供了您与 Places SDK for Android 交互所需的代码。如需了解详情,请参阅有关获取当前地点的指南。

  1. 为选项菜单创建布局文件 (current_place_menu.xml),并替换 onCreateOptionsMenu() 方法,以便设置选项菜单。请参阅随附的示例应用,获取相关代码。
  2. 替换 onOptionsItemSelected() 方法以在用户点击获取地点选项时获取当前地点:
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.option_get_place) {
            showCurrentPlace();
        }
        return true;
    }
    
  3. 创建 showCurrentPlace() 方法以获取设备当前位置的可能地点列表:

    private void showCurrentPlace() {
        if (mMap == null) {
            return;
        }
    
        if (mLocationPermissionGranted) {
            // Use fields to define the data types to return.
            List<Place.Field> placeFields = Arrays.asList(Place.Field.NAME, Place.Field.ADDRESS,
                    Place.Field.LAT_LNG);
    
            // Use the builder to create a FindCurrentPlaceRequest.
            FindCurrentPlaceRequest request =
                    FindCurrentPlaceRequest.newInstance(placeFields);
    
            // Get the likely places - that is, the businesses and other points of interest that
            // are the best match for the device's current location.
            @SuppressWarnings("MissingPermission") final
            Task<FindCurrentPlaceResponse> placeResult =
                    mPlacesClient.findCurrentPlace(request);
            placeResult.addOnCompleteListener (new OnCompleteListener<FindCurrentPlaceResponse>() {
                @Override
                public void onComplete(@NonNull Task<FindCurrentPlaceResponse> task) {
                    if (task.isSuccessful() && task.getResult() != null) {
                        FindCurrentPlaceResponse likelyPlaces = task.getResult();
    
                        // Set the count, handling cases where less than 5 entries are returned.
                        int count;
                        if (likelyPlaces.getPlaceLikelihoods().size() < M_MAX_ENTRIES) {
                            count = likelyPlaces.getPlaceLikelihoods().size();
                        } else {
                            count = M_MAX_ENTRIES;
                        }
    
                        int i = 0;
                        mLikelyPlaceNames = new String[count];
                        mLikelyPlaceAddresses = new String[count];
                        mLikelyPlaceAttributions = new List[count];
                        mLikelyPlaceLatLngs = new LatLng[count];
    
                        for (PlaceLikelihood placeLikelihood : likelyPlaces.getPlaceLikelihoods()) {
                            // Build a list of likely places to show the user.
                            mLikelyPlaceNames[i] = placeLikelihood.getPlace().getName();
                            mLikelyPlaceAddresses[i] = placeLikelihood.getPlace().getAddress();
                            mLikelyPlaceAttributions[i] = placeLikelihood.getPlace()
                                    .getAttributions();
                            mLikelyPlaceLatLngs[i] = placeLikelihood.getPlace().getLatLng();
    
                            i++;
                            if (i > (count - 1)) {
                                break;
                            }
                        }
    
                        // Show a dialog offering the user the list of likely places, and add a
                        // marker at the selected place.
                        MapsActivityCurrentPlace.this.openPlacesDialog();
                    }
                    else {
                        Log.e(TAG, "Exception: %s", task.getException());
                    }
                }
            });
        } else {
            // The user has not granted permission.
            Log.i(TAG, "The user did not grant location permission.");
    
            // Add a default marker, because the user hasn't selected a place.
            mMap.addMarker(new MarkerOptions()
                    .title(getString(R.string.default_info_title))
                    .position(mDefaultLocation)
                    .snippet(getString(R.string.default_info_snippet)));
    
            // Prompt the user for permission.
            getLocationPermission();
        }
    }
    
  4. 创建 openPlacesDialog() 方法以显示一个表单,允许用户从可能地点的列表中选择地点。在地图上为所选地点添加标记。标记内容包括地点的名称和地址以及 API 提供的任何属性:

    private void openPlacesDialog() {
        // Ask the user to choose the place where they are now.
        DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // The "which" argument contains the position of the selected item.
                LatLng markerLatLng = mLikelyPlaceLatLngs[which];
                String markerSnippet = mLikelyPlaceAddresses[which];
                if (mLikelyPlaceAttributions[which] != null) {
                    markerSnippet = markerSnippet + "\n" + mLikelyPlaceAttributions[which];
                }
    
                // Add a marker for the selected place, with an info window
                // showing information about that place.
                mMap.addMarker(new MarkerOptions()
                        .title(mLikelyPlaceNames[which])
                        .position(markerLatLng)
                        .snippet(markerSnippet));
    
                // Position the map's camera at the location of the marker.
                mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(markerLatLng,
                        DEFAULT_ZOOM));
            }
        };
    
        // Display the dialog.
        AlertDialog dialog = new AlertDialog.Builder(this)
                .setTitle(R.string.pick_place)
                .setItems(mLikelyPlaceNames, listener)
                .show();
    }
    
  5. 针对信息窗口内容创建自定义布局。这让您可以在信息窗口中显示多行内容。首先,添加一个 XML 布局文件 custom_info_contents.xml,其中包含一个信息窗口标题文本视图,以及另一个代码段(即信息窗口的文本内容)文本视图:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layoutDirection="locale"
        android:orientation="vertical">
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:textColor="#ff000000"
            android:textStyle="bold" />
    
        <TextView
            android:id="@+id/snippet"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#ff7f7f7f" />
    </LinearLayout>
    
    
  6. 实现 InfoWindowAdapter 接口以扩充此布局并加载信息窗口内容:

    @Override
    public void onMapReady(GoogleMap map) {
        // Do other setup activities here too, as described elsewhere in this tutorial.
        mMap.setInfoWindowAdapter(new GoogleMap.InfoWindowAdapter() {
    
        @Override
        // Return null here, so that getInfoContents() is called next.
        public View getInfoWindow(Marker arg0) {
            return null;
        }
    
        @Override
        public View getInfoContents(Marker marker) {
            // Inflate the layouts for the info window, title and snippet.
            View infoWindow = getLayoutInflater().inflate(R.layout.custom_info_contents, null);
    
            TextView title = ((TextView) infoWindow.findViewById(R.id.title));
            title.setText(marker.getTitle());
    
            TextView snippet = ((TextView) infoWindow.findViewById(R.id.snippet));
            snippet.setText(marker.getSnippet());
    
            return infoWindow;
          }
        });
    }
    

保存地图的状态

保存地图的相机位置和设备位置。当用户旋转 Android 设备或更改配置时,Android 框架将销毁地图 Activity 并进行重建。为确保顺畅的用户体验,最好存储相关的应用状态并在需要时恢复它。

本教程提供了保存地图的状态所需的所有代码。如需了解详情,请参阅有关 savedInstanceState 软件包的指南。

  1. 在地图 Activity 中,设置用于存储 Activity 状态的键值:

    private static final String KEY_CAMERA_POSITION = "camera_position";
    private static final String KEY_LOCATION = "location";
    
  2. 实现 onSaveInstanceState() 回调以便在 Activity 暂停时保存状态:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        if (mMap != null) {
            outState.putParcelable(KEY_CAMERA_POSITION, mMap.getCameraPosition());
            outState.putParcelable(KEY_LOCATION, mLastKnownLocation);
            super.onSaveInstanceState(outState);
        }
    }
    
  3. 在 Activity 的 onCreate() 方法中,检索设备的位置和地图的相机位置(如果之前已保存):

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState != null) {
            mCurrentLocation = savedInstanceState.getParcelable(KEY_LOCATION);
            mCameraPosition = savedInstanceState.getParcelable(KEY_CAMERA_POSITION);
        }
        ...
    }