Picture-in-picture (PIP) est un type particulier de mode multifenêtre principalement utilisé pour la lecture de vidéos. Elle permet à l'utilisateur de regarder une vidéo dans une petite fenêtre épinglée à un coin de l'écran tout en naviguant entre des applications ou en parcourant du contenu sur l'écran principal.
PIP exploite les API multifenêtres disponibles dans Android 7.0 pour fournir la fenêtre de superposition vidéo épinglée. Pour ajouter le mode PIP à votre application, vous devez enregistrer votre activité, passer en mode PIP si nécessaire, et vous assurer que les éléments d'interface utilisateur sont masqués et que la lecture de la vidéo se poursuit lorsque l'activité est en mode PIP.
Ce guide explique comment ajouter le mode PIP dans Compose à votre application avec une implémentation vidéo Compose. Consultez l'application Socialite pour voir ces bonnes pratiques en action.
Configurer votre application pour le PIP
Dans le tag d'activité de votre fichier AndroidManifest.xml
, procédez comme suit:
- Ajoutez
supportsPictureInPicture
et définissez-le surtrue
pour déclarer que vous utiliserez PIP dans votre application. Ajoutez
configChanges
et définissez-le surorientation|screenLayout|screenSize|smallestScreenSize
pour spécifier que votre activité gère les modifications de configuration de la mise en page. Ainsi, votre activité ne sera pas relancée lorsque des modifications de mise en page se produisent lors des transitions en mode PIP.<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
Dans votre code Compose, procédez comme suit:
- Ajoutez cette extension sur
Context
. Vous utiliserez cette extension plusieurs fois tout au long du guide pour accéder à l'activité.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") }
Ajout du mode PIP en cas d'absence pour les versions antérieures à Android 12
Pour ajouter le PIP pour les versions antérieures à Android 12, utilisez addOnUserLeaveHintProvider
. Pour ajouter le mode PiP sur une version antérieure à Android 12, procédez comme suit:
- Ajoutez une porte de version afin que ce code ne soit accessible que dans les versions O jusqu'à R.
- Utilisez un
DisposableEffect
avecContext
comme clé. - Dans
DisposableEffect
, définissez le comportement correspondant au déclenchement deonUserLeaveHintProvider
à l'aide d'un lambda. Dans le lambda, appelezenterPictureInPictureMode()
surfindActivity()
et transmettezPictureInPictureParams.Builder().build()
. - Ajoutez
addOnUserLeaveHintListener
à l'aide defindActivity()
et transmettez le lambda. - Dans
onDispose
, ajoutezremoveOnUserLeaveHintListener
à l'aide defindActivity()
et transmettez le 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") }
Ajout du mode PIP en cas d'absence pour une application postérieure à Android 12
Après Android 12, le PictureInPictureParams.Builder
est ajouté via un modificateur transmis au lecteur vidéo de l'application.
- Créez un
modifier
et appelezonGloballyPositioned
sur celui-ci. Les coordonnées de mise en page seront utilisées lors d'une prochaine étape. - Créez une variable pour
PictureInPictureParams.Builder()
. - Ajoutez une instruction
if
pour vérifier si le SDK est en S ou supérieur. Si tel est le cas, ajoutezsetAutoEnterEnabled
au compilateur et définissez-le surtrue
pour passer en mode PIP lorsque l'utilisateur balaie l'écran. L'animation est plus fluide que si vous passiez parenterPictureInPictureMode
. - Utilisez
findActivity()
pour appelersetPictureInPictureParams()
. Appelezbuild()
surbuilder
et transmettez-le.
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)
Ajouter PIP via un bouton
Pour passer en mode PIP via un clic sur un bouton, appelez enterPictureInPictureMode()
sur findActivity()
.
Les paramètres sont déjà définis par les appels précédents de PictureInPictureParams.Builder
. Vous n'avez donc pas besoin de définir de nouveaux paramètres dans le compilateur. Toutefois, si vous souhaitez modifier des paramètres lors d'un clic sur un bouton, vous pouvez les définir ici.
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!") }
Gérer l'interface utilisateur en mode PIP
Lorsque vous passez en mode PIP, l'ensemble de l'interface utilisateur de votre application entre dans la fenêtre PIP, sauf si vous spécifiez son apparence en mode PIP et en dehors.
Tout d'abord, vous devez savoir si votre application est en mode PIP. Pour ce faire, vous pouvez utiliser OnPictureInPictureModeChangedProvider
.
Le code ci-dessous vous indique si votre application est en mode PIP.
@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 } }
Vous pouvez maintenant utiliser rememberIsInPipMode()
pour activer/désactiver les éléments d'interface utilisateur à afficher lorsque l'application passe en mode PIP:
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() }
Assurez-vous que votre application passe en mode PIP au bon moment
Votre application ne doit pas passer en mode PIP dans les cas suivants:
- Indique si la vidéo est interrompue ou mise en pause.
- Vous vous trouvez sur une page de l'application différente de celle du lecteur vidéo.
Pour contrôler le moment où votre application passe en mode PIP, ajoutez une variable qui suit l'état du lecteur vidéo à l'aide d'un mutableStateOf
.
Activer/Désactiver l'état selon si la vidéo est en cours de lecture
Pour activer ou désactiver l'état en fonction de la lecture ou non du lecteur vidéo, ajoutez un écouteur sur le lecteur vidéo. Basculez l'état de votre variable d'état selon que le lecteur est en lecture ou non:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Activer/Désactiver l'état en fonction de la sortie du lecteur
Une fois le lecteur libéré, définissez votre variable d'état sur false
:
fun releasePlayer() { shouldEnterPipMode = false }
Utiliser l'état pour déterminer si le mode PIP doit être activé (versions antérieures à Android 12)
- Étant donné que l'ajout de PiP aux versions antérieures à la version 12 utilise un
DisposableEffect
, vous devez créer une variable parrememberUpdatedState
avecnewValue
défini comme variable d'état. Cela garantit que la version mise à jour est utilisée dans leDisposableEffect
. Dans le lambda qui définit le comportement lors du déclenchement de
OnUserLeaveHintListener
, ajoutez une instructionif
avec la variable d'état autour de l'appel à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") }
Utiliser l'état pour déterminer si le mode PIP doit être activé (après Android 12)
Transmettez votre variable d'état à setAutoEnterEnabled
afin que votre application ne passe en mode PIP qu'au bon moment:
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)
Utiliser setSourceRectHint
pour implémenter une animation fluide
L'API setSourceRectHint
crée une animation plus fluide lorsque vous passez en mode PIP. Dans Android 12 et versions ultérieures, elle crée également une animation plus fluide pour quitter le mode PIP.
Ajoutez cette API au compilateur PIP pour indiquer la zone de l'activité visible après la transition vers le mode PIP.
- N'ajoutez
setSourceRectHint()
àbuilder
que si l'état indique que l'application doit passer en mode PIP. Cela évite de calculersourceRect
lorsque l'application n'a pas besoin d'entrer PIP. - Pour définir la valeur
sourceRect
, utilisez leslayoutCoordinates
fournis à partir de la fonctiononGloballyPositioned
sur le modificateur. - Appelez
setSourceRectHint()
surbuilder
et transmettez la variablesourceRect
.
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)
Utiliser setAspectRatio
pour définir le format de la fenêtre PIP
Pour définir le format de la fenêtre PIP, vous pouvez choisir un format spécifique ou utiliser la largeur et la hauteur de la taille de la vidéo du lecteur. Si vous utilisez un lecteur media3, vérifiez que la valeur du lecteur n'est pas "null" et que la taille de la vidéo n'est pas égale à VideoSize.UNKNOWN
avant de définir le format.
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)
Si vous utilisez un lecteur personnalisé, définissez les proportions sur la hauteur et la largeur du lecteur à l'aide de la syntaxe spécifique à votre lecteur. Sachez que si votre lecteur est redimensionné lors de l'initialisation et qu'il ne respecte pas les limites valides du format, votre application plantera. Vous devrez peut-être vérifier le moment où le format peut être calculé, de la même manière que pour un lecteur media3.
Ajouter des actions à distance
Si vous souhaitez ajouter des commandes (lecture, pause, etc.) à votre fenêtre PIP, créez un RemoteAction
pour chaque commande à ajouter.
- Ajoutez des constantes pour vos commandes de diffusion :
// 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
- Créez une liste de
RemoteActions
pour les commandes de votre fenêtre PIP. - Ajoutez ensuite un
BroadcastReceiver
et ignorezonReceive()
pour définir les actions de chaque bouton. Utilisez unDisposableEffect
pour enregistrer le destinataire et les actions à distance. Une fois le lecteur supprimé, annulez l'enregistrement du destinataire.@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) } } }
- Transmettez la liste de vos actions à distance à
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)
Étapes suivantes
Dans ce guide, vous avez découvert les bonnes pratiques à suivre pour ajouter le PIP dans Compose avant et après Android 12.
- Consultez l'application Socialite pour voir les bonnes pratiques de PIP pour Compose.
- Pour en savoir plus, consultez les conseils de conception PiP.