saket / telephoto

Building blocks for designing media experiences in Compose UI
https://saket.github.io/telephoto/
Apache License 2.0
869 stars 28 forks source link

Unable to capture bitmap from ZoomableAsyncImage composable #64

Closed MitulVarmora closed 2 months ago

MitulVarmora commented 4 months ago

Android official guide to capture bitmap from composable is here

Using that official way, capturing bitmap from ZoomableAsyncImage or it's parent composable is not working.

My main goal is to capture bitmap which is visible to the user (Area which is zoomed out of screen should not be the part of it) If there are other ways to crop the image by zoomed size and offsets, please let me know.

Code snippet 1 throws error java.lang.IllegalStateException: Picture already recording, must call #endRecording():

val picture = remember { Picture() }
ZoomableAsyncImage(
  modifier = Modifier
      .fillMaxSize()
      .background(MaterialTheme.colorScheme.secondaryContainer)
      .drawWithCache {
          val width = this.size.width.toInt()
          val height = this.size.height.toInt()
          onDrawWithContent {
              val pictureCanvas = Canvas(picture.beginRecording(width, height))

              draw(this, this.layoutDirection, pictureCanvas, this.size) {
                  this@onDrawWithContent.drawContent()
              }

              picture.endRecording()

              drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
          }
      },
  model = ImageRequest.Builder(LocalContext.current)
      .data(uriOrBitmap)
      .crossfade(true)
      .build(),
  contentScale = ContentScale.Crop,
  contentDescription = null,
)

Code snippet 2, ZoomableAsyncImage doesn't display image (Just shifted recording work to the parent composable):

val picture = remember { Picture() }
Column(
    modifier = Modifier
        .fillMaxSize()
        .background(MaterialTheme.colorScheme.secondaryContainer)
        .drawWithCache {
            val width = this.size.width.toInt()
            val height = this.size.height.toInt()
            onDrawWithContent {
                val pictureCanvas = Canvas(picture.beginRecording(width, height))

                draw(this, this.layoutDirection, pictureCanvas, this.size) {
                    this@onDrawWithContent.drawContent()
                }

                picture.endRecording()

                drawIntoCanvas { canvas -> canvas.nativeCanvas.drawPicture(picture) }
            }
        }
) {
    ZoomableAsyncImage(
        modifier = Modifier
            .fillMaxSize(),
        model = ImageRequest.Builder(LocalContext.current)
            .data(uriOrBitmap)
            .crossfade(true)
            .build(),
        contentScale = ContentScale.Crop,
        contentDescription = null,
    )
}

Above issue was observed on lib version 0.7.1

On lib version 0.8.0 the observation is different:

saket commented 4 months ago

Can you share the full stacktrace? And just to confirm, does this work with AsyncImage?

MitulVarmora commented 4 months ago

Can you share the full stacktrace? And just to confirm, does this work with AsyncImage?

With AsyncImage and SubcomposeAsyncImage, It works fine when data is a Uri but not when data is bitmap (when bitmap, it neither show image nor throw errors).

Full stack trace of code snippet 1:

java.lang.IllegalStateException: Picture already recording, must call #endRecording()
    at android.graphics.Picture.beginRecording(Picture.java:110)
        ... my package and classes lines which I can not share here.
    at androidx.compose.ui.draw.CacheDrawModifierNodeImpl.draw(DrawModifier.kt:221)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105)
    at androidx.compose.ui.node.LayoutNodeDrawScope.performDraw(LayoutNodeDrawScope.kt:76)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:55)
    at androidx.compose.foundation.BackgroundNode.draw(Background.kt:159)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105)
    at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:364)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:66)
        ... my package and classes lines which I can not share here.
    at androidx.compose.ui.draw.CacheDrawModifierNodeImpl.draw(DrawModifier.kt:221)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105)
    at androidx.compose.ui.node.LayoutNodeDrawScope.performDraw(LayoutNodeDrawScope.kt:76)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:55)
    at androidx.compose.foundation.BackgroundNode.draw(Background.kt:159)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105)
    at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:364)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.access$drawContainedDrawModifiers(NodeCoordinator.kt:54)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:383)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:382)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2303)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:500)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:256)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:382)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:380)
    at androidx.compose.ui.platform.RenderNodeLayer.drawLayer(RenderNodeLayer.android.kt:311)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:348)
    at androidx.compose.ui.node.LayoutModifierNodeCoordinator.performDraw(LayoutModifierNodeCoordinator.kt:176)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawContent(LayoutNodeDrawScope.kt:66)
    at androidx.compose.foundation.BackgroundNode.draw(Background.kt:159)
    at androidx.compose.ui.node.LayoutNodeDrawScope.drawDirect-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:105)
    at androidx.compose.ui.node.LayoutNodeDrawScope.draw-x_KDEd0$ui_release(LayoutNodeDrawScope.kt:86)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:364)
    at androidx.compose.ui.node.NodeCoordinator.access$drawContainedDrawModifiers(NodeCoordinator.kt:54)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:383)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1$1.invoke(NodeCoordinator.kt:382)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:2303)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver$ObservedScopeMap.observe(SnapshotStateObserver.kt:500)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:256)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:133)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:382)
    at androidx.compose.ui.node.NodeCoordinator$drawBlock$1.invoke(NodeCoordinator.kt:380)
    at androidx.compose.ui.platform.RenderNodeLayer.drawLayer(RenderNodeLayer.android.kt:311)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:348)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.node.InnerNodeCoordinator.performDraw(InnerNodeCoordinator.kt:174)
    at androidx.compose.ui.node.NodeCoordinator.drawContainedDrawModifiers(NodeCoordinator.kt:361)
    at androidx.compose.ui.node.NodeCoordinator.draw(NodeCoordinator.kt:353)
    at androidx.compose.ui.node.LayoutNode.draw$ui_release(LayoutNode.kt:926)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:1231)
    at android.view.View.draw_Original(View.java:23198)
    at android.view.View_Delegate.draw(View_Delegate.java:56)
    at android.view.View.draw(View.java:23168)
    at android.view.View.draw_Original(View.java:23068)
    at android.view.View_Delegate.draw(View_Delegate.java:68)
    at android.view.View.draw(View.java:22846)
    at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
    at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
    at android.view.View.draw_Original(View.java:23066)
    at android.view.View_Delegate.draw(View_Delegate.java:68)
    at android.view.View.draw(View.java:22846)
    at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
    at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
    at androidx.compose.ui.tooling.ComposeViewAdapter.dispatchDraw(ComposeViewAdapter.android.kt:397)
    at android.view.View.draw_Original(View.java:23066)
    at android.view.View_Delegate.draw(View_Delegate.java:68)
    at android.view.View.draw(View.java:22846)
    at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
    at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
    at android.view.View.draw_Original(View.java:23066)
    at android.view.View_Delegate.draw(View_Delegate.java:68)
    at android.view.View.draw(View.java:22846)
    at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
    at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
    at android.view.View.draw_Original(View.java:23066)
    at android.view.View_Delegate.draw(View_Delegate.java:68)
    at android.view.View.draw(View.java:22846)
    at android.view.ViewGroup.drawChild(ViewGroup.java:4529)
    at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4290)
    at android.view.View.draw_Original(View.java:23198)
    at android.view.View_Delegate.draw(View_Delegate.java:56)
    at android.view.View.draw(View.java:23168)
    at com.android.layoutlib.bridge.impl.RenderSessionImpl.renderAndBuildResult(RenderSessionImpl.java:581)
    at com.android.layoutlib.bridge.impl.RenderSessionImpl.render(RenderSessionImpl.java:468)
    at com.android.layoutlib.bridge.BridgeRenderSession.render(BridgeRenderSession.java:120)
    at com.android.ide.common.rendering.api.RenderSession.render(RenderSession.java:135)
    at com.android.tools.rendering.RenderTask.lambda$renderInner$14(RenderTask.java:1070)
    at com.android.tools.rendering.RenderExecutor$runAsyncActionWithTimeout$3.run(RenderExecutor.kt:202)
    at com.android.tools.rendering.RenderExecutor$PriorityRunnable.run(RenderExecutor.kt:316)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.base/java.lang.Thread.run(Unknown Source)
MitulVarmora commented 4 months ago

@saket Is there any way to change zoom and offset programatically?

saket commented 4 months ago

There isn't one yet. Please file a new issue with your usecase?

saket commented 4 months ago

@MitulVarmora I wasn't able to reproduce the above crash on an API 31 emulator. I would double check your code to verify it isn't being affected by something else. Perhaps try out a third party library?

That said, I don't think your code is compatible with telephoto because it's only recording the canvas once and ignoring all subsequent updates. ~This may also be the reason why it's not drawing anything with AsyncImage because the image is loaded asynchronously by coil.~

MitulVarmora commented 4 months ago

@saket I am not using those third party libraries. I have also updated issue description, I now mentioned versions as well. (Please check last 3 lines of issue description)

saket commented 2 months ago

@MitulVarmora have you had a chance to try out the new APIs for capturing bitmaps?

https://developer.android.com/develop/ui/compose/graphics/draw/modifiers#composable-to-bitmap

MitulVarmora commented 2 months ago

@MitulVarmora have you had a chance to try out the new APIs for capturing bitmaps?

https://developer.android.com/develop/ui/compose/graphics/draw/modifiers#composable-to-bitmap

Yes, I tried new approach, It works fine.

saket commented 2 months ago

Awesome. I'll add this to telephoto's recipes webpage.