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 17package android.support.transition; 18 19import android.support.annotation.RestrictTo; 20import android.support.v4.util.ArrayMap; 21import android.support.v4.view.ViewCompat; 22import android.util.Log; 23import android.view.View; 24import android.view.ViewGroup; 25import android.view.ViewTreeObserver; 26 27import java.lang.ref.WeakReference; 28import java.util.ArrayList; 29 30import static android.support.annotation.RestrictTo.Scope.GROUP_ID; 31 32class TransitionManagerPort { 33 // TODO: how to handle enter/exit? 34 35 private static final String[] EMPTY_STRINGS = new String[0]; 36 37 private static String LOG_TAG = "TransitionManager"; 38 39 private static TransitionPort sDefaultTransition = new AutoTransitionPort(); 40 41 private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<TransitionPort>>>> 42 sRunningTransitions = new ThreadLocal<>(); 43 44 static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<>(); 45 46 ArrayMap<ScenePort, TransitionPort> mSceneTransitions = new ArrayMap<>(); 47 48 ArrayMap<ScenePort, ArrayMap<ScenePort, TransitionPort>> mScenePairTransitions = 49 new ArrayMap<>(); 50 51 ArrayMap<ScenePort, ArrayMap<String, TransitionPort>> mSceneNameTransitions = new ArrayMap<>(); 52 53 ArrayMap<String, ArrayMap<ScenePort, TransitionPort>> mNameSceneTransitions = new ArrayMap<>(); 54 55 /** 56 * Gets the current default transition. The initial value is an {@link 57 * AutoTransition} instance. 58 * 59 * @return The current default transition. 60 * @hide pending later changes 61 * @see #setDefaultTransition(TransitionPort) 62 */ 63 @RestrictTo(GROUP_ID) 64 public static TransitionPort getDefaultTransition() { 65 return sDefaultTransition; 66 } 67 68 /** 69 * Sets the transition to be used for any scene change for which no 70 * other transition is explicitly set. The initial value is 71 * an {@link AutoTransition} instance. 72 * 73 * @param transition The default transition to be used for scene changes. 74 * @hide pending later changes 75 */ 76 @RestrictTo(GROUP_ID) 77 public void setDefaultTransition(TransitionPort transition) { 78 sDefaultTransition = transition; 79 } 80 81 /** 82 * This is where all of the work of a transition/scene-change is 83 * orchestrated. This method captures the start values for the given 84 * transition, exits the current Scene, enters the new scene, captures 85 * the end values for the transition, and finally plays the 86 * resulting values-populated transition. 87 * 88 * @param scene The scene being entered 89 * @param transition The transition to play for this scene change 90 */ 91 private static void changeScene(ScenePort scene, TransitionPort transition) { 92 93 final ViewGroup sceneRoot = scene.getSceneRoot(); 94 95 TransitionPort transitionClone = null; 96 if (transition != null) { 97 transitionClone = transition.clone(); 98 transitionClone.setSceneRoot(sceneRoot); 99 } 100 101 ScenePort oldScene = ScenePort.getCurrentScene(sceneRoot); 102 if (oldScene != null && oldScene.isCreatedFromLayoutResource()) { 103 transitionClone.setCanRemoveViews(true); 104 } 105 106 sceneChangeSetup(sceneRoot, transitionClone); 107 108 scene.enter(); 109 110 sceneChangeRunTransition(sceneRoot, transitionClone); 111 } 112 113 static ArrayMap<ViewGroup, ArrayList<TransitionPort>> getRunningTransitions() { 114 WeakReference<ArrayMap<ViewGroup, ArrayList<TransitionPort>>> runningTransitions = 115 sRunningTransitions.get(); 116 if (runningTransitions == null || runningTransitions.get() == null) { 117 ArrayMap<ViewGroup, ArrayList<TransitionPort>> transitions = new ArrayMap<>(); 118 runningTransitions = new WeakReference<>(transitions); 119 sRunningTransitions.set(runningTransitions); 120 } 121 return runningTransitions.get(); 122 } 123 124 private static void sceneChangeRunTransition(final ViewGroup sceneRoot, 125 final TransitionPort transition) { 126 if (transition != null && sceneRoot != null) { 127 MultiListener listener = new MultiListener(transition, sceneRoot); 128 sceneRoot.addOnAttachStateChangeListener(listener); 129 sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener); 130 } 131 } 132 133 private static void sceneChangeSetup(ViewGroup sceneRoot, TransitionPort transition) { 134 135 // Capture current values 136 ArrayList<TransitionPort> runningTransitions = getRunningTransitions().get(sceneRoot); 137 138 if (runningTransitions != null && runningTransitions.size() > 0) { 139 for (TransitionPort runningTransition : runningTransitions) { 140 runningTransition.pause(sceneRoot); 141 } 142 } 143 144 if (transition != null) { 145 transition.captureValues(sceneRoot, true); 146 } 147 148 // Notify previous scene that it is being exited 149 ScenePort previousScene = ScenePort.getCurrentScene(sceneRoot); 150 if (previousScene != null) { 151 previousScene.exit(); 152 } 153 } 154 155 public static void go(ScenePort scene) { 156 changeScene(scene, sDefaultTransition); 157 } 158 159 public static void go(ScenePort scene, TransitionPort transition) { 160 changeScene(scene, transition); 161 } 162 163 public static void beginDelayedTransition(final ViewGroup sceneRoot) { 164 beginDelayedTransition(sceneRoot, null); 165 } 166 167 public static void beginDelayedTransition(final ViewGroup sceneRoot, 168 TransitionPort transition) { 169 if (!sPendingTransitions.contains(sceneRoot) && ViewCompat.isLaidOut(sceneRoot)) { 170 if (TransitionPort.DBG) { 171 Log.d(LOG_TAG, "beginDelayedTransition: root, transition = " + 172 sceneRoot + ", " + transition); 173 } 174 sPendingTransitions.add(sceneRoot); 175 if (transition == null) { 176 transition = sDefaultTransition; 177 } 178 final TransitionPort transitionClone = transition.clone(); 179 sceneChangeSetup(sceneRoot, transitionClone); 180 ScenePort.setCurrentScene(sceneRoot, null); 181 sceneChangeRunTransition(sceneRoot, transitionClone); 182 } 183 } 184 185 public void setTransition(ScenePort scene, TransitionPort transition) { 186 mSceneTransitions.put(scene, transition); 187 } 188 189 public void setTransition(ScenePort fromScene, ScenePort toScene, TransitionPort transition) { 190 ArrayMap<ScenePort, TransitionPort> sceneTransitionMap = mScenePairTransitions.get(toScene); 191 if (sceneTransitionMap == null) { 192 sceneTransitionMap = new ArrayMap<>(); 193 mScenePairTransitions.put(toScene, sceneTransitionMap); 194 } 195 sceneTransitionMap.put(fromScene, transition); 196 } 197 198 /** 199 * Returns the Transition for the given scene being entered. The result 200 * depends not only on the given scene, but also the scene which the 201 * {@link ScenePort#getSceneRoot() sceneRoot} of the Scene is currently in. 202 * 203 * @param scene The scene being entered 204 * @return The Transition to be used for the given scene change. If no 205 * Transition was specified for this scene change, the default transition 206 * will be used instead. 207 */ 208 private TransitionPort getTransition(ScenePort scene) { 209 TransitionPort transition; 210 ViewGroup sceneRoot = scene.getSceneRoot(); 211 if (sceneRoot != null) { 212 // TODO: cached in Scene instead? long-term, cache in View itself 213 ScenePort currScene = ScenePort.getCurrentScene(sceneRoot); 214 if (currScene != null) { 215 ArrayMap<ScenePort, TransitionPort> sceneTransitionMap = mScenePairTransitions 216 .get(scene); 217 if (sceneTransitionMap != null) { 218 transition = sceneTransitionMap.get(currScene); 219 if (transition != null) { 220 return transition; 221 } 222 } 223 } 224 } 225 transition = mSceneTransitions.get(scene); 226 return (transition != null) ? transition : sDefaultTransition; 227 } 228 229 /** 230 * Retrieve the transition from a named scene to a target defined scene if one has been 231 * associated with this TransitionManager. 232 * 233 * <p>A named scene is an indirect link for a transition. Fundamentally a named 234 * scene represents a potentially arbitrary intersection point of two otherwise independent 235 * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" 236 * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. 237 * In this way applications may define an API for more sophisticated transitions between 238 * caller and called activities very similar to the way that <code>Intent</code> extras 239 * define APIs for arguments and data propagation between activities.</p> 240 * 241 * @param fromName Named scene that this transition corresponds to 242 * @param toScene Target scene that this transition will move to 243 * @return Transition corresponding to the given fromName and toScene or null 244 * if no association exists in this TransitionManager 245 * @see #setTransition(String, ScenePort, TransitionPort) 246 */ 247 public TransitionPort getNamedTransition(String fromName, ScenePort toScene) { 248 ArrayMap<ScenePort, TransitionPort> m = mNameSceneTransitions.get(fromName); 249 if (m != null) { 250 return m.get(toScene); 251 } 252 return null; 253 } 254 255 ; 256 257 /** 258 * Retrieve the transition from a defined scene to a target named scene if one has been 259 * associated with this TransitionManager. 260 * 261 * <p>A named scene is an indirect link for a transition. Fundamentally a named 262 * scene represents a potentially arbitrary intersection point of two otherwise independent 263 * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" 264 * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. 265 * In this way applications may define an API for more sophisticated transitions between 266 * caller and called activities very similar to the way that <code>Intent</code> extras 267 * define APIs for arguments and data propagation between activities.</p> 268 * 269 * @param fromScene Scene that this transition starts from 270 * @param toName Name of the target scene 271 * @return Transition corresponding to the given fromScene and toName or null 272 * if no association exists in this TransitionManager 273 */ 274 public TransitionPort getNamedTransition(ScenePort fromScene, String toName) { 275 ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene); 276 if (m != null) { 277 return m.get(toName); 278 } 279 return null; 280 } 281 282 /** 283 * Retrieve the supported target named scenes when transitioning away from the given scene. 284 * 285 * <p>A named scene is an indirect link for a transition. Fundamentally a named 286 * scene represents a potentially arbitrary intersection point of two otherwise independent 287 * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" 288 * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. 289 * In this way applications may define an API for more sophisticated transitions between 290 * caller and called activities very similar to the way that <code>Intent</code> extras 291 * define APIs for arguments and data propagation between activities.</p> 292 * 293 * @param fromScene Scene to transition from 294 * @return An array of Strings naming each supported transition starting from 295 * <code>fromScene</code>. If no transitions to a named scene from the given 296 * scene are supported this function will return a String[] of length 0. 297 * @see #setTransition(ScenePort, String, TransitionPort) 298 */ 299 public String[] getTargetSceneNames(ScenePort fromScene) { 300 final ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene); 301 if (m == null) { 302 return EMPTY_STRINGS; 303 } 304 final int count = m.size(); 305 final String[] result = new String[count]; 306 for (int i = 0; i < count; i++) { 307 result[i] = m.keyAt(i); 308 } 309 return result; 310 } 311 312 /** 313 * Set a transition from a specific scene to a named scene. 314 * 315 * <p>A named scene is an indirect link for a transition. Fundamentally a named 316 * scene represents a potentially arbitrary intersection point of two otherwise independent 317 * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" 318 * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. 319 * In this way applications may define an API for more sophisticated transitions between 320 * caller and called activities very similar to the way that <code>Intent</code> extras 321 * define APIs for arguments and data propagation between activities.</p> 322 * 323 * @param fromScene Scene to transition from 324 * @param toName Named scene to transition to 325 * @param transition Transition to use 326 * @see #getTargetSceneNames(ScenePort) 327 */ 328 public void setTransition(ScenePort fromScene, String toName, TransitionPort transition) { 329 ArrayMap<String, TransitionPort> m = mSceneNameTransitions.get(fromScene); 330 if (m == null) { 331 m = new ArrayMap<>(); 332 mSceneNameTransitions.put(fromScene, m); 333 } 334 m.put(toName, transition); 335 } 336 337 /** 338 * Set a transition from a named scene to a concrete scene. 339 * 340 * <p>A named scene is an indirect link for a transition. Fundamentally a named 341 * scene represents a potentially arbitrary intersection point of two otherwise independent 342 * transitions. Activity A may define a transition from scene X to "com.example.scene.FOO" 343 * while activity B may define a transition from scene "com.example.scene.FOO" to scene Y. 344 * In this way applications may define an API for more sophisticated transitions between 345 * caller and called activities very similar to the way that <code>Intent</code> extras 346 * define APIs for arguments and data propagation between activities.</p> 347 * 348 * @param fromName Named scene to transition from 349 * @param toScene Scene to transition to 350 * @param transition Transition to use 351 * @see #getNamedTransition(String, ScenePort) 352 */ 353 public void setTransition(String fromName, ScenePort toScene, TransitionPort transition) { 354 ArrayMap<ScenePort, TransitionPort> m = mNameSceneTransitions.get(fromName); 355 if (m == null) { 356 m = new ArrayMap<>(); 357 mNameSceneTransitions.put(fromName, m); 358 } 359 m.put(toScene, transition); 360 } 361 362 public void transitionTo(ScenePort scene) { 363 // Auto transition if there is no transition declared for the Scene, but there is 364 // a root or parent view 365 changeScene(scene, getTransition(scene)); 366 } 367 368 /** 369 * This private utility class is used to listen for both OnPreDraw and 370 * OnAttachStateChange events. OnPreDraw events are the main ones we care 371 * about since that's what triggers the transition to take place. 372 * OnAttachStateChange events are also important in case the view is removed 373 * from the hierarchy before the OnPreDraw event takes place; it's used to 374 * clean up things since the OnPreDraw listener didn't get called in time. 375 */ 376 private static class MultiListener implements ViewTreeObserver.OnPreDrawListener, 377 View.OnAttachStateChangeListener { 378 379 TransitionPort mTransition; 380 381 ViewGroup mSceneRoot; 382 383 MultiListener(TransitionPort transition, ViewGroup sceneRoot) { 384 mTransition = transition; 385 mSceneRoot = sceneRoot; 386 } 387 388 private void removeListeners() { 389 mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); 390 mSceneRoot.removeOnAttachStateChangeListener(this); 391 } 392 393 @Override 394 public void onViewAttachedToWindow(View v) { 395 } 396 397 @Override 398 public void onViewDetachedFromWindow(View v) { 399 removeListeners(); 400 401 sPendingTransitions.remove(mSceneRoot); 402 ArrayList<TransitionPort> runningTransitions = getRunningTransitions().get(mSceneRoot); 403 if (runningTransitions != null && runningTransitions.size() > 0) { 404 for (TransitionPort runningTransition : runningTransitions) { 405 runningTransition.resume(mSceneRoot); 406 } 407 } 408 mTransition.clearValues(true); 409 } 410 411 @Override 412 public boolean onPreDraw() { 413 removeListeners(); 414 sPendingTransitions.remove(mSceneRoot); 415 // Add to running list, handle end to remove it 416 final ArrayMap<ViewGroup, ArrayList<TransitionPort>> runningTransitions = 417 getRunningTransitions(); 418 ArrayList<TransitionPort> currentTransitions = runningTransitions.get(mSceneRoot); 419 ArrayList<TransitionPort> previousRunningTransitions = null; 420 if (currentTransitions == null) { 421 currentTransitions = new ArrayList<>(); 422 runningTransitions.put(mSceneRoot, currentTransitions); 423 } else if (currentTransitions.size() > 0) { 424 previousRunningTransitions = new ArrayList<>(currentTransitions); 425 } 426 currentTransitions.add(mTransition); 427 mTransition.addListener(new TransitionPort.TransitionListenerAdapter() { 428 @Override 429 public void onTransitionEnd(TransitionPort transition) { 430 ArrayList<TransitionPort> currentTransitions = 431 runningTransitions.get(mSceneRoot); 432 currentTransitions.remove(transition); 433 } 434 }); 435 mTransition.captureValues(mSceneRoot, false); 436 if (previousRunningTransitions != null) { 437 for (TransitionPort runningTransition : previousRunningTransitions) { 438 runningTransition.resume(mSceneRoot); 439 } 440 } 441 mTransition.playTransition(mSceneRoot); 442 443 return true; 444 } 445 } 446} 447