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