Adicionar um mapa ao seu app Android (Kotlin com Compose)

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

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

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.

  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/googlemaps-samples/codelab-maps-platform-101-compose.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 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:

  1. No Android Studio, abra o arquivo build.gradle.kts de nível superior e adicione o seguinte código ao elemento dependencies em buildscript.
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. Abra o arquivo build.gradle.kts no nível do módulo e adicione o seguinte código ao elemento plugins.
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. No arquivo build.gradle.kts no nível do módulo, defina targetSdk e compileSdk como pelo menos 34.
  4. Salve o arquivo e sincronize seu projeto com o Gradle.
  5. Abra o arquivo secrets.properties no diretório de nível superior e adicione o código a seguir. Substitua YOUR_API_KEY pela sua chave de API. Armazene sua chave nesse arquivo porque secrets.properties não é verificado em um sistema de controle de versões.
    MAPS_API_KEY=YOUR_API_KEY
    
  6. Salve o arquivo.
  7. Crie o arquivo local.defaults.properties no seu diretório de nível superior, na mesma pasta que o arquivo secrets.properties, e depois adicione o seguinte código.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    O objetivo desse arquivo é oferecer um local de backup para a chave da API se o arquivo 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 arquivo secrets.properties localmente para fornecer sua chave de API.
  8. Salve o arquivo.
  9. No seu arquivo AndroidManifest.xml, acesse com.google.android.geo.API_KEY e atualize o atributo android: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}" />
    
  10. No Android Studio, abra o arquivo build.gradle.kts no nível do módulo e edite a propriedade secrets. Se a propriedade secrets não existir, adicione-a.Edite as propriedades do plug-in para definir propertiesFileName como secrets.properties, defaultPropertiesFileName como local.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:

  1. Crie um ID do mapa.
  2. 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.

Botão &quot;Extensões&quot;

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 BitmapDescriptors 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 BitmapDescriptors 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 AdvancedMarkers 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 BitmapDescriptors, 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 ClusterItems. 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 MountainClusterItems 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 Polylines e Polygons 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 Polygons. 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:

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

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