我想截取喷气背包撰写上特定可组合功能的屏幕截图。我该怎么做?拜托,有人帮助我。我想截取可组合功能的屏幕截图并与其他应用程序共享。
我的函数示例:
@Composable
fun PhotoCard() {
Stack() {
Image(imageResource(id = R.drawable.background))
Text(text = "Example")
}
}
如何截取此功能的屏幕截图?
正如@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