Skip to content

Instantly share code, notes, and snippets.

@raamcosta
Last active December 6, 2023 09:59
Show Gist options
  • Save raamcosta/b1d3a1554f00f4707024d88d8a4ed9f1 to your computer and use it in GitHub Desktop.
Save raamcosta/b1d3a1554f00f4707024d88d8a4ed9f1 to your computer and use it in GitHub Desktop.
Render a Composable without showing it on screen
class ComposeRenderer(
private val activity: ComponentActivity
) {
suspend fun render(
content: @Composable () -> Unit
): Result<Bitmap> {
val completableBitmap = CompletableDeferred<Result<Bitmap>>()
activity.awaitWindowToken()
val wm = activity.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val renderingView = createComposeView(wm, activity, content) { result ->
completableBitmap.complete(result)
}
val maxDimension = 2500
wm.addView(
renderingView,
LayoutParams(
maxDimension,
maxDimension,
LayoutParams.TYPE_APPLICATION_MEDIA,
LayoutParams.FLAG_NOT_FOCUSABLE or
LayoutParams.FLAG_NOT_TOUCHABLE or
LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.RGBA_8888
)
)
val result = completableBitmap.await()
wm.removeView(renderingView)
return result
}
private suspend fun Activity.awaitWindowToken() {
withTimeoutOrNull(1000) {
val attachedCompletable = CompletableDeferred<Unit>()
window.decorView.doOnAttach {
attachedCompletable.complete(Unit)
}
attachedCompletable.await()
} ?: error("Timed out waiting for activity window token.")
}
private fun createComposeView(
windowManager: WindowManager,
activity: ComponentActivity,
content: @Composable () -> Unit,
onBitmapCaptured: (Result<Bitmap>) -> Unit
): View = ComposeView(activity).apply {
setViewTreeLifecycleOwner(activity)
setViewTreeViewModelStoreOwner(activity)
setViewTreeSavedStateRegistryOwner(activity)
setContent {
var viewSize by remember { mutableStateOf(IntSize.Zero) }
var viewDrawn by remember { mutableStateOf(false) }
var viewReady by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.wrapContentSize(unbounded = true)
.onGloballyPositioned { viewSize = it.size }
) {
content()
// Wait until the content is drawn in the screen
if (viewSize.width != 0 && viewSize.height != 0) {
viewDrawn = true
}
}
LaunchedEffect(viewDrawn) {
if (!viewDrawn) {
return@LaunchedEffect
}
// Update view layout with the given width and length
windowManager.updateViewLayout(
this@apply,
LayoutParams(
viewSize.width,
viewSize.height,
LayoutParams.TYPE_APPLICATION_MEDIA,
LayoutParams.FLAG_NOT_FOCUSABLE or
LayoutParams.FLAG_NOT_TOUCHABLE or
LayoutParams.FLAG_LAYOUT_NO_LIMITS,
PixelFormat.RGB_565
)
)
// Update the state after updating the view layout
handler.post {
viewReady = true
}
}
LaunchedEffect(viewReady) {
if (!viewReady) {
return@LaunchedEffect
}
// Capture the compose view and draw it to a bitmap
val bitmap = withContext(Dispatchers.Main) {
runCatching {
this@apply.drawToBitmap(Bitmap.Config.RGB_565)
}
}
onBitmapCaptured(bitmap)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment