Adicionar a dependência
A biblioteca Media3 inclui um módulo de interface baseado no Jetpack Compose. Para usá-lo, adicione a seguinte dependência:
Kotlin
implementation("androidx.media3:media3-ui-compose:1.6.0")
Groovy
implementation "androidx.media3:media3-ui-compose:1.6.0"
Recomendamos que você desenvolva seu app com foco no Compose ou migre de visualizações.
App de demonstração totalmente do 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 desenvolvido no Compose que evita soluções de
interoperabilidade, como o agrupamento de PlayerView
em AndroidView
. O app de demonstração
usa as classes de detentor de estado da IU do módulo media3-ui-compose
e faz
uso da biblioteca Compose Material3.
Detentores de estado da interface
Para entender melhor como usar a flexibilidade dos detentores de estado da interface em comparação com os elementos combináveis, leia sobre como o Compose gerencia o estado.
Detentores de estado do 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-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
Constante |
NextButtonState |
rememberNextButtonState |
Constante |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
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 tema, como o ícone a ser usado para reproduzir
ou pausar. A única responsabilidade dele é transformar o Player
no estado da interface.
Você pode misturar e combinar 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
armazena 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 plataforma para
a proporção desejada (consulte os documentos do ContentScale para mais tipos) e
presentationState.coverSurface
para saber quando o momento não é certo para
mostrar a plataforma. Nesse caso, você pode posicionar uma cortina opaca sobre
a superfície, que vai desaparecer quando a superfície estiver pronta.
Onde estão os fluxos?
Muitos desenvolvedores Android já sabem usar objetos Flow
do Kotlin para coletar
dados de interface em constante mudança. Por exemplo, você pode estar procurando um
fluxo Player.isPlaying
que possa ser collect
de acordo com o ciclo de vida. Ou
algo como Player.eventsFlow
para fornecer um Flow<Player.Events>
que você possa filter
como quiser.
No entanto, o uso de 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 da interface que estão fora de sincronia com o Player
.
Outros pontos incluem:
- Um fluxo com todos os
Player.Events
não adere a um único princípio de responsabilidade. Cada consumidor precisa filtrar os eventos relevantes. - Ao criar um fluxo para cada
Player.Event
, você precisa 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. O uso decombine
pode levar a interface a estados potencialmente ilegais.
Criar estados de interface personalizados
É possível adicionar estados de IU 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 de detentor de estado de interface típica faz o seguinte:
- Recebe um
Player
. - Faz a assinatura do
Player
usando corrotinas. ConsultePlayer.listen
para mais detalhes. - Responde a
Player.Events
específicos atualizando o estado interno. - Aceita 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 mantém uma visualização consistente do estado do jogador.
- Expõe campos
State
do Compose que podem ser consumidos por um elemento combinável para responder dinamicamente às 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 à sua própria Player.Events
, você pode detectá-la usando Player.listen
,
que é um suspend fun
que permite que você entre no mundo da corrotina e
detecte a Player.Events
indefinidamente. A implementação do Media3 de vários estados
de IU ajuda o desenvolvedor final a não se preocupar em aprender sobre
Player.Events
.