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