Библиотека 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 показан порядок, в котором отрисовываются слои.
Player.
Элементы управления в центре и внизу показывают слоты PlayerDefaults.CenterControls и PlayerDefaults.BottomControls. По умолчанию слоты topControls, above и below пусты.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может привести к недопустимым состояниям интерфейса.
Создание пользовательских состояний пользовательского интерфейса
Если существующие состояния интерфейса не соответствуют вашим потребностям, вы можете добавить собственные. Посмотрите исходный код существующего состояния, чтобы скопировать шаблон. Типичный класс хранилища состояния пользовательского интерфейса выполняет следующие действия:
- Принимает объект
Player. - Подписывается на
Playerс помощью сопрограмм. ПодробнееPlayer.listen… - Реагирует на определенные
Player.Events, обновляя свое внутреннее состояние. - Принимает команды бизнес-логики, которые будут преобразованы в соответствующее обновление
Player. - Может быть создан в нескольких местах дерева пользовательского интерфейса и всегда будет поддерживать согласованное представление состояния проигрывателя.
- Предоставляет доступ к полям
StateCompose, которые могут использоваться composable-функцией для динамического реагирования на изменения. - Включает функцию
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.