Core Compose

La libreria media3-ui-compose fornisce i componenti di base per la creazione di un'interfaccia utente multimediale in Jetpack Compose. È progettata per gli sviluppatori che hanno bisogno di una personalizzazione maggiore rispetto a quella offerta dalla libreria media3-ui-compose-material3. Questa pagina spiega come utilizzare i componenti principali e i contenitori di stato per creare un'interfaccia utente personalizzata per il lettore multimediale.

Combinazione di componenti Material3 e Compose personalizzati

La libreria media3-ui-compose-material3 è progettata per essere flessibile. Puoi utilizzare i componenti predefiniti per la maggior parte della tua UI, ma sostituire un singolo componente con un'implementazione personalizzata quando hai bisogno di un maggiore controllo. È qui che entra in gioco la libreria media3-ui-compose.

Ad esempio, supponiamo di voler utilizzare PreviousButton e NextButton standard della libreria Material3, ma hai bisogno di un PlayPauseButton completamente personalizzato. Puoi ottenere questo risultato utilizzando PlayPauseButton dalla libreria media3-ui-compose di base e posizionandolo accanto ai componenti predefiniti.

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)
}

Componenti disponibili

La libreria media3-ui-compose fornisce un insieme di componenti componibili predefiniti per i controlli comuni del player. Ecco alcuni componenti che puoi utilizzare direttamente nella tua app:

Componente Descrizione
PlayPauseButton Un contenitore di stato per un pulsante che alterna la riproduzione e la pausa.
SeekBackButton Un contenitore di stato per un pulsante che esegue la ricerca all'indietro di un incremento definito.
SeekForwardButton Un contenitore di stato per un pulsante che avanza di un incremento definito.
NextButton Un contenitore di stato per un pulsante che cerca l'elemento multimediale successivo.
PreviousButton Un contenitore di stato per un pulsante che cerca l'elemento multimediale precedente.
RepeatButton Un contenitore di stato per un pulsante che scorre le modalità di ripetizione.
ShuffleButton Un contenitore di stato per un pulsante che attiva/disattiva la modalità Shuffle.
MuteButton Un contenitore di stato per un pulsante che disattiva e riattiva l'audio del player.
TimeText Un contenitore di stato per un composable che mostra l'avanzamento del giocatore.
ContentFrame Una superficie per la visualizzazione di contenuti multimediali che gestisce le proporzioni, il ridimensionamento e un otturatore
PlayerSurface Superficie grezza che racchiude SurfaceView e TextureView in AndroidView.

Contenitori di stato UI

Se nessuno dei componenti di scaffolding soddisfa le tue esigenze, puoi anche utilizzare direttamente gli oggetti di stato. In genere è consigliabile utilizzare i metodi remember corrispondenti per preservare l'aspetto dell'interfaccia utente tra le ricomposizioni.

Per capire meglio come utilizzare la flessibilità dei titolari dello stato dell'interfaccia utente rispetto ai composable, leggi come Compose gestisce lo stato.

Contenitori di stato dei pulsanti

Per alcuni stati dell'interfaccia utente, la libreria presuppone che verranno utilizzati molto probabilmente da composable simili a pulsanti.

Stato remember*State Tipo
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Valore costante
NextButtonState rememberNextButtonState Valore costante
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menu o N-Toggle

Esempio di utilizzo di 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),
  )
}

Contenitori di stato dell'output visivo

PresentationState contiene informazioni su quando l'output video in un PlayerSurface può essere mostrato o deve essere coperto da un elemento dell'interfaccia utente segnaposto. ContentFrame Composable combina la gestione delle proporzioni con la cura di mostrare l'otturatore su una superficie non ancora pronta.

@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()
  }
}

Qui possiamo utilizzare sia presentationState.videoSizeDp per scalare la superficie in base alle proporzioni scelte (per altri tipi, consulta la documentazione di ContentScale) sia presentationState.coverSurface per sapere quando non è il momento giusto per mostrare la superficie. In questo caso, puoi posizionare un otturatore opaco sopra la superficie, che scomparirà quando la superficie sarà pronta. ContentFrame ti consente di personalizzare l'otturatore come lambda finale, ma per impostazione predefinita sarà nero @Composable Box e riempirà le dimensioni del contenitore principale.

Dove si trovano i flussi?

Molti sviluppatori Android hanno familiarità con l'utilizzo degli oggetti Kotlin Flow per raccogliere dati dell'interfaccia utente in continua evoluzione. Ad esempio, potresti essere alla ricerca di un flusso Player.isPlaying che puoi collect in modo consapevole del ciclo di vita. Oppure qualcosa come Player.eventsFlow per fornirti un Flow<Player.Events> che puoi filter come preferisci.

Tuttavia, l'utilizzo dei flussi per lo stato dell'interfaccia utente Player presenta alcuni svantaggi. Uno dei principali problemi è la natura asincrona del trasferimento dei dati. Vogliamo ottenere la latenza più bassa possibile tra un Player.Event e il suo utilizzo lato UI, evitando di mostrare elementi della UI non sincronizzati con Player.

Altri punti includono:

  • Un flusso con tutti i Player.Events non rispetterebbe un singolo principio di responsabilità, ogni consumatore dovrebbe filtrare gli eventi pertinenti.
  • La creazione di un flusso per ogni Player.Event richiede di combinarli (con combine) per ogni elemento UI. Esiste una mappatura many-to-many tra un Player.Event e una modifica dell'elemento UI. L'utilizzo di combine potrebbe portare la UI a stati potenzialmente illegali.

Creare stati dell'interfaccia utente personalizzati

Puoi aggiungere stati dell'interfaccia utente personalizzati se quelli esistenti non soddisfano le tue esigenze. Controlla il codice sorgente dello stato esistente per copiare il pattern. Una tipica classe UI State Holder esegue le seguenti operazioni:

  1. Include un Player.
  2. Consente di iscriversi a Player utilizzando le coroutine. Per maggiori dettagli, consulta Player.listen.
  3. Risponde a particolari Player.Events aggiornando il suo stato interno.
  4. Accetta comandi di logica di business che verranno trasformati in un aggiornamento Player appropriato.
  5. Possono essere creati in più posizioni nell'albero della UI e manterranno sempre una visualizzazione coerente dello stato del giocatore.
  6. Espone i campi State di Compose che possono essere utilizzati da un elemento componibile per rispondere in modo dinamico alle modifiche.
  7. È dotato di una funzione remember*State per ricordare l'istanza tra le composizioni.

Cosa succede dietro le quinte:

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)
      }
    }
}

Per reagire ai tuoi Player.Events, puoi rilevarli utilizzando Player.listen, che è una suspend fun che ti consente di entrare nel mondo delle coroutine e ascoltare indefinitamente Player.Events. L'implementazione di vari stati dell'interfaccia utente in Media3 aiuta lo sviluppatore finale a non preoccuparsi di imparare a utilizzare Player.Events.