GithubHelp home page GithubHelp logo

patilshreyas / capturable Goto Github PK

View Code? Open in Web Editor NEW
990.0 4.0 30.0 2.56 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 hacktoberfest

capturable's Introduction

Capturable

Capturable

🚀A Jetpack Compose utility library for converting Composable content into Bitmap image 🖼️.
Made with ❤️ for Android Developers and Composers

Build Maven Central

💡Introduction

In the previous View system, drawing Bitmap Image from View was very straightforward. But that's not the case with Jetpack Compose since it's different in many aspects from previous system. This library helps easy way to achieve the same results.

🚀 Implementation

You can check /app directory which includes example application for demonstration.

Gradle setup

In build.gradle of app module, include this dependency

dependencies {
    implementation "dev.shreyaspatil:capturable:2.1.0"
}

You can find latest version and changelogs in the releases.

Usage

1. Setup the controller

To be able to capture Composable content, you need instance of CaptureController by which you can decide when to capture the content. You can get the instance as follow.

@Composable
fun TicketScreen() {
    val captureController = rememberCaptureController()
}

rememberCaptureController() is a Composable function.

2. Add the content

The component which needs to be captured, a capturable() Modifier should be applied on that @Composable component as follows.

@Composable
fun TicketScreen() {
    val captureController = rememberCaptureController()

    // Composable content to be captured.
    // Here, everything inside below Column will be get captured
    Column(modifier = Modifier.capturable(captureController)) {
        MovieTicketContent(...)
    }
}

3. Capture the content

To capture the content, use CaptureController#captureAsync() as follows.

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

On calling this method, request for capturing the content will be sent and ImageBitmap will be returned asynchronously. This method is safe to be called from Main thread.

By default, it captures the Bitmap using Bitmap.Config ARGB_8888. If you want to modify, you can provide config from Bitmap.Config enum.

Example:

captureController.captureAsync(Bitmap.Config.ALPHA_8)

That's all needed!

📄 API Documentation

Visit the API documentation of this library to get more information in detail.


🙋‍♂️ Contribute

Read contribution guidelines for more information regarding contribution.

💬 Discuss?

Have any questions, doubts or want to present your opinions, views? You're always welcome. You can start discussions.

📝 License

MIT License

Copyright (c) 2022 Shreyas Patil

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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

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!

edges are cut off and there is a white background when capturing in devices below API 29

I use Capturable to capture the canvas and save it in the gallery. The problem occurs on devices with API<29 when I zoom out from the canvas with the graphicsLayer to add additional pictures. Distant parts are simply cut off and so on only in devices with API<29, but on devices above everything is captured perfectly without cutting, etc. How can this be fixed?
photo_2024-05-02_01-16-59

Error when saving bitmap to PDF: Software rendering doesn't support hardware bitmaps

I'm trying to save a PDF file with a bitmap captured using this library drawn into it, however, I'm facing this error.

java.lang.Throwable: Software rendering doesn't support hardware bitmaps
	at android.graphics.BaseCanvas.throwIfHwBitmapInSwMode(:1031)
	at android.graphics.BaseCanvas.throwIfCannotDraw(:146)
	at android.graphics.BaseCanvas.drawBitmap(:186)
	at android.graphics.Canvas.drawBitmap(:1583)
	at ir.masterz.monex.content.monitor.ecg.EcgViewModel.generatePdf(:280)
...

My code:

        val file = File(fileDir, "ecg.jpg")
        file.createNewFile()

        val document = PdfDocument()
        val pageInfo = PdfDocument.PageInfo
            .Builder(595, 842, 1)
            .create()
        val page = document.startPage(pageInfo)
//line 280 is below
        page.canvas.drawBitmap(chartBitmaps.first()
            .asAndroidBitmap(), 0f, 0f, null)
        document.finishPage(page)
        document.writeTo(FileOutputStream(file))
        document.close()

Checking other issues like #31 and #13 it seems I need to disable the hardware rendering but how can I do that with my current code?
There's a page.canvas.isHardwareAccelerated but it's just a getter.

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!

.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?

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.

Not all elements are captured

lib version: 2.1.0

I have a screen like this:

Column(
            modifier = Modifier
                .fillMaxSize()
                .verticalScroll(rememberScrollState())
                .allWindowInsetsPadding()
                .padding(vertical = 40.dp)
        ) {
            Column(
                modifier = Modifier
                    .capturable(captureController)
                    .fillMaxWidth()
                    .background(AppTheme.colors.background)
                    .padding(top = 40.dp, bottom = 70.dp)
                    .padding(horizontal = 10.dp)
            ) {
                Text(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(bottom = 20.dp),
                    text = stringResource(R.string.Statistics),
                    color = AppTheme.colors.text,
                    style = MaterialTheme.typography.displaySmall,
                    fontWeight = FontWeight.SemiBold,
                    maxLines = 1,
                    textAlign = TextAlign.Center
                )
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.spacedBy(15.dp)
                ) {
                    statistics.map { StatisticsCard(it) }
                }
            }
        }

I'm expecting a full screenshot like this:

image

But I only get part:

image

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.

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.

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 ?

Not able to capture Modifier.blur()

I was trying to capture an Image composable modified with blur(). But it's not working so I had to use the lagacy RenderScript blurring method. Any idea why it's not capturing jepack compose blur?

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.

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!

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.

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?

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.

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)
                }
        )

    }

}

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.

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:

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! 😄

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.

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.