I'm working on a share feature that would give use...
# compose
c
I'm working on a share feature that would give users an image of their stats/post to share elsewhere. I'm able to use the
onDrawWithContent
modifier to draw my composable to a Picture. I then convert that Picture to a bitmap, save as a PNG, and share the URI accordingly. However, I haven't figured out how to draw the picture off-screen / invisible / somehow hidden in the parent composable. While the content is only drawn to the picture, I still have to preserve the total space of the needed canvas on the screen. Any ideas on how to work around this, or if there is a better overall approach? Thanks!
Copy code
val picture = remember { Picture() }
    
Button(onClick = {share(picture)}) {
    Text("Share")
}

Box(
    modifier = Modifier.size(400.dp).drawWithCache {
        val width = this.size.width.toInt()
        val height = this.size.height.toInt()

        onDrawWithContent {
            val pictureCanvas =
                androidx.compose.ui.graphics.Canvas(
                    picture.beginRecording(
                        width,
                        height
                    )
                )

            draw(this, this.layoutDirection, pictureCanvas, this.size) {
                this@onDrawWithContent.drawContent()
            }

            picture.endRecording()
        }
    }
) {
    SharedContent()
}
n
The latest Compose 1.7 alpha introduces the new GraphicsLayer API which can be used to capture drawing commands for a portion of the composition UI hierarchy and replay those drawings instructions elsewhere including to a bitmap.
Copy code
@Composable
@Composable
fun MyComposable(
    modifier: Modifier, 
    content: @Composable () -> Unit
) {
    val context = LocalContext.current
    Box(modifier.drawWithCache {
        val layer = obtainGraphicsLayer().apply { 
            record { 
                drawContent()
            }
        }
        GlobalScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
            val imageBitmap = layer.toImageBitmap()
            val outputStream = context.openFileOutput("MyGraphicsLayerImageBitmap.png",
                Context.MODE_PRIVATE
            )
            // Create a bitmap from the graphicsLayer and save it to disk
            imageBitmap.asAndroidBitmap().compress(Bitmap.CompressFormat.PNG, 100, outputStream)
        }
        onDrawWithContent {
            // Draw the layer into the original destination
            drawLayer(layer)
        }
    }) {
        content()
    }    
}
👍 1
c
Interesting - I've given GraphicsLayer a look and there are some neat things in there! I'm still not able to figure out what I'm after though. Simply put, it there a way to only draw something offscreen, and never actually show it on screen. Omitting the
OnDrawContent
block in your example almost works for this goal. The content is indeed not drawn. However, the parent composable Box must still be a large enough size to contain the content. In this case, I end up with a blank space. I can use another box to draw the blank space behind some other content that I do want to show. However, I'm curious if there is a more graceful alternative!
n
If you don't want to draw the content on screen you can have an empty onDrawWithContent implementation. Also the GraphicsLayer#record method consumes a size parameter as well. By default it will leverage the same size as the draw modifier it is used within
c
@Nader Jawad - I Still can't sort out this one. I found that setting a size on the record method does set the size of the captured image accordingly, regardless of the composable "content()" size. However, because the composable's size is being set to 0, the captured image ends up as a black square. Thanks for taking a deep dive on this one with me so far!
Copy code
Box(
    modifier = Modifier
        .size(0.dp)
        .drawWithCache {
            graphicsLayer = obtainGraphicsLayer().apply {
                record(size = IntSize(1080, 1080)) {
                    drawContent()
                }
            }
            onDrawWithContent {}
        }
) {
    Surface {
        Text("Share Me")
    }
}
Actually, after a bit of experimenting, this can be achieved by ... • set parent Box size to 0.dp • set record size in graphics layer to 400.dp • leave onDrawWithContent blank • set wrapContentHeight & width to unbound true in child composable (the one being recorded) • set requiredSize to 400.dp in the child composable
Copy code
Box(
    modifier = Modifier
        .size(0.dp)
        .drawWithCache {
            graphicsLayer = obtainGraphicsLayer().apply {
                record(size = IntSize(400.dp.toPx().toInt(), 400.dp.toPx().toInt())) {
                    drawContent()
                }
            }
            onDrawWithContent { }
        }
) {
    Box(
        modifier = Modifier
            .wrapContentHeight(unbounded = true, align = Alignment.Top)
            .wrapContentWidth(unbounded = true, align = Alignment.Start)
            .requiredSize(400.dp)
    ) {
        Surface(modifier = Modifier.fillMaxSize()) {
            Text("Share Me")
        }
    }
}
n
If the size of a composable is zero, it will not draw. Can you provide some background for what you are trying to achieve here?
c
The use case is creating a graphic that the user can save to their device or share on social media. Some examples I can think of would be creating a receipt or invoice, an "achievement" to post of instagram, or a PDF version of a report, etc. I'm looking for a way to generate these images without having to show them to the user onscreen first. For comparison, I use ImageRenderer with Swift to accomplish this on the iOS version of my app