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