Core Compose

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.Events nie byłoby zgodne z zasadą pojedynczej odpowiedzialności, ponieważ każdy odbiorca musiałby odfiltrować odpowiednie zdarzenia.
  • Utworzenie flow dla każdego Player.Event bę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żywanie combine moż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:

  1. Przyjmuje obiekt Player.
  2. Subskrybuje Player za pomocą korutyn. Więcej informacji znajdziesz w artykule Player.listen.
  3. Reaguje na określone Player.Events, aktualizując swój stan wewnętrzny.
  4. Akceptuje polecenia logiki biznesowej, które zostaną przekształcone w odpowiednią aktualizację Player.
  5. Może być tworzona w wielu miejscach w drzewie interfejsu i zawsze będzie utrzymywać spójny widok stanu odtwarzacza.
  6. Udostępnia pola State w Compose, które mogą być używane przez komponent, aby dynamicznie reagować na zmiany.
  7. 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.