Основная функция создания

Библиотека media3-ui-compose предоставляет базовые компоненты для создания интерфейса медиапроигрывателя в Jetpack Compose. Он предназначен для разработчиков, которым требуется больше возможностей настройки, чем предлагает библиотека media3-ui-compose-material3. На этой странице объясняется, как использовать основные компоненты и держатели состояний для создания пользовательского интерфейса медиаплеера.

Сочетание Material3 и пользовательских компонентов Compose

Библиотека media3-ui-compose-material3 разработана так, чтобы быть гибкой. Вы можете использовать готовые компоненты для большей части интерфейса, но заменять отдельные компоненты на собственные, когда вам нужен больший контроль. Здесь на помощь приходит библиотека media3-ui-compose.

Например, вы хотите использовать стандартные PreviousButton и NextButton из библиотеки Material3, но вам нужен полностью настраиваемый PlayPauseButton. Для этого используйте PlayPauseButton из основной библиотеки media3-ui-compose и разместите его рядом со встроенными компонентами.

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

Доступные компоненты

Библиотека media3-ui-compose содержит набор готовых composable-функций для распространенных элементов управления проигрывателем. Вот некоторые компоненты, которые вы можете использовать непосредственно в своем приложении:

Компонент Описание
PlayPauseButton Контейнер состояния для кнопки, которая переключается между воспроизведением и паузой.
SeekBackButton Контейнер состояния для кнопки, которая перематывает назад на заданный интервал.
SeekForwardButton Контейнер состояния для кнопки, которая перематывает вперед на заданный интервал.
NextButton Контейнер состояния для кнопки, которая переходит к следующему мультимедийному объекту.
PreviousButton Контейнер состояния для кнопки, которая переходит к предыдущему медиаобъекту.
RepeatButton Контейнер состояния для кнопки, которая переключает режимы повтора.
ShuffleButton Контейнер состояния для кнопки, которая включает и отключает воспроизведение в случайном порядке.
MuteButton Контейнер состояния для кнопки, которая включает и отключает звук в проигрывателе.
TimeText Контейнер состояния для composable-функции, которая показывает прогресс проигрывателя.
ContentFrame Поверхность для показа медиаконтента, которая управляет соотношением сторон, изменением размера и затвором.
PlayerSurface Необработанная поверхность, которая оборачивает SurfaceView и TextureView в AndroidView.

Настройте composable-функцию Player

При использовании библиотеки media3-ui-compose-material3 composable-функция Player обеспечивает полноценное воспроизведение медиаконтента. Вы можете настроить макет, добавив собственные composable-функции в слоты контента, или использовать объект PlayerDefaults, чтобы настроить определенные части пользовательского интерфейса, например TopControls, CenterControls, BottomControls и ErrorOverlay.

На рисунке 1 показано, где composable-функция Player размещает свои слоты. На рисунке 2 показан порядок, в котором отрисовываются слои.

Видеопроигрыватель с пунктирными контурами, обозначающими рекламные места. Сверху вниз: topControls вдоль верхнего края, errorOverlay над центром, centerControls в середине с кнопками backSecondary, back, central, forward и forwardSecondary, субтитры внизу и bottomControls вдоль нижнего края со слотами above, left, progressSlider, right и below. ContentFrame заполняет область за ними.
Рисунок 1. Слоты composable-функции Player. Элементы управления в центре и внизу показывают слоты PlayerDefaults.CenterControls и PlayerDefaults.BottomControls. По умолчанию слоты topControls, above и below пусты.
Разнесенный вид шести слоев, расположенных друг над другом. Снизу вверх: 1, PlayerSurface; 2, shutter; 3, artwork; 4, subtitles; 5, errorOverlay; и 6, topControls, centerControls и bottomControls. Слои с первого по четвертый составляют ContentFrame, а все шесть – Player.
Рисунок 2. Слои composable-функции Player, пронумерованные в порядке их отрисовки.

Например, с помощью PlayerDefaults.CenterControls можно переопределить только центральную кнопку воспроизведения/паузы, оставив остальные элементы управления без изменений. Вы также можете передать полностью настраиваемую composable-функцию для параметра, например topControls, и полностью опустить другие, например bottomControls, чтобы оставить их по умолчанию.

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      Text("My custom title")
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

Хранилища состояния в интерфейсе

Если ни один из компонентов каркаса не соответствует вашим потребностям, вы также можете использовать объекты состояния напрямую. Как правило, рекомендуется использовать соответствующие методы remember, чтобы сохранить внешний вид пользовательского интерфейса между рекомпозициями.

Чтобы лучше понять, как использовать гибкость хранилищ состояния пользовательского интерфейса по сравнению с composable-функциями, прочитайте о том, как Compose управляет состоянием.

Хранилища состояния кнопок

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

Штат remember*State Тип
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Константа
NextButtonState rememberNextButtonState Константа
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Меню или переключатель N

Пример использования 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),
  )
}

Хранилища состояния визуального вывода

CurrentMediaItemState предоставляет информацию о воспроизводимом в данный момент MediaItem, а PlaylistState – о MediaItems, установленном в проигрывателе. Это полезно для отображения метаданных в пользовательском интерфейсе.

ErrorState предоставляет информацию о текущем состоянии ошибки проигрывателя, которая может использоваться для отображения оверлея ошибки.

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

@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.videoAspectRatio)

  // Always leave PlayerSurface to be part of the Compose tree because it will be initialized 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.videoAspectRatio, чтобы масштабировать Surface до выбранного соотношения сторон (см. Масштаб контента для других типов), так и presentationState.coverSurface, чтобы узнать, когда не подходит время для показа Surface. В этом случае вы можете разместить поверх поверхности непрозрачную шторку, которая исчезнет, когда поверхность будет готова. ContentFrame позволяет настроить затвор в виде завершающего лямбда-выражения, но по умолчанию это будет черный @Composable Box, заполняющий размер родительского контейнера.

Где найти Flows?

Многие разработчики 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(COMMAND_ACTION_A))
    private set

  var someFieldValue by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe(): Nothing = player.listen { events ->
    if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
      someFieldValue = this.someField
      isEnabled = this.isCommandAvailable(COMMAND_ACTION_A)
    }
  }
}

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