Skip to content

Instantly share code, notes, and snippets.

@LennyLizowzskiy
Created July 21, 2023 12:26
Show Gist options
  • Save LennyLizowzskiy/e55f86b912ebb13b4196b472a577c27a to your computer and use it in GitHub Desktop.
Save LennyLizowzskiy/e55f86b912ebb13b4196b472a577c27a to your computer and use it in GitHub Desktop.
Jetpack Compose / Composable to Bitmap converter
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.view.PixelCopy
import android.view.View
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
@Stable
interface BitmappableScope {
@Stable
suspend fun convertContentToBitmap(): Bitmap
}
@Stable
private class BitmappableScopeImpl(
val view: View,
val bounds: Rect
) : BitmappableScope {
@Stable
override suspend fun convertContentToBitmap() = view.clipContent(bounds)
}
@Composable
fun Bitmappable(
modifier: Modifier = Modifier,
content: @Composable BitmappableScope.() -> Unit
) {
var contentBounds by remember {
mutableStateOf<Rect?>(null)
}
Box(
modifier = modifier
.onGloballyPositioned {
contentBounds = it.boundsInWindow()
}
) {
val view = LocalView.current
BitmappableScopeImpl(view, contentBounds ?: Rect.Zero).content()
}
}
@Stable
fun Context.getActivity(): Activity? =
when (this) {
is Activity -> this
is ContextWrapper -> this.getActivity()
else -> null
}
@Stable
fun View.clipContent(
bounds: Rect
): Bitmap {
val bitmap = Bitmap.createBitmap(
bounds.width.roundToInt(),
bounds.height.roundToInt(),
Bitmap.Config.ARGB_8888
)
PixelCopy.request( // Android Oreo+
checkNotNull(context.getActivity()).window,
android.graphics.Rect(
bounds.left.toInt(),
bounds.top.toInt(),
bounds.right.toInt(),
bounds.bottom.toInt()
),
bitmap, {}, Handler(Looper.getMainLooper())
)
return bitmap
}
@Preview
@Composable
fun BitmappablePreview() {
var bitmap by remember {
mutableStateOf<Bitmap?>(null)
}
DisposableEffect(Unit) {
onDispose {
bitmap?.recycle()
}
}
var runConverter by remember {
mutableStateOf(false)
}
Column(
modifier = Modifier.padding(50.dp)
) {
Bitmappable(
modifier = Modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
runConverter = true
}
) {
LaunchedEffect(runConverter) {
if (runConverter) {
bitmap = convertContentToBitmap()
}
}
Box(
Modifier
.size(100.dp)
.background(Color.Yellow)
)
}
Spacer(Modifier.height(16.dp))
Box {
if (bitmap != null) {
Image(bitmap = bitmap!!.asImageBitmap(), contentDescription = null)
} else {
Box(Modifier.size(100.dp)) {
Text(text = "NO CONTENT", modifier = Modifier.size(100.dp))
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment