1. Trước khi bắt đầu
Lớp học lập trình này hướng dẫn bạn cách tích hợp Maps SDK cho Android với ứng dụng của bạn và sử dụng các tính năng cốt lõi của SDK này bằng cách tạo một ứng dụng hiển thị bản đồ các ngọn núi ở Colorado, Hoa Kỳ, bằng nhiều loại điểm đánh dấu. Ngoài ra, bạn sẽ học cách vẽ các hình dạng khác trên bản đồ.
Sau đây là hình minh hoạ sản phẩm của bạn sau khi hoàn tất lớp học lập trình này:
Điều kiện tiên quyết
- Có kiến thức cơ bản về Kotlin, Jetpack Compose và phát triển Android
Bạn sẽ thực hiện
- Bật và sử dụng thư viện Maps Compose cho Maps SDK dành cho Android để thêm
GoogleMap
vào một ứng dụng Android - Thêm và tuỳ chỉnh điểm đánh dấu
- Vẽ đa giác trên bản đồ
- Điều khiển điểm nhìn của camera theo phương thức lập trình
Bạn cần có
- Maps SDK cho Android
- Tài khoản Google có bật tính năng thanh toán
- Phiên bản ổn định mới nhất của Android Studio
- Một thiết bị Android hoặc trình mô phỏng Android chạy nền tảng API của Google dựa trên Android 5.0 trở lên (xem phần Chạy ứng dụng trên Trình mô phỏng Android để biết các bước cài đặt).
- Kết nối Internet
2. Bắt đầu thiết lập
Đối với bước bật sau đây, bạn cần bật Maps SDK cho Android.
Thiết lập Nền tảng Google Maps
Nếu bạn chưa có tài khoản Google Cloud Platform và dự án có bật tính năng thanh toán, vui lòng xem hướng dẫn Bắt đầu sử dụng Google Maps Platform để tạo tài khoản thanh toán và dự án.
- Trong Cloud Console, hãy nhấp vào trình đơn thả xuống dự án rồi chọn dự án mà bạn muốn sử dụng cho lớp học lập trình này.
- Bật các API và SDK của Google Maps Platform cần thiết cho lớp học lập trình này trong Google Cloud Marketplace. Để làm như vậy, hãy làm theo các bước trong video này hoặc tài liệu này.
- Tạo khoá API trong trang Thông tin xác thực của Cloud Console. Bạn có thể làm theo các bước trong video này hoặc tài liệu này. Tất cả các yêu cầu gửi đến Nền tảng Google Maps đều cần có khoá API.
3. Bắt đầu nhanh
Để giúp bạn bắt đầu nhanh nhất có thể, sau đây là một số mã khởi đầu giúp bạn theo dõi lớp học lập trình này. Bạn có thể chuyển ngay đến giải pháp, nhưng nếu muốn làm theo tất cả các bước để tự xây dựng, hãy tiếp tục đọc.
- Sao chép kho lưu trữ nếu bạn đã cài đặt
git
.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Ngoài ra, bạn có thể nhấp vào nút sau đây để tải mã nguồn xuống.
- Sau khi nhận được mã, hãy mở dự án trong thư mục
starter
trong Android Studio.
4. Thêm khoá API vào dự án
Phần này mô tả cách lưu trữ khoá API để ứng dụng của bạn có thể tham chiếu một cách an toàn. Bạn không nên kiểm tra khoá API trong hệ thống kiểm soát phiên bản, vì vậy, bạn nên lưu trữ khoá này trong tệp secrets.properties
. Tệp này sẽ được đặt trong bản sao cục bộ của thư mục gốc của dự án. Để biết thêm thông tin về tệp secrets.properties
, hãy xem phần Tệp thuộc tính Gradle.
Để đơn giản hoá tác vụ này, bạn nên sử dụng Trình bổ trợ Secrets Gradle cho Android.
Cách cài đặt Trình bổ trợ Secrets Gradle cho Android trong dự án Google Maps:
- Trong Android Studio, hãy mở tệp
build.gradle.kts
cấp cao nhất rồi thêm mã sau vào phần tửdependencies
trongbuildscript
.buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- Mở tệp
build.gradle.kts
ở cấp mô-đun rồi thêm đoạn mã sau vào phần tửplugins
.plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- Trong tệp
build.gradle.kts
ở cấp mô-đun, hãy đảm bảo rằngtargetSdk
vàcompileSdk
được đặt thành ít nhất là 34. - Lưu tệp và đồng bộ hoá dự án với Gradle.
- Mở tệp
secrets.properties
trong thư mục cấp cao nhất, sau đó thêm đoạn mã sau. Thay thếYOUR_API_KEY
bằng khoá API của bạn. Lưu trữ khoá của bạn trong tệp này vìsecrets.properties
không được đưa vào hệ thống quản lý phiên bản.MAPS_API_KEY=YOUR_API_KEY
- Lưu tệp.
- Tạo tệp
local.defaults.properties
trong thư mục cấp cao nhất (cùng thư mục với tệpsecrets.properties
), rồi thêm mã sau. Mục đích của tệp này là cung cấp một vị trí sao lưu cho khoá API nếu không tìm thấy tệpMAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
để các bản dựng không bị lỗi. Điều này sẽ xảy ra khi bạn sao chép ứng dụng từ một hệ thống kiểm soát phiên bản và bạn chưa tạo tệpsecrets.properties
cục bộ để cung cấp khoá API. - Lưu tệp.
- Trong tệp
AndroidManifest.xml
, hãy chuyển đếncom.google.android.geo.API_KEY
rồi cập nhật thuộc tínhandroid:value
. Nếu thẻ<meta-data>
không tồn tại, hãy tạo thẻ này dưới dạng thẻ con của thẻ<application>
.<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- Trong Android Studio, hãy mở tệp
build.gradle.kts
ở cấp mô-đun rồi chỉnh sửa thuộc tínhsecrets
. Nếu thuộc tínhsecrets
không tồn tại, hãy thêm thuộc tính này.Chỉnh sửa các thuộc tính của trình bổ trợ để đặtpropertiesFileName
thànhsecrets.properties
, đặtdefaultPropertiesFileName
thànhlocal.defaults.properties
và đặt mọi thuộc tính khác.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. Thêm Google Maps
Trong phần này, bạn sẽ thêm một Google Map để Google Map tải khi bạn chạy ứng dụng.
Thêm phần phụ thuộc Maps Compose
Giờ đây, khoá API của bạn có thể được truy cập trong ứng dụng. Bước tiếp theo là thêm phần phụ thuộc Maps SDK for Android vào tệp build.gradle.kts
của ứng dụng. Để tạo bằng Jetpack Compose, hãy dùng thư viện Maps Compose. Thư viện này cung cấp các phần tử của Maps SDK cho Android dưới dạng các hàm có khả năng kết hợp và kiểu dữ liệu.
build.gradle.kts
Trong tệp build.gradle.kts
ở cấp ứng dụng, hãy thay thế các phần phụ thuộc SDK Bản đồ dành cho Android không phải 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")
}
với các thành phần kết hợp tương ứng:
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")
}
Thêm thành phần kết hợp Google Maps
Trong MountainMap.kt
, hãy thêm thành phần kết hợp GoogleMap
bên trong thành phần kết hợp Box
được lồng trong thành phần kết hợp 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 }
)
// ...
}
}
Bây giờ, hãy tạo bản dựng và chạy ứng dụng. Hãy xem! Bạn sẽ thấy một bản đồ được căn giữa trên Đảo Null khét tiếng, còn được gọi là vĩ độ 0 và kinh độ 0. Sau này, bạn sẽ tìm hiểu cách đặt bản đồ vào vị trí và mức thu phóng mà bạn muốn, nhưng trước mắt, hãy ăn mừng chiến thắng đầu tiên của bạn!
6. Định kiểu bản đồ dựa trên đám mây
Bạn có thể tuỳ chỉnh kiểu bản đồ bằng tính năng Định kiểu bản đồ dựa trên đám mây.
Tạo mã bản đồ
Nếu bạn chưa tạo mã bản đồ có kiểu bản đồ được liên kết, hãy xem hướng dẫn về Mã bản đồ để hoàn tất các bước sau:
- Tạo mã bản đồ.
- Liên kết mã bản đồ với kiểu bản đồ.
Thêm mã bản đồ vào ứng dụng
Để sử dụng mã bản đồ mà bạn đã tạo, khi khởi tạo thành phần kết hợp GoogleMap
, hãy sử dụng mã bản đồ khi tạo đối tượng GoogleMapOptions
được chỉ định cho tham số googleMapOptionsFactory
trong hàm khởi tạo.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
Sau khi hoàn tất, hãy chạy ứng dụng để xem bản đồ theo kiểu mà bạn đã chọn!
7. Tải dữ liệu điểm đánh dấu
Nhiệm vụ chính của ứng dụng là tải một bộ sưu tập núi từ bộ nhớ cục bộ và hiển thị những ngọn núi đó trong GoogleMap
. Trong bước này, bạn sẽ tìm hiểu về cơ sở hạ tầng được cung cấp để tải dữ liệu về núi và trình bày dữ liệu đó cho giao diện người dùng.
Núi
Lớp dữ liệu Mountain
lưu giữ tất cả dữ liệu về từng ngọn núi.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
Xin lưu ý rằng sau này, các ngọn núi sẽ được phân vùng dựa trên độ cao. Những ngọn núi cao ít nhất 14.000 feet được gọi là núi cao 14.000 feet. Mã khởi đầu bao gồm một hàm mở rộng để thực hiện việc kiểm tra này cho bạn.
/**
* 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
Lớp MountainsScreenViewState
chứa tất cả dữ liệu cần thiết để hiển thị khung hiển thị. Trạng thái này có thể là Loading
hoặc MountainList
, tuỳ thuộc vào việc danh sách núi đã tải xong hay chưa.
/**
* 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()
}
Các lớp được cung cấp: MountainsRepository
và MountainsViewModel
Trong dự án khởi đầu, lớp MountainsRepository
đã được cung cấp cho bạn. Lớp này đọc danh sách các địa điểm trên núi được lưu trữ trong tệp GPS Exchange Format
hoặc GPX, top_peaks.gpx
. Khi gọi mountainsRepository.loadMountains()
, hệ thống sẽ trả về 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
là một lớp ViewModel
tải các bộ sưu tập núi và hiển thị các bộ sưu tập đó cũng như các phần khác của trạng thái giao diện người dùng thông qua mountainsScreenViewState
. mountainsScreenViewState
là một StateFlow
nóng mà giao diện người dùng có thể quan sát dưới dạng trạng thái có thể thay đổi bằng hàm mở rộng collectAsState
.
Theo các nguyên tắc cấu trúc âm thanh, MountainsViewModel
giữ tất cả trạng thái của ứng dụng. Giao diện người dùng gửi các hoạt động tương tác của người dùng đến mô hình hiển thị bằng phương thức 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) }
}
}
Nếu muốn tìm hiểu về cách triển khai các lớp này, bạn có thể truy cập vào các lớp đó trên GitHub hoặc mở các lớp MountainsRepository
và MountainsViewModel
trong Android Studio.
Sử dụng ViewModel
Mô hình hiển thị được dùng trong MainActivity
để lấy viewState
. Bạn sẽ dùng viewState
để kết xuất các điểm đánh dấu sau này trong lớp học lập trình này. Xin lưu ý rằng mã này đã có trong dự án khởi đầu và chỉ xuất hiện ở đây để tham khảo.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. Đặt camera
GoogleMap
mặc định sẽ đặt tâm ở vĩ độ 0, kinh độ 0. Các điểm đánh dấu mà bạn sẽ kết xuất nằm ở Tiểu bang Colorado, Hoa Kỳ. viewState
do mô hình hiển thị cung cấp sẽ trình bày một LatLngBounds chứa tất cả các điểm đánh dấu.
Trong MountainMap.kt
, hãy tạo một CameraPositionState
được khởi tạo ở giữa hộp giới hạn. Đặt tham số cameraPositionState
của GoogleMap
thành biến cameraPositionState
mà bạn vừa tạo.
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
Bây giờ, hãy chạy mã và xem bản đồ tập trung vào Colorado.
Phóng to đến phạm vi của điểm đánh dấu
Để thực sự tập trung bản đồ vào các điểm đánh dấu, hãy thêm hàm zoomAll
vào cuối tệp MountainMap.kt
. Xin lưu ý rằng hàm này cần có CoroutineScope
vì việc chuyển động camera đến một vị trí mới là một thao tác không đồng bộ và mất thời gian để hoàn tất.
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
Tiếp theo, hãy thêm mã để gọi hàm zoomAll
bất cứ khi nào ranh giới xung quanh bộ sưu tập điểm đánh dấu thay đổi hoặc khi người dùng nhấp vào nút thu phóng phạm vi trong TopApp bar. Xin lưu ý rằng nút thu phóng phạm vi đã được kết nối để gửi các sự kiện đến mô hình hiển thị. Bạn chỉ cần thu thập những sự kiện đó từ mô hình hiển thị và gọi hàm zoomAll
để phản hồi.
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)
}
}
}
}
}
Giờ đây, khi bạn chạy ứng dụng, bản đồ sẽ bắt đầu tập trung vào khu vực mà các điểm đánh dấu sẽ xuất hiện. Bạn có thể định vị lại và thay đổi mức thu phóng. Khi nhấp vào nút thu phóng theo phạm vi, bản đồ sẽ tập trung lại vào khu vực điểm đánh dấu. Đó là một bước tiến! Nhưng bản đồ thực sự cần có nội dung để xem. Và đó là những gì bạn sẽ làm trong bước tiếp theo!
9. Điểm đánh dấu cơ bản
Trong bước này, bạn sẽ thêm Marker vào bản đồ để biểu thị những địa điểm yêu thích mà bạn muốn làm nổi bật trên bản đồ. Bạn sẽ sử dụng danh sách các ngọn núi đã được cung cấp trong dự án khởi đầu và thêm những địa điểm này làm điểm đánh dấu trên bản đồ.
Bắt đầu bằng cách thêm một khối nội dung vào GoogleMap
. Sẽ có nhiều loại điểm đánh dấu, vì vậy, hãy thêm câu lệnh when
để phân nhánh cho từng loại và bạn sẽ triển khai từng loại theo thứ tự trong các bước tiếp theo.
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
Thêm điểm đánh dấu
Chú giải BasicMarkersMapContent
bằng @GoogleMapComposable
. Xin lưu ý rằng bạn chỉ được phép sử dụng các hàm @GoogleMapComposable
trong khối nội dung GoogleMap
. Đối tượng mountains
có một danh sách các đối tượng Mountain
. Bạn sẽ thêm một điểm đánh dấu cho từng ngọn núi trong danh sách đó, sử dụng vị trí, tên và độ cao từ đối tượng Mountain
. Vị trí này được dùng để đặt tham số trạng thái của Marker
, từ đó kiểm soát vị trí của điểm đánh dấu.
// ...
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
)
}
}
Hãy chạy ứng dụng, bạn sẽ thấy các điểm đánh dấu mà bạn vừa thêm!
Tuỳ chỉnh điểm đánh dấu
Có một số lựa chọn tuỳ chỉnh cho các điểm đánh dấu mà bạn vừa thêm để giúp các điểm này nổi bật và truyền tải thông tin hữu ích cho người dùng. Trong nhiệm vụ này, bạn sẽ khám phá một số lựa chọn trong số đó bằng cách tuỳ chỉnh hình ảnh của từng điểm đánh dấu.
Dự án khởi đầu có một hàm trợ giúp vectorToBitmap
để tạo BitmapDescriptor
từ @DrawableResource
.
Đoạn mã khởi đầu bao gồm một biểu tượng núi baseline_filter_hdr_24.xml
mà bạn sẽ dùng để tuỳ chỉnh các điểm đánh dấu.
Hàm vectorToBitmap
chuyển đổi một vectơ vẽ được thành BitmapDescriptor
để dùng với thư viện Maps. Màu biểu tượng được đặt bằng cách sử dụng một thực thể 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 {
// ...
}
Sử dụng hàm vectorToBitmap
để tạo 2 BitmapDescriptor
tuỳ chỉnh; một cho núi cao trên 4.267 mét và một cho núi thông thường. Sau đó, hãy dùng tham số icon
của thành phần kết hợp Marker
để đặt biểu tượng. Ngoài ra, hãy đặt tham số anchor
để thay đổi vị trí của điểm neo so với biểu tượng. Sử dụng tâm sẽ phù hợp hơn với những biểu tượng hình tròn này.
@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,
)
}
}
Chạy ứng dụng và chiêm ngưỡng các điểm đánh dấu tuỳ chỉnh. Bật nút chuyển Show all
để xem toàn bộ dãy núi. Các ngọn núi sẽ có các điểm đánh dấu khác nhau tuỳ thuộc vào việc ngọn núi đó có phải là một ngọn núi cao trên 14.000 feet (4.267 mét) hay không.
10. Thẻ đánh dấu nâng cao
AdvancedMarker
s bổ sung các tính năng khác cho Markers
cơ bản. Trong bước này, bạn sẽ đặt hành vi va chạm và định cấu hình kiểu ghim.
Thêm @GoogleMapComposable
vào hàm AdvancedMarkersMapContent
. Lặp lại trên mountains
bằng cách thêm một AdvancedMarker
cho mỗi 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
}
)
}
}
Hãy lưu ý tham số collisionBehavior
. Bằng cách đặt thông số này thành REQUIRED_AND_HIDES_OPTIONAL
, điểm đánh dấu của bạn sẽ thay thế mọi điểm đánh dấu có mức độ ưu tiên thấp hơn. Bạn có thể thấy điều này bằng cách phóng to một điểm đánh dấu cơ bản so với một điểm đánh dấu nâng cao. Điểm đánh dấu cơ bản có thể sẽ có cả điểm đánh dấu của bạn và điểm đánh dấu được đặt ở cùng một vị trí trên bản đồ cơ sở. Điểm đánh dấu nâng cao sẽ khiến điểm đánh dấu có mức độ ưu tiên thấp hơn bị ẩn.
Chạy ứng dụng để xem các điểm đánh dấu Nâng cao. Nhớ chọn thẻ Advanced markers
trong hàng điều hướng dưới cùng.
Tuỳ chỉnh AdvancedMarkers
Các biểu tượng sử dụng bảng phối màu chính và phụ để phân biệt giữa các đỉnh núi cao trên 4.267 mét và các đỉnh núi khác. Dùng hàm vectorToBitmap
để tạo 2 BitmapDescriptor
; một cho các đỉnh núi cao trên 14.000 feet và một cho các đỉnh núi khác. Sử dụng các biểu tượng đó để tạo một pinConfig
tuỳ chỉnh cho từng loại. Cuối cùng, hãy áp dụng ghim cho AdvancedMarker
tương ứng dựa trên hàm 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. Điểm đánh dấu được phân cụm
Ở bước này, bạn sẽ dùng thành phần kết hợp Clustering
để thêm tính năng nhóm các mục dựa trên mức thu phóng.
Thành phần kết hợp Clustering
yêu cầu phải có một tập hợp các ClusterItem
. MountainClusterItem
triển khai giao diện ClusterItem
. Thêm lớp này vào tệp 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
}
Giờ đây, hãy thêm mã để tạo MountainClusterItem
từ danh sách các ngọn núi. Xin lưu ý rằng mã này sử dụng UnitsConverter
để chuyển đổi thành các đơn vị hiển thị phù hợp với người dùng dựa trên ngôn ngữ của họ. Điều này được thiết lập trong MainActivity
bằng cách sử dụng 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,
)
}
Với mã đó, các điểm đánh dấu được nhóm lại dựa trên mức thu phóng. Đẹp và gọn gàng!
Tuỳ chỉnh cụm
Giống như các loại điểm đánh dấu khác, bạn có thể tuỳ chỉnh điểm đánh dấu theo cụm. Tham số clusterItemContent
của thành phần kết hợp Clustering
sẽ đặt một khối thành phần kết hợp tuỳ chỉnh để kết xuất một mục không được phân cụm. Triển khai hàm @Composable
để tạo điểm đánh dấu. Hàm SingleMountain
kết xuất một Icon
có khả năng kết hợp Material 3 với bảng phối màu nền tuỳ chỉnh.
Trong ClusteringMarkersMapContent.kt
, hãy tạo một lớp dữ liệu xác định bảng phối màu cho một điểm đánh dấu:
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
Ngoài ra, trong ClusteringMarkersMapContent.kt
, hãy tạo một hàm có khả năng kết hợp để hiển thị biểu tượng cho một bảng phối màu nhất định:
@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)
)
}
Bây giờ, hãy tạo một bảng phối màu cho các đỉnh núi cao trên 4.000 mét và một bảng phối màu khác cho các đỉnh núi khác. Trong khối clusterItemContent
, hãy chọn bảng phối màu dựa trên việc ngọn núi đã cho có phải là ngọn núi cao trên 14.000 feet hay không.
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)
},
)
}
Bây giờ, hãy chạy ứng dụng để xem các phiên bản tuỳ chỉnh của từng mục.
12. Vẽ trên bản đồ
Mặc dù bạn đã khám phá một cách vẽ trên bản đồ (bằng cách thêm điểm đánh dấu), nhưng Maps SDK cho Android hỗ trợ nhiều cách khác để bạn có thể vẽ nhằm hiển thị thông tin hữu ích trên bản đồ.
Ví dụ: nếu muốn biểu thị các tuyến đường và khu vực trên bản đồ, bạn có thể dùng Polyline
và Polygon
để hiển thị các đối tượng này trên bản đồ. Hoặc nếu muốn cố định hình ảnh vào bề mặt đất, bạn có thể dùng GroundOverlay
.
Trong nhiệm vụ này, bạn sẽ tìm hiểu cách vẽ các hình dạng, cụ thể là đường viền xung quanh tiểu bang Colorado. Ranh giới của Colorado được xác định là giữa vĩ độ 37°B và 41°B, kinh độ 102°03'T và 109°03'T. Điều này giúp việc vẽ đường viền trở nên khá đơn giản.
Đoạn mã khởi đầu bao gồm một lớp DMS
để chuyển đổi từ ký hiệu độ-phút-giây sang độ thập phân.
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
Với lớp DMS, bạn có thể vẽ đường biên giới của Colorado bằng cách xác định vị trí của 4 góc LatLng
và hiển thị các vị trí đó dưới dạng Polygon
. Thêm mã sau vào 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),
)
}
Bây giờ, hãy gọi ColoradoPolyon()
bên trong khối nội dung GoogleMap
.
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
Giờ đây, ứng dụng sẽ phác thảo Tiểu bang Colorado trong khi vẫn giữ nguyên màu nền.
13. Thêm lớp KML và thanh tỷ lệ
Trong phần cuối cùng này, bạn sẽ phác thảo sơ bộ các dãy núi và thêm một thanh tỷ lệ vào bản đồ.
Vẽ đường viền cho dãy núi
Trước đây, bạn đã vẽ một đường viền xung quanh Colorado. Tại đây, bạn sẽ thêm các hình dạng phức tạp hơn vào bản đồ. Mã khởi động bao gồm một tệp Ngôn ngữ đánh dấu Keyhole (KML) phác thảo sơ bộ các dãy núi quan trọng. Thư viện tiện ích SDK Maps dành cho Android có một chức năng để thêm lớp KML vào bản đồ. Trong MountainMap.kt
, hãy thêm lệnh gọi MapEffect
vào khối nội dung GoogleMap
sau khối when
. Hàm MapEffect
được gọi bằng một đối tượng GoogleMap
. Nền tảng này có thể đóng vai trò là cầu nối hữu ích giữa các API và thư viện không kết hợp được, yêu cầu đối tượng 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()
}
}
Thêm tỷ lệ bản đồ
Trong nhiệm vụ cuối cùng, bạn sẽ thêm một tỷ lệ vào bản đồ. ScaleBar
triển khai một thành phần kết hợp tỷ lệ có thể được thêm vào bản đồ. Xin lưu ý rằng ScaleBar
không phải là
@GoogleMapComposable
và do đó không thể thêm vào nội dung GoogleMap
. Thay vào đó, bạn sẽ thêm đối tượng này vào Box
chứa bản đồ.
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
Chạy ứng dụng để xem lớp học lập trình đã triển khai đầy đủ.
14. Lấy đoạn mã giải pháp
Để tải mã này xuống khi lớp học lập trình đã kết thúc, bạn có thể sử dụng các lệnh sau:
- Sao chép kho lưu trữ nếu bạn đã cài đặt
git
.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Ngoài ra, bạn có thể nhấp vào nút sau đây để tải mã nguồn xuống.
- Sau khi nhận được mã, hãy mở dự án trong thư mục
solution
trong Android Studio.
15. Xin chúc mừng
Xin chúc mừng! Bạn đã tìm hiểu nhiều nội dung và hy vọng rằng bạn đã hiểu rõ hơn về các tính năng cốt lõi có trong Maps SDK dành cho Android.
Tìm hiểu thêm
- Maps SDK for Android – Tạo bản đồ, vị trí và trải nghiệm không gian địa lý linh hoạt, mang tính tương tác và được tuỳ chỉnh cho các ứng dụng Android của bạn.
- Thư viện Maps Compose – một tập hợp các hàm có khả năng kết hợp và kiểu dữ liệu nguồn mở mà bạn có thể dùng với Jetpack Compose để tạo ứng dụng.
- android-maps-compose – mã mẫu trên GitHub minh hoạ tất cả các tính năng được đề cập trong lớp học lập trình này và nhiều tính năng khác.
- Các lớp học lập trình khác về Kotlin để tạo ứng dụng Android bằng Google Maps Platform