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      */
DetailsBackgroundVideoHelper( PlaybackGlue playbackGlue, DetailsParallax detailsParallax, Drawable backgroundDrawable)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 
startParallax()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 
stopParallax()109     void stopParallax() {
110         mDetailsParallax.removeEffect(mParallaxEffect);
111     }
112 
isVideoVisible()113     boolean isVideoVisible() {
114         return mCurrentState == PLAY_VIDEO;
115     }
116 
updateState(int state)117     private void updateState(int state) {
118         if (state == mCurrentState) {
119             return;
120         }
121         mCurrentState = state;
122         applyState();
123     }
124 
applyState()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 
setPlaybackGlue(PlaybackGlue playbackGlue)148     void setPlaybackGlue(PlaybackGlue playbackGlue) {
149         if (mPlaybackGlue != null) {
150             mPlaybackGlue.removePlayerCallback(mControlStateCallback);
151         }
152         mPlaybackGlue = playbackGlue;
153         applyState();
154     }
155 
internalStartPlayback()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 
crossFadeBackgroundToVideo(boolean crossFadeToVideo)168     void crossFadeBackgroundToVideo(boolean crossFadeToVideo) {
169         crossFadeBackgroundToVideo(crossFadeToVideo, false);
170     }
171 
crossFadeBackgroundToVideo(boolean crossFadeToVideo, boolean immediate)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
onPreparedStateChanged(PlaybackGlue glue)238         public void onPreparedStateChanged(PlaybackGlue glue) {
239             if (glue.isPrepared()) {
240                 internalStartPlayback();
241             }
242         }
243     }
244 
245     PlaybackControlStateCallback mControlStateCallback = new PlaybackControlStateCallback();
246 }
247