استخدام مكتبة "نافذة ضمن النافذة" في Jetpack

تقدّم مكتبة "نافذة ضمن النافذة" في Jetpack حلاً مبسطًا وفعّالاً لمطوّري تطبيقات Android لتنفيذ وظيفة "نافذة ضمن النافذة"، خاصةً لتطبيقات تشغيل الوسائط وتطبيقات التواصل عبر الفيديو وتطبيقات التنقّل. من خلال توفير واجهة برمجة تطبيقات موحّدة، تساعد المكتبة في إزالة رمز النص النموذجي والأخطاء الشائعة داخل التطبيق وتحسين الجودة الإجمالية لتجربة المستخدمين في وضع "نافذة ضمن النافذة".

تسهّل مكتبة "نافذة ضمن النافذة" في Jetpack استخدام واجهات برمجة التطبيقات الحالية الخاصة بميزة "نافذة ضمن النافذة" من خلال معالجة العديد من التحديات الرئيسية وحالات عدم الاتساق في منظومة Android المتكاملة، وهي:

  • تجزئة نظام التشغيل: تعالج المكتبة تلقائيًا الاختلافات في طلبات البيانات من واجهة برمجة التطبيقات "نافذة ضمن النافذة" في مختلف إصدارات Android، مثل استخدام enterPictureInPictureMode قبل Android 12 وisAutoEnterEnabled بعده، لذلك لا يحتاج المطوّرون إلى إدارة الاختلافات بين الإصدارات.
  • مَعلمات غير صحيحة لوضع "نافذة ضمن النافذة": يوفّر هذا الخيار حلاً موحّدًا لضبط مَعلمات وضع "نافذة ضمن النافذة" بشكل صحيح، مثل setSourceRectHint، وذلك لإنشاء رسوم متحركة سلسة وعالية الجودة أثناء تشغيل الوسائط.
  • عمليات معاودة الاتصال الموحّدة لحالة "نافذة ضمن النافذة": تدمج هذه العمليات onPictureInPictureModeChanged وonPictureInPictureUiStateChanged في واجهة موحّدة واحدة لمعاودة الاتصال (PictureInPictureDelegate.OnPictureInPictureEventListener) من أجل تبسيط إدارة الحالة وواجهة المستخدم.
  • تقليل الرمز النموذجي: تقلّل المكتبة من مقدار الرمز النموذجي المتكرّر من خلال توفير مجموعات محدّدة مسبقًا من RemoteActions لحالات الاستخدام الشائعة، مثل عناصر التحكّم في التشغيل وإجراءات مكالمات الفيديو.
  • التوافق مع الإصدارات المستقبلية: يتم توفير ميزات إضافية في وضع "نافذة ضمن النافذة" من خلال مكتبة Jetpack، ما يتيح للمستخدمين الاستفادة من وظائف إضافية بأقل جهد ممكن.

سير عمل نقل البيانات

تحديد فئة حالة استخدام التطبيق ومنطق ميزة "نافذة ضمن النافذة" القديم:

الفئات: تشغيل الفيديو أو التنقّل أو مكالمة الفيديو

المنطق القديم لتحديد وضع "نافذة ضمن النافذة":

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams.

2. إعداد AndroidManifest

تأكَّد من أنّ النشاط الذي يدخل وضع "نافذة ضمن النافذة" يعلن عن إمكانية استخدامه في AndroidManifest.xml مع configChanges اللازمَين لمنع عمليات إعادة التشغيل غير الضرورية:

<activity
android:name="VideoActivity" android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>

3- إعداد البيئة

أضِف الاعتمادات المطلوبة إلى ملف build.gradle:

dependencies {
implementation("androidx.core:core:1.18.0")
implementation("androidx.activity:activity:1.13.0")
implementation("androidx.core:core-pip:1.0.0-alpha02") }

استخدِم أحدث مكتبات AndroidX للتبعيات، وراجِع صفحة الإصدارات للحصول على هذه المعلومات.

4. اختيار النموذج وإعداده

اختَر نموذج التنفيذ الذي يناسب حالة استخدام التطبيق على أفضل نحو:

  • التنقّل ومكالمة الفيديو: BasicPictureInPicture؛ لا تتوفّر عادةً إمكانية تغيير الحجم بسلاسة، ولا تحتاج إلى تلميح مستطيل المصدر.
  • تشغيل الفيديو: VideoPlaybackPictureInPicture; يتتبّع تلقائيًا حدود عرض المشغّل للحصول على تلميح المستطيل المصدر ويتيح تغيير الحجم بسلاسة تلقائيًا.

لاستخدام مكتبة Jetpack، عليك استبدال عملية التنفيذ الحالية المخصّصة لوضع "نافذة ضمن النافذة" بواجهات برمجة التطبيقات الخاصة بمكتبة Jetpack. سيختلف مدى تعقيد عملية التنفيذ وتكلفتها استنادًا إلى عملية التنفيذ الحالية للتطبيق.

توضّح الأقسام التالية بعض حالات الاستخدام النموذجية لميزة "نافذة ضمن النافذة" وخطوات التنفيذ اللازمة:

يُعلم التطبيق المكتبة بحالة التنقّل النشطة أو غير النشطة ويضبط نسبة العرض إلى الارتفاع، وتتولّى مكتبة Jetpack بقية المهام.

الاختلافات الرئيسية:

  1. ليس من الضروري التمييز بين ميزة "الإدخال التلقائي" و"الإدخال القديم" من جهة التطبيق.
  2. واجهات رد الاتصال الموحّدة
  3. تمت إضافة أداة إنشاء PictureInPictureParams جديدة للتوافق مع الأنظمة القديمة.

مكالمة فيديو

يُعلم التطبيق المكتبة بحالة المكالمة النشطة أو غير النشطة ويضبط نسبة العرض إلى الارتفاع.

الاختلافات الرئيسية:

  1. ليس من الضروري التمييز بين ميزة "الإدخال التلقائي" و"الإدخال القديم" من جهة التطبيق.
  2. واجهات رد الاتصال الموحّدة
  3. تمت إضافة أداة إنشاء PictureInPictureParams جديدة للتوافق مع الأنظمة القديمة.
  4. رموز الإجراءات الموحّدة لمكالمة الفيديو

5. نقل الرموز

  • منطق الإدخال: استبدِل المنطق الخاص بواجهة برمجة التطبيقات، مثل setAutoEnterEnabled للإصدار 12 من نظام التشغيل Android والإصدارات الأحدث، أو onUserLeaveHint للإصدار 11 من نظام التشغيل Android والإصدارات الأقدم بالرمز setEnabled. يجب تشغيل هذا الإجراء كلما تغيّرت حالة الأهلية لاستخدام ميزة &quot;نافذة ضمن النافذة&quot;.
  • عمليات ردّ الاتصال: يمكنك دمج onPictureInPictureModeChanged (تبديل التنسيق) وonPictureInPictureUiStateChanged (الصور المتحركة/الحالات) في عملية ردّ اتصال موحّدة مستندة إلى الأحداث onPictureInPictureEvent.
  • الإجراءات والمَعلمات: عدِّل المَعلمات باستخدام setActions وsetAspectRatio في نسخة النموذج كلما تغيّرت.

أنماط التنفيذ المرجعية

أمثلة على عمليات التنفيذ

التنقّل ومكالمة الفيديو

class NavOrVideoCallJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: BasicPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = BasicPictureInPicture(this)
        // BasicPictureInPicture is ideal for Navigation and Video call use cases.
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTERED -> { /* Toggle to PiP layout */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Toggle to Full-screen layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* Optional: PiP is stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* Optional: PiP is unstashed */ }
        }
    }
}

تشغيل الفيديو

class VideoPlaybackJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = VideoPlaybackPictureInPicture(this)
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
            ContentScreen(pictureInPictureImpl)
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> { /* Hide overlays */ }
            PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> { /* Animation finished */ }
            PictureInPictureDelegate.Event.ENTERED -> { /* Switch to PiP layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* PiP stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* PiP unstashed */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Return to full-screen */ }
        }
    }

    @Composable
    fun ContentScreen(pipController: VideoPlaybackPictureInPicture) {
        DisposableEffect(pipController) {
            onDispose {
                pipController.close()
            }
        }
    }
}