Skip to content

Commit fbefa67

Browse files
authored
feat(animations): Add map steady check for better animations (#20)
* feat(animations): Add map steady check for smoother animations Introduces a mechanism to wait for the map to be fully loaded and idle (isMapSteady) before initiating camera animations. This prevents animations from starting on a partially rendered or moving map, resulting in a smoother and more professional user experience. - Adds awaitMapSteady() to Map3dViewModel to suspend execution until the map is stable, with an optional timeout. - Creates a new WaitUntilTheMapIsSteadyStep for use in declarative animation sequences. - Refactors ThreeDMap, ScenarioScreen, and CameraControlDemoScreen to pass the ViewModel directly, simplifying state management and removing the need for onMap3dViewReady and onReleaseMap callbacks. - Integrates the steady check into existing animation scenarios to ensure they begin from a stable map state. * fix: remove param from @stringres parameter
1 parent ad1cbca commit fbefa67

File tree

11 files changed

+167
-99
lines changed

11 files changed

+167
-99
lines changed

Maps3DSamples/ApiDemos/common/src/main/java/com/example/maps3d/common/Units.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ fun Meters.plus(other: Meters) = Meters(value = this.value + other.value)
9393
* @property value: The numerical value.
9494
* @property unitsTemplate: The string resource ID for the units.
9595
*/
96-
data class ValueWithUnitsTemplate(val value: Double, @param:StringRes val unitsTemplate: Int)
96+
data class ValueWithUnitsTemplate(val value: Double, @StringRes val unitsTemplate: Int)
9797

9898
/** Abstract base class for all units converters. */
9999
abstract class UnitsConverter {

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/common/Map3dViewModel.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ import kotlinx.coroutines.channels.awaitClose
4444
import kotlinx.coroutines.flow.Flow
4545
import kotlinx.coroutines.flow.MutableSharedFlow
4646
import kotlinx.coroutines.flow.MutableStateFlow
47+
import kotlinx.coroutines.TimeoutCancellationException
4748
import kotlinx.coroutines.flow.asStateFlow
4849
import kotlinx.coroutines.flow.callbackFlow
4950
import kotlinx.coroutines.flow.filterNotNull
51+
import kotlinx.coroutines.flow.first
5052
import kotlinx.coroutines.flow.map
5153
import kotlinx.coroutines.launch
54+
import kotlinx.coroutines.withTimeout
5255
import kotlin.time.Duration
56+
import kotlin.time.ExperimentalTime
5357

5458
abstract class Map3dViewModel : ViewModel() {
5559
abstract val TAG: String
@@ -68,6 +72,9 @@ abstract class Map3dViewModel : ViewModel() {
6872
private val _cameraRestriction = MutableStateFlow<CameraRestriction?>(null)
6973
val cameraRestriction = _cameraRestriction.asStateFlow()
7074

75+
private val _isMapSteady = MutableStateFlow(false)
76+
val isMapSteady = _isMapSteady.asStateFlow()
77+
7178
private val _mapMode = MutableStateFlow(Map3DMode.SATELLITE)
7279
val mapMode = _mapMode.asStateFlow()
7380

@@ -292,6 +299,32 @@ abstract class Map3dViewModel : ViewModel() {
292299
_mapMode.value = mode
293300
}
294301

302+
fun onMapSteadyChange(isSteady: Boolean) {
303+
_isMapSteady.value = isSteady
304+
}
305+
306+
/**
307+
* Suspends until the map has entered a steady state, meaning it is not moving and has
308+
* finished loading. This is useful to ensure that camera animations start from a stable
309+
* and predictable state.
310+
*
311+
* @param timeoutMillis The maximum time to wait in milliseconds. A value of 0 means no timeout.
312+
*/
313+
@OptIn(ExperimentalTime::class)
314+
suspend fun awaitMapSteady(timeoutMillis: Long) {
315+
if (timeoutMillis > 0) {
316+
try {
317+
withTimeout(timeoutMillis) {
318+
isMapSteady.first { it }
319+
}
320+
} catch (_: TimeoutCancellationException) {
321+
Log.w("Map3dViewModel", "awaitMapSteady timed out after $timeoutMillis ms")
322+
}
323+
} else {
324+
isMapSteady.first { it }
325+
}
326+
}
327+
295328
override fun onCleared() {
296329
_googleMap3D.value = null
297330
super.onCleared()

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Animations.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ data class DelayStep(val durationMillis: Long) : AnimationStep {
2828
}
2929
}
3030

31+
/**
32+
* An [AnimationStep] that waits until the map is fully loaded and idle before proceeding
33+
* to the next step in the animation sequence.
34+
*
35+
* @param timeoutMillis The maximum time to wait in milliseconds. A value of 0 means no timeout.
36+
*/
37+
data class WaitUntilTheMapIsSteadyStep(val timeoutMillis: Long = 0) : AnimationStep {
38+
override suspend fun invoke(viewModel: ScenariosViewModel) {
39+
viewModel.awaitMapSteady(timeoutMillis)
40+
}
41+
}
42+
3143
data class FlyToStep(val flyToOptions: FlyToOptions) : AnimationStep {
3244
override suspend operator fun invoke(viewModel: ScenariosViewModel) {
3345
viewModel.awaitFlyTo(flyToOptions)

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/CameraAttributesScreen.kt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ private val rollSliderRange = -360f..360f
4848
* which changes based on the currently selected [attribute].
4949
*
5050
* @param scenario The scenario providing options for the map.
51-
* @param onMap3dViewReady A callback function invoked when the [GoogleMap3D] view is ready.
52-
* @param onReleaseMap A callback function invoked when the map should be released.
51+
* @param viewModel The view model that manages the map's state.
5352
* @param modifier The modifier to be applied to the composable.
5453
* @param heading The current heading value of the camera.
5554
* @param tilt The current tilt value of the camera.
@@ -60,8 +59,7 @@ private val rollSliderRange = -360f..360f
6059
@Composable
6160
fun CameraControlDemoScreen(
6261
scenario: Scenario,
63-
onMap3dViewReady: (GoogleMap3D) -> Unit,
64-
onReleaseMap: () -> Unit,
62+
viewModel: ScenariosViewModel,
6563
modifier: Modifier = Modifier,
6664
heading: Float,
6765
tilt: Float,
@@ -74,8 +72,7 @@ fun CameraControlDemoScreen(
7472
ThreeDMap(
7573
modifier = Modifier.fillMaxWidth().weight(1f),
7674
options = scenario.mapsOptions,
77-
onMap3dViewReady = onMap3dViewReady,
78-
onReleaseMap = onReleaseMap,
75+
viewModel = viewModel,
7976
)
8077
Spacer(modifier = Modifier.height(16.dp))
8178
Crossfade(

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/Scenario.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
package com.example.advancedmaps3dsamples.scenarios
1616

17-
import android.util.Log // Import Log
1817
import androidx.compose.runtime.Composable
1918
import androidx.compose.ui.res.stringResource
2019
import com.google.android.gms.maps3d.Map3DOptions
@@ -39,7 +38,6 @@ data class Scenario(
3938
@Composable fun getTitle() = stringResource(titleId)
4039

4140
fun reset(viewModel: ScenariosViewModel) {
42-
Log.d("ScenarioReset", "Resetting scenario: $name")
4341
viewModel.setCamera(mapsOptions.toCamera()) // Set initial camera
4442
viewModel.setMapMode(mapsOptions.mapMode)
4543

@@ -60,8 +58,6 @@ data class Scenario(
6058
polygons.forEach { polygon ->
6159
viewModel.addPolygon(polygon)
6260
}
63-
64-
Log.d("ScenarioReset", "Reset complete for scenario: $name")
6561
}
6662
}
6763

0 commit comments

Comments
 (0)