Erste Schritte mit der Compose-basierten Benutzeroberfläche

Abhängigkeit hinzufügen

Die Media3-Bibliothek enthält ein Jetpack Compose-basiertes UI-Modul. Wenn Sie sie verwenden möchten, fügen Sie die folgende Abhängigkeit hinzu:

Kotlin

implementation("androidx.media3:media3-ui-compose:1.6.0")

Groovy

implementation "androidx.media3:media3-ui-compose:1.6.0"

Wir empfehlen Ihnen dringend, Ihre App mit Compose als Hauptelement zu entwickeln oder von der Verwendung von Ansichten zu migrieren.

Demo-App für Fully Compose

Die media3-ui-compose-Bibliothek enthält zwar keine sofort einsatzbereiten Composables (z. B. Schaltflächen, Indikatoren, Bilder oder Dialoge), aber es gibt eine Demo-App, die vollständig in Compose geschrieben wurde, bei der keine Interoperabilitätslösungen wie das Einbetten von PlayerView in AndroidView erforderlich sind. Die Demo-App verwendet die UI-Statushalterklassen aus dem media3-ui-compose-Modul und die Compose Material3-Bibliothek.

UI-Statushalter

Wenn Sie mehr darüber erfahren möchten, wie Sie die Flexibilität von UI-Statushaltern im Vergleich zu Composeables nutzen können, lesen Sie den Artikel Status in Compose verwalten.

State Holder für Schaltflächen

Bei einigen UI-Status gehen wir davon aus, dass sie höchstwahrscheinlich von Schaltflächen-ähnlichen Composables verwendet werden.

Bundesland remember*State Eingeben
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Konstante
NextButtonState rememberNextButtonState Konstante
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menü oder N-Toggle

Beispiel für die Verwendung von PlayPauseButtonState:

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  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),
    )
  }
}

Beachten Sie, dass state keine Informationen zur Themengestaltung enthält, z. B. ein Symbol für die Wiedergabe oder Pausierung. Seine einzige Aufgabe besteht darin, die Player in den UI-Status umzuwandeln.

Sie können die Schaltflächen dann nach Belieben im Layout platzieren:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

State Holder für visuelle Ausgabe

PresentationState enthält Informationen dazu, wann die Videoausgabe in einem PlayerSurface angezeigt werden kann oder von einem Platzhalter-UI-Element abgedeckt werden soll.

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // 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 = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

Hier können wir sowohl presentationState.videoSizeDp verwenden, um das Surface auf das gewünschte Seitenverhältnis zu skalieren (weitere Typen finden Sie in der ContentScale-Dokumentation), als auch presentationState.coverSurface, um zu erfahren, wann das Timing nicht richtig ist, um das Surface anzuzeigen. In diesem Fall können Sie einen undurchsichtigen Schiebevorhang auf der Oberfläche platzieren, der verschwindet, wenn die Oberfläche bereit ist.

Wo finde ich Abläufe?

Viele Android-Entwickler sind mit der Verwendung von Kotlin-Flow-Objekten zum Erfassen sich ständig ändernder UI-Daten vertraut. Angenommen, Sie suchen nach einem Player.isPlaying-Vorgang, den Sie collect schleifenbasiert ausführen können. Oder etwas wie Player.eventsFlow, um Ihnen eine Flow<Player.Events> zu geben, die Sie nach Belieben filter können.

Die Verwendung von Aufrufabfolgen für den UI-Status von Player hat jedoch einige Nachteile. Eines der Hauptprobleme ist die asynchrone Natur der Datenübertragung. Wir möchten für möglichst wenig Latenz zwischen einem Player.Event und seiner Verwendung auf der UI-Seite sorgen, um die Anzeige von UI-Elementen zu vermeiden, die nicht mit dem Player synchronisiert sind.

Weitere Punkte:

  • Ein Fluss mit allen Player.Events würde nicht dem Prinzip der einzelnen Verantwortung entsprechen, da jeder Nutzer die relevanten Ereignisse herausfiltern müsste.
  • Wenn Sie einen Ablauf für jede Player.Event erstellen möchten, müssen Sie sie für jedes UI-Element kombinieren (mit combine). Es gibt eine Mehrfachzuordnung zwischen einem Player.Event und einer Änderung des UI-Elements. Wenn combine verwendet werden muss, kann dies zu potenziell illegalen Zuständen der Benutzeroberfläche führen.

Benutzerdefinierte UI-Status erstellen

Sie können benutzerdefinierte UI-Zustände hinzufügen, wenn die vorhandenen nicht Ihren Anforderungen entsprechen. Sehen Sie sich den Quellcode des vorhandenen Status an, um das Muster zu kopieren. Eine typische UI-Statushalterklasse führt Folgendes aus:

  1. Nimmt eine Player entgegen.
  2. Abonniert Player mithilfe von Koroutinen. Weitere Informationen finden Sie unter Player.listen.
  3. Reagiert auf bestimmte Player.Events, indem der interne Status aktualisiert wird.
  4. Akzeptieren Sie Geschäftslogikbefehle, die in eine entsprechende Player-Aktualisierung umgewandelt werden.
  5. Kann an mehreren Stellen im UI-Baum erstellt werden und bietet immer eine konsistente Ansicht des Spielerstatus.
  6. Stellt Compose State-Felder bereit, die von einem Composable verwendet werden können, um dynamisch auf Änderungen zu reagieren.
  7. Bietet eine remember*State-Funktion, um die Instanz zwischen Kompositionen zu speichern.

Was passiert hinter den Kulissen:

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)
      }
    }
}

Wenn Sie auf Ihre eigenen Player.Events reagieren möchten, können Sie sie mit Player.listen abfangen. Das ist eine suspend fun, mit der Sie die Welt der coroutines betreten und unbegrenzt auf Player.Events hören können. Die Implementierung verschiedener UI-Zustände in Media3 hilft dem Endentwickler, sich nicht mit Player.Events befassen zu müssen.