blob: 1b0612e872025975b123a15c35b50df10605bd81 [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
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
17package android.transition;
18
19import android.annotation.TestApi;
20import android.compat.annotation.UnsupportedAppUsage;
21import android.content.Context;
22import android.util.ArrayMap;
23import android.util.Log;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.ViewTreeObserver;
27
28import java.lang.ref.WeakReference;
29import 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 */
65public 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 */
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 */
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 */
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 */
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
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 */
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
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
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
258 MultiListener(Transition transition, ViewGroup sceneRoot) {
259 mTransition = transition;
260 mSceneRoot = sceneRoot;
261 mViewTreeObserver = mSceneRoot.getViewTreeObserver();
262 }
263
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
274 public void onViewAttachedToWindow(View v) {
275 }
276
277 @Override
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
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
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 */
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 */
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 */
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 */
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 */
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 */
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}