La bibliothèque media3-ui-compose fournit les composants de base pour créer une UI multimédia dans Jetpack Compose. Elle est conçue pour les développeurs qui ont besoin de plus de personnalisation que ce que propose la bibliothèque media3-ui-compose-material3. Cette page explique comment utiliser les composants principaux et les conteneurs d'état pour créer une UI de lecteur multimédia personnalisée.
Combiner des composants Material3 et Compose personnalisés
La bibliothèque media3-ui-compose-material3 est conçue pour être flexible. Vous pouvez utiliser les composants prédéfinis pour la plupart de votre UI, mais remplacer un seul composant par une implémentation personnalisée lorsque vous avez besoin de plus de contrôle. C'est là qu'intervient la bibliothèque media3-ui-compose.
Par exemple, imaginez que vous souhaitez utiliser les boutons standards
PreviousButton et NextButton de la bibliothèque
Material3, mais que vous avez besoin d'un bouton PlayPauseButton entièrement personnalisé. Pour ce faire, utilisez PlayPauseButton à partir de la bibliothèque media3-ui-compose
principale et placez-le à côté des composants prédéfinis.
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) }
Composants disponibles
La bibliothèque media3-ui-compose fournit un ensemble de composables prédéfinis pour les commandes de lecteur courantes. Voici quelques-uns des composants que vous pouvez utiliser directement dans votre application :
| Composant | Description |
|---|---|
PlayPauseButton |
Conteneur d'état pour un bouton qui bascule entre lecture et pause. |
SeekBackButton |
Conteneur d'état pour un bouton qui recule d'un incrément défini. |
SeekForwardButton |
Conteneur d'état pour un bouton qui avance d'un incrément défini. |
NextButton |
Conteneur d'état pour un bouton qui passe à l'élément multimédia suivant. |
PreviousButton |
Conteneur d'état pour un bouton qui passe à l'élément multimédia précédent. |
RepeatButton |
Conteneur d'état pour un bouton qui parcourt les modes de répétition. |
ShuffleButton |
Conteneur d'état pour un bouton qui active/désactive le mode aléatoire. |
MuteButton |
Conteneur d'état pour un bouton qui coupe et réactive le son du lecteur. |
TimeText |
Conteneur d'état pour un composable qui affiche la progression du lecteur. |
ContentFrame |
Surface pour afficher du contenu multimédia qui gère la gestion du format d'image, le redimensionnement et un obturateur |
PlayerSurface |
Surface brute qui encapsule SurfaceView et TextureView dans AndroidView. |
Personnaliser le composable Player
Lorsque vous utilisez la bibliothèque media3-ui-compose-material3, le
Player composable offre une expérience de lecture multimédia
complète. Vous pouvez personnaliser sa mise en page en fournissant vos propres composables à
ses emplacements de contenu, ou vous pouvez utiliser l'objet PlayerDefaults pour
personnaliser des parties spécifiques de l'UI, telles que TopControls, CenterControls,
BottomControls, et ErrorOverlay.
Par exemple, vous pouvez utiliser PlayerDefaults.CenterControls pour remplacer uniquement le bouton central de lecture/pause tout en laissant intact le reste des commandes centrales.
Vous pouvez également transmettre un composable entièrement personnalisé pour un paramètre tel que topControls, et omettre complètement d'autres paramètres tels que bottomControls pour les laisser à leur valeur par défaut.
@Composable fun CustomPlayerSlots(player: Player, modifier: Modifier = Modifier) { Player( player = player, modifier = modifier, topControls = { p, visible -> // Fully custom top controls AnimatedVisibility(visible) { Text("My custom title") } }, centerControls = { p, visible -> // Use default CenterControls but override the central button PlayerDefaults.CenterControls( player = p, visible = visible, central = { // A custom play/pause button Material3PlayPauseButton(it, modifier = Modifier.size(64.dp)) }, ) }, // bottomControls are left as default ) }
Conteneurs d'état de l'UI
Si aucun des composants d'échafaudage ne répond à vos besoins, vous pouvez également utiliser directement les objets d'état. Il est généralement conseillé d'utiliser les méthodes remember correspondantes pour conserver l'apparence de votre UI entre les recompositions.
Pour mieux comprendre comment utiliser la flexibilité des conteneurs d'état de l'UI par rapport aux composables, découvrez comment Compose gère l'état.
Conteneurs d'état des boutons
Pour certains états de l'UI, la bibliothèque suppose qu'ils seront très probablement utilisés par des composables de type bouton.
| État | remember*State | Type |
|---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2 états |
PreviousButtonState |
rememberPreviousButtonState |
Constante |
NextButtonState |
rememberNextButtonState |
Constante |
RepeatButtonState |
rememberRepeatButtonState |
3 états |
ShuffleButtonState |
rememberShuffleButtonState |
2 états |
PlaybackSpeedState |
rememberPlaybackSpeedState |
Menu ou N états |
Exemple d'utilisation 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), ) }
Conteneurs d'état de sortie visuelle
CurrentMediaItemState fournit des informations sur le
MediaItem en cours de lecture, tandis que PlaylistState expose des informations sur l'ensemble MediaItems
défini sur le lecteur. Ces éléments sont utiles pour afficher des métadonnées dans votre UI personnalisée.
ErrorState fournit des informations sur l'état d'erreur actuel du lecteur, qui peuvent être utilisées pour afficher une superposition d'erreur.
PresentationState contient des informations indiquant quand la sortie vidéo d'un
PlayerSurface peut être affichée ou doit être couverte par un élément d'UI d'espace réservé.
ContentFrame composable combine la gestion du format d'image et l'affichage de l'obturateur sur une surface qui n'est pas encore prête.
@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() } }
Ici, nous pouvons utiliser à la fois presentationState.videoAspectRatio pour mettre à l'échelle la surface
au format d'image choisi (voir Mise à l'échelle du contenu pour plus de types) et
presentationState.coverSurface pour savoir quand le moment n'est pas approprié pour
afficher la surface. Dans ce cas, vous pouvez placer un obturateur opaque sur la surface, qui disparaîtra lorsque la surface sera prête.
ContentFrame vous permet de personnaliser l'obturateur en tant que lambda de fin, mais par défaut, il s'agit d'une @Composable Box noire qui remplit la taille du conteneur parent.
Où se trouvent les flux ?
De nombreux développeurs Android connaissent l'utilisation des objets Flow Kotlin pour collecter des données d'UI en constante évolution. Par exemple, vous pouvez rechercher le flux Player.isPlaying que vous pouvez collect d'une manière tenant compte du cycle de vie. Ou
quelque chose comme Player.eventsFlow pour vous fournir un Flow<Player.Events>
que vous pouvez filter comme vous le souhaitez.
Cependant, l'utilisation de flux pour l'état de l'UI Player présente certains inconvénients. L'une des principales préoccupations est la nature asynchrone du transfert de données. Nous voulons obtenir la
latence la plus faible possible entre un Player.Event et sa consommation côté
UI, en évitant d'afficher des éléments d'UI qui ne sont pas synchronisés avec le Player.
Autres points :
- Un flux avec tous les
Player.Eventsne respecterait pas un principe de responsabilité unique, chaque consommateur devrait filtrer les événements pertinents. - La création d'un flux pour chaque
Player.Eventvous obligera à les combiner (aveccombine) pour chaque élément d'UI. Il existe un mappage de plusieurs à plusieurs entre un Player.Event et une modification d'élément d'UI. L'utilisation decombinepeut entraîner des états potentiellement illégaux pour l'UI.
Créer des états d'UI personnalisés
Vous pouvez ajouter des états d'UI personnalisés si les états existants ne répondent pas à vos besoins. Consultez le code source de l'état existant pour copier le modèle. Une classe de conteneur d'état d'UI standard effectue les opérations suivantes :
- Prend un
Playerobjet. - S'abonne au
Playerà l'aide de coroutines. Pour en savoir plus, consultezPlayer.listenpour plus de détails. - Répond à des
Player.Eventsspécifiques en mettant à jour son état interne. - Accepte les commandes de logique métier qui seront transformées en une mise à jour
Playerappropriée. - Peut être créé à plusieurs endroits de l'arborescence de l'UI et conserve toujours une vue cohérente de l'état du lecteur.
- Expose les champs
StateCompose qui peuvent être utilisés par un composable pour répondre dynamiquement aux modifications. - Est fourni avec une fonction
remember*Statepour mémoriser l'instance entre les compositions.
Que se passe-t-il en arrière-plan ?
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) } } }
Pour réagir à vos propres Player.Events, vous pouvez les intercepter à l'aide de Player.listen
qui est une suspend fun vous permettant d'entrer dans le monde des coroutines et
d'écouter indéfiniment les Player.Events. L'implémentation Media3 de différents états d'UI permet au développeur final de ne pas se soucier de l'apprentissage des Player.Events.