Core Compose

Die media3-ui-compose-Bibliothek bietet die grundlegenden Komponenten zum Erstellen einer Medien-UI 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 erläutert, wie Sie die Kernkomponenten und State Holder verwenden, um eine benutzerdefinierte Media Player-UI zu erstellen.

Material 3- und benutzerdefinierte Compose-Komponenten kombinieren

Die media3-ui-compose-material3-Bibliothek ist flexibel einsetzbar. Sie können die vordefinierten 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 Core-Bibliothek media3-ui-compose verwenden und neben den vordefinierten 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 vordefinierter Composables für gängige Steuerelemente des Videoplayers. Hier sind einige der 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 gewechselt wird.
SeekBackButton Ein Statuscontainer für eine Schaltfläche, mit der um einen definierten Wert zurückgesprungen wird.
SeekForwardButton Ein Statuscontainer für eine Schaltfläche, mit der um einen definierten Wert vorgespult wird.
NextButton Ein Statuscontainer für eine Schaltfläche, mit der zur nächsten Mediendatei gesprungen wird.
PreviousButton Ein Statuscontainer für eine Schaltfläche, mit der zur vorherigen Mediendatei gesprungen wird.
RepeatButton Ein Statuscontainer für eine Schaltfläche, mit der zwischen Wiederholungsmodi gewechselt werden kann.
ShuffleButton Ein Statuscontainer für eine Schaltfläche, mit der der Zufallsmodus ein- und ausgeschaltet wird.
MuteButton Ein Statuscontainer für eine Schaltfläche, mit der der Player stummgeschaltet und die Stummschaltung aufgehoben werden kann.
TimeText Ein Zustandscontainer für eine komponierbare Funktion, die den Fortschritt des Players anzeigt.
ContentFrame Eine Oberfläche zum Anzeigen von Medieninhalten, die das Seitenverhältnis, die Größenanpassung und einen Shutter verwaltet
PlayerSurface Rohfläche, die SurfaceView und TextureView in AndroidView umschließt.

Das Compose-Element Player anpassen

Wenn du die media3-ui-compose-material3-Bibliothek verwendest, bietet die Player-Komposition eine vollständige Medienwiedergabe. Sie können das Layout anpassen, indem Sie eigene Composables für die Inhaltsslots bereitstellen, oder Sie können das Objekt PlayerDefaults verwenden, um bestimmte Teile der Benutzeroberfläche anzupassen, z. B. TopControls, CenterControls, BottomControls und ErrorOverlay.

Abbildung 1 zeigt, wo die Player-komponierbare Funktion ihre Slots platziert. Abbildung 2 zeigt die Reihenfolge, in der die Ebenen gezeichnet werden.

Ein Videoplayer mit gestrichelten Umrissen, die die Anzeigenflächen markieren. Von oben nach unten: „topControls“ am oberen Rand, „errorOverlay“ über der Mitte, „centerControls“ in der Mitte mit den Schaltflächen „backSecondary“, „back“, „central“, „forward“ und „forwardSecondary“, Untertitel am unteren Rand und „bottomControls“ am unteren Rand mit den Slots „above“, „left“, „progressSlider“, „right“ und „below“. ContentFrame füllt den Bereich dahinter aus.
Abbildung 1. Die Slots des Player-Kompositums. Die mittleren und unteren Steuerelemente zeigen die Slots von PlayerDefaults.CenterControls und PlayerDefaults.BottomControls. Die Slots topControls, above und below sind standardmäßig leer.
Eine Explosionszeichnung von sechs übereinander liegenden Schichten. Von unten nach oben: 1, PlayerSurface; 2, Shutter; 3, Artwork; 4, Untertitel; 5, ErrorOverlay; und 6, TopControls, CenterControls und BottomControls. Die Ebenen 1 bis 4 bilden den ContentFrame, alle sechs Ebenen den Player.
Abbildung 2. Die Ebenen der zusammensetzbaren Funktion Player, nummeriert in der Reihenfolge, in der sie gezeichnet werden.

Mit PlayerDefaults.CenterControls kannst du beispielsweise nur die zentrale Wiedergabe-/Pausetaste überschreiben, während die restlichen Steuerelemente in der Mitte unverändert bleiben. Sie können auch ein vollständig benutzerdefiniertes Composable für einen Parameter wie topControls übergeben und andere wie bottomControls vollständig weglassen, um sie auf dem Standardwert zu belassen.

@Composable
fun CustomPlayerSlots(player: Player, showControls: Boolean, modifier: Modifier = Modifier) {
  Player(
    player = player,
    modifier = modifier,
    showControls = showControls,
    topControls = {
      // Fully custom top controls
      Text("My custom title")
    },
    centerControls = {
      // Use default CenterControls but override the central button
      PlayerDefaults.CenterControls(
        player = player,
        central = {
          // A custom play/pause button
          Material3PlayPauseButton(player, modifier = Modifier.size(64.dp))
        },
      )
    },
    // bottomControls are left as default
  )
}

State Holder in der UI

Wenn keine der Gerüstkomponenten Ihren Anforderungen entspricht, können Sie die Zustandsobjekte auch direkt verwenden. Es ist im Allgemeinen ratsam, die entsprechenden remember-Methoden zu verwenden, um das Erscheinungsbild der Benutzeroberfläche zwischen Neuzusammensetzungen beizubehalten.

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

State Holder für Schaltflächen

Bei einigen UI-Zuständen geht die Bibliothek davon aus, dass sie wahrscheinlich von schaltflächenähnlichen Composables verwendet werden.

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

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

CurrentMediaItemState liefert Informationen zum aktuell wiedergegebenen MediaItem, während PlaylistState Informationen zum MediaItems liefert, der auf dem Player festgelegt ist. Diese sind nützlich, um Metadateninformationen in Ihrer benutzerdefinierten UI anzuzeigen.

ErrorState liefert Informationen zum aktuellen Fehlerstatus des Players, die zum Anzeigen eines Fehler-Overlays verwendet werden können.

PresentationState enthält Informationen dazu, wann die Videoausgabe in einer PlayerSurface angezeigt werden kann oder durch ein Platzhalter-UI-Element abgedeckt werden soll. Das Compose-Element ContentFrame kombiniert die Verarbeitung des Seitenverhältnisses mit der Anzeige des Verschlusses über 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.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()
  }
}

Hier können wir sowohl presentationState.videoAspectRatio verwenden, um die Oberfläche auf das ausgewählte Seitenverhältnis zu skalieren (weitere Typen finden Sie unter Inhaltsskalierung), als auch presentationState.coverSurface, um zu wissen, wann der Zeitpunkt nicht richtig ist, um die Oberfläche anzuzeigen. In diesem Fall können Sie einen undurchsichtigen Shutter über der Oberfläche positionieren, der verschwindet, wenn die Oberfläche bereit ist. Mit ContentFrame kannst du den Shutter als nachgestellten Lambda-Ausdruck anpassen. Standardmäßig ist er 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 vertraut, um sich ständig ändernde UI-Daten zu erfassen. Beispielsweise könnten Sie nach einem Player.isPlaying-Flow suchen, den Sie auf lebenszyklusbewusste Weise collect können. Oder etwas wie Player.eventsFlow, um Ihnen eine Flow<Player.Events> bereitzustellen, die Sie nach Ihren Wünschen filter können.

Die Verwendung von Abläufen für den UI-Status Player hat jedoch einige Nachteile. Eines der Hauptprobleme ist die asynchrone Natur der Datenübertragung. Wir möchten die Latenz zwischen einem Player.Event und der 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 Einzelverantwortung entsprechen. Jeder Nutzer müsste die relevanten Ereignisse herausfiltern.
  • Wenn Sie für jeden Player.Event einen Ablauf erstellen, müssen Sie sie für jedes UI-Element kombinieren (mit combine). Es gibt eine n:m-Zuordnung zwischen einem Player.Event und einer Änderung des UI-Elements. Die Verwendung von combine könnte dazu führen, dass die UI in einen potenziell unzulässigen Zustand gerät.

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 UI-Zustandshalterklasse führt die folgenden Schritte aus:

  1. Nimmt ein Player-Objekt auf.
  2. Abonniert Player mithilfe von Koroutinen. Weitere Informationen finden Sie unter Player.listen.
  3. Reagiert auf bestimmte Player.Events, indem es seinen internen Status aktualisiert.
  4. Akzeptiert Geschäftslogikbefehle, die in ein entsprechendes Player-Update umgewandelt werden.
  5. Kann an mehreren Stellen im UI-Baum erstellt werden und behält immer eine konsistente Ansicht des Player-Status bei.
  6. Stellt Compose-State-Felder bereit, die von einer zusammensetzbaren Funktion genutzt werden können, um dynamisch auf Änderungen zu reagieren.
  7. Enthält eine remember*State-Funktion, mit der die Instanz zwischen Kompositionen gespeichert wird.

Was passiert hinter den Kulissen:

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

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