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