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.
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
- SDK do Maps para Android
- Uma Conta do Google com faturamento ativado
- Android Studio 2020.3.1 ou uma versão mais recente
- Google Play Services instalado no Android Studio
- Um dispositivo Android ou um Android Emulator que execute a plataforma de APIs do Google com base no Android 4.2.2 ou versões mais recentes (consulte Executar apps no Android Emulator para ver as etapas de instalação)
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.
- No Console do Cloud, clique no menu suspenso do projeto e selecione o projeto que você quer usar neste codelab.
- 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.
- 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.
- 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.
- 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.
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.
- Para fazer isso, abra o arquivo chamado
local.properties
no diretório raiz do seu projeto (o mesmo nível em quegradle.properties
esettings.gradle
estão). - 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.
- 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órioapp/
, e inclua a seguinte linha no blocoplugins
:
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
- 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'
}
- Em seguida, adicione uma nova tag
meta-data
no arquivoAndroidManifest.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 tagmeta-data
no objetoapplication
doAndroidManifest.xml
, localizado emapp/src/main
.
AndroidManifest.xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- Em seguida, crie um novo arquivo de layout chamado
activity_main.xml
no diretórioapp/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.
- Por fim, atualize a classe
MainActivity
localizada emapp/src/main/java/com/google/codelabs/buildyourfirstmap
, adicionando o seguinte código para substituir o métodoonCreate
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)
}
- 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:
- Criar um ID do mapa
- 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.
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.
- Crie um método em
MainActivity
chamadoaddMarkers()
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.
- 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.
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
.
- 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.
- Na caixa de diálogo, digite
marker_info_contents
no campo File name eLinearLayout
no campoRoot element
, depois clique em OK.
Depois, esse arquivo de layout é inflado para representar o conteúdo da janela de informações.
- Copie o conteúdo no snippet de código a seguir, adicionando três
TextViews
em um grupo de visualização em grupo verticalLinearLayout
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.
- Crie um novo arquivo Kotlin chamado
MarkerInfoWindowAdapter
no mesmo pacote queMainActivity
. Clique com o botão direito do mouse na pastaapp/src/main/java/com/google/codelabs/buildyourfirstmap
na visualização do projeto no Android Studio e selecione New > Kotlin File/Class.
- Na caixa de diálogo, digite
MarkerInfoWindowAdapter
e mantenha a opção File destacada.
- 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
.
- Para isso, adicione o seguinte código após a chamada de método
addMarkers()
dentro do lambdagetMapAsync()
.
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.
- Para fazer isso, modifique a chamada
places.forEach{}
na funçãoaddMarkers()
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.
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
.
- Usando essa propriedade, invoque o método
icon
doMarkerOptions
no métodoaddMarkers()
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)
)
- 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.
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:
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
.
- Atualize a seção
dependencies
do arquivoapp/build.gradle
.
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- Ao adicionar essa linha, você precisa executar uma sincronização de projeto para buscar as novas dependências.
Implementar o clustering
Para implementar o clustering no seu app, siga estas três etapas:
- Implemente a interface
ClusterItem
. - Crie uma subclasse da classe
DefaultClusterRenderer
. - 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 oLatLng
do lugar.getTitle()
, que representa o nome do lugargetSnippet()
, 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 doMarkerOptions
. 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 objetoMarker
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
.
- Para começar, modifique o conteúdo do lambda na chamada
getMapAsync()
emMainActivity.onCreate()
. Adicione um comentário sobre a chamadaaddMarkers()
esetInfoWindowAdapter()
e invoque um método chamadoaddClusteredMarkers()
, que você definirá a seguir.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- A seguir, em
MainActivity
, definaaddClusteredMarkers()
.
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.
- 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.
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
.
- No método
addClusteredMarkers()
emMainActivity
, adicione a seguinte linha logo após a invocação paracluster()
.
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.
- Em seguida, você precisa definir a propriedade
circle
e o métodoaddCircle()
emMainActivity
.
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.
- 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.
- 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)
.
- 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.
- 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.
- 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étodoaddClusteredMarkers()
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()
}
- 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:
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.
- 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'
}
- 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.
- 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.
- 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