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