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

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

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

טרמינולוגיה

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

כיוון המסך
הוא מציין איזה צד של המכשיר נמצא במיקום כלפי מעלה, והוא יכול להיות אחד מתוך ארבעה ערכים: לאורך, לרוחב, לאורך או הפוך לרוחב.
סיבוב התצוגה
זהו הערך שהוחזר על ידי 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

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

ניתוח תמונה

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

val rotation = imageProxy.imageInfo.rotationDegrees

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

צילום תמונה

קריאה חוזרת (callback) מצורפת למופע של 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) של Activity ב-onCreate().
להשתמש ב-OrientationEventListener onOrientationChanged(). בתוך הקריאה החוזרת, מעדכנים את סבב היעד של התרחישים לדוגמה. כך תטפל במקרים שבהם המערכת לא ליצור מחדש את Activity גם אחרי שינוי כיוון, כמו כשהמכשיר מסובב ב-180 מעלות. הכינוי גם יופיע במסך הפוך כיוון לאורך והמכשיר לא מסתובבים לכיוון הפוך על ידי כברירת מחדל. טיפול גם במקרים שבהם Activity לא נוצר מחדש כשהמכשיר מסתובב (לדוגמה, ב-90 מעלות). הפעולה הזו מתבצעת במקרים הבאים מכשירים קטנים של גורם צורה כשהאפליקציה תופסת חצי מהמסך, כשהאפליקציה תופסת שני שלישים מהמסך.
אופציונלי: אפשר להגדיר את screenOrientation של Activity לנכס fullSensor ב-AndroidManifest חדש. פעולה זו מאפשרת לממשק המשתמש לפעול בצורה זקופה כשהמכשיר הפוך לאורך, ומאפשרים ליצור מחדש את Activity המערכת בכל פעם שמסובבים את המכשיר ב-90 מעלות. אין השפעה על מכשירים שלא מסתובבים כדי להפוך את הדיוקן על ידי כברירת מחדל. אין תמיכה במצב ריבוי חלונות כשהמסך מוצג כיוון הפוך לאורך.
כיוון נעול מגדירים את התרחישים לדוגמה רק פעם אחת, Activity נוצר לראשונה, כמוActivity התקשרות חזרה onCreate().
להשתמש ב-OrientationEventListener onOrientationChanged(). בתוך הקריאה החוזרת, מעדכנים את סבב היעד של התרחישים לדוגמה, חוץ מ-Preview. טיפול גם במקרים שבהם Activity לא נוצר מחדש כשהמכשיר מסתובב (לדוגמה, ב-90 מעלות). הפעולה הזו מתבצעת במקרים הבאים מכשירים קטנים של גורם צורה כשהאפליקציה תופסת חצי מהמסך, כשהאפליקציה תופסת שני שלישים מהמסך.
שינויים בהגדרות הכיוון בוטלו מגדירים את התרחישים לדוגמה רק פעם אחת, Activity נוצר לראשונה, כמוActivity התקשרות חזרה onCreate().
להשתמש ב-OrientationEventListener onOrientationChanged(). בתוך הקריאה החוזרת, מעדכנים את סבב היעד של התרחישים לדוגמה. טיפול גם במקרים שבהם Activity לא נוצר מחדש כשהמכשיר מסתובב (לדוגמה, ב-90 מעלות). הפעולה הזו מתבצעת במקרים הבאים מכשירים קטנים של גורם צורה כשהאפליקציה תופסת חצי מהמסך, כשהאפליקציה תופסת שני שלישים מהמסך.
אופציונלי: מגדירים את מאפיין screenOrientation של הפעילות כ- fullSensor הקובץ AndroidManifest. מאפשרת לממשק המשתמש להופיע בצורה זקופה כשהמכשיר נמצא בפריסה לאורך. אין השפעה על מכשירים שלא מסתובבים כדי להפוך את הדיוקן על ידי כברירת מחדל. אין תמיכה במצב ריבוי חלונות כשהמסך מוצג כיוון הפוך לאורך.

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

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

תרחיש הנחיות מצב מסך מפוצל עם מספר חלונות
הכיוון לא נעול להגדיר את תרחישי השימוש בכל זמן היצירה של Activity, כמו הקריאה החוזרת (callback) של Activity ב-onCreate().
להשתמש ב-DisplayListener onDisplayChanged(). בתוך קריאה חוזרת, מעדכנים את הרוטציה ביעד של התרחישים לדוגמה, למשל כאשר המכשיר מסובב ב-180 מעלות. טיפול גם במקרים שבהם Activity לא נוצר מחדש כשהמכשיר מסתובב (לדוגמה, ב-90 מעלות). הפעולה הזו מתבצעת במקרים הבאים מכשירים קטנים של גורם צורה כשהאפליקציה תופסת חצי מהמסך, כשהאפליקציה תופסת שני שלישים מהמסך.
כיוון נעול מגדירים את התרחישים לדוגמה רק פעם אחת, Activity נוצר לראשונה, כמוActivity התקשרות חזרה onCreate().
להשתמש ב-OrientationEventListener onOrientationChanged(). בתוך הקריאה החוזרת, מעדכנים את סבב היעד של התרחישים לדוגמה. טיפול גם במקרים שבהם Activity לא נוצר מחדש כשהמכשיר מסתובב (לדוגמה, ב-90 מעלות). הפעולה הזו מתבצעת במקרים הבאים מכשירים קטנים של גורם צורה כשהאפליקציה תופסת חצי מהמסך, כשהאפליקציה תופסת שני שלישים מהמסך.
שינויים בהגדרות הכיוון בוטלו מגדירים את התרחישים לדוגמה רק פעם אחת, Activity נוצר לראשונה, כמוActivity התקשרות חזרה onCreate().
להשתמש ב-DisplayListener onDisplayChanged(). בתוך קריאה חוזרת, מעדכנים את הרוטציה ביעד של התרחישים לדוגמה, למשל כאשר המכשיר מסובב ב-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 נוצר לראשונה.

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)
    }
}