Hlavní body
Pinch-to-Zoom (štípnutí nebo stisknutí) — multi-touch gesto, které používá dva prsty ke změně měřítka zobrazeného obsahu. Při přibližování prstů (pinch close) se měřítko zmenšuje, při oddalování (pinch open) — zvětšuje. Gesto je kontinuální: systém sleduje vzdálenost mezi prsty v reálném čase a generuje mezilehlé události, což aplikaci umožňuje plynule aktualizovat měřítko.
Mechanika gesta je založena na výpočtu vzdálenosti mezi dvěma body dotyku. iOS a Android používají euklidovskou vzdálenost: sqrt((x2-x1)² + (y2-y1)²). Aktuální scale se vypočítá jako poměr aktuální vzdálenosti k počáteční. Pokud byla počáteční vzdálenost 100px a stala se 150px — scale = 1.5 (zvýšení o 50%). iOS předává scale vůči předchozímu stavu (delta), Android — akumulovaný scale přes ScaleGestureDetector.
Podle Google Material Design Guidelines je Pinch-to-Zoom povinný pro aplikace zobrazující obrázky, mapy nebo dokumenty. V IT Sectr implementujeme toto gesto pomocí UIPinchGestureRecognizer a ScaleGestureDetector ve všech projektech pracujících s vizuálním obsahem — od galerií po editory obrázků.
UIPinchGestureRecognizer — kontinuální podtřída UIGestureRecognizer pro rozpoznávání gesta štípnutí. Na rozdíl od diskrétních rozpoznávačů (UITapGestureRecognizer) generuje sekvenci událostí: .began (prsty se dotkly obrazovky), .changed (každý pohyb prstů), .ended (prsty zvednuty).
Klíčové vlastnosti: scale (CGFloat) — relativní změna vzdálenosti mezi prsty od začátku gesta; velocity (CGFloat) — rychlost změny scale za sekundu. Počáteční scale = 1.0. Při oddalování prstů scale roste (1.0 → 1.5 → 2.0), při přibližování — klesá (1.0 → 0.8 → 0.5). Vlastnost velocity se používá pro setrvačnost: po .ended lze animovat pokračování škálování s útlumem.
UIPinchGestureRecognizer podporuje vlastnost delegate pro současnou práci s jinými rozpoznávači (např. pan + pinch na mapě). Metoda gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) vrací true a obě gesta pracují paralelně. Apple doporučuje přidávat UIPinchGestureRecognizer na kontejnerový view, ne na jednotlivé prvky — to zjednodušuje výpočet pivot bodu škálování.
ScaleGestureDetector — třída z Android Support Library (android.view), určená pro rozpoznávání multi-touch škálování. Na rozdíl od GestureDetector.onFling je ScaleGestureDetector speciálně navržen pro zoom dvěma prsty a automaticky vypočítává focusX, focusY (střed mezi prsty) a scaleFactor (delta měřítka od předchozí události).
ScaleGestureDetector pracuje přes rozhraní OnScaleGestureListener se třemi metodami: onScaleBegin(detector) (ekvivalent .began), onScale(detector) (ekvivalent .changed, vrací boolean — true pokud je měřítko přijato), onScaleEnd(detector) (ekvivalent .ended). Vlastnosti: scaleFactor — násobitel měřítka vůči předchozí události (~1.01 při pomalém oddalování), focusX/focusY — souřadnice středu mezi prsty.
Pro aplikaci měřítka na View se obvykle používá Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix podporuje akumulaci transformací — každé volání postScale přidává novou transformaci ke stávající. Důležité: ScaleGestureDetector neomezuje minimální/maximální měřítko — to je úkolem vývojáře. V IT Sectr aplikujeme ScaleGestureDetector s vlastními hranicemi scale (0.5–3.0) a animací návratu při překročení.
Při implementaci Pinch-to-Zoom je kriticky důležité omezit minimální a maximální měřítko — bez toho může uživatel zvětšit obrázek do nekonečna nebo jej zmenšit do neviditelnosti. Standardní hodnoty: min = 0.5 (50% původní velikosti), max = 3.0 (300%). V iOS se omezení provádí přes clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). V Android — přes kontrolu po postScale.
Pivot bod — střed mezi prsty, vůči kterému se provádí škálování. iOS automaticky vypočítá centroid dvou dotyků a předá jej přes location(in:) — vývojář nemusí počítat pivot ručně. Android poskytuje focusX/focusY v ScaleGestureDetector, ale při aplikaci přes Matrix se pivot předává jako třetí a čtvrtý argument v postScale(scaleFactor, scaleFactor, focusX, focusY). Bez pivot bodu se škálování provádí vůči levému hornímu rohu (0,0), což vede k posunu obsahu při zoomu.
Podle zkušeností IT Sectr je správný výpočet pivot bodu nejčastějším zdrojem chyb při zavádění Pinch-to-Zoom. V 90% případů stačí použít vestavěné souřadnice GestureRecognizer a ScaleGestureDetector, bez ručního výpočtu pivotu. Pokud se obsah při škálování posouvá — zkontrolujte, zda předáváte focusX/focusY do matrix.postScale().
Pro plynulé UX po dokončení gesta lze přidat setrvačnost: v iOS — použijte velocity z UIPinchGestureRecognizer pro UIView.animate s parametry spring; v Android — OverScroller nebo ValueAnimator. Setrvačnost by neměla překračovat hranice min/max scale — konečná hodnota je oříznuta s spring animací návratu při překročení.
Přidává UIPinchGestureRecognizer na UIImageView s omezením měřítka od 0.5 do 3.0. Ukazuje minimální implementaci s uchováním akumulovaného měřítka.
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: v .began/.changed se měřítko aplikuje přes CGAffineTransform s clamp. V .ended se scale resetuje na 1.0 a akumulovaná hodnota se uloží do currentScale. To zabraňuje "driftu" měřítka při vícenásobných gestech — každé nové gesto začíná z aktuálního stavu. Pro obrázky se místo CGAffineTransform doporučuje UIScrollView s minimumZoomScale/maximumZoomScale.
Implementuje škálování ImageView přes ScaleGestureDetector s Matrix. Ukazuje správné zpracování pivot bodu a omezení měřítka.
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
}
}
Vlastní ImageView přepisuje onTouchEvent a předává události ScaleGestureDetector. OnScaleListener.onScale() se volá při každém pohybu prstů. detector.scaleFactor — delta (1.01 při pomalém oddalování). currentScale se akumuluje s coerceIn pro clamp. focusX/focusY — střed mezi prsty, předaný do postScale pro správné pivot škálování.
UIScrollView má vestavěnou podporu Pinch-to-Zoom přes delegáta. Toto je preferovaný způsob pro obrázky a dokumenty — ScrollView spravuje měřítko, setrvačnost a posouvání automaticky.
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 samostatně vytváří UIPinchGestureRecognizer, spravuje měřítko pomocí CALayer transformace a přidává bounce efekt při překročení hranic. Kód se redukuje na nastavení min/maxZoomScale a vrácení viewForZooming. V IT Sectr používáme UIScrollView pro všechny celoobrazovkové obrázky — to 5krát snižuje objem kódu ve srovnání s ruční implementací přes UIPinchGestureRecognizer.
Často kladené otázky
Na dotykové obrazovce pinch používá dva prsty se sledováním vzdálenosti mezi nimi. Na trackpadu MacBook je škálování implementováno přes UIPinchGestureRecognizer se stejnými stavy (.began → .changed → .ended), ale zdrojem dotyků je trackpad s Force Touch, nikoli kapacitní obrazovka. API je stejné, mechanika identická.
V iOS: po získání scale z UIPinchGestureRecognizer vynásobte currentScale a aplikujte clamp(min: 0.5, max: 3.0). V Android: přes coerceIn(minScale, maxScale) před matrix.postScale(). V UIScrollView — přes vlastnosti minimumZoomScale a maximumZoomScale. Bez omezení může měřítko jít do nekonečna nebo nuly.
Ne, pinch vyžaduje minimálně dva prsty. Pro zoom jedním prstem (double-tap + tažení nahoru/dolů) je vyžadována vlastní implementace přes UIPanGestureRecognizer nebo GestureDetector.onScroll. Toto je jiné gesto s jinou logikou — obvykle se používá pro zoom v kameře nebo mapách.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také