Core Compose

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 d'une personnalisation plus poussée que celle proposée par la bibliothèque media3-ui-compose-material3. Cette page explique comment utiliser les composants principaux et les détenteurs d'état pour créer une UI de lecteur multimédia personnalisée.

Combiner des composants Compose Material3 et 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 majeure partie 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à que la bibliothèque media3-ui-compose entre en jeu.

Par exemple, imaginez que vous souhaitez utiliser les PreviousButton et NextButton standards de la bibliothèque Material3, mais que vous avez besoin d'un PlayPauseButton entièrement personnalisé. Pour ce faire, utilisez PlayPauseButton 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 :

Component Description
PlayPauseButton Conteneur d'état pour un bouton qui bascule entre la lecture et la pause.
SeekBackButton Conteneur d'état pour un bouton qui effectue une recherche en arrière par 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 recherche l'élément multimédia suivant.
PreviousButton Conteneur d'état pour un bouton qui recherche 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 ou désactive le mode aléatoire.
MuteButton Conteneur d'état pour un bouton qui active et désactive le son du lecteur.
TimeText Conteneur d'état pour un composable qui affiche la progression du lecteur.
ContentFrame Surface permettant d'afficher du contenu multimédia, qui gère le format, le redimensionnement et un obturateur
PlayerSurface Surface brute qui encapsule SurfaceView et TextureView dans AndroidView.

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 préserver 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 d'UI, la bibliothèque part du principe qu'ils seront très probablement consommés par des Composables de type bouton.

État remember*State Saisie
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Constante
NextButtonState rememberNextButtonState Constante
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menu ou N-Toggle

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

PresentationState contient des informations sur le moment où la sortie vidéo d'un PlayerSurface peut être affichée ou doit être couverte par un élément d'espace réservé de l'UI. Le composable ContentFrame combine la gestion du format avec 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.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()
  }
}

Ici, nous pouvons utiliser à la fois presentationState.videoSizeDp pour mettre à l'échelle la Surface au format choisi (voir la documentation ContentScale pour plus de types) et presentationState.coverSurface pour savoir quand le timing n'est pas bon pour afficher la Surface. Dans ce cas, vous pouvez placer un cache 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'un @Composable Box noir 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 un flux Player.isPlaying que vous pouvez collect en 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 flows pour l'état de l'UI Player présente certains inconvénients. L'une des principales préoccupations concerne la nature asynchrone du transfert de données. Nous souhaitons 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.

Voici d'autres points à retenir :

  • Un flux avec tous les Player.Events ne respecterait pas le principe de responsabilité unique, car chaque consommateur devrait filtrer les événements pertinents.
  • La création d'un flux pour chaque Player.Event vous obligera à les combiner (avec combine) pour chaque élément d'interface utilisateur. Il existe un mappage de type plusieurs à plusieurs entre un Player.Event et une modification d'élément d'UI. L'utilisation de combine peut entraîner des états potentiellement illégaux de l'UI.

Créer des états d'UI personnalisés

Vous pouvez ajouter des états d'interface utilisateur personnalisés si ceux 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 typique effectue les opérations suivantes :

  1. Accepte un Player.
  2. S'abonne à Player à l'aide de coroutines. Pour en savoir plus, consultez Player.listen.
  3. Répond à un Player.Events spécifique en mettant à jour son état interne.
  4. Accepte les commandes de logique métier qui seront transformées en mise à jour Player appropriée.
  5. Ils peuvent être créés à plusieurs endroits de l'arborescence de l'UI et conservent toujours une vue cohérente de l'état du lecteur.
  6. Expose les champs State de Compose qui peuvent être utilisés par un composable pour répondre dynamiquement aux modifications.
  7. Fourni avec une fonction remember*State pour 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(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)
      }
    }
}

Pour réagir à vos propres Player.Events, vous pouvez les intercepter à l'aide de Player.listen, qui est un 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 avoir à se soucier de l'apprentissage de Player.Events.