Belangrijkste punten
Pinch-to-Zoom (knijpen of samenknijpen) — een multi-touch gebaar dat twee vingers gebruikt om de schaal van weergegeven inhoud te wijzigen. Bij het samenbrengen van vingers (pinch close) wordt de schaal kleiner, bij het spreiden (pinch open) — groter. Het gebaar is continu: het systeem volgt de afstand tussen de vingers in realtime en genereert tussentijdse gebeurtenissen, zodat de app de schaal soepel kan bijwerken.
De mechanica van het gebaar is gebaseerd op het berekenen van de afstand tussen twee aanraakpunten. iOS en Android gebruiken de Euclidische afstand: sqrt((x2-x1)² + (y2-y1)²). De huidige scale wordt berekend als de verhouding van de huidige afstand tot de beginwaarde. Als de beginwaarde 100px was en 150px werd — scale = 1.5 (50% toename). iOS geeft scale door ten opzichte van de vorige toestand (delta), Android — geaccumuleerde scale via ScaleGestureDetector.
Volgens Google Material Design Guidelines is Pinch-to-Zoom verplicht voor apps die afbeeldingen, kaarten of documenten weergeven. Bij IT Sectr implementeren we dit gebaar met UIPinchGestureRecognizer en ScaleGestureDetector in alle projecten die met visuele inhoud werken — van galerijen tot beeldbewerkers.
UIPinchGestureRecognizer — een continue subklasse van UIGestureRecognizer voor het herkennen van het knijpgebaar. In tegenstelling tot discrete herkenners (UITapGestureRecognizer) genereert het een reeks gebeurtenissen: .began (vingers raakten het scherm), .changed (elke beweging van vingers), .ended (vingers opgeheven).
Belangrijkste eigenschappen: scale (CGFloat) — relatieve verandering van de afstand tussen vingers sinds het begin van het gebaar; velocity (CGFloat) — snelheid van scale-verandering per seconde. Begin-scale = 1.0. Bij het spreiden van vingers neemt scale toe (1.0 → 1.5 → 2.0), bij samenbrengen — af (1.0 → 0.8 → 0.5). De eigenschap velocity wordt gebruikt voor traagheid: na .ended kan men het schalen met demping animeren.
UIPinchGestureRecognizer ondersteunt de eigenschap delegate voor gelijktijdig werken met andere herkenners (bijv. pan + pinch op een kaart). De methode gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) geeft true terug en beide gebaren werken parallel. Apple raadt aan om UIPinchGestureRecognizer toe te voegen aan een container view, niet aan afzonderlijke elementen — dit vereenvoudigt de berekening van het pivotpunt van schalen.
ScaleGestureDetector — een klasse uit Android Support Library (android.view), bedoeld voor het herkennen van multi-touch schalen. In tegenstelling tot GestureDetector.onFling is ScaleGestureDetector speciaal ontworpen voor twee-vinger zoom en berekent automatisch focusX, focusY (midden tussen vingers) en scaleFactor (delta van schaal ten opzichte van vorige gebeurtenis).
ScaleGestureDetector werkt via de interface OnScaleGestureListener met drie methoden: onScaleBegin(detector) (equivalent van .began), onScale(detector) (equivalent van .changed, geeft boolean terug — true als schaal is geaccepteerd), onScaleEnd(detector) (equivalent van .ended). Eigenschappen: scaleFactor — vermenigvuldiger van schaal ten opzichte van vorige gebeurtenis (~1.01 bij langzaam spreiden), focusX/focusY — coördinaten van het midden tussen vingers.
Voor het toepassen van schaal op View wordt meestal Matrix gebruikt: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix ondersteunt accumulatie van transformaties — elke postScale-aanroep voegt een nieuwe transformatie toe aan de bestaande. Belangrijk: ScaleGestureDetector beperkt de minimale/maximale schaal niet — dit is de taak van de ontwikkelaar. Bij IT Sectr passen we ScaleGestureDetector toe met aangepaste scale-grenzen (0.5–3.0) en terugkeeranimatie bij overschrijding.
Bij de implementatie van Pinch-to-Zoom is het beperken van de minimale en maximale schaal van cruciaal belang — zonder dit kan de gebruiker de afbeelding oneindig vergroten of tot onzichtbaarheid verkleinen. Standaardwaarden: min = 0.5 (50% van originele grootte), max = 3.0 (300%). In iOS gebeurt de beperking via clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). In Android — via controle na postScale.
Het pivotpunt — het midden tussen de vingers ten opzichte waarvan het schalen wordt uitgevoerd. iOS berekent automatisch het middelpunt van twee aanrakingen en geeft het door via location(in:) — de ontwikkelaar hoeft pivot niet handmatig te berekenen. Android biedt focusX/focusY in ScaleGestureDetector, maar bij toepassing via Matrix wordt pivot doorgegeven als derde en vierde argument in postScale(scaleFactor, scaleFactor, focusX, focusY). Zonder pivotpunt wordt het schalen uitgevoerd ten opzichte van de linkerbovenhoek (0,0), wat leidt tot verschuiving van inhoud bij zoom.
Volgens ervaring van IT Sectr is de juiste berekening van het pivotpunt de meest voorkomende bron van bugs bij de implementatie van Pinch-to-Zoom. In 90% van de gevallen is het voldoende om de ingebouwde coördinaten van GestureRecognizer en ScaleGestureDetector te gebruiken, zonder pivot handmatig te berekenen. Als de inhoud verschuift bij schalen — controleer of u focusX/focusY doorgeeft aan matrix.postScale().
Voor een soepele UX na het einde van het gebaar kan traagheid worden toegevoegd: in iOS — gebruik velocity van UIPinchGestureRecognizer voor UIView.animate met spring-parameters; in Android — OverScroller of ValueAnimator. Traagheid mag niet buiten de min/max scale-grenzen komen — de uiteindelijke waarde wordt afgekapt met spring-terugkeeranimatie bij overschrijding.
Voegt UIPinchGestureRecognizer toe aan UIImageView met schaalbeperking van 0.5 tot 3.0. Toont een minimale implementatie met behoud van geaccumuleerde schaal.
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 wordt schaal toegepast via CGAffineTransform met clamp. In .ended wordt scale gereset naar 1.0 en de geaccumuleerde waarde opgeslagen in currentScale. Dit voorkomt „drift“ van schaal bij meerdere gebaren — elk nieuw gebaar begint vanuit de huidige toestand. Voor afbeeldingen wordt in plaats van CGAffineTransform UIScrollView met minimumZoomScale/maximumZoomScale aanbevolen.
Implementeert het schalen van ImageView via ScaleGestureDetector met Matrix. Toont correcte verwerking van het pivotpunt en schaalbeperking.
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
}
}
Aangepaste ImageView overschrijft onTouchEvent en geeft gebeurtenissen door aan ScaleGestureDetector. OnScaleListener.onScale() wordt aangeroepen bij elke vingerbeweging. detector.scaleFactor — delta (1.01 bij langzaam spreiden). currentScale accumuleert met coerceIn voor clamp. focusX/focusY — midden tussen vingers, doorgegeven aan postScale voor correct pivot-schalen.
UIScrollView heeft ingebouwde ondersteuning voor Pinch-to-Zoom via een delegatie. Dit is de geprefereerde methode voor afbeeldingen en documenten — ScrollView beheert schaal, traagheid en scrollen automatisch.
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 maakt zelfstandig UIPinchGestureRecognizer aan, beheert schaal via CALayer-transformatie en voegt een bounce-effect toe bij overschrijding van grenzen. De code beperkt zich tot het instellen van min/maxZoomScale en het retourneren van viewForZooming. Bij IT Sectr gebruiken we UIScrollView voor alle volledige-scherm afbeeldingen — dit vermindert de codehoeveelheid met 5 keer in vergelijking met handmatige implementatie via UIPinchGestureRecognizer.
Veelgestelde vragen
Op een aanraakscherm gebruikt pinch twee vingers met het volgen van de afstand ertussen. Op een MacBook-trackpad wordt schalen geïmplementeerd via UIPinchGestureRecognizer met dezelfde toestanden (.began → .changed → .ended), maar de bron van aanrakingen is het trackpad met Force Touch, niet een capacitief scherm. De API is hetzelfde, de mechanica identiek.
In iOS: vermenigvuldig scale van UIPinchGestureRecognizer met currentScale en pas clamp(min: 0.5, max: 3.0) toe. In Android: via coerceIn(minScale, maxScale) vóór matrix.postScale(). In UIScrollView — via de eigenschappen minimumZoomScale en maximumZoomScale. Zonder beperking kan de schaal naar oneindig of nul gaan.
Nee, pinch vereist minimaal twee vingers. Voor zoom met één vinger (double-tap + slepen omhoog/omlaag) is een aangepaste implementatie nodig via UIPanGestureRecognizer of GestureDetector.onScroll. Dit is een ander gebaar met een andere logica — meestal gebruikt voor zoom in camera of kaarten.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook