Port fix for Fade
Fix the behavior of Fade Transition when it is interrupted and
reversed.
Revised I65b6e32bcb144a410552cafee984596704a76d5d
Test: FadeTest pass on 15, 18, 19, 21, and 25
Bug: 31363964
Change-Id: I912f00a8d55f68b11bbed74bfe6c71fdb5fb592a
diff --git a/transition/api14/android/support/transition/AnimatorUtilsApi14.java b/transition/api14/android/support/transition/AnimatorUtilsApi14.java
index c3d47cc..d9f870b 100644
--- a/transition/api14/android/support/transition/AnimatorUtilsApi14.java
+++ b/transition/api14/android/support/transition/AnimatorUtilsApi14.java
@@ -21,6 +21,8 @@
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
+import java.util.ArrayList;
+
@RequiresApi(14)
class AnimatorUtilsApi14 implements AnimatorUtilsImpl {
@@ -30,4 +32,43 @@
// Do nothing
}
+ @Override
+ public void pause(@NonNull Animator animator) {
+ final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
+ if (listeners != null) {
+ for (int i = 0, size = listeners.size(); i < size; i++) {
+ final Animator.AnimatorListener listener = listeners.get(i);
+ if (listener instanceof AnimatorPauseListenerCompat) {
+ ((AnimatorPauseListenerCompat) listener).onAnimationPause(animator);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void resume(@NonNull Animator animator) {
+ final ArrayList<Animator.AnimatorListener> listeners = animator.getListeners();
+ if (listeners != null) {
+ for (int i = 0, size = listeners.size(); i < size; i++) {
+ final Animator.AnimatorListener listener = listeners.get(i);
+ if (listener instanceof AnimatorPauseListenerCompat) {
+ ((AnimatorPauseListenerCompat) listener).onAnimationResume(animator);
+ }
+ }
+ }
+ }
+
+ /**
+ * Listeners can implement this interface in addition to the platform AnimatorPauseListener to
+ * make them compatible with API level 18 and below. Animators will not be paused or resumed,
+ * but the callbacks here are invoked.
+ */
+ interface AnimatorPauseListenerCompat {
+
+ void onAnimationPause(Animator animation);
+
+ void onAnimationResume(Animator animation);
+
+ }
+
}
diff --git a/transition/api14/android/support/transition/GhostViewApi14.java b/transition/api14/android/support/transition/GhostViewApi14.java
index c164a4c..9c91c99 100644
--- a/transition/api14/android/support/transition/GhostViewApi14.java
+++ b/transition/api14/android/support/transition/GhostViewApi14.java
@@ -68,7 +68,9 @@
if (ghostView.mReferences <= 0) {
ViewParent parent = ghostView.getParent();
if (parent instanceof ViewGroup) {
- ((ViewGroup) parent).removeView(ghostView);
+ ViewGroup group = (ViewGroup) parent;
+ group.endViewTransition(ghostView);
+ group.removeView(ghostView);
}
}
}
diff --git a/transition/api19/android/support/transition/AnimatorUtilsApi19.java b/transition/api19/android/support/transition/AnimatorUtilsApi19.java
index 38e3942..0f4ae6b 100644
--- a/transition/api19/android/support/transition/AnimatorUtilsApi19.java
+++ b/transition/api19/android/support/transition/AnimatorUtilsApi19.java
@@ -21,7 +21,7 @@
import android.support.annotation.RequiresApi;
@RequiresApi(19)
-class AnimatorUtilsApi19 extends AnimatorUtilsApi14 {
+class AnimatorUtilsApi19 implements AnimatorUtilsImpl {
@Override
public void addPauseListener(@NonNull Animator animator,
@@ -29,4 +29,14 @@
animator.addPauseListener(listener);
}
+ @Override
+ public void pause(@NonNull Animator animator) {
+ animator.pause();
+ }
+
+ @Override
+ public void resume(@NonNull Animator animator) {
+ animator.resume();
+ }
+
}
diff --git a/transition/base/android/support/transition/AnimatorUtilsImpl.java b/transition/base/android/support/transition/AnimatorUtilsImpl.java
index ce1c9cc..68f222d 100644
--- a/transition/base/android/support/transition/AnimatorUtilsImpl.java
+++ b/transition/base/android/support/transition/AnimatorUtilsImpl.java
@@ -24,4 +24,8 @@
void addPauseListener(@NonNull Animator animator, @NonNull AnimatorListenerAdapter listener);
+ void pause(@NonNull Animator animator);
+
+ void resume(@NonNull Animator animator);
+
}
diff --git a/transition/src/android/support/transition/AnimatorUtils.java b/transition/src/android/support/transition/AnimatorUtils.java
index 4bd7625..215d768 100644
--- a/transition/src/android/support/transition/AnimatorUtils.java
+++ b/transition/src/android/support/transition/AnimatorUtils.java
@@ -38,4 +38,12 @@
IMPL.addPauseListener(animator, listener);
}
+ static void pause(@NonNull Animator animator) {
+ IMPL.pause(animator);
+ }
+
+ static void resume(@NonNull Animator animator) {
+ IMPL.resume(animator);
+ }
+
}
diff --git a/transition/src/android/support/transition/Fade.java b/transition/src/android/support/transition/Fade.java
index 1b65c18..230895c 100644
--- a/transition/src/android/support/transition/Fade.java
+++ b/transition/src/android/support/transition/Fade.java
@@ -22,6 +22,7 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.support.annotation.NonNull;
import android.support.v4.content.res.TypedArrayUtils;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
@@ -60,6 +61,8 @@
*/
public class Fade extends Visibility {
+ private static final String PROPNAME_TRANSITION_ALPHA = "android:fade:transitionAlpha";
+
private static final String LOG_TAG = "Fade";
/**
@@ -103,10 +106,17 @@
a.recycle();
}
+ @Override
+ public void captureStartValues(@NonNull TransitionValues transitionValues) {
+ super.captureStartValues(transitionValues);
+ transitionValues.values.put(PROPNAME_TRANSITION_ALPHA,
+ ViewUtils.getTransitionAlpha(transitionValues.view));
+ }
+
/**
* Utility method to handle creating and running the Animator.
*/
- private Animator createAnimation(View view, float startAlpha, float endAlpha) {
+ private Animator createAnimation(final View view, float startAlpha, float endAlpha) {
if (startAlpha == endAlpha) {
return null;
}
@@ -118,7 +128,13 @@
}
FadeAnimatorListener listener = new FadeAnimatorListener(view);
anim.addListener(listener);
- AnimatorUtils.addPauseListener(anim, listener);
+ addListener(new TransitionListenerAdapter() {
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ ViewUtils.setTransitionAlpha(view, 1);
+ transition.removeListener(this);
+ }
+ });
return anim;
}
@@ -131,20 +147,34 @@
Log.d(LOG_TAG, "Fade.onAppear: startView, startVis, endView, endVis = "
+ startView + ", " + view);
}
- return createAnimation(view, 0, 1);
+ float startAlpha = getStartAlpha(startValues, 0);
+ if (startAlpha == 1) {
+ startAlpha = 0;
+ }
+ return createAnimation(view, startAlpha, 1);
}
@Override
public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues,
TransitionValues endValues) {
- return createAnimation(view, 1, 0);
+ float startAlpha = getStartAlpha(startValues, 1);
+ return createAnimation(view, startAlpha, 0);
+ }
+
+ private static float getStartAlpha(TransitionValues startValues, float fallbackValue) {
+ float startAlpha = fallbackValue;
+ if (startValues != null) {
+ Float startAlphaFloat = (Float) startValues.values.get(PROPNAME_TRANSITION_ALPHA);
+ if (startAlphaFloat != null) {
+ startAlpha = startAlphaFloat;
+ }
+ }
+ return startAlpha;
}
private static class FadeAnimatorListener extends AnimatorListenerAdapter {
private final View mView;
- private boolean mCanceled = false;
- private float mPausedAlpha = -1;
private boolean mLayerTypeChanged = false;
FadeAnimatorListener(View view) {
@@ -161,34 +191,13 @@
}
@Override
- public void onAnimationCancel(Animator animation) {
- mCanceled = true;
- if (mPausedAlpha >= 0) {
- ViewUtils.setTransitionAlpha(mView, mPausedAlpha);
- }
- }
-
- @Override
public void onAnimationEnd(Animator animation) {
- if (!mCanceled) {
- ViewUtils.setTransitionAlpha(mView, 1);
- }
+ ViewUtils.setTransitionAlpha(mView, 1);
if (mLayerTypeChanged) {
mView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}
- @Override
- public void onAnimationPause(Animator animation) {
- mPausedAlpha = ViewUtils.getTransitionAlpha(mView);
- ViewUtils.setTransitionAlpha(mView, 1);
- }
-
- @Override
- public void onAnimationResume(Animator animation) {
- ViewUtils.setTransitionAlpha(mView, mPausedAlpha);
- }
-
}
}
diff --git a/transition/src/android/support/transition/Transition.java b/transition/src/android/support/transition/Transition.java
index 807e353..8e15b75 100644
--- a/transition/src/android/support/transition/Transition.java
+++ b/transition/src/android/support/transition/Transition.java
@@ -1721,7 +1721,7 @@
AnimationInfo info = runningAnimators.valueAt(i);
if (info.mView != null && windowId.equals(info.mWindowId)) {
Animator anim = runningAnimators.keyAt(i);
- anim.cancel(); // pause() is API Level 19
+ AnimatorUtils.pause(anim);
}
}
if (mListeners != null && mListeners.size() > 0) {
@@ -1754,7 +1754,7 @@
AnimationInfo info = runningAnimators.valueAt(i);
if (info.mView != null && windowId.equals(info.mWindowId)) {
Animator anim = runningAnimators.keyAt(i);
- anim.end(); // resume() is API Level 19
+ AnimatorUtils.resume(anim);
}
}
if (mListeners != null && mListeners.size() > 0) {
@@ -1787,7 +1787,8 @@
Animator anim = runningAnimators.keyAt(i);
if (anim != null) {
AnimationInfo oldInfo = runningAnimators.get(anim);
- if (oldInfo != null && oldInfo.mView != null && oldInfo.mWindowId == windowId) {
+ if (oldInfo != null && oldInfo.mView != null
+ && windowId.equals(oldInfo.mWindowId)) {
TransitionValues oldValues = oldInfo.mValues;
View oldView = oldInfo.mView;
TransitionValues startValues = getTransitionValues(oldView, true);
@@ -2178,6 +2179,8 @@
clone.mAnimators = new ArrayList<>();
clone.mStartValues = new TransitionValuesMaps();
clone.mEndValues = new TransitionValuesMaps();
+ clone.mStartValuesList = null;
+ clone.mEndValuesList = null;
return clone;
} catch (CloneNotSupportedException e) {
return null;
diff --git a/transition/src/android/support/transition/TransitionManager.java b/transition/src/android/support/transition/TransitionManager.java
index 508e4a6..105aca4 100644
--- a/transition/src/android/support/transition/TransitionManager.java
+++ b/transition/src/android/support/transition/TransitionManager.java
@@ -166,22 +166,27 @@
private static void changeScene(Scene scene, Transition transition) {
final ViewGroup sceneRoot = scene.getSceneRoot();
- Transition transitionClone = null;
- if (transition != null) {
- transitionClone = transition.clone();
- transitionClone.setSceneRoot(sceneRoot);
+ if (!sPendingTransitions.contains(sceneRoot)) {
+ if (transition == null) {
+ scene.enter();
+ } else {
+ sPendingTransitions.add(sceneRoot);
+
+ Transition transitionClone = transition.clone();
+ transitionClone.setSceneRoot(sceneRoot);
+
+ Scene oldScene = Scene.getCurrentScene(sceneRoot);
+ if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
+ transitionClone.setCanRemoveViews(true);
+ }
+
+ sceneChangeSetup(sceneRoot, transitionClone);
+
+ scene.enter();
+
+ sceneChangeRunTransition(sceneRoot, transitionClone);
+ }
}
-
- Scene oldScene = Scene.getCurrentScene(sceneRoot);
- if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
- transitionClone.setCanRemoveViews(true);
- }
-
- sceneChangeSetup(sceneRoot, transitionClone);
-
- scene.enter();
-
- sceneChangeRunTransition(sceneRoot, transitionClone);
}
static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
@@ -250,7 +255,12 @@
@Override
public boolean onPreDraw() {
removeListeners();
- sPendingTransitions.remove(mSceneRoot);
+
+ // Don't start the transition if it's no longer pending.
+ if (!sPendingTransitions.remove(mSceneRoot)) {
+ return true;
+ }
+
// Add to running list, handle end to remove it
final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
getRunningTransitions();
diff --git a/transition/src/android/support/transition/ViewUtils.java b/transition/src/android/support/transition/ViewUtils.java
index 0bf0d61..6654279 100644
--- a/transition/src/android/support/transition/ViewUtils.java
+++ b/transition/src/android/support/transition/ViewUtils.java
@@ -21,15 +21,23 @@
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
+import android.util.Log;
import android.util.Property;
import android.view.View;
+import java.lang.reflect.Field;
+
/**
* Compatibility utilities for platform features of {@link View}.
*/
class ViewUtils {
private static final ViewUtilsImpl IMPL;
+ private static final String TAG = "ViewUtils";
+
+ private static Field sViewFlagsField;
+ private static boolean sViewFlagsFieldFetched;
+ private static final int VISIBILITY_MASK = 0x0000000C;
static {
if (Build.VERSION.SDK_INT >= 21) {
@@ -99,6 +107,31 @@
}
/**
+ * Copy of a hidden platform method, View#setTransitionVisibility.
+ *
+ * <p>Change the visibility of the View without triggering any other changes. This is
+ * important for transitions, where visibility changes should not adjust focus or
+ * trigger a new layout. This is only used when the visibility has already been changed
+ * and we need a transient value during an animation. When the animation completes,
+ * the original visibility value is always restored.</p>
+ *
+ * @param view The target view.
+ * @param visibility One of {@link View#VISIBLE}, {@link View#INVISIBLE}, or
+ * {@link View#GONE}.
+ */
+ static void setTransitionVisibility(@NonNull View view, int visibility) {
+ fetchViewFlagsField();
+ if (sViewFlagsField != null) {
+ try {
+ int viewFlags = sViewFlagsField.getInt(view);
+ sViewFlagsField.setInt(view, (viewFlags & ~VISIBILITY_MASK) | visibility);
+ } catch (IllegalAccessException e) {
+ // Do nothing
+ }
+ }
+ }
+
+ /**
* Modifies the input matrix such that it maps view-local coordinates to
* on-screen coordinates.
*
@@ -140,4 +173,16 @@
IMPL.setAnimationMatrix(v, m);
}
+ private static void fetchViewFlagsField() {
+ if (!sViewFlagsFieldFetched) {
+ try {
+ sViewFlagsField = View.class.getDeclaredField("mViewFlags");
+ sViewFlagsField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ Log.i(TAG, "fetchViewFlagsField: ");
+ }
+ sViewFlagsFieldFetched = true;
+ }
+ }
+
}
diff --git a/transition/src/android/support/transition/Visibility.java b/transition/src/android/support/transition/Visibility.java
index 0c6c9d9..b71db02 100644
--- a/transition/src/android/support/transition/Visibility.java
+++ b/transition/src/android/support/transition/Visibility.java
@@ -413,49 +413,20 @@
if (viewToKeep != null) {
int originalVisibility = viewToKeep.getVisibility();
- viewToKeep.setVisibility(View.VISIBLE);
+ ViewUtils.setTransitionVisibility(viewToKeep, View.VISIBLE);
Animator animator = onDisappear(sceneRoot, viewToKeep, startValues, endValues);
if (animator != null) {
- final View finalViewToKeep = viewToKeep;
- animator.addListener(new AnimatorListenerAdapter() {
- boolean mCanceled = false;
-
- @Override
- public void onAnimationPause(Animator animation) {
- if (!mCanceled) {
- //noinspection WrongConstant
- finalViewToKeep.setVisibility(finalVisibility);
- }
- }
-
- @Override
- public void onAnimationResume(Animator animation) {
- if (!mCanceled) {
- finalViewToKeep.setVisibility(View.VISIBLE);
- }
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCanceled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- if (!mCanceled) {
- //noinspection WrongConstant
- finalViewToKeep.setVisibility(finalVisibility);
- }
- }
- });
+ DisappearListener disappearListener = new DisappearListener(viewToKeep,
+ finalVisibility, true);
+ animator.addListener(disappearListener);
+ AnimatorUtils.addPauseListener(animator, disappearListener);
+ addListener(disappearListener);
} else {
- viewToKeep.setVisibility(originalVisibility);
+ ViewUtils.setTransitionVisibility(viewToKeep, originalVisibility);
}
return animator;
}
return null;
-
-
}
/**
@@ -489,6 +460,107 @@
|| changeInfo.mEndVisibility == View.VISIBLE);
}
+ private static class DisappearListener extends AnimatorListenerAdapter
+ implements TransitionListener, AnimatorUtilsApi14.AnimatorPauseListenerCompat {
+
+ private final View mView;
+ private final int mFinalVisibility;
+ private final ViewGroup mParent;
+ private final boolean mSuppressLayout;
+
+ private boolean mLayoutSuppressed;
+ boolean mCanceled = false;
+
+ DisappearListener(View view, int finalVisibility, boolean suppressLayout) {
+ mView = view;
+ mFinalVisibility = finalVisibility;
+ mParent = (ViewGroup) view.getParent();
+ mSuppressLayout = suppressLayout;
+ // Prevent a layout from including mView in its calculation.
+ suppressLayout(true);
+ }
+
+ // This overrides both AnimatorListenerAdapter and
+ // AnimatorUtilsApi14.AnimatorPauseListenerCompat
+ @Override
+ public void onAnimationPause(Animator animation) {
+ if (!mCanceled) {
+ ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
+ }
+ }
+
+ // This overrides both AnimatorListenerAdapter and
+ // AnimatorUtilsApi14.AnimatorPauseListenerCompat
+ @Override
+ public void onAnimationResume(Animator animation) {
+ if (!mCanceled) {
+ ViewUtils.setTransitionVisibility(mView, View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCanceled = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ hideViewWhenNotCanceled();
+ }
+
+ @Override
+ public void onTransitionStart(@NonNull Transition transition) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTransitionEnd(@NonNull Transition transition) {
+ hideViewWhenNotCanceled();
+ transition.removeListener(this);
+ }
+
+ @Override
+ public void onTransitionCancel(@NonNull Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(@NonNull Transition transition) {
+ suppressLayout(false);
+ }
+
+ @Override
+ public void onTransitionResume(@NonNull Transition transition) {
+ suppressLayout(true);
+ }
+
+ private void hideViewWhenNotCanceled() {
+ if (!mCanceled) {
+ // Recreate the parent's display list in case it includes mView.
+ ViewUtils.setTransitionVisibility(mView, mFinalVisibility);
+ if (mParent != null) {
+ mParent.invalidate();
+ }
+ }
+ // Layout is allowed now that the View is in its final state
+ suppressLayout(false);
+ }
+
+ private void suppressLayout(boolean suppress) {
+ if (mSuppressLayout && mLayoutSuppressed != suppress && mParent != null) {
+ mLayoutSuppressed = suppress;
+ ViewGroupUtils.suppressLayout(mParent, suppress);
+ }
+ }
+ }
+
// TODO: Implement API 23; isTransitionRequired
}
diff --git a/transition/tests/src/android/support/transition/FadeTest.java b/transition/tests/src/android/support/transition/FadeTest.java
index 0493a99..971015b 100644
--- a/transition/tests/src/android/support/transition/FadeTest.java
+++ b/transition/tests/src/android/support/transition/FadeTest.java
@@ -16,12 +16,26 @@
package android.support.transition;
+import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.lessThan;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.MediumTest;
import android.view.View;
@@ -100,4 +114,134 @@
assertThat(animator, is(nullValue()));
}
+ @Test
+ public void testFadeOutThenIn() throws Throwable {
+ // Fade out
+ final Runnable interrupt = mock(Runnable.class);
+ float[] valuesOut = new float[2];
+ final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, interrupt,
+ valuesOut);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+
+ // The view is in the middle of fading out
+ verify(interrupt, timeout(3000)).run();
+
+ // Fade in
+ float[] valuesIn = new float[2];
+ final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, null, valuesIn);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerOut, timeout(3000)).onTransitionPause(any(Transition.class));
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+ assertThat(valuesOut[1], allOf(greaterThan(0f), lessThan(1f)));
+ if (Build.VERSION.SDK_INT >= 19) {
+ // These won't match on API levels 18 and below due to lack of Animator pause.
+ assertEquals(valuesOut[1], valuesIn[0], 0.01f);
+ }
+
+ verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class));
+ assertThat(mView.getVisibility(), is(View.VISIBLE));
+ assertEquals(valuesIn[1], 1.f, 0.01f);
+ }
+
+ @Test
+ public void testFadeInThenOut() throws Throwable {
+ changeVisibility(null, mRoot, mView, View.INVISIBLE);
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ // Fade in
+ final Runnable interrupt = mock(Runnable.class);
+ float[] valuesIn = new float[2];
+ final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, interrupt, valuesIn);
+ final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
+ fadeIn.addListener(listenerIn);
+ changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
+
+ // The view is in the middle of fading in
+ verify(interrupt, timeout(3000)).run();
+
+ // Fade out
+ float[] valuesOut = new float[2];
+ final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, null, valuesOut);
+ final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
+ fadeOut.addListener(listenerOut);
+ changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
+ verify(listenerIn, timeout(3000)).onTransitionPause(any(Transition.class));
+ verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
+ assertThat(valuesIn[1], allOf(greaterThan(0f), lessThan(1f)));
+ if (Build.VERSION.SDK_INT >= 19) {
+ // These won't match on API levels 18 and below due to lack of Animator pause.
+ assertEquals(valuesIn[1], valuesOut[0], 0.01f);
+ }
+
+ verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
+ assertThat(mView.getVisibility(), is(View.INVISIBLE));
+ }
+
+ private void changeVisibility(final Fade fade, final ViewGroup container, final View target,
+ final int visibility) throws Throwable {
+ rule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (fade != null) {
+ TransitionManager.beginDelayedTransition(container, fade);
+ }
+ target.setVisibility(visibility);
+ }
+ });
+ }
+
+ /**
+ * A special version of {@link Fade} that runs a specified {@link Runnable} soon after the
+ * target starts fading in or out.
+ */
+ private static class InterruptibleFade extends Fade {
+
+ static final float ALPHA_THRESHOLD = 0.2f;
+
+ float mInitialAlpha = -1;
+ Runnable mMiddle;
+ final float[] mAlphaValues;
+
+ InterruptibleFade(int mode, Runnable middle, float[] alphaValues) {
+ super(mode);
+ mMiddle = middle;
+ mAlphaValues = alphaValues;
+ }
+
+ @Nullable
+ @Override
+ public Animator createAnimator(@NonNull ViewGroup sceneRoot,
+ @Nullable final TransitionValues startValues,
+ @Nullable final TransitionValues endValues) {
+ final Animator animator = super.createAnimator(sceneRoot, startValues, endValues);
+ if (animator instanceof ObjectAnimator) {
+ ((ObjectAnimator) animator).addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float alpha = (float) animation.getAnimatedValue();
+ mAlphaValues[1] = alpha;
+ if (mInitialAlpha < 0) {
+ mInitialAlpha = alpha;
+ mAlphaValues[0] = mInitialAlpha;
+ } else if (Math.abs(alpha - mInitialAlpha) > ALPHA_THRESHOLD) {
+ if (mMiddle != null) {
+ mMiddle.run();
+ mMiddle = null;
+ }
+ }
+ }
+ });
+ }
+ return animator;
+ }
+
+ }
+
}