**Latest Mechanism (Compose 1.7.0-beta02+)**
---------------------------------------
A new mechanism for drawing a composable into a bitmap has been made available. Using `graphicsLayer.record{}` you can redirect drawing commands to a certain graphics layer, and then call `DrawScope#drawLayer()` to draw the layer to screen.
GraphicsLayers now also have the ability to call `graphicsLayer.toImageBitmap()` to get a screenshot of the composable.
```
val coroutineScope = rememberCoroutineScope()
val graphicsLayer = rememberGraphicsLayer()
Box(
modifier = Modifier
.drawWithContent {
// call record to capture the content in the graphics layer
graphicsLayer.record {
// draw the contents of the composable into the graphics layer
this@drawWithContent.drawContent()
}
// draw the graphics layer on the visible canvas
drawLayer(graphicsLayer)
}
.clickable {
coroutineScope.launch {
val bitmap = graphicsLayer.toImageBitmap()
// do something with the newly acquired bitmap
}
}
.background(Color.White)
) {
Text("Hello Android", fontSize = 26.sp)
}
```
More information can be found here:
https://developer.android.com/develop/ui/compose/graphics/draw/modifiers#composable-to-bitmap
Full snippet:
```
@OptIn(ExperimentalPermissionsApi::class)
@Preview
@Composable
fun BitmapFromComposableFullSnippet() {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val graphicsLayer = rememberGraphicsLayer()
val writeStorageAccessState = rememberMultiplePermissionsState(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// No permissions are needed on Android 10+ to add files in the shared storage
emptyList()
} else {
listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
)
// This logic should live in your ViewModel - trigger a side effect to invoke URI sharing.
// checks permissions granted, and then saves the bitmap from a Picture that is already capturing content
// and shares it with the default share sheet.
fun shareBitmapFromComposable() {
if (writeStorageAccessState.allPermissionsGranted) {
coroutineScope.launch {
val bitmap = graphicsLayer.toImageBitmap()
val uri = bitmap.asAndroidBitmap().saveToDisk(context)
shareBitmap(context, uri)
}
} else if (writeStorageAccessState.shouldShowRationale) {
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = "The storage permission is needed to save the image",
actionLabel = "Grant Access"
)
if (result == SnackbarResult.ActionPerformed) {
writeStorageAccessState.launchMultiplePermissionRequest()
}
}
} else {
writeStorageAccessState.launchMultiplePermissionRequest()
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
FloatingActionButton(onClick = {
shareBitmapFromComposable()
}) {
Icon(Icons.Default.Share, "share")
}
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.drawWithCache {
onDrawWithContent {
graphicsLayer.record {
this@onDrawWithContent.drawContent()
}
drawLayer(graphicsLayer)
}
}
) {
ScreenContentToCapture()
}
}
}
@Composable
private fun ScreenContentToCapture() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.background(
Brush.linearGradient(
listOf(
Color(0xFFF5D5C0),
Color(0xFFF8E8E3)
)
)
)
) {
Image(
painterResource(id = R.drawable.sunset),
contentDescription = null,
modifier = Modifier
.aspectRatio(1f)
.padding(32.dp),
contentScale = ContentScale.Crop
)
Text(
"Into the Ocean depths",
fontSize = 18.sp
)
}
}
private suspend fun Bitmap.saveToDisk(context: Context): Uri {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"screenshot-${System.currentTimeMillis()}.png"
)
file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100)
return scanFilePath(context, file.path) ?: throw Exception("File could not be saved")
}
/**
* We call [MediaScannerConnection] to index the newly created image inside MediaStore to be visible
* for other apps, as well as returning its [MediaStore] Uri
*/
private suspend fun scanFilePath(context: Context, filePath: String): Uri? {
return suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile(
context,
arrayOf(filePath),
arrayOf("image/png")
) { _, scannedUri ->
if (scannedUri == null) {
continuation.cancel(Exception("File $filePath could not be scanned"))
} else {
continuation.resume(scannedUri)
}
}
}
}
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
outputStream().use { out ->
bitmap.compress(format, quality, out)
out.flush()
}
}
private fun shareBitmap(context: Context, uri: Uri) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/png"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(context, createChooser(intent, "Share your image"), null)
}
```
**OLDER WAY (Pre Compose 1.7.0-alpha07)**
-----------------------------------------
You can use `drawIntoCanvas` method and delegate to a `android.graphics.Picture` class.
```kotlin
val picture = remember { android.graphics.Picture() }
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.drawWithCache {
// Example that shows how to redirect rendering to an Android Picture and then
// draw the picture into the original destination
val width = this.size.width.toInt()
val height = this.size.height.toInt()
onDrawWithContent {
val pictureCanvas =
androidx.compose.ui.graphics.Canvas(
picture.beginRecording(
width,
height
)
)
draw(this, this.layoutDirection, pictureCanvas, this.size) {
this@onDrawWithContent.drawContent()
}
picture.endRecording()
drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
}
}
) {
ScreenContentToCapture()
}
```
You can then save the `Picture` to disk using the following code:
```kotlin
private fun createBitmapFromPicture(picture: Picture): Bitmap {
val bitmap = Bitmap.createBitmap(
picture.width,
picture.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
canvas.drawColor(android.graphics.Color.WHITE)
canvas.drawPicture(picture)
return bitmap
}
private suspend fun Bitmap.saveToDisk(context: Context): Uri {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"screenshot-${System.currentTimeMillis()}.png"
)
file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100)
return scanFilePath(context, file.path) ?: throw Exception("File could not be saved")
}
```
The full snippet put together:
```kotlin
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.Intent.createChooser
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Picture
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import com.example.compose.snippets.R
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import java.io.File
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
/*
* Copyright 2022 The Android Open Source Project
*
* 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.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Preview
@Composable
fun BitmapFromComposableSnippet() {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val picture = remember { Picture() }
val writeStorageAccessState = rememberMultiplePermissionsState(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// No permissions are needed on Android 10+ to add files in the shared storage
emptyList()
} else {
listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
)
// This logic should live in your ViewModel - trigger a side effect to invoke URI sharing.
// checks permissions granted, and then saves the bitmap from a Picture that is already capturing content
// and shares it with the default share sheet.
fun shareBitmapFromComposable() {
if (writeStorageAccessState.allPermissionsGranted) {
coroutineScope.launch(Dispatchers.IO) {
val bitmap = createBitmapFromPicture(picture)
val uri = bitmap.saveToDisk(context)
shareBitmap(context, uri)
}
} else if (writeStorageAccessState.shouldShowRationale) {
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = "The storage permission is needed to save the image",
actionLabel = "Grant Access"
)
if (result == SnackbarResult.ActionPerformed) {
writeStorageAccessState.launchMultiplePermissionRequest()
}
}
} else {
writeStorageAccessState.launchMultiplePermissionRequest()
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
FloatingActionButton(onClick = {
shareBitmapFromComposable()
}) {
Icon(Icons.Default.Share, "share")
}
}
) { padding ->
// [START android_compose_draw_into_bitmap]
Column(
modifier = Modifier
.padding(padding)
.fillMaxSize()
.drawWithCache {
// Example that shows how to redirect rendering to an Android Picture and then
// draw the picture into the original destination
val width = this.size.width.toInt()
val height = this.size.height.toInt()
onDrawWithContent {
val pictureCanvas =
androidx.compose.ui.graphics.Canvas(
picture.beginRecording(
width,
height
)
)
draw(this, this.layoutDirection, pictureCanvas, this.size) {
this@onDrawWithContent.drawContent()
}
picture.endRecording()
drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
}
}
) {
ScreenContentToCapture()
}
// [END android_compose_draw_into_bitmap]
}
}
@Composable
private fun ScreenContentToCapture() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.background(
Brush.linearGradient(
listOf(
Color(0xFFF5D5C0),
Color(0xFFF8E8E3)
)
)
)
) {
Image(
painterResource(id = R.drawable.sunset),
contentDescription = null,
modifier = Modifier
.aspectRatio(1f)
.padding(32.dp),
contentScale = ContentScale.Crop
)
Text(
"Into the Ocean depths",
fontSize = 18.sp
)
}
}
private fun createBitmapFromPicture(picture: Picture): Bitmap {
val bitmap = Bitmap.createBitmap(
picture.width,
picture.height,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
canvas.drawColor(android.graphics.Color.WHITE)
canvas.drawPicture(picture)
return bitmap
}
private suspend fun Bitmap.saveToDisk(context: Context): Uri {
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
"screenshot-${System.currentTimeMillis()}.png"
)
file.writeBitmap(this, Bitmap.CompressFormat.PNG, 100)
return scanFilePath(context, file.path) ?: throw Exception("File could not be saved")
}
/**
* We call [MediaScannerConnection] to index the newly created image inside MediaStore to be visible
* for other apps, as well as returning its [MediaStore] Uri
*/
private suspend fun scanFilePath(context: Context, filePath: String): Uri? {
return suspendCancellableCoroutine { continuation ->
MediaScannerConnection.scanFile(
context,
arrayOf(filePath),
arrayOf("image/png")
) { _, scannedUri ->
if (scannedUri == null) {
continuation.cancel(Exception("File $filePath could not be scanned"))
} else {
continuation.resume(scannedUri)
}
}
}
}
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
outputStream().use { out ->
bitmap.compress(format, quality, out)
out.flush()
}
}
private fun shareBitmap(context: Context, uri: Uri) {
val intent = Intent(Intent.ACTION_SEND).apply {
type = "image/png"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(context, createChooser(intent, "Share your image"), null)
}
```