1. قبل البدء
ما هي الميزات الخاصة بالأجهزة القابلة للطي؟
الهواتف القابلة للطيّ هي ابتكارات فريدة من نوعها. فهي توفّر تجارب فريدة، وتتيح فرصًا فريدة لإبهار المستخدمين بميزات مختلفة، مثل واجهة المستخدم المخصّصة لوضع "التثبيت على سطح مستوٍ" لاستخدام الجهاز بدون لمسه.
المتطلبات الأساسية
- معرفة أساسية بتطوير تطبيقات Android
- معرفة أساسية بإطار عمل Hilt Dependency Injection
ما ستنشئه
في هذا الدرس التطبيقي حول الترميز، ستنشئ تطبيق كاميرا يتضمّن تصميمات محسّنة للأجهزة القابلة للطي.

تبدأ بتطبيق كاميرا أساسي لا يتفاعل مع أي وضعية للجهاز ولا يستفيد من الكاميرا الخلفية الأفضل لالتقاط صور سيلفي محسّنة. عليك تعديل الرمز المصدر لنقل المعاينة إلى الشاشة الأصغر عندما يكون الجهاز مفتوحًا والاستجابة لوضع الهاتف على سطح مستوٍ.
على الرغم من أنّ تطبيق الكاميرا هو حالة الاستخدام الأكثر ملاءمةً لواجهة برمجة التطبيقات هذه، يمكن تطبيق كلتا الميزتين اللتين ستتعرّف عليهما في هذا الدرس التطبيقي حول الترميز على أي تطبيق.
ما ستتعلمه
- كيفية استخدام Jetpack Window Manager للتفاعل مع تغيير وضع الجهاز
- كيفية نقل تطبيقك إلى الشاشة الأصغر لجهاز قابل للطي
المتطلبات
- إصدار حديث من "استوديو Android"
- جهاز قابل للطي أو محاكي جهاز قابل للطي
2. طريقة الإعداد
الحصول على الرمز الأولي
- إذا كان Git مثبّتًا، يمكنك ببساطة تنفيذ الأمر أدناه. للتحقّق من تثبيت Git، اكتب
git --versionفي الوحدة الطرفية أو سطر الأوامر وتأكَّد من تنفيذه بشكل صحيح.
git clone https://github.com/android/large-screen-codelabs.git
- اختياري: إذا لم يكن لديك Git، يمكنك النقر على الزر التالي لتنزيل كل الرمز البرمجي لهذا الدرس التطبيقي حول الترميز:
فتح الوحدة الأولى
- في "استوديو Android"، افتح الوحدة الأولى ضمن
/step1.

إذا طُلب منك استخدام أحدث إصدار من Gradle، يمكنك المتابعة وتحديثه.
3- التشغيل والمراقبة
- نفِّذ الرمز على الوحدة
step1.
كما ترى، هذا تطبيق بسيط للكاميرا. يمكنك التبديل بين الكاميرا الأمامية والخلفية، ويمكنك ضبط نسبة العرض إلى الارتفاع. ومع ذلك، لا يؤدي الزر الأول من اليسار حاليًا أي وظيفة، ولكن سيصبح نقطة الدخول إلى وضع الصورة الذاتية باستخدام الكاميرا الخلفية.

- الآن، حاوِل وضع الجهاز في وضع نصف مفتوح، أي عندما لا يكون المفصل مسطحًا أو مغلقًا تمامًا، بل يشكّل زاوية 90 درجة.
كما تلاحظ، لا يستجيب التطبيق لأوضاع الجهاز المختلفة، وبالتالي لا يتغير التنسيق، ما يؤدي إلى بقاء المفصلة في منتصف عدسة الكاميرا.
4. مزيد من المعلومات حول Jetpack WindowManager
تساعد مكتبة Jetpack WindowManager مطوّري التطبيقات في إنشاء تجارب محسَّنة للأجهزة القابلة للطي. يحتوي على الفئة FoldingFeature التي تصف طيًّا في شاشة مرنة أو مفصّلة بين لوحتَي شاشة مادية. تتيح واجهة برمجة التطبيقات الوصول إلى معلومات مهمة متعلقة بالجهاز:
- تعرض الدالة
state()القيمةFLATإذا تم فتح المفصلة بزاوية 180 درجة أوHALF_OPENEDفي الحالات الأخرى. - تعرض الدالة
orientation()القيمةFoldingFeature.Orientation.HORIZONTALإذا كان عرضFoldingFeatureأكبر من الارتفاع، وإلا تعرض القيمةFoldingFeature.Orientation.VERTICAL. - توفّر السمة
bounds()حدودFoldingFeatureبتنسيقRect.
يحتوي الصف FoldingFeature على معلومات إضافية، مثل occlusionType() أو isSeparating()، ولكن لا يستكشف هذا الدرس التدريبي هذه المعلومات بالتفصيل.
بدءًا من الإصدار 1.2.0-beta01، تستخدم المكتبة WindowAreaController، وهي واجهة برمجة تطبيقات تتيح نقل النافذة الحالية إلى الشاشة المتوافقة مع الكاميرا الخلفية، ما يتيح التقاط صور سيلفي بالكاميرا الخلفية والعديد من حالات الاستخدام الأخرى.
إضافة عناصر تابعة
- لاستخدام Jetpack WindowManager في تطبيقك، عليك إضافة التبعيات التالية إلى ملف
build.gradleعلى مستوى الوحدة:
step1/build.gradle
def work_version = '1.2.0-beta01'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"
يمكنك الآن الوصول إلى الفئتين FoldingFeature وWindowAreaController في تطبيقك، واستخدامهما لإنشاء أفضل تجربة كاميرا على الأجهزة القابلة للطي.
5- تنفيذ وضع "الصور الذاتية بالكاميرا الخلفية"
ابدأ بوضع "الشاشة الخلفية".
واجهة برمجة التطبيقات التي تتيح هذا الوضع هي WindowAreaController، والتي توفّر المعلومات والسلوكيات المتعلقة بنقل النوافذ بين الشاشات أو مناطق العرض على الجهاز.
تتيح لك هذه الطريقة طلب قائمة بعناصر WindowAreaInfo التي يمكن التفاعل معها حاليًا.
باستخدام WindowAreaInfo، يمكنك الوصول إلى WindowAreaSession، وهي واجهة لتمثيل ميزة منطقة النافذة النشطة وحالة التوفّر لـ WindowAreaCapability. معيّن.
- عليك تعريف هذه المتغيّرات في
MainActivity:
step1/MainActivity.kt
private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var rearDisplaySession: WindowAreaSession? = null
private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
private var rearDisplayStatus: WindowAreaCapability.Status =
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA
- ويمكنك تهيئتها في الطريقة
onCreate():
step1/MainActivity.kt
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
windowAreaController.windowAreaInfos
.map{info->info.firstOrNull{it.type==WindowAreaInfo.Type.TYPE_REAR_FACING}}
.onEach { info -> rearDisplayWindowAreaInfo = info }
.map{it?.getCapability(rearDisplayOperation)?.status?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
.distinctUntilChanged()
.collect {
rearDisplayStatus = it
updateUI()
}
}
}
- الآن، نفِّذ الدالة
updateUI()لتفعيل زر التقاط صور السيلفي بالكاميرا الخلفية أو إيقافه، وذلك حسب الحالة الحالية:
step1/MainActivity.kt
private fun updateUI() {
if(rearDisplaySession != null) {
binding.rearDisplay.isEnabled = true
// A session is already active, clicking on the button will disable it
} else {
when(rearDisplayStatus) {
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not supported on this device"
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not currently available
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
binding.rearDisplay.isEnabled = true
// You can enable RearDisplay Mode
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
binding.rearDisplay.isEnabled = true
// You can disable RearDisplay Mode
}
else -> {
binding.rearDisplay.isEnabled = false
// RearDisplay status is unknown
}
}
}
}
هذه الخطوة الأخيرة اختيارية، ولكنّها مفيدة جدًا للتعرّف على جميع الحالات المحتملة WindowAreaCapability.
- الآن، نفِّذ الدالة
toggleRearDisplayModeالتي ستغلق الجلسة إذا كانت الميزة مفعّلة، أو استدعِ الدالةtransferActivityToWindowArea:
step1/CameraViewModel.kt
private fun toggleRearDisplayMode() {
if(rearDisplayStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
if(rearDisplaySession == null) {
rearDisplaySession = rearDisplayWindowAreaInfo?.getActiveSession(rearDisplayOperation)
}
rearDisplaySession?.close()
} else {
rearDisplayWindowAreaInfo?.token?.let { token ->
windowAreaController.transferActivityToWindowArea(
token = token,
activity = this,
executor = displayExecutor,
windowAreaSessionCallback = this
)
}
}
}
لاحظ استخدام MainActivity كـ WindowAreaSessionCallback.
تعمل واجهة برمجة التطبيقات Rear Display API من خلال أسلوب المتتبِّع: عند طلب نقل المحتوى إلى الشاشة الأخرى، تبدأ جلسة يتم عرضها من خلال طريقة onSessionStarted() الخاصة بالمتتبِّع. عندما تريد العودة إلى الشاشة الداخلية (الأكبر)، عليك إغلاق الجلسة، وسيصلك تأكيد في الطريقة onSessionEnded(). لإنشاء أداة معالجة من هذا النوع، عليك تنفيذ واجهة WindowAreaSessionCallback.
- عدِّل تعريف
MainActivityلكي ينفّذ واجهةWindowAreaSessionCallback:
step1/MainActivity.kt
class MainActivity : AppCompatActivity(), WindowAreaSessionCallback
الآن، نفِّذ الطريقتَين onSessionStarted وonSessionEnded داخل MainActivity. تُعدّ طرق رد الاتصال هذه مفيدة للغاية لتلقّي إشعارات بشأن حالة الجلسة وتعديل التطبيق وفقًا لذلك.
ولكن هذه المرة، ولتبسيط الأمر، ما عليك سوى التحقّق في نص الدالة مما إذا كانت هناك أي أخطاء وتسجيل الحالة.
step1/MainActivity.kt
override fun onSessionEnded(t: Throwable?) {
if(t != null) {
Log.d("Something was broken: ${t.message}")
}else{
Log.d("rear session ended")
}
}
override fun onSessionStarted(session: WindowAreaSession) {
Log.d("rear session started [session=$session]")
}
- أنشئ التطبيق وشغِّله. بعد ذلك، إذا فتحت جهازك ونقرت على زر الشاشة الخلفية، ستظهر لك رسالة مشابهة لما يلي:

- انقر على تبديل الشاشات الآن لمشاهدة المحتوى الذي تم نقله إلى الشاشة الخارجية.
6. تنفيذ وضع "التثبيت على سطح مستوٍ"
حان الوقت الآن لجعل تطبيقك متوافقًا مع الأجهزة القابلة للطي: يمكنك نقل المحتوى إلى الجانب أو أعلى مفصلة الجهاز استنادًا إلى اتجاه الطي. لإجراء ذلك، عليك العمل داخل FoldingStateActor حتى يتم فصل الرمز عن Activity لتسهيل قراءته.
يتألف الجزء الأساسي من واجهة برمجة التطبيقات هذه من واجهة WindowInfoTracker، والتي يتم إنشاؤها باستخدام طريقة ثابتة تتطلّب Activity:
step1/CameraCodelabDependencies.kt
@Provides
fun provideWindowInfoTracker(activity: Activity) =
WindowInfoTracker.getOrCreate(activity)
ليس عليك كتابة هذا الرمز لأنّه متوفّر مسبقًا، ولكن من المفيد فهم طريقة إنشاء WindowInfoTracker.
- للاستماع إلى أي تغيير في النافذة، استمع إلى هذه التغييرات في طريقة
onResume()الخاصة بـActivity:
step1/MainActivity.kt
lifecycleScope.launch {
foldingStateActor.checkFoldingState(
this@MainActivity,
binding.viewFinder
)
}
- افتح الآن ملف
FoldingStateActor، فقد حان الوقت لملء طريقةcheckFoldingState().
كما رأيت من قبل، يتم تشغيلها في مرحلة RESUMED من Activity، وتستفيد من WindowInfoTracker للاستماع إلى أي تغيير في التنسيق.
step1/FoldingStateActor.kt
windowInfoTracker.windowLayoutInfo(activity)
.collect { newLayoutInfo ->
activeWindowLayoutInfo = newLayoutInfo
updateLayoutByFoldingState(cameraViewfinder)
}
باستخدام واجهة WindowInfoTracker، يمكنك استدعاء windowLayoutInfo() من أجل جمع Flow من WindowLayoutInfo التي تحتوي على جميع المعلومات المتاحة في DisplayFeature.
الخطوة الأخيرة هي التفاعل مع هذه التغييرات ونقل المحتوى وفقًا لذلك. يمكنك إجراء ذلك داخل طريقة updateLayoutByFoldingState()، خطوة واحدة في كل مرة.
- تأكَّد من أنّ
activityLayoutInfoيحتوي على بعض خصائصDisplayFeature، وأنّ إحدى هذه الخصائص على الأقل هيFoldingFeature، وإلا لن تحتاج إلى اتّخاذ أي إجراء:
step1/FoldingStateActor.kt
val foldingFeature = activeWindowLayoutInfo?.displayFeatures
?.firstOrNull { it is FoldingFeature } as FoldingFeature?
?: return
- احتسِب موضع الطي للتأكّد من أنّ موضع الجهاز يؤثّر في التصميم وليس خارج حدود التسلسل الهرمي:
step1/FoldingStateActor.kt
val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
foldingFeature,
cameraViewfinder.parent as View
) ?: return
الآن، أنت متأكّد من أنّ لديك FoldingFeature يؤثر في تخطيطك، لذا عليك نقل المحتوى.
- تحقَّق ممّا إذا كان
FoldingFeatureهوHALF_OPENأو يمكنك استعادة موضع المحتوى. إذا كانت القيمةHALF_OPEN، عليك إجراء عملية تحقّق أخرى واتّخاذ إجراء مختلف استنادًا إلى اتجاه الطي:
step1/FoldingStateActor.kt
if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
when (foldingFeature.orientation) {
FoldingFeature.Orientation.VERTICAL -> {
cameraViewfinder.moveToRightOf(foldPosition)
}
FoldingFeature.Orientation.HORIZONTAL -> {
cameraViewfinder.moveToTopOf(foldPosition)
}
}
} else {
cameraViewfinder.restore()
}
إذا كان موضع الطي VERTICAL، عليك نقل المحتوى إلى اليسار، وإلا عليك نقله إلى أعلى موضع الطي.
- أنشئ تطبيقك وشغِّله، ثم افتح جهازك وضعه في وضع "التثبيت على سطح مستوٍ" لترى المحتوى ينتقل وفقًا لذلك.
7. تهانينا!
في هذا الدرس التطبيقي حول الترميز، تعرّفت على بعض الإمكانات الفريدة للأجهزة القابلة للطي، مثل "وضع الشاشة الخلفية" أو "وضع سطح الطاولة"، وكيفية إتاحتها باستخدام Jetpack WindowManager.
أنت الآن جاهز لتوفير تجارب مستخدم رائعة في تطبيق الكاميرا.