Core Compose

La libreria media3-ui-compose fornisce i componenti di base per la creazione di una UI multimediale in Jetpack Compose. È progettata per gli sviluppatori che hanno bisogno di una maggiore personalizzazione 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 una UI del media player personalizzata.

Combinazione di componenti Compose personalizzati e Material3

La libreria media3-ui-compose-material3 è progettata per essere flessibile. Puoi utilizzare i componenti predefiniti per la maggior parte della 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, immagina di voler utilizzare i componenti standard PreviousButton e NextButton della libreria Material3, ma di aver bisogno di un componente PlayPauseButton completamente personalizzato. Puoi ottenere questo risultato utilizzando PlayPauseButton della 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 composabili 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 consente di passare dalla riproduzione alla pausa e viceversa.
SeekBackButton Un contenitore di stato per un pulsante che consente di tornare indietro di un incremento definito.
SeekForwardButton Un container di stato per un pulsante che avanza di un incremento definito.
NextButton Un container di stato per un pulsante che passa all'elemento multimediale successivo.
PreviousButton Un contenitore di stato per un pulsante che consente di passare all'elemento multimediale precedente.
RepeatButton Un contenitore di stato per un pulsante che consente di scorrere le modalità repeat.
ShuffleButton Un container di stato per un pulsante che attiva/disattiva la modalità shuffle.
MuteButton Un container di stato per un pulsante che disattiva e riattiva l'audio del player.
TimeText Un container 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.

Personalizza il composable Player

Quando utilizzi la libreria media3-ui-compose-material3, il composable Player offre un'esperienza di riproduzione multimediale completa. Puoi personalizzare il layout fornendo i tuoi composabili agli slot dei contenuti o puoi sfruttare l'oggetto PlayerDefaults per personalizzare parti specifiche della UI, come TopControls, CenterControls, BottomControls e ErrorOverlay.

La Figura 1 mostra dove il componibile Player posiziona i suoi slot. La Figura 2 mostra l'ordine in cui vengono disegnati i livelli.

Un video player con contorni tratteggiati che contrassegnano le sue aree annuncio. Dall'alto verso il basso: topControls lungo il bordo superiore, errorOverlay sopra il centro, centerControls al centro con i pulsanti backSecondary, back, central, forward e forwardSecondary, sottotitoli vicino alla parte inferiore e bottomControls lungo il bordo inferiore con gli slot above, left, progressSlider, right e below. ContentFrame riempie l'area dietro di loro.
Figura 1. Gli slot del componibile Player. I controlli centrali e inferiori mostrano gli slot di PlayerDefaults.CenterControls e PlayerDefaults.BottomControls. Gli slot topControls, above e below sono vuoti per impostazione predefinita.
Una vista esplosa di sei livelli impilati. Dal basso verso l'alto: 1,
      PlayerSurface; 2, shutter; 3, artwork; 4, subtitles; 5, errorOverlay; e
      6, topControls, centerControls e bottomControls. I livelli da 1 a 4 costituiscono ContentFrame, mentre tutti e sei costituiscono Player.
Figura 2. I livelli del componibile Player, numerati nell'ordine in cui vengono disegnati.

Ad esempio, puoi utilizzare PlayerDefaults.CenterControls per eseguire l'override solo del pulsante centrale di riproduzione/pausa, lasciando invariati gli altri controlli centrali. Puoi anche passare un composable completamente personalizzato per un parametro come topControls e omettere completamente altri come bottomControls per lasciarli al valore predefinito.

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

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 della UI tra le ricomposizioni.

Per comprendere meglio come sfruttare la flessibilità dei contenitori di stato della UI rispetto ai componibili, scopri come Compose gestisce lo stato.

Contenitori di stato dei pulsanti

Per alcuni stati della UI, la libreria presuppone che molto probabilmente verranno utilizzati da elementi componibili 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 sul MediaItem attualmente in riproduzione, mentre PlaylistState espone informazioni sul MediaItems impostato sul player. Questi sono utili per visualizzare i metadati visibili nella tua UI personalizzata.

ErrorState fornisce informazioni sullo stato di errore corrente del player, che possono essere utilizzate per visualizzare un overlay di errore.

PresentationState contiene informazioni su quando l'output video in un PlayerSurface può essere mostrato o deve essere coperto da un elemento UI segnaposto. Il composable ContentFrame 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()
  }
}

In questo caso, possiamo utilizzare sia presentationState.videoAspectRatio per scalare la superficie alle proporzioni scelte (consulta 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 consente di personalizzare l'otturatore come lambda finale, ma per impostazione predefinita sarà un @Composable Box nero che riempie le dimensioni del container 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 cercare un flusso Player.isPlaying che puoi collect in modo da tenere conto 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 della UI di Player presenta alcuni svantaggi. Uno dei problemi principali è la natura asincrona del trasferimento di dati. Vogliamo ottenere la minore latenza possibile tra un Player.Event e la sua fruizione sul lato UI, evitando di mostrare elementi UI non sincronizzati con il Player.

Altri punti includono:

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

Crea stati UI personalizzati

Puoi aggiungere stati della UI personalizzati se quelli esistenti non soddisfano le tue esigenze. Controlla il codice sorgente dello stato esistente per copiare il pattern. Una tipica classe contenitore di stato della UI esegue le seguenti operazioni:

  1. Accetta un oggetto Player.
  2. Consente di iscriversi a Player utilizzando le coroutine. Per ulteriori dettagli, consulta Player.listen.
  3. Risponde a particolari Player.Events aggiornando il suo stato interno.
  4. Accetta i comandi della logica di business che verranno trasformati in un aggiornamento Player appropriato.
  5. Può essere creato in più punti dell'albero dell'interfaccia utente e manterrà sempre una visualizzazione coerente dello stato del Player.
  6. Espone i campi Compose State che possono essere utilizzati da un composable per rispondere dinamicamente alle modifiche.
  7. Include 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 intercettarli utilizzando Player.listen, che è un suspend fun che ti consente di entrare nel mondo delle coroutine e ascoltare Player.Events a tempo indeterminato. L'implementazione di Media3 di vari stati della UI aiuta lo sviluppatore finale a non doversi preoccupare di imparare a utilizzare Player.Events.