Добавьте зависимость
Библиотека Media3 включает модуль пользовательского интерфейса на основе Jetpack Compose. Чтобы использовать его, добавьте следующую зависимость:
Котлин
implementation("androidx.media3:media3-ui-compose:1.6.0")
классный
implementation "androidx.media3:media3-ui-compose:1.6.0"
Мы настоятельно рекомендуем вам разрабатывать свое приложение по принципу Compose или перейти с использования Views .
Демо-приложение Fully Compose
Хотя библиотека media3-ui-compose
не включает в себя готовые составные элементы (такие как кнопки, индикаторы, изображения или диалоговые окна), вы можете найти демонстрационное приложение, полностью написанное на Compose, которое позволяет избежать каких-либо решений по совместимости, таких как упаковка PlayerView
в AndroidView
. Демо-приложение использует классы держателей состояний пользовательского интерфейса из модуля media3-ui-compose
и библиотеку Compose Material3 .
Держатели состояния пользовательского интерфейса
Чтобы лучше понять, как можно использовать гибкость держателей состояний пользовательского интерфейса по сравнению с компонуемыми, прочитайте, как Compose управляет State .
Держатели состояний кнопок
Для некоторых состояний пользовательского интерфейса мы предполагаем, что они, скорее всего, будут использоваться компонентами Composable, похожими на кнопки.
Состояние | запомнить*Состояние | Тип |
---|---|---|
PlayPauseButtonState | rememberPlayPauseButtonState | 2-переключить |
PreviousButtonState | rememberPreviousButtonState | Постоянный |
NextButtonState | rememberNextButtonState | Постоянный |
RepeatButtonState | rememberRepeatButtonState | 3-переключить |
ShuffleButtonState | rememberShuffleButtonState | 2-переключить |
PlaybackSpeedState | rememberPlaybackSpeedState | Меню или N-переключатель |
Пример использования 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 неподходящее. В этом случае сверху поверхности можно расположить непрозрачную шторку, которая исчезнет, когда поверхность будет готова.
Где потоки?
Многие разработчики 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(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
.
Добавьте зависимость
Библиотека Media3 включает модуль пользовательского интерфейса на основе Jetpack Compose. Чтобы использовать его, добавьте следующую зависимость:
Котлин
implementation("androidx.media3:media3-ui-compose:1.6.0")
классный
implementation "androidx.media3:media3-ui-compose:1.6.0"
Мы настоятельно рекомендуем вам разрабатывать свое приложение по принципу Compose или перейти с использования Views .
Демо-приложение Fully Compose
Хотя библиотека media3-ui-compose
не включает в себя готовые составные элементы (такие как кнопки, индикаторы, изображения или диалоговые окна), вы можете найти демонстрационное приложение, полностью написанное на Compose, которое позволяет избежать каких-либо решений по совместимости, таких как упаковка PlayerView
в AndroidView
. Демо-приложение использует классы держателей состояний пользовательского интерфейса из модуля media3-ui-compose
и библиотеку Compose Material3 .
Держатели состояния пользовательского интерфейса
Чтобы лучше понять, как можно использовать гибкость держателей состояний пользовательского интерфейса по сравнению с компонуемыми, прочитайте, как Compose управляет State .
Держатели состояний кнопок
Для некоторых состояний пользовательского интерфейса мы предполагаем, что они, скорее всего, будут использоваться компонентами Composable, похожими на кнопки.
Состояние | запомнить*Состояние | Тип |
---|---|---|
PlayPauseButtonState | rememberPlayPauseButtonState | 2-переключить |
PreviousButtonState | rememberPreviousButtonState | Постоянный |
NextButtonState | rememberNextButtonState | Постоянный |
RepeatButtonState | rememberRepeatButtonState | 3-переключить |
ShuffleButtonState | rememberShuffleButtonState | 2-переключить |
PlaybackSpeedState | rememberPlaybackSpeedState | Меню или N-переключатель |
Пример использования 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 неподходящее. В этом случае сверху поверхности можно расположить непрозрачную шторку, которая исчезнет, когда поверхность будет готова.
Где потоки?
Многие разработчики 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(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
.