UI customizations

Media3 provides a default PlayerView that provides some customization options. For any further customization, app developers are expected to implement their own UI components.

Best practices

When implementing a media UI that connects to a Media3 Player (for example ExoPlayer, MediaController or a custom Player implementation), apps are advised to follow these best practices for the best UI experience.

Play/Pause button

The play and pause button does not directly correspond to a single player state. For example, a user should be able to restart playback after it ended or failed even if the player isn't paused.

To simplify the implementation, Media3 provides util methods to decide which button to show (Util.shouldShowPlayButton) and to handle button presses (Util.handlePlayPauseButtonAction):

Kotlin

val shouldShowPlayButton: Boolean = Util.shouldShowPlayButton(player)
playPauseButton.setImageDrawable(if (shouldShowPlayButton) playDrawable else pauseDrawable)
playPauseButton.setOnClickListener { Util.handlePlayPauseButtonAction(player) }

Java

boolean shouldShowPlayButton = Util.shouldShowPlayButton(player);
playPauseButton.setImageDrawable(shouldShowPlayButton ? playDrawable : pauseDrawable);
playPauseButton.setOnClickListener(view -> Util.handlePlayPauseButtonAction(player));

Listen to state updates

The UI component needs to add a Player.Listener to be informed of state changes that require a corresponding UI update. See Listen to playback events for details.

Refreshing the UI can be costly and multiple player events often arrive together. To avoid refreshing the UI too often in a short period of time, it's generally better to listen to just onEvents and trigger UI updates from there:

Kotlin

player.addListener(object : Player.Listener{
  override fun onEvents(player: Player, events: Player.Events){
    if (events.containsAny(
        Player.EVENT_PLAY_WHEN_READY_CHANGED,
        Player.EVENT_PLAYBACK_STATE_CHANGED,
        Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) {
      updatePlayPauseButton()
    }
    if (events.containsAny(Player.EVENT_REPEAT_MODE_CHANGED)) {
      updateRepeatModeButton()
    }
  }
})

Java

player.addListener(new Player.Listener() {
  @Override
  public void onEvents(Player player, Player.Events events) {
    if (events.containsAny(
        Player.EVENT_PLAY_WHEN_READY_CHANGED,
        Player.EVENT_PLAYBACK_STATE_CHANGED,
        Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED)) {
      updatePlayPauseButton();
    }
    if (events.containsAny(Player.EVENT_REPEAT_MODE_CHANGED)) {
      updateRepeatModeButton();
    }
  }
});

Handle available commands

A general purpose UI component that may need to work with different Player implementations should check the available player commands to show or hide buttons and to avoid calling unsupported methods:

Kotlin

nextButton.isEnabled = player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT)

Java

nextButton.setEnabled(player.isCommandAvailable(Player.COMMAND_SEEK_TO_NEXT));