Главное
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 автоматически вычисляет centroid двух касаний и передаёт его через 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также