Adicionar um mapa ao seu app Android (Kotlin)

1. Antes de começar

Este codelab ensina como integrar o SDK do Maps para Android ao seu app e usar os recursos principais, criando um aplicativo que exiba um mapa de lojas de bicicletas em São Francisco, Califórnia, EUA.

f05e1ca27ff42bf6.png

Prerequisites

  • Conhecimento básico de desenvolvimento Android e Kotlin

Atividades deste laboratório

  • Ativar e usar o SDK do Maps para Android para adicionar o Google Maps a um app Android
  • Adicionar, personalizar e agrupar marcadores
  • Desenhar polilinhas e polígonos no mapa
  • Controlar o ponto de vista da câmera de forma programática

Pré-requisitos

2. Começar a configuração

Para a etapa de ativação a seguir , é necessário ativar o SDK do Maps para Android.

Configurar a Plataforma Google Maps

Caso você ainda não tenha uma conta do Google Cloud Platform e um projeto com faturamento ativado, veja como criá-los no guia Primeiros passos com a Plataforma Google Maps.

  1. No Console do Cloud, clique no menu suspenso do projeto e selecione o projeto que você quer usar neste codelab.

  1. Ative as APIs e os SDKs da Plataforma Google Maps necessários para este codelab no Google Cloud Marketplace. Para fazer isso, siga as etapas descritas neste vídeo ou nesta documentação.
  2. Gere uma chave de API na página Credenciais do Console do Cloud. Siga as etapas indicadas neste vídeo ou nesta documentação. Todas as solicitações à Plataforma Google Maps exigem uma chave de API.

3. Início rápido

Veja aqui o código inicial para ajudar você a acompanhar este codelab e começar o mais rápido possível. Se preferir, você pode ir direto para a solução, mas continue lendo se quiser desenvolver por conta própria.

  1. Clone o repositório se você tiver o git instalado.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

Se preferir, clique no botão a seguir para fazer o download do código-fonte.

  1. Depois de receber o código, abra o projeto no diretório starter do Android Studio.

4. Adicionar o Google Maps

Nesta seção, você adicionará o Google Maps para que ele seja carregado quando você iniciar o app.

d1d068b5d4ae38b9.png

Adicionar sua chave de API

A chave de API criada em uma etapa anterior precisa ser informada ao app para que o SDK do Maps para Android possa associar sua chave ao aplicativo.

  1. Para fazer isso, abra o arquivo chamado local.properties no diretório raiz do seu projeto (o mesmo nível em que gradle.properties e settings.gradle estão).
  2. Nesse arquivo, defina uma nova chave GOOGLE_MAPS_API_KEY, sendo que o valor dela é a chave de API que você criou.

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

O local.properties está incluído no arquivo .gitignore no repositório Git. Isso ocorre porque sua chave de API é considerada informação confidencial e não deve ser verificada no controle de origem, se possível.

  1. Em seguida, para expor a API e usá-la em todo o app, inclua o plug-in Secrets Gradle para Android no arquivo build.gradle do app, localizado no diretório app/, e inclua a seguinte linha no bloco plugins:

build.gradle no nível do app

plugins {
    // ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

Você também precisará modificar seu arquivo build.gradle no nível do projeto para incluir o seguinte caminho de classe:

build.gradle no nível do projeto

buildscript {
    dependencies {
        // ...
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
    }
}

Esse plug-in disponibilizará as chaves definidas no arquivo local.properties como variáveis de build no arquivo de manifesto do Android e como variáveis na classe BuildConfig gerada pelo Gradle no momento da compilação. O uso desse plug-in remove o código boilerplate que seria necessário para ler as propriedades de local.properties, para que elas possam ser acessadas em todo o app.

Adicionar dependência do Google Maps

  1. Agora que sua chave de API pode ser acessada no app, a próxima etapa é adicionar a dependência do SDK do Maps para Android ao arquivo build.gradle do seu aplicativo.

No projeto inicial que acompanha este codelab, essa dependência já foi adicionada para você.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. Em seguida, adicione uma nova tag meta-data no arquivo AndroidManifest.xml para transmitir a chave de API criada em uma etapa anterior. Para fazer isso, abra esse arquivo no Android Studio e adicione a seguinte tag meta-data no objeto application do AndroidManifest.xml, localizado em app/src/main.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. Em seguida, crie um novo arquivo de layout chamado activity_main.xml no diretório app/src/main/res/layout/ e defina-o da seguinte maneira:

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>

Esse layout tem um único FrameLayout contendo um SupportMapFragment. O fragmento contém o objeto GoogleMaps subjacente que você usará nas etapas posteriores.

  1. Por fim, atualize a classe MainActivity localizada em app/src/main/java/com/google/codelabs/buildyourfirstmap, adicionando o seguinte código para substituir o método onCreate e permitir a definição do conteúdo com o novo layout que você acabou de criar.

MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Agora execute o aplicativo. Você verá o carregamento do mapa na tela do seu dispositivo.

5. Estilização de mapas baseada na nuvem (opcional)

Você pode personalizar o estilo do mapa usando a Estilização de mapas baseada na nuvem.

Criar um ID do mapa

Se você ainda não criou um ID do mapa com um estilo associado a ele, consulte o guia de IDs do mapa para concluir as seguintes etapas:

  1. Criar um ID do mapa
  2. Associar um ID do mapa a um estilo

Adicionar um ID do mapa ao seu app

Para usar o ID de mapa que você criou, modifique o arquivo activity_main.xml e transmita seu ID de mapa no 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" />

Depois de fazer isso, execute o app para ver o mapa no estilo selecionado.

6. Adicionar marcadores

Nesta tarefa, você adicionará marcadores ao mapa que representam os pontos de interesse que gostaria de destacar no mapa. Primeiro, recupere uma lista dos lugares indicados no projeto inicial e adicione-os ao mapa. Neste exemplo, são lojas de bicicleta.

bc5576877369b554.png

Conseguir uma referência para o GoogleMap

Primeiro, você precisa obter a referência ao objeto GoogleMap para poder usar os respectivos métodos. Para fazer isso, adicione o seguinte código no seu método MainActivity.onCreate() logo após a chamada para setContentView():

MainActivity.onCreate()

val mapFragment = supportFragmentManager.findFragmentById(
    R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
    addMarkers(googleMap)
}

A implementação encontra primeiro o SupportMapFragment que você adicionou na etapa anterior usando o método findFragmentById() no objeto SupportFragmentManager. Quando uma referência é recebida, a chamada do getMapAsync() é invocada, seguida por uma transmissão de lambda. Esse lambda é onde o objeto GoogleMap é transmitido. Dentro do lambda, a chamada de método addMarkers() é invocada, que será definida em breve.

Classe informada: PlacesReader

No projeto inicial, a classe PlacesReader foi informada para você. Essa classe lê uma lista de 49 lugares armazenados em um arquivo JSON chamado places.json e os retorna como List<Place>. Os lugares propriamente ditos representam uma lista de lojas de bicicleta em São Francisco, Califórnia, EUA.

Se você quiser saber mais sobre a implementação dessa classe, pode acessá-la no GitHub ou abrir a classe PlacesReader no 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()
       }
   }

Carregar lugares

Para carregar a lista de lojas de bicicleta, adicione uma propriedade em MainActivity chamada places e defina-a da seguinte maneira:

MainActivity.places

private val places: List<Place> by lazy {
   PlacesReader(this).read()
}

Esse código invoca o método read() em um PlacesReader, que retorna um List<Place>. Um Place tem uma propriedade chamada name, o nome do lugar e um latLng (as coordenadas do local).

Place

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: LatLng,
   val rating: Float
)

Adicionar marcadores ao mapa

Agora que a lista de lugares foi carregada na memória, a próxima etapa é representá-los no mapa.

  1. Crie um método em MainActivity chamado addMarkers() e defina-o da seguinte maneira:

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

Esse método faz a iteração na lista de places, seguido pela invocação do método addMarker() no objeto GoogleMap fornecido. O marcador é criado ao instanciar um objeto MarkerOptions, que permite personalizar o próprio marcador. Nesse caso, o título e a posição do marcador são informados, para representar o nome da loja de bicicletas e as coordenadas, respectivamente.

  1. Execute o app e dirija-se a São Francisco para ver os marcadores que você acabou de adicionar.

7. Personalizar marcadores

Existem várias opções de personalização dos marcadores que você acabou de adicionar para ajudar a diferenciá-los e transmitir informações úteis aos usuários. Nesta tarefa, você conhecerá alguns deles, personalizando a imagem de cada marcador, bem como a janela de informações exibida quando um marcador é selecionado.

a26f82802fe838e9.png

Adicionar uma janela de informações

Por padrão, a janela de informações ao tocar em um marcador exibe o título e o snippet (se ele estiver definido). Você pode personalizar essa janela para exibir outros dados, como o endereço e a classificação do lugar.

Criar marker_info_contents.xml

Primeiro, crie um novo arquivo de layout chamado marker_info_contents.xml.

  1. Para fazer isso, clique com o botão direito na pasta app/src/main/res/layout na visualização de projeto no Android Studio e selecione New > Layout Resource File.

8cac51fcbef9171b.png

  1. Na caixa de diálogo, digite marker_info_contents no campo File name e LinearLayout no campo Root element, depois clique em OK.

8783af12baf07a80.png

Depois, esse arquivo de layout é inflado para representar o conteúdo da janela de informações.

  1. Copie o conteúdo no snippet de código a seguir, adicionando três TextViews em um grupo de visualização em grupo vertical LinearLayout e substituindo o código padrão no arquivo.

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>

Criar uma implementação de um InfoWindowAdapter

Depois de criar o arquivo de layout para a janela de informações personalizada, a próxima etapa é implementar a interface GoogleMap.InfoWindowAdapter. Essa interface contém dois métodos, getInfoWindow() e getInfoContents(). Os dois métodos retornam um objeto View opcional, onde o primeiro é usado para personalizar a janela, e o último para personalizar o conteúdo. No seu caso, você implementa e personaliza o retorno de getInfoContents() ao retornar "null" em getInfoWindow(), o que indica que a janela padrão deve ser usada.

  1. Crie um novo arquivo Kotlin chamado MarkerInfoWindowAdapter no mesmo pacote que MainActivity. Clique com o botão direito do mouse na pasta app/src/main/java/com/google/codelabs/buildyourfirstmap na visualização do projeto no Android Studio e selecione New > Kotlin File/Class.

3975ba36eba9f8e1.png

  1. Na caixa de diálogo, digite MarkerInfoWindowAdapter e mantenha a opção File destacada.

992235af53d3897f.png

  1. Depois de criar o arquivo, copie o conteúdo do seguinte snippet de código no novo arquivo.

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
   }
}

No conteúdo do método getInfoContents(), o marcador informado no método é transmitido para um tipo Place e, se o envio não for possível, o método retornará "null". Você não definiu a propriedade da tag no Marker ainda, mas fará isso na próxima etapa.

Em seguida, o layout marker_info_contents.xml é inflado, seguido pela definição do texto em TextViews para a tag Place.

Atualizar MainActivity

Para agrupar todos os componentes que você criou até agora, é preciso adicionar duas linhas à classe MainActivity.

Primeiro, para transmitir o InfoWindowAdapter personalizado, o MarkerInfoWindowAdapter, dentro da chamada do método getMapAsync, invoque o método setInfoWindowAdapter() no objeto GoogleMap e crie uma nova instância de MarkerInfoWindowAdapter.

  1. Para isso, adicione o seguinte código após a chamada de método addMarkers() dentro do lambda getMapAsync().

MainActivity.onCreate()

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

Por fim, você precisa definir cada lugar como a propriedade da tag em todos os marcadores adicionados ao mapa.

  1. Para fazer isso, modifique a chamada places.forEach{} na função addMarkers() com o seguinte:

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
}

Adicionar uma imagem de marcador personalizada

Personalizar a imagem do marcador é uma das maneiras divertidas de comunicar o tipo de lugar que ele representa no mapa. Nesta etapa, você exibirá bicicletas em vez dos marcadores vermelhos padrão para representar cada loja no mapa. O projeto inicial inclui o ícone de bicicleta ic_directions_bike_black_24dp.xml em app/src/res/drawable, que é usado.

6eb7358bb61b0a88.png

Definir um bitmap personalizado no marcador

Com o ícone de bicicleta drawable vetorial à sua disposição, a próxima etapa é definir esse drawable como o ícone de cada marcador no mapa. O MarkerOptions tem um método icon, que usa um BitmapDescriptor que você usa para fazer isso.

Primeiro, converta o drawable vetorial que você acabou de adicionar em um BitmapDescriptor. Um arquivo chamado BitMapHelper incluído no projeto inicial contém uma função auxiliar chamada vectorToBitmap(), que faz exatamente isso.

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

Esse método usa um Context, um ID de recurso drawable e um número inteiro de cor, e cria uma representação BitmapDescriptor dele.

Usando o método auxiliar, declarar uma nova propriedade chamada bicycleIcon e atribuir a ela a definição 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)
}

Essa propriedade usa a cor predefinida colorPrimary no seu app para pintar o ícone de bicicleta e o retornar como BitmapDescriptor.

  1. Usando essa propriedade, invoque o método icon do MarkerOptions no método addMarkers() para concluir a personalização do ícone. Ao fazer isso, a propriedade "marker" terá esta aparência:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. Execute o app para ver os marcadores atualizados.

8. Marcadores de cluster

Dependendo do zoom aplicado no mapa, você pode notar que os marcadores adicionados se sobrepõem. É difícil interagir com marcadores sobrepostos, que criam muito ruído e afetam a usabilidade do seu app.

68591edc86d73724.png

Para melhorar a experiência do usuário, sempre que você tiver um grande conjunto de dados em cluster, a prática recomendada é implementar o clustering de marcadores. Com o clustering, ao aumentar e diminuir o zoom do mapa, os marcadores que estão próximos uns dos outros são agrupados juntos:

f05e1ca27ff42bf6.png

Para implementar, você precisa da ajuda da biblioteca de utilitários do SDK do Maps para Android.

Biblioteca de utilitários do SDK do Maps para Android

A biblioteca de utilitários do SDK do Maps para Android foi criada como uma forma de ampliar a funcionalidade do SDK do Maps para Android. Ela oferece recursos avançados, como clustering de marcadores, mapas de calor, compatibilidade com KML e GeoJson, codificação e decodificação de polilinhas e diversas funções auxiliares relacionadas à geometria esférica.

Atualizar seu build.gradle

Como a biblioteca de utilitários é empacotada separadamente do SDK do Maps para Android, você precisa adicionar outra dependência ao arquivo build.gradle.

  1. Atualize a seção dependencies do arquivo app/build.gradle.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. Ao adicionar essa linha, você precisa executar uma sincronização de projeto para buscar as novas dependências.

b7b030ec82c007fd.png

Implementar o clustering

Para implementar o clustering no seu app, siga estas três etapas:

  1. Implemente a interface ClusterItem.
  2. Crie uma subclasse da classe DefaultClusterRenderer.
  3. Crie um ClusterManager e adicione itens.

Implementar a interface ClusterItem

Todos os objetos que representam um marcador em cluster no mapa precisam implementar a interface ClusterItem. No seu caso, isso significa que o modelo Place precisa estar em conformidade com ClusterItem. Abra o arquivo Place.kt e faça as seguintes modificações nele:

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
}

O ClusterItem define estes três métodos:

  • getPosition(), que representa o LatLng do lugar.
  • getTitle(), que representa o nome do lugar
  • getSnippet(), que representa o endereço do lugar.

Criar uma subclasse da classe DefaultClusterRenderer

A classe responsável por implementar o clustering, ClusterManager, usa internamente uma classe ClusterRenderer para lidar com a criação dos clusters conforme você movimenta e aplica zoom no mapa. Por padrão, ela vem com o renderizador padrão, DefaultClusterRenderer, que implementa ClusterRenderer. Para casos simples, isso deve ser suficiente. No entanto, no seu caso, como os marcadores precisam ser personalizados, você precisa ampliar essa classe e adicionar as personalizações.

Crie o arquivo Kotlin PlaceRenderer.kt no pacote com.google.codelabs.buildyourfirstmap.place e defina-o da seguinte maneira:

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
   }
}

Essa classe modifica estas duas funções:

  • onBeforeClusterItemRendered(), que é chamado antes que o cluster seja renderizado no mapa. Aqui, você pode disponibilizar as personalizações por meio do MarkerOptions. Neste caso, ele define o título, a posição e o ícone do marcador.
  • onClusterItemRenderer(), chamado logo depois que o marcador é renderizado no mapa. É nesse local que você pode acessar o objeto Marker criado. Nesse caso, ele define a propriedade da tag do marcador.

Criar um ClusterManager e adicionar itens

Por fim, para fazer o clustering funcionar, você precisa modificar MainActivity para instanciar um ClusterManager e informar as dependências necessárias para ele. O ClusterManager lida internamente com a adição dos marcadores (os objetos ClusterItem) e, em vez de incluir marcadores diretamente no mapa, essa responsabilidade é delegada ao ClusterManager. Além disso, ClusterManager também chama setInfoWindowAdapter() internamente. Portanto, será necessário definir uma janela de informações personalizada no objeto MarkerManager.Collection do ClusterManger.

  1. Para começar, modifique o conteúdo do lambda na chamada getMapAsync() em MainActivity.onCreate(). Adicione um comentário sobre a chamada addMarkers() e setInfoWindowAdapter() e invoque um método chamado addClusteredMarkers(), que você definirá a seguir.

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. A seguir, em MainActivity, defina 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()
   }
}

Esse método instancia um ClusterManager, transmite o renderizador personalizado PlacesRenderer a ele, adiciona todos os locais e invoca o método cluster(). Além disso, como o ClusterManager usa o método setInfoWindowAdapter() no objeto "map", a configuração da janela de informações personalizada precisará ser feita no objeto ClusterManager.markerCollection. Por fim, como você quer que o clustering mude à medida que o usuário movimenta e aplica zoom no mapa, um OnCameraIdleListener é fornecido ao googleMap, de forma que, quando a câmera ficar inativa, o clusterManager.onCameraIdle() será invocado.

  1. Execute o app para ver as novas lojas agrupadas em cluster.

9. Desenhar no mapa

Você já explorou uma maneira de desenhar no mapa (adicionando marcadores), mas o SDK do Maps para Android é compatível com diversas outras formas de desenho para exibir informações úteis no mapa.

Por exemplo, se você quer representar trajetos e áreas no mapa, pode usar polilinhas e polígonos para exibi-los. Caso queira corrigir uma imagem na superfície do chão, pode usar sobreposições de solo.

Nesta tarefa, você aprenderá a desenhar formas, especificamente um círculo, ao redor de um marcador sempre que ele for selecionado.

f98ce13055430352.png

Adicionar listener de cliques

Normalmente, para adicionar um listener de clique a um marcador, esse listener é transmitido diretamente no objeto GoogleMap via setOnMarkerClickListener(). No entanto, como você está usando clustering, o listener de clique precisa ser enviado ao ClusterManager.

  1. No método addClusteredMarkers() em MainActivity, adicione a seguinte linha logo após a invocação para cluster().

MainActivity.addClusteredMarkers()

// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
   addCircle(googleMap, item)
   return@setOnClusterItemClickListener false
}

Esse método adiciona um listener e invoca o método addCircle(), que você definirá a seguir. Por fim, false é retornado por esse método para indicar que ele não consumiu o evento.

  1. Em seguida, você precisa definir a propriedade circle e o método addCircle() em 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))
   )
}

A propriedade circle é definida de forma que, sempre que um novo marcador for selecionado, o círculo anterior será removido e um novo será adicionado. A API para adicionar um círculo é bastante parecida com a usada para adicionar um marcador.

  1. Execute o app para ver as alterações.

10. Controle de câmera

Como última tarefa, veja alguns controles da câmera para concentrar a visualização em uma determinada região.

Câmera e visualização

Você deve ter percebido que, ao iniciar o app, a câmera exibe o continente da África e é preciso movimentar o mapa até São Francisco e aplicar zoom para encontrar os marcadores adicionados. Embora possa ser uma maneira divertida de explorar o mundo, não é útil se você quer exibir os marcadores imediatamente.

Para ajudar nisso, é possível definir a posição da câmera de maneira programática para que a visualização fique centralizada em um ponto escolhido.

  1. Adicione o código a seguir à chamada getMapAsync() para ajustar a visualização da câmera de modo que ela seja inicializada em São Francisco quando o app for aberto.

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

Primeiro, o setOnMapLoadedCallback() é chamado para que a atualização da câmera seja realizada somente após o carregamento do mapa. Essa etapa é necessária porque as propriedades do mapa, como dimensões, precisam ser computadas antes de fazer uma chamada de atualização da câmera.

No lambda, um novo objeto LatLngBounds é construído, o que define uma região retangular no mapa. Isso é feito gradualmente, incluindo todos os valores LatLng do local para garantir que os locais estejam dentro dos limites. Depois que esse objeto é criado, o método moveCamera() em GoogleMap é invocado e um CameraUpdate é fornecido por meio de CameraUpdateFactory.newLatLngBounds(bounds.build(), 20).

  1. Execute o app e veja que a câmera é inicializada em São Francisco.

Detectar as mudanças na câmera

Além de modificar a posição da câmera, você também pode detectar as atualizações da câmera conforme o usuário move o mapa. Isso pode ser útil se você quiser modificar a IU à medida que a câmera se move.

Só por diversão, modifique o código para deixar os marcadores translúcidos sempre que a câmera for movida.

  1. No método addClusteredMarkers(), adicione as seguintes linhas na parte inferior do 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 }
}

Isso adiciona um OnCameraMoveStartedListener para que, sempre que a câmera começar a se mover, todos os valores alfa dos marcadores (de clusters e de marcadores) sejam modificados para 0.3f e os marcadores apareçam translúcidos.

  1. Por fim, para que os marcadores translúcidos voltem a ficar opacos quando a câmera parar, modifique o conteúdo de setOnCameraIdleListener no método addClusteredMarkers() para o seguinte:

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. Execute o app para ver os resultados.

11. KTX do Maps

Para apps Kotlin que usam um ou mais SDKs do Android da Plataforma Google Maps, a extensão Kotlin ou as bibliotecas KTX estão disponíveis para que você possa aproveitar os recursos da linguagem Kotlin, como corrotinas, propriedades/funções de extensão e muito mais. Cada SDK do Google Maps tem uma biblioteca KTX correspondente, conforme mostrado abaixo:

Diagrama do KTX da Plataforma Google Maps

Nesta tarefa, você usará as bibliotecas Maps KTX e Maps Utils KTX no app e refatorará as tarefas anteriores para implementar recursos da linguagem específica do Kotlin no app.

  1. Incluir dependências do KTX no arquivo build.gradle no nível do app

Como o app usa o SDK do Maps para Android e a biblioteca de utilitários desse SDK, você precisará incluir as bibliotecas KTX correspondentes delas. Você também usará um recurso encontrado na biblioteca AndroidX Lifecycle KTX nesta tarefa, então inclua essa dependência também no arquivo build.gradle no nível do 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. Usar as funções de extensão GoogleMap.addMarker() e GoogleMap.addCircle()

A biblioteca KTX do Maps oferece uma alternativa de API no estilo DSL para GoogleMap.addMarker(MarkerOptions) e GoogleMap.addCircle(CircleOptions) usadas nas etapas anteriores. Para usar as APIs mencionadas acima, a construção de uma classe com opções para um marcador ou círculo é necessária, enquanto com as alternativas KTX, é possível definir as opções de marcador ou círculo no lambda fornecido.

Para usar essas APIs, atualize os métodos MainActivity.addMarkers(GoogleMap) e 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))
    }
}

A reescrita dos métodos acima dessa maneira é muito mais concisa para ler, o que é possível usando o literal de função do Kotlin com Kotlin.

  1. Usar as funções de suspensão da extensão SupportMapFragment.awaitMap() e GoogleMap.awaitMapLoad()

A biblioteca Maps KTX também fornece extensões de função de suspensão para serem usadas em corrotinas. Especificamente, há alternativas de funções de suspensão para SupportMapFragment.getMapAsync(OnMapReadyCallback) e GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). O uso dessas APIs alternativas elimina a necessidade de transmitir callbacks. Em vez disso, ele permite receber a resposta desses métodos de maneira síncrona e em série.

Como esses métodos estão suspendendo funções, o uso deles precisa ocorrer em uma corrotina. A biblioteca Lifecycle Runtime KTX oferece uma extensão para fornecer escopos de corrotina com reconhecimento de ciclo de vida para que as corrotinas sejam executadas e interrompidas no evento apropriado.

Combinando esses conceitos, atualize o método MainActivity.onCreate(Bundle):

MainActivity.onCreate(pacote).

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

O escopo de corrotina lifecycleScope.launchWhenCreated executará o bloco quando a atividade estiver pelo menos no estado criado. As chamadas para recuperar o objeto GoogleMap e aguardar o carregamento completo do mapa foram substituídas por SupportMapFragment.awaitMap() e GoogleMap.awaitMapLoad(), respectivamente. A refatoração de código usando essas funções de suspensão permite que você escreva o código baseado em callback equivalente de maneira sequencial.

  1. Recrie o app com as mudanças refatoradas.

12. Parabéns

Parabéns! Você aprendeu bastante conteúdo e, esperamos, entende melhor os principais recursos oferecidos no SDK do Maps para Android.

Saiba mais

  • SDK do Places para Android: explore o amplo conjunto de dados de lugares para descobrir empresas perto de você.
  • android-maps-ktx: uma biblioteca de código aberto que permite a integração com o SDK do Maps para Android e a biblioteca de utilitários do SDK do Maps para Android otimizada para o Kotlin.
  • android-place-ktx: uma biblioteca de código aberto que permite a integração com o SDK do Places para Android de uma maneira compatível com Kotlin.
  • android-samples: exemplo de código no GitHub que demonstra todos os recursos abordados neste codelab e muito mais.
  • Mais codelabs do Kotlin para criar apps Android com a Plataforma Google Maps