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 androidx.transition;
18
19import android.content.Context;
20import android.util.Log;
21import android.view.View;
22import android.view.ViewGroup;
23import android.view.ViewTreeObserver;
24
25import androidx.annotation.NonNull;
26import androidx.annotation.Nullable;
27import androidx.collection.ArrayMap;
28import androidx.core.view.ViewCompat;
29
30import java.lang.ref.WeakReference;
31import java.util.ArrayList;
32
33/**
34 * This class manages the set of transitions that fire when there is a
35 * change of {@link Scene}. To use the manager, add scenes along with
36 * transition objects with calls to {@link #setTransition(Scene, Transition)}
37 * or {@link #setTransition(Scene, Scene, Transition)}. Setting specific
38 * transitions for scene changes is not required; by default, a Scene change
39 * will use {@link AutoTransition} to do something reasonable for most
40 * situations. Specifying other transitions for particular scene changes is
41 * only necessary if the application wants different transition behavior
42 * in these situations.
43 *
44 * <p>TransitionManagers can be declared in XML resource files inside the
45 * <code>res/transition</code> directory. TransitionManager resources consist of
46 * the <code>transitionManager</code>tag name, containing one or more
47 * <code>transition</code> tags, each of which describe the relationship of
48 * that transition to the from/to scene information in that tag.
49 * For example, here is a resource file that declares several scene
50 * transitions:</p>
51 *
52 * <pre>
53 *     &lt;transitionManager xmlns:android="http://schemas.android.com/apk/res/android"&gt;
54 *         &lt;transition android:fromScene="@layout/transition_scene1"
55 *                     android:toScene="@layout/transition_scene2"
56 *                     android:transition="@transition/changebounds"/&gt;
57 *         &lt;transition android:fromScene="@layout/transition_scene2"
58 *                     android:toScene="@layout/transition_scene1"
59 *                     android:transition="@transition/changebounds"/&gt;
60 *         &lt;transition android:toScene="@layout/transition_scene3"
61 *                     android:transition="@transition/changebounds_fadein_together"/&gt;
62 *         &lt;transition android:fromScene="@layout/transition_scene3"
63 *                     android:toScene="@layout/transition_scene1"
64 *                     android:transition="@transition/changebounds_fadeout_sequential"/&gt;
65 *         &lt;transition android:fromScene="@layout/transition_scene3"
66 *                     android:toScene="@layout/transition_scene2"
67 *                     android:transition="@transition/changebounds_fadeout_sequential"/&gt;
68 *     &lt;/transitionManager&gt;
69 * </pre>
70 *
71 * <p>For each of the <code>fromScene</code> and <code>toScene</code> attributes,
72 * there is a reference to a standard XML layout file. This is equivalent to
73 * creating a scene from a layout in code by calling
74 * {@link Scene#getSceneForLayout(ViewGroup, int, Context)}. For the
75 * <code>transition</code> attribute, there is a reference to a resource
76 * file in the <code>res/transition</code> directory which describes that
77 * transition.</p>
78 */
79public class TransitionManager {
80
81    private static final String LOG_TAG = "TransitionManager";
82
83    private static Transition sDefaultTransition = new AutoTransition();
84
85    private ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<>();
86    private ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions = new ArrayMap<>();
87    private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>
88            sRunningTransitions = new ThreadLocal<>();
89    private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<>();
90
91    /**
92     * Sets a specific transition to occur when the given scene is entered.
93     *
94     * @param scene      The scene which, when applied, will cause the given
95     *                   transition to run.
96     * @param transition The transition that will play when the given scene is
97     *                   entered. A value of null will result in the default behavior of
98     *                   using the default transition instead.
99     */
100    public void setTransition(@NonNull Scene scene, @Nullable Transition transition) {
101        mSceneTransitions.put(scene, transition);
102    }
103
104    /**
105     * Sets a specific transition to occur when the given pair of scenes is
106     * exited/entered.
107     *
108     * @param fromScene  The scene being exited when the given transition will
109     *                   be run
110     * @param toScene    The scene being entered when the given transition will
111     *                   be 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(@NonNull Scene fromScene, @NonNull Scene toScene,
117            @Nullable Transition transition) {
118        ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene);
119        if (sceneTransitionMap == null) {
120            sceneTransitionMap = new ArrayMap<>();
121            mScenePairTransitions.put(toScene, sceneTransitionMap);
122        }
123        sceneTransitionMap.put(fromScene, transition);
124    }
125
126    /**
127     * Returns the Transition for the given scene being entered. The result
128     * depends not only on the given scene, but also the scene which the
129     * {@link Scene#getSceneRoot() sceneRoot} of the Scene is currently in.
130     *
131     * @param scene The scene being entered
132     * @return The Transition to be used for the given scene change. If no
133     * Transition was specified for this scene change, the default transition
134     * will be used instead.
135     */
136    private Transition getTransition(Scene scene) {
137        Transition transition;
138        ViewGroup sceneRoot = scene.getSceneRoot();
139        if (sceneRoot != null) {
140            // TODO: cached in Scene instead? long-term, cache in View itself
141            Scene currScene = Scene.getCurrentScene(sceneRoot);
142            if (currScene != null) {
143                ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions
144                        .get(scene);
145                if (sceneTransitionMap != null) {
146                    transition = sceneTransitionMap.get(currScene);
147                    if (transition != null) {
148                        return transition;
149                    }
150                }
151            }
152        }
153        transition = mSceneTransitions.get(scene);
154        return (transition != null) ? transition : sDefaultTransition;
155    }
156
157    /**
158     * This is where all of the work of a transition/scene-change is
159     * orchestrated. This method captures the start values for the given
160     * transition, exits the current Scene, enters the new scene, captures
161     * the end values for the transition, and finally plays the
162     * resulting values-populated transition.
163     *
164     * @param scene      The scene being entered
165     * @param transition The transition to play for this scene change
166     */
167    private static void changeScene(Scene scene, Transition transition) {
168        final ViewGroup sceneRoot = scene.getSceneRoot();
169
170        if (!sPendingTransitions.contains(sceneRoot)) {
171            if (transition == null) {
172                scene.enter();
173            } else {
174                sPendingTransitions.add(sceneRoot);
175
176                Transition transitionClone = transition.clone();
177                transitionClone.setSceneRoot(sceneRoot);
178
179                Scene oldScene = Scene.getCurrentScene(sceneRoot);
180                if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
181                    transitionClone.setCanRemoveViews(true);
182                }
183
184                sceneChangeSetup(sceneRoot, transitionClone);
185
186                scene.enter();
187
188                sceneChangeRunTransition(sceneRoot, transitionClone);
189            }
190        }
191    }
192
193    static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
194        WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
195                sRunningTransitions.get();
196        if (runningTransitions != null) {
197            ArrayMap<ViewGroup, ArrayList<Transition>> transitions = runningTransitions.get();
198            if (transitions != null) {
199                return transitions;
200            }
201        }
202        ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
203        runningTransitions = new WeakReference<>(transitions);
204        sRunningTransitions.set(runningTransitions);
205        return transitions;
206    }
207
208    private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
209            final Transition transition) {
210        if (transition != null && sceneRoot != null) {
211            MultiListener listener = new MultiListener(transition, sceneRoot);
212            sceneRoot.addOnAttachStateChangeListener(listener);
213            sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
214        }
215    }
216
217    /**
218     * This private utility class is used to listen for both OnPreDraw and
219     * OnAttachStateChange events. OnPreDraw events are the main ones we care
220     * about since that's what triggers the transition to take place.
221     * OnAttachStateChange events are also important in case the view is removed
222     * from the hierarchy before the OnPreDraw event takes place; it's used to
223     * clean up things since the OnPreDraw listener didn't get called in time.
224     */
225    private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
226            View.OnAttachStateChangeListener {
227
228        Transition mTransition;
229
230        ViewGroup mSceneRoot;
231
232        MultiListener(Transition transition, ViewGroup sceneRoot) {
233            mTransition = transition;
234            mSceneRoot = sceneRoot;
235        }
236
237        private void removeListeners() {
238            mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
239            mSceneRoot.removeOnAttachStateChangeListener(this);
240        }
241
242        @Override
243        public void onViewAttachedToWindow(View v) {
244        }
245
246        @Override
247        public void onViewDetachedFromWindow(View v) {
248            removeListeners();
249
250            sPendingTransitions.remove(mSceneRoot);
251            ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
252            if (runningTransitions != null && runningTransitions.size() > 0) {
253                for (Transition runningTransition : runningTransitions) {
254                    runningTransition.resume(mSceneRoot);
255                }
256            }
257            mTransition.clearValues(true);
258        }
259
260        @Override
261        public boolean onPreDraw() {
262            removeListeners();
263
264            // Don't start the transition if it's no longer pending.
265            if (!sPendingTransitions.remove(mSceneRoot)) {
266                return true;
267            }
268
269            // Add to running list, handle end to remove it
270            final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
271                    getRunningTransitions();
272            ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
273            ArrayList<Transition> previousRunningTransitions = null;
274            if (currentTransitions == null) {
275                currentTransitions = new ArrayList<>();
276                runningTransitions.put(mSceneRoot, currentTransitions);
277            } else if (currentTransitions.size() > 0) {
278                previousRunningTransitions = new ArrayList<>(currentTransitions);
279            }
280            currentTransitions.add(mTransition);
281            mTransition.addListener(new TransitionListenerAdapter() {
282                @Override
283                public void onTransitionEnd(@NonNull Transition transition) {
284                    ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
285                    currentTransitions.remove(transition);
286                }
287            });
288            mTransition.captureValues(mSceneRoot, false);
289            if (previousRunningTransitions != null) {
290                for (Transition runningTransition : previousRunningTransitions) {
291                    runningTransition.resume(mSceneRoot);
292                }
293            }
294            mTransition.playTransition(mSceneRoot);
295
296            return true;
297        }
298    }
299
300    private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
301        // Capture current values
302        ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
303
304        if (runningTransitions != null && runningTransitions.size() > 0) {
305            for (Transition runningTransition : runningTransitions) {
306                runningTransition.pause(sceneRoot);
307            }
308        }
309
310        if (transition != null) {
311            transition.captureValues(sceneRoot, true);
312        }
313
314        // Notify previous scene that it is being exited
315        Scene previousScene = Scene.getCurrentScene(sceneRoot);
316        if (previousScene != null) {
317            previousScene.exit();
318        }
319    }
320
321    /**
322     * Change to the given scene, using the
323     * appropriate transition for this particular scene change
324     * (as specified to the TransitionManager, or the default
325     * if no such transition exists).
326     *
327     * @param scene The Scene to change to
328     */
329    public void transitionTo(@NonNull Scene scene) {
330        // Auto transition if there is no transition declared for the Scene, but there is
331        // a root or parent view
332        changeScene(scene, getTransition(scene));
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(@NonNull 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(@NonNull Scene scene, @Nullable 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(@NonNull 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(@NonNull final ViewGroup sceneRoot,
400            @Nullable Transition transition) {
401        if (!sPendingTransitions.contains(sceneRoot) && ViewCompat.isLaidOut(sceneRoot)) {
402            if (Transition.DBG) {
403                Log.d(LOG_TAG, "beginDelayedTransition: root, transition = "
404                        + sceneRoot + ", " + transition);
405            }
406            sPendingTransitions.add(sceneRoot);
407            if (transition == null) {
408                transition = sDefaultTransition;
409            }
410            final Transition transitionClone = transition.clone();
411            sceneChangeSetup(sceneRoot, transitionClone);
412            Scene.setCurrentScene(sceneRoot, null);
413            sceneChangeRunTransition(sceneRoot, transitionClone);
414        }
415    }
416
417    /**
418     * Ends all pending and ongoing transitions on the specified scene root.
419     *
420     * @param sceneRoot The root of the View hierarchy to end transitions on.
421     */
422    public static void endTransitions(final ViewGroup sceneRoot) {
423        sPendingTransitions.remove(sceneRoot);
424        final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
425        if (runningTransitions != null && !runningTransitions.isEmpty()) {
426            // Make a copy in case this is called by an onTransitionEnd listener
427            ArrayList<Transition> copy = new ArrayList<>(runningTransitions);
428            for (int i = copy.size() - 1; i >= 0; i--) {
429                final Transition transition = copy.get(i);
430                transition.forceToEnd(sceneRoot);
431            }
432        }
433    }
434
435}
436