Exporting a Composable as an image (in an Android app)

How to export a Composable as an image?

When searching for ways to export something drawn by a Composable on screen, the answer often proposed is to fall back on the underlying ComposeView object and use the pre-existing View.draw(Canvas) method.

This works just fine for a lot of cases, but unfortunately for me, it does not work at all if the contents of your Composable has been drawn and altered using BlendMode (for example). In that case, something happens to the pixels drawn on screen that is lost when exporting the image.

Luckily, there are other methods to export Composables as an image that do not rely on the View object, as we’ll see.

What does View.draw(Canvas) do ?

Manually render this view (and all of its children) to the given Canvas. The view must have already done a full layout before this function is called. When implementing a view, implement onDraw(android.graphics.Canvas) instead of overriding this method. If you do need to override this method, call the superclass version.
If you override this method you must call through to the superclass implementation.
https://developer.android.com/reference/android/view/View#draw(android.graphics.Canvas)

Making the problem with View.draw obvious

Here is a little screenshot from a demo application which gives an example of how View.draw will fail to correctly render your composable as an image. Both original images have a beautiful green background to clearly show the background and foreground layers. Both images are tinted in red, and have an overlay of gray-colored polka dots. (Though they may appear pink to the human eye, all the dots are in fact gray).

You may recognize the briefcase Vector image and the sofa chair Raster image from my examples on using BlendModes.

There is an issue with the size of the rectangle which is exported, as you can see a wider than expected zone has been copied. This zone needs to be adjusted with Rectangle bounds to define precisely what to keep, and I’ve not done a great job defining these bounds in my example code.

As for the differences between the two images, these are listed below, though it is clear neither image correctly represents the original.

Briefcase example original image:

  • Vector image
  • Briefcase drawn before motif (an ImageShader ShaderBrush)
  • Briefcase tinted red with BlendMode.Modulate
  • Motif blended onto image with BlendMode.SrcAtop

Briefcase Vector image example result:

  • The dot motif extends outside the briefcase shape
  • The BlendMode.SrcAtop information has been lost

Sofa chair example original image:

  • Raster image
  • Chair drawn after the polka-dot motif (but same ShaderBrush)
  • Chair tinted red with BlendMode.Modulate
  • Chair blended onto motif rectangle with BlendMode.DstAtop

Chair raster image result:

  • The background color (green) is replaced by black
  • The chair tint (red) is lost, we see the green background instead
  • The raster image’s inner shading is lost, all we have is contour and gray dots (dots appear pink to us because of the juxtaposed green)

Side note: yes, those dots are gray

In case you do not believe the dots are actually gray (I was doubtful myself), here is a screen capture where I used a color-picking tool to check the color code of a dot.

The human eye is wonderful, but is prone to some weird illusions. This one is called the « Munker-White illusion« .

What’s happening in the image export in the View.draw function?

Clearly, something is happening to the pixels drawn on screen that is lost when exporting the image with View.draw.

What happens is that Hardware acceleration is used (« The view drawing cache was largely made obsolete with the introduction of hardware-accelerated rendering in API 11. ») for handling operations such as compositing pixels, and the View does not know about these « accelerated » steps, and so cannot redraw the complete contents into an image.

Alternatives to View.draw: Pixel Copy, and the new rememberGraphicsLayer()

The two following illustrations show the resulting exported image when using either Pixel Copy library or the newly released rememberGraphicsLayer() function (available from Compose 1.7.0-alpha07+).

The results for the Vector Drawable image (the briefcase) are similarly perfect.

In terms of ease of use, rememberGraphicsLayer is the easiest to set up, but if you cannot use this version on Compose for some reason, Pixel Copy is fine but just requires a little more code to set up.

I won’t go into the source code here for now, Google gave some example code for rememberGraphicsLayer here, and my example code is available as well.

Wrapping up

The SEO thingy tells me I need to repeat this, so here we go.

With these examples, I’ve tried to show how to export Composables as an image, how to avoid problems with exporting a Composable as an image, and what the available solutions are.

Source code

All of the source code for these examples is available on my github.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *