Библиотека 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. |
Artwork |
Composable-функция, которая показывает обложку текущего медиаобъекта, например альбома. |
Настройка 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 ) }
Функция Player также показывает субтитры и, если видео нет, обложку текущего медиаобъекта. Чтобы настроить их внешний вид, передайте собственные composable-функции в слоты subtitles и artwork. Чтобы скрыть их, передайте пустую composable-функцию в subtitles и null в artwork.
Хранилища состояния в интерфейсе
Если ни один из компонентов каркаса не соответствует вашим потребностям, вы также можете использовать объекты состояния напрямую. Как правило, рекомендуется использовать соответствующие методы remember, чтобы сохранить внешний вид пользовательского интерфейса между рекомпозициями.
Чтобы лучше понять, как использовать гибкость хранилищ состояния пользовательского интерфейса по сравнению с composable-функциями, прочитайте о том, как Compose управляет состоянием.
Хранилища состояния кнопок
Для некоторых состояний интерфейса библиотека предполагает, что они, скорее всего, будут использоваться composable-функциями, похожими на кнопки.
| Штат | remember*State | Тип |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle или N-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
Константа |
NextButtonState |
rememberNextButtonState |
Константа |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Меню или переключатель N-Toggle |
Помимо состояний воспроизведения и паузы, PlayPauseButtonState предоставляет showBuffering, showReplay и showRetry. Используйте их, чтобы показывать индикатор буферизации, значок повтора после окончания воспроизведения и значок повторной попытки после ошибки воспроизведения. showReplay и showRetry уточняют showPlay: если одно из них равно true, то showPlay также равно true, поэтому проверяйте их перед showPlay. Метод onClick также обрабатывает эти состояния, например подготавливает проигрыватель после ошибки.
Чтобы выбрать, какие из этих состояний показывать, передайте комбинацию SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED и SHOW_RETRY_ON_ERROR в параметр displayMode метода rememberPlayPauseButtonState. По умолчанию displayMode объединяет SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED и SHOW_RETRY_ON_ERROR, что позволяет показывать все три состояния, а состояние буферизации появляется даже во время паузы.
Пример использования 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 предоставляет информацию о текущем состоянии ошибки проигрывателя, которая может использоваться для показа оверлея с ошибкой.
PlaybackState предоставляет доступ к основному состоянию воспроизведения проигрывателя, например playbackState, isPlaying и playerError, независимо от конкретного компонента пользовательского интерфейса. Используйте его для управления оверлеями или пользовательскими макетами, например для отображения индикатора загрузки во время буферизации проигрывателя. Для встроенного индикатора Material3 используйте BufferingIndicator.
PresentationState содержит информацию о том, когда видеовыход в PlayerSurface может быть показан или должен быть закрыт элементом интерфейса плейсхолдера.
ContentFrame composable-функция сочетает в себе обработку соотношения сторон с заботой о показе затвора над поверхностью, которая ещё не готова.
@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, заполняющий размер родительского контейнера.
PresentationState также предоставляет showArtwork, который равен true, когда текущий мультимедийный объект воспроизводится без видеодорожки, например контент только со звуком. В этом случае ContentFrame отображает компонент Artwork, который показывает обложку из MediaMetadata элемента. Чтобы настроить или скрыть обложку, передайте собственную composable-функцию или null в слот artwork функции ContentFrame.
По умолчанию Artwork загружает только данные обложки, встроенные в метаданные, или из URI content, file или android.resource. Чтобы загрузить обложку из удаленных URI или других источников, воспользуйтесь одним из следующих вариантов:
- Передайте
BitmapLoader, напримерDataSourceBitmapLoader, вArtwork. - Используйте перегрузку
Artwork, которая принимаетpainter, чтобы загружать изображения с помощью выбранной вами библиотеки.
В отличие от composable-функции Player, которая по умолчанию отображает субтитры, ContentFrame оставляет слот subtitles пустым. Чтобы отобразить субтитры в ContentFrame или настроить их отображение в Player, отслеживайте текущие подсказки проигрывателя с помощью CuesState и отображайте их в слоте subtitles. ContentFrame размещает этот слот в границах масштабированного видео и рисует его над затвором и обложкой, чтобы субтитры оставались видимыми, когда видео нет.
Отслеживание хранилищ состояния выбора
TrackSelectionState помогает создать меню для одного типа дорожки, например для языка аудио или субтитров. Создайте его с помощью rememberTrackSelectionState, передав тип дорожки и комбинацию фильтров, например TRACK_FILTER_LANGUAGE или TRACK_FILTER_RESOLUTION, которые определяют, какие свойства дорожки отличают варианты. Задайте для hasOffOption или hasAutoOption значение true, чтобы добавить параметр, который отключает тип дорожки или позволяет проигрывателю выбирать дорожки автоматически.
В этом состоянии доступны selectionOptions и selectedOption.
Каждый элемент TrackSelectionOption указывает, поддерживается ли он устройством и выбран ли он. Передача параметра в selectOption соответствующим образом обновляет параметры выбора дорожки проигрывателя. Прежде чем показывать меню, проверьте canSetTrackSelectionParameters и убедитесь, что вариантов достаточно. Например, в меню субтитров ниже предлагается по одному варианту для каждого языка субтитров и вариант отключения субтитров:
@Composable fun SubtitleMenu(player: Player, modifier: Modifier = Modifier) { val state = rememberTrackSelectionState( player, trackType = C.TRACK_TYPE_TEXT, trackFilters = TRACK_FILTER_LANGUAGE, hasOffOption = true, ) // Hide the menu if the player can't change tracks or has no subtitles to choose from if (!state.canSetTrackSelectionParameters || state.selectionOptions.size < 2) return Column(modifier.selectableGroup()) { for (option in state.selectionOptions) { Row( Modifier.fillMaxWidth() .heightIn(min = 48.dp) .selectable( selected = option.isSelected, enabled = option.isSupported, role = Role.RadioButton, onClick = { state.selectOption(option) }, ) .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton(selected = option.isSelected, onClick = null, enabled = option.isSupported) Text( text = if (option.onOffState == TRACK_SELECTION_OFF) "Off" else option.language?.let { Locale.forLanguageTag(it).displayName } ?: "Unknown", color = if (option.isSupported) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.38f), ) } } } }
Если вам нужен прямой доступ к Tracks и TrackSelectionParameters игрока, используйте TrackSelectionParametersState. Подробнее о том, как проигрыватель выбирает дорожки…
Где найти 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. - Может быть создан в нескольких местах дерева пользовательского интерфейса и всегда будет поддерживать согласованное представление состояния проигрывателя.
- Предоставляет поля Compose
State, которые могут использоваться 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.