Библиотека 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 | Контейнер состояния для составного объекта, отображающий прогресс игрока. |
ContentFrame | Поверхность для отображения медиаконтента, которая управляет соотношением сторон, изменением размера и имеет затвор. |
PlayerSurface | Исходная поверхность, которая оборачивает SurfaceView и TextureView в AndroidView . |
Владельцы статуса UI
Если ни один из вспомогательных компонентов не соответствует вашим потребностям, вы также можете использовать объекты состояния напрямую. Как правило, рекомендуется использовать соответствующие методы remember , чтобы сохранить внешний вид пользовательского интерфейса между перекомпозициями.
Чтобы лучше понять, как можно использовать гибкость держателей состояния пользовательского интерфейса по сравнению с компонентами Composable, ознакомьтесь с тем, как Compose управляет состоянием .
Держатели состояния кнопок
Для некоторых состояний пользовательского интерфейса библиотека исходит из предположения, что они, скорее всего, будут использоваться компонентами, подобными кнопкам.
| Состояние | помните*Штат | Тип |
|---|---|---|
PlayPauseButtonState | rememberPlayPauseButtonState | 2-Переключатель |
PreviousButtonState | rememberPreviousButtonState | Постоянный |
NextButtonState | rememberNextButtonState | Постоянный |
RepeatButtonState | rememberRepeatButtonState | 3-Переключатель |
ShuffleButtonState | rememberShuffleButtonState | 2-Переключатель |
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), ) }
Визуальные держатели состояний вывода
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.videoSizeDp) // 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, scaledModifier, surfaceType) if (presentationState.coverSurface) { // Cover the surface that is being prepared with a shutter shutter() } }
Здесь мы можем использовать как presentationState.videoSizeDp для масштабирования Surface до выбранного соотношения сторон (см. документацию ContentScale для получения информации о других типах), так и presentationState.coverSurface , чтобы определить, когда неподходящее время для отображения Surface. В этом случае вы можете разместить непрозрачную завесу поверх Surface, которая исчезнет, когда Surface будет готов. ContentFrame позволяет настроить завесу как лямбда-функцию в конце, но по умолчанию это будет черный @Composable Box заполняющий размер родительского контейнера.
Где находятся потоки?
Многие разработчики 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. - Может быть создан в нескольких местах дерева пользовательского интерфейса и всегда будет поддерживать согласованное представление состояния игрока.
- Предоставляет доступ к полям
Stateкомпозиции, которые могут быть использованы компонентом Composable для динамического реагирования на изменения. - Включает функцию
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 .