Lottie-iOS的应用及部分源码分析

4,275 阅读11分钟

Lottie-iOS的应用及部分源码分析

Lottie是Airbnb在2017年2月份开源的一个能够为原生应用添加动画效果的牛逼的动画框架,通过加载Bundled JSON文件或URL,以AE导出的文件为资源,完美实现之前那些一看就头大的动画效果。告别复杂的动画绘制工作,节约大量时间。基本所有不涉及复杂交互行为的需求动画都可以通过Lottie实现。

Lottie 目前提供了 iOS, Android, 和 React Native 版本,能够实时渲染 After Effects 动画特效。支持ios8以上系统。

1. 接入方式

由 CocoaPods 引入该库pod 'lottie-ios'

2. 项目应用

  • 将AE文件导出成json导入到项目bundle中。
  • 如果动画中包含图片,需要将json文件和它所依赖的图片一同导入项目bundle中。 当我们在做启动导航图时,有时可能会需要多个json文件,比如像开头的动画那样,需要三个json文件。而UI导出的图片基本上名字都差不多,这时候如果我们不加分辨,直接导入三个json文件,会使得图片出现错乱。因为三个 json 文件中的图片名基本完全相同,这时候我们就需要打开 json 文件中的 assets 属性,修改其中的图片名。
  • 项目中使用的地方引入头文件#import <Lottie/Lottie.h>
使用 LOTAnimationView 加载 json 文件

        LOTAnimationView * loadingView = [LOTAnimationView animationNamed:json inBundle:[NSBundle mainBundle]];
        loadingView.frame = CGRectMake(0, 133*self.ratioScreenW, self.frame.size.width, 200*self.ratioScreenW);
//        loadingView.loopAnimation = YES;
        [self addSubview:loadingView];
        self.guidanceView = loadingView;
        [self.guidanceView play];

支持动画控制,也可以通过监听动画属性实现需求效果。

//加载本地json文件
+ (instancetype)animationNamed:(NSString *)animationName NS_SWIFT_NAME(init(name:));
+ (instancetype)animationNamed:(NSString *)animationName inBundle:(NSBundle *)bundle NS_SWIFT_NAME(init(name:bundle:));
+ (instancetype)animationFromJSON:(NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));
//加载远程文件
- (instancetype)initWithContentsOfURL:(NSURL *)url;
复制代码
@property (nonatomic, readonly) BOOL isAnimationPlaying; @property (nonatomic, assign) BOOL loopAnimation;//循环播放 @property (nonatomic, assign) CGFloat animationProgress;//动画进度 @property (nonatomic, assign) CGFloat animationSpeed;//动画速率 @property (nonatomic, readonly) CGFloat animationDuration;//动画时长

-(void)playWithCompletion:(LOTAnimationCompletionBlock)completion;

-(void)play;//播放动画

-(void)pause;//暂停动画

-(void)addSubview:(LOTView )view toLayerNamed:(NSString )layer;
  • 使用起来是不是非常简单,且效果完美,也可结合UIView相关动画控制使用,再也不用被ui交互指指点点了。
  • 而且Lottie用的方法也是计算各种bezier path,只不过这些path已经被AE导出的json预先算好了,然后通过框架做插件,交给系统SDK提供的动画框架渲染,保证性能。

3. 部分源码解析

Lottie是如何解析json文件的?

  • LOTAnimationView是创建实例并加载动画文件:
/// 默认从main bundle中加载json文件和图片,animationName实际上就是json文件的名字

-(nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));
/// 从指定的bundle中加载json文件和图片

-(nonnull instancetype)animationNamed:(nonnull NSString )animationName inBundle:(nonnull NSBundle )bundle NS_SWIFT_NAME(init(name:bundle:));
///直接从给定的json文件中加载动画,默认从main bundle加载

-(nonnull instancetype)animationFromJSON:(nonnull NSDictionary *)animationJSON NS_SWIFT_NAME(init(json:));
/// 从一个文件URL加载动画,但是不能使用web url作为参数

-(nonnull instancetype)animationWithFilePath:(nonnull NSString *)filePath NS_SWIFT_NAME(init(filePath:));
/// 给定一个反序列化的json文件数据和一个特定的bundle来初始化加载动画

-(nonnull instancetype)animationFromJSON:(nullable NSDictionary )animationJSON inBundle:(nullable NSBundle )bundle NS_SWIFT_NAME(init(json:bundle:));
/// 直接使用LOTComposition来创建动画, 图片从指定的bundle中加载

-(nonnull instancetype)initWithModel:(nullable LOTComposition )model inBundle:(nullable NSBundle )bundle;
/// 异步的从指定的URL中加载动画,这个url为webUrl

-(nonnull instancetype)initWithContentsOfURL:(nonnull NSURL *)url;

LOTAnimationView.m中初始化方法最后会调用下面这个:

-(nonnull instancetype)animationFromJSON:(nullable NSDictionary )animationJSON inBundle:(nullable NSBundle )bundle { 
LOTComposition *comp = [LOTComposition animationFromJSON:animationJSON inBundle:bundle]; 
return [[LOTAnimationView alloc] initWithModel:comp inBundle:bundle];
}
  • LOTComposition文件:
    至于最后是如何初始化的,我们可以根据初始化方法在LOTComposition类中找到这个方法,这个方法用来解析json文件。
#pragma mark - Initializer初始化方法

-(instancetype _Nonnull)initWithJSON:(NSDictionary _Nullable)jsonDictionary withAssetBundle:(NSBundle _Nullable)bundle { 
self = [super init]; 
if (self) { 
if (jsonDictionary) {
[self _mapFromJSON:jsonDictionary withAssetBundle:bundle]; 
} } 
return self; 
}
#pragma mark - Internal Methods内部方法解析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];//初始化frame 
_endFrame = [jsonDictionary[@"op"] copy];//动画结束frame 
_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]; 
} 
//动画layer数组
NSArray layersJSON = jsonDictionary[@"layers"]; 
if (layersJSON) { 
_layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON withAssetGroup:_assetGroup];
}

[_assetGroup finalizeInitialization]; }

在这个方法中,我们注意到了它调用了LOTAssetGroup的图片数组的初始化方法和LOTLayerGroup的layer层数组的初始化方法,具体来看一下。

  • LOTAssetGroup初始化方法到最后我们在LOTAsset文件中找到了这个解析json数据的方法。
-(void)_mapFromJSON:(NSDictionary )jsonDictionary withAssetGroup:(LOTAssetGroup _Nullable)assetGroup { 
_referenceID = [jsonDictionary[@"id"] copy];//// 指定图片的 referenceID,这个id是json文件自动为每张图片编号的,表示他的唯一标识符

if (jsonDictionary[@"w"]) {//图片宽度 
_assetWidth = [jsonDictionary[@"w"] copy]; 
}

if (jsonDictionary[@"h"]) {//图片高度 
_assetHeight = [jsonDictionary[@"h"] copy]; 
}

if (jsonDictionary[@"u"]) {//图片的路径,这个路径表示存储图片的文件夹 
_imageDirectory = [jsonDictionary[@"u"] copy];
}

if (jsonDictionary[@"p"]) {// p表示图片的真实id,而不是referenceID 
_imageName = [jsonDictionary[@"p"] copy];
}

NSArray *layersJSON = jsonDictionary[@"layers"];
if (layersJSON) {//// 对图片layer的配置,和LOTComposition中对layer 层数组的初始化方法相同。具体来看一下 
_layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON withAssetGroup:assetGroup];
} }
  • LOTALayerGroup文件中对json文件的解析
//

-(void)_mapFromJSON:(NSArray )layersJSON withAssetGroup:(LOTAssetGroup _Nullable)assetGroup { 
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]; 
[layers addObject:layer]; 
modelMap[layer.layerID] = layer; 
if (layer.referenceID) { 
referenceMap[layer.referenceID] = layer;
} } 
_referenceIDMap = referenceMap; 
_modelMap = modelMap;
_layers = layers;
}

在这个方法内部更深层的调用了LOTALayer的初始化方法

-(void)_mapFromJSON:(NSDictionary )jsonDictionary withAssetGroup:(LOTAssetGroup )assetGroup {

_layerName = [jsonDictionary[@"nm"] copy];//layer的名字 
_layerID = [jsonDictionary[@"ind"] copy];//layer的id,表示这是第几个layer

NSNumber *layerType = jsonDictionary[@"ty"];//表示layer的类型,这个变量是一个枚举类型 
_layerType = layerType.integerValue; // typedef 
enum : NSInteger { // LOTLayerTypePrecomp, // LOTLayerTypeSolid, // LOTLayerTypeImage, // LOTLayerTypeNull, // LOTLayerTypeShape, // LOTLayerTypeUnknown // } LOTLayerType;

if (jsonDictionary[@"refId"]) {// 这里的refId和图片文件的referenceID指向的是同一个标识符,表示这个layer动画会作用在 referenceID 指向的图片上 _referenceID = [jsonDictionary[@"refId"] copy]; }

_parentID = [jsonDictionary[@"parent"] copy];// 父layer

if (jsonDictionary[@"st"]) { 
_startFrame = [jsonDictionary[@"st"] copy];// 开始的 frame 
} 
_inFrame = [jsonDictionary[@"ip"] copy];// 开始的 frame,通常和 startFrame 值相同 
_outFrame = [jsonDictionary[@"op"] copy];//最后一帧的frame //判断layer是哪种类型,并且做相应的处理 
if (_layerType == LOTLayerTypePrecomp) { 
_layerHeight = [jsonDictionary[@"h"] copy];//高度 
_layerWidth = [jsonDictionary[@"w"] copy];//宽度 
[assetGroup buildAssetNamed:_referenceID]; 
} else if (_layerType == LOTLayerTypeImage) { 
[assetGroup buildAssetNamed:_referenceID]; 
_imageAsset = [assetGroup assetModelForID:_referenceID]; _layerWidth = [_imageAsset.assetWidth copy]; 
_layerHeight = [_imageAsset.assetHeight copy]; 
} else if (_layerType == LOTLayerTypeSolid) { 
_layerWidth = jsonDictionary[@"sw"]; 
_layerHeight = jsonDictionary[@"sh"]; 
NSString *solidColor = jsonDictionary[@"sc"]; 
_solidColor = [UIColor LOT_colorWithHexString:solidColor]; 
}

_layerBounds = CGRectMake(0, 0, _layerWidth.floatValue, _layerHeight.floatValue);

NSDictionary *ks = jsonDictionary[@"ks"];

NSDictionary *opacity = ks[@"o"];//不透明度 
if (opacity) { 
_opacity = [[LOTKeyframeGroup alloc] initWithData:opacity]; [_opacity remapKeyframesWithBlock:^CGFloat(CGFloat inValue) { return LOT_RemapValue(inValue, 0, 100, 0, 1); }]; 
}

NSDictionary *rotation = ks[@"r"];//旋转 
if (rotation == nil) { 
rotation = ks[@"rz"];
} 
if (rotation) { 
_rotation = [[LOTKeyframeGroup alloc] initWithData:rotation]; [_rotation remapKeyframesWithBlock:^CGFloat(CGFloat inValue) { return LOT_DegreesToRadians(inValue); }]; 
}

NSDictionary *position = ks[@"p"];// 位置 
if ([position[@"s"] boolValue]) { // Separate dimensions 
_positionX = [[LOTKeyframeGroup alloc] initWithData:position[@"x"]]; 
_positionY = [[LOTKeyframeGroup alloc] initWithData:position[@"y"]]; 
} else { 
_position = [[LOTKeyframeGroup alloc] initWithData:position ]; 
}

NSDictionary *anchor = ks[@"a"];//锚点 
if (anchor) { 
_anchor = [[LOTKeyframeGroup alloc] initWithData:anchor]; 
}

NSDictionary *scale = ks[@"s"];//缩放比例 
if (scale) { 
_scale = [[LOTKeyframeGroup alloc] initWithData:scale];
[_scale remapKeyframesWithBlock:^CGFloat(CGFloat inValue) { return LOT_RemapValue(inValue, 0, 100, 0, 1); }]; 
}

_matteType = [jsonDictionary[@"tt"] integerValue];

NSMutableArray masks = [NSMutableArray array]; 
for (NSDictionary maskJSON in jsonDictionary[@"masksProperties"]) { 
LOTMask *mask = [[LOTMask alloc] initWithJSON:maskJSON]; 
[masks addObject:mask]; 
} 
_masks = masks.count ? masks : nil;

NSMutableArray shapes = [NSMutableArray array]; 
for (NSDictionary shapeJSON in jsonDictionary[@"shapes"]) {//尺寸 id 
shapeItem = [LOTShapeGroup shapeItemWithJSON:shapeJSON]; 
if (shapeItem) {
[shapes addObject:shapeItem]; 
} }
_shapes = shapes; //额外效果 
NSArray *effects = jsonDictionary[@"ef"]; 
if (effects.count > 0) {

NSDictionary *effectNames = @{ @0: @"slider",
                               @1: @"angle",
                               @2: @"color",
                               @3: @"point",
                               @4: @"checkbox",
                               @5: @"group",
                               @6: @"noValue",
                               @7: @"dropDown",
                               @9: @"customValue",
                               @10: @"layerIndex",
                               @20: @"tint",
                               @21: @"fill" };
                         
for (NSDictionary *effect in effects) {
  NSNumber *typeNumber = effect[@"ty"];
  NSString *name = effect[@"nm"];
  NSString *internalName = effect[@"mn"];
  NSString *typeString = effectNames[typeNumber];
  if (typeString) {
    NSLog(@"%s: Warning: %@ effect not supported: %@ / %@", __PRETTY_FUNCTION__, typeString, internalName, name);
  }
}
} }

由上可以看到,在 LOTComposition , LOTLayer, LOTAssets 这几个类中完成了 json 数据的解析。通过LOTALayer获取到layer动画的数据后,根据数据创建动画的layer层。

  • LOTAniamtionView 的初始化之外的属性和方法
/// 判断是否正在播放动画 
@property (nonatomic, readonly) BOOL isAnimationPlaying;

/// 判断动画是否需要循环播放 
@property (nonatomic, assign) BOOL loopAnimation;

/// 自动倒序动画,如果这个属性值和loopAnimation都为YES,那么动画会先倒序播放,然后再正序播放 
@property (nonatomic, assign) BOOL autoReverseAnimation;

/// 设置progress的值从 0 ~ 1,表示这个动画执行的程度,当设定为表示动画结束。绝对时间内当前动画的进度。 
@property (nonatomic, assign) CGFloat animationProgress;

/// 为动画设置一个播放速度,如果想倒序播放,则可以把这个值设定为负数 @property (nonatomic, assign) CGFloat animationSpeed;

/// 只读属性,获取的值为当 speed = 1 时动画的播放秒数 
@property (nonatomic, readonly) CGFloat animationDuration;

/// 是否缓存动画,默认为YES,对于只需要播放一次的动画,比如启动页动画,可以设定为 NO 
@property (nonatomic, assign) BOOL cacheEnable;

/// 当动画播放完毕所调用的 block 
@property (nonatomic, copy, nullable) LOTAnimationCompletionBlock completionBlock;

/// 直接设定播放数据,也就是解析 json 文件后的数据模型
@property (nonatomic, strong, nullable) LOTComposition *sceneModel;

/*

这个方法表示 animation 将从当前的 position 播放到指定的 progressloopAnimation 属性为 YES 时,这个动画将从当前 position 到指定 progress 并且无限循环
loopAnimation 属性为 YES 时,这个动画将从当前 position 到指定 progress 然后停到 progress 的那一帧
当动画完成后会调用指定 block

-(void)playToProgress:(CGFloat)toProgress withCompletion:(nullable LOTAnimationCompletionBlock)completion;

和上面的方法差不多,主要是开始的 progress 也可以由我们来指定。

-(void)playFromProgress:(CGFloat)fromStartProgress toProgress:(CGFloat)toEndProgress withCompletion:(nullable LOTAnimationCompletionBlock)completion;

从当前的 position 动画到指定的 frame

(void)playToFrame:(nonnull NSNumber *)toFrame withCompletion:(nullable LOTAnimationCompletionBlock)completion;

设置开始的 frame 并且动画到指定的 frame

-(void)playFromFrame:(nonnull NSNumber )fromStartFrame toFrame:(nonnull NSNumber )toEndFrame withCompletion:(nullable LOTAnimationCompletionBlock)completion;

从当前的 position 完成到结束 position。播放结束的时候调用completion

(void)playWithCompletion:(nullable LOTAnimationCompletionBlock)completion;
/// 播放动画,播放完成后会调用完成block

(void)play;
/// 暂停动画并且调用完成block

(void)pause;
/// 停止当前动画并且倒回到最开始的那一帧,完成后调用block

(void)stop;
/// 设置当前的动画到指定的frame,如果当前动画正在播放则会停止动画并且调用完成block

(void)setProgressWithFrame:(nonnull NSNumber *)currentFrame; /// 打印所有继承链上的 keypath
(void)logHierarchyKeypaths;
  • 在上述属性中我们可以看到一个缓存cache属性,我们来探究Lottie究竟是如何缓存动画的。在LOTAniamtionView的实现文件中可以看到以下方法:
pragma mark - External Methods - Cache
-(void)setCacheEnable:(BOOL)cacheEnable { 
	_cacheEnable = cacheEnable; 
    if (!self.sceneModel.cacheKey) { return; } 
    if (cacheEnable) { 
    // 如果支持,则向 cache 中添加这个key所代表的对象已经向字典中添加这个 key 以及它对应的 value 值,也就是动画数据对象 
    [[LOTAnimationCache sharedCache] addAnimation:_sceneModel forKey:self.sceneModel.cacheKey]; 
    } else { 
    // 如果不支持,则从字典中移除这个 key,和这个 key 所代表的对象,以及数组中的 key 
    [[LOTAnimationCache sharedCache] removeAnimationForKey:self.sceneModel.cacheKey]; 
    } 
}

LOTAniamtionCache的实现文件中,可以看到,LOTAnimationCache 维护了一个添加 keyvalue 到字典和一个数组。
整个cache是一个单例,也就是存在于app的整个生命周期中不会被销毁,一旦 app 关闭,由于数据存储也仅仅是简单的使用一个数组和一个字典来存储,并未进行持久化处理,单例中所缓存的数据也会被销毁,所以我们对于动画的缓存仅限于我们在使用 app 时。

const NSInteger kLOTCacheSize = 50; 
@implementation LOTAnimationCache { 
	NSMutableDictionary animationsCache_; 
    NSMutableArray lruOrderArray_; 
} //单例

-(instancetype)sharedCache { 
	static LOTAnimationCache *sharedCache = nil; 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
    	sharedCache = [[self alloc] init]; 
        }); 
    return sharedCache; 
 }
 
-(instancetype)init { 
	self = [super init]; 
    if (self) { 
    	animationsCache = [[NSMutableDictionary alloc] init]; 
        lruOrderArray = [[NSMutableArray alloc] init]; 
     } 
	return self;
} //当添加动画时:首先需要判断当前数组的size是否已经大于了最大的size,如果是的话,则先清除最前面缓存的动画,然后再添加新的动画,而这个kLOTCacheSize = 50;最大为50。

-(void)addAnimation:(LOTComposition )animation forKey:(NSString )key {
	if (lruOrderArray.count >= kLOTCacheSize) { 
    	NSString *oldKey = lruOrderArray[0]; 
        [animationsCache_ removeObjectForKey:oldKey]; 					[lruOrderArray_ removeObject:oldKey]; 
     } 
     [lruOrderArray_ removeObject:key]; 
     [lruOrderArray_ addObject:key]; 
     [animationsCache_ setObject:animation forKey:key]; 
} //当移除动画时:则直接将缓存有该动画的数组中移除这个key,并且在 cache 字典中也移除这个key和它所对应的对象。

-(void)removeAnimationForKey:(NSString *)key { 
	[lruOrderArray_ removeObject:key]; 
    [animationsCache_ removeObjectForKey:key]; 
}

4.总结

  • Lottie是基于CALayer的动画, 所有的路径预先在AE中计算好, 转换为Json文件, 然后自动转换为Layer的动画, 所以性能理论上是非常不错的。
  • 加载动画过程:LOTAnimationView初始化创建实例加载资源文件->LOTComposition解析json文件->LOTAssetGroup解析json文件中解析出的图片数组->LOTLayerGroup解析json文件中解析出的layer动画数组和图片的layer数组->LOTAnimationView设置其他属性或方法。
  • 如果使用了素材, 那么素材图片的每个像素都会直接加载进内存,避免bundle资源中额外多余的图片占用内存。尽量不使用图片素材,而是在AE中直接绘制或者iconfont矢量图之类的则没有这个问题。
  • 如果一个项目中使用了多个Lottie的动画,需要注意Json文件中的路径及素材名称不能重复, 否则会错乱。
  • 使用Lottie的场合大多为复杂的播放式形变动画,因为形变动画由程序员一点点的写路径确实不直观且效率低。Lottie真的是我们在CoreAnimation之后一个很好的补充.