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

التقاط صورة

يتم إرفاق معاودة الاتصال بمثيل 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 الخاصة بالسمة screenOrientation على fullSensor.Activity

في وضع النوافذ المتعددة، إذا كان الجهاز لا يتيح عكس اتجاه أفقي أو عمودي، لن يتم عكس اتجاه أفقي أو عمودي، حتى إذا تم ضبط سمة 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)
    }
}