Android Zoom and Pan Image Transformation
Example of how to implement a zoom and pan feature in Android Compose.

In this post I will show you how I implemented a zoom and pan feature to an image in Android Compose using the pointerInput modifier to capture gestures.

This sample I am showing is directly derived from one of shown in the Android Developer examples that can be viewed in the link below.

https://developer.android.com/develop/ui/compose/touch-input/pointer-input/tap-and-press#double-tap

I am using the Landscapist Glide library for this example. I have recently utilized this library in another project and it looks promising. One of the benefits of it is that you can add custom composables. They can be added to the loading and error methods to display while the work is being processed. This is not necessary for this example but I wanted to point out one of the features I believe is important.

Inside the Glide Composable, I am capturing the screen size in a mutable state. The pointerInput modifier is used to capture the double tap gesture. I included this in the image composable so the double tap gestures do not get comsumed by the parent composable.

Next inside the parent composable, in this case a Box, I am capturing the scale and offset values using the pointerInput modifier to adjust the zoom and pan values.

Code

First add the Landscapist Glide library, add this to build.gradle.

    implementation(libs.landscapist.glide)

Then add to the libs.versions.toml.

[versions]
...

landscapistGlide = "2.4.7"

[libraries]
...

landscapist-glide = { module = "com.github.skydoves:landscapist-glide", version.ref = "landscapistGlide" }

This is the custom composable named ImageTransform.

@Composable
fun ImageTransform(modifier: Modifier = Modifier, image: Int) {

    var scale by remember { mutableStateOf(1.0f) }
    var offset by remember { mutableStateOf(Offset.Zero) }
    var screenPos by remember { mutableStateOf(IntSize.Zero) }
    Box(modifier = modifier
        .pointerInput(Unit) {
            detectTransformGestures { _, pan, zoom, _ ->
                scale *= zoom
                offset = offset.copy(x = offset.x + pan.x, y = offset.y + pan.y)
            }

        }
        .graphicsLayer {
            scaleX = scale
            scaleY = scale
            translationX = offset.x
            translationY = offset.y
        }, contentAlignment = Alignment.Center
    ) {
        GlideImage(
            modifier = Modifier.fillMaxSize()
                .onGloballyPositioned {
                screenPos = it.size
            }.
            pointerInput(Unit){
                detectTapGestures(
                    onDoubleTap = {
                        scale *= 1.5f
                        offset = offset.copy(x = -(it.x - (screenPos.width / 2)) * scale, y = -(it.y - (screenPos.height / 2)) * scale)
                    }
                )
            },
            imageModel = { image },
            imageOptions = ImageOptions(
                contentScale = ContentScale.Fit,
                alignment = Alignment.Center
            ),
            loading = {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) { CircularProgressIndicator() }
            },
            failure = { GlideImage(imageModel = { R.mipmap.ic_launcher }) }
        )
    }

}

An example of how to call this Composable into view from the MainActivity.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            SampleImageTransformationTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    ImageTransform(
                        modifier = Modifier
                            .padding(innerPadding),
                        R.mipmap.ic_launcher
                    )
                }
            }
        }
    }
}

Hope this was useful.

Leave a Reply

Your email address will not be published. Required fields are marked *