Commit 158cf0c8 by Görkem Güclü

Added missing files

parent 16af5b7d
package com.google.android.exoplayer2.demo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.LruCache;
import android.view.View;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.thumbnail.ThumbnailDescription;
import com.google.android.exoplayer2.util.Log;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
public class DefaultThumbnailProvider implements ThumbnailProvider {
private static final String TAG_DEBUG = DefaultThumbnailProvider.class.getSimpleName();
private LruCache<String, Bitmap> bitmapCache;
private View parent;
//dummy bitmap to indicate that a download is already triggered but not finished yet
private final Bitmap dummyBitmap = Bitmap.createBitmap(1,1,Bitmap.Config.ARGB_8888);
@Nullable ExoPlayer exoPlayer;
public DefaultThumbnailProvider(ExoPlayer exoPlayer, View view) {
this.exoPlayer = exoPlayer;
this.parent = view;
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 4;
bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
}
public Bitmap getThumbnail(long position) {
return getThumbnail(position, true);
}
private Bitmap getThumbnail(long position, boolean retrigger) {
if (exoPlayer != null) {
Object manifest = exoPlayer.getCurrentManifest();
ThumbnailDescription thumbnailDescription = null;
if (manifest instanceof DashManifest) {
DashManifest dashManifest = (DashManifest) manifest;
List<ThumbnailDescription> thumbnailDescs = dashManifest.getThumbnailDescriptions(position);
//selected thumbnail description with lowest bitrate
for (ThumbnailDescription desc : thumbnailDescs) {
if (thumbnailDescription == null || thumbnailDescription.getBitrate() > desc.getBitrate()) {
thumbnailDescription = desc;
}
}
if (bitmapNotAvailableOrDownloadNotTriggeredYet(thumbnailDescription.getUri())) {
this.initThumbnailSource(thumbnailDescription);
return null;
}
}
if (retrigger) {
//also download next and prev thumbnails to have a nicer UI user experience
getThumbnail(thumbnailDescription.getStartTimeMs() + thumbnailDescription.getDurationMs(), false);
getThumbnail(thumbnailDescription.getStartTimeMs() - thumbnailDescription.getDurationMs(), false);
}
return getThumbnailInternal(position, thumbnailDescription);
}
return null;
}
private boolean bitmapNotAvailableOrDownloadNotTriggeredYet(Uri uri) {
Bitmap tmp = bitmapCache.get(uri.toString());
if (tmp != null) return false;
return true;
}
private Bitmap getThumbnailInternal(long position, ThumbnailDescription thumbnailDescription) {
if (thumbnailDescription == null) return null;
Bitmap thumbnailSource = bitmapCache.get(thumbnailDescription.getUri().toString());
if (thumbnailSource == null || thumbnailSource.getWidth() == 1) return null;
if (position < thumbnailDescription.getStartTimeMs() || position > thumbnailDescription.getStartTimeMs() + thumbnailDescription.getDurationMs()) return null;
int count = thumbnailDescription.getColumns() * thumbnailDescription.getRows();
int durationPerImage = (int)(thumbnailDescription.getDurationMs() / count);
int imageNumberToUseWithinTile = (int)((position - thumbnailDescription.getStartTimeMs()) / durationPerImage);
//handle special case if position == duration
if (imageNumberToUseWithinTile > count-1) imageNumberToUseWithinTile = count-1;
int intRowToUse = (int)(imageNumberToUseWithinTile / thumbnailDescription.getColumns());
int intColToUse = imageNumberToUseWithinTile - intRowToUse * thumbnailDescription.getColumns();
double thumbnailWidth = (double) thumbnailDescription.getImageWidth() / thumbnailDescription.getColumns();
double thumbnailHeight = (double) thumbnailDescription.getImageHeight() / thumbnailDescription.getRows();
int cropXLeft = (int)Math.round(intColToUse * thumbnailWidth);
int cropYTop = (int)Math.round(intRowToUse * thumbnailHeight);
if (cropXLeft + thumbnailWidth <= thumbnailSource.getWidth() && cropYTop + thumbnailHeight <= thumbnailSource.getHeight()) {
return Bitmap.createBitmap(thumbnailSource
, cropXLeft, cropYTop, (int) thumbnailWidth, (int) thumbnailHeight);
}
else {
Log.d(TAG_DEBUG, "Image does not have expected (" + thumbnailDescription.getImageWidth() + "x" + thumbnailDescription.getImageHeight() + ") dimensions to crop. Source " + thumbnailDescription.getUri());
return null;
}
}
private synchronized void initThumbnailSource(ThumbnailDescription thumbnailDescription){
String path = thumbnailDescription.getUri().toString();
if (path == null) return;
if (bitmapCache.get(path) != null) return;
bitmapCache.put(path, dummyBitmap);
RetrieveThumbnailImageTask currentTask = new RetrieveThumbnailImageTask();
currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, path);
}
class RetrieveThumbnailImageTask extends AsyncTask<String, Integer, Bitmap> {
String downloadedUrl;
RetrieveThumbnailImageTask() {
}
@Override
protected void onCancelled() {
super.onCancelled();
if (downloadedUrl != null) bitmapCache.remove(downloadedUrl);
}
protected Bitmap doInBackground(String... urls) {
downloadedUrl = urls[0];
InputStream in =null;
Bitmap thumbnailToDownload=null;
int responseCode = -1;
try{
URL url = new URL(downloadedUrl);
if (!isCancelled()) {
HttpURLConnection httpURLConnection = (HttpURLConnection)url.openConnection();
httpURLConnection.setDoInput(true);
httpURLConnection.connect();
responseCode = httpURLConnection.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_OK)
{
if (!isCancelled()) {
in = httpURLConnection.getInputStream();
if (!isCancelled()) {
thumbnailToDownload = BitmapFactory.decodeStream(in);
}
in.close();
}
}
}
}
catch(Exception ex){
bitmapCache.remove(downloadedUrl);
System.out.println(ex);
}
return thumbnailToDownload;
}
protected void onPostExecute(Bitmap downloadedThumbnail) {
if (downloadedThumbnail != null) {
bitmapCache.put(downloadedUrl, downloadedThumbnail);
if (parent != null) parent.invalidate();
}
else {
bitmapCache.remove(downloadedUrl);
}
}
}
}
/*
* Copyright (C) 2017 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.demo;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ui.TimeBar;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.Collections;
import java.util.Formatter;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArraySet;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A time bar that shows a current position, buffered position, duration and ad markers.
*
* <p>A DefaultTimeBar can be customized by setting attributes, as outlined below.
*
* <h2>Attributes</h2>
*
* The following attributes can be set on a DefaultTimeBar when used in a layout XML file:
*
* <ul>
* <li><b>{@code bar_height}</b> - Dimension for the height of the time bar.
* <ul>
* <li>Default: {@link #DEFAULT_BAR_HEIGHT_DP}
* </ul>
* <li><b>{@code touch_target_height}</b> - Dimension for the height of the area in which touch
* interactions with the time bar are handled. If no height is specified, this also determines
* the height of the view.
* <ul>
* <li>Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
* </ul>
* <li><b>{@code ad_marker_width}</b> - Dimension for the width of any ad markers shown on the
* bar. Ad markers are superimposed on the time bar to show the times at which ads will play.
* <ul>
* <li>Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
* </ul>
* <li><b>{@code scrubber_enabled_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle
* should be shown.
* <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
* </ul>
* <li><b>{@code scrubber_disabled_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown.
* <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
* </ul>
* <li><b>{@code scrubber_dragged_size}</b> - Dimension for the diameter of the circular scrubber
* handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown.
* <ul>
* <li>Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
* </ul>
* <li><b>{@code scrubber_drawable}</b> - Optional reference to a drawable to draw for the
* scrubber handle. If set, this overrides the default behavior, which is to draw a circle for
* the scrubber handle.
* <li><b>{@code played_color}</b> - Color for the portion of the time bar representing media
* before the current playback position.
* <ul>
* <li>Corresponding method: {@link #setPlayedColor(int)}
* <li>Default: {@link #DEFAULT_PLAYED_COLOR}
* </ul>
* <li><b>{@code scrubber_color}</b> - Color for the scrubber handle.
* <ul>
* <li>Corresponding method: {@link #setScrubberColor(int)}
* <li>Default: {@link #DEFAULT_SCRUBBER_COLOR}
* </ul>
* <li><b>{@code buffered_color}</b> - Color for the portion of the time bar after the current
* played position up to the current buffered position.
* <ul>
* <li>Corresponding method: {@link #setBufferedColor(int)}
* <li>Default: {@link #DEFAULT_BUFFERED_COLOR}
* </ul>
* <li><b>{@code unplayed_color}</b> - Color for the portion of the time bar after the current
* buffered position.
* <ul>
* <li>Corresponding method: {@link #setUnplayedColor(int)}
* <li>Default: {@link #DEFAULT_UNPLAYED_COLOR}
* </ul>
* <li><b>{@code ad_marker_color}</b> - Color for unplayed ad markers.
* <ul>
* <li>Corresponding method: {@link #setAdMarkerColor(int)}
* <li>Default: {@link #DEFAULT_AD_MARKER_COLOR}
* </ul>
* <li><b>{@code played_ad_marker_color}</b> - Color for played ad markers.
* <ul>
* <li>Corresponding method: {@link #setPlayedAdMarkerColor(int)}
* <li>Default: {@link #DEFAULT_PLAYED_AD_MARKER_COLOR}
* </ul>
* </ul>
*/
public class DefaultThumbnailTimeBar extends View implements TimeBar {
/** Default height for the time bar, in dp. */
public static final int DEFAULT_BAR_HEIGHT_DP = 4;
/** Default height for the touch target, in dp. */
public static final int DEFAULT_TOUCH_TARGET_HEIGHT_DP = 26;
/** Default width for ad markers, in dp. */
public static final int DEFAULT_AD_MARKER_WIDTH_DP = 4;
/** Default diameter for the scrubber when enabled, in dp. */
public static final int DEFAULT_SCRUBBER_ENABLED_SIZE_DP = 12;
/** Default diameter for the scrubber when disabled, in dp. */
public static final int DEFAULT_SCRUBBER_DISABLED_SIZE_DP = 0;
/** Default diameter for the scrubber when dragged, in dp. */
public static final int DEFAULT_SCRUBBER_DRAGGED_SIZE_DP = 16;
/** Default color for the played portion of the time bar. */
public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF;
/** Default color for the unplayed portion of the time bar. */
public static final int DEFAULT_UNPLAYED_COLOR = 0x33FFFFFF;
/** Default color for the buffered portion of the time bar. */
public static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF;
/** Default color for the scrubber handle. */
public static final int DEFAULT_SCRUBBER_COLOR = 0xFFFFFFFF;
/** Default color for ad markers. */
public static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00;
/** Default color for played ad markers. */
public static final int DEFAULT_PLAYED_AD_MARKER_COLOR = 0x33FFFF00;
/** Vertical gravity for progress bar to be located at the center in the view. */
public static final int BAR_GRAVITY_CENTER = 0;
/** Vertical gravity for progress bar to be located at the bottom in the view. */
public static final int BAR_GRAVITY_BOTTOM = 1;
/** The threshold in dps above the bar at which touch events trigger fine scrub mode. */
private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50;
/** The ratio by which times are reduced in fine scrub mode. */
private static final int FINE_SCRUB_RATIO = 3;
/**
* The time after which the scrubbing listener is notified that scrubbing has stopped after
* performing an incremental scrub using key input.
*/
private static final long STOP_SCRUBBING_TIMEOUT_MS = 1000;
private static final int DEFAULT_INCREMENT_COUNT = 20;
private static final float SHOWN_SCRUBBER_SCALE = 1.0f;
private static final float HIDDEN_SCRUBBER_SCALE = 0.0f;
/**
* The name of the Android SDK view that most closely resembles this custom view. Used as the
* class name for accessibility.
*/
private static final String ACCESSIBILITY_CLASS_NAME = "android.widget.SeekBar";
private final Rect seekBounds;
private final Rect progressBar;
private final Rect bufferedBar;
private final Rect scrubberBar;
private final Paint playedPaint;
private final Paint bufferedPaint;
private final Paint unplayedPaint;
private final Paint adMarkerPaint;
private final Paint playedAdMarkerPaint;
private final Paint scrubberPaint;
@Nullable private final Drawable scrubberDrawable;
private final int barHeight;
private final int touchTargetHeight;
private final int barGravity;
private final int adMarkerWidth;
private final int scrubberEnabledSize;
private final int scrubberDisabledSize;
private final int scrubberDraggedSize;
private final int scrubberPadding;
private final int fineScrubYThreshold;
private final StringBuilder formatBuilder;
private final Formatter formatter;
private final Runnable stopScrubbingRunnable;
private final CopyOnWriteArraySet<OnScrubListener> listeners;
private final Point touchPosition;
private final float density;
private int keyCountIncrement;
private long keyTimeIncrement;
private int lastCoarseScrubXPosition;
private @MonotonicNonNull Rect lastExclusionRectangle;
private ValueAnimator scrubberScalingAnimator;
private float scrubberScale;
private boolean scrubberPaddingDisabled;
private boolean scrubbing;
private long scrubPosition;
private long duration;
private long position;
private long bufferedPosition;
private int adGroupCount;
@Nullable private long[] adGroupTimesMs;
@Nullable private boolean[] playedAdGroups;
private ThumbnailProvider thumbnailUtils;
//TODO put in ressource file
int targetThumbnailHeightInDp = 80;
public DefaultThumbnailTimeBar(Context context) {
this(context, null);
}
public DefaultThumbnailTimeBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DefaultThumbnailTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, attrs);
}
public DefaultThumbnailTimeBar(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet timebarAttrs) {
this(context, attrs, defStyleAttr, timebarAttrs, 0);
}
// Suppress warnings due to usage of View methods in the constructor.
@SuppressWarnings("nullness:method.invocation")
public DefaultThumbnailTimeBar(
Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
@Nullable AttributeSet timebarAttrs,
int defStyleRes) {
super(context, attrs, defStyleAttr);
seekBounds = new Rect();
progressBar = new Rect();
bufferedBar = new Rect();
scrubberBar = new Rect();
playedPaint = new Paint();
bufferedPaint = new Paint();
unplayedPaint = new Paint();
adMarkerPaint = new Paint();
playedAdMarkerPaint = new Paint();
scrubberPaint = new Paint();
scrubberPaint.setAntiAlias(true);
listeners = new CopyOnWriteArraySet<>();
touchPosition = new Point();
// Calculate the dimensions and paints for drawn elements.
Resources res = context.getResources();
DisplayMetrics displayMetrics = res.getDisplayMetrics();
density = displayMetrics.density;
fineScrubYThreshold = dpToPx(density, FINE_SCRUB_Y_THRESHOLD_DP);
int defaultBarHeight = dpToPx(density, DEFAULT_BAR_HEIGHT_DP);
int defaultTouchTargetHeight = dpToPx(density, DEFAULT_TOUCH_TARGET_HEIGHT_DP);
int defaultAdMarkerWidth = dpToPx(density, DEFAULT_AD_MARKER_WIDTH_DP);
int defaultScrubberEnabledSize = dpToPx(density, DEFAULT_SCRUBBER_ENABLED_SIZE_DP);
int defaultScrubberDisabledSize = dpToPx(density, DEFAULT_SCRUBBER_DISABLED_SIZE_DP);
int defaultScrubberDraggedSize = dpToPx(density, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP);
if (timebarAttrs != null) {
TypedArray a =
context
.getTheme()
.obtainStyledAttributes(
timebarAttrs, com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar, defStyleAttr, defStyleRes);
try {
scrubberDrawable = a.getDrawable(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_drawable);
if (scrubberDrawable != null) {
setDrawableLayoutDirection(scrubberDrawable);
defaultTouchTargetHeight =
Math.max(scrubberDrawable.getMinimumHeight(), defaultTouchTargetHeight);
}
barHeight =
a.getDimensionPixelSize(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_bar_height, defaultBarHeight);
touchTargetHeight =
a.getDimensionPixelSize(
com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_touch_target_height, defaultTouchTargetHeight);
barGravity = a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_bar_gravity, BAR_GRAVITY_CENTER);
adMarkerWidth =
a.getDimensionPixelSize(
com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_ad_marker_width, defaultAdMarkerWidth);
scrubberEnabledSize =
a.getDimensionPixelSize(
com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_enabled_size, defaultScrubberEnabledSize);
scrubberDisabledSize =
a.getDimensionPixelSize(
com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_disabled_size, defaultScrubberDisabledSize);
scrubberDraggedSize =
a.getDimensionPixelSize(
com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize);
int playedColor = a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR);
int scrubberColor =
a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_scrubber_color, DEFAULT_SCRUBBER_COLOR);
int bufferedColor =
a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_buffered_color, DEFAULT_BUFFERED_COLOR);
int unplayedColor =
a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_unplayed_color, DEFAULT_UNPLAYED_COLOR);
int adMarkerColor =
a.getInt(com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR);
int playedAdMarkerColor =
a.getInt(
com.google.android.exoplayer2.ui.R.styleable.DefaultTimeBar_played_ad_marker_color, DEFAULT_PLAYED_AD_MARKER_COLOR);
playedPaint.setColor(playedColor);
scrubberPaint.setColor(scrubberColor);
bufferedPaint.setColor(bufferedColor);
unplayedPaint.setColor(unplayedColor);
adMarkerPaint.setColor(adMarkerColor);
playedAdMarkerPaint.setColor(playedAdMarkerColor);
} finally {
a.recycle();
}
} else {
barHeight = defaultBarHeight;
touchTargetHeight = defaultTouchTargetHeight;
barGravity = BAR_GRAVITY_CENTER;
adMarkerWidth = defaultAdMarkerWidth;
scrubberEnabledSize = defaultScrubberEnabledSize;
scrubberDisabledSize = defaultScrubberDisabledSize;
scrubberDraggedSize = defaultScrubberDraggedSize;
playedPaint.setColor(DEFAULT_PLAYED_COLOR);
scrubberPaint.setColor(DEFAULT_SCRUBBER_COLOR);
bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR);
unplayedPaint.setColor(DEFAULT_UNPLAYED_COLOR);
adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR);
playedAdMarkerPaint.setColor(DEFAULT_PLAYED_AD_MARKER_COLOR);
scrubberDrawable = null;
}
formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault());
stopScrubbingRunnable = () -> stopScrubbing(/* canceled= */ false);
if (scrubberDrawable != null) {
scrubberPadding = (scrubberDrawable.getMinimumWidth() + 1) / 2;
} else {
scrubberPadding =
(Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1)
/ 2;
}
scrubberScale = 1.0f;
scrubberScalingAnimator = new ValueAnimator();
scrubberScalingAnimator.addUpdateListener(
animation -> {
scrubberScale = (float) animation.getAnimatedValue();
invalidate(seekBounds);
});
duration = C.TIME_UNSET;
keyTimeIncrement = C.TIME_UNSET;
keyCountIncrement = DEFAULT_INCREMENT_COUNT;
setFocusable(true);
if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
public void setThumbnailUtils(ThumbnailProvider thumbnailUtils) {
this.thumbnailUtils = thumbnailUtils;
}
/** Shows the scrubber handle. */
public void showScrubber() {
if (scrubberScalingAnimator.isStarted()) {
scrubberScalingAnimator.cancel();
}
scrubberPaddingDisabled = false;
scrubberScale = 1;
invalidate(seekBounds);
}
/**
* Shows the scrubber handle with animation.
*
* @param showAnimationDurationMs The duration for scrubber showing animation.
*/
public void showScrubber(long showAnimationDurationMs) {
if (scrubberScalingAnimator.isStarted()) {
scrubberScalingAnimator.cancel();
}
scrubberPaddingDisabled = false;
scrubberScalingAnimator.setFloatValues(scrubberScale, SHOWN_SCRUBBER_SCALE);
scrubberScalingAnimator.setDuration(showAnimationDurationMs);
scrubberScalingAnimator.start();
}
/** Hides the scrubber handle. */
public void hideScrubber(boolean disableScrubberPadding) {
if (scrubberScalingAnimator.isStarted()) {
scrubberScalingAnimator.cancel();
}
scrubberPaddingDisabled = disableScrubberPadding;
scrubberScale = 0;
invalidate(seekBounds);
}
/**
* Hides the scrubber handle with animation.
*
* @param hideAnimationDurationMs The duration for scrubber hiding animation.
*/
public void hideScrubber(long hideAnimationDurationMs) {
if (scrubberScalingAnimator.isStarted()) {
scrubberScalingAnimator.cancel();
}
scrubberScalingAnimator.setFloatValues(scrubberScale, HIDDEN_SCRUBBER_SCALE);
scrubberScalingAnimator.setDuration(hideAnimationDurationMs);
scrubberScalingAnimator.start();
}
/**
* Sets the color for the portion of the time bar representing media before the playback position.
*
* @param playedColor The color for the portion of the time bar representing media before the
* playback position.
*/
public void setPlayedColor(@ColorInt int playedColor) {
playedPaint.setColor(playedColor);
invalidate(seekBounds);
}
/**
* Sets the color for the scrubber handle.
*
* @param scrubberColor The color for the scrubber handle.
*/
public void setScrubberColor(@ColorInt int scrubberColor) {
scrubberPaint.setColor(scrubberColor);
invalidate(seekBounds);
}
/**
* Sets the color for the portion of the time bar after the current played position up to the
* current buffered position.
*
* @param bufferedColor The color for the portion of the time bar after the current played
* position up to the current buffered position.
*/
public void setBufferedColor(@ColorInt int bufferedColor) {
bufferedPaint.setColor(bufferedColor);
invalidate(seekBounds);
}
/**
* Sets the color for the portion of the time bar after the current played position.
*
* @param unplayedColor The color for the portion of the time bar after the current played
* position.
*/
public void setUnplayedColor(@ColorInt int unplayedColor) {
unplayedPaint.setColor(unplayedColor);
invalidate(seekBounds);
}
/**
* Sets the color for unplayed ad markers.
*
* @param adMarkerColor The color for unplayed ad markers.
*/
public void setAdMarkerColor(@ColorInt int adMarkerColor) {
adMarkerPaint.setColor(adMarkerColor);
invalidate(seekBounds);
}
/**
* Sets the color for played ad markers.
*
* @param playedAdMarkerColor The color for played ad markers.
*/
public void setPlayedAdMarkerColor(@ColorInt int playedAdMarkerColor) {
playedAdMarkerPaint.setColor(playedAdMarkerColor);
invalidate(seekBounds);
}
// TimeBar implementation.
@Override
public void addListener(OnScrubListener listener) {
Assertions.checkNotNull(listener);
listeners.add(listener);
}
@Override
public void removeListener(OnScrubListener listener) {
listeners.remove(listener);
}
@Override
public void setKeyTimeIncrement(long time) {
Assertions.checkArgument(time > 0);
keyCountIncrement = C.INDEX_UNSET;
keyTimeIncrement = time;
}
@Override
public void setKeyCountIncrement(int count) {
Assertions.checkArgument(count > 0);
keyCountIncrement = count;
keyTimeIncrement = C.TIME_UNSET;
}
@Override
public void setPosition(long position) {
if (this.position == position) {
return;
}
this.position = position;
setContentDescription(getProgressText());
update();
}
@Override
public void setBufferedPosition(long bufferedPosition) {
if (this.bufferedPosition == bufferedPosition) {
return;
}
this.bufferedPosition = bufferedPosition;
update();
}
@Override
public void setDuration(long duration) {
if (this.duration == duration) {
return;
}
this.duration = duration;
if (scrubbing && duration == C.TIME_UNSET) {
stopScrubbing(/* canceled= */ true);
}
update();
}
@Override
public long getPreferredUpdateDelay() {
int timeBarWidthDp = pxToDp(density, progressBar.width());
return timeBarWidthDp == 0 || duration == 0 || duration == C.TIME_UNSET
? Long.MAX_VALUE
: duration / timeBarWidthDp;
}
@Override
public void setAdGroupTimesMs(
@Nullable long[] adGroupTimesMs, @Nullable boolean[] playedAdGroups, int adGroupCount) {
Assertions.checkArgument(
adGroupCount == 0 || (adGroupTimesMs != null && playedAdGroups != null));
this.adGroupCount = adGroupCount;
this.adGroupTimesMs = adGroupTimesMs;
this.playedAdGroups = playedAdGroups;
update();
}
// View methods.
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (scrubbing && !enabled) {
stopScrubbing(/* canceled= */ true);
}
}
@Override
public void onDraw(Canvas canvas) {
canvas.save();
drawTimeBar(canvas);
drawPlayhead(canvas);
canvas.restore();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled() || duration <= 0) {
return false;
}
Point touchPosition = resolveRelativeTouchPosition(event);
int x = touchPosition.x;
int y = touchPosition.y;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isInSeekBar(x, y)) {
positionScrubber(x);
startScrubbing(getScrubberPosition());
update();
invalidate();
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (scrubbing) {
if (y < fineScrubYThreshold) {
int relativeX = x - lastCoarseScrubXPosition;
positionScrubber(lastCoarseScrubXPosition + relativeX / FINE_SCRUB_RATIO);
} else {
lastCoarseScrubXPosition = x;
positionScrubber(x);
}
updateScrubbing(getScrubberPosition());
update();
invalidate();
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (scrubbing) {
stopScrubbing(/* canceled= */ event.getAction() == MotionEvent.ACTION_CANCEL);
return true;
}
break;
default:
// Do nothing.
}
return false;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (isEnabled()) {
long positionIncrement = getPositionIncrement();
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
positionIncrement = -positionIncrement;
// Fall through.
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (scrubIncrementally(positionIncrement)) {
removeCallbacks(stopScrubbingRunnable);
postDelayed(stopScrubbingRunnable, STOP_SCRUBBING_TIMEOUT_MS);
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (scrubbing) {
stopScrubbing(/* canceled= */ false);
return true;
}
break;
default:
// Do nothing.
}
}
return super.onKeyDown(keyCode, event);
}
@Override
protected void onFocusChanged(
boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
if (scrubbing && !gainFocus) {
stopScrubbing(/* canceled= */ false);
}
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
updateDrawableState();
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (scrubberDrawable != null) {
scrubberDrawable.jumpToCurrentState();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height =
heightMode == MeasureSpec.UNSPECIFIED
? touchTargetHeight
: heightMode == MeasureSpec.EXACTLY
? heightSize
: Math.min(touchTargetHeight, heightSize);
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height);
updateDrawableState();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int width = right - left;
int height = bottom - top;
int seekLeft = getPaddingLeft();
int seekRight = width - getPaddingRight();
int seekBoundsY;
int progressBarY;
int scrubberPadding = scrubberPaddingDisabled ? 0 : this.scrubberPadding;
if (barGravity == BAR_GRAVITY_BOTTOM) {
seekBoundsY = height - getPaddingBottom() - touchTargetHeight;
progressBarY =
height - getPaddingBottom() - barHeight - Math.max(scrubberPadding - (barHeight / 2), 0);
} else {
seekBoundsY = (height - touchTargetHeight) / 2;
progressBarY = (height - barHeight) / 2;
}
seekBounds.set(seekLeft, seekBoundsY, seekRight, seekBoundsY + touchTargetHeight);
progressBar.set(
seekBounds.left + scrubberPadding,
progressBarY,
seekBounds.right - scrubberPadding,
progressBarY + barHeight);
if (Util.SDK_INT >= 29) {
setSystemGestureExclusionRectsV29(width, height);
}
update();
}
@Override
public void onRtlPropertiesChanged(int layoutDirection) {
if (scrubberDrawable != null && setDrawableLayoutDirection(scrubberDrawable, layoutDirection)) {
invalidate();
}
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) {
event.getText().add(getProgressText());
}
event.setClassName(ACCESSIBILITY_CLASS_NAME);
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(ACCESSIBILITY_CLASS_NAME);
info.setContentDescription(getProgressText());
if (duration <= 0) {
return;
}
if (Util.SDK_INT >= 21) {
info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
} else {
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
}
}
@Override
public boolean performAccessibilityAction(int action, @Nullable Bundle args) {
if (super.performAccessibilityAction(action, args)) {
return true;
}
if (duration <= 0) {
return false;
}
if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
if (scrubIncrementally(-getPositionIncrement())) {
stopScrubbing(/* canceled= */ false);
}
} else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) {
if (scrubIncrementally(getPositionIncrement())) {
stopScrubbing(/* canceled= */ false);
}
} else {
return false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
return true;
}
// Internal methods.
private void startScrubbing(long scrubPosition) {
this.scrubPosition = scrubPosition;
scrubbing = true;
setPressed(true);
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
for (OnScrubListener listener : listeners) {
listener.onScrubStart(this, scrubPosition);
}
}
private void updateScrubbing(long scrubPosition) {
if (this.scrubPosition == scrubPosition) {
return;
}
this.scrubPosition = scrubPosition;
for (OnScrubListener listener : listeners) {
listener.onScrubMove(this, scrubPosition);
}
}
private void stopScrubbing(boolean canceled) {
removeCallbacks(stopScrubbingRunnable);
scrubbing = false;
setPressed(false);
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(false);
}
invalidate();
for (OnScrubListener listener : listeners) {
listener.onScrubStop(this, scrubPosition, canceled);
}
}
/**
* Incrementally scrubs the position by {@code positionChange}.
*
* @param positionChange The change in the scrubber position, in milliseconds. May be negative.
* @return Returns whether the scrubber position changed.
*/
private boolean scrubIncrementally(long positionChange) {
if (duration <= 0) {
return false;
}
long previousPosition = scrubbing ? scrubPosition : position;
long scrubPosition = Util.constrainValue(previousPosition + positionChange, 0, duration);
if (scrubPosition == previousPosition) {
return false;
}
if (!scrubbing) {
startScrubbing(scrubPosition);
} else {
updateScrubbing(scrubPosition);
}
update();
return true;
}
private void update() {
bufferedBar.set(progressBar);
scrubberBar.set(progressBar);
long newScrubberTime = scrubbing ? scrubPosition : position;
if (duration > 0) {
int bufferedPixelWidth = (int) ((progressBar.width() * bufferedPosition) / duration);
bufferedBar.right = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right);
int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration);
scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right);
} else {
bufferedBar.right = progressBar.left;
scrubberBar.right = progressBar.left;
}
invalidate(seekBounds);
}
private void positionScrubber(float xPosition) {
scrubberBar.right = Util.constrainValue((int) xPosition, progressBar.left, progressBar.right);
}
private Point resolveRelativeTouchPosition(MotionEvent motionEvent) {
touchPosition.set((int) motionEvent.getX(), (int) motionEvent.getY());
return touchPosition;
}
private long getScrubberPosition() {
if (progressBar.width() <= 0 || duration == C.TIME_UNSET) {
return 0;
}
return (scrubberBar.width() * duration) / progressBar.width();
}
private boolean isInSeekBar(float x, float y) {
return seekBounds.contains((int) x, (int) y);
}
private void drawTimeBar(Canvas canvas) {
int progressBarHeight = progressBar.height();
int barTop = progressBar.centerY() - progressBarHeight / 2;
int barBottom = barTop + progressBarHeight;
if (duration <= 0) {
canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, unplayedPaint);
return;
}
int bufferedLeft = bufferedBar.left;
int bufferedRight = bufferedBar.right;
int progressLeft = Math.max(Math.max(progressBar.left, bufferedRight), scrubberBar.right);
if (progressLeft < progressBar.right) {
canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, unplayedPaint);
}
bufferedLeft = Math.max(bufferedLeft, scrubberBar.right);
if (bufferedRight > bufferedLeft) {
canvas.drawRect(bufferedLeft, barTop, bufferedRight, barBottom, bufferedPaint);
}
if (scrubberBar.width() > 0) {
canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint);
}
if (adGroupCount == 0) {
return;
}
long[] adGroupTimesMs = Assertions.checkNotNull(this.adGroupTimesMs);
boolean[] playedAdGroups = Assertions.checkNotNull(this.playedAdGroups);
int adMarkerOffset = adMarkerWidth / 2;
for (int i = 0; i < adGroupCount; i++) {
long adGroupTimeMs = Util.constrainValue(adGroupTimesMs[i], 0, duration);
int markerPositionOffset =
(int) (progressBar.width() * adGroupTimeMs / duration) - adMarkerOffset;
int markerLeft =
progressBar.left
+ Math.min(progressBar.width() - adMarkerWidth, Math.max(0, markerPositionOffset));
Paint paint = playedAdGroups[i] ? playedAdMarkerPaint : adMarkerPaint;
canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, paint);
}
}
private void drawPlayhead(Canvas canvas) {
if (duration <= 0) {
return;
}
int playheadX = Util.constrainValue(scrubberBar.right, scrubberBar.left, progressBar.right);
int playheadY = scrubberBar.centerY();
if (scrubberDrawable == null) {
int scrubberSize =
(scrubbing || isFocused())
? scrubberDraggedSize
: (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize);
int playheadRadius = (int) ((scrubberSize * scrubberScale) / 2);
canvas.drawCircle(playheadX, playheadY, playheadRadius, scrubberPaint);
if (scrubbing) drawThumbnail(canvas, playheadRadius, playheadX, playheadY);
} else {
int scrubberDrawableWidth = (int) (scrubberDrawable.getIntrinsicWidth() * scrubberScale);
int scrubberDrawableHeight = (int) (scrubberDrawable.getIntrinsicHeight() * scrubberScale);
scrubberDrawable.setBounds(
playheadX - scrubberDrawableWidth / 2,
playheadY - scrubberDrawableHeight / 2,
playheadX + scrubberDrawableWidth / 2,
playheadY + scrubberDrawableHeight / 2);
scrubberDrawable.draw(canvas);
}
}
private void drawThumbnail(Canvas canvas, int playheadRadius, int playheadX, int playheadY) {
if (thumbnailUtils == null) return;
Bitmap b = thumbnailUtils.getThumbnail(getScrubberPosition());
if (b == null) return;
//adapt thumbnail to desired UI size
double arFactor = (double) b.getWidth() / b.getHeight();
b = Bitmap.createScaledBitmap(b, dpToPx((int)(targetThumbnailHeightInDp * arFactor)), dpToPx(targetThumbnailHeightInDp), false);
int width = b.getWidth();
int height = b.getHeight();
int offset = (int)width / 2;
int left = playheadX-offset;
//handle full left, full right position cases
if (left < 0 ) left = 0;
if (left + width > progressBar.width() + playheadRadius) left = progressBar.width() + playheadRadius - width;
canvas.drawBitmap(b, left, playheadY-playheadRadius*2-height, null);
}
private int dpToPx(int dp){
return (int) (dp * getContext().getResources().getDisplayMetrics().density);
}
private void updateDrawableState() {
if (scrubberDrawable != null
&& scrubberDrawable.isStateful()
&& scrubberDrawable.setState(getDrawableState())) {
invalidate();
}
}
@RequiresApi(29)
private void setSystemGestureExclusionRectsV29(int width, int height) {
if (lastExclusionRectangle != null
&& lastExclusionRectangle.width() == width
&& lastExclusionRectangle.height() == height) {
// Allocating inside onLayout is considered a DrawAllocation lint error, so avoid if possible.
return;
}
lastExclusionRectangle = new Rect(/* left= */ 0, /* top= */ 0, width, height);
setSystemGestureExclusionRects(Collections.singletonList(lastExclusionRectangle));
}
private String getProgressText() {
return Util.getStringForTime(formatBuilder, formatter, position);
}
private long getPositionIncrement() {
return keyTimeIncrement == C.TIME_UNSET
? (duration == C.TIME_UNSET ? 0 : (duration / keyCountIncrement))
: keyTimeIncrement;
}
private boolean setDrawableLayoutDirection(Drawable drawable) {
return Util.SDK_INT >= 23 && setDrawableLayoutDirection(drawable, getLayoutDirection());
}
private static boolean setDrawableLayoutDirection(Drawable drawable, int layoutDirection) {
return Util.SDK_INT >= 23 && drawable.setLayoutDirection(layoutDirection);
}
private static int dpToPx(float density, int dps) {
return (int) (dps * density + 0.5f);
}
private static int pxToDp(float density, int px) {
return (int) (px / density);
}
}
package com.google.android.exoplayer2.demo;
import android.graphics.Bitmap;
public interface ThumbnailProvider {
public Bitmap getThumbnail(long position);
}
<?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">
<!-- 0dp dimensions are used to prevent this view from influencing the size of
the parent view if it uses "wrap_content". It is expanded to occupy the
entirety of the parent in code, after the parent's size has been
determined. See: https://github.com/google/ExoPlayer/issues/8726.
-->
<View android:id="@id/exo_controls_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/exo_black_opacity_60"/>
<FrameLayout android:id="@id/exo_bottom_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_styled_bottom_bar_height"
android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top"
android:layout_gravity="bottom"
android:background="@color/exo_bottom_bar_background"
android:layoutDirection="ltr">
<LinearLayout android:id="@id/exo_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding"
android:layout_gravity="center_vertical|start"
android:layoutDirection="ltr">
<TextView android:id="@id/exo_position"
style="@style/ExoStyledControls.TimeText.Position"/>
<TextView
style="@style/ExoStyledControls.TimeText.Separator"/>
<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="center_vertical|end"
android:layoutDirection="ltr">
<ImageButton android:id="@id/exo_vr"
style="@style/ExoStyledControls.Button.Bottom.VR"/>
<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_subtitle"
style="@style/ExoStyledControls.Button.Bottom.CC"/>
<ImageButton android:id="@id/exo_settings"
style="@style/ExoStyledControls.Button.Bottom.Settings"/>
<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="center_vertical|end"
android:visibility="invisible">
<LinearLayout android:id="@id/exo_extra_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layoutDirection="ltr">
<ImageButton android:id="@id/exo_overflow_hide"
style="@style/ExoStyledControls.Button.Bottom.OverflowHide"/>
</LinearLayout>
</HorizontalScrollView>
</FrameLayout>
<com.google.android.exoplayer2.demo.DefaultThumbnailTimeBar android:id="@id/exo_progress"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_gravity="bottom"
style="@style/ExoStyledControls.TimeBar"
android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/>
<LinearLayout android:id="@id/exo_minimal_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layoutDirection="ltr">
<ImageButton android:id="@id/exo_minimal_fullscreen"
style="@style/ExoStyledControls.Button.Bottom.FullScreen"/>
</LinearLayout>
<LinearLayout
android:id="@id/exo_center_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@android:color/transparent"
android:gravity="center"
android:padding="@dimen/exo_styled_controls_padding"
android:clipToPadding="false">
<ImageButton android:id="@id/exo_prev"
style="@style/ExoStyledControls.Button.Center.Previous"/>
<include layout="@layout/exo_styled_player_control_rewind_button" />
<ImageButton android:id="@id/exo_play_pause"
style="@style/ExoStyledControls.Button.Center.PlayPause"/>
<include layout="@layout/exo_styled_player_control_ffwd_button" />
<ImageButton android:id="@id/exo_next"
style="@style/ExoStyledControls.Button.Center.Next"/>
</LinearLayout>
</merge>
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