Зум і панорамування — базова функціональність галерей, карт і в'юверів у мобільних додатках. Однак некоректна обробка жестів псує користувацький досвід: зображення смикається, вилітає за межі, а конфлікти між пінчем і свайпом призводять до роздратування. Ми реалізуємо цю механіку з гарантією плавності 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 робочих днів, що вкладається в типовий спринт. Зв'яжіться з нами для обговорення вашого проєкту — ми підберемо оптимальне рішення під ваш стек і бюджет. Замовте реалізацію плавного зуму і панорамування, щоб ваш додаток виділявся на фоні конкурентів. Отримайте консультацію по вашому проєкту вже сьогодні.







