https://kotlinlang.org logo
#compose
Title
# compose
g

Guy Bieber

06/24/2020, 11:33 PM
I am trying to use pixelcopy to capture the screen in a compose application, however, it asks for a view (which is kind of hidden in compose). What is the relationship between compose and views? How do I get the view currently being rendered by compose?
Copy code
fun getScreenShotFromView(view: View, activity: Activity, callback: (Bitmap) -> Unit) {
    activity.window?.let { window ->
        val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
        val locationOfViewInWindow = IntArray(2)
        view.getLocationInWindow(locationOfViewInWindow)
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                PixelCopy.request(
                    window,
                    Rect(
                        locationOfViewInWindow[0],
                        locationOfViewInWindow[1],
                        locationOfViewInWindow[0] + view.width,
                        locationOfViewInWindow[1] + view.height
                    ), bitmap, { copyResult ->
                        if (copyResult == PixelCopy.SUCCESS) {
                            callback(bitmap) }
                        else {

                        }
                        // possible to handle other result codes ...
                    },
                    Handler()
                )
            }
        } catch (e: IllegalArgumentException) {
            // PixelCopy may throw IllegalArgumentException, make sure to handle it
            e.printStackTrace()
        }
    }
}
l

Leland Richardson [G]

06/24/2020, 11:37 PM
@Filip Pavlis might have a better recommendation here so pinging him… But the only view is the root view of the compose hierarchy, so you can send that one if you want. it is created in Activity.setContent. If you look at the source of that function you should be able to see how to get it off of the window
r

romainguy

06/24/2020, 11:58 PM
PixelCopy
does not need a
View
Do you use a
View
just to get the coordinates of part of the window?
f

Filip Pavlis

06/25/2020, 9:58 AM
Are you doing this as part of tests? For such thing we have
findRoot().captureToBitmap()
which would capture your whole compose view for you. You can also capture individual composables if needed.
👍 1
In any other case you don't need the view in theory, but if you want to capture only that view's area (excluding app bars or others views) the view is used to get its coordinates in the window.
m

Mark Murphy

06/25/2020, 3:43 PM
I am trying to use pixelcopy to capture the screen in a compose application
Can you just render the composable to a bitmap-backed
Canvas
and bypass all the "capture the screen" stuff?
g

Guy Bieber

06/25/2020, 4:32 PM
Where is findRoot? I did not see it in the docs.
m

Mantas Varnagiris

07/05/2020, 6:11 PM
@Mark Murphy any hints on how to do that?
m

Mark Murphy

07/05/2020, 7:38 PM
@Mantas Varnagiris I was hoping there was a Compose-direct option for this -- in the end, everything in Compose winds up on a
Canvas
, after all. Looking at
AndroidComposeView
, though, it appears as though "render to a `Canvas`" is intertwined with lots of other stuff. ☹️ That being said, calling
draw()
on a
View
is fairly simple. Given a layout like this:
Copy code
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="<http://schemas.android.com/apk/res/android>"
  xmlns:app="<http://schemas.android.com/apk/res-auto>"
  xmlns:tools="<http://schemas.android.com/tools>"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <FrameLayout
    android:id="@+id/composeTarget"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/capturedImage"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <ImageView
    android:id="@+id/capturedImage"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/composeTarget" />
</androidx.constraintlayout.widget.ConstraintLayout>
...then this code renders a composable into the
FrameLayout
and displays a captured
Bitmap
of that output into the
ImageView
Copy code
class MainActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding = ActivityMainBinding.inflate(layoutInflater)

    setContentView(binding.root)

    binding.composeTarget.setContent(Recomposer.current()) {
      MaterialTheme {
        Text("hello, world!", modifier = Modifier.padding(8.dp))
      }
    }

    binding.composeTarget.doOnLayout {
      val bitmap = Bitmap.createBitmap(
        binding.composeTarget.width,
        binding.composeTarget.height,
        Bitmap.Config.ARGB_8888
      )
      val canvas = Canvas(bitmap)

      binding.root.draw(canvas)

      binding.capturedImage.setImageBitmap(bitmap)
    }
  }
}
👍 1
m

Mantas Varnagiris

07/06/2020, 11:07 AM
This is super helpful. Thank you! 🙏
Just FYI, trying to create a bitmap from a view that users
Modifier.drawLayer(...)
Seems like all modifications inside
drawLayer
are ignored
m

Mark Murphy

07/20/2020, 9:42 PM
do you have an example of a failing scenario?
m

Mantas Varnagiris

07/21/2020, 7:27 AM
I will create a failing scenario later today. But just reading documentation on
View.drawToBitmap
which effectively does the same thing as code above, it says:
Copy code
/**
 * Return a [Bitmap] representation of this [View].
 *
 * The resulting bitmap will be the same width and height as this view's current layout
 * dimensions. This does not take into account any transformations such as scale or translation.
 *
 * Note, this will use the software rendering pipeline to draw the view to the bitmap. This may
 * result with different drawing to what is rendered on a hardware accelerated canvas (such as
 * the device screen).
 *
 * If this view has not been laid out this method will throw a [IllegalStateException].
 *
 * @param config Bitmap config of the desired bitmap. Defaults to [Bitmap.Config.ARGB_8888].
 */
Seems like the issue is that
drawLayer
users hardware accelerated canvas that's why all transformations seem to be ignored for me. Now need to investigate if it's possible to capture pixels from hardware accelerated canvas.
f

Filip Pavlis

07/27/2020, 5:03 PM
findRoot
or now called
onRoot
is a testing API. if you don't need it for testing then my advise above does not apply (sorry for delay).
m

Mantas Varnagiris

07/27/2020, 5:09 PM
No, it's not for testing. I'll have to find another way. I could not find a way to capture bitmap from hardware accelerated layer
p

Piotr Prus

03/05/2021, 12:38 PM
@Mantas Varnagiris Did you get it working? I would like to make a screenshot of composable and send it as a bitmap. On running app, not for testing
m

Mantas Varnagiris

03/08/2021, 8:28 AM
No in the end I gave up.
👍 1
230 Views