'Jetpack Compose take screenshot of composable function?

I want to take screenshot of specific composable function on Jetpack Compose. How can I do this? Please, anyone help me. I want to take screenshot of composable function and share with other applications.

Example of my function:

@Composable
fun PhotoCard() {
    Stack() {
        Image(imageResource(id = R.drawable.background))
        Text(text = "Example")
    }
}

How to take screenshot of this function?



Solution 1:[1]

As @Commonsware mentioned in the comment, and assuming this is not about screenshot testing:

According to official docs you can access the view version of your composable function using LocalView.current, and export that view to a bitmap file like this (the following code goes inside the composable function):

    val view = LocalView.current
    val context = LocalContext.current

    val handler = Handler(Looper.getMainLooper())
    handler.postDelayed(Runnable {
        val bmp = Bitmap.createBitmap(view.width, view.height,
            Bitmap.Config.ARGB_8888).applyCanvas {
            view.draw(this)
        }
        bmp.let {
            File(context.filesDir, "screenshot.png")
                .writeBitmap(bmp, Bitmap.CompressFormat.PNG, 85)
        }
    }, 1000)

The writeBitmap method is a simple extension function for File class. Example:

private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
    outputStream().use { out ->
        bitmap.compress(format, quality, out)
        out.flush()
    }
}

Solution 2:[2]

You can create a test, set the content to that composable and then call composeTestRule.captureToImage(). It returns an ImageBitmap.

Example of usage in a screenshot comparator: https://github.com/android/compose-samples/blob/e6994123804b976083fa937d3f5bf926da4facc5/Rally/app/src/androidTest/java/com/example/compose/rally/ScreenshotComparator.kt

Solution 3:[3]

You can get position of a composable view inside the root compose view using onGloballyPositioned, and then draw the needed part of the root view into the Bitmap:

val view = LocalView.current
var capturingViewBounds by remember { mutableStateOf<Rect?>(null) }
Button(onClick = {
    val bounds = capturingViewBounds ?: return@Button
    val image = Bitmap.createBitmap(
        bounds.width.roundToInt(), bounds.height.roundToInt(),
        Bitmap.Config.ARGB_8888
    ).applyCanvas {
        translate(-bounds.left, -bounds.top)
        view.draw(this)
    }
}) {
    Text("Capture")
}
ViewToCapture(
    modifier = Modifier
        .onGloballyPositioned {
            capturingViewBounds = it.boundsInRoot()
        }
)

Note that if you have some view on top of ViewToCapture, like placed with a Box, it'll still be on the image.

p.s. there's a bug which makes Modifier.graphicsLayer effects, offset { IntOffset(...) }(you still can use offset(dp) in this case), scrollable and lazy views position not being displayed correctly on the screenshot. If you've faced it, please star the issue to get more attention.

Solution 4:[4]

I made a small library that screenshots Composables with single shot or periodically.

A state that is used for capturing and storing Bitmap or ImageBitmap

/**
 * Create a State of screenshot of composable that is used with that is kept on each recomposition.
 * @param delayInMillis delay before each screenshot if [liveScreenshotFlow] is collected.
 */
@Composable
fun rememberScreenshotState(delayInMillis: Long = 20) = remember {
    ScreenshotState(delayInMillis)
}

/**
 * State of screenshot of composable that is used with.
 * @param timeInMillis delay before each screenshot if [liveScreenshotFlow] is collected.
 */
class ScreenshotState internal constructor(
    private val timeInMillis: Long = 20
) {
    internal var callback: (() -> Bitmap?)? = null

    private val bitmapState = mutableStateOf(callback?.invoke())

    /**
     * Captures current state of Composables inside [ScreenshotBox]
     */
    fun capture() {
        bitmapState.value = callback?.invoke()
    }

    val liveScreenshotFlow = flow {
        while (true) {
            val bmp = callback?.invoke()
            bmp?.let {
                emit(it)
            }
            delay(timeInMillis)
        }
    }

    val bitmap: Bitmap?
        get() = bitmapState.value

    val imageBitmap: ImageBitmap?
        get() = bitmap?.asImageBitmap()
}

Composable that captures screenshot of its children Composables

/**
 * A composable that gets screenshot of Composable that is in [content].
 * @param screenshotState state of screenshot that contains [Bitmap].
 * @param content Composable that will be captured to bitmap on action or periodically.
 */
@Composable
fun ScreenshotBox(
    modifier: Modifier = Modifier,
    screenshotState: ScreenshotState,
    content: @Composable () -> Unit,
) {
    val view = LocalView.current

    var composableBounds by remember {
        mutableStateOf<Rect?>(null)
    }

    DisposableEffect(Unit) {

        var bitmap: Bitmap? = null

        screenshotState.callback = {
            composableBounds?.let { bounds ->

                if (bounds.width == 0f || bounds.height == 0f) return@let

                bitmap = Bitmap.createBitmap(
                    bounds.width.toInt(),
                    bounds.height.toInt(),
                    Bitmap.Config.ARGB_8888
                )

                bitmap?.let { bmp ->
                    val canvas = Canvas(bmp)
                        .apply {
                            translate(-bounds.left, -bounds.top)
                        }
                    view.draw(canvas)
                }
            }
            bitmap
        }

        onDispose {
            bitmap?.apply {
                if (!isRecycled) {
                    recycle()
                    bitmap = null
                }
            }
            screenshotState.callback = null
        }
    }

    Box(modifier = modifier
        .onGloballyPositioned {
            composableBounds = it.boundsInRoot()
        }
    ) {
        content()
    }
}

Implementation

val screenshotState = rememberScreenshotState()

var progress by remember { mutableStateOf(0f) }

ScreenshotBox(screenshotState = screenshotState) {
    Column(
        modifier = Modifier
            .border(2.dp, Color.Green)
            .padding(5.dp)
    ) {

        Image(
            bitmap = ImageBitmap.imageResource(
                LocalContext.current.resources,
                R.drawable.landscape
            ),
            contentDescription = null,
            modifier = Modifier
                .background(Color.LightGray)
                .fillMaxWidth()
                // This is for displaying different ratio, optional
                .aspectRatio(4f / 3),
            contentScale = ContentScale.Crop
        )

        Text(text = "Counter: $counter")
        Slider(value = progress, onValueChange = { progress = it })
    }
}

Capturing screenshot

Button(onClick = {
    screenshotState.capture()
}) {
    Text(text = "Take Screenshot")
}

Result

enter image description here

Solution 5:[5]

You can create a preview function with @Preview , run the function on phone or emulator and take the screenshot of the component.

Solution 6:[6]

Using PixelCopy worked for me:

@RequiresApi(Build.VERSION_CODES.O)
suspend fun Window.drawToBitmap(
    config: Bitmap.Config = Bitmap.Config.ARGB_8888,
    timeoutInMs: Long = 1000
): Bitmap {
    var result = PixelCopy.ERROR_UNKNOWN
    val latch = CountDownLatch(1)

    val bitmap = Bitmap.createBitmap(decorView.width, decorView.height, config)
    PixelCopy.request(this, bitmap, { copyResult ->
        result = copyResult
        latch.countDown()
    }, Handler(Looper.getMainLooper()))

    var timeout = false
    withContext(Dispatchers.IO) {
        runCatching {
            timeout = !latch.await(timeoutInMs, TimeUnit.MILLISECONDS)
        }
    }

    if (timeout) error("Failed waiting for PixelCopy")
    if (result != PixelCopy.SUCCESS) error("Non success result: $result")

    return bitmap
}

Example:

val scope = rememberCoroutineScope()
val context = LocalContext.current as Activity
var bitmap by remember { mutableStateOf<Bitmap?>(null) }

Button(onClick = {
    scope.launch {
        //wrap in a try catch/block
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            bitmap = context.window.drawToBitmap()
        }
    }

}) {
    Text(text = "Take Screenshot")
}

Box(
    modifier = Modifier
        .background(Color.Red)
        .padding(10.dp)
) {
    bitmap?.let {
        Image(
            bitmap = it.asImageBitmap(),
            contentDescription = null,
            modifier = Modifier.fillMaxSize(),
        )
    }
}

Solution 7:[7]

I was looking for how to take screenshot of a composable in tests and this question appeared as the first in results. So, for future users who want to take/save/compare screenshots in tests or do screenshot testing, I put my answer here (thanks to this).

Ensure you have this dependency along with other Compose dependencies:

debugImplementation("androidx.compose.ui:ui-test-manifest:<version>")

Note: Instead of the above dependency, you can simply add an AndroidManifest.xml file in androidTest directory and add <activity android:name="androidx.activity.ComponentActivity" /> in manifest ? application element.
Refer to this answer.

The following is a complete example of taking, saving, reading, and comparing screenshots of a composable function called MyComposableFunction:

class ScreenshotTest {

    @get:Rule val composeTestRule = createComposeRule()

    @Test fun takeAndSaveScreenshot() {
        composeTestRule.setContent { MyComposableFunction() }
        val node = composeTestRule.onRoot()
        val screenshot = node.captureToImage().asAndroidBitmap()
        saveScreenshot("screenshot.png", screenshot)
    }

    @Test fun readAndCompareScreenshots() {
        composeTestRule.setContent { MyComposableFunction() }
        val node = composeTestRule.onRoot()
        val screenshot = node.captureToImage().asAndroidBitmap()

        val context = InstrumentationRegistry.getInstrumentation().targetContext
        val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val file = File(path, "screenshot.png")
        val saved = readScreenshot(file)

        println("Are screenshots the same: ${screenshot.sameAs(saved)}")
    }

    private fun readScreenshot(file: File) = BitmapFactory.decodeFile(file.path)

    private fun saveScreenshot(filename: String, screenshot: Bitmap) {
        val context = InstrumentationRegistry.getInstrumentation().targetContext
        // Saves in /Android/data/your.package.name.test/files/Pictures on external storage
        val path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val file = File(path, filename)
        file.outputStream().use { stream ->
            screenshot.compress(Bitmap.CompressFormat.PNG, 100, stream)
        }
    }
}

I've also answered a similar question here.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Hrafn
Solution 2 Jose Alcérreca
Solution 3
Solution 4 Thracian
Solution 5 LDQ
Solution 6 Bruno
Solution 7