Najważniejsze
Pinch-to-Zoom (szczypanie lub ściskanie) — wielodotykowy gest używający dwóch palców do zmiany skali wyświetlanej zawartości. Przy zbliżaniu palców (pinch close) skala zmniejsza się, przy rozsuwaniu (pinch open) — zwiększa. Gest jest ciągły: system śledzi odległość między palcami w czasie rzeczywistym i generuje zdarzenia pośrednie, umożliwiając aplikacji płynną aktualizację skali.
Mechanika gestu opiera się na obliczaniu odległości między dwoma punktami dotyku. iOS i Android używają odległości euklidesowej: sqrt((x2-x1)² + (y2-y1)²). Bieżący scale jest obliczany jako stosunek bieżącej odległości do początkowej. Jeśli początkowa odległość wynosiła 100px, a stała się 150px — scale = 1.5 (zwiększenie o 50%). iOS przekazuje scale względem poprzedniego stanu (delta), Android — skumulowany scale przez ScaleGestureDetector.
Według Google Material Design Guidelines, Pinch-to-Zoom jest obowiązkowy dla aplikacji wyświetlających obrazy, mapy lub dokumenty. W IT Sectr implementujemy ten gest z UIPinchGestureRecognizer i ScaleGestureDetector we wszystkich projektach pracujących z treścią wizualną — od galerii po edytory obrazów.
UIPinchGestureRecognizer — ciągła podklasa UIGestureRecognizer do rozpoznawania gestu szczypania. W przeciwieństwie do dyskretnych rozpoznawaczy (UITapGestureRecognizer), generuje sekwencję zdarzeń: .began (palce dotknęły ekranu), .changed (każdy ruch palców), .ended (palce oderwane).
Kluczowe właściwości: scale (CGFloat) — względna zmiana odległości między palcami od początku gestu; velocity (CGFloat) — prędkość zmiany scale na sekundę. Początkowy scale = 1.0. Przy rozsuwaniu palców scale rośnie (1.0 → 1.5 → 2.0), przy zbliżaniu — maleje (1.0 → 0.8 → 0.5). Właściwość velocity jest używana do inercji: po .ended można animować kontynuację skalowania z wygaszaniem.
UIPinchGestureRecognizer obsługuje właściwość delegate do jednoczesnej pracy z innymi rozpoznawaczami (np. pan + pinch na mapie). Metoda gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) zwraca true, a oba gesty działają równolegle. Apple zaleca dodawanie UIPinchGestureRecognizer do widoku kontenera, a nie do poszczególnych elementów — upraszcza to obliczanie punktu pivot skalowania.
ScaleGestureDetector — klasa z Android Support Library (android.view), przeznaczona do rozpoznawania wielodotykowego skalowania. W przeciwieństwie do GestureDetector.onFling, ScaleGestureDetector jest specjalnie zaprojektowany do zoomu dwoma palcami i automatycznie oblicza focusX, focusY (środek między palcami) oraz scaleFactor (delta skali z poprzedniego zdarzenia).
ScaleGestureDetector działa przez interfejs OnScaleGestureListener z trzema metodami: onScaleBegin(detector) (odpowiednik .began), onScale(detector) (odpowiednik .changed, zwraca boolean — true, jeśli skala przyjęta), onScaleEnd(detector) (odpowiednik .ended). Właściwości: scaleFactor — mnożnik skali względem poprzedniego zdarzenia (~1.01 przy wolnym rozsuwaniu), focusX/focusY — współrzędne środka między palcami.
Do zastosowania skali do View zwykle używa się Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix obsługuje kumulację transformacji — każde wywołanie postScale dodaje nowe przekształcenie do istniejącego. Ważne: ScaleGestureDetector nie ogranicza minimalnej/maksymalnej skali — to zadanie programisty. W IT Sectr stosujemy ScaleGestureDetector z niestandardowymi granicami scale (0.5–3.0) i animacją powrotu przy przekroczeniu.
Przy implementacji Pinch-to-Zoom krytycznie ważne jest ograniczenie minimalnej i maksymalnej skali — bez tego użytkownik może powiększyć obraz do nieskończoności lub zmniejszyć do niewidoczności. Standardowe wartości: min = 0.5 (50% oryginalnego rozmiaru), max = 3.0 (300%). W iOS ograniczenie wykonuje się przez clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). W Android — przez sprawdzenie po postScale.
Punkt pivot — środek między palcami, względem którego wykonywane jest skalowanie. iOS automatycznie oblicza centroid dwóch dotknięć i przekazuje go przez location(in:) — programista nie musi obliczać pivotu ręcznie. Android udostępnia focusX/focusY w ScaleGestureDetector, ale przy zastosowaniu przez Matrix pivot przekazywany jest jako trzeci i czwarty argument w postScale(scaleFactor, scaleFactor, focusX, focusY). Bez punktu pivot skalowanie będzie wykonywane względem lewego górnego rogu (0,0), co prowadzi do przesunięcia zawartości podczas zoomu.
Z doświadczenia IT Sectr, prawidłowe obliczenie punktu pivot — najczęstsze źródło błędów przy wdrażaniu Pinch-to-Zoom. W 90% przypadków wystarczy użyć wbudowanych współrzędnych GestureRecognizer i ScaleGestureDetector, nie obliczając pivotu ręcznie. Jeśli zawartość przesuwa się podczas skalowania — sprawdź, czy przekazujesz focusX/focusY do matrix.postScale().
Do płynnego UX po zakończeniu gestu można dodać inercję: w iOS — użyj velocity z UIPinchGestureRecognizer dla UIView.animate z parametrami spring; w Android — OverScroller lub ValueAnimator. Inercja nie powinna wychodzić poza granice min/max scale — końcowa wartość jest przycinana z animacją powrotu spring przy przekroczeniu.
Dodaje UIPinchGestureRecognizer do UIImageView z ograniczeniem skali od 0.5 do 3.0. Pokazuje minimalną implementację z zachowaniem skumulowanej skali.
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
}
}
}
Logika: w .began/.changed stosowana jest skala przez CGAffineTransform z clamp. W .ended scale jest resetowany do 1.0, a skumulowana wartość zapisywana w currentScale. Zapobiega to „dryfowaniu" skali przy wielokrotnych gestach — każdy nowy gest zaczyna od bieżącego stanu. Dla obrazów zamiast CGAffineTransform zaleca się UIScrollView z minimumZoomScale/maximumZoomScale.
Implementuje skalowanie ImageView przez ScaleGestureDetector z Matrix. Pokazuje poprawną obsługę punktu pivot i ograniczenie skali.
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
}
}
Niestandardowy ImageView nadpisuje onTouchEvent, przekazując zdarzenia do ScaleGestureDetector. OnScaleListener.onScale() jest wywoływany przy każdym ruchu palców. detector.scaleFactor — delta (1.01 przy wolnym rozsuwaniu). currentScale kumuluje się z coerceIn dla clamp. focusX/focusY — środek między palcami, przekazany do postScale dla poprawnego skalowania względem pivotu.
UIScrollView ma wbudowaną obsługę Pinch-to-Zoom przez delegata. Jest to preferowany sposób dla obrazów i dokumentów — ScrollView zarządza skalą, inercją i przewijaniem automatycznie.
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 samodzielnie tworzy UIPinchGestureRecognizer, zarządza skalą przez transformację CALayer i dodaje efekt bounce przy przekroczeniu granic. Kod sprowadza się do ustawienia min/maxZoomScale i zwrócenia viewForZooming. W IT Sectr używamy UIScrollView dla wszystkich pełnoekranowych obrazów — to 5-krotnie zmniejsza ilość kodu w porównaniu z ręczną implementacją przez UIPinchGestureRecognizer.
Często zadawane pytania
Na ekranie dotykowym pinch używa dwóch palców ze śledzeniem odległości między nimi. Na gładziku MacBook powiększanie jest realizowane przez UIPinchGestureRecognizer z tymi samymi stanami (.began → .changed → .ended), ale źródłem dotknięć jest gładzik z Force Touch, a nie ekran pojemnościowy. API jest takie samo, mechanika identyczna.
W iOS: po otrzymaniu scale z UIPinchGestureRecognizer pomnóż przez currentScale i zastosuj clamp(min: 0.5, max: 3.0). W Android: przez coerceIn(minScale, maxScale) przed matrix.postScale(). W UIScrollView — przez właściwości minimumZoomScale i maximumZoomScale. Bez ograniczenia skala może uciec do nieskończoności lub do zera.
Nie, pinch wymaga co najmniej dwóch palców. Do zoomu jednym palcem (double-tap + przeciągnięcie w górę/dół) wymagana jest niestandardowa implementacja przez UIPanGestureRecognizer lub GestureDetector.onScroll. To inny gest z inną logiką — zwykle używany do zoomu w kamerze lub mapach.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również