Головне
Pinch-to-Zoom (щипок) — мультитач-жест, який використовує два пальці для зміни масштабу вмісту, що відображається. При зведенні пальців (pinch close) масштаб зменшується, при розведенні (pinch open) — збільшується. Жест безперервний: система відстежує відстань між пальцями в реальному часі та генерує проміжні події, дозволяючи застосунку плавно оновлювати масштаб.
Механіка жесту заснована на обчисленні відстані між двома точками дотику. iOS та Android використовують евклідову відстань: sqrt((x2−x1)² + (y2−y1)²). Поточний scale обчислюється як відношення поточної відстані до початкової. Якщо початкова відстань була 100px, а стала 150px — scale = 1.5 (збільшення на 50%). iOS передає scale відносно попереднього стану (дельту), Android — накопичений scale через ScaleGestureDetector.
За даними Google Material Design Guidelines, Pinch-to-Zoom обов’язковий для застосунків, що відображають зображення, карти або документи. В IT Sectr ми реалізуємо цей жест за допомогою UIPinchGestureRecognizer та ScaleGestureDetector в усіх проектах, що працюють із візуальним контентом, — від галерей до редакторів зображень.
UIPinchGestureRecognizer — безперервний підклас UIGestureRecognizer для розпізнавання жесту щипка. На відміну від дискретних розпізнавачів (UITapGestureRecognizer), він генерує послідовність подій: .began (пальці торкнулися екрана), .changed (кожен рух пальців), .ended (пальці відірвано).
Ключові властивості: scale (CGFloat) — відносна зміна відстані між пальцями з початку жесту; velocity (CGFloat) — швидкість зміни scale за секунду. Початковий scale = 1.0. При розведенні пальців scale зростає (1.0 → 1.5 → 2.0), при зведенні — зменшується (1.0 → 0.8 → 0.5). Властивість velocity використовується для інерції: після .ended можна анімувати продовження масштабування зі згасанням.
UIPinchGestureRecognizer підтримує властивість delegate для одночасної роботи з іншими розпізнавачами (наприклад, pan + pinch на карті). Метод gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) повертає true, і обидва жести працюють паралельно. Apple рекомендує додавати UIPinchGestureRecognizer на контейнерну в’ю, а не на окремі елементи — це спрощує розрахунок pivot-точки масштабування.
ScaleGestureDetector — клас з Android Support Library (android.view), призначений для розпізнавання мультитач-масштабування. На відміну від GestureDetector.onFling, ScaleGestureDetector спеціально спроектований для двопальцевого зуму та автоматично обчислює focusX, focusY (центр між пальцями) та scaleFactor (дельта масштабу з попередньої події).
ScaleGestureDetector працює через інтерфейс OnScaleGestureListener із трьома методами: onScaleBegin(detector) (аналог .began), onScale(detector) (аналог .changed, повертає boolean — true, якщо масштаб прийнято), onScaleEnd(detector) (аналог .ended). Властивості: scaleFactor — множник масштабу відносно попередньої події (~1.01 при повільному розведенні), focusX/focusY — координати центру між пальцями.
Для застосування масштабу до View зазвичай використовується Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix підтримує накопичення трансформацій — кожен виклик postScale додає нове перетворення до існуючого. Важливо пам’ятати: ScaleGestureDetector не обмежує мінімальний/максимальний масштаб — це задача розробника. В IT Sectr ми застосовуємо ScaleGestureDetector з кастомними межами scale (0.5–3.0) та анімацією повернення при перевищенні.
При реалізації Pinch-to-Zoom критично важливо обмежити мінімальний і максимальний масштаб — без цього користувач може збільшити зображення до безмежності або зменшити до невидимості. Стандартні значення: min = 0.5 (50% вихідного розміру), max = 3.0 (300%). На iOS обмеження виконується через clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). На Android — через перевірку після postScale.
Pivot-точка — центр між пальцями, відносно якого виконується масштабування. iOS автоматично обчислює центроїд двох дотиків і передає його через location(in:) — розробнику не потрібно розраховувати pivot вручну. Android надає focusX/focusY в ScaleGestureDetector, але при застосуванні через Matrix pivot передається третім і четвертим аргументом в postScale(scaleFactor, scaleFactor, focusX, focusY). Без pivot-точки масштабування буде виконуватися відносно лівого верхнього кута (0,0), що призводить до зміщення вмісту при зумі.
За досвідом IT Sectr, правильний розрахунок pivot-точки — найчастіше джерело багів при впровадженні Pinch-to-Zoom. У 90% випадків достатньо використовувати вбудовані координати GestureRecognizer та ScaleGestureDetector, не обчислюючи pivot вручну. Якщо вміст зміщується при масштабуванні — перевірте, чи передаєте ви focusX/focusY в matrix.postScale().
Для плавного UX після завершення жесту можна додати інерцію: на iOS — використовуйте velocity з UIPinchGestureRecognizer для UIView.animate із spring-параметрами; на Android — OverScroller або ValueAnimator. Інерція не повинна виходити за межі min/max scale — фінальне значення обрізається з анімацією spring-повернення при перевищенні.
Додає UIPinchGestureRecognizer на UIImageView з обмеженням масштабу від 0.5 до 3.0. Показує мінімальну реалізацію зі збереженням накопиченого масштабу.
import UIKit
class ZoomableImageViewController: UIViewController {
@IBOutlet private var imageView: UIImageView!
private var currentScale: CGFloat = 1.0
private let minScale: CGFloat = 0.5
private let maxScale: CGFloat = 3.0
override func viewDidLoad() {
super.viewDidLoad()
let pinch = UIPinchGestureRecognizer(
target: self,
action: #selector(handlePinch(_:))
)
imageView.addGestureRecognizer(pinch)
imageView.isUserInteractionEnabled = true
}
@objc private func handlePinch(_: UIPinchGestureRecognizer) {
if gestureRecognizer.state == .began
|| gestureRecognizer.state == .changed {
let newScale = currentScale * gestureRecognizer.scale
imageView.transform = CGAffineTransform(
scaleX: min(max(newScale, minScale), maxScale),
y: min(max(newScale, minScale), maxScale)
)
}
if gestureRecognizer.state == .ended {
currentScale = min(max(
currentScale * gestureRecognizer.scale,
minScale
), maxScale)
gestureRecognizer.scale = 1.0
}
}
}
Логіка: в .began/.changed застосовується масштаб через CGAffineTransform із clamp. В .ended scale скидається на 1.0, а накопичене значення зберігається в currentScale. Це запобігає «дрейфу» масштабу при багаторазових жестах — кожен новий жест починає з поточного стану. Для зображень замість CGAffineTransform рекомендується UIScrollView з minimumZoomScale/maximumZoomScale.
Реалізує масштабування ImageView через ScaleGestureDetector з Matrix. Показує коректну обробку pivot-точки та обмеження масштабу.
import android.content.Context
import android.graphics.Matrix
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.widget.ImageView
class PinchZoomImageView(context: Context) : ImageView(context) {
private val matrix = Matrix()
private var currentScale = 1f
private val minScale = 0.5f
private val maxScale = 3.0f
private val scaleDetector =
ScaleGestureDetector(context, ScaleListener())
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
currentScale = (currentScale * detector.scaleFactor)
.coerceIn(minScale, maxScale)
matrix.postScale(
currentScale,
currentScale,
detector.focusX,
detector.focusY
)
imageMatrix = matrix
return true
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
scaleDetector.onTouchEvent(event)
return true
}
}
Кастомний ImageView перевизначає onTouchEvent, передаючи події ScaleGestureDetector. OnScaleListener.onScale() викликається при кожному русі пальців. detector.scaleFactor — дельта (1.01 при повільному розведенні). currentScale накопичується з coerceIn для clamp. focusX/focusY — центр між пальцями, переданий в postScale для коректного pivot-масштабування.
UIScrollView має вбудовану підтримку Pinch-to-Zoom через делегат. Це найкращий спосіб для зображень і документів — ScrollView керує масштабом, інерцією та прокруткою автоматично.
class ScrollableImageViewController: UIViewController,
UIScrollViewDelegate {
@IBOutlet private var scrollView: UIScrollView!
@IBOutlet private var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.minimumZoomScale = 0.5
scrollView.maximumZoomScale = 3.0
scrollView.delegate = self
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return imageView
}
}
UIScrollView самостійно створює UIPinchGestureRecognizer, керує масштабом через CALayer трансформацію та додає bounce-ефект при перевищенні меж. Код зводиться до встановлення min/maxZoomScale та повернення viewForZooming. В IT Sectr ми використовуємо UIScrollView для всіх повноекранних зображень — це в 5 разів скорочує обсяг коду порівняно з ручною реалізацією через UIPinchGestureRecognizer.
Часті запитання
На сенсорному екрані pinch використовує два пальці з відстеженням відстані між ними. На трекпаді MacBook масштабування реалізовано через UIPinchGestureRecognizer з тими ж станами (.began → .changed → .ended), але джерело дотиків — трекпад із Force Touch, а не ємнісний екран. API однаковий, механіка ідентична.
В iOS: після отримання scale від UIPinchGestureRecognizer помножте на currentScale та застосуйте clamp(min: 0.5, max: 3.0). В Android: через coerceIn(minScale, maxScale) перед matrix.postScale(). В UIScrollView — через властивості minimumZoomScale та maximumZoomScale. Без обмеження масштаб може піти в безмежність або в нуль.
Ні, pinch потребує мінімум двох пальців. Для однопальцевого зуму (double-tap + drag вгору/вниз) потрібна кастомна реалізація через UIPanGestureRecognizer або GestureDetector.onScroll. Це інший жест з іншою логікою — зазвичай використовується для зуму в камері або на картах.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також