Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
      5  * in compliance with the License. You may obtain a copy of the License at
      6  *
      7  * http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the License
     10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
     11  * or implied. See the License for the specific language governing permissions and limitations under
     12  * the License.
     13  */
     14 package androidx.leanback.app;
     15 
     16 import android.animation.Animator;
     17 import android.animation.AnimatorInflater;
     18 import android.animation.TimeInterpolator;
     19 import android.animation.ValueAnimator;
     20 import android.animation.ValueAnimator.AnimatorUpdateListener;
     21 import android.content.Context;
     22 import android.graphics.Color;
     23 import android.graphics.drawable.ColorDrawable;
     24 import android.os.Bundle;
     25 import android.os.Handler;
     26 import android.os.Message;
     27 import android.util.Log;
     28 import android.view.InputEvent;
     29 import android.view.KeyEvent;
     30 import android.view.LayoutInflater;
     31 import android.view.MotionEvent;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 import android.view.animation.AccelerateInterpolator;
     35 
     36 import androidx.annotation.NonNull;
     37 import androidx.annotation.Nullable;
     38 import androidx.annotation.RestrictTo;
     39 import androidx.fragment.app.Fragment;
     40 import androidx.leanback.R;
     41 import androidx.leanback.animation.LogAccelerateInterpolator;
     42 import androidx.leanback.animation.LogDecelerateInterpolator;
     43 import androidx.leanback.media.PlaybackGlueHost;
     44 import androidx.leanback.widget.ArrayObjectAdapter;
     45 import androidx.leanback.widget.BaseOnItemViewClickedListener;
     46 import androidx.leanback.widget.BaseOnItemViewSelectedListener;
     47 import androidx.leanback.widget.ClassPresenterSelector;
     48 import androidx.leanback.widget.ItemAlignmentFacet;
     49 import androidx.leanback.widget.ItemBridgeAdapter;
     50 import androidx.leanback.widget.ObjectAdapter;
     51 import androidx.leanback.widget.PlaybackRowPresenter;
     52 import androidx.leanback.widget.PlaybackSeekDataProvider;
     53 import androidx.leanback.widget.PlaybackSeekUi;
     54 import androidx.leanback.widget.Presenter;
     55 import androidx.leanback.widget.PresenterSelector;
     56 import androidx.leanback.widget.Row;
     57 import androidx.leanback.widget.RowPresenter;
     58 import androidx.leanback.widget.SparseArrayObjectAdapter;
     59 import androidx.leanback.widget.VerticalGridView;
     60 import androidx.recyclerview.widget.RecyclerView;
     61 
     62 /**
     63  * A fragment for displaying playback controls and related content.
     64  *
     65  * <p>
     66  * A PlaybackSupportFragment renders the elements of its {@link ObjectAdapter} as a set
     67  * of rows in a vertical list.  The Adapter's {@link PresenterSelector} must maintain subclasses
     68  * of {@link RowPresenter}.
     69  * </p>
     70  * <p>
     71  * A playback row is a row rendered by {@link PlaybackRowPresenter}.
     72  * App can call {@link #setPlaybackRow(Row)} to set playback row for the first element of adapter.
     73  * App can call {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to set presenter for it.
     74  * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} are
     75  * optional, app can pass playback row and PlaybackRowPresenter in the adapter using
     76  * {@link #setAdapter(ObjectAdapter)}.
     77  * </p>
     78  * <p>
     79  * Auto hide controls upon playing: best practice is calling
     80  * {@link #setControlsOverlayAutoHideEnabled(boolean)} upon play/pause. The auto hiding timer will
     81  * be cancelled upon {@link #tickle()} triggered by input event.
     82  * </p>
     83  */
     84 public class PlaybackSupportFragment extends Fragment {
     85     static final String BUNDLE_CONTROL_VISIBLE_ON_CREATEVIEW = "controlvisible_oncreateview";
     86 
     87     /**
     88      * No background.
     89      */
     90     public static final int BG_NONE = 0;
     91 
     92     /**
     93      * A dark translucent background.
     94      */
     95     public static final int BG_DARK = 1;
     96     PlaybackGlueHost.HostCallback mHostCallback;
     97 
     98     PlaybackSeekUi.Client mSeekUiClient;
     99     boolean mInSeek;
    100     ProgressBarManager mProgressBarManager = new ProgressBarManager();
    101 
    102     /**
    103      * Resets the focus on the button in the middle of control row.
    104      * @hide
    105      */
    106     @RestrictTo(RestrictTo.Scope.LIBRARY)
    107     public void resetFocus() {
    108         ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
    109                 .findViewHolderForAdapterPosition(0);
    110         if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {
    111             ((PlaybackRowPresenter) vh.getPresenter()).onReappear(
    112                     (RowPresenter.ViewHolder) vh.getViewHolder());
    113         }
    114     }
    115 
    116     private class SetSelectionRunnable implements Runnable {
    117         int mPosition;
    118         boolean mSmooth = true;
    119 
    120         @Override
    121         public void run() {
    122             if (mRowsSupportFragment == null) {
    123                 return;
    124             }
    125             mRowsSupportFragment.setSelectedPosition(mPosition, mSmooth);
    126         }
    127     }
    128 
    129     /**
    130      * A light translucent background.
    131      */
    132     public static final int BG_LIGHT = 2;
    133     RowsSupportFragment mRowsSupportFragment;
    134     ObjectAdapter mAdapter;
    135     PlaybackRowPresenter mPresenter;
    136     Row mRow;
    137     BaseOnItemViewSelectedListener mExternalItemSelectedListener;
    138     BaseOnItemViewClickedListener mExternalItemClickedListener;
    139     BaseOnItemViewClickedListener mPlaybackItemClickedListener;
    140 
    141     private final BaseOnItemViewClickedListener mOnItemViewClickedListener =
    142             new BaseOnItemViewClickedListener() {
    143                 @Override
    144                 public void onItemClicked(Presenter.ViewHolder itemViewHolder,
    145                                           Object item,
    146                                           RowPresenter.ViewHolder rowViewHolder,
    147                                           Object row) {
    148                     if (mPlaybackItemClickedListener != null
    149                             && rowViewHolder instanceof PlaybackRowPresenter.ViewHolder) {
    150                         mPlaybackItemClickedListener.onItemClicked(
    151                                 itemViewHolder, item, rowViewHolder, row);
    152                     }
    153                     if (mExternalItemClickedListener != null) {
    154                         mExternalItemClickedListener.onItemClicked(
    155                                 itemViewHolder, item, rowViewHolder, row);
    156                     }
    157                 }
    158             };
    159 
    160     private final BaseOnItemViewSelectedListener mOnItemViewSelectedListener =
    161             new BaseOnItemViewSelectedListener() {
    162                 @Override
    163                 public void onItemSelected(Presenter.ViewHolder itemViewHolder,
    164                                            Object item,
    165                                            RowPresenter.ViewHolder rowViewHolder,
    166                                            Object row) {
    167                     if (mExternalItemSelectedListener != null) {
    168                         mExternalItemSelectedListener.onItemSelected(
    169                                 itemViewHolder, item, rowViewHolder, row);
    170                     }
    171                 }
    172             };
    173 
    174     private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
    175 
    176     public ObjectAdapter getAdapter() {
    177         return mAdapter;
    178     }
    179 
    180     /**
    181      * Listener allowing the application to receive notification of fade in and/or fade out
    182      * completion events.
    183      * @hide
    184      */
    185     @RestrictTo(RestrictTo.Scope.LIBRARY)
    186     public static class OnFadeCompleteListener {
    187         public void onFadeInComplete() {
    188         }
    189 
    190         public void onFadeOutComplete() {
    191         }
    192     }
    193 
    194     private static final String TAG = "PlaybackSupportFragment";
    195     private static final boolean DEBUG = false;
    196     private static final int ANIMATION_MULTIPLIER = 1;
    197 
    198     private static final int START_FADE_OUT = 1;
    199 
    200     // Fading status
    201     private static final int IDLE = 0;
    202     private static final int ANIMATING = 1;
    203 
    204     int mPaddingBottom;
    205     int mOtherRowsCenterToBottom;
    206     View mRootView;
    207     View mBackgroundView;
    208     int mBackgroundType = BG_DARK;
    209     int mBgDarkColor;
    210     int mBgLightColor;
    211     int mShowTimeMs;
    212     int mMajorFadeTranslateY, mMinorFadeTranslateY;
    213     int mAnimationTranslateY;
    214     OnFadeCompleteListener mFadeCompleteListener;
    215     View.OnKeyListener mInputEventHandler;
    216     boolean mFadingEnabled = true;
    217     boolean mControlVisibleBeforeOnCreateView = true;
    218     boolean mControlVisible = true;
    219     int mBgAlpha;
    220     ValueAnimator mBgFadeInAnimator, mBgFadeOutAnimator;
    221     ValueAnimator mControlRowFadeInAnimator, mControlRowFadeOutAnimator;
    222     ValueAnimator mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator;
    223 
    224     private final Animator.AnimatorListener mFadeListener =
    225             new Animator.AnimatorListener() {
    226                 @Override
    227                 public void onAnimationStart(Animator animation) {
    228                     enableVerticalGridAnimations(false);
    229                 }
    230 
    231                 @Override
    232                 public void onAnimationRepeat(Animator animation) {
    233                 }
    234 
    235                 @Override
    236                 public void onAnimationCancel(Animator animation) {
    237                 }
    238 
    239                 @Override
    240                 public void onAnimationEnd(Animator animation) {
    241                     if (DEBUG) Log.v(TAG, "onAnimationEnd " + mBgAlpha);
    242                     if (mBgAlpha > 0) {
    243                         enableVerticalGridAnimations(true);
    244                         if (mFadeCompleteListener != null) {
    245                             mFadeCompleteListener.onFadeInComplete();
    246                         }
    247                     } else {
    248                         VerticalGridView verticalView = getVerticalGridView();
    249                         // reset focus to the primary actions only if the selected row was the controls row
    250                         if (verticalView != null && verticalView.getSelectedPosition() == 0) {
    251                             ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
    252                                     verticalView.findViewHolderForAdapterPosition(0);
    253                             if (vh != null && vh.getPresenter() instanceof PlaybackRowPresenter) {
    254                                 ((PlaybackRowPresenter)vh.getPresenter()).onReappear(
    255                                         (RowPresenter.ViewHolder) vh.getViewHolder());
    256                             }
    257                         }
    258                         if (mFadeCompleteListener != null) {
    259                             mFadeCompleteListener.onFadeOutComplete();
    260                         }
    261                     }
    262                 }
    263             };
    264 
    265     public PlaybackSupportFragment() {
    266         mProgressBarManager.setInitialDelay(500);
    267     }
    268 
    269     VerticalGridView getVerticalGridView() {
    270         if (mRowsSupportFragment == null) {
    271             return null;
    272         }
    273         return mRowsSupportFragment.getVerticalGridView();
    274     }
    275 
    276     private final Handler mHandler = new Handler() {
    277         @Override
    278         public void handleMessage(Message message) {
    279             if (message.what == START_FADE_OUT && mFadingEnabled) {
    280                 hideControlsOverlay(true);
    281             }
    282         }
    283     };
    284 
    285     private final VerticalGridView.OnTouchInterceptListener mOnTouchInterceptListener =
    286             new VerticalGridView.OnTouchInterceptListener() {
    287                 @Override
    288                 public boolean onInterceptTouchEvent(MotionEvent event) {
    289                     return onInterceptInputEvent(event);
    290                 }
    291             };
    292 
    293     private final VerticalGridView.OnKeyInterceptListener mOnKeyInterceptListener =
    294             new VerticalGridView.OnKeyInterceptListener() {
    295                 @Override
    296                 public boolean onInterceptKeyEvent(KeyEvent event) {
    297                     return onInterceptInputEvent(event);
    298                 }
    299             };
    300 
    301     private void setBgAlpha(int alpha) {
    302         mBgAlpha = alpha;
    303         if (mBackgroundView != null) {
    304             mBackgroundView.getBackground().setAlpha(alpha);
    305         }
    306     }
    307 
    308     private void enableVerticalGridAnimations(boolean enable) {
    309         if (getVerticalGridView() != null) {
    310             getVerticalGridView().setAnimateChildLayout(enable);
    311         }
    312     }
    313 
    314     /**
    315      * Enables or disables auto hiding controls overlay after a short delay fragment is resumed.
    316      * If enabled and fragment is resumed, the view will fade out after a time period.
    317      * {@link #tickle()} will kill the timer, next time fragment is resumed,
    318      * the timer will be started again if {@link #isControlsOverlayAutoHideEnabled()} is true.
    319      */
    320     public void setControlsOverlayAutoHideEnabled(boolean enabled) {
    321         if (DEBUG) Log.v(TAG, "setControlsOverlayAutoHideEnabled " + enabled);
    322         if (enabled != mFadingEnabled) {
    323             mFadingEnabled = enabled;
    324             if (isResumed() && getView().hasFocus()) {
    325                 showControlsOverlay(true);
    326                 if (enabled) {
    327                     // StateGraph 7->2 5->2
    328                     startFadeTimer();
    329                 } else {
    330                     // StateGraph 4->5 2->5
    331                     stopFadeTimer();
    332                 }
    333             } else {
    334                 // StateGraph 6->1 1->6
    335             }
    336         }
    337     }
    338 
    339     /**
    340      * Returns true if controls will be auto hidden after a delay when fragment is resumed.
    341      */
    342     public boolean isControlsOverlayAutoHideEnabled() {
    343         return mFadingEnabled;
    344     }
    345 
    346     /**
    347      * @deprecated Uses {@link #setControlsOverlayAutoHideEnabled(boolean)}
    348      */
    349     @Deprecated
    350     public void setFadingEnabled(boolean enabled) {
    351         setControlsOverlayAutoHideEnabled(enabled);
    352     }
    353 
    354     /**
    355      * @deprecated Uses {@link #isControlsOverlayAutoHideEnabled()}
    356      */
    357     @Deprecated
    358     public boolean isFadingEnabled() {
    359         return isControlsOverlayAutoHideEnabled();
    360     }
    361 
    362     /**
    363      * Sets the listener to be called when fade in or out has completed.
    364      * @hide
    365      */
    366     @RestrictTo(RestrictTo.Scope.LIBRARY)
    367     public void setFadeCompleteListener(OnFadeCompleteListener listener) {
    368         mFadeCompleteListener = listener;
    369     }
    370 
    371     /**
    372      * Returns the listener to be called when fade in or out has completed.
    373      * @hide
    374      */
    375     @RestrictTo(RestrictTo.Scope.LIBRARY)
    376     public OnFadeCompleteListener getFadeCompleteListener() {
    377         return mFadeCompleteListener;
    378     }
    379 
    380     /**
    381      * Sets the input event handler.
    382      */
    383     public final void setOnKeyInterceptListener(View.OnKeyListener handler) {
    384         mInputEventHandler = handler;
    385     }
    386 
    387     /**
    388      * Tickles the playback controls. Fades in the view if it was faded out. {@link #tickle()} will
    389      * also kill the timer created by {@link #setControlsOverlayAutoHideEnabled(boolean)}. When
    390      * next time fragment is resumed, the timer will be started again if
    391      * {@link #isControlsOverlayAutoHideEnabled()} is true. In most cases app does not need call
    392      * this method, tickling on input events is handled by the fragment.
    393      */
    394     public void tickle() {
    395         if (DEBUG) Log.v(TAG, "tickle enabled " + mFadingEnabled + " isResumed " + isResumed());
    396         //StateGraph 2->4
    397         stopFadeTimer();
    398         showControlsOverlay(true);
    399     }
    400 
    401     private boolean onInterceptInputEvent(InputEvent event) {
    402         final boolean controlsHidden = !mControlVisible;
    403         if (DEBUG) Log.v(TAG, "onInterceptInputEvent hidden " + controlsHidden + " " + event);
    404         boolean consumeEvent = false;
    405         int keyCode = KeyEvent.KEYCODE_UNKNOWN;
    406         int keyAction = 0;
    407 
    408         if (event instanceof KeyEvent) {
    409             keyCode = ((KeyEvent) event).getKeyCode();
    410             keyAction = ((KeyEvent) event).getAction();
    411             if (mInputEventHandler != null) {
    412                 consumeEvent = mInputEventHandler.onKey(getView(), keyCode, (KeyEvent) event);
    413             }
    414         }
    415 
    416         switch (keyCode) {
    417             case KeyEvent.KEYCODE_DPAD_CENTER:
    418             case KeyEvent.KEYCODE_DPAD_DOWN:
    419             case KeyEvent.KEYCODE_DPAD_UP:
    420             case KeyEvent.KEYCODE_DPAD_LEFT:
    421             case KeyEvent.KEYCODE_DPAD_RIGHT:
    422                 // Event may be consumed; regardless, if controls are hidden then these keys will
    423                 // bring up the controls.
    424                 if (controlsHidden) {
    425                     consumeEvent = true;
    426                 }
    427                 if (keyAction == KeyEvent.ACTION_DOWN) {
    428                     tickle();
    429                 }
    430                 break;
    431             case KeyEvent.KEYCODE_BACK:
    432             case KeyEvent.KEYCODE_ESCAPE:
    433                 if (mInSeek) {
    434                     // when in seek, the SeekUi will handle the BACK.
    435                     return false;
    436                 }
    437                 // If controls are not hidden, back will be consumed to fade
    438                 // them out (even if the key was consumed by the handler).
    439                 if (!controlsHidden) {
    440                     consumeEvent = true;
    441 
    442                     if (((KeyEvent) event).getAction() == KeyEvent.ACTION_UP) {
    443                         hideControlsOverlay(true);
    444                     }
    445                 }
    446                 break;
    447             default:
    448                 if (consumeEvent) {
    449                     if (keyAction == KeyEvent.ACTION_DOWN) {
    450                         tickle();
    451                     }
    452                 }
    453         }
    454         return consumeEvent;
    455     }
    456 
    457     @Override
    458     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    459         super.onViewCreated(view, savedInstanceState);
    460         // controls view are initially visible, make it invisible
    461         // if app has called hideControlsOverlay() before view created.
    462         mControlVisible = true;
    463         if (!mControlVisibleBeforeOnCreateView) {
    464             showControlsOverlay(false, false);
    465             mControlVisibleBeforeOnCreateView = true;
    466         }
    467     }
    468 
    469     @Override
    470     public void onResume() {
    471         super.onResume();
    472 
    473         if (mControlVisible) {
    474             //StateGraph: 6->5 1->2
    475             if (mFadingEnabled) {
    476                 // StateGraph 1->2
    477                 startFadeTimer();
    478             }
    479         } else {
    480             //StateGraph: 6->7 1->3
    481         }
    482         getVerticalGridView().setOnTouchInterceptListener(mOnTouchInterceptListener);
    483         getVerticalGridView().setOnKeyInterceptListener(mOnKeyInterceptListener);
    484         if (mHostCallback != null) {
    485             mHostCallback.onHostResume();
    486         }
    487     }
    488 
    489     private void stopFadeTimer() {
    490         if (mHandler != null) {
    491             mHandler.removeMessages(START_FADE_OUT);
    492         }
    493     }
    494 
    495     private void startFadeTimer() {
    496         if (mHandler != null) {
    497             mHandler.removeMessages(START_FADE_OUT);
    498             mHandler.sendEmptyMessageDelayed(START_FADE_OUT, mShowTimeMs);
    499         }
    500     }
    501 
    502     private static ValueAnimator loadAnimator(Context context, int resId) {
    503         ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(context, resId);
    504         animator.setDuration(animator.getDuration() * ANIMATION_MULTIPLIER);
    505         return animator;
    506     }
    507 
    508     private void loadBgAnimator() {
    509         AnimatorUpdateListener listener = new AnimatorUpdateListener() {
    510             @Override
    511             public void onAnimationUpdate(ValueAnimator arg0) {
    512                 setBgAlpha((Integer) arg0.getAnimatedValue());
    513             }
    514         };
    515 
    516         Context context = getContext();
    517         mBgFadeInAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_in);
    518         mBgFadeInAnimator.addUpdateListener(listener);
    519         mBgFadeInAnimator.addListener(mFadeListener);
    520 
    521         mBgFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_bg_fade_out);
    522         mBgFadeOutAnimator.addUpdateListener(listener);
    523         mBgFadeOutAnimator.addListener(mFadeListener);
    524     }
    525 
    526     private TimeInterpolator mLogDecelerateInterpolator = new LogDecelerateInterpolator(100, 0);
    527     private TimeInterpolator mLogAccelerateInterpolator = new LogAccelerateInterpolator(100, 0);
    528 
    529     private void loadControlRowAnimator() {
    530         final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
    531             @Override
    532             public void onAnimationUpdate(ValueAnimator arg0) {
    533                 if (getVerticalGridView() == null) {
    534                     return;
    535                 }
    536                 RecyclerView.ViewHolder vh = getVerticalGridView()
    537                         .findViewHolderForAdapterPosition(0);
    538                 if (vh == null) {
    539                     return;
    540                 }
    541                 View view = vh.itemView;
    542                 if (view != null) {
    543                     final float fraction = (Float) arg0.getAnimatedValue();
    544                     if (DEBUG) Log.v(TAG, "fraction " + fraction);
    545                     view.setAlpha(fraction);
    546                     view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
    547                 }
    548             }
    549         };
    550 
    551         Context context = getContext();
    552         mControlRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
    553         mControlRowFadeInAnimator.addUpdateListener(updateListener);
    554         mControlRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
    555 
    556         mControlRowFadeOutAnimator = loadAnimator(context,
    557                 R.animator.lb_playback_controls_fade_out);
    558         mControlRowFadeOutAnimator.addUpdateListener(updateListener);
    559         mControlRowFadeOutAnimator.setInterpolator(mLogAccelerateInterpolator);
    560     }
    561 
    562     private void loadOtherRowAnimator() {
    563         final AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
    564             @Override
    565             public void onAnimationUpdate(ValueAnimator arg0) {
    566                 if (getVerticalGridView() == null) {
    567                     return;
    568                 }
    569                 final float fraction = (Float) arg0.getAnimatedValue();
    570                 final int count = getVerticalGridView().getChildCount();
    571                 for (int i = 0; i < count; i++) {
    572                     View view = getVerticalGridView().getChildAt(i);
    573                     if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
    574                         view.setAlpha(fraction);
    575                         view.setTranslationY((float) mAnimationTranslateY * (1f - fraction));
    576                     }
    577                 }
    578             }
    579         };
    580 
    581         Context context = getContext();
    582         mOtherRowFadeInAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_in);
    583         mOtherRowFadeInAnimator.addUpdateListener(updateListener);
    584         mOtherRowFadeInAnimator.setInterpolator(mLogDecelerateInterpolator);
    585 
    586         mOtherRowFadeOutAnimator = loadAnimator(context, R.animator.lb_playback_controls_fade_out);
    587         mOtherRowFadeOutAnimator.addUpdateListener(updateListener);
    588         mOtherRowFadeOutAnimator.setInterpolator(new AccelerateInterpolator());
    589     }
    590 
    591     /**
    592      * Fades out the playback overlay immediately.
    593      * @deprecated Call {@link #hideControlsOverlay(boolean)}
    594      */
    595     @Deprecated
    596     public void fadeOut() {
    597         showControlsOverlay(false, false);
    598     }
    599 
    600     /**
    601      * Show controls overlay.
    602      *
    603      * @param runAnimation True to run animation, false otherwise.
    604      */
    605     public void showControlsOverlay(boolean runAnimation) {
    606         showControlsOverlay(true, runAnimation);
    607     }
    608 
    609     /**
    610      * Returns true if controls overlay is visible, false otherwise.
    611      *
    612      * @return True if controls overlay is visible, false otherwise.
    613      * @see #showControlsOverlay(boolean)
    614      * @see #hideControlsOverlay(boolean)
    615      */
    616     public boolean isControlsOverlayVisible() {
    617         return mControlVisible;
    618     }
    619 
    620     /**
    621      * Hide controls overlay.
    622      *
    623      * @param runAnimation True to run animation, false otherwise.
    624      */
    625     public void hideControlsOverlay(boolean runAnimation) {
    626         showControlsOverlay(false, runAnimation);
    627     }
    628 
    629     /**
    630      * if first animator is still running, reverse it; otherwise start second animator.
    631      */
    632     static void reverseFirstOrStartSecond(ValueAnimator first, ValueAnimator second,
    633             boolean runAnimation) {
    634         if (first.isStarted()) {
    635             first.reverse();
    636             if (!runAnimation) {
    637                 first.end();
    638             }
    639         } else {
    640             second.start();
    641             if (!runAnimation) {
    642                 second.end();
    643             }
    644         }
    645     }
    646 
    647     /**
    648      * End first or second animator if they are still running.
    649      */
    650     static void endAll(ValueAnimator first, ValueAnimator second) {
    651         if (first.isStarted()) {
    652             first.end();
    653         } else if (second.isStarted()) {
    654             second.end();
    655         }
    656     }
    657 
    658     /**
    659      * Fade in or fade out rows and background.
    660      *
    661      * @param show True to fade in, false to fade out.
    662      * @param animation True to run animation.
    663      */
    664     void showControlsOverlay(boolean show, boolean animation) {
    665         if (DEBUG) Log.v(TAG, "showControlsOverlay " + show);
    666         if (getView() == null) {
    667             mControlVisibleBeforeOnCreateView = show;
    668             return;
    669         }
    670         // force no animation when fragment is not resumed
    671         if (!isResumed()) {
    672             animation = false;
    673         }
    674         if (show == mControlVisible) {
    675             if (!animation) {
    676                 // End animation if needed
    677                 endAll(mBgFadeInAnimator, mBgFadeOutAnimator);
    678                 endAll(mControlRowFadeInAnimator, mControlRowFadeOutAnimator);
    679                 endAll(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator);
    680             }
    681             return;
    682         }
    683         // StateGraph: 7<->5 4<->3 2->3
    684         mControlVisible = show;
    685         if (!mControlVisible) {
    686             // StateGraph 2->3
    687             stopFadeTimer();
    688         }
    689 
    690         mAnimationTranslateY = (getVerticalGridView() == null
    691                 || getVerticalGridView().getSelectedPosition() == 0)
    692                 ? mMajorFadeTranslateY : mMinorFadeTranslateY;
    693 
    694         if (show) {
    695             reverseFirstOrStartSecond(mBgFadeOutAnimator, mBgFadeInAnimator, animation);
    696             reverseFirstOrStartSecond(mControlRowFadeOutAnimator, mControlRowFadeInAnimator,
    697                     animation);
    698             reverseFirstOrStartSecond(mOtherRowFadeOutAnimator, mOtherRowFadeInAnimator, animation);
    699         } else {
    700             reverseFirstOrStartSecond(mBgFadeInAnimator, mBgFadeOutAnimator, animation);
    701             reverseFirstOrStartSecond(mControlRowFadeInAnimator, mControlRowFadeOutAnimator,
    702                     animation);
    703             reverseFirstOrStartSecond(mOtherRowFadeInAnimator, mOtherRowFadeOutAnimator, animation);
    704         }
    705         if (animation) {
    706             getView().announceForAccessibility(getString(show
    707                     ? R.string.lb_playback_controls_shown
    708                     : R.string.lb_playback_controls_hidden));
    709         }
    710     }
    711 
    712     /**
    713      * Sets the selected row position with smooth animation.
    714      */
    715     public void setSelectedPosition(int position) {
    716         setSelectedPosition(position, true);
    717     }
    718 
    719     /**
    720      * Sets the selected row position.
    721      */
    722     public void setSelectedPosition(int position, boolean smooth) {
    723         mSetSelectionRunnable.mPosition = position;
    724         mSetSelectionRunnable.mSmooth = smooth;
    725         if (getView() != null && getView().getHandler() != null) {
    726             getView().getHandler().post(mSetSelectionRunnable);
    727         }
    728     }
    729 
    730     private void setupChildFragmentLayout() {
    731         setVerticalGridViewLayout(mRowsSupportFragment.getVerticalGridView());
    732     }
    733 
    734     void setVerticalGridViewLayout(VerticalGridView listview) {
    735         if (listview == null) {
    736             return;
    737         }
    738 
    739         // we set the base line of alignment to -paddingBottom
    740         listview.setWindowAlignmentOffset(-mPaddingBottom);
    741         listview.setWindowAlignmentOffsetPercent(
    742                 VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
    743 
    744         // align other rows that arent the last to center of screen, since our baseline is
    745         // -mPaddingBottom, we need subtract that from mOtherRowsCenterToBottom.
    746         listview.setItemAlignmentOffset(mOtherRowsCenterToBottom - mPaddingBottom);
    747         listview.setItemAlignmentOffsetPercent(50);
    748 
    749         // Push last row to the bottom padding
    750         // Padding affects alignment when last row is focused
    751         listview.setPadding(listview.getPaddingLeft(), listview.getPaddingTop(),
    752                 listview.getPaddingRight(), mPaddingBottom);
    753         listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_HIGH_EDGE);
    754     }
    755 
    756     @Override
    757     public void onCreate(Bundle savedInstanceState) {
    758         super.onCreate(savedInstanceState);
    759 
    760         mOtherRowsCenterToBottom = getResources()
    761                 .getDimensionPixelSize(R.dimen.lb_playback_other_rows_center_to_bottom);
    762         mPaddingBottom =
    763                 getResources().getDimensionPixelSize(R.dimen.lb_playback_controls_padding_bottom);
    764         mBgDarkColor =
    765                 getResources().getColor(R.color.lb_playback_controls_background_dark);
    766         mBgLightColor =
    767                 getResources().getColor(R.color.lb_playback_controls_background_light);
    768         mShowTimeMs =
    769                 getResources().getInteger(R.integer.lb_playback_controls_show_time_ms);
    770         mMajorFadeTranslateY =
    771                 getResources().getDimensionPixelSize(R.dimen.lb_playback_major_fade_translate_y);
    772         mMinorFadeTranslateY =
    773                 getResources().getDimensionPixelSize(R.dimen.lb_playback_minor_fade_translate_y);
    774 
    775         loadBgAnimator();
    776         loadControlRowAnimator();
    777         loadOtherRowAnimator();
    778     }
    779 
    780     /**
    781      * Sets the background type.
    782      *
    783      * @param type One of BG_LIGHT, BG_DARK, or BG_NONE.
    784      */
    785     public void setBackgroundType(int type) {
    786         switch (type) {
    787             case BG_LIGHT:
    788             case BG_DARK:
    789             case BG_NONE:
    790                 if (type != mBackgroundType) {
    791                     mBackgroundType = type;
    792                     updateBackground();
    793                 }
    794                 break;
    795             default:
    796                 throw new IllegalArgumentException("Invalid background type");
    797         }
    798     }
    799 
    800     /**
    801      * Returns the background type.
    802      */
    803     public int getBackgroundType() {
    804         return mBackgroundType;
    805     }
    806 
    807     private void updateBackground() {
    808         if (mBackgroundView != null) {
    809             int color = mBgDarkColor;
    810             switch (mBackgroundType) {
    811                 case BG_DARK:
    812                     break;
    813                 case BG_LIGHT:
    814                     color = mBgLightColor;
    815                     break;
    816                 case BG_NONE:
    817                     color = Color.TRANSPARENT;
    818                     break;
    819             }
    820             mBackgroundView.setBackground(new ColorDrawable(color));
    821             setBgAlpha(mBgAlpha);
    822         }
    823     }
    824 
    825     private final ItemBridgeAdapter.AdapterListener mAdapterListener =
    826             new ItemBridgeAdapter.AdapterListener() {
    827                 @Override
    828                 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
    829                     if (DEBUG) Log.v(TAG, "onAttachedToWindow " + vh.getViewHolder().view);
    830                     if (!mControlVisible) {
    831                         if (DEBUG) Log.v(TAG, "setting alpha to 0");
    832                         vh.getViewHolder().view.setAlpha(0);
    833                     }
    834                 }
    835 
    836                 @Override
    837                 public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
    838                     Presenter.ViewHolder viewHolder = vh.getViewHolder();
    839                     if (viewHolder instanceof PlaybackSeekUi) {
    840                         ((PlaybackSeekUi) viewHolder).setPlaybackSeekUiClient(mChainedClient);
    841                     }
    842                 }
    843 
    844                 @Override
    845                 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) {
    846                     if (DEBUG) Log.v(TAG, "onDetachedFromWindow " + vh.getViewHolder().view);
    847                     // Reset animation state
    848                     vh.getViewHolder().view.setAlpha(1f);
    849                     vh.getViewHolder().view.setTranslationY(0);
    850                     vh.getViewHolder().view.setAlpha(1f);
    851                 }
    852 
    853                 @Override
    854                 public void onBind(ItemBridgeAdapter.ViewHolder vh) {
    855                 }
    856             };
    857 
    858     @Override
    859     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    860                              Bundle savedInstanceState) {
    861         mRootView = inflater.inflate(R.layout.lb_playback_fragment, container, false);
    862         mBackgroundView = mRootView.findViewById(R.id.playback_fragment_background);
    863         mRowsSupportFragment = (RowsSupportFragment) getChildFragmentManager().findFragmentById(
    864                 R.id.playback_controls_dock);
    865         if (mRowsSupportFragment == null) {
    866             mRowsSupportFragment = new RowsSupportFragment();
    867             getChildFragmentManager().beginTransaction()
    868                     .replace(R.id.playback_controls_dock, mRowsSupportFragment)
    869                     .commit();
    870         }
    871         if (mAdapter == null) {
    872             setAdapter(new ArrayObjectAdapter(new ClassPresenterSelector()));
    873         } else {
    874             mRowsSupportFragment.setAdapter(mAdapter);
    875         }
    876         mRowsSupportFragment.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
    877         mRowsSupportFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
    878 
    879         mBgAlpha = 255;
    880         updateBackground();
    881         mRowsSupportFragment.setExternalAdapterListener(mAdapterListener);
    882         ProgressBarManager progressBarManager = getProgressBarManager();
    883         if (progressBarManager != null) {
    884             progressBarManager.setRootView((ViewGroup) mRootView);
    885         }
    886         return mRootView;
    887     }
    888 
    889     /**
    890      * Sets the {@link PlaybackGlueHost.HostCallback}. Implementor of this interface will
    891      * take appropriate actions to take action when the hosting fragment starts/stops processing.
    892      */
    893     public void setHostCallback(PlaybackGlueHost.HostCallback hostCallback) {
    894         this.mHostCallback = hostCallback;
    895     }
    896 
    897     @Override
    898     public void onStart() {
    899         super.onStart();
    900         setupChildFragmentLayout();
    901         mRowsSupportFragment.setAdapter(mAdapter);
    902         if (mHostCallback != null) {
    903             mHostCallback.onHostStart();
    904         }
    905     }
    906 
    907     @Override
    908     public void onStop() {
    909         if (mHostCallback != null) {
    910             mHostCallback.onHostStop();
    911         }
    912         super.onStop();
    913     }
    914 
    915     @Override
    916     public void onPause() {
    917         if (mHostCallback != null) {
    918             mHostCallback.onHostPause();
    919         }
    920         if (mHandler.hasMessages(START_FADE_OUT)) {
    921             // StateGraph: 2->1
    922             mHandler.removeMessages(START_FADE_OUT);
    923         } else {
    924             // StateGraph: 5->6, 7->6, 4->1, 3->1
    925         }
    926         super.onPause();
    927     }
    928 
    929     /**
    930      * This listener is called every time there is a selection in {@link RowsSupportFragment}. This can
    931      * be used by users to take additional actions such as animations.
    932      */
    933     public void setOnItemViewSelectedListener(final BaseOnItemViewSelectedListener listener) {
    934         mExternalItemSelectedListener = listener;
    935     }
    936 
    937     /**
    938      * This listener is called every time there is a click in {@link RowsSupportFragment}. This can
    939      * be used by users to take additional actions such as animations.
    940      */
    941     public void setOnItemViewClickedListener(final BaseOnItemViewClickedListener listener) {
    942         mExternalItemClickedListener = listener;
    943     }
    944 
    945     /**
    946      * Sets the {@link BaseOnItemViewClickedListener} that would be invoked for clicks
    947      * only on {@link androidx.leanback.widget.PlaybackRowPresenter.ViewHolder}.
    948      */
    949     public void setOnPlaybackItemViewClickedListener(final BaseOnItemViewClickedListener listener) {
    950         mPlaybackItemClickedListener = listener;
    951     }
    952 
    953     @Override
    954     public void onDestroyView() {
    955         mRootView = null;
    956         mBackgroundView = null;
    957         super.onDestroyView();
    958     }
    959 
    960     @Override
    961     public void onDestroy() {
    962         if (mHostCallback != null) {
    963             mHostCallback.onHostDestroy();
    964         }
    965         super.onDestroy();
    966     }
    967 
    968     /**
    969      * Sets the playback row for the playback controls. The row will be set as first element
    970      * of adapter if the adapter is {@link ArrayObjectAdapter} or {@link SparseArrayObjectAdapter}.
    971      * @param row The row that represents the playback.
    972      */
    973     public void setPlaybackRow(Row row) {
    974         this.mRow = row;
    975         setupRow();
    976         setupPresenter();
    977     }
    978 
    979     /**
    980      * Sets the presenter for rendering the playback row set by {@link #setPlaybackRow(Row)}. If
    981      * adapter does not set a {@link PresenterSelector}, {@link #setAdapter(ObjectAdapter)} will
    982      * create a {@link ClassPresenterSelector} by default and map from the row object class to this
    983      * {@link PlaybackRowPresenter}.
    984      *
    985      * @param  presenter Presenter used to render {@link #setPlaybackRow(Row)}.
    986      */
    987     public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
    988         this.mPresenter = presenter;
    989         setupPresenter();
    990         setPlaybackRowPresenterAlignment();
    991     }
    992 
    993     void setPlaybackRowPresenterAlignment() {
    994         if (mAdapter != null && mAdapter.getPresenterSelector() != null) {
    995             Presenter[] presenters = mAdapter.getPresenterSelector().getPresenters();
    996             if (presenters != null) {
    997                 for (int i = 0; i < presenters.length; i++) {
    998                     if (presenters[i] instanceof PlaybackRowPresenter
    999                             && presenters[i].getFacet(ItemAlignmentFacet.class) == null) {
   1000                         ItemAlignmentFacet itemAlignment = new ItemAlignmentFacet();
   1001                         ItemAlignmentFacet.ItemAlignmentDef def =
   1002                                 new ItemAlignmentFacet.ItemAlignmentDef();
   1003                         def.setItemAlignmentOffset(0);
   1004                         def.setItemAlignmentOffsetPercent(100);
   1005                         itemAlignment.setAlignmentDefs(new ItemAlignmentFacet.ItemAlignmentDef[]
   1006                                 {def});
   1007                         presenters[i].setFacet(ItemAlignmentFacet.class, itemAlignment);
   1008                     }
   1009                 }
   1010             }
   1011         }
   1012     }
   1013 
   1014     /**
   1015      * Updates the ui when the row data changes.
   1016      */
   1017     public void notifyPlaybackRowChanged() {
   1018         if (mAdapter == null) {
   1019             return;
   1020         }
   1021         mAdapter.notifyItemRangeChanged(0, 1);
   1022     }
   1023 
   1024     /**
   1025      * Sets the list of rows for the fragment. A default {@link ClassPresenterSelector} will be
   1026      * created if {@link ObjectAdapter#getPresenterSelector()} is null. if user provides
   1027      * {@link #setPlaybackRow(Row)} and {@link #setPlaybackRowPresenter(PlaybackRowPresenter)},
   1028      * the row and presenter will be set onto the adapter.
   1029      *
   1030      * @param adapter The adapter that contains related rows and optional playback row.
   1031      */
   1032     public void setAdapter(ObjectAdapter adapter) {
   1033         mAdapter = adapter;
   1034         setupRow();
   1035         setupPresenter();
   1036         setPlaybackRowPresenterAlignment();
   1037 
   1038         if (mRowsSupportFragment != null) {
   1039             mRowsSupportFragment.setAdapter(adapter);
   1040         }
   1041     }
   1042 
   1043     private void setupRow() {
   1044         if (mAdapter instanceof ArrayObjectAdapter && mRow != null) {
   1045             ArrayObjectAdapter adapter = ((ArrayObjectAdapter) mAdapter);
   1046             if (adapter.size() == 0) {
   1047                 adapter.add(mRow);
   1048             } else {
   1049                 adapter.replace(0, mRow);
   1050             }
   1051         } else if (mAdapter instanceof SparseArrayObjectAdapter && mRow != null) {
   1052             SparseArrayObjectAdapter adapter = ((SparseArrayObjectAdapter) mAdapter);
   1053             adapter.set(0, mRow);
   1054         }
   1055     }
   1056 
   1057     private void setupPresenter() {
   1058         if (mAdapter != null && mRow != null && mPresenter != null) {
   1059             PresenterSelector selector = mAdapter.getPresenterSelector();
   1060             if (selector == null) {
   1061                 selector = new ClassPresenterSelector();
   1062                 ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);
   1063                 mAdapter.setPresenterSelector(selector);
   1064             } else if (selector instanceof ClassPresenterSelector) {
   1065                 ((ClassPresenterSelector) selector).addClassPresenter(mRow.getClass(), mPresenter);
   1066             }
   1067         }
   1068     }
   1069 
   1070     final PlaybackSeekUi.Client mChainedClient = new PlaybackSeekUi.Client() {
   1071         @Override
   1072         public boolean isSeekEnabled() {
   1073             return mSeekUiClient == null ? false : mSeekUiClient.isSeekEnabled();
   1074         }
   1075 
   1076         @Override
   1077         public void onSeekStarted() {
   1078             if (mSeekUiClient != null) {
   1079                 mSeekUiClient.onSeekStarted();
   1080             }
   1081             setSeekMode(true);
   1082         }
   1083 
   1084         @Override
   1085         public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
   1086             return mSeekUiClient == null ? null : mSeekUiClient.getPlaybackSeekDataProvider();
   1087         }
   1088 
   1089         @Override
   1090         public void onSeekPositionChanged(long pos) {
   1091             if (mSeekUiClient != null) {
   1092                 mSeekUiClient.onSeekPositionChanged(pos);
   1093             }
   1094         }
   1095 
   1096         @Override
   1097         public void onSeekFinished(boolean cancelled) {
   1098             if (mSeekUiClient != null) {
   1099                 mSeekUiClient.onSeekFinished(cancelled);
   1100             }
   1101             setSeekMode(false);
   1102         }
   1103     };
   1104 
   1105     /**
   1106      * Interface to be implemented by UI widget to support PlaybackSeekUi.
   1107      */
   1108     public void setPlaybackSeekUiClient(PlaybackSeekUi.Client client) {
   1109         mSeekUiClient = client;
   1110     }
   1111 
   1112     /**
   1113      * Show or hide other rows other than PlaybackRow.
   1114      * @param inSeek True to make other rows visible, false to make other rows invisible.
   1115      */
   1116     void setSeekMode(boolean inSeek) {
   1117         if (mInSeek == inSeek) {
   1118             return;
   1119         }
   1120         mInSeek = inSeek;
   1121         getVerticalGridView().setSelectedPosition(0);
   1122         if (mInSeek) {
   1123             stopFadeTimer();
   1124         }
   1125         // immediately fade in control row.
   1126         showControlsOverlay(true);
   1127         final int count = getVerticalGridView().getChildCount();
   1128         for (int i = 0; i < count; i++) {
   1129             View view = getVerticalGridView().getChildAt(i);
   1130             if (getVerticalGridView().getChildAdapterPosition(view) > 0) {
   1131                 view.setVisibility(mInSeek ? View.INVISIBLE : View.VISIBLE);
   1132             }
   1133         }
   1134     }
   1135 
   1136     /**
   1137      * Called when size of the video changes. App may override.
   1138      * @param videoWidth Intrinsic width of video
   1139      * @param videoHeight Intrinsic height of video
   1140      */
   1141     protected void onVideoSizeChanged(int videoWidth, int videoHeight) {
   1142     }
   1143 
   1144     /**
   1145      * Called when media has start or stop buffering. App may override. The default initial state
   1146      * is not buffering.
   1147      * @param start True for buffering start, false otherwise.
   1148      */
   1149     protected void onBufferingStateChanged(boolean start) {
   1150         ProgressBarManager progressBarManager = getProgressBarManager();
   1151         if (progressBarManager != null) {
   1152             if (start) {
   1153                 progressBarManager.show();
   1154             } else {
   1155                 progressBarManager.hide();
   1156             }
   1157         }
   1158     }
   1159 
   1160     /**
   1161      * Called when media has error. App may override.
   1162      * @param errorCode Optional error code for specific implementation.
   1163      * @param errorMessage Optional error message for specific implementation.
   1164      */
   1165     protected void onError(int errorCode, CharSequence errorMessage) {
   1166     }
   1167 
   1168     /**
   1169      * Returns the ProgressBarManager that will show or hide progress bar in
   1170      * {@link #onBufferingStateChanged(boolean)}.
   1171      * @return The ProgressBarManager that will show or hide progress bar in
   1172      * {@link #onBufferingStateChanged(boolean)}.
   1173      */
   1174     public ProgressBarManager getProgressBarManager() {
   1175         return mProgressBarManager;
   1176     }
   1177 }
   1178