Jetpack Composable πŸš€ to Bitmap Image πŸŒ†

jetpackcompose Oct 24, 2021

How to convert a jetpack composable to the bitmap image by not going insane jumping between the interpolation.



Mobile App Circular

Jetpack Composable πŸš€ to Bitmap Image πŸŒ†
How to convert a jetpack composable to the bitmap image by not going insane jumping between the interpolation. Hacktoberfest 2021 is live and many developers like me who are enthusiastic about this…

Hacktoberfest 2021 is live and many developers like me who are enthusiastic about this event and love Open Source take some time to contribute to some Open Source Projects.

My 3rd PR is on Shreyas Patil’s NotyKT repository Issue number 119 :

[JetpackCompose] Add support to share note as Image Β· Issue #119 Β· PatilShreyas/NotyKT
In the notes details screen, share as Image option should be provided. Question: How to convert Composables into Bitmap?

That is to add support to share composable into Bitmap, so the app could share Notes in Image format as well.

The solution for this problem is already there on StackOverflow and Medium. Quick google with get you those, I also went over them to understand how to provide this feature, but there was a lot of complexity and Manual hardCoded height, width, Canvas hacks, and Ugly ass API design for Creating View to capture bitmap was there.

Providing this Complexity but a huge no-no for me but the way they solve seems to be the solution for now. Mostly solution required you to convert Composable β†’ View, View β†’ Bitmap, and there was a lot of passing and multiple composable calling in the solution.

For which I decided to clean up and use my small brain 🧠 to reduce those repeated work and clean up the API design.

How to Convert Composable β†’ View??

Simple we can use ComposeView which would accept composable as its View.

// Native View which accepts Composable as its View Content
ComposeView(context).apply {
    setContent {
        // composable goes here!
    }
}

We can Create Compose View Item at call site programatically. Since our app would be in Compose It would be great if we could use this inside of Composable.

How to Covert Native View β†’ Composable ??

We can use AndroidView to wrap our Compose View then externally pass it the composable context to apply on ComposeView.

@Composable
fun CaptureBitmap(...){
  
  // we will use compose view as target to get bitmap
  val composeView = ComposeView(...)
  
  // put compose native view to compose ui using AndroidView
  AndroidView(
    factory = {
      composeView.apply {
        setContent {
          .... put compose here 
        }
      }
    }
  )
}



We will get the compose content body from outside so it could be re-used as a scaffold for all the places that need to be screenshot.

How to create Scaffolding Composables for Template Like Reusing?

@Composable
fun CaptureBitmap(
  content: @Composable ()->Unit  // <--- we use callback as parameter
){
  
  val composeView = ComposeView(...)
  
  AndroidView(
    factory = {
      composeView.apply {
        setContent {
          content.invoke() // <--- Content get places in between this code
        }
      }
    }
  )
}

We have created the logic to get our composable content wrapped inside of a native view which we can use to get bitmap, BUT : this capturing can’t be done instantly cause when this code will run it will take some time to inflate this view.

We can have multiple ways to solve this waiting time, from observing view tree to writing a state which callbacks when view is rendered. This will make to capture your view bitmap only once the layout is over.

But what might happen that you composable UI might get updated many time cause of from state changes. This will ask you to recapture bitmap every time the UI changes and cache it to get the latest value. This flow is very complex to deal with and code.

Ideally, we should be able to get bitmap out for some sharing action is getting performed on the UI so we can get the latest value from the state itself.

I solved this by using a callback. Ideally composable function holding UI thus should not return a value but this is an exceptional case I guess where we need action-based returned value.

How to create a callback to get latest bitmap?

@Composable
fun CaptureBitmap(
  content: @Composable ()->Unit
) : () -> Bitmap // <---- this will return a callback which returns a bitmap
{
  
  val composeView = ComposeView(...)
  
  //callback that convert view to bitmap
  fun captureBitmap() = composeView.drawToBitmap() 
  
  AndroidView(
    factory = {
      composeView.apply {
        setContent {
          content.invoke() // <--- Content get places in between this code
        }
      }
    }
  )
  
  return ::captureBitmap // <--- return functional reference of the callback
}

captureBitmap() will be returned from function which would be a callback that would trigger ComposeView to return a bitmap of the View.

Final Composable Would look like?

@Composable
fun CaptureBitmap(
    content: @Composable () -> Unit,
): () -> Bitmap {

    val context = LocalContext.current
    
    /** 
     * ComposeView that would take composable as its content
     * Kept in remember so recomposition doesn't re-initialize it
     **/
    val composeView = remember { ComposeView(context) }
    
    /**
     * Callback function which could get latest image bitmap
     **/
    fun captureBitmap(): Bitmap {
        return composeView.drawToBitmap()
    }
    
    /** Use Native View inside Composable **/
    AndroidView(
        factory = {
            composeView.apply {
                setContent {
                    content.invoke()
                }
            }
        }
    )

    /** returning callback to bitmap **/
    return ::captureBitmap
}



How to use composable at the call-site?

val snapShot = CaptureBitmap {
    //.. wite composable you want to capture
    // it would be visible on view as well
}

// Caution : needs to be done on click action
// ui must be visible/laid out before calling this
val bitmap = snapShot.invoke()

when snapShot.invoke() it called, it will trigger the callback and would get the latest bitmap value. Check out examples to understand the use case.

Example :

Coloumn {
    val snapShot = CaptureBitmap {
        Button(
        onClick = {},
        modifier = Modifier
            .fillMaxWidth()
            .padding(24.dp)
            .height(55.dp),
        ) {
            Text(text = "Capture my image")
        }
    }

    Button(
        onClick = {
            MainScope().launch {
                val bitmap = snapShot.invoke()
                val uri = saveImage(bitmap, context)
                if (uri != null) {
                    shareImageUri(context, uri)
                }
            }
        },
        modifier = Modifier
            .fillMaxWidth()
            .padding(24.dp)
            .height(55.dp),
        ) {
            Text(text = "Share")
        }
}

Checkout full Implementation at :

πŸ–ΌοΈ ISSUE-119 Share Note as image by ch8n Β· Pull Request #269 Β· PatilShreyas/NotyKT
Summary Added share Note as Image feature #119 Description for the changelog added Share Drop Down Modified Share Action behavior Added CaptureBitmap Composable CaptureBitmap returns a callback to...

If you guys have suggestions that could help me implement a better API I’m all ears reach me out on my social media.

If you like my content and want to see more, click right here to see more πŸ‘‡

About πŸ‘¨πŸ»β€πŸ’»
@ch8nLinktree. Make your link do more.Linktree [https://linktr.ee/ch8n]Hey bud πŸ™Œ, Welcome to my personal blogging site! This is a place where I keep all of my tips and tricks regarding Android Development , Kotlin and Java, which I have gathered from my experience and various sources. So this is …

Until next time, Happy Hacking! πŸ‘¨πŸ»β€πŸ’»


Enjoy the article?

a clap is much appreciated if you enjoyed. No sign up or cost associated :)




Chetan gupta

Hi there! call me Ch8n, I'm a mobile technology enthusiast! love Android #kotlinAlltheWay, CodingMantra: #cleanCoder #TDD #SOLID #designpatterns

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.