Skip to content

Instantly share code, notes, and snippets.

@chiragthummar
Created September 6, 2023 14:13
Show Gist options
  • Save chiragthummar/35aa1d15882c4d5a105a3f89be821214 to your computer and use it in GitHub Desktop.
Save chiragthummar/35aa1d15882c4d5a105a3f89be821214 to your computer and use it in GitHub Desktop.
package com.example.jetpackcomposeplayground
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Picture
import android.net.Uri
import android.os.Environment
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.drawscope.draw
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.unit.dp
import java.io.File
import android.Manifest
import android.content.Intent
import android.content.Intent.createChooser
import android.media.MediaScannerConnection
import android.os.Build
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.aspectRatio
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.graphics.Brush
import androidx.compose.ui.graphics.Color
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.sp
import androidx.core.content.ContextCompat.startActivity
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlin.coroutines.resume
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@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.banner),
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 = android.graphics.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")
}
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)
}
@DawnNguyenAhiho
Copy link

Hello, thanks for sharing this great code. But when apply it into my project, there is a problem occur. It can't capture the image which is loaded by Coil. The picture saved to Gallery is totally black. Can I do something with this.

@chiragthummar
Copy link
Author

i'll check it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment