diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 63f8b2594d..4fe20d018d 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -202,6 +202,7 @@
* Upgrade Truth dependency from 0.44 to 1.0.
* Upgrade to JUnit 4.13-rc-2.
* UI
+ * Add `StyledPlayerView` and `StyledPlayerControlView`.
* Remove `SimpleExoPlayerView` and `PlaybackControlView`.
* Remove deperecated `exo_simple_player_view.xml` and
`exo_playback_control_view.xml` from resource.
diff --git a/constants.gradle b/constants.gradle
index c95315df6d..0b3362a18a 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -34,6 +34,7 @@ project.ext {
androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1'
androidxMultidexVersion = '2.0.0'
+ androidxRecyclerViewVersion = '1.1.0'
androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0'
diff --git a/library/ui/build.gradle b/library/ui/build.gradle
index 2184443a5d..5b24cfbc62 100644
--- a/library/ui/build.gradle
+++ b/library/ui/build.gradle
@@ -20,6 +20,7 @@ dependencies {
api 'androidx.media:media:' + androidxMediaVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
+ implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
implementation 'com.google.guava:guava:' + guavaVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java
new file mode 100644
index 0000000000..5da2445f7e
--- /dev/null
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java
@@ -0,0 +1,2210 @@
+/*
+ * 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 com.google.android.exoplayer2.ui;
+
+import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Typeface;
+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.ImageView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+import androidx.annotation.Nullable;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ControlDispatcher;
+import com.google.android.exoplayer2.DefaultControlDispatcher;
+import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackPreparer;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.RendererCapabilities;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.TrackGroup;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder;
+import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride;
+import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.RepeatModeUtil;
+import com.google.android.exoplayer2.util.Util;
+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.
+ *
+ *
A StyledPlayerControlView can be customized by setting attributes (or calling corresponding
+ * methods), overriding drawables, overriding the view's layout file, or by specifying a custom view
+ * layout file.
+ *
+ *
Attributes
+ *
+ * The following attributes can be set on a StyledPlayerControlView when used in a layout XML file:
+ *
+ *
+ *
{@code 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.
+ *
{@code rewind_increment} - The duration of the rewind applied when the user taps the
+ * rewind button, in milliseconds. Use zero to disable the rewind button.
+ *
All attributes that can be set on {@link DefaultTimeBar} can also be set on a
+ * StyledPlayerControlView, and will be propagated to the inflated {@link DefaultTimeBar}
+ * unless the layout is overridden to specify a custom {@code exo_progress} (see below).
+ *
+ *
+ *
Overriding drawables
+ *
+ * The drawables used by StyledPlayerControlView (with its default layout file) can be overridden by
+ * drawables with the same names defined in your application. The drawables that can be overridden
+ * are:
+ *
+ *
+ *
{@code exo_styled_controls_play} - The play icon.
+ *
{@code exo_styled_controls_pause} - The pause icon.
+ *
{@code exo_styled_controls_rewind} - The background of rewind icon.
+ *
{@code exo_styled_controls_fastforward} - The background of fast forward icon.
+ *
{@code exo_styled_controls_previous} - The previous icon.
+ *
{@code exo_styled_controls_next} - The next icon.
+ *
{@code exo_styled_controls_repeat_off} - The repeat icon for {@link
+ * Player#REPEAT_MODE_OFF}.
+ *
{@code exo_styled_controls_repeat_one} - The repeat icon for {@link
+ * Player#REPEAT_MODE_ONE}.
+ *
{@code exo_styled_controls_repeat_all} - The repeat icon for {@link
+ * Player#REPEAT_MODE_ALL}.
+ *
{@code exo_styled_controls_shuffle_off} - The shuffle icon when shuffling is
+ * disabled.
+ *
{@code exo_styled_controls_shuffle_on} - The shuffle icon when shuffling is enabled.
+ *
{@code exo_styled_controls_vr} - The VR icon.
+ *
+ *
+ *
Overriding the layout file
+ *
+ * To customize the layout of StyledPlayerControlView throughout your app, or just for certain
+ * configurations, you can define {@code exo_styled_player_control_view.xml} layout files in your
+ * application {@code res/layout*} directories. But, in this case, you need to be careful since the
+ * default animation implementation expects certain relative positions between children. See also Specifying a custom layout file.
+ *
+ *
The layout files in your {@code res/layout*} will override the one provided by the ExoPlayer
+ * library, and will be inflated for use by StyledPlayerControlView. The view identifies and binds
+ * its children by looking for the following ids:
+ *
+ *
+ *
{@code exo_play} - The play button.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_pause} - The pause button.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_rew} - The rewind button.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_rew_with_amount} - The rewind button with rewind amount.
+ *
+ *
Type: {@link TextView}
+ *
Note: StyledPlayerControlView will programmatically set the text with the rewind
+ * amount in seconds. Ignored if an {@code exo_rew} exists. Otherwise, it works as the
+ * rewind button.
+ *
+ *
{@code exo_ffwd} - The fast forward button.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_ffwd_with_amount} - The fast forward button with fast forward amount.
+ *
+ *
Type: {@link TextView}
+ *
Note: StyledPlayerControlView will programmatically set the text with the fast
+ * forward amount in seconds. Ignored if an {@code exo_ffwd} exists. Otherwise, it works
+ * as the fast forward button.
+ *
+ *
{@code exo_prev} - The previous button.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_next} - The next button.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_repeat_toggle} - The repeat toggle button.
+ *
+ *
Type: {@link ImageView}
+ *
Note: StyledPlayerControlView will programmatically set the drawable on the repeat
+ * toggle button according to the player's current repeat mode. The drawables used are
+ * {@code exo_controls_repeat_off}, {@code exo_controls_repeat_one} and {@code
+ * exo_controls_repeat_all}. See the section above for information on overriding these
+ * drawables.
+ *
+ *
{@code exo_shuffle} - The shuffle button.
+ *
+ *
Type: {@link ImageView}
+ *
Note: StyledPlayerControlView will programmatically set the drawable on the shuffle
+ * button according to the player's current repeat mode. The drawables used are {@code
+ * exo_controls_shuffle_off} and {@code exo_controls_shuffle_on}. See the section above
+ * for information on overriding these drawables.
+ *
+ *
{@code exo_vr} - The VR mode button.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_position} - Text view displaying the current playback position.
+ *
+ *
Type: {@link TextView}
+ *
+ *
{@code exo_duration} - Text view displaying the current media duration.
+ *
+ *
Type: {@link TextView}
+ *
+ *
{@code exo_progress_placeholder} - A placeholder that's replaced with the inflated
+ * {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists.
+ *
+ *
Type: {@link View}
+ *
+ *
{@code exo_progress} - Time bar that's updated during playback and allows seeking.
+ * {@link DefaultTimeBar} attributes set on the StyledPlayerControlView will not be
+ * automatically propagated through to this instance. If a view exists with this id, any
+ * {@code exo_progress_placeholder} view will be ignored.
+ *
+ *
Type: {@link TimeBar}
+ *
+ *
+ *
+ *
All child views are optional and so can be omitted if not required, however where defined they
+ * must be of the expected type.
+ *
+ *
Specifying a custom layout file
+ *
+ * Defining your own {@code exo_styled_player_control_view.xml} is useful to customize the layout of
+ * StyledPlayerControlView throughout your application. It's also possible to customize the layout
+ * for a single instance in a layout file. This is achieved by setting the {@code
+ * controller_layout_id} attribute on a StyledPlayerControlView. This will cause the specified
+ * layout to be inflated instead of {@code exo_styled_player_control_view.xml} for only the instance
+ * on which the attribute is set.
+ *
+ *
You need to be careful when you set the {@code controller_layout_id}, because the default
+ * animation implementation expects certain relative positions between children.
+ */
+public class StyledPlayerControlView extends FrameLayout {
+
+ static {
+ ExoPlayerLibraryInfo.registerModule("goog.exo.ui");
+ }
+
+ /** Listener to be notified about changes of the visibility of the UI control. */
+ 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);
+ }
+
+ /**
+ * Listener to be invoked to inform the fullscreen mode is changed. Application should handle the
+ * fullscreen mode accordingly.
+ */
+ 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;
+
+ private static final int SETTINGS_PLAYBACK_SPEED_POSITION = 0;
+ private static final int SETTINGS_AUDIO_TRACK_SELECTION_POSITION = 1;
+ private static final int UNDEFINED_POSITION = -1;
+
+ private final ComponentListener componentListener;
+ private final CopyOnWriteArrayList visibilityListeners;
+ @Nullable private final View previousButton;
+ @Nullable private final View nextButton;
+ @Nullable private final View 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 View vrButton;
+ @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 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 fullScreenExitDrawable;
+ private final Drawable fullScreenEnterDrawable;
+ private final String fullScreenExitContentDescription;
+ private final String fullScreenEnterContentDescription;
+
+ @Nullable private Player player;
+ private com.google.android.exoplayer2.ControlDispatcher controlDispatcher;
+ @Nullable private ProgressUpdateListener progressUpdateListener;
+ @Nullable private PlaybackPreparer playbackPreparer;
+
+ @Nullable private OnFullScreenModeChangedListener onFullScreenModeChangedListener;
+ private boolean isFullScreen;
+ private boolean isAttachedToWindow;
+ private boolean showMultiWindowTimeBar;
+ private boolean multiWindowTimeBar;
+ private boolean scrubbing;
+ private int showTimeoutMs;
+ private int timeBarMinUpdateIntervalMs;
+ private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes;
+ private boolean showRewindButton;
+ private boolean showFastForwardButton;
+ private boolean showPreviousButton;
+ private boolean showNextButton;
+ private boolean showShuffleButton;
+ private boolean showSubtitleButton;
+ private long[] adGroupTimesMs;
+ private boolean[] playedAdGroups;
+ private long[] extraAdGroupTimesMs;
+ private boolean[] extraPlayedAdGroups;
+ private long currentWindowOffset;
+ private long rewindMs;
+ private long fastForwardMs;
+
+ private StyledPlayerControlViewLayoutManager controlViewLayoutManager;
+ private Resources resources;
+
+ // Relating to Settings List View
+ private int selectedMainSettingsPosition;
+ private RecyclerView settingsView;
+ private SettingsAdapter settingsAdapter;
+ private SubSettingsAdapter subSettingsAdapter;
+ private PopupWindow settingsWindow;
+ private List playbackSpeedTextList;
+ private List playbackSpeedMultBy100List;
+ private int customPlaybackSpeedIndex;
+ private int selectedPlaybackSpeedIndex;
+ private boolean needToHideBars;
+ private int settingsWindowMargin;
+
+ @Nullable private DefaultTrackSelector trackSelector;
+ private TrackSelectionAdapter textTrackSelectionAdapter;
+ private TrackSelectionAdapter audioTrackSelectionAdapter;
+ // TODO(insun): Add setTrackNameProvider to use customized track name provider.
+ private TrackNameProvider trackNameProvider;
+
+ // Relating to Bottom Bar Right View
+ @Nullable private View subtitleButton;
+ @Nullable private ImageView fullScreenButton;
+ @Nullable private View settingsButton;
+
+ public StyledPlayerControlView(Context context) {
+ this(context, /* attrs= */ null);
+ }
+
+ public StyledPlayerControlView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, /* defStyleAttr= */ 0);
+ }
+
+ public StyledPlayerControlView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, attrs);
+ }
+
+ @SuppressWarnings({
+ "nullness:argument.type.incompatible",
+ "nullness:method.invocation.invalid",
+ "nullness:methodref.receiver.bound.invalid"
+ })
+ public StyledPlayerControlView(
+ Context context,
+ @Nullable AttributeSet attrs,
+ int defStyleAttr,
+ @Nullable AttributeSet playbackAttrs) {
+ super(context, attrs, defStyleAttr);
+ int controllerLayoutId = R.layout.exo_styled_player_control_view;
+ rewindMs = DefaultControlDispatcher.DEFAULT_REWIND_MS;
+ fastForwardMs = DefaultControlDispatcher.DEFAULT_FAST_FORWARD_MS;
+ showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
+ repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES;
+ timeBarMinUpdateIntervalMs = DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS;
+ showRewindButton = true;
+ showFastForwardButton = true;
+ showPreviousButton = true;
+ showNextButton = true;
+ showShuffleButton = false;
+ showSubtitleButton = false;
+ boolean disableAnimation = false;
+
+ boolean showVrButton = false;
+
+ if (playbackAttrs != null) {
+ TypedArray a =
+ context
+ .getTheme()
+ .obtainStyledAttributes(playbackAttrs, R.styleable.StyledPlayerControlView, 0, 0);
+ try {
+ rewindMs = a.getInt(R.styleable.StyledPlayerControlView_rewind_increment, (int) rewindMs);
+ fastForwardMs =
+ a.getInt(
+ R.styleable.StyledPlayerControlView_fastforward_increment, (int) fastForwardMs);
+ controllerLayoutId =
+ a.getResourceId(
+ R.styleable.StyledPlayerControlView_controller_layout_id, controllerLayoutId);
+ showTimeoutMs = a.getInt(R.styleable.StyledPlayerControlView_show_timeout, showTimeoutMs);
+ repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes);
+ showRewindButton =
+ a.getBoolean(R.styleable.StyledPlayerControlView_show_rewind_button, showRewindButton);
+ showFastForwardButton =
+ a.getBoolean(
+ R.styleable.StyledPlayerControlView_show_fastforward_button, showFastForwardButton);
+ showPreviousButton =
+ a.getBoolean(
+ R.styleable.StyledPlayerControlView_show_previous_button, showPreviousButton);
+ showNextButton =
+ a.getBoolean(R.styleable.StyledPlayerControlView_show_next_button, showNextButton);
+ showShuffleButton =
+ a.getBoolean(
+ R.styleable.StyledPlayerControlView_show_shuffle_button, showShuffleButton);
+ showSubtitleButton =
+ a.getBoolean(
+ R.styleable.StyledPlayerControlView_show_subtitle_button, showSubtitleButton);
+ showVrButton =
+ a.getBoolean(R.styleable.StyledPlayerControlView_show_vr_button, showVrButton);
+ setTimeBarMinUpdateInterval(
+ a.getInt(
+ R.styleable.StyledPlayerControlView_time_bar_min_update_interval,
+ timeBarMinUpdateIntervalMs));
+ disableAnimation =
+ a.getBoolean(R.styleable.StyledPlayerControlView_disable_animation, disableAnimation);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ controlViewLayoutManager = new StyledPlayerControlViewLayoutManager();
+ controlViewLayoutManager.setDisableAnimation(disableAnimation);
+ 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];
+ componentListener = new ComponentListener();
+ controlDispatcher =
+ new com.google.android.exoplayer2.DefaultControlDispatcher(fastForwardMs, rewindMs);
+ updateProgressAction = this::updateProgress;
+
+ LayoutInflater.from(context).inflate(controllerLayoutId, /* root= */ this);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+
+ // Relating to Bottom Bar Left View
+ durationView = findViewById(R.id.exo_duration);
+ positionView = findViewById(R.id.exo_position);
+
+ // Relating to Bottom Bar Right View
+ subtitleButton = findViewById(R.id.exo_subtitle);
+ if (subtitleButton != null) {
+ subtitleButton.setOnClickListener(componentListener);
+ subtitleButton.setVisibility(showSubtitleButton ? VISIBLE : GONE);
+ }
+ fullScreenButton = findViewById(R.id.exo_fullscreen);
+ if (fullScreenButton != null) {
+ fullScreenButton.setOnClickListener(fullScreenModeChangedListener);
+ }
+ settingsButton = findViewById(R.id.exo_settings);
+ if (settingsButton != null) {
+ settingsButton.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 attrs 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);
+ 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);
+ }
+ playPauseButton = findViewById(R.id.exo_play_pause);
+ if (playPauseButton != null) {
+ playPauseButton.setOnClickListener(componentListener);
+ }
+ previousButton = findViewById(R.id.exo_prev);
+ if (previousButton != null) {
+ previousButton.setOnClickListener(componentListener);
+ }
+ nextButton = findViewById(R.id.exo_next);
+ if (nextButton != null) {
+ nextButton.setOnClickListener(componentListener);
+ }
+ Typeface typeface = ResourcesCompat.getFont(context, R.font.roboto_medium_numbers);
+ View rewButton = findViewById(R.id.exo_rew);
+ rewindButtonTextView = rewButton == null ? findViewById(R.id.exo_rew_with_amount) : null;
+ if (rewindButtonTextView != null) {
+ rewindButtonTextView.setTypeface(typeface);
+ }
+ rewindButton = rewButton == null ? rewindButtonTextView : rewButton;
+ if (rewindButton != null) {
+ rewindButton.setOnClickListener(componentListener);
+ }
+ View ffwdButton = findViewById(R.id.exo_ffwd);
+ fastForwardButtonTextView = ffwdButton == null ? findViewById(R.id.exo_ffwd_with_amount) : null;
+ if (fastForwardButtonTextView != null) {
+ fastForwardButtonTextView.setTypeface(typeface);
+ }
+ fastForwardButton = ffwdButton == null ? fastForwardButtonTextView : ffwdButton;
+ 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);
+ }
+
+ resources = context.getResources();
+
+ 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) {
+ setShowVrButton(showVrButton);
+ }
+
+ // Related to Settings List View
+ List settingsMainTextsList =
+ Arrays.asList(resources.getStringArray(R.array.exo_settings_main_texts));
+ TypedArray settingsIconTypedArray = resources.obtainTypedArray(R.array.exo_settings_icon_ids);
+ playbackSpeedTextList =
+ new ArrayList<>(Arrays.asList(resources.getStringArray(R.array.exo_playback_speeds)));
+ String normalSpeed = resources.getString(R.string.exo_controls_playback_speed_normal);
+ selectedPlaybackSpeedIndex = playbackSpeedTextList.indexOf(normalSpeed);
+
+ playbackSpeedMultBy100List = new ArrayList();
+ int[] speeds = resources.getIntArray(R.array.exo_speed_multiplied_by_100);
+ for (int speed : speeds) {
+ playbackSpeedMultBy100List.add(speed);
+ }
+ customPlaybackSpeedIndex = UNDEFINED_POSITION;
+ settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset);
+
+ settingsAdapter = new SettingsAdapter(settingsMainTextsList, settingsIconTypedArray);
+ subSettingsAdapter = new SubSettingsAdapter();
+ subSettingsAdapter.setCheckPosition(UNDEFINED_POSITION);
+ settingsView =
+ (RecyclerView)
+ LayoutInflater.from(context).inflate(R.layout.exo_styled_settings_list, null);
+ settingsView.setAdapter(settingsAdapter);
+ settingsView.setLayoutManager(new LinearLayoutManager(getContext()));
+ settingsWindow =
+ new PopupWindow(settingsView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, true);
+ settingsWindow.setOnDismissListener(componentListener);
+ needToHideBars = true;
+
+ trackNameProvider = new DefaultTrackNameProvider(getResources());
+ textTrackSelectionAdapter = new TextTrackSelectionAdapter();
+ audioTrackSelectionAdapter = new AudioTrackSelectionAdapter();
+
+ fullScreenExitDrawable = resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_exit);
+ fullScreenEnterDrawable =
+ resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_enter);
+ repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_repeat_off);
+ repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_repeat_one);
+ repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_repeat_all);
+ shuffleOnButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_shuffle_on);
+ shuffleOffButtonDrawable = resources.getDrawable(R.drawable.exo_styled_controls_shuffle_off);
+ 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);
+
+ addOnLayoutChangeListener(
+ new OnLayoutChangeListener() {
+ @Override
+ public 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 xoff = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
+ int yoff = -settingsWindow.getHeight() - settingsWindowMargin;
+
+ settingsWindow.update(v, xoff, yoff, -1, -1);
+ }
+ }
+ });
+ }
+
+ @SuppressWarnings("ResourceType")
+ private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(
+ TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
+ return a.getInt(R.styleable.StyledPlayerControlView_repeat_toggle_modes, repeatToggleModes);
+ }
+
+ /**
+ * 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);
+ }
+ if (player != null && player.getTrackSelector() instanceof DefaultTrackSelector) {
+ this.trackSelector = (DefaultTrackSelector) player.getTrackSelector();
+ } else {
+ this.trackSelector = null;
+ }
+ updateAll();
+ updateSettingsPlaybackSpeedLists();
+ }
+
+ /**
+ * Sets whether the time bar should show all windows, as opposed to just the current one. If the
+ * timeline has a period with unknown duration or more than {@link
+ * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single
+ * window.
+ *
+ * @param showMultiWindowTimeBar Whether the time bar should show all windows.
+ */
+ public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
+ this.showMultiWindowTimeBar = showMultiWindowTimeBar;
+ updateTimeline();
+ }
+
+ /**
+ * 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();
+ }
+
+ /**
+ * Adds a {@link VisibilityListener}.
+ *
+ * @param listener The listener to be notified about visibility changes.
+ */
+ public void addVisibilityListener(VisibilityListener listener) {
+ visibilityListeners.add(listener);
+ }
+
+ /**
+ * Removes a {@link VisibilityListener}.
+ *
+ * @param listener The listener to be removed.
+ */
+ 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 the {@link PlaybackPreparer}.
+ *
+ * @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
+ * preparer.
+ */
+ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
+ this.playbackPreparer = playbackPreparer;
+ }
+
+ /**
+ * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}.
+ *
+ * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}.
+ */
+ public void setControlDispatcher(ControlDispatcher controlDispatcher) {
+ if (this.controlDispatcher != controlDispatcher) {
+ this.controlDispatcher = controlDispatcher;
+ updateNavigation();
+ }
+ }
+
+ /**
+ * Sets whether the rewind button is shown.
+ *
+ * @param showRewindButton Whether the rewind button is shown.
+ */
+ public void setShowRewindButton(boolean showRewindButton) {
+ this.showRewindButton = showRewindButton;
+ updateNavigation();
+ }
+
+ /**
+ * Sets whether the fast forward button is shown.
+ *
+ * @param showFastForwardButton Whether the fast forward button is shown.
+ */
+ public void setShowFastForwardButton(boolean showFastForwardButton) {
+ this.showFastForwardButton = showFastForwardButton;
+ updateNavigation();
+ }
+
+ /**
+ * Sets whether the previous button is shown.
+ *
+ * @param showPreviousButton Whether the previous button is shown.
+ */
+ public void setShowPreviousButton(boolean showPreviousButton) {
+ this.showPreviousButton = showPreviousButton;
+ updateNavigation();
+ }
+
+ /**
+ * Sets whether the next button is shown.
+ *
+ * @param showNextButton Whether the next button is shown.
+ */
+ public void setShowNextButton(boolean showNextButton) {
+ this.showNextButton = showNextButton;
+ updateNavigation();
+ }
+
+ /**
+ * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
+ * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
+ */
+ @SuppressWarnings("deprecation")
+ @Deprecated
+ public void setRewindIncrementMs(int rewindMs) {
+ if (controlDispatcher instanceof DefaultControlDispatcher) {
+ ((DefaultControlDispatcher) controlDispatcher).setRewindIncrementMs(rewindMs);
+ updateNavigation();
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
+ * DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
+ */
+ @SuppressWarnings("deprecation")
+ @Deprecated
+ public void setFastForwardIncrementMs(int fastForwardMs) {
+ if (controlDispatcher instanceof DefaultControlDispatcher) {
+ ((DefaultControlDispatcher) controlDispatcher).setFastForwardIncrementMs(fastForwardMs);
+ 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.RepeatMode int currentMode = player.getRepeatMode();
+ if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
+ && currentMode != Player.REPEAT_MODE_OFF) {
+ controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF);
+ } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE
+ && currentMode == Player.REPEAT_MODE_ALL) {
+ controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE);
+ } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
+ && currentMode == Player.REPEAT_MODE_ONE) {
+ controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL);
+ }
+ }
+ updateRepeatModeButton();
+ }
+
+ /** Returns whether the shuffle button is shown. */
+ public boolean getShowShuffleButton() {
+ return showShuffleButton;
+ }
+
+ /**
+ * Sets whether the shuffle button is shown.
+ *
+ * @param showShuffleButton Whether the shuffle button is shown.
+ */
+ public void setShowShuffleButton(boolean showShuffleButton) {
+ this.showShuffleButton = showShuffleButton;
+ updateShuffleButton();
+ }
+
+ /** Returns whether the subtitle button is shown. */
+ public boolean getShowSubtitleButton() {
+ return showSubtitleButton;
+ }
+
+ /**
+ * Sets whether the subtitle button is shown.
+ *
+ * @param showSubtitleButton Whether the subtitle button is shown.
+ */
+ public void setShowSubtitleButton(boolean showSubtitleButton) {
+ this.showSubtitleButton = showSubtitleButton;
+ if (subtitleButton != null) {
+ subtitleButton.setVisibility(showSubtitleButton ? VISIBLE : GONE);
+ }
+ }
+
+ /** Returns whether the VR button is shown. */
+ public boolean getShowVrButton() {
+ return vrButton != null && vrButton.getVisibility() == VISIBLE;
+ }
+
+ /**
+ * Sets whether the VR button is shown.
+ *
+ * @param showVrButton Whether the VR button is shown.
+ */
+ public void setShowVrButton(boolean showVrButton) {
+ if (vrButton != null) {
+ updateButton(showVrButton, vrButton.hasOnClickListeners(), vrButton);
+ }
+ }
+
+ /**
+ * 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(getShowVrButton(), onClickListener != null, vrButton);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @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);
+ }
+
+ /**
+ * Sets a listener to be called when the fullscreen mode should be changed. A non-null listener
+ * needs to be set in order to display the fullscreen button.
+ *
+ * @param listener The listener to be called. A value of null removes any existing
+ * listener and hides the fullscreen button.
+ */
+ public void setOnFullScreenModeChangedListener(
+ @Nullable OnFullScreenModeChangedListener listener) {
+ if (fullScreenButton == null) {
+ return;
+ }
+
+ onFullScreenModeChangedListener = listener;
+ if (onFullScreenModeChangedListener == null) {
+ fullScreenButton.setVisibility(GONE);
+ } else {
+ fullScreenButton.setVisibility(VISIBLE);
+ }
+ }
+
+ /**
+ * 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();
+ }
+
+ /** 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;
+ }
+
+ /* package */ void notifyOnVisibilityChange() {
+ for (VisibilityListener visibilityListener : visibilityListeners) {
+ visibilityListener.onVisibilityChange(getVisibility());
+ }
+ }
+
+ /* package */ void updateAll() {
+ updatePlayPauseButton();
+ updateNavigation();
+ updateRepeatModeButton();
+ updateShuffleButton();
+ updateTrackLists();
+ updateTimeline();
+ }
+
+ private void updatePlayPauseButton() {
+ if (!isVisible() || !isAttachedToWindow) {
+ return;
+ }
+ if (playPauseButton != null) {
+ if (player != null && player.isPlaying()) {
+ ((ImageView) playPauseButton)
+ .setImageDrawable(resources.getDrawable(R.drawable.exo_styled_controls_pause));
+ playPauseButton.setContentDescription(
+ resources.getString(R.string.exo_controls_pause_description));
+ } else {
+ ((ImageView) playPauseButton)
+ .setImageDrawable(resources.getDrawable(R.drawable.exo_styled_controls_play));
+ playPauseButton.setContentDescription(
+ resources.getString(R.string.exo_controls_play_description));
+ }
+ }
+ }
+
+ 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) {
+ Timeline timeline = player.getCurrentTimeline();
+ if (!timeline.isEmpty() && !player.isPlayingAd()) {
+ timeline.getWindow(player.getCurrentWindowIndex(), window);
+ boolean isSeekable = window.isSeekable;
+ enableSeeking = isSeekable;
+ enablePrevious = isSeekable || !window.isDynamic || player.hasPrevious();
+ enableRewind = isSeekable && controlDispatcher.isRewindEnabled();
+ enableFastForward = isSeekable && controlDispatcher.isFastForwardEnabled();
+ enableNext = window.isDynamic || player.hasNext();
+ }
+ }
+
+ if (enableRewind) {
+ updateRewindButton();
+ }
+ if (enableFastForward) {
+ updateFastForwardButton();
+ }
+
+ updateButton(showPreviousButton, enablePrevious, previousButton);
+ updateButton(showRewindButton, enableRewind, rewindButton);
+ updateButton(showFastForwardButton, enableFastForward, fastForwardButton);
+ updateButton(showNextButton, enableNext, nextButton);
+ if (timeBar != null) {
+ timeBar.setEnabled(enableSeeking);
+ }
+ }
+
+ private void updateRewindButton() {
+ if (controlDispatcher instanceof DefaultControlDispatcher) {
+ rewindMs = ((DefaultControlDispatcher) controlDispatcher).getRewindIncrementMs();
+ }
+ long rewindSec = rewindMs / 1_000;
+ if (rewindButtonTextView != null) {
+ rewindButtonTextView.setText(String.valueOf(rewindSec));
+ }
+ if (rewindButton != null) {
+ rewindButton.setContentDescription(
+ resources.getString(R.string.exo_controls_rewind_desc_holder, rewindSec));
+ }
+ }
+
+ private void updateFastForwardButton() {
+ if (controlDispatcher instanceof DefaultControlDispatcher) {
+ fastForwardMs = ((DefaultControlDispatcher) controlDispatcher).getFastForwardIncrementMs();
+ }
+ long fastForwardSec = fastForwardMs / 1_000;
+ if (fastForwardButtonTextView != null) {
+ fastForwardButtonTextView.setText(String.valueOf(fastForwardSec));
+ }
+ if (fastForwardButton != null) {
+ fastForwardButton.setContentDescription(
+ resources.getString(R.string.exo_controls_ffwd_desc_holder, fastForwardSec));
+ }
+ }
+
+ private void updateRepeatModeButton() {
+ if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) {
+ return;
+ }
+
+ if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
+ updateButton(/* visible= */ false, /* enabled= */ false, repeatToggleButton);
+ return;
+ }
+
+ @Nullable Player player = this.player;
+ if (player == null) {
+ updateButton(/* visible= */ true, /* enabled= */ false, repeatToggleButton);
+ repeatToggleButton.setImageDrawable(repeatOffButtonDrawable);
+ repeatToggleButton.setContentDescription(repeatOffButtonContentDescription);
+ return;
+ }
+
+ updateButton(/* visible= */ true, /* 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 (!showShuffleButton) {
+ updateButton(/* visible= */ false, /* enabled= */ false, shuffleButton);
+ } else if (player == null) {
+ updateButton(/* visible= */ true, /* enabled= */ false, shuffleButton);
+ shuffleButton.setImageDrawable(shuffleOffButtonDrawable);
+ shuffleButton.setContentDescription(shuffleOffContentDescription);
+ } else {
+ updateButton(/* visible= */ true, /* enabled= */ true, shuffleButton);
+ shuffleButton.setImageDrawable(
+ player.getShuffleModeEnabled() ? shuffleOnButtonDrawable : shuffleOffButtonDrawable);
+ shuffleButton.setContentDescription(
+ player.getShuffleModeEnabled()
+ ? shuffleOnContentDescription
+ : shuffleOffContentDescription);
+ }
+ }
+
+ private void updateTrackLists() {
+ initTrackSelectionAdapter();
+ updateButton(showSubtitleButton, textTrackSelectionAdapter.getItemCount() > 0, subtitleButton);
+ }
+
+ private void initTrackSelectionAdapter() {
+ textTrackSelectionAdapter.clear();
+ audioTrackSelectionAdapter.clear();
+ if (player == null || trackSelector == null) {
+ return;
+ }
+ DefaultTrackSelector trackSelector = this.trackSelector;
+ @Nullable MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
+ if (mappedTrackInfo == null) {
+ return;
+ }
+ List textTracks = new ArrayList<>();
+ List audioTracks = new ArrayList<>();
+ List textRendererIndices = new ArrayList<>();
+ List audioRendererIndices = new ArrayList<>();
+ for (int rendererIndex = 0;
+ rendererIndex < mappedTrackInfo.getRendererCount();
+ rendererIndex++) {
+ if (mappedTrackInfo.getRendererType(rendererIndex) == C.TRACK_TYPE_TEXT
+ && showSubtitleButton) {
+ // Get TrackSelection at the corresponding renderer index.
+ gatherTrackInfosForAdapter(mappedTrackInfo, rendererIndex, textTracks);
+ textRendererIndices.add(rendererIndex);
+ } else if (mappedTrackInfo.getRendererType(rendererIndex) == C.TRACK_TYPE_AUDIO) {
+ gatherTrackInfosForAdapter(mappedTrackInfo, rendererIndex, audioTracks);
+ audioRendererIndices.add(rendererIndex);
+ }
+ }
+ textTrackSelectionAdapter.init(textRendererIndices, textTracks, mappedTrackInfo);
+ audioTrackSelectionAdapter.init(audioRendererIndices, audioTracks, mappedTrackInfo);
+ }
+
+ private void gatherTrackInfosForAdapter(
+ MappedTrackInfo mappedTrackInfo, int rendererIndex, List tracks) {
+ TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
+
+ TrackSelectionArray trackSelections = checkNotNull(player).getCurrentTrackSelections();
+ @Nullable TrackSelection trackSelection = trackSelections.get(rendererIndex);
+
+ for (int groupIndex = 0; groupIndex < trackGroupArray.length; groupIndex++) {
+ TrackGroup trackGroup = trackGroupArray.get(groupIndex);
+ for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
+ Format format = trackGroup.getFormat(trackIndex);
+ if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex)
+ == RendererCapabilities.FORMAT_HANDLED) {
+ boolean trackIsSelected =
+ trackSelection != null && trackSelection.indexOf(format) != C.INDEX_UNSET;
+ tracks.add(
+ new TrackInfo(
+ rendererIndex,
+ groupIndex,
+ trackIndex,
+ trackNameProvider.getTrackName(format),
+ trackIsSelected));
+ }
+ }
+ }
+ }
+
+ private void updateTimeline() {
+ @Nullable Player player = this.player;
+ if (player == null) {
+ return;
+ }
+ multiWindowTimeBar =
+ showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window);
+ currentWindowOffset = 0;
+ long durationUs = 0;
+ int adGroupCount = 0;
+ Timeline timeline = player.getCurrentTimeline();
+ if (!timeline.isEmpty()) {
+ int currentWindowIndex = player.getCurrentWindowIndex();
+ int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex;
+ int lastWindowIndex = multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex;
+ for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {
+ if (i == currentWindowIndex) {
+ currentWindowOffset = C.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 periodAdGroupCount = period.getAdGroupCount();
+ for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; 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] = C.usToMs(durationUs + adGroupTimeInWindowUs);
+ playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex);
+ adGroupCount++;
+ }
+ }
+ }
+ durationUs += window.durationUs;
+ }
+ }
+ long durationMs = C.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) {
+ 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 playbackSpeed into account.
+ float playbackSpeed = player.getPlaybackSpeed();
+ 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 updateSettingsPlaybackSpeedLists() {
+ if (player == null) {
+ return;
+ }
+ float speed = player.getPlaybackSpeed();
+ int currentSpeedMultBy100 = Math.round(speed * 100);
+ int indexForCurrentSpeed = playbackSpeedMultBy100List.indexOf(currentSpeedMultBy100);
+ if (indexForCurrentSpeed == UNDEFINED_POSITION) {
+ if (customPlaybackSpeedIndex != UNDEFINED_POSITION) {
+ playbackSpeedMultBy100List.remove(customPlaybackSpeedIndex);
+ playbackSpeedTextList.remove(customPlaybackSpeedIndex);
+ customPlaybackSpeedIndex = UNDEFINED_POSITION;
+ }
+ indexForCurrentSpeed =
+ -Collections.binarySearch(playbackSpeedMultBy100List, currentSpeedMultBy100) - 1;
+ String customSpeedText =
+ resources.getString(R.string.exo_controls_custom_playback_speed, speed);
+ playbackSpeedMultBy100List.add(indexForCurrentSpeed, currentSpeedMultBy100);
+ playbackSpeedTextList.add(indexForCurrentSpeed, customSpeedText);
+ customPlaybackSpeedIndex = indexForCurrentSpeed;
+ }
+
+ selectedPlaybackSpeedIndex = indexForCurrentSpeed;
+ settingsAdapter.updateSubTexts(
+ SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedTextList.get(indexForCurrentSpeed));
+ }
+
+ 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) {
+ settingsView.setAdapter(adapter);
+
+ updateSettingsWindowSize();
+
+ needToHideBars = false;
+ settingsWindow.dismiss();
+ needToHideBars = true;
+
+ int xoff = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
+ int yoff = -settingsWindow.getHeight() - settingsWindowMargin;
+
+ settingsWindow.showAsDropDown(this, xoff, yoff);
+ }
+
+ private void setPlaybackSpeed(float speed) {
+ if (player == null) {
+ return;
+ }
+ player.setPlaybackSpeed(speed);
+ }
+
+ /* package */ void requestPlayPauseFocus() {
+ if (playPauseButton != null) {
+ playPauseButton.requestFocus();
+ }
+ }
+
+ private void updateButton(boolean visible, boolean enabled, @Nullable View view) {
+ if (view == null) {
+ return;
+ }
+ view.setEnabled(enabled);
+ view.setAlpha(enabled ? buttonAlphaEnabled : buttonAlphaDisabled);
+ view.setVisibility(visible ? VISIBLE : GONE);
+ }
+
+ private void seekToTimeBarPosition(Player player, long positionMs) {
+ int windowIndex;
+ Timeline timeline = player.getCurrentTimeline();
+ if (multiWindowTimeBar && !timeline.isEmpty()) {
+ int windowCount = timeline.getWindowCount();
+ 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++;
+ }
+ } else {
+ windowIndex = player.getCurrentWindowIndex();
+ }
+ boolean dispatched = seekTo(player, windowIndex, positionMs);
+ if (!dispatched) {
+ // The seek wasn't dispatched then the progress bar scrubber will be in the wrong position.
+ // Trigger a progress update to snap it back.
+ updateProgress();
+ }
+ }
+
+ private boolean seekTo(Player player, int windowIndex, long positionMs) {
+ return controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs);
+ }
+
+ private final OnClickListener fullScreenModeChangedListener =
+ new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (onFullScreenModeChangedListener == null || fullScreenButton == null) {
+ return;
+ }
+
+ isFullScreen = !isFullScreen;
+ if (isFullScreen) {
+ fullScreenButton.setImageDrawable(fullScreenExitDrawable);
+ fullScreenButton.setContentDescription(fullScreenExitContentDescription);
+ } else {
+ fullScreenButton.setImageDrawable(fullScreenEnterDrawable);
+ fullScreenButton.setContentDescription(fullScreenEnterContentDescription);
+ }
+
+ if (onFullScreenModeChangedListener != null) {
+ onFullScreenModeChangedListener.onFullScreenModeChanged(isFullScreen);
+ }
+ }
+ };
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ controlViewLayoutManager.onViewAttached(this);
+ isAttachedToWindow = true;
+ if (isFullyVisible()) {
+ controlViewLayoutManager.resetHideCallbacks();
+ }
+ updateAll();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ controlViewLayoutManager.onViewDetached(this);
+ 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) {
+ controlDispatcher.dispatchFastForward(player);
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) {
+ controlDispatcher.dispatchRewind(player);
+ } else if (event.getRepeatCount() == 0) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady());
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ controlDispatcher.dispatchSetPlayWhenReady(player, true);
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ controlDispatcher.dispatchSetPlayWhenReady(player, false);
+ break;
+ case KeyEvent.KEYCODE_MEDIA_NEXT:
+ controlDispatcher.dispatchNext(player);
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
+ controlDispatcher.dispatchPrevious(player);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return true;
+ }
+
+ private boolean shouldShowPauseButton() {
+ return player != null
+ && player.getPlaybackState() != Player.STATE_ENDED
+ && player.getPlaybackState() != Player.STATE_IDLE
+ && player.getPlayWhenReady();
+ }
+
+ @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_MEDIA_PLAY
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE
+ || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS;
+ }
+
+ /**
+ * Returns whether the specified {@code timeline} can be shown on a multi-window time bar.
+ *
+ * @param timeline The {@link Timeline} 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(Timeline timeline, Timeline.Window window) {
+ if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) {
+ return false;
+ }
+ int windowCount = timeline.getWindowCount();
+ for (int i = 0; i < windowCount; i++) {
+ if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private final class ComponentListener
+ implements Player.EventListener,
+ TimeBar.OnScrubListener,
+ OnClickListener,
+ PopupWindow.OnDismissListener {
+
+ @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 onPlaybackStateChanged(@Player.State int playbackState) {
+ updatePlayPauseButton();
+ updateProgress();
+ }
+
+ @Override
+ public void onPlayWhenReadyChanged(
+ boolean playWhenReady, @Player.PlayWhenReadyChangeReason int state) {
+ updatePlayPauseButton();
+ updateProgress();
+ }
+
+ @Override
+ public void onIsPlayingChanged(boolean isPlaying) {
+ updateProgress();
+ }
+
+ @Override
+ public void onRepeatModeChanged(int repeatMode) {
+ updateRepeatModeButton();
+ updateNavigation();
+ }
+
+ @Override
+ public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
+ updateShuffleButton();
+ updateNavigation();
+ }
+
+ @Override
+ public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) {
+ updateNavigation();
+ updateTimeline();
+ }
+
+ @Override
+ public void onPlaybackSpeedChanged(float playbackSpeed) {
+ updateSettingsPlaybackSpeedLists();
+ }
+
+ @Override
+ public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
+ updateTrackLists();
+ }
+
+ @Override
+ public void onTimelineChanged(Timeline timeline, @Player.TimelineChangeReason int reason) {
+ updateNavigation();
+ updateTimeline();
+ }
+
+ @Override
+ public void onDismiss() {
+ if (needToHideBars) {
+ controlViewLayoutManager.resetHideCallbacks();
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ @Nullable Player player = StyledPlayerControlView.this.player;
+ if (player == null) {
+ return;
+ }
+ controlViewLayoutManager.resetHideCallbacks();
+ if (nextButton == view) {
+ controlDispatcher.dispatchNext(player);
+ } else if (previousButton == view) {
+ controlDispatcher.dispatchPrevious(player);
+ } else if (fastForwardButton == view) {
+ controlDispatcher.dispatchFastForward(player);
+ } else if (rewindButton == view) {
+ controlDispatcher.dispatchRewind(player);
+ } else if (playPauseButton == view) {
+ if (player.getPlaybackState() == Player.STATE_IDLE) {
+ if (playbackPreparer != null) {
+ playbackPreparer.preparePlayback();
+ }
+ } else if (player.getPlaybackState() == Player.STATE_ENDED) {
+ seekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
+ }
+ controlDispatcher.dispatchSetPlayWhenReady(player, !player.isPlaying());
+ } else if (repeatToggleButton == view) {
+ controlDispatcher.dispatchSetRepeatMode(
+ player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes));
+ } else if (shuffleButton == view) {
+ controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled());
+ } else if (settingsButton == view) {
+ controlViewLayoutManager.removeHideCallbacks();
+ displaySettingsWindow(settingsAdapter);
+ } else if (subtitleButton == view) {
+ controlViewLayoutManager.removeHideCallbacks();
+ displaySettingsWindow(textTrackSelectionAdapter);
+ }
+ }
+ }
+
+ private class SettingsAdapter extends RecyclerView.Adapter {
+ private List mainTexts;
+ @Nullable private List subTexts;
+ @Nullable private TypedArray iconIds;
+
+ public SettingsAdapter(List mainTexts, @Nullable TypedArray iconIds) {
+ this.mainTexts = mainTexts;
+ this.subTexts = Arrays.asList(new String[mainTexts.size()]);
+ this.iconIds = iconIds;
+ }
+
+ @Override
+ public SettingsViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
+ View v =
+ LayoutInflater.from(getContext()).inflate(R.layout.exo_styled_settings_list_item, null);
+ return new SettingsViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(SettingsViewHolder holder, int position) {
+ holder.mainTextView.setText(mainTexts.get(position));
+
+ if (subTexts == null || subTexts.get(position) == null) {
+ holder.subTextView.setVisibility(GONE);
+ } else {
+ holder.subTextView.setText(subTexts.get(position));
+ }
+
+ if (iconIds == null || iconIds.getDrawable(position) == null) {
+ holder.iconView.setVisibility(GONE);
+ } else {
+ holder.iconView.setImageDrawable(iconIds.getDrawable(position));
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mainTexts.size();
+ }
+
+ public void updateSubTexts(int position, String subText) {
+ if (this.subTexts != null) {
+ this.subTexts.set(position, subText);
+ }
+ }
+
+ private class SettingsViewHolder extends RecyclerView.ViewHolder {
+ TextView mainTextView;
+ TextView subTextView;
+ ImageView iconView;
+
+ SettingsViewHolder(View itemView) {
+ super(itemView);
+
+ 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(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = SettingsViewHolder.this.getAdapterPosition();
+ if (position == RecyclerView.NO_POSITION) {
+ return;
+ }
+
+ if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
+ subSettingsAdapter.setTexts(playbackSpeedTextList);
+ subSettingsAdapter.setCheckPosition(selectedPlaybackSpeedIndex);
+ selectedMainSettingsPosition = SETTINGS_PLAYBACK_SPEED_POSITION;
+ displaySettingsWindow(subSettingsAdapter);
+ } else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
+ selectedMainSettingsPosition = SETTINGS_AUDIO_TRACK_SELECTION_POSITION;
+ displaySettingsWindow(audioTrackSelectionAdapter);
+ } else {
+ settingsWindow.dismiss();
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private class SubSettingsAdapter
+ extends RecyclerView.Adapter {
+ @Nullable private List texts;
+ private int checkPosition;
+
+ @Override
+ public SubSettingsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v =
+ LayoutInflater.from(getContext())
+ .inflate(R.layout.exo_styled_sub_settings_list_item, null);
+ return new SubSettingsViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(SubSettingsViewHolder holder, int position) {
+ if (texts != null) {
+ holder.textView.setText(texts.get(position));
+ }
+ holder.checkView.setVisibility(position == checkPosition ? VISIBLE : INVISIBLE);
+ }
+
+ @Override
+ public int getItemCount() {
+ return texts != null ? texts.size() : 0;
+ }
+
+ public void setTexts(@Nullable List texts) {
+ this.texts = texts;
+ }
+
+ public void setCheckPosition(int checkPosition) {
+ this.checkPosition = checkPosition;
+ }
+
+ private class SubSettingsViewHolder extends RecyclerView.ViewHolder {
+ TextView textView;
+ View checkView;
+
+ SubSettingsViewHolder(View itemView) {
+ super(itemView);
+
+ textView = itemView.findViewById(R.id.exo_text);
+ checkView = itemView.findViewById(R.id.exo_check);
+
+ itemView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int position = SubSettingsViewHolder.this.getAdapterPosition();
+ if (position == RecyclerView.NO_POSITION) {
+ return;
+ }
+
+ if (selectedMainSettingsPosition == SETTINGS_PLAYBACK_SPEED_POSITION) {
+ if (position != selectedPlaybackSpeedIndex) {
+ float speed = playbackSpeedMultBy100List.get(position) / 100.0f;
+ setPlaybackSpeed(speed);
+ }
+ }
+ settingsWindow.dismiss();
+ }
+ });
+ }
+ }
+ }
+
+ private static final class TrackInfo {
+ public final int rendererIndex;
+ public final int groupIndex;
+ public final int trackIndex;
+ public final String trackName;
+ public final boolean selected;
+
+ public TrackInfo(
+ int rendererIndex, int groupIndex, int trackIndex, String trackName, boolean selected) {
+ this.rendererIndex = rendererIndex;
+ this.groupIndex = groupIndex;
+ this.trackIndex = trackIndex;
+ this.trackName = trackName;
+ this.selected = selected;
+ }
+ }
+
+ private final class TextTrackSelectionAdapter extends TrackSelectionAdapter {
+ @Override
+ public void init(
+ List rendererIndices,
+ List trackInfos,
+ MappedTrackInfo mappedTrackInfo) {
+ this.rendererIndices = rendererIndices;
+ this.tracks = trackInfos;
+ this.mappedTrackInfo = mappedTrackInfo;
+ }
+
+ @Override
+ public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder 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).selected) {
+ isTrackSelectionOff = false;
+ break;
+ }
+ }
+ holder.checkView.setVisibility(isTrackSelectionOff ? VISIBLE : INVISIBLE);
+ holder.itemView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (trackSelector != null) {
+ ParametersBuilder parametersBuilder = trackSelector.getParameters().buildUpon();
+ for (int i = 0; i < rendererIndices.size(); i++) {
+ int rendererIndex = rendererIndices.get(i);
+ parametersBuilder =
+ parametersBuilder
+ .clearSelectionOverrides(rendererIndex)
+ .setRendererDisabled(rendererIndex, true);
+ }
+ checkNotNull(trackSelector).setParameters(parametersBuilder);
+ settingsWindow.dismiss();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void updateSettingsSubtext(String subtext) {
+ // Do nothing. Text track selection exists outside of Settings menu.
+ }
+ }
+
+ private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {
+
+ @Override
+ public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder 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".
+ boolean hasSelectionOverride = false;
+ DefaultTrackSelector.Parameters parameters = checkNotNull(trackSelector).getParameters();
+ for (int i = 0; i < rendererIndices.size(); i++) {
+ int rendererIndex = rendererIndices.get(i);
+ TrackGroupArray trackGroups = checkNotNull(mappedTrackInfo).getTrackGroups(rendererIndex);
+ if (parameters.hasSelectionOverride(rendererIndex, trackGroups)) {
+ hasSelectionOverride = true;
+ break;
+ }
+ }
+ holder.checkView.setVisibility(hasSelectionOverride ? INVISIBLE : VISIBLE);
+ holder.itemView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (trackSelector != null) {
+ ParametersBuilder parametersBuilder = trackSelector.getParameters().buildUpon();
+ for (int i = 0; i < rendererIndices.size(); i++) {
+ int rendererIndex = rendererIndices.get(i);
+ parametersBuilder = parametersBuilder.clearSelectionOverrides(rendererIndex);
+ }
+ checkNotNull(trackSelector).setParameters(parametersBuilder);
+ }
+ settingsAdapter.updateSubTexts(
+ SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
+ getResources().getString(R.string.exo_track_selection_auto));
+ settingsWindow.dismiss();
+ }
+ });
+ }
+
+ @Override
+ public void updateSettingsSubtext(String subtext) {
+ settingsAdapter.updateSubTexts(SETTINGS_AUDIO_TRACK_SELECTION_POSITION, subtext);
+ }
+
+ @Override
+ public void init(
+ List rendererIndices,
+ List trackInfos,
+ MappedTrackInfo mappedTrackInfo) {
+ // Update subtext in settings menu with current audio track selection.
+ boolean hasSelectionOverride = false;
+ for (int i = 0; i < rendererIndices.size(); i++) {
+ int rendererIndex = rendererIndices.get(i);
+ TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
+ if (trackSelector != null
+ && trackSelector.getParameters().hasSelectionOverride(rendererIndex, trackGroups)) {
+ hasSelectionOverride = true;
+ break;
+ }
+ }
+ if (trackInfos.isEmpty()) {
+ settingsAdapter.updateSubTexts(
+ 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) {
+ settingsAdapter.updateSubTexts(
+ SETTINGS_AUDIO_TRACK_SELECTION_POSITION,
+ getResources().getString(R.string.exo_track_selection_auto));
+ } else {
+ for (int i = 0; i < tracks.size(); i++) {
+ TrackInfo track = tracks.get(i);
+ if (track.selected) {
+ settingsAdapter.updateSubTexts(
+ SETTINGS_AUDIO_TRACK_SELECTION_POSITION, track.trackName);
+ break;
+ }
+ }
+ }
+ this.rendererIndices = rendererIndices;
+ this.tracks = trackInfos;
+ this.mappedTrackInfo = mappedTrackInfo;
+ }
+ }
+
+ private abstract class TrackSelectionAdapter
+ extends RecyclerView.Adapter {
+ protected List rendererIndices;
+ protected List tracks;
+ protected @Nullable MappedTrackInfo mappedTrackInfo;
+
+ public TrackSelectionAdapter() {
+ this.rendererIndices = new ArrayList<>();
+ this.tracks = new ArrayList<>();
+ this.mappedTrackInfo = null;
+ }
+
+ public abstract void init(
+ List rendererIndices, List trackInfos, MappedTrackInfo mappedTrackInfo);
+
+ @Override
+ public TrackSelectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v =
+ LayoutInflater.from(getContext())
+ .inflate(R.layout.exo_styled_sub_settings_list_item, null);
+ return new TrackSelectionViewHolder(v);
+ }
+
+ public abstract void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder);
+
+ public abstract void updateSettingsSubtext(String subtext);
+
+ @Override
+ public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
+ if (trackSelector == null || mappedTrackInfo == null) {
+ return;
+ }
+ if (position == 0) {
+ onBindViewHolderAtZeroPosition(holder);
+ } else {
+ TrackInfo track = tracks.get(position - 1);
+ TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(track.rendererIndex);
+ boolean explicitlySelected =
+ checkNotNull(trackSelector)
+ .getParameters()
+ .hasSelectionOverride(track.rendererIndex, trackGroups)
+ && track.selected;
+ holder.textView.setText(track.trackName);
+ holder.checkView.setVisibility(explicitlySelected ? VISIBLE : INVISIBLE);
+ holder.itemView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mappedTrackInfo != null && trackSelector != null) {
+ ParametersBuilder parametersBuilder = trackSelector.getParameters().buildUpon();
+ for (int i = 0; i < rendererIndices.size(); i++) {
+ int rendererIndex = rendererIndices.get(i);
+ if (rendererIndex == track.rendererIndex) {
+ parametersBuilder =
+ parametersBuilder
+ .setSelectionOverride(
+ rendererIndex,
+ checkNotNull(mappedTrackInfo).getTrackGroups(rendererIndex),
+ new SelectionOverride(track.groupIndex, track.trackIndex))
+ .setRendererDisabled(rendererIndex, false);
+ } else {
+ parametersBuilder =
+ parametersBuilder
+ .clearSelectionOverrides(rendererIndex)
+ .setRendererDisabled(rendererIndex, true);
+ }
+ }
+ checkNotNull(trackSelector).setParameters(parametersBuilder);
+ updateSettingsSubtext(track.trackName);
+ settingsWindow.dismiss();
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return tracks.isEmpty() ? 0 : tracks.size() + 1;
+ }
+
+ public void clear() {
+ tracks = Collections.emptyList();
+ mappedTrackInfo = null;
+ }
+ }
+
+ private static class TrackSelectionViewHolder extends RecyclerView.ViewHolder {
+ public final TextView textView;
+ public final View checkView;
+
+ public TrackSelectionViewHolder(View itemView) {
+ super(itemView);
+ textView = itemView.findViewById(R.id.exo_text);
+ checkView = itemView.findViewById(R.id.exo_check);
+ }
+ }
+}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java
new file mode 100644
index 0000000000..ef89023406
--- /dev/null
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlViewLayoutManager.java
@@ -0,0 +1,736 @@
+/*
+ * Copyright 2020 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 com.google.android.exoplayer2.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.animation.LinearInterpolator;
+import androidx.annotation.Nullable;
+import java.util.ArrayList;
+
+/* package */ final class StyledPlayerControlViewLayoutManager
+ implements View.OnLayoutChangeListener {
+ private static final long ANIMATION_INTERVAL_MS = 2_000;
+ private static final long DURATION_FOR_HIDING_ANIMATION_MS = 250;
+ private static final long DURATION_FOR_SHOWING_ANIMATION_MS = 250;
+
+ // Int for defining the UX state where all the views (TitleBar, ProgressBar, BottomBar) are
+ // all visible.
+ private static final int UX_STATE_ALL_VISIBLE = 0;
+ // Int for defining the UX state where only the ProgressBar view is visible.
+ private static final int UX_STATE_ONLY_PROGRESS_VISIBLE = 1;
+ // Int for defining the UX state where none of the views are visible.
+ private static final int UX_STATE_NONE_VISIBLE = 2;
+ // Int for defining the UX state where the views are being animated to be hidden.
+ private static final int UX_STATE_ANIMATING_HIDE = 3;
+ // Int for defining the UX state where the views are being animated to be shown.
+ private static final int UX_STATE_ANIMATING_SHOW = 4;
+
+ private int uxState = UX_STATE_ALL_VISIBLE;
+ private boolean isMinimalMode;
+ private boolean needToShowBars;
+ private boolean disableAnimation = false;
+
+ @Nullable private StyledPlayerControlView styledPlayerControlView;
+
+ @Nullable private ViewGroup titleBar;
+ @Nullable private ViewGroup embeddedTransportControls;
+ @Nullable private ViewGroup bottomBar;
+ @Nullable private ViewGroup minimalControls;
+ @Nullable private ViewGroup basicControls;
+ @Nullable private ViewGroup extraControls;
+ @Nullable private ViewGroup extraControlsScrollView;
+ @Nullable private ViewGroup timeView;
+ @Nullable private View timeBar;
+ @Nullable private View overflowShowButton;
+
+ @Nullable private AnimatorSet hideMainBarsAnimator;
+ @Nullable private AnimatorSet hideProgressBarAnimator;
+ @Nullable private AnimatorSet hideAllBarsAnimator;
+ @Nullable private AnimatorSet showMainBarsAnimator;
+ @Nullable private AnimatorSet showAllBarsAnimator;
+ @Nullable private ValueAnimator overflowShowAnimator;
+ @Nullable private ValueAnimator overflowHideAnimator;
+
+ void show() {
+ if (this.styledPlayerControlView == null) {
+ return;
+ }
+ StyledPlayerControlView styledPlayerControlView = this.styledPlayerControlView;
+ if (!styledPlayerControlView.isVisible()) {
+ styledPlayerControlView.setVisibility(View.VISIBLE);
+ styledPlayerControlView.updateAll();
+ styledPlayerControlView.requestPlayPauseFocus();
+ }
+ styledPlayerControlView.post(showAllBars);
+ }
+
+ void hide() {
+ if (styledPlayerControlView == null
+ || uxState == UX_STATE_ANIMATING_HIDE
+ || uxState == UX_STATE_NONE_VISIBLE) {
+ return;
+ }
+ removeHideCallbacks();
+ if (isAnimationDisabled()) {
+ postDelayedRunnable(hideController, 0);
+ } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
+ postDelayedRunnable(hideProgressBar, 0);
+ } else {
+ postDelayedRunnable(hideAllBars, 0);
+ }
+ }
+
+ void setDisableAnimation(boolean disableAnimation) {
+ this.disableAnimation = disableAnimation;
+ }
+
+ void resetHideCallbacks() {
+ if (uxState == UX_STATE_ANIMATING_HIDE) {
+ return;
+ }
+ removeHideCallbacks();
+ int showTimeoutMs =
+ styledPlayerControlView != null ? styledPlayerControlView.getShowTimeoutMs() : 0;
+ if (showTimeoutMs > 0) {
+ if (isAnimationDisabled()) {
+ postDelayedRunnable(hideController, showTimeoutMs);
+ } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
+ postDelayedRunnable(hideProgressBar, ANIMATION_INTERVAL_MS);
+ } else {
+ postDelayedRunnable(hideMainBars, showTimeoutMs);
+ }
+ }
+ }
+
+ void removeHideCallbacks() {
+ if (styledPlayerControlView == null) {
+ return;
+ }
+ styledPlayerControlView.removeCallbacks(hideController);
+ styledPlayerControlView.removeCallbacks(hideAllBars);
+ styledPlayerControlView.removeCallbacks(hideMainBars);
+ styledPlayerControlView.removeCallbacks(hideProgressBar);
+ }
+
+ void onViewAttached(StyledPlayerControlView v) {
+ styledPlayerControlView = v;
+
+ v.addOnLayoutChangeListener(this);
+
+ // Relating to Title Bar View
+ ViewGroup titleBar = v.findViewById(R.id.exo_title_bar);
+
+ // Relating to Center View
+ ViewGroup centerView = v.findViewById(R.id.exo_center_view);
+ embeddedTransportControls = v.findViewById(R.id.exo_embedded_transport_controls);
+
+ // Relating to Minimal Layout
+ minimalControls = v.findViewById(R.id.exo_minimal_controls);
+
+ // Relating to Bottom Bar View
+ ViewGroup bottomBar = v.findViewById(R.id.exo_bottom_bar);
+
+ // Relating to Bottom Bar Left View
+ timeView = v.findViewById(R.id.exo_time);
+ View timeBar = v.findViewById(R.id.exo_progress);
+
+ // Relating to Bottom Bar Right View
+ basicControls = v.findViewById(R.id.exo_basic_controls);
+ extraControls = v.findViewById(R.id.exo_extra_controls);
+ extraControlsScrollView = v.findViewById(R.id.exo_extra_controls_scroll_view);
+ overflowShowButton = v.findViewById(R.id.exo_overflow_show);
+ View overflowHideButton = v.findViewById(R.id.exo_overflow_hide);
+ if (overflowShowButton != null && overflowHideButton != null) {
+ overflowShowButton.setOnClickListener(overflowListener);
+ overflowHideButton.setOnClickListener(overflowListener);
+ }
+
+ this.titleBar = titleBar;
+ this.bottomBar = bottomBar;
+ this.timeBar = timeBar;
+
+ Resources resources = v.getResources();
+ float titleBarHeight = resources.getDimension(R.dimen.exo_title_bar_height);
+ float progressBarHeight = resources.getDimension(R.dimen.exo_custom_progress_thumb_size);
+ float bottomBarHeight = resources.getDimension(R.dimen.exo_bottom_bar_height);
+
+ ValueAnimator fadeOutAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
+ fadeOutAnimator.setInterpolator(new LinearInterpolator());
+ fadeOutAnimator.addUpdateListener(
+ animation -> {
+ float animatedValue = (float) animation.getAnimatedValue();
+
+ if (centerView != null) {
+ centerView.setAlpha(animatedValue);
+ }
+ if (minimalControls != null) {
+ minimalControls.setAlpha(animatedValue);
+ }
+ });
+ fadeOutAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (timeBar instanceof DefaultTimeBar && !isMinimalMode) {
+ ((DefaultTimeBar) timeBar).hideScrubber(DURATION_FOR_HIDING_ANIMATION_MS);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (centerView != null) {
+ centerView.setVisibility(View.INVISIBLE);
+ }
+ if (minimalControls != null) {
+ minimalControls.setVisibility(View.INVISIBLE);
+ }
+ }
+ });
+
+ ValueAnimator fadeInAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ fadeInAnimator.setInterpolator(new LinearInterpolator());
+ fadeInAnimator.addUpdateListener(
+ animation -> {
+ float animatedValue = (float) animation.getAnimatedValue();
+
+ if (centerView != null) {
+ centerView.setAlpha(animatedValue);
+ }
+ if (minimalControls != null) {
+ minimalControls.setAlpha(animatedValue);
+ }
+ });
+ fadeInAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (centerView != null) {
+ centerView.setVisibility(View.VISIBLE);
+ }
+ if (minimalControls != null) {
+ minimalControls.setVisibility(isMinimalMode ? View.VISIBLE : View.INVISIBLE);
+ }
+ if (timeBar instanceof DefaultTimeBar && !isMinimalMode) {
+ ((DefaultTimeBar) timeBar).showScrubber(DURATION_FOR_SHOWING_ANIMATION_MS);
+ }
+ }
+ });
+
+ hideMainBarsAnimator = new AnimatorSet();
+ hideMainBarsAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS);
+ hideMainBarsAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setUxState(UX_STATE_ANIMATING_HIDE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setUxState(UX_STATE_ONLY_PROGRESS_VISIBLE);
+ if (needToShowBars) {
+ if (styledPlayerControlView != null) {
+ styledPlayerControlView.post(showAllBars);
+ }
+ needToShowBars = false;
+ }
+ }
+ });
+ hideMainBarsAnimator
+ .play(fadeOutAnimator)
+ .with(ofTranslationY(0, -titleBarHeight, titleBar))
+ .with(ofTranslationY(0, bottomBarHeight, timeBar))
+ .with(ofTranslationY(0, bottomBarHeight, bottomBar));
+
+ hideProgressBarAnimator = new AnimatorSet();
+ hideProgressBarAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS);
+ hideProgressBarAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setUxState(UX_STATE_ANIMATING_HIDE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setUxState(UX_STATE_NONE_VISIBLE);
+ if (needToShowBars) {
+ if (styledPlayerControlView != null) {
+ styledPlayerControlView.post(showAllBars);
+ }
+ needToShowBars = false;
+ }
+ }
+ });
+ hideProgressBarAnimator
+ .play(ofTranslationY(bottomBarHeight, bottomBarHeight + progressBarHeight, timeBar))
+ .with(ofTranslationY(bottomBarHeight, bottomBarHeight + progressBarHeight, bottomBar));
+
+ hideAllBarsAnimator = new AnimatorSet();
+ hideAllBarsAnimator.setDuration(DURATION_FOR_HIDING_ANIMATION_MS);
+ hideAllBarsAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setUxState(UX_STATE_ANIMATING_HIDE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setUxState(UX_STATE_NONE_VISIBLE);
+ if (needToShowBars) {
+ if (styledPlayerControlView != null) {
+ styledPlayerControlView.post(showAllBars);
+ }
+ needToShowBars = false;
+ }
+ }
+ });
+ hideAllBarsAnimator
+ .play(fadeOutAnimator)
+ .with(ofTranslationY(0, -titleBarHeight, titleBar))
+ .with(ofTranslationY(0, bottomBarHeight + progressBarHeight, timeBar))
+ .with(ofTranslationY(0, bottomBarHeight + progressBarHeight, bottomBar));
+
+ showMainBarsAnimator = new AnimatorSet();
+ showMainBarsAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS);
+ showMainBarsAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setUxState(UX_STATE_ANIMATING_SHOW);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setUxState(UX_STATE_ALL_VISIBLE);
+ }
+ });
+ showMainBarsAnimator
+ .play(fadeInAnimator)
+ .with(ofTranslationY(-titleBarHeight, 0, titleBar))
+ .with(ofTranslationY(bottomBarHeight, 0, timeBar))
+ .with(ofTranslationY(bottomBarHeight, 0, bottomBar));
+
+ showAllBarsAnimator = new AnimatorSet();
+ showAllBarsAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS);
+ showAllBarsAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ setUxState(UX_STATE_ANIMATING_SHOW);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setUxState(UX_STATE_ALL_VISIBLE);
+ }
+ });
+ showAllBarsAnimator
+ .play(fadeInAnimator)
+ .with(ofTranslationY(-titleBarHeight, 0, titleBar))
+ .with(ofTranslationY(bottomBarHeight + progressBarHeight, 0, timeBar))
+ .with(ofTranslationY(bottomBarHeight + progressBarHeight, 0, bottomBar));
+
+ overflowShowAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ overflowShowAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS);
+ overflowShowAnimator.addUpdateListener(
+ animation -> animateOverflow((float) animation.getAnimatedValue()));
+ overflowShowAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (extraControlsScrollView != null) {
+ extraControlsScrollView.setVisibility(View.VISIBLE);
+ extraControlsScrollView.setTranslationX(extraControlsScrollView.getWidth());
+ extraControlsScrollView.scrollTo(extraControlsScrollView.getWidth(), 0);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (basicControls != null) {
+ basicControls.setVisibility(View.INVISIBLE);
+ }
+ }
+ });
+
+ overflowHideAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
+ overflowHideAnimator.setDuration(DURATION_FOR_SHOWING_ANIMATION_MS);
+ overflowHideAnimator.addUpdateListener(
+ animation -> animateOverflow((float) animation.getAnimatedValue()));
+ overflowHideAnimator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (basicControls != null) {
+ basicControls.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (extraControlsScrollView != null) {
+ extraControlsScrollView.setVisibility(View.INVISIBLE);
+ }
+ }
+ });
+ }
+
+ void onViewDetached(StyledPlayerControlView v) {
+ v.removeOnLayoutChangeListener(this);
+ }
+
+ boolean isFullyVisible() {
+ if (styledPlayerControlView == null) {
+ return false;
+ }
+ return uxState == UX_STATE_ALL_VISIBLE;
+ }
+
+ private void setUxState(int uxState) {
+ int prevUxState = this.uxState;
+ this.uxState = uxState;
+ if (styledPlayerControlView != null) {
+ StyledPlayerControlView styledPlayerControlView = this.styledPlayerControlView;
+ if (uxState == UX_STATE_NONE_VISIBLE) {
+ styledPlayerControlView.setVisibility(View.GONE);
+ } else if (prevUxState == UX_STATE_NONE_VISIBLE) {
+ styledPlayerControlView.setVisibility(View.VISIBLE);
+ }
+ // TODO(insun): Notify specific uxState. Currently reuses legacy visibility listener for API
+ // compatibility.
+ if (prevUxState != uxState) {
+ styledPlayerControlView.notifyOnVisibilityChange();
+ }
+ }
+ }
+
+ private boolean isAnimationDisabled() {
+ return disableAnimation;
+ }
+
+ private final Runnable showAllBars =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (isAnimationDisabled()) {
+ setUxState(UX_STATE_ALL_VISIBLE);
+ resetHideCallbacks();
+ return;
+ }
+
+ switch (uxState) {
+ case UX_STATE_NONE_VISIBLE:
+ if (showAllBarsAnimator != null) {
+ showAllBarsAnimator.start();
+ }
+ break;
+ case UX_STATE_ONLY_PROGRESS_VISIBLE:
+ if (showMainBarsAnimator != null) {
+ showMainBarsAnimator.start();
+ }
+ break;
+ case UX_STATE_ANIMATING_HIDE:
+ needToShowBars = true;
+ break;
+ case UX_STATE_ANIMATING_SHOW:
+ return;
+ default:
+ break;
+ }
+ resetHideCallbacks();
+ }
+ };
+
+ private final Runnable hideAllBars =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (hideAllBarsAnimator == null) {
+ return;
+ }
+ hideAllBarsAnimator.start();
+ }
+ };
+
+ private final Runnable hideMainBars =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (hideMainBarsAnimator == null) {
+ return;
+ }
+ hideMainBarsAnimator.start();
+ postDelayedRunnable(hideProgressBar, ANIMATION_INTERVAL_MS);
+ }
+ };
+
+ private final Runnable hideProgressBar =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (hideProgressBarAnimator == null) {
+ return;
+ }
+ hideProgressBarAnimator.start();
+ }
+ };
+
+ private final Runnable hideController =
+ new Runnable() {
+ @Override
+ public void run() {
+ setUxState(UX_STATE_NONE_VISIBLE);
+ }
+ };
+
+ private static ObjectAnimator ofTranslationY(float startValue, float endValue, View target) {
+ return ObjectAnimator.ofFloat(target, "translationY", startValue, endValue);
+ }
+
+ private void postDelayedRunnable(Runnable runnable, long interval) {
+ if (styledPlayerControlView != null && interval >= 0) {
+ styledPlayerControlView.postDelayed(runnable, interval);
+ }
+ }
+
+ private void animateOverflow(float animatedValue) {
+ if (extraControlsScrollView != null) {
+ int extraControlTranslationX =
+ (int) (extraControlsScrollView.getWidth() * (1 - animatedValue));
+ extraControlsScrollView.setTranslationX(extraControlTranslationX);
+ }
+
+ if (timeView != null) {
+ timeView.setAlpha(1 - animatedValue);
+ }
+ if (basicControls != null) {
+ basicControls.setAlpha(1 - animatedValue);
+ }
+ }
+
+ private final OnClickListener overflowListener =
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ resetHideCallbacks();
+ if (v.getId() == R.id.exo_overflow_show && overflowShowAnimator != null) {
+ overflowShowAnimator.start();
+ } else if (v.getId() == R.id.exo_overflow_hide && overflowHideAnimator != null) {
+ overflowHideAnimator.start();
+ }
+ }
+ };
+
+ @Override
+ public void onLayoutChange(
+ View v,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) {
+
+ boolean shouldBeMinimalMode = shouldBeMinimalMode();
+ if (isMinimalMode != shouldBeMinimalMode) {
+ isMinimalMode = shouldBeMinimalMode;
+ v.post(() -> updateLayoutForSizeChange());
+ }
+ boolean widthChanged = (right - left) != (oldRight - oldLeft);
+ if (!isMinimalMode && widthChanged) {
+ v.post(() -> onLayoutWidthChanged());
+ }
+ }
+
+ private static int getWidth(@Nullable View v) {
+ return (v != null ? v.getWidth() : 0);
+ }
+
+ private static int getHeight(@Nullable View v) {
+ return (v != null ? v.getHeight() : 0);
+ }
+
+ private boolean shouldBeMinimalMode() {
+ if (this.styledPlayerControlView == null) {
+ return isMinimalMode;
+ }
+ ViewGroup playerControlView = this.styledPlayerControlView;
+
+ int width =
+ playerControlView.getWidth()
+ - playerControlView.getPaddingLeft()
+ - playerControlView.getPaddingRight();
+ int height =
+ playerControlView.getHeight()
+ - playerControlView.getPaddingBottom()
+ - playerControlView.getPaddingTop();
+ int defaultModeWidth =
+ Math.max(
+ getWidth(embeddedTransportControls), getWidth(timeView) + getWidth(overflowShowButton));
+ int defaultModeHeight =
+ getHeight(embeddedTransportControls)
+ + getHeight(titleBar)
+ + getHeight(timeBar)
+ + getHeight(bottomBar);
+
+ return (width <= defaultModeWidth || height <= defaultModeHeight);
+ }
+
+ private void updateLayoutForSizeChange() {
+ if (this.styledPlayerControlView == null) {
+ return;
+ }
+ ViewGroup playerControlView = this.styledPlayerControlView;
+
+ if (minimalControls != null) {
+ minimalControls.setVisibility(isMinimalMode ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ View fullScreenButton = playerControlView.findViewById(R.id.exo_fullscreen);
+ if (fullScreenButton != null) {
+ ViewGroup parent = (ViewGroup) fullScreenButton.getParent();
+ parent.removeView(fullScreenButton);
+
+ if (isMinimalMode && minimalControls != null) {
+ minimalControls.addView(fullScreenButton);
+ } else if (!isMinimalMode && basicControls != null) {
+ int index = Math.max(0, basicControls.getChildCount() - 1);
+ basicControls.addView(fullScreenButton, index);
+ } else {
+ parent.addView(fullScreenButton);
+ }
+ }
+ if (timeBar != null) {
+ View timeBar = this.timeBar;
+ MarginLayoutParams timeBarParams = (MarginLayoutParams) timeBar.getLayoutParams();
+ int timeBarMarginBottom =
+ playerControlView
+ .getResources()
+ .getDimensionPixelSize(R.dimen.exo_custom_progress_margin_bottom);
+ timeBarParams.bottomMargin = (isMinimalMode ? 0 : timeBarMarginBottom);
+ timeBar.setLayoutParams(timeBarParams);
+ if (timeBar instanceof DefaultTimeBar
+ && uxState != UX_STATE_ANIMATING_HIDE
+ && uxState != UX_STATE_ANIMATING_SHOW) {
+ if (isMinimalMode || uxState != UX_STATE_ALL_VISIBLE) {
+ ((DefaultTimeBar) timeBar).hideScrubber();
+ } else {
+ ((DefaultTimeBar) timeBar).showScrubber();
+ }
+ }
+ }
+
+ int[] idsToHideInMinimalMode = {
+ R.id.exo_title_bar,
+ R.id.exo_bottom_bar,
+ R.id.exo_prev,
+ R.id.exo_next,
+ R.id.exo_rew,
+ R.id.exo_rew_with_amount,
+ R.id.exo_ffwd,
+ R.id.exo_ffwd_with_amount
+ };
+ for (int id : idsToHideInMinimalMode) {
+ View v = playerControlView.findViewById(id);
+ if (v != null) {
+ v.setVisibility(isMinimalMode ? View.INVISIBLE : View.VISIBLE);
+ }
+ }
+ }
+
+ private void onLayoutWidthChanged() {
+ if (basicControls == null || extraControls == null) {
+ return;
+ }
+ ViewGroup basicControls = this.basicControls;
+ ViewGroup extraControls = this.extraControls;
+
+ int width =
+ (styledPlayerControlView != null
+ ? styledPlayerControlView.getWidth()
+ - styledPlayerControlView.getPaddingLeft()
+ - styledPlayerControlView.getPaddingRight()
+ : 0);
+ int basicBottomBarWidth = getWidth(timeView);
+ for (int i = 0; i < basicControls.getChildCount(); ++i) {
+ basicBottomBarWidth += basicControls.getChildAt(i).getWidth();
+ }
+
+ // BasicControls keeps overflow button at least.
+ int minBasicControlsChildCount = 1;
+ // ExtraControls keeps overflow button and settings button at least.
+ int minExtraControlsChildCount = 2;
+
+ if (basicBottomBarWidth > width) {
+ // move control views from basicControls to extraControls
+ ArrayList movingChildren = new ArrayList<>();
+ int movingWidth = 0;
+ int endIndex = basicControls.getChildCount() - minBasicControlsChildCount;
+ for (int index = 0; index < endIndex; index++) {
+ View child = basicControls.getChildAt(index);
+ movingWidth += child.getWidth();
+ movingChildren.add(child);
+ if (basicBottomBarWidth - movingWidth <= width) {
+ break;
+ }
+ }
+
+ if (!movingChildren.isEmpty()) {
+ basicControls.removeViews(0, movingChildren.size());
+
+ for (View child : movingChildren) {
+ int index = extraControls.getChildCount() - minExtraControlsChildCount;
+ extraControls.addView(child, index);
+ }
+ }
+
+ } else {
+ // move controls from extraControls to basicControls if possible, else do nothing
+ ArrayList movingChildren = new ArrayList<>();
+ int movingWidth = 0;
+ int startIndex = extraControls.getChildCount() - minExtraControlsChildCount - 1;
+ for (int index = startIndex; index >= 0; index--) {
+ View child = extraControls.getChildAt(index);
+ movingWidth += child.getWidth();
+ if (basicBottomBarWidth + movingWidth > width) {
+ break;
+ }
+ movingChildren.add(child);
+ }
+
+ if (!movingChildren.isEmpty()) {
+ extraControls.removeViews(startIndex - movingChildren.size() + 1, movingChildren.size());
+
+ for (View child : movingChildren) {
+ basicControls.addView(child, 0);
+ }
+ }
+ }
+ }
+}
diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java
new file mode 100644
index 0000000000..46849979c8
--- /dev/null
+++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerView.java
@@ -0,0 +1,1709 @@
+/*
+ * 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 com.google.android.exoplayer2.ui;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ControlDispatcher;
+import com.google.android.exoplayer2.DefaultControlDispatcher;
+import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.PlaybackPreparer;
+import com.google.android.exoplayer2.Player;
+import com.google.android.exoplayer2.Player.DiscontinuityReason;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.Timeline.Period;
+import com.google.android.exoplayer2.metadata.Metadata;
+import com.google.android.exoplayer2.metadata.flac.PictureFrame;
+import com.google.android.exoplayer2.metadata.id3.ApicFrame;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.source.ads.AdsLoader;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.TextOutput;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
+import com.google.android.exoplayer2.ui.spherical.SingleTapListener;
+import com.google.android.exoplayer2.ui.spherical.SphericalGLSurfaceView;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.ErrorMessageProvider;
+import com.google.android.exoplayer2.util.RepeatModeUtil;
+import com.google.android.exoplayer2.util.Util;
+import com.google.android.exoplayer2.video.VideoDecoderGLSurfaceView;
+import com.google.android.exoplayer2.video.VideoListener;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
+import org.checkerframework.checker.nullness.qual.RequiresNonNull;
+
+/**
+ * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art
+ * during playback, and displays playback controls using a {@link StyledPlayerControlView}.
+ *
+ *