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