Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.leanback.app;
     18 
     19 import android.animation.Animator;
     20 import android.animation.ValueAnimator;
     21 import android.graphics.drawable.Drawable;
     22 
     23 import androidx.leanback.media.PlaybackGlue;
     24 import androidx.leanback.widget.DetailsParallax;
     25 import androidx.leanback.widget.Parallax;
     26 import androidx.leanback.widget.ParallaxEffect;
     27 import androidx.leanback.widget.ParallaxTarget;
     28 
     29 /**
     30  * Helper class responsible for controlling video playback in {@link DetailsFragment}. This
     31  * takes {@link DetailsParallax}, {@link PlaybackGlue} and a drawable as input.
     32  * Video is played when {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of screen.
     33  * Video is stopped when {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above top
     34  * edge of screen. The drawable will change alpha to 0 when video is ready to play.
     35  * App does not directly use this class.
     36  * @see DetailsFragmentBackgroundController
     37  * @see DetailsSupportFragmentBackgroundController
     38  */
     39 final class DetailsBackgroundVideoHelper {
     40     private static final long BACKGROUND_CROSS_FADE_DURATION = 500;
     41     // Temporarily add CROSSFADE_DELAY waiting for video surface ready.
     42     // We will remove this delay once PlaybackGlue have a callback for videoRenderingReady event.
     43     private static final long CROSSFADE_DELAY = 1000;
     44 
     45     /**
     46      * Different states {@link DetailsFragment} can be in.
     47      */
     48     static final int INITIAL = 0;
     49     static final int PLAY_VIDEO = 1;
     50     static final int NO_VIDEO = 2;
     51 
     52     private final DetailsParallax mDetailsParallax;
     53     private ParallaxEffect mParallaxEffect;
     54 
     55     private int mCurrentState = INITIAL;
     56 
     57     private ValueAnimator mBackgroundAnimator;
     58     private Drawable mBackgroundDrawable;
     59     private PlaybackGlue mPlaybackGlue;
     60     private boolean mBackgroundDrawableVisible;
     61 
     62     /**
     63      * Constructor to setup a Helper for controlling video playback in DetailsFragment.
     64      * @param playbackGlue The PlaybackGlue used to control underlying player.
     65      * @param detailsParallax The DetailsParallax to add special parallax effect to control video
     66      *                        start/stop. Video is played when
     67      *                        {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of
     68      *                        screen. Video is stopped when
     69      *                        {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above
     70      *                        top edge of screen.
     71      * @param backgroundDrawable The drawable will change alpha to 0 when video is ready to play.
     72      */
     73     DetailsBackgroundVideoHelper(
     74             PlaybackGlue playbackGlue,
     75             DetailsParallax detailsParallax,
     76             Drawable backgroundDrawable) {
     77         this.mPlaybackGlue = playbackGlue;
     78         this.mDetailsParallax = detailsParallax;
     79         this.mBackgroundDrawable = backgroundDrawable;
     80         mBackgroundDrawableVisible = true;
     81         mBackgroundDrawable.setAlpha(255);
     82         startParallax();
     83     }
     84 
     85     void startParallax() {
     86         if (mParallaxEffect != null) {
     87             return;
     88         }
     89         Parallax.IntProperty frameTop = mDetailsParallax.getOverviewRowTop();
     90         final float maxFrameTop = 1f;
     91         final float minFrameTop = 0f;
     92         mParallaxEffect = mDetailsParallax
     93                 .addEffect(frameTop.atFraction(maxFrameTop), frameTop.atFraction(minFrameTop))
     94                 .target(new ParallaxTarget() {
     95                     @Override
     96                     public void update(float fraction) {
     97                         if (fraction == maxFrameTop) {
     98                             updateState(NO_VIDEO);
     99                         } else {
    100                             updateState(PLAY_VIDEO);
    101                         }
    102                     }
    103                 });
    104         // In case the VideoHelper is created after RecyclerView is created: perform initial
    105         // parallax effect.
    106         mDetailsParallax.updateValues();
    107     }
    108 
    109     void stopParallax() {
    110         mDetailsParallax.removeEffect(mParallaxEffect);
    111     }
    112 
    113     boolean isVideoVisible() {
    114         return mCurrentState == PLAY_VIDEO;
    115     }
    116 
    117     private void updateState(int state) {
    118         if (state == mCurrentState) {
    119             return;
    120         }
    121         mCurrentState = state;
    122         applyState();
    123     }
    124 
    125     private void applyState() {
    126         switch (mCurrentState) {
    127             case PLAY_VIDEO:
    128                 if (mPlaybackGlue != null) {
    129                     if (mPlaybackGlue.isPrepared()) {
    130                         internalStartPlayback();
    131                     } else {
    132                         mPlaybackGlue.addPlayerCallback(mControlStateCallback);
    133                     }
    134                 } else {
    135                     crossFadeBackgroundToVideo(false);
    136                 }
    137                 break;
    138             case NO_VIDEO:
    139                 crossFadeBackgroundToVideo(false);
    140                 if (mPlaybackGlue != null) {
    141                     mPlaybackGlue.removePlayerCallback(mControlStateCallback);
    142                     mPlaybackGlue.pause();
    143                 }
    144                 break;
    145         }
    146     }
    147 
    148     void setPlaybackGlue(PlaybackGlue playbackGlue) {
    149         if (mPlaybackGlue != null) {
    150             mPlaybackGlue.removePlayerCallback(mControlStateCallback);
    151         }
    152         mPlaybackGlue = playbackGlue;
    153         applyState();
    154     }
    155 
    156     private void internalStartPlayback() {
    157         if (mPlaybackGlue != null) {
    158             mPlaybackGlue.play();
    159         }
    160         mDetailsParallax.getRecyclerView().postDelayed(new Runnable() {
    161             @Override
    162             public void run() {
    163                 crossFadeBackgroundToVideo(true);
    164             }
    165         }, CROSSFADE_DELAY);
    166     }
    167 
    168     void crossFadeBackgroundToVideo(boolean crossFadeToVideo) {
    169         crossFadeBackgroundToVideo(crossFadeToVideo, false);
    170     }
    171 
    172     void crossFadeBackgroundToVideo(boolean crossFadeToVideo, boolean immediate) {
    173         final boolean newVisible = !crossFadeToVideo;
    174         if (mBackgroundDrawableVisible == newVisible) {
    175             if (immediate) {
    176                 if (mBackgroundAnimator != null) {
    177                     mBackgroundAnimator.cancel();
    178                     mBackgroundAnimator = null;
    179                 }
    180                 if (mBackgroundDrawable != null) {
    181                     mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
    182                     return;
    183                 }
    184             }
    185             return;
    186         }
    187         mBackgroundDrawableVisible = newVisible;
    188         if (mBackgroundAnimator != null) {
    189             mBackgroundAnimator.cancel();
    190             mBackgroundAnimator = null;
    191         }
    192 
    193         float startAlpha = crossFadeToVideo ? 1f : 0f;
    194         float endAlpha = crossFadeToVideo ? 0f : 1f;
    195 
    196         if (mBackgroundDrawable == null) {
    197             return;
    198         }
    199         if (immediate) {
    200             mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
    201             return;
    202         }
    203         mBackgroundAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);
    204         mBackgroundAnimator.setDuration(BACKGROUND_CROSS_FADE_DURATION);
    205         mBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    206             @Override
    207             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    208                 mBackgroundDrawable.setAlpha(
    209                         (int) ((Float) (valueAnimator.getAnimatedValue()) * 255));
    210             }
    211         });
    212 
    213         mBackgroundAnimator.addListener(new Animator.AnimatorListener() {
    214             @Override
    215             public void onAnimationStart(Animator animator) {
    216             }
    217 
    218             @Override
    219             public void onAnimationEnd(Animator animator) {
    220                 mBackgroundAnimator = null;
    221             }
    222 
    223             @Override
    224             public void onAnimationCancel(Animator animator) {
    225             }
    226 
    227             @Override
    228             public void onAnimationRepeat(Animator animator) {
    229             }
    230         });
    231 
    232         mBackgroundAnimator.start();
    233     }
    234 
    235     private class PlaybackControlStateCallback extends PlaybackGlue.PlayerCallback {
    236 
    237         @Override
    238         public void onPreparedStateChanged(PlaybackGlue glue) {
    239             if (glue.isPrepared()) {
    240                 internalStartPlayback();
    241             }
    242         }
    243     }
    244 
    245     PlaybackControlStateCallback mControlStateCallback = new PlaybackControlStateCallback();
    246 }
    247