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

1. 事前準備

本程式碼研究室可教導您如何整合 Maps SDK for Android 與自己的應用程式,以及如何使用 Google Play 核心功能,建立應用程式在美國加州舊金山的自行車店地圖。

f05e1ca27ff42bf6.png

必要條件

  • Kotlin 和 Android 開發的基本知識

執行步驟

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

軟硬體需求

2. 做好準備

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

設定 Google 地圖平台

如果您還沒有 Google Cloud Platform 帳戶和已啟用計費功能的專案,請參閱開始使用 Google 地圖平台指南,建立帳單帳戶和專案。

  1. Cloud Console 中按一下專案下拉式選單,然後選取您要用於這個程式碼研究室的專案。

  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

請注意,local.properties 會列在 Git 存放區的 .gitignore 檔案中。這是因為系統會將您的 API 金鑰視為機密資訊,因此不可在原始碼中登錄資訊。

  1. 接下來,如要讓 API 在整個應用程式中使用,請在應用程式的 app/ 目錄中加入 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 金鑰,下一步就是將 Maps SDK for 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. 接著,在 AndroidManifest.xml 中新增 meta-data 標記,以傳入您在先前步驟中建立的 API 金鑰。如要這麼做,請在 Android Studio 中開啟這個檔案,然後在「AndroidManifest.xml」檔案 (位於 app/src/main) 中的「application」物件中加入以下 meta-data 標記。

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. 最後,加入下列程式碼來覆寫 onCreate 方法,以便更新 app/src/main/java/com/google/codelabs/buildyourfirstmap 中的 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 檔案,並將地圖 ID 傳送至 SupportMapFragmentmap:mapId 屬性。

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 類別。

Places Reader

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,然後依序選取 [新增] > [版面配置資源檔案]

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 物件,其中 / 物件會用來自訂視窗本身,而後者則可用來自訂其內容。就您的案例而言,您實作了一種自訂功能,並且自訂 getInfoContents() 的傳回結果,但在 getInfoWindow() 中傳回空值,這表示應該使用預設視窗。

  1. 在 Android Studio 專案檢視中的 app/src/main/java/com/google/codelabs/buildyourfirstmap 資料夾上按一下滑鼠右鍵,然後選取 [新增] > [Kotlin 檔案/類別],在相同套件中建立名為 MarkerInfoWindowAdapter 的新 Kotlin 檔案。

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() 方法的內容中,此方法提供的標記會轉換為 Place 類型;如果無法使用投放功能,則方法會傳回 null (您尚未在 Marker 上設定標記屬性,但在下一個步驟中會這麼做)。

接著,將 marker_info_contents.xml 版面配置加起來,接著將含有 TextViews 的文字設為 Place 標記。

更新 MainActivity

如要為目前已建立的所有元件加上黏附度,您必須在 MainActivity 類別中加入兩行。

首先,如要在 getMapAsync 方法呼叫內傳遞自訂 InfoWindowAdapter MarkerInfoWindowAdapter,請在 GoogleMap 物件上叫用 setInfoWindowAdapter() 方法,並建立 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

在標記上設定自訂點陣圖

使用向量可繪製單車圖示,下一步就是將可繪畫設定為各標記 (#39; ),在地圖上顯示。MarkerOptions 包含 icon 方法,這個函式需要您在 BitmapDescriptor 中完成這項作業。

首先,您必須將您剛新增的向量可繪項目轉換為 BitmapDescriptor。入門專案中名為 BitMapHelper 的檔案包含名為 vectorToBitmap() 的輔助函式。

點陣圖輔助程式

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 類別來處理叢集,而且您可在瀏覽地圖時縮放地圖。根據預設,預設會導入 DefaultClusterRenderer,這個工具會執行 ClusterRenderer。以簡單的情況來說,這應該已足夠。不過,由於標記需要自訂,因此您需要擴充這個類別,並在其中加入自訂項目。

請直接在 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 物件上設定自訂資訊視窗。最後,由於叢集會在使用者平移及縮放地圖時變更叢集,因此系統會將 OnCameraIdleListener 提供給 googleMap,如此一來,當攝影機進入閒置狀態時,系統就會叫用 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 與新增標記十分類似。

  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. 執行應用程式,並發現相機已經於舊金山初始化。

監聽相機變更

除了修改相機位置以外,您還可以在使用者移動地圖時監聽攝影機更新。如果您想在相機移動時修改使用者介面,這個功能就能派上用場。

為方便起見,您可以修改程式碼讓標記在移動時移動為半透明。

  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 和 標記) 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. Google 地圖 KTX

針對使用一或多個 Google 地圖平台 Android SDK 的 Kotlin 應用程式,您可以使用 Kotlin 擴充功能或 KTX 程式庫來運用 Kotlin 語言功能,例如協同程式、擴充功能屬性/函式等等。每個 Google Maps 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 即可省去傳遞回呼的麻煩,並且能夠以連續且同步的方式接收這些方法的回應。

這些方法屬於暫停函式,因此必須在協同程式內使用。Lifecycle Runtime 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 應用程式