Biblioteka media3-ui-compose zawiera podstawowe komponenty do tworzenia interfejsu multimediów w Jetpack Compose. Jest przeznaczona dla deweloperów, którzy potrzebują większej możliwości dostosowania niż ta, którą oferuje biblioteka media3-ui-compose-material3. Z tej strony dowiesz się, jak używać podstawowych komponentów i zmiennych stanu do tworzenia niestandardowego interfejsu odtwarzacza multimediów.
Łączenie komponentów Material3 i niestandardowych komponentów Compose
Biblioteka media3-ui-compose-material3 jest elastyczna. Większość interfejsu możesz tworzyć za pomocą gotowych komponentów, ale w razie potrzeby możesz zastąpić pojedynczy komponent implementacją niestandardową. Wtedy właśnie przydaje się biblioteka media3-ui-compose.
Załóżmy, że chcesz używać standardowych
PreviousButton i NextButton z biblioteki
Material3, ale potrzebujesz całkowicie niestandardowego komponentu PlayPauseButton. Możesz
to zrobić, używając PlayPauseButton z podstawowej media3-ui-compose
biblioteki i umieszczając go obok gotowych komponentów.
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) }
Dostępne komponenty
Biblioteka media3-ui-compose zawiera zestaw gotowych komponentów do typowych elementów sterujących odtwarzacza. Oto niektóre komponenty, których możesz używać bezpośrednio w aplikacji:
| Komponent | Opis |
|---|---|
PlayPauseButton |
Kontener stanu przycisku, który przełącza się między odtwarzaniem a wstrzymaniem. |
SeekBackButton |
Kontener stanu przycisku, który przewija do tyłu o zdefiniowany przyrost. |
SeekForwardButton |
Kontener stanu przycisku, który przewija do przodu o zdefiniowany przyrost. |
NextButton |
Kontener stanu przycisku, który przewija do następnego elementu multimedialnego. |
PreviousButton |
Kontener stanu przycisku, który przewija do poprzedniego elementu multimedialnego. |
RepeatButton |
Kontener stanu przycisku, który przełącza tryby powtarzania. |
ShuffleButton |
Kontener stanu przycisku, który włącza i wyłącza tryb losowy. |
MuteButton |
Kontener stanu przycisku, który wycisza i wyłącza wyciszenie odtwarzacza. |
TimeText |
Kontener stanu komponentu, który wyświetla postęp odtwarzania. |
ContentFrame |
Powierzchnia do wyświetlania treści multimedialnych, która obsługuje zarządzanie proporcjami obrazu, zmianę rozmiaru i migawkę. |
PlayerSurface |
Surowa powierzchnia, która opakowuje SurfaceView i TextureView w AndroidView. |
Dostosowywanie komponentu Player
Gdy używasz biblioteki media3-ui-compose-material3, komponent
Player zapewnia pełną obsługę odtwarzania multimediów. Możesz dostosować jego układ, udostępniając własne komponenty w jego miejscach na treści, lub użyć obiektu PlayerDefaults, aby dostosować określone części interfejsu, takie jak TopControls, CenterControls, BottomControls i ErrorOverlay.
Możesz na przykład użyć PlayerDefaults.CenterControls, aby zastąpić tylko środkowy przycisk odtwarzania/wstrzymywania, a resztę elementów sterujących pozostawić bez zmian.
Możesz też przekazać w pełni niestandardowy komponent jako parametr, np. topControls, i całkowicie pominąć inne, np. bottomControls, aby pozostawić je w domyślnej postaci.
@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 ) }
Zmienne stanu interfejsu
Jeśli żaden z komponentów szkieletowych nie spełnia Twoich potrzeb, możesz też użyć bezpośrednio obiektów stanu. Zalecamy używanie odpowiednich metod remember, aby zachować wygląd interfejsu między ponownymi komponowaniami.
Aby lepiej zrozumieć, jak możesz wykorzystać elastyczność zmiennych stanu interfejsu w porównaniu z komponentami, przeczytaj, jak Compose zarządza stanem.
Zmienne stanu przycisku
W przypadku niektórych stanów interfejsu biblioteka zakłada, że będą one najprawdopodobniej używane przez komponenty podobne do przycisków.
| Stan | remember*State | Typ |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-stanowy |
PreviousButtonState |
rememberPreviousButtonState |
Stały |
NextButtonState |
rememberNextButtonState |
Stały |
RepeatButtonState |
rememberRepeatButtonState |
3-stanowy |
ShuffleButtonState |
rememberShuffleButtonState |
2-stanowy |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Menu lub N-stanowy |
Przykład użycia 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), ) }
Zmienne stanu wyjścia wizualnego
CurrentMediaItemState zawiera informacje o aktualnie odtwarzanym
MediaItem, a PlaylistState – informacje o MediaItems
ustawionych w odtwarzaczu. Przydają się one do wyświetlania metadanych w niestandardowym interfejsie.
ErrorState zawiera informacje o bieżącym stanie błędu odtwarzacza, które można wykorzystać do wyświetlania nakładki błędu.
PresentationState zawiera informacje o tym, kiedy można wyświetlać wyjście wideo w
PlayerSurface lub kiedy powinno być ono zasłonięte przez element zastępczy interfejsu.
ContentFrame composable łączy obsługę proporcji obrazu z wyświetlaniem migawki na powierzchni, która nie jest jeszcze gotowa.
@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() } }
Możemy tu użyć zarówno presentationState.videoAspectRatio, aby przeskalować powierzchnię
do wybranych proporcji obrazu (więcej typów znajdziesz w sekcji Skala treści), jak i
presentationState.coverSurface, aby wiedzieć, kiedy nie należy
wyświetlać powierzchni. W takim przypadku możesz umieścić nieprzezroczystą migawkę na powierzchni, która zniknie, gdy powierzchnia będzie gotowa.
ContentFrame umożliwia dostosowanie migawki jako lambdy końcowej, ale domyślnie będzie to czarny @Composable Box wypełniający rozmiar kontenera nadrzędnego.
Gdzie są Flows?
Wielu deweloperów Androida zna obiekty Flow w Kotlinie, które służą do zbierania stale zmieniających się danych interfejsu. Możesz na przykład szukać Player.isPlaying flow, które możesz collect w sposób uwzględniający cykl życia. Lub
czegoś takiego jak Player.eventsFlow, aby udostępnić Ci Flow<Player.Events>
, które możesz filter w dowolny sposób.
Używanie flow do stanu interfejsu Player ma jednak pewne wady. Jednym z głównych problemów jest asynchroniczny charakter przesyłania danych. Chcemy osiągnąć jak
najmniejsze opóźnienie między Player.Event a jego wykorzystaniem po stronie
interfejsu, unikając wyświetlania elementów interfejsu, które są niezsynchronizowane z Player.
Inne kwestie:
- Flow ze wszystkimi
Player.Eventsnie byłoby zgodne z zasadą pojedynczej odpowiedzialności, ponieważ każdy odbiorca musiałby odfiltrować odpowiednie zdarzenia. - Utworzenie flow dla każdego
Player.Eventbędzie wymagało połączenia ich (za pomocącombine) dla każdego elementu interfejsu. Istnieje mapowanie wiele-do-wielu między Player.Event a zmianą elementu interfejsu. Używaniecombinemoże prowadzić do potencjalnie nieprawidłowych stanów interfejsu.
Tworzenie niestandardowych stanów interfejsu
Jeśli istniejące stany nie spełniają Twoich potrzeb, możesz dodać stany niestandardowe. Aby skopiować wzorzec, sprawdź kod źródłowy istniejącego stanu. Typowa klasa zmiennej stanu interfejsu robi to:
- Przyjmuje obiekt
Player. - Subskrybuje
Playerza pomocą korutyn. Więcej informacji znajdziesz w artykulePlayer.listen. - Reaguje na określone
Player.Events, aktualizując swój stan wewnętrzny. - Akceptuje polecenia logiki biznesowej, które zostaną przekształcone w odpowiednią aktualizację
Player. - Może być tworzona w wielu miejscach w drzewie interfejsu i zawsze będzie utrzymywać spójny widok stanu odtwarzacza.
- Udostępnia pola
Statew Compose, które mogą być używane przez komponent, aby dynamicznie reagować na zmiany. - Zawiera funkcję
remember*State, która zapamiętuje instancję między komponowaniami.
Co się dzieje w tle:
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) } } }
Aby reagować na własne Player.Events, możesz je przechwytywać za pomocą Player.listen
czyli suspend fun, która pozwala wejść do świata korutyn i
nieprzerwanie nasłuchiwać Player.Events. Implementacja różnych stanów interfejsu w Media3 pomaga deweloperowi nie przejmować się poznawaniem Player.Events.