TransitionManagerPort.java revision 761ea02d06e2181c6ba5f0b413bc5068bfb2733c
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.support.v4.util.ArrayMap;
20import android.support.v4.view.ViewCompat;
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
29class TransitionManagerPort {
30    // TODO: how to handle enter/exit?
31
32    private static final String[] EMPTY_STRINGS = new String[0];
33
34    private static String LOG_TAG = "TransitionManager";
35
36    private static TransitionPort sDefaultTransition = new AutoTransitionPort();
37
38    private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<TransitionPort>>>>
39            sRunningTransitions = new ThreadLocal<>();
40
41    static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<>();
42
43    ArrayMap<ScenePort, TransitionPort> mSceneTransitions = new ArrayMap<>();
44
45    ArrayMap<ScenePort, ArrayMap<ScenePort, TransitionPort>> mScenePairTransitions =
46            new ArrayMap<>();
47
48    ArrayMap<ScenePort, ArrayMap<String, TransitionPort>> mSceneNameTransitions = new ArrayMap<>();
49
50    ArrayMap<String, ArrayMap<ScenePort, TransitionPort>> mNameSceneTransitions = new ArrayMap<>();
51
52    /**
53     * Gets the current default transition. The initial value is an {@link
54     * AutoTransition} instance.
55     *
56     * @return The current default transition.
57     * @hide pending later changes
58     * @see #setDefaultTransition(TransitionPort)
59     */
60    public static TransitionPort getDefaultTransition() {
61        return sDefaultTransition;
62    }
63
64    /**
65     * Sets the transition to be used for any scene change for which no
66     * other transition is explicitly set. The initial value is
67     * an {@link AutoTransition} instance.
68     *
69     * @param transition The default transition to be used for scene changes.
70     * @hide pending later changes
71     */
72    public void setDefaultTransition(TransitionPort transition) {
73        sDefaultTransition = transition;
74    }
75
76    /**
77     * This is where all of the work of a transition/scene-change is
78     * orchestrated. This method captures the start values for the given
79     * transition, exits the current Scene, enters the new scene, captures
80     * the end values for the transition, and finally plays the
81     * resulting values-populated transition.
82     *
83     * @param scene      The scene being entered
84     * @param transition The transition to play for this scene change
85     */
86    private static void changeScene(ScenePort scene, TransitionPort transition) {
87
88        final ViewGroup sceneRoot = scene.getSceneRoot();
89
90        TransitionPort transitionClone = null;
91        if (transition != null) {
92            transitionClone = transition.clone();
93            transitionClone.setSceneRoot(sceneRoot);
94        }
95
96        ScenePort oldScene = ScenePort.getCurrentScene(sceneRoot);
97        if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
98            transitionClone.setCanRemoveViews(true);
99        }
100
101        sceneChangeSetup(sceneRoot, transitionClone);
102
103        scene.enter();
104
105        sceneChangeRunTransition(sceneRoot, transitionClone);
106    }
107
108    static ArrayMap<ViewGroup, ArrayList<TransitionPort>> getRunningTransitions() {
109        WeakReference<ArrayMap<ViewGroup, ArrayList<TransitionPort>>> runningTransitions =
110                sRunningTransitions.get();
111        if (runningTransitions == null || runningTransitions.get() == null) {
112            ArrayMap<ViewGroup, ArrayList<TransitionPort>> transitions = new ArrayMap<>();
113            runningTransitions = new WeakReference<>(transitions);
114            sRunningTransitions.set(runningTransitions);
115        }
116        return runningTransitions.get();
117    }
118
119    private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
120            final TransitionPort transition) {
121        if (transition != null && sceneRoot != null) {
122            MultiListener listener = new MultiListener(transition, sceneRoot);
123            sceneRoot.addOnAttachStateChangeListener(listener);
124            sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
125        }
126    }
127
128    private static void sceneChangeSetup(ViewGroup sceneRoot, TransitionPort transition) {
129
130        // Capture current values
131        ArrayList<TransitionPort> runningTransitions = getRunningTransitions().get(sceneRoot);
132
133        if (runningTransitions != null && runningTransitions.size() > 0) {
134            for (TransitionPort runningTransition : runningTransitions) {
135                runningTransition.pause(sceneRoot);
136            }
137        }
138
139        if (transition != null) {
140            transition.captureValues(sceneRoot, true);
141        }
142
143        // Notify previous scene that it is being exited
144        ScenePort previousScene = ScenePort.getCurrentScene(sceneRoot);
145        if (previousScene != null) {
146            previousScene.exit();
147        }
148    }
149
150    public static void go(ScenePort scene) {
151        changeScene(scene, sDefaultTransition);
152    }
153
154    public static void go(ScenePort scene, TransitionPort transition) {
155        changeScene(scene, transition);
156    }
157
158    public static void beginDelayedTransition(final ViewGroup sceneRoot) {
159        beginDelayedTransition(sceneRoot, null);
160    }
161
162    public static void beginDelayedTransition(final ViewGroup sceneRoot,
163            TransitionPort transition) {
164        if (!sPendingTransitions.contains(sceneRoot) && ViewCompat.isLaidOut(sceneRoot)) {
165            if (TransitionPort.DBG) {
166                Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " +
167                        sceneRoot + ", " + transition);
168            }
169            sPendingTransitions.add(sceneRoot);
170            if (transition == null) {
171                transition = sDefaultTransition;
172            }
173            final TransitionPort transitionClone = transition.clone();
174            sceneChangeSetup(sceneRoot, transitionClone);
175            ScenePort.setCurrentScene(sceneRoot, null);
176            sceneChangeRunTransition(sceneRoot, transitionClone);
177        }
178    }
179
180    public void setTransition(ScenePort scene, TransitionPort transition) {
181        mSceneTransitions.put(scene, transition);
182    }
183
184    public void setTransition(ScenePort fromScene, ScenePort toScene, TransitionPort transition) {
185        ArrayMap<ScenePort, TransitionPort> sceneTransitionMap = mScenePairTransitions.get(toScene);
186        if (sceneTransitionMap == null) {
187            sceneTransitionMap = new ArrayMap<>();
188            mScenePairTransitions.put(toScene, sceneTransitionMap);
189        }
190        sceneTransitionMap.put(fromScene, transition);
191    }
192
193    /**
194     * Returns the Transition for the given scene being entered. The result
195     * depends not only on the given scene, but also the scene which the
196     * {@link ScenePort#getSceneRoot() sceneRoot} of the Scene is currently in.
197     *
198     * @param scene The scene being entered
199     * @return The Transition to be used for the given scene change. If no
200     * Transition was specified for this scene change, the default transition
201     * will be used instead.
202     */
203    private TransitionPort getTransition(ScenePort scene) {
204        TransitionPort transition;
205        ViewGroup sceneRoot = scene.getSceneRoot();
206        if (sceneRoot != null) {
207            // TODO: cached in Scene instead? long-term, cache in View itself
208            ScenePort currScene = ScenePort.getCurrentScene(sceneRoot);
209            if (currScene != null) {
210                ArrayMap<ScenePort, TransitionPort> sceneTransitionMap = mScenePairTransitions
211                        .get(scene);
212                if (sceneTransitionMap != null) {
213                    transition = sceneTransitionMap.get(currScene);
214                    if (transition != null) {
215                        return transition;
216                    }
217                }
218            }
219        }
220        transition = mSceneTransitions.get(scene);
221        return (transition != null) ? transition : sDefaultTransition;
222    }
223
224    /**
225     * Retrieve the transition from a named scene to a target defined scene if one has been
226     * associated with this TransitionManager.
227     *
228     * <p>A named scene is an indirect link for a transition. Fundamentally a named
229     * scene represents a potentially arbitrary intersection point of two otherwise independent
230     * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO"
231     * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y.
232     * In this way applications may define an API for more sophisticated transitions between
233     * caller and called activities very similar to the way that <code>Intent</code> extras
234     * define APIs for arguments and data propagation between activities.</p>
235     *
236     * @param fromName Named scene that this transition corresponds to
237     * @param toScene  Target scene that this transition will move to
238     * @return Transition corresponding to the given fromName and toScene or null
239     * if no association exists in this TransitionManager
240     * @see #setTransition(String, ScenePort, TransitionPort)
241     */
242    public TransitionPort getNamedTransition(String fromName, ScenePort toScene) {
243        ArrayMap<ScenePort, TransitionPort> m = mNameSceneTransitions.get(fromName);
244        if (m != null) {
245            return m.get(toScene);
246        }
247        return null;
248    }
249
250    ;
251
252    /**
253     * Retrieve the transition from a defined scene to a target named scene if one has been
254     * associated with this TransitionManager.
255     *
256     * <p>A named scene is an indirect link for a transition. Fundamentally a named
257     * scene represents a potentially arbitrary intersection point of two otherwise independent
258     * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO"
259     * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y.
260     * In this way applications may define an API for more sophisticated transitions between
261     * caller and called activities very similar to the way that <code>Intent</code> extras
262     * define APIs for arguments and data propagation between activities.</p>
263     *
264     * @param fromScene Scene that this transition starts from
265     * @param toName    Name of the target scene
266     * @return Transition corresponding to the given fromScene and toName or null
267     * if no association exists in this TransitionManager
268     */
269    public TransitionPort getNamedTransition(ScenePort fromScene, String toName) {
270        ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene);
271        if (m != null) {
272            return m.get(toName);
273        }
274        return null;
275    }
276
277    /**
278     * Retrieve the supported target named scenes when transitioning away from the given scene.
279     *
280     * <p>A named scene is an indirect link for a transition. Fundamentally a named
281     * scene represents a potentially arbitrary intersection point of two otherwise independent
282     * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO"
283     * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y.
284     * In this way applications may define an API for more sophisticated transitions between
285     * caller and called activities very similar to the way that <code>Intent</code> extras
286     * define APIs for arguments and data propagation between activities.</p>
287     *
288     * @param fromScene Scene to transition from
289     * @return An array of Strings naming each supported transition starting from
290     * <code>fromScene</code>. If no transitions to a named scene from the given
291     * scene are supported this function will return a String[] of length 0.
292     * @see #setTransition(ScenePort, String, TransitionPort)
293     */
294    public String[] getTargetSceneNames(ScenePort fromScene) {
295        final ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene);
296        if (m == null) {
297            return EMPTY_STRINGS;
298        }
299        final int count = m.size();
300        final String[] result = new String[count];
301        for (int i = 0; i < count; i++) {
302            result[i] = m.keyAt(i);
303        }
304        return result;
305    }
306
307    /**
308     * Set a transition from a specific scene to a named scene.
309     *
310     * <p>A named scene is an indirect link for a transition. Fundamentally a named
311     * scene represents a potentially arbitrary intersection point of two otherwise independent
312     * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO"
313     * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y.
314     * In this way applications may define an API for more sophisticated transitions between
315     * caller and called activities very similar to the way that <code>Intent</code> extras
316     * define APIs for arguments and data propagation between activities.</p>
317     *
318     * @param fromScene  Scene to transition from
319     * @param toName     Named scene to transition to
320     * @param transition Transition to use
321     * @see #getTargetSceneNames(ScenePort)
322     */
323    public void setTransition(ScenePort fromScene, String toName, TransitionPort transition) {
324        ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene);
325        if (m == null) {
326            m = new ArrayMap<>();
327            mSceneNameTransitions.put(fromScene, m);
328        }
329        m.put(toName, transition);
330    }
331
332    /**
333     * Set a transition from a named scene to a concrete scene.
334     *
335     * <p>A named scene is an indirect link for a transition. Fundamentally a named
336     * scene represents a potentially arbitrary intersection point of two otherwise independent
337     * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO"
338     * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y.
339     * In this way applications may define an API for more sophisticated transitions between
340     * caller and called activities very similar to the way that <code>Intent</code> extras
341     * define APIs for arguments and data propagation between activities.</p>
342     *
343     * @param fromName   Named scene to transition from
344     * @param toScene    Scene to transition to
345     * @param transition Transition to use
346     * @see #getNamedTransition(String, ScenePort)
347     */
348    public void setTransition(String fromName, ScenePort toScene, TransitionPort transition) {
349        ArrayMap<ScenePort, TransitionPort> m = mNameSceneTransitions.get(fromName);
350        if (m == null) {
351            m = new ArrayMap<>();
352            mNameSceneTransitions.put(fromName, m);
353        }
354        m.put(toScene, transition);
355    }
356
357    public void transitionTo(ScenePort scene) {
358        // Auto transition if there is no transition declared for the Scene, but there is
359        // a root or parent view
360        changeScene(scene, getTransition(scene));
361    }
362
363    /**
364     * This private utility class is used to listen for both OnPreDraw and
365     * OnAttachStateChange events. OnPreDraw events are the main ones we care
366     * about since that's what triggers the transition to take place.
367     * OnAttachStateChange events are also important in case the view is removed
368     * from the hierarchy before the OnPreDraw event takes place; it's used to
369     * clean up things since the OnPreDraw listener didn't get called in time.
370     */
371    private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
372            View.OnAttachStateChangeListener {
373
374        TransitionPort mTransition;
375
376        ViewGroup mSceneRoot;
377
378        MultiListener(TransitionPort transition, ViewGroup sceneRoot) {
379            mTransition = transition;
380            mSceneRoot = sceneRoot;
381        }
382
383        private void removeListeners() {
384            mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
385            mSceneRoot.removeOnAttachStateChangeListener(this);
386        }
387
388        @Override
389        public void onViewAttachedToWindow(View v) {
390        }
391
392        @Override
393        public void onViewDetachedFromWindow(View v) {
394            removeListeners();
395
396            sPendingTransitions.remove(mSceneRoot);
397            ArrayList<TransitionPort> runningTransitions = getRunningTransitions().get(mSceneRoot);
398            if (runningTransitions != null && runningTransitions.size() > 0) {
399                for (TransitionPort runningTransition : runningTransitions) {
400                    runningTransition.resume(mSceneRoot);
401                }
402            }
403            mTransition.clearValues(true);
404        }
405
406        @Override
407        public boolean onPreDraw() {
408            removeListeners();
409            sPendingTransitions.remove(mSceneRoot);
410            // Add to running list, handle end to remove it
411            final ArrayMap<ViewGroup, ArrayList<TransitionPort>> runningTransitions =
412                    getRunningTransitions();
413            ArrayList<TransitionPort> currentTransitions = runningTransitions.get(mSceneRoot);
414            ArrayList<TransitionPort> previousRunningTransitions = null;
415            if (currentTransitions == null) {
416                currentTransitions = new ArrayList<>();
417                runningTransitions.put(mSceneRoot, currentTransitions);
418            } else if (currentTransitions.size() > 0) {
419                previousRunningTransitions = new ArrayList<>(currentTransitions);
420            }
421            currentTransitions.add(mTransition);
422            mTransition.addListener(new TransitionPort.TransitionListenerAdapter() {
423                @Override
424                public void onTransitionEnd(TransitionPort transition) {
425                    ArrayList<TransitionPort> currentTransitions =
426                            runningTransitions.get(mSceneRoot);
427                    currentTransitions.remove(transition);
428                }
429            });
430            mTransition.captureValues(mSceneRoot, false);
431            if (previousRunningTransitions != null) {
432                for (TransitionPort runningTransition : previousRunningTransitions) {
433                    runningTransition.resume(mSceneRoot);
434                }
435            }
436            mTransition.playTransition(mSceneRoot);
437
438            return true;
439        }
440    }
441}
442