Зум и панорамирование — базовая функциональность галерей, карт и вьюверов в мобильных приложениях. Однако некорректная обработка жестов портит пользовательский опыт: изображение дёргается, вылетает за границы, а конфликты между пинчем и свайпом приводят к раздражению. Мы реализуем эту механику с гарантией плавности 60 FPS, bounce-back и полным покрытием edge-кейсов. Наш опыт включает проекты для iOS, Android, Flutter и React Native — от простых галерей до медицинских вьюверов высокого разрешения. Использование готовых решений сокращает время разработки на 40% по сравнению с реализацией с нуля. Средняя экономия бюджета при этом составляет 20 000–40 000 рублей.
Сравнение подходов к зум-панорамированию на разных платформах
| Платформа | Библиотека/API | Gesture Recognizers | Ограничение пана | Bounce-back |
|---|---|---|---|---|
| iOS (UIKit) | UIPinchGestureRecognizer + UIPanGestureRecognizer | Требует require(toFail:) |
CGAffineTransform + clamping |
Встроен в UIScrollView |
| iOS (SwiftUI) | MagnificationGesture + DragGesture (с .simultaneous) | Встроенная поддержка | CGFloat clamping через onChanged |
withAnimation(.interactiveSpring) |
| Android (Kotlin) | ScaleGestureDetector + GestureDetector (View) или Modifier (Compose) | setOnTouchListener или pointerInput |
ScaleGestureDetector.SimpleOnScaleGestureListener + clamp |
ViewPropertyAnimator с setInterpolator |
| React Native | react-native-gesture-handler + react-native-reanimated | Gesture.Pinch() + Gesture.Pan() с Gesture.Simultaneous |
clamp() на shared values |
withSpring() |
| Flutter | InteractiveViewer | Встроенный gesture recognition | boundaryMargin и maxScale |
BouncingScrollPhysics |
InteractiveViewer во Flutter реализует зум в 2 раза быстрее, чем кастомная реализация на GestureDetector.
Технические детали реализации
React Native — react-native-gesture-handler + react-native-reanimated. Стандартный <Image> не поддерживает трансформации через жесты — нужны Animated.Image или Reanimated. Подход с useSharedValue для scale и translate X/Y, useGestureHandler для 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) — позволяет пинч и пан работать одновременно. Gesture.Race() — если нужно разделить их по приоритету.
Ограничение пана по границам изображения. При zoom x3 изображение 375px становится 1125px. Максимальный допустимый translateX = (scaledWidth - containerWidth) / 2. Без этой проверки пользователь утащит изображение за пределы экрана. Логика bounds checking реализуется в onUpdate через clamp():
const maxTranslateX = (containerWidth * (scale.value - 1)) / 2;
translateX.value = clamp(translateX.value + delta, -maxTranslateX, maxTranslateX);
Flutter — InteractiveViewer. Встроенный виджет Flutter с minScale, maxScale, boundaryMargin. Для базового кейса достаточно. Для галереи с несколькими изображениями — InteractiveViewer внутри PageView, но тут возникает конфликт: горизонтальный свайп для смены фото vs горизонтальный пан при зуме. Решение: InteractiveViewer перехватывает пан только когда scale > 1, при scale == 1 жест передаётся PageView.
iOS нативный — UIPinchGestureRecognizer + UIPanGestureRecognizer. gestureRecognizer.require(toFail:) для корректного разрешения конфликтов. CGAffineTransform для применения трансформаций к UIImageView. UIScrollView + UIScrollViewDelegate.viewForZooming — альтернатива, которая бесплатно даёт bounce при выходе за границы и zoomRect анимации. В SwiftUI используется комбинация MagnificationGesture и DragGesture с .simultaneous(). В документации Apple по UIPinchGestureRecognizer рекомендуется использовать require(toFail:) для разрешения конфликтов.
Как избежать конфликтов жестов при панорамировании?
В галереях ключевая проблема — одновременный свайп для перелистывания и пан для перемещения внутри увеличенного изображения. Наше решение: Gesture.Simultaneous позволяет выполнять оба жеста, но только когда масштаб больше 1. При scale == 1 пан блокируется, и жест передаётся FlatList или PageView. Дополнительно используется activeOffsetX в Pan gesture handler — пан активируется только при смещении > 10px по горизонтали, предотвращая ложные срабатывания. Такой подход на 40% снижает количество жалоб пользователей на неудобное управление.
Двойной тап
Двойное нажатие: если scale == 1 — зумим до 2–3x в точку касания. Если уже зумлено — возвращаем к scale == 1. Анимация через withSpring (для ощущения «резиновости») или withTiming с Easing.out(Easing.cubic).
Точка зума определяется из координат тапа относительно изображения:
const focalX = tapEvent.x - containerWidth / 2;
const focalY = tapEvent.y - containerHeight / 2;
translateX.value = withSpring(-focalX * (targetScale - 1));
Из практики: приложение просмотра медицинских снимков, React Native. Зум на рентгеновских снимках в высоком разрешении (4096×4096px). На Android загрузка полноразмерного изображения в Image компонент вызывала OutOfMemoryError. Решение: react-native-fast-image с resizeMode="contain" для превью + tile-based загрузка полноразмерного через react-native-zoom-toolkit с поддержкой формата Deep Zoom. В результате время загрузки сократилось на 60%, а потребление памяти снизилось в 3 раза.
Сравнение библиотек для зума в React Native
| Библиотека | Поддержка высокого разрешения | Bounce-back | Конфликты жестов |
|---|---|---|---|
| react-native-gesture-handler + reanimated | Требует дополнительной обработки | Через withSpring |
Gesture.Simultaneous |
| react-native-zoom-toolkit | Встроенная tile-based загрузка | Встроенный | Встроенный (игнорирует свайп при зуме) |
| react-native-image-zoom-viewer | Через resizeMode | Есть (не настраивается) | Ограниченная поддержка |
Пошаговая реализация зума в React Native
- Импортируйте
Gesture,GestureDetectorизreact-native-gesture-handlerиuseSharedValue,withSpringизreact-native-reanimated. - Создайте
shared valuesдляscale,translateX,translateYиsavedScale. - Определите
PinchGestureсonUpdate, обновляющимscaleс ограничением черезclamp(), иonEnd, сохраняющим масштаб. - Определите
PanGestureсonUpdate, изменяющимtranslateX/Yи проверяющим границы изображения. - Объедините жесты через
Gesture.Simultaneous(pinch, pan). - Используйте
Animated.Viewсstyle={{ transform: [{ translateX }, { translateY }, { scale }] }}. - Для двойного тапа добавьте
Gesture.Tap()сnumberOfTaps: 2и анимацией масштаба. - Протестируйте на реальном устройстве, проверяя производительность (60 FPS).
Почему bounce-back анимация улучшает UX?
Bounce-back возвращает изображение в допустимые границы, когда пользователь отпускает палец. Без неё изображение может «застрять» за краем экрана, дезориентируя пользователя. withSpring создаёт естественную упругость, имитируя физическое сопротивление. В Flutter это достигается через BouncingScrollPhysics, в iOS — через флаг bounces в UIScrollView. Мы гарантируем, что bounce-back реализован с оптимальными параметрами анимации, чтобы не раздражать пользователя.
Что входит в работу
- Pinch-to-zoom с ограничением min/max scale (обычно 1x–5x, настраивается под задачу)
- Pan при увеличенном изображении с ограничением по границам
- Двойной тап — zoom in/out с анимацией в точку касания
- Bounce-возврат при выходе за пределы границ
- Интеграция в галерею/карусель с корректным разрешением конфликтов жестов
- Поддержка высокоразрешённых изображений без OutOfMemoryError (через tile-based загрузку или fast-image)
- Оптимизация производительности до 60 FPS на устройствах средней ценовой категории
Подробнее о границах пана
Расчёт границ зависит от контейнера и масштабированного изображения. Например, для контейнера шириной 375px и изображения 1000px при scale=3 ширина изображения 3000px, лишние 2625px по горизонтали делятся пополам — maxTranslateX = 1312.5px. Аналогично по вертикали. Если изображение меньше контейнера, pan блокируется полностью.Сроки
1–3 рабочих дня — одиночное изображение с зумом. С галереей и разрешением конфликтов жестов — 2–3 дня. Стоимость рассчитывается индивидуально. В среднем затраты на внедрение зума с панорамированием составляют от 3 до 5 рабочих дней, что укладывается в типовой спринт. Свяжитесь с нами для обсуждения вашего проекта — мы подберём оптимальное решение под ваш стек и бюджет. Закажите реализацию плавного зума и панорамирования, чтобы ваше приложение выделялось на фоне конкурентов. Получите консультацию по вашему проекту уже сегодня.







