blob: b5fc444d63d14be4cd7e333ce59011d73bf3c841 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.leanback.app;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.graphics.drawable.Drawable;
import androidx.leanback.media.PlaybackGlue;
import androidx.leanback.widget.DetailsParallax;
import androidx.leanback.widget.Parallax;
import androidx.leanback.widget.ParallaxEffect;
import androidx.leanback.widget.ParallaxTarget;
/**
* Helper class responsible for controlling video playback in {@link DetailsFragment}. This
* takes {@link DetailsParallax}, {@link PlaybackGlue} and a drawable as input.
* Video is played when {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of screen.
* Video is stopped when {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above top
* edge of screen. The drawable will change alpha to 0 when video is ready to play.
* App does not directly use this class.
* @see DetailsFragmentBackgroundController
* @see DetailsSupportFragmentBackgroundController
*/
final class DetailsBackgroundVideoHelper {
private static final long BACKGROUND_CROSS_FADE_DURATION = 500;
// Temporarily add CROSSFADE_DELAY waiting for video surface ready.
// We will remove this delay once PlaybackGlue have a callback for videoRenderingReady event.
private static final long CROSSFADE_DELAY = 1000;
/**
* Different states {@link DetailsFragment} can be in.
*/
static final int INITIAL = 0;
static final int PLAY_VIDEO = 1;
static final int NO_VIDEO = 2;
private final DetailsParallax mDetailsParallax;
private ParallaxEffect mParallaxEffect;
private int mCurrentState = INITIAL;
private ValueAnimator mBackgroundAnimator;
private Drawable mBackgroundDrawable;
private PlaybackGlue mPlaybackGlue;
private boolean mBackgroundDrawableVisible;
/**
* Constructor to setup a Helper for controlling video playback in DetailsFragment.
* @param playbackGlue The PlaybackGlue used to control underlying player.
* @param detailsParallax The DetailsParallax to add special parallax effect to control video
* start/stop. Video is played when
* {@link DetailsParallax#getOverviewRowTop()} moved bellow top edge of
* screen. Video is stopped when
* {@link DetailsParallax#getOverviewRowTop()} reaches or scrolls above
* top edge of screen.
* @param backgroundDrawable The drawable will change alpha to 0 when video is ready to play.
*/
DetailsBackgroundVideoHelper(
PlaybackGlue playbackGlue,
DetailsParallax detailsParallax,
Drawable backgroundDrawable) {
this.mPlaybackGlue = playbackGlue;
this.mDetailsParallax = detailsParallax;
this.mBackgroundDrawable = backgroundDrawable;
mBackgroundDrawableVisible = true;
mBackgroundDrawable.setAlpha(255);
startParallax();
}
void startParallax() {
if (mParallaxEffect != null) {
return;
}
Parallax.IntProperty frameTop = mDetailsParallax.getOverviewRowTop();
final float maxFrameTop = 1f;
final float minFrameTop = 0f;
mParallaxEffect = mDetailsParallax
.addEffect(frameTop.atFraction(maxFrameTop), frameTop.atFraction(minFrameTop))
.target(new ParallaxTarget() {
@Override
public void update(float fraction) {
if (fraction == maxFrameTop) {
updateState(NO_VIDEO);
} else {
updateState(PLAY_VIDEO);
}
}
});
// In case the VideoHelper is created after RecyclerView is created: perform initial
// parallax effect.
mDetailsParallax.updateValues();
}
void stopParallax() {
mDetailsParallax.removeEffect(mParallaxEffect);
}
boolean isVideoVisible() {
return mCurrentState == PLAY_VIDEO;
}
private void updateState(int state) {
if (state == mCurrentState) {
return;
}
mCurrentState = state;
applyState();
}
private void applyState() {
switch (mCurrentState) {
case PLAY_VIDEO:
if (mPlaybackGlue != null) {
if (mPlaybackGlue.isPrepared()) {
internalStartPlayback();
} else {
mPlaybackGlue.addPlayerCallback(mControlStateCallback);
}
} else {
crossFadeBackgroundToVideo(false);
}
break;
case NO_VIDEO:
crossFadeBackgroundToVideo(false);
if (mPlaybackGlue != null) {
mPlaybackGlue.removePlayerCallback(mControlStateCallback);
mPlaybackGlue.pause();
}
break;
}
}
void setPlaybackGlue(PlaybackGlue playbackGlue) {
if (mPlaybackGlue != null) {
mPlaybackGlue.removePlayerCallback(mControlStateCallback);
}
mPlaybackGlue = playbackGlue;
applyState();
}
private void internalStartPlayback() {
if (mPlaybackGlue != null) {
mPlaybackGlue.play();
}
mDetailsParallax.getRecyclerView().postDelayed(new Runnable() {
@Override
public void run() {
crossFadeBackgroundToVideo(true);
}
}, CROSSFADE_DELAY);
}
void crossFadeBackgroundToVideo(boolean crossFadeToVideo) {
crossFadeBackgroundToVideo(crossFadeToVideo, false);
}
void crossFadeBackgroundToVideo(boolean crossFadeToVideo, boolean immediate) {
final boolean newVisible = !crossFadeToVideo;
if (mBackgroundDrawableVisible == newVisible) {
if (immediate) {
if (mBackgroundAnimator != null) {
mBackgroundAnimator.cancel();
mBackgroundAnimator = null;
}
if (mBackgroundDrawable != null) {
mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
return;
}
}
return;
}
mBackgroundDrawableVisible = newVisible;
if (mBackgroundAnimator != null) {
mBackgroundAnimator.cancel();
mBackgroundAnimator = null;
}
float startAlpha = crossFadeToVideo ? 1f : 0f;
float endAlpha = crossFadeToVideo ? 0f : 1f;
if (mBackgroundDrawable == null) {
return;
}
if (immediate) {
mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
return;
}
mBackgroundAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);
mBackgroundAnimator.setDuration(BACKGROUND_CROSS_FADE_DURATION);
mBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mBackgroundDrawable.setAlpha(
(int) ((Float) (valueAnimator.getAnimatedValue()) * 255));
}
});
mBackgroundAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
mBackgroundAnimator = null;
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
mBackgroundAnimator.start();
}
private class PlaybackControlStateCallback extends PlaybackGlue.PlayerCallback {
@Override
public void onPreparedStateChanged(PlaybackGlue glue) {
if (glue.isPrepared()) {
internalStartPlayback();
}
}
}
PlaybackControlStateCallback mControlStateCallback = new PlaybackControlStateCallback();
}