This sample demonstrates how to keep the viewpoints of two GeoViews (a 2D MapView and a 3D SceneView) synchronized with each other.

Use case
You might need to synchronize GeoView viewpoints if you had two map views in one application - a main map and an inset. An inset map view could display all the layers at their full extent and contain a hollow rectangular graphic that represents the visible extent of the main map view. As you zoom or pan in the main map view, the extent graphic in the inset map would adjust accordingly.
How to use the sample
Interact with the MapView or SceneView by zooming or panning. The other MapView or SceneView will automatically focus on the same viewpoint.
How it works
- The ViewModel creates two containers: an ArcGISMap and an ArcGISScene. Both are initialized with the same initial Viewpoint (center coordinate and scale).
- The ViewModel also exposes a MapViewProxy and a SceneViewProxy. These proxies provide convenience functions to set viewpoints on the map/scene from the ViewModel without needing direct MapView/SceneView references.
- Each GeoView composable registers two callbacks:
- onNavigationChanged: notifies when the user is actively navigating (panning/zooming/rotating).
- onViewpointChangedForCenterAndScale: notifies about center-and-scale viewpoint updates.
- When the MapView viewpoint changes and the SceneView is not currently navigating, the ViewModel sets the SceneViewProxy to the new viewpoint. Symmetrically, when the SceneView viewpoint changes and the MapView is not navigating, the ViewModel sets the MapViewProxy to that viewpoint.
Relevant API
- GeoView
- MapView
- SceneView
- Viewpoint
About the data
This application provides two different perspectives of the arcGISImagery basemap, A 2D MapView as well as a 3D SceneView, displayed side by side.
Tags
3D, automatic refresh, event, event handler, events, extent, interaction, interactions, pan, zoom
Sample Code
/* Copyright 2026 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.matchviewpointofgeoviews
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.matchviewpointofgeoviews.screens.MatchViewpointOfGeoViewsScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
setContent { SampleAppTheme { MatchViewpointOfGeoViewsApp() } } }
@Composable private fun MatchViewpointOfGeoViewsApp() { Surface(color = MaterialTheme.colorScheme.background) { MatchViewpointOfGeoViewsScreen( sampleName = getString(R.string.match_viewpoint_of_geo_views_app_name) ) } }}/* Copyright 2025 Esri * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */
package com.esri.arcgismaps.sample.matchviewpointofgeoviews.screens
import androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Composable screen that displays a MapView and a SceneView stacked vertically and keeps their * center-and-scale viewpoints synchronized. The MapView and SceneView notify the ViewModel * about navigation state changes and viewpoint updates. */@Composablefun MatchViewpointOfGeoViewsScreen(sampleName: String) { val viewModel: MatchViewpointOfGeoViewsViewModel = viewModel()
Column(modifier = Modifier.fillMaxSize()) { SampleTopAppBar(title = sampleName)
Column(modifier = Modifier.fillMaxSize()) { MapView( modifier = Modifier .weight(1f) .fillMaxSize(), arcGISMap = viewModel.arcGISMap, mapViewProxy = viewModel.mapViewProxy, onNavigationChanged = { isNavigating -> viewModel.updateMapIsNavigating(isNavigating) }, onViewpointChangedForCenterAndScale = { newViewpoint: Viewpoint -> viewModel.onMapViewpointChanged(newViewpoint) } ) SceneView( modifier = Modifier .weight(1f) .fillMaxSize(), arcGISScene = viewModel.arcGISScene, sceneViewProxy = viewModel.sceneViewProxy, onNavigationChanged = { isNavigating -> viewModel.updateSceneIsNavigating(isNavigating) }, onViewpointChangedForCenterAndScale = { newViewpoint: Viewpoint -> viewModel.onSceneViewpointChanged(newViewpoint) } ) } }}package com.esri.arcgismaps.sample.matchviewpointofgeoviews.screens
import androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.ViewModelimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxy
class MatchViewpointOfGeoViewsViewModel : ViewModel() { val arcGISMap = ArcGISMap(BasemapStyle.ArcGISImagery) val arcGISScene = ArcGISScene(BasemapStyle.ArcGISImagery) val mapViewProxy = MapViewProxy() val sceneViewProxy = SceneViewProxy()
// Initial viewpoint private val initialViewpoint = Viewpoint( center = Point(-13637000.0, 4550000.0, SpatialReference.webMercator()), scale = 100_000.0 )
// Track navigation state var isMapNavigating by mutableStateOf(false) var isSceneNavigating by mutableStateOf(false)
init { arcGISMap.initialViewpoint = initialViewpoint arcGISScene.initialViewpoint = initialViewpoint }
fun updateMapIsNavigating(navigating: Boolean) { isMapNavigating = navigating }
fun updateSceneIsNavigating(navigating: Boolean) { isSceneNavigating = navigating }
fun onMapViewpointChanged(newViewpoint: Viewpoint) { if (!isSceneNavigating) { sceneViewProxy.setViewpoint(newViewpoint) } }
fun onSceneViewpointChanged(newViewpoint: Viewpoint) { if (!isMapNavigating) { mapViewProxy.setViewpoint(newViewpoint) } }}