Biblioteka media3-ui-compose udostępnia podstawowe komponenty do tworzenia interfejsu multimediów 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 wyjaśniamy, jak używać podstawowych komponentów i zmiennych stanu do tworzenia niestandardowego interfejsu odtwarzacza multimediów.
Łą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 takiej sytuacji 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 do obsługi 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 się 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 umożliwia przejście 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 przełą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 na treści multimedialne, która obsługuje zarządzanie formatem obrazu, zmianę rozmiaru, migawkę, grafikę i miejsce na napisy. |
PlayerSurface |
Nieprzetworzona powierzchnia, która zawiera SurfaceView i TextureView w AndroidView. |
Artwork |
Funkcja kompozycyjna, która wyświetla grafikę bieżącego elementu multimedialnego, np. okładkę albumu. |
Dostosowywanie funkcji kompozycyjnej Player
Gdy używasz biblioteki media3-ui-compose-material3, funkcja kompozycyjna Player zapewnia pełne odtwarzanie multimediów. Możesz dostosować jego układ, dodając własne elementy kompozycyjne do boksów 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ą gniazda 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 ) }
Komponent Player wyświetla też napisy, a gdy nie ma filmu, grafikę bieżącego elementu multimedialnego. Aby dostosować ich wygląd, przekaż własne elementy kompozycyjne do slotów subtitles i artwork. Aby je ukryć, przekaż pusty element kompozycyjny do subtitles i null do artwork.
Zmienne stanów interfejsu
Jeśli żaden z komponentów szkieletu nie spełnia Twoich potrzeb, 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 lub N-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
Stała |
NextButtonState |
rememberNextButtonState |
Stała |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Menu lub N-Toggle |
Oprócz stanów odtwarzania i wstrzymania PlayPauseButtonState udostępnia też stany showBuffering, showReplay i showRetry. Używaj ich do wyświetlania wskaźnika buforowania podczas buforowania odtwarzacza, ikony ponownego odtwarzania po zakończeniu odtwarzania i ikony ponownej próby po wystąpieniu błędu odtwarzania. showReplay i showRetry uściślają showPlay: gdy jeden z nich jest true, showPlay również jest true, więc sprawdź je przed showPlay. Metoda onClick obsługuje również te stany, na przykład przygotowując odtwarzacz ponownie po wystąpieniu błędu.
Aby wybrać, które z tych stanów mają być wyświetlane, przekaż kombinację wartości SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED i SHOW_RETRY_ON_ERROR do parametru displayMode metody rememberPlayPauseButtonState. Domyślnie displayMode łączy SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED i SHOW_RETRY_ON_ERROR, co powoduje wyświetlanie wszystkich 3 stanów, a stan buforowania pojawia się nawet wtedy, gdy odtwarzanie jest wstrzymane.
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
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.
PlaybackState udostępnia podstawowy stan odtwarzania, taki jak playbackState, isPlaying i playerError, niezależnie od konkretnego komponentu interfejsu. Możesz go używać do wyświetlania nakładek lub niestandardowych układów, np. wskaźnika wczytywania, gdy odtwarzacz buforuje dane. Aby użyć gotowego wskaźnika Material3, użyj BufferingIndicator.
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() } }
W tym przypadku możemy użyć zarówno presentationState.videoAspectRatio, aby przeskalować powierzchnię 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 powierzchni. W takim przypadku możesz umieścić na powierzchni nieprzezroczystą przesł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.
PresentationState udostępnia też właściwość showArtwork, która ma wartość true, gdy bieżący element multimedialny jest odtwarzany bez ścieżki wideo, np. w przypadku treści zawierających tylko dźwięk. W takim przypadku ContentFrame wyświetla element kompozycyjny Artwork, który pokazuje grafikę z MediaMetadata elementu. Aby dostosować lub ukryć grafikę, przekaż własny element kompozycyjny lub null do boksu artwork elementu ContentFrame.
Domyślnie Artwork wczytuje tylko dane grafiki umieszczone w metadanych lub z identyfikatora URI content, file lub android.resource. Aby wczytać grafikę ze zdalnych identyfikatorów URI lub innych źródeł, użyj jednej z tych opcji:
- Przekaż
BitmapLoader, np.DataSourceBitmapLoader, doArtwork. - Użyj przeciążenia
Artwork, które przyjmujepainter, aby wczytywać obrazy za pomocą wybranej biblioteki.
W przeciwieństwie do funkcji kompozycyjnej Player, która domyślnie renderuje napisy, ContentFrame pozostawia pusty slot subtitles. Aby renderować napisy w ContentFrame lub dostosować renderowanie napisów w Player, obserwuj bieżące wskazówki odtwarzacza za pomocą CuesState i renderuj je w slocie subtitles. ContentFrame umieszcza ten slot w granicach przeskalowanego filmu i wyświetla go nad migawką i grafiką, dzięki czemu napisy pozostają widoczne, gdy nie ma filmu.
Zmienne stanu wyboru ścieżki
TrackSelectionState pomaga utworzyć menu dla jednego typu ścieżki, np. menu języka audio lub napisów. Utwórz go za pomocą rememberTrackSelectionState, przekazując typ ścieżki i kombinację filtrów, takich jak TRACK_FILTER_LANGUAGE lub TRACK_FILTER_RESOLUTION, które określają, które właściwości ścieżki odróżniają opcje. Ustaw hasOffOption lub hasAutoOption na true, aby dodać opcję, która wyłącza typ ścieżki lub umożliwia odtwarzaczowi automatyczny wybór ścieżek.
Stan pokazuje dostępne selectionOptions i selectedOption.
Każda ikona TrackSelectionOption wskazuje, czy urządzenie obsługuje daną funkcję i czy jest ona wybrana. Przekazanie opcji do selectOption odpowiednio aktualizuje parametry wyboru ścieżki odtwarzacza. Przed wyświetleniem menu sprawdź canSetTrackSelectionParameters i czy jest wystarczająco dużo opcji do wyboru. Na przykład poniższe menu napisów zawiera po jednej opcji dla każdego języka napisów oraz opcję wyłączenia napisów:
@Composable fun SubtitleMenu(player: Player, modifier: Modifier = Modifier) { val state = rememberTrackSelectionState( player, trackType = C.TRACK_TYPE_TEXT, trackFilters = TRACK_FILTER_LANGUAGE, hasOffOption = true, ) // Hide the menu if the player can't change tracks or has no subtitles to choose from if (!state.canSetTrackSelectionParameters || state.selectionOptions.size < 2) return Column(modifier.selectableGroup()) { for (option in state.selectionOptions) { Row( Modifier.fillMaxWidth() .heightIn(min = 48.dp) .selectable( selected = option.isSelected, enabled = option.isSupported, role = Role.RadioButton, onClick = { state.selectOption(option) }, ) .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton(selected = option.isSelected, onClick = null, enabled = option.isSupported) Text( text = if (option.onOffState == TRACK_SELECTION_OFF) "Off" else option.language?.let { Locale.forLanguageTag(it).displayName } ?: "Unknown", color = if (option.isSupported) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.38f), ) } } } }
Jeśli potrzebujesz bezpośredniego dostępu do Tracks i TrackSelectionParameters odtwarzacza, użyj TrackSelectionParametersState. Więcej informacji o tym, jak odtwarzacz wybiera ścieżki, znajdziesz w sekcji Wybór ścieżki.
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ć prompta takiego jak Player.eventsFlow, aby otrzymać 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 zdarzeniem Player.Event a zmianą elementu interfejsu. Konieczność użyciacombinemoże prowadzić do potencjalnie nielegalnych stanów interfejsu.
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*Statedo zapamiętywania instancji 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 pomaga deweloperowi, który nie musi uczyć się o Player.Events.