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