From aabe02cd4a35b807d3a2b69533a49745290a968a Mon Sep 17 00:00:00 2001 From: Chris Arriola Date: Thu, 15 Dec 2022 19:03:07 -0800 Subject: [PATCH] feat: Add support for StreetView. (#209) * feat: Add support for StreetView. Change-Id: Id7f5b23a2458d77a08422886ef12799ffa0ebe7c * Add click support. Change-Id: Ie6ee4d41bbef9e9242fc3a33fd53dee8fca2d87a * Add sample. Change-Id: I8177feadc27d466f39e5d4e8d04b629afd241b4c * Adding properties to StreetView composable and updater. Change-Id: I416a77dbba34689d3c60d766bce2429fa3479641 * Use rememberUpdatedState. Change-Id: I945630a8e36edea272462fed72ccd1662efcb497 * Create StreetViewCameraPositionState. Change-Id: I7f7d0c165cf38738a624a8333c1d8bb35ecbf048 * Add test. Change-Id: Ib140373efa7fe265369ff3b81bfe2f9a4b202b24 * Clean tests Change-Id: I0c19a5ec5b716b22bffa05f0c916fb67fca745be * PR feedback. Change-Id: I4121f8774fedb592f0c9d9a86a153f0399bad666 * Add Street View example to README. * PR feedback * Add docs to StreetView composable. --- README.md | 18 +- .../maps/android/compose/StreetViewTests.kt | 52 ++++++ app/src/main/AndroidManifest.xml | 3 + .../maps/android/compose/BasicMapActivity.kt | 8 +- .../compose/LocationTrackingActivity.kt | 2 - .../maps/android/compose/MainActivity.kt | 7 + .../android/compose/MapClusteringActivity.kt | 2 - .../android/compose/MapInColumnActivity.kt | 3 - .../maps/android/compose/ScaleBarActivity.kt | 2 - .../android/compose/StreetViewActivity.kt | 105 ++++++++++++ app/src/main/res/values/strings.xml | 1 + .../google/maps/android/compose/GoogleMap.kt | 2 +- .../android/compose/streetview/StreetView.kt | 161 ++++++++++++++++++ .../StreetViewCameraPositionState.kt | 89 ++++++++++ .../streetview/StreetViewPanoramaApplier.kt | 23 +++ .../StreetViewPanoramaEventListeners.kt | 15 ++ .../streetview/StreetViewPanoramaUpdater.kt | 77 +++++++++ 17 files changed, 554 insertions(+), 16 deletions(-) create mode 100644 app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt create mode 100644 app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt create mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt create mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewCameraPositionState.kt create mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaApplier.kt create mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaEventListeners.kt create mode 100644 maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt diff --git a/README.md b/README.md index f94300c1..bfc5102f 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ GoogleMap( } ``` -#### Customizing a marker's info window +### Customizing a marker's info window You can customize a marker's info window contents by using the `MarkerInfoWindowContent` element, or if you want to customize the entire info @@ -153,7 +153,21 @@ MarkerInfoWindow( } ``` -#### Obtaining Access to the raw GoogleMap (Experimental) +### Street View + +You can add a Street View given a location using the `StreetView` composable. +To use it, provide a `StreetViewPanoramaOptions` object as follows: + +```kotlin +val singapore = LatLng(1.35, 103.87) +StreetView( + streetViewPanoramaOptionsFactory = { + StreetViewPanoramaOptions().position(singapore) + } +) +``` + +### Obtaining Access to the raw GoogleMap (Experimental) Certain use cases require extending the `GoogleMap` object to decorate / augment the map. For example, while marker clustering is not yet supported by Maps Compose diff --git a/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt b/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt new file mode 100644 index 00000000..3cf26474 --- /dev/null +++ b/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt @@ -0,0 +1,52 @@ +package com.google.maps.android.compose + +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.test.junit4.createComposeRule +import com.google.android.gms.maps.StreetViewPanoramaOptions +import com.google.android.gms.maps.model.StreetViewPanoramaOrientation +import com.google.maps.android.compose.streetview.StreetView +import com.google.maps.android.compose.streetview.StreetViewCameraPositionState +import com.google.maps.android.ktx.MapsExperimentalFeature +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class StreetViewTests { + @get:Rule + val composeTestRule = createComposeRule() + + private lateinit var cameraPositionState: StreetViewCameraPositionState + private val initialLatLng = singapore + + @Before + fun setUp() { + cameraPositionState = StreetViewCameraPositionState() + } + + @OptIn(MapsExperimentalFeature::class) + private fun initStreetView(onClick: (StreetViewPanoramaOrientation) -> Unit = {}) { + composeTestRule.setContent { + StreetView( + Modifier.semantics { contentDescription = "StreetView" }, + cameraPositionState = cameraPositionState, + streetViewPanoramaOptionsFactory = { + StreetViewPanoramaOptions() + .position(initialLatLng) + }, + onClick = onClick + ) + } + composeTestRule.waitUntil(8000) { + cameraPositionState.location.position.latitude != 0.0 && + cameraPositionState.location.position.longitude != 0.0 + } + } + + @Test + fun testStartingStreetViewPosition() { + initStreetView() + initialLatLng.assertEquals(cameraPositionState.location.position) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b274e1e5..8e938ca4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,9 @@ + diff --git a/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt index 2e0d02d4..7c032310 100644 --- a/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt @@ -61,10 +61,10 @@ import kotlinx.coroutines.launch private const val TAG = "BasicMapActivity" -private val singapore = LatLng(1.35, 103.87) -private val singapore2 = LatLng(1.40, 103.77) -private val singapore3 = LatLng(1.45, 103.77) -private val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) +val singapore = LatLng(1.35, 103.87) +val singapore2 = LatLng(1.40, 103.77) +val singapore3 = LatLng(1.45, 103.77) +val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) class BasicMapActivity : ComponentActivity() { diff --git a/app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt b/app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt index 2967f563..86462124 100644 --- a/app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt @@ -30,8 +30,6 @@ import kotlin.random.Random private const val TAG = "LocationTrackActivity" private const val zoom = 8f -private val singapore = LatLng(1.35, 103.87) -private val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, zoom) /** * This shows how to use a custom location source to show a blue dot on the map based on your own diff --git a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt index 301ed2c4..b452868a 100644 --- a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt @@ -105,6 +105,13 @@ class MainActivity : ComponentActivity() { }) { Text(getString(R.string.scale_bar_activity)) } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, StreetViewActivity::class.java)) + }) { + Text(getString(R.string.street_view)) + } } } } diff --git a/app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt index 234f7e6d..912187b5 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt @@ -21,8 +21,6 @@ import com.google.maps.android.clustering.ClusterItem import com.google.maps.android.clustering.ClusterManager import kotlin.random.Random -private val singapore = LatLng(1.35, 103.87) -private val singapore2 = LatLng(2.50, 103.87) private val TAG = MapClusteringActivity::class.simpleName class MapClusteringActivity : ComponentActivity() { diff --git a/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt index 6c42b790..087a42b9 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt @@ -43,9 +43,6 @@ import com.google.android.gms.maps.model.Marker private const val TAG = "ScrollingMapActivity" -private val singapore = LatLng(1.35, 103.87) -private val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) - class MapInColumnActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt b/app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt index 5595ca2f..297587e7 100644 --- a/app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/ScaleBarActivity.kt @@ -42,8 +42,6 @@ import com.google.maps.android.compose.widgets.ScaleBar private const val TAG = "ScaleBarActivity" private const val zoom = 8f -private val singapore = LatLng(1.35, 103.87) -private val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, zoom) class ScaleBarActivity : ComponentActivity() { diff --git a/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt b/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt new file mode 100644 index 00000000..e7d4d6be --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt @@ -0,0 +1,105 @@ +package com.google.maps.android.compose + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material.Button +import androidx.compose.material.Switch +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.StreetViewPanoramaOptions +import com.google.maps.android.compose.streetview.StreetView +import com.google.maps.android.compose.streetview.rememberStreetViewCameraPositionState +import com.google.maps.android.ktx.MapsExperimentalFeature +import kotlinx.coroutines.launch + +class StreetViewActivity : ComponentActivity() { + + private val TAG = StreetViewActivity::class.java.simpleName + + @OptIn(MapsExperimentalFeature::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + var isPanningEnabled by remember { mutableStateOf(false) } + var isZoomEnabled by remember { mutableStateOf(false) } + val camera = rememberStreetViewCameraPositionState() + LaunchedEffect(camera) { + launch { + snapshotFlow { camera.panoramaCamera } + .collect { + Log.d(TAG, "Camera at: $it") + } + } + launch { + snapshotFlow { camera.location } + .collect { + Log.d(TAG, "Location at: $it") + } + } + } + Box(Modifier.fillMaxSize(), Alignment.BottomStart) { + StreetView( + Modifier.matchParentSize(), + cameraPositionState = camera, + streetViewPanoramaOptionsFactory = { + StreetViewPanoramaOptions().position(singapore) + }, + isPanningGesturesEnabled = isPanningEnabled, + isZoomGesturesEnabled = isZoomEnabled, + onClick = { + Log.d(TAG, "Street view clicked") + }, + onLongClick = { + Log.d(TAG, "Street view long clicked") + } + ) + Column( + Modifier + .fillMaxWidth() + .background(Color.White) + .padding(8.dp) + ) { + StreetViewSwitch(title = "Panning", checked = isPanningEnabled) { + isPanningEnabled = it + } + StreetViewSwitch(title = "Zooming", checked = isZoomEnabled) { + isZoomEnabled = it + } + } + } + } + } +} + +@Composable +fun StreetViewSwitch(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { + Row(Modifier.padding(4.dp)) { + Text(title) + Spacer(Modifier.weight(1f)) + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 51a91f28..5e7a0bc2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,4 +22,5 @@ Map Clustering Location Tracking Scale Bar + Street View \ No newline at end of file diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 3b2302f8..f2349511 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -138,7 +138,7 @@ public fun GoogleMap( } } -private suspend inline fun disposingComposition(factory: () -> Composition) { +internal suspend inline fun disposingComposition(factory: () -> Composition) { val composition = factory() try { awaitCancellation() diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt new file mode 100644 index 00000000..cc774f4c --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt @@ -0,0 +1,161 @@ +package com.google.maps.android.compose.streetview + +import android.content.ComponentCallbacks +import android.content.res.Configuration +import android.os.Bundle +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Composition +import androidx.compose.runtime.CompositionContext +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCompositionContext +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.google.android.gms.maps.StreetViewPanoramaOptions +import com.google.android.gms.maps.StreetViewPanoramaView +import com.google.android.gms.maps.model.StreetViewPanoramaCamera +import com.google.android.gms.maps.model.StreetViewPanoramaOrientation +import com.google.maps.android.compose.disposingComposition +import com.google.maps.android.ktx.MapsExperimentalFeature +import com.google.maps.android.ktx.awaitStreetViewPanorama + +/** + * A composable for displaying a Street View for a given location. + * + * @param modifier Modifier to be applied to the StreetView + * @param cameraPositionState the [StreetViewCameraPositionState] to be used to control or observe + * the Street View's camera + * @param streetViewPanoramaOptionsFactory a factory lambda for providing a + * [StreetViewPanoramaOptions] object which is used when the underlying [StreetViewPanoramaView] is + * constructed + * @param isPanningGesturesEnabled whether panning gestures are enabled or not + * @param isStreetNamesEnabled whether street names are enabled or not + * @param isUserNavigationEnabled whether user navigation is enabled or not + * @param isZoomGesturesEnabled whether zoom gestures are enabled or not + * @param onClick lambda to receive events when the Street View is clicked + * @param onLongClick lambda to receive events when the Street View is long clicked + */ +@MapsExperimentalFeature +@Composable +public fun StreetView( + modifier: Modifier = Modifier, + cameraPositionState: StreetViewCameraPositionState = rememberStreetViewCameraPositionState(), + streetViewPanoramaOptionsFactory: () -> StreetViewPanoramaOptions = { + StreetViewPanoramaOptions() + }, + isPanningGesturesEnabled: Boolean = true, + isStreetNamesEnabled: Boolean = true, + isUserNavigationEnabled: Boolean = true, + isZoomGesturesEnabled: Boolean = true, + onClick: (StreetViewPanoramaOrientation) -> Unit = {}, + onLongClick: (StreetViewPanoramaOrientation) -> Unit = {}, +) { + val context = LocalContext.current + val streetView = + remember(context) { StreetViewPanoramaView(context, streetViewPanoramaOptionsFactory()) } + + AndroidView(modifier = modifier, factory = { streetView }) {} + StreetViewLifecycle(streetView) + + val currentCameraPositionState by rememberUpdatedState(cameraPositionState) + val currentIsPanningGestureEnabled by rememberUpdatedState(isPanningGesturesEnabled) + val currentIsStreetNamesEnabled by rememberUpdatedState(isStreetNamesEnabled) + val currentIsUserNavigationEnabled by rememberUpdatedState(isUserNavigationEnabled) + val currentIsZoomGesturesEnabled by rememberUpdatedState(isZoomGesturesEnabled) + val clickListeners by rememberUpdatedState(StreetViewPanoramaEventListeners().also { + it.onClick = onClick + it.onLongClick = onLongClick + }) + val parentComposition = rememberCompositionContext() + + LaunchedEffect(Unit) { + disposingComposition { + streetView.newComposition(parentComposition) { + StreetViewUpdater( + cameraPositionState = currentCameraPositionState, + isPanningGesturesEnabled = currentIsPanningGestureEnabled, + isStreetNamesEnabled = currentIsStreetNamesEnabled, + isUserNavigationEnabled = currentIsUserNavigationEnabled, + isZoomGesturesEnabled = currentIsZoomGesturesEnabled, + clickListeners = clickListeners + ) + } + } + } +} + +@Composable +private fun StreetViewLifecycle(streetView: StreetViewPanoramaView) { + val context = LocalContext.current + val lifecycle = LocalLifecycleOwner.current.lifecycle + val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) } + DisposableEffect(context, lifecycle, streetView) { + val streetViewLifecycleObserver = streetView.lifecycleObserver(previousState) + val callbacks = streetView.componentCallbacks() + + lifecycle.addObserver(streetViewLifecycleObserver) + context.registerComponentCallbacks(callbacks) + + onDispose { + lifecycle.removeObserver(streetViewLifecycleObserver) + context.unregisterComponentCallbacks(callbacks) + streetView.onDestroy() + } + } +} + +private suspend inline fun StreetViewPanoramaView.newComposition( + parent: CompositionContext, + noinline content: @Composable () -> Unit +): Composition { + val panorama = awaitStreetViewPanorama() + Log.d("StreetView", "Location is ${panorama.location}") + return Composition( + StreetViewPanoramaApplier(panorama), parent + ).apply { + setContent(content) + } +} + +private fun StreetViewPanoramaView.lifecycleObserver(previousState: MutableState): LifecycleEventObserver = + LifecycleEventObserver { _, event -> + event.targetState + when (event) { + Lifecycle.Event.ON_CREATE -> { + // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in + // this case the GoogleMap composable also doesn't leave the composition. So, + // recreating the map does not restore state properly which must be avoided. + if (previousState.value != Lifecycle.Event.ON_STOP) { + this.onCreate(Bundle()) + } + } + Lifecycle.Event.ON_START -> this.onStart() + Lifecycle.Event.ON_RESUME -> this.onResume() + Lifecycle.Event.ON_PAUSE -> this.onPause() + Lifecycle.Event.ON_STOP -> this.onStop() + Lifecycle.Event.ON_DESTROY -> { + //handled in onDispose + } + else -> throw IllegalStateException() + } + previousState.value = event + } + +private fun StreetViewPanoramaView.componentCallbacks(): ComponentCallbacks = + object : ComponentCallbacks { + override fun onConfigurationChanged(config: Configuration) {} + + override fun onLowMemory() { + this@componentCallbacks.onLowMemory() + } + } \ No newline at end of file diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewCameraPositionState.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewCameraPositionState.kt new file mode 100644 index 00000000..2c5c16b7 --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewCameraPositionState.kt @@ -0,0 +1,89 @@ +package com.google.maps.android.compose.streetview + +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 com.google.android.gms.maps.StreetViewPanorama +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.StreetViewPanoramaCamera +import com.google.android.gms.maps.model.StreetViewPanoramaLocation +import com.google.android.gms.maps.model.StreetViewSource + +@Composable +public inline fun rememberStreetViewCameraPositionState( + crossinline init: StreetViewCameraPositionState.() -> Unit = {} +): StreetViewCameraPositionState = remember { + StreetViewCameraPositionState().apply(init) +} + +public class StreetViewCameraPositionState { + + /** + * The location of the panorama. + * + * This is read-only - to update the camera's position use [setPosition]. + * + * Note that this property is observable and if you use it in a composable function it will be + * recomposed on every change. Use `snapshotFlow` to observe it instead. + */ + public val location: StreetViewPanoramaLocation + get() = rawLocation + + internal var rawLocation by mutableStateOf(StreetViewPanoramaLocation(arrayOf(), LatLng(0.0,0.0), "")) + + /** + * The camera of the panorama. + * + * Note that this property is observable and if you use it in a composable function it will be + * recomposed on every change. Use `snapshotFlow` to observe it instead. + */ + public val panoramaCamera: StreetViewPanoramaCamera + get() = rawPanoramaCamera + + internal var rawPanoramaCamera by mutableStateOf(StreetViewPanoramaCamera(0f, 0f, 0f )) + + internal var panorama: StreetViewPanorama? = null + set(value) { + // Set value + if (field == null && value == null) return + if (field != null && value != null) { + error("StreetViewCameraPositionState may only be associated with one StreetView at a time.") + } + field = value + } + + /** + * Animates the camera to be at [camera] in [durationMs] milliseconds. + * @param camera the camera to update to + * @param durationMs the duration of the animation in milliseconds + */ + public fun animateTo(camera: StreetViewPanoramaCamera, durationMs: Int) { + panorama?.animateTo(camera, durationMs.toLong()) + } + + /** + * Sets the position of the panorama. + * @param position the LatLng of the panorama + * @param radius the area in which to search for a panorama in meters + * @param source the source of the panoramas + */ + public fun setPosition(position: LatLng, radius: Int? = null, source: StreetViewSource? = null) { + if (radius == null && source == null) { + panorama?.setPosition(position) + } else if (radius != null && source == null) { + panorama?.setPosition(position, radius) + } else if (radius != null) { + panorama?.setPosition(position, radius, source) + } + } + + /** + * Sets the StreetViewPanorama to the given panorama ID. + * @param panoId the ID of the panorama to set to + */ + public fun setPosition(panoId: String) { + panorama?.setPosition(panoId) + } +} \ No newline at end of file diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaApplier.kt new file mode 100644 index 00000000..18f69286 --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaApplier.kt @@ -0,0 +1,23 @@ +package com.google.maps.android.compose.streetview + +import androidx.compose.runtime.AbstractApplier +import com.google.android.gms.maps.StreetViewPanorama +import com.google.maps.android.compose.MapNode + +private object StreetViewPanoramaNodeRoot : MapNode + +internal class StreetViewPanoramaApplier( + val streetViewPanorama: StreetViewPanorama +) : AbstractApplier(StreetViewPanoramaNodeRoot) { + override fun onClear() { } + + override fun insertBottomUp(index: Int, instance: MapNode) { + instance.onAttached() + } + + override fun insertTopDown(index: Int, instance: MapNode) { } + + override fun move(from: Int, to: Int, count: Int) { } + + override fun remove(index: Int, count: Int) { } +} diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaEventListeners.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaEventListeners.kt new file mode 100644 index 00000000..7b15e6b9 --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaEventListeners.kt @@ -0,0 +1,15 @@ +package com.google.maps.android.compose.streetview + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.google.android.gms.maps.model.StreetViewPanoramaCamera +import com.google.android.gms.maps.model.StreetViewPanoramaOrientation + +/** + * Holder class for top-level event listeners for [StreetViewPanorama]. + */ +internal class StreetViewPanoramaEventListeners { + var onClick: (StreetViewPanoramaOrientation) -> Unit by mutableStateOf({}) + var onLongClick: (StreetViewPanoramaOrientation) -> Unit by mutableStateOf({}) +} \ No newline at end of file diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt new file mode 100644 index 00000000..2898ec8f --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetViewPanoramaUpdater.kt @@ -0,0 +1,77 @@ +package com.google.maps.android.compose.streetview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.currentComposer +import com.google.android.gms.maps.StreetViewPanorama +import com.google.maps.android.compose.MapNode + +internal class StreetViewPanoramaPropertiesNode( + val cameraPositionState: StreetViewCameraPositionState, + val panorama: StreetViewPanorama, + var eventListeners: StreetViewPanoramaEventListeners, +) : MapNode { + init { + cameraPositionState.panorama = panorama + } + + override fun onAttached() { + super.onAttached() + panorama.setOnStreetViewPanoramaClickListener { + eventListeners.onClick(it) + } + panorama.setOnStreetViewPanoramaLongClickListener { + eventListeners.onLongClick(it) + } + panorama.setOnStreetViewPanoramaCameraChangeListener { + cameraPositionState.rawPanoramaCamera = it + } + panorama.setOnStreetViewPanoramaChangeListener { + cameraPositionState.rawLocation = it + } + } + + override fun onRemoved() { + cameraPositionState.panorama = null + } + + override fun onCleared() { + cameraPositionState.panorama = null + } +} + +/** + * Used to keep the street view panorama properties up-to-date. + */ +@Suppress("NOTHING_TO_INLINE") +@Composable +internal inline fun StreetViewUpdater( + cameraPositionState: StreetViewCameraPositionState, + isPanningGesturesEnabled: Boolean, + isStreetNamesEnabled: Boolean, + isUserNavigationEnabled: Boolean, + isZoomGesturesEnabled: Boolean, + clickListeners: StreetViewPanoramaEventListeners +) { + val streetViewPanorama = + (currentComposer.applier as StreetViewPanoramaApplier).streetViewPanorama + ComposeNode( + factory = { + StreetViewPanoramaPropertiesNode( + cameraPositionState = cameraPositionState, + panorama = streetViewPanorama, + eventListeners = clickListeners, + ) + } + ) { + set(isPanningGesturesEnabled) { + panorama.isPanningGesturesEnabled = isPanningGesturesEnabled + } + set(isStreetNamesEnabled) { panorama.isStreetNamesEnabled = isStreetNamesEnabled } + set(isUserNavigationEnabled) { + panorama.isUserNavigationEnabled = isUserNavigationEnabled + } + set(isZoomGesturesEnabled) { panorama.isZoomGesturesEnabled = isZoomGesturesEnabled } + set(clickListeners) { this.eventListeners = it } + } +} \ No newline at end of file