在 Android 應用程式中加入地圖 (Kotlin)

1. 事前準備

本程式碼研究室會教您如何整合 Maps SDK for Android 與應用程式,並使用其核心功能,建構可顯示美國加州舊金山自行車店地圖的應用程式。

f05e1ca27ff42bf6.png

必要條件

  • 具備 Kotlin 和 Android 開發作業的基本知識

學習內容

  • 啟用並使用 Maps SDK for Android,在 Android 應用程式中加入 Google 地圖。
  • 新增、自訂及叢集標記。
  • 在地圖上繪製折線和多邊形。
  • 以程式輔助方式控制攝影機的視點。

軟硬體需求

2. 做好準備

在啟用步驟中,您需要啟用 Maps SDK for Android

設定 Google 地圖平台

如果您尚未建立 Google Cloud Platform 帳戶,以及啟用計費功能的專案,請參閱「開始使用 Google 地圖平台」指南,建立帳單帳戶和專案。

  1. Cloud 控制台中,按一下專案下拉式選單,然後選取要用於本程式碼研究室的專案。

  1. Google Cloud Marketplace 中,啟用本程式碼研究室所需的 Google 地圖平台 API 和 SDK。如要瞭解如何操作,請觀看這部影片或參閱這份說明文件
  2. 在 Cloud Console 的「憑證」頁面中產生 API 金鑰。你可以按照這部影片這份文件中的步驟操作。所有 Google 地圖平台要求都需要 API 金鑰。

3. 快速入門

為協助您盡快上手,我們提供一些範例程式碼,方便您跟著本程式碼研究室的說明操作。歡迎直接前往解決方案,但如果您想按照所有步驟自行建構,請繼續閱讀。

  1. 如果已安裝 git,請複製存放區。
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

或者,您也可以點選下列按鈕下載原始碼。

  1. 取得程式碼後,請在 Android Studio 中開啟 starter 目錄內的專案。

4. 新增 Google 地圖

在本節中,您將新增 Google 地圖,以便在啟動應用程式時載入地圖。

d1d068b5d4ae38b9.png

新增 API 金鑰

您必須將先前步驟中建立的 API 金鑰提供給應用程式,這樣 Maps SDK for Android 才能將金鑰與應用程式建立關聯。

  1. 如要提供這項資訊,請開啟專案根目錄中的 local.properties 檔案 (與 gradle.propertiessettings.gradle 位於同一層級)。
  2. 在該檔案中,定義新的金鑰 GOOGLE_MAPS_API_KEY,並將值設為您建立的 API 金鑰。

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

請注意,Git 存放區的 .gitignore 檔案中會列出 local.properties。這是因為 API 金鑰屬於機密資訊,盡可能不要簽入原始碼控管。

  1. 接下來,如要公開 API 以供整個應用程式使用,請在 app/ 目錄的應用程式 build.gradle 檔案中加入 Secrets Gradle Plugin for Android 外掛程式,並在 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 檔案中定義的金鑰,當成 Android 資訊清單檔案中的建構變數,以及 Gradle 產生的 BuildConfig 類別中的變數。使用這個外掛程式可移除原本需要用來從 local.properties 讀取屬性的樣板程式碼,讓應用程式各處都能存取屬性。

新增 Google 地圖依附元件

  1. 現在應用程式可以存取 API 金鑰,下一步是在應用程式的 build.gradle 檔案中新增 Maps SDK for Android 依附元件。

在本程式碼研究室隨附的起始專案中,這項依附元件已新增完畢。

build.gradle

dependencies {
   // Dependency to include Maps SDK for Android
   implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
  1. 接下來,請在 AndroidManifest.xml 中新增 meta-data 標記,傳遞您在先前步驟中建立的 API 金鑰。如要這麼做,請在 Android Studio 中開啟這個檔案,然後在 app/src/main 中的 AndroidManifest.xml 檔案內,將下列 meta-data 標記加進 application 物件。

AndroidManifest.xml

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="${GOOGLE_MAPS_API_KEY}" />
  1. 接著,在 app/src/main/res/layout/ 目錄中建立名為 activity_main.xml 的新版面配置檔案,並定義如下:

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. 最後,請更新 app/src/main/java/com/google/codelabs/buildyourfirstmap 中的 MainActivity 類別,加入下列程式碼來覆寫 onCreate 方法,以便使用您剛建立的新版面配置設定其內容。

MainActivity

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)
}
  1. 現在請執行應用程式,裝置螢幕上應該會載入地圖。

5. 雲端式地圖樣式設定 (選用)

您可以使用雲端式地圖樣式設定自訂地圖樣式。

建立地圖 ID

如果尚未建立地圖 ID 並與地圖樣式建立關聯,請參閱「地圖 ID」指南,完成下列步驟:

  1. 建立地圖 ID。
  2. 將地圖 ID 與地圖樣式建立關聯。

在應用程式中加入地圖 ID

如要使用您建立的地圖 ID,請修改 activity_main.xml 檔案,並在 SupportMapFragmentmap:mapId 屬性中傳遞地圖 ID。

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

取得 GoogleMap 的參照

首先,您需要取得 GoogleMap 物件的參照,才能使用其方法。如要這麼做,請在呼叫 setContentView() 後,於 MainActivity.onCreate() 方法中加入下列程式碼:

MainActivity.onCreate()

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

實作方式是先使用 SupportFragmentManager 物件的 findFragmentById() 方法,找出您在上一個步驟中新增的 SupportMapFragment。取得參照後,系統會叫用 getMapAsync() 呼叫,然後傳遞 lambda。這個 lambda 會傳遞 GoogleMap 物件。在這個 lambda 內,系統會叫用 addMarkers() 方法呼叫,我們很快就會定義這個方法。

提供的類別:PlacesReader

在範例專案中,系統已為您提供 PlacesReader 類別。這個類別會讀取儲存在名為 places.json 的 JSON 檔案中的 49 個地點清單,並以 List<Place> 形式傳回。這些地點代表美國加州舊金山附近的自行車店清單。

如要瞭解這個類別的實作方式,請前往 GitHub 或在 Android Studio 中開啟 PlacesReader 類別。

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 的屬性,並定義如下:

MainActivity.places

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

這段程式碼會對 PlacesReader 叫用 read() 方法,並傳回 List<Place>Place 具有名為 name 的屬性 (地點名稱) 和 latLng (地點所在位置的座標)。

地點

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

在地圖中加入標記

現在地點清單已載入記憶體,下一步是在地圖上呈現這些地點。

  1. MainActivity 中建立名為 addMarkers() 的方法,並定義如下:

MainActivity.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 清單,然後在提供的 GoogleMap 物件上叫用 addMarker() 方法。標記是透過例項化 MarkerOptions 物件建立,可供您自訂標記本身。在本例中,系統會提供標記的名稱和位置,分別代表自行車店名稱和座標。

  1. 請執行應用程式,然後前往舊金山,查看您剛才新增的標記!

7. 自訂標記

您可以透過多種自訂選項,讓剛新增的標記更加顯眼,並向使用者傳達實用資訊。在這項工作中,您將自訂每個標記的圖片,以及輕觸標記時顯示的資訊視窗,進一步瞭解這些功能。

a26f82802fe838e9.png

新增資訊視窗

根據預設,輕觸標記時,資訊視窗會顯示標題和文字片段 (如有設定)。您可以自訂這項資訊,顯示地點的地址和評分等額外資訊。

建立 marker_info_contents.xml

首先,建立名為 marker_info_contents.xml 的新版面配置檔案。

  1. 方法是在 Android Studio 的專案檢視畫面中,對 app/src/main/res/layout 資料夾按一下滑鼠右鍵,然後依序選取「New」 >「Layout Resource File」

8cac51fcbef9171b.png

  1. 在對話方塊的「File name」(檔案名稱) 欄位中輸入 marker_info_contents,並在 Root element 欄位中輸入 LinearLayout,然後按一下「OK」(確定)

8783af12baf07a80.png

這個版面配置檔案稍後會膨脹,代表資訊視窗內的內容。

  1. 複製以下程式碼片段中的內容,在垂直 LinearLayout 檢視群組中新增三個 TextViews,然後覆寫檔案中的預設程式碼。

marker_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 的實作項目

為自訂資訊視窗建立版面配置檔案後,下一步就是實作 GoogleMap.InfoWindowAdapter 介面。這個介面包含 getInfoWindow()getInfoContents() 這兩種方法。這兩種方法都會傳回選用的 View 物件,前者用於自訂視窗本身,後者則用於自訂視窗內容。在您的情況下,您會同時實作這兩者,並在 getInfoWindow() 中傳回空值時,自訂 getInfoContents() 的傳回值,這表示應使用預設視窗。

  1. 在與 MainActivity 相同的套件中,建立名為 MarkerInfoWindowAdapter 的新 Kotlin 檔案。方法是在 Android Studio 的專案檢視畫面中,以滑鼠右鍵按一下 app/src/main/java/com/google/codelabs/buildyourfirstmap 資料夾,然後依序選取「New」 >「Kotlin File/Class」

3975ba36eba9f8e1.png

  1. 在對話方塊中輸入 MarkerInfoWindowAdapter,並保持「File」醒目顯示。

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() 方法的內容中,方法中提供的標記會轉換為 Place 型別,如果無法轉換,方法會傳回空值 (您尚未在 Marker 上設定標記屬性,但會在下一個步驟中執行此操作)。

接著,版面配置 marker_info_contents.xml 會膨脹,然後在包含 TextViewsPlace 標記上設定文字。

更新 MainActivity

如要將目前建立的所有元件黏合在一起,您需要在 MainActivity 類別中新增兩行。

首先,如要傳遞自訂 InfoWindowAdapter,請在 getMapAsync 方法呼叫中,對 GoogleMap 物件叫用 setInfoWindowAdapter() 方法,並建立 MarkerInfoWindowAdapter 的新例項。MarkerInfoWindowAdapter

  1. 方法是在 getMapAsync() lambda 內,於 addMarkers() 方法呼叫後新增下列程式碼。

MainActivity.onCreate()

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

最後,您需要在新增至地圖的每個標記上,將每個地點設為標記屬性。

  1. 為此,請將 addMarkers() 函式中的 places.forEach{} 呼叫修改為下列內容:

MainActivity.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
}

新增自訂標記圖片

自訂標記圖片是很有趣的方式,可讓地圖上的標記代表特定類型的地點。在這個步驟中,您會顯示單車,而非預設的紅色標記,代表地圖上的每間商店。入門專案的 app/src/res/drawable 中包含自行車圖示 ic_directions_bike_black_24dp.xml,您可以使用該圖示。

6eb7358bb61b0a88.png

在標記上設定自訂點陣圖

有了向量可繪項目自行車圖示,下一步就是將該可繪項目設為地圖上每個標記的圖示。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、可繪製資源 ID 和顏色整數,並建立 BitmapDescriptor 的表示法。

使用輔助方法宣告名為 bicycleIcon 的新屬性,並提供下列定義:MainActivity.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. 使用這個屬性,在 addMarkers() 方法中叫用 MarkerOptionsicon 方法,完成圖示自訂作業。完成後,標記屬性應如下所示:

MainActivity.addMarkers()

val marker = googleMap.addMarker(
    MarkerOptions()
        .title(place.name)
        .position(place.latLng)
        .icon(bicycleIcon)
)
  1. 執行應用程式,即可查看更新後的標記!

8. 叢集標記

您可能已發現,根據地圖的縮放程度,新增的標記可能會重疊。重疊的標記難以互動,且會產生大量干擾,影響應用程式的可用性。

68591edc86d73724.png

為改善這項功能的使用者體驗,建議您在有大量緊密叢集的資料集時,導入標記叢集。使用叢集功能時,當您縮放地圖,鄰近的標記會聚類在一起,如下所示:

f05e1ca27ff42bf6.png

如要實作這項功能,請使用 Maps SDK for Android 公用程式庫

Maps SDK for Android 公用程式庫

Maps SDK for Android 公用程式庫的用途是擴充 Maps SDK for Android 的功能。這項工具提供多項進階功能,例如標記叢集、熱度圖、KML 和 GeoJSON 支援、折線編碼和解碼,以及與球面幾何相關的輔助函式。

更新 build.gradle

公用程式庫與 Maps SDK for Android 是分開封裝,因此您需要在 build.gradle 檔案中新增其他依附元件。

  1. 請繼續更新 app/build.gradle 檔案的 dependencies 區段。

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
}

ClusterItem 定義了下列三種方法:

  • getPosition(),代表地點的LatLng
  • getTitle(),代表地點名稱
  • getSnippet(),代表地點的地址。

將 DefaultClusterRenderer 類別設為子類別

負責實作叢集功能的 ClusterManager 類別,會在您平移及縮放地圖時,於內部使用 ClusterRenderer 類別處理叢集建立作業。根據預設,它會隨附實作 ClusterRenderer 的預設轉譯器 DefaultClusterRenderer。在簡單的情況下,這應該就足夠了。不過,由於標記需要自訂,因此您必須擴充這個類別,並在其中新增自訂項目。

請在 com.google.codelabs.buildyourfirstmap.place 套件中建立 Kotlin 檔案 PlaceRenderer.kt,並定義如下:

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(),因此必須在 ClusterMangerMarkerManager.Collection 物件上設定自訂資訊視窗。

  1. 首先,請修改 MainActivity.onCreate()getMapAsync() 呼叫的 lambda 內容。請註解掉對 addMarkers()setInfoWindowAdapter() 的呼叫,然後改為叫用名為 addClusteredMarkers() 的方法,您將在下一個步驟中定義這個方法。

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. 接著在 MainActivity 中定義 addClusteredMarkers()

MainActivity.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 物件上設定自訂資訊視窗。最後,由於您希望叢集會隨著使用者平移和縮放地圖而變更,因此請為 googleMap 提供 OnCameraIdleListener,這樣攝影機閒置時就會叫用 clusterManager.onCameraIdle()

  1. 請執行應用程式,查看新的分群商店!

9. 在地圖上繪圖

您已瞭解在地圖上繪製內容的一種方式 (新增標記),但 Maps SDK for Android 支援多種其他繪製方式,可在地圖上顯示實用資訊。

舉例來說,如要在地圖上顯示路線和區域,可以使用折線和多邊形。如要將圖片固定在地面上,可以使用區域疊加層

在這項工作中,您將學習如何在輕觸標記時,在標記周圍繪製形狀 (具體來說是圓圈)。

f98ce13055430352.png

新增點擊事件監聽器

一般來說,如要將點擊事件監聽器新增至標記,請透過 setOnMarkerClickListener() 直接在 GoogleMap 物件中傳入點擊事件監聽器。不過,由於您使用叢集功能,因此點擊事件監聽器必須提供給 ClusterManager

  1. MainActivityaddClusteredMarkers() 方法中,請在呼叫 cluster() 後新增下列程式碼。

MainActivity.addClusteredMarkers()

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

這個方法會新增監聽器並叫用 addCircle() 方法,您將在下一個步驟中定義這個方法。最後,這個方法會傳回 false,表示這個方法尚未取用該事件。

  1. 接下來,您需要在 MainActivity 中定義屬性 circle 和方法 addCircle()

MainActivity.addCircle()

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 與新增標記的 API 非常相似。

  1. 現在請執行應用程式,查看變更。

10. 攝影機控制

最後一項工作是查看攝影機控制選項,以便將畫面焦點放在特定區域。

攝影機和檢視畫面

如果您在執行應用程式時發現,相機顯示的是非洲大陸,而且您必須費力平移和縮放才能找到新增的標記,雖然這是不錯的探索世界方式,但如果想立即顯示標記,就不適合使用這項功能。

為此,您可以透過程式輔助設定攝影機位置,將畫面中心對準所需位置。

  1. 請繼續將下列程式碼新增至 getMapAsync() 呼叫,調整攝影機檢視畫面,以便在應用程式啟動時初始化為舊金山。

MainActivity.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(),確保攝影機更新作業只會在載入地圖後執行。這是必要步驟,因為必須先計算地圖屬性 (例如維度),才能發出攝影機更新呼叫。

在 lambda 中,系統會建構新的 LatLngBounds 物件,定義地圖上的矩形區域。這會逐步建構,方法是納入所有地點的 LatLng 值,確保所有地點都在界線內。建構這個物件後,系統會叫用 GoogleMap 上的 moveCamera() 方法,並透過 CameraUpdateFactory.newLatLngBounds(bounds.build(), 20) 提供 CameraUpdate

  1. 執行應用程式,並注意攝影機現在已在舊金山初始化。

監聽攝影機變更

除了修改攝影機位置,您也可以在使用者移動地圖時,監聽攝影機更新。如果想在攝影機移動時修改 UI,這項功能就非常實用。

為了好玩,您修改了程式碼,讓標記在攝影機移動時變成半透明。

  1. addClusteredMarkers() 方法中,繼續在方法底部新增下列程式碼:

MainActivity.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,因此每當攝影機開始移動時,所有標記 (叢集和標記) 的 Alpha 值都會修改為 0.3f,讓標記呈現半透明狀態。

  1. 最後,如要讓半透明標記在攝影機停止時恢復不透明,請將 addClusteredMarkers() 方法中的 setOnCameraIdleListener 內容修改為下列內容:

MainActivity.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. Maps KTX

如果 Kotlin 應用程式使用一或多個 Google 地圖平台 Android SDK,您可以運用 Kotlin 擴充功能或 KTX 程式庫,充分利用 Kotlin 語言功能,例如協同程式、擴充屬性/函式等。每個 Google 地圖 SDK 都有對應的 KTX 程式庫,如下所示:

Google 地圖平台 KTX 圖表

在這項工作中,您會將 Maps KTX 和 Maps Utils KTX 程式庫新增至應用程式,並重構先前工作的實作項目,以便在應用程式中使用 Kotlin 專屬語言功能。

  1. 在應用程式層級的 build.gradle 檔案中加入 KTX 依附元件

由於應用程式同時使用 Maps SDK for Android 和 Maps SDK for Android 公用程式庫,因此您需要為這些程式庫加入對應的 KTX 程式庫。您也會在本工作中使用 AndroidX Lifecycle KTX 程式庫中的功能,因此請在應用程式層級的 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.addCircle() 擴充功能函式

Maps KTX 程式庫提供 DSL 樣式的 API,可替代先前步驟中使用的 GoogleMap.addMarker(MarkerOptions)GoogleMap.addCircle(CircleOptions)。如要使用上述 API,必須建構含有標記或圓圈選項的類別,但使用 KTX 替代方案時,您可以在提供的 lambda 中設定標記或圓圈選項。

如要使用這些 API,請更新 MainActivity.addMarkers(GoogleMap)MainActivity.addCircle(GoogleMap) 方法:

MainActivity.addMarkers(GoogleMap)

/**
 * 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
    }
}

MainActivity.addCircle(GoogleMap)

/**
 * 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. 使用 SupportMapFragment.awaitMap() 和 GoogleMap.awaitMapLoad() 擴充功能暫停函式

Maps KTX 程式庫也提供暫停函式擴充功能,可在協同程式中使用。具體來說,SupportMapFragment.getMapAsync(OnMapReadyCallback)GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback) 都有暫停函式替代方案。使用這些替代 API 時,您不必傳遞回呼,而是能以序列和同步方式接收這些方法的回應。

由於這些方法是暫停函式,因此必須在協同程式中使用。生命週期執行階段 KTX 程式庫提供擴充功能,可提供生命週期感知協同程式範圍,確保協同程式在適當的生命週期事件中執行及停止。

結合這些概念,更新 MainActivity.onCreate(Bundle) 方法:

MainActivity.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()。使用這些暫停函式重構程式碼,即可依序撰寫對等的以回呼為基礎的程式碼。

  1. 請使用重構的變更重新建構應用程式!

12. 恭喜

恭喜!您已瞭解許多內容,希望您對 Maps SDK for Android 提供的核心功能有更深入的認識。

瞭解詳情

  • Places SDK for Android:探索豐富的地點資料,發掘附近的商家。
  • android-maps-ktx:開放原始碼程式庫,可讓您以 Kotlin 友善的方式整合 Maps SDK for Android 和 Maps SDK for Android 公用程式庫。
  • android-place-ktx:開放原始碼程式庫,可讓您以 Kotlin 友善的方式整合 Places SDK for Android。
  • android-samples:GitHub 上的程式碼範例,說明本程式碼研究室涵蓋的所有功能,以及更多內容。
  • 更多 Kotlin 程式碼研究室,協助您使用 Google 地圖平台建構 Android 應用程式