导读:Lottie动画是Airbnb开源的一个支持 Android、iOS 以及 ReactNative。通过AE导出的JSON文件+Lottie库可快速实现动画绘制。本文主要讲述将Lottie中的动画拆解成独立图层,并在独立图层中添加动画的过程。

Lottie动画原理概述

android json动画播放 json动画是什么_让一个动画一直执行的属性是

上面这个流程 是 Lottie动画库从AE导出动画到绘制到客户端屏幕的过程,在上一篇文章Lottie动画json文件解析中已经介绍过JSON到Model过程,主要是将JSON转成OC语言可以识别的数据模型Model, Model实际上是一个Object类型的对象,我们可以通过属性key快速查找数据内容,本文主要讲述:Model(数据模型)依附到CALayer(图层)上,就像写一个CALayer一样,把model数据一一赋值给CALayer的属性上,必要时再做特殊处理,最后在图层CALayer上添加Animation(动画)。

Lottie底层原理实际是用到了CALayer 和 Core Animation。我们经常可以直观感受到iOS设备中内容的切换很流畅,就如下图,弹框不是一闪而出,而是有很平滑从小到大和透明度从0到1的过渡效果。这是因为在一个图层中,当我们修改一个图层属性时,比如宽度从100px到200px, 它会产生很平滑地从一个值过渡到下一个值这种动画效果,这个图层就是CALayer, 执行动画效果的是Core Animation,我们将这一行为称为隐式动画。而Lottie使用的正是这种机制。

android json动画播放 json动画是什么_图层_02

绘制图层

lottie绘制图层过程用到了两个主要的类:LOTCompositionContainer 和 LOTLayerContainer。

架构

android json动画播放 json动画是什么_lottie动画_03

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, 在初始化时执行以下步骤:

android json动画播放 json动画是什么_图层_04

  • 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)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的情况。

android json动画播放 json动画是什么_json动画_05

- (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;
}