Change a graphic’s symbol based on the camera’s proximity to it.

Use case
When showing dense datasets, it is beneficial to reduce the detail of individual points when zooming out to avoid visual clutter and to avoid data points overlapping and obscuring each other.
How to use the sample
The sample starts looking at a plane. Zoom out from the plane to see it turn into a cone. Keeping zooming out and it will turn into a point.
How it works
- Create a
GraphicsOverlayobject and add it to aSceneView. - Create a
DistanceCompositeSceneSymbolobject. - Create
DistanceSymbolRangeobjects specifying aSymboland the min and max distance within which the symbol should be visible. - Add the ranges to the range collection of the distance composite scene symbol.
- Create a
Graphicobject with the distance composite scene symbol at a location and add it to the graphics overlay.
Relevant API
- DistanceCompositeSceneSymbol
- DistanceSymbolRange
- OrbitGeoElementCameraController
Tags
3D, data, graphic
Sample Code
/* 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.stylepointwithdistancecompositescenesymbol
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), getString(R.string.style_point_with_distance_composite_scene_symbol_app_name), listOf( "https://www.arcgis.com/home/item.html?id=681d6f7694644709a7c830ec57a2d72b" ) ) }}/* 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.stylepointwithdistancecompositescenesymbol
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.stylepointwithdistancecompositescenesymbol.screens.StylePointWithDistanceCompositeSceneSymbolScreen
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 { StylePointWithDistanceCompositeSceneSymbolApp() } } }
@Composable private fun StylePointWithDistanceCompositeSceneSymbolApp() { Surface(color = MaterialTheme.colorScheme.background) { StylePointWithDistanceCompositeSceneSymbolScreen( sampleName = getString(R.string.style_point_with_distance_composite_scene_symbol_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.stylepointwithdistancecompositescenesymbol.components
import android.app.Applicationimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.Colorimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.ArcGISTiledElevationSourceimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.view.GraphicsOverlayimport com.arcgismaps.mapping.view.Graphicimport com.arcgismaps.mapping.symbology.DistanceCompositeSceneSymbolimport com.arcgismaps.mapping.symbology.DistanceSymbolRangeimport com.arcgismaps.mapping.symbology.ModelSceneSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSceneSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolimport com.arcgismaps.mapping.symbology.SimpleMarkerSymbolStyleimport com.arcgismaps.mapping.view.Cameraimport com.arcgismaps.mapping.view.SurfacePlacementimport com.arcgismaps.mapping.view.OrbitGeoElementCameraControllerimport com.arcgismaps.toolkit.geoviewcompose.SceneViewProxyimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol.Rimport kotlinx.coroutines.launchimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport java.io.File
/** * ViewModel for the sample. Builds an ArcGISScene, a distance composite scene symbol and an orbit * camera controller. */class StylePointWithDistanceCompositeSceneSymbolViewModel(app: Application) : AndroidViewModel(app) {
// Lazy provision path for reference offline resources. private val provisionPath: String by lazy { app.getExternalFilesDir(null)?.path.toString() + File.separator + app.getString(R.string.style_point_with_distance_composite_scene_symbol_app_name) }
// Construct the model file URI from the provision path. private val bristolModelUri get() = "$provisionPath${File.separator}Bristol.dae"
// The model (3D) graphic target. private val planePosition = Point( x = -2.708, y = 56.096, z = 5000.0, spatialReference = SpatialReference.wgs84() )
// Distance composite symbol with three ranges (detailed model, simplified model, and a simple circle). private val distanceCompositeSymbol: DistanceCompositeSceneSymbol by lazy { // Close-up: Detailed 3D model. val planeModel = ModelSceneSymbol( uri = bristolModelUri, scale = 100.0F )
// Mid-distance: Simple cone symbol. val coneSymbol = SimpleMarkerSceneSymbol.cone( color = Color.red, diameter = 200.0, height = 600.0 )
// Far-distance: Simple 2D symbol. val circleSymbol = SimpleMarkerSymbol( style = SimpleMarkerSymbolStyle.Circle, color = Color.red, size = 10f )
DistanceCompositeSceneSymbol().apply { // Close-up: Detailed 3D model. ranges.add( DistanceSymbolRange( symbol = planeModel, minDistance = null, maxDistance = 10000.0 ) ) // Mid-distance: Simple cone symbol. ranges.add( DistanceSymbolRange( symbol = coneSymbol, minDistance = 10001.0, maxDistance = 30000.0 ) ) // Far-distance: Simple 2D symbol. ranges.add( DistanceSymbolRange( symbol = circleSymbol, minDistance = 30001.0, maxDistance = null ) ) } }
// Graphic for the plane using the distance composite symbol. private val planeGraphic by lazy { Graphic( geometry = planePosition, symbol = distanceCompositeSymbol ) }
// Orbit camera controller that targets the plane graphic. val orbitCameraController: OrbitGeoElementCameraController by lazy { OrbitGeoElementCameraController(planeGraphic, 4000.0).apply { setCameraPitchOffset(80.0) setCameraHeadingOffset(-30.0) } }
// Scene using imagery basemap and a world elevation service. val arcGISScene = ArcGISScene(BasemapStyle.ArcGISImagery).apply { // Set the tiled elevation source baseSurface.elevationSources += ArcGISTiledElevationSource( uri = "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer" ) // Set the initial viewpoint val camera = Camera( latitude = 56.096, longitude = -2.708, altitude = 5000.0, heading = -30.0, pitch = 80.0, roll = 0.0 ) initialViewpoint = Viewpoint(boundingGeometry = camera.location, camera = camera) }
// Graphics overlay to display the plane graphic using a distance composite symbol. private val graphicsOverlay by lazy { GraphicsOverlay(graphics = listOf(planeGraphic)).apply { sceneProperties.surfacePlacement = SurfacePlacement.Relative } }
// Expose the graphics overlay list for the SceneView val graphicsOverlays = listOf(graphicsOverlay)
// SceneView proxy to hand to the composable SceneView val sceneViewProxy = SceneViewProxy()
// Flow exposing the distance between camera and target (meters). private val _cameraDistanceMeters = MutableStateFlow(0.0) val cameraDistanceMeters = _cameraDistanceMeters.asStateFlow()
// Message dialog VM to surface errors. val messageDialogVM = MessageDialogViewModel()
init { // Load the scene and surface errors via the message dialog VM. viewModelScope.launch { arcGISScene.load().onFailure { messageDialogVM.showMessageDialog(it) } }
// Collect the cameraDistance flow exposed by the controller viewModelScope.launch { // Update flow to keep UI reactive. orbitCameraController.cameraDistance.collect { distance -> _cameraDistanceMeters.value = distance } } }}/* 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.stylepointwithdistancecompositescenesymbol.screens
import androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.stylepointwithdistancecompositescenesymbol.components.StylePointWithDistanceCompositeSceneSymbolViewModel
/** * Main screen for the sample. Hosts a SceneView and a simple overlay with instructions and distance display. */@Composablefun StylePointWithDistanceCompositeSceneSymbolScreen(sampleName: String) { val viewModel: StylePointWithDistanceCompositeSceneSymbolViewModel = viewModel()
// Collect camera distance exposed by the ViewModel. val cameraDistance by viewModel.cameraDistanceMeters.collectAsStateWithLifecycle()
Scaffold( topBar = { SampleTopAppBar(title = sampleName) } ) { padding -> Column(modifier = Modifier.padding(padding)) { // Provide the scene, camera controller, overlays and the SceneViewProxy. SceneView( modifier = Modifier.fillMaxSize().weight(1f), arcGISScene = viewModel.arcGISScene, sceneViewProxy = viewModel.sceneViewProxy, cameraController = viewModel.orbitCameraController, graphicsOverlays = viewModel.graphicsOverlays, ) Column( modifier = Modifier .fillMaxWidth() .padding(8.dp), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( modifier = Modifier.padding(top = 6.dp), text = "Distance from target: ${cameraDistance.toInt()} m", style = MaterialTheme.typography.bodyLarge ) Text( text = "Zoom in and out to see the symbol change.", style = MaterialTheme.typography.bodySmall ) } } // Display message dialog if any error occurs viewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } }}