Aggiungere una mappa alla tua app per Android (Kotlin con Compose)

1. Prima di iniziare

Questo codelab ti insegna a integrare Maps SDK for Android con la tua app e a utilizzare le sue funzionalità principali creando un'app che mostri una mappa delle montagne del Colorado, negli Stati Uniti, utilizzando vari tipi di indicatori. Inoltre, imparerai a disegnare altre forme sulla mappa.

Ecco come apparirà al termine del codelab:

Prerequisiti

In questo lab proverai a:

  • Attiva e utilizza la libreria Maps Compose per Maps SDK for Android per aggiungere un GoogleMap a un'app per Android
  • Aggiungere e personalizzare i segnaposto
  • Disegnare poligoni sulla mappa
  • Controllare il punto di vista della videocamera in modo programmatico

Che cosa ti serve

2. Configurazione

Per il seguente passaggio di attivazione, devi abilitare Maps SDK for Android.

Configurare Google Maps Platform

Se non hai ancora un account Google Cloud Platform e un progetto con la fatturazione abilitata, consulta la guida Guida introduttiva a Google Maps Platform per creare un account di fatturazione e un progetto.

  1. Nella console Cloud, fai clic sul menu a discesa del progetto e seleziona il progetto che vuoi utilizzare per questo codelab.

  1. Abilita le API e gli SDK di Google Maps Platform richiesti per questo codelab in Google Cloud Marketplace. Per farlo, segui i passaggi descritti in questo video o in questa documentazione.
  2. Genera una chiave API nella pagina Credenziali di Cloud Console. Puoi seguire i passaggi descritti in questo video o in questa documentazione. Tutte le richieste a Google Maps Platform richiedono una chiave API.

3. Avvio rapido

Per iniziare il più rapidamente possibile, ecco un codice iniziale che ti aiuterà a seguire questo codelab. Puoi passare direttamente alla soluzione, ma se vuoi seguire tutti i passaggi per crearla autonomamente, continua a leggere.

  1. Clona il repository se hai installato git.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

In alternativa, puoi fare clic sul seguente pulsante per scaricare il codice sorgente.

  1. Una volta ottenuto il codice, apri il progetto che si trova nella directory starter in Android Studio.

4. Aggiungere la chiave API al progetto

Questa sezione descrive come archiviare la chiave API in modo che possa essere referenziata in modo sicuro dalla tua app. Non devi archiviare la chiave API nel sistema di controllo delle versioni, pertanto ti consigliamo di archiviarla nel file secrets.properties, che verrà inserito nella copia locale della directory principale del progetto. Per ulteriori informazioni sul file secrets.properties, consulta File delle proprietà di Gradle.

Per semplificare questa attività, ti consigliamo di utilizzare il plug-in Secrets Gradle per Android.

Per installare il plug-in Secrets Gradle per Android nel tuo progetto Google Maps:

  1. In Android Studio, apri il file build.gradle.kts di primo livello e aggiungi il seguente codice all'elemento dependencies in buildscript.
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. Apri il file build.gradle.kts a livello di modulo e aggiungi il seguente codice all'elemento plugins.
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. Nel file build.gradle.kts a livello di modulo, assicurati che targetSdk e compileSdk siano impostati almeno su 34.
  4. Salva il file e sincronizza il progetto con Gradle.
  5. Apri il file secrets.properties nella directory di primo livello, quindi aggiungi il codice seguente. Sostituisci YOUR_API_KEY con la tua chiave API. Memorizza la chiave in questo file perché secrets.properties è escluso dal controllo in un sistema di controllo delle versioni.
    MAPS_API_KEY=YOUR_API_KEY
    
  6. Salva il file.
  7. Crea il file local.defaults.properties nella directory di primo livello, la stessa cartella del file secrets.properties, quindi aggiungi il seguente codice.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    Lo scopo di questo file è fornire una posizione di backup per la chiave API se il file secrets.properties non viene trovato, in modo che le build non non vadano a buon fine. Ciò si verifica quando cloni l'app da un sistema di controllo delle versioni e non hai ancora creato un file secrets.properties in locale per fornire la chiave API.
  8. Salva il file.
  9. Nel file AndroidManifest.xml, vai a com.google.android.geo.API_KEY e aggiorna l'attributo android:value. Se il tag <meta-data> non esiste, crealo come tag secondario del tag <application>.
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${MAPS_API_KEY}" />
    
  10. In Android Studio, apri il file build.gradle.kts a livello di modulo e modifica la proprietà secrets. Se la proprietà secrets non esiste, aggiungila.Modifica le proprietà del plug-in per impostare propertiesFileName su secrets.properties, defaultPropertiesFileName su local.defaults.properties e qualsiasi altra proprietà.
    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. Aggiungere Google Maps

In questa sezione aggiungerai una mappa di Google in modo che venga caricata all'avvio dell'app.

Aggiungi le dipendenze di Maps Compose

Ora che è possibile accedere alla chiave API all'interno dell'app, il passaggio successivo consiste nell'aggiungere la dipendenza dell'SDK Maps per Android al file build.gradle.kts dell'app. Per creare con Jetpack Compose, utilizza la libreria Maps Compose che fornisce elementi di Maps SDK for Android come funzioni componibili e tipi di dati.

build.gradle.kts

Nel file build.gradle.kts a livello di app, sostituisci le dipendenze Maps SDK for Android non 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")
}

con le loro controparti componibili:

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

Aggiungere un composable Google Map

In MountainMap.kt, aggiungi il composable GoogleMap all'interno del composable Box nidificato all'interno del composable 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 }
        )

        // ...
    }
}

Ora crea ed esegui l'app. Ecco fatto. Dovresti vedere una mappa centrata sulla famigerata Null Island, nota anche come latitudine zero e longitudine zero. In seguito, imparerai a posizionare la mappa nella posizione e nel livello di zoom che preferisci, ma per ora festeggia la tua prima vittoria.

6. Personalizzazione delle mappe basata su cloud

Puoi personalizzare lo stile della mappa utilizzando la personalizzazione delle mappe basata su cloud.

Creare un ID mappa

Se non hai ancora creato un ID mappa a cui è associato uno stile di mappa, consulta la guida ID mappa per completare i seguenti passaggi:

  1. Crea un ID mappa.
  2. Associa un ID mappa a uno stile di mappa.

Aggiungere l'ID mappa all'app

Per utilizzare l'ID mappa che hai creato, quando crei un'istanza del composable GoogleMap, utilizza l'ID mappa quando crei un oggetto GoogleMapOptions che viene assegnato al parametro googleMapOptionsFactory nel costruttore.

GoogleMap(
    // ...
    googleMapOptionsFactory = {
        GoogleMapOptions().mapId("MyMapId")
    }
)

Una volta completata questa operazione, esegui l'app per visualizzare la mappa nello stile che hai selezionato.

7. Caricare i dati dei marcatori

Il compito principale dell'app è caricare una raccolta di montagne dallo spazio di archiviazione locale e visualizzarle in GoogleMap. In questo passaggio, farai un tour dell'infrastruttura fornita per caricare i dati delle montagne e presentarli nell'interfaccia utente.

Montagna

La classe di dati Mountain contiene tutti i dati relativi a ogni montagna.

data class Mountain(
    val id: Int,
    val name: String,
    val location: LatLng,
    val elevation: Meters,
)

Tieni presente che le montagne verranno suddivise in base alla loro elevazione. Le montagne alte almeno 4200 metri sono chiamate quattordicimila. Il codice iniziale include una funzione di estensione che esegue questo controllo per te.

/**
 * 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

La classe MountainsScreenViewState contiene tutti i dati necessari per il rendering della visualizzazione. Può essere in stato Loading o MountainList a seconda che l'elenco delle montagne sia stato caricato.

/**
 * 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()
}

Classi fornite: MountainsRepository e MountainsViewModel

Nel progetto iniziale, la classe MountainsRepository è già stata fornita. Questa classe legge un elenco di luoghi di montagna archiviati in un file GPS Exchange Format o GPX, top_peaks.gpx. La chiamata a mountainsRepository.loadMountains() restituisce un 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 è una classe ViewModel che carica le raccolte di montagne ed espone queste raccolte, nonché altre parti dello stato dell'interfaccia utente tramite mountainsScreenViewState. mountainsScreenViewState è un hot StateFlow che la UI può osservare come stato modificabile utilizzando la funzione di estensione collectAsState.

Seguendo solidi principi di architettura, MountainsViewModel contiene tutto lo stato dell'app. L'interfaccia utente invia le interazioni dell'utente al modello di visualizzazione utilizzando il metodo 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 vuoi saperne di più sull'implementazione di queste classi, puoi accedervi su GitHub o aprire le classi MountainsRepository e MountainsViewModel in Android Studio.

Utilizzare il ViewModel

Il modello di visualizzazione viene utilizzato in MainActivity per ottenere viewState. Utilizzerai viewState per visualizzare i marcatori più avanti in questo codelab. Tieni presente che questo codice è già incluso nel progetto iniziale e viene mostrato qui solo a scopo di riferimento.

val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value

8. Posizionare la videocamera

Un GoogleMap predefinito è centrato sulla latitudine zero e sulla longitudine zero. I segnaposto che visualizzerai si trovano nello stato del Colorado, negli Stati Uniti. Il viewState fornito dal modello di visualizzazione presenta un LatLngBounds che contiene tutti i marcatori.

In MountainMap.kt crea un CameraPositionState inizializzato al centro del riquadro di delimitazione. Imposta il parametro cameraPositionState di GoogleMap sulla variabile cameraPositionState che hai appena creato.

fun MountainMap(
    // ...
) {
    // ...
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
    }

    GoogleMap(
        // ...
        cameraPositionState = cameraPositionState,
    )
}

Ora esegui il codice e guarda la mappa centrata sul Colorado.

Zoom sull'estensione del marcatore

Per mettere a fuoco la mappa sugli indicatori, aggiungi la funzione zoomAll alla fine del file MountainMap.kt. Tieni presente che questa funzione richiede un CoroutineScope perché l'animazione della videocamera in una nuova posizione è un'operazione asincrona che richiede tempo per essere completata.

fun zoomAll(
    scope: CoroutineScope,
    cameraPositionState: CameraPositionState,
    boundingBox: LatLngBounds
) {
    scope.launch {
        cameraPositionState.animate(
            update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
            durationMs = 1000
        )
    }
}

Successivamente, aggiungi il codice per richiamare la funzione zoomAll ogni volta che cambiano i limiti intorno alla raccolta di marcatori o quando l'utente fa clic sul pulsante di estensione dello zoom nella barra delle app in alto. Tieni presente che il pulsante di estensione dello zoom è già collegato per inviare eventi al modello di visualizzazione. Devi solo raccogliere questi eventi dal modello di visualizzazione e chiamare la funzione zoomAll in risposta.

Pulsante Estensioni

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

Ora, quando esegui l'app, la mappa si avvia con la messa a fuoco sull'area in cui verranno posizionati i marker. Puoi riposizionare e modificare lo zoom e, se fai clic sul pulsante di estensione dello zoom, la mappa si focalizzerà nuovamente sull'area dei marker. Un bel passo avanti. Ma la mappa dovrebbe avere qualcosa da mostrare. Ed è quello che farai nel passaggio successivo.

9. Indicatori di base

In questo passaggio, aggiungi indicatori alla mappa che rappresentano i punti di interesse che vuoi evidenziare. Utilizzerai l'elenco di montagne fornito nel progetto iniziale e aggiungerai questi luoghi come indicatori sulla mappa.

Per iniziare, aggiungi un blocco di contenuti al GoogleMap. Esistono più tipi di marcatori, quindi aggiungi un'istruzione when per passare a ogni tipo e implementali a turno nei passaggi successivi.

GoogleMap(
    // ...
) {
    when (selectedMarkerType) {
        MarkerType.Basic -> {
            BasicMarkersMapContent(
                mountains = viewState.mountains,
            )
        }

        MarkerType.Advanced -> {
            AdvancedMarkersMapContent(
                mountains = viewState.mountains,
            )
        }

        MarkerType.Clustered -> {
            ClusteringMarkersMapContent(
                mountains = viewState.mountains,
            )
        }
    }
}

Aggiungi indicatori

Annota BasicMarkersMapContent con @GoogleMapComposable. Tieni presente che puoi utilizzare solo le funzioni @GoogleMapComposable nel blocco di contenuti GoogleMap. L'oggetto mountains ha un elenco di oggetti Mountain. Aggiungerai un indicatore per ogni montagna nell'elenco, utilizzando la posizione, il nome e l'altitudine dell'oggetto Mountain. La posizione viene utilizzata per impostare il parametro di stato di Marker, che a sua volta controlla la posizione del marcatore.

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

Esegui l'app e vedrai i marcatori che hai appena aggiunto.

Personalizzare gli indicatori

Esistono diverse opzioni di personalizzazione per i segnaposto che hai appena aggiunto, per metterli in evidenza e fornire informazioni utili agli utenti. In questa attività, ne esplorerai alcuni personalizzando l'immagine di ogni indicatore.

Il progetto iniziale include una funzione helper, vectorToBitmap, per creare BitmapDescriptor da un @DrawableResource.

Il codice di avvio include un'icona di montagna, baseline_filter_hdr_24.xml, che utilizzerai per personalizzare i marcatori.

La funzione vectorToBitmap converte un elemento disegnabile vettoriale in un BitmapDescriptor da utilizzare con la libreria Maps. I colori delle icone vengono impostati utilizzando un'istanza 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 {
    // ...
}

Utilizza la funzione vectorToBitmap per creare due BitmapDescriptor personalizzati: uno per le montagne di 4000 metri e uno per le montagne normali. Quindi, utilizza il parametro icon del componente componibile Marker per impostare l'icona. Imposta anche il parametro anchor per modificare la posizione dell'ancora rispetto all'icona. L'utilizzo del centro funziona meglio per queste icone circolari.

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

Esegui l'app e ammira gli indicatori personalizzati. Attiva/disattiva l'opzione Show all per visualizzare l'intera catena montuosa. Le montagne avranno indicatori diversi a seconda che siano quattordicimila piedi.

10. Indicatori avanzati

Gli AdvancedMarker aggiungono funzionalità extra a Markers di base. In questo passaggio, imposterai il comportamento di collisione e configurerai lo stile del segnaposto.

Aggiungi @GoogleMapComposable alla funzione AdvancedMarkersMapContent. Esegui il loop su mountains aggiungendo un AdvancedMarker per ciascuno.

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

Nota il parametro collisionBehavior. Se imposti questo parametro su REQUIRED_AND_HIDES_OPTIONAL, il tuo indicatore sostituirà qualsiasi indicatore con priorità inferiore. Puoi notarlo se aumenti lo zoom su un indicatore di base rispetto a un indicatore avanzato. Il marcatore di base probabilmente avrà sia il tuo marcatore sia il marcatore posizionato nella stessa posizione nella mappa di base. Il marcatore avanzato farà in modo che il marcatore con priorità inferiore venga nascosto.

Esegui l'app per visualizzare gli indicatori avanzati. Assicurati di selezionare la scheda Advanced markers nella riga di navigazione in basso.

AdvancedMarkers personalizzato

Le icone utilizzano le combinazioni di colori primario e secondario per distinguere i fourteeners dalle altre montagne. Utilizza la funzione vectorToBitmap per creare due BitmapDescriptor: uno per le montagne di oltre 4000 metri e uno per le altre montagne. Utilizza queste icone per creare un pinConfig personalizzato per ogni tipo. Infine, applica il pin al AdvancedMarker corrispondente in base alla funzione 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. Indicatori raggruppati

In questo passaggio, utilizzerai il componente componibile Clustering per aggiungere il raggruppamento degli elementi in base allo zoom.

Il composable Clustering richiede una raccolta di ClusterItem. MountainClusterItem implementa l'interfaccia ClusterItem. Aggiungi questo corso al file 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
}

Ora aggiungi il codice per creare MountainClusterItem dall'elenco di montagne. Tieni presente che questo codice utilizza un UnitsConverter per la conversione in unità di visualizzazione appropriate per l'utente in base alle impostazioni locali. Questa operazione viene configurata in MainActivity utilizzando un 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,
    )
}

Con questo codice, i marcatori vengono raggruppati in cluster in base al livello di zoom. Tutto in ordine.

Personalizzare i cluster

Come per gli altri tipi di indicatori, gli indicatori raggruppati sono personalizzabili. Il parametro clusterItemContent del componibile Clustering imposta un blocco componibile personalizzato per il rendering di un elemento non raggruppato. Implementa una funzione @Composable per creare il marker. La funzione SingleMountain esegue il rendering di un elemento componibile Material 3 Icon con una combinazione di colori di sfondo personalizzata.

In ClusteringMarkersMapContent.kt, crea una classe di dati che definisca la combinazione di colori per un indicatore:

data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)

Inoltre, in ClusteringMarkersMapContent.kt crea una funzione componibile per eseguire il rendering di un'icona per una determinata combinazione di colori:

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

Ora crea una combinazione di colori per le montagne di 14.000 piedi e un'altra per le altre montagne. Nel blocco clusterItemContent, seleziona la combinazione di colori in base al fatto che la montagna specificata sia un quattordicimila o meno.

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

Ora esegui l'app per visualizzare le versioni personalizzate dei singoli elementi.

12. Disegna sulla mappa

Anche se hai già esplorato un modo per disegnare sulla mappa (aggiungendo indicatori), Maps SDK for Android supporta molti altri modi per disegnare e visualizzare informazioni utili sulla mappa.

Ad esempio, se vuoi rappresentare percorsi e aree sulla mappa, puoi utilizzare Polyline e Polygon per visualizzarli sulla mappa. In alternativa, se vuoi fissare un'immagine alla superficie del terreno, puoi utilizzare un GroundOverlay.

In questa attività, imparerai a disegnare forme, in particolare un contorno intorno allo stato del Colorado. Il confine del Colorado è definito tra 37° N e 41° N di latitudine e 102°03' O e 109°03' O di longitudine. In questo modo, disegnare il contorno è piuttosto semplice.

Il codice iniziale include una classe DMS per la conversione dalla notazione in gradi, minuti e secondi a gradi decimali.

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

Con la classe DMS, puoi disegnare il confine del Colorado definendo le quattro posizioni LatLng degli angoli e visualizzandole come Polygon. Aggiungi il seguente codice 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),
    )
}

Ora chiama ColoradoPolyon() all'interno del blocco di contenuti GoogleMap.

@Composable
fun MountainMap(
    // ...
) {
   Box(
    // ...
    ) {
        GoogleMap(
            // ...
        ) {
            ColoradoPolygon()
        }
    }
}

Ora l'app delinea lo stato del Colorado, riempiendolo in modo sottile.

13. Aggiungere un livello KML e una barra della scala

In questa sezione finale, delineerai approssimativamente le diverse catene montuose e aggiungerai una scala alla mappa.

Delinea le catene montuose

In precedenza, hai disegnato un contorno intorno al Colorado. Qui aggiungerai forme più complesse alla mappa. Il codice iniziale include un file Keyhole Markup Language (KML) che delinea approssimativamente le catene montuose più importanti. La libreria di utilità di Maps SDK for Android ha una funzione per aggiungere un livello KML alla mappa. In MountainMap.kt aggiungi una chiamata MapEffect nel blocco di contenuti GoogleMap dopo il blocco when. La funzione MapEffect viene chiamata con un oggetto GoogleMap. Può fungere da ponte utile tra API e librerie non componibili che richiedono un oggetto 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()
      }
    }

Aggiungere una scala della mappa

Come ultima attività, aggiungerai una scala alla mappa. ScaleBar implementa un composable di scala che può essere aggiunto alla mappa. Tieni presente che ScaleBar non è un

@GoogleMapComposable e pertanto non può essere aggiunto ai contenuti di GoogleMap. Invece, aggiungilo al Box che contiene la mappa.

Box(
  // ...
) {
    GoogleMap(
      // ...
    ) {
        // ...
    }

    ScaleBar(
        modifier = Modifier
            .padding(top = 5.dp, end = 15.dp)
            .align(Alignment.TopEnd),
        cameraPositionState = cameraPositionState
    )
    // ...
}

Esegui l'app per visualizzare il codelab completamente implementato.

14. Recuperare il codice della soluzione

Per scaricare il codice del codelab completato, puoi utilizzare questi comandi:

  1. Clona il repository se hai installato git.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

In alternativa, puoi fare clic sul seguente pulsante per scaricare il codice sorgente.

  1. Una volta ottenuto il codice, apri il progetto che si trova nella directory solution in Android Studio.

15. Complimenti

Complimenti! Hai esaminato molti contenuti e ci auguriamo che tu abbia una migliore comprensione delle funzionalità principali offerte in Maps SDK for Android.

Scopri di più

  • Maps SDK for Android: crea mappe, posizioni ed esperienze geospaziali dinamiche, interattive e personalizzate per le tue app per Android.
  • Libreria Maps Compose: un insieme di funzioni componibili e tipi di dati open source che puoi utilizzare con Jetpack Compose per creare la tua app.
  • android-maps-compose: codice di esempio su GitHub che mostra tutte le funzionalità trattate in questo codelab e altro ancora.
  • Altri codelab Kotlin per la creazione di app per Android con Google Maps Platform