From b51c78a0c50ff85dc281adfa76599d524782588f Mon Sep 17 00:00:00 2001
From: Dale Hawkins <107309+dkhawk@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:42:45 -0600
Subject: [PATCH] feat(scenarios): add scenic world tour with mid-orbit map
mode transitions and immersive recording UX
- Introduce scenic_world_tour with 6 photorealistic destinations: Copacabana (Rio), Mt Fuji, Grand Canyon, Golden Gate Bridge, The Shard (London), and Seattle Space Needle.
- Support seamless mid-orbit map mode switching (switchMapMode in FlyAroundStep) revealing hybrid labels and 3D building POIs dynamically mid-orbit.
- Surface direct tour shortcuts on MainActivity launcher for instant access.
- Implement sticky immersive mode in ScenariosActivity to hide system status and navigation bars during scenario playback for clean screen recordings.
- Remove skip button overlay during tour playback for unobscured recording view.
- Fix coroutine await and state flow exposure in Map3dViewModel.
---
.../advanced/app/src/main/AndroidManifest.xml | 9 +-
.../advancedmaps3dsamples/MainActivity.kt | 25 +++-
.../common/Map3dViewModel.kt | 6 +-
.../scenarios/Animations.kt | 31 ++++-
.../scenarios/Scenario.kt | 3 +
.../scenarios/ScenarioData.kt | 122 ++++++++++++++++++
.../scenarios/ScenarioMapper.kt | 14 +-
.../scenarios/ScenariosActivity.kt | 60 +++++++--
.../scenarios/ScenariosViewModel.kt | 61 +++++++--
.../scenarios/ThreeDMap.kt | 4 +-
.../app/src/main/res/values/strings.xml | 4 +
11 files changed, 302 insertions(+), 37 deletions(-)
diff --git a/Maps3DSamples/advanced/app/src/main/AndroidManifest.xml b/Maps3DSamples/advanced/app/src/main/AndroidManifest.xml
index c573539a..11cffce5 100644
--- a/Maps3DSamples/advanced/app/src/main/AndroidManifest.xml
+++ b/Maps3DSamples/advanced/app/src/main/AndroidManifest.xml
@@ -49,7 +49,14 @@
+ android:launchMode="singleTop">
+
+
+
+
+
+
+
)
+data class MapSample(
+ @StringRes val label: Int,
+ val clazz: Class<*>,
+ val scenario: String? = null,
+)
private val samples =
listOf(
- MapSample(R.string.map_sample_basic, BasicComposeMapActivity::class.java),
+ MapSample(R.string.scenarios_seattle_cinematic_reveal, ScenariosActivity::class.java, "seattle_cinematic_reveal"),
+ MapSample(R.string.scenarios_uk_shard_tower_bridge, ScenariosActivity::class.java, "uk_shard_to_tower_bridge"),
+ MapSample(R.string.scenarios_brazil_copacabana_ipanema, ScenariosActivity::class.java, "brazil_copacabana_to_ipanema"),
+ MapSample(R.string.scenarios_scenic_world_tour, ScenariosActivity::class.java, "scenic_world_tour"),
MapSample(R.string.map_sample_scenarios, ScenariosActivity::class.java),
+ MapSample(R.string.map_sample_basic, BasicComposeMapActivity::class.java),
MapSample(R.string.map_sample_route, RouteSampleActivity::class.java),
)
@@ -83,7 +91,7 @@ class MainActivity : ComponentActivity() {
for (sample in samples) {
Sample(
sample = sample,
- onClick = { launchActivity(sample.clazz) },
+ onClick = { launchActivity(sample) },
modifier = Modifier.fillMaxWidth(),
)
}
@@ -94,12 +102,15 @@ class MainActivity : ComponentActivity() {
}
- private fun launchActivity(clazz: Class<*>) {
+ private fun launchActivity(sample: MapSample) {
try {
- startActivity(Intent(this, clazz))
+ val intent = Intent(this, sample.clazz).apply {
+ sample.scenario?.let { putExtra("scenario", it) }
+ }
+ startActivity(intent)
} catch (e: ClassNotFoundException) {
- Log.e("SamplesMenu", "Activity not found: $clazz", e)
- Toast.makeText(this, "Activity not found: $clazz", Toast.LENGTH_SHORT).show()
+ Log.e("SamplesMenu", "Activity not found: ${sample.clazz}", e)
+ Toast.makeText(this, "Activity not found: ${sample.clazz}", Toast.LENGTH_SHORT).show()
}
}
}
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/common/Map3dViewModel.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/common/Map3dViewModel.kt
index 2b872293..a4ee0e46 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/common/Map3dViewModel.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/common/Map3dViewModel.kt
@@ -68,6 +68,7 @@ abstract class Map3dViewModel : ViewModel() {
* Consumers should use the `mapReady` flow to react to the availability of the map.
*/
private var _googleMap3D = MutableStateFlow(null)
+ val googleMap3D = _googleMap3D.asStateFlow()
private val _cameraRestriction = MutableStateFlow(null)
val cameraRestriction = _cameraRestriction.asStateFlow()
@@ -402,8 +403,7 @@ abstract class Map3dViewModel : ViewModel() {
}
suspend fun awaitCameraUpdate(cameraUpdate: CameraUpdate) {
- _googleMap3D.value?.let { controller ->
- com.example.advancedmaps3dsamples.utils.awaitCameraUpdate(controller, cameraUpdate)
- }
+ val controller = _googleMap3D.filterNotNull().first()
+ com.example.advancedmaps3dsamples.utils.awaitCameraUpdate(controller, cameraUpdate)
}
}
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Animations.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Animations.kt
index f1b97e52..08c8a763 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Animations.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Animations.kt
@@ -16,7 +16,9 @@ package com.example.advancedmaps3dsamples.scenarios
import com.google.android.gms.maps3d.model.FlyAroundOptions
import com.google.android.gms.maps3d.model.FlyToOptions
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
sealed interface AnimationStep {
suspend operator fun invoke(viewModel: ScenariosViewModel)
@@ -46,8 +48,33 @@ data class FlyToStep(val flyToOptions: FlyToOptions) : AnimationStep {
}
}
-data class FlyAroundStep(val flyAroundOptions: FlyAroundOptions) : AnimationStep {
+data class FlyAroundStep(
+ val flyAroundOptions: FlyAroundOptions,
+ val switchMapMode: Int? = null,
+ val switchDelayMillis: Long = 0L,
+) : AnimationStep {
override suspend operator fun invoke(viewModel: ScenariosViewModel) {
- viewModel.awaitFlyAround(flyAroundOptions)
+ if (switchMapMode != null && switchDelayMillis > 0) {
+ coroutineScope {
+ val switchJob = launch {
+ delay(switchDelayMillis)
+ android.util.Log.i("FlyAroundStep", "Switching map mode to $switchMapMode after ${switchDelayMillis}ms")
+ viewModel.setMapMode(switchMapMode)
+ }
+ try {
+ viewModel.awaitFlyAround(flyAroundOptions)
+ } finally {
+ switchJob.cancel()
+ }
+ }
+ } else {
+ viewModel.awaitFlyAround(flyAroundOptions)
+ }
+ }
+}
+
+data class SetMapModeStep(val mapMode: Int) : AnimationStep {
+ override suspend operator fun invoke(viewModel: ScenariosViewModel) {
+ viewModel.setMapMode(mapMode)
}
}
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Scenario.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Scenario.kt
index f2390032..8dbd2b3b 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Scenario.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Scenario.kt
@@ -34,6 +34,7 @@ data class Scenario(
val models: List = emptyList(), // Add models property
val polylines: List = emptyList(),
val polygons: List = emptyList(),
+ val loop: Boolean = false,
) {
@Composable fun getTitle() = stringResource(titleId)
@@ -70,6 +71,7 @@ fun createScenario(
models: String = "",
polylines: String = "",
polygon: String? = null,
+ loop: Boolean = false,
): Scenario {
return Scenario(
name = name,
@@ -80,6 +82,7 @@ fun createScenario(
models = models.toModels(),
polylines = polylines.toPolylines(),
polygons = polygon?.toPolygons() ?: emptyList(),
+ loop = loop,
)
}
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioData.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioData.kt
index 8da75307..5ab1fcc4 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioData.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioData.kt
@@ -183,6 +183,128 @@ val denverPolygon = """
val scenarios =
listOf(
+ createScenario(
+ name = "seattle_cinematic_reveal",
+ titleId = R.string.scenarios_seattle_cinematic_reveal,
+ initialState =
+ "mode=roadmap;camera=lat=47.6100,lng=-122.3950,alt=1200.0,hdg=55.0,tilt=60.0,range=7500.0",
+ animationString =
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ // Stop 1: Space Needle, Seattle (Fly-in in ROADMAP, orbit, and reveal HYBRID mid-orbit)
+ "setMapMode=roadmap;" +
+ "flyTo=lat=47.620506,lng=-122.349277,alt=120.0,hdg=135.0,tilt=67.0,range=650.0,dur=4500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=47.620506,lng=-122.349277,alt=120.0,hdg=135.0,tilt=67.0,range=650.0,dur=20000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1500;" +
+ // Stop 2: Downtown Seattle & Columbia Center (Glide to downtown high-rises in HYBRID)
+ "flyTo=lat=47.604500,lng=-122.330700,alt=180.0,hdg=315.0,tilt=68.0,range=750.0,dur=4000;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=47.604500,lng=-122.330700,alt=180.0,hdg=315.0,tilt=68.0,range=750.0,dur=18000,count=1.0;" +
+ "delay=dur=1500",
+ loop = true,
+ ),
+ createScenario(
+ name = "uk_shard_to_tower_bridge",
+ titleId = R.string.scenarios_uk_shard_tower_bridge,
+ initialState =
+ "mode=satellite;camera=lat=51.504500,lng=-0.086500,alt=140.0,hdg=35.0,tilt=55.0,range=1200.0",
+ animationString =
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ // Stop 1: The Shard, London
+ "setMapMode=satellite;" +
+ "flyTo=lat=51.504500,lng=-0.086500,alt=140.0,hdg=35.0,tilt=68.0,range=650.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=51.504500,lng=-0.086500,alt=140.0,hdg=35.0,tilt=68.0,range=650.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // Stop 2: Tower Bridge, London
+ "setMapMode=satellite;" +
+ "flyTo=lat=51.505500,lng=-0.075400,alt=20.0,hdg=-16.4,tilt=65.0,range=564.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=51.505500,lng=-0.075400,alt=20.0,hdg=-16.4,tilt=65.0,range=564.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ "setMapMode=satellite",
+ loop = true,
+ ),
+ createScenario(
+ name = "brazil_copacabana_to_ipanema",
+ titleId = R.string.scenarios_brazil_copacabana_ipanema,
+ initialState =
+ "mode=satellite;camera=lat=-22.971100,lng=-43.182500,alt=25.0,hdg=38.0,tilt=55.0,range=2200.0",
+ animationString =
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ // Stop 1: Copacabana Beach, Rio de Janeiro
+ "setMapMode=satellite;" +
+ "flyTo=lat=-22.971100,lng=-43.182500,alt=25.0,hdg=38.0,tilt=68.0,range=1350.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=-22.971100,lng=-43.182500,alt=25.0,hdg=38.0,tilt=68.0,range=1350.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // Stop 2: Ipanema Beach & Arpoador, Rio de Janeiro
+ "setMapMode=satellite;" +
+ "flyTo=lat=-22.988800,lng=-43.192600,alt=25.0,hdg=282.0,tilt=67.0,range=1150.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=-22.988800,lng=-43.192600,alt=25.0,hdg=282.0,tilt=67.0,range=1150.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ "setMapMode=satellite",
+ loop = true,
+ ),
+ createScenario(
+ name = "scenic_world_tour",
+ titleId = R.string.scenarios_scenic_world_tour,
+ initialState =
+ "mode=satellite;camera=lat=51.504500,lng=-0.086500,alt=140.0,hdg=35.0,tilt=55.0,range=1200.0",
+ animationString =
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ // 1. UK: The Shard, London
+ "setMapMode=satellite;" +
+ "flyTo=lat=51.504500,lng=-0.086500,alt=140.0,hdg=35.0,tilt=68.0,range=650.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=51.504500,lng=-0.086500,alt=140.0,hdg=35.0,tilt=68.0,range=650.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // 2. UK: Tower Bridge, London
+ "setMapMode=satellite;" +
+ "flyTo=lat=51.505500,lng=-0.075400,alt=20.0,hdg=-16.4,tilt=65.0,range=564.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=51.505500,lng=-0.075400,alt=20.0,hdg=-16.4,tilt=65.0,range=564.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // 3. Brazil: Copacabana Beach, Rio de Janeiro
+ "setMapMode=satellite;" +
+ "flyTo=lat=-22.971100,lng=-43.182500,alt=25.0,hdg=38.0,tilt=68.0,range=1350.0,dur=4500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=-22.971100,lng=-43.182500,alt=25.0,hdg=38.0,tilt=68.0,range=1350.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // 4. Brazil: Ipanema Beach & Arpoador, Rio de Janeiro
+ "setMapMode=satellite;" +
+ "flyTo=lat=-22.988800,lng=-43.192600,alt=25.0,hdg=282.0,tilt=67.0,range=1150.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=-22.988800,lng=-43.192600,alt=25.0,hdg=282.0,tilt=67.0,range=1150.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // 5. USA: Golden Gate Bridge & Marin Headlands (wide framing to capture full span and bay)
+ "setMapMode=satellite;" +
+ "flyTo=lat=37.819900,lng=-122.478300,alt=180.0,hdg=106.0,tilt=62.0,range=4000.0,dur=4500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=37.819900,lng=-122.478300,alt=180.0,hdg=106.0,tilt=62.0,range=4000.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // 6. USA: Grand Canyon South Rim (panoramic vista of canyon gorges and red strata)
+ "setMapMode=satellite;" +
+ "flyTo=lat=36.057000,lng=-112.143000,alt=2200.0,hdg=35.0,tilt=68.0,range=14000.0,dur=4000;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=36.057000,lng=-112.143000,alt=2200.0,hdg=35.0,tilt=68.0,range=14000.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // 7. USA: Delicate Arch, Arches National Park
+ "setMapMode=satellite;" +
+ "flyTo=lat=38.743502,lng=-109.499374,alt=1467.0,hdg=-10.4,tilt=58.1,range=180.0,dur=3500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=38.743502,lng=-109.499374,alt=1467.0,hdg=-10.4,tilt=58.1,range=180.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ // 8. Japan: Mount Fuji Summit Crater
+ "setMapMode=satellite;" +
+ "flyTo=lat=35.360600,lng=138.727400,alt=3650.0,hdg=145.0,tilt=64.0,range=4200.0,dur=4500;" +
+ "waitUntilTheMapIsSteady=timeout=10000;" +
+ "flyAround=lat=35.360600,lng=138.727400,alt=3650.0,hdg=145.0,tilt=64.0,range=4200.0,dur=12000,count=1.0,switchmode=hybrid,switchdelay=6000;" +
+ "delay=dur=1000;" +
+ "setMapMode=satellite",
+ loop = true,
+ ),
createScenario(
name = "tower_bridge",
titleId = R.string.scenarios_tower_bridge,
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioMapper.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioMapper.kt
index 03892003..acdff54c 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioMapper.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioMapper.kt
@@ -152,6 +152,7 @@ fun String.toMap3DMode(): Int {
return when (this.lowercase()) {
"hybrid" -> Map3DMode.HYBRID
"satellite" -> Map3DMode.SATELLITE
+ "roadmap" -> Map3DMode.ROADMAP
else -> {
Log.w(TAG, "Unsupported map mode '$this', defaulting to SATELLITE.")
Map3DMode.SATELLITE
@@ -172,7 +173,18 @@ fun String.toAnimation(): List {
when (key.trim().lowercase()) {
"flyto" -> add(FlyToStep(value.toFlyTo()))
"delay" -> add(DelayStep(value.toDelay()))
- "flyaround" -> add(FlyAroundStep(value.toFlyAround()))
+ "flyaround" -> {
+ val options = value.toFlyAround()
+ val attributes = value.toAttributesMap()
+ val switchModeStr = attributes["switchmode"] ?: attributes["switchMode"]
+ val switchMode = switchModeStr?.toMap3DMode()
+ val switchDelay = (attributes["switchdelay"] ?: attributes["switchDelay"])?.toLongOrNull() ?: 0L
+ Log.d(TAG, "Parsed flyAround: switchMode=$switchMode ($switchModeStr), switchDelay=$switchDelay")
+ add(FlyAroundStep(options, switchMapMode = switchMode, switchDelayMillis = switchDelay))
+ }
+ "setmapmode", "mapmode" -> {
+ add(SetMapModeStep(value.trim().toMap3DMode()))
+ }
"waituntilthemapissteady" -> {
add(WaitUntilTheMapIsSteadyStep(value.toTimeout()))
}
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosActivity.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosActivity.kt
index d7f87b90..8aac6412 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosActivity.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosActivity.kt
@@ -33,7 +33,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.filled.CameraAlt
@@ -44,11 +46,13 @@ import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -60,6 +64,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.advancedmaps3dsamples.R
import com.example.advancedmaps3dsamples.ui.theme.AdvancedMaps3DSamplesTheme
@@ -79,14 +86,14 @@ class ScenariosActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
- intent.extras?.getString("scenario")?.let { scenarioName: String ->
- viewModel.setScenario(scenarioName)
- }
+ setIntent(intent)
+ handleScenarioIntent(intent)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
+ handleScenarioIntent(intent)
setContent {
val viewState by viewModel.viewState.collectAsStateWithLifecycle()
val currentCamera by viewModel.currentCamera.collectAsStateWithLifecycle(Camera.DEFAULT_CAMERA)
@@ -96,24 +103,40 @@ class ScenariosActivity : ComponentActivity() {
val cameraAttribute by viewModel.trackedAttribute.collectAsStateWithLifecycle()
+ DisposableEffect(viewState.scenario) {
+ val insetsController = WindowCompat.getInsetsController(window, window.decorView)
+ if (viewState.scenario != null) {
+ insetsController.systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ insetsController.hide(WindowInsetsCompat.Type.systemBars())
+ } else {
+ insetsController.show(WindowInsetsCompat.Type.systemBars())
+ }
+ onDispose {
+ insetsController.show(WindowInsetsCompat.Type.systemBars())
+ }
+ }
+
AdvancedMaps3DSamplesTheme(
dynamicColor = false
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
- CenterAlignedTopAppBar(
- colors = topAppBarColors(
- containerColor = MaterialTheme.colorScheme.primaryContainer,
- titleContentColor = MaterialTheme.colorScheme.primary,
- ),
- title = {
- Text(stringResource(viewState.scenario?.titleId ?: R.string.scenarios_none))
- }
- )
+ if (viewState.scenario == null) {
+ CenterAlignedTopAppBar(
+ colors = topAppBarColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer,
+ titleContentColor = MaterialTheme.colorScheme.primary,
+ ),
+ title = {
+ Text(stringResource(R.string.scenarios_none))
+ },
+ )
+ }
}
) { innerPadding ->
- val modifier = Modifier.padding(innerPadding)
+ val modifier = if (viewState.scenario == null) Modifier.padding(innerPadding) else Modifier
viewState.scenario?.let { scenario ->
Box(modifier = modifier.fillMaxSize()) {
@@ -164,6 +187,16 @@ class ScenariosActivity : ComponentActivity() {
}
}
}
+
+ private fun handleScenarioIntent(intent: Intent?) {
+ val scenarioName =
+ intent?.extras?.getString("scenario")
+ ?: intent?.data?.lastPathSegment
+ ?: intent?.data?.host
+ if (!scenarioName.isNullOrBlank()) {
+ viewModel.setScenario(scenarioName)
+ }
+ }
}
/**
@@ -182,6 +215,7 @@ fun ScenarioPicker(modifier: Modifier, onScenarioClicked: (Scenario) -> Unit) {
modifier = modifier
.testTag("scenario_picker")
.fillMaxSize()
+ .verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosViewModel.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosViewModel.kt
index 63e6f499..8214239b 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosViewModel.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenariosViewModel.kt
@@ -80,6 +80,8 @@ class ScenariosViewModel @Inject constructor() : Map3dViewModel() {
private val _roll = MutableStateFlow(DEFAULT_ROLL)
val roll = _roll as StateFlow
+ private var currentStepIndex: Int = 0
+
init {
viewModelScope.launch {
viewState
@@ -186,8 +188,33 @@ class ScenariosViewModel @Inject constructor() : Map3dViewModel() {
setScenario(nextScenario) // Set the next scenario
}
+ /**
+ * Advances to the next FlyTo stop within the current scenario, wrapping around to the first
+ * FlyTo stop if at the end. If the scenario has fewer than 2 FlyTo stops, advances to the next
+ * scenario.
+ */
+ fun nextStop() {
+ val scenario = _viewState.value.scenario ?: return
+ val steps = scenario.animationSteps
+ val flyToIndices = steps.indices.filter { steps[it] is FlyToStep }
+ if (flyToIndices.size < 2) {
+ nextScenario()
+ return
+ }
+ val nextFlyToIndex =
+ flyToIndices.firstOrNull { it > currentStepIndex } ?: flyToIndices.first()
+ val actualStartIndex =
+ if (nextFlyToIndex > 0 && steps[nextFlyToIndex - 1] is SetMapModeStep) {
+ nextFlyToIndex - 1
+ } else {
+ nextFlyToIndex
+ }
+ Log.i(TAG, "nextStop called, currentStepIndex=$currentStepIndex, jumping to $actualStartIndex")
+ playAnimation(steps, startStepIndex = actualStartIndex, showCountDown = false)
+ }
+
private fun isMapReady(): Boolean {
- return _viewState.value.scenario != null
+ return _viewState.value.scenario != null && googleMap3D.value != null
}
/**
@@ -195,19 +222,27 @@ class ScenariosViewModel @Inject constructor() : Map3dViewModel() {
*
* The function first cancels any ongoing animation jobs.
* It then starts a countdown displayed to the user before executing the animation steps.
- * The animation steps are executed sequentially.
- * After the animation is finished, a delay is introduced before the "finished" state is shown.
+ * The animation steps are executed sequentially, looping back to the first [FlyToStep] if
+ * the current scenario has `loop = true`.
*
* @param animation The list of [AnimationStep] to play.
+ * @param startStepIndex The step index to start playback from.
+ * @param showCountDown Whether to display the 3-second countdown overlay before starting.
*/
- private fun playAnimation(animation: List) {
+ private fun playAnimation(
+ animation: List,
+ startStepIndex: Int = 0,
+ showCountDown: Boolean = true,
+ ) {
animationJob?.cancel()
animationJob =
viewModelScope.launch {
- for (count in 3 downTo 1) {
- _viewState.value = viewState.value.copy(countDown = count, countDownVisible = true)
- delay(1.seconds)
+ if (showCountDown) {
+ for (count in 3 downTo 1) {
+ _viewState.value = viewState.value.copy(countDown = count, countDownVisible = true)
+ delay(1.seconds)
+ }
}
while (!isMapReady()) {
@@ -215,7 +250,17 @@ class ScenariosViewModel @Inject constructor() : Map3dViewModel() {
}
_viewState.value = viewState.value.copy(countDown = 0, countDownVisible = false)
- animation.forEach { step -> step(this@ScenariosViewModel) }
+ var index = startStepIndex
+ while (index < animation.size) {
+ currentStepIndex = index
+ Log.i(TAG, "Playing step $index: ${animation[index]}")
+ animation[index](this@ScenariosViewModel)
+ index++
+ if (index >= animation.size && _viewState.value.scenario?.loop == true) {
+ val loopStartIndex = animation.indexOfFirst { it is FlyToStep || it is SetMapModeStep }
+ index = if (loopStartIndex >= 0) loopStartIndex else 0
+ }
+ }
delay(2.seconds)
_viewState.value = viewState.value.copy(showFinished = true)
}
diff --git a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ThreeDMap.kt b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ThreeDMap.kt
index 8bead431..632cc264 100644
--- a/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ThreeDMap.kt
+++ b/Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ThreeDMap.kt
@@ -45,7 +45,7 @@ internal fun ThreeDMap(
viewModel.onMapSteadyChange(isSteady)
}
- val map3dView = remember(mapsConfig) {
+ val map3dView = remember(mapsConfig.centerLat, mapsConfig.centerLng, mapsConfig.range, mapsConfig.heading, mapsConfig.tilt) {
Map3DView(context = context, config = mapsConfig).apply {
getMap3DViewAsync(
object : OnMap3DViewReadyCallback {
@@ -64,7 +64,7 @@ internal fun ThreeDMap(
}
}
- DisposableEffect(lifecycleOwner, map3dView) {
+ DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> map3dView.onCreate(null)
diff --git a/Maps3DSamples/advanced/app/src/main/res/values/strings.xml b/Maps3DSamples/advanced/app/src/main/res/values/strings.xml
index 0a9ce978..d3e6e518 100644
--- a/Maps3DSamples/advanced/app/src/main/res/values/strings.xml
+++ b/Maps3DSamples/advanced/app/src/main/res/values/strings.xml
@@ -48,6 +48,10 @@
Pick a scenario
+ Seattle: Roadmap to Hybrid Cinematic Orbit (Loop)
+ UK: The Shard to Tower Bridge (Loop)
+ Brazil: Copacabana to Ipanema & Arpoador (Loop)
+ Scenic World Tour: Natural & Cityscapes (Loop)
Tower Bridge
NYC hybrid
NYC satellite