O picture-in-picture (PiP) é um tipo especial do modo de várias janelas usado principalmente para a reprodução de vídeos. Ele permite que o usuário assista 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, registre o alterne sua atividade para o modo picture-in-picture conforme necessário e verifique se os elementos da interface ficam ocultos e a reprodução do vídeo continua quando a atividade está no modo picture-in-picture.
Este guia descreve como adicionar o PiP no Compose ao seu app com uma implementação de vídeo do Compose. Acesse o app Socialite para conferir as melhores práticas de IA 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ê vai fazer o seguinte: usando o picture-in-picture no seu app. Adicionar
configChanges
e defini-lo comoorientation|screenLayout|screenSize|smallestScreenSize
para especificar sua atividade lida com mudanças de configuração de layout. Dessa forma, sua atividade não é reiniciado quando ocorrem alterações 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:
- Adicionar essa extensão ao
Context
. Você usará essa extensão várias vezes ao longo do 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 picture-in-picture ao sair do app para versões anteriores ao Android 12
Para adicionar o PiP em versões anteriores ao Android 12, use addOnUserLeaveHintProvider
. Siga
estas etapas para adicionar o PiP para versões anteriores ao Android 12:
- Adicione uma porta de versão para que esse código seja acessado apenas nas versões O até R.
- Use um
DisposableEffect
comContext
como a chave. - No
DisposableEffect
, defina o comportamento quando oonUserLeaveHintProvider
é acionado usando uma lambda. Na lambda, chameenterPictureInPictureMode()
emfindActivity()
e transmitaPictureInPictureParams.Builder().build()
- Adicione
addOnUserLeaveHintListener
usandofindActivity()
e transmita a lambda. - Em
onDispose
, adicioneremoveOnUserLeaveHintListener
usandofindActivity()
e transmitir 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 info", "API does not support PiP") }
Adicionar picture-in-picture ao sair do app para o pós-Android 12
Após o Android 12, a PictureInPictureParams.Builder
é adicionada usando uma
transmitido ao player de vídeo do app.
- Crie uma
modifier
e chameonGloballyPositioned
nela. O 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. Em caso afirmativo, adicionesetAutoEnterEnabled
como o builder e o defina comotrue
para entrar no picture-in-picture. ao deslizar. Isso proporciona uma animação mais suaveenterPictureInPictureMode
- Use
findActivity()
para chamarsetPictureInPictureParams()
. Ligar parabuild()
em obuilder
e transmiti-lo.
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 picture-in-picture com um botão
Para entrar no modo PiP com um clique no botão, chame
enterPictureInPictureMode()
em findActivity()
.
Os parâmetros já foram 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 os parâmetros 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!") }
Processar a interface no modo picture-in-picture
Quando você entra no modo picture-in-picture, toda a interface do app entra nessa janela, a menos que você especificar como a interface vai ficar dentro e fora do modo picture-in-picture.
Primeiro, você precisa saber quando seu app está no modo picture-in-picture ou não. Você pode usar
OnPictureInPictureModeChangedProvider
para fazer isso.
O código abaixo informa se o aplicativo 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() }
Conferir 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 for interrompido ou pausado.
- Se você estiver em uma página diferente do app do player de vídeo.
Para controlar quando seu 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 no estado de reprodução do player, adicione um listener ao player. Alterna o estado da variável de estado com base em se o player está sendo reproduzido ou não:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Alternar o estado com base no lançamento do jogador
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 é usado (anteriores ao Android 12)
- Como a adição do PiP antes do Android 12 usa um
DisposableEffect
, é necessário criar uma nova variável comrememberUpdatedState
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 o
OnUserLeaveHintListener
for acionado, adicione uma instruçãoif
com a variável de estado ao redor 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 info", "API does not support PiP") }
Usar o estado para definir se o modo picture-in-picture é usado (após o Android 12)
Transmita a variável de estado ao setAutoEnterEnabled
para que o app só entre
Modo picture-in-picture 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)
Use setSourceRectHint
para implementar uma animação suave
A API setSourceRectHint
cria uma animação mais suave ao entrar no picture-in-picture.
modo No Android 12 e versões mais recentes, ele também cria uma animação mais suave para sair do modo picture-in-picture.
Adicione essa API ao builder do picture-in-picture para indicar a área da atividade que está
visível após a transição para o picture-in-picture.
- Só adicione
setSourceRectHint()
àbuilder
se o estado definir que o deve entrar no modo picture-in-picture. Isso evita calcularsourceRect
quando o app não precisará entrar no modo picture-in-picture. - Para definir o valor
sourceRect
, use oslayoutCoordinates
fornecidos pela funçãoonGloballyPositioned
no modificador. - Chame
setSourceRectHint()
nobuilder
e transmita osourceRect
variável.
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 PiP, escolha uma proporção
específica ou use a largura e a altura do tamanho do vídeo do player. Se você for
usando um player de mídia3, verifique se o player não é nulo e se a
o tamanho do vídeo não é igual a VideoSize.UNKNOWN
antes de definir o aspecto
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 do player. e largura usando a sintaxe específica do seu player. Se o player for redimensionado durante a inicialização, se ele estiver fora dos limites válidos de qual pode ser a proporção, o app vai falhar. Talvez seja necessário adicionar verificações quando a proporção pode ser calculada, semelhante ao que é feito para um player media3.
Adicionar ações remotas
Para adicionar controles (reproduzir, pausar etc.) à janela picture-in-picture, crie um
RemoteAction
para cada controle que você quer adicionar.
- Adicione constantes para os 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 substituaonReceive()
para definir o 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 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 das 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ê conheceu as práticas recomendadas para adicionar picture-in-picture no Compose versões anteriores ao Android 12 e posteriores ao Android 12.
- Consulte o app Socialite para ver as práticas recomendadas de picture-in-picture do Compose em ação.
- Consulte as orientações de design de PiP para mais informações.