Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture scrollable content #59

Closed
jsericksk opened this issue Jul 22, 2022 · 4 comments · Fixed by #137
Closed

Capture scrollable content #59

jsericksk opened this issue Jul 22, 2022 · 4 comments · Fixed by #137
Labels
enhancement New feature or request

Comments

@jsericksk
Copy link

jsericksk commented Jul 22, 2022

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:

@PatilShreyas
Copy link
Owner

@jsericksk Thanks for the solutions. Will see how these can fit in this library

@showrav017
Copy link

Thanks @jsericksk . you solution worked like charm.

@5peak2me
Copy link

5peak2me commented Aug 10, 2023

It seems to be possible to use coil's ImageRequest.allowHardware(false) method to solve the problem of java.lang.IllegalArgumentException: Software rendering doesn't support hardware bitmaps

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data("https://raw.githubusercontent.com/PatilShreyas/Capturable/master/art/header.png")
        .allowHardware(false)
        .crossfade(true)
        .build(),
    contentDescription = null,
    modifier = Modifier
        .size(200.dp)
        .clip(RoundedCornerShape(12.dp)),
    contentScale = ContentScale.Crop
)

@PatilShreyas PatilShreyas added the enhancement New feature or request label Oct 2, 2023
@PatilShreyas PatilShreyas linked a pull request Jan 25, 2024 that will close this issue
@PatilShreyas
Copy link
Owner

This issue has been fixed and released in version 2.0.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants