Добавить зависимость
Библиотека 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
не включает готовые Composables (такие как кнопки, индикаторы, изображения или диалоги), вы можете найти демонстрационное приложение, полностью написанное на 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-переключатель |
Пример использования 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
в состояние UI.
Затем вы можете смешивать и комбинировать кнопки в макете по своему усмотрению:
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
. - Может быть создан в нескольких местах дерева пользовательского интерфейса и всегда будет поддерживать единообразное представление состояния игрока.
- Предоставляет поля
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
.