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

Le Picture-in-picture (PIP) est un type spécial de mode multifenêtre principalement utilisé pour la lecture des vidéos. Elle permet à l'utilisateur de regarder une vidéo dans une petite fenêtre épinglée à situé dans un coin de l'écran pendant que vous naviguez d'une application à une autre ou lorsque vous parcourez du contenu sur la l'écran principal.

Il exploite les API multifenêtres disponibles dans Android 7.0 pour fournir la une vidéo en superposition épinglée. Pour ajouter le PIP à votre application, vous devez enregistrer votre l'activité, passez en mode PIP si nécessaire et assurez-vous que les éléments de l'UI sont masquées et 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 à l'aide d'une vidéo Compose. la mise en œuvre. Accédez à l'appli Socialite pour découvrir les meilleures pratiques en action.

Configurer votre application pour le mode PiP

Dans la balise 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 serez à l'aide du mode 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. De cette façon, votre activité ne redémarre pas en cas de modification de la mise en page 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")
    }

Ajouter le mode PiP en quittant l'application pour les versions antérieures à Android 12

Pour ajouter la fonctionnalité PIP pour les versions antérieures à Android 12, utilisez addOnUserLeaveHintProvider. Pour ajouter le mode PiP pour les versions antérieures à 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 lorsque onUserLeaveHintProvider est déclenché à 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 transmettre 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 info", "API does not support PiP")
}

Ajouter le mode PIP en quittant l'application pour les versions ultérieures à Android 12

Après Android 12, le PictureInPictureParams.Builder est ajouté via un qui est 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 S ou supérieur. Si c'est le cas, ajoutez setAutoEnterEnabled au compilateur et le définir sur true pour entrer dans le mode PIP. lorsque vous balayez l'écran. Cela permet d'obtenir une animation plus fluide enterPictureInPictureMode
  4. Utilisez findActivity() pour appeler setPictureInPictureParams(). Appeler build() au builder et le transmettre.

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 le mode PiP via un bouton

Pour passer en mode PIP en cliquant sur un bouton, appelez enterPictureInPictureMode() sur findActivity().

Ces paramètres sont déjà définis par les appels précédents à la fonction PictureInPictureParams.Builder. Vous n'avez donc pas besoin de définir de nouveaux paramètres. sur le compilateur. Toutefois, si vous souhaitez modifier des paramètres 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 votre UI en mode PIP

Lorsque vous passez en mode PIP, toute l'UI de votre application entre dans la fenêtre PIP, sauf si vous spécifier l'apparence de votre UI en mode PIP et en dehors.

Tout d'abord, vous devez savoir si votre application est en mode PiP ou non. 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 situations suivantes:

  • Indique si la vidéo est arrêtée ou mise en pause.
  • Si vous vous trouvez sur une page de l'application différente de celle du lecteur vidéo.

Pour contrôler à quel moment 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 que la vidéo est en cours de lecture

Pour changer l'état selon que le lecteur vidéo est en cours de lecture, ajoutez un écouteur sur le lecteur vidéo. Activez ou désactivez l'état de votre variable d'état en fonction de la présence ou non du joueur :

player.addListener(object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        shouldEnterPipMode = isPlaying
    }
})

Activer/Désactiver l'état selon que le lecteur est relâché

Lorsque le lecteur est libéré, définissez votre variable d'état sur false :

fun releasePlayer() {
    shouldEnterPipMode = false
}

Utiliser l'état pour définir si le mode PiP est activé (avant Android 12)

  1. Étant donné que l'ajout du PIP antérieur à la version 12 utilise un DisposableEffect, vous devez créer une nouvelle variable de rememberUpdatedState en définissant newValue comme variable d'état. Vous aurez ainsi l'assurance que la version mise à jour sera utilisée dans le DisposableEffect
  2. Dans le lambda qui définit le comportement lorsque OnUserLeaveHintListener est déclenché, ajoutez une instruction if avec la variable d'état autour de l'appel de 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")
    }

Utiliser l'état pour déterminer si le mode PIP est activé (postérieur à Android 12)

Transmettez votre variable d'état à setAutoEnterEnabled afin que votre application n'entre que le mode PIP 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 pour passer en mode PiP. Dans Android 12 et versions ultérieures, cette fonctionnalité 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é qui est visible après la transition vers le mode PIP.

  1. N'ajoutez setSourceRectHint() à builder que si l'état définit que le l'application devrait passer en mode PIP. Cela évite de calculer sourceRect lorsque l'application n'a pas besoin de passer en mode PIP.
  2. Pour définir la valeur sourceRect, utilisez les layoutCoordinates fournis. à partir de la fonction onGloballyPositioned du modificateur.
  3. Appelez setSourceRectHint() sur builder et transmettez 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 taille de la vidéo du lecteur. Si vous utilisez à l'aide d'un lecteur media3, vérifiez que la valeur du lecteur n'est pas nulle et que la valeur la taille de la vidéo n'est pas égale à VideoSize.UNKNOWN avant de définir l'aspect ratio.

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 le format sur la hauteur du lecteur et sa largeur à l'aide de la syntaxe propre à votre lecteur. Sachez que si votre lecteur est redimensionnée lors de l'initialisation, s'il n'est pas compris quel que soit le format, votre application plantera. Vous devrez peut-être ajouter des vérifications pour savoir quand le format peut être calculé, comme c'est le cas pour un lecteur media3.

Ajouter des actions à distance

Si vous souhaitez ajouter des commandes (lecture, pause, etc.) à votre fenêtre PIP, créez une RemoteAction pour chaque commande à ajouter.

  1. Ajoutez des constantes à 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 remplacez onReceive() pour définir les actions de chaque bouton. Utilisez un DisposableEffect pour enregistrer le récepteur et les actions à distance. Une fois le joueur éliminé, annulez l'enregistrement 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 une 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 mode PiP dans Compose avant et après Android 12.