1 /* 2 * Copyright (C) 2013 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 17 package android.transition; 18 19 import android.annotation.TestApi; 20 import android.content.Context; 21 import android.util.ArrayMap; 22 import android.util.Log; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.ViewTreeObserver; 26 27 import java.lang.ref.WeakReference; 28 import java.util.ArrayList; 29 30 /** 31 * This class manages the set of transitions that fire when there is a 32 * change of {@link Scene}. To use the manager, add scenes along with 33 * transition objects with calls to {@link #setTransition(Scene, Transition)} 34 * or {@link #setTransition(Scene, Scene, Transition)}. Setting specific 35 * transitions for scene changes is not required; by default, a Scene change 36 * will use {@link AutoTransition} to do something reasonable for most 37 * situations. Specifying other transitions for particular scene changes is 38 * only necessary if the application wants different transition behavior 39 * in these situations. 40 * 41 * <p>TransitionManagers can be declared in XML resource files inside the 42 * <code>res/transition</code> directory. TransitionManager resources consist of 43 * the <code>transitionManager</code>tag name, containing one or more 44 * <code>transition</code> tags, each of which describe the relationship of 45 * that transition to the from/to scene information in that tag. 46 * For example, here is a resource file that declares several scene 47 * transitions:</p> 48 * 49 * {@sample development/samples/ApiDemos/res/transition/transitions_mgr.xml TransitionManager} 50 * 51 * <p>For each of the <code>fromScene</code> and <code>toScene</code> attributes, 52 * there is a reference to a standard XML layout file. This is equivalent to 53 * creating a scene from a layout in code by calling 54 * {@link Scene#getSceneForLayout(ViewGroup, int, Context)}. For the 55 * <code>transition</code> attribute, there is a reference to a resource 56 * file in the <code>res/transition</code> directory which describes that 57 * transition.</p> 58 * 59 * Information on XML resource descriptions for transitions can be found for 60 * {@link android.R.styleable#Transition}, {@link android.R.styleable#TransitionSet}, 61 * {@link android.R.styleable#TransitionTarget}, {@link android.R.styleable#Fade}, 62 * and {@link android.R.styleable#TransitionManager}. 63 */ 64 public class TransitionManager { 65 // TODO: how to handle enter/exit? 66 67 private static String LOG_TAG = "TransitionManager"; 68 69 private static Transition sDefaultTransition = new AutoTransition(); 70 71 private static final String[] EMPTY_STRINGS = new String[0]; 72 73 ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<Scene, Transition>(); 74 ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions = 75 new ArrayMap<Scene, ArrayMap<Scene, Transition>>(); 76 private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>> 77 sRunningTransitions = 78 new ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>(); 79 private static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<ViewGroup>(); 80 81 82 /** 83 * Sets the transition to be used for any scene change for which no 84 * other transition is explicitly set. The initial value is 85 * an {@link AutoTransition} instance. 86 * 87 * @param transition The default transition to be used for scene changes. 88 * 89 * @hide pending later changes 90 */ setDefaultTransition(Transition transition)91 public void setDefaultTransition(Transition transition) { 92 sDefaultTransition = transition; 93 } 94 95 /** 96 * Gets the current default transition. The initial value is an {@link 97 * AutoTransition} instance. 98 * 99 * @return The current default transition. 100 * @see #setDefaultTransition(Transition) 101 * 102 * @hide pending later changes 103 */ getDefaultTransition()104 public static Transition getDefaultTransition() { 105 return sDefaultTransition; 106 } 107 108 /** 109 * Sets a specific transition to occur when the given scene is entered. 110 * 111 * @param scene The scene which, when applied, will cause the given 112 * transition to run. 113 * @param transition The transition that will play when the given scene is 114 * entered. A value of null will result in the default behavior of 115 * using the default transition instead. 116 */ setTransition(Scene scene, Transition transition)117 public void setTransition(Scene scene, Transition transition) { 118 mSceneTransitions.put(scene, transition); 119 } 120 121 /** 122 * Sets a specific transition to occur when the given pair of scenes is 123 * exited/entered. 124 * 125 * @param fromScene The scene being exited when the given transition will 126 * be run 127 * @param toScene The scene being entered when the given transition will 128 * be run 129 * @param transition The transition that will play when the given scene is 130 * entered. A value of null will result in the default behavior of 131 * using the default transition instead. 132 */ setTransition(Scene fromScene, Scene toScene, Transition transition)133 public void setTransition(Scene fromScene, Scene toScene, Transition transition) { 134 ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene); 135 if (sceneTransitionMap == null) { 136 sceneTransitionMap = new ArrayMap<Scene, Transition>(); 137 mScenePairTransitions.put(toScene, sceneTransitionMap); 138 } 139 sceneTransitionMap.put(fromScene, transition); 140 } 141 142 /** 143 * Returns the Transition for the given scene being entered. The result 144 * depends not only on the given scene, but also the scene which the 145 * {@link Scene#getSceneRoot() sceneRoot} of the Scene is currently in. 146 * 147 * @param scene The scene being entered 148 * @return The Transition to be used for the given scene change. If no 149 * Transition was specified for this scene change, the default transition 150 * will be used instead. 151 * @hide 152 */ 153 @TestApi getTransition(Scene scene)154 public Transition getTransition(Scene scene) { 155 Transition transition = null; 156 ViewGroup sceneRoot = scene.getSceneRoot(); 157 if (sceneRoot != null) { 158 // TODO: cached in Scene instead? long-term, cache in View itself 159 Scene currScene = Scene.getCurrentScene(sceneRoot); 160 if (currScene != null) { 161 ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(scene); 162 if (sceneTransitionMap != null) { 163 transition = sceneTransitionMap.get(currScene); 164 if (transition != null) { 165 return transition; 166 } 167 } 168 } 169 } 170 transition = mSceneTransitions.get(scene); 171 return (transition != null) ? transition : sDefaultTransition; 172 } 173 174 /** 175 * This is where all of the work of a transition/scene-change is 176 * orchestrated. This method captures the start values for the given 177 * transition, exits the current Scene, enters the new scene, captures 178 * the end values for the transition, and finally plays the 179 * resulting values-populated transition. 180 * 181 * @param scene The scene being entered 182 * @param transition The transition to play for this scene change 183 */ changeScene(Scene scene, Transition transition)184 private static void changeScene(Scene scene, Transition transition) { 185 186 final ViewGroup sceneRoot = scene.getSceneRoot(); 187 if (!sPendingTransitions.contains(sceneRoot)) { 188 if (transition == null) { 189 scene.enter(); 190 } else { 191 sPendingTransitions.add(sceneRoot); 192 193 Transition transitionClone = transition.clone(); 194 transitionClone.setSceneRoot(sceneRoot); 195 196 Scene oldScene = Scene.getCurrentScene(sceneRoot); 197 if (oldScene != null && oldScene.isCreatedFromLayoutResource()) { 198 transitionClone.setCanRemoveViews(true); 199 } 200 201 sceneChangeSetup(sceneRoot, transitionClone); 202 203 scene.enter(); 204 205 sceneChangeRunTransition(sceneRoot, transitionClone); 206 } 207 } 208 } 209 getRunningTransitions()210 private static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() { 211 WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions = 212 sRunningTransitions.get(); 213 if (runningTransitions == null || runningTransitions.get() == null) { 214 ArrayMap<ViewGroup, ArrayList<Transition>> transitions = 215 new ArrayMap<ViewGroup, ArrayList<Transition>>(); 216 runningTransitions = new WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>( 217 transitions); 218 sRunningTransitions.set(runningTransitions); 219 } 220 return runningTransitions.get(); 221 } 222 sceneChangeRunTransition(final ViewGroup sceneRoot, final Transition transition)223 private static void sceneChangeRunTransition(final ViewGroup sceneRoot, 224 final Transition transition) { 225 if (transition != null && sceneRoot != null) { 226 MultiListener listener = new MultiListener(transition, sceneRoot); 227 sceneRoot.addOnAttachStateChangeListener(listener); 228 sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener); 229 } 230 } 231 232 /** 233 * This private utility class is used to listen for both OnPreDraw and 234 * OnAttachStateChange events. OnPreDraw events are the main ones we care 235 * about since that's what triggers the transition to take place. 236 * OnAttachStateChange events are also important in case the view is removed 237 * from the hierarchy before the OnPreDraw event takes place; it's used to 238 * clean up things since the OnPreDraw listener didn't get called in time. 239 */ 240 private static class MultiListener implements ViewTreeObserver.OnPreDrawListener, 241 View.OnAttachStateChangeListener { 242 243 Transition mTransition; 244 ViewGroup mSceneRoot; 245 final ViewTreeObserver mViewTreeObserver; 246 MultiListener(Transition transition, ViewGroup sceneRoot)247 MultiListener(Transition transition, ViewGroup sceneRoot) { 248 mTransition = transition; 249 mSceneRoot = sceneRoot; 250 mViewTreeObserver = mSceneRoot.getViewTreeObserver(); 251 } 252 removeListeners()253 private void removeListeners() { 254 if (mViewTreeObserver.isAlive()) { 255 mViewTreeObserver.removeOnPreDrawListener(this); 256 } else { 257 mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); 258 } 259 mSceneRoot.removeOnAttachStateChangeListener(this); 260 } 261 262 @Override onViewAttachedToWindow(View v)263 public void onViewAttachedToWindow(View v) { 264 } 265 266 @Override onViewDetachedFromWindow(View v)267 public void onViewDetachedFromWindow(View v) { 268 removeListeners(); 269 270 sPendingTransitions.remove(mSceneRoot); 271 ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot); 272 if (runningTransitions != null && runningTransitions.size() > 0) { 273 for (Transition runningTransition : runningTransitions) { 274 runningTransition.resume(mSceneRoot); 275 } 276 } 277 mTransition.clearValues(true); 278 } 279 280 @Override onPreDraw()281 public boolean onPreDraw() { 282 removeListeners(); 283 284 // Don't start the transition if it's no longer pending. 285 if (!sPendingTransitions.remove(mSceneRoot)) { 286 return true; 287 } 288 289 // Add to running list, handle end to remove it 290 final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions = 291 getRunningTransitions(); 292 ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot); 293 ArrayList<Transition> previousRunningTransitions = null; 294 if (currentTransitions == null) { 295 currentTransitions = new ArrayList<Transition>(); 296 runningTransitions.put(mSceneRoot, currentTransitions); 297 } else if (currentTransitions.size() > 0) { 298 previousRunningTransitions = new ArrayList<Transition>(currentTransitions); 299 } 300 currentTransitions.add(mTransition); 301 mTransition.addListener(new TransitionListenerAdapter() { 302 @Override 303 public void onTransitionEnd(Transition transition) { 304 ArrayList<Transition> currentTransitions = 305 runningTransitions.get(mSceneRoot); 306 currentTransitions.remove(transition); 307 } 308 }); 309 mTransition.captureValues(mSceneRoot, false); 310 if (previousRunningTransitions != null) { 311 for (Transition runningTransition : previousRunningTransitions) { 312 runningTransition.resume(mSceneRoot); 313 } 314 } 315 mTransition.playTransition(mSceneRoot); 316 317 return true; 318 } 319 }; 320 sceneChangeSetup(ViewGroup sceneRoot, Transition transition)321 private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) { 322 323 // Capture current values 324 ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot); 325 326 if (runningTransitions != null && runningTransitions.size() > 0) { 327 for (Transition runningTransition : runningTransitions) { 328 runningTransition.pause(sceneRoot); 329 } 330 } 331 332 if (transition != null) { 333 transition.captureValues(sceneRoot, true); 334 } 335 336 // Notify previous scene that it is being exited 337 Scene previousScene = Scene.getCurrentScene(sceneRoot); 338 if (previousScene != null) { 339 previousScene.exit(); 340 } 341 } 342 343 /** 344 * Change to the given scene, using the 345 * appropriate transition for this particular scene change 346 * (as specified to the TransitionManager, or the default 347 * if no such transition exists). 348 * 349 * @param scene The Scene to change to 350 */ transitionTo(Scene scene)351 public void transitionTo(Scene scene) { 352 // Auto transition if there is no transition declared for the Scene, but there is 353 // a root or parent view 354 changeScene(scene, getTransition(scene)); 355 } 356 357 /** 358 * Convenience method to simply change to the given scene using 359 * the default transition for TransitionManager. 360 * 361 * @param scene The Scene to change to 362 */ go(Scene scene)363 public static void go(Scene scene) { 364 changeScene(scene, sDefaultTransition); 365 } 366 367 /** 368 * Convenience method to simply change to the given scene using 369 * the given transition. 370 * 371 * <p>Passing in <code>null</code> for the transition parameter will 372 * result in the scene changing without any transition running, and is 373 * equivalent to calling {@link Scene#exit()} on the scene root's 374 * current scene, followed by {@link Scene#enter()} on the scene 375 * specified by the <code>scene</code> parameter.</p> 376 * 377 * @param scene The Scene to change to 378 * @param transition The transition to use for this scene change. A 379 * value of null causes the scene change to happen with no transition. 380 */ go(Scene scene, Transition transition)381 public static void go(Scene scene, Transition transition) { 382 changeScene(scene, transition); 383 } 384 385 /** 386 * Convenience method to animate, using the default transition, 387 * to a new scene defined by all changes within the given scene root between 388 * calling this method and the next rendering frame. 389 * Equivalent to calling {@link #beginDelayedTransition(ViewGroup, Transition)} 390 * with a value of <code>null</code> for the <code>transition</code> parameter. 391 * 392 * @param sceneRoot The root of the View hierarchy to run the transition on. 393 */ beginDelayedTransition(final ViewGroup sceneRoot)394 public static void beginDelayedTransition(final ViewGroup sceneRoot) { 395 beginDelayedTransition(sceneRoot, null); 396 } 397 398 /** 399 * Convenience method to animate to a new scene defined by all changes within 400 * the given scene root between calling this method and the next rendering frame. 401 * Calling this method causes TransitionManager to capture current values in the 402 * scene root and then post a request to run a transition on the next frame. 403 * At that time, the new values in the scene root will be captured and changes 404 * will be animated. There is no need to create a Scene; it is implied by 405 * changes which take place between calling this method and the next frame when 406 * the transition begins. 407 * 408 * <p>Calling this method several times before the next frame (for example, if 409 * unrelated code also wants to make dynamic changes and run a transition on 410 * the same scene root), only the first call will trigger capturing values 411 * and exiting the current scene. Subsequent calls to the method with the 412 * same scene root during the same frame will be ignored.</p> 413 * 414 * <p>Passing in <code>null</code> for the transition parameter will 415 * cause the TransitionManager to use its default transition.</p> 416 * 417 * @param sceneRoot The root of the View hierarchy to run the transition on. 418 * @param transition The transition to use for this change. A 419 * value of null causes the TransitionManager to use the default transition. 420 */ beginDelayedTransition(final ViewGroup sceneRoot, Transition transition)421 public static void beginDelayedTransition(final ViewGroup sceneRoot, Transition transition) { 422 if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) { 423 if (Transition.DBG) { 424 Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " + 425 sceneRoot + ", " + transition); 426 } 427 sPendingTransitions.add(sceneRoot); 428 if (transition == null) { 429 transition = sDefaultTransition; 430 } 431 final Transition transitionClone = transition.clone(); 432 sceneChangeSetup(sceneRoot, transitionClone); 433 Scene.setCurrentScene(sceneRoot, null); 434 sceneChangeRunTransition(sceneRoot, transitionClone); 435 } 436 } 437 438 /** 439 * Ends all pending and ongoing transitions on the specified scene root. 440 * 441 * @param sceneRoot The root of the View hierarchy to end transitions on. 442 */ endTransitions(final ViewGroup sceneRoot)443 public static void endTransitions(final ViewGroup sceneRoot) { 444 sPendingTransitions.remove(sceneRoot); 445 446 final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot); 447 if (runningTransitions != null && !runningTransitions.isEmpty()) { 448 // Make a copy in case this is called by an onTransitionEnd listener 449 ArrayList<Transition> copy = new ArrayList(runningTransitions); 450 for (int i = copy.size() - 1; i >= 0; i--) { 451 final Transition transition = copy.get(i); 452 transition.forceToEnd(sceneRoot); 453 } 454 } 455 456 } 457 } 458