Punti chiave
Pinch-to-Zoom (pizzicamento) è un gesto multitouch che utilizza due dita per modificare la scala del contenuto visualizzato. Avvicinando le dita (pinch close), la scala diminuisce; allontanandole (pinch open), aumenta. Il gesto è continuo: il sistema traccia la distanza tra le dita in tempo reale e genera eventi intermedi, consentendo all'applicazione di aggiornare la scala in modo fluido.
La meccanica del gesto si basa sul calcolo della distanza tra due punti di tocco. iOS e Android utilizzano la distanza euclidea: sqrt((x2−x1)² + (y2−y1)²). La scala attuale viene calcolata come rapporto tra la distanza attuale e la distanza iniziale. Se la distanza iniziale era 100px e diventa 150px — scala = 1.5 (aumento del 50%). iOS passa la scala relativa allo stato precedente (delta), Android passa la scala accumulata tramite ScaleGestureDetector.
Secondo le Google Material Design Guidelines, Pinch-to-Zoom è obbligatorio per le applicazioni che mostrano immagini, mappe o documenti. In IT Sectr, implementiamo questo gesto con UIPinchGestureRecognizer e ScaleGestureDetector in tutti i progetti che lavorano con contenuti visivi — dalle gallerie agli editor di immagini.
UIPinchGestureRecognizer è una sottoclasse continua di UIGestureRecognizer per riconoscere il gesto di pizzicamento. A differenza dei riconoscitori discreti (UITapGestureRecognizer), genera una sequenza di eventi: .began (le dita toccano lo schermo), .changed (ogni movimento delle dita), .ended (le dita vengono sollevate).
Proprietà chiave: scale (CGFloat) — cambiamento relativo della distanza tra le dita dall'inizio del gesto; velocity (CGFloat) — tasso di cambiamento della scala al secondo. Scala iniziale = 1.0. Allontanando le dita, la scala aumenta (1.0 → 1.5 → 2.0); avvicinandole, diminuisce (1.0 → 0.8 → 0.5). La proprietà velocity viene utilizzata per l'inerzia: dopo .ended, puoi animare la continuazione della scala con decelerazione.
UIPinchGestureRecognizer supporta la proprietà delegate per il riconoscimento simultaneo con altri riconoscitori (ad esempio, pan + pinch su una mappa). Il metodo gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) restituisce true ed entrambi i gesti funzionano in parallelo. Apple raccomanda di aggiungere UIPinchGestureRecognizer alla vista contenitore piuttosto che a singoli elementi — questo semplifica il calcolo del punto pivot di scala.
ScaleGestureDetector è una classe della Android Support Library (android.view) progettata per riconoscere la scala multitouch. A differenza di GestureDetector.onFling, ScaleGestureDetector è specificamente costruito per lo zoom a due dita e calcola automaticamente focusX, focusY (il punto centrale tra le dita) e scaleFactor (delta di scala dall'evento precedente).
ScaleGestureDetector funziona tramite l'interfaccia OnScaleGestureListener con tre metodi: onScaleBegin(detector) (analogo a .began), onScale(detector) (analogo a .changed, restituisce boolean — true se la scala viene accettata), onScaleEnd(detector) (analogo a .ended). Proprietà: scaleFactor — moltiplicatore di scala relativo all'evento precedente (~1.01 per allontanamento lento), focusX/focusY — coordinate del punto centrale tra le dita.
Per applicare la scala a una View, di solito si utilizza Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix supporta l'accumulo di trasformazioni — ogni chiamata postScale aggiunge una nuova trasformazione a quella esistente. È importante ricordare: ScaleGestureDetector non limita la scala minima/massima — questa è responsabilità dello sviluppatore. In IT Sectr, utilizziamo ScaleGestureDetector con limiti di scala personalizzati (0.5–3.0) e un'animazione di ritorno in caso di superamento.
Nell'implementare Pinch-to-Zoom, è di fondamentale importanza limitare la scala minima e massima — senza questo, l'utente potrebbe ingrandire all'infinito o ridurre fino all'invisibilità. Valori standard: min = 0.5 (50% della dimensione originale), max = 3.0 (300%). In iOS, la limitazione avviene tramite clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). In Android — tramite un controllo dopo postScale.
Il punto pivot è il centro tra le dita, rispetto al quale viene eseguita la scala. iOS calcola automaticamente il centroide dei due tocchi e lo passa tramite location(in:) — lo sviluppatore non deve calcolare il pivot manualmente. Android fornisce focusX/focusY in ScaleGestureDetector, ma quando si applica tramite Matrix, il pivot viene passato come terzo e quarto argomento in postScale(scaleFactor, scaleFactor, focusX, focusY). Senza un punto pivot, la scala verrà eseguita rispetto all'angolo superiore sinistro (0,0), causando lo spostamento del contenuto durante lo zoom.
Secondo l'esperienza di IT Sectr, il calcolo corretto del punto pivot è la fonte più comune di bug nell'implementazione di Pinch-to-Zoom. Nel 90% dei casi, utilizzare le coordinate integrate di GestureRecognizer e ScaleGestureDetector è sufficiente — non è necessario calcolare il pivot manualmente. Se il contenuto si sposta durante la scala, verifica se stai passando focusX/focusY a matrix.postScale().
Per un'esperienza utente fluida dopo la fine del gesto, puoi aggiungere inerzia: in iOS — utilizza velocity da UIPinchGestureRecognizer con UIView.animate e parametri spring; in Android — OverScroller o ValueAnimator. L'inerzia non deve superare i limiti di scala min/max — il valore finale viene limitato con un'animazione di ritorno spring in caso di superamento.
Aggiunge UIPinchGestureRecognizer a UIImageView con limitazione della scala da 0.5 a 3.0. Mostra un'implementazione minima con conservazione della scala accumulata.
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
}
}
}
Logica: in .began/.changed, la scala viene applicata tramite CGAffineTransform con clamp. In .ended, la scala viene reimpostata a 1.0 e il valore accumulato viene memorizzato in currentScale. Ciò impedisce la deriva della scala durante gesti ripetuti — ogni nuovo gesto parte dallo stato corrente. Per le immagini, si raccomanda UIScrollView con minimumZoomScale/maximumZoomScale invece di CGAffineTransform.
Implementa la scala di ImageView tramite ScaleGestureDetector con Matrix. Mostra la corretta gestione del punto pivot e la limitazione della scala.
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
}
}
La ImageView personalizzata sovrascrive onTouchEvent, passando gli eventi a ScaleGestureDetector. OnScaleListener.onScale() viene chiamato ad ogni movimento delle dita. detector.scaleFactor è il delta (1.01 per allontanamento lento). currentScale si accumula con coerceIn per il clamp. focusX/focusY — il punto centrale tra le dita, passato a postScale per una corretta scala del pivot.
UIScrollView ha il supporto integrato di Pinch-to-Zoom tramite il suo delegato. Questo è l'approccio preferito per immagini e documenti — ScrollView gestisce automaticamente scala, inerzia e scorrimento.
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 crea automaticamente un UIPinchGestureRecognizer, gestisce la scala tramite la trasformazione CALayer e aggiunge un effetto di rimbalzo quando si superano i limiti. Il codice si riduce all'impostazione di min/maxZoomScale e alla restituzione di viewForZooming. In IT Sectr, utilizziamo UIScrollView per tutte le immagini a schermo intero — questo riduce il volume di codice di 5 volte rispetto all'implementazione manuale tramite UIPinchGestureRecognizer.
Domande frequenti
Su uno schermo tattile, pinch utilizza due dita con tracciamento della distanza tra di esse. Su un trackpad MacBook, lo zoom è implementato tramite UIPinchGestureRecognizer con gli stessi stati (.began → .changed → .ended), ma la fonte del tocco è un trackpad con Force Touch, non uno schermo capacitivo. L'API è la stessa, la meccanica è identica.
In iOS: dopo aver ottenuto la scala da UIPinchGestureRecognizer, moltiplica per currentScale e applica clamp(min: 0.5, max: 3.0). In Android: usa coerceIn(minScale, maxScale) prima di matrix.postScale(). In UIScrollView — tramite le proprietà minimumZoomScale e maximumZoomScale. Senza limitazione, la scala può andare all'infinito o a zero.
No, il pinch richiede almeno due dita. Per lo zoom a un dito (doppio tocco + trascinamento verso l'alto/basso), è necessaria un'implementazione personalizzata tramite UIPanGestureRecognizer o GestureDetector.onScroll. Questo è un gesto diverso con logica diversa — di solito utilizzato per lo zoom nella fotocamera o nelle mappe.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche