Добавить зависимость
Библиотека Media3 включает модуль пользовательского интерфейса на базе Jetpack Compose. Чтобы использовать его, добавьте следующую зависимость:
Котлин
implementation("androidx.media3:media3-ui-compose:1.7.1")
Круто
implementation "androidx.media3:media3-ui-compose:1.7.1"
Мы настоятельно рекомендуем вам разрабатывать свое приложение в стиле 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.resize(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
. - Может быть создан в нескольких местах дерева пользовательского интерфейса и всегда будет поддерживать единообразное представление состояния игрока.
- Предоставляет поля
State
Compose, которые могут использоваться 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
.