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
17package androidx.leanback.app;
18
19import android.animation.Animator;
20import android.animation.ValueAnimator;
21import android.graphics.drawable.Drawable;
22
23import androidx.leanback.media.PlaybackGlue;
24import androidx.leanback.widget.DetailsParallax;
25import androidx.leanback.widget.Parallax;
26import androidx.leanback.widget.ParallaxEffect;
27import 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 */
39final 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