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.
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.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.Eventswürde nicht dem Prinzip der Einzelverantwortung entsprechen. Jeder Nutzer müsste die relevanten Ereignisse herausfiltern. - Wenn Sie für jeden
Player.Eventeinen Ablauf erstellen, müssen Sie sie für jedes UI-Element kombinieren (mitcombine). Es gibt eine n:m-Zuordnung zwischen einem Player.Event und einer Änderung des UI-Elements. Die Verwendung voncombinekö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:
- Nimmt ein
Player-Objekt auf. - Abonniert
Playermithilfe von Koroutinen. Weitere Informationen finden Sie unterPlayer.listen. - Reagiert auf bestimmte
Player.Events, indem es seinen internen Status aktualisiert. - Akzeptiert Geschäftslogikbefehle, die in ein entsprechendes
Player-Update umgewandelt werden. - Kann an mehreren Stellen im UI-Baum erstellt werden und behält immer eine konsistente Ansicht des Player-Status bei.
- Stellt Compose-
State-Felder bereit, die von einer zusammensetzbaren Funktion genutzt werden können, um dynamisch auf Änderungen zu reagieren. - 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.