La función pantalla en pantalla (PIP) es un tipo especial de modo multiventana que se usa principalmente para la reproducción de videos. Permite que el usuario vea un video en una ventana pequeña fijada en una esquina de la pantalla mientras navega entre apps o explora 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 la actividad, cambiar la actividad al modo de PIP según sea necesario y asegurarte de que los elementos de la IU estén ocultos y la reproducción de video continúe cuando la actividad esté en modo de PIP.
En esta guía, se describe cómo agregar PIP en Compose a tu app con una implementación de video de Compose. Consulta la app de Socialite para ver estas prácticas recomendadas en acción.
Cómo configurar tu app para PIP
En la etiqueta de actividad de tu archivo AndroidManifest.xml
, haz lo siguiente:
- Agrega
supportsPictureInPicture
y establécelo entrue
para declarar que usarás PIP en tu app. Agrega
configChanges
y establécelo enorientation|screenLayout|screenSize|smallestScreenSize
para especificar que tu actividad controla los cambios de configuración de diseño. De esta manera, tu actividad no se reiniciará cuando se produzcan 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:
- Agrega esta extensión a
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 la app de PIP cuando se sale de Android 12
Si quieres agregar PIP para versiones anteriores a Android 12, usa addOnUserLeaveHintProvider
. Sigue estos pasos para agregar PIP en versiones anteriores a Android 12:
- Agrega una puerta de versión para que solo se pueda acceder a este código en las versiones O hasta R.
- Usa un objeto
DisposableEffect
conContext
como clave. - Dentro de
DisposableEffect
, define el comportamiento para cuandoonUserLeaveHintProvider
se active con una expresión lambda. En la expresión lambda, llama aenterPictureInPictureMode()
enfindActivity()
y pasaPictureInPictureParams.Builder().build()
. - Agrega
addOnUserLeaveHintListener
confindActivity()
y pasa la lambda. - En
onDispose
, agregaremoveOnUserLeaveHintListener
confindActivity()
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_TAG, "API does not support PiP") }
Cómo agregar la app de PIP cuando se sale de Android 12
Después de Android 12, el PictureInPictureParams.Builder
se agrega a través de un modificador que se pasa al reproductor de video de la app.
- Crea una
modifier
y llama aonGloballyPositioned
en ella. Las coordenadas de diseño se usarán en un paso posterior. - Crea una variable para
PictureInPictureParams.Builder()
. - Agrega una sentencia
if
para verificar si el SDK es S o superior. Si es así, agregasetAutoEnterEnabled
al compilador y establécelo entrue
para ingresar al modo de PIP cuando se desliza el dedo. De esta manera, obtendrás una animación más fluida que si pasas porenterPictureInPictureMode
. - Usa
findActivity()
para llamar asetPictureInPictureParams()
. Llama abuild()
enbuilder
y pásalo.
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)
Cómo agregar PIP con un botón
Para ingresar al modo de PIP con un clic en un botón, llama a enterPictureInPictureMode()
en findActivity()
.
Los parámetros ya se establecen en llamadas anteriores a PictureInPictureParams.Builder
, por lo que no necesitas establecer parámetros nuevos en el compilador. Sin embargo, si deseas cambiar algún parámetro cuando se hace clic en el botón, puedes configurarlos 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 especifiques cómo debería verse dentro y fuera del modo de PIP.
Primero, debes saber cuándo la app está en modo de PIP o no. Puedes usar OnPictureInPictureModeChangedProvider
para lograrlo.
En el siguiente código, se 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 modo de 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 la app ingrese al modo de PIP en los momentos correctos
Tu app no debe ingresar al modo de PIP en las siguientes situaciones:
- Si el video se detiene o se pausa
- Si estás en una página de la app diferente a la 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 mediante un mutableStateOf
.
Activar o desactivar el estado según si se está reproduciendo el video
Para activar o desactivar el estado en función de si el reproductor de video se está reproduciendo, agrega un objeto de escucha en el reproductor. Activa o desactiva el estado de tu variable de estado en función de si el reproductor está reproduciendo o no:
player.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { shouldEnterPipMode = isPlaying } })
Activar o desactivar el estado si se suelta el reproductor
Cuando se libere el reproductor, establece tu variable de estado en false
:
fun releasePlayer() { shouldEnterPipMode = false }
Usa el estado para definir si se ingresa el modo de PIP (versiones anteriores a Android 12).
- Dado que agregar PIP pre-12 usa un
DisposableEffect
, debes crear una variable nueva medianterememberUpdatedState
connewValue
establecido como la variable de estado. Esto garantizará que la versión actualizada se use enDisposableEffect
. En la expresión lambda que define el comportamiento cuando se activa
OnUserLeaveHintListener
, agrega una declaraciónif
con la variable de estado alrededor de la llamada aenterPictureInPictureMode()
: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_TAG, "API does not support PiP") }
Usa el estado para definir si se ingresa el modo de PIP (versiones posteriores a Android 12)
Pasa la variable de estado a setAutoEnterEnabled
para que tu app solo ingrese al modo de PIP en el momento correcto:
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)
Cómo usar setSourceRectHint
para implementar una animación fluida
La API de setSourceRectHint
crea una animación más fluida para ingresar al modo de 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 a PIP.
- Solo agrega
setSourceRectHint()
abuilder
si el estado define que la app debe ingresar al modo de PIP. De esta manera, se evita calcularsourceRect
cuando la app no necesita ingresar el modo de PIP. - Para establecer el valor
sourceRect
, usa loslayoutCoordinates
que se proporcionan de la funciónonGloballyPositioned
en el modificador. - Llama a
setSourceRectHint()
en elbuilder
y pasa la variablesourceRect
.
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 el alto del tamaño del video del reproductor. Si usas un reproductor media3, verifica que este no sea nulo y que el tamaño de video del reproductor no sea igual a VideoSize.UNKNOWN
antes de configurar la relación de aspecto.
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 la altura y el ancho del reproductor con la sintaxis específica de este. Ten en cuenta que, si el reproductor cambia de tamaño durante la inicialización, si se encuentra fuera de los límites válidos de la relación de aspecto, la app fallará. Es posible que debas agregar verificaciones para determinar cuándo se puede calcular la relación de aspecto, de manera similar a como se hace para un reproductor media3.
Cómo agregar acciones remotas
Si quieres agregar controles (reproducir, pausar, etc.) a tu ventana de PIP, crea un RemoteAction
para cada control que desees agregar.
- 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
- Crea una lista de
RemoteActions
para los controles en tu ventana de PIP. - A continuación, agrega un
BroadcastReceiver
y anulaonReceive()
para configurar las acciones de cada botón. Usa unDisposableEffect
para registrar el receptor y las acciones remotas. Cuando se deseche el reproductor, 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) } } }
- Pasa una lista de tus acciones remotas a
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 en versiones anteriores a Android 12 y posteriores a 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.