1. 准备工作
此 Codelab 介绍了如何通过构建一个可显示美国加利福尼亚州旧金山市自行车商店地图的应用,将 Maps SDK for Android 与您的应用集成以及使用其核心功能。
前提条件
- 具备 Kotlin 和 Android 开发方面的基础知识
您应执行的操作
- 启用并使用 Maps SDK for Android,以将 Google 地图添加到 Android 应用中。
- 添加和自定义标记,以及为标记划分聚类。
- 在地图上绘制多段线和多边形。
- 以编程方式控制相机视点。
您需要满足的条件
- Maps SDK for Android
- 启用了结算功能的 Google 帐号
- Android Studio 2020.3.1 或更高版本
- 已安装在 Android Studio 中的 Google Play 服务
- Android 设备或 Android 模拟器,搭载的是基于 Android 4.2.2 或更高版本的 Google API 平台(如需了解具体安装步骤,请参阅在 Android 模拟器上运行应用。)
2. 进行设置
对于以下启用步骤,您需要启用 Maps SDK for Android。
设置 Google Maps Platform
如果您还没有已启用结算功能的 Google Cloud Platform 帐号和项目,请参阅 Google Maps Platform 使用入门指南,创建结算帐号和项目。
- 在 Cloud Console 中,点击项目下拉菜单,选择要用于此 Codelab 的项目。
3.快速上手
为帮助您尽快入门,我们在下面提供了一些起始代码,帮助您顺利完成此 Codelab。您可以跳到解决方案部分,但如果您想要按照所有步骤自行构建,请继续阅读。
- 克隆代码库(如果您已安装
git
)。
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
或者,您也可以点击下面的按钮,下载源代码。
- 获取代码后,在 Android Studio 中打开
starter
目录中的相应项目。
4.添加 Google 地图
在本部分中,您将添加 Google 地图,以便它在您启动应用时进行加载。
添加您的 API 密钥
您需要将在之前的步骤中创建的 API 密钥提供给应用,以便 Maps SDK for Android 将您的密钥与应用相关联。
- 如需提供此密钥,请打开项目的根目录中名为
local.properties
的文件(与gradle.properties
和settings.gradle
所在级别相同)。 - 在该文件中,定义一个新密钥
GOOGLE_MAPS_API_KEY
,其值为您创建的 API 密钥。
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
请注意,local.properties
已列在 Git 代码库中的 .gitignore
文件内。这是因为您的 API 密钥被视为敏感信息,不得签入源代码控制(如果可能的话)。
- 接下来,公开您的 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 地图依赖项
- 现在,您的 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'
}
- 接下来,在
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}" />
- 然后,在
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>
此布局具有一个包含 SupportMapFragment
的 FrameLayout
。此 fragment 包含您要在后续步骤中使用的底层 GoogleMaps
对象。
- 最后,更新位于
app/src/main/java/com/google/codelabs/buildyourfirstmap
的MainActivity
类,具体做法是,添加下面的代码以替换onCreate
方法,这样您就可以使用刚刚创建的新布局设置其内容。
MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- 接下来运行应用,您应该会看到地图在您的设备屏幕上加载。
5. 云端地图样式设置(可选)
您可以使用云端地图样式设置自定义地图样式。
创建地图 ID
如果您尚未使用关联的地图样式创建地图 ID,请参阅地图 ID 指南完成以下步骤:
- 创建地图 ID。
- 将地图 ID 与地图样式相关联。
为您的应用添加地图 ID
如需使用您创建的地图 ID,请修改 activity_main.xml
文件并在 SupportMapFragment
的 map: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. 添加标记
在此任务中,您要向地图添加标记,以表示要在地图上突出显示的地图注点。首先,检索入门级项目中为您提供的地点列表,然后将这些地点添加到地图中。在此示例中,这些地点为自行车商店。
获取对 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
)
向地图添加标记
现在,地点列表已加载到内存中,下一步就是在地图上表示这些地点。
- 在
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
对象创建的,以便您自定义标记本身。在此示例中,标记的名称和位置均已提供,分别表示自行车商店的名称及其坐标。
- 接下来运行应用,在地图上转到旧金山,看看您刚刚添加的标记!
7. 自定义标记
下面介绍了一些自定义选项,可帮助您刚刚添加的标记脱颖而出,同时向用户传达实用的信息。在此任务中,您将通过自定义每个标记的图像以及点按标记时显示的信息窗口,探索其中一部分自定义选项。
添加信息窗口
默认情况下,当您点按标记时,信息窗口会显示其名称和摘要(如果已设置)。您可以自定义此窗口,使其能够显示更多信息(例如,地点的地址和评分)。
创建 marker_info_contents.xml
首先,创建一个名为 marker_info_contents.xml
的新布局文件。
- 为此,请在 Android Studio 的项目视图中右键点击
app/src/main/res/layout
文件夹,然后依次选择 New > Layout Resource File。
- 在对话框中,在 File name 字段中输入
marker_info_contents
,并在Root element
字段中输入LinearLayout
,然后点击 OK。
此布局文件随后会进行扩充,用于表示信息窗口中的内容。
- 复制以下代码段中的内容(可在垂直的
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,以表明应使用默认窗口。
- 在
MainActivity
所在的同一软件包中创建一个名为MarkerInfoWindowAdapter
的新 Kotlin 文件,具体做法是,在 Android Studio 的项目视图中右键点击app/src/main/java/com/google/codelabs/buildyourfirstmap
文件夹,然后依次选择 New > Kotlin File/Class。
- 在对话框中,输入
MarkerInfoWindowAdapter
,并突出显示 File。
- 创建文件后,请将以下代码段中的内容复制到您的新文件中。
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
方法调用中传递自定义 InfoWindowAdapter
和 MarkerInfoWindowAdapter
,请针对 GoogleMap
对象调用 setInfoWindowAdapter()
方法,并创建一个新的 MarkerInfoWindowAdapter
实例。
- 为此,请在
addMarkers()
方法调用结束后在getMapAsync()
lambda 内添加以下代码。
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
最后,您需要将每个 Place 设置为已添加到地图中的每个 Marker 的代码属性。
- 为此,请使用以下代码在
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
中)以供您使用。
针对标记设置自定义位图
在获得矢量可绘制对象(即自行车图标)后,下一步就是在地图上将该可绘制对象设置为各个标记的图标。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
返回。
- 借助此属性,您可以在
addMarkers()
方法中调用MarkerOptions
的icon
方法,以完成图标自定义。完成此操作后,标记属性应如下所示:
MainActivity.addMarkers()
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- 运行应用,看看更新后的标记!
8. 聚类标记
您可能已经注意到自己添加的标记出现重叠,具体取决于地图放大程度。如果标记出现重叠,不仅很难进行互动,还会造成许多干扰,从而影响应用易用性。
为了改善这方面的用户体验,每当您有大型数据集密集地聚集在一起时,最好是实现标记聚类。实现聚类后,当您放大和缩小地图时,距离较近的标记就会被聚类,如下所示:
为了实现这一点,您需要借助 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
文件添加额外的依赖项。
- 接下来,更新
app/build.gradle
文件的dependencies
部分。
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- 添加完上面这行代码后,您必须执行项目同步以提取新的依赖项。
实现聚类
如需在您的应用中实现聚类,请按以下三个步骤操作:
- 实现
ClusterItem
接口。 - 创建
DefaultClusterRenderer
类的子类。 - 创建
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()
,因此必须对 ClusterManger
的 MarkerManager.Collection
对象设置自定义信息窗口。
- 首先,在
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))
}
- 接下来,在
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()
。
- 接下来运行应用,看看已划分聚类的新商店!
9. 在地图上绘制
虽然您已经探索了一种在地图上绘制的方法(通过添加标记),但 Maps SDK for Android 还支持许多其他绘制方法,可帮助您在地图上显示实用信息。
例如,如果您想要在地图上表示路线和区域,可以使用多段线和多边形在地图上显示这些内容。或者,如果您想要将图像固定到地面,可以使用地面叠加层。
在此任务中,您将学习如何绘制形状(尤其是圆形),这些形状会在每次点按标记时显示在其周围。
添加点击监听器
如需向标记添加点击监听器,通常的做法是通过 setOnMarkerClickListener()
直接在 GoogleMap
对象上传入点击监听器。不过,由于您正在使用聚类,因此需要改为向 ClusterManager
提供点击监听器。
- 在
MainActivity
的addClusteredMarkers()
方法中,在调用cluster()
之后立即添加下面这行代码。
MainActivity.addClusteredMarkers()
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
此方法会添加一个监听器并调用 addCircle()
方法(您将在下文中对其进行定义)。最后,此方法会返回 false
,以表明它尚未使用此事件。
- 接下来,您需要在
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 非常相似。
- 现在运行应用,看看您所做的更改。
10. 相机控制
最后一项任务是了解一些相机控制,从而以特定区域为中心聚焦视图。
相机和视图
如果在运行应用时,您发现相机显示的是非洲大陆,那么您必须通过平移和缩放费力地转到旧金山,才能找到您添加的标记。虽然这是探索世界的一种有趣方式,但如果您想要立即显示标记,这种方式就毫无用处了。
为帮助解决此问题,您可以编程方式设置相机位置,让视图以您希望的位置为中心。
- 接下来,将以下代码添加到
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
。
- 运行应用,然后您会发现相机现已初始化为显示旧金山。
监听相机更改
除了修改相机位置之外,您还可以在用户四处移动地图时,监听相机更新。如果您想要在相机四处移动时修改界面,此功能会非常有用。
您也可以增添一点乐趣:修改代码,使标记在每次相机移动时呈现半透明状态。
- 接下来,在
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
,以便标记以半透明状态显示。
- 最后,如需在相机停止后将标记从半透明状态修改为不透明状态,请修改
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()
}
- 接下来运行应用,看看成效!
11. Maps KTX
对于使用一个或多个 Google Maps Platform Android SDK 的 Kotlin 应用,您可以利用 Kotlin 扩展或 KTX 库来充分利用 Kotlin 语言功能,例如协程、扩展属性/函数等。每个 Google 地图 SDK 都有一个对应的 KTX 库,如下所示:
在此任务中,您将使用 Maps KTX 和 Maps Utils KTX 库来运行应用并重构之前的任务和实现,以便在应用中使用 Kotlin 特定的语言功能。
- 在应用级 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'
}
- 使用 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 的接收器函数字面量来实现的。
- 使用 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()
。通过使用这些挂起函数重构代码,您可以依序编写等效的基于回调的代码。
- 接下来,使用重构后的应用重新构建应用!
12. 恭喜
恭喜!您已经掌握了许多内容,希望您对 Maps SDK for Android 中提供的核心功能有了进一步的了解。
了解详情
- Places SDK for Android - 探索丰富的地点数据,发掘您附近的商家。
- android-maps-ktx - 一个开源库,可让您以支持 Kotlin 的方式与 Maps SDK for Android 和 Maps SDK for Android 实用程序库集成。
- android-place-ktx - 一个开源库,可让您以支持 Kotlin 的方式与 Places SDK for Android 集成。
- android-samples - GitHub 上的示例代码,演示了此 Codelab 及更多 Codelab 中介绍的所有功能。
- 更多使用 Kotlin 构建 Android 应用的 Kotlin Codelab