1. 始める前に
この Codelab では、さまざまな種類のマーカーを使用して米国コロラド州の山々の地図を表示するアプリを作成することで、Maps SDK for Android をアプリに統合する方法と、その主要な機能を使う方法について説明します。また、地図上に他の図形を描画する方法も学習します。
Codelab を完了すると、アプリは次のようになります。
前提条件
- Kotlin、Jetpack Compose、Android 開発に関する基本的な知識
演習内容
- Maps SDK for Android の Maps Compose ライブラリを有効にして使用し、Android アプリに
GoogleMap
を追加する - マーカーを追加、カスタマイズする
- 地図上にポリゴンを描画する
- カメラの視点をプログラムで制御する
必要なもの
- Maps SDK for Android
- 課金が有効になっている Google アカウント
- Android Studio の最新の安定版
- Android 5.0 以降ベースの 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/googlemaps-samples/codelab-maps-platform-101-compose.git
あるいは、以下のボタンをクリックしてソースコードをダウンロードすることもできます。
- コードを入手したら、Android Studio の
starter
ディレクトリにあるプロジェクトを開いてみましょう。
4. API キーをプロジェクトに追加する
このセクションでは、アプリで安全に参照されるように API キーを保存する方法を説明します。API キーは、バージョン管理システムにはチェックインせず、プロジェクトのルート ディレクトリのローカルコピーに配置される secrets.properties
ファイルに保存することをおすすめします。secrets.properties
ファイルについて詳しくは、Gradle プロパティ ファイルをご覧ください。
このタスクを効率化するには、Android 用 Secrets Gradle プラグインの使用をおすすめします。
Android 用 Secrets Gradle プラグインを Google マップ プロジェクトにインストールする手順は以下のとおりです。
- Android Studio で最上位レベルの
build.gradle.kts
ファイルを開き、buildscript
の配下にあるdependencies
要素に次のコードを追加します。buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- モジュール レベルの
build.gradle.kts
ファイルを開き、次のコードをplugins
要素に追加します。plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- モジュール レベルの
build.gradle.kts
ファイルで、targetSdk
とcompileSdk
を 34 以上に設定します。 - ファイルを保存して、プロジェクトを Gradle と同期します。
- 最上位レベルのディレクトリで
secrets.properties
ファイルを開き、次のコードを追加します。YOUR_API_KEY
は実際の API キーに置き換えてください。secrets.properties
はバージョン管理システムにチェックインされないため、このファイルにキーを保存します。MAPS_API_KEY=YOUR_API_KEY
- ファイルを保存します。
- 最上位レベルのディレクトリ(
secrets.properties
ファイルと同じフォルダ)にlocal.defaults.properties
ファイルを作成し、次のコードを追加します。 このファイルの目的は、MAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
ファイルがない場合に API キーのバックアップ場所を提供し、ビルドが失敗しないようにすることです。この状況は、バージョン管理システムからアプリのクローンを作成し、API キーを提供するためにsecrets.properties
ファイルをまだローカルに作成していない場合に発生します。 - ファイルを保存します。
AndroidManifest.xml
ファイルでcom.google.android.geo.API_KEY
に移動し、android:value
属性を更新します。<meta-data>
タグがない場合は、<application>
タグの子として作成します。<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- Android Studio でモジュール レベルの
build.gradle.kts
ファイルを開き、secrets
プロパティを編集します。secrets
プロパティがない場合は追加します。プラグインのプロパティを編集してpropertiesFileName
をsecrets.properties
に、defaultPropertiesFileName
をlocal.defaults.properties
に設定し、他のプロパティも設定します。secrets { // Optionally specify a different file name containing your secrets. // The plugin defaults to "local.properties" propertiesFileName = "secrets.properties" // A properties file containing default secret values. This file can be // checked in version control. defaultPropertiesFileName = "local.defaults.properties" }
5. Google マップを追加する
このセクションでは、Google マップを追加して、アプリの起動時に読み込まれるようにします。
Maps Compose の依存関係を追加する
これで、アプリ内で API キーにアクセスできるようになりました。次に、Maps SDK for Android の依存関係をアプリの build.gradle.kts
ファイルに追加します。Jetpack Compose でビルドするには、Maps SDK for Android の要素をコンポーズ可能な関数とデータ型として提供する Maps Compose ライブラリを使用します。
build.gradle.kts
アプリレベルの build.gradle.kts
ファイルで、Compose 以外の Maps SDK for Android の依存関係を置き換えます。
dependencies {
// ...
// Google Maps SDK -- these are here for the data model. Remove these dependencies and replace
// with the compose versions.
implementation("com.google.android.gms:play-services-maps:18.2.0")
// KTX for the Maps SDK for Android library
implementation("com.google.maps.android:maps-ktx:5.0.0")
// KTX for the Maps SDK for Android Utility Library
implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
}
コンポーザブルの対応する関数は次のとおりです。
dependencies {
// ...
// Google Maps Compose library
val mapsComposeVersion = "4.4.1"
implementation("com.google.maps.android:maps-compose:$mapsComposeVersion")
// Google Maps Compose utility library
implementation("com.google.maps.android:maps-compose-utils:$mapsComposeVersion")
// Google Maps Compose widgets library
implementation("com.google.maps.android:maps-compose-widgets:$mapsComposeVersion")
}
Google マップのコンポーザブルを追加する
MountainMap.kt
で、MapMountain
コンポーザブル内にネストされた Box
コンポーザブル内に GoogleMap
コンポーザブルを追加します。
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...
@Composable
fun MountainMap(
paddingValues: PaddingValues,
viewState: MountainsScreenViewState.MountainList,
eventFlow: Flow<MountainsScreenEvent>,
selectedMarkerType: MarkerType,
) {
var isMapLoaded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Add GoogleMap here
GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapLoaded = { isMapLoaded = true }
)
// ...
}
}
アプリをビルドして実行します。悪名高い Null 島(緯度 0 度、経度 0 度とも呼ばれます)を中心とした地図が表示されます。後で、地図を目的の場所とズームレベルに配置する方法を学習しますが、まずは最初の成功を祝いましょう。
6. Cloud ベースのマップのスタイル設定
Cloud ベースのマップのスタイル設定によって地図のスタイルをカスタマイズすることも可能です。
マップ ID を作成する
地図スタイルを関連付けたマップ ID の作成が済んでいない場合は、マップ ID のガイドを参照して、以下を行いましょう。
- マップ ID を作成します。
- マップ ID を地図スタイルに関連付けます。
マップ ID をアプリに追加する
作成したマップ ID を使用するには、GoogleMap
コンポーザブルをインスタンス化するときに、コンストラクタの googleMapOptionsFactory
パラメータに割り当てられる GoogleMapOptions
オブジェクトを作成する際にマップ ID を使用します。
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
以上が完了したら、アプリを実行してみましょう。指定したスタイルで地図が表示されるはずです。
7. マーカーデータを読み込む
このアプリの主なタスクは、ローカル ストレージから山のコレクションを読み込み、それらの山を GoogleMap
に表示することです。このステップでは、山のデータを読み込んで UI に表示するために用意されているインフラストラクチャについて説明します。
山
Mountain
データクラスは、各山に関するすべてのデータを保持します。
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
山は後で標高に基づいて分割されます。標高が 14,000 フィート以上の山は、フォーティナーと呼ばれます。スターター コードには、このチェックを行う拡張関数が含まれています。
/**
* Extension function to determine whether a mountain is a "14er", i.e., has an elevation greater
* than 14,000 feet (~4267 meters).
*/
fun Mountain.is14er() = elevation >= 14_000.feet
MountainsScreenViewState
MountainsScreenViewState
クラスには、ビューのレンダリングに必要なすべてのデータが保持されます。山のリストの読み込みが完了したかどうかに応じて、Loading
状態または MountainList
状態になります。
/**
* Sealed class representing the state of the mountain map view.
*/
sealed class MountainsScreenViewState {
data object Loading : MountainsScreenViewState()
data class MountainList(
// List of the mountains to display
val mountains: List<Mountain>,
// Bounding box that contains all of the mountains
val boundingBox: LatLngBounds,
// Switch indicating whether all the mountains or just the 14ers
val showingAllPeaks: Boolean = false,
) : MountainsScreenViewState()
}
提供されるクラス: MountainsRepository
、MountainsViewModel
スターター プロジェクトでは、MountainsRepository
クラスが提供されています。このクラスは、GPS Exchange Format
または GPX ファイル top_peaks.gpx
に保存されている山岳地のリストを読み取ります。mountainsRepository.loadMountains()
を呼び出すと、StateFlow<List<Mountain>>
が返されます。
MountainsRepository
class MountainsRepository(@ApplicationContext val context: Context) {
private val _mountains = MutableStateFlow(emptyList<Mountain>())
val mountains: StateFlow<List<Mountain>> = _mountains
private var loaded = false
/**
* Loads the list of mountains from the list of mountains from the raw resource.
*/
suspend fun loadMountains(): StateFlow<List<Mountain>> {
if (!loaded) {
loaded = true
_mountains.value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.top_peaks).use { inputStream ->
readMountains(inputStream)
}
}
}
return mountains
}
/**
* Reads the [Waypoint]s from the given [inputStream] and returns a list of [Mountain]s.
*/
private fun readMountains(inputStream: InputStream) =
readWaypoints(inputStream).mapIndexed { index, waypoint ->
waypoint.toMountain(index)
}.toList()
// ...
}
MountainsViewModel
MountainsViewModel
は ViewModel
クラスで、山のコレクションを読み込み、そのコレクションと UI 状態の他の部分を mountainsScreenViewState
経由で公開します。mountainsScreenViewState
は、UI が collectAsState
拡張関数を使用して可変状態として監視できるホット StateFlow
です。
健全なアーキテクチャの原則に従い、MountainsViewModel
はアプリの状態をすべて保持します。UI は onEvent
メソッドを使用して、ユーザー操作をビューモデルに送信します。
@HiltViewModel
class MountainsViewModel
@Inject
constructor(
mountainsRepository: MountainsRepository
) : ViewModel() {
private val _eventChannel = Channel<MountainsScreenEvent>()
// Event channel to send events to the UI
internal fun getEventChannel() = _eventChannel.receiveAsFlow()
// Whether or not to show all of the high peaks
private var showAllMountains = MutableStateFlow(false)
val mountainsScreenViewState =
mountainsRepository.mountains.combine(showAllMountains) { allMountains, showAllMountains ->
if (allMountains.isEmpty()) {
MountainsScreenViewState.Loading
} else {
val filteredMountains =
if (showAllMountains) allMountains else allMountains.filter { it.is14er() }
val boundingBox = filteredMountains.map { it.location }.toLatLngBounds()
MountainsScreenViewState.MountainList(
mountains = filteredMountains,
boundingBox = boundingBox,
showingAllPeaks = showAllMountains,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MountainsScreenViewState.Loading
)
init {
// Load the full set of mountains
viewModelScope.launch {
mountainsRepository.loadMountains()
}
}
// Handle user events
fun onEvent(event: MountainsViewModelEvent) {
when (event) {
OnZoomAll -> onZoomAll()
OnToggleAllPeaks -> toggleAllPeaks()
}
}
private fun onZoomAll() {
sendScreenEvent(MountainsScreenEvent.OnZoomAll)
}
private fun toggleAllPeaks() {
showAllMountains.value = !showAllMountains.value
}
// Send events back to the UI via the event channel
private fun sendScreenEvent(event: MountainsScreenEvent) {
viewModelScope.launch { _eventChannel.send(event) }
}
}
これらのクラスの実装に興味がおありの場合は、GitHub からアクセスするか、Android Studio で MountainsRepository
クラスと MountainsViewModel
クラスを開いてください。
ViewModel を使用する
ビューモデルは MainActivity
で viewState
を取得するために使用されます。この Codelab の後半で、viewState
を使用してマーカーをレンダリングします。このコードはスターター プロジェクトにすでに含まれています。ここでは参照用にのみ示しています。
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. カメラの位置を調整する
GoogleMap
のデフォルトは、緯度 0、経度 0 の中心に設定されます。レンダリングするマーカーは、米国のコロラド州にあります。ビューモデルが提供する viewState
は、すべてのマーカーを含む LatLngBounds を表します。
MountainMap.kt
で、境界ボックスの中心に初期化された CameraPositionState
を作成します。GoogleMap
の cameraPositionState
パラメータを、作成した cameraPositionState
変数に設定します。
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
コードを実行して、地図がコロラド州の中央に表示されることを確認します。
マーカーの範囲に合わせてズーム
地図をマーカーにフォーカスするには、MountainMap.kt
ファイルの末尾に zoomAll
関数を追加します。カメラを新しい位置に移動するアニメーションは、完了までに時間がかかる非同期オペレーションであるため、この関数には CoroutineScope
が必要です。
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
次に、マーカー コレクションの境界が変更されたとき、またはユーザーが TopApp バーのズーム範囲ボタンをクリックしたときに zoomAll
関数を呼び出すコードを追加します。ズーム範囲ボタンは、すでにビューモデルにイベントを送信するように設定されています。ビューモデルからこれらのイベントを収集し、レスポンスとして zoomAll
関数を呼び出すだけで済みます。
fun MountainMap(
// ...
) {
// ...
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = viewState.boundingBox) {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
LaunchedEffect(true) {
eventFlow.collect { event ->
when (event) {
MountainsScreenEvent.OnZoomAll -> {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
}
}
}
}
アプリを実行すると、マーカーが配置されるエリアに地図がフォーカスされます。地図の位置を変更したり、ズームレベルを変更したりできます。ズーム範囲ボタンをクリックすると、マーカー エリアを中心に地図が再フォーカスされます。前進です。しかし、地図には見るべきものがあるべきです。次のステップでこの処理を行います。
9. 基本的なマーカー
このステップでは、地図上でハイライト表示したいスポットを示す Marker を地図に追加します。スターター プロジェクトで提供されている山のリストを使用して、これらの場所を地図上のマーカーとして追加します。
まず、GoogleMap
にコンテンツ ブロックを追加します。マーカーの種類は複数あるため、when
ステートメントを追加して各タイプに分岐します。各タイプは、次の手順で順番に実装します。
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
マーカーの追加
BasicMarkersMapContent
に @GoogleMapComposable
アノテーションを付けます。GoogleMap
コンテンツ ブロックで使用できるのは @GoogleMapComposable
関数のみです。mountains
オブジェクトには Mountain
オブジェクトのリストが含まれます。Mountain
オブジェクトの位置、名前、標高を使用して、リスト内の各山に対応するマーカーを追加します。この位置は Marker
の状態パラメータの設定に使用され、このパラメータがマーカーの位置を制御します。
// ...
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMapComposable
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false }
) {
mountains.forEach { mountain ->
Marker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
tag = mountain,
onClick = { marker ->
onMountainClick(marker)
false
},
zIndex = if (mountain.is14er()) 5f else 2f
)
}
}
アプリを実行すると、先ほど追加したマーカーが表示されます。
マーカーをカスタマイズする
先ほど追加したマーカーを目立たせ、有益な情報をユーザーに伝えるのに役立つ、カスタマイズのオプションがいくつかあります。このタスクでは、各マーカーの画像をカスタマイズしながら、それらのオプションのいくつかについて説明します。
スターター プロジェクトには、@DrawableResource
から BitmapDescriptor
を作成するヘルパー関数 vectorToBitmap
が含まれています。
スターター コードには、マーカーのカスタマイズに使用する山のアイコン baseline_filter_hdr_24.xml
が含まれています。
vectorToBitmap
関数は、地図ライブラリで使用するために、ベクター ドローアブルを BitmapDescriptor
に変換します。アイコンの色は BitmapParameters
インスタンスを使用して設定されます。
data class BitmapParameters(
@DrawableRes val id: Int,
@ColorInt val iconColor: Int,
@ColorInt val backgroundColor: Int? = null,
val backgroundAlpha: Int = 168,
val padding: Int = 16,
)
fun vectorToBitmap(context: Context, parameters: BitmapParameters): BitmapDescriptor {
// ...
}
vectorToBitmap
関数を使用して、14,000 フィート級の山と通常の山用に 2 つのカスタマイズされた BitmapDescriptor
を作成します。次に、Marker
コンポーザブルの icon
パラメータを使用してアイコンを設定します。また、anchor
パラメータを設定して、アイコンに対するアンカーの位置を変更します。円形のアイコンの場合は、中央を使用する方が効果的です。
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
// ...
) {
// Create mountainIcon and fourteenerIcon
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
)
)
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
)
)
mountains.forEach { mountain ->
val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
Marker(
// ...
anchor = Offset(0.5f, 0.5f),
icon = icon,
)
}
}
アプリを実行して、カスタマイズされたマーカーを確認します。Show all
スイッチを切り替えると、山脈の全景が表示されます。山が 14,000 フィート級の山であるかどうかによって、山のマーカーが異なります。
10. 高度なマーカー
AdvancedMarker
は、基本的な Markers
にさまざまな機能を追加します。このステップでは、衝突動作を設定し、ピンのスタイルを構成します。
AdvancedMarkersMapContent
関数に @GoogleMapComposable
を追加します。mountains
をループ処理し、それぞれに AdvancedMarker
を追加します。
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
mountains.forEach { mountain ->
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
collisionBehavior
パラメータに注目してください。このパラメータを REQUIRED_AND_HIDES_OPTIONAL
に設定すると、マーカーは優先度の低いマーカーを置き換えます。これは、基本マーカーと高度なマーカーを拡大することで確認できます。基本マーカーでは、マーカーとマーカーの両方がベースマップの同じ場所に配置される可能性があります。優先度の低いマーカーは、優先度の高いマーカーによって非表示になります。
アプリを実行して、高度なマーカーを確認します。下部のナビゲーション行で Advanced markers
タブを選択してください。
カスタマイズされた AdvancedMarkers
アイコンは、プライマリ カラーとセカンダリ カラーの配色を使用して、標高 14,000 フィート以上の山とその他の山を区別しています。vectorToBitmap
関数を使用して、2 つの BitmapDescriptor
を作成します。1 つは 14,000 フィート級の山用、もう 1 つはその他の山用です。これらのアイコンを使用して、各タイプにカスタム pinConfig
を作成します。最後に、is14er()
関数に基づいて、対応する AdvancedMarker
にピンを適用します。
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
)
)
val mountainPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(mountainIcon))
setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
build()
}
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
)
)
val fourteenerPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(fourteenerIcon))
setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
build()
}
mountains.forEach { mountain ->
val pin = if (mountain.is14er()) fourteenerPin else mountainPin
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
pinConfig = pin,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
11. クラスタ化されたマーカー
このステップでは、Clustering
コンポーザブルを使用して、ズームベースのアイテム グループ化を追加します。
Clustering
コンポーザブルには、ClusterItem
のコレクションが必要です。MountainClusterItem
は ClusterItem
インターフェースを実装します。このクラスを ClusteringMarkersMapContent.kt
ファイルに追加します。
data class MountainClusterItem(
val mountain: Mountain,
val snippetString: String
) : ClusterItem {
override fun getPosition() = mountain.location
override fun getTitle() = mountain.name
override fun getSnippet() = snippetString
override fun getZIndex() = 0f
}
次に、山のリストから MountainClusterItem
を作成するコードを追加します。このコードでは、UnitsConverter
を使用して、ユーザーのロケールに基づいて適切な表示単位に変換しています。これは、CompositionLocal
を使用して MainActivity
で設定されます。
@OptIn(MapsComposeExperimentalApi::class)
@Composable
@GoogleMapComposable
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
val unitsConverter = LocalUnitsConverter.current
val resources = LocalContext.current.resources
val mountainClusterItems by remember(mountains) {
mutableStateOf(
mountains.map { mountain ->
MountainClusterItem(
mountain = mountain,
snippetString = unitsConverter.toElevationString(resources, mountain.elevation)
)
}
)
}
Clustering(
items = mountainClusterItems,
)
}
このコードを使用すると、マーカーはズームレベルに基づいてクラスタ化されます。すっきりしましたね。
クラスタをカスタマイズする
他のマーカー タイプと同様に、クラスタ化されたマーカーはカスタマイズ可能です。Clustering
コンポーザブルの clusterItemContent
パラメータは、クラスタ化されていないアイテムをレンダリングするカスタム コンポーザブル ブロックを設定します。@Composable
関数を実装してマーカーを作成します。SingleMountain
関数は、カスタマイズされた背景色のカラースキームで、コンポーザブルなマテリアル 3 の Icon
をレンダリングします。
ClusteringMarkersMapContent.kt
で、マーカーの配色を定義するデータクラスを作成します。
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
また、ClusteringMarkersMapContent.kt
で、指定されたカラーパターン用のアイコンをレンダリングするコンポーズ可能な関数を作成します。
@Composable
private fun SingleMountain(
colors: IconColor,
) {
Icon(
painterResource(id = R.drawable.baseline_filter_hdr_24),
tint = colors.iconColor,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.padding(1.dp)
.drawBehind {
drawCircle(color = colors.backgroundColor, style = Fill)
drawCircle(color = colors.borderColor, style = Stroke(width = 3f))
}
.padding(4.dp)
)
}
次に、標高 14,000 フィート以上の山用のカラーパターンと、その他の山用のカラーパターンを作成します。clusterItemContent
ブロックで、指定された山が 14er かどうかに基づいて配色を選択します。
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
// ...
val backgroundAlpha = 0.6f
val fourteenerColors = IconColor(
iconColor = MaterialTheme.colorScheme.onPrimary,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.primary
)
val otherColors = IconColor(
iconColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.secondary
)
// ...
Clustering(
items = mountainClusterItems,
clusterItemContent = { mountainItem ->
val colors = if (mountainItem.mountain.is14er()) {
fourteenerColors
} else {
otherColors
}
SingleMountain(colors)
},
)
}
アプリを実行して、個々のアイテムのカスタマイズされたバージョンを確認します。
12. 地図上に図形を描画する
地図上で描画を行う方法の一つ(マーカーの追加)についてはすでに確認しましたが、Maps SDK for Android ではその他にも、描画によって地図上に有益な情報を表示できるさまざまな方法をサポートしています。
たとえば、地図上にルートやエリアを示す場合は、Polyline
と Polygon
を使用してこれらを地図に表示できます。また、地面に画像を固定したい場合は、GroundOverlay
を使用することも可能です。
このタスクでは、図形(特にコロラド州の輪郭)を描画する方法を説明します。コロラド州の境界は、北緯 37 度から 41 度、西経 102 度 3 分から 109 度 3 分の間と定義されています。これにより、アウトラインの描画が非常に簡単になります。
スターター コードには、度分秒表記から度数表記に変換する DMS
クラスが含まれています。
enum class Direction(val sign: Int) {
NORTH(1),
EAST(1),
SOUTH(-1),
WEST(-1)
}
/**
* Degrees, minutes, seconds utility class
*/
data class DMS(
val direction: Direction,
val degrees: Double,
val minutes: Double = 0.0,
val seconds: Double = 0.0,
)
fun DMS.toDecimalDegrees(): Double =
(degrees + (minutes / 60) + (seconds / 3600)) * direction.sign
DMS クラスを使用すると、4 つの角の LatLng
の位置を定義し、それらを Polygon
としてレンダリングすることで、コロラド州の境界線を描画できます。MountainMap.kt
に次のコードを追加します。
@Composable
@GoogleMapComposable
fun ColoradoPolygon() {
val north = 41.0
val south = 37.0
val east = DMS(WEST, 102.0, 3.0).toDecimalDegrees()
val west = DMS(WEST, 109.0, 3.0).toDecimalDegrees()
val locations = listOf(
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
LatLng(north, west),
)
Polygon(
points = locations,
strokeColor = MaterialTheme.colorScheme.tertiary,
strokeWidth = 3F,
fillColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
)
}
次に、GoogleMap
コンテンツ ブロック内で ColoradoPolyon()
を呼び出します。
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
これで、コロラド州の輪郭が描かれ、微妙な塗りつぶしが施されます。
13. KML レイヤと縮尺バーを追加する
最後のセクションでは、さまざまな山脈の概要を説明し、地図にスケールバーを追加します。
山脈の輪郭を描く
以前は、コロラド州の輪郭を描画しました。ここでは、より複雑なシェイプを地図に追加します。スターター コードには、重要な山脈の概要を示す Keyhole Markup Language(KML)ファイルが含まれています。Maps SDK for Android ユーティリティ ライブラリには、KML レイヤを地図に追加する関数があります。MountainMap.kt
の GoogleMap
コンテンツ ブロックで、when
ブロックの後に MapEffect
呼び出しを追加します。MapEffect
関数が GoogleMap
オブジェクトで呼び出されます。これは、GoogleMap
オブジェクトを必要とするコンポーザブルでない API とライブラリ間の便利なブリッジとして機能します。
fun MountainMap(
// ...
) {
var isMapLoaded by remember { mutableStateOf(false) }
val context = LocalContext.current
GoogleMap(
// ...
) {
// ...
when (selectedMarkerType) {
// ...
}
// This code belongs inside the GoogleMap content block, but outside of
// the 'when' statement
MapEffect(key1 = true) {map ->
val layer = KmlLayer(map, R.raw.mountain_ranges, context)
layer.addLayerToMap()
}
}
地図の縮尺を追加する
最後のタスクとして、地図に縮尺を追加します。ScaleBar
は、地図に追加できるスケール コンポーザブルを実装します。ScaleBar
は
@GoogleMapComposable
であるため、GoogleMap
コンテンツに追加できません。代わりに、地図を保持する Box
に追加します。
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
アプリを実行して、完全に実装された Codelab を確認します。
14. 解答コードを取得する
この Codelab の完成したコードをダウンロードするには、以下のコマンドを使用します。
git
がインストールされている場合は、リポジトリのクローンを作成します。
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
あるいは、以下のボタンをクリックしてソースコードをダウンロードすることもできます。
- コードを入手したら、Android Studio の
solution
ディレクトリにあるプロジェクトを開いてみましょう。
15. 完了
これで、ここまで多くの内容を学習し、Maps SDK for Android で提供されている主要な機能についての理解を深めていただけたと思います。
その他の情報
- Maps SDK for Android - Android アプリ用に動的でインタラクティブな地図、位置情報、地理空間のエクスペリエンスを作成し、カスタマイズすることができます。
- Maps Compose ライブラリ - Jetpack Compose で使用できる、アプリをビルドするためのコンポーズ可能な関数とデータ型のセットがオープンソースとして用意されています。
- android-maps-compose - この Codelab などで説明されているすべての機能を示す GitHub のサンプルコードです。
- Google Maps Platform で Android アプリをビルドするためのその他の Kotlin Codelab