PatilShreyas / Capturable

🚀Jetpack Compose utility library for capturing Composable content and transforming it into Bitmap Image🖼️
https://patilshreyas.github.io/Capturable/
MIT License
1.06k stars 39 forks source link

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

Closed naikon33 closed 1 month ago

naikon33 commented 6 months ago

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

PatilShreyas commented 6 months ago

@naikon33 can you show a example of how you're using a modifier?

naikon33 commented 6 months ago

yes , here is part of the code that is responsible for zooming out and zooming in

`Capturable( controller = captureController, modifier = Modifier .size(capturableW.value, capturableH.value) .graphicsLayer( translationX = offsetCanvasX, translationY = offsetCanvasY, ), onCaptured = { bitmap, error -> if (bitmap != null) { saveToFile( context = context, bitmap = bitmap.asAndroidBitmap() ) { filePath -> onSave(filePath) } }

                                if (error != null) {
                                    // Error occurred. Handle it!
                                }
                            }
                        ) {

                            BoxWithConstraints(
                                Modifier
                                    .size(646.dp, 318.dp)
                                    .clip(RoundedCornerShape(16.dp))
                                    .background(Color.White)
                                    .graphicsLayer(
                                        translationX = offsetCanvasX,
                                        translationY = offsetCanvasY,
                                        scaleX = contentScale,
                                        scaleY = contentScale
                                    )
                                    .onGloballyPositioned { layoutCoordinates ->
                                        drawingAreaSize.value = layoutCoordinates.size
                                        /*selectedDrawables.forEach { drawableState ->
                                        val drawable =
                                            ContextCompat.getDrawable(context, drawableState.id)
                                        drawable?.let {
                                            val width = it.intrinsicWidth * drawableState.scale
                                            val height =
                                                it.intrinsicHeight * drawableState.scale
                                            drawable.setBounds(
                                                0,
                                                0,
                                                width.toInt(),
                                                height.toInt()
                                            )
                                        }
                                    }*/
                                    }`

I can’t understand why saving with distance doesn’t work on devices with API<29 but it works on devices with API>29

naikon33 commented 6 months ago

I'm using the library version: implementation "dev.shreyaspatil:capturable:1.0.3"

PatilShreyas commented 6 months ago

Can you try with the latest version please and with the latest modifier API?

naikon33 commented 6 months ago

I used the latest versions and even installed the latest version implementation "dev.shreyaspatil:capturable:2.1.0" . But this did not help, everything is also cut off in API below 29

PatilShreyas commented 6 months ago

Are you maintaining modifier order properly with the latest version's implementation?

naikon33 commented 6 months ago

yes, if this were not so, then saving with distance would not work correctly in API>29

naikon33 commented 6 months ago

This is my order:


Capturable(
                                controller = captureController,
                                modifier = Modifier
                                    .size(capturableW.value, capturableH.value)
                                    .graphicsLayer(
                                        translationX = offsetCanvasX,
                                        translationY = offsetCanvasY,
                                    ),
                                onCaptured = { bitmap, error ->
                                    // This is captured bitmap of a content inside Capturable Composable.
                                    if (bitmap != null) {
                                        saveToFile(
                                            context = context,
                                            bitmap = bitmap.asAndroidBitmap()
                                        ) { filePath ->
                                            onSave(filePath)
                                        }
                                        // Bitmap is captured successfully. Do something with it!
                                    }

                                    if (error != null) {
                                        // Error occurred. Handle it!
                                    }
                                }
                            ) {
                         BoxWithConstraints(
                                    Modifier
                                        .size(646.dp, 318.dp)
                                        .clip(RoundedCornerShape(16.dp))
                                        .background(Color.White)
                                        .graphicsLayer(
                                            translationX = offsetCanvasX,
                                            translationY = offsetCanvasY,
                                            scaleX = contentScale,
                                            scaleY = contentScale
                                        )
                                        .onGloballyPositioned { layoutCoordinates ->
                                            drawingAreaSize.value = layoutCoordinates.size
                                            /*selectedDrawables.forEach { drawableState ->
                                            val drawable =
                                                ContextCompat.getDrawable(context, drawableState.id)
                                            drawable?.let {
                                                val width = it.intrinsicWidth * drawableState.scale
                                                val height =
                                                    it.intrinsicHeight * drawableState.scale
                                                drawable.setBounds(
                                                    0,
                                                    0,
                                                    width.toInt(),
                                                    height.toInt()
                                                )
                                            }
                                        }*/
                                        }
                                        .pointerInput(Unit) {
                                            detectTapGestures { offset ->
                                                if (selectedSegment == TabItem.Text) {
                                                    isTextMode = true
                                                    textFields = textFields + TextFieldData(
                                                        "",
                                                        offset.x,
                                                        offset.y,
                                                        editable = true
                                                    )

                                                } else {
                                                    val touchedLines =
                                                        lines.firstOrNull { lineGroup ->
                                                            lineGroup.any { line ->
                                                                isPointCloseToLine(
                                                                    offset,
                                                                    line
                                                                )
                                                            }
                                                        }

                                                    if (touchedLines != null) {
                                                        selectedLines =
                                                            touchedLines.also { lineGroup ->
                                                                lines.forEach {
                                                                    it.forEach { line ->
                                                                        line.isSelected.value =
                                                                            false
                                                                    }
                                                                }
                                                                lineGroup.forEach {
                                                                    it.isSelected.value = true
                                                                }
                                                            }
                                                    } else {
                                                        lines.forEach {
                                                            it.forEach { line ->
                                                                line.isSelected.value = false
                                                            }
                                                        }
                                                        selectedLines = null
                                                    }
                                                }
                                                selectedDrawables.forEachIndexed { i, ds ->
                                                    ds.isSelected.value = false
                                                }
                                                lastMovedIndex = -1
                                            }
                                        }
                                        .pointerInput(Unit) {
                                            if (selectedSegment != TabItem.Text) {
                                                detectTransformGestures { centroid, pan, zoom, rotation ->
                                                    scope.launch {
                                                        val newScale = contentScale * zoom
                                                        contentScale = newScale.coerceIn(0.43f, 5f)

                                                        val centroidOffsetX =
                                                            centroid.x - (1041 / 2)
                                                        val centroidOffsetY = centroid.y - (603 / 2)

                                                        val proposedOffsetX =
                                                            offsetCanvasX + pan.x - (zoom - 1) * centroidOffsetX
                                                        val proposedOffsetY =
                                                            offsetCanvasY + pan.y - (zoom - 1) * centroidOffsetY

                                                        val maxOffsetX =
                                                            max(0f, 1041 * (contentScale - 1) / 2)
                                                        val maxOffsetY =
                                                            max(0f, 603 * (contentScale - 1) / 2)

                                                        offsetCanvasX = proposedOffsetX.coerceIn(
                                                            -maxOffsetX,
                                                            maxOffsetX
                                                        )
                                                        offsetCanvasY = proposedOffsetY.coerceIn(
                                                            -maxOffsetY,
                                                            maxOffsetY
                                                        )
                                                    }
                                                }
                                            }
                                        }
                                        .pointerInput(selectedLines) {
                                            if (selectedSegment == TabItem.Lines) {
                                                detectTransformGestures { centroid, pan, zoom, rotation ->
                                                    val groupCenter =
                                                        selectedLines?.let { findGroupCenter(it) }
                                                            ?: return@detectTransformGestures
                                                    val degrees =
                                                        rotation * (180.0 / Math.PI).toFloat() * 0.1f

                                                    selectedLines?.forEach { line ->
                                                        line.offsetX.value += pan.x
                                                        line.offsetY.value += pan.y
                                                        line.rotateAround(groupCenter, degrees)
                                                    }
                                                }
                                            }
                                        }
                                ) {

ActionButton(
                                modifier = Modifier
                                    .width(156.dp)
                                    .height(40.dp),
                                title = stringResource(R.string.save).toUpperCase(Locale.current),
                                isFilled = true,
                                onClick = {
                                        captureController.capture()
                                }
                            )
naikon33 commented 6 months ago

and here's my implementation:

dependencies {
     implementation "androidx.appcompat:appcompat:1.6.1"
    implementation "androidx.drawerlayout:drawerlayout:1.1.1"
    implementation 'androidx.core:core-ktx:1.13.1'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.9.0'
    implementation "androidx.compose.ui:ui:1.6.7"
    implementation "androidx.compose.ui:ui-tooling-preview:1.6.7"
    implementation "io.coil-kt:coil-compose:2.4.0"
    implementation 'androidx.compose.material:material:1.6.7'

    implementation "dev.shreyaspatil:capturable:2.1.0"

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.7"
    debugImplementation "androidx.compose.ui:ui-tooling:1.6.7"
    debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.7"
}
PatilShreyas commented 6 months ago

Don't use Capturable composable function which is deprecated now.

Use capturable() modifier on the exact component which you need to capture and see if that works or not.

naikon33 commented 6 months ago

I did as in the example in a new way, it didn’t help, the edges are still cut off:

 Column(
                                modifier = Modifier.capturable(captureController)
                            ) {

                                BoxWithConstraints(
                                    Modifier
                                        .size(646.dp, 318.dp)
                                        .clip(RoundedCornerShape(16.dp))
                                        .background(Color.White)
                                        .graphicsLayer(
                                            translationX = offsetCanvasX,
                                            translationY = offsetCanvasY,
                                            scaleX = contentScale,
                                            scaleY = contentScale
                                        )
                                        .onGloballyPositioned { layoutCoordinates ->
                                            drawingAreaSize.value = layoutCoordinates.size

                                        }
                                        .pointerInput(Unit) {

                                        }
                                ) {
ActionButton(
                                modifier = Modifier
                                    .width(156.dp)
                                    .height(40.dp),
                                title = stringResource(R.string.save).toUpperCase(Locale.current),
                                isFilled = true,
                                onClick = {
                                        scope.launch {
                                            val bitmapAsync = captureController.captureAsync()
                                            try {
                                                val bitmap = bitmapAsync.await()
                                                saveToFile(
                                                    context = context,
                                                    bitmap = bitmap.asAndroidBitmap()
                                                ) { filePath ->
                                                    onSave(filePath)
                                                }

    } catch (error: Throwable) {
                                                // Error occurred, do something.
                                            }
                                    }
                                }
                            )

here is a photo of it still being cropped: Screenshot_20240506_100136

And this is what it looks like on canvas:

Screenshot_20240506_100540

PatilShreyas commented 6 months ago

I'm not sure on this, but I think this is happening because of translation and scale applied via graphicsLayer() modifier. Maybe that's causing cropping of the captured area. Just for the test, can you try removing that graphicsLayer() for instance?

naikon33 commented 6 months ago

Well, graphicsLayer() is needed to zoom out and these distant places are cut off.P.s it turns out that the edges are cut off only in API 28; in other versions everything works correctly. Then why is the problem with API 28?

PatilShreyas commented 1 month ago

Probably fixed in v3.0.0, please try and let us know if the issue re-occurs.