Добавить зависимость
Библиотека Media3 включает модуль пользовательского интерфейса на базе Jetpack Compose. Чтобы использовать его, добавьте следующую зависимость:
Котлин
implementation("androidx.media3:media3-ui-compose:1.8.0")
Круто
implementation "androidx.media3:media3-ui-compose:1.8.0"
Мы настоятельно рекомендуем вам разрабатывать свое приложение в стиле Compose-first или отказаться от использования Views .
Полностью демо-приложение Compose
Хотя библиотека media3-ui-compose не включает готовые элементы Composable (такие как кнопки, индикаторы, изображения или диалоговые окна), вы можете найти демонстрационное приложение, полностью написанное на Compose , которое не использует решения для обеспечения взаимодействия, такие как обёртывание PlayerView в AndroidView . Демонстрационное приложение использует классы-держатели состояний пользовательского интерфейса из модуля media3-ui-compose и библиотеку Compose Material3 .
держатели состояния UI
Чтобы лучше понять, как можно использовать гибкость держателей состояний пользовательского интерфейса по сравнению с компонуемыми элементами, ознакомьтесь с тем, как Compose управляет State .
Держатели состояния кнопки
Для некоторых состояний пользовательского интерфейса мы предполагаем, что они, скорее всего, будут использоваться компонуемыми элементами в виде кнопок.
| Состояние | помните*государство | Тип |
|---|---|---|
PlayPauseButtonState | rememberPlayPauseButtonState | 2-Переключить |
PreviousButtonState | rememberPreviousButtonState | Постоянный |
NextButtonState | rememberNextButtonState | Постоянный |
RepeatButtonState | rememberRepeatButtonState | 3-Переключатель |
ShuffleButtonState | rememberShuffleButtonState | 2-Переключить |
PlaybackSpeedState | rememberPlaybackSpeedState | Меню или N-Toggle |
Пример использования PlayPauseButtonState :
@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
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),
)
}
}
Обратите внимание, что state не содержит информации о теме, например, значка для воспроизведения или паузы. Его единственная задача — преобразовать Player в состояние пользовательского интерфейса.
Затем вы можете комбинировать кнопки в макете по своему усмотрению:
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player)
PlayPauseButton(player)
NextButton(player)
}
Визуальные держатели выходных состояний
PresentationState хранит информацию о том, когда видеовыход в PlayerSurface может быть отображен или должен быть перекрыт элементом-заполнителем пользовательского интерфейса.
val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resizeWithContentScale(ContentScale.Fit, presentationState.videoSizeDp)
Box(modifier) {
// 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 = player,
surfaceType = SURFACE_TYPE_SURFACE_VIEW,
modifier = scaledModifier,
)
if (presentationState.coverSurface) {
// Cover the surface that is being prepared with a shutter
Box(Modifier.background(Color.Black))
}
Здесь мы можем использовать как метод presentationState.videoSizeDp для масштабирования Surface до нужного соотношения сторон (см. документацию ContentScale для получения дополнительных типов), так и presentationState.coverSurface для определения момента, когда отображение Surface нежелательно. В этом случае можно разместить поверх Surface непрозрачную шторку, которая исчезнет, когда поверхность будет готова.
Где находятся потоки?
Многие разработчики 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(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 .