Redacción mágica

La biblioteca de media3-ui-compose proporciona los componentes básicos para compilar una IU de medios en Jetpack Compose. Está diseñada para los 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 personalizada del reproductor multimedia.

Combinación de componentes personalizados de Material 3 y Compose

La biblioteca media3-ui-compose-material3 está diseñada para ser flexible. Puedes usar los componentes prediseñados para la mayor parte de tu IU, pero reemplazar un solo componente por una implementación personalizada cuando necesites más control. Es aquí cuando entra en juego la biblioteca de media3-ui-compose.

Por ejemplo, supongamos que quieres usar los elementos PreviousButton y NextButton estándar de la biblioteca de Material3, pero necesitas un PlayPauseButton completamente personalizado. Para ello, usa PlayPauseButton de la biblioteca principal media3-ui-compose y colócalo junto a 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 de 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 busca hacia atrás 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 alterna los modos de repetición.
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 reactiva el reproductor.
TimeText Es un contenedor de estado para un elemento componible que muestra el progreso del jugador.
ContentFrame Una superficie para mostrar contenido multimedia que controla la administración de la relación de aspecto, el cambio de tamaño y un obturador
PlayerSurface Superficie sin procesar que une SurfaceView y TextureView en AndroidView.

Contenedores de estado de la IU

Si ninguno de los componentes de andamiaje satisface tus necesidades, también puedes usar los objetos de estado directamente. En general, es recomendable usar los métodos remember correspondientes para conservar el aspecto de la IU entre las 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 del botón

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 2-Toggle
PreviousButtonState rememberPreviousButtonState Constante
NextButtonState rememberNextButtonState Constante
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menú o N-Toggle

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

PresentationState contiene información sobre cuándo se puede mostrar el video en un PlayerSurface o cuándo se debe cubrir con un elemento de IU de marcador de posición. El elemento ContentFrame componible combina el control de la relación de aspecto con el cuidado 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.videoSizeDp)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised 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 tanto presentationState.videoSizeDp para ajustar la escala de Surface a la relación de aspecto elegida (consulta la documentación de ContentScale para obtener más tipos) como presentationState.coverSurface para saber cuándo no es el momento adecuado para mostrar 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 rellene el tamaño del contenedor principal.

¿Dónde están los Flows?

Muchos desarrolladores de Android saben cómo usar objetos Flow de Kotlin para recopilar datos de la IU que cambian constantemente. Por ejemplo, podrías estar buscando un flujo de Player.isPlaying que puedas collect de una manera que tenga en cuenta el ciclo de vida. O algo como Player.eventsFlow para proporcionarte un Flow<Player.Events> que puedes filter como 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, y evitar mostrar elementos de la IU que no estén sincronizados con el Player.

Otros puntos incluyen los siguientes:

  • Un flujo con todos los Player.Events no cumpliría con un solo principio de responsabilidad, y cada consumidor tendría que filtrar los eventos relevantes.
  • Crear un flujo para cada Player.Event requerirá que los combines (con combine) para cada elemento de la IU. Existe una asignación de varios a varios entre un Player.Event y un cambio en un elemento de la IU. Tener que usar combine podría llevar a la IU a estados potencialmente ilegales.

Crea estados de IU personalizados

Puedes agregar estados de 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 IU típica hace lo siguiente:

  1. Toma un Player.
  2. Se suscribe a Player con corrutinas. Consulta Player.listen para obtener más detalles.
  3. Responde a un Player.Events en particular actualizando su estado interno.
  4. Acepta comandos de lógica empresarial que se transformarán en una actualización de Player adecuada.
  5. Se puede crear en varios lugares del árbol de la IU y siempre mantendrá una vista coherente del estado del reproductor.
  6. Expone los campos State de Compose que un elemento componible puede consumir para responder de forma dinámica a los cambios.
  7. Incluye una función remember*State para recordar la instancia entre composiciones.

Qué sucede en segundo plano:

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_ACTION_A))
    private set

  var someField by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe() =
    player.listen { events ->
      if (
        events.containsAny(
          Player.EVENT_B_CHANGED,
          Player.EVENT_C_CHANGED,
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
        )
      ) {
        someField = this.someField
        isEnabled = this.isCommandAvailable(Player.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.