ميزة "نافذة ضمن النافذة" (PiP) هي نوع خاص من وضع النوافذ المتعددة يُستخدَم عادةً في تشغيل الفيديو. يتيح للمستخدم مشاهدة مقطع فيديو في نافذة صغيرة مثبّتة في زاوية الشاشة أثناء التنقل بين التطبيقات أو تصفح المحتوى على الشاشة الرئيسية.
تستفيد ميزة "نافذة ضمن النافذة" من واجهات برمجة التطبيقات ذات النوافذ المتعددة المتاحة في الإصدار 7.0 من نظام التشغيل Android لتوفير نافذة تراكب الفيديو المثبتة. لإضافة ميزة "نافذة ضمن النافذة" إلى تطبيقك، عليك تسجيل نشاطك وتبديله إلى وضع "نافذة ضمن النافذة" حسب الحاجة والتأكّد من أنّه تم إخفاء عناصر واجهة المستخدم ومواصلة تشغيل الفيديو عندما يكون النشاط في وضع "نافذة ضمن النافذة".
يوضِّح هذا الدليل كيفية إضافة ميزة "نافذة ضمن النافذة" في نافذة Compose إلى تطبيقك باستخدام فيديو Compose. التنفيذ. يمكنك الاطّلاع على تطبيق Socialite للتعرّف على أفضل الممارسات المتعلّقة بالتسويق على الشبكات الاجتماعية.
إعداد تطبيقك ليتم عرضه في وضع "نافذة ضمن النافذة"
في علامة النشاط لملف AndroidManifest.xml
، نفِّذ ما يلي:
- إضافة
supportsPictureInPicture
وضبطه علىtrue
للإعلان عن أنّك ستكون باستخدام الميزة "نافذة ضمن النافذة" (PIP) في تطبيقك. إضافة
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") }
إضافة ميزة "وضع الصورة في الصورة" عند مغادرة التطبيق على الإصدارات الأقدم من Android 12
لإضافة ميزة "نافذة ضمن النافذة" للإصدارات الأقدم من Android 12، استخدِم addOnUserLeaveHintProvider
. متابعة
الخطوات التالية لإضافة ميزة "نافذة ضمن النافذة" على الأجهزة التي تعمل بالإصدارات الأقدم من نظام Android 12:
- أضف بوابة إصدار بحيث يتم الوصول إلى هذا الرمز فقط في الإصدارات O حتى R.
- استخدِم
DisposableEffect
معContext
كمفتاح. - وداخل
DisposableEffect
، حدِّد السلوك الذي سيتم عنده يتم تشغيلonUserLeaveHintProvider
باستخدام دالة lambda. في lambda، اطلبenterPictureInPictureMode()
علىfindActivity()
وتمريرهPictureInPictureParams.Builder().build()
- أضِف
addOnUserLeaveHintListener
باستخدامfindActivity()
وأدخِل دالة lambda. - في
onDispose
، إضافةremoveOnUserLeaveHintListener
باستخدامfindActivity()
وأمرر في لامدا.
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 info", "API does not support PiP") }
إضافة ميزة "نافذة ضمن النافذة" (PIP) إلى تطبيق "المغادرة" بعد Android 12
بعد الإصدار Android 12، تتم إضافة 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)
إضافة "نافذة ضمن النافذة" باستخدام زرّ
للدخول إلى وضع "نافذة ضمن النافذة" من خلال النقر على زر، عليك الاتصال
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() }
تأكَّد من دخول التطبيق في وضع "نافذة ضمن النافذة" في الأوقات المناسبة.
يجب ألا يدخل التطبيق في وضع "نافذة ضمن النافذة" (PIP) في الحالات التالية:
- إذا تم إيقاف الفيديو أو إيقافه مؤقتًا
- إذا كنت في صفحة مختلفة من التطبيق غير صفحة مشغّل الفيديو
للتحكّم في وقت دخول التطبيق في وضع "نافذة ضمن النافذة"، أضِف متغيّرًا يتتبّع الحالة.
في مشغّل الفيديو باستخدام mutableStateOf
.
تبديل الحالة استنادًا إلى ما إذا كان الفيديو يتم تشغيله
لتبديل الحالة حسب ما إذا كان مشغّل الفيديو قيد التشغيل، عليك إضافة مستمع على مشغّل الفيديو. بدِّل حالة متغيّر الحالة استنادًا إلى ما إذا كان اللاعب يلعب أم لا:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
يمكنك تبديل الحالة بناءً على ما إذا تم تحرير اللاعب.
عند إصدار المُشغِّل، اضبط متغيّر الحالة على false
:
fun releasePlayer() { shouldEnterPipMode = false }
استخدام الحالة لتحديد ما إذا كان سيتم تفعيل وضع "نافذة ضمن النافذة" (قبل Android 12)
- بما أنّ إضافة "نافذة ضمن النافذة (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 info", "API does not support PiP") }
استخدام الحالة لتحديد ما إذا تم تفعيل وضع "نافذة ضمن النافذة" (بعد إصدار Android 12)
أدخِل متغيّر الحالة إلى setAutoEnterEnabled
كي يدخل تطبيقك فقط.
وضع "نافذة ضمن النافذة" في الوقت المناسب:
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 والإصدارات الأحدث، يمكن أيضًا إنشاء صور متحركة أكثر سلاسة للخروج من وضع "نافذة ضمن النافذة".
أضف واجهة برمجة التطبيقات هذه إلى أداة إنشاء "نافذة ضمن النافذة" للإشارة إلى منطقة النشاط
بعد الانتقال إلى وضع "نافذة ضمن النافذة".
- لا تُضِف
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)
إذا كنت تستخدم مشغّلاً مخصّصًا، عليك ضبط نسبة العرض إلى الارتفاع على ارتفاع المشغّل. والعرض باستخدام بناء الجملة الخاص باللاعب. يُرجى العِلم أنّه إذا تم تغيير حجم المشغّل أثناء الإعداد، سيتعطّل تطبيقك إذا خرج عن الحدود الصالحة لنسبة قياس الارتفاع إلى العرض. قد تحتاج إلى إضافة عمليات تحقق حول للوقت الذي يمكن فيه حساب نسبة العرض إلى الارتفاع، على غرار الطريقة التي يتم فيها حساب الوسائط3 لاعب.
إضافة إجراءات عن بُعد
إذا أردت إضافة عناصر تحكّم (تشغيل أو إيقاف مؤقت أو غير ذلك) إلى نافذة "نافذة ضمن النافذة"، أنشِئ
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 12.
- اطّلع على تطبيق التواصل الاجتماعي لمعرفة أفضل ممارسات إنشاء "نافذة ضمن النافذة" بشكل عملي
- يمكنك الاطّلاع على إرشادات تصميم ميزة "نافذة ضمن النافذة" للحصول على مزيد من المعلومات.