Jetpack之MotionLayout

1,564 阅读10分钟

MotionLayout 继承自 ConstraintLayout ,用来管理布局中元素的运动轨迹和动画。可向后兼容到API 14。

MotionLayout 可以让布局转换和复杂运动更加简单,提供了属性动画TransitionManager和和 CoordinatorLayout的各种能力。

先来个简单的效果试试水

一个MotionLayout的布局有两部分组成,一个是我们的正常的xml文件,另一个是与之对应的在目录res/xml/文件夹下面对应的一个xml文件用来描述动画场景的文件,下面我们就称之为场景动画文件

新建一个Activity,名为MotionLayoutActivity,这个时候系统已经为我们创建好了一个默认的布局文件,根布局为ConstraintLayout, AndoridStudio中可以快速创建res/xml文件夹下面的文件,如下图,布局文件切换到全预览界面,找到ConstraintLayout ,右击选择convert to MotionLayout,就会看到res目录下面已经创建了一个xml目录和一个跟我们当前布局对应的场景动画xml文件。

首先在布局文件中添加一个ImageView,该布局中MotionLayout的app:layoutDescription引用了该布局所引用的场景动画的文件

<?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:id="@+id/iv_3"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/activity_motion_layout_scene"
    tools:context=".ui.motionlayout.MotionLayoutActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:srcCompat="@drawable/expression_3"
        tools:layout_editor_absoluteY="74dp" />

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

然后在场景动画文件中添加动画

<?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">
       <KeyFrameSet>
       </KeyFrameSet>
        <OnClick motion:targetId="@id/imageView" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:layout_height="50dp"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            android:layout_width="50dp"
            android:id="@+id/imageView"
            motion:layout_constraintHorizontal_bias="0.052"
            motion:layout_constraintBottom_toBottomOf="parent"
            android:rotation="0"
             />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:layout_height="50dp"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            android:layout_width="50dp"
            android:id="@+id/imageView"
            motion:layout_constraintHorizontal_bias="0.915"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_editor_absoluteY="43dp"
            android:rotation="360" />
    </ConstraintSet>
</MotionScene>

这样一个简单的ImageView从屏幕左边滑动到屏幕右边的动画就完成了。效果如下

注意:AndoridStudio4.0以后对于MotionLayout动画的编辑功能非常强大,在AndroidStudio中通过编辑器可以很快的完成这个动画,并自动生成上面的代码,所以有时候编辑器跟代码结合使用会更快捷,用编辑器迅速拖拽出大体的实现,然后通过代码修改细节

下面先来了解一下场景动画文件中这些标签代表的啥,然后在玩后面的例子。下面的标签只是列出了一些重要的属性,所有属性请点击查看属性文档

场景动画文件的根元素,包含一个或多个的<Transition>元素,每个元素用来定义动画的开始和结束状态以及这两种状态之间的转换

<Transition>标签是必须要有的,<ConstraintSet>标签是可以有的。

<MotionScene>标签有个属性defaultDuration,这个是设置其所有子元素在没有设置过渡时间的时候的默认过渡时间,单位为毫秒。

指定一个运动序列的起始和结束状态,还有中间状态和触发该运动的方式,比如点击触发或者滑动触发

重要属性:

  • motion:constraintSetStart:运动序列的初始状态。可以是 <ConstraintSet> 的 ID,也可以是布局。如需指定 <ConstraintSet>,请将此属性设置为 "@+id/constraintSetId"。如需指定布局,请设置为 “@layout/layoutState”。
  • motion:constraintSetEnd:运动序列的最终状态,设置方式跟start状态一样
  • motion:duration:运动序列的时长,以毫秒为单位。如果未指定,则使用 <MotionScene> 元素的 defaultDuration。
  • motionInterpolator:插值器效果跟属性动画中的插值器一样,取值有:linear线性、bounce弹簧、easeIn淡入、easeOut淡出、easeInOut淡入淡出。

其内部可以有3中子标签

  • <onClick> :表示用户通过点击触发动画
  • <onSwipe> :表示用户通过滑动触发动画
  • <KeyFrameSet>:关键帧,用于处理一个动画序列中的中间位置,比如曲线运动,折线运动等,他是一个关键帧的集合,内部包含<KeyPosition/><KeyAttribute>这两个标签,用来控制各种中间状态。这俩哥们的属性都比较多,具体属性可以查看文档KeyPosition属性文档KeyAttribute属性文档

这个标签用来指定所有界面元素在某个状态下在页面上的位置,一般是两个一个起始位置,一个结束位置,<Transition>标签的constraintSetEnd和constraintSetStart属性通过id指自定义的向定义的<ConstraintSet>

<ConstraintSet>是一个约束标签的集合,其内部必须包含一个或者多个约束标签<Constraint>,这个约束标签跟我们的主布局文件中的控件是通过id一一对应的,比如主布局中有一个ImageView,这边就有一个与之对应的<Constraint>标签,然后把控制ImageView位置的属性放到该标签下面。

OK 一些主要的标签的意思都知道啦,下面来尝试使用这些标签来做一些动画

先看下面这个给一个View设置颜色过渡

源文件

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

    <View
        android:id="@+id/btn_view"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginEnd="411dp"
        android:layout_marginBottom="731dp"
        android:background="@color/colorAccent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

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

场景动画文件

<?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">
       <KeyFrameSet>
       </KeyFrameSet>
        <OnSwipe
            motion:touchAnchorId="@id/btn_view"
            motion:dragDirection="dragRight"
            motion:touchAnchorSide="right"
            />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:layout_width="50dp"
            android:layout_height="50dp"
            motion:layout_constraintTop_toTopOf="parent"
            android:id="@+id/btn_view"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintLeft_toLeftOf="parent"
            >
            <CustomAttribute
                motion:attributeName="BackgroundColor"
                motion:customColorValue="@color/colorPrimary" />
        </Constraint>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:layout_width="50dp"
            android:layout_height="50dp"
            motion:layout_constraintTop_toTopOf="parent"
            android:id="@+id/btn_view"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintRight_toRightOf="parent" >
            <CustomAttribute
                motion:attributeName="BackgroundColor"
                motion:customColorValue="@color/colorAccent" />
        </Constraint>
    </ConstraintSet>
</MotionScene>

这次使用的是<OnSwipe/>标签,在手指滑动的时候来触发动画,例子中motion:touchAnchorId代表动画控制的View的id,motion:dragDirection代表手指向右滑动,motion:touchAnchorSide代表最后停在右边。

在看下面这个动画,让ImageView沿着Y轴旋转60度,这个很简单,在第一个动画的基础上,在加一个rotationY属性即可,在开始状态rotationY属性设置为0,结束状态rotationY属性设置为60就实现啦。

下一个~~

这个动画实现起来也非常简单,首先给ImageView设置起始位置和终止位置,然后使用了<Transition/>标签中的pathMotionArc这个属性,属性值设置为startVertical就完成了,该属性还有几个别的值,比如startHorizontal就是先往下在往上,练习的时候一试就知道啦。

下一个~~

这个效果虽然看起来跟上一个很像,不过实现过程是不一样的,上一个通过pathMotionArc这个属性实现的,但是它的值只有那几个,无法定制化,而这个效果是通过KeyFrameSet关键帧集合中的KeyPosition来实现的

    <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">
       <KeyFrameSet>
           <KeyPosition
               motion:motionTarget="@+id/imageView"
               motion:framePosition="46"
               motion:keyPositionType="pathRelative"
               motion:percentY="0.255" />
       </KeyFrameSet>
        <OnClick motion:targetId="@id/imageView" />
    </Transition>

我们可以通过调整percentY属性的值,来调整这个运动曲线的弧度。

下一个~~

这个动画使用了<Transition/>标签中的KeyCycle来实现

  <Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">
       <KeyFrameSet>
           <KeyPosition />
           <KeyCycle
               motion:motionTarget="@+id/imageView"
               motion:framePosition="99"
               motion:wavePeriod="1"
               android:translationX="70dp"
               motion:waveShape="sin" />
       </KeyFrameSet>
        <OnClick motion:targetId="@id/imageView" />
    </Transition>
  • framePosition:1-99的整数 用于指定在运动序列中视图何时具有该 指定的属性
  • wavePeriod: 在这个区域内循环震荡的次数,值越大震荡的越快
  • translationX: 在X轴上震荡的距离
  • waveShape :震荡曲线的样式,系统提供了很多样式 sin|square|triangle|sawtooth|reverseSawtooth|cos|bounce

下一个~~

这个gif录制的问题,实际的效果是这个ImageView迅速的旋转3600度

这个实现起来非常简单,起始位置跟结束位置不变,在结束位置那添加一个android:rotation="3600"属性就能实现这个效果了。

实现这个的目的是为了说明,我们不仅可以在xml实现这个动画,还能在代码中动态的改变动画的属性,这就给编码带来了很大的灵活性,比如下面

        val constraintSet = motionlayout.getConstraintSet(R.id.end)
        constraintSet.setRotation(R.id.imageView,7200f)
        motionlayout.updateState(R.id.end,constraintSet)

我们通过id拿到ConstraintSet,然后改变其属性,最后在更新state就可以了,比如上面的把之前的3600度改为7200度

下一个~~

很久以前要实现这个效果,需要自定义一个ViewGroup,然后计算各种位置和实现动画

现在通过motionlayout可以很快的实现,首先在主xml文件中添加5个ImageView。在场景动画的xml文件中,让他们起始位置都一样,结束位置让他们摆放在一个圆弧的不同位置,这个ConstraintLayout本身就支持,分别使用layout_constraintCircle,layout_constraintCircleRadius,layout_constraintCircleAngle这是哪个属性定位就可以啦。比如下面其中一个的代码

<Constraint
            motion:layout_editor_absoluteY="651dp"
            android:layout_height="50dp"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintCircle="@id/imageView"
            motion:layout_constraintCircleRadius="180dp"
            motion:layout_constraintCircleAngle="60"
            motion:layout_constraintEnd_toEndOf="parent"
            android:layout_width="50dp"
            android:rotation="360"
            android:id="@+id/imageView2" />

我们还可以给<Transition/>标签添加插值器motionInterpolator,让它有回弹效果或者淡入淡出效果等

下一个~~

这个动画实现起来也简单,当点击某个IngeView的时候,就让它移动到中间位置,并将其X轴和Y轴都缩放两倍,其余的ImageView的alph值都设置为0变透明就可以啦。

因为点击不同的ImageView会有不同的效果,这时候就需要设置4个Transition和4个ConstraintSet来对应每一种状态了,比如Transition的代码

<Transition
        motion:constraintSetEnd="@+id/end"
        motion:constraintSetStart="@id/start"
        motion:duration="500">
        <OnClick motion:targetId="@id/imageView4" />
    </Transition>
    <Transition
        android:id="@+id/action1"
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end1"
        motion:duration="500">
        <OnClick motion:targetId="@id/imageView2" />
    </Transition>
    <Transition
        android:id="@+id/action2"
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end2"
        motion:duration="500">
        <OnClick motion:targetId="@id/imageView" />
    </Transition>
    <Transition
        android:id="@+id/action3"
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end3"
        motion:duration="500">
        <OnClick motion:targetId="@id/imageView3" />
    </Transition>

MotionLayout动画可以给我们带来无限的想象力,仅官方demo中就有好多炫酷的效果实现,比如下面这个官方demo中的视差动画

鼠标滑动的时候不灵敏就点击了,滑动ViewPager的时候动画也会同步执行。

最上面是一个自定义的MotionLayout,中间是TabLayout,最下面是ViewPager

设置小车,树,山的起始位置和终止位置,自定义MotionLayout的目的就是监听ViewPager的滑动事件,在滑动事件中设置当前动画的进度就可以了。

class ViewpagerHeader @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), ViewPager.OnPageChangeListener {
    override fun onPageScrollStateChanged(state: Int) {
    }
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        val numPages = 3
        progress = (position + positionOffset) / (numPages - 1)
    }
    override fun onPageSelected(position: Int) {
    }
}

官方demo还有很多有趣的实现,大家可以下载来运行看看官方demo我的demo

MotionLayout可以很方便的实现一些复杂炫酷的动画,当设计师小姐姐拿来一个很炫酷的动画的时候,可以首先想想使用MotionLayout来实现,说不定可以迅速完成节省很多时间。