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;
+        }
+
+    }
+
 }