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