Главне тачке
Pinch-to-Zoom (штипање или стискање) — мултитач гест који користи два прста за промену размере приказаног садржаја. При састављању прстију (pinch close) размера се смањује, при раздвајању (pinch open) — повећава. Гест је непрекидан: систем прати растојање између прстију у реалном времену и генерише међу догађаје, омогућавајући апликацији да глатко ажурира размеру.
Механика геста се заснива на израчунавању растојања између две тачке додира. iOS и Android користе Еуклидско растојање: sqrt((x2-x1)² + (y2-y1)²). Тренутни scale се израчунава као однос тренутног растојања и почетног. Ако је почетно растојање било 100px, а постало 150px — scale = 1.5 (повећање од 50%). iOS преноси scale у односу на претходно стање (делта), Android — акумулирани scale кроз ScaleGestureDetector.
Према Google Material Design Guidelines, Pinch-to-Zoom је обавезан за апликације које приказују слике, мапе или документе. У IT Sectr имплементирамо овај гест са UIPinchGestureRecognizer и ScaleGestureDetector у свим пројектима који раде са визуелним садржајем — од галерија до едитора слика.
UIPinchGestureRecognizer — непрекидна подкласа UIGestureRecognizer за препознавање геста штипања. За разлику од дискретних препознавача (UITapGestureRecognizer), генерише секвенцу догађаја: .began (прсти су додирнули екран), .changed (сваки покрет прстију), .ended (прсти су подигнути).
Кључна својства: scale (CGFloat) — релативна промена растојања између прстију од почетка геста; velocity (CGFloat) — брзина промене scale у секунди. Почетни scale = 1.0. При раздвајању прстију scale расте (1.0 → 1.5 → 2.0), при састављању — опада (1.0 → 0.8 → 0.5). Својство velocity се користи за инерцију: после .ended може се анимирати наставак скалирања са пригушењем.
UIPinchGestureRecognizer подржава својство delegate за истовремени рад са другим препознавачима (нпр. pan + pinch на мапи). Метод gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) враћа true, и оба геста раде паралелно. Apple препоручује додавање UIPinchGestureRecognizer на контејнерски view, а не на појединачне елементе — то поједностављује израчунавање пивот тачке скалирања.
ScaleGestureDetector — класа из Android Support Library (android.view), намењена за препознавање мултитач скалирања. За разлику од GestureDetector.onFling, ScaleGestureDetector је специјално дизајниран за зум са два прста и аутоматски израчунава focusX, focusY (центар између прстију) и scaleFactor (делта размере у односу на претходни догађај).
ScaleGestureDetector ради кроз интерфејс OnScaleGestureListener са три метода: onScaleBegin(detector) (еквивалент .began), onScale(detector) (еквивалент .changed, враћа boolean — true ако је размера прихваћена), onScaleEnd(detector) (еквивалент .ended). Својства: scaleFactor — множилац размере у односу на претходни догађај (~1.01 при спором раздвајању), focusX/focusY — координате центра између прстију.
За примену размере на View обично се користи Matrix: view.imageMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY). Matrix подржава акумулацију трансформација — сваки позив postScale додаје нову трансформацију постојећој. Важно је запамтити: ScaleGestureDetector не ограничава минималну/максималну размеру — то је задатак програмера. У IT Sectr примењујемо ScaleGestureDetector са прилагођеним границама scale (0.5–3.0) и анимацијом повратка при прекорачењу.
При имплементацији Pinch-to-Zoom критично је важно ограничити минималну и максималну размеру — без тога корисник може повећати слику до бесконачности или је смањити до невидљивости. Стандардне вредности: min = 0.5 (50% оригиналне величине), max = 3.0 (300%). На iOS ограничење се врши кроз clamp: newScale = min(max(currentScale * gesture.scale, minScale), maxScale). На Android — кроз проверу после postScale.
Пивот тачка — центар између прстију у односу на који се врши скалирање. iOS аутоматски израчунава центар два додира и преноси га кроз location(in:) — програмер не мора да израчунава пивот ручно. Android пружа focusX/focusY у ScaleGestureDetector, али при примени кроз Matrix пивот се преноси као трећи и четврти аргумент у postScale(scaleFactor, scaleFactor, focusX, focusY). Без пивот тачке, скалирање ће се вршити у односу на горњи леви угао (0,0), што доводи до померања садржаја при зуму.
По искуству IT Sectr, правилно израчунавање пивот тачке — најчешћи извор грешака при увођењу Pinch-to-Zoom. У 90% случајева довољно је користити уграђене координате GestureRecognizer и ScaleGestureDetector, без ручног израчунавања пивота. Ако се садржај помера при скалирању — проверите да ли преносите focusX/focusY у matrix.postScale().
За глатки UX после завршетка геста може се додати инерција: на iOS — користите velocity из UIPinchGestureRecognizer за UIView.animate са spring параметрима; на Android — OverScroller или ValueAnimator. Инерција не треба да прелази границе min/max scale — коначна вредност се одсеца са spring анимацијом повратка при прекорачењу.
Додаје UIPinchGestureRecognizer на UIImageView са ограничењем размере од 0.5 до 3.0. Показује минималну имплементацију са чувањем акумулиране размере.
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
}
}
}
Логика: у .began/.changed примењује се размера кроз CGAffineTransform са clamp. У .ended scale се ресетује на 1.0, а акумулирана вредност чува у currentScale. Ово спречава „дрифт" размере при вишеструким гестовима — сваки нови гест почиње из тренутног стања. За слике уместо CGAffineTransform препоручује се UIScrollView са minimumZoomScale/maximumZoomScale.
Имплементира скалирање ImageView кроз ScaleGestureDetector са Matrix. Показује коректну обраду пивот тачке и ограничење размере.
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
}
}
Прилагођени ImageView преписује onTouchEvent, преносећи догађаје ScaleGestureDetector-у. OnScaleListener.onScale() се позива при сваком покрету прстију. detector.scaleFactor — делта (1.01 при спором раздвајању). currentScale се акумулира са coerceIn за clamp. focusX/focusY — центар између прстију, прослеђен postScale-у за коректно пивот скалирање.
UIScrollView има уграђену подршку за Pinch-to-Zoom кроз делегата. Ово је пожељан начин за слике и документе — ScrollView управља размером, инерцијом и померањем аутоматски.
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 самостално креира UIPinchGestureRecognizer, управља размером кроз CALayer трансформацију и додаје bounce ефекат при прекорачењу граница. Код се своди на постављање min/maxZoomScale и враћање viewForZooming. У IT Sectr користимо UIScrollView за све слике преко целог екрана — то 5 пута смањује обим кода у поређењу са ручном имплементацијом кроз UIPinchGestureRecognizer.
Често постављана питања
На осетљивом екрану pinch користи два прста са праћењем растојања између њих. На MacBook трекпаду скалирање је имплементирано кроз UIPinchGestureRecognizer са истим стањима (.began → .changed → .ended), али извор додира је трекпад са Force Touch, а не капацитивни екран. API је исти, механика идентична.
У iOS: након добијања scale од UIPinchGestureRecognizer помножите са currentScale и примените clamp(min: 0.5, max: 3.0). У Android: кроз coerceIn(minScale, maxScale) пре matrix.postScale(). У UIScrollView — кроз својства minimumZoomScale и maximumZoomScale. Без ограничења размера може отићи у бесконачност или нулу.
Не, pinch захтева најмање два прста. За зум једним прстом (double-tap + превлачење горе/доле) потребна је прилагођена имплементација кроз UIPanGestureRecognizer или GestureDetector.onScroll. Ово је другачији гест са другом логиком — обично се користи за зум у камери или мапама.
Резиме
Развићемо мобилну апликацију под кључ
IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође