Open panoramix360 opened 2 years ago
From what I've seen is not possible, would be nice to have that option though :) @PatilShreyas wdyt?
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.
I had the same problem, I had to create another screen for the desired image capturing
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.
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.
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?
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
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 🤔
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?) {}
})
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)
Maybe something to follow along with https://issuetracker.google.com/issues/288494724
Let's keep an eye on it 👁️
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.
@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")
}
}
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.
I think this API allows this without all your window manager code
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.
But I think this will occupy a space in UI. We don't want that.
Let me see if that can be avoided. I suspect it can.
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
@yschimke current API has these limitations, that's why this solution so far has worked (even if it's a hack)
我尝试了一种快速解决方法,并在这里进行了解释: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
Is it possible to capture another version of a Composable just to the Capturable and capture a bitmap?