Cómo agregar pantalla en pantalla (PIP) a tu app con un reproductor de video de Compose

El modo de pantalla en pantalla (PIP) es un tipo especial de modo multiventana que se usa principalmente para la reproducción de videos. Permite al usuario ver un video en una ventana pequeña fijada a una de la esquina de la pantalla mientras navegas entre apps o exploras contenido en la pantalla principal.

PIP aprovecha las API de multiventana disponibles en Android 7.0 para proporcionar la ventana de video fijada superpuesta. Para agregar PIP a tu app, debes registrar tu la actividad, cambia tu actividad al modo de PIP según sea necesario y asegúrate de que los elementos de la IU están ocultos y la reproducción de video continúa cuando la actividad está en modo de PIP.

En esta guía, se describe cómo agregar PIP en Compose a tu app con un video de Compose. para implementarlos. Visita la app de Socialite para ver estos videos prácticas de IA responsable en acción.

Cómo configurar tu app para PIP

En la etiqueta de actividad de tu archivo AndroidManifest.xml, haz lo siguiente:

  1. Agrega supportsPictureInPicture y configúralo como true para declarar que usarás PiP en tu app.
  2. Agrega configChanges y establécela en orientation|screenLayout|screenSize|smallestScreenSize para especificar que tu actividad maneja los cambios de configuración de diseño. De esta manera, tu actividad no se reinicia cuando se producen cambios de diseño durante las transiciones del modo de PIP.

      <activity
        android:name=".SnippetsActivity"
        android:exported="true"
        android:supportsPictureInPicture="true"
        android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
        android:theme="@style/Theme.Snippets">

En tu código de Compose, haz lo siguiente:

  1. Agrega esta extensión en Context. Usarás esta extensión varias veces a lo largo de la guía para acceder a la actividad.
    internal fun Context.findActivity(): ComponentActivity {
        var context = this
        while (context is ContextWrapper) {
            if (context is ComponentActivity) return context
            context = context.baseContext
        }
        throw IllegalStateException("Picture in picture should be called in the context of an Activity")
    }

Agrega el modo PIP en la app para salir en versiones anteriores a Android 12

Si quieres agregar PIP en versiones anteriores a Android 12, usa addOnUserLeaveHintProvider. Seguir estos pasos para agregar PIP en versiones anteriores a Android 12:

  1. Agrega una puerta de versión para que solo se acceda a este código en las versiones O hasta R.
  2. Usa un DisposableEffect con Context como clave.
  3. Dentro de DisposableEffect, define el comportamiento que tendrá el onUserLeaveHintProvider se activa con una lambda. En la expresión lambda, llama enterPictureInPictureMode() en findActivity() y pasar PictureInPictureParams.Builder().build()
  4. Agrega addOnUserLeaveHintListener con findActivity() y pasa la lambda.
  5. En onDispose, agrega removeOnUserLeaveHintListener con findActivity(). y pasa la lambda.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
    Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
    val context = LocalContext.current
    DisposableEffect(context) {
        val onUserLeaveBehavior: () -> Unit = {
            context.findActivity()
                .enterPictureInPictureMode(PictureInPictureParams.Builder().build())
        }
        context.findActivity().addOnUserLeaveHintListener(
            onUserLeaveBehavior
        )
        onDispose {
            context.findActivity().removeOnUserLeaveHintListener(
                onUserLeaveBehavior
            )
        }
    }
} else {
    Log.i("PiP info", "API does not support PiP")
}

Se agregó el modo PIP en la app para salir después de Android 12.

Después de Android 12, se agrega PictureInPictureParams.Builder a través de un modificador que se pasa al reproductor de video de la app.

  1. Crea un elemento modifier y llama a onGloballyPositioned en él. El diseño se usarán en un paso posterior.
  2. Crea una variable para PictureInPictureParams.Builder().
  3. Agrega una sentencia if para verificar si el SDK es S o superior. Si es así, agrega setAutoEnterEnabled al compilador y establecerlo en true para ingresar PIP al deslizar el dedo. Esto proporciona una animación más fluida que pasar por enterPictureInPictureMode
  4. Usa findActivity() para llamar a setPictureInPictureParams(). Llamar a build() en el builder y lo pasas.

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(true)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}
VideoPlayer(pipModifier)

Agrega PIP a través de un botón

Para ingresar al modo de PIP con un clic en un botón, llama enterPictureInPictureMode() en findActivity()

Las llamadas anteriores a la API ya establecieron los parámetros PictureInPictureParams.Builder, por lo que no necesitas establecer parámetros nuevos en el compilador. Sin embargo, si quieres cambiar algún parámetro del botón puedes establecerlos aquí.

val context = LocalContext.current
Button(onClick = {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        context.findActivity().enterPictureInPictureMode(
            PictureInPictureParams.Builder().build()
        )
    } else {
        Log.i(PIP_TAG, "API does not support PiP")
    }
}) {
    Text(text = "Enter PiP mode!")
}

Controla tu IU en modo de PIP

Cuando ingresas al modo de PIP, toda la IU de tu app ingresa a la ventana de PIP, a menos que Especifica cómo debería verse tu IU dentro y fuera del modo de PIP.

Primero, debes saber cuándo tu app está en modo de PIP o no. Puedes usar OnPictureInPictureModeChangedProvider para lograrlo. El siguiente código te indica si tu app está en modo de PIP.

@Composable
fun rememberIsInPipMode(): Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val activity = LocalContext.current.findActivity()
        var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) }
        DisposableEffect(activity) {
            val observer = Consumer<PictureInPictureModeChangedInfo> { info ->
                pipMode = info.isInPictureInPictureMode
            }
            activity.addOnPictureInPictureModeChangedListener(
                observer
            )
            onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) }
        }
        return pipMode
    } else {
        return false
    }
}

Ahora, puedes usar rememberIsInPipMode() para activar o desactivar los elementos de la IU que se mostrarán cuando la app entre en el modo PIP:

val inPipMode = rememberIsInPipMode()

Column(modifier = modifier) {
    // This text will only show up when the app is not in PiP mode
    if (!inPipMode) {
        Text(
            text = "Picture in Picture",
        )
    }
    VideoPlayer()
}

Asegúrate de que tu app ingrese al modo de PIP en el momento adecuado

Tu app no debe ingresar al modo PiP en las siguientes situaciones:

  • Si el video se detiene o se pausa.
  • Si estás en una página de la app diferente del reproductor de video.

Para controlar cuándo tu app ingresa al modo de PIP, agrega una variable que realice un seguimiento del estado. del reproductor de video con un mutableStateOf.

Activar o desactivar el estado si se está reproduciendo un video

Para activar o desactivar el estado en función de si se está reproduciendo el video, agrega un objeto de escucha en el reproductor de video. Activa o desactiva el estado de tu variable de estado según si el jugador está jugando o no:

player.addListener(object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        shouldEnterPipMode = isPlaying
    }
})

Activa o desactiva el estado según si se libera el reproductor

Cuando se lance el reproductor, establece la variable de estado en false:

fun releasePlayer() {
    shouldEnterPipMode = false
}

Usa el estado para definir si se ingresa al modo de PIP (versiones anteriores a Android 12)

  1. Como agregar PIP anteriores a la 12 se usa un DisposableEffect, debes crear una nueva variable de rememberUpdatedState con newValue configurado como tu de estado. Esto garantizará que la versión actualizada se use dentro del DisposableEffect
  2. En la expresión lambda que define el comportamiento cuando OnUserLeaveHintListener , agrega una sentencia if con la variable de estado alrededor de la llamada a enterPictureInPictureMode():

    val currentShouldEnterPipMode by rememberUpdatedState(newValue = shouldEnterPipMode)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
        Build.VERSION.SDK_INT < Build.VERSION_CODES.S
    ) {
        val context = LocalContext.current
        DisposableEffect(context) {
            val onUserLeaveBehavior: () -> Unit = {
                if (currentShouldEnterPipMode) {
                    context.findActivity()
                        .enterPictureInPictureMode(PictureInPictureParams.Builder().build())
                }
            }
            context.findActivity().addOnUserLeaveHintListener(
                onUserLeaveBehavior
            )
            onDispose {
                context.findActivity().removeOnUserLeaveHintListener(
                    onUserLeaveBehavior
                )
            }
        }
    } else {
        Log.i("PiP info", "API does not support PiP")
    }

Usa el estado para definir si se ingresa al modo de PIP (posterior a Android 12)

Pasa tu variable de estado a setAutoEnterEnabled para que tu app solo ingrese Modo de PIP en el momento adecuado:

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()

    // Add autoEnterEnabled for versions S and up
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

Usa setSourceRectHint para implementar una animación fluida

La API de setSourceRectHint crea una animación más fluida para ingresar a PIP. . En Android 12 y versiones posteriores, también crea una animación más fluida para salir del modo de PiP. Agrega esta API al compilador de PiP para indicar el área de la actividad que es visible después de la transición al modo PiP.

  1. Agrega setSourceRectHint() solo a builder si el estado define que la app debe ingresar al modo PiP. De esta manera, se evita calcular sourceRect cuando la app no necesita ingresar PIP.
  2. Para establecer el valor sourceRect, usa el layoutCoordinates que se proporciona desde la función onGloballyPositioned en el modificador.
  3. Llama a setSourceRectHint() en builder y pasa sourceRect. de salida.

val context = LocalContext.current

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()
    if (shouldEnterPipMode) {
        val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
        builder.setSourceRectHint(sourceRect)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

Usa setAspectRatio para establecer la relación de aspecto de la ventana de PIP.

Para establecer la relación de aspecto de la ventana de PiP, puedes elegir una relación de aspecto específica o usar el ancho y la altura del tamaño del video del reproductor. Si eres con un reproductor media3, comprueba que el reproductor no sea nulo y que su el tamaño del video no es igual a VideoSize.UNKNOWN antes de configurar el aspecto proporción.

val context = LocalContext.current

val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
    val builder = PictureInPictureParams.Builder()
    if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) {
        val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
        builder.setSourceRectHint(sourceRect)
        builder.setAspectRatio(
            Rational(player.videoSize.width, player.videoSize.height)
        )
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        builder.setAutoEnterEnabled(shouldEnterPipMode)
    }
    context.findActivity().setPictureInPictureParams(builder.build())
}

VideoPlayer(pipModifier)

Si usas un reproductor personalizado, establece la relación de aspecto en su altura. y ancho con la sintaxis específica del reproductor. Ten en cuenta que, si el reproductor cambia de tamaño durante la inicialización y se encuentra fuera de los límites válidos de la relación de aspecto, tu app fallará. Es posible que debas agregar verificaciones cuando se puede calcular la relación de aspecto, similar a como se hace para un de fútbol favorito.

Agrega acciones remotas

Si quieres agregar controles (reproducir, pausar, etc.) a tu ventana de PIP, crea una RemoteAction para cada control que desees agregar.

  1. Agrega constantes para los controles de transmisión:
    // Constant for broadcast receiver
    const val ACTION_BROADCAST_CONTROL = "broadcast_control"
    
    // Intent extras for broadcast controls from Picture-in-Picture mode.
    const val EXTRA_CONTROL_TYPE = "control_type"
    const val EXTRA_CONTROL_PLAY = 1
    const val EXTRA_CONTROL_PAUSE = 2
  2. Crea una lista de RemoteActions para los controles de tu ventana de PIP.
  3. A continuación, agrega un BroadcastReceiver y anula onReceive() para establecer las acciones de cada botón. Usa un DisposableEffect para registrar el receptor y las acciones remotas. Cuando el reproductor esté descartado, cancela el registro del receptor.
    @RequiresApi(Build.VERSION_CODES.O)
    @Composable
    fun PlayerBroadcastReceiver(player: Player?) {
        val isInPipMode = rememberIsInPipMode()
        if (!isInPipMode || player == null) {
            // Broadcast receiver is only used if app is in PiP mode and player is non null
            return
        }
        val context = LocalContext.current
    
        DisposableEffect(player) {
            val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context?, intent: Intent?) {
                    if ((intent == null) || (intent.action != ACTION_BROADCAST_CONTROL)) {
                        return
                    }
    
                    when (intent.getIntExtra(EXTRA_CONTROL_TYPE, 0)) {
                        EXTRA_CONTROL_PAUSE -> player.pause()
                        EXTRA_CONTROL_PLAY -> player.play()
                    }
                }
            }
            ContextCompat.registerReceiver(
                context,
                broadcastReceiver,
                IntentFilter(ACTION_BROADCAST_CONTROL),
                ContextCompat.RECEIVER_NOT_EXPORTED
            )
            onDispose {
                context.unregisterReceiver(broadcastReceiver)
            }
        }
    }
  4. Pasa una lista de tus acciones remotas al PictureInPictureParams.Builder:
    val context = LocalContext.current
    
    val pipModifier = modifier.onGloballyPositioned { layoutCoordinates ->
        val builder = PictureInPictureParams.Builder()
        builder.setActions(
            listOfRemoteActions()
        )
    
        if (shouldEnterPipMode && player != null && player.videoSize != VideoSize.UNKNOWN) {
            val sourceRect = layoutCoordinates.boundsInWindow().toAndroidRectF().toRect()
            builder.setSourceRectHint(sourceRect)
            builder.setAspectRatio(
                Rational(player.videoSize.width, player.videoSize.height)
            )
        }
    
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            builder.setAutoEnterEnabled(shouldEnterPipMode)
        }
        context.findActivity().setPictureInPictureParams(builder.build())
    }
    VideoPlayer(modifier = pipModifier)

Próximos pasos

En esta guía, aprendiste las prácticas recomendadas para agregar PiP en Compose, tanto antes como después de Android 12.

  • Consulta la app de Socialite para ver las prácticas recomendadas de PiP de Compose en acción.
  • Consulta la guía de diseño de PIP para obtener más información.