Lottie-iOS
Lottie动画的原理:
- 一个完整动画View,是由很多个子Layer 组成,而每个子Layer主要通过shapes(形状),masks(蒙版),transform三大部分进行动画。
- Lottie框架通过读取JSON文件,获取到每个子Layer 的shapes,masks,以及出现时间,消失时间以及Transform各个属性的关键帧数组。
- 动画则是通过给
CompositionLayer
(所有的子layer都添加在这个Layer 上)的currentFrame
属性添加一个CABaseAnimation
来实现。 - 所有的子Layer根据
currentFrame
属性的变化,根据JSON中的关键帧数组计算出自己的当前状态并进行显示。
Lottie的创建方法:
/// 默认从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;
Lottie的使用:
/* * 将动画从其当前位置播放到特定进度。* 动画将从其当前位置开始。* 如果循环动画,则动画将从开始位置无限循环到进度。* 如果 loopAnimation 为 NO,则动画将停止并调用完成块。*/
- (void)playToProgress:(CGFloat)toProgresswithCompletion:(nullable LOTAnimationCompletionBlock)completion;
/** 播放从特定进度到特定进度的动画* 动画将从其当前位置开始。* 如果循环动画,则动画将从开始无限期地循环到结束进度* 如果 loopAnimation 为 NO,则动画将停止并调用完成块。*/
- (void)playFromProgress:(CGFloat)fromStartProgresstoProgress:(CGFloat)toEndProgresswithCompletion:(nullable LOTAnimationCompletionBlock)completion;
/** Plays the animation from its current position to a specific frame.* The animation will start from its current position.* If loopAnimation is YES the animation will loop from beginning to toFrame indefinitely.* If loopAnimation is NO the animation will stop and the completion block will be called.*/
- (void)playToFrame:(nonnull NSNumber *)toFramewithCompletion:(nullable LOTAnimationCompletionBlock)completion;
/** 将动画从特定帧播放到特定帧。* 动画将从其当前位置开始。* 如果循环动画,则动画将无限期地循环开始帧到结束帧。* 如果 loopAnimation 为 NO,则动画将停止并调用完成块。*/
- (void)playFromFrame:(nonnull NSNumber *)fromStartFrametoFrame:(nonnull NSNumber *)toEndFramewithCompletion:(nullable LOTAnimationCompletionBlock)completion;
/*** 播放动画从当前位置到动画结束。* 动画将从其当前位置开始。* 如果循环动画,则动画将从头到尾无限循环。* 如果 loopAnimation 为 NO,则动画将停止并调用完成块。**/
- (void)playWithCompletion:(nullable LOTAnimationCompletionBlock)completion;/// 播放
- (void)play;/// 暂停到当前帧,调用完成块。
- (void)pause;/// 停止,回到开头,调用完成块。
- (void)stop;/// 将动画进度设置为特定帧。如果动画正在播放,它将停止并调用完成块。
- (void)setProgressWithFrame:(nonnull NSNumber *)currentFrame;/// 将动画进度设置为特定帧。如果动画正在播放,它将停止,如果调用完成为是,则将调用完成块。
- (void)setProgressWithFrame:(nonnull NSNumber *)currentFrame callCompletionIfNecessary:(BOOL)callCompletion;
Lottie部分源码解析:
LOTAnimationView.m中初始化方法最后会调用下面这个方法:
- (instancetype)initWithModel:(LOTComposition *)model inBundle:(NSBundle *)bundle {self = [self initWithFrame:model.compBounds];if (self) {_bundle = bundle;[self _initializeAnimationContainer];[self _setupWithSceneModel:model];}return self;
}
LOTComposition
对象包含一个LOTAssetGroup
对象,该对象用于管理动画中使用的所有资源。LOTAssetGroup
对象包含一个或多个LOTAsset
对象,每个LOTAsset
对象代表一个资源文件。例如,当动画中使用了一个图片资源时,LOTAssetGroup
会创建一个LOTImageAsset
对象来管理该图片资源。
LOTComposition
还包含一个LOTLayerGroup
,该对象用于管理所有图层和子图层。LOTLayerGroup
对象包含一个或多个LOTLayer
,每个LOTLayer
代表一个图层。LOTLayer
包含了图层的各种属性和元素,例如位置、大小、旋转、透明度等等。
每个LOTLayer
都可以包含一个或多个子图层,每个子图层都可以包含一个或多个子视图。这种嵌套关系使得CALayer
可以创建复杂的视图层级结构,并实现高效的渲染过程。
在Lottie中,LOTComposition
被转换为一个名为LOTCompositionContainer
的模型对象。LOTCompositionContainer
包含了动画的各种属性和元素,例如图层、路径、形状、颜色等等。此外,LOTCompositionContainer
还包含了一个CALayer
用于渲染动画。
LOTComposition
可以在LOTComposition
的代码里面找到Lottie解析json文件的过程:
#pragma mark - Initializer 基础初始化方法,json和bundle
- (instancetype _Nonnull)initWithJSON:(NSDictionary * _Nullable)jsonDictionarywithAssetBundle:(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 *)jsonDictionarywithAssetBundle:(NSBundle *)bundle {NSNumber *width = jsonDictionary[@"w"]; // 宽度 NSNumber *height = jsonDictionary[@"h"]; // 高度 if (width && height) {CGRect bounds = CGRectMake(0, 0, width.floatValue, height.floatValue);_compBounds = bounds;}// 动画开始frame_startFrame = [jsonDictionary[@"ip"] copy];// 动画结束frame_endFrame = [jsonDictionary[@"op"] copy];// 动画变化率_framerate = [jsonDictionary[@"fr"] copy];if (_startFrame && _endFrame && _framerate) {NSInteger frameDuration = (_endFrame.integerValue - _startFrame.integerValue) - 1;NSTimeInterval timeDuration = frameDuration / _framerate.floatValue;// 计算动画duration_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:layersJSONwithAssetGroup:_assetGroupwithFramerate:_framerate];}[_assetGroup finalizeInitializationWithFramerate:_framerate];
}
在这个方法中,它调用了LOTAssetGroup
的图片数组的初始化方法和LOTLayerGroup
的layer层数组的初始化方法,生成了_assetGroup
并使用其初始化了_layerGroup
。具体来看一下。
LOTAssetGroup
LOTAssetGroup的初始化:
- (instancetype _Nonnull)initWithJSON:(NSArray * _Nonnull)jsonArraywithAssetBundle:(NSBundle * _Nullable)bundlewithFramerate:(NSNumber * _Nonnull)framerate {self = [super init];if (self) {_assetBundle = bundle;_assetMap = [NSMutableDictionary dictionary];NSMutableDictionary *assetJSONMap = [NSMutableDictionary dictionary];for (NSDictionary<NSString *, NSString *> *assetDictionary in jsonArray) {NSString *referenceID = assetDictionary[@"id"];if (referenceID) {assetJSONMap[referenceID] = assetDictionary;}}_assetJSONMap = assetJSONMap;}return self;
}
assetJSONMap
存放的数据,里面是图片的各种信息:
{"image_0" : {"id":"image_0","w":180,"h":180,"u":"images/","p":"img_0.png","e":0},"image_1" : {"id":"image_1","w":600,"h":600,"u":"images/","p":"img_1.png","e":0},...
}
我们最终在LOTAsset
文件中找到了解析json数据的方法:
- (void)_mapFromJSON:(NSDictionary *)jsonDictionarywithAssetGroup:(LOTAssetGroup * _Nullable)assetGroupwithFramerate:(NSNumber *)framerate {_referenceID = [jsonDictionary[@"id"] copy];if (jsonDictionary[@"w"]) { // 宽度_assetWidth = [jsonDictionary[@"w"] copy];}if (jsonDictionary[@"h"]) { // 高度_assetHeight = [jsonDictionary[@"h"] copy];}if (jsonDictionary[@"u"]) { // 图片路径(图片文件夹)_imageDirectory = [jsonDictionary[@"u"] copy];}if (jsonDictionary[@"p"]) { // 图片名_imageName = [jsonDictionary[@"p"] copy];}NSArray *layersJSON = jsonDictionary[@"layers"];if (layersJSON) {_layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSONwithAssetGroup:assetGroupwithFramerate:framerate];}
}
LOTALayerGroup
LOTALayerGroup对json文件的解析:
- (void)_mapFromJSON:(NSArray *)layersJSONwithAssetGroup:(LOTAssetGroup * _Nullable)assetGroupwithFramerate:(NSNumber *)framerate {NSMutableArray *layers = [NSMutableArray array];NSMutableDictionary *modelMap = [NSMutableDictionary dictionary];NSMutableDictionary *referenceMap = [NSMutableDictionary dictionary];for (NSDictionary *layerJSON in layersJSON) {LOTLayer *layer = [[LOTLayer alloc] initWithJSON:layerJSONwithAssetGroup:assetGroupwithFramerate:framerate];[layers addObject:layer];if (layer.layerID) {modelMap[layer.layerID] = layer;}if (layer.referenceID) {referenceMap[layer.referenceID] = layer;}}_referenceIDMap = referenceMap;_modelMap = modelMap;_layers = layers;
}
里面调用了LOTALayer的初始化方法:
- (void)_mapFromJSON:(NSDictionary *)jsonDictionarywithAssetGroup:(LOTAssetGroup *)assetGroupwithFramerate:(NSNumber *)framerate {_layerName = [jsonDictionary[@"nm"] copy]; // layer名_layerID = [jsonDictionary[@"ind"] copy]; // layer的id,表示这是第几个layerNSNumber *layerType = jsonDictionary[@"ty"]; // 表示layer的类型,这个变量是一个枚举类型_layerType = layerType.integerValue;// LOTLayerTypePrecomp, // LOTLayerTypeSolid, // LOTLayerTypeImage, // LOTLayerTypeNull, // LOTLayerTypeShape, // LOTLayerTypeUnknownif (jsonDictionary[@"refId"]) {// 这里的refId和图片文件的referenceID指向的是同一个标识符,// 表示这个layer动画会作用在 referenceID 指向的图片上_referenceID = [jsonDictionary[@"refId"] copy];}// 父layer_parentID = [jsonDictionary[@"parent"] copy];if (jsonDictionary[@"st"]) {// 开始的 frame_startFrame = [jsonDictionary[@"st"] copy];}// 开始的 frame,通常和 startFrame 值相同_inFrame = [jsonDictionary[@"ip"] copy];//最后一帧的frame_outFrame = [jsonDictionary[@"op"] copy];if (jsonDictionary[@"sr"]) {_timeStretch = [jsonDictionary[@"sr"] copy];} else {_timeStretch = @1;}if (_layerType == LOTLayerTypePrecomp) {_layerHeight = [jsonDictionary[@"h"] copy]; // 高度 _layerWidth = [jsonDictionary[@"w"] copy]; // 宽度 [assetGroup buildAssetNamed:_referenceID withFramerate:framerate];} else if (_layerType == LOTLayerTypeImage) {[assetGroup buildAssetNamed:_referenceID withFramerate:framerate];_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 *timeRemap = jsonDictionary[@"tm"];if (timeRemap) {_timeRemapping = [[LOTKeyframeGroup alloc] initWithData:timeRemap];[_timeRemapping remapKeyframesWithBlock:^CGFloat(CGFloat inValue) {return inValue * framerate.doubleValue;}];}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, -100, 100, -1, 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);}}}
}
LOTCompositionContainer
Lottie的整个动画,就是一个自定义属性的CAAnimation
动画,自定义的属性就是currentFrame
,这个动画是添加到LOTCompositionContainer
上的。而LOTCompositionContainer
是LOTLayerContainer
的子类,内部通过差值计算来得出对应帧在不同时间的值。
_compContainer = [[LOTCompositionContainer alloc] initWithModel:nil inLayerGroup:nil withLayerGroup:_sceneModel.layerGroup withAssestGroup:_sceneModel.assetGroup];
child = [[LOTLayerContainer alloc] initWithModel:layer inLayerGroup:childGroup];
不断的对子Layer预合成。
LOTLayerContainer
初始化代码如下:
- (void)commonInitializeWith:(LOTLayer *)layerinLayerGroup:(LOTLayerGroup *)layerGroup {if (layer == nil) {return;}_layerName = layer.layerName;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);}if (layer.layerType == LOTLayerTypeImage) {[self _setImageForAsset:layer.imageAsset];}_inFrame = [layer.inFrame copy];_outFrame = [layer.outFrame copy];_timeStretchFactor = [layer.timeStretch copy];_transformInterpolator = [LOTTransformInterpolator transformForLayer:layer];if (layer.parentID != nil) {NSNumber *parentID = layer.parentID;LOTTransformInterpolator *childInterpolator = _transformInterpolator;while (parentID != nil) {LOTLayer *parentModel = [layerGroup layerModelForID:parentID];LOTTransformInterpolator *interpolator = [LOTTransformInterpolator transformForLayer:parentModel];childInterpolator.inputNode = interpolator;childInterpolator = interpolator;parentID = parentModel.parentID;}}_opacityInterpolator = [[LOTNumberInterpolator alloc] initWithKeyframes:layer.opacity.keyframes];if (layer.layerType == LOTLayerTypeShape &&layer.shapes.count) {[self buildContents:layer.shapes];}if (layer.layerType == LOTLayerTypeSolid) {_wrapperLayer.backgroundColor = layer.solidColor.CGColor;}if (layer.masks.count) {_maskLayer = [[LOTMaskContainer alloc] initWithMasks:layer.masks];_wrapperLayer.mask = _maskLayer;}NSMutableDictionary *interpolators = [NSMutableDictionary dictionary];// 设置属性_valueInterpolators = interpolators;
}
有图动画设置图片方法:
- (void)_setImageForAsset:(LOTAsset *)asset {self.asyncLoadingResource = NO;if (asset.imageName) {UIImage *image;if ([asset.imageName hasPrefix:@"data:"]) {// Contents look like a data: URL. Ignore asset.imageDirectory and simply load the image directly.NSURL *imageUrl = [NSURL URLWithString:asset.imageName];NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];image = [UIImage imageWithData:imageData];} else if (asset.rootDirectory.length > 0) {// 有rootDirectoryNSString *rootDirectory = asset.rootDirectory;if (asset.imageDirectory.length > 0) {// 拼接图片资源“p”字段,图片文件名rootDirectory = [rootDirectory stringByAppendingPathComponent:asset.imageDirectory];}NSString *imagePath = [rootDirectory stringByAppendingPathComponent:asset.imageName];id<LOTImageCache> imageCache = [LOTCacheProvider imageCache];// 存在imageCacheif (imageCache) {image = [imageCache imageForKey:imagePath];if (!image) {image = [UIImage imageWithContentsOfFile:imagePath];[imageCache setImage:image forKey:imagePath];}} else {image = [UIImage imageWithContentsOfFile:imagePath];}} else if (!asset.ignoreBundleResource) {NSString *imagePath = [asset.assetBundle pathForResource:asset.imageName ofType:nil];image = [UIImage imageWithContentsOfFile:imagePath];}//try loading from asset catalogue instead if all else failsif (!image && !asset.ignoreBundleResource) {image = [UIImage imageNamed:asset.imageName inBundle:asset.assetBundle compatibleWithTraitCollection:nil];}if (image) {_wrapperLayer.contents = (__bridge id _Nullable)(image.CGImage);} else if (asset.baseURL) {// 通过url下载} else {NSLog(@"%s: Warn: image not found: %@", __PRETTY_FUNCTION__, asset.imageName);}}
}
播放:
- 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:]
在LOTLayerContainer
里面可以看到needsDisplayForKey
指定了key
为currentFrame
时触发重绘。actionForKey
是接收指定key
被修改时触发的行为操作,在下面代码中看到当key
为currentFrame
时添加一个CABasicAnimation
动画。
+ (BOOL)needsDisplayForKey:(NSString *)key {if ([key isEqualToString:@"currentFrame"]) {return YES;}return [super needsDisplayForKey:key];
}
- (id<CAAction>)actionForKey:(NSString *)event {if ([event isEqualToString:@"currentFrame"]) {CABasicAnimation *theAnimation = [CABasicAnimationanimationWithKeyPath:event];theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];theAnimation.fromValue = [[self presentationLayer] valueForKey:event];return theAnimation;}return [super actionForKey:event];
}
- (void)playFromFrame:(nonnull NSNumber *)fromStartFrametoFrame:(nonnull NSNumber *)toEndFramewithCompletion:(nullable LOTAnimationCompletionBlock)completion {if (_isAnimationPlaying) {return;}_playRangeStartFrame = fromStartFrame;_playRangeEndFrame = toEndFrame;if (completion) {self.completionBlock = completion;}if (!_sceneModel) {_isAnimationPlaying = YES;return;}BOOL playingForward = ((_animationSpeed > 0) && (toEndFrame.floatValue > fromStartFrame.floatValue))|| ((_animationSpeed < 0) && (fromStartFrame.floatValue > toEndFrame.floatValue));CGFloat leftFrameValue = MIN(fromStartFrame.floatValue, toEndFrame.floatValue);CGFloat rightFrameValue = MAX(fromStartFrame.floatValue, toEndFrame.floatValue);NSNumber *currentFrame = [self _frameForProgress:_animationProgress];currentFrame = @(MAX(MIN(currentFrame.floatValue, rightFrameValue), leftFrameValue));if (currentFrame.floatValue == rightFrameValue && playingForward) {currentFrame = @(leftFrameValue);} else if (currentFrame.floatValue == leftFrameValue && !playingForward) {currentFrame = @(rightFrameValue);}_animationProgress = [self _progressForFrame:currentFrame];CGFloat currentProgress = _animationProgress * (_sceneModel.endFrame.floatValue - _sceneModel.startFrame.floatValue);CGFloat skipProgress;if (playingForward) {skipProgress = currentProgress - leftFrameValue;} else {skipProgress = rightFrameValue - currentProgress;}NSTimeInterval offset = MAX(0, skipProgress) / _sceneModel.framerate.floatValue;if (!self.window) {_shouldRestoreStateWhenAttachedToWindow = YES;_completionBlockToRestoreWhenAttachedToWindow = self.completionBlock;self.completionBlock = nil;} else {float repeatCount = self.repeatCount == 0 ? HUGE_VALF : self.repeatCount;NSTimeInterval duration = (ABS(toEndFrame.floatValue - fromStartFrame.floatValue) / _sceneModel.framerate.floatValue);CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"currentFrame"];animation.speed = _animationSpeed;animation.fromValue = fromStartFrame;animation.toValue = toEndFrame;animation.duration = duration;animation.fillMode = kCAFillModeBoth;animation.repeatCount = _loopAnimation ? repeatCount : 1;animation.autoreverses = _autoReverseAnimation;animation.delegate = self;animation.removedOnCompletion = NO;if (offset != 0) {CFTimeInterval currentTime = CACurrentMediaTime();CFTimeInterval currentLayerTime = [self.layer convertTime:currentTime fromLayer:nil];animation.beginTime = currentLayerTime - (offset * 1 / _animationSpeed);}[_compContainer addAnimation:animation forKey:kCompContainerAnimationKey];_compContainer.shouldRasterize = NO;}_isAnimationPlaying = YES;
}
最底层的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[_contentsGroup updateWithFrame:newFrame withModifierBlock:nil forceLocalUpdate:forceUpdate];_maskLayer.currentFrame = newFrame;
}
它实际上完成了以下几件事:
- 根据子Layer的起始帧和结束帧判断当前帧子Layer是否显示
- 更新子Layer当前帧的透明度
- 更新子Layer当前帧的transform
- 更新子Layer中路径和形状等内容的变化
上面动画显示的2,3,4步都是通过XXInterpolator这种类,来从当前frame中计算出我们需要的值。下面看一个示例:
@interface LOTTransformInterpolator : NSObject
// 。。。
@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;
@property (nonatomic, strong, nullable) NSString *parentKeyName;
// 。。。
针对transform变换需要很多的信息,LOTTransformInterpolator
中提供了这些所需的信息。
- (CATransform3D)transformForFrame:(NSNumber *)frame {CATransform3D baseXform = CATransform3DIdentity;if (_inputNode) {baseXform = [_inputNode transformForFrame:frame];}CGPoint position = CGPointZero;if (_positionInterpolator) {position = [_positionInterpolator pointValueForFrame:frame];}if (_positionXInterpolator &&_positionYInterpolator) {position.x = [_positionXInterpolator floatValueForFrame:frame];position.y = [_positionYInterpolator floatValueForFrame:frame];}CGPoint anchor = [_anchorInterpolator pointValueForFrame:frame];CGSize scale = [_scaleInterpolator sizeValueForFrame:frame];CGFloat rotation = [_rotationInterpolator floatValueForFrame:frame];CATransform3D translateXform = CATransform3DTranslate(baseXform, position.x, position.y, 0);CATransform3D rotateXform = CATransform3DRotate(translateXform, rotation, 0, 0, 1);CATransform3D scaleXform = CATransform3DScale(rotateXform, scale.width, scale.height, 1);CATransform3D anchorXform = CATransform3DTranslate(scaleXform, -1 * anchor.x, -1 * anchor.y, 0);return anchorXform;
}
插值计算过程:
// 根据前一帧与后一帧进行计算
- (CGPoint)pointValueForFrame:(NSNumber *)frame {CGFloat progress = [self progressForFrame:frame];CGPoint returnPoint;if (progress == 0) {returnPoint = self.leadingKeyframe.pointValue;} else if (progress == 1) {returnPoint = self.trailingKeyframe.pointValue;} else if (!CGPointEqualToPoint(self.leadingKeyframe.spatialOutTangent, CGPointZero) ||!CGPointEqualToPoint(self.trailingKeyframe.spatialInTangent, CGPointZero)) {// Spatial Bezier pathCGPoint outTan = LOT_PointAddedToPoint(self.leadingKeyframe.pointValue, self.leadingKeyframe.spatialOutTangent);CGPoint inTan = LOT_PointAddedToPoint(self.trailingKeyframe.pointValue, self.trailingKeyframe.spatialInTangent);returnPoint = LOT_PointInCubicCurve(self.leadingKeyframe.pointValue, outTan, inTan, self.trailingKeyframe.pointValue, progress);} else {returnPoint = LOT_PointInLine(self.leadingKeyframe.pointValue, self.trailingKeyframe.pointValue, progress);}if (self.hasDelegateOverride) {return [self.delegate pointForFrame:frame.floatValuestartKeyframe:self.leadingKeyframe.keyframeTime.floatValueendKeyframe:self.trailingKeyframe.keyframeTime.floatValueinterpolatedProgress:progressstartPoint:self.leadingKeyframe.pointValueendPoint:self.trailingKeyframe.pointValuecurrentPoint:returnPoint];}return returnPoint;
}
CGPoint LOT_PointInLine(CGPoint A, CGPoint B, CGFloat T) {CGPoint C;C.x = A.x - ((A.x - B.x) * T);C.y = A.y - ((A.y - B.y) * T);return C;
}
当传入当前frame时,这些interpolator
会返回不同的数值,从而组成当前的transform
。这些不同的Interpolar
会根据自己的算法返回当前所需要的值,但是他们大体的流程都是一样的:
- 在关键帧数组中找到当前frame的前一个关键帧(leadingKeyframe)和后一个关键帧(trailingKeyframe)
- 计算当前frame 在 leadingKeyframe 和 trailingKeyframe 的进度(progress)
- 根据这个progress以及 leadingKeyframe,trailingKeyframe算出当前frame下的值。(不同的Interpolator算法不同)
小结:
设计师做设计,开发者写实现,各司其职。
Lottie内部帮我们做了json文件映射到不同类的不同属性中,通过一系列的计算,确定出每一帧的数据,然后完美的显示在屏幕上。开发者只需要通过创建方法创建、播放方法播放,简单几行代码,就可以实现炫酷的动画。