Take a screenshot of the map.

Use case
GIS users may want to export or save a screenshot of a map to enable sharing as an image or printing.
How to use the sample
Pan and zoom to find an interesting location, then use the button to take a screenshot. The screenshot will be displayed. You can share the screenshot by tapping the share button. Or save it by tapping the save button.
How it works
- Wait for the
MapViewto finish drawing. - Call
exportImage()to get aBitmapDrawable. - Save screenshot into a File using
FileOutputStream. - Share the file using
context.startActivity. - Save the file using
context.contentResolver.
Relevant API
- MapView
Tags
capture, export, image, print, screen capture, screenshot, share, shot
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.takescreenshot
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.takescreenshot.screens.TakeScreenshotScreen
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 { TakeScreenshotApp() } } }
@Composable private fun TakeScreenshotApp() { Surface(color = MaterialTheme.colorScheme.background) { TakeScreenshotScreen( sampleName = getString(R.string.take_screenshot_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.takescreenshot.components
import android.app.Applicationimport android.content.ContentValuesimport android.content.Contextimport android.graphics.Bitmapimport android.graphics.drawable.BitmapDrawableimport android.media.MediaScannerConnectionimport android.net.Uriimport android.os.Buildimport android.os.Environmentimport android.provider.MediaStoreimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.core.content.ContextCompat.getStringimport androidx.core.content.FileProviderimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport com.esri.arcgismaps.sample.takescreenshot.Rimport kotlinx.coroutines.launchimport java.io.Fileimport java.io.FileOutputStreamimport java.io.IOExceptionimport java.text.SimpleDateFormatimport java.util.Dateimport java.util.Locale
class TakeScreenshotViewModel(app: Application) : AndroidViewModel(app) {
val arcGISMap = ArcGISMap(BasemapStyle.ArcGISNavigationNight).apply { initialViewpoint = Viewpoint(39.8, -98.6, 10e7) }
// Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
// screenshot image for display var screenshotImage: BitmapDrawable? by mutableStateOf(null) private set
// class to interact with MapView val mapViewProxy = MapViewProxy()
init { viewModelScope.launch { arcGISMap.load().onFailure { messageDialogVM.showMessageDialog(it) } } }
// clears the current screenshot image by setting it to null fun clearScreenshotImage(){ screenshotImage = null }
// function to take screenshot of MapView fun takeScreenshot(){ viewModelScope.launch { screenshotImage = mapViewProxy.exportImage().getOrNull() } }
// Function to save a bitmap image to a file and return its URI fun saveBitmapToFile(context: Context, bitmap: Bitmap): Uri? { // Create a file in the cache directory val file = File(context.cacheDir, "take-screenshot-sample-screenshot.png") if (!file.exists()) { try { file.createNewFile() } catch (e: IOException) { e.printStackTrace() } } val outputStream = FileOutputStream(file)
// Compress the bitmap and save it to the file bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) outputStream.flush() outputStream.close()
// Return the URI for the file return FileProvider.getUriForFile(context, getString(context, R.string.take_screenshot_provider_authority), file) }
fun saveBitmapToGallery(context: Context, bitmap: Bitmap): Uri? { val resolver = context.contentResolver val filename = "screenshot-${SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())}.jpg" if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) val imageFile = File(imagesDir, filename) FileOutputStream(imageFile).use { stream -> bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) } // Notify the media scanner about the new file. MediaScannerConnection.scanFile( context, arrayOf(imageFile.toString()), arrayOf("image/jpeg"), null ) return Uri.fromFile(imageFile) }
// Use MediaStore API for Android 10 (API 29) and above val contentValues = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, filename) put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) put(MediaStore.Images.Media.IS_PENDING, 1) }
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) uri?.let { resolver.openOutputStream(it)?.use { stream -> bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) } contentValues.clear() contentValues.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(it, contentValues, null, null) } return uri }
}
// Custom FileProvider for handling file sharing of screenshotsclass TakeScreenshotFileProvider : FileProvider()/* 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.takescreenshot.screens
import android.Manifestimport android.content.Contextimport android.content.Intentimport android.content.pm.PackageManagerimport android.graphics.Bitmapimport android.net.Uriimport android.os.Buildimport android.widget.Toastimport androidx.activity.compose.rememberLauncherForActivityResultimport androidx.activity.result.contract.ActivityResultContractsimport androidx.compose.foundation.Imageimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Shareimport androidx.compose.material3.Buttonimport androidx.compose.material3.Iconimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.ImageBitmapimport androidx.compose.ui.graphics.RectangleShapeimport androidx.compose.ui.graphics.asAndroidBitmapimport androidx.compose.ui.graphics.asImageBitmapimport androidx.compose.ui.hapticfeedback.HapticFeedbackTypeimport androidx.compose.ui.layout.ContentScaleimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.platform.LocalHapticFeedbackimport androidx.compose.ui.res.painterResourceimport androidx.compose.ui.unit.dpimport androidx.core.content.ContextCompatimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.takescreenshot.Rimport com.esri.arcgismaps.sample.takescreenshot.components.TakeScreenshotViewModel
/** * Main screen layout for the sample app */@Composablefun TakeScreenshotScreen(sampleName: String) { val mapViewModel: TakeScreenshotViewModel = viewModel()
val context = LocalContext.current val haptic = LocalHapticFeedback.current
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxSize() .padding(it), ) { MapView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISMap = mapViewModel.arcGISMap, mapViewProxy = mapViewModel.mapViewProxy )
Button( onClick = { mapViewModel.takeScreenshot() haptic.performHapticFeedback(HapticFeedbackType.Confirm) }, modifier = Modifier .fillMaxWidth() .height(56.dp), shape = RectangleShape ) { Text(text = "Take Screenshot") } }
// Show DialogWithImage if screenshotImage is not null mapViewModel.screenshotImage?.let { screenshotImage -> DialogWithImage( context = context, onConfirmation = { mapViewModel.clearScreenshotImage() mapViewModel.saveBitmapToFile(context, screenshotImage.bitmap)?.let { uri -> shareImage(context, uri) } }, onSaveBitmapToGallery = mapViewModel::saveBitmapToGallery, onDismissRequest = mapViewModel::clearScreenshotImage, imageBitmap = screenshotImage.bitmap.asImageBitmap(), imageDescription = "Screenshot", ) }
mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
/** * Displays a dialog with an image and a share button. */@Composablefun DialogWithImage( context: Context, onDismissRequest: () -> Unit, onConfirmation: () -> Unit, onSaveBitmapToGallery: (Context, Bitmap) -> Uri?, imageBitmap: ImageBitmap, imageDescription: String,) { SampleDialog(onDismissRequest = onDismissRequest) { Column( modifier = Modifier .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Image( bitmap = imageBitmap, contentDescription = imageDescription, contentScale = ContentScale.Fit, modifier = Modifier .fillMaxSize() ) Row ( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, ) { TextButton(onClick = onConfirmation) { Icon(Icons.Default.Share, "Share icon", Modifier.padding(horizontal = 4.dp)) Text("Share", style = MaterialTheme.typography.labelLarge) } SaveImageButton(context, imageBitmap.asAndroidBitmap(), onSaveBitmapToGallery) } } }}
// Function to display a pop-up for sharing an image filefun shareImage(context: Context, imageUri: Uri) { // Create an intent to share the image val shareIntent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_STREAM, imageUri) type = "image/png" } // Start the sharing activity with a chooser context.startActivity(Intent.createChooser(shareIntent, "Share Image"))}
@Composablefun SaveImageButton(context: Context, bitmap: Bitmap, saveBitmapToGallery: (Context, Bitmap) -> Uri?) {
val permission = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { Manifest.permission.WRITE_EXTERNAL_STORAGE } else null
val permissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { saveBitmapToGallery(context, bitmap) showToast(context, "Image saved to Photos gallery!") } else { showToast(context, "Permission denied") } }
TextButton(onClick = { // If permission is null (i.e., running on API > 28), or if permission is already granted if (permission == null || ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED) { saveBitmapToGallery(context, bitmap) showToast(context, "Image saved to Photos gallery!") } else { permissionLauncher.launch(permission) } }) { Icon(painter = painterResource(R.drawable.arrow_circle_down), "Share icon", Modifier.padding(horizontal = 4.dp)) Text("Save", style = MaterialTheme.typography.labelLarge) }}
fun showToast(context: Context, message: String) { Toast.makeText(context, message, Toast.LENGTH_LONG).show()}