/* * 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 android.support.v17.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.support.v17.leanback.R; import android.support.v17.leanback.animation.LogAccelerateInterpolator; import android.support.v17.leanback.animation.LogDecelerateInterpolator; import android.support.v17.leanback.media.PlaybackGlueHost; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.BaseOnItemViewClickedListener; import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.ItemAlignmentFacet; import android.support.v17.leanback.widget.ItemBridgeAdapter; import android.support.v17.leanback.widget.ObjectAdapter; import android.support.v17.leanback.widget.PlaybackRowPresenter; import android.support.v17.leanback.widget.PlaybackSeekDataProvider; import android.support.v17.leanback.widget.PlaybackSeekUi; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.PresenterSelector; import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.RowPresenter; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.support.v17.leanback.widget.VerticalGridView; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; import android.util.Log; 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; /** * A fragment for displaying playback controls and related content. * *

* 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}. *

*

* 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)}. *

*

* Auto hide controls upon playing: best practice is calling * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will * be cancelled upon {@link #tickle()} triggered by input event. *

*/ 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 */ 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; @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 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 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 */ 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 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 mShowTimeMs; int mMajorFadeTranslateY, mMinorFadeTranslateY; int mAnimationTranslateY; OnFadeCompleteListener mFadeCompleteListener; View.OnKeyListener mInputEventHandler; boolean mFadingEnabled = true; boolean mControlVisibleBeforeOnCreateView = true; boolean mControlVisible = 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); } }; private void setBgAlpha(int alpha) { mBgAlpha = alpha; if (mBackgroundView != null) { mBackgroundView.getBackground().setAlpha(alpha); } } private void enableVerticalGridAnimations(boolean enable) { if (getVerticalGridView() != null) { getVerticalGridView().setAnimateChildLayout(enable); } } /** * 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. * {@link #tickle()} will kill the timer, next time fragment is resumed, * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true. */ 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(); } 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 */ public void setFadeCompleteListener(OnFadeCompleteListener listener) { mFadeCompleteListener = listener; } /** * Returns the listener to be called when fade in or out has completed. * @hide */ 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 * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When * next time fragment is resumed, the timer will be started again if * {@link #isControlsOverlayAutoHideEnabled()} is true. In most cases app does not need call * this method, tickling on input events is handled by the fragment. */ public void tickle() { if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed()); //StateGraph 2->4 stopFadeTimer(); showControlsOverlay(true); } private 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 (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 (!controlsHidden) { consumeEvent = true; if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) { hideControlsOverlay(true); } } break; default: if (consumeEvent) { if (keyAction == KeyEvent.ACTION_DOWN) { tickle(); } } } return consumeEvent; } @Override public void onViewCreated(View view, 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(); } } 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() { if (mHandler != null) { mHandler.removeMessages(START_FADE_OUT); mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs); } } 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); mShowTimeMs = getResources().getInteger(R.integer.lb_playback_controls_show_time_ms); 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 android.support.v17.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; } }