1. 始める前に
この Codelab では、米国カリフォルニア州サンフランシスコにある自転車店の地図を表示するアプリを作成することで、Maps SDK for Android をアプリに統合する方法と、その主要な機能を使う方法について説明します。
Prerequisites
- 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 Maps Platform を設定する
課金を有効にした Google Cloud Platform アカウントとプロジェクトをまだ作成していない場合は、Google Maps Platform スタートガイドに沿って請求先アカウントとプロジェクトを作成してください。
- Cloud Console で、プロジェクトのプルダウン メニューをクリックし、この Codelab に使用するプロジェクトを選択します。
3. クイック スタート
できるだけ早く演習を開始できるように、この Codelab で使用できるスターター コードが用意されています。すぐに次のステップに進んでも問題ありませんが、ご自身で構築するためのすべての手順を確認したい場合は、最後までお読みください。
git
がインストールされている場合は、リポジトリのクローンを作成します。
git clone https://github.com/googlecodelabs/maps-platform-101-android.git
あるいは、以下のボタンをクリックしてソースコードをダウンロードすることもできます。
- コードを入手したら、Android Studio の
starter
ディレクトリにあるプロジェクトを開いてみましょう。
4. Google マップを追加する
このセクションでは、Google マップを追加して、アプリの起動時に読み込まれるようにします。
API キーを追加する
Maps SDK for Android で API キーがアプリに関連付けられるように、前の手順で作成した API キーをアプリに提供する必要があります。
- これを行うには、プロジェクトのルート ディレクトリ(
gradle.properties
とsettings.gradle
と同じレベル)でlocal.properties
というファイルを開きます。 - このファイルに、作成した API キーが値として指定された新しいキー(
GOOGLE_MAPS_API_KEY
)を定義します。
local.properties
GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE
local.properties
は、Git リポジトリの .gitignore
ファイルに含まれています。これは、API キーが機密情報と見なされ、ソース コントロールにはできるだけチェックインしないことが推奨されるからです。
- 次に、アプリ全体で使用できるように API を公開するには、
app/
ディレクトリにあるアプリのbuild.gradle
ファイルに Android 用 Secrets Gradle プラグインを含め、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
ファイルに追加します。
この Codelab に含まれるスターター プロジェクトでは、この依存関係がすでに追加されています。
build.gradle
dependencies {
// Dependency to include Maps SDK for Android
implementation 'com.google.android.gms:play-services-maps:17.0.0'
}
- 次に、新しい
meta-data
タグをAndroidManifest.xml
内に追加して、前の手順で作成した 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>
このレイアウトには SupportMapFragment
を含む単一の FrameLayout
が含まれ、このフラグメントには、後の手順で使用する、基盤となる GoogleMaps
オブジェクトが含まれています。
- 最後に、以下のコードを追加して
app/src/main/java/com/google/codelabs/buildyourfirstmap
にあるMainActivity
クラスを更新し、onCreate
メソッドをオーバーライドします。これにより、新しく作成したレイアウトでその内容を設定できるようになります。
MainActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
- アプリを実行して、デバイスの画面に地図の読み込みが表示されるのを確認してみましょう。
5. Cloud ベースのマップのスタイル設定(任意)
Cloud ベースのマップのスタイル設定によって地図のスタイルをカスタマイズすることも可能です。
マップ ID を作成する
地図スタイルを関連付けたマップ ID の作成が済んでいない場合は、マップ ID のガイドを参照して、以下を行いましょう。
- マップ ID を作成します。
- マップ ID を地図スタイルに関連付けます。
マップ ID をアプリに追加する
作成したマップ ID を使用するには、activity_main.xml
ファイルを変更し、SupportMapFragment
の map: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. マーカーを追加する
このタスクでは、地図上でハイライト表示したいスポットを示すマーカーを地図に追加します。まず、スターター プロジェクトで提供されている場所のリストを取得し、次に、それらの場所を地図に追加します。次の例では、自転車店がそれらの場所に該当します。
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()
が呼び出され、それに続いてラムダが渡されます。このラムダは、GoogleMap
オブジェクトが渡される場所です。このラムダ内で、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()
}
}
場所を読み込む
自転車店のリストを読み込むには、places
というプロパティを MainActivity
に追加し、以下のように定義します。
MainActivity.places
private val places: List<Place> by lazy {
PlacesReader(this).read()
}
このコードは、PlacesReader
で read()
メソッドを呼び出し、List<Place>
を返します。Place
には、name
というプロパティ、場所の名前、latLng
(場所の位置を示す座標)が含まれます。
Place
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
フォルダを右クリックし、[New] > [Layout Resource File] を選択します。
- ダイアログの [File name] フィールドに「
marker_info_contents
」、[Root element
] フィールドに「LinearLayout
」と入力し、[OK] をクリックします。
このレイアウト ファイルは、情報ウィンドウ内にコンテンツを表すように、後でインフレートされます。
- 縦方向の
LinearLayout
ビューグループ内に 3 つの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()
の 2 つのメソッドが含まれています。どちらのメソッドも View
オブジェクト(省略可)を返し、前者はウィンドウ自体のカスタマイズに使用され、後者はそのコンテンツのカスタマイズに使用されます。ここでは、両方を実装し、getInfoContents()
の戻り値をカスタマイズする一方、getInfoWindow()
では、デフォルトのウィンドウを使用する必要があることを示す null を返します。
- Android Studio のプロジェクト ビューで
app/src/main/java/com/google/codelabs/buildyourfirstmap
フォルダを右クリックして、MainActivity
と同じパッケージ内にMarkerInfoWindowAdapter
という新しい Kotlin ファイルを作成してから、[New] > [Kotlin File/Class] を選択します。
- ダイアログで「
MarkerInfoWindowAdapter
」と入力し、[File] はハイライト表示された状態のままにします。
- ファイルを作成したら、以下のコード スニペットの内容を新しいファイルにコピーします。
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()
メソッドの内容では、メソッド内の指定された Marker が Place
タイプにキャストされます。キャストができない場合、メソッドは null を返します(まだ Marker
でタグのプロパティは設定していませんが、それは次の手順で行います)。
次に、レイアウト marker_info_contents.xml
がインフレートされ、それに続いて、TextViews
を含むテキストが Place
タグに設定されます。
MainActivity を更新する
これまでに作成したすべてのコンポーネントを結びつけるには、MainActivity
クラスに 2 行を追加する必要があります。
まず、getMapAsync
メソッド呼び出し内のカスタム InfoWindowAdapter
、MarkerInfoWindowAdapter
を渡すために、GoogleMap
オブジェクトで setInfoWindowAdapter()
メソッドを呼び出し、MarkerInfoWindowAdapter
の新しいインスタンスを作成します。
- これを行うには、
getMapAsync()
ラムダ内のaddMarkers()
メソッド呼び出しの後に以下のコードを追加します。
MainActivity.onCreate()
// Set custom info window adapter
googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
最後に、各 Place を、地図に追加するすべての Marker のタグプロパティとして設定する必要があります。
- これを行うには、
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
}
カスタム マーカー画像を追加する
マーカー画像のカスタマイズは、マーカーが地図上で示す場所のタイプを効果的に伝える方法の一つです。この手順では、デフォルトの赤いマーカーの代わりに自転車を表示して、地図上で各店舗を示します。スターター プロジェクトには、今回使用する自転車アイコン(ic_directions_bike_black_24dp.xml
)が app/src/res/drawable
に含まれています。
マーカーにカスタム ビットマップを設定する
ベクター型ドローアブルの自転車アイコンを使用できるようになったら、次はそのドローアブルを地図上の各マーカーのアイコンとして設定します。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
として返します。
- このプロパティを使って、
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 を更新する
ユーティリティ ライブラリは Android SDK for Android とは別にパッケージ化されているため、build.gradle
ファイルに別の依存関係を追加する必要があります。
app/build.gradle
ファイルのdependencies
セクションを更新してみましょう。
build.gradle
implementation 'com.google.maps.android:android-maps-utils:1.1.0'
- この行を追加する際には、新しい依存関係を取得するためにプロジェクトの同期を実行する必要があります。
クラスタリングを実装する
アプリにクラスタリングを実装するには、以下の 3 つの手順を行います。
ClusterItem
インターフェースを実装します。DefaultClusterRenderer
クラスをサブクラス化します。ClusterManager
を作成し、アイテムを追加します。
ClusterItem インターフェースを実装する
クラスタリング可能なマーカーを地図上で示すオブジェクトはすべて、ClusterItem
インターフェースを実装する必要があります。今回の場合、Place
モデルは ClusterItem
に準拠する必要があるということです。Place.kt
ファイルを開き、以下の変更を加えてください。
Place
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 は、以下の 3 つのメソッドを定義します。
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
}
}
このクラスは、以下の 2 つの関数をオーバーライドします。
onBeforeClusterItemRendered()
: クラスタが地図上にレンダリングされる前に呼び出されます。ここでは、MarkerOptions
を使ってカスタマイズを追加できます。今回の場合は、マーカーのタイトル、位置、アイコンが設定されます。onClusterItemRenderer()
: マーカーが地図上にレンダリングされた直後に呼び出されます。ここでは、作成したMarker
オブジェクトにアクセスできます。今回の場合は、マーカーのタグプロパティが設定されます。
ClusterManager を作成してアイテムを追加する
最後に、クラスタリングを動作させるため、MainActivity
に変更を加えて ClusterManager
をインスタンス化し、必要な依存関係を提供する必要があります。ClusterManager
では、マーカー(ClusterItem
オブジェクト)の追加を内部で処理するため、地図上でマーカーを直接追加する代わりに、この役割が ClusterManager
に委任されます。また、ClusterManager
の内部でも setInfoWindowAdapter()
が呼び出されるため、カスタム情報ウィンドウの設定は ClusterManger
の MarkerManager.Collection
オブジェクトで行う必要があります。
- まず、
MainActivity.onCreate()
のgetMapAsync()
呼び出しのラムダの内容を変更します。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 は、マーカーを追加する 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()
が呼び出されます。この手順が必要となるのは、カメラの更新を呼び出す前に地図のプロパティ(サイズなど)を地図で計算する必要があるためです。
ラムダでは、地図の長方形の領域を定義する新しい LatLngBounds
オブジェクトが作成されます。これは、すべての場所が境界内に収まるように、場所のすべての LatLng
値を含めて段階的に作成されます。このオブジェクトが作成されると、GoogleMap
の moveCamera()
メソッドが呼び出され、CameraUpdateFactory.newLatLngBounds(bounds.build(), 20)
を介してそれに CameraUpdate
が渡されます。
- アプリを実行すると、カメラがサンフランシスコで初期化されて表示されるようになります。
カメラの変更をリッスンする
カメラ位置の変更に加えて、ユーザーが地図を移動する際にカメラの更新をリッスンすることもできます。これは、カメラの移動に合わせて UI を変更する場合に便利です。
試しに、カメラが動くたびにマーカーが半透明になるようにコードを変更します。
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
が追加され、カメラが移動を開始するたびにすべてのマーカー(クラスタとマーカーの両方)のアルファ値が 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. マップ KTX
1 つ以上の Google Maps Platform 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 ライブラリには、前の手順で使用した GoogleMap.addMarker(MarkerOptions)
と GoogleMap.addCircle(CircleOptions)
に代わる DSL スタイルの API が用意されています。前述の API を使用するには、マーカーまたは円のオプションを含むクラスを構築する必要があります。一方、KTX の代替手段では、指定するラムダでマーカーまたは円のオプションを設定できます。
これらの 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() 拡張機能の suspend 関数を使用する
Maps KTX ライブラリには、コルーチン内で使用する suspend 関数拡張機能もあります。具体的には、SupportMapFragment.getMapAsync(OnMapReadyCallback)
と GoogleMap.setOnMapLoadedCallback(OnMapLoadedCallback)
の代わりとなる suspend 関数が用意されています。これらの代替 API を使用すると、コールバックを渡す必要がなくなり、代わりにこれらのメソッドのレスポンスを連続して同期的に受信できます。
これらのメソッドは suspend 関数であるため、コルーチン内で使用する必要があります。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()
に置き換えられている点に注意してください。これらの suspend 関数を使用してコードをリファクタリングすることで、同等のコールバック ベースのコードを順次記述できます。
- リファクタリングされた変更を使用してアプリを再構築してください。
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: この Codelab などで説明されているすべての機能を示す GitHub のサンプルコードです。
- Google Maps Platform で Android アプリを作成するための Kotlin の他の Codelab