Last active
December 6, 2023 09:59
-
-
Save raamcosta/b1d3a1554f00f4707024d88d8a4ed9f1 to your computer and use it in GitHub Desktop.
Render a Composable without showing it on screen
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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