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

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

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

Котлин

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

классный

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

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

Демо-приложение Fully Compose

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

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

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

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

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

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

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. Предоставляет поля Compose 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 .

,

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

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

Котлин

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

классный

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

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

Демо-приложение Fully Compose

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

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

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

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

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

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

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. Предоставляет поля Compose 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 .