PatilShreyas/Capturable

Capture scrollable content

Closed this issue ยท 4 comments

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:

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

Thanks @jsericksk . you solution worked like charm.

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
)

This issue has been fixed and released in version 2.0.0.