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:

  1. Há uma experiência contínua que o usuário quer manter visível? (vídeo, chamada, rota, monitoramento)
  2. O valor cai se a Activity sair de primeiro plano sem uma janela pequena?
  3. 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árioPiP faz sentido?Alternativa melhor
Player de curso / streamingSim
Chamada de vídeoSimTelecom / ConnectionService quando for chamada de verdade
Navegação turn-by-turnSimWidget ou notificação rica se o mapa não for central
Upload longo com progressoNãoForeground service + notificação
Sync adiávelNãoWorkManager
Abrir deep link e voltarNãoApp Links / deep links
Atalho para tarefa rápidaNãoApp 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.
  • configChanges evita 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:

  1. Botão explícito “PiP” / “Flutuante” no player.
  2. Saída da Activity (Home, app switcher) com auto-enter habilitado.
  3. 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:

  1. Continuar a reprodução ao entrar em PiP (não pause no onPause cegamente).
  2. Pausar ou liberar somente quando a sessão realmente terminar.
  3. Integrar MediaSession para 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 PendingIntent imutá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 onPause sem cancelar o playback;
  • analytics deve registrar pip_enter / pip_exit sem duplicar screen_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:

  1. Botão PiP entra na janela com aspect ratio correto.
  2. Home / app switcher entra via auto-enter quando elegível.
  3. Conteúdo continua reproduzindo (ou mantém estado esperado).
  4. Play/pause por RemoteAction funciona com processo sob pressão de memória.
  5. Rotação e mudança de ratio (trailer 16:9 → story 9:16) atualizam params.
  6. Sair de PiP restaura chrome sem duplicar ViewModel nem recriar ExoPlayer.
  7. Deep link aberto em paralelo não “rouba” a Activity de forma confusa.
  8. Dispositivo sem feature esconde a opção.
  9. OEM com gestos diferentes: arrastar para fechar, expandir e pin não quebra o player.
  10. 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;
  • PictureInPictureParams usa ratio real e sourceRectHint;
  • auto-enter habilitado onde a versão permitir;
  • player não pausa só porque onPause disparou 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.