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 bloccata in un angolo dello schermo mentre naviga tra le app o sfoglia i contenuti sulla schermata principale.
PiP sfrutta le API multi-finestra rese disponibili in Android 7.0 per fornire la finestra overlay video fissata. Per aggiungere PIP all'app, devi registrare la tua attività, passare alla tua attività in modalità PIP, se necessario, e assicurarti che gli elementi UI siano nascosti e che la riproduzione del video continui quando l'attività è in modalità PIP.
Questa guida descrive come aggiungere PIP in Compose alla tua app con un'implementazione di video di Compose. Guarda l'app Socialite per vedere come funzionano queste best practice.
Configurare l'app per PIP
Nel tag attività del file AndroidManifest.xml
, procedi nel seguente modo:
- Aggiungi
supportsPictureInPicture
e impostalo sutrue
per dichiarare che userai PIP nell'app. Aggiungi
configChanges
e impostalo suorientation|screenLayout|screenSize|smallestScreenSize
per specificare che la tua attività gestisce le modifiche alla configurazione del layout. In questo modo, l'attività non viene riavviata quando si verificano modifiche al layout durante le transizioni alla 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:
- Aggiungi questa estensione su
Context
. Utilizzerai questa estensione più volte nella 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 un'app PIP all'uscita per versioni precedenti ad Android 12
Per aggiungere la funzionalità PIP per le versioni precedenti ad Android 12, usa addOnUserLeaveHintProvider
. Per aggiungere la funzionalità PIP per le versioni precedenti ad Android 12:
- Aggiungi una porta di versione in modo che questo codice sia accessibile solo nelle versioni da O fino alla versione R.
- Utilizza un
DisposableEffect
conContext
come chiave. - All'interno di
DisposableEffect
, definisci il comportamento per l'attivazione dionUserLeaveHintProvider
utilizzando una funzione lambda. Nella lambda, chiamaenterPictureInPictureMode()
sufindActivity()
e passaPictureInPictureParams.Builder().build()
. - Aggiungi
addOnUserLeaveHintListener
usandofindActivity()
e passa nella lambda. - In
onDispose
, aggiungiremoveOnUserLeaveHintListener
usandofindActivity()
e passa nella 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") }
Aggiungere un'app PIP all'uscita dopo Android 12
Dopo Android 12, l'elemento PictureInPictureParams.Builder
viene aggiunto tramite un
modificatore che viene trasmesso al video player dell'app.
- Crea un
modifier
e chiamaonGloballyPositioned
. Le coordinate di layout verranno utilizzate in un passaggio successivo. - Crea una variabile per
PictureInPictureParams.Builder()
. - Aggiungi un'istruzione
if
per verificare se l'SDK è S o superiore. In questo caso, aggiungisetAutoEnterEnabled
al builder e impostalo sutrue
per attivare la modalità PIP allo scorrimento. In questo modo avrai un'animazione più fluida rispetto a quella offerta daenterPictureInPictureMode
. - Usa
findActivity()
per chiamaresetPictureInPictureParams()
. Chiamabuild()
sulbuilder
e trasmettilo.
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 tramite un clic su un pulsante, chiama
enterPictureInPictureMode()
su findActivity()
.
I parametri sono già impostati dalle chiamate precedenti a PictureInPictureParams.Builder
, quindi non è necessario impostarne di nuovi nel builder. Tuttavia, se vuoi modificare parametri
al clic sul pulsante, puoi impostarli 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 l'UI in modalità PIP
Quando attivi la modalità PIP, l'intera UI dell'app entra nella finestra PIP, a meno che tu non specifichi l'aspetto dell'interfaccia utente dentro e fuori la modalità PIP.
Innanzitutto, devi sapere quando la tua app è in modalità PIP o meno. Puoi utilizzare OnPictureInPictureModeChangedProvider
per raggiungere questo obiettivo.
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 usare 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
La tua app non deve attivare la modalità PIP nelle seguenti situazioni:
- Se il video viene interrotto o messo in pausa.
- Se ti trovi su una pagina dell'app diversa da quella del video player.
Per controllare quando la tua 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 in base alla riproduzione del video player, aggiungi un listener sul video player. Attiva/disattiva lo stato della variabile di stato in base al fatto che il player sia 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 }
Utilizza lo stato per definire se è stata attivata la modalità PIP (prima di Android 12)
- Poiché l'aggiunta di PIP pre-12 utilizza un elemento
DisposableEffect
, devi creare una nuova variabile tramiterememberUpdatedState
connewValue
impostato come variabile di stato. Ciò garantirà che la versione aggiornata venga utilizzata all'interno diDisposableEffect
. Nella funzione lambda che definisce il comportamento quando viene attivato
OnUserLeaveHintListener
, aggiungi un'istruzioneif
con la variabile di stato relativa 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_TAG, "API does not support PiP") }
Usa lo stato per definire se è stata attivata la modalità PIP (dopo Android 12)
Trasmetti la tua variabile di stato in setAutoEnterEnabled
in modo che la tua app entri in modalità PIP solo 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 l'accesso alla modalità PIP. In Android 12 e versioni successive, crea anche un'animazione più fluida per l'uscita dalla modalità PIP.
Aggiungi questa API al generatore PIP per indicare l'area dell'attività visibile dopo la transizione a PIP.
- Aggiungi
setSourceRectHint()
abuilder
solo se lo stato definisce che l'app deve passare in modalità PIP. Questo evita di calcolaresourceRect
quando l'app non deve inserire PIP. - Per impostare il valore
sourceRect
, utilizza il valorelayoutCoordinates
fornito dalla funzioneonGloballyPositioned
nel modificatore. - Richiama
setSourceRectHint()
subuilder
e passa nella variabilesourceRect
.
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 oppure utilizzare la larghezza e l'altezza delle dimensioni del video del player. Se utilizzi un player Media3, verifica che il player non sia null e che le dimensioni del video del player non siano uguali a VideoSize.UNKNOWN
prima di impostare le proporzioni.
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 in base all'altezza e alla larghezza del player con la sintassi specifica del tuo player. Tieni presente che se il player viene ridimensionato durante l'inizializzazione e non rientra nei limiti validi delle proporzioni, l'app si arresta in modo anomalo. Potresti dover aggiungere controlli per capire quando è possibile calcolare le proporzioni, come si fa per un player media3.
Aggiungi azioni remote
Se vuoi aggiungere controlli (riproduzione, pausa e così via) alla finestra Picture in picture, crea un elemento RemoteAction
per ogni controllo che vuoi aggiungere.
- Aggiungi costanti per i controlli della 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. - Successivamente, aggiungi un elemento
BroadcastReceiver
e sostituiscionReceive()
per impostare le azioni di ogni pulsante. Utilizza un elementoDisposableEffect
per registrare il ricevitore e le azioni remote. Quando il player è pronto, annulla la registrazione del ricevitore.@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) } } }
- Passa un elenco delle tue azioni da remoto a
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 sia prima che dopo Android 12.
- Guarda l'app Socialite per vedere le best practice di Compose PiP in azione.
- Per ulteriori informazioni, consulta le linee guida sulla progettazione PIP.