地図を Android アプリに追加する(Kotlin)

1. 始める前に

この Codelab では、米国カリフォルニア州サンフランシスコにある自転車店の地図を表示するアプリを作成することで、Maps SDK for Android をアプリに統合する方法と、その主要な機能を使う方法について説明します。

f05e1ca27ff42bf6.png

Prerequisites

  • Kotlin と Android の開発に関する基本的な知識

演習内容

  • Maps SDK for Android を有効にして使用し、Google マップを Android アプリに追加します。
  • マーカーを追加、カスタマイズ、およびクラスタ化します。
  • 地図上でポリラインとポリゴンを描画します。
  • カメラの視点をプログラムで制御します。

必要なもの

2. 設定する

以下の有効化の手順では、Maps SDK for Android を有効にする必要があります。

Google Maps Platform を設定する

課金を有効にした Google Cloud Platform アカウントとプロジェクトをまだ作成していない場合は、Google Maps Platform スタートガイドに沿って請求先アカウントとプロジェクトを作成してください。

  1. Cloud Console で、プロジェクトのプルダウン メニューをクリックし、この Codelab に使用するプロジェクトを選択します。

  1. Google Cloud Marketplace で、この Codelab に必要な Google Maps Platform API と SDK を有効にします。詳しい手順については、こちらの動画またはドキュメントをご覧ください。
  2. Cloud Console の [認証情報] ページで API キーを生成します。詳しい手順については、こちらの動画またはドキュメントをご覧ください。Google Maps Platform へのリクエストでは、例外なく API キーが必要です。

3. クイック スタート

できるだけ早く演習を開始できるように、この Codelab で使用できるスターター コードが用意されています。すぐに次のステップに進んでも問題ありませんが、ご自身で構築するためのすべての手順を確認したい場合は、最後までお読みください。

  1. git がインストールされている場合は、リポジトリのクローンを作成します。
git clone https://github.com/googlecodelabs/maps-platform-101-android.git

あるいは、以下のボタンをクリックしてソースコードをダウンロードすることもできます。

  1. コードを入手したら、Android Studio の starter ディレクトリにあるプロジェクトを開いてみましょう。

4. Google マップを追加する

このセクションでは、Google マップを追加して、アプリの起動時に読み込まれるようにします。

d1d068b5d4ae38b9.png

API キーを追加する

Maps SDK for Android で API キーがアプリに関連付けられるように、前の手順で作成した API キーをアプリに提供する必要があります。

  1. これを行うには、プロジェクトのルート ディレクトリ(gradle.propertiessettings.gradle と同じレベル)で local.properties というファイルを開きます。
  2. このファイルに、作成した API キーが値として指定された新しいキー(GOOGLE_MAPS_API_KEY)を定義します。

local.properties

GOOGLE_MAPS_API_KEY=YOUR_KEY_HERE

local.properties は、Git リポジトリの .gitignore ファイルに含まれています。これは、API キーが機密情報と見なされ、ソース コントロールにはできるだけチェックインしないことが推奨されるからです。

  1. 次に、アプリ全体で使用できるように 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 マップの依存関係を追加する

  1. これで、アプリ内で 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'
}
  1. 次に、新しい 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}" />
  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>

このレイアウトには SupportMapFragment を含む単一の FrameLayout が含まれ、このフラグメントには、後の手順で使用する、基盤となる 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. Cloud ベースのマップのスタイル設定(任意)

Cloud ベースのマップのスタイル設定によって地図のスタイルをカスタマイズすることも可能です。

マップ 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() が呼び出され、それに続いてラムダが渡されます。このラムダは、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()
}

このコードは、PlacesReaderread() メソッドを呼び出し、List<Place> を返します。Place には、name というプロパティ、場所の名前、latLng(場所の位置を示す座標)が含まれます。

Place

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

地図にマーカーを追加する

これで、場所のリストがメモリに読み込まれました。次に、それらの場所を地図上に表します。

  1. MainActivityaddMarkers() というメソッドを作成し、以下のように定義します。

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 ビューグループ内に 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 を返します。

  1. Android Studio のプロジェクト ビューで app/src/main/java/com/google/codelabs/buildyourfirstmap フォルダを右クリックして、MainActivity と同じパッケージ内に MarkerInfoWindowAdapter という新しい Kotlin ファイルを作成してから、[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() メソッドの内容では、メソッド内の指定された Marker が Place タイプにキャストされます。キャストができない場合、メソッドは null を返します(まだ Marker でタグのプロパティは設定していませんが、それは次の手順で行います)。

次に、レイアウト marker_info_contents.xml がインフレートされ、それに続いて、TextViews を含むテキストが Place タグに設定されます。

MainActivity を更新する

これまでに作成したすべてのコンポーネントを結びつけるには、MainActivity クラスに 2 行を追加する必要があります。

まず、getMapAsync メソッド呼び出し内のカスタム InfoWindowAdapterMarkerInfoWindowAdapter を渡すために、GoogleMap オブジェクトで setInfoWindowAdapter() メソッドを呼び出し、MarkerInfoWindowAdapter の新しいインスタンスを作成します。

  1. これを行うには、getMapAsync() ラムダ内の addMarkers() メソッド呼び出しの後に以下のコードを追加します。

MainActivity.onCreate()

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

最後に、各 Place を、地図に追加するすべての Marker のタグプロパティとして設定する必要があります。

  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
}

カスタム マーカー画像を追加する

マーカー画像のカスタマイズは、マーカーが地図上で示す場所のタイプを効果的に伝える方法の一つです。この手順では、デフォルトの赤いマーカーの代わりに自転車を表示して、地図上で各店舗を示します。スターター プロジェクトには、今回使用する自転車アイコン(ic_directions_bike_black_24dp.xml)が app/src/res/drawable に含まれています。

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 を更新する

ユーティリティ ライブラリは Android 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

クラスタリングを実装する

アプリにクラスタリングを実装するには、以下の 3 つの手順を行います。

  1. ClusterItem インターフェースを実装します。
  2. DefaultClusterRenderer クラスをサブクラス化します。
  3. 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() が呼び出されるため、カスタム情報ウィンドウの設定は ClusterMangerMarkerManager.Collection オブジェクトで行う必要があります。

  1. まず、MainActivity.onCreate()getMapAsync() 呼び出しのラムダの内容を変更します。addMarkers()setInfoWindowAdapter() の呼び出しをコメントアウトし、代わりに、次の手順で定義する addClusteredMarkers() というメソッドを呼び出してみましょう。

MainActivity.onCreate()

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

    // Set custom info window adapter.
    // googleMap.setInfoWindowAdapter(MarkerInfoWindowAdapter(this))
}
  1. 次に、MainActivityaddClusteredMarkers() を定義します。

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 オブジェクトに対して行う必要があります。最後に、ユーザーが地図を移動およびズームした際にクラスタリングを変更したい場合、OnCameraIdleListenergoogleMap に提供され、カメラがアイドル状態になると 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. 次に、MainActivitycircle プロパティと 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() が呼び出されます。この手順が必要となるのは、カメラの更新を呼び出す前に地図のプロパティ(サイズなど)を地図で計算する必要があるためです。

ラムダでは、地図の長方形の領域を定義する新しい LatLngBounds オブジェクトが作成されます。これは、すべての場所が境界内に収まるように、場所のすべての LatLng 値を含めて段階的に作成されます。このオブジェクトが作成されると、GoogleMapmoveCamera() メソッドが呼び出され、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 が追加され、カメラが移動を開始するたびにすべてのマーカー(クラスタとマーカーの両方)のアルファ値が 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. マップ KTX

1 つ以上の Google Maps Platform Android SDK を使用する Kotlin アプリでは、Kotlin 拡張機能または KTX ライブラリを使用して、コルーチン、拡張機能のプロパティ/関数などの Kotlin 言語機能を利用できます。各 Google Maps SDK には、以下に示すように対応する KTX ライブラリがあります。

Google Maps Platform 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 ライブラリには、前の手順で使用した 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 の関数リテラル(レシーバー)を使用することで可能になります。

  1. 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 関数を使用してコードをリファクタリングすることで、同等のコールバック ベースのコードを順次記述できます。

  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: この Codelab などで説明されているすべての機能を示す GitHub のサンプルコードです。
  • Google Maps Platform で Android アプリを作成するための Kotlin の他の Codelab