【问题标题】:Jetpack Compose take screenshot of composable function?Jetpack Compose 截取可组合功能的截图?
【发布时间】:2020-12-30 19:19:52
【问题描述】:

我想截取 Jetpack Compose 上特定可组合功能的屏幕截图。我怎样才能做到这一点?请任何人帮助我。我想截取可组合功能的屏幕截图并与其他应用程序共享。

我的函数示例:

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

这个功能怎么截图?

【问题讨论】:

  • 目前,AFAIK 的 Compose 中没有直接用于此的内容。但是,您可以让AndroidComposeView 渲染您的可组合,然后将AndroidComposeView draw() 的内容添加到Bitmap 支持的Canvas。这不会涵盖所有内容,但它应该与普通视图的“屏幕截图”一样好。有关示例,请参阅 this Kotlinlang Slack post,尽管它已有几个月的历史,需要针对当前版本的 Compose 进行更新。
  • 一年后,开发团队谈到了截图(around 25:00),但它是在测试的上下文中。如果这个功能在任何地方都可用,那就太好了。或者我们可以将测试依赖添加到我们的生产应用程序中?
  • @CommonsWare 你介意发布实际的解决方案吗?只有拥有经过验证的 Jetbrains 帐户的人才能访问该 Slack 频道。
  • @Maarten:“你介意发布实际的解决方案吗?” - 那个 Slack 线程大约有一年的历史。从那时起,几乎所有关于 Compose 的东西都已经过时了。请参阅 this Medium postcorresponding GitHub repo 了解更新的内容。
  • 谢谢!这仍然将View 与可组合项交织在一起,如果能够轻松访问类似于SemanticsNodeInteraction.captureToImage 的东西会很好。我已经为它提交了功能请求:issuetracker.google.com/issues/198182887

标签: android kotlin android-jetpack-compose android-jetpack


【解决方案1】:

您可以创建一个测试,将内容设置为该可组合项,然后调用composeTestRule.captureToImage()。它返回一个ImageBitmap

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

【讨论】:

    【解决方案2】:

    您可以使用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 一起放置,它仍然会出现在图像上。

    附言有一个bug 产生Modifier.graphicsLayer 效果,offset { IntOffset(...) }(在这种情况下你仍然可以使用offset(dp)),scrollable 和惰性视图位置没有正确显示在屏幕截图上。如果您遇到过,请给问题加注星标以获得更多关注。

    【讨论】:

    • 这很好用,但是我发现的一个问题是,如果我将任何可组合项拖到新位置,此方法不会在屏幕截图中找到它,知道吗?
    • @alfietap 尝试将拖动偏移值添加到translate。如果这没有帮助,请使用 minimal reproducible example 提出单独的问题。
    • 我给你发了一个问题,谢谢!
    【解决方案3】:

    正如评论中提到的@Commonsware,并假设这不是关于截图测试:

    根据official docs,您可以使用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()
        }
    }
    

    【讨论】:

    • 这似乎不起作用,你能提供更多关于这个答案的上下文吗?
    • 当然可以,请问您在哪一部分需要帮助?请确保第一个代码在可组合函数中。
    • 此解决方案采用整个窗口屏幕截图,而不是为可组合函数 ej 呈现的区域。对具有工具栏的可组合项进行屏幕截图,您的屏幕截图中也会有工具栏。
    • @Akhha8 没错,可以通过 Modifier.onGloballyPositioned {it.positionInRoot().. } 获取当前的可组合坐标,然后用它们来裁剪位图。有关官方文档的更多信息:developer.android.com/reference/kotlin/androidx/compose/ui/… 如果您需要更多帮助,请告诉我。 :)
    【解决方案4】:

    您可以使用 @Preview 创建预览功能,在手机或模拟器上运行该功能并截取​​组件的屏幕截图。

    【讨论】:

    • OP 询问是否以编程方式截取 Compose 渲染的屏幕截图。
    【解决方案5】:

    使用 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(),
            )
        }
    }
    

    【讨论】:

      【解决方案6】:

      我正在寻找如何在 测试 中截取可组合的屏幕截图,这个问题出现在结果中的第一个问题。所以,对于未来想要在测试中截取/保存/比较屏幕截图或进行屏幕截图测试的用户,我将我的答案放在这里(感谢this)。

      确保您拥有此依赖项以及其他 Compose 依赖项:

      debugImplementation("androidx.compose.ui:ui-test-manifest:<version>")
      

      注意:你可以简单地在androidTest目录中添加一个AndroidManifest.xml文件,并在manifest中添加&lt;activity android:name="androidx.activity.ComponentActivity" /&gt;而不是上面的依赖➜ 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)
              }
          }
      }
      

      我也回答了类似的问题here

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-07-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多