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