הוספת מפה לאפליקציית Android שלכם (Kotlin)

קל לארגן דפים בעזרת אוספים אפשר לשמור ולסווג תוכן על סמך ההעדפות שלך.

1. לפני שתתחיל

בשיעור Lab זה תלמדו איך לשלב את SDK של Android ל-Android עם האפליקציה, ולהשתמש בתכונות הליבה שלה על ידי בניית אפליקציה שמציגה מפה של חנויות אופניים בסן פרנסיסקו, קליפורניה, ארה"ב.

f05e1ca27ff42bf6.png

דרישות מוקדמות

  • ידע בסיסי בפיתוח של Kotlin ו-Android

הפעולות שתבצעו:

  • יש להפעיל את ה-SDK של מפות Google ל-Android ולהשתמש בו כדי להוסיף את מפות Google לאפליקציה ל-Android.
  • הוספה, התאמה אישית וקיבוץ של סמנים.
  • שרטטו מצולעים ומצולעים על המפה.
  • שליטה בנקודת המבט של המצלמה באופן פרוגרמטי.

מה תצטרך להכין

2. להגדרה

לשלב ההפעלה הבא צריך להפעיל את Maps SDK ל-Android.

הגדרת מפות Google

אם עדיין אין לכם חשבון Google Cloud Platform ופרויקט שבו מופעל חיוב, כדאי לעיין במדריך תחילת העבודה עם הפלטפורמה של מפות Google ליצירת חשבון לחיוב ופרויקט.

  1. ב-Cloud Console, לוחצים על התפריט הנפתח של הפרויקט ובוחרים את הפרויקט שבו רוצים להשתמש ב-Codelab הזה.

  1. מפעילים את ממשקי ה-API ואת ערכות ה-SDK של מפות Google הנדרשים למעבדת קוד זו ב-Google Cloud Marketplace. כדי לעשות זאת, יש לבצע את השלבים המפורטים בסרטון הזה או בתיעוד הזה.
  2. יוצרים מפתח API בדף פרטי הכניסה ב-Cloud Console. ניתן לבצע את השלבים המפורטים בסרטון הזה או בתיעוד הזה. לכל הבקשות שנשלחות לפלטפורמה של מפות Google נדרש מפתח API.

3. התחלה מהירה

כדי לעזור לך להתחיל במהירות האפשרית, לפניך קוד התחלה שיעזור לך לעקוב אחר שיעור Lab זה. אתם מוזמנים לקפוץ לפתרון, אבל אם אתם רוצים להמשיך בכל השלבים בעצמכם, המשיכו לקרוא.

  1. משכפלים את המאגר אם git מותקן.
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

לחלופין, אפשר ללחוץ על הלחצן הבא כדי להוריד את קוד המקור.

  1. אחרי קבלת הקוד, יש לפתוח את הפרויקט שנמצא בתוך הספרייה של starter ב-Android Studio.

4. הוספת מפות Google

בקטע הזה, מוסיפים את מפות Google כך שהוא ייטען כאשר מפעילים את האפליקציה.

d1d068b5d4ae38b9.png

הוספת מפתח API

צריך לספק לאפליקציה את מפתח ה-API שיצרתם בשלב מוקדם יותר. כך, ה-SDK של מפות Google ל-Android יוכל לשייך את המפתח לאפליקציה.

  1. כדי לספק זאת, יש לפתוח את הקובץ בשם local.properties בספריית הבסיס של הפרויקט (ברמה שבה נמצאים gradle.properties ו-settings.gradle).
  2. בקובץ הזה, עליך להגדיר מפתח חדש GOOGLE_MAPS_API_KEY והערך שלו הוא מפתח ה-API שיצרת.

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

הערה: local.properties רשום בקובץ .gitignore במאגר של Git. הסיבה לכך היא שמפתח ה-API נחשב למידע רגיש, ואין לבדוק אותו כדי לשלוט במקור, אם הדבר אפשרי.

  1. לאחר מכן, כדי לחשוף את ה-API כך שניתן יהיה להשתמש בו בכל האפליקציה, יש לכלול את הפלאגין Secrets Gradle for Android בקובץ build.gradle של האפליקציה. הקובץ ממוקם בספרייה של app/ ומוסיפים את השורה הבאה בקטע plugins:

build.gradle ברמת האפליקציה

plugins {
    // ...
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}

צריך לשנות גם את קובץ ה-build.gradle ברמת הפרויקט כך שיכלול את נתיב הכיתה הבא:

build.gradle ברמת הפרויקט

buildscript {
    dependencies {
        // ...
        classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.3.0"
    }
}

הפלאגין הזה יאפשר לך להגדיר מפתחות בקובץ local.properties כמשתני build בקובץ המניפסט של Android, וכמשתנים בקורס BuildConfig שנוצר על ידי Gradle בזמן היצירה. שימוש בפלאגין הזה מסיר את הקוד המבודד שלפיהם ניתן היה לקרוא נכסים מ-local.properties כדי שניתן יהיה לגשת אליהם מהאפליקציה.

הוספת תלות במפות Google

  1. עכשיו אפשר לגשת למפתח ה-API באפליקציה, בשלב הבא עליך להוסיף את התלות של מפות Google ל-Android בקובץ build.gradle של האפליקציה שלך.

בפרויקט למתחילים שמגיע עם קוד שיעור זה, התלות הזאת כבר נוספה עבורך.

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. בשלב הבא, עליך להוסיף תג meta-data חדש ב-AndroidManifest.xml כדי להעביר את מפתח ה-API שיצרת בשלב קודם. כדי לעשות את זה, צריך לפתוח את הקובץ הזה ב-Android Studio ולהוסיף את התג meta-data הבא לאובייקט application בקובץ AndroidManifest.xml, שנמצא ב-app/src/main.

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. לאחר מכן, יוצרים קובץ פריסה חדש בשם activity_main.xml בספרייה app/src/main/res/layout/ ומגדירים אותו באופן הבא:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

   <fragment
       class="com.google.android.gms.maps.SupportMapFragment"
       android:id="@+id/map_fragment"
       android:layout_width="match_parent"
       android:layout_height="match_parent" />

</FrameLayout>

לפריסה הזו יש FrameLayout יחיד שמכיל SupportMapFragment. קטע זה מכיל את אובייקט GoogleMaps הבסיסי שבו תשתמשו בשלבים הבאים.

  1. לסיום, יש לעדכן את הכיתה MainActivity שנמצאת ב-app/src/main/java/com/google/codelabs/buildyourfirstmap על ידי הוספת הקוד הבא כדי לעקוף את השיטה onCreate כדי להגדיר את התוכן שלו בפריסה החדשה שיצרתם.

פעילות עיקרית

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. עכשיו צריך להריץ את האפליקציה. עכשיו אמורה להופיע הטעינה של המסך במסך המכשיר.

5. עיצוב מפה מבוסס ענן (אופציונלי)

אפשר להתאים אישית את סגנון המפה באמצעות סגנון המפה המבוסס על ענן.

יצירת מזהה מפה

אם עדיין לא יצרתם מזהה מפה שמשויכים אליו סגנון מפה, תוכלו להיעזר במדריך מזהי מפות כדי:

  1. יוצרים מזהה מפה.
  2. משייכים מזהה מפה לסגנון מפה.

הוספת מזהה המפה לאפליקציה

כדי להשתמש במזהה המפה שיצרת, עליך לשנות את הקובץ activity_main.xml ולהעביר את מזהה המפה במאפיין map:mapId של SupportMapFragment.

activity_main.xml

<fragment xmlns:map="http://schemas.android.com/apk/res-auto"
    class="com.google.android.gms.maps.SupportMapFragment"
    <!-- ... -->
    map:mapId="YOUR_MAP_ID" />

אחרי שמסיימים, מפעילים את האפליקציה כדי לראות את המפה בסגנון שבחרתם.

6. הוספת סמנים

במשימה הזו אתם מוסיפים סמנים למפה שמייצגים נקודות עניין שאתם רוצים להדגיש במפה. קודם כל, מאחזרים רשימה של מקומות שסופקו בפרויקט לתחילת העבודה עבורכם, ואז מוסיפים את המקומות האלה למפה. בדוגמה הזו, אלה חנויות אופניים.

bc5576877369b554.png

קבלת עזרה לגבי מפות Google

תחילה, עליך לקבל הפניה לאובייקט GoogleMap כדי שתוכל להשתמש בשיטות שלו. כדי לעשות זאת, יש להוסיף את הקוד הבא לשיטה MainActivity.onCreate() מיד לאחר השיחה אל setContentView():

PrimaryActivity.onCreate()

val mapFragment = supportFragmentManager.findFragmentById(   
    R.id.map_fragment
) as? SupportMapFragment
mapFragment?.getMapAsync { googleMap ->
    addMarkers(googleMap)
}

האפליקציה מוצאת תחילה את SupportMapFragment שהוספת בשלב הקודם באמצעות השיטה findFragmentById() באובייקט SupportFragmentManager. לאחר שמתקבלת הפניה, הקריאה אל getMapAsync() מופעלת ועוברת בה למדה. למדה זו היא המקום שבו האובייקט של GoogleMap מועבר. בתוך המלבה הזו, מתבצעת קריאה לשיטה addMarkers(). היא מוגדרת תוך זמן קצר.

מחלקה שסופקה: PlacesReader

בפרויקט למתחילים, הוזמנת לכיתה PlacesReader. סיווג זה קורא רשימה של 49 מקומות המאוחסנים בקובץ JSON בשם places.json ומחזיר אותם כ-List<Place>. המקומות עצמם מייצגים רשימה של חנויות אופניים סביב סן פרנסיסקו, קליפורניה, ארה"ב.

אם סקרן לגבי ההטמעה של הכיתה הזו, יש לך אפשרות לגשת אליה ב-GitHub או לפתוח את הכיתה PlacesReader ב-Android Studio.

placesReader

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import com.google.codelabs.buildyourfirstmap.R
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.InputStream
import java.io.InputStreamReader

/**
* Reads a list of place JSON objects from the file places.json
*/
class PlacesReader(private val context: Context) {

   // GSON object responsible for converting from JSON to a Place object
   private val gson = Gson()

   // InputStream representing places.json
   private val inputStream: InputStream
       get() = context.resources.openRawResource(R.raw.places)

   /**
    * Reads the list of place JSON objects in the file places.json
    * and returns a list of Place objects
    */
   fun read(): List<Place> {
       val itemType = object : TypeToken<List<PlaceResponse>>() {}.type
       val reader = InputStreamReader(inputStream)
       return gson.fromJson<List<PlaceResponse>>(reader, itemType).map {
           it.toPlace()
       }
   }

טעינת מקומות

כדי לטעון את הרשימה של חנויות האופניים, יש להוסיף נכס ב-MainActivity שנקרא places ולהגדיר אותו כך:

PrimaryActivity.places

private val places: List<Place> by lazy {
   PlacesReader(this).read()
}

הקוד הזה מפעיל את שיטת read() ב-PlacesReader, שמחזירה List<Place>. לPlace יש נכס בשם name, שם המקום ו-latLng – הקואורדינטות שבהן המקום נמצא.

מקום

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: LatLng,
   val rating: Float
)

הוספת סמנים למפה

כעת, לאחר שרשימת המקומות נטענת בזיכרון, השלב הבא הוא לייצג את המקומות האלה במפה.

  1. יוצרים שיטה בשם MainActivity שנקראת addMarkers() ומגדירים אותה כך:

PrimaryActivity.addMarkers()

/**
* Adds marker representations of the places list on the provided GoogleMap object
*/
private fun addMarkers(googleMap: GoogleMap) {
   places.forEach { place ->
       val marker = googleMap.addMarker(
           MarkerOptions()
               .title(place.name)
               .position(place.latLng)
       )
   }
}

שיטה זו חוזרת על עצמה דרך הרשימה places ולאחר מכן מפעילה את השיטה addMarker() באובייקט GoogleMap שסופק. הסמן נוצר על ידי יצירת אובייקט באובייקט MarkerOptions שמאפשר לך להתאים אישית את הסמן עצמו. במקרה כזה, הכותרת והמיקום של הסמן מסופקים, המייצג את שם חנות האופניים ואת הקואורדינטות שלו, בהתאמה.

  1. מריצים את האפליקציה ועוברים לסן פרנסיסקו כדי לראות את הסמנים שהוספתם!

7. התאמה אישית של הסמנים

יש כמה אפשרויות של התאמה אישית של סמנים שהוספת כדי לעזור להם להתבלט ולהעביר מידע שימושי למשתמשים. במשימה זו, תוכלו לסקור חלק מהן על ידי התאמה אישית של התמונה של כל סמן וכן של חלון המידע המוצג בעת הקשה על סמן.

a26f82802fe838e9.png

הוספת חלון מידע

כברירת מחדל, חלון המידע כשמקישים על סמן מציג את הכותרת ואת קטע הקוד שלו (אם הוא מוגדר). התאמה אישית מאפשרת להציג מידע נוסף, כמו הכתובת והדירוג של המקום.

אפשר ליצור סמן_info_contents.xml

ראשית, יוצרים קובץ פריסה חדש בשם marker_info_contents.xml.

  1. כדי לעשות זאת, לוחצים לחיצה ימנית על התיקייה app/src/main/res/layout בתצוגת הפרויקט ב-Android Studio ובוחרים באפשרות חדש > פריסת משאב של משאבים.

8cac51fcbef9171b.png

  1. בתיבת הדו-שיח, מקלידים marker_info_contents בשדה שם הקובץ ואז LinearLayout בשדה Root element, ואז לוחצים על אישור.

8783af12baf07a80.png

מאוחר יותר, קובץ הפריסה הזה יופחת כדי לייצג את התוכן שמופיע בחלון המידע.

  1. יש להעתיק את התוכן שבקטע הקוד הבא, שמוסיף שלושה TextViews לקבוצת תצוגה LinearLayout אנכית, ומחליף את קוד ברירת המחדל בקובץ.

placemarks_info_contents.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:orientation="vertical"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:gravity="center_horizontal"
   android:padding="8dp">

   <TextView
       android:id="@+id/text_view_title"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="18sp"
       android:textStyle="bold"
       tools:text="Title"/>

   <TextView
       android:id="@+id/text_view_address"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="123 Main Street"/>

   <TextView
       android:id="@+id/text_view_rating"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textColor="@android:color/black"
       android:textSize="16sp"
       tools:text="Rating: 3"/>

</LinearLayout>

יצירת הטמעה של InfoWindowAdapter

אחרי שיוצרים את קובץ הפריסה עבור חלון המידע המותאם אישית, השלב הבא הוא להטמיע את הממשק GoogleMaps.InfoWindowAdapter. ממשק זה מכיל שתי שיטות, getInfoWindow() ו-getInfoContents(). שתי השיטות מחזירות אובייקט View אופציונלי, שבו הפרמטר הקודם משמש להתאמה אישית של החלון עצמו, ואילו השני הוא התאמה אישית של התוכן שלו. במקרה כזה, אפשר להטמיע את שניהם ולהתאים אישית את ההחזרה של getInfoContents() תוך החזרת הערך null ב-getInfoWindow(), המציינת שיש להשתמש בחלון ברירת המחדל.

  1. יוצרים קובץ Kotlin חדש בשם MarkerInfoWindowAdapter באותה חבילה כמו MainActivity, לוחצים לחיצה ימנית על התיקייה app/src/main/java/com/google/codelabs/buildyourfirstmap בתצוגת הפרויקט ב-Android Studio, ואז בוחרים באפשרות New > Kolin File/Class.

3975ba36eba9f8e1.png

  1. בתיבת הדו-שיח, מקלידים MarkerInfoWindowAdapter ומדגישים את הקובץ.

992235af53d3897f.png

  1. לאחר יצירת הקובץ, מעתיקים את התוכן מקטע הקוד הבא לתוך הקובץ החדש.

MarkerInfoWindowAdapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.Marker
import com.google.codelabs.buildyourfirstmap.place.Place

class MarkerInfoWindowAdapter(
    private val context: Context
) : GoogleMap.InfoWindowAdapter {
   override fun getInfoContents(marker: Marker?): View? {
       // 1. Get tag
       val place = marker?.tag as? Place ?: return null

       // 2. Inflate view and set title, address, and rating
       val view = LayoutInflater.from(context).inflate(
           R.layout.marker_info_contents, null
       )
       view.findViewById<TextView>(
           R.id.text_view_title
       ).text = place.name
       view.findViewById<TextView>(
           R.id.text_view_address
       ).text = place.address
       view.findViewById<TextView>(
           R.id.text_view_rating
       ).text = "Rating: %.2f".format(place.rating)

       return view
   }

   override fun getInfoWindow(marker: Marker?): View? {
       // Return null to indicate that the 
       // default window (white bubble) should be used
       return null
   }
}

בתוכן של השיטה getInfoContents(), הסמן שצוין בשיטה מועבר (cast) לסוג Place, ואם לא ניתן לבצע את ההעברה, השיטה תחזיר null (עדיין לא הגדרת את מאפיין התג ב-Marker, אבל זה מתבצע בשלב הבא).

לאחר מכן, הפריסה marker_info_contents.xml מנופחת ולאחר מכן הטקסט מוגדר כמכיל את TextViews בתג Place.

עדכון הפעילות הראשית

כדי להדביק את כל הרכיבים שיצרתם עד עכשיו, צריך להוסיף שתי שורות בכיתה MainActivity.

תחילה, כדי להעביר את הפרמטר InfoWindowAdapter המותאם אישית, MarkerInfoWindowAdapter, בתוך קריאה לשיטה getMapAsync, יש להפעיל את השיטה setInfoWindowAdapter() באובייקט GoogleMap וליצור מופע חדש של MarkerInfoWindowAdapter.

  1. לשם כך, יש להוסיף את הקוד הבא אחרי הקריאה לשיטת addMarkers() בתוך למדת getMapAsync().

PrimaryActivity.onCreate()

// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

לבסוף, תצטרכו להגדיר כל מקום כמאפיין התג בכל סמן שנוסף למפה.

  1. כדי לעשות זאת, צריך לשנות את הקריאה לפונקציה places.forEach{} בפונקציה addMarkers() באופן הבא:

PrimaryActivity.addMarkers()

places.forEach { place ->
   val marker = googleMap.addMarker(
       MarkerOptions()
           .title(place.name)
           .position(place.latLng)
           .icon(bicycleIcon)
   )

   // Set place as the tag on the marker object so it can be referenced within
   // MarkerInfoWindowAdapter
   marker.tag = place
}

הוספה של תמונת סמן מותאמת אישית

התאמה אישית של תמונת הסמן היא אחת הדרכים הטובות ביותר להעברת סוג המקום שהסמן מייצג במפה. בשלב זה, אתם מציגים אופניים במקום הסמנים האדומים המוגדרים כברירת מחדל כדי לייצג כל חנות במפה. הפרויקט למתחילים כולל את סמל האופניים ic_directions_bike_black_24dp.xml ב-app/src/res/drawable, שבו אתם משתמשים.

6eb7358bb61b0a88.png

הגדרת מפת סיביות מותאמת אישית על סמן

בשלב הבא, עם סמל האופניים הציורי, אתה יכול לשרטט את הסמל הבא לפי הסימון שלו לפי הסימונים של כל סמן&#39. לMarkerOptions יש שיטה icon, שמשתמשת בBitmapDescriptor בשיטה הזו כדי לעשות זאת.

בשלב הראשון, צריך להמיר את הציור וקטור שהוספת עכשיו לקובץ BitmapDescriptor. קובץ בשם BitMapHelper הכלול בפרויקט למתחילים מכיל פונקציית עזרה בשם vectorToBitmap(), שעושים בדיוק את זה.

BitmapHelper

package com.google.codelabs.buildyourfirstmap

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.util.Log
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory

object BitmapHelper {
   /**
    * Demonstrates converting a [Drawable] to a [BitmapDescriptor], 
    * for use as a marker icon. Taken from ApiDemos on GitHub:
    * https://github.com/googlemaps/android-samples/blob/main/ApiDemos/kotlin/app/src/main/java/com/example/kotlindemos/MarkerDemoActivity.kt
    */
   fun vectorToBitmap(
      context: Context,
      @DrawableRes id: Int, 
      @ColorInt color: Int
   ): BitmapDescriptor {
       val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, null)
       if (vectorDrawable == null) {
           Log.e("BitmapHelper", "Resource not found")
           return BitmapDescriptorFactory.defaultMarker()
       }
       val bitmap = Bitmap.createBitmap(
           vectorDrawable.intrinsicWidth,
           vectorDrawable.intrinsicHeight,
           Bitmap.Config.ARGB_8888
       )
       val canvas = Canvas(bitmap)
       vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
       DrawableCompat.setTint(vectorDrawable, color)
       vectorDrawable.draw(canvas)
       return BitmapDescriptorFactory.fromBitmap(bitmap)
   }
}

שיטה זו מקבלת Context, מזהה משאב הניתן לציור וכן מספר שלם צבעוני, ויוצרת ייצוג שלו BitmapDescriptor.

באמצעות שיטת העזרה, יש להצהיר על נכס חדש בשם bicycleIcon ולתת לו את ההגדרה הבאה: PrimaryActivity.bicycleIcon

private val bicycleIcon: BitmapDescriptor by lazy {
   val color = ContextCompat.getColor(this, R.color.colorPrimary)
   BitmapHelper.vectorToBitmap(this, R.drawable.ic_directions_bike_black_24dp, color)
}

בנכס הזה נעשה שימוש בצבע המוגדר מראש colorPrimary באפליקציה שלך, והוא משתמש בו כדי ליצור גוון של סמל האופניים ולהחזיר אותו בתור BitmapDescriptor.

  1. באמצעות הנכס הזה, יש להפעיל את השיטה icon של MarkerOptions בשיטה addMarkers() כדי להשלים את ההתאמה האישית של הסמלים. לשם כך, מאפיין הסמן אמור להיראות כך:

PrimaryActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. הפעילו את האפליקציה כדי לראות את הסמנים המעודכנים!

8. סמני אשכול

אם התקרבתם למפה, ייתכן ששמתם לב שהסמנים שהוספתם חופפים. קשה מאוד לקיים אינטראקציה עם סמנים חופפים וליצור רעשים רבים, שמשפיעים על נוחות השימוש של האפליקציה.

68591edc86d73724.png

כדי לשפר את חוויית המשתמש בכל פעם שיש לכם מערך נתונים גדול שמקובצים יחד, כדאי להשתמש באשכולות של סמן. ככל שמקרבים את המפה תוך התקרבות למפה, סמנים שבקרבת מקום מקובצים יחד:

f05e1ca27ff42bf6.png

כדי להטמיע את התוסף הזה, יש צורך בSDK של מפות Google ל-Android Utility Directory.

SDK של מפות לספריית השירותים ב-Android

הספרייה 'מפות Google' ל-Android Utility נוצרה כדרך להרחבת הפונקציונליות של ה-SDK של מפות Google ל-Android. היא מציעה תכונות מתקדמות, כגון אשכולות סמנים, מפות חום, תמיכה ב-KML וב-geoJson, קידוד וכתיבת קוד של פוליגון, ומספר פונקציות מסייעות בנושא גיאומטריה כדורית.

עדכון build.gradle

ספריית כלי השירות ארוזה בנפרד מ-SDK של מפות ל-Android, ולכן יש להוסיף תלות נוספת לקובץ ה-build.gradle.

  1. יש לעדכן את הקטע dependencies של הקובץ app/build.gradle.

build.gradle

implementation 'com.google.maps.android:android-maps-utils:1.1.0'
  1. עם הוספת השורה הזו, צריך לבצע סנכרון של פרויקט כדי לאחזר את התלות החדשות.

b7b030ec82c007fd.png

הטמעת אשכולות

כדי להטמיע אשכולות באפליקציה, יש לבצע את שלושת השלבים הבאים:

  1. הטמעת הממשק ClusterItem.
  2. חלוקת המשנה של הכיתה DefaultClusterRenderer.
  3. יצירה של ClusterManager והוספת פריטים.

הטמעה של ממשק ClusterItem

כל האובייקטים שמייצגים סמן שניתן לקבץ במפה צריכים להטמיע את הממשק ClusterItem. במקרה שלך, פירוש הדבר שהמודל Place צריך לעמוד בדרישות של ClusterItem. יש לפתוח את הקובץ Place.kt ולבצע בו את השינויים הבאים:

מקום

data class Place(
   val name: String,
   val latLng: LatLng,
   val address: String,
   val rating: Float
) : ClusterItem {
   override fun getPosition(): LatLng =
       latLng

   override fun getTitle(): String =
       name

   override fun getSnippet(): String =
       address
}

שלוש האשכול מגדיר את שלוש השיטות הבאות:

  • getPosition(), המייצג את המקום LatLng.
  • getTitle(), שמייצג את שם המקום
  • getSnippet(), שמייצגת את הכתובת של המקום.

סיווג המשנה ClassClusterRenderer של ברירת המחדל

הכיתה שאחראית להטמעת האשכולות ClusterManager משתמשת באופן פנימי בכיתה ClusterRenderer כדי לטפל ביצירת האשכולות תוך כדי הזזת המפה ושינוי מרחק התצוגה במפה. כברירת מחדל, הוא מגיע עם כלי הרינדור המוגדר כברירת מחדל, DefaultClusterRenderer, שמטמיע את ClusterRenderer. במקרים פשוטים, זה מספיק. עם זאת, במקרה שלך יש צורך בהתאמה אישית של סמנים, לכן עליך להרחיב את הכיתה ולהוסיף בה את ההתאמות האישיות.

לאחר מכן, יוצרים את קובץ Kotlin PlaceRenderer.kt בחבילה com.google.codelabs.buildyourfirstmap.place ומגדירים אותו באופן הבא:

PlaceRenderer

package com.google.codelabs.buildyourfirstmap.place

import android.content.Context
import androidx.core.content.ContextCompat
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.Marker
import com.google.android.gms.maps.model.MarkerOptions
import com.google.codelabs.buildyourfirstmap.BitmapHelper
import com.google.codelabs.buildyourfirstmap.R
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.clustering.view.DefaultClusterRenderer

/**
* A custom cluster renderer for Place objects.
*/
class PlaceRenderer(
   private val context: Context,
   map: GoogleMap,
   clusterManager: ClusterManager<Place>
) : DefaultClusterRenderer<Place>(context, map, clusterManager) {

   /**
    * The icon to use for each cluster item
    */
   private val bicycleIcon: BitmapDescriptor by lazy {
       val color = ContextCompat.getColor(context,
           R.color.colorPrimary
       )
       BitmapHelper.vectorToBitmap(
           context,
           R.drawable.ic_directions_bike_black_24dp,
           color
       )
   }

   /**
    * Method called before the cluster item (the marker) is rendered.
    * This is where marker options should be set.
    */
   override fun onBeforeClusterItemRendered(
      item: Place,
      markerOptions: MarkerOptions
   ) {
       markerOptions.title(item.name)
           .position(item.latLng)
           .icon(bicycleIcon)
   }

   /**
    * Method called right after the cluster item (the marker) is rendered.
    * This is where properties for the Marker object should be set.
    */
   override fun onClusterItemRendered(clusterItem: Place, marker: Marker) {
       marker.tag = clusterItem
   }
}

מחלקה זו מבטלת את שתי הפונקציות הבאות:

  • onBeforeClusterItemRendered(), הנקראת לפני שאשכול הנתונים מעובד במפה. כאן תוכלו לספק התאמות אישיות באמצעות MarkerOptions – במקרה זה, הוא מגדיר את הכותרת, המיקום והסמל של הסמן.
  • onClusterItemRenderer(), הנקראת מיד אחרי שהסמן מעובד במפה. כאן אפשר לגשת לאובייקט Marker שנוצר – במקרה זה, הוא מגדיר את מאפיין התג של הסמן.

יצירת ClusterManager והוספת פריטים

לבסוף, כדי לעבוד באשכולות, צריך לשנות את MainActivity כדי ליצור אובייקט ClusterManager ולספק את התלות הדרושות. ClusterManager מטפלת בהוספת הסמנים (האובייקטים ClusterItem) לשימוש פנימי, לכן במקום להוסיף סמנים ישירות במפה, האחריות הזו מוקצית ל-ClusterManager. נוסף לכך, ClusterManager קוראת אל setInfoWindowAdapter() באופן פנימי, לכן צריך להגדיר חלון מידע מותאם אישית באובייקט MarkerManager.Collection&ClusterManger3.

  1. כדי להתחיל, יש לשנות את התוכן של המבדה בקריאת getMapAsync() ב-MainActivity.onCreate(). יש לך אפשרות להגיב על השיחה ל-addMarkers() ול-setInfoWindowAdapter(), ובמקום זאת להפעיל שיטה שנקראת addClusteredMarkers(). צריך להגדיר אותה בהמשך.

PrimaryActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
    //addMarkers(googleMap)
    addClusteredMarkers(googleMap)

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. בשלב הבא, מגדירים את MainActivity ב-addClusteredMarkers().

PrimaryActivity.addClusteredMarkers()

/**
* Adds markers to the map with clustering support.
*/
private fun addClusteredMarkers(googleMap: GoogleMap) {
   // Create the ClusterManager class and set the custom renderer.
   val clusterManager = ClusterManager<Place>(this, googleMap)
   clusterManager.renderer =
       PlaceRenderer(
           this,
           googleMap,
           clusterManager
       )

   // Set custom info window adapter
   clusterManager.markerCollection.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))

   // Add the places to the ClusterManager.
   clusterManager.addItems(places)
   clusterManager.cluster()

   // Set ClusterManager as the OnCameraIdleListener so that it
   // can re-cluster when zooming in and out.
   googleMap.setOnCameraIdleListener {
       clusterManager.onCameraIdle()
   }
}

שיטה זו יוצרת ClusterManager, מעבירה אליה את כלי הרינדור המותאם אישית PlacesRenderer, מוסיפה את כל המקומות ומפעילה את שיטת cluster(). בנוסף, מאחר ש-ClusterManager משתמש בשיטה setInfoWindowAdapter() באובייקט המפה, יש להגדיר את חלון המידע המותאם אישית על אובייקט ClusterManager.markerCollection. לסיום, מאחר שברצונך שאשכול ישתנה כאשר המשתמש תזיז את המפה ומגדילים את הזום, המפה OnCameraIdleListener ניתנת ל-googleMap, למשל כשהמצלמה לא פעילה, clusterManager.onCameraIdle() מופעל.

  1. כדאי להריץ את האפליקציה כדי לראות את החנויות החדשות שמקובצות כאן.

9. ציור על המפה

כבר קראת דרך אחת לשרטט על המפה (באמצעות הוספת סמנים), וה-SDK של מפות Google ל-Android תומך בדרכים רבות נוספות לציור, כדי להציג מידע שימושי במפה.

לדוגמה, אם אתם רוצים לייצג מסלולים ואזורים במפה, אתם יכולים להשתמש במצולעים ובפוליגונים כדי להציג אותם במפה. לחלופין, אם אתם רוצים לתקן תמונה על פני השטח, תוכלו להשתמש בשכבות-על של קרקע.

במשימה הזו לומדים לשרטט צורות, במיוחד עיגול, מסביב לסמן בכל פעם שמקישים עליו.

f98ce13055430352.png

הוספת event listener

בדרך כלל, הדרך שבה מוסיפים פונקציות למעקב אחר קליקים לסמן היא העברת האזנה לקליק ישירות על אובייקט GoogleMap דרך setOnMarkerClickListener(). עם זאת, מאחר שאתם משתמשים באשכולות, עליכם לספק את הפונקציה האזנה של קליקים כ-ClusterManager במקום זאת.

  1. בשיטת addClusteredMarkers() ב-MainActivity, הוסף את השורה הבאה מיד לאחר ההפעלה אל cluster().

PrimaryActivity.addClusteredMarkers()

// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
   addCircle(googleMap, item)
   return@setOnClusterItemClickListener false
}

השיטה הזו מוסיפה event listener ומפעילה את השיטה addCircle(), שאותה מגדירים בשלב הבא. לסיום, false מוחזר מהשיטה הזו כדי לציין ששיטה זו לא צרפה את האירוע הזה.

  1. לאחר מכן, עליך להגדיר את הנכס circle ואת השיטה addCircle() ב-MainActivity.

PrimaryActivity.addמעגל()

private var circle: Circle? = null

/**
* Adds a [Circle] around the provided [item]
*/
private fun addCircle(googleMap: GoogleMap, item: Place) {
   circle?.remove()
   circle = googleMap.addCircle(
       CircleOptions()
           .center(item.latLng)
           .radius(1000.0)
           .fillColor(ContextCompat.getColor(this, R.color.colorPrimaryTranslucent))
           .strokeColor(ContextCompat.getColor(this, R.color.colorPrimary))
   )
}

הנכס circle מוגדר כך שבכל פעם שמקישים על סמן חדש, המעגל הקודם מוסר ומתווסף מעגל חדש. שים לב שה-API להוספת מעגל דומה מאוד להוספת סמן.

  1. קדימה, אפשר להריץ את האפליקציה כדי לראות את השינויים.

10. בקרת מצלמות

המשימה האחרונה היא לבדוק כמה בקרות מצלמה כדי שתוכלו להתמקד בתצוגה סביב אזור מסוים.

מצלמה ותצוגה

אם הבחנתם בהפעלת האפליקציה, המצלמה מציגה את יבשת אפריקה, ואתם צריכים להזיז ולהתקרבות לסן פרנסיסקו כדי למצוא את הסמנים שהוספתם. אמנם זו דרך מהנה לחקור את העולם, אבל היא לא שימושית אם רוצים להציג את הסמנים מיד.

כדי לעשות זאת, אתם יכולים להגדיר את המיקום של המצלמה באופן פרוגרמטי כך שהתצוגה תמוקם במקום הרצוי.

  1. צריך להוסיף את הקוד הבא לשיחה של getMapAsync() כדי לכוונן את תצוגת המצלמה כך שתופעל בסן פרנסיסקו בזמן הפעלת האפליקציה.

PrimaryActivity.onCreate()

mapFragment?.getMapAsync { googleMap ->
   // Ensure all places are visible in the map.
   googleMap.setOnMapLoadedCallback {
       val bounds = LatLngBounds.builder()
       places.forEach { bounds.include(it.latLng) }
       googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))
   }
}

קודם כל, המערכת שולחת קריאה אל setOnMapLoadedCallback() כך שעדכון המצלמה מתבצע רק אחרי שהמפה נטענת. שלב זה נדרש, מאחר שצריך לחשב את מאפייני המפה, כמו מאפיינים, לפני ביצוע קריאה לעדכון מצלמה.

בלאמבה נבנה אובייקט LatLngBounds חדש, המגדיר אזור מלבני במפה. כדי לעשות זאת, אנחנו כוללים את כל הערכים של LatLng במקום הזה, כדי להבטיח שכל המקומות נמצאים בתוך הגבול. לאחר יצירת האובייקט הזה, מתבצעת הפעלה של השיטה moveCamera() ב-GoogleMap ומסופקת לו CameraUpdate באמצעות CameraUpdateFactory.newLatLngBounds(bounds.build(), 20).

  1. מפעילים את האפליקציה ושימו לב שהמצלמה מאותחלת בסן פרנסיסקו.

האזנה לשינויים במצלמה

בנוסף לשינוי מיקום המצלמה, ניתן גם להאזין לעדכוני מצלמה כשהמשתמש זז במפה. אפשרות זו שימושית אם אתם רוצים לשנות את ממשק המשתמש בזמן שהמצלמה זזה.

בשביל הכיף, אתם משנים את הקוד כדי להפוך את הסמנים למעוררים בכל פעם שהמצלמה זזה.

  1. בשיטה addClusteredMarkers(), מוסיפים את השורות הבאות לתחתית השיטה:

PrimaryActivity.addClusteredMarkers()

// When the camera starts moving, change the alpha value of the marker to translucent.
googleMap.setOnCameraMoveStartedListener {
   clusterManager.markerCollection.markers.forEach { it.alpha = 0.3f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 0.3f }
}

הפעולה הזו מוסיפה OnCameraMoveStartedListener כך שבכל פעם שהמצלמה מתחילה לנוע, כל הסמנים' (האשכולות והסמנים) משתנים לאלפא 0.3f כך שהסמנים ייראו שקופים.

  1. לבסוף, כדי לשנות את הסימונים השקופים חזרה לאטומים כאשר המצלמה מפסיקה, יש לשנות את תוכן ה-setOnCameraIdleListener בשיטה addClusteredMarkers() כך:

PrimaryActivity.addClusteredMarkers()

googleMap.setOnCameraIdleListener {
   // When the camera stops moving, change the alpha value back to opaque.
   clusterManager.markerCollection.markers.forEach { it.alpha = 1.0f }
   clusterManager.clusterMarkerCollection.markers.forEach { it.alpha = 1.0f }

   // Call clusterManager.onCameraIdle() when the camera stops moving so that reclustering
   // can be performed when the camera stops moving.
   clusterManager.onCameraIdle()
}
  1. קדימה, כדאי להריץ את האפליקציה כדי לראות את התוצאות!

11. KTX של מפות Google

באפליקציות של Kotlin הכוללות SDK אחד או יותר של Android Maps Platform, ספריות Kotlin או KTX זמינות כדי לאפשר לך להשתמש בתכונות השפה של Kotlin, כמו שגרות, תכונות/פונקציות של תוספים ועוד. לכל SDK של מפות Google יש ספריית KTX תואמת, כפי שמוצג בהמשך:

תרשים KTX של מפות Google

במשימה הזו, תשתמשו בספריות KTX של מפות Google וב'מפות' מסוג KTX של Maps U

  1. הכללת תלויות KTX בקובץ build.gradle ברמת האפליקציה

האפליקציה משתמשת ב-SDK של מפות ל-Android וגם ב-SDK של מפות Google ל-Android Utility Directory, ולכן צריך לכלול את ספריות KTX התואמות לספריות האלה. במשימה הזו תשתמשו גם בתכונה שנמצאת בספריית ה-KTX של מחזור החיים של Android, כך שהתלויות גם בקובץ build.gradle ברמת האפליקציה.

build.gradle

dependencies {
    // ...

    // Maps SDK for Android KTX Library
    implementation 'com.google.maps.android:maps-ktx:3.0.0'

    // Maps SDK for Android Utility Library KTX Library
    implementation 'com.google.maps.android:maps-utils-ktx:3.0.0'

    // Lifecycle Runtime KTX Library
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}
  1. שימוש בפונקציות של התוספים מסוג Googlemap.addMarker() ו-Googlemap.add המעגל()

ספריית KTX במפות מספקת חלופה לממשק ה-API בסגנון DSL עבור GoogleMap.addMarker(MarkerOptions) ו-GoogleMap.addCircle(CircleOptions) ששימשו בשלבים הקודמים. כדי להשתמש בממשקי ה-API שהוזכרו, יש צורך בבנייה של כיתה שמכילה אפשרויות עבור סמן או עיגול, ואילו באמצעות חלופות ה-KTX אפשר להגדיר את אפשרויות הסמן או העיגול במעבדה שאתם מספקים.

כדי להשתמש בממשקי ה-API האלה, יש לעדכן את השיטות MainActivity.addMarkers(GoogleMap) ו-MainActivity.addCircle(GoogleMap):

PrimaryActivity.addMarkers(GoogleMaps)

/**
 * Adds markers to the map. These markers won't be clustered.
 */
private fun addMarkers(googleMap: GoogleMap) {
    places.forEach { place ->
        val marker = googleMap.addMarker {
            title(place.name)
            position(place.latLng)
            icon(bicycleIcon)
        }
        // Set place as the tag on the marker object so it can be referenced within
        // MarkerInfoWindowAdapter
        marker.tag = place
    }
}

PrimaryActivity.addמעגל(Googleמפת)

/**
 * Adds a [Circle] around the provided [item]
 */
private fun addCircle(googleMap: GoogleMap, item: Place) {
    circle?.remove()
    circle = googleMap.addCircle {
        center(item.latLng)
        radius(1000.0)
        fillColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimaryTranslucent))
        strokeColor(ContextCompat.getColor(this@MainActivity, R.color.colorPrimary))
    }
}

כתיבת התסריט של השיטות שלמעלה באופן הרבה יותר תמציתי לקריאה, באמצעות הפונקציות ליטרליות של Kotlin.

  1. שימוש בפונקציות ההשעיה של התמיכה ב-SupportMapsFragment.aPendingmap() וב-GoogleMaps.aPendingmapLoad()

ספריית KTX במפות מספקת גם השעיה של תוספי פונקציות לשימוש בשגרות. באופן ספציפי, יש חלופות חלופיות ל-SupportMapFragment.getMapAsync(OnMapReadyCallback) ול-GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback). השימוש בממשקי ה-API החלופיים האלה מבטל את הצורך להעביר קריאה חוזרת (callback) ובמקום זאת מאפשר לכם לקבל את התגובה לשיטות אלה באופן סידורי וסינכרוני.

השיטות האלה משעות פונקציות, ולכן השימוש בהן חייב להתבצע בתוך שגרת העבודה. בספרייה Time התוכן Runtime KTX יש הרחבה שמאפשרת לספק היקפי קורקוטין המבוססים על מחזור חיים, כך ששגרות טריגר יופעלו ותיעצר באירוע המתאים של מחזור החיים.

המערכת משלבת את הקונספטים האלה כדי לעדכן את שיטת MainActivity.onCreate(Bundle):

PrimaryActivity.onCreate(Bundle)

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    val mapFragment =
        supportFragmentManager.findFragmentById(R.id.map_fragment) as SupportMapFragment
    lifecycleScope.launchWhenCreated {
        // Get map
        val googleMap = mapFragment.awaitMap()

        // Wait for map to finish loading
        googleMap.awaitMapLoad()

        // Ensure all places are visible in the map
        val bounds = LatLngBounds.builder()
        places.forEach { bounds.include(it.latLng) }
        googleMap.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds.build(), 20))

        addClusteredMarkers(googleMap)
    }
}

ההיקף הנוכחי של lifecycleScope.launchWhenCreated יבצע את החסימה אם הפעילות היא לפחות במצב שנוצר. כמו כן, לידיעתך, הקריאות לאחזור של האובייקט GoogleMap והמתנה לסיום הטעינה של המפה הוחלפו ב-SupportMapFragment.awaitMap() וב-GoogleMap.awaitMapLoad(), בהתאמה. שילוב מחדש של הקוד באמצעות פונקציות ההשעיה האלה מאפשר לכם לכתוב את הקוד המקביל המבוסס על קריאה חוזרת (callback) באופן רציף.

  1. הגיע הזמן לבנות את האפליקציה מחדש עם השינויים החדשים.

12. מזל טוב

מעולה! פרסמתם הרבה תכנים ואנחנו מקווים שהבנתם טוב יותר את התכונות העיקריות המוצעות ב-SDK של מפות Google ל-Android.

מידע נוסף

  • places SDK ל-Android – באמצעותה תוכלו למצוא את מגוון המקומות העשירים במידע על עסקים בסביבתכם.
  • android-maps-ktx – ספריית קוד פתוח שמאפשרת לשלב את ה-SDK של מפות Google ל-Android וב-SDK ל-Android SDK ל-Android Utility, בדרך ידידותית למשתמשים.
  • android-place-ktx – ספריית קוד פתוח המאפשרת שילוב עם SDK של מקומות ל-Android בצורה ידידותית ל-Kotlin.
  • android-samples – קוד לדוגמה ב-GitHub שמדגים את כל התכונות הכלולות ב-Codelab הזה ועוד.
  • פעולות קוד קוטלין נוספות לבניית אפליקציות ל-Android באמצעות פלטפורמת מפות Google