1. Zanim zaczniesz
W tym laboratorium kodowania dowiesz się, jak zintegrować pakiet Maps SDK na Androida z aplikacją i korzystać z jego podstawowych funkcji, tworząc aplikację, która wyświetla mapę sklepów rowerowych w San Francisco w Stanach Zjednoczonych.
Wymagania wstępne
- Podstawowa wiedza o Kotlinie i programowaniu na Androida
Jakie zadania wykonasz
- Włącz i używaj pakietu Maps SDK na Androida, aby dodać Mapy Google do aplikacji na Androida.
- Dodawaj, dostosowuj i grupuj znaczniki.
- Rysuj na mapie linie łamane i wielokąty.
- Programowe sterowanie punktem widzenia kamery.
Czego potrzebujesz
- Maps SDK na Androida
- konto Google z włączonymi płatnościami,
- Android Studio 2020.3.1 lub nowszy
- Usługi Google Play zainstalowane w Android Studio
- Urządzenie z Androidem lub emulator Androida z platformą interfejsów API Google opartą na Androidzie 4.2.2 lub nowszym (instrukcje instalacji znajdziesz w artykule Uruchamianie aplikacji w emulatorze Androida).
2. Konfiguracja
W kolejnym kroku musisz włączyć Maps SDK na Androida.
Konfigurowanie Google Maps Platform
Jeśli nie masz jeszcze konta Google Cloud Platform i projektu z włączonymi płatnościami, zapoznaj się z przewodnikiem Pierwsze kroki z Google Maps Platform, aby utworzyć konto rozliczeniowe i projekt.
- W konsoli Google Cloud kliknij menu projektu i wybierz projekt, którego chcesz użyć w tym samouczku.
- Włącz interfejsy API i pakiety SDK Google Maps Platform wymagane w tym samouczku w Google Cloud Marketplace. Aby to zrobić, wykonaj czynności opisane w tym filmie lub tej dokumentacji.
- Wygeneruj klucz interfejsu API na stronie Dane logowania w konsoli Cloud. Możesz wykonać czynności opisane w tym filmie lub tej dokumentacji. Wszystkie żądania wysyłane do Google Maps Platform wymagają klucza interfejsu API.
3. Szybki start
Aby jak najszybciej rozpocząć pracę, przygotowaliśmy kod początkowy, który pomoże Ci w tym samouczku. Możesz przejść od razu do rozwiązania, ale jeśli chcesz wykonać wszystkie czynności, aby samodzielnie je utworzyć, czytaj dalej.
- Sklonuj repozytorium, jeśli masz zainstalowany program
git
.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
Możesz też kliknąć ten przycisk, aby pobrać kod źródłowy.
- Po otrzymaniu kodu otwórz projekt znajdujący się w katalogu
starter
w Android Studio.
4. Dodawanie Map Google
W tej sekcji dodasz Mapy Google, aby wczytywały się po uruchomieniu aplikacji.
Dodawanie klucza interfejsu API
Klucz interfejsu API utworzony w poprzednim kroku musi zostać udostępniony aplikacji, aby pakiet SDK Map Google na Androida mógł powiązać klucz z aplikacją.
- Aby to zrobić, otwórz plik o nazwie
local.properties
w katalogu głównym projektu (na tym samym poziomie co plikigradle.properties
isettings.gradle
). - W tym pliku zdefiniuj nowy klucz
GOOGLE_MAPS_API_KEY
, którego wartością będzie utworzony przez Ciebie klucz interfejsu API.
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
Zwróć uwagę, że pole local.properties
jest wymienione w pliku .gitignore
w repozytorium Git. Dzieje się tak, ponieważ klucz interfejsu API jest uważany za informacje poufne i w miarę możliwości nie powinien być sprawdzany w systemie kontroli wersji.
- Następnie, aby udostępnić interfejs API do użycia w całej aplikacji, uwzględnij wtyczkę Secrets Gradle Plugin for Android w pliku
build.gradle
aplikacji znajdującym się w kataloguapp/
i dodaj ten wiersz w blokuplugins
:
build.gradle na poziomie aplikacji
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
Musisz też zmodyfikować plik build.gradle
na poziomie projektu, aby zawierał następującą ścieżkę klasy:
plik build.gradle na poziomie projektu
buildscript {
dependencies {
// ...
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
}
}
Ta wtyczka udostępni klucze zdefiniowane w pliku local.properties
jako zmienne kompilacji w pliku manifestu Androida oraz jako zmienne w klasie BuildConfig
wygenerowanej przez Gradle w czasie kompilacji. Użycie tej wtyczki usuwa kod szablonowy, który w przeciwnym razie byłby potrzebny do odczytywania właściwości z local.properties
, aby można było uzyskać do nich dostęp w całej aplikacji.
Dodawanie zależności od Map Google
- Teraz, gdy klucz interfejsu API jest dostępny w aplikacji, kolejnym krokiem jest dodanie zależności pakietu SDK Map Google na Androida do pliku
build.gradle
aplikacji.
W projekcie początkowym dołączonym do tego laboratorium ta zależność została już dodana.
build.gradle
dependencies {
// Dependency to include Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
- Następnie dodaj nowy tag
meta-data
wAndroidManifest.xml
, aby przekazać klucz interfejsu API utworzony w poprzednim kroku. Aby to zrobić, otwórz ten plik w Android Studio i dodaj ten tagmeta-data
wewnątrz obiektuapplication
w plikuAndroidManifest.xml
znajdującym się wapp/src/main
.
AndroidManifest.xml
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${GOOGLE_MAPS_API_KEY}" />
- Następnie utwórz nowy plik układu o nazwie
activity_main.xml
w kataloguapp/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 ma jeden element FrameLayout
zawierający element SupportMapFragment
. Ten fragment zawiera bazowy obiekt GoogleMaps
, którego użyjesz w późniejszych krokach.
- Na koniec zaktualizuj klasę
MainActivity
znajdującą się wapp/src/main/java/com/google/codelabs/buildyourfirstmap
, dodając ten kod, aby zastąpić metodęonCreate
i móc ustawić jej zawartość za pomocą nowo utworzonego układu.
MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- Uruchom aplikację. Na ekranie urządzenia powinna pojawić się mapa.
5. Definiowanie stylów map w Google Cloud (opcjonalnie)
Styl mapy możesz dostosować za pomocą definiowania stylów map w Google Cloud.
Tworzenie identyfikatora mapy
Jeśli nie masz jeszcze identyfikatora mapy powiązanego ze stylem mapy, zapoznaj się z przewodnikiem Identyfikatory mapy i wykonaj te czynności:
- Utwórz identyfikator mapy.
- powiązać 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
elementu 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" />
Gdy to zrobisz, uruchom aplikację, aby zobaczyć mapę w wybranym stylu.
6. Dodaj znaczniki
W tym zadaniu dodasz do mapy znaczniki reprezentujące ciekawe miejsca, które chcesz wyróżnić. Najpierw pobierz listę miejsc, które zostały udostępnione w projekcie początkowym, a potem dodaj je do mapy. W tym przykładzie są to sklepy rowerowe.
Uzyskiwanie odwołania do obiektu GoogleMap
Najpierw musisz uzyskać odwołanie do obiektu GoogleMap
, aby móc korzystać z jego metod. Aby to zrobić, dodaj ten kod do metody MainActivity.onCreate()
bezpośrednio po wywołaniu setContentView()
:
MainActivity.onCreate()
val mapFragment = supportFragmentManager.findFragmentById(
R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
addMarkers(googleMap)
}
Implementacja najpierw znajduje element SupportMapFragment
dodany w poprzednim kroku, używając metody findFragmentById()
na obiekcie SupportFragmentManager
. Po uzyskaniu odwołania wywoływane jest wywołanie getMapAsync()
, a następnie przekazywana jest lambda. To w tej funkcji lambda przekazywany jest obiekt GoogleMap
. W tej funkcji lambda wywoływana jest metoda addMarkers()
, która jest zdefiniowana poniżej.
Dostarczona klasa: PlacesReader
W projekcie początkowym klasa PlacesReader
jest już dostępna. Ta klasa odczytuje listę 49 miejsc przechowywanych w pliku JSON o nazwie places.json
i zwraca je jako List<Place>
. Miejsca te to lista sklepów rowerowych w San Francisco w Kalifornii w USA.
Jeśli chcesz dowiedzieć się więcej o implementacji tej klasy, możesz ją otworzyć w Android Studio lub znaleźć w GitHubie.PlacesReader
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 we właściwości MainActivity
właściwość o nazwie places
i zdefiniuj ją w ten sposób:
MainActivity.places
private val places: List<Place> by lazy {
PlacesReader(this).read()
}
Ten kod wywołuje metodę read()
na obiekcie PlacesReader
, która zwraca obiekt List<Place>
. Place
ma właściwość o nazwie name
, czyli nazwę miejsca, oraz latLng
– współrzędne, pod którymi znajduje się to miejsce.
Miejsce
data class Place(
val name: String,
val latLng: LatLng,
val address: LatLng,
val rating: Float
)
Dodawanie znaczników do mapy
Lista miejsc została już wczytana do pamięci. Następnym krokiem jest przedstawienie tych miejsc na mapie.
- Utwórz w
MainActivity
metodę o nazwieaddMarkers()
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 po liście places
, a następnie wywołuje metodę addMarker()
na podanym obiekcie GoogleMap
. Marker jest tworzony przez utworzenie instancji obiektu MarkerOptions
, co umożliwia dostosowanie samego markera. W tym przypadku podany jest tytuł i pozycja znacznika, które odpowiednio reprezentują nazwę sklepu rowerowego i jego współrzędne.
- Uruchom aplikację i przejdź do San Francisco, aby zobaczyć dodane przez siebie markery.
7. Dostosowywanie znaczników
Markery, które właśnie zostały dodane, można dostosować na kilka sposobów, aby wyróżniały się i przekazywały użytkownikom przydatne informacje. W tym zadaniu poznasz niektóre z nich, dostosowując obraz każdego znacznika oraz okno informacyjne wyświetlane po kliknięciu znacznika.
Dodawanie okna informacyjnego
Domyślnie po kliknięciu markera w oknie informacji wyświetlany jest jego tytuł i fragment (jeśli został ustawiony). Możesz dostosować ten widok, aby wyświetlać dodatkowe informacje, takie jak adres i ocena miejsca.
Tworzenie pliku marker_info_contents.xml
Najpierw utwórz nowy plik układu o nazwie marker_info_contents.xml
.
- 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.
- W oknie dialogowym wpisz
marker_info_contents
w polu Nazwa pliku iLinearLayout
w poluRoot element
, a następnie kliknij OK.
Ten plik układu jest później rozwijany, aby reprezentować zawartość okna informacji.
- Skopiuj zawartość tego fragmentu kodu, który dodaje 3 elementy
TextViews
w pionowej grupie widokówLinearLayout
, i zastąp nim 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 interfejsu InfoWindowAdapter
Po utworzeniu pliku układu niestandardowego okna informacyjnego kolejnym krokiem jest zaimplementowanie interfejsu GoogleMap.InfoWindowAdapter. Ten interfejs zawiera 2 metody: getInfoWindow()
i getInfoContents()
. Obie metody zwracają opcjonalny obiekt View
, przy czym pierwsza służy do dostosowywania samego okna, a druga – jego zawartości. W Twoim przypadku implementujesz obie funkcje i dostosowujesz zwracanie wartości getInfoContents()
, zwracając wartość null w funkcji getInfoWindow()
, co oznacza, że należy użyć domyślnego okresu.
- Utwórz nowy plik Kotlin o nazwie
MarkerInfoWindowAdapter
w tym samym pakiecie coMainActivity
. W tym celu kliknij prawym przyciskiem myszy folderapp/src/main/java/com/google/codelabs/buildyourfirstmap
w widoku projektu w Android Studio, a następnie wybierz Nowy > Plik/klasa Kotlin.
- W oknie wpisz
MarkerInfoWindowAdapter
i pozostaw zaznaczoną opcję Plik.
- Po utworzeniu pliku skopiuj zawartość tego fragmentu kodu i wklej ją do nowego pliku.
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
}
}
W treści metody getInfoContents()
podany w niej obiekt Marker jest rzutowany na typ Place
. Jeśli rzutowanie nie jest możliwe, metoda zwraca wartość null (nie ustawiono jeszcze właściwości tagu w obiekcie Marker
, ale zrobisz to w następnym kroku).
Następnie układ marker_info_contents.xml
jest rozwijany, a tekst w TextViews
jest ustawiany na tag Place
.
Aktualizowanie pliku MainActivity
Aby połączyć wszystkie utworzone do tej pory komponenty, musisz dodać 2 wiersze w klasie MainActivity
.
Najpierw, aby przekazać niestandardowe InfoWindowAdapter
, MarkerInfoWindowAdapter
w wywołaniu metody getMapAsync
, wywołaj metodę setInfoWindowAdapter()
w obiekcie GoogleMap
i utwórz nową instancję MarkerInfoWindowAdapter
.
- Aby to zrobić, dodaj ten kod po wywołaniu metody
addMarkers()
w funkcji lambdagetMapAsync()
.
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
Na koniec musisz ustawić każde miejsce jako właściwość tagu na każdym znaczniku dodanym do mapy.
- Aby to zrobić, zmodyfikuj wywołanie
places.forEach{}
w funkcjiaddMarkers()
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 markera
Dostosowywanie obrazu znacznika to jeden z ciekawych sposobów na przekazywanie informacji o rodzaju miejsca, które reprezentuje znacznik na mapie. W tym kroku zamiast domyślnych czerwonych znaczników wyświetlisz rowery, które będą reprezentować każdy sklep na mapie. Projekt początkowy zawiera ikonę roweru ic_directions_bike_black_24dp.xml
w app/src/res/drawable
, której używasz.
Ustawianie niestandardowej mapy bitowej na znaczniku
Gdy masz już ikonę roweru w formacie wektorowym, możesz ustawić ją jako ikonę każdego znacznika na mapie. MarkerOptions
ma metodę icon
, która przyjmuje BitmapDescriptor
, za pomocą którego możesz to zrobić.
Najpierw musisz przekonwertować dodany rysunek wektorowy na BitmapDescriptor
. Plik o nazwie BitMapHelper
dołączony do projektu początkowego zawiera funkcję pomocniczą o nazwie vectorToBitmap()
, która właśnie to robi.
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 przyjmuje obiekt Context
, identyfikator zasobu rysowalnego oraz liczbę całkowitą koloru i tworzy jego reprezentację BitmapDescriptor
.
Za pomocą metody pomocniczej zadeklaruj nową właściwość o nazwie bicycleIcon
i nadaj jej 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 predefiniowanego koloru colorPrimary
w aplikacji, aby zabarwić ikonę roweru i zwrócić ją jako BitmapDescriptor
.
- Korzystając z tej właściwości, wywołaj metodę
icon
obiektuMarkerOptions
w metodzieaddMarkers()
, aby dokończyć dostosowywanie ikony. Właściwość znacznika powinna wyglądać tak:
MainActivity.addMarkers()
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- Uruchom aplikację, aby zobaczyć zaktualizowane znaczniki.
8. Klastrowanie znaczników
W zależności od stopnia powiększenia mapy możesz zauważyć, że dodane przez Ciebie znaczniki się nakładają. Nakładające się na siebie markery utrudniają interakcję i generują dużo szumu, co wpływa na użyteczność aplikacji.
Aby poprawić wrażenia użytkowników, w przypadku dużych zbiorów danych, które są ściśle zgrupowane, warto wdrożyć grupowanie znaczników. Dzięki grupowaniu podczas powiększania i pomniejszania mapy znaczniki znajdujące się blisko siebie są grupowane w ten sposób:
Aby to zrobić, musisz skorzystać z biblioteki narzędziowej pakietu Maps SDK na Androida.
Biblioteka narzędziowa pakietu Maps SDK na Androida
Biblioteka narzędziowa pakietu Maps SDK na Androida została utworzona w celu rozszerzenia funkcjonalności pakietu Maps SDK na Androida. Oferuje zaawansowane funkcje, takie jak grupowanie znaczników, mapy cieplne, obsługa formatów KML i GeoJSON, kodowanie i dekodowanie polilinii oraz kilka funkcji pomocniczych związanych z geometrią sferyczną.
Aktualizowanie pliku build.gradle
Biblioteka narzędziowa jest pakowana oddzielnie od pakietu Maps SDK na Androida, więc musisz dodać dodatkową zależność do pliku build.gradle
.
- Zaktualizuj sekcję
dependencies
w plikuapp/build.gradle
.
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- Po dodaniu tego wiersza musisz zsynchronizować projekt, aby pobrać nowe zależności.
Wdrażanie grupowania
Aby wdrożyć grupowanie w aplikacji, wykonaj te 3 kroki:
- Zaimplementuj interfejs
ClusterItem
. - Utwórz podklasę klasy
DefaultClusterRenderer
. - Utwórz
ClusterManager
i dodaj do niego produkty.
Zaimplementuj interfejs ClusterItem
Wszystkie obiekty, które reprezentują na mapie znacznik klastrowany, muszą implementować interfejs ClusterItem
. W Twoim przypadku oznacza to, że model Place
musi być zgodny z 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
}
Interfejs ClusterItem definiuje te 3 metody:
getPosition()
, który reprezentujeLatLng
miejsca.getTitle()
, który reprezentuje nazwę miejsca.getSnippet()
, który reprezentuje adres miejsca.
Utwórz klasę podrzędną klasy DefaultClusterRenderer
Klasa odpowiedzialna za implementację klastrowania, ClusterManager
, wewnętrznie używa klasy ClusterRenderer
do tworzenia klastrów podczas przesuwania i powiększania mapy. Domyślnie jest on dostarczany z domyślnym mechanizmem renderowania DefaultClusterRenderer
, który implementuje interfejs ClusterRenderer
. W prostych przypadkach powinno to wystarczyć. W Twoim przypadku jednak, ponieważ markery wymagają dostosowania, musisz rozszerzyć tę klasę i dodać w niej dostosowania.
Utwórz plik Kotlin PlaceRenderer.kt
w pakiecie com.google.codelabs.buildyourfirstmap.place
i zdefiniuj go w ten sposób:
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
}
}
Ta klasa zastępuje te 2 funkcje:
onBeforeClusterItemRendered()
, która jest wywoływana przed wyrenderowaniem klastra na mapie. W tym miejscu możesz wprowadzić zmiany za pomocą parametruMarkerOptions
– w tym przypadku ustawia on tytuł, pozycję i ikonę markera.onClusterItemRenderer()
, która jest wywoływana natychmiast po wyrenderowaniu znacznika na mapie. W tym miejscu możesz uzyskać dostęp do utworzonego obiektuMarker
– w tym przypadku ustawia on właściwość tagu znacznika.
Tworzenie obiektu ClusterManager i dodawanie do niego elementów
Aby klastrowanie działało, musisz zmodyfikować MainActivity
, aby utworzyć instancję ClusterManager
i zapewnić jej niezbędne zależności. ClusterManager
wewnętrznie obsługuje dodawanie znaczników (obiektów ClusterItem
), więc zamiast dodawać znaczniki bezpośrednio na mapie, to zadanie jest delegowane do ClusterManager
. Dodatkowo ClusterManager
wewnętrznie wywołuje setInfoWindowAdapter()
, więc ustawienie niestandardowego okna informacyjnego musi zostać wykonane na obiekcie ClusterManger
MarkerManager.Collection
.
- Na początek zmień zawartość lambdy w wywołaniu
getMapAsync()
wMainActivity.onCreate()
. Zakomentuj wywołanie funkcjiaddMarkers()
isetInfoWindowAdapter()
, a zamiast tego wywołaj metodęaddClusteredMarkers()
, którą zdefiniujesz w następnym kroku.
MainActivity.onCreate()
mapFragment?.getMapAsync { googleMap ->
//addMarkers(googleMap)
addClusteredMarkers(googleMap)
// Set custom info window adapter.
// googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
- Następnie w
MainActivity
zdefiniujaddClusteredMarkers()
.
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()
}
}
Ta metoda tworzy instancję klasy ClusterManager
, przekazuje do niej niestandardowy moduł renderujący PlacesRenderer
, dodaje wszystkie miejsca i wywołuje metodę cluster()
. Ponieważ ClusterManager
używa metody setInfoWindowAdapter()
na obiekcie mapy, ustawienie niestandardowego okna informacyjnego będzie musiało zostać wykonane na obiekcie ClusterManager.markerCollection
. Na koniec, ponieważ chcesz, aby klastrowanie zmieniało się w miarę przesuwania i powiększania mapy przez użytkownika, do funkcji googleMap
przekazywana jest funkcja OnCameraIdleListener
, dzięki czemu po przejściu kamery w stan bezczynności wywoływana jest funkcja clusterManager.onCameraIdle()
.
- Uruchom aplikację, aby zobaczyć nowe zgrupowane sklepy.
9. Rysuj na mapie
Chociaż znasz już jeden sposób rysowania na mapie (dodawanie znaczników), pakiet Maps SDK na Androida obsługuje wiele innych sposobów rysowania, które umożliwiają wyświetlanie przydatnych informacji na mapie.
Jeśli na przykład chcesz przedstawić na mapie trasy i obszary, możesz użyć linii łamanych i wielokątów. Jeśli chcesz przymocować obraz do powierzchni ziemi, możesz użyć nakładek na podłoże.
W tym ćwiczeniu dowiesz się, jak rysować kształty, a konkretnie okrąg, wokół znacznika po jego kliknięciu.
Dodawanie odbiornika kliknięć
Zazwyczaj detektor kliknięć dodaje się do markera, przekazując go bezpośrednio do obiektu GoogleMap
za pomocą setOnMarkerClickListener()
. Jednak ze względu na to, że używasz klastrowania, odbiornik kliknięć musi być przekazywany do ClusterManager
.
- W metodzie
addClusteredMarkers()
wMainActivity
dodaj ten wiersz bezpośrednio po wywołaniucluster()
.
MainActivity.addClusteredMarkers()
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
Ta metoda dodaje odbiornik i wywołuje metodę addCircle()
, którą zdefiniujesz w następnym kroku. Na koniec ta metoda zwraca wartość false
, aby wskazać, że nie wykorzystała tego zdarzenia.
- Następnie musisz zdefiniować właściwość
circle
i metodęaddCircle()
wMainActivity
.
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, że po kliknięciu nowego markera poprzedni okrąg jest usuwany i dodawany jest nowy. Zwróć uwagę, że interfejs API do dodawania okręgu jest bardzo podobny do interfejsu API do dodawania markera.
- Uruchom teraz aplikację, aby zobaczyć zmiany.
10. Sterowanie kamerą
Na koniec przyjrzyj się elementom sterującym kamery, aby móc skupić widok na określonym regionie.
Kamera i widok
Jeśli podczas uruchamiania aplikacji zauważysz, że aparat wyświetla kontynent afrykański, a Ty musisz mozolnie przesuwać i powiększać widok, aby znaleźć dodane znaczniki w San Francisco. Chociaż może to być ciekawy sposób na odkrywanie świata, nie jest to przydatne, jeśli chcesz od razu wyświetlić markery.
Aby to ułatwić, możesz programowo ustawić pozycję kamery, tak aby widok był wyśrodkowany w wybranym miejscu.
- Dodaj do wywołania funkcji
getMapAsync()
ten kod, aby dostosować widok kamery tak, aby po uruchomieniu aplikacji była ona ustawiona na San Francisco.
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ływana jest funkcja setOnMapLoadedCallback()
, aby aktualizacja kamery była przeprowadzana dopiero po wczytaniu mapy. Ten krok jest niezbędny, ponieważ właściwości mapy, takie jak wymiary, muszą zostać obliczone przed wywołaniem aktualizacji kamery.
W funkcji lambda tworzony jest nowy obiekt LatLngBounds
, który definiuje prostokątny region na mapie. Jest on stopniowo tworzony przez uwzględnianie wszystkich wartości LatLng
miejsca, aby mieć pewność, że wszystkie miejsca znajdują się w jego granicach. Po utworzeniu tego obiektu wywoływana jest metoda moveCamera()
w obiekcie GoogleMap
, a obiekt CameraUpdate
jest do niej przekazywany za pomocą obiektu CameraUpdateFactory.newLatLngBounds(bounds.build(), 20)
.
- Uruchom aplikację i zobacz, że kamera jest teraz zainicjowana w San Francisco.
Nasłuchiwanie zmian w kamerze
Oprócz modyfikowania pozycji kamery możesz też nasłuchiwać aktualizacji kamery, gdy użytkownik porusza się po mapie. Może to być przydatne, jeśli chcesz modyfikować interfejs w miarę przesuwania kamery.
Dla zabawy modyfikujesz kod, aby znaczniki stawały się półprzezroczyste, gdy kamera jest przesuwana.
- W metodzie
addClusteredMarkers()
dodaj te wiersze na końcu 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 }
}
Dodaje to OnCameraMoveStartedListener
, dzięki czemu za każdym razem, gdy kamera zaczyna się poruszać, wszystkie wartości alfa znaczników (zarówno klastrów, jak i znaczników) są modyfikowane do 0.3f
, aby znaczniki były półprzezroczyste.
- Na koniec, aby przywrócić nieprzezroczystość półprzezroczystych znaczników po zatrzymaniu kamery, zmodyfikuj zawartość
setOnCameraIdleListener
w metodzieaddClusteredMarkers()
w ten sposób:
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()
}
- Uruchom aplikację, aby zobaczyć wyniki.
11. Mapy KTX
W przypadku aplikacji w języku Kotlin, które korzystają z co najmniej jednego pakietu SDK Google Maps Platform na Androida, dostępne są biblioteki rozszerzeń Kotlin lub KTX, które umożliwiają korzystanie z funkcji języka Kotlin, takich jak korutyny, właściwości i funkcje rozszerzeń itp. Każdy pakiet SDK Map Google ma odpowiednią bibliotekę KTX, jak pokazano poniżej:
W tym zadaniu użyjesz w aplikacji bibliotek Maps KTX i Maps Utils KTX oraz zmodyfikujesz implementacje z poprzednich zadań, aby móc używać w aplikacji funkcji języka Kotlin.
- Dodawanie zależności KTX w pliku build.gradle na poziomie aplikacji
Aplikacja korzysta zarówno z pakietu Maps SDK na Androida, jak i z biblioteki narzędziowej pakietu Maps SDK na Androida, więc musisz dołączyć odpowiednie biblioteki KTX. W tym zadaniu użyjesz też funkcji z biblioteki AndroidX Lifecycle KTX, więc dodaj tę zależność do 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'
}
- Używanie funkcji rozszerzających GoogleMap.addMarker() i GoogleMap.addCircle()
Biblioteka Maps KTX udostępnia alternatywny interfejs API w stylu DSL dla funkcji GoogleMap.addMarker(MarkerOptions)
i GoogleMap.addCircle(CircleOptions)
używanych w poprzednich krokach. Aby używać wspomnianych interfejsów API, musisz utworzyć klasę zawierającą opcje znacznika lub okręgu, natomiast w przypadku alternatywnych interfejsów KTX możesz ustawić opcje znacznika lub okręgu w dostarczonej lambdzie.
Aby korzystać z 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))
}
}
Przepisanie powyższych metod w ten sposób jest znacznie bardziej zwięzłe, co jest możliwe dzięki literalowi funkcji z odbiorcą w Kotlinie.
- Używaj funkcji zawieszających rozszerzenia SupportMapFragment.awaitMap() i GoogleMap.awaitMapLoad()
Biblioteka Maps KTX udostępnia też funkcje rozszerzeń zawieszania, które można wykorzystywać w korutynach. W przypadku funkcji SupportMapFragment.getMapAsync(OnMapReadyCallback)
i GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback)
istnieją alternatywne funkcje zawieszania. Korzystanie z tych alternatywnych interfejsów API eliminuje konieczność przekazywania wywołań zwrotnych i umożliwia odbieranie odpowiedzi z tych metod w sposób szeregowy i synchroniczny.
Ponieważ te metody są funkcjami zawieszającymi, muszą być używane w korutynie. Biblioteka Lifecycle Runtime KTX oferuje rozszerzenie, które udostępnia zakresy coroutine uwzględniające cykl życia, dzięki czemu coroutine są uruchamiane i zatrzymywane w odpowiednim momencie cyklu życia.
Połączenie tych koncepcji pozwala zaktualizować metodę 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)
}
}
Zakres coroutine lifecycleScope.launchWhenCreated
wykona blok, gdy aktywność będzie co najmniej w stanie utworzenia. Zwróć też uwagę, że wywołania służące do pobierania obiektu GoogleMap
i oczekiwania na zakończenie wczytywania mapy zostały zastąpione odpowiednio wywołaniami SupportMapFragment.awaitMap()
i GoogleMap.awaitMapLoad()
. Refaktoryzacja kodu za pomocą tych funkcji zawieszających umożliwia pisanie równoważnego kodu opartego na wywołaniach zwrotnych w sposób sekwencyjny.
- Ponownie skompiluj aplikację ze zmianami wprowadzonymi w ramach refaktoryzacji.
12. Gratulacje
Gratulacje! Omówiliśmy wiele tematów i mamy nadzieję, że lepiej rozumiesz podstawowe funkcje pakietu Maps SDK na Androida.
Więcej informacji
- Pakiet SDK Miejsc na Androida – poznaj bogaty zestaw danych o miejscach, aby odkrywać firmy w Twojej okolicy.
- android-maps-ktx – biblioteka open source, która umożliwia integrację z pakietem Maps SDK na Androida i biblioteką narzędziową pakietu Maps SDK na Androida w sposób przyjazny dla języka Kotlin.
- android-place-ktx – biblioteka open source, która umożliwia integrację z pakietem SDK Miejsc na Androida w sposób przyjazny dla języka Kotlin.
- android-samples – przykładowy kod na GitHubie, który demonstruje wszystkie funkcje omówione w tym module i nie tylko.
- Więcej ćwiczeń z programowania w Kotlinie dotyczących tworzenia aplikacji na Androida za pomocą Google Maps Platform