静若处子动若脱兔-Constraintlayout2.0一探究竟

语言: CN / TW / HK

这篇文章是我去年在公司内部的分享,当时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来进行转换,并生成相关的配置文件。

4a07a4a744e1d86bac51de2c05bfb709

MotionLayout实际上是Constraintlayout的子类,直接在代码中,将Constraintlayout替换为MotionLayout也是一样的。MotionLayout的整体架构如下图所示。

d60f65f6b8eb6e6b2a2271c588503df3

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了,如图所示。

ce936297fa2f0e4bec4cf036d1942bbf

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的核心描述文件。其设计架构如图所示。

95e62d6fc7633090237fc90e15e4d34f

一个MotionScene文件可以包含动画所需的所用内容:

  • 组件的的ConstraintSets
  • 组件ConstraintSet之间的transition,即动画过渡
  • 关键帧、事件处理

下面通过MotionEditor来创建一个简单的MotionScene。

在默认界面上,创建一个布局,作为初始布局,如图所示。

b8de2beb7ee7707897e8d3688d6fc95c

在界面上,可以像ConstrainLayout一样,建立UI的布局,这个布局,实际上就是作为动画的原始布局,在界面上点击start的界面,就是这个布局,这时候,再点击end界面,就可以在当前布局的基础上,通过修改依赖约束,来创建新的布局,如图所示。

992ce311dfc50f6a74bd9f924f829e6c

通过这种方式,就创建了动画的起始-结束状态。这时候,点击start-end上面的连接线,就可以预览动画的过渡了,如图所示。

f9ebdf0622f63ed5b78e533a64754992

这时候,MotionEditor就已经帮你创建好了MotionScene。

90178df070b39c060880cd6d550b5e3d

可以发现,这里实际上对动画进行了描述,首先是Transition,定义了动画的起始和结束状态,这里使用的是自包含的MotionScene,即约束的定义直接写在MotionScene中,而不是单独的ConstraintSet文件,这也是MotionEditor推荐的方式。在ConstraintSet中,就是描述的当前状态下的约束关系,这里的一个约束就是将ImageView的在顶部的约束,改成了在底部的约束。

这样就很简单的实现了一个MotionScene,不需要你做任何处理,只要定义好动画的起始-结束约束关系,动画自动就生成了,这也正符合动画的实际概念,即物体状态的改变过程。

触发事件

点击start-end连接线左上角的图标,可以创建click or swipe handler,如图所示。

72349fe707cdfaf17abcfd650b685310

即点击和滑动事件。

Click handler

Click handler比较简单,指定好targetId即可在点击该ID的View时触发动画。

0cc07c231622ce77690e40922fdb9e48

Click能设置的参数比较简单,clickAction可以设置点击的切换方式,如图所示。

2b43544a8c53d9ec3f2095e595ae5953

OnSwipe handler

OnSwipe handler相对来说就复杂一些,包含的属性比较多,如图所示。

4fc08a44bb38905eb880ac234b442a78

点击创建时,如图所示。

116c30630b85e946fe19108ed473f08e

这些属性的基本解释如下所示。

  • touchAnchorId:需要跟踪的对象
  • touchAnchorSide:跟踪手指的一侧(right/left/top/bottom)其功能是设置触摸操作将会拖动对象的哪一边,该属性可用于实现可折叠效果
  • dragDirection:跟踪手指运动的方向 (dragRight/dragLeft/dragUp/dragDown将决定进度值的变化0-1)
  • onTouchUp:决定手指抬起的时候的动作,默认抬手后动画会根据当前进度来选择回退动画或者继续完成动画。

Custom attribute

需要注意的是,在MotionScene中,ConstraintSet只能描述约束的变化,但是对于属性的变化是不能生效的,例如改变背景色,这个时候,就需要使用Custom Attribute,其结构如下所示。

77eab057e7177400f680a7b88d77f329

通过指定attributeName及其value,来描述属性的变化,但属性名字需要和对象中的getter/setter方法对应:

  • getter: getName (e.g. getBackgroundColor)
  • setter: setName (e.g. setBackgroundColor)

下面通过一个例子来改变动画过程中,View的背景色,首先,选中start界面,并选中要改变的View的ID,在右边的CustomAttributes中,点击添加,如图所示。

de1b1cad3a3995324aaf40c5dc69ff5e

在弹出界面中,选择color,并指定backgroundColor属性,设置初始颜色,如图所示。

cecd582de73b1ea07816132771ffa567

同样的方式,再给end界面创建CustomAttribute,指定动画结束时的背景色。

这个时候,再通过动画预览,就可以发现颜色的动画效果了,此时MotionScene的文件被修改成下面的结构。

7c2c65d565d08f8c3ec249cdd2a34253

其原理实际上就是在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支持的属性非常多,如图所示。

3168345744a37ab9eed230e98fe0de85

在Transition界面中,选择添加KeyPosition,如图所示。

797b1f5c6f2ded8bcabcd095adfd3883

这样原本直线运动的动画,就因为KeyPosition的加入而变成了曲线动画,如图所示。

5fdf45b69342d523f55e3c14e25129c2

当然,开发者并不需要去设置非常准确的偏移值,在Transition模式下,选中要生成KeyPosition的View,就可以直接拖动曲线来进行编辑运动曲线,如图所示。

da46c4d40ce1e0ab9a9f339743e9c440

所以KeyPosition只需要设置好framePosition即可,这是关键帧所处的位置。

在下面这个例子中,就是设置了25、50、75的framePosition,再拖动成曲线的示例,如图所示。

91a6ead2bd6332bab70164daf94f9eed

KeyPosition中的几个关键属性如下所示。

  • keyPositionType:它定义了KeyPosition的坐标系类型
  • percentX/percentY:当前坐标系下的xy坐标

KeyPosition坐标系

KeyPosition的坐标系共3种,即parentRelative、deltaRelative、pathRelative。不同的坐标系下,xy的值不同,产生的位置变化也不相同,MotionLayout屏蔽了不同坐标系的差别,最终产生了一种统一的变换曲线。

相对父容器(parentRelative)

坐标是根据相对父容器表示的,是一种绝对坐标,与Android View的坐标体系相同,如图所示。

eff666896c08e9cd742dfc9a74d3ec0e

增量定位(deltaRelative)

第二个坐标系通过使用开始/结束位置定义来解决这个问题,开始位置为坐标原点,水平方向为X轴,垂直方向为Y轴。坐标表示起点和终点之间的百分比。如图所示。

d85ef4f5806510cb2002de7a828b4812

相对路径(pathRelative)

最后一个坐标系定义了一个相对于从开始状态到结束状态的直线路径,并支持负坐标,以起始位置为坐标原点,起始位置到结束位置的path为X轴,垂直方向为Y轴,如图所示。

35465a6bbd676632368ba1b8a8dc1d1b

Arc Motion

Arc Motion的作用同样是为了创建曲线运动路径,它与前面提到的使用KeyFrame设置运动关键帧的效果相同,但是不用设置KeyFrame,直接设置属性即可生效,简化了操作步骤。

要注意的是,Arc Motion的设置必须建立在Target对象有水平竖直位移的基础上,否则是没有Arc效果的。

下面这个例子演示了Arc Motion最简单的使用。

首先设置一个从左上角到右下角的Motion Layout。

点击Transition之后,在属性中增加一个pathMotionArc的属性,并设置为startVertical。

ee0ee836f3b28ea38a0c180ba38edca4

这时候运动曲线就变成了下面这张图。

8bceb71914209442782d696f3ecc0284

如果将值设置为startHorizontal,则曲线变为下面这样。

64c7d7fade24a95aeb08fa70314b819b

startVertical和startHorizontal的区别就是曲线开始的运动方向。

除此之外,Arc Motion还可以和KeyFrame协作使用。让Arc Motion在多个KeyFrame分段之间,产生曲线效果。

首先,在50%处增加一个KeyPosition,然后再给Transition设置Arc Motion,就降整个过程分成了两段,同时对每段运动都产生了Arc Motion,如图所示。

5458851c975023334faf7883aed44d57

同时,可以在KeyPosition中,针对单个KeyPosition重置Arc Motion,例如下面将50%到100%的KeyPosition设置为相反的曲线。

b1224c32ef3a204ea03c9c4b5f9835ac

只需要在KeyPosition中增加pathMotionArc的属性即可,这里还有另外两个属性可以设置,分别是none和flip,分别用于曲线Arc Motion的作用和取之前Arc Motion的相反值的作用。

KeyAttribute

KeyAttribute与CustomAttribute类似,KeyPosition定义了KeyFrame的位置变化关键帧,而KeyFrame的属性变化关键帧,则需要使用KeyAttribute来进行定义。

c431220f7b151a2282250590db89ddca

在Transition界面中,点击创建KeyAttribute,选择需要修改的属性即可,如图所示。

7fa8c0cd36b054ee07fbf666e35659a8

设置好之后,在动画过程中,就增加了变换的中间状态,这个中间状态的属性变化,就是KeyAttribute,如图所示。

3ba6558c8e0f779eaee40f0bf0350062

生成的MotionScene代码如下所示。

5e6a68822b1d5f677a20b5e4877d45b2

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后是否会有

优劣势

  1. 使用前必须对ConstraintLayout非常了解,对其布局思想了如指掌。

  2. 代码编写比较复杂,如果不使用MotionEditor,编写会非常复杂,所以刚入门的时候必须要先通过MotionEditor来了解其布局原理和思想,在熟练掌握后,才能半UI半代码的方式进行改造。

  3. 比较复杂的UI界面的约束会比较复杂,维护成本比较高,需要在团队中建立比较统一的约束风格。

  4. UI与动画进行了分离,MotionLayout将所有的动画逻辑放在了Scene中,跟最早Android布局的写法,将UI和代码进行分离的方式类似,但这种方式在现在的开发模式下,并不是很直观,因为MotionLayout的设计思想已经是声明式UI的设计思路了,注重的是UI的状态的改变,这种方式针对静态的界面很方便,但是对于动态的UI界面,依然很复杂。