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.
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.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.Eventsnon rispetterebbe il principio di responsabilità singola, in quanto ogni consumer dovrebbe filtrare gli eventi pertinenti. - La creazione di un flusso per ogni
Player.Eventrichiederà la combinazione (concombine) per ogni elemento UI. Esiste una mappatura many-to-many tra un Player.Event e una modifica di un elemento UI. L'utilizzo dicombinepotrebbe 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:
- Accetta un oggetto
Player. - Consente di iscriversi a
Playerutilizzando le coroutine. Per ulteriori dettagli, consultaPlayer.listen. - Risponde a particolari
Player.Eventsaggiornando il suo stato interno. - Accetta i comandi della logica di business che verranno trasformati in un aggiornamento
Playerappropriato. - Può essere creato in più punti dell'albero dell'interfaccia utente e manterrà sempre una visualizzazione coerente dello stato del Player.
- Espone i campi Compose
Stateche possono essere utilizzati da un composable per rispondere dinamicamente alle modifiche. - Include una funzione
remember*Stateper 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.