上一篇讲了lottie整体的动画原理,但主要写的是Lottie 动画中layer的transform动画,这一篇将主要分析layer中shapes动画的原理。
shapes是Lottie动画中最重要的一部分,它负责绘制出每个LOTLayer中的画面,并作出相应的动画。
首先以这个这个拳击小人json为例,它的效果图如下:
打开格式化后的这个json文件,可以发现这个动画由8个layer构成,json如图
每个layer含有一个LOTRenderGroup用于渲染动画效果,LOTRenderGroup又是由一个个连接的节点构成,如下图
先来看头部是如何绘制上去的。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中。
上图中模仿了一个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的大致轮廓绘制出来,如下图所示:
了解了这些之后,回到上文提到的拳击小人头部的描绘过程。根据json 中的‘gr’,‘sh’,‘st’字段中的内容,可知其头部结构如下
其显示流程为:
节点3从json读取线条的基本信息(线宽,线色,等),并使用节点2的outputPath,即可设置节点3(LOTStrokeRender)中的outputLayer的path属性。
注意点:
渲染节点:(LOTRenderNode),动画节点:(LOTAnimatorNode)
5控制着2,3,4这些动画节点的线条颜色,线条宽度,线条端口类型等渲染方式。
动画节点从json中获取路径的方式
上文中提到动画节点给渲染节点提供具体路径的,动画节点是如何把json中的信息转换成path的是一个。以LOTPathAnimator动画节点为例。原理同上一篇文章提到的transform 动画一样,依然是根据关键帧数组,计算出当前的路径值。找到json文件中‘sh’所对应的关键帧数组,会发现是以‘i’,‘o’,‘v’开头的三个数组。如下一个贝塞尔曲线可以由三个点构成,而json中的i,o,v数组就是用于组建贝塞尔曲线的三个点,然后LOTPathAnimator中使用LOTPathInterpolato插值器r计算出当前关键帧(frame)下的leadingframe的i,o,v点,再找到trailingframe下的i,o,v点,从而算出组成当前贝塞尔曲线的i,o,v点。