Biblioteka media3-ui-compose udostępnia podstawowe komponenty do tworzenia interfejsu multimedialnego w Jetpack Compose. Jest przeznaczony dla deweloperów, którzy potrzebują większych możliwości dostosowywania niż te, które oferuje biblioteka media3-ui-compose-material3. Na tej stronie dowiesz się, jak używać podstawowych komponentów i zmiennych stanu do tworzenia niestandardowego interfejsu odtwarzacza multimedialnego.
Łączenie komponentów Material 3 i niestandardowych komponentów Compose
Biblioteka media3-ui-compose-material3 została zaprojektowana z myślą o elastyczności. Możesz używać gotowych komponentów w większości interfejsu, ale w razie potrzeby możesz zastąpić pojedynczy komponent niestandardową implementacją, aby mieć większą kontrolę. W tym momencie przydaje się biblioteka media3-ui-compose.
Załóżmy na przykład, że chcesz użyć standardowych komponentów PreviousButton i NextButton z biblioteki Material3, ale potrzebujesz całkowicie niestandardowego komponentu PlayPauseButton. Możesz to zrobić, używając PlayPauseButton z podstawowej biblioteki media3-ui-compose 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 funkcji kompozycyjnych dla typowych elementów sterujących odtwarzaczem. Oto kilka komponentów, których możesz używać bezpośrednio w aplikacji:
| Komponent | Opis |
|---|---|
PlayPauseButton |
Kontener stanu przycisku, który przełącza między odtwarzaniem a wstrzymaniem. |
SeekBackButton |
Kontener stanu przycisku, który przewija do tyłu o określony interwał. |
SeekForwardButton |
Kontener stanu przycisku, który przewija do przodu o określony interwał. |
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 tryb odtwarzania losowego. |
MuteButton |
Kontener stanu przycisku, który wycisza i wyłącza wyciszenie odtwarzacza. |
TimeText |
Kontener stanu dla funkcji kompozycyjnej, która wyświetla postęp odtwarzacza. |
ContentFrame |
Powierzchnia do wyświetlania treści multimedialnych, która obsługuje zarządzanie formatem obrazu, zmianę rozmiaru i migawkę |
PlayerSurface |
Surowa powierzchnia, która zawiera SurfaceView i TextureView w AndroidView. |
Dostosowywanie funkcji kompozycyjnej Player
Gdy używasz biblioteki media3-ui-compose-material3, funkcja kompozycyjna Player zapewnia pełne możliwości odtwarzania multimediów. Możesz dostosować jego układ, udostępniając własne elementy kompozycyjne w miejscach na treści, lub użyć obiektu PlayerDefaults, aby dostosować określone części interfejsu, takie jak TopControls, CenterControls, BottomControls i ErrorOverlay.
Rysunek 1 pokazuje, gdzie funkcja kompozycyjna Player umieszcza swoje boksy. Rysunek 2 przedstawia kolejność, w jakiej rysowane są warstwy.
Player.
Elementy sterujące na środku i na dole pokazują boksy PlayerDefaults.CenterControls i PlayerDefaults.BottomControls. Gniazda topControls, above i below są domyślnie puste.Player, ponumerowane w kolejności ich rysowania.Możesz na przykład użyć PlayerDefaults.CenterControls, aby zastąpić tylko środkowy przycisk odtwarzania/wstrzymania, pozostawiając pozostałe elementy sterujące bez zmian.
Możesz też przekazać w pełni niestandardowy element kompozycyjny dla parametru takiego jak topControls i całkowicie pominąć inne, takie jak bottomControls, aby pozostawić je w stanie domyślnym.
@Composable fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, showControls = showControls, topControls = { // Fully custom top controls Text("My custom title") }, centerControls = { // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = player, central = { // A custom play/pause button Material3PlayPauseButton(player, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
Zmienne stanów interfejsu
Jeśli żaden z komponentów szkieletu nie odpowiada Twoim potrzebom, możesz też używać bezpośrednio obiektów stanu. Zwykle zaleca się używanie odpowiednich metod remember, aby zachować wygląd interfejsu między rekompozycjami.
Aby lepiej zrozumieć, jak możesz wykorzystać elastyczność zmiennych stanu interfejsu w porównaniu z funkcjami kompozytowymi, przeczytaj o tym, jak Compose zarządza stanem.
Zmienne stanu przycisku
W przypadku niektórych stanów interfejsu biblioteka zakłada, że będą one najprawdopodobniej wykorzystywane przez elementy kompozycyjne przypominające przyciski.
| Stan | 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 |
Przykładowe użycie 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 danych wyjściowych w formie wizualnej
CurrentMediaItemState zawiera informacje o aktualnie odtwarzanym elemencie MediaItem, a PlaylistState udostępnia informacje o elemencie MediaItems ustawionym w odtwarzaczu. Są one przydatne do wyświetlania metadanych w niestandardowym interfejsie.
ErrorState zawiera informacje o bieżącym stanie błędu odtwarzacza, które można wykorzystać do wyświetlenia nakładki błędu.
PresentationState zawiera informacje o tym, kiedy dane wyjściowe wideo w PlayerSurface mogą być wyświetlane lub powinny być zakryte przez element interfejsu zastępczego.
Funkcja kompozycyjna ContentFrame łączy obsługę formatu 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 użyć zarówno presentationState.videoAspectRatio, aby przeskalować Surface do wybranego formatu obrazu (więcej typów znajdziesz w sekcji Skala treści), jak i presentationState.coverSurface, aby wiedzieć, kiedy nie jest odpowiedni moment na wyświetlanie Surface. W takim przypadku możesz umieścić na powierzchni nieprzezroczystą zasłonę, która zniknie, gdy powierzchnia będzie gotowa.
ContentFrame pozwala dostosować migawkę jako końcową funkcję lambda, ale domyślnie będzie to czarny element @Composable Box wypełniający rozmiar kontenera nadrzędnego.
Gdzie są przepływy?
Wielu programistów aplikacji na Androida zna obiekty Flow w języku Kotlin, które służą do zbierania stale zmieniających się danych interfejsu. Możesz na przykład szukać przepływu Player.isPlaying, 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 dowolny sposób.
Jednak używanie przepływów do obsługi stanu interfejsu Player ma 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 nie są zsynchronizowane z Player.
Inne punkty:
- Przepływ ze wszystkimi
Player.Eventsnie byłby zgodny z zasadą pojedynczej odpowiedzialności, ponieważ każdy konsument musiałby odfiltrowywać odpowiednie zdarzenia. - Utworzenie przepływu dla każdego elementu
Player.Eventbędzie wymagało ich połączenia (za pomocącombine) dla każdego elementu interfejsu. Istnieje mapowanie wiele do wielu między Player.Event a zmianą elementu interfejsu. Użyciecombinemoże spowodować, że interfejs użytkownika 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. Sprawdź kod źródłowy istniejącego stanu, aby skopiować wzorzec. Typowa klasa zmiennej stanu interfejsu:
- Przyjmuje obiekt
Player. - Subskrybuje
Playerza pomocą współprogramów. Więcej informacji znajdziesz na stroniePlayer.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żna go utworzyć w wielu miejscach w drzewie interfejsu i zawsze będzie zachowywać spójny widok stanu odtwarzacza.
- Udostępnia pola
StateCompose, które mogą być używane przez funkcję kompozycyjną do dynamicznego reagowania 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(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 współprogramów i bezterminowo nasłuchiwać Player.Events. Implementacja różnych stanów interfejsu w Media3 sprawia, że deweloper nie musi się martwić o Player.Events.