UI component improvements

- Make sure no events are posted on PlaybackControlView
  if it's not attached to a window. This can cause leaks.
  The target hide time is recorded if necessary and
  processed when the view is re-attached.
- Deduplicated PlaybackControlView.VisibilityListener
  invocations.
- Fixed timeouts to be more intuitive (I think).
- Fixed initial visibility of PlaybackControlView when
  used as part of SimpleExoPlayerView.
- Made some more attributes configurable from layout xml.

Issue: #1908
Issue: #1919
Issue: #1923

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=135679988
This commit is contained in:
olly 2016-07-28 08:48:37 +01:00 committed by Oliver Woodman
parent 97020e0bab
commit 91f8328a5f
3 changed files with 199 additions and 76 deletions

View File

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -52,7 +54,7 @@ public class PlaybackControlView extends FrameLayout {
public static final int DEFAULT_FAST_FORWARD_MS = 15000; public static final int DEFAULT_FAST_FORWARD_MS = 15000;
public static final int DEFAULT_REWIND_MS = 5000; public static final int DEFAULT_REWIND_MS = 5000;
public static final int DEFAULT_SHOW_DURATION_MS = 5000; public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000;
private static final int PROGRESS_BAR_MAX = 1000; private static final int PROGRESS_BAR_MAX = 1000;
private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000;
@ -74,9 +76,10 @@ public class PlaybackControlView extends FrameLayout {
private VisibilityListener visibilityListener; private VisibilityListener visibilityListener;
private boolean dragging; private boolean dragging;
private int rewindMs = DEFAULT_REWIND_MS; private int rewindMs;
private int fastForwardMs = DEFAULT_FAST_FORWARD_MS; private int fastForwardMs;
private int showDurationMs = DEFAULT_SHOW_DURATION_MS; private int showTimeoutMs;
private long hideAtMs;
private final Runnable updateProgressAction = new Runnable() { private final Runnable updateProgressAction = new Runnable() {
@Override @Override
@ -103,6 +106,22 @@ public class PlaybackControlView extends FrameLayout {
public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); super(context, attrs, defStyleAttr);
rewindMs = DEFAULT_REWIND_MS;
fastForwardMs = DEFAULT_FAST_FORWARD_MS;
showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS;
if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.PlaybackControlView, 0, 0);
try {
rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs);
fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment,
fastForwardMs);
showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs);
} finally {
a.recycle();
}
}
currentWindow = new Timeline.Window(); currentWindow = new Timeline.Window();
formatBuilder = new StringBuilder(); formatBuilder = new StringBuilder();
formatter = new Formatter(formatBuilder, Locale.getDefault()); formatter = new Formatter(formatBuilder, Locale.getDefault());
@ -124,7 +143,6 @@ public class PlaybackControlView extends FrameLayout {
rewindButton.setOnClickListener(componentListener); rewindButton.setOnClickListener(componentListener);
fastForwardButton = findViewById(R.id.ffwd); fastForwardButton = findViewById(R.id.ffwd);
fastForwardButton.setOnClickListener(componentListener); fastForwardButton.setOnClickListener(componentListener);
updateAll();
} }
/** /**
@ -169,6 +187,7 @@ public class PlaybackControlView extends FrameLayout {
*/ */
public void setRewindIncrementMs(int rewindMs) { public void setRewindIncrementMs(int rewindMs) {
this.rewindMs = rewindMs; this.rewindMs = rewindMs;
updateNavigation();
} }
/** /**
@ -178,51 +197,60 @@ public class PlaybackControlView extends FrameLayout {
*/ */
public void setFastForwardIncrementMs(int fastForwardMs) { public void setFastForwardIncrementMs(int fastForwardMs) {
this.fastForwardMs = fastForwardMs; this.fastForwardMs = fastForwardMs;
updateNavigation();
} }
/** /**
* Sets the duration to show the playback control in milliseconds. * Returns the playback controls timeout. The playback controls are automatically hidden after
* this duration of time has elapsed without user input.
* *
* @param showDurationMs The duration in milliseconds. * @return The duration in milliseconds. A non-positive value indicates that the controls will
* remain visible indefinitely.
*/ */
public void setShowDurationMs(int showDurationMs) { public int getShowTimeoutMs() {
this.showDurationMs = showDurationMs; return showTimeoutMs;
} }
/** /**
* Shows the controller for the duration last passed to {@link #setShowDurationMs(int)}, or for * Sets the playback controls timeout. The playback controls are automatically hidden after this
* {@link #DEFAULT_SHOW_DURATION_MS} if {@link #setShowDurationMs(int)} has not been called. * duration of time has elapsed without user input.
*
* @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls
* to remain visible indefinitely.
*/
public void setShowTimeoutMs(int showTimeoutMs) {
this.showTimeoutMs = showTimeoutMs;
}
/**
* Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
* be automatically hidden after this duration of time has elapsed without user input.
*/ */
public void show() { public void show() {
show(showDurationMs); if (!isVisible()) {
} setVisibility(VISIBLE);
if (visibilityListener != null) {
/** visibilityListener.onVisibilityChange(getVisibility());
* Shows the controller for the {@code durationMs}. If {@code durationMs} is 0 the controller is }
* shown until {@link #hide()} is called. updateAll();
*
* @param durationMs The duration in milliseconds.
*/
public void show(int durationMs) {
setVisibility(VISIBLE);
if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
} }
updateAll(); // Call hideAfterTimeout even if already visible to reset the timeout.
showDurationMs = durationMs; hideAfterTimeout();
hideDeferred();
} }
/** /**
* Hides the controller. * Hides the controller.
*/ */
public void hide() { public void hide() {
setVisibility(GONE); if (isVisible()) {
if (visibilityListener != null) { setVisibility(GONE);
visibilityListener.onVisibilityChange(getVisibility()); if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
hideAtMs = C.TIME_UNSET;
} }
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
} }
/** /**
@ -232,10 +260,15 @@ public class PlaybackControlView extends FrameLayout {
return getVisibility() == VISIBLE; return getVisibility() == VISIBLE;
} }
private void hideDeferred() { private void hideAfterTimeout() {
removeCallbacks(hideAction); removeCallbacks(hideAction);
if (showDurationMs > 0) { if (showTimeoutMs > 0) {
postDelayed(hideAction, showDurationMs); hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs;
if (isAttachedToWindow()) {
postDelayed(hideAction, showTimeoutMs);
}
} else {
hideAtMs = C.TIME_UNSET;
} }
} }
@ -246,7 +279,7 @@ public class PlaybackControlView extends FrameLayout {
} }
private void updatePlayPauseButton() { private void updatePlayPauseButton() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
boolean playing = player != null && player.getPlayWhenReady(); boolean playing = player != null && player.getPlayWhenReady();
@ -258,7 +291,7 @@ public class PlaybackControlView extends FrameLayout {
} }
private void updateNavigation() { private void updateNavigation() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null;
@ -276,13 +309,13 @@ public class PlaybackControlView extends FrameLayout {
} }
setButtonEnabled(enablePrevious , previousButton); setButtonEnabled(enablePrevious , previousButton);
setButtonEnabled(enableNext, nextButton); setButtonEnabled(enableNext, nextButton);
setButtonEnabled(isSeekable, fastForwardButton); setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton);
setButtonEnabled(isSeekable, rewindButton); setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton);
progressBar.setEnabled(isSeekable); progressBar.setEnabled(isSeekable);
} }
private void updateProgress() { private void updateProgress() {
if (!isVisible()) { if (!isVisible() || !isAttachedToWindow()) {
return; return;
} }
long duration = player == null ? 0 : player.getDuration(); long duration = player == null ? 0 : player.getDuration();
@ -377,13 +410,40 @@ public class PlaybackControlView extends FrameLayout {
} }
private void rewind() { private void rewind() {
if (rewindMs <= 0) {
return;
}
player.seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); player.seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0));
} }
private void fastForward() { private void fastForward() {
if (fastForwardMs <= 0) {
return;
}
player.seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration())); player.seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration()));
} }
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (hideAtMs != C.TIME_UNSET) {
long delayMs = hideAtMs - SystemClock.uptimeMillis();
if (delayMs <= 0) {
hide();
} else {
postDelayed(hideAction, delayMs);
}
}
updateAll();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
}
@Override @Override
public boolean dispatchKeyEvent(KeyEvent event) { public boolean dispatchKeyEvent(KeyEvent event) {
if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) { if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) {
@ -440,7 +500,7 @@ public class PlaybackControlView extends FrameLayout {
public void onStopTrackingTouch(SeekBar seekBar) { public void onStopTrackingTouch(SeekBar seekBar) {
dragging = false; dragging = false;
player.seekTo(positionValue(seekBar.getProgress())); player.seekTo(positionValue(seekBar.getProgress()));
hideDeferred(); hideAfterTimeout();
} }
@Override @Override
@ -485,7 +545,7 @@ public class PlaybackControlView extends FrameLayout {
} else if (playButton == view) { } else if (playButton == view) {
player.setPlayWhenReady(!player.getPlayWhenReady()); player.setPlayWhenReady(!player.getPlayWhenReady());
} }
hideDeferred(); hideAfterTimeout();
} }
} }

View File

@ -48,8 +48,10 @@ public final class SimpleExoPlayerView extends FrameLayout {
private final AspectRatioFrameLayout layout; private final AspectRatioFrameLayout layout;
private final PlaybackControlView controller; private final PlaybackControlView controller;
private final ComponentListener componentListener; private final ComponentListener componentListener;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private boolean useController = true; private boolean useController = true;
private int controllerShowTimeoutMs;
public SimpleExoPlayerView(Context context) { public SimpleExoPlayerView(Context context) {
this(context, null); this(context, null);
@ -64,6 +66,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
boolean useTextureView = false; boolean useTextureView = false;
int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT;
int rewindMs = PlaybackControlView.DEFAULT_REWIND_MS;
int fastForwardMs = PlaybackControlView.DEFAULT_FAST_FORWARD_MS;
int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS;
if (attrs != null) { if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.SimpleExoPlayerView, 0, 0); R.styleable.SimpleExoPlayerView, 0, 0);
@ -73,6 +78,11 @@ public final class SimpleExoPlayerView extends FrameLayout {
useTextureView); useTextureView);
resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode,
AspectRatioFrameLayout.RESIZE_MODE_FIT); AspectRatioFrameLayout.RESIZE_MODE_FIT);
rewindMs = a.getInt(R.styleable.SimpleExoPlayerView_rewind_increment, rewindMs);
fastForwardMs = a.getInt(R.styleable.SimpleExoPlayerView_fastforward_increment,
fastForwardMs);
controllerShowTimeoutMs = a.getInt(R.styleable.SimpleExoPlayerView_show_timeout,
controllerShowTimeoutMs);
} finally { } finally {
a.recycle(); a.recycle();
} }
@ -82,12 +92,17 @@ public final class SimpleExoPlayerView extends FrameLayout {
componentListener = new ComponentListener(); componentListener = new ComponentListener();
layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame); layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame);
layout.setResizeMode(resizeMode); layout.setResizeMode(resizeMode);
controller = (PlaybackControlView) findViewById(R.id.control);
shutterView = findViewById(R.id.shutter); shutterView = findViewById(R.id.shutter);
subtitleLayout = (SubtitleView) findViewById(R.id.subtitles); subtitleLayout = (SubtitleView) findViewById(R.id.subtitles);
subtitleLayout.setUserDefaultStyle(); subtitleLayout.setUserDefaultStyle();
subtitleLayout.setUserDefaultTextSize(); subtitleLayout.setUserDefaultTextSize();
controller = (PlaybackControlView) findViewById(R.id.control);
controller.hide();
controller.setRewindIncrementMs(rewindMs);
controller.setFastForwardIncrementMs(fastForwardMs);
this.controllerShowTimeoutMs = controllerShowTimeoutMs;
View view = useTextureView ? new TextureView(context) : new SurfaceView(context); View view = useTextureView ? new TextureView(context) : new SurfaceView(context);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@ -122,6 +137,9 @@ public final class SimpleExoPlayerView extends FrameLayout {
this.player.setVideoSurface(null); this.player.setVideoSurface(null);
} }
this.player = player; this.player = player;
if (useController) {
controller.setPlayer(player);
}
if (player != null) { if (player != null) {
if (surfaceView instanceof TextureView) { if (surfaceView instanceof TextureView) {
player.setVideoTextureView((TextureView) surfaceView); player.setVideoTextureView((TextureView) surfaceView);
@ -131,20 +149,36 @@ public final class SimpleExoPlayerView extends FrameLayout {
player.setVideoListener(componentListener); player.setVideoListener(componentListener);
player.addListener(componentListener); player.addListener(componentListener);
player.setTextOutput(componentListener); player.setTextOutput(componentListener);
maybeShowController(false);
} else { } else {
shutterView.setVisibility(VISIBLE); shutterView.setVisibility(VISIBLE);
} controller.hide();
if (useController) {
controller.setPlayer(player);
} }
} }
/** /**
* Set the {@code useController} flag which indicates whether the playback control view should * Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT},
* be used or not. If set to {@code false} the controller is never visible and is disconnected * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or
* from the player. * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}.
* *
* @param useController If {@code false} the playback control is never used. * @param resizeMode The resize mode.
*/
public void setResizeMode(int resizeMode) {
layout.setResizeMode(resizeMode);
}
/**
* Returns whether the playback controls are enabled.
*/
public boolean getUseController() {
return useController;
}
/**
* Sets whether playback controls are enabled. If set to {@code false} the playback controls are
* never visible and are disconnected from the player.
*
* @param useController Whether playback controls should be enabled.
*/ */
public void setUseController(boolean useController) { public void setUseController(boolean useController) {
if (this.useController == useController) { if (this.useController == useController) {
@ -160,14 +194,26 @@ public final class SimpleExoPlayerView extends FrameLayout {
} }
/** /**
* Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT}, * Returns the playback controls timeout. The playback controls are automatically hidden after
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or * this duration of time has elapsed without user input and with playback or buffering in
* {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}. * progress.
* *
* @param resizeMode The resize mode. * @return The timeout in milliseconds. A non-positive value will cause the controller to remain
* visible indefinitely.
*/ */
public void setResizeMode(int resizeMode) { public int getControllerShowTimeoutMs() {
layout.setResizeMode(resizeMode); return controllerShowTimeoutMs;
}
/**
* Sets the playback controls timeout. The playback controls are automatically hidden after this
* duration of time has elapsed without user input and with playback or buffering in progress.
*
* @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause
* the controller to remain visible indefinitely.
*/
public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) {
this.controllerShowTimeoutMs = controllerShowTimeoutMs;
} }
/** /**
@ -197,15 +243,6 @@ public final class SimpleExoPlayerView extends FrameLayout {
controller.setFastForwardIncrementMs(fastForwardMs); controller.setFastForwardIncrementMs(fastForwardMs);
} }
/**
* Sets the duration to show the playback control in milliseconds.
*
* @param showDurationMs The duration in milliseconds.
*/
public void setControlShowDurationMs(int showDurationMs) {
controller.setShowDurationMs(showDurationMs);
}
/** /**
* Get the view onto which video is rendered. This is either a {@link SurfaceView} (default) * Get 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. * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true.
@ -218,21 +255,23 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public boolean onTouchEvent(MotionEvent ev) { public boolean onTouchEvent(MotionEvent ev) {
if (useController && ev.getActionMasked() == MotionEvent.ACTION_DOWN) { if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) {
if (controller.isVisible()) { return false;
controller.hide(); }
} else { if (controller.isVisible()) {
controller.show(); controller.hide();
} } else {
maybeShowController(true);
} }
return true; return true;
} }
@Override @Override
public boolean onTrackballEvent(MotionEvent ev) { public boolean onTrackballEvent(MotionEvent ev) {
if (!useController) { if (!useController || player == null) {
return false; return false;
} }
controller.show(); maybeShowController(true);
return true; return true;
} }
@ -241,6 +280,20 @@ public final class SimpleExoPlayerView extends FrameLayout {
return useController ? controller.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); return useController ? controller.dispatchKeyEvent(event) : super.dispatchKeyEvent(event);
} }
private void maybeShowController(boolean isForced) {
if (!useController || player == null) {
return;
}
int playbackState = player.getPlaybackState();
boolean showIndefinitely = playbackState == ExoPlayer.STATE_IDLE
|| playbackState == ExoPlayer.STATE_ENDED || !player.getPlayWhenReady();
boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0;
controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs);
if (isForced || showIndefinitely || wasShowingIndefinitely) {
controller.show();
}
}
private final class ComponentListener implements SimpleExoPlayer.VideoListener, private final class ComponentListener implements SimpleExoPlayer.VideoListener,
TextRenderer.Output, ExoPlayer.EventListener { TextRenderer.Output, ExoPlayer.EventListener {
@ -278,9 +331,7 @@ public final class SimpleExoPlayerView extends FrameLayout {
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (useController && playbackState == ExoPlayer.STATE_ENDED) { maybeShowController(false);
controller.show(0);
}
} }
@Override @Override

View File

@ -20,10 +20,16 @@
<enum name="fixed_width" value="1"/> <enum name="fixed_width" value="1"/>
<enum name="fixed_height" value="2"/> <enum name="fixed_height" value="2"/>
</attr> </attr>
<attr name="show_timeout" format="integer"/>
<attr name="rewind_increment" format="integer"/>
<attr name="fastforward_increment" format="integer"/>
<declare-styleable name="SimpleExoPlayerView"> <declare-styleable name="SimpleExoPlayerView">
<attr name="use_controller" format="boolean"/> <attr name="use_controller" format="boolean"/>
<attr name="use_texture_view" format="boolean"/> <attr name="use_texture_view" format="boolean"/>
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
<attr name="resize_mode"/> <attr name="resize_mode"/>
</declare-styleable> </declare-styleable>
@ -31,4 +37,10 @@
<attr name="resize_mode"/> <attr name="resize_mode"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="PlaybackControlView">
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
</declare-styleable>
</resources> </resources>