A biblioteca media3-ui-compose fornece os componentes básicos para
criar uma interface de mídia no Jetpack Compose. Ele foi criado para desenvolvedores que precisam de mais personalização do que a oferecida pela biblioteca media3-ui-compose-material3. Nesta página, explicamos como usar os componentes principais e os titulares de estado para
criar uma interface personalizada de player de mídia.
Como misturar componentes personalizados do Material3 e do Compose
A biblioteca media3-ui-compose-material3 foi projetada para ser flexível. É possível
usar os componentes pré-criados para a maior parte da interface, mas substituir um único componente
por uma implementação personalizada quando precisar de mais controle. É aí que a biblioteca
media3-ui-compose entra em ação.
Por exemplo, imagine que você quer usar os
PreviousButton e NextButton padrão da
biblioteca Material3, mas precisa de um PlayPauseButton completamente personalizado. Para isso, use PlayPauseButton da biblioteca principal media3-ui-compose
e coloque ao lado dos componentes pré-criados.
Row { // Use prebuilt component from the Media3 UI Compose Material3 library PreviousButton(player) // Use the scaffold component from Media3 UI Compose library PlayPauseButton(player) { // `this` is PlayPauseButtonState FilledTonalButton( onClick = { Log.d("PlayPauseButton", "Clicking on play-pause button") this.onClick() }, enabled = this.isEnabled, ) { Icon( imageVector = if (showPlay) Icons.Default.PlayArrow else Icons.Default.Pause, contentDescription = if (showPlay) "Play" else "Pause", ) } } // Use prebuilt component from the Media3 UI Compose Material3 library NextButton(player) }
Componentes disponíveis
A biblioteca media3-ui-compose fornece um conjunto de elementos combináveis pré-criados para
controles comuns do player. Confira alguns componentes que podem ser usados diretamente no
seu app:
| Componente | Descrição |
|---|---|
PlayPauseButton |
Um contêiner de estado para um botão que alterna entre reproduzir e pausar. |
SeekBackButton |
Um contêiner de estado para um botão que busca para trás em um incremento definido. |
SeekForwardButton |
Um contêiner de estado para um botão que avança em um incremento definido. |
NextButton |
Um contêiner de estado para um botão que busca o próximo item de mídia. |
PreviousButton |
Um contêiner de estado para um botão que busca o item de mídia anterior. |
RepeatButton |
Um contêiner de estado para um botão que passa por modos de repetição. |
ShuffleButton |
Um contêiner de estado para um botão que ativa/desativa o modo aleatório. |
MuteButton |
Um contêiner de estado para um botão que ativa e desativa o som do player. |
TimeText |
Um contêiner de estado para um elemento combinável que mostra o progresso do jogador. |
ContentFrame |
Uma superfície para mostrar conteúdo de mídia que lida com gerenciamento de proporção, redimensionamento e um obturador. |
PlayerSurface |
Superfície bruta que une SurfaceView e TextureView em AndroidView. |
Detentores de estado da interface
Se nenhum dos componentes de scaffolding atender às suas necessidades, use os
objetos de estado diretamente. Em geral, é recomendável usar os métodos remember correspondentes para preservar a aparência da interface entre recomposições.
Para entender melhor como usar a flexibilidade dos detentores de estado da UI em vez de Composables, leia sobre como o Compose gerencia o estado.
Detentores de estado de botão
Para alguns estados da interface, a biblioteca pressupõe 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:
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), ) }
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.
O elemento combinável ContentFrame combina o processamento da proporção com o cuidado
de mostrar o obturador em uma superfície que ainda não está pronta.
@Composable fun ContentFrame( player: Player?, modifier: Modifier = Modifier, surfaceType: @SurfaceType Int = SURFACE_TYPE_SURFACE_VIEW, contentScale: ContentScale = ContentScale.Fit, keepContentOnReset: Boolean = false, shutter: @Composable () -> Unit = { Box(Modifier.fillMaxSize().background(Color.Black)) }, ) { val presentationState = rememberPresentationState(player, keepContentOnReset) val scaledModifier = modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp) // 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, scaledModifier, surfaceType) if (presentationState.coverSurface) { // Cover the surface that is being prepared with a shutter shutter() } }
Aqui, podemos usar presentationState.videoSizeDp para dimensionar a superfície de acordo com a proporção escolhida (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. ContentFrame
permite personalizar o obturador como uma lambda final, mas, por padrão, ele será um
@Composable Box preto preenchendo o tamanho do contêiner pai.
Onde ficam os Flows?
Muitos desenvolvedores Android estão acostumados a usar 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 alcançar a menor latência possível entre um Player.Event e o consumo dele no lado da interface, evitando mostrar elementos que estão dessincronizados com o Player.
Outros pontos incluem:
- Um fluxo com todos os
Player.Eventsnã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 usarcombinepode 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 detentora de estado de UI típica faz o seguinte:
- Recebe um
Player. - Inscreve-se no
Playerusando corrotinas. ConsultePlayer.listenpara mais detalhes. - Responde a um
Player.Eventsespecífico atualizando o estado interno. - Aceita comandos de lógica de negócios que serão transformados em uma atualização
Playeradequada. - Pode ser criado em vários lugares na árvore da interface e sempre mantém uma visualização consistente do estado do player.
- Expõe campos
Statedo Compose que podem ser consumidos por um elemento combinável para responder dinamicamente a mudanças. - Vem com uma função
remember*Statepara 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.