[TOC]

introduce

The origin of

With the continuous iterative optimization of the Android system, the dynamic function is more and more powerful. Android introduced Transition from API 19 to Transition between components and pages. This is the first time that Android has officially supported Transition between pages.

The core concept

Transition has two core concepts: Scenes and Transitions, where scenes are the current state of the UI and transitions define the transitions between scenes. So Transition does two things: it saves two states that start and end a scene, and it creates an animation between them. Because the scene records the start and end states of all views within the scene, the Transition animation is more consistent. Who does the animation? The TransitionManager is responsible for executing the animation.

Scene

Scene refers to a Scene, which contains a complete View structure, from the root layout to all the child views and their state information.

Transition

Transition is a transformation, going from one Scene to another, and each view can have a different Transition. The Transition transformation can be a change to any property of the view, such as transparency, scale, height, and so on. It is also internally supported by attribute changes.

Begin to use

The Transition framework is a very concise framework that contains the following apis:

Scene

The getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context Context) function is used to generate the Scene

Parameter Description:

SceneRoot: root ViewGroup

LayoutId: the layout resource ID of the view, representing a scene that is involved in the dynamic layout

-Leonard: The context?

Transition

Transition refers to the way the view changes between scenes. This works by internally recording a list of targets (the list of views to be transformed), and then recording specific information about each view before and after the transformation. During the animation period, this information is used to generate the animators for each view, and then execute the animators.

The Android library comes with some common transformations:

  • ChangeBounds: Detects the position boundaries of the view to create move and zoom animations
  • ChangeTransform: Checks the view’s scale and rotation to create a zoom and rotate animation
  • ChangeClipBounds: Checks the position bounds of the clipping region of the view, similar to ChangeBounds. But ChangeBounds are for the view and ChangeClipBounds are for the Rect of the view (setClipBound(Rect Rect)). No animation if not set
  • ChangeImageTransform: Checks the size, position, and ScaleType of the ImageView and creates the corresponding animation.
  • Fade,Slide,Explode: Perform Fade in,Slide, and Explode based on the view’s visibility state

After creating the Transition instance, we can specify which views the transformation effect will be applied to. By default, apply to all views with the same ID in the Scene before and after the transform. You can also specify two views with different ids by setting transitionName.

There are several ways to specify or exclude views in the Transition class:

  • AddTarget (int targetId) Adds the view with the specified ID to the transform
  • AddTarget (String targetName) Adds the specified transitionName view to the transform
  • AddTarget (Class targetType) adds a view of the specified Class type to the transform
  • AddTarget (View targetView) Adds the instance View to the transform
  • ExcludeTarget (String targetName, Boolean exclude) exclude the corresponding transitionName
  • ExcludeTarget (int targetId, Boolean exclude) excludes the view with the corresponding ID and does not perform the transform

In summary, the Transition class is an implementation of a transformation that internally animates properties and processes multiple views simultaneously.

TransitionManager

TransitionManager controls the execution of Transitions when the scene changes.

Scene and Transition transitions can be added through TransitionManager, but setting a default Transitions for scene transitions is not necessary, since AutoTransition is used by default.

TransitionManager How to start animation when the scene changes: beginDelayedTransition(ViewGroup sceneRoot, Transition transition) beginDelayedTransition(ViewGroup sceneRoot)

The view root sceneRoot and transition animations are passed to the scene as the scene changes. If Transition is not specified, the default is AutoTransition.

There are two ways to invoke a scene transform

  • go(Scene scene, Transition transition)
  • go(Scene scene)

The go method calls in to the scene, and the scene is generated by the scene using the View. If Transition is not specified, AutoTransition is default.

For example,

Effect of video

Let’s look at the effect to be achieved first:

www.bilibili.com/video/BV1Ep…

There are three elements in the interface, with a button at the bottom for switching scenes. The upper left corner is a Text, and the lower right corner is a rubik’s cube.

And when you press the Switch button, text and images switch places, which is perfect for this effect!

Code implementation

First, let’s analyze the layout structure of the page. I divide the page into two parts. At the top of the page is the scene container, which is used to carry the implementation of dynamic effects. The other part is the bottom button to activate the transform action.

Therefore, the layout code in the Fragment is:

<! -- fragment_first.xml-->

      
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".FirstFragment">

<! -- Scene container -->
    <FrameLayout
        android:padding="20dp"
        android:id="@+id/fl_scene_root"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:layout_constraintTop_toTopOf="parent" />

<! -- Start transform button -->
    <Button
        android:id="@+id/button_first"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="50dp"
        android:text="@string/change_scene"
        android:textColor="@color/black"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

Then, in the Fragment, we initialize the scene action and prepare for it.

In the onViewCreated method, we first find the scene container and the view of the toggle button, and then initialize the two scenes, scene1 and scene2. Notice that the sceneRoot in the constructor is passed to the scene container, which is FrameLayout we defined earlier.

After initialization, the go method of TransitionManager is called, and scenario 1 is entered by default.

// FirstFragment.kt 
override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)

// view.findViewById
// findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
/ /}
        
        changeSceneButton = view.findViewById(R.id.button_first)
        // Scene container
        sceneRoot = view.findViewById(R.id.fl_scene_root)
        
        changeSceneButton.setOnClickListener {
            changeScene()
        }

        // Change the scene before and after
        scene1 = Scene.getSceneForLayout(sceneRoot, R.layout.scene_1, context)
        scene2 = Scene.getSceneForLayout(sceneRoot, R.layout.scene_2, context)

        // Scenario 1 is displayed by default
        TransitionManager.go(scene1)
        isScene1 = true
    }
Copy the code

To instantiate the Scene, you pass in a Layout, which in this case means the layout of the page before and after the transform.

<! -- scene_1.xml-->

      
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<! -- Text in upper left -->
    <TextView
        android:id="@+id/text_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="I am the transiton text"
        android:textColor="@color/colorPrimary"
        android:textSize="18sp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

<! -- Rubik's Cube at bottom right
    <ImageView
        android:id="@+id/image_1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/rubik_cube"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code
<! -- scene_2.xml-->

      
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<! -- Text at bottom right -->
    <TextView
        android:id="@+id/text_1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:layout_marginBottom="50dp"
        android:text="I am the transiton text"
        android:textColor="@color/colorPrimary"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

<! -- Rubik's Cube on the upper left -->
    <ImageView
        android:id="@+id/image_1"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/rubik_cube"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

The above two XML sets the layout structure before and after the dynamic effect, which will be switched when the button is clicked.

Switch to the go method of TransitionManager, where toScene is the scene after the switch and changeBounds is the specific transformation mode during the switch.

// FirstFragment.kt
private fun changeScene(a) {
    val toScene = if (isScene1) scene2 elsescene1 isScene1 = ! isScene1val changeBounds = ChangeBounds()
    TransitionManager.go(toScene, changeBounds)
}
Copy the code

At this point, a simple scene switch dynamic effect can be achieved, very simple there is no!

Source code analysis

Based on the Android SDK 29 source code, let’s take a closer look at how the Transition framework works.

We will analyze the source code by creating the scene and changing the scene.

Create a scenario

A Scene, also known as a Scene, is typically created using the static method getSceneForLayout for the Scene:

// Scene.java
public static Scene getSceneForLayout(ViewGroup sceneRoot, int layoutId, Context context) {
	// Store the Scene information as 
      
        in SparseArray, via the View tag
      ,>
    SparseArray<Scene> scenes = (SparseArray<Scene>) sceneRoot.getTag(
            com.android.internal.R.id.scene_layoutid_cache);
    if (scenes == null) {
        scenes = new SparseArray<Scene>();
        sceneRoot.setTagInternal(com.android.internal.R.id.scene_layoutid_cache, scenes);
    }
	// Get the Scene for layoutId
    Scene scene = scenes.get(layoutId);
    if(scene ! =null) {
        return scene;
    } else {
		// The first call creates a new Scene instance and assigns values of sceneRoot, layoutId, and context to the instance variables
        scene = new Scene(sceneRoot, layoutId, context);
        scenes.put(layoutId, scene);
        returnscene; }}Copy the code

As you can see, the process of creating a scene is relatively simple, just instantiate it and store it in the sceneRoot tag. Therefore, a sceneRoot can bind multiple scenes, which makes perfect logical sense because the scene container is the view that hosts the scene and can also be understood as the parent layout of the scene.

Therefore, the root view corresponding to layoutId cannot have a parent layout, otherwise adding a scene in a scene change will be a problem.

Changing scene

From the previous use, we can see that the TransitionManager is responsible for scheduling and changing scenarios. There are two ways to change the scene:

// TransitionManager.java
private static Transition sDefaultTransition = new AutoTransition();

public static void go(Scene scene) {
    changeScene(scene, sDefaultTransition);
}

public static void go(Scene scene, Transition transition) {
    changeScene(scene, transition);
}
Copy the code

If Transition is not specified, the default AutoTransition is used. The changeScene method will eventually be called:

// TransitionManager.java
/**
 * 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();
	// Whether you are currently joined to sPendingTransitions
    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 {
			// From the go method, transition is not empty, add sceneRoot to sPendingTransitions
            sPendingTransitions.add(sceneRoot);

            Transition transitionClone = transition.clone();
            transitionClone.setSceneRoot(sceneRoot);

            if(oldScene ! =null && oldScene.isCreatedFromLayoutResource()) {
                transitionClone.setCanRemoveViews(true);
            }
			
			//captures the start values for the given transition, exits the current Scene
            sceneChangeSetup(sceneRoot, transitionClone);

			//enters the new scene, captures the end values for the transition
            scene.enter();


			//finally plays the resulting values-populated transitionsceneChangeRunTransition(sceneRoot, transitionClone); }}}Copy the code

The method execution link is explained more clearly in the comments. It mainly consists of three execution sub-processes: (1) capture the start information and exit the current scene; (2) Enter a new scene; (3) Capture end information and implement dynamic effect of scene transformation.

Because of the complexity and complexity of the content here, let’s break it down:

1) Capture the start information and exit the current scene

// TransitionManager.java
private static void sceneChangeSetup(ViewGroup sceneRoot, Transition transition) {

    // Capture current values
    ArrayList<Transition> runningTransitions = getRunningTransitions().get(sceneRoot);

	// Pause the current transformation action
    if(runningTransitions ! =null && runningTransitions.size() > 0) {
        for(Transition runningTransition : runningTransitions) { runningTransition.pause(sceneRoot); }}// Capture the view information needed for transition
    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(); }}Copy the code

The captureValues(ViewGroup, Boolean) method of the transform is used to capture the view information needed for the transform in the ViewGroup, where Boolean is true before the transform and false after the transform.

Before getting into the specifics of the captureValues method, it’s important to understand how the transformation stores view information.

// TransitionValues.java
public TransitionValues(@NonNull View view) {
    this.view = view;
}

/** * The View with these values */
public View view;

/** * The set of values tracked by transitions for this scene */
@NonNull
public final Map<String, Object> values = new ArrayMap<String, Object>();

/** * The Transitions that targeted this view. */
@NonNull
final ArrayList<Transition> targetedTransitions = new ArrayList<Transition>();
Copy the code

TransitionValues is the information class used to store the view. The view is bound internally, while the necessary state information (values) is stored through a map, and the transitions to be performed by the view are recorded in targetedTransitions.

// TransitionValuesMaps.java
class TransitionValuesMaps {
    ArrayMap<View, TransitionValues> viewValues =
            new ArrayMap<View, TransitionValues>();
    SparseArray<View> idValues = new SparseArray<View>();
    LongSparseArray<View> itemIdValues = new LongSparseArray<View>();
    ArrayMap<String, View> nameValues = new ArrayMap<String, View>();
}
Copy the code

The other class is TransitionValuesMaps. Internal viewValues store the view’s TransitionValues, and idValues store the <viewId, view> key-value pairs using a map. NameValues stores key-value pairs of <transitionName, view>, which are used to find views with specified transitionName.

Therefore, different Transtionnames should be used for the sub-views in the scene during transformation action, otherwise an exception will occur.

// Transition.java
private TransitionValuesMaps mStartValues = new TransitionValuesMaps();
private TransitionValuesMaps mEndValues = new TransitionValuesMaps();

/**
 * Recursive method that captures values for the given view and the
 * hierarchy underneath it.
 * @param sceneRoot The root of the view hierarchy being captured
 * @param start true if this capture is happening before the scene change,
 * false otherwise
 */
void captureValues(ViewGroup sceneRoot, boolean start) {
    clearValues(start);
    if ((mTargetIds.size() > 0 || mTargets.size() > 0)
            && (mTargetNames == null || mTargetNames.isEmpty())
            && (mTargetTypes == null || mTargetTypes.isEmpty())) {
        for (int i = 0; i < mTargetIds.size(); ++i) {
            int id = mTargetIds.get(i);
            View view = sceneRoot.findViewById(id);
            if(view ! =null) {
                TransitionValues values = new TransitionValues(view);
                if (start) {
                    captureStartValues(values);
                } else {
                    captureEndValues(values);
                }
                values.targetedTransitions.add(this);
                capturePropagationValues(values);
                if (start) {
                    addViewValues(mStartValues, view, values);
                } else{ addViewValues(mEndValues, view, values); }}}for (int i = 0; i < mTargets.size(); ++i) {
            View view = mTargets.get(i);
            TransitionValues values = new TransitionValues(view);
            if (start) {
                captureStartValues(values);
            } else {
                captureEndValues(values);
            }
            values.targetedTransitions.add(this);
            capturePropagationValues(values);
            if (start) {
                addViewValues(mStartValues, view, values);
            } else{ addViewValues(mEndValues, view, values); }}}else {
        captureHierarchy(sceneRoot, start);
    }
	//listView special handling. }Copy the code

CaptureValues is a bit more complicated, so let’s go through it step by step. It first clears Values, which is all information that starts or ends the view. Check if Transition has a value in the targetId, targetName, and targetType lists.

Transition can be transitionName, transitionName, or class type.

Since we didn’t specify it specifically in our instance, we go to the else branch and call the captureHierarchy method. The if branch does the same thing as the else branch, so we can analyze the else branch to see what it’s doing.

// Transition.java
private void captureHierarchy(View view, boolean start) {
    if (view == null) {
        return;
    }
    int id = view.getId();
    if(mTargetIdExcludes ! =null && mTargetIdExcludes.contains(id)) {
        return;
    }
    if(mTargetExcludes ! =null && mTargetExcludes.contains(view)) {
        return;
    }
    if(mTargetTypeExcludes ! =null&& view ! =null) {
        int numTypes = mTargetTypeExcludes.size();
        for (int i = 0; i < numTypes; ++i) {
            if (mTargetTypeExcludes.get(i).isInstance(view)) {
                return; }}}if (view.getParent() instanceof ViewGroup) {
        TransitionValues values = new TransitionValues(view);
        if (start) {
            captureStartValues(values);
        } else {
            captureEndValues(values);
        }
        values.targetedTransitions.add(this);
        capturePropagationValues(values);
        if (start) {
            addViewValues(mStartValues, view, values);
        } else{ addViewValues(mEndValues, view, values); }}if (view instanceof ViewGroup) {
        // Don't traverse child hierarchy if there are any child-excludes on this view
        if(mTargetIdChildExcludes ! =null && mTargetIdChildExcludes.contains(id)) {
            return;
        }
        if(mTargetChildExcludes ! =null && mTargetChildExcludes.contains(view)) {
            return;
        }
        if(mTargetTypeChildExcludes ! =null) {
            int numTypes = mTargetTypeChildExcludes.size();
            for (int i = 0; i < numTypes; ++i) {
                if (mTargetTypeChildExcludes.get(i).isInstance(view)) {
                    return;
                }
            }
        }
        ViewGroup parent = (ViewGroup) view;
        for (int i = 0; i < parent.getChildCount(); ++i) { captureHierarchy(parent.getChildAt(i), start); }}}Copy the code

This method is a recursive method that traverses the view tree recursively.

Exclude is not included in the list. If it is not specified, it can be ignored.

The captureStartValues and captureEndValues methods are then called, depending on whether start is true or false. Both methods are abstract methods implemented by concrete subclasses. In general, methods record the initial and final state information of the view, and then use property animation to realize the specific action of a single view.

Let’s use the Slide transformation as an example:

// Slide.java
private void captureValues(TransitionValues transitionValues) {
    View view = transitionValues.view;
    int[] position = new int[2];
    view.getLocationOnScreen(position);
    transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
}

@Override
public void captureStartValues(TransitionValues transitionValues) {
    super.captureStartValues(transitionValues);
    captureValues(transitionValues);
}

@Override
public void captureEndValues(TransitionValues transitionValues) {
    super.captureEndValues(transitionValues);
    captureValues(transitionValues);
}
Copy the code

Internally, the captureValues method is called uniformly, which records the position information of the view on the screen and is used to slide the displacement of dynamic effects.

Back inside the captureHierarchy method, there is one more important point:

// Transition.java
static void addViewValues(TransitionValuesMaps transitionValuesMaps, View view, TransitionValues transitionValues) {
    transitionValuesMaps.viewValues.put(view, transitionValues);
    int id = view.getId();
    if (id >= 0) {
        if (transitionValuesMaps.idValues.indexOfKey(id) >= 0) {
            // Duplicate IDs cannot match by ID.
            transitionValuesMaps.idValues.put(id, null);
        } else {
            transitionValuesMaps.idValues.put(id, view);
        }
    }
    String name = view.getTransitionName();
    if(name ! =null) {
        if (transitionValuesMaps.nameValues.containsKey(name)) {
            // Duplicate transitionNames: cannot match by transitionName.
            transitionValuesMaps.nameValues.put(name, null);
        } else{ transitionValuesMaps.nameValues.put(name, view); }}//ListView special processing, not needed yet. }Copy the code

The addViewValues method stores the recorded information to idValues and nameValues of transitionValuesMaps. Note the special handling of the same ID or transitionName.

Now that all information about the sceneRoot view tree has been analyzed, let’s go back to the way we started the analysis:

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

	// Pause the current transformation action
    if(runningTransitions ! =null && runningTransitions.size() > 0) {
        for(Transition runningTransition : runningTransitions) { runningTransition.pause(sceneRoot); }}// Capture the view information needed for transition
    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(); }}Copy the code

We have finished recording the initial information, and the last piece of code in the method body is to exit the currently displayed scene.

2) Enter a new scene

// Scene.java
public void enter(a) {
    // Apply layout change, if any
    if (mLayoutId > 0|| mLayout ! =null) {
        // empty out parent container before adding to it
        getSceneRoot().removeAllViews();

        if (mLayoutId > 0) {
            LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
        } else{ mSceneRoot.addView(mLayout); }}// Notify next scene that it is entering. Subclasses may override to configure scene.
    if(mEnterAction ! =null) {
        mEnterAction.run();
    }

    setCurrentScene(mSceneRoot, this);
}
Copy the code

The Scene Enter method is used to enter the Scene container. In this case, the layout or view passed in to create the Scene is added to the Scene container (mSceneRoot). Then sweeten the current scene into a “SceneRoot” tag.

3) Capture end information and implement transformation dynamic effect

// TransitionManager.java
private static void sceneChangeRunTransition(final ViewGroup sceneRoot,
        final Transition transition) {
    if(transition ! =null&& sceneRoot ! =null) {
        MultiListener listener = newMultiListener(transition, sceneRoot); sceneRoot.addOnAttachStateChangeListener(listener); sceneRoot.getViewTreeObserver().addOnPreDrawListener(listener); }}Copy the code

A redrawing of the view is triggered when a new scene is entered. Transformation execution is actually implemented through MultiListener, which listens for the attCH state and onPreDraw state of the scene container.

// TransitionManager.java
/** * 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;
    final ViewTreeObserver mViewTreeObserver;

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

    private void removeListeners(a) {
        if (mViewTreeObserver.isAlive()) {
            mViewTreeObserver.removeOnPreDrawListener(this);
        } else {
            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(a) {
        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<Transition>();
            runningTransitions.put(mSceneRoot, currentTransitions);
        } else if (currentTransitions.size() > 0) {
            previousRunningTransitions = new ArrayList<Transition>(currentTransitions);
        }
        currentTransitions.add(mTransition);
        mTransition.addListener(new TransitionListenerAdapter() {
            @Override
            public void onTransitionEnd(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; }};Copy the code

When a MultiListener is created, the transformation and the scene container are passed in. Let’s focus here on how the onPreDraw method actually generates an action effect and executes it.

Start by determining that sPendingTransitions contain the current transitions to be executed, or exit. This has been added before, so let’s go to the next code.

GetRunningTransitions gets the ArrayList of currently running transitions, while adding the currently running transitions to currentTransitions, setting the transition end callback, and removing the finished transitions from currentTransitions.

Then, we come across the familiar captureValues method, which records the state information of the view after the scene container has switched the scene, that is, the terminated state information (the transformed scene view). Resume previously paused animation.

And then finally, the final dynamic execution, we do the Transition playTransition.

// Transition.java
/** * Called by TransitionManager to play the transition. This calls * createAnimators() to set things up and create all  of the animations and then * runAnimations() to actually start the animations. */
void playTransition(ViewGroup sceneRoot) {
    mStartValuesList = new ArrayList<TransitionValues>();
    mEndValuesList = new ArrayList<TransitionValues>();
    matchStartAndEnd(mStartValues, mEndValues);

    ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
    int numOldAnims = runningAnimators.size();
    WindowId windowId = sceneRoot.getWindowId();
    for (int i = numOldAnims - 1; i >= 0; i--) {
        Animator anim = runningAnimators.keyAt(i);
        if(anim ! =null) {
            AnimationInfo oldInfo = runningAnimators.get(anim);
            if(oldInfo ! =null&& oldInfo.view ! =null && oldInfo.windowId == windowId) {
                TransitionValues oldValues = oldInfo.values;
                View oldView = oldInfo.view;
                TransitionValues startValues = getTransitionValues(oldView, true);
                TransitionValues endValues = getMatchedTransitionValues(oldView, true);
                if (startValues == null && endValues == null) {
                    endValues = mEndValues.viewValues.get(oldView);
                }
                booleancancel = (startValues ! =null|| endValues ! =null) &&
                        oldInfo.transition.isTransitionRequired(oldValues, endValues);
                if (cancel) {
                    if (anim.isRunning() || anim.isStarted()) {
                        if (DBG) {
                            Log.d(LOG_TAG, "Canceling anim " + anim);
                        }
                        anim.cancel();
                    } else {
                        if (DBG) {
                            Log.d(LOG_TAG, "removing anim from info list: " + anim);
                        }
                        runningAnimators.remove(anim);
                    }
                }
            }
        }
    }

    createAnimators(sceneRoot, mStartValues, mEndValues, mStartValuesList, mEndValuesList);
    runAnimators();
}
Copy the code

I’m going to do some matching to the view initially, but it’s a little bit more complicated here, so I’ll leave it at that.

GetRunningAnimators get the currently running property animation and cancel it. Then call the createAnimators method:

// Transition.java
protected void createAnimators(ViewGroup sceneRoot, TransitionValuesMaps startValues, TransitionValuesMaps endValues, ArrayList
       
         startValuesList, ArrayList
        
          endValuesList)
        
        {
    if (DBG) {
        Log.d(LOG_TAG, "createAnimators() for " + this);
    }
    ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
    long minStartDelay = Long.MAX_VALUE;
    int minAnimator = mAnimators.size();
    SparseLongArray startDelays = new SparseLongArray();
    int startValuesListCount = startValuesList.size();
    for (int i = 0; i < startValuesListCount; ++i) {
        TransitionValues start = startValuesList.get(i);
        TransitionValues end = endValuesList.get(i);
        if(start ! =null && !start.targetedTransitions.contains(this)) {
            start = null;
        }
        if(end ! =null && !end.targetedTransitions.contains(this)) {
            end = null;
        }
        if (start == null && end == null) {
            continue;
        }
        // Only bother trying to animate with values that differ between start/end
        boolean isChanged = start == null || end == null || isTransitionRequired(start, end);
        if (isChanged) {
            if(DBG) { View view = (end ! =null)? end.view : start.view; Log.d(LOG_TAG," differing start/end values for view " + view);
                if (start == null || end == null) {
                    Log.d(LOG_TAG, "" + ((start == null)?"start null, end non-null" : "start non-null, end null"));
                } else {
                    for (String key : start.values.keySet()) {
                        Object startValue = start.values.get(key);
                        Object endValue = end.values.get(key);
                        if(startValue ! = endValue && ! startValue.equals(endValue)) { Log.d(LOG_TAG,"" + key + ": start(" + startValue +
                                    "), end(" + endValue + ")"); }}}}// TODO: what to do about targetIds and itemIds?
            Animator animator = createAnimator(sceneRoot, start, end);
            if(animator ! =null) {
                // Save animation info for future cancellation purposes
                View view = null;
                TransitionValues infoValues = null;
                if(end ! =null) {
                    view = end.view;
                    String[] properties = getTransitionProperties();
                    if(properties ! =null && properties.length > 0) {
                        infoValues = new TransitionValues(view);
                        TransitionValues newValues = endValues.viewValues.get(view);
                        if(newValues ! =null) {
                            for (int j = 0; j < properties.length; ++j) { infoValues.values.put(properties[j], newValues.values.get(properties[j])); }}int numExistingAnims = runningAnimators.size();
                        for (int j = 0; j < numExistingAnims; ++j) {
                            Animator anim = runningAnimators.keyAt(j);
                            AnimationInfo info = runningAnimators.get(anim);
                            if(info.values ! =null && info.view == view &&
                                    ((info.name == null && getName() == null) ||
                                            info.name.equals(getName()))) {
                                if (info.values.equals(infoValues)) {
                                    // Favor the old animator
                                    animator = null;
                                    break;
                                }
                            }
                        }
                    }
                } else{ view = (start ! =null)? start.view :null;
                }
                if(animator ! =null) {
                    if(mPropagation ! =null) {
                        long delay = mPropagation
                                .getStartDelay(sceneRoot, this, start, end);
                        startDelays.put(mAnimators.size(), delay);
                        minStartDelay = Math.min(delay, minStartDelay);
                    }
                    AnimationInfo info = new AnimationInfo(view, getName(), this, sceneRoot.getWindowId(), infoValues); runningAnimators.put(animator, info); mAnimators.add(animator); }}}}if(startDelays.size() ! =0) {
        for (int i = 0; i < startDelays.size(); i++) {
            int index = startDelays.keyAt(i);
            Animator animator = mAnimators.get(index);
            longdelay = startDelays.valueAt(i) - minStartDelay + animator.getStartDelay(); animator.setStartDelay(delay); }}Copy the code

This method does some encapsulation to the subclass to create attribute animation, first judge whether the initial and termination state information is inconsistent, there is a change in the implementation of transformation, through the abstract method to let the subclass to achieve the creation of attribute animation details.

Add property animations to sRunningAnimators and mAnimators after they are created and set execution delays.

The final step is to animate these properties through runAnimators () :

// Transition.java
/**
 * This is called internally once all animations have been set up by the
 * transition hierarchy.
 *
 * @hide* /
protected void runAnimators(a) {
    if (DBG) {
        Log.d(LOG_TAG, "runAnimators() on " + this);
    }
    start();
    ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators();
    // Now start every Animator that was previously created for this transition
    for (Animator anim : mAnimators) {
        if (DBG) {
            Log.d(LOG_TAG, " anim: " + anim);
        }
        if (runningAnimators.containsKey(anim)) {
            start();
            runAnimator(anim, runningAnimators);
        }
    }
    mAnimators.clear();
    end();
}
Copy the code

Walk through the mAnimators and perform these property animations.

Finally, the transformation of the scene through ** [overlay different transformation in the scene], [overlay different attribute animation in the transformation] ** to achieve perfect.

Advanced – Custom Transition

After the above analysis, we have understood the principle and implementation process of the Transition framework. However, it is also found that for a particular type of view, the specific transformation method can be completely customized by the developer.

Transformation frameworks also provide this extensibility, primarily through custom Transition. The Android SDK already provides some common implementations:

Specific usage on their own Baidu or look at the source can be.

There are three abstract methods or public methods that you usually implement when you customize Transition:

Public abstract void captureStartValues(TransitionValues);

Capture the information of the view before the transform, such as alpha, width, height, and so on, according to the custom transform scene

Public abstract void captureEndValues(TransitionValues TransitionValues);

Capture information about the back view of the transform, such as alpha, width, height, and so on, according to the custom transform scene

Third, the public Animator createAnimator (ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues);

According to the customized transformation scene and the view information before and after the transformation, to create the final property animation to be executed

Next, we’ll take a look at two custom transitions that appear in the DiDi Food client and in the Conductor library.

Custom font transformation analysis

The DiDi Food client involves the sharing of multiple elements in the dynamic effect of home page entry. We will explain a relatively simple child effect: the scene transformation process of changing the merchant name on the card from a small size to a large size.

As usual, let’s start with the effects video:

www.bilibili.com/video/BV1F5…

Look closely and notice how the size of the name on the business card goes from small to large.

// TextSizeTransition.java
@TargetApi(VERSION_CODES.KITKAT)
public class TextSizeTransition extends Transition {

    private static final String PROPNAME_TEXT_SIZE = "sodaglobaldidifood:transition:textsize";
    private static final String[] TRANSITION_PROPERTIES = {PROPNAME_TEXT_SIZE};

    private static final Property<TextView, Float> TEXT_SIZE_PROPERTY = AnimationProperty.TEXT_SIZE;

    public TextSizeTransition(a) {}@Override
    public void captureEndValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        captureValues(transitionValues);
    }

    @Override
    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) {
        if (startValues == null || endValues == null) {
            return null;
        }

        Float startSize = (Float) startValues.values.get(PROPNAME_TEXT_SIZE);
        Float endSize = (Float) endValues.values.get(PROPNAME_TEXT_SIZE);
        if (startSize == null || endSize == null ||
            startSize.floatValue() == endSize.floatValue()) {
            return null;
        }

        TextView view = (TextView) endValues.view;
        view.setTextSize(TypedValue.COMPLEX_UNIT_PX, startSize);
        return ObjectAnimator.ofFloat(view, TEXT_SIZE_PROPERTY, startSize, endSize);
    }

    @Override
    public String[] getTransitionProperties() {
        return TRANSITION_PROPERTIES;
    }

    private void captureValues(TransitionValues transitionValues) {
        if (transitionValues.view instanceofTextView) { TextView textView = (TextView) transitionValues.view; transitionValues.values.put(PROPNAME_TEXT_SIZE, textView.getTextSize()); }}}Copy the code

In this custom size transform, we capture the TextSize information of the view before and after the transform, and the property animation of the size change is returned in createAnimator.

Don’t you think it’s easy!! Basically, property values defined in a view such as alpha, scale, translateY, and so on can be handled this way.

Conductor custom Fab transform analysis

Conductor libraries are very focused on visual transitions, and one of the most interesting transitions is the transition effect. Click on FloatingActionButton to pop up a Dialog that transforms the scene using the custom Transition.

www.bilibili.com/video/BV1nX…

Due to space and time, there is no way to analyze it here. Interested students can climb down the source code to have a look.

Refer to the content

1, Android transition animation learning