نکات اصلی
Pinch-to-Zoom (نیشگون یا فشار) — یک حرکت چندلمسی است که از دو انگشت برای تغییر مقیاس محتوای نمایش داده شده استفاده میکند. هنگام نزدیک کردن انگشتان (pinch close) مقیاس کاهش مییابد، هنگام باز کردن (pinch open) — افزایش مییابد. حرکت پیوسته است: سیستم فاصله بین انگشتان را در زمان واقعی ردیابی میکند و رویدادهای میانی تولید میکند و به برنامه اجازه میدهد مقیاس را به آرامی بهروزرسانی کند.
مکانیک حرکت بر اساس محاسبه فاصله بین دو نقطه تماس است. iOS و Android از فاصله اقلیدسی استفاده میکنند: sqrt((x2-x1)² + (y2-y1)²). مقیاس فعلی به عنوان نسبت فاصله فعلی به فاصله اولیه محاسبه میشود. اگر فاصله اولیه 100px بود و 150px شد — scale = 1.5 (افزایش 50%). iOS scale را نسبت به حالت قبلی (delta) منتقل میکند، 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 را به نمای ظرف اضافه کنید نه به عناصر جداگانه — این کار محاسبه نقطه محوری بزرگنمایی را ساده میکند.
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:) منتقل میکند — توسعهدهنده نیازی به محاسبه دستی pivot ندارد. Android focusX/focusY را در ScaleGestureDetector فراهم میکند، اما هنگام اعمال از طریق Matrix pivot به عنوان آرگومان سوم و چهارم در postScale(scaleFactor, scaleFactor, focusX, focusY) منتقل میشود. بدون نقطه محوری، بزرگنمایی نسبت به گوشه بالا سمت چپ (0,0) انجام میشود که منجر به جابجایی محتوا هنگام زوم میشود.
بر اساس تجربه IT Sectr، محاسبه صحیح نقطه محوری — رایجترین منبع اشکال در پیادهسازی Pinch-to-Zoom است. در 90% موارد استفاده از مختصات داخلی GestureRecognizer و ScaleGestureDetector کافی است بدون محاسبه دستی pivot. اگر محتوا هنگام بزرگنمایی جابجا میشود — بررسی کنید که 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 از طریق delegate است. این روش ترجیحی برای تصاویر و اسناد است — 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 برای تمام تصاویر تمام صفحه استفاده میکنیم — این حجم کد را در مقایسه با پیادهسازی دستی از طریق UIPinchGestureRecognizer 5 برابر کاهش میدهد.
سوالات متداول
در صفحه لمسی 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 از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید