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

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

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

Котлин

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

Классный

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

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

Демонстрационное приложение Fully Compose

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

Владельцы статуса UI

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

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

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

Состояние помните*Штат Тип
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 в состояние пользовательского интерфейса.

Затем вы можете комбинировать кнопки в соответствии со своими предпочтениями:

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

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

PresentationState содержит информацию о том, когда видеовыход в PlayerSurface может быть показан или должен быть закрыт элементом пользовательского интерфейса-заполнителем. ContentFrame Composable объединяет обработку соотношения сторон с обеспечением отображения затвора над поверхностью, которая еще не готова.

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

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

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

Многие разработчики 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 композиции, которые могут быть использованы компонентом 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 .