סיבובי תרחישי שימוש של CameraX

בנושא הזה נסביר איך להגדיר תרחישי שימוש של CameraX באפליקציה כדי לקבל תמונות עם פרטי הסיבוב הנכונים, בין שבתרחיש לדוגמה ImageAnalysis ובין שבתרחיש לדוגמה ImageCapture. כך:

  • השדה Analyzer בתרחיש לדוגמה ImageAnalysis אמור לקבל פריימים עם הסיבוב הנכון.
  • בתרחיש לדוגמה ImageCapture, התמונות צריכות להצטלם עם הסיבוב הנכון.

טרמינולוגיה

בנושא הזה נעשה שימוש במונחים הבאים, לכן חשוב להבין את המשמעות של כל מונח:

כיוון המסך
הערך הזה מציין איזה צד של המכשיר נמצא במצב שפונה למעלה, והוא יכול להיות אחד מארבעה ערכים: portrait,‏ landscape,‏ reverse portrait או reverse landscape.
סיבוב מסך
זהו הערך שמוחזר על ידי Display.getRotation(), והוא מייצג את מספר המעלות שבהן המכשיר מסובב נגד כיוון השעון מהכיוון הטבעי שלו.
רוטציה של מודעות יעד
הערך הזה מייצג את מספר המעלות שצריך לסובב את המכשיר בכיוון השעון כדי להגיע לכיוון הטבעי שלו.

איך קובעים את רוטציית היעדים

בדוגמאות הבאות מוסבר איך לקבוע את סיבוב היעד של מכשיר על סמך הכיוון הטבעי שלו.

דוגמה 1: כיוון טבעי לאורך

דוגמה למכשיר: Pixel 3 XL

כיוון טבעי = לאורך
כיוון נוכחי = לאורך

סיבוב המסך = 0
סיבוב היעד = 0

כיוון טבעי = לאורך
כיוון נוכחי = לרוחב

סיבוב המסך = 90
סיבוב היעד = 90

דוגמה 2: כיוון טבעי לרוחב

דוגמה למכשיר: Pixel C

כיוון טבעי = לרוחב
כיוון נוכחי = לרוחב

סיבוב המסך = 0
סיבוב היעד = 0

כיוון טבעי = לרוחב
כיוון נוכחי = לאורך

סיבוב המסך = 270
סיבוב היעד = 270

סיבוב תמונה

איזה קצה למעלה? כיוון החיישן מוגדר ב-Android כערך קבוע שמייצג את המעלות (0,‏ 90,‏ 180,‏ 270) שבהן החיישן מסתובב מחלקו העליון של המכשיר כשהמכשיר נמצא במצב טבעי. בכל המקרים בתרשים, כיוון הסיבוב של התמונה מתאר את האופן שבו צריך לסובב את הנתונים בכיוון השעון כדי שהם יופיעו בצורה זקופה.

בדוגמאות הבאות מוצגת כיוון הסיבוב של התמונה בהתאם לכיוון של חיישן המצלמה. הם גם מניחים שהכיוון של היעד מוגדר לכיוון של המסך.

דוגמה 1: חיישן שהופנה ב-90 מעלות

דוגמה למכשיר: Pixel 3 XL

סיבוב המסך = 0
כיוון המסך = לאורך
סיבוב התמונה = 90

סיבוב המסך = 90
כיוון המסך = לרוחב
סיבוב התמונה = 0

דוגמה 2: חיישן שהסתובב ב-270 מעלות

דוגמה למכשיר: Nexus 5X

סיבוב המסך = 0
כיוון המסך = לאורך
סיבוב התמונה = 270

סיבוב המסך = 90
כיוון המסך = לרוחב
סיבוב התמונה = 180

דוגמה 3: החיישן סובב ב-0 מעלות

דוגמה למכשיר: Pixel C‏ (טאבלט)

סיבוב המסך = 0
כיוון המסך = לרוחב
סיבוב התמונה = 0

סיבוב המסך = 270
כיוון המסך = לאורך
סיבוב התמונה = 90

חישוב סיבוב של תמונה

ImageAnalysis

ה-Analyzer של ImageAnalysis מקבל תמונות מהמצלמה בצורת ImageProxy. כל תמונה מכילה מידע על סיבוב, שאפשר לגשת אליו באמצעות:

val rotation = imageProxy.imageInfo.rotationDegrees

הערך הזה מייצג את מספר המעלות שבהן צריך לסובב את התמונה בכיוון השעון כדי שתתאים לסיבוב היעד של ImageAnalysis. בהקשר של אפליקציה ל-Android, בדרך כלל כיוון היעד של ImageAnalysis יתאים לכיוון המסך.

ImageCapture

פונקציית קריאה חוזרת מצורפת למכונה של ImageCapture כדי לסמן מתי תוצאת הצילום מוכנה. התוצאה יכולה להיות התמונה שצולמה או הודעת שגיאה.

כשמצלמים תמונה, אפשר לספק פונקציית קריאה חוזרת (callback) מאחד מהסוגים הבאים:

  • OnImageCapturedCallback: מקבלת תמונה עם גישה בזיכרון בדמות ImageProxy.
  • OnImageSavedCallback: הפונקציה מופעלת כשהתמונה שצולמה נשמרת בהצלחה במיקום שצוין ב-ImageCapture.OutputFileOptions. האפשרויות יכולות לציין File, OutputStream או מיקום ב-MediaStore.

סיבוב התמונה שצולמה, ללא קשר לפורמט שלה (ImageProxy,‏ File, ‏ OutputStream, ‏ MediaStore Uri), מייצג את מעלות הסיבוב שצריך לסובב את התמונה שצולמה בכיוון השעון כדי שתתאים לסיבוב היעד של ImageCapture. שוב, בהקשר של אפליקציה ל-Android, סיבוב התמונה בדרך כלל תואם לכיוון המסך.

אפשר לאחזר את כיוון התמונה שצולמה באחת מהדרכים הבאות:

ImageProxy

val rotation = imageProxy.imageInfo.rotationDegrees

File

val exif = Exif.createFromFile(file)
val rotation = exif.rotation

OutputStream

val byteArray = outputStream.toByteArray()
val exif = Exif.createFromInputStream(ByteArrayInputStream(byteArray))
val rotation = exif.rotation

MediaStore uri

val inputStream = contentResolver.openInputStream(outputFileResults.savedUri)
val exif = Exif.createFromInputStream(inputStream)
val rotation = exif.rotation

אימות הסיבוב של תמונה

בתרחישי השימוש ImageAnalysis ו-ImageCapture, ה-ImageProxy מתקבל מהמצלמה אחרי בקשת צילום מוצלחת. ה-ImageProxy עוטף תמונה ומידע עליה, כולל הסיבוב שלה. פרטי הסיבוב מייצגים את מספר המעלות שבהן צריך לסובב את התמונה כדי שתתאים לסיבוב היעד של תרחיש השימוש.

תהליך אימות של רוטציית תמונה

הנחיות לסיבוב היעד ב-ImageCapture/‏ImageAnalysis

מאחר שבמכשירים רבים אין תמיכה כברירת מחדל בכיוון צילום הפוך (לאורך או לרוחב), חלק מהאפליקציות ל-Android לא תומכות בכיוונים האלה. אם האפליקציה תומכת בכך או לא, המשמעות היא שיהיה שינוי באופן שבו אפשר לעדכן את רוטציית היעדים בתרחישי השימוש.

בהמשך מפורטות שתי טבלאות שמגדירות איך לשמור על סנכרון בין כיוון התצוגה לבין כיוון היעד של התרחישים לדוגמה. בקוד הראשון מוסבר איך לעשות זאת תוך תמיכה בכל ארבעת הכיוונים, ובקוד השני מוסבר איך לעשות זאת רק בכיוונים שהמכשיר מסתובב אליהם כברירת מחדל.

כדי לבחור אילו הנחיות ליישם באפליקציה:

  1. מוודאים אם למצלמה Activity של האפליקציה יש כיוון נעילה, כיוון לא נעול או אם היא מבטלת שינויים בהגדרות הכיוון.

  2. קובעים אם המצלמה Activity של האפליקציה תטפל בכל ארבעת הכיוונים של המכשיר (לאורך, הפוך לרוחב, לרוחב והפוך לרוחב), או רק בכיוונים שהמכשיר שבו היא פועלת תומך בהם כברירת מחדל.

תמיכה בכל ארבעת הכיוונים

בטבלה הזו מפורטות הנחיות מסוימות שצריך לפעול לפיהן במקרים שבהם המכשיר לא מסתובב לכיוון האנכי ההפוך. אפשר לעשות את אותו הדבר במכשירים שלא מסתובבים לפריסה לרוחב.

תרחיש הנחיות מצב חלון יחיד מצב מסך מפוצל עם כמה חלונות
כיוון פתוח מגדירים את תרחישים לדוגמה בכל פעם שנוצר Activity, למשל בקריאה החוזרת (callback) של onCreate() ב-Activity.
משתמשים ב-onOrientationChanged() של OrientationEventListener. בתוך קריאת החזרה (callback), מעדכנים את רוטציית היעדים של תרחישים לדוגמה. כך ניתן לטפל במקרים שבהם המערכת לא יוצרת מחדש את Activity גם אחרי שינוי כיוון, למשל כשמסובבים את המכשיר ב-180 מעלות. הקוד הזה מטפל גם במקרים שבהם המסך בכיוון לאורך הפוך והמכשיר לא מסתובב לכיוון לאורך הפוך כברירת מחדל. הוא מטפל גם במקרים שבהם ה-Activity לא נוצר מחדש כשהמכשיר מסתובב (למשל, ב-90 מעלות). המצב הזה מתרחש במכשירים בפורמט קטן כשהאפליקציה תופסת חצי מהמסך, ובמכשירים גדולים יותר כשהאפליקציה תופסת שני שלישים מהמסך.
אופציונלי: מגדירים את המאפיין screenOrientation של Activity לערך fullSensor בקובץ AndroidManifest. כך ממשק המשתמש יהיה זקוף כשהמכשיר במצב 'פורטרט' הפוך, ואפשר יהיה ליצור מחדש את Activity בכל פעם שהמכשיר מסתובב ב-90 מעלות. ההגדרה הזו לא משפיעה על מכשירים שלא מסתובבים לכיוון הפוך כברירת מחדל. אי אפשר להשתמש במצב 'חלונות מרובים' כשהמסך בכיוון לאורך הפוך.
כיוון נעול מגדירים את תרחישים לדוגמה רק פעם אחת, כשיוצרים את Activity בפעם הראשונה, למשל בקריאה החוזרת (callback) של onCreate().Activity
משתמשים ב-onOrientationChanged() של OrientationEventListener. בתוך קריאת החזרה (callback), מעדכנים את רוטציית היעד של תרחישים לדוגמה, מלבד 'תצוגה מקדימה'. הוא מטפל גם במקרים שבהם ה-Activity לא נוצר מחדש כשהמכשיר מסתובב (למשל, ב-90 מעלות). המצב הזה מתרחש במכשירים בפורמט קטן כשהאפליקציה תופסת חצי מהמסך, ובמכשירים גדולים יותר כשהאפליקציה תופסת שני שלישים מהמסך.
Orientation configChanges overridden מגדירים את תרחישים לדוגמה רק פעם אחת, כשיוצרים את Activity בפעם הראשונה, למשל בקריאה החוזרת (callback) של onCreate().Activity
משתמשים ב-onOrientationChanged() של OrientationEventListener. בתוך קריאת החזרה (callback), מעדכנים את רוטציית היעדים של תרחישים לדוגמה. הוא מטפל גם במקרים שבהם ה-Activity לא נוצר מחדש כשהמכשיר מסתובב (למשל, ב-90 מעלות). המצב הזה מתרחש במכשירים בפורמט קטן כשהאפליקציה תופסת חצי מהמסך, ובמכשירים גדולים יותר כשהאפליקציה תופסת שני שלישים מהמסך.
אופציונלי: מגדירים את המאפיין screenOrientation של הפעילות לערך fullSensor בקובץ AndroidManifest. מאפשרת לממשק המשתמש להיות זקוף כשהמכשיר במצב לאורך הפוך. ההגדרה הזו לא משפיעה על מכשירים שלא מבצעים סיבוב לכיוון הפוך כברירת מחדל. אי אפשר להשתמש במצב 'חלונות מרובים' כשהמסך בכיוון לאורך הפוך.

תמיכה רק בכיוונים הנתמכים במכשיר

תמיכה רק בכיוונים שהמכשיר תומך בהם כברירת מחדל (יכול להיות שזה כולל כיוון לאורך הפוך או לרוחב הפוך, ויכול להיות שלא).

תרחיש הנחיות מצב מסך מפוצל עם כמה חלונות
כיוון פתוח מגדירים את תרחישים לדוגמה בכל פעם שנוצר Activity, למשל בקריאה החוזרת (callback) של onCreate() ב-Activity.
משתמשים ב-onDisplayChanged() של DisplayListener. בתוך הקריאה החוזרת, מעדכנים את סיבוב היעד של תרחישי השימוש, למשל כשהמכשיר מסתובב ב-180 מעלות. הוא מטפל גם במקרים שבהם ה-Activity לא נוצר מחדש כשהמכשיר מסתובב (למשל, ב-90 מעלות). המצב הזה מתרחש במכשירים בפורמט קטן כשהאפליקציה תופסת חצי מהמסך, ובמכשירים גדולים יותר כשהאפליקציה תופסת שני שלישים מהמסך.
כיוון נעול מגדירים את תרחישים לדוגמה רק פעם אחת, כשיוצרים את Activity בפעם הראשונה, למשל בקריאה החוזרת (callback) של onCreate().Activity
משתמשים ב-onOrientationChanged() של OrientationEventListener. בתוך קריאת החזרה (callback), מעדכנים את רוטציית היעדים של תרחישים לדוגמה. הוא מטפל גם במקרים שבהם ה-Activity לא נוצר מחדש כשהמכשיר מסתובב (למשל, ב-90 מעלות). המצב הזה מתרחש במכשירים בפורמט קטן כשהאפליקציה תופסת חצי מהמסך, ובמכשירים גדולים יותר כשהאפליקציה תופסת שני שלישים מהמסך.
Orientation configChanges overridden מגדירים את תרחישים לדוגמה רק פעם אחת, כשיוצרים את Activity בפעם הראשונה, למשל בקריאה החוזרת (callback) של onCreate().Activity
משתמשים ב-onDisplayChanged() של DisplayListener. בתוך הקריאה החוזרת, מעדכנים את סיבוב היעד של תרחישי השימוש, למשל כשהמכשיר מסתובב ב-180 מעלות. הוא מטפל גם במקרים שבהם ה-Activity לא נוצר מחדש כשהמכשיר מסתובב (למשל, ב-90 מעלות). המצב הזה מתרחש במכשירים בפורמט קטן כשהאפליקציה תופסת חצי מהמסך, ובמכשירים גדולים יותר כשהאפליקציה תופסת שני שלישים מהמסך.

כיוון לא נעול

כיוון התצוגה של Activity לא נעול כשהוא תואם לכיוון הפיזי של המכשיר (למשל, לאורך או לרוחב), מלבד כיוון הפוך לרוחב או לאורך, שלא תומכים בו כברירת מחדל במכשירים מסוימים. כדי לאלץ את המכשיר להתרוטט לכל ארבעת הכיוונים, מגדירים את המאפיין screenOrientation של Activity לערך fullSensor.

במצב'חלונות מרובים', מכשיר שלא תומך בפריסה לרוחב או בפריסה לרוחב הפוכה כברירת מחדל לא יתהפך לפריסה לרוחב או לפריסה לרוחב הפוכה, גם אם המאפיין screenOrientation מוגדר ל-fullSensor.

<!-- The Activity has an unlocked orientation, but might not rotate to reverse
portrait/landscape in single-window mode if the device doesn't support it by
default. -->
<activity android:name=".UnlockedOrientationActivity" />

<!-- The Activity has an unlocked orientation, and will rotate to all four
orientations in single-window mode. -->
<activity
   android:name=".UnlockedOrientationActivity"
   android:screenOrientation="fullSensor" />

כיוון נעול

המסך נעול בכיוון מסוים כשהוא נשאר באותו כיוון תצוגה (למשל לאורך או לרוחב) ללא קשר לכיוון הפיזי של המכשיר. כדי לעשות זאת, מציינים את המאפיין screenOrientation של Activity בתוך ההצהרה שלו בקובץ AndroidManifest.xml.

כשהמסך נעול בכיוון מסוים, המערכת לא משמידה את Activity ויוצרת אותו מחדש כשמסובבים את המכשיר.

<!-- The Activity keeps a portrait orientation even as the device rotates. -->
<activity
   android:name=".LockedOrientationActivity"
   android:screenOrientation="portrait" />

שינויים בהגדרות של כיוון המסך בוטלו

כשהמערכת מעדכנת את Activity כדי לעקוף שינויים בהגדרות של כיוון המסך, היא לא משמידה את הקוד הזה ויוצרת אותו מחדש כשהכיוון הפיזי של המכשיר משתנה. עם זאת, המערכת מעדכנת את ממשק המשתמש כך שיתאים לכיוון הפיזי של המכשיר.

<!-- The Activity's UI might not rotate in reverse portrait/landscape if the
device doesn't support it by default. -->
<activity
   android:name=".OrientationConfigChangesOverriddenActivity"
   android:configChanges="orientation|screenSize" />

<!-- The Activity's UI will rotate to all 4 orientations in single-window
mode. -->
<activity
   android:name=".OrientationConfigChangesOverriddenActivity"
   android:configChanges="orientation|screenSize"
   android:screenOrientation="fullSensor" />

הגדרת תרחישים לדוגמה של שימוש במצלמה

בתרחישים המתוארים למעלה, אפשר להגדיר את התרחישים לדוגמה של שימוש במצלמה כשאתם יוצרים את Activity בפעם הראשונה.

במקרה של Activity עם כיוון פתוח, ההגדרה הזו מתבצעת בכל פעם שמסובבים את המכשיר, כי המערכת משמידה את Activity ויוצרת אותו מחדש כשכיוון התצוגה משתנה. כתוצאה מכך, בכל פעם שתרחישי לדוגמה יתבצעו, כיוון היעד שלהם יוגדר כברירת מחדל בהתאם לכיוון המסך.

במקרה של Activity עם כיוון נעילה או Activity שמבטל שינויים בהגדרות הכיוון, ההגדרה הזו מתבצעת פעם אחת, כשיוצרים את ה-Activity בפעם הראשונה.

class CameraActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       val cameraProcessFuture = ProcessCameraProvider.getInstance(this)
       cameraProcessFuture.addListener(Runnable {
          val cameraProvider = cameraProcessFuture.get()

          // By default, the use cases set their target rotation to match the
          // display’s rotation.
          val preview = buildPreview()
          val imageAnalysis = buildImageAnalysis()
          val imageCapture = buildImageCapture()

          cameraProvider.bindToLifecycle(
              this, cameraSelector, preview, imageAnalysis, imageCapture)
       }, mainExecutor)
   }
}

הגדרת OrientationEventListener

שימוש ב-OrientationEventListener מאפשר לכם לעדכן ברציפות את סיבוב היעד של תרחישי השימוש במצלמה כשכיוון המכשיר משתנה.

class CameraActivity : AppCompatActivity() {

    private val orientationEventListener by lazy {
        object : OrientationEventListener(this) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == ORIENTATION_UNKNOWN) {
                    return
                }

                val rotation = when (orientation) {
                     in 45 until 135 -> Surface.ROTATION_270
                     in 135 until 225 -> Surface.ROTATION_180
                     in 225 until 315 -> Surface.ROTATION_90
                     else -> Surface.ROTATION_0
                 }

                 imageAnalysis.targetRotation = rotation
                 imageCapture.targetRotation = rotation
            }
        }
    }

    override fun onStart() {
        super.onStart()
        orientationEventListener.enable()
    }

    override fun onStop() {
        super.onStop()
        orientationEventListener.disable()
    }
}

הגדרת DisplayListener

שימוש ב-DisplayListener מאפשר לעדכן את סיבוב היעד של תרחישי השימוש במצלמה במצבים מסוימים, למשל כשהמערכת לא משמידה את ה-Activity ויוצרת אותו מחדש אחרי שהמכשיר מסתובב ב-180 מעלות.

class CameraActivity : AppCompatActivity() {

    private val displayListener = object : DisplayManager.DisplayListener {
        override fun onDisplayChanged(displayId: Int) {
            if (rootView.display.displayId == displayId) {
                val rotation = rootView.display.rotation
                imageAnalysis.targetRotation = rotation
                imageCapture.targetRotation = rotation
            }
        }

        override fun onDisplayAdded(displayId: Int) {
        }

        override fun onDisplayRemoved(displayId: Int) {
        }
    }

    override fun onStart() {
        super.onStart()
        val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        displayManager.registerDisplayListener(displayListener, null)
    }

    override fun onStop() {
        super.onStop()
        val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        displayManager.unregisterDisplayListener(displayListener)
    }
}