Основни точки
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 към контейнерен view, а не към отделни елементи — това опростява изчисляването на pivot точката на мащабиране.
ScaleGestureDetector — клас от Android Support Library (android.view), предназначен за разпознаване на мултитъч мащабиране. За разлика от GestureDetector.onFling, ScaleGestureDetector е специално проектиран за двупръстов zoom и автоматично изчислява 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), което води до изместване на съдържанието при zoom.
По опит на IT Sectr, правилното изчисляване на pivot точката е най-честият източник на грешки при внедряване на Pinch-to-Zoom. В 90% от случаите е достатъчно да използвате вградените координати на GestureRecognizer и ScaleGestureDetector, без да изчислявате pivot ръчно. Ако съдържанието се измества при мащабиране — проверете дали предавате focusX/focusY на matrix.postScale().
За плавно потребителско изживяване след завършване на жеста може да се добави инерция: в 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 изисква минимум два пръста. За zoom с един пръст (double-tap + плъзгане нагоре/надолу) е необходима персонализирана имплементация чрез UIPanGestureRecognizer или GestureDetector.onScroll. Това е различен жест с различна логика — обикновено се използва за zoom в камера или карти.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също