O picture-in-picture (PiP) é um tipo especial de modo de várias janelas usado principalmente para reprodução de vídeos. Ele permite que o usuário assista a um vídeo em uma pequena janela fixada em um canto da tela enquanto navega entre apps ou pelo conteúdo na tela principal.
O modo picture-in-picture aproveita as APIs de várias janelas disponíveis no Android 7.0 para fornecer a janela fixa de sobreposição de vídeo. Para adicionar o picture-in-picture ao seu app, é necessário registrar a atividade, alternar para o modo picture-in-picture conforme necessário e garantir que os elementos da interface estejam ocultos e a reprodução do vídeo continue quando a atividade estiver no modo picture-in-picture.
Este guia descreve como adicionar o picture-in-picture no Compose ao seu app com uma implementação de vídeo do Compose. Acesse o app Socialite para conferir essas práticas recomendadas em ação.
Configurar seu app para picture-in-picture
Na tag de atividade do arquivo AndroidManifest.xml
, faça o seguinte:
- Adicione
supportsPictureInPicture
e defina-o comotrue
para declarar que você usará o picture-in-picture no seu app. Adicione
configChanges
e defina-o comoorientation|screenLayout|screenSize|smallestScreenSize
para especificar que sua atividade processa as mudanças de configuração de layout. Dessa forma, a atividade não será reiniciada quando ocorrerem mudanças de layout durante as transições do modo picture-in-picture.<activity android:name=".SnippetsActivity" android:exported="true" android:supportsPictureInPicture="true" android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize" android:theme="@style/Theme.Snippets">
No código do Compose, faça o seguinte:
- Adicione essa extensão no
Context
. Você usará essa extensão várias vezes em todo o guia para acessar a atividade.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") }
Adicionar o app PiP ao sair para versões anteriores ao Android 12
Para adicionar o picture-in-picture às versões anteriores ao Android 12, use addOnUserLeaveHintProvider
. Siga
estas etapas para adicionar o picture-in-picture a versões anteriores ao Android 12:
- Adicione uma porta de versão para que esse código seja acessado somente na versão O até R.
- Use um
DisposableEffect
comContext
como chave. - Dentro do
DisposableEffect
, defina o comportamento para quando aonUserLeaveHintProvider
for acionada usando uma lambda. Na lambda, chameenterPictureInPictureMode()
emfindActivity()
e transmitaPictureInPictureParams.Builder().build()
. - Adicione
addOnUserLeaveHintListener
usandofindActivity()
e transmita a lambda. - Em
onDispose
, adicioneremoveOnUserLeaveHintListener
usandofindActivity()
e transmita a 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") }
Adicionar o app PiP ao sair após o Android 12
Após o Android 12, o PictureInPictureParams.Builder
é adicionado por um
modificador transmitido ao player de vídeo do app.
- Crie uma
modifier
e chameonGloballyPositioned
nela. As coordenadas do layout serão usadas em uma etapa posterior. - Crie uma variável para o
PictureInPictureParams.Builder()
. - Adicione uma instrução
if
para verificar se o SDK é S ou mais recente. Se esse for o caso, adicionesetAutoEnterEnabled
ao builder e defina-o comotrue
para entrar no modo de picture-in-picture ao deslizar. Isso proporciona uma animação mais suave do que passar porenterPictureInPictureMode
. - Use
findActivity()
para chamarsetPictureInPictureParams()
. Chamebuild()
nobuilder
e transmita-o.
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)
Adicionar o picture-in-picture usando um botão
Para entrar no modo picture-in-picture clicando em um botão, chame
enterPictureInPictureMode()
em findActivity()
.
Os parâmetros já são definidos por chamadas anteriores para o
PictureInPictureParams.Builder
. Portanto, não é necessário definir novos parâmetros
no builder. No entanto, se você quiser mudar algum parâmetro ao clicar no botão, defina-os aqui.
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!") }
Gerenciar a interface no modo picture-in-picture
Quando você entra no modo picture-in-picture, toda a interface do app entra na janela picture-in-picture, a menos que você especifique como será a aparência da interface dentro e fora desse modo.
Primeiro, você precisa saber quando o app está ou não no modo picture-in-picture. Você pode usar
OnPictureInPictureModeChangedProvider
para fazer isso.
O código abaixo informa se o app está no modo picture-in-picture.
@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 } }
Agora, você pode usar rememberIsInPipMode()
para alternar quais elementos da interface serão mostrados
quando o app entrar no modo picture-in-picture:
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() }
Verificar se o app entra no modo picture-in-picture nos momentos certos
Seu app não deve entrar no modo picture-in-picture nas seguintes situações:
- Se o vídeo foi interrompido ou pausado.
- Se você estiver em uma página do app diferente da que está no player de vídeo.
Para controlar quando o app entra no modo picture-in-picture, adicione uma variável que rastreie o estado
do player de vídeo usando um mutableStateOf
.
Alternar o estado com base na reprodução do vídeo
Para alternar o estado com base na reprodução do player de vídeo, adicione um listener. Alterne o estado da sua variável de estado com base na reprodução ou não do player:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Alterna o estado com base na liberação do player
Quando o player for liberado, defina a variável de estado como false
:
fun releasePlayer() { shouldEnterPipMode = false }
Usar o estado para definir se o modo picture-in-picture está no modo picture-in-picture (antes do Android 12)
- Como a adição do picture-in-picture pré-12 usa uma
DisposableEffect
, é necessário criar uma nova variável porrememberUpdatedState
comnewValue
definido como sua variável de estado. Isso garante que a versão atualizada seja usada noDisposableEffect
. Na lambda que define o comportamento quando a
OnUserLeaveHintListener
é acionada, adicione uma instruçãoif
com a variável de estado em torno da chamada paraenterPictureInPictureMode()
: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") }
Usar o estado para definir se o modo picture-in-picture foi ativado (após o Android 12)
Transmita a variável de estado para setAutoEnterEnabled
para que o app entre
no modo picture-in-picture apenas no momento certo:
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)
Usar setSourceRectHint
para implementar uma animação suave.
A API setSourceRectHint
cria uma animação mais suave para entrar no modo
de picture-in-picture. No Android 12 e versões mais recentes, ela também cria uma animação mais suave para sair do modo picture-in-picture.
Adicione essa API ao builder do PIP para indicar a área da atividade que vai ficar
visível após a transição para o picture-in-picture.
- Adicione
setSourceRectHint()
aobuilder
somente se o estado definir que o app precisa entrar no modo picture-in-picture. Isso evitará calcularsourceRect
quando o app não precisar inserir o picture-in-picture. - Para definir o valor de
sourceRect
, use olayoutCoordinates
fornecido pela funçãoonGloballyPositioned
no modificador. - Chame
setSourceRectHint()
embuilder
e transmita a variávelsourceRect
.
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)
Usar setAspectRatio
para definir a proporção da janela picture-in-picture
Para definir a proporção da janela picture-in-picture, escolha uma proporção
específica ou use a largura e a altura do tamanho do vídeo do player. Se você estiver
usando um player media3, confira se ele não é nulo e se o tamanho do
vídeo dele não é igual a VideoSize.UNKNOWN
antes de definir a proporção.
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 você estiver usando um player personalizado, defina a proporção na altura e largura dele usando a sintaxe específica dele. Se o player for redimensionado durante a inicialização, se ele ficar fora dos limites válidos da proporção, o app vai falhar. Talvez seja necessário adicionar verificações sobre quando a proporção pode ser calculada, de forma semelhante a como isso é feito para um player de media3.
Adicionar ações remotas
Se você quiser adicionar controles (reproduzir, pausar etc.) à janela do picture-in-picture, crie um
RemoteAction
para cada controle.
- Adicione constantes para seus controles de transmissão:
// 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
- Crie uma lista de
RemoteActions
para os controles na janela picture-in-picture. - Em seguida, adicione um
BroadcastReceiver
e modifiqueonReceive()
para definir as ações de cada botão. Use umDisposableEffect
para registrar o receptor e as ações remotas. Quando o jogador for descartado, cancele a inscrição do receptor.@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) } } }
- Transmita uma lista de suas ações remotas para o
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)
Próximas etapas
Neste guia, você aprendeu as práticas recomendadas para adicionar o picture-in-picture no Compose antes e depois do Android 12.
- Consulte o app Socialite para conferir as práticas recomendadas do PIP do Compose em ação.
- Para mais informações, consulte as orientações de design de PIP.