From 9d20a8d41c0cc6cd01b20bd72f3bbfcf11e49cae Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Mar 2017 05:23:02 -0700 Subject: [PATCH] Add a custom time bar view. Also add an isAd flag to Timeline.Period so that periods can be declared as containing ads. The times of these periods are indicated using ad markers in the new TimeBar. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=151116208 --- .../android/exoplayer2/ExoPlayerTest.java | 2 +- .../google/android/exoplayer2/Timeline.java | 8 +- .../exoplayer2/source/LoopingMediaSource.java | 1 + .../source/SinglePeriodTimeline.java | 2 +- .../android/exoplayer2/util/ClosedSource.java | 31 - .../google/android/exoplayer2/util/Util.java | 34 ++ library/core/src/main/proguard-rules.txt | 7 - .../source/dash/DashMediaSource.java | 2 +- .../android/exoplayer2/ui/DefaultTimeBar.java | 574 ++++++++++++++++++ .../exoplayer2/ui/PlaybackControlView.java | 281 ++++++--- .../exoplayer2/ui/SimpleExoPlayerView.java | 10 + .../google/android/exoplayer2/ui/TimeBar.java | 120 ++++ .../res/layout/exo_playback_control_view.xml | 7 +- library/ui/src/main/res/values/attrs.xml | 12 + 14 files changed, 951 insertions(+), 140 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/ClosedSource.java delete mode 100644 library/core/src/main/proguard-rules.txt create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index daa845298b6..9fc55d5c77c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -334,7 +334,7 @@ public int getPeriodCount() { public Period getPeriod(int periodIndex, Period period, boolean setIds) { TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex]; Object id = setIds ? periodIndex : null; - return period.set(id, id, periodIndex, windowDefinition.durationUs, 0); + return period.set(id, id, periodIndex, windowDefinition.durationUs, 0, false); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 333dd25cbe7..eb3966ae4db 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -383,18 +383,24 @@ public static final class Period { */ public long durationUs; + /** + * Whether this period contains an ad. + */ + public boolean isAd; + private long positionInWindowUs; /** * Sets the data held by this period. */ public Period set(Object id, Object uid, int windowIndex, long durationUs, - long positionInWindowUs) { + long positionInWindowUs, boolean isAd) { this.id = id; this.uid = uid; this.windowIndex = windowIndex; this.durationUs = durationUs; this.positionInWindowUs = positionInWindowUs; + this.isAd = isAd; return this; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index d893d602628..b26ae3a6acb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -158,6 +158,7 @@ public int getIndexOfPeriod(Object uid) { int periodIndexOffset = loopCount * childPeriodCount; return childTimeline.getIndexOfPeriod(loopCountAndChildUid.second) + periodIndexOffset; } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index ae367ef14ca..447839392ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -99,7 +99,7 @@ public int getPeriodCount() { public Period getPeriod(int periodIndex, Period period, boolean setIds) { Assertions.checkIndex(periodIndex, 0, 1); Object id = setIds ? ID : null; - return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs); + return period.set(id, id, 0, periodDurationUs, -windowPositionInPeriodUs, false); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ClosedSource.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ClosedSource.java deleted file mode 100644 index ea70920d207..00000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ClosedSource.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2016 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.util; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * An annotation for classes and interfaces that should not be open sourced. - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.SOURCE) -@ClosedSource(reason = "Not required") -public @interface ClosedSource { - String reason(); -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index e4810667203..8fa89dea28e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -45,6 +45,7 @@ import java.util.Arrays; import java.util.Calendar; import java.util.Collections; +import java.util.Formatter; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; @@ -309,6 +310,18 @@ public static int constrainValue(int value, int min, int max) { return Math.max(min, Math.min(value, max)); } + /** + * Constrains a value to the specified bounds. + * + * @param value The value to constrain. + * @param min The lower bound. + * @param max The upper bound. + * @return The constrained value {@code Math.max(min, Math.min(value, max))}. + */ + public static long constrainValue(long value, long min, long max) { + return Math.max(min, Math.min(value, max)); + } + /** * Constrains a value to the specified bounds. * @@ -835,6 +848,27 @@ public static int inferContentType(String fileName) { } } + /** + * Returns the specified millisecond time formatted as a string. + * + * @param builder The builder that {@code formatter} will write to. + * @param formatter The formatter. + * @param timeMs The time to format as a string, in milliseconds. + * @return The time formatted as a string. + */ + public static String getStringForTime(StringBuilder builder, Formatter formatter, long timeMs) { + if (timeMs == C.TIME_UNSET) { + timeMs = 0; + } + long totalSeconds = (timeMs + 500) / 1000; + long seconds = totalSeconds % 60; + long minutes = (totalSeconds / 60) % 60; + long hours = totalSeconds / 3600; + builder.setLength(0); + return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() + : formatter.format("%02d:%02d", minutes, seconds).toString(); + } + /** * Maps a {@link C} {@code TRACK_TYPE_*} constant to the corresponding {@link C} * {@code DEFAULT_*_BUFFER_SIZE} constant. diff --git a/library/core/src/main/proguard-rules.txt b/library/core/src/main/proguard-rules.txt deleted file mode 100644 index 75f2d095bed..00000000000 --- a/library/core/src/main/proguard-rules.txt +++ /dev/null @@ -1,7 +0,0 @@ -# Accessed via reflection in SubtitleDecoderFactory.DEFAULT --keepclassmembers class com.google.android.exoplayer2.text.cea.Cea608Decoder { - public (java.lang.String, int); -} --keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder { - public (int); -} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index eec99521f1b..10f88c6460b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -648,7 +648,7 @@ public Period getPeriod(int periodIndex, Period period, boolean setIdentifiers) + Assertions.checkIndex(periodIndex, 0, manifest.getPeriodCount()) : null; return period.set(id, uid, 0, manifest.getPeriodDurationUs(periodIndex), C.msToUs(manifest.getPeriod(periodIndex).startMs - manifest.getPeriod(0).startMs) - - offsetInFirstPeriodUs); + - offsetInFirstPeriodUs, false); } @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java new file mode 100644 index 00000000000..85e30d207d9 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -0,0 +1,574 @@ +/* + * 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.ui; + +import android.annotation.TargetApi; +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.os.Bundle; +import android.support.annotation.Nullable; +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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.util.Formatter; +import java.util.Locale; + +/** + * A time bar that shows a current position, buffered position, duration and ad markers. + */ +public class DefaultTimeBar extends View implements TimeBar { + + /** + * The threshold in dps above the bar at which touch events trigger fine scrub mode. + */ + private static final int FINE_SCRUB_Y_THRESHOLD = -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 int DEFAULT_BAR_HEIGHT = 4; + private static final int DEFAULT_TOUCH_TARGET_HEIGHT = 24; + private static final int DEFAULT_PLAYED_COLOR = 0x33FFFFFF; + private static final int DEFAULT_BUFFERED_COLOR = 0xCCFFFFFF; + private static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; + private static final int DEFAULT_AD_MARKER_WIDTH = 4; + private static final int DEFAULT_SCRUBBER_ENABLED_SIZE = 12; + private static final int DEFAULT_SCRUBBER_DISABLED_SIZE = 0; + private static final int DEFAULT_SCRUBBER_DRAGGED_SIZE = 16; + private static final int OPAQUE_COLOR = 0xFF000000; + + private final Rect seekBounds; + private final Rect progressBar; + private final Rect bufferedBar; + private final Rect scrubberBar; + private final Paint progressPaint; + private final Paint bufferedPaint; + private final Paint scrubberPaint; + private final Paint adMarkerPaint; + private final int barHeight; + private final int touchTargetHeight; + 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 int scrubberSize; + private OnScrubListener listener; + private int keyCountIncrement; + private long keyTimeIncrement; + private int lastCoarseScrubXPosition; + private int[] locationOnScreen; + private Point touchPosition; + + private boolean scrubbing; + private long scrubPosition; + private long duration; + private long position; + private long bufferedPosition; + private int adBreakCount; + private long[] adBreakTimesMs; + + /** + * Creates a new time bar. + */ + public DefaultTimeBar(Context context, AttributeSet attrs) { + super(context, attrs); + seekBounds = new Rect(); + progressBar = new Rect(); + bufferedBar = new Rect(); + scrubberBar = new Rect(); + progressPaint = new Paint(); + bufferedPaint = new Paint(); + scrubberPaint = new Paint(); + adMarkerPaint = new Paint(); + + // Calculate the dimensions and paints for drawn elements. + Resources res = context.getResources(); + DisplayMetrics displayMetrics = res.getDisplayMetrics(); + fineScrubYThreshold = dpToPx(displayMetrics, FINE_SCRUB_Y_THRESHOLD); + int defaultBarHeight = dpToPx(displayMetrics, DEFAULT_BAR_HEIGHT); + int defaultTouchTargetHeight = dpToPx(displayMetrics, DEFAULT_TOUCH_TARGET_HEIGHT); + int defaultAdMarkerWidth = dpToPx(displayMetrics, DEFAULT_AD_MARKER_WIDTH); + int defaultScrubberEnabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_ENABLED_SIZE); + int defaultScrubberDisabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DISABLED_SIZE); + int defaultScrubberDraggedSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DRAGGED_SIZE); + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0, + 0); + try { + barHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_bar_height, + defaultBarHeight); + touchTargetHeight = a.getDimensionPixelSize(R.styleable.DefaultTimeBar_touch_target_height, + defaultTouchTargetHeight); + 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 bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color, + DEFAULT_BUFFERED_COLOR); + int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, + DEFAULT_AD_MARKER_COLOR); + progressPaint.setColor(playedColor); + scrubberPaint.setColor(OPAQUE_COLOR | playedColor); + bufferedPaint.setColor(bufferedColor); + adMarkerPaint.setColor(adMarkerColor); + } finally { + a.recycle(); + } + } else { + barHeight = defaultBarHeight; + touchTargetHeight = defaultTouchTargetHeight; + adMarkerWidth = defaultAdMarkerWidth; + scrubberEnabledSize = defaultScrubberEnabledSize; + scrubberDisabledSize = defaultScrubberDisabledSize; + scrubberDraggedSize = defaultScrubberDraggedSize; + scrubberPaint.setColor(OPAQUE_COLOR | DEFAULT_PLAYED_COLOR); + progressPaint.setColor(DEFAULT_PLAYED_COLOR); + bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); + adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); + } + formatBuilder = new StringBuilder(); + formatter = new Formatter(formatBuilder, Locale.getDefault()); + stopScrubbingRunnable = new Runnable() { + @Override + public void run() { + stopScrubbing(false); + } + }; + scrubberSize = scrubberEnabledSize; + scrubberPadding = + (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) + / 2; + duration = C.TIME_UNSET; + keyTimeIncrement = C.TIME_UNSET; + keyCountIncrement = DEFAULT_INCREMENT_COUNT; + setFocusable(true); + if (Util.SDK_INT >= 16 + && getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + public void setListener(OnScrubListener listener) { + this.listener = 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) { + this.position = position; + setContentDescription(getProgressText()); + } + + @Override + public void setBufferedPosition(long bufferedPosition) { + this.bufferedPosition = bufferedPosition; + } + + @Override + public void setDuration(long duration) { + this.duration = duration; + if (scrubbing && duration == C.TIME_UNSET) { + stopScrubbing(true); + } else { + updateScrubberState(); + } + } + + @Override + public void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount) { + Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null); + this.adBreakCount = adBreakCount; + this.adBreakTimesMs = adBreakTimesMs; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + updateScrubberState(); + if (scrubbing && !enabled) { + stopScrubbing(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)) { + startScrubbing(); + positionScrubber(x); + scrubPosition = 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); + } + scrubPosition = getScrubberPosition(); + if (listener != null) { + listener.onScrubMove(this, scrubPosition); + } + update(); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (scrubbing) { + stopScrubbing(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) { + removeCallbacks(stopScrubbingRunnable); + stopScrubbingRunnable.run(); + return true; + } + break; + default: + // Do nothing. + } + } + return super.onKeyDown(keyCode, event); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measureWidth = MeasureSpec.getSize(widthMeasureSpec); + int measureHeight = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(measureWidth, measureHeight); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + int barY = height - touchTargetHeight; + int seekLeft = getPaddingLeft(); + int seekRight = width - getPaddingRight(); + int progressY = barY + (touchTargetHeight - barHeight) / 2; + seekBounds.set(seekLeft, barY, seekRight, barY + touchTargetHeight); + progressBar.set(seekBounds.left + scrubberPadding, progressY, + seekBounds.right - scrubberPadding, progressY + barHeight); + update(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + } + + @TargetApi(14) + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SELECTED) { + event.getText().add(getProgressText()); + } + event.setClassName(DefaultTimeBar.class.getName()); + } + + @TargetApi(14) + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(DefaultTimeBar.class.getCanonicalName()); + 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 if (Util.SDK_INT >= 16) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } + + @TargetApi(16) + @Override + public boolean performAccessibilityAction(int action, Bundle args) { + if (super.performAccessibilityAction(action, args)) { + return true; + } + if (duration <= 0) { + return false; + } + if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { + if (scrubIncrementally(-getPositionIncrement())) { + stopScrubbing(false); + } + } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) { + if (scrubIncrementally(getPositionIncrement())) { + stopScrubbing(false); + } + } else { + return false; + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + return true; + } + + // Internal methods. + + private void startScrubbing() { + scrubbing = true; + updateScrubberState(); + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + if (listener != null) { + listener.onScrubStart(this); + } + } + + private void stopScrubbing(boolean canceled) { + scrubbing = false; + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(false); + } + updateScrubberState(); + invalidate(); + if (listener != null) { + listener.onScrubStop(this, getScrubberPosition(), canceled); + } + } + + private void updateScrubberState() { + scrubberSize = scrubbing ? scrubberDraggedSize + : (isEnabled() && duration >= 0 ? scrubberEnabledSize : scrubberDisabledSize); + } + + 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 = progressBar.left + bufferedPixelWidth; + int scrubberPixelPosition = + (int) ((progressBar.width() * newScrubberTime) / duration); + scrubberBar.right = progressBar.left + scrubberPixelPosition; + } 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) { + if (locationOnScreen == null) { + locationOnScreen = new int[2]; + touchPosition = new Point(); + } + getLocationOnScreen(locationOnScreen); + touchPosition.set( + ((int) motionEvent.getRawX()) - locationOnScreen[0], + ((int) motionEvent.getRawY()) - locationOnScreen[1]); + 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, progressPaint); + 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, progressPaint); + } + 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, scrubberPaint); + } + int adMarkerOffset = adMarkerWidth / 2; + for (int i = 0; i < adBreakCount; i++) { + long adBreakTimeMs = Util.constrainValue(adBreakTimesMs[i], 0, duration); + int markerPositionOffset = + (int) (progressBar.width() * adBreakTimeMs / duration) - adMarkerOffset; + int markerLeft = progressBar.left + Math.min(progressBar.width() - adMarkerWidth, + Math.max(0, markerPositionOffset)); + canvas.drawRect(markerLeft, barTop, markerLeft + adMarkerWidth, barBottom, adMarkerPaint); + } + } + + private void drawPlayhead(Canvas canvas) { + if (duration <= 0) { + return; + } + int playheadRadius = scrubberSize / 2; + int playheadCenter = Util.constrainValue(scrubberBar.right, scrubberBar.left, + progressBar.right); + canvas.drawCircle(playheadCenter, scrubberBar.centerY(), playheadRadius, scrubberPaint); + } + + 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; + } + + /** + * 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 scrubberPosition = getScrubberPosition(); + scrubPosition = Util.constrainValue(scrubberPosition + positionChange, 0, duration); + if (scrubPosition == scrubberPosition) { + return false; + } + if (!scrubbing) { + startScrubbing(); + } + if (listener != null) { + listener.onScrubMove(this, scrubPosition); + } + update(); + return true; + } + + private static int dpToPx(DisplayMetrics displayMetrics, int dps) { + return (int) (dps * displayMetrics.density + 0.5f); + } + +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 757252297de..a5c667635a5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -15,16 +15,17 @@ */ package com.google.android.exoplayer2.ui; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.SystemClock; import android.util.AttributeSet; +import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.widget.FrameLayout; -import android.widget.SeekBar; import android.widget.TextView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -34,6 +35,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; import java.util.Formatter; import java.util.Locale; @@ -125,9 +127,9 @@ *
  • Type: {@link TextView}
  • * * - *
  • {@code exo_progress} - Seek bar that's updated during playback and allows seeking. + *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. *
      - *
    • Type: {@link SeekBar}
    • + *
    • Type: {@link TimeBar}
    • *
    *
  • * @@ -144,6 +146,8 @@ */ public class PlaybackControlView extends FrameLayout { + private static final String TAG = "PlaybackControlView"; + /** * Listener to be notified about changes of the visibility of the UI control. */ @@ -191,7 +195,11 @@ public boolean dispatchSeek(ExoPlayer player, int windowIndex, long positionMs) public static final int DEFAULT_REWIND_MS = 5000; public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; - private static final int PROGRESS_BAR_MAX = 1000; + /** + * The maximum number of windows that can be shown in a multi-window time bar. + */ + public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; + private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; private final ComponentListener componentListener; @@ -203,21 +211,25 @@ public boolean dispatchSeek(ExoPlayer player, int windowIndex, long positionMs) private final View rewindButton; private final TextView durationView; private final TextView positionView; - private final SeekBar progressBar; + private final TimeBar timeBar; private final StringBuilder formatBuilder; private final Formatter formatter; - private final Timeline.Window currentWindow; + private final Timeline.Period period; + private final Timeline.Window window; private ExoPlayer player; private SeekDispatcher seekDispatcher; private VisibilityListener visibilityListener; private boolean isAttachedToWindow; - private boolean dragging; + private boolean showMultiWindowTimeBar; + private boolean multiWindowTimeBar; + private boolean scrubbing; private int rewindMs; private int fastForwardMs; private int showTimeoutMs; private long hideAtMs; + private long[] adBreakTimesMs; private final Runnable updateProgressAction = new Runnable() { @Override @@ -262,9 +274,11 @@ public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr a.recycle(); } } - currentWindow = new Timeline.Window(); + period = new Timeline.Period(); + window = new Timeline.Window(); formatBuilder = new StringBuilder(); formatter = new Formatter(formatBuilder, Locale.getDefault()); + adBreakTimesMs = new long[0]; componentListener = new ComponentListener(); seekDispatcher = DEFAULT_SEEK_DISPATCHER; @@ -273,10 +287,9 @@ public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr durationView = (TextView) findViewById(R.id.exo_duration); positionView = (TextView) findViewById(R.id.exo_position); - progressBar = (SeekBar) findViewById(R.id.exo_progress); - if (progressBar != null) { - progressBar.setOnSeekBarChangeListener(componentListener); - progressBar.setMax(PROGRESS_BAR_MAX); + timeBar = (TimeBar) findViewById(R.id.exo_progress); + if (timeBar != null) { + timeBar.setListener(componentListener); } playButton = findViewById(R.id.exo_play); if (playButton != null) { @@ -314,7 +327,7 @@ public ExoPlayer getPlayer() { /** * Sets the {@link ExoPlayer} to control. * - * @param player the {@code ExoPlayer} to control. + * @param player The {@code ExoPlayer} to control. */ public void setPlayer(ExoPlayer player) { if (this.player == player) { @@ -330,6 +343,18 @@ public void setPlayer(ExoPlayer player) { updateAll(); } + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. If the + * timeline has more than {@link #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will + * fall back to showing a single window. + * + * @param showMultiWindowTimeBar Whether the time bar should show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + this.showMultiWindowTimeBar = showMultiWindowTimeBar; + updateTimeBarMode(); + } + /** * Sets the {@link VisibilityListener}. * @@ -473,51 +498,122 @@ private void updateNavigation() { if (!isVisible() || !isAttachedToWindow) { return; } - Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; - boolean haveNonEmptyTimeline = currentTimeline != null && !currentTimeline.isEmpty(); + Timeline timeline = player != null ? player.getCurrentTimeline() : null; + boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); boolean isSeekable = false; boolean enablePrevious = false; boolean enableNext = false; if (haveNonEmptyTimeline) { - int currentWindowIndex = player.getCurrentWindowIndex(); - currentTimeline.getWindow(currentWindowIndex, currentWindow); - isSeekable = currentWindow.isSeekable; - enablePrevious = currentWindowIndex > 0 || isSeekable || !currentWindow.isDynamic; - enableNext = (currentWindowIndex < currentTimeline.getWindowCount() - 1) - || currentWindow.isDynamic; - } - setButtonEnabled(enablePrevious , previousButton); + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + isSeekable = window.isSeekable; + enablePrevious = windowIndex > 0 || isSeekable || !window.isDynamic; + enableNext = (windowIndex < timeline.getWindowCount() - 1) || window.isDynamic; + } + setButtonEnabled(enablePrevious, previousButton); setButtonEnabled(enableNext, nextButton); setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); - if (progressBar != null) { - progressBar.setEnabled(isSeekable); + if (timeBar != null) { + timeBar.setEnabled(isSeekable); } } + private void updateTimeBarMode() { + if (player == null) { + return; + } + if (showMultiWindowTimeBar) { + if (player.getCurrentTimeline().getWindowCount() <= MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { + multiWindowTimeBar = true; + return; + } + Log.w(TAG, "Too many windows for multi-window time bar. Falling back to showing one window."); + } + multiWindowTimeBar = false; + } + private void updateProgress() { if (!isVisible() || !isAttachedToWindow) { return; } - long duration = player == null ? 0 : player.getDuration(); - long position = player == null ? 0 : player.getCurrentPosition(); + + long position = 0; + long bufferedPosition = 0; + long duration = 0; + if (player != null) { + if (multiWindowTimeBar) { + Timeline timeline = player.getCurrentTimeline(); + int windowCount = timeline.getWindowCount(); + int periodIndex = player.getCurrentPeriodIndex(); + long positionUs = 0; + long bufferedPositionUs = 0; + long durationUs = 0; + boolean isInAdBreak = false; + boolean isPlayingAd = false; + int adBreakCount = 0; + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window); + for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { + if (timeline.getPeriod(j, period).isAd) { + isPlayingAd |= j == periodIndex; + if (!isInAdBreak) { + isInAdBreak = true; + if (adBreakCount == adBreakTimesMs.length) { + adBreakTimesMs = Arrays.copyOf(adBreakTimesMs, + adBreakTimesMs.length == 0 ? 1 : adBreakTimesMs.length * 2); + } + adBreakTimesMs[adBreakCount++] = C.usToMs(durationUs); + } + } else { + isInAdBreak = false; + long periodDurationUs = period.getDurationUs(); + if (periodDurationUs == C.TIME_UNSET) { + durationUs = C.TIME_UNSET; + break; + } + long periodDurationInWindowUs = periodDurationUs; + if (j == window.firstPeriodIndex) { + periodDurationInWindowUs -= window.positionInFirstPeriodUs; + } + if (i < periodIndex) { + positionUs += periodDurationInWindowUs; + bufferedPositionUs += periodDurationInWindowUs; + } + durationUs += periodDurationInWindowUs; + } + } + } + position = C.usToMs(positionUs); + bufferedPosition = C.usToMs(bufferedPositionUs); + duration = C.usToMs(durationUs); + if (!isPlayingAd) { + position += player.getCurrentPosition(); + bufferedPosition += player.getBufferedPosition(); + } + if (timeBar != null) { + timeBar.setAdBreakTimesMs(adBreakTimesMs, adBreakCount); + } + } else { + position = player.getCurrentPosition(); + bufferedPosition = player.getBufferedPosition(); + duration = player.getDuration(); + } + } if (durationView != null) { - durationView.setText(stringForTime(duration)); + durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration)); } - if (positionView != null && !dragging) { - positionView.setText(stringForTime(position)); + if (positionView != null && !scrubbing) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); } - - if (progressBar != null) { - if (!dragging) { - progressBar.setProgress(progressBarValue(position)); - } - long bufferedPosition = player == null ? 0 : player.getBufferedPosition(); - progressBar.setSecondaryProgress(progressBarValue(bufferedPosition)); - // Remove scheduled updates. + if (timeBar != null) { + timeBar.setPosition(position); + timeBar.setBufferedPosition(bufferedPosition); + timeBar.setDuration(duration); } + + // Cancel any pending updates and schedule a new one if necessary. removeCallbacks(updateProgressAction); - // Schedule an update if necessary. int playbackState = player == null ? ExoPlayer.STATE_IDLE : player.getPlaybackState(); if (playbackState != ExoPlayer.STATE_IDLE && playbackState != ExoPlayer.STATE_ENDED) { long delayMs; @@ -560,55 +656,31 @@ private void setViewAlphaV11(View view, float alpha) { view.setAlpha(alpha); } - private String stringForTime(long timeMs) { - if (timeMs == C.TIME_UNSET) { - timeMs = 0; - } - long totalSeconds = (timeMs + 500) / 1000; - long seconds = totalSeconds % 60; - long minutes = (totalSeconds / 60) % 60; - long hours = totalSeconds / 3600; - formatBuilder.setLength(0); - return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() - : formatter.format("%02d:%02d", minutes, seconds).toString(); - } - - private int progressBarValue(long position) { - long duration = player == null ? C.TIME_UNSET : player.getDuration(); - return duration == C.TIME_UNSET || duration == 0 ? 0 - : (int) ((position * PROGRESS_BAR_MAX) / duration); - } - - private long positionValue(int progress) { - long duration = player == null ? C.TIME_UNSET : player.getDuration(); - return duration == C.TIME_UNSET ? 0 : ((duration * progress) / PROGRESS_BAR_MAX); - } - private void previous() { - Timeline currentTimeline = player.getCurrentTimeline(); - if (currentTimeline.isEmpty()) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { return; } - int currentWindowIndex = player.getCurrentWindowIndex(); - currentTimeline.getWindow(currentWindowIndex, currentWindow); - if (currentWindowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (currentWindow.isDynamic && !currentWindow.isSeekable))) { - seekTo(currentWindowIndex - 1, C.TIME_UNSET); + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + if (windowIndex > 0 && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || (window.isDynamic && !window.isSeekable))) { + seekTo(windowIndex - 1, C.TIME_UNSET); } else { seekTo(0); } } private void next() { - Timeline currentTimeline = player.getCurrentTimeline(); - if (currentTimeline.isEmpty()) { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { return; } - int currentWindowIndex = player.getCurrentWindowIndex(); - if (currentWindowIndex < currentTimeline.getWindowCount() - 1) { - seekTo(currentWindowIndex + 1, C.TIME_UNSET); - } else if (currentTimeline.getWindow(currentWindowIndex, currentWindow, false).isDynamic) { - seekTo(currentWindowIndex, C.TIME_UNSET); + int windowIndex = player.getCurrentWindowIndex(); + if (windowIndex < timeline.getWindowCount() - 1) { + seekTo(windowIndex + 1, C.TIME_UNSET); + } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { + seekTo(windowIndex, C.TIME_UNSET); } } @@ -714,6 +786,7 @@ public boolean dispatchMediaKeyEvent(KeyEvent event) { return true; } + @SuppressLint("InlinedApi") private static boolean isHandledMediaKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND @@ -724,33 +797,52 @@ private static boolean isHandledMediaKey(int keyCode) { || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; } - private final class ComponentListener implements ExoPlayer.EventListener, - SeekBar.OnSeekBarChangeListener, OnClickListener { + private final class ComponentListener implements ExoPlayer.EventListener, TimeBar.OnScrubListener, + OnClickListener { @Override - public void onStartTrackingTouch(SeekBar seekBar) { + public void onScrubStart(TimeBar timeBar) { removeCallbacks(hideAction); - dragging = true; + scrubbing = true; } @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - if (fromUser) { - long position = positionValue(progress); - if (positionView != null) { - positionView.setText(stringForTime(position)); - } - if (player != null && !dragging) { - seekTo(position); - } + public void onScrubMove(TimeBar timeBar, long position) { + if (positionView != null) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); } } @Override - public void onStopTrackingTouch(SeekBar seekBar) { - dragging = false; - if (player != null) { - seekTo(positionValue(seekBar.getProgress())); + public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { + scrubbing = false; + if (!canceled && player != null) { + if (showMultiWindowTimeBar) { + Timeline timeline = player.getCurrentTimeline(); + int windowCount = timeline.getWindowCount(); + long remainingMs = position; + for (int i = 0; i < windowCount; i++) { + timeline.getWindow(i, window); + if (!timeline.getPeriod(window.firstPeriodIndex, period).isAd) { + long windowDurationMs = window.getDurationMs(); + if (windowDurationMs == C.TIME_UNSET) { + break; + } + if (i == windowCount - 1 && remainingMs >= windowDurationMs) { + // Seeking past the end of the last window should seek to the end of the timeline. + seekTo(i, windowDurationMs); + break; + } + if (remainingMs < windowDurationMs) { + seekTo(i, remainingMs); + break; + } + remainingMs -= windowDurationMs; + } + } + } else { + seekTo(position); + } } hideAfterTimeout(); } @@ -775,6 +867,7 @@ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { updateNavigation(); + updateTimeBarMode(); updateProgress(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 9d221be60a1..c2ad8ddeb53 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -527,6 +527,16 @@ public void setFastForwardIncrementMs(int fastForwardMs) { controller.setFastForwardIncrementMs(fastForwardMs); } + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. + * + * @param showMultiWindowTimeBar Whether to show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + Assertions.checkState(controller != null); + controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); + } + /** * Gets the view onto which video is rendered. This is either a {@link SurfaceView} (default) * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java new file mode 100644 index 00000000000..aeb8e0255ee --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TimeBar.java @@ -0,0 +1,120 @@ +/* + * 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.ui; + +import android.support.annotation.Nullable; +import android.view.View; + +/** + * Interface for time bar views that can display a playback position, buffered position, duration + * and ad markers, and that have a listener for scrubbing (seeking) events. + */ +public interface TimeBar { + + /** + * @see View#isEnabled() + */ + void setEnabled(boolean enabled); + + /** + * Sets the listener for the scrubbing events. + * + * @param listener The listener for scrubbing events. + */ + void setListener(OnScrubListener listener); + + /** + * Sets the position increment for key presses and accessibility actions, in milliseconds. + *

    + * Clears any increment specified in a preceding call to {@link #setKeyCountIncrement(int)}. + * + * @param time The time increment, in milliseconds. + */ + void setKeyTimeIncrement(long time); + + /** + * Sets the position increment for key presses and accessibility actions, as a number of + * increments that divide the duration of the media. For example, passing 20 will cause key + * presses to increment/decrement the position by 1/20th of the duration (if known). + *

    + * Clears any increment specified in a preceding call to {@link #setKeyTimeIncrement(long)}. + * + * @param count The number of increments that divide the duration of the media. + */ + void setKeyCountIncrement(int count); + + /** + * Sets the current position. + * + * @param position The current position to show, in milliseconds. + */ + void setPosition(long position); + + /** + * Sets the buffered position. + * + * @param bufferedPosition The current buffered position to show, in milliseconds. + */ + void setBufferedPosition(long bufferedPosition); + + /** + * Sets the duration. + * + * @param duration The duration to show, in milliseconds. + */ + void setDuration(long duration); + + /** + * Sets the times of ad breaks. + * + * @param adBreakTimesMs An array where the first {@code adBreakCount} elements are the times of + * ad breaks in milliseconds. May be {@code null} if there are no ad breaks. + * @param adBreakCount The number of ad breaks. + */ + void setAdBreakTimesMs(@Nullable long[] adBreakTimesMs, int adBreakCount); + + /** + * Listener for scrubbing events. + */ + interface OnScrubListener { + + /** + * Called when the user starts moving the scrubber. + * + * @param timeBar The time bar. + */ + void onScrubStart(TimeBar timeBar); + + /** + * Called when the user moves the scrubber. + * + * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. + */ + void onScrubMove(TimeBar timeBar, long position); + + /** + * Called when the user stops moving the scrubber. + * + * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. + * @param canceled Whether scrubbing was canceled. + */ + void onScrubStop(TimeBar timeBar, long position, boolean canceled); + + } + +} diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index f8ef5a6fdd7..665852936b8 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -65,12 +65,11 @@ android:includeFontPadding="false" android:textColor="#FFBEBEBE"/> - + android:layout_height="24dp"/> + + + + + + + + + + + +