Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible to capture Composables not visible on screen #33

Open
panoramix360 opened this issue Apr 5, 2022 · 19 comments
Open

Possible to capture Composables not visible on screen #33

panoramix360 opened this issue Apr 5, 2022 · 19 comments

Comments

@panoramix360
Copy link

panoramix360 commented Apr 5, 2022

Is it possible to capture another version of a Composable just to the Capturable and capture a bitmap?

@renanboni
Copy link

From what I've seen is not possible, would be nice to have that option though :) @PatilShreyas wdyt?

@panoramix360
Copy link
Author

Maybe using something like a canvas or a graphics context, I don't know if it's really possible.

But has great advantages in doing so, sometimes the app has the need to generate another version of a shown component.

@jayesh83
Copy link

jayesh83 commented May 2, 2022

I had the same problem, I had to create another screen for the desired image capturing

@PatilShreyas
Copy link
Owner

If something which is not yet rendered on screen then it's not possible to capture. This is not case only limited to compose but it's also not possible in View as well.

@renanboni
Copy link

hey @PatilShreyas, in fact, it is possible to render even if the view (regular Android view not compose) hasn't been laid out yet, but for compose I think it's not possible so far.

@panoramix360
Copy link
Author

hey @PatilShreyas, in fact, it is possible to render even if the view (regular Android view not compose) hasn't been laid out yet, but for compose I think it's not possible so far.

Can you describe how this solution using View could work? Maybe we can abstract away from Jetpack Compose and use AndroidView or something like that.

Do you think it's possible?

@renanboni
Copy link

hey @PatilShreyas, in fact, it is possible to render even if the view (regular Android view not compose) hasn't been laid out yet, but for compose I think it's not possible so far.

Can you describe how this solution using View could work? Maybe we can abstract away from Jetpack Compose and use AndroidView or something like that.

Do you think it's possible?

sure, I'll write a snippet over the weekend and will share it here

@StephenVinouze
Copy link

Creating a Bitmap from a View is clearly doable without being visible on the screen. But It must be laid out with the desired width and height then you can use the drawToBitmap() KTX extension method. A typical example when you need such things is when you draw custom markers on a map since the renderer accepts a Bitmap that must be created before it will be attached to the screen.
I've written this snippet: https://gist.github.com/StephenVinouze/6cbba532cb202fa9eb507f5224f73462

As for Compose, there would be a way to capture a Bitmap from a Composable not visible on the screen given this article. Not sure I'd recommend it though 🤔

@akardas16
Copy link

akardas16 commented Nov 6, 2023

If something which is not yet rendered on screen then it's not possible to capture. This is not case only limited to compose but it's also not possible in View as well.

could you check this library https://github.com/guhungry/android-photo-manipulator I can overlay images or text on each other without showing on screen.
Example code

 Glide.with(context).asBitmap()
            .load(backgroundUrl)
            .into(object : CustomTarget<Bitmap>() {
                override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
                    // saveBitmapAsImageToDevice(resource)
                    val point = PointF()
                    point.x = 30f
                    point.y = 30f

                   // val mIcon = BitmapFactory.decodeResource(resources, R.drawable.overlay)
                    // BitmapUtils.overlay(background,mIcon, point)
                    BitmapUtils.printText(resource, "Made with\nSnapface", point, Color.WHITE, 32f)

                    context.saveBitmapAsImageToDevice(resource)


                }

                override fun onLoadCleared(placeholder: Drawable?) {}
            })

@kezc
Copy link

kezc commented Mar 5, 2024

I see someone has done it on StackOverflow https://stackoverflow.com/a/74814850/6745085
Sadly, it doesn't handle many corner cases as this library does (e.g. doesn't work with Coil)

@yschimke
Copy link

Maybe something to follow along with https://issuetracker.google.com/issues/288494724

@PatilShreyas
Copy link
Owner

Let's keep an eye on it 👁️

@PatilShreyas
Copy link
Owner

I tried it with a hack for a quick workaround and explained it here: https://stackoverflow.com/a/78170757/11326621


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")
}

Usage

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

    InvisibleContent {
        Ticket(modifier = Modifier.capturable(captureController))
    }
    
    Button(
        onClick = {
            uiScope.launch {
               ticketBitmap = captureController.captureAsync().await()
            }
        }
    ) {
        Text("Preview Ticket Image")
    }
}

Here, the content of the Ticket 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. Let me know your thoughts and if it works for you.

@yschimke
Copy link

I think this API allows this without all your window manager code

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt?q=buildLayer%20rememberGraphicsLayer

You can create a new graphics layer, the draw into it as well as or instead of the content

            val graphicsLayer = rememberGraphicsLayer()
...
    @Composable
    private fun Modifier.drawIntoLayer(
        layer: GraphicsLayer = obtainLayer()
    ): Modifier {
        return drawWithContent {
            layer.buildLayer {
                this@drawWithContent.drawContent()
            }
            drawLayer(layer)
        }
    }

But I'd have to try to confirm.

@PatilShreyas
Copy link
Owner

But I think this will occupy a space in UI. We don't want that.

@yschimke
Copy link

Let me see if that can be avoided. I suspect it can.

@yschimke
Copy link

Yeah, I couldn't get it working. I was trying to create a new graphics layer, and a modifier to avoid drawing to the screen, and then either capture a bitmap with beginRecording, or just draw to the new layer and write that to a canvas/ImageBitmap.

But it's still treating the Composables as part of the main composition, so I can't actually change the size to something greater.

I suspect I need non landed CLs to get this working. https://android-review.googlesource.com/c/platform/frameworks/support/+/2969199/4

@PatilShreyas
Copy link
Owner

@yschimke current API has these limitations, that's why this solution so far has worked (even if it's a hack)

@LZRight123
Copy link

我尝试了一种快速解决_方法_,并在这里进行了解释:https://stackoverflow.com/a/78170757/11326621

有一种方法可以通过将可组合内容渲染到不可见窗口中并从那里秘密捕获它来捕获可组合内容。

创建一个不可见的可组合项

@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")
}

用法

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

    InvisibleContent {
        Ticket(modifier = Modifier.capturable(captureController))
    }
    
    Button(
        onClick = {
            uiScope.launch {
               ticketBitmap = captureController.captureAsync().await()
            }
        }
    ) {
        Text("Preview Ticket Image")
    }
}

在这里,可组合项的内容Ticket不会显示在 UI 上,也不会与相关可组合项一起出现在同一视图的 UI 中。相反,它会秘密地添加到另一个不可见的窗口上。

我已经尝试过这个并且有效。让我知道您的想法以及它是否适合您。

The content in InvisibleContent is displayed at the front of the screen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants