Aggiungere Picture in picture (PIP) alla tua app con un video player di Compose

La modalità Picture in picture (PIP) è un tipo speciale di modalità multi-finestra utilizzata principalmente per la riproduzione di video. Consente all'utente di guardare un video in una piccola finestra fissata a un nell'angolo dello schermo durante la navigazione tra le app o la consultazione di contenuti sul schermata principale.

PIP sfrutta le API multi-finestra rese disponibili in Android 7.0 per fornire finestra overlay video fissata. Per aggiungere PIP alla tua app, devi registrare il tuo l'attività, passa alla modalità PIP in base alle tue esigenze e verifica che gli elementi UI sono nascosti e la riproduzione del video continua quando l'attività è in modalità PIP.

Questa guida descrive come aggiungere PIP in Compose alla tua app con un video di Compose implementazione. Vai sull'app Socialite per vedere i risultati migliori pratiche in azione.

Configurare l'app per PiP

Nel tag attività del file AndroidManifest.xml, procedi nel seguente modo:

  1. Aggiungi supportsPictureInPicture e impostalo su true per dichiarare che dovrai usando PIP nella tua app.
  2. Aggiungi configChanges e impostala su orientation|screenLayout|screenSize|smallestScreenSize per specificare la tua attività gestisce le modifiche alla configurazione del layout. In questo modo, la tua attività non viene riavviato quando vengono apportate modifiche al layout durante le transizioni in modalità PIP.

      <activity
        android:name=".SnippetsActivity"
        android:exported="true"
        android:supportsPictureInPicture="true"
        android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
        android:theme="@style/Theme.Snippets">

Nel codice di Compose, segui questi passaggi:

  1. Aggiungi questa estensione su Context. Userai questa estensione più volte della guida per accedere all'attività.
    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")
    }

Aggiungere PIP all'app per uscire dalle versioni precedenti ad Android 12

Per aggiungere PIP per le versioni precedenti ad Android 12, utilizza addOnUserLeaveHintProvider. Segui questi passaggi per aggiungere PIP per le versioni precedenti ad Android 12:

  1. Aggiungi un gate di versione in modo che sia possibile accedere a questo codice solo nelle versioni O fino alla R.
  2. Utilizza un DisposableEffect con Context come chiave.
  3. All'interno di DisposableEffect, definisci il comportamento relativo a quando onUserLeaveHintProvider viene attivato utilizzando un comando lambda. In lambda, chiama enterPictureInPictureMode() su findActivity() e passa PictureInPictureParams.Builder().build().
  4. Aggiungi addOnUserLeaveHintListener usando findActivity() e passa il lambda.
  5. In onDispose, aggiungi removeOnUserLeaveHintListener utilizzando findActivity() e passiamo il 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")
}

Aggiungere PiP all'app di abbandono per le versioni successive ad Android 12

Dopo Android 12, la PictureInPictureParams.Builder viene aggiunta tramite una che viene passato al video player dell'app.

  1. Crea un modifier e chiama onGloballyPositioned. Le coordinate del layout verranno utilizzate in un passaggio successivo.
  2. Crea una variabile per PictureInPictureParams.Builder().
  3. Aggiungi un'istruzione if per verificare se l'SDK è S o versioni successive. In questo caso, aggiungi setAutoEnterEnabled al generatore e impostalo su true per attivare la modalità PiP con uno scorrimento. In questo modo l'animazione sarà più fluida rispetto a quella successiva enterPictureInPictureMode
  4. Usa findActivity() per chiamare setPictureInPictureParams(). Chiama build() su builder e lo passiamo.

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)

Aggiungere PiP tramite un pulsante

Per attivare la modalità PIP con un clic su un pulsante, chiama enterPictureInPictureMode() su findActivity().

I parametri sono già impostati da chiamate precedenti al parametro PictureInPictureParams.Builder, quindi non è necessario impostare nuovi parametri. sul builder. Tuttavia, per modificare i parametri del pulsante fare clic, puoi impostarle qui.

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!")
}

Gestire la UI in modalità PIP

Quando attivi la modalità PIP, l'intera UI dell'app entra nella finestra PIP, a meno che tu specificare l'aspetto della UI in e fuori dalla modalità PIP.

Innanzitutto, devi sapere se la tua app è attiva o meno in modalità PIP. Per farlo, puoi utilizzare OnPictureInPictureModeChangedProvider. Il codice riportato di seguito indica se la tua app è in modalità 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
    }
}

Ora puoi utilizzare rememberIsInPipMode() per attivare/disattivare gli elementi UI da mostrare Quando l'app entra in modalità 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()
}

Assicurati che l'app entri in modalità PIP al momento giusto

L'app non deve attivare la modalità PIP nelle seguenti situazioni:

  • Se il video è fermo o in pausa.
  • Se ti trovi in una pagina dell'app diversa rispetto al video player.

Per controllare quando l'app deve attivare la modalità PIP, aggiungi una variabile che monitori lo stato del video player utilizzando un elemento mutableStateOf.

Attiva/disattiva lo stato in base alla riproduzione del video

Per attivare/disattivare lo stato a seconda che il video player sia in riproduzione, aggiungi un listener nel video player. Attiva/disattiva lo stato della variabile di stato in base al fatto che il player è in riproduzione o meno:

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

Attiva/disattiva lo stato in base al rilascio del player

Quando il player viene rilasciato, imposta la variabile di stato su false:

fun releasePlayer() {
    shouldEnterPipMode = false
}

Usa lo stato per definire se viene attivata la modalità PIP (prima di Android 12)

  1. Poiché l'aggiunta di PIP pre-12 utilizza un elemento DisposableEffect, devi creare una nuova variabile di rememberUpdatedState con newValue impostato come tuo di stato. In questo modo ti assicuri che la versione aggiornata venga utilizzata all'interno del DisposableEffect.
  2. Nel lambda che definisce il comportamento quando OnUserLeaveHintListener , aggiungi un'istruzione if con la variabile di stato intorno alla chiamata a 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")
    }

Utilizza lo stato per definire se è stata attivata la modalità PiP (dopo Android 12)

Passa la variabile di stato a setAutoEnterEnabled in modo che l'app entri solo Modalità PIP al momento giusto:

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)

Utilizza setSourceRectHint per implementare un'animazione fluida

L'API setSourceRectHint crea un'animazione più fluida per accedere alla modalità PiP. In Android 12 e versioni successive, viene creata anche un'animazione più fluida per uscire dalla modalità PIP. Aggiungi questa API al generatore PIP per indicare l'area dell'attività che viene visibile dopo la transizione in PIP.

  1. Aggiungi setSourceRectHint() alla builder solo se lo stato definisce che dovrebbe attivare la modalità PIP. In questo modo non viene calcolato il sourceRect quando l'app non deve inserire PIP.
  2. Per impostare il valore sourceRect, utilizza gli attributi layoutCoordinates forniti dalla funzione onGloballyPositioned sul modificatore.
  3. Chiama setSourceRectHint() sul builder e passa nel 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)

Usa setAspectRatio per impostare le proporzioni della finestra PIP

Per impostare le proporzioni della finestra PiP, puoi scegliere proporzioni specifiche o utilizzare la larghezza e l'altezza delle dimensioni del video del player. Se utilizzando un player media3, controlla che il player non sia null e che le dimensioni del video non sono uguali a VideoSize.UNKNOWN prima di impostare l'aspetto rapporto.

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)

Se utilizzi un player personalizzato, imposta le proporzioni all'altezza del player. e la larghezza usando la sintassi specifica del player. Tieni presente che se il tuo player viene ridimensionato durante l'inizializzazione, se non rientra nei limiti validi le proporzioni possono essere, l'app avrà un arresto anomalo. Potresti dover aggiungere dei controlli quando è possibile calcolare le proporzioni, come avviene per un media3 un player.

Aggiungi azioni da remoto

Se vuoi aggiungere controlli (riproduzione, pausa ecc.) alla finestra PIP, crea un RemoteAction per ogni controllo che vuoi aggiungere.

  1. Aggiungi costanti per i controlli di trasmissione:
    // 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. Crea un elenco di RemoteActions per i controlli nella finestra PIP.
  3. Poi, aggiungi un BroadcastReceiver e sostituisci onReceive() per impostare azioni di ciascun pulsante. Utilizza un DisposableEffect per registrare il ricevitore e delle azioni remote. Quando il player viene eliminato, annulla la registrazione destinatario.
    @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. Trasmetti un elenco delle tue azioni remote alla 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)

Passaggi successivi

In questa guida hai appreso le best practice per aggiungere PIP in Compose versioni precedenti ad Android 12 e post-Android 12.