Dodawanie mapy do aplikacji na Androida (Kotlin z Compose)

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

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

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.

  1. W konsoli Google Cloud kliknij menu projektu i wybierz projekt, którego chcesz użyć w tym samouczku.

  1. 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.
  2. 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.

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

  1. 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:

  1. W Android Studio otwórz plik build.gradle.kts najwyższego poziomu i dodaj podany niżej kod do elementu dependencies w sekcji buildscript.
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. Otwórz plik build.gradle.kts na poziomie modułu i dodaj ten kod do elementu plugins.
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. W pliku build.gradle.kts na poziomie modułu sprawdź, czy wartości targetSdkcompileSdk wynoszą co najmniej 34.
  4. Zapisz plik i zsynchronizuj projekt z Gradle.
  5. Otwórz plik secrets.properties w katalogu najwyższego poziomu i dodaj ten kod: Zastąp YOUR_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
    
  6. Zapisz plik.
  7. Utwórz plik local.defaults.properties w katalogu najwyższego poziomu, czyli w tym samym folderze co plik secrets.properties, a następnie dodaj ten kod.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    Ten plik służy jako lokalizacja kopii zapasowej klucza interfejsu API, jeśli nie można znaleźć pliku 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 pliku secrets.properties, aby podać klucz interfejsu API.
  8. Zapisz plik.
  9. W pliku AndroidManifest.xml otwórz com.google.android.geo.API_KEY i zaktualizuj atrybut android: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}" />
    
  10. 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 na secrets.properties, defaultPropertiesFileName na local.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:

  1. Utwórz identyfikator mapy.
  2. 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: MountainsRepositoryMountainsViewModel

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

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.

Przycisk zakresów

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.

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)

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ć PolylinePolygon, 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 DMSklasę, 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ń:

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

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