[Maps SDK for Android] 地図にMarkerを置く(その2・Utility Library編)


前に書いた記事で、Maps SDK for Android で表示した地図に配置する Marker について書きました。基本機能だけでも色々できますが、Google Maps Android API Utility Library を導入すると更にできることが広がります。
ここでは Utility Library の導入と使用法についてまとめてみました。

  1. 基本機能だけでできること(前の記事)
  2. Utility Library を使ってできること(この記事)

この記事でやること

  • Utility Library の導入
  • Marker をクラスタにまとめる
  • クラスタ化した Marker をカスタマイズする
  • Icon Generator を使って地図に地名を表示する

注意事項

Android Studio で Maps SDK for Android が使えることを前提にしています。そのための手順は以前まとめましたので、そちらご確認下さい。

環境

このページに書かれているコード等は以下の環境下で動作・検証しています。

macOS Mojave バージョン10.14.6
Android Studio 3.5
Pixel3a + Android 9

また、ここに書いた内容をもとにしたサンプルコードを GitHub で公開しています。

Utility Library の導入

モジュールの build.gradle に以下の dependencies を追加します。

dependencies {
    compile 'com.google.maps.android:android-maps-utils:0.5+'
}

Marker をクラスタにまとめる

地図のズームレベルが下がって複数の Marker が一箇所に集まったとき、「クラスタ」にまとまるようにすることができます。
下の図だと、左が全ての Marker がバラバラに表示されている状態、右がクラスタになって表示されている状態です。

以下の手順で実装することができます。

  1. ClusterItem を実装したクラスを作成
  2. ClusterManager オブジェクトを生成
  3. ClusterItem を ClusterManager に追加

ClusterItem を実装したクラスを作成

Marker ひとつひとつに相当するクラスです。 com.google.maps.android.clustering.ClusterItem を実装して、必要なメソッドを定義すればOKです。
getSnippet / getTitle / getPosition では、それぞれ Marker の snippet / titie / position に相当するものを返すようにします。以下は実装例です。

class SegmentClusterItem(val segment: Segment) : ClusterItem {
    override fun getSnippet(): String = segment.flowerName
    override fun getTitle(): String = segment.title
    override fun getPosition(): LatLng = segment.coordinate
}

ClusterManager オブジェクトを生成

com.google.maps.android.clustering.ClusterManager クラスをインスタンス化します。
このとき GoogleMap の以下のメソッドに ClusterManager インスタンスを設定するようにします。

  • setOnCameraIdleListener
  • setOnMarkerClickListener

以下は ClusterManager をインスタンス化しているところです。

val manager = ClusterManager<SegmentClusterItem>(this@ClusteringActivity, this).apply {
    setOnCameraIdleListener(this)
    setOnMarkerClickListener(this)
}

ClusterItem を ClusterManager に追加

あとは ClusterManager#addItem で ClusterManager に ClusterItem を追加していくだけです。これで Marker がある程度まで接近するとクラスタにまとまるようになります。

Segment.values().forEach {
    manager.addItem(SegmentClusterItem(it))
}

クラスタにならない場合

上記の実装をしても Marker がクラスタにまとまらない場合は、Marker の数が少ないことが考えられます。
Marker がクラスタにまとまるかどうかは、一定の領域内に含まれる Marker(=ClusterItem)の数で判定されます。デフォルトでは、5個以上の Marker が領域内に含まれないとクラスタになってくれません。これを変更するには、クラスタにする Marker の数を定義した ClusterRenderer オブジェクトを ClusterManager#setRenderer にセットする必要があります。

以下は、DefaultClusterRenderer を継承した ClusterRenderer クラスの実装例です。shouldRenderAsCluster で「何個以上の Marker が集まったらクラスタにするか」を判定します。この例では、2つ以上でクラスタになるようにしています。

class SegmentClusterRenderer(context: Context, map: GoogleMap, manager: ClusterManager<SegmentClusterItem>) : DefaultClusterRenderer<SegmentClusterItem>(context, map, manager) {
    override fun shouldRenderAsCluster(cluster: Cluster<SegmentClusterItem>?): Boolean {
        // ClusterItemが一定距離内にいくつ集まったらクラスタ化するかをBooleanで返す
        return cluster?.size ?: 0 >= 2
    }
}

以下でこの Renderer を Manager にセットしています。

manager.renderer = SegmentClusterRenderer(this@ClusteringActivity, map, manager)

クラスタ化した Marker をカスタマイズする

ClusterRenderer と IconGenerator を併用することで、Marker のアイコンやクラスタのアイコンを変更することができます。
下の図は、大阪市の各区役所に Marker を立てて、区の花をアイコンで表示しています。クラスタになると、そこに含まれる Marker の数が女性のイラストに表示されるようにしています。

ClusterRenderer で継承すべきメソッドは以下の通りです。
両方とも実装すべき処理は同じで、Marker やクラスタが描画されるときの MarkerOptions を変更することになります。

メソッド 実装内容
onBeforeClusterItemRendered Marker(=ClusterItem) のカスタマイズ
onBeforeClusterRendered クラスタのカスタマイズ

また com.google.maps.android.ui.IconGenerator を使えば任意のレイアウトで Bitmap を生成することが可能です。生成した Bitmap を、BitmapDescriptorFactory#fromBitmap を使って MarkerOptions#icon にセットすることで、Marker やクラスタのアイコンを任意に変更しています。
以下は DefaultClusterRenderer を継承した ClusterRenderer の例で、 res/layout/icon_segment.xml のレイアウトを使って Marker にアイコンを設定しています。icon_segment.xml には imageIcon というIDが付与された ImageView だけが配置されています。

private val itemImageView: ImageView
private val itemIconGenerator: IconGenerator = IconGenerator(context).apply {
    val iconView = LayoutInflater.from(context).inflate(R.layout.icon_segment, null, false).apply {
        itemImageView = findViewById(R.id.imageIcon)
    }
    setContentView(iconView)
}

override fun onBeforeClusterItemRendered(item: SegmentClusterItem, markerOptions: MarkerOptions) {
    itemImageView.setImageResource(item.segment.imageResId)
    val icon = itemIconGenerator.makeIcon()
    markerOptions.icon(BitmapDescriptorFactory.fromBitmap(icon))
}

ざっくり以下のような手順です。

  1. レイアウトから View をインフレート
  2. インフレートした View から、操作したい View を findViewById で取得
  3. IconGenerator#setContentView で、1の View をセット
  4. 2に任意の文字や画像をセット
  5. IconGenerator#makeIcon で Bitmap を生成
  6. MarkerOptions#icon に生成した Bitmap をセット

Icon Generator を使って地図に地名を表示する

地図上に地名などを表示したいという要件もあると思いますが、Marker に title や snippet をセットするだけでは Info Window のコンテンツとしてしか表示できません。
なのでここでは IconGenerator を使って、マーカーとして地名を強引に表示してみました。

IconGenerator で文字画像を作成するのは、前項と同じやり方で可能です。TextView を含んだレイアウト(もしくは TextView そのもの)を IconGenerator のコンテンツとして設定し、任意の文字列を TextView にセットしてから IconGenerator#makeIcon するという手順になります。
これで Bitmap ができますので、あとは Marker を生成して icon にセットするということになります。

val textView: TextView
val iconGenerator = IconGenerator(this@CustomIconActivity).apply {
    setBackground(null)    // nullにしないとデフォルトの背景が表示されてしまう(文字だけにならない)
    textView = layoutInflater.inflate(R.layout.icon_station_name, null, false) as TextView
    setContentView(textView)
}

textView.text = station.title

addMarker(MarkerOptions()
    .icon(BitmapDescriptorFactory.fromBitmap(iconGenerator.makeIcon()))
    .position(station.position)
    .anchor(0.5f, 0.0f)
    .zIndex(2.1f))

まとめ

以上、Utility Library を使った Marker のカスタマイズについてまとめてみました。
Utility Library には KML を扱ったりヒートマップを追加したりと、他にも多彩な機能が用意されています。
「参考」に公式ドキュメントへのリンクを貼っておきましたので、実際に読んで色々触ってみるのをお勧めします。この記事がそのきっかけになれば、大変嬉しく思います。

参考

Google Maps Android API Utility Library
googlemaps/android-maps-utils (GitHub)