Motion动画

官方MotionLayout的教程

1.在说动画实现之前先针对几个设置相关的属性解释一下

<?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
        android:id="@+id/backward"
        app:constraintSetEnd="@+id/previous"
        app:constraintSetStart="@id/start"
        app:duration="1000">

        <OnSwipe
            app:dragDirection="dragLeft"
            app:touchAnchorSide="left" />

    </Transition>

    <Transition
        android:id="@+id/forward"
        app:constraintSetEnd="@id/next"
        app:constraintSetStart="@id/start">

        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorSide="right" />

    </Transition>


    <ConstraintSet android:id="@+id/previous">

        <Constraint
            android:id="@id/iv0"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv1"
            android:layout_width="0dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/iv2"
            app:layout_constraintStart_toEndOf="@id/iv0"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv2"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv3"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </ConstraintSet>


    <ConstraintSet android:id="@+id/start">

        <Constraint
            android:id="@id/iv0"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv1"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv2"
            android:layout_width="0dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/iv3"
            app:layout_constraintStart_toEndOf="@id/iv1"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv3"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv4"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </ConstraintSet>

    <ConstraintSet android:id="@+id/next">

        <Constraint
            android:id="@id/iv1"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv2"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv3"
            android:layout_width="0dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/iv4"
            app:layout_constraintStart_toEndOf="@id/iv2"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv4"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>
</MotionScene>

针对上面的xml解释一些概念,MotionScene用于描述MotionLayout动画流程以及相关设置的所有属性

Transition定义了动画的开始和结束的状态,以及它持续的时间,它的属性包括:

  • constraintSetStart动画的开始状态
  • constraintSetEnd动画的结束状态
  • duration动画的持续时间
  • motionInterpolator差值器,数值变化的差值器,例如线性的变化,缓入缓存,缓入等等效果。
    在一个MotionScene中可以有多个Transition的存在,用于实现在不同条件下的状态变化,它的组成包括有,OnSwipe,OnClick以及KeyFrameSet,下面我们来分别介绍它。
    OnSwipe定义了在滑动时相关属性,它监听的是MotionLayout,而不是指定的id的视图,OnSwip相关的属性:
  • touchAnchorId是所跟踪的视图,它通过移动来响应轻触操作,MotionLayout会将该视图保持与快速滑动的手机相同的距离
  • touchAnchorSide:用于确定应该跟踪视图的哪一侧。对于要调整大小,遵循复杂路径或一侧的移动数据比另一侧快的视图很重要。
  • dragDirection用于确定可对该动画效果产生影响的方向(上下左右)
    OnClick定义了在点击发生时的响应条件
  • clickAction,事件响应类型,包括有toggle(切换,也就是在开始和结束状态之间来回的切换,类如一个view冲左移动到右边,那边再点击一次,那么它会执行从右边到左边移动的效果,反复执行),transitionToStart和jumpToStart最终效果都是执行view到start状态上,区别就是有没有过度的过程,transitionToEnd和jumpToEnd类似。
  • targetId,响应这个点击事件的View的id
    KeyFrameSet是transition的子集,它包含有KeyPosition
    KeyPosition是当需要进行复杂路径的Motion动画时使用进行某一进度时的状态,而KeyFrameSet就是这些点的集合,这些点就可以构成整个动画的路径状态,KeyPosition的属性:
  • framePosition:是一个介于0到100的数字,它定义了动画中应用该KeyPosition的时间,其中1代表动画播放到1%的位置,99代表动画播放到99%的位置,
  • motionTarget:是被该KeyPosition修改路径的视图
  • keyPositionType:是该KeyPosition修改路径的方式。它可以是parentRelative,pathRelative或deltaRelative
  • percentX|percentY是指在framePosition按多大百分比来修改路径(值介于0.0到1.0之间)允许使用负数和大于1的值
  • curveFit默认情况下,MotionLayout 会将因修改路径而产生的所有角设为圆角,如果不需要这个弧度那么就是修改这个值,linear线性的变化,spline弧度变化。
    其中keyPositionType三个值的区别就是它的坐标系的构成,下面是三个坐标系的表现形式:



    官方关于keyPositionType的详细介绍

案例分析

「分析的案例的代码地址」

案例1侧滑菜单

以往我们实现侧滑菜单时我们时通过DrawerLayout,实现DrawerListener,在回掉方法中动态修改view的状态和属性,现在我们通过结合DrawerLayout和MotionLayout来实现一个侧滑菜单:

import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.drawerlayout.widget.DrawerLayout

class DrawerContent @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr), DrawerLayout.DrawerListener {

    override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
        //设置MotionLayout的变化进度
        progress = slideOffset
    }

    override fun onDrawerOpened(drawerView: View) {
    }

    override fun onDrawerClosed(drawerView: View) {
    }

    override fun onDrawerStateChanged(newState: Int) {
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        //注册相应的监听
        (parent as? DrawerLayout)?.addDrawerListener(this)
    }
}

定义Scene

<?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="250"
        motion:motionInterpolator="linear">

    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            motion:layout_constraintStart_toStartOf="parent" />


    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginStart="180dp"
            android:scaleX="0.8"
            android:scaleY="0.8"
            motion:layout_constraintStart_toStartOf="parent" />
    </ConstraintSet>
</MotionScene>

这里可以看到我们的Transition是没有设置触发条件的,它就是通过progress来确定当前的动画的进度,变化效果就是在距离Parent的start的0-180dp的范围内变化,并且伴随有x轴和y的缩放,缩放范围1-0.8,在xml中的使用:

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


    <com.arms.flowview.motion.DrawerContent
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutDescription="@xml/fragemnt_motion_swipe_content_scene">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

    </com.arms.flowview.motion.DrawerContent>


    <RelativeLayout
        android:layout_width="180dp"
        android:layout_height="match_parent"
        android:layout_gravity="start">

        <TextView
            android:layout_width="180dp"
            android:layout_height="match_parent"
            android:background="@color/black"
            android:gravity="center"
            android:text="123123"
            android:textColor="@color/white"
            android:textSize="20sp" />

    </RelativeLayout>

</androidx.drawerlayout.widget.DrawerLayout>

这样我们就实现了一个带有侧滑菜单并且主视图内容有缩放的效果。

效果2


先分析当前的动画效果,向右滑动时,整体右移,视图1移出屏幕,视图2宽度缩小移动到视图1的位置,
视图3放大移动到视图2的位置,屏幕外的视图4移动到屏幕内视图3的位置,向左滑动类似,左右滑动都需要处理反向已经在屏幕外的视图,所以视图的总数是5。
动画的Sence:

<?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
        android:id="@+id/backward"
        app:constraintSetEnd="@+id/previous"
        app:constraintSetStart="@id/start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragLeft"
            app:touchAnchorSide="left" />
    </Transition>

    <Transition
        android:id="@+id/forward"
        app:constraintSetEnd="@id/next"
        app:constraintSetStart="@id/start">

        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorSide="right" />

    </Transition>


    <ConstraintSet android:id="@+id/previous">

        <Constraint
            android:id="@id/iv0"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv1"
            android:layout_width="0dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/iv2"
            app:layout_constraintStart_toEndOf="@id/iv0"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv2"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv3"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </ConstraintSet>


    <ConstraintSet android:id="@+id/start">

        <Constraint
            android:id="@id/iv0"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv1"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv2"
            android:layout_width="0dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/iv3"
            app:layout_constraintStart_toEndOf="@id/iv1"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv3"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv4"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </ConstraintSet>

    <ConstraintSet android:id="@+id/next">

        <Constraint
            android:id="@id/iv1"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv2"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv3"
            android:layout_width="0dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/iv4"
            app:layout_constraintStart_toEndOf="@id/iv2"
            app:layout_constraintTop_toTopOf="parent" />

        <Constraint
            android:id="@id/iv4"
            android:layout_width="60dp"
            android:layout_height="200dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>
</MotionScene>

这里面包含了三组的状态,分别对应了起始状态Start,向右滑动的Next状态,向左滑动的后的previous状态,当每次状态变化执行后,那么相应的状态会重置为Start,但是view的位置是已经发生了变化的,这也就是为什么能够用三个状态来实现对应效果的原因,Constraint所使用的id,应该就是为了在状态第一次生效时确定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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/fragment_motion_image_scene">

    <ImageView
        android:id="@+id/iv_back"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/dm1"
        android:scaleType="centerCrop"
        />

    <ImageView
        android:id="@+id/iv0"
        android:layout_width="60dp"
        android:layout_height="200dp"
        android:scaleType="centerCrop"
        android:src="@drawable/dm4"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv1"
        android:layout_width="60dp"
        android:layout_height="200dp"
        android:scaleType="centerCrop"
        android:src="@drawable/dm5"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <ImageView
        android:id="@+id/iv2"
        android:layout_width="0dp"
        android:layout_height="200dp"
        android:scaleType="centerCrop"
        android:src="@drawable/dm1"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@id/iv3"
        app:layout_constraintStart_toEndOf="@id/iv1"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv3"
        android:layout_width="60dp"
        android:layout_height="200dp"
        android:scaleType="centerCrop"
        android:src="@drawable/dm2"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/iv4"
        android:layout_width="60dp"
        android:layout_height="200dp"
        android:scaleType="centerCrop"
        android:src="@drawable/dm3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.constraintlayout.helper.widget.Carousel
        android:id="@+id/carousel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:carousel_backwardTransition="@id/backward"
        app:carousel_firstView="@id/iv2"
        app:carousel_forwardTransition="@id/forward"
        app:carousel_infinite="true"
        app:carousel_nextState="@id/next"
        app:carousel_previousState="@id/previous"
        app:constraint_referenced_ids="iv0,iv1,iv2,iv3,iv4" />

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

Carousel设置了相关的属性:

  • carousel_backwardTransition在Secene中向后执行的Transition
  • carousel_forwardTransition在Secene中向前执行的Transition
  • carousel_firstView:确定第一个view是哪个,会关系到数据的展示顺序
  • carousel_infinite:是否无限循环
  • carousel_nextState:next状态对应的ConstraintSet
  • carousel_previousState:previous状态对应的ConstraintSet
  • constraint_referenced_ids:受到控制的id
    对应在代码中设置Carousel的Adapter代码
 binding.carousel.setAdapter(object : Carousel.Adapter {
            override fun count(): Int {
                return 5
            }

            override fun populate(view: View?, index: Int) {
                if (view is ImageView) {
                    view.load(imags[index])
                }
            }

            override fun onNewItem(index: Int) {
                binding.ivBack.load(imags[index])
            }

        })

在代码中count确定了总的数量,populate视图变化后的子view以及它的下标,onNewItem在firstView位置当前视图的下标。
Motion适合实现一些复杂的动画效果,不过因为是通过xml预先配置好的,所以并不适合一些动态视图的效果。


参考链接:

MotionLayout 的高级玩法我学会了


完整项目地址