Puntos clave
Pinch-to-Zoom (pellizco) es un gesto multitáctil que usa dos dedos para cambiar la escala del contenido mostrado. Al juntar los dedos (pinch close), la escala disminuye; al separarlos (pinch open), aumenta. El gesto es continuo: el sistema rastrea la distancia entre los dedos en tiempo real y genera eventos intermedios, permitiendo que la aplicación actualice suavemente la escala.
La mecánica del gesto se basa en calcular la distancia entre dos puntos de contacto. iOS y Android usan la distancia euclidiana: sqrt((x2−x1)² + (y2−y1)²). La escala actual se calcula como la relación entre la distancia actual y la distancia inicial. Si la distancia inicial era 100px y se convierte en 150px — scale = 1.5 (aumento del 50%). iOS pasa la escala relativa al estado anterior (delta), Android pasa la escala acumulada a través de ScaleGestureDetector.
Según Google Material Design Guidelines, Pinch-to-Zoom es obligatorio para aplicaciones que muestran imágenes, mapas o documentos. En IT Sectr, implementamos este gesto con UIPinchGestureRecognizer y ScaleGestureDetector en todos los proyectos que trabajan con contenido visual — desde galerías hasta editores de imágenes.
UIPinchGestureRecognizer es una subclase continua de UIGestureRecognizer para reconocer el gesto de pellizco. A diferencia de los reconocedores discretos (UITapGestureRecognizer), genera una secuencia de eventos: .began (los dedos tocan la pantalla), .changed (cada movimiento de los dedos), .ended (los dedos se levantan).
Propiedades clave: scale (CGFloat) — cambio relativo en la distancia entre los dedos desde el inicio del gesto; velocity (CGFloat) — tasa de cambio de la escala por segundo. Escala inicial = 1.0. Al separar los dedos, la escala aumenta (1.0 → 1.5 → 2.0); al juntarlos, disminuye (1.0 → 0.8 → 0.5). La propiedad velocity se usa para la inercia: después de .ended, se puede animar la continuación del escalado con desaceleración.
UIPinchGestureRecognizer admite la propiedad delegate para el reconocimiento simultáneo con otros reconocedores (por ejemplo, pan + pinch en un mapa). El método gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) devuelve true, y ambos gestos funcionan en paralelo. Apple recomienda agregar UIPinchGestureRecognizer a la vista contenedora en lugar de a elementos individuales — esto simplifica el cálculo del punto pivote de escalado.
ScaleGestureDetector es una clase de Android Support Library (android.view) diseñada para reconocer el escalado multitáctil. A diferencia de GestureDetector.onFling, ScaleGestureDetector está construido específicamente para el zoom con dos dedos y calcula automáticamente focusX, focusY (el punto central entre los dedos) y scaleFactor (delta de escala del evento anterior).
ScaleGestureDetector funciona a través de la interfaz OnScaleGestureListener con tres métodos: onScaleBegin(detector) (análogo a .began), onScale(detector) (análogo a .changed, devuelve boolean — true si se acepta la escala), onScaleEnd(detector) (análogo a .ended). Propiedades: scaleFactor — multiplicador de escala relativo al evento anterior (~1.01 para separación lenta), focusX/focusY — coordenadas del punto central entre los dedos.
Para aplicar el escalado a una View, normalmente se usa Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix admite la acumulación de transformaciones — cada llamada a postScale agrega una nueva transformación a la existente. Es importante recordar: ScaleGestureDetector no limita la escala mínima/máxima — esa es responsabilidad del desarrollador. En IT Sectr, usamos ScaleGestureDetector con límites de escala personalizados (0.5–3.0) y una animación de retorno al excederlos.
Al implementar Pinch-to-Zoom, es críticamente importante limitar la escala mínima y máxima — sin esto, el usuario podría acercar hasta el infinito o alejar hasta la invisibilidad. Valores estándar: min = 0.5 (50% del tamaño original), max = 3.0 (300%). En iOS, la limitación se hace mediante clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). En Android — mediante una verificación después de postScale.
El punto pivote es el centro entre los dedos, respecto al cual se realiza el escalado. iOS calcula automáticamente el centroide de los dos toques y lo pasa a través de location(in:) — el desarrollador no necesita calcular el pivote manualmente. Android proporciona focusX/focusY en ScaleGestureDetector, pero al aplicar mediante Matrix, el pivote se pasa como tercer y cuarto argumento en postScale(scaleFactor, scaleFactor, focusX, focusY). Sin un punto pivote, el escalado se realizará respecto a la esquina superior izquierda (0,0), lo que provoca que el contenido se desplace durante el zoom.
Según la experiencia de IT Sectr, el cálculo correcto del punto pivote es la fuente más común de errores al implementar Pinch-to-Zoom. En el 90% de los casos, usar las coordenadas integradas de GestureRecognizer y ScaleGestureDetector es suficiente — no es necesario calcular el pivote manualmente. Si el contenido se desplaza al escalar, verifica si estás pasando focusX/focusY a matrix.postScale().
Para una experiencia de usuario fluida después de que termine el gesto, puedes agregar inercia: en iOS — usa velocity de UIPinchGestureRecognizer con UIView.animate y parámetros spring; en Android — OverScroller o ValueAnimator. La inercia no debe superar los límites de escala mín/máx — el valor final se ajusta con una animación de retorno spring al exceder el límite.
Agrega UIPinchGestureRecognizer a UIImageView con limitación de escala de 0.5 a 3.0. Muestra una implementación mínima con preservación de la escala acumulada.
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
}
}
}
Lógica: en .began/.changed, la escala se aplica mediante CGAffineTransform con clamp. En .ended, la escala se restablece a 1.0 y el valor acumulado se guarda en currentScale. Esto evita la deriva de la escala durante gestos repetidos — cada nuevo gesto comienza desde el estado actual. Para imágenes, se recomienda UIScrollView con minimumZoomScale/maximumZoomScale en lugar de CGAffineTransform.
Implementa el escalado de ImageView mediante ScaleGestureDetector con Matrix. Muestra el manejo correcto del punto pivote y la limitación de escala.
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
}
}
El ImageView personalizado sobrescribe onTouchEvent, pasando eventos a ScaleGestureDetector. OnScaleListener.onScale() se llama en cada movimiento de los dedos. detector.scaleFactor es el delta (1.01 para separación lenta). currentScale se acumula con coerceIn para el clamp. focusX/focusY — el punto central entre los dedos, pasado a postScale para un escalado correcto del pivote.
UIScrollView tiene soporte integrado de Pinch-to-Zoom a través de su delegado. Este es el enfoque preferido para imágenes y documentos — ScrollView gestiona la escala, la inercia y el desplazamiento automáticamente.
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 automáticamente un UIPinchGestureRecognizer, gestiona el escalado mediante la transformación de CALayer y añade un efecto de rebote al superar los límites. El código se reduce a establecer min/maxZoomScale y devolver viewForZooming. En IT Sectr, usamos UIScrollView para todas las imágenes a pantalla completa — esto reduce el volumen de código en 5 veces en comparación con la implementación manual mediante UIPinchGestureRecognizer.
Preguntas frecuentes
En una pantalla táctil, pinch usa dos dedos con seguimiento de la distancia entre ellos. En un trackpad de MacBook, el zoom se implementa mediante UIPinchGestureRecognizer con los mismos estados (.began → .changed → .ended), pero la fuente de toque es un trackpad con Force Touch, no una pantalla capacitiva. La API es la misma, la mecánica es idéntica.
En iOS: después de obtener la escala de UIPinchGestureRecognizer, multiplica por currentScale y aplica clamp(min: 0.5, max: 3.0). En Android: usa coerceIn(minScale, maxScale) antes de matrix.postScale(). En UIScrollView — a través de las propiedades minimumZoomScale y maximumZoomScale. Sin limitación, la escala puede llegar al infinito o a cero.
No, el pinch requiere al menos dos dedos. Para el zoom con un solo dedo (doble toque + arrastrar hacia arriba/abajo), se requiere una implementación personalizada mediante UIPanGestureRecognizer o GestureDetector.onScroll. Es un gesto diferente con otra lógica — normalmente se usa para el zoom en la cámara o mapas.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también