1. Antes de comenzar
Este codelab te enseña cómo integrar el SDK de Maps para Android con tu app y usar sus funciones principales. Para ello, compilarás una app que muestra un mapa de tiendas de bicicletas en San Francisco, CA, EE.UU.
Requisitos previos
- Conocimientos básicos sobre desarrollo en Kotlin y para Android
Actividades
- Habilitarás y usarás el SDK de Maps para Android a fin de agregar Google Maps a una app para Android.
- Agregarás, personalizarás y agruparás marcadores.
- Dibujarás polilíneas y polígonos en el mapa.
- Controlarás el punto de vista de la cámara de manera programática.
Requisitos
- SDK de Maps para Android
- Una Cuenta de Google con facturación habilitada
- Android Studio 2020.3.1 o una versión posterior
- Servicios de Google Play instalados en Android Studio
- Un dispositivo Android o Android Emulator que ejecute la plataforma de API de Google basada en Android 4.2.2 o una versión posterior (consulta Cómo ejecutar apps en Android Emulator para conocer los pasos de instalación).
2. Prepárate
Para el paso siguiente , 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/googlecodelabs/maps-platform-101-android.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 Google Maps
En esta sección, agregarás Google Maps para que se cargue cuando inicies la app.
Agrega tu clave de API
La app necesita la clave de API que creaste en un paso anterior para que el SDK de Maps para Android pueda asociarla con tu app.
- Para proporcionársela, abre el archivo llamado
local.properties
en el directorio raíz de tu proyecto (el mismo nivel en el que estángradle.properties
ysettings.gradle
). - En ese archivo, define una nueva clave
GOOGLE_MAPS_API_KEY
cuyo valor sea la clave de API que generaste.
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
Ten en cuenta que local.properties
está incluido en el archivo .gitignore
, en el repositorio de Git. Esto se debe a que tu clave de API se considera información sensible y, de ser posible, no debe incluirse en el control de versiones.
- Luego, debes exponer tu API a fin de que pueda usarse en toda tu app. Para eso, incluye el complemento Secrets Gradle Plugin for Android en el archivo
build.gradle
de tu app ubicado en el directorioapp/
y agrega la siguiente línea dentro del bloqueplugins
:
build.gradle de la app
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
También deberás modificar el archivo build.gradle
a nivel de proyecto para incluir la siguiente ruta de clase:
build.gradle de proyecto
buildscript {
dependencies {
// ...
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
}
}
Este complemento hará que las claves que definiste dentro del archivo local.properties
estén disponibles como variables de compilación en el archivo de manifiesto de Android y como variables en la clase BuildConfig
generada por Gradle en el momento de la compilación. Con este complemento, se quita el código estándar que, de lo contrario, sería necesario para leer propiedades de local.properties
a fin de acceder a ellos desde tu app.
Agrega la dependencia de Google Maps
- 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
de tu app.
En el proyecto inicial que viene con este codelab, esta dependencia ya está incluida.
build.gradle
dependencies {
// Dependency to include Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
- A continuación, agrega una nueva etiqueta
meta-data
en el archivoAndroidManifest.xml
para pasar la clave de API que generaste en un paso anterior. Para ello, ve a Android Studio y agrega la siguiente etiquetameta-data
dentro del objetoapplication
en el archivoAndroidManifest.xml
, ubicado enapp/src/main
.
AndroidManifest.xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- A continuación, crea un archivo de diseño nuevo llamado
activity_main.xml
en el directorioapp/src/main/res/layout/
y defínelo de la siguiente manera:
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>
Este diseño tiene un único elemento FrameLayout
con un SupportMapFragment
. Este fragmento contiene el objeto GoogleMaps
subyacente que usarás en pasos posteriores.
- Por último, actualiza la clase
MainActivity
ubicada enapp/src/main/java/com/google/codelabs/buildyourfirstmap
. Para ello, agrega el siguiente código a fin de anular el métodoonCreate
, de modo que puedas establecer su contenido con el nuevo diseño que acabas de crear.
MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- Ahora, ejecuta la app. Deberías ver que el mapa se carga en la pantalla de tu dispositivo.
5. Diseño de mapas basado en Cloud (opcional)
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, modifica el archivo activity_main.xml
y pasa el ID del mapa en el atributo map:mapId
de SupportMapFragment
.
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" />
Una vez que completes esto, ejecuta la app para ver tu mapa con el estilo que seleccionaste.
6. Agrega marcadores
En esta tarea, agregarás marcadores al mapa para representar lugares de interés que deseas destacar en el mapa. Primero, recuperarás una lista de los lugares que se proporcionaron en el proyecto inicial y, luego, los agregarás al mapa. En este ejemplo, son tiendas de bicicletas.
Obtén una referencia a GoogleMap
Primero, debes obtener una referencia al objeto GoogleMap
a fin de usar sus métodos. Para ello, agrega el siguiente código a tu método MainActivity.onCreate()
justo después de la llamada a setContentView()
:
MainActivity.onCreate()
val mapFragment = supportFragmentManager.findFragmentById(
R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
addMarkers(googleMap)
}
La implementación primero busca el fragmento SupportMapFragment
que agregaste en el paso anterior mediante el método findFragmentById()
del objeto SupportFragmentManager
. Una vez que se obtiene una referencia, se invoca la llamada a getMapAsync()
, a la que se le pasa una expresión lambda. En esta expresión, se pasa el objeto GoogleMap
y se invoca la llamada de método addMarkers()
, que se define más abajo.
Clase proporcionada: PlacesReader
En el proyecto inicial, se incluye la clase PlacesReader
. Esta clase lee una lista de 49 lugares almacenados en un archivo JSON llamado places.json
y los devuelve como List<Place>
. Estos lugares representan una lista de tiendas de bicicletas en San Francisco, CA, EE.UU.
Si te interesa la implementación de esta clase, puedes acceder a ella en GitHub o abrir la clase PlacesReader
en Android Studio.
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()
}
}
Carga los lugares
Para cargar la lista de tiendas de bicicletas, agrega una propiedad en MainActivity
llamada places
y defínela de la siguiente manera:
MainActivity.places
private val places: List<Place> by lazy {
PlacesReader(this).read()
}
Este código invoca el método read()
en una clase PlacesReader
, que devuelve un elemento List<Place>
. Cada elemento Place
tiene una propiedad llamada name
, que es el nombre del lugar, y otra llamada latLng
, que indica las coordenadas en las que se encuentra el lugar.
Place
data class Place(
val name: String,
val latLng: LatLng,
val address: LatLng,
val rating: Float
)
Agrega marcadores al mapa
Ahora que la lista de lugares se cargó en la memoria, el paso siguiente es representar estos lugares en el mapa.
- Crea un método en
MainActivity
llamadoaddMarkers()
y defínelo de la siguiente manera:
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)
)
}
}
Este método itera recorriendo la lista de places
y, luego, invoca el método addMarker()
en el objeto GoogleMap
proporcionado. Para generar el marcador, se crea una instancia de un objeto MarkerOptions
, que te permite personalizar el marcador en sí. En este caso, se proporcionan el título y la posición del marcador, que representan el nombre de la tienda de bicicletas y sus coordenadas, respectivamente.
- Ejecuta la app y busca San Francisco para ver los marcadores que acabas de agregar.
7. 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 así como la ventana de información que se muestra cuando se lo presiona.
Cómo agregar una ventana de información
De forma predeterminada, la ventana de información que aparece cuando presionas un marcador muestra el título y un fragmento (si están configurados). Puedes personalizar esta ventana para que presente información adicional, como la dirección y la calificación del lugar.
Crea el archivo marker_info_contents.xml
Primero, crea un nuevo archivo de diseño llamado marker_info_contents.xml
.
- Para ello, haz clic con el botón derecho en la carpeta
app/src/main/res/layout
de la vista de proyectos de Android Studio y selecciona New (Nuevo) > Layout Resource File.
- En el cuadro de diálogo, escribe
marker_info_contents
en el campo Nombre del archivo (File name) yLinearLayout
en el campo Elemento raíz (Root element
). Luego, haz clic en Aceptar (OK).
Luego, este archivo de diseño se aumenta para representar el contenido de la ventana de información.
- Copia el contenido del siguiente fragmento de código, que agrega tres
TextViews
dentro de un grupo de vistas deLinearLayout
vertical, y reemplaza el código predeterminado en el archivo.
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>
Crea una implementación de un objeto InfoWindowAdapter
Después de crear el archivo de diseño para la ventana de información personalizada, el siguiente paso es implementar la interfaz GoogleMap.InfoWindowAdapter. Esta interfaz contiene dos métodos: getInfoWindow()
y getInfoContents()
. Ambos métodos devuelven un objeto View
opcional: el primero permite personalizar la ventana en sí y el segundo, su contenido. En tu caso, implementa ambos y personaliza la devolución de getInfoContents()
, pero devuelve un valor nulo en getInfoWindow()
, para indicar que se debe usar la ventana predeterminada.
- Crea un nuevo archivo Kotlin llamado
MarkerInfoWindowAdapter
en el mismo paquete queMainActivity
. Para ello, haz clic con el botón derecho en la carpetaapp/src/main/java/com/google/codelabs/buildyourfirstmap
de la vista de proyectos en Android Studio y, luego, selecciona Nuevo (New) > Kotlin File/Class:
- En el cuadro de diálogo, escribe
MarkerInfoWindowAdapter
y mantén la palabra Archivo (File) destacada.
- Una vez que creaste el archivo, copia el contenido del siguiente fragmento de código en tu nuevo archivo.
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
}
}
En el contenido del método getInfoContents()
, el marcador proporcionado se convierte a un tipo Place
. En el caso de que no se pueda hacer la conversión, el método muestra un valor nulo (no configuraste aún la propiedad tag del Marker
, pero lo harás en el paso siguiente).
Luego, el diseño marker_info_contents.xml
se aumenta y se establece que el texto de las TextViews
correspondientes se tome de la etiqueta Place
.
Actualiza MainActivity
Para unir todos los componentes que creaste hasta ahora, debes agregar dos líneas en tu clase MainActivity
.
Primero, para pasar el InfoWindowAdapter
personalizado, MarkerInfoWindowAdapter
, dentro de la llamada de método getMapAsync
, invoca el método setInfoWindowAdapter()
en el objeto GoogleMap
y crea una instancia nueva de MarkerInfoWindowAdapter
.
- Para ello, agrega el siguiente código después de la llamada de método
addMarkers()
dentro de la función lambdagetMapAsync()
.
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
Por último, deberás configurar la propiedad tag de cada marcador (marker) que se agregue al mapa para que represente cada lugar (place).
- Para ello, modifica la llamada a
places.forEach{}
en la funciónaddMarkers()
con lo siguiente:
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
}
Agrega una imagen de marcador personalizada
Personalizar la imagen del marcador es una de las formas divertidas de comunicar el tipo de sitio que el marcador representa en el mapa. En este paso, se muestran bicicletas en lugar de los marcadores de color rojo predeterminados para representar cada tienda en el mapa. El proyecto inicial incluye el ícono de bicicleta ic_directions_bike_black_24dp.xml
en la ruta de acceso app/src/res/drawable
, que es el que estás usando.
Establece un mapa de bits personalizado en el marcador
Ya tienes el ícono de bicicleta de la interfaz dibujable en vector. El paso siguiente consiste en establecer ese elemento de diseño como el ícono de cada uno de los marcadores que hay en el mapa. MarkerOptions
tiene un método icon
, el cual acepta un BitmapDescriptor
que puedes usar para conseguir este resultado.
Primero, debes tomar la interfaz dibujable en vector que acabas de agregar y convertirla en un BitmapDescriptor
. El proyecto inicial contiene un archivo llamado BitMapHelper
, el cual tiene una función auxiliar llamada vectorToBitmap()
, que puedes usar para hacer justamente eso.
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)
}
}
Este método toma un objeto Context
, un ID de recurso de elementos de diseño, un valor entero de color y, con eso, crea una representación de BitmapDescriptor
.
Con el método auxiliar, declara una nueva propiedad llamada bicycleIcon
y asígnale la siguiente definición: 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)
}
Esta propiedad usa el color predefinido colorPrimary
de tu app para ajustar el tono del ícono de bicicleta y devolverlo como BitmapDescriptor
.
- Con esta propiedad, invoca el método
icon
deMarkerOptions
en el métodoaddMarkers()
para completar la personalización de tus íconos. Una vez que haces esto, la propiedad del marcador debería ser similar a lo siguiente:
MainActivity.addMarkers()
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- Ejecuta la app para ver los marcadores actualizados.
8. Agrupa los marcadores
Según el nivel de zoom del mapa, es posible que notes que los marcadores que agregaste se superponen. La superposición de marcadores no permite interactuar fácilmente y crea mucha ruido, lo cual afecta la usabilidad de tu app.
Para mejorar la experiencia del usuario, siempre que tengas un conjunto de datos grande con clústeres muy cercanos, se recomienda implementar el agrupamiento de marcadores en clústeres. Así, cuando te acercas y alejas del mapa, los marcadores que se encuentran cerca se agrupan en clústeres tal como se muestra en la siguiente imagen:
Para implementar esto, necesitas la ayuda de la Biblioteca de utilidades del SDK de Maps para Android.
Biblioteca de utilidades del SDK de Maps para Android
Esta biblioteca se creó para extender la funcionalidad del SDK de Maps para Android. Ofrece funciones avanzadas, como agrupamiento de marcadores en clústeres, mapas de calor, compatibilidad con KML y GeoJson, codificación y decodificación de polilíneas, y algunas funciones auxiliares relacionadas con la geometría esférica.
Actualiza tu archivo build.gradle
Debido a que la biblioteca de utilidades se empaqueta por separado del SDK de Maps para Android, debes agregar una dependencia adicional a tu archivo build.gradle
.
- Actualiza la sección
dependencies
de tu archivoapp/build.gradle
.
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- Después de agregar esta línea, debes sincronizar el proyecto para obtener las dependencias nuevas.
Implementa el agrupamiento en clústeres
Para implementar el agrupamiento en clústeres en tu app, sigue estos tres pasos:
- Implementa la interfaz
ClusterItem
. - Crea la subclase
DefaultClusterRenderer
. - Crea un
ClusterManager
y agrégale elementos.
Implementa la interfaz ClusterItem
Todos los objetos que representan un marcador agrupable en el mapa deben implementar la interfaz ClusterItem
. En tu caso, eso significa que el modelo Place
debe ser compatible con ClusterItem
. Abre el archivo Place.kt
y hazle las siguientes modificaciones:
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 define los siguientes tres métodos:
getPosition()
, que representa el valorLatLng
del lugar.getTitle()
, que representa el nombre del lugargetSnippet()
, que representa la dirección del lugar.
Crea una subclase a partir de la clase DefaultClusterRenderer
La clase a cargo de implementar el agrupamiento en clústeres, ClusterManager
, utiliza internamente una clase ClusterRenderer
para controlar la creación de los clústeres cuando te desplazas lateralmente por el mapa y le aplicas zoom. De forma predeterminada, incluye el procesador DefaultClusterRenderer
, el cual implementa ClusterRenderer
. Para los casos simples, esto debería ser suficiente. Sin embargo, en tu caso, dado que personalizarás los marcadores, debes extender esta clase y agregar las personalizaciones allí.
Crea el archivo Kotlin PlaceRenderer.kt
en el paquete com.google.codelabs.buildyourfirstmap.place
y defínelo de la siguiente manera:
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
}
}
Esta clase anula las dos funciones siguientes:
onBeforeClusterItemRendered()
, que se llama antes de que el clúster se renderice en el mapa. Aquí puedes proporcionar personalizaciones medianteMarkerOptions
. En este caso, se establecen el título, la posición y el ícono del marcador.onClusterItemRenderer()
, a la que se llama inmediatamente después de que se renderiza el marcador en el mapa. Aquí es donde puedes acceder al objetoMarker
creado; en este caso, se configura la propiedad tag del marcador.
Crea un ClusterManager y agrégale elementos
Por último, para que el agrupamiento en clústeres funcione, debes modificar MainActivity
para crear una instancia de ClusterManager
y proporcionarle las dependencias necesarias. ClusterManager
se ocupa de agregar los marcadores (los objetos ClusterItem
) de forma interna. Así, en lugar de que se agreguen directamente en el mapa, esta responsabilidad se delega a ClusterManager
. Además, ClusterManager
también llama a setInfoWindowAdapter()
a nivel interno, por lo que se debe configurar una ventana de información personalizada en el objeto MarkerManager.Collection
de ClusterManger
.
- Para comenzar, modifica el contenido de la expresión lambda en la llamada a
getMapAsync()
, enMainActivity.onCreate()
. Ahora, marca la llamada aaddMarkers()
ysetInfoWindowAdapter()
como comentario. En su lugar, invoca un método llamadoaddClusteredMarkers()
, que definirás a continuación.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- Luego, en
MainActivity
, defineaddClusteredMarkers()
.
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()
}
}
Este método crea una instancia de ClusterManager
, le pasa el procesador personalizado PlacesRenderer
, agrega todos los lugares y, luego, invoca el método cluster()
. Además, como ClusterManager
usa el método setInfoWindowAdapter()
en el objeto de mapa, la ventana de información personalizada deberá realizarse en el objeto ClusterManager.markerCollection
. Por último, como deseas que el agrupamiento en clústeres cambie a medida que el usuario se desplaza lateralmente y cambia el zoom del mapa, se proporciona la interfaz OnCameraIdleListener
para googleMap
. Así, cuando la cámara se vuelve inactiva, se invoca clusterManager.onCameraIdle()
.
- Ejecuta la app para ver las nuevas tiendas agrupadas en clústeres.
9. 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 polilíneas y polígonos para mostrarlas. Si deseas fijar una imagen a la superficie del suelo, puedes usar superposiciones de suelo.
En esta tarea, aprenderás a dibujar formas, específicamente un círculo, alrededor de un marcador cuando se lo presiona.
Agrega un objeto de escucha de clics
Por lo general, la forma en que puedes agregar un objeto de escucha de clics a un marcador es pasándoselo directamente en el objeto GoogleMap
mediante setOnMarkerClickListener()
. Sin embargo, debido a que estás usando agrupamiento en clústeres, el objeto de escucha de clics debe proporcionarse a ClusterManager
.
- En el método
addClusteredMarkers()
deMainActivity
, agrega la siguiente línea inmediatamente después de la invocación acluster()
.
MainActivity.addClusteredMarkers()
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
Este método agrega un objeto de escucha y, luego, invoca el método addCircle()
, que definirás a continuación. Por último, este método devuelve false
para indicar que no consumió este evento.
- Luego, debes definir la propiedad
circle
y el métodoaddCircle()
enMainActivity
.
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))
)
}
La propiedad circle
se establece para que, cada vez que se presione un nuevo marcador, se quite el círculo anterior y se agregue uno nuevo. Ten en cuenta que la API para agregar un círculo es bastante similar a la que se usa para agregar un marcador.
- Ahora ejecuta la app para ver los cambios.
10. Control de la cámara
Como última tarea, examinarás algunos controles de la cámara para enfocar la vista alrededor de una región específica.
Cámara y vista
Seguramente, notaste que cuando ejecutas la app, la cámara muestra el continente de África, y debes tomarte el trabajo de desplazar el mapa lateralmente y acercarlo hasta llegar a San Francisco para ver los marcadores que agregaste. Si bien puede ser una forma divertida de explorar el mundo, no es útil si quieres mostrar los marcadores de inmediato.
Para ayudar con eso, puedes establecer la posición de la cámara de manera programática a fin de que la vista se centre en el lugar que desees.
- Agrega el siguiente código en la llamada a
getMapAsync()
para ajustar la vista de la cámara a fin de que se inicialice en San Francisco cuando se abra la app.
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))
}
}
En primer lugar, se llama a setOnMapLoadedCallback()
para que la actualización de la cámara solo se realice una vez cargado el mapa. Este paso es necesario porque deben considerarse las propiedades del mapa, como las dimensiones, antes de realizar una llamada para actualizar la cámara.
En la expresión lambda, se crea un nuevo objeto LatLngBounds
, el cual define una región rectangular en el mapa. Se lo construye de forma incremental incluyendo todos los valores LatLng
de los sitios para garantizar que todos los lugares queden dentro de los límites. Una vez creado el objeto, se invoca el método moveCamera()
en GoogleMap
y se le proporciona la acción CameraUpdate
mediante CameraUpdateFactory.newLatLngBounds(bounds.build(), 20)
.
- Ejecuta la app y observa que la cámara ahora se inicializa en San Francisco.
Cómo escuchar los cambios de la cámara
Además de modificar la posición de la cámara, también puedes escuchar las actualizaciones que surgen cuando el usuario se mueve por el mapa. Esto podría ser útil si deseas modificar la IU a medida que la cámara se mueve.
Solo por diversión, modifica el código para que los marcadores se vuelvan translúcidos cada vez que se mueva la cámara.
- En el método
addClusteredMarkers()
, agrega las siguientes líneas en la parte inferior del método:
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 }
}
Esto agrega una interfaz OnCameraMoveStartedListener
para que, cuando la cámara comience a moverse, todos los valores alfa de los marcadores (tanto clústeres como marcadores) se modifiquen a 0.3f
. Así, los marcadores se volverán translúcidos.
- Por último, para que los marcadores vuelvan a ser opacos cuando se detiene la cámara, modifica el contenido de
setOnCameraIdleListener
en el métodoaddClusteredMarkers()
de la siguiente manera:
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()
}
- Ejecuta la app para ver los resultados.
11. Maps KTX
En el caso de las apps de Kotlin que usan uno o más SDK de Android para Google Maps Platform, hay extensiones Kotlin o de KTX disponibles para permitirte aprovechar las funciones del lenguaje Kotlin, como corrutinas, propiedades y funciones de extensiones, y más. Cada SDK de Google Maps tiene una biblioteca KTX correspondiente, como se muestra a continuación:
En esta tarea, usarás las bibliotecas de Maps KTX y Maps Utils KTX para tu app, y refactorizarás tareas anteriores a fin de usar funciones de lenguaje específicas de Kotlin en tu app.
- Cómo incluir dependencias de KTX en tu archivo build.gradle de nivel de app
Dado que la app usa tanto el SDK de Maps para Android como la Biblioteca de utilidades del SDK de Maps para Android, deberás incluir las bibliotecas KTX de estas bibliotecas. En esta tarea, también usarás una función que se encuentra en la biblioteca KTX de AndroidX Lifecycle. Por lo tanto, incluye también esa dependencia en el archivo build.gradle
de nivel de la app.
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'
}
- Cómo usar las funciones de extensión GoogleMap.addMarker() y GoogleMap.addCircle()
La biblioteca de Maps KTX ofrece una alternativa a la API de estilo DSL para GoogleMap.addMarker(MarkerOptions)
y GoogleMap.addCircle(CircleOptions)
que se usaron en los pasos anteriores. Para usar las API antes mencionadas, es necesario construir una clase que contenga opciones para un marcador o círculo, mientras que, con las alternativas KTX, puedes establecer las opciones del marcador o círculo en la expresión lambda que proporciones.
Para usar estas API, actualiza los métodos MainActivity.addMarkers(GoogleMap)
y 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))
}
}
Reescribir los métodos anteriores de esta manera es mucho más conciso de leer, lo que es posible gracias al literal de función con Kotlin del receptor.
- Cómo usar las funciones de suspensión de extensiones SupportMapFragment.awaitMap() y GoogleMap.awaitMapLoad()
La biblioteca de Maps KTX también proporciona extensiones de funciones de suspensión que se pueden usar en corrutinas. En particular, hay alternativas de suspensión para SupportMapFragment.getMapAsync(OnMapReadyCallback)
y GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback)
. Usar estas API alternativas elimina la necesidad de pasar devoluciones de llamada y, en su lugar, te permite recibir la respuesta de estos métodos de forma serial y síncrona.
Dado que estos métodos son funciones de suspensión, su uso deberá ocurrir dentro de una corrutina. La biblioteca Lifecycle Runtime KTX ofrece una extensión para proporcionar alcances de corrutinas optimizados para ciclos de vida a fin de que las corrutinas se ejecuten y se detengan en el evento de ciclo de vida apropiado.
Combina estos conceptos, actualiza el método MainActivity.onCreate(Bundle)
:
MainActivity.onCreate(Bundle)
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)
}
}
El alcance de la corrutina lifecycleScope.launchWhenCreated
ejecutará el bloque cuando la actividad esté al menos en el estado creado. Además, ten en cuenta que las llamadas para recuperar el objeto GoogleMap
y esperar a que el mapa termine de cargarse se reemplazaron por SupportMapFragment.awaitMap()
y GoogleMap.awaitMapLoad()
, respectivamente. La refactorización de código con estas funciones de suspensión te permite escribir el código basado en devoluciones de llamada equivalente de manera secuencial.
- Vuelve a compilar la app con los cambios refactorizados.
12. 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 Places para Android: Explora el amplio conjunto de datos de lugares para descubrir las empresas cercanas.
- android-maps-ktx: Es una biblioteca de código abierto que se puede integrar con el SDK de Maps para Android y la Biblioteca de utilidades del SDK de Maps para Android de una manera compatible con Kotlin.
- android-place-ktx: Es una biblioteca de código abierto que se puede integrar con el SDK de Places para Android de una manera compatible con Kotlin.
- android-samples: 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 a fin de compilar apps para Android con Google Maps Platform