Key Takeaways
Pinch-to-Zoom (finger pinch) is a multi-touch gesture that uses two fingers to change the scale of displayed content. When bringing fingers together (pinch close), the scale decreases; when spreading them apart (pinch open), it increases. The gesture is continuous: the system tracks the distance between fingers in real time and generates intermediate events, allowing the application to smoothly update the scale.
The mechanics of the gesture are based on calculating the distance between two touch points. iOS and Android use Euclidean distance: sqrt((x2−x1)² + (y2−y1)²). The current scale is calculated as the ratio of the current distance to the initial distance. If the initial distance was 100px and becomes 150px — scale = 1.5 (50% increase). iOS passes scale relative to the previous state (delta), Android passes accumulated scale through ScaleGestureDetector.
According to Google Material Design Guidelines, Pinch-to-Zoom is mandatory for applications displaying images, maps, or documents. At IT Sectr, we implement this gesture with UIPinchGestureRecognizer and ScaleGestureDetector in all projects that work with visual content — from galleries to image editors.
UIPinchGestureRecognizer is a continuous subclass of UIGestureRecognizer for recognizing the pinch gesture. Unlike discrete recognizers (UITapGestureRecognizer), it generates a sequence of events: .began (fingers touch the screen), .changed (each finger movement), .ended (fingers are lifted).
Key properties: scale (CGFloat) — relative change in distance between fingers since the start of the gesture; velocity (CGFloat) — rate of scale change per second. Initial scale = 1.0. When spreading fingers, scale increases (1.0 → 1.5 → 2.0); when bringing together, it decreases (1.0 → 0.8 → 0.5). The velocity property is used for inertia: after .ended, you can animate continued scaling with deceleration.
UIPinchGestureRecognizer supports the delegate property for simultaneous recognition with other recognizers (e.g., pan + pinch on a map). The method gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) returns true, and both gestures work in parallel. Apple recommends adding UIPinchGestureRecognizer to the container view rather than individual elements — this simplifies calculating the scaling pivot point.
ScaleGestureDetector is a class from the Android Support Library (android.view) designed for recognizing multi-touch scaling. Unlike GestureDetector.onFling, ScaleGestureDetector is specifically built for two-finger zoom and automatically calculates focusX, focusY (the center point between fingers) and scaleFactor (scale delta from the previous event).
ScaleGestureDetector works through the OnScaleGestureListener interface with three methods: onScaleBegin(detector) (analogous to .began), onScale(detector) (analogous to .changed, returns boolean — true if the scale is accepted), onScaleEnd(detector) (analogous to .ended). Properties: scaleFactor — scale multiplier relative to the previous event (~1.01 for slow spreading), focusX/focusY — coordinates of the center point between fingers.
To apply scaling to a View, Matrix is typically used: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix supports accumulating transformations — each postScale call adds a new transformation to the existing one. It is important to remember: ScaleGestureDetector does not limit minimum/maximum scale — that is the developer’s responsibility. At IT Sectr, we use ScaleGestureDetector with custom scale boundaries (0.5–3.0) and a return animation when exceeded.
When implementing Pinch-to-Zoom, it is critically important to limit the minimum and maximum scale — without this, a user could zoom in to infinity or zoom out to invisibility. Standard values: min = 0.5 (50% of original size), max = 3.0 (300%). On iOS, limiting is done via clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). On Android — through a check after postScale.
The pivot point is the center point between fingers, relative to which scaling is performed. iOS automatically calculates the centroid of the two touches and passes it through location(in:) — the developer does not need to compute the pivot manually. Android provides focusX/focusY in ScaleGestureDetector, but when applying via Matrix, the pivot is passed as the third and fourth arguments in postScale(scaleFactor, scaleFactor, focusX, focusY). Without a pivot point, scaling will be performed relative to the top-left corner (0,0), which causes content to shift during zoom.
Based on IT Sectr’s experience, correct pivot point calculation is the most common source of bugs when implementing Pinch-to-Zoom. In 90% of cases, using the built-in coordinates of GestureRecognizer and ScaleGestureDetector is sufficient — no need to compute the pivot manually. If content shifts during scaling, check whether you are passing focusX/focusY to matrix.postScale().
For a smooth UX after the gesture ends, you can add inertia: on iOS — use velocity from UIPinchGestureRecognizer with UIView.animate and spring parameters; on Android — OverScroller or ValueAnimator. Inertia should not exceed the min/max scale boundaries — the final value is clamped with a spring-return animation upon exceeding the limit.
Adds UIPinchGestureRecognizer to UIImageView with scale limiting from 0.5 to 3.0. Demonstrates a minimal implementation with accumulated scale preservation.
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
}
}
}
Logic: in .began/.changed, scale is applied via CGAffineTransform with clamp. In .ended, scale is reset to 1.0, and the accumulated value is stored in currentScale. This prevents scale drift during repeated gestures — each new gesture starts from the current state. For images, UIScrollView with minimumZoomScale/maximumZoomScale is recommended instead of CGAffineTransform.
Implements ImageView scaling via ScaleGestureDetector with Matrix. Demonstrates correct pivot point handling and scale limiting.
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
}
}
The custom ImageView overrides onTouchEvent, passing events to ScaleGestureDetector. OnScaleListener.onScale() is called on each finger movement. detector.scaleFactor is the delta (1.01 for slow spreading). currentScale accumulates with coerceIn for clamping. focusX/focusY — the center point between fingers, passed to postScale for correct pivot scaling.
UIScrollView has built-in Pinch-to-Zoom support through its delegate. This is the preferred approach for images and documents — ScrollView manages scale, inertia, and scrolling automatically.
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 automatically creates a UIPinchGestureRecognizer, manages scaling via CALayer transformation, and adds a bounce effect when boundaries are exceeded. The code reduces to setting min/maxZoomScale and returning viewForZooming. At IT Sectr, we use UIScrollView for all full-screen images — this reduces code volume by 5x compared to manual implementation via UIPinchGestureRecognizer.
Frequently Asked Questions
On a touch screen, pinch uses two fingers with distance tracking between them. On a MacBook trackpad, zoom is implemented through UIPinchGestureRecognizer with the same states (.began → .changed → .ended), but the touch source is a Force Touch trackpad, not a capacitive screen. The API is the same, the mechanics are identical.
On iOS: after getting the scale from UIPinchGestureRecognizer, multiply by currentScale and apply clamp(min: 0.5, max: 3.0). On Android: use coerceIn(minScale, maxScale) before matrix.postScale(). In UIScrollView — through the minimumZoomScale and maximumZoomScale properties. Without limiting, the scale can go to infinity or zero.
No, pinch requires at least two fingers. For single-finger zoom (double-tap + drag up/down), a custom implementation via UIPanGestureRecognizer or GestureDetector.onScroll is required. This is a different gesture with different logic — typically used for zoom in the camera or maps.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also