'תמונה בתוך תמונה' (PiP) הוא סוג מיוחד של מצב ריבוי חלונות שמשמש בעיקר הפעלת הסרטון. היא מאפשרת למשתמש לצפות בסרטון בחלון קטן המוצמד בפינת המסך בזמן ניווט בין אפליקציות או עיון בתוכן במסך הראשי.
התכונה 'תמונה בתוך תמונה' משתמשת בממשקי ה-API עם ריבוי חלונות שזמינים ב-Android 7.0 כדי לספק חלון שכבת-על של סרטון מוצמד. כדי להוסיף את 'תמונה בתוך תמונה' לאפליקציה, צריך לרשום את להעביר את הפעילות למצב 'תמונה בתוך תמונה' לפי הצורך ולוודא רכיבים בממשק המשתמש. מוסתרות והפעלת הסרטונים נמשכת כשהפעילות במצב 'תמונה בתוך תמונה'.
במדריך הזה מוסבר איך להוסיף את התכונה PiP ב-Compose לאפליקציה באמצעות הטמעת וידאו ב-Compose. כדאי להיכנס לאפליקציית Socialite כדי לראות את התמונות הכי טובות בפועל.
הגדרת האפליקציה ל'תמונה בתוך תמונה'
בתג הפעילות של הקובץ 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">
בקוד הכתיבה, מבצעים את הפעולות הבאות:
- צריך להתקין את התוסף הזה ב-
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") }
הוספת 'תמונה בתוך תמונה' באפליקציית Waze ל-Android 12 ואילך
כדי להוסיף את התכונה 'תמונה בתוך תמונה' למכשירים עם מערכת Android 12 טרום-Android, משתמשים ב-addOnUserLeaveHintProvider
. הוספה למעקב
כדי להוסיף 'תמונה בתוך תמונה' למכשירים עם Android 12, צריך לבצע את השלבים הבאים:
- מוסיפים שער גרסאות כדי שניתן יהיה לגשת לקוד הזה רק בגרסאות O עד R.
- משתמשים ב-
DisposableEffect
עםContext
בתור המפתח. - בתוך
DisposableEffect
, מגדירים את ההתנהגות כאשרonUserLeaveHintProvider
מופעל באמצעות lambda. ב-lambda, קוראיםenterPictureInPictureMode()
ב-findActivity()
והעברהPictureInPictureParams.Builder().build()
. - מוסיפים
addOnUserLeaveHintListener
באמצעותfindActivity()
ומעבירים במבדה. - ב-
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") }
הוספת 'תמונה בתוך תמונה' באפליקציית Waze ל-Android 12
אחרי Android 12, אנחנו מוסיפים את PictureInPictureParams.Builder
באמצעות
מקש צירוף שמועבר לנגן הווידאו של האפליקציה.
- צריך ליצור
modifier
ולהתקשר אליוonGloballyPositioned
. בשלב מאוחר יותר נשתמש בקואורדינטות של הפריסה. - יוצרים משתנה בשביל
PictureInPictureParams.Builder()
. - צריך להוסיף הצהרת
if
כדי לבדוק אם גרסת ה-SDK היא S ואילך. אם כן, עליך להוסיףsetAutoEnterEnabled
ל-builder ומגדירים אותו לערך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
, כך שאין צורך להגדיר פרמטרים חדשים ב-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, כל ממשק המשתמש של האפליקציה נכנס לחלון ה-PIP, אלא אם מציינים איך ממשק המשתמש אמור להיראות במצב 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
.
החלפת מצב אם הסרטון מופעל
כדי להפעיל או להשבית את מצב ההפעלה של נגן הווידאו, צריך להוסיף את ה-listener נגן הווידאו. החלפת המצב של משתנה המצב בהתאם למצב של הנגן מופעל או לא:
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
כדי להטמיע אנימציה חלקה
ה-API של setSourceRectHint
יוצר אנימציה חלקה יותר בכניסה ל'תמונה בתוך תמונה'
במצב 'סינון תוכן'. ב-Android מגרסה 12 ואילך, התכונה גם יוצרת אנימציה חלקה יותר ליציאה ממצב 'תמונה בתוך תמונה'.
צריך להוסיף את ה-API הזה ל-builder של 'תמונה בתוך תמונה' כדי לציין את אזור הפעילות.
לאחר המעבר ל'תמונה בתוך תמונה'.
- מוסיפים את
setSourceRectHint()
ל-builder
רק אם המצב מגדיר שהאפליקציה צריכה לעבור למצב PiP. כך לא צריך לחשב אתsourceRect
כשהאפליקציה לא צריכה לעבור למצב PiP. - כדי להגדיר את הערך של
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
כדי להגדיר את יחס הגובה-רוחב של חלון PiP
כדי להגדיר את יחס הגובה-רוחב של חלון 'תמונה בתוך תמונה', אפשר לבחור תמונה ספציפית
יחס גובה-רוחב או להשתמש ברוחב ובגובה של גודל הסרטון בנגן. אם אתם
באמצעות נגן 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)
אם משתמשים בנגן מותאם אישית, צריך להגדיר את יחס הגובה-רוחב לגובה של הנגן ורוחב באמצעות התחביר הספציפי לנגן שלך. חשוב לזכור שאם הנגן ישתנה במהלך האינטראקציה הראשונית, והיחס של גובה-רוחב לא יהיה בטווח האפשרי, האפליקציה תקרוס. יכול להיות שיהיה צורך להוסיף בדיקות כשניתן לחשב את יחס הגובה-רוחב, בדומה לאופן שבו מחשבים את יחס הגובה-רוחב נגן.
הוספת פעולות מרחוק
אם רוצים להוסיף פקדים (הפעלה, השהיה וכו') לחלון 'תמונה בתוך תמונה', צריך ליצור
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)
השלבים הבאים
במדריך הזה למדתם את השיטות המומלצות להוספת 'תמונה בתוך תמונה' בניסוח אוטומטי לפני Android 12 ואחרי Android 12.
- באפליקציית Socialite מפורטות שיטות מומלצות: הרכב 'תמונה בתוך תמונה' בפעולה.
- מידע נוסף זמין בהנחיות לגבי עיצוב PiP.