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.compat.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      */
setDefaultTransition(Transition transition)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      */
getDefaultTransition()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      */
setTransition(Scene scene, Transition transition)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      */
setTransition(Scene fromScene, Scene toScene, Transition transition)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
getTransition(Scene scene)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      */
changeScene(Scene scene, Transition transition)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
getRunningTransitions()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 
sceneChangeRunTransition(final ViewGroup sceneRoot, final Transition transition)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 
MultiListener(Transition transition, ViewGroup sceneRoot)258         MultiListener(Transition transition, ViewGroup sceneRoot) {
259             mTransition = transition;
260             mSceneRoot = sceneRoot;
261             mViewTreeObserver = mSceneRoot.getViewTreeObserver();
262         }
263 
removeListeners()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
onViewAttachedToWindow(View v)274         public void onViewAttachedToWindow(View v) {
275         }
276 
277         @Override
onViewDetachedFromWindow(View v)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
onPreDraw()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 
sceneChangeSetup(ViewGroup sceneRoot, Transition transition)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      */
transitionTo(Scene scene)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      */
go(Scene scene)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      */
go(Scene scene, Transition transition)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      */
beginDelayedTransition(final ViewGroup sceneRoot)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      */
beginDelayedTransition(final ViewGroup sceneRoot, Transition transition)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      */
endTransitions(final ViewGroup sceneRoot)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