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    ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<Scene, Transition>();
71    ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions =
72            new ArrayMap<Scene, ArrayMap<Scene, Transition>>();
73    private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>
74            sRunningTransitions =
75            new ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>();
76    private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<ViewGroup>();
77
78
79    /**
80     * Sets the transition to be used for any scene change for which no
81     * other transition is explicitly set. The initial value is
82     * an {@link AutoTransition} instance.
83     *
84     * @param transition The default transition to be used for scene changes.
85     *
86     * @hide pending later changes
87     */
88    public void setDefaultTransition(Transition transition) {
89        sDefaultTransition = transition;
90    }
91
92    /**
93     * Gets the current default transition. The initial value is an {@link
94     * AutoTransition} instance.
95     *
96     * @return The current default transition.
97     * @see #setDefaultTransition(Transition)
98     *
99     * @hide pending later changes
100     */
101    public static Transition getDefaultTransition() {
102        return sDefaultTransition;
103    }
104
105    /**
106     * Sets a specific transition to occur when the given scene is entered.
107     *
108     * @param scene The scene which, when applied, will cause the given
109     * transition to run.
110     * @param transition The transition that will play when the given scene is
111     * entered. A value of null will result in the default behavior of
112     * using the default transition instead.
113     */
114    public void setTransition(Scene scene, Transition transition) {
115        mSceneTransitions.put(scene, transition);
116    }
117
118    /**
119     * Sets a specific transition to occur when the given pair of scenes is
120     * exited/entered.
121     *
122     * @param fromScene The scene being exited when the given transition will
123     * be run
124     * @param toScene The scene being entered when the given transition will
125     * be run
126     * @param transition The transition that will play when the given scene is
127     * entered. A value of null will result in the default behavior of
128     * using the default transition instead.
129     */
130    public void setTransition(Scene fromScene, Scene toScene, Transition transition) {
131        ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene);
132        if (sceneTransitionMap == null) {
133            sceneTransitionMap = new ArrayMap<Scene, Transition>();
134            mScenePairTransitions.put(toScene, sceneTransitionMap);
135        }
136        sceneTransitionMap.put(fromScene, transition);
137    }
138
139    /**
140     * Returns the Transition for the given scene being entered. The result
141     * depends not only on the given scene, but also the scene which the
142     * {@link Scene#getSceneRoot() sceneRoot} of the Scene is currently in.
143     *
144     * @param scene The scene being entered
145     * @return The Transition to be used for the given scene change. If no
146     * Transition was specified for this scene change, the default transition
147     * will be used instead.
148     */
149    private Transition getTransition(Scene scene) {
150        Transition transition = null;
151        ViewGroup sceneRoot = scene.getSceneRoot();
152        if (sceneRoot != null) {
153            // TODO: cached in Scene instead? long-term, cache in View itself
154            Scene currScene = Scene.getCurrentScene(sceneRoot);
155            if (currScene != null) {
156                ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(scene);
157                if (sceneTransitionMap != null) {
158                    transition = sceneTransitionMap.get(currScene);
159                    if (transition != null) {
160                        return transition;
161                    }
162                }
163            }
164        }
165        transition = mSceneTransitions.get(scene);
166        return (transition != null) ? transition : sDefaultTransition;
167    }
168
169    /**
170     * This is where all of the work of a transition/scene-change is
171     * orchestrated. This method captures the start values for the given
172     * transition, exits the current Scene, enters the new scene, captures
173     * the end values for the transition, and finally plays the
174     * resulting values-populated transition.
175     *
176     * @param scene The scene being entered
177     * @param transition The transition to play for this scene change
178     */
179    private static void changeScene(Scene scene, Transition transition) {
180
181        final ViewGroup sceneRoot = scene.getSceneRoot();
182
183        Transition transitionClone = transition.clone();
184        transitionClone.setSceneRoot(sceneRoot);
185
186        Scene oldScene = Scene.getCurrentScene(sceneRoot);
187        if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
188            transitionClone.setCanRemoveViews(true);
189        }
190
191        sceneChangeSetup(sceneRoot, transitionClone);
192
193        scene.enter();
194
195        sceneChangeRunTransition(sceneRoot, transitionClone);
196    }
197
198    private static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
199        WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
200                sRunningTransitions.get();
201        if (runningTransitions == null || runningTransitions.get() == null) {
202            ArrayMap<ViewGroup, ArrayList<Transition>> transitions =
203                    new ArrayMap<ViewGroup, ArrayList<Transition>>();
204            runningTransitions = new WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>(
205                    transitions);
206            sRunningTransitions.set(runningTransitions);
207        }
208        return runningTransitions.get();
209    }
210
211    private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
212            final Transition transition) {
213        if (transition != null && sceneRoot != null) {
214            MultiListener listener = new MultiListener(transition, sceneRoot);
215            sceneRoot.addOnAttachStateChangeListener(listener);
216            sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
217        }
218    }
219
220    /**
221     * This private utility class is used to listen for both OnPreDraw and
222     * OnAttachStateChange events. OnPreDraw events are the main ones we care
223     * about since that's what triggers the transition to take place.
224     * OnAttachStateChange events are also important in case the view is removed
225     * from the hierarchy before the OnPreDraw event takes place; it's used to
226     * clean up things since the OnPreDraw listener didn't get called in time.
227     */
228    private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
229            View.OnAttachStateChangeListener {
230
231        Transition mTransition;
232        ViewGroup mSceneRoot;
233
234        MultiListener(Transition transition, ViewGroup sceneRoot) {
235            mTransition = transition;
236            mSceneRoot = sceneRoot;
237        }
238
239        private void removeListeners() {
240            mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
241            mSceneRoot.removeOnAttachStateChangeListener(this);
242        }
243
244        @Override
245        public void onViewAttachedToWindow(View v) {
246        }
247
248        @Override
249        public void onViewDetachedFromWindow(View v) {
250            removeListeners();
251
252            sPendingTransitions.remove(mSceneRoot);
253            ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
254            if (runningTransitions != null && runningTransitions.size() > 0) {
255                for (Transition runningTransition : runningTransitions) {
256                    runningTransition.resume();
257                }
258            }
259            mTransition.clearValues(true);
260        }
261
262        @Override
263        public boolean onPreDraw() {
264            removeListeners();
265            sPendingTransitions.remove(mSceneRoot);
266            // Add to running list, handle end to remove it
267            final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
268                    getRunningTransitions();
269            ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
270            ArrayList<Transition> previousRunningTransitions = null;
271            if (currentTransitions == null) {
272                currentTransitions = new ArrayList<Transition>();
273                runningTransitions.put(mSceneRoot, currentTransitions);
274            } else if (currentTransitions.size() > 0) {
275                previousRunningTransitions = new ArrayList<Transition>(currentTransitions);
276            }
277            currentTransitions.add(mTransition);
278            mTransition.addListener(new Transition.TransitionListenerAdapter() {
279                @Override
280                public void onTransitionEnd(Transition transition) {
281                    ArrayList<Transition> currentTransitions =
282                            runningTransitions.get(mSceneRoot);
283                    currentTransitions.remove(transition);
284                }
285            });
286            mTransition.captureValues(mSceneRoot, false);
287            if (previousRunningTransitions != null) {
288                for (Transition runningTransition : previousRunningTransitions) {
289                    runningTransition.resume();
290                }
291            }
292            mTransition.playTransition(mSceneRoot);
293
294            return true;
295        }
296    };
297
298    private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
299
300        // Capture current values
301        ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
302
303        if (runningTransitions != null && runningTransitions.size() > 0) {
304            for (Transition runningTransition : runningTransitions) {
305                runningTransition.pause();
306            }
307        }
308
309        if (transition != null) {
310            transition.captureValues(sceneRoot, true);
311        }
312
313        // Notify previous scene that it is being exited
314        Scene previousScene = Scene.getCurrentScene(sceneRoot);
315        if (previousScene != null) {
316            previousScene.exit();
317        }
318    }
319
320    /**
321     * Change to the given scene, using the
322     * appropriate transition for this particular scene change
323     * (as specified to the TransitionManager, or the default
324     * if no such transition exists).
325     *
326     * @param scene The Scene to change to
327     */
328    public void transitionTo(Scene scene) {
329        // Auto transition if there is no transition declared for the Scene, but there is
330        // a root or parent view
331        changeScene(scene, getTransition(scene));
332
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     */
341    public static void go(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     */
359    public static void go(Scene scene, 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     */
372    public static void beginDelayedTransition(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     */
399    public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) {
400        if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
401            if (Transition.DBG) {
402                Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " +
403                        sceneRoot + ", " + transition);
404            }
405            sPendingTransitions.add(sceneRoot);
406            if (transition == null) {
407                transition = sDefaultTransition;
408            }
409            final Transition transitionClone = transition.clone();
410            sceneChangeSetup(sceneRoot, transitionClone);
411            Scene.setCurrentScene(sceneRoot, null);
412            sceneChangeRunTransition(sceneRoot, transitionClone);
413        }
414    }
415}
416