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.Eventsnon rispetterebbe un principio di responsabilità singola, ogni consumer dovrebbe filtrare gli eventi pertinenti. - La creazione di un flusso per ogni
Player.Eventrichiederà di combinarli (concombine) 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 dicombinepotrebbe 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:
- Accetta un
Playeroggetto. - Si iscrive a
Playerutilizzando le coroutine. Per maggiori dettagli, consultaPlayer.listenper più dettagli. - Risponde a
Player.Eventsspecifici aggiornando il suo stato interno. - Accetta comandi di logica di business che verranno trasformati in un aggiornamento
Playerappropriato. - Può essere creato in più posizioni nell'albero dell'interfaccia utente e manterrà sempre una visualizzazione coerente dello stato del player.
- Espone i campi
Statedi Compose che possono essere utilizzati da un composable per rispondere dinamicamente alle modifiche. - Viene fornito con 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 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.