From 6a0a5b2607fbceb66bc7cabcafd83e591ed920cc Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:54:15 -0600 Subject: [PATCH 1/5] feat(showcase): introduce unified front door and catalog with edge-to-edge camera cutout support --- .../ApiDemos/common/build.gradle.kts | 4 + .../maps3d/common/showcase/ShowcaseModels.kt | 119 ++++ .../common/showcase/ShowcasePreferences.kt | 69 +++ .../common/showcase/ShowcaseRegistry.kt | 249 ++++++++ .../showcase/ui/CrossFrameworkSwitcher.kt | 139 +++++ .../common/showcase/ui/FrontDoorScreen.kt | 209 +++++++ .../showcase/ui/UnifiedCatalogScreen.kt | 565 ++++++++++++++++++ .../maps3dkotlin/mainactivity/MainActivity.kt | 254 +++----- .../ComposeDemos/app/build.gradle.kts | 8 +- .../com/example/composedemos/MainActivity.kt | 240 ++------ maps3d-compose-demo/build.gradle.kts | 8 +- maps3d-compose/build.gradle.kts | 8 +- .../maps/android/compose3d/Map3DState.kt | 24 +- .../maps/android/compose3d/utils/Units.kt | 8 +- .../maps/android/compose3d/utils/Utilities.kt | 5 +- snippets/build.gradle.kts | 8 +- 16 files changed, 1534 insertions(+), 383 deletions(-) create mode 100644 Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseModels.kt create mode 100644 Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcasePreferences.kt create mode 100644 Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt create mode 100644 Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt create mode 100644 Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/FrontDoorScreen.kt create mode 100644 Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/UnifiedCatalogScreen.kt diff --git a/Maps3DSamples/ApiDemos/common/build.gradle.kts b/Maps3DSamples/ApiDemos/common/build.gradle.kts index 76a30dc8..2e3f0fc7 100644 --- a/Maps3DSamples/ApiDemos/common/build.gradle.kts +++ b/Maps3DSamples/ApiDemos/common/build.gradle.kts @@ -76,7 +76,11 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.ui) + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.foundation:foundation-layout") implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) api(libs.play.services.base) // "com.google.android.gms:play-services-base:18.10.0" api(libs.play.services.maps3d) // "com.google.android.gms:play-services-maps3d:0.2.2" diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseModels.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseModels.kt new file mode 100644 index 00000000..e4b15d4d --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseModels.kt @@ -0,0 +1,119 @@ +/* + * 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.example.maps3d.common.showcase + +/** + * Enumerates the supported development frameworks for Google Maps 3D on Android. + * + * Each framework represents a distinct pedagogical pillar with full feature parity. + * + * @property id Unique programmatic identifier. + * @property displayName Human-readable name for UI titles and headers. + * @property badge Short badge label displayed on sample cards. + * @property description Brief summary of the architecture and target audience. + * @property iconEmoji Distinctive emoji/glyph icon representing the framework. + * @property accentColorHex Hex color value for theme accents. + */ +enum class FrameworkType( + val id: String, + val displayName: String, + val badge: String, + val description: String, + val iconEmoji: String, + val accentColorHex: Long, +) { + COMPOSE( + id = "compose", + displayName = "Jetpack Compose", + badge = "Compose", + description = "Modern declarative 3D UI with GoogleMap3D composable, state flows & coroutines.", + iconEmoji = "βš›οΈ", + accentColorHex = 0xFF4285F4, + ), + KOTLIN_VIEWS( + id = "kotlin", + displayName = "Kotlin Views", + badge = "Kotlin", + description = "Traditional Android XML Views with ViewModels, Kotlin DSLs & lifecycle scopes.", + iconEmoji = "πŸ’œ", + accentColorHex = 0xFF7F52FF, + ), + JAVA_VIEWS( + id = "java", + displayName = "Java Views", + badge = "Java", + description = "Enterprise Android XML Views with standard Java builders and event listeners.", + iconEmoji = "β˜•", + accentColorHex = 0xFFE76F51, + ); + + companion object { + fun fromId(id: String?): FrameworkType = + entries.find { it.id.equals(id, ignoreCase = true) } ?: COMPOSE + } +} + +/** + * Pedagogical difficulty tiers for organizing samples progressively. + * + * @property displayName User-facing tier header title. + * @property badge Emoji icon indicating the tier. + * @property order Sort order from beginner to advanced. + */ +enum class SampleTier( + val displayName: String, + val badge: String, + val order: Int, +) { + BEGINNER("Beginner", "🟒", 1), + INTERMEDIATE("Intermediate", "🟑", 2), + ADVANCED("Advanced", "πŸ”΄", 3); +} + +/** + * Metadata definition for a sample across frameworks. + * + * @property id Canonical feature identifier (e.g. "path_following"). + * @property title User-facing title. + * @property subtitle Short summary of what is demonstrated. + * @property tier Pedagogical difficulty classification. + * @property tags Keyword tags for search filtering. + * @property iconEmoji Distinctive visual emoji for the sample. + * @property composeActivity Full class name of Compose Activity implementation. + * @property kotlinActivity Full class name of Kotlin Views Activity implementation. + * @property javaActivity Full class name of Java Views Activity implementation. + */ +data class ShowcaseSample( + val id: String, + val title: String, + val subtitle: String, + val tier: SampleTier, + val tags: List, + val iconEmoji: String, + val composeActivity: String? = null, + val kotlinActivity: String? = null, + val javaActivity: String? = null, +) { + fun getActivityClassName(framework: FrameworkType): String? = when (framework) { + FrameworkType.COMPOSE -> composeActivity + FrameworkType.KOTLIN_VIEWS -> kotlinActivity + FrameworkType.JAVA_VIEWS -> javaActivity + } + + fun isAvailable(framework: FrameworkType): Boolean = + getActivityClassName(framework) != null +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcasePreferences.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcasePreferences.kt new file mode 100644 index 00000000..4e4d6c76 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcasePreferences.kt @@ -0,0 +1,69 @@ +/* + * 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.example.maps3d.common.showcase + +import android.content.Context +import android.content.SharedPreferences + +/** + * Manages user framework preferences and persistent navigation states for the Maps 3D Showcase. + */ +class ShowcasePreferences(context: Context) { + + private val prefs: SharedPreferences = + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + var preferredFramework: FrameworkType? + get() { + val id = prefs.getString(KEY_PREFERRED_FRAMEWORK, null) ?: return null + return FrameworkType.fromId(id) + } + set(value) { + prefs.edit().apply { + if (value == null) { + remove(KEY_PREFERRED_FRAMEWORK) + } else { + putString(KEY_PREFERRED_FRAMEWORK, value.id) + } + apply() + } + } + + var rememberChoice: Boolean + get() = prefs.getBoolean(KEY_REMEMBER_CHOICE, true) + set(value) { + prefs.edit().putBoolean(KEY_REMEMBER_CHOICE, value).apply() + } + + fun clear() { + prefs.edit().clear().apply() + } + + companion object { + private const val PREFS_NAME = "maps3d_showcase_preferences" + private const val KEY_PREFERRED_FRAMEWORK = "key_preferred_framework" + private const val KEY_REMEMBER_CHOICE = "key_remember_choice" + + @Volatile + private var instance: ShowcasePreferences? = null + + fun getInstance(context: Context): ShowcasePreferences = + instance ?: synchronized(this) { + instance ?: ShowcasePreferences(context.applicationContext).also { instance = it } + } + } +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt new file mode 100644 index 00000000..2332f50f --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt @@ -0,0 +1,249 @@ +/* + * 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.example.maps3d.common.showcase + +/** + * Master catalog registry of all Google Maps 3D showcase features. + * + * Categorizes samples into Beginner, Intermediate, and Advanced tiers, + * and maintains mappings to their respective Compose, Kotlin Views, and Java Views activities. + */ +object ShowcaseRegistry { + + val SAMPLES: List = listOf( + // ========================================== + // 🟒 BEGINNER TIER + // ========================================== + ShowcaseSample( + id = "hello_map", + title = "Hello 3D Map", + subtitle = "Map instantiation, lifecycle management, and initial camera positioning.", + tier = SampleTier.BEGINNER, + tags = listOf("#basics", "#init", "#lifecycle", "#quickstart"), + iconEmoji = "πŸ“", + composeActivity = "com.example.composedemos.hellomap.HelloMapActivity", + kotlinActivity = "com.example.maps3dkotlin.hellomap.HelloMapActivity", + javaActivity = "com.example.maps3djava.hellomap.HelloMapActivity", + ), + ShowcaseSample( + id = "roadmap_mode", + title = "Map Modes", + subtitle = "Toggle dynamically between Satellite, Hybrid, and Roadmap 3D rendering modes.", + tier = SampleTier.BEGINNER, + tags = listOf("#modes", "#satellite", "#roadmap", "#hybrid", "#styling"), + iconEmoji = "πŸ›°οΈ", + composeActivity = "com.example.composedemos.roadmapmode.RoadmapModeActivity", + kotlinActivity = "com.example.maps3dkotlin.roadmapmode.RoadmapModeActivity", + javaActivity = "com.example.maps3djava.roadmapmode.RoadmapModeActivity", + ), + ShowcaseSample( + id = "camera_controls", + title = "Camera Controls", + subtitle = "Programmatic control of camera center coordinates, range, heading, tilt, and roll.", + tier = SampleTier.BEGINNER, + tags = listOf("#camera", "#tilt", "#heading", "#range", "#navigation"), + iconEmoji = "πŸŽ₯", + composeActivity = "com.example.composedemos.cameracontrols.CameraControlsActivity", + kotlinActivity = "com.example.maps3dkotlin.cameracontrols.CameraControlsActivity", + javaActivity = "com.example.maps3djava.cameracontrols.CameraControlsActivity", + ), + ShowcaseSample( + id = "camera_restrictions", + title = "Camera Restrictions", + subtitle = "Enforce bounding boxes, min/max altitude bounds, and clamped heading/tilt angles.", + tier = SampleTier.BEGINNER, + tags = listOf("#camera", "#bounds", "#clamping", "#restrictions"), + iconEmoji = "πŸ”’", + composeActivity = "com.example.composedemos.camerarestrictions.CameraRestrictionsActivity", + kotlinActivity = "com.example.maps3dkotlin.camerarestrictions.CameraRestrictionsActivity", + javaActivity = "com.example.maps3djava.camerarestrictions.CameraRestrictionsActivity", + ), + + // ========================================== + // 🟑 INTERMEDIATE TIER + // ========================================== + ShowcaseSample( + id = "markers", + title = "3D Markers", + subtitle = "Place and style 3D markers with altitude modes, drag listeners, and custom icons.", + tier = SampleTier.INTERMEDIATE, + tags = listOf("#markers", "#pins", "#altitude", "#overlays", "#interaction"), + iconEmoji = "🏷️", + composeActivity = "com.example.composedemos.markers.MarkersActivity", + kotlinActivity = "com.example.maps3dkotlin.markers.MarkersActivity", + javaActivity = "com.example.maps3djava.markers.MarkersActivity", + ), + ShowcaseSample( + id = "polygons", + title = "Extruded Polygons", + subtitle = "Render 2D surfaces and extruded 3D volumetric buildings with colors and holes.", + tier = SampleTier.INTERMEDIATE, + tags = listOf("#polygons", "#extrusion", "#3d-buildings", "#geometry"), + iconEmoji = "πŸ”·", + composeActivity = "com.example.composedemos.polygons.PolygonsActivity", + kotlinActivity = "com.example.maps3dkotlin.polygons.PolygonsActivity", + javaActivity = "com.example.maps3djava.polygons.PolygonsActivity", + ), + ShowcaseSample( + id = "polylines", + title = "Polylines & Paths", + subtitle = "Draw flat, extruded, and terrain-clamped geodesic polylines with custom strokes.", + tier = SampleTier.INTERMEDIATE, + tags = listOf("#polylines", "#paths", "#geodesic", "#strokes", "#altitude"), + iconEmoji = "〰️", + composeActivity = "com.example.composedemos.polylines.PolylinesActivity", + kotlinActivity = "com.example.maps3dkotlin.polylines.PolylinesActivity", + javaActivity = "com.example.maps3djava.polylines.PolylinesActivity", + ), + ShowcaseSample( + id = "models", + title = "3D Models (GLB)", + subtitle = "Load and position 3D GLB mesh assets with scale, roll/pitch/yaw orientation.", + tier = SampleTier.INTERMEDIATE, + tags = listOf("#models", "#glb", "#3d-mesh", "#orientation", "#ufo"), + iconEmoji = "πŸ›Έ", + composeActivity = "com.example.composedemos.models.ModelsActivity", + kotlinActivity = "com.example.maps3dkotlin.models.ModelsActivity", + javaActivity = "com.example.maps3djava.models.ModelsActivity", + ), + ShowcaseSample( + id = "popovers", + title = "Popovers & Info Windows", + subtitle = "Anchor rich HTML/View popovers to 3D markers with auto-close and pan behaviors.", + tier = SampleTier.INTERMEDIATE, + tags = listOf("#popovers", "#infowindow", "#html", "#ui", "#anchors"), + iconEmoji = "πŸ’¬", + composeActivity = "com.example.composedemos.popovers.PopoversActivity", + kotlinActivity = "com.example.maps3dkotlin.popovers.PopoversActivity", + javaActivity = "com.example.maps3djava.popovers.PopoversActivity", + ), + + // ========================================== + // πŸ”΄ ADVANCED TIER + // ========================================== + ShowcaseSample( + id = "advanced_camera", + title = "Cinematic Camera Tours", + subtitle = "Orchestrate multi-keyframe orbital camera tours with heading & tilt interpolations.", + tier = SampleTier.ADVANCED, + tags = listOf("#cinematic", "#camera", "#animation", "#keyframe", "#orbit", "#tour"), + iconEmoji = "🎒", + composeActivity = "com.example.composedemos.advancedcameraanimation.AdvancedCameraAnimationActivity", + kotlinActivity = "com.example.maps3dkotlin.advancedcameraanimation.AdvancedCameraAnimationActivity", + javaActivity = "com.example.maps3djava.advancedcameraanimation.AdvancedCameraAnimationActivity", + ), + ShowcaseSample( + id = "path_following", + title = "Path Following (VSYNC)", + subtitle = "Interactive flight simulation with VSYNC Choreographer, speed multipliers & gestures.", + tier = SampleTier.ADVANCED, + tags = listOf("#physics", "#path-following", "#choreographer", "#vsync", "#flight", "#gestures"), + iconEmoji = "🏎️", + composeActivity = "com.example.composedemos.pathfollowing.PathFollowingActivity", + kotlinActivity = "com.example.maps3dkotlin.pathfollowing.PathFollowingActivity", + javaActivity = "com.example.maps3djava.pathfollowing.PathFollowingActivity", + ), + ShowcaseSample( + id = "data_visualization", + title = "Real-Time Data Viz", + subtitle = "Simulate rising water levels and regional flood zones using extruded 3D polygons.", + tier = SampleTier.ADVANCED, + tags = listOf("#data-viz", "#flood-fill", "#extrusion", "#elevation", "#simulation"), + iconEmoji = "🌊", + composeActivity = "com.example.composedemos.datavisualization.DataVisualizationActivity", + kotlinActivity = "com.example.maps3dkotlin.datavisualization.DataVisualizationActivity", + javaActivity = "com.example.maps3djava.datavisualization.DataVisualizationActivity", + ), + ShowcaseSample( + id = "field_of_view", + title = "Dynamic FOV Lens", + subtitle = "Manipulate camera lens field-of-view perspective from telephoto zoom to wide-angle.", + tier = SampleTier.ADVANCED, + tags = listOf("#fov", "#field-of-view", "#lens", "#perspective", "#zoom"), + iconEmoji = "πŸ”­", + composeActivity = "com.example.composedemos.fieldofview.FieldOfViewActivity", + kotlinActivity = "com.example.maps3dkotlin.fieldofview.FieldOfViewActivity", + javaActivity = "com.example.maps3djava.fieldofview.FieldOfViewActivity", + ), + ShowcaseSample( + id = "routes", + title = "Routes API Navigation", + subtitle = "Compute driving routes via Google Routes API and visualize 3D path corridors.", + tier = SampleTier.ADVANCED, + tags = listOf("#routes", "#directions", "#navigation", "#turn-by-turn", "#polyline"), + iconEmoji = "πŸ›£οΈ", + composeActivity = "com.example.composedemos.routes.RoutesActivity", + kotlinActivity = "com.example.maps3dkotlin.routes.RoutesActivity", + javaActivity = "com.example.maps3djava.routes.RoutesActivity", + ), + ShowcaseSample( + id = "flight_simulator", + title = "Flight Simulator", + subtitle = "Interactive first-person flight controls over high-resolution 3D photorealistic mesh.", + tier = SampleTier.ADVANCED, + tags = listOf("#flight-simulator", "#physics", "#cockpit", "#3d-controls"), + iconEmoji = "✈️", + composeActivity = "com.example.composedemos.flightsimulator.FlightSimulatorActivity", + kotlinActivity = "com.example.maps3dkotlin.flightsimulator.FlightSimulatorActivity", + javaActivity = "com.example.maps3djava.flightsimulator.FlightSimulatorActivity", + ), + ShowcaseSample( + id = "animating_models", + title = "Animating Models", + subtitle = "Real-time coordinate translations, heading rotations, and speed animations for 3D GLB models.", + tier = SampleTier.ADVANCED, + tags = listOf("#models", "#animation", "#glb", "#ufo", "#movement"), + iconEmoji = "πŸ›Έ", + composeActivity = "com.example.composedemos.animatingmodels.AnimatingModelsActivity", + kotlinActivity = "com.example.maps3dkotlin.animatingmodels.AnimatingModelsActivity", + javaActivity = "com.example.maps3djava.animatingmodels.AnimatingModelsActivity", + ), + ShowcaseSample( + id = "map_interactions", + title = "Gestures & Place Clicks", + subtitle = "Tap listeners on 3D buildings, POIs, and custom touch gesture delegates.", + tier = SampleTier.ADVANCED, + tags = listOf("#gestures", "#poi", "#place-click", "#touch", "#listeners"), + iconEmoji = "πŸ‘†", + composeActivity = "com.example.composedemos.mapinteractions.MapInteractionsActivity", + kotlinActivity = "com.example.maps3dkotlin.mapinteractions.MapInteractionsActivity", + javaActivity = "com.example.maps3djava.mapinteractions.MapInteractionsActivity", + ), + ) + + fun getSampleById(id: String): ShowcaseSample? = SAMPLES.find { it.id == id } + + fun filter( + framework: FrameworkType, + query: String = "", + tier: SampleTier? = null, + tag: String? = null, + ): List { + return SAMPLES.filter { sample -> + sample.isAvailable(framework) && + (tier == null || sample.tier == tier) && + (tag == null || sample.tags.any { it.equals(tag, ignoreCase = true) }) && + (query.isBlank() || + sample.title.contains(query, ignoreCase = true) || + sample.subtitle.contains(query, ignoreCase = true) || + sample.tags.any { it.contains(query, ignoreCase = true) }) + } + } + + fun allTags(): List = + SAMPLES.flatMap { it.tags }.distinct().sorted() +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt new file mode 100644 index 00000000..a801d650 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt @@ -0,0 +1,139 @@ +/* + * 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.example.maps3d.common.showcase.ui + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.widget.Toast +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.maps3d.common.showcase.FrameworkType +import com.example.maps3d.common.showcase.ShowcaseRegistry + +/** + * Reusable UI and helper utilities for the cross-framework "Rosetta Stone" sample switcher. + * + * Allows developers to instantly jump between Jetpack Compose, Kotlin Views, and Java Views + * implementations of the same sample. + */ +object CrossFrameworkSwitcher { + + /** + * Launches the equivalent sample in the target framework. + */ + fun switchSample( + context: Context, + sampleId: String, + targetFramework: FrameworkType, + finishCurrent: Boolean = false, + ) { + val sample = ShowcaseRegistry.getSampleById(sampleId) + if (sample == null) { + Toast.makeText(context, "Sample not found: $sampleId", Toast.LENGTH_SHORT).show() + return + } + + val className = sample.getActivityClassName(targetFramework) + if (className == null) { + Toast.makeText( + context, + "No ${targetFramework.displayName} implementation available for ${sample.title}", + Toast.LENGTH_SHORT, + ).show() + return + } + + try { + val targetClass = Class.forName(className) + val intent = Intent(context, targetClass).apply { + putExtra("EXTRA_SAMPLE_ID", sampleId) + } + context.startActivity(intent) + if (finishCurrent && context is Activity) { + context.finish() + } + } catch (e: ClassNotFoundException) { + Toast.makeText( + context, + "Could not load activity: $className", + Toast.LENGTH_SHORT, + ).show() + } + } +} + +/** + * Composable pill button that switches to another framework. + */ +@Composable +fun FrameworkSwitchPill( + sampleId: String, + targetFramework: FrameworkType, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + val accentColor = Color(targetFramework.accentColorHex) + + Surface( + shape = RoundedCornerShape(16.dp), + color = accentColor.copy(alpha = 0.12f), + border = BorderStroke(1.dp, accentColor.copy(alpha = 0.4f)), + modifier = modifier.clickable { + CrossFrameworkSwitcher.switchSample(context, sampleId, targetFramework) + }, + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.SwapHoriz, + contentDescription = "Switch framework", + tint = accentColor, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "${targetFramework.iconEmoji} ${targetFramework.badge}", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = accentColor, + ) + } + } +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/FrontDoorScreen.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/FrontDoorScreen.kt new file mode 100644 index 00000000..5c02e072 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/FrontDoorScreen.kt @@ -0,0 +1,209 @@ +/* + * 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.example.maps3d.common.showcase.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.safeDrawingPadding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.maps3d.common.showcase.FrameworkType +import com.example.maps3d.common.showcase.ShowcaseRegistry + +/** + * The "Front Door" framework selection screen for Google Maps 3D Showcase. + * + * Allows developers to choose their preferred UI framework on initial launch + * and optionally persist the selection for future sessions. + */ +@Composable +fun FrontDoorScreen( + initialRememberChoice: Boolean = true, + onFrameworkSelected: (FrameworkType, Boolean) -> Unit, +) { + var rememberChoice by remember { mutableStateOf(initialRememberChoice) } + + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Column( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "πŸ—ΊοΈ", + fontSize = 48.sp, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "Google Maps 3D", + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + ) + + Spacer(modifier = Modifier.height(6.dp)) + + Text( + text = "Choose your preferred framework to explore interactive samples and architectural blueprints.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 12.dp), + ) + + Spacer(modifier = Modifier.height(28.dp)) + + FrameworkType.entries.forEach { framework -> + val sampleCount = ShowcaseRegistry.SAMPLES.count { it.isAvailable(framework) } + FrameworkCard( + framework = framework, + sampleCount = sampleCount, + onClick = { onFrameworkSelected(framework, rememberChoice) }, + ) + Spacer(modifier = Modifier.height(16.dp)) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { rememberChoice = !rememberChoice } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Checkbox( + checked = rememberChoice, + onCheckedChange = { rememberChoice = it }, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Always remember my choice (change anytime)", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Composable +private fun FrameworkCard( + framework: FrameworkType, + sampleCount: Int, + onClick: () -> Unit, +) { + val accentColor = Color(framework.accentColorHex) + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + ), + border = BorderStroke(1.5.dp, accentColor.copy(alpha = 0.4f)), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), + ) { + Column( + modifier = Modifier.padding(20.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = framework.iconEmoji, + fontSize = 28.sp, + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = framework.displayName, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + Surface( + shape = RoundedCornerShape(8.dp), + color = accentColor.copy(alpha = 0.15f), + ) { + Text( + text = "$sampleCount Samples", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.SemiBold, + color = accentColor, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + ) + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = framework.description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 20.sp, + ) + } + } +} diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/UnifiedCatalogScreen.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/UnifiedCatalogScreen.kt new file mode 100644 index 00000000..b4928a75 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/UnifiedCatalogScreen.kt @@ -0,0 +1,565 @@ +/* + * 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.example.maps3d.common.showcase.ui + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.displayCutout +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.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.SwapHoriz +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.maps3d.common.showcase.FrameworkType +import com.example.maps3d.common.showcase.SampleTier +import com.example.maps3d.common.showcase.ShowcaseRegistry +import com.example.maps3d.common.showcase.ShowcaseSample +import kotlinx.coroutines.launch + +/** + * Unified interactive catalog screen for Google Maps 3D Showcase. + * + * Provides real-time search, pedagogical tier filtering, keyword tag filtering, + * and a persistent "Escape Hatch" to switch between Jetpack Compose, Kotlin Views, and Java Views. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UnifiedCatalogScreen( + currentFramework: FrameworkType, + onFrameworkChanged: (FrameworkType) -> Unit, + onResetToFrontDoor: () -> Unit, + onSampleClick: (ShowcaseSample) -> Unit, +) { + var searchQuery by remember { mutableStateOf("") } + var selectedTier by remember { mutableStateOf(null) } + var selectedTag by remember { mutableStateOf(null) } + var showFrameworkSheet by remember { mutableStateOf(false) } + + val filteredSamples = remember(currentFramework, searchQuery, selectedTier, selectedTag) { + ShowcaseRegistry.filter( + framework = currentFramework, + query = searchQuery, + tier = selectedTier, + tag = selectedTag, + ) + } + + val accentColor = Color(currentFramework.accentColorHex) + + Scaffold( + contentWindowInsets = WindowInsets.safeDrawing, + topBar = { + TopAppBar( + windowInsets = TopAppBarDefaults.windowInsets.union(WindowInsets.displayCutout), + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Maps 3D", + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleLarge, + ) + } + }, + actions = { + // Escape Hatch Framework Pill + Surface( + shape = RoundedCornerShape(20.dp), + color = accentColor.copy(alpha = 0.15f), + border = BorderStroke(1.dp, accentColor.copy(alpha = 0.5f)), + modifier = Modifier + .padding(end = 12.dp) + .clickable { showFrameworkSheet = true }, + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = currentFramework.iconEmoji, + fontSize = 16.sp, + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = currentFramework.badge, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = accentColor, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "β–Ύ", + color = accentColor, + fontSize = 12.sp, + ) + } + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding), + ) { + // Search Input + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + placeholder = { Text("Search samples (e.g. camera, extrusion, vsync)...") }, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = "Search") }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Default.Clear, contentDescription = "Clear search") + } + } + }, + singleLine = true, + shape = RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = accentColor, + focusedLabelColor = accentColor, + ), + ) + + // Filter Chips Scrollable Row + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + FilterChip( + selected = selectedTier == null && selectedTag == null, + onClick = { + selectedTier = null + selectedTag = null + }, + label = { Text("All (${ShowcaseRegistry.SAMPLES.count { it.isAvailable(currentFramework) }})") }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = accentColor.copy(alpha = 0.2f), + selectedLabelColor = accentColor, + ), + ) + + SampleTier.entries.forEach { tier -> + val count = ShowcaseRegistry.SAMPLES.count { it.isAvailable(currentFramework) && it.tier == tier } + FilterChip( + selected = selectedTier == tier, + onClick = { + selectedTier = if (selectedTier == tier) null else tier + selectedTag = null + }, + label = { Text("${tier.badge} ${tier.displayName} ($count)") }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = accentColor.copy(alpha = 0.2f), + selectedLabelColor = accentColor, + ), + ) + } + + listOf("#camera", "#animation", "#overlays", "#models", "#data-viz", "#routes").forEach { tag -> + FilterChip( + selected = selectedTag == tag, + onClick = { + selectedTag = if (selectedTag == tag) null else tag + selectedTier = null + }, + label = { Text(tag) }, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = accentColor.copy(alpha = 0.2f), + selectedLabelColor = accentColor, + ), + ) + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Grouped Samples List + if (filteredSamples.isEmpty()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text("πŸ”", fontSize = 40.sp) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "No matching samples found", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Try searching for a different keyword or reset filters.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { + searchQuery = "" + selectedTier = null + selectedTag = null + }, + colors = ButtonDefaults.buttonColors(containerColor = accentColor), + ) { + Text("Reset Filters") + } + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + SampleTier.entries.forEach { tier -> + val tierSamples = filteredSamples.filter { it.tier == tier } + if (tierSamples.isNotEmpty()) { + item(key = "header_${tier.name}") { + TierHeader(tier = tier, count = tierSamples.size) + } + items(tierSamples, key = { it.id }) { sample -> + SampleCard( + sample = sample, + framework = currentFramework, + onClick = { onSampleClick(sample) }, + ) + } + } + } + } + } + } + } + + // Framework Switcher Bottom Sheet + if (showFrameworkSheet) { + FrameworkSwitchSheet( + currentFramework = currentFramework, + onDismiss = { showFrameworkSheet = false }, + onSelectFramework = { framework -> + showFrameworkSheet = false + onFrameworkChanged(framework) + }, + onResetToFrontDoor = { + showFrameworkSheet = false + onResetToFrontDoor() + }, + ) + } +} + +@Composable +private fun TierHeader(tier: SampleTier, count: Int) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "${tier.badge} ${tier.displayName.uppercase()}", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + letterSpacing = 1.sp, + ) + Spacer(modifier = Modifier.width(8.dp)) + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant, + ) { + Text( + text = "$count", + style = MaterialTheme.typography.labelSmall, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SampleCard( + sample: ShowcaseSample, + framework: FrameworkType, + onClick: () -> Unit, +) { + val accentColor = Color(framework.accentColorHex) + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + shape = RoundedCornerShape(14.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f), + ), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Surface( + modifier = Modifier.size(44.dp), + shape = RoundedCornerShape(10.dp), + color = accentColor.copy(alpha = 0.12f), + ) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = sample.iconEmoji, + fontSize = 22.sp, + ) + } + } + + Spacer(modifier = Modifier.width(14.dp)) + + Column( + modifier = Modifier.weight(1f), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = sample.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + } + + Spacer(modifier = Modifier.height(3.dp)) + + Text( + text = sample.subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 16.sp, + ) + + Spacer(modifier = Modifier.height(6.dp)) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + sample.tags.take(3).forEach { tag -> + Text( + text = tag, + style = MaterialTheme.typography.labelSmall, + color = accentColor.copy(alpha = 0.8f), + fontSize = 11.sp, + ) + } + } + } + + Spacer(modifier = Modifier.width(8.dp)) + + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = "Open sample", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + modifier = Modifier.size(18.dp), + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun FrameworkSwitchSheet( + currentFramework: FrameworkType, + onDismiss: () -> Unit, + onSelectFramework: (FrameworkType) -> Unit, + onResetToFrontDoor: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 16.dp), + ) { + Text( + text = "Switch Development Framework", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "Select a framework to browse its native implementations.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + FrameworkType.entries.forEach { framework -> + val isSelected = framework == currentFramework + val color = Color(framework.accentColorHex) + + Surface( + modifier = Modifier + .fillMaxWidth() + .clickable { onSelectFramework(framework) } + .padding(vertical = 4.dp), + shape = RoundedCornerShape(12.dp), + color = if (isSelected) color.copy(alpha = 0.15f) else Color.Transparent, + border = if (isSelected) BorderStroke(1.5.dp, color) else null, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = framework.iconEmoji, fontSize = 24.sp) + Spacer(modifier = Modifier.width(12.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = framework.displayName, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + ) + Text( + text = framework.description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) + } + if (isSelected) { + Text( + text = "βœ“ Active", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = color, + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = onResetToFrontDoor, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + ) { + Icon( + imageVector = Icons.Default.SwapHoriz, + contentDescription = "Return to Front Door", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Return to Welcome Screen (Front Door)", + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt index 00fd104b..50c273e9 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/mainactivity/MainActivity.kt @@ -1,216 +1,104 @@ -// Copyright 2025 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. +/* + * 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.example.maps3dkotlin.mainactivity -import android.app.Activity -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.annotation.StringRes -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.example.maps3dcommon.R -import com.example.maps3dkotlin.cameracontrols.CameraControlsActivity -import com.example.maps3dkotlin.hellomap.HelloMapActivity -import com.example.maps3dkotlin.mapinteractions.MapInteractionsActivity -import com.example.maps3dkotlin.markers.MarkersActivity -import com.example.maps3dkotlin.models.ModelsActivity -import com.example.maps3dkotlin.polygons.PolygonsActivity -import com.example.maps3dkotlin.polylines.PolylinesActivity -import com.example.maps3dkotlin.popovers.PopoversActivity -import com.example.maps3dkotlin.camerarestrictions.CameraRestrictionsActivity -import com.example.maps3dkotlin.flightsimulator.FlightSimulatorActivity -import com.example.maps3dkotlin.routes.RoutesActivity -import com.example.maps3dkotlin.pathfollowing.PathFollowingActivity -import com.example.maps3dkotlin.pathstyling.PathStylingActivity -import com.example.maps3dkotlin.animatingmodels.AnimatingModelsActivity -import com.example.maps3dkotlin.placesearch.PlaceSearchActivity -import com.example.maps3dkotlin.placeautocomplete.PlaceAutocompleteActivity -import com.example.maps3dkotlin.placedetails.PlaceDetailsActivity -import com.example.maps3dkotlin.advancedcameraanimation.AdvancedCameraAnimationActivity -import com.example.maps3dkotlin.datavisualization.DataVisualizationActivity -import com.example.maps3dkotlin.cloudstyling.CloudStylingActivity -import com.example.maps3dkotlin.roadmapmode.RoadmapModeActivity -import com.example.maps3dkotlin.fieldofview.FieldOfViewActivity -import com.example.maps3dkotlin.theme.Maps3DSamplesTheme -import kotlinx.coroutines.launch +import androidx.compose.ui.platform.LocalContext +import com.example.maps3d.common.showcase.FrameworkType +import com.example.maps3d.common.showcase.ShowcasePreferences +import com.example.maps3d.common.showcase.ui.CrossFrameworkSwitcher +import com.example.maps3d.common.showcase.ui.FrontDoorScreen +import com.example.maps3d.common.showcase.ui.UnifiedCatalogScreen /** - * A data class to represent a single sample in the list. - * Using a data class provides type safety and makes the code more readable - * compared to using a Map or a Pair. + * Main activity of the Kotlin Views 3D Maps SDK Samples application. * - * @param titleResId The string resource ID for the sample's title. - * @param activityClass The activity to launch for this sample. Null if not implemented. - */ -data class Sample( - @StringRes val titleResId: Int, - val activityClass: Class? -) - -/** - * The main activity of the 3D Maps SDK Samples application. - * This activity displays a list of available samples that users can select from. + * Integrates the unified Showcase catalog and framework switching. */ class MainActivity : ComponentActivity() { - // A list of all the samples to be displayed in the app. - // This approach is more idiomatic and type-safe than using a map. - private val samples = listOf( - Sample(R.string.feature_title_overview_hello_3d_map, HelloMapActivity::class.java), - Sample(R.string.feature_title_camera_controls, CameraControlsActivity::class.java), - Sample(R.string.feature_title_markers, MarkersActivity::class.java), - Sample(R.string.feature_title_polygons, PolygonsActivity::class.java), - Sample(R.string.feature_title_polylines, PolylinesActivity::class.java), - Sample(R.string.feature_title_3d_models, ModelsActivity::class.java), - Sample(R.string.feature_title_popovers, PopoversActivity::class.java), - Sample(R.string.feature_title_map_interactions, MapInteractionsActivity::class.java), - Sample(R.string.feature_title_camera_restrictions, CameraRestrictionsActivity::class.java), - Sample(R.string.feature_title_flight_simulator, FlightSimulatorActivity::class.java), - Sample(R.string.feature_title_routes_api, RoutesActivity::class.java), - Sample(R.string.feature_title_path_following, PathFollowingActivity::class.java), - Sample(R.string.feature_title_path_styling, PathStylingActivity::class.java), - Sample(R.string.feature_title_animating_models, AnimatingModelsActivity::class.java), - Sample(R.string.feature_title_place_search, PlaceSearchActivity::class.java), - Sample(R.string.feature_title_place_autocomplete, PlaceAutocompleteActivity::class.java), - Sample(R.string.feature_title_place_details, PlaceDetailsActivity::class.java), - Sample(R.string.feature_title_advanced_camera_animation, AdvancedCameraAnimationActivity::class.java), - Sample(R.string.feature_title_data_visualization, DataVisualizationActivity::class.java), - Sample(R.string.feature_title_cloud_styling, CloudStylingActivity::class.java), - Sample(R.string.feature_title_roadmap_mode, RoadmapModeActivity::class.java), - Sample(R.string.feature_title_field_of_view, FieldOfViewActivity::class.java), - ) - - @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - setContent { - Maps3DSamplesTheme { - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - - Scaffold( + MaterialTheme { + Surface( modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar(title = { Text(stringResource(R.string.samples_menu_title)) }) - }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) } - ) { innerPadding -> - SampleMenuList( - samples = samples, - modifier = Modifier.padding(innerPadding) - ) { sample -> - val activityClass = sample.activityClass - if (activityClass != null) { - startActivity(Intent(this, activityClass)) - } else { - coroutineScope.launch { - snackbarHostState.showSnackbar( - message = getString(R.string.feature_not_implemented) - ) - } - } - } + color = MaterialTheme.colorScheme.background, + ) { + KotlinShowcaseApp() } } } } } -/** - * A Composable function that displays the list of samples. - * This function is responsible for rendering the list of available map samples. - * It uses a `LazyColumn` for efficient rendering of the list. - * - * @param samples The list of samples to display. - * @param modifier A `Modifier` to be applied to the layout. - * @param onItemClick A callback that is invoked when a sample is clicked. - */ @Composable -fun SampleMenuList( - samples: List, - modifier: Modifier = Modifier, - onItemClick: (Sample) -> Unit -) { - Column( - modifier = modifier - .fillMaxSize() - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - LazyColumn(modifier = Modifier.fillMaxWidth()) { - items(samples) { sample -> - SampleListItem(sample = sample) { - onItemClick(sample) - } - HorizontalDivider() - } - } - } -} +fun KotlinShowcaseApp() { + val context = LocalContext.current + val prefs = remember { ShowcasePreferences.getInstance(context) } -/** - * A Composable function for a single item in the sample list. - * This function displays the title of the sample and handles click events. - * The item's appearance changes based on whether the feature is enabled. - * - * @param sample The sample to display. - * @param onClick A callback that is invoked when the item is clicked. - */ -@Composable -fun SampleListItem(sample: Sample, onClick: () -> Unit) { - val isEnabled = sample.activityClass != null - val color = if (isEnabled) { - MaterialTheme.colorScheme.onBackground + var selectedFramework by remember { mutableStateOf(prefs.preferredFramework ?: FrameworkType.KOTLIN_VIEWS) } + var showFrontDoor by remember { mutableStateOf(false) } + + if (showFrontDoor) { + FrontDoorScreen( + initialRememberChoice = prefs.rememberChoice, + onFrameworkSelected = { framework, rememberChoice -> + prefs.rememberChoice = rememberChoice + if (rememberChoice) { + prefs.preferredFramework = framework + } + selectedFramework = framework + showFrontDoor = false + }, + ) } else { - MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) + UnifiedCatalogScreen( + currentFramework = selectedFramework, + onFrameworkChanged = { newFramework -> + selectedFramework = newFramework + if (prefs.rememberChoice) { + prefs.preferredFramework = newFramework + } + }, + onResetToFrontDoor = { + prefs.preferredFramework = null + showFrontDoor = true + }, + onSampleClick = { sample -> + CrossFrameworkSwitcher.switchSample( + context = context, + sampleId = sample.id, + targetFramework = selectedFramework, + ) + }, + ) } - - Text( - text = stringResource(sample.titleResId), - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = isEnabled, onClick = onClick) - .padding(vertical = 16.dp, horizontal = 8.dp), - fontSize = 18.sp, - style = MaterialTheme.typography.bodyMedium, - color = color, - ) -} \ No newline at end of file +} diff --git a/Maps3DSamples/ComposeDemos/app/build.gradle.kts b/Maps3DSamples/ComposeDemos/app/build.gradle.kts index 0aa16053..e552bfde 100644 --- a/Maps3DSamples/ComposeDemos/app/build.gradle.kts +++ b/Maps3DSamples/ComposeDemos/app/build.gradle.kts @@ -25,7 +25,13 @@ plugins { configure { kotlin { target("**/*.kt") - ktlint().editorConfigOverride(mapOf("indent_size" to "4", "ktlint_function_naming_ignore_when_annotated_with" to "Composable")) + ktlint().editorConfigOverride( + mapOf( + "indent_size" to "4", + "ktlint_function_naming_ignore_when_annotated_with" to "Composable", + "ktlint_standard_max-line-length" to "disabled", + ), + ) trimTrailingWhitespace() endWithNewline() } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt index 5e475d91..ff143504 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/MainActivity.kt @@ -16,50 +16,34 @@ package com.example.composedemos -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text 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.platform.LocalContext -import androidx.compose.ui.unit.dp -import com.example.composedemos.advancedcameraanimation.AdvancedCameraAnimationActivity -import com.example.composedemos.animatingmodels.AnimatingModelsActivity -import com.example.composedemos.cameracontrols.CameraControlsActivity -import com.example.composedemos.camerarestrictions.CameraRestrictionsActivity -import com.example.composedemos.cloudstyling.CloudStylingActivity -import com.example.composedemos.datavisualization.DataVisualizationActivity -import com.example.composedemos.fieldofview.FieldOfViewActivity -import com.example.composedemos.flightsimulator.FlightSimulatorActivity -import com.example.composedemos.hellomap.HelloMapActivity -import com.example.composedemos.mapinteractions.MapInteractionsActivity -import com.example.composedemos.markers.MarkersActivity -import com.example.composedemos.models.ModelsActivity -import com.example.composedemos.pathfollowing.PathFollowingActivity -import com.example.composedemos.pathstyling.PathStylingActivity -import com.example.composedemos.placeautocomplete.PlaceAutocompleteActivity -import com.example.composedemos.placedetails.PlaceDetailsActivity -import com.example.composedemos.placesearch.PlaceSearchActivity -import com.example.composedemos.polygons.PolygonsActivity -import com.example.composedemos.polylines.PolylinesActivity -import com.example.composedemos.popovers.PopoversActivity -import com.example.composedemos.roadmapmode.RoadmapModeActivity -import com.example.composedemos.routes.RoutesActivity +import com.example.maps3d.common.showcase.FrameworkType +import com.example.maps3d.common.showcase.ShowcasePreferences +import com.example.maps3d.common.showcase.ui.CrossFrameworkSwitcher +import com.example.maps3d.common.showcase.ui.FrontDoorScreen +import com.example.maps3d.common.showcase.ui.UnifiedCatalogScreen +/** + * The main "Front Door" launcher activity for the Google Maps 3D Showcase. + * + * Integrates framework selection, persistent user preferences, real-time search, + * and pedagogical tier navigation. + */ class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -69,7 +53,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background, ) { - CatalogScreen() + ShowcaseApp() } } } @@ -77,162 +61,46 @@ class MainActivity : ComponentActivity() { } @Composable -fun CatalogScreen() { +fun ShowcaseApp() { val context = LocalContext.current - LazyColumn( - modifier = Modifier - .fillMaxSize() - .safeDrawingPadding(), - ) { - item { - Text( - text = "Compose Demos Catalog", - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier.padding(16.dp), - ) - } - - // ApiDemos Parity - item { CategoryHeader("ApiDemos Parity") } - item { - SampleItem("Hello Map") { - context.startActivity(Intent(context, HelloMapActivity::class.java)) - } - } - item { - SampleItem("Polylines") { - context.startActivity(Intent(context, PolylinesActivity::class.java)) - } - } - item { - SampleItem("Map Interactions") { - context.startActivity(Intent(context, MapInteractionsActivity::class.java)) - } - } - item { - SampleItem("Popovers") { - context.startActivity(Intent(context, PopoversActivity::class.java)) - } - } - item { - SampleItem("Camera Controls") { - context.startActivity(Intent(context, CameraControlsActivity::class.java)) - } - } - item { - SampleItem("Polygons") { - context.startActivity(Intent(context, PolygonsActivity::class.java)) - } - } - item { - SampleItem("Models") { - context.startActivity(Intent(context, ModelsActivity::class.java)) - } - } - item { - SampleItem("Markers") { - context.startActivity(Intent(context, MarkersActivity::class.java)) - } - } - - // Additional Catalog Requirements - item { CategoryHeader("Additional Catalog Requirements") } - item { - SampleItem("Camera Restrictions") { - context.startActivity(Intent(context, CameraRestrictionsActivity::class.java)) - } - } - item { - SampleItem("Flight Simulator") { - context.startActivity(Intent(context, FlightSimulatorActivity::class.java)) - } - } - item { - SampleItem("Routes API") { - context.startActivity(Intent(context, RoutesActivity::class.java)) - } - } - item { - SampleItem("Path Following") { - context.startActivity(Intent(context, PathFollowingActivity::class.java)) - } - } - item { - SampleItem("Path Styling") { - context.startActivity(Intent(context, PathStylingActivity::class.java)) - } - } - item { - SampleItem("Animating Models") { - context.startActivity(Intent(context, AnimatingModelsActivity::class.java)) - } - } - item { - SampleItem("Place Search") { - context.startActivity(Intent(context, PlaceSearchActivity::class.java)) - } - } - item { - SampleItem("Place Autocomplete") { - context.startActivity(Intent(context, PlaceAutocompleteActivity::class.java)) - } - } - item { - SampleItem("Place Details") { - context.startActivity(Intent(context, PlaceDetailsActivity::class.java)) - } - } - item { - SampleItem("Advanced Camera Animation") { - context.startActivity(Intent(context, AdvancedCameraAnimationActivity::class.java)) - } - } - item { - SampleItem("Data Visualization (Flood Fill)") { - context.startActivity(Intent(context, DataVisualizationActivity::class.java)) - } - } - item { - SampleItem("Cloud Map Styling") { - context.startActivity(Intent(context, CloudStylingActivity::class.java)) - } - } - item { - SampleItem("Roadmap Mode") { - context.startActivity(Intent(context, RoadmapModeActivity::class.java)) - } - } - item { - SampleItem("Field Of View") { - context.startActivity(Intent(context, FieldOfViewActivity::class.java)) - } - } - } -} + val prefs = remember { ShowcasePreferences.getInstance(context) } -@Composable -fun CategoryHeader(text: String) { - Text( - text = text, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - color = MaterialTheme.colorScheme.primary, - ) -} + // Read stored framework preference + var selectedFramework by remember { mutableStateOf(prefs.preferredFramework) } + var showFrontDoor by remember { mutableStateOf(selectedFramework == null || !prefs.rememberChoice) } -@Composable -fun SampleItem(title: String, onClick: () -> Unit) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp) - .clickable { onClick() }, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - ) { - Text( - text = title, - modifier = Modifier.padding(16.dp), - style = MaterialTheme.typography.bodyLarge, + if (showFrontDoor || selectedFramework == null) { + FrontDoorScreen( + initialRememberChoice = prefs.rememberChoice, + onFrameworkSelected = { framework, rememberChoice -> + prefs.rememberChoice = rememberChoice + if (rememberChoice) { + prefs.preferredFramework = framework + } + selectedFramework = framework + showFrontDoor = false + }, + ) + } else { + UnifiedCatalogScreen( + currentFramework = selectedFramework!!, + onFrameworkChanged = { newFramework -> + selectedFramework = newFramework + if (prefs.rememberChoice) { + prefs.preferredFramework = newFramework + } + }, + onResetToFrontDoor = { + prefs.preferredFramework = null + showFrontDoor = true + }, + onSampleClick = { sample -> + CrossFrameworkSwitcher.switchSample( + context = context, + sampleId = sample.id, + targetFramework = selectedFramework!!, + ) + }, ) } } diff --git a/maps3d-compose-demo/build.gradle.kts b/maps3d-compose-demo/build.gradle.kts index a1ee2461..f450ae7d 100644 --- a/maps3d-compose-demo/build.gradle.kts +++ b/maps3d-compose-demo/build.gradle.kts @@ -25,7 +25,13 @@ plugins { configure { kotlin { target("**/*.kt") - ktlint().editorConfigOverride(mapOf("indent_size" to "4", "ktlint_function_naming_ignore_when_annotated_with" to "Composable")) + ktlint().editorConfigOverride( + mapOf( + "indent_size" to "4", + "ktlint_function_naming_ignore_when_annotated_with" to "Composable", + "ktlint_standard_max-line-length" to "disabled", + ), + ) trimTrailingWhitespace() endWithNewline() } diff --git a/maps3d-compose/build.gradle.kts b/maps3d-compose/build.gradle.kts index 8db06bc1..84c6fdfb 100644 --- a/maps3d-compose/build.gradle.kts +++ b/maps3d-compose/build.gradle.kts @@ -24,7 +24,13 @@ plugins { configure { kotlin { target("**/*.kt") - ktlint().editorConfigOverride(mapOf("indent_size" to "4", "ktlint_function_naming_ignore_when_annotated_with" to "Composable")) + ktlint().editorConfigOverride( + mapOf( + "indent_size" to "4", + "ktlint_function_naming_ignore_when_annotated_with" to "Composable", + "ktlint_standard_max-line-length" to "disabled", + ), + ) trimTrailingWhitespace() endWithNewline() } diff --git a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/Map3DState.kt b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/Map3DState.kt index 11539222..85695a52 100644 --- a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/Map3DState.kt +++ b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/Map3DState.kt @@ -85,7 +85,11 @@ class Map3DState { } } - private fun createMarker(map: GoogleMap3D, config: MarkerConfig, overrideId: String? = null): Marker? { + private fun createMarker( + map: GoogleMap3D, + config: MarkerConfig, + overrideId: String? = null, + ): Marker? { val marker = map.addMarker(config.toMarkerOptions(overrideId)) config.onClick?.let { callback -> marker?.setClickListener { @@ -129,7 +133,11 @@ class Map3DState { } } - private fun createPolyline(map: GoogleMap3D, config: PolylineConfig, overrideId: String? = null): Polyline { + private fun createPolyline( + map: GoogleMap3D, + config: PolylineConfig, + overrideId: String? = null, + ): Polyline { val polyline = map.addPolyline(config.toPolylineOptions(overrideId)) config.onClick?.let { callback -> polyline.setClickListener { @@ -173,7 +181,11 @@ class Map3DState { } } - private fun createPolygon(map: GoogleMap3D, config: PolygonConfig, overrideId: String? = null): Polygon { + private fun createPolygon( + map: GoogleMap3D, + config: PolygonConfig, + overrideId: String? = null, + ): Polygon { val polygon = map.addPolygon(config.toPolygonOptions(overrideId)) config.onClick?.let { callback -> polygon.setClickListener { @@ -220,7 +232,11 @@ class Map3DState { } } - private fun createModel(map: GoogleMap3D, config: ModelConfig, overrideId: String? = null): Model { + private fun createModel( + map: GoogleMap3D, + config: ModelConfig, + overrideId: String? = null, + ): Model { val model = map.addModel(config.toModelOptions(overrideId)) config.onClick?.let { callback -> model.setClickListener { diff --git a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Units.kt b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Units.kt index d0d96c02..6a114f5b 100644 --- a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Units.kt +++ b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Units.kt @@ -137,7 +137,9 @@ fun getUnitsConverter(countryCode: String?): UnitsConverter { /** Class to render measurements in imperial units. */ object ImperialUnitsConverter : UnitsConverter() { - override fun toDistanceUnits(meters: Meters): ValueWithUnitsTemplate = if (meters < 0.25.miles) { + override fun toDistanceUnits(meters: Meters): ValueWithUnitsTemplate = if (meters < + 0.25.miles + ) { ValueWithUnitsTemplate(meters.toFeet, R.string.in_feet) } else { ValueWithUnitsTemplate(meters.toMiles, R.string.in_miles) @@ -148,7 +150,9 @@ object ImperialUnitsConverter : UnitsConverter() { /** Class to render measurements in metric units. */ object MetricUnitsConverter : UnitsConverter() { - override fun toDistanceUnits(meters: Meters): ValueWithUnitsTemplate = if (meters < 1000.meters) { + override fun toDistanceUnits(meters: Meters): ValueWithUnitsTemplate = if (meters < + 1000.meters + ) { ValueWithUnitsTemplate(meters.toMeters, R.string.in_meters) } else { ValueWithUnitsTemplate(meters.toKilometers, R.string.in_kilometers) diff --git a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Utilities.kt b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Utilities.kt index 951e8f1a..ea7c987a 100644 --- a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Utilities.kt +++ b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/utils/Utilities.kt @@ -319,10 +319,7 @@ fun FlyAroundOptions.copy( } } -fun FlyToOptions.copy( - endCamera: Camera? = null, - durationInMillis: Long? = null, -): FlyToOptions { +fun FlyToOptions.copy(endCamera: Camera? = null, durationInMillis: Long? = null): FlyToOptions { val objectToCopy = this return flyToOptions { diff --git a/snippets/build.gradle.kts b/snippets/build.gradle.kts index f5ce5c09..e4a5d8a7 100644 --- a/snippets/build.gradle.kts +++ b/snippets/build.gradle.kts @@ -35,7 +35,13 @@ subprojects { } kotlin { target("**/*.kt") - ktlint().editorConfigOverride(mapOf("indent_size" to "4", "ktlint_function_naming_ignore_when_annotated_with" to "Composable")) + ktlint().editorConfigOverride( + mapOf( + "indent_size" to "4", + "ktlint_function_naming_ignore_when_annotated_with" to "Composable", + "ktlint_standard_max-line-length" to "disabled", + ), + ) trimTrailingWhitespace() endWithNewline() } From e585894e44c38d1e6aac8483c336dee3615cf9ef Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:06:32 -0600 Subject: [PATCH 2/5] fix(maps3d): add MapResetHelper to reliably reset map mode, restrictions, and camera between samples --- .../example/maps3d/common/MapResetHelper.kt | 129 ++++++++++++++++++ .../roadmapmode/RoadmapModeActivity.java | 5 + .../sampleactivity/SampleBaseActivity.java | 47 +++++-- .../roadmapmode/RoadmapModeActivity.kt | 1 + .../sampleactivity/SampleBaseActivity.kt | 41 ++++-- .../maps/android/compose3d/GoogleMap3D.kt | 13 ++ 6 files changed, 216 insertions(+), 20 deletions(-) create mode 100644 Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/MapResetHelper.kt diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/MapResetHelper.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/MapResetHelper.kt new file mode 100644 index 00000000..9fdbf963 --- /dev/null +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/MapResetHelper.kt @@ -0,0 +1,129 @@ +/* + * 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.example.maps3d.common + +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.model.Camera +import com.google.android.gms.maps3d.model.CameraRestriction +import com.google.android.gms.maps3d.model.Map3DMode + +/** + * Robust map state resetting utility for Google Maps 3D. + * + * The Maps 3D SDK reuses its underlying native rendering engine across activity transitions + * within the same application process. As a result, properties such as [Map3DMode] (e.g. SATELLITE, + * ROADMAP, HYBRID), camera bounds/restrictions, and click listeners can persist across samples. + * + * This utility applies an immediate reset and a delayed stabilization reset (after the native + * viewport layout pass completes) to guarantee every sample starts in its expected clean state. + */ +object MapResetHelper { + + private const val TAG = "MapResetHelper" + + /** + * Standard delay in milliseconds to allow the native 3D engine viewport to settle + * before reapplying mode, camera position, and restrictions. + */ + const val STABILIZATION_DELAY_MS: Long = 400L + + /** + * Resets the [GoogleMap3D] instance to the specified baseline state. + * + * @param map The [GoogleMap3D] instance to reset. + * @param expectedMode The target [Map3DMode] (defaults to [Map3DMode.HYBRID]). + * @param initialCamera The target initial [Camera] position, or null if unchanged. + * @param cameraRestriction The target [CameraRestriction], or null to clear all restrictions. + */ + @JvmStatic + @JvmOverloads + fun resetMapState( + map: GoogleMap3D?, + expectedMode: Int = Map3DMode.HYBRID, + initialCamera: Camera? = null, + cameraRestriction: CameraRestriction? = null, + ) { + if (map == null) return + try { + // 1. Force explicit Map Rendering Mode (fixes Satellite/Roadmap bleed) + map.setMapMode(expectedMode) + + // 2. Reset / apply camera restrictions (prevents bounds clamping from bleeding) + map.setCameraRestriction(cameraRestriction) + + // 3. Apply target camera orientation and position + if (initialCamera != null) { + map.setCamera(initialCamera.toValidCamera()) + } + } catch (e: Exception) { + Log.w(TAG, "Error applying map state reset: ${e.message}") + } + } + + /** + * Schedules a delayed stabilization reset on the main thread Looper. + * + * @param mapProvider A lambda returning the current [GoogleMap3D] instance. + * @param isAlive A predicate checking if the host Activity/View is still active and not destroyed. + * @param expectedMode The target [Map3DMode] (defaults to [Map3DMode.HYBRID]). + * @param initialCamera The target initial [Camera] position. + * @param cameraRestriction The target [CameraRestriction], or null. + * @param delayMillis Delay in milliseconds (defaults to [STABILIZATION_DELAY_MS]). + * @param onStabilized Optional callback invoked after stabilization is complete. + */ + @JvmStatic + @JvmOverloads + fun scheduleStabilizationReset( + mapProvider: () -> GoogleMap3D?, + isAlive: () -> Boolean, + expectedMode: Int = Map3DMode.HYBRID, + initialCamera: Camera? = null, + cameraRestriction: CameraRestriction? = null, + delayMillis: Long = STABILIZATION_DELAY_MS, + onStabilized: ((GoogleMap3D) -> Unit)? = null, + ) { + Handler(Looper.getMainLooper()).postDelayed({ + if (isAlive()) { + val map = mapProvider() + if (map != null) { + resetMapState(map, expectedMode, initialCamera, cameraRestriction) + onStabilized?.invoke(map) + } + } + }, delayMillis) + } + + /** + * Teardown cleanup to clear listeners and restrictions when exiting a sample. + */ + @JvmStatic + fun teardownMap(map: GoogleMap3D?) { + if (map == null) return + try { + map.setCameraChangedListener(null) + map.setOnMapSteadyListener(null) + map.setCameraRestriction(null) + // Reset mode to standard HYBRID baseline for next sample + map.setMapMode(Map3DMode.HYBRID) + } catch (e: Exception) { + Log.w(TAG, "Error during map teardown: ${e.message}") + } + } +} diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/roadmapmode/RoadmapModeActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/roadmapmode/RoadmapModeActivity.java index e50eebf0..8fb7432b 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/roadmapmode/RoadmapModeActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/roadmapmode/RoadmapModeActivity.java @@ -69,6 +69,11 @@ */ public class RoadmapModeActivity extends SampleBaseActivity { + @Override + public int getExpectedMapMode() { + return Map3DMode.ROADMAP; + } + // --- Constants & Geographical Bounds --- /** Focal landmark centered on the San Francisco Financial District. */ diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java index bbc65a7a..a323b74a 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java @@ -76,6 +76,14 @@ public abstract class SampleBaseActivity extends AppCompatActivity implements On public abstract Camera getInitialCamera(); public abstract String getTAG(); + public int getExpectedMapMode() { + return com.google.android.gms.maps3d.model.Map3DMode.HYBRID; + } + + public com.google.android.gms.maps3d.model.CameraRestriction getInitialCameraRestriction() { + return null; + } + private OnCameraChangedListener cameraChangedListener; @CallSuper @@ -149,9 +157,9 @@ protected void onPause() { @Override protected void onDestroy() { super.onDestroy(); - map3DView.onDestroy(); - if (googleMap3D != null && cameraChangedListener != null) { - googleMap3D.setCameraChangedListener(null); + com.example.maps3d.common.MapResetHelper.teardownMap(googleMap3D); + if (map3DView != null) { + map3DView.onDestroy(); } } @@ -184,15 +192,27 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { public void onMap3DViewReady(GoogleMap3D googleMap3D) { this.googleMap3D = googleMap3D; - // Workaround: The Maps 3D SDK onMap3DViewReady callback fires when the map object - // is instantiated, but the internal native rendering pipeline and layout pass may briefly - // override initial programmatic camera positions. A short delay ensures the native map viewport - // has fully stabilized before applying the initial camera position. - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (!isDestroyed() && !isFinishing() && this.googleMap3D != null) { - this.googleMap3D.setCamera(getInitialCamera()); + // Immediate baseline reset + com.example.maps3d.common.MapResetHelper.resetMapState( + googleMap3D, + getExpectedMapMode(), + getInitialCamera(), + getInitialCameraRestriction() + ); + + // Delayed stabilization reset + com.example.maps3d.common.MapResetHelper.scheduleStabilizationReset( + () -> this.googleMap3D, + () -> !isDestroyed() && !isFinishing(), + getExpectedMapMode(), + getInitialCamera(), + getInitialCameraRestriction(), + com.example.maps3d.common.MapResetHelper.STABILIZATION_DELAY_MS, + map -> { + onMapStabilized(map); + return kotlin.Unit.INSTANCE; } - }, 350L); + ); // Mark map view as steady when rendering stabilizes for automated visual tests googleMap3D.setOnMapSteadyListener(isSceneSteady -> { @@ -205,7 +225,12 @@ public void onMap3DViewReady(GoogleMap3D googleMap3D) { cameraChangedListener = cameraPosition -> { }; googleMap3D.setCameraChangedListener(cameraChangedListener); + } + /** + * Optional hook invoked after the map has fully stabilized and re-applied its baseline state. + */ + protected void onMapStabilized(GoogleMap3D googleMap3D) { } @CallSuper diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/roadmapmode/RoadmapModeActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/roadmapmode/RoadmapModeActivity.kt index 01b9e5b3..adf16c83 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/roadmapmode/RoadmapModeActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/roadmapmode/RoadmapModeActivity.kt @@ -66,6 +66,7 @@ import com.google.android.material.button.MaterialButton class RoadmapModeActivity : SampleBaseActivity() { override val TAG = "RoadmapModeActivity" + override val expectedMapMode: Int = Map3DMode.ROADMAP override val initialCamera: Camera get() = camera { diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt index b8c5ed81..6cf1f432 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt @@ -83,6 +83,9 @@ abstract class SampleBaseActivity : AppCompatActivity(), OnMap3DViewReadyCallbac abstract val initialCamera: Camera abstract val TAG: String + open val expectedMapMode: Int = com.google.android.gms.maps3d.model.Map3DMode.HYBRID + open val initialCameraRestriction: com.google.android.gms.maps3d.model.CameraRestriction? = null + // Private mutable state to hold the current camera position // This is updated by the listener when it's active. private val _currentCamera = MutableStateFlow(DEFAULT_CAMERA) @@ -195,6 +198,7 @@ abstract class SampleBaseActivity : AppCompatActivity(), OnMap3DViewReadyCallbac @CallSuper override fun onDestroy() { super.onDestroy() + com.example.maps3d.common.MapResetHelper.teardownMap(googleMap3D) map3DView.onDestroy() } @@ -228,15 +232,34 @@ abstract class SampleBaseActivity : AppCompatActivity(), OnMap3DViewReadyCallbac if (isMapInitialized) return isMapInitialized = true Log.d(TAG, "onMapReady called (guaranteed once)") - // Workaround: The Maps 3D SDK onMapReady callback fires when the map object - // is instantiated, but the internal native rendering pipeline and layout pass may briefly - // override initial programmatic camera positions. A short delay ensures the native map viewport - // has fully stabilized before applying the initial camera position. - Handler(Looper.getMainLooper()).postDelayed({ - if (!isDestroyed && !isFinishing) { - this.googleMap3D?.setCamera(initialCamera) - } - }, 350L) + + // Immediate baseline reset + com.example.maps3d.common.MapResetHelper.resetMapState( + map = googleMap3D, + expectedMode = expectedMapMode, + initialCamera = initialCamera, + cameraRestriction = initialCameraRestriction, + ) + + // Delayed stabilization reset: + // Workaround: The Maps 3D SDK native engine may briefly override mapMode/camera + // during viewport layout. This delayed reset guarantees the map mode and camera stabilize. + com.example.maps3d.common.MapResetHelper.scheduleStabilizationReset( + mapProvider = { this.googleMap3D }, + isAlive = { !isDestroyed && !isFinishing }, + expectedMode = expectedMapMode, + initialCamera = initialCamera, + cameraRestriction = initialCameraRestriction, + onStabilized = { map -> + onMapStabilized(map) + }, + ) + } + + /** + * Optional hook invoked after the map has fully stabilized and re-applied its baseline state. + */ + protected open fun onMapStabilized(googleMap3D: GoogleMap3D) { } @CallSuper diff --git a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt index bd14fbdc..4180e3d4 100644 --- a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt +++ b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt @@ -163,6 +163,13 @@ fun GoogleMap3D( state.syncPolygons(googleMap3D, polygons) state.syncModels(googleMap3D, models) state.syncPopovers(map3dView.context, googleMap3D, popovers) + + // Workaround: Delayed stabilization reset to guarantee native 3D engine enforces mapMode and camera + map3dView.postDelayed({ + googleMap3D.setMapMode(mapMode) + googleMap3D.setCamera(camera.toValidCamera()) + googleMap3D.setCameraRestriction(cameraRestriction.toValidCameraRestriction()) + }, 400L) } if (Map3DRegistry.isMapReady) { @@ -179,6 +186,12 @@ fun GoogleMap3D( } }, onRelease = { map3dView -> + googleMap3DState.value?.let { map -> + map.setCameraRestriction(null) + map.setCameraChangedListener(null) + map.setOnMapSteadyListener(null) + map.setMapMode(Map3DMode.HYBRID) + } state.clear() Map3DRegistry.clearInstance() map3dView.onDestroy() From 0118eb7937d04492b05f3a7af19b8b44437e4c5d Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:13:53 -0600 Subject: [PATCH 3/5] feat(showcase): package Kotlin, Java, and Compose activities into single unified APK --- .../ApiDemos/java-app/build.gradle.kts | 16 ++------------ .../java-app/src/main/AndroidManifest.xml | 21 ++----------------- .../ApiDemos/kotlin-app/build.gradle.kts | 8 ++----- .../kotlin-app/src/main/AndroidManifest.xml | 21 ++----------------- .../ComposeDemos/app/build.gradle.kts | 2 ++ 5 files changed, 10 insertions(+), 58 deletions(-) diff --git a/Maps3DSamples/ApiDemos/java-app/build.gradle.kts b/Maps3DSamples/ApiDemos/java-app/build.gradle.kts index 8eedc77d..4afdfaff 100644 --- a/Maps3DSamples/ApiDemos/java-app/build.gradle.kts +++ b/Maps3DSamples/ApiDemos/java-app/build.gradle.kts @@ -17,7 +17,7 @@ val isCI = rootProject.extra["isCI"] as? Boolean ?: false plugins { - alias(libs.plugins.android.application) + alias(libs.plugins.android.library) alias(libs.plugins.secrets.gradle.plugin) } @@ -29,13 +29,9 @@ android { compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { - applicationId = "com.example.maps3djava" minSdk = libs.versions.minSdk.get().toInt() - targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = 1 - versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") manifestPlaceholders["MAPS3D_API_KEY"] = "DEFAULT_API_KEY" manifestPlaceholders["PLACES_API_KEY"] = "DEFAULT_API_KEY" @@ -109,11 +105,3 @@ secrets { } defaultPropertiesFileName = "local.defaults.properties" } - -tasks.register("installAndLaunch") { - description = "Installs and launches the demo app." - group = "install" - dependsOn("installDebug") - // Retrieve the absolute path of adb from the Android extension to avoid reliance on system PATH. - commandLine(android.adbExecutable.absolutePath, "shell", "am", "start", "-n", "com.example.maps3djava/.mainactivity.MainActivity") -} diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml b/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml index 7649c6cd..d37bb0b6 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml +++ b/Maps3DSamples/ApiDemos/java-app/src/main/AndroidManifest.xml @@ -17,18 +17,7 @@ - - + - - - - - - + android:theme="@style/Theme.Maps3DSamples" /> - - + - - - - - - + android:theme="@style/Theme.Maps3DSamples" /> Date: Mon, 31 Aug 2026 18:18:57 -0600 Subject: [PATCH 4/5] fix(compose): bridge Android lifecycle events and call onResume to unblock 3D renderer in GoogleMap3D --- .../maps/android/compose3d/GoogleMap3D.kt | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt index 4180e3d4..464ef73c 100644 --- a/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt +++ b/maps3d-compose/src/main/java/com/google/maps/android/compose3d/GoogleMap3D.kt @@ -68,7 +68,7 @@ fun GoogleMap3D( models: List = emptyList(), popovers: List = emptyList(), cameraRestriction: CameraRestriction? = null, - @Map3DMode mapMode: Int = Map3DMode.SATELLITE, + @Map3DMode mapMode: Int = Map3DMode.HYBRID, options: Map3DInitConfig = Map3DInitConfig.create( centerLat = 0.0, centerLng = 0.0, @@ -82,7 +82,7 @@ fun GoogleMap3D( minTilt = 0.0, maxTilt = 90.0, bounds = null, - mapMode = Map3DMode.SATELLITE, // using the class from com.google.android.gms.maps3d.model.Map3DMode + mapMode = Map3DMode.HYBRID, mapId = null, minAltitude = 0.0, maxAltitude = 1000000.0, @@ -98,6 +98,25 @@ fun GoogleMap3D( val state = remember { Map3DState() } val hasCalledOnMapReady = remember { mutableStateOf(false) } val googleMap3DState = remember { mutableStateOf(null) } + val map3dViewRef = remember { mutableStateOf(null) } + + val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current + + androidx.compose.runtime.DisposableEffect(lifecycleOwner) { + val observer = androidx.lifecycle.LifecycleEventObserver { _, event -> + val view = map3dViewRef.value ?: return@LifecycleEventObserver + when (event) { + androidx.lifecycle.Lifecycle.Event.ON_RESUME -> view.onResume() + androidx.lifecycle.Lifecycle.Event.ON_PAUSE -> view.onPause() + androidx.lifecycle.Lifecycle.Event.ON_DESTROY -> view.onDestroy() + else -> {} + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } // Use rememberUpdatedState to avoid capturing stale lambdas in the async callback val currentOnMapSteady by rememberUpdatedState(onMapSteady) @@ -110,7 +129,9 @@ fun GoogleMap3D( modifier = modifier, factory = { context -> val map3dView = Map3DView(context, options) + map3dViewRef.value = map3dView map3dView.onCreate(null) + map3dView.onResume() map3dView.getMap3DViewAsync(object : OnMap3DViewReadyCallback { override fun onMap3DViewReady(googleMap3D: GoogleMap3D) { From 931a97439b8d85d119ece8ce55feb9f5d4c5ec86 Mon Sep 17 00:00:00 2001 From: Dale Hawkins <107309+dkhawk@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:30:52 -0600 Subject: [PATCH 5/5] WIP: feat(showcase): overhaul sample navigation with unified Front Door, in-sample cross-framework switching, single APK packaging, and map state reset --- .../common/showcase/ShowcaseRegistry.kt | 32 ++++ .../showcase/ui/CrossFrameworkSwitcher.kt | 165 +++++++++++++++++- .../sampleactivity/SampleBaseActivity.java | 1 + .../sampleactivity/SampleBaseActivity.kt | 2 +- .../AdvancedCameraAnimationActivity.kt | 11 +- .../AnimatingModelsActivity.kt | 11 +- .../cameracontrols/CameraControlsActivity.kt | 20 +-- .../CameraRestrictionsActivity.kt | 20 +-- .../cloudstyling/CloudStylingActivity.kt | 11 +- .../DataVisualizationActivity.kt | 1 + .../fieldofview/FieldOfViewActivity.kt | 1 + .../FlightSimulatorActivity.kt | 11 +- .../composedemos/hellomap/HelloMapActivity.kt | 22 +-- .../MapInteractionsActivity.kt | 20 +-- .../composedemos/markers/MarkersActivity.kt | 20 +-- .../composedemos/models/ModelsActivity.kt | 20 +-- .../pathfollowing/PathFollowingActivity.kt | 11 +- .../pathstyling/PathStylingActivity.kt | 11 +- .../PlaceAutocompleteActivity.kt | 11 +- .../placedetails/PlaceDetailsActivity.kt | 1 + .../placesearch/PlaceSearchActivity.kt | 11 +- .../composedemos/polygons/PolygonsActivity.kt | 20 +-- .../polylines/PolylinesActivity.kt | 20 +-- .../composedemos/popovers/PopoversActivity.kt | 20 +-- .../roadmapmode/RoadmapModeActivity.kt | 5 + .../composedemos/routes/RoutesActivity.kt | 29 +-- 26 files changed, 342 insertions(+), 165 deletions(-) diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt index 2332f50f..b657edc0 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ShowcaseRegistry.kt @@ -246,4 +246,36 @@ object ShowcaseRegistry { fun allTags(): List = SAMPLES.flatMap { it.tags }.distinct().sorted() + + @JvmStatic + fun findSampleForActivity(className: String): ShowcaseSample? { + val simpleName = className.substringAfterLast('.') + return SAMPLES.find { sample -> + sample.composeActivity == className || + sample.kotlinActivity == className || + sample.javaActivity == className || + sample.composeActivity?.endsWith(simpleName) == true || + sample.kotlinActivity?.endsWith(simpleName) == true || + sample.javaActivity?.endsWith(simpleName) == true + } + } + + @JvmStatic + fun detectFramework(className: String): FrameworkType { + return when { + className.contains("composedemos") || className.contains("compose") -> FrameworkType.COMPOSE + className.contains("maps3djava") || className.contains("java") -> FrameworkType.JAVA_VIEWS + else -> FrameworkType.KOTLIN_VIEWS + } + } + + @JvmStatic + fun getNextAvailableFramework(sample: ShowcaseSample, current: FrameworkType): FrameworkType? { + val order = when (current) { + FrameworkType.COMPOSE -> listOf(FrameworkType.KOTLIN_VIEWS, FrameworkType.JAVA_VIEWS) + FrameworkType.KOTLIN_VIEWS -> listOf(FrameworkType.JAVA_VIEWS, FrameworkType.COMPOSE) + FrameworkType.JAVA_VIEWS -> listOf(FrameworkType.COMPOSE, FrameworkType.KOTLIN_VIEWS) + } + return order.firstOrNull { sample.isAvailable(it) } + } } diff --git a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt index a801d650..aa7c1d0e 100644 --- a/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt +++ b/Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/showcase/ui/CrossFrameworkSwitcher.kt @@ -19,31 +19,47 @@ package com.example.maps3d.common.showcase.ui import android.app.Activity import android.content.Context import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Color as AndroidColor +import android.view.Menu +import android.view.MenuItem +import android.view.View import android.widget.Toast +import androidx.appcompat.R as AppCompatR import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.example.maps3d.common.showcase.FrameworkType import com.example.maps3d.common.showcase.ShowcaseRegistry +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.button.MaterialButton +import com.google.android.material.R as MaterialR /** * Reusable UI and helper utilities for the cross-framework "Rosetta Stone" sample switcher. @@ -56,6 +72,8 @@ object CrossFrameworkSwitcher { /** * Launches the equivalent sample in the target framework. */ + @JvmStatic + @JvmOverloads fun switchSample( context: Context, sampleId: String, @@ -95,6 +113,62 @@ object CrossFrameworkSwitcher { ).show() } } + + /** + * Configures the [MaterialToolbar] in standard Views (Kotlin/Java) with a back navigation arrow + * and a prominent next-framework cycler button. + */ + @JvmStatic + @JvmOverloads + fun setupToolbarSwitcher( + activity: Activity, + toolbar: MaterialToolbar, + sampleId: String? = null, + ) { + val targetSampleId = sampleId + ?: activity.intent?.getStringExtra("EXTRA_SAMPLE_ID") + ?: ShowcaseRegistry.findSampleForActivity(activity.javaClass.name)?.id + ?: return + + val sample = ShowcaseRegistry.getSampleById(targetSampleId) ?: return + val currentFramework = ShowcaseRegistry.detectFramework(activity.javaClass.name) + val nextFramework = ShowcaseRegistry.getNextAvailableFramework(sample, currentFramework) ?: return + + // Set navigation icon (Back arrow) to finish activity + toolbar.navigationIcon = androidx.appcompat.content.res.AppCompatResources.getDrawable(activity, AppCompatR.drawable.abc_ic_ab_back_material) + toolbar.setNavigationOnClickListener { activity.finish() } + + // Remove any previous switcher button if any + toolbar.findViewWithTag("FRAMEWORK_SWITCHER_TAG")?.let { + toolbar.removeView(it) + } + + val button = MaterialButton( + activity, + null, + MaterialR.attr.materialButtonOutlinedStyle, + ).apply { + tag = "FRAMEWORK_SWITCHER_TAG" + text = "${nextFramework.iconEmoji} ${nextFramework.badge}" + textSize = 12f + setPadding(24, 0, 24, 0) + setTextColor(AndroidColor.WHITE) + strokeColor = ColorStateList.valueOf(AndroidColor.WHITE) + strokeWidth = 2 + cornerRadius = 32 + layoutParams = androidx.appcompat.widget.Toolbar.LayoutParams( + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT, + android.view.Gravity.END or android.view.Gravity.CENTER_VERTICAL, + ).apply { + marginEnd = (16 * activity.resources.displayMetrics.density).toInt() + } + setOnClickListener { + switchSample(activity, sample.id, nextFramework, finishCurrent = true) + } + } + toolbar.addView(button) + } } /** @@ -104,6 +178,7 @@ object CrossFrameworkSwitcher { fun FrameworkSwitchPill( sampleId: String, targetFramework: FrameworkType, + finishCurrent: Boolean = false, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -111,23 +186,23 @@ fun FrameworkSwitchPill( Surface( shape = RoundedCornerShape(16.dp), - color = accentColor.copy(alpha = 0.12f), - border = BorderStroke(1.dp, accentColor.copy(alpha = 0.4f)), + color = accentColor.copy(alpha = 0.15f), + border = BorderStroke(1.dp, accentColor.copy(alpha = 0.5f)), modifier = modifier.clickable { - CrossFrameworkSwitcher.switchSample(context, sampleId, targetFramework) + CrossFrameworkSwitcher.switchSample(context, sampleId, targetFramework, finishCurrent = finishCurrent) }, ) { Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp), + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( imageVector = Icons.Default.SwapHoriz, - contentDescription = "Switch framework", + contentDescription = "Switch to ${targetFramework.displayName}", tint = accentColor, modifier = Modifier.size(16.dp), ) - Spacer(modifier = Modifier.width(6.dp)) + Spacer(modifier = Modifier.width(4.dp)) Text( text = "${targetFramework.iconEmoji} ${targetFramework.badge}", style = MaterialTheme.typography.labelSmall, @@ -137,3 +212,79 @@ fun FrameworkSwitchPill( } } } + +@Composable +fun SampleTopBar( + title: String, + modifier: Modifier = Modifier, + sampleId: String? = null, + onBackClick: (() -> Unit)? = null, + actions: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit = {}, +) { + val context = LocalContext.current + val detectedSample = remember(sampleId) { + sampleId?.let { ShowcaseRegistry.getSampleById(it) } + ?: (context as? Activity)?.let { act -> + act.intent?.getStringExtra("EXTRA_SAMPLE_ID")?.let { ShowcaseRegistry.getSampleById(it) } + ?: ShowcaseRegistry.findSampleForActivity(act.javaClass.name) + } + } + + val currentFramework = FrameworkType.COMPOSE + val nextFramework = remember(detectedSample) { + detectedSample?.let { ShowcaseRegistry.getNextAvailableFramework(it, currentFramework) } + } + + Box( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.85f)) + .statusBarsPadding() + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f), + ) { + IconButton( + onClick = { + onBackClick?.invoke() ?: (context as? Activity)?.finish() + }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurface, + ) + } + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + actions() + if (detectedSample != null && nextFramework != null) { + Spacer(modifier = Modifier.width(4.dp)) + FrameworkSwitchPill( + sampleId = detectedSample.id, + targetFramework = nextFramework, + finishCurrent = true, + ) + } + } + } + } +} diff --git a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java index a323b74a..b177b8ca 100644 --- a/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java +++ b/Maps3DSamples/ApiDemos/java-app/src/main/java/com/example/maps3djava/sampleactivity/SampleBaseActivity.java @@ -99,6 +99,7 @@ protected void onCreate(Bundle savedInstanceState) { MaterialToolbar topBar = findViewById(R.id.top_bar); topBar.setTitle(getTitle()); + com.example.maps3d.common.showcase.ui.CrossFrameworkSwitcher.setupToolbarSwitcher(this, topBar); ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, windowInsets) -> { Insets statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()); diff --git a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt index 6cf1f432..a2942f14 100644 --- a/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt +++ b/Maps3DSamples/ApiDemos/kotlin-app/src/main/java/com/example/maps3dkotlin/sampleactivity/SampleBaseActivity.kt @@ -140,8 +140,8 @@ abstract class SampleBaseActivity : AppCompatActivity(), OnMap3DViewReadyCallbac setContentView(R.layout.activity_common_map) val rootView = findViewById(R.id.map_container) val topBar = findViewById(R.id.top_bar) - topBar.title = title + com.example.maps3d.common.showcase.ui.CrossFrameworkSwitcher.setupToolbarSwitcher(this, topBar) ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars()) diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/advancedcameraanimation/AdvancedCameraAnimationActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/advancedcameraanimation/AdvancedCameraAnimationActivity.kt index 1c9ccc9d..450b3322 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/advancedcameraanimation/AdvancedCameraAnimationActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/advancedcameraanimation/AdvancedCameraAnimationActivity.kt @@ -97,6 +97,7 @@ import com.example.maps3d.common.TourData import com.example.maps3d.common.TrajectoryFlightAnimator import com.example.maps3d.common.WorldState import com.example.maps3d.common.awaitCameraUpdate +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.example.maps3d.common.toCameraUpdate import com.google.android.gms.maps3d.GoogleMap3D import com.google.android.gms.maps3d.model.AltitudeMode @@ -124,7 +125,15 @@ class AdvancedCameraAnimationActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Advanced Camera Animation", + sampleId = "advanced_camera_animation", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Box(modifier = Modifier.fillMaxSize().padding(innerPadding)) { AdvancedCameraAnimationScreen() } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/animatingmodels/AnimatingModelsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/animatingmodels/AnimatingModelsActivity.kt index 2f56d49d..57ea2cdd 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/animatingmodels/AnimatingModelsActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/animatingmodels/AnimatingModelsActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar class AnimatingModelsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,7 +39,15 @@ class AnimatingModelsActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Animating Models", + sampleId = "animating_models", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cameracontrols/CameraControlsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cameracontrols/CameraControlsActivity.kt index 4b24b679..d0dba380 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cameracontrols/CameraControlsActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cameracontrols/CameraControlsActivity.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.camera import com.google.android.gms.maps3d.model.latLngAltitude import com.google.maps.android.compose3d.GoogleMap3D @@ -110,20 +111,11 @@ fun CameraControlsScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Camera Controls", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Camera Controls", + sampleId = "camera_controls", + ) // 3. Controls Panel at the bottom Card( diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/camerarestrictions/CameraRestrictionsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/camerarestrictions/CameraRestrictionsActivity.kt index b81264d3..f2994e3f 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/camerarestrictions/CameraRestrictionsActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/camerarestrictions/CameraRestrictionsActivity.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.camera import com.google.android.gms.maps3d.model.cameraRestriction import com.google.android.gms.maps3d.model.latLngAltitude @@ -129,20 +130,11 @@ fun CameraRestrictionsScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Camera Restrictions", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Camera Restrictions", + sampleId = "camera_restrictions", + ) // 3. Info Card Card( diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt index 802b7f50..db04a9df 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/cloudstyling/CloudStylingActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar class CloudStylingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,7 +39,15 @@ class CloudStylingActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Cloud-based Map Styling", + sampleId = "cloud_styling", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/datavisualization/DataVisualizationActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/datavisualization/DataVisualizationActivity.kt index 19b32770..31ac051d 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/datavisualization/DataVisualizationActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/datavisualization/DataVisualizationActivity.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps3d.GoogleMap3D import com.google.android.gms.maps3d.model.AltitudeMode diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/fieldofview/FieldOfViewActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/fieldofview/FieldOfViewActivity.kt index e5033938..3eb132b1 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/fieldofview/FieldOfViewActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/fieldofview/FieldOfViewActivity.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps3d.model.Camera import com.google.android.gms.maps3d.model.Map3DMode diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/flightsimulator/FlightSimulatorActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/flightsimulator/FlightSimulatorActivity.kt index 29a5face..f4419045 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/flightsimulator/FlightSimulatorActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/flightsimulator/FlightSimulatorActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar class FlightSimulatorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,7 +39,15 @@ class FlightSimulatorActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Flight Simulator", + sampleId = "flight_simulator", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/hellomap/HelloMapActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/hellomap/HelloMapActivity.kt index 45caf24a..5068890c 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/hellomap/HelloMapActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/hellomap/HelloMapActivity.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.camera import com.google.android.gms.maps3d.model.latLngAltitude import com.google.maps.android.compose3d.GoogleMap3D @@ -102,21 +103,10 @@ fun HelloMapScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - // 75% opaque surface color - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - // Respect status bar / cutout area for padding the content - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Hello Map", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Hello Map", + sampleId = "hello_map", + ) } } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/mapinteractions/MapInteractionsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/mapinteractions/MapInteractionsActivity.kt index dddc866c..c79dc7c9 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/mapinteractions/MapInteractionsActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/mapinteractions/MapInteractionsActivity.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.camera import com.google.android.gms.maps3d.model.latLngAltitude @@ -114,20 +115,11 @@ fun MapInteractionsScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Map Interactions", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Map Interactions", + sampleId = "map_interactions", + ) // 3. Click Info Card Card( diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/markers/MarkersActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/markers/MarkersActivity.kt index f6d18e8a..28074d66 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/markers/MarkersActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/markers/MarkersActivity.kt @@ -46,6 +46,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.example.composedemos.R +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.AltitudeMode import com.google.android.gms.maps3d.model.CollisionBehavior import com.google.android.gms.maps3d.model.ImageView @@ -217,19 +218,10 @@ fun MarkersScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Markers", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Markers", + sampleId = "markers", + ) } } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/models/ModelsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/models/ModelsActivity.kt index c4baf59f..1c0ffdef 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/models/ModelsActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/models/ModelsActivity.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.AltitudeMode import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.camera @@ -156,20 +157,11 @@ private fun ModelsScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Models", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "3D Models", + sampleId = "models", + ) // 3. UI Controls (FABs) FloatingActionButton( diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathfollowing/PathFollowingActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathfollowing/PathFollowingActivity.kt index 3b6cf0fa..0a727775 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathfollowing/PathFollowingActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathfollowing/PathFollowingActivity.kt @@ -93,6 +93,7 @@ import com.example.maps3d.common.PathData import com.example.maps3d.common.PathEngine import com.example.maps3d.common.PathFollowingViewModel import com.example.maps3d.common.PathPlaybackState +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.AltitudeMode import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.camera @@ -110,7 +111,15 @@ class PathFollowingActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Path Following", + sampleId = "path_following", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Surface( modifier = Modifier .fillMaxSize() diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathstyling/PathStylingActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathstyling/PathStylingActivity.kt index f999a024..7648a7c3 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathstyling/PathStylingActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/pathstyling/PathStylingActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar class PathStylingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,7 +39,15 @@ class PathStylingActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Path Styling", + sampleId = "path_styling", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placeautocomplete/PlaceAutocompleteActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placeautocomplete/PlaceAutocompleteActivity.kt index e3e8f2d3..b426af21 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placeautocomplete/PlaceAutocompleteActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placeautocomplete/PlaceAutocompleteActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar class PlaceAutocompleteActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,7 +39,15 @@ class PlaceAutocompleteActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Place Autocomplete", + sampleId = "place_autocomplete", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placedetails/PlaceDetailsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placedetails/PlaceDetailsActivity.kt index 80b08ad8..4fffad71 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placedetails/PlaceDetailsActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placedetails/PlaceDetailsActivity.kt @@ -65,6 +65,7 @@ import androidx.fragment.app.commit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.example.composedemos.R +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.Camera import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.camera diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placesearch/PlaceSearchActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placesearch/PlaceSearchActivity.kt index bdff996a..eaabad8c 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placesearch/PlaceSearchActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/placesearch/PlaceSearchActivity.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar class PlaceSearchActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -38,7 +39,15 @@ class PlaceSearchActivity : ComponentActivity() { enableEdgeToEdge() setContent { MaterialTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Scaffold( + topBar = { + SampleTopBar( + title = "Place Search", + sampleId = "place_search", + ) + }, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> Box( modifier = Modifier .fillMaxSize() diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polygons/PolygonsActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polygons/PolygonsActivity.kt index c75df91b..8bc01fa6 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polygons/PolygonsActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polygons/PolygonsActivity.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.AltitudeMode import com.google.android.gms.maps3d.model.LatLngAltitude import com.google.android.gms.maps3d.model.Map3DMode @@ -244,20 +245,11 @@ fun PolygonsScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Polygons", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Polygons", + sampleId = "polygons", + ) } } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polylines/PolylinesActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polylines/PolylinesActivity.kt index cdbdc2c6..e604f33b 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polylines/PolylinesActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/polylines/PolylinesActivity.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.AltitudeMode import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.camera @@ -148,19 +149,10 @@ fun PolylinesScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Polylines", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Polylines", + sampleId = "polylines", + ) } } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/popovers/PopoversActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/popovers/PopoversActivity.kt index 09290daa..490d11c5 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/popovers/PopoversActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/popovers/PopoversActivity.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps3d.model.AltitudeMode import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.camera @@ -153,19 +154,10 @@ fun PopoversScreen() { }, ) - // 2. Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Text( - text = "Popovers", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - } + // 2. Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Popovers", + sampleId = "popovers", + ) } } diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/roadmapmode/RoadmapModeActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/roadmapmode/RoadmapModeActivity.kt index d5829b06..ca6fa922 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/roadmapmode/RoadmapModeActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/roadmapmode/RoadmapModeActivity.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps3d.model.Map3DMode import com.google.android.gms.maps3d.model.camera @@ -99,6 +100,10 @@ fun RoadmapModeScreen() { } Box(modifier = Modifier.fillMaxSize()) { + SampleTopBar( + title = "Map Modes", + sampleId = "roadmap_mode", + ) GoogleMap3D( camera = currentCameraState, mapMode = selectedMapMode, diff --git a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/routes/RoutesActivity.kt b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/routes/RoutesActivity.kt index 45134bc2..5113bac4 100644 --- a/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/routes/RoutesActivity.kt +++ b/Maps3DSamples/ComposeDemos/app/src/main/java/com/example/composedemos/routes/RoutesActivity.kt @@ -95,6 +95,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.example.composedemos.BuildConfig import com.example.composedemos.R +import com.example.maps3d.common.showcase.ui.SampleTopBar import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps3d.model.AltitudeMode import com.google.android.gms.maps3d.model.Camera @@ -413,25 +414,11 @@ fun RouteSampleScreen(viewModel: RouteViewModel) { }, ) - // Custom Translucent Top Bar - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.75f)) - .statusBarsPadding() - .padding(horizontal = 16.dp, vertical = 8.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = "Routes API", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - + // Top Bar with Cross-Framework Switcher + SampleTopBar( + title = "Routes API", + sampleId = "routes", + actions = { IconButton(onClick = { currentTracker = when (currentTracker) { RouteTracker.Marker -> RouteTracker.RedCar @@ -449,8 +436,8 @@ fun RouteSampleScreen(viewModel: RouteViewModel) { tint = MaterialTheme.colorScheme.onSurface, ) } - } - } + }, + ) // Interactive Overlay UI if (!flyModeActive) {