Ajouter le Picture-in-picture (PIP) à votre application avec un lecteur vidéo Compose

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:

  1. Ajoutez supportsPictureInPicture et définissez-le sur true pour déclarer que vous utiliserez PIP dans votre application.
  2. Ajoutez configChanges et définissez-le sur orientation|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:

  1. 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:

  1. Ajoutez une porte de version afin que ce code ne soit accessible que dans les versions O jusqu'à R.
  2. Utilisez un DisposableEffect avec Context comme clé.
  3. Dans DisposableEffect, définissez le comportement correspondant au déclenchement de onUserLeaveHintProvider à l'aide d'un lambda. Dans le lambda, appelez enterPictureInPictureMode() sur findActivity() et transmettez PictureInPictureParams.Builder().build().
  4. Ajoutez addOnUserLeaveHintListener à l'aide de findActivity() et transmettez le lambda.
  5. Dans onDispose, ajoutez removeOnUserLeaveHintListener à l'aide de findActivity() 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.

  1. Créez un modifier et appelez onGloballyPositioned sur celui-ci. Les coordonnées de mise en page seront utilisées lors d'une prochaine étape.
  2. Créez une variable pour PictureInPictureParams.Builder().
  3. Ajoutez une instruction if pour vérifier si le SDK est en S ou supérieur. Si tel est le cas, ajoutez setAutoEnterEnabled au compilateur et définissez-le sur true pour passer en mode PIP lorsque l'utilisateur balaie l'écran. L'animation est plus fluide que si vous passiez par enterPictureInPictureMode.
  4. Utilisez findActivity() pour appeler setPictureInPictureParams(). Appelez build() sur builder 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)

  1. Étant donné que l'ajout de PiP aux versions antérieures à la version 12 utilise un DisposableEffect, vous devez créer une variable par rememberUpdatedState avec newValue défini comme variable d'état. Cela garantit que la version mise à jour est utilisée dans le DisposableEffect.
  2. Dans le lambda qui définit le comportement lors du déclenchement de OnUserLeaveHintListener, ajoutez une instruction if 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.

  1. N'ajoutez setSourceRectHint() à builder que si l'état indique que l'application doit passer en mode PIP. Cela évite de calculer sourceRect lorsque l'application n'a pas besoin d'entrer PIP.
  2. Pour définir la valeur sourceRect, utilisez les layoutCoordinates fournis à partir de la fonction onGloballyPositioned sur le modificateur.
  3. Appelez setSourceRectHint() sur builder et transmettez la variable 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)

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 variable 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)
        builder.setAspectRatio(
            Rational(sourceRect.width(), sourceRect.height())
        )
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

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.

  1. 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
  2. Créez une liste de RemoteActions pour les commandes de votre fenêtre PIP.
  3. Ajoutez ensuite un BroadcastReceiver et ignorez onReceive() pour définir les actions de chaque bouton. Utilisez un DisposableEffect 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)
            }
        }
    }
  4. 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) {
            val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
            builder.setSourceRectHint(sourceRect)
            builder.setAspectRatio(
                Rational(sourceRect.width(), sourceRect.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.