将地图添加到您的 Android 应用 (Kotlin)

1. 准备工作

此 Codelab 介绍了如何通过构建一个可显示美国加利福尼亚州旧金山市自行车商店地图的应用,将 Maps SDK for Android 与您的应用集成以及使用其核心功能。

f05e1ca27ff42bf6.png

前提条件

  • 具备 Kotlin 和 Android 开发方面的基础知识

您应执行的操作

  • 启用并使用 Maps SDK for Android,以将 Google 地图添加到 Android 应用中。
  • 添加和自定义标记,以及为标记划分聚类。
  • 在地图上绘制多段线和多边形。
  • 以编程方式控制相机视点。

您需要满足的条件

2. 进行设置

对于以下启用步骤,您需要启用 Maps SDK for Android

设置 Google Maps Platform

如果您还没有已启用结算功能的 Google Cloud Platform 帐号和项目,请参阅 Google Maps Platform 使用入门指南,创建结算帐号和项目。

  1. Cloud Console 中,点击项目下拉菜单,选择要用于此 Codelab 的项目。

  1. Google Cloud Marketplace 中启用此 Codelab 所需的 Google Maps Platform API 和 SDK。为此,请按照此视频此文档中的步骤操作。
  2. 在 Cloud Console 的凭据页面中生成 API 密钥。您可以按照此视频此文档中的步骤操作。向 Google Maps Platform 发出的所有请求都需要 API 密钥。

3.快速上手

为帮助您尽快入门,我们在下面提供了一些起始代码,帮助您顺利完成此 Codelab。您可以跳到解决方案部分,但如果您想要按照所有步骤自行构建,请继续阅读。

  1. 克隆代码库(如果您已安装 git)。
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

或者,您也可以点击下面的按钮,下载源代码。

  1. 获取代码后,在 Android Studio 中打开 starter 目录中的相应项目。

4.添加 Google 地图

在本部分中,您将添加 Google 地图,以便它在您启动应用时进行加载。

d1d068b5d4ae38b9.png

添加您的 API 密钥

您需要将在之前的步骤中创建的 API 密钥提供给应用,以便 Maps SDK for Android 将您的密钥与应用相关联。

  1. 如需提供此密钥,请打开项目的根目录中名为 local.properties 的文件(与 gradle.propertiessettings.gradle 所在级别相同)。
  2. 在该文件中,定义一个新密钥 GOOGLE_MAPS_API_KEY,其值为您创建的 API 密钥。

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

请注意,local.properties 已列在 Git 代码库中的 .gitignore 文件内。这是因为您的 API 密钥被视为敏感信息,不得签入源代码控制(如果可能的话)。

  1. 接下来,公开您的 API 以便在整个应用中使用,为此,请在您应用的 build.gradle 文件(位于 app/ 目录)中添加 Android 版 Secrets Gradle 插件,并在 plugins 块中添加下面一行代码:

应用级 build.gradle

plugins {
    // ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

您还需要修改项目级 build.gradle 文件,以添加以下类路径:

项目级 build.gradle

buildscript {
    dependencies {
        // ...
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
    }
}

构建时,此插件会将您在 local.properties 文件中定义的密钥以构建变量的形式在 Android 清单文件中提供,并以变量的形式在 Gradle 生成的 BuildConfig 类中提供。使用此插件可移除样板代码,否则需要使用该代码从 local.properties 中读取属性,以便在整个应用中对其进行访问。

添加 Google 地图依赖项

  1. 现在,您的 API 密钥可在应用内进行访问,下一步就是将 Maps SDK for Android 依赖项添加到您应用的 build.gradle 文件中。

在此 Codelab 附带的入门级项目中,已为您添加此依赖项。

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. 接下来,在 AndroidManifest.xml 中添加一个新的 meta-data 代码,以传入您在之前的步骤中创建的 API 密钥。为此,请在 Android Studio 中打开此文件,然后将下面的 meta-data 代码添加到 AndroidManifest.xml 文件(位于 app/src/main)中的 application 对象内。

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. 然后,在 app/src/main/res/layout/ 目录中创建一个名为 activity_main.xml 的新布局文件,并按如下方式定义:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <fragment
       class="com.google.android.gms.maps.SupportMapFragment"
       android:id="@+id/map_fragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</FrameLayout>

此布局具有一个包含 SupportMapFragmentFrameLayout。此 fragment 包含您要在后续步骤中使用的底层 GoogleMaps 对象。

  1. 最后,更新位于 app/src/main/java/com/google/codelabs/buildyourfirstmapMainActivity 类,具体做法是,添加下面的代码以替换 onCreate 方法,这样您就可以使用刚刚创建的新布局设置其内容。

MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. 接下来运行应用,您应该会看到地图在您的设备屏幕上加载。

5. 云端地图样式设置(可选)

您可以使用云端地图样式设置自定义地图样式。

创建地图 ID

如果您尚未使用关联的地图样式创建地图 ID,请参阅地图 ID 指南完成以下步骤:

  1. 创建地图 ID。
  2. 将地图 ID 与地图样式相关联。

为您的应用添加地图 ID

如需使用您创建的地图 ID,请修改 activity_main.xml 文件并在 SupportMapFragmentmap:mapId 属性中传递您的地图 ID。

activity_main.xml

<fragment xmlns:map="http://schemas.android.com/apk/res-auto"
    class="com.google.android.gms.maps.SupportMapFragment"
    <!-- ... -->
    map:mapId="YOUR_MAP_ID" />

完成上述操作后,继续并运行应用,以您选择的样式查看您的地图!

6. 添加标记

在此任务中,您要向地图添加标记,以表示要在地图上突出显示的地图注点。首先,检索入门级项目中为您提供的地点列表,然后将这些地点添加到地图中。在此示例中,这些地点为自行车商店。

bc5576877369b554.png

获取对 GoogleMap 的引用

首先,您需要获取对 GoogleMap 对象的引用,以便使用其方法。为此,请在调用 setContentView() 后立即将以下代码添加到您的 MainActivity.onCreate() 方法中:

MainActivity.onCreate()

val mapFragment = supportFragmentManager.findFragmentById(
    R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
    addMarkers(googleMap)
}

该实现首先会使用 SupportFragmentManager 对象上的 findFragmentById() 方法查找您在之前的步骤中添加的 SupportMapFragment。获取引用后,系统会先调用 getMapAsync(),然后传入 lambda。传递 GoogleMap 对象会在此 lambda 中完成。在此 lambda 中,系统会调用 addMarkers() 方法,我们很快就会介绍其定义。

提供的类:PlacesReader

入门级项目中已为您提供 PlacesReader 类。此类会读取包含 49 个地点的列表(存储在名为 places.json 的 JSON 文件中)并以 List<Place> 的形式返回这些地点。这些地点本身表示美国加利福尼亚州旧金山周边的一系列自行车商店。

如果您有兴趣了解此类的实现,可以在 GitHub 上访问它,也可以在 Android Studio 中打开 PlacesReader 类。

PlacesReader

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import com.google.codelabs.buildyourfirstmap.R
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader

/**
* Reads a list of place JSON objects from the file places.json
*/
class PlacesReader(private val context: Context) {

   // GSON object responsible for converting from JSON to a Place object
   private val gson = Gson()

   // InputStream representing places.json
   private val inputStream: InputStream
       get() = context.resources.openRawResource(R.raw.places)

   /**
    * Reads the list of place JSON objects in the file places.json
    * and returns a list of Place objects
    */
   fun read(): List<Place> {
       val itemType = object : TypeToken<List<PlaceResponse>>() {}.type
       val reader = InputStreamReader(inputStream)
       return gson.fromJson<List<PlaceResponse>>(reader, itemType).map {
           it.toPlace()
       }
   }

加载地点

如需加载自行车商店列表,请在 MainActivity 中添加一个名为 places 的属性,并按如下方式定义:

MainActivity.places

private val places: List<Place> by lazy {
   PlacesReader(this).read()
}

此代码会针对 PlacesReader 调用 read() 方法,该方法会返回 List<Place>Place 包含一个名为 name 的属性(表示地点名称)以及一个 latLng 属性(表示地点所在的坐标)。

Place

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: LatLng,
   val rating: Float
)

向地图添加标记

现在,地点列表已加载到内存中,下一步就是在地图上表示这些地点。

  1. MainActivity 中创建一个名为 addMarkers() 的方法,并按如下方式定义:

MainActivity.addMarkers()

/**
* Adds marker representations of the places list on the provided GoogleMap object
*/
private fun addMarkers(googleMap: GoogleMap) {
   places.forEach { place ->
       val marker = googleMap.addMarker(
           MarkerOptions()
               .title(place.name)
               .position(place.latLng)
       )
   }
}

此方法会先遍历 places 列表,然后针对已提供的 GoogleMap 对象调用 addMarker() 方法。标记是通过实例化 MarkerOptions 对象创建的,以便您自定义标记本身。在此示例中,标记的名称和位置均已提供,分别表示自行车商店的名称及其坐标。

  1. 接下来运行应用,在地图上转到旧金山,看看您刚刚添加的标记!

7. 自定义标记

下面介绍了一些自定义选项,可帮助您刚刚添加的标记脱颖而出,同时向用户传达实用的信息。在此任务中,您将通过自定义每个标记的图像以及点按标记时显示的信息窗口,探索其中一部分自定义选项。

a26f82802fe838e9.png

添加信息窗口

默认情况下,当您点按标记时,信息窗口会显示其名称和摘要(如果已设置)。您可以自定义此窗口,使其能够显示更多信息(例如,地点的地址和评分)。

创建 marker_info_contents.xml

首先,创建一个名为 marker_info_contents.xml 的新布局文件。

  1. 为此,请在 Android Studio 的项目视图中右键点击 app/src/main/res/layout 文件夹,然后依次选择 New > Layout Resource File

8cac51fcbef9171b.png

  1. 在对话框中,在 File name 字段中输入 marker_info_contents,并在 Root element 字段中输入 LinearLayout,然后点击 OK

8783af12baf07a80.png

此布局文件随后会进行扩充,用于表示信息窗口中的内容。

  1. 复制以下代码段中的内容(可在垂直的 LinearLayout 视图组中添加三个 TextViews),然后覆盖文件中的默认代码。

marker_info_contents.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:orientation="vertical"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:gravity="center_horizontal"
   android:padding="8dp">

   <TextView
       android:id="@+id/text_view_title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="18sp"
       android:textStyle="bold"
       tools:text="Title"/>

   <TextView
       android:id="@+id/text_view_address"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="123 Main Street"/>

   <TextView
       android:id="@+id/text_view_rating"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="Rating: 3"/>

</LinearLayout>

创建 InfoWindowAdapter 的实现

为自定义信息窗口创建布局文件后,下一步是实现 GoogleMap.InfoWindowAdapter 接口。此接口包含两个方法:getInfoWindow()getInfoContents()。这两个方法都会返回一个可选的 View 对象,其中前者用于自定义窗口本身,而后者用于自定义窗口内容。在本示例中,您可以同时实现这两个方法,然后自定义 getInfoContents() 返回的内容,同时在 getInfoWindow() 中返回 null,以表明应使用默认窗口。

  1. MainActivity 所在的同一软件包中创建一个名为 MarkerInfoWindowAdapter 的新 Kotlin 文件,具体做法是,在 Android Studio 的项目视图中右键点击 app/src/main/java/com/google/codelabs/buildyourfirstmap 文件夹,然后依次选择 New > Kotlin File/Class

3975ba36eba9f8e1.png

  1. 在对话框中,输入 MarkerInfoWindowAdapter,并突出显示 File

992235af53d3897f.png

  1. 创建文件后,请将以下代码段中的内容复制到您的新文件中。

MarkerInfoWindowAdapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.codelabs.buildyourfirstmap.place.Place

class MarkerInfoWindowAdapter(
    private val context: Context
) : GoogleMap.InfoWindowAdapter {
   override fun getInfoContents(marker: Marker?): View? {
       // 1. Get tag
       val place = marker?.tag as? Place ?: return null

       // 2. Inflate view and set title, address, and rating
       val view = LayoutInflater.from(context).inflate(
           R.layout.marker_info_contents, null
       )
       view.findViewById<TextView>(
           R.id.text_view_title
       ).text = place.name
       view.findViewById<TextView>(
           R.id.text_view_address
       ).text = place.address
       view.findViewById<TextView>(
           R.id.text_view_rating
       ).text = "Rating: %.2f".format(place.rating)

       return view
   }

   override fun getInfoWindow(marker: Marker?): View? {
       // Return null to indicate that the
       // default window (white bubble) should be used
       return null
   }
}

getInfoContents() 方法返回的内容中,该方法中已提供的 Marker 会转换为 Place 类型;如果您还没有设置 Marker 的代码属性,则转化无法进行,该方法会返回 null,不过您可以在后续步骤中执行此操作。

接下来,布局 marker_info_contents.xml 会进行扩充,接着在将 TextViews 添加到 Place 代码中时设置文本。

更新 MainActivity

如需将您目前为止创建的所有组件整合到一起,您需要在 MainActivity 类中添加两行代码。

首先,如需在 getMapAsync 方法调用中传递自定义 InfoWindowAdapterMarkerInfoWindowAdapter,请针对 GoogleMap 对象调用 setInfoWindowAdapter() 方法,并创建一个新的 MarkerInfoWindowAdapter 实例。

  1. 为此,请在 addMarkers() 方法调用结束后在 getMapAsync() lambda 内添加以下代码。

MainActivity.onCreate()

// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

最后,您需要将每个 Place 设置为已添加到地图中的每个 Marker 的代码属性。

  1. 为此,请使用以下代码在 addMarkers() 函数中修改 places.forEach{} 调用:

MainActivity.addMarkers()

places.forEach { place ->
   val marker = googleMap.addMarker(
       MarkerOptions()
           .title(place.name)
           .position(place.latLng)
           .icon(bicycleIcon)
   )

   // Set place as the tag on the marker object so it can be referenced within
   // MarkerInfoWindowAdapter
   marker.tag = place
}

添加自定义标记图像

您可以通过有趣的方式来告知地图上的标记所表示的地点类型,而自定义标记图像便是其中一种。在此步骤中,您将通过显示自行车(而非默认的红色标记)来表示地图上的各家自行车商店。入门级项目中包含了自行车图标 ic_directions_bike_black_24dp.xml(在 app/src/res/drawable 中)以供您使用。

6eb7358bb61b0a88.png

针对标记设置自定义位图

在获得矢量可绘制对象(即自行车图标)后,下一步就是在地图上将该可绘制对象设置为各个标记的图标。MarkerOptions 包含一个 icon 方法,该方法采用的 BitmapDescriptor 可用来完成此操作。

首先,您需要将刚刚添加的矢量可绘制对象转换为 BitmapDescriptor。在入门级项目中,一个名为 BitMapHelper 的文件中包含一个名为 vectorToBitmap() 辅助函数,该函数正好可以执行此操作。

BitmapHelper

package com.google.codelabs.buildyourfirstmap

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory

object BitmapHelper {
   /**
    * Demonstrates converting a [Drawable] to a [BitmapDescriptor],
    * for use as a marker icon. Taken from ApiDemos on GitHub:
    * https://github.com/googlemaps/android-samples/blob/main/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MarkerDemoActivity.kt
    */
   fun vectorToBitmap(
      context: Context,
      @DrawableRes id: Int,
      @ColorInt color: Int
   ): BitmapDescriptor {
       val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, null)
       if (vectorDrawable == null) {
           Log.e("BitmapHelper", "Resource not found")
           return BitmapDescriptorFactory.defaultMarker()
       }
       val bitmap = Bitmap.createBitmap(
           vectorDrawable.intrinsicWidth,
           vectorDrawable.intrinsicHeight,
           Bitmap.Config.ARGB_8888
       )
       val canvas = Canvas(bitmap)
       vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
       DrawableCompat.setTint(vectorDrawable, color)
       vectorDrawable.draw(canvas)
       return BitmapDescriptorFactory.fromBitmap(bitmap)
   }
}

此方法采用一个 Context、一个可绘制资源 ID 和一个颜色整数,并会创建 BitmapDescriptor 的表示法。

使用辅助方法,声明一个名为 bicycleIcon 的新属性并为其指定以下定义:MainActivity.bicycleIcon

private val bicycleIcon: BitmapDescriptor by lazy {
   val color = ContextCompat.getColor(this, R.color.colorPrimary)
   BitmapHelper.vectorToBitmap(this, R.drawable.ic_directions_bike_black_24dp, color)
}

此属性使用的是您应用中预定义的颜色 colorPrimary,它会使用该颜色调整自行车图标的色调并将其作为 BitmapDescriptor 返回。

  1. 借助此属性,您可以在 addMarkers() 方法中调用 MarkerOptionsicon 方法,以完成图标自定义。完成此操作后,标记属性应如下所示:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. 运行应用,看看更新后的标记!

8. 聚类标记

您可能已经注意到自己添加的标记出现重叠,具体取决于地图放大程度。如果标记出现重叠,不仅很难进行互动,还会造成许多干扰,从而影响应用易用性。

68591edc86d73724.png

为了改善这方面的用户体验,每当您有大型数据集密集地聚集在一起时,最好是实现标记聚类。实现聚类后,当您放大和缩小地图时,距离较近的标记就会被聚类,如下所示:

f05e1ca27ff42bf6.png

为了实现这一点,您需要借助 Maps SDK for Android 实用程序库

Maps SDK for Android 实用程序库

创建 Maps SDK for Android 实用程序库是为了扩展 Maps SDK for Android 的功能。该库会提供一些高级功能,例如标记聚类、热图、KML 和 GeoJson 支持、多段线编码和解码,以及一些与球面几何图形相关的辅助函数。

更新您的 build.gradle

由于该实用程序库是独立于 Maps SDK for Android 打包的,因此您需要向 build.gradle 文件添加额外的依赖项。

  1. 接下来,更新 app/build.gradle 文件的 dependencies 部分。

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. 添加完上面这行代码后,您必须执行项目同步以提取新的依赖项。

b7b030ec82c007fd.png

实现聚类

如需在您的应用中实现聚类,请按以下三个步骤操作:

  1. 实现 ClusterItem 接口。
  2. 创建 DefaultClusterRenderer 类的子类。
  3. 创建 ClusterManager 并添加相应项。

实现 ClusterItem 接口

用于表示地图上可聚类标记的所有对象都需要实现 ClusterItem 接口。在本示例中,这意味着 Place 模式需要与 ClusterItem 吻合。接下来,打开 Place.kt 文件并对其进行以下修改:

Place

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: String,
   val rating: Float
) : ClusterItem {
   override fun getPosition(): LatLng =
       latLng

   override fun getTitle(): String =
       name

   override fun getSnippet(): String =
       address
}

ClusterItem 定义了以下三个方法:

  • getPosition():表示地点的 LatLng
  • getTitle():表示地点的名称
  • getSnippet():表示地点的地址。

创建 DefaultClusterRenderer 类的子类

ClusterManager 类负责实现聚类,会在您平移或缩放地图时在内部使用 ClusterRenderer 类处理聚类的创建。默认情况下,该类附带有可实现 ClusterRenderer 的默认渲染程序 DefaultClusterRenderer。对于简单的情况,这应该已经足够了。但在本示例中,您需要自定义标记,因此需要对此类进行扩展并在其中添加自定义设置。

接下来,在软件包 com.google.codelabs.buildyourfirstmap.place 中创建 Kotlin 文件 PlaceRenderer.kt,并按如下方式定义:

PlaceRenderer

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.codelabs.buildyourfirstmap.BitmapHelper
import com.google.codelabs.buildyourfirstmap.R
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer

/**
* A custom cluster renderer for Place objects.
*/
class PlaceRenderer(
   private val context: Context,
   map: GoogleMap,
   clusterManager: ClusterManager<Place>
) : DefaultClusterRenderer<Place>(context, map, clusterManager) {

   /**
    * The icon to use for each cluster item
    */
   private val bicycleIcon: BitmapDescriptor by lazy {
       val color = ContextCompat.getColor(context,
           R.color.colorPrimary
       )
       BitmapHelper.vectorToBitmap(
           context,
           R.drawable.ic_directions_bike_black_24dp,
           color
       )
   }

   /**
    * Method called before the cluster item (the marker) is rendered.
    * This is where marker options should be set.
    */
   override fun onBeforeClusterItemRendered(
      item: Place,
      markerOptions: MarkerOptions
   ) {
       markerOptions.title(item.name)
           .position(item.latLng)
           .icon(bicycleIcon)
   }

   /**
    * Method called right after the cluster item (the marker) is rendered.
    * This is where properties for the Marker object should be set.
    */
   override fun onClusterItemRendered(clusterItem: Place, marker: Marker) {
       marker.tag = clusterItem
   }
}

此类会替换以下两个函数:

  • onBeforeClusterItemRendered():在地图上渲染聚类之前调用。在这里,您可以通过 MarkerOptions 提供自定义设置;在本示例中,可用于设置标记的名称、位置和图标。
  • onClusterItemRenderer():在地图上渲染标记后立即调用。您可以在此访问创建的 Marker 对象;在本示例中,可用于设置标记的代码属性。

创建 ClusterManager 并添加相应项

最后,为了让聚类正常运行,您需要修改 MainActivity 以实例化 ClusterManager,并向其提供必要的依赖项。ClusterManager 负责在内部添加标记(ClusterItem 对象),因此,此责任将委托给 ClusterManager,而不是直接在地图上添加标记。此外,ClusterManager 还会在内部调用 setInfoWindowAdapter(),因此必须对 ClusterMangerMarkerManager.Collection 对象设置自定义信息窗口。

  1. 首先,在 MainActivity.onCreate() 中修改 getMapAsync() 调用中 lambda 的内容。接下来,注释掉对 addMarkers()setInfoWindowAdapter() 的调用,改为调用 addClusteredMarkers() 方法(您将在下文中对其进行定义)。

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
    //addMarkers(googleMap)
    addClusteredMarkers(googleMap)

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. 接下来,在 MainActivity 中定义 addClusteredMarkers()

MainActivity.addClusteredMarkers()

/**
* Adds markers to the map with clustering support.
*/
private fun addClusteredMarkers(googleMap: GoogleMap) {
   // Create the ClusterManager class and set the custom renderer.
   val clusterManager = ClusterManager<Place>(this, googleMap)
   clusterManager.renderer =
       PlaceRenderer(
           this,
           googleMap,
           clusterManager
       )

   // Set custom info window adapter
   clusterManager.markerCollection.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

   // Add the places to the ClusterManager.
   clusterManager.addItems(places)
   clusterManager.cluster()

   // Set ClusterManager as the OnCameraIdleListener so that it
   // can re-cluster when zooming in and out.
   googleMap.setOnCameraIdleListener {
       clusterManager.onCameraIdle()
   }
}

此方法会实例化 ClusterManager、向其传递自定义渲染程序 PlacesRenderer、添加所有地点,以及调用 cluster() 方法。此外,由于 ClusterManager 对地图对象使用 setInfoWindowAdapter() 方法,因此必须对 ClusterManager.markerCollection 对象设置自定义信息窗口。最后,由于您想让聚类随着用户平移和缩放地图而发生变化,因此可向 googleMap 提供 OnCameraIdleListener,以便在相机进入空闲状态时调用 clusterManager.onCameraIdle()

  1. 接下来运行应用,看看已划分聚类的新商店!

9. 在地图上绘制

虽然您已经探索了一种在地图上绘制的方法(通过添加标记),但 Maps SDK for Android 还支持许多其他绘制方法,可帮助您在地图上显示实用信息。

例如,如果您想要在地图上表示路线和区域,可以使用多段线和多边形在地图上显示这些内容。或者,如果您想要将图像固定到地面,可以使用地面叠加层

在此任务中,您将学习如何绘制形状(尤其是圆形),这些形状会在每次点按标记时显示在其周围。

f98ce13055430352.png

添加点击监听器

如需向标记添加点击监听器,通常的做法是通过 setOnMarkerClickListener() 直接在 GoogleMap 对象上传入点击监听器。不过,由于您正在使用聚类,因此需要改为向 ClusterManager 提供点击监听器。

  1. MainActivityaddClusteredMarkers() 方法中,在调用 cluster() 之后立即添加下面这行代码。

MainActivity.addClusteredMarkers()

// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
   addCircle(googleMap, item)
   return@setOnClusterItemClickListener false
}

此方法会添加一个监听器并调用 addCircle() 方法(您将在下文中对其进行定义)。最后,此方法会返回 false,以表明它尚未使用此事件。

  1. 接下来,您需要在 MainActivity 中定义 circle 属性和 addCircle() 方法。

MainActivity.addCircle()

private var circle: Circle? = null

/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
   circle?.remove()
   circle = googleMap.addCircle(
       CircleOptions()
           .center(item.latLng)
           .radius(1000.0)
           .fillColor(ContextCompat.getColor(this, R.color.colorPrimaryTranslucent))
           .strokeColor(ContextCompat.getColor(this, R.color.colorPrimary))
   )
}

circle 属性已设置,这样一来,每当点按新标记时,系统都会移除之前的圆形,然后添加一个新的圆形。请注意,用于添加圆形的 API 与用于添加标记的 API 非常相似。

  1. 现在运行应用,看看您所做的更改。

10. 相机控制

最后一项任务是了解一些相机控制,从而以特定区域为中心聚焦视图。

相机和视图

如果在运行应用时,您发现相机显示的是非洲大陆,那么您必须通过平移和缩放费力地转到旧金山,才能找到您添加的标记。虽然这是探索世界的一种有趣方式,但如果您想要立即显示标记,这种方式就毫无用处了。

为帮助解决此问题,您可以编程方式设置相机位置,让视图以您希望的位置为中心。

  1. 接下来,将以下代码添加到 getMapAsync() 调用以调整相机视图,以便视图在应用启动时初始化为旧金山。

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
   // Ensure all places are visible in the map.
   googleMap.setOnMapLoadedCallback {
       val bounds = LatLngBounds.builder()
       places.forEach { bounds.include(it.latLng) }
       googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
   }
}

首先,调用 setOnMapLoadedCallback() 以便仅在地图加载后执行相机更新。此步骤是必要的,因为需要先计算维度等地图属性,然后才能进行相机更新调用。

在 lambda 中,系统构建了一个新的 LatLngBounds 对象,用来定义地图上的矩形区域。此对象是通过在其中添加地点的所有 LatLng 值逐步构建的,可确保所有地点都位于边界内。构建此对象后,系统会调用 GoogleMap 上的 moveCamera() 方法,并通过 CameraUpdateFactory.newLatLngBounds(bounds.build(), 20) 向其提供 CameraUpdate

  1. 运行应用,然后您会发现相机现已初始化为显示旧金山。

监听相机更改

除了修改相机位置之外,您还可以在用户四处移动地图时,监听相机更新。如果您想要在相机四处移动时修改界面,此功能会非常有用。

您也可以增添一点乐趣:修改代码,使标记在每次相机移动时呈现半透明状态。

  1. 接下来,在 addClusteredMarkers() 方法底部添加下面几行代码:

MainActivity.addClusteredMarkers()

// When the camera starts moving, change the alpha value of the marker to translucent.
googleMap.setOnCameraMoveStartedListener {
   clusterManager.markerCollection.markers.forEach { it.alpha = 0.3f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 0.3f }
}

此操作会添加一个 OnCameraMoveStartedListener,这样一来,每当相机开始移动时,所有标记(包括聚类和标记)的 alpha 值都会被修改为 0.3f,以便标记以半透明状态显示。

  1. 最后,如需在相机停止后将标记从半透明状态修改为不透明状态,请修改 addClusteredMarkers() 方法中 setOnCameraIdleListener 的内容,如下所示:

MainActivity.addClusteredMarkers()

googleMap.setOnCameraIdleListener {
   // When the camera stops moving, change the alpha value back to opaque.
   clusterManager.markerCollection.markers.forEach { it.alpha = 1.0f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 1.0f }

   // Call clusterManager.onCameraIdle() when the camera stops moving so that reclustering
   // can be performed when the camera stops moving.
   clusterManager.onCameraIdle()
}
  1. 接下来运行应用,看看成效!

11. Maps KTX

对于使用一个或多个 Google Maps Platform Android SDK 的 Kotlin 应用,您可以利用 Kotlin 扩展或 KTX 库来充分利用 Kotlin 语言功能,例如协程、扩展属性/函数等。每个 Google 地图 SDK 都有一个对应的 KTX 库,如下所示:

Google Maps Platform KTX 示意图

在此任务中,您将使用 Maps KTX 和 Maps Utils KTX 库来运行应用并重构之前的任务和实现,以便在应用中使用 Kotlin 特定的语言功能。

  1. 在应用级 build.gradle 文件中添加 KTX 依赖项

由于该应用同时使用 Maps SDK for Android 和 Maps SDK for Android 实用程序库,因此您需要包含这些库的相应 KTX 库。您还将在此任务中使用 AndroidX Lifecycle KTX 库中提供的一项功能,因此请在应用级 build.gradle 文件中也添加该依赖项。

build.gradle

dependencies {
    // ...

    // Maps SDK for Android KTX Library
    implementation 'com.google.maps.android:maps-ktx:3.0.0'

    // Maps SDK for Android Utility Library KTX Library
    implementation 'com.google.maps.android:maps-utils-ktx:3.0.0'

    // Lifecycle Runtime KTX Library
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}
  1. 使用 GoogleMap.addMarker() 和 GoogleMap.addCircle() 扩展函数

Maps KTX 库为之前的步骤中使用的 GoogleMap.addMarker(MarkerOptions)GoogleMap.addCircle(CircleOptions) 提供了 DSL 样式的 API 替代方案。要使用上述 API,需要构造包含标记或圆形选项的类,而借助 KTX 替代项,您可以在提供的 lambda 中设置标记或圆形选项。

如需使用这些 API,请更新 MainActivity.addMarkers(GoogleMap)MainActivity.addCircle(GoogleMap) 方法:

MainActivity.addMarkers(GoogleMap)

/**
 * Adds markers to the map. These markers won't be clustered.
 */
private fun addMarkers(googleMap: GoogleMap) {
    places.forEach { place ->
        val marker = googleMap.addMarker {
            title(place.name)
            position(place.latLng)
            icon(bicycleIcon)
        }
        // Set place as the tag on the marker object so it can be referenced within
        // MarkerInfoWindowAdapter
        marker.tag = place
    }
}

MainActivity.addCircle(GoogleMap)

/**
 * Adds a [Circle] around the provided [item]
 */
private fun addCircle(googleMap: GoogleMap, item: Place) {
    circle?.remove()
    circle = googleMap.addCircle {
        center(item.latLng)
        radius(1000.0)
        fillColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryTranslucent))
        strokeColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
    }
}

以这种方式重写上述方法要精简得多,因为这是使用 Kotlin 的接收器函数字面量来实现的。

  1. 使用 SupportMapFragment.awaitMap() 和 GoogleMap.awaitMapLoad() 扩展挂起函数

Maps KTX 库还提供了要在协程内使用的挂起函数扩展。具体来说,SupportMapFragment.getMapAsync(OnMapReadyCallback)GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback) 存在挂起函数替代方案。使用这些替代 API 后,就不再需要传递回调,而是允许您以串行和同步方式接收这些方法的响应。

由于这些方法是挂起函数,因此它们需要在协程中使用。Lifecycle Runtime KTX 库提供了一个扩展程序,可以提供生命周期感知型协程作用域,以便在适当的生命周期事件中运行和停止协程。

结合以下概念,更新 MainActivity.onCreate(Bundle) 方法:

MainActivity.onCreate(软件包)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val mapFragment =
        supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment
    lifecycleScope.launchWhenCreated {
        // Get map
        val googleMap = mapFragment.awaitMap()

        // Wait for map to finish loading
        googleMap.awaitMapLoad()

        // Ensure all places are visible in the map
        val bounds = LatLngBounds.builder()
        places.forEach { bounds.include(it.latLng) }
        googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))

        addClusteredMarkers(googleMap)
    }
}

当 activity 至少处于已创建状态时,lifecycleScope.launchWhenCreated 协程作用域将会执行代码块。另请注意,用于检索 GoogleMap 对象的调用以及用于等待地图加载完成的调用已分别替换为 SupportMapFragment.awaitMap()GoogleMap.awaitMapLoad()。通过使用这些挂起函数重构代码,您可以依序编写等效的基于回调的代码。

  1. 接下来,使用重构后的应用重新构建应用!

12. 恭喜

恭喜!您已经掌握了许多内容,希望您对 Maps SDK for Android 中提供的核心功能有了进一步的了解。

了解详情