java.lang.Object
↳View
↳androidx.media3.ui.DefaultTimeBar
Gradle dependencies
compile group: 'androidx.media3', name: 'media3-ui', version: '1.5.0-alpha01'
- groupId: androidx.media3
- artifactId: media3-ui
- version: 1.5.0-alpha01
Artifact androidx.media3:media3-ui:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)
Overview
A time bar that shows a current position, buffered position, duration and ad markers.
A DefaultTimeBar can be customized by setting attributes, as outlined below.
Attributes
The following attributes can be set on a DefaultTimeBar when used in a layout XML file:
- bar_height - Dimension for the height of the time bar.
- touch_target_height - 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.
- ad_marker_width - 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.
- scrubber_enabled_size - 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.
- scrubber_disabled_size - Dimension for the diameter of the circular scrubber
handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown.
- scrubber_dragged_size - Dimension for the diameter of the circular scrubber
handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown.
- scrubber_drawable - 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.
- played_color - Color for the portion of the time bar representing media
before the current playback position.
- scrubber_color - Color for the scrubber handle.
- buffered_color - Color for the portion of the time bar after the current
played position up to the current buffered position.
- unplayed_color - Color for the portion of the time bar after the current
buffered position.
- ad_marker_color - Color for unplayed ad markers.
- played_ad_marker_color - Color for played ad markers.
Summary
Constructors |
---|
public | DefaultTimeBar(Context context)
|
public | DefaultTimeBar(Context context, AttributeSet attrs)
|
public | DefaultTimeBar(Context context, AttributeSet attrs, int defStyleAttr)
|
public | DefaultTimeBar(Context context, AttributeSet attrs, int defStyleAttr, AttributeSet timebarAttrs)
|
public | DefaultTimeBar(Context context, AttributeSet attrs, int defStyleAttr, AttributeSet timebarAttrs, int defStyleRes)
|
Methods |
---|
public void | addListener(TimeBar.OnScrubListener listener)
|
protected void | drawableStateChanged()
|
public long | getPreferredUpdateDelay()
|
public void | hideScrubber(boolean disableScrubberPadding)
Hides the scrubber handle. |
public void | hideScrubber(long hideAnimationDurationMs)
Hides the scrubber handle with animation. |
public void | jumpDrawablesToCurrentState()
|
public void | onDraw(Canvas canvas)
|
protected void | onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)
|
public void | onInitializeAccessibilityEvent(AccessibilityEvent event)
|
public void | onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)
|
public boolean | onKeyDown(int keyCode, KeyEvent event)
|
protected void | onLayout(boolean changed, int left, int top, int right, int bottom)
|
protected void | onMeasure(int widthMeasureSpec, int heightMeasureSpec)
|
public void | onRtlPropertiesChanged(int layoutDirection)
|
public boolean | onTouchEvent(MotionEvent event)
|
public boolean | performAccessibilityAction(int action, Bundle args)
|
public void | removeListener(TimeBar.OnScrubListener listener)
|
public void | setAdGroupTimesMs(long[] adGroupTimesMs[], boolean[] playedAdGroups[], int adGroupCount)
|
public void | setAdMarkerColor(int adMarkerColor)
Sets the color for unplayed ad markers. |
public void | setBufferedColor(int bufferedColor)
Sets the color for the portion of the time bar after the current played position up to the
current buffered position. |
public void | setBufferedPosition(long bufferedPosition)
|
public void | setDuration(long duration)
|
public void | setEnabled(boolean enabled)
|
public void | setKeyCountIncrement(int count)
|
public void | setKeyTimeIncrement(long time)
|
public void | setPlayedAdMarkerColor(int playedAdMarkerColor)
Sets the color for played ad markers. |
public void | setPlayedColor(int playedColor)
Sets the color for the portion of the time bar representing media before the playback position. |
public void | setPosition(long position)
|
public void | setScrubberColor(int scrubberColor)
Sets the color for the scrubber handle. |
public void | setUnplayedColor(int unplayedColor)
Sets the color for the portion of the time bar after the current played position. |
public void | showScrubber()
Shows the scrubber handle. |
public void | showScrubber(long showAnimationDurationMs)
Shows the scrubber handle with animation. |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
DEFAULT_BAR_HEIGHT_DPDefault height for the time bar, in dp.
public static final int
DEFAULT_TOUCH_TARGET_HEIGHT_DPDefault height for the touch target, in dp.
public static final int
DEFAULT_AD_MARKER_WIDTH_DPDefault width for ad markers, in dp.
public static final int
DEFAULT_SCRUBBER_ENABLED_SIZE_DPDefault diameter for the scrubber when enabled, in dp.
public static final int
DEFAULT_SCRUBBER_DISABLED_SIZE_DPDefault diameter for the scrubber when disabled, in dp.
public static final int
DEFAULT_SCRUBBER_DRAGGED_SIZE_DPDefault diameter for the scrubber when dragged, in dp.
public static final int
DEFAULT_PLAYED_COLORDefault color for the played portion of the time bar.
public static final int
DEFAULT_UNPLAYED_COLORDefault color for the unplayed portion of the time bar.
public static final int
DEFAULT_BUFFERED_COLORDefault color for the buffered portion of the time bar.
public static final int
DEFAULT_SCRUBBER_COLORDefault color for the scrubber handle.
public static final int
DEFAULT_AD_MARKER_COLORDefault color for ad markers.
public static final int
DEFAULT_PLAYED_AD_MARKER_COLORDefault color for played ad markers.
public static final int
BAR_GRAVITY_CENTERVertical gravity for progress bar to be located at the center in the view.
public static final int
BAR_GRAVITY_BOTTOMVertical gravity for progress bar to be located at the bottom in the view.
Constructors
public
DefaultTimeBar(Context context)
public
DefaultTimeBar(Context context, AttributeSet attrs)
public
DefaultTimeBar(Context context, AttributeSet attrs, int defStyleAttr)
public
DefaultTimeBar(Context context, AttributeSet attrs, int defStyleAttr, AttributeSet timebarAttrs)
public
DefaultTimeBar(Context context, AttributeSet attrs, int defStyleAttr, AttributeSet timebarAttrs, int defStyleRes)
Methods
public void
showScrubber()
Shows the scrubber handle.
public void
showScrubber(long showAnimationDurationMs)
Shows the scrubber handle with animation.
Parameters:
showAnimationDurationMs: The duration for scrubber showing animation.
public void
hideScrubber(boolean disableScrubberPadding)
Hides the scrubber handle.
public void
hideScrubber(long hideAnimationDurationMs)
Hides the scrubber handle with animation.
Parameters:
hideAnimationDurationMs: The duration for scrubber hiding animation.
public void
setPlayedColor(int playedColor)
Sets the color for the portion of the time bar representing media before the playback position.
Parameters:
playedColor: The color for the portion of the time bar representing media before the
playback position.
public void
setScrubberColor(int scrubberColor)
Sets the color for the scrubber handle.
Parameters:
scrubberColor: The color for the scrubber handle.
public void
setBufferedColor(int bufferedColor)
Sets the color for the portion of the time bar after the current played position up to the
current buffered position.
Parameters:
bufferedColor: The color for the portion of the time bar after the current played
position up to the current buffered position.
public void
setUnplayedColor(int unplayedColor)
Sets the color for the portion of the time bar after the current played position.
Parameters:
unplayedColor: The color for the portion of the time bar after the current played
position.
public void
setAdMarkerColor(int adMarkerColor)
Sets the color for unplayed ad markers.
Parameters:
adMarkerColor: The color for unplayed ad markers.
public void
setPlayedAdMarkerColor(int playedAdMarkerColor)
Sets the color for played ad markers.
Parameters:
playedAdMarkerColor: The color for played ad markers.
public void
setKeyTimeIncrement(long time)
public void
setKeyCountIncrement(int count)
public void
setPosition(long position)
public void
setBufferedPosition(long bufferedPosition)
public void
setDuration(long duration)
public long
getPreferredUpdateDelay()
public void
setAdGroupTimesMs(long[] adGroupTimesMs[], boolean[] playedAdGroups[], int adGroupCount)
public void
setEnabled(boolean enabled)
public void
onDraw(Canvas canvas)
public boolean
onTouchEvent(MotionEvent event)
public boolean
onKeyDown(int keyCode, KeyEvent event)
protected void
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)
protected void
drawableStateChanged()
public void
jumpDrawablesToCurrentState()
protected void
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void
onLayout(boolean changed, int left, int top, int right, int bottom)
public void
onRtlPropertiesChanged(int layoutDirection)
public void
onInitializeAccessibilityEvent(AccessibilityEvent event)
public void
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)
public boolean
performAccessibilityAction(int action, Bundle args)
Source
/*
* 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 androidx.media3.ui;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
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 androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.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>
*/
@UnstableApi
public class DefaultTimeBar 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;
public DefaultTimeBar(Context context) {
this(context, null);
}
public DefaultTimeBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DefaultTimeBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, attrs);
}
public DefaultTimeBar(
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 DefaultTimeBar(
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, R.styleable.DefaultTimeBar, defStyleAttr, defStyleRes);
try {
scrubberDrawable = a.getDrawable(R.styleable.DefaultTimeBar_scrubber_drawable);
if (scrubberDrawable != null) {
setDrawableLayoutDirection(scrubberDrawable);
defaultTouchTargetHeight =
Math.max(scrubberDrawable.getMinimumHeight(), defaultTouchTargetHeight);
}
barHeight =
a.getDimensionPixelSize(R.styleable.DefaultTimeBar_bar_height, defaultBarHeight);
touchTargetHeight =
a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_touch_target_height, defaultTouchTargetHeight);
barGravity = a.getInt(R.styleable.DefaultTimeBar_bar_gravity, BAR_GRAVITY_CENTER);
adMarkerWidth =
a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_ad_marker_width, defaultAdMarkerWidth);
scrubberEnabledSize =
a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_enabled_size, defaultScrubberEnabledSize);
scrubberDisabledSize =
a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_disabled_size, defaultScrubberDisabledSize);
scrubberDraggedSize =
a.getDimensionPixelSize(
R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize);
int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR);
int scrubberColor =
a.getInt(R.styleable.DefaultTimeBar_scrubber_color, DEFAULT_SCRUBBER_COLOR);
int bufferedColor =
a.getInt(R.styleable.DefaultTimeBar_buffered_color, DEFAULT_BUFFERED_COLOR);
int unplayedColor =
a.getInt(R.styleable.DefaultTimeBar_unplayed_color, DEFAULT_UNPLAYED_COLOR);
int adMarkerColor =
a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR);
int playedAdMarkerColor =
a.getInt(
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);
}
}
/** 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;
}
info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
info.addAction(AccessibilityAction.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);
} 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 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);
}
}