MotionLayout 基础教程

阅读说明:php

  • 本文假设读者已掌握如何使用 ConstraintLayout
  • 本文是一篇 MotionLayout 基础教程,如您已了解如何使用 MotionLayout,本文可能对您帮助不大。
  • 本教程共有两篇文章,这是第一篇,另外一篇请点击 这里
  • 建议读者跟随本文一块儿动手操做,如您如今不方便,建议稍后阅读。
  • 本文基于 ConstraintLayout 2.0.0-alpha4 版本编写,建议读者优先使用这一版本。
  • 因为 MotionLayout 官方文档不全,有些知识点是根据笔者本身的理解总结的,若有错误,欢迎指正。

添加支持库:html

dependencies {
    ...
    implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
}
复制代码

MotionLayout 最低支持到 Android 4.3(API 18),还有就是 MotionLayoutConstraintLayout 2.0 添加的,所以必须确保支持库的版本不低于 2.0java

简介

MotionLayout 类继承自 ConstraintLayout 类,容许你为各类状态之间的布局设置过渡动画。因为 MotionLayout 继承了 ConstraintLayout,所以能够直接在 XML 布局文件中使用 MotionLayout 替换 ConstraintLayoutandroid

MotionLayout 是彻底声明式的,你能够彻底在 XML 文件中描述一个复杂的过渡动画而 无需任何代码(若是您打算使用代码建立过渡动画,那建议您优先使用属性动画,而不是 MotionLayout)。app

开始使用

因为 MotionLayout 类继承自 ConstraintLayout 类,所以能够在布局中使用 MotionLayout 替换掉 ConstraintLayout框架

MotionLayoutConstraintLayout 不一样的是,MotionLayout 须要连接到一个 MotionScene 文件。使用 MotionLayoutapp:layoutDescription 属性将 MotionLayout 连接到一个 MotionScene 文件。ide

例:布局

<?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>
复制代码

注意!必须为 MotionLayout 布局的全部直接子 View 都设置一个 Id(容许不为非直接子 View 设置 Id)。post

建立 MotionScene 文件

MotionScene 文件描述了两个场景间的过渡动画,存放在 res/xml 目录下。gradle

要使用 MotionLayout 建立过渡动画,你须要建立两个 layout 布局文件来描述两个不一样场景的属性。当从一个场景切换到另外一个场景时,MotionLayout 框架会自动检测这两个场景中具备相同 idView 的属性差异,而后针对这些差异属性应用过渡动画(相似于 TransitionManger)。

MotionLayout 框架支持的标准属性:

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

MationLayout 除了支持上面列出的标准属性外,还支持所有的 ConstraintLayout 属性。

下面来看一个完整的例子,这个例子分为如下 3 步。

1 步:建立场景 1 的布局文件:

文件名: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>
复制代码

场景 1 的布局预览以下图所示:

2 步:建立场景 2 的布局文件:

文件名: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>
复制代码

场景 2 的布局预览以下图所示:

说明:场景 1 与场景 2 中都有一个 id 值为 imageImageView,它们的差异是:场景 1 中的 image 是水平垂直居中放置的,而场景 2 中的 image 是水平居中,垂直对齐到父布局顶部的。所以当从场景 1 切换到场景 2 时,MotionLayout 将针对 image 的位置差异自动应用位移过渡动画。

3 步:建立 MotionScene 文件:

文件名:activity_main_motion_scene.xml,存放在 res/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">

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

    </Transition>

</MotionScene>
复制代码

编写完 MotionLayout 文件后就能够直接运行程序了。点击 image 便可进行场景切换。当进行场景切换时,MotionLayout 会自动计算出两个场景之间的差异,而后应用相应的过渡动画。

MotionLayout Demo

下面对 MotionLayout 文件进行说明:

如上例所示,MotionScene 文件的根元素是 <MotionScene>。在 <MotionScene> 元素中使用 <Transition> 子元素来描述一个过渡,使用 <Transition> 元素的 app:constraintSetStart 属性指定起始场景的布局文件,使用 app:constraintSetEnd 指定结束场景的布局文件。在 <Transition> 元素中使用 <OnClick> 或者 <OnSwip> 子元素来描述过渡的触发条件。

<Transition> 元素的属性:

  • app:constraintSetStart:设置为起始场景的布局文件 Id
  • app:constraintSetEnd:设置为结束场景的布局文件 Id
  • app:duration:过渡动画的持续时间。
  • app:motionInterpolator:过渡动画的插值器。共有如下 6 个可选值:
    • linear:线性
    • easeIn:缓入
    • easeOut:缓出
    • easeInOut:缓入缓出
    • bounce:弹簧
    • anticipate:(功能未知,没有找到文档)
  • app:staggered:【浮点类型】(功能未知,没有找到文档)

能够在 <Transition> 元素中使用一个 <OnClick> 或者 <OnSwipe> 子元素来描述过渡的触发条件。

<OnClick> 元素的属性:

  • app:targetId:【id 值】设置用来触发过渡的那个 ViewId(例如:@id/image@+id/image)。

提示app:targetId 的值的前缀既能够是 @+id/ 也能够是 @id/,二者均可以。官方示例中使用的是 @+id/。不过,使用 @id/ 前缀彷佛更加符合语义,由于 @+id/ 前缀在布局中经常使用来建立一个新的 Id,而 @id/ 前缀则经常使用来引用其余的 Id 值。为了突出这里引用的是其余的 Id 而不是新建了一个 Id,使用 @id/ 前缀要更加符合语义。

  • app:clickAction:设置点击时执行的动做。该属性共有如下 5 个可选的值:
    • toggle:在 Start 场景和 End 场景之间循环的切换。
    • transitionToEnd:过渡到 End 场景。
    • transitionToStart:过渡到 Start 场景。
    • jumpToEnd:跳到 End 场景(不执行过渡动画)。
    • jumpToStart:跳到 Start 场景(不执行过渡动画)。

<OnSwipe> 元素的属性:

  • app:touchAnchorId:【id 值】设置拖动操做要关联到的对象,让触摸操做看起来像是在拖动这个对象的由 app:touchAnchorSide 属性指定的那个边。
  • app:touchAnchorSide:设置触摸操做将会拖动对象的哪一边,共有如下 4 个可选值:
    • top
    • left
    • right
    • bottom
  • app:dragDirection:设置拖动的方向(注意,只有设置了 app:touchAnchorId 属性后该属性才有效)。共有如下 4 个可选值:
    • dragUp:手指从下往上拖动(↑)。
    • dragDown:手指从上往下拖动(↓)。
    • dragLeft:手指从右往左拖动(←)。
    • dragRight:手指从左往右拖动(→)。
  • app:maxVelocity:【浮点值】设置动画在拖动时的最大速度(单位:像素每秒 px/s)。
  • app:maxAcceleration:【浮点值】设置动画在拖动时的最大加速度(单位:像素每二次方秒 px/s^2)。

能够同时设置 <OnClick><OnSwipe> ,或者都不设置,而是使用代码来触发过渡。

还能够在 <Transition> 元素中设置多个 <OnClick>,每一个 <OnClick> 均可以关联到一个不一样的控件上。虽然 <Transition> 元素中也能够设置多个 <OnSwipe>,可是后面的 <OnSwipe> 会替换掉前面的 <OnSwipe>,最终使用的是最后一个 <OnSwipe>

<OnSwipe> 拖动操做

因为 <OnSwipe> 拖动操做涉及的交互较为复杂,这里单独对它的如下 3 个属性进行说明:

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

首先是 app:touchAnchorId 属性与 app:dragDirection 属性。app:touchAnchorId 属性用于设置拖动操做要关联到的对象;app:dragDirection 属性用于指定拖动方向。

默认状况下,由上往下 拖动时会运行过渡动画,此时 <OnSwipe/> 元素不须要设置任何属性,只要在 <Transition> 中加一个 <OnSwipe/> 标签便可。

例:

<Transition ...>

    <OnSwipe/>

</Transition>
复制代码

可是,若是你要支持 由下往上(↑)或者 由左往右(→)或者 由右往左(←),那么至少应该设置好 app:touchAnchorIdapp:dragDirection 属性。

app:dragDirection 属性设置的拖动方向与 app:touchAnchorId 属性关联到的对象在 Start 场景和 End 场景中的位置是息息相关的。例以下图 a 中,End 场景中的 Widget 位于 Start 场景中的 Widget 的上方,那么应该设置 app:dragDirection="dragUp"。再看图 b 中,End 场景中的 Widget 位于 Start 场景中的 Widget 的右边,那么应该设置 app:dragDirection="dragRight"

若是 End 场景中的 Widget 相对于 Start 场景中的 Widget 是倾斜的(以下图所示),将会有两个可选的方向,下图中的可选方向是 dragUpdragRight,具体使用哪一个方向,由你本身决定。

设置一个正确的拖动方向是很是重要的,不然拖动时,过渡动画将表现不佳。

提示MotionLayout 将使用 app:touchAnchorId 关联到的对象在 app:dragDirection 方向上的拖动进度(progress)做为整个过渡动画的进度,当关联对象在 app:dragDirection 方向上的拖动完成时,也就意味着整个过渡动画完成了。

:实现拖动效果

删除 <Transition> 元素元素的 <OnClick> 标签,并加入一个 <OnSwipe> 标签,修改后的 MotionScene 文件内容以下所示:

<?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,加入 OnSwipe -->
        
        <OnSwipe app:touchAnchorId="@id/image" app:dragDirection="dragUp"/>

    </Transition>

</MotionScene>
复制代码

注意:若是将 <OnClick><OnSwipe> 关联到了同一个控件,或者 <OnSwipe> 关联到的那个控件是可点击的,点击事件将会影响到拖动,你将没法按住控件进行拖动,只能按住控件的外面才能拖动。

<Transition> 标签中加入 <OnClick>,修改后的 MotionScene 文件内容以下所示:

<?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"/>

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

    </Transition>

</MotionScene>
复制代码

效果以下所示:

app:touchAnchorSide 属性:

app:touchAnchorSide 属性的功能是 “设置触摸操做将会拖动对象的哪一边”,该属性可用于实现可折叠效果,例如可折叠标题栏。

:在底部实现一个向上拉的折叠效果。

1. 修改 acticity_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" />

    <!-- 增长如下代码 -->
    <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>
复制代码

2. 修改 acticity_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" />

    <!-- 增长如下代码 -->
    <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>
复制代码

3. 修改 MotionScene 文件(文件名: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">

        <!-- 关联到 bottomBar 上-->
        <OnSwipe app:touchAnchorId="@id/bottomBar" app:touchAnchorSide="top" app:dragDirection="dragUp"/>

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

    </Transition>

</MotionScene>
复制代码

效果以下所示:

提示:其实 <OnSwipe> 能够不关联到 bottomBar 上,在由于在前面的例子中咱们已经把 <OnSwipe> 关联到了 image 上,且拖动方向也设置正确(drageUp),这样其实已经能够正常拖动了。可是因为 bottomBar 是可折叠的,把 <OnSwipe> 拖动关联到它上面更加合适,这样能够设置 app:touchAnchorSide="top",告诉 MotionLayout 控件 bottomBar 的上边界是可拖动的,这样更符合语义。

使用代码触发过渡动画

除了使用 <OnClick> 元素与 <OnSwipe> 元素来设置触发过渡动画的触发条件外,还可使用代码来手动触发过渡动画。

下面对场景 1 的布局文件进行修改,在布局中添加 2 个按钮,预览以下图所示:

场景 1 修改后的布局文件内容为:

<?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>
复制代码

场景 2 的布局文件不须要修改。

MainActivity 中添加以下代码来手动执行过渡动画:

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) {
                // 切换到 Start 场景
                mMotionLayout.transitionToStart();
            }
        });

        btnToEndScene.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 切换到 End 场景
                mMotionLayout.transitionToEnd();
            }
        });
    }
}
复制代码

如上面代码中所示,调用 MotionLayouttransitionToStart() 方法能够切换到 Start 场景,调用 MotionLayouttransitionToStart() 方法能够切换到 End 场景。

效果以下所示:

调整过渡动画的进度

MotionLayout 还支持手动调整过渡动画的播放进度。使用 MotionLayoutsetProgress(float pos) 方法(pos 参数的取值范围为 [0.0 ~ 1.0])来调整过渡动画的播放进度。

下面对场景 1 的布局文件进行修改,移除两个按钮,加入一个 SeekBar,修改后的布局代码以下所示:

<?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>
复制代码

布局预览以下图所示:

修改 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) {

            }
        });
    }
}
复制代码

效果以下图所示:

监听 MotionLayout 过渡

能够调用 MotionLayoutsetTransitionListener() 方法向 MotionLayout 对象注册一个过渡动画监听器,这个监听器能够监听过渡动画的播放进度和结束事件。

public void setTransitionListener(MotionLayout.TransitionListener listener) 复制代码

TransitionListener 监听器接口:

public interface TransitionListener {
    // 过渡动画正在运行时调用
    void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress);
    // 过渡动画结束时调用
    void onTransitionCompleted(MotionLayout motionLayout, int currentId);
}
复制代码

提示TransitionListener 接口在 alpha 版本中有所改动,可多出了 2 个回调方法:onTransitionStartedonTransitionTrigger。因为 MotionLayout 还处于 alpha 版本,并未正式发布,所以有所改动也是正常。

例:

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");
    }
});
复制代码

结语

本篇文章到此就结束了,你可能会以为前面的例子不够炫酷,这里给出一个炫酷点的例子(这个例子很简单,建议读者动手尝试实现一下):

MotionLayout Cool Demo

后续文章:

参考文章:

相关文章
相关标签/搜索