带你学会MotionLayout

5,037 阅读7分钟

1、前言

最近在开发中,同事居然对MontionLayout一知半解,那怎么行!百里偷闲写出此文章,一起学习、一起进步。如果写的不好,或者有错误之处,恳请在评论、私信、邮箱指出,万分感谢🙏

希望你在阅读这篇文章的时候,已经对下面的内容熟练掌握了

对了还有ConstraintLayout务必熟练掌握

对了,如果可以,请跟随敲代码,毕竟你脑补的代码,没有编译器。

当然你也可以阅读相关文章

2、简介

1)根据功能将MontionLayout视为属性动画框架、TransitionManagerCoordinatorLayout 的混合体。允许描述两个布局之间的转换(如 TransitionManager),但也可以为任何属性设置动画(不仅仅是布局属性)。

2)支持可搜索的过渡,如 CoordinatorLayout(过渡可以完全由触摸驱动并立即过渡到的任何点)。支持触摸处理和关键帧,允许开发人员根据自己的需要轻松自定义过渡。

3)在这个范围之外,另一个关键区别是 MotionLayout 是完全声明式的——你可以用 XML 完整地描述一个复杂的转换——不需要代码(如果你需要通过代码来表达运动,现有的属性动画框架已经提供了一种很好的方式正在做)。

4)MotionLayout 只会为其直接子级提供其功能——与 TransitionManager 相反,TransitionManager 可以使用嵌套布局层次结构以及 Activity 转换。

3、何时使用

MotionLayout 设想的场景是当需要移动、调整实际 UI 元素(按钮、标题栏等)或为其设置动画时——用户需要与之交互的元素。

重要的是要认识到运动是有目的的——不应该只是你应用程序中一个无偿的特殊效果;应该用来帮助用户了解应用程序在做什么。Material Design 原则网站很好地介绍了这些概念。

有一类动画只需要处理播放预定义的内容,用户不会——或不需要——直接与内容交互。视频、GIF,或者以有限的方式,动画矢量可绘制对象或lottie文件通常属于此类。MotionLayout 并不专门尝试处理此类动画(但当然可以将们包含在 MotionLayout 中)。

4、依赖

确保constraintlayout版本>=2.0.0即可

build.gradle

dependencies {
    implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
}
//or
dependencies {
    implementation("androidx.constraintlayout:constraintlayout:2.1.3")
}

5、ConstraintSet

如果使用ConstraintLayout并不够多,那对ConstraintSets的认识可能不够完善,我们也展开说说

ConstraintSets包含了一个或多个约束关系,每个约束关系定义了一个视图与其父布局或其他视图之间的位置关系。通过使用ConstraintSets,开发者可以在运行时更改布局的约束关系,从而实现动画或动态变化的布局效果。

比如ConstraintSets包含了以下方法:

  1. clone():克隆一个ConstraintSet实例。
  2. clear():清除所有的约束关系。
  3. connect():连接一个视图与其父布局或其他视图之间的约束关系。
  4. center():将一个视图水平或垂直居中于其父布局或其他视图。
  5. create():创建一个新的ConstraintSet实例。
  6. constrain*():约束一个视图的位置、大小、宽高比、可见性等属性。
  7. applyTo():将约束关系应用到一个ConstraintLayout实例。

还有更多方法就不一一列举了

只使用 ConstraintSet 和 TransitionManager 来实现一个平移动画

fragment_motion_01_basic.xml

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

    <View
        android:id="@+id/button"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginStart="8dp"
        android:background="@color/orange"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
// 定义起始状态的 ConstraintSet (clContainer顶层容器)
val startConstraintSet = ConstraintSet()
startConstraintSet.clone(clContainer)
// 定义结束状态的 ConstraintSet
val endConstraintSet = ConstraintSet()
endConstraintSet.clone(clContainer)
endConstraintSet.connect(
    R.id.button,
    ConstraintSet.END,
    ConstraintSet.PARENT_ID,
    ConstraintSet.END,
    8.dp
)
endConstraintSet.setHorizontalBias(R.id.button,1f)
clContainer.postDelayed({
    // 在需要执行动画的地方
    TransitionManager.beginDelayedTransition(clContainer)
    // 设置结束状态的 ConstraintSet
    endConstraintSet.applyTo(clContainer)
}, 1000)

我们首先使用 ConstraintSet.clone() 方法来创建起始状态的 ConstraintSet。然后,我们通过 ConstraintSet.clone() 和 ConstraintSet.connect() 方法来创建结束状态的 ConstraintSet,其中 connect() 方法用于连接视图到另一个视图或父容器的指定位置。在这里,我们将按钮连接到父容器的右端(左端在布局中已经声明了),从而使其水平居中。接着我们使用setHorizontalBias使其水平居右。

在需要执行动画的地方,我们调用 TransitionManager.beginDelayedTransition() 方法告诉系统要开始执行动画。然后,我们将结束状态的 ConstraintSet 应用到 MotionLayout 中,从而实现平滑的过渡。

图片转存失败,建议将图片保存下来直接上传

ConstraintSet 的一般思想是它们封装了布局的所有定位规则;由于您可以使用多个 ConstraintSet,因此您可以即时决定将哪组规则应用于您的布局,而无需重新创建您的视图——只有它们的位置/尺寸会改变。

MotionLayout 基本上建立在这个想法之上,并进一步扩展了这个概念

6、引用现有布局

在第5点中,我们新建了一个xml,我们继续使用,不过需要将androidx.constraintlayout.widget.ConstraintLayout修改为androidx.constraintlayout.motion.widget.MotionLayout

fragment_motion_01_basic.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/button"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginStart="8dp"
        android:background="@color/orange"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.motion.widget.MotionLayout>

你会得到一个错误

image-20230407090719919

 靠着强大的编辑器,生成一个

image-20230407090719919

你就会得到下面这个和一个新的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"
    app:layoutDescription="@xml/motion_layout_01_scene">

</androidx.constraintlayout.motion.widget.MotionLayout>

也就是一个MotionScene文件

motion_layout_01_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">
</MotionScene>

这里我们先用再新建两个xml,代表开始位置和结束位置

motion_01_cl_start.xml

<?xml version="1.0" encoding="utf-8"?>
<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">
    <View
        android:id="@+id/button"
        android:background="@color/orange"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginStart="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

motion_01_cl_end.xml

<?xml version="1.0" encoding="utf-8"?>
<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">
    <View
        android:id="@+id/button"
        android:background="@color/orange"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

修改一下

motion_layout_01_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:motion="http://schemas.android.com/apk/res-auto">
    <!-- Transition 定义动画过程中的开始状态和结束状态 -->
    <!-- constraintSetStart 动画开始状态的布局文件引用 -->
    <!-- constraintSetEnd 动画结束状态的布局文件引用 -->
    <Transition
        motion:constraintSetEnd="@layout/motion_01_cl_end"
        motion:constraintSetStart="@layout/motion_01_cl_start"
        motion:duration="1000">
        <!--OnClick 用于处理用户点击事件 -->
        <!--targetId 设置触发点击事件的组件 -->
        <!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
        <OnClick
            motion:clickAction="toggle"
            motion:targetId="@+id/button" />
    </Transition>
</MotionScene>

部分解释都在注释中啦。好了 ,运行吧。

图片转存失败,建议将图片保存下来直接上传

这里的TransitionOnClick我们先按下不表。

7、独立的 MotionScene

上面的例子中,我们使用了两个XML+一个原有的布局为基础,进行的修改。最终重用您可能已经拥有的布局。MotionLayout 还支持直接在目录中的 MotionScene 文件中描述 ConstraintSet res/xml

我们在res/xml目录中新建一个xml文件

motion_layout_02_scene.xml

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

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">
        <!--OnClick 用于处理用户点击事件 -->
        <!--targetId 设置触发点击事件的组件 -->
        <!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
        <OnClick
            motion:clickAction="toggle"
            motion:targetId="@+id/button" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

</MotionScene>

首先,在 <MotionScene> 标签内定义了两个 <ConstraintSet>,分别代表动画的开始状态(start)和结束状态(end)。每个 <ConstraintSet> 内包含一个 <Constraint>,用于描述一个界面组件(如按钮或文本框)的属性。

<Transition> 标签中,我们通过 app:constraintSetStartapp:constraintSetEnd 属性指定了动画的起始和终止状态。在这个简单的示例中,我们没有插值器等属性,但可以通过添加相应的属性(如 android:durationapp:interpolator 等)来自定义动画效果。

运行一下,一样

图片转存失败,建议将图片保存下来直接上传

7、注意

  1. ConstraintSet 用于替换受影响View的所有现有约束。
  2. 每个 Constraint 元素应包含要应用于View的所有约束。
  3. ConstraintSet 不是应用增量约束,而是清除并仅应用指定的约束。
  4. 对于只有一个View需要动画的场景,MotionScene 中的 ConstraintSet 只需包含该View的 Constraint。
  5. 可以看出 MotionScene 定义和之前是相同的,但是我们将开始和结束 ConstraintSet 的定义直接放在文件中。与普通布局文件的主要区别在于我们不指定此处使用的View的类型,而是将约束作为元素的属性。

8、AndroidStudio预览工具

Android Studio 支持预览 MotionLayout,可以使用设计模式查看并编辑 MotionLayout

Snipaste_2023-04-11_14-25-19

标号含义如下

  1. 点击第一个你可以看到,当前页面的具有IDimage-20230411160718057
  2. 点击第二个,可以看到起始动画的位置 image-20230411160815353
  3. 点击第三个,可以看到终止动画的位置 image-20230411160808841
  4. 第四个,可以操作动画的预览,暂停,播放,加速,拖动,等等。
  5. 而你可以看到途中有一条线,可以使用tools:showPaths="true"开启

9、补充

今天回过来一看,示例还是少了,我稍微加几个

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">
​
    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@+id/start"
        motion:duration="1000">
        <!--OnClick 用于处理用户点击事件 -->
        <!--targetId 设置触发点击事件的组件 -->
        <!--clickAction 设置点击操作的响应行为,这里是使动画过渡到结束状态 -->
        <OnSwipe
            motion:dragDirection="dragEnd"
            motion:touchAnchorId="@+id/button1"
            motion:touchAnchorSide="end" />
​
    </Transition>
​
    <ConstraintSet android:id="@+id/start">
​
        <Constraint
            android:id="@+id/button1"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toTopOf="@id/button2"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
​
        <Constraint
            android:id="@+id/button2"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:alpha="1"
            motion:layout_constraintBottom_toTopOf="@id/button3"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button1" />
​
        <Constraint
            android:id="@+id/button3"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:rotation="0"
            motion:layout_constraintBottom_toTopOf="@id/button4"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button2" />
​
        <Constraint
            android:id="@+id/button4"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:elevation="0dp"
            motion:layout_constraintBottom_toTopOf="@id/button5"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button3" />
​
        <Constraint
            android:id="@+id/button5"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:scaleX="1"
            android:scaleY="1"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button4" />
    </ConstraintSet>
​
    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button1"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintHorizontal_bias="1"
            motion:layout_constraintBottom_toTopOf="@id/button2"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
​
        <Constraint
            android:id="@+id/button2"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:alpha="0.2"
            motion:layout_constraintBottom_toTopOf="@id/button3"
            motion:layout_constraintHorizontal_bias="1"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button1" />
​
​
        <Constraint
            android:id="@+id/button3"
            android:layout_width="64dp"
            android:layout_height="64dp"
            motion:layout_constraintHorizontal_bias="1"
            android:layout_marginStart="8dp"
            android:rotation="360"
            motion:layout_constraintBottom_toTopOf="@id/button4"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button2" />
​
        <Constraint
            android:id="@+id/button4"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:elevation="10dp"
            motion:layout_constraintBottom_toTopOf="@id/button5"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintHorizontal_bias="1"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button3" />
​
        <Constraint
            android:id="@+id/button5"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            android:scaleX="2"
            motion:layout_constraintHorizontal_bias="1"
            android:scaleY="2"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toBottomOf="@id/button4" />
    </ConstraintSet>
​
</MotionScene>

其余部分就不一一展示了,因为你们肯定都知道啦。效果如下

2023-04-12_11-49-35 (1)

10、下个篇章

因为篇幅原因,我们先到这,这篇文章,只是了解一下,下一篇我们将会深入了解各种没有详细讲解的情况。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

11、感谢

  1. 校稿:ChatGpt
  2. 文笔优化:ChatGpt