1. ก่อนที่คุณจะเริ่มต้น
Codelab นี้จะสอนวิธีผสานรวม Maps SDK สำหรับ Android กับแอปและใช้ฟีเจอร์หลักของ SDK โดยการสร้างแอปที่แสดงแผนที่ภูเขาในโคโลราโด สหรัฐอเมริกา โดยใช้เครื่องหมายประเภทต่างๆ นอกจากนี้ คุณยังจะได้เรียนรู้วิธีวาดรูปร่างอื่นๆ บนแผนที่ด้วย
เมื่อทำ Codelab เสร็จแล้ว แอปจะมีลักษณะดังนี้
ข้อกำหนดเบื้องต้น
- ความรู้พื้นฐานเกี่ยวกับ Kotlin, Jetpack Compose และการพัฒนาแอป Android
สิ่งที่คุณต้องดำเนินการ
- เปิดใช้และใช้ไลบรารี Maps Compose สำหรับ Maps SDK สำหรับ Android เพื่อเพิ่ม
GoogleMap
ลงในแอป Android - เพิ่มและปรับแต่งเครื่องหมาย
- วาดรูปหลายเหลี่ยมบนแผนที่
- ควบคุมมุมมองของกล้องโดยใช้โปรแกรม
สิ่งที่คุณต้องมี
- Maps SDK สำหรับ Android
- บัญชี Google ที่เปิดใช้การเรียกเก็บเงิน
- Android Studio เวอร์ชันเสถียรล่าสุด
- อุปกรณ์ Android หรือโปรแกรมจำลอง Android ที่ใช้แพลตฟอร์ม Google APIs ที่อิงตาม Android 5.0 ขึ้นไป (ดูขั้นตอนการติดตั้งได้ที่เรียกใช้แอปในโปรแกรมจำลอง Android)
- การเชื่อมต่ออินเทอร์เน็ต
2. ตั้งค่า
สำหรับขั้นตอนการเปิดใช้ต่อไปนี้ คุณต้องเปิดใช้ Maps SDK สำหรับ Android
ตั้งค่า Google Maps Platform
หากยังไม่มีบัญชี Google Cloud Platform และโปรเจ็กต์ที่เปิดใช้การเรียกเก็บเงิน โปรดดูคู่มือเริ่มต้นใช้งาน Google Maps Platform เพื่อสร้างบัญชีสำหรับการเรียกเก็บเงินและโปรเจ็กต์
- ใน Cloud Console ให้คลิกเมนูแบบเลื่อนลงของโปรเจ็กต์ แล้วเลือกโปรเจ็กต์ที่ต้องการใช้สำหรับ Codelab นี้
- เปิดใช้ Google Maps Platform APIs และ SDK ที่จำเป็นสำหรับ Codelab นี้ใน Google Cloud Marketplace โดยทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้
- สร้างคีย์ API ในหน้าข้อมูลเข้าสู่ระบบของ Cloud Console คุณสามารถทำตามขั้นตอนในวิดีโอนี้หรือเอกสารประกอบนี้ คำขอทั้งหมดไปยัง Google Maps Platform ต้องใช้คีย์ API
3. การเริ่มใช้งานอย่างง่าย
เรามีโค้ดเริ่มต้นที่จะช่วยให้คุณเริ่มต้นใช้งานได้อย่างรวดเร็วที่สุด และช่วยให้คุณทำตาม Codelab นี้ได้ คุณสามารถข้ามไปยังโซลูชันได้ แต่หากต้องการทำตามขั้นตอนทั้งหมดเพื่อสร้างโซลูชันด้วยตนเอง โปรดอ่านต่อ
- โคลนที่เก็บหากคุณติดตั้ง
git
ไว้
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
หรือจะคลิกปุ่มต่อไปนี้เพื่อดาวน์โหลดซอร์สโค้ดก็ได้
- เมื่อได้รับโค้ดแล้ว ให้เปิดโปรเจ็กต์ที่อยู่ในไดเรกทอรี
starter
ใน Android Studio
4. เพิ่มคีย์ API ลงในโปรเจ็กต์
ส่วนนี้อธิบายวิธีจัดเก็บคีย์ API เพื่อให้แอปอ้างอิงได้อย่างปลอดภัย คุณไม่ควรเช็คอินคีย์ API ในระบบควบคุมเวอร์ชัน ดังนั้นเราขอแนะนำให้จัดเก็บคีย์ในไฟล์ secrets.properties
ซึ่งจะอยู่ในสำเนาในเครื่องของไดเรกทอรีรากของโปรเจ็กต์ ดูข้อมูลเพิ่มเติมเกี่ยวกับไฟล์ secrets.properties
ได้ที่ไฟล์พร็อพเพอร์ตี้ Gradle
เราขอแนะนำให้ใช้ปลั๊กอินข้อมูลลับ Gradle สำหรับ Android เพื่อให้งานนี้มีประสิทธิภาพมากขึ้น
วิธีติดตั้งปลั๊กอินข้อมูลลับ Gradle สำหรับ Android ในโปรเจ็กต์ Google Maps
- ใน Android Studio ให้เปิดไฟล์
build.gradle.kts
ระดับบนสุด แล้วเพิ่มโค้ดต่อไปนี้ลงในองค์ประกอบdependencies
ภายในbuildscript
buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- เปิดไฟล์
build.gradle.kts
ระดับโมดูล แล้วเพิ่มโค้ดต่อไปนี้ลงในองค์ประกอบplugins
plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- ในไฟล์
build.gradle.kts
ระดับโมดูล ให้ตรวจสอบว่าได้ตั้งค่าtargetSdk
และcompileSdk
เป็นอย่างน้อย 34 - บันทึกไฟล์และซิงค์โปรเจ็กต์กับ Gradle
- เปิดไฟล์
secrets.properties
ในไดเรกทอรีระดับบนสุด แล้วเพิ่มโค้ดต่อไปนี้ แทนที่YOUR_API_KEY
ด้วยคีย์ API ของคุณ จัดเก็บคีย์ไว้ในไฟล์นี้เนื่องจากระบบจะไม่ตรวจสอบsecrets.properties
ในระบบควบคุมเวอร์ชันMAPS_API_KEY=YOUR_API_KEY
- บันทึกไฟล์
- สร้างไฟล์
local.defaults.properties
ในไดเรกทอรีระดับบนสุด ซึ่งเป็นโฟลเดอร์เดียวกับไฟล์secrets.properties
แล้วเพิ่มโค้ดต่อไปนี้ จุดประสงค์ของไฟล์นี้คือการระบุตำแหน่งสำรองสำหรับคีย์ API ในกรณีที่ไม่พบไฟล์MAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
เพื่อให้การสร้างไม่ล้มเหลว ปัญหานี้จะเกิดขึ้นเมื่อคุณโคลนแอปจากระบบควบคุมเวอร์ชันและยังไม่ได้สร้างไฟล์secrets.properties
ในเครื่องเพื่อระบุคีย์ API - บันทึกไฟล์
- ในไฟล์
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}" />
- ใน 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. การจัดรูปแบบแผนที่ในระบบคลาวด์
คุณปรับแต่งสไตล์ของแผนที่ได้โดยใช้การจัดรูปแบบแผนที่ในระบบคลาวด์
สร้างรหัสแผนที่
หากยังไม่ได้สร้างรหัสแมปที่มีรูปแบบแผนที่เชื่อมโยงอยู่ โปรดดูคำแนะนำเกี่ยวกับรหัสแมปเพื่อทำตามขั้นตอนต่อไปนี้
- สร้างรหัสแผนที่
- เชื่อมโยงรหัสแผนที่กับรูปแบบแผนที่
เพิ่มรหัสแมปลงในแอป
หากต้องการใช้รหัสแผนที่ที่สร้างขึ้น เมื่อสร้างอินสแตนซ์ของ 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. เครื่องหมายขั้นสูง
AdvancedMarker
s เพิ่มฟีเจอร์พิเศษให้กับ 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 ที่เสร็จสมบูรณ์แล้ว คุณสามารถใช้คำสั่งต่อไปนี้
- โคลนที่เก็บหากคุณติดตั้ง
git
ไว้
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
หรือจะคลิกปุ่มต่อไปนี้เพื่อดาวน์โหลดซอร์สโค้ดก็ได้
- เมื่อได้รับโค้ดแล้ว ให้เปิดโปรเจ็กต์ที่อยู่ในไดเรกทอรี
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