Начало работы с пользовательским интерфейсом на основе Compose

Добавить зависимость

Библиотека Media3 включает модуль пользовательского интерфейса на основе Jetpack Compose. Чтобы использовать его, добавьте следующую зависимость:

Котлин

implementation("androidx.media3:media3-ui-compose:1.7.1")

Круто

implementation "androidx.media3:media3-ui-compose:1.7.1"

Мы настоятельно рекомендуем вам разрабатывать приложение в стиле Compose-first или отказаться от использования Views .

Полностью демо-приложение Compose

Хотя библиотека media3-ui-compose не включает готовые Composables (такие как кнопки, индикаторы, изображения или диалоги), вы можете найти демонстрационное приложение, полностью написанное на Compose, которое избегает любых решений по обеспечению взаимодействия, таких как обёртывание PlayerView в AndroidView . Демонстрационное приложение использует классы-держатели состояний пользовательского интерфейса из модуля media3-ui-compose и использует библиотеку Compose Material3 .

Держатели состояния UI

Чтобы лучше понять, как можно использовать гибкость держателей состояний пользовательского интерфейса по сравнению с компонуемыми элементами, ознакомьтесь с тем, как Compose управляет State .

Держатели состояния кнопки

Для некоторых состояний пользовательского интерфейса мы предполагаем, что они, скорее всего, будут использоваться кнопочными компоновочными элементами.

Состояние помните*государство Тип
PlayPauseButtonState rememberPlayPauseButtonState 2-Переключить
PreviousButtonState rememberPreviousButtonState Постоянный
NextButtonState rememberNextButtonState Постоянный
RepeatButtonState rememberRepeatButtonState 3-Переключатель
ShuffleButtonState rememberShuffleButtonState 2-Переключить
PlaybackSpeedState rememberPlaybackSpeedState Меню или N-переключатель

Пример использования PlayPauseButtonState :

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  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),
    )
  }
}

Обратите внимание, что state не имеет информации о тематизации, например, значка для воспроизведения или паузы. Его единственная обязанность — преобразовать Player в состояние UI.

Затем вы можете смешивать и комбинировать кнопки в макете по своему усмотрению:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

Визуальные держатели выходного состояния

PresentationState хранит информацию о том, когда видеовыход в PlayerSurface может быть отображен или должен быть закрыт элементом-заполнителем пользовательского интерфейса.

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // 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 = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

Здесь мы можем использовать как presentationState.videoSizeDp для масштабирования Surface до желаемого соотношения сторон (см. документацию ContentScale для получения дополнительных типов), так и presentationState.coverSurface для определения времени, когда показывать Surface неподходящее. В этом случае вы можете разместить непрозрачную шторку поверх поверхности, которая исчезнет, ​​когда поверхность будет готова.

Где находятся потоки?

Многие разработчики Android знакомы с использованием объектов Kotlin Flow для сбора постоянно меняющихся данных пользовательского интерфейса. Например, вы можете искать поток Player.isPlaying , который можно collect с учетом жизненного цикла. Или что-то вроде Player.eventsFlow , чтобы предоставить вам Flow<Player.Events> , который можно filter нужным вам способом.

Однако использование потоков для состояния пользовательского интерфейса Player имеет некоторые недостатки. Одной из главных проблем является асинхронная природа передачи данных. Мы хотим обеспечить как можно меньшую задержку между Player.Event и его потреблением на стороне пользовательского интерфейса, избегая отображения элементов пользовательского интерфейса, которые не синхронизированы с Player .

Другие пункты включают в себя:

  • Поток со всеми Player.Events не будет придерживаться единого принципа ответственности, каждому потребителю придется отфильтровывать соответствующие события.
  • Создание потока для каждого Player.Event потребует от вас их объединения (с combine ) для каждого элемента пользовательского интерфейса. Существует сопоставление «многие ко многим» между Player.Event и изменением элемента пользовательского интерфейса. Необходимость использования combine может привести пользовательский интерфейс к потенциально недопустимым состояниям.

Создавайте пользовательские состояния пользовательского интерфейса

Вы можете добавить пользовательские состояния пользовательского интерфейса, если существующие не удовлетворяют вашим потребностям. Проверьте исходный код существующего состояния, чтобы скопировать шаблон. Типичный класс-держатель состояния пользовательского интерфейса делает следующее:

  1. Принимает Player .
  2. Подписывается на Player с помощью сопрограмм. Подробнее см. Player.listen .
  3. Реагирует на определенные Player.Events , обновляя свое внутреннее состояние.
  4. Принимайте команды бизнес-логики, которые будут преобразованы в соответствующее обновление Player .
  5. Может быть создан в нескольких местах дерева пользовательского интерфейса и всегда будет поддерживать единообразное представление состояния игрока.
  6. Предоставляет поля State Compose, которые могут использоваться Composable для динамического реагирования на изменения.
  7. Поставляется с функцией remember*State для запоминания экземпляра между композициями.

Что происходит за кулисами:

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)
      }
    }
}

Чтобы реагировать на собственные Player.Events , вы можете перехватывать их с помощью Player.listen , который представляет собой suspend fun , позволяющее вам войти в мир сопрограмм и бесконечно слушать Player.Events . Реализация Media3 различных состояний пользовательского интерфейса помогает конечному разработчику не беспокоиться об изучении Player.Events .