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