Karte in Ihre Android-App einfügen (Kotlin mit Compose)

1. Vorbereitung

In diesem Codelab erfahren Sie, wie Sie das Maps SDK for Android in Ihre App einbinden und die wichtigsten Funktionen nutzen. Dazu erstellen Sie eine App, in der eine Karte von Bergen in Colorado, USA, mit verschiedenen Arten von Markierungen angezeigt wird. Außerdem erfahren Sie, wie Sie andere Formen auf der Karte zeichnen.

So sieht es aus, wenn Sie das Codelab abgeschlossen haben:

Vorbereitung

Aufgaben

  • Maps Compose-Bibliothek für das Maps SDK for Android aktivieren und verwenden, um einer Android-App eine GoogleMap hinzuzufügen
  • Markierungen hinzufügen und anpassen
  • Polygone auf der Karte zeichnen
  • Kamerablickwinkel programmatisch steuern

Voraussetzungen

2. Einrichten

Im nächsten Schritt müssen Sie das Maps SDK for Android aktivieren.

Google Maps Platform einrichten

Wenn Sie noch kein Google Cloud-Konto und kein Projekt mit aktivierter Abrechnung haben, lesen Sie bitte den Leitfaden Erste Schritte mit Google Maps Platform, um ein Rechnungskonto und ein Projekt zu erstellen.

  1. Klicken Sie in der Cloud Console auf das Drop-down-Menü für das Projekt und wählen Sie das Projekt aus, das Sie für dieses Codelab verwenden möchten.

  1. Aktivieren Sie die für dieses Codelab erforderlichen APIs und SDKs der Google Maps Platform im Google Cloud Marketplace. Folgen Sie dazu der Anleitung in diesem Video oder dieser Dokumentation.
  2. Generieren Sie einen API-Schlüssel in der Cloud Console auf der Seite Anmeldedaten. Folgen Sie dazu dieser Anleitung oder dieser Dokumentation. Für alle Anfragen an die Google Maps Platform ist ein API-Schlüssel erforderlich.

3. Schnelleinstieg

Damit Sie so schnell wie möglich loslegen können, finden Sie hier einige Startcodes, die Ihnen helfen, diesem Codelab zu folgen. Sie können direkt zur Lösung springen. Wenn Sie jedoch alle Schritte nachvollziehen möchten, um die Lösung selbst zu erstellen, lesen Sie weiter.

  1. Klonen Sie das Repository, wenn Sie git installiert haben.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

Alternativ können Sie auf die folgende Schaltfläche klicken, um den Quellcode herunterzuladen.

  1. Nachdem Sie den Code erhalten haben, öffnen Sie das Projekt im Verzeichnis starter in Android Studio.

4. API-Schlüssel zum Projekt hinzufügen

In diesem Abschnitt wird beschrieben, wie Sie Ihren API-Schlüssel speichern, damit er von Ihrer App sicherer referenziert werden kann. Er sollte nicht in Ihrem Versionsverwaltungssystem eingecheckt werden. Stattdessen empfehlen wir Ihnen, ihn in der Datei secrets.properties zu speichern, die in Ihrer lokalen Kopie des Stammverzeichnisses Ihres Projekts abgelegt wird. Weitere Informationen zur Datei secrets.properties finden Sie unter Gradle-Attributdateien.

Sie können das Secrets Gradle-Plug-in für Android verwenden, um diese Aufgabe zu optimieren.

So installieren Sie das Secrets Gradle-Plug-in für Android in Ihrem Google Maps-Projekt:

  1. Öffnen Sie in Android Studio die Datei build.gradle.kts auf oberster Ebene und fügen Sie den folgenden Code in das dependencies-Element unter buildscript ein.
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. Öffnen Sie die Datei build.gradle.kts auf Modulebene und fügen Sie dem plugins-Element den folgenden Code hinzu.
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. In der Datei build.gradle.kts auf Modulebene müssen targetSdk und compileSdk auf mindestens 34 gesetzt sein.
  4. Speichern Sie die Datei und synchronisieren Sie Ihr Projekt mit Gradle.
  5. Öffnen Sie die Datei secrets.properties im Verzeichnis der obersten Ebene und fügen Sie den folgenden Code ein. Ersetzen Sie dabei YOUR_API_KEY durch Ihren eigenen API-Schlüssel. Speichern Sie den Schlüssel in dieser Datei, da secrets.properties nicht in ein Versionsverwaltungssystem eingecheckt werden kann.
    MAPS_API_KEY=YOUR_API_KEY
    
  6. Speichern Sie die Datei.
  7. Erstellen Sie die Datei local.defaults.properties im Verzeichnis der obersten Ebene, im selben Ordner wie die Datei secrets.properties, und fügen Sie folgenden Code ein.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    Diese Datei ist ein Ersatzspeicherort für den API-Schlüssel, damit Builds nicht fehlschlagen, falls die Datei secrets.properties nicht gefunden wird. Das kann passieren, wenn Sie die App aus einem Versionsverwaltungssystem klonen und noch keine lokale Datei secrets.properties erstellt haben, um Ihren API-Schlüssel bereitzustellen.
  8. Speichern Sie die Datei.
  9. Gehen Sie in der Datei AndroidManifest.xml zu com.google.android.geo.API_KEY und aktualisieren Sie das Attribut android:value. Falls das <meta-data>-Tag nicht vorhanden ist, erstellen Sie es als untergeordnetes Element des <application>-Tags.
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${MAPS_API_KEY}" />
    
  10. Öffnen Sie in Android Studio die Datei build.gradle.kts auf Modulebene und bearbeiten Sie das Attribut secrets. Wenn die Property secrets nicht vorhanden ist, fügen Sie sie hinzu.Bearbeiten Sie die Eigenschaften des Plug-ins, um propertiesFileName auf secrets.properties, defaultPropertiesFileName auf local.defaults.properties und alle anderen Eigenschaften festzulegen.
    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. Google Maps hinzufügen

In diesem Abschnitt fügen Sie eine Google-Karte hinzu, die beim Starten der App geladen wird.

Maps Compose-Abhängigkeiten hinzufügen

Nachdem auf Ihren API-Schlüssel in der App zugegriffen werden kann, müssen Sie als Nächstes die Maps SDK for Android-Abhängigkeit in die build.gradle.kts-Datei Ihrer App einfügen. Wenn Sie Jetpack Compose verwenden möchten, nutzen Sie die Maps Compose-Bibliothek, die Elemente des Maps SDK for Android als zusammensetzbare Funktionen und Datentypen bereitstellt.

build.gradle.kts

Ersetzen Sie in der Datei build.gradle.kts auf App-Ebene die Maps SDK for Android-Abhängigkeiten, die nicht auf Compose basieren:

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

mit ihren zusammensetzbaren Pendants:

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

Composable für Google Maps hinzufügen

Fügen Sie in MountainMap.kt die GoogleMap-Composable in die Box-Composable ein, die in der MapMountain-Composable verschachtelt ist.

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

        // ...
    }
}

Erstellen Sie nun die App und führen Sie sie aus. Es sollte eine Karte mit dem berüchtigten Null Island (auch als Breiten- und Längengrad 0 bezeichnet) in der Mitte angezeigt werden. Später erfahren Sie, wie Sie die Karte auf den gewünschten Standort und die gewünschte Zoomstufe ausrichten. Jetzt können Sie aber erst einmal Ihren ersten Erfolg feiern.

6. Cloudbasiertes Gestalten von Karteninhalten

Sie können den Stil Ihrer Karte mit cloudbasiertem Gestalten von Karteninhalten anpassen.

Karten-ID erstellen

Wenn Sie noch keine Karten-ID mit einem zugehörigen Kartenstil erstellt haben, folgen Sie der Anleitung unter Karten-IDs, um die folgenden Schritte auszuführen:

  1. Erstellen Sie eine Karten-ID.
  2. Verknüpfen Sie eine Karten-ID mit einem Kartenstil.

Karten-ID in App einbinden

Wenn Sie die von Ihnen erstellte Karten-ID verwenden möchten, geben Sie sie beim Instanziieren der zusammensetzbaren Funktion GoogleMap an, indem Sie ein GoogleMapOptions-Objekt erstellen, das dem Parameter googleMapOptionsFactory im Konstruktor zugewiesen wird.

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

Wenn Sie das erledigt haben, können Sie die App ausführen, um Ihre Karte im ausgewählten Stil zu sehen.

7. Markierungsdaten laden

Die Hauptaufgabe der App besteht darin, eine Sammlung von Bergen aus dem lokalen Speicher zu laden und diese Berge in der GoogleMap anzuzeigen. In diesem Schritt sehen Sie sich die bereitgestellte Infrastruktur zum Laden der Berggipfeldaten und zur Darstellung in der Benutzeroberfläche an.

Berg

Die Datenklasse Mountain enthält alle Daten zu den einzelnen Bergen.

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

Die Berge werden später nach ihrer Höhe partitioniert. Berge, die mindestens 14.000 Fuß hoch sind, werden als Fourteeners bezeichnet. Der Startcode enthält eine Erweiterungsfunktion, die diese Prüfung für Sie durchführt.

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

Die Klasse MountainsScreenViewState enthält alle Daten, die zum Rendern der Ansicht erforderlich sind. Der Status kann entweder Loading oder MountainList sein, je nachdem, ob die Liste der Berge geladen wurde.

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

Bereitgestellte Klassen: MountainsRepository und MountainsViewModel

Im Starterprojekt ist die Klasse MountainsRepository bereits vorhanden. Diese Klasse liest eine Liste von Bergorten, die in einer GPS Exchange Format- oder GPX-Datei (top_peaks.gpx) gespeichert sind. Der Aufruf von mountainsRepository.loadMountains() gibt ein StateFlow<List<Mountain>> zurück.

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 ist eine ViewModel-Klasse, die die Sammlungen von Bergen lädt und diese Sammlungen sowie andere Teile des UI-Zustands über mountainsScreenViewState bereitstellt. mountainsScreenViewState ist ein heißen StateFlow, der von der Benutzeroberfläche als veränderlicher Status mit der Erweiterungsfunktion collectAsState beobachtet werden kann.

Gemäß den Prinzipien der Softwarearchitektur enthält MountainsViewModel den gesamten Status der App. Die Benutzeroberfläche sendet Nutzerinteraktionen mit der Methode onEvent an das View-Modell.

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

Wenn Sie mehr über die Implementierung dieser Klassen erfahren möchten, können Sie auf GitHub darauf zugreifen oder die Klassen MountainsRepository und MountainsViewModel in Android Studio öffnen.

ViewModel verwenden

Das Ansichtsmodell wird in MainActivity verwendet, um den viewState abzurufen. Sie verwenden viewState später in diesem Codelab, um die Markierungen zu rendern. Dieser Code ist bereits im Starterprojekt enthalten und wird hier nur als Referenz angezeigt.

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

8. Kamera positionieren

Bei der Standardeinstellung für GoogleMap wird die Mitte auf den Breiten- und Längengrad 0 gesetzt. Die Markierungen, die Sie rendern, befinden sich im US-Bundesstaat Colorado. Die von der Ansicht bereitgestellte viewState enthält eine LatLngBounds-Klasse, die alle Markierungen enthält.

Erstellen Sie in MountainMap.kt ein CameraPositionState, das auf den Mittelpunkt des Begrenzungsrahmens initialisiert wird. Legen Sie den Parameter cameraPositionState des GoogleMap auf die Variable cameraPositionState fest, die Sie gerade erstellt haben.

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

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

Führen Sie den Code jetzt aus. Die Karte wird auf Colorado zentriert.

Auf die Ausdehnung der Markierung zoomen

Damit die Karte wirklich auf die Markierungen fokussiert wird, fügen Sie die Funktion zoomAll am Ende der Datei MountainMap.kt hinzu. Für diese Funktion ist ein CoroutineScope erforderlich, da die Animation der Kamera an einen neuen Ort ein asynchroner Vorgang ist, der Zeit in Anspruch nimmt.

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

Fügen Sie als Nächstes Code hinzu, um die Funktion zoomAll aufzurufen, wenn sich die Grenzen um die Markierungssammlung ändern oder wenn der Nutzer in der Top-App-Leiste auf die Schaltfläche „Zoomgrenzen“ klickt. Die Schaltfläche zum Zoomen ist bereits so konfiguriert, dass Ereignisse an das Ansichtsmodell gesendet werden. Sie müssen diese Ereignisse nur aus dem Ansichtsmodell erfassen und als Reaktion darauf die Funktion zoomAll aufrufen.

Schaltfläche „Umfang“

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

Wenn Sie die App jetzt ausführen, wird die Karte auf den Bereich fokussiert, in dem die Markierungen platziert werden. Sie können die Karte neu positionieren und den Zoom ändern. Wenn Sie auf die Schaltfläche „Zoomgrenzen“ klicken, wird die Karte wieder auf den Bereich mit den Markierungen fokussiert. Das ist ein Fortschritt! Auf der Karte sollte aber auch etwas zu sehen sein. Das ist der nächste Schritt.

9. Einfache Markierungen

In diesem Schritt fügen Sie der Karte Markierungen hinzu, die POIs darstellen, die Sie auf der Karte hervorheben möchten. Sie verwenden die Liste der Berge, die im Starterprojekt enthalten ist, und fügen diese Orte als Markierungen auf der Karte hinzu.

Fügen Sie zuerst einen Inhaltsblock in GoogleMap ein. Es gibt mehrere Markertypen. Fügen Sie daher eine when-Anweisung hinzu, um zu jedem Typ zu verzweigen. Die einzelnen Typen werden dann in den folgenden Schritten implementiert.

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

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

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

Markierungen hinzufügen

Kommentieren Sie BasicMarkersMapContent mit @GoogleMapComposable. Sie können im Inhaltsblock GoogleMap nur @GoogleMapComposable-Funktionen verwenden. Das mountains-Objekt enthält eine Liste von Mountain-Objekten. Sie fügen für jeden Berg in dieser Liste eine Markierung hinzu und verwenden dazu den Ort, den Namen und die Höhe aus dem Mountain-Objekt. Der Standort wird verwendet, um den Statusparameter von Marker festzulegen, der wiederum die Position der Markierung steuert.

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

Führen Sie die App aus. Sie sehen dann die Markierungen, die Sie gerade hinzugefügt haben.

Markierungen anpassen

Es gibt verschiedene Anpassungsoptionen für Markierungen, die Sie gerade hinzugefügt haben. So können Sie sie hervorheben und Nutzern nützliche Informationen liefern. In dieser Aufgabe passen Sie das Bild für jede Markierung an.

Das Starterprojekt enthält die Hilfsfunktion vectorToBitmap, mit der BitmapDescriptors aus einem @DrawableResource erstellt werden.

Der Starter-Code enthält ein Bergsymbol, baseline_filter_hdr_24.xml, mit dem Sie die Markierungen anpassen können.

Die Funktion vectorToBitmap konvertiert eine Vektorgrafik in ein BitmapDescriptor zur Verwendung mit der Maps-Bibliothek. Die Symbolfarben werden mit einer BitmapParameters-Instanz festgelegt.

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

Verwenden Sie die Funktion vectorToBitmap, um zwei benutzerdefinierte BitmapDescriptors zu erstellen: eine für Berge mit einer Höhe von mindestens 14.000 Fuß und eine für normale Berge. Verwenden Sie dann den Parameter icon der zusammensetzbaren Funktion Marker, um das Symbol festzulegen. Mit dem Parameter anchor können Sie die Ankerposition relativ zum Symbol ändern. Bei diesen kreisförmigen Symbolen ist es besser, den Mittelpunkt zu verwenden.

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

Führen Sie die App aus und sehen Sie sich die benutzerdefinierten Markierungen an. Stellen Sie den Schalter Show all auf „Ein“, um alle Berge zu sehen. Die Berge haben je nachdem, ob es sich um einen Viertausender handelt, unterschiedliche Markierungen.

10. Erweiterte Markierungen

AdvancedMarkers bieten zusätzliche Funktionen für die grundlegenden Markers. In diesem Schritt legen Sie das Kollisionsverhalten fest und konfigurieren den Pin-Stil.

Fügen Sie @GoogleMapComposable zur Funktion AdvancedMarkersMapContent hinzu. Wiederholen Sie die mountains und fügen Sie für jede ein AdvancedMarker hinzu.

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

Beachten Sie den Parameter collisionBehavior. Wenn Sie diesen Parameter auf REQUIRED_AND_HIDES_OPTIONAL setzen, wird Ihre Markierung durch Markierungen mit niedrigerer Priorität ersetzt. Das lässt sich gut erkennen, wenn Sie einen einfachen Marker mit einem erweiterten Marker vergleichen. Die Standardmarkierung befindet sich wahrscheinlich an derselben Stelle wie Ihre Markierung auf der Basiskarte. Die erweiterte Markierung führt dazu, dass die Markierung mit niedrigerer Priorität ausgeblendet wird.

Führen Sie die App aus, um die erweiterten Markierungen zu sehen. Achten Sie darauf, dass Sie in der unteren Navigationsleiste den Tab Advanced markers auswählen.

Angepasste AdvancedMarkers

Die Symbole verwenden die primären und sekundären Farbschemas, um zwischen den „Fourteeners“ und anderen Bergen zu unterscheiden. Verwenden Sie die Funktion vectorToBitmap, um zwei BitmapDescriptors zu erstellen: eines für die Berge mit einer Höhe von mindestens 14.000 Fuß und eines für die anderen Berge. Erstellen Sie mit diesen Symbolen für jeden Typ ein benutzerdefiniertes pinConfig. Wenden Sie den Pin schließlich auf die entsprechende AdvancedMarker basierend auf der Funktion is14er() an.

@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. Marker-Cluster

In diesem Schritt verwenden Sie die zusammensetzbare Funktion Clustering, um die zoomabhängige Gruppierung von Elementen hinzuzufügen.

Für die zusammensetzbare Funktion Clustering ist eine Sammlung von ClusterItem-Objekten erforderlich. MountainClusterItem implementiert die ClusterItem-Schnittstelle. Fügen Sie diese Klasse in die Datei ClusteringMarkersMapContent.kt ein.

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
}

Fügen Sie nun den Code hinzu, um MountainClusterItem-Objekte aus der Liste der Berge zu erstellen. In diesem Code wird UnitsConverter verwendet, um die Anzeigeeinheiten basierend auf dem Gebietsschema des Nutzers in die für ihn geeigneten Einheiten umzuwandeln. Dies wird in MainActivity mit einem CompositionLocal eingerichtet.

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

Mit diesem Code werden die Markierungen basierend auf der Zoomstufe gruppiert. Schön ordentlich!

Cluster anpassen

Wie bei den anderen Markierungstypen können Sie auch Cluster-Markierungen anpassen. Mit dem Parameter clusterItemContent der zusammensetzbaren Funktion Clustering wird ein benutzerdefinierter zusammensetzbarer Block zum Rendern eines nicht gruppierten Elements festgelegt. Implementieren Sie eine @Composable-Funktion, um die Markierung zu erstellen. Mit der Funktion SingleMountain wird ein zusammensetzbares Material 3-Element Icon mit einem benutzerdefinierten Hintergrundfarbschema gerendert.

Erstellen Sie in ClusteringMarkersMapContent.kt eine Datenklasse, die das Farbschema für eine Markierung definiert:

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

Erstellen Sie außerdem in ClusteringMarkersMapContent.kt eine zusammensetzbare Funktion zum Rendern eines Symbols für ein bestimmtes Farbschema:

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

Erstellen Sie nun ein Farbschema für die Berge mit einer Höhe von mindestens 14.000 Fuß und ein weiteres Farbschema für die anderen Berge. Wählen Sie im clusterItemContent-Block das Farbschema aus, je nachdem, ob der angegebene Berg ein Viertausender ist oder nicht.

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

Führen Sie die App jetzt aus, um angepasste Versionen der einzelnen Elemente zu sehen.

12. Auf Karten zeichnen

Sie haben bereits eine Möglichkeit kennengelernt, auf der Karte zu zeichnen (durch Hinzufügen von Markierungen). Das Maps SDK for Android unterstützt jedoch zahlreiche weitere Möglichkeiten, um nützliche Informationen auf der Karte darzustellen.

Wenn Sie beispielsweise Routen und Gebiete auf der Karte darstellen möchten, können Sie Polyline und Polygon verwenden, um diese auf der Karte anzuzeigen. Wenn Sie ein Bild auf der Erdoberfläche fixieren möchten, können Sie ein GroundOverlay verwenden.

In dieser Aufgabe lernen Sie, wie Sie Formen zeichnen, insbesondere eine Umrisslinie um den Bundesstaat Colorado. Die Grenze von Colorado liegt zwischen 37° N und 41° N und zwischen 102°03′ W und 109°03′ W. Das macht das Zeichnen des Umrisses recht einfach.

Der Startcode enthält die DMS-Klasse, mit der die Notation in Grad, Minuten und Sekunden in Dezimalgrad umgewandelt werden kann.

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

Mit der DMS-Klasse können Sie die Grenze von Colorado zeichnen, indem Sie die vier Eckpunkte LatLng definieren und als Polygon rendern. Fügen Sie den folgenden Code zu MountainMap.kt hinzu.

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

Rufen Sie nun ColoradoPolyon() innerhalb des Inhaltsblocks GoogleMap auf.

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

Die App zeigt nun den Umriss des US-Bundesstaates Colorado mit einer dezenten Füllung an.

13. KML-Ebene und Maßstabsleiste hinzufügen

In diesem letzten Abschnitt skizzieren Sie die verschiedenen Gebirgszüge und fügen der Karte eine Maßstabsleiste hinzu.

Gebirgsketten umreißen

Zuvor haben Sie eine Umrisslinie um Colorado gezeichnet. Hier fügen Sie der Karte komplexere Formen hinzu. Der Startcode enthält eine KML-Datei (Keyhole Markup Language), in der die wichtigsten Gebirgszüge grob umrissen sind. Die Maps SDK for Android-Dienstprogrammbibliothek enthält eine Funktion zum Hinzufügen einer KML-Ebene zur Karte. Fügen Sie in MountainMap.kt nach dem when-Block einen MapEffect-Aufruf im GoogleMap-Inhaltsblock hinzu. Die Funktion MapEffect wird mit einem GoogleMap-Objekt aufgerufen. Sie kann als nützliche Brücke zwischen nicht zusammensetzbaren APIs und Bibliotheken dienen, die ein GoogleMap-Objekt erfordern.

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

Maßstabsleiste hinzufügen

Als letzte Aufgabe fügen Sie der Karte eine Maßstabsleiste hinzu. Das ScaleBar implementiert eine zusammensetzbare Skala, die der Karte hinzugefügt werden kann. Beachten Sie, dass ScaleBar kein ist.

@GoogleMapComposable und kann daher nicht den GoogleMap-Inhalten hinzugefügt werden. Stattdessen fügen Sie sie dem Box hinzu, das die Karte enthält.

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

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

Führen Sie die App aus, um das vollständig implementierte Codelab zu sehen.

14. Lösungscode abrufen

Mit den folgenden Befehlen können Sie den Code für das fertige Codelab herunterladen:

  1. Klonen Sie das Repository, wenn Sie git installiert haben.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

Alternativ können Sie auf die folgende Schaltfläche klicken, um den Quellcode herunterzuladen.

  1. Nachdem Sie den Code erhalten haben, öffnen Sie das Projekt im Verzeichnis solution in Android Studio.

15. Glückwunsch

Glückwunsch! Wir haben viele Inhalte behandelt und hoffentlich haben Sie jetzt ein besseres Verständnis der wichtigsten Funktionen des Maps SDK for Android.

Weitere Informationen

  • Maps SDK for Android: Dynamische, interaktive, benutzerdefinierte Karten, Standortdaten und raumbezogene Daten in Ihre Android-Apps einbinden
  • Maps Compose-Bibliothek: Eine Reihe von zusammensetzbaren Open-Source-Funktionen und Datentypen, die Sie mit Jetpack Compose zum Erstellen Ihrer App verwenden können.
  • android-maps-compose: Beispielcode auf GitHub, der alle in diesem Codelab behandelten Funktionen und mehr demonstriert.
  • Weitere Kotlin-Codelabs für die Entwicklung von Android-Apps mit der Google Maps Platform