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:
- Aggiungi
supportsPictureInPicture
e impostalo sutrue
per dichiarare che dovrai usando PIP nella tua app. Aggiungi
configChanges
e impostala suorientation|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:
- 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:
- Aggiungi un gate di versione in modo che sia possibile accedere a questo codice solo nelle versioni O fino alla R.
- Utilizza un
DisposableEffect
conContext
come chiave. - All'interno di
DisposableEffect
, definisci il comportamento relativo a quandoonUserLeaveHintProvider
viene attivato utilizzando un comando lambda. In lambda, chiamaenterPictureInPictureMode()
sufindActivity()
e passaPictureInPictureParams.Builder().build()
. - Aggiungi
addOnUserLeaveHintListener
usandofindActivity()
e passa il lambda. - In
onDispose
, aggiungiremoveOnUserLeaveHintListener
utilizzandofindActivity()
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.
- Crea un
modifier
e chiamaonGloballyPositioned
. Le coordinate del layout verranno utilizzate in un passaggio successivo. - Crea una variabile per
PictureInPictureParams.Builder()
. - Aggiungi un'istruzione
if
per verificare se l'SDK è S o versioni successive. In questo caso, aggiungisetAutoEnterEnabled
al generatore e impostalo sutrue
per attivare la modalità PiP con uno scorrimento. In questo modo l'animazione sarà più fluida rispetto a quella successivaenterPictureInPictureMode
- Usa
findActivity()
per chiamaresetPictureInPictureParams()
. Chiamabuild()
subuilder
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)
- Poiché l'aggiunta di PIP pre-12 utilizza un elemento
DisposableEffect
, devi creare una nuova variabile dirememberUpdatedState
connewValue
impostato come tuo di stato. In questo modo ti assicuri che la versione aggiornata venga utilizzata all'interno delDisposableEffect
. Nel lambda che definisce il comportamento quando
OnUserLeaveHintListener
, aggiungi un'istruzioneif
con la variabile di stato intorno alla chiamata aenterPictureInPictureMode()
: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.
- Aggiungi
setSourceRectHint()
allabuilder
solo se lo stato definisce che dovrebbe attivare la modalità PIP. In questo modo non viene calcolato ilsourceRect
quando l'app non deve inserire PIP. - Per impostare il valore
sourceRect
, utilizza gli attributilayoutCoordinates
forniti dalla funzioneonGloballyPositioned
sul modificatore. - Chiama
setSourceRectHint()
sulbuilder
e passa nelsourceRect
.
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.
- 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
- Crea un elenco di
RemoteActions
per i controlli nella finestra PIP. - Poi, aggiungi un
BroadcastReceiver
e sostituiscionReceive()
per impostare azioni di ciascun pulsante. Utilizza unDisposableEffect
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) } } }
- 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.
- Vai all'app Socialite per scoprire le best practice di Componi PIP in azione.
- Per saperne di più, consulta le linee guida per la progettazione PIP.