g

    Guy Bieber

    2 years ago
    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?
    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]

    2 years ago
    @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

    2 years ago
    PixelCopy
    does not need a
    View
    Do you use a
    View
    just to get the coordinates of part of the window?
    f

    Filip Pavlis

    2 years ago
    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.
    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

    2 years ago
    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

    2 years ago
    Where is findRoot? I did not see it in the docs.
    m

    Mantas Varnagiris

    2 years ago
    @Mark Murphy any hints on how to do that?
    m

    Mark Murphy

    2 years ago
    @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:
    <?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
    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)
        }
      }
    }
    m

    Mantas Varnagiris

    2 years ago
    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

    2 years ago
    do you have an example of a failing scenario?
    m

    Mantas Varnagiris

    2 years ago
    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:
    /**
     * 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

    2 years ago
    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

    2 years ago
    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
    Piotr Prus

    Piotr Prus

    1 year ago
    @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

    1 year ago
    No in the end I gave up.