Core Compose

Biblioteka media3-ui-compose zawiera 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 media3-ui-compose-material3biblioteka. Na tej stronie dowiesz się, jak za pomocą podstawowych komponentów i obiektów przechowujących stan utworzyć niestandardowy interfejs odtwarzacza multimediów.

Łączenie komponentów Material3 i komponentów niestandardowych Compose

Biblioteka media3-ui-compose-material3 została zaprojektowana tak, aby była elastyczna. Większość interfejsu możesz utworzyć za pomocą gotowych komponentów, ale w razie potrzeby możesz zastąpić pojedynczy komponent implementacją niestandardową, aby uzyskać większą kontrolę. W tym momencie przydaje się biblioteka media3-ui-compose.

Załóżmy, że chcesz użyć standardowych elementów PreviousButtonNextButton z biblioteki Material3, ale potrzebujesz całkowicie niestandardowego elementu PlayPauseButton. Możesz to zrobić, używając komponentu PlayPauseButton z biblioteki podstawowej 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

media3-ui-compose Biblioteka zawiera zestaw gotowych komponentów kompozycyjnych 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 i wstrzymaniem.
SeekBackButton Kontener stanu przycisku, który przewija do tyłu o określony przyrost.
SeekForwardButton Kontener stanu przycisku, który przewija do przodu o określony przyrost.
NextButton Kontener stanu przycisku, który przechodzi 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 losowego odtwarzania.
MuteButton Kontener stanu przycisku, który wycisza i wyłącza wyciszenie odtwarzacza.
TimeText Kontener stanu dla komponentu, który wyświetla postęp odtwarzania.
ContentFrame Powierzchnia do wyświetlania treści multimedialnych, która zarządza współczynnikiem proporcji, zmianą rozmiaru i migawką.
PlayerSurface Surowa powierzchnia, która opakowuje elementy SurfaceView i TextureView w AndroidView.

Zmienne stanów interfejsu

Jeśli żaden z komponentów szkieletowych nie spełnia Twoich potrzeb, możesz też użyć bezpośrednio obiektów stanu. Zwykle zaleca się używanie odpowiednich metod remember, aby zachować wygląd interfejsu podczas ponownego komponowania.

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 biblioteka zakłada, ż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łady 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 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. ContentFrame Komponent łą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.videoSizeDp)

  // 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, scaledModifier, surfaceType)

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    shutter()
  }
}

Możemy tu użyć zarówno presentationState.videoSizeDp, aby dopasować powierzchnię do wybranego współczynnika proporcji (więcej typów znajdziesz w dokumentacji ContentScale), jak i presentationState.coverSurface, aby wiedzieć, kiedy nie jest odpowiedni czas na wyświetlanie powierzchni. W takim przypadku możesz umieścić nieprzezroczystą zasłonę na powierzchni, która zniknie, gdy będzie gotowa. ContentFrame umożliwia dostosowanie migawki jako lambdy końcowej, ale domyślnie będzie to czarna @Composable Box wypełniająca rozmiar kontenera nadrzędnego.

Gdzie znajdę Flows?

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żna 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 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 kwestie:

  • Przepływ z samymi 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ą elementu combine) w przypadku każdego elementu interfejsu. Istnieje relacja wiele-do-wielu między zdarzeniem Player.Event a zmianą elementu interfejsu. Używanie combine 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ć stany niestandardowe. Sprawdź kod źródłowy istniejącego stanu, aby skopiować wzorzec. Typowa klasa przechowująca stan interfejsu:

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