What You Will Build

A zoomable and pannable content area that responds to pinch gestures for scaling and drag gestures for panning. Includes a reset button and a live scale indicator. This is the same interaction used in photo viewers, map views, and document readers.

Why This Pattern Matters

Pinch-to-zoom is fundamental to any media-heavy app. The detectTransformGestures API in Compose handles multi-touch elegantly, giving you pan, zoom, and rotation in a single callback. Mastering it unlocks image galleries, map interactions, and canvas-based drawing tools.

Step 1: State for Scale and Offset

@Composable
fun PinchToZoomScreen() {
    var scale by remember { mutableFloatStateOf(1f) }
    var offsetX by remember { mutableFloatStateOf(0f) }
    var offsetY by remember { mutableFloatStateOf(0f) }

Step 2: Apply Transform Gestures

The detectTransformGestures function provides pan offset, zoom factor, and rotation angle in one callback:

Box(
    modifier = Modifier
        .size(280.dp, 350.dp)
        .graphicsLayer {
            scaleX = scale
            scaleY = scale
            translationX = offsetX
            translationY = offsetY
        }
        .clip(RoundedCornerShape(16.dp))
        .background(
            Brush.linearGradient(
                listOf(Color(0xFF667EEA), Color(0xFF764BA2))
            )
        )
        .pointerInput(Unit) {
            detectTransformGestures { _, pan, zoom, _ ->
                scale = (scale * zoom).coerceIn(0.5f, 4f)
                offsetX += pan.x
                offsetY += pan.y
            }
        },
    contentAlignment = Alignment.Center
) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Zoom Me",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
            color = Color.White)
        Text("Scale: ${"%.1f".format(scale)}x",
            fontSize = 14.sp,
            color = Color.White.copy(alpha = 0.8f))
    }
}

Step 3: Add a Reset Button

Button(
    onClick = {
        scale = 1f
        offsetX = 0f
        offsetY = 0f
    },
    modifier = Modifier.padding(16.dp)
) {
    Text("Reset")
}

Tips and Pitfalls

  • Clamp the scale: coerceIn(0.5f, 4f) prevents the content from becoming too small or too large. Adjust these bounds for your use case.
  • Pan limits: In production, calculate max offset based on current scale to prevent the content from being dragged completely off-screen.
  • Animated reset: Replace instant reset with Animatable values and animateTo for a smooth snap-back.
  • Double-tap to zoom: Add detectTapGestures(onDoubleTap = { scale = if (scale > 1f) 1f else 2f }) alongside the transform gesture.

Min SDK: API 21+ with Compose 1.0+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.