1. לפני שתתחיל
בשיעור הזה תלמדו איך לשלב את Maps SDK for Android באפליקציה שלכם ולהשתמש בתכונות הליבה שלו. כדי לעשות את זה, תבנו אפליקציה שמציגה מפה של הרים בקולורדו, ארה"ב, באמצעות סוגים שונים של סמנים. בנוסף, נסביר איך לצייר צורות אחרות במפה.
כך זה ייראה בסיום ה-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
אם עדיין אין לכם חשבון ב-Google Cloud Platform ופרויקט עם חיוב מופעל, תוכלו לעיין במדריך תחילת העבודה עם הפלטפורמה של מפות Google כדי ליצור חשבון לחיוב ופרויקט.
- בCloud Console, לוחצים על התפריט הנפתח של הפרויקט ובוחרים את הפרויקט שבו רוצים להשתמש ב-codelab הזה.
- מפעילים ב-Google Cloud Marketplace את ממשקי ה-API וערכות ה-SDK של הפלטפורמה של מפות Google שנדרשים ל-codelab הזה. כדי לעשות זאת, פועלים לפי השלבים בסרטון הזה או בתיעוד הזה.
- יוצרים מפתח API בדף Credentials במסוף Cloud. אפשר לפעול לפי השלבים שמפורטים בסרטון הזה או בתיעוד הזה. כל הבקשות אל הפלטפורמה של מפות Google מחייבות מפתח API.
3. התחלה מהירה
כדי לעזור לכם להתחיל כמה שיותר מהר, הנה קוד התחלתי שיעזור לכם לעקוב אחרי ההוראות במעבדת התכנות הזו. אתם יכולים לדלג לפתרון, אבל אם אתם רוצים לבצע את כל השלבים בעצמכם, המשיכו לקרוא.
- משכפלים את המאגר אם
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.
כדי לייעל את המשימה הזו, מומלץ להשתמש בפלאגין של Secrets Gradle ל-Android.
כדי להתקין את הפלאגין Secrets Gradle ל-Android בפרויקט של מפות Google:
- ב-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
בקטע הזה תוסיפו מפת Google כדי שהיא תיטען כשמפעילים את האפליקציה.
הוספת יחסי תלות של Maps Compose
עכשיו, כשמפתח ה-API נגיש בתוך האפליקציה, השלב הבא הוא להוסיף את התלות ב-Maps SDK for Android לקובץ build.gradle.kts
של האפליקציה. כדי ליצור אפליקציות עם Jetpack Compose, צריך להשתמש בספריית Maps Compose שמספקת רכיבים של Maps SDK ל-Android כפונקציות וסוגי נתונים שניתנים להרכבה.
build.gradle.kts
בקובץ build.gradle.kts
ברמת האפליקציה, מחליפים את יחסי התלות של Maps SDK for 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")
}
הוספה של רכיב שאפשר להרכיב ממנו ממשק משתמש במפות Google
ב-MountainMap.kt
, מוסיפים את רכיב ה-GoogleMap
composable בתוך רכיב ה-Box
composable שמוטמע בתוך רכיב ה-MapMountain
composable.
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 }
)
// ...
}
}
עכשיו בונים ומריצים את האפליקציה. הנה! אמורה להופיע מפה שבמרכזה אי האפס הידוע לשמצה, שנקרא גם קו רוחב אפס וקו אורך אפס. בהמשך נסביר איך למקם את המפה במיקום וברמת הזום הרצויים, אבל בינתיים כדאי לחגוג את הניצחון הראשון!
6. עיצוב מפות מבוסס-ענן
אתם יכולים להתאים אישית את הסגנון של המפה באמצעות עיצוב מפות מבוסס-ענן.
יצירת מזהה מפה
אם עדיין לא יצרתם מזהה מפה עם סגנון מפה שמשויך אליו, כדאי לעיין במדריך בנושא מזהי מפה כדי לבצע את השלבים הבאים:
- יוצרים מזהה מפה.
- משייכים מזהה מפה לסגנון מפה.
הוספת מזהה המפה לאפליקציה
כדי להשתמש במזהה המפה שיצרתם, כשמפעילים את ה-composable GoogleMap
, משתמשים במזהה המפה כשיוצרים אובייקט GoogleMapOptions
שמוקצה לפרמטר googleMapOptionsFactory
בבונה.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
אחרי שמסיימים את הפעולות האלה, מריצים את האפליקציה כדי לראות את המפה בסגנון שבחרתם.
7. טעינת נתוני הסמן
המשימה העיקרית של האפליקציה היא לטעון אוסף של הרים מהאחסון המקומי ולהציג את ההרים האלה ב-GoogleMap
. בשלב הזה, נסביר על התשתית שסיפקנו לטעינת נתוני ההרים ולהצגתם בממשק המשתמש.
Mountain
מחלקת הנתונים Mountain
מכילה את כל הנתונים על כל הר.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
שימו לב שההרים יחולקו בהמשך למחיצות על סמך הגובה שלהם. הרים בגובה של לפחות 14,000 רגל נקראים fourteeners. קוד ההתחלה כולל פונקציית הרחבה שמבצעת את הבדיקה הזו בשבילכם.
/**
* 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
שמעלה את האוספים של ההרים וחושפת את האוספים האלה וגם חלקים אחרים של מצב ממשק המשתמש באמצעות mountainsScreenViewState
. mountainsScreenViewState
הוא Hot StateFlow
שהממשק יכול לעקוב אחריו כמצב ניתן לשינוי באמצעות פונקציית התוסף collectAsState
.
בהתאם לעקרונות ארכיטקטוניים מוצקים, MountainsViewModel
מחזיק בכל מצב האפליקציה. ממשק המשתמש שולח את האינטראקציות של המשתמשים למודל התצוגה באמצעות השיטה 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
מודל התצוגה משמש ב-MainActivity
כדי לקבל את viewState
. בהמשך ה-codelab הזה תשתמשו ב-viewState
כדי להציג את הסמנים. הערה: הקוד הזה כבר כלול בפרויקט המתחיל, והוא מוצג כאן רק לצורך עיון.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. מיקום המצלמה
GoogleMap
ברירת המחדל היא מרכז בקו רוחב אפס, קו אורך אפס. הסמנים שיוצגו ממוקמים במדינת קולורדו בארה"ב. ה-viewState
שמועבר על ידי מודל התצוגה מייצג LatLngBounds שמכיל את כל הסמנים.
ב-MountainMap.kt
יוצרים CameraPositionState
שמאותחל למרכז של תיבת התוחמת. מגדירים את הפרמטר 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
בכל פעם שהגבולות סביב אוסף הסמנים משתנים או כשהמשתמש לוחץ על הלחצן להרחבת התצוגה בסרגל העליון של האפליקציה. שימו לב שכפתור ההרחבה של התצוגה כבר מחובר לשליחת אירועים למודל התצוגה. צריך לאסוף את האירועים האלה רק ממודל התצוגה ולהפעיל את הפונקציה 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
ממירה פריט גרפי וקטורי שניתן לשרטוט ל-BitmapDescriptor
לשימוש בספריית המפות. צבעי הסמלים מוגדרים באמצעות מופע 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
מותאמים אישית: אחד בשביל פסגות בגובה 14,000 רגל ואחד בשביל הרים רגילים. לאחר מכן משתמשים בפרמטר icon
של Marker
composable כדי להגדיר את הסמל. בנוסף, צריך להגדיר את הפרמטר 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
למצב מופעל. ההרים יכללו סמנים שונים בהתאם לגובה שלהם.
10. סמנים מתקדמים
AdvancedMarker
s add extra features to basic 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
הסמלים משתמשים בסכמות הצבעים הראשיות והמשניות כדי להבחין בין ההרים שגובהם מעל 4,000 מטר לבין הרים אחרים. משתמשים בפונקציה vectorToBitmap
כדי ליצור שני BitmapDescriptor
s: אחד בשביל ההרים שגובהם מעל 4,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. סמנים מקובצים
בשלב הזה, תשתמשו ב-composable Clustering
כדי להוסיף קיבוץ פריטים על סמך זום.
רכיב ה-Composable Clustering
דורש אוסף של רכיבי 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
s מרשימת ההרים. הערה: הקוד הזה משתמש ב-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
של הקומפוזבל Clustering
מגדיר בלוק קומפוזבל מותאם אישית לעיבוד פריט לא מקובץ. מטמיעים פונקציה @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)
)
}
עכשיו יוצרים ערכת צבעים ל-fourteeners וערכת צבעים נוספת להרים אחרים. בבלוק clusterItemContent
, בוחרים את ערכת הצבעים בהתאם לגובה ההר.
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
s וב-Polygon
s כדי להציג אותם במפה. לחלופין, אם רוצים להצמיד תמונה לפני השטח של הקרקע, אפשר להשתמש ב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
והצגתם כ-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 Markup Language, או 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
מטמיעה קומפוננטה שאפשר להוסיף למפה כדי להציג קנה מידה. הערה: 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 for Android – יצירת מפות דינמיות, אינטראקטיביות ומותאמות אישית, וחוויות מיקום וגיאו-מרחביות לאפליקציות ל-Android.
- Maps Compose Library – קבוצה של פונקציות וסוגי נתונים שניתנים להרכבה בקוד פתוח, שאפשר להשתמש בהם עם Jetpack Compose כדי לבנות את האפליקציה.
- android-maps-compose – קוד לדוגמה ב-GitHub שמדגים את כל התכונות שמוסברות ב-codelab הזה ועוד.
- עוד סדנאות קוד בנושא Kotlin ליצירת אפליקציות ל-Android באמצעות הפלטפורמה של מפות Google