MotionLayout 是 ConstraintLayout 的子类,可以基于动画和手势实现效果炫酷的布局切换。
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
ConstraintLayout 的 2.0 以上就可以使用 MotionLayout 了,目前最新版是2.1.0-beta02。
将布局转换为MotionLayout
MotionLayout 是ConstraintLayout 的子类,用ConstraintLayout 写的布局,用MotionLayout也可以。
所有就放心的将ConstraintLayout 转换为MotionLayout 吧
打开布局 在视图预览处或则在Component Tree 栏选中选择ConstraintLayout 右击在菜单栏中点击 Covert to MotionLayout 选项
认识 MotionLayout 工作台
当我们通过上面方式将ConstraintLayout转换为MotionLayout 之后,会帮我们创建一个MotionScene文件 在 res/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:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<KeyFrameSet>
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start">
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
</ConstraintSet>
</MotionScene>
这个文件和布局关联在一起是通过在布局文件声明的 app:layoutDescription
<androidx.constraintlayout.motion.widget.MotionLayout
……
app:layoutDescription="@xml/activity_main_scene"
>
再来看看布局,Android studio(4.0及以上版本) 给我带来一个MotionLayout 可视化的动画编辑工具,如下图所示
① 普通状态,选中后预览视图显示原始的状态
② 表示id="start"的ConstraintSet,选中后预览视图显示此约束集的布局,xml 中代码如下
<ConstraintSet android:id="@+id/start">
</ConstraintSet>
💡 ConstraintSet 标签 官方解释:"指定所有视图在动画序列中某一点上的位置和属性。通常,一个
Transition
元素可指向两个ConstraintSet
元素,其中一个定义动画序列的开始,另一个定义结束" 我的理解 ConstraintSet 用来存放一些View在某个状态下的约束集和属性
③ 表示id="end"的ConstraintSet,选中后预览视图显示此约束集的布局
④表示从start 约束集到end 的转场,对应xml 代码如下
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
</Transition>
⑤ 创建一个新的ConstraintSet
⑥ 创建一个新的转场Transition
⑦ 创建触发转场的行为,是点击Click还是滑动swipe
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<!-- 点击-->
<OnClick />
<!-- 滑动-->
<OnSwipe />
</Transition>
仔细看这三个图标还挺形象的
⑧ 点击后有有个帮助教程
⑨ 表示约束集的元素
动画的开始与结束
动画,有开始有结束。我们享受的是从开始到结束的过程。
入门一个简单的效果
预览
实现一个简单的矩形平移动画
💡 上图是运行效果,图中虚线表示运动轨迹。要显示运动轨迹的虚线 需要把MotionLayout的showPaths 属性设置为true
app:showPaths="true"
创建动画元素
在布局中创建一个View 如下
<androidx.constraintlayout.motion.widget.MotionLayout
……
app:layoutDescription="@xml/activity_main_scene"
app:showPaths="true">
<View
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@color/colorAccent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
定义动画开始&结束状态(ConstraintSet)
将id="box" 的添加到id="start" 的约束集(ConstraintSet)中经过上面一波操作,就会在约束集(id="start") 中为View(id="box") 创建一个新的约束。这一波操作对应的xml 代码如下
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintStart_toStartOf="parent" />
</ConstraintSet>
ConstraintSet
上文我们说了,它是存放一些view 约束和属性的的集合,描述View约束和属性是通过Constraint
标签。根据自己需要我们修改Constraint(id="box") 的约束如下(垂直居中,水平方向靠最左边)
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
类似的做法,我们在ConstraintSet(id="end") 添加View(id="box") 的约束如下(垂直居中,水平方向靠最右边)
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
转场(Transition)
现在View(id="box")start 和 end的约束已经写好了,怎么动起来呢,请看下面代码
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<!-- 点击-->
<OnClick />
</Transition>
Transition 定义可视化就是上图红色区域那条带箭头的线,有没有注意到线上还有个小点就是表示点击
Transition 标签,通过 motion:constraintSetStart
指定运动开始状态(其值是ConstrainSet的id) 到motion:constraintSetEnd
指定运动结束状态 的转场,motion:duration
来指定动画执行的时间。Transition 只是定义了指定开始和结束的状态,要让用户去触发ta,还要在Transition 标签下添加或
💡 Transition 有一个属性
motion``:autoTransition
可以设置自动执行转场的行为,无需用户自己操作。
OnClick
表示由用户点击触发
属性
-
motion:targetId="@id/target_view" (目标View的id)
如果不指定次属性,就是点击整个屏幕触发如果写了这个属性,就是点击对应id的View 触发转场动画
-
motion:clickAction="action" 点击后要进行的行为 ,此属性可以设置以下几个值
transitionToStart
过渡到 <Transition>
元素 motion::constraintSetStart
属性指定的状态,有过度动画效果transitionToEnd
过渡到 <Transition>
元素motion:constraintSetEnd
属性指定的状态,有过度动画效果jumpToStart
直接跳转到 <Transition>
元素 motion::constraintSetStart
属性指定的状态,没有动画效果jumpToEnd
直接跳转到 <Transition>
元素 motion:constraintSetEnd
属性指定的状态。toggle
默认值就是这个。在<Transition>
元素motion::constraintSetStart
和 motion:constraintSetEnd
指定的布局之间切换,如果处于start状态就过度到end状态,如果处于end状态就过度到start状态,有过度动画。
OnSwipe
表示由用户滑动触发,它会根据用户滑动行为调整动画的进度。一个Transition
标签下可以包含多个OnSwipe
。
示例
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnSwipe
motion:dragDirection="dragEnd"
motion:touchRegionId="@+id/box" />
</Transition>
它有以下几个属性motion:touchAnchorId
其值为View的Id,滑动后做出相应的视图
💡 OnSwipe 监听的是MotionLayout 的滑动,不是
motion:touchAnchorId
的滑动
motion:touchAnchorSide
官网解释:滑动所固定到的目标视图的一侧。MotionLayout 将尝试在该固定点与用户手指之间保持恒定的距离。
可接受的值包括 "left"
、"right"
、"top"
和 "bottom"
。motion:dragDirection
用户滑动动作的方向。如果设置了此属性,此 <onSwipe>
将仅适用于沿特定方向的滑动。可接受的值包括 "dragLeft"
、"dragRight"
、"dragUp"
和 "dragDown"
。
这个属性有意思,可以让目标跟着你手指滑动方向走,可以按你滑动的反方向走,更或者按你滑动的垂直方向走,就看你怎么设置这个属性
motion:dragScale
控制视图相对于滑动长度的移动距离。默认值为 1,表明视图移动的距离应与滑动距离一致。如果 dragScale
小于 1,视图移动的距离会远远小于滑动距离(例如,dragScale
为 0.5 意味着如果滑动移动 4 厘米,目标视图会移动 2 厘米)。如果 dragScale
大于 1,视图移动的距离会大于滑动距离(例如,dragScale
为 1.5 意味着如果滑动移动 4 厘米,目标视图会移动 6 厘米)。motion:maxVelocity
目标视图的最大速度。当手指滑动一定速度,目标View 会按照惯性继续运作,进行先加速后减速运行(默认情况)。如果运动过程中加速到了我们设置的最大值(根据我们学的初中那点物理知识可知,能不能到达速度最大值,受加速度()、时间()有关、以及初始速度(),),那么就是先加速,然后按最大速度匀速运动,然后在减速运动。motion:maxAcceleration
目标视图的最大加速度,如果想让View 运动变快,就把加速度调大一点吧
属性转场动画
上面我们说了,ConstraintSet的 Constraint 中可以指定约束和属性,上面的平移的例子中我们只用了约束,下面我们看看加些属性的效果吧。
1.设置透明度属性
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
android:alpha="1"
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/box"
android:layout_width="64dp"
android:layout_height="64dp"
android:alpha="0.1"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</ConstraintSet>
在动画开始位置设置android:alpha="1"
结束位置设置android:alpha="0.1"
MotionLayout就会帮我们自动过度,效果如下
2. 设置个旋转属性
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
android:rotationX="0"
……约束条件同上/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/box"
android:layout_width="64dp"
android:layout_height="64dp"
……约束条件同上/>
</ConstraintSet>
在动画开始位置设置android:rotationX="0"
结束位置设置android:rotationX="0"
你还可以改变其它属性试试,比如设置不同的背景颜色、设置不同大小等
动画的运动过程 🏃
上面的动画,都是我们定义了开始与结束两个状态,然后它自己过度的动画。下面我们来说说如果干涉他运动的过程中的
关键帧(KeyFrameSet)
KeyFrameSet是Transition的子元素
默认情况下,我们只需要定义动画开始和结束状态就行了,MotionLayout 会帮我们平滑的过度。如果我想搞一个复杂些的运动,就需要KeyFrameSet来设置,我们在动画运动过程中的某些点设置特定的约束或属性,点与点之前的过度,MotionLayout同样会帮我们平滑的过度。
KeyFrameSet 中可以包含KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger,这几种使用编辑添加如下图所示
我们点击选中①,下面会出现一个时间轴的工具,我们点击②处播放按钮就可以在编辑器预览的动画效果,我们点击③吃就可以看到 添加KeyFrameSet 的子元素的选项,大家可以动手试一试,下面以xml的方式讲解这几种元素
KeyPosition
可以改变动画运动过程中的位置。
上面我们实现的运动都是直线运行,下面我用KeyPosition来实现曲线运动
图中的菱形点就是我们用KeyPosition 修改的点,代码如下
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnClick motion:targetId="@id/box"/>
<KeyFrameSet>
<KeyPosition
motion:framePosition="50"
motion:motionTarget="@id/box"
motion:keyPositionType="parentRelative"
motion:percentY="0.3"
motion:percentX="0.8"/>
</KeyFrameSet>
</Transition>
在上面给KeyPosition 添加了几个属性motion:framePosition
表示动画的进度,取值范围为[0,100]。上面写的50就表示动画进度执行50%的地方。motion:motionTarget
表示修改路径的视图idmotion:keyPositionType
表示坐标系类型,取值可以是parentRelative、pathRelative、deltaRelativemotion:percentY
和 motion:percentX
就是相对参考系的纵向和横向的比例。
重点说一下keyPositionType
1.parentRelative
表示以MotionLayout 布局为参考系,布局左上角为(0,0),右下角为(1,1) 那么motion:percentX="0.8",motion:percentY="0.3"就是(0.3,0.8)的位置,如下图所示
上面我画了 两个图,按我们上面说的(0.3,0.8)应该是图1两条灰色的焦点,但我们发现图1和实际的点有些偏差。我们用KeyPosition 指定点时,是指定视图的中心点,图1是理想状态,如果我们的视图无限小,那就是图1 情况。事实上我们视图不可能无限小,真实的情况应该是图2 所示。
2.deltaRelative
此类型下,视图的起始点坐标为(0,0), 终点坐标为(1,1),示意图如图所示
在这里插入图片描述
<KeyFrameSet>
<KeyPosition
motion:framePosition="50"
motion:motionTarget="@id/box"
motion:keyPositionType="deltaRelative"
motion:percentY="0.5"
motion:percentX="0.8"
/>
</KeyFrameSet>
这次故意将视图终点移动右上角,不然按上面的定义,起始位置和终点位置在统一水平线上,在deltaRelative 下X轴和Y轴就重合了。
3.pathRelative
这个就有意思了,起始点(0,0)是还是视图开始位置,视图的终点位置是(1,0),那么坐标系建立如下
Y轴正方向是,X轴顺时针90度的方向,和Android 中的View坐标系一个味道,就是和我们在学习学的是反的。
KeyAttribute
keyAttribute 它可以让我们改变在动画的过程中某个时刻的属性
例:
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="2000">
<OnClick motion:targetId="@id/box"/>
<KeyFrameSet>
<KeyAttribute
motion:framePosition="50"
motion:motionTarget="@id/box"
android:rotationY="180"
android:alpha="0"
/>
</KeyFrameSet>
</Transition>
上面我们在KeyFrameSet添加了一个KeyAttribute,并指定了一个属性android:rotationY="180",其运行效果如下
可以看到视图先沿Y从0旋转到180,然后再从180 旋转到0 结束,透明的也随着动画进度进行,由1到0,再0到1。
你还可以尝试改变其实属性值试一试。
KeyAttibute 定义的是的Android 中View 原生属性,支持的属性如下
android:visibility
,android:alpha
,android:elevation
,android:rotation
,android:rotationX
,android:rotationY
,android:scaleX
,android:scaleY
,android:translationX
,android:translationY
,android:translationZ
如果是自定义的属性,可以使用KeyAttibute 的子元素CustomAttribute
<KeyFrameSet>
<KeyAttribute
android:rotationY="180"
motion:framePosition="50"
motion:motionTarget="@id/box">
<CustomAttribute
motion:attributeName="rectangleColor"
motion:customColorValue="@color/colorAccent" />
</KeyAttribute>
</KeyFrameSet>
CustomAttribute需要指定两个属性,attributeName 自定义属性的名字,另一个是属性值,可以是颜色、整数、浮点数、字符串、尺寸、布尔值,上面自定义属性是颜色,所以就是使用motion:customColorValue属性来指定其值。
KeyCycle
可以让视图在动画的过程中按照一些周期函数,周期性的改变其属性值
在这里插入图片描述
上面演示是是在动画[0%,80%] 这个过程中以sin这个周期函数,周期性的改变 android:translationY 属性
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="2000">
<OnClick />
<KeyFrameSet>
<KeyCycle
motion:framePosition="80"
motion:motionTarget="@+id/box"
motion:wavePeriod="1"
motion:waveShape="sin"
android:translationY="50dp"/>
</KeyFrameSet>
</Transition>
motion:framePosition="80" 表示KeyCycle 作用范围到动画的80%,motion:wavePeriod 表示运动的周期数,motion:waveShape 表示周期的类型,这是指定的是sin,就会按sin周期函数变化。
KeyTimeCycle
在关键帧上按照一些周期函数,周期性的改变其属性值,效果如下图所示
可以看到 KeyTimeCycle与KeyCycle比较,KeyTimeCycle在帧上做周期性,KeyCycle是在动画过程中做周期性
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="2000">
<OnClick />
<KeyFrameSet>
<KeyTimeCycle
motion:motionTarget="@+id/box"
motion:wavePeriod="1"
android:translationY="50dp"
/>
</KeyFrameSet>
</Transition>
参数和KeyCycle 类似。如果我们指定motion:wavePeriod 的周期数越大,变化频率就会加快。
KeyTrigger
在动画的过程中可以触发视图中的函数,例如
我们自定义一个View,,在这个类中我们定义两个公开的函数 who()
和where()
,代码如下(最简单的自定义View 😄 )
class MyTextView(context: Context?, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {
fun who() {
text = "我是谁"
}
fun where() {
text = "我在那"
}
}
布局代码如下
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
……>
<View android:id="@+id/box"…… />
<com.wkk.motionlayoutdemo.MyTextView
android:id="@+id/myTextView"
…… />
</androidx.constraintlayout.motion.widget.MotionLayout>
MotionScene 核心代码
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="2000">
<OnClick />
<KeyFrameSet>
<KeyTrigger
motion:framePosition="20"
motion:motionTarget="@id/myTextView"
motion:onCross="who" />
<KeyTrigger
motion:framePosition="80"
motion:motionTarget="@id/myTextView"
motion:onCross="where" />
</KeyFrameSet>
</Transition>
framePosition
动画的进度,motionTarget
作用的目标, motion:onCross
要触发的函数名
onCross 是动画不管是正向还是反向,只要到达设置的framePosition 就会执行函数,还有两个函数也会触发, onPositiveCross 只有正向执行动画是到达设置的framePosition 才会执行,而 onNegativeCros 则反之。
上面的定义,就是在动画进度20%的时候触发id 为myTextView 视图的who
方法,动画进度在80%的时候触发id 为myTextView 视图的where
方法,其运行效果如下。
写在最后
MotionLayout 功能强大,但相关知识点也很多,上面只是对一些属性简单的演示,要想实现复杂的动画,还需要多加练习。MotionLayout 有很好的编辑器,刚开始学的时候XML 并不太会写,可以借助图形化的编辑工具点一点,看看它帮我们生成的xml是什么样的,下面我也粘贴了一些参考链接以供学习,MotionLayout 一起学起来吧。
参考链接
MotionLayout 官方文档
MotionLayout/ConstraintLayout 官方示例 (GitHub)
使用 MotionLayout 为 Android 应用添加动画效果 (Codelab)
MotionLayout API