זום ופאן הם פונקציות בסיסיות לגלריות, מפות וצופים באפליקציות מובייל. עם זאת, טיפול לא נכון במחוות הורס את חוויית המשתמש: תמונות קופצות, יוצאות מגבולות המסך, וקונפליקטים בין צביטה לסחיפה מובילים לתסכול. אנו מיישמים מכניקה זו עם הבטחה ל-60 FPS חלקים, החזרה לאחור (bounce-back), וכיסוי מלא של מקרי קצה. הניסיון שלנו כולל פרויקטים ל-iOS, אנדרואיד, Flutter ו-React Native — מגלריות פשוטות ועד צופים רפואיים ברזולוציה גבוהה. שימוש בפתרונות מוכנים מקצר את זמן הפיתוח ב-40% בהשוואה לבנייה מאפס. החיסכון הממוצע בתקציב עומד על $200–400.
השוואת גישות זום-פאן בין פלטפורמות
| פלטפורמה | ספרייה/API | מזהי מחוות | מגבלת פאן | החזרה לאחור |
|---|---|---|---|---|
| iOS (UIKit) | UIPinchGestureRecognizer + UIPanGestureRecognizer | דורש require(toFail:) |
CGAffineTransform + הגבלה |
מובנה ב-UIScrollView |
| iOS (SwiftUI) | MagnificationGesture + DragGesture (עם CGFloat) |
תמיכה מובנית | onChanged הגבלה דרך withAnimation(.interactiveSpring) |
setOnTouchListener |
| אנדרואיד (Kotlin) | ScaleGestureDetector + GestureDetector (View) או Modifier (Compose) | pointerInput או ScaleGestureDetector.SimpleOnScaleGestureListener |
clamp + ViewPropertyAnimator |
setInterpolator עם Gesture.Pinch() |
| React Native | react-native-gesture-handler + react-native-reanimated | Gesture.Pan() + Gesture.Simultaneous עם clamp() |
withSpring() על ערכים משותפים |
boundaryMargin |
| Flutter | InteractiveViewer | זיהוי מחוות מובנה | maxScale ו-BouncingScrollPhysics |
<Image> |
InteractiveViewer ב-Flutter מיישם זום פי 2 מהר יותר מאשר יישום מותאם אישית באמצעות GestureDetector.
פרטי יישום טכניים
React Native — react-native-gesture-handler + react-native-reanimated. ה-Animated.Image הסטנדרטי אינו תומך בטרנספורמציות דרך מחוות — יש צורך ב-Reanimated או useSharedValue. גישה עם useGestureHandler לסקלה ו-translate X/Y, PinchGesture עבור PanGesture + const scale = useSharedValue(1); const translateX = useSharedValue(0); const translateY = useSharedValue(0); const pinchGesture = Gesture.Pinch() .onUpdate((e) => { scale.value = clamp(savedScale.value * e.scale, 1, 5); }) .onEnd(() => { savedScale.value = scale.value; if (scale.value < 1) { scale.value = withSpring(1); } }); :
const scale = useSharedValue(1);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const pinchGesture = Gesture.Pinch()
.onUpdate((e) => {
scale.value = clamp(savedScale.value * e.scale, 1, 5);
})
.onEnd(() => {
savedScale.value = scale.value;
if (scale.value < 1) {
scale.value = withSpring(1);
}
});Gesture.Simultaneous(pinchGesture, panGesture) — מאפשר לצביטה ופאן לעבוד במקביל. Gesture.Race() — אם יש צורך להפריד ביניהם לפי עדיפות.
הגבלת פאן לגבולות התמונה. בזום x3, תמונה של 375px הופכת ל-1125px. ה-translateX המקסימלי המותר = (scaledWidth - containerWidth) / 2. ללא בדיקה זו, המשתמש יכול לגרור את התמונה מחוץ למסך. בדיקת גבולות מיושמת ב-onUpdate דרך clamp():
const maxTranslateX = (containerWidth * (scale.value - 1)) / 2;
translateX.value = clamp(translateX.value + delta, -maxTranslateX, maxTranslateX);Flutter — InteractiveViewer. ווידג'ט מובנה עם const maxTranslateX = (containerWidth * (scale.value - 1)) / 2; translateX.value = clamp(translateX.value + delta, -maxTranslateX, maxTranslateX); , minScale, maxScale. למקרים בסיסיים, זה מספיק. לגלריה עם מספר תמונות — boundaryMargin בתוך InteractiveViewer, אבל נוצר קונפליקט: סחיפה אופקית להחלפת תמונות לעומת פאן אופקי כשמתבצע זום. פתרון: PageView מיירט פאן רק כאשר InteractiveViewer, ב-scale > 1 המחווה מועברת ל-scale == 1.
iOS native — UIPinchGestureRecognizer + UIPanGestureRecognizer. PageView לפתרון קונפליקטים נכון. gestureRecognizer.require(toFail:) להחלת טרנספורמציות על CGAffineTransform. UIImageView + UIScrollView — חלופה שנותנת החזרה חופשית בגבולות ו-UIScrollViewDelegate.viewForZooming אנימציות. ב-SwiftUI, נעשה שימוש בשילוב של zoomRect ו-MagnificationGesture עם DragGesture. התיעוד של Apple עבור UIPinchGestureRecognizer ממליץ על .simultaneous() לפתרון קונפליקטים.
איך להימנע מקונפליקטי מחוות בפאן?
בגלריות, הבעיה המרכזית היא סחיפה בו-זמנית להחלפת תמונות ופאן לתנועה בתוך התמונה הממוזגת. הפתרון שלנו: require(toFail:) מאפשר שתי מחוות, אבל רק כאשר scale > 1. ב-Gesture.Simultaneous, הפאן חסום, והמחווה מועברת ל-scale == 1 או FlatList. בנוסף, נעשה שימוש ב-PageView ב-handler של מחוות הפאן — הפאן מופעל רק על שינוי אופקי > 10px, מה שמונע הפעלות שווא. גישה זו מפחיתה תלונות משתמשים על שליטה לא נוחה ב-40%.
הקשה כפולה
הקשה כפולה: אם activeOffsetX — זום ל-2–3x בנקודת המגע. אם כבר ממוזג — חזרה ל-scale == 1. אנימציה דרך scale == 1 (לתחושת גומי) או withSpring עם withTiming.
נקודת הזום נקבעת מקואורדינטות ההקשה ביחס לתמונה:
const focalX = tapEvent.x - containerWidth / 2;
const focalY = tapEvent.y - containerHeight / 2;
translateX.value = withSpring(-focalX * (targetScale - 1));
מניסיון: אפליקציית צופה בתמונות רפואיות, React Native. זום על תמונות רנטגן ברזולוציה גבוהה (4096×4096px). באנדרואיד, טעינת התמונה בגודל מלא לרכיב Easing.out(Easing.cubic) גרמה ל-const focalX = tapEvent.x - containerWidth / 2; const focalY = tapEvent.y - containerHeight / 2; translateX.value = withSpring(-focalX * (targetScale - 1)); . פתרון: Image עם OutOfMemoryError לתצוגה מקדימה + טעינה מבוססת אריחים בגודל מלא דרך react-native-fast-image עם תמיכה בפורמט Deep Zoom. כתוצאה מכך, זמן הטעינה ירד ב-60%, וצריכת הזיכרון ירדה פי 3.
השוואת ספריות זום ב-React Native
| ספרייה | תמיכה ברזולוציה גבוהה | החזרה לאחור | קונפליקטי מחוות |
|---|---|---|---|
| react-native-gesture-handler + reanimated | דורש טיפול נוסף | דרך resizeMode="contain" |
react-native-zoom-toolkit |
| react-native-zoom-toolkit | טעינה מבוססת אריחים מובנית | מובנה | מובנה (מתעלם מסחיפה כשממוזג) |
| react-native-image-zoom-viewer | דרך resizeMode | כן (לא ניתן להגדרה) | תמיכה מוגבלת |
יישום זום שלב-אחר-שלב ב-React Native
- ייבוא
withSpring,Gesture.Simultaneousמ-Gestureו-GestureDetector,react-native-gesture-handlerמ-useSharedValue. - יצירת
withSpringעבורreact-native-reanimated,shared values,scaleו-translateX. - הגדרת
translateYעםsavedScaleהמעדכן אתPinchGestureעם הגבלה דרךonUpdate, ו-scaleהשומר את הסקלה. - הגדרת
clamp()עםonEndהמשנה אתPanGestureובודק את גבולות התמונה. - שילוב מחוות דרך
onUpdate. - שימוש ב-
translateX/YעםGesture.Simultaneous(pinch, pan). - להקשה כפולה, הוספת
Animated.Viewעםstyle={{ transform: [{ translateX }, { translateY }, { scale }] }}ואנימציית סקלה. - בדיקה על מכשיר אמיתי, כולל בדיקת ביצועים (60 FPS).
למה אנימציית החזרה לאחור משפרת את חוויית המשתמש?
החזרה לאחור מחזירה את התמונה לגבולות חוקיים כשהמשתמש משחרר את האצבע. בלעדיה, התמונה יכולה "להיתקע" מחוץ למסך, מה שמבלבל את המשתמש. Gesture.Tap() יוצר גמישות טבעית, המדמה התנגדות פיזית. ב-Flutter, זה מושג דרך numberOfTaps: 2, ב-iOS דרך דגל withSpring ב-BouncingScrollPhysics. אנו מבטיחים שהחזרה לאחור מיושמת עם פרמטרי אנימציה אופטימליים כדי לא להטריד את המשתמש.
מה כלול בעבודה
- צביטה לזום עם מגבלות סקלה מינימליות/מקסימליות (בדרך כלל 1x–5x, ניתן להגדרה לפי משימה)
- פאן כשממוזג עם מגבלות גבולות
- הקשה כפולה — זום פנימה/החוצה עם אנימציה לנקודת המגע
- החזרה לאחור כשחורגים מהגבולות
- אינטגרציה לגלריה/קרוסלה עם פתרון נכון לקונפליקטי מחוות
- תמיכה בתמונות ברזולוציה גבוהה ללא OutOfMemoryError (דרך טעינה מבוססת אריחים או fast-image)
- אופטימיזציית ביצועים ל-60 FPS במכשירים בינוניים
מידע נוסף על גבולות פאן
חישוב הגבולות תלוי בקונטיינר ובתמונה המוקטנת. לדוגמה, עבור קונטיינר ברוחב 375px ותמונה של 1000px בסקלה=3, רוחב התמונה הוא 3000px, תוספת של 2625px אופקית המחולקת לחצי — maxTranslateX = 1312.5px. באופן דומה לאנכי. אם התמונה קטנה מהקונטיינר, הפאן חסום לחלוטין.לוחות זמנים
1–3 ימי עסקים — תמונה בודדת עם זום. עם גלריה ופתרון קונפליקטי מחוות — 2–3 ימים. העלות מחושבת באופן אישי. בממוצע, יישום זום עם פאן אורך 3 עד 5 ימי עסקים, ומתאים לספרינט טיפוסי. צרו קשר כדי לדון בפרויקט שלכם — נמצא את הפתרון האופטימלי לסטאק ולתקציב שלכם. הזמינו יישום של זום ופאן חלקים כדי להפוך את האפליקציה שלכם לבולטת מול המתחרים. קבלו ייעוץ לפרויקט שלכם עוד היום.







