إضافة خريطة إلى تطبيق Android (استخدام Kotlin مع Compose)

1. قبل البدء

يعلّمك هذا الدرس التطبيقي كيفية دمج حزمة تطوير البرامج (SDK) لخدمة "خرائط Google" لنظام التشغيل Android مع تطبيقك واستخدام ميزاتها الأساسية من خلال إنشاء تطبيق يعرض خريطة للجبال في ولاية كولورادو الأمريكية باستخدام أنواع مختلفة من العلامات. بالإضافة إلى ذلك، ستتعلّم كيفية رسم أشكال أخرى على الخريطة.

في ما يلي الشكل الذي ستظهر به بعد الانتهاء من تجربة الترميز:

المتطلبات الأساسية

الإجراءات التي ستنفذّها

  • تفعيل مكتبة Maps Compose واستخدامها مع "حزمة تطوير البرامج بالاستناد إلى بيانات خرائط Google" لتطبيقات Android من أجل إضافة GoogleMap إلى تطبيق Android
  • إضافة علامات وتخصيصها
  • رسم مضلّعات على الخريطة
  • التحكّم في نقطة عرض الكاميرا آليًا

المتطلبات

2. طريقة الإعداد

في خطوة التفعيل التالية، عليك تفعيل حزمة تطوير البرامج بالاستناد إلى بيانات "خرائط Google" لتطبيقات Android.

إعداد Google Maps Platform

إذا لم يكن لديك حساب على Google Cloud Platform ومشروع مفعَّل فيه نظام الفوترة، يُرجى الاطّلاع على دليل البدء باستخدام Google Maps Platform لإنشاء حساب فوترة ومشروع.

  1. في Cloud Console، انقر على القائمة المنسدلة الخاصة بالمشروع واختَر المشروع الذي تريد استخدامه في هذا الدرس العملي.

  1. فعِّل واجهات برمجة التطبيقات وحِزم تطوير البرامج (SDK) في Google Maps Platform المطلوبة لهذا الدرس العملي في Google Cloud Marketplace. لإجراء ذلك، اتّبِع الخطوات الواردة في هذا الفيديو أو هذه المستندات.
  2. أنشئ مفتاح واجهة برمجة التطبيقات في صفحة بيانات الاعتماد في Cloud Console. يمكنك اتّباع الخطوات الواردة في هذا الفيديو أو هذه المستندات. تتطلّب جميع الطلبات إلى "منصة خرائط Google" مفتاح واجهة برمجة تطبيقات.

3- البدء بسرعة

لمساعدتك في البدء بأسرع ما يمكن، إليك بعض الرموز البرمجية الأولية لمساعدتك في متابعة هذا الدرس العملي. يمكنك الانتقال إلى الحلّ مباشرةً، ولكن إذا أردت اتّباع جميع الخطوات لإنشائه بنفسك، يمكنك مواصلة القراءة.

  1. استنسِخ المستودع إذا كان لديك git مثبَّتًا.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

يمكنك بدلاً من ذلك النقر على الزر التالي لتنزيل رمز المصدر.

  1. بعد الحصول على الرمز، افتح المشروع الموجود في الدليل starter في "استوديو Android".

4. إضافة مفتاح واجهة برمجة التطبيقات إلى المشروع

يوضّح هذا القسم كيفية تخزين مفتاح واجهة برمجة التطبيقات حتى يتمكّن تطبيقك من الرجوع إليه بأمان. يجب عدم إدخال مفتاح واجهة برمجة التطبيقات في نظام التحكّم في الإصدارات، لذا ننصح بتخزينه في الملف secrets.properties، الذي سيتم وضعه في نسختك المحلية من الدليل الجذر لمشروعك. لمزيد من المعلومات عن ملف secrets.properties، يُرجى الاطّلاع على ملفات Gradle.

لتبسيط هذه المهمة، ننصحك باستخدام المكوّن الإضافي Secrets Gradle لأجهزة Android.

لتثبيت المكوّن الإضافي Secrets Gradle لأجهزة Android في مشروع "خرائط Google"، اتّبِع الخطوات التالية:

  1. في Android Studio، افتح ملف build.gradle.kts ذي المستوى الأعلى وأضِف الرمز التالي إلى العنصر dependencies ضمن buildscript.
    buildscript {
        dependencies {
            classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
        }
    }
    
  2. افتح ملف build.gradle.kts على مستوى الوحدة وأضِف الرمز التالي إلى العنصر plugins.
    plugins {
        // ...
        id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
    }
    
  3. في ملف build.gradle.kts على مستوى الوحدة، تأكَّد من ضبط targetSdk وcompileSdk على 34 على الأقل.
  4. احفظ الملف وزامِن مشروعك مع Gradle.
  5. افتح ملف secrets.properties في الدليل ذي المستوى الأعلى، ثم أضِف الرمز التالي. استبدِل YOUR_API_KEY بمفتاح واجهة برمجة التطبيقات. خزِّن مفتاحك في هذا الملف لأنّ secrets.properties مستبعد من إمكانية التحقّق من نظام التحكّم بالإصدارات.
    MAPS_API_KEY=YOUR_API_KEY
    
  6. احفظ الملف.
  7. أنشئ ملف local.defaults.properties في الدليل على المستوى الأعلى، أي المجلد نفسه الذي يحتوي على ملف secrets.properties، ثم أضِف الرمز التالي.
        MAPS_API_KEY=DEFAULT_API_KEY
    
    الغرض من هذا الملف هو توفير موقع احتياطي لمفتاح واجهة برمجة التطبيقات في حال تعذّر العثور على الملف secrets.properties، وذلك لضمان عدم تعذّر إنشاء الإصدارات. سيحدث ذلك عند استنساخ التطبيق من نظام التحكّم في الإصدارات ولم تنشئ بعد ملف secrets.properties على جهازك لتوفير مفتاح واجهة برمجة التطبيقات.
  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"

في هذا القسم، ستضيف خريطة Google ليتم تحميلها عند تشغيل التطبيق.

إضافة تبعيات Maps Compose

بعد أن أصبح بإمكانك الوصول إلى مفتاح واجهة برمجة التطبيقات داخل التطبيق، تتمثّل الخطوة التالية في إضافة تبعية "حزمة تطوير البرامج (SDK) لخرائط Google" لنظام التشغيل Android إلى ملف build.gradle.kts الخاص بتطبيقك. لإنشاء تطبيقات باستخدام Jetpack Compose، استخدِم مكتبة Maps Compose التي توفّر عناصر "حزمة تطوير البرامج بالاستناد إلى بيانات خرائط Google للتطبيقات المتوافقة مع Android" كدوال قابلة للإنشاء وأنواع بيانات.

build.gradle.kts

في ملف build.gradle.kts على مستوى التطبيق، استبدِل التبعيات غير المتوافقة مع Compose لحزمة تطوير البرامج بالاستناد إلى بيانات "خرائط Google" لتطبيقات 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"

في MountainMap.kt، أضِف العنصر GoogleMap القابل للإنشاء داخل العنصر Box القابل للإنشاء المتداخل مع العنصر MapMountain القابل للإنشاء.

import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...

@Composable
fun MountainMap(
    paddingValues: PaddingValues,
    viewState: MountainsScreenViewState.MountainList,
    eventFlow: Flow<MountainsScreenEvent>,
    selectedMarkerType: MarkerType,
) {
    var isMapLoaded by remember { mutableStateOf(false) }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(paddingValues)
    ) {
        // Add GoogleMap here
        GoogleMap(
            modifier = Modifier.fillMaxSize(),
            onMapLoaded = { isMapLoaded = true }
        )

        // ...
    }
}

الآن، أنشئ التطبيق وشغِّله. من المفترض أن تظهر لك خريطة في وسطها جزيرة Null الشهيرة، المعروفة أيضًا باسم خط العرض صفر وخط الطول صفر. في وقت لاحق، ستتعرّف على كيفية تحديد موضع الخريطة على الموقع الجغرافي ومستوى التكبير/التصغير الذي تريده، ولكن في الوقت الحالي، احتفل بفوزك الأول.

6. تصميم الخرائط باستخدام السحابة الإلكترونية

يمكنك تخصيص نمط الخريطة باستخدام تصميم الخرائط باستخدام السحابة الإلكترونية.

إنشاء رقم تعريف خريطة

إذا لم يسبق لك إنشاء معرّف خريطة مرتبط بنمط خريطة، يمكنك الاطّلاع على دليل معرّفات الخرائط لإكمال الخطوات التالية:

  1. أنشئ معرّف خريطة.
  2. ربط رقم تعريف خريطة بنمط خريطة

إضافة معرّف الخريطة إلى تطبيقك

لاستخدام معرّف الخريطة الذي أنشأته، استخدِم معرّف الخريطة عند إنشاء عنصر GoogleMap القابل للإنشاء، وذلك عند إنشاء عنصر GoogleMapOptions يتم تعيينه إلى المَعلمة googleMapOptionsFactory في الدالة الإنشائية.

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

بعد إكمال هذه الخطوات، يمكنك تشغيل التطبيق للاطّلاع على الخريطة بالنمط الذي اخترته.

7. تحميل بيانات العلامات

المهمة الرئيسية للتطبيق هي تحميل مجموعة من الجبال من وحدة التخزين المحلية وعرض هذه الجبال في GoogleMap. في هذه الخطوة، ستتعرّف على البنية الأساسية المتوفّرة لتحميل بيانات الجبال وعرضها في واجهة المستخدم.

جبل

يحتوي فئة البيانات Mountain على جميع البيانات المتعلقة بكل جبل.

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

يُرجى العِلم أنّه سيتم تقسيم الجبال لاحقًا استنادًا إلى ارتفاعها. تُعرف الجبال التي يبلغ ارتفاعها 14,000 قدم على الأقل باسم الجبال الأربعة عشر. يتضمّن الرمز البرمجي المُعد مسبقًا دالة إضافية لإجراء عملية التحقّق هذه نيابةً عنك.

/**
 * 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 هو تدفق نشط 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".

استخدام ViewModel

يتم استخدام نموذج العرض في MainActivity للحصول على viewState. ستستخدِم 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 كلما تغيّرت الحدود المحيطة بمجموعة العلامات أو عندما ينقر المستخدم على زر "تكبير المدى" في شريط TopApp. يُرجى العِلم أنّه تمّ ربط زرّ تكبير/تصغير المدى لإرسال الأحداث إلى نموذج العرض. ما عليك سوى جمع هذه الأحداث من نموذج العرض واستدعاء الدالة 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، لإنشاء BitmapDescriptors من @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 لضبط الرمز. اضبط أيضًا المَعلمة anchor لتغيير موقع نقطة الارتكاز بالنسبة إلى الرمز. ويكون استخدام المركز أفضل لهذه الرموز الدائرية.

@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
    // ...
) {
    // Create mountainIcon and fourteenerIcon
    val mountainIcon = vectorToBitmap(
        LocalContext.current,
        BitmapParameters(
            id = R.drawable.baseline_filter_hdr_24,
            iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
            backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
        )
    )

    val fourteenerIcon = vectorToBitmap(
        LocalContext.current,
        BitmapParameters(
            id = R.drawable.baseline_filter_hdr_24,
            iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
            backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
        )
    )

    mountains.forEach { mountain ->
        val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
        Marker(
            // ...
            anchor = Offset(0.5f, 0.5f),
            icon = icon,
        )
    }
}

شغِّل التطبيق واستمتع بالعلامات المخصّصة. انقر على زر التبديل Show all للاطّلاع على المجموعة الكاملة من الجبال. ستتضمّن الجبال علامات مختلفة حسب ما إذا كان الجبل يبلغ ارتفاعه 14,000 قدم أو أكثر.

10. العلامات المتقدّمة

تضيف AdvancedMarkers ميزات إضافية إلى Markers الأساسي. في هذه الخطوة، عليك ضبط سلوك التصادم وتحديد نمط الدبوس.

أضِف @GoogleMapComposable إلى الدالة AdvancedMarkersMapContent. كرِّر mountains مع إضافة AdvancedMarker لكل منها.

@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
    mountains: List<Mountain>,
    onMountainClick: (Marker) -> Boolean = { false },
) {
    mountains.forEach { mountain ->
        AdvancedMarker(
            state = rememberMarkerState(position = mountain.location),
            title = mountain.name,
            snippet = mountain.elevation.toElevationString(),
            collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
            onClick = { marker ->
                onMountainClick(marker)
                false
            }
        )
    }
}

لاحظ المَعلمة collisionBehavior. من خلال ضبط هذه المَعلمة على REQUIRED_AND_HIDES_OPTIONAL، سيحلّ المؤشر محلّ أي مؤشر ذي أولوية أقل. يمكنك ملاحظة ذلك من خلال تكبير محدّد أساسي مقارنةً بمحدّد متقدّم. من المحتمل أن يتضمّن المحدّد الأساسي كلاً من المحدّد الخاص بك والمحدّد الذي تم وضعه في الموقع الجغرافي نفسه في الخريطة الأساسية. سيؤدي استخدام العلامة المتقدّمة إلى إخفاء العلامة ذات الأولوية الأقل.

شغِّل التطبيق ��لاطّلاع على العلامات المتقدّمة. احرص على النقر على علامة التبويب Advanced markers في صف شريط التنقل في أسفل الشاشة.

مخصّص AdvancedMarkers

تستخدم الرموز أنظمة الألوان الأساسية والثانوية للتمييز بين الجبال التي يزيد ارتفاعها عن 14,000 قدم والجبال الأخرى. استخدِم الدالة vectorToBitmap لإنشاء BitmapDescriptor، أحدهما للجبال التي يزيد ارتفاعها عن 14,000 قدم والآخر للجبال الأخرى. استخدِم هذه الرموز لإنشاء pinConfig مخصّص لكل نوع. أخيرًا، طبِّق الدبوس على AdvancedMarker المناسب استنادًا إلى الدالة is14er().

@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
    mountains: List<Mountain>,
    onMountainClick: (Marker) -> Boolean = { false },
) {
    val mountainIcon = vectorToBitmap(
        LocalContext.current,
        BitmapParameters(
            id = R.drawable.baseline_filter_hdr_24,
            iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
        )
    )

    val mountainPin = with(PinConfig.builder()) {
        setGlyph(PinConfig.Glyph(mountainIcon))
        setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
        setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
        build()
    }

    val fourteenerIcon = vectorToBitmap(
        LocalContext.current,
        BitmapParameters(
            id = R.drawable.baseline_filter_hdr_24,
            iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
        )
    )

    val fourteenerPin = with(PinConfig.builder()) {
        setGlyph(PinConfig.Glyph(fourteenerIcon))
        setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
        setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
        build()
    }

    mountains.forEach { mountain ->
        val pin = if (mountain.is14er()) fourteenerPin else mountainPin
        AdvancedMarker(
            state = rememberMarkerState(position = mountain.location),
            title = mountain.name,
            snippet = mountain.elevation.toElevationString(),
            collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
            pinConfig = pin,
            onClick = { marker ->
                onMountainClick(marker)
                false
            }
        )
    }
}

11. علامات مجمّعة

في هذه الخطوة، ستستخدم Clustering القابلة للإنشاء لإضافة تجميع العناصر المستند إلى التكبير.

يتطلّب العنصر القابل للإنشاء Clustering مجموعة من عناصر 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
}

الآن، أضِف الرمز لإنشاء MountainClusterItems من قائمة الجبال. يُرجى العِلم أنّ هذا الرمز يستخدم 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)
    )
}

الآن، أنشئ نظام ألوان للجبال التي يزيد ارتفاعها عن 14,000 قدم ونظام ألوان آخر للجبال الأخرى. في المربّع clusterItemContent، اختَر نظام الألوان استنادًا إلى ما إذا كان الجبل المحدّد يبلغ ارتفاعه 14, 000 قدم أم لا.

fun ClusteringMarkersMapContent(
    mountains: List<Mountain>,
    // ...
) {
  // ...

  val backgroundAlpha = 0.6f

  val fourteenerColors = IconColor(
      iconColor = MaterialTheme.colorScheme.onPrimary,
      backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
      borderColor = MaterialTheme.colorScheme.primary
  )

  val otherColors = IconColor(
      iconColor = MaterialTheme.colorScheme.secondary,
      backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
      borderColor = MaterialTheme.colorScheme.secondary
  )

  // ...
  Clustering(
      items = mountainClusterItems,
      clusterItemContent = { mountainItem ->
          val colors = if (mountainItem.mountain.is14er()) {
              fourteenerColors
          } else {
              otherColors
          }
          SingleMountain(colors)
      },
  )
}

الآن، شغِّل التطبيق للاطّلاع على إصدارات مخصّصة من العناصر الفردية.

12. الرسم على الخريطة

بعد أن تعرّفت على إحدى طرق الرسم على الخريطة (من خلال إضافة علامات)، تتيح "حزمة تطوير البرامج بالاستناد إلى بيانات خرائط Google" لنظام التشغيل Android العديد من الطرق الأخرى التي يمكنك من خلالها الرسم لعرض معلومات مفيدة على الخريطة.

على سبيل المثال، إذا أردت تمثيل المسارات والمناطق على الخريطة، يمكنك استخدام Polyline و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 أو KML يحدّد بشكل تقريبي السلاسل الجبلية المهمة. تتضمّن مكتبة أدوات "حزمة تطوير البرامج بالاستناد إلى بيانات خرائط Google" لتطبيقات Android دالة لإضافة طبقة KML إلى الخريطة. في MountainMap.kt، أضِف MapEffect مكالمة في قسم المحتوى GoogleMap بعد القسم when. يتم استدعاء الدالة MapEffect باستخدام العنصر GoogleMap. ويمكن أن يكون هذا النوع من الدوال مفيدًا في الربط بين واجهات برمجة التطبيقات والمكتبات غير القابلة للإنشاء والتي تتطلّب كائن 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
    )
    // ...
}

شغِّل التطبيق للاطّلاع على الدرس التطبيقي المبرمَج الذي تم تنفيذه بالكامل.

14. الحصول على رمز الحلّ

لتنزيل الرمز البرمجي الخاص بدرس البرمجة المكتمل، يمكنك استخدام الأوامر التالية:

  1. استنسِخ المستودع إذا كان لديك git مثبَّتًا.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git

يمكنك بدلاً من ذلك النقر على الزر التالي لتنزيل رمز المصدر.

  1. بعد الحصول على الرمز، افتح المشروع الموجود في الدليل solution في "استوديو Android".

15. تهانينا

تهانينا! لقد تناولنا الكثير من المحتوى، ونأمل أن تكون لديك الآن فكرة أفضل عن الميزات الأساسية المتوفّرة في "حزمة تطوير البرامج بالاستناد إلى بيانات خرائط Google" لنظام التشغيل Android.

مزيد من المعلومات