Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Composable to bitmap without displaying it

When using classic Views it is easy to obtain a bitmap from a view without displaying it. I create the view class through a LayoutInflater, and then, since it hasn't been attached to a view, I measure it first. I have the following extension function which measures it and draws the view on a bitmap:

fun View.toBitmap(width, height): Bitmap {
    this.measure(
        View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
        View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY),
    )
    val bitmap = Bitmap.createBitmap(this.measuredWidth, this.measuredHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    this.layout(0, 0, this.measuredWidth, this.measuredHeight)
    this.draw(canvas)
    return bitmap
}

When using Composables I can't succeed in exporting a bitmap from a view.

I imagined something like this:

class MyComposableView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
): AbstractComposeView(context, attrs, defStyleAttr) {
    @Composable
    override fun Content() {
        MyComposable()
    }
}

What I did is instancing a MyComposableView with the application context, and then I tried to obtain a bitmap with the extension function toBitmap. The result is the following exception:

java.lang.IllegalStateException: Cannot locate windowRecomposer; View io.myapp.MyComposableView{f66ecdd V.E...... ......I. 0,0-0,0} is not attached to a window

What I can't understand is why the exception is thrown for the AbstractComposeView but is not thrown for the view obtained through the inflater.


EDIT: on 09 Apr. 2022 it seems there's not a solution other than using a classic XML layout.

like image 314
Massimo Avatar asked Aug 16 '26 01:08

Massimo


1 Answers

There's a way for it to capture the composable content by rendering composable content into an Invisible window and capturing it secretly from there.

Create a invisible composable

@Composable
fun InvisibleContent(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val windowManager = context.getSystemService<WindowManager>()!!

    DisposableEffect(key1 = content) {
        val composeView = ComposeView(context).apply {
            setParentCompositionContext(null)
            setContent {
                content()
            }
            setOwners(context.findActivity())
        }

        windowManager.addView(
            /* view = */ composeView,
            /* params = */ WindowManager.LayoutParams(
                /* w = */ WindowManager.LayoutParams.WRAP_CONTENT,
                /* h = */ WindowManager.LayoutParams.WRAP_CONTENT,
                /* _type = */ WindowManager.LayoutParams.TYPE_APPLICATION_PANEL,
                /* _flags = */ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                /* _format = */ PixelFormat.TRANSLUCENT
            )
        )

        onDispose { windowManager.removeView(composeView) }
    }
}

private fun View.setOwners(fromActivity: ComponentActivity) {
    if (findViewTreeLifecycleOwner() == null) {
        setViewTreeLifecycleOwner(fromActivity)
    }
    if (findViewTreeViewModelStoreOwner() == null) {
        setViewTreeViewModelStoreOwner(fromActivity)
    }
    if (findViewTreeSavedStateRegistryOwner() == null) {
        setViewTreeSavedStateRegistryOwner(fromActivity)
    }
}

/**
 * Traverses through this [Context] and finds [Activity] wrapped inside it.
 */
private fun Context.findActivity(): ComponentActivity {
    var context = this
    while (context is ContextWrapper) {
        if (context is ComponentActivity) return context
        context = context.baseContext
    }
    throw IllegalStateException("Unable to retrieve Activity from the current context")
}

Capture a composable from the above composable (Example)

In this example, using Capturable to capture the content because it uses the Compose canvas to draw picture.

@Composable
fun CaptureDemo() {
    val captureController = rememberCaptureController()
    val uiScope = rememberCoroutineScope()

    InvisibleContent {
        ContentToCaptureComposable(modifier = Modifier.capturable(captureController))
    }
    
    Button(
        onClick = {
            uiScope.launch {
               contentBitmap = captureController.captureAsync().await()
            }
        }
    ) {
        Text("Capture content Image")
    }
}

Here, the content of the ContentToCaptureComposable composable won't be displayed on the UI and it won't take place in the UI in the same View along with relative composables. Instead, it'll secretly added on another window with no visibility.

I've tried this and it works.

like image 151
Shreyas Patil Avatar answered Aug 17 '26 15:08

Shreyas Patil



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!