A biblioteca media3-ui-compose fornece os componentes básicos para
criar uma interface de mídia no Jetpack Compose. Ela foi projetada 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 detentores de estado para
criar uma interface personalizada de player de mídia.
Como misturar componentes do Material 3 e personalizados 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 trocar um único componente
por uma implementação personalizada quando você precisar de mais controle. É aí que entra a biblioteca media3-ui-compose.
Por exemplo, imagine que você queira usar os elementos padrão
PreviousButton e NextButton da
biblioteca Material3, mas precise de um PlayPauseButton totalmente personalizado. Para isso,
use PlayPauseButton da biblioteca principal media3-ui-compose
e coloque-o 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 oferece um conjunto de elementos combináveis pré-criados para
controles comuns do player. Confira alguns dos 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 retrocede em um incremento definido. |
SeekForwardButton |
Um contêiner de estado para um botão que avança por 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 alterna entre os modos de repetição. |
ShuffleButton |
Um contêiner de estado para um botão que ativa/desativa o modo de ordem aleatória. |
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 exibir conteúdo de mídia que lida com o gerenciamento de proporção, o redimensionamento e um obturador. |
PlayerSurface |
Superfície bruta que envolve SurfaceView e TextureView em AndroidView. |
Personalizar o elemento combinável Player
Ao usar a biblioteca media3-ui-compose-material3, o elemento combinável
Player oferece uma experiência completa de reprodução
de mídia. É possível personalizar o layout fornecendo seus próprios elementos de composição para
os slots de conteúdo ou usar o objeto PlayerDefaults para
personalizar partes específicas da interface, como TopControls, CenterControls,
BottomControls e ErrorOverlay.
A Figura 1 mostra onde o elemento combinável Player coloca os slots. A Figura 2 mostra
a ordem em que as camadas são desenhadas.
Player.
Os controles central e inferior mostram os slots de
PlayerDefaults.CenterControls e
PlayerDefaults.BottomControls. Os slots topControls,
above e below ficam vazios por
padrão.Player,
numeradas na ordem em que são desenhadas.Por exemplo, é possível usar PlayerDefaults.CenterControls para substituir apenas o
botão central de reproduzir/pausar, deixando o restante dos controles centrais intactos.
Também é possível transmitir um elemento combinável totalmente personalizado para um parâmetro como topControls
e omitir completamente outros, como bottomControls, para deixá-los no padrão.
@Composable fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, showControls = showControls, topControls = { // Fully custom top controls Text("My custom title") }, centerControls = { // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = player, central = { // A custom play/pause button Material3PlayPauseButton(player, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
Detentores de estado da IU
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 as recomposições.
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 de 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-Alternar |
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
CurrentMediaItemState fornece informações sobre o MediaItem em reprodução, enquanto PlaylistState expõe informações sobre o MediaItems definido no player. Eles são úteis para exibir metadados de exibição na sua interface personalizada.
ErrorState fornece informações sobre o estado de erro atual do player,
que podem ser usadas para mostrar uma sobreposição de erro.
PresentationState contém informações sobre quando a saída de vídeo em um
PlayerSurface pode ser mostrada ou precisa ser coberta por um elemento da 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.videoAspectRatio) // Always leave PlayerSurface to be part of the Compose tree because it will be initialized 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.videoAspectRatio para dimensionar a Surface
para a proporção escolhida (consulte Escala de conteúdo para mais tipos) e
presentationState.coverSurface para saber quando não é o momento certo para
mostrar a Surface. Nesse caso, é possível posicionar um obturador opaco sobre
a superfície, que vai desaparecer quando ela estiver pronta.
O ContentFrame permite personalizar o obturador como uma lambda final, mas, por
padrão, ele será um @Composable Box preto que preenche o tamanho do contêiner
pai.
Onde estão 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, talvez você esteja 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ê pode filter como quiser.
No entanto, o uso de fluxos para o estado da interface do 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 da interface que não estejam sincronizados com o Player.
Outros pontos incluem:
- Um fluxo com todos os
Player.Eventsnão seguiria um princípio de responsabilidade única, e cada consumidor teria que filtrar os eventos relevantes. - Criar um fluxo para cada
Player.Eventexige que você os combine (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 IU a estados potencialmente ilegais.
Criar estados de interface personalizados
É possível 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 interface típica faz o seguinte:
- Recebe um objeto
Player. - Inscreve-se no
Playerusando corrotinas. VejaPlayer.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 vai manter uma visualização consistente do estado do Player.
- Expõe campos
Statedo Compose que podem ser consumidos por um elemento de composição para responder dinamicamente às 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(COMMAND_ACTION_A)) private set var someFieldValue by mutableStateOf(someFieldDefault) private set fun onClick() { player.actionA() } suspend fun observe(): Nothing = player.listen { events -> if (events.containsAny(EVENT_B_CHANGED, EVENT_C_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) { someFieldValue = this.someField isEnabled = this.isCommandAvailable(COMMAND_ACTION_A) } } }
Para reagir aos seus próprios Player.Events, você pode capturá-los usando Player.listen,
que é um suspend fun que permite que você entre no mundo das corrotinas e
detecte Player.Events indefinidamente. A implementação de vários estados de interface
pela Media3 ajuda o desenvolvedor final a não se preocupar em aprender sobre
Player.Events.