Lottie动画使用及原理分析

1.Lottie是什么?

Lottie是Airbnb开源的一个动画渲染库,支持多平台,包括iOS、Android、React Native以及Flutter(https://github.com/airbnb/lottie-ios)。除了官方支持的平台,更有大神实现了支持Windows、Qt、Skia以及React、Vue、Angular等平台,感兴趣的可以去github搜罗一番。

Lottie动画产生的流程如下:
Lottie动画使用及原理分析_第1张图片

整体工作流程为:
1)动效设计师使用After Effects制作动画,然后使用Bodymovin导出JSON文件,可以将JSON文件放到Bodymovin网站上运行看效果,也可以放在lottiefiles网站上运行看效果,而且lottiefiles有很多免费动画JSON资源可以下载看。
2)各个端使用对应的LottieSDK加载JSON文件,实现动画效果。

注意点:Lottie 3.0之后已经全部使用swift实现,所以如果需要使用Objective-C版本需要使用Lottie 2.5.3版本!

2.为什么使用Lottie?

Lottie出现之前:
1> 使用GIF,占用内存大,某些动画显示效果需要进行屏幕适配,安卓原生不支持GIF动画显示。
2> 使用帧动画,同样占用空间大,依然有屏幕适配的问题。
3> 原生实现组合式动画,需要写打量代码实现复杂动画效果,对技术要求比较高。

Lottie可以解决的问题:
1> 开发人员无需编写动画,只需加载
2> 多平台支持,一次设计多端使用
3> 解决设计提供的动效与实现不一致问题
4> 因为只是加载json文件,占用空间更小
5> 专业的人做专业的事,设计师安心做酷炫动画,开发者专心写逻辑

3.Lottie适用于哪些场景?

首先不是所有的动画都可以用Lottie来实现,一些通过属性动画实现的简单动画不需要Lottie实现,或者是有交互的动画Lottie实现不了。我们使用Lottie动画可以替代一些用代码实现很复杂的不带交互的动效,替代GIF动画和帧动画,具体可以使用在以下场景:

1> 启动(splash)动画:典型场景是APP logo动画的播放
2> 上下拉刷新(refresh)动画:所有APP都必备的功能,利用 Lottie可以做的更加简单酷炫
3> 加载(loading)动画:典型场景是网络请求的loading动画
4> 提示(tips)动画:典型场景是空白页的提示
5> 按钮(button)动画:典型场景如switch按钮、编辑按钮、播放按钮等按钮的动画
6> 礼物(gift)动画:典型场景是直播类APP的高级动画播放
等等。。。

4.Lottie的使用和原理

4.1 Lottie使用

Lottie动画使用及原理分析_第2张图片

如上图,首先第一步,将动效师输出的json文件以及需要的images文件添加到工程中(建议使用单独文件夹专门进行Lottie源文件的管理),导出的json文件名称默认都是data.json,需要我们根据功能重新命名。

我们先来感受一下这个json文件是个什么样,这只是一个json文件的一部分而已哦

接下来需要写代码来加载动画。

1)最简单的加载方式:

LOTAnimationView *animation = [LOTAnimationView animationNamed:@"data"];
[self.view addSubview:animation];
[animation playWithCompletion:^(BOOL animationFinished) {
  // Do Something when finished
}];

2)通过url来加载:

这种情况一般是将动画效果存储在服务端或者从LottieFiles网站动态加载动效。

LOTAnimationView *animation = [[LOTAnimationView alloc] initWithContentsOfURL:[NSURL URLWithString:URL]];
[self.view addSubview:animation];

3)通过xib或者sb来加载:

Lottie动画使用及原理分析_第3张图片

如上图的viewA,指定viewA类型为LOTAnimationView,并在箭头所指处设置animationName为对应的动画json文件名,代码中只需要调用play即可开始执行动画。

4)当然还有很多其他的方式加载lottie动画

/// 从一个反序列化的json字典加载
+ (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));

/// 从一个特定的文件路径加载,但注意不是网络url
+ (nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath NS_SWIFT_NAME(init(filePath:));

/// 使用LOTComposition创建animation,从特定bundle中加载需要的图片资源
- (nonnull instancetype)initWithModel:(nullable LOTComposition *)model inBundle:(nullable NSBundle *)bundle;

5)我们可以控制动画的进度,制作有交互的动效

CGPoint translation = [gesture getTranslationInView:self.view];
CGFloat progress = translation.y / self.view.bounds.size.height;
animationView.animationProgress = progress;

除了控制进度,lottie中还有许多的属性提供给我们以控制动画,包括动画速度、持续时长、是否重复执行、是否反向执行、是否缓存动画等,查看LOTAnimationView头文件就能找到它们。

4.2 Lottie原理

接下来就是我们的重头戏,这么6的动画框架,它是怎么实现的呢?

Lottie整体的原理如下:

1)首先要知道,一个完整动画View,是由很多个子Layer 组成,而每个子Layer主要通过shapes(形状),masks(蒙版),transform三大部分进行动画。
2)Lottie框架通过读取JSON文件,获取到每个子Layer 的shapes,masks,以及出现时间,消失时间以及Transform各个属性的关键帧数组。
3)动画则是通过给CompositionLayer (所有的子layer都添加在这个Layer 上)的 currentFrame属性添加一个CABaseAnimation 来实现。
4)所有的子Layer根据currentFrame 属性的变化,根据JSON中的关键帧数组计算出自己的当前状态并进行显示。

接下来让我们深入它的源码(OC版本)去看看,对它的原理有一个更深刻的认识

1)入口类为LOTAnimationView,提供了一系列加载和设置动画的方法及属性以及对动画的操作,这里列举一二

...
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));
+ (nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));
+ (nonnull instancetype)animationFromJSON:(nullable NSDictionary *)animationJSON 
inBundle:(nullable NSBundle *)bundle NS_SWIFT_NAME(init(json:bundle:));
...
@property (nonatomic, assign) CGFloat animationProgress;
@property (nonatomic, assign) CGFloat animationSpeed;
...
- (void)play;
- (void)pause; 
...

LOTAnimationView所有的加载方法,最终执行的都是把JSON字典传到LOTComposition类中,组装LOTComposition对象,当然还会有一些缓存获取,值判断等的逻辑,但是核心就是产生一个LOTComposition对象:

+ (nullable instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle {
  ...
  if (JSONObject && !error) {
    LOTComposition *laScene = [[self alloc] initWithJSON:JSONObject withAssetBundle:bundle];
    [[LOTAnimationCache sharedCache] addAnimation:laScene forKey:animationName];
    laScene.cacheKey = animationName;
    return laScene;
  }
  NSLog(@"%s: Animation Not Found", __PRETTY_FUNCTION__);
  return nil;
}

2)LOTComposition类用来解析整个动画的json字典,获取整个动画所需的数据。

- (void)_mapFromJSON:(NSDictionary *)jsonDictionary
     withAssetBundle:(NSBundle *)bundle {
  NSNumber *width = jsonDictionary[@"w"];
  NSNumber *height = jsonDictionary[@"h"];
  if (width && height) {
    CGRect bounds = CGRectMake(0, 0, width.floatValue, height.floatValue);
    _compBounds = bounds;
  }
  
  _startFrame = [jsonDictionary[@"ip"] copy];
  _endFrame = [jsonDictionary[@"op"] copy];
  _framerate = [jsonDictionary[@"fr"] copy];
  
  if (_startFrame && _endFrame && _framerate) {
    NSInteger frameDuration = (_endFrame.integerValue - _startFrame.integerValue) - 1;
    NSTimeInterval timeDuration = frameDuration / _framerate.floatValue;
    _timeDuration = timeDuration;
  }
  
  NSArray *assetArray = jsonDictionary[@"assets"];
  if (assetArray.count) {
    _assetGroup = [[LOTAssetGroup alloc] initWithJSON:assetArray withAssetBundle:bundle withFramerate:_framerate];
  }
  
  NSArray *layersJSON = jsonDictionary[@"layers"];
  if (layersJSON) {
    _layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON
                                            withAssetGroup:_assetGroup
                                             withFramerate:_framerate];
  }
  
  [_assetGroup finalizeInitializationWithFramerate:_framerate];
}

在对JSON字典的解析过程中,会拆分成几种不同的信息,包括:整体关键帧信息、所需图片资源信息、所有子layer的信息。并将图片组和layer组分别传入到LOTAssetGroup和LOTLayerGroup中做进一步处理。

3)LOTLayerGroup类用于解析JSON中“layers”层的数据,并将单独的layer数据传递给LOTLayer处理。核心代码如下:

- (void)_mapFromJSON:(NSArray *)layersJSON
      withAssetGroup:(LOTAssetGroup * _Nullable)assetGroup
       withFramerate:(NSNumber *)framerate {
  
  NSMutableArray *layers = [NSMutableArray array];
  NSMutableDictionary *modelMap = [NSMutableDictionary dictionary];
  NSMutableDictionary *referenceMap = [NSMutableDictionary dictionary];
  
  for (NSDictionary *layerJSON in layersJSON) {
    LOTLayer *layer = [[LOTLayer alloc] initWithJSON:layerJSON
                                      withAssetGroup:assetGroup
                                       withFramerate:framerate];
    [layers addObject:layer];
    modelMap[layer.layerID] = layer;
    if (layer.referenceID) {
      referenceMap[layer.referenceID] = layer;
    }
  }
  
  _referenceIDMap = referenceMap;
  _modelMap = modelMap;
  _layers = layers;
}

4)接下来进入LOTLayer类中,这里最终把json文件中单个layer对应的数据映射出来。

...
@property (nonatomic, readonly) NSString *layerName;
@property (nonatomic, readonly, nullable) NSString *referenceID;
@property (nonatomic, readonly) NSNumber *layerID;
@property (nonatomic, readonly) LOTLayerType layerType;
@property (nonatomic, readonly, nullable) NSNumber *parentID;
@property (nonatomic, readonly) NSNumber *startFrame;
@property (nonatomic, readonly) NSNumber *inFrame;
@property (nonatomic, readonly) NSNumber *outFrame;
@property (nonatomic, readonly) NSNumber *timeStretch;
@property (nonatomic, readonly) CGRect layerBounds;
@property (nonatomic, readonly, nullable) NSArray *shapes;
@property (nonatomic, readonly, nullable) NSArray *masks;
...

以上属性和json文件对应的key有一一对应关系,比如layerName对应json文件中的nm,layerType对应ty等等,每个layer中包含Layer所需的基本信息,transform变化需要的则是每个LOTKeyframeGroup 类型的属性。这里面包含了该Layer 的 transform变化的关键帧数组,而masks 和 shapes 的信息包含在上面的两个同名数组中。

5)前面四步,已经把动画需要的数据全部准备好了,接下来就需要进行动画显示。

最底层的LOTLayerContainer继承自CALayer,添加了currentFrame属性,LOTCompositionContainer又是继承自LOTLayerContainer,为LOTCompositionContainer对象添加了一个CABaseAnimation动画,然后重写CALayer的display方法,在display方法中通过 CALayer中的presentationLayer获取在动画中变化的currentFrame数值 ,再通过遍历每一个子layer,将更新后的currentFrame传入,来实时更新每一个子Layer的显示。核心代码在LOTLayerContainer中,如下:

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

它实际上完成了以下几件事:
1.根据子Layer的起始帧和结束帧判断当前帧子Layer是否显示
2.更新子Layer当前帧的透明度
3.更新子Layer当前帧的transform
4.更新子Layer中路径和形状等内容的变化

6)上面动画显示的2,3,4步都是通过XXInterpolator这些类,来从当前frame中计算出我们需要的值,我们以LOTTransformInterpolator为例,其他类似,看看它都有些什么:

...
@property (nonatomic, readonly) LOTPointInterpolator *positionInterpolator;
@property (nonatomic, readonly) LOTPointInterpolator *anchorInterpolator;
@property (nonatomic, readonly) LOTSizeInterpolator *scaleInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *rotationInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *positionXInterpolator;
@property (nonatomic, readonly) LOTNumberInterpolator *positionYInterpolator;
...

针对transform变换需要很多的信息,LOTTransformInterpolator中提供了这些所需的信息。

当传入当前frame时,这些interpolator会返回不同的数值,从而组成当前的transform。这些不同的Interpolar会根据自己的算法返回当前所需要的值,但是他们大体的流程都是一样的:

1.在关键帧数组中找到当前frame的前一个关键帧(leadingKeyframe)和后一个关键帧(trailingKeyframe)
2.计算当前frame 在 leadingKeyframe 和 trailingKeyframe 的进度(progress)
3.根据这个progress以及 leadingKeyframe,trailingKeyframe算出当前frame下的值。(不同的Interpolator算法不同)

总结:

Lottie提供了多种便利的方式,供我们加载酷炫的动画,对用户体验有极大的提升。对使用者来说,只需要引入包含动效的json文件和资源文件,调用lottie提供的属性和api完成动画绘制。Lottie内部帮我们做了json文件映射到不同类的不同属性中,通过一系列的计算,确定出每一帧的数据,然后完美的显示在屏幕上,这样的神器,以后要多多用起来啦!

你可能感兴趣的:(ios开发)