Agrega un mapa a tu app para Android (Kotlin)

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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.

f05e1ca27ff42bf6.png

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

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.

  1. En Cloud Console, haz clic en el menú desplegable del proyecto y selecciona el proyecto que deseas usar para este codelab.

  1. 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.
  2. 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.

  1. 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.

  1. 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.

d1d068b5d4ae38b9.png

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.

  1. Para proporcionársela, abre el archivo llamado local.properties en el directorio raíz de tu proyecto (el mismo nivel en el que están gradle.properties y settings.gradle).
  2. 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.

  1. 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 directorio app/ y agrega la siguiente línea dentro del bloque plugins:

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

  1. 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'
}
  1. A continuación, agrega una nueva etiqueta meta-data en el archivo AndroidManifest.xml para pasar la clave de API que generaste en un paso anterior. Para ello, ve a Android Studio y agrega la siguiente etiqueta meta-data dentro del objeto application en el archivo AndroidManifest.xml, ubicado en app/src/main.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. A continuación, crea un archivo de diseño nuevo llamado activity_main.xml en el directorio app/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.

  1. Por último, actualiza la clase MainActivity ubicada en app/src/main/java/com/google/codelabs/buildyourfirstmap. Para ello, agrega el siguiente código a fin de anular el método onCreate, 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)
}
  1. 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:

  1. Crear un ID de mapa
  2. 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.

bc5576877369b554.png

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.

  1. Crea un método en MainActivity llamado addMarkers() 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.

  1. 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.

a26f82802fe838e9.png

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.

  1. 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.

8cac51fcbef9171b.png

  1. En el cuadro de diálogo, escribe marker_info_contents en el campo Nombre del archivo (File name) y LinearLayout en el campo Elemento raíz (Root element). Luego, haz clic en Aceptar (OK).

8783af12baf07a80.png

Luego, este archivo de diseño se aumenta para representar el contenido de la ventana de información.

  1. Copia el contenido del siguiente fragmento de código, que agrega tres TextViews dentro de un grupo de vistas de LinearLayout 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.

  1. Crea un nuevo archivo Kotlin llamado MarkerInfoWindowAdapter en el mismo paquete que MainActivity. Para ello, haz clic con el botón derecho en la carpeta app/src/main/java/com/google/codelabs/buildyourfirstmap de la vista de proyectos en Android Studio y, luego, selecciona Nuevo (New) > Kotlin File/Class:

3975ba36eba9f8e1.png

  1. En el cuadro de diálogo, escribe MarkerInfoWindowAdapter y mantén la palabra Archivo (File) destacada.

992235af53d3897f.png

  1. 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.

  1. Para ello, agrega el siguiente código después de la llamada de método addMarkers() dentro de la función lambda getMapAsync().

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).

  1. Para ello, modifica la llamada a places.forEach{} en la función addMarkers() 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.

6eb7358bb61b0a88.png

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.

  1. Con esta propiedad, invoca el método icon de MarkerOptions en el método addMarkers() 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)
)
  1. 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.

68591edc86d73724.png

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:

f05e1ca27ff42bf6.png

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.

  1. Actualiza la sección dependencies de tu archivo app/build.gradle.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. Después de agregar esta línea, debes sincronizar el proyecto para obtener las dependencias nuevas.

b7b030ec82c007fd.png

Implementa el agrupamiento en clústeres

Para implementar el agrupamiento en clústeres en tu app, sigue estos tres pasos:

  1. Implementa la interfaz ClusterItem.
  2. Crea la subclase DefaultClusterRenderer.
  3. 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 valor LatLng del lugar.
  • getTitle(), que representa el nombre del lugar
  • getSnippet(), 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 mediante MarkerOptions. 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 objeto Marker 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.

  1. Para comenzar, modifica el contenido de la expresión lambda en la llamada a getMapAsync(), en MainActivity.onCreate(). Ahora, marca la llamada a addMarkers() y setInfoWindowAdapter() como comentario. En su lugar, invoca un método llamado addClusteredMarkers(), que definirás a continuación.

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Luego, en MainActivity, define addClusteredMarkers().

MainActivity.addClusteredMarkers()

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

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

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

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

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().

  1. 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.

f98ce13055430352.png

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.

  1. En el método addClusteredMarkers() de MainActivity, agrega la siguiente línea inmediatamente después de la invocación a cluster().

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.

  1. Luego, debes definir la propiedad circle y el método addCircle() en MainActivity.

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.

  1. 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.

  1. 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).

  1. 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.

  1. 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.

  1. Por último, para que los marcadores vuelvan a ser opacos cuando se detiene la cámara, modifica el contenido de setOnCameraIdleListener en el método addClusteredMarkers() 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()
}
  1. 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:

Diagrama de KTX de Google Maps Platform

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.

  1. 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'
}
  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.

  1. 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.

  1. 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