Core Compose

Die media3-ui-compose-Bibliothek bietet die grundlegenden Komponenten zum Erstellen einer Media-Benutzeroberfläche in Jetpack Compose. Sie ist für Entwickler gedacht, die mehr Anpassungsmöglichkeiten benötigen, als die media3-ui-compose-material3-Bibliothek bietet. Auf dieser Seite wird beschrieben, wie Sie mit den Kernkomponenten und Status-Holdern eine benutzerdefinierte Benutzeroberfläche für den Media Player erstellen.

Material3- und benutzerdefinierte Compose-Komponenten kombinieren

Die media3-ui-compose-material3-Bibliothek ist flexibel konzipiert. Sie können die vorgefertigten Komponenten für den Großteil Ihrer Benutzeroberfläche verwenden, aber eine einzelne Komponente durch eine benutzerdefinierte Implementierung ersetzen, wenn Sie mehr Kontrolle benötigen. Hier kommt die media3-ui-compose-Bibliothek ins Spiel.

Angenommen, Sie möchten die Standard-PreviousButton und NextButton aus der Material3-Bibliothek verwenden, benötigen aber eine vollständig benutzerdefinierte PlayPauseButton. Dazu können Sie PlayPauseButton aus der media3-ui-compose-Kernbibliothek verwenden und neben den vorgefertigten Komponenten platzieren.

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

Verfügbare Komponenten

Die media3-ui-compose-Bibliothek bietet eine Reihe von vorgefertigten Composables für gängige Player-Steuerelemente. Hier sind einige Komponenten, die Sie direkt in Ihrer App verwenden können:

Komponente Beschreibung
PlayPauseButton Ein Statuscontainer für eine Schaltfläche, mit der zwischen Wiedergabe und Pause umgeschaltet wird.
SeekBackButton Ein Statuscontainer für eine Schaltfläche, mit der um ein definiertes Inkrement zurückgespult wird.
SeekForwardButton Ein Statuscontainer für eine Schaltfläche, mit der um ein definiertes Inkrement vorwärts gesucht wird.
NextButton Ein Statuscontainer für eine Schaltfläche, mit der zum nächsten Media-Element gesucht wird.
PreviousButton Ein Statuscontainer für eine Schaltfläche, mit der zum vorherigen Media-Element gesucht wird.
RepeatButton Ein Statuscontainer für eine Schaltfläche, mit der zwischen den Wiederholungsmodi gewechselt wird.
ShuffleButton Ein Statuscontainer für eine Schaltfläche, mit der der Zufallsmodus aktiviert oder deaktiviert wird.
MuteButton Ein Statuscontainer für eine Schaltfläche, mit der der Player stummgeschaltet und die Stummschaltung aufgehoben werden kann.
TimeText Ein Statuscontainer für ein Composable, in dem der Fortschritt des Spielers angezeigt wird.
ContentFrame Eine Oberfläche zur Darstellung von Medieninhalten, die das Seitenverhältnis, die Größenanpassung und einen Verschluss berücksichtigt
PlayerSurface Die Rohoberfläche, die SurfaceView und TextureView in AndroidView umschließt.

UI-State-Holder

Wenn keine der Gerüstkomponenten Ihren Anforderungen entspricht, können Sie auch die Statusobjekte direkt verwenden. Im Allgemeinen ist es ratsam, die entsprechenden remember-Methoden zu verwenden, um das Aussehen der Benutzeroberfläche zwischen den Neuzusammenstellungen beizubehalten.

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 geht die Bibliothek davon aus, dass sie höchstwahrscheinlich von button-ä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:

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

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. ContentFrame Composable kombiniert die Verarbeitung des Seitenverhältnisses mit der Anzeige des Verschlusses auf einer Oberfläche, die noch nicht bereit ist.

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

Hier können wir sowohl presentationState.videoSizeDp verwenden, um die Oberfläche an das ausgewählte Seitenverhältnis anzupassen (weitere Typen finden Sie in der ContentScale-Dokumentation), als auch presentationState.coverSurface, um zu erkennen, wann der Zeitpunkt für die Anzeige der Oberfläche nicht richtig ist. In diesem Fall können Sie einen nicht transparenten Shutter über der Oberfläche positionieren, der verschwindet, sobald die Oberfläche bereit ist. Mit ContentFrame können Sie den Verschluss als nachgestellte Lambda-Funktion anpassen. Standardmäßig ist er jedoch ein schwarzes @Composable Box, das die Größe des übergeordneten Containers ausfüllt.

Wo finde ich Flows?

Viele Android-Entwickler sind mit der Verwendung von Kotlin-Flow-Objekten zum Erfassen sich ständig ändernder UI-Daten vertraut. Sie suchen 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 geben, 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 die Latenz zwischen einem Player.Event und seiner Nutzung auf der UI-Seite so gering wie möglich halten und 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 Single Responsibility entsprechen. Jeder Consumer 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 in potenziell illegale Zustände gerät.

Benutzerdefinierte UI-Zustände erstellen

Sie können benutzerdefinierte UI-Status 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. Akzeptiert ein Player.
  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. Akzeptiert Befehle für die Geschäftslogik, die in ein entsprechendes Player-Update umgewandelt werden.
  5. Sie können an mehreren Stellen im UI-Baum erstellt werden und bieten 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)
      }
    }
}

Wenn Sie auf Ihre eigenen Player.Events reagieren möchten, können Sie sie mit Player.listen abfangen. Dies ist eine suspend fun, mit der Sie die Coroutine-Welt betreten 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.