Home | History | Annotate | Download | only in transition
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package androidx.transition;
     18 
     19 import android.content.Context;
     20 import android.util.Log;
     21 import android.view.View;
     22 import android.view.ViewGroup;
     23 import android.view.ViewTreeObserver;
     24 
     25 import androidx.annotation.NonNull;
     26 import androidx.annotation.Nullable;
     27 import androidx.collection.ArrayMap;
     28 import androidx.core.view.ViewCompat;
     29 
     30 import java.lang.ref.WeakReference;
     31 import java.util.ArrayList;
     32 
     33 /**
     34  * This class manages the set of transitions that fire when there is a
     35  * change of {@link Scene}. To use the manager, add scenes along with
     36  * transition objects with calls to {@link #setTransition(Scene, Transition)}
     37  * or {@link #setTransition(Scene, Scene, Transition)}. Setting specific
     38  * transitions for scene changes is not required; by default, a Scene change
     39  * will use {@link AutoTransition} to do something reasonable for most
     40  * situations. Specifying other transitions for particular scene changes is
     41  * only necessary if the application wants different transition behavior
     42  * in these situations.
     43  *
     44  * <p>TransitionManagers can be declared in XML resource files inside the
     45  * <code>res/transition</code> directory. TransitionManager resources consist of
     46  * the <code>transitionManager</code>tag name, containing one or more
     47  * <code>transition</code> tags, each of which describe the relationship of
     48  * that transition to the from/to scene information in that tag.
     49  * For example, here is a resource file that declares several scene
     50  * transitions:</p>
     51  *
     52  * <pre>
     53  *     &lt;transitionManager xmlns:android="http://schemas.android.com/apk/res/android"&gt;
     54  *         &lt;transition android:fromScene="@layout/transition_scene1"
     55  *                     android:toScene="@layout/transition_scene2"
     56  *                     android:transition="@transition/changebounds"/&gt;
     57  *         &lt;transition android:fromScene="@layout/transition_scene2"
     58  *                     android:toScene="@layout/transition_scene1"
     59  *                     android:transition="@transition/changebounds"/&gt;
     60  *         &lt;transition android:toScene="@layout/transition_scene3"
     61  *                     android:transition="@transition/changebounds_fadein_together"/&gt;
     62  *         &lt;transition android:fromScene="@layout/transition_scene3"
     63  *                     android:toScene="@layout/transition_scene1"
     64  *                     android:transition="@transition/changebounds_fadeout_sequential"/&gt;
     65  *         &lt;transition android:fromScene="@layout/transition_scene3"
     66  *                     android:toScene="@layout/transition_scene2"
     67  *                     android:transition="@transition/changebounds_fadeout_sequential"/&gt;
     68  *     &lt;/transitionManager&gt;
     69  * </pre>
     70  *
     71  * <p>For each of the <code>fromScene</code> and <code>toScene</code> attributes,
     72  * there is a reference to a standard XML layout file. This is equivalent to
     73  * creating a scene from a layout in code by calling
     74  * {@link Scene#getSceneForLayout(ViewGroup, int, Context)}. For the
     75  * <code>transition</code> attribute, there is a reference to a resource
     76  * file in the <code>res/transition</code> directory which describes that
     77  * transition.</p>
     78  */
     79 public class TransitionManager {
     80 
     81     private static final String LOG_TAG = "TransitionManager";
     82 
     83     private static Transition sDefaultTransition = new AutoTransition();
     84 
     85     private ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<>();
     86     private ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions = new ArrayMap<>();
     87     private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>
     88             sRunningTransitions = new ThreadLocal<>();
     89     private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<>();
     90 
     91     /**
     92      * Sets a specific transition to occur when the given scene is entered.
     93      *
     94      * @param scene      The scene which, when applied, will cause the given
     95      *                   transition to run.
     96      * @param transition The transition that will play when the given scene is
     97      *                   entered. A value of null will result in the default behavior of
     98      *                   using the default transition instead.
     99      */
    100     public void setTransition(@NonNull Scene scene, @Nullable Transition transition) {
    101         mSceneTransitions.put(scene, transition);
    102     }
    103 
    104     /**
    105      * Sets a specific transition to occur when the given pair of scenes is
    106      * exited/entered.
    107      *
    108      * @param fromScene  The scene being exited when the given transition will
    109      *                   be run
    110      * @param toScene    The scene being entered when the given transition will
    111      *                   be run
    112      * @param transition The transition that will play when the given scene is
    113      *                   entered. A value of null will result in the default behavior of
    114      *                   using the default transition instead.
    115      */
    116     public void setTransition(@NonNull Scene fromScene, @NonNull Scene toScene,
    117             @Nullable Transition transition) {
    118         ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene);
    119         if (sceneTransitionMap == null) {
    120             sceneTransitionMap = new ArrayMap<>();
    121             mScenePairTransitions.put(toScene, sceneTransitionMap);
    122         }
    123         sceneTransitionMap.put(fromScene, transition);
    124     }
    125 
    126     /**
    127      * Returns the Transition for the given scene being entered. The result
    128      * depends not only on the given scene, but also the scene which the
    129      * {@link Scene#getSceneRoot() sceneRoot} of the Scene is currently in.
    130      *
    131      * @param scene The scene being entered
    132      * @return The Transition to be used for the given scene change. If no
    133      * Transition was specified for this scene change, the default transition
    134      * will be used instead.
    135      */
    136     private Transition getTransition(Scene scene) {
    137         Transition transition;
    138         ViewGroup sceneRoot = scene.getSceneRoot();
    139         if (sceneRoot != null) {
    140             // TODO: cached in Scene instead? long-term, cache in View itself
    141             Scene currScene = Scene.getCurrentScene(sceneRoot);
    142             if (currScene != null) {
    143                 ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions
    144                         .get(scene);
    145                 if (sceneTransitionMap != null) {
    146                     transition = sceneTransitionMap.get(currScene);
    147                     if (transition != null) {
    148                         return transition;
    149                     }
    150                 }
    151             }
    152         }
    153         transition = mSceneTransitions.get(scene);
    154         return (transition != null) ? transition : sDefaultTransition;
    155     }
    156 
    157     /**
    158      * This is where all of the work of a transition/scene-change is
    159      * orchestrated. This method captures the start values for the given
    160      * transition, exits the current Scene, enters the new scene, captures
    161      * the end values for the transition, and finally plays the
    162      * resulting values-populated transition.
    163      *
    164      * @param scene      The scene being entered
    165      * @param transition The transition to play for this scene change
    166      */
    167     private static void changeScene(Scene scene, Transition transition) {
    168         final ViewGroup sceneRoot = scene.getSceneRoot();
    169 
    170         if (!sPendingTransitions.contains(sceneRoot)) {
    171             if (transition == null) {
    172                 scene.enter();
    173             } else {
    174                 sPendingTransitions.add(sceneRoot);
    175 
    176                 Transition transitionClone = transition.clone();
    177                 transitionClone.setSceneRoot(sceneRoot);
    178 
    179                 Scene oldScene = Scene.getCurrentScene(sceneRoot);
    180                 if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
    181                     transitionClone.setCanRemoveViews(true);
    182                 }
    183 
    184                 sceneChangeSetup(sceneRoot, transitionClone);
    185 
    186                 scene.enter();
    187 
    188                 sceneChangeRunTransition(sceneRoot, transitionClone);
    189             }
    190         }
    191     }
    192 
    193     static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
    194         WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
    195                 sRunningTransitions.get();
    196         if (runningTransitions != null) {
    197             ArrayMap<ViewGroup, ArrayList<Transition>> transitions = runningTransitions.get();
    198             if (transitions != null) {
    199                 return transitions;
    200             }
    201         }
    202         ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
    203         runningTransitions = new WeakReference<>(transitions);
    204         sRunningTransitions.set(runningTransitions);
    205         return transitions;
    206     }
    207 
    208     private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
    209             final Transition transition) {
    210         if (transition != null && sceneRoot != null) {
    211             MultiListener listener = new MultiListener(transition, sceneRoot);
    212             sceneRoot.addOnAttachStateChangeListener(listener);
    213             sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
    214         }
    215     }
    216 
    217     /**
    218      * This private utility class is used to listen for both OnPreDraw and
    219      * OnAttachStateChange events. OnPreDraw events are the main ones we care
    220      * about since that's what triggers the transition to take place.
    221      * OnAttachStateChange events are also important in case the view is removed
    222      * from the hierarchy before the OnPreDraw event takes place; it's used to
    223      * clean up things since the OnPreDraw listener didn't get called in time.
    224      */
    225     private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
    226             View.OnAttachStateChangeListener {
    227 
    228         Transition mTransition;
    229 
    230         ViewGroup mSceneRoot;
    231 
    232         MultiListener(Transition transition, ViewGroup sceneRoot) {
    233             mTransition = transition;
    234             mSceneRoot = sceneRoot;
    235         }
    236 
    237         private void removeListeners() {
    238             mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
    239             mSceneRoot.removeOnAttachStateChangeListener(this);
    240         }
    241 
    242         @Override
    243         public void onViewAttachedToWindow(View v) {
    244         }
    245 
    246         @Override
    247         public void onViewDetachedFromWindow(View v) {
    248             removeListeners();
    249 
    250             sPendingTransitions.remove(mSceneRoot);
    251             ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
    252             if (runningTransitions != null && runningTransitions.size() > 0) {
    253                 for (Transition runningTransition : runningTransitions) {
    254                     runningTransition.resume(mSceneRoot);
    255                 }
    256             }
    257             mTransition.clearValues(true);
    258         }
    259 
    260         @Override
    261         public boolean onPreDraw() {
    262             removeListeners();
    263 
    264             // Don't start the transition if it's no longer pending.
    265             if (!sPendingTransitions.remove(mSceneRoot)) {
    266                 return true;
    267             }
    268 
    269             // Add to running list, handle end to remove it
    270             final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
    271                     getRunningTransitions();
    272             ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
    273             ArrayList<Transition> previousRunningTransitions = null;
    274             if (currentTransitions == null) {
    275                 currentTransitions = new ArrayList<>();
    276                 runningTransitions.put(mSceneRoot, currentTransitions);
    277             } else if (currentTransitions.size() > 0) {
    278                 previousRunningTransitions = new ArrayList<>(currentTransitions);
    279             }
    280             currentTransitions.add(mTransition);
    281             mTransition.addListener(new TransitionListenerAdapter() {
    282                 @Override
    283                 public void onTransitionEnd(@NonNull Transition transition) {
    284                     ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
    285                     currentTransitions.remove(transition);
    286                 }
    287             });
    288             mTransition.captureValues(mSceneRoot, false);
    289             if (previousRunningTransitions != null) {
    290                 for (Transition runningTransition : previousRunningTransitions) {
    291                     runningTransition.resume(mSceneRoot);
    292                 }
    293             }
    294             mTransition.playTransition(mSceneRoot);
    295 
    296             return true;
    297         }
    298     }
    299 
    300     private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
    301         // Capture current values
    302         ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
    303 
    304         if (runningTransitions != null && runningTransitions.size() > 0) {
    305             for (Transition runningTransition : runningTransitions) {
    306                 runningTransition.pause(sceneRoot);
    307             }
    308         }
    309 
    310         if (transition != null) {
    311             transition.captureValues(sceneRoot, true);
    312         }
    313 
    314         // Notify previous scene that it is being exited
    315         Scene previousScene = Scene.getCurrentScene(sceneRoot);
    316         if (previousScene != null) {
    317             previousScene.exit();
    318         }
    319     }
    320 
    321     /**
    322      * Change to the given scene, using the
    323      * appropriate transition for this particular scene change
    324      * (as specified to the TransitionManager, or the default
    325      * if no such transition exists).
    326      *
    327      * @param scene The Scene to change to
    328      */
    329     public void transitionTo(@NonNull Scene scene) {
    330         // Auto transition if there is no transition declared for the Scene, but there is
    331         // a root or parent view
    332         changeScene(scene, getTransition(scene));
    333     }
    334 
    335     /**
    336      * Convenience method to simply change to the given scene using
    337      * the default transition for TransitionManager.
    338      *
    339      * @param scene The Scene to change to
    340      */
    341     public static void go(@NonNull Scene scene) {
    342         changeScene(scene, sDefaultTransition);
    343     }
    344 
    345     /**
    346      * Convenience method to simply change to the given scene using
    347      * the given transition.
    348      *
    349      * <p>Passing in <code>null</code> for the transition parameter will
    350      * result in the scene changing without any transition running, and is
    351      * equivalent to calling {@link Scene#exit()} on the scene root's
    352      * current scene, followed by {@link Scene#enter()} on the scene
    353      * specified by the <code>scene</code> parameter.</p>
    354      *
    355      * @param scene      The Scene to change to
    356      * @param transition The transition to use for this scene change. A
    357      *                   value of null causes the scene change to happen with no transition.
    358      */
    359     public static void go(@NonNull Scene scene, @Nullable Transition transition) {
    360         changeScene(scene, transition);
    361     }
    362 
    363     /**
    364      * Convenience method to animate, using the default transition,
    365      * to a new scene defined by all changes within the given scene root between
    366      * calling this method and the next rendering frame.
    367      * Equivalent to calling {@link #beginDelayedTransition(ViewGroup, Transition)}
    368      * with a value of <code>null</code> for the <code>transition</code> parameter.
    369      *
    370      * @param sceneRoot The root of the View hierarchy to run the transition on.
    371      */
    372     public static void beginDelayedTransition(@NonNull final ViewGroup sceneRoot) {
    373         beginDelayedTransition(sceneRoot, null);
    374     }
    375 
    376     /**
    377      * Convenience method to animate to a new scene defined by all changes within
    378      * the given scene root between calling this method and the next rendering frame.
    379      * Calling this method causes TransitionManager to capture current values in the
    380      * scene root and then post a request to run a transition on the next frame.
    381      * At that time, the new values in the scene root will be captured and changes
    382      * will be animated. There is no need to create a Scene; it is implied by
    383      * changes which take place between calling this method and the next frame when
    384      * the transition begins.
    385      *
    386      * <p>Calling this method several times before the next frame (for example, if
    387      * unrelated code also wants to make dynamic changes and run a transition on
    388      * the same scene root), only the first call will trigger capturing values
    389      * and exiting the current scene. Subsequent calls to the method with the
    390      * same scene root during the same frame will be ignored.</p>
    391      *
    392      * <p>Passing in <code>null</code> for the transition parameter will
    393      * cause the TransitionManager to use its default transition.</p>
    394      *
    395      * @param sceneRoot  The root of the View hierarchy to run the transition on.
    396      * @param transition The transition to use for this change. A
    397      *                   value of null causes the TransitionManager to use the default transition.
    398      */
    399     public static void beginDelayedTransition(@NonNull final ViewGroup sceneRoot,
    400             @Nullable Transition transition) {
    401         if (!sPendingTransitions.contains(sceneRoot) && ViewCompat.isLaidOut(sceneRoot)) {
    402             if (Transition.DBG) {
    403                 Log.d(LOG_TAG, "beginDelayedTransition: root, transition = "
    404                         + sceneRoot + ", " + transition);
    405             }
    406             sPendingTransitions.add(sceneRoot);
    407             if (transition == null) {
    408                 transition = sDefaultTransition;
    409             }
    410             final Transition transitionClone = transition.clone();
    411             sceneChangeSetup(sceneRoot, transitionClone);
    412             Scene.setCurrentScene(sceneRoot, null);
    413             sceneChangeRunTransition(sceneRoot, transitionClone);
    414         }
    415     }
    416 
    417     /**
    418      * Ends all pending and ongoing transitions on the specified scene root.
    419      *
    420      * @param sceneRoot The root of the View hierarchy to end transitions on.
    421      */
    422     public static void endTransitions(final ViewGroup sceneRoot) {
    423         sPendingTransitions.remove(sceneRoot);
    424         final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
    425         if (runningTransitions != null && !runningTransitions.isEmpty()) {
    426             // Make a copy in case this is called by an onTransitionEnd listener
    427             ArrayList<Transition> copy = new ArrayList<>(runningTransitions);
    428             for (int i = copy.size() - 1; i >= 0; i--) {
    429                 final Transition transition = copy.get(i);
    430                 transition.forceToEnd(sceneRoot);
    431             }
    432         }
    433     }
    434 
    435 }
    436