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      */
setDefaultTransition(Transition transition)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      */
getDefaultTransition()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      */
setTransition(Scene scene, Transition transition)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      */
setTransition(Scene fromScene, Scene toScene, Transition transition)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
getTransition(Scene scene)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      */
changeScene(Scene scene, Transition transition)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 
getRunningTransitions()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 
sceneChangeRunTransition(final ViewGroup sceneRoot, final Transition transition)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 
MultiListener(Transition transition, ViewGroup sceneRoot)247         MultiListener(Transition transition, ViewGroup sceneRoot) {
248             mTransition = transition;
249             mSceneRoot = sceneRoot;
250             mViewTreeObserver = mSceneRoot.getViewTreeObserver();
251         }
252 
removeListeners()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
onViewAttachedToWindow(View v)263         public void onViewAttachedToWindow(View v) {
264         }
265 
266         @Override
onViewDetachedFromWindow(View v)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
onPreDraw()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 
sceneChangeSetup(ViewGroup sceneRoot, Transition transition)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      */
transitionTo(Scene scene)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      */
go(Scene scene)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      */
go(Scene scene, Transition transition)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      */
beginDelayedTransition(final ViewGroup sceneRoot)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      */
beginDelayedTransition(final ViewGroup sceneRoot, Transition transition)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      */
endTransitions(final ViewGroup sceneRoot)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