Főbb pontok
Pinch-to-Zoom (csípés vagy szorítás) — egy multitouch gesztus, amely két ujjat használ a megjelenített tartalom skálájának megváltoztatásához. Az ujjak összehúzásakor (pinch close) a skála csökken, széttárásakor (pinch open) — nő. A gesztus folyamatos: a rendszer valós időben követi az ujjak közötti távolságot és köztes eseményeket generál, lehetővé téve az alkalmazás számára a skála zökkenőmentes frissítését.
A gesztus mechanikája két érintési pont közötti távolság kiszámításán alapul. Az iOS és Android az euklideszi távolságot használja: sqrt((x2-x1)² + (y2-y1)²). Az aktuális scale a jelenlegi távolság és a kezdeti távolság arányaként számítható ki. Ha a kezdeti távolság 100px volt és 150px lett — scale = 1.5 (50%-os növekedés). Az iOS a scale-t az előző állapothoz képest (delta) adja át, az Android — felhalmozott scale-t a ScaleGestureDetector-on keresztül.
A Google Material Design Guidelines szerint a Pinch-to-Zoom kötelező a képeket, térképeket vagy dokumentumokat megjelenítő alkalmazások számára. Az IT Sectr-nél ezt a gesztust UIPinchGestureRecognizer és ScaleGestureDetector segítségével implementáljuk minden vizuális tartalommal dolgozó projektben — a galériáktól a képszerkesztőkig.
UIPinchGestureRecognizer — az UIGestureRecognizer folyamatos alosztálya a csípés gesztus felismerésére. A diszkrét felismerőkkel (UITapGestureRecognizer) ellentétben események sorozatát generálja: .began (ujjak érintették a képernyőt), .changed (ujjak minden mozgása), .ended (ujjak felemelkedtek).
Kulcsfontosságú tulajdonságok: scale (CGFloat) — az ujjak közötti távolság relatív változása a gesztus kezdetétől; velocity (CGFloat) — a scale másodpercenkénti változásának sebessége. Kezdeti scale = 1.0. Az ujjak széttárásakor a scale nő (1.0 → 1.5 → 2.0), összehúzásakor — csökken (1.0 → 0.8 → 0.5). A velocity tulajdonság tehetetlenséghez használatos: .ended után a skálázás folytatása csillapítással animálható.
Az UIPinchGestureRecognizer támogatja a delegate tulajdonságot más felismerőkkel (pl. pan + pinch térképen) való egyidejű működéshez. A gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) metódus true-t ad vissza, és mindkét gesztus párhuzamosan működik. Az Apple azt ajánlja, hogy az UIPinchGestureRecognizer-t konténer nézethez adjuk hozzá, ne egyedi elemekhez — ez leegyszerűsíti a skálázás pivot pontjának kiszámítását.
ScaleGestureDetector — egy osztály az Android Support Library-ből (android.view), amely a multitouch skálázás felismerésére szolgál. A GestureDetector.onFling-től eltérően a ScaleGestureDetector kifejezetten kétujjas nagyításhoz tervezték, és automatikusan kiszámítja a focusX, focusY (ujjak közötti középpont) és scaleFactor (skála delta az előző eseménytől) értékeket.
A ScaleGestureDetector az OnScaleGestureListener interfészen keresztül működik három metódussal: onScaleBegin(detector) (.began megfelelője), onScale(detector) (.changed megfelelője, boolean-t ad vissza — true, ha a skála elfogadva), onScaleEnd(detector) (.ended megfelelője). Tulajdonságok: scaleFactor — a skála szorzója az előző eseményhez képest (~1.01 lassú széttáráskor), focusX/focusY — az ujjak közötti középpont koordinátái.
A skála View-ra történő alkalmazásához általában Matrix-ot használnak: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). A Matrix támogatja a transzformációk felhalmozását — minden postScale hívás egy új transzformációt ad a meglévőhöz. Fontos megjegyezni: a ScaleGestureDetector nem korlátozza a minimális/maximális skálát — ez a fejlesztő feladata. Az IT Sectr-nél a ScaleGestureDetector-t egyedi skála határokkal (0.5–3.0) és túllépés esetén visszatérési animációval alkalmazzuk.
A Pinch-to-Zoom implementációjában kritikus fontosságú a minimális és maximális skála korlátozása — enélkül a felhasználó a végtelenségig nagyíthatja a képet, vagy láthatatlanságig kicsinyítheti. Alapértékek: min = 0.5 (az eredeti méret 50%-a), max = 3.0 (300%). iOS-ben a korlátozás clamp segítségével történik: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). Android-ban — postScale utáni ellenőrzéssel.
A pivot pont — az ujjak közötti középpont, amelyhez képest a skálázás történik. Az iOS automatikusan kiszámítja két érintés középpontját és átadja a location(in:) metóduson keresztül — a fejlesztőnek nem kell manuálisan kiszámítania a pivot-ot. Az Android focusX/focusY-t biztosít a ScaleGestureDetector-ban, de Matrix-on keresztüli alkalmazáskor a pivot a harmadik és negyedik argumentumként kerül átadásra a postScale(scaleFactor, scaleFactor, focusX, focusY) hívásban. Pivot pont nélkül a skálázás a bal felső sarokhoz (0,0) képest történik, ami a tartalom eltolódásához vezet nagyításkor.
Az IT Sectr tapasztalata szerint a pivot pont helyes kiszámítása a leggyakoribb hibaforrás a Pinch-to-Zoom bevezetésénél. Az esetek 90%-ában elegendő a GestureRecognizer és ScaleGestureDetector beépített koordinátáit használni, anélkül hogy a pivot-ot manuálisan számítanánk ki. Ha a tartalom eltolódik skálázáskor — ellenőrizze, hogy átadja-e a focusX/focusY-t a matrix.postScale() metódusnak.
A gördülékeny felhasználói élmény érdekében a gesztus befejezése után tehetetlenség adható hozzá: iOS-ben — használja a velocity-t az UIPinchGestureRecognizer-ből a UIView.animate spring paramétereivel; Android-ban — OverScroller vagy ValueAnimator. A tehetetlenség nem lépheti át a min/max scale határokat — a végső érték spring visszatérési animációval levágásra kerül túllépés esetén.
UIPinchGestureRecognizer-t ad hozzá UIImageView-hez 0.5-től 3.0-ig terjedő skála korlátozással. Minimális implementációt mutat a felhalmozott skála megtartásával.
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: .began/.changed állapotban a skála CGAffineTransform segítségével clamp-pel kerül alkalmazásra. .ended állapotban a scale visszaáll 1.0-ra, és a felhalmozott érték a currentScale-ben tárolódik. Ez megakadályozza a skála „sodródását" többszörös gesztusok esetén — minden új gesztus az aktuális állapotból indul. Képek esetén a CGAffineTransform helyett UIScrollView ajánlott minimumZoomScale/maximumZoomScale tulajdonságokkal.
ImageView skálázását implementálja ScaleGestureDetector segítségével Matrix-szal. Megmutatja a pivot pont helyes kezelését és a skála korlátozását.
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
}
}
Az egyedi ImageView felülírja az onTouchEvent metódust, átadva az eseményeket a ScaleGestureDetector-nak. Az OnScaleListener.onScale() minden ujjmozgáskor meghívódik. detector.scaleFactor — delta (1.01 lassú széttáráskor). A currentScale coerceIn segítségével halmozódik fel a clamp-hez. A focusX/focusY — az ujjak közötti középpont, amely a postScale metódusnak kerül átadásra a helyes pivot skálázáshoz.
Az UIScrollView beépített Pinch-to-Zoom támogatással rendelkezik delegált segítségével. Ez az előnyben részesített módszer képek és dokumentumok esetén — a ScrollView automatikusan kezeli a skálát, a tehetetlenséget és a görgetést.
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
}
}
Az UIScrollView önállóan létrehozza az UIPinchGestureRecognizer-t, kezeli a skálát CALayer transzformáción keresztül, és bounce effektust ad hozzá a határok túllépésekor. A kód a min/maxZoomScale beállítására és a viewForZooming visszaadására redukálódik. Az IT Sectr-nél az UIScrollView-t használjuk minden teljes képernyős képhez — ez ötszörösére csökkenti a kód mennyiségét a kézi UIPinchGestureRecognizer implementációhoz képest.
Gyakran ismételt kérdések
Az érintőképernyőn a pinch két ujjat használ a köztük lévő távolság követésével. MacBook trackpad-en a nagyítás UIPinchGestureRecognizer segítségével van implementálva ugyanazokkal az állapotokkal (.began → .changed → .ended), de az érintések forrása a Force Touch-os trackpad, nem kapacitív képernyő. Az API ugyanaz, a mechanika azonos.
iOS-ben: az UIPinchGestureRecognizer scale értékének megszerzése után szorozza meg a currentScale-lel és alkalmazza a clamp(min: 0.5, max: 3.0) függvényt. Android-ban: a coerceIn(minScale, maxScale) segítségével a matrix.postScale() előtt. UIScrollView-ben — a minimumZoomScale és maximumZoomScale tulajdonságokon keresztül. Korlátozás nélkül a skála a végtelenbe vagy nullába mehet.
Nem, a pinch legalább két ujjat igényel. Az egyujjas nagyításhoz (double-tap + húzás felfelé/lefelé) egyedi implementáció szükséges UIPanGestureRecognizer vagy GestureDetector.onScroll segítségével. Ez egy másik gesztus eltérő logikával — általában kamera vagy térképek nagyításához használatos.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is