/* * 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.os.Handler; import android.support.annotation.IntRange; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.SparseBooleanArray; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; import com.devbrackets.android.exomedia.R; import com.devbrackets.android.exomedia.listener.VideoControlsButtonListener; import com.devbrackets.android.exomedia.listener.VideoControlsSeekListener; import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener; import com.devbrackets.android.exomedia.util.Repeater; import com.devbrackets.android.exomedia.util.ResourceUtil; import java.util.LinkedList; import java.util.List; /** * This is a simple abstraction for the {@link VideoView} to have a single "View" to add * or remove for the Default Video Controls. */ @SuppressWarnings("unused") public abstract class VideoControls extends RelativeLayout { public static final int DEFAULT_CONTROL_HIDE_DELAY = 2_000; protected static final long CONTROL_VISIBILITY_ANIMATION_LENGTH = 300; protected TextView currentTimeTextView; protected TextView endTimeTextView; protected TextView titleTextView; protected TextView subTitleTextView; protected TextView descriptionTextView; protected ImageButton playPauseButton; protected ImageButton previousButton; protected ImageButton nextButton; protected ProgressBar loadingProgressBar; protected ViewGroup controlsContainer; protected ViewGroup textContainer; protected Drawable playDrawable; protected Drawable pauseDrawable; @NonNull protected Handler visibilityHandler = new Handler(); @NonNull protected Repeater progressPollRepeater = new Repeater(); @Nullable protected VideoView videoView; @Nullable protected VideoControlsSeekListener seekListener; @Nullable protected VideoControlsButtonListener buttonsListener; @Nullable protected VideoControlsVisibilityListener visibilityListener; @NonNull protected InternalListener internalListener = new InternalListener(); @NonNull protected SparseBooleanArray enabledViews = new SparseBooleanArray(); protected long hideDelay = DEFAULT_CONTROL_HIDE_DELAY; protected boolean isLoading = false; protected boolean isVisible = true; protected boolean canViewHide = true; protected boolean hideEmptyTextContainer = true; /** * Sets the current video position, updating the seek bar * and the current time field * * @param position The position in milliseconds */ public abstract void setPosition(@IntRange(from = 0) long position); /** * Sets the video duration in Milliseconds to display * at the end of the progress bar * * @param duration The duration of the video in milliseconds */ public abstract void setDuration(@IntRange(from = 0) long duration); /** * Performs the progress update on the current time field, * and the seek bar */ public abstract void updateProgress(@IntRange(from = 0) long position, @IntRange(from = 0) long duration, @IntRange(from = 0, to = 100) int bufferPercent); /** * Used to retrieve the layout resource identifier to inflate * * @return The layout resource identifier to inflate */ @LayoutRes protected abstract int getLayoutResource(); /** * Performs the control visibility animation for showing or hiding * this view * @param toVisible True if the view should be visible at the end of the animation */ protected abstract void animateVisibility(boolean toVisible); /** * Update the current visibility of the text block independent of * the controls visibility */ protected abstract void updateTextContainerVisibility(); /** * Update the controls to indicate that the video * is loading. * * @param initialLoad <code>True</code> if the loading is the initial state, not for seeking or buffering */ public abstract void showLoading(boolean initialLoad); /** * Update the controls to indicate that the video is no longer loading * which will re-display the play/pause, progress, etc. controls */ public abstract void finishLoading(); public VideoControls(Context context) { super(context); setup(context); } public VideoControls(Context context, AttributeSet attrs) { super(context, attrs); setup(context); } public VideoControls(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setup(context); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public VideoControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); setup(context); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); //A poll used to periodically update the progress bar progressPollRepeater.setRepeatListener(new Repeater.RepeatListener() { @Override public void onRepeat() { updateProgress(); } }); if (videoView != null && videoView.isPlaying()) { updatePlaybackState(true); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); progressPollRepeater.stop(); progressPollRepeater.setRepeatListener(null); } /** * Sets the parent view to use for determining playback length, position, * state, etc. This should only be called once, during the setup process * * @param VideoView The Parent view to these controls */ public void setVideoView(@Nullable VideoView VideoView) { this.videoView = VideoView; } /** * Sets the callbacks to inform of progress seek events * * @param callbacks The callbacks to inform */ public void setSeekListener(@Nullable VideoControlsSeekListener callbacks) { this.seekListener = callbacks; } /** * Specifies the callback to inform of button click events * * @param callback The callback */ public void setButtonListener(@Nullable VideoControlsButtonListener callback) { this.buttonsListener = callback; } /** * Sets the callbacks to inform of visibility changes * * @param callbacks The callbacks to inform */ public void setVisibilityListener(@Nullable VideoControlsVisibilityListener callbacks) { this.visibilityListener = callbacks; } /** * Informs the controls that the playback state has changed. This will * update to display the correct views, and manage progress polling. * * @param isPlaying True if the media is currently playing */ public void updatePlaybackState(boolean isPlaying) { updatePlayPauseImage(isPlaying); progressPollRepeater.start(); if (isPlaying) { hideDelayed(); } else { show(); } } /** * Sets the title to display for the current item in playback * * @param title The title to display */ public void setTitle(@Nullable CharSequence title) { titleTextView.setText(title); updateTextContainerVisibility(); } /** * Sets the subtitle to display for the current item in playback. This will be displayed * as the second line of text * * @param subTitle The sub title to display */ public void setSubTitle(@Nullable CharSequence subTitle) { subTitleTextView.setText(subTitle); updateTextContainerVisibility(); } /** * Sets the description text to display for the current item in playback. This will be displayed * as the third line of text and unlike the {@link #setTitle(CharSequence)} and {@link #setSubTitle(CharSequence)} * this text wont be limited to a single line of text * * @param description The artist to display */ public void setDescription(@Nullable CharSequence description) { descriptionTextView.setText(description); updateTextContainerVisibility(); } /** * Sets the drawables to use for the PlayPause button * * @param playDrawable The drawable to represent play * @param pauseDrawable The drawable to represent pause */ public void setPlayPauseDrawables(Drawable playDrawable, Drawable pauseDrawable) { this.playDrawable = playDrawable; this.pauseDrawable = pauseDrawable; updatePlayPauseImage(videoView != null && videoView.isPlaying()); } /** * Sets the drawable for the previous button * * @param drawable The drawable to use */ public void setPreviousDrawable(Drawable drawable) { previousButton.setImageDrawable(drawable); } /** * Sets the drawable for the next button * * @param drawable The drawable to use */ public void setNextDrawable(Drawable drawable) { nextButton.setImageDrawable(drawable); } /** * Sets the drawable for the rewind button * * @param drawable The drawable to use */ public void setRewindDrawable(Drawable drawable) { //Purposefully let blank } /** * Sets the drawable for the Fast button * * @param drawable The drawable to use */ public void setFastForwardDrawable(Drawable drawable) { //Purposefully let blank } /** * Makes sure the playPause button represents the correct playback state * * @param isPlaying If the video is currently playing */ public void updatePlayPauseImage(boolean isPlaying) { playPauseButton.setImageDrawable(isPlaying ? pauseDrawable : playDrawable); } /** * Sets the button state for the Previous button. This will just * change the images specified with {@link #setPreviousDrawable(Drawable)}, * or use the defaults if they haven't been set, and block any click events. * <p> * This method will NOT re-add buttons that have previously been removed with * {@link #setNextButtonRemoved(boolean)}. * * @param enabled If the Previous button is enabled [default: false] */ public void setPreviousButtonEnabled(boolean enabled) { previousButton.setEnabled(enabled); enabledViews.put(R.id.exomedia_controls_previous_btn, enabled); } /** * Sets the button state for the Next button. This will just * change the images specified with {@link #setNextDrawable(Drawable)}, * or use the defaults if they haven't been set, and block any click events. * <p> * This method will NOT re-add buttons that have previously been removed with * {@link #setPreviousButtonRemoved(boolean)}. * * @param enabled If the Next button is enabled [default: false] */ public void setNextButtonEnabled(boolean enabled) { nextButton.setEnabled(enabled); enabledViews.put(R.id.exomedia_controls_next_btn, enabled); } /** * Sets the button state for the Rewind button. This will just * change the images specified with {@link #setRewindDrawable(Drawable)}, * or use the defaults if they haven't been set * <p> * This method will NOT re-add buttons that have previously been removed with * {@link #setRewindButtonRemoved(boolean)}. * * @param enabled If the Rewind button is enabled [default: false] */ public void setRewindButtonEnabled(boolean enabled) { //Purposefully left blank } /** * Sets the button state for the Fast Forward button. This will just * change the images specified with {@link #setFastForwardDrawable(Drawable)}, * or use the defaults if they haven't been set * <p> * This method will NOT re-add buttons that have previously been removed with * {@link #setFastForwardButtonRemoved(boolean)}. * * @param enabled If the Rewind button is enabled [default: false] */ public void setFastForwardButtonEnabled(boolean enabled) { //Purposefully left blank } /** * Adds or removes the Previous button. This will change the visibility * of the button, if you want to change the enabled/disabled images see {@link #setPreviousButtonEnabled(boolean)} * * @param removed If the Previous button should be removed [default: true] */ public void setPreviousButtonRemoved(boolean removed) { previousButton.setVisibility(removed ? View.GONE : View.VISIBLE); } /** * Adds or removes the Next button. This will change the visibility * of the button, if you want to change the enabled/disabled images see {@link #setNextButtonEnabled(boolean)} * * @param removed If the Next button should be removed [default: true] */ public void setNextButtonRemoved(boolean removed) { nextButton.setVisibility(removed ? View.GONE : View.VISIBLE); } /** * Adds or removes the Rewind button. This will change the visibility * of the button, if you want to change the enabled/disabled images see {@link #setRewindButtonEnabled(boolean)} * * @param removed If the Rewind button should be removed [default: true] */ public void setRewindButtonRemoved(boolean removed) { //Purposefully left blank } /** * Adds or removes the FastForward button. This will change the visibility * of the button, if you want to change the enabled/disabled images see {@link #setFastForwardButtonEnabled(boolean)} * * @param removed If the FastForward button should be removed [default: true] */ public void setFastForwardButtonRemoved(boolean removed) { //Purposefully left blank } public void addExtraView(@NonNull View view) { //Purposefully left blank } public void removeExtraView(@NonNull View view) { //Purposefully left blank } @NonNull public List<View> getExtraViews() { return new LinkedList<>(); } /** * Immediately starts the animation to show the controls */ public void show() { //Makes sure we don't have a hide animation scheduled visibilityHandler.removeCallbacksAndMessages(null); clearAnimation(); animateVisibility(true); } /** * Immediately starts the animation to hide the controls */ public void hide() { if (!canViewHide || isLoading) { return; } //Makes sure we don't have a separate hide animation scheduled visibilityHandler.removeCallbacksAndMessages(null); clearAnimation(); animateVisibility(false); } /** * After the specified delay the view will be hidden. If the user is interacting * with the controls then we wait until after they are done to start the delay. */ public void hideDelayed() { hideDelayed(hideDelay); } /** * After the specified delay the view will be hidden. If the user is interacting * with the controls then we wait until after they are done to start the delay. * * @param delay The delay in milliseconds to wait to start the hide animation */ public void hideDelayed(long delay) { hideDelay = delay; if (delay < 0 || !canViewHide || isLoading) { return; } visibilityHandler.postDelayed(new Runnable() { @Override public void run() { hide(); } }, delay); } /** * Sets the delay to use when hiding the controls via the {@link #hideDelayed()} * method. This value will be overridden if {@link #hideDelayed(long)} is called. * * @param delay The delay in milliseconds to wait to start the hide animation */ public void setHideDelay(long delay) { hideDelay = delay; } /** * Sets weather this control can be hidden. * * @param canHide If this control can be hidden [default: true] */ public void setCanHide(boolean canHide) { canViewHide = canHide; } /** * Sets weather the text block and associated container will be hidden * when no content is specified. * * @param hide If the empty text blocks can be hidden [default: true] */ public void setHideEmptyTextContainer(boolean hide) { this.hideEmptyTextContainer = hide; updateTextContainerVisibility(); } /** * Returns <code>true</code> if the {@link VideoControls} are visible * * @return <code>true</code> if the controls are visible */ public boolean isVisible() { return isVisible; } /** * Retrieves the view references from the xml layout */ protected void retrieveViews() { currentTimeTextView = (TextView) findViewById(R.id.exomedia_controls_current_time); endTimeTextView = (TextView) findViewById(R.id.exomedia_controls_end_time); titleTextView = (TextView) findViewById(R.id.exomedia_controls_title); subTitleTextView = (TextView) findViewById(R.id.exomedia_controls_sub_title); descriptionTextView = (TextView) findViewById(R.id.exomedia_controls_description); playPauseButton = (ImageButton) findViewById(R.id.exomedia_controls_play_pause_btn); previousButton = (ImageButton) findViewById(R.id.exomedia_controls_previous_btn); nextButton = (ImageButton) findViewById(R.id.exomedia_controls_next_btn); loadingProgressBar = (ProgressBar) findViewById(R.id.exomedia_controls_video_loading); controlsContainer = (ViewGroup) findViewById(R.id.exomedia_controls_interactive_container); textContainer = (ViewGroup) findViewById(R.id.exomedia_controls_text_container); } /** * Registers any internal listeners to perform the playback controls, * such as play/pause, next, and previous */ protected void registerListeners() { playPauseButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onPlayPauseClick(); } }); previousButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onPreviousClick(); } }); nextButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { onNextClick(); } }); } /** * Updates the drawables used for the buttons to AppCompatTintDrawables */ protected void updateButtonDrawables() { playDrawable = ResourceUtil.tintList(getContext(), R.drawable.exomedia_ic_play_arrow_white, R.color.exomedia_default_controls_button_selector); pauseDrawable = ResourceUtil.tintList(getContext(), R.drawable.exomedia_ic_pause_white, R.color.exomedia_default_controls_button_selector); playPauseButton.setImageDrawable(playDrawable); Drawable previousDrawable = ResourceUtil.tintList(getContext(), R.drawable.exomedia_ic_skip_previous_white, R.color.exomedia_default_controls_button_selector); previousButton.setImageDrawable(previousDrawable); Drawable nextDrawable = ResourceUtil.tintList(getContext(), R.drawable.exomedia_ic_skip_next_white, R.color.exomedia_default_controls_button_selector); nextButton.setImageDrawable(nextDrawable); } /** * Performs the functionality when the PlayPause button is clicked. This * includes invoking the callback method if it is enabled, posting the bus * event, and toggling the video playback. */ protected void onPlayPauseClick() { if (buttonsListener == null || !buttonsListener.onPlayPauseClicked()) { internalListener.onPlayPauseClicked(); } } /** * Performs the functionality to inform any listeners that the previous * button has been clicked */ protected void onPreviousClick() { if (buttonsListener == null || !buttonsListener.onPreviousClicked()) { internalListener.onPreviousClicked(); } } /** * Performs the functionality to inform any listeners that the next * button has been clicked */ protected void onNextClick() { if (buttonsListener == null || !buttonsListener.onNextClicked()) { internalListener.onNextClicked(); } } /** * Performs any initialization steps such as retrieving views, registering listeners, * and updating any drawables. * * @param context The context to use for retrieving the correct layout */ protected void setup(Context context) { View.inflate(context, getLayoutResource(), this); retrieveViews(); registerListeners(); updateButtonDrawables(); } /** * Determines if the <code>textContainer</code> doesn't have any text associated with it * @return True if there is no text contained in the views in the <code>textContainer</code> */ @SuppressWarnings("RedundantIfStatement") protected boolean isTextContainerEmpty() { if (titleTextView.getText() != null && titleTextView.getText().length() > 0) { return false; } if (subTitleTextView.getText() != null && subTitleTextView.getText().length() > 0) { return false; } if (descriptionTextView.getText() != null && descriptionTextView.getText().length() > 0) { return false; } return true; } /** * Performs the functionality to inform the callback * that the DefaultControls visibility has changed */ protected void onVisibilityChanged() { if (visibilityListener == null) { return; } if (isVisible) { visibilityListener.onControlsShown(); } else { visibilityListener.onControlsHidden(); } } /** * Called by the {@link #progressPollRepeater} to update the progress * bar using the {@link #videoView} to retrieve the correct information */ protected void updateProgress() { if (videoView != null) { updateProgress(videoView.getCurrentPosition(), videoView.getDuration(), videoView.getBufferPercentage()); } } /** * An internal class used to handle the default functionality for the * VideoControls */ protected class InternalListener implements VideoControlsSeekListener, VideoControlsButtonListener { protected boolean pausedForSeek = false; @Override public boolean onPlayPauseClicked() { if (videoView == null) { return false; } if (videoView.isPlaying()) { videoView.pause(); } else { videoView.start(); } return true; } @Override public boolean onPreviousClicked() { //Purposefully left blank return false; } @Override public boolean onNextClicked() { //Purposefully left blank return false; } @Override public boolean onRewindClicked() { //Purposefully left blank return false; } @Override public boolean onFastForwardClicked() { //Purposefully left blank return false; } @Override public boolean onSeekStarted() { if (videoView == null) { return false; } if (videoView.isPlaying()) { pausedForSeek = true; videoView.pause(); } show(); return true; } @Override public boolean onSeekEnded(long seekTime) { if (videoView == null) { return false; } videoView.seekTo(seekTime); if (pausedForSeek) { pausedForSeek = false; videoView.start(); hideDelayed(); } return true; } } }