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:
- Agrega
supportsPictureInPicture
y configúralo comotrue
para declarar que usarás PiP en tu app. Agrega
configChanges
y establécela enorientation|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:
- 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:
- Agrega una puerta de versión para que solo se acceda a este código en las versiones O hasta R.
- Usa un
DisposableEffect
conContext
como clave. - Dentro de
DisposableEffect
, define el comportamiento que tendrá elonUserLeaveHintProvider
se activa con una lambda. En la expresión lambda, llamaenterPictureInPictureMode()
enfindActivity()
y pasarPictureInPictureParams.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 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.
- Crea un elemento
modifier
y llama aonGloballyPositioned
en él. El 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 establecerlo entrue
para ingresar PIP al deslizar el dedo. Esto proporciona una animación más fluida que pasar porenterPictureInPictureMode
- Usa
findActivity()
para llamar asetPictureInPictureParams()
. Llamar abuild()
en elbuilder
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)
- Como agregar PIP anteriores a la 12 se usa un
DisposableEffect
, debes crear una nueva variable derememberUpdatedState
connewValue
configurado como tu de estado. Esto garantizará que la versión actualizada se use dentro delDisposableEffect
En la expresión lambda que define el comportamiento cuando
OnUserLeaveHintListener
, agrega una sentenciaif
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 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.
- Agrega
setSourceRectHint()
solo abuilder
si el estado define que la app debe ingresar al modo PiP. De esta manera, se evita calcularsourceRect
cuando la app no necesita ingresar PIP. - Para establecer el valor
sourceRect
, usa ellayoutCoordinates
que se proporciona desde la funciónonGloballyPositioned
en el modificador. - Llama a
setSourceRectHint()
enbuilder
y pasasourceRect
. 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.
- 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 de tu ventana de PIP. - A continuación, agrega un
BroadcastReceiver
y anulaonReceive()
para establecer las acciones de cada botón. Usa unDisposableEffect
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) } } }
- 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.