静若处子动若脱兔-Constraintlayout2.0一探究竟
这篇文章是我去年在公司内部的分享,当时Constraintlayout2.0还没Release,所以只在公司内部进行了分享,希望等Release之后,就可以正式在项目中使用了。
那么为什么我这么热衷于使用constraintlayout呢?从团队项目来说,3年前我刚进公司的时候,做了一次Constraintlayout1.0的分享,让大家了解到了这一强大的布局方式,大家也认识到了这一神器的强大,从那以后,constraintlayout在项目中的使用越来越广泛,以前可能需要很多代码计算才能实现的布局效果,利用Constraintlayout,可能一行Java代码都不用写,直接在XML布局中就能实现,这也体现了科技提高生产力的强大优势。
如果说Constraintlayout1.0是对静态布局的革命,那么这次Constraintlayout2.0的升级,则是对布局中的动画进行了革命,这是对Constraintlayout1.0布局基本形式的强大补充,至此,Constraintlayout几乎可以完全替代原始的布局方式,同时让动画的实现变的异常方便,所以,我会花几篇文章来阐述Constraintlayout2.0的革命之处。
不过,为了偷懒,这篇文章直接使用了去年中分享的内容,当时Constraintlayout2.0还未Release,所以可能有些地方和正式版本有些区别,不过不影响,因为整体是一致的。
这是本系列的第一篇文章,简述了Constraintlayout中MotionLayout的基本使用。
Android系统框架中已经提供下面几种动画:
- Animated Vector Drawable
- Property Animation framework
- LayoutTransition animations
- Layout transitions with TransitionManager
- CoordinatorLayout
MotionLayout基于Constraintlayout2.0,同时有AndroidX和非AndroidX两个版本,最低支持API14,即Android4.0。
MotionLayout的设计初衷是为了简化Android中的过渡动画,因此它几乎可以替代TransitionManager来实现组件间的过渡效果。与传统的Android动画设计方式不同,这次的设计思路完全使用了申明式的UI设方式,MotionLayout完全通过申明约束的方式进行驱动。
通过下面的代码可以直接接入MotionLayout。
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
在创建布局的时候,系统已经默认使用constraintlayout,在界面上可以直接点击convert to motionlayout来进行转换,并生成相关的配置文件。
MotionLayout实际上是Constraintlayout的子类,直接在代码中,将Constraintlayout替换为MotionLayout也是一样的。MotionLayout的整体架构如下图所示。
ConstrainLayout与MotionLayout的主要不同点是,MotionLayout将过渡动画的描述文件放置在另一个xml文件中,这样MotionLayout与ConstrainLayout一样只描述静态的界面UI约束关系。而在独立的xml文件中,描述约束的变化,这个独立的xml就是MotionScene文件,它独立在res/xml文件夹下,一个MotionLayout均对应一个MotionScene。
MotionLayout目前可以通过全手写代码,或者通过Android Studio 4.0+的MotionEditor来进行编写,这里笔者使用AS来进行编写,原因如下:
- MotionLayout的后续发展将深度集成Android Studio,所以直接通过MotionEditor来讲解,更加符合后续发展
- 手写太麻烦了
使用Android Studio创建MotionLayout后,就可以打开MotionEditor了,如图所示。
MotionLayout
MotionLayout作为根布局,其需要做动画的View都必须包含ID,另外,它还具有一些辅助性的属性设置。
- app:applyMotionScene="boolean":是否要启用MotionScene,默认为true
- app:showPaths="boolean":是否绘制运动轨迹的辅助线
- app:progress="float":MotionScene的运动进度
- app:motionDebug:显示额外的调试信息,"SHOW_PROGRESS", "SHOW_PATH", or "SHOW_ALL"
MotionScene
MotionScene是MotionLayout的核心描述文件。其设计架构如图所示。
一个MotionScene文件可以包含动画所需的所用内容:
- 组件的的ConstraintSets
- 组件ConstraintSet之间的transition,即动画过渡
- 关键帧、事件处理
下面通过MotionEditor来创建一个简单的MotionScene。
在默认界面上,创建一个布局,作为初始布局,如图所示。
在界面上,可以像ConstrainLayout一样,建立UI的布局,这个布局,实际上就是作为动画的原始布局,在界面上点击start的界面,就是这个布局,这时候,再点击end界面,就可以在当前布局的基础上,通过修改依赖约束,来创建新的布局,如图所示。
通过这种方式,就创建了动画的起始-结束状态。这时候,点击start-end上面的连接线,就可以预览动画的过渡了,如图所示。
这时候,MotionEditor就已经帮你创建好了MotionScene。
可以发现,这里实际上对动画进行了描述,首先是Transition,定义了动画的起始和结束状态,这里使用的是自包含的MotionScene,即约束的定义直接写在MotionScene中,而不是单独的ConstraintSet文件,这也是MotionEditor推荐的方式。在ConstraintSet中,就是描述的当前状态下的约束关系,这里的一个约束就是将ImageView的在顶部的约束,改成了在底部的约束。
这样就很简单的实现了一个MotionScene,不需要你做任何处理,只要定义好动画的起始-结束约束关系,动画自动就生成了,这也正符合动画的实际概念,即物体状态的改变过程。
触发事件
点击start-end连接线左上角的图标,可以创建click or swipe handler,如图所示。
即点击和滑动事件。
Click handler
Click handler比较简单,指定好targetId即可在点击该ID的View时触发动画。
Click能设置的参数比较简单,clickAction可以设置点击的切换方式,如图所示。
OnSwipe handler
OnSwipe handler相对来说就复杂一些,包含的属性比较多,如图所示。
点击创建时,如图所示。
这些属性的基本解释如下所示。
- touchAnchorId:需要跟踪的对象
- touchAnchorSide:跟踪手指的一侧(right/left/top/bottom)其功能是设置触摸操作将会拖动对象的哪一边,该属性可用于实现可折叠效果
- dragDirection:跟踪手指运动的方向 (dragRight/dragLeft/dragUp/dragDown将决定进度值的变化0-1)
- onTouchUp:决定手指抬起的时候的动作,默认抬手后动画会根据当前进度来选择回退动画或者继续完成动画。
Custom attribute
需要注意的是,在MotionScene中,ConstraintSet只能描述约束的变化,但是对于属性的变化是不能生效的,例如改变背景色,这个时候,就需要使用Custom Attribute,其结构如下所示。
通过指定attributeName及其value,来描述属性的变化,但属性名字需要和对象中的getter/setter方法对应:
- getter: getName (e.g. getBackgroundColor)
- setter: setName (e.g. setBackgroundColor)
下面通过一个例子来改变动画过程中,View的背景色,首先,选中start界面,并选中要改变的View的ID,在右边的CustomAttributes中,点击添加,如图所示。
在弹出界面中,选择color,并指定backgroundColor属性,设置初始颜色,如图所示。
同样的方式,再给end界面创建CustomAttribute,指定动画结束时的背景色。
这个时候,再通过动画预览,就可以发现颜色的动画效果了,此时MotionScene的文件被修改成下面的结构。
其原理实际上就是在Constraint中,增加了描述属性状态改变的CustomAttribute标签。
那么通过CustomAttributes和Constraint,就可以实现组件的尺寸约束以及组件属性的动画过渡效果。
KeyFrame
创建默认的Transition时,Transition从起始状态直接变换到结束状态,其变换路径都是线性的,沿直线进行的运动,但实际上很多动画可以设置更加丰富的细节,这时候,就需要在起始和结束中间插入一些KeyFrame,来丰富动画的运动过程,KeyFrame的属性非常多,如图所示。
MotionLayout支持下面的关键帧类型:
- 位置关键帧 KeyPosition
- 属性关键帧 KeyAttribute
- 循环关键帧 KeyCycle
- 周期关键帧 KeyTimeCycle
所有的关键帧都支持下面的这些参数设置:
- motion:framePosition:关键帧所处的进度百分比
- motion:target:指定ID的组件
- motion:transitionEasing:插值器
- motion:curveFit:拟合曲线
KeyPosition
KeyPosition支持的属性非常多,如图所示。
在Transition界面中,选择添加KeyPosition,如图所示。
这样原本直线运动的动画,就因为KeyPosition的加入而变成了曲线动画,如图所示。
当然,开发者并不需要去设置非常准确的偏移值,在Transition模式下,选中要生成KeyPosition的View,就可以直接拖动曲线来进行编辑运动曲线,如图所示。
所以KeyPosition只需要设置好framePosition即可,这是关键帧所处的位置。
在下面这个例子中,就是设置了25、50、75的framePosition,再拖动成曲线的示例,如图所示。
KeyPosition中的几个关键属性如下所示。
- keyPositionType:它定义了KeyPosition的坐标系类型
- percentX/percentY:当前坐标系下的xy坐标
KeyPosition坐标系
KeyPosition的坐标系共3种,即parentRelative、deltaRelative、pathRelative。不同的坐标系下,xy的值不同,产生的位置变化也不相同,MotionLayout屏蔽了不同坐标系的差别,最终产生了一种统一的变换曲线。
相对父容器(parentRelative)
坐标是根据相对父容器表示的,是一种绝对坐标,与Android View的坐标体系相同,如图所示。
增量定位(deltaRelative)
第二个坐标系通过使用开始/结束位置定义来解决这个问题,开始位置为坐标原点,水平方向为X轴,垂直方向为Y轴。坐标表示起点和终点之间的百分比。如图所示。
相对路径(pathRelative)
最后一个坐标系定义了一个相对于从开始状态到结束状态的直线路径,并支持负坐标,以起始位置为坐标原点,起始位置到结束位置的path为X轴,垂直方向为Y轴,如图所示。
Arc Motion
Arc Motion的作用同样是为了创建曲线运动路径,它与前面提到的使用KeyFrame设置运动关键帧的效果相同,但是不用设置KeyFrame,直接设置属性即可生效,简化了操作步骤。
要注意的是,Arc Motion的设置必须建立在Target对象有水平竖直位移的基础上,否则是没有Arc效果的。
下面这个例子演示了Arc Motion最简单的使用。
首先设置一个从左上角到右下角的Motion Layout。
点击Transition之后,在属性中增加一个pathMotionArc的属性,并设置为startVertical。
这时候运动曲线就变成了下面这张图。
如果将值设置为startHorizontal,则曲线变为下面这样。
startVertical和startHorizontal的区别就是曲线开始的运动方向。
除此之外,Arc Motion还可以和KeyFrame协作使用。让Arc Motion在多个KeyFrame分段之间,产生曲线效果。
首先,在50%处增加一个KeyPosition,然后再给Transition设置Arc Motion,就降整个过程分成了两段,同时对每段运动都产生了Arc Motion,如图所示。
同时,可以在KeyPosition中,针对单个KeyPosition重置Arc Motion,例如下面将50%到100%的KeyPosition设置为相反的曲线。
只需要在KeyPosition中增加pathMotionArc的属性即可,这里还有另外两个属性可以设置,分别是none和flip,分别用于曲线Arc Motion的作用和取之前Arc Motion的相反值的作用。
KeyAttribute
KeyAttribute与CustomAttribute类似,KeyPosition定义了KeyFrame的位置变化关键帧,而KeyFrame的属性变化关键帧,则需要使用KeyAttribute来进行定义。
在Transition界面中,点击创建KeyAttribute,选择需要修改的属性即可,如图所示。
设置好之后,在动画过程中,就增加了变换的中间状态,这个中间状态的属性变化,就是KeyAttribute,如图所示。
生成的MotionScene代码如下所示。
KeyAttribute支持了所有的View属性。
插值器
插值器代表了曲线运动的速率变化,在MotionLayout中,插值器可以设置给ConstraintSets或者Keyframe,同时,插值器支持两种设置方式,一种是使用0-1的cubic bezier参数,另一种是使用内置的几种预设曲线。
http://cubic-bezier.com
插值器的使用要以下几个需要注意的地方:
下的app:transitionEasing属性与 下的app:motionInterpolator属性都可以设置插值器。但是 下的app:transitionEasing只能设置某个组件的插值器,而 定义的是整个动画的插值器 下的app:transitionEasing必须在start和end中都定义
KeyCycle
KeyCycle实际上是KeyPosition的简化版,可以理解为一个波函数生成器,它的几个关键的参数如下。
- wavePeriod:waves的数量
- waveOffset:wave的起始偏移量
- waveShape:波形,可以为sin、cos、方波、锯齿波等等
总结
传统的Android动画为什么难做?
- 布局的限制,Android的布局将每个View限制在了它的Measure范围内,导致突破区域的动画很难做
- 命令式编程,需要制定动画对象的所有行为
- 参数难调,编译时间太长
借助MotionLayout,Google将动画也变成了声明式,所以整个动画的过程,就变成了动画状态的描述,让动画的制作的中间态,都由MotionLayout来生成了。
使用场景
ConstraintLayout是一盘大棋,Google先通过ConstraintLayout来将整个布局打平,再借助MotionLayout来实现动画就自然而然解决了很多原始Android布局的限制。
- 单页面
- 静态元素,动态生成的元素很难融入原有约束
- 可拆分为多个中间态
动态场景正在开发中,不知道Release后是否会有
优劣势
-
使用前必须对ConstraintLayout非常了解,对其布局思想了如指掌。
-
代码编写比较复杂,如果不使用MotionEditor,编写会非常复杂,所以刚入门的时候必须要先通过MotionEditor来了解其布局原理和思想,在熟练掌握后,才能半UI半代码的方式进行改造。
-
比较复杂的UI界面的约束会比较复杂,维护成本比较高,需要在团队中建立比较统一的约束风格。
-
UI与动画进行了分离,MotionLayout将所有的动画逻辑放在了Scene中,跟最早Android布局的写法,将UI和代码进行分离的方式类似,但这种方式在现在的开发模式下,并不是很直观,因为MotionLayout的设计思想已经是声明式UI的设计思路了,注重的是UI的状态的改变,这种方式针对静态的界面很方便,但是对于动态的UI界面,依然很复杂。
- 从源码看Flutter BuildContext的秘密
- 闲言碎语-第八期
- kotlin修炼指南9-Sequence的秘密
- 起点客户端精准化测试的演进之路
- Flutter混编工程之打通纹理之路
- Android壁纸还是B站玩得花
- Flutter布局指南之谁动了我的Key
- Material Components——ShapeableImageView
- JetPack指路明灯—Navigation
- Material Components—预备役选手Transition
- 静若处子动若脱兔-Constraintlayout2.0一探究竟
- Kotlin修炼指南5
- 重走Flutter状态管理之路—Riverpod最终篇
- Material Components——MaterialButton
- ConstraintLayout2.0进阶之路-欢迎新同学
- ConstraintLayout使用场景必知必会
- 重走Flutter状态管理之路—Riverpod进阶篇
- 它来了!Flutter3.0新特性全接触
- 重走Flutter状态管理之路—Riverpod入门篇
- 它来了!Flutter3.0发布全解析