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