Giter Site home page Giter Site logo

patilshreyas / capturable Goto Github PK

View Code? Open in Web Editor NEW
926.0 4.0 30.0 2.55 MB

๐Ÿš€Jetpack Compose utility library for capturing Composable content and transforming it into Bitmap Image๐Ÿ–ผ๏ธ

Home Page: https://patilshreyas.github.io/Capturable/

License: MIT License

Kotlin 100.00%
android jetpack-compose android-app kotlin bitmap screenshot jetpack-android composable photos image

capturable's Introduction

Hi there๐Ÿ‘‹! I'm Shreyas ๐Ÿ™‹โ€โ™‚๏ธ

LATEST UPDATE: Exploring Android Jetpack Compose and Backend stuff with Kotlin ๐Ÿฅฝ.

๐ŸŽ Welcome to my hub ๐Ÿ‘จโ€๐Ÿ’ป

  • ๐Ÿ‘ฆ Google Developer Expert @Android
  • ๐Ÿ’ผ Sr. Android Developer @ Paytm.
  • ๐Ÿ‘จโ€๐Ÿ’ป #SelfTaught Developer.
  • ๐Ÿ‘จโ€๐Ÿ’ป I develop Mobile, Web apps and can also develop Backend server for apps.
  • โœ๏ธ Write blogs on blog.shreyaspatil.dev.
  • Loves ๐ŸŽต and ๐ŸŽน.
  • Proud ๐Ÿ‡ฎ๐Ÿ‡ณ.

๐Ÿ“Š Github Stats

Shreyas Patil | Stats


๐Ÿ”— Know more about me

Portfolio Mail Twitter Linkedin Medium Google Play Instagram

capturable's People

Contributors

dependabot[bot] avatar patilshreyas avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

capturable's Issues

Stateful UI content inside `capturable()` Modifier not updating on UI

The content wrapped with capturable() modifier is not updating.

How to produce?

In MainActivity.kt file, I added a counter in BookingDetail composable:

@Composable
fun BookingDetail() {
    var time by remember { mutableStateOf(Date()) }

    fun formatDateTo12HourClock(date: Date): String {
        val sdf = SimpleDateFormat("hh:mm:ss a", Locale.getDefault())
        return sdf.format(date)
    }

    LaunchedEffect(Unit) {
        while (true) {
            delay(1000)

            time = Date()
        }
    }

    Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            Text("Sat, 1 Jan", style = MaterialTheme.typography.caption)
            Text(formatDateTo12HourClock(time), style = MaterialTheme.typography.subtitle2)
        }

        Column {
            Text("SCREEN", style = MaterialTheme.typography.caption)
            Text("JET 01", style = MaterialTheme.typography.subtitle2)
        }

        Column {
            Text("SEATS", style = MaterialTheme.typography.caption)
            Text("J1, J2, J3", style = MaterialTheme.typography.subtitle2)
        }
    }
}

Now run the app. The content will not change with the time variable.

Then comment out the .capturable(captureController) part, and run again. You will see the value is updating correctly.

How to solve?

If I directly try this then everything works. So maybe DelegatingNode does not update the content for some reason.

Can't get the Bitmap when Capturable includes Network image

Hello!! When I press the button where I have the controller.capture function, I get the same error all the time, at first I thought I had something wrong configured, I cloned the repository to see the example, and I had it similar, but the example does not give me the same error as me

Error obtained

java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps

Capturable

                        Capturable(
                            modifier = Modifier.constrainAs(ivLetterImage) {
                                linkTo(parent.start, parent.end)
                                linkTo(parent.top, tvAddressee.top)
                                width = Dimension.fillToConstraints
                            },
                            controller = controller,
                            onCaptured = { bitmap, error ->
                                //ticketBitmap = bitmap
                                error
                                context.share(
                                    nameOfImage = letters,
                                    message = "",
                                    bitmap = bitmap?.asAndroidBitmap().orEmpty()
                                )
                            }
                        ) {
                            LetterImage(
                                addressee = addressee.value.text,
                                message = message.value.text,
                                sender = sender.value.text,
                                letterImage = letterImage
                            )
                        }

LetterImage Composable

@ExperimentalFoundationApi
@Composable
fun LetterImage(
    modifier: Modifier = Modifier,
    addressee: String,
    message: String,
    sender: String,
    letterImage: String
) {

    ConstraintLayout(
        modifier = modifier
    ) {

        val (
            ivLetter,
            tvAddressee,
            tvMessage,
            tvSender
        ) = createRefs()

        NetworkImage(
            modifier = Modifier
                .wrapContentHeight()
                .fillMaxWidth()
                .constrainAs(ivLetter) {
                    linkTo(parent.start, parent.end)
                    linkTo(parent.top, parent.bottom)
                },
            contentScale = ContentScale.Crop,
            url = letterImage,
            builder = {
                crossfade(true)
            }
        )

        Text(
            text = addressee,
            maxLines = 1,
            color = lightGreyPastel,
            fontWeight = FontWeight.Bold,
            style = TypographyNook.h6,
            modifier = Modifier
                .padding(top = 30.dp, start = 40.dp)
                .constrainAs(tvAddressee) {
                    start.linkTo(parent.start)
                    top.linkTo(ivLetter.top)
                }
        )

        Text(
            text = message,
            maxLines = 7,
            color = lightGreyPastel,
            fontWeight = FontWeight.Bold,
            style = TypographyNook.h6,
            modifier = Modifier
                .padding(top = 20.dp, start = 60.dp, end = 60.dp)
                .constrainAs(tvMessage) {
                    linkTo(parent.start, parent.end)
                    top.linkTo(tvAddressee.bottom)
                    bottom.linkTo(tvSender.top)
                }
        )

        Text(
            text = sender, //16,
            maxLines = 1,
            color = lightGreyPastel,
            fontWeight = FontWeight.Bold,
            style = TypographyNook.h6,
            modifier = Modifier
                .padding(top = 20.dp, bottom = 30.dp, end = 40.dp)
                .constrainAs(tvSender) {
                    end.linkTo(parent.end)
                    linkTo(tvMessage.bottom, parent.bottom)
                }
        )

    }

}

Scrolling capturing not possible?

Shreyas, is it possible to capture entire lazyColum/colum of wrapping content (scrolling) in a bitmap instead of just capturing what is on the screen?

When will the new version be released ?

It's a long time from the time the latest version 1.0.3 was released. New pull requests were merged but no release was created. Please release new version because we need it for new Compose APIs. Thank you.

Problems with rounded corners when generating bitmaps

Inside the example in the repository, I have tried to see the things that can be done and I have seen that once inside the card (in this case the example), everything you put with a RoundedCornerShape, does not reproduce it, but it is shown as a rectangle with square corners

To reproduce the problem, follow these steps
In the example Code

  1. Go to BookingConfirmedContent Composable function
  2. Add inside the Modifier of the Text containing "Booking Confirmed" the following: .clip(RoundedCornerShape(16.dp)).background(Color.Red)
  3. Compile and see that the text "Booking Confirmed" has a red background with rounded corners
  4. Click on the "Preview Ticket Image" button and you will see that the red background with rounded corners has changed to a red background with square corners.

Captures only the visible part in screen

I am using LazyColumn, where I have several items.

Each Item have a button to capture that specific item.

If I click the button on a item, which is partially visible on the screen, it's only capturing the visible part.

Is there a way, where I can capture the complete item, even if it's partially visible.

Expose public constructor of `CaptureController`

If for certain use cases, instance of CaptureController needs to be created globally, like follows:

class MyComposeActivity : ComponentActivity {
  val controller = CaptureController()

  override fun onCreate(savedInstanceState: Bundle?) {
    setContent {
      // do something with `controller` in Compose scope
    }
  }
}

Currently, only rememberCaptureController() function is exposed which can be only called from @Composable function.

PixelCopy is taking screenshots of layers outside of the composable

I was studying how the inners of this library and discovered there's a slight problem with the drawBitmapWithPixelCopy() method which is used to capture the View into bitmap.

You can test this by modifying the library to force use drawBitmapWithPixelCopy() over view.drawToBitmap().

  1. Add throw java.lang.IllegalArgumentException("force throw") before this line and use any SDK Android Oreo or above.

  2. Copy and use the composable below.

@Composable
fun Main() {
    Box {
        val captureController = rememberCaptureController()
        val context = LocalContext.current

        Capturable(controller = captureController, onCaptured = { bitmap, error ->
            bitmap?.let {
                File(context.filesDir, "screenshot.png")
                    .writeBitmap(bitmap.asAndroidBitmap(), Bitmap.CompressFormat.PNG, 85)
            }
            Log.i("capturable", context.filesDir.toString())

            if (error != null) {
                Log.e("ERROR!", error.toString())
            }
        }) {
            Image(painter = painterResource(id = R.drawable.poster),
                contentDescription = null,
                modifier = Modifier.fillMaxWidth(),
                contentScale = ContentScale.Crop)
        }


        Button(onClick = {
            captureController.capture()
        }) {
            Text("Take a screenshot!!!!")
        }

        Text(text = "You shouldn't see me", fontSize = 140.sp, color= Color.White, modifier = Modifier.padding(15.dp))
    }
}
  1. Hit the screen button.
  2. Locate the screenshot.
  3. You will see that Capturable took a screenshot which contains the forbidden text, even though the text is not within the Capturable composable.

image

Any help is very much appreciated by our team!

Problem trying to capture scrollable Column content

I know this has been reported before, but I was confused by some comments that it works, as in my tests, it didn't. I'm trying to capture content from a scrollable Column, but only the visible part is captured.

The only workaround I have in mind and has worked for me is to wrap the Composable in a ScrollView with XML and then capture the ScrollView's contents, as in the old days. But for that I'd have to use Fragments and XMLs, and it's definitely not something anyone would want with Compose.

Anyway, thanks for the nice work on the library.

Code used: https://pastebin.com/yQF7NhgF

Screenshots:

screenshot_screen
capture

Consider an alternate capture result syntax

The v2 syntax for capturing uses Kotlin's Deferred interface to return the captured ImageBitmap. While this allows for async execution, I think it has a major flaw, and that is its ability to properly catch errors. Exceptions in Kotlin aren't exactly type-checked, meaning that it's very easy to forget the try-catch clause. Hence, my first proposal - custom return value based on a sealed interface:

sealed interface CaptureResult {

    @JvmInline
    value class Success(val bitmap: ImageBitmap) : CaptureResult

    @JvmInline
    value class Error(val error: Throwable) : CaptureResult
}

The idea is simple, if the operation was successful, return CaptureResult.Success, else return CaptureResult.Error.

Next, I'd like to discuss the actual capture syntax. I have 3 proposals, each with their pros and cons:

Option 1

class CaptureController internal constructor(internal val onCapture: (CaptureResult.Success) -> Unit) {
    private val _captureRequests = MutableSharedFlow<CaptureRequest>(extraBufferCapacity = 1)
    internal val captureRequests = _captureRequests.asSharedFlow()

    fun capture(config: Bitmap.Config = Bitmap.Config.ARGB_8888) {
        _captureRequests.tryEmit(CaptureRequest(config, onCapture))
    }

    internal class CaptureRequest(
        val config: Bitmap.Config
    )
}

@Composable
fun rememberCaptureController(onCapture: (CaptureResult.Success)): CaptureController {
    return remember(onCapture) { CaptureController(onCapture) }
}

private class CapturableModifierNode(
    var controller: CaptureController
) : DelegatingNode(), DelegatableNode {

    ...

    override fun onAttach() {
        super.onAttach()
        coroutineScope.launch {
            controller.captureRequests.collect { request ->
                try {
                    val bitmap = withContext(Dispatchers.Default) {
                        picture.asBitmap(request.config)
                    }
                    controller.onCapture(CaptureResult.Success(bitmap.asImageBitmap()))
                } catch (error: Throwable) {
                    controller.onCapture(CaptureResult.Error(error))
                }
            }
        }
    }
}

@Composable
fun TestScreen() {
	val controller = rememberCapturableController { result -> 
		when (result) {
            is CaptureResult.Success -> { /*TODO*/ }
            is CaptureResult.Error -> { /*TODO*/ }
        }
	}
	ShouldBeCaptured(modifier = Modifier.capturable(controller))
	Button(onClick = { controller.capture() }) {
		Text("Capture")
	}
}
โœ… Pros โŒ Cons
Easy to assign actions to captures Hard to migrate from the Capturable composable, as the library wouldn't be able to migrate onCaptured: (ImageBitmap?, Throwable?) -> Unit to the new controller syntax.
Elegant syntax

Option 2

class CaptureController internal constructor() {
    private val _captureRequests = MutableSharedFlow<CaptureRequest>(extraBufferCapacity = 1)
    internal val captureRequests = _captureRequests.asSharedFlow()

    fun capture(
        config: Bitmap.Config = Bitmap.Config.ARGB_8888,
        onCapture: (CaptureResult) -> Unit
    ) {
        _captureRequests.tryEmit(CaptureRequest(config, onCapture))
    }

    suspend fun capture(config: Bitmap.Config = Bitmap.Config.ARGB_8888): CaptureResult {
        return suspendCoroutine { continuation ->
            val request = CaptureRequest(config) {
                continuation.resume(it)
            }
            _captureRequests.tryEmit(request)
        }
    }

    internal class CaptureRequest(
        val config: Bitmap.Config,
        val onCapture: (CaptureResult) -> Unit
    )
}

private class CapturableModifierNode(
    var controller: CaptureController
) : DelegatingNode(), DelegatableNode {

    ...

    override fun onAttach() {
        super.onAttach()
        coroutineScope.launch {
            controller.captureRequests.collect { request ->
                try {
                    val bitmap = withContext(Dispatchers.Default) {
                        picture.asBitmap(request.config)
                    }
                    request.onCapture(CaptureResult.Success(bitmap.asImageBitmap()))
                } catch (error: Throwable) {
                    request.onCapture(CaptureResult.Error(error))
                }
            }
        }
    }
}

@Composable
fun TestScreen() {
	val coroutineScope = rememberCoroutineScope()
	val controller = rememberCapturableController()
	ShouldBeCaptured(modifier = Modifier.capturable(controller))
	Button(onClick = { 
		controller.capture { result ->
			when (result) {
                is CaptureResult.Success -> { /*TODO*/ }
                is CaptureResult.Error -> { /*TODO*/ }
             }
		}
	 }) {
		Text("Capture Callback")
	}
	Button(onClick = { 
		coroutineScope.launch {
			val result = controller.capture()
			when (result) {
                is CaptureResult.Success -> { /*TODO*/ }
                is CaptureResult.Error -> { /*TODO*/ }
             }
		}
	 }) {
		Text("Capture Suspending")
	}
}
โœ… Pros โŒ Cons
Allows for different logic per different capture Hard to migrate from the Capturable composable, as the library wouldn't be able to migrate onCaptured: (ImageBitmap?, Throwable?) -> Unit to the new controller syntax.
Allows for both suspendable and callback-based methods Can get repetitive if you have multiple capture handlers

Option 3

class CaptureController internal constructor() {
    private val _captureRequests = MutableSharedFlow<CaptureRequest>(extraBufferCapacity = 1)
    internal val captureRequests = _captureRequests.asSharedFlow()

    fun capture(config: Bitmap.Config = Bitmap.Config.ARGB_8888) {
        _captureRequests.tryEmit(CaptureRequest(config, onCapture))
    }

    internal class CaptureRequest(
        val config: Bitmap.Config
    )
}

fun Modifier.capturable(
	controller: CaptureController,
	onCapture: (CaptureResult) -> Unit
): Modifier {
    return this then CapturableModifierNodeElement(controller, onCapture)
}

private data class CapturableModifierNodeElement(
    private val controller: CaptureController,
	private val onCapture: (CaptureResult) -> Unit
) : ModifierNodeElement<CapturableModifierNode>() {
    override fun create(): CapturableModifierNode {
        return CapturableModifierNode(controller, onCapture)
    }

    override fun update(node: CapturableModifierNode) {
        node.controller = controller
		node.onCapture = onCapture
    }
}

private class CapturableModifierNode(
    var controller: CaptureController,
 	var onCapture: (CaptureResult) -> Unit
) : DelegatingNode(), DelegatableNode {

    ...

    override fun onAttach() {
        super.onAttach()
        coroutineScope.launch {
            controller.captureRequests.collect { request ->
                try {
                    val bitmap = withContext(Dispatchers.Default) {
                        picture.asBitmap(request.config)
                    }
                    onCapture(CaptureResult.Success(bitmap.asImageBitmap()))
                } catch (error: Throwable) {
                    onCapture(CaptureResult.Error(error))
                }
            }
        }
    }
}

@Composable
fun TestScreen() {
	val coroutineScope = rememberCoroutineScope()
	val controller = rememberCapturableController()
	ShouldBeCaptured(
		modifier = Modifier.capturable(
			controller = controller,
			onCapture = { result -> 
				when (result) {
					is CaptureResult.Success -> { /*TODO*/ }
				    is CaptureResult.Error -> { /*TODO*/ }
              	}
			}
		)
	)
	Button(onClick = { controller.capture() }) {
		Text("Capture")
	}
}
โœ… Pros โŒ Cons
Matches the style of other foundation modifiers, such as `.clickable() Probably not as intuitive as previous 2
Allows for easy migration from Capturable, a onCapture(CaptureResult) from the modifier can invoke onCaptured: (ImageBitmap?, Throwable?) -> Unit

Personally, I like the 3rd approach the most, as it minimizes the migration hassle and comes in-line with the foundation APIs. Also, Kotlin's Result class would fit as a return type too, I just think it's a bit of a heavy class for tasks as simple as this. Please, let me know what you think!

It captures TextField's cursor too.

I am taking a capture with my custom TextField.

And it captures the cursor too.

And if I print it in the receipt printer(Only black&white), the transparent color look black. How can I solve this ?

Mismatch in catches of comparables

Hello again!

I've been doing some tests, and I've seen a couple of things that I don't know if this is really the case or if they are errors. I'll tell you about them so that you can clarify them for me.

First case: Composable capture in LazyColumn

I have a LazyColumn that has several items both above and below Capturable. I have in one of them a button with which I perform the controller.capture() action and at that moment I generate the bitmap to share/save it. The problem is that it does not really capture the composable that you have indicated, it obtains the size of the real composable, but if you have made scroll and that composable is between the TopAppBar for example the generated image is not really the one previously indicated.

Capture of the image generated after the scroll has taken place
Capture generated when the compostable is fully displayed on the screen


LazyColumn {

 { ... }

            item {
                Capturable(
                    controller = controller,
                    onCaptured = { bitmap, _ ->
                        if (bitmap != null)
                            with(context) {
                                saveBitmap(bitmap.asAndroidBitmap())?.let { bitmapUri ->
                                    share(
                                        message = "",
                                        uri = bitmapUri
                                    )
                                }
                            }
                    }
                ) {
                    LetterImage(
                        addressee = addressee.value.text,
                        message = message.value.text,
                        sender = sender.value.text,
                        textColor = textColor.value,
                        bgColor = bgColor.value,
                        letterImage = letterImage
                    )
                }
            }

  { ... }

}

Second case: Background colour of the capture

Even if no background colour has been specified in the composable to be captured, the background colour that is inside the view is obtained, i.e. there is no option to capture the composable with a transparent background if I want it to capture it. As you can see in the images above, the background colour I have on the screen is red, but when I capture the image of the card, it adds a red background colour even though I want it to be transparent.

Thank you in advance! And thank you for your work! ๐Ÿ˜„

View needs to be laid out before calling drawToBitmap()

Hi, rare crash, but it happened 2 times on different devices (Android 10 and 12, both Samsung devices).

Fatal Exception: java.lang.IllegalStateException
View needs to be laid out before calling drawToBitmap()
androidx.core.view.ViewKt.drawToBitmap (ViewKt.java:236)
dev.shreyaspatil.capturable.CapturableKt$drawToBitmapPostLaidOut$lambda-2$$inlined$doOnLayout$1.onLayoutChange (View.kt:415)
android.view.View.layout (View.java:23768)
android.view.ViewGroup.layout (ViewGroup.java:7277)
androidx.compose.ui.viewinterop.AndroidViewHolder.onLayout (AndroidViewHolder.android.kt:201)
android.view.View.layout (View.java:23750)
android.view.ViewGroup.layout (ViewGroup.java:7277)

It might be an issue related to some of the frameworks changes done by a vendor, but anyway, would be nice to have some workaround.

material3 card compatibility

This tool is really helpful. But I found this issue with Card() in material3. When using this tool to capture a roundedcorner Card(), the screenshot isn't rounded, left white corner there. How can I fix this? Than you.

Possibility to capture a compostable without displaying it

Hello again!

Based on the issue that I have opened that when taking a screenshot in a LazyColumn the captured image is cut, I have come up with an idea.

I don't know if it could be done but I think it is a good suggestion, and it would be the ability to generate captures of the composables without the need to show them on the screen, that way as now, when pressing a button you would simply put as now the controller.capture() and previously have configured the Capturable with the Composable that you want to capture but without the need to be displayed in the view.

Again, many thanks in advance!

.await() cannot be called inside a button onClick callback

Following the documentation:

// Example: Capture the content when button is clicked
Button(onClick = {
    // Capture content
    val bitmapAsync = captureController.captureAsync()
    try {
        val bitmap = bitmapAsync.await()
        // Do something with `bitmap`.
    } catch (error: Throwable) {
        // Error occurred, do something.
    }
}) { ... }

This is not possible as, the function await() cannot be called inside the onClick callback. What's the workaround? Should the docs be updated?

Update instance of `CaptureController` in ModifierNodeElement#update

ModifierNodeElement#update is called when a modifier is applied to a Layout whose inputs have changed from the previous application. This function will have the current node instance passed in as a parameter, and it is expected that the node will be brought up to date. So if across recompositions, if an instance of CaptureController is changed then all capture requests will be lost.

Capture scrollable content

For those who are looking for a solution to capture scrollable content, I've created a small solution that I believe with some modifications and improvements, could be a functionality of the library.

The solution is based on AndroidView with a ScrollView with Composable content. The difference is that the captured content is that of the ScrollView, with scrollView.getChildAt(0).height. Since ScrollableCapturable is scrollable by default, it is important not to use other scrollable layouts such as LazyColumn or scrollable Column.

Unfortunately, in my tests, drawToBitmapPostLaidOut() did not work as expected to resolve the problems with network image loading. Despite solving problems with "Software rendering doesn't support hardware bitmaps", the Bitmap image is distorted and the Composable content is not completely captured.
The solution I found for this, which is not really a solution, is to use a different library and test if the problem disappears. In my case, I used the landscapist library (with the glide version) instead of the coil and it worked fine.

ScrollableCapturable:
/**
 * @param controller A [CaptureController] which gives control to capture the [content].
 * @param modifier The [Modifier] to be applied to the layout.
 * @param onCaptured The callback which gives back [Bitmap] after composable is captured.
 * If any error is occurred while capturing bitmap, [Exception] is provided.
 * @param content [Composable] content to be captured.
 *
 * Note: Don't use scrollable layouts such as LazyColumn or scrollable Column. This will cause
 * an error. The content will be scrollable by default, because it's wrapped in a ScrollView.
 */
@Composable
fun ScrollableCapturable(
    modifier: Modifier = Modifier,
    controller: CaptureController,
    onCaptured: (Bitmap?, Throwable?) -> Unit,
    content: @Composable () -> Unit
) {
    AndroidView(
        factory = { context ->
            val scrollView = ScrollView(context)
            val composeView = ComposeView(context).apply {
                setContent {
                    content()
                }
            }
            scrollView.addView(composeView)
            scrollView
        },
        update = { scrollView ->
            if (controller.readyForCapture) {
                // Hide scrollbars for capture
                scrollView.isVerticalScrollBarEnabled = false
                scrollView.isHorizontalScrollBarEnabled = false
                try {
                    val bitmap = loadBitmapFromScrollView(scrollView)
                    onCaptured(bitmap, null)
                } catch (throwable: Throwable) {
                    onCaptured(null, throwable)
                }
                scrollView.isVerticalScrollBarEnabled = true
                scrollView.isHorizontalScrollBarEnabled = true
                controller.captured()
            }
        },
        modifier = modifier
    )
}

/**
 * Need to use view.getChildAt(0).height instead of just view.height,
 * so you can get all ScrollView content.
 */
private fun loadBitmapFromScrollView(scrollView: ScrollView): Bitmap {
    val bitmap = Bitmap.createBitmap(
        scrollView.width,
        scrollView.getChildAt(0).height,
        Bitmap.Config.ARGB_8888
    )
    val canvas = Canvas(bitmap)
    scrollView.draw(canvas)
    return bitmap
}

class CaptureController {
    var readyForCapture by mutableStateOf(false)
        private set

    fun capture() {
        readyForCapture = true
    }

    internal fun captured() {
        readyForCapture = false
    }
}

@Composable
fun rememberCaptureController(): CaptureController {
    return remember { CaptureController() }
}
Example of use:
@Composable
fun CapturableScreen() {
    val captureController = rememberCaptureController()

    Column(modifier = Modifier.fillMaxSize()) {
        ScrollableCapturable(
            controller = captureController,
            onCaptured = { bitmap, error ->
                bitmap?.let {
                    Log.d("Capturable", "Success in capturing.")
                }
                error?.let {
                    Log.d("Capturable", "Error: ${it.message}\n${it.stackTrace.joinToString()}")
                }
            },
            modifier = Modifier.weight(1f)
        ) {
            ScreenContent()
        }

        Button(
            onClick = { captureController.capture() },
            modifier = Modifier.align(Alignment.CenterHorizontally)
        ) {
            Text(text = "Take screenshot")
        }
    }
}

@Composable
private fun ScreenContent() {
    val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " +
            "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis" +
            " nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." +
            " Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore" +
            " eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt" +
            " in culpa qui officia deserunt mollit anim id est laborum."
    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White)
            .padding(12.dp)
    ) {
        /**
         * When using the "io.coil-kt:coil-compose" library it can cause:
         * java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps
         * You can use "com.github.skydoves:landcapist-glide" to try to solve this.
         */
        GlideImage(
            imageModel = "https://raw.githubusercontent.com/PatilShreyas/Capturable/master/art/header.png",
            modifier = Modifier
                .size(200.dp)
                .clip(RoundedCornerShape(12.dp))
        )

        Spacer(Modifier.height(10.dp))

        for (i in 0..3) {
            Box(
                modifier = Modifier
                    .size(100.dp)
                    .background(Color.Black)
            )
            Spacer(Modifier.height(4.dp))
            Text(
                text = text,
                color = Color.Black,
                fontSize = 18.sp,
            )
        }
    }
}
Screenshots:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.