Usar a biblioteca Picture-in-Picture do Jetpack

A biblioteca Picture-in-picture (PiP) do Jetpack oferece uma solução simplificada e robustez para que desenvolvedores de apps Android implementem a funcionalidade PiP, principalmente para apps de reprodução de mídia, comunicação por vídeo e navegação. Ao fornecer uma API unificada, a biblioteca ajuda a eliminar o código boilerplate, bugs comuns no app e melhorar a qualidade geral da experiência do usuário com o PiP.

A biblioteca PiP Jetpack facilita as APIs PiP atuais ao resolver vários desafios e inconsistências importantes em todo o ecossistema Android:

  • Fragmentação do SO: a biblioteca processa automaticamente as diferenças nas chamadas da API PiP em várias versões do Android, como o uso de enterPictureInPictureMode antes do Android 12 e isAutoEnterEnabled depois. Assim, os desenvolvedores não precisam gerenciar as diferenças de versão.
  • Parâmetros PiP incorretos: oferece uma solução unificada para definir corretamente parâmetros PiP, por exemplo, setSourceRectHint, para criar animações suaves e de alta qualidade durante a reprodução de mídia.
  • Callbacks de estado PiP unificados: consolida onPictureInPictureModeChanged e onPictureInPictureUiStateChanged em uma única interface de callback unificada (PictureInPictureDelegate.OnPictureInPictureEventListener) para gerenciamento simplificado de estado e UI.
  • Redução do código boilerplate: a biblioteca reduz a quantidade de código repetitivo e boilerplate ao oferecer conjuntos predefinidos de RemoteActions para casos de uso comuns, como controles de reprodução e ações de videochamada.
  • Preparação para o futuro: mais recursos de PiP são fornecidos pela biblioteca Jetpack, permitindo que os usuários acessem funcionalidades adicionais com pouco ou nenhum esforço.

Workflow de migração

Identifique a categoria de caso de uso do app e a lógica PiP legada:

Categorias:Reprodução de vídeo, Navegação ou Videochamada.

Lógica legada de PiP para identificar:

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams.

2. Configuração do AndroidManifest

Verifique se a atividade que entra no modo picture-in-picture declara suporte em AndroidManifest.xml com o configChanges necessário para evitar reinicializações desnecessárias:

<activity
android:name="VideoActivity" android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation">
</activity>

3. Configuração do ambiente

Adicione as dependências necessárias ao build.gradle:

dependencies {
implementation("androidx.core:core:1.18.0")
implementation("androidx.activity:activity:1.13.0")
implementation("androidx.core:core-pip:1.0.0-alpha02") }

Use as bibliotecas AndroidX mais recentes para as dependências e consulte a página de lançamentos para mais informações.

4. Seleção e inicialização de modelos

Escolha o modelo de implementação que melhor se adapta ao caso de uso do app:

  • Navegação e videochamada: BasicPictureInPicture; o redimensionamento contínuo geralmente não é compatível, e você não precisa de uma dica de retângulo de origem.
  • Reprodução de vídeo: VideoPlaybackPictureInPicture; rastreia automaticamente os limites da visualização do player para a dica de retângulo de origem e permite o redimensionamento contínuo por padrão.

Para adotar a biblioteca Jetpack, substitua a implementação personalizada de PiP pelas APIs da biblioteca Jetpack. A complexidade e o custo da adoção variam de acordo com a implementação atual do app.

As seções a seguir descrevem alguns dos casos de uso típicos do PiP e as etapas de implementação necessárias:

O app informa à biblioteca o estado ativo ou inativo da navegação e define a proporção. A biblioteca Jetpack cuida do restante.

Principais diferenças:

  1. Não é necessário diferenciar a entrada automática e a entrada legada no lado do app.
  2. Interfaces de callback consolidadas.
  3. Novo criador de PictureInPictureParams para compatibilidade com versões anteriores.

Videochamada

O app informa à biblioteca o estado ativo ou inativo da chamada e define a proporção da tela.

Principais diferenças:

  1. Não é necessário diferenciar a entrada automática e a entrada legada no lado do app.
  2. Interfaces de callback consolidadas.
  3. Novo criador de PictureInPictureParams para compatibilidade com versões anteriores.
  4. Ícones de ação padronizados para videochamadas.

5. Migração de código

  • Lógica de entrada:substitua a lógica específica da API, como setAutoEnterEnabled para Android 12 e versões mais recentes ou onUserLeaveHint para Android 11 e versões anteriores com setEnabled. Acione isso sempre que o status de qualificação para o PiP mudar.
  • Callbacks:consolide onPictureInPictureModeChanged (alternância de layout) e onPictureInPictureUiStateChanged (animação/estados) em um callback unificado baseado em eventos onPictureInPictureEvent.
  • Ações e parâmetros:atualize os parâmetros usando setActions e setAspectRatio na instância do modelo sempre que eles mudarem.

Padrões de implementação de referência

Exemplos de implementações.

Navegação e videochamada

class NavOrVideoCallJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: BasicPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = BasicPictureInPicture(this)
        // BasicPictureInPicture is ideal for Navigation and Video call use cases.
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTERED -> { /* Toggle to PiP layout */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Toggle to Full-screen layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* Optional: PiP is stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* Optional: PiP is unstashed */ }
        }
    }
}

Reprodução de vídeo

class VideoPlaybackJpipActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener {
    private lateinit var pictureInPictureImpl: VideoPlaybackPictureInPicture
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        pictureInPictureImpl = VideoPlaybackPictureInPicture(this)
        pictureInPictureImpl.addOnPictureInPictureEventListener(
            ContextCompat.getMainExecutor(this),
            this
        )
        setContent {
            ContentScreen(pictureInPictureImpl)
        }
    }
    override fun onPictureInPictureEvent(
        event: PictureInPictureDelegate.Event,
        config: Configuration?
    ) {
        when (event) {
            PictureInPictureDelegate.Event.ENTER_ANIMATION_START -> { /* Hide overlays */ }
            PictureInPictureDelegate.Event.ENTER_ANIMATION_END -> { /* Animation finished */ }
            PictureInPictureDelegate.Event.ENTERED -> { /* Switch to PiP layout */ }
            PictureInPictureDelegate.Event.STASHED -> { /* PiP stashed */ }
            PictureInPictureDelegate.Event.UNSTASHED -> { /* PiP unstashed */ }
            PictureInPictureDelegate.Event.EXITED -> { /* Return to full-screen */ }
        }
    }

    @Composable
    fun ContentScreen(pipController: VideoPlaybackPictureInPicture) {
        DisposableEffect(pipController) {
            onDispose {
                pipController.close()
            }
        }
    }
}