La biblioteca media3-ui-compose proporciona los componentes básicos para compilar una IU de contenido multimedia en Jetpack Compose. Está diseñada para desarrolladores que necesitan
más personalización que la que ofrece la biblioteca
de media3-ui-compose-material3. En esta página, se explica cómo usar los componentes principales y los contenedores de estado para crear una IU de reproductor multimedia personalizada.
Cómo mezclar componentes de Material 3 y personalizados de Compose
La biblioteca de media3-ui-compose-material3 está diseñada para ser flexible. Puedes
usar los componentes compilados previamente para la mayor parte de tu IU, pero intercambiar un solo componente
por una implementación personalizada cuando necesites más control. Aquí es cuando entra en juego la biblioteca de media3-ui-compose.
Por ejemplo, imagina que quieres usar los elementos estándar PreviousButton y NextButton de la biblioteca de Material 3, pero necesitas un PlayPauseButton completamente personalizado. Para ello, puedes usar PlayPauseButton de la biblioteca principal de media3-ui-compose y colocarlo junto con los componentes compilados previamente.
Row { // Use prebuilt component from the Media3 UI Compose Material3 library PreviousButton(player) // Use the scaffold component from Media3 UI Compose library PlayPauseButton(player) { // `this` is PlayPauseButtonState FilledTonalButton( onClick = { Log.d("PlayPauseButton", "Clicking on play-pause button") this.onClick() }, enabled = this.isEnabled, ) { Icon( imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (showPlay) "Play" else "Pause", ) } } // Use prebuilt component from the Media3 UI Compose Material3 library NextButton(player) }
Componentes disponibles
La biblioteca media3-ui-compose proporciona un conjunto de elementos componibles compilados previamente para los controles del reproductor comunes. Estos son algunos de los componentes que puedes usar directamente en tu app:
| Componente | Descripción |
|---|---|
PlayPauseButton |
Es un contenedor de estado para un botón que alterna entre reproducir y pausar. |
SeekBackButton |
Es un contenedor de estado para un botón que retrocede en un incremento definido. |
SeekForwardButton |
Es un contenedor de estado para un botón que avanza en un incremento definido. |
NextButton |
Es un contenedor de estado para un botón que busca el siguiente elemento multimedia. |
PreviousButton |
Es un contenedor de estado para un botón que busca el elemento multimedia anterior. |
RepeatButton |
Es un contenedor de estado para un botón que cambia de un modo de repetición a otro. |
ShuffleButton |
Es un contenedor de estado para un botón que activa o desactiva el modo aleatorio. |
MuteButton |
Es un contenedor de estado para un botón que silencia y activa el sonido del reproductor. |
TimeText |
Es un contenedor de estado para un elemento componible que muestra el progreso del reproductor. |
ContentFrame |
Es una plataforma para contenido multimedia que controla la administración de la relación de aspecto, el cambio de tamaño, un obturador, el material gráfico y un espacio para subtítulos. |
PlayerSurface |
Plataforma sin procesar que une SurfaceView y TextureView en AndroidView |
Artwork |
Es un elemento componible que muestra la portada del elemento multimedia actual, como la portada de un álbum. |
Personaliza el elemento Player componible
Cuando usas la biblioteca media3-ui-compose-material3, el elemento componible Player proporciona una experiencia de reproducción de contenido multimedia completa. Puedes personalizar su diseño proporcionando tus propios elementos componibles a sus ranuras de contenido, o bien puedes aprovechar el objeto PlayerDefaults para personalizar partes específicas de la IU, como TopControls, CenterControls, BottomControls y ErrorOverlay.
En la Figura 1, se muestra dónde el elemento Player componible coloca sus ranuras. En la figura 2, se muestra el orden en el que se dibujan las capas.
Player que admite composición
Los controles central e inferior muestran los espacios de PlayerDefaults.CenterControls y PlayerDefaults.BottomControls. Las ranuras topControls, above y below están vacías de forma predeterminada.Player, numeradas en el orden en que se dibujan.Por ejemplo, puedes usar PlayerDefaults.CenterControls para anular solo el botón central de reproducción/pausa y dejar intacto el resto de los controles centrales.
También puedes pasar un elemento componible completamente personalizado para un parámetro como topControls y omitir por completo otros, como bottomControls, para dejarlos en su valor predeterminado.
@Composable fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, showControls = showControls, topControls = { // Fully custom top controls Text("My custom title") }, centerControls = { // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = player, central = { // A custom play/pause button Material3PlayPauseButton(player, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
El elemento de componibilidad Player también muestra subtítulos y, cuando no hay video para mostrar, la carátula del elemento multimedia actual. Para personalizar su apariencia, pasa tus propios elementos componibles a las ranuras subtitles y artwork. Para ocultarlos, pasa un elemento componible vacío a subtitles y null a artwork.
Contenedores de estado de la IU
Si ninguno de los componentes de scaffolding satisface tus necesidades, también puedes usar los objetos de estado directamente. Por lo general, se recomienda usar los métodos remember correspondientes para preservar el aspecto de la IU entre recomposiciones.
Para comprender mejor cómo puedes usar la flexibilidad de los contenedores de estado de la IU en comparación con los elementos componibles, lee sobre cómo Compose administra el estado.
Contenedores de estado de botones
Para algunos estados de la IU, la biblioteca supone que es más probable que los consuman elementos componibles similares a botones.
| Estado | remember*State | Tipo |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
Alternancia de 2 o N |
PreviousButtonState |
rememberPreviousButtonState |
Constante |
NextButtonState |
rememberNextButtonState |
Constante |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Menú o N-Toggle |
Además de los estados de reproducción y pausa, PlayPauseButtonState expone showBuffering, showReplay y showRetry. Úsalos para mostrar un indicador de almacenamiento en búfer mientras el reproductor almacena en búfer, un ícono de reproducción después de que finaliza la reproducción y un ícono de reintento después de un error de reproducción. showReplay y showRetry definen mejor
showPlay: cada vez que uno de ellos es true, showPlay también es true, por lo que debes verificarlos
antes de showPlay. El método onClick también controla estos estados, por ejemplo, preparando el reproductor de nuevo después de un error.
Para elegir cuál de estos estados mostrar, pasa una combinación de SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED y SHOW_RETRY_ON_ERROR al parámetro displayMode de rememberPlayPauseButtonState. De forma predeterminada, displayMode combina SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED y SHOW_RETRY_ON_ERROR, lo que muestra los tres estados, y el estado de almacenamiento en búfer incluso aparece mientras la reproducción está en pausa.
Ejemplo de uso de PlayPauseButtonState:
val state = rememberPlayPauseButtonState(player) IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) { Icon( imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (state.showPlay) stringResource(R.string.playpause_button_play) else stringResource(R.string.playpause_button_pause), ) }
Contenedores de estado de salida visual
CurrentMediaItemState proporciona información sobre el MediaItem que se está reproduciendo, mientras que PlaylistState expone información sobre el MediaItems establecido en el reproductor. Son útiles para mostrar información de metadatos en tu IU personalizada.
ErrorState proporciona información sobre el estado de error actual del reproductor, que se puede usar para mostrar una superposición de error.
PlaybackState expone el estado de reproducción principal del reproductor, como playbackState, isPlaying y playerError, independientemente de cualquier componente específico de la IU. Úsala para controlar las superposiciones o los diseños personalizados, por ejemplo, para mostrar un indicador de carga mientras el reproductor almacena en búfer. Para un indicador de Material 3 compilado previamente, usa BufferingIndicator.
PresentationState contiene información sobre cuándo se puede mostrar la salida de video en un PlayerSurface o cuándo debe cubrirse con un elemento de la IU de marcador de posición.
El elemento de componibilidad ContentFrame combina el control de la relación de aspecto con la tarea de mostrar el obturador sobre una superficie que aún no está lista.
@Composable fun ContentFrame( player: Player?, modifier: Modifier = Modifier, surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW, contentScale: ContentScale = ContentScale.Fit, keepContentOnReset: Boolean = false, shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) }, ) { val presentationState = rememberPresentationState(player, keepContentOnReset) val scaledModifier = modifier.resizeWithContentScale(contentScale, presentationState.videoAspectRatio) // Always leave PlayerSurface to be part of the Compose tree because it will be initialized in // the process. If this composable is guarded by some condition, it might never become visible // because the Player won't emit the relevant event, e.g. the first frame being ready. PlayerSurface(player, scaledModifier, surfaceType) if (presentationState.coverSurface) { // Cover the surface that is being prepared with a shutter shutter() } }
Aquí, podemos usar presentationState.videoAspectRatio para escalar la Surface a la relación de aspecto elegida (consulta Escala de contenido para ver más tipos) y presentationState.coverSurface para saber cuándo no es el momento adecuado para mostrar la Surface. En este caso, puedes colocar un obturador opaco sobre la superficie, que desaparecerá cuando la superficie esté lista.
ContentFrame te permite personalizar el obturador como una expresión lambda final, pero, de forma predeterminada, será un @Composable Box negro que llenará el tamaño del contenedor superior.
PresentationState también expone showArtwork, que es true cuando el elemento multimedia actual se reproduce sin una pista de video, como el contenido solo de audio. En ese caso, ContentFrame muestra el elemento de componibilidad Artwork, que muestra la imagen de portada del MediaMetadata del elemento. Para personalizar o esconder la carátula, pasa tu propio elemento componible o null a la ranura artwork de ContentFrame.
De forma predeterminada, Artwork solo carga los datos de la carátula incorporados en los metadatos o desde un URI content, file o android.resource. Para cargar material gráfico desde URIs remotas o de otras fuentes, usa una de las siguientes opciones:
- Pasa un
BitmapLoader, comoDataSourceBitmapLoader, aArtwork. - Usa la sobrecarga de
Artworkque toma unpainterpara cargar imágenes con la biblioteca que elijas.
A diferencia del elemento componible Player, que renderiza subtítulos de forma predeterminada, ContentFrame deja vacía su ranura subtitles. Para renderizar subtítulos en un ContentFrame o personalizar la renderización de subtítulos en Player, observa las indicaciones actuales del reproductor con CuesState y renderízalas en la ranura subtitles. ContentFrame dispone este espacio dentro de los límites del video ajustado y lo dibuja sobre el obturador y la carátula, de modo que los subtítulos permanezcan visibles cuando no haya video.
Contenedores de estado de selección de pistas
TrackSelectionState te ayuda a crear un menú para un solo tipo de pista, como un menú de idioma de audio o de subtítulos. Créalo con
rememberTrackSelectionState, pasando el tipo de pista y una combinación de
filtros, como TRACK_FILTER_LANGUAGE o TRACK_FILTER_RESOLUTION, que
determinan qué propiedades de la pista distinguen las opciones. Establece hasOffOption o hasAutoOption en true para agregar una opción que inhabilite el tipo de pista o una que permita que el reproductor elija pistas automáticamente.
El estado expone el selectionOptions disponible y el selectedOption.
Cada TrackSelectionOption indica si el dispositivo lo admite y si está seleccionado. Pasar una opción a selectOption actualiza los parámetros de selección de pistas del reproductor según corresponda. Antes de mostrar el menú, verifica canSetTrackSelectionParameters y si hay suficientes opciones para elegir. Por ejemplo, el siguiente menú de subtítulos ofrece una opción para cada idioma de subtítulos y una opción para desactivarlos:
@Composable fun SubtitleMenu(player: Player, modifier: Modifier = Modifier) { val state = rememberTrackSelectionState( player, trackType = C.TRACK_TYPE_TEXT, trackFilters = TRACK_FILTER_LANGUAGE, hasOffOption = true, ) // Hide the menu if the player can't change tracks or has no subtitles to choose from if (!state.canSetTrackSelectionParameters || state.selectionOptions.size < 2) return Column(modifier.selectableGroup()) { for (option in state.selectionOptions) { Row( Modifier.fillMaxWidth() .heightIn(min = 48.dp) .selectable( selected = option.isSelected, enabled = option.isSupported, role = Role.RadioButton, onClick = { state.selectOption(option) }, ) .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton(selected = option.isSelected, onClick = null, enabled = option.isSupported) Text( text = if (option.onOffState == TRACK_SELECTION_OFF) "Off" else option.language?.let { Locale.forLanguageTag(it).displayName } ?: "Unknown", color = if (option.isSupported) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.38f), ) } } } }
Si necesitas acceso directo a los objetos Tracks y TrackSelectionParameters del reproductor, usa TrackSelectionParametersState en su lugar. Para
obtener más información sobre cómo el reproductor selecciona las pistas, consulta Selección de pistas.
¿Dónde están los flujos?
Muchos desarrolladores de Android están familiarizados con el uso de objetos Flow de Kotlin para recopilar datos de la IU que cambian constantemente. Por ejemplo, es posible que estés buscando un flujo de Player.isPlaying que puedas collect de manera optimizada para ciclos de vida. O algo como Player.eventsFlow para proporcionarte un Flow<Player.Events> que puedas filter de la manera que quieras.
Sin embargo, usar flujos para el estado de la IU de Player tiene algunas desventajas. Una de las principales
preocupaciones es la naturaleza asíncrona de la transferencia de datos. Queremos lograr la menor latencia posible entre un Player.Event y su consumo en el lado de la IU, evitando mostrar elementos de la IU que no estén sincronizados con el Player.
Otros puntos incluyen los siguientes:
- Un flujo con todos los
Player.Eventsno se adheriría a un principio de responsabilidad única, y cada consumidor tendría que filtrar los eventos relevantes. - Crear un flujo para cada
Player.Eventrequerirá que los combines (concombine) para cada elemento de la IU. Existe una asignación de varios a varios entre un Player.Event y un cambio de elemento de la IU. Tener que usarcombinepodría llevar a la IU a estados potencialmente no permitidos.
Crea estados de IU personalizados
Puedes agregar estados de la IU personalizados si los existentes no satisfacen tus necesidades. Consulta el código fuente del estado existente para copiar el patrón. Una clase de contenedor de estado de la IU típica hace lo siguiente:
- Recibe un objeto
Player. - Se suscribe a
Playercon corrutinas. ConsultaPlayer.listenpara obtener más información. - Responde a un
Player.Eventsen particular actualizando su estado interno. - Acepta comandos de lógica empresarial que se transformarán en una actualización
de
Playeradecuada. - Se puede crear en varios lugares del árbol de la IU y siempre mantendrá una vista coherente del estado de Player.
- Expone los campos
Statede Compose que puede consumir una función de componibilidad para responder de forma dinámica a los cambios. - Incluye una función
remember*Statepara recordar la instancia entre composiciones.
Qué sucede tras bambalinas:
class SomeButtonState(private val player: Player) { var isEnabled by mutableStateOf(player.isCommandAvailable(COMMAND_ACTION_A)) private set var someFieldValue by mutableStateOf(someFieldDefault) private set fun onClick() { player.actionA() } suspend fun observe(): Nothing = player.listen { events -> if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { someFieldValue = this.someField isEnabled = this.isCommandAvailable(COMMAND_ACTION_A) } } }
Para reaccionar a tus propios Player.Events, puedes detectarlos con Player.listen, que es un suspend fun que te permite ingresar al mundo de las corrutinas y escuchar Player.Events de forma indefinida. La implementación de Media3 de varios estados de la IU ayuda al desarrollador final a no preocuparse por aprender sobre Player.Events.