Cómo usar la biblioteca de pantalla en pantalla de Jetpack

La biblioteca de Jetpack de pantalla en pantalla (PIP) ofrece una solución optimizada y sólida para que los desarrolladores de apps para Android implementen la funcionalidad de PIP, en especial para las apps de reproducción de contenido multimedia, comunicación por video y navegación. Como proporciona una API unificada, la biblioteca ayuda a eliminar el código estándar y los errores comunes en la app, y mejora la calidad general de la experiencia del usuario de pantalla en pantalla.

La biblioteca de Jetpack de PiP facilita las APIs de PiP existentes, ya que aborda varios desafíos e inconsistencias clave en el ecosistema de Android:

  • Fragmentación del SO: La biblioteca controla automáticamente las diferencias en las llamadas a la API de PiP en varias versiones de Android, como el uso de enterPictureInPictureMode antes de Android 12 y isAutoEnterEnabled después, por lo que los desarrolladores no necesitan administrar las diferencias de versión.
  • Parámetros de PiP incorrectos: Proporciona una solución unificada para configurar correctamente los parámetros de PiP, por ejemplo, setSourceRectHint, para crear animaciones fluidas y de alta calidad durante la reproducción de contenido multimedia.
  • Devoluciones de llamada de estado de PiP unificadas: Consolida onPictureInPictureModeChanged y onPictureInPictureUiStateChanged en una sola interfaz de devolución de llamada unificada (PictureInPictureDelegate.OnPictureInPictureEventListener) para simplificar la administración del estado y de la IU.
  • Reducción del código estándar: La biblioteca reduce la cantidad de código estándar repetitivo, ya que ofrece conjuntos predefinidos de RemoteActions para casos de uso comunes, como controles de reproducción y acciones de videollamadas.
  • Preparación para el futuro: Se ofrecen más funciones de PIP a través de la biblioteca de Jetpack, lo que permite que los usuarios accedan a funciones adicionales con un esfuerzo mínimo o nulo.

Flujo de trabajo de migración

Identifica la categoría de caso de uso de la app y la lógica de PiP heredada:

Categorías: Reproducción de video, navegación o videollamada

Lógica de PiP heredada para identificar:

  • onUserLeaveHint
  • setAutoEnterEnabled
  • onPictureInPictureModeChanged
  • onPictureInPictureUiStateChanged
  • setPictureInPictureParams.

2. Configuración de AndroidManifest

Asegúrate de que la actividad que ingresa a PiP declare compatibilidad en AndroidManifest.xml con el configChanges necesario para evitar reinicios innecesarios:

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

3. Configuración del entorno

Agrega las dependencias necesarias a 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") }

Usa las bibliotecas de AndroidX más recientes para las dependencias y consulta la página de versiones para obtener esa información.

4. Selección e inicialización de plantillas

Elige la plantilla de implementación que mejor se adapte al caso de uso de la app:

  • Navegación y videollamada: BasicPictureInPicture; por lo general, no se admite el cambio de tamaño sin interrupciones y no necesitas una sugerencia de rectángulo de origen.
  • Reproducción de video: VideoPlaybackPictureInPicture; hace un seguimiento automático de los límites de la vista del reproductor para la sugerencia de rectángulo de origen y habilita el cambio de tamaño sin interrupciones de forma predeterminada.

Para adoptar la biblioteca de Jetpack, reemplaza tu implementación personalizada de PIP existente por las APIs de la biblioteca de Jetpack. La complejidad y el costo de la adopción variarán según la implementación actual de la app.

En las siguientes secciones, se describen algunos de los casos de uso típicos de la pantalla en pantalla y los pasos de implementación necesarios:

La app informa a la biblioteca sobre el estado activo o inactivo de la navegación y establece la relación de aspecto. La biblioteca de Jetpack se encarga del resto.

Diferencias clave:

  1. No es necesario diferenciar entre la entrada automática y la heredada en el lado de la app.
  2. Se consolidaron las interfaces de devolución de llamada.
  3. Se agregó un nuevo compilador PictureInPictureParams para la retrocompatibilidad.

Videollamada

La app informa a la biblioteca sobre el estado activo o inactivo de la llamada y establece la relación de aspecto.

Diferencias clave:

  1. No es necesario diferenciar entre la entrada automática y la heredada en el lado de la app.
  2. Se consolidaron las interfaces de devolución de llamada.
  3. Se agregó un nuevo compilador PictureInPictureParams para la retrocompatibilidad.
  4. Íconos de acción estandarizados para videollamadas.

5. Migración de código

  • Lógica de entrada: Reemplaza la lógica específica de la API, como setAutoEnterEnabled para Android 12 y versiones posteriores, o onUserLeaveHint para Android 11 y versiones anteriores, por setEnabled. Activa este método cada vez que cambie el estado de elegibilidad de la pantalla en pantalla.
  • Devoluciones de llamada: Consolida onPictureInPictureModeChanged (activación o desactivación del diseño) y onPictureInPictureUiStateChanged (animación/estados) en una devolución de llamada unificada basada en eventos onPictureInPictureEvent.
  • Acciones y parámetros: Actualiza los parámetros con setActions y setAspectRatio en la instancia de plantilla cada vez que cambien.

Patrones de implementación de referencia

Ejemplos de implementaciones

Navegación y videollamada

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 */ }
        }
    }
}

Reproducción de los videos

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()
            }
        }
    }
}