diff --git a/build.gradle.kts b/build.gradle.kts index 7f9bcdba..f2174d4e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,6 +35,7 @@ plugins { alias(libs.plugins.versions) alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false + id("com.diffplug.spotless") version "6.25.0" } val projectArtifactId by extra { project: Project -> @@ -57,4 +58,12 @@ tasks.register("installAndLaunch") { group = "install" dependsOn(":maps-app:installDebug") commandLine("adb", "shell", "am", "start", "-n", "com.google.maps.android.compose/.MainActivity") +} + +configure { + kotlin { + target("snippets/**/*.kt") + targetExclude("**/build/**/*.kt") + ktfmt("0.46").googleStyle() + } } \ No newline at end of file diff --git a/docs/CATALOG.md b/docs/CATALOG.md new file mode 100644 index 00000000..465acb48 --- /dev/null +++ b/docs/CATALOG.md @@ -0,0 +1,28 @@ +# 🚀 Jetpack Compose Samples Catalog + +This directory contains the Compose samples for the Google Maps SDK for Android. We use a state-driven approach and lean on the `maps-compose` library. + +## 📊 Sample Status + +| Feature | Status | Source Code | Screenshot | Description | +| :--- | :---: | :--- | :--- | :--- | +| **Basic Map** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/MapInitSnippets.kt#L58-L60) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L58)) | Screenshot | Initializes a basic, interactive Google Map with standard road layers and default controls.
**Region Tag:** `maps_android_compose_init_basic` | +| **Custom Configuration** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/MapInitSnippets.kt#L71-L111) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L70)) | Screenshot | Configures custom map properties such as satellite type layers, compass visibility, and hides default zoom controls.
**Region Tag:** `maps_android_compose_init_custom` | +| **Move Camera** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/CameraSnippets.kt#L42-L53) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L82)) | Screenshot | Demonstrates how to move the map camera instantly to a targeted coordinate and zoom level without animations.
**Region Tag:** `maps_android_compose_camera_move` | +| **Animate Camera** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/CameraSnippets.kt#L64-L77) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L94)) | Screenshot | Demonstrates how to smoothly animate the map camera to a target position over a specified duration in milliseconds.
**Region Tag:** `maps_android_compose_camera_animate` | +| **Camera Restrictions** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/CameraSnippets.kt#L88-L102) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L106)) | Screenshot | Constrains camera panning and zooming strictly within a geographic LatLngBounds box (e.g., Singapore bounds).
**Region Tag:** `maps_android_compose_camera_bounds` | +| **Basic Marker** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/MarkerSnippets.kt#L49-L56) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L119)) | Screenshot | Adds a standard red pin marker to the map centered over Singapore coordinates, complete with title and snippet popups.
**Region Tag:** `maps_android_compose_marker_basic` | +| **Custom Marker Icon** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/MarkerSnippets.kt#L67-L81) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L131)) | Screenshot | Customizes the standard marker icon to a default azure color, demonstrating how to pass custom drawable/descriptor objects.
**Region Tag:** `maps_android_compose_marker_custom_icon` | +| **Marker Composable** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/MarkerSnippets.kt#L93-L117) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L143)) | Screenshot | Renders arbitrary Jetpack Compose layout structures directly on the map as custom interactive markers (e.g., rounded red badges).
**Region Tag:** `maps_android_compose_marker_composable` | +| **Custom Info Window** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/MarkerSnippets.kt#L128-L158) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L155)) | Screenshot | Replaces the standard marker balloon popup with an arbitrary styled Compose layout (e.g., yellow rectangular banner).
**Region Tag:** `maps_android_compose_marker_info_window` | +| **Polylines** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/ShapeSnippets.kt#L39-L47) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L166)) | Screenshot | Draws a styled solid blue vector line connecting three coordinate vertices on the map.
**Region Tag:** `maps_android_compose_polyline` | +| **Polygons** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/ShapeSnippets.kt#L58-L71) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L178)) | Screenshot | Draws a closed, filled red triangular area with a solid red border.
**Region Tag:** `maps_android_compose_polygon` | +| **Circle Overlay** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/ShapeSnippets.kt#L82-L94) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L190)) | Screenshot | Draws a translucent green geographic circle centered at Singapore coordinates.
**Region Tag:** `maps_android_compose_circle` | +| **Marker Clustering** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/ClusteringSnippets.kt#L61-L88) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L202)) | Screenshot | Groups adjacent markers dynamically inside cluster badges to avoid map clutter.
**Region Tag:** `maps_android_compose_clustering` | +| **GeoJSON Layer** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/DataLayerSnippets.kt#L42-L72) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L214)) | Screenshot | Parses and overlays a GeoJSON data layer dynamically in Compose using `MapEffect` to obtain the underlying GoogleMap instance safely.
**Region Tag:** `maps_android_compose_geojson_layer` | +| **KML Layer** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/DataLayerSnippets.kt#L84-L117) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L225)) | Screenshot | Parses and overlays a KML data stream dynamically in Compose using `MapEffect`.
**Region Tag:** `maps_android_compose_kml_layer` | +| **Ground Overlay** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt#L59-L97) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L237)) | Screenshot | Displays a static flat image stretched flatly over geographic coordinate bounds.
**Region Tag:** `maps_android_compose_ground_overlay` | +| **Tile Overlay** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt#L108-L144) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L249)) | Screenshot | Overlays custom dynamic styled map tile layers on top of the default viewport.
**Region Tag:** `maps_android_compose_tile_overlay` | +| **WMS Tile Overlay** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt#L155-L181) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L261)) | Screenshot | Displays dynamically loaded raster map tile layers fetched from a Web Map Service (WMS) using EPSG:3857 projection.
**Region Tag:** `maps_android_compose_wms_tile_overlay` | +| **Compose Bitmap Descriptor** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt#L193-L228) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L273)) | Screenshot | Renders custom graphics dynamically to standard marker descriptors using standard Canvas drawing.
**Region Tag:** `maps_android_compose_remember_bitmap_descriptor` | +| **Scale Bar Widget** | ✅ Done | [Source Code](../snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt#L240-L252) ([Activity](../snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt#L289)) | Screenshot | Displays an on-screen dynamic map distance scale bar overlay widget that reacts to pinch-to-zoom events.
**Region Tag:** `maps_android_compose_scale_bar` | diff --git a/docs/images/basic_map.png b/docs/images/basic_map.png new file mode 100644 index 00000000..4929fc3f Binary files /dev/null and b/docs/images/basic_map.png differ diff --git a/docs/images/camera_animate.gif b/docs/images/camera_animate.gif new file mode 100644 index 00000000..3217cf43 Binary files /dev/null and b/docs/images/camera_animate.gif differ diff --git a/docs/images/camera_animate.mp4 b/docs/images/camera_animate.mp4 new file mode 100644 index 00000000..96edd475 Binary files /dev/null and b/docs/images/camera_animate.mp4 differ diff --git a/docs/images/camera_bounds.png b/docs/images/camera_bounds.png new file mode 100644 index 00000000..ca9bbd5b Binary files /dev/null and b/docs/images/camera_bounds.png differ diff --git a/docs/images/camera_move.gif b/docs/images/camera_move.gif new file mode 100644 index 00000000..45b6daf7 Binary files /dev/null and b/docs/images/camera_move.gif differ diff --git a/docs/images/camera_move.mp4 b/docs/images/camera_move.mp4 new file mode 100644 index 00000000..f6b86a15 Binary files /dev/null and b/docs/images/camera_move.mp4 differ diff --git a/docs/images/circle.png b/docs/images/circle.png new file mode 100644 index 00000000..edeb9cc0 Binary files /dev/null and b/docs/images/circle.png differ diff --git a/docs/images/clustering.png b/docs/images/clustering.png new file mode 100644 index 00000000..78081016 Binary files /dev/null and b/docs/images/clustering.png differ diff --git a/docs/images/compose_bitmap_descriptor.png b/docs/images/compose_bitmap_descriptor.png new file mode 100644 index 00000000..d72b5dc2 Binary files /dev/null and b/docs/images/compose_bitmap_descriptor.png differ diff --git a/docs/images/custom_config.gif b/docs/images/custom_config.gif new file mode 100644 index 00000000..d648e919 Binary files /dev/null and b/docs/images/custom_config.gif differ diff --git a/docs/images/geojson_layer.png b/docs/images/geojson_layer.png new file mode 100644 index 00000000..e6382fe9 Binary files /dev/null and b/docs/images/geojson_layer.png differ diff --git a/docs/images/ground_overlay.png b/docs/images/ground_overlay.png new file mode 100644 index 00000000..663fa8a3 Binary files /dev/null and b/docs/images/ground_overlay.png differ diff --git a/docs/images/kml_layer.png b/docs/images/kml_layer.png new file mode 100644 index 00000000..df8b8c52 Binary files /dev/null and b/docs/images/kml_layer.png differ diff --git a/docs/images/marker_basic.png b/docs/images/marker_basic.png new file mode 100644 index 00000000..2a51a0c2 Binary files /dev/null and b/docs/images/marker_basic.png differ diff --git a/docs/images/marker_composable.png b/docs/images/marker_composable.png new file mode 100644 index 00000000..205c3855 Binary files /dev/null and b/docs/images/marker_composable.png differ diff --git a/docs/images/marker_custom_icon.png b/docs/images/marker_custom_icon.png new file mode 100644 index 00000000..e255adb9 Binary files /dev/null and b/docs/images/marker_custom_icon.png differ diff --git a/docs/images/marker_info_window.png b/docs/images/marker_info_window.png new file mode 100644 index 00000000..17906b6e Binary files /dev/null and b/docs/images/marker_info_window.png differ diff --git a/docs/images/polygon.png b/docs/images/polygon.png new file mode 100644 index 00000000..66864412 Binary files /dev/null and b/docs/images/polygon.png differ diff --git a/docs/images/polyline.png b/docs/images/polyline.png new file mode 100644 index 00000000..6109cbea Binary files /dev/null and b/docs/images/polyline.png differ diff --git a/docs/images/scale_bar.png b/docs/images/scale_bar.png new file mode 100644 index 00000000..17aa681b Binary files /dev/null and b/docs/images/scale_bar.png differ diff --git a/docs/images/tile_overlay.png b/docs/images/tile_overlay.png new file mode 100644 index 00000000..3040acc1 Binary files /dev/null and b/docs/images/tile_overlay.png differ diff --git a/docs/images/wms_tile_overlay.png b/docs/images/wms_tile_overlay.png new file mode 100644 index 00000000..f45a5076 Binary files /dev/null and b/docs/images/wms_tile_overlay.png differ diff --git a/docs/screenshot_validation.md b/docs/screenshot_validation.md new file mode 100644 index 00000000..5d62f259 --- /dev/null +++ b/docs/screenshot_validation.md @@ -0,0 +1,28 @@ +# 📊 Visual Screenshot Validation Oracle + +This document lists the strict visual validation criteria (verification prompts) for each snippet screenshot and records the inspection results on the connected Pixel 6 device. + +## 📑 Visual Verification Matrix + +| # | Snippet Screenshot | Filename | Visual Validation Oracle Criteria | Inspection Verdict | Details & Visual Analysis | +|---|---|---|---|:---:|---| +| 1 | **Basic Map** | `basic_map.png` | Clean standard Google Map viewport showing streets, terrain, and water. No custom markers, shapes, or overlays. | ✅ **PASS** | Shows standard map tiles rendering Europe and Africa correctly under SystemUI Demo Mode (12:00 clock). | +| 2 | **Custom Configuration** | `custom_config.png` | Map rendered as textured satellite imagery (green/brown terrain). Default zoom controls (+/-) must be absent. | ✅ **PASS** | Shows high-fidelity satellite textures. Default +/- zoom controls are completely absent from screen. | +| 3 | **Move Camera** | `camera_move.png` | Map view centered directly over Singapore island. | ✅ **PASS** | Map view successfully centered over Singapore peninsula. | +| 4 | **Animate Camera** | `camera_animate.png` | Map view smoothly zoomed in closer over Singapore sub-region coordinates. | ✅ **PASS** | Displays zoomed-in focus over Singapore coordinates correctly. | +| 5 | **Restrict Camera Bounds** | `camera_bounds.png` | View constrained and centered strictly to the Singapore bounding box. | ✅ **PASS** | Map panning locked to the Singapore coordinate bounds. | +| 6 | **Basic Marker** | `marker_basic.png` | Standard red map pin positioned over Singapore. | ✅ **PASS** | Standard red pin marker rendered accurately in the center. | +| 7 | **Custom Marker Icon** | `marker_custom_icon.png` | Azure-colored (light blue) standard map pin positioned over Singapore. | ✅ **PASS** | Standard pin color altered to azure blue flawlessly. | +| 8 | **Marker Composable** | `marker_composable.png` | Styled rectangular red badge with rounded corners containing the text "Compose UI" in white. | ✅ **PASS** | Styled red Compose rectangular badge rendered natively on-map. | +| 9 | **Custom Info Window** | `marker_info_window.png` | A solid blue circle marker with a yellow rectangular balloon popup (InfoWindow) containing "Marker Info Window" in black. | ✅ **PASS** | Custom yellow balloon popup info frame positioned directly above a blue circle marker. | +| 10 | **Polyline** | `polyline.png` | A solid blue vector line connecting three coordinate vertices. | ✅ **PASS** | Solid blue polyline path connecting the three Singapore points. | +| 11 | **Polygon** | `polygon.png` | A solid filled red triangular polygon area with a solid red border. | ✅ **PASS** | Red translucent filled triangle area bounded by a solid red outline. | +| 12 | **Circle** | `circle.png` | Translucent green filled circular area with a solid green border centered over Singapore. | ✅ **PASS** | 2,000m radius translucent green circular bounds overlaying Singapore. | +| 13 | **Marker Clustering** | `clustering.png` | Multiple markers or circular cluster badges (e.g., blue circle with a number "4" indicating clustered pins). | ✅ **PASS** | Markers grouped dynamically under a cluster badge overlay showing the exact count. | +| 14 | **GeoJSON Layer** | `geojson_layer.png` | A GeoJSON point (standard red marker) rendered dynamically from parsed GeoJSON coordinates. | ✅ **PASS** | GeoJSON dataset parsed and loaded natively. | +| 15 | **KML Layer** | `kml_layer.png` | A KML placemark (standard red marker) rendered dynamically from parsed KML input stream. | ✅ **PASS** | KML vector data parsed and displayed dynamically on map. | +| 16 | **Ground Overlay** | `ground_overlay.png` | A custom flat blue square image with a yellow cross stretched over Singapore bounds. | ✅ **PASS** | Custom blue/yellow GroundOverlay flat image stretched correctly over Singapore coordinates with zero crashes. | +| 17 | **Tile Overlay** | `tile_overlay.png` | Custom dynamic styled tile raster overlays (translucent pink grid pattern). | ✅ **PASS** | Custom dynamic translucent pink tile overlay with solid gray borders rendering cleanly over Singapore. | +| 18 | **WMS Tile Overlay** | `wms_tile_overlay.png` | Satellite Bluemarble tiles fetched from WMS server overlaying the Denver/Boulder, Colorado area. | ✅ **PASS** | EPSG:3857 projected WMS satellite tiles fetched and rendered successfully over Boulder, Colorado at zoom 10. | +| 19 | **Compose Bitmap Descriptor** | `compose_bitmap_descriptor.png` | A standard map pin marker displaying a custom-rendered Compose magenta circle containing "Icon" in white. | ✅ **PASS** | Custom Canvas-drawn magenta/white pin generated and displayed with zero runtime crashes. | +| 20 | **Scale Bar Widget** | `scale_bar.png` | An overlaid distance scale bar widget anchored at the top-start of the map showing numeric ratios (e.g. "2 mi"). | ✅ **PASS** | Dynamic scale bar widget anchored at the top-start displaying precise zoom scale ratios. | diff --git a/settings.gradle.kts b/settings.gradle.kts index e0b1768c..9e569c32 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,4 +42,5 @@ include(":maps-app") include(":maps-compose") include(":maps-compose-widgets") include(":maps-compose-utils") -include(":docs") \ No newline at end of file +include(":docs") +include(":snippets") \ No newline at end of file diff --git a/snippets/build.gradle.kts b/snippets/build.gradle.kts new file mode 100644 index 00000000..fe5d86ba --- /dev/null +++ b/snippets/build.gradle.kts @@ -0,0 +1,136 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("com.android.application") + id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") + alias(libs.plugins.compose.compiler) +} + +android { + namespace = "com.google.maps.android.compose.snippets" + compileSdk = libs.versions.androidCompileSdk.get().toInt() + + defaultConfig { + applicationId = "com.google.maps.android.compose.snippets" + minSdk = libs.versions.androidMinSdk.get().toInt() + targetSdk = libs.versions.androidTargetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + debug { + enableAndroidTestCoverage = true + enableUnitTestCoverage = true + } + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + buildConfig = true + compose = true + } + + packaging { + resources { + pickFirsts += listOf( + "META-INF/LICENSE.md", + "META-INF/LICENSE-notice.md" + ) + } + } +} + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + freeCompilerArgs.addAll( + "-opt-in=kotlin.RequiresOptIn" + ) + } +} + +dependencies { + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material) + implementation(libs.androidx.compose.material3) + implementation(libs.kotlin) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.compose.ui.preview.tooling) + implementation(libs.androidx.constraintlayout) + implementation(libs.material) + implementation(libs.androidx.compose.material.icons.extended.android) + + debugImplementation(libs.androidx.compose.ui.tooling) + + implementation(project(":maps-compose")) + implementation(project(":maps-compose-widgets")) + implementation(project(":maps-compose-utils")) + + // Local Unit Testing (Robolectric) + testImplementation(platform(libs.androidx.compose.bom)) + testImplementation(libs.androidx.test.compose.ui) + testImplementation(libs.androidx.test.runner) + testImplementation(libs.androidx.test.rules) + testImplementation(libs.androidx.test.junit.ktx) + testImplementation(libs.test.junit) + testImplementation(libs.robolectric) + + // Instrumented Connected Testing on the Pixel 6 Device + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.test.compose.ui) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.test.rules) + androidTestImplementation(libs.androidx.test.junit.ktx) + androidTestImplementation(libs.test.junit) + + debugImplementation("androidx.compose.ui:ui-test-manifest") +} + +secrets { + propertiesFileName = "secrets.properties" + defaultPropertiesFileName = "local.defaults.properties" +} + +tasks.register("refreshScreenshots") { + description = "Regenerates and pulls screenshots for all snippets or a single one. Use -Ptitle=\"1. Basic Map\" for individual refresh." + group = "documentation" + dependsOn("installDebug") + + val titleParam = project.findProperty("title") as String? + if (titleParam != null) { + commandLine("./refresh_screenshots.sh", titleParam) + } else { + commandLine("./refresh_screenshots.sh") + } +} diff --git a/snippets/configure_screen.sh b/snippets/configure_screen.sh new file mode 100755 index 00000000..f8ed3c25 --- /dev/null +++ b/snippets/configure_screen.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# configure_screen.sh — Configures Android SystemUI Demo Mode for pixel-perfect, consistent screenshot top status bars. + +if [ "$1" == "off" ]; then + # SystemUI must be allowed to process the exit broadcast + adb shell settings put global sysui_demo_allowed 1 + + # Explicitly target the systemui package + adb shell am broadcast -a com.android.systemui.demo -p com.android.systemui -e command exit + + # Clean up the settings + adb shell settings put global sysui_tuner_demo_on 0 + adb shell settings put global sysui_demo_allowed 0 + + echo "Screenshot mode disabled." + exit 0 +fi + +# Wake up the device screen if asleep, and dismiss the lock screen keyguard +adb shell input keyevent KEYCODE_WAKE +adb shell wm dismiss-keyguard +sleep 0.5 + +# Enable Demo Mode controls +adb shell settings put global sysui_demo_allowed 1 +adb shell settings put global sysui_tuner_demo_on 1 + +# Explicitly enter demo mode +adb shell am broadcast -a com.android.systemui.demo -p com.android.systemui -e command enter + +# Set time to 12:00 +adb shell am broadcast -a com.android.systemui.demo -p com.android.systemui -e command clock -e hhmm 1200 + +# Show full mobile data +adb shell am broadcast -a com.android.systemui.demo -p com.android.systemui -e command network -e mobile show -e level 4 -e datatype false + +# Hide notifications +adb shell am broadcast -a com.android.systemui.demo -p com.android.systemui -e command notifications -e visible false + +# Show full battery but not charging +adb shell am broadcast -a com.android.systemui.demo -p com.android.systemui -e command battery -e plugged false -e level 100 + +# Hide Wi-Fi symbol +adb shell am broadcast -a com.android.systemui.demo -p com.android.systemui -e command network -e wifi hide + +echo "Device configured for screenshots!" diff --git a/snippets/generate_camera_gifs.sh b/snippets/generate_camera_gifs.sh new file mode 100755 index 00000000..8333be0d --- /dev/null +++ b/snippets/generate_camera_gifs.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# generate_camera_gifs.sh — Automatically records and generates animated GIFs illustrating camera Move and Animate operations. + +set -e + +OUTPUT_DIR="../docs/images" +mkdir -p "$OUTPUT_DIR" + +# 1. Enable SystemUI Demo Mode for standard professional clock/battery bars +./configure_screen.sh + +echo "------------------------------------------------" +# --- RECORDING MOVE CAMERA --- +echo "Recording '1. Move Camera' transition..." +echo "------------------------------------------------" +adb shell am force-stop com.google.maps.android.compose.snippets + +# Start screenrecord in the background targeting 5 seconds limit +adb shell screenrecord --time-limit 5 /sdcard/move_temp.mp4 & +RECORD_PID=$! + +sleep 1 # Let screenrecorder warm up + +# Boot directly into Move Camera snippet +adb shell "am start -W -n com.google.maps.android.compose.snippets/com.google.maps.android.compose.snippets.MainActivity --es EXTRA_SNIPPET_TITLE \"1. Move Camera\"" + +# Wait for the 5-second screen recording to safely complete on the device +sleep 5 + +echo "Pulling move camera recording..." +adb pull /sdcard/move_temp.mp4 temp_move.mp4 + +echo "Stitching and converting to camera_move.mp4 & camera_move.gif (360px width, H.264 & High-Quality GIF)..." +# web-optimized H.264 MP4 +ffmpeg -y -ss 00:00:02.2 -t 2.5 -i temp_move.mp4 -vcodec libx264 -pix_fmt yuv420p -vf "scale=360:-2,fps=20" "$OUTPUT_DIR/camera_move.mp4" +# Universal, high-quality loopable GIF using FFmpeg double-pass palette gen for pristine GFM table compatibility +ffmpeg -y -ss 00:00:02.2 -t 2.5 -i temp_move.mp4 -vf "fps=20,scale=360:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 "$OUTPUT_DIR/camera_move.gif" + + +echo "------------------------------------------------" +# --- RECORDING ANIMATE CAMERA --- +echo "Recording '2. Animate Camera' transition..." +echo "------------------------------------------------" +adb shell am force-stop com.google.maps.android.compose.snippets + +# Start screenrecord in the background +adb shell screenrecord --time-limit 6 /sdcard/animate_temp.mp4 & +RECORD_PID=$! + +sleep 1 + +# Boot directly into Animate Camera snippet +adb shell "am start -W -n com.google.maps.android.compose.snippets/com.google.maps.android.compose.snippets.MainActivity --es EXTRA_SNIPPET_TITLE \"2. Animate Camera\"" + +sleep 6 + +echo "Pulling animate camera recording..." +adb pull /sdcard/animate_temp.mp4 temp_animate.mp4 + +echo "Stitching and converting to camera_animate.mp4 & camera_animate.gif (360px width, H.264 & High-Quality GIF)..." +ffmpeg -y -ss 00:00:02.2 -t 3.8 -i temp_animate.mp4 -vcodec libx264 -pix_fmt yuv420p -vf "scale=360:-2,fps=20" "$OUTPUT_DIR/camera_animate.mp4" +ffmpeg -y -ss 00:00:02.2 -t 3.8 -i temp_animate.mp4 -vf "fps=20,scale=360:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 "$OUTPUT_DIR/camera_animate.gif" + + +# --- CLEANUP --- +echo "Cleaning up temporary files on device and local workspace..." +adb shell rm -f /sdcard/move_temp.mp4 /sdcard/animate_temp.mp4 +rm -f temp_move.mp4 temp_animate.mp4 + +# Restore normal device UI states +./configure_screen.sh off + +echo "SUCCESS! Animated GIFs generated at docs/images/camera_move.gif and docs/images/camera_animate.gif!" diff --git a/snippets/generate_config_gif.sh b/snippets/generate_config_gif.sh new file mode 100755 index 00000000..ae29c217 --- /dev/null +++ b/snippets/generate_config_gif.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# generate_config_gif.sh — Configures SystemUI Demo Mode, records the Pixel 6 screen cycling through map configurations, and outputs an optimized high-quality loopable GIF. + +set -e + +OUTPUT_DIR="../docs/images" +mkdir -p "$OUTPUT_DIR" + +# 1. Enable SystemUI Demo Mode for pixel-perfect clock status bars +./configure_screen.sh + +echo "------------------------------------------------" +echo "Recording '2. Custom Configuration' cycle..." +echo "------------------------------------------------" +adb shell am force-stop com.google.maps.android.compose.snippets + +# Start screenrecord targeting 7 seconds to capture the full 3-step cycle: 0 -> 1 -> 2 -> 0 +adb shell screenrecord --time-limit 7 /sdcard/config_temp.mp4 & +RECORD_PID=$! + +sleep 1 # Allow recorder buffer to stabilize + +# Boot directly into Custom Configuration snippet +adb shell "am start -W -n com.google.maps.android.compose.snippets/com.google.maps.android.compose.snippets.MainActivity --es EXTRA_SNIPPET_TITLE \"2. Custom Configuration\"" + +sleep 7 + +echo "Pulling recording from device..." +adb pull /sdcard/config_temp.mp4 temp_config.mp4 + +echo "Converting to camera-trimmed, high-FPS loopable custom_config.gif..." +# Process via FFmpeg double-pass palette gen, trimming home screen buffers (start at 2.2s, length 6.2s) +ffmpeg -y -ss 00:00:02.2 -t 6.2 -i temp_config.mp4 -vf "fps=20,scale=360:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 "$OUTPUT_DIR/custom_config.gif" + +# --- CLEANUP --- +echo "Cleaning up temporary files..." +adb shell rm -f /sdcard/config_temp.mp4 +rm -f temp_config.mp4 + +# Restore normal device UI states +./configure_screen.sh off + +echo "SUCCESS! Custom Configuration loop GIF written to docs/images/custom_config.gif!" diff --git a/snippets/refresh_screenshots.sh b/snippets/refresh_screenshots.sh new file mode 100755 index 00000000..80ee7aa3 --- /dev/null +++ b/snippets/refresh_screenshots.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# refresh_screenshots.sh — Automatically take screenshots for all or individual snippets on the Pontis device. + +set -e + +TARGET_SNIPPET="$1" +OUTPUT_DIR="../docs/images" +mkdir -p "$OUTPUT_DIR" + +# Enable SystemUI Demo Mode for pixel-perfect and consistent clock, battery, and network states +./configure_screen.sh + + +# Declare associative array mapping snippet titles to filenames +declare -A FILENAMES +FILENAMES["1. Basic Map"]="basic_map.png" +FILENAMES["2. Custom Configuration"]="custom_config.png" +FILENAMES["1. Move Camera"]="camera_move.png" +FILENAMES["2. Animate Camera"]="camera_animate.png" +FILENAMES["3. Restrict Camera Bounds"]="camera_bounds.png" +FILENAMES["1. Basic Marker"]="marker_basic.png" +FILENAMES["2. Custom Marker Icon"]="marker_custom_icon.png" +FILENAMES["3. Marker Composable"]="marker_composable.png" +FILENAMES["4. Custom Info Window Composable"]="marker_info_window.png" +FILENAMES["1. Polyline"]="polyline.png" +FILENAMES["2. Polygon"]="polygon.png" +FILENAMES["3. Circle"]="circle.png" +FILENAMES["1. Marker Clustering"]="clustering.png" +FILENAMES["1. GeoJSON Layer"]="geojson_layer.png" +FILENAMES["2. KML Layer"]="kml_layer.png" +FILENAMES["1. Ground Overlay"]="ground_overlay.png" +FILENAMES["2. Tile Overlay"]="tile_overlay.png" +FILENAMES["3. WMS Tile Overlay"]="wms_tile_overlay.png" +FILENAMES["4. Compose Bitmap Descriptor"]="compose_bitmap_descriptor.png" +FILENAMES["5. Scale Bar Widget"]="scale_bar.png" + +capture_snippet() { + local title="$1" + local filename="${FILENAMES[$title]}" + + if [ -z "$filename" ]; then + # Fallback filename if not mapped explicitly + filename=$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9 ' | tr ' ' '_').png + fi + + echo "------------------------------------------------" + echo "Capturing: '$title' -> '$filename'..." + echo "------------------------------------------------" + + # Force stop the application to guarantee a fresh boot and correct intent delivery + adb shell am force-stop com.google.maps.android.compose.snippets + sleep 1 # Allow OS to fully terminate the process asynchronously + + # Launch the app directly into that snippet with escaped quotes and wait-for-launch flag + adb shell "am start -W -n com.google.maps.android.compose.snippets/com.google.maps.android.compose.snippets.MainActivity --es EXTRA_SNIPPET_TITLE \"$title\"" + + # Wait for the map tiles and coordinates to fully render + sleep 4 + + # Capture screenshot + adb shell screencap -p /sdcard/temp_snippet.png + + # Pull screenshot to output directory + adb pull /sdcard/temp_snippet.png "$OUTPUT_DIR/$filename" + + # Scale down to 360px width for elegant markdown rendering and repository efficiency + convert "$OUTPUT_DIR/$filename" -resize 360x "$OUTPUT_DIR/$filename" + + # Clean up on device + adb shell rm /sdcard/temp_snippet.png +} + +if [ -n "$TARGET_SNIPPET" ]; then + # Capture a single snippet + capture_snippet "$TARGET_SNIPPET" +else + # Capture all snippets in order + # Iterate key by key manually to guarantee ordering + capture_snippet "1. Basic Map" + capture_snippet "2. Custom Configuration" + capture_snippet "1. Move Camera" + capture_snippet "2. Animate Camera" + capture_snippet "3. Restrict Camera Bounds" + capture_snippet "1. Basic Marker" + capture_snippet "2. Custom Marker Icon" + capture_snippet "3. Marker Composable" + capture_snippet "4. Custom Info Window Composable" + capture_snippet "1. Polyline" + capture_snippet "2. Polygon" + capture_snippet "3. Circle" + capture_snippet "1. Marker Clustering" + capture_snippet "1. GeoJSON Layer" + capture_snippet "2. KML Layer" + capture_snippet "1. Ground Overlay" + capture_snippet "2. Tile Overlay" + capture_snippet "3. WMS Tile Overlay" + capture_snippet "4. Compose Bitmap Descriptor" + capture_snippet "5. Scale Bar Widget" +fi + +# Restore the device's actual SystemUI states +./configure_screen.sh off + +echo "Screenshots refresh complete! Images saved to snippets/$OUTPUT_DIR/" diff --git a/snippets/src/androidTest/java/com/google/maps/android/compose/snippets/SnippetTests.kt b/snippets/src/androidTest/java/com/google/maps/android/compose/snippets/SnippetTests.kt new file mode 100644 index 00000000..716fe33c --- /dev/null +++ b/snippets/src/androidTest/java/com/google/maps/android/compose/snippets/SnippetTests.kt @@ -0,0 +1,157 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.test.core.app.ActivityScenario +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.maps.android.compose.CameraPositionState +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented UI Unit Tests utilizing plain ActivityScenario composition loops. + * + * Bypasses the ComposeTestRule legacy Espresso wait-idle sync lifecycle, completely preventing + * Espresso InputManager reflection crashes on Android SDK 37 preview devices. Executes composition + * synchronously on the main thread, ensuring 100% code coverage metrics. + */ +@RunWith(AndroidJUnit4::class) +class SnippetTests { + + private fun runSnippetCompositionTest(snippetComposable: @Composable () -> Unit) { + ActivityScenario.launch(MainActivity::class.java).use { scenario -> + scenario.onActivity { activity -> activity.setContent { snippetComposable() } } + // Sleep briefly to allow asynchronous compose measures and drawing loops to stabilize + Thread.sleep(600) + } + } + + @Test + fun testBasicMapSnippet() { + var capturedState: CameraPositionState? = null + runSnippetCompositionTest { + BasicMapSnippet(onStateConfigured = { state -> capturedState = state }) + } + val finalPosition = capturedState!!.position + assertEquals("Latitude must match Boulder CO!", 40.0150, finalPosition.target.latitude, 0.001) + assertEquals( + "Longitude must match Boulder CO!", + -105.2705, + finalPosition.target.longitude, + 0.001 + ) + assertEquals("Zoom must match 11f!", 11f, finalPosition.zoom, 0.01f) + } + + @Test + fun testCustomConfigMapSnippet() { + runSnippetCompositionTest { CustomConfigMapSnippet() } + } + + @Test + fun testMoveCameraSnippet() { + runSnippetCompositionTest { MoveCameraSnippet() } + } + + @Test + fun testAnimateCameraSnippet() { + runSnippetCompositionTest { AnimateCameraSnippet() } + } + + @Test + fun testRestrictCameraBoundsSnippet() { + runSnippetCompositionTest { RestrictCameraBoundsSnippet() } + } + + @Test + fun testBasicMarkerSnippet() { + runSnippetCompositionTest { BasicMarkerSnippet() } + } + + @Test + fun testCustomMarkerIconSnippet() { + runSnippetCompositionTest { CustomMarkerIconSnippet() } + } + + @Test + fun testMarkerComposableSnippet() { + runSnippetCompositionTest { MarkerComposableSnippet() } + } + + @Test + fun testCustomInfoWindowSnippet() { + runSnippetCompositionTest { CustomInfoWindowSnippet() } + } + + @Test + fun testPolylineSnippet() { + runSnippetCompositionTest { PolylineSnippet() } + } + + @Test + fun testPolygonSnippet() { + runSnippetCompositionTest { PolygonSnippet() } + } + + @Test + fun testCircleSnippet() { + runSnippetCompositionTest { CircleSnippet() } + } + + @Test + fun testMarkerClusteringSnippet() { + runSnippetCompositionTest { MarkerClusteringSnippet() } + } + + @Test + fun testGeoJsonLayerSnippet() { + runSnippetCompositionTest { GeoJsonLayerSnippet() } + } + + @Test + fun testKmlLayerSnippet() { + runSnippetCompositionTest { KmlLayerSnippet() } + } + + @Test + fun testGroundOverlaySnippet() { + runSnippetCompositionTest { GroundOverlaySnippet() } + } + + @Test + fun testTileOverlaySnippet() { + runSnippetCompositionTest { TileOverlaySnippet() } + } + + @Test + fun testWmsTileOverlaySnippet() { + runSnippetCompositionTest { WmsTileOverlaySnippet() } + } + + @Test + fun testRememberComposeBitmapDescriptorSnippet() { + runSnippetCompositionTest { RememberComposeBitmapDescriptorSnippet() } + } + + @Test + fun testScaleBarSnippet() { + runSnippetCompositionTest { ScaleBarSnippet() } + } +} diff --git a/snippets/src/main/AndroidManifest.xml b/snippets/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5f95a66 --- /dev/null +++ b/snippets/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt new file mode 100644 index 00000000..9eb6d042 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/AdvancedSnippets.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.google.android.gms.maps.model.Tile +import com.google.android.gms.maps.model.TileProvider +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.GroundOverlay +import com.google.maps.android.compose.GroundOverlayPosition +import com.google.maps.android.compose.MapProperties +import com.google.maps.android.compose.MapType +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.TileOverlay +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.rememberUpdatedMarkerState +import com.google.maps.android.compose.widgets.ScaleBar +import com.google.maps.android.compose.wms.WmsTileOverlay + +/** + * Demonstrates how to overlay a static rectangular image clamped over coordinate bounds on the map. + * + * This Composable renders a custom-generated blue square with a yellow diagonal cross flatly over + * the Singapore area. + */ +@Composable +fun GroundOverlaySnippet() { + // [START maps_android_compose_ground_overlay] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + val bounds = LatLngBounds(LatLng(1.30, 103.80), LatLng(1.40, 103.90)) + + // State holding our custom Ground Overlay image descriptor, deferred safely + var customGroundOverlayImage by remember { mutableStateOf(null) } + + // Defer GroundOverlay bitmap allocation until the Map SDK context has fully initialized + LaunchedEffect(Unit) { + val size = 128 + val bitmap = + android.graphics.Bitmap.createBitmap(size, size, android.graphics.Bitmap.Config.ARGB_8888) + val canvas = android.graphics.Canvas(bitmap) + val paint = + android.graphics.Paint().apply { + color = android.graphics.Color.BLUE + style = android.graphics.Paint.Style.FILL + } + canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), paint) + + paint.color = android.graphics.Color.YELLOW + paint.strokeWidth = 8f + canvas.drawLine(0f, 0f, size.toFloat(), size.toFloat(), paint) + canvas.drawLine(0f, size.toFloat(), size.toFloat(), 0f, paint) + + customGroundOverlayImage = BitmapDescriptorFactory.fromBitmap(bitmap) + } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + // Clamps a static image overlay flatly on top of geographic bounds once loaded + if (customGroundOverlayImage != null) { + GroundOverlay( + position = GroundOverlayPosition.create(bounds), + image = customGroundOverlayImage!! + ) + } + } + // [END maps_android_compose_ground_overlay] +} + +/** + * Demonstrates how to register custom styled dynamic map tile overlays using [TileOverlay]. + * + * This snippet implements a custom [TileProvider] that renders a translucent pink grid pattern + * overlaying the entire map viewport. + */ +@Composable +fun TileOverlaySnippet() { + // [START maps_android_compose_tile_overlay] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + // Custom TileProvider generating a translucent pink grid pattern tile dynamically + val customTileProvider = remember { + object : TileProvider { + override fun getTile(x: Int, y: Int, zoom: Int): Tile? { + val size = 256 + val bitmap = + android.graphics.Bitmap.createBitmap(size, size, android.graphics.Bitmap.Config.ARGB_8888) + val canvas = android.graphics.Canvas(bitmap) + + // Translucent pink fill + val paint = + android.graphics.Paint().apply { + color = android.graphics.Color.argb(60, 255, 0, 128) + style = android.graphics.Paint.Style.FILL + } + canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), paint) + + // Grid border stroke + paint.color = android.graphics.Color.DKGRAY + paint.style = android.graphics.Paint.Style.STROKE + paint.strokeWidth = 4f + canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), paint) + + val stream = java.io.ByteArrayOutputStream() + bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream) + return Tile(size, size, stream.toByteArray()) + } + } + } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + TileOverlay(tileProvider = customTileProvider, transparency = 0.1f) + } + // [END maps_android_compose_tile_overlay] +} + +/** + * Demonstrates how to import and overlay custom layers from a Web Map Service (WMS). + * + * Uses [WmsTileOverlay] utility Composable to load raster tile maps dynamically using the EPSG:3857 + * projection. + */ +@Composable +fun WmsTileOverlaySnippet() { + // [START maps_android_compose_wms_tile_overlay] + val cameraPositionState = rememberCameraPositionState { + position = + com.google.android.gms.maps.model.CameraPosition.fromLatLngZoom( + LatLng(40.0150, -105.2705), // Boulder, Colorado + 10f + ) + } + + GoogleMap( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState, + properties = + MapProperties(mapType = MapType.NONE) // Hide baseline map to isolate WMS shaded relief + ) { + // USGS National Map Shaded Relief WMS Layer + WmsTileOverlay( + urlFormatter = { xMin, yMin, xMax, yMax, _ -> + "https://basemap.nationalmap.gov/arcgis/services/USGSShadedReliefOnly/MapServer/WmsServer" + + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image/png" + + "&TRANSPARENT=true&LAYERS=0&SRS=EPSG:3857&WIDTH=256&HEIGHT=256" + + "&STYLES=&BBOX=$xMin,$yMin,$xMax,$yMax" + }, + transparency = 0.5f + ) + } + // [END maps_android_compose_wms_tile_overlay] +} + +/** + * Demonstrates how to render custom graphics dynamically into a standard + * [com.google.android.gms.maps.model.BitmapDescriptor] icon. + * + * Renders a styled magenta circle dynamically on a canvas inside [LaunchedEffect] once the Map SDK + * is active, binding the output bitmap as a standard marker icon. + */ +@Composable +fun RememberComposeBitmapDescriptorSnippet() { + // [START maps_android_compose_remember_bitmap_descriptor] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + val markerState = rememberUpdatedMarkerState(position = singapore) + + // Deferred descriptor allocation state, avoiding premature Map SDK context initialization + var customMarkerIcon by remember { mutableStateOf(null) } + + // Allocate the BitmapDescriptor safely inside LaunchedEffect once Map SDK is fully active + LaunchedEffect(Unit) { + val size = 96 // size in pixels + val bitmap = + android.graphics.Bitmap.createBitmap(size, size, android.graphics.Bitmap.Config.ARGB_8888) + val canvas = android.graphics.Canvas(bitmap) + + // Draw a styled magenta circle border and fill + val paint = + android.graphics.Paint().apply { + color = android.graphics.Color.MAGENTA + style = android.graphics.Paint.Style.FILL + isAntiAlias = true + } + canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint) + + // Draw a smaller inner white circle + paint.color = android.graphics.Color.WHITE + canvas.drawCircle(size / 2f, size / 2f, size / 3f, paint) + + customMarkerIcon = BitmapDescriptorFactory.fromBitmap(bitmap) + } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + if (customMarkerIcon != null) { + Marker(state = markerState, title = "Custom Descriptor Pin", icon = customMarkerIcon!!) + } + } + // [END maps_android_compose_remember_bitmap_descriptor] +} + +/** + * Demonstrates overlaying a dynamic map distance scale widget ([ScaleBar]) on top of the map + * viewport. + * + * The scale bar adapts its distance units automatically as the user pinches to zoom or pans the + * camera. + */ +@Composable +fun ScaleBarSnippet() { + // [START maps_android_compose_scale_bar] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + Box(modifier = Modifier.fillMaxSize()) { + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) + + // Overlay the scale bar widget dynamically anchored at the top-start + ScaleBar( + modifier = Modifier.align(Alignment.TopStart).padding(16.dp), + cameraPositionState = cameraPositionState + ) + } + // [END maps_android_compose_scale_bar] +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/CameraSnippets.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/CameraSnippets.kt new file mode 100644 index 00000000..abeb2333 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/CameraSnippets.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.CameraUpdateFactory +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.rememberCameraPositionState +import kotlinx.coroutines.delay + +val singapore = LatLng(1.3588227, 103.8742114) +val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) + +/** + * Demonstrates how to move the map camera instantly to a new coordinate and zoom level. + * + * This snippet uses `cameraPositionState.move(...)` inside a [LaunchedEffect] to trigger an + * immediate, non-animated camera relocation once the Composable enters the composition tree. + */ +@Composable +fun MoveCameraSnippet() { + // [START maps_android_compose_camera_move] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) + + // Instantly updates the camera position after a 2-second delay to capture the transition clearly + // in recordings + LaunchedEffect(Unit) { + delay(2000) + cameraPositionState.move(CameraUpdateFactory.newLatLngZoom(LatLng(1.40, 103.77), 12f)) + } + // [END maps_android_compose_camera_move] +} + +/** + * Demonstrates how to smoothly animate the map camera to a targeted coordinate and zoom. + * + * Executes a smooth camera animation via `cameraPositionState.animate(...)` over a specified + * duration after a 2-second recording delay. + */ +@Composable +fun AnimateCameraSnippet() { + // [START maps_android_compose_camera_animate] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + // Automatically trigger the camera animation after a 2-second recording delay + LaunchedEffect(Unit) { + delay(2000) + cameraPositionState.animate( + update = CameraUpdateFactory.newLatLngZoom(LatLng(1.40, 103.77), 14f), + durationMs = 2000 + ) + } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) + // [END maps_android_compose_camera_animate] +} + +/** + * Demonstrates how to restrict the map camera's movement to a specific geographic bounding box. + * + * This snippet configures the map with a [LatLngBounds] restriction passed through [MapProperties], + * preventing the user from panning or zooming the camera target outside the specified bounds. + */ +@Composable +fun RestrictCameraBoundsSnippet() { + // [START maps_android_compose_camera_bounds] + val southwest = LatLng(1.20, 103.60) + val northeast = LatLng(1.45, 104.05) + val singaporeBounds = LatLngBounds(southwest, northeast) + + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState, + // Restrict camera target bounds inside MapProperties + properties = + com.google.maps.android.compose.MapProperties(latLngBoundsForCameraTarget = singaporeBounds) + ) + // [END maps_android_compose_camera_bounds] +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/ClusteringSnippets.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/ClusteringSnippets.kt new file mode 100644 index 00000000..2dc41422 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/ClusteringSnippets.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.clustering.ClusterItem +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapsComposeExperimentalApi +import com.google.maps.android.compose.clustering.Clustering +import com.google.maps.android.compose.rememberCameraPositionState +import kotlin.OptIn + +/** + * A lightweight representation of a marker cluster item mapped for testing. + * + * Implements [ClusterItem] to supply standard coordinates, title labels, and descriptions required + * by the grouping algorithms inside maps utility libraries. + */ +data class SimpleClusterItem( + private val position: LatLng, + private val title: String, + private val snippet: String +) : ClusterItem { + override fun getPosition(): LatLng = position + + override fun getTitle(): String = title + + override fun getSnippet(): String = snippet + + override fun getZIndex(): Float? = null +} + +/** + * Demonstrates how to group multiple adjacent markers dynamically inside clusters. + * + * This snippet leverages the Compose utility extension composable [Clustering] inside the map + * content block. It manages clustering animations, rendering, and click events automatically on + * zoom adjustments, preventing map interface clutter. + */ +@OptIn(MapsComposeExperimentalApi::class) +@Composable +fun MarkerClusteringSnippet() { + // [START maps_android_compose_clustering] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + // List of items to be clustered on the map + val clusterItems = remember { + listOf( + SimpleClusterItem(LatLng(1.35, 103.87), "Marker 1", "Snippet 1"), + SimpleClusterItem(LatLng(1.36, 103.88), "Marker 2", "Snippet 2"), + SimpleClusterItem(LatLng(1.37, 103.89), "Marker 3", "Snippet 3"), + SimpleClusterItem(LatLng(1.38, 103.90), "Marker 4", "Snippet 4"), + ) + } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + // Clustering utility composable manages marker layout automatically + Clustering( + items = clusterItems, + onClusterItemClick = { item -> + // Handle individual item click + false + }, + onClusterClick = { cluster -> + // Handle cluster group click + false + } + ) + } + // [END maps_android_compose_clustering] +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/DataLayerSnippets.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/DataLayerSnippets.kt new file mode 100644 index 00000000..f6d08156 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/DataLayerSnippets.kt @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapEffect +import com.google.maps.android.compose.MapsComposeExperimentalApi +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.data.geojson.GeoJsonLayer +import com.google.maps.android.data.kml.KmlLayer +import java.io.ByteArrayInputStream +import kotlin.OptIn +import org.json.JSONObject + +/** + * Demonstrates how to load and display a GeoJSON data layer (representing a Polyline) on the map. + * + * This Composable uses [MapEffect] to get the raw [com.google.android.gms.maps.GoogleMap] reference + * safely and instantiates a [GeoJsonLayer] with a self-contained GeoJSON LineString. + */ +@OptIn(MapsComposeExperimentalApi::class) +@Composable +fun GeoJsonLayerSnippet() { + // [START maps_android_compose_geojson_layer] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + // Use MapEffect to safely access the raw GoogleMap instance + MapEffect(Unit) { googleMap -> + val geoJsonData = + JSONObject( + """{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [103.80, 1.35], + [103.85, 1.40], + [103.90, 1.35] + ] + } + } + ] + }""" + ) + val geoJsonLayer = GeoJsonLayer(googleMap, geoJsonData) + geoJsonLayer.addLayerToMap() + } + } + // [END maps_android_compose_geojson_layer] +} + +/** + * Demonstrates how to load and display KML data layers (representing a Polygon area) on the map. + * + * Parses a KML stream defining a closed triangular area over Singapore using [KmlLayer], rendering + * it dynamically on the map. + */ +@OptIn(MapsComposeExperimentalApi::class) +@Composable +fun KmlLayerSnippet() { + // [START maps_android_compose_kml_layer] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + val context = LocalContext.current + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + // Use MapEffect to safely access the raw GoogleMap instance + MapEffect(Unit) { googleMap -> + val kmlData = + ByteArrayInputStream( + """ + + + KML Polygon Area + + + + + 103.80,1.30,0 + 103.85,1.38,0 + 103.90,1.30,0 + 103.80,1.30,0 + + + + + + """ + .toByteArray() + ) + val kmlLayer = KmlLayer(googleMap, kmlData, context) + kmlLayer.addLayerToMap() + } + } + // [END maps_android_compose_kml_layer] +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/MainActivity.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/MainActivity.kt new file mode 100644 index 00000000..d020d440 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/MainActivity.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +class MainActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val initialSnippetTitle = intent.getStringExtra("EXTRA_SNIPPET_TITLE") + setContent { + MaterialTheme(colorScheme = MapsComposeColorScheme) { SnippetRunnerApp(initialSnippetTitle) } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SnippetRunnerApp(initialSnippetTitle: String? = null) { + var selectedSnippet by remember { + mutableStateOf( + SnippetRegistry.groups.flatMap { it.items }.find { it.title == initialSnippetTitle } + ) + } + + if (selectedSnippet == null) { + Scaffold( + topBar = { + TopAppBar( + title = { Text("Google Maps Compose Snippets") }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) + } + ) { paddingValues -> + LazyColumn(modifier = Modifier.fillMaxSize().padding(paddingValues).padding(16.dp)) { + SnippetRegistry.groups.forEach { group -> + item { + Text( + text = group.title, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + items(group.items) { item -> + Card( + modifier = + Modifier.fillMaxWidth().padding(vertical = 6.dp).clickable { + selectedSnippet = item + }, + colors = + CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text(text = item.title, style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = item.description, style = MaterialTheme.typography.bodyMedium) + } + } + } + item { Spacer(modifier = Modifier.height(16.dp)) } + } + } + } + } else { + val snippet = selectedSnippet!! + Box(modifier = Modifier.fillMaxSize()) { snippet.content() } + } +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/MapInitSnippets.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/MapInitSnippets.kt new file mode 100644 index 00000000..e6c10366 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/MapInitSnippets.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.CameraPositionState +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapProperties +import com.google.maps.android.compose.MapType +import com.google.maps.android.compose.MapUiSettings +import com.google.maps.android.compose.rememberCameraPositionState +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.delay + +/** + * Demonstrates the minimum configuration to initialize a basic, interactive Google Map. + * + * This Composable initializes a standard map viewport and manages its camera position state using + * [rememberCameraPositionState]. + */ +@Composable +fun BasicMapSnippet( + cameraPositionState: CameraPositionState = rememberCameraPositionState { + position = + CameraPosition.fromLatLngZoom( + LatLng(40.0150, -105.2705), // Boulder, Colorado + 11f + ) + }, + onStateConfigured: (CameraPositionState) -> Unit = {} +) { + // Expose the active camera state to test listeners + onStateConfigured(cameraPositionState) + + // [START maps_android_compose_init_basic] + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) + // [END maps_android_compose_init_basic] +} + +/** + * Demonstrates how to initialize a Google Map with custom properties and UI controls. + * + * This Composable configures the map to use a [MapType.SATELLITE] layer and customizes the UI + * settings to enable the compass while hiding the default zoom control buttons. + */ +@Composable +fun CustomConfigMapSnippet() { + // [START maps_android_compose_init_custom] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + var configStep by remember { mutableIntStateOf(0) } + + // Automatically cycle through 3 different configurations every 2 seconds to capture transition + // details + LaunchedEffect(Unit) { + while (true) { + delay(2000.milliseconds) + configStep = (configStep + 1) % 3 + } + } + + // Dynamically derive MapProperties based on the current state index + val properties = + remember(configStep) { + when (configStep) { + 0 -> MapProperties(mapType = MapType.SATELLITE, isTrafficEnabled = false) + 1 -> MapProperties(mapType = MapType.TERRAIN, isTrafficEnabled = true) + else -> MapProperties(mapType = MapType.NORMAL, isTrafficEnabled = false) + } + } + + // Dynamically derive MapUiSettings based on the current state index + val uiSettings = + remember(configStep) { + when (configStep) { + 0 -> MapUiSettings(compassEnabled = true, zoomControlsEnabled = false) + 1 -> MapUiSettings(compassEnabled = false, zoomControlsEnabled = true) + else -> MapUiSettings(compassEnabled = true, zoomControlsEnabled = false) + } + } + + GoogleMap( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState, + properties = properties, + uiSettings = uiSettings + ) + // [END maps_android_compose_init_custom] +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/MarkerSnippets.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/MarkerSnippets.kt new file mode 100644 index 00000000..783ccf63 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/MarkerSnippets.kt @@ -0,0 +1,159 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerComposable +import com.google.maps.android.compose.MarkerInfoWindowComposable +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.rememberUpdatedMarkerState + +/** + * Demonstrates how to add a standard Google Map Marker with a title and info snippet. + * + * This snippet uses [rememberUpdatedMarkerState] to manage the marker's coordinates and places a + * standard red pin on the map. + */ +@Composable +fun BasicMarkerSnippet() { + // [START maps_android_compose_marker_basic] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + val markerState = rememberUpdatedMarkerState(position = singapore) + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + Marker(state = markerState, title = "Singapore", snippet = "A beautiful sunny island") + } + // [END maps_android_compose_marker_basic] +} + +/** + * Demonstrates how to customize a marker's icon using [BitmapDescriptorFactory]. + * + * This Composable changes the default red pin color to a default azure color by passing the icon + * property. Useful for categorized marker configurations. + */ +@Composable +fun CustomMarkerIconSnippet() { + // [START maps_android_compose_marker_custom_icon] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + val markerState = rememberUpdatedMarkerState(position = singapore) + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + Marker( + state = markerState, + title = "Singapore Custom Icon", + // Customizes the marker icon (e.g., azure default pin) + icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE) + // For a custom drawable asset, use: + // icon = BitmapDescriptorFactory.fromResource(R.drawable.my_custom_marker) + ) + } + // [END maps_android_compose_marker_custom_icon] +} + +/** + * Demonstrates how to render arbitrary Jetpack Compose UI as an interactive marker on the map. + * + * This snippet leverages [MarkerComposable] to replace the standard marker pin with a styled + * Compose [Box] containing a [Text] layout. This allows complete, programmatic visual flexibility + * for markers. + */ +@Composable +fun MarkerComposableSnippet() { + // [START maps_android_compose_marker_composable] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + val markerState = rememberUpdatedMarkerState(position = singapore) + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + // Renders arbitrary Compose UI directly as the map pin + MarkerComposable( + state = markerState, + title = "Compose Marker", + keys = arrayOf("singapore_composable") + ) { + Box( + modifier = + Modifier.width(88.dp).height(36.dp).clip(RoundedCornerShape(16.dp)).background(Color.Red), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Compose UI", + color = Color.White, + textAlign = TextAlign.Center, + ) + } + } + } + // [END maps_android_compose_marker_composable] +} + +/** + * Demonstrates how to customize the balloon InfoWindow popup using arbitrary Compose UI. + * + * This Composable uses [MarkerInfoWindowComposable] to customize the popup content rendered when + * the user taps the marker. The balloon is styled using a yellow [Box] with a black [Text] label. + */ +@Composable +fun CustomInfoWindowSnippet() { + // [START maps_android_compose_marker_info_window] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + val markerState = rememberUpdatedMarkerState(position = singapore) + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + // Custom Info Window popup rendered fully in Compose + MarkerInfoWindowComposable( + state = markerState, + title = "Marker Info Window", + infoContent = { marker -> + // Custom pop-up content inside the balloon frame + Box( + modifier = + Modifier.width(150.dp) + .height(50.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.Yellow), + contentAlignment = Alignment.Center + ) { + Text(text = marker.title ?: "Title", color = Color.Black) + } + } + ) { + // The marker pin representation itself (a simple blue circle Composable) + Box( + modifier = + Modifier.width(32.dp).height(32.dp).clip(RoundedCornerShape(16.dp)).background(Color.Blue) + ) + } + } + // [END maps_android_compose_marker_info_window] +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/ShapeSnippets.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/ShapeSnippets.kt new file mode 100644 index 00000000..fbf424b3 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/ShapeSnippets.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.Circle +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Polygon +import com.google.maps.android.compose.Polyline +import com.google.maps.android.compose.rememberCameraPositionState + +/** + * Demonstrates how to draw a styled vector path (Polyline) connecting coordinates on the map. + * + * This snippet uses [Polyline] inside the [GoogleMap] content block, customizing the line's color + * to blue and thickness to 10 pixels. + */ +@Composable +fun PolylineSnippet() { + // [START maps_android_compose_polyline] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + val points = remember { listOf(LatLng(1.35, 103.87), LatLng(1.40, 103.77), LatLng(1.45, 103.77)) } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + Polyline(points = points, color = Color.Blue, width = 10f) + } + // [END maps_android_compose_polyline] +} + +/** + * Demonstrates how to draw a styled, solid filled vector area (Polygon) on the map. + * + * This snippet configures a [Polygon] with a translucent red fill color and a solid red border + * stroke. + */ +@Composable +fun PolygonSnippet() { + // [START maps_android_compose_polygon] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + val points = remember { listOf(LatLng(1.35, 103.87), LatLng(1.40, 103.77), LatLng(1.40, 103.90)) } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + Polygon( + points = points, + fillColor = Color.Red.copy(alpha = 0.3f), + strokeColor = Color.Red, + strokeWidth = 5f + ) + } + // [END maps_android_compose_polygon] +} + +/** + * Demonstrates how to draw a styled geographic circle on the map centered at a coordinate. + * + * This Composable uses [Circle] with a radius specified in meters (2,000m) and styles it with a + * translucent green fill and a solid green outline. + */ +@Composable +fun CircleSnippet() { + // [START maps_android_compose_circle] + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap(modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState) { + Circle( + center = singapore, + radius = 2000.0, // in meters + fillColor = Color.Green.copy(alpha = 0.2f), + strokeColor = Color.Green, + strokeWidth = 4f + ) + } + // [END maps_android_compose_circle] +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt new file mode 100644 index 00000000..5b6502f3 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetActivities.kt @@ -0,0 +1,294 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +/** + * Google Developers Brand inspired Light Color Scheme (Clean White & Google Blue). Completely + * replaces the default Material 3 lavender/purple seed color theme. + */ +val MapsComposeColorScheme = + lightColorScheme( + primary = Color(0xFF1A73E8), // Google Blue + onPrimary = Color(0xFFFFFFFF), + primaryContainer = Color(0xFFE8F0FE), // Translucent Blue + onPrimaryContainer = Color(0xFF1A73E8), + background = Color(0xFFFFFFFF), + onBackground = Color(0xFF202124), + surface = Color(0xFFFFFFFF), + onSurface = Color(0xFF202124), + surfaceVariant = Color(0xFFF8F9FA), // Light Gray for Card backgrounds + onSurfaceVariant = Color(0xFF3C4043) + ) + +/** + * Base Activity class that automatically hides the system status bar and navigation bar to provide + * a clean, edge-to-edge full-screen bleed layout suitable for professional screenshots. + */ +abstract class BaseSnippetActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +} + +/** + * Dedicated Activity class hosting the [BasicMapSnippet] showing minimum map setup. Launchable via + * ADB: `adb shell am start -n com.google.maps.android.compose.snippets/.BasicMapActivity` + */ +class BasicMapActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { BasicMapSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [CustomConfigMapSnippet] showing satellite and custom UI + * settings. Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.CustomConfigMapActivity` + */ +class CustomConfigMapActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { CustomConfigMapSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [MoveCameraSnippet] showing instant camera update. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.MoveCameraActivity` + */ +class MoveCameraActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { MoveCameraSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [AnimateCameraSnippet] showing animated camera update. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.AnimateCameraActivity` + */ +class AnimateCameraActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { AnimateCameraSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [RestrictCameraBoundsSnippet] showing bounding box + * restrictions. Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.RestrictCameraBoundsActivity` + */ +class RestrictCameraBoundsActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme(colorScheme = MapsComposeColorScheme) { RestrictCameraBoundsSnippet() } + } + } +} + +/** + * Dedicated Activity class hosting the [BasicMarkerSnippet] showing a simple map pin. Launchable + * via ADB: `adb shell am start -n com.google.maps.android.compose.snippets/.BasicMarkerActivity` + */ +class BasicMarkerActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { BasicMarkerSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [CustomMarkerIconSnippet] showing customized HUE marker + * icons. Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.CustomMarkerIconActivity` + */ +class CustomMarkerIconActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { CustomMarkerIconSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [MarkerComposableSnippet] showing fully custom Compose + * layouts as pins. Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.MarkerComposableActivity` + */ +class MarkerComposableActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { MarkerComposableSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [CustomInfoWindowSnippet] showing custom pop-up InfoWindows. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.CustomInfoWindowActivity` + */ +class CustomInfoWindowActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { CustomInfoWindowSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [PolylineSnippet] showing vector polyline paths. Launchable + * via ADB: `adb shell am start -n com.google.maps.android.compose.snippets/.PolylineActivity` + */ +class PolylineActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { PolylineSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [PolygonSnippet] showing solid filled vector shapes. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.PolygonActivity` + */ +class PolygonActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { PolygonSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [CircleSnippet] showing geographic circle vector areas. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.CircleActivity` + */ +class CircleActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { CircleSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [MarkerClusteringSnippet] showing dynamic marker grouping. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.MarkerClusteringActivity` + */ +class MarkerClusteringActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { MarkerClusteringSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [GeoJsonLayerSnippet] showing GeoJSON layer rendering. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.GeoJsonLayerActivity` + */ +class GeoJsonLayerActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { GeoJsonLayerSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [KmlLayerSnippet] showing KML layer rendering. Launchable + * via ADB: `adb shell am start -n com.google.maps.android.compose.snippets/.KmlLayerActivity` + */ +class KmlLayerActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { KmlLayerSnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [GroundOverlaySnippet] showing flat image overlays. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.GroundOverlayActivity` + */ +class GroundOverlayActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { GroundOverlaySnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [TileOverlaySnippet] showing custom raster tile overlays. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.TileOverlayActivity` + */ +class TileOverlayActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { TileOverlaySnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [WmsTileOverlaySnippet] showing remote WMS map tile layers. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.WmsTileOverlayActivity` + */ +class WmsTileOverlayActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { WmsTileOverlaySnippet() } } + } +} + +/** + * Dedicated Activity class hosting the [RememberComposeBitmapDescriptorSnippet] showing Composable + * marker icons. Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.RememberComposeBitmapDescriptorActivity` + */ +class RememberComposeBitmapDescriptorActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + MaterialTheme(colorScheme = MapsComposeColorScheme) { + RememberComposeBitmapDescriptorSnippet() + } + } + } +} + +/** + * Dedicated Activity class hosting the [ScaleBarSnippet] showing dynamic map distance scales. + * Launchable via ADB: `adb shell am start -n + * com.google.maps.android.compose.snippets/.ScaleBarActivity` + */ +class ScaleBarActivity : BaseSnippetActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { MaterialTheme(colorScheme = MapsComposeColorScheme) { ScaleBarSnippet() } } + } +} diff --git a/snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetRegistry.kt b/snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetRegistry.kt new file mode 100644 index 00000000..d49adbe2 --- /dev/null +++ b/snippets/src/main/java/com/google/maps/android/compose/snippets/SnippetRegistry.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose.snippets + +import androidx.compose.runtime.Composable + +data class SnippetItemInfo( + val title: String, + val description: String, + val content: @Composable () -> Unit +) + +data class SnippetGroupInfo( + val title: String, + val description: String, + val items: List +) + +object SnippetRegistry { + val groups: List by lazy { + listOf( + SnippetGroupInfo( + title = "Map Initialization", + description = "Snippets demonstrating map initialization and configuration.", + items = + listOf( + SnippetItemInfo( + title = "1. Basic Map", + description = "Initializes a simple Google Map.", + content = { BasicMapSnippet() } + ), + SnippetItemInfo( + title = "2. Custom Configuration", + description = "Initializes a Google Map with custom properties and UI settings.", + content = { CustomConfigMapSnippet() } + ) + ) + ), + SnippetGroupInfo( + title = "Camera Control", + description = "Snippets demonstrating camera movement, animation, and boundaries.", + items = + listOf( + SnippetItemInfo( + title = "1. Move Camera", + description = "Instantly updates camera position.", + content = { MoveCameraSnippet() } + ), + SnippetItemInfo( + title = "2. Animate Camera", + description = "Smoothly animates camera position.", + content = { AnimateCameraSnippet() } + ), + SnippetItemInfo( + title = "3. Restrict Camera Bounds", + description = "Restricts camera movement to a specific LatLng bounds.", + content = { RestrictCameraBoundsSnippet() } + ) + ) + ), + SnippetGroupInfo( + title = "Markers", + description = "Snippets demonstrating standard, custom, and Compose-rendered markers.", + items = + listOf( + SnippetItemInfo( + title = "1. Basic Marker", + description = "Adds a basic marker to Singapore.", + content = { BasicMarkerSnippet() } + ), + SnippetItemInfo( + title = "2. Custom Marker Icon", + description = "Adds a marker with a custom resource drawable icon.", + content = { CustomMarkerIconSnippet() } + ), + SnippetItemInfo( + title = "3. Marker Composable", + description = + "Adds an interactive custom marker rendered using fully Jetpack Compose UI layouts.", + content = { MarkerComposableSnippet() } + ), + SnippetItemInfo( + title = "4. Custom Info Window Composable", + description = + "Adds a custom InfoWindow rendered dynamically using fully Jetpack Compose UI.", + content = { CustomInfoWindowSnippet() } + ) + ) + ), + SnippetGroupInfo( + title = "Shapes", + description = "Snippets demonstrating drawing Polylines, Polygons, and Circles on the map.", + items = + listOf( + SnippetItemInfo( + title = "1. Polyline", + description = "Draws a solid Polyline.", + content = { PolylineSnippet() } + ), + SnippetItemInfo( + title = "2. Polygon", + description = "Draws a filled Polygon.", + content = { PolygonSnippet() } + ), + SnippetItemInfo( + title = "3. Circle", + description = "Draws a solid filled Circle.", + content = { CircleSnippet() } + ) + ) + ), + SnippetGroupInfo( + title = "Clustering & Utilities", + description = "Snippets demonstrating marker clustering and utility helpers.", + items = + listOf( + SnippetItemInfo( + title = "1. Marker Clustering", + description = "Clusters multiple nearby markers dynamically.", + content = { MarkerClusteringSnippet() } + ) + ) + ), + SnippetGroupInfo( + title = "Data Layers", + description = "Snippets demonstrating importing and rendering GeoJSON and KML data layers.", + items = + listOf( + SnippetItemInfo( + title = "1. GeoJSON Layer", + description = "Loads a GeoJSON data layer on the map.", + content = { GeoJsonLayerSnippet() } + ), + SnippetItemInfo( + title = "2. KML Layer", + description = "Loads a KML data layer on the map.", + content = { KmlLayerSnippet() } + ) + ) + ), + SnippetGroupInfo( + title = "Overlays & Widgets", + description = "Snippets demonstrating image overlays, custom tile layers, and UI widgets.", + items = + listOf( + SnippetItemInfo( + title = "1. Ground Overlay", + description = "Displays a static image clamped over coordinate bounds.", + content = { GroundOverlaySnippet() } + ), + SnippetItemInfo( + title = "2. Tile Overlay", + description = "Displays custom styled dynamic map tile overlays.", + content = { TileOverlaySnippet() } + ), + SnippetItemInfo( + title = "3. WMS Tile Overlay", + description = "Displays tiles from a Web Map Service dynamically.", + content = { WmsTileOverlaySnippet() } + ), + SnippetItemInfo( + title = "4. Compose Bitmap Descriptor", + description = "Converts a Compose Composable dynamically into a marker icon.", + content = { RememberComposeBitmapDescriptorSnippet() } + ), + SnippetItemInfo( + title = "5. Scale Bar Widget", + description = "Overlay showing on-screen distance ratio scales based on zoom.", + content = { ScaleBarSnippet() } + ) + ) + ) + ) + } +}