Usar a biblioteca Picture-in-Picture do Jetpack

A biblioteca Jetpack Picture-in-Picture (PiP) oferece uma solução simplificada e robusta para que desenvolvedores de apps Android implementem a funcionalidade PiP, especialmente 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 do Jetpack facilita o uso das APIs PiP atuais, abordando vários desafios e inconsistências importantes no ecossistema Android:

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

Workflow de migração

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

Categorias:reprodução de vídeo, navegação ou videochamada.

Lógica de PiP legada para identificar:

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams.

2. Configuração do AndroidManifest

Verifique se a atividade que entra no PiP 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 mais recentes do AndroidX para as dependências e consulte a página de versões para ver essas 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 de 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 do Jetpack, substitua sua implementação personalizada de PiP pelas APIs da biblioteca do 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 do Jetpack cuida do resto.

Principais diferenças:

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

Videochamada

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

Principais diferenças:

  1. Não é necessário diferenciar a entrada automática e a entrada legada no app.
  2. Interfaces de callback consolidadas.
  3. Novo builder 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 o Android 12 e versões mais recentes ou onUserLeaveHint para o Android 11 e versões anteriores, por setEnabled. Acione isso sempre que o status de qualificação do 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()
            }
        }
    }
}