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