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