Dodawanie mapy do aplikacji na Androida (Kotlin)

1. Zanim zaczniesz

Dzięki nim dowiesz się, jak zintegrować pakiet SDK Map na Androida z aplikacją i korzystać z najważniejszych funkcji tej usługi, tworząc aplikację wyświetlającą mapę sklepów rowerowych w Kalifornii (USA).

F05e1ca27ff42bf6.png

Wymagania wstępne

  • Podstawowa wiedza na temat programowania Kotlin i Androida

Jakie zadania wykonasz:

  • Włącz i użyj pakietu SDK Maps na Androida, aby dodać Mapy Google do aplikacji na Androida.
  • Dodawaj, zmieniaj i klastruj znaczniki.
  • Rysuj linie łamane i wielokąty na mapie.
  • Automatycznie steruj punktami widzenia kamery.

Czego potrzebujesz

2. Konfiguracja

W kolejnym kroku musisz włączyć Pakiet SDK Map Google na Androida.

Konfigurowanie Google Maps Platform

Jeśli nie masz jeszcze konta Google Cloud Platform ani projektu z włączonymi płatnościami, przeczytaj przewodnik Pierwsze kroki z Google Maps Platform, by utworzyć konto rozliczeniowe i projekt.

  1. W Cloud Console kliknij menu projektu i wybierz projekt, którego chcesz użyć w tym ćwiczeniu z programowania.

  1. Włącz interfejsy API i pakiety SDK Google Maps Platform wymagane w ramach tego ćwiczenia z ćwiczeń w Google Cloud Marketplace. W tym celu wykonaj czynności opisane w tym filmie lub w tej dokumentacji.
  2. Wygeneruj klucz interfejsu API na stronie Dane logowania w Cloud Console. Odpowiednie instrukcje znajdziesz w tym filmie lub w tej dokumentacji. Wszystkie żądania wysyłane do Google Maps Platform wymagają klucza interfejsu API.

3. Szybki start

Aby ułatwić Ci rozpoczęcie tego ćwiczenia, skorzystaj z tego kodu, który ułatwia rozpoczęcie ćwiczeń z programowania. Zachęcamy do przejścia do rozwiązania, ale jeśli chcesz samodzielnie wykonać wszystkie kroki niezbędne do jego zbudowania, czytaj dalej.

  1. Skopiuj repozytorium, jeśli masz zainstalowane narzędzie git.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

Możesz też kliknąć poniższy przycisk, aby pobrać kod źródłowy.

  1. Po pobraniu kodu otwórz projekt znajdujący się w katalogu starter w Android Studio.

4. Dodawanie Map Google

W tej sekcji dodasz Mapy Google, które będą się wczytywać podczas uruchamiania aplikacji.

d1d068b5d4ae38b9.png

Dodaj klucz interfejsu API

Klucz interfejsu API utworzony we wcześniejszym kroku należy przekazać do aplikacji, aby pakiet SDK Maps na Androida mógł powiązać go z Twoją aplikacją.

  1. Aby to zrobić, otwórz plik o nazwie local.properties w katalogu głównym projektu (na tym samym poziomie, na którym są gradle.properties i settings.gradle).
  2. W tym pliku zdefiniuj nowy klucz GOOGLE_MAPS_API_KEY, którego wartość to utworzony przez Ciebie klucz interfejsu API.

Local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

Zwróć uwagę, że plik local.properties znajduje się w pliku .gitignore w repozytorium Git. Wynika to z faktu, że klucz interfejsu API jest uważany za informacje poufne i nie należy go sprawdzać w celu kontrolowania źródła.

  1. Następnie, aby udostępnić interfejs API i korzystać z niego w całej aplikacji, umieść wtyczkę Secrets Gradle na Androida w pliku build.gradle aplikacji w katalogu app/ i dodaj ten wiersz w bloku plugins:

build.gradle na poziomie aplikacji

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

Musisz również zmodyfikować plik build.gradle na poziomie projektu tak, aby zawierał tę ścieżkę klasy:

build.gradle na poziomie projektu

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

Ta wtyczka sprawia, że klucze zdefiniowane przez Ciebie w pliku local.properties są dostępne jako zmienne kompilacji w pliku manifestu Androida i jako zmienne w klasie BuildConfig wygenerowanej przez Gradle. Używanie tej wtyczki usuwa kod stały, który w innym przypadku byłby potrzebny do odczytania właściwości w elemencie local.properties, aby był on dostępny w aplikacji.

Dodawanie zależności w Mapach Google

  1. Skoro klucz dostępu do interfejsu API jest już dostępny w aplikacji, następnym krokiem jest dodanie zależności aplikacji Maps SDK na Androida do pliku build.gradle aplikacji.

W projekcie wstępnym w ramach tego ćwiczenia z ćwiczeniami dodaliśmy tę zależność.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. Następnie dodaj nowy tag meta-data w AndroidManifest.xml, aby przekazać klucz interfejsu API utworzony we wcześniejszym kroku. Aby to zrobić, otwórz ten plik w Android Studio i dodaj ten tag meta-data do obiektu application w pliku AndroidManifest.xml znajdującym się w sekcji app/src/main.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. Następnie utwórz nowy plik układu o nazwie activity_main.xml w katalogu app/src/main/res/layout/ i zdefiniuj go w ten sposób:

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>

Ten układ zawiera pojedynczy element FrameLayout zawierający SupportMapFragment. Ten fragment zawiera obiekt GoogleMaps, którego używasz w kolejnych krokach.

  1. Na koniec zaktualizuj klasę MainActivity w obiekcie app/src/main/java/com/google/codelabs/buildyourfirstmap, dodając poniższy kod, aby zastąpić metodę onCreate i ustawić jej zawartość na nowo utworzony układ.

Główna aktywność

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. Teraz możesz uruchomić aplikację. Na ekranie urządzenia powinien pojawić się błąd ładowania mapy.

5. definiowanie stylów map w Google Cloud (opcjonalne)

Możesz dostosować styl mapy, korzystając ze stylu mapy w Google Cloud.

Utwórz identyfikator mapy

Jeśli nie masz jeszcze identyfikatora, ale nie jest on jeszcze powiązany ze stylem mapy, zapoznaj się z przewodnikiem po identyfikatorach map, by wykonać te czynności:

  1. Utwórz identyfikator mapy.
  2. Powiąż identyfikator mapy ze stylem mapy.

Dodawanie identyfikatora mapy do aplikacji

Aby użyć utworzonego identyfikatora mapy, zmodyfikuj plik activity_main.xml i przekaż identyfikator mapy w atrybucie map:mapId 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" />

Po wykonaniu tych czynności uruchom aplikację, aby zobaczyć mapę w wybranym przez siebie stylu.

6. Dodaj znaczniki

W tym zadaniu dodajesz na mapie znaczniki reprezentujące ciekawe miejsca, które chcesz wyróżnić na mapie. Najpierw pobierasz listę miejsc, które zostały Ci udostępnione w projekcie początkowym, a następnie dodajesz je do mapy. W tym przykładzie są to sklepy rowerowe.

bc5576877369b554.png

Pobierz odniesienie do GoogleMap

Najpierw musisz uzyskać odwołanie do obiektu GoogleMap, aby móc używać jego metod. Aby to zrobić, dodaj ten kod do metody MainActivity.onCreate() zaraz po wywołaniu funkcji setContentView():

MainActivity.onCreate()

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

Implementacja najpierw znajduje obiekt SupportMapFragment dodany w poprzednim kroku, korzystając z metody findFragmentById() w obiekcie SupportFragmentManager. Po uzyskaniu odniesienia następuje wywołanie wywołania getMapAsync(), po którym następuje lambda. Lambda to miejsce, do którego jest przekazywany obiekt GoogleMap. W lambdzie jest wywoływane wywołanie metody addMarkers(), która wkrótce zostanie zdefiniowana.

Podana klasa: PlacesReader

W projekcie początkowym utworzyliśmy dla Ciebie zajęcia PlacesReader. Ta klasa odczytuje listę 49 miejsc zapisanych w pliku JSON o nazwie places.json i zwraca je jako List<Place>. Te miejsca reprezentują listę sklepów rowerowych w pobliżu Krakowa.

Jeśli chcesz dowiedzieć się więcej o implementacji tych zajęć, możesz uzyskać do nich dostęp na GitHubie lub otworzyć klasę PlacesReader w Android Studio.

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

Wczytaj miejsca

Aby wczytać listę sklepów rowerowych, dodaj właściwość MainActivity o nazwie places i zdefiniuj ją w ten sposób:

GłównaAktywność.miejsca

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

Ten kod wywołuje metodę read() w obiekcie PlacesReader, która zwraca List<Place>. Place ma właściwość o nazwie name, nazwę miejsca i latLng – współrzędne miejsca.

Miejsce

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

Dodawanie znaczników do mapy

Gdy lista miejsc została już załadowana do pamięci, następnym krokiem jest zaprezentowanie tych miejsc na mapie.

  1. Utwórz metodę MainActivity o nazwie addMarkers() i zdefiniuj ją w ten sposób:

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

Ta metoda iteruje przez listę places, a następnie wywołuje metodę addMarker() w podanym obiekcie GoogleMap. Znacznik jest tworzony przez utworzenie instancji MarkerOptions, co umożliwia dostosowanie samego znacznika. W tym przypadku podany zostanie tytuł i położenie znacznika, odpowiednio odpowiadające nazwie sklepu rowerowego i jego współrzędnych.

  1. Uruchom aplikację i przejdź do San Francisco, aby zobaczyć dodane właśnie znaczniki.

7. Dostosowywanie znaczników

Jest kilka opcji dostosowywania właśnie dodanych znaczników, które pozwalają wyróżnić się i przekazać przydatne informacje użytkownikom. W tym zadaniu poznasz niektóre z nich przez dostosowanie obrazu każdego znacznika oraz okna informacyjnego po kliknięciu znacznika.

A26F82802fe838e9.png

Dodawanie okna informacyjnego

Domyślnie po kliknięciu znacznika informacyjnego w oknie informacyjnym wyświetla się jego tytuł i fragment (jeśli został ustawiony). Dostosuj go tak, by zawierał dodatkowe informacje, np. adres i ocenę miejsca.

Utwórz znacznik_informacji_treści.xml

Najpierw utwórz nowy plik układu o nazwie marker_info_contents.xml.

  1. Aby to zrobić, kliknij prawym przyciskiem myszy folder app/src/main/res/layout w widoku projektu w Android Studio i wybierz Nowy > Plik zasobu układu.

8CAC51fcbef9171b.png

  1. W oknie wpisz marker_info_contents w polu Nazwa pliku i LinearLayout w polu Root element, a następnie kliknij OK.

8783af12baf07a80.png

Ten plik układu jest później zawyżany, aby reprezentował treść w oknie informacyjnym.

  1. Skopiuj poniższy fragment kodu, który spowoduje dodanie 3 elementów TextViews w pionowej grupie widoków danych LinearLayout, i zastąp domyślny kod w pliku.

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>

Tworzenie implementacji obiektu InfoWindowAdapter

Po utworzeniu pliku układu niestandardowego okna informacyjnego musisz zaimplementować interfejs GoogleMap.InfoWindowAdapter. Ten interfejs zawiera 2 metody: getInfoWindow() i getInfoContents(). Obie metody zwracają opcjonalny obiekt View, w którym pierwsza jest używana do dostosowywania samego okna, a druga służy do dostosowywania zawartości okna. W tym przypadku implementujesz oba i dostosowujesz zwracanie wartości getInfoContents() przy wartości null w getInfoWindow(), co oznacza, że należy użyć okna domyślnego.

  1. Utwórz nowy plik Kotlin o nazwie MarkerInfoWindowAdapter w tym samym pakiecie co MainActivity, klikając prawym przyciskiem myszy folder app/src/main/java/com/google/codelabs/buildyourfirstmap w widoku projektu w Android Studio i wybierając New > Kotlin File/Class.

3975ba36eba9f8e1.png

  1. W oknie wpisz MarkerInfoWindowAdapter i zaznacz Plik.

992235af53d3897f.png

  1. Po utworzeniu pliku skopiuj do niego zawartość znajdującego się poniżej.

TagerInfoWindow

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

W treści metody getInfoContents() podany znacznik w metodzie jest przesyłany do typu Place, a jeśli przesyłanie jest niemożliwe, metoda zwraca wartość null (nie ustawiono jeszcze tagu w Marker, ale musisz to zrobić w następnym kroku).

Następnie układ marker_info_contents.xml jest zawyżany, a tekst zawierający TextViews – tag do tagu Place.

Aktualizacja głównej aktywności

Aby spakować wszystkie utworzone do tej pory komponenty, musisz dodać 2 wiersze do klasy MainActivity.

Aby przekazać niestandardową właściwość InfoWindowAdapter, MarkerInfoWindowAdapter, w wywołaniu metody getMapAsync, wywołaj metodę setInfoWindowAdapter() w obiekcie GoogleMap i utwórz nowe wystąpienie MarkerInfoWindowAdapter.

  1. Aby to zrobić, dodaj poniższy kod po wywołaniu metody addMarkers() wewnątrz lambdy getMapAsync().

MainActivity.onCreate()

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

Na koniec musisz ustawić każde miejsce jako właściwość tagu dla każdego znacznika, który został dodany do mapy.

  1. Aby to zrobić, zmień wywołanie places.forEach{} w funkcji addMarkers() w ten sposób:

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
}

Dodawanie niestandardowego obrazu

Dostosowywanie obrazu znacznika to jeden ze zabawnych sposobów na informowanie o typie miejsca na mapie. W tym kroku zamiast domyślnych czerwonych znaczników chcesz wyświetlać wszystkie rowery na mapie. Projekt startowy zawiera ikonę roweru ic_directions_bike_black_24dp.xml w app/src/res/drawable, której używasz.

6eb7358bb61b0a88.png

Ustaw niestandardową mapę bitową na znaczniku

Mając do dyspozycji ikonę rysowania wektorowego, możesz ustawić tę ikonę jako ikonę do rysowania na mapie. MarkerOptions zawiera metodę icon, która pobiera BitmapDescriptor, której używasz do tego celu.

Najpierw musisz przekonwertować dodaną do wektora wektorową wartość na BitmapDescriptor. Plik o nazwie BitMapHelper w projekcie startowym zawiera funkcję pomocniczą o nazwie vectorToBitmap() służącą tylko do tego.

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

Ta metoda pobiera Context, rysowalny identyfikator zasobu, a także liczbę kolorów, i tworzy jej reprezentację BitmapDescriptor.

Za pomocą metody pomocniczej zadeklaruj nową właściwość o nazwie bicycleIcon i nadaj mu tę definicję: 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)
}

Ta właściwość używa wstępnie zdefiniowanego koloru colorPrimary w Twojej aplikacji i używa jej do zabarwienia ikony rowerowej i zwracania jej jako BitmapDescriptor.

  1. Wykorzystaj tę właściwość i wywołaj metodę icon metody MarkerOptions w metodzie addMarkers(), by dokończyć dostosowywanie ikony. Właściwość znacznika powinna wyglądać następująco:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. Uruchom aplikację, aby zobaczyć zaktualizowane znaczniki.

8. Znaczniki klastra

W zależności od tego, jak bardzo powiększysz mapę, możesz zauważyć, że dodane przez Ciebie znaczniki nakładają się na siebie. Nakładające się znaczniki są bardzo trudne w interakcji i tworzą dużo zakłóceń, co wpływa na łatwość obsługi Twojej aplikacji.

68591edc86d73724.png

Jeśli masz duży zbiór danych, który jest szczegółowo zgrupowany, to aby zwiększyć wygodę użytkowników, warto wdrożyć klasterowe znaczniki. Dzięki grupowaniu w miarę powiększania i pomniejszania mapy znaczniki znajdujące się w pobliżu są grupowane w następujący sposób:

F05e1ca27ff42bf6.png

Aby to zrobić, potrzebujesz pomocy pakietu SDK Maps na Androida.

Maps SDK na Android Utility Library

Pakiet Maps SDK na Androida został stworzony jako sposób na zwiększenie funkcjonalności pakietu Maps SDK na Androida. Oferuje zaawansowane funkcje, takie jak klastry znaczników, mapy termiczne, obsługa plików KML i GeoJson, kodowanie i dekodowanie poliline, a także szereg pomocnych funkcji związanych z geometrią sferyczną.

Aktualizowanie pliku build.gradle

Ponieważ biblioteka narzędzi jest w pakiecie innym niż pakiet SDK na Androida, musisz dodać do pliku build.gradle dodatkową zależność.

  1. Możesz zaktualizować sekcję dependencies pliku app/build.gradle.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. Po dodaniu tego wiersza musisz wykonać synchronizację projektu, aby pobrać nowe zależności.

b7b030ec82c007fd.png

Wdrażanie klastra

Aby wdrożyć klastry w aplikacji, wykonaj te 3 czynności:

  1. Zaimplementuj interfejs ClusterItem.
  2. Określ klasę podrzędną DefaultClusterRenderer.
  3. Utwórz ClusterManager i dodaj elementy.

Wdrażanie interfejsu klastra

Wszystkie obiekty reprezentujące znacznik klastra na mapie muszą zaimplementować interfejs ClusterItem. W Twoim przypadku oznacza to, że model Place musi spełniać wymagania typu ClusterItem. Otwórz plik Place.kt i wprowadź w nim te zmiany:

Miejsce

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
}

Te 3 metody określają:

  • getPosition(), który reprezentuje miejsce LatLng.
  • getTitle(), która reprezentuje nazwę miejsca
  • getSnippet(), który reprezentuje adres tego miejsca.

Podgrupa klasy DefaultClusterRenderer

Klasa odpowiedzialna za klastry ClusterManager, wewnętrznie używa klasy ClusterRenderer do tworzenia klastrów podczas przesuwania i powiększania mapy. Domyślnie dostępny jest domyślny mechanizm renderowania DefaultClusterRenderer, który obsługuje ClusterRenderer. W prostych przypadkach to wystarczy. W Twoim przypadku musisz jednak dostosować znaczniki, bo musisz w nich uwzględnić klasę.

Możesz utworzyć plik Kotlin PlaceRenderer.kt w pakiecie com.google.codelabs.buildyourfirstmap.place i definiować go w ten sposób:

Renderer miejsc

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

Ta klasa zastępuje te 2 funkcje:

  • onBeforeClusterItemRendered(), która jest wywoływana przed wyrenderowaniem klastra na mapie. W tym miejscu możesz wprowadzić modyfikacje za pomocą tagu MarkerOptions – w tym przypadku ustawi ona znacznik, pozycję i ikonę.
  • onClusterItemRenderer(), która jest wywoływana tuż po wyrenderowaniu znacznika na mapie. Tutaj możesz uzyskać dostęp do utworzonego obiektu Marker – w tym przypadku ustawia on właściwość znacznika i znacznika.

Tworzenie Menedżera klastrów i dodawanie do niego elementów

Aby móc działać w klastrze, musisz zmodyfikować instancję MainActivity, aby utworzyć instancję ClusterManager i dodać do niej niezbędne zależności. ClusterManager zajmuje się dodawaniem znaczników (obiektów ClusterItem), więc zamiast dodawać je bezpośrednio na mapie, ta odpowiedzialność jest przypisana do użytkownika ClusterManager. Dodatkowo ClusterManager wywołuje również wewnętrznie setInfoWindowAdapter(), więc ustawienie niestandardowego okna informacyjnego musi zostać wykonane w obiekcie MarkerManager.Collection ClusterManger.

  1. Zacznij od zmiany zawartości lambdy w wywołaniu getMapAsync() w obiekcie MainActivity.onCreate(). Możesz skomentować wywołanie addMarkers() i setInfoWindowAdapter(), a następnie wywołać metodę o nazwie addClusteredMarkers(), która została zdefiniowana w następnej kolejności.

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. Następnie w sekcji MainActivity podaj addClusteredMarkers().

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

Tworzy instancję ClusterManager, przekazuje do niej niestandardowy mechanizm renderowania PlacesRenderer, dodaje wszystkie miejsca i wywołuje metodę cluster(). Ponieważ obiekt ClusterManager używa metody setInfoWindowAdapter() w obiekcie mapy, ustawienie niestandardowego okna informacyjnego należy wykonać na obiekcie ClusterManager.markerCollection. Ponieważ chcesz, aby klaster ulegał zmianie, gdy użytkownik przesuwa się i powiększa mapę, googleMap otrzymuje OnCameraIdleListener. Dzięki temu, gdy kamera jest nieaktywna, wywoływane jest clusterManager.onCameraIdle().

  1. Możesz teraz uruchomić aplikację, aby zobaczyć nowe, pogrupowane sklepy.

9. Rysuj na mapie

Chociaż znasz już jeden ze sposobów rysowania na mapie (przez dodawanie znaczników), pakiet SDK do Map Google na Androida obsługuje wiele innych sposobów rysowania, aby wyświetlać przydatne informacje na mapie.

Jeśli na przykład chcesz wyświetlić na mapie trasy i obszary, możesz użyć linii i wielokątów, aby wyświetlić je na mapie. Jeśli chcesz poprawić obraz na powierzchni ziemi, możesz użyć nakładek na powierzchni.

W tym zadaniu nauczysz się rysować kształty, a w szczególności okręg, przy każdym kliknięciu.

f98ce13055430352.png

Dodaj detektor kliknięć

Zazwyczaj detektor kliknięć jest dodawany do znacznika, przekazując go bezpośrednio na obiekt GoogleMap za pomocą metody setOnMarkerClickListener(). Ponieważ jednak używasz grupowania, zamiast tego musisz użyć detektora kliknięć do ClusterManager.

  1. W metodzie addClusteredMarkers() w MainActivity dodaj ten wiersz zaraz po wywołaniu cluster().

MainActivity.addClusteredMarkers()

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

Ta metoda dodaje detektor i wywołuje metodę addCircle(), którą zdefiniujesz w następnej kolejności. Na koniec zwracana jest wartość false z tej metody, aby wskazać, że ta metoda nie wykorzystała tego zdarzenia.

  1. Następnie musisz zdefiniować właściwość circle i metodę addCircle() w tagu MainActivity.

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

Właściwość circle jest ustawiona tak, aby po każdym kliknięciu nowego znacznika usunąć poprzedni okrąg i dodać nowy. Zauważ, że interfejs API służący do dodawania okręgu jest bardzo podobny do dodawania znacznika.

  1. Możesz teraz uruchomić aplikację, by sprawdzić zmiany.

10. Sterowanie kamerą

Ostatnim zadaniem jest sprawdzenie ustawień kamery, aby móc skupić uwagę na określonym obszarze.

Kamera i widok

Gdy uruchomisz aplikację, aparat wyświetli kontynent afrykański i konieczne będzie przesuwanie widoku do San Francisco i powiększenie widoku, by znaleźć dodane znaczniki. Chociaż może to być zabawny sposób odkrywania świata, nie jest on przydatny, jeśli chcesz od razu wyświetlić znaczniki.

W tym celu możesz automatycznie ustawić kamerę i położenie w odpowiednim miejscu.

  1. Możesz dodać poniższy kod do wywołania getMapAsync(), aby dostosować widok kamery tak, aby była inicjowana w San Francisco po uruchomieniu aplikacji.

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

Najpierw wywoływany jest obiekt setOnMapLoadedCallback(), który aktualizuje kamerę dopiero po wczytaniu mapy. Ten krok jest konieczny, ponieważ właściwości mapy, takie jak wymiary, muszą zostać obliczone przed wywołaniem aktualizacji kamery.

Lambda tworzy nowy obiekt LatLngBounds, który definiuje prostokąt na mapie. Tworzy się stopniowo, uwzględniając wszystkie wartości miejsca LatLng, by mieć pewność, że wszystkie miejsca mieści się w granicach. Po utworzeniu tego obiektu wywołana jest metoda moveCamera() w GoogleMap i jest do niej przekazywana właściwość CameraUpdate przez CameraUpdateFactory.newLatLngBounds(bounds.build(), 20).

  1. Uruchom aplikację i zauważ, że aparat w San Francisco został zainicjowany.

Słucham zmian w aparacie

Oprócz modyfikowania pozycji kamery możesz też śledzić aktualizacje kamery po przesunięciu się użytkownika po mapie. To przydaje się wtedy, gdy chcesz zmienić interfejs w trakcie ruchu kamery.

Dla zabawy modyfikujesz kod, by znaczniki były przezroczyste, gdy kamera się porusza.

  1. W metodzie addClusteredMarkers() dodaj następujące wiersze w dolnej części metody:

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

Powoduje to dodanie OnCameraMoveStartedListener, aby za każdym razem, gdy kamera zaczyna się poruszać, wszystkie znaczniki (zarówno w klastrze, jak i w znacznikach) są modyfikowane do wartości 0.3f, dzięki czemu znaczniki są przezroczyste.

  1. Na koniec, aby zmodyfikować półprzezroczyste znaczniki jako nieprzezroczyste po zatrzymaniu aparatu, zmień zawartość elementu setOnCameraIdleListener w metodzie addClusteredMarkers() na:

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. Śmiało uruchom aplikację, aby zobaczyć wyniki!

11. Mapy KTX

W przypadku aplikacji Kotlin korzystających z co najmniej jednego pakietu SDK Google Maps Platform na Androida dostępne są rozszerzenia Kotlin lub biblioteki KTX, które umożliwiają korzystanie z funkcji języka kotlina takich jak współprogramy, właściwości lub funkcje rozszerzeń. Każdy pakiet SDK Map Google ma odpowiednią bibliotekę KTX, jak pokazano poniżej:

Schemat KTX Google Maps Platform

W tym zadaniu skorzystasz z bibliotek Map KTX i Map Utils KTX w swojej aplikacji i odtworzysz wcześniejsze zadania, tak aby móc używać w aplikacji funkcji językowych kotlina.

  1. Dodaj zależności KTX do pliku build.gradle na poziomie aplikacji

Aplikacja korzysta zarówno z pakietu Maps SDK na Androida, jak i pakietu Maps SDK na Androida, dlatego musisz dołączyć odpowiednie biblioteki KTX dla tych bibliotek. W tym zadaniu będziesz też korzystać z funkcji dostępnej w bibliotece AndroidX Lifecycle KTX, więc dodaj tę zależność także w pliku build.gradle na poziomie aplikacji.

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. Używanie funkcji rozszerzeń GoogleMap.addMarker() i GoogleMap.addCircle()

Biblioteka Map KTX to alternatywa dla interfejsu API w stylu DSL dla elementów GoogleMap.addMarker(MarkerOptions) i GoogleMap.addCircle(CircleOptions) używanych w poprzednich krokach. Do używania wspomnianych interfejsów API wymagana jest konstrukcja klasy zawierającej opcje dla znacznika lub okręgu, natomiast w przypadku alternatywnych rozwiązań KTX można ustawić opcje znacznika lub okręgu w podanej lambdzie.

Aby używać tych interfejsów API, zaktualizuj metody MainActivity.addMarkers(GoogleMap) i 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))
    }
}

Sposób opisanych powyżej jest o wiele bardziej zwięzły, co jest możliwe dzięki użyciu dosłownej funkcji odbiornika Kotlin.

  1. Użyj funkcji zawieszania rozszerzeń SupportMapFragment.awaitMap() i GoogleMap.awaitMapLoad()

Biblioteka Map KTX zawiera też rozszerzenia zawieszające, których można używać w korelacjach. W szczególności chodzi o zawieszanie rozwiązań alternatywnych dla funkcji SupportMapFragment.getMapAsync(OnMapReadyCallback) i GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). Korzystanie z tych alternatywnych interfejsów API eliminuje potrzebę przekazywania wywołań zwrotnych i pozwala na otrzymywanie odpowiedzi tych metod w serialu i synchronicznie.

Te metody zawieszają funkcje, więc ich użycie musi nastąpić w ramach rutyny. Biblioteka Lifecycle Runtime KTX zawiera rozszerzenie zapewniające zakresy dostosowane do cyklu życia, aby umożliwić ich uruchamianie i zatrzymywanie w odpowiednim zdarzeniu cyklu życia.

Po połączeniu tych metod zaktualizuj metodę MainActivity.onCreate(Bundle):

MainActivity.onCreate(pakiet)

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

Zakres odpowiedniej blokady lifecycleScope.launchWhenCreated uruchomi blok, gdy aktywność będzie co najmniej w stanie utworzenia. Pamiętaj też, że wywołania dotyczące obiektu GoogleMap i poczekaj na zakończenie wczytywania mapy zostały zastąpione zarówno elementami SupportMapFragment.awaitMap(), jak i GoogleMap.awaitMapLoad(). Dzięki refaktoryzacji kodu za pomocą tych funkcji zawieszenia możesz utworzyć odpowiedni kod wywołania zwrotnego w sposób sekwencyjny.

  1. Możesz ponownie utworzyć aplikację z wprowadzonymi zmianami.

12. Gratulacje

Gratulacje! Dużo omówiliśmy treści i mamy nadzieję, że dzięki temu lepiej zrozumiesz podstawowe funkcje dostępne w pakiecie Maps SDK na Androida.

Więcej informacji

  • Pakiet SDK Miejsc na Androida – poznaj bogaty zbiór danych o miejscach, aby odkrywać firmy w swojej okolicy.
  • android-maps-ktx – biblioteka open source umożliwiająca integrację z pakietem Maps SDK na Androida i pakietem Maps SDK na Androida w sposób przyjazny dla platformy Kotlin.
  • android-place-ktx – biblioteka typu open source umożliwiająca integrację z pakietem SDK SDK na Androida w sposób przyjazny dla platformy Kotlin.
  • android-samples – przykładowy kod w GitHubie pokazujący wszystkie funkcje z tego modułu ćwiczeń.
  • Więcej ćwiczeń z programowania w Kotlin na temat tworzenia aplikacji na Androida za pomocą Google Maps Platform