Adicionar a dependência
A biblioteca Media3 inclui um módulo de interface baseado no Jetpack Compose. Para usar, adicione a seguinte dependência:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.7.1")
Groovy
implementation "androidx.media3:media3-ui-compose:1.7.1"
Recomendamos que você desenvolva seu app com o Compose em primeiro lugar ou migre do uso de Views.
App de demonstração totalmente no Compose
Embora a biblioteca media3-ui-compose
não inclua elementos combináveis prontos para uso (como botões, indicadores, imagens ou caixas de diálogo), você pode encontrar um app de demonstração totalmente escrito em Compose que evita soluções de interoperabilidade, como o encapsulamento de PlayerView
em AndroidView
. O app de demonstração
usa as classes de suporte de estado da interface do módulo media3-ui-compose
e a
biblioteca Compose Material3.
Detentores de estado da interface
Para entender melhor como usar a flexibilidade dos detentores de estado da interface em vez de elementos combináveis, leia sobre como o Compose gerencia o estado.
Detentores de estado de botão
Para alguns estados da interface, presumimos que eles provavelmente serão consumidos por elementos combináveis semelhantes a botões.
Estado | remember*State | Tipo |
---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Alternar |
PreviousButtonState |
rememberPreviousButtonState |
Constante |
NextButtonState |
rememberNextButtonState |
Constante |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Alternar |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Menu ou N-Toggle |
Exemplo de uso de 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),
)
}
}
Observe como state
não tem informações de temas, como ícones para tocar
ou pausar. A única responsabilidade dele é transformar o Player
em estado da interface.
Em seguida, combine os botões no layout de sua preferência:
Row(
modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
) {
PreviousButton(player)
PlayPauseButton(player)
NextButton(player)
}
Detentores de estado de saída visual
PresentationState
contém informações sobre quando a saída de vídeo em um
PlayerSurface
pode ser mostrada ou deve ser coberta por um elemento de interface de marcador de posição.
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))
}
Aqui, podemos usar presentationState.videoSizeDp
para dimensionar a superfície até a proporção desejada (consulte os documentos do ContentScale para mais tipos) e presentationState.coverSurface
para saber quando não é o momento certo de mostrar a superfície. Nesse caso, posicione um obturador opaco em cima da superfície, que vai desaparecer quando ela estiver pronta.
Onde ficam os Flows?
Muitos desenvolvedores Android estão familiarizados com o uso de objetos Flow
do Kotlin para coletar
dados de interface em constante mudança. Por exemplo, você pode procurar um fluxo Player.isPlaying
que pode ser collect
de maneira compatível com o ciclo de vida. Ou
algo como Player.eventsFlow
para oferecer um Flow<Player.Events>
que você pode filter
do jeito que quiser.
No entanto, usar fluxos para o estado da interface Player
tem algumas desvantagens. Uma das principais preocupações é a natureza assíncrona da transferência de dados. Queremos garantir a menor latência possível entre um Player.Event
e o consumo dele no lado da interface, evitando mostrar elementos que estejam dessincronizados com o Player
.
Outros pontos incluem:
- Um fluxo com todos os
Player.Events
não obedeceria a um único princípio de responsabilidade, e cada consumidor teria que filtrar os eventos relevantes. - Para criar um fluxo para cada
Player.Event
, é necessário combiná-los (comcombine
) para cada elemento da interface. Há um mapeamento de muitos para muitos entre um Player.Event e uma mudança de elemento da interface. Ter que usarcombine
pode levar a interface a estados potencialmente ilegais.
Criar estados de UI personalizados
Você pode adicionar estados de interface personalizados se os atuais não atenderem às suas necessidades. Confira o código-fonte do estado atual para copiar o padrão. Uma classe típica de detentor de estado da UI faz o seguinte:
- Recebe um
Player
. - Inscreve-se no
Player
usando corrotinas. ConsultePlayer.listen
para mais detalhes. - Responde a um
Player.Events
específico atualizando o estado interno. - Aceitar comandos de lógica de negócios que serão transformados em uma atualização
Player
adequada. - Pode ser criado em vários lugares na árvore da interface e sempre manterá uma visualização consistente do estado do player.
- Expõe campos
State
do Compose que podem ser consumidos por um elemento combinável para responder dinamicamente a mudanças. - Vem com uma função
remember*State
para lembrar a instância entre composições.
O que acontece nos bastidores:
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)
}
}
}
Para reagir aos seus próprios Player.Events
, capture-os usando Player.listen
, que é um suspend fun
que permite entrar no mundo das corrotinas e detectar Player.Events
indefinidamente. A implementação do Media3 de vários estados da interface
ajuda o desenvolvedor final a não se preocupar em aprender sobre
Player.Events
.