| /* |
| * 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 androidx.leanback.app; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorInflater; |
| import android.animation.TimeInterpolator; |
| import android.animation.ValueAnimator; |
| import android.animation.ValueAnimator.AnimatorUpdateListener; |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.graphics.drawable.ColorDrawable; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.InputEvent; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.animation.AccelerateInterpolator; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.fragment.app.Fragment; |
| import androidx.leanback.R; |
| import androidx.leanback.animation.LogAccelerateInterpolator; |
| import androidx.leanback.animation.LogDecelerateInterpolator; |
| import androidx.leanback.media.PlaybackGlueHost; |
| import androidx.leanback.widget.ArrayObjectAdapter; |
| import androidx.leanback.widget.BaseOnItemViewClickedListener; |
| import androidx.leanback.widget.BaseOnItemViewSelectedListener; |
| import androidx.leanback.widget.ClassPresenterSelector; |
| import androidx.leanback.widget.ItemAlignmentFacet; |
| import androidx.leanback.widget.ItemBridgeAdapter; |
| import androidx.leanback.widget.ObjectAdapter; |
| import androidx.leanback.widget.PlaybackRowPresenter; |
| import androidx.leanback.widget.PlaybackSeekDataProvider; |
| import androidx.leanback.widget.PlaybackSeekUi; |
| import androidx.leanback.widget.Presenter; |
| import androidx.leanback.widget.PresenterSelector; |
| import androidx.leanback.widget.Row; |
| import androidx.leanback.widget.RowPresenter; |
| import androidx.leanback.widget.SparseArrayObjectAdapter; |
| import androidx.leanback.widget.VerticalGridView; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| /** |
| * A fragment for displaying playback controls and related content. |
| * |
| * <p> |
| * A PlaybackSupportFragment renders the elements of its {@link ObjectAdapter} as a set |
| * of rows in a vertical list. The Adapter's {@link PresenterSelector} must maintain subclasses |
| * of {@link RowPresenter}. |
| * </p> |
| * <p> |
| * A playback row is a row rendered by {@link PlaybackRowPresenter}. |
| * App can call {@link #setPlaybackRow(Row)} to set playback row for the first element of adapter. |
| * App can call {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to set presenter for it. |
| * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} are |
| * optional, app can pass playback row and PlaybackRowPresenter in the adapter using |
| * {@link #setAdapter(ObjectAdapter)}. |
| * </p> |
| * <p> |
| * Hiding and showing controls: the controls are initially visible and automatically show/hide |
| * when play/pause or user interacts with fragment. |
| * <ul> |
| * <li> |
| * App may manually call {@link #showControlsOverlay(boolean)} or |
| * {@link #hideControlsOverlay(boolean)} to show or hide the controls. |
| * <li> |
| * <li> |
| * The controls are visible by default upon onViewCreated(). To make it initially invisible, |
| * call hideControlsOverlay(false) in overridden onViewCreated(). |
| * </li> |
| * Upon play or pause, PlaybackControlGlue or PlaybackTransportControlGlue will fade-in |
| * the controls and automatically fade out after a delay customized by |
| * {@link R.attr#playbackControlsAutoHideTimeout}. To disable the fade in and fade out |
| * behavior: call {@link androidx.leanback.media.PlaybackBaseControlGlue |
| * #setControlsOverlayAutoHideEnabled(boolean)} with false. |
| * </li> |
| * <li> |
| * Upon user interaction event, fragment will fade-in the controls and automatically fade |
| * out after a delay customized by {@link R.attr#playbackControlsAutoHideTickleTimeout}. |
| * To disable the fade in and fade out behavior, call {@link |
| * #setShowOrHideControlsOverlayOnUserInteraction} with false. |
| * </li> |
| * </ul> |
| */ |
| public class PlaybackSupportFragment extends Fragment { |
| static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview"; |
| |
| /** |
| * No background. |
| */ |
| public static final int BG_NONE = 0; |
| |
| /** |
| * A dark translucent background. |
| */ |
| public static final int BG_DARK = 1; |
| PlaybackGlueHost.HostCallback mHostCallback; |
| |
| PlaybackSeekUi.Client mSeekUiClient; |
| boolean mInSeek; |
| ProgressBarManager mProgressBarManager = new ProgressBarManager(); |
| |
| /** |
| * Resets the focus on the button in the middle of control row. |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public void resetFocus() { |
| ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView() |
| .findViewHolderForAdapterPosition(0); |
| if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) { |
| ((PlaybackRowPresenter) vh.getPresenter()).onReappear( |
| (RowPresenter.ViewHolder) vh.getViewHolder()); |
| } |
| } |
| |
| private class SetSelectionRunnable implements Runnable { |
| int mPosition; |
| boolean mSmooth = true; |
| |
| SetSelectionRunnable() { |
| } |
| |
| @Override |
| public void run() { |
| if (mRowsSupportFragment == null) { |
| return; |
| } |
| mRowsSupportFragment.setSelectedPosition(mPosition, mSmooth); |
| } |
| } |
| |
| /** |
| * A light translucent background. |
| */ |
| public static final int BG_LIGHT = 2; |
| RowsSupportFragment mRowsSupportFragment; |
| ObjectAdapter mAdapter; |
| PlaybackRowPresenter mPresenter; |
| Row mRow; |
| BaseOnItemViewSelectedListener mExternalItemSelectedListener; |
| BaseOnItemViewClickedListener mExternalItemClickedListener; |
| BaseOnItemViewClickedListener mPlaybackItemClickedListener; |
| |
| private final BaseOnItemViewClickedListener mOnItemViewClickedListener = |
| new BaseOnItemViewClickedListener() { |
| @Override |
| @SuppressWarnings("unchecked") |
| public void onItemClicked(Presenter.ViewHolder itemViewHolder, |
| Object item, |
| RowPresenter.ViewHolder rowViewHolder, |
| Object row) { |
| if (mPlaybackItemClickedListener != null |
| && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) { |
| mPlaybackItemClickedListener.onItemClicked( |
| itemViewHolder, item, rowViewHolder, row); |
| } |
| if (mExternalItemClickedListener != null) { |
| mExternalItemClickedListener.onItemClicked( |
| itemViewHolder, item, rowViewHolder, row); |
| } |
| } |
| }; |
| |
| private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener = |
| new BaseOnItemViewSelectedListener() { |
| @Override |
| @SuppressWarnings("unchecked") |
| public void onItemSelected(Presenter.ViewHolder itemViewHolder, |
| Object item, |
| RowPresenter.ViewHolder rowViewHolder, |
| Object row) { |
| if (mExternalItemSelectedListener != null) { |
| mExternalItemSelectedListener.onItemSelected( |
| itemViewHolder, item, rowViewHolder, row); |
| } |
| } |
| }; |
| |
| private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); |
| |
| public ObjectAdapter getAdapter() { |
| return mAdapter; |
| } |
| |
| /** |
| * Listener allowing the application to receive notification of fade in and/or fade out |
| * completion events. |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public static class OnFadeCompleteListener { |
| public void onFadeInComplete() { |
| } |
| |
| public void onFadeOutComplete() { |
| } |
| } |
| |
| private static final String TAG = "PlaybackSupportFragment"; |
| private static final boolean DEBUG = false; |
| private static final int ANIMATION_MULTIPLIER = 1; |
| |
| private static final int START_FADE_OUT = 1; |
| |
| // Fading status |
| private static final int IDLE = 0; |
| private static final int ANIMATING = 1; |
| |
| int mPaddingBottom; |
| int mOtherRowsCenterToBottom; |
| View mRootView; |
| View mBackgroundView; |
| int mBackgroundType = BG_DARK; |
| int mBgDarkColor; |
| int mBgLightColor; |
| int mAutohideTimerAfterPlayingInMs; |
| int mAutohideTimerAfterTickleInMs; |
| int mMajorFadeTranslateY, mMinorFadeTranslateY; |
| int mAnimationTranslateY; |
| OnFadeCompleteListener mFadeCompleteListener; |
| View.OnKeyListener mInputEventHandler; |
| boolean mFadingEnabled = true; |
| boolean mControlVisibleBeforeOnCreateView = true; |
| boolean mControlVisible = true; |
| boolean mShowOrHideControlsOverlayOnUserInteraction = true; |
| int mBgAlpha; |
| ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator; |
| ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator; |
| ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator; |
| |
| private final Animator.AnimatorListener mFadeListener = |
| new Animator.AnimatorListener() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| enableVerticalGridAnimations(false); |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animator animation) { |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha); |
| if (mBgAlpha > 0) { |
| enableVerticalGridAnimations(true); |
| if (mFadeCompleteListener != null) { |
| mFadeCompleteListener.onFadeInComplete(); |
| } |
| } else { |
| VerticalGridView verticalView = getVerticalGridView(); |
| // reset focus to the primary actions only if the selected row was the controls row |
| if (verticalView != null && verticalView.getSelectedPosition() == 0) { |
| ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) |
| verticalView.findViewHolderForAdapterPosition(0); |
| if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) { |
| ((PlaybackRowPresenter)vh.getPresenter()).onReappear( |
| (RowPresenter.ViewHolder) vh.getViewHolder()); |
| } |
| } |
| if (mFadeCompleteListener != null) { |
| mFadeCompleteListener.onFadeOutComplete(); |
| } |
| } |
| } |
| }; |
| |
| public PlaybackSupportFragment() { |
| mProgressBarManager.setInitialDelay(500); |
| } |
| |
| VerticalGridView getVerticalGridView() { |
| if (mRowsSupportFragment == null) { |
| return null; |
| } |
| return mRowsSupportFragment.getVerticalGridView(); |
| } |
| |
| private final Handler mHandler = new Handler() { |
| @Override |
| public void handleMessage(Message message) { |
| if (message.what == START_FADE_OUT && mFadingEnabled) { |
| hideControlsOverlay(true); |
| } |
| } |
| }; |
| |
| private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener = |
| new VerticalGridView.OnTouchInterceptListener() { |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent event) { |
| return onInterceptInputEvent(event); |
| } |
| }; |
| |
| private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener = |
| new VerticalGridView.OnKeyInterceptListener() { |
| @Override |
| public boolean onInterceptKeyEvent(KeyEvent event) { |
| return onInterceptInputEvent(event); |
| } |
| }; |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void setBgAlpha(int alpha) { |
| mBgAlpha = alpha; |
| if (mBackgroundView != null) { |
| mBackgroundView.getBackground().setAlpha(alpha); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| void enableVerticalGridAnimations(boolean enable) { |
| if (getVerticalGridView() != null) { |
| getVerticalGridView().setAnimateChildLayout(enable); |
| } |
| } |
| |
| /** |
| * Enables or disables showing and auto-hiding controls when user interacts. Enabled by default. |
| * Auto-hide timer length is defined by {@link R.attr#playbackControlsAutoHideTickleTimeout}. |
| */ |
| public void setShowOrHideControlsOverlayOnUserInteraction(boolean |
| showOrHideControlsOverlayOnUserInteraction) { |
| mShowOrHideControlsOverlayOnUserInteraction = showOrHideControlsOverlayOnUserInteraction; |
| } |
| |
| /** |
| * Returns true if showing and auto-hiding controls when user interacts; false otherwise. |
| */ |
| public boolean isShowOrHideControlsOverlayOnUserInteraction() { |
| return mShowOrHideControlsOverlayOnUserInteraction; |
| } |
| |
| /** |
| * Enables or disables auto hiding controls overlay after a short delay fragment is resumed. |
| * If enabled and fragment is resumed, the view will fade out after a time period. |
| * User interaction will kill the timer, next time fragment is resumed, |
| * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true. |
| * <p> |
| * In most cases app should not directly call setControlsOverlayAutoHideEnabled() as it's |
| * called by {@link androidx.leanback.media.PlaybackBaseControlGlue} on play or pause. |
| */ |
| public void setControlsOverlayAutoHideEnabled(boolean enabled) { |
| if (DEBUG) Log.v(TAG, "setControlsOverlayAutoHideEnabled " + enabled); |
| if (enabled != mFadingEnabled) { |
| mFadingEnabled = enabled; |
| if (isResumed() && getView().hasFocus()) { |
| showControlsOverlay(true); |
| if (enabled) { |
| // StateGraph 7->2 5->2 |
| startFadeTimer(mAutohideTimerAfterPlayingInMs); |
| } else { |
| // StateGraph 4->5 2->5 |
| stopFadeTimer(); |
| } |
| } else { |
| // StateGraph 6->1 1->6 |
| } |
| } |
| } |
| |
| /** |
| * Returns true if controls will be auto hidden after a delay when fragment is resumed. |
| */ |
| public boolean isControlsOverlayAutoHideEnabled() { |
| return mFadingEnabled; |
| } |
| |
| /** |
| * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)} |
| */ |
| @Deprecated |
| public void setFadingEnabled(boolean enabled) { |
| setControlsOverlayAutoHideEnabled(enabled); |
| } |
| |
| /** |
| * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()} |
| */ |
| @Deprecated |
| public boolean isFadingEnabled() { |
| return isControlsOverlayAutoHideEnabled(); |
| } |
| |
| /** |
| * Sets the listener to be called when fade in or out has completed. |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public void setFadeCompleteListener(OnFadeCompleteListener listener) { |
| mFadeCompleteListener = listener; |
| } |
| |
| /** |
| * Returns the listener to be called when fade in or out has completed. |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| public OnFadeCompleteListener getFadeCompleteListener() { |
| return mFadeCompleteListener; |
| } |
| |
| /** |
| * Sets the input event handler. |
| */ |
| public final void setOnKeyInterceptListener(View.OnKeyListener handler) { |
| mInputEventHandler = handler; |
| } |
| |
| /** |
| * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will |
| * kill and re-create a timer if {@link R.attr#playbackControlsAutoHideTickleTimeout} is |
| * positive. |
| * <p> |
| * In most cases app does not need call tickle() as it's automatically called on user |
| * interactions. |
| */ |
| public void tickle() { |
| if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed()); |
| //StateGraph 2->4 |
| stopFadeTimer(); |
| showControlsOverlay(true); |
| // Optionally start fading out timer if it's currently playing (mFadingEnabled is true) |
| if (mAutohideTimerAfterTickleInMs > 0 && mFadingEnabled) { |
| startFadeTimer(mAutohideTimerAfterTickleInMs); |
| } |
| } |
| |
| @SuppressWarnings("WeakerAccess") /* synthetic access */ |
| boolean onInterceptInputEvent(InputEvent event) { |
| final boolean controlsHidden = !mControlVisible; |
| if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event); |
| boolean consumeEvent = false; |
| int keyCode = KeyEvent.KEYCODE_UNKNOWN; |
| int keyAction = 0; |
| |
| if (event instanceof KeyEvent) { |
| keyCode = ((KeyEvent) event).getKeyCode(); |
| keyAction = ((KeyEvent) event).getAction(); |
| if (mInputEventHandler != null) { |
| consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event); |
| } |
| } |
| |
| switch (keyCode) { |
| case KeyEvent.KEYCODE_DPAD_CENTER: |
| case KeyEvent.KEYCODE_DPAD_DOWN: |
| case KeyEvent.KEYCODE_DPAD_UP: |
| case KeyEvent.KEYCODE_DPAD_LEFT: |
| case KeyEvent.KEYCODE_DPAD_RIGHT: |
| // Event may be consumed; regardless, if controls are hidden then these keys will |
| // bring up the controls. |
| if (controlsHidden) { |
| consumeEvent = true; |
| } |
| if (mShowOrHideControlsOverlayOnUserInteraction |
| && keyAction == KeyEvent.ACTION_DOWN) { |
| tickle(); |
| } |
| break; |
| case KeyEvent.KEYCODE_BACK: |
| case KeyEvent.KEYCODE_ESCAPE: |
| if (mInSeek) { |
| // when in seek, the SeekUi will handle the BACK. |
| return false; |
| } |
| // If controls are not hidden, back will be consumed to fade |
| // them out (even if the key was consumed by the handler). |
| if (mShowOrHideControlsOverlayOnUserInteraction && !controlsHidden) { |
| consumeEvent = true; |
| |
| if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) { |
| hideControlsOverlay(true); |
| } |
| } |
| break; |
| default: |
| if (mShowOrHideControlsOverlayOnUserInteraction && consumeEvent) { |
| if (keyAction == KeyEvent.ACTION_DOWN) { |
| tickle(); |
| } |
| } |
| } |
| return consumeEvent; |
| } |
| |
| @Override |
| public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { |
| super.onViewCreated(view, savedInstanceState); |
| // controls view are initially visible, make it invisible |
| // if app has called hideControlsOverlay() before view created. |
| mControlVisible = true; |
| if (!mControlVisibleBeforeOnCreateView) { |
| showControlsOverlay(false, false); |
| mControlVisibleBeforeOnCreateView = true; |
| } |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| |
| if (mControlVisible) { |
| //StateGraph: 6->5 1->2 |
| if (mFadingEnabled) { |
| // StateGraph 1->2 |
| startFadeTimer(mAutohideTimerAfterPlayingInMs); |
| } |
| } else { |
| //StateGraph: 6->7 1->3 |
| } |
| getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener); |
| getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener); |
| if (mHostCallback != null) { |
| mHostCallback.onHostResume(); |
| } |
| } |
| |
| private void stopFadeTimer() { |
| if (mHandler != null) { |
| mHandler.removeMessages(START_FADE_OUT); |
| } |
| } |
| |
| private void startFadeTimer(int fadeOutTimeout) { |
| if (mHandler != null) { |
| mHandler.removeMessages(START_FADE_OUT); |
| mHandler.sendEmptyMessageDelayed(START_FADE_OUT, fadeOutTimeout); |
| } |
| } |
| |
| private static ValueAnimator loadAnimator(Context context, int resId) { |
| ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId); |
| animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER); |
| return animator; |
| } |
| |
| private void loadBgAnimator() { |
| AnimatorUpdateListener listener = new AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator arg0) { |
| setBgAlpha((Integer) arg0.getAnimatedValue()); |
| } |
| }; |
| |
| Context context = getContext(); |
| mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in); |
| mBgFadeInAnimator.addUpdateListener(listener); |
| mBgFadeInAnimator.addListener(mFadeListener); |
| |
| mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out); |
| mBgFadeOutAnimator.addUpdateListener(listener); |
| mBgFadeOutAnimator.addListener(mFadeListener); |
| } |
| |
| private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0); |
| private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0); |
| |
| private void loadControlRowAnimator() { |
| final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator arg0) { |
| if (getVerticalGridView() == null) { |
| return; |
| } |
| RecyclerView.ViewHolder vh = getVerticalGridView() |
| .findViewHolderForAdapterPosition(0); |
| if (vh == null) { |
| return; |
| } |
| View view = vh.itemView; |
| if (view != null) { |
| final float fraction = (Float) arg0.getAnimatedValue(); |
| if (DEBUG) Log.v(TAG, "fraction " + fraction); |
| view.setAlpha(fraction); |
| view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); |
| } |
| } |
| }; |
| |
| Context context = getContext(); |
| mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); |
| mControlRowFadeInAnimator.addUpdateListener(updateListener); |
| mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); |
| |
| mControlRowFadeOutAnimator = loadAnimator(context, |
| R.animator.lb_playback_controls_fade_out); |
| mControlRowFadeOutAnimator.addUpdateListener(updateListener); |
| mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator); |
| } |
| |
| private void loadOtherRowAnimator() { |
| final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator arg0) { |
| if (getVerticalGridView() == null) { |
| return; |
| } |
| final float fraction = (Float) arg0.getAnimatedValue(); |
| final int count = getVerticalGridView().getChildCount(); |
| for (int i = 0; i < count; i++) { |
| View view = getVerticalGridView().getChildAt(i); |
| if (getVerticalGridView().getChildAdapterPosition(view) > 0) { |
| view.setAlpha(fraction); |
| view.setTranslationY((float) mAnimationTranslateY * (1f - fraction)); |
| } |
| } |
| } |
| }; |
| |
| Context context = getContext(); |
| mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in); |
| mOtherRowFadeInAnimator.addUpdateListener(updateListener); |
| mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator); |
| |
| mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out); |
| mOtherRowFadeOutAnimator.addUpdateListener(updateListener); |
| mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator()); |
| } |
| |
| /** |
| * Fades out the playback overlay immediately. |
| * @deprecated Call {@link #hideControlsOverlay(boolean)} |
| */ |
| @Deprecated |
| public void fadeOut() { |
| showControlsOverlay(false, false); |
| } |
| |
| /** |
| * Show controls overlay. |
| * |
| * @param runAnimation True to run animation, false otherwise. |
| */ |
| public void showControlsOverlay(boolean runAnimation) { |
| showControlsOverlay(true, runAnimation); |
| } |
| |
| /** |
| * Returns true if controls overlay is visible, false otherwise. |
| * |
| * @return True if controls overlay is visible, false otherwise. |
| * @see #showControlsOverlay(boolean) |
| * @see #hideControlsOverlay(boolean) |
| */ |
| public boolean isControlsOverlayVisible() { |
| return mControlVisible; |
| } |
| |
| /** |
| * Hide controls overlay. |
| * |
| * @param runAnimation True to run animation, false otherwise. |
| */ |
| public void hideControlsOverlay(boolean runAnimation) { |
| showControlsOverlay(false, runAnimation); |
| } |
| |
| /** |
| * if first animator is still running, reverse it; otherwise start second animator. |
| */ |
| static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second, |
| boolean runAnimation) { |
| if (first.isStarted()) { |
| first.reverse(); |
| if (!runAnimation) { |
| first.end(); |
| } |
| } else { |
| second.start(); |
| if (!runAnimation) { |
| second.end(); |
| } |
| } |
| } |
| |
| /** |
| * End first or second animator if they are still running. |
| */ |
| static void endAll(ValueAnimator first, ValueAnimator second) { |
| if (first.isStarted()) { |
| first.end(); |
| } else if (second.isStarted()) { |
| second.end(); |
| } |
| } |
| |
| /** |
| * Fade in or fade out rows and background. |
| * |
| * @param show True to fade in, false to fade out. |
| * @param animation True to run animation. |
| */ |
| void showControlsOverlay(boolean show, boolean animation) { |
| if (DEBUG) Log.v(TAG, "showControlsOverlay " + show); |
| if (getView() == null) { |
| mControlVisibleBeforeOnCreateView = show; |
| return; |
| } |
| // force no animation when fragment is not resumed |
| if (!isResumed()) { |
| animation = false; |
| } |
| if (show == mControlVisible) { |
| if (!animation) { |
| // End animation if needed |
| endAll(mBgFadeInAnimator, mBgFadeOutAnimator); |
| endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator); |
| endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator); |
| } |
| return; |
| } |
| // StateGraph: 7<->5 4<->3 2->3 |
| mControlVisible = show; |
| if (!mControlVisible) { |
| // StateGraph 2->3 |
| stopFadeTimer(); |
| } |
| |
| mAnimationTranslateY = (getVerticalGridView() == null |
| || getVerticalGridView().getSelectedPosition() == 0) |
| ? mMajorFadeTranslateY : mMinorFadeTranslateY; |
| |
| if (show) { |
| reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation); |
| reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator, |
| animation); |
| reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation); |
| } else { |
| reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation); |
| reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator, |
| animation); |
| reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation); |
| } |
| if (animation) { |
| getView().announceForAccessibility(getString(show |
| ? R.string.lb_playback_controls_shown |
| : R.string.lb_playback_controls_hidden)); |
| } |
| } |
| |
| /** |
| * Sets the selected row position with smooth animation. |
| */ |
| public void setSelectedPosition(int position) { |
| setSelectedPosition(position, true); |
| } |
| |
| /** |
| * Sets the selected row position. |
| */ |
| public void setSelectedPosition(int position, boolean smooth) { |
| mSetSelectionRunnable.mPosition = position; |
| mSetSelectionRunnable.mSmooth = smooth; |
| if (getView() != null && getView().getHandler() != null) { |
| getView().getHandler().post(mSetSelectionRunnable); |
| } |
| } |
| |
| private void setupChildFragmentLayout() { |
| setVerticalGridViewLayout(mRowsSupportFragment.getVerticalGridView()); |
| } |
| |
| void setVerticalGridViewLayout(VerticalGridView listview) { |
| if (listview == null) { |
| return; |
| } |
| |
| // we set the base line of alignment to -paddingBottom |
| listview.setWindowAlignmentOffset(-mPaddingBottom); |
| listview.setWindowAlignmentOffsetPercent( |
| VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); |
| |
| // align other rows that arent the last to center of screen, since our baseline is |
| // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom. |
| listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom); |
| listview.setItemAlignmentOffsetPercent(50); |
| |
| // Push last row to the bottom padding |
| // Padding affects alignment when last row is focused |
| listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(), |
| listview.getPaddingRight(), mPaddingBottom); |
| listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE); |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| mOtherRowsCenterToBottom = getResources() |
| .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom); |
| mPaddingBottom = |
| getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom); |
| mBgDarkColor = |
| getResources().getColor(R.color.lb_playback_controls_background_dark); |
| mBgLightColor = |
| getResources().getColor(R.color.lb_playback_controls_background_light); |
| TypedValue outValue = new TypedValue(); |
| getContext().getTheme().resolveAttribute( |
| R.attr.playbackControlsAutoHideTimeout, outValue, true); |
| mAutohideTimerAfterPlayingInMs = outValue.data; |
| getContext().getTheme().resolveAttribute( |
| R.attr.playbackControlsAutoHideTickleTimeout, outValue, true); |
| mAutohideTimerAfterTickleInMs = outValue.data; |
| mMajorFadeTranslateY = |
| getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y); |
| mMinorFadeTranslateY = |
| getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y); |
| |
| loadBgAnimator(); |
| loadControlRowAnimator(); |
| loadOtherRowAnimator(); |
| } |
| |
| /** |
| * Sets the background type. |
| * |
| * @param type One of BG_LIGHT, BG_DARK, or BG_NONE. |
| */ |
| public void setBackgroundType(int type) { |
| switch (type) { |
| case BG_LIGHT: |
| case BG_DARK: |
| case BG_NONE: |
| if (type != mBackgroundType) { |
| mBackgroundType = type; |
| updateBackground(); |
| } |
| break; |
| default: |
| throw new IllegalArgumentException("Invalid background type"); |
| } |
| } |
| |
| /** |
| * Returns the background type. |
| */ |
| public int getBackgroundType() { |
| return mBackgroundType; |
| } |
| |
| private void updateBackground() { |
| if (mBackgroundView != null) { |
| int color = mBgDarkColor; |
| switch (mBackgroundType) { |
| case BG_DARK: |
| break; |
| case BG_LIGHT: |
| color = mBgLightColor; |
| break; |
| case BG_NONE: |
| color = Color.TRANSPARENT; |
| break; |
| } |
| mBackgroundView.setBackground(new ColorDrawable(color)); |
| setBgAlpha(mBgAlpha); |
| } |
| } |
| |
| private final ItemBridgeAdapter.AdapterListener mAdapterListener = |
| new ItemBridgeAdapter.AdapterListener() { |
| @Override |
| public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) { |
| if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view); |
| if (!mControlVisible) { |
| if (DEBUG) Log.v(TAG, "setting alpha to 0"); |
| vh.getViewHolder().view.setAlpha(0); |
| } |
| } |
| |
| @Override |
| public void onCreate(ItemBridgeAdapter.ViewHolder vh) { |
| Presenter.ViewHolder viewHolder = vh.getViewHolder(); |
| if (viewHolder instanceof PlaybackSeekUi) { |
| ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient); |
| } |
| } |
| |
| @Override |
| public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) { |
| if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view); |
| // Reset animation state |
| vh.getViewHolder().view.setAlpha(1f); |
| vh.getViewHolder().view.setTranslationY(0); |
| vh.getViewHolder().view.setAlpha(1f); |
| } |
| |
| @Override |
| public void onBind(ItemBridgeAdapter.ViewHolder vh) { |
| } |
| }; |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false); |
| mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background); |
| mRowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById( |
| R.id.playback_controls_dock); |
| if (mRowsSupportFragment == null) { |
| mRowsSupportFragment = new RowsSupportFragment(); |
| getChildFragmentManager().beginTransaction() |
| .replace(R.id.playback_controls_dock, mRowsSupportFragment) |
| .commit(); |
| } |
| if (mAdapter == null) { |
| setAdapter(new ArrayObjectAdapter(new ClassPresenterSelector())); |
| } else { |
| mRowsSupportFragment.setAdapter(mAdapter); |
| } |
| mRowsSupportFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener); |
| mRowsSupportFragment.setOnItemViewClickedListener(mOnItemViewClickedListener); |
| |
| mBgAlpha = 255; |
| updateBackground(); |
| mRowsSupportFragment.setExternalAdapterListener(mAdapterListener); |
| ProgressBarManager progressBarManager = getProgressBarManager(); |
| if (progressBarManager != null) { |
| progressBarManager.setRootView((ViewGroup) mRootView); |
| } |
| return mRootView; |
| } |
| |
| /** |
| * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will |
| * take appropriate actions to take action when the hosting fragment starts/stops processing. |
| */ |
| public void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) { |
| this.mHostCallback = hostCallback; |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| setupChildFragmentLayout(); |
| mRowsSupportFragment.setAdapter(mAdapter); |
| if (mHostCallback != null) { |
| mHostCallback.onHostStart(); |
| } |
| } |
| |
| @Override |
| public void onStop() { |
| if (mHostCallback != null) { |
| mHostCallback.onHostStop(); |
| } |
| super.onStop(); |
| } |
| |
| @Override |
| public void onPause() { |
| if (mHostCallback != null) { |
| mHostCallback.onHostPause(); |
| } |
| if (mHandler.hasMessages(START_FADE_OUT)) { |
| // StateGraph: 2->1 |
| mHandler.removeMessages(START_FADE_OUT); |
| } else { |
| // StateGraph: 5->6, 7->6, 4->1, 3->1 |
| } |
| super.onPause(); |
| } |
| |
| /** |
| * This listener is called every time there is a selection in {@link RowsSupportFragment}. This can |
| * be used by users to take additional actions such as animations. |
| */ |
| public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) { |
| mExternalItemSelectedListener = listener; |
| } |
| |
| /** |
| * This listener is called every time there is a click in {@link RowsSupportFragment}. This can |
| * be used by users to take additional actions such as animations. |
| */ |
| public void setOnItemViewClickedListener(final BaseOnItemViewClickedListener listener) { |
| mExternalItemClickedListener = listener; |
| } |
| |
| /** |
| * Sets the {@link BaseOnItemViewClickedListener} that would be invoked for clicks |
| * only on {@link androidx.leanback.widget.PlaybackRowPresenter.ViewHolder}. |
| */ |
| public void setOnPlaybackItemViewClickedListener(final BaseOnItemViewClickedListener listener) { |
| mPlaybackItemClickedListener = listener; |
| } |
| |
| @Override |
| public void onDestroyView() { |
| mRootView = null; |
| mBackgroundView = null; |
| super.onDestroyView(); |
| } |
| |
| @Override |
| public void onDestroy() { |
| if (mHostCallback != null) { |
| mHostCallback.onHostDestroy(); |
| } |
| super.onDestroy(); |
| } |
| |
| /** |
| * Sets the playback row for the playback controls. The row will be set as first element |
| * of adapter if the adapter is {@link ArrayObjectAdapter} or {@link SparseArrayObjectAdapter}. |
| * @param row The row that represents the playback. |
| */ |
| public void setPlaybackRow(Row row) { |
| this.mRow = row; |
| setupRow(); |
| setupPresenter(); |
| } |
| |
| /** |
| * Sets the presenter for rendering the playback row set by {@link #setPlaybackRow(Row)}. If |
| * adapter does not set a {@link PresenterSelector}, {@link #setAdapter(ObjectAdapter)} will |
| * create a {@link ClassPresenterSelector} by default and map from the row object class to this |
| * {@link PlaybackRowPresenter}. |
| * |
| * @param presenter Presenter used to render {@link #setPlaybackRow(Row)}. |
| */ |
| public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) { |
| this.mPresenter = presenter; |
| setupPresenter(); |
| setPlaybackRowPresenterAlignment(); |
| } |
| |
| void setPlaybackRowPresenterAlignment() { |
| if (mAdapter != null && mAdapter.getPresenterSelector() != null) { |
| Presenter[] presenters = mAdapter.getPresenterSelector().getPresenters(); |
| if (presenters != null) { |
| for (int i = 0; i < presenters.length; i++) { |
| if (presenters[i] instanceof PlaybackRowPresenter |
| && presenters[i].getFacet(ItemAlignmentFacet.class) == null) { |
| ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet(); |
| ItemAlignmentFacet.ItemAlignmentDef def = |
| new ItemAlignmentFacet.ItemAlignmentDef(); |
| def.setItemAlignmentOffset(0); |
| def.setItemAlignmentOffsetPercent(100); |
| itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[] |
| {def}); |
| presenters[i].setFacet(ItemAlignmentFacet.class, itemAlignment); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Updates the ui when the row data changes. |
| */ |
| public void notifyPlaybackRowChanged() { |
| if (mAdapter == null) { |
| return; |
| } |
| mAdapter.notifyItemRangeChanged(0, 1); |
| } |
| |
| /** |
| * Sets the list of rows for the fragment. A default {@link ClassPresenterSelector} will be |
| * created if {@link ObjectAdapter#getPresenterSelector()} is null. if user provides |
| * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)}, |
| * the row and presenter will be set onto the adapter. |
| * |
| * @param adapter The adapter that contains related rows and optional playback row. |
| */ |
| public void setAdapter(ObjectAdapter adapter) { |
| mAdapter = adapter; |
| setupRow(); |
| setupPresenter(); |
| setPlaybackRowPresenterAlignment(); |
| |
| if (mRowsSupportFragment != null) { |
| mRowsSupportFragment.setAdapter(adapter); |
| } |
| } |
| |
| private void setupRow() { |
| if (mAdapter instanceof ArrayObjectAdapter && mRow != null) { |
| ArrayObjectAdapter adapter = ((ArrayObjectAdapter) mAdapter); |
| if (adapter.size() == 0) { |
| adapter.add(mRow); |
| } else { |
| adapter.replace(0, mRow); |
| } |
| } else if (mAdapter instanceof SparseArrayObjectAdapter && mRow != null) { |
| SparseArrayObjectAdapter adapter = ((SparseArrayObjectAdapter) mAdapter); |
| adapter.set(0, mRow); |
| } |
| } |
| |
| private void setupPresenter() { |
| if (mAdapter != null && mRow != null && mPresenter != null) { |
| PresenterSelector selector = mAdapter.getPresenterSelector(); |
| if (selector == null) { |
| selector = new ClassPresenterSelector(); |
| ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter); |
| mAdapter.setPresenterSelector(selector); |
| } else if (selector instanceof ClassPresenterSelector) { |
| ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter); |
| } |
| } |
| } |
| |
| final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() { |
| @Override |
| public boolean isSeekEnabled() { |
| return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled(); |
| } |
| |
| @Override |
| public void onSeekStarted() { |
| if (mSeekUiClient != null) { |
| mSeekUiClient.onSeekStarted(); |
| } |
| setSeekMode(true); |
| } |
| |
| @Override |
| public PlaybackSeekDataProvider getPlaybackSeekDataProvider() { |
| return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider(); |
| } |
| |
| @Override |
| public void onSeekPositionChanged(long pos) { |
| if (mSeekUiClient != null) { |
| mSeekUiClient.onSeekPositionChanged(pos); |
| } |
| } |
| |
| @Override |
| public void onSeekFinished(boolean cancelled) { |
| if (mSeekUiClient != null) { |
| mSeekUiClient.onSeekFinished(cancelled); |
| } |
| setSeekMode(false); |
| } |
| }; |
| |
| /** |
| * Interface to be implemented by UI widget to support PlaybackSeekUi. |
| */ |
| public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) { |
| mSeekUiClient = client; |
| } |
| |
| /** |
| * Show or hide other rows other than PlaybackRow. |
| * @param inSeek True to make other rows visible, false to make other rows invisible. |
| */ |
| void setSeekMode(boolean inSeek) { |
| if (mInSeek == inSeek) { |
| return; |
| } |
| mInSeek = inSeek; |
| getVerticalGridView().setSelectedPosition(0); |
| if (mInSeek) { |
| stopFadeTimer(); |
| } |
| // immediately fade in control row. |
| showControlsOverlay(true); |
| final int count = getVerticalGridView().getChildCount(); |
| for (int i = 0; i < count; i++) { |
| View view = getVerticalGridView().getChildAt(i); |
| if (getVerticalGridView().getChildAdapterPosition(view) > 0) { |
| view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE); |
| } |
| } |
| } |
| |
| /** |
| * Called when size of the video changes. App may override. |
| * @param videoWidth Intrinsic width of video |
| * @param videoHeight Intrinsic height of video |
| */ |
| protected void onVideoSizeChanged(int videoWidth, int videoHeight) { |
| } |
| |
| /** |
| * Called when media has start or stop buffering. App may override. The default initial state |
| * is not buffering. |
| * @param start True for buffering start, false otherwise. |
| */ |
| protected void onBufferingStateChanged(boolean start) { |
| ProgressBarManager progressBarManager = getProgressBarManager(); |
| if (progressBarManager != null) { |
| if (start) { |
| progressBarManager.show(); |
| } else { |
| progressBarManager.hide(); |
| } |
| } |
| } |
| |
| /** |
| * Called when media has error. App may override. |
| * @param errorCode Optional error code for specific implementation. |
| * @param errorMessage Optional error message for specific implementation. |
| */ |
| protected void onError(int errorCode, CharSequence errorMessage) { |
| } |
| |
| /** |
| * Returns the ProgressBarManager that will show or hide progress bar in |
| * {@link #onBufferingStateChanged(boolean)}. |
| * @return The ProgressBarManager that will show or hide progress bar in |
| * {@link #onBufferingStateChanged(boolean)}. |
| */ |
| public ProgressBarManager getProgressBarManager() { |
| return mProgressBarManager; |
| } |
| } |