用 MotionLayout 来做过渡动画

题图

MotionLayout 是一个 Google 官方出品用于制作 Android 中的过渡动画的框架。用来它就能轻松的做出一些较为复杂的动画效果。

由于 MotionLayout 是基于 ConstraintLayout ,所以其中涉及到了部分关于 ConstraintLayout 的基本知识,本文按下不表,对 ConstraintLayout 不熟悉的同学,可以查看鸿洋的这篇博客

MotionLayout 是 ConstraintLayout 的子类,并且在 ConstraintLayout 发展到 2.0 时才加入 ConstraintLayout 这个库,本文所使用的依赖为:

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'

接下来让我们进入正题,先来看看我用 MotionLayout 制作的一个 Demo。

image

在这个例子中,当点击 Login 按钮时,Login 按钮的长度进行不断缩小,缩小到一定尺寸时,外层的 ProgressBar 还是逐渐由不可见变为可见,同时,Login 按钮上的字进行了淡入淡出的动画效果。

MotionLayout 能做的不仅如此,它还能做到其他更为好玩有趣的过渡动画。现在让我们来学一下吧。

过渡动画,顾名思义就是在状态之间进行过渡的动画效果,防止页面内 View 出现瞬间移动的效果。而 MotionLayout 的重点其实就是状态。开发者只需要定义好对应状态下 View 的相对位置,以及相关属性,其后 MotionLayout 便会自动为其增加动的效果。

image

这样的一个最简单的效果是怎么做出来的呢?

首先我们需要在资源文件夹 res 下新建一个名为 xml 的资源文件夹,然后再 xml 文件夹内新建一个根节点是 MotionScene 的 xml 文件,demo 中这个 xml 的文件名为 login_animator。

以下就是实现 Login 按钮长度变换的过渡动画。

<?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:constraintSetEnd="@id/a_login_end"
        app:constraintSetStart="@id/a_login_start"
        app:duration="1000">
        <OnClick
            app:clickAction="toggle"
            app:targetId="@id/tv_action_login" />

    </Transition>

    <ConstraintSet android:id="@+id/a_login_start">
        <Constraint android:id="@+id/tv_action_login">
            <Layout
                android:layout_width="match_parent"
                android:layout_height="48dp"
                                android:layout_marginTop="30dp"
                android:layout_marginStart="30dp"
                android:layout_marginEnd="30dp"
                app:layout_constraintTop_toBottomOf="@id/et_passwd" />
        </Constraint>

    </ConstraintSet>

    <ConstraintSet android:id="@+id/a_login_end">
        <Constraint android:id="@+id/tv_action_login">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_marginTop="30dp"
                app:layout_constraintEnd_toEndOf="@+id/et_account"
                app:layout_constraintStart_toStartOf="@+id/et_account"
                app:layout_constraintTop_toBottomOf="@id/et_passwd" />
        </Constraint>

    </ConstraintSet>

</MotionScene>

仔细看其中的信息,其中大部分是我们都熟悉的,无非就是对 View 的相对位置的约定或是 View 自身属性的规定,少部分是关于过渡动画的。

我们先来看看这个文件的整体结构,首先根节点是 MotionScene ,MotionScene 节点下有一个 Transition 与两个 ConstraintSet 节点,而且 Transition 中有两个属性,一个是 constraintSetStart 另一个是 constraintSetEnd,这两个属性的值正好是两个 ConstraintSet 节点的 id,而 Transition 内子节点 OnClick 节点内的属性 targetId 则表明了当前 Transition 所指定的动画是作用于具体的 View 上。

如你所想,通过在 Transition 内指定某个 View 的两个状态下的不同属性,就能产生在这两个状态内的过渡动画,并且在 Translation 内通过组合不同的动画事件进行显示。比如点击产生的动画(OnClick),滑动产生的动画(OnSwipe),以及可改变某一帧动画效果的关键帧动画(KeyFrameSet)。

当我们把初始及结束状态下的属性及动画定义完成后,还需要回到我们的布局文件,将需要实现过渡动画的 View 的父布局改为 MotionLayout 并且给它添加一个值为刚才我们新建那个 xml 文件的引用的属性 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:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/login_animator">
    ......
</androidx.constraintlayout.motion.widget.MotionLayout>

这就是一个最为简单的使用 MotionLayout 实现过渡动画的例子,它与开头我自己写的那个 demo 没什么差别,无非就是 demo 中变换的 View 的个数及属性多少不同而已。

在这个例子中,我们通过在 Transition 中定义了一个 OnClick 的子节点,而达到点击产生动画的效果。其中,targetId 即为产生动画效果的目标 View 的 id;clickAction 则是指明在是在开始或是再结束状态时产生动画,toggle 表示在开始和结束状态时均有效,它还有 transitionToStart 和 transitionToEnd 表示只在开始或是结束状态下有效。有兴趣的可以去试试。

除了 OnClick,我们还可以在 Translation 中定义 OnSwipe 节点,OnSwipe 就是用来处理屏幕上的滑动事件,以此配合指定的 View 实现过渡动画的效果。

image

给 MotionLayout 添加 motionDebug="SHOW_PATH" 这个属性,即可查看 View 的过渡动画的轨迹。

通过指定 View 的开始状态(靠近屏幕左边)和结束状态(靠近屏幕右边),然后在 Translation 中声明出滑动事件,即可。

<?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:constraintSetEnd="@id/v_swipe_end"
        app:constraintSetStart="@id/v_swipe_start"
        app:duration="1000">
        <OnSwipe
            app:dragDirection="dragRight"
            app:touchRegionId="@id/v_swipe" />

    </Transition>
    
    <ConstraintSet android:id="@+id/v_swipe_start">
        <Constraint android:id="@+id/v_swipe">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>

    </ConstraintSet>

    <ConstraintSet android:id="@+id/v_swipe_end">
        <Constraint android:id="@+id/v_swipe">
            <Layout
                android:layout_width="48dp"
                android:layout_height="48dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
        </Constraint>

    </ConstraintSet>

</MotionScene>

在 OnSwipe 中,有两个属性,一个是 dragDirection 代表的是滑动的方向,touchRegionId 则指明了监听的滑动区域为 View 的滑动区域。既然能作用于 View 的滑动区域,是不是也能作用于整个屏幕的滑动区域呢?没错,touchAnchorId 则表示全部的滑动区域。OnSwipe 还有一些其他属性,比如:touchAnchorSide 表示监听 View 的哪个区域的滑动监听,如果不设置的话,是 View 外的所有区域;onTouchUp 表示当在滑动过程中手指抬起时动画的动作(回到开始状态、回到结束状态、自动完成、停止等等)。

说实话,我在开始尝试 MotionLayout 的时候被 OnSwipe 给吓到了,但是当我更进一步的使用 KeyFrameSet 的时候直喊 666。原因就是因为 KeyFrameSet 能做出更炫酷的效果。

KeyFrameSet 是作用于在过渡动画过程中的关键帧,通过指定动画关键进程时的状态来实现不同的效果。举个例子,当前的 View 滑动是一条直线,我想让在滑动过程中有一个先向上滑动,然后向下滑动以这种效果达到屏幕的最右侧。

image

View 的开始与结束状态没有发生改变,只是在过渡动画的中点区域进行改变 View 的坐标。

<Transition
    app:constraintSetEnd="@id/v_swipe_end"
    app:constraintSetStart="@id/v_swipe_start"
    app:duration="1000">
    <OnSwipe
        app:dragDirection="dragRight"
        app:touchAnchorId="@id/v_swipe"
        app:touchAnchorSide="bottom" />

    <KeyFrameSet>
        <KeyPosition
            app:framePosition="50"
            app:keyPositionType="parentRelative"
            app:motionTarget="@+id/v_swipe"
            app:percentY="0.3" />
    </KeyFrameSet>

</Transition>

framePosition 表示在运动到整个运动过程的 50% 处,这个值的取值范围是 0 - 100,motionTarget 表示作用的 View,而 keyPositionType 与percentY 则共同决定了运动轨迹中弧度的变化方向。keyPositionType 控制 percentY 的坐标系的工作方式,它一共有 3 个值。parentRelative、deltaRelative、pathRelative。percentY 取值范围为 0 - 1,同时允许负数及大于 1 的值。

parentRelative 表示,坐标按照父布局的坐标进行处理,X,Y 轴的最大值均为1,X 轴向右为正,向左为负,Y 轴向下为正,向上为负。

image

deltaRelative 表示开始状态的中心点为坐标系原点,X,Y 轴的最大值均为1,X 轴向右为正,向左为负,Y 轴向下为负,向上为正。

image

pathRelative 表示开始状态的中心点为坐标系原点,X 轴为两个状中心点的构成的直线。X,Y 轴的最大值均为1,X 轴向结束状态方向为正,向开始状态方向为负,Y 轴向下为负,向上为正。

image

keyPositionType 三个属性的描述图均来自 CodeLab

KeyPostition 还有些其他有趣的属性,比如,控制运动轨迹是平滑的曲线还是直线的 curveFit,以及 transitionEasing 控制运动过程的加速或是减速等等。这里就不一一举例了。

而且,还可以同时存在多个关键帧进行控制动画效果。

<KeyFrameSet>
    <KeyPosition
        app:framePosition="50"
        app:keyPositionType="parentRelative"
        app:motionTarget="@+id/v_swipe"
        app:percentY="0.3" />
    <KeyAttribute
        android:alpha="0"
        app:framePosition="50"
        app:motionTarget="@+id/v_swipe" />
</KeyFrameSet>

keyAttribute 是用于在过渡动画中控制 View 的属性,比如在动画执行 50% 时,View 的 alpha 值为 0 ,那么在从 0 - 50% 及 50% - 100% 的过程中,则由 MotionLayout 根据其执行时间自动改变 View 的状态。

image

刚才聊的都是关于动画本身的内容,实际上,MotionLayout 提供更多方式来对 View 进行状态改变,不只是通过在 ConstraintSet 中指定 Layout 来改变 View 的相对位置,它还提供了更为丰富的方法进行改变 View 的状态,比如:

Motion 用于改变动画效果,例如加速、减速、先水平方法还是先垂直方向进行移动

CustomAttribute 用于改变自定义属性;

PropertySet 用于改变 View 特定的几个属性;

Transform 用于改变 View 中涉及到属性动画的属性,例如:rotation、scaleX 等。用法也很简单,像 Layout 那样声明出来即可。

<ConstraintSet android:id="@+id/v_swipe_start">
    <Constraint android:id="@+id/v_swipe">
        <CustomAttribute
            app:attributeName="backgroundColor"
            app:customColorValue="@color/colorAccent" />
        <Transform
            android:scaleX="1.0"
            android:scaleY="1.0" />
        <Layout
            android:layout_width="48dp"
            android:layout_height="48dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </Constraint>

</ConstraintSet>

<ConstraintSet android:id="@+id/v_swipe_end">
    <Constraint android:id="@+id/v_swipe">
        <CustomAttribute
            app:attributeName="backgroundColor"
            app:customColorValue="@color/colorPrimary" />
        <Transform
            android:scaleX="3.0"
            android:scaleY="3.0" />
        <Layout
            android:layout_width="48dp"
            android:layout_height="48dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </Constraint>

</ConstraintSet>
image

由于放大倍数较大,超出屏幕,所以在结束状态时显示存在异常。

怎么样,MotionLayout 是不是比想象中的好玩一些,就是现在不太方便调试,每次调试都需要运行,不过呢,现在这个还没发布正式版,估计在正式版中 Google 应该会解决这个问题。

本文首发于个人博客,文中全部源代码已上传至 GitHub,代码分支为:motionLayout。喜欢的麻烦点个🌟。

推荐学习网站:CodeLab

本文封面图:Photo by NASA on Unsplash

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350