یک نقشه به برنامه اندروید خود اضافه کنید (Kotlin با Compose)

1. قبل از شروع

این کد لبه به شما می آموزد که چگونه Maps SDK برای اندروید را با برنامه خود ادغام کنید و از ویژگی های اصلی آن با ساختن برنامه ای استفاده کنید که نقشه کوه های کلرادو، ایالات متحده را با استفاده از انواع مختلف نشانگرها نمایش می دهد. علاوه بر این، شما یاد خواهید گرفت که اشکال دیگر را روی نقشه بکشید.

وقتی کار با کد لبه تمام شد به این شکل خواهد بود:

پیش نیازها

کاری که خواهی کرد

  • فعال کردن و استفاده از کتابخانه Maps Compose برای Maps SDK برای Android برای افزودن GoogleMap به برنامه Android
  • نشانگرها را اضافه و سفارشی کنید
  • چند ضلعی ها را روی نقشه بکشید
  • زاویه دید دوربین را به صورت برنامه ای کنترل کنید

آنچه شما نیاز دارید

2. راه اندازی شوید

برای مرحله فعال سازی زیر، باید Maps SDK برای Android را فعال کنید.

پلتفرم نقشه های گوگل را راه اندازی کنید

اگر قبلاً یک حساب Google Cloud Platform و پروژه‌ای با صورت‌حساب فعال ندارید، لطفاً راهنمای شروع به کار با Google Maps Platform را برای ایجاد یک حساب صورت‌حساب و یک پروژه ببینید.

  1. در Cloud Console ، روی منوی کشویی پروژه کلیک کنید و پروژه ای را که می خواهید برای این کد لبه استفاده کنید انتخاب کنید.

  1. APIها و SDKهای پلتفرم Google Maps مورد نیاز برای این لبه کد را در Google Cloud Marketplace فعال کنید. برای انجام این کار، مراحل این ویدیو یا این مستند را دنبال کنید.
  2. یک کلید API در صفحه Credentials در Cloud Console ایجاد کنید. می توانید مراحل این ویدئو یا این مستند را دنبال کنید. همه درخواست‌ها به پلتفرم Google Maps به یک کلید API نیاز دارند.

3. شروع سریع

برای شروع هر چه سریع‌تر، در اینجا چند کد شروع وجود دارد که به شما کمک می‌کند تا این کد را دنبال کنید. می‌توانید به سراغ راه‌حل بروید، اما اگر می‌خواهید تمام مراحل ساخت آن را خودتان دنبال کنید، به خواندن ادامه دهید.

  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 مراجعه کنید.

برای ساده‌سازی این کار، توصیه می‌کنیم از افزونه Secrets Gradle برای اندروید استفاده کنید.

برای نصب افزونه Secrets Gradle برای اندروید در پروژه 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 را در دایرکتوری سطح بالای خود باز کنید و سپس کد زیر را اضافه کنید. کلید API خود را جایگزین YOUR_API_KEY کنید. کلید خود را در این فایل ذخیره کنید زیرا 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 را اضافه کنید

در این بخش، نقشه گوگل را اضافه می‌کنید تا با راه‌اندازی برنامه، بارگیری شود.

افزودن نقشه‌ها وابستگی‌های نوشتن

اکنون که کلید API شما در داخل برنامه قابل دسترسی است، گام بعدی این است که وابستگی Maps SDK برای اندروید را به فایل build.gradle.kts برنامه خود اضافه کنید. برای ساخت با Jetpack Compose، از کتابخانه Maps Compose استفاده کنید که عناصری از Maps SDK برای Android را به‌عنوان توابع و انواع داده‌های قابل نوشتن ارائه می‌کند.

build.gradle.kts

در فایل build.gradle.kts در سطح برنامه ، Maps SDK غیرکامپوزیشن را برای وابستگی‌های Android جایگزین کنید:

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 Mapsable اضافه کنید

در 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. یک ظاهر طراحی نقشه مبتنی بر ابر

می توانید سبک نقشه خود را با استفاده از استایل نقشه مبتنی بر ابر سفارشی کنید.

یک شناسه نقشه ایجاد کنید

اگر هنوز شناسه نقشه با سبک نقشه مرتبط با آن ایجاد نکرده‌اید، برای تکمیل مراحل زیر، راهنمای Map IDs را ببینید:

  1. یک شناسه نقشه ایجاد کنید.
  2. شناسه نقشه را به سبک نقشه مرتبط کنید.

شناسه نقشه را به برنامه خود اضافه کنید

برای استفاده از شناسه نقشه‌ای که ایجاد کرده‌اید، هنگام نمونه‌سازی GoogleMap composable خود، از شناسه نقشه هنگام ایجاد یک شی 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,
)

توجه داشته باشید که بعداً کوه ها بر اساس ارتفاعشان تقسیم بندی خواهند شد. کوه هایی که حداقل 14000 فوت ارتفاع دارند، چهارده نامیده می شوند. کد شروع شامل یک تابع افزونه است که این کار را برای شما بررسی کنید.

/**
 * 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>> برمی گرداند.

مخزن کوهستان

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 یک StateFlow داغ است که UI می تواند با استفاده از تابع extension collectAsState آن را به عنوان یک حالت تغییرپذیر مشاهده کند.

با پیروی از اصول صحیح معماری، MountainsViewModel تمام وضعیت برنامه را حفظ می کند. رابط کاربری تعاملات کاربر را با استفاده از روش onEvent به مدل view ارسال می کند.

@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 در اندروید استودیو باز کنید.

از ViewModel استفاده کنید

مدل view در MainActivity برای بدست آوردن viewState استفاده می شود. شما از viewState برای رندر کردن نشانگرها در این کد لبه استفاده خواهید کرد. توجه داشته باشید که این کد قبلاً در پروژه شروع گنجانده شده است و فقط برای مرجع در اینجا نشان داده شده است.

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

8. دوربین را قرار دهید

یک GoogleMap پیش‌فرض روی عرض جغرافیایی صفر و طول جغرافیایی صفر متمرکز می‌شود. نشانگرهایی که ارائه خواهید کرد در ایالت کلرادو در ایالات متحده آمریکا قرار دارند. viewState ارائه شده توسط مدل view یک 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
        )
    }
}

در مرحله بعد، هر زمان که محدوده اطراف مجموعه نشانگر تغییر می کند یا زمانی که کاربر روی دکمه وسعت بزرگنمایی در نوار TopApp کلیک می کند، کدی را برای فراخوانی تابع zoomAll اضافه کنید. توجه داشته باشید که دکمه وسعت بزرگنمایی از قبل برای ارسال رویدادها به مدل view متصل شده است. شما فقط باید آن رویدادها را از مدل view جمع آوری کنید و تابع zoomAll را در پاسخ فراخوانی کنید.

دکمه Extents

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

نشانگرها را اضافه کنید

با @GoogleMapComposable BasicMarkersMapContent حاشیه نویسی کنید. توجه داشته باشید که شما محدود به استفاده از توابع @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 سفارشی شده استفاده کنید. یکی برای چهارده نفر و دیگری برای کوه های معمولی. سپس از پارامتر 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 ویژگی های اضافی را به 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 سفارشی شده

آیکون ها از طرح های رنگی اولیه و ثانویه برای تمایز بین چهارده و سایر کوه ها استفاده می کنند. از تابع vectorToBitmap برای ایجاد دو BitmapDescriptor استفاده کنید. یکی برای چهارده ها و یکی برای کوه های دیگر. از این نمادها برای ایجاد یک 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 composable برای اضافه کردن گروه بندی آیتم های مبتنی بر زوم استفاده خواهید کرد.

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 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 یک بلوک سفارشی را تنظیم می کند تا یک مورد غیر خوشه ای را ارائه دهد. برای ایجاد نشانگر یک تابع @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)
    )
}

حالا یک طرح رنگی برای چهارده نفر و یک طرح رنگی دیگر برای کوه های دیگر ایجاد کنید. در بلوک 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 برای نمایش آن‌ها روی نقشه استفاده کنید. یا اگر می‌خواهید تصویری را روی سطح زمین ثابت کنید، می‌توانید از GroundOverlay استفاده کنید.

در این کار، شما یاد می گیرید که چگونه شکل ها را بکشید، به ویژه طرح کلی در اطراف ایالت کلرادو. مرز کلرادو بین 37 درجه شمالی و 41 درجه شمالی و 102 درجه و 03 دقیقه غربی و 109 درجه و 03 دقیقه غربی تعریف می شود. این ترسیم طرح کلی را بسیار ساده می کند.

کد شروع شامل یک کلاس 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 for Android Utility Library عملکردی برای افزودن یک لایه 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 a نیست

@GoogleMapComposable و بنابراین نمی توان آن را به محتوای GoogleMap اضافه کرد. در عوض آن را به Box که نقشه را در خود نگه می دارد اضافه می کنید.

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

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

برنامه را اجرا کنید تا کد لبه کاملاً پیاده سازی شده را ببینید.

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 for Android - نقشه‌های پویا، تعاملی، سفارشی‌شده، موقعیت مکانی و تجربیات مکانی را برای برنامه‌های Android خود بسازید.
  • Maps Compose Library - مجموعه ای از توابع و انواع داده های قابل نوشتن منبع باز که می توانید با Jetpack Compose برای ساخت برنامه خود استفاده کنید.
  • android-maps-compose - نمونه کد در GitHub که تمام ویژگی های پوشش داده شده در این Codelab و موارد دیگر را نشان می دهد.
  • کدهای Kotlin بیشتر برای ساخت برنامه های اندروید با پلتفرم نقشه های گوگل