[英]Jetpack Compose take screenshot of composable function?
I want to take screenshot of specific composable function on Jetpack Compose.我想在 Jetpack Compose 上截取特定可组合 function 的屏幕截图。 How can I do this?
我怎样才能做到这一点? Please, anyone help me.
请任何人帮助我。 I want to take screenshot of composable function and share with other applications.
我想截取可组合 function 的屏幕截图并与其他应用程序共享。
Example of my function:我的 function 示例:
@Composable
fun PhotoCard() {
Stack() {
Image(imageResource(id = R.drawable.background))
Text(text = "Example")
}
}
How to take screenshot of this function?如何截取这个 function 的截图?
I made a small library that screenshots Composables with single shot or periodically.我制作了一个小型库,可以单次或定期对 Composables 进行截图。
A state that is used for capturing and storing Bitmap or ImageBitmap一个 state 用于捕获和存储 Bitmap 或 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 [ScreenshotState.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,
) {
val imageState = mutableStateOf<ImageResult>(ImageResult.Initial)
val bitmapState = mutableStateOf<Bitmap?>(null)
internal var callback: (() -> Unit)? = null
/**
* Captures current state of Composables inside [ScreenshotBox]
*/
fun capture() {
callback?.invoke()
}
val liveScreenshotFlow = flow {
while (true) {
callback?.invoke()
delay(timeInMillis)
bitmapState.value?.let {
emit(it)
}
}
}
.map {
it.asImageBitmap()
}
.flowOn(Dispatchers.Default)
val bitmap: Bitmap?
get() = bitmapState.value
val imageBitmap: ImageBitmap?
get() = bitmap?.asImageBitmap()
}
ImageResult that contains error or success depending on process result根据处理结果包含错误或成功的 ImageResult
sealed class ImageResult {
object Initial : ImageResult()
data class Error(val exception: Exception) : ImageResult()
data class Success(val data: Bitmap) : ImageResult()
}
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: View = LocalView.current
var composableBounds by remember {
mutableStateOf<Rect?>(null)
}
DisposableEffect(Unit) {
screenshotState.callback = {
composableBounds?.let { bounds ->
if (bounds.width == 0f || bounds.height == 0f) return@let
view.screenshot(bounds) { imageResult: ImageResult ->
screenshotState.imageState.value = imageResult
if (imageResult is ImageResult.Success) {
screenshotState.bitmapState.value = imageResult.data
}
}
}
}
onDispose {
val bmp = screenshotState.bitmapState.value
bmp?.apply {
if (!isRecycled) {
recycle()
}
}
screenshotState.bitmapState.value = null
screenshotState.callback = null
}
}
Box(modifier = modifier
.onGloballyPositioned {
composableBounds = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
it.boundsInWindow()
} else {
it.boundsInRoot()
}
}
) {
content()
}
}
Functions for Capturing screenshot.用于捕获屏幕截图的功能。 PixelCopy is required for Devices with version O and above.
O 及以上版本的设备需要PixelCopy 。 And you can use these functions without Composables either
您也可以在没有 Composables 的情况下使用这些功能
fun View.screenshot(
bounds: Rect
): ImageResult {
try {
val bitmap = Bitmap.createBitmap(
bounds.width.toInt(),
bounds.height.toInt(),
Bitmap.Config.ARGB_8888,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Above Android O not using PixelCopy throws exception
// https://stackoverflow.com/questions/58314397/java-lang-illegalstateexception-software-rendering-doesnt-support-hardware-bit
PixelCopy.request(
(this.context as Activity).window,
bounds.toAndroidRect(),
bitmap,
{},
Handler(Looper.getMainLooper())
)
} else {
val canvas = Canvas(bitmap)
.apply {
translate(-bounds.left, -bounds.top)
}
this.draw(canvas)
canvas.setBitmap(null)
}
return ImageResult.Success(bitmap)
} catch (e: Exception) {
return ImageResult.Error(e)
}
}
fun View.screenshot(
bounds: Rect,
bitmapCallback: (ImageResult) -> Unit
) {
try {
val bitmap = Bitmap.createBitmap(
bounds.width.toInt(),
bounds.height.toInt(),
Bitmap.Config.ARGB_8888,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Above Android O not using PixelCopy throws exception
// https://stackoverflow.com/questions/58314397/java-lang-illegalstateexception-software-rendering-doesnt-support-hardware-bit
PixelCopy.request(
(this.context as Activity).window,
bounds.toAndroidRect(),
bitmap,
{
when (it) {
PixelCopy.SUCCESS -> {
bitmapCallback.invoke(ImageResult.Success(bitmap))
}
PixelCopy.ERROR_DESTINATION_INVALID -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"The destination isn't a valid copy target. " +
"If the destination is a bitmap this can occur " +
"if the bitmap is too large for the hardware to " +
"copy to. " +
"It can also occur if the destination " +
"has been destroyed"
)
)
)
}
PixelCopy.ERROR_SOURCE_INVALID -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"It is not possible to copy from the source. " +
"This can happen if the source is " +
"hardware-protected or destroyed."
)
)
)
}
PixelCopy.ERROR_TIMEOUT -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"A timeout occurred while trying to acquire a buffer " +
"from the source to copy from."
)
)
)
}
PixelCopy.ERROR_SOURCE_NO_DATA -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"The source has nothing to copy from. " +
"When the source is a Surface this means that " +
"no buffers have been queued yet. " +
"Wait for the source to produce " +
"a frame and try again."
)
)
)
}
else -> {
bitmapCallback.invoke(
ImageResult.Error(
Exception(
"The pixel copy request failed with an unknown error."
)
)
)
}
}
},
Handler(Looper.getMainLooper())
)
} else {
val canvas = Canvas(bitmap)
.apply {
translate(-bounds.left, -bounds.top)
}
this.draw(canvas)
canvas.setBitmap(null)
bitmapCallback.invoke(ImageResult.Success(bitmap))
}
} catch (e: Exception) {
bitmapCallback.invoke(ImageResult.Error(e))
}
}
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结果
You can create a test, set the content to that composable and then call composeTestRule.captureToImage()
.您可以创建一个测试,将内容设置为可组合的,然后调用
composeTestRule.captureToImage()
。 It returns an ImageBitmap
.它返回一个
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截图比较器中的使用示例: https : //github.com/android/compose-samples/blob/e6994123804b976083fa937d3f5bf926da4facc5/Rally/app/src/androidTest/java/com/example/compose/rally/ScreenshotComparator.kt
As @Commonsware mentioned in the comment, and assuming this is not about screenshot testing:正如@Commonsware在评论中提到的,假设这与屏幕截图测试无关:
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):根据官方文档,您可以使用
LocalView.current
访问可组合函数的视图版本,并将该视图导出到这样的位图文件(以下代码位于可组合函数内部):
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. writeBitmap
方法是 File 类的简单扩展函数。 Example:例子:
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
outputStream().use { out ->
bitmap.compress(format, quality, out)
out.flush()
}
}
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
:您可以使用
onGloballyPositioned
在根组合视图中获取可组合视图的位置,然后将根视图的所需部分绘制onGloballyPositioned
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.请注意,如果您在
ViewToCapture
之上有一些视图,例如与Box
一起放置,它仍然会在图像上。
ps 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. ps 有一个错误,它使
Modifier.graphicsLayer
效果、 offset { IntOffset(...) }
(在这种情况下您仍然可以使用offset(dp)
)、 scrollable
和惰性视图位置未正确显示在屏幕截图上。 If you've faced it, please star the issue to get more attention.如果您遇到过,请为该问题加注星标以获得更多关注。
您可以使用 @Preview 创建预览功能,在手机或模拟器上运行该功能并截取组件的屏幕截图。
Using PixelCopy worked for me:使用 PixelCopy 对我有用:
@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(),
)
}
}
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:确保您拥有此依赖项以及其他 Compose 依赖项:
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.注意:您可以简单地在androidTest目录中添加一个AndroidManifest.xml文件,并在
manifest
➜ application
元素中添加<activity android:name="androidx.activity.ComponentActivity" />
而不是上述依赖项。
Refer to this answer .参考这个答案。
The following is a complete example of taking, saving, reading, and comparing screenshots of a composable function called MyComposableFunction :以下是获取、保存、读取和比较名为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 .我也在这里回答了一个类似的问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.