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 montanhas no Colorado, EUA, usando vários tipos de marcadores. Além disso, você vai aprender a desenhar outras formas no mapa.
Confira como ele vai ficar quando você terminar o codelab:
Pré-requisitos
- Conhecimento básico de Kotlin, Jetpack Compose e desenvolvimento para Android
Atividades deste laboratório
- Ativar e usar a biblioteca do Maps Compose para o SDK do Maps para Android e adicionar um
GoogleMap
a um app Android - Adicionar e personalizar marcadores
- Desenhar polígonos no mapa
- Controlar o ponto de vista da câmera de forma programática
O que é necessário
- SDK do Maps para Android
- Uma Conta do Google com faturamento ativado
- Versão estável mais recente do Android Studio
- Um dispositivo Android ou um Android Emulator que execute a plataforma de APIs do Google com base no Android 5.0 ou versões mais recentes (consulte Executar apps no Android Emulator para ver as etapas de instalação).
- Uma conexão com a Internet.
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 da 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/googlemaps-samples/codelab-maps-platform-101-compose.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 sua chave de API ao projeto
Nesta seção, descrevemos como armazenar sua chave de API para que ela possa ser referenciada com segurança pelo seu app. Não faça a verificação dela no sistema de controle de versões. Recomendamos armazená-la no arquivo secrets.properties
, que será colocado na sua cópia local do diretório raiz do projeto. Para saber mais sobre o arquivo secrets.properties
, consulte Arquivos de propriedades do Gradle.
Para otimizar essa tarefa, recomendamos usar o plug-in Secrets Gradle para Android.
Para instalar esse plug-in no seu projeto do Google Maps, faça o seguinte:
- No Android Studio, abra o arquivo
build.gradle.kts
de nível superior e adicione o seguinte código ao elementodependencies
embuildscript
.buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- Abra o arquivo
build.gradle.kts
no nível do módulo e adicione o seguinte código ao elementoplugins
.plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- No arquivo
build.gradle.kts
no nível do módulo, definatargetSdk
ecompileSdk
como pelo menos 34. - Salve o arquivo e sincronize seu projeto com o Gradle.
- Abra o arquivo
secrets.properties
no diretório de nível superior e adicione o código a seguir. SubstituaYOUR_API_KEY
pela sua chave de API. Armazene sua chave nesse arquivo porquesecrets.properties
não é verificado em um sistema de controle de versões.MAPS_API_KEY=YOUR_API_KEY
- Salve o arquivo.
- Crie o arquivo
local.defaults.properties
no seu diretório de nível superior, na mesma pasta que o arquivosecrets.properties
, e depois adicione o seguinte código. O objetivo desse arquivo é oferecer um local de backup para a chave da API se o arquivoMAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
não for encontrado, para que os builds não apresentem falha. Isso vai acontecer quando você clonar o app de um sistema de controle de versões e ainda não tiver criado um arquivosecrets.properties
localmente para fornecer sua chave de API. - Salve o arquivo.
- No seu arquivo
AndroidManifest.xml
, acessecom.google.android.geo.API_KEY
e atualize o atributoandroid:value
. Se a tag<meta-data>
não existir, crie-a como um elemento filho da tag<application>
.<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- No Android Studio, abra o arquivo
build.gradle.kts
no nível do módulo e edite a propriedadesecrets
. Se a propriedadesecrets
não existir, adicione-a.Edite as propriedades do plug-in para definirpropertiesFileName
comosecrets.properties
,defaultPropertiesFileName
comolocal.defaults.properties
e outras propriedades.secrets { // Optionally specify a different file name containing your secrets. // The plugin defaults to "local.properties" propertiesFileName = "secrets.properties" // A properties file containing default secret values. This file can be // checked in version control. defaultPropertiesFileName = "local.defaults.properties" }
5. Adicionar o Google Maps
Nesta seção, você vai adicionar um mapa do Google para que ele seja carregado quando você iniciar o app.
Adicionar dependências do Maps Compose
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.kts
do seu aplicativo. Para criar com o Jetpack Compose, use a biblioteca do Maps Compose, que fornece elementos do SDK do Maps para Android como funções combináveis e tipos de dados.
build.gradle.kts
No arquivo build.gradle.kts
no nível do app, substitua as dependências do SDK do Maps para Android que não são do Compose:
dependencies {
// ...
// Google Maps SDK -- these are here for the data model. Remove these dependencies and replace
// with the compose versions.
implementation("com.google.android.gms:play-services-maps:18.2.0")
// KTX for the Maps SDK for Android library
implementation("com.google.maps.android:maps-ktx:5.0.0")
// KTX for the Maps SDK for Android Utility Library
implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
}
com as contrapartes combináveis:
dependencies {
// ...
// Google Maps Compose library
val mapsComposeVersion = "4.4.1"
implementation("com.google.maps.android:maps-compose:$mapsComposeVersion")
// Google Maps Compose utility library
implementation("com.google.maps.android:maps-compose-utils:$mapsComposeVersion")
// Google Maps Compose widgets library
implementation("com.google.maps.android:maps-compose-widgets:$mapsComposeVersion")
}
Adicionar um elemento combinável do Google Maps
Em MountainMap.kt
, adicione o elemento combinável GoogleMap
dentro do elemento Box
aninhado no elemento MapMountain
.
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...
@Composable
fun MountainMap(
paddingValues: PaddingValues,
viewState: MountainsScreenViewState.MountainList,
eventFlow: Flow<MountainsScreenEvent>,
selectedMarkerType: MarkerType,
) {
var isMapLoaded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Add GoogleMap here
GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapLoaded = { isMapLoaded = true }
)
// ...
}
}
Agora crie e execute o app. Pronto! Um mapa centralizado na famosa Ilha Nula, também conhecida como latitude zero e longitude zero, vai aparecer. Depois, você vai aprender a posicionar o mapa no local e no nível de zoom desejados, mas por enquanto comemore sua primeira vitória!
6. Estilização de mapas baseada na nuvem
Com a Estilização de mapas baseada na nuvem, você pode personalizar o estilo do mapa.
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:
- Crie um ID do mapa.
- Associe um ID do mapa a um estilo.
Adicionar o ID do mapa ao seu app
Para usar o ID do mapa que você criou, ao instanciar o elemento combinável GoogleMap
, use o ID do mapa ao criar um objeto GoogleMapOptions
, que é atribuído ao parâmetro googleMapOptionsFactory
no construtor.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
Depois de fazer isso, execute o app para ver o mapa no estilo selecionado.
7. Carregar os dados de marcador
A principal tarefa do app é carregar uma coleção de montanhas do armazenamento local e mostrar essas montanhas no GoogleMap
. Nesta etapa, você vai conhecer a infraestrutura fornecida para carregar os dados das montanhas e apresentá-los à interface.
Montanha
A classe de dados Mountain
contém todos os dados sobre cada montanha.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
As montanhas serão particionadas mais tarde com base na elevação. Montanhas com pelo menos 4.267 metros de altura são chamadas de fourteeners (em inglês). O código inicial inclui uma função de extensão para fazer essa verificação.
/**
* Extension function to determine whether a mountain is a "14er", i.e., has an elevation greater
* than 14,000 feet (~4267 meters).
*/
fun Mountain.is14er() = elevation >= 14_000.feet
MountainsScreenViewState
A classe MountainsScreenViewState
contém todos os dados necessários para renderizar a visualização. Ele pode estar no estado Loading
ou MountainList
, dependendo se a lista de montanhas terminou de carregar.
/**
* Sealed class representing the state of the mountain map view.
*/
sealed class MountainsScreenViewState {
data object Loading : MountainsScreenViewState()
data class MountainList(
// List of the mountains to display
val mountains: List<Mountain>,
// Bounding box that contains all of the mountains
val boundingBox: LatLngBounds,
// Switch indicating whether all the mountains or just the 14ers
val showingAllPeaks: Boolean = false,
) : MountainsScreenViewState()
}
Classes fornecidas: MountainsRepository
e MountainsViewModel
No projeto inicial, a classe MountainsRepository
foi informada para você. Essa classe lê uma lista de lugares de montanhas armazenados em um GPS Exchange Format
ou arquivo GPX, top_peaks.gpx
. Chamar mountainsRepository.loadMountains()
retorna um StateFlow<List<Mountain>>
.
MountainsRepository
class MountainsRepository(@ApplicationContext val context: Context) {
private val _mountains = MutableStateFlow(emptyList<Mountain>())
val mountains: StateFlow<List<Mountain>> = _mountains
private var loaded = false
/**
* Loads the list of mountains from the list of mountains from the raw resource.
*/
suspend fun loadMountains(): StateFlow<List<Mountain>> {
if (!loaded) {
loaded = true
_mountains.value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.top_peaks).use { inputStream ->
readMountains(inputStream)
}
}
}
return mountains
}
/**
* Reads the [Waypoint]s from the given [inputStream] and returns a list of [Mountain]s.
*/
private fun readMountains(inputStream: InputStream) =
readWaypoints(inputStream).mapIndexed { index, waypoint ->
waypoint.toMountain(index)
}.toList()
// ...
}
MountainsViewModel
MountainsViewModel
é uma classe ViewModel
que carrega as coleções de montanhas e as expõe, assim como outras partes do estado da interface, usando o mountainsScreenViewState
. mountainsScreenViewState
é um StateFlow
ativo que a interface pode observar como um estado mutável usando a função de extensão collectAsState
.
Seguindo princípios arquitetônicos sólidos, o MountainsViewModel
mantém todo o estado do app. A interface envia as interações do usuário para o modelo de visualização usando o método onEvent
.
@HiltViewModel
class MountainsViewModel
@Inject
constructor(
mountainsRepository: MountainsRepository
) : ViewModel() {
private val _eventChannel = Channel<MountainsScreenEvent>()
// Event channel to send events to the UI
internal fun getEventChannel() = _eventChannel.receiveAsFlow()
// Whether or not to show all of the high peaks
private var showAllMountains = MutableStateFlow(false)
val mountainsScreenViewState =
mountainsRepository.mountains.combine(showAllMountains) { allMountains, showAllMountains ->
if (allMountains.isEmpty()) {
MountainsScreenViewState.Loading
} else {
val filteredMountains =
if (showAllMountains) allMountains else allMountains.filter { it.is14er() }
val boundingBox = filteredMountains.map { it.location }.toLatLngBounds()
MountainsScreenViewState.MountainList(
mountains = filteredMountains,
boundingBox = boundingBox,
showingAllPeaks = showAllMountains,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MountainsScreenViewState.Loading
)
init {
// Load the full set of mountains
viewModelScope.launch {
mountainsRepository.loadMountains()
}
}
// Handle user events
fun onEvent(event: MountainsViewModelEvent) {
when (event) {
OnZoomAll -> onZoomAll()
OnToggleAllPeaks -> toggleAllPeaks()
}
}
private fun onZoomAll() {
sendScreenEvent(MountainsScreenEvent.OnZoomAll)
}
private fun toggleAllPeaks() {
showAllMountains.value = !showAllMountains.value
}
// Send events back to the UI via the event channel
private fun sendScreenEvent(event: MountainsScreenEvent) {
viewModelScope.launch { _eventChannel.send(event) }
}
}
Se você quiser saber mais sobre a implementação dessas classes, pode acessá-las no GitHub ou abrir as classes MountainsRepository
e MountainsViewModel
no Android Studio.
Usar o ViewModel
O modelo de visualização é usado em MainActivity
para receber o viewState
. Você vai usar o viewState
para renderizar os marcadores mais adiante neste codelab. Esse código já está incluído no projeto inicial e é mostrado aqui apenas para referência.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. Posicione a câmera
Um padrão GoogleMap
é centralizado na latitude zero e na longitude zero. Os marcadores que você vai renderizar estão localizados no estado do Colorado, nos EUA. O viewState
fornecido pelo modelo de visualização apresenta um LatLngBounds que contém todos os marcadores.
Em MountainMap.kt
, crie um CameraPositionState
inicializado no centro da caixa delimitadora. Defina o parâmetro cameraPositionState
do GoogleMap
como a variável cameraPositionState
que você acabou de criar.
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
Agora execute o código e veja o mapa centralizado no Colorado.
Aplicar zoom nas extensões do marcador
Para focar o mapa nos marcadores, adicione a função zoomAll
ao final do arquivo MountainMap.kt
. Essa função precisa de um CoroutineScope
porque a animação da câmera para um novo local é uma operação assíncrona que leva tempo para ser concluída.
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
Em seguida, adicione código para invocar a função zoomAll
sempre que os limites ao redor da coleção de marcadores mudarem ou quando o usuário clicar no botão de extensão de zoom na barra de apps na parte de cima. O botão de extensão de zoom já está conectado para enviar eventos ao modelo de visualização. Você só precisa coletar esses eventos do modelo de visualização e chamar a função zoomAll
em resposta.
fun MountainMap(
// ...
) {
// ...
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = viewState.boundingBox) {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
LaunchedEffect(true) {
eventFlow.collect { event ->
when (event) {
MountainsScreenEvent.OnZoomAll -> {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
}
}
}
}
Agora, quando você executar o app, o mapa vai começar focado na área em que os marcadores serão colocados. Você pode reposicionar e mudar o zoom. Ao clicar no botão de extensão do zoom, o mapa será reorientado na área do marcador. Isso é progresso! Mas o mapa precisa ter algo para mostrar. E é isso que você vai fazer na próxima etapa.
9. Marcadores básicos
Nesta etapa, você adicionará Marcadores ao mapa que representam os pontos de interesse que você quer destacar. Você vai usar a lista de montanhas fornecida no projeto inicial e adicionar esses lugares como marcadores no mapa.
Comece adicionando um bloco de conteúdo ao GoogleMap
. Como haverá vários tipos de marcadores, adicione uma instrução when
para ramificar cada tipo e implemente cada um por vez nas etapas a seguir.
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
Adicionar marcadores
Inclua a anotação @GoogleMapComposable
na BasicMarkersMapContent
. Só é possível usar funções @GoogleMapComposable
no bloco de conteúdo GoogleMap
. O objeto mountains
tem uma lista de objetos Mountain
. Você vai adicionar um marcador para cada montanha na lista, usando a localização, o nome e a elevação do objeto Mountain
. O local é usado para definir o parâmetro de estado do Marker
, que, por sua vez, controla a posição do marcador.
// ...
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMapComposable
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false }
) {
mountains.forEach { mountain ->
Marker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
tag = mountain,
onClick = { marker ->
onMountainClick(marker)
false
},
zIndex = if (mountain.is14er()) 5f else 2f
)
}
}
Execute o app para ver os marcadores que você acabou de adicionar.
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ê vai conhecer alguns deles, personalizando a imagem de cada marcador.
O projeto inicial inclui uma função auxiliar, vectorToBitmap
, para criar BitmapDescriptor
s de um @DrawableResource
.
O código inicial inclui um ícone de montanha, baseline_filter_hdr_24.xml
, que você vai usar para personalizar os marcadores.
A função vectorToBitmap
converte um drawable vetorial em um BitmapDescriptor
para uso com a biblioteca de mapas. As cores do ícone são definidas usando uma instância BitmapParameters
.
data class BitmapParameters(
@DrawableRes val id: Int,
@ColorInt val iconColor: Int,
@ColorInt val backgroundColor: Int? = null,
val backgroundAlpha: Int = 168,
val padding: Int = 16,
)
fun vectorToBitmap(context: Context, parameters: BitmapParameters): BitmapDescriptor {
// ...
}
Use a função vectorToBitmap
para criar dois BitmapDescriptor
s personalizados: um para montanhas com mais de 14.000 pés e outro para montanhas comuns. Em seguida, use o parâmetro icon
do elemento combinável Marker
para definir o ícone. Além disso, defina o parâmetro anchor
para mudar o local da âncora em relação ao ícone. Usar o centro funciona melhor para esses ícones circulares.
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
// ...
) {
// Create mountainIcon and fourteenerIcon
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
)
)
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
)
)
mountains.forEach { mountain ->
val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
Marker(
// ...
anchor = Offset(0.5f, 0.5f),
icon = icon,
)
}
}
Execute o app e admire os marcadores personalizados. Toque no botão Show all
para ver todas as montanhas. As montanhas terão marcadores diferentes dependendo se são de 14.000 pés.
10. Marcadores Avançados
Os AdvancedMarker
s adicionam recursos extras ao Markers
básico. Nesta etapa, você vai definir o comportamento de colisão e configurar o estilo do marcador.
Adicione @GoogleMapComposable
à função AdvancedMarkersMapContent
. Faça um loop na mountains
adicionando um AdvancedMarker
para cada um.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
mountains.forEach { mountain ->
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
Observe o parâmetro collisionBehavior
. Ao definir esse parâmetro como REQUIRED_AND_HIDES_OPTIONAL
, seu marcador vai substituir qualquer marcador de prioridade mais baixa. Para ver isso, aumente o zoom em um marcador básico e em um avançado. O marcador básico provavelmente terá seu marcador e o marcador colocados no mesmo local no mapa de base. O marcador avançado vai ocultar o de prioridade mais baixa.
Execute o app para ver os Marcadores Avançados. Selecione a guia Advanced markers
na linha de navegação da parte de baixo.
AdvancedMarkers
personalizado
Os ícones usam os esquemas de cores primárias e secundárias para distinguir entre os picos de 14.000 pés e outras montanhas. Use a função vectorToBitmap
para criar dois BitmapDescriptor
s, um para os picos de 14.000 pés e outro para as outras montanhas. Use esses ícones para criar um pinConfig
personalizado para cada tipo. Por fim, aplique o marcador ao AdvancedMarker
correspondente com base na função is14er()
.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
)
)
val mountainPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(mountainIcon))
setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
build()
}
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
)
)
val fourteenerPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(fourteenerIcon))
setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
build()
}
mountains.forEach { mountain ->
val pin = if (mountain.is14er()) fourteenerPin else mountainPin
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
pinConfig = pin,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
11. Marcadores em cluster
Nesta etapa, você vai usar o elemento combinável Clustering
para adicionar o agrupamento de itens com base no zoom.
O elemento combinável Clustering
exige uma coleção de ClusterItem
s. MountainClusterItem
implementa a interface ClusterItem
. Adicione essa classe ao arquivo ClusteringMarkersMapContent.kt
.
data class MountainClusterItem(
val mountain: Mountain,
val snippetString: String
) : ClusterItem {
override fun getPosition() = mountain.location
override fun getTitle() = mountain.name
override fun getSnippet() = snippetString
override fun getZIndex() = 0f
}
Agora adicione o código para criar MountainClusterItem
s com base na lista de montanhas. Esse código usa um UnitsConverter
para converter em unidades de exibição adequadas ao usuário com base na localidade dele. Isso é configurado no MainActivity
usando um CompositionLocal
.
@OptIn(MapsComposeExperimentalApi::class)
@Composable
@GoogleMapComposable
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
val unitsConverter = LocalUnitsConverter.current
val resources = LocalContext.current.resources
val mountainClusterItems by remember(mountains) {
mutableStateOf(
mountains.map { mountain ->
MountainClusterItem(
mountain = mountain,
snippetString = unitsConverter.toElevationString(resources, mountain.elevation)
)
}
)
}
Clustering(
items = mountainClusterItems,
)
}
Com esse código, os marcadores são agrupados com base no nível de zoom. Tudo em ordem!
Personalizar clusters
Assim como os outros tipos de marcadores, os agrupados podem ser personalizados. O parâmetro clusterItemContent
do elemento combinável Clustering
define um bloco combinável personalizado para renderizar um item não agrupado. Implemente uma função @Composable
para criar o marcador. A função SingleMountain
renderiza um Icon
combinável do Material 3 com um esquema de cores de plano de fundo personalizado.
Em ClusteringMarkersMapContent.kt
, crie uma classe de dados que defina o esquema de cores de um marcador:
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
Além disso, em ClusteringMarkersMapContent.kt
, crie uma função combinável para renderizar um ícone para um determinado esquema de cores:
@Composable
private fun SingleMountain(
colors: IconColor,
) {
Icon(
painterResource(id = R.drawable.baseline_filter_hdr_24),
tint = colors.iconColor,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.padding(1.dp)
.drawBehind {
drawCircle(color = colors.backgroundColor, style = Fill)
drawCircle(color = colors.borderColor, style = Stroke(width = 3f))
}
.padding(4.dp)
)
}
Agora crie um esquema de cores para os picos de 14.000 pés e outro para as outras montanhas. No bloco clusterItemContent
, selecione o esquema de cores com base em se a montanha é um "fourteener" ou não.
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
// ...
val backgroundAlpha = 0.6f
val fourteenerColors = IconColor(
iconColor = MaterialTheme.colorScheme.onPrimary,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.primary
)
val otherColors = IconColor(
iconColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.secondary
)
// ...
Clustering(
items = mountainClusterItems,
clusterItemContent = { mountainItem ->
val colors = if (mountainItem.mountain.is14er()) {
fourteenerColors
} else {
otherColors
}
SingleMountain(colors)
},
)
}
Agora, execute o app para conferir versões personalizadas dos itens individuais.
12. 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 Polyline
s e Polygon
s para exibi-los. Caso queira corrigir uma imagem na superfície do chão, use uma GroundOverlay
.
Nesta tarefa, você vai aprender a desenhar formas, especificamente um contorno ao redor do estado do Colorado. A fronteira do Colorado é definida entre 37°N e 41°N de latitude e 102°03'W e 109°03'W. Isso facilita bastante o desenho do contorno.
O código inicial inclui uma classe DMS
para converter da notação graus-minutos-segundos para graus decimais.
enum class Direction(val sign: Int) {
NORTH(1),
EAST(1),
SOUTH(-1),
WEST(-1)
}
/**
* Degrees, minutes, seconds utility class
*/
data class DMS(
val direction: Direction,
val degrees: Double,
val minutes: Double = 0.0,
val seconds: Double = 0.0,
)
fun DMS.toDecimalDegrees(): Double =
(degrees + (minutes / 60) + (seconds / 3600)) * direction.sign
Com a classe DMS, é possível desenhar a fronteira do Colorado definindo os quatro locais de LatLng
nos cantos e renderizando-os como Polygon
s. Adicione o seguinte código a MountainMap.kt
:
@Composable
@GoogleMapComposable
fun ColoradoPolygon() {
val north = 41.0
val south = 37.0
val east = DMS(WEST, 102.0, 3.0).toDecimalDegrees()
val west = DMS(WEST, 109.0, 3.0).toDecimalDegrees()
val locations = listOf(
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
LatLng(north, west),
)
Polygon(
points = locations,
strokeColor = MaterialTheme.colorScheme.tertiary,
strokeWidth = 3F,
fillColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
)
}
Agora, chame ColoradoPolyon()
dentro do bloco de conteúdo GoogleMap
.
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
Agora, o app descreve o estado do Colorado enquanto o preenche sutilmente.
13. Adicionar uma camada KML e uma barra de escala
Nesta seção final, você vai descrever as diferentes cordilheiras e adicionar uma barra de escala ao mapa.
Descreva as cordilheiras
Antes, você desenhou um contorno ao redor do Colorado. Aqui, você vai adicionar formas mais complexas ao mapa. O código inicial inclui um arquivo KML (Keyhole Markup Language) que descreve aproximadamente as cordilheiras importantes. A biblioteca de utilitários do SDK do Maps para Android tem uma função para adicionar uma camada KML ao mapa. Em MountainMap.kt
, adicione uma chamada MapEffect
no bloco de conteúdo GoogleMap
depois do bloco when
. A função MapEffect
é chamada com um objeto GoogleMap
. Ele pode servir como uma ponte útil entre APIs e bibliotecas não combináveis que exigem um objeto GoogleMap
.
fun MountainMap(
// ...
) {
var isMapLoaded by remember { mutableStateOf(false) }
val context = LocalContext.current
GoogleMap(
// ...
) {
// ...
when (selectedMarkerType) {
// ...
}
// This code belongs inside the GoogleMap content block, but outside of
// the 'when' statement
MapEffect(key1 = true) {map ->
val layer = KmlLayer(map, R.raw.mountain_ranges, context)
layer.addLayerToMap()
}
}
Adicionar uma escala de mapa
Como tarefa final, você vai adicionar uma escala ao mapa. O ScaleBar
implementa um elemento combinável de escala que pode ser adicionado ao mapa. Observe que o ScaleBar
não é um
@GoogleMapComposable
e, portanto, não pode ser adicionado ao conteúdo GoogleMap
. Em vez disso, adicione-o à Box
que contém o mapa.
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
Execute o app para conferir o codelab totalmente implementado.
14. Acessar o código da solução
Para fazer o download do código do codelab concluído, use estes comandos:
- Clone o repositório se você tiver o
git
instalado.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.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
solution
do Android Studio.
15. 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 Maps para Android: crie mapas dinâmicos, interativos e personalizados, experiências geoespaciais e relacionadas aos locais para seus apps Android.
- Biblioteca Maps Compose: um conjunto de funções combináveis de código aberto e tipos de dados que podem ser usados com o Jetpack Compose para criar seu app.
- android-maps-compose: 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