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