Aggiungi la dipendenza
La libreria Media3 include un modulo UI basato su Jetpack Compose. Per utilizzarlo, aggiungi la seguente dipendenza:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.7.1")
Groovy
implementation "androidx.media3:media3-ui-compose:1.7.1"
Ti consigliamo vivamente di sviluppare la tua app in modo da dare la priorità a Compose o di eseguire la migrazione dall'utilizzo di View.
App demo Fully Compose
Sebbene la libreria media3-ui-compose
non includa
Componibili predefiniti (come pulsanti, indicatori, immagini o dialoghi), puoi trovare un'app demo scritta interamente in Compose che evita qualsiasi soluzione di interoperabilità, come il wrapping di PlayerView
in AndroidView
. L'app demo
utilizza le classi di gestione dello stato della UI del modulo media3-ui-compose
e si avvale
della libreria Compose Material3.
Contenitori di stato UI
Per capire meglio come utilizzare la flessibilità dei contenitori di stato dell'interfaccia utente rispetto ai composable, leggi come Compose gestisce lo stato.
Contenitori di stato dei pulsanti
Per alcuni stati dell'interfaccia utente, presupponiamo 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
:
@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
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),
)
}
}
Tieni presente che state
non contiene informazioni sui temi, ad esempio l'icona da utilizzare per la riproduzione
o la pausa. La sua unica responsabilità è trasformare Player
nello stato dell'interfaccia utente.
Puoi quindi combinare i pulsanti nel layout che preferisci:
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player)
PlayPauseButton(player)
NextButton(player)
}
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.
val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)
Box(modifier) {
// 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 = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = scaledModifier,
)
if (presentationState.coverSurface) {
// Cover the surface that is being prepared with a shutter
Box(Modifier.background(Color.Black))
}
Qui possiamo utilizzare sia presentationState.videoSizeDp
per scalare la superficie in base alle proporzioni desiderate (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.
Dove si trovano i flussi?
Molti sviluppatori Android hanno familiarità con l'utilizzo degli oggetti Kotlin Flow
per raccogliere
dati UI in continua evoluzione. Ad esempio, potresti essere alla ricerca di un flusso Player.isPlaying
che puoi collect
in modo consapevole del ciclo di vita. o
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 garantire la minor latenza possibile tra un Player.Event
e il suo consumo 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à e ogni consumatore dovrebbe filtrare gli eventi pertinenti. - La creazione di un flusso per ogni
Player.Event
richiede di combinarli (concombine
) per ogni elemento UI. Esiste una mappatura many-to-many tra un Player.Event e una modifica dell'elemento UI. L'utilizzo dicombine
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 di gestione dello stato della UI esegue le seguenti operazioni:
- Include un
Player
. - Esegue la sottoscrizione a
Player
utilizzando le coroutine. Per maggiori dettagli, consultaPlayer.listen
. - Risponde a particolari
Player.Events
aggiornando il suo stato interno. - Accetta i comandi della logica di business che verranno trasformati in un aggiornamento
Player
appropriato. - Possono essere creati in più posizioni nell'albero dell'interfaccia utente e manterranno sempre una visualizzazione coerente dello stato del giocatore.
- Espone i campi
State
di Compose che possono essere utilizzati da un elemento componibile per rispondere in modo dinamico alle modifiche. - È 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 catturarli 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
.