/*
* Copyright (C) 2016 Brian Wernick
*
* 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.devbrackets.android.exomedia.ui.widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.IntRange;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import com.devbrackets.android.exomedia.R;
import com.devbrackets.android.exomedia.ui.animation.BottomViewHideShowAnimation;
import com.devbrackets.android.exomedia.util.ResourceUtil;
import com.devbrackets.android.exomedia.util.TimeFormatUtil;
/**
* Provides playback controls for the {@link VideoView} on TV devices.
*/
@SuppressWarnings("unused")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class VideoControlsLeanback extends VideoControls {
protected static final int FAST_FORWARD_REWIND_AMOUNT = 10000; //10 seconds
protected ProgressBar progressBar;
protected ImageView rippleIndicator;
protected ViewGroup controlsParent;
protected ImageButton fastForwardButton;
protected ImageButton rewindButton;
protected View currentFocus;
protected ButtonFocusChangeListener buttonFocusChangeListener = new ButtonFocusChangeListener();
public VideoControlsLeanback(Context context) {
super(context);
}
public VideoControlsLeanback(Context context, AttributeSet attrs) {
super(context, attrs);
}
public VideoControlsLeanback(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public VideoControlsLeanback(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void setup(Context context) {
super.setup(context);
internalListener = new LeanbackInternalListener();
registerForInput();
setFocusable(true);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
playPauseButton.requestFocus();
currentFocus = playPauseButton;
}
@Override
protected int getLayoutResource() {
return R.layout.exomedia_default_controls_leanback;
}
@Override
public void setPosition(long position) {
currentTimeTextView.setText(TimeFormatUtil.formatMs(position));
progressBar.setProgress((int) position);
}
@Override
public void setDuration(long duration) {
if (duration != progressBar.getMax()) {
endTimeTextView.setText(TimeFormatUtil.formatMs(duration));
progressBar.setMax((int) duration);
}
}
@Override
public void updateProgress(@IntRange(from = 0) long position, @IntRange(from = 0) long duration, @IntRange(from = 0, to = 100) int bufferPercent) {
progressBar.setSecondaryProgress((int) (progressBar.getMax() * ((float)bufferPercent / 100)));
progressBar.setProgress((int) position);
currentTimeTextView.setText(TimeFormatUtil.formatMs(position));
}
@Override
public void setRewindDrawable(Drawable drawable) {
if (rewindButton != null) {
rewindButton.setImageDrawable(drawable);
}
}
@Override
public void setFastForwardDrawable(Drawable drawable) {
if (fastForwardButton != null) {
fastForwardButton.setImageDrawable(drawable);
}
}
@Override
public void setRewindButtonEnabled(boolean enabled) {
if (rewindButton != null) {
rewindButton.setEnabled(enabled);
enabledViews.put(R.id.exomedia_controls_rewind_btn, enabled);
}
}
@Override
public void setFastForwardButtonEnabled(boolean enabled) {
if (fastForwardButton != null) {
fastForwardButton.setEnabled(enabled);
enabledViews.put(R.id.exomedia_controls_fast_forward_btn, enabled);
}
}
@Override
public void setRewindButtonRemoved(boolean removed) {
if (rewindButton != null) {
rewindButton.setVisibility(removed ? View.GONE : View.VISIBLE);
}
}
@Override
public void setFastForwardButtonRemoved(boolean removed) {
if (fastForwardButton != null) {
fastForwardButton.setVisibility(removed ? View.GONE : View.VISIBLE);
}
}
@Override
protected void retrieveViews() {
super.retrieveViews();
progressBar = (ProgressBar) findViewById(R.id.exomedia_controls_video_progress);
rewindButton = (ImageButton) findViewById(R.id.exomedia_controls_rewind_btn);
fastForwardButton = (ImageButton) findViewById(R.id.exomedia_controls_fast_forward_btn);
rippleIndicator = (ImageView) findViewById(R.id.exomedia_controls_leanback_ripple);
controlsParent = (ViewGroup) findViewById(R.id.exomedia_controls_parent);
}
@Override
protected void registerListeners() {
super.registerListeners();
rewindButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onRewindClick();
}
});
fastForwardButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onFastForwardClick();
}
});
//Registers the buttons for focus changes in order to update the ripple selector
previousButton.setOnFocusChangeListener(buttonFocusChangeListener);
rewindButton.setOnFocusChangeListener(buttonFocusChangeListener);
playPauseButton.setOnFocusChangeListener(buttonFocusChangeListener);
fastForwardButton.setOnFocusChangeListener(buttonFocusChangeListener);
nextButton.setOnFocusChangeListener(buttonFocusChangeListener);
}
@Override
protected void updateButtonDrawables() {
super.updateButtonDrawables();
Drawable rewindDrawable = ResourceUtil.tintList(getContext(), R.drawable.exomedia_ic_rewind_white, R.color.exomedia_default_controls_button_selector);
rewindButton.setImageDrawable(rewindDrawable);
Drawable fastForwardDrawable = ResourceUtil.tintList(getContext(), R.drawable.exomedia_ic_fast_forward_white, R.color.exomedia_default_controls_button_selector);
fastForwardButton.setImageDrawable(fastForwardDrawable);
}
@Override
protected void animateVisibility(boolean toVisible) {
if (isVisible == toVisible) {
return;
}
if (!isLoading) {
controlsParent.startAnimation(new BottomViewHideShowAnimation(controlsParent, toVisible, CONTROL_VISIBILITY_ANIMATION_LENGTH));
}
isVisible = toVisible;
onVisibilityChanged();
}
@Override
protected void updateTextContainerVisibility() {
if (!isVisible) {
return;
}
boolean emptyText = isTextContainerEmpty();
if (hideEmptyTextContainer && emptyText && textContainer.getVisibility() == VISIBLE) {
textContainer.clearAnimation();
textContainer.startAnimation(new BottomViewHideShowAnimation(textContainer, false, CONTROL_VISIBILITY_ANIMATION_LENGTH));
} else if ((!hideEmptyTextContainer || !emptyText) && textContainer.getVisibility() != VISIBLE) {
textContainer.clearAnimation();
textContainer.startAnimation(new BottomViewHideShowAnimation(textContainer, true, CONTROL_VISIBILITY_ANIMATION_LENGTH));
}
}
@Override
public void showLoading(boolean initialLoad) {
if (isLoading) {
return;
}
isLoading = true;
controlsContainer.setVisibility(View.GONE);
rippleIndicator.setVisibility(View.GONE);
loadingProgressBar.setVisibility(View.VISIBLE);
show();
}
@Override
public void finishLoading() {
if (!isLoading) {
return;
}
isLoading = false;
controlsContainer.setVisibility(View.VISIBLE);
rippleIndicator.setVisibility(View.VISIBLE);
loadingProgressBar.setVisibility(View.GONE);
updatePlaybackState(videoView != null && videoView.isPlaying());
}
/**
* Performs the functionality to rewind the current video by
* {@value #FAST_FORWARD_REWIND_AMOUNT} milliseconds.
*/
protected void onRewindClick() {
if (buttonsListener == null || !buttonsListener.onRewindClicked()) {
internalListener.onRewindClicked();
}
}
/**
* Performs the functionality to fast forward the current video by
* {@value #FAST_FORWARD_REWIND_AMOUNT} milliseconds.
*/
protected void onFastForwardClick() {
if (buttonsListener == null || !buttonsListener.onFastForwardClicked()) {
internalListener.onFastForwardClicked();
}
}
/**
* Performs the functionality to inform any listeners that the video has been
* seeked to the specified time.
*
* @param seekToTime The time to seek to in milliseconds
*/
protected void performSeek(long seekToTime) {
if (seekListener == null || !seekListener.onSeekEnded(seekToTime)) {
internalListener.onSeekEnded(seekToTime);
}
}
/**
* Temporarily shows the default controls, hiding after the standard
* delay. If the {@link #videoView} is not playing then the controls
* will not be hidden.
*/
protected void showTemporary() {
show();
if (videoView != null && videoView.isPlaying()) {
hideDelayed();
}
}
/**
* Registers all selectable fields for key events in order
* to correctly handle navigation.
*/
protected void registerForInput() {
RemoteKeyListener remoteKeyListener = new RemoteKeyListener();
setOnKeyListener(remoteKeyListener);
//Registers each button to make sure we catch the key events
playPauseButton.setOnKeyListener(remoteKeyListener);
previousButton.setOnKeyListener(remoteKeyListener);
nextButton.setOnKeyListener(remoteKeyListener);
rewindButton.setOnKeyListener(remoteKeyListener);
fastForwardButton.setOnKeyListener(remoteKeyListener);
}
/**
* Focuses the next visible view specified in the <code>view</code>
*
* @param view The view to find the next focus for
*/
protected void focusNext(View view) {
int nextId = view.getNextFocusRightId();
if (nextId == NO_ID) {
return;
}
View nextView = findViewById(nextId);
if (nextView.getVisibility() != View.VISIBLE) {
focusNext(nextView);
return;
}
nextView.requestFocus();
currentFocus = nextView;
buttonFocusChangeListener.onFocusChange(nextView, true);
}
/**
* Focuses the previous visible view specified in the <code>view</code>
*
* @param view The view to find the previous focus for
*/
protected void focusPrevious(View view) {
int previousId = view.getNextFocusLeftId();
if (previousId == NO_ID) {
return;
}
View previousView = findViewById(previousId);
if (previousView.getVisibility() != View.VISIBLE) {
focusPrevious(previousView);
return;
}
previousView.requestFocus();
currentFocus = previousView;
buttonFocusChangeListener.onFocusChange(previousView, true);
}
/**
* A listener to monitor the selected button and move the ripple
* indicator when the focus shifts.
*/
protected class ButtonFocusChangeListener implements OnFocusChangeListener {
@Override
public void onFocusChange(View view, boolean hasFocus) {
if (!hasFocus) {
return;
}
//Performs the move animation
int xDelta = getHorizontalDelta(view);
rippleIndicator.startAnimation(new RippleTranslateAnimation(xDelta));
}
protected int getHorizontalDelta(View selectedView) {
int[] position = new int[2];
selectedView.getLocationOnScreen(position);
int viewX = position[0];
rippleIndicator.getLocationOnScreen(position);
int newRippleX = viewX - ((rippleIndicator.getWidth() - selectedView.getWidth()) / 2);
return newRippleX - position[0];
}
}
/**
* A listener to catch the key events so that we can correctly perform the
* playback functionality and to hide/show the controls
*/
protected class RemoteKeyListener implements OnKeyListener {
/**
* NOTE: the view is not always the currently focused view, thus the
* {@link #currentFocus} variable
*/
@Override
public boolean onKey(View view, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
if (isVisible && canViewHide && !isLoading) {
hide();
return true;
} else if (controlsParent.getAnimation() != null) {
//This occurs if we are animating the hide or show of the controls
return true;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
showTemporary();
return true;
case KeyEvent.KEYCODE_DPAD_DOWN:
hide();
return true;
case KeyEvent.KEYCODE_DPAD_LEFT:
showTemporary();
focusPrevious(currentFocus);
return true;
case KeyEvent.KEYCODE_DPAD_RIGHT:
showTemporary();
focusNext(currentFocus);
return true;
case KeyEvent.KEYCODE_DPAD_CENTER:
showTemporary();
currentFocus.callOnClick();
return true;
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
onPlayPauseClick();
return true;
case KeyEvent.KEYCODE_MEDIA_PLAY:
if (videoView != null && !videoView.isPlaying()) {
videoView.start();
return true;
}
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
if (videoView != null && videoView.isPlaying()) {
videoView.pause();
return true;
}
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
onNextClick();
return true;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
onPreviousClick();
return true;
case KeyEvent.KEYCODE_MEDIA_REWIND:
onRewindClick();
return true;
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
onFastForwardClick();
return true;
}
return false;
}
}
/**
* An animation for moving the ripple indicator to the correctly
* focused view.
*/
protected class RippleTranslateAnimation extends TranslateAnimation implements Animation.AnimationListener {
protected static final long DURATION = 250;
protected int xDelta;
public RippleTranslateAnimation(int xDelta) {
super(0, xDelta, 0, 0);
this.xDelta = xDelta;
setDuration(DURATION);
setAnimationListener(this);
}
@Override
public void onAnimationStart(Animation animation) {
//Purposefully left blank
}
@Override
public void onAnimationEnd(Animation animation) {
rippleIndicator.setX(rippleIndicator.getX() + xDelta);
rippleIndicator.clearAnimation();
}
@Override
public void onAnimationRepeat(Animation animation) {
//Purposefully left blank
}
}
protected class LeanbackInternalListener extends InternalListener {
@Override
public boolean onFastForwardClicked() {
if (videoView == null) {
return false;
}
long newPosition = videoView.getCurrentPosition() + FAST_FORWARD_REWIND_AMOUNT;
if (newPosition > progressBar.getMax()) {
newPosition = progressBar.getMax();
}
performSeek(newPosition);
return true;
}
@Override
public boolean onRewindClicked() {
if (videoView == null) {
return false;
}
long newPosition = videoView.getCurrentPosition() - FAST_FORWARD_REWIND_AMOUNT;
if (newPosition < 0) {
newPosition = 0;
}
performSeek(newPosition);
return true;
}
}
}