1. 准备工作
此 Codelab 介绍了如何通过构���一个可使用各种类型的标记显示美国科罗拉多州山脉地图的应用,将 Maps SDK for Android 与您的应用集成以及使用其核心功能。此外,您还将学习如何在地图上绘制其他形状。
以下是完成此 Codelab 后,您构建的应用的样子。
前提条件
- 具备 Kotlin、Jetpack Compose 和 Android 开发方面的基础知识
您将执行的操作
- 启用并使用 Maps Compose 库(适用于 Maps SDK for Android),以将
GoogleMap
添加到 Android 应用中 - 添加和自定义标记
- 在地图上绘制多边形
- 以编程方式控制相机视点
所需条件
- Maps SDK for Android
- 启用了结算功能的 Google 账号
- 最新稳定版 Android Studio
- Android 设备或 Android 模拟器,搭载的是基于 Android 5.0 或更高版本的 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/googlemaps-samples/codelab-maps-platform-101-compose.git
或者,您也可以点击下面的按钮,下载源代码。
- 获取代码后,在 Android Studio 中打开
starter
目录中的相应项目。
4. 将您的 API 密钥添加到项目中
本部分介绍了如何存储 API 密钥,以便您的应用可以安全引用相应密钥。您不应将 API 密钥签入版本控制系统,因此建议您将其存储在 secrets.properties
文件中,该文件将放置在项目根目录的本地副本中。如需详细了解 secrets.properties
文件,请参阅 Gradle 属性文件。
为了简化此任务,建议您使用 Android 版 Secrets Gradle 插件。
如需在 Google 地图项目中安装 Android 版 Secret Gradle 插件,请执行以下操作:
- 在 Android Studio 中,打开顶级
build.gradle.kts
文件,并将以下代码添加到buildscript
下的dependencies
元素中。buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- 打开模块级
build.gradle.kts
文件,并将以下代码添加到plugins
元素中。plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- 在模块级
build.gradle.kts
文件中,请务必将targetSdk
和compileSdk
设置为至少 34。 - 保存文件并将项目与 Gradle 同步。
- 在顶级目录中打开
secrets.properties
文件,然后添加以下代码。将YOUR_API_KEY
替换为您的 API 密钥。secrets.properties
不会签入版本控制系统,因此请将您的密钥存储在此文件中。MAPS_API_KEY=YOUR_API_KEY
- 保存文件。
- 在顶级目录(即
secrets.properties
文件所在的文件夹)中创建local.defaults.properties
文件,然后添加以下代码。 此文件的作用是为 API 密钥提供备用位置,以免在找不到MAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
文件的情况下构建失败。如果您是从版本控制系统中克隆应用,而您还没有在本地创建secrets.properties
文件来提供 API 密钥,就会出现这种情况。 - 保存文件。
- 在
AndroidManifest.xml
文件中,定位到com.google.android.geo.API_KEY
并更新android:value
属性。如果<meta-data>
标记不存在,请创建该标记作为<application>
标记的子标记。<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- 在 Android Studio 中,打开模块级
build.gradle.kts
文件并修改secrets
属性。如果secrets
属性不存在,请添加该属性。修改插件的属性,将propertiesFileName
设置为secrets.properties
,将defaultPropertiesFileName
设置为local.defaults.properties
,并设置任何其他属性。secrets { // Optionally specify a different file name containing your secrets. // The plugin defaults to "local.properties" propertiesFileName = "secrets.properties" // A properties file containing default secret values. This file can be // checked in version control. defaultPropertiesFileName = "local.defaults.properties" }
5. 添加 Google 地图
在本部分中,您将添加 Google 地图,以便它在您启动应用时进行加载。
添加 Maps Compose 依赖项
现在,您的 API 密钥可在应用内进行访问,下一步就是将 Maps SDK for Android 依赖项添加到您应用的 build.gradle.kts
文件中。如需使用 Jetpack Compose 进行构建,请使用 Maps Compose 库,该库以可组合函数和数据类型的形式提供 Maps SDK for Android 的元素。
build.gradle.kts
在应用级 build.gradle.kts
文件中,替换非 Compose Maps SDK for Android 依赖项:
dependencies {
// ...
// Google Maps SDK -- these are here for the data model. Remove these dependencies and replace
// with the compose versions.
implementation("com.google.android.gms:play-services-maps:18.2.0")
// KTX for the Maps SDK for Android library
implementation("com.google.maps.android:maps-ktx:5.0.0")
// KTX for the Maps SDK for Android Utility Library
implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
}
与可组合的对应项:
dependencies {
// ...
// Google Maps Compose library
val mapsComposeVersion = "4.4.1"
implementation("com.google.maps.android:maps-compose:$mapsComposeVersion")
// Google Maps Compose utility library
implementation("com.google.maps.android:maps-compose-utils:$mapsComposeVersion")
// Google Maps Compose widgets library
implementation("com.google.maps.android:maps-compose-widgets:$mapsComposeVersion")
}
添加 Google 地图可组合项
在 MountainMap.kt
中,将 GoogleMap
可组合项添加到嵌套在 MapMountain
可组合项内的 Box
可组合项中。
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...
@Composable
fun MountainMap(
paddingValues: PaddingValues,
viewState: MountainsScreenViewState.MountainList,
eventFlow: Flow<MountainsScreenEvent>,
selectedMarkerType: MarkerType,
) {
var isMapLoaded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Add GoogleMap here
GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapLoaded = { isMapLoaded = true }
)
// ...
}
}
现在,构建并运行应用。瞧!您应该会看到一张以臭名昭著的空虚岛(也称为纬度零度和经度零度)为中心的地图。稍后,您将学习如何将地图定位到所需的位置和缩放级别,但现在,请��庆祝您的首次成功!
6. 云端地图样式设置
您可以使用云端地图样式设置自定义地图样式。
创建地图 ID
如果您尚未使用关联的地图样式创建地图 ID,请参阅地图 ID 指南完成以下步骤:
- 创建地图 ID。
- 将地图 ID 与地图样式相关联。
为您的应用添加地图 ID
若要使用您创建的地图 ID,请在实例化 GoogleMap
可组合项时,在创建分配给构造函数中 googleMapOptionsFactory
参数的 GoogleMapOptions
对象时使用该地图 ID。
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
完成上述操作后,继续并运行应用,以您选择的样式查看您的地图!
7. 加载标记数据
应用的主要任务是从本地存储空间加载山脉集合,并在 GoogleMap
中显示这些山脉。在此步骤中,您将了解所提供的用于加载山脉数据并将其呈现给界面的基础架构。
山地
Mountain
数据类包含每座山的所有数据。
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
请注意,稍后将根据山脉的海拔高度对其进行分区。海拔至少为 14,000 英尺的山峰称为 fourteener。起始代码包含一个扩展函数,可为您执行此检查。
/**
* Extension function to determine whether a mountain is a "14er", i.e., has an elevation greater
* than 14,000 feet (~4267 meters).
*/
fun Mountain.is14er() = elevation >= 14_000.feet
MountainsScreenViewState
MountainsScreenViewState
类包含呈现视图所需的所有数据。它可以处于 Loading
或 MountainList
状态,具体取决于山脉列表是否已完成加载。
/**
* Sealed class representing the state of the mountain map view.
*/
sealed class MountainsScreenViewState {
data object Loading : MountainsScreenViewState()
data class MountainList(
// List of the mountains to display
val mountains: List<Mountain>,
// Bounding box that contains all of the mountains
val boundingBox: LatLngBounds,
// Switch indicating whether all the mountains or just the 14ers
val showingAllPeaks: Boolean = false,
) : MountainsScreenViewState()
}
提供的类:MountainsRepository
和 MountainsViewModel
入门级项目中已为您提供 MountainsRepository
类。此类会读取存储在 GPS Exchange Format
(即 GPX 文件)top_peaks.gpx
中的山脉地点列表。调用 mountainsRepository.loadMountains()
会返回 StateFlow<List<Mountain>>
。
MountainsRepository
class MountainsRepository(@ApplicationContext val context: Context) {
private val _mountains = MutableStateFlow(emptyList<Mountain>())
val mountains: StateFlow<List<Mountain>> = _mountains
private var loaded = false
/**
* Loads the list of mountains from the list of mountains from the raw resource.
*/
suspend fun loadMountains(): StateFlow<List<Mountain>> {
if (!loaded) {
loaded = true
_mountains.value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.top_peaks).use { inputStream ->
readMountains(inputStream)
}
}
}
return mountains
}
/**
* Reads the [Waypoint]s from the given [inputStream] and returns a list of [Mountain]s.
*/
private fun readMountains(inputStream: InputStream) =
readWaypoints(inputStream).mapIndexed { index, waypoint ->
waypoint.toMountain(index)
}.toList()
// ...
}
MountainsViewModel
MountainsViewModel
是一个 ViewModel
类,用于加载山脉集合,并通过 mountainsScreenViewState
公开该集合以及界面状态的其他部分。mountainsScreenViewState
是一个热 StateFlow
,界面可以使用 collectAsState
扩展函数将其作为可变状态进行观测。
遵循合理的架构原则,MountainsViewModel
会保存应用的所有状态。界面使用 onEvent
方法将用户互动发送到视图模型。
@HiltViewModel
class MountainsViewModel
@Inject
constructor(
mountainsRepository: MountainsRepository
) : ViewModel() {
private val _eventChannel = Channel<MountainsScreenEvent>()
// Event channel to send events to the UI
internal fun getEventChannel() = _eventChannel.receiveAsFlow()
// Whether or not to show all of the high peaks
private var showAllMountains = MutableStateFlow(false)
val mountainsScreenViewState =
mountainsRepository.mountains.combine(showAllMountains) { allMountains, showAllMountains ->
if (allMountains.isEmpty()) {
MountainsScreenViewState.Loading
} else {
val filteredMountains =
if (showAllMountains) allMountains else allMountains.filter { it.is14er() }
val boundingBox = filteredMountains.map { it.location }.toLatLngBounds()
MountainsScreenViewState.MountainList(
mountains = filteredMountains,
boundingBox = boundingBox,
showingAllPeaks = showAllMountains,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MountainsScreenViewState.Loading
)
init {
// Load the full set of mountains
viewModelScope.launch {
mountainsRepository.loadMountains()
}
}
// Handle user events
fun onEvent(event: MountainsViewModelEvent) {
when (event) {
OnZoomAll -> onZoomAll()
OnToggleAllPeaks -> toggleAllPeaks()
}
}
private fun onZoomAll() {
sendScreenEvent(MountainsScreenEvent.OnZoomAll)
}
private fun toggleAllPeaks() {
showAllMountains.value = !showAllMountains.value
}
// Send events back to the UI via the event channel
private fun sendScreenEvent(event: MountainsScreenEvent) {
viewModelScope.launch { _eventChannel.send(event) }
}
}
如果您有兴趣了解这些类的实现,可以在 GitHub 上访问它们,也可以在 Android Studio 中打开 MountainsRepository
和 MountainsViewModel
类。
使用 ViewModel
视图模型在 MainActivity
中用于获取 viewState
。您将在本 Codelab 的后面部分使用 viewState
来渲染标记。请注意,此代码已包含在起始项目中,此处仅供参考。
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. 放置摄像头
GoogleMap
默认居中于纬度 0、经度 0。您将渲染的标记位于美国科罗拉多州。视图模型提供的 viewState
表示包含所有标记的 LatLngBounds。
在 MountainMap.kt
中,创建一个初始化为边界框中心的 CameraPositionState
。将 GoogleMap
的 cameraPositionState
参数设置为您刚刚创建的 cameraPositionState
变量。
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
现在运行代码,并观看地图以科罗拉多州为中心。
缩放至标记范围
为了真正让地图聚焦于标记,请将 zoomAll
函数添加到 MountainMap.kt
文件的末尾。请注意,此函数需要 CoroutineScope
,因为以动画形式将相机移动到新位置是一项异步操作,需要一段时间才能完成。
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
接下来,添加代码以在标记集合周围的边界发生变化时或用户点击顶部应用栏中的“缩放至范围”按钮时调用 zoomAll
函数。请注意,缩放���围按钮已连接好,可向视图模型发送事件。您只需从视图模型中收集这些事件,并调用 zoomAll
函数作为响应。
fun MountainMap(
// ...
) {
// ...
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = viewState.boundingBox) {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
LaunchedEffect(true) {
eventFlow.collect { event ->
when (event) {
MountainsScreenEvent.OnZoomAll -> {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
}
}
}
}
现在,当您运行应用时,地图将开始聚焦于放置标记的区域。您可以重新定位并更改缩放级别,点击“缩放范围”按钮可使地图重新聚焦于标记区域。这是向前迈进的一大步!但地图上确实应该有可供查看的内容。这正是您将在下一步中完成的任务!
9. 基本标记
在此步骤中,您要向地图添加 Marker,以表示要在地图上突出显示的地图注点。您将使用入门级项目中提供的山脉列表,并将这些地点作为标记添加到地图中。
首先,向 GoogleMap
添加内容块。将有多种标记类型,因此请添加 when
语句以分支到每种类型,您将在后续步骤中依次实现每种类型。
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
添加标记
为 BasicMarkersMapContent
添加 @GoogleMapComposable
注解。请注意,您只能在 GoogleMap
内容块中使用 @GoogleMapComposable
函数。mountains
对象包含一个 Mountain
对象列表。您将使用 Mountain
对象中的位置、名称和海拔高度为该列表中的每座山添加标记。该位置用于设置 Marker
的状态参数,而该参数会控制标记的位置。
// ...
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMapComposable
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false }
) {
mountains.forEach { mountain ->
Marker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
tag = mountain,
onClick = { marker ->
onMountainClick(marker)
false
},
zIndex = if (mountain.is14er()) 5f else 2f
)
}
}
接下来运行应用,您会看到刚刚添加的标记!
自定义标记
下面介绍了一些自定义选项,可帮助您刚刚添加的标记脱颖而出,同时向用户传达实用的信息。在此任务中,您将通过自定义每个标记的图像,探索其中一部分自定义选项。
初始项目包含一个辅助函数 vectorToBitmap
,用于从 @DrawableResource
创建 BitmapDescriptor
。
起始代码包含一个山脉图标 baseline_filter_hdr_24.xml
,您将使用该图标自定义标记。
vectorToBitmap
函数会将矢量可绘制对象转换为 BitmapDescriptor
,以便与地图库搭配使用。图标颜色是使用 BitmapParameters
实例设置的。
data class BitmapParameters(
@DrawableRes val id: Int,
@ColorInt val iconColor: Int,
@ColorInt val backgroundColor: Int? = null,
val backgroundAlpha: Int = 168,
val padding: Int = 16,
)
fun vectorToBitmap(context: Context, parameters: BitmapParameters): BitmapDescriptor {
// ...
}
使用 vectorToBitmap
函数创建两个自定义 BitmapDescriptor
,一个用于海拔超过 14,000 英尺的山峰,另一个用于普通山峰。然后,使用 Marker
可组合项的 icon
参数设置图标。您还可以设置 anchor
参数来更改相对于图标的锚点位置。对于这些圆形图标,使用中心效果更好。
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
// ...
) {
// Create mountainIcon and fourteenerIcon
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
)
)
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
)
)
mountains.forEach { mountain ->
val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
Marker(
// ...
anchor = Offset(0.5f, 0.5f),
icon = icon,
)
}
}
运行应用,欣赏自定义标记。切换 Show all
开关,即可查看完整山脉集。山峰将显示不同的标记,具体取决于山峰是否为四千米级山峰。
10. 高级标记
AdvancedMarker
可为基本版 Markers
添加更多功能。在此步骤中,您将设置碰撞行为并配置图钉样式。
向 AdvancedMarkersMapContent
函数中添加一个 @GoogleMapComposable
。遍历 mountains
,为每个元素添加一个 AdvancedMarker
。
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
mountains.forEach { mountain ->
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
请注意 collisionBehavior
参数。将此参数设置为 REQUIRED_AND_HIDES_OPTIONAL
后,您的标记将替换任何优���级较低的标记。您可以放大基本标记和高级标记,对比两者之间的差异。基本标记很可能在基础地图中将您的标记和放置的标记放在同一位置。高级标记会导致优先级较低的标记被隐藏。
运行应用,看看高级标记。请务必选择底部导航栏中的 Advanced markers
标签页。
自定义 AdvancedMarkers
这些图标使用原色和间色方案来区分海拔超过 14,000 英尺的山峰与其他山峰。使用 vectorToBitmap
函数创建两个 BitmapDescriptor
,一个用于海拔超过 14,000 英尺的山峰,另一个用于其他山峰。使用这些图标为每种类型创建自定义 pinConfig
。最后,根据 is14er()
函数将 pin 应用到相应的 AdvancedMarker
。
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
)
)
val mountainPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(mountainIcon))
setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
build()
}
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
)
)
val fourteenerPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(fourteenerIcon))
setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
build()
}
mountains.forEach { mountain ->
val pin = if (mountain.is14er()) fourteenerPin else mountainPin
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
pinConfig = pin,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
11. 聚类标记
在此步骤中,您将使用 Clustering
可组合项添加基于缩放的项分组。
Clustering
可组合函数需要 ClusterItem
的集合。MountainClusterItem
实现 ClusterItem
接口。将此类添加到 ClusteringMarkersMapContent.kt
文件中。
data class MountainClusterItem(
val mountain: Mountain,
val snippetString: String
) : ClusterItem {
override fun getPosition() = mountain.location
override fun getTitle() = mountain.name
override fun getSnippet() = snippetString
override fun getZIndex() = 0f
}
现在,添加代码以根据山脉列表创建 MountainClusterItem
。请注意,此代码使用 UnitsConverter
根据用户的语言区域设置转换为适合用户的显示单位。这是在 MainActivity
中使用 CompositionLocal
设置的
@OptIn(MapsComposeExperimentalApi::class)
@Composable
@GoogleMapComposable
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
val unitsConverter = LocalUnitsConverter.current
val resources = LocalContext.current.resources
val mountainClusterItems by remember(mountains) {
mutableStateOf(
mountains.map { mountain ->
MountainClusterItem(
mountain = mountain,
snippetString = unitsConverter.toElevationString(resources, mountain.elevation)
)
}
)
}
Clustering(
items = mountainClusterItems,
)
}
有了这段代码,标记就会根据缩放级别进行聚类。整洁美观!
自定义集群
与其他标记类型一样,聚类标记也是可自定义的。Clustering
可组合函数的 clusterItemContent
参数用于设置自定义可组合块,以渲染非聚类项。实现 @Composable
函数以创建标记。SingleMountain
函数会渲染具有自定义背景配色方案的可组合 Material 3 Icon
。
在 ClusteringMarkersMapContent.kt
中,创建一个用于定义标记配色方案的数据类:
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
此外,在 ClusteringMarkersMapContent.kt
中创建一个可组合函数,以针对给定的配色方案渲染图标:
@Composable
private fun SingleMountain(
colors: IconColor,
) {
Icon(
painterResource(id = R.drawable.baseline_filter_hdr_24),
tint = colors.iconColor,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.padding(1.dp)
.drawBehind {
drawCircle(color = colors.backgroundColor, style = Fill)
drawCircle(color = colors.borderColor, style = Stroke(width = 3f))
}
.padding(4.dp)
)
}
现在,为海拔超过 14,000 英尺的山峰创建一个配色方案,并为其他山峰创建另一个配色方案。在 clusterItemContent
代码块中,根据给定的山峰是否为四千米级山峰来选择颜色方案。
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
// ...
val backgroundAlpha = 0.6f
val fourteenerColors = IconColor(
iconColor = MaterialTheme.colorScheme.onPrimary,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.primary
)
val otherColors = IconColor(
iconColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.secondary
)
// ...
Clustering(
items = mountainClusterItems,
clusterItemContent = { mountainItem ->
val colors = if (mountainItem.mountain.is14er()) {
fourteenerColors
} else {
otherColors
}
SingleMountain(colors)
},
)
}
现在,运行应用以查看各个商品的自定义版本。
12. 在地图上绘制
虽然您已经探索了一种在地图上绘制的方法(通过添加标记),但 Maps SDK for Android 还支持许多其他绘制方法��可������您���地图上显示实用信息。
例如,如果您想要在地图上表示路线和区域,可以使用 Polyline
和 Polygon
在地图上显示这些内容。或者,如果您想要将图像固定到地面,可以使用 GroundOverlay
。
在此任务中,您将学习如何绘制形状,尤其是科罗拉多州周围的轮廓。科罗拉多州的边界定义为介于北纬 37° 和 41° 之间,以及西经 102°03' 和 109°03' 之间。这样一来,绘制轮廓就非常简单了。
起始代码包含一个 DMS
类,用于将度-分-秒表示法转换为十进制度。
enum class Direction(val sign: Int) {
NORTH(1),
EAST(1),
SOUTH(-1),
WEST(-1)
}
/**
* Degrees, minutes, seconds utility class
*/
data class DMS(
val direction: Direction,
val degrees: Double,
val minutes: Double = 0.0,
val seconds: Double = 0.0,
)
fun DMS.toDecimalDegrees(): Double =
(degrees + (minutes / 60) + (seconds / 3600)) * direction.sign
借助 DMS 类,您可以通过定义四个角 LatLng
位置并将这些位置渲染为 Polygon
来绘制科罗拉多州的边界。将以下代码添加到 MountainMap.kt
@Composable
@GoogleMapComposable
fun ColoradoPolygon() {
val north = 41.0
val south = 37.0
val east = DMS(WEST, 102.0, 3.0).toDecimalDegrees()
val west = DMS(WEST, 109.0, 3.0).toDecimalDegrees()
val locations = listOf(
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
LatLng(north, west),
)
Polygon(
points = locations,
strokeColor = MaterialTheme.colorScheme.tertiary,
strokeWidth = 3F,
fillColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
)
}
现在,在 GoogleMap
内容块内调用 ColoradoPolyon()
。
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
现在,应用会勾勒出科罗拉多州的轮廓,同时为其添加细微的填充效果。
13. 添加 KML 图层和比例尺
在最后这一部分中,您将大致勾勒出不同的山脉,并向地图添加比例尺。
简要列出山脉
之前,您在科罗拉多州周围绘制了轮廓。在此处,您将向地图添加更复杂的形状。初始代码包含一个 Keyhole 标记语言 (KML) 文件,其中大致列出了重要的山脉。Maps SDK for Android 实用程序库具有向地图添加 KML 图层的功能。在 MountainMap.kt
中,在 when
代码块之后的 GoogleMap
内容代码块中添加 MapEffect
调用。使用 GoogleMap
对象调用 MapEffect
函数。它可以在需要 GoogleMap
对象的非可组合 API 和库之间架起一座有用的桥梁。
fun MountainMap(
// ...
) {
var isMapLoaded by remember { mutableStateOf(false) }
val context = LocalContext.current
GoogleMap(
// ...
) {
// ...
when (selectedMarkerType) {
// ...
}
// This code belongs inside the GoogleMap content block, but outside of
// the 'when' statement
MapEffect(key1 = true) {map ->
val layer = KmlLayer(map, R.raw.mountain_ranges, context)
layer.addLayerToMap()
}
}
添加地图比例尺
作为最后一项任务,您将向地图添加比例尺。ScaleBar
实���了一个可添加到地图中的比例尺可组合项。请注意,ScaleBar
不是
@GoogleMapComposable
,因此无法添加到 GoogleMap
内容中。而是将其添加到包含地图的 Box
中。
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
运行应用,查看完全实现的 Codelab。
14. 获取解决方案代码
如需下载完成后的 Codelab 代码,可以使用以下命令:
- 克隆代码库(如果您已安装
git
)。
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
或者,您也可以点击下面的按钮,下载源代码。
- 获取代码后,在 Android Studio 中打开
solution
目录中的相应项目。
15. 恭喜
恭喜!您已经掌握了许多内容,希望您对 Maps SDK for Android 中提供的核心功能有了进一步的了解。
了解详情
- Maps SDK for Android - 为您的 Android 应用量身打造动态的互动式地图、地理位置和地理空间体验。
- Maps Compose 库 - 一套开源的可组合函数和数据类型,您可以将其与 Jetpack Compose 结合使用来构建自己的应用。
- android-maps-compose - GitHub 上的示例代码,演示了此 Codelab 及更多 Codelab 中介绍的所有功能。
- 更多 Kotlin Codelab,了解如何使用 Google Maps Platform 构建 Android 应用