androidandroid-layoutbitmapandroid-canvasscreenshot

Saving a screenshot of a custom view on Android


I'm trying to save a screenshot of my current Activity, but not the whole view, just part of it. In my case: My activity

At the attached image, i want to save a bitmap constructed from views B,C & D.

B is a linear layout, C is a pure bitmap, and D is a relative layout.

To my understanding, one way to go is creating a canvas, add all 'elements' into it and finally have the custom bitmap desired.

I'm having trouble with the following code:

iViewBHeight= viewB.getHeight();
// Prepare empty bitmap as output
Bitmap result = Bitmap.createBitmap(bitmapC.getWidth(),bitmapC.getHeight() +     iViewBHeight, Config.ARGB_8888);
// Flush source image into canvas
Canvas canvas = new Canvas(result);
// Draw bitmap C to canvas 'under' view B
canvas.drawBitmap(bitmapC, 0, iViewBHeight, null);
// Draw view B to canvas
viewB.setDrawingCacheEnabled(true);
viewB.buildDrawingCache(true);
canvas.drawBitmap(Bitmap.createBitmap(viewB.getDrawingCache()), 0, 0, null);
viewB.setDrawingCacheEnabled(false);

// Desired bitmap is at 'result'

The result is that bitmap C is drawn Ok, but view B is too large and extends the bitmap. I didn't try adding view D..

Can anyone help me? Maybe there are better ways achieving my goal? Thanks!


Solution

  • View's drawing cache has been deprecated for a while now, and it was never really a good idea for stuff like this anyway, even though it seemed to be what all of the available examples demonstrated (including mine here).

    Messing with the cache is kinda silly to begin with because any View that works with that approach is also able to be drawn directly to our own Canvas and Bitmap, so forcing the View to do it internally too is redundant and wasteful.

    We can further simplify things by allowing the desired image bounds to be defined by the set of Views that it contains, rather than doing tedious arithmetic for specific setups. Basing the bounds calculations on the Window's decor View makes things even easier, and will allow us to capture pretty much any part of an Activity, even if we don't own all of the content; e.g., like when the ActionBar is provided automagically.

    import android.app.Activity
    import android.graphics.Bitmap
    import android.graphics.Rect
    import android.view.View
    import androidx.core.graphics.applyCanvas
    import androidx.core.graphics.withTranslation
    
    fun Activity.captureToBitmap(
        vararg boundedViews: View,
        config: Bitmap.Config = Bitmap.Config.ARGB_8888
    ): Bitmap {
        val bounds = Rect()
        val viewBounds = Rect()
        val viewLocation = IntArray(2)
        boundedViews.forEach { view ->
            view.getLocationInWindow(viewLocation)
            viewBounds.set(0, 0, view.width, view.height)
            viewBounds.offset(viewLocation[0], viewLocation[1])
            bounds.union(viewBounds)
        }
        return drawToBitmap(bounds, config)
    }
    
    fun Activity.drawToBitmap(
        bounds: Rect,
        config: Bitmap.Config = Bitmap.Config.ARGB_8888
    ): Bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), config)
        .applyCanvas {
            val x = -bounds.left.toFloat()
            val y = -bounds.top.toFloat()
            withTranslation(x, y, window.decorView::draw)
        }
    

    I'm not sure of the exact setup in the question's layout, but if B contains C and D, then calling captureToBitmap(viewB) is sufficient. Otherwise, there's really no harm in listing everything to make sure:

    val bitmap = captureToBitmap(viewB, viewC, viewD)
    

    Of course, you should do this on a separate thread or coroutine, but be careful not to modify any Views while off the main thread.

    If you need a solution in Compose, there's an official example on the Graphics modifiers page.