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