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

Библиотека 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 содержит набор готовых компонентов для распространенных элементов управления проигрывателем. Вот некоторые компоненты, которые можно использовать непосредственно в приложении:

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

Как настроить компонуемый элемент Player

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

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

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

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

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

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

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

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

Штат 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. Предоставляет поля Compose State, которые могут быть использованы 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.