เพิ่มแผนที่ลงในแอป Android (Kotlin พร้อม Compose)

1. ก่อนที่คุณจะเริ่มต้น

Codelab นี้จะสอนวิธีผสานรวม Maps SDK สำหรับ Android กับแอปและใช้ฟีเจอร์หลักของ SDK โดยการสร้างแอปที่แสดงแผนที่ภูเขาในโคโลราโด สหรัฐอเมริกา โดยใช้เครื่องหมายประเภทต่างๆ นอกจากนี้ คุณยังจะได้เรียนรู้วิธีวาดรูปร่างอื่นๆ บนแผนที่ด้วย

เมื่อทำ Codelab เสร็จแล้ว แอปจะมีลักษณะดังนี้

ข้อกำหนดเบื้องต้น

สิ่งที่คุณต้องดำเนินการ

  • เปิดใช้และใช้ไลบรารี Maps Compose สำหรับ Maps SDK สำหรับ Android เพื่อเพิ่ม GoogleMap ลงในแอป Android
  • เพิ่มและปรับแต่งเครื่องหมาย
  • วาดรูปหลายเหลี่ยมบนแผนที่
  • ควบคุมมุมมองของกล้องโดยใช้โปรแกรม

สิ่งที่คุณต้องมี

2. ตั้งค่า

สำหรับขั้นตอนการเปิดใช้ต่อไปนี้ คุณต้องเปิดใช้ Maps SDK สำหรับ Android

ตั้งค่า Google Maps Platform

หากยังไม่มีบัญชี Google Cloud Platform และโปรเจ็กต์ที่เปิดใช้การเรียกเก็บเงิน โปรดดูคู่มือเริ่มต้นใช้งาน Google Maps Platform เพื่อสร้างบัญชีสำหรับการเรียกเก็บเงินและโปรเจ็กต์

  1. ใน Cloud Console ให้คลิกเมนูแบบเลื่อนลงของโปรเจ็กต์ แล้วเลือกโปรเจ็กต์ที่ต้องการใช้สำหรับ Codelab นี้

  1. เปิดใช้ Google Maps Platform APIs และ SDK ที่จำเป็นสำหรับ Codelab นี้ใน Google Cloud Marketplace โดยทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้
  2. สร้างคีย์ API ในหน้าข้อมูลเข้าสู่ระบบของ Cloud Console คุณสามารถทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้ คำขอทั้งหมดไปยัง Google Maps Platform ต้องใช้คีย์ API

3. การเริ่มใช้งานอย่างง่าย

เรามีโค้ดเริ่มต้นที่จะช่วยให้คุณเริ่มต้นใช้งานได้อย่างรวดเร็วที่สุด และช่วยให้คุณทำตาม Codelab นี้ได้ คุณสามารถข้ามไปยังโซลูชันได้ แต่หากต้องการทำตามขั้นตอนทั้งหมดเพื่อสร้างโซลูชันด้วยตนเอง โปรดอ่านต่อ

  1. โคลนที่เก็บหากคุณติดตั้ง git ไว้
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

หรือจะคลิกปุ่มต่อไปนี้เพื่อดาวน์โหลดซอร์สโค้ดก็ได้

  1. เมื่อได้รับโค้ดแล้ว ให้เปิดโปรเจ็กต์ที่อยู่ในไดเรกทอรี starter ใน Android Studio

4. เพิ่มคีย์ API ลงในโปรเจ็กต์

ส่วนนี้อธิบายวิธีจัดเก็บคีย์ API เพื่อให้แอปอ้างอิงได้อย่างปลอดภัย คุณไม่ควรเช็คอินคีย์ API ในระบบควบคุมเวอร์ชัน ดังนั้นเราขอแนะนำให้จัดเก็บคีย์ในไฟล์ secrets.properties ซึ่งจะอยู่ในสำเนาในเครื่องของไดเรกทอรีรากของโปรเจ็กต์ ดูข้อมูลเพิ่มเติมเกี่ยวกับไฟล์ secrets.properties ได้ที่ไฟล์พร็อพเพอร์ตี้ Gradle

เราขอแนะนำให้ใช้ปลั๊กอินข้อมูลลับ Gradle สำหรับ Android เพื่อให้งานนี้มีประสิทธิภาพมากขึ้น

วิธีติดตั้งปลั๊กอินข้อมูลลับ Gradle สำหรับ Android ในโปรเจ็กต์ Google Maps

  1. ใน Android Studio ให้เปิดไฟล์ build.gradle.kts ระดับบนสุด แล้วเพิ่มโค้ดต่อไปนี้ลงในองค์ประกอบ dependencies ภายใน buildscript
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. เปิดไฟล์ build.gradle.kts ระดับโมดูล แล้วเพิ่มโค้ดต่อไปนี้ลงในองค์ประกอบ plugins
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. ในไฟล์ build.gradle.kts ระดับโมดูล ให้ตรวจสอบว่าได้ตั้งค่า targetSdk และ compileSdk เป็นอย่างน้อย 34
  4. บันทึกไฟล์และซิงค์โปรเจ็กต์กับ Gradle
  5. เปิดไฟล์ secrets.properties ในไดเรกทอรีระดับบนสุด แล้วเพิ่มโค้ดต่อไปนี้ แทนที่ YOUR_API_KEY ด้วยคีย์ API ของคุณ จัดเก็บคีย์ไว้ในไฟล์นี้เนื่องจากระบบจะไม่ตรวจสอบ secrets.properties ในระบบควบคุมเวอร์ชัน
    MAPS_API_KEY=YOUR_API_KEY
    
  6. บันทึกไฟล์
  7. สร้างไฟล์ local.defaults.properties ในไดเรกทอรีระดับบนสุด ซึ่งเป็นโฟลเดอร์เดียวกับไฟล์ secrets.properties แล้วเพิ่มโค้ดต่อไปนี้
        MAPS_API_KEY=DEFAULT_API_KEY
    
    จุดประสงค์ของไฟล์นี้คือการระบุตำแหน่งสำรองสำหรับคีย์ API ในกรณีที่ไม่พบไฟล์ secrets.properties เพื่อให้การสร้างไม่ล้มเหลว ปัญหานี้จะเกิดขึ้นเมื่อคุณโคลนแอปจากระบบควบคุมเวอร์ชันและยังไม่ได้สร้างไฟล์ secrets.properties ในเครื่องเพื่อระบุคีย์ API
  8. บันทึกไฟล์
  9. ในไฟล์ AndroidManifest.xml ให้ไปที่ com.google.android.geo.API_KEY แล้วอัปเดตแอตทริบิวต์ android:value หากไม่มีแท็ก <meta-data> ให้สร้างเป็นแท็กย่อยของแท็ก <application>
        <meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="${MAPS_API_KEY}" />
    
  10. ใน Android Studio ให้เปิดไฟล์ build.gradle.kts ระดับโมดูล แล้วแก้ไขพร็อพเพอร์ตี้ secrets หากไม่มีพร็อพเพอร์ตี้ secrets ให้เพิ่มพร็อพเพอร์ตี้นั้น แก้ไขพร็อพเพอร์ตี้ของปลั๊กอินเพื่อตั้งค่า propertiesFileName เป็น secrets.properties ตั้งค่า defaultPropertiesFileName เป็น local.defaults.properties และตั้งค่าพร็อพเพอร์ตี้อื่นๆ
    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. เพิ่ม Google Maps

ในส่วนนี้ คุณจะเพิ่ม Google Map เพื่อให้โหลดเมื่อเปิดแอป

เพิ่มการอ้างอิง Maps Compose

ตอนนี้คุณเข้าถึงคีย์ API ภายในแอปได้แล้ว ขั้นตอนถัดไปคือการเพิ่มทรัพยากร Dependency ของ Maps SDK สำหรับ Android ลงในไฟล์ build.gradle.kts ของแอป หากต้องการสร้างด้วย Jetpack Compose ให้ใช้ไลบรารี Maps Compose ซึ่งมีองค์ประกอบของ Maps SDK สำหรับ Android เป็นฟังก์ชันที่ใช้ร่วมกันได้และประเภทข้อมูล

build.gradle.kts

ในไฟล์ build.gradle.kts ระดับแอป ให้แทนที่ทรัพยากร Dependency ของ Maps SDK สำหรับ Android ที่ไม่ใช่ 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")
}

กับฟังก์ชันที่ประกอบกันได้ที่เกี่ยวข้อง

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

เพิ่ม Composable ของ Google Maps

ใน MountainMap.kt ให้เพิ่ม GoogleMap ที่ใช้ร่วมกันได้ภายใน Box ที่ใช้ร่วมกันได้ซึ่งซ้อนอยู่ภายใน 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 }
        )

        // ...
    }
}

ตอนนี้ให้สร้างและเรียกใช้แอป แล้วคุณจะเห็นผลลัพธ์ คุณควรเห็นแผนที่ที่อยู่ตรงกลางของเกาะนัลล์ที่โด่งดัง หรือที่รู้จักกันในชื่อละติจูด 0 และลองจิจูด 0 ในภายหลัง คุณจะได้เรียนรู้วิธีจัดตำแหน่งแผนที่ไปยังสถานที่และระดับการซูมที่ต้องการ แต่ตอนนี้ขอให้ฉลองชัยชนะครั้งแรกของคุณก่อน

6. การจัดรูปแบบแผนที่ในระบบคลาวด์

คุณปรับแต่งสไตล์ของแผนที่ได้โดยใช้การจัดรูปแบบแผนที่ในระบบคลาวด์

สร้างรหัสแผนที่

หากยังไม่ได้สร้างรหัสแมปที่มีรูปแบบแผนที่เชื่อมโยงอยู่ โปรดดูคำแนะนำเกี่ยวกับรหัสแมปเพื่อทำตามขั้นตอนต่อไปนี้

  1. สร้างรหัสแผนที่
  2. เชื่อมโยงรหัสแผนที่กับรูปแบบแผนที่

เพิ่มรหัสแมปลงในแอป

หากต้องการใช้รหัสแผนที่ที่สร้างขึ้น เมื่อสร้างอินสแตนซ์ของ GoogleMap ที่ใช้ร่วมกันได้ ให้ใช้รหัสแผนที่เมื่อสร้างออบเจ็กต์ GoogleMapOptions ซึ่งกำหนดให้กับพารามิเตอร์ googleMapOptionsFactory ในตัวสร้าง

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

เมื่อทำเสร็จแล้ว ให้เรียกใช้แอปเพื่อดูแผนที่ในสไตล์ที่คุณเลือก

7. โหลดข้อมูลเครื่องหมาย

งานหลักของแอปคือการโหลดคอลเล็กชันภูเขาจากที่เก็บข้อมูลในเครื่องและแสดงภูเขาเหล่านั้นใน GoogleMap ในขั้นตอนนี้ คุณจะได้ดูโครงสร้างพื้นฐานที่จัดไว้ให้สำหรับการโหลดข้อมูลภูเขาและนำเสนอต่อ UI

ภูเขา

Mountainคลาสข้อมูลจะเก็บข้อมูลทั้งหมดเกี่ยวกับภูเขาแต่ละลูก

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

โปรดทราบว่าในภายหลังระบบจะแบ่งพาร์ติชันภูเขาตามระดับความสูง ภูเขาที่มีความสูงอย่างน้อย 14,000 ฟุตเรียกว่า fourteener โค้ดเริ่มต้นมีฟังก์ชันส่วนขยายที่ทำการตรวจสอบนี้ให้คุณ

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

MountainsScreenViewState คลาสมีข้อมูลทั้งหมดที่จำเป็นต่อการแสดงผลมุมมอง โดยอาจอยู่ในสถานะ Loading หรือ MountainList ขึ้นอยู่กับว่าระบบโหลดรายชื่อภูเขาเสร็จแล้วหรือไม่

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

ชั้นเรียนที่ให้บริการ: MountainsRepository และ MountainsViewModel

ในโปรเจ็กต์เริ่มต้น เราได้จัดเตรียมคลาส MountainsRepository ไว้ให้คุณแล้ว คลาสนี้จะอ่านรายการสถานที่ที่เป็นภูเขาซึ่งจัดเก็บไว้ในไฟล์ GPS Exchange Format หรือ GPX top_peaks.gpx การเรียกใช้ mountainsRepository.loadMountains() จะแสดงผล 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 คือคลาส ViewModel ที่โหลดคอลเล็กชันของภูเขาและแสดงคอลเล็กชันนั้น รวมถึงส่วนอื่นๆ ของสถานะ UI ผ่าน mountainsScreenViewState mountainsScreenViewState คือ Hot StateFlow ที่ UI สามารถสังเกตได้ในฐานะสถานะที่เปลี่ยนแปลงได้โดยใช้ฟังก์ชันส่วนขยาย collectAsState

MountainsViewModel จะเก็บสถานะทั้งหมดของแอปไว้ตามหลักการออกแบบเสียง UI จะส่งการโต้ตอบของผู้ใช้ไปยัง ViewModel โดยใช้เมธอด 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) }
  }
}

หากสนใจเกี่ยวกับการใช้งานคลาสเหล่านี้ คุณสามารถเข้าถึงได้ใน GitHub หรือเปิดคลาส MountainsRepository และ MountainsViewModel ใน Android Studio

ใช้ ViewModel

ใช้ View Model ใน MainActivity เพื่อรับ viewState คุณจะใช้ viewState เพื่อแสดงเครื่องหมายในภายหลังใน Codelab นี้ โปรดทราบว่าโค้ดนี้รวมอยู่ในโปรเจ็กต์เริ่มต้นแล้ว และแสดงที่นี่เพื่อใช้อ้างอิงเท่านั้น

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

8. จัดตำแหน่งกล้อง

GoogleMapค่าเริ่มต้นจะอยู่ตรงกลางที่ละติจูด 0 ลองจิจูด 0 เครื่องหมายที่คุณจะแสดงตั้งอยู่ในรัฐโคโลราโดในสหรัฐอเมริกา viewStateที่จัดทำโดยโมเดลมุมมองจะแสดง LatLngBounds ซึ่งมีเครื่องหมายทั้งหมด

ใน MountainMap.kt create a CameraPositionState initialized to the center of the bounding box ตั้งค่าพารามิเตอร์ cameraPositionState ของ GoogleMap เป็นตัวแปร cameraPositionState ที่คุณเพิ่งสร้าง

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

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

ตอนนี้ให้เรียกใช้โค้ดและดูแผนที่ที่กึ่งกลางของโคโลราโด

ซูมไปยังขอบเขตของเครื่องหมาย

หากต้องการโฟกัสแผนที่ไปที่เครื่องหมายจริงๆ ให้เพิ่มฟังก์ชัน zoomAll ที่ส่วนท้ายของไฟล์ MountainMap.kt โปรดทราบว่าฟังก์ชันนี้ต้องมี CoroutineScope เนื่องจากภาพเคลื่อนไหวของกล้องไปยังตำแหน่งใหม่เป็นการดำเนินการแบบไม่พร้อมกันซึ่งต้องใช้เวลาในการดำเนินการให้เสร็จสมบูรณ์

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

จากนั้นเพิ่มโค้ดเพื่อเรียกใช้ฟังก์ชัน zoomAll เมื่อใดก็ตามที่ขอบเขตของคอลเล็กชันเครื่องหมายเปลี่ยนแปลง หรือเมื่อผู้ใช้คลิกปุ่มซูมขอบเขตในแถบแอปด้านบน โปรดทราบว่าปุ่มขยายการซูมได้รับการเชื่อมต่อเพื่อส่งเหตุการณ์ไปยัง ViewModel แล้ว คุณเพียงแค่ต้องรวบรวมเหตุการณ์เหล่านั้นจาก View Model และเรียกใช้ฟังก์ชัน 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)
                }
            }
        }
    }
}

ตอนนี้เมื่อเรียกใช้แอป แผนที่จะเริ่มโฟกัสเหนือพื้นที่ที่จะวางเครื่องหมาย คุณสามารถเปลี่ยนตำแหน่งและเปลี่ยนระดับการซูมได้ และการคลิกปุ่มขยายการซูมจะโฟกัสแผนที่ใหม่รอบๆ พื้นที่เครื่องหมาย ถือว่าได้ก้าวไปอีกขั้นแล้ว แต่แผนที่ควรมีอะไรให้ดู และคุณจะทำเช่นนั้นในขั้นตอนถัดไป

9. เครื่องหมายพื้นฐาน

ในขั้นตอนนี้ คุณจะเพิ่มเครื่องหมายลงในแผนที่เพื่อแสดงจุดที่น่าสนใจซึ่งต้องการไฮไลต์บนแผนที่ คุณจะใช้รายชื่อภูเขาที่ระบุไว้ในโปรเจ็กต์เริ่มต้นและเพิ่มสถานที่เหล่านี้เป็นเครื่องหมายบนแผนที่

เริ่มต้นด้วยการเพิ่มบล็อกเนื้อหาลงใน GoogleMap เครื่องหมายจะมีหลายประเภท ดังนั้นให้เพิ่มคำสั่ง when เพื่อแยกไปยังแต่ละประเภท แล้วคุณจะนำแต่ละประเภทไปใช้ตามลำดับในขั้นตอนถัดไป

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

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

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

เพิ่มเครื่องหมาย

ใส่คำอธิบายประกอบ BasicMarkersMapContent ด้วย @GoogleMapComposable โปรดทราบว่าคุณใช้ได้เฉพาะฟังก์ชัน @GoogleMapComposable ในGoogleMapบล็อกเนื้อหา ออบเจ็กต์ mountains มีรายการออบเจ็กต์ Mountain คุณจะเพิ่มเครื่องหมายสำหรับภูเขาแต่ละลูกในรายการนั้น โดยใช้ตำแหน่ง ชื่อ และความสูงจากออบเจ็กต์ Mountain ระบบจะใช้ตำแหน่งเพื่อตั้งค่าพารามิเตอร์สถานะของ Marker ซึ่งจะควบคุมตำแหน่งของเครื่องหมาย

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

เรียกใช้แอป แล้วคุณจะเห็นเครื่องหมายที่เพิ่งเพิ่ม

ปรับแต่งเครื่องหมาย

มีตัวเลือกการปรับแต่งหลายอย่างสำหรับเครื่องหมายที่คุณเพิ่งเพิ่มเพื่อช่วยให้เครื่องหมายโดดเด่นและสื่อข้อมูลที่เป็นประโยชน์แก่ผู้ใช้ ในงานนี้ คุณจะได้สำรวจเครื่องหมายเหล่านั้นบางส่วนโดยการปรับแต่งรูปภาพของเครื่องหมายแต่ละรายการ

โปรเจ็กต์เริ่มต้นมีฟังก์ชันตัวช่วย vectorToBitmap สำหรับสร้าง BitmapDescriptor จาก @DrawableResource

โค้ดเริ่มต้นมีไอคอนภูเขา baseline_filter_hdr_24.xml ซึ่งคุณจะใช้เพื่อปรับแต่งเครื่องหมาย

ฟังก์ชัน vectorToBitmap จะแปลง Vector Drawable เป็น BitmapDescriptor เพื่อใช้กับไลบรารี Maps ตั้งค่าสีไอคอนโดยใช้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 {
    // ...
}

ใช้ฟังก์ชัน vectorToBitmap เพื่อสร้าง BitmapDescriptor ที่กำหนดเอง 2 รายการ รายการหนึ่งสำหรับภูเขาสูง 14,000 ฟุต และอีกรายการหนึ่งสำหรับภูเขาปกติ จากนั้นใช้พารามิเตอร์ icon ของ Marker ที่ใช้ร่วมกันได้เพื่อตั้งค่าไอคอน นอกจากนี้ ให้ตั้งค่าพารามิเตอร์ anchor เพื่อเปลี่ยนตำแหน่งจุดยึดที่สัมพันธ์กับไอคอน การใช้ตรงกลางจะเหมาะกับไอคอนวงกลมเหล่านี้มากกว่า

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

เรียกใช้แอปและดูเครื่องหมายที่ปรับแต่ง สลับShow allเพื่อดูภูเขาทั้งหมด ภูเขาจะมีเครื่องหมายแตกต่างกันไปตามว่าภูเขานั้นเป็นภูเขาสูง 14,000 ฟุตหรือไม่

10. เครื่องหมายขั้นสูง

AdvancedMarkers เพิ่มฟีเจอร์พิเศษให้กับ Markers พื้นฐาน ในขั้นตอนนี้ คุณจะกำหนดลักษณะการทับซ้อนและกำหนดค่ารูปแบบหมุด

เพิ่ม @GoogleMapComposable ลงในฟังก์ชัน AdvancedMarkersMapContent วนซ้ำโดยmountainsเพิ่ม AdvancedMarker สำหรับแต่ละรายการ

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

สังเกตพารามิเตอร์ collisionBehavior การตั้งค่าพารามิเตอร์นี้เป็น REQUIRED_AND_HIDES_OPTIONAL จะทำให้เครื่องหมายแทนที่เครื่องหมายที่มีลำดับความสำคัญต่ำกว่า คุณดูได้โดยการซูมเข้าที่เครื่องหมายพื้นฐานเทียบกับเครื่องหมายขั้นสูง เครื่องหมายพื้นฐานน่าจะมีทั้งเครื่องหมายและเครื่องหมายที่วางไว้ในตำแหน่งเดียวกันในแผนที่ฐาน เครื่องหมายขั้นสูงจะทำให้เครื่องหมายที่มีลำดับความสำคัญต่ำกว่าถูกซ่อน

เรียกใช้แอปเพื่อดูเครื่องหมายขั้นสูง อย่าลืมเลือกแท็บ Advanced markers ในแถบนำทางด้านล่าง

AdvancedMarkers ที่ปรับแต่งแล้ว

ไอคอนใช้รูปแบบสีหลักและสีรองเพื่อแยกความแตกต่างระหว่างยอดเขาที่สูงกว่า 14,000 ฟุตกับภูเขาอื่นๆ ใช้ฟังก์ชัน vectorToBitmap เพื่อสร้าง BitmapDescriptor 2 รายการ รายการหนึ่งสำหรับยอดเขาที่สูงกว่า 14,000 ฟุต และอีกรายการสำหรับภูเขาอื่นๆ ใช้ไอคอนเหล่านั้นเพื่อสร้าง pinConfig ที่กำหนดเองสำหรับแต่ละประเภท สุดท้าย ให้ใช้หมุดกับAdvancedMarkerที่เกี่ยวข้องตามฟังก์ชัน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. เครื่องหมายที่จัดกลุ่ม

ในขั้นตอนนี้ คุณจะใช้ Clustering ที่ประกอบได้เพื่อเพิ่มการจัดกลุ่มรายการตามการซูม

Clustering Composable ต้องมีคอลเล็กชันของ ClusterItem MountainClusterItem ใช้ClusterItemอินเทอร์เฟซ เพิ่มคลาสนี้ลงในไฟล์ 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
}

ตอนนี้ให้เพิ่มโค้ดเพื่อสร้าง MountainClusterItem จากรายการภูเขา โปรดทราบว่าโค้ดนี้ใช้ UnitsConverter เพื่อแปลงเป็นหน่วยแสดงผลที่เหมาะสมสำหรับผู้ใช้ตามภาษา โดยตั้งค่าใน MainActivity โดยใช้ 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,
    )
}

และด้วยโค้ดดังกล่าว ระบบจะจัดกลุ่มเครื่องหมายตามระดับการซูม เรียบร้อยดี

ปรับแต่งคลัสเตอร์

เครื่องหมายคลัสเตอร์ปรับแต่งได้เช่นเดียวกับเครื่องหมายประเภทอื่นๆ พารามิเตอร์ clusterItemContent ของ Composable Clustering จะตั้งค่าบล็อก Composable ที่กำหนดเองเพื่อแสดงผลรายการที่ไม่ได้จัดกลุ่ม ใช้ฟังก์ชัน @Composable เพื่อสร้างเครื่องหมาย ฟังก์ชัน SingleMountain จะแสดงผล Icon ที่ใช้ได้ของ Material 3 พร้อมรูปแบบสีพื้นหลังที่ปรับแต่งแล้ว

ใน ClusteringMarkersMapContent.kt ให้สร้างคลาสข้อมูลที่กำหนดรูปแบบสีสำหรับเครื่องหมาย

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

นอกจากนี้ ใน ClusteringMarkersMapContent.kt ให้สร้างฟังก์ชันที่ใช้ร่วมกันได้เพื่อแสดงไอคอนสำหรับรูปแบบสีที่กำหนด

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

ตอนนี้ให้สร้างรูปแบบสีสำหรับยอดเขาที่สูงกว่า 14,000 ฟุต และรูปแบบสีอีกรูปแบบสำหรับภูเขาอื่นๆ ในclusterItemContent ให้เลือกรูปแบบสีตามว่าภูเขาที่ระบุเป็นภูเขาสูง 14, 000 ฟุตหรือไม่

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

ตอนนี้ให้เรียกใช้แอปเพื่อดูเวอร์ชันที่ปรับแต่งแล้วของแต่ละรายการ

12. วาดในแผนที่

แม้ว่าคุณจะเคยสำรวจวิธีวาดบนแผนที่ (โดยการเพิ่มเครื่องหมาย) มาแล้ว แต่ Maps SDK สำหรับ Android ก็รองรับวิธีอื่นๆ อีกมากมายที่คุณสามารถใช้วาดเพื่อแสดงข้อมูลที่เป็นประโยชน์บนแผนที่

เช่น หากต้องการแสดงเส้นทางและพื้นที่บนแผนที่ คุณสามารถใช้ Polyline และ Polygon เพื่อแสดงข้อมูลเหล่านี้บนแผนที่ หรือหากต้องการยึดรูปภาพไว้กับพื้นผิว คุณสามารถใช้ GroundOverlay ได้

ในงานนี้ คุณจะได้เรียนรู้วิธีวาดรูปร่าง โดยเฉพาะอย่างยิ่งเส้นขอบรอบรัฐโคโลราโด พรมแดนโคโลราโดกำหนดไว้ที่ละติจูดระหว่าง 37°N ถึง 41°N และลองจิจูดระหว่าง 102°03'W ถึง 109°03'W ซึ่งจะทำให้การวาดเส้นขอบตรงไปตรงมา

โค้ดเริ่มต้นมีคลาส DMS สำหรับแปลงจากสัญกรณ์องศา-นาที-วินาทีเป็นองศาทศนิยม

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

คลาส DMS ช่วยให้คุณวาดเส้นขอบของโคโลราโดได้โดยการกำหนดLatLngตำแหน่งทั้ง 4 มุมและแสดงผลเป็นPolygon เพิ่มโค้ดต่อไปนี้ลงใน 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),
    )
}

ตอนนี้เรียกใช้ ColoradoPolyon() ภายในบล็อกเนื้อหา GoogleMap

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

ตอนนี้แอปจะวาดเส้นขอบรัฐโคโลราโดพร้อมกับเติมสีแบบบางๆ

13. เพิ่มเลเยอร์ KML และแถบมาตราส่วน

ในส่วนสุดท้ายนี้ คุณจะร่างคร่าวๆ เกี่ยวกับเทือกเขาต่างๆ และเพิ่มแถบมาตราส่วนลงในแผนที่

วาดเส้นขอบเทือกเขา

ก่อนหน้านี้คุณวาดเส้นรอบรัฐโคโลราโด ในส่วนนี้ คุณจะเพิ่มรูปร่างที่ซับซ้อนมากขึ้นลงในแผนที่ โค้ดเริ่มต้นมีไฟล์ภาษามาร์กอัปของ Keyhole หรือ KML ซึ่งระบุแนวเทือกเขาที่สำคัญโดยคร่าว ไลบรารียูทิลิตี Maps SDK สำหรับ Android มีฟังก์ชันสำหรับเพิ่มเลเยอร์ KML ลงในแผนที่ ใน MountainMap.kt ให้เพิ่มการเรียกใช้ MapEffect ในGoogleMap บล็อกเนื้อหาหลังwhen บล็อก ฟังก์ชัน MapEffect จะเรียกใช้ด้วยออบเจ็กต์ GoogleMap ซึ่งอาจเป็นตัวกลางที่มีประโยชน์ระหว่าง API และไลบรารีที่ไม่สามารถคอมโพสได้ซึ่งต้องใช้ออบเจ็กต์ 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()
      }
    }

เพิ่มมาตราส่วนแผนที่

งานสุดท้ายคือการเพิ่มมาตราส่วนลงในแผนที่ ScaleBar ใช้ Composable ของมาตราส่วนที่เพิ่มลงในแผนที่ได้ โปรดทราบว่า ScaleBar ไม่ใช่

@GoogleMapComposable จึงเพิ่มลงในเนื้อหา GoogleMap ไม่ได้ แต่ให้เพิ่มลงใน Box ที่มีแผนที่แทน

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

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

เรียกใช้แอปเพื่อดู Codelab ที่ใช้งานได้เต็มรูปแบบ

14. รับรหัสโซลูชัน

หากต้องการดาวน์โหลดโค้ดสำหรับ Codelab ที่เสร็จสมบูรณ์แล้ว คุณสามารถใช้คำสั่งต่อไปนี้

  1. โคลนที่เก็บหากคุณติดตั้ง git ไว้
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

หรือจะคลิกปุ่มต่อไปนี้เพื่อดาวน์โหลดซอร์สโค้ดก็ได้

  1. เมื่อได้รับโค้ดแล้ว ให้เปิดโปรเจ็กต์ที่อยู่ในไดเรกทอรี solution ใน Android Studio

15. ขอแสดงความยินดี

ยินดีด้วย คุณได้เรียนรู้เนื้อหามากมาย และเราหวังว่าคุณจะเข้าใจฟีเจอร์หลักที่อยู่ใน Maps SDK สำหรับ Android ได้ดียิ่งขึ้น

ดูข้อมูลเพิ่มเติม

  • Maps SDK สำหรับ Android - สร้างแผนที่ ตำแหน่ง และประสบการณ์ด้านภูมิสารสนเทศแบบไดนามิก อินเทอร์แอกทีฟ และปรับแต่งได้สำหรับแอป Android
  • Maps Compose Library - ชุดฟังก์ชันที่ใช้ร่วมกันได้แบบโอเพนซอร์สและประเภทข้อมูลที่คุณใช้กับ Jetpack Compose เพื่อสร้างแอปได้
  • android-maps-compose - ตัวอย่างโค้ดใน GitHub ที่แสดงฟีเจอร์ทั้งหมดที่ครอบคลุมในโค้ดแล็บนี้และอื่นๆ
  • Codelab ของ Kotlin เพิ่มเติมสำหรับการสร้างแอป Android ด้วย Google Maps Platform