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点。

你可能感兴趣的:(Lottie 动画原理(二))