Implementing Image Zoom and Pan in Mobile Application
Zoom and pan look like two gesture recognizers. In reality it's a connected transformation system that must work smoothly at 60 fps, not conflict with other app gestures and correctly handle edge cases — double tap, image boundaries during pan, return to original state.
Technical Implementation Details
React Native — react-native-gesture-handler + react-native-reanimated. Standard <Image> doesn't support gesture transformations — need Animated.Image or Reanimated. Approach with useSharedValue for scale and translateX/Y, useGestureHandler for PinchGesture + PanGesture:
const scale = useSharedValue(1);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onUpdate((e) => {
scale.value = clamp(savedScale.value * e.scale, 1, 5);
})
.onEnd(() => {
savedScale.value = scale.value;
if (scale.value < 1) {
scale.value = withSpring(1);
}
});
Gesture.Simultaneous(pinchGesture, panGesture) allows pinch and pan simultaneously. Gesture.Race() — if you need to divide by priority.
Pan boundary limitation. At zoom x3, 375px image becomes 1125px. Max allowed translateX = (scaledWidth - containerWidth) / 2. Without this check user drags image off screen. Bounds checking in onUpdate via clamp():
const maxTranslateX = (containerWidth * (scale.value - 1)) / 2;
translateX.value = clamp(translateX.value + delta, -maxTranslateX, maxTranslateX);
Flutter — InteractiveViewer. Built-in Flutter widget with minScale, maxScale, boundaryMargin. For basic case sufficient. For gallery with multiple images — InteractiveViewer inside PageView, but horizontal swipe for photo change vs horizontal pan on zoom conflict. Solution: InteractiveViewer intercepts pan only when scale > 1; at scale == 1 gesture passes to PageView.
iOS native — UIPinchGestureRecognizer + UIPanGestureRecognizer. gestureRecognizer.require(toFail:) for conflict resolution. CGAffineTransform for transformations to UIImageView. UIScrollView + UIScrollViewDelegate.viewForZooming — alternative getting bounce at bounds and zoomRect animations free.
Double Tap
Double tap: if scale == 1 — zoom to 2–3x at tap location. If already zoomed — return to scale == 1. Animation via withSpring (for "rubber" feel) or withTiming with Easing.out(Easing.cubic).
Zoom point from tap coordinates relative to image:
const focalX = tapEvent.x - containerWidth / 2;
const focalY = tapEvent.y - containerHeight / 2;
translateX.value = withSpring(-focalX * (targetScale - 1));
From practice: medical imaging viewer app, React Native. Zoom on high-resolution X-ray images (4096×4096px). Loading fullsize image in Image component caused OutOfMemoryError on Android. Solution: react-native-fast-image with resizeMode="contain" for preview + tile-based loading of fullsize via react-native-zoom-toolkit with Deep Zoom format support.
Gallery with Zoom
Gallery: horizontal FlatList with pagingEnabled={true} (or ViewPager on Android). Each element — zoomable image. Horizontal pan conflict during zoom — resolve via activeOffsetX in Pan gesture handler: pan activates only on > 10px shift horizontally; while scale > 1 block page swipe.
What's Included
- Pinch-to-zoom with min/max scale limit (usually 1x–5x)
- Pan on zoomed image with boundary limits
- Double tap — zoom in/out with animation to tap point
- Bounce-return when exceeding boundaries
- Integration into gallery/carousel with correct gesture conflict resolution
- High-resolution image support without OutOfMemoryError
Timeline
1–3 working days — single image with zoom. With gallery and gesture conflict resolution — 2–3 days. Cost calculated individually.







