Dodawanie zależności
Biblioteka Media3 zawiera moduł interfejsu użytkownika oparty na Jetpack Compose. Aby go użyć, dodaj tę zależność:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.6.0")
Groovy
implementation "androidx.media3:media3-ui-compose:1.6.0"
Zdecydowanie zalecamy, aby najpierw tworzyć aplikację w Compose lub przejść z użycia widoków.
Pełna aplikacja demo Compose
Biblioteka media3-ui-compose
nie zawiera gotowych komponentów kompozytowych (takich jak przyciski, wskaźniki, obrazy czy dialogi), ale możesz znaleźć aplikację demonstracyjną napisaną w pełni w Compose, która nie korzysta z rozwiązań zapewniających interoperacyjność, takich jak owijanie PlayerView
w AndroidView
. Aplikacja demonstracyjna korzysta z klas uchwytujących stan interfejsu z modułu media3-ui-compose
i z biblioteki Compose Material3.
Zmienne stanów interfejsu
Aby lepiej zrozumieć, jak możesz korzystać z elastyczności komponentów stanu interfejsu użytkownika w porównaniu z komponentami składanymi, przeczytaj, jak Compose zarządza stanem.
Zmienne stanów przycisku
W przypadku niektórych stanów interfejsu zakładamy, że będą one najprawdopodobniej używane przez komponenty kompozytowe podobne do przycisków.
Region | remember*State | Typ |
---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
Stała |
NextButtonState |
rememberNextButtonState |
Stała |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Menu lub przełącznik N-Toggle |
Przykład użycia 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),
)
}
}
Zwróć uwagę, że state
nie zawiera informacji o motywie, np. ikony do odtwarzania lub wstrzymywania. Jego jedynym zadaniem jest przekształcenie Player
w stan interfejsu użytkownika.
Następnie możesz dowolnie łączyć przyciski w układ:
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player)
PlayPauseButton(player)
NextButton(player)
}
Zmienne stanów wizualnych
PresentationState
zawiera informacje o tym, kiedy wyjście wideo w PlayerSurface
może być wyświetlane lub powinno być zastąpione przez element zastępczy interfejsu.
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))
}
Tutaj możemy użyć zarówno presentationState.videoSizeDp
, aby dopasować powierzchnię do żądanego formatu obrazu (więcej typów znajdziesz w dokumentacji ContentScale), jak i presentationState.coverSurface
, aby określić, kiedy nie jest odpowiedni czas na wyświetlanie powierzchni. W takim przypadku możesz umieścić na powierzchni nieprzezroczystą przesłonę, która zniknie, gdy powierzchnia będzie gotowa.
Gdzie znajdują się przepływy danych?
Wielu deweloperów aplikacji na Androida zna obiekty Kotlina Flow
, które służą do zbierania stale zmieniających się danych interfejsu. Możesz na przykład szukać Player.isPlaying
przepływu, który możesz collect
w sposób uwzględniający cykl życia. Możesz też użyć Player.eventsFlow
, aby uzyskać Flow<Player.Events>
, który możesz filter
w sposób, który Ci odpowiada.
Korzystanie z przepływów w przypadku stanu interfejsu Player
ma jednak pewne wady. Jednym z głównych problemów jest asynchroniczny charakter przesyłania danych. Chcemy zapewnić jak najmniejsze opóźnienie między Player.Event
a jego wykorzystaniem po stronie interfejsu użytkownika, unikając wyświetlania elementów interfejsu, które są niezsynchronizowane z Player
.
Inne kwestie:
- Przebieg z wszystkimi
Player.Events
nie byłby zgodny z zasadami odpowiedzialności, ponieważ każdy konsument musiałby odfiltrowywać odpowiednie zdarzenia. - Aby utworzyć przepływ dla każdego elementu
Player.Event
, musisz połączyć je (za pomocącombine
) dla każdego elementu interfejsu. Zdarzenie Player.Event jest mapowane jeden-do-wielu na zmianę elementu interfejsu. Wymaganie korzystania z funkcjicombine
może spowodować, że interfejs będzie działać w sposób niezgodny z prawem.
Tworzenie niestandardowych stanów interfejsu
Jeśli istniejące stany interfejsu nie spełniają Twoich potrzeb, możesz dodać niestandardowe stany interfejsu. Sprawdź kod źródłowy dotychczasowego stanu, aby skopiować wzór. Typowa klasa uchwytu stanu interfejsu użytkownika:
- Trwa
Player
. - Subskrybuje
Player
za pomocą coroutines. Więcej informacji znajdziesz w artykulePlayer.listen
. - Reaguje na określone
Player.Events
, aktualizując swój stan wewnętrzny. - Akceptuj polecenia biznesowe, które zostaną przekształcone w odpowiednie aktualizacje
Player
. - Można je tworzyć w różnych miejscach w drzewie interfejsu użytkownika i zawsze będą one zachowywać spójny widok stanu odtwarzacza.
- Wyświetla pola Compose
State
, które mogą być używane przez kompozyt, aby dynamicznie reagować na zmiany. - Zawiera funkcję
remember*State
, która zapamiętuje instancję między składaniami.
Co dzieje się za kulisami:
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)
}
}
}
Aby zareagować na własne Player.Events
, możesz je złapać za pomocą Player.listen
, czyli suspend fun
, który pozwala wejść do świata współbieżnego i bez końca słuchać Player.Events
. Implementacja Media3 różnych stanów interfejsu użytkownika pomaga programiście końcowemu uniknąć konieczności zapoznania się z informacjami na temat Player.Events
.