MotionLayout 基础教程

作者: cff70524f5cf | 来源:发表于2019-04-12 21:56 被阅读3次

    阅读说明:

    • 本文假设读者已掌握如何使用 ConstraintLayout
    • 本文是一篇 MotionLayout 基础教程,如您已了解如何使用 MotionLayout,本文可能对您帮助不大。
    • 建议读者跟随本文一起动手操作,如您现在不方便,建议稍后阅读。
    • 本文基于 ConstraintLayout 2.0.0-alpha4 版本编写,建议读者优先使用这一版本。
    • 由于 MotionLayout 官方文档不全,有些知识点是根据笔者自己的理解总结的,如有错误,欢迎指正。

    添加支持库:

    dependencies {
        ...
        implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha4'
    }
    

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

    简介

    MotionLayout 类继承自 ConstraintLayout 类,允许你为各种状态之间的布局设置过渡动画。由于 MotionLayout 继承了 ConstraintLayout,因此可以直接在 XML 布局文件中使用 MotionLayout 替换 ConstraintLayout

    MotionLayout 是完全声明式的,你可以完全在 XML 文件中描述一个复杂的过渡动画而 无需任何代码(如果您打算使用代码创建过渡动画,那建议您优先使用属性动画,而不是 MotionLayout)。

    开始使用

    由于 MotionLayout 类继承自 ConstraintLayout 类,因此可以在布局中使用 MotionLayout 替换掉 ConstraintLayout

    MotionLayoutConstraintLayout 不同的是,MotionLayout 需要链接到一个 MotionScene 文件。使用 MotionLayoutapp:layoutDescription 属性将 MotionLayout 链接到一个 MotionScene 文件。

    例:

    <?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)。

    创建 MotionScene 文件

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

    要使用 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 值】设置需要追踪的对象(例如:@id/image@+id/image)。
    • app:touchAnchorSide:设置需要追踪你手指运动的对象边界,共有以下 4 个可选值:
      • top
      • left
      • right
      • bottom
    • app:dragDirection:设置触发过渡动画的拖动方向。共有以下 4 个可选值:
      • dragUp:手指从下往上拖动(↑)。
      • dragDown:手指从上往下拖动(↓)。
      • dragLeft:手指从右往左拖动(←)。
      • dragRight:手指从左往右拖动(→)。
    • app:maxVelocity:【浮点值】设置动画在拖动时的最大速度(单位:像素每秒 px/s)。
    • app:maxAcceleration:【浮点值】设置动画在拖动时的最大加速度(单位:像素每二次方秒 px/s^2)。

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

    使用代码触发过渡动画

    除了使用 <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 场景。

    效果如下所示:

    image

    调整过渡动画的进度

    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>
    

    布局预览如下图所示:

    image.png

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

    效果如下图所示:

    image

    监听 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

    好了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个学习思路及方向,从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。
    由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的加群 Android IOC架构设计免费获取。
    群内还有免费的高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。

    点赞+加群免费获取 Android IOC架构设计
    image

    相关文章

      网友评论

        本文标题:MotionLayout 基础教程

        本文链接:https://www.haomeiwen.com/subject/eoahwqtx.html