blob: 7857f573bf81a3c45ce5c63257fdf4c95309b5a1 [file] [log] [blame]
/*
* 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 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;
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<>();
private 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)
*/
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
*/
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);
}
private 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();
}
}
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();
}
}
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();
}
}
mTransition.playTransition(mSceneRoot);
return true;
}
}
}