'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
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 |