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 del player multimediale personalizzata.

Combinare componenti Material3 e Compose personalizzati

La libreria media3-ui-compose-material3 è progettata per essere flessibile. Puoi utilizzare i componenti predefiniti per la maggior parte dell'interfaccia utente, 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 che tu voglia utilizzare i pulsanti standard PreviousButton e NextButton della libreria Material3, ma hai bisogno di un pulsante PlayPauseButton completamente personalizzato. Puoi farlo utilizzando PlayPauseButton dalla libreria media3-ui-compose principale 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 composable predefiniti per i controlli del player comuni. Ecco alcuni dei 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 esegue la ricerca in avanti di un incremento definito.
NextButton Un contenitore di stato per un pulsante che esegue la ricerca dell'elemento multimediale successivo.
PreviousButton Un contenitore di stato per un pulsante che esegue la ricerca dell'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à di riproduzione casuale.
MuteButton Un contenitore di stato per un pulsante che attiva/disattiva l'audio del player.
TimeText Un contenitore di stato per un composable che mostra l'avanzamento del player.
ContentFrame Una superficie per la visualizzazione di contenuti multimediali che gestisce la gestione delle proporzioni, il ridimensionamento e un otturatore
PlayerSurface Superficie non elaborata che racchiude SurfaceView e TextureView in AndroidView.

Personalizzare il composable Player

Quando utilizzi la librario media3-ui-compose-material3, il Player composable offre un'esperienza di riproduzione multimediale completa. Puoi personalizzare il layout fornendo i tuoi composable ai suoi slot di contenuti oppure puoi utilizzare l'oggetto PlayerDefaults per personalizzare parti specifiche dell'interfaccia utente, come TopControls, CenterControls, BottomControls e ErrorOverlay.

Ad esempio, puoi utilizzare PlayerDefaults.CenterControls per sostituire solo il pulsante centrale di riproduzione/pausa, lasciando intatti gli altri controlli centrali. Puoi anche passare un composable completamente personalizzato per un parametro come topControls e omettere completamente altri parametri come bottomControls per lasciarli ai valori predefiniti.

@Composable
fun CustomPlayerSlots(player: Player, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    topControls = { p, visible ->
      // Fully custom top controls
      AnimatedVisibility(visible) { Text("My custom title") }
    },
    centerControls = { p, visible ->
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = p,
        visible = visible,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(it, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

Contenitori di stato dell'interfaccia utente

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 mantenere l'aspetto dell'interfaccia utente tra le ricomposizioni.

Per comprendere meglio come utilizzare la flessibilità dei contenitori di stato dell'interfaccia utente rispetto ai composable, leggi la documentazione su 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 Costante
NextButtonState rememberNextButtonState 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

CurrentMediaItemState fornisce informazioni su MediaItem in riproduzione, mentre PlaylistState espone informazioni su MediaItems impostato sul player. Questi sono utili per visualizzare le informazioni sui metadati nell'interfaccia utente personalizzata.

ErrorState fornisce informazioni sullo stato di errore corrente del player, che può essere utilizzato per visualizzare una sovrapposizione di errore.

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 visualizzazione dell'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.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()
  }
}

Qui possiamo utilizzare sia presentationState.videoAspectRatio per scalare la superficie alle proporzioni scelte (vedi Scala dei contenuti per altri tipi) 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à un @Composable Box nero che riempie le dimensioni del contenitore principale.

Dove sono i flussi?

Molti sviluppatori Android hanno familiarità con l'utilizzo degli oggetti Flow Kotlin per raccogliere i dati dell'interfaccia utente in continua evoluzione. Ad esempio, potresti cercare il flusso Player.isPlaying che puoi collect in modo da tenere conto del ciclo di vita. Oppure qualcosa di simile a Player.eventsFlow per fornirti un Flow<Player.Events> che puoi filter nel modo che 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 sul lato dell'interfaccia utente, evitando di mostrare elementi dell'interfaccia utente non sincronizzati con il Player.

Altri punti includono:

  • Un flusso con tutti i Player.Events non rispetterebbe un principio di responsabilità singola, ogni consumer dovrebbe filtrare gli eventi pertinenti.
  • La creazione di un flusso per ogni Player.Event richiederà di combinarli (con combine) per ogni elemento dell'interfaccia utente. Esiste una mappatura many-to-many tra un Player.Event e una modifica dell'elemento dell'interfaccia utente. L'utilizzo di combine potrebbe portare l'interfaccia utente 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 classe di contenitore di stato dell'interfaccia utente tipica esegue le seguenti operazioni:

  1. Accetta un Player oggetto.
  2. Si iscrive a Player utilizzando le coroutine. Per maggiori dettagli, consulta Player.listen per più dettagli.
  3. Risponde a Player.Events specifici aggiornando il suo stato interno.
  4. Accetta comandi di logica di business che verranno trasformati in un aggiornamento Player appropriato.
  5. Può essere creato in più posizioni nell'albero dell'interfaccia utente e manterrà sempre una visualizzazione coerente dello stato del player.
  6. Espone i campi State di Compose che possono essere utilizzati da un composable per rispondere dinamicamente alle modifiche.
  7. Viene fornito con 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(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)
    }
  }
}

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 di Media3 aiuta lo sviluppatore finale a non doversi preoccupare di imparare a conoscere Player.Events.

.