Erste Schritte mit der Compose-basierten Benutzeroberfläche

Abhängigkeit hinzufügen

Die Media3-Bibliothek enthält ein auf Jetpack Compose basierendes UI-Modul. Fügen Sie dazu die folgende Abhängigkeit hinzu:

Kotlin

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

Groovy

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

Wir empfehlen Ihnen dringend, Ihre App in erster Linie mit Compose zu entwickeln oder von der Verwendung von Views zu migrieren.

Vollständige Compose-Demo-App

Die media3-ui-compose-Bibliothek enthält keine sofort einsatzbereiten Composables wie Schaltflächen, Anzeigen, Bilder oder Dialogfelder. Sie können jedoch eine vollständig in Compose geschriebene Demo-App verwenden, in der keine Interoperabilitätslösungen wie das Einbetten von PlayerView in AndroidView verwendet werden. Die Demo-App verwendet die UI-Status-Holder-Klassen aus dem media3-ui-compose-Modul und die Compose Material3-Bibliothek.

UI-State Holder

Weitere Informationen dazu, wie Sie die Flexibilität von UI-Zustandshaltern im Vergleich zu Composables nutzen können, finden Sie unter Zustand in Compose verwalten.

State Holder für Schaltflächen

Bei einigen UI-Zuständen gehen wir davon aus, dass sie höchstwahrscheinlich von schaltflächenartigen 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 zum Design enthält, z. B. das Symbol zum Abspielen oder Pausieren. Die einzige Aufgabe besteht darin, die Player in den UI-Status zu transformieren.

Sie können die Schaltflächen dann im gewünschten Layout kombinieren:

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

State Holder für die visuelle Ausgabe

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

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 die Oberfläche auf das gewünschte Seitenverhältnis zu skalieren (weitere Typen finden Sie in der ContentScale-Dokumentation), als auch presentationState.coverSurface, um zu wissen, wann der Zeitpunkt für die Anzeige der Oberfläche nicht richtig ist. In diesem Fall können Sie einen nicht transparenten Verschluss über der Oberfläche positionieren, der verschwindet, sobald die Oberfläche bereit ist.

Wo finde ich Flows?

Viele Android-Entwickler sind mit der Verwendung von Kotlin Flow-Objekten zum Erfassen sich ständig ändernder UI-Daten vertraut. Möglicherweise suchen Sie beispielsweise nach einem Player.isPlaying-Ablauf, den Sie collect können, ohne den Lebenszyklus zu berücksichtigen. Oder etwas wie Player.eventsFlow, um Ihnen eine Flow<Player.Events> zu liefern, die Sie nach Belieben filter können.

Die Verwendung von Flows für den Player-UI-Zustand hat jedoch einige Nachteile. Eines der Hauptprobleme ist die asynchrone Datenübertragung. Wir möchten, dass die Latenz zwischen einem Player.Event und seiner Verwendung auf der UI-Seite so gering wie möglich ist. Außerdem möchten wir vermeiden, dass UI-Elemente angezeigt werden, die nicht mit dem Player synchronisiert sind.

Weitere Punkte:

  • Ein Ablauf mit allen Player.Events würde nicht dem Prinzip der einzelnen Verantwortung entsprechen. Jeder Nutzer müsste die relevanten Ereignisse herausfiltern.
  • Wenn Sie einen Flow für jedes Player.Event erstellen, müssen Sie sie für jedes UI-Element mit combine kombinieren. Es gibt eine Many-to-Many-Zuordnung zwischen einem Player.Event und einer Änderung des UI-Elements. Die Verwendung von combine kann dazu führen, dass die Benutzeroberfläche potenziell illegale Zustände erreicht.

Benutzerdefinierte UI-Zustände 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 Klasse für den UI-Status-Holder führt Folgendes aus:

  1. Nimmt einen 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 Befehle für die Geschäftslogik, die in ein entsprechendes Player-Update umgewandelt werden.
  5. Kann an mehreren Stellen im UI-Baum erstellt werden und bietet immer eine konsistente Ansicht des Player-Status.
  6. Macht Compose-State-Felder verfügbar, die von einem Composable verwendet werden können, um dynamisch auf Änderungen zu reagieren.
  7. Enthält eine remember*State-Funktion, mit der sich die Instanz zwischen Kompositionen merken lässt.

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

Um auf eigene Player.Events zu reagieren, können Sie sie mit Player.listen abfangen. Das ist ein suspend fun, mit dem Sie in die Coroutine-Welt eintreten und unbegrenzt auf Player.Events warten können. Die Media3-Implementierung verschiedener UI-Zustände hilft dem Entwickler, sich nicht mit Player.Events auseinandersetzen zu müssen.