1. 事前準備
本程式碼研究室可教導您如何整合 Maps SDK for Android 與自己的應用程式,以及如何使用 Google Play 核心功能,建立應用程式在美國加州舊金山的自行車店地圖。
必要條件
- Kotlin 和 Android 開發的基本知識
執行步驟
- 啟用並使用 Maps SDK for Android,將 Google 地圖新增至 Android 應用程式。
- 新增、自訂和叢集標記。
- 在地圖上繪製折線和多邊形。
- 透過程式輔助方式控制攝影機的視角。
軟硬體需求
- Maps SDK for Android
- 已啟用帳單功能的 Google 帳戶
- Android Studio 2020.3.1 以上版本
- 在 Android Studio 上安裝的 Google Play 服務
- 搭載 Android 4.2.2 以上版本的 Google API 平台的 Android 裝置或 Android Emulator (如需安裝步驟說明,請參閱在 Android Emulator 上執行應用程式)。
2. 做好準備
您在以下啟用步驟中需要啟用 Maps SDK for Android。
設定 Google 地圖平台
如果您還沒有 Google Cloud Platform 帳戶和已啟用計費功能的專案,請參閱開始使用 Google 地圖平台指南,建立帳單帳戶和專案。
- 在 Cloud Console 中按一下專案下拉式選單,然後選取您要用於這個程式碼研究室的專案。
3. 快速入門
以下提供一些入門程式碼,協助您快速上手,幫助您快速上手。我們決定直接跳到解決方案,但如果您想依照自己的所有步驟逐步進行,請繼續閱讀本文。
- 如果您已安裝
git
,請複製存放區。
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
或者,您也可以點擊下方按鈕來下載原始碼。
- 取得程式碼後,請在 Android Studio 中開啟
starter
目錄中找到的專案。
4. 新增 Google 地圖
在本節中,您將新增「Google 地圖」,以便在您啟動應用程式時載入。
新增 API 金鑰
您在先前步驟中建立的 API 金鑰必須提供給應用程式,以便 Maps SDK for Android 將您的金鑰與應用程式建立關聯。
- 如要提供這項資訊,請在專案的根目錄開啟名為
local.properties
的檔案 (與gradle.properties
和settings.gradle
相同層級)。 - 在該檔案中,定義新金鑰
GOOGLE_MAPS_API_KEY
,其值即為您建立的 API 金鑰。
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
請注意,local.properties
會列在 Git 存放區的 .gitignore
檔案中。這是因為系統會將您的 API 金鑰視為機密資訊,因此不可在原始碼中登錄資訊。
- 接下來,如要讓 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 地圖依附元件
- 現在,您可以在應用程式內存取 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'
}
- 接著,在
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}" />
- 接著,在
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
物件。
- 最後,加入下列程式碼來覆寫
onCreate
方法,以便更新app/src/main/java/com/google/codelabs/buildyourfirstmap
中的MainActivity
類別,以便使用剛建立的新版面配置來設定其內容。
主要活動
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- 現在,請直接執行應用程式。你現在應該能在裝置的螢幕上看到地圖載入量。
5. 雲端式地圖樣式設定 (選用)
您可以使用雲端式地圖樣式設定功能自訂地圖樣式。
建立地圖 ID
如果您尚未建立具有相關地圖樣式的地圖 ID,請參閱地圖 ID 指南,完成以下步驟:
- 建立地圖 ID。
- 將地圖 ID 與地圖樣式建立關聯。
在應用程式中加入地圖 ID
如要使用您建立的地圖 ID,請修改 activity_main.xml
檔案,並將地圖 ID 傳送至 SupportMapFragment
的 map: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. 新增標記
在這項工作中,您可以在地圖上新增標記,代表要在地圖上醒目顯示的搜尋點。首先,請擷取在新手專案中提供的地點清單,然後將這些地點加進地圖。這裡是單車店。
取得 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
)
新增標記至地圖
現在地點清單已載入至記憶體,接下來請在地圖上代表這些地點。
- 在
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
物件執行個體化以建立標記,即可自訂標記。在此情況下,系統會提供標記的標題和位置,這就代表自行車店的名稱及座標。
- 啟動應用程式後,前往舊金山看看您剛剛新增的標記!
7. 自訂標記
您可以根據自己新增的標記,透過多種自訂選項加以突顯,並為使用者提供實用的資訊。在這項工作中,您可以自訂每個標記的圖片以及輕觸標記時所顯示的資訊視窗,藉此探索其中部分內容。
新增資訊視窗
根據預設,輕觸標記時,資訊視窗會顯示其標題和文字片段 (如有設定)。您可以自訂這項設定,讓網站顯示其他資訊,例如地點的地址和評分。
建立 Marker_info_contents.xml
首先,請建立名稱為「marker_info_contents.xml
」的新版面配置檔案,
- 方法是在 Android Studio 的專案檢視模式中,按一下滑鼠右鍵
app/src/main/res/layout
,然後依序選取 [新增] > [版面配置資源檔案]。
- 在對話方塊中,在 [File name] (檔案名稱) 欄位中輸入
marker_info_contents
,在Root element
欄位輸入LinearLayout
,然後按一下 [OK] (確定)。
這個版面配置檔案之後會隨即展開,代表資訊視窗中的內容。
- 複製下列程式碼片段中的內容,這會在類別
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()
中傳回空值,這表示應該使用預設視窗。
- 在 Android Studio 專案檢視中的
app/src/main/java/com/google/codelabs/buildyourfirstmap
資料夾上按一下滑鼠右鍵,然後選取 [新增] > [Kotlin 檔案/類別],在相同套件中建立名為MarkerInfoWindowAdapter
的新 Kotlin 檔案。
- 在對話方塊中輸入
MarkerInfoWindowAdapter
,並標明 [檔案]。
- 建立檔案後,請將以下程式碼片段的內容複製到新檔案。
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
的新執行個體。
- 請在
getMapAsync()
lambda 內的addMarkers()
方法呼叫之後加入下列程式碼,以完成此操作。
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
最後,您必須為每個地點加入標記,將每個地點都設定為標記屬性。
- 如要這麼做,請使用下列指令修改
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
。
在標記上設定自訂點陣圖
使用向量可繪製單車圖示,下一步就是將可繪畫設定為各標記 (#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
。
- 使用此屬性,請在
addMarkers()
方法中叫用MarkerOptions
的icon
方法以完成圖示自訂作業。讓標記屬性看起來像這樣:
MainActivity.addMarkers()
val marker = googleMap.addMarker(
MarkerOptions()
.title(place.name)
.position(place.latLng)
.icon(bicycleIcon)
)
- 執行應用程式以查看更新後的標記!
8. 叢集標記
視您在地圖上放大的程度而定,您注意到新增的標記可能會重疊。重疊的標記很難與許多使用者互動,因而產生大量雜訊,進而影響應用程式的可用性。
為了改善使用者體驗,當您擁有大量叢集,並根據該標準進行標記叢集的最佳做法時,這是最佳作法。透過分群法,放大和縮小地圖時,鄰近的標記會集中在一起,如下所示:
為此,您需要 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
檔案中新增其他依附元件。
- 請直接更新
app/build.gradle
檔案的dependencies
部分。
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- 新增這一行後,您必須執行專案同步處理來擷取新的依附元件。
實作分群法
如要在應用程式中實作叢集,請按照以下三個步驟操作:
- 導入
ClusterItem
介面。 DefaultClusterRenderer
類別的子類別。- 建立
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()
,因此您必須在 ClusterManger
的 MarkerManager.Collection
物件上設定自訂資訊視窗。
- 若要開始,請在
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))
}
- 接著,在
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()
。
- 立即執行應用程式,即可瀏覽全新的分店!
9. 在地圖上繪圖
您已經探索了在地圖上繪製標記的方法 (透過新增標記),但 Maps SDK for Android 支援數種其他繪圖方式,讓您在地圖上顯示實用資訊。
舉例來說,如果您想在地圖上顯示路線和區域,可以使用折線和多邊形在地圖上顯示這些地點和多邊形。或者,如果您想將圖片固定在地面上,可以使用區域疊加層。
在這項工作中,您會學習如何在輕觸標記時畫出形狀 (尤其是圓圈)。
新增點擊接聽器
一般來說,如要將點擊事件監聽器新增至標記的方式,就是透過 setOnMarkerClickListener()
直接在 GoogleMap
物件上傳遞點擊接聽器。不過,因為您正在使用分群法,因此必須將點擊接聽器改為 ClusterManager
。
- 在
MainActivity
的addClusteredMarkers()
方法中,請直接在叫用後,將以下這一行加入cluster()
。
MainActivity.addClusteredMarkers()
// Show polygon
clusterManager.setOnClusterItemClickListener { item ->
addCircle(googleMap, item)
return@setOnClusterItemClickListener false
}
此方法會新增事件監聽器並叫用 addCircle()
方法,也就是您接下來定義的方法。最後,此方法會傳回 false
,表示此方法尚未使用這個事件。
- 接著,您必須在
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 與新增標記十分類似。
- 立即執行應用程式,看看相關變更。
10. 相機控制
在您完成最後一項工作後,我們會提供部分相機控制項,讓您將焦點對準特定區域。
相機和檢視畫面
如果在執行應用程式時發現,相機顯示非洲的大陸,您必須巧妙地平移和縮放至舊金山,找到您新增的標記。雖然這是探索世界的絕佳方式,但如果您想要立即顯示標記,這並不方便。
為此,您可以程式輔助的方式設定攝影機的位置,讓檢視的目標位置保持在畫面中央。
- 請直接在
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
提供給該物件。
- 執行應用程式,並發現相機已經於舊金山初始化。
監聽相機變更
除了修改相機位置以外,您還可以在使用者移動地圖時監聽攝影機更新。如果您想在相機移動時修改使用者介面,這個功能就能派上用場。
為方便起見,您可以修改程式碼讓標記在移動時移動為半透明。
- 在
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
,如此一來,標記才會進入半透明。
- 最後,如要在相機停止時將半透明標記修改成不透明,請將
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()
}
- 直接執行應用程式即可查看結果!
11. Google 地圖 KTX
針對使用一或多個 Google 地圖平台 Android SDK 的 Kotlin 應用程式,您可以使用 Kotlin 擴充功能或 KTX 程式庫來運用 Kotlin 語言功能,例如協同程式、擴充功能屬性/函式等等。每個 Google Maps SDK 都有對應的 KTX 程式庫,如下所示:
在這項工作中,您會在應用程式中使用 Maps KTX 和 Maps Utils KTX 程式庫,並重構先前的工作' 實作,以便在應用程式中使用 Kotlin 專屬語言功能。
- 在應用程式層級的 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'
}
- 使用 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 的函式常值和接收器來完成。
- 使用 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()
。使用這些暫停函式重構程式碼可讓您以循序方式編寫對等的回呼回呼程式碼。
- 請直接使用重組變更來重新建構應用程式!
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 應用程式