Add a scale bar to visually gauge distances on a map.

Use case
Allows a user to have a visual reference for distances when navigating the map.
How to use the sample
Zoom in or out of the map. The scale bar will automatically display the appropriate scale based on zoom level. Units can be in metric and/or imperial based on locale/optional settings.
How it works
- Create an
ArcGISMapand add it to aMapViewcomposable. - Listen and track callback changes from
onUnitsPerDipChanged,onViewpointChangedForCenterAndScale,onSpatialReferenceChangedusing the composableMapView. - Add the
Scalebarcomposable positioned on top of theMapView - Pass in the latest values of
unitsPerDip,viewpoint,spatialReferenceand use a preferredmaxWidthinto theScaleBar.
Relevant API
- ArcGISMap
- MapView
- Scalebar
- UnitSystem
Additional information
The scale will be accurate for the center of the map, and in general more accurate at larger scales (zoomed in). This means at smaller scales (zoomed out), the reading may be inaccurate at the extremes of the visible extent.
This sample uses the Scalebar toolkit component. For information about setting up the toolkit, as well as code for the underlying component, visit the toolkit docs.
Tags
map, measure, scale, toolkit
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.showscalebar
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport 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.showscalebar.screens.ShowScaleBarScreen
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)
enableEdgeToEdge() setContent { SampleAppTheme { ShowScaleBarApp() } } }
@Composable private fun ShowScaleBarApp() { Surface(color = MaterialTheme.colorScheme.background) { ShowScaleBarScreen( sampleName = getString(R.string.show_scale_bar_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.showscalebar.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableDoubleStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.launchimport kotlin.time.Duration
class ShowScaleBarViewModel(application: Application) : AndroidViewModel(application) { // Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
val arcGISMap = ArcGISMap(BasemapStyle.ArcGISStreets).apply { initialViewpoint = Viewpoint(33.723271, -117.945793, 30452.0) }
// Scale bar properties updated by the composable mapview. var viewpoint by mutableStateOf<Viewpoint?>(null) private set var unitsPerDip by mutableDoubleStateOf(Double.NaN) private set var spatialReference by mutableStateOf<SpatialReference?>(null) private set
init { viewModelScope.launch { arcGISMap.load().onFailure { error -> messageDialogVM.showMessageDialog( title = "Failed to load map", description = error.message.toString() ) } } }
fun updateViewpoint(newViewpoint: Viewpoint?) { viewpoint = newViewpoint }
fun updateUnitsPerDip(newUnitsPerDip: Double) { unitsPerDip = newUnitsPerDip }
fun updateSpacialReference(newSpacialReference: SpatialReference?) { spatialReference = newSpacialReference }}
/** * Convert a duration to a string representation of seconds. */fun Duration.durationToSeconds(): String { return if (this == Duration.INFINITE) "Infinite" else this.inWholeSeconds.toString()}/* 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. * */
@file:OptIn(ExperimentalMaterial3Api::class)
package com.esri.arcgismaps.sample.showscalebar.screens
import android.content.res.Configurationimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.rememberScrollStateimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.foundation.verticalScrollimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Settingsimport androidx.compose.material3.BasicAlertDialogimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.Iconimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.OutlinedButtonimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Surfaceimport androidx.compose.material3.Switchimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.clipimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.UnitSystemimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.arcgismaps.toolkit.scalebar.Scalebarimport com.arcgismaps.toolkit.scalebar.ScalebarStyleimport com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBoximport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.showscalebar.components.ShowScaleBarViewModelimport com.esri.arcgismaps.sample.showscalebar.components.durationToSecondsimport kotlin.time.Durationimport kotlin.time.Duration.Companion.seconds
/** * Main screen layout for the sample app */@Composablefun ShowScaleBarScreen(sampleName: String) { val mapViewModel: ShowScaleBarViewModel = viewModel() // Keep track of the currently selected scalebar var currentScalebarStyle by remember { mutableStateOf(ScalebarStyle.AlternatingBar) } var isScalebarDialogOptionsVisible by remember { mutableStateOf(false) } var currentAutoHideDelay by remember { mutableStateOf(Duration.INFINITE) } var isGeodeticCalculationsEnabled by remember { mutableStateOf(true) } var currentScalebarUnitSystem by remember { mutableStateOf<UnitSystem>(UnitSystem.Imperial) }
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, floatingActionButton = { if (!isScalebarDialogOptionsVisible) { FloatingActionButton( modifier = Modifier.padding(bottom = 36.dp, end = 12.dp), onClick = { isScalebarDialogOptionsVisible = true } ) { Icon(Icons.Filled.Settings, contentDescription = "Show Scalebar Options") } } }, content = { paddingValues -> Box( modifier = Modifier .fillMaxSize() .padding(paddingValues) ) { MapView( modifier = Modifier.fillMaxSize(), arcGISMap = mapViewModel.arcGISMap, isAttributionBarVisible = false, onSpatialReferenceChanged = mapViewModel::updateSpacialReference, onUnitsPerDipChanged = mapViewModel::updateUnitsPerDip, onViewpointChangedForCenterAndScale = mapViewModel::updateViewpoint ) Scalebar( modifier = Modifier .padding(horizontal = 24.dp, vertical = 48.dp) .align(Alignment.BottomStart), maxWidth = 300.dp, unitsPerDip = mapViewModel.unitsPerDip, viewpoint = mapViewModel.viewpoint, spatialReference = mapViewModel.spatialReference, style = currentScalebarStyle, units = currentScalebarUnitSystem, autoHideDelay = currentAutoHideDelay, useGeodeticCalculations = isGeodeticCalculationsEnabled )
if (isScalebarDialogOptionsVisible) { ScalebarDialogOptions( currentScalebarStyle = currentScalebarStyle, currentScalebarUnitSystem = currentScalebarUnitSystem, currentAutoHideDelay = currentAutoHideDelay, isGeodeticCalculationsEnabled = isGeodeticCalculationsEnabled, onScalebarUnitSystemSelected = { currentScalebarUnitSystem = it }, onScalebarStyleSelected = { currentScalebarStyle = it }, onDismissRequest = { isScalebarDialogOptionsVisible = false }, onAutoHideDelaySelected = { currentAutoHideDelay = it }, onGeodeticCalculationsToggled = { isGeodeticCalculationsEnabled = !isGeodeticCalculationsEnabled } ) } } mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
@Composablefun ScalebarDialogOptions( currentScalebarStyle: ScalebarStyle, currentScalebarUnitSystem: UnitSystem, currentAutoHideDelay: Duration, onDismissRequest: () -> Unit, isGeodeticCalculationsEnabled: Boolean, onScalebarStyleSelected: (ScalebarStyle) -> Unit, onScalebarUnitSystemSelected: (UnitSystem) -> Unit, onAutoHideDelaySelected: (Duration) -> Unit, onGeodeticCalculationsToggled: (Boolean) -> Unit) { // List of all the supported scale bar properties val scalebarStyles = ScalebarStyle.entries.toList() val scalebarUnitSystems = listOf(UnitSystem.Metric, UnitSystem.Imperial) val autoHideDelays = listOf( Duration.INFINITE, 1.seconds, 3.seconds, 5.seconds )
BasicAlertDialog(onDismissRequest = onDismissRequest) { Column( modifier = Modifier .clip(RoundedCornerShape(12.dp)) .background(MaterialTheme.colorScheme.background) .padding(12.dp) .fillMaxWidth() .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text("Scale bar options: ", style = MaterialTheme.typography.titleMedium) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Enable Geodetic Calculations", style = MaterialTheme.typography.labelLarge ) Switch( checked = isGeodeticCalculationsEnabled, onCheckedChange = onGeodeticCalculationsToggled ) } DropDownMenuBox( textFieldLabel = "Scale bar style:", textFieldValue = currentScalebarStyle.name, dropDownItemList = scalebarStyles.map { it.name }, onIndexSelected = { index -> onScalebarStyleSelected(scalebarStyles[index]) } ) DropDownMenuBox( textFieldLabel = "Scale bar unit system:", textFieldValue = currentScalebarUnitSystem.javaClass.simpleName, dropDownItemList = scalebarUnitSystems.map { it.javaClass.simpleName }, onIndexSelected = { index -> onScalebarUnitSystemSelected(scalebarUnitSystems[index]) } ) DropDownMenuBox( textFieldLabel = "Scale bar auto-hide delay:", textFieldValue = currentAutoHideDelay.durationToSeconds(), dropDownItemList = autoHideDelays.map { it.durationToSeconds() }, onIndexSelected = { index -> onAutoHideDelaySelected(autoHideDelays[index]) } ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { OutlinedButton(onClick = onDismissRequest) { Text("Dismiss") } } } }}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun PreviewScalebarDialogOptions() { SampleAppTheme { Surface { ScalebarDialogOptions( currentScalebarStyle = ScalebarStyle.AlternatingBar, currentScalebarUnitSystem = UnitSystem.Imperial, currentAutoHideDelay = Duration.INFINITE, isGeodeticCalculationsEnabled = true, onDismissRequest = { }, onScalebarStyleSelected = { }, onScalebarUnitSystemSelected = { }, onAutoHideDelaySelected = { }, onGeodeticCalculationsToggled = { } ) } }}