Reading Instructions:

  • Does this article assume that the reader already knows how to use itConstraintLayout.
  • This article is an articleMotionLayoutBasic tutorial, if you already know how to useMotionLayout, this article may not help you much.
  • This is the first of two articles in this tutorial, and the other is here.
  • It is recommended that readers follow this article to operate together, if you are not convenient now, it is recommended to read later.
  • This article is based onConstraintLayout 2.0.0 - alpha4Version of the preparation, it is recommended that readers use this version first.
  • Due to theMotionLayoutThe official document is not complete, some knowledge points are summarized according to the author’s own understanding, if there is any mistake, welcome to correct.

Add support libraries:

dependencies{... implementation'androidx. Constraintlayout: constraintlayout: 2.0.0 - alpha4'
}
Copy the code

MotionLayout is supported as low as Android 4.3(API 18). MotionLayout is ConstraintLayout 2.0, so make sure the support library is at least 2.0.

Introduction to the

The MotionLayout class inherits from the ConstraintLayout class and allows you to animate transitions between layouts of various states. Because MotionLayout inherits ConstraintLayout, you can replace ConstraintLayout directly with MotionLayout in an XML layout file.

MotionLayout is fully declarative, and you can describe a complex transition animation entirely in an XML file without any code. (If you plan to create transitions using code, it is recommended that you use property animations instead of MotionLayout.)

Begin to use

Because the MotionLayout class inherits from the ConstraintLayout class, you can replace ConstraintLayout with MotionLayout in your layout.

MotionLayout differs from ConstraintLayout in that MotionLayout needs to be linked to a MotionScene file. Link MotionLayout to a MotionScene file using the App :layoutDescription property of MotionLayout.

Ex. :

<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.motion.widget.MotionLayout 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"
    
    app:layoutDescription="@xml/scene_01">
    
    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

Attention! You must set an Id for all direct child views of the MotionLayout (you are not allowed to set an Id for indirect child views).

Create the MotionScene file

The MotionScene file, which describes the transition animation between two scenes, is stored in the RES/XML directory.

To create transition animations using MotionLayout, you need to create two Layout files that describe the properties of two different scenes. When switching from one scene to another, the MotionLayout framework automatically detects the difference in properties between views with the same ID in the two scenes, and then applies a transition animation (similar to TransitionManger) to those properties.

MotionLayoutStandard attributes supported by the framework:

  • android:visibility
  • android:alpha
  • android:elevation
  • android:rotation
  • android:rotationX
  • android:rotationY
  • android:scaleX
  • android:scaleY
  • android:translationX
  • android:translationY
  • android:translationZ

MationLayout supports all ConstraintLayout attributes in addition to the standard attributes listed above.

Let’s look at a complete example, which is divided into the following3Step.

The first1Step: Create a scenario1Layout file:

File name: activity_main_scene1.xml

<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.motion.widget.MotionLayout 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"
    android:id="@+id/motionLayout"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

The layout preview for Scenario 1 is shown below:

The first2Step: Create a scenario2Layout file:

File name: activity_main_scene2.xml

<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

The layout preview for Scenario 2 is shown below:

Note: Both scene 1 and Scene 2 have an ImageView whose ID is image. The difference is that the image in scene 1 is centered horizontally and vertically, while the image in scene 2 is centered horizontally and aligned vertically to the top of the parent layout. So when you switch from scene 1 to scene 2, MotionLayout automatically applies a shift transition animation to the image’s position difference.

The first3Step: createMotionSceneFile:

The file name is activity_main_motion_scene. XML and stored in the RES/XML directory

<?xml version="1.0" encoding="utf-8"? >
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetStart="@layout/activity_main_scene1"
        app:constraintSetEnd="@layout/activity_main_scene2"
        app:duration="1000">

        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/image" />

    </Transition>

</MotionScene>
Copy the code

After writing the MotionLayout file, you can run the program directly. Click image to switch scenes. When switching scenes, MotionLayout automatically calculates the difference between the two scenes and applies the appropriate transition animation.

In the face of theMotionLayoutDescription of the document:

As shown in the example above, the root element of the MotionScene file is

. Use the
child element in the

element to describe a Transition, specify the layout file for the starting scene using the
element’s app:constraintSetStart attribute, Use app:constraintSetEnd to specify the layout file that ends the scene. Use the

or

child element in the
element to describe the triggering condition of the Transition.



<Transition>Element attributes:

  • app:constraintSetStart: set as the layout file for the starting sceneId.
  • app:constraintSetEnd: Layout file that is set to end the sceneId.
  • App :duration :duration of transition animation.
  • app:motionInterpolator: Interpolator for transition animation. There are the following6Four optional values:
    • linear: linear
    • easeIn: slowly into
    • easeOut: slow out
    • easeInOut: Slow in and slow out
    • bounceSpring:
    • anticipate(Function unknown, document not found)
  • App: Staggered: floating point (functionality unknown, documentation not found)

Can be found in<Transition>Element<OnClick>or<OnSwipe>Child element to describe the triggering conditions of the transition.

<OnClick>Element attributes:

  • app:targetId: 【idValue 】 sets the one used to trigger the transitionViewId(such as:@id/image@+id/image).

Tip: the app:targetId value can be prefixed with either @+id/ or @id/, or both. The official example uses @+id/. However, using @id/ prefix seems more semantically sound, because @+ ID/prefix is often used to create a new ID in a layout, and @id/ prefix is often used to reference other ID values. To highlight the fact that we are referencing another Id rather than creating a new one, it is more semantically appropriate to use the @id/ prefix.

  • app:clickAction: Sets the action to perform when clicked. This property has the following properties5Three optional values:
    • toggleIn:StartScenarios andEndCyclic switching between scenes.
    • transitionToEnd: the transition toEndScenario.
    • transitionToStart: the transition toStartScenario.
    • jumpToEnd: to jumpEndScene (do not perform transition animation).
    • jumpToStart: to jumpStartScene (do not perform transition animation).

<OnSwipe>Element attributes:

  • app:touchAnchorId: 【idValue sets the object to which the drag operation is associated so that the touch appears to be dragging the object’s causeapp:touchAnchorSideProperty specifies the edge.
  • app:touchAnchorSide: sets which side of the object the touch will drag4Four optional values:
    • top
    • left
    • right
    • bottom
  • app:dragDirection: Set the drag directionapp:touchAnchorIdProperty to be valid. There are the following4Four optional values:
    • dragUp: Drag your finger from bottom up (↑).
    • dragDown: Drag your finger down (↓).
    • dragLeft: Drag your finger from right to left (←).
    • dragRight: Drag your finger from left to right (→).
  • app:maxVelocity: [floating point] Sets the maximum speed (pixels per second) at which the animation can be draggedpx/s).
  • app:maxAcceleration: [floating point value] Sets the maximum acceleration of the animation while dragging (in pixels per second)px/s^2).

You can set

and

at the same time, or you can set neither and instead use code to trigger transitions.

You can also set multiple

in the
element, and each

can be associated with a different control. Although more than one

can be set in the
element, the latter

replaces the previous

and the last

is used.





<OnSwipe>Drag operation

Because of the complex interaction involved in the

drag operation, the following three attributes are explained separately:

  • app:touchAnchorId
  • app:dragDirection
  • app:touchAnchorSide

The first isapp:touchAnchorIdProperties andapp:dragDirectionProperties.app:touchAnchorIdProperty to set the object to which the drag operation is associated;app:dragDirectionProperty to specify the drag direction.

By default,On the downThe transition animation runs when you drag<OnSwipe/>The element does not need to set any attributes, as long as the<Transition>To add a<OnSwipe/>Labels are ok.

Ex. :

<Transition
    .>

    <OnSwipe/>

</Transition>
Copy the code

But if you want to supportFrom down to up(↑) orFrom left to right(→) orFrom right to left(←), then at least it should be set wellapp:touchAnchorIdapp:dragDirectionProperties.

The dragDirection of the app:dragDirection property is related to the position of the object associated with the app:touchAnchorId property in the Start and End scenes. For example, in figure A below, if the Widget in the End scenario is above the Widget in the Start scenario, you should set app:dragDirection=”dragUp”. In Figure B, the Widget in the End scenario is to the right of the Widget in the Start scenario, so you should set app:dragDirection=”dragRight” :

If the Widget in the End scenario is slanted relative to the Widget in the Start scenario (as shown in the image below), there are two directions to choose from. The options in the image below are dragUp and dragRight, which direction to use is up to you.

Setting up the right drag direction is very important, otherwise the transition animations will not perform well when dragging.

Tip: MotionLayout will use the progress of the app:dragDirection associated with the touchAnchorId object as the progress of the entire transition animation. When the drag of the associated object in the app:dragDirection direction is complete, it means that the entire transition animation is complete.

Example: to achieve the drag effect

Remove the

tag from the
element and add a

tag. The modified MotionScene looks like this:

<?xml version="1.0" encoding="utf-8"? >
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetStart="@layout/activity_main_scene1"
        app:constraintSetEnd="@layout/activity_main_scene2"
        app:duration="1000">

        <! Delete OnClick and add OnSwipe -->
        
        <OnSwipe
            app:touchAnchorId="@id/image"
            app:dragDirection="dragUp"/>

    </Transition>

</MotionScene>
Copy the code

Note: If you associate

and

to the same control, or if the control to which

is associated is clickable, the click event will affect the drag, and you will not be able to drag by holding the control, you will have to hold the outside of the control to drag.


Ex. :

Add

to the
tag and the modified MotionScene file will look like this:

<?xml version="1.0" encoding="utf-8"? >
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetStart="@layout/activity_main_scene1"
        app:constraintSetEnd="@layout/activity_main_scene2"
        app:duration="1000">

        <OnSwipe
            app:touchAnchorId="@id/image"
            app:dragDirection="dragUp"/>

        <! -- add OnClick -->
        <OnClick
            app:targetId="@id/image"
            app:clickAction="toggle"/>

    </Transition>

</MotionScene>
Copy the code

The effect is as follows:

app:touchAnchorSideProperties:

app:touchAnchorSideProperty to “set which side of the object will be dragged by a touch” and can be used for collapsible effects, such as collapsible title bars.

Example: Achieve a pull-up fold at the bottom.

1. Modify the acticity_main_scene1. XML file.

<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.motion.widget.MotionLayout 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"
    android:id="@+id/motionLayout"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <! Add the following code -->
    <FrameLayout
        android:id="@+id/bottomBar"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:background="@color/colorPrimary"
        app:layout_constraintTop_toBottomOf="parent"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageView
            android:layout_gravity="center"
            android:src="@mipmap/ic_launcher"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </FrameLayout>

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

2. Modify the acticity_main_scene2. XML file.

<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <! Add the following code -->
    <FrameLayout
        android:id="@+id/bottomBar"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:background="@color/colorPrimary"
        app:layout_constraintBottom_toBottomOf="parent">

        <ImageView
            android:layout_gravity="center"
            android:src="@mipmap/ic_launcher"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </FrameLayout>

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

3. Modify the MotionScene file (activity_main_motion_scene.xml).

<?xml version="1.0" encoding="utf-8"? >
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetStart="@layout/activity_main_scene1"
        app:constraintSetEnd="@layout/activity_main_scene2"
        app:duration="1000">

        <! Correlating to bottomBar
        <OnSwipe
            app:touchAnchorId="@id/bottomBar"
            app:touchAnchorSide="top"
            app:dragDirection="dragUp"/>

        <OnClick
            app:targetId="@id/image"
            app:clickAction="toggle"/>

    </Transition>

</MotionScene>
Copy the code

The effect is as follows:

Note:

can not be associated to bottomBar because in the previous example we have associated

to the image and we have drageUp correctly, so we can actually drag normally. But since bottomBar is collapsible, it would be more appropriate to drag

to associate it with your app:touchAnchorSide=”top”, which tells the MotionLayout control that the bottomBar’s upper boundary is collapsible, This is more semantic.


Use code to trigger transition animations

In addition to using the

element and the

element to set the trigger conditions that trigger the transition animation, you can also use code to trigger the transition animation manually.

Modify the layout file of scenario 1 by adding 2 buttons to the layout, and preview it as shown below:

scenario1The modified layout file contains the following contents:

<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnToStartScene"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="To Start Scene"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@id/btnToEndScene" />

    <Button
        android:id="@+id/btnToEndScene"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:text="To End Scene"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@id/btnToStartScene"
        app:layout_constraintRight_toRightOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

The layout file for Scenario 2 does not need to be modified.

Add the following code to MainActivity to perform the transition animation manually:

public class MainActivity extends AppCompatActivity {
    private MotionLayout mMotionLayout;
    private Button btnToStartScene;
    private Button btnToEndScene;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_scene1);

        mMotionLayout = findViewById(R.id.motionLayout);
        btnToStartScene = findViewById(R.id.btnToStartScene);
        btnToEndScene = findViewById(R.id.btnToEndScene);

        btnToStartScene.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Switch to the Start scenariomMotionLayout.transitionToStart(); }}); btnToEndScene.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Switch to the End scenariomMotionLayout.transitionToEnd(); }}); }}Copy the code

As shown in the code above, the transitionToStart() method calling MotionLayout switches to the Start scenario, and the transitionToStart() method calling MotionLayout switches to the End scenario.

The effect is as follows:

Adjust the progress of the transition animation

MotionLayout also allows you to manually adjust the playback progress of transition animations. Use MotionLayout’s setProgress(float pos) method (pos parameter ranges from 0.0 to 1.0) to adjust the progress of the transition animation.

Modify the layout file for scenario 1 by removing two buttons and adding a SeekBar. The modified layout code looks like this:

<?xml version="1.0" encoding="utf-8"? >
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_main_motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:src="@mipmap/ic_launcher"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <SeekBar
        android:id="@+id/seekBar"
        android:layout_width="240dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="56dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

The layout preview is shown below:

Modify the code in MainActivity:

public class MainActivity extends AppCompatActivity {
    private MotionLayout mMotionLayout;
    private SeekBar mSeekBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_scene1);

        mMotionLayout = findViewById(R.id.motionLayout);
        mSeekBar = findViewById(R.id.seekBar);

        mSeekBar.setMax(0);
        mSeekBar.setMax(100);
        mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                mMotionLayout.setProgress((float) (progress * 0.01));
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {}@Override
            public void onStopTrackingTouch(SeekBar seekBar) {}}); }}Copy the code

The effect is shown below:

Listen for the MotionLayout transition

You can callMotionLayoutsetTransitionListener()Methods toMotionLayoutObject registers a transition animation listener that listens for transition animation playback progress and termination events.

public void setTransitionListener(MotionLayout.TransitionListener listener)
Copy the code

TransitionListenerListener interface:

public interface TransitionListener {
    // Transition animation is called at runtime
    void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress);
    // called when the transition animation ends
    void onTransitionCompleted(MotionLayout motionLayout, int currentId);
}
Copy the code

Note: The TransitionListener interface has been changed in the alpha release to include two more callback methods: onTransitionStarted and onTransitionTrigger. Since MotionLayout is still in alpha and has not been officially released, it is normal to change it.

Ex. :

MotionLayout motionLayout = findViewById(R.id.motionLayout);
motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
    @Override
    public void onTransitionChange(MotionLayout motionLayout, int i, int i1, float v) {
        Log.d("App"."onTransitionChange: " + v);
    }

    @Override
    public void onTransitionCompleted(MotionLayout motionLayout, int i) {
        Log.d("App"."onTransitionCompleted"); }});Copy the code

conclusion

That’s the end of this article. If you think the previous example isn’t cool enough, here’s a cool one (it’s a simple one, and I suggest you try it out) :

Follow-up articles:

  • MotionLayout Basics 2

Reference article:

  • https://www.jianshu.com/p/5203cf11d943