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      */
setDefaultTransition(Transition transition)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      */
getDefaultTransition()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      */
setTransition(Scene scene, Transition transition)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      */
setTransition(Scene fromScene, Scene toScene, Transition transition)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      */
getTransition(Scene scene)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      */
changeScene(Scene scene, Transition transition)181     private static void changeScene(Scene scene, Transition transition) {
182 
183         final ViewGroup sceneRoot = scene.getSceneRoot();
184         if (!sPendingTransitions.contains(sceneRoot)) {
185             sPendingTransitions.add(sceneRoot);
186 
187             Transition transitionClone = null;
188             if (transition != null) {
189                 transitionClone = transition.clone();
190                 transitionClone.setSceneRoot(sceneRoot);
191             }
192 
193             Scene oldScene = Scene.getCurrentScene(sceneRoot);
194             if (oldScene != null && transitionClone != null &&
195                     oldScene.isCreatedFromLayoutResource()) {
196                 transitionClone.setCanRemoveViews(true);
197             }
198 
199             sceneChangeSetup(sceneRoot, transitionClone);
200 
201             scene.enter();
202 
203             sceneChangeRunTransition(sceneRoot, transitionClone);
204         }
205     }
206 
getRunningTransitions()207     private static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
208         WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
209                 sRunningTransitions.get();
210         if (runningTransitions == null || runningTransitions.get() == null) {
211             ArrayMap<ViewGroup, ArrayList<Transition>> transitions =
212                     new ArrayMap<ViewGroup, ArrayList<Transition>>();
213             runningTransitions = new WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>(
214                     transitions);
215             sRunningTransitions.set(runningTransitions);
216         }
217         return runningTransitions.get();
218     }
219 
sceneChangeRunTransition(final ViewGroup sceneRoot, final Transition transition)220     private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
221             final Transition transition) {
222         if (transition != null && sceneRoot != null) {
223             MultiListener listener = new MultiListener(transition, sceneRoot);
224             sceneRoot.addOnAttachStateChangeListener(listener);
225             sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
226         }
227     }
228 
229     /**
230      * This private utility class is used to listen for both OnPreDraw and
231      * OnAttachStateChange events. OnPreDraw events are the main ones we care
232      * about since that's what triggers the transition to take place.
233      * OnAttachStateChange events are also important in case the view is removed
234      * from the hierarchy before the OnPreDraw event takes place; it's used to
235      * clean up things since the OnPreDraw listener didn't get called in time.
236      */
237     private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
238             View.OnAttachStateChangeListener {
239 
240         Transition mTransition;
241         ViewGroup mSceneRoot;
242 
MultiListener(Transition transition, ViewGroup sceneRoot)243         MultiListener(Transition transition, ViewGroup sceneRoot) {
244             mTransition = transition;
245             mSceneRoot = sceneRoot;
246         }
247 
removeListeners()248         private void removeListeners() {
249             mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
250             mSceneRoot.removeOnAttachStateChangeListener(this);
251         }
252 
253         @Override
onViewAttachedToWindow(View v)254         public void onViewAttachedToWindow(View v) {
255         }
256 
257         @Override
onViewDetachedFromWindow(View v)258         public void onViewDetachedFromWindow(View v) {
259             removeListeners();
260 
261             sPendingTransitions.remove(mSceneRoot);
262             ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
263             if (runningTransitions != null && runningTransitions.size() > 0) {
264                 for (Transition runningTransition : runningTransitions) {
265                     runningTransition.resume(mSceneRoot);
266                 }
267             }
268             mTransition.clearValues(true);
269         }
270 
271         @Override
onPreDraw()272         public boolean onPreDraw() {
273             removeListeners();
274 
275             // Don't start the transition if it's no longer pending.
276             if (!sPendingTransitions.remove(mSceneRoot)) {
277                 return true;
278             }
279 
280             // Add to running list, handle end to remove it
281             final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
282                     getRunningTransitions();
283             ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
284             ArrayList<Transition> previousRunningTransitions = null;
285             if (currentTransitions == null) {
286                 currentTransitions = new ArrayList<Transition>();
287                 runningTransitions.put(mSceneRoot, currentTransitions);
288             } else if (currentTransitions.size() > 0) {
289                 previousRunningTransitions = new ArrayList<Transition>(currentTransitions);
290             }
291             currentTransitions.add(mTransition);
292             mTransition.addListener(new Transition.TransitionListenerAdapter() {
293                 @Override
294                 public void onTransitionEnd(Transition transition) {
295                     ArrayList<Transition> currentTransitions =
296                             runningTransitions.get(mSceneRoot);
297                     currentTransitions.remove(transition);
298                 }
299             });
300             mTransition.captureValues(mSceneRoot, false);
301             if (previousRunningTransitions != null) {
302                 for (Transition runningTransition : previousRunningTransitions) {
303                     runningTransition.resume(mSceneRoot);
304                 }
305             }
306             mTransition.playTransition(mSceneRoot);
307 
308             return true;
309         }
310     };
311 
sceneChangeSetup(ViewGroup sceneRoot, Transition transition)312     private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
313 
314         // Capture current values
315         ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
316 
317         if (runningTransitions != null && runningTransitions.size() > 0) {
318             for (Transition runningTransition : runningTransitions) {
319                 runningTransition.pause(sceneRoot);
320             }
321         }
322 
323         if (transition != null) {
324             transition.captureValues(sceneRoot, true);
325         }
326 
327         // Notify previous scene that it is being exited
328         Scene previousScene = Scene.getCurrentScene(sceneRoot);
329         if (previousScene != null) {
330             previousScene.exit();
331         }
332     }
333 
334     /**
335      * Change to the given scene, using the
336      * appropriate transition for this particular scene change
337      * (as specified to the TransitionManager, or the default
338      * if no such transition exists).
339      *
340      * @param scene The Scene to change to
341      */
transitionTo(Scene scene)342     public void transitionTo(Scene scene) {
343         // Auto transition if there is no transition declared for the Scene, but there is
344         // a root or parent view
345         changeScene(scene, getTransition(scene));
346     }
347 
348     /**
349      * Convenience method to simply change to the given scene using
350      * the default transition for TransitionManager.
351      *
352      * @param scene The Scene to change to
353      */
go(Scene scene)354     public static void go(Scene scene) {
355         changeScene(scene, sDefaultTransition);
356     }
357 
358     /**
359      * Convenience method to simply change to the given scene using
360      * the given transition.
361      *
362      * <p>Passing in <code>null</code> for the transition parameter will
363      * result in the scene changing without any transition running, and is
364      * equivalent to calling {@link Scene#exit()} on the scene root's
365      * current scene, followed by {@link Scene#enter()} on the scene
366      * specified by the <code>scene</code> parameter.</p>
367      *
368      * @param scene The Scene to change to
369      * @param transition The transition to use for this scene change. A
370      * value of null causes the scene change to happen with no transition.
371      */
go(Scene scene, Transition transition)372     public static void go(Scene scene, Transition transition) {
373         changeScene(scene, transition);
374     }
375 
376     /**
377      * Convenience method to animate, using the default transition,
378      * to a new scene defined by all changes within the given scene root between
379      * calling this method and the next rendering frame.
380      * Equivalent to calling {@link #beginDelayedTransition(ViewGroup, Transition)}
381      * with a value of <code>null</code> for the <code>transition</code> parameter.
382      *
383      * @param sceneRoot The root of the View hierarchy to run the transition on.
384      */
beginDelayedTransition(final ViewGroup sceneRoot)385     public static void beginDelayedTransition(final ViewGroup sceneRoot) {
386         beginDelayedTransition(sceneRoot, null);
387     }
388 
389     /**
390      * Convenience method to animate to a new scene defined by all changes within
391      * the given scene root between calling this method and the next rendering frame.
392      * Calling this method causes TransitionManager to capture current values in the
393      * scene root and then post a request to run a transition on the next frame.
394      * At that time, the new values in the scene root will be captured and changes
395      * will be animated. There is no need to create a Scene; it is implied by
396      * changes which take place between calling this method and the next frame when
397      * the transition begins.
398      *
399      * <p>Calling this method several times before the next frame (for example, if
400      * unrelated code also wants to make dynamic changes and run a transition on
401      * the same scene root), only the first call will trigger capturing values
402      * and exiting the current scene. Subsequent calls to the method with the
403      * same scene root during the same frame will be ignored.</p>
404      *
405      * <p>Passing in <code>null</code> for the transition parameter will
406      * cause the TransitionManager to use its default transition.</p>
407      *
408      * @param sceneRoot The root of the View hierarchy to run the transition on.
409      * @param transition The transition to use for this change. A
410      * value of null causes the TransitionManager to use the default transition.
411      */
beginDelayedTransition(final ViewGroup sceneRoot, Transition transition)412     public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) {
413         if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
414             if (Transition.DBG) {
415                 Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " +
416                         sceneRoot + ", " + transition);
417             }
418             sPendingTransitions.add(sceneRoot);
419             if (transition == null) {
420                 transition = sDefaultTransition;
421             }
422             final Transition transitionClone = transition.clone();
423             sceneChangeSetup(sceneRoot, transitionClone);
424             Scene.setCurrentScene(sceneRoot, null);
425             sceneChangeRunTransition(sceneRoot, transitionClone);
426         }
427     }
428 
429     /**
430      * Ends all pending and ongoing transitions on the specified scene root.
431      *
432      * @param sceneRoot The root of the View hierarchy to end transitions on.
433      */
endTransitions(final ViewGroup sceneRoot)434     public static void endTransitions(final ViewGroup sceneRoot) {
435         sPendingTransitions.remove(sceneRoot);
436 
437         final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
438         if (runningTransitions != null && !runningTransitions.isEmpty()) {
439             // Make a copy in case this is called by an onTransitionEnd listener
440             ArrayList<Transition> copy = new ArrayList(runningTransitions);
441             for (int i = copy.size() - 1; i >= 0; i--) {
442                 final Transition transition = copy.get(i);
443                 transition.end();
444             }
445         }
446 
447     }
448 }
449