Lottie 动画原理(二)

上一篇讲了lottie整体的动画原理,但主要写的是Lottie 动画中layer的transform动画,这一篇将主要分析layer中shapes动画的原理。

shapes是Lottie动画中最重要的一部分,它负责绘制出每个LOTLayer中的画面,并作出相应的动画。

首先以这个这个拳击小人json为例,它的效果图如下:

boxing.gif

打开格式化后的这个json文件,可以发现这个动画由8个layer构成,json如图


boxing.json

每个layer含有一个LOTRenderGroup用于渲染动画效果,LOTRenderGroup又是由一个个连接的节点构成,如下图
shapes

先来看头部是如何绘制上去的。json文件中,第一个layer中的shapes数组只有一个字典,这个字典里包含了type为‘gr’,‘sh’,‘st’,‘tr’四个部分,通过在代码上的对应,可以知道前三个部分对应的渲染类分别是‘LOTRenderGroup’,‘LOTPathAnimator’,‘LOTStrokeRenderer’,而‘tr’对应的是LOTRenderGroup上CALayer的transform变化,不属于渲染节点。
依据的代码如下,在LOTRenderGroup类中

- (void)buildContents:(NSArray *)contents {
  LOTAnimatorNode *previousNode = nil;
  LOTShapeTransform *transform;
  for (id item in contents) {
    if ([item isKindOfClass:[LOTShapeFill class]]) {
     //颜色和透明度填充
      LOTFillRenderer *fillRenderer = [[LOTFillRenderer alloc] initWithInputNode:previousNode
                                                                       shapeFill:(LOTShapeFill *)item];
      [self.containerLayer insertSublayer:fillRenderer.outputLayer atIndex:0];
        
      previousNode = fillRenderer;
    } else if ([item isKindOfClass:[LOTShapeStroke class]]) {
    //线条颜色宽度透明度dash offset
      LOTStrokeRenderer *strokRenderer = [[LOTStrokeRenderer alloc] initWithInputNode:previousNode
                                                                          shapeStroke:(LOTShapeStroke *)item];
      [self.containerLayer insertSublayer:strokRenderer.outputLayer atIndex:0];
       
      previousNode = strokRenderer;
    } else if ([item isKindOfClass:[LOTShapePath class]]) {
    //路径动画
      LOTPathAnimator *pathAnimator = [[LOTPathAnimator alloc] initWithInputNode:previousNode
                                                                       shapePath:(LOTShapePath *)item];
      previousNode = pathAnimator;
    } else if ([item isKindOfClass:[LOTShapeRectangle class]]) {
        //圆角动画
      LOTRoundedRectAnimator *rectAnimator = [[LOTRoundedRectAnimator alloc] initWithInputNode:previousNode
                                                                                shapeRectangle:(LOTShapeRectangle *)item];
      previousNode = rectAnimator;
    } else if ([item isKindOfClass:[LOTShapeCircle class]]) {
        //圆形动画,center size 修改
      LOTCircleAnimator *circleAnimator = [[LOTCircleAnimator alloc] initWithInputNode:previousNode
                                                                           shapeCircle:(LOTShapeCircle *)item];
      previousNode = circleAnimator;
    } else if ([item isKindOfClass:[LOTShapeGroup class]]) {
        //动画组
      LOTShapeGroup *shapeGroup = (LOTShapeGroup *)item;
      LOTRenderGroup *renderGroup = [[LOTRenderGroup alloc] initWithInputNode:previousNode contents:shapeGroup.items keyname:shapeGroup.keyname];
      [self.containerLayer insertSublayer:renderGroup.containerLayer atIndex:0];
      previousNode = renderGroup;
    } else if ([item isKindOfClass:[LOTShapeTransform class]]) {
         //shape transform
      transform = (LOTShapeTransform *)item;
    } else if ([item isKindOfClass:[LOTShapeTrimPath class]]) {
      LOTTrimPathNode *trim = [[LOTTrimPathNode alloc] initWithInputNode:previousNode trimPath:(LOTShapeTrimPath *)item];
      previousNode = trim;
    } else if ([item isKindOfClass:[LOTShapeStar class]]) {
      LOTShapeStar *star = (LOTShapeStar *)item;
      if (star.type == LOTPolystarShapeStar) {
        LOTPolystarAnimator *starAnimator = [[LOTPolystarAnimator alloc] initWithInputNode:previousNode shapeStar:star];
        previousNode = starAnimator;
      }
      if (star.type == LOTPolystarShapePolygon) {
        LOTPolygonAnimator *polygonAnimator = [[LOTPolygonAnimator alloc] initWithInputNode:previousNode shapePolygon:star];
        previousNode = polygonAnimator;
      }
    } else if ([item isKindOfClass:[LOTShapeGradientFill class]]) {
      LOTGradientFillRender *gradientFill = [[LOTGradientFillRender alloc] initWithInputNode:previousNode shapeGradientFill:(LOTShapeGradientFill *)item];
      previousNode = gradientFill;
      [self.containerLayer insertSublayer:gradientFill.outputLayer atIndex:0];
    } else if ([item isKindOfClass:[LOTShapeRepeater class]]) {
      LOTRepeaterRenderer *repeater = [[LOTRepeaterRenderer alloc] initWithInputNode:previousNode shapeRepeater:(LOTShapeRepeater *)item];
      previousNode = repeater;
      [self.containerLayer insertSublayer:repeater.outputLayer atIndex:0];
    }
  }
  if (transform) {
    _opacityInterpolator = [[LOTNumberInterpolator alloc] initWithKeyframes:transform.opacity.keyframes];
    _transformInterolator = [[LOTTransformInterpolator alloc] initWithPosition:transform.position.keyframes
                                                                      rotation:transform.rotation.keyframes
                                                                        anchor:transform.anchor.keyframes
                                                                         scale:transform.scale.keyframes];
  }
  _rootNode = previousNode;
}

由这段代码可以知道,LOTRenderGroup在初始化的过程中建立了一个由节点构成的渲染链,在循环的过程中,初始化每个node会将上一次循环生成的node作为参数传入,从而形成这样一条链接。而这些节点分为两大类一类是 LOTAnimatorNode,另一类为LOTRenderNode

LOTAnimatorNode 负责计算构成形状的线条
LOTRenderNode 负责计算线条颜色,线宽,填充色等
LOTRenderNode继承于LOTAnimatorNode,多了outputLayer属性,所以上述代码中所有LOTRenderNode的子类都需要把outLayer 插入到外层layer中。

nodes

上图中模仿了一个LOTRenderGroup中的节点结构,这五个节点既包含了动画节点(LOTAnimatorNode),又包含了渲染节点(LOTRenderNode)。由LOTRenderNode类中可知,其outputlayer的样式由自己决定(线宽,颜色,等),但其本身不包含路径信息,其outputlayer 的path由之前所有LOTAnimatorNode来决定。下面是LOTRenderNode中设置path的代码。

- (void)rebuildOutputs {
  self.outputLayer.path = self.inputNode.outputPath.CGPath;
}

可知LOTRenderNode的path由其inputNodes决定,这里有必要提及一下LOTAnimatorNode类中,localPath和outputPath属性的作用。

/// This nodes path in local object space
@property (nonatomic, strong) LOTBezierPath * _Nonnull localPath;
/// The sum of all paths in the tree including this node
@property (nonatomic, strong) LOTBezierPath * _Nonnull outputPath;

由于LOTRenderNode继承于LOTAnimatorNode,而且其json中并不包含path信息,所以这两个属性完全由其上一个节点的相同属性来决定,如果上一个节点还是LOTRenderNode则继续往上寻找用于进行动画的LOTAnimatorNode节点。

而在LOTAnimatorNode中,localPath由不同子类不同实现,outputPath则是把自己的localPath加上之前总的localPath,所以我们可以通过调用最外层的RenderGroup的outputPath方法将画面轮廓取出来。例如这段代码将所有layer的renderGroup取出并将第一帧绘制出来

LOTComposition *composition = [LOTComposition animationNamed:animateName];
UIBezierPath *path = [UIBezierPath bezierPath];
for (LOTLayer *layer in composition.layerGroup.layers) {
        LOTRenderGroup *renderGroup = [[LOTRenderGroup alloc]initWithInputNode:nil contents:layer.shapes keyname:nil];
        [renderGroup updateWithFrame:@0];
        UIBezierPath *rPath = [UIBezierPath bezierPathWithCGPath:renderGroup.outputPath.CGPath];
        [path appendPath:rPath];
}
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc]init];
shapeLayer.path = path.CGPath;
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:shapeLayer];

可以将json的大致轮廓绘制出来,如下图所示:


屏幕快照 2018-05-07 下午8.04.43.png

了解了这些之后,回到上文提到的拳击小人头部的描绘过程。根据json 中的‘gr’,‘sh’,‘st’字段中的内容,可知其头部结构如下


小人的头部
其显示流程为:

节点3从json读取线条的基本信息(线宽,线色,等),并使用节点2的outputPath,即可设置节点3(LOTStrokeRender)中的outputLayer的path属性。

注意点:

渲染节点:(LOTRenderNode),动画节点:(LOTAnimatorNode)

LOTRenderGroup中每个渲染节点都使用其上一个动画节点的outputPath,而不是使用上一个节点的localPath。也就是说每一次经过一个渲染节点,都是把之前所有的动画节点路径通过这个渲染节点重新渲染一遍,这样做可能导致一条线被设置两遍颜色,只不过显示的时候第一次的被覆盖掉了,这样做到原因是:动画节点和渲染节点并不是一一对应的,一个渲染节点可以对应多个动画节点,如果这些动画节点的渲染方式都相同。例如这种情况
renderNode

5控制着2,3,4这些动画节点的线条颜色,线条宽度,线条端口类型等渲染方式。

动画节点从json中获取路径的方式

上文中提到动画节点给渲染节点提供具体路径的,动画节点是如何把json中的信息转换成path的是一个。以LOTPathAnimator动画节点为例。原理同上一篇文章提到的transform 动画一样,依然是根据关键帧数组,计算出当前的路径值。找到json文件中‘sh’所对应的关键帧数组,会发现是以‘i’,‘o’,‘v’开头的三个数组。如下
sh字段下的路径信息

一个贝塞尔曲线可以由三个点构成,而json中的i,o,v数组就是用于组建贝塞尔曲线的三个点,然后LOTPathAnimator中使用LOTPathInterpolato插值器r计算出当前关键帧(frame)下的leadingframe的i,o,v点,再找到trailingframe下的i,o,v点,从而算出组成当前贝塞尔曲线的i,o,v点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容