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

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, die eine Karte mit Fahrradgeschäften in San Francisco, Kalifornien, USA, anzeigt.

f05e1ca27ff42bf6.png

Vorbereitung

  • Grundkenntnisse in Kotlin und Android-Entwicklung

Aufgaben

  • Aktivieren und verwenden Sie das Maps SDK for Android, um Google Maps in eine Android-App einzufügen.
  • Markierungen hinzufügen, anpassen und gruppieren
  • Polylinien und Polygone auf der Karte zeichnen
  • Den Blickwinkel der Kamera 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, aber wenn Sie alle Schritte nachvollziehen möchten, um sie selbst zu erstellen, lesen Sie weiter.

  1. Klonen Sie das Repository, wenn Sie git installiert haben.
git clone https://github.com/googlecodelabs/maps-platform-101-android.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. Google Maps hinzufügen

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

d1d068b5d4ae38b9.png

Eigenen API-Schlüssel hinzufügen

Der API-Schlüssel, den Sie in einem vorherigen Schritt erstellt haben, muss der App zur Verfügung gestellt werden, damit das Maps SDK for Android Ihren Schlüssel mit Ihrer App verknüpfen kann.

  1. Öffnen Sie dazu die Datei local.properties im Stammverzeichnis Ihres Projekts (auf derselben Ebene wie gradle.properties und settings.gradle).
  2. Definieren Sie in dieser Datei einen neuen Schlüssel GOOGLE_MAPS_API_KEY mit dem von Ihnen erstellten API-Schlüssel als Wert.

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

Beachten Sie, dass local.properties in der Datei .gitignore im Git-Repository aufgeführt ist. Das liegt daran, dass Ihr API-Schlüssel als vertrauliche Information gilt und nach Möglichkeit nicht in die Quellcodeverwaltung eingecheckt werden sollte.

  1. Als Nächstes müssen Sie das Secrets Gradle-Plug-in für Android in die build.gradle-Datei Ihrer App im Verzeichnis app/ einfügen und die folgende Zeile in den plugins-Block einfügen, damit Ihre API in der gesamten App verwendet werden kann:

build.gradle-Datei auf App-Ebene

plugins {
    // ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

Außerdem müssen Sie die Datei build.gradle auf Projektebene so ändern, dass sie den folgenden Klassenpfad enthält:

build.gradle-Datei auf Projektebene

buildscript {
    dependencies {
        // ...
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
    }
}

Dieses Plug-in stellt Schlüssel, die Sie in der Datei local.properties definiert haben, beim Build-Vorgang als Build-Variablen in der Android-Manifestdatei und als Variablen in der von Gradle generierten BuildConfig-Klasse zur Verfügung. Durch die Verwendung dieses Plug-ins wird der Boilerplate-Code entfernt, der ansonsten erforderlich wäre, um Eigenschaften aus local.properties zu lesen, damit in Ihrer gesamten App darauf zugegriffen werden kann.

Google Maps-Abhängigkeit hinzufügen

  1. 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-Datei Ihrer App einfügen.

Im Starterprojekt dieses Codelabs wurde diese Abhängigkeit bereits für Sie hinzugefügt.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. Fügen Sie als Nächstes in AndroidManifest.xml ein neues meta-data-Tag hinzu, um den API-Schlüssel zu übergeben, den Sie in einem früheren Schritt erstellt haben. Öffnen Sie dazu diese Datei in Android Studio und fügen Sie das folgende meta-data-Tag in das application-Objekt in der Datei AndroidManifest.xml ein, die sich in app/src/main befindet.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. Erstellen Sie als Nächstes im Verzeichnis app/src/main/res/layout/ eine neue Layoutdatei mit dem Namen activity_main.xml und definieren Sie sie so:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <fragment
       class="com.google.android.gms.maps.SupportMapFragment"
       android:id="@+id/map_fragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</FrameLayout>

Dieses Layout hat ein einzelnes FrameLayout, das ein SupportMapFragment enthält. Dieses Fragment enthält das zugrunde liegende GoogleMaps-Objekt, das Sie in späteren Schritten verwenden.

  1. Aktualisieren Sie zum Schluss die Klasse MainActivity in app/src/main/java/com/google/codelabs/buildyourfirstmap, indem Sie den folgenden Code hinzufügen, um die Methode onCreate zu überschreiben. So können Sie ihren Inhalt mit dem neuen Layout festlegen, das Sie gerade erstellt haben.

MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Führen Sie die App nun aus. Die Karte sollte jetzt auf dem Bildschirm Ihres Geräts geladen werden.

5. Cloudbasiertes Gestalten von Karteninhalten (optional)

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 zur App hinzufügen

Wenn Sie die erstellte Karten-ID verwenden möchten, ändern Sie die Datei activity_main.xml und übergeben Sie Ihre Karten-ID im Attribut map:mapId des SupportMapFragment.

activity_main.xml

<fragment xmlns:map="http://schemas.android.com/apk/res-auto"
    class="com.google.android.gms.maps.SupportMapFragment"
    <!-- ... -->
    map:mapId="YOUR_MAP_ID" />

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

6. Markierungen hinzufügen

In dieser Aufgabe fügen Sie der Karte Markierungen hinzu, die POIs darstellen, die Sie auf der Karte hervorheben möchten. Zuerst rufen Sie eine Liste von Orten ab, die im Starterprojekt für Sie bereitgestellt wurden, und fügen diese Orte dann der Karte hinzu. In diesem Beispiel sind das Fahrradgeschäfte.

bc5576877369b554.png

Referenz auf GoogleMap abrufen

Zuerst müssen Sie einen Verweis auf das GoogleMap-Objekt abrufen, damit Sie seine Methoden verwenden können. Fügen Sie dazu den folgenden Code in Ihre MainActivity.onCreate()-Methode direkt nach dem Aufruf von setContentView() ein:

MainActivity.onCreate()

val mapFragment = supportFragmentManager.findFragmentById(   
    R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
    addMarkers(googleMap)
}

In der Implementierung wird zuerst die SupportMapFragment gesucht, die Sie im vorherigen Schritt hinzugefügt haben. Dazu wird die Methode findFragmentById() für das SupportFragmentManager-Objekt verwendet. Sobald ein Verweis abgerufen wurde, wird der getMapAsync()-Aufruf mit einem Lambda aufgerufen. An diese Lambda-Funktion wird das GoogleMap-Objekt übergeben. Innerhalb dieses Lambda wird der Methodenaufruf addMarkers() aufgerufen, der kurz definiert wird.

Bereitgestellte Klasse: PlacesReader

Im Starterprojekt ist die Klasse PlacesReader bereits vorhanden. Diese Klasse liest eine Liste mit 49 Orten, die in einer JSON-Datei mit dem Namen places.json gespeichert sind, und gibt sie als List<Place> zurück. Die Orte selbst stellen eine Liste von Fahrradgeschäften in der Umgebung von San Francisco, Kalifornien, USA dar.

Wenn Sie mehr über die Implementierung dieser Klasse erfahren möchten, können Sie sie auf GitHub aufrufen oder die Klasse PlacesReader in Android Studio öffnen.

PlacesReader

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import com.google.codelabs.buildyourfirstmap.R
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader

/**
* Reads a list of place JSON objects from the file places.json
*/
class PlacesReader(private val context: Context) {

   // GSON object responsible for converting from JSON to a Place object
   private val gson = Gson()

   // InputStream representing places.json
   private val inputStream: InputStream
       get() = context.resources.openRawResource(R.raw.places)

   /**
    * Reads the list of place JSON objects in the file places.json
    * and returns a list of Place objects
    */
   fun read(): List<Place> {
       val itemType = object : TypeToken<List<PlaceResponse>>() {}.type
       val reader = InputStreamReader(inputStream)
       return gson.fromJson<List<PlaceResponse>>(reader, itemType).map {
           it.toPlace()
       }
   }

Orte laden

Wenn Sie die Liste der Fahrradgeschäfte laden möchten, fügen Sie in MainActivity eine Property namens places hinzu und definieren Sie sie so:

MainActivity.places

private val places: List<Place> by lazy {
   PlacesReader(this).read()
}

Mit diesem Code wird die Methode read() für ein PlacesReader aufgerufen, das ein List<Place> zurückgibt. Ein Place hat ein Attribut namens name, den Namen des Orts, und ein latLng, die Koordinaten des Orts.

Ort

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: LatLng,
   val rating: Float
)

Markierungen zur Karte hinzufügen

Nachdem die Liste der Orte in den Arbeitsspeicher geladen wurde, müssen sie auf der Karte dargestellt werden.

  1. Erstellen Sie in MainActivity eine Methode mit dem Namen addMarkers() und definieren Sie sie so:

MainActivity.addMarkers()

/**
* Adds marker representations of the places list on the provided GoogleMap object
*/
private fun addMarkers(googleMap: GoogleMap) {
   places.forEach { place ->
       val marker = googleMap.addMarker(
           MarkerOptions()
               .title(place.name)
               .position(place.latLng)
       )
   }
}

Diese Methode durchläuft die Liste der places und ruft dann die Methode addMarker() für das bereitgestellte GoogleMap-Objekt auf. Die Markierung wird durch Instanziieren eines MarkerOptions-Objekts erstellt. So können Sie die Markierung selbst anpassen. In diesem Fall werden der Titel und die Position der Markierung angegeben, die jeweils für den Namen des Fahrradgeschäfts und seine Koordinaten stehen.

  1. Führen Sie die App aus und fahren Sie nach San Francisco, um die Markierungen zu sehen, die Sie gerade hinzugefügt haben.

7. 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 sehen wir uns einige davon an. Sie passen das Bild jeder Markierung sowie das Informationsfenster an, das angezeigt wird, wenn auf eine Markierung getippt wird.

a26f82802fe838e9.png

Infofenster hinzufügen

Standardmäßig werden im Infofenster, das angezeigt wird, wenn Sie auf eine Markierung tippen, der Titel und das Snippet der Markierung (falls festgelegt) angezeigt. Sie können diese so anpassen, dass zusätzliche Informationen angezeigt werden, z. B. die Adresse und die Bewertung des Orts.

marker_info_contents.xml erstellen

Erstellen Sie zuerst eine neue Layoutdatei mit dem Namen marker_info_contents.xml.

  1. Klicken Sie dazu in der Projektansicht in Android Studio mit der rechten Maustaste auf den Ordner app/src/main/res/layout und wählen Sie Neu > Layout Resource File (Layout-Ressourcendatei) aus.

8cac51fcbef9171b.png

  1. Geben Sie im Dialogfeld marker_info_contents in das Feld Dateiname und LinearLayout in das Feld Root element ein und klicken Sie dann auf OK.

8783af12baf07a80.png

Diese Layoutdatei wird später aufgebläht, um den Inhalt des Infofensters darzustellen.

  1. Kopieren Sie den Inhalt des folgenden Code-Snippets, mit dem drei TextViews in einer vertikalen LinearLayout-Ansichtsgruppe hinzugefügt werden, und überschreiben Sie den Standardcode in der Datei.

marker_info_contents.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:orientation="vertical"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:gravity="center_horizontal"
   android:padding="8dp">

   <TextView
       android:id="@+id/text_view_title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="18sp"
       android:textStyle="bold"
       tools:text="Title"/>

   <TextView
       android:id="@+id/text_view_address"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="123 Main Street"/>

   <TextView
       android:id="@+id/text_view_rating"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="Rating: 3"/>

</LinearLayout>

InfoWindowAdapter implementieren

Nachdem Sie die Layoutdatei für das benutzerdefinierte Infofenster erstellt haben, müssen Sie als Nächstes die Schnittstelle GoogleMap.InfoWindowAdapter implementieren. Diese Schnittstelle enthält zwei Methoden: getInfoWindow() und getInfoContents(). Beide Methoden geben ein optionales View-Objekt zurück. Mit der ersten Methode wird das Fenster selbst angepasst, mit der zweiten der Inhalt. In Ihrem Fall implementieren Sie beide und passen die Rückgabe von getInfoContents() an, während Sie in getInfoWindow() „null“ zurückgeben. Das bedeutet, dass das Standardzeitfenster verwendet werden soll.

  1. Erstellen Sie eine neue Kotlin-Datei namens MarkerInfoWindowAdapter im selben Paket wie MainActivity. Klicken Sie dazu in der Projektansicht in Android Studio mit der rechten Maustaste auf den Ordner app/src/main/java/com/google/codelabs/buildyourfirstmap und wählen Sie Neu > Kotlin-Datei/Klasse aus.

3975ba36eba9f8e1.png

  1. Geben Sie im Dialogfeld MarkerInfoWindowAdapter ein und lassen Sie Datei markiert.

992235af53d3897f.png

  1. Kopieren Sie den Inhalt des folgenden Code-Snippets in die neue Datei.

MarkerInfoWindowAdapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.codelabs.buildyourfirstmap.place.Place

class MarkerInfoWindowAdapter(
    private val context: Context
) : GoogleMap.InfoWindowAdapter {
   override fun getInfoContents(marker: Marker?): View? {
       // 1. Get tag
       val place = marker?.tag as? Place ?: return null

       // 2. Inflate view and set title, address, and rating
       val view = LayoutInflater.from(context).inflate(
           R.layout.marker_info_contents, null
       )
       view.findViewById<TextView>(
           R.id.text_view_title
       ).text = place.name
       view.findViewById<TextView>(
           R.id.text_view_address
       ).text = place.address
       view.findViewById<TextView>(
           R.id.text_view_rating
       ).text = "Rating: %.2f".format(place.rating)

       return view
   }

   override fun getInfoWindow(marker: Marker?): View? {
       // Return null to indicate that the 
       // default window (white bubble) should be used
       return null
   }
}

Im Inhalt der Methode getInfoContents() wird der bereitgestellte Marker in der Methode in den Typ Place umgewandelt. Wenn die Umwandlung nicht möglich ist, gibt die Methode „null“ zurück. Sie haben das Attribut „tag“ für Marker noch nicht festgelegt, aber das tun Sie im nächsten Schritt.

Als Nächstes wird das Layout marker_info_contents.xml aufgeblasen und dann der Text im Container TextViews auf das Tag Place festgelegt.

MainActivity aktualisieren

Um alle bisher erstellten Komponenten zu verbinden, müssen Sie Ihrer MainActivity-Klasse zwei Zeilen hinzufügen.

Um die benutzerdefinierten InfoWindowAdapter- und MarkerInfoWindowAdapter-Parameter im getMapAsync-Methodenaufruf zu übergeben, rufen Sie zuerst die setInfoWindowAdapter()-Methode für das GoogleMap-Objekt auf und erstellen Sie eine neue Instanz von MarkerInfoWindowAdapter.

  1. Fügen Sie dazu den folgenden Code nach dem addMarkers()-Methodenaufruf im getMapAsync()-Lambda ein.

MainActivity.onCreate()

// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

Schließlich müssen Sie jeden Ort als Tag-Eigenschaft für jede Markierung festlegen, die der Karte hinzugefügt wird.

  1. Ändern Sie dazu den places.forEach{}-Aufruf in der Funktion addMarkers() so:

MainActivity.addMarkers()

places.forEach { place ->
   val marker = googleMap.addMarker(
       MarkerOptions()
           .title(place.name)
           .position(place.latLng)
           .icon(bicycleIcon)
   )

   // Set place as the tag on the marker object so it can be referenced within
   // MarkerInfoWindowAdapter
   marker.tag = place
}

Benutzerdefiniertes Markierungsbild hinzufügen

Das Markierungsbild anzupassen ist eine gute Möglichkeit, den Typ des Orts, den die Markierung auf Ihrer Karte darstellt, zu kommunizieren. In diesem Schritt werden Fahrräder anstelle der roten Standardmarkierungen verwendet, um die einzelnen Geschäfte auf der Karte darzustellen. Das Starterprojekt enthält das Fahrradsymbol ic_directions_bike_black_24dp.xml in app/src/res/drawable, das Sie verwenden.

6eb7358bb61b0a88.png

Benutzerdefinierte Bitmap für Markierung festlegen

Nachdem Sie das Vektorsymbol für das Fahrrad haben, müssen Sie es als Symbol für jede Markierung auf der Karte festlegen. MarkerOptions hat eine Methode icon, die ein BitmapDescriptor akzeptiert, mit dem Sie dies erreichen können.

Zuerst müssen Sie die gerade hinzugefügte Vektordrawable in ein BitmapDescriptor konvertieren. Eine Datei namens BitMapHelper, die im Starterprojekt enthalten ist, enthält eine Hilfsfunktion namens vectorToBitmap(), die genau das tut.

BitmapHelper

package com.google.codelabs.buildyourfirstmap

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory

object BitmapHelper {
   /**
    * Demonstrates converting a [Drawable] to a [BitmapDescriptor], 
    * for use as a marker icon. Taken from ApiDemos on GitHub:
    * https://github.com/googlemaps/android-samples/blob/main/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MarkerDemoActivity.kt
    */
   fun vectorToBitmap(
      context: Context,
      @DrawableRes id: Int, 
      @ColorInt color: Int
   ): BitmapDescriptor {
       val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, null)
       if (vectorDrawable == null) {
           Log.e("BitmapHelper", "Resource not found")
           return BitmapDescriptorFactory.defaultMarker()
       }
       val bitmap = Bitmap.createBitmap(
           vectorDrawable.intrinsicWidth,
           vectorDrawable.intrinsicHeight,
           Bitmap.Config.ARGB_8888
       )
       val canvas = Canvas(bitmap)
       vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
       DrawableCompat.setTint(vectorDrawable, color)
       vectorDrawable.draw(canvas)
       return BitmapDescriptorFactory.fromBitmap(bitmap)
   }
}

Diese Methode nimmt eine Context, eine ID für eine zeichnungsfähige Ressource sowie eine Farbganzzahl entgegen und erstellt eine BitmapDescriptor-Darstellung davon.

Deklarieren Sie mit der Hilfsmethode ein neues Attribut namens bicycleIcon und geben Sie ihm die folgende Definition: MainActivity.bicycleIcon

private val bicycleIcon: BitmapDescriptor by lazy {
   val color = ContextCompat.getColor(this, R.color.colorPrimary)
   BitmapHelper.vectorToBitmap(this, R.drawable.ic_directions_bike_black_24dp, color)
}

Für diese Property wird die vordefinierte Farbe colorPrimary in Ihrer App verwendet, um das Fahrradsymbol zu tönen und als BitmapDescriptor zurückzugeben.

  1. Rufen Sie mit dieser Eigenschaft die Methode icon von MarkerOptions in der Methode addMarkers() auf, um die Symbolanpassung abzuschließen. Die Markierungseigenschaft sollte dann so aussehen:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. Führen Sie die App aus, um die aktualisierten Markierungen zu sehen.

8. Markierungs-Clustering

Je nachdem, wie weit Sie in die Karte hineingezoomt haben, haben Sie vielleicht bemerkt, dass sich die von Ihnen hinzugefügten Markierungen überlappen. Überlappende Markierungen sind sehr schwer zu bedienen und verursachen viel Rauschen, was die Nutzerfreundlichkeit Ihrer App beeinträchtigt.

68591edc86d73724.png

Um die Nutzerfreundlichkeit zu verbessern, empfiehlt es sich, Markierungscluster zu implementieren, wenn Sie ein großes Dataset haben, das eng geclustert ist. Beim Ein- und Auszoomen der Karte werden Markierungen, die sich in unmittelbarer Nähe befinden, so gruppiert:

f05e1ca27ff42bf6.png

Dazu benötigen Sie die Maps SDK for Android-Dienstprogrammbibliothek.

Maps SDK for Android-Dienstprogrammbibliothek

Die Maps SDK for Android-Dienstprogrammbibliothek wurde entwickelt, um die Funktionalität des Maps SDK for Android zu erweitern. Es bietet erweiterte Funktionen wie Marker-Clustering, Heatmaps, KML- und GeoJSON-Unterstützung, Polyline-Codierung und -Decodierung sowie eine Reihe von Hilfsfunktionen für die sphärische Geometrie.

build.gradle-Datei aktualisieren

Da die Dienstprogrammbibliothek separat vom Maps SDK for Android verpackt ist, müssen Sie Ihrer build.gradle-Datei eine zusätzliche Abhängigkeit hinzufügen.

  1. Aktualisieren Sie den Abschnitt dependencies Ihrer Datei app/build.gradle.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. Nachdem Sie diese Zeile hinzugefügt haben, müssen Sie das Projekt synchronisieren, um die neuen Abhängigkeiten abzurufen.

b7b030ec82c007fd.png

Clustering implementieren

So implementieren Sie Clustering in Ihrer App:

  1. Implementieren Sie die ClusterItem-Schnittstelle.
  2. Leiten Sie die Klasse DefaultClusterRenderer ab.
  3. Erstellen Sie ein ClusterManager und fügen Sie Elemente hinzu.

ClusterItem-Schnittstelle implementieren

Alle Objekte, die eine clusterfähige Markierung auf der Karte darstellen, müssen die ClusterItem-Schnittstelle implementieren. In Ihrem Fall bedeutet das, dass das Place-Modell ClusterItem entsprechen muss. Öffnen Sie die Datei Place.kt und nehmen Sie die folgenden Änderungen vor:

Ort

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: String,
   val rating: Float
) : ClusterItem {
   override fun getPosition(): LatLng =
       latLng

   override fun getTitle(): String =
       name

   override fun getSnippet(): String =
       address
}

Die ClusterItem-Klasse definiert diese drei Methoden:

  • getPosition(), das für die LatLng des Orts steht.
  • getTitle(), das für den Namen des Orts steht
  • getSnippet(), das für die Adresse des Orts steht.

Unterklasse der DefaultClusterRenderer-Klasse erstellen

Die Klasse, die für die Implementierung des Clusterings zuständig ist, ClusterManager, verwendet intern eine ClusterRenderer-Klasse, um die Cluster zu erstellen, während Sie die Karte schwenken und zoomen. Standardmäßig ist der Standard-Renderer DefaultClusterRenderer enthalten, der ClusterRenderer implementiert. Für einfache Fälle sollte das ausreichen. Da Markierungen in Ihrem Fall angepasst werden müssen, müssen Sie diese Klasse erweitern und die Anpassungen dort hinzufügen.

Erstellen Sie die Kotlin-Datei PlaceRenderer.kt im Paket com.google.codelabs.buildyourfirstmap.place und definieren Sie sie so:

PlaceRenderer

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.codelabs.buildyourfirstmap.BitmapHelper
import com.google.codelabs.buildyourfirstmap.R
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer

/**
* A custom cluster renderer for Place objects.
*/
class PlaceRenderer(
   private val context: Context,
   map: GoogleMap,
   clusterManager: ClusterManager<Place>
) : DefaultClusterRenderer<Place>(context, map, clusterManager) {

   /**
    * The icon to use for each cluster item
    */
   private val bicycleIcon: BitmapDescriptor by lazy {
       val color = ContextCompat.getColor(context,
           R.color.colorPrimary
       )
       BitmapHelper.vectorToBitmap(
           context,
           R.drawable.ic_directions_bike_black_24dp,
           color
       )
   }

   /**
    * Method called before the cluster item (the marker) is rendered.
    * This is where marker options should be set.
    */
   override fun onBeforeClusterItemRendered(
      item: Place,
      markerOptions: MarkerOptions
   ) {
       markerOptions.title(item.name)
           .position(item.latLng)
           .icon(bicycleIcon)
   }

   /**
    * Method called right after the cluster item (the marker) is rendered.
    * This is where properties for the Marker object should be set.
    */
   override fun onClusterItemRendered(clusterItem: Place, marker: Marker) {
       marker.tag = clusterItem
   }
}

In dieser Klasse werden die folgenden beiden Funktionen überschrieben:

  • onBeforeClusterItemRendered() wird aufgerufen, bevor der Cluster auf der Karte gerendert wird. Hier können Sie Anpassungen über MarkerOptions vornehmen. In diesem Fall werden Titel, Position und Symbol der Markierung festgelegt.
  • onClusterItemRenderer(), die direkt nach dem Rendern der Markierung auf der Karte aufgerufen wird. Hier können Sie auf das erstellte Marker-Objekt zugreifen. In diesem Fall wird damit die Tag-Eigenschaft der Markierung festgelegt.

ClusterManager erstellen und Elemente hinzufügen

Damit das Clustering funktioniert, müssen Sie MainActivity so ändern, dass eine ClusterManager instanziiert und die erforderlichen Abhängigkeiten bereitgestellt werden. ClusterManager kümmert sich intern um das Hinzufügen der Markierungen (der ClusterItem-Objekte). Anstatt Markierungen direkt auf der Karte hinzuzufügen, wird diese Aufgabe also an ClusterManager delegiert. Außerdem wird durch ClusterManager intern auch setInfoWindowAdapter() aufgerufen. Das Festlegen eines benutzerdefinierten Infofensters muss also für das MarkerManager.Collection-Objekt von ClusterManger erfolgen.

  1. Ändern Sie zuerst den Inhalt des Lambda-Ausdrucks im getMapAsync()-Aufruf in MainActivity.onCreate(). Kommentieren Sie den Aufruf von addMarkers() und setInfoWindowAdapter() aus und rufen Sie stattdessen eine Methode namens addClusteredMarkers() auf, die Sie als Nächstes definieren.

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
    //addMarkers(googleMap)
    addClusteredMarkers(googleMap)

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Als Nächstes definieren Sie addClusteredMarkers() in MainActivity.

MainActivity.addClusteredMarkers()

/**
* Adds markers to the map with clustering support.
*/
private fun addClusteredMarkers(googleMap: GoogleMap) {
   // Create the ClusterManager class and set the custom renderer.
   val clusterManager = ClusterManager<Place>(this, googleMap)
   clusterManager.renderer =
       PlaceRenderer(
           this,
           googleMap,
           clusterManager
       )

   // Set custom info window adapter
   clusterManager.markerCollection.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

   // Add the places to the ClusterManager.
   clusterManager.addItems(places)
   clusterManager.cluster()

   // Set ClusterManager as the OnCameraIdleListener so that it
   // can re-cluster when zooming in and out.
   googleMap.setOnCameraIdleListener {
       clusterManager.onCameraIdle()
   }
}

Mit dieser Methode wird ein ClusterManager instanziiert, der benutzerdefinierte Renderer PlacesRenderer übergeben, alle Orte hinzugefügt und die Methode cluster() aufgerufen. Da ClusterManager die setInfoWindowAdapter()-Methode für das Kartenobjekt verwendet, muss das benutzerdefinierte Infofenster für das ClusterManager.markerCollection-Objekt festgelegt werden. Da sich das Clustering ändern soll, wenn der Nutzer die Karte schwenkt und zoomt, wird OnCameraIdleListener für googleMap bereitgestellt. Wenn die Kamera inaktiv wird, wird clusterManager.onCameraIdle() aufgerufen.

  1. Führen Sie die App aus, um die neuen gruppierten Geschäfte zu sehen.

9. 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 Polylinien und Polygone verwenden. Wenn Sie ein Bild auf der Erdoberfläche fixieren möchten, können Sie Boden-Overlays verwenden.

In dieser Aufgabe lernen Sie, wie Sie Formen, insbesondere einen Kreis, um eine Markierung zeichnen, wenn darauf getippt wird.

f98ce13055430352.png

Klick-Listener hinzufügen

Normalerweise fügen Sie einer Markierung einen Klick-Listener hinzu, indem Sie über setOnMarkerClickListener() einen Klick-Listener direkt für das GoogleMap-Objekt übergeben. Da Sie jedoch Clustering verwenden, muss der Klick-Listener stattdessen für ClusterManager bereitgestellt werden.

  1. Fügen Sie in der Methode addClusteredMarkers() in MainActivity die folgende Zeile direkt nach dem Aufruf von cluster() hinzu.

MainActivity.addClusteredMarkers()

// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
   addCircle(googleMap, item)
   return@setOnClusterItemClickListener false
}

Mit dieser Methode wird ein Listener hinzugefügt und die Methode addCircle() aufgerufen, die Sie als Nächstes definieren. Schließlich wird false von dieser Methode zurückgegeben, um anzugeben, dass dieses Ereignis nicht verarbeitet wurde.

  1. Als Nächstes müssen Sie das Attribut circle und die Methode addCircle() in MainActivity definieren.

MainActivity.addCircle()

private var circle: Circle? = null

/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
   circle?.remove()
   circle = googleMap.addCircle(
       CircleOptions()
           .center(item.latLng)
           .radius(1000.0)
           .fillColor(ContextCompat.getColor(this, R.color.colorPrimaryTranslucent))
           .strokeColor(ContextCompat.getColor(this, R.color.colorPrimary))
   )
}

Das Attribut circle ist so festgelegt, dass bei jedem Tippen auf eine neue Markierung der vorherige Kreis entfernt und ein neuer Kreis hinzugefügt wird. Die API zum Hinzufügen eines Kreises ähnelt der zum Hinzufügen einer Markierung.

  1. Führen Sie die App jetzt aus, um die Änderungen zu sehen.

10. Kamerasteuerung

Als letzte Aufgabe sehen Sie sich einige Kamerasteuerelemente an, damit Sie den Blick auf eine bestimmte Region richten können.

Kamera und Ansicht

Wenn Sie die App ausführen, wird auf der Kamera der Kontinent Afrika angezeigt. Sie müssen dann mühsam schwenken und zoomen, um die von Ihnen hinzugefügten Markierungen in San Francisco zu finden. Das kann eine unterhaltsame Möglichkeit sein, die Welt zu erkunden, ist aber nicht hilfreich, wenn Sie die Markierungen sofort anzeigen möchten.

Dazu können Sie die Position der Kamera programmatisch so festlegen, dass die Ansicht dort zentriert wird, wo Sie sie haben möchten.

  1. Fügen Sie dem getMapAsync()-Aufruf den folgenden Code hinzu, um die Kameraansicht so anzupassen, dass sie beim Start der App auf San Francisco initialisiert wird.

MainActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
   // Ensure all places are visible in the map.
   googleMap.setOnMapLoadedCallback {
       val bounds = LatLngBounds.builder()
       places.forEach { bounds.include(it.latLng) }
       googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
   }
}

Zuerst wird setOnMapLoadedCallback() aufgerufen, damit die Kamera erst aktualisiert wird, nachdem die Karte geladen wurde. Dieser Schritt ist erforderlich, da die Karteneigenschaften wie Dimensionen berechnet werden müssen, bevor ein Kamera-Update-Aufruf erfolgt.

Im Lambda wird ein neues LatLngBounds-Objekt erstellt, das einen rechteckigen Bereich auf der Karte definiert. Sie wird inkrementell erstellt, indem alle LatLng-Werte für Orte einbezogen werden, damit alle Orte innerhalb der Grenzen liegen. Sobald dieses Objekt erstellt wurde, wird die moveCamera()-Methode für GoogleMap aufgerufen und ein CameraUpdate wird über CameraUpdateFactory.newLatLngBounds(bounds.build(), 20) bereitgestellt.

  1. Führen Sie die App aus. Die Kamera wird jetzt in San Francisco initialisiert.

Kameraänderungen beobachten

Sie können nicht nur die Kameraposition ändern, sondern auch auf Kamera-Updates reagieren, wenn sich der Nutzer auf der Karte bewegt. Das kann nützlich sein, wenn Sie die Benutzeroberfläche anpassen möchten, während sich die Kamera bewegt.

Sie ändern den Code, damit die Markierungen durchscheinend werden, wenn die Kamera bewegt wird.

  1. Fügen Sie in der Methode addClusteredMarkers() die folgenden Zeilen am Ende der Methode hinzu:

MainActivity.addClusteredMarkers()

// When the camera starts moving, change the alpha value of the marker to translucent.
googleMap.setOnCameraMoveStartedListener {
   clusterManager.markerCollection.markers.forEach { it.alpha = 0.3f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 0.3f }
}

Dadurch wird ein OnCameraMoveStartedListener hinzugefügt. Wenn sich die Kamera bewegt, werden die Alphawerte aller Markierungen (sowohl Cluster als auch einzelne Markierungen) in 0.3f geändert, sodass die Markierungen durchscheinend dargestellt werden.

  1. Wenn Sie die durchscheinenden Markierungen wieder in undurchsichtige Markierungen ändern möchten, wenn die Kamera stoppt, ändern Sie den Inhalt von setOnCameraIdleListener in der Methode addClusteredMarkers() in Folgendes:

MainActivity.addClusteredMarkers()

googleMap.setOnCameraIdleListener {
   // When the camera stops moving, change the alpha value back to opaque.
   clusterManager.markerCollection.markers.forEach { it.alpha = 1.0f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 1.0f }

   // Call clusterManager.onCameraIdle() when the camera stops moving so that reclustering
   // can be performed when the camera stops moving.
   clusterManager.onCameraIdle()
}
  1. Führen Sie die App aus, um die Ergebnisse zu sehen.

11. Maps KTX

Für Kotlin-Apps, die ein oder mehrere Google Maps Platform Android SDKs verwenden, sind Kotlin-Erweiterungs- oder KTX-Bibliotheken verfügbar, mit denen Sie Kotlin-Sprachfunktionen wie Koroutinen, Erweiterungseigenschaften/-funktionen usw. nutzen können. Für jedes Google Maps SDK gibt es eine entsprechende KTX-Bibliothek, wie unten dargestellt:

Google Maps Platform – KTX-Diagramm

In dieser Aufgabe verwenden Sie die Maps KTX- und Maps Utils KTX-Bibliotheken in Ihrer App und überarbeiten die Implementierungen der vorherigen Aufgaben, damit Sie Kotlin-spezifische Sprachfunktionen in Ihrer App verwenden können.

  1. KTX-Abhängigkeiten in die Datei „build.gradle“ auf App-Ebene einfügen

Da in der App sowohl das Maps SDK for Android als auch die Maps SDK for Android-Dienstprogrammbibliothek verwendet werden, müssen Sie die entsprechenden KTX-Bibliotheken für diese Bibliotheken einbinden. In dieser Aufgabe verwenden Sie auch eine Funktion aus der AndroidX Lifecycle KTX-Bibliothek. Fügen Sie diese Abhängigkeit daher auch in die build.gradle-Datei auf App-Ebene ein.

build.gradle

dependencies {
    // ...

    // Maps SDK for Android KTX Library
    implementation 'com.google.maps.android:maps-ktx:3.0.0'

    // Maps SDK for Android Utility Library KTX Library
    implementation 'com.google.maps.android:maps-utils-ktx:3.0.0'

    // Lifecycle Runtime KTX Library
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}
  1. Erweiterungsfunktionen „GoogleMap.addMarker()“ und „GoogleMap.addCircle()“ verwenden

Die Maps KTX-Bibliothek bietet eine API-Alternative im DSL-Stil für die GoogleMap.addMarker(MarkerOptions) und GoogleMap.addCircle(CircleOptions), die in den vorherigen Schritten verwendet wurden. Für die Verwendung der oben genannten APIs ist die Erstellung einer Klasse mit Optionen für eine Markierung oder einen Kreis erforderlich. Bei den KTX-Alternativen können Sie die Markierungs- oder Kreisoptionen in der bereitgestellten Lambda-Funktion festlegen.

Wenn Sie diese APIs verwenden möchten, aktualisieren Sie die Methoden MainActivity.addMarkers(GoogleMap) und MainActivity.addCircle(GoogleMap):

MainActivity.addMarkers(GoogleMap)

/**
 * Adds markers to the map. These markers won't be clustered.
 */
private fun addMarkers(googleMap: GoogleMap) {
    places.forEach { place ->
        val marker = googleMap.addMarker {
            title(place.name)
            position(place.latLng)
            icon(bicycleIcon)
        }
        // Set place as the tag on the marker object so it can be referenced within
        // MarkerInfoWindowAdapter
        marker.tag = place
    }
}

MainActivity.addCircle(GoogleMap)

/**
 * Adds a [Circle] around the provided [item]
 */
private fun addCircle(googleMap: GoogleMap, item: Place) {
    circle?.remove()
    circle = googleMap.addCircle {
        center(item.latLng)
        radius(1000.0)
        fillColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryTranslucent))
        strokeColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
    }
}

Die oben genannten Methoden auf diese Weise neu zu schreiben, ist viel prägnanter. Das ist dank des Funktionsliterals mit Empfänger von Kotlin möglich.

  1. Die suspend-Funktionen SupportMapFragment.awaitMap() und GoogleMap.awaitMapLoad() verwenden

Die Maps KTX-Bibliothek bietet auch Erweiterungen für suspend-Funktionen, die in Koroutinen verwendet werden können. Konkret gibt es Alternativen für die suspend-Funktionen für SupportMapFragment.getMapAsync(OnMapReadyCallback) und GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). Wenn Sie diese alternativen APIs verwenden, müssen Sie keine Callbacks übergeben. Stattdessen können Sie die Antwort dieser Methoden seriell und synchron empfangen.

Da es sich bei diesen Methoden um suspend-Funktionen handelt, muss ihre Verwendung innerhalb einer Coroutine erfolgen. Die Lifecycle Runtime KTX-Bibliothek bietet eine Erweiterung, mit der lebenszyklusbezogene Coroutine-Scopes bereitgestellt werden können, sodass Coroutines beim entsprechenden Lebenszyklusereignis ausgeführt und beendet werden.

Kombinieren Sie diese Konzepte und aktualisieren Sie die Methode MainActivity.onCreate(Bundle):

MainActivity.onCreate(Bundle)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val mapFragment =
        supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment
    lifecycleScope.launchWhenCreated {
        // Get map
        val googleMap = mapFragment.awaitMap()

        // Wait for map to finish loading
        googleMap.awaitMapLoad()

        // Ensure all places are visible in the map
        val bounds = LatLngBounds.builder()
        places.forEach { bounds.include(it.latLng) }
        googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))

        addClusteredMarkers(googleMap)
    }
}

Der lifecycleScope.launchWhenCreated-Coroutine-Scope führt den Block aus, wenn sich die Aktivität mindestens im Status „created“ befindet. Die Aufrufe zum Abrufen des GoogleMap-Objekts und zum Warten auf das vollständige Laden der Karte wurden durch SupportMapFragment.awaitMap() bzw. GoogleMap.awaitMapLoad() ersetzt. Durch das Refactoring von Code mit diesen suspend-Funktionen können Sie den entsprechenden Callback-basierten Code sequenziell schreiben.

  1. Erstellen Sie die App mit den refaktorierten Änderungen neu.

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

  • Places SDK for Android: Nutzen Sie die umfangreichen Ortsdaten, um Unternehmen in Ihrer Nähe zu finden.
  • android-maps-ktx: Eine Open-Source-Bibliothek, mit der Sie das Maps SDK for Android und die Maps SDK for Android-Dienstprogrammbibliothek auf Kotlin-freundliche Weise einbinden können.
  • android-place-ktx: Eine Open-Source-Bibliothek, mit der Sie das Places SDK for Android auf Kotlin-freundliche Weise einbinden können.
  • android-samples: Beispielcode auf GitHub, der alle in diesem Codelab behandelten Funktionen und mehr veranschaulicht.
  • Weitere Kotlin-Codelabs für die Entwicklung von Android-Apps mit der Google Maps Platform