![](/img/trans.png)
[英]Take screenshot of a composable fun programmatically in Jetpack Compose
[英]Jetpack Compose take screenshot of composable function?
我想在 Jetpack Compose 上截取特定可組合 function 的屏幕截圖。 我怎樣才能做到這一點? 請任何人幫助我。 我想截取可組合 function 的屏幕截圖並與其他應用程序共享。
我的 function 示例:
@Composable
fun PhotoCard() {
Stack() {
Image(imageResource(id = R.drawable.background))
Text(text = "Example")
}
}
如何截取這個 function 的截圖?
我制作了一個小型庫,可以單次或定期對 Composables 進行截圖。
一個 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
sealed class ImageResult {
object Initial : ImageResult()
data class Error(val exception: Exception) : ImageResult()
data class Success(val data: Bitmap) : ImageResult()
}
捕獲其子項的屏幕截圖的可組合項
/**
* 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()
}
}
用於捕獲屏幕截圖的功能。 O 及以上版本的設備需要PixelCopy 。 您也可以在沒有 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))
}
}
執行
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")
}
結果
您可以創建一個測試,將內容設置為可組合的,然后調用composeTestRule.captureToImage()
。 它返回一個ImageBitmap
。
截圖比較器中的使用示例: https : //github.com/android/compose-samples/blob/e6994123804b976083fa937d3f5bf926da4facc5/Rally/app/src/androidTest/java/com/example/compose/rally/ScreenshotComparator.kt
正如@Commonsware在評論中提到的,假設這與屏幕截圖測試無關:
根據官方文檔,您可以使用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
方法是 File 類的簡單擴展函數。 例子:
private fun File.writeBitmap(bitmap: Bitmap, format: Bitmap.CompressFormat, quality: Int) {
outputStream().use { out ->
bitmap.compress(format, quality, out)
out.flush()
}
}
您可以使用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()
}
)
請注意,如果您在ViewToCapture
之上有一些視圖,例如與Box
一起放置,它仍然會在圖像上。
ps 有一個錯誤,它使Modifier.graphicsLayer
效果、 offset { IntOffset(...) }
(在這種情況下您仍然可以使用offset(dp)
)、 scrollable
和惰性視圖位置未正確顯示在屏幕截圖上。 如果您遇到過,請為該問題加注星標以獲得更多關注。
您可以使用 @Preview 創建預覽功能,在手機或模擬器上運行該功能並截取組件的屏幕截圖。
使用 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(),
)
}
}
我一直在尋找如何在測試中截取可組合的屏幕截圖,這個問題出現在結果中的第一個。 因此,對於想要在測試中截取/保存/比較屏幕截圖或進行屏幕截圖測試的未來用戶,我將我的答案放在這里(感謝這個)。
確保您擁有此依賴項以及其他 Compose 依賴項:
debugImplementation("androidx.compose.ui:ui-test-manifest:<version>")
注意:您可以簡單地在androidTest目錄中添加一個AndroidManifest.xml文件,並在manifest
➜ application
元素中添加<activity android:name="androidx.activity.ComponentActivity" />
而不是上述依賴項。
參考這個答案。
以下是獲取、保存、讀取和比較名為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)
}
}
}
我也在這里回答了一個類似的問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.