public class

TransitionManager

extends java.lang.Object

 java.lang.Object

↳androidx.transition.TransitionManager

Gradle dependencies

compile group: 'androidx.transition', name: 'transition', version: '1.5.1'

  • groupId: androidx.transition
  • artifactId: transition
  • version: 1.5.1

Artifact androidx.transition:transition:1.5.1 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.transition:transition com.android.support:transition

Androidx class mapping:

androidx.transition.TransitionManager android.support.transition.TransitionManager

Overview

This class manages the set of transitions that fire when there is a change of Scene. To use the manager, add scenes along with transition objects with calls to TransitionManager.setTransition(Scene, Transition) or TransitionManager.setTransition(Scene, Scene, Transition). Setting specific transitions for scene changes is not required; by default, a Scene change will use AutoTransition to do something reasonable for most situations. Specifying other transitions for particular scene changes is only necessary if the application wants different transition behavior in these situations.

TransitionManagers can be declared in XML resource files inside the res/transition directory. TransitionManager resources consist of the transitionManagertag name, containing one or more transition tags, each of which describe the relationship of that transition to the from/to scene information in that tag. For example, here is a resource file that declares several scene transitions:

     <transitionManager xmlns:android="http://schemas.android.com/apk/res/android">
         <transition android:fromScene="@layout/transition_scene1"
                     android:toScene="@layout/transition_scene2"
                     android:transition="@transition/changebounds"/>
         <transition android:fromScene="@layout/transition_scene2"
                     android:toScene="@layout/transition_scene1"
                     android:transition="@transition/changebounds"/>
         <transition android:toScene="@layout/transition_scene3"
                     android:transition="@transition/changebounds_fadein_together"/>
         <transition android:fromScene="@layout/transition_scene3"
                     android:toScene="@layout/transition_scene1"
                     android:transition="@transition/changebounds_fadeout_sequential"/>
         <transition android:fromScene="@layout/transition_scene3"
                     android:toScene="@layout/transition_scene2"
                     android:transition="@transition/changebounds_fadeout_sequential"/>
     </transitionManager>
 

For each of the fromScene and toScene attributes, there is a reference to a standard XML layout file. This is equivalent to creating a scene from a layout in code by calling Scene.getSceneForLayout(ViewGroup, int, Context). For the transition attribute, there is a reference to a resource file in the res/transition directory which describes that transition.

Summary

Constructors
publicTransitionManager()

Methods
public static voidbeginDelayedTransition(ViewGroup sceneRoot)

Convenience method to animate, using the default transition, to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame.

public static voidbeginDelayedTransition(ViewGroup sceneRoot, Transition transition)

Convenience method to animate to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame.

public static TransitionSeekControllercontrolDelayedTransition(ViewGroup sceneRoot, Transition transition)

Create a TransitionSeekController to allow seeking an animation to a new scene defined by all changes within the given scene root between calling this method and the next rendered frame.

public static TransitionSeekControllercreateSeekController(Scene scene, Transition transition)

Convenience method to seek to the given scene using the given transition.

public static voidendTransitions(ViewGroup sceneRoot)

Ends all pending and ongoing transitions on the specified scene root.

public static voidgo(Scene scene)

Convenience method to simply change to the given scene using the default transition for TransitionManager.

public static voidgo(Scene scene, Transition transition)

Convenience method to simply change to the given scene using the given transition.

public voidsetTransition(Scene fromScene, Scene toScene, Transition transition)

Sets a specific transition to occur when the given pair of scenes is exited/entered.

public voidsetTransition(Scene scene, Transition transition)

Sets a specific transition to occur when the given scene is entered.

public voidtransitionTo(Scene scene)

Change to the given scene, using the appropriate transition for this particular scene change (as specified to the TransitionManager, or the default if no such transition exists).

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public TransitionManager()

Methods

public void setTransition(Scene scene, Transition transition)

Sets a specific transition to occur when the given scene is entered.

Parameters:

scene: The scene which, when applied, will cause the given transition to run.
transition: The transition that will play when the given scene is entered. A value of null will result in the default behavior of using the default transition instead.

public void setTransition(Scene fromScene, Scene toScene, Transition transition)

Sets a specific transition to occur when the given pair of scenes is exited/entered.

Parameters:

fromScene: The scene being exited when the given transition will be run
toScene: The scene being entered when the given transition will be run
transition: The transition that will play when the given scene is entered. A value of null will result in the default behavior of using the default transition instead.

public void transitionTo(Scene scene)

Change to the given scene, using the appropriate transition for this particular scene change (as specified to the TransitionManager, or the default if no such transition exists).

Parameters:

scene: The Scene to change to

public static void go(Scene scene)

Convenience method to simply change to the given scene using the default transition for TransitionManager.

Parameters:

scene: The Scene to change to

public static TransitionSeekController createSeekController(Scene scene, Transition transition)

Convenience method to seek to the given scene using the given transition. If seeking is not supported because the device is or earlier, the scene transition is immediate and null is returned.

Parameters:

scene: The Scene to change to
transition: The transition to use for this scene change.

Returns:

a TransitionSeekController that can be used control the animation to the destination scene. null is returned when seeking is not supported on the scene, either because it is running on or earlier, another Transition is being captured for sceneRoot, or sceneRoot hasn't had a layout yet.

public static void go(Scene scene, Transition transition)

Convenience method to simply change to the given scene using the given transition.

Passing in null for the transition parameter will result in the scene changing without any transition running, and is equivalent to calling Scene.exit() on the scene root's current scene, followed by Scene.enter() on the scene specified by the scene parameter.

Parameters:

scene: The Scene to change to
transition: The transition to use for this scene change. A value of null causes the scene change to happen with no transition.

public static void beginDelayedTransition(ViewGroup sceneRoot)

Convenience method to animate, using the default transition, to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame. Equivalent to calling TransitionManager.beginDelayedTransition(ViewGroup, Transition) with a value of null for the transition parameter.

Parameters:

sceneRoot: The root of the View hierarchy to run the transition on.

public static void beginDelayedTransition(ViewGroup sceneRoot, Transition transition)

Convenience method to animate to a new scene defined by all changes within the given scene root between calling this method and the next rendering frame. Calling this method causes TransitionManager to capture current values in the scene root and then post a request to run a transition on the next frame. At that time, the new values in the scene root will be captured and changes will be animated. There is no need to create a Scene; it is implied by changes which take place between calling this method and the next frame when the transition begins.

Calling this method several times before the next frame (for example, if unrelated code also wants to make dynamic changes and run a transition on the same scene root), only the first call will trigger capturing values and exiting the current scene. Subsequent calls to the method with the same scene root during the same frame will be ignored.

Passing in null for the transition parameter will cause the TransitionManager to use its default transition.

Parameters:

sceneRoot: The root of the View hierarchy to run the transition on.
transition: The transition to use for this change. A value of null causes the TransitionManager to use the default transition.

public static TransitionSeekController controlDelayedTransition(ViewGroup sceneRoot, Transition transition)

Create a TransitionSeekController to allow seeking an animation to a new scene defined by all changes within the given scene root between calling this method and the next rendered frame. Calling this method causes TransitionManager to capture current values in the scene root and then post a request to run a transition on the next frame. At that time, the new values in the scene root will be captured and changes will be animated. There is no need to create a Scene; it is implied by changes which take place between calling this method and the next frame when the transition begins.

Calling this method several times before the next frame (for example, if unrelated code also wants to make dynamic changes and run a transition on the same scene root), only the first call will trigger capturing values and exiting the current scene. Subsequent calls to the method with the same scene root during the same frame will be ignored.

Parameters:

sceneRoot: The root of the View hierarchy to run the transition on.
transition: The transition to use for this change.

Returns:

a TransitionSeekController that can be used control the animation to the destination scene. null is returned when seeking is not supported on the scene, either because it is running on or earlier, another Transition is being captured for sceneRoot, or sceneRoot hasn't had a layout yet.

public static void endTransitions(ViewGroup sceneRoot)

Ends all pending and ongoing transitions on the specified scene root.

Parameters:

sceneRoot: The root of the View hierarchy to end transitions on.

Source

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.transition;

import android.content.Context;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;

import java.lang.ref.WeakReference;
import java.util.ArrayList;

/**
 * This class manages the set of transitions that fire when there is a
 * change of {@link Scene}. To use the manager, add scenes along with
 * transition objects with calls to {@link #setTransition(Scene, Transition)}
 * or {@link #setTransition(Scene, Scene, Transition)}. Setting specific
 * transitions for scene changes is not required; by default, a Scene change
 * will use {@link AutoTransition} to do something reasonable for most
 * situations. Specifying other transitions for particular scene changes is
 * only necessary if the application wants different transition behavior
 * in these situations.
 *
 * <p>TransitionManagers can be declared in XML resource files inside the
 * <code>res/transition</code> directory. TransitionManager resources consist of
 * the <code>transitionManager</code>tag name, containing one or more
 * <code>transition</code> tags, each of which describe the relationship of
 * that transition to the from/to scene information in that tag.
 * For example, here is a resource file that declares several scene
 * transitions:</p>
 *
 * <pre>
 *     &lt;transitionManager xmlns:android="http://schemas.android.com/apk/res/android"&gt;
 *         &lt;transition android:fromScene="@layout/transition_scene1"
 *                     android:toScene="@layout/transition_scene2"
 *                     android:transition="@transition/changebounds"/&gt;
 *         &lt;transition android:fromScene="@layout/transition_scene2"
 *                     android:toScene="@layout/transition_scene1"
 *                     android:transition="@transition/changebounds"/&gt;
 *         &lt;transition android:toScene="@layout/transition_scene3"
 *                     android:transition="@transition/changebounds_fadein_together"/&gt;
 *         &lt;transition android:fromScene="@layout/transition_scene3"
 *                     android:toScene="@layout/transition_scene1"
 *                     android:transition="@transition/changebounds_fadeout_sequential"/&gt;
 *         &lt;transition android:fromScene="@layout/transition_scene3"
 *                     android:toScene="@layout/transition_scene2"
 *                     android:transition="@transition/changebounds_fadeout_sequential"/&gt;
 *     &lt;/transitionManager&gt;
 * </pre>
 *
 * <p>For each of the <code>fromScene</code> and <code>toScene</code> attributes,
 * there is a reference to a standard XML layout file. This is equivalent to
 * creating a scene from a layout in code by calling
 * {@link Scene#getSceneForLayout(ViewGroup, int, Context)}. For the
 * <code>transition</code> attribute, there is a reference to a resource
 * file in the <code>res/transition</code> directory which describes that
 * transition.</p>
 */
public class TransitionManager {

    private static final String LOG_TAG = "TransitionManager";

    private static Transition sDefaultTransition = new AutoTransition();

    private ArrayMap<Scene, Transition> mSceneTransitions = new ArrayMap<>();
    private ArrayMap<Scene, ArrayMap<Scene, Transition>> mScenePairTransitions = new ArrayMap<>();
    private static ThreadLocal<WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>>>
            sRunningTransitions = new ThreadLocal<>();
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static ArrayList<ViewGroup> sPendingTransitions = new ArrayList<>();

    /**
     * Sets a specific transition to occur when the given scene is entered.
     *
     * @param scene      The scene which, when applied, will cause the given
     *                   transition to run.
     * @param transition The transition that will play when the given scene is
     *                   entered. A value of null will result in the default behavior of
     *                   using the default transition instead.
     */
    public void setTransition(@NonNull Scene scene, @Nullable Transition transition) {
        mSceneTransitions.put(scene, transition);
    }

    /**
     * Sets a specific transition to occur when the given pair of scenes is
     * exited/entered.
     *
     * @param fromScene  The scene being exited when the given transition will
     *                   be run
     * @param toScene    The scene being entered when the given transition will
     *                   be run
     * @param transition The transition that will play when the given scene is
     *                   entered. A value of null will result in the default behavior of
     *                   using the default transition instead.
     */
    public void setTransition(@NonNull Scene fromScene, @NonNull Scene toScene,
            @Nullable Transition transition) {
        ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions.get(toScene);
        if (sceneTransitionMap == null) {
            sceneTransitionMap = new ArrayMap<>();
            mScenePairTransitions.put(toScene, sceneTransitionMap);
        }
        sceneTransitionMap.put(fromScene, transition);
    }

    /**
     * Returns the Transition for the given scene being entered. The result
     * depends not only on the given scene, but also the scene which the
     * {@link Scene#getSceneRoot() sceneRoot} of the Scene is currently in.
     *
     * @param scene The scene being entered
     * @return The Transition to be used for the given scene change. If no
     * Transition was specified for this scene change, the default transition
     * will be used instead.
     */
    private Transition getTransition(Scene scene) {
        Transition transition;
        ViewGroup sceneRoot = scene.getSceneRoot();
        // TODO: cached in Scene instead? long-term, cache in View itself
        Scene currScene = Scene.getCurrentScene(sceneRoot);
        if (currScene != null) {
            ArrayMap<Scene, Transition> sceneTransitionMap = mScenePairTransitions
                    .get(scene);
            if (sceneTransitionMap != null) {
                transition = sceneTransitionMap.get(currScene);
                if (transition != null) {
                    return transition;
                }
            }
        }
        transition = mSceneTransitions.get(scene);
        return (transition != null) ? transition : sDefaultTransition;
    }

    /**
     * This is where all of the work of a transition/scene-change is
     * orchestrated. This method captures the start values for the given
     * transition, exits the current Scene, enters the new scene, captures
     * the end values for the transition, and finally plays the
     * resulting values-populated transition.
     *
     * @param scene      The scene being entered
     * @param transition The transition to play for this scene change
     */
    private static void changeScene(Scene scene, Transition transition) {
        final ViewGroup sceneRoot = scene.getSceneRoot();

        if (!sPendingTransitions.contains(sceneRoot)) {
            Scene oldScene = Scene.getCurrentScene(sceneRoot);
            if (transition == null) {
                // Notify old scene that it is being exited
                if (oldScene != null) {
                    oldScene.exit();
                }

                scene.enter();
            } else {
                sPendingTransitions.add(sceneRoot);

                Transition transitionClone = transition.clone();

                if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
                    transitionClone.setCanRemoveViews(true);
                }

                sceneChangeSetup(sceneRoot, transitionClone);

                scene.enter();

                sceneChangeRunTransition(sceneRoot, transitionClone);
            }
        }
    }

    @VisibleForTesting
    static ArrayMap<ViewGroup, ArrayList<Transition>> getRunningTransitions() {
        WeakReference<ArrayMap<ViewGroup, ArrayList<Transition>>> runningTransitions =
                sRunningTransitions.get();
        if (runningTransitions != null) {
            ArrayMap<ViewGroup, ArrayList<Transition>> transitions = runningTransitions.get();
            if (transitions != null) {
                return transitions;
            }
        }
        ArrayMap<ViewGroup, ArrayList<Transition>> transitions = new ArrayMap<>();
        runningTransitions = new WeakReference<>(transitions);
        sRunningTransitions.set(runningTransitions);
        return transitions;
    }

    private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
            final Transition transition) {
        if (transition != null && sceneRoot != null) {
            MultiListener listener = new MultiListener(transition, sceneRoot);
            sceneRoot.addOnAttachStateChangeListener(listener);
            sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener);
        }
    }

    /**
     * This private utility class is used to listen for both OnPreDraw and
     * OnAttachStateChange events. OnPreDraw events are the main ones we care
     * about since that's what triggers the transition to take place.
     * OnAttachStateChange events are also important in case the view is removed
     * from the hierarchy before the OnPreDraw event takes place; it's used to
     * clean up things since the OnPreDraw listener didn't get called in time.
     */
    private static class MultiListener implements ViewTreeObserver.OnPreDrawListener,
            View.OnAttachStateChangeListener {

        Transition mTransition;

        ViewGroup mSceneRoot;

        MultiListener(Transition transition, ViewGroup sceneRoot) {
            mTransition = transition;
            mSceneRoot = sceneRoot;
        }

        private void removeListeners() {
            mSceneRoot.getViewTreeObserver().removeOnPreDrawListener(this);
            mSceneRoot.removeOnAttachStateChangeListener(this);
        }

        @Override
        public void onViewAttachedToWindow(View v) {
        }

        @Override
        public void onViewDetachedFromWindow(View v) {
            removeListeners();

            sPendingTransitions.remove(mSceneRoot);
            ArrayList<Transition> runningTransitions = getRunningTransitions().get(mSceneRoot);
            if (runningTransitions != null && runningTransitions.size() > 0) {
                for (Transition runningTransition : runningTransitions) {
                    runningTransition.resume(mSceneRoot);
                }
            }
            mTransition.clearValues(true);
        }

        @Override
        public boolean onPreDraw() {
            removeListeners();

            // Don't start the transition if it's no longer pending.
            if (!sPendingTransitions.remove(mSceneRoot)) {
                return true;
            }

            // Add to running list, handle end to remove it
            final ArrayMap<ViewGroup, ArrayList<Transition>> runningTransitions =
                    getRunningTransitions();
            ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
            ArrayList<Transition> previousRunningTransitions = null;
            if (currentTransitions == null) {
                currentTransitions = new ArrayList<>();
                runningTransitions.put(mSceneRoot, currentTransitions);
            } else if (currentTransitions.size() > 0) {
                previousRunningTransitions = new ArrayList<>(currentTransitions);
            }
            currentTransitions.add(mTransition);
            mTransition.addListener(new TransitionListenerAdapter() {
                @Override
                public void onTransitionEnd(@NonNull Transition transition) {
                    ArrayList<Transition> currentTransitions = runningTransitions.get(mSceneRoot);
                    currentTransitions.remove(transition);
                    transition.removeListener(this);
                }
            });
            mTransition.captureValues(mSceneRoot, false);
            if (previousRunningTransitions != null) {
                for (Transition runningTransition : previousRunningTransitions) {
                    runningTransition.resume(mSceneRoot);
                }
            }
            mTransition.playTransition(mSceneRoot);

            return true;
        }
    }

    private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {
        // Capture current values
        ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);

        if (runningTransitions != null && runningTransitions.size() > 0) {
            for (Transition runningTransition : runningTransitions) {
                runningTransition.pause(sceneRoot);
            }
        }

        if (transition != null) {
            transition.captureValues(sceneRoot, true);
        }

        // Notify previous scene that it is being exited
        Scene previousScene = Scene.getCurrentScene(sceneRoot);
        if (previousScene != null) {
            previousScene.exit();
        }
    }

    /**
     * Change to the given scene, using the
     * appropriate transition for this particular scene change
     * (as specified to the TransitionManager, or the default
     * if no such transition exists).
     *
     * @param scene The Scene to change to
     */
    public void transitionTo(@NonNull Scene scene) {
        // Auto transition if there is no transition declared for the Scene, but there is
        // a root or parent view
        changeScene(scene, getTransition(scene));
    }

    /**
     * Convenience method to simply change to the given scene using
     * the default transition for TransitionManager.
     *
     * @param scene The Scene to change to
     */
    public static void go(@NonNull Scene scene) {
        changeScene(scene, sDefaultTransition);
    }

    /**
     * Convenience method to seek to the given scene using the given transition. If seeking
     * is not supported because the device is {@link Build.VERSION_CODES.TIRAMISU} or earlier,
     * the scene transition is immediate and {@code null} is returned.
     *
     * @param scene      The Scene to change to
     * @param transition The transition to use for this scene change.
     * @return a {@link TransitionSeekController} that can be used control the animation to the
     * destination scene. {@code null} is returned when seeking is not supported on the scene,
     * either because it is running on {@link android.os.Build.VERSION_CODES.TIRAMISU} or earlier,
     * another Transition is being captured for {@code sceneRoot}, or {@code sceneRoot} hasn't
     * had a layout yet.
     * @throws IllegalArgumentException if {@code transition} returns {@code false} from
     *                                  {@link Transition#isSeekingSupported()}.
     */
    @Nullable
    public static TransitionSeekController createSeekController(
            @NonNull Scene scene,
            @NonNull Transition transition
    ) {
        final ViewGroup sceneRoot = scene.getSceneRoot();

        if (!transition.isSeekingSupported()) {
            throw new IllegalArgumentException("The Transition must support seeking.");
        }
        if (sPendingTransitions.contains(sceneRoot)) {
            return null; // Already in the process of transitioning
        }
        Scene oldScene = Scene.getCurrentScene(sceneRoot);
        if (!sceneRoot.isLaidOut()
                || Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE
        ) {
            // Can't control it, so just change the scene immediately
            if (oldScene != null) {
                oldScene.exit();
            }
            scene.enter();
            return null;
        }
        sPendingTransitions.add(sceneRoot);
        final Transition transitionClone = transition.clone();
        final TransitionSet set = new TransitionSet();
        set.addTransition(transitionClone);
        if (oldScene != null && oldScene.isCreatedFromLayoutResource()) {
            set.setCanRemoveViews(true);
        }
        sceneChangeSetup(sceneRoot, set);
        scene.enter();

        sceneChangeRunTransition(sceneRoot, set);
        return set.createSeekController();
    }

    /**
     * Convenience method to simply change to the given scene using
     * the given transition.
     *
     * <p>Passing in <code>null</code> for the transition parameter will
     * result in the scene changing without any transition running, and is
     * equivalent to calling {@link Scene#exit()} on the scene root's
     * current scene, followed by {@link Scene#enter()} on the scene
     * specified by the <code>scene</code> parameter.</p>
     *
     * @param scene      The Scene to change to
     * @param transition The transition to use for this scene change. A
     *                   value of null causes the scene change to happen with no transition.
     */
    public static void go(@NonNull Scene scene, @Nullable Transition transition) {
        changeScene(scene, transition);
    }

    /**
     * Convenience method to animate, using the default transition,
     * to a new scene defined by all changes within the given scene root between
     * calling this method and the next rendering frame.
     * Equivalent to calling {@link #beginDelayedTransition(ViewGroup, Transition)}
     * with a value of <code>null</code> for the <code>transition</code> parameter.
     *
     * @param sceneRoot The root of the View hierarchy to run the transition on.
     */
    public static void beginDelayedTransition(@NonNull final ViewGroup sceneRoot) {
        beginDelayedTransition(sceneRoot, null);
    }

    /**
     * Convenience method to animate to a new scene defined by all changes within
     * the given scene root between calling this method and the next rendering frame.
     * Calling this method causes TransitionManager to capture current values in the
     * scene root and then post a request to run a transition on the next frame.
     * At that time, the new values in the scene root will be captured and changes
     * will be animated. There is no need to create a Scene; it is implied by
     * changes which take place between calling this method and the next frame when
     * the transition begins.
     *
     * <p>Calling this method several times before the next frame (for example, if
     * unrelated code also wants to make dynamic changes and run a transition on
     * the same scene root), only the first call will trigger capturing values
     * and exiting the current scene. Subsequent calls to the method with the
     * same scene root during the same frame will be ignored.</p>
     *
     * <p>Passing in <code>null</code> for the transition parameter will
     * cause the TransitionManager to use its default transition.</p>
     *
     * @param sceneRoot  The root of the View hierarchy to run the transition on.
     * @param transition The transition to use for this change. A
     *                   value of null causes the TransitionManager to use the default transition.
     */
    public static void beginDelayedTransition(@NonNull final ViewGroup sceneRoot,
            @Nullable Transition transition) {
        if (!sPendingTransitions.contains(sceneRoot) && sceneRoot.isLaidOut()) {
            if (Transition.DBG) {
                Log.d(LOG_TAG, "beginDelayedTransition: root, transition = "
                        + sceneRoot + ", " + transition);
            }
            sPendingTransitions.add(sceneRoot);
            if (transition == null) {
                transition = sDefaultTransition;
            }
            final Transition transitionClone = transition.clone();
            sceneChangeSetup(sceneRoot, transitionClone);
            Scene.setCurrentScene(sceneRoot, null);
            sceneChangeRunTransition(sceneRoot, transitionClone);
        }
    }

    /**
     * Create a {@link TransitionSeekController} to allow seeking an animation to a new
     * scene defined by all changes within the given scene root between calling this method and
     * the next rendered frame. Calling this method causes TransitionManager to capture current
     * values in the scene root and then post a request to run a transition on the next frame.
     * At that time, the new values in the scene root will be captured and changes
     * will be animated. There is no need to create a Scene; it is implied by
     * changes which take place between calling this method and the next frame when
     * the transition begins.
     *
     * <p>Calling this method several times before the next frame (for example, if
     * unrelated code also wants to make dynamic changes and run a transition on
     * the same scene root), only the first call will trigger capturing values
     * and exiting the current scene. Subsequent calls to the method with the
     * same scene root during the same frame will be ignored.</p>
     *
     * @param sceneRoot  The root of the View hierarchy to run the transition on.
     * @param transition The transition to use for this change.
     * @return a {@link TransitionSeekController} that can be used control the animation to the
     * destination scene. {@code null} is returned when seeking is not supported on the scene,
     * either because it is running on {@link android.os.Build.VERSION_CODES#TIRAMISU} or earlier,
     * another Transition is being captured for {@code sceneRoot}, or {@code sceneRoot} hasn't
     * had a layout yet.
     * @throws IllegalArgumentException if {@code transition} returns {@code false} from
     *                                  {@link Transition#isSeekingSupported()}.
     */
    @Nullable
    public static TransitionSeekController controlDelayedTransition(
            @NonNull final ViewGroup sceneRoot,
            @NonNull Transition transition
    ) {
        if (sPendingTransitions.contains(sceneRoot) || !sceneRoot.isLaidOut()
                || Build.VERSION.SDK_INT < 34) {
            return null;
        }
        if (!transition.isSeekingSupported()) {
            throw new IllegalArgumentException("The Transition must support seeking.");
        }
        if (Transition.DBG) {
            Log.d(LOG_TAG, "controlDelayedTransition: root, transition = "
                    + sceneRoot + ", " + transition);
        }
        sPendingTransitions.add(sceneRoot);
        final Transition transitionClone = transition.clone();
        final TransitionSet set = new TransitionSet();
        set.addTransition(transitionClone);
        sceneChangeSetup(sceneRoot, set);
        Scene.setCurrentScene(sceneRoot, null);
        sceneChangeRunTransition(sceneRoot, set);
        sceneRoot.invalidate();
        return set.createSeekController();
    }

    /**
     * Ends all pending and ongoing transitions on the specified scene root.
     *
     * @param sceneRoot The root of the View hierarchy to end transitions on.
     */
    public static void endTransitions(final @Nullable ViewGroup sceneRoot) {
        sPendingTransitions.remove(sceneRoot);
        final ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);
        if (runningTransitions != null && !runningTransitions.isEmpty()) {
            // Make a copy in case this is called by an onTransitionEnd listener
            ArrayList<Transition> copy = new ArrayList<>(runningTransitions);
            for (int i = copy.size() - 1; i >= 0; i--) {
                final Transition transition = copy.get(i);
                transition.forceToEnd(sceneRoot);
            }
        }
    }
}