1. قبل البدء
ما الذي يميّز الأجهزة القابلة للطي؟
الهواتف القابلة للطيّ هي ابتكارات لا مثيل لها. وهي توفر تجارب فريدة، وتوفّر فرصًا فريدة لرضا المستخدمين بميزات مختلفة مثل واجهة المستخدم على الطاولة للاستخدام بدون لمس الجهاز.
المتطلبات الأساسية
- معرفة أساسية بتطوير تطبيقات Android
- معرفة أساسية بإطار عمل حقن التبعية لأداة Hilt
ما الذي ستنشئه
في هذا الدرس التطبيقي حول الترميز، أنشئ تطبيق كاميرا بتنسيقات محسَّنة للأجهزة القابلة للطي.
تبدأ باستخدام تطبيق كاميرا أساسي لا يستجيب لأي وضعية على الجهاز ولا يستفيد من الكاميرا الخلفية الأفضل لتحسين الصور الذاتية. يمكنك تعديل رمز المصدر لنقل المعاينة إلى الشاشة الأصغر حجمًا عندما يكون الجهاز غير مطوي ويتفاعل مع الهاتف الذي يتم ضبطه في وضع "الشاشة المسطحة".
على الرغم من أنّ تطبيق الكاميرا هو حالة الاستخدام الأكثر ملاءمةً لواجهة برمجة التطبيقات هذه، يمكن تطبيق الميزتَين اللتَين تتعلّمهما في هذا الدرس التطبيقي حول الترميز على أي تطبيق.
المعلومات التي ستطّلع عليها
- طريقة استخدام 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.
أنت على استعداد لتنفيذ تجارب مستخدم رائعة لتطبيق الكاميرا.