Menambahkan peta ke aplikasi Android (Kotlin dengan Compose)

1. Sebelum Memulai

Codelab ini akan mengajarkan cara mengintegrasikan Maps SDK for Android dengan aplikasi Anda dan menggunakan fitur intinya dengan membuat aplikasi yang menampilkan peta pegunungan di Colorado, AS, menggunakan berbagai jenis penanda. Selain itu, Anda akan mempelajari cara menggambar bentuk lain di peta.

Tampilannya akan terlihat seperti berikut setelah Anda menyelesaikan codelab:

Prasyarat

Yang akan Anda lakukan

  • Mengaktifkan dan menggunakan library Maps Compose untuk Maps SDK for Android guna menambahkan GoogleMap ke aplikasi Android
  • Menambahkan dan menyesuaikan penanda
  • Menggambar poligon di peta
  • Mengontrol titik pandang kamera secara terprogram

Yang Anda butuhkan

2. Memulai persiapan

Untuk langkah pengaktifan berikut, Anda harus mengaktifkan Maps SDK for Android.

Menyiapkan Google Maps Platform

Jika Anda belum memiliki akun Google Cloud Platform dan project dengan penagihan diaktifkan, lihat panduan Memulai Google Maps Platform untuk membuat akun penagihan dan project.

  1. Di Cloud Console, klik menu drop-down project lalu pilih project yang ingin Anda gunakan untuk codelab ini.

  1. Aktifkan API dan SDK Google Maps Platform yang diperlukan untuk codelab ini di Google Cloud Marketplace. Untuk melakukannya, ikuti langkah-langkah dalam video ini atau dokumentasi ini.
  2. Buat kunci API di halaman Kredensial di Cloud Console. Anda dapat mengikuti langkah-langkah dalam video ini atau dokumentasi ini. Semua permintaan ke Google Maps Platform memerlukan kunci API.

3. Mulai cepat

Untuk membantu Anda memulai secepatnya, berikut beberapa kode awal untuk membantu Anda mengikuti codelab ini. Anda dapat langsung ke bagian solusi, tetapi jika Anda ingin mengikuti semua langkah untuk membuatnya sendiri, baca semuanya.

  1. Lakukan clone repositori jika sudah menginstal git.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

Atau, Anda dapat mengklik tombol berikut untuk mendownload kode sumber.

  1. Setelah mendapatkan kode, lanjutkan dan buka project yang ada dalam direktori starter di Android Studio.

4. Tambahkan kunci API Anda ke project

Bagian ini menjelaskan cara menyimpan kunci API sehingga dapat dirujuk dengan aman oleh aplikasi Anda. Anda tidak boleh memasukkan kunci API ke dalam sistem kontrol versi Anda, jadi sebaiknya simpan kunci tersebut dalam file secrets.properties, yang akan ditempatkan di salinan lokal direktori root project Anda. Untuk informasi selengkapnya tentang file secrets.properties, lihat File properti Gradle.

Untuk menyederhanakan tugas ini, sebaiknya Anda menggunakan Plugin Secrets Gradle untuk Android.

Untuk menginstal Plugin Secrets Gradle untuk Android di project Google Maps:

  1. Di Android Studio, buka file build.gradle.kts tingkat teratas dan tambahkan kode berikut ke elemen dependencies di bagian buildscript.
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. Buka file build.gradle.kts tingkat modul dan tambahkan kode berikut ke elemen plugins.
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. Di file build.gradle.kts tingkat modul, pastikan targetSdk dan compileSdk ditetapkan ke minimal 34.
  4. Simpan file dan sinkronkan project Anda dengan Gradle.
  5. Buka file secrets.properties di direktori tingkat teratas, lalu tambahkan kode berikut. Ganti YOUR_API_KEY dengan kunci API Anda. Simpan kunci Anda dalam file ini karena secrets.properties tidak di-check in ke dalam sistem kontrol versi.
    MAPS_API_KEY=YOUR_API_KEY
    
  6. Simpan file.
  7. Buat file local.defaults.properties di direktori tingkat teratas, folder yang sama dengan file secrets.properties, lalu tambahkan kode berikut.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    File ini ditujukan untuk menyediakan lokasi cadangan Kunci API jika file secrets.properties tidak dapat ditemukan agar build tidak gagal. Hal ini akan terjadi saat Anda meng-clone aplikasi dari sistem kontrol versi dan Anda belum membuat file secrets.properties secara lokal untuk memberikan kunci API Anda.
  8. Simpan file.
  9. Dalam file AndroidManifest.xml, buka com.google.android.geo.API_KEY, lalu perbarui atribut android:value. Jika tag <meta-data> tidak ada, buat tag tersebut sebagai turunan dari tag <application>.
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${MAPS_API_KEY}" />
    
  10. Di Android Studio, buka file build.gradle.kts tingkat modul dan edit properti secrets. Jika properti secrets tidak ada, tambahkan.Edit properti plugin untuk menetapkan propertiesFileName ke secrets.properties, menetapkan defaultPropertiesFileName ke local.defaults.properties, dan menetapkan properti lainnya.
    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. Menambahkan Google Maps

Di bagian ini, Anda akan menambahkan Google Maps sehingga dimuat saat Anda meluncurkan aplikasi.

Menambahkan dependensi Maps Compose

Setelah kunci API Anda bisa diakses dalam aplikasi, langkah berikutnya adalah menambahkan dependensi Maps SDK for Android ke file build.gradle.kts aplikasi Anda. Untuk membuat dengan Jetpack Compose, gunakan library Maps Compose yang menyediakan elemen Maps SDK for Android sebagai fungsi composable dan jenis data.

build.gradle.kts

Dalam file build.gradle.kts tingkat aplikasi, ganti dependensi Maps SDK for Android non-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")
}

dengan padanannya yang dapat dikomposisikan:

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

Menambahkan composable Peta Google

Di MountainMap.kt, tambahkan composable GoogleMap di dalam composable Box yang di-nest di dalam composable 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 }
        )

        // ...
    }
}

Sekarang bangun dan jalankan aplikasi. Lihat! Anda akan melihat peta yang berpusat di Pulau Null yang terkenal, juga dikenal sebagai lintang nol dan bujur nol. Nanti, Anda akan mempelajari cara memosisikan peta ke lokasi dan tingkat zoom yang Anda inginkan, tetapi untuk saat ini, rayakan kemenangan pertama Anda.

6. Gaya visual peta berbasis cloud

Anda dapat menyesuaikan gaya peta menggunakan Penataan gaya peta berbasis cloud.

Membuat ID Peta

Jika Anda belum membuat ID peta dengan gaya peta terkait, lihat panduan ID Peta untuk menyelesaikan langkah-langkah berikut:

  1. Membuat ID peta.
  2. Mengaitkan ID peta ke gaya peta.

Menambahkan ID Peta ke aplikasi Anda

Untuk menggunakan ID peta yang Anda buat, saat membuat instance composable GoogleMap, gunakan ID peta saat membuat objek GoogleMapOptions yang ditetapkan ke parameter googleMapOptionsFactory dalam konstruktor.

GoogleMap(
    // ...
    googleMapOptionsFactory = {
        GoogleMapOptions().mapId("MyMapId")
    }
)

Setelah menyelesaikannya, jalankan aplikasi untuk melihat peta dengan gaya yang Anda pilih.

7. Memuat data penanda

Tugas utama aplikasi ini adalah memuat kumpulan gunung dari penyimpanan lokal dan menampilkannya di GoogleMap. Pada langkah ini, Anda akan menjelajahi infrastruktur yang disediakan untuk memuat data gunung dan menampilkannya ke UI.

Mountain

Class data Mountain menyimpan semua data tentang setiap gunung.

data class Mountain(
    val id: Int,
    val name: String,
    val location: LatLng,
    val elevation: Meters,
)

Perhatikan bahwa pegunungan akan dipartisi berdasarkan ketinggiannya. Gunung yang memiliki ketinggian setidaknya 14.000 kaki disebut fourteener. Kode awal menyertakan fungsi ekstensi untuk melakukan pemeriksaan ini bagi Anda.

/**
 * 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

Class MountainsScreenViewState menyimpan semua data yang diperlukan untuk merender tampilan. Status dapat berupa Loading atau MountainList, bergantung pada apakah daftar gunung telah selesai dimuat.

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

Class yang disediakan: MountainsRepository dan MountainsViewModel

Dalam project permulaan, class MountainsRepository telah disediakan untuk Anda. Class ini membaca daftar tempat pegunungan yang disimpan dalam GPS Exchange Format, atau file GPX, top_peaks.gpx. Memanggil mountainsRepository.loadMountains() akan menampilkan 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 adalah class ViewModel yang memuat koleksi pegunungan dan mengekspos koleksi tersebut serta bagian lain dari status UI melalui mountainsScreenViewState. mountainsScreenViewState adalah hot StateFlow yang dapat diamati UI sebagai status yang dapat diubah menggunakan fungsi ekstensi collectAsState.

Dengan mengikuti prinsip arsitektur yang baik, MountainsViewModel menyimpan semua status aplikasi. UI mengirim interaksi pengguna ke model tampilan menggunakan metode 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) }
  }
}

Jika penasaran dengan penerapan class ini, Anda dapat mengaksesnya di GitHub atau membuka class MountainsRepository dan MountainsViewModel di Android Studio.

Menggunakan ViewModel

Model tampilan digunakan di MainActivity untuk mendapatkan viewState. Anda akan menggunakan viewState untuk merender penanda nanti dalam codelab ini. Perhatikan bahwa kode ini sudah disertakan dalam project awal dan ditampilkan di sini hanya sebagai referensi.

val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value

8. Posisikan kamera

Default GoogleMap berpusat pada lintang nol, bujur nol. Penanda yang akan Anda render berada di Negara Bagian Colorado di Amerika Serikat. viewState yang disediakan oleh model tampilan menampilkan LatLngBounds yang berisi semua penanda.

Di MountainMap.kt, buat CameraPositionState yang diinisialisasi ke tengah kotak pembatas. Tetapkan parameter cameraPositionState dari GoogleMap ke variabel cameraPositionState yang baru saja Anda buat.

fun MountainMap(
    // ...
) {
    // ...
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
    }

    GoogleMap(
        // ...
        cameraPositionState = cameraPositionState,
    )
}

Sekarang jalankan kode dan lihat peta yang berpusat di Colorado.

Melakukan zoom ke cakupan penanda

Untuk benar-benar memfokuskan peta pada penanda, tambahkan fungsi zoomAll ke akhir file MountainMap.kt. Perhatikan bahwa fungsi ini memerlukan CoroutineScope karena menganimasikan kamera ke lokasi baru adalah operasi asinkron yang memerlukan waktu untuk diselesaikan.

fun zoomAll(
    scope: CoroutineScope,
    cameraPositionState: CameraPositionState,
    boundingBox: LatLngBounds
) {
    scope.launch {
        cameraPositionState.animate(
            update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
            durationMs = 1000
        )
    }
}

Selanjutnya, tambahkan kode untuk memanggil fungsi zoomAll setiap kali batas di sekitar kumpulan penanda berubah atau saat pengguna mengklik tombol rentang zoom di panel TopApp. Perhatikan bahwa tombol rentang zoom sudah terhubung untuk mengirim peristiwa ke model tampilan. Anda hanya perlu mengumpulkan peristiwa tersebut dari model tampilan dan memanggil fungsi zoomAll sebagai respons.

Tombol jangkauan

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

Sekarang, saat Anda menjalankan aplikasi, peta akan dimulai dengan fokus pada area tempat penanda akan ditempatkan. Anda dapat memosisikan ulang dan mengubah zoom, serta mengklik tombol batas zoom akan memfokuskan kembali peta di sekitar area penanda. Itu adalah kemajuan! Namun, peta harus memiliki sesuatu untuk dilihat. Dan itulah yang akan Anda lakukan di langkah berikutnya.

9. Penanda dasar

Pada langkah ini, Anda menambahkan Penanda ke peta yang mewakili lokasi menarik yang ingin Anda sorot pada peta. Anda akan menggunakan daftar gunung yang telah disediakan dalam project permulaan dan menambahkan tempat ini sebagai penanda di peta.

Mulai dengan menambahkan blok konten ke GoogleMap. Akan ada beberapa jenis penanda, jadi tambahkan pernyataan when untuk membuat cabang ke setiap jenis dan Anda akan menerapkannya satu per satu dalam langkah-langkah berikutnya.

GoogleMap(
    // ...
) {
    when (selectedMarkerType) {
        MarkerType.Basic -> {
            BasicMarkersMapContent(
                mountains = viewState.mountains,
            )
        }

        MarkerType.Advanced -> {
            AdvancedMarkersMapContent(
                mountains = viewState.mountains,
            )
        }

        MarkerType.Clustered -> {
            ClusteringMarkersMapContent(
                mountains = viewState.mountains,
            )
        }
    }
}

Menambahkan penanda

Anotasikan BasicMarkersMapContent dengan @GoogleMapComposable. Perhatikan bahwa Anda hanya dapat menggunakan fungsi @GoogleMapComposable di blok konten GoogleMap. Objek mountains memiliki daftar objek Mountain. Anda akan menambahkan penanda untuk setiap gunung dalam daftar tersebut, menggunakan lokasi, nama, dan ketinggian dari objek Mountain. Lokasi digunakan untuk menetapkan parameter status Marker yang, pada gilirannya, mengontrol posisi penanda.

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

Lanjutkan dan jalankan aplikasi, Anda akan melihat penanda yang baru saja Anda tambahkan.

Menyesuaikan penanda

Ada beberapa opsi penyesuaian untuk penanda yang baru saja Anda tambahkan untuk membantu menonjolkan penanda dan memberikan informasi yang berguna kepada pengguna. Dalam tugas ini, Anda akan menjelajahi beberapa di antaranya dengan menyesuaikan gambar setiap penanda.

Project permulaan mencakup fungsi helper, vectorToBitmap, untuk membuat BitmapDescriptor dari @DrawableResource.

Kode awal menyertakan ikon gunung, baseline_filter_hdr_24.xml, yang akan Anda gunakan untuk menyesuaikan penanda.

Fungsi vectorToBitmap mengonversi vektor drawable menjadi BitmapDescriptor untuk digunakan dengan library Maps. Warna ikon ditetapkan menggunakan instance 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 {
    // ...
}

Gunakan fungsi vectorToBitmap untuk membuat dua BitmapDescriptor yang disesuaikan; satu untuk gunung dengan ketinggian lebih dari 4.267 meter dan satu untuk gunung biasa. Kemudian, gunakan parameter icon dari composable Marker untuk menyetel ikon. Selain itu, tetapkan parameter anchor untuk mengubah lokasi penanda relatif terhadap ikon. Menggunakan bagian tengah akan lebih baik untuk ikon melingkar ini.

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

Jalankan aplikasi dan kagumi penanda yang disesuaikan. Aktifkan tombol Show all untuk melihat rangkaian pegunungan lengkap. Pegunungan akan memiliki penanda yang berbeda, bergantung pada apakah pegunungan tersebut merupakan pegunungan empat belas ribu kaki.

10. Penanda lanjutan

AdvancedMarker menambahkan fitur ekstra ke Markers dasar. Pada langkah ini, Anda akan menetapkan perilaku tabrakan dan mengonfigurasi gaya pin.

Tambahkan @GoogleMapComposable ke fungsi AdvancedMarkersMapContent. Lakukan pengulangan pada mountains dengan menambahkan AdvancedMarker untuk setiap mountains.

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

Perhatikan parameter collisionBehavior. Dengan menyetel parameter ini ke REQUIRED_AND_HIDES_OPTIONAL, penanda Anda akan menggantikan penanda dengan prioritas yang lebih rendah. Anda dapat melihatnya dengan memperbesar penanda dasar dibandingkan dengan penanda lanjutan. Penanda dasar kemungkinan akan memiliki penanda Anda dan penanda yang ditempatkan di lokasi yang sama dalam peta dasar. Penanda lanjutan akan menyebabkan penanda berprioritas lebih rendah disembunyikan.

Jalankan aplikasi untuk melihat Penanda lanjutan. Pastikan untuk memilih tab Advanced markers di baris navigasi bawah.

AdvancedMarkers yang disesuaikan

Ikon menggunakan skema warna primer dan sekunder untuk membedakan antara gunung berketinggian lebih dari 4.267 meter dan gunung lainnya. Gunakan fungsi vectorToBitmap untuk membuat dua BitmapDescriptor; satu untuk gunung berketinggian lebih dari 4.000 meter dan satu untuk gunung lainnya. Gunakan ikon tersebut untuk membuat pinConfig kustom untuk setiap jenis. Terakhir, terapkan pin ke AdvancedMarker yang sesuai berdasarkan fungsi 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. Marker berkelompok

Pada langkah ini, Anda akan menggunakan composable Clustering untuk menambahkan pengelompokan item berbasis zoom.

Composable Clustering memerlukan kumpulan ClusterItem. MountainClusterItem mengimplementasikan antarmuka ClusterItem. Tambahkan class ini ke file 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
}

Sekarang tambahkan kode untuk membuat MountainClusterItem dari daftar gunung. Perhatikan bahwa kode ini menggunakan UnitsConverter untuk mengonversi ke unit tampilan yang sesuai untuk pengguna berdasarkan lokalitasnya. Hal ini disiapkan di MainActivity menggunakan 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,
    )
}

Dengan kode tersebut, penanda dikelompokkan berdasarkan tingkat zoom. Bagus dan rapi!

Menyesuaikan cluster

Seperti jenis penanda lainnya, penanda yang dikelompokkan dapat disesuaikan. Parameter clusterItemContent dari composable Clustering menetapkan blok composable kustom untuk merender item yang tidak dikelompokkan. Terapkan fungsi @Composable untuk membuat penanda. Fungsi SingleMountain merender composable Icon Material 3 dengan skema warna latar belakang yang disesuaikan.

Di ClusteringMarkersMapContent.kt, buat class data yang menentukan skema warna untuk penanda:

data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)

Selain itu, di ClusteringMarkersMapContent.kt, buat fungsi composable untuk merender ikon untuk skema warna tertentu:

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

Sekarang buat skema warna untuk gunung berketinggian lebih dari 4.000 meter dan skema warna lain untuk gunung lainnya. Di blok clusterItemContent, pilih skema warna berdasarkan apakah gunung yang diberikan merupakan gunung dengan ketinggian lebih dari 14.000 kaki atau tidak.

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

Sekarang, jalankan aplikasi untuk melihat versi item individual yang disesuaikan.

12. Gambar di peta

Meskipun Anda telah mempelajari satu cara untuk menggambar di peta (dengan menambahkan penanda), SDK Maps for Android mendukung berbagai cara lain untuk menggambar agar dapat menampilkan informasi yang berguna pada peta.

Misalnya, jika ingin menampilkan rute dan area pada peta, Anda dapat menggunakan Polyline dan Polygon untuk menampilkannya pada peta. Atau, jika Anda ingin memperbaiki gambar pada permukaan bumi, Anda dapat menggunakan GroundOverlay.

Dalam tugas ini, Anda akan mempelajari cara menggambar bentuk, khususnya garis luar di sekitar Negara Bagian Colorado. Batas Colorado ditentukan antara 37°LU dan 41°LU serta 102°03'BB dan 109°03'BB. Hal ini membuat penggambaran garis luar menjadi cukup mudah.

Kode awal menyertakan class DMS untuk mengonversi dari notasi derajat-menit-detik ke derajat desimal.

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

Dengan class DMS, Anda dapat menggambar batas Colorado dengan menentukan empat lokasi sudut LatLng dan merendernya sebagai Polygon. Tambahkan kode berikut ke MountainMap.kt

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

Sekarang panggil ColoradoPolyon() di dalam blok konten GoogleMap.

@Composable
fun MountainMap(
    // ...
) {
   Box(
    // ...
    ) {
        GoogleMap(
            // ...
        ) {
            ColoradoPolygon()
        }
    }
}

Sekarang aplikasi menguraikan Negara Bagian Colorado sambil memberikan pengisian yang halus.

13. Menambahkan lapisan KML dan skala batang

Di bagian terakhir ini, Anda akan menguraikan secara kasar berbagai pegunungan dan menambahkan skala batang ke peta.

Gambarkan pegunungan

Sebelumnya, Anda telah menggambar garis batas di sekitar Colorado. Di sini, Anda akan menambahkan bentuk yang lebih kompleks ke peta. Kode awal mencakup file Keyhole Markup Language, atau KML, yang secara kasar menguraikan pegunungan penting. Library Utilitas Maps SDK for Android memiliki fungsi untuk menambahkan lapisan KML ke peta. Di MountainMap.kt, tambahkan panggilan MapEffect di blok konten GoogleMap setelah blok when. Fungsi MapEffect dipanggil dengan objek GoogleMap. Composable ini dapat berfungsi sebagai jembatan yang berguna antara API dan library non-composable yang memerlukan objek 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()
      }
    }

Menambahkan skala peta

Sebagai tugas terakhir, Anda akan menambahkan skala ke peta. ScaleBar menerapkan composable skala yang dapat ditambahkan ke peta. Perhatikan bahwa ScaleBar bukan

@GoogleMapComposable dan oleh karena itu tidak dapat ditambahkan ke konten GoogleMap. Sebagai gantinya, Anda menambahkannya ke Box yang memuat peta.

Box(
  // ...
) {
    GoogleMap(
      // ...
    ) {
        // ...
    }

    ScaleBar(
        modifier = Modifier
            .padding(top = 5.dp, end = 15.dp)
            .align(Alignment.TopEnd),
        cameraPositionState = cameraPositionState
    )
    // ...
}

Jalankan aplikasi untuk melihat codelab yang telah diimplementasikan sepenuhnya.

14. Mendapatkan kode solusi

Untuk mendownload kode codelab yang sudah selesai, Anda dapat menggunakan perintah berikut:

  1. Lakukan clone repositori jika sudah menginstal git.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

Atau, Anda dapat mengklik tombol berikut untuk mendownload kode sumber.

  1. Setelah mendapatkan kode, lanjutkan dan buka project yang ada dalam direktori solution di Android Studio.

15. Selamat

Selamat! Anda telah mempelajari begitu banyak konten dan semoga Anda memiliki pemahaman yang lebih baik tentang fitur inti yang ditawarkan di Maps SDK for Android.

Pelajari lebih lanjut

  • Maps SDK for Android - Buat peta, lokasi, dan pengalaman geospasial yang dinamis, interaktif, dan disesuaikan untuk aplikasi Android Anda.
  • Library Maps Compose - sekumpulan fungsi composable dan jenis data open source yang dapat Anda gunakan dengan Jetpack Compose untuk membuat aplikasi.
  • android-maps-compose - kode contoh di GitHub yang menunjukkan semua fitur yang dibahas dalam codelab ini dan lainnya.
  • Codelab Kotlin lainnya untuk membuat aplikasi Android dengan Google Maps Platform