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