DetailsFragment: coordinate transition and PlaybackGlue
1. Exclude SurfaceView from transition because
it's quite a big hit on performance and unknown consequence
if we force the color format to be RGBA8888.
Disabled setTransitionVisibility() which causes
visual flickering even view is not running
transition. We will see the SurfaceView disappears
immediately after activity return transition is finished.
2. DetailsFragment will postpone setHost on PlaybackGlue until
all three conditions satisfied:
- onStart() is called
- activity enter transition is finished
- entrance transition is finished
It is tricky to query if enter transition will run. We did
this by checking if there is enter transition started after
200ms of data loading.
3. When DetailsFragment execute return transition:
- call DetailsBackgroundVideoHelper.disableParallax()
to avoid auto-play video when detailsFrame is
running a slide transition to bottom.
- if video is not playing: immediately remove VideoFragment
so we wont see a dark SurfaceView during return transition.
- if video is playing: Let the Video continue playing when
running return transition. (Because it's much better to
pops out VideoFragment at end than in the middle of running
transition).
4. removed 1 sec CROSSFADE_DELAY between play() and fade out
background drawable, this seems unnecessary after we have
the onPlaybackReady() callback.
Bug: 32987665
Test: leanback tests (and there is no way to test activity transition
in ActivityTestRule)
Change-Id: I2ecee0276aa6ffc7963b92cd2104bb1ca62b01cd
diff --git a/v17/leanback/res/layout/lb_playback_fragment.xml b/v17/leanback/res/layout/lb_playback_fragment.xml
index 85d52cf..1b0ffa1 100644
--- a/v17/leanback/res/layout/lb_playback_fragment.xml
+++ b/v17/leanback/res/layout/lb_playback_fragment.xml
@@ -18,11 +18,12 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/playback_fragment_root"
android:layout_width="match_parent"
+ android:transitionGroup="false"
android:layout_height="match_parent">
<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/playback_controls_dock"
+ android:transitionGroup="true"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_video_surface.xml b/v17/leanback/res/layout/lb_video_surface.xml
index a3b0fe0..9c6c8fd 100644
--- a/v17/leanback/res/layout/lb_video_surface.xml
+++ b/v17/leanback/res/layout/lb_video_surface.xml
@@ -14,7 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<SurfaceView xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.v17.leanback.widget.VideoSurfaceView
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/video_surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />
diff --git a/v17/leanback/res/transition-v21/lb_details_enter_transition.xml b/v17/leanback/res/transition-v21/lb_details_enter_transition.xml
index 3bb7c9f..d535ff4 100644
--- a/v17/leanback/res/transition-v21/lb_details_enter_transition.xml
+++ b/v17/leanback/res/transition-v21/lb_details_enter_transition.xml
@@ -25,6 +25,7 @@
<target android:excludeId="@id/title_text" />
<target android:excludeId="@id/title_orb" />
<target android:excludeId="@id/details_background_view" />
+ <target android:excludeId="@id/video_surface" />
</targets>
</transition>
<!-- The ParallaxTransition runs with with Slide transition, must use same duration
@@ -37,5 +38,8 @@
</transition>
<fade
android:duration="350">
+ <targets>
+ <target android:excludeId="@id/video_surface" />
+ </targets>
</fade>
</transitionSet>
\ No newline at end of file
diff --git a/v17/leanback/res/transition-v21/lb_details_return_transition.xml b/v17/leanback/res/transition-v21/lb_details_return_transition.xml
index 5e54d2c..82a55f4 100644
--- a/v17/leanback/res/transition-v21/lb_details_return_transition.xml
+++ b/v17/leanback/res/transition-v21/lb_details_return_transition.xml
@@ -24,6 +24,7 @@
<target android:excludeId="@id/title_text" />
<target android:excludeId="@id/title_orb" />
<target android:excludeId="@id/details_background_view" />
+ <target android:excludeId="@id/video_surface" />
</targets>
</transition>
<!-- The ParallaxTransition runs with with Slide transition, must use same duration
@@ -36,5 +37,8 @@
</transition>
<fade
android:duration="350">
+ <targets>
+ <target android:excludeId="@id/video_surface" />
+ </targets>
</fade>
</transitionSet>
\ No newline at end of file
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
index cb3fa2f..72072a2 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
@@ -22,6 +22,7 @@
import android.support.v17.leanback.media.PlaybackGlue;
import android.support.v17.leanback.widget.DetailsParallax;
import android.support.v17.leanback.widget.Parallax;
+import android.support.v17.leanback.widget.ParallaxEffect;
import android.support.v17.leanback.widget.ParallaxTarget;
/**
@@ -36,19 +37,19 @@
*/
final class DetailsBackgroundVideoHelper {
private static final long BACKGROUND_CROSS_FADE_DURATION = 500;
- private static final long CROSSFADE_DELAY = 1000;
+ private static final long CROSSFADE_DELAY = 0;
/**
* Different states {@link DetailsFragment} can be in.
*/
- enum STATE {
- INITIAL,
- PLAY_VIDEO,
- NO_VIDEO
- }
+ static final int INITIAL = 0;
+ static final int PLAY_VIDEO = 1;
+ static final int NO_VIDEO = 2;
private final DetailsParallax mDetailsParallax;
- private STATE mCurrentState = STATE.INITIAL;
+ private ParallaxEffect mParallaxEffect;
+
+ private int mCurrentState = INITIAL;
private ValueAnimator mBackgroundAnimator;
private Drawable mBackgroundDrawable;
@@ -72,14 +73,17 @@
this.mPlaybackGlue = playbackGlue;
this.mDetailsParallax = detailsParallax;
this.mBackgroundDrawable = backgroundDrawable;
- setupParallax();
+ startParallax();
}
- void setupParallax() {
+ void startParallax() {
+ if (mParallaxEffect != null) {
+ return;
+ }
Parallax.IntProperty frameTop = mDetailsParallax.getOverviewRowTop();
final float maxFrameTop = 1f;
final float minFrameTop = 0f;
- mDetailsParallax
+ mParallaxEffect = mDetailsParallax
.addEffect(frameTop.atFraction(maxFrameTop), frameTop.atFraction(minFrameTop))
.target(new ParallaxTarget() {
@@ -87,9 +91,9 @@
@Override
public void update(float fraction) {
if (fraction == maxFrameTop) {
- updateState(STATE.NO_VIDEO);
+ updateState(NO_VIDEO);
} else {
- updateState(STATE.PLAY_VIDEO);
+ updateState(PLAY_VIDEO);
}
mFraction = fraction;
}
@@ -101,7 +105,15 @@
});
}
- private void updateState(STATE state) {
+ void stopParallax() {
+ mDetailsParallax.removeEffect(mParallaxEffect);
+ }
+
+ boolean isVideoVisible() {
+ return mCurrentState == PLAY_VIDEO;
+ }
+
+ private void updateState(int state) {
if (state == mCurrentState) {
return;
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index b26aabc..ac11fde 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -13,13 +13,16 @@
*/
package android.support.v17.leanback.app;
+import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.CallSuper;
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
import android.support.v17.leanback.widget.BrowseFrameLayout;
@@ -38,6 +41,8 @@
import android.view.View;
import android.view.ViewGroup;
+import java.lang.ref.WeakReference;
+
/**
* A fragment for creating Leanback details screens.
*
@@ -76,6 +81,23 @@
static final String TAG = "DetailsFragment";
static boolean DEBUG = false;
+ /**
+ * Flag for "possibly" having enter transition not finished yet.
+ * @see #mStartAndTransitionFlag
+ */
+ static final int PF_ENTER_TRANSITION_PENDING = 0x1 << 0;
+ /**
+ * Flag for having entrance transition not finished yet.
+ * @see #mStartAndTransitionFlag
+ */
+ static final int PF_ENTRANCE_TRANSITION_PENDING = 0x1 << 1;
+ /**
+ * Flag that onStart() has been called and about to call onSafeStart() when
+ * pending transitions are finished.
+ * @see #mStartAndTransitionFlag
+ */
+ static final int PF_PENDING_START = 0x1 << 2;
+
private class SetSelectionRunnable implements Runnable {
int mPosition;
boolean mSmooth = true;
@@ -92,6 +114,61 @@
}
}
+ /**
+ * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
+ * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
+ * @see #mStartAndTransitionFlag
+ */
+ static class WaitEnterTransitionTimeout implements Runnable {
+ static final long WAIT_ENTERTRANSITION_START = 200;
+
+ final WeakReference<DetailsFragment> mRef;
+
+ WaitEnterTransitionTimeout(DetailsFragment f) {
+ mRef = new WeakReference(f);
+ f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
+ }
+
+ @Override
+ public void run() {
+ DetailsFragment f = mRef.get();
+ if (f != null) {
+ f.clearPendingEnterTransition();
+ }
+ }
+ }
+
+ /**
+ * @see #mStartAndTransitionFlag
+ */
+ TransitionListener mEnterTransitionListener = new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ if (mWaitEnterTransitionTimeout != null) {
+ // cancel task of WaitEnterTransitionTimeout, we will clearPendingEnterTransition
+ // when transition finishes.
+ mWaitEnterTransitionTimeout.mRef.clear();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(Object transition) {
+ clearPendingEnterTransition();
+ }
+
+ @Override
+ public void onTransitionEnd(Object transition) {
+ clearPendingEnterTransition();
+ }
+ };
+
+ TransitionListener mReturnTransitionListener = new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ onReturnTransitionStart();
+ }
+ };
+
BrowseFrameLayout mRootView;
View mBackgroundView;
Drawable mBackgroundDrawable;
@@ -104,6 +181,25 @@
BaseOnItemViewClickedListener mOnItemViewClickedListener;
DetailsFragmentBackgroundController mDetailsBackgroundController;
+
+ /**
+ * Flags for enter transition, entrance transition and onStart. When onStart() is called
+ * and both enter transiton and entrance transition are finished, we could call onSafeStart().
+ * 1. in onCreate:
+ * if user call prepareEntranceTransition, set PF_ENTRANCE_TRANSITION_PENDING
+ * if there is enterTransition, set PF_ENTER_TRANSITION_PENDING, but we dont know if
+ * user will run enterTransition or not.
+ * 2. when user add row, start WaitEnterTransitionTimeout to wait possible enter transition
+ * start. If enter transition onTransitionStart is not invoked with a period, we can assume
+ * there is no enter transition running, then WaitEnterTransitionTimeout will clear
+ * PF_ENTER_TRANSITION_PENDING.
+ * 3. When enterTransition runs (either postponed or not), we will stop the
+ * WaitEnterTransitionTimeout, and let onTransitionEnd/onTransitionCancel to clear
+ * PF_ENTER_TRANSITION_PENDING.
+ */
+ int mStartAndTransitionFlag = 0;
+ WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
+
Object mSceneAfterEntranceTransition;
final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
@@ -181,6 +277,19 @@
super.onCreate(savedInstanceState);
mContainerListAlignTop =
getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
+
+ Activity activity = getActivity();
+ if (activity != null) {
+ Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
+ if (transition != null) {
+ mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
+ TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+ }
+ transition = TransitionHelper.getReturnTransition(activity.getWindow());
+ if (transition != null) {
+ TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
+ }
+ }
}
@Override
@@ -369,6 +478,11 @@
if (adapter != null && adapter.size() > selectedPosition) {
final VerticalGridView gridView = getVerticalGridView();
final int count = gridView.getChildCount();
+ if (count > 0 && (mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
+ if (mWaitEnterTransitionTimeout == null) {
+ mWaitEnterTransitionTimeout = new WaitEnterTransitionTimeout(this);
+ }
+ }
for (int i = 0; i < count; i++) {
ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
gridView.getChildViewHolder(gridView.getChildAt(i));
@@ -381,6 +495,52 @@
}
}
+ void clearPendingEnterTransition() {
+ if ((mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
+ mStartAndTransitionFlag &= ~PF_ENTER_TRANSITION_PENDING;
+ dispatchOnStartAndTransitionFinished();
+ }
+ }
+
+ void dispatchOnStartAndTransitionFinished() {
+ /**
+ * if onStart() was called and there is no pending enter transition or entrance transition.
+ */
+ if ((mStartAndTransitionFlag & PF_PENDING_START) != 0
+ && (mStartAndTransitionFlag
+ & (PF_ENTER_TRANSITION_PENDING | PF_ENTRANCE_TRANSITION_PENDING)) == 0) {
+ mStartAndTransitionFlag &= ~PF_PENDING_START;
+ onSafeStart();
+ }
+ }
+
+ /**
+ * Called when onStart and enter transition (postponed/none postponed) and entrance transition
+ * are all finished.
+ */
+ @CallSuper
+ void onSafeStart() {
+ if (mDetailsBackgroundController != null) {
+ mDetailsBackgroundController.enablePlaybackHost();
+ }
+ }
+
+ @CallSuper
+ void onReturnTransitionStart() {
+ if (mDetailsBackgroundController != null) {
+ // first disable parallax effect that auto-start PlaybackGlue.
+ boolean isVideoVisible = mDetailsBackgroundController.disableVideoParallax();
+ // if video is not visible we can safely remove VideoFragment,
+ // otherwise let video playing during return transition.
+ if (!isVideoVisible && mVideoFragment != null) {
+ FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
+ ft2.remove(mVideoFragment);
+ ft2.commit();
+ mVideoFragment = null;
+ }
+ }
+ }
+
/**
* Called on every visible row to change view status when current selected row position
* or selected sub position changed. Subclass may override. The default
@@ -439,6 +599,10 @@
@Override
public void onStart() {
super.onStart();
+
+ mStartAndTransitionFlag |= PF_PENDING_START;
+ dispatchOnStartAndTransitionFinished();
+
setupChildFragmentLayout();
if (isEntranceTransitionEnabled()) {
mRowsFragment.setEntranceTransitionState(false);
@@ -462,11 +626,14 @@
@Override
protected void onEntranceTransitionEnd() {
+ mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
+ dispatchOnStartAndTransitionFinished();
mRowsFragment.onTransitionEnd();
}
@Override
protected void onEntranceTransitionPrepare() {
+ mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
mRowsFragment.onTransitionPrepare();
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
index 9f9ed26..a0bf00c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -114,6 +114,7 @@
private DetailsBackgroundVideoHelper mVideoHelper;
private Bitmap mCoverBitmap;
private int mSolidColor;
+ private boolean mCanUseHost = false;
/**
* Creates a DetailsFragmentBackgroundController for a DetailsFragment. Note that
@@ -208,17 +209,47 @@
/**
* Enable video playback and set proper {@link PlaybackGlueHost}. This method by default
* creates a VideoFragment and VideoFragmentGlueHost to host the PlaybackGlue.
- * This method must be called after calling Fragment super.onCreate().
+ * This method must be called after calling details Fragment super.onCreate().
*
* @param playbackGlue
* @see #onCreateVideoFragment()
* @see #onCreateGlueHost().
*/
public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {
+ if (mPlaybackGlue == playbackGlue) {
+ return;
+ }
mPlaybackGlue = playbackGlue;
- mPlaybackGlue.setHost(onCreateGlueHost());
mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
+ if (mCanUseHost) {
+ mPlaybackGlue.setHost(onCreateGlueHost());
+ }
+ }
+
+ /**
+ * Enable Host for PlaybackGlue. This is delayed until: onStart() is called,
+ * activity transitions are finished.
+ */
+ void enablePlaybackHost() {
+ if (!mCanUseHost) {
+ mCanUseHost = true;
+ if (mPlaybackGlue != null) {
+ mPlaybackGlue.setHost(onCreateGlueHost());
+ }
+ }
+ }
+
+ /**
+ * Disable parallax that would auto-start video playback
+ * @return true if video fragment is visible or false otherwise.
+ */
+ boolean disableVideoParallax() {
+ if (mVideoHelper != null) {
+ mVideoHelper.stopParallax();
+ return mVideoHelper.isVideoVisible();
+ }
+ return false;
}
/**
@@ -360,15 +391,4 @@
return mParallaxDrawableMaxOffset;
}
- void onStart() {
- if (mPlaybackGlue != null) {
- mPlaybackGlue.play();
- }
- }
-
- void onStop() {
- if (mPlaybackGlue != null) {
- mPlaybackGlue.pause();
- }
- }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
index 2a759de..53300ac 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -16,13 +16,16 @@
*/
package android.support.v17.leanback.app;
+import android.support.v4.app.FragmentActivity;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.CallSuper;
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.transition.TransitionListener;
import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
import android.support.v17.leanback.widget.BrowseFrameLayout;
@@ -41,6 +44,8 @@
import android.view.View;
import android.view.ViewGroup;
+import java.lang.ref.WeakReference;
+
/**
* A fragment for creating Leanback details screens.
*
@@ -79,6 +84,23 @@
static final String TAG = "DetailsSupportFragment";
static boolean DEBUG = false;
+ /**
+ * Flag for "possibly" having enter transition not finished yet.
+ * @see #mStartAndTransitionFlag
+ */
+ static final int PF_ENTER_TRANSITION_PENDING = 0x1 << 0;
+ /**
+ * Flag for having entrance transition not finished yet.
+ * @see #mStartAndTransitionFlag
+ */
+ static final int PF_ENTRANCE_TRANSITION_PENDING = 0x1 << 1;
+ /**
+ * Flag that onStart() has been called and about to call onSafeStart() when
+ * pending transitions are finished.
+ * @see #mStartAndTransitionFlag
+ */
+ static final int PF_PENDING_START = 0x1 << 2;
+
private class SetSelectionRunnable implements Runnable {
int mPosition;
boolean mSmooth = true;
@@ -95,6 +117,61 @@
}
}
+ /**
+ * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
+ * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
+ * @see #mStartAndTransitionFlag
+ */
+ static class WaitEnterTransitionTimeout implements Runnable {
+ static final long WAIT_ENTERTRANSITION_START = 200;
+
+ final WeakReference<DetailsSupportFragment> mRef;
+
+ WaitEnterTransitionTimeout(DetailsSupportFragment f) {
+ mRef = new WeakReference(f);
+ f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
+ }
+
+ @Override
+ public void run() {
+ DetailsSupportFragment f = mRef.get();
+ if (f != null) {
+ f.clearPendingEnterTransition();
+ }
+ }
+ }
+
+ /**
+ * @see #mStartAndTransitionFlag
+ */
+ TransitionListener mEnterTransitionListener = new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ if (mWaitEnterTransitionTimeout != null) {
+ // cancel task of WaitEnterTransitionTimeout, we will clearPendingEnterTransition
+ // when transition finishes.
+ mWaitEnterTransitionTimeout.mRef.clear();
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(Object transition) {
+ clearPendingEnterTransition();
+ }
+
+ @Override
+ public void onTransitionEnd(Object transition) {
+ clearPendingEnterTransition();
+ }
+ };
+
+ TransitionListener mReturnTransitionListener = new TransitionListener() {
+ @Override
+ public void onTransitionStart(Object transition) {
+ onReturnTransitionStart();
+ }
+ };
+
BrowseFrameLayout mRootView;
View mBackgroundView;
Drawable mBackgroundDrawable;
@@ -107,6 +184,25 @@
BaseOnItemViewClickedListener mOnItemViewClickedListener;
DetailsSupportFragmentBackgroundController mDetailsBackgroundController;
+
+ /**
+ * Flags for enter transition, entrance transition and onStart. When onStart() is called
+ * and both enter transiton and entrance transition are finished, we could call onSafeStart().
+ * 1. in onCreate:
+ * if user call prepareEntranceTransition, set PF_ENTRANCE_TRANSITION_PENDING
+ * if there is enterTransition, set PF_ENTER_TRANSITION_PENDING, but we dont know if
+ * user will run enterTransition or not.
+ * 2. when user add row, start WaitEnterTransitionTimeout to wait possible enter transition
+ * start. If enter transition onTransitionStart is not invoked with a period, we can assume
+ * there is no enter transition running, then WaitEnterTransitionTimeout will clear
+ * PF_ENTER_TRANSITION_PENDING.
+ * 3. When enterTransition runs (either postponed or not), we will stop the
+ * WaitEnterTransitionTimeout, and let onTransitionEnd/onTransitionCancel to clear
+ * PF_ENTER_TRANSITION_PENDING.
+ */
+ int mStartAndTransitionFlag = 0;
+ WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
+
Object mSceneAfterEntranceTransition;
final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
@@ -184,6 +280,19 @@
super.onCreate(savedInstanceState);
mContainerListAlignTop =
getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
+
+ FragmentActivity activity = getActivity();
+ if (activity != null) {
+ Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
+ if (transition != null) {
+ mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
+ TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+ }
+ transition = TransitionHelper.getReturnTransition(activity.getWindow());
+ if (transition != null) {
+ TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
+ }
+ }
}
@Override
@@ -372,6 +481,11 @@
if (adapter != null && adapter.size() > selectedPosition) {
final VerticalGridView gridView = getVerticalGridView();
final int count = gridView.getChildCount();
+ if (count > 0 && (mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
+ if (mWaitEnterTransitionTimeout == null) {
+ mWaitEnterTransitionTimeout = new WaitEnterTransitionTimeout(this);
+ }
+ }
for (int i = 0; i < count; i++) {
ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
gridView.getChildViewHolder(gridView.getChildAt(i));
@@ -384,6 +498,52 @@
}
}
+ void clearPendingEnterTransition() {
+ if ((mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
+ mStartAndTransitionFlag &= ~PF_ENTER_TRANSITION_PENDING;
+ dispatchOnStartAndTransitionFinished();
+ }
+ }
+
+ void dispatchOnStartAndTransitionFinished() {
+ /**
+ * if onStart() was called and there is no pending enter transition or entrance transition.
+ */
+ if ((mStartAndTransitionFlag & PF_PENDING_START) != 0
+ && (mStartAndTransitionFlag
+ & (PF_ENTER_TRANSITION_PENDING | PF_ENTRANCE_TRANSITION_PENDING)) == 0) {
+ mStartAndTransitionFlag &= ~PF_PENDING_START;
+ onSafeStart();
+ }
+ }
+
+ /**
+ * Called when onStart and enter transition (postponed/none postponed) and entrance transition
+ * are all finished.
+ */
+ @CallSuper
+ void onSafeStart() {
+ if (mDetailsBackgroundController != null) {
+ mDetailsBackgroundController.enablePlaybackHost();
+ }
+ }
+
+ @CallSuper
+ void onReturnTransitionStart() {
+ if (mDetailsBackgroundController != null) {
+ // first disable parallax effect that auto-start PlaybackGlue.
+ boolean isVideoVisible = mDetailsBackgroundController.disableVideoParallax();
+ // if video is not visible we can safely remove VideoSupportFragment,
+ // otherwise let video playing during return transition.
+ if (!isVideoVisible && mVideoSupportFragment != null) {
+ FragmentTransaction ft2 = getChildFragmentManager().beginTransaction();
+ ft2.remove(mVideoSupportFragment);
+ ft2.commit();
+ mVideoSupportFragment = null;
+ }
+ }
+ }
+
/**
* Called on every visible row to change view status when current selected row position
* or selected sub position changed. Subclass may override. The default
@@ -442,6 +602,10 @@
@Override
public void onStart() {
super.onStart();
+
+ mStartAndTransitionFlag |= PF_PENDING_START;
+ dispatchOnStartAndTransitionFinished();
+
setupChildFragmentLayout();
if (isEntranceTransitionEnabled()) {
mRowsSupportFragment.setEntranceTransitionState(false);
@@ -465,11 +629,14 @@
@Override
protected void onEntranceTransitionEnd() {
+ mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
+ dispatchOnStartAndTransitionFinished();
mRowsSupportFragment.onTransitionEnd();
}
@Override
protected void onEntranceTransitionPrepare() {
+ mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
mRowsSupportFragment.onTransitionPrepare();
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
index e450c3b..5ab42c3 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -117,6 +117,7 @@
private DetailsBackgroundVideoHelper mVideoHelper;
private Bitmap mCoverBitmap;
private int mSolidColor;
+ private boolean mCanUseHost = false;
/**
* Creates a DetailsSupportFragmentBackgroundController for a DetailsSupportFragment. Note that
@@ -211,17 +212,47 @@
/**
* Enable video playback and set proper {@link PlaybackGlueHost}. This method by default
* creates a VideoSupportFragment and VideoSupportFragmentGlueHost to host the PlaybackGlue.
- * This method must be called after calling Fragment super.onCreate().
+ * This method must be called after calling details Fragment super.onCreate().
*
* @param playbackGlue
* @see #onCreateVideoSupportFragment()
* @see #onCreateGlueHost().
*/
public void setupVideoPlayback(@NonNull PlaybackGlue playbackGlue) {
+ if (mPlaybackGlue == playbackGlue) {
+ return;
+ }
mPlaybackGlue = playbackGlue;
- mPlaybackGlue.setHost(onCreateGlueHost());
mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
+ if (mCanUseHost) {
+ mPlaybackGlue.setHost(onCreateGlueHost());
+ }
+ }
+
+ /**
+ * Enable Host for PlaybackGlue. This is delayed until: onStart() is called,
+ * activity transitions are finished.
+ */
+ void enablePlaybackHost() {
+ if (!mCanUseHost) {
+ mCanUseHost = true;
+ if (mPlaybackGlue != null) {
+ mPlaybackGlue.setHost(onCreateGlueHost());
+ }
+ }
+ }
+
+ /**
+ * Disable parallax that would auto-start video playback
+ * @return true if video fragment is visible or false otherwise.
+ */
+ boolean disableVideoParallax() {
+ if (mVideoHelper != null) {
+ mVideoHelper.stopParallax();
+ return mVideoHelper.isVideoVisible();
+ }
+ return false;
}
/**
@@ -363,15 +394,4 @@
return mParallaxDrawableMaxOffset;
}
- void onStart() {
- if (mPlaybackGlue != null) {
- mPlaybackGlue.play();
- }
- }
-
- void onStop() {
- if (mPlaybackGlue != null) {
- mPlaybackGlue.pause();
- }
- }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java b/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
new file mode 100644
index 0000000..29d778c
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/VideoSurfaceView.java
@@ -0,0 +1,50 @@
+/*
+ * 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 android.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.SurfaceView;
+
+/**
+ * Activity transition will change transitionVisibility multiple times even the view is not
+ * running transition, which causes visual flickering during activity return transition.
+ * This class disables setTransitionVisibility() to avoid the problem.
+ * @hide
+ */
+public class VideoSurfaceView extends SurfaceView {
+
+ public VideoSurfaceView(Context context) {
+ super(context);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Overrides hidden method View.setTransitionVisibility() to disable visibility changes
+ * in activity transition.
+ */
+ public void setTransitionVisibility(int visibility) {
+ }
+
+}