/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.transition; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.annotation.TargetApi; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.support.v4.util.ArrayMap; import android.support.v4.view.ViewCompat; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import java.lang.ref.WeakReference; import java.util.ArrayList; @RequiresApi(14) @TargetApi(14) class TransitionManagerPort { // TODO: how to handle enter/exit? private static final String[] EMPTY_STRINGS = new String[0]; private static String LOG_TAG = "TransitionManager"; private static TransitionPort sDefaultTransition = new AutoTransitionPort(); private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<TransitionPort>>>> sRunningTransitions = new ThreadLocal<>(); static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<>(); ArrayMap<ScenePort, TransitionPort> mSceneTransitions = new ArrayMap<>(); ArrayMap<ScenePort, ArrayMap<ScenePort, TransitionPort>> mScenePairTransitions = new ArrayMap<>(); ArrayMap<ScenePort, ArrayMap<String, TransitionPort>> mSceneNameTransitions = new ArrayMap<>(); ArrayMap<String, ArrayMap<ScenePort, TransitionPort>> mNameSceneTransitions = new ArrayMap<>(); /** * Gets the current default transition. The initial value is an {@link * AutoTransition} instance. * * @return The current default transition. * @hide pending later changes * @see #setDefaultTransition(TransitionPort) */ @RestrictTo(LIBRARY_GROUP) public static TransitionPort getDefaultTransition() { return sDefaultTransition; } /** * Sets the transition to be used for any scene change for which no * other transition is explicitly set. The initial value is * an {@link AutoTransition} instance. * * @param transition The default transition to be used for scene changes. * @hide pending later changes */ @RestrictTo(LIBRARY_GROUP) public void setDefaultTransition(TransitionPort transition) { sDefaultTransition = transition; } /** * This is where all of the work of a transition/scene-change is * orchestrated. This method captures the start values for the given * transition, exits the current Scene, enters the new scene, captures * the end values for the transition, and finally plays the * resulting values-populated transition. * * @param scene The scene being entered * @param transition The transition to play for this scene change */ private static void changeScene(ScenePort scene, TransitionPort transition) { final ViewGroup sceneRoot = scene.getSceneRoot(); TransitionPort transitionClone = null; if (transition != null) { transitionClone = transition.clone(); transitionClone.setSceneRoot(sceneRoot); } ScenePort oldScene = ScenePort.getCurrentScene(sceneRoot); if (oldScene != null && oldScene.isCreatedFromLayoutResource()) { transitionClone.setCanRemoveViews(true); } sceneChangeSetup(sceneRoot, transitionClone); scene.enter(); sceneChangeRunTransition(sceneRoot, transitionClone); } static ArrayMap<ViewGroup, ArrayList<TransitionPort>> getRunningTransitions() { WeakReference<ArrayMap<ViewGroup, ArrayList<TransitionPort>>> runningTransitions = sRunningTransitions.get(); if (runningTransitions == null || runningTransitions.get() == null) { ArrayMap<ViewGroup, ArrayList<TransitionPort>> transitions = new ArrayMap<>(); runningTransitions = new WeakReference<>(transitions); sRunningTransitions.set(runningTransitions); } return runningTransitions.get(); } private static void sceneChangeRunTransition(final ViewGroup sceneRoot, final TransitionPort transition) { if (transition != null && sceneRoot != null) { MultiListener listener = new MultiListener(transition, sceneRoot); sceneRoot.addOnAttachStateChangeListener(listener); sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener); } } private static void sceneChangeSetup(ViewGroup sceneRoot, TransitionPort transition) { // Capture current values ArrayList<TransitionPort> runningTransitions = getRunningTransitions().get(sceneRoot); if (runningTransitions != null && runningTransitions.size() > 0) { for (TransitionPort runningTransition : runningTransitions) { runningTransition.pause(sceneRoot); } } if (transition != null) { transition.captureValues(sceneRoot, true); } // Notify previous scene that it is being exited ScenePort previousScene = ScenePort.getCurrentScene(sceneRoot); if (previousScene != null) { previousScene.exit(); } } public static void go(ScenePort scene) { changeScene(scene, sDefaultTransition); } public static void go(ScenePort scene, TransitionPort transition) { changeScene(scene, transition); } public static void beginDelayedTransition(final ViewGroup sceneRoot) { beginDelayedTransition(sceneRoot, null); } public static void beginDelayedTransition(final ViewGroup sceneRoot, TransitionPort transition) { if (!sPendingTransitions.contains(sceneRoot) && ViewCompat.isLaidOut(sceneRoot)) { if (TransitionPort.DBG) { Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " + sceneRoot + ", " + transition); } sPendingTransitions.add(sceneRoot); if (transition == null) { transition = sDefaultTransition; } final TransitionPort transitionClone = transition.clone(); sceneChangeSetup(sceneRoot, transitionClone); ScenePort.setCurrentScene(sceneRoot, null); sceneChangeRunTransition(sceneRoot, transitionClone); } } public void setTransition(ScenePort scene, TransitionPort transition) { mSceneTransitions.put(scene, transition); } public void setTransition(ScenePort fromScene, ScenePort toScene, TransitionPort transition) { ArrayMap<ScenePort, TransitionPort> sceneTransitionMap = mScenePairTransitions.get(toScene); if (sceneTransitionMap == null) { sceneTransitionMap = new ArrayMap<>(); mScenePairTransitions.put(toScene, sceneTransitionMap); } sceneTransitionMap.put(fromScene, transition); } /** * Returns the Transition for the given scene being entered. The result * depends not only on the given scene, but also the scene which the * {@link ScenePort#getSceneRoot() sceneRoot} of the Scene is currently in. * * @param scene The scene being entered * @return The Transition to be used for the given scene change. If no * Transition was specified for this scene change, the default transition * will be used instead. */ private TransitionPort getTransition(ScenePort scene) { TransitionPort transition; ViewGroup sceneRoot = scene.getSceneRoot(); if (sceneRoot != null) { // TODO: cached in Scene instead? long-term, cache in View itself ScenePort currScene = ScenePort.getCurrentScene(sceneRoot); if (currScene != null) { ArrayMap<ScenePort, TransitionPort> sceneTransitionMap = mScenePairTransitions .get(scene); if (sceneTransitionMap != null) { transition = sceneTransitionMap.get(currScene); if (transition != null) { return transition; } } } } transition = mSceneTransitions.get(scene); return (transition != null) ? transition : sDefaultTransition; } /** * Retrieve the transition from a named scene to a target defined scene if one has been * associated with this TransitionManager. * * <p>A named scene is an indirect link for a transition. Fundamentally a named * scene represents a potentially arbitrary intersection point of two otherwise independent * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. * In this way applications may define an API for more sophisticated transitions between * caller and called activities very similar to the way that <code>Intent</code> extras * define APIs for arguments and data propagation between activities.</p> * * @param fromName Named scene that this transition corresponds to * @param toScene Target scene that this transition will move to * @return Transition corresponding to the given fromName and toScene or null * if no association exists in this TransitionManager * @see #setTransition(String, ScenePort, TransitionPort) */ public TransitionPort getNamedTransition(String fromName, ScenePort toScene) { ArrayMap<ScenePort, TransitionPort> m = mNameSceneTransitions.get(fromName); if (m != null) { return m.get(toScene); } return null; } ; /** * Retrieve the transition from a defined scene to a target named scene if one has been * associated with this TransitionManager. * * <p>A named scene is an indirect link for a transition. Fundamentally a named * scene represents a potentially arbitrary intersection point of two otherwise independent * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. * In this way applications may define an API for more sophisticated transitions between * caller and called activities very similar to the way that <code>Intent</code> extras * define APIs for arguments and data propagation between activities.</p> * * @param fromScene Scene that this transition starts from * @param toName Name of the target scene * @return Transition corresponding to the given fromScene and toName or null * if no association exists in this TransitionManager */ public TransitionPort getNamedTransition(ScenePort fromScene, String toName) { ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene); if (m != null) { return m.get(toName); } return null; } /** * Retrieve the supported target named scenes when transitioning away from the given scene. * * <p>A named scene is an indirect link for a transition. Fundamentally a named * scene represents a potentially arbitrary intersection point of two otherwise independent * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. * In this way applications may define an API for more sophisticated transitions between * caller and called activities very similar to the way that <code>Intent</code> extras * define APIs for arguments and data propagation between activities.</p> * * @param fromScene Scene to transition from * @return An array of Strings naming each supported transition starting from * <code>fromScene</code>. If no transitions to a named scene from the given * scene are supported this function will return a String[] of length 0. * @see #setTransition(ScenePort, String, TransitionPort) */ public String[] getTargetSceneNames(ScenePort fromScene) { final ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene); if (m == null) { return EMPTY_STRINGS; } final int count = m.size(); final String[] result = new String[count]; for (int i = 0; i < count; i++) { result[i] = m.keyAt(i); } return result; } /** * Set a transition from a specific scene to a named scene. * * <p>A named scene is an indirect link for a transition. Fundamentally a named * scene represents a potentially arbitrary intersection point of two otherwise independent * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. * In this way applications may define an API for more sophisticated transitions between * caller and called activities very similar to the way that <code>Intent</code> extras * define APIs for arguments and data propagation between activities.</p> * * @param fromScene Scene to transition from * @param toName Named scene to transition to * @param transition Transition to use * @see #getTargetSceneNames(ScenePort) */ public void setTransition(ScenePort fromScene, String toName, TransitionPort transition) { ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene); if (m == null) { m = new ArrayMap<>(); mSceneNameTransitions.put(fromScene, m); } m.put(toName, transition); } /** * Set a transition from a named scene to a concrete scene. * * <p>A named scene is an indirect link for a transition. Fundamentally a named * scene represents a potentially arbitrary intersection point of two otherwise independent * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. * In this way applications may define an API for more sophisticated transitions between * caller and called activities very similar to the way that <code>Intent</code> extras * define APIs for arguments and data propagation between activities.</p> * * @param fromName Named scene to transition from * @param toScene Scene to transition to * @param transition Transition to use * @see #getNamedTransition(String, ScenePort) */ public void setTransition(String fromName, ScenePort toScene, TransitionPort transition) { ArrayMap<ScenePort, TransitionPort> m = mNameSceneTransitions.get(fromName); if (m == null) { m = new ArrayMap<>(); mNameSceneTransitions.put(fromName, m); } m.put(toScene, transition); } public void transitionTo(ScenePort scene) { // Auto transition if there is no transition declared for the Scene, but there is // a root or parent view changeScene(scene, getTransition(scene)); } /** * This private utility class is used to listen for both OnPreDraw and * OnAttachStateChange events. OnPreDraw events are the main ones we care * about since that's what triggers the transition to take place. * OnAttachStateChange events are also important in case the view is removed * from the hierarchy before the OnPreDraw event takes place; it's used to * clean up things since the OnPreDraw listener didn't get called in time. */ private static class MultiListener implements ViewTreeObserver.OnPreDrawListener, View.OnAttachStateChangeListener { TransitionPort mTransition; ViewGroup mSceneRoot; MultiListener(TransitionPort transition, ViewGroup sceneRoot) { mTransition = transition; mSceneRoot = sceneRoot; } private void removeListeners() { mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); mSceneRoot.removeOnAttachStateChangeListener(this); } @Override public void onViewAttachedToWindow(View v) { } @Override public void onViewDetachedFromWindow(View v) { removeListeners(); sPendingTransitions.remove(mSceneRoot); ArrayList<TransitionPort> runningTransitions = getRunningTransitions().get(mSceneRoot); if (runningTransitions != null && runningTransitions.size() > 0) { for (TransitionPort runningTransition : runningTransitions) { runningTransition.resume(mSceneRoot); } } mTransition.clearValues(true); } @Override public boolean onPreDraw() { removeListeners(); sPendingTransitions.remove(mSceneRoot); // Add to running list, handle end to remove it final ArrayMap<ViewGroup, ArrayList<TransitionPort>> runningTransitions = getRunningTransitions(); ArrayList<TransitionPort> currentTransitions = runningTransitions.get(mSceneRoot); ArrayList<TransitionPort> previousRunningTransitions = null; if (currentTransitions == null) { currentTransitions = new ArrayList<>(); runningTransitions.put(mSceneRoot, currentTransitions); } else if (currentTransitions.size() > 0) { previousRunningTransitions = new ArrayList<>(currentTransitions); } currentTransitions.add(mTransition); mTransition.addListener(new TransitionPort.TransitionListenerAdapter() { @Override public void onTransitionEnd(TransitionPort transition) { ArrayList<TransitionPort> currentTransitions = runningTransitions.get(mSceneRoot); currentTransitions.remove(transition); } }); mTransition.captureValues(mSceneRoot, false); if (previousRunningTransitions != null) { for (TransitionPort runningTransition : previousRunningTransitions) { runningTransition.resume(mSceneRoot); } } mTransition.playTransition(mSceneRoot); return true; } } }