يعرض هذا الموضوع كيفية إعداد حالات استخدام CameraX داخل تطبيقك للحصول على
صور تتضمّن معلومات صحيحة عن التدوير، سواء كانت من حالة الاستخدام
ImageAnalysis
أو حالة الاستخدام ImageCapture
. وبالتالي:
- يجب أن تتلقّى
Analyzer
حالة الاستخدامAnalyzer
لقطات ذات اتجاه دوران صحيح.ImageAnalysis
- يجب أن تلتقط حالة استخدام
ImageCapture
صورًا بالاتجاه الصحيح.
المصطلحات
يستخدم هذا الموضوع المصطلحات التالية، لذا من المهم فهم معنى كل مصطلح:
- اتجاه العرض
- يشير ذلك إلى جانب الجهاز الذي يكون في الوضع العلوي، ويمكن أن يكون إحدى القيم الأربع التالية: عمودي أو أفقي أو عمودي معكوس أو أفقي معكوس.
- تدوير الشاشة
- هذه هي القيمة التي تعرضها دالة
Display.getRotation()
، وتمثل الدرجات التي يتم من خلالها تدوير الجهاز عكس اتجاه عقارب الساعة عن اتجاهه الطبيعي. - دورة الاستهداف
- يشير ذلك إلى عدد الدرجات التي يجب تدوير الجهاز بها باتجاه عقارب الساعة للوصول إلى الاتجاه الطبيعي.
كيفية تحديد معدّل دوران الإعلانات المستهدَفة
توضِّح الأمثلة التالية كيفية تحديد التناوب المستهدَف لجهاز معيّن استنادًا إلى اتجاهه الطبيعي.
المثال 1: الاتجاه العمودي الطبيعي
مثال على الجهاز: Pixel 3 XL | |
---|---|
الاتجاه الطبيعي = عمودي دوران الشاشة = 0 |
|
الاتجاه الطبيعي = عمودي دوران الشاشة = 90 |
المثال 2: الاتجاه الأفقي الطبيعي
مثال على الجهاز: Pixel C | |
---|---|
الاتجاه الطبيعي = أفقي دوران الشاشة = 0 |
|
الاتجاه الطبيعي = أفقي دوران شاشة العرض = 270 |
تدوير الصورة
أيّهما الجهة العلوية؟ يتم تعريف اتجاه المستشعر في Android على أنّه قيمة ثابتة تمثّل الدرجات (0 و90 و180 و270) التي يتم تدوير المستشعر بها من أعلى الجهاز عندما يكون الجهاز في وضع طبيعي. في جميع الحالات الواردة في المخططات البيانية، يصف دوران الصورة كيفية تدوير البيانات باتجاه عقارب الساعة لتظهر بشكلٍ رأسي.
توضِّح الأمثلة التالية كيفية دوران الصورة استنادًا إلى اتجاه ملف تعريف حساس الكاميرا. ويفترضون أيضًا أنّه تم ضبط التدوير المستهدَف على تدوير الشاشة.
المثال 1: تم تدوير أداة الاستشعار 90 درجة
مثال على الجهاز: Pixel 3 XL | |
---|---|
دوران الشاشة = 0 |
|
دوران الشاشة = 90 |
المثال 2: تم تدوير أداة الاستشعار 270 درجة
مثال على الجهاز: Nexus 5X | |
---|---|
دوران الشاشة = 0 |
|
دوران الشاشة = 90 |
المثال 3: تم تدوير أداة الاستشعار 0 درجة
مثال على الجهاز: Pixel C (جهاز لوحي) | |
---|---|
دوران الشاشة = 0 |
|
دوران الشاشة = 270 |
احتساب درجة دوران الصورة
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 مع هذين الوضعَين. ويؤدي ما إذا كان التطبيق يتوافق مع هذه الميزة أم لا إلى تغيير طريقة تعديل تدوير الاستهداف في حالات الاستخدام.
في ما يلي جدولان يحدّدان كيفية الحفاظ على تزامن التدوير المستهدَف لحالات الاستخدام مع تدوير الشاشة. يعرض الأول كيفية إجراء ذلك مع إتاحة جميع الوضعيات الأربعة، بينما يتعامل الثاني فقط مع الوضعيات التي يدور إليها الجهاز تلقائيًا.
لاختيار الإرشادات التي يجب اتّباعها في تطبيقك:
تحقَّق مما إذا كانت كاميرا
Activity
في تطبيقك ذات اتجاه قفل أو اتجاه غير مقفل أو إذا كانت تلغي تغييرات إعدادات الاتجاه.حدِّد ما إذا كان يجب أن تتعامل كاميرا تطبيقك
Activity
مع جميع أوضاع الجهاز الأربعة (العمودي والعمودي العكسي والأفقي والأفقي العكسي)، أو ما إذا كان يجب أن تتعامل فقط مع الأوضاع التي يتيحها الجهاز الذي يعمل عليه التطبيق بشكل تلقائي.
أن تتيح جميع الاتجاهات الأربعة
يشير هذا الجدول إلى إرشادات معيّنة يجب اتّباعها في الحالات التي لا يدور فيها الجهاز إلى الوضع العمودي العكسي. ويمكن تطبيق الإجراء نفسه على الأجهزة التي لا يمكن تحويلها إلى الوضع الأفقي العكسي.
السيناريو | الإرشادات | وضع النافذة الواحدة | وضع تقسيم الشاشة إلى عدة نوافذ |
---|---|---|---|
اتجاه الجهاز غير المُقفَل |
يمكنك إعداد حالات الاستخدام في كل
مرّة يتم فيها إنشاء Activity ، مثل
onCreate() المخصّص للرجوع إلى Activity .
|
||
استخدِم OrientationEventListener
onOrientationChanged() .
في دالة الاستدعاء، عدِّل دورة الاستهداف لحالات الاستخدام. يعالج هذا الإجراء الحالات التي لا يعيد فيها النظام
إنشاء Activity حتى بعد تغيير الاتجاه، مثل
عند تدوير الجهاز 180 درجة.
|
تتعامل هذه السمة أيضًا مع الحالات التي تكون فيها الشاشة في الوضع العمودي المقلوب ولا يتم تدوير الجهاز تلقائيًا إلى الوضع العمودي المقلوب. |
تعالج هذه الميزة أيضًا الحالات التي لا تتم فيها مجددًا
إنشاء الرمز Activity عند تدوير الجهاز (90 درجة مثلاً). يحدث ذلك على
الأجهزة الصغيرة عندما يشغل التطبيق نصف الشاشة، وعلى
الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
|
|
اختياري: اضبط السمة screenOrientation
في Activity على fullSensor في ملف AndroidManifest .
|
يتيح ذلك عرض واجهة المستخدم بشكل عمودي عندما يكون الجهاز في الوضع العميق
العمودي، كما يتيح للنظام
إعادة إنشاء Activity عند تدوير الجهاز بزاوية 90 درجة.
|
لا يؤثّر هذا الإعداد في الأجهزة التي لا تدور تلقائيًا إلى الوضع العمودي المعاكس. لا يتوفّر وضع "النوافذ المتعددة" عندما تكون الشاشة في الوضع عمودي معكوَف. | |
اتجاه قفل |
يمكنك إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء
Activity لأول مرة، مثل Activity
onCreate() في عملية معاودة الاتصال.
|
||
استخدِم
onOrientationChanged() في OrientationEventListener .
في دالة الاستدعاء، عدِّل دورة الاستهداف لحالات الاستخدام باستثناء "المعاينة".
|
تعالج هذه الطريقة أيضًا الحالات التي لا تتم فيها
إعادة إنشاء الرمز Activity عند تدوير الجهاز (90 درجة مثلاً). يحدث ذلك على
الأجهزة الصغيرة عندما يشغل التطبيق نصف الشاشة، وعلى
الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
|
||
تم إلغاء تغييرات إعدادات الاتجاه |
يمكنك إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء
Activity لأول مرة، مثل Activity
onCreate() في عملية معاودة الاتصال.
|
||
استخدِم OrientationEventListener
onOrientationChanged() .
في دالة الاستدعاء، عدِّل دورة الاستهداف لحالات الاستخدام.
|
تعالج هذه الطريقة أيضًا الحالات التي لا تتم فيها
إعادة إنشاء الرمز Activity عند تدوير الجهاز (90 درجة مثلاً). يحدث ذلك على
الأجهزة الصغيرة عندما يشغل التطبيق نصف الشاشة، وعلى
الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
|
||
اختياري: اضبط سمة screenOrientation للنشاط على fullSensor فيملف AndroidManifest. | السماح بعرض واجهة المستخدم في الوضع العمودي عندما يكون الجهاز في الوضع العمودي العكسي | لا يؤثّر هذا الإعداد في الأجهزة التي لا تدور تلقائيًا إلى الوضع العمودي المعاكس. لا يتوفّر وضع "النوافذ المتعددة" عندما تكون الشاشة في الوضع عمودي معكوَف. |
السماح بوضعيات العرض المتوافقة مع الجهاز فقط
أن تتيح الاتجاهات التي يتيحها الجهاز تلقائيًا فقط (قد تشمل أو لا تشمل الاتجاه العمودي/الأفقي العكسي)
السيناريو | الإرشادات | وضع تقسيم الشاشة إلى عدة نوافذ |
---|---|---|
اتجاه الجهاز غير المُقفَل |
يمكنك إعداد حالات الاستخدام في كل
مرّة يتم فيها إنشاء Activity ، مثل
onCreate() المخصّص للرجوع إلى Activity .
|
|
استخدِم
onDisplayChanged() في DisplayListener . في دالة callback الخاصة بحالة التفعيل، عدِّل التدوير المستهدَف لحالات الاستخدام، مثل عند تدوير
الجهاز بمقدار 180 درجة.
|
تعالج هذه الطريقة أيضًا الحالات التي لا تتم فيها
إعادة إنشاء الرمز Activity عند تدوير الجهاز (90 درجة مثلاً). يحدث ذلك على
الأجهزة الصغيرة عندما يشغل التطبيق نصف الشاشة، وعلى
الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
|
|
اتجاه مُقفَل |
يمكنك إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء
Activity لأول مرة، مثل Activity
onCreate() في عملية معاودة الاتصال.
|
|
استخدِم OrientationEventListener
onOrientationChanged() .
في دالة الاستدعاء، عدِّل دورة الاستهداف لحالات الاستخدام.
|
تعالج هذه الطريقة أيضًا الحالات التي لا تتم فيها
إعادة إنشاء الرمز Activity عند تدوير الجهاز (90 درجة مثلاً). يحدث ذلك على
الأجهزة الصغيرة عندما يشغل التطبيق نصف الشاشة، وعلى
الأجهزة الأكبر حجمًا عندما يشغل التطبيق ثلثي الشاشة.
|
|
تم إلغاء تغييرات إعدادات الاتجاه |
يمكنك إعداد حالات الاستخدام مرة واحدة فقط عند إنشاء
Activity لأول مرة، مثل Activity
onCreate() في عملية معاودة الاتصال.
|
|
استخدِم
onDisplayChanged() في DisplayListener . في دالة callback الخاصة بحالة التفعيل، عدِّل التدوير المستهدَف لحالات الاستخدام، مثل عند تدوير
الجهاز بمقدار 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) } }