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

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

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

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

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

حدِّد فئة حالة استخدام التطبيق ومنطق PiP القديم:

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

منطق PiP القديم لتحديد ما يلي:

  • 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. سيختلف مدى تعقيد عملية الاعتماد وتكلفتها حسب طريقة التنفيذ الحالية في التطبيق.

توضّح الأقسام التالية بعض حالات الاستخدام الشائعة لميزة &quot;نافذة ضمن النافذة&quot; وخطوات التنفيذ اللازمة:

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

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

  1. No need to differentiate auto-enter and legacy-enter on app side.
  2. واجهات رد الاتصال الموحّدة
  3. أداة إنشاء PictureInPictureParams جديدة للتوافق مع الأنظمة القديمة

مكالمة فيديو

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

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

  1. No need to differentiate auto-enter and legacy-enter on app side.
  2. واجهات رد الاتصال الموحّدة
  3. أداة إنشاء PictureInPictureParams جديدة للتوافق مع الأنظمة القديمة
  4. رموز إجراءات موحّدة لمكالمات الفيديو

5. نقل الرموز

  • منطق الإدخال: استبدِل المنطق الخاص بواجهة برمجة التطبيقات، مثل setAutoEnterEnabled لنظام التشغيل Android 12 والإصدارات الأحدث، أو onUserLeaveHint لنظام التشغيل Android 11 والإصدارات الأقدم باستخدام setEnabled. يتم تفعيل هذا الإجراء عندما تتغيّر حالة الأهلية لاستخدام ميزة "نافذة ضمن النافذة".
  • عمليات الاستدعاء: يمكنك دمج 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()
            }
        }
    }
}