Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
SDK
/
exoplayer
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
158cf0c8
authored
Nov 18, 2022
by
Görkem Güclü
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
Added missing files
parent
16af5b7d
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
1381 additions
and
0 deletions
demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java
demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java
demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java
demos/main/src/main/res/layout/exo_styled_player_control_view.xml
demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailProvider.java
0 → 100644
View file @
158cf0c8
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
);
}
}
}
}
demos/main/src/main/java/com/google/android/exoplayer2/demo/DefaultThumbnailTimeBar.java
0 → 100644
View file @
158cf0c8
/*
* 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
);
}
}
demos/main/src/main/java/com/google/android/exoplayer2/demo/ThumbnailProvider.java
0 → 100644
View file @
158cf0c8
package
com
.
google
.
android
.
exoplayer2
.
demo
;
import
android.graphics.Bitmap
;
public
interface
ThumbnailProvider
{
public
Bitmap
getThumbnail
(
long
position
);
}
demos/main/src/main/res/layout/exo_styled_player_control_view.xml
0 → 100644
View file @
158cf0c8
<?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>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment