Core Compose

Die media3-ui-compose-Bibliothek enthält die grundlegenden Komponenten zum Erstellen einer Medien-UI in Jetpack Compose. Sie wurde für Entwickler entwickelt, 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 Medienplayer-UI zu erstellen.

Material3- und benutzerdefinierte Compose-Komponenten kombinieren

Die media3-ui-compose-material3-Bibliothek ist flexibel. Sie können die vordefinierten Komponenten für den Großteil Ihrer UI 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 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 Player-Steuerelemente. Hier sind einige der Komponenten, die Sie direkt in Ihrer App verwenden können:

Komponente Beschreibung
PlayPauseButton Ein State-Container für eine Schaltfläche, mit der zwischen Wiedergabe und Pause umgeschaltet wird.
SeekBackButton Ein State-Container für eine Schaltfläche, mit der um ein definiertes Inkrement zurückgespult wird.
SeekForwardButton Ein State-Container für eine Schaltfläche, mit der um ein definiertes Inkrement vorgespult wird.
NextButton Ein State-Container für eine Schaltfläche, mit der zum nächsten Medienelement gespult wird.
PreviousButton Ein State-Container für eine Schaltfläche, mit der zum vorherigen Medienelement gespult wird.
RepeatButton Ein State-Container für eine Schaltfläche, mit der zwischen den Wiederholungsmodi gewechselt wird.
ShuffleButton Ein State-Container für eine Schaltfläche, mit der der Zufallsmodus aktiviert und deaktiviert wird.
MuteButton Ein State-Container für eine Schaltfläche, mit der der Player stummgeschaltet und die Stummschaltung aufgehoben wird.
TimeText Ein State-Container für ein Composables, das den Fortschritt des Players anzeigt.
ContentFrame Eine Oberfläche zum Anzeigen von Medieninhalten, die das Seitenverhältnis, die Größenänderung und einen Verschluss verarbeitet
PlayerSurface Rohe Oberfläche, die SurfaceView und TextureView in AndroidView umschließt.

Das Player-Composable anpassen

Wenn Sie die media3-ui-compose-material3 Bibliothek verwenden, bietet das Player Composable eine vollständige Medienwiedergabe Erfahrung. Sie können das Layout anpassen, indem Sie eigene Composables für die Inhalts-Slots bereitstellen. Alternativ können Sie das PlayerDefaults-Objekt verwenden, um bestimmte Teile der UI anzupassen, z. B. TopControls, CenterControls, BottomControls und ErrorOverlay.

Mit PlayerDefaults.CenterControls können Sie beispielsweise nur die zentrale Schaltfläche für die Wiedergabe/Pause überschreiben, während die restlichen Steuerelemente in der Mitte unverändert bleiben. Sie können auch ein vollständig benutzerdefiniertes Composables für einen Parameter wie topControls übergeben und andere wie bottomControls vollständig weglassen, damit die Standardeinstellung beibehalten wird.

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

UI-State Holder

Wenn keine der Gerüstkomponenten Ihren Anforderungen entspricht, können Sie auch die State-Objekte direkt verwenden. Im Allgemeinen empfiehlt es sich, die entsprechenden remember-Methoden zu verwenden, um das Aussehen der UI zwischen den Neukompositionen beizubehalten.

Weitere Informationen zur flexiblen Verwendung von UI-State Holdern im Vergleich zu Composables finden Sie unter Status in Compose verwalten.

Button-State Holder

Für einige UI-Status geht die Bibliothek davon aus, dass sie höchstwahrscheinlich von buttonähnlichen Composables verwendet werden.

Status remember*State Typ
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

CurrentMediaItemState enthält Informationen zum aktuell wiedergegebenen MediaItem, während PlaylistState Informationen zu den MediaItems enthält, die im Player festgelegt sind. Diese sind nützlich, um Metadaten in der benutzerdefinierten UI anzuzeigen.

ErrorState enthält Informationen zum aktuellen Fehlerstatus des Players, die verwendet werden können, um ein Fehler-Overlay anzuzeigen.

PresentationState enthält Informationen dazu, wann die Videoausgabe in einer PlayerSurface angezeigt werden kann oder durch ein Platzhalter-UI-Element verdeckt werden sollte. ContentFrame composable kombiniert die Verarbeitung des Seitenverhältnisses mit dem Anzeigen 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 Inhaltsskala), als auch presentationState.coverSurface, um zu ermitteln, wann der Zeitpunkt für die Anzeige der Oberfläche nicht geeignet ist. In diesem Fall können Sie einen undurchsichtigen Verschluss über der Oberfläche positionieren, der verschwindet, wenn die Oberfläche bereit ist. ContentFrame können Sie den Verschluss als nachfolgende Lambda-Funktion anpassen. Standardmäßig ist es jedoch eine schwarze @Composable Box, die die Größe des übergeordneten Containers ausfüllt.

Wo sind 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 dem Player.isPlaying-Flow, den Sie auf lebenszyklusbewusste Weise collect können. Oder etwas wie Player.eventsFlow, um einen Flow<Player.Events> bereitzustellen, den Sie nach Belieben filter können.

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

Weitere Punkte:

  • Ein Flow 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 kombinieren (combine). 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 UI potenziell ungültige Status erreicht.

Benutzerdefinierte UI-Status 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 UI-State Holder-Klasse führt folgende Schritte aus:

  1. Übernimmt ein Player-Objekt.
  2. Abonniert den Player mit Coroutinen. Weitere Informationen finden Sie unter Player.listen für weitere Details.
  3. Reagiert auf bestimmte Player.Events, indem der interne Status aktualisiert wird.
  4. Akzeptiert Befehle der Geschäftslogik, die in eine entsprechende Player-Aktualisierung umgewandelt werden.
  5. Kann an mehreren Stellen im UI-Baum erstellt werden und behält immer eine konsistente Ansicht des Player-Status bei.
  6. Macht Compose-State-Felder verfügbar, die von einem Composables verwendet werden können, um dynamisch auf Änderungen zu reagieren.
  7. Enthält eine remember*State-Funktion, um die Instanz zwischen den Kompositionen zu speichern.

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 eigene Player.Events reagieren möchten, können Sie sie mit Player.listen abfangen. Dies ist eine suspend fun, mit der Sie die Coroutinen-Welt betreten und unbegrenzt auf Player.Events warten können. Die Media3-Implementierung verschiedener UI-Status hilft dem Endentwickler, sich nicht mit Player.Events auseinandersetzen zu müssen.