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. Você pode
usar os componentes pré-criados para a maior parte da interface, mas trocar um único componente
por uma implementação personalizada quando 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 de composição 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 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 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 conteúdo de mídia que lida com o gerenciamento de proporção, redimensionamento, um obturador, arte e um espaço para legendas. |
PlayerSurface |
Superfície bruta que envolve SurfaceView e TextureView em AndroidView. |
Artwork |
Um elemento de composição que mostra a arte do item de mídia atual, como a capa de um álbum. |
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 combináveis 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 ) }
O elemento combinável Player também mostra legendas e, quando não há vídeo para
mostrar, a arte do item de mídia atual. Para personalizar a aparência, transmita
seus próprios elementos combináveis para os slots subtitles e artwork. Para ocultá-los, transmita
um elemento combinável vazio para subtitles e null para artwork.
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 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 |
Alternância dupla ou múltipla |
PreviousButtonState |
rememberPreviousButtonState |
Constante |
NextButtonState |
rememberNextButtonState |
Constante |
RepeatButtonState |
rememberRepeatButtonState |
3-Alternar |
ShuffleButtonState |
rememberShuffleButtonState |
2-Alternar |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Menu ou N-Toggle |
Além dos estados de reprodução e pausa, o PlayPauseButtonState expõe
showBuffering, showReplay e showRetry. Use-os para mostrar um indicador
de armazenamento em buffer enquanto o player estiver armazenando em buffer, um ícone de repetição após o término da reprodução e
um ícone de nova tentativa após um erro de reprodução. showReplay e showRetry refinam
showPlay: sempre que um deles é true, showPlay também é true. Portanto, verifique-os
antes de showPlay. O método onClick também processa esses estados, por
exemplo, preparando o player novamente após um erro.
Para escolher quais desses estados mostrar, transmita uma combinação de
SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED
e SHOW_RETRY_ON_ERROR para o parâmetro displayMode de
rememberPlayPauseButtonState. Por padrão, displayMode combina
SHOW_BUFFERING_ALWAYS, SHOW_REPLAY_ON_ENDED e SHOW_RETRY_ON_ERROR,
que mostra os três estados, e o estado de armazenamento em buffer aparece mesmo quando
a reprodução está pausada.
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 que está sendo reproduzido,
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.
PlaybackState expõe o estado de reprodução principal do player, como
playbackState, isPlaying e playerError, independentemente de qualquer componente
específico da interface. Use-o para gerar sobreposições ou layouts personalizados, por exemplo, para mostrar um
indicador de carregamento enquanto o player está em buffer. Para um indicador pré-criado do Material 3, use BufferingIndicator.
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.
O PresentationState também expõe showArtwork, que é true quando o item de mídia
atual é reproduzido sem uma faixa de vídeo, como conteúdo somente de áudio. Nesse
caso, ContentFrame mostra o elemento combinável Artwork, que exibe a
arte do MediaMetadata do item. Para personalizar ou ocultar a arte, transmita
seu próprio elemento combinável ou null para o slot artwork de ContentFrame.
Por padrão, o Artwork só carrega dados de arte incorporados nos metadados ou de
um URI content, file ou android.resource. Para carregar artes de URIs remotos
ou de outras fontes, use uma das seguintes opções:
- Transmita um
BitmapLoader, comoDataSourceBitmapLoader, paraArtwork. - Use a sobrecarga
Artworkque usa umpainterpara carregar imagens com a biblioteca de sua escolha.
Ao contrário do elemento combinável Player, que renderiza legendas por padrão,
ContentFrame deixa o slot subtitles vazio. Para renderizar legendas em um
ContentFrame ou personalizar a renderização de legendas em Player, observe as
indicações atuais do player com CuesState e renderize-as no slot
subtitles. O ContentFrame define esse slot dentro dos limites do vídeo dimensionado
e o desenha acima do obturador e da arte, para que as legendas permaneçam visíveis
quando não houver vídeo.
Detentores de estado de seleção de faixas
TrackSelectionState ajuda você a criar um menu para um único tipo de faixa, como
um menu de idioma de áudio ou de legendas. Crie-o com
rememberTrackSelectionState, transmitindo o tipo de faixa e uma combinação de
filtros, como TRACK_FILTER_LANGUAGE ou TRACK_FILTER_RESOLUTION, que
determinam quais propriedades de faixa distinguem as opções. Defina hasOffOption ou
hasAutoOption como true para adicionar uma opção que desativa o tipo de faixa ou uma
que permite que o player escolha as faixas automaticamente.
O estado expõe o selectionOptions e o selectedOption disponíveis.
Cada TrackSelectionOption indica se o dispositivo é compatível com o recurso e se
ele está selecionado. Transmitir uma opção para selectOption atualiza os parâmetros de seleção de faixa do player de acordo. Antes de mostrar o menu, verifique
canSetTrackSelectionParameters e se há opções suficientes para
escolher. Por exemplo, o menu de legendas a seguir oferece uma opção para cada
idioma de legenda e uma opção para desativar as legendas:
@Composable fun SubtitleMenu(player: Player, modifier: Modifier = Modifier) { val state = rememberTrackSelectionState( player, trackType = C.TRACK_TYPE_TEXT, trackFilters = TRACK_FILTER_LANGUAGE, hasOffOption = true, ) // Hide the menu if the player can't change tracks or has no subtitles to choose from if (!state.canSetTrackSelectionParameters || state.selectionOptions.size < 2) return Column(modifier.selectableGroup()) { for (option in state.selectionOptions) { Row( Modifier.fillMaxWidth() .heightIn(min = 48.dp) .selectable( selected = option.isSelected, enabled = option.isSupported, role = Role.RadioButton, onClick = { state.selectOption(option) }, ) .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { RadioButton(selected = option.isSelected, onClick = null, enabled = option.isSupported) Text( text = if (option.onOffState == TRACK_SELECTION_OFF) "Off" else option.language?.let { Locale.forLanguageTag(it).displayName } ?: "Unknown", color = if (option.isSupported) Color.Unspecified else LocalContentColor.current.copy(alpha = 0.38f), ) } } } }
Se você precisar de acesso direto ao Tracks e ao
TrackSelectionParameters do player, use TrackSelectionParametersState. Para
mais informações sobre como o player seleciona faixas, consulte Seleção de faixas.
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. O uso decombinepode 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 da interface típica faz o seguinte:
- Usa 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 os 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.