diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 13c5d14df0f..3823f1760ec 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.ui; import android.content.Context; +import android.content.res.TypedArray; +import android.os.SystemClock; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -52,7 +54,7 @@ public interface VisibilityListener { public static final int DEFAULT_FAST_FORWARD_MS = 15000; 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 long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; @@ -74,9 +76,10 @@ public interface VisibilityListener { private VisibilityListener visibilityListener; private boolean dragging; - private int rewindMs = DEFAULT_REWIND_MS; - private int fastForwardMs = DEFAULT_FAST_FORWARD_MS; - private int showDurationMs = DEFAULT_SHOW_DURATION_MS; + private int rewindMs; + private int fastForwardMs; + private int showTimeoutMs; + private long hideAtMs; private final Runnable updateProgressAction = new Runnable() { @Override @@ -103,6 +106,22 @@ public PlaybackControlView(Context context, AttributeSet attrs) { public PlaybackControlView(Context context, AttributeSet attrs, int 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(); formatBuilder = new StringBuilder(); formatter = new Formatter(formatBuilder, Locale.getDefault()); @@ -124,7 +143,6 @@ public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr rewindButton.setOnClickListener(componentListener); fastForwardButton = findViewById(R.id.ffwd); fastForwardButton.setOnClickListener(componentListener); - updateAll(); } /** @@ -169,6 +187,7 @@ public void setVisibilityListener(VisibilityListener listener) { */ public void setRewindIncrementMs(int rewindMs) { this.rewindMs = rewindMs; + updateNavigation(); } /** @@ -178,51 +197,60 @@ public void setRewindIncrementMs(int rewindMs) { */ public void setFastForwardIncrementMs(int 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) { - this.showDurationMs = showDurationMs; + public int getShowTimeoutMs() { + return showTimeoutMs; } /** - * Shows the controller for the duration last passed to {@link #setShowDurationMs(int)}, or for - * {@link #DEFAULT_SHOW_DURATION_MS} if {@link #setShowDurationMs(int)} has not been called. + * Sets the playback controls timeout. The playback controls are automatically hidden after this + * 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 show() { - show(showDurationMs); + public void setShowTimeoutMs(int showTimeoutMs) { + this.showTimeoutMs = showTimeoutMs; } /** - * Shows the controller for the {@code durationMs}. If {@code durationMs} is 0 the controller is - * shown until {@link #hide()} is called. - * - * @param durationMs The duration in milliseconds. + * 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(int durationMs) { - setVisibility(VISIBLE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); + public void show() { + if (!isVisible()) { + setVisibility(VISIBLE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + updateAll(); } - updateAll(); - showDurationMs = durationMs; - hideDeferred(); + // Call hideAfterTimeout even if already visible to reset the timeout. + hideAfterTimeout(); } /** * Hides the controller. */ public void hide() { - setVisibility(GONE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); + if (isVisible()) { + setVisibility(GONE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + removeCallbacks(updateProgressAction); + removeCallbacks(hideAction); + hideAtMs = C.TIME_UNSET; } - removeCallbacks(updateProgressAction); - removeCallbacks(hideAction); } /** @@ -232,10 +260,15 @@ public boolean isVisible() { return getVisibility() == VISIBLE; } - private void hideDeferred() { + private void hideAfterTimeout() { removeCallbacks(hideAction); - if (showDurationMs > 0) { - postDelayed(hideAction, showDurationMs); + if (showTimeoutMs > 0) { + hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; + if (isAttachedToWindow()) { + postDelayed(hideAction, showTimeoutMs); + } + } else { + hideAtMs = C.TIME_UNSET; } } @@ -246,7 +279,7 @@ private void updateAll() { } private void updatePlayPauseButton() { - if (!isVisible()) { + if (!isVisible() || !isAttachedToWindow()) { return; } boolean playing = player != null && player.getPlayWhenReady(); @@ -258,7 +291,7 @@ private void updatePlayPauseButton() { } private void updateNavigation() { - if (!isVisible()) { + if (!isVisible() || !isAttachedToWindow()) { return; } Timeline currentTimeline = player != null ? player.getCurrentTimeline() : null; @@ -276,13 +309,13 @@ private void updateNavigation() { } setButtonEnabled(enablePrevious , previousButton); setButtonEnabled(enableNext, nextButton); - setButtonEnabled(isSeekable, fastForwardButton); - setButtonEnabled(isSeekable, rewindButton); + setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); + setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); progressBar.setEnabled(isSeekable); } private void updateProgress() { - if (!isVisible()) { + if (!isVisible() || !isAttachedToWindow()) { return; } long duration = player == null ? 0 : player.getDuration(); @@ -377,13 +410,40 @@ private void next() { } private void rewind() { + if (rewindMs <= 0) { + return; + } player.seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); } private void fastForward() { + if (fastForwardMs <= 0) { + return; + } 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 public boolean dispatchKeyEvent(KeyEvent event) { if (player == null || event.getAction() != KeyEvent.ACTION_DOWN) { @@ -440,7 +500,7 @@ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { public void onStopTrackingTouch(SeekBar seekBar) { dragging = false; player.seekTo(positionValue(seekBar.getProgress())); - hideDeferred(); + hideAfterTimeout(); } @Override @@ -485,7 +545,7 @@ public void onClick(View view) { } else if (playButton == view) { player.setPlayWhenReady(!player.getPlayWhenReady()); } - hideDeferred(); + hideAfterTimeout(); } } diff --git a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index cd0acb77fa0..51955ccef32 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -48,8 +48,10 @@ public final class SimpleExoPlayerView extends FrameLayout { private final AspectRatioFrameLayout layout; private final PlaybackControlView controller; private final ComponentListener componentListener; + private SimpleExoPlayer player; private boolean useController = true; + private int controllerShowTimeoutMs; public SimpleExoPlayerView(Context context) { this(context, null); @@ -64,6 +66,9 @@ public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr boolean useTextureView = false; 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) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -73,6 +78,11 @@ public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr useTextureView); resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, 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 { a.recycle(); } @@ -82,12 +92,17 @@ public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr componentListener = new ComponentListener(); layout = (AspectRatioFrameLayout) findViewById(R.id.video_frame); layout.setResizeMode(resizeMode); - controller = (PlaybackControlView) findViewById(R.id.control); shutterView = findViewById(R.id.shutter); subtitleLayout = (SubtitleView) findViewById(R.id.subtitles); subtitleLayout.setUserDefaultStyle(); 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); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -122,6 +137,9 @@ public void setPlayer(SimpleExoPlayer player) { this.player.setVideoSurface(null); } this.player = player; + if (useController) { + controller.setPlayer(player); + } if (player != null) { if (surfaceView instanceof TextureView) { player.setVideoTextureView((TextureView) surfaceView); @@ -131,20 +149,36 @@ public void setPlayer(SimpleExoPlayer player) { player.setVideoListener(componentListener); player.addListener(componentListener); player.setTextOutput(componentListener); + maybeShowController(false); } else { shutterView.setVisibility(VISIBLE); + controller.hide(); } - if (useController) { - controller.setPlayer(player); - } } /** - * Set the {@code useController} flag which indicates whether the playback control view should - * be used or not. If set to {@code false} the controller is never visible and is disconnected - * from the player. + * Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT}, + * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or + * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}. + * + * @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 If {@code false} the playback control is never used. + * @param useController Whether playback controls should be enabled. */ public void setUseController(boolean useController) { if (this.useController == useController) { @@ -160,14 +194,26 @@ public void setUseController(boolean useController) { } /** - * Sets the resize mode which can be of value {@link AspectRatioFrameLayout#RESIZE_MODE_FIT}, - * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_HEIGHT} or - * {@link AspectRatioFrameLayout#RESIZE_MODE_FIXED_WIDTH}. + * Returns 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 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) { - layout.setResizeMode(resizeMode); + public int getControllerShowTimeoutMs() { + 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 void setFastForwardIncrementMs(int 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) * or a {@link TextureView} if the {@code use_texture_view} view attribute has been set to true. @@ -218,21 +255,23 @@ public View getVideoSurfaceView() { @Override public boolean onTouchEvent(MotionEvent ev) { - if (useController && ev.getActionMasked() == MotionEvent.ACTION_DOWN) { - if (controller.isVisible()) { - controller.hide(); - } else { - controller.show(); - } + if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + if (controller.isVisible()) { + controller.hide(); + } else { + maybeShowController(true); } return true; } + @Override public boolean onTrackballEvent(MotionEvent ev) { - if (!useController) { + if (!useController || player == null) { return false; } - controller.show(); + maybeShowController(true); return true; } @@ -241,6 +280,20 @@ public boolean dispatchKeyEvent(KeyEvent 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, TextRenderer.Output, ExoPlayer.EventListener { @@ -278,9 +331,7 @@ public void onLoadingChanged(boolean isLoading) { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (useController && playbackState == ExoPlayer.STATE_ENDED) { - controller.show(0); - } + maybeShowController(false); } @Override diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml index 5f39dcb6c4b..d58882c0aa7 100644 --- a/library/src/main/res/values/attrs.xml +++ b/library/src/main/res/values/attrs.xml @@ -20,10 +20,16 @@ + + + + + + @@ -31,4 +37,10 @@ + + + + + +