java.lang.Object
↳FrameLayout
↳androidx.media3.ui.PlayerControlView
Gradle dependencies
compile group: 'androidx.media3', name: 'media3-ui', version: '1.5.0-alpha01'
- groupId: androidx.media3
- artifactId: media3-ui
- version: 1.5.0-alpha01
Artifact androidx.media3:media3-ui:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
A view for controlling Player instances.
A PlayerControlView can be customized by setting attributes (or calling corresponding
methods), or overriding drawables.
Attributes
The following attributes can be set on a PlayerControlView when used in a layout XML
file:
- show_timeout - The time between the last user interaction and the controls
being automatically hidden, in milliseconds. Use zero if the controls should not
automatically timeout.
- show_rewind_button - Whether the rewind button is shown.
- show_fastforward_button - Whether the fast forward button is shown.
- show_previous_button - Whether the previous button is shown.
- show_next_button - Whether the next button is shown.
- repeat_toggle_modes - A flagged enumeration value specifying which repeat
mode toggle options are enabled. Valid values are: none, one, all,
or one|all.
- show_shuffle_button - Whether the shuffle button is shown.
- show_subtitle_button - Whether the subtitle button is shown.
- animation_enabled - Whether an animation is used to show and hide the
playback controls.
- time_bar_min_update_interval - Specifies the minimum interval between time
bar position updates.
- All attributes that can be set on DefaultTimeBar can also be set on a PlayerControlView, and will be propagated to the inflated DefaultTimeBar.
Overriding drawables globally
The drawables used by PlayerControlView can be overridden by drawables with the same
names defined in your application. Note that these icons will be the same across all usages of
PlayerView/PlayerControlView in your app. The drawables that can be overridden
are:
- exo_styled_controls_play - The play icon.
- exo_styled_controls_pause - The pause icon.
- exo_styled_controls_rewind - The background of rewind icon.
- exo_styled_controls_fastforward - The background of fast forward icon.
- exo_styled_controls_previous - The previous icon.
- exo_styled_controls_next - The next icon.
- exo_styled_controls_repeat_off - The repeat icon for Player.REPEAT_MODE_OFF.
- exo_styled_controls_repeat_one - The repeat icon for Player.REPEAT_MODE_ONE.
- exo_styled_controls_repeat_all - The repeat icon for Player.REPEAT_MODE_ALL.
- exo_styled_controls_shuffle_off - The shuffle icon when shuffling is
disabled.
- exo_styled_controls_shuffle_on - The shuffle icon when shuffling is enabled.
- exo_styled_controls_vr - The VR icon.
- exo_styled_controls_fullscreen_enter - The fullscreen icon for when the
player is minimized.
- exo_styled_controls_fullscreen_exit - The fullscreen icon for when the
player is in fullscreen mode.
Overriding drawables locally
If you want to customize drawable per PlayerView instance, you can use the following attributes:
PlayerView/PlayerControlView in your app. The drawables that can be overridden
are:
- play_icon - The drawable resource ID for the play/pause button when play is
shown.
- Default: @drawable/exo_styled_controls_play
- pause_icon - The drawable resource ID for the play/pause button when pause
is shown.
- Default: @drawable/exo_styled_controls_pause
- fastforward_icon - The drawable resource ID for the simple fast forward
button without the seek-forward amount. The ID of the
in such case should be exo_ffwd, specified in the exo_player_control_ffwd_button layout.
- Default: @drawable/exo_styled_controls_simple_fastforward
- rewind_icon - The drawable resource ID for the simple rewind button without
the seek-back amount. The ID of the in such case
should be exo_rew, specified in the exo_player_control_rewind_button
layout.
- Default: @drawable/exo_styled_controls_simple_rewind
- previous_icon - The drawable resource ID for the previous button.
- Default: @drawable/exo_styled_controls_previous
- next_icon - The drawable resource ID for the next button.
- Default: @drawable/exo_styled_controls_next
- repeat_off_icon - The drawable resource ID for the repeat button when the
mode is none.
- Default: @drawable/exo_styled_controls_repeat_off
- repeat_one_icon - The drawable resource ID for the repeat button when the
mode is one.
- Default: @drawable/exo_styled_controls_repeat_one
- repeat_all_icon - The drawable resource ID for the repeat button when the
mode is all.
- Default: @drawable/exo_styled_controls_repeat_all
- shuffle_on_icon - The drawable resource ID for the repeat button when the
mode is one.
- Default: @drawable/exo_styled_controls_shuffle_on
- shuffle_off_icon - The drawable resource ID for the repeat button when the
mode is all.
- Default: @drawable/exo_styled_controls_shuffle_off
- subtitle_on_icon - The drawable resource ID for the subtitle button when the
text track is on.
- Default: @drawable/exo_styled_controls_subtitle_on
- subtitle_off_icon - The drawable resource ID for the subtitle button when
the text track is off.
- Default: @drawable/exo_styled_controls_subtitle_off
- vr_icon - The drawable resource ID for the VR button.
- Default: @drawable/exo_styled_controls_vr
- fullscreen_enter_icon - The drawable resource ID for the fullscreen button
when the player is minimized.
- Default: @drawable/exo_styled_controls_fullscreen_enter
- fullscreen_exit_icon - The drawable resource ID for the fullscreen button
when the player is in fullscreen mode.
- Default: @drawable/exo_styled_controls_fullscreen_exit
Summary
Methods |
---|
public void | addVisibilityListener(PlayerControlView.VisibilityListener listener)
|
public boolean | dispatchKeyEvent(KeyEvent event)
|
public boolean | dispatchMediaKeyEvent(KeyEvent event)
Called to process media key events. |
public Player | getPlayer()
Returns the Player currently being controlled by this view, or null if no player is
set. |
public int | getRepeatToggleModes()
Returns which repeat toggle modes are enabled. |
public boolean | getShowShuffleButton()
Returns whether the shuffle button is shown. |
public boolean | getShowSubtitleButton()
Returns whether the subtitle button is shown. |
public int | getShowTimeoutMs()
Returns the playback controls timeout. |
public boolean | getShowVrButton()
Returns whether the VR button is shown. |
public void | hide()
Hides the controller. |
public void | hideImmediately()
Hides the controller without any animation. |
public boolean | isAnimationEnabled()
Returns whether an animation is used to show and hide the playback controls. |
public boolean | isFullyVisible()
Returns whether the controller is fully visible, which means all UI controls are visible. |
public boolean | isVisible()
Returns whether the controller is currently visible. |
public void | onAttachedToWindow()
|
public void | onDetachedFromWindow()
|
protected void | onLayout(boolean changed, int left, int top, int right, int bottom)
|
public void | removeVisibilityListener(PlayerControlView.VisibilityListener listener)
|
public void | setAnimationEnabled(boolean animationEnabled)
Sets whether an animation is used to show and hide the playback controls. |
public void | setExtraAdGroupMarkers(long[] extraAdGroupTimesMs[], boolean[] extraPlayedAdGroups[])
Sets the millisecond positions of extra ad markers relative to the start of the window (or
timeline, if in multi-window mode) and whether each extra ad has been played or not. |
public void | setOnFullScreenModeChangedListener(PlayerControlView.OnFullScreenModeChangedListener listener)
|
public void | setPlayer(Player player)
Sets the Player to control. |
public void | setProgressUpdateListener(PlayerControlView.ProgressUpdateListener listener)
Sets the PlayerControlView.ProgressUpdateListener. |
public void | setRepeatToggleModes(int repeatToggleModes)
Sets which repeat toggle modes are enabled. |
public void | setShowFastForwardButton(boolean showFastForwardButton)
Sets whether the fast forward button is shown. |
public void | setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar)
|
public void | setShowNextButton(boolean showNextButton)
Sets whether the next button is shown. |
public void | setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed)
Sets whether a play button is shown if playback is suppressed. |
public void | setShowPreviousButton(boolean showPreviousButton)
Sets whether the previous button is shown. |
public void | setShowRewindButton(boolean showRewindButton)
Sets whether the rewind button is shown. |
public void | setShowShuffleButton(boolean showShuffleButton)
Sets whether the shuffle button is shown. |
public void | setShowSubtitleButton(boolean showSubtitleButton)
Sets whether the subtitle button is shown. |
public void | setShowTimeoutMs(int showTimeoutMs)
Sets the playback controls timeout. |
public void | setShowVrButton(boolean showVrButton)
Sets whether the VR button is shown. |
public void | setTimeBarMinUpdateInterval(int minUpdateIntervalMs)
Sets the minimum interval between time bar position updates. |
public void | setVrButtonListener(OnClickListener onClickListener)
Sets listener for the VR button. |
public void | show()
Shows the playback controls. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
DEFAULT_SHOW_TIMEOUT_MSThe default show timeout, in milliseconds.
public static final int
DEFAULT_REPEAT_TOGGLE_MODESThe default repeat toggle modes.
public static final int
DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MSThe default minimum interval between time bar position updates.
public static final int
MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BARThe maximum number of windows that can be shown in a multi-window time bar.
Constructors
public
PlayerControlView(Context context)
public
PlayerControlView(Context context, AttributeSet attrs)
public
PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr)
public
PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs)
Methods
Returns the Player currently being controlled by this view, or null if no player is
set.
public void
setPlayer(
Player player)
Sets the Player to control.
Parameters:
player: The Player to control, or null to detach the current player. Only
players which are accessed on the main thread are supported (player.getApplicationLooper() == Looper.getMainLooper()).
public void
setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar)
Deprecated: Replace multi-window time bar display by merging source windows together instead,
for example using ExoPlayer's ConcatenatingMediaSource2.
public void
setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed)
Sets whether a play button is shown if playback is suppressed.
The default is true.
Parameters:
showPlayButtonIfSuppressed: Whether to show a play button if playback is suppressed.
public void
setExtraAdGroupMarkers(long[] extraAdGroupTimesMs[], boolean[] extraPlayedAdGroups[])
Sets the millisecond positions of extra ad markers relative to the start of the window (or
timeline, if in multi-window mode) and whether each extra ad has been played or not. The
markers are shown in addition to any ad markers for ads in the player's timeline.
Parameters:
extraAdGroupTimesMs: The millisecond timestamps of the extra ad markers to show, or
null to show no extra ad markers.
extraPlayedAdGroups: Whether each ad has been played. Must be the same length as extraAdGroupTimesMs, or null if extraAdGroupTimesMs is null.
Deprecated: Register a PlayerView.ControllerVisibilityListener via PlayerView.setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)
instead. Using PlayerControlView as a standalone class without PlayerView
is deprecated.
Deprecated: Register a PlayerView.ControllerVisibilityListener via PlayerView.setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)
instead. Using PlayerControlView as a standalone class without PlayerView
is deprecated.
Sets the PlayerControlView.ProgressUpdateListener.
Parameters:
listener: The listener to be notified about when progress is updated.
public void
setShowRewindButton(boolean showRewindButton)
Sets whether the rewind button is shown.
Parameters:
showRewindButton: Whether the rewind button is shown.
public void
setShowFastForwardButton(boolean showFastForwardButton)
Sets whether the fast forward button is shown.
Parameters:
showFastForwardButton: Whether the fast forward button is shown.
public void
setShowPreviousButton(boolean showPreviousButton)
Sets whether the previous button is shown.
Parameters:
showPreviousButton: Whether the previous button is shown.
public void
setShowNextButton(boolean showNextButton)
Sets whether the next button is shown.
Parameters:
showNextButton: Whether the next button is shown.
public int
getShowTimeoutMs()
Returns the playback controls timeout. The playback controls are automatically hidden after
this duration of time has elapsed without user input.
Returns:
The duration in milliseconds. A non-positive value indicates that the controls will
remain visible indefinitely.
public void
setShowTimeoutMs(int showTimeoutMs)
Sets the playback controls timeout. The playback controls are automatically hidden after this
duration of time has elapsed without user input.
Parameters:
showTimeoutMs: The duration in milliseconds. A non-positive value will cause the controls
to remain visible indefinitely.
public int
getRepeatToggleModes()
Returns which repeat toggle modes are enabled.
Returns:
The currently enabled .
public void
setRepeatToggleModes(int repeatToggleModes)
Sets which repeat toggle modes are enabled.
Parameters:
repeatToggleModes: A set of .
public boolean
getShowShuffleButton()
Returns whether the shuffle button is shown.
public void
setShowShuffleButton(boolean showShuffleButton)
Sets whether the shuffle button is shown.
Parameters:
showShuffleButton: Whether the shuffle button is shown.
public boolean
getShowSubtitleButton()
Returns whether the subtitle button is shown.
public void
setShowSubtitleButton(boolean showSubtitleButton)
Sets whether the subtitle button is shown.
Parameters:
showSubtitleButton: Whether the subtitle button is shown.
public boolean
getShowVrButton()
Returns whether the VR button is shown.
public void
setShowVrButton(boolean showVrButton)
Sets whether the VR button is shown.
Parameters:
showVrButton: Whether the VR button is shown.
public void
setVrButtonListener(OnClickListener onClickListener)
Sets listener for the VR button.
Parameters:
onClickListener: Listener for the VR button, or null to clear the listener.
public void
setAnimationEnabled(boolean animationEnabled)
Sets whether an animation is used to show and hide the playback controls.
Parameters:
animationEnabled: Whether an animation is applied to show and hide playback controls.
public boolean
isAnimationEnabled()
Returns whether an animation is used to show and hide the playback controls.
public void
setTimeBarMinUpdateInterval(int minUpdateIntervalMs)
Sets the minimum interval between time bar position updates.
Note that smaller intervals, e.g. 33ms, will result in a smooth movement but will use more
CPU resources while the time bar is visible, whereas larger intervals, e.g. 200ms, will result
in a step-wise update with less CPU usage.
Parameters:
minUpdateIntervalMs: The minimum interval between time bar position updates, in
milliseconds.
Deprecated: Register a PlayerView.FullscreenButtonClickListener via PlayerView.setFullscreenButtonClickListener(PlayerView.FullscreenButtonClickListener)
instead. Using PlayerControlView as a standalone class without PlayerView
is deprecated.
Shows the playback controls. If PlayerControlView.getShowTimeoutMs() is positive then the controls will
be automatically hidden after this duration of time has elapsed without user input.
Hides the controller.
public void
hideImmediately()
Hides the controller without any animation.
public boolean
isFullyVisible()
Returns whether the controller is fully visible, which means all UI controls are visible.
public boolean
isVisible()
Returns whether the controller is currently visible.
public void
onAttachedToWindow()
public void
onDetachedFromWindow()
public boolean
dispatchKeyEvent(KeyEvent event)
public boolean
dispatchMediaKeyEvent(KeyEvent event)
Called to process media key events. Any can be passed but only media key
events will be handled.
Parameters:
event: A key event.
Returns:
Whether the key event was handled.
protected void
onLayout(boolean changed, int left, int top, int right, int bottom)
Source
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.ui;
import static androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_GET_TIMELINE;
import static androidx.media3.common.Player.COMMAND_GET_TRACKS;
import static androidx.media3.common.Player.COMMAND_PLAY_PAUSE;
import static androidx.media3.common.Player.COMMAND_SEEK_BACK;
import static androidx.media3.common.Player.COMMAND_SEEK_FORWARD;
import static androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH;
import static androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS;
import static androidx.media3.common.Player.EVENT_AVAILABLE_COMMANDS_CHANGED;
import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED;
import static androidx.media3.common.Player.EVENT_PLAYBACK_PARAMETERS_CHANGED;
import static androidx.media3.common.Player.EVENT_PLAYBACK_STATE_CHANGED;
import static androidx.media3.common.Player.EVENT_PLAY_WHEN_READY_CHANGED;
import static androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY;
import static androidx.media3.common.Player.EVENT_REPEAT_MODE_CHANGED;
import static androidx.media3.common.Player.EVENT_SEEK_BACK_INCREMENT_CHANGED;
import static androidx.media3.common.Player.EVENT_SEEK_FORWARD_INCREMENT_CHANGED;
import static androidx.media3.common.Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED;
import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED;
import static androidx.media3.common.Player.EVENT_TRACKS_CHANGED;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.getDrawable;
import static androidx.media3.common.util.Util.msToUs;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.res.ResourcesCompat;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.Player;
import androidx.media3.common.Player.Events;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.RepeatModeUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* A view for controlling {@link Player} instances.
*
* <p>A {@code PlayerControlView} can be customized by setting attributes (or calling corresponding
* methods), or overriding drawables.
*
* <h2>Attributes</h2>
*
* The following attributes can be set on a {@code PlayerControlView} when used in a layout XML
* file:
*
* <ul>
* <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls
* being automatically hidden, in milliseconds. Use zero if the controls should not
* automatically timeout.
* <ul>
* <li>Corresponding method: {@link #setShowTimeoutMs(int)}
* <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
* </ul>
* <li><b>{@code show_rewind_button}</b> - Whether the rewind button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowRewindButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code show_fastforward_button}</b> - Whether the fast forward button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowFastForwardButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code show_previous_button}</b> - Whether the previous button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowPreviousButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code show_next_button}</b> - Whether the next button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowNextButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code repeat_toggle_modes}</b> - A flagged enumeration value specifying which repeat
* mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all},
* or {@code one|all}.
* <ul>
* <li>Corresponding method: {@link #setRepeatToggleModes(int)}
* <li>Default: {@link #DEFAULT_REPEAT_TOGGLE_MODES}
* </ul>
* <li><b>{@code show_shuffle_button}</b> - Whether the shuffle button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
* <li>Default: false
* </ul>
* <li><b>{@code show_subtitle_button}</b> - Whether the subtitle button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowSubtitleButton(boolean)}
* <li>Default: false
* </ul>
* <li><b>{@code animation_enabled}</b> - Whether an animation is used to show and hide the
* playback controls.
* <ul>
* <li>Corresponding method: {@link #setAnimationEnabled(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code time_bar_min_update_interval}</b> - Specifies the minimum interval between time
* bar position updates.
* <ul>
* <li>Corresponding method: {@link #setTimeBarMinUpdateInterval(int)}
* <li>Default: {@link #DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS}
* </ul>
* <li>All attributes that can be set on {@link DefaultTimeBar} can also be set on a {@code
* PlayerControlView}, and will be propagated to the inflated {@link DefaultTimeBar}.
* </ul>
*
* <h2>Overriding drawables globally</h2>
*
* The drawables used by {@code PlayerControlView} can be overridden by drawables with the same
* names defined in your application. Note that these icons will be the same across all usages of
* {@code PlayerView}/{@code PlayerControlView} in your app. The drawables that can be overridden
* are:
*
* <ul>
* <li><b>{@code exo_styled_controls_play}</b> - The play icon.
* <li><b>{@code exo_styled_controls_pause}</b> - The pause icon.
* <li><b>{@code exo_styled_controls_rewind}</b> - The background of rewind icon.
* <li><b>{@code exo_styled_controls_fastforward}</b> - The background of fast forward icon.
* <li><b>{@code exo_styled_controls_previous}</b> - The previous icon.
* <li><b>{@code exo_styled_controls_next}</b> - The next icon.
* <li><b>{@code exo_styled_controls_repeat_off}</b> - The repeat icon for {@link
* Player#REPEAT_MODE_OFF}.
* <li><b>{@code exo_styled_controls_repeat_one}</b> - The repeat icon for {@link
* Player#REPEAT_MODE_ONE}.
* <li><b>{@code exo_styled_controls_repeat_all}</b> - The repeat icon for {@link
* Player#REPEAT_MODE_ALL}.
* <li><b>{@code exo_styled_controls_shuffle_off}</b> - The shuffle icon when shuffling is
* disabled.
* <li><b>{@code exo_styled_controls_shuffle_on}</b> - The shuffle icon when shuffling is enabled.
* <li><b>{@code exo_styled_controls_vr}</b> - The VR icon.
* <li><b>{@code exo_styled_controls_fullscreen_enter}</b> - The fullscreen icon for when the
* player is minimized.
* <li><b>{@code exo_styled_controls_fullscreen_exit}</b> - The fullscreen icon for when the
* player is in fullscreen mode.
* </ul>
*
* <h2>Overriding drawables locally</h2>
*
* If you want to customize drawable per PlayerView instance, you can use the following attributes:
* {@code PlayerView}/{@code PlayerControlView} in your app. The drawables that can be overridden
* are:
*
* <ul>
* <li><b>{@code play_icon}</b> - The drawable resource ID for the play/pause button when play is
* shown.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_play}</b>
* </ul>
* <li><b>{@code pause_icon}</b> - The drawable resource ID for the play/pause button when pause
* is shown.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_pause}</b>
* </ul>
* <li><b>{@code fastforward_icon}</b> - The drawable resource ID for the simple fast forward
* button without the seek-forward amount. The ID of the {@linkplain ImageButton image button}
* in such case should be {@code exo_ffwd}, specified in the {@code
* exo_player_control_ffwd_button} layout.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_simple_fastforward}</b>
* </ul>
* <li><b>{@code rewind_icon}</b> - The drawable resource ID for the simple rewind button without
* the seek-back amount. The ID of the {@linkplain ImageButton image button} in such case
* should be {@code exo_rew}, specified in the {@code exo_player_control_rewind_button}
* layout.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_simple_rewind}</b>
* </ul>
* <li><b>{@code previous_icon}</b> - The drawable resource ID for the previous button.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_previous}</b>
* </ul>
* <li><b>{@code next_icon}</b> - The drawable resource ID for the next button.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_next}</b>
* </ul>
* <li><b>{@code repeat_off_icon}</b> - The drawable resource ID for the repeat button when the
* mode is {@code none}.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_repeat_off}</b>
* </ul>
* <li><b>{@code repeat_one_icon}</b> - The drawable resource ID for the repeat button when the
* mode is {@code one}.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_repeat_one}</b>
* </ul>
* <li><b>{@code repeat_all_icon}</b> - The drawable resource ID for the repeat button when the
* mode is {@code all}.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_repeat_all}</b>
* </ul>
* <li><b>{@code shuffle_on_icon}</b> - The drawable resource ID for the repeat button when the
* mode is {@code one}.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_shuffle_on}</b>
* </ul>
* <li><b>{@code shuffle_off_icon}</b> - The drawable resource ID for the repeat button when the
* mode is {@code all}.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_shuffle_off}</b>
* </ul>
* <li><b>{@code subtitle_on_icon}</b> - The drawable resource ID for the subtitle button when the
* text track is on.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_subtitle_on}</b>
* </ul>
* <li><b>{@code subtitle_off_icon}</b> - The drawable resource ID for the subtitle button when
* the text track is off.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_subtitle_off}</b>
* </ul>
* <li><b>{@code vr_icon}</b> - The drawable resource ID for the VR button.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_vr}</b>
* </ul>
* <li><b>{@code fullscreen_enter_icon}</b> - The drawable resource ID for the fullscreen button
* when the player is minimized.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_fullscreen_enter}</b>
* </ul>
* <li><b>{@code fullscreen_exit_icon}</b> - The drawable resource ID for the fullscreen button
* when the player is in fullscreen mode.
* <ul>
* <li>Default: <b>{@code @drawable/exo_styled_controls_fullscreen_exit}</b>
* </ul>
* </ul>
*/
@UnstableApi
public class PlayerControlView extends FrameLayout {
static {
MediaLibraryInfo.registerModule("media3.ui");
}
/**
* @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link
* PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)}
* instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
* is deprecated.
*/
@Deprecated
public interface VisibilityListener {
/**
* Called when the visibility changes.
*
* @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}.
*/
void onVisibilityChange(int visibility);
}
/** Listener to be notified when progress has been updated. */
public interface ProgressUpdateListener {
/**
* Called when progress needs to be updated.
*
* @param position The current position.
* @param bufferedPosition The current buffered position.
*/
void onProgressUpdate(long position, long bufferedPosition);
}
/**
* @deprecated Register a {@link PlayerView.FullscreenButtonClickListener} via {@link
* PlayerView#setFullscreenButtonClickListener(PlayerView.FullscreenButtonClickListener)}
* instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
* is deprecated.
*/
@Deprecated
public interface OnFullScreenModeChangedListener {
/**
* Called to indicate a fullscreen mode change.
*
* @param isFullScreen {@code true} if the video rendering surface should be fullscreen {@code
* false} otherwise.
*/
void onFullScreenModeChanged(boolean isFullScreen);
}
/** The default show timeout, in milliseconds. */
public static final int DEFAULT_SHOW_TIMEOUT_MS = 5_000;
/** The default repeat toggle modes. */
public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES =
RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE;
/** The default minimum interval between time bar position updates. */
public static final int DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS = 200;
/** The maximum number of windows that can be shown in a multi-window time bar. */
public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100;
/** The maximum interval between time bar position updates. */
private static final int MAX_UPDATE_INTERVAL_MS = 1_000;
// LINT.IfChange(playback_speeds)
private static final float[] PLAYBACK_SPEEDS =
new float[] {0.25f, 0.5f, 0.75f, 1f, 1.25f, 1.5f, 2f};
private static final int SETTINGS_PLAYBACK_SPEED_POSITION = 0;
private static final int SETTINGS_AUDIO_TRACK_SELECTION_POSITION = 1;
private final PlayerControlViewLayoutManager controlViewLayoutManager;
private final Resources resources;
private final ComponentListener componentListener;
@SuppressWarnings("deprecation") // Using the deprecated type for now.
private final CopyOnWriteArrayList<VisibilityListener> visibilityListeners;
private final RecyclerView settingsView;
private final SettingsAdapter settingsAdapter;
private final PlaybackSpeedAdapter playbackSpeedAdapter;
private final TextTrackSelectionAdapter textTrackSelectionAdapter;
private final AudioTrackSelectionAdapter audioTrackSelectionAdapter;
// TODO(insun): Add setTrackNameProvider to use customized track name provider.
private final TrackNameProvider trackNameProvider;
private final PopupWindow settingsWindow;
private final int settingsWindowMargin;
@Nullable private final ImageView previousButton;
@Nullable private final ImageView nextButton;
@Nullable private final ImageView playPauseButton;
@Nullable private final View fastForwardButton;
@Nullable private final View rewindButton;
@Nullable private final TextView fastForwardButtonTextView;
@Nullable private final TextView rewindButtonTextView;
@Nullable private final ImageView repeatToggleButton;
@Nullable private final ImageView shuffleButton;
@Nullable private final ImageView vrButton;
@Nullable private final ImageView subtitleButton;
@Nullable private final ImageView fullScreenButton;
@Nullable private final ImageView minimalFullScreenButton;
@Nullable private final View settingsButton;
@Nullable private final View playbackSpeedButton;
@Nullable private final View audioTrackButton;
@Nullable private final TextView durationView;
@Nullable private final TextView positionView;
@Nullable private final TimeBar timeBar;
private final StringBuilder formatBuilder;
private final Formatter formatter;
private final Timeline.Period period;
private final Timeline.Window window;
private final Runnable updateProgressAction;
private final Drawable playButtonDrawable;
private final Drawable pauseButtonDrawable;
private final Drawable repeatOffButtonDrawable;
private final Drawable repeatOneButtonDrawable;
private final Drawable repeatAllButtonDrawable;
private final String repeatOffButtonContentDescription;
private final String repeatOneButtonContentDescription;
private final String repeatAllButtonContentDescription;
private final Drawable shuffleOnButtonDrawable;
private final Drawable shuffleOffButtonDrawable;
private final float buttonAlphaEnabled;
private final float buttonAlphaDisabled;
private final String shuffleOnContentDescription;
private final String shuffleOffContentDescription;
private final Drawable subtitleOnButtonDrawable;
private final Drawable subtitleOffButtonDrawable;
private final String subtitleOnContentDescription;
private final String subtitleOffContentDescription;
private final Drawable fullScreenExitDrawable;
private final Drawable fullScreenEnterDrawable;
private final String fullScreenExitContentDescription;
private final String fullScreenEnterContentDescription;
@Nullable private Player player;
@Nullable private ProgressUpdateListener progressUpdateListener;
@SuppressWarnings("deprecation") // Supporting deprecated listener
@Nullable
private OnFullScreenModeChangedListener onFullScreenModeChangedListener;
private boolean isFullScreen;
private boolean isAttachedToWindow;
private boolean showMultiWindowTimeBar;
private boolean showPlayButtonIfSuppressed;
private boolean multiWindowTimeBar;
private boolean scrubbing;
private int showTimeoutMs;
private int timeBarMinUpdateIntervalMs;
private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
private long[] adGroupTimesMs;
private boolean[] playedAdGroups;
private long[] extraAdGroupTimesMs;
private boolean[] extraPlayedAdGroups;
private long currentWindowOffset;
private boolean needToHideBars;
public PlayerControlView(Context context) {
this(context, /* attrs= */ null);
}
public PlayerControlView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, /* defStyleAttr= */ 0);
}
public PlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, attrs);
}
// TODO: b/301602565 - See if there's a reasonable non-null root view group we could use below to
// resolve InflateParams lint.
@SuppressWarnings({
"InflateParams",
"nullness:argument",
"nullness:assignment",
"nullness:method.invocation",
"nullness:methodref.receiver.bound"
})
public PlayerControlView(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet playbackAttrs) {
super(context, attrs, defStyleAttr);
int controllerLayoutId = R.layout.exo_player_control_view;
int playDrawableResId = R.drawable.exo_styled_controls_play;
int pauseDrawableResId = R.drawable.exo_styled_controls_pause;
int nextDrawableResId = R.drawable.exo_styled_controls_next;
int fastForwardDrawableResId = R.drawable.exo_styled_controls_simple_fastforward;
int previousDrawableResId = R.drawable.exo_styled_controls_previous;
int rewindDrawableResId = R.drawable.exo_styled_controls_simple_rewind;
int fullScreenExitDrawableResId = R.drawable.exo_styled_controls_fullscreen_exit;
int fullScreenEnterDrawableResId = R.drawable.exo_styled_controls_fullscreen_enter;
int repeatOffDrawableResId = R.drawable.exo_styled_controls_repeat_off;
int repeatOneDrawableResId = R.drawable.exo_styled_controls_repeat_one;
int repeatAllDrawableResId = R.drawable.exo_styled_controls_repeat_all;
int shuffleOnDrawableResId = R.drawable.exo_styled_controls_shuffle_on;
int shuffleOffDrawableResId = R.drawable.exo_styled_controls_shuffle_off;
int subtitleOnDrawableResId = R.drawable.exo_styled_controls_subtitle_on;
int subtitleOffDrawableResId = R.drawable.exo_styled_controls_subtitle_off;
int vrDrawableResId = R.drawable.exo_styled_controls_vr;
showPlayButtonIfSuppressed = true;
showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;
timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS;
boolean showRewindButton = true;
boolean showFastForwardButton = true;
boolean showPreviousButton = true;
boolean showNextButton = true;
boolean showShuffleButton = false;
boolean showSubtitleButton = false;
boolean animationEnabled = true;
boolean showVrButton = false;
if (playbackAttrs != null) {
TypedArray a =
context
.getTheme()
.obtainStyledAttributes(
playbackAttrs, R.styleable.PlayerControlView, defStyleAttr, /* defStyleRes= */ 0);
try {
controllerLayoutId =
a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId);
playDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_play_icon, playDrawableResId);
pauseDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_pause_icon, pauseDrawableResId);
nextDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_next_icon, nextDrawableResId);
fastForwardDrawableResId =
a.getResourceId(
R.styleable.PlayerControlView_fastforward_icon, fastForwardDrawableResId);
previousDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_previous_icon, previousDrawableResId);
rewindDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_rewind_icon, rewindDrawableResId);
fullScreenExitDrawableResId =
a.getResourceId(
R.styleable.PlayerControlView_fullscreen_exit_icon, fullScreenExitDrawableResId);
fullScreenEnterDrawableResId =
a.getResourceId(
R.styleable.PlayerControlView_fullscreen_enter_icon, fullScreenEnterDrawableResId);
repeatOffDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_repeat_off_icon, repeatOffDrawableResId);
repeatOneDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_repeat_one_icon, repeatOneDrawableResId);
repeatAllDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_repeat_all_icon, repeatAllDrawableResId);
shuffleOnDrawableResId =
a.getResourceId(R.styleable.PlayerControlView_shuffle_on_icon, shuffleOnDrawableResId);
shuffleOffDrawableResId =
a.getResourceId(
R.styleable.PlayerControlView_shuffle_off_icon, shuffleOffDrawableResId);
subtitleOnDrawableResId =
a.getResourceId(
R.styleable.PlayerControlView_subtitle_on_icon, subtitleOnDrawableResId);
subtitleOffDrawableResId =
a.getResourceId(
R.styleable.PlayerControlView_subtitle_off_icon, subtitleOffDrawableResId);
vrDrawableResId = a.getResourceId(R.styleable.PlayerControlView_vr_icon, vrDrawableResId);
showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs);
repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes);
showRewindButton =
a.getBoolean(R.styleable.PlayerControlView_show_rewind_button, showRewindButton);
showFastForwardButton =
a.getBoolean(
R.styleable.PlayerControlView_show_fastforward_button, showFastForwardButton);
showPreviousButton =
a.getBoolean(R.styleable.PlayerControlView_show_previous_button, showPreviousButton);
showNextButton =
a.getBoolean(R.styleable.PlayerControlView_show_next_button, showNextButton);
showShuffleButton =
a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton);
showSubtitleButton =
a.getBoolean(R.styleable.PlayerControlView_show_subtitle_button, showSubtitleButton);
showVrButton = a.getBoolean(R.styleable.PlayerControlView_show_vr_button, showVrButton);
setTimeBarMinUpdateInterval(
a.getInt(
R.styleable.PlayerControlView_time_bar_min_update_interval,
timeBarMinUpdateIntervalMs));
animationEnabled =
a.getBoolean(R.styleable.PlayerControlView_animation_enabled, animationEnabled);
} finally {
a.recycle();
}
}
LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
componentListener = new ComponentListener();
visibilityListeners = new CopyOnWriteArrayList<>();
period = new Timeline.Period();
window = new Timeline.Window();
formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault());
adGroupTimesMs = new long[0];
playedAdGroups = new boolean[0];
extraAdGroupTimesMs = new long[0];
extraPlayedAdGroups = new boolean[0];
updateProgressAction = this::updateProgress;
durationView = findViewById(R.id.exo_duration);
positionView = findViewById(R.id.exo_position);
subtitleButton = findViewById(R.id.exo_subtitle);
if (subtitleButton != null) {
subtitleButton.setOnClickListener(componentListener);
}
fullScreenButton = findViewById(R.id.exo_fullscreen);
initializeFullScreenButton(fullScreenButton, this::onFullScreenButtonClicked);
minimalFullScreenButton = findViewById(R.id.exo_minimal_fullscreen);
initializeFullScreenButton(minimalFullScreenButton, this::onFullScreenButtonClicked);
settingsButton = findViewById(R.id.exo_settings);
if (settingsButton != null) {
settingsButton.setOnClickListener(componentListener);
}
playbackSpeedButton = findViewById(R.id.exo_playback_speed);
if (playbackSpeedButton != null) {
playbackSpeedButton.setOnClickListener(componentListener);
}
audioTrackButton = findViewById(R.id.exo_audio_track);
if (audioTrackButton != null) {
audioTrackButton.setOnClickListener(componentListener);
}
TimeBar customTimeBar = findViewById(R.id.exo_progress);
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
if (customTimeBar != null) {
timeBar = customTimeBar;
} else if (timeBarPlaceholder != null) {
// Propagate playbackAttrs as timebarAttrs so that DefaultTimeBar's custom attributes are
// transferred, but standard attributes (e.g. background) are not.
DefaultTimeBar defaultTimeBar =
new DefaultTimeBar(context, null, 0, playbackAttrs, R.style.ExoStyledControls_TimeBar);
defaultTimeBar.setId(R.id.exo_progress);
defaultTimeBar.setLayoutParams(timeBarPlaceholder.getLayoutParams());
ViewGroup parent = ((ViewGroup) timeBarPlaceholder.getParent());
int timeBarIndex = parent.indexOfChild(timeBarPlaceholder);
parent.removeView(timeBarPlaceholder);
parent.addView(defaultTimeBar, timeBarIndex);
timeBar = defaultTimeBar;
} else {
timeBar = null;
}
if (timeBar != null) {
timeBar.addListener(componentListener);
}
resources = context.getResources();
playPauseButton = findViewById(R.id.exo_play_pause);
if (playPauseButton != null) {
playPauseButton.setOnClickListener(componentListener);
}
previousButton = findViewById(R.id.exo_prev);
if (previousButton != null) {
previousButton.setImageDrawable(getDrawable(context, resources, previousDrawableResId));
previousButton.setOnClickListener(componentListener);
}
nextButton = findViewById(R.id.exo_next);
if (nextButton != null) {
nextButton.setImageDrawable(getDrawable(context, resources, nextDrawableResId));
nextButton.setOnClickListener(componentListener);
}
Typeface typeface = ResourcesCompat.getFont(context, R.font.roboto_medium_numbers);
ImageView rewButton = findViewById(R.id.exo_rew);
TextView rewButtonWithAmount = findViewById(R.id.exo_rew_with_amount);
if (rewButton != null) {
// For a simple rewind button without seek-back increment value
rewButton.setImageDrawable(getDrawable(context, resources, rewindDrawableResId));
rewindButton = rewButton;
rewindButtonTextView = null;
} else if (rewButtonWithAmount != null) {
// For a circular rewind button with the amount in the middle
rewButtonWithAmount.setTypeface(typeface);
rewindButtonTextView = rewButtonWithAmount;
rewindButton = rewindButtonTextView;
} else {
rewindButtonTextView = null;
rewindButton = null;
}
if (rewindButton != null) {
rewindButton.setOnClickListener(componentListener);
}
ImageView ffwdButton = findViewById(R.id.exo_ffwd);
TextView ffwdButtonWithAmount = findViewById(R.id.exo_ffwd_with_amount);
if (ffwdButton != null) {
// For a simple fast forward button without seek-forward increment value
ffwdButton.setImageDrawable(getDrawable(context, resources, fastForwardDrawableResId));
fastForwardButton = ffwdButton;
fastForwardButtonTextView = null;
} else if (ffwdButtonWithAmount != null) {
// For a circular fastforward button with the amount in the middle
ffwdButtonWithAmount.setTypeface(typeface);
fastForwardButtonTextView = ffwdButtonWithAmount;
fastForwardButton = fastForwardButtonTextView;
} else {
fastForwardButtonTextView = null;
fastForwardButton = null;
}
if (fastForwardButton != null) {
fastForwardButton.setOnClickListener(componentListener);
}
repeatToggleButton = findViewById(R.id.exo_repeat_toggle);
if (repeatToggleButton != null) {
repeatToggleButton.setOnClickListener(componentListener);
}
shuffleButton = findViewById(R.id.exo_shuffle);
if (shuffleButton != null) {
shuffleButton.setOnClickListener(componentListener);
}
buttonAlphaEnabled =
(float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_enabled) / 100;
buttonAlphaDisabled =
(float) resources.getInteger(R.integer.exo_media_button_opacity_percentage_disabled) / 100;
vrButton = findViewById(R.id.exo_vr);
if (vrButton != null) {
vrButton.setImageDrawable(getDrawable(context, resources, vrDrawableResId));
updateButton(/* enabled= */ false, vrButton);
}
controlViewLayoutManager = new PlayerControlViewLayoutManager(this);
controlViewLayoutManager.setAnimationEnabled(animationEnabled);
String[] settingTexts = new String[2];
Drawable[] settingIcons = new Drawable[2];
settingTexts[SETTINGS_PLAYBACK_SPEED_POSITION] =
resources.getString(R.string.exo_controls_playback_speed);
settingIcons[SETTINGS_PLAYBACK_SPEED_POSITION] =
getDrawable(context, resources, R.drawable.exo_styled_controls_speed);
settingTexts[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
resources.getString(R.string.exo_track_selection_title_audio);
settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
getDrawable(context, resources, R.drawable.exo_styled_controls_audiotrack);
settingsAdapter = new SettingsAdapter(settingTexts, settingIcons);
settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset);
settingsView =
(RecyclerView)
LayoutInflater.from(context)
.inflate(R.layout.exo_styled_settings_list, /* root= */ null);
settingsView.setAdapter(settingsAdapter);
settingsView.setLayoutManager(new LinearLayoutManager(getContext()));
settingsWindow =
new PopupWindow(settingsView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true);
if (Util.SDK_INT < 23) {
// Work around issue where tapping outside of the menu area or pressing the back button
// doesn't dismiss the menu as expected. See: https://github.com/google/ExoPlayer/issues/8272.
settingsWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
settingsWindow.setOnDismissListener(componentListener);
needToHideBars = true;
trackNameProvider = new DefaultTrackNameProvider(getResources());
subtitleOnButtonDrawable = getDrawable(context, resources, subtitleOnDrawableResId);
subtitleOffButtonDrawable = getDrawable(context, resources, subtitleOffDrawableResId);
subtitleOnContentDescription =
resources.getString(R.string.exo_controls_cc_enabled_description);
subtitleOffContentDescription =
resources.getString(R.string.exo_controls_cc_disabled_description);
textTrackSelectionAdapter = new TextTrackSelectionAdapter();
audioTrackSelectionAdapter = new AudioTrackSelectionAdapter();
playbackSpeedAdapter =
new PlaybackSpeedAdapter(
resources.getStringArray(R.array.exo_controls_playback_speeds), PLAYBACK_SPEEDS);
playButtonDrawable = getDrawable(context, resources, playDrawableResId);
pauseButtonDrawable = getDrawable(context, resources, pauseDrawableResId);
fullScreenExitDrawable = getDrawable(context, resources, fullScreenExitDrawableResId);
fullScreenEnterDrawable = getDrawable(context, resources, fullScreenEnterDrawableResId);
repeatOffButtonDrawable = getDrawable(context, resources, repeatOffDrawableResId);
repeatOneButtonDrawable = getDrawable(context, resources, repeatOneDrawableResId);
repeatAllButtonDrawable = getDrawable(context, resources, repeatAllDrawableResId);
shuffleOnButtonDrawable = getDrawable(context, resources, shuffleOnDrawableResId);
shuffleOffButtonDrawable = getDrawable(context, resources, shuffleOffDrawableResId);
fullScreenExitContentDescription =
resources.getString(R.string.exo_controls_fullscreen_exit_description);
fullScreenEnterContentDescription =
resources.getString(R.string.exo_controls_fullscreen_enter_description);
repeatOffButtonContentDescription =
resources.getString(R.string.exo_controls_repeat_off_description);
repeatOneButtonContentDescription =
resources.getString(R.string.exo_controls_repeat_one_description);
repeatAllButtonContentDescription =
resources.getString(R.string.exo_controls_repeat_all_description);
shuffleOnContentDescription = resources.getString(R.string.exo_controls_shuffle_on_description);
shuffleOffContentDescription =
resources.getString(R.string.exo_controls_shuffle_off_description);
// TODO(insun) : Make showing bottomBar configurable. (ex. show_bottom_bar attribute).
ViewGroup bottomBar = findViewById(R.id.exo_bottom_bar);
controlViewLayoutManager.setShowButton(bottomBar, true);
controlViewLayoutManager.setShowButton(fastForwardButton, showFastForwardButton);
controlViewLayoutManager.setShowButton(rewindButton, showRewindButton);
controlViewLayoutManager.setShowButton(previousButton, showPreviousButton);
controlViewLayoutManager.setShowButton(nextButton, showNextButton);
controlViewLayoutManager.setShowButton(shuffleButton, showShuffleButton);
controlViewLayoutManager.setShowButton(subtitleButton, showSubtitleButton);
controlViewLayoutManager.setShowButton(vrButton, showVrButton);
controlViewLayoutManager.setShowButton(
repeatToggleButton, repeatToggleModes != RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE);
addOnLayoutChangeListener(this::onLayoutChange);
}
/**
* Returns the {@link Player} currently being controlled by this view, or null if no player is
* set.
*/
@Nullable
public Player getPlayer() {
return player;
}
/**
* Sets the {@link Player} to control.
*
* @param player The {@link Player} to control, or {@code null} to detach the current player. Only
* players which are accessed on the main thread are supported ({@code
* player.getApplicationLooper() == Looper.getMainLooper()}).
*/
public void setPlayer(@Nullable Player player) {
Assertions.checkState(Looper.myLooper() == Looper.getMainLooper());
Assertions.checkArgument(
player == null || player.getApplicationLooper() == Looper.getMainLooper());
if (this.player == player) {
return;
}
if (this.player != null) {
this.player.removeListener(componentListener);
}
this.player = player;
if (player != null) {
player.addListener(componentListener);
}
updateAll();
}
/**
* @deprecated Replace multi-window time bar display by merging source windows together instead,
* for example using ExoPlayer's {@code ConcatenatingMediaSource2}.
*/
@Deprecated
public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
this.showMultiWindowTimeBar = showMultiWindowTimeBar;
updateTimeline();
}
/**
* Sets whether a play button is shown if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*
* <p>The default is {@code true}.
*
* @param showPlayButtonIfSuppressed Whether to show a play button if playback is {@linkplain
* Player#getPlaybackSuppressionReason() suppressed}.
*/
public void setShowPlayButtonIfPlaybackIsSuppressed(boolean showPlayButtonIfSuppressed) {
this.showPlayButtonIfSuppressed = showPlayButtonIfSuppressed;
updatePlayPauseButton();
}
/**
* Sets the millisecond positions of extra ad markers relative to the start of the window (or
* timeline, if in multi-window mode) and whether each extra ad has been played or not. The
* markers are shown in addition to any ad markers for ads in the player's timeline.
*
* @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or
* {@code null} to show no extra ad markers.
* @param extraPlayedAdGroups Whether each ad has been played. Must be the same length as {@code
* extraAdGroupTimesMs}, or {@code null} if {@code extraAdGroupTimesMs} is {@code null}.
*/
public void setExtraAdGroupMarkers(
@Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {
if (extraAdGroupTimesMs == null) {
this.extraAdGroupTimesMs = new long[0];
this.extraPlayedAdGroups = new boolean[0];
} else {
extraPlayedAdGroups = checkNotNull(extraPlayedAdGroups);
Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length);
this.extraAdGroupTimesMs = extraAdGroupTimesMs;
this.extraPlayedAdGroups = extraPlayedAdGroups;
}
updateTimeline();
}
/**
* @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link
* PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)}
* instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
* is deprecated.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void addVisibilityListener(VisibilityListener listener) {
checkNotNull(listener);
visibilityListeners.add(listener);
}
/**
* @deprecated Register a {@link PlayerView.ControllerVisibilityListener} via {@link
* PlayerView#setControllerVisibilityListener(PlayerView.ControllerVisibilityListener)}
* instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
* is deprecated.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void removeVisibilityListener(VisibilityListener listener) {
visibilityListeners.remove(listener);
}
/**
* Sets the {@link ProgressUpdateListener}.
*
* @param listener The listener to be notified about when progress is updated.
*/
public void setProgressUpdateListener(@Nullable ProgressUpdateListener listener) {
this.progressUpdateListener = listener;
}
/**
* Sets whether the rewind button is shown.
*
* @param showRewindButton Whether the rewind button is shown.
*/
public void setShowRewindButton(boolean showRewindButton) {
controlViewLayoutManager.setShowButton(rewindButton, showRewindButton);
updateNavigation();
}
/**
* Sets whether the fast forward button is shown.
*
* @param showFastForwardButton Whether the fast forward button is shown.
*/
public void setShowFastForwardButton(boolean showFastForwardButton) {
controlViewLayoutManager.setShowButton(fastForwardButton, showFastForwardButton);
updateNavigation();
}
/**
* Sets whether the previous button is shown.
*
* @param showPreviousButton Whether the previous button is shown.
*/
public void setShowPreviousButton(boolean showPreviousButton) {
controlViewLayoutManager.setShowButton(previousButton, showPreviousButton);
updateNavigation();
}
/**
* Sets whether the next button is shown.
*
* @param showNextButton Whether the next button is shown.
*/
public void setShowNextButton(boolean showNextButton) {
controlViewLayoutManager.setShowButton(nextButton, showNextButton);
updateNavigation();
}
/**
* Returns the playback controls timeout. The playback controls are automatically hidden after
* this duration of time has elapsed without user input.
*
* @return The duration in milliseconds. A non-positive value indicates that the controls will
* remain visible indefinitely.
*/
public int getShowTimeoutMs() {
return showTimeoutMs;
}
/**
* Sets the playback controls timeout. The playback controls are automatically hidden after this
* duration of time has elapsed without user input.
*
* @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
* to remain visible indefinitely.
*/
public void setShowTimeoutMs(int showTimeoutMs) {
this.showTimeoutMs = showTimeoutMs;
if (isFullyVisible()) {
controlViewLayoutManager.resetHideCallbacks();
}
}
/**
* Returns which repeat toggle modes are enabled.
*
* @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}.
*/
public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() {
return repeatToggleModes;
}
/**
* Sets which repeat toggle modes are enabled.
*
* @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
*/
public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
this.repeatToggleModes = repeatToggleModes;
if (player != null && player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
@Player.RepeatMode int currentMode = player.getRepeatMode();
if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
&& currentMode != Player.REPEAT_MODE_OFF) {
player.setRepeatMode(Player.REPEAT_MODE_OFF);
} else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
&& currentMode == Player.REPEAT_MODE_ALL) {
player.setRepeatMode(Player.REPEAT_MODE_ONE);
} else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
&& currentMode == Player.REPEAT_MODE_ONE) {
player.setRepeatMode(Player.REPEAT_MODE_ALL);
}
}
controlViewLayoutManager.setShowButton(
repeatToggleButton, repeatToggleModes != RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE);
updateRepeatModeButton();
}
/** Returns whether the shuffle button is shown. */
public boolean getShowShuffleButton() {
return controlViewLayoutManager.getShowButton(shuffleButton);
}
/**
* Sets whether the shuffle button is shown.
*
* @param showShuffleButton Whether the shuffle button is shown.
*/
public void setShowShuffleButton(boolean showShuffleButton) {
controlViewLayoutManager.setShowButton(shuffleButton, showShuffleButton);
updateShuffleButton();
}
/** Returns whether the subtitle button is shown. */
public boolean getShowSubtitleButton() {
return controlViewLayoutManager.getShowButton(subtitleButton);
}
/**
* Sets whether the subtitle button is shown.
*
* @param showSubtitleButton Whether the subtitle button is shown.
*/
public void setShowSubtitleButton(boolean showSubtitleButton) {
controlViewLayoutManager.setShowButton(subtitleButton, showSubtitleButton);
}
/** Returns whether the VR button is shown. */
public boolean getShowVrButton() {
return controlViewLayoutManager.getShowButton(vrButton);
}
/**
* Sets whether the VR button is shown.
*
* @param showVrButton Whether the VR button is shown.
*/
public void setShowVrButton(boolean showVrButton) {
controlViewLayoutManager.setShowButton(vrButton, showVrButton);
}
/**
* Sets listener for the VR button.
*
* @param onClickListener Listener for the VR button, or null to clear the listener.
*/
public void setVrButtonListener(@Nullable OnClickListener onClickListener) {
if (vrButton != null) {
vrButton.setOnClickListener(onClickListener);
updateButton(onClickListener != null, vrButton);
}
}
/**
* Sets whether an animation is used to show and hide the playback controls.
*
* @param animationEnabled Whether an animation is applied to show and hide playback controls.
*/
public void setAnimationEnabled(boolean animationEnabled) {
controlViewLayoutManager.setAnimationEnabled(animationEnabled);
}
/** Returns whether an animation is used to show and hide the playback controls. */
public boolean isAnimationEnabled() {
return controlViewLayoutManager.isAnimationEnabled();
}
/**
* Sets the minimum interval between time bar position updates.
*
* <p>Note that smaller intervals, e.g. 33ms, will result in a smooth movement but will use more
* CPU resources while the time bar is visible, whereas larger intervals, e.g. 200ms, will result
* in a step-wise update with less CPU usage.
*
* @param minUpdateIntervalMs The minimum interval between time bar position updates, in
* milliseconds.
*/
public void setTimeBarMinUpdateInterval(int minUpdateIntervalMs) {
// Do not accept values below 16ms (60fps) and larger than the maximum update interval.
timeBarMinUpdateIntervalMs =
Util.constrainValue(minUpdateIntervalMs, 16, MAX_UPDATE_INTERVAL_MS);
}
/**
* @deprecated Register a {@link PlayerView.FullscreenButtonClickListener} via {@link
* PlayerView#setFullscreenButtonClickListener(PlayerView.FullscreenButtonClickListener)}
* instead. Using {@link PlayerControlView} as a standalone class without {@link PlayerView}
* is deprecated.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setOnFullScreenModeChangedListener(
@Nullable OnFullScreenModeChangedListener listener) {
onFullScreenModeChangedListener = listener;
updateFullScreenButtonVisibility(fullScreenButton, listener != null);
updateFullScreenButtonVisibility(minimalFullScreenButton, listener != null);
}
/**
* Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
* be automatically hidden after this duration of time has elapsed without user input.
*/
public void show() {
controlViewLayoutManager.show();
}
/** Hides the controller. */
public void hide() {
controlViewLayoutManager.hide();
}
/** Hides the controller without any animation. */
public void hideImmediately() {
controlViewLayoutManager.hideImmediately();
}
/** Returns whether the controller is fully visible, which means all UI controls are visible. */
public boolean isFullyVisible() {
return controlViewLayoutManager.isFullyVisible();
}
/** Returns whether the controller is currently visible. */
public boolean isVisible() {
return getVisibility() == VISIBLE;
}
@SuppressWarnings("deprecation") // Calling the deprecated listener for now.
/* package */ void notifyOnVisibilityChange() {
for (VisibilityListener visibilityListener : visibilityListeners) {
visibilityListener.onVisibilityChange(getVisibility());
}
}
/* package */ void updateAll() {
updatePlayPauseButton();
updateNavigation();
updateRepeatModeButton();
updateShuffleButton();
updateTrackLists();
updatePlaybackSpeedList();
updateTimeline();
}
private void updatePlayPauseButton() {
if (!isVisible() || !isAttachedToWindow) {
return;
}
if (playPauseButton != null) {
boolean shouldShowPlayButton = Util.shouldShowPlayButton(player, showPlayButtonIfSuppressed);
Drawable drawable = shouldShowPlayButton ? playButtonDrawable : pauseButtonDrawable;
@StringRes
int stringRes =
shouldShowPlayButton
? R.string.exo_controls_play_description
: R.string.exo_controls_pause_description;
playPauseButton.setImageDrawable(drawable);
playPauseButton.setContentDescription(resources.getString(stringRes));
boolean enablePlayPause = shouldEnablePlayPauseButton();
updateButton(enablePlayPause, playPauseButton);
}
}
private void updateNavigation() {
if (!isVisible() || !isAttachedToWindow) {
return;
}
@Nullable Player player = this.player;
boolean enableSeeking = false;
boolean enablePrevious = false;
boolean enableRewind = false;
boolean enableFastForward = false;
boolean enableNext = false;
if (player != null) {
enableSeeking =
(showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window))
? player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)
: player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
enablePrevious = player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS);
enableRewind = player.isCommandAvailable(COMMAND_SEEK_BACK);
enableFastForward = player.isCommandAvailable(COMMAND_SEEK_FORWARD);
enableNext = player.isCommandAvailable(COMMAND_SEEK_TO_NEXT);
}
if (enableRewind) {
updateRewindButton();
}
if (enableFastForward) {
updateFastForwardButton();
}
updateButton(enablePrevious, previousButton);
updateButton(enableRewind, rewindButton);
updateButton(enableFastForward, fastForwardButton);
updateButton(enableNext, nextButton);
if (timeBar != null) {
timeBar.setEnabled(enableSeeking);
}
}
private void updateRewindButton() {
long rewindMs =
player != null ? player.getSeekBackIncrement() : C.DEFAULT_SEEK_BACK_INCREMENT_MS;
int rewindSec = (int) (rewindMs / 1_000);
if (rewindButtonTextView != null) {
rewindButtonTextView.setText(String.valueOf(rewindSec));
}
if (rewindButton != null) {
rewindButton.setContentDescription(
resources.getQuantityString(
R.plurals.exo_controls_rewind_by_amount_description, rewindSec, rewindSec));
}
}
private void updateFastForwardButton() {
long fastForwardMs =
player != null ? player.getSeekForwardIncrement() : C.DEFAULT_SEEK_FORWARD_INCREMENT_MS;
int fastForwardSec = (int) (fastForwardMs / 1_000);
if (fastForwardButtonTextView != null) {
fastForwardButtonTextView.setText(String.valueOf(fastForwardSec));
}
if (fastForwardButton != null) {
fastForwardButton.setContentDescription(
resources.getQuantityString(
R.plurals.exo_controls_fastforward_by_amount_description,
fastForwardSec,
fastForwardSec));
}
}
private void updateRepeatModeButton() {
if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) {
return;
}
if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
updateButton(/* enabled= */ false, repeatToggleButton);
return;
}
@Nullable Player player = this.player;
if (player == null || !player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
updateButton(/* enabled= */ false, repeatToggleButton);
repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
return;
}
updateButton(/* enabled= */ true, repeatToggleButton);
switch (player.getRepeatMode()) {
case Player.REPEAT_MODE_OFF:
repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
break;
case Player.REPEAT_MODE_ONE:
repeatToggleButton.setImageDrawable(repeatOneButtonDrawable);
repeatToggleButton.setContentDescription(repeatOneButtonContentDescription);
break;
case Player.REPEAT_MODE_ALL:
repeatToggleButton.setImageDrawable(repeatAllButtonDrawable);
repeatToggleButton.setContentDescription(repeatAllButtonContentDescription);
break;
default:
// Never happens.
}
}
private void updateShuffleButton() {
if (!isVisible() || !isAttachedToWindow || shuffleButton == null) {
return;
}
@Nullable Player player = this.player;
if (!controlViewLayoutManager.getShowButton(shuffleButton)) {
updateButton(/* enabled= */ false, shuffleButton);
} else if (player == null || !player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) {
updateButton(/* enabled= */ false, shuffleButton);
shuffleButton.setImageDrawable(shuffleOffButtonDrawable);
shuffleButton.setContentDescription(shuffleOffContentDescription);
} else {
updateButton(/* enabled= */ true, shuffleButton);
shuffleButton.setImageDrawable(
player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable);
shuffleButton.setContentDescription(
player.getShuffleModeEnabled()
? shuffleOnContentDescription
: shuffleOffContentDescription);
}
}
private void updateTrackLists() {
initTrackSelectionAdapter();
updateButton(textTrackSelectionAdapter.getItemCount() > 0, subtitleButton);
updateSettingsButton();
}
private void initTrackSelectionAdapter() {
textTrackSelectionAdapter.clear();
audioTrackSelectionAdapter.clear();
if (player == null
|| !player.isCommandAvailable(COMMAND_GET_TRACKS)
|| !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
return;
}
Tracks tracks = player.getCurrentTracks();
audioTrackSelectionAdapter.init(gatherSupportedTrackInfosOfType(tracks, C.TRACK_TYPE_AUDIO));
if (controlViewLayoutManager.getShowButton(subtitleButton)) {
textTrackSelectionAdapter.init(gatherSupportedTrackInfosOfType(tracks, C.TRACK_TYPE_TEXT));
} else {
textTrackSelectionAdapter.init(ImmutableList.of());
}
}
private ImmutableList<TrackInformation> gatherSupportedTrackInfosOfType(
Tracks tracks, @C.TrackType int trackType) {
ImmutableList.Builder<TrackInformation> trackInfos = new ImmutableList.Builder<>();
List<Tracks.Group> trackGroups = tracks.getGroups();
for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.size(); trackGroupIndex++) {
Tracks.Group trackGroup = trackGroups.get(trackGroupIndex);
if (trackGroup.getType() != trackType) {
continue;
}
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
if (!trackGroup.isTrackSupported(trackIndex)) {
continue;
}
Format trackFormat = trackGroup.getTrackFormat(trackIndex);
if ((trackFormat.selectionFlags & C.SELECTION_FLAG_FORCED) != 0) {
continue;
}
String trackName = trackNameProvider.getTrackName(trackFormat);
trackInfos.add(new TrackInformation(tracks, trackGroupIndex, trackIndex, trackName));
}
}
return trackInfos.build();
}
private void updateTimeline() {
@Nullable Player player = this.player;
if (player == null) {
return;
}
multiWindowTimeBar = showMultiWindowTimeBar && canShowMultiWindowTimeBar(player, window);
currentWindowOffset = 0;
long durationUs = 0;
int adGroupCount = 0;
Timeline timeline =
player.isCommandAvailable(COMMAND_GET_TIMELINE)
? player.getCurrentTimeline()
: Timeline.EMPTY;
if (!timeline.isEmpty()) {
int currentWindowIndex = player.getCurrentMediaItemIndex();
int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex;
int lastWindowIndex = multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex;
for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {
if (i == currentWindowIndex) {
currentWindowOffset = Util.usToMs(durationUs);
}
timeline.getWindow(i, window);
if (window.durationUs == C.TIME_UNSET) {
Assertions.checkState(!multiWindowTimeBar);
break;
}
for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) {
timeline.getPeriod(j, period);
int removedGroups = period.getRemovedAdGroupCount();
int totalGroups = period.getAdGroupCount();
for (int adGroupIndex = removedGroups; adGroupIndex < totalGroups; adGroupIndex++) {
long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex);
if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {
if (period.durationUs == C.TIME_UNSET) {
// Don't show ad markers for postrolls in periods with unknown duration.
continue;
}
adGroupTimeInPeriodUs = period.durationUs;
}
long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs();
if (adGroupTimeInWindowUs >= 0) {
if (adGroupCount == adGroupTimesMs.length) {
int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2;
adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);
playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);
}
adGroupTimesMs[adGroupCount] = Util.usToMs(durationUs + adGroupTimeInWindowUs);
playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex);
adGroupCount++;
}
}
}
durationUs += window.durationUs;
}
} else if (player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) {
long playerDurationMs = player.getContentDuration();
if (playerDurationMs != C.TIME_UNSET) {
durationUs = msToUs(playerDurationMs);
}
}
long durationMs = Util.usToMs(durationUs);
if (durationView != null) {
durationView.setText(Util.getStringForTime(formatBuilder, formatter, durationMs));
}
if (timeBar != null) {
timeBar.setDuration(durationMs);
int extraAdGroupCount = extraAdGroupTimesMs.length;
int totalAdGroupCount = adGroupCount + extraAdGroupCount;
if (totalAdGroupCount > adGroupTimesMs.length) {
adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount);
playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount);
}
System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount);
System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount);
timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount);
}
updateProgress();
}
private void updateProgress() {
if (!isVisible() || !isAttachedToWindow) {
return;
}
@Nullable Player player = this.player;
long position = 0;
long bufferedPosition = 0;
if (player != null && player.isCommandAvailable(COMMAND_GET_CURRENT_MEDIA_ITEM)) {
position = currentWindowOffset + player.getContentPosition();
bufferedPosition = currentWindowOffset + player.getContentBufferedPosition();
}
if (positionView != null && !scrubbing) {
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
}
if (timeBar != null) {
timeBar.setPosition(position);
timeBar.setBufferedPosition(bufferedPosition);
}
if (progressUpdateListener != null) {
progressUpdateListener.onProgressUpdate(position, bufferedPosition);
}
// Cancel any pending updates and schedule a new one if necessary.
removeCallbacks(updateProgressAction);
int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState();
if (player != null && player.isPlaying()) {
long mediaTimeDelayMs =
timeBar != null ? timeBar.getPreferredUpdateDelay() : MAX_UPDATE_INTERVAL_MS;
// Limit delay to the start of the next full second to ensure position display is smooth.
long mediaTimeUntilNextFullSecondMs = 1000 - position % 1000;
mediaTimeDelayMs = Math.min(mediaTimeDelayMs, mediaTimeUntilNextFullSecondMs);
// Calculate the delay until the next update in real time, taking playback speed into account.
float playbackSpeed = player.getPlaybackParameters().speed;
long delayMs =
playbackSpeed > 0 ? (long) (mediaTimeDelayMs / playbackSpeed) : MAX_UPDATE_INTERVAL_MS;
// Constrain the delay to avoid too frequent / infrequent updates.
delayMs = Util.constrainValue(delayMs, timeBarMinUpdateIntervalMs, MAX_UPDATE_INTERVAL_MS);
postDelayed(updateProgressAction, delayMs);
} else if (playbackState != Player.STATE_ENDED && playbackState != Player.STATE_IDLE) {
postDelayed(updateProgressAction, MAX_UPDATE_INTERVAL_MS);
}
}
private void updatePlaybackSpeedList() {
if (player == null) {
return;
}
playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed);
settingsAdapter.setSubTextAtPosition(
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText());
updateSettingsButton();
}
private void updateSettingsButton() {
updateButton(settingsAdapter.hasSettingsToShow(), settingsButton);
}
private void updateSettingsWindowSize() {
settingsView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
int maxWidth = getWidth() - settingsWindowMargin * 2;
int itemWidth = settingsView.getMeasuredWidth();
int width = Math.min(itemWidth, maxWidth);
settingsWindow.setWidth(width);
int maxHeight = getHeight() - settingsWindowMargin * 2;
int totalHeight = settingsView.getMeasuredHeight();
int height = Math.min(maxHeight, totalHeight);
settingsWindow.setHeight(height);
}
private void displaySettingsWindow(RecyclerView.Adapter<?> adapter, View anchorView) {
settingsView.setAdapter(adapter);
updateSettingsWindowSize();
needToHideBars = false;
settingsWindow.dismiss();
needToHideBars = true;
int xoff = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
int yoff = -settingsWindow.getHeight() - settingsWindowMargin;
settingsWindow.showAsDropDown(anchorView, xoff, yoff);
}
private void setPlaybackSpeed(float speed) {
if (player == null || !player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH)) {
return;
}
player.setPlaybackParameters(player.getPlaybackParameters().withSpeed(speed));
}
/* package */ void requestPlayPauseFocus() {
if (playPauseButton != null) {
playPauseButton.requestFocus();
}
}
private void updateButton(boolean enabled, @Nullable View view) {
if (view == null) {
return;
}
view.setEnabled(enabled);
view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled);
}
private void seekToTimeBarPosition(Player player, long positionMs) {
if (multiWindowTimeBar) {
if (player.isCommandAvailable(COMMAND_GET_TIMELINE)
&& player.isCommandAvailable(COMMAND_SEEK_TO_MEDIA_ITEM)) {
Timeline timeline = player.getCurrentTimeline();
int windowCount = timeline.getWindowCount();
int windowIndex = 0;
while (true) {
long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs();
if (positionMs < windowDurationMs) {
break;
} else if (windowIndex == windowCount - 1) {
// Seeking past the end of the last window should seek to the end of the timeline.
positionMs = windowDurationMs;
break;
}
positionMs -= windowDurationMs;
windowIndex++;
}
player.seekTo(windowIndex, positionMs);
}
} else if (player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM)) {
player.seekTo(positionMs);
}
updateProgress();
}
private void onFullScreenButtonClicked(View v) {
if (onFullScreenModeChangedListener == null) {
return;
}
isFullScreen = !isFullScreen;
updateFullScreenButtonForState(fullScreenButton, isFullScreen);
updateFullScreenButtonForState(minimalFullScreenButton, isFullScreen);
if (onFullScreenModeChangedListener != null) {
onFullScreenModeChangedListener.onFullScreenModeChanged(isFullScreen);
}
}
private void updateFullScreenButtonForState(
@Nullable ImageView fullScreenButton, boolean isFullScreen) {
if (fullScreenButton == null) {
return;
}
if (isFullScreen) {
fullScreenButton.setImageDrawable(fullScreenExitDrawable);
fullScreenButton.setContentDescription(fullScreenExitContentDescription);
} else {
fullScreenButton.setImageDrawable(fullScreenEnterDrawable);
fullScreenButton.setContentDescription(fullScreenEnterContentDescription);
}
}
private void onSettingViewClicked(int position) {
if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
displaySettingsWindow(playbackSpeedAdapter, checkNotNull(settingsButton));
} else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
displaySettingsWindow(audioTrackSelectionAdapter, checkNotNull(settingsButton));
} else {
settingsWindow.dismiss();
}
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
controlViewLayoutManager.onAttachedToWindow();
isAttachedToWindow = true;
if (isFullyVisible()) {
controlViewLayoutManager.resetHideCallbacks();
}
updateAll();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
controlViewLayoutManager.onDetachedFromWindow();
isAttachedToWindow = false;
removeCallbacks(updateProgressAction);
controlViewLayoutManager.removeHideCallbacks();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event);
}
/**
* Called to process media key events. Any {@link KeyEvent} can be passed but only media key
* events will be handled.
*
* @param event A key event.
* @return Whether the key event was handled.
*/
public boolean dispatchMediaKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
@Nullable Player player = this.player;
if (player == null || !isHandledMediaKey(keyCode)) {
return false;
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) {
if (player.getPlaybackState() != Player.STATE_ENDED
&& player.isCommandAvailable(COMMAND_SEEK_FORWARD)) {
player.seekForward();
}
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
&& player.isCommandAvailable(COMMAND_SEEK_BACK)) {
player.seekBack();
} else if (event.getRepeatCount() == 0) {
switch (keyCode) {
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
case KeyEvent.KEYCODE_HEADSETHOOK:
Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed);
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
Util.handlePlayButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
Util.handlePauseButtonAction(player);
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) {
player.seekToNext();
}
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) {
player.seekToPrevious();
}
break;
default:
break;
}
}
}
return true;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
controlViewLayoutManager.onLayout(changed, left, top, right, bottom);
}
private void onLayoutChange(
View v,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
int width = right - left;
int height = bottom - top;
int oldWidth = oldRight - oldLeft;
int oldHeight = oldBottom - oldTop;
if ((width != oldWidth || height != oldHeight) && settingsWindow.isShowing()) {
updateSettingsWindowSize();
int xOffset = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
int yOffset = -settingsWindow.getHeight() - settingsWindowMargin;
settingsWindow.update(v, xOffset, yOffset, -1, -1);
}
}
private boolean shouldEnablePlayPauseButton() {
return player != null
&& player.isCommandAvailable(COMMAND_PLAY_PAUSE)
&& (!player.isCommandAvailable(COMMAND_GET_TIMELINE)
|| !player.getCurrentTimeline().isEmpty());
}
@SuppressLint("InlinedApi")
private static boolean isHandledMediaKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|| keyCode == KeyEvent.KEYCODE_MEDIA_REWIND
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|| keyCode == KeyEvent.KEYCODE_HEADSETHOOK
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY
|| keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
|| keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
|| keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;
}
/**
* Returns whether the specified {@code player} can be shown on a multi-window time bar.
*
* @param player The {@link Player} to check.
* @param window A scratch {@link Timeline.Window} instance.
* @return Whether the specified timeline can be shown on a multi-window time bar.
*/
private static boolean canShowMultiWindowTimeBar(Player player, Timeline.Window window) {
if (!player.isCommandAvailable(COMMAND_GET_TIMELINE)) {
return false;
}
Timeline timeline = player.getCurrentTimeline();
int windowCount = timeline.getWindowCount();
if (windowCount <= 1 || windowCount > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {
return false;
}
for (int i = 0; i < windowCount; i++) {
if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) {
return false;
}
}
return true;
}
private static void initializeFullScreenButton(View fullScreenButton, OnClickListener listener) {
if (fullScreenButton == null) {
return;
}
fullScreenButton.setVisibility(GONE);
fullScreenButton.setOnClickListener(listener);
}
private static void updateFullScreenButtonVisibility(
@Nullable View fullScreenButton, boolean visible) {
if (fullScreenButton == null) {
return;
}
if (visible) {
fullScreenButton.setVisibility(VISIBLE);
} else {
fullScreenButton.setVisibility(GONE);
}
}
@SuppressWarnings("ResourceType")
private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(
TypedArray a, @RepeatModeUtil.RepeatToggleModes int defaultValue) {
return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, defaultValue);
}
private final class ComponentListener
implements Player.Listener,
TimeBar.OnScrubListener,
OnClickListener,
PopupWindow.OnDismissListener {
@Override
public void onEvents(Player player, Events events) {
if (events.containsAny(
EVENT_PLAYBACK_STATE_CHANGED,
EVENT_PLAY_WHEN_READY_CHANGED,
EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updatePlayPauseButton();
}
if (events.containsAny(
EVENT_PLAYBACK_STATE_CHANGED,
EVENT_PLAY_WHEN_READY_CHANGED,
EVENT_IS_PLAYING_CHANGED,
EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updateProgress();
}
if (events.containsAny(EVENT_REPEAT_MODE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updateRepeatModeButton();
}
if (events.containsAny(
EVENT_SHUFFLE_MODE_ENABLED_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updateShuffleButton();
}
if (events.containsAny(
EVENT_REPEAT_MODE_CHANGED,
EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
EVENT_POSITION_DISCONTINUITY,
EVENT_TIMELINE_CHANGED,
EVENT_SEEK_BACK_INCREMENT_CHANGED,
EVENT_SEEK_FORWARD_INCREMENT_CHANGED,
EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updateNavigation();
}
if (events.containsAny(
EVENT_POSITION_DISCONTINUITY, EVENT_TIMELINE_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updateTimeline();
}
if (events.containsAny(EVENT_PLAYBACK_PARAMETERS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updatePlaybackSpeedList();
}
if (events.containsAny(EVENT_TRACKS_CHANGED, EVENT_AVAILABLE_COMMANDS_CHANGED)) {
updateTrackLists();
}
}
@Override
public void onScrubStart(TimeBar timeBar, long position) {
scrubbing = true;
if (positionView != null) {
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
}
controlViewLayoutManager.removeHideCallbacks();
}
@Override
public void onScrubMove(TimeBar timeBar, long position) {
if (positionView != null) {
positionView.setText(Util.getStringForTime(formatBuilder, formatter, position));
}
}
@Override
public void onScrubStop(TimeBar timeBar, long position, boolean canceled) {
scrubbing = false;
if (!canceled && player != null) {
seekToTimeBarPosition(player, position);
}
controlViewLayoutManager.resetHideCallbacks();
}
@Override
public void onDismiss() {
if (needToHideBars) {
controlViewLayoutManager.resetHideCallbacks();
}
}
@Override
public void onClick(View view) {
@Nullable Player player = PlayerControlView.this.player;
if (player == null) {
return;
}
controlViewLayoutManager.resetHideCallbacks();
if (nextButton == view) {
if (player.isCommandAvailable(COMMAND_SEEK_TO_NEXT)) {
player.seekToNext();
}
} else if (previousButton == view) {
if (player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS)) {
player.seekToPrevious();
}
} else if (fastForwardButton == view) {
if (player.getPlaybackState() != Player.STATE_ENDED
&& player.isCommandAvailable(COMMAND_SEEK_FORWARD)) {
player.seekForward();
}
} else if (rewindButton == view) {
if (player.isCommandAvailable(COMMAND_SEEK_BACK)) {
player.seekBack();
}
} else if (playPauseButton == view) {
Util.handlePlayPauseButtonAction(player, showPlayButtonIfSuppressed);
} else if (repeatToggleButton == view) {
if (player.isCommandAvailable(COMMAND_SET_REPEAT_MODE)) {
player.setRepeatMode(
RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));
}
} else if (shuffleButton == view) {
if (player.isCommandAvailable(COMMAND_SET_SHUFFLE_MODE)) {
player.setShuffleModeEnabled(!player.getShuffleModeEnabled());
}
} else if (settingsButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(settingsAdapter, settingsButton);
} else if (playbackSpeedButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(playbackSpeedAdapter, playbackSpeedButton);
} else if (audioTrackButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(audioTrackSelectionAdapter, audioTrackButton);
} else if (subtitleButton == view) {
controlViewLayoutManager.removeHideCallbacks();
displaySettingsWindow(textTrackSelectionAdapter, subtitleButton);
}
}
}
private class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> {
private final String[] mainTexts;
private final String[] subTexts;
private final Drawable[] iconIds;
public SettingsAdapter(String[] mainTexts, Drawable[] iconIds) {
this.mainTexts = mainTexts;
this.subTexts = new String[mainTexts.length];
this.iconIds = iconIds;
}
@Override
public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v =
LayoutInflater.from(getContext())
.inflate(R.layout.exo_styled_settings_list_item, parent, /* attachToRoot= */ false);
return new SettingViewHolder(v);
}
@Override
public void onBindViewHolder(SettingViewHolder holder, int position) {
if (shouldShowSetting(position)) {
holder.itemView.setLayoutParams(
new RecyclerView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
} else {
holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(0, 0));
}
holder.mainTextView.setText(mainTexts[position]);
if (subTexts[position] == null) {
holder.subTextView.setVisibility(GONE);
} else {
holder.subTextView.setText(subTexts[position]);
}
if (iconIds[position] == null) {
holder.iconView.setVisibility(GONE);
} else {
holder.iconView.setImageDrawable(iconIds[position]);
}
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemCount() {
return mainTexts.length;
}
public void setSubTextAtPosition(int position, String subText) {
this.subTexts[position] = subText;
}
public boolean hasSettingsToShow() {
return shouldShowSetting(SETTINGS_AUDIO_TRACK_SELECTION_POSITION)
|| shouldShowSetting(SETTINGS_PLAYBACK_SPEED_POSITION);
}
private boolean shouldShowSetting(int position) {
if (player == null) {
return false;
}
switch (position) {
case SETTINGS_AUDIO_TRACK_SELECTION_POSITION:
return player.isCommandAvailable(COMMAND_GET_TRACKS)
&& player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS);
case SETTINGS_PLAYBACK_SPEED_POSITION:
return player.isCommandAvailable(COMMAND_SET_SPEED_AND_PITCH);
default:
return true;
}
}
}
private final class SettingViewHolder extends RecyclerView.ViewHolder {
private final TextView mainTextView;
private final TextView subTextView;
private final ImageView iconView;
public SettingViewHolder(View itemView) {
super(itemView);
if (Util.SDK_INT < 26) {
// Workaround for https://github.com/google/ExoPlayer/issues/9061.
itemView.setFocusable(true);
}
mainTextView = itemView.findViewById(R.id.exo_main_text);
subTextView = itemView.findViewById(R.id.exo_sub_text);
iconView = itemView.findViewById(R.id.exo_icon);
itemView.setOnClickListener(v -> onSettingViewClicked(getBindingAdapterPosition()));
}
}
private final class PlaybackSpeedAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
private final String[] playbackSpeedTexts;
private final float[] playbackSpeeds;
private int selectedIndex;
public PlaybackSpeedAdapter(String[] playbackSpeedTexts, float[] playbackSpeeds) {
this.playbackSpeedTexts = playbackSpeedTexts;
this.playbackSpeeds = playbackSpeeds;
}
public void updateSelectedIndex(float playbackSpeed) {
int closestMatchIndex = 0;
float closestMatchDifference = Float.MAX_VALUE;
for (int i = 0; i < playbackSpeeds.length; i++) {
float difference = Math.abs(playbackSpeed - playbackSpeeds[i]);
if (difference < closestMatchDifference) {
closestMatchIndex = i;
closestMatchDifference = difference;
}
}
selectedIndex = closestMatchIndex;
}
public String getSelectedText() {
return playbackSpeedTexts[selectedIndex];
}
@Override
public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v =
LayoutInflater.from(getContext())
.inflate(
R.layout.exo_styled_sub_settings_list_item, parent, /* attachToRoot= */ false);
return new SubSettingViewHolder(v);
}
@Override
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
if (position < playbackSpeedTexts.length) {
holder.textView.setText(playbackSpeedTexts[position]);
}
if (position == selectedIndex) {
holder.itemView.setSelected(true);
holder.checkView.setVisibility(VISIBLE);
} else {
holder.itemView.setSelected(false);
holder.checkView.setVisibility(INVISIBLE);
}
holder.itemView.setOnClickListener(
v -> {
if (position != selectedIndex) {
setPlaybackSpeed(playbackSpeeds[position]);
}
settingsWindow.dismiss();
});
}
@Override
public int getItemCount() {
return playbackSpeedTexts.length;
}
}
private static final class TrackInformation {
public final Tracks.Group trackGroup;
public final int trackIndex;
public final String trackName;
public TrackInformation(Tracks tracks, int trackGroupIndex, int trackIndex, String trackName) {
this.trackGroup = tracks.getGroups().get(trackGroupIndex);
this.trackIndex = trackIndex;
this.trackName = trackName;
}
public boolean isSelected() {
return trackGroup.isTrackSelected(trackIndex);
}
}
private final class TextTrackSelectionAdapter extends TrackSelectionAdapter {
@Override
public void init(List<TrackInformation> trackInformations) {
boolean subtitleIsOn = false;
for (int i = 0; i < trackInformations.size(); i++) {
if (trackInformations.get(i).isSelected()) {
subtitleIsOn = true;
break;
}
}
if (subtitleButton != null) {
subtitleButton.setImageDrawable(
subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable);
subtitleButton.setContentDescription(
subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription);
}
this.tracks = trackInformations;
}
@Override
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
// CC options include "Off" at the first position, which disables text rendering.
holder.textView.setText(R.string.exo_track_selection_none);
boolean isTrackSelectionOff = true;
for (int i = 0; i < tracks.size(); i++) {
if (tracks.get(i).isSelected()) {
isTrackSelectionOff = false;
break;
}
}
holder.checkView.setVisibility(isTrackSelectionOff ? VISIBLE : INVISIBLE);
holder.itemView.setOnClickListener(
v -> {
if (player != null
&& player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
TrackSelectionParameters trackSelectionParameters =
player.getTrackSelectionParameters();
player.setTrackSelectionParameters(
trackSelectionParameters
.buildUpon()
.clearOverridesOfType(C.TRACK_TYPE_TEXT)
.setIgnoredTextSelectionFlags(~C.SELECTION_FLAG_FORCED)
.build());
settingsWindow.dismiss();
}
});
}
@Override
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
if (position > 0) {
TrackInformation track = tracks.get(position - 1);
holder.checkView.setVisibility(track.isSelected() ? VISIBLE : INVISIBLE);
}
}
@Override
public void onTrackSelection(String subtext) {
// No-op
}
}
private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {
@Override
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
// Audio track selection option includes "Auto" at the top.
holder.textView.setText(R.string.exo_track_selection_auto);
// hasSelectionOverride is true means there is an explicit track selection, not "Auto".
TrackSelectionParameters parameters = checkNotNull(player).getTrackSelectionParameters();
boolean hasSelectionOverride = hasSelectionOverride(parameters);
holder.checkView.setVisibility(hasSelectionOverride ? INVISIBLE : VISIBLE);
holder.itemView.setOnClickListener(
v -> {
if (player == null
|| !player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
return;
}
TrackSelectionParameters trackSelectionParameters =
player.getTrackSelectionParameters();
castNonNull(player)
.setTrackSelectionParameters(
trackSelectionParameters
.buildUpon()
.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ false)
.build());
settingsAdapter.setSubTextAtPosition(
SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
getResources().getString(R.string.exo_track_selection_auto));
settingsWindow.dismiss();
});
}
private boolean hasSelectionOverride(TrackSelectionParameters trackSelectionParameters) {
for (int i = 0; i < tracks.size(); i++) {
TrackGroup trackGroup = tracks.get(i).trackGroup.getMediaTrackGroup();
if (trackSelectionParameters.overrides.containsKey(trackGroup)) {
return true;
}
}
return false;
}
@Override
public void onTrackSelection(String subtext) {
settingsAdapter.setSubTextAtPosition(SETTINGS_AUDIO_TRACK_SELECTION_POSITION, subtext);
}
@Override
public void init(List<TrackInformation> trackInformations) {
this.tracks = trackInformations;
// Update subtext in settings menu with current audio track selection.
TrackSelectionParameters params = checkNotNull(player).getTrackSelectionParameters();
if (trackInformations.isEmpty()) {
settingsAdapter.setSubTextAtPosition(
SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
getResources().getString(R.string.exo_track_selection_none));
// TODO(insun) : Make the audio item in main settings (settingsAdapater)
// to be non-clickable.
} else if (!hasSelectionOverride(params)) {
settingsAdapter.setSubTextAtPosition(
SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
getResources().getString(R.string.exo_track_selection_auto));
} else {
for (int i = 0; i < trackInformations.size(); i++) {
TrackInformation track = trackInformations.get(i);
if (track.isSelected()) {
settingsAdapter.setSubTextAtPosition(
SETTINGS_AUDIO_TRACK_SELECTION_POSITION, track.trackName);
break;
}
}
}
}
}
private abstract class TrackSelectionAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
protected List<TrackInformation> tracks;
protected TrackSelectionAdapter() {
this.tracks = new ArrayList<>();
}
public abstract void init(List<TrackInformation> trackInformations);
@Override
public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v =
LayoutInflater.from(getContext())
.inflate(
R.layout.exo_styled_sub_settings_list_item, parent, /* attachToRoot= */ false);
return new SubSettingViewHolder(v);
}
protected abstract void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder);
protected abstract void onTrackSelection(String subtext);
@Override
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
@Nullable Player player = PlayerControlView.this.player;
if (player == null) {
return;
}
if (position == 0) {
onBindViewHolderAtZeroPosition(holder);
} else {
TrackInformation track = tracks.get(position - 1);
TrackGroup mediaTrackGroup = track.trackGroup.getMediaTrackGroup();
TrackSelectionParameters params = player.getTrackSelectionParameters();
boolean explicitlySelected =
params.overrides.get(mediaTrackGroup) != null && track.isSelected();
holder.textView.setText(track.trackName);
holder.checkView.setVisibility(explicitlySelected ? VISIBLE : INVISIBLE);
holder.itemView.setOnClickListener(
v -> {
if (!player.isCommandAvailable(COMMAND_SET_TRACK_SELECTION_PARAMETERS)) {
return;
}
TrackSelectionParameters trackSelectionParameters =
player.getTrackSelectionParameters();
player.setTrackSelectionParameters(
trackSelectionParameters
.buildUpon()
.setOverrideForType(
new TrackSelectionOverride(
mediaTrackGroup, ImmutableList.of(track.trackIndex)))
.setTrackTypeDisabled(track.trackGroup.getType(), /* disabled= */ false)
.build());
onTrackSelection(track.trackName);
settingsWindow.dismiss();
});
}
}
@Override
public int getItemCount() {
return tracks.isEmpty() ? 0 : tracks.size() + 1;
}
protected void clear() {
tracks = Collections.emptyList();
}
}
private static class SubSettingViewHolder extends RecyclerView.ViewHolder {
public final TextView textView;
public final View checkView;
public SubSettingViewHolder(View itemView) {
super(itemView);
if (Util.SDK_INT < 26) {
// Workaround for https://github.com/google/ExoPlayer/issues/9061.
itemView.setFocusable(true);
}
textView = itemView.findViewById(R.id.exo_text);
checkView = itemView.findViewById(R.id.exo_check);
}
}
}