MotionLayout
什么是MotionLayout?
MotionLayout(运动布局)
MotionLayout is a new class available in the ConstraintLayout 2.0 library to help Android developers manage motion and widget animation in their application.
MotionLayout 是 ConstraintLayout 2.0 库中提供的一个新类,用于帮助 Android 开发人员在其应用程序中管理运动和组件动画。
正如其名,MotionLayout 首先是一个布局,它实际上是 ConstraintLayout 的一个子类,并建立在其丰富的布局能力之上(可以使用 ConstraintLayout 的各种特性)。
创建 MotionLayout 是为了在布局转换和复杂运动处理之间架起一座桥梁,您可以将其视为属性动画框架、TransitionManager 和 CoordinatorLayout 之间的组合。它允许您描述两个布局之间的转换(如 TransitionManager)。此外,它本质上支持可查找的转换,转换可以完全由触摸驱动,并立即转换到它的任何点(MotionLayout 让布局元素能够在动画过渡期间也能够响应用户的交互)。它支持触摸处理和关键帧,允许开发人员根据自己的需要轻松自定义过渡。
MotionLayout 是完全声明性的,也就是说您可以使用 XML 描述任何转换而不需要代码(如果需要通过代码表达运动,现有的属性动画框架已经提供了一种很好的方法),无论复杂程度如何。
集成与简单使用
集成
添加 ConstraintLayout 依赖项。要在项目中使用 MotionLayout,请向应用的 build.gradle 文件添加 ConstraintLayout 2.0 依赖项。如果您使用了 AndroidX,请添加以下依赖项:
1 | dependencies { |
使用
创建 MotionLayout 文件。MotionLayout 是 ConstraintLayout 的子类,因此您可以通过替换布局资源文件中的类名称,将任何现有的 ConstraintLayout 转换为 MotionLayout,如下面的示例所示:
1 | <!-- before: ConstraintLayout --> |
ConstraintLayout 和 MotionLayout 在 XML 级别上的主要区别是,MotionLayout 所做的实际描述不一定包含在布局文件中。相反,MotionLayout 通常将所有这些信息保存在它引用的单独 XML 文件(一个 MotionScene )中,并且这些信息将优先于布局文件中的信息。这样,布局文件可以只包含视图及其属性,而不包含它们的位置或移动。
在布局中使用 app:layoutDescription 声明一个运动场景文件。
1 | <?xml version="1.0" encoding="utf-8"?> |
创建 MotionScene。MotionScene 是一个 XML 资源文件,其中包含相应布局的所有运动描述。为了将布局信息与运动描述分开,每个 MotionLayout 都引用一个单独的 MotionScene,通常存储在 res/xml 目录下。
一个 MotionScene 文件通常包含指定动画所需的所有内容。
- 使用的 ConstraintSets(约束集)
- 这些 ConstraintSets(约束集) 之间的转换
- 关键帧,点击、触摸处理等
简单案例
要实现上图所示的运动,可以有两种方案。
实现1
引用已有的 layouts
一个指向初始的 layout(view在屏幕左端),一个指向结束的 layout(view在屏幕的右端),我们需要定义两个 layout。
1 | // view在左端的layout: motion_01_cl_start |
然后定义一个 MotionLayout 文件:
1 | <?xml version="1.0" encoding="utf-8"?> |
注意这个 layout 文件指向了一个 MotionScene 文件—scene_01
1 | <?xml version="1.0" encoding="utf-8"?> |
scene_01 通过指定开始和结束的 ConstraintSet (motion_01_cl_start 和 motion_01_cl_end),规定了过渡动画。
实现2
将 ConstraintSets 直接定义在 MotionScene 中
MotionLayout 同样支持直接在 MotionScene 中定义 ConstraintSets,我们定义一个与实现 1 中一样的 MotionLayout 文件,只需要引用 scene_02.xml
1 |
|
在 scene_02.xml 中,Transition 标签内部是一致的,区别在于我们直接将 start 和 end 的 Constrait 定义在了当前文件中,并由一个 ConstraintSet 标签包含着。
1 | <?xml version="1.0" encoding="utf-8"?> |
方案对比
上述两种实现方式,实现1布局文件较多,实现2只需要一个文件,就可以包括所有的 ConstraintSet,相对更清晰。另外实现1的方式在 xml 布局预览时无法预览 start 和 end 场景(当然也无法预览动画),而实现2可以。
MotionLayout 配置项
- app:applyMotionScene=”boolean” 表示是否应用 MotionScene。此属性的默认值为 true
- app:showPaths=”boolean” 表示在运动进行时是否显示运动路径。此属性的默认值为 false
- app:progress=”float” 可让您明确指定进度。您可以使用从 0(转换开始)到 1(转换结束)之间的任意浮点值
- app:currentState=”reference” 可以指定具体的 ConstraintSet
- app:motionDebug 可让您显示与运动有关的其他调试信息。可能的值为“SHOW_PROGRESS”、“SHOW_PATH”或“SHOW_ALL”
MotionScene
ConstraintSet
它是存放 View 约束和属性的的集合,描述 View 约束和属性是通过 Constraint 标签。一般情况下一个动画有两个节点 start 和 end,start 节点作为动画开始时的状态,end 节点作为动画结束时的状态。
Constraint 的属性完全等同于 ConstraintLayout 的属性。
除了位置和边界之外,可配置的属性包括:
- alpha
- visibility
- elevation
- rotation、rotationX、rotationY
- translationX、translationY、translationZ
- scaleX、scaleY
自定义属性:您可以使用 CustomAttribute 来实现自定义的属性,一个 CustomAttribute 本身包含两个属性:
- motion:attributeName 是必需属性,并且属性需要在类中提供相应的 get、set 方法。例如,backgroundColor 受支持,因为我们的视图具有基本的 getBackgroundColor() 和 setBackgroundColor() 方法。
- motion:customColorValue(或 Integer、Float、String、Dimension、Boolean)
- motion:customColorValue 适用于颜色
- motion:customIntegerValue 适用于整数
- motion:customFloatValue 适用于浮点值
- motion:customStringValue 适用于字符串
- motion:customDimension 适用于尺寸
- motion:customBoolean 适用于布尔值
ConstraintOverride:对于非 layout_constraintXXX_toXXXOf 的约束,可以使用 ConstraintOverride 来直接覆写,这样可以少写很多重复的约束。
代码示例:
1 | <ConstraintSet android:id="@+id/start"> |
OnClick
点击事件。
- app:targetId 目标 View 的 id,点击操作的对象
- app:clickAction 设置点击时执行的动作,此属性可以设置以下几个值
- toggle:默认值。在 Start 场景和 End 场景之间切换。
- transitionToStart:过渡到 Start 场景。
- transitionToEnd:过渡到 End 场景。
- jumpToStart:跳到 Start 场景(不执行过渡动画)。
- jumpToEnd:跳到 End 场景(不执行过渡动画)。
OnSwipe
由用户滑动触发,它会根据用户滑动行为调整动画的进度。
- app:touchAnchorId 目标 View 的 id,设置滑动操作要关联到的对象
- app:touchAnchorSide 用户滑动界面时 MotionLayout 将尝试在 touchAnchorId 指定的视图和用户手指之间保持恒定的距离,而此属性指定的是手指和 view 的哪一侧保持恒定的距离。有以下可选值:
- top
- start
- left
- right
- end
- bottom
- middle
- app:dragDirection:设置拖动的方向。例如,motion:dragDirection=”dragRight” 表示当您向右拖动时,进度会增加。有以下可选值:
- dragUp:手指从下往上拖动(↑)
- dragDown:手指从上往下拖动(↓)
- dragLeft:手指从右往左拖动(←)
- dragStart:手指从右往左拖动(←)
- dragRight:手指从左往右拖动(→)
- dragEnd:手指从左往右拖动(→)
- app:maxVelocity:设置动画在拖动时的最大速度(单位:像素每秒 px/s)。
- app:maxAcceleration:设置动画在拖动时的最大加速度(单位:像素每二次方秒 px/s^2)。
- app:dragScale: 控制视图相对于滑动长度的移动距离。默认值为 1,表明视图移动的距离应与滑动距离一致。如果 dragScale 小于 1,视图移动的距离会小于滑动距离(例如,dragScale 为 0.5 意味着如果滑动移动 4 厘米,目标视图会移动 2 厘米)。如果 dragScale 大于 1,视图移动的距离会大于滑动距离(例如,dragScale 为 1.5 意味着如果滑动移动 4 厘米,目标视图会移动 6 厘米)。
Transition
- Transition 包含运动的基本定义。
- constraintSetStart 定义运动的初始状态,这可以是 ConstraintSet 的 id,也可以是布局文件(要指定 ConstraintSet,请将此属性设置为@+id/constraintSetId;要指定布局,请设置为@layout/layoutState)。
- constraintSetEnd 定义运动的最终状态,取值同 constraintSetStart
- duration 定义运动的持续时间,以毫秒为单位
- motionInterpolator 定义运动插值器
- easeIn 缓入
- easeOut 缓出
- easeInOut 缓入缓出
- linear 线性
- bounce 弹跳效果
- autoTransition 自动过渡
- none 不运动
- jumpToStart 直接跳到开始
- jumpToEnd 直接跳到结束
- animateToStart 执行动画到开始
- animateToEnd 执行动画到结束
KeyFrameSet
默认情况下,运动会从初始状态进行到结束状态。通过使用 KeyFrameSet 您可以构建更复杂的动作,我们可以在动画运动过程中的某些点设置特定的约束或属性,MotionLayout 从起始状态到这些节点,再到结束状态,使视图平滑地过渡。
KeyFrameSet 中可以包含 KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger。
KeyFrameSet 可以实现更精确的动画控制
KeyPosition
指定运动序列中特定时刻的视图位置。此属性用于调整运动的默认路径。
用 KeyPosition 来实现曲线运动。
图中的菱形点就是我们用 KeyPosition 修改的点,代码如下:
1 | <Transition |
- framePosition 表示动画的进度,取值范围为[0,100]。上面的50就表示动画进度执行50%的地方
- motionTarget 表示修改路径的视图 id
- percentY 和 percentX 就是相对参考系的纵向和横向的比例
- keyPositionType 表示坐标系类型,取值可以是 parentRelative、pathRelative、deltaRelative
重点介绍一下 keyPositionType
- 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 所示。
- deltaRelative
此类型下,视图的起始点坐标为(0,0), 终点坐标为(1,1),如图所示:
1 | <KeyFrameSet> |
- pathRelative
起始点(0,0)还是视图开始位置,视图的终点位置是(1,0),如下图:
Y 轴正方向是,X 轴顺时针 90 度的方向。
KeyAttribute
keyAttribute 它可以让我们改变在动画的过程中某个时刻的属性。支持的属性如下 android:visibility, android:alpha, android:elevation, android:rotation, android:rotationX, android:rotationY, android:scaleX, android:scaleY, android:translationX, android:translationY, android:translationZ。如果是自定义的属性,可以使用 KeyAttibute 的子元素 CustomAttribute。
示例如下:
1 | <Transition |
上面我们在 KeyFrameSet 添加了一个 KeyAttribute,并指定了属性android:rotationY=”180” 和 android:alpha=”0”,其运行效果如下:
KeyCycle
可以让视图在动画的过程中按照一些周期函数,周期性的改变其属性值。
上图演示是在动画[0%,80%] 这个过程中以 sin 这个周期函数,周期性的改变 android:translationY 属性。
1 | <KeyCycle |
motion:framePosition=”80” 表示 KeyCycle 作用范围到动画的 80%,motion:wavePeriod 表示运动的周期数,motion:waveShape 表示周期的类型,这是指定的是 sin,就会按 sin 周期函数变化(可指定 sin、cos、bounce、square、triangle、sawtooth、reveresSawtooth)。motion:waveOffset 表示偏移量。
KeyTimeCycle
在关键帧上按照一些周期函数,周期性的改变其属性值,如下图所示:
与 KeyCycle 比较,KeyTimeCycle 在帧上做周期性,KeyCycle 是在动画过程中做周期性。
KeyTrigger
在动画的过程中可以触发视图中的函数。例如我们自定义一个 View,在这个类中我们定义两个公开的函数 who() 和 where()。
1 | public class KeyTriggerTextView extends androidx.appcompat.widget.AppCompatTextView { |
KeyTrigger 设置代码如下:
1 | <KeyTrigger |
framePosition 动画的进度,motionTarget 作用的目标, motion:onCross 要触发的函数名(其中 onCross 可取值 onCross、onPositiveCross、onNegativeCros)。
onCross 是动画不管是正向还是反向,只要到达设置的 framePosition 就会执行函数,还有两个函数也会触发, onPositiveCross 只有正向执行动画是到达设置的 framePosition 才会执行,而 onNegativeCros 则反之。
Arc Motion
在 ConstraintLayout 2.0.0 alpha 2 中,引入了一种实现完美圆弧运动的新方案——而且它更加容易使用。你只需要添加 motion:pathMotionArc 属性,从而让默认的线形运动 (linear motion) 切换到弧线运动 (arc motion) 。
让我们来看一个简单的例子,开始状态是屏幕的右下,结束的位置是屏幕的顶部并且水平居中。添加下面这个属性就可以产生弧线运动:
motion:pathMotionArc=”startHorizontal”
如果把属性换成:
motion:pathMotionArc=”startVertical”
就会改变弧线的方向:
你仍然可以使用位置关键帧来创造更复杂的弧线路径。如下:
它是通过在动画中添加一个垂直居中的位置关键帧来实现的:
1 | <KeyPosition |
通过设置 motion:pathMotionArc 属性,还可以在该场景中使用关键帧来更改圆弧的方向。属性可以是 flip (翻转当前的圆弧方向)、none (还原为线性运动),也可以是 startHorizontal 或 startVertical。
1 | <KeyPosition |
1 | <KeyPosition |
Easing
时间模型(Easing)
你可以使用 motion:transitionEasing 属性来改变动画改变的速率。你可以将这个属性使用在 ConstraintSet 或关键帧,它接受这些值:standard, accelerate, decelerate, linear。
- Standard easing
通常用于在非触摸驱动的动画中。它最适合于开始和结束都是静止状态的元素。
- Accelerate easing
加速通常用于一个元素移出屏幕。
- Decelerate easing
减速通常用于一个元素进入屏幕。
其它
ViewTransition
详情请参考官方文档:
MotionEffect
详情请参考官方文档:
Carousel
详情请参考官方文档:
MotionLabel
MotionLayout 工作台
MotionLayout 是 ConstraintLayout 的子类,所以可以放心的将 ConstraintLayout 转换为 MotionLayout。
打开布局,在 Component Tree 栏选中选择 ConstraintLayout,右击在菜单栏中点击 Covert to MotionLayout 选项即可将 ConstraintLayout 转换为 MotionLayout。
通过此操作,会将 ConstraintLayout 重命名为 MotionLayout,并在布局下自动添加 app:layoutDescription=”@xml/xxx_scene” 并自动创建 xxx_scene.xml 文件(该文件为一个 MotionScene 文件,文件内容如下代码)。xxx 即为当前 xml 的名称。当然,您也可以手动重命名并且手动创建 MotionScene 文件。
1 | <?xml version="1.0" encoding="utf-8"?> |
Android studio 中 MotionLayout 的可视化的动画编辑工具,如下图所示:
- 普通状态,选中后预览视图显示原始的状态(即界面上所包含的所有控件)
- 表示 id=”start” 的 ConstraintSet,选中后预览视图显示此约束集的布局
- 表示 id=”end” 的 ConstraintSet,选中后预览视图显示此约束集的布局
- 表示从 start 约束集到 end 约束集的转场,对应 xml 中的 Transition 标签
- 创建一个新的 ConstraintSet
- 创建一个新的 Transition
- 创建触发转场的行为,是点击 click 还是滑动 swipe
- 点击后有个帮助教程
- 表示约束集的元素
点击上图中 4 这条转场线,还可以对 Transition 进行预览和编辑。
我们点击选中①,下面会出现一个时间轴的工具(可以逐帧拖动预览),点击②处播放按钮就可以直接预览动画效果(还有各种倍速可选),点击③处就可以看到添加 KeyFrameSet 的子元素的选项,可以进行添加 KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger。
MotionLayout 代码相关
场景切换
1 | mMotionLayout.transitionToStart(); |
设置进度
1 | mMotionLayout.setProgress(50); |
设置动画时间
1 | // 设置0无效,源码中最小值为8 |
设置 Listener
1 | // 设置监听,监听进度、状态 |
几个主要的类
- MotionLayout(androidx.constraintlayout.motion.widget.MotionLayout)
- MotionScene (androidx.constraintlayout.motion.widget.MotionScene)
- MotionController (androidx.constraintlayout.motion.widget.MotionController)
主要的入口方法
具体请看源码。
1 | // 加载 LayoutDescription |
MotionLayout 使用相关问题
Transition 成对与切换相关问题
同一个 MotionScene 写多组 Transition,且这些 Transition 对应的 constraintSetStart 和 constraintSetEnd 是同一对场景(如均为 start 和 end),则多组 Transition 内的 OnClick 事件有效(但是因为是同一对场景,各个点击事件会互相影响,需要注意且不建议这么写–概括就是不要用多个点击事件控制同一组场景)。如果多组 Transition 对应的场景不是同一对场景(比如第一对 Transition 对应的是 start 和 end,第二对 Transition 对应的是 start2 和 end2,则非第一对 Transition 下的 OnClick 事件无效)。解决这种问题的方案是:不在 Transition 中定义 OnClick,使用代码定义点击事件,然后使用代码来切换场景)。【具体参见 demo 中的 MotionActivityArcMotion 使用】
可以使用代码来判断当前是哪个场景:
1 | // 判断当前场景是否是 end2 |
在一个 MotionScene 中定义多个[多组]场景时,需要在每个场景对每个控件进行充分约束,确保该场景下的控件位置是符合预期的,而不能省略对某个控件的约束,否则就会导致在切换某个场景时控件位置发生不符合预期的变化。
如果使用一个 MotionLayout 布局(一个 MotionScene)定义多组(一个开始和一个结束为一对,多对动画为多组)动画,一是需要注意在每个场景对每个控件的充分约束,二是需要注意每个场景切换对其它场景带来的影响。即便是这样,每个场景的状态仍是不可控的。比如现在有两个场景,场景1是有开始和结束两种状态的,场景2也是有开始和结束两种状态的,他们所有的状态组合是4种,如果我们只定义了默认的两种,显然是覆盖不全的,这就会造成状态的切换可能不符合预期,但是如果我们定义4种状态,再由代码去判断将会非常繁琐,同时场景越多,组合也会爆炸式增长(四个场景就有4X4=16种组合),显然是不符合实际也不能这么做。
所以,建议是:
- 使用 ViewTransition
- 使用多个 MotionLayout 布局,各自做各自的事情,互不干扰
使用 progress 调节进度及场景立即切换问题
我们知道,MotionLayout 是状态(场景)之间平滑过渡的布局,如果有多个场景,且在某个场景切换下我不想要平滑的过渡而是想要立即完成,该怎么设置呢?
首先运动布局有切换场景的方法:transitionToState() ,另外既然是动画,那我们再设置动画的时间为0不就可以实现立即切换场景了吗?
1 | mMotionLayout.setTransitionDuration(0); |
想法是美好的,但是设置后发现并不能立即切换到目标状态,还是有动画的。通过 setTransitionDuration() 的源码发现,里边最小的时间值为8,所以设置为0是无效的。那么我们就没办法设置了吗?
当然不是,既然场景的切换是平滑过渡的,同时也是有进度的,而且是可以通过设置进度来来控制场景状态的,那么直接设置进度为1(或者为0,根据需求)是不是就可以了?
1 | mMotionLayout.transitionToState(R.id.progress_end); |
是的,结果是符合我们预期效果的,所以可以使用 setProgress() 来实现场景状态的无动画切换。