Lottie是Airbnb在2017年2月份开源的一个能够为原生应用添加动画效果的牛逼的动画框架,通过加载Bundled JSON文件或URL,以AE导出的文件为资源,完美实现之前那些一看就头大的动画效果。告别复杂的动画绘制工作,节约大量时间。基本所有不涉及复杂交互行为的需求动画都可以通过Lottie实现。
Lottie 目前提供了 iOS, Android, 和 React Native 版本,能够实时渲染 After Effects 动画特效。支持ios8以上系统。
- Android : github.com/airbnb/lott…
- iOS : github.com/airbnb/lott…
- React Native : github.com/airbnb/lott…
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相关动画和手势,往往只需几个小时就可以解决我们3天的工作量,且效果完美,再也不用被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 播放到指定的 progress
* 当 loopAnimation 属性为 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 维护了一个添加 key 和 value 到字典和一个数组。
整个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之后一个很好的补充.