Начало работы с пользовательским интерфейсом на основе 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 не включает готовые элементы Composable (такие как кнопки, индикаторы, изображения или диалоговые окна), вы можете найти демонстрационное приложение, полностью написанное на 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-Toggle

Пример использования 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 может быть отображен или должен быть перекрыт элементом-заполнителем пользовательского интерфейса.

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 нежелательно. В этом случае можно разместить поверх 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 .