MontionLayout:打开动画新世界大门(其一)

前言

本文转载自水月沐风,分享了 MontionLayout 的动画效果,相信对大家有所帮助。

水月沐风的博客地址

https://juejin.im/post/5d595328f265da03c34bfa59

最初接触到 MotionLayout 是在国外知名博客的 Android 专栏上。第一眼见到 MotionLayoutMotionLayout 的外文专栏相关介绍已足够详细,所以本文仅对其进行总结和简单应用。老规矩,正文开始前先上一张图:

简介

由于本文的受众需要有一点 ConstraintLayout 的用法基础,如果你对它并不熟悉,可以先去花几分钟看一下本人之前的译文:带你领略 ConstraintLayout 1.1 的新功能。回到正题,什么是MontionLayout ?很多人可能会对这个名词比较陌生,但如果说到它的前身 — ConstraintLayoutMontionLayout 其实是 Google 在去年开发者大会上新推的布局组件。我们先来看看 Android 官方对于它的定义:

>MotionLayout is a layout type that helps you manage motion and widget animation in your app. MotionLayout is a subclass of ConstraintLayout and builds upon its rich layout capabilities.

简单翻译过来就是:MontionLayout 是一个能够帮助我们在 app 中 管理手势 和控件 动画 的布局组件。它是ConstraintLayout 的子类并且基于它自身丰富的布局功能来进行构建。

当然,你也可以按照字面意思将它简单理解为“运动布局”。为什么这么说呢?通过上图来对比传统的布局组件(如:FrameLayoutLinearLayoutMotionLayout 是布局组件中的一个“里程碑”,由此开始就告别了 XML 文件中只能”静态“操作 UI 的历史。通过MotionLayout,我们就能更加轻易处理其内部子 View 的手势操作和"运动"效果了。正如 Nicolas Roard 所说的那样:

你可以在 MontionLayout 功能方面将其看作是属性动画、TransitionManager 和 CoordinatorLayout 的结合体 。

MotionLayout 基础

首先,我们需要从 MotionLayout 的一些基本属性和用法讲起,这样对于我们后面的实际操作将会很有帮助。

引入 MotionLayout 库

1dependencies {
2    implementation 'com.android.support.constraint:constraint-layout:2.0.0-beta2'
3}
4复制代码

目前,MotionLayout 仍处于 beta 版本,虽然官方之前说过 MotionLayout 的动画辅助工具将会在 betaConstraintLayout

在布局文件中使用 MotionLayout

想要使用 MotionLayout,只需要在布局文件中作如下声明即可:

 1<android.support.constraint.motion.MotionLayout
 2        xmlns:android="http://schemas.android.com/apk/res/android"
 3        xmlns:tools="http://schemas.android.com/tools"
 4        xmlns:app="http://schemas.android.com/apk/res-auto"
 5        android:layout_width="match_parent"
 6        android:layout_height="match_parent"
 7        app:layoutDescription="@xml/scene1">
 8
 9</android.support.constraint.motion.MotionLayout>
10复制代码

由于 MotionLayout 作为 ConstraintLayout 的子类,那么就自然而然地可以像 ConstraintLayoutMotionLayout 的用处可远不止这些。我们先来看看 MotionLayout 的构成:

640?wx_fmt=png

由上图可知,MotionLayout 可分为 <View> 和 <Helper> 两个部分。 <View> 部分可简单理解为一个ConstraintLayout,至于 <Helper> 其实就是我们的“动画层”了。MotionLayout 为我们提供了layoutDescription 属性,我们需要为它传入一个 MotionScene 包裹的 XML

MotionScene:传说中的“百宝袋”

什么是 MotionScene?结合上图 MotionScene 主要由三部分组成: StateSet 、ConstraintSet 和 Transition 。为了让大家快速理解和使用 MotionScene,本文将重点讲解ConstarintSet 和 Transition,至于 StateSet

首先,我们从实现下面这个简单的效果讲起:

640?wx_fmt=gif

GIF 画质有点渣,见谅,但从上图我们可以发现这是一个简单的平移动画,通过点击自身(篮球)来触发,让我们来通过 MotionLayout

 1&lt;?xml version="1.0" encoding="utf-8"?&gt;
 2&lt;android.support.constraint.motion.MotionLayout
 3        xmlns:android="http://schemas.android.com/apk/res/android"
 4        xmlns:tools="http://schemas.android.com/tools"
 5        xmlns:app="http://schemas.android.com/apk/res-auto"
 6        android:layout_width="match_parent"
 7        android:layout_height="match_parent"
 8        app:layoutDescription="@xml/step1"
 9        tools:context=".practice.MotionSampleActivity"&gt;
10    &lt;ImageView
11        android:id="@+id/ball"
12        android:layout_width="wrap_content"
13        android:layout_height="wrap_content"
14        android:src="@drawable/ic_basketball"/&gt;
15&lt;/android.support.constraint.motion.MotionLayout&gt;
16复制代码

布局文件很简单,只不过你可能会注意到,我们对 ImageView 并没有添加任何约束,原因在于:我们会在 MotionScene 中声明ConstraintSet,里面将包含该 ImageView 的“运动”起始点和终点的约束信息。当然你也可以在布局文件中对其加以约束,MotionScene 中对于控件约束的优先级会高于布局文件中的设定 。这里我们通过 layoutDescription 来为MotionLayout 设置它的 MotionScene 为 step1,接下来就让我们一睹 MotionScene 的芳容:

 1&lt;?xml version="1.0" encoding="utf-8"?&gt;
 2&lt;!--describe the animation for activity_motion_sample_step1.xml--&gt;
 3&lt;MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
 4             xmlns:app="http://schemas.android.com/apk/res-auto"&gt;
 5    &lt;!-- A transition describes an animation via start and end state --&gt;
 6    &lt;Transition
 7        app:constraintSetStart="@id/start"
 8        app:constraintSetEnd="@id/end"
 9        app:duration="2200"&gt;
10        &lt;OnClick
11                app:targetId="@id/ball"
12                app:clickAction="toggle" /&gt;
13    &lt;/Transition&gt;
14
15    &lt;!-- Constraints to apply at the start of the animation --&gt;
16    &lt;ConstraintSet android:id="@+id/start"&gt;
17        &lt;Constraint
18                android:id="@+id/ball"
19                android:layout_width="48dp"
20                android:layout_height="48dp"
21                android:layout_marginStart="12dp"
22                android:layout_marginTop="12dp"
23                app:layout_constraintStart_toStartOf="parent"
24                app:layout_constraintTop_toTopOf="parent"/&gt;
25    &lt;/ConstraintSet&gt;
26
27    &lt;!-- Constraints to apply at the end of the animation --&gt;
28    &lt;ConstraintSet android:id="@+id/end"&gt;
29        &lt;Constraint
30                android:id="@+id/ball"
31                android:layout_width="48dp"
32                android:layout_height="48dp"
33                android:layout_marginEnd="12dp"
34                android:layout_marginBottom="12dp"
35                app:layout_constraintEnd_toEndOf="parent"
36                app:layout_constraintBottom_toBottomOf="parent"/&gt;
37    &lt;/ConstraintSet&gt;
38
39&lt;/MotionScene&gt;
40复制代码

首先,可以发现我们定义了两个 <ConstraintSet> ,分别描述了这个? ImageView<Transition> 元素了。事实上,我们都知道,动画都是有开始位置和结束位置的,而 MotionLayoutPath

回到上面这个例子,我们只需要为 Transition 设置起始位置和结束位置的 ConstraintSet 并设置动画时间即可,剩下的都交给MotionLayout 自动去帮我们完成。当然你也可以通过 onClick 点击事件来触发动画,绑定目标控件的 id 以及通过clickAction 属性来设置点击事件的类型,这里我们设置的是toggle,即通过反复点击控件来切换动画的状态,其他还有很多属性可以参照官方文档去研究,比较简单,这里就不一一讲解它们的效果了。如此一来,运行一下就能看到上面的效果了。另外,为了方便测试,我们可以给MotionLayout 加上调试属性: app:motionDebug="SHOW_PATH" ,然后就能轻易的查看其动画内部的运动轨迹:

640?wx_fmt=gif

什么?你说这个动画效果太基础?那好,我就来个简陋版的“百花齐放”效果吧,比如下面这样:

640?wx_fmt=gif

首先,让我们分析一下这个效果:仔细看我们可以发现,通过向上滑动蓝色的 Android

 1&lt;?xml version="1.0" encoding="utf-8"?&gt;
 2&lt;android.support.constraint.motion.MotionLayout
 3        xmlns:android="http://schemas.android.com/apk/res/android"
 4        xmlns:tools="http://schemas.android.com/tools"
 5        xmlns:app="http://schemas.android.com/apk/res-auto"
 6        android:layout_width="match_parent"
 7        android:layout_height="match_parent"
 8        app:motionDebug="SHOW_PATH"
 9        app:layoutDescription="@xml/step2"
10        tools:context=".practice.MotionSampleActivity"&gt;
11    &lt;ImageView
12            android:id="@+id/ic_android_blue"
13            android:layout_width="42dp"
14            android:layout_height="42dp"
15            android:src="@mipmap/android_icon_blue"/&gt;
16    &lt;ImageView
17            android:id="@+id/ic_android_left"
18            android:layout_width="42dp"
19            android:layout_height="42dp"
20            android:src="@mipmap/android_icon_purple"/&gt;
21    &lt;ImageView
22            android:id="@+id/ic_android_right"
23            android:layout_width="42dp"
24            android:layout_height="42dp"
25            android:src="@mipmap/android_icon_orange"/&gt;
26    &lt;TextView
27            android:id="@+id/tipText"
28            android:text="Swipe the blue android icon up"
29            android:layout_width="wrap_content"
30            android:layout_height="wrap_content"
31            app:layout_constraintEnd_toEndOf="parent"
32            android:layout_marginEnd="16dp"
33            android:layout_marginTop="16dp"
34            app:layout_constraintTop_toTopOf="parent"/&gt;
35&lt;/android.support.constraint.motion.MotionLayout&gt;
36复制代码

下面我们来看下 step2 中的 MotionScene:

 1&lt;?xml version="1.0" encoding="utf-8"?&gt;
 2&lt;!--describe the animation for activity_motion_sample_step2.xml--&gt;
 3&lt;!--animate by dragging target view--&gt;
 4&lt;MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
 5             xmlns:app="http://schemas.android.com/apk/res-auto"&gt;
 6    &lt;!--At the start, all three stars are centered at the bottom of the screen.--&gt;
 7    &lt;ConstraintSet android:id="@+id/start"&gt;
 8        &lt;Constraint
 9                android:id="@+id/ic_android_blue"
10                android:layout_width="42dp"
11                android:layout_height="42dp"
12                android:layout_marginBottom="20dp"
13                app:layout_constraintStart_toStartOf="parent"
14                app:layout_constraintEnd_toEndOf="parent"
15                app:layout_constraintBottom_toBottomOf="parent"/&gt;
16        &lt;Constraint
17                android:id="@+id/ic_android_left"
18                android:layout_width="42dp"
19                android:layout_height="42dp"
20                android:alpha="0.0"
21                android:layout_marginBottom="20dp"
22                app:layout_constraintStart_toStartOf="parent"
23                app:layout_constraintEnd_toEndOf="parent"
24                app:layout_constraintBottom_toBottomOf="parent"/&gt;
25        &lt;Constraint
26                android:id="@+id/ic_android_right"
27                android:layout_width="42dp"
28                android:layout_height="42dp"
29                android:layout_marginBottom="20dp"
30                android:alpha="0.0"
31                app:layout_constraintStart_toStartOf="parent"
32                app:layout_constraintEnd_toEndOf="parent"
33                app:layout_constraintBottom_toBottomOf="parent"/&gt;
34    &lt;/ConstraintSet&gt;
35
36    &lt;!--Define the end constraint to set use a chain to position all three stars together below @id/tipText.--&gt;
37    &lt;ConstraintSet android:id="@+id/end"&gt;
38        &lt;Constraint
39                android:id="@+id/ic_android_left"
40                android:layout_width="58dp"
41                android:layout_height="58dp"
42                android:layout_marginEnd="90dp"
43                android:alpha="1.0"
44                app:layout_constraintHorizontal_chainStyle="packed"
45                app:layout_constraintStart_toStartOf="parent"
46                app:layout_constraintEnd_toStartOf="@id/ic_android_blue"
47                app:layout_constraintTop_toBottomOf="@id/tipText"/&gt;
48        &lt;Constraint
49                android:id="@+id/ic_android_blue"
50                android:layout_width="58dp"
51                android:layout_height="58dp"
52                app:layout_constraintEnd_toStartOf="@id/ic_android_right"
53                app:layout_constraintStart_toEndOf="@id/ic_android_left"
54                app:layout_constraintTop_toBottomOf="@id/tipText"/&gt;
55        &lt;Constraint
56                android:id="@+id/ic_android_right"
57                android:layout_width="58dp"
58                android:layout_height="58dp"
59                android:layout_marginStart="90dp"
60                android:alpha="1.0"
61                app:layout_constraintStart_toEndOf="@id/ic_android_blue"
62                app:layout_constraintEnd_toEndOf="parent"
63                app:layout_constraintTop_toBottomOf="@id/tipText"/&gt;
64    &lt;/ConstraintSet&gt;
65    &lt;!-- A transition describes an animation via start and end state --&gt;
66    &lt;Transition
67            app:constraintSetStart="@id/start"
68            app:constraintSetEnd="@id/end"&gt;
69        &lt;!-- MotionLayout will track swipes relative to this view --&gt;
70        &lt;OnSwipe app:touchAnchorId="@id/ic_android_blue"/&gt;
71    &lt;/Transition&gt;
72&lt;/MotionScene&gt;
73复制代码

上面代码其实很好理解,之前我们定义了一个控件的 Constraint,现在只需要多加两个即可。由于三个 AndroidConstraintLayout 的基础,就不多说了。接着将结束位置的左、右 Android 机器人透明度设置为MotionLayout 会自动处理目标控件 alpha 属性的变化效果,让其看起来依旧丝滑。

另外,我们这里没有再通过 <OnClick> 来触发动画效果,类似的,我们使用了 <OnSwipe> 手势滑动来触发动画,只需要指定touchAnchorId 为蓝色小机器人即可,怎么样,是不是有种“拍案惊奇”的感觉?。此外,你可以通过指定 touchAnchorSide 和dragDirection

到这里,你可能会说:前面两个示例的动画轨迹一直是"直线",如果想要某段动画过程的轨迹是"曲线"效果可以吗?当然没问题! Keyframes

KeyFrameSet:让动画独树一帜

如果我们想实现“独树一帜”的动画交互效果,那就离不开 KeyFrameSet

640?wx_fmt=gif

以大家的慧眼不难发现:风车的运动轨迹为曲线,并且旋转并放大至中间位置时会达到零界点,然后开始缩小。布局代码就不上了,很简单,里面唯一重要的就是我们需要实现的step3.xml 了:

 1&lt;?xml version="1.0" encoding="utf-8"?&gt;
 2&lt;!--describe the animation for activity_motion_sample_step3.xml--&gt;
 3&lt;!--animate in the path way on a view--&gt;
 4&lt;MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
 5             xmlns:app="http://schemas.android.com/apk/res-auto"&gt;
 6    &lt;!-- Constraints to apply at the start of the animation --&gt;
 7    &lt;ConstraintSet android:id="@+id/start"&gt;
 8
 9        &lt;Constraint
10                android:id="@id/windmill"
11                android:layout_width="40dp"
12                android:layout_height="40dp"
13                android:layout_marginStart="12dp"
14                android:layout_marginBottom="12dp"
15                app:layout_constraintStart_toStartOf="parent"
16                app:layout_constraintBottom_toBottomOf="parent"/&gt;
17
18        &lt;Constraint
19                android:id="@id/tipText"
20                android:layout_width="wrap_content"
21                android:layout_height="wrap_content"
22                android:alpha="0.0"
23                app:layout_constraintStart_toStartOf="parent"
24                app:layout_constraintBottom_toBottomOf="@id/windmill"
25                app:layout_constraintTop_toTopOf="@id/windmill"/&gt;
26    &lt;/ConstraintSet&gt;
27    &lt;!-- Constraints to apply at the end of the animation --&gt;
28    &lt;ConstraintSet android:id="@+id/end"&gt;
29        &lt;!--this view end point should be at bottom of parent--&gt;
30        &lt;Constraint
31                android:id="@id/windmill"
32                android:layout_width="40dp"
33                android:layout_height="40dp"
34                android:layout_marginBottom="12dp"
35                android:layout_marginEnd="12dp"
36                app:layout_constraintEnd_toEndOf="parent"
37                app:layout_constraintBottom_toBottomOf="parent"/&gt;
38        &lt;Constraint
39                android:id="@+id/tipText"
40                android:layout_width="wrap_content"
41                android:layout_height="wrap_content"
42                android:layout_marginBottom="12dp"
43                android:alpha="1.0"
44                android:layout_marginEnd="72dp"
45                app:layout_constraintEnd_toEndOf="parent"
46                app:layout_constraintBottom_toBottomOf="parent"/&gt;
47    &lt;/ConstraintSet&gt;
48
49    &lt;!-- A transition describes an animation via start and end state --&gt;
50    &lt;Transition
51            app:constraintSetStart="@id/start"
52            app:constraintSetEnd="@id/end"&gt;
53
54        &lt;KeyFrameSet&gt;
55            &lt;KeyPosition
56                    app:framePosition="50"
57                    app:motionTarget="@id/windmill"
58                    app:keyPositionType="parentRelative"
59                    app:percentY="0.5"/&gt;
60            &lt;!--apply other animation attributes--&gt;
61            &lt;!--前半段的动画效果:逆时针旋转一圈,同时放大一倍--&gt;
62            &lt;KeyAttribute
63                    app:motionTarget="@id/windmill"
64                    android:rotation="-360"
65                    android:scaleX="2.0"
66                    android:scaleY="2.0"
67                    app:framePosition="50"/&gt;
68            &lt;!--后半段的动画效果:逆时针旋转一圈,同时变回原样--&gt;
69            &lt;KeyAttribute
70                    app:motionTarget="@id/windmill"
71                    android:rotation="-720"
72                    app:framePosition="100"/&gt;
73            &lt;!--延迟动画——0-85过程中将透明度一直维持在0.0--&gt;
74            &lt;KeyAttribute
75                    app:motionTarget="@id/tipText"
76                    app:framePosition="85"
77                    android:alpha="0.0"/&gt;
78        &lt;/KeyFrameSet&gt;
79
80        &lt;OnSwipe
81            app:touchAnchorId="@id/windmill"
82            app:touchAnchorSide="bottom"
83            app:dragDirection="dragRight"/&gt;
84    &lt;/Transition&gt;
85
86&lt;/MotionScene&gt;
87复制代码

从上述代码我们可以发现:KeyFrameSet 需要被包含在 Transition 里面,同时 KeyFrameSet 中定义了<KeyPosition> 和 <KeyAttribute> 两种元素,它们主要用来设置动画某个位置的关键帧,进而为某段动画指定所期望的效果 。顾名思义,KeyPosition 用于指定动画某个关键帧的位置信息,而 KeyAttributeKeyFrameSet 中还支持 <KeyCycle> 和<KeyTimeCycle> 来让动画变得更加有趣和灵活,因篇幅有限,将在后续文章对二者进行讲解。

我们先来看下 KeyPosition 的构成:

640?wx_fmt=png

从上图可见,keyPositionType 一共有三种,本文使用的是 parentRelative,即以整个 MotionLayoutframePosition 属性来 指定关键帧所在的位置 ,取值范围为 0 - 100,本示例中设置的 50percentX 和 percentY 来设置该关键帧位置的偏移量,它们取值一般为 0 — 1,当然也可以设置为负数或者大于一,比如,本示例中如果没有设置偏移量,那么动画的轨迹无疑是一条平行于 x 轴的直线,但通过设置app:percentY="0.5",那么风车就会在动画中点位置向 y 轴方向偏移一半的高度,即下图的效果(开始 debug 模式):

640?wx_fmt=png

可能会有人问了:为什么轨迹不是三角形,而是曲线呢?哈哈,这个问题问得好!因为 MotionLayout

了解完 KeyFrameSet 的用法,那么我们就很轻易的实现下面这个效果啦:

640?wx_fmt=gif

代码就不贴了,MotionLayout 系列代码都会上传至 GitHub

本文全部代码:github.com/Moosphan/Co…

最后

本文的出发点是希望仅仅为大家提供一个“钥匙孔”的角色,通过这个“孔”,大家可以依稀看见门里“宝藏”的余光,想要打开门寻得宝藏,就需要大家"事必躬亲",拿到“钥匙”来打开这扇门了?。当然,大家也可以继续关注我的后续之作,来发现更多MontionLayout 的宝藏。

推荐阅读

Android 面试必备 - 知识图谱

致刚入职场的你 - 程序员的成长笔记

干起来,你就超过了 50% 的人

一个程序员的五年总结,给你不一样的角度

Fragment全解析系列(一):那些年踩过的坑

Fragment全解析系列(二):正确的使用姿势

带你全面了解 Android 内存优化

Android自定义控件进阶篇,自定义LayoutManager

策略模式 — 孔明排兵布阵

640?wx_fmt=jpeg
stormjun94

扫一扫,欢迎关注我的公众号 stormjun94。如果你有好的文章,也欢迎你的投稿。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页