Resposta rápida: use Picture-in-Picture (PiP) no Android quando o usuário precisa continuar assistindo ou acompanhando uma experiência contínua enquanto navega para outro app. Em Kotlin, declare suporte no manifesto com android:supportsPictureInPicture="true" e configChanges adequados, construa um PictureInPictureParams com aspect ratio e ações, chame enterPictureInPictureMode(params) no momento certo (botão explícito, home ou fim da Activity) e adapte a UI — especialmente com Jetpack Compose e Media3/ExoPlayer — para um layout mínimo dentro da janela flutuante. PiP não é um “mini app genérico”: ele funciona melhor para vídeo, chamada, navegação turn-by-turn ou qualquer superfície cuja pausa quebra o valor da tarefa.
Em produtos brasileiros de educação, streaming interno, atendimento, telemedicina, delivery com mapa ao vivo e fintech com onboarding por vídeo, o usuário frequentemente precisa responder mensagem, abrir outro app ou conferir um documento sem perder o contexto. Sem PiP, a opção ruim vira gravar a tela, forçar landscape fullscreen ou manter um foreground service só para “não morrer” — soluções mais caras e menos claras. Este guia mostra como implementar PiP de forma correta em 2026, com Compose, Media3, parâmetros, gestos, testes e checklist de produção.
Antes de fixar detalhes de API ou comportamento por OEM, consulte a documentação oficial de Picture-in-Picture. Fabricantes e versões do Android podem alterar tamanho mínimo, gestos e disponibilidade; projete degradação segura.
Quando Picture-in-Picture é a escolha certa
Faça três perguntas:
- Há uma experiência contínua que o usuário quer manter visível? (vídeo, chamada, rota, monitoramento)
- O valor cai se a Activity sair de primeiro plano sem uma janela pequena?
- A UI cabe de forma útil em uma janela reduzida?
Três “sim” → PiP. Se a resposta for “quero só manter trabalho em background”, prefira WorkManager, MediaSession ou foreground service — PiP é interface, não mecanismo de sobrevivência do processo.
| Cenário | PiP faz sentido? | Alternativa melhor |
|---|---|---|
| Player de curso / streaming | Sim | — |
| Chamada de vídeo | Sim | Telecom / ConnectionService quando for chamada de verdade |
| Navegação turn-by-turn | Sim | Widget ou notificação rica se o mapa não for central |
| Upload longo com progresso | Não | Foreground service + notificação |
| Sync adiável | Não | WorkManager |
| Abrir deep link e voltar | Não | App Links / deep links |
| Atalho para tarefa rápida | Não | App Shortcuts |
Regra de produto
PiP é bom quando completa esta frase:
“A pessoa precisa continuar vendo isto enquanto faz outra coisa.”
“Continuar processando no servidor” não é PiP. “Continuar assistindo a aula enquanto anota no Notion” é PiP.
Manifesto e Activity preparados para PiP
Declare suporte na Activity que hospeda o player ou a superfície contínua:
<activity
android:name=".PlayerActivity"
android:exported="false"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:resizeableActivity="true">
</activity>
Pontos importantes:
supportsPictureInPicture="true"habilita o contrato com o sistema.configChangesevita recriação destrutiva ao entrar/sair de PiP; você ainda precisa reagir a mudanças de layout.resizeableActivity="true"é coerente com janelas multi-window e PiP em dispositivos modernos.- Não declare PiP em toda Activity do app. Reserve para a tela que realmente continua útil em miniatura.
Se o app usa uma única Activity com Compose Navigation, a Activity raiz pode declarar suporte, mas só deve entrar em PiP quando o destino atual for elegível (player, chamada, mapa ao vivo). Entrar em PiP a partir de uma lista ou formulário gera uma janela vazia e reclamações.
Construindo PictureInPictureParams
O coração da API moderna é PictureInPictureParams. Em Kotlin:
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.graphics.Rect
import android.util.Rational
import android.os.Build
fun buildPipParams(
sourceRect: Rect?,
aspectRatio: Rational = Rational(16, 9),
actions: List<RemoteAction> = emptyList(),
autoEnter: Boolean = true,
seamlessResize: Boolean = true,
): PictureInPictureParams {
val builder = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
.setActions(actions)
if (sourceRect != null) {
builder.setSourceRectHint(sourceRect)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(autoEnter)
builder.setSeamlessResizeEnabled(seamlessResize)
}
return builder.build()
}
Aspect ratio
Use a proporção real do conteúdo, não a da tela cheia. Vídeo 16:9, chamada 3:4 ou 9:16, mapa amplo conforme o layout. Ratio inválido ou extremo pode fazer o sistema rejeitar ou cortar mal a janela.
sourceRectHint
Informe o retângulo do player na tela cheia. Isso melhora a animação de entrada: o sistema “encolhe” a partir da área do vídeo, em vez de um salto genérico. Em Compose, obtenha o bounds com onGloballyPositioned e converta para coordenadas de tela.
Auto-enter (Android 12+)
Com setAutoEnterEnabled(true), o app pode entrar em PiP automaticamente quando o usuário toca Home ou troca de app, desde que os params estejam atualizados. Isso evita o padrão antigo de interceptar onUserLeaveHint() em todas as versões — ainda útil como fallback, mas não é a história principal em 2026.
Entrando em Picture-in-Picture no momento certo
Há três gatilhos comuns:
- Botão explícito “PiP” / “Flutuante” no player.
- Saída da Activity (Home, app switcher) com auto-enter habilitado.
- Evento de produto (usuário aceita chamada enquanto está em outra tela do app e você promove a superfície).
Exemplo com botão e fallback:
fun Activity.enterPipOrExplain(params: PictureInPictureParams) {
if (!packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
// Dispositivo sem suporte: esconda o botão ou explique a limitação
return
}
val entered = enterPictureInPictureMode(params)
if (!entered) {
// Params inválidos, OEM bloqueou ou Activity não está em estado elegível
// Registre analytics e mantenha o player em tela cheia
}
}
Não chame enterPictureInPictureMode em onCreate nem em resposta a deep link frio. O usuário precisa estar em uma sessão ativa. Também não entre em PiP quando o conteúdo está pausado sem intenção de continuar: janela preta ou estática parece bug.
onUserLeaveHint como fallback
Em APIs mais antigas, ainda é comum:
override fun onUserLeaveHint() {
super.onUserLeaveHint()
if (shouldEnterPipOnLeave && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
enterPictureInPictureMode(buildPipParams(sourceRectHint))
}
}
Em Android 12+, prefira params com auto-enter atualizados continuamente enquanto o conteúdo elegível estiver ativo. Assim o sistema decide com mais previsibilidade.
Adaptando Jetpack Compose para a janela PiP
Compose não muda a API de PiP, mas muda a responsabilidade da UI: a mesma hierarquia precisa viver bem em tela cheia e em miniatura.
Padrão prático:
var inPipMode by remember { mutableStateOf(false) }
DisposableEffect(Unit) {
val activity = context.findActivity()
val callback = object : Consumer<Boolean> {
override fun accept(isInPip: Boolean) {
inPipMode = isInPip
}
}
// Em Activity: sobrescreva onPictureInPictureModeChanged e atualize um StateFlow/CompositionLocal
onDispose { }
}
Scaffold { padding ->
if (inPipMode) {
PipPlayerSurface(
player = player,
modifier = Modifier.fillMaxSize()
)
} else {
FullPlayerScreen(
player = player,
onEnterPip = { activity.enterPipOrExplain(params) },
modifier = Modifier.padding(padding)
)
}
}
O que esconder em PiP
Na janela pequena, remova ou compacte:
- top app bars, abas e navegação inferior;
- listas de episódios, comentários e recomendações;
- formulários, banners e CTAs promocionais;
- controles densos demais para o tamanho mínimo.
Mantenha:
- superfície de vídeo / mapa / câmera;
- play/pause e, se couber, próxima/anterior via
RemoteAction; - indicador de estado (ao vivo, mudo, reconectando).
Se o app já trata edge-to-edge e WindowInsets, revise paddings: em PiP as barras do sistema e gestos mudam, e insets de tela cheia podem sobrar como espaço morto.
Source rect em Compose
var playerRect by remember { mutableStateOf<Rect?>(null) }
AndroidView(
factory = { ctx -> PlayerView(ctx).apply { this.player = exoPlayer } },
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
.onGloballyPositioned { coordinates ->
val pos = coordinates.positionInWindow()
val size = coordinates.size
playerRect = Rect(
pos.x.toInt(),
pos.y.toInt(),
(pos.x + size.width).toInt(),
(pos.y + size.height).toInt()
)
}
)
Atualize setPictureInPictureParams sempre que o retângulo ou o ratio mudarem de forma relevante (rotação, troca de mídia, entrada em fullscreen imersivo).
Integração com Media3 / ExoPlayer
PiP e Media3 andam juntos na maior parte dos apps de vídeo. O player deve:
- Continuar a reprodução ao entrar em PiP (não pause no
onPausecegamente). - Pausar ou liberar somente quando a sessão realmente terminar.
- Integrar
MediaSessionpara fones, controles de notificação e Android Auto quando aplicável.
Evite este erro clássico:
override fun onPause() {
super.onPause()
player.pause() // quebra PiP: a Activity pausa, mas o vídeo deveria seguir
}
Prefira distinguir estados:
override fun onPause() {
super.onPause()
if (!isInPictureInPictureMode) {
player.pauseIfPolicyRequires()
}
}
Ou, melhor, conduza pausa/play por política de produto + lifecycle de mídia, não por onPause genérico. O guia de Media3 com Compose cobre criação, release() e AndroidView; aqui o ponto extra é não tratar PiP como background descartável.
Para áudio em background sem vídeo visível, MediaSession + notificação (e às vezes foreground service de mídia) costuma ser mais adequado do que uma janela PiP preta.
RemoteActions: controles na janela
PictureInPictureParams.setActions aceita uma lista pequena de RemoteAction (play/pause, pular, encerrar chamada). Cada ação precisa de ícone, título acessível e PendingIntent seguro.
Boas práticas:
- limite-se a 2–3 ações essenciais;
- localize títulos (“Pausar”, “Próximo”, “Encerrar”);
- use
PendingIntentimutável e explícito; - atualize as ações quando o estado mudar (playing ↔ paused);
- não coloque ação destrutiva sem rótulo claro.
RemoteActions não substituem uma UI Compose rica; elas existem porque a janela PiP é pequena demais para a chrome completa do app.
Ciclo de vida, multi-window e predictive back
Ao entrar em PiP, a Activity passa por mudanças de ciclo de vida semelhantes a multi-window. Consequências práticas:
- coroutines e coletores ligados à UI devem tolerar
onPausesem cancelar o playback; - analytics deve registrar
pip_enter/pip_exitsem duplicarscreen_view; - se você usa predictive back, defina se o gesto sai do fullscreen, fecha o player ou encerra PiP — documente a regra;
- em foldables e telas grandes, combine PiP com o pensamento de layouts adaptativos / window size class: o usuário pode já estar em multi-window sem precisar de PiP.
Também atualize params em onPictureInPictureModeChanged:
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
pipState.value = isInPictureInPictureMode
if (isInPictureInPictureMode) {
hideChrome()
} else {
restoreChrome()
}
}
Permissões, políticas e expectativas do Play
PiP em si não pede uma permissão perigosa clássica, mas o contexto do app pode:
- câmera/microfone em chamada — siga o guia de permissões Android;
- notificação de mídia / chamada — canal correto e, quando aplicável, foreground service type honesto;
- overlay de outros tipos não é PiP: não misture com
SYSTEM_ALERT_WINDOW.
Do ponto de vista de produto e store:
- não use PiP para anúncio flutuante;
- não force PiP em fluxos onde o usuário só quer sair;
- se o conteúdo exigir login e a sessão expirar em PiP, mostre estado mínimo e encerre com dignidade — não deixe uma janela autenticada “vazando” dados.
Como testar Picture-in-Picture
Checklist mínimo de QA:
- Botão PiP entra na janela com aspect ratio correto.
- Home / app switcher entra via auto-enter quando elegível.
- Conteúdo continua reproduzindo (ou mantém estado esperado).
- Play/pause por RemoteAction funciona com processo sob pressão de memória.
- Rotação e mudança de ratio (trailer 16:9 → story 9:16) atualizam params.
- Sair de PiP restaura chrome sem duplicar ViewModel nem recriar
ExoPlayer. - Deep link aberto em paralelo não “rouba” a Activity de forma confusa.
- Dispositivo sem feature esconde a opção.
- OEM com gestos diferentes: arrastar para fechar, expandir e pin não quebra o player.
- Acessibilidade: ações nomeadas, contraste do frame mínimo, foco não preso fora da janela.
Automatizar 100% de PiP em emulador é limitado; cubra a lógica de params e estados com testes de unidade, e valide animação/gestos em dispositivo físico. Para regressão da tela cheia ao redor, mantenha a disciplina do cluster de testes Compose / instrumentados do site.
Erros comuns
Pausar o player em todo onPause
A Activity pausa ao entrar em PiP. Se você pausa a mídia ali, o recurso perde o sentido. Distinga PiP de background real.
Aspect ratio “chutado”
Usar Rational(1, 1) para tudo gera letterbox feio ou rejeição. Derive do vídeo/mapa atual.
Entrar em PiP a partir de tela errada
Lista, settings ou onboarding em PiP produzem janela inútil. Gateie pelo destino elegível.
Esquecer setPictureInPictureParams atualizado
Auto-enter com params velhos (ratio antigo, actions de outro estado, sourceRect errado) gera animação ruim e controles incorretos.
Tratar PiP como overlay infinito
Fechar PiP deve ser fácil. Não lute contra o gesto do sistema nem recrie a janela em loop.
Misturar PiP com “app forever”
Se a necessidade é trabalho contínuo sem UI flutuante, use foreground service / MediaSession. PiP é para ver, não para burlar limites de background.
Ignorar Compose chrome
Deixar TopAppBar, chips e listas visíveis na miniatura torna o conteúdo ilegível. Tenha um layout PiP dedicado.
Checklist de produção
Antes de liberar PiP:
- manifesto declara suporte só na Activity correta;
- feature check esconde UI em dispositivos sem suporte;
-
PictureInPictureParamsusa ratio real esourceRectHint; - auto-enter habilitado onde a versão permitir;
- player não pausa só porque
onPausedisparou em PiP; - layout Compose mínimo para a janela;
- RemoteActions limitadas, localizadas e atualizadas;
- analytics de enter/exit sem contagem duplicada;
- sessão expirada e erros de rede têm estado claro;
- saída de PiP restaura UI sem recriar o player;
- QA em dispositivo físico com gestos de fechar/expandir;
- política de produto não usa PiP para spam ou anúncio.
Perguntas frequentes
Picture-in-Picture funciona com Jetpack Compose?
Sim. Compose renderiza a UI; a Activity chama as APIs de PiP. O trabalho extra é reagir a onPictureInPictureModeChanged, simplificar o layout e não recriar o player na recomposição.
Preciso de Media3 para usar PiP?
Não. PiP é do framework Android. Media3 é o caminho usual quando o conteúdo é vídeo/áudio. Chamadas, mapas e outras superfícies também podem usar PiP sem ExoPlayer.
Qual a diferença entre PiP e multi-window?
Multi-window divide a tela entre apps ou Activities. PiP cria uma janela flutuante especial, tipicamente pequena, com regras próprias de actions e elegibilidade. Um app pode viver nos dois mundos, mas os layouts e o ciclo de vida precisam ser testados separadamente.
Posso abrir PiP automaticamente ao tocar Home?
A partir do Android 12, sim, com setAutoEnterEnabled(true) e params atualizados enquanto o conteúdo elegível estiver ativo. Em versões anteriores, o padrão clássico envolve onUserLeaveHint().
Quantas RemoteActions posso exibir?
O sistema impõe um limite pequeno. Trate como espaço premium: play/pause e uma ação secundária costumam bastar. Não tente portar a toolbar inteira do app.
PiP consome muita bateria?
A janela em si não é o vilão — o conteúdo contínuo (câmera, decoding de vídeo, GPS) é. Otimize o player, pause quando o usuário pausar, e não mantenha sensores ligados sem necessidade. Para trabalho adiável, volte ao WorkManager.
Conclusão
Picture-in-Picture é uma ponte entre “o usuário quer sair” e “o usuário ainda precisa ver o que estava fazendo”. A implementação sólida em Kotlin combina manifesto correto, PictureInPictureParams honestos, UI Compose reduzida, playback que sobrevive a onPause e disciplina de produto para não transformar PiP em gambiarra de background.
Comece pelo caso mais claro do seu app — em geral um player Media3 com botão PiP, auto-enter e layout mínimo. Meça se as pessoas expandem de volta, fecham rápido ou concluem a tarefa paralela sem perder o contexto. Se o recurso ajudar de verdade, expanda para chamada ou mapa; se a janela ficar ociosa, simplifique em vez de forçar presença flutuante.
Para fechar o cluster de mídia e ciclo de vida no Android, avance a partir deste guia para Media3/ExoPlayer com Compose, foreground services e notificações push com FCM — cada um resolve um pedaço diferente do mesmo problema: continuar útil quando o app deixa de ocupar a tela inteira.