内容简介:导语:Lottie动画是Airbnb开源的一个支持 Android、iOS 以及 ReactNative。通过AE导出的JSON文件+Lottie库可快速实现动画绘制。本文主要讲述从AE的bodymovin插件导出的JSON文件到OC的数据模型,再将数据模型拆解成独立图层,并为图层添加动画的过程。上图是Lottie动画库从AE导出动画到绘制到客户端屏幕的过程,第一阶段是JSON到Model(OC数据模型)的转换过程,主要是将JSON转成OC语言可以识别的数据模型Model, Model实际上是一个Objec
导语:Lottie动画是Airbnb开源的一个支持 Android、iOS 以及 ReactNative。通过AE导出的JSON文件+Lottie库可快速实现动画绘制。本文主要讲述从AE的bodymovin插件导出的JSON文件到OC的数据模型,再将数据模型拆解成独立图层,并为图层添加动画的过程。
Lottie动画原理概述
上图是Lottie动画库从AE导出动画到绘制到客户端屏幕的过程,第一阶段是JSON到Model(OC数据模型)的转换过程,主要是将JSON转成OC语言可以识别的数据模型Model, Model实际上是一个Object类型的对象,我们可以通过属性key快速查找数据内容,第二阶段是Model(数据模型)依附到CALayer(图层)上,就像写一个CALayer一样,把Model数据一一赋值给CALayer的属性上,必要时再做特殊处理,最后在图层CALayer上添加Animation(动画)。
Lottie结构图
上图为Lottie的结构图
-
LOTAnimationView: 承接控制动画的功能,如播放暂停
-
LOTComposition: 主要解析JSON文件内容
-
LOTCompositionContainer: 承载LOTComposition的内容,绘制图层和添加动画
JSON字段解读
一级属性
JSON最外一层的数据,包括一个动画的基础数据:动画帧率、起始/结束关键帧,动画的宽高等,还有子图层的信息和关联的资源信息,如图片,矢量图等。
{ "v": "5.6.10", // bodymovin插件版本 "fr": 25, // 帧率 "ip": 0, // 起始关键帧 "op": 277, // 结束关键帧 "w": 110, // 视图宽度 "h": 110, // 视图高度 "nm": "cloud", // 动画名称 "ddd": 0, // 是否是3D "assets": [...] // 资源集合 "layers": [...] // 图层集合 }
assets 资源集合
assets是一个数组,资源信息包含的是矢量图信息,如形状,大小等等,也包含位图;还可能是预合成层,即对已存在的某些图层进行分组,把它们放置到新的合成中,作为新的一个资源对象,这里layers的对象结构是跟上面一级属性中的layers图层集合是一样的图层结构。
"assets": [ { "id": "image_0", // 图片唯一识别的id,图层获取图片的标识 "w": 167, // 图片的宽度 "h": 165, // 图片的高度 "u": "images/", // 图片的路径 "p": "img_0.png", // 图片的名称 "layers": [] // 预合成层 } ]
layers 图层集合
layers对象也是一个数组,数组中的每个元素对应一个图层,图层信息包括的图层的位置,大小,形状,起始关键帧,结束关键帧等,一个个图层动画叠加起来构成最终的动画效果。
"layers": [ { "ddd": 0, // 是否是3D图层 "ind": 1, // 在AE里的图层标序号 "ty": 4, // 图层类型 "nm": "形状图层 1", // 在AE下的命名 "ks": {}, // 动画属性值,下面有进一步拆解 "shapes": {}, // 矢量图形图层信息,下面有进一步拆解 "ip": 0, // 起始关键帧 "op": 750, // 结束关键帧 "refId: 0, // 引用资源ID "parent": 0, // 父图层的id,默认都添加到根图层上,如果指定了id不为0会寻找父图层并添加到上面 "masksProperties":[], // 蒙版的数组 "w": 100, // 预合成层:宽度 "h": 100, // 预合成层:图层高度 "sw": 0, // 固态层:宽度 "sh": 0, // 固态层:图层高度 "sc": 0 , // 固态层:颜色 } ]
图层类型ty
图层有6种类型,不同类型的图层获取宽高的方式不同,如图片层需要从关联的refId获取asset,从而获取到图片资源的宽高来作为该图层的宽高等,具体如下:
-
0 代表 预合成层:从属性值w和h获取
-
1 代表 固态层:从属性值w和h获取
-
2 代表 图片层:从图片资源属性获取
-
3 代表 空层:从根图层获取
-
4 代表 形状层:从根图层获取
-
5 代表 位置层:从根图层获取
图层动画ks
-
ks属性:这是一个比较关键的属性,包含图层变换transform的信息,包含透明度、位置、锚点、缩放、旋转等。格式如下
"ks": { "o": { // 透明度 "k": 100 }, "p": { // 位置 "k": [ 126.5, 963, 0 ] } }
-
属性对应的值主要通过K值获取, 如上面的例子中透明度o为
100
, 位置p为(126.5,963,0)
-
k对应的值有如下几种情况:
-
数字或3个数字组成的数组:不带动画。表示对应属性的值。比如透明度
100
, 位置(126.5,963,0)
等。 -
数组类型并且数字第一个对象的t有值:带帧动画。第一个对象表示动画开始的属性,第二个对象表示动画结束的属性。通过以下参数可以拼装出关键帧的属性值,关键帧时间点,关键帧之间的时间函数,t表示开始/结束帧,s和e表示开始/结束属性值,i和o决定动画的时间函数。
-
举个例子:
比如下面的动画,是有个矩形从上往下的动画。
从导出的JSON文件截取以下片段:
"ks": { ... "p": { // 位置信息 "a": 1, "k": [ // 数组类型并且数字第一个对象的t有值 { "t": 0, // 起始关键帧 "s": [ 300, 700, 0 ], }, { "t": 49, // 结束关键帧 "s": [ 250, 1800, 0 ] } ], "ix": 2 } },
从以上片段中我们读到位置p的k值是一个数组,并且是带有t的元素, 即为帧动画。从内容我们可以读出关键帧帧为0时,位置信息为(300,700,0) , 变换到关键帧为49时,位置信息变为(250,1800,0)。
图层形状shapes
shape是一个形状图层的数组,对应AE中图层的内容中的形状设置,描述形状的特征,通过描边信息、颜色填充等信息的组合形成一个个矢量图。
"shapes": { "ty": "gr", // 形状的类型 "it": [ { "ty": "rc", "d": 1, "s": { // 形状的大小 "k": [ 450.094, 140.297 ] }, "p": { "k": [ 0, 0 ] }, "nm": "矩形路径 1" } ] }
-
形状类型ty
ty为形状的类型,对应的类型值如下:
gr(ShapeGroup): 图形组合 st(ShapeStroke): 图形描边 fl(ShapeFill): 图形填充 tr(ShapeTransform): 图形变换 sh(ShapePath): 图形路径 rc(RectPath): 矩形路径 el(EllipsePath): 椭圆路径 tm(trimPath): 裁剪路径
生成OC数据模型
LOTComposition类
LOTComposition类是记录动画信息的类,继承 NSObject, 作为整个json文件内容的映射,用于记录所有动画信息的类。在这个类中我们可以看到动画的基础信息,包含创建AE文件时的设置:合成名称、宽高、帧速率(帧/秒),也是JSON文件中一级属性的映射。以下是一个LOTComposition的实例信息:
LOTLayerGroup 和 LOTLayer
从上图我们可以看到两个集合类,LOTLayerGroup记录图层信息的数组,对应JSON对象中layers数组,由一个个LOTLayer组成。一个LOTLayer是一个图层,是一个动画被拆解的最小单位个体。
例如以下云朵动画
可以看出云朵的运动速度是不一样的,因此可以判断他们并不是在一个图层中,而是由多个图层的动画叠加起来的效果,即每个云朵为一个图层, LOTLayer就是记录一个图层单位的信息
以下是一个LOTLayer携带的信息
LOTAssetsGroup 和 LOTAsset
LOTAssetsGroup是记录资源信息,对应JSON对象中的assets数组,若图层需要依赖资源,可以通过自身信息refId关联到对应的资源ID寻找资源。如以上云朵动画,每个云朵即为一个资源,LOTAsset为记录一个资源的信息。
数据模型转为图层
Lottie底层原理实际是用到了CALayer 和 Core Animation。我们经常可以直观感受到iOS设备中内容的切换很流畅,就如下图,弹框不是一闪而出,而是有很平滑从小到大和透明度从0到1的过渡效果。这是因为在一个图层中,当我们修改一个图层属性时,比如宽度从100px到200px, 它会产生很平滑地从一个值过渡到下一个值这种动画效果,这个图层就是CALayer, 执行动画效果的是Core Animation,我们将这一行为称为隐式动画。而Lottie使用的正是这种机制。
图片引用自 https://juejin.im/post/5de481226fb9a0717b5fce84
图层绘制
lottie绘制图层过程用到了两个主要的类:LOTCompositionContainer 和 LOTLayerContainer。
LOTCompositionContainer
-
顾名思义,LOTCompositionContainer 是 LOTComposition的container(容器),承载LOTComposition的内容。LOTComposition是JSON映射的OC数据模型
-
LOTCompositionContainer 继承CALayer , 是一个图层,动画的根图层。我们设定的动画内容,都会放置在这个图层中
-
执行子图层的循环,并且将所有子图层赋在该根图层上
// LOTCompositionContainer.m // ps: 代码有删减 NSArray *reversedItems = [[childGroup.layers reverseObjectEnumerator] allObjects]; // 获取到子图层的数据模型 for (LOTLayer *layer in reversedItems) { child = [[LOTLayerContainer alloc] initWithModel:layer inLayerGroup:childGroup]; // 将子图层数据模型处理的一个个图层 } [self.wrapperLayer addSublayer:child]; // 将子图层添加到该根图层上
LOTLayerContainer
LOTLayerContainer是一个很重要的类,它相当于我们上文中讲到的LOTLayer,也即是整个动画拆解成的最小单元的一个层级,不需要依赖其他图层就可以完整实现自身动画。这是一个继承CALayer的类。
我们可以在这里回顾下CALayer图层绘制时需要做的事情:
-
创建一个CALayer实例:
CALayer *layer = [CALayer layer];
-
添加到根图层:
[self.view.layer addSublayer:layer];
-
创建动画
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"]; animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 0)]; animation.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 0)];
-
给图层添加动画
在Lottie中也一样实现了上面四个步骤:
LOTLayerContainer
类继承CALayer, 在初始化时执行以下步骤:
-
CALayer属性: LOTComposition中有一个属性
CALayer *wrapperLayer
写入当前图层的信息,从类型可以看出是一个CALayer,因此我们可以在CALayer中使用隐式动画,也就是文中开头所讲的内容。 -
添加宽高信息:在LOTComposition初始化时,会先判断当前的layer是什么类型, 图片/立方体/预补偿层,如果是图片,会将图片的宽高,锚点等信息作为该图层
wrapperLayer
的宽高,锚点等。
// LOTLayerContainer.m if (layer.layerType == LOTLayerTypeImage || layer.layerType == LOTLayerTypeSolid || layer.layerType == LOTLayerTypePrecomp) { _wrapperLayer.bounds = CGRectMake(0, 0, layer.layerWidth.floatValue, layer.layerHeight.floatValue); _wrapperLayer.anchorPoint = CGPointMake(0, 0); _wrapperLayer.masksToBounds = YES; DEBUG_Center.position = LOT_RectGetCenterPoint(self.bounds); }
-
添加Transform信息:接下来寻找Transform(位置/旋转/锚点/缩放/透明度)信息,添加在该图层
wrapperLayer
上 -
填充资源:当图层类型为图片时,需要为
wrapperLayer
添加content
属性内容,即图片的内容。_setImageForAsset
方法实现了判断图片类型,并赋值在content
属性上
// LOTLayerContainer.m if (layer.layerType == LOTLayerTypeImage) { [self _setImageForAsset:layer.imageAsset]; }
-
填充图形:当图层类型为形状shape时,shape是对矢量图的信息携带,这在lottie动画中被大量使用。因为矢量图要比位图加载更快,并且也会大大减少对设备内存的使用。这里的
buildContents
方法实现了对矢量图进行描边、填充颜色等操作。
// LOTLayerContainer.m if (layer.layerType == LOTLayerTypeShape && layer.shapes.count) { [self buildContents:layer.shapes]; }
-
如何绘制矢量图
-
初始化LOTRenderGroup,LOTRenderGroup作为一个矢量图形的类,包含了LOTRenderNode 和 LOTAnimatorNode 拥有的属性和方法。
-
渲染节点:LOTRenderNode 类中有属性
CAShapeLayer * _Nonnull outputLayer
,它负责计算线条颜色,线宽,填充色等 -
动画节点:LOTAnimatorNode 计算构成形状的线条
-
遮罩层:判断是否有遮罩层并赋给
wrapperLayer
-
添加到父图层:在上面过程中已经准备好一个CALayer的绘制属性:宽高、转换信息、资源内容、图形绘制内容、遮罩层等。这儿的self.wrapperLayer并非上述几个过程的wrapperLayer,而是根图层中的属性
// LOTCompositionContainer.m [self.wrapperLayer addSublayer:child];
动画合成
CALayer添加动画
在上面讲述到绘制图层,但如何将这些图层变成动画呢,在了解之前我们得先知道CALayer方法重绘响应链与runloop机制,如何让图层重新绘制呈现出新的画面,从而形成动画。
-
layer首次加载时会调用 +(BOOL)needsDisplayForKey:(NSString *)key方法来判断当前指定的属性key改变是否需要重新绘制,默认返回NO
-
当Core Animartion中的key或者keypath等于+(BOOL)needsDisplayForKey:(NSString *)key 方法中指定的key,便会自动调用
setNeedsDisplay
方法 -
当指定key发生更改时,会触发
actionForKey
-
runloop是一个循环处理事件和消息的方法,CATransaction begin和 CATransaction commit 进行修改和提交新事务。
-
每个RunLoop周期中会自动开始一次新的事务,即使你不显式的使用[CATranscation begin]开始一次事务,任何在一次RunLoop运行时循环中属性的改变都会被集中起来,执行默认0.25秒的动画,在runloop快结束时,它会调用下一个事务
display
-
CALayer方法重绘响应链
-
[layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layerDelegate displayLayer:]
-
[layer setNeedDisplay] -> [layer displayIfNeed] -> [layer display] -> [layer drawInContext:] -> [layerDelegate drawLayer: inContext:]
Lottie动画绘制
-
根图层LOTCompositionContainer继承CALayer ,添加Currentframe 属性,给这个属性添加一个CABaseAnimation 动画
-
所有的子Layer根据CurrentFrame 属性的变化
-
子图层layer首次加载时会调用 +(BOOL)needsDisplayForKey:(NSString *)key方法来判断当前指定的属性key改变是否需要重新绘制。在LOTLayerContainer可以看到
needsDisplayForKey
指定了key为currentFrame
时触发重绘
// LOTLayerContainer.m + (BOOL)needsDisplayForKey:(NSString *)key { if ([key isEqualToString:@"currentFrame"]) { return YES; } return [super needsDisplayForKey:key]; }
-
actionForKey
是接收指定key被修改时触发的行为操作,在下面代码中看到当key为currentFrame
时添加一个CABasicAnimation动画
- (id<CAAction>)actionForKey:(NSString *)event { if ([event isEqualToString:@"currentFrame"]) { CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:event]; theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; theAnimation.fromValue = [[self presentationLayer] valueForKey:event]; return theAnimation; } return [super actionForKey:event]; }
-
display
方法是在一个runloop即将结束时调用,主要实现重绘的内容。下面是display
调用的方法,它会根据当前帧是否在该子图层的显示帧范围内,如果不在,则隐藏,否则赋予图层新的动画属性。如下图,当currentFrame在inFrame和outFrame之间时,动画显示,否则隐藏。下图列举了多个Layer的情况,每一个Layer在初始化时已经准备好,时间跟根图层一样从startFrame
到endFrame
, 在这个时间线中会根据inFrame
和outFrame
来判断是否显示
- (void)displayWithFrame:(NSNumber *)frame forceUpdate:(BOOL)forceUpdate { NSNumber *newFrame = @(frame.floatValue / self.timeStretchFactor.floatValue); // if (ENABLE_DEBUG_LOGGING) NSLog(@"View %@ Displaying Frame %@, with local time %@", self, frame, newFrame); BOOL hidden = NO; if (_inFrame && _outFrame) { hidden = (frame.floatValue < _inFrame.floatValue || frame.floatValue > _outFrame.floatValue); } self.hidden = hidden; if (hidden) { return; } if (_opacityInterpolator && [_opacityInterpolator hasUpdateForFrame:newFrame]) { self.opacity = [_opacityInterpolator floatValueForFrame:newFrame]; } if (_transformInterpolator && [_transformInterpolator hasUpdateForFrame:newFrame]) { _wrapperLayer.transform = [_transformInterpolator transformForFrame:newFrame]; } [_contentsGroup updateWithFrame:newFrame withModifierBlock:nil forceLocalUpdate:forceUpdate]; _maskLayer.currentFrame = newFrame; }
至此,每个图层的绘制和动画的添加均准备完毕,Lottie提供了 play
播放动画的方式,实际上就是将根节点的动画添加到根图层上,使其可以开始播放动画。
以上讲述的是从AE导出JSON文件到OC读取后转成Model再到绘制图层动画的过程,这有助于我们理解一个动画的内部结构,可方便后续理解整个动画的运作,也对于我们实践开发中遇到的缺陷或者调优有极大的帮助。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Flutter 动画全解析(动画四要素、动画组件、隐式动画组件原理等)
- Vue 中 CSS 动画原理
- Flutter动画的实现原理浅析
- 动画+原理+代码,解读十大经典排序算法
- Android 属性动画的原理及应用
- Vue中的基础过渡动画原理解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。