Implementing Pinch to Zoom in Jetpack Compose
Jetpack Compose is the modern Android UI toolkit, developed by Google. It has revolutionized the creation of user interfaces for Android apps. With its declarative syntax and powerful features, Jetpack Compose simplifies UI development. An essential feature in many applications is the ability to zoom in on images or content. Pinch-to-zoom is a widely used gesture for this purpose.
In this article, we’ll create a Jetpack Compose function to build a smooth and interactive image viewer for Android apps. We’ll use the detectTransformGestures
function from Jetpack Compose. This function allows us to implement pinch-to-zoom functionality for Android apps to enhance the user experience.
Implement Pinch-to-Zoom
We create PinchToZoomView
composable function. The primary goal of this function is to display an image with pinch-to-zoom functionality. It allows users to zoom in/out, and pan around the image using gestures.
Key features of PinchToZoomView
composable function are:
- Pinch-to-Zoom: Users can pinch to zoom in or out, limiting how much they can zoom in or out.
- Pan: Users can move the zoomed-in image around the screen while ensuring it stays within bounds.
- Double-Tap Gesture: Double-tapping the image resets the zoom level and offset. If the image is already at its default zoom level, double-tapping zooms in further.
@Composable
fun PinchToZoomView(
modifier: Modifier,
imageContentDescription: String = ""
) {
// Mutable state variables to hold scale and offset values
var scale by remember { mutableStateOf(1f) }
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
val minScale = 1f
val maxScale = 4f
// Remember the initial offset
var initialOffset by remember { mutableStateOf(Offset(0f, 0f)) }
// Coefficient for slowing down movement
val slowMovement = 0.5f
// Box composable containing the image
Box(
modifier = modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
// Update scale with the zoom
val newScale = scale * zoom
scale = newScale.coerceIn(minScale, maxScale)
// Calculate new offsets based on zoom and pan
val centerX = size.width / 2
val centerY = size.height / 2
val offsetXChange = (centerX - offsetX) * (newScale / scale - 1)
val offsetYChange = (centerY - offsetY) * (newScale / scale - 1)
// Calculate min and max offsets
val maxOffsetX = (size.width / 2) * (scale - 1)
val minOffsetX = -maxOffsetX
val maxOffsetY = (size.height / 2) * (scale - 1)
val minOffsetY = -maxOffsetY
// Update offsets while ensuring they stay within bounds
if (scale * zoom <= maxScale) {
offsetX = (offsetX + pan.x * scale * slowMovement + offsetXChange)
.coerceIn(minOffsetX, maxOffsetX)
offsetY = (offsetY + pan.y * scale * slowMovement + offsetYChange)
.coerceIn(minOffsetY, maxOffsetY)
}
// Store initial offset on pan
if (pan != Offset(0f, 0f) && initialOffset == Offset(0f, 0f)) {
initialOffset = Offset(offsetX, offsetY)
}
}
}
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
// Reset scale and offset on double tap
if (scale != 1f) {
scale = 1f
offsetX = initialOffset.x
offsetY = initialOffset.y
} else {
scale = 2f
}
}
)
}
.graphicsLayer {
scaleX = scale
scaleY = scale
translationX = offsetX
translationY = offsetY
}
) {
// Image to be displayed with pinch-to-zoom functionality
Image(
painter = painterResource(id = R.drawable.resId),
contentDescription = imageContentDescription,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.FillWidth
)
}
}
Explanation:
- State Variables: We define three mutable state variables. They are
scale
,offsetX
, andoffsetY
. We define them using the remember function. These variables hold the current scale and offset values. Changes to these values will trigger the recomposition of the UI. - Scale Bounds: We set the
minScale
to 1f, which is the normal size of the image. ThemaxScale
is set to 4f, meaning the image can be zoomed in up to four times its original size. - Initial Offset: The
initialOffset
variable is used to remember where the image starts. When you begin panning. - Slow Movement Coefficient: The
slowMovement
defines a factor for slowing down movement. It happens when panning the image. You can adjust it to fit your needs. - Box Composable: A
Box
composable is used as the container for the image. It sets up modifiers for the Box, filling the maximum size, and handling pointer input. - Gesture Detection: In the
pointerInput
modifier, we use thedetectTransformGestures
function. It is used to detect pinch-to-zoom and pan gestures. Calculations are performed to adjust offsets based on thezoom
andpan
gestures. - Boundaries Check: The code checks the offset values to ensure they stay within bounds. These bounds are defined by
minOffsetX
,maxOffsetX
,minOffsetY
, andmaxOffsetY
. - Transform Gestures: In
detectTransformGestures
, the scale is updated from the zoom gesture. The offsets adjust to keep the image centered. - Double Tap Gesture:
detectTapGestures
is used to detect double taps. On a double tap, the scale and offsets are reset to their initial values or adjusted to a predefined scale. - Graphics Layer: The
graphicsLayer
modifier is used to apply transformations to theBox
composable. Here we usegraphicsLayer{}
lambda function instead ofgraphicsLayer()
. We do this to reduce the recomposition count while performing pinch-to-zoom. - Image: Finally, an
Image
composable is used to display the image. The pinch-to-zoom functionality is then enabled for the image.
Output
The output is a smooth and interactive image viewer for Android apps. It allows users to zoom in and out of images with ease, improving their interaction with the app.
Summary
In this guide, we implement pinch-to-zoom functionality in Jetpack Compose using detectTransformGestures
. We created a PinchToZoomView
composable that lets users zoom, pan, and reset an image with gestures. Key features include zoom limits and pan boundaries. Double-tap gestures are also supported. State variables and the graphicsLayer
modifier manage these interactions. The result is a smooth and interactive image viewer for Android apps.
Happy coding!