النقاط الرئيسية
Pinch-to-Zoom (القرص) هي إيماءة متعددة اللمس تستخدم إصبعين لتغيير مقياس المحتوى المعروض. عند تقريب الأصابع (pinch close)، يقل المقياس؛ عند تباعدها (pinch open)، يزداد. الإيماءة مستمرة: يتتبع النظام المسافة بين الأصابع في الوقت الفعلي ويولد أحداثاً وسيطة، مما يسمح للتطبيق بتحديث المقياس بسلاسة.
تعتمد ميكانيكية الإيماءة على حساب المسافة بين نقطتي اللمس. يستخدم iOS و Android المسافة الإقليدية: sqrt((x2−x1)² + (y2−y1)²). يُحسب المقياس الحالي كنسبة المسافة الحالية إلى المسافة الأولية. إذا كانت المسافة الأولية 100px وأصبحت 150px — يكون المقياس 1.5 (زيادة بنسبة 50%). يمرر iOS المقياس نسبة إلى الحالة السابقة (delta)، ويمرر Android المقياس المتراكم عبر ScaleGestureDetector.
وفقاً لـ Google Material Design Guidelines، فإن Pinch-to-Zoom إلزامي للتطبيقات التي تعرض الصور أو الخرائط أو المستندات. في IT Sectr، ننفذ هذه الإيماءة باستخدام UIPinchGestureRecognizer و ScaleGestureDetector في جميع المشاريع التي تعمل مع المحتوى المرئي — من المعارض إلى محرري الصور.
UIPinchGestureRecognizer هو فئة فرعية مستمرة من UIGestureRecognizer للتعرف على إيماءة القرص. على عكس المعرفات المنفصلة (UITapGestureRecognizer)، فإنه يُنشئ سلسلة من الأحداث: .began (لمست الأصابع الشاشة)، .changed (كل حركة للأصابع)، .ended (رفعت الأصابع).
الخصائص الرئيسية: scale (CGFloat) — التغيير النسبي في المسافة بين الأصابع منذ بداية الإيماءة؛ velocity (CGFloat) — معدل تغيير المقياس في الثانية. المقياس الأولي = 1.0. عند تباعد الأصابع، يزداد المقياس (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 بحدود مقياس مخصصة (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().
لتجربة مستخدم سلسة بعد انتهاء الإيماءة، يمكنك إضافة القصور الذاتي: في iOS — استخدم velocity من UIPinchGestureRecognizer مع UIView.animate ومعاملات spring؛ في Android — OverScroller أو ValueAnimator. يجب ألا يتجاوز القصور الذاتي حدود المقياس الأدنى/الأقصى — تُقطع القيمة النهائية مع رسم متحرك للعودة 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، يُعاد تعيين المقياس إلى 1.0 وتُخزن القيمة المتراكمة في currentScale. يمنع هذا انحراف المقياس أثناء الإيماءات المتكررة — كل إيماءة جديدة تبدأ من الحالة الحالية. للصور، يُوصى باستخدام UIScrollView مع minimumZoomScale/maximumZoomScale بدلاً من CGAffineTransform.
ينفذ تكبير 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 للتحديد. 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، ويضيف تأثير الارتداد عند تجاوز الحدود. يختزل الكود إلى تعيين min/maxZoomScale وإرجاع viewForZooming. في IT Sectr، نستخدم UIScrollView لجميع الصور بملء الشاشة — وهذا يقلل حجم الكود بمقدار 5 مرات مقارنة بالتنفيذ اليدوي عبر UIPinchGestureRecognizer.
الأسئلة الشائعة
على الشاشة التي تعمل باللمس، يستخدم القرص إصبعين مع تتبع المسافة بينهما. على لوحة اللمس في MacBook، يتم تنفيذ التكبير عبر UIPinchGestureRecognizer بنفس الحالات (.began → .changed → .ended)، ولكن مصدر اللمس هو لوحة لمس مع Force Touch، وليست شاشة سعوية. API هو نفسه، الميكانيكية متطابقة.
في iOS: بعد الحصول على المقياس من UIPinchGestureRecognizer، اضرب في currentScale وطبق clamp(min: 0.5, max: 3.0). في Android: استخدم coerceIn(minScale, maxScale) قبل matrix.postScale(). في UIScrollView — من خلال خصائص minimumZoomScale و maximumZoomScale. بدون تحديد، يمكن أن يصل المقياس إلى ما لا نهاية أو الصفر.
لا، يتطلب القرص على الأقل إصبعين. للتكبير بإصبع واحد (نقر مزدوج + سحب لأعلى/لأسفل)، يلزم تنفيذ مخصص عبر UIPanGestureRecognizer أو GestureDetector.onScroll. هذه إيماءة مختلفة بمنطق مختلف — تُستخدم عادة للتكبير في الكاميرا أو الخرائط.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا