1. Antes de comenzar
En este codelab, aprenderás a integrar el SDK de Maps para Android con tu app y a usar sus funciones principales. Para ello, compilarás una app que muestra un mapa de las montañas de Colorado, EE.UU., con varios tipos de marcadores. Además, aprenderás a dibujar otras formas en el mapa.
Así se verá cuando termines el codelab:
Requisitos previos
- Conocimientos básicos de Kotlin, Jetpack Compose y desarrollo para Android
Actividades
- Habilitarás y usarás la biblioteca de Maps Compose para el SDK de Maps para Android a fin de agregar un
GoogleMap
a una app para Android. - Cómo agregar y personalizar marcadores
- Dibuja polígonos en el mapa
- Controlar el punto de vista de la cámara de forma programática
Requisitos
- SDK de Maps para Android
- Una Cuenta de Google con facturación habilitada
- La versión estable más reciente de Android Studio
- Un dispositivo Android o un emulador de Android que ejecute la plataforma de las APIs de Google basada en Android 5.0 o una versión posterior (consulta Cómo ejecutar apps en el emulador de Android para conocer los pasos de instalación).
- Una conexión a Internet
2. Prepárate
Para el siguiente paso, debes habilitar el SDK de Maps para Android.
Configura Google Maps Platform
Si todavía no tienes una cuenta de Google Cloud Platform y un proyecto con la facturación habilitada, consulta la guía Cómo comenzar a utilizar Google Maps Platform para crear una cuenta de facturación y un proyecto.
- En Cloud Console, haz clic en el menú desplegable del proyecto y selecciona el proyecto que deseas usar para este codelab.
- Habilita las API y los SDK de Google Maps Platform necesarios para este codelab en Google Cloud Marketplace. Para hacerlo, sigue los pasos que se indican en este video o esta documentación.
- Genera una clave de API en la página Credenciales de Cloud Console. Puedes seguir los pasos que se indican en este video o esta documentación. Todas las solicitudes a Google Maps Platform requieren una clave de API.
3. Inicio rápido
Para que puedas comenzar lo más rápido posible, te ofrecemos un código inicial que te ayudará a seguir este codelab. Puedes pasar directamente a la solución, pero si quieres ir paso a paso para ver cómo crearla tú mismo, sigue leyendo.
- Si tienes
git
instalado, clona el repositorio.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
También puedes hacer clic en el botón siguiente para descargar el código fuente.
- Una vez que tengas el código, abre el proyecto del directorio
starter
en Android Studio.
4. Agrega tu clave de API al proyecto
En esta sección, se describe cómo almacenar tu clave de API para que tu app pueda hacer referencia a ella de manera segura. No debes incluir la clave de API en el sistema de control de versión, por lo que te recomendamos almacenarla en el archivo secrets.properties
, que se colocará en tu copia local del directorio raíz de tu proyecto. Para obtener más información sobre el archivo secrets.properties
, consulta los archivos de propiedades de Gradle.
Para optimizar esta tarea, te recomendamos que uses el complemento Secrets Gradle para Android.
Si deseas instalar el complemento Secrets Gradle para Android en tu proyecto de Google Maps, haz lo siguiente:
- En Android Studio, abre tu archivo
build.gradle.kts
de nivel superior y agrega el siguiente código al elementodependencies
enbuildscript
.buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- Abre el archivo
build.gradle.kts
a nivel del módulo y agrega el siguiente código al elementoplugins
.plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- En tu archivo
build.gradle.kts
a nivel del módulo, asegúrate de quetargetSdk
ycompileSdk
estén establecidos en, al menos, 34. - Guarda el archivo y sincroniza tu proyecto con Gradle.
- Abre el archivo
secrets.properties
en tu directorio de nivel superior y agrega el siguiente código. ReemplazaYOUR_API_KEY
por tu clave de API. Almacena tu clave en este archivo, ya quesecrets.properties
no se registra en un sistema de control de versión.MAPS_API_KEY=YOUR_API_KEY
- Guarda el archivo.
- Crea el archivo
local.defaults.properties
en tu directorio de nivel superior, en la misma carpeta en la que se encuentra el archivosecrets.properties
, y agrega el siguiente código. El propósito de este archivo es proporcionar una ubicación de copia de seguridad de la clave de API si no se encuentra el archivoMAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
, de modo que no fallen las compilaciones. Esto ocurrirá cuando clones la app desde un sistema de control de versión y aún no hayas creado un archivosecrets.properties
localmente para proporcionar tu clave de API. - Guarda el archivo.
- En tu archivo
AndroidManifest.xml
, ve acom.google.android.geo.API_KEY
y actualiza el atributoandroid:value
. Si la etiqueta<meta-data>
no existe, créala como un elemento secundario de la etiqueta<application>
.<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- En Android Studio, abre el archivo
build.gradle.kts
a nivel del módulo y edita la propiedadsecrets
. Si la propiedadsecrets
no existe, agrégala.Edita las propiedades del complemento para establecerpropertiesFileName
ensecrets.properties
,defaultPropertiesFileName
enlocal.defaults.properties
y cualquier otra propiedad.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. Agrega Google Maps
En esta sección, agregarás un mapa de Google Maps para que se cargue cuando inicies la app.
Cómo agregar dependencias de Maps Compose
Ahora que se puede acceder a tu clave de API dentro de la app, el siguiente paso es agregar la dependencia del SDK de Maps para Android al archivo build.gradle.kts
de tu app. Para compilar con Jetpack Compose, usa la biblioteca de Maps Compose, que proporciona elementos del SDK de Maps para Android como funciones de componibilidad y tipos de datos.
build.gradle.kts
En el archivo build.gradle.kts
a nivel de la app, reemplaza las dependencias de SDK de Maps para Android que no son de Compose:
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")
}
con sus contrapartes componibles:
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")
}
Cómo agregar un elemento componible de Google Maps
En MountainMap.kt
, agrega el elemento GoogleMap
componible dentro del elemento Box
componible anidado dentro del elemento MapMountain
componible.
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 }
)
// ...
}
}
Ahora, compila y ejecuta la app. ¡Listo! Deberías ver un mapa centrado en la famosa Null Island, también conocida como latitud cero y longitud cero. Más adelante, aprenderás a posicionar el mapa en la ubicación y el nivel de zoom que desees, pero, por ahora, celebra tu primera victoria.
6. Diseño de mapas basado en Cloud
Puedes personalizar el estilo de tu mapa con el diseño de mapas basado en Cloud.
Crea un ID de mapa
Si todavía no creaste un ID de mapa con un estilo de mapa asociado, consulta la guía sobre ID de mapa para completar los siguientes pasos:
- Crear un ID de mapa
- Asociar un ID de mapa a un estilo de mapa
Cómo agregar el ID de mapa a tu app
Para usar el ID de mapa que creaste, cuando crees una instancia de tu elemento GoogleMap
componible, usa el ID de mapa cuando crees un objeto GoogleMapOptions
que se asigne al parámetro googleMapOptionsFactory
en el constructor.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
Una vez que completes esto, ejecuta la app para ver tu mapa con el estilo que seleccionaste.
7. Carga los datos de marcadores
La tarea principal de la app es cargar una colección de montañas desde el almacenamiento local y mostrarlas en el GoogleMap
. En este paso, harás un recorrido por la infraestructura proporcionada para cargar los datos de las montañas y presentarlos en la IU.
Montaña
La clase de datos Mountain
contiene todos los datos sobre cada montaña.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
Ten en cuenta que las montañas se particionarán más adelante según su elevación. Las montañas que miden al menos 4,267 metros de altura se llaman catorcenas. El código de inicio incluye una función de extensión que realiza esta verificación por ti.
/**
* 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
La clase MountainsScreenViewState
contiene todos los datos necesarios para renderizar la vista. Puede estar en estado Loading
o MountainList
, según si terminó de cargarse la lista de montañas.
/**
* 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()
}
Clases proporcionadas: MountainsRepository
y MountainsViewModel
En el proyecto inicial, se incluye la clase MountainsRepository
. Esta clase lee una lista de lugares de montaña que se almacenan en un archivo GPS Exchange Format
o GPX, top_peaks.gpx
. Si se llama a mountainsRepository.loadMountains()
, se muestra un 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
es una clase ViewModel
que carga las colecciones de montañas y expone esas colecciones, así como otras partes del estado de la IU a través de mountainsScreenViewState
. mountainsScreenViewState
es un StateFlow
activo que la IU puede observar como un estado mutable con la función de extensión collectAsState
.
Si sigue principios de arquitectura sólidos, MountainsViewModel
contiene todo el estado de la app. La IU envía las interacciones del usuario al modelo de vista con el método 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) }
}
}
Si te interesa la implementación de estas clases, puedes acceder a ellas en GitHub o abrir las clases MountainsRepository
y MountainsViewModel
en Android Studio.
Cómo usar el ViewModel
El modelo de vista se usa en MainActivity
para obtener el viewState
. Usarás viewState
para renderizar los marcadores más adelante en este codelab. Ten en cuenta que este código ya está incluido en el proyecto inicial y se muestra aquí solo como referencia.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. Cómo posicionar la cámara
Un GoogleMap
predeterminado se centra en la latitud cero y la longitud cero. Los marcadores que renderizarás se encuentran en el estado de Colorado, en EE.UU. El viewState
que proporciona el modelo de vista presenta un LatLngBounds que contiene todos los marcadores.
En MountainMap.kt
, crea un CameraPositionState
inicializado en el centro del cuadro de límite. Establece el parámetro cameraPositionState
del elemento GoogleMap
en la variable cameraPositionState
que acabas de crear.
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
Ahora ejecuta el código y observa cómo el mapa se centra en Colorado.
Acercar la vista a la extensión del marcador
Para enfocar realmente el mapa en los marcadores, agrega la función zoomAll
al final del archivo MountainMap.kt
. Ten en cuenta que esta función necesita un CoroutineScope
porque animar la cámara a una nueva ubicación es una operación asíncrona que lleva tiempo completar.
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
A continuación, agrega código para invocar la función zoomAll
cada vez que cambien los límites alrededor de la colección de marcadores o cuando el usuario haga clic en el botón de extensión del zoom en la barra superior de la aplicación. Ten en cuenta que el botón de extensión del zoom ya está conectado para enviar eventos al modelo de vista. Solo necesitas recopilar esos eventos del modelo de vista y llamar a la función zoomAll
en respuesta.
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)
}
}
}
}
}
Ahora, cuando ejecutes la app, el mapa se enfocará en el área donde se colocarán los marcadores. Puedes cambiar la posición y el zoom, y hacer clic en el botón de extensión del zoom para volver a enfocar el mapa en el área de los marcadores. ¡Eso es progreso! Pero el mapa debería tener algo para ver. Y eso es lo que harás en el siguiente paso.
9. Marcadores básicos
En este paso, agregarás marcadores al mapa que representen lugares de interés que deseas destacar en el mapa. Usarás la lista de montañas que se proporcionó en el proyecto inicial y agregarás estos lugares como marcadores en el mapa.
Comienza por agregar un bloque de contenido a GoogleMap
. Habrá varios tipos de marcadores, por lo que agregarás una instrucción when
para bifurcar cada tipo y, luego, implementarás cada uno en los pasos siguientes.
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
Agrega marcadores
Anota BasicMarkersMapContent
con @GoogleMapComposable
. Ten en cuenta que solo puedes usar funciones de @GoogleMapComposable
en el bloque de contenido GoogleMap
. El objeto mountains
tiene una lista de objetos Mountain
. Agregarás un marcador para cada montaña de esa lista, con la ubicación, el nombre y la elevación del objeto Mountain
. La ubicación se usa para establecer el parámetro de estado de Marker
, que, a su vez, controla la posición del marcador.
// ...
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
)
}
}
Ejecuta la app y verás los marcadores que acabas de agregar.
Personaliza los marcadores
Hay varias opciones para personalizar estos marcadores que los ayudarán a destacarse y transmitir información útil a los usuarios. En esta tarea, utilizarás algunas de esas opciones para personalizar la imagen de cada marcador.
El proyecto inicial incluye una función auxiliar, vectorToBitmap
, para crear objetos BitmapDescriptor
a partir de un objeto @DrawableResource
.
El código de partida incluye un ícono de montaña, baseline_filter_hdr_24.xml
, que usarás para personalizar los marcadores.
La función vectorToBitmap
convierte un elemento de diseño de vectores en un BitmapDescriptor
para usarlo con la biblioteca de mapas. Los colores de los íconos se configuran con una instancia de 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 {
// ...
}
Usa la función vectorToBitmap
para crear dos BitmapDescriptor
s personalizados: uno para las montañas de más de 4,000 metros y otro para las montañas normales. Luego, usa el parámetro icon
del elemento Marker
componible para establecer el ícono. También puedes establecer el parámetro anchor
para cambiar la ubicación del ancla en relación con el ícono. Usar el centro funciona mejor para estos íconos circulares.
@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,
)
}
}
Ejecuta la app y maravíllate con los marcadores personalizados. Activa el interruptor Show all
para ver el conjunto completo de montañas. Las montañas tendrán diferentes marcadores según si son de más de 4,000 metros.
10. Marcadores avanzados
Los AdvancedMarker
s agregan funciones adicionales a los Markers
básicos. En este paso, establecerás el comportamiento de colisión y configurarás el estilo del marcador.
Agrega @GoogleMapComposable
a la función AdvancedMarkersMapContent
. Itera sobre el mountains
y agrega un AdvancedMarker
para cada uno.
@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
}
)
}
}
Observa el parámetro collisionBehavior
. Si configuras este parámetro en REQUIRED_AND_HIDES_OPTIONAL
, tu marcador reemplazará cualquier marcador de menor prioridad. Puedes ver esto si acercas un marcador básico en comparación con un marcador avanzado. Es probable que el marcador básico tenga tu marcador y el marcador colocados en la misma ubicación del mapa base. El marcador avanzado hará que se oculte el marcador de menor prioridad.
Ejecuta la app para ver los marcadores avanzados. Asegúrate de seleccionar la pestaña Advanced markers
en la fila de navegación inferior.
AdvancedMarkers
PersonalizadoAdvancedMarkers
Los íconos usan los esquemas de color principal y secundario para distinguir entre los picos de más de 4,000 metros y otras montañas. Usa la función vectorToBitmap
para crear dos BitmapDescriptor
s: uno para los picos de más de 4,000 metros y otro para las demás montañas. Usa esos íconos para crear un pinConfig
personalizado para cada tipo. Por último, aplica el pin al AdvancedMarker
correspondiente según la función is14er()
.
@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. Marcadores agrupados en clústeres
En este paso, usarás el elemento Clustering
componible para agregar la agrupación de elementos basada en el zoom.
El elemento Clustering
componible requiere una colección de ClusterItem
. MountainClusterItem
implementa la interfaz ClusterItem
. Agrega esta clase al archivo 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
}
Ahora agrega el código para crear objetos MountainClusterItem
a partir de la lista de montañas. Ten en cuenta que este código usa un UnitsConverter
para convertir las unidades de visualización adecuadas para el usuario según su configuración regional. Esto se configura en MainActivity
con un 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,
)
}
Con ese código, los marcadores se agrupan según el nivel de zoom. ¡Todo está en orden!
Cómo personalizar clústeres
Al igual que con los otros tipos de marcadores, los marcadores agrupados en clústeres se pueden personalizar. El parámetro clusterItemContent
del elemento Clustering
componible establece un bloque componible personalizado para renderizar un elemento no agrupado. Implementa una función @Composable
para crear el marcador. La función SingleMountain
renderiza un elemento Icon
componible de Material 3 con un esquema de color de fondo personalizado.
En ClusteringMarkersMapContent.kt
, crea una clase de datos que defina el esquema de color para un marcador:
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
Además, en ClusteringMarkersMapContent.kt
, crea una función de componibilidad para renderizar un ícono para un esquema de color determinado:
@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)
)
}
Ahora, crea un esquema de colores para los picos de más de 4,000 metros y otro para las demás montañas. En el bloque clusterItemContent
, selecciona el esquema de color según si la montaña determinada es o no un pico de más de 4,000 metros.
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)
},
)
}
Ahora, ejecuta la app para ver las versiones personalizadas de los elementos individuales.
12. Dibuja en el mapa
Si bien exploraste una forma de dibujar en el mapa (agregándole marcadores), el SDK de Maps para Android admite muchas otras formas de dibujar para mostrar información útil en el mapa.
Por ejemplo, si deseas representar rutas y áreas en el mapa, puedes usar Polyline
s y Polygon
s para mostrarlas. Si deseas fijar una imagen a la superficie del suelo, puedes usar un GroundOverlay
.
En esta tarea, aprenderás a dibujar formas, específicamente un contorno alrededor del estado de Colorado. El límite de Colorado se define entre los 37° N y los 41° N de latitud, y entre los 102°03′ O y los 109°03′ O de longitud. Esto hace que dibujar el contorno sea bastante sencillo.
El código de inicio incluye una clase DMS
para convertir la notación de grados, minutos y segundos a grados decimales.
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
Con la clase DMS, puedes dibujar la frontera de Colorado definiendo las cuatro ubicaciones de las esquinas LatLng
y renderizándolas como Polygon
s. Agrega el siguiente código a 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),
)
}
Ahora llama a ColoradoPolyon()
dentro del bloque de contenido GoogleMap
.
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
Ahora, la app delinea el estado de Colorado y le da un relleno sutil.
13. Agrega una capa KML y una barra de escala
En esta última sección, describirás a grandes rasgos las diferentes cordilleras y agregarás una barra de escala al mapa.
Delimita las cordilleras
Anteriormente, dibujaste un contorno alrededor de Colorado. Aquí agregarás formas más complejas al mapa. El código inicial incluye un archivo de lenguaje de marcado de Keyhole (KML) que describe de forma aproximada las cordilleras importantes. La biblioteca de utilidades del SDK de Maps para Android tiene una función para agregar una capa de KML al mapa. En MountainMap.kt
, agrega una llamada a MapEffect
en el bloque de contenido GoogleMap
después del bloque when
. Se llama a la función MapEffect
con un objeto GoogleMap
. Puede servir como un puente útil entre las APIs no componibles y las bibliotecas que requieren un objeto GoogleMap
.
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()
}
}
Cómo agregar una escala de mapa
Como tarea final, agregarás una escala al mapa. El objeto ScaleBar
implementa un elemento componible de escala que se puede agregar al mapa. Ten en cuenta que ScaleBar
no es un
@GoogleMapComposable
y, por lo tanto, no se puede agregar al contenido de GoogleMap
En cambio, debes agregarlo al Box
que contiene el mapa.
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
Ejecuta la app para ver el codelab completamente implementado.
14. Obtén el código de la solución
Para descargar el código del codelab terminado, puedes usar estos comandos:
- Si tienes
git
instalado, clona el repositorio.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
También puedes hacer clic en el botón siguiente para descargar el código fuente.
- Una vez que tengas el código, abre el proyecto del directorio
solution
en Android Studio.
15. Felicitaciones
¡Felicitaciones! Viste una gran cantidad de contenido y esperamos que ahora puedas entender mejor las funciones principales que ofrece el SDK de Maps para Android.
Más información
- SDK de Maps para Android: Crea mapas dinámicos, interactivos y personalizados con ubicaciones y experiencias geoespaciales para tus apps para Android.
- Biblioteca de Maps Compose: Es un conjunto de funciones de componibilidad y tipos de datos de código abierto que puedes usar con Jetpack Compose para compilar tu app.
- android-maps-compose: Es código de muestra en GitHub que incluye todas las funciones que se utilizan en este codelab y mucho más.
- Más codelabs de Kotlin para crear apps para Android con Google Maps Platform