Zooming each image inside LazyColumn causes content to overlap

575 views Asked by At

I am trying to zoom images which are items inside my LazyColumn but when i try to zoom in it overlaps content with the other images and the scrolling of lazycolumn also becomes too difficult

 var scale by remember { mutableStateOf(1f) }
 var offset by remember { mutableStateOf(Offset(0f, 0f)) }

LazyColumn() {
                            items(mPdfRenderer?.pageCount!!) { message ->
Box(
                                    modifier = Modifier
                                        .fillMaxSize()
                                        .pointerInput(Unit) {
                                            detectTransformGestures { _, pan, zoom, _ ->
                                                scale *= zoom
                                                offset = Offset(
                                                    (offset.x + pan.x).coerceIn(
                                                        -200.dp.toPx(),
                                                        200.dp.toPx()
                                                    ),
                                                    (offset.y + pan.y).coerceIn(
                                                        -200.dp.toPx(),
                                                        200.dp.toPx()
                                                    )
                                                )
                                            }
                                        }
                                ) {
                                    Image(
                                        bitmap = bitmap!!.asImageBitmap(),
                                        contentDescription = "some useful description",
                                        modifier = Modifier
                                            .fillMaxWidth()
                                            .border(width = 1.dp, color = Color.Gray)
                                            .scale(scale)
                                            .offset(offset.x.dp, offset.y.dp)
                                            .aspectRatio(1f)
                                    )
                                }
                            }
                        }

I am actually creating a pdf viewer. How can the zooming be improved so that content is not overlapped and the vertical scrolling remains smooth?

Check this to understand the zooming issue

2

There are 2 answers

2
Thracian On BEST ANSWER

The issue in your code is you are keeping single

 var scale by remember { mutableStateOf(1f) }
 var offset by remember { mutableStateOf(Offset(0f, 0f)) }

which applies to every item in LazyColumn while each pages should have its own scale and offset properties.

private class PdfProperties() {
    var zoom = mutableStateOf(1f)
    var offset = mutableStateOf(Offset.Zero)
}

or

val offsetMap = remember {
    mutableStateMapOf<Int, Offset>()
}

val scaleMap = remember {
    mutableStateMapOf<Int, Float>()
}

then you should apply and change offset and scale for each page.

This is how you should implement it with the Modifier which clips PDF page as in this ZoomableList.

https://stackoverflow.com/a/72668732/5457853

For smooth scrolling you should consume PointerInputChange conditionally when number of fingers is bigger than 1 or zoom is bigger than 1f as in answer below.

https://stackoverflow.com/a/76021552/5457853

This custom gesture i wrote that is available in link above or as library here makes it easy to consume PointerInputChange conditionally as

            Modifier.pointerInput(Unit) {

                //zoom in/out and move around
                detectTransformGestures(
                    pass = PointerEventPass.Initial,
                    onGesture = { gestureCentroid: Offset,
                                  gesturePan: Offset,
                                  gestureZoom: Float,
                                  _,
                                  _,
                                  changes: List<PointerInputChange> ->



// Consume touch when multiple fingers down
// or zoom > 1f
// This prevents LazyColumn scrolling
                        val size = changes.size
                        if (size > 1) {
                            changes.forEach { it.consume() }
                        }
                    }
                )
            })

Also you can refer this answer too.

How to handle horizontal scroll gesture combined with transform gestures in Jetpack Compose

Extra

As i asked in comments if you want to limit zoom into Image you should also call Modifier.clipToBounds if you wish to limit zoom inside Box also you might want to change zoom center and empty area by limiting pan in limits with

detectTransformGestures(
    onGesture = { _, gesturePan, gestureZoom, _ ->

        zoom = (zoom * gestureZoom).coerceIn(1f, 3f)
        val newOffset = offset + gesturePan.times(zoom)

        val maxX = (size.width * (zoom - 1) / 2f)
        val maxY = (size.height * (zoom - 1) / 2f)

        offset = Offset(
            newOffset.x.coerceIn(-maxX, maxX),
            newOffset.y.coerceIn(-maxY, maxY)
        )
    }
)
}

https://stackoverflow.com/a/72856350/5457853

And for natural pan and zooming which happens center of fingers instead of center of screen you can refer this official code

https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/package-summary#(androidx.compose.ui.input.pointer.PointerInputScope).detectTransformGestures(kotlin.Boolean,kotlin.Function4)

You can also consider checking this zoom library which offers features i mentioned above and more out of the box.

https://github.com/SmartToolFactory/Compose-Zoom

12
VonC On

You can try first one of the approaches suggested in "Zoom Lazycolumn item"

It involves:

  • Creating a Custom Gesture Detector: Instead of using the default detectTransformGestures, a custom gesture detector detectZoom is introduced to only detect zoom gestures, thus allowing the LazyColumn to still scroll properly.

  • Using Modifier.zIndex: To prevent overlapping issues, Modifier.zIndex is used. When the user zooms an image, the zIndex ensures the zoomed image is drawn on top of other images.

So first, define the custom zoom gesture:

suspend fun PointerInputScope.detectZoom(
    onGesture: (zoom: Float) -> Unit,
) {
    // [Same as provided in the SO answer]
}

Next, in your LazyColumn:

val list = /* Your data source. In the given example, it's a list of image URLs. */

LazyColumn(
    modifier = Modifier.fillMaxSize()
) {
    itemsIndexed(list) { index, item ->
        var scale by remember { mutableStateOf(1f) }

        Image(
            // Adjust based on your image source. In the example, they're using an online image.
            bitmap = bitmap!!.asImageBitmap(), 
            contentDescription = "some useful description",
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(1f)
                .pointerInput(Unit) {
                    detectZoom { zoom ->
                        scale *= zoom
                    }
                }
                .graphicsLayer {
                    scaleX = maxOf(1f, minOf(3f, scale))
                    scaleY = maxOf(1f, minOf(3f, scale))
                }
                .zIndex(scale)
        )
    }
}

That would:

  • make sure only zoom gestures are detected, not panning.
  • leverage zIndex to make sure zoomed images appear above other images to prevent overlapping.
  • simplify the zoom logic by focusing only on the zoom aspect and not mixing it with panning.