提问者:小点点

喷气背包 撰写可组合功能的屏幕截图?


我想截取喷气背包撰写上特定可组合功能的屏幕截图。我该怎么做?拜托,有人帮助我。我想截取可组合功能的屏幕截图并与其他应用程序共享。

我的函数示例:

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

如何截取此功能的屏幕截图?


共3个答案

匿名用户

正如@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()
    }
}

匿名用户

我制作了一个小型库,可以单次或定期截取可组合项。

用于捕获和存储位图或图像位图的状态

/**
 * 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()
}

图像包含错误或成功的结果,具体取决于过程结果

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 及更高版本的设备需要像素复制。您也可以在没有可组合对象的情况下使用这些函数

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()。它返回一个图像位图

屏幕截图比较器中的用法示例:https://github.com/android/compose-samples/blob/e6994123804b976083fa937d3f5bf926da4facc5/Rally/app/src/androidTest/java/com/example/compose/rally/ScreenshotComparator.kt