אם אפליקציית Android שלכם משתמשת במצלמות, יש כמה שיקולים מיוחדים שצריך לקחת בחשבון כשמטפלים בכיוונים. ההנחה במסמך הזה היא שיש לכם הבנה בסיסית במושגים של camera2 API ל-Android. בפוסט בבלוג או בסיכום שלנו אפשר לקרוא סקירה כללית על camera2. מומלץ גם לנסות לכתוב אפליקציית מצלמה לפני שקוראים את המאמר הזה.
רקע
הטיפול בכיוונים באפליקציות מצלמה ל-Android הוא מסובך וצריך לקחת בחשבון את הגורמים הבאים:
- התמצאות טבעית: כיוון התצוגה כשהמכשיר נמצא במיקום 'הרגיל' שלו בהתאם לעיצוב – בדרך כלל תצוגה לאורך בטלפונים ניידים ותצוגה לרוחב במחשבים ניידים.
- כיוון החיישן: הכיוון של החיישן שמותקן פיזית במכשיר.
- סיבוב המסך: כמה המכשיר מסובב פיזית מהכיוון הטבעי.
- גודל העינית: הגודל של העינית שמשמשת להצגת התצוגה המקדימה של המצלמה.
- גודל התמונה שמופק על ידי המצלמה.
השילוב של הגורמים האלה יוצר מספר רב של אפשרויות להגדרות של ממשק המשתמש ותצוגה מקדימה באפליקציות מצלמה. המטרה של המסמך הזה היא להראות למפתחים איך לנווט בין ההגדרות האלה ולטפל בצורה נכונה בכיווני המצלמה באפליקציות ל-Android.
כדי לפשט את הדברים, נניח שכל הדוגמאות מתייחסות למצלמה אחורית, אלא אם צוין אחרת. בנוסף, כל התמונות הבאות הן סימולציות שנועדו להבהיר את האיורים.
הסבר על כיוונים
כיוון טבעי
האוריינטציה הטבעית מוגדרת כאוריינטציית התצוגה כשהמכשיר נמצא במיקום שבו הוא אמור להיות בדרך כלל. בטלפונים, האוריינטציה הטבעית היא לרוב לאורך. במילים אחרות, הטלפונים רחבים פחות וגבוהים יותר. במחשבים ניידים, האוריינטציה הטבעית היא לרוחב, כלומר הרוחב ארוך יותר והגובה קצר יותר. בטאבלטים המצב קצת יותר מורכב – הם יכולים להיות לאורך או לרוחב.
כיוון החיישן
מבחינה פורמלית, כיוון החיישן נמדד לפי המעלות שבהן צריך לסובב את תמונת הפלט מהחיישן בכיוון השעון כדי שתתאים לכיוון הטבעי של המכשיר. במילים אחרות, אוריינטציית החיישן היא מספר המעלות שבהן החיישן מסובב נגד כיוון השעון לפני שהוא מותקן במכשיר. כשמסתכלים על המסך, נראה שהסיבוב הוא בכיוון השעון, כי חיישן המצלמה האחורית מותקן בצד 'האחורי' של המכשיר.
לפי הגדרת התאימות של Android 10 7.5.5 כיוון המצלמה, המצלמות הקדמיות והאחוריות "חייבות להיות מכוונות כך שהמימד הארוך של המצלמה יהיה מיושר עם המימד הארוך של המסך".
מאגרי הפלט מהמצלמות הם בגודל לרוחב. מכיוון שהכיוון הטבעי של הטלפונים הוא בדרך כלל אנכי, הכיוון של החיישן הוא בדרך כלל 90 או 270 מעלות מהכיוון הטבעי, כדי שהצד הארוך של מאגר הפלט יתאים לצד הארוך של המסך. הכיוון של החיישן שונה במכשירים שהכיוון הטבעי שלהם הוא לרוחב, כמו מכשירי Chromebook. במכשירים האלה, חיישני התמונה ממוקמים שוב כך שהצד הארוך של מאגר הפלט תואם לצד הארוך של המסך. מכיוון ששני הגדלים הם לרוחב, הכיוונים זהים והכיוון של החיישן הוא 0 או 180 מעלות.
באיורים הבאים אפשר לראות איך הדברים נראים מנקודת המבט של צופה שמסתכל על מסך המכשיר:
נניח שיש לכם את הסצנה הבאה:
| טלפון | מחשב נייד |
|---|---|
![]() |
![]() |
בטלפונים, כיוון החיישן הוא בדרך כלל 90 או 270 מעלות. לכן, בלי להתחשב בכיוון החיישן, התמונות שיתקבלו ייראו כך:
| טלפון | מחשב נייד |
|---|---|
![]() |
![]() |
נניח שכיוון החיישן נגד כיוון השעון מאוחסן במשתנה sensorOrientation. כדי לפצות על כיוון החיישן, צריך לסובב את מאגרי הפלט ב `sensorOrientation` עם כיוון השעון כדי להחזיר את הכיוון להתאמה לכיוון הטבעי של המכשיר.
ב-Android, אפליקציות יכולות להשתמש ב-TextureView או ב-SurfaceView כדי להציג את התצוגה המקדימה של המצלמה. שניהם יכולים לטפל בכיוון החיישן אם האפליקציות משתמשות בהם בצורה נכונה. בקטעים הבאים נסביר איך צריך להתייחס לכיוון החיישן.
סיבוב מסך
סיבוב המסך מוגדר באופן רשמי על ידי הסיבוב של הגרפיקה שמצוירת על המסך, שהוא בכיוון ההפוך לסיבוב הפיזי של המכשיר מהאוריינטציה הטבעית שלו. בקטעים הבאים נניח שכל סיבובי המסך הם כפולות של 90. אם מאחזרים את סיבוב המסך לפי מעלות אבסולוטיות, מעגלים אותו כלפי מעלה לערך הקרוב ביותר מתוך {0, 90, 180, 270}.
המונח 'כיוון התצוגה' שמופיע בקטעים הבאים מתייחס לכיוון שבו מחזיקים את המכשיר – לרוחב או לאורך – והוא שונה מהמונח 'סיבוב התצוגה'.
נניח שאתם מסובבים את המכשירים ב-90 מעלות נגד כיוון השעון מהמיקומים הקודמים שלהם, כמו שמוצג באיור הבא:
בהנחה שמאגרי הפלט כבר מסובבים על סמך כיוון החיישן, מאגרי הפלט שיתקבלו יהיו:
| טלפון | מחשב נייד |
|---|---|
![]() |
![]() |
אם סיבוב התצוגה מאוחסן במשתנה displayRotation, כדי לקבל את התמונה הנכונה צריך לסובב את מאגרי הפלט נגד כיוון השעון לפי displayRotation.
במצלמות קדמיות, סיבוב התצוגה פועל על מאגרי התמונות בכיוון ההפוך ביחס למסך. אם מדובר במצלמה קדמית, צריך לסובב את המאגרים לפי displayRotation בכיוון השעון.
נקודות שחשוב לדעת
סיבוב המסך מודד את הסיבוב של המכשיר נגד כיוון השעון. זה לא נכון לגבי כל ממשקי ה-API של הכיוון או הסיבוב.
לדוגמה,
-
אם אתם משתמשים ב-
Display#getRotation(), תקבלו סיבוב נגד כיוון השעון כמו שמתואר במסמך הזה. - אם משתמשים ב-OrientationEventListener#onOrientationChanged(int), מקבלים במקום זאת את הסיבוב עם כיוון השעון.
חשוב לשים לב שהרוטציה של המסך היא ביחס לאוריינטציה הטבעית. לדוגמה, אם מסובבים טלפון פיזית ב-90 או ב-270 מעלות, מקבלים מסך בצורת לרוחב. לעומת זאת, אם תסובבו מחשב נייד באותה מידה, תקבלו מסך בצורת פורטרט. מפתחים צריכים תמיד לזכור את זה ולא להניח הנחות לגבי הכיוון הטבעי של המכשיר.
דוגמאות
נשתמש בנתונים הקודמים כדי להמחיש את הכיוונים והסיבובים.
| טלפון | מחשב נייד |
|---|---|
| כיוון טבעי = לאורך | כיוון טבעי = לרוחב |
| Sensor Orientation = 90 | כיוון החיישן = 0 |
| סיבוב מסך = 0 | סיבוב מסך = 0 |
| כיוון המסך = לאורך | כיוון המסך = לרוחב |
| טלפון | מחשב נייד |
|---|---|
| כיוון טבעי = לאורך | כיוון טבעי = לרוחב |
| Sensor Orientation = 90 | כיוון החיישן = 0 |
| סיבוב המסך = 90 | סיבוב המסך = 90 |
| כיוון המסך = לרוחב | כיוון המסך = לאורך |
גודל העינית
האפליקציות צריכות לשנות את הגודל של העינית בהתאם לכיוון, לסיבוב ולרזולוציית המסך. באופן כללי, הכיוון של העינית באפליקציות צריך להיות זהה לכיוון הנוכחי של המסך. במילים אחרות, האפליקציות צריכות ליישר את הקצה הארוך של העדשה עם הקצה הארוך של המסך.
גודל פלט התמונה לפי המצלמה
כשבוחרים את גודל הפלט של התמונה לתצוגה המקדימה, כדאי לבחור גודל ששווה לגודל של העינית או קצת יותר גדול ממנה, אם אפשר. בדרך כלל לא כדאי להגדיל את מאגרי הפלט, כי זה עלול לגרום לפיקסול. לא מומלץ לבחור גודל גדול מדי, כי זה עלול לפגוע בביצועים ולצרוך יותר סוללה.
כיוון JPEG
נתחיל עם מצב נפוץ – צילום תמונה בפורמט JPEG. ב-camera2 API, אפשר להעביר את JPEG_ORIENTATION בבקשת הצילום כדי לציין בכמה מעלות רוצים לסובב את קובצי ה-JPEG של הפלט עם כיוון השעון.
סיכום קצר של מה שציינו:
-
כדי לטפל בכיוון החיישן, צריך לסובב את מאגר התמונות ב-
sensorOrientationבכיוון השעון. -
כדי לטפל בסיבוב התצוגה, צריך לסובב את המאגר ב-
displayRotationנגד כיוון השעון עבור מצלמות אחוריות, ועם כיוון השעון עבור מצלמות קדמיות.
הסכום של שני הגורמים שווה לזווית שבה רוצים לסובב את הצורה עם כיוון השעון.
-
sensorOrientation - displayRotationלמצלמות אחוריות. -
sensorOrientation + displayRotationלמצלמות קדמיות.
אפשר לראות את דוגמת הקוד של הלוגיקה הזו בתיעוד של JPEG_ORIENTATION. הערה: בדוגמה של הקוד במסמכים, deviceOrientation משתמש בסיבוב המכשיר עם כיוון השעון. לכן הסימנים של סיבוב המסך הפוכים.
תצוגה מקדימה
מה לגבי התצוגה המקדימה של המצלמה? יש 2 דרכים עיקריות שבהן אפליקציה יכולה להציג תצוגה מקדימה של המצלמה: SurfaceView ו-TextureView. לכל אחד מהם נדרשת גישה שונה כדי לטפל בכיוון בצורה נכונה.
SurfaceView
בדרך כלל מומלץ להשתמש ב-SurfaceView לתצוגות מקדימות של המצלמה, בתנאי שלא צריך לעבד או להנפיש את מאגרי התצוגה המקדימה. הוא יעיל יותר ודורש פחות משאבים מ-TextureView.
גם קל יחסית להגדיר את SurfaceView. צריך להתייחס רק ליחס הגובה-רוחב של SurfaceView שבו מוצגת התצוגה המקדימה של המצלמה.
מקור
מתחת ל-SurfaceView, פלטפורמת Android מסובבת את מאגרי הפלט כך שיתאימו לכיוון התצוגה של המכשיר. במילים אחרות, הוא מתייחס גם לכיוון החיישן וגם לסיבוב המסך. במילים פשוטות יותר, כשהמסך שלנו הוא לרוחב, אנחנו מקבלים תצוגה מקדימה שהיא גם לרוחב, ולהפך לגבי פורמט לאורך.
הטבלה הבאה ממחישה את זה. חשוב לזכור שהסיבוב של התצוגה לבדו לא קובע את הכיוון של המקור.
| סיבוב מסך | טלפון (כיוון טבעי = לאורך) | מחשב נייד (כיוון טבעי = לרוחב) |
|---|---|---|
| 0 | ![]() |
![]() |
| 90 | ![]() |
![]() |
| 180 | ![]() |
![]() |
| 270 | ![]() |
![]() |
פריסה
כפי שאפשר לראות, SurfaceView כבר מטפל בחלק מהדברים המורכבים בשבילנו. אבל עכשיו צריך לקחת בחשבון את הגודל של העינית, או את הגודל של התצוגה המקדימה שרוצים לראות על המסך. המערכת משנה את גודל מאגר המקור באופן אוטומטי כך שיתאים למידות של SurfaceView. צריך לוודא שיחס הגובה-רוחב של העינית זהה לזה של מאגר הנתונים הזמני של המקור. לדוגמה, אם מנסים להתאים תצוגה מקדימה בצורת פורטרט ל-SurfaceView בצורת לרוחב, מקבלים משהו מעוות כמו זה:
בדרך כלל, מומלץ שיחס הגובה-רוחב (כלומר, רוחב/גובה) של העינית יהיה זהה ליחס הגובה-רוחב של המקור. אם לא רוצים לחתוך את התמונה בתצוגה המקדימה – כלומר, לא רוצים להסיר חלק מהפיקסלים כדי לתקן את התצוגה – יש 2 מקרים שצריך לקחת בחשבון: כש-aspectRatioActivity גדול מ-aspectRatioSource וכש-aspectRatioActivity קטן מ-aspectRatioSource או שווה לו.
aspectRatioActivity > aspectRatioSource
אפשר לחשוב על המקרה כפעילות שהיא 'רחבה' יותר. בהמשך מופיעה דוגמה שבה יש פעילות ביחס רוחב-גובה של 16:9 ומקור ביחס רוחב-גובה של 4:3.
aspectRatioActivity = 16/9 ≈ 1.78 aspectRatioSource = 4/3 ≈ 1.33
קודם כל, צריך לוודא שגם העינית היא ביחס רוחב-גובה של 4:3. אחר כך צריך להתאים את המקור ואת העינית לפעילות באופן הבא:
במקרה כזה, צריך להתאים את הגובה של העינית לגובה של הפעילות, ולוודא שיחס הגובה-רוחב של העינית זהה ליחס הגובה-רוחב של המקור. הקוד המדומה הוא:
viewfinderHeight = activityHeight; viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource
מקרה נוסף הוא כשהפעילות צרה או גבוהה יותר. אפשר להשתמש שוב בדוגמה הקודמת, אבל בדוגמה הבאה מסובבים את המכשיר ב-90 מעלות, כך שהפעילות היא 9:16 והמקור הוא 3:4.
aspectRatioActivity = 9/16 = 0.5625 aspectRatioSource = 3/4 = 0.75
במקרה כזה, כדאי להתאים את המקור ואת העינית לפעילות באופן הבא:
צריך לוודא שהרוחב של העינית זהה לרוחב של הפעילות (בניגוד לגובה במקרה הקודם), ושיחס הגובה-רוחב של העינית זהה ליחס הגובה-רוחב של המקור. קוד מדומה:
viewfinderWidth = activityWidth; viewfinderHeight = activityWidth / aspectRatioSource;
חיתוך
AutoFitSurfaceView.kt (github) מתוך הדוגמאות של Camera2 מבטל את ההגדרה של SurfaceView ומטפל ביחסי גובה-רוחב לא תואמים באמצעות תמונה ששווה לפעילות או "גדולה יותר" ממנה בשני הממדים, ואז חותך את התוכן שחורג מהגבולות. האפשרות הזו שימושית לאפליקציות שרוצות שהתצוגה המקדימה תכסה את כל הפעילות או תמלא לחלוטין תצוגה של מימדים קבועים, בלי לעוות את התמונה.
הערה
בדוגמה הקודמת ניסינו למקסם את שטח המסך על ידי הגדלת התצוגה המקדימה כך שהיא תהיה גדולה יותר מהפעילות, כדי שלא יישאר שטח ריק. ההסתמכות הזו מבוססת על העובדה שחלקים שגולשים נחתכים על ידי פריסת ההורה (או ViewGroup) כברירת מחדל. ההתנהגות הזו זהה לזו של RelativeLayout ו-LinearLayout, אבל לא לזו של ConstraintLayout. יכול להיות שרכיב ConstraintLayout ישנה את הגודל של רכיבי המשנה מסוג View כדי להתאים אותם לפריסה, מה שיפגע באפקט הרצוי של 'חיתוך במרכז' ויגרום לתצוגות מקדימות מתוחות. אפשר לעיין בהתחייבות הזו כרפרנס.
TextureView
ה-TextureView מאפשר שליטה מקסימלית בתוכן של התצוגה המקדימה של המצלמה, אבל יש לכך השפעה על הביצועים. בנוסף, צריך להשקיע יותר עבודה כדי שהתצוגה המקדימה של המצלמה תוצג בצורה הנכונה.
מקור
מתחת ל-TextureView, פלטפורמת Android מסובבת את מאגרי הפלט בהתאם לכיוון החיישן כדי להתאים לכיוון הטבעי של המכשיר. TextureView מטפל בכיוון החיישן, אבל לא בסיבובי המסך. הוא מיישר את מאגרי הפלט עם הכיוון הטבעי של המכשיר, כלומר תצטרכו לטפל בסיבובים של התצוגה בעצמכם.
הטבלה הבאה ממחישה את זה. אם תנסו לסובב את הדמויות לפי סיבוב המסך המתאים, תקבלו את אותן דמויות ב-SurfaceView.
| סיבוב מסך | טלפון (כיוון טבעי = לאורך) | מחשב נייד (כיוון טבעי = לרוחב) |
|---|---|---|
| 0 | ![]() |
![]() |
| 90 | ![]() |
![]() |
| 180 | ![]() |
![]() |
| 270 | ![]() |
![]() |
פריסה
הפריסה קצת מסובכת במקרה של TextureView. בעבר הצענו להשתמש במטריצת טרנספורמציה עבור TextureView, אבל השיטה הזו לא פועלת בכל המכשירים. במקום זאת, מומלץ לפעול לפי השלבים שמפורטים כאן.
תהליך 3 השלבים ליצירת פריסה נכונה של תצוגות מקדימות ב-TextureView:
- מגדירים את הגודל של TextureView כך שיהיה זהה לגודל התצוגה המקדימה שנבחר.
- שינוי קנה המידה של TextureView שאולי נמתח בחזרה למימדים המקוריים של התצוגה המקדימה.
-
סיבוב ה-TextureView ב-
displayRotationנגד כיוון השעון.
נניח שיש לכם טלפון עם סיבוב מסך של 90 מעלות.
1. מגדירים את הגודל של TextureView כך שיהיה זהה לגודל התצוגה המקדימה שנבחר
נניח שגודל התצוגה המקדימה שבחרתם הוא previewWidth × previewHeight, כאשר previewWidth > previewHeight (פלט החיישן הוא בצורת לרוחב באופן טבעי). כשמגדירים סשן של צילום, צריך להתקשר אל SurfaceTexture#setDefaultBufferSize(int width, height) כדי לציין את גודל התצוגה המקדימה (previewWidth × previewHeight).
לפני שמבצעים קריאה ל-setDefaultBufferSize, חשוב להגדיר גם את הגודל של TextureView ל- `previewWidth × previewHeight` עם View#setLayoutParams(android.view.ViewGroup.LayoutParams). הסיבה לכך היא ש-TextureView קורא ל-SurfaceTexture#setDefaultBufferSize(int width, height) עם הרוחב והגובה שנמדדו. אם הגודל של TextureView לא מוגדר מראש באופן מפורש, יכול להיות שייווצר מצב של תחרות (race condition). כדי לפתור את הבעיה הזו, צריך להגדיר במפורש את הגודל של TextureView.
יכול להיות שעכשיו גודל ה-TextureView לא יהיה זהה לגודל המקור. במקרה של טלפונים, המקור הוא בצורת דיוקן, אבל ה-TextureView הוא בצורת נוף בגלל ה-layoutParams שהגדרתם. כתוצאה מכך, התצוגה המקדימה תהיה מתוחה, כמו בדוגמה הבאה:
2. שינוי קנה המידה של TextureView שייתכן שנמתח בחזרה לממדים המקוריים של התצוגה המקדימה
כדי לשנות את גודל התצוגה המקדימה המתוחה בחזרה למידות המקוריות, אפשר לפעול לפי השלבים הבאים.
המאפיינים של המקור (sourceWidth × sourceHeight) הם:
-
previewHeight × previewWidth, אם הכיוון הטבעי הוא לאורך או לאורך הפוך (כיוון החיישן הוא 90 או 270 מעלות) -
previewWidth × previewHeight, אם הכיוון הטבעי הוא לרוחב או לרוחב הפוך (כיוון החיישן הוא 0 או 180 מעלות)
כדי לתקן את המתיחה, משתמשים במאפיינים View#setScaleX(float) ו-View#setScaleY(float)
-
setScaleX(
sourceWidth / previewWidth) -
setScaleY(
sourceHeight / previewHeight)
3. סיבוב התצוגה המקדימה ב `displayRotation` נגד כיוון השעון
כמו שציינתי קודם, צריך לסובב את התצוגה המקדימה ב-displayRotation נגד כיוון השעון כדי לפצות על סיבוב המסך.
כדי לעשות את זה, View#setRotation(float)
-
setRotation(
-displayRotation), כי הוא מבצע סיבוב בכיוון השעון.
דוגמה
-
PreviewViewמ-camerax ב-Jetpack מטפל בפריסת TextureView כמו שמתואר למעלה. הוא מגדיר את הטרנספורמציה באמצעות PreviewCorrector.
הערה: אם השתמשתם בעבר במטריצת טרנספורמציה עבור TextureView בקוד שלכם, יכול להיות שהתצוגה המקדימה לא תיראה נכון במכשיר עם כיוון טבעי לרוחב כמו Chromebook. הסיבה לכך היא כנראה שמטריצת הטרנספורמציה מניחה באופן שגוי שהכיוון של החיישן הוא 90 או 270 מעלות. אפשר לעיין בהתחייבות הזו ב-GitHub כדי למצוא פתרון עקיף, אבל מומלץ מאוד להעביר את האפליקציה לשימוש בשיטה שמתוארת כאן.





















