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)toProgress
withCompletion:(nullable LOTAnimationCompletionBlock)completion;
/*
* 播放从特定进度到特定进度的动画
* 动画将从其当前位置开始。
* 如果循环动画,则动画将从开始无限期地循环到结束进度
* 如果 loopAnimation 为 NO,则动画将停止并调用完成块。
*/
- (void)playFromProgress:(CGFloat)fromStartProgress
toProgress:(CGFloat)toEndProgress
withCompletion:(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 *)toFrame
withCompletion:(nullable LOTAnimationCompletionBlock)completion;
/*
* 将动画从特定帧播放到特定帧。
* 动画将从其当前位置开始。
* 如果循环动画,则动画将无限期地循环开始帧到结束帧。
* 如果 loopAnimation 为 NO,则动画将停止并调用完成块。
*/
- (void)playFromFrame:(nonnull NSNumber *)fromStartFrame
toFrame:(nonnull NSNumber *)toEndFrame
withCompletion:(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)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;
}
// 动画开始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:layersJSON
withAssetGroup:_assetGroup
withFramerate:_framerate];
}
[_assetGroup finalizeInitializationWithFramerate:_framerate];
}
在这个方法中,它调用了LOTAssetGroup
的图片数组的初始化方法和LOTLayerGroup
的layer层数组的初始化方法,生成了_assetGroup
并使用其初始化了_layerGroup
。具体来看一下。
LOTAssetGroup
LOTAssetGroup的初始化:
- (instancetype _Nonnull)initWithJSON:(NSArray * _Nonnull)jsonArray
withAssetBundle:(NSBundle * _Nullable)bundle
withFramerate:(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 *)jsonDictionary
withAssetGroup:(LOTAssetGroup * _Nullable)assetGroup
withFramerate:(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:layersJSON
withAssetGroup:assetGroup
withFramerate:framerate];
}
}
LOTALayerGroup
LOTALayerGroup对json文件的解析:
- (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];
if (layer.layerID) {
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
withFramerate:(NSNumber *)framerate {
_layerName = [jsonDictionary[@"nm"] copy]; // layer名
_layerID = [jsonDictionary[@"ind"] copy]; // layer的id,表示这是第几个layer
NSNumber *layerType = jsonDictionary[@"ty"]; // 表示layer的类型,这个变量是一个枚举类型
_layerType = layerType.integerValue;
// LOTLayerTypePrecomp,
// LOTLayerTypeSolid,
// LOTLayerTypeImage,
// LOTLayerTypeNull,
// LOTLayerTypeShape,
// LOTLayerTypeUnknown
if (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 *)layer
inLayerGroup:(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) {
// 有rootDirectory
NSString *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];
// 存在imageCache
if (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 fails
if (!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 = [CABasicAnimation
animationWithKeyPath:event];
theAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
theAnimation.fromValue = [[self presentationLayer] valueForKey:event];
return theAnimation;
}
return [super actionForKey:event];
}
- (void)playFromFrame:(nonnull NSNumber *)fromStartFrame
toFrame:(nonnull NSNumber *)toEndFrame
withCompletion:(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 path
CGPoint 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.floatValue
startKeyframe:self.leadingKeyframe.keyframeTime.floatValue
endKeyframe:self.trailingKeyframe.keyframeTime.floatValue
interpolatedProgress:progress
startPoint:self.leadingKeyframe.pointValue
endPoint:self.trailingKeyframe.pointValue
currentPoint: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文件映射到不同类的不同属性中,通过一系列的计算,确定出每一帧的数据,然后完美的显示在屏幕上。开发者只需要通过创建方法创建、播放方法播放,简单几行代码,就可以实现炫酷的动画。