Dodawanie zależności
Biblioteka Media3 zawiera moduł interfejsu oparty na Jetpack Compose. Aby go użyć, dodaj tę zależność:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.7.1")
Groovy
implementation "androidx.media3:media3-ui-compose:1.7.1"
Zdecydowanie zalecamy tworzenie aplikacji w pierwszej kolejności w Compose lub migrację z używania widoków.
Pełna aplikacja demonstracyjna Compose
Biblioteka media3-ui-compose
nie zawiera gotowych komponentów kompozycyjnych (takich jak przyciski, wskaźniki, obrazy czy okna), ale możesz znaleźć aplikację demonstracyjną napisaną w całości w Compose, która nie korzysta z rozwiązań zapewniających interoperacyjność, takich jak umieszczanie PlayerView
w AndroidView
. Aplikacja demonstracyjna korzysta z klas przechowujących stan interfejsu z modułu media3-ui-compose
i biblioteki Compose Material3.
Zmienne stanów interfejsu
Aby lepiej zrozumieć, jak wykorzystać elastyczność elementów przechowujących stan interfejsu w porównaniu z funkcjami kompozycyjnymi, przeczytaj artykuł o tym, jak Compose zarządza stanem.
Zmienne stanu przycisku
W przypadku niektórych stanów interfejsu zakładamy, że będą one najprawdopodobniej używane przez komponenty kompozycyjne podobne do przycisków.
Województwo | 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 N-Toggle |
Przykładowe użycie elementu 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.
Możesz następnie dopasować przyciski do wybranego układu:
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player)
PlayPauseButton(player)
NextButton(player)
}
Zmienne stanu danych wyjściowych wizualnych
PresentationState
zawiera informacje o tym, kiedy można wyświetlić dane wyjściowe wideo w PlayerSurface
lub kiedy powinny być one zastąpione elementem interfejsu użytkownika.
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))
}
Możemy tu użyć zarówno presentationState.videoSizeDp
, aby dostosować powierzchnię do żądanego współczynnika proporcji (więcej typów znajdziesz w dokumentacji ContentScale), jak i presentationState.coverSurface
, aby wiedzieć, kiedy nie jest odpowiedni moment na wyświetlenie powierzchni. W takim przypadku możesz umieścić nieprzezroczystą zasłonę na powierzchni, która zniknie, gdy będzie gotowa.
Gdzie są przepływy?
Wielu deweloperów Androida wie, jak używać obiektów Kotlin Flow
do zbierania stale zmieniających się danych interfejsu. Możesz na przykład szukać Player.isPlaying
, które możesz collect
w sposób uwzględniający cykl życia. lub coś w rodzaju Player.eventsFlow
, aby zapewnić Ci Flow<Player.Events>
które możesz filter
w dowolny sposób.
Używanie przepływów do zarządzania stanem 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, unikając wyświetlania elementów interfejsu, które nie są zsynchronizowane z Player
.
Inne kwestie:
- Przepływ ze wszystkimi
Player.Events
nie byłby zgodny z zasadą pojedynczej odpowiedzialności, ponieważ każdy odbiorca musiałby odfiltrowywać odpowiednie zdarzenia. - Utworzenie przepływu dla każdego elementu
Player.Event
będzie wymagać połączenia ich (za pomocą elementucombine
) w przypadku każdego elementu interfejsu. Między obiektem Player.Event a zmianą elementu interfejsu istnieje mapowanie wiele do wielu. Używaniecombine
może spowodować, że interfejs użytkownika znajdzie się w stanie potencjalnie niezgodnym z prawem.
Tworzenie niestandardowych stanów interfejsu
Jeśli istniejące stany interfejsu nie spełniają Twoich potrzeb, możesz dodać własne. Sprawdź kod źródłowy istniejącego stanu, aby skopiować wzorzec. Typowa klasa stanu interfejsu użytkownika:
- Przyjmuje wartość
Player
. - Subskrybuje
Player
za pomocą korutyn. Więcej informacji znajdziesz wPlayer.listen
. - Odpowiada na określone
Player.Events
, aktualizując swój stan wewnętrzny. - akceptować polecenia logiki biznesowej, które zostaną przekształcone w odpowiednią
Player
aktualizację; - Można go utworzyć w wielu miejscach w drzewie interfejsu i zawsze będzie on wyświetlać spójny widok stanu odtwarzacza.
- Udostępnia pola
State
Compose, które mogą być używane przez komponent kompozycyjny, aby dynamicznie reagować na zmiany. - Zawiera funkcję
remember*State
, która zapamiętuje instancję między kompozycjami.
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 reagować na własne Player.Events
, możesz je przechwytywać za pomocą Player.listen
, czyli suspend fun
, które pozwala wejść do świata korutyn i bezterminowo nasłuchiwać Player.Events
. Implementacja różnych stanów interfejsu w Media3 pomaga programistom nie przejmować się Player.Events
.