Commit a3bbcf33 by insun Committed by Oliver Woodman

Add StyledPlayerView and StyledPlayerControlView into ui/

Moved ui2/ code and resources into ui/

PiperOrigin-RevId: 318984707
parent 2e749f70
Showing with 6229 additions and 18 deletions
...@@ -202,6 +202,7 @@ ...@@ -202,6 +202,7 @@
* Upgrade Truth dependency from 0.44 to 1.0. * Upgrade Truth dependency from 0.44 to 1.0.
* Upgrade to JUnit 4.13-rc-2. * Upgrade to JUnit 4.13-rc-2.
* UI * UI
* Add `StyledPlayerView` and `StyledPlayerControlView`.
* Remove `SimpleExoPlayerView` and `PlaybackControlView`. * Remove `SimpleExoPlayerView` and `PlaybackControlView`.
* Remove deperecated `exo_simple_player_view.xml` and * Remove deperecated `exo_simple_player_view.xml` and
`exo_playback_control_view.xml` from resource. `exo_playback_control_view.xml` from resource.
......
...@@ -34,6 +34,7 @@ project.ext { ...@@ -34,6 +34,7 @@ project.ext {
androidxCollectionVersion = '1.1.0' androidxCollectionVersion = '1.1.0'
androidxMediaVersion = '1.0.1' androidxMediaVersion = '1.0.1'
androidxMultidexVersion = '2.0.0' androidxMultidexVersion = '2.0.0'
androidxRecyclerViewVersion = '1.1.0'
androidxTestCoreVersion = '1.2.0' androidxTestCoreVersion = '1.2.0'
androidxTestJUnitVersion = '1.1.1' androidxTestJUnitVersion = '1.1.1'
androidxTestRunnerVersion = '1.2.0' androidxTestRunnerVersion = '1.2.0'
......
...@@ -20,6 +20,7 @@ dependencies { ...@@ -20,6 +20,7 @@ dependencies {
api 'androidx.media:media:' + androidxMediaVersion api 'androidx.media:media:' + androidxMediaVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
implementation 'com.google.guava:guava:' + guavaVersion implementation 'com.google.guava:guava:' + guavaVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
......
/*
* 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.
*
* <p>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.
*
* <h3>Attributes</h3>
*
* The following attributes can be set on a StyledPlayerControlView when used in a layout XML file:
*
* <ul>
* <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls
* being automatically hidden, in milliseconds. Use zero if the controls should not
* automatically timeout.
* <ul>
* <li>Corresponding method: {@link #setShowTimeoutMs(int)}
* <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
* </ul>
* <li><b>{@code show_rewind_button}</b> - Whether the rewind button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowRewindButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code show_fastforward_button}</b> - Whether the fast forward button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowFastForwardButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code show_previous_button}</b> - Whether the previous button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowPreviousButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code show_next_button}</b> - Whether the next button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowNextButton(boolean)}
* <li>Default: true
* </ul>
* <li><b>{@code rewind_increment}</b> - The duration of the rewind applied when the user taps the
* rewind button, in milliseconds. Use zero to disable the rewind button.
* <ul>
* <li>Corresponding method: {@link #setControlDispatcher(ControlDispatcher)}
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_REWIND_MS}
* </ul>
* <li><b>{@code fastforward_increment}</b> - Like {@code rewind_increment}, but for fast forward.
* <ul>
* <li>Corresponding method: {@link #setControlDispatcher(ControlDispatcher)}
* <li>Default: {@link DefaultControlDispatcher#DEFAULT_FAST_FORWARD_MS}
* </ul>
* <li><b>{@code repeat_toggle_modes}</b> - A flagged enumeration value specifying which repeat
* mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all},
* or {@code one|all}.
* <ul>
* <li>Corresponding method: {@link #setRepeatToggleModes(int)}
* <li>Default: {@link #DEFAULT_REPEAT_TOGGLE_MODES}
* </ul>
* <li><b>{@code show_shuffle_button}</b> - Whether the shuffle button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
* <li>Default: false
* </ul>
* <li><b>{@code disable_animation}</b> - Whether animation is applied when hide and show
* controls.
* <ul>
* <li>Corresponding method: None
* <li>Default: false
* </ul>
* <li><b>{@code time_bar_min_update_interval}</b> - Specifies the minimum interval between time
* bar position updates.
* <ul>
* <li>Corresponding method: {@link #setTimeBarMinUpdateInterval(int)}
* <li>Default: {@link #DEFAULT_TIME_BAR_MIN_UPDATE_INTERVAL_MS}
* </ul>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout to be inflated. See
* below for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.layout.exo_styled_player_control_view}
* </ul>
* <li>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).
* </ul>
*
* <h3>Overriding drawables</h3>
*
* 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:
*
* <ul>
* <li><b>{@code exo_styled_controls_play}</b> - The play icon.
* <li><b>{@code exo_styled_controls_pause}</b> - The pause icon.
* <li><b>{@code exo_styled_controls_rewind}</b> - The background of rewind icon.
* <li><b>{@code exo_styled_controls_fastforward}</b> - The background of fast forward icon.
* <li><b>{@code exo_styled_controls_previous}</b> - The previous icon.
* <li><b>{@code exo_styled_controls_next}</b> - The next icon.
* <li><b>{@code exo_styled_controls_repeat_off}</b> - The repeat icon for {@link
* Player#REPEAT_MODE_OFF}.
* <li><b>{@code exo_styled_controls_repeat_one}</b> - The repeat icon for {@link
* Player#REPEAT_MODE_ONE}.
* <li><b>{@code exo_styled_controls_repeat_all}</b> - The repeat icon for {@link
* Player#REPEAT_MODE_ALL}.
* <li><b>{@code exo_styled_controls_shuffle_off}</b> - The shuffle icon when shuffling is
* disabled.
* <li><b>{@code exo_styled_controls_shuffle_on}</b> - The shuffle icon when shuffling is enabled.
* <li><b>{@code exo_styled_controls_vr}</b> - The VR icon.
* </ul>
*
* <h3>Overriding the layout file</h3>
*
* 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 <a
* href="CustomLayout">Specifying a custom layout file</a>.
*
* <p>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:
*
* <ul>
* <li><b>{@code exo_play}</b> - The play button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_pause}</b> - The pause button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_rew}</b> - The rewind button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_rew_with_amount}</b> - The rewind button with rewind amount.
* <ul>
* <li>Type: {@link TextView}
* <li>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.
* </ul>
* <li><b>{@code exo_ffwd}</b> - The fast forward button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_ffwd_with_amount}</b> - The fast forward button with fast forward amount.
* <ul>
* <li>Type: {@link TextView}
* <li>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.
* </ul>
* <li><b>{@code exo_prev}</b> - The previous button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_next}</b> - The next button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_repeat_toggle}</b> - The repeat toggle button.
* <ul>
* <li>Type: {@link ImageView}
* <li>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.
* </ul>
* <li><b>{@code exo_shuffle}</b> - The shuffle button.
* <ul>
* <li>Type: {@link ImageView}
* <li>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.
* </ul>
* <li><b>{@code exo_vr}</b> - The VR mode button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_position}</b> - Text view displaying the current playback position.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_duration}</b> - Text view displaying the current media duration.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_progress_placeholder}</b> - A placeholder that's replaced with the inflated
* {@link DefaultTimeBar}. Ignored if an {@code exo_progress} view exists.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_progress}</b> - 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.
* <ul>
* <li>Type: {@link TimeBar}
* </ul>
* </ul>
*
* <p>All child views are optional and so can be omitted if not required, however where defined they
* must be of the expected type.
*
* <h3 id="CustomLayout">Specifying a custom layout file</h3>
*
* 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.
*
* <p>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<VisibilityListener> 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<String> playbackSpeedTextList;
private List<Integer> 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<String> 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<Integer>();
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.
*
* <p>Note that smaller intervals, e.g. 33ms, will result in a smooth movement but will use more
* CPU resources while the time bar is visible, whereas larger intervals, e.g. 200ms, will result
* in a step-wise update with less CPU usage.
*
* @param minUpdateIntervalMs The minimum interval between time bar position updates, in
* milliseconds.
*/
public void setTimeBarMinUpdateInterval(int minUpdateIntervalMs) {
// Do not accept values below 16ms (60fps) and larger than the maximum update interval.
timeBarMinUpdateIntervalMs =
Util.constrainValue(minUpdateIntervalMs, 16, MAX_UPDATE_INTERVAL_MS);
}
/**
* 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 <code>null</code> 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<TrackInfo> textTracks = new ArrayList<>();
List<TrackInfo> audioTracks = new ArrayList<>();
List<Integer> textRendererIndices = new ArrayList<>();
List<Integer> 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<TrackInfo> 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<SettingsAdapter.SettingsViewHolder> {
private List<String> mainTexts;
@Nullable private List<String> subTexts;
@Nullable private TypedArray iconIds;
public SettingsAdapter(List<String> 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<SubSettingsAdapter.SubSettingsViewHolder> {
@Nullable private List<String> 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<String> 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<Integer> rendererIndices,
List<TrackInfo> 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<Integer> rendererIndices,
List<TrackInfo> 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<TrackSelectionViewHolder> {
protected List<Integer> rendererIndices;
protected List<TrackInfo> tracks;
protected @Nullable MappedTrackInfo mappedTrackInfo;
public TrackSelectionAdapter() {
this.rendererIndices = new ArrayList<>();
this.tracks = new ArrayList<>();
this.mappedTrackInfo = null;
}
public abstract void init(
List<Integer> rendererIndices, List<TrackInfo> 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);
}
}
}
/*
* 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<View> 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<View> 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);
}
}
}
}
}
/*
* 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}.
*
* <p>A StyledPlayerView 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.
*
* <h3>Attributes</h3>
*
* The following attributes can be set on a StyledPlayerView when used in a layout XML file:
*
* <ul>
* <li><b>{@code use_artwork}</b> - Whether artwork is used if available in audio streams.
* <ul>
* <li>Corresponding method: {@link #setUseArtwork(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code default_artwork}</b> - Default artwork to use if no artwork available in audio
* streams.
* <ul>
* <li>Corresponding method: {@link #setDefaultArtwork(Drawable)}
* <li>Default: {@code null}
* </ul>
* <li><b>{@code use_controller}</b> - Whether the playback controls can be shown.
* <ul>
* <li>Corresponding method: {@link #setUseController(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code hide_on_touch}</b> - Whether the playback controls are hidden by touch events.
* <ul>
* <li>Corresponding method: {@link #setControllerHideOnTouch(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code auto_show}</b> - Whether the playback controls are automatically shown when
* playback starts, pauses, ends, or fails. If set to false, the playback controls can be
* manually operated with {@link #showController()} and {@link #hideController()}.
* <ul>
* <li>Corresponding method: {@link #setControllerAutoShow(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code hide_during_ads}</b> - Whether the playback controls are hidden during ads.
* Controls are always shown during ads if they are enabled and the player is paused.
* <ul>
* <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code show_buffering}</b> - Whether the buffering spinner is displayed when the player
* is buffering. Valid values are {@code never}, {@code when_playing} and {@code always}.
* <ul>
* <li>Corresponding method: {@link #setShowBuffering(int)}
* <li>Default: {@code never}
* </ul>
* <li><b>{@code resize_mode}</b> - Controls how video and album art is resized within the view.
* Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}.
* <ul>
* <li>Corresponding method: {@link #setResizeMode(int)}
* <li>Default: {@code fit}
* </ul>
* <li><b>{@code surface_type}</b> - The type of surface view used for video playbacks. Valid
* values are {@code surface_view}, {@code texture_view}, {@code spherical_gl_surface_view},
* {@code video_decoder_gl_surface_view} and {@code none}. Using {@code none} is recommended
* for audio only applications, since creating the surface can be expensive. Using {@code
* surface_view} is recommended for video applications. Note, TextureView can only be used in
* a hardware accelerated window. When rendered in software, TextureView will draw nothing.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code surface_view}
* </ul>
* <li><b>{@code use_sensor_rotation}</b> - Whether to use the orientation sensor for rotation
* during spherical playbacks (if available).
* <ul>
* <li>Corresponding method: {@link #setUseSensorRotation(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter}
* view.
* <ul>
* <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
* <li>Default: {@code unset}
* </ul>
* <li><b>{@code keep_content_on_player_reset}</b> - Whether the currently displayed video frame
* or media artwork is kept visible when the player is reset.
* <ul>
* <li>Corresponding method: {@link #setKeepContentOnPlayerReset(boolean)}
* <li>Default: {@code false}
* </ul>
* <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
* for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.layout.exo_styled_player_view}
* </ul>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout resource to be
* inflated by the child {@link StyledPlayerControlView}. See below for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.layout.exo_styled_player_control_view}
* </ul>
* <li>All attributes that can be set on {@link StyledPlayerControlView} and {@link
* DefaultTimeBar} can also be set on a StyledPlayerView, and will be propagated to the
* inflated {@link StyledPlayerControlView} unless the layout is overridden to specify a
* custom {@code exo_controller} (see below).
* </ul>
*
* <h3>Overriding drawables</h3>
*
* The drawables used by {@link StyledPlayerControlView} (with its default layout file) can be
* overridden by drawables with the same names defined in your application. See the {@link
* StyledPlayerControlView} documentation for a list of drawables that can be overridden.
*
* <h3>Overriding the layout file</h3>
*
* To customize the layout of StyledPlayerView throughout your app, or just for certain
* configurations, you can define {@code exo_player_view.xml} layout files in your application
* {@code res/layout*} directories. These layouts will override the one provided by the ExoPlayer
* library, and will be inflated for use by StyledPlayerView. The view identifies and binds its
* children by looking for the following ids:
*
* <ul>
* <li><b>{@code exo_content_frame}</b> - A frame whose aspect ratio is resized based on the video
* or album art of the media being played, and the configured {@code resize_mode}. The video
* surface view is inflated into this frame as its first child.
* <ul>
* <li>Type: {@link AspectRatioFrameLayout}
* </ul>
* <li><b>{@code exo_shutter}</b> - A view that's made visible when video should be hidden. This
* view is typically an opaque view that covers the video surface, thereby obscuring it when
* visible. Obscuring the surface in this way also helps to prevent flicker at the start of
* playback when {@code surface_type="surface_view"}.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_buffering}</b> - A view that's made visible when the player is buffering.
* This view typically displays a buffering spinner or animation.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_subtitles}</b> - Displays subtitles.
* <ul>
* <li>Type: {@link SubtitleView}
* </ul>
* <li><b>{@code exo_artwork}</b> - Displays album art.
* <ul>
* <li>Type: {@link ImageView}
* </ul>
* <li><b>{@code exo_error_message}</b> - Displays an error message to the user if playback fails.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_controller_placeholder}</b> - A placeholder that's replaced with the inflated
* {@link StyledPlayerControlView}. Ignored if an {@code exo_controller} view exists.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_controller}</b> - An already inflated {@link StyledPlayerControlView}. Allows
* use of a custom extension of {@link StyledPlayerControlView}. {@link
* StyledPlayerControlView} and {@link DefaultTimeBar} attributes set on the StyledPlayerView
* will not be automatically propagated through to this instance. If a view exists with this
* id, any {@code exo_controller_placeholder} view will be ignored.
* <ul>
* <li>Type: {@link StyledPlayerControlView}
* </ul>
* <li><b>{@code exo_ad_overlay}</b> - A {@link FrameLayout} positioned on top of the player which
* is used to show ad UI (if applicable).
* <ul>
* <li>Type: {@link FrameLayout}
* </ul>
* <li><b>{@code exo_overlay}</b> - A {@link FrameLayout} positioned on top of the player which
* the app can access via {@link #getOverlayFrameLayout()}, provided for convenience.
* <ul>
* <li>Type: {@link FrameLayout}
* </ul>
* </ul>
*
* <p>All child views are optional and so can be omitted if not required, however where defined they
* must be of the expected type.
*
* <h3>Specifying a custom layout file</h3>
*
* Defining your own {@code exo_styled_player_view.xml} is useful to customize the layout of
* StyledPlayerView 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 player_layout_id}
* attribute on a StyledPlayerView. This will cause the specified layout to be inflated instead of
* {@code exo_styled_player_view.xml} for only the instance on which the attribute is set.
*/
public class StyledPlayerView extends FrameLayout implements AdsLoader.AdViewProvider {
// LINT.IfChange
/**
* Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link
* #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS})
public @interface ShowBuffering {}
/** The buffering view is never shown. */
public static final int SHOW_BUFFERING_NEVER = 0;
/**
* The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering}
* state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}.
*/
public static final int SHOW_BUFFERING_WHEN_PLAYING = 1;
/**
* The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING
* buffering} state.
*/
public static final int SHOW_BUFFERING_ALWAYS = 2;
// LINT.ThenChange(../../../../../../res/values/attrs.xml)
// LINT.IfChange
private static final int SURFACE_TYPE_NONE = 0;
private static final int SURFACE_TYPE_SURFACE_VIEW = 1;
private static final int SURFACE_TYPE_TEXTURE_VIEW = 2;
private static final int SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW = 3;
private static final int SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW = 4;
// LINT.ThenChange(../../../../../../res/values/attrs.xml)
private final ComponentListener componentListener;
@Nullable private final AspectRatioFrameLayout contentFrame;
@Nullable private final View shutterView;
@Nullable private final View surfaceView;
@Nullable private final ImageView artworkView;
@Nullable private final SubtitleView subtitleView;
@Nullable private final View bufferingView;
@Nullable private final TextView errorMessageView;
@Nullable private final StyledPlayerControlView controller;
@Nullable private final FrameLayout adOverlayFrameLayout;
@Nullable private final FrameLayout overlayFrameLayout;
@Nullable private Player player;
private boolean useController;
@Nullable private StyledPlayerControlView.VisibilityListener controllerVisibilityListener;
private boolean useArtwork;
@Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset;
private boolean useSensorRotation;
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
@Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs;
private boolean controllerAutoShow;
private boolean controllerHideDuringAds;
private boolean controllerHideOnTouch;
private int textureViewRotation;
private boolean isTouching;
private static final int PICTURE_TYPE_FRONT_COVER = 3;
private static final int PICTURE_TYPE_NOT_SET = -1;
public StyledPlayerView(Context context) {
this(context, /* attrs= */ null);
}
public StyledPlayerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, /* defStyleAttr= */ 0);
}
@SuppressWarnings({"nullness:argument.type.incompatible", "nullness:method.invocation.invalid"})
public StyledPlayerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
componentListener = new ComponentListener();
if (isInEditMode()) {
contentFrame = null;
shutterView = null;
surfaceView = null;
artworkView = null;
subtitleView = null;
bufferingView = null;
errorMessageView = null;
controller = null;
adOverlayFrameLayout = null;
overlayFrameLayout = null;
ImageView logo = new ImageView(context);
if (Util.SDK_INT >= 23) {
configureEditModeLogoV23(getResources(), logo);
} else {
configureEditModeLogo(getResources(), logo);
}
addView(logo);
return;
}
boolean shutterColorSet = false;
int shutterColor = 0;
int playerLayoutId = R.layout.exo_player_view;
boolean useArtwork = true;
int defaultArtworkId = 0;
boolean useController = true;
int surfaceType = SURFACE_TYPE_SURFACE_VIEW;
int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
int controllerShowTimeoutMs = StyledPlayerControlView.DEFAULT_SHOW_TIMEOUT_MS;
boolean controllerHideOnTouch = true;
boolean controllerAutoShow = true;
boolean controllerHideDuringAds = true;
int showBuffering = SHOW_BUFFERING_NEVER;
useSensorRotation = true;
if (attrs != null) {
TypedArray a =
context.getTheme().obtainStyledAttributes(attrs, R.styleable.StyledPlayerView, 0, 0);
try {
shutterColorSet = a.hasValue(R.styleable.StyledPlayerView_shutter_background_color);
shutterColor =
a.getColor(R.styleable.StyledPlayerView_shutter_background_color, shutterColor);
playerLayoutId =
a.getResourceId(R.styleable.StyledPlayerView_player_layout_id, playerLayoutId);
useArtwork = a.getBoolean(R.styleable.StyledPlayerView_use_artwork, useArtwork);
defaultArtworkId =
a.getResourceId(R.styleable.StyledPlayerView_default_artwork, defaultArtworkId);
useController = a.getBoolean(R.styleable.StyledPlayerView_use_controller, useController);
surfaceType = a.getInt(R.styleable.StyledPlayerView_surface_type, surfaceType);
resizeMode = a.getInt(R.styleable.StyledPlayerView_resize_mode, resizeMode);
controllerShowTimeoutMs =
a.getInt(R.styleable.StyledPlayerView_show_timeout, controllerShowTimeoutMs);
controllerHideOnTouch =
a.getBoolean(R.styleable.StyledPlayerView_hide_on_touch, controllerHideOnTouch);
controllerAutoShow =
a.getBoolean(R.styleable.StyledPlayerView_auto_show, controllerAutoShow);
showBuffering = a.getInteger(R.styleable.StyledPlayerView_show_buffering, showBuffering);
keepContentOnPlayerReset =
a.getBoolean(
R.styleable.StyledPlayerView_keep_content_on_player_reset,
keepContentOnPlayerReset);
controllerHideDuringAds =
a.getBoolean(R.styleable.StyledPlayerView_hide_during_ads, controllerHideDuringAds);
useSensorRotation =
a.getBoolean(R.styleable.StyledPlayerView_use_sensor_rotation, useSensorRotation);
} finally {
a.recycle();
}
}
LayoutInflater.from(context).inflate(playerLayoutId, this);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
// Content frame.
contentFrame = findViewById(R.id.exo_content_frame);
if (contentFrame != null) {
setResizeModeRaw(contentFrame, resizeMode);
}
// Shutter view.
shutterView = findViewById(R.id.exo_shutter);
if (shutterView != null && shutterColorSet) {
shutterView.setBackgroundColor(shutterColor);
}
// Create a surface view and insert it into the content frame, if there is one.
if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) {
ViewGroup.LayoutParams params =
new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
switch (surfaceType) {
case SURFACE_TYPE_TEXTURE_VIEW:
surfaceView = new TextureView(context);
break;
case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW:
SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context);
sphericalGLSurfaceView.setSingleTapListener(componentListener);
sphericalGLSurfaceView.setUseSensorRotation(useSensorRotation);
surfaceView = sphericalGLSurfaceView;
break;
case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW:
surfaceView = new VideoDecoderGLSurfaceView(context);
break;
default:
surfaceView = new SurfaceView(context);
break;
}
surfaceView.setLayoutParams(params);
contentFrame.addView(surfaceView, 0);
} else {
surfaceView = null;
}
// Ad overlay frame layout.
adOverlayFrameLayout = findViewById(R.id.exo_ad_overlay);
// Overlay frame layout.
overlayFrameLayout = findViewById(R.id.exo_overlay);
// Artwork view.
artworkView = findViewById(R.id.exo_artwork);
this.useArtwork = useArtwork && artworkView != null;
if (defaultArtworkId != 0) {
defaultArtwork = ContextCompat.getDrawable(getContext(), defaultArtworkId);
}
// Subtitle view.
subtitleView = findViewById(R.id.exo_subtitles);
if (subtitleView != null) {
subtitleView.setUserDefaultStyle();
subtitleView.setUserDefaultTextSize();
}
// Buffering view.
bufferingView = findViewById(R.id.exo_buffering);
if (bufferingView != null) {
bufferingView.setVisibility(View.GONE);
}
this.showBuffering = showBuffering;
// Error message view.
errorMessageView = findViewById(R.id.exo_error_message);
if (errorMessageView != null) {
errorMessageView.setVisibility(View.GONE);
}
// Playback control view.
StyledPlayerControlView customController = findViewById(R.id.exo_controller);
View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder);
if (customController != null) {
this.controller = customController;
} else if (controllerPlaceholder != null) {
// Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are
// transferred, but standard attributes (e.g. background) are not.
this.controller = new StyledPlayerControlView(context, null, 0, attrs);
controller.setId(R.id.exo_controller);
controller.setLayoutParams(controllerPlaceholder.getLayoutParams());
ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent());
int controllerIndex = parent.indexOfChild(controllerPlaceholder);
parent.removeView(controllerPlaceholder);
parent.addView(controller, controllerIndex);
} else {
this.controller = null;
}
this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0;
this.controllerHideOnTouch = controllerHideOnTouch;
this.controllerAutoShow = controllerAutoShow;
this.controllerHideDuringAds = controllerHideDuringAds;
this.useController = useController && controller != null;
hideController();
updateContentDescription();
if (controller != null) {
controller.addVisibilityListener(/* listener= */ componentListener);
}
}
/**
* Switches the view targeted by a given {@link Player}.
*
* @param player The player whose target view is being switched.
* @param oldPlayerView The old view to detach from the player.
* @param newPlayerView The new view to attach to the player.
*/
public static void switchTargetView(
Player player,
@Nullable StyledPlayerView oldPlayerView,
@Nullable StyledPlayerView newPlayerView) {
if (oldPlayerView == newPlayerView) {
return;
}
// We attach the new view before detaching the old one because this ordering allows the player
// to swap directly from one surface to another, without transitioning through a state where no
// surface is attached. This is significantly more efficient and achieves a more seamless
// transition when using platform provided video decoders.
if (newPlayerView != null) {
newPlayerView.setPlayer(player);
}
if (oldPlayerView != null) {
oldPlayerView.setPlayer(null);
}
}
/** Returns the player currently set on this view, or null if no player is set. */
@Nullable
public Player getPlayer() {
return player;
}
/**
* Set the {@link Player} to use.
*
* <p>To transition a {@link Player} from targeting one view to another, it's recommended to use
* {@link #switchTargetView(Player, StyledPlayerView, StyledPlayerView)} rather than this method.
* If you do wish to use this method directly, be sure to attach the player to the new view
* <em>before</em> calling {@code setPlayer(null)} to detach it from the old one. This ordering is
* significantly more efficient and may allow for more seamless transitions.
*
* @param player The {@link Player} to use, 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;
}
@Nullable Player oldPlayer = this.player;
if (oldPlayer != null) {
oldPlayer.removeListener(componentListener);
@Nullable Player.VideoComponent oldVideoComponent = oldPlayer.getVideoComponent();
if (oldVideoComponent != null) {
oldVideoComponent.removeVideoListener(componentListener);
if (surfaceView instanceof TextureView) {
oldVideoComponent.clearVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SphericalGLSurfaceView) {
((SphericalGLSurfaceView) surfaceView).setVideoComponent(null);
} else if (surfaceView instanceof VideoDecoderGLSurfaceView) {
oldVideoComponent.setVideoDecoderOutputBufferRenderer(null);
} else if (surfaceView instanceof SurfaceView) {
oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView);
}
}
@Nullable Player.TextComponent oldTextComponent = oldPlayer.getTextComponent();
if (oldTextComponent != null) {
oldTextComponent.removeTextOutput(componentListener);
}
}
if (subtitleView != null) {
subtitleView.setCues(null);
}
this.player = player;
if (useController()) {
controller.setPlayer(player);
}
updateBuffering();
updateErrorMessage();
updateForCurrentTrackSelections(/* isNewPlayer= */ true);
if (player != null) {
@Nullable Player.VideoComponent newVideoComponent = player.getVideoComponent();
if (newVideoComponent != null) {
if (surfaceView instanceof TextureView) {
newVideoComponent.setVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SphericalGLSurfaceView) {
((SphericalGLSurfaceView) surfaceView).setVideoComponent(newVideoComponent);
} else if (surfaceView instanceof VideoDecoderGLSurfaceView) {
newVideoComponent.setVideoDecoderOutputBufferRenderer(
((VideoDecoderGLSurfaceView) surfaceView).getVideoDecoderOutputBufferRenderer());
} else if (surfaceView instanceof SurfaceView) {
newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView);
}
newVideoComponent.addVideoListener(componentListener);
}
@Nullable Player.TextComponent newTextComponent = player.getTextComponent();
if (newTextComponent != null) {
newTextComponent.addTextOutput(componentListener);
if (subtitleView != null) {
subtitleView.setCues(newTextComponent.getCurrentCues());
}
}
player.addListener(componentListener);
maybeShowController(false);
} else {
hideController();
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (surfaceView instanceof SurfaceView) {
// Work around https://github.com/google/ExoPlayer/issues/3160.
surfaceView.setVisibility(visibility);
}
}
/**
* Sets the {@link ResizeMode}.
*
* @param resizeMode The {@link ResizeMode}.
*/
public void setResizeMode(@ResizeMode int resizeMode) {
Assertions.checkStateNotNull(contentFrame);
contentFrame.setResizeMode(resizeMode);
}
/** Returns the {@link ResizeMode}. */
public @ResizeMode int getResizeMode() {
Assertions.checkStateNotNull(contentFrame);
return contentFrame.getResizeMode();
}
/** Returns whether artwork is displayed if present in the media. */
public boolean getUseArtwork() {
return useArtwork;
}
/**
* Sets whether artwork is displayed if present in the media.
*
* @param useArtwork Whether artwork is displayed.
*/
public void setUseArtwork(boolean useArtwork) {
Assertions.checkState(!useArtwork || artworkView != null);
if (this.useArtwork != useArtwork) {
this.useArtwork = useArtwork;
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
}
/** Returns the default artwork to display. */
@Nullable
public Drawable getDefaultArtwork() {
return defaultArtwork;
}
/**
* Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is
* present in the media.
*
* @param defaultArtwork the default artwork to display
*/
public void setDefaultArtwork(@Nullable Drawable defaultArtwork) {
if (this.defaultArtwork != defaultArtwork) {
this.defaultArtwork = defaultArtwork;
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
}
/** Returns whether the playback controls can be shown. */
public boolean getUseController() {
return useController;
}
/**
* Sets whether the playback controls can be shown. If set to {@code false} the playback controls
* are never visible and are disconnected from the player.
*
* @param useController Whether the playback controls can be shown.
*/
public void setUseController(boolean useController) {
Assertions.checkState(!useController || controller != null);
if (this.useController == useController) {
return;
}
this.useController = useController;
if (useController()) {
controller.setPlayer(player);
} else if (controller != null) {
controller.hide();
controller.setPlayer(/* player= */ null);
}
updateContentDescription();
}
/**
* Sets the background color of the {@code exo_shutter} view.
*
* @param color The background color.
*/
public void setShutterBackgroundColor(int color) {
if (shutterView != null) {
shutterView.setBackgroundColor(color);
}
}
/**
* Sets whether the currently displayed video frame or media artwork is kept visible when the
* player is reset. A player reset is defined to mean the player being re-prepared with different
* media, the player transitioning to unprepared media, {@link Player#stop(boolean)} being called
* with {@code reset=true}, or the player being replaced or cleared by calling {@link
* #setPlayer(Player)}.
*
* <p>If enabled, the currently displayed video frame or media artwork will be kept visible until
* the player set on the view has been successfully prepared with new media and loaded enough of
* it to have determined the available tracks. Hence enabling this option allows transitioning
* from playing one piece of media to another, or from using one player instance to another,
* without clearing the view's content.
*
* <p>If disabled, the currently displayed video frame or media artwork will be hidden as soon as
* the player is reset. Note that the video frame is hidden by making {@code exo_shutter} visible.
* Hence the video frame will not be hidden if using a custom layout that omits this view.
*
* @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is
* kept visible when the player is reset.
*/
public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) {
if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) {
this.keepContentOnPlayerReset = keepContentOnPlayerReset;
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
}
/**
* Sets whether to use the orientation sensor for rotation during spherical playbacks (if
* available)
*
* @param useSensorRotation Whether to use the orientation sensor for rotation during spherical
* playbacks.
*/
public void setUseSensorRotation(boolean useSensorRotation) {
if (this.useSensorRotation != useSensorRotation) {
this.useSensorRotation = useSensorRotation;
if (surfaceView instanceof SphericalGLSurfaceView) {
((SphericalGLSurfaceView) surfaceView).setUseSensorRotation(useSensorRotation);
}
}
}
/**
* Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default.
*
* @param showBuffering The mode that defines when the buffering spinner is displayed. One of
* {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and {@link
* #SHOW_BUFFERING_ALWAYS}.
*/
public void setShowBuffering(@ShowBuffering int showBuffering) {
if (this.showBuffering != showBuffering) {
this.showBuffering = showBuffering;
updateBuffering();
}
}
/**
* Sets the optional {@link ErrorMessageProvider}.
*
* @param errorMessageProvider The error message provider.
*/
public void setErrorMessageProvider(
@Nullable ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider) {
if (this.errorMessageProvider != errorMessageProvider) {
this.errorMessageProvider = errorMessageProvider;
updateErrorMessage();
}
}
/**
* Sets a custom error message to be displayed by the view. The error message will be displayed
* permanently, unless it is cleared by passing {@code null} to this method.
*
* @param message The message to display, or {@code null} to clear a previously set message.
*/
public void setCustomErrorMessage(@Nullable CharSequence message) {
Assertions.checkState(errorMessageView != null);
customErrorMessage = message;
updateErrorMessage();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (player != null && player.isPlayingAd()) {
return super.dispatchKeyEvent(event);
}
boolean isDpadKey = isDpadKey(event.getKeyCode());
boolean handled = false;
if (isDpadKey && useController() && !controller.isFullyVisible()) {
// Handle the key event by showing the controller.
maybeShowController(true);
handled = true;
} else if (dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event)) {
// The key event was handled as a media key or by the super class. We should also show the
// controller, or extend its show timeout if already visible.
maybeShowController(true);
handled = true;
} else if (isDpadKey && useController()) {
// The key event wasn't handled, but we should extend the controller's show timeout.
maybeShowController(true);
}
return handled;
}
/**
* Called to process media key events. Any {@link KeyEvent} can be passed but only media key
* events will be handled. Does nothing if playback controls are disabled.
*
* @param event A key event.
* @return Whether the key event was handled.
*/
public boolean dispatchMediaKeyEvent(KeyEvent event) {
return useController() && controller.dispatchMediaKeyEvent(event);
}
/** Returns whether the controller is currently fully visible. */
public boolean isControllerFullyVisible() {
return controller != null && controller.isFullyVisible();
}
/**
* Shows the playback controls. Does nothing if playback controls are disabled.
*
* <p>The playback controls are automatically hidden during playback after {{@link
* #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet,
* is paused, has ended or failed.
*/
public void showController() {
showController(shouldShowControllerIndefinitely());
}
/** Hides the playback controls. Does nothing if playback controls are disabled. */
public void hideController() {
if (controller != null) {
controller.hide();
}
}
/**
* Returns the playback controls timeout. The playback controls are automatically hidden after
* this duration of time has elapsed without user input and with playback or buffering in
* progress.
*
* @return The timeout in milliseconds. A non-positive value will cause the controller to remain
* visible indefinitely.
*/
public int getControllerShowTimeoutMs() {
return controllerShowTimeoutMs;
}
/**
* Sets the playback controls timeout. The playback controls are automatically hidden after this
* duration of time has elapsed without user input and with playback or buffering in progress.
*
* @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the
* controller to remain visible indefinitely.
*/
public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
Assertions.checkStateNotNull(controller);
this.controllerShowTimeoutMs = controllerShowTimeoutMs;
if (controller.isFullyVisible()) {
// Update the controller's timeout if necessary.
showController();
}
}
/** Returns whether the playback controls are hidden by touch events. */
public boolean getControllerHideOnTouch() {
return controllerHideOnTouch;
}
/**
* Sets whether the playback controls are hidden by touch events.
*
* @param controllerHideOnTouch Whether the playback controls are hidden by touch events.
*/
public void setControllerHideOnTouch(boolean controllerHideOnTouch) {
Assertions.checkStateNotNull(controller);
this.controllerHideOnTouch = controllerHideOnTouch;
updateContentDescription();
}
/**
* Returns whether the playback controls are automatically shown when playback starts, pauses,
* ends, or fails. If set to false, the playback controls can be manually operated with {@link
* #showController()} and {@link #hideController()}.
*/
public boolean getControllerAutoShow() {
return controllerAutoShow;
}
/**
* Sets whether the playback controls are automatically shown when playback starts, pauses, ends,
* or fails. If set to false, the playback controls can be manually operated with {@link
* #showController()} and {@link #hideController()}.
*
* @param controllerAutoShow Whether the playback controls are allowed to show automatically.
*/
public void setControllerAutoShow(boolean controllerAutoShow) {
this.controllerAutoShow = controllerAutoShow;
}
/**
* Sets whether the playback controls are hidden when ads are playing. Controls are always shown
* during ads if they are enabled and the player is paused.
*
* @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing.
*/
public void setControllerHideDuringAds(boolean controllerHideDuringAds) {
this.controllerHideDuringAds = controllerHideDuringAds;
}
/**
* Set the {@link StyledPlayerControlView.VisibilityListener}.
*
* @param listener The listener to be notified about visibility changes, or null to remove the
* current listener.
*/
public void setControllerVisibilityListener(
@Nullable StyledPlayerControlView.VisibilityListener listener) {
Assertions.checkStateNotNull(controller);
if (this.controllerVisibilityListener == listener) {
return;
}
if (this.controllerVisibilityListener != null) {
controller.removeVisibilityListener(this.controllerVisibilityListener);
}
this.controllerVisibilityListener = listener;
if (listener != null) {
controller.addVisibilityListener(listener);
}
}
/**
* Sets the {@link StyledPlayerControlView.OnFullScreenModeChangedListener}.
*
* @param listener The listener to be notified when the fullscreen button is clicked, or null to
* remove the current listener and hide the fullscreen button.
*/
public void setControllerOnFullScreenModeChangedListener(
@Nullable StyledPlayerControlView.OnFullScreenModeChangedListener listener) {
Assertions.checkStateNotNull(controller);
controller.setOnFullScreenModeChangedListener(listener);
}
/**
* Sets the {@link PlaybackPreparer}.
*
* @param playbackPreparer The {@link PlaybackPreparer}, or null to remove the current playback
* preparer.
*/
public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
Assertions.checkStateNotNull(controller);
controller.setPlaybackPreparer(playbackPreparer);
}
/**
* Sets the {@link ControlDispatcher}.
*
* @param controlDispatcher The {@link ControlDispatcher}.
*/
public void setControlDispatcher(ControlDispatcher controlDispatcher) {
Assertions.checkStateNotNull(controller);
controller.setControlDispatcher(controlDispatcher);
}
/**
* Sets whether the rewind button is shown.
*
* @param showRewindButton Whether the rewind button is shown.
*/
public void setShowRewindButton(boolean showRewindButton) {
Assertions.checkStateNotNull(controller);
controller.setShowRewindButton(showRewindButton);
}
/**
* Sets whether the fast forward button is shown.
*
* @param showFastForwardButton Whether the fast forward button is shown.
*/
public void setShowFastForwardButton(boolean showFastForwardButton) {
Assertions.checkStateNotNull(controller);
controller.setShowFastForwardButton(showFastForwardButton);
}
/**
* Sets whether the previous button is shown.
*
* @param showPreviousButton Whether the previous button is shown.
*/
public void setShowPreviousButton(boolean showPreviousButton) {
Assertions.checkStateNotNull(controller);
controller.setShowPreviousButton(showPreviousButton);
}
/**
* Sets whether the next button is shown.
*
* @param showNextButton Whether the next button is shown.
*/
public void setShowNextButton(boolean showNextButton) {
Assertions.checkStateNotNull(controller);
controller.setShowNextButton(showNextButton);
}
/**
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
* DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setRewindIncrementMs(int rewindMs) {
Assertions.checkStateNotNull(controller);
controller.setRewindIncrementMs(rewindMs);
}
/**
* @deprecated Use {@link #setControlDispatcher(ControlDispatcher)} with {@link
* DefaultControlDispatcher#DefaultControlDispatcher(long, long)}.
*/
@SuppressWarnings("deprecation")
@Deprecated
public void setFastForwardIncrementMs(int fastForwardMs) {
Assertions.checkStateNotNull(controller);
controller.setFastForwardIncrementMs(fastForwardMs);
}
/**
* Sets which repeat toggle modes are enabled.
*
* @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}.
*/
public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) {
Assertions.checkStateNotNull(controller);
controller.setRepeatToggleModes(repeatToggleModes);
}
/**
* Sets whether the shuffle button is shown.
*
* @param showShuffleButton Whether the shuffle button is shown.
*/
public void setShowShuffleButton(boolean showShuffleButton) {
Assertions.checkStateNotNull(controller);
controller.setShowShuffleButton(showShuffleButton);
}
/**
* Sets whether the subtitle button is shown.
*
* @param showSubtitleButton Whether the subtitle button is shown.
*/
public void setShowSubtitleButton(boolean showSubtitleButton) {
Assertions.checkStateNotNull(controller);
controller.setShowSubtitleButton(showSubtitleButton);
}
/**
* Sets whether the vr button is shown.
*
* @param showVrButton Whether the vr button is shown.
*/
public void setShowVrButton(boolean showVrButton) {
Assertions.checkStateNotNull(controller);
controller.setShowVrButton(showVrButton);
}
/**
* Sets whether the time bar should show all windows, as opposed to just the current one.
*
* @param showMultiWindowTimeBar Whether to show all windows.
*/
public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) {
Assertions.checkStateNotNull(controller);
controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar);
}
/**
* 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, or {@code null} to show no extra ad
* markers.
*/
public void setExtraAdGroupMarkers(
@Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) {
Assertions.checkStateNotNull(controller);
controller.setExtraAdGroupMarkers(extraAdGroupTimesMs, extraPlayedAdGroups);
}
/**
* Set the {@link AspectRatioFrameLayout.AspectRatioListener}.
*
* @param listener The listener to be notified about aspect ratios changes of the video content or
* the content frame.
*/
public void setAspectRatioListener(
@Nullable AspectRatioFrameLayout.AspectRatioListener listener) {
Assertions.checkStateNotNull(contentFrame);
contentFrame.setAspectRatioListener(listener);
}
/**
* Gets the view onto which video is rendered. This is a:
*
* <ul>
* <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code
* surface_view}.
* <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.
* <li>{@link SphericalGLSurfaceView} if {@code surface_type} is {@code
* spherical_gl_surface_view}.
* <li>{@link VideoDecoderGLSurfaceView} if {@code surface_type} is {@code
* video_decoder_gl_surface_view}.
* <li>{@code null} if {@code surface_type} is {@code none}.
* </ul>
*
* @return The {@link SurfaceView}, {@link TextureView}, {@link SphericalGLSurfaceView}, {@link
* VideoDecoderGLSurfaceView} or {@code null}.
*/
@Nullable
public View getVideoSurfaceView() {
return surfaceView;
}
/**
* Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of
* the player.
*
* @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and
* the overlay is not present.
*/
@Nullable
public FrameLayout getOverlayFrameLayout() {
return overlayFrameLayout;
}
/**
* Gets the {@link SubtitleView}.
*
* @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the
* subtitle view is not present.
*/
@Nullable
public SubtitleView getSubtitleView() {
return subtitleView;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!useController() || player == null) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouching = true;
return true;
case MotionEvent.ACTION_UP:
if (isTouching) {
isTouching = false;
return performClick();
}
return false;
default:
return false;
}
}
@Override
public boolean performClick() {
super.performClick();
return toggleControllerVisibility();
}
@Override
public boolean onTrackballEvent(MotionEvent ev) {
if (!useController() || player == null) {
return false;
}
maybeShowController(true);
return true;
}
/**
* Should be called when the player is visible to the user and if {@code surface_type} is {@code
* spherical_gl_surface_view}. It is the counterpart to {@link #onPause()}.
*
* <p>This method should typically be called in {@code Activity.onStart()}, or {@code
* Activity.onResume()} for API versions &lt;= 23.
*/
public void onResume() {
if (surfaceView instanceof SphericalGLSurfaceView) {
((SphericalGLSurfaceView) surfaceView).onResume();
}
}
/**
* Should be called when the player is no longer visible to the user and if {@code surface_type}
* is {@code spherical_gl_surface_view}. It is the counterpart to {@link #onResume()}.
*
* <p>This method should typically be called in {@code Activity.onStop()}, or {@code
* Activity.onPause()} for API versions &lt;= 23.
*/
public void onPause() {
if (surfaceView instanceof SphericalGLSurfaceView) {
((SphericalGLSurfaceView) surfaceView).onPause();
}
}
/**
* Called when there's a change in the aspect ratio of the content being displayed. The default
* implementation sets the aspect ratio of the content frame to that of the content, unless the
* content view is a {@link SphericalGLSurfaceView} in which case the frame's aspect ratio is
* cleared.
*
* @param contentAspectRatio The aspect ratio of the content.
* @param contentFrame The content frame, or {@code null}.
* @param contentView The view that holds the content being displayed, or {@code null}.
*/
protected void onContentAspectRatioChanged(
float contentAspectRatio,
@Nullable AspectRatioFrameLayout contentFrame,
@Nullable View contentView) {
if (contentFrame != null) {
contentFrame.setAspectRatio(
contentView instanceof SphericalGLSurfaceView ? 0 : contentAspectRatio);
}
}
// AdsLoader.AdViewProvider implementation.
@Override
public ViewGroup getAdViewGroup() {
return Assertions.checkStateNotNull(
adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback");
}
@Override
public View[] getAdOverlayViews() {
ArrayList<View> overlayViews = new ArrayList<>();
if (overlayFrameLayout != null) {
overlayViews.add(overlayFrameLayout);
}
if (controller != null) {
overlayViews.add(controller);
}
return overlayViews.toArray(new View[0]);
}
// Internal methods.
@EnsuresNonNullIf(expression = "controller", result = true)
private boolean useController() {
if (useController) {
Assertions.checkStateNotNull(controller);
return true;
}
return false;
}
@EnsuresNonNullIf(expression = "artworkView", result = true)
private boolean useArtwork() {
if (useArtwork) {
Assertions.checkStateNotNull(artworkView);
return true;
}
return false;
}
private boolean toggleControllerVisibility() {
if (!useController() || player == null) {
return false;
}
if (!controller.isFullyVisible()) {
maybeShowController(true);
return true;
} else if (controllerHideOnTouch) {
controller.hide();
return true;
}
return false;
}
/** Shows the playback controls, but only if forced or shown indefinitely. */
private void maybeShowController(boolean isForced) {
if (isPlayingAd() && controllerHideDuringAds) {
return;
}
if (useController()) {
boolean wasShowingIndefinitely =
controller.isFullyVisible() && controller.getShowTimeoutMs() <= 0;
boolean shouldShowIndefinitely = shouldShowControllerIndefinitely();
if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) {
showController(shouldShowIndefinitely);
}
}
}
private boolean shouldShowControllerIndefinitely() {
if (player == null) {
return true;
}
int playbackState = player.getPlaybackState();
return controllerAutoShow
&& (playbackState == Player.STATE_IDLE
|| playbackState == Player.STATE_ENDED
|| !player.getPlayWhenReady());
}
private void showController(boolean showIndefinitely) {
if (!useController()) {
return;
}
controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);
controller.show();
}
private boolean isPlayingAd() {
return player != null && player.isPlayingAd() && player.getPlayWhenReady();
}
private void updateForCurrentTrackSelections(boolean isNewPlayer) {
@Nullable Player player = this.player;
if (player == null || player.getCurrentTrackGroups().isEmpty()) {
if (!keepContentOnPlayerReset) {
hideArtwork();
closeShutter();
}
return;
}
if (isNewPlayer && !keepContentOnPlayerReset) {
// Hide any video from the previous player.
closeShutter();
}
TrackSelectionArray selections = player.getCurrentTrackSelections();
for (int i = 0; i < selections.length; i++) {
if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) {
// Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in
// onRenderedFirstFrame().
hideArtwork();
return;
}
}
// Video disabled so the shutter must be closed.
closeShutter();
// Display artwork if enabled and available, else hide it.
if (useArtwork()) {
for (int i = 0; i < selections.length; i++) {
@Nullable TrackSelection selection = selections.get(i);
if (selection != null) {
for (int j = 0; j < selection.length(); j++) {
@Nullable Metadata metadata = selection.getFormat(j).metadata;
if (metadata != null && setArtworkFromMetadata(metadata)) {
return;
}
}
}
}
if (setDrawableArtwork(defaultArtwork)) {
return;
}
}
// Artwork disabled or unavailable.
hideArtwork();
}
@RequiresNonNull("artworkView")
private boolean setArtworkFromMetadata(Metadata metadata) {
boolean isArtworkSet = false;
int currentPictureType = PICTURE_TYPE_NOT_SET;
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry metadataEntry = metadata.get(i);
int pictureType;
byte[] bitmapData;
if (metadataEntry instanceof ApicFrame) {
bitmapData = ((ApicFrame) metadataEntry).pictureData;
pictureType = ((ApicFrame) metadataEntry).pictureType;
} else if (metadataEntry instanceof PictureFrame) {
bitmapData = ((PictureFrame) metadataEntry).pictureData;
pictureType = ((PictureFrame) metadataEntry).pictureType;
} else {
continue;
}
// Prefer the first front cover picture. If there aren't any, prefer the first picture.
if (currentPictureType == PICTURE_TYPE_NOT_SET || pictureType == PICTURE_TYPE_FRONT_COVER) {
Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);
isArtworkSet = setDrawableArtwork(new BitmapDrawable(getResources(), bitmap));
currentPictureType = pictureType;
if (currentPictureType == PICTURE_TYPE_FRONT_COVER) {
break;
}
}
}
return isArtworkSet;
}
@RequiresNonNull("artworkView")
private boolean setDrawableArtwork(@Nullable Drawable drawable) {
if (drawable != null) {
int drawableWidth = drawable.getIntrinsicWidth();
int drawableHeight = drawable.getIntrinsicHeight();
if (drawableWidth > 0 && drawableHeight > 0) {
float artworkAspectRatio = (float) drawableWidth / drawableHeight;
onContentAspectRatioChanged(artworkAspectRatio, contentFrame, artworkView);
artworkView.setImageDrawable(drawable);
artworkView.setVisibility(VISIBLE);
return true;
}
}
return false;
}
private void hideArtwork() {
if (artworkView != null) {
artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference.
artworkView.setVisibility(INVISIBLE);
}
}
private void closeShutter() {
if (shutterView != null) {
shutterView.setVisibility(View.VISIBLE);
}
}
private void updateBuffering() {
if (bufferingView != null) {
boolean showBufferingSpinner =
player != null
&& player.getPlaybackState() == Player.STATE_BUFFERING
&& (showBuffering == SHOW_BUFFERING_ALWAYS
|| (showBuffering == SHOW_BUFFERING_WHEN_PLAYING && player.getPlayWhenReady()));
bufferingView.setVisibility(showBufferingSpinner ? View.VISIBLE : View.GONE);
}
}
private void updateErrorMessage() {
if (errorMessageView != null) {
if (customErrorMessage != null) {
errorMessageView.setText(customErrorMessage);
errorMessageView.setVisibility(View.VISIBLE);
return;
}
@Nullable ExoPlaybackException error = player != null ? player.getPlayerError() : null;
if (error != null && errorMessageProvider != null) {
CharSequence errorMessage = errorMessageProvider.getErrorMessage(error).second;
errorMessageView.setText(errorMessage);
errorMessageView.setVisibility(View.VISIBLE);
} else {
errorMessageView.setVisibility(View.GONE);
}
}
}
private void updateContentDescription() {
if (controller == null || !useController) {
setContentDescription(/* contentDescription= */ null);
} else if (controller.isFullyVisible()) {
setContentDescription(
/* contentDescription= */ controllerHideOnTouch
? getResources().getString(R.string.exo_controls_hide)
: null);
} else {
setContentDescription(
/* contentDescription= */ getResources().getString(R.string.exo_controls_show));
}
}
private void updateControllerVisibility() {
if (isPlayingAd() && controllerHideDuringAds) {
hideController();
} else {
maybeShowController(false);
}
}
@RequiresApi(23)
private static void configureEditModeLogoV23(Resources resources, ImageView logo) {
logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null));
logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null));
}
private static void configureEditModeLogo(Resources resources, ImageView logo) {
logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo));
logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color));
}
@SuppressWarnings("ResourceType")
private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) {
aspectRatioFrame.setResizeMode(resizeMode);
}
/** Applies a texture rotation to a {@link TextureView}. */
private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) {
Matrix transformMatrix = new Matrix();
float textureViewWidth = textureView.getWidth();
float textureViewHeight = textureView.getHeight();
if (textureViewWidth != 0 && textureViewHeight != 0 && textureViewRotation != 0) {
float pivotX = textureViewWidth / 2;
float pivotY = textureViewHeight / 2;
transformMatrix.postRotate(textureViewRotation, pivotX, pivotY);
// After rotation, scale the rotated texture to fit the TextureView size.
RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight);
RectF rotatedTextureRect = new RectF();
transformMatrix.mapRect(rotatedTextureRect, originalTextureRect);
transformMatrix.postScale(
textureViewWidth / rotatedTextureRect.width(),
textureViewHeight / rotatedTextureRect.height(),
pivotX,
pivotY);
}
textureView.setTransform(transformMatrix);
}
@SuppressLint("InlinedApi")
private boolean isDpadKey(int keyCode) {
return keyCode == KeyEvent.KEYCODE_DPAD_UP
|| keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT
|| keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
|| keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT
|| keyCode == KeyEvent.KEYCODE_DPAD_DOWN
|| keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT
|| keyCode == KeyEvent.KEYCODE_DPAD_LEFT
|| keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT
|| keyCode == KeyEvent.KEYCODE_DPAD_CENTER;
}
private final class ComponentListener
implements Player.EventListener,
TextOutput,
VideoListener,
OnLayoutChangeListener,
SingleTapListener,
StyledPlayerControlView.VisibilityListener {
private final Period period;
private @Nullable Object lastPeriodUidWithTracks;
public ComponentListener() {
period = new Period();
}
// TextOutput implementation
@Override
public void onCues(List<Cue> cues) {
if (subtitleView != null) {
subtitleView.onCues(cues);
}
}
// VideoListener implementation
@Override
public void onVideoSizeChanged(
int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
float videoAspectRatio =
(height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height;
if (surfaceView instanceof TextureView) {
// Try to apply rotation transformation when our surface is a TextureView.
if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) {
// We will apply a rotation 90/270 degree to the output texture of the TextureView.
// In this case, the output video's width and height will be swapped.
videoAspectRatio = 1 / videoAspectRatio;
}
if (textureViewRotation != 0) {
surfaceView.removeOnLayoutChangeListener(this);
}
textureViewRotation = unappliedRotationDegrees;
if (textureViewRotation != 0) {
// The texture view's dimensions might be changed after layout step.
// So add an OnLayoutChangeListener to apply rotation after layout step.
surfaceView.addOnLayoutChangeListener(this);
}
applyTextureViewRotation((TextureView) surfaceView, textureViewRotation);
}
onContentAspectRatioChanged(videoAspectRatio, contentFrame, surfaceView);
}
@Override
public void onRenderedFirstFrame() {
if (shutterView != null) {
shutterView.setVisibility(INVISIBLE);
}
}
@Override
public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) {
// Suppress the update if transitioning to an unprepared period within the same window. This
// is necessary to avoid closing the shutter when such a transition occurs. See:
// https://github.com/google/ExoPlayer/issues/5507.
Player player = Assertions.checkNotNull(StyledPlayerView.this.player);
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
lastPeriodUidWithTracks = null;
} else if (!player.getCurrentTrackGroups().isEmpty()) {
lastPeriodUidWithTracks =
timeline.getPeriod(player.getCurrentPeriodIndex(), period, /* setIds= */ true).uid;
} else if (lastPeriodUidWithTracks != null) {
int lastPeriodIndexWithTracks = timeline.getIndexOfPeriod(lastPeriodUidWithTracks);
if (lastPeriodIndexWithTracks != C.INDEX_UNSET) {
int lastWindowIndexWithTracks =
timeline.getPeriod(lastPeriodIndexWithTracks, period).windowIndex;
if (player.getCurrentWindowIndex() == lastWindowIndexWithTracks) {
// We're in the same window. Suppress the update.
return;
}
}
lastPeriodUidWithTracks = null;
}
updateForCurrentTrackSelections(/* isNewPlayer= */ false);
}
// Player.EventListener implementation
@Override
public void onPlaybackStateChanged(@Player.State int playbackState) {
updateBuffering();
updateErrorMessage();
updateControllerVisibility();
}
@Override
public void onPlayWhenReadyChanged(
boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {
updateBuffering();
updateControllerVisibility();
}
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
if (isPlayingAd() && controllerHideDuringAds) {
hideController();
}
}
// OnLayoutChangeListener implementation
@Override
public void onLayoutChange(
View view,
int left,
int top,
int right,
int bottom,
int oldLeft,
int oldTop,
int oldRight,
int oldBottom) {
applyTextureViewRotation((TextureView) view, textureViewRotation);
}
// SingleTapListener implementation
@Override
public boolean onSingleTapUp(MotionEvent e) {
return toggleControllerVisibility();
}
// StyledPlayerControlView.VisibilityListener implementation
@Override
public void onVisibilityChange(int visibility) {
updateContentDescription();
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/exo_gray">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@color/exo_white" />
</shape>
</item>
<item android:id="@android:id/background"
android:drawable="@drawable/exo_styled_controls_fastforward" />
</ripple>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/exo_gray">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@color/exo_white" />
</shape>
</item>
<item android:id="@android:id/background"
android:drawable="@drawable/exo_styled_controls_rewind" />
</ripple>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18.2,1L9.8,1C8.81,1 8,1.81 8,2.8v14.4c0,0.99 0.81,1.79 1.8,1.79l8.4,0.01c0.99,0 1.8,-0.81 1.8,-1.8L20,2.8c0,-0.99 -0.81,-1.8 -1.8,-1.8zM14,3c1.1,0 2,0.89 2,2s-0.9,2 -2,2 -2,-0.89 -2,-2 0.9,-2 2,-2zM14,16.5c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M14,12.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M6,5H4v16c0,1.1 0.89,2 2,2h10v-2H6V5z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0 0h24v24H0z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
</vector>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"
android:fillColor="#FFFFFF"/>
</vector>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#616161"
android:pathData="M 0 0 H 512 V 512 H 0 V 0 Z" />
<path
android:fillColor="#525252"
android:pathData="M256,151v123.14c-6.88-4.02-14.82-6.48-23.33-6.48 c-25.78,0-46.67,20.88-46.67,46.67c0,25.78,20.88,46.67,46.67,46.67s46.67-20.88,46.67-46.67V197.67H326V151H256z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="-0.5"
android:scaleY="0.5"
android:pivotX="12"
android:pivotY="12">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z"/>
</group>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="24dp"
android:height="24dp">
<group>
<clip-path
android:pathData="M24 24L0 24 0 0 24 0 24 24Z" />
<path
android:pathData="M9.6 13.5l0.4 0c0.2 0 0.4 -0.1 0.5 -0.2 0.1 -0.1 0.2 -0.2 0.2 -0.4l0 -0.2c0 0 -0.1 -0.1 -0.1 -0.2 0 -0.1 -0.1 -0.1 -0.2 -0.1l-0.5 0c0 0 -0.1 0.1 -0.2 0.1 -0.1 0 -0.1 0.1 -0.1 0.2l0 0.2 -1 0c0 -0.2 0 -0.3 0.1 -0.5 0.1 -0.2 0.2 -0.3 0.3 -0.4 0.1 -0.1 0.3 -0.2 0.4 -0.2 0.1 0 0.4 -0.1 0.5 -0.1 0.2 0 0.4 0 0.6 0.1 0.2 0.1 0.3 0.1 0.5 0.2 0.2 0.1 0.2 0.2 0.3 0.4 0.1 0.2 0.1 0.3 0.1 0.5l0 0.3c0 0 -0.1 0.2 -0.1 0.3 0 0.1 -0.1 0.2 -0.2 0.2 -0.1 0 -0.2 0.1 -0.3 0.2 0.2 0.1 0.4 0.2 0.5 0.4 0.1 0.2 0.2 0.4 0.2 0.6 0 0.2 0 0.4 -0.1 0.5 -0.1 0.1 -0.2 0.3 -0.3 0.4C11 15.9 10.8 16 10.6 16 10.4 16 10.2 16.1 10 16.1 9.8 16.1 9.6 16.1 9.5 16 9.4 15.9 9.2 15.9 9 15.8 8.8 15.7 8.8 15.6 8.7 15.4 8.6 15.2 8.6 15 8.6 14.8l0.8 0 0 0.2c0 0 0.1 0.1 0.1 0.2 0 0.1 0.1 0.1 0.2 0.1l0.5 0c0 0 0.1 -0.1 0.2 -0.1 0.1 0 0.1 -0.1 0.1 -0.2l0 -0.5c0 0 -0.1 -0.1 -0.1 -0.2 0 -0.1 -0.1 -0.1 -0.2 -0.1l-0.6 0 0 -0.7zm5.7 0.7c0 0.3 0 0.6 -0.1 0.8l-0.3 0.6c0 0 -0.3 0.3 -0.5 0.3 -0.2 0 -0.4 0.1 -0.6 0.1 -0.2 0 -0.4 0 -0.6 -0.1C13 15.8 12.9 15.7 12.7 15.6 12.5 15.5 12.5 15.3 12.4 15 12.3 14.7 12.3 14.5 12.3 14.2l0 -0.7c0 -0.3 0 -0.6 0.1 -0.8l0.3 -0.6c0 0 0.3 -0.3 0.5 -0.3 0.2 0 0.4 -0.1 0.6 -0.1 0.2 0 0.4 0 0.6 0.1 0.2 0.1 0.3 0.2 0.5 0.3 0.2 0.1 0.2 0.3 0.3 0.6 0.1 0.3 0.1 0.5 0.1 0.8l0 0.7zm-0.9 -0.8l0 -0.5c0 0 -0.1 -0.2 -0.1 -0.3 0 -0.1 -0.1 -0.1 -0.2 -0.2 -0.1 -0.1 -0.2 -0.1 -0.3 -0.1 -0.1 0 -0.2 0 -0.3 0.1l-0.2 0.2c0 0 -0.1 0.2 -0.1 0.3l0 2c0 0 0.1 0.2 0.1 0.3 0 0.1 0.1 0.1 0.2 0.2 0.1 0.1 0.2 0.1 0.3 0.1 0.1 0 0.2 0 0.3 -0.1l0.2 -0.2c0 0 0.1 -0.2 0.1 -0.3l0 -1.5zM4 13c0 4.4 3.6 8 8 8 4.4 0 8 -3.6 8 -8l-2 0c0 3.3 -2.7 6 -6 6C8.7 19 6 16.3 6 13 6 9.7 8.7 7 12 7l0 4 5 -5 -5 -5 0 4C7.6 5 4 8.6 4 13Z"
android:fillColor="#FFFFFF" />
</group>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,16L9,16L9,8h2v8zM15,16h-2L13,8h2v8z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0,0h24v24H0V0z" />
<path
android:fillColor="#FFFFFF"
android:fillType="evenOdd"
android:pathData="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10c5.52,0,10-4.48,10-10
C22,6.48,17.52,2,12,2z
M18,12c0,3.31-2.69,6-6,6c-3.31,0-6-2.69-6-6h2c0,2.21,1.79,4,4,4s4-1.79,4-4s-1.79-4-4-4v3L8,7l4-4v3
C15.31,6,18,8.69,18,12z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.5"
android:scaleY="0.5"
android:pivotX="12"
android:pivotY="12">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,5V1L7,6l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6H4c0,4.42 3.58,8 8,8s8,-3.58 8,-8 -3.58,-8 -8,-8z"/>
</group>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:viewportWidth="24"
android:viewportHeight="24"
android:width="24dp"
android:height="24dp">
<group>
<clip-path
android:pathData="M0 0L24 0 24 24 0 24 0 0Z" />
<path
android:pathData="M12 5l0 -4 -5 5 5 5 0 -4c3.3 0 6 2.7 6 6 0 3.3 -2.7 6 -6 6 -3.3 0 -6 -2.7 -6 -6l-2 0c0 4.4 3.6 8 8 8 4.4 0 8 -3.6 8 -8 0 -4.4 -3.6 -8 -8 -8zm-1.1 11l-0.9 0 0 -3.3 -1 0.3 0 -0.7 1.8 -0.6 0.1 0 0 4.3zm4.3 -1.8c0 0.3 0 0.6 -0.1 0.8l-0.3 0.6c0 0 -0.3 0.3 -0.5 0.3 -0.2 0 -0.4 0.1 -0.6 0.1 -0.2 0 -0.4 0 -0.6 -0.1 -0.2 -0.1 -0.3 -0.2 -0.5 -0.3 -0.2 -0.1 -0.2 -0.3 -0.3 -0.6 -0.1 -0.3 -0.1 -0.5 -0.1 -0.8l0 -0.7c0 -0.3 0 -0.6 0.1 -0.8l0.3 -0.6c0 0 0.3 -0.3 0.5 -0.3 0.2 0 0.4 -0.1 0.6 -0.1 0.2 0 0.4 0 0.6 0.1 0.2 0.1 0.3 0.2 0.5 0.3 0.2 0.1 0.2 0.3 0.3 0.6 0.1 0.3 0.1 0.5 0.1 0.8l0 0.7zm-0.9 -0.8l0 -0.5c0 0 -0.1 -0.2 -0.1 -0.3 0 -0.1 -0.1 -0.1 -0.2 -0.2 -0.1 -0.1 -0.2 -0.1 -0.3 -0.1 -0.1 0 -0.2 0 -0.3 0.1l-0.2 0.2c0 0 -0.1 0.2 -0.1 0.3l0 2c0 0 0.1 0.2 0.1 0.3 0 0.1 0.1 0.1 0.2 0.2 0.1 0.1 0.2 0.1 0.3 0.1 0.1 0 0.2 0 0.3 -0.1l0.2 -0.2c0 0 0.1 -0.2 0.1 -0.3l0 -1.5z"
android:fillColor="#FFFFFF" />
</group>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.05,9.79L10,7.5v9l3.05,-2.29L16,12zM13.05,9.79L10,7.5v9l3.05,-2.29L16,12zM13.05,9.79L10,7.5v9l3.05,-2.29L16,12zM11,4.07L11,2.05c-2.01,0.2 -3.84,1 -5.32,2.21L7.1,5.69c1.11,-0.86 2.44,-1.44 3.9,-1.62zM5.69,7.1L4.26,5.68C3.05,7.16 2.25,8.99 2.05,11h2.02c0.18,-1.46 0.76,-2.79 1.62,-3.9zM4.07,13L2.05,13c0.2,2.01 1,3.84 2.21,5.32l1.43,-1.43c-0.86,-1.1 -1.44,-2.43 -1.62,-3.89zM5.68,19.74C7.16,20.95 9,21.75 11,21.95v-2.02c-1.46,-0.18 -2.79,-0.76 -3.9,-1.62l-1.42,1.43zM22,12c0,5.16 -3.92,9.42 -8.95,9.95v-2.02C16.97,19.41 20,16.05 20,12s-3.03,-7.41 -6.95,-7.93L13.05,2.05C18.08,2.58 22,6.84 22,12z"
android:fillColor="#FFFFFF"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0,0h24v24H0V0z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M19.5,5.5v13h-15v-13H19.5z M19,4H5C3.89,4,3,4.9,3,6v12c0,1.1,0.89,2,2,2h14c1.1,0,2-0.9,2-2V6C21,4.9,20.1,4,19,4L19,4z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M11,11H9.5v-0.5h-2v3h2V13H11v1c0,0.55-0.45,1-1,1H7c-0.55,0-1-0.45-1-1v-4c0-0.55,0.45-1,1-1h3c0.55,0,1,0.45,1,1V11z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M18,11h-1.5v-0.5h-2v3h2V13H18v1c0,0.55-0.45,1-1,1h-3c-0.55,0-1-0.45-1-1v-4c0-0.55,0.45-1,1-1h3c0.55,0,1,0.45,1,1V11z" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M0 0h24v24H0z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M19 4H5c-1.11 0-2 0.9-2 2v12c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2V6c0-1.1-0.9-2-2-2zm-8 7H9.5v-0.5h-2v3h2V13H11v1c0 0.55-0.45 1-1 1H7c-0.55 0-1-0.45-1-1v-4c0-0.55 0.45 -1 1-1h3c0.55 0 1 0.45 1 1v1zm7 0h-1.5v-0.5h-2v3h2V13H18v1c0 0.55-0.45 1-1 1h-3c-0.55 0-1-0.45-1-1v-4c0-0.55 0.45 -1 1-1h3c0.55 0 1 0.45 1 1v1z" />
</vector>
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@android:id/background">
<shape android:shape="rectangle" >
<solid android:color="#26000000" />
</shape>
</item>
<item android:id="@android:id/secondaryProgress">
<clip>
<shape android:shape="rectangle" >
<solid android:color="#5Cffffff" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle" >
<solid android:color="#ffffff" />
</shape>
</clip>
</item>
</layer-list>
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:scaleWidth="100%"
android:scaleHeight="100%"
android:scaleGravity="center"
android:level="10000">
<shape android:shape="oval" >
<solid android:color="#ffffff" />
<size android:height="@dimen/exo_custom_progress_thumb_size"
android:width="@dimen/exo_custom_progress_thumb_size" />
</shape>
</scale>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<layer-list>
<item>
<shape>
<solid android:color="@color/exo_gray_ripple"/>
</shape>
</item>
<item android:drawable="@drawable/exo_styled_controls_fastforward"/>
</layer-list>
</item>
<item android:drawable="@drawable/exo_styled_controls_fastforward" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<layer-list>
<item>
<shape>
<solid android:color="@color/exo_gray_ripple"/>
</shape>
</item>
<item android:drawable="@drawable/exo_styled_controls_rewind"/>
</layer-list>
</item>
<item android:drawable="@drawable/exo_styled_controls_rewind" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
android:layout_height="match_parent"
android:layout_width="match_parent">
<gradient
android:layout_height="match_parent"
android:layout_width="match_parent"
android:startColor="@color/exo_title_bar_gradient_start"
android:endColor="@color/exo_title_bar_gradient_end"
android:angle="270" />
</shape>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:visibility="visible">
<ImageButton android:id="@+id/exo_prev" style="@style/ExoStyledControls.Button.Previous" />
<Button android:id="@+id/exo_rew_with_amount" style="@style/ExoStyledControls.Button.RewWithAmount" />
<ImageButton android:id="@+id/exo_play_pause" style="@style/ExoStyledControls.Button.PlayPause" />
<Button android:id="@+id/exo_ffwd_with_amount" style="@style/ExoStyledControls.Button.FfwdWithAmount" />
<ImageButton android:id="@+id/exo_next" style="@style/ExoStyledControls.Button.Next" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<FrameLayout
android:id="@+id/exo_center_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:layoutDirection="ltr">
<View
android:id="@+id/exo_center_view_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/exo_widget_center_view_background" />
<include
android:id="@+id/exo_embedded_transport_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
layout="@layout/exo_styled_embedded_transport_controls" />
</FrameLayout>
<LinearLayout
android:id="@+id/exo_title_bar"
android:background="@drawable/exo_title_bar_gradient"
android:baselineAligned="false"
android:layout_gravity="top"
style="@style/ExoStyledControls.TitleBar">
<LinearLayout
android:id="@+id/exo_title_bar_left"
android:gravity="center_vertical"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:id="@+id/exo_title_text"
android:gravity="center_vertical"
android:ellipsize="middle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:paddingStart="@dimen/exo_icon_padding"
android:paddingEnd="@dimen/exo_icon_padding"
android:paddingLeft="@dimen/exo_icon_padding"
android:paddingRight="@dimen/exo_icon_padding"
android:textSize="15sp"
android:textColor="#FFFFFFFF"/>
</LinearLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/exo_bottom_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_bottom_bar_height"
android:layout_gravity="bottom"
android:background="@color/exo_bottom_bar_background"
android:layoutDirection="ltr">
<LinearLayout
android:id="@+id/exo_time"
android:layout_width="@dimen/exo_time_view_width"
android:layout_height="@dimen/exo_bottom_bar_height"
android:paddingStart="@dimen/exo_time_view_padding"
android:paddingEnd="@dimen/exo_time_view_padding"
android:paddingLeft="@dimen/exo_time_view_padding"
android:paddingRight="@dimen/exo_time_view_padding"
android:layout_gravity="bottom|start"
android:layoutDirection="ltr">
<TextView
android:id="@+id/exo_position"
style="@style/ExoStyledControls.TimeText.Position" />
<TextView
android:id="@+id/exo_time_interpunct"
style="@style/ExoStyledControls.TimeText.Interpunct" />
<TextView
android:id="@+id/exo_duration"
style="@style/ExoStyledControls.TimeText.Duration" />
</LinearLayout>
<LinearLayout
android:id="@+id/exo_basic_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:orientation="horizontal"
android:layoutDirection="ltr">
<ImageButton
android:id="@id/exo_shuffle"
style="@style/ExoStyledControls.Button.Bottom.Shuffle"/>
<ImageButton
android:id="@id/exo_repeat_toggle"
style="@style/ExoStyledControls.Button.Bottom.RepeatToggle"/>
<ImageButton
android:id="@id/exo_vr"
style="@style/ExoStyledControls.Button.Bottom.VR"/>
<ImageButton
android:id="@+id/exo_subtitle"
style="@style/ExoStyledControls.Button.Bottom.CC"
android:alpha="0.5"
android:scaleType="fitCenter"/>
<ImageButton
android:id="@+id/exo_fullscreen"
style="@style/ExoStyledControls.Button.Bottom.FullScreen"/>
<ImageButton
android:id="@+id/exo_overflow_show"
style="@style/ExoStyledControls.Button.Bottom.OverflowShow" />
</LinearLayout>
<HorizontalScrollView
android:id="@+id/exo_extra_controls_scroll_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:visibility="invisible">
<LinearLayout
android:id="@+id/exo_extra_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:orientation="horizontal"
android:layoutDirection="ltr">
<ImageButton
android:id="@+id/exo_settings"
style="@style/ExoStyledControls.Button.Bottom.Settings" />
<ImageButton
android:id="@+id/exo_overflow_hide"
style="@style/ExoStyledControls.Button.Bottom.OverflowHide" />
</LinearLayout>
</HorizontalScrollView>
</FrameLayout>
<LinearLayout
android:id="@+id/exo_minimal_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|end"
android:visibility="invisible">
</LinearLayout>
<View android:id="@id/exo_progress_placeholder"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_custom_progress_thumb_size"
android:layout_gravity="bottom"
android:layout_marginBottom="@dimen/exo_custom_progress_margin_bottom"/>
</merge>
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 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.
-->
<merge>
<include layout="@layout/exo_player_view"/>
</merge>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/exo_settings_listview"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:background="@color/exo_black_opacity_70"
android:scrollbars="vertical"/>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/exo_setting_width"
android:minHeight="@dimen/exo_settings_height"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal">
<ImageView
android:id="@+id/exo_icon"
android:layout_width="@dimen/exo_settings_icon_size"
android:layout_height="@dimen/exo_settings_icon_size"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="12dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="@dimen/exo_settings_height"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:gravity="center|start"
android:orientation="vertical">
<TextView
android:id="@+id/exo_main_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/exo_white"
android:textSize="@dimen/exo_settings_main_text_size"/>
<TextView
android:id="@+id/exo_sub_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/exo_white_opacity_70"
android:textSize="@dimen/exo_settings_sub_text_size"/>
</LinearLayout>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="@dimen/exo_setting_width"
android:minHeight="@dimen/exo_settings_height"
android:background="?android:attr/selectableItemBackground"
android:orientation="horizontal">
<ImageView
android:id="@+id/exo_check"
android:layout_width="@dimen/exo_settings_icon_size"
android:layout_height="@dimen/exo_settings_icon_size"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="12dp"
android:visibility="invisible"
android:src="@drawable/exo_styled_controls_check"/>
<TextView
android:id="@+id/exo_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="@dimen/exo_settings_height"
android:paddingStart="2dp"
android:paddingLeft="2dp"
android:gravity="center|start"
android:textColor="@color/exo_white"
android:textSize="@dimen/exo_settings_main_text_size"/>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<integer-array name="exo_speed_multiplied_by_100">
<item>25</item>
<item>50</item>
<item>75</item>
<item>100</item>
<item>125</item>
<item>150</item>
<item>200</item>
</integer-array>
<string-array translatable="false" name="exo_playback_speeds">
<item>0.25x</item>
<item>0.5x</item>
<item>0.75x</item>
<item>@string/exo_controls_playback_speed_normal</item>
<item>1.25x</item>
<item>1.5x</item>
<item>2x</item>
</string-array>
<string-array translatable="false" name="exo_settings_main_texts">
<item>@string/exo_controls_playback_speed</item>
<item>@string/exo_controls_audio_track</item>
</string-array>
<array name="exo_settings_icon_ids">
<item>@drawable/exo_styled_controls_speed</item>
<item>@drawable/exo_styled_controls_audiotrack</item>
</array>
</resources>
...@@ -40,7 +40,24 @@ ...@@ -40,7 +40,24 @@
<flag name="all" value="2"/> <flag name="all" value="2"/>
</attr> </attr>
<!-- PlayerControlView attributes --> <!-- PlayerView and StyledPlayerView attributes -->
<attr name="use_artwork" format="boolean"/>
<attr name="shutter_background_color" format="color"/>
<attr name="default_artwork" format="reference"/>
<attr name="use_controller" format="boolean"/>
<attr name="hide_on_touch" format="boolean"/>
<attr name="hide_during_ads" format="boolean"/>
<attr name="auto_show" format="boolean"/>
<attr name="show_buffering" format="enum">
<enum name="never" value="0"/>
<enum name="when_playing" value="1"/>
<enum name="always" value="2"/>
</attr>
<attr name="keep_content_on_player_reset" format="boolean"/>
<attr name="use_sensor_rotation" format="boolean"/>
<attr name="player_layout_id" format="reference"/>
<!-- PlayerControlView and StyledPlayerControlView attributes -->
<attr name="show_timeout" format="integer"/> <attr name="show_timeout" format="integer"/>
<attr name="rewind_increment" format="integer"/> <attr name="rewind_increment" format="integer"/>
<attr name="fastforward_increment" format="integer"/> <attr name="fastforward_increment" format="integer"/>
...@@ -49,8 +66,11 @@ ...@@ -49,8 +66,11 @@
<attr name="show_previous_button" format="boolean"/> <attr name="show_previous_button" format="boolean"/>
<attr name="show_next_button" format="boolean"/> <attr name="show_next_button" format="boolean"/>
<attr name="show_shuffle_button" format="boolean"/> <attr name="show_shuffle_button" format="boolean"/>
<attr name="show_subtitle_button" format="boolean"/>
<attr name="show_vr_button" format="boolean"/>
<attr name="time_bar_min_update_interval" format="integer"/> <attr name="time_bar_min_update_interval" format="integer"/>
<attr name="controller_layout_id" format="reference"/> <attr name="controller_layout_id" format="reference"/>
<attr name="disable_animation" format="boolean"/>
<!-- DefaultTimeBar attributes --> <!-- DefaultTimeBar attributes -->
<attr name="bar_height" format="dimension"/> <attr name="bar_height" format="dimension"/>
...@@ -68,21 +88,56 @@ ...@@ -68,21 +88,56 @@
<attr name="played_ad_marker_color" format="color"/> <attr name="played_ad_marker_color" format="color"/>
<declare-styleable name="PlayerView"> <declare-styleable name="PlayerView">
<attr name="use_artwork" format="boolean"/> <attr name="use_artwork"/>
<attr name="shutter_background_color" format="color"/> <attr name="shutter_background_color"/>
<attr name="default_artwork" format="reference"/> <attr name="default_artwork"/>
<attr name="use_controller" format="boolean"/> <attr name="use_controller"/>
<attr name="hide_on_touch" format="boolean"/> <attr name="hide_on_touch"/>
<attr name="hide_during_ads" format="boolean"/> <attr name="hide_during_ads"/>
<attr name="auto_show" format="boolean"/> <attr name="auto_show"/>
<attr name="show_buffering" format="enum"> <attr name="show_buffering"/>
<enum name="never" value="0"/> <attr name="keep_content_on_player_reset"/>
<enum name="when_playing" value="1"/> <attr name="use_sensor_rotation"/>
<enum name="always" value="2"/> <attr name="player_layout_id"/>
</attr> <attr name="surface_type"/>
<attr name="keep_content_on_player_reset" format="boolean"/> <!-- AspectRatioFrameLayout attributes -->
<attr name="use_sensor_rotation" format="boolean"/> <attr name="resize_mode"/>
<attr name="player_layout_id" format="reference"/> <!-- PlayerControlView attributes -->
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
<attr name="repeat_toggle_modes"/>
<attr name="show_shuffle_button"/>
<attr name="time_bar_min_update_interval"/>
<attr name="controller_layout_id"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
<attr name="scrubber_disabled_size"/>
<attr name="scrubber_dragged_size"/>
<attr name="scrubber_drawable"/>
<attr name="played_color"/>
<attr name="scrubber_color"/>
<attr name="buffered_color" />
<attr name="unplayed_color"/>
<attr name="ad_marker_color"/>
<attr name="played_ad_marker_color"/>
</declare-styleable>
<declare-styleable name="StyledPlayerView">
<attr name="use_artwork"/>
<attr name="shutter_background_color"/>
<attr name="default_artwork"/>
<attr name="use_controller"/>
<attr name="hide_on_touch"/>
<attr name="hide_during_ads"/>
<attr name="auto_show"/>
<attr name="show_buffering"/>
<attr name="keep_content_on_player_reset"/>
<attr name="use_sensor_rotation"/>
<attr name="player_layout_id"/>
<attr name="surface_type"/> <attr name="surface_type"/>
<!-- AspectRatioFrameLayout attributes --> <!-- AspectRatioFrameLayout attributes -->
<attr name="resize_mode"/> <attr name="resize_mode"/>
...@@ -92,8 +147,11 @@ ...@@ -92,8 +147,11 @@
<attr name="fastforward_increment"/> <attr name="fastforward_increment"/>
<attr name="repeat_toggle_modes"/> <attr name="repeat_toggle_modes"/>
<attr name="show_shuffle_button"/> <attr name="show_shuffle_button"/>
<attr name="show_subtitle_button"/>
<attr name="show_vr_button"/>
<attr name="time_bar_min_update_interval"/> <attr name="time_bar_min_update_interval"/>
<attr name="controller_layout_id"/> <attr name="controller_layout_id"/>
<attr name="disable_animation"/>
<!-- DefaultTimeBar attributes --> <!-- DefaultTimeBar attributes -->
<attr name="bar_height"/> <attr name="bar_height"/>
<attr name="touch_target_height"/> <attr name="touch_target_height"/>
...@@ -142,6 +200,37 @@ ...@@ -142,6 +200,37 @@
<attr name="played_ad_marker_color"/> <attr name="played_ad_marker_color"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="StyledPlayerControlView">
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
<attr name="repeat_toggle_modes"/>
<attr name="show_rewind_button"/>
<attr name="show_fastforward_button"/>
<attr name="show_previous_button"/>
<attr name="show_next_button"/>
<attr name="show_shuffle_button"/>
<attr name="show_subtitle_button"/>
<attr name="show_vr_button"/>
<attr name="time_bar_min_update_interval"/>
<attr name="controller_layout_id"/>
<attr name="disable_animation"/>
<!-- DefaultTimeBar attributes -->
<attr name="bar_height"/>
<attr name="touch_target_height"/>
<attr name="ad_marker_width"/>
<attr name="scrubber_enabled_size"/>
<attr name="scrubber_disabled_size"/>
<attr name="scrubber_dragged_size"/>
<attr name="scrubber_drawable"/>
<attr name="played_color"/>
<attr name="scrubber_color"/>
<attr name="buffered_color" />
<attr name="unplayed_color"/>
<attr name="ad_marker_color"/>
<attr name="played_ad_marker_color"/>
</declare-styleable>
<declare-styleable name="DefaultTimeBar"> <declare-styleable name="DefaultTimeBar">
<attr name="bar_height"/> <attr name="bar_height"/>
<attr name="touch_target_height"/> <attr name="touch_target_height"/>
......
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<color name="exo_gray">#808080</color>
<color name="exo_gray_ripple">#80808080</color>
<color name="exo_white">#ffffff</color>
<color name="exo_white_opacity_70">#B3ffffff</color>
<color name="exo_black_opacity_70">#B3000000</color>
<color name="exo_title_bar_gradient_start">#d0000000</color>
<color name="exo_title_bar_gradient_end">#00000000</color>
<color name="exo_widget_center_view_background">#90000000</color>
<color name="exo_bottom_bar_background">#b0000000</color>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<resources>
<dimen name="exo_setting_width">150dp</dimen>
<dimen name="exo_settings_height">48dp</dimen>
<dimen name="exo_settings_icon_size">24dp</dimen>
<dimen name="exo_settings_text_height">24dp</dimen>
<dimen name="exo_settings_main_text_size">14sp</dimen>
<dimen name="exo_settings_sub_text_size">12sp</dimen>
<dimen name="exo_settings_offset">8dp</dimen>
<dimen name="exo_icon_size">48dp</dimen>
<dimen name="exo_icon_margin">10dp</dimen>
<dimen name="exo_pause_icon_padding">6dp</dimen>
<dimen name="exo_icon_padding">12dp</dimen>
<dimen name="exo_icon_text_size">8dp</dimen>
<dimen name="exo_icon_padding_bottom">18.5dp</dimen>
<dimen name="exo_custom_progress_max_size">2dp</dimen>
<dimen name="exo_custom_progress_thumb_size">16dp</dimen>
<dimen name="exo_custom_progress_margin_bottom">41dp</dimen>
<dimen name="exo_title_bar_height">48dp</dimen>
<dimen name="exo_bottom_bar_height">48dp</dimen>
<dimen name="exo_time_view_padding">10dp</dimen>
<dimen name="exo_time_view_width">170sp</dimen>
<!-- Rounded corner radius for video subtitles. -->
<dimen name="exo_subtitle_corner_radius">2dp</dimen>
<!-- Shadow radius for video subtitles. -->
<dimen name="exo_subtitle_shadow_radius">2dp</dimen>
<!-- Shadow offset for video subtitles. -->
<dimen name="exo_subtitle_shadow_offset">2dp</dimen>
<!-- Outline width for video subtitles. -->
<dimen name="exo_subtitle_outline_width">2dp</dimen>
</resources>
...@@ -36,4 +36,28 @@ ...@@ -36,4 +36,28 @@
<drawable name="exo_notification_rewind">@drawable/exo_icon_rewind</drawable> <drawable name="exo_notification_rewind">@drawable/exo_icon_rewind</drawable>
<drawable name="exo_notification_stop">@drawable/exo_icon_stop</drawable> <drawable name="exo_notification_stop">@drawable/exo_icon_stop</drawable>
<drawable name="exo_notification_small_icon">@drawable/exo_icon_circular_play</drawable> <drawable name="exo_notification_small_icon">@drawable/exo_icon_circular_play</drawable>
<drawable name="exo_styled_controls_play">@drawable/exo_ic_play_circle_filled</drawable>
<drawable name="exo_styled_controls_pause">@drawable/exo_ic_pause_circle_filled</drawable>
<drawable name="exo_styled_controls_replay">@drawable/exo_ic_replay_circle_filled</drawable>
<drawable name="exo_styled_controls_next">@drawable/exo_ic_skip_next</drawable>
<drawable name="exo_styled_controls_previous">@drawable/exo_ic_skip_previous</drawable>
<drawable name="exo_styled_controls_fastforward">@drawable/exo_ic_forward</drawable>
<drawable name="exo_styled_controls_rewind">@drawable/exo_ic_rewind</drawable>
<drawable name="exo_styled_controls_repeat_all">@drawable/exo_icon_repeat_all</drawable>
<drawable name="exo_styled_controls_repeat_off">@drawable/exo_icon_repeat_off</drawable>
<drawable name="exo_styled_controls_repeat_one">@drawable/exo_icon_repeat_one</drawable>
<drawable name="exo_styled_controls_shuffle_off">@drawable/exo_icon_shuffle_off</drawable>
<drawable name="exo_styled_controls_shuffle_on">@drawable/exo_icon_shuffle_on</drawable>
<drawable name="exo_styled_controls_fullscreen_enter">@drawable/exo_ic_fullscreen_enter</drawable>
<drawable name="exo_styled_controls_fullscreen_exit">@drawable/exo_ic_fullscreen_exit</drawable>
<drawable name="exo_styled_controls_vr">@drawable/exo_icon_vr</drawable>
<drawable name="exo_styled_controls_subtitle_off">@drawable/exo_ic_subtitle_off</drawable>
<drawable name="exo_styled_controls_subtitle_on">@drawable/exo_ic_subtitle_on</drawable>
<drawable name="exo_styled_controls_overflow_show">@drawable/exo_ic_chevron_right</drawable>
<drawable name="exo_styled_controls_overflow_hide">@drawable/exo_ic_chevron_left</drawable>
<drawable name="exo_styled_controls_settings">@drawable/exo_ic_settings</drawable>
<drawable name="exo_styled_controls_check">@drawable/exo_ic_check</drawable>
<drawable name="exo_styled_controls_audiotrack">@drawable/exo_ic_audiotrack</drawable>
<drawable name="exo_styled_controls_speed">@drawable/exo_ic_speed</drawable>
</resources> </resources>
...@@ -42,8 +42,6 @@ ...@@ -42,8 +42,6 @@
<string name="exo_controls_shuffle_on_description">Shuffle on</string> <string name="exo_controls_shuffle_on_description">Shuffle on</string>
<!-- Description for a button that controls the shuffle mode of media playback. In this mode shuffle is off. [CHAR LIMIT=40] --> <!-- Description for a button that controls the shuffle mode of media playback. In this mode shuffle is off. [CHAR LIMIT=40] -->
<string name="exo_controls_shuffle_off_description">Shuffle off</string> <string name="exo_controls_shuffle_off_description">Shuffle off</string>
<!-- Description for a media control button that toggles whether a video playback is fullscreen. [CHAR LIMIT=30] -->
<string name="exo_controls_fullscreen_description">Fullscreen mode</string>
<!-- Description for a media control button that toggles whether a video playback is in VR mode. [CHAR LIMIT=30] --> <!-- Description for a media control button that toggles whether a video playback is in VR mode. [CHAR LIMIT=30] -->
<string name="exo_controls_vr_description">VR mode</string> <string name="exo_controls_vr_description">VR mode</string>
<!-- Description for a button that downloads a piece of media content onto the device. [CHAR LIMIT=20] --> <!-- Description for a button that downloads a piece of media content onto the device. [CHAR LIMIT=20] -->
...@@ -94,4 +92,61 @@ ...@@ -94,4 +92,61 @@
<string name="exo_track_bitrate"><xliff:g id="bitrate" example="5.2">%1$.2f</xliff:g> Mbps</string> <string name="exo_track_bitrate"><xliff:g id="bitrate" example="5.2">%1$.2f</xliff:g> Mbps</string>
<!-- Defines a way of appending an item to a list of items. For example appending "banana" to "apple, pear" to get "apple, pear, banana". Note: the command separator will appear between all consecutive list items, so do not use an equivalent of 'and'. [CHAR LIMIT=40] --> <!-- Defines a way of appending an item to a list of items. For example appending "banana" to "apple, pear" to get "apple, pear, banana". Note: the command separator will appear between all consecutive list items, so do not use an equivalent of 'and'. [CHAR LIMIT=40] -->
<string name="exo_item_list"><xliff:g id="list" example="apple, pear">%1$s</xliff:g>, <xliff:g id="item" example="banana">%2$s</xliff:g></string> <string name="exo_item_list"><xliff:g id="list" example="apple, pear">%1$s</xliff:g>, <xliff:g id="item" example="banana">%2$s</xliff:g></string>
<!-- The title of audio track selection. It is shown with the currently selected audio track's
information (such as language). If a user clicks it, the list of possible audio tracks
will be shown. [CHAR_LIMIT=20] -->
<string name="exo_controls_audio_track">Audio track</string>
<!-- The title of playback speed selection. It is shown with the current playback speed.
If a user clicks it, the list of possible playback speeds will be shown.
[CHAR_LIMIT=32] -->
<string name="exo_controls_playback_speed">Playback speed</string>
<!-- It implies that the playback speed is normal (1.0x). [CHAR_LIMIT=16] -->
<string name="exo_controls_playback_speed_normal">Normal</string>
<!-- Text for displaying custom playback speed. [CHAR_LIMIT=16] -->
<string translatable="false" name="exo_controls_custom_playback_speed">
<xliff:g id="playback_speed" example="1.05">%1$.2f</xliff:g>x
</string>
<!-- Placeholder text for displaying time. Used to calculate which size layout to use. -->
<string translatable="false" name="exo_controls_time_placeholder">00:00:00</string>
<!-- Content description of the left arrow button to navigate a list of buttons.
If a user clicks the arrow, it goes back to the previous button list.
[CHAR_LIMIT=32] -->
<string name="exo_controls_overflow_left_button_description">Back to previous button list</string>
<!-- Content description of the right arrow button to navigate a list of buttons.
If a user clicks the arrow, it shows a list of other buttons.
[CHAR_LIMIT=32] -->
<string name="exo_controls_overflow_right_button_description">See more buttons</string>
<!-- Content description of the seek bar, which indicates the playback progress.
[CHAR_LIMIT=32] -->
<string name="exo_controls_seek_bar_description">Playback progress</string>
<!-- Content description of the settings button.
[CHAR_LIMIT=32] -->
<string name="exo_controls_settings_description">Settings</string>
<!-- Content description of the close caption (subtitle) button, when subtitle is turned on.
If a user clicks the button, it will turned off.
[CHAR_LIMIT=40] -->
<string name="exo_controls_cc_is_on">Subtitle is on. Click to hide it.</string>
<!-- Content description of the close caption (subtitle) button, when subtitle is turned off.
If a user clicks the button, it will turned on.
[CHAR_LIMIT=40] -->
<string name="exo_controls_cc_is_off">Subtitle is off. Click to show it.</string>
<!-- Content description of the "rewind" button.
If a user clicks the button, it rewinds back by rewind amount.
[CHAR_LIMIT=32] -->
<string name="exo_controls_rewind_desc_holder">
Rewind by <xliff:g id="rewind_amount" example="10">%d</xliff:g> seconds</string>
<!-- Content description of the "fast-forward" button.
If a user clicks the button, it fast-forwards by fast forward amount.
[CHAR_LIMIT=32] -->
<string name="exo_controls_ffwd_desc_holder">
Go forward by <xliff:g id="fastforward_amount" example="30">%d</xliff:g> seconds</string>
<!-- Content description of the full screen enter button.
If a user clicks the button, the widget goes into full-screen mode.
[CHAR_LIMIT=32] -->
<string name="exo_controls_fullscreen_enter_description">Full screen enter</string>
<!-- Content description of the full screen exit button.
If a user clicks the button, the widget exits full-screen mode.
[CHAR_LIMIT=32] -->
<string name="exo_controls_fullscreen_exit_description">Full screen exit</string>
</resources> </resources>
...@@ -56,4 +56,142 @@ ...@@ -56,4 +56,142 @@
<item name="android:contentDescription">@string/exo_controls_vr_description</item> <item name="android:contentDescription">@string/exo_controls_vr_description</item>
</style> </style>
<style name="ExoStyledControls" />
<style name="ExoStyledControls.Button">
<item name="android:background">?android:attr/selectableItemBackground</item>
<item name="android:scaleType">fitXY</item>
<item name="android:layout_width">@dimen/exo_icon_size</item>
<item name="android:layout_height">@dimen/exo_icon_size</item>
</style>
<style name="ExoStyledControls.Button.Previous">
<item name="android:src">@drawable/exo_styled_controls_previous</item>
<item name="android:contentDescription">@string/exo_controls_previous_description</item>
<item name="android:padding">@dimen/exo_icon_padding</item>
</style>
<style name="ExoStyledControls.Button.Next">
<item name="android:src">@drawable/exo_styled_controls_next</item>
<item name="android:contentDescription">@string/exo_controls_next_description</item>
<item name="android:padding">@dimen/exo_icon_padding</item>
</style>
<style name="ExoStyledControls.Button.PlayPause">
<item name="android:src">@drawable/exo_styled_controls_play</item>
<item name="android:contentDescription">@string/exo_controls_play_description</item>
<item name="android:padding">@dimen/exo_pause_icon_padding</item>
</style>
<style name="ExoStyledControls.Button.FfwdWithAmount">
<item name="android:background">@drawable/exo_ripple_ffwd</item>
<item name="android:contentDescription">@string/exo_controls_ffwd_desc_holder</item>
<item name="android:gravity">center|bottom</item>
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
</style>
<style name="ExoStyledControls.Button.RewWithAmount">
<item name="android:background">@drawable/exo_ripple_rew</item>
<item name="android:contentDescription">@string/exo_controls_rewind_desc_holder</item>
<item name="android:gravity">center|bottom</item>
<item name="android:paddingBottom">@dimen/exo_icon_padding_bottom</item>
<item name="android:textAppearance">@style/ExoStyledControls.ButtonText</item>
</style>
<style name="ExoStyledControls.ButtonText">
<item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/exo_icon_text_size</item>
<item name="android:textColor">@color/exo_white</item>
</style>
<style name="ExoStyledControls.Button.Minimal" />
<!-- TODO(insun): Merge play and pause buttons in minimal mode as well. -->
<style name="ExoStyledControls.Button.Minimal.Pause">
<item name="android:src">@drawable/exo_styled_controls_pause</item>
<item name="android:contentDescription">@string/exo_controls_pause_description</item>
</style>
<style name="ExoStyledControls.Button.Minimal.Play">
<item name="android:src">@drawable/exo_styled_controls_play</item>
<item name="android:contentDescription">@string/exo_controls_play_description</item>
</style>
<style name="ExoStyledControls.TitleBar">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">@dimen/exo_title_bar_height</item>
</style>
<style name="ExoStyledControls.TimeText">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:paddingLeft">4dp</item>
<item name="android:paddingRight">4dp</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">14sp</item>
<item name="android:gravity">center</item>
</style>
<style name="ExoStyledControls.TimeText.Position">
<item name="android:textColor">@color/exo_white</item>
<item name="android:text">@string/exo_controls_time_placeholder</item>
</style>
<style name="ExoStyledControls.TimeText.Interpunct">
<item name="android:textColor">@color/exo_white_opacity_70</item>
<item name="android:text">·</item>
</style>
<style name="ExoStyledControls.TimeText.Duration">
<item name="android:textColor">@color/exo_white_opacity_70</item>
<item name="android:text">@string/exo_controls_time_placeholder</item>
</style>
<style name="ExoStyledControls.Button.Bottom">
<item name="android:gravity">center_horizontal</item>
<item name="android:padding">@dimen/exo_icon_padding</item>
</style>
<style name="ExoStyledControls.Button.Bottom.Shuffle">
<item name="android:src">@drawable/exo_styled_controls_shuffle_off</item>
<item name="android:contentDescription">@string/exo_controls_shuffle_off_description</item>
</style>
<style name="ExoStyledControls.Button.Bottom.RepeatToggle">
<item name="android:src">@drawable/exo_styled_controls_repeat_off</item>
<item name="android:contentDescription">@string/exo_controls_repeat_off_description</item>
</style>
<style name="ExoStyledControls.Button.Bottom.VR">
<item name="android:src">@drawable/exo_styled_controls_vr</item>
<item name="android:contentDescription">@string/exo_controls_vr_description</item>
</style>
<style name="ExoStyledControls.Button.Bottom.CC">
<item name="android:src">@drawable/exo_styled_controls_subtitle_off</item>
<item name="android:contentDescription">@string/exo_controls_cc_is_off</item>
</style>
<style name="ExoStyledControls.Button.Bottom.FullScreen">
<item name="android:src">@drawable/exo_styled_controls_fullscreen_enter</item>
<item name="android:contentDescription">@string/exo_controls_fullscreen_enter_description</item>
</style>
<style name="ExoStyledControls.Button.Bottom.OverflowShow">
<item name="android:src">@drawable/exo_styled_controls_overflow_show</item>
<item name="android:contentDescription">@string/exo_controls_overflow_right_button_description</item>
</style>
<style name="ExoStyledControls.Button.Bottom.OverflowHide">
<item name="android:src">@drawable/exo_styled_controls_overflow_hide</item>
<item name="android:contentDescription">@string/exo_controls_overflow_left_button_description</item>
</style>
<style name="ExoStyledControls.Button.Bottom.Settings">
<item name="android:src">@drawable/exo_styled_controls_settings</item>
<item name="android:contentDescription">@string/exo_controls_settings_description</item>
</style>
</resources> </resources>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment