نافذة ضمن النافذة (PiP) هي نوع خاص من أوضاع النوافذ المتعددة التي تُستخدَم غالبًا في تشغيل الفيديو. وهو يتيح للمستخدم مشاهدة فيديو في نافذة صغيرة مثبتة في زاوية من الشاشة أثناء التنقل بين التطبيقات أو تصفُّح المحتوى على الشاشة الرئيسية.
تستفيد ميزة "نافذة ضمن النافذة" من واجهات برمجة التطبيقات للنوافذ المتعددة والمتاحة في الإصدار Android 7.0، وذلك لتوفير نافذة تراكب فيديو مثبّتة. لإضافة هذه الميزة إلى تطبيقك، عليك تسجيل نشاطك، وتحويل نشاطك إلى وضع "نافذة ضمن النافذة" حسب الحاجة والتأكّد من إخفاء عناصر واجهة المستخدم ومواصلة تشغيل الفيديو عندما يكون النشاط في وضع "نافذة ضمن النافذة".
يوضّح هذا الدليل كيفية إضافة ميزة "نافذة ضمن النافذة" في Compose إلى تطبيقك باستخدام ميزة ComposeAllowed للفيديو. يمكنك الانتقال إلى تطبيق Socialite للاطّلاع على أفضل الممارسات هذه.
ضبط إعدادات تطبيقك في وضع "نافذة ضمن النافذة" (PIP)
في علامة النشاط لملف AndroidManifest.xml
، قم بما يلي:
- يُرجى إضافة السمة
supportsPictureInPicture
وضبطها علىtrue
للإشارة إلى أنّك ستستخدم ميزة "نافذة ضمن النافذة" في تطبيقك. أضِف السمة
configChanges
واضبطها علىorientation|screenLayout|screenSize|smallestScreenSize
لتحديد أنّ نشاطك يتعامل مع التغييرات في إعدادات التنسيق. وبهذه الطريقة، لن تتم إعادة تشغيل نشاطك عند حدوث تغييرات في التصميم أثناء عمليات الانتقال في وضع "نافذة ضمن النافذة".<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
في رمز Compose، اتّبِع الخطوات التالية:
- ثبِّت هذه الإضافة في
Context
. ستستخدم هذه الإضافة عدة مرات في الدليل للوصول إلى النشاط.internal fun Context.findActivity(): ComponentActivity { var context = this while (context is ContextWrapper) { if (context is ComponentActivity) return context context = context.baseContext } throw IllegalStateException("Picture in picture should be called in the context of an Activity") }
إضافة ميزة "نافذة ضمن النافذة" (PiP) عند مغادرة التطبيق للأجهزة التي تعمل بالإصدارات السابقة من Android 12
لإضافة هذه الميزة إلى الأجهزة التي تعمل بالإصدارات الأقدم من نظام التشغيل Android 12، استخدِم addOnUserLeaveHintProvider
. يُرجى اتّباع الخطوات التالية لإضافة ميزة "نافذة ضمن النافذة" (PiP) إلى الإصدارات السابقة من Android 12:
- أضف بوابة إصدار بحيث لا يتم الوصول إلى هذا الرمز إلا في الإصدارات O حتى R.
- استخدِم
DisposableEffect
مع المفتاحContext
. - في
DisposableEffect
، حدِّد السلوك عند تشغيلonUserLeaveHintProvider
باستخدام lambda. في لامدا، اتصل بـenterPictureInPictureMode()
علىfindActivity()
ثم مرِّرPictureInPictureParams.Builder().build()
. - أضِف
addOnUserLeaveHintListener
باستخدامfindActivity()
ومرِّر في دالة lambda. - في
onDispose
، أضِفremoveOnUserLeaveHintListener
باستخدامfindActivity()
واستخدِم كلمة المرور في lambda.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i(PIP_TAG, "API does not support PiP") }
إضافة ميزة "نافذة ضمن النافذة" في تطبيق المغادرة للإصدار 12 من نظام التشغيل Android
بعد الإصدار 12 من نظام التشغيل Android، تتم إضافة PictureInPictureParams.Builder
من خلال
معدِّل يتم تمريره إلى مشغّل الفيديو في التطبيق.
- يمكنك إنشاء
modifier
والاتصال بـonGloballyPositioned
من خلاله. سيتم استخدام إحداثيات التخطيط في خطوة لاحقة. - أنشئ متغيّرًا للسمة
PictureInPictureParams.Builder()
. - أضِف عبارة
if
للتحقّق مما إذا كانت حزمة تطوير البرامج (SDK) هي S أو إصدارًا أحدث. في هذه الحالة، أضِفsetAutoEnterEnabled
إلى أداة الإنشاء واضبطها علىtrue
للدخول في وضع "نافذة ضمن النافذة" عند التمرير السريع. يوفر ذلك صورة متحركة أكثر سلاسة من قراءةenterPictureInPictureMode
. - استخدِم
findActivity()
للاتصال بالرقمsetPictureInPictureParams()
. اتصل بـbuild()
علىbuilder
ومرره.
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(true) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
إضافة "نافذة ضمن النافذة" (PIP) من خلال زرّ
للدخول إلى وضع "نافذة ضمن النافذة" (PiP) بنقرة زر، يُرجى الاتصال بالرقم
enterPictureInPictureMode()
على الرقم findActivity()
.
يتم ضبط المعلَمات من خلال عمليات الاستدعاء السابقة إلى
PictureInPictureParams.Builder
، لذلك لا تحتاج إلى ضبط معلَمات جديدة
في أداة الإنشاء. ومع ذلك، إذا كنت تريد تغيير أي معلمات عند النقر على
الزر، فيمكنك تعيينها هنا.
val context = LocalContext.current Button(onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.findActivity().enterPictureInPictureMode( PictureInPictureParams.Builder().build() ) } else { Log.i(PIP_TAG, "API does not support PiP") } }) { Text(text = "Enter PiP mode!") }
التعامل مع واجهة المستخدم في وضع "نافذة ضمن النافذة"
عند تفعيل الوضع "نافذة ضمن النافذة"، تدخل واجهة المستخدم الخاصة بالتطبيق بالكامل في هذه النافذة، ما لم تحدّد الشكل الذي يجب أن تظهر به واجهة المستخدم داخل هذا الوضع وخارجه.
عليك أولاً معرفة ما إذا كان تطبيقك في وضع "نافذة ضمن النافذة" (PiP). يمكنك استخدام
OnPictureInPictureModeChangedProvider
لتحقيق ذلك.
يوضّح الرمز أدناه ما إذا كان تطبيقك في وضع "نافذة ضمن النافذة".
@Composable fun rememberIsInPipMode(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val activity = LocalContext.current.findActivity() var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) } DisposableEffect(activity) { val observer = Consumer<PictureInPictureModeChangedInfo> { info -> pipMode = info.isInPictureInPictureMode } activity.addOnPictureInPictureModeChangedListener( observer ) onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } } return pipMode } else { return false } }
يمكنك الآن استخدام rememberIsInPipMode()
لاختيار عناصر واجهة المستخدم التي سيتم عرضها
عندما يدخل التطبيق في وضع "نافذة ضمن النافذة":
val inPipMode = rememberIsInPipMode() Column(modifier = modifier) { // This text will only show up when the app is not in PiP mode if (!inPipMode) { Text( text = "Picture in Picture", ) } VideoPlayer() }
احرِص على إدخال تطبيقك في وضع "نافذة ضمن النافذة" في الأوقات المناسبة.
يجب ألا يدخل تطبيقك في وضع "نافذة ضمن النافذة" في الحالات التالية:
- ما إذا كان الفيديو متوقفًا أو متوقفًا مؤقتًا:
- عندما تكون في صفحة في التطبيق مختلفة عن مشغّل الفيديو
للتحكّم في وقت ظهور تطبيقك في وضع "نافذة ضمن النافذة"، عليك إضافة متغيّر يتتبّع حالة
مشغّل الفيديو باستخدام mutableStateOf
.
تبديل الحالة بناءً على ما إذا كان يتم تشغيل الفيديو
لتبديل الحالة استنادًا إلى ما إذا كان مشغّل الفيديو قيد التشغيل، أضِف مستمعًا إلى مشغّل الفيديو. بدِّل حالة متغيّر الحالة استنادًا إلى ما إذا كان المشغّل قيد التشغيل أم لا:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
تبديل الحالة بناءً على ما إذا تم إصدار اللاعب
عندما يتم إصدار المشغّل، اضبط متغيّر الحالة على false
:
fun releasePlayer() { shouldEnterPipMode = false }
يمكنك استخدام هذه الحالة لتحديد ما إذا تم إدخال وضع "نافذة ضمن النافذة" (PiP (في ما قبل الإصدار 12 من Android).
- بما أنّ إضافة نافذة ضمن النافذة (PIP) في مرحلة ما قبل 12 تستخدم
DisposableEffect
، عليك إنشاء متغيّر جديد عن طريقrememberUpdatedState
مع ضبطnewValue
كمتغيّر الحالة. سيضمن ذلك استخدام الإصدار المحدَّث فيDisposableEffect
. في دالة lambda التي تحدّد السلوك عند تشغيل
OnUserLeaveHintListener
، أضِف عبارةif
مع متغيّر الحالة حول الطلب إلىenterPictureInPictureMode()
:val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && Build.VERSION.SDK_INT < Build.VERSION_CODES.S ) { val context = LocalContext.current DisposableEffect(context) { val onUserLeaveBehavior: () -> Unit = { if (currentShouldEnterPipMode) { context.findActivity() .enterPictureInPictureMode(PictureInPictureParams.Builder().build()) } } context.findActivity().addOnUserLeaveHintListener( onUserLeaveBehavior ) onDispose { context.findActivity().removeOnUserLeaveHintListener( onUserLeaveBehavior ) } } } else { Log.i(PIP_TAG, "API does not support PiP") }
يمكنك استخدام الحالة لتحديد ما إذا تم إدخال وضع "نافذة ضمن النافذة" (بعد إصدار Android 12).
مرِّر متغيّر الحالة إلى setAutoEnterEnabled
كي لا يدخل تطبيقك
سوى وضع "نافذة ضمن النافذة" (PiP) في الوقت المناسب:
val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() // Add autoEnterEnabled for versions S and up if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
استخدِم setSourceRectHint
لإنشاء صورة متحركة سلسة.
تنشئ واجهة برمجة التطبيقات setSourceRectHint
صورة متحركة أكثر سلاسة للدخول في وضع "نافذة ضمن النافذة". في الإصدار 12 من نظام التشغيل Android والإصدارات الأحدث، يتم أيضًا إنشاء صورة متحركة أكثر سلاسة للخروج من وضع "نافذة ضمن النافذة".
أضِف واجهة برمجة التطبيقات هذه إلى منصة إنشاء "نافذة ضمن النافذة" (PiP) للإشارة إلى منطقة النشاط التي تظهر بعد الانتقال إلى "نافذة ضمن النافذة".
- لا تضِف
setSourceRectHint()
إلىbuilder
إلا إذا حدّدت الحالة أنّ التطبيق يجب أن يدخل في وضع "نافذة ضمن النافذة". يتجنّب ذلك احتسابsourceRect
عندما لا يحتاج التطبيق إلى الدخول في وضع "نافذة ضمن النافذة". - لضبط قيمة
sourceRect
، استخدِم الدالةlayoutCoordinates
المتوفرة من الدالةonGloballyPositioned
على مفتاح التعديل. - اتصل بـ
setSourceRectHint()
علىbuilder
ومرر المتغيّرsourceRect
.
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
يمكنك استخدام "setAspectRatio
" لضبط نسبة العرض إلى الارتفاع لنافذة "نافذة ضمن النافذة".
لضبط نسبة العرض إلى الارتفاع في نافذة "نافذة ضمن النافذة"، يمكنك اختيار نسبة عرض إلى ارتفاع محدّدة أو استخدام عرض وارتفاع حجم فيديو المشغّل. في حال استخدام مشغّل
media3، يُرجى التأكّد من أنّ قيمة المشغّل غير فارغة وأنّ حجم فيديو المشغّل لا يساوي VideoSize.UNKNOWN
قبل ضبط نسبة العرض إلى الارتفاع.
val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(pipModifier)
إذا كنت تستخدم مشغّلاً مخصّصًا، اضبط نسبة العرض إلى الارتفاع على ارتفاع المشغّل وعرضه باستخدام البنية الخاصة بالمشغّل. ويجب الانتباه إلى أنّه إذا تغيّر حجم المشغّل أثناء عملية الإعداد، وكان يتخطى الحدود الصالحة لنسبة العرض إلى الارتفاع، سيتعطّل تطبيقك. قد تحتاج إلى إضافة عمليات تحقُّق حول وقت احتساب نسبة العرض إلى الارتفاع، على غرار طريقة الدفع في مشغّل media3.
إضافة إجراءات عن بُعد
إذا أردت إضافة عناصر التحكّم (التشغيل والإيقاف المؤقت وغير ذلك) إلى نافذة "نافذة ضمن النافذة"، يمكنك إنشاء
RemoteAction
لكل عنصر تحكّم تريد إضافته.
- أضِف ثوابت إلى عناصر التحكّم في البث:
// Constant for broadcast receiver const val ACTION_BROADCAST_CONTROL = "broadcast_control" // Intent extras for broadcast controls from Picture-in-Picture mode. const val EXTRA_CONTROL_TYPE = "control_type" const val EXTRA_CONTROL_PLAY = 1 const val EXTRA_CONTROL_PAUSE = 2
- أنشِئ قائمة تتضمّن
RemoteActions
لعناصر التحكّم في نافذة "نافذة ضمن النافذة". - بعد ذلك، أضِف
BroadcastReceiver
وألغِonReceive()
لضبط إجراءات كل زر. يمكنك استخدامDisposableEffect
لتسجيل جهاز الاستقبال والإجراءات عن بُعد. عندما لا يكون بإمكان اللاعب تسجيل المتلقّي، يُرجى إلغاء تسجيله.@RequiresApi(Build.VERSION_CODES.O) @Composable fun PlayerBroadcastReceiver(player: Player?) { val isInPipMode = rememberIsInPipMode() if (!isInPipMode || player == null) { // Broadcast receiver is only used if app is in PiP mode and player is non null return } val context = LocalContext.current DisposableEffect(player) { val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) { return } when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) { EXTRA_CONTROL_PAUSE -> player.pause() EXTRA_CONTROL_PLAY -> player.play() } } } ContextCompat.registerReceiver( context, broadcastReceiver, IntentFilter(ACTION_BROADCAST_CONTROL), ContextCompat.RECEIVER_NOT_EXPORTED ) onDispose { context.unregisterReceiver(broadcastReceiver) } } }
- مرِّر قائمة بالإجراءات عن بُعد إلى
PictureInPictureParams.Builder
:val context = LocalContext.current val pipModifier = modifier.onGloballyPositioned { layoutCoordinates -> val builder = PictureInPictureParams.Builder() builder.setActions( listOfRemoteActions() ) if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) { val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect() builder.setSourceRectHint(sourceRect) builder.setAspectRatio( Rational(player.videoSize.width, player.videoSize.height) ) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setAutoEnterEnabled(shouldEnterPipMode) } context.findActivity().setPictureInPictureParams(builder.build()) } VideoPlayer(modifier = pipModifier)
الخطوات التالية
تعلّمنا في هذا الدليل أفضل الممارسات الخاصة بإضافة ميزة "نافذة ضمن النافذة" في Compose ما قبل الإصدار 12 من نظام التشغيل Android وما بعده.
- يمكنك الانتقال إلى تطبيق Socialite للاطّلاع على أفضل ممارسات Compose PiP بشكل عملي.
- يمكنك الاطّلاع على إرشادات حول تصميم بروتوكول "نافذة ضمن النافذة" (PiP) للحصول على مزيد من المعلومات.