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