1

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

Chup bae
  • 69
  • 1
  • 7
  • If you wish to contain draw area inside its Image or container call Modifier.clipToBounds(). But there are several things that could be improved in your code. Such as consuming selectively. Zooming from center of fingers/pointer instead of Image center and limit panning to not leave blank space on each side. – Thracian Aug 22 '23 at 09:20
  • You can check out this for which specific setup you wish to have not even using the library it's easy to implement a gesture specific to your needs. https://github.com/SmartToolFactory/Compose-Zoom – Thracian Aug 22 '23 at 09:30

2 Answers2

2

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.
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • Does not work, check this link https://drive.google.com/file/d/1eEtai7_tV2cj43na1tZ2VkinA9aGIVmv/view?usp=sharing – Chup bae Aug 22 '23 at 10:22
  • @Chupbae I cannot (site is blocked). Any chance for a Github gist or small repository? – VonC Aug 22 '23 at 12:18
  • https://github.com/kyadalu1/PdfLazy – Chup bae Aug 22 '23 at 14:51
  • What i mean is the image in the right in the link with two images with red borders. https://stackoverflow.com/questions/72922205/how-to-have-natural-pan-and-zoom-with-modifier-graphicslayer-pointerinput If you assign pointerInput before graphicsLayer that touch area is stationary while you might change where image drawn. So in that case you should only start the motion again from original bounds of Image or Box not where you translated graphics layer – Thracian Aug 22 '23 at 16:35
  • 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.This is not a good thing especially with PDF files because when you zoom in but you do not increase the bounds of Modifier.pointerInput. After you lift you finger the area out of original Image bounds might not react to user interaction if user touches out of original bounds – Thracian Aug 22 '23 at 16:37
  • @Thracian OK, The issue seems to be that the touch detection area (from pointerInput) doesn't change, even though the visual representation of the image (from graphicsLayer) does. This discrepancy can cause touch gestures to be detected incorrectly, especially when the image is panned or zoomed outside its original bounds. – VonC Aug 22 '23 at 17:54
  • What's incorrect in OPs code is he's keeping one offset and zoom State that he applies to every page. Even if he zooms page one every page is zoomed and overlap with each other. And even after correcting that he might need to limit zoom area with Modifier.clipToBounds if not, he might consider changing order of pointerInput and graphicsLayer if he wants to apply gesture in zoomed image bounds but not original bounds which would not have any effect if user tries to pan out of original image bounds – Thracian Aug 22 '23 at 17:57
  • And if he decides to limit zoom and transformation bounds it would also be good idea to limit pan to not have empty space near edges or sides and another improvement over this also would be to add natural zoom using the code from official pages that adjusts centroid with changing TranformationOrigin – Thracian Aug 22 '23 at 18:00
  • Natural zoom code is available here. https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/package-summary#(androidx.compose.ui.input.pointer.PointerInputScope).detectTransformGestures(kotlin.Boolean,kotlin.Function4) – Thracian Aug 22 '23 at 18:02
  • @Thracian You are right; each page should indeed have its own scale and offset properties. The method you have proposed to maintain individual states for each page using a `mutableStateMapOf` or a dedicated `PdfProperties` class is a more accurate solution. It ensures that each page can be zoomed and panned independently. I have upvoted your answer. – VonC Aug 22 '23 at 18:02
  • Thanks also as you pointed he needs to consume PointerInputChange when zoom is bigger than 1f or when user zooms with 2 fingers, etc. That's also another thing for smooth scrolling and zooming experience. – Thracian Aug 22 '23 at 18:06
  • doing a proper PDF Viewer or zoomable images requires several steps but you can have any of them while each one improves user experience further – Thracian Aug 22 '23 at 18:08
2

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

Thracian
  • 43,021
  • 16
  • 133
  • 222