Jetpack Compose 截取可组合函数的屏幕截图?
Jetpack Compose take screenshot of composable function?
我想截取 Jetpack Compose 上特定可组合函数的屏幕截图。我怎样才能做到这一点?请任何人帮助我。我想截取可组合函数的屏幕截图并与其他应用程序共享。
我的函数示例:
@Composable
fun PhotoCard() {
Stack() {
Image(imageResource(id = R.drawable.background))
Text(text = "Example")
}
}
这个功能怎么截图?
您可以使用 @Preview 创建预览功能,运行 phone 或模拟器上的功能并截取组件的屏幕截图。
您可以创建一个测试,将内容设置为该可组合项,然后调用 composeTestRule.captureToImage()
。它 returns 一个 ImageBitmap
.
如评论中@Commonsware所述,并假设这是而非关于屏幕截图测试:
根据 official docs you can access the view version of your composable function using 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)
writeBitmap
方法是Fileclass的简单扩展函数。示例:
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
outputStream().use { out ->
bitmap.compress(format, quality, out)
out.flush()
}
}
我在 tests 中寻找如何截取可组合项的屏幕截图,这个问题出现在结果中的第一个。因此,对于想要在测试中 take/save/compare 屏幕截图或进行 屏幕截图测试 的未来用户,我将答案放在这里(感谢 this)。
确保您拥有此依赖项以及其他 Compose 依赖项:
debugImplementation("androidx.compose.ui:ui-test-manifest:<version>")
注意:除了上面的依赖,你可以简单地在androidTest[=37=中添加一个AndroidManifest.xml文件] 目录并在 manifest
中添加 <activity android:name="androidx.activity.ComponentActivity" />
➜ application
元素。
参考this answer.
以下是截取、保存、读取和比较名为 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)
}
}
}
我也回答过类似的问题。
使用 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
}
示例:
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(),
)
}
}
您可以使用 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()
}
)
请注意,如果您在 ViewToCapture
之上有一些视图,例如与 Box
一起放置,它仍会出现在图像上。
p.s。有一个 bug 会产生 Modifier.graphicsLayer
效果,offset { IntOffset(...) }
(在这种情况下您仍然可以使用 offset(dp)
),scrollable
和惰性视图位置无法正确显示屏幕截图。如果您遇到过,请给问题加注星号以引起更多关注。
我制作了一个小型库,可以单次或定期对可组合项进行屏幕截图。
用于捕获和存储 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 [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()
}
捕获其子可组合项屏幕截图的可组合项
/**
* 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()
}
}
实施
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 })
}
}
正在截屏
Button(onClick = {
screenshotState.capture()
}) {
Text(text = "Take Screenshot")
}
结果
我想截取 Jetpack Compose 上特定可组合函数的屏幕截图。我怎样才能做到这一点?请任何人帮助我。我想截取可组合函数的屏幕截图并与其他应用程序共享。
我的函数示例:
@Composable
fun PhotoCard() {
Stack() {
Image(imageResource(id = R.drawable.background))
Text(text = "Example")
}
}
这个功能怎么截图?
您可以使用 @Preview 创建预览功能,运行 phone 或模拟器上的功能并截取组件的屏幕截图。
您可以创建一个测试,将内容设置为该可组合项,然后调用 composeTestRule.captureToImage()
。它 returns 一个 ImageBitmap
.
如评论中@Commonsware所述,并假设这是而非关于屏幕截图测试:
根据 official docs you can access the view version of your composable function using 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)
writeBitmap
方法是Fileclass的简单扩展函数。示例:
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
outputStream().use { out ->
bitmap.compress(format, quality, out)
out.flush()
}
}
我在 tests 中寻找如何截取可组合项的屏幕截图,这个问题出现在结果中的第一个。因此,对于想要在测试中 take/save/compare 屏幕截图或进行 屏幕截图测试 的未来用户,我将答案放在这里(感谢 this)。
确保您拥有此依赖项以及其他 Compose 依赖项:
debugImplementation("androidx.compose.ui:ui-test-manifest:<version>")
注意:除了上面的依赖,你可以简单地在androidTest[=37=中添加一个AndroidManifest.xml文件] 目录并在 manifest
中添加 <activity android:name="androidx.activity.ComponentActivity" />
➜ application
元素。
参考this answer.
以下是截取、保存、读取和比较名为 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)
}
}
}
我也回答过类似的问题
使用 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
}
示例:
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(),
)
}
}
您可以使用 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()
}
)
请注意,如果您在 ViewToCapture
之上有一些视图,例如与 Box
一起放置,它仍会出现在图像上。
p.s。有一个 bug 会产生 Modifier.graphicsLayer
效果,offset { IntOffset(...) }
(在这种情况下您仍然可以使用 offset(dp)
),scrollable
和惰性视图位置无法正确显示屏幕截图。如果您遇到过,请给问题加注星号以引起更多关注。
我制作了一个小型库,可以单次或定期对可组合项进行屏幕截图。
用于捕获和存储 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 [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()
}
捕获其子可组合项屏幕截图的可组合项
/**
* 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()
}
}
实施
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 })
}
}
正在截屏
Button(onClick = {
screenshotState.capture()
}) {
Text(text = "Take Screenshot")
}
结果