1. Zanim zaczniesz
W tym laboratorium kodowania dowiesz się, jak zintegrować pakiet Maps SDK na Androida z aplikacją i korzystać z jego podstawowych funkcji. W tym celu utworzysz aplikację, która wyświetla mapę gór w Kolorado w Stanach Zjednoczonych za pomocą różnych typów znaczników. Dowiesz się też, jak rysować na mapie inne kształty.
Po ukończeniu ćwiczenia będzie to wyglądać tak:
Wymagania wstępne
- Podstawowa znajomość języka Kotlin, Jetpack Compose i tworzenia aplikacji na Androida
Jakie zadania wykonasz
- Włącz i użyj biblioteki Maps Compose w pakiecie Maps SDK na Androida, aby dodać
GoogleMap
do aplikacji na Androida. - Dodawanie i dostosowywanie znaczników
- Rysowanie wielokątów na mapie
- Programowe sterowanie punktem widzenia kamery
Czego potrzebujesz
- Maps SDK na Androida
- konto Google z włączonymi płatnościami,
- Najnowsza stabilna wersja Android Studio
- urządzenie z Androidem lub emulator Androida z platformą Google API opartą na Androidzie 5.0 lub nowszym (instrukcje instalacji znajdziesz w artykule Uruchamianie aplikacji na emulatorze Androida);
- połączenie z internetem.
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 i samodzielnie je zbudować, czytaj dalej.
- Sklonuj repozytorium, jeśli masz zainstalowany program
git
.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.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 klucza interfejsu API do projektu
W tej sekcji opisujemy, jak przechowywać klucz interfejsu API, aby aplikacja mogła się do niego bezpiecznie odwoływać. Nie należy umieszczać klucza interfejsu API w systemie kontroli wersji, dlatego zalecamy przechowywanie go w pliku secrets.properties
, który zostanie umieszczony w lokalnej kopii katalogu głównego projektu. Więcej informacji o pliku secrets.properties
znajdziesz w artykule Pliki właściwości Gradle.
Aby uprościć to zadanie, zalecamy użycie wtyczki Gradle obiektów tajnych na Androida.
Aby zainstalować wtyczkę Gradle obiektów tajnych na Androida w projekcie Google Maps:
- W Android Studio otwórz plik
build.gradle.kts
najwyższego poziomu i dodaj podany niżej kod do elementudependencies
w sekcjibuildscript
.buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- Otwórz plik
build.gradle.kts
na poziomie modułu i dodaj ten kod do elementuplugins
.plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- W pliku
build.gradle.kts
na poziomie modułu sprawdź, czy wartościtargetSdk
icompileSdk
wynoszą co najmniej 34. - Zapisz plik i zsynchronizuj projekt z Gradle.
- Otwórz plik
secrets.properties
w katalogu najwyższego poziomu i dodaj ten kod: ZastąpYOUR_API_KEY
swoim kluczem interfejsu API. Przechowuj klucz w tym pliku, ponieważsecrets.properties
jest wykluczony z systemu kontroli wersji.MAPS_API_KEY=YOUR_API_KEY
- Zapisz plik.
- Utwórz plik
local.defaults.properties
w katalogu najwyższego poziomu, czyli w tym samym folderze co pliksecrets.properties
, a następnie dodaj ten kod. Ten plik służy jako lokalizacja kopii zapasowej klucza interfejsu API, jeśli nie można znaleźć plikuMAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
, aby kompilacje nie kończyły się niepowodzeniem. Dzieje się tak, gdy klonujesz aplikację z systemu kontroli wersji i nie masz jeszcze lokalnie utworzonego plikusecrets.properties
, aby podać klucz interfejsu API. - Zapisz plik.
- W pliku
AndroidManifest.xml
otwórzcom.google.android.geo.API_KEY
i zaktualizuj atrybutandroid:value
. Jeśli tag<meta-data>
nie istnieje, utwórz go jako tag podrzędny tagu<application>
.<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- W Android Studio otwórz plik
build.gradle.kts
na poziomie modułu i edytuj właściwośćsecrets
. Jeśli właściwośćsecrets
nie istnieje, dodaj ją.Edytuj właściwości wtyczki, aby ustawićpropertiesFileName
nasecrets.properties
,defaultPropertiesFileName
nalocal.defaults.properties
i inne właściwości.secrets { // Optionally specify a different file name containing your secrets. // The plugin defaults to "local.properties" propertiesFileName = "secrets.properties" // A properties file containing default secret values. This file can be // checked in version control. defaultPropertiesFileName = "local.defaults.properties" }
5. Dodawanie Map Google
W tej sekcji dodasz Mapę Google, która będzie się wczytywać po uruchomieniu aplikacji.
Dodawanie zależności Map Compose
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.kts
aplikacji. Aby tworzyć aplikacje za pomocą Jetpack Compose, użyj biblioteki Maps Compose, która udostępnia elementy pakietu Maps SDK na Androida jako funkcje i typy danych, które można łączyć.
build.gradle.kts
W pliku build.gradle.kts
na poziomie aplikacji zastąp zależności pakietu Maps SDK na Androida, które nie są oparte na Compose:
dependencies {
// ...
// Google Maps SDK -- these are here for the data model. Remove these dependencies and replace
// with the compose versions.
implementation("com.google.android.gms:play-services-maps:18.2.0")
// KTX for the Maps SDK for Android library
implementation("com.google.maps.android:maps-ktx:5.0.0")
// KTX for the Maps SDK for Android Utility Library
implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
}
z odpowiednikami, które można łączyć:
dependencies {
// ...
// Google Maps Compose library
val mapsComposeVersion = "4.4.1"
implementation("com.google.maps.android:maps-compose:$mapsComposeVersion")
// Google Maps Compose utility library
implementation("com.google.maps.android:maps-compose-utils:$mapsComposeVersion")
// Google Maps Compose widgets library
implementation("com.google.maps.android:maps-compose-widgets:$mapsComposeVersion")
}
Dodawanie komponentu kompozycyjnego Mapy Google
W funkcji MountainMap.kt
dodaj funkcję GoogleMap
w funkcji Box
zagnieżdżonej w funkcji MapMountain
.
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...
@Composable
fun MountainMap(
paddingValues: PaddingValues,
viewState: MountainsScreenViewState.MountainList,
eventFlow: Flow<MountainsScreenEvent>,
selectedMarkerType: MarkerType,
) {
var isMapLoaded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Add GoogleMap here
GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapLoaded = { isMapLoaded = true }
)
// ...
}
}
Teraz skompiluj i uruchom aplikację. Powinna się wyświetlić mapa wyśrodkowana na słynnej Wyspie Zero, znanej też jako szerokość geograficzna zero i długość geograficzna zero. Później dowiesz się, jak ustawić mapę w wybranej lokalizacji i na wybranym poziomie powiększenia, ale na razie ciesz się pierwszym zwycięstwem.
6. Definiowanie stylów map w Google Cloud
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, podczas tworzenia instancji funkcji kompozycyjnej GoogleMap
użyj identyfikatora mapy podczas tworzenia obiektu GoogleMapOptions
, który jest przypisywany do parametru googleMapOptionsFactory
w konstruktorze.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
Gdy to zrobisz, uruchom aplikację, aby zobaczyć mapę w wybranym stylu.
7. Wczytywanie danych markerów
Głównym zadaniem aplikacji jest wczytanie kolekcji gór z pamięci lokalnej i wyświetlenie ich w GoogleMap
. W tym kroku zapoznasz się z infrastrukturą do wczytywania danych o górach i wyświetlania ich w interfejsie.
Góry
Klasa danych Mountain
zawiera wszystkie dane o poszczególnych górach.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
Pamiętaj, że góry zostaną później podzielone na podstawie wysokości. Góry o wysokości co najmniej 14 000 stóp nazywane są czternastotysięcznikami. Kod początkowy zawiera funkcję rozszerzenia, która wykonuje to sprawdzenie.
/**
* Extension function to determine whether a mountain is a "14er", i.e., has an elevation greater
* than 14,000 feet (~4267 meters).
*/
fun Mountain.is14er() = elevation >= 14_000.feet
MountainsScreenViewState
Klasa MountainsScreenViewState
zawiera wszystkie dane potrzebne do renderowania widoku. Może być w stanie Loading
lub MountainList
w zależności od tego, czy lista gór została wczytana.
/**
* Sealed class representing the state of the mountain map view.
*/
sealed class MountainsScreenViewState {
data object Loading : MountainsScreenViewState()
data class MountainList(
// List of the mountains to display
val mountains: List<Mountain>,
// Bounding box that contains all of the mountains
val boundingBox: LatLngBounds,
// Switch indicating whether all the mountains or just the 14ers
val showingAllPeaks: Boolean = false,
) : MountainsScreenViewState()
}
Dostępne klasy: MountainsRepository
i MountainsViewModel
W projekcie początkowym klasa MountainsRepository
jest już dostępna. Ta klasa odczytuje listę miejsc w górach, które są przechowywane w pliku GPS Exchange Format
lub GPX, top_peaks.gpx
. Wywołanie mountainsRepository.loadMountains()
zwraca wartość StateFlow<List<Mountain>>
.
MountainsRepository
class MountainsRepository(@ApplicationContext val context: Context) {
private val _mountains = MutableStateFlow(emptyList<Mountain>())
val mountains: StateFlow<List<Mountain>> = _mountains
private var loaded = false
/**
* Loads the list of mountains from the list of mountains from the raw resource.
*/
suspend fun loadMountains(): StateFlow<List<Mountain>> {
if (!loaded) {
loaded = true
_mountains.value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.top_peaks).use { inputStream ->
readMountains(inputStream)
}
}
}
return mountains
}
/**
* Reads the [Waypoint]s from the given [inputStream] and returns a list of [Mountain]s.
*/
private fun readMountains(inputStream: InputStream) =
readWaypoints(inputStream).mapIndexed { index, waypoint ->
waypoint.toMountain(index)
}.toList()
// ...
}
MountainsViewModel
MountainsViewModel
to klasa ViewModel
, która wczytuje kolekcje gór i udostępnia je oraz inne części stanu interfejsu za pomocą mountainsScreenViewState
. mountainsScreenViewState
to gorący StateFlow
, który interfejs może obserwować jako stan modyfikowalny za pomocą funkcji rozszerzenia collectAsState
.
Zgodnie z zasadami architektury dźwięku MountainsViewModel
przechowuje wszystkie stany aplikacji. Interfejs wysyła interakcje użytkownika do modelu widoku za pomocą metody onEvent
.
@HiltViewModel
class MountainsViewModel
@Inject
constructor(
mountainsRepository: MountainsRepository
) : ViewModel() {
private val _eventChannel = Channel<MountainsScreenEvent>()
// Event channel to send events to the UI
internal fun getEventChannel() = _eventChannel.receiveAsFlow()
// Whether or not to show all of the high peaks
private var showAllMountains = MutableStateFlow(false)
val mountainsScreenViewState =
mountainsRepository.mountains.combine(showAllMountains) { allMountains, showAllMountains ->
if (allMountains.isEmpty()) {
MountainsScreenViewState.Loading
} else {
val filteredMountains =
if (showAllMountains) allMountains else allMountains.filter { it.is14er() }
val boundingBox = filteredMountains.map { it.location }.toLatLngBounds()
MountainsScreenViewState.MountainList(
mountains = filteredMountains,
boundingBox = boundingBox,
showingAllPeaks = showAllMountains,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MountainsScreenViewState.Loading
)
init {
// Load the full set of mountains
viewModelScope.launch {
mountainsRepository.loadMountains()
}
}
// Handle user events
fun onEvent(event: MountainsViewModelEvent) {
when (event) {
OnZoomAll -> onZoomAll()
OnToggleAllPeaks -> toggleAllPeaks()
}
}
private fun onZoomAll() {
sendScreenEvent(MountainsScreenEvent.OnZoomAll)
}
private fun toggleAllPeaks() {
showAllMountains.value = !showAllMountains.value
}
// Send events back to the UI via the event channel
private fun sendScreenEvent(event: MountainsScreenEvent) {
viewModelScope.launch { _eventChannel.send(event) }
}
}
Jeśli chcesz dowiedzieć się więcej o implementacji tych klas, możesz uzyskać do nich dostęp w GitHubie lub otworzyć klasy MountainsRepository
i MountainsViewModel
w Android Studio.
Używanie klasy ViewModel
Model widoku jest używany w MainActivity
do pobierania viewState
. Symbolu viewState
użyjesz później w tym laboratorium do renderowania znaczników. Pamiętaj, że ten kod jest już uwzględniony w projekcie początkowym i jest tu podany tylko w celach informacyjnych.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. Ustaw kamerę
GoogleMap
Domyślny środek to szerokość geograficzna 0 i długość geograficzna 0. Znaczniki, które będziesz renderować, znajdują się w stanie Kolorado w USA. viewState
dostarczony przez model widoku zawiera LatLngBounds, który zawiera wszystkie markery.
W MountainMap.kt
utwórz CameraPositionState
zainicjowany w środku ramki ograniczającej. Ustaw parametr cameraPositionState
funkcji GoogleMap
na utworzoną przed chwilą zmienną cameraPositionState
.
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
Uruchom teraz kod i obserwuj, jak mapa wyśrodkowuje się na Kolorado.
Powiększanie do zakresu znacznika
Aby jeszcze bardziej skupić mapę na znacznikach, dodaj funkcję zoomAll
na końcu pliku MountainMap.kt
. Pamiętaj, że ta funkcja wymaga CoroutineScope
, ponieważ animowanie kamery do nowej lokalizacji jest operacją asynchroniczną, która wymaga czasu.
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
Następnie dodaj kod, który będzie wywoływać funkcję zoomAll
za każdym razem, gdy zmienią się granice wokół kolekcji znaczników lub gdy użytkownik kliknie przycisk powiększenia w górnym pasku aplikacji. Zwróć uwagę, że przycisk powiększenia jest już połączony z wysyłaniem zdarzeń do modelu widoku. Wystarczy, że zbierzesz te zdarzenia z modelu widoku i w odpowiedzi wywołasz funkcję zoomAll
.
fun MountainMap(
// ...
) {
// ...
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = viewState.boundingBox) {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
LaunchedEffect(true) {
eventFlow.collect { event ->
when (event) {
MountainsScreenEvent.OnZoomAll -> {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
}
}
}
}
Teraz, gdy uruchomisz aplikację, mapa będzie początkowo skupiona na obszarze, w którym pojawią się znaczniki. Możesz zmienić położenie i powiększenie, a kliknięcie przycisku powiększenia spowoduje ponowne ustawienie ostrości mapy na obszar znacznika. To postęp! Ale na mapie powinno być coś, na co można popatrzeć. W następnym kroku dowiesz się, jak to zrobić.
9. Podstawowe znaczniki
W tym kroku dodasz do mapy znaczniki, które będą reprezentować ciekawe miejsca, które chcesz wyróżnić na mapie. Użyjesz listy gór, która została udostępniona w projekcie początkowym, i dodasz te miejsca jako znaczniki na mapie.
Zacznij od dodania bloku treści do GoogleMap
. Będzie wiele typów znaczników, więc dodaj instrukcję when
, aby przejść do każdego typu, a następnie w kolejnych krokach zaimplementuj każdy z nich.
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
Dodaj znaczniki
Dodaj adnotację BasicMarkersMapContent
do @GoogleMapComposable
. Pamiętaj, że w bloku treści GoogleMap
możesz używać tylko funkcji @GoogleMapComposable
. Obiekt mountains
zawiera listę obiektów Mountain
. Dodasz znacznik dla każdej góry na tej liście, używając lokalizacji, nazwy i wysokości z obiektu Mountain
. Lokalizacja jest używana do ustawiania parametru stanu Marker
, który z kolei kontroluje pozycję znacznika.
// ...
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMapComposable
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false }
) {
mountains.forEach { mountain ->
Marker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
tag = mountain,
onClick = { marker ->
onMountainClick(marker)
false
},
zIndex = if (mountain.is14er()) 5f else 2f
)
}
}
Uruchom aplikację, a zobaczysz dodane przez siebie znaczniki.
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 markera.
Projekt startowy zawiera funkcję pomocniczą vectorToBitmap
, która tworzy obiekty BitmapDescriptor
z obiektu @DrawableResource
.
Kod startowy zawiera ikonę góry baseline_filter_hdr_24.xml
, której użyjesz do dostosowania znaczników.
Funkcja vectorToBitmap
przekształca obiekt rysowalny wektorowo w BitmapDescriptor
do użycia w bibliotece map. Kolory ikon są ustawiane za pomocą instancji BitmapParameters
.
data class BitmapParameters(
@DrawableRes val id: Int,
@ColorInt val iconColor: Int,
@ColorInt val backgroundColor: Int? = null,
val backgroundAlpha: Int = 168,
val padding: Int = 16,
)
fun vectorToBitmap(context: Context, parameters: BitmapParameters): BitmapDescriptor {
// ...
}
Użyj funkcji vectorToBitmap
, aby utworzyć 2 dostosowane BitmapDescriptor
: jeden dla szczytów powyżej 14 tys. stóp, a drugi dla zwykłych gór. Następnie użyj parametru icon
funkcji Marker
, aby ustawić ikonę. Możesz też ustawić parametr anchor
, aby zmienić lokalizację punktu zakotwiczenia względem ikony. W przypadku tych okrągłych ikon lepiej jest używać środka.
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
// ...
) {
// Create mountainIcon and fourteenerIcon
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
)
)
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
)
)
mountains.forEach { mountain ->
val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
Marker(
// ...
anchor = Offset(0.5f, 0.5f),
icon = icon,
)
}
}
Uruchom aplikację i podziwiaj dostosowane markery. Przesuń przełącznik Show all
, aby zobaczyć pełny zestaw gór. Góry będą miały różne znaczniki w zależności od tego, czy są to czternastotysięczniki.
10. Znaczniki zaawansowane
AdvancedMarker
dodają dodatkowe funkcje do podstawowych Markers
. W tym kroku ustawisz zachowanie w przypadku kolizji i skonfigurujesz styl pinezki.
Dodaj @GoogleMapComposable
do funkcji AdvancedMarkersMapContent
. Przejdź w pętli po mountains
, dodając AdvancedMarker
dla każdego z nich.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
mountains.forEach { mountain ->
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
Zwróć uwagę na parametr collisionBehavior
. Jeśli ustawisz ten parametr na REQUIRED_AND_HIDES_OPTIONAL
, Twój znacznik zastąpi każdy znacznik o niższym priorytecie. Możesz to sprawdzić, powiększając podstawowy znacznik w porównaniu z zaawansowanym. Podstawowy znacznik będzie prawdopodobnie zawierać zarówno Twój znacznik, jak i znacznik umieszczony w tym samym miejscu na mapie bazowej. Zaawansowany znacznik spowoduje ukrycie znacznika o niższym priorytecie.
Uruchom aplikację, aby zobaczyć zaawansowane znaczniki. Upewnij się, że w dolnym rzędzie nawigacyjnym wybrana jest karta Advanced markers
.
Spersonalizowane AdvancedMarkers
Ikony wykorzystują podstawową i dodatkową paletę kolorów, aby odróżnić szczyty powyżej 14 tys. stóp od innych gór. Użyj funkcji vectorToBitmap
, aby utworzyć 2 BitmapDescriptor
: jeden dla szczytów powyżej 14 tys. stóp, a drugi dla pozostałych gór. Użyj tych ikon, aby utworzyć niestandardowy pinConfig
dla każdego typu. Na koniec przypnij pinezkę do odpowiedniego elementu AdvancedMarker
na podstawie funkcji is14er()
.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
)
)
val mountainPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(mountainIcon))
setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
build()
}
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
)
)
val fourteenerPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(fourteenerIcon))
setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
build()
}
mountains.forEach { mountain ->
val pin = if (mountain.is14er()) fourteenerPin else mountainPin
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
pinConfig = pin,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
11. Znaczniki w klastrach
W tym kroku użyjesz komponentu Clustering
, aby dodać grupowanie elementów na podstawie powiększenia.
Komponent Clustering
wymaga kolekcji elementów ClusterItem
. MountainClusterItem
implementuje interfejs ClusterItem
. Dodaj tę klasę do pliku ClusteringMarkersMapContent.kt
.
data class MountainClusterItem(
val mountain: Mountain,
val snippetString: String
) : ClusterItem {
override fun getPosition() = mountain.location
override fun getTitle() = mountain.name
override fun getSnippet() = snippetString
override fun getZIndex() = 0f
}
Teraz dodaj kod, aby utworzyć MountainClusterItem
z listy gór. Pamiętaj, że ten kod używa UnitsConverter
do konwersji na jednostki wyświetlania odpowiednie dla użytkownika na podstawie jego ustawień regionalnych. Konfiguruje się to w MainActivity
za pomocą CompositionLocal
.
@OptIn(MapsComposeExperimentalApi::class)
@Composable
@GoogleMapComposable
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
val unitsConverter = LocalUnitsConverter.current
val resources = LocalContext.current.resources
val mountainClusterItems by remember(mountains) {
mutableStateOf(
mountains.map { mountain ->
MountainClusterItem(
mountain = mountain,
snippetString = unitsConverter.toElevationString(resources, mountain.elevation)
)
}
)
}
Clustering(
items = mountainClusterItems,
)
}
Dzięki temu kodowi markery są grupowane na podstawie poziomu powiększenia. Ładnie i schludnie.
Dostosowywanie klastrów
Podobnie jak w przypadku innych typów znaczników, zgrupowane znaczniki można dostosowywać. Parametr clusterItemContent
w funkcji kompozycyjnej Clustering
ustawia niestandardowy blok kompozycyjny do renderowania elementu nieklastrowanego. Zaimplementuj funkcję @Composable
, aby utworzyć znacznik. Funkcja SingleMountain
renderuje komponent Material 3 Icon
z dostosowanym schematem kolorów tła.
W ClusteringMarkersMapContent.kt
utwórz klasę danych określającą schemat kolorów znacznika:
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
W ClusteringMarkersMapContent.kt
utwórz też funkcję kompozycyjną do renderowania ikony dla danego schematu kolorów:
@Composable
private fun SingleMountain(
colors: IconColor,
) {
Icon(
painterResource(id = R.drawable.baseline_filter_hdr_24),
tint = colors.iconColor,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.padding(1.dp)
.drawBehind {
drawCircle(color = colors.backgroundColor, style = Fill)
drawCircle(color = colors.borderColor, style = Stroke(width = 3f))
}
.padding(4.dp)
)
}
Teraz utwórz schemat kolorów dla szczytów powyżej 14 tys. stóp i inny schemat kolorów dla pozostałych gór. W bloku clusterItemContent
wybierz schemat kolorów w zależności od tego, czy dana góra jest czterotysięcznikiem.
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
// ...
val backgroundAlpha = 0.6f
val fourteenerColors = IconColor(
iconColor = MaterialTheme.colorScheme.onPrimary,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.primary
)
val otherColors = IconColor(
iconColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.secondary
)
// ...
Clustering(
items = mountainClusterItems,
clusterItemContent = { mountainItem ->
val colors = if (mountainItem.mountain.is14er()) {
fourteenerColors
} else {
otherColors
}
SingleMountain(colors)
},
)
}
Teraz uruchom aplikację, aby zobaczyć dostosowane wersje poszczególnych produktów.
12. 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ć Polyline
i Polygon
, aby wyświetlić je na mapie. Jeśli chcesz przymocować obraz do powierzchni ziemi, możesz użyć GroundOverlay
.
W tym zadaniu dowiesz się, jak rysować kształty, a konkretnie kontur wokół stanu Kolorado. Granice stanu Kolorado wyznaczają szerokości geograficzne od 37°N do 41°N oraz długości geograficzne od 102°03'W do 109°03'W. Dzięki temu narysowanie konturu jest dość proste.
Kod początkowy zawiera DMS
klasę, która umożliwia konwersję notacji stopni-minut-sekund na stopnie dziesiętne.
enum class Direction(val sign: Int) {
NORTH(1),
EAST(1),
SOUTH(-1),
WEST(-1)
}
/**
* Degrees, minutes, seconds utility class
*/
data class DMS(
val direction: Direction,
val degrees: Double,
val minutes: Double = 0.0,
val seconds: Double = 0.0,
)
fun DMS.toDecimalDegrees(): Double =
(degrees + (minutes / 60) + (seconds / 3600)) * direction.sign
Za pomocą klasy DMS możesz narysować granicę Kolorado, definiując 4 lokalizacje LatLng
w rogach i renderując je jako Polygon
. Dodaj do pliku MountainMap.kt
ten kod:
@Composable
@GoogleMapComposable
fun ColoradoPolygon() {
val north = 41.0
val south = 37.0
val east = DMS(WEST, 102.0, 3.0).toDecimalDegrees()
val west = DMS(WEST, 109.0, 3.0).toDecimalDegrees()
val locations = listOf(
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
LatLng(north, west),
)
Polygon(
points = locations,
strokeColor = MaterialTheme.colorScheme.tertiary,
strokeWidth = 3F,
fillColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
)
}
Teraz wywołaj funkcję ColoradoPolyon()
w bloku treści GoogleMap
.
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
Aplikacja obrysowuje teraz stan Kolorado, wypełniając go delikatnym kolorem.
13. Dodawanie warstwy KML i paska skali
W tej ostatniej sekcji zarysujesz różne pasma górskie i dodasz do mapy skalę.
Zaznacz pasma górskie
Wcześniej narysowano kontur wokół Kolorado. Tutaj dodasz do mapy bardziej złożone kształty. Kod początkowy zawiera plik KML (Keyhole Markup Language), który z grubsza przedstawia ważne pasma górskie. Biblioteka narzędziowa pakietu Maps SDK na Androida zawiera funkcję dodawania warstwy KML do mapy. W MountainMap.kt
dodaj wywołanie MapEffect
w bloku treści GoogleMap
po bloku when
. Funkcja MapEffect
jest wywoływana z obiektem GoogleMap
. Może to być przydatne połączenie między interfejsami API, które nie obsługują kompozycji, a bibliotekami wymagającymi obiektu GoogleMap
.
fun MountainMap(
// ...
) {
var isMapLoaded by remember { mutableStateOf(false) }
val context = LocalContext.current
GoogleMap(
// ...
) {
// ...
when (selectedMarkerType) {
// ...
}
// This code belongs inside the GoogleMap content block, but outside of
// the 'when' statement
MapEffect(key1 = true) {map ->
val layer = KmlLayer(map, R.raw.mountain_ranges, context)
layer.addLayerToMap()
}
}
Dodawanie skali mapy
Na koniec dodasz do mapy skalę. ScaleBar
implementuje komponent skali, który można dodać do mapy. Pamiętaj, że ScaleBar
nie jest
@GoogleMapComposable
, dlatego nie można go dodać do treści GoogleMap
. Zamiast tego dodaj ją do elementu Box
, który zawiera mapę.
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
Uruchom aplikację, aby zobaczyć w pełni zaimplementowany przewodnik.
14. Pobieranie kodu rozwiązania
Aby pobrać kod ukończonego ćwiczenia, możesz użyć tych poleceń:
- Sklonuj repozytorium, jeśli masz zainstalowany program
git
.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Możesz też kliknąć ten przycisk, aby pobrać kod źródłowy.
- Po otrzymaniu kodu otwórz projekt znajdujący się w katalogu
solution
w Android Studio.
15. Gratulacje
Gratulacje! Omówiliśmy wiele tematów i mamy nadzieję, że lepiej rozumiesz podstawowe funkcje pakietu Maps SDK na Androida.
Więcej informacji
- Maps SDK na Androida – twórz dynamiczne, interaktywne i dostosowane mapy, lokalizacje i funkcje geoprzestrzenne dla aplikacji na Androida.
- Biblioteka Maps Compose – zestaw funkcji i typów danych typu open source, których możesz używać z Jetpack Compose do tworzenia aplikacji.
- android-maps-compose – 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