[Maps SDK for Android] AndroidアプリにGoogle Mapを組み込む

最近 Maps SDK for Android を触る機会があったので、アレコレと試しています。
アプリに地図を組み込むのは面倒だと放置していましたが、凝ったことをしなければ組み込みは簡単ですし、マップの上に好きなオブジェクトを置いたり、それらにインタラクションをつけるのも手軽にできます。
そこで、ここでは以下についてまとめました。

* 地図を組み込んだアクティビティの作成と起動
* ジェスチャなど、地図についての設定
* マーカーや図形など、マップオブジェクトの配置
* マップオブジェクトをタップに反応させるなど、インタラクションの設定

なお、この記事は下記ページにある内容を元に書きました。Maps SDK を使う際には1度確認して頂ければと思います。

[Maps SDK for Android Overview](https://developers.google.com/maps/documentation/android-sdk/intro)

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

* macOS Mojave バージョン10.14.5
* Android Studio 3.4.2
* Pixel3a + Android 9

自分で実装してみたものは GitHub で公開しています。

[MapImpl](https://github.com/outerlet/MapImpl)

# APIキーの取得

Maps API を利用するためには、Google Cloud Platform Console にログインして API キーを取得し、それを Android Studio プロジェクトに設定する必要があります。キーを取得するための手順は[こちら](https://developers.google.com/maps/documentation/android-sdk/get-api-key)でご確認ください。

<details>
<summary>APIキーの取り扱いについて</summary><div>この記事では取得したキーの用途に制限をかけず、アプリもデバッグモードでしか実行していません。リリース用のキーには、不正使用や割り当ての盗用を防止する措置を実施したうえで使用して下さい。</div></details>

<details>
<summary>私が取得したときの手順</summary>
参考までに、現時点（2019/07）で私が取得したときの手順を記載しておきます。
ちなみに Google Cloud Platform 上に自分のアカウントがありますので、そのアカウントを使って作ったプロジェクトが存在する状態です。
<div>
1. [Google Cloud Platform Console](https://console.cloud.google.com/) にアクセスします。
2. Platform 上の任意のプロジェクトを開きます。
3. コンソールの左ペインに"Googleマップ"がありますので、それをクリック。
4. "Maps SDK for Android" をクリック。
5. 『有効にする』ボタンをクリックすると、『APIを有効にしています』というメッセージが出るのでしばらく待ちます。
6. 有効になったら『認証情報』をクリック。
7. 『認証情報を作成』をクリックし、展開されるサブメニューから『APIキー』をクリック。
8. API キーが発行されるので、これをコピーしておきます。
</div></details>

# アクティビティの作成と起動

地図が組み込まれたアクティビティを作成するには、Android Studio のメニューから作成するのが簡単です。
~~※直接 MapView をレイアウトに埋め込むこともできるはずですが、調べきれてません…。~~
MapView を Fragment に配置する方法は分かったので、[記事](https://qiita.com/outerlet/items/36a9152c1cce46a9cfa6)にまとめました。

### 1. アクティビティの作成

プロジェクトと一緒に作成する方法と、既存のプロジェクトに追加する方法があります。
ちなみに必要なライブラリは Android Studio が自動的に追加してくれます。作成後の build.gradle を確認してみてください。

#### 1-1. プロジェクトと一緒に作成する

Android Studio の起動画面で "Start a new Android Studio Project" を選択し、『Create New Project』の画面で "Google Maps Activity" を選択します。
そのままプロジェクト名や Package Name などを入力してプロジェクトを作成すると、地図が組み込まれたアクティビティも一緒に作成されます。

#### 1-2.既存のプロジェクトに追加する

Android Studio のメニューから "File" を選択するか、プロジェクトビューで右クリックすると表示されるコンテキストメニューで "New" > "Google" > "Google Maps Activity" をクリックします。
そのあとアクティビティ名などを任意に設定して『Finish』をクリックすれば地図が組み込まれたアクティビティが追加されます。

### 2. APIキーの設定

res/values に『google_maps_api.xml』というファイルができています。
『YOUR_KEY_HERE』の部分を Console で取得したAPIキーに変更して下さい。

```xml
<string name="google_maps_key" translatable="false" templateMergeStrategy="preserve">YOUR_KEY_HERE</string>
```

### 3. アクティビティの起動

作成したアクティビティを起動すると地図が表示されます。ちなみにシドニーを中心に表示されているのは、自動生成されたコードがそのようになっているためです。
![map_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/d730f62f-d17a-11ed-958e-7824e3b70138.jpeg)

# 地図自体の設定や操作

作成されたアクティビティには OnMapReadyCallback インターフェイスが実装されており、 onMapReady というメソッドが追加されています。このメソッドは、地図の準備ができたときに呼び出されるコールバックです。
地図関連の操作をするときは、基本的に onMapReady で取得できる [GoogleMap](https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap) オブジェクトの各メソッドを呼び出します。

### 表示位置の変更

作成されたアクティビティを実行すると、カメラ位置（＝地図の中心）がシドニーになっています。これを別の場所に変更します。
GoogleMap#moveCamera メソッドで、カメラ位置や表示倍率を変更できます。
moveCamera の引数は [CameraUpdate クラス](https://developers.google.com/android/reference/com/google/android/gms/maps/CameraUpdate)です。これは [CameraUpdateFactory](https://developers.google.com/android/reference/com/google/android/gms/maps/CameraUpdateFactory) クラスで生成できます。

**CameraUpdateFactory のメソッド（一部）**

|メソッド|機能|
|:--|:--|
|newLatLng(LatLng latLng)|任意の緯度経度を中心に表示|
|newLatLngZoom(LatLng latLng, float zoom)|任意の緯度経度を中心に、任意の倍率で表示|
|zoomTo(float zoom)|現在の表示位置を任意の倍率で表示|
|newCameraPosition(CameraPosition cameraPosition)|CameraPositionオブジェクトに設定した内容で表示|

例えば、大阪駅を中心に16倍で表示したい場合は以下のようになります。

```kotlin
val osakaStation = LatLng(34.702423,135.495972)
moveCamera(CameraUpdateFactory.newLatLngZoom(osakaStation, 16.0f))
```

位置や倍率だけでなく仰角や回転角など、より詳細な表示位置を設定したい場合は newCameraPosition メソッドを使ってください。引数の CameraPosition は [CameraPosition.Builder](https://developers.google.com/android/reference/com/google/android/gms/maps/model/CameraPosition.Builder)を使って生成できます。

### 地図の種類を変更

GoogleMap#setMapType(int type) で表示する地図の種類を変更することができます。type にセットする値は、GoogleMap クラスの定数5つから選べます。

**setMapTypeの引数と表示結果**

|MAP_TYPE_NORMAL|MAP_TYPE_SATELLITE|MAP_TYPE_HYBRID|MAP_TYPE_TERRAIN|MAP_TYPE_NONE|
|:-:|:-:|:-:|:-:|:-:|
|通常|衛星写真|衛星+地図マーカー|地形図|なし|
|![normal1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/b02a1c76-70ec-774e-3758-346d79a4fd43.jpeg)|![satellite1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/37918b36-67d5-b55d-5b82-686e4a4481e1.jpeg)|![hybrid1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/cc3c75fb-4314-20dd-1975-64e55916fb56.jpeg)|![terrain1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/bdaf049c-0846-c6e4-e78d-9663020e55a4.jpeg)|![none1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/42ed18aa-61c0-84c0-7a12-0bcf98a7c8fd.jpeg)|

### 地図表示に制限を設ける

GoogleMap クラスのメソッドで、表示可能な領域や倍率を制限することができます。

**表示制限に関するメソッド（一部）**

|メソッド|機能|
|:--|:--|
|setLatLngBoundsForCameraTarget(LatLngBounds bounds)|指定した領域からカメラ位置が外に移動しないよう制限する|
|setMaxZoomPreference(float maxZoomPreference)|指定した倍率以上にズームインしないよう制限する|
|setMinZoomPreference(float minZoomPreference)|指定した倍率以下にズームアウトしないよう制限する|

以下はズームを14倍〜16倍に制限し、特定の領域外にカメラが移動しないようにするコードです。

```kotlin
googleMap.apply {
	setMaxZoomPreference(16.0f)
	setMinZoomPreference(14.0f)
	setLatLngBoundsForCameraTarget(
	    LatLngBounds(LatLng(34.695332 ,135.486831), LatLng(34.707469, 135.501508)))
}
```

余談ですが LatLngBounds をコンストラクタから生成する場合は、地図上の2点を指定して領域を定義します。このとき**緯度経度の指定は「southwest, northeast」の順**ですので注意してください（私はしばらく「northwest, southeast」と勝手に間違って悩みました）。

### 地図の操作に制限を設ける

GoogleMap#getUiSettings() メソッドで得られる [UiSettings](https://developers.google.com/android/reference/com/google/android/gms/maps/UiSettings) オブジェクトを操作することで、ジェスチャの有効／無効などを切り替えることができます。
UiSettings クラスには操作だけでなく、UI部品関連のメソッドも用意されていますので、APIを確認してみてください。

**操作制限に関するメソッド（一部）**

|メソッド|機能|
|:--|:--|
|setScrollGesturesEnabled(boolean enabled)|スクロール操作（1本指でドラッグ）の有効／無効を設定|
|setZoomGesturesEnabled(boolean enabled)|ズーム操作（ピンチイン・アウト）の有効／無効を設定|
|setRotateGesturesEnabled(boolean enabled)|回転操作（2本指で回す）の有効／無効を設定|
|setTiltGesturesEnabled(boolean enabled)|チルト操作（2本指で引き起こす）の有効／無効を設定|
|setAllGesturesEnabled(boolean enabled)|全てのジェスチャの有効／無効を設定|

以下では、スクロール／ズーム／回転／チルトを全て無効にしています。 setAllGesturesEnabled を使えば一括で無効にできます。

```kotlin
// Kotlinなのでプロパティで操作
googleMap.uiSettings.run {
    isScrollGesturesEnabled = false
    isTiltGesturesEnabled = false
    isRotateGesturesEnabled = false
    isZoomGesturesEnabled = false
}
```

# マップオブジェクト

マップオブジェクトは、地図上に描画できるマーカーや円、多角形などのことです。

### 種類

マップオブジェクトの種類は以下の通りです。

|種類|クラス|描画用のメソッド|クリックリスナ[^1]|
|:--|:--|:--|:--|
|マーカー|Marker|addMarker(MarkerOptions options)|OnMarkerClickListener|
|円|Circle|addCircle(CircleOptions options)|OnCircleClickListener|
|線|Polyline|addPolyline(PolylineOptions options)|OnPolylineClickListener|
|多角形|Polygon|addPolygon(PolygonOptions options)|OnPolygonClickListener|
|画像|GroundOverlay|addGroundOverlay(GroundOverlayOptions options)|OnGroundOverlayClickListener|
|タイル画像|TileOverlay|addTileOverlay(TileOverlayOptions options)|-|

[^1]: 全て GoogleMap クラスのインターフェイスです。

個人的に気になったのは、**Circle を描画するときの半径がメートル指定**ということです。
例えばランドマークに何らかのマークをつけたいときに Circle にしてしまうと、ズームするとその円が大きくなってしまいます。
何かの目印にしたい場合は、Circle でなく Marker をつける方がよさそうです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/b90df95e-6d0e-4da9-0e45-06637deb81d4.jpeg" width="240"/>&nbsp;<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67520/9d53458f-6f0d-c84d-3bda-aa6b5b3b905a.jpeg" width="240" />

### マップオブジェクトを描画

これも GoogleMap オブジェクトを介して描画します。どのオブジェクトであれ、基本的に手順は以下の通りです。各オブジェクトを描画するためのオプションクラスについては、前項の表で描画用メソッドの引数を参照して下さい。

1. 目的のマップオブジェクトに対応したオプションクラスのオブジェクトを生成
2. 1.のオブジェクトに、描画する色やサイズなどのプロパティを設定
3. GoogleMap の各セッタに、2.のオブジェクトをセット

例1：枠線が緑色＆内部が黄色＆半径20メートルの円を、大阪駅を中心として描画

```kotlin
googleMap.addCircle(
    CircleOptions()
        .center(LatLng(34.702423, 135.495972))
        .radius(20.0)
        .fillColor(Color.YELLOW)
        .strokeColor(Color.GREEN)
        .zIndex(1.0f)
)
```

例2："ground_overlay" という Drawable 画像を、大阪駅を中心とした1辺300メートルの領域に描画

```kotlin
googleMap.addGroundOverlay(
    GroundOverlayOptions()
        .position(LatLng(34.702423, 135.495972), 300.0f)
        .image(BitmapDescriptorFactory.fromResource(R.drawable.ground_overlay))
        .zIndex(2.0f)
)
```

なお zIndex プロパティには、図形が重なるときにどちらを前面にするか指定します。
例1と例2を同じアクティビティの地図に描画すると Ground Overlay が前にくるので画像しか見えませんが、zIndex 値を逆にすると、円が画像に乗っかった形で描画されます。

### マップオブジェクトにインタラクションを設定する

インタラクションを設定したいマップオブジェクトに対応するリスナを作成し、GoogleMap オブジェクトに設定します。リスナについては前項の表を参考にしてください。
クリックの対象とするオブジェクトには、オプションで```clickable(true)```をセットしないと反応しないことに注意してください。
また、Marker には Click 以外にもリスナがあります。APIドキュメントで確認してみてください。

以下の例では、描画した Circle をタップしたときに『Circle is Tapped!!』とトーストで表示しています。

```kotlin
googleMap.run {
    addCircle(
        CircleOptions()
            .center(LatLng(34.702423, 135.495972))
            .radius(20.0)
            .fillColor(Color.YELLOW)
            .strokeColor(Color.GREEN)
            .zIndex(2.0f)
            .clickable(true)
    )

    setOnCircleClickListener { circle ->
        Toast.makeText(this@MapsActivity, "Circle is tapped!!", Toast.LENGTH_SHORT).show()
    }
}
```

#### タップされたのが何か判別する

複数のマップオブジェクトを描画したとき、何がタップされたか判別する方法についてです。
マップオブジェクトは、生成された際に固有のIDを割り当てられます。GoogleMap#addXXXXX の戻り値は描画されたオブジェクトなので、このIDを保持しておいて、リスナの戻り値と照らし合わせるという手段があります。

以下の例では、2つの円のどちらかをタップしたときに、IDからタップされた方を判別してトーストを出力しています。

```kotlin
googleMap.run {
    val firstCircle = addCircle(
        CircleOptions().center(LatLng(34.702423, 135.495972)).clickable(true)
    )

    val secondCircle = addCircle(
        CircleOptions().center(LatLng(34.695332, 135.501508)).clickable(true)
    )

    setOnCircleClickListener { circle ->
        val toastText = when (circle.id) {
            firstCircle.id -> "First"
            secondCircle.id -> "Second"
            else -> "Unknown"
        }

        Toast.makeText(this@MapsActivity, "$toastText is tapped!!", Toast.LENGTH_SHORT).show()
    }
}
```

これ以外にも、マップオブジェクトは View と同じく setTag/getTag メソッドが用意されています。
GoogleMap#addXXXXX メソッドでマップオブジェクトを描画したあとに setTag で必要な情報を格納しておいて、クリックリスナで getTag を使うという手もよいかも知れません。　

# まとめ

Maps SDK for Android を使ってアプリに地図を組み込む方法について書いてみました。
Maps SDK はここに書いた以外にも、様々な機能を備えています。以下の2点については特に便利だと感じました。

* マップオブジェクトの Marker をカスタマイズする方法
* Google Maps Android API Utility Library を使って、地図を更に拡張する方法

ただこれも書くと長くなりすぎるので、そのうち別の記事で書きたいと思います。

# 改訂履歴

* 2019/08/24
    * MapView を Fragment に配置する方法へのリンクを追加しました。