تدوير حالة استخدام 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

احتساب دوران الصورة

ImageAnalysis

يتلقّى "Analyzer" في "ImageAnalysis" صورًا من الكاميرا على شكل ImageProxy. تحتوي كل صورة على معلومات التدوير، والتي يمكن الوصول إليها من خلال:

val rotation = imageProxy.imageInfo.rotationDegrees

تمثّل هذه القيمة الدرجات التي يجب تدوير الصورة بها في اتجاه عقارب الساعة لتتطابق مع التدوير المستهدَف في "ImageAnalysis". في سياق تطبيق Android، يتطابق عادةً التدوير المستهدَف لـ ImageAnalysis مع اتجاه الشاشة.

ImageCapture

يتم إرفاق دالة ردّ اتصال بمثيل ImageCapture للإشارة إلى وقت جاهزية نتيجة الالتقاط. يمكن أن تكون النتيجة إما الصورة التي تم التقاطها أو رسالة خطأ.

عند التقاط صورة، يمكن أن يكون رد الاتصال المقدَّم من أحد الأنواع التالية:

  • ‫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، مثلاً في دالة الاستدعاء onCreate() في Activity.
استخدِم onOrientationChanged() في "OrientationEventListener". داخل دالة الاستدعاء، عدِّل التدوير المستهدَف لحالات الاستخدام. يعالج هذا الإجراء الحالات التي لا يعيد فيها النظام إنشاء Activity حتى بعد تغيير الاتجاه، مثل تدوير الجهاز بمقدار 180 درجة. ويتعامل أيضًا مع الحالات التي تكون فيها الشاشة في وضع عمودي معكوس ولا يتم تدوير الجهاز إلى الوضع العمودي المعكوس تلقائيًا. ويتعامل أيضًا مع الحالات التي لا تتم فيها إعادة إنشاء Activity عند تدوير الجهاز (90 درجة، على سبيل المثال). يحدث ذلك على الأجهزة ذات التصميم الصغير عندما يشغل التطبيق نصف الشاشة، وعلى الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
اختياري: اضبط السمة screenOrientation الخاصة بـ Activity على fullSensor في ملف AndroidManifest. يسمح هذا الإجراء بأن تكون واجهة المستخدم في وضع عمودي عندما يكون الجهاز في وضع عمودي معكوس، ويسمح للنظام بإعادة إنشاء Activity كلما تم تدوير الجهاز بمقدار 90 درجة. وليس له أي تأثير على الأجهزة التي لا يتم تدويرها إلى الوضع العمودي العكسي تلقائيًا. لا يتوفّر وضع النوافذ المتعددة عندما تكون الشاشة في الاتجاه العمودي المعكوس.
الاتجاه مقفل يجب إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء Activity لأول مرة، مثلاً في دالة الاستدعاء onCreate() الخاصة بـ Activity.
استخدِم onOrientationChanged() في "OrientationEventListener". داخل دالة الاستدعاء، عدِّل التدوير المستهدَف لحالات الاستخدام باستثناء "المعاينة". ويتعامل أيضًا مع الحالات التي لا تتم فيها إعادة إنشاء Activity عند تدوير الجهاز (90 درجة، على سبيل المثال). يحدث ذلك على الأجهزة ذات التصميم الصغير عندما يشغل التطبيق نصف الشاشة، وعلى الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
تم إلغاء configChanges للاتجاه يجب إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء Activity لأول مرة، مثلاً في دالة الاستدعاء onCreate() الخاصة بـ Activity.
استخدِم onOrientationChanged() في "OrientationEventListener". داخل دالة الاستدعاء، عدِّل التدوير المستهدَف لحالات الاستخدام. ويتعامل أيضًا مع الحالات التي لا تتم فيها إعادة إنشاء Activity عند تدوير الجهاز (90 درجة، على سبيل المثال). يحدث ذلك على الأجهزة ذات التصميم الصغير عندما يشغل التطبيق نصف الشاشة، وعلى الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
اختياري: اضبط السمة screenOrientation الخاصة بالنشاط على fullSensor في ملف AndroidManifest. يسمح هذا الإعداد بأن تكون واجهة المستخدم في وضع عمودي عندما يكون الجهاز في وضع عمودي معكوس. وليس له أي تأثير على الأجهزة التي لا يتم تدويرها إلى الوضع العمودي العكسي تلقائيًا. لا يتوفّر وضع النوافذ المتعددة عندما تكون الشاشة في الاتجاه العمودي المعكوس.

إتاحة الاتجاهات المتوافقة مع الجهاز فقط

يجب أن يتيح التطبيق الاتجاهات التي يتيحها الجهاز تلقائيًا فقط (والتي قد تشمل أو لا تشمل الاتجاه العمودي العكسي أو الاتجاه الأفقي العكسي).

السيناريو الإرشادات وضع تقسيم الشاشة إلى نوافذ متعددة
الاتجاه غير المقفل يجب إعداد حالات الاستخدام في كل مرة يتم فيها إنشاء Activity، مثلاً في دالة الاستدعاء onCreate() في Activity.
استخدِم onDisplayChanged() في DisplayListener. داخل دالة الاستدعاء، عدِّل التدوير المستهدَف لحالات الاستخدام، مثلاً عند تدوير الجهاز بمقدار 180 درجة. ويتعامل أيضًا مع الحالات التي لا تتم فيها إعادة إنشاء Activity عند تدوير الجهاز (90 درجة، على سبيل المثال). يحدث ذلك على الأجهزة ذات التصميم الصغير عندما يشغل التطبيق نصف الشاشة، وعلى الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
الاتجاه المقفل يجب إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء Activity لأول مرة، مثلاً في دالة الاستدعاء onCreate() الخاصة بـ Activity.
استخدِم onOrientationChanged() في "OrientationEventListener". داخل دالة الاستدعاء، عدِّل التدوير المستهدَف لحالات الاستخدام. ويتعامل أيضًا مع الحالات التي لا تتم فيها إعادة إنشاء Activity عند تدوير الجهاز (90 درجة، على سبيل المثال). يحدث ذلك على الأجهزة ذات التصميم الصغير عندما يشغل التطبيق نصف الشاشة، وعلى الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
تم إلغاء configChanges للاتجاه يجب إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء Activity لأول مرة، مثلاً في دالة الاستدعاء 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 لأول مرة.

class CameraActivity : ComponentActivity() {
    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)
    }
}