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