Lottie是Airbnb开源的一套动画框架,它可以帮助把开发人员从动画的制作上解放出来。设计师可以直接通过AE设计并导出动画,客户端无需做处理就可以直接使用。这确实是一个伟大的创新,强烈推荐大家使用。关于Lottie的安装及使用这里就不啰嗦了,属于Baidu+翻墙+Google可以解决的问题。(https://github.com/airbnb/lottie-ios)
正好最近一段时间得闲,所以就看了看Lottie-iOS的源码,在这里记录一点,有不正确的地方也希望大家可以不吝指教。
1. 动画
关于iOS动画的一些基本知识可以参见 https://www.gitbook.com/book/zsisme/ios-/details 很棒的一本书,内容丰富讲解细致,推荐大家读读。
2. 代码组织
整套代码同时支持Mac和iOS为了方便我们只分析iOS的部分。
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#import
@compatibility_alias LOTView UIView;
#else
#import
@compatibility_alias LOTView NSView;
#endif
代码功能组织如下
AnimatableLayers: 动画View和Layer的相关定义和实现
AnimatableProperties: 属性动画的相关定义和实现
AnimationCache: 动画数据的LRU Cache
Extensions: 相关类的扩展
MacCompatability: 为支持Mac的一些移植文件
Models: 基础数据结构的定义
Private/PublicHeaders: Interface的定义
3. 代码详解
从PublicHeaders我们可以看到Lottie暴露出来的给我们的是两个类:
LOTAnimationView(动画的View)
LOTAnimationTransitionController(Controller了之间的专场动画)
本文着重分析LOTAnimationView
LOTAnimationView结构
大体上来讲LOTAnimationView的结构如下,负责动画显示的LOTCompositionLayer,记录动画状态的LOTAnimationState以及负责动画生命周期的CADisplayLink。
LOTCompositionLayer通过动画数据LOTComposition来初始化。Lottie的动画数据为json文件/数据,针对json文件在性能上也实现了针对LOTComposition数据的LRU Cache也就是LOTAnimationCache,这点我们可以从头文件定义和内部实现上可以看到。
+ (instancetype)animationNamed:(NSString *)animationName inBundle:(NSBundle *)bundle {
NSArray *components = [animationName componentsSeparatedByString:@"."];
animationName = components.firstObject;
LOTComposition *comp = [[LOTAnimationCache sharedCache] animationForKey:animationName];
if (comp) {
return [[LOTAnimationView alloc] initWithModel:comp];
}
NSError *error;
NSString *filePath = [bundle pathForResource:animationName ofType:@"json"];
NSData *jsonData = [[NSData alloc] initWithContentsOfFile:filePath];
NSDictionary *JSONObject = jsonData ? [NSJSONSerialization JSONObjectWithData:jsonData
options:0 error:&error] : nil;
if (JSONObject && !error) {
LOTComposition *laScene = [[LOTComposition alloc] initWithJSON:JSONObject];
[[LOTAnimationCache sharedCache] addAnimation:laScene forKey:animationName];
return [[LOTAnimationView alloc] initWithModel:laScene];
}
NSException* resourceNotFoundException = [NSException exceptionWithName:@"ResourceNotFoundException"
reason:[error localizedDescription]
userInfo:nil];
@throw resourceNotFoundException;
}
#import
@class LOTComposition;
@interface LOTAnimationCache : NSObject
+ (instancetype)sharedCache;
- (void)addAnimation:(LOTComposition *)animation forKey:(NSString *)key;
- (LOTComposition *)animationForKey:(NSString *)key;
@end
LOTAnimationCache的实现
LOTAnimationCache是一个LRU的Cache,如果不了解LRU Cache可以去https://en.wikipedia.org/wiki/Cache_replacement_policies 科普下
@implementation LOTAnimationCache {
NSMutableDictionary *animationsCache_; // cache数据
NSMutableArray *lruOrderArray_; // 通过key数组来保证lru顺序,最近使用的在数组最尾部,该淘汰的在数组最顶部,
}
- (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];
}
- (LOTComposition *)animationForKey:(NSString *)key {
LOTComposition *animation = [animationsCache_ objectForKey:key];
[lruOrderArray_ removeObject:key];
[lruOrderArray_ addObject:key];
return animation;
}
LOTComposition (构图数据)
@property (nonatomic, readonly) CGRect compBounds;
@property (nonatomic, readonly) NSNumber *startFrame;
@property (nonatomic, readonly) NSNumber *endFrame;
@property (nonatomic, readonly) NSNumber *framerate;
@property (nonatomic, readonly) NSTimeInterval timeDuration;
@property (nonatomic, readonly) LOTLayerGroup *layerGroup;
@property (nonatomic, readonly) LOTAssetGroup *assetGroup;
看到上面的属性值是不是一头雾水,但是也可以稍微做一些推测:动画包含了整个显示大小,时长,开始和结束帧,图层资源和图片资源,如果希望进一步理解则需要从底层数据结构往上进行分析。
所以我们看看AnimatableProperties,Models,是不是第一眼看上去觉得和一些我们已知的概念相似?animatable properties,是不是联想到了CALayer的属性动画?我们知道CALayer的属性值修改时会有隐式动画,我们可以温习一下 https://zsisme.gitbooks.io/ios-/content/chapter7/transactions.html
当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
事务是通过CATransaction类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)。
Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
在这里,这些属性值也是有异曲同工之妙的,我们可以看看定义
@interface LOTAnimatableColorValue ()
@property (nonatomic, readonly) NSArray *colorKeyframes;
@property (nonatomic, readonly) NSArray *keyTimes;
@property (nonatomic, readonly) NSArray *timingFunctions;
@property (nonatomic, readonly) NSTimeInterval delay;
@property (nonatomic, readonly) NSTimeInterval duration;
@property (nonatomic, readonly) NSNumber *startFrame;
@property (nonatomic, readonly) NSNumber *durationFrames;
@property (nonatomic, readonly) NSNumber *frameRate;
@end
所有的Animatable Properties都包含如上相似的结构,timingFunctions.count + 1 = colorKeyframes.count
包含了属性的值数组,以及开始结束帧,持续时长,帧与帧之间的时间函数等。而这些就是构成整个动画的基础,也可以简单的理解为一组更加灵活的动画属性。值得说一点的是,Lottie的动画核心基础是CAKeyframeAnimation 关键帧动画,是贯穿整个动画最底层的原理。
@protocol LOTAnimatableValue
- (CAKeyframeAnimation *)animationForKeyPath:(NSString *)keypath;
- (BOOL)hasAnimation;
@end
- (nullable CAKeyframeAnimation *)animationForKeyPath:(nonnull NSString *)keypath {
if (self.hasAnimation == NO) {
return nil;
}
CAKeyframeAnimation *keyframeAnimation = [CAKeyframeAnimation animationWithKeyPath:keypath];
keyframeAnimation.keyTimes = self.keyTimes;
keyframeAnimation.values = self.boundsKeyframes;
keyframeAnimation.timingFunctions = self.timingFunctions;
keyframeAnimation.duration = self.duration;
keyframeAnimation.beginTime = self.delay;
keyframeAnimation.fillMode = kCAFillModeForwards;
return keyframeAnimation;
}
引用一段关键帧动画的描述
关键帧动画,是CAPropertyAnimation的子类,与CABasicAnimation的区别是:
CABasicAnimation只能从一个数值(fromValue)变到另一个数值(toValue),而CAKeyframeAnimation会使用一个NSArray保存这些数值
属性说明:
values:上述的NSArray对象。里面的元素称为“关键帧”(keyframe)。动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧
path:代表路径可以设置一个CGPathRef、CGMutablePathRef,让图层按照路径轨迹移动。path只对CALayer的anchorPoint和position起作用。如果设置了path,那么values将被忽略
keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0,keyTimes中的每一个时间值都对应values中的每一帧。如果没有设置keyTimes,各个关键帧的时间是平分的
那么有了这么些属性值,再往上我们可以猜测应该到了构建layer的时候了。
在Models里我们可以看到有如下的定义
#import "LOTComposition.h"
#import "LOTLayer.h"
#import "LOTMask.h"
#import "LOTShapeCircle.h"
#import "LOTShapeFill.h"
#import "LOTShapeGroup.h"
#import "LOTShapePath.h"
#import "LOTShapeRectangle.h"
#import "LOTShapeStroke.h"
#import "LOTShapeTransform.h"
#import "LOTShapeTrimPath.h"
#import "LOTLayerGroup.h"
#import "LOTAsset.h"
这些类定义了Lottie的基础数据结构,注意的是LOTLayer等并不是图层,而是图层的数据,有兴趣的同学可以对比着CALayer的属性看一看,结构上有很多相通的地方。
再往上就是通过数据构成的AnimatableLayers
值得一提的是,在构建Layer上涉及到很多数学知识,有兴趣的可以研究下两个扩展文件
CGGeometry+LOTAdditions
CGRect LOT_RectIntegral(CGRect rect);
// Centering
// Returns a rectangle of the given size, centered at a point
CGRect LOT_RectCenteredAtPoint(CGPoint center, CGSize size, BOOL integral);
// Returns the center point of a CGRect
CGPoint LOT_RectGetCenterPoint(CGRect rect);
// Insetting
// Inset the rectangle on a single edge
CGRect LOT_RectInsetLeft(CGRect rect, CGFloat inset);
CGRect LOT_RectInsetRight(CGRect rect, CGFloat inset);
CGRect LOT_RectInsetTop(CGRect rect, CGFloat inset);
CGRect LOT_RectInsetBottom(CGRect rect, CGFloat inset);
// Inset the rectangle on two edges
CGRect LOT_RectInsetHorizontal(CGRect rect, CGFloat leftInset, CGFloat rightInset);
CGRect LOT_RectInsetVertical(CGRect rect, CGFloat topInset, CGFloat bottomInset);
// Inset the rectangle on all edges
CGRect LOT_RectInsetAll(CGRect rect, CGFloat leftInset, CGFloat rightInset, CGFloat topInset, CGFloat bottomInset);
// Framing
// Returns a rectangle of size framed in the center of the given rectangle
CGRect LOT_RectFramedCenteredInRect(CGRect rect, CGSize size, BOOL integral);
// Returns a rectangle of size framed in the given rectangle and inset
CGRect LOT_RectFramedLeftInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);
CGRect LOT_RectFramedRightInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);
CGRect LOT_RectFramedTopInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);
CGRect LOT_RectFramedBottomInRect(CGRect rect, CGSize size, CGFloat inset, BOOL integral);
CGRect LOT_RectFramedTopLeftInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);
CGRect LOT_RectFramedTopRightInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);
CGRect LOT_RectFramedBottomLeftInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);
CGRect LOT_RectFramedBottomRightInRect(CGRect rect, CGSize size, CGFloat insetWidth, CGFloat insetHeight, BOOL integral);
// Divides a rect into sections and returns the section at specified index
CGRect LOT_RectDividedSection(CGRect rect, NSInteger sections, NSInteger index, CGRectEdge fromEdge);
// Returns a rectangle of size attached to the given rectangle
CGRect LOT_RectAttachedLeftToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);
CGRect LOT_RectAttachedRightToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);
CGRect LOT_RectAttachedTopToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);
CGRect LOT_RectAttachedBottomToRect(CGRect rect, CGSize size, CGFloat margin, BOOL integral);
CGRect LOT_RectAttachedBottomLeftToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);
CGRect LOT_RectAttachedBottomRightToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);
CGRect LOT_RectAttachedTopRightToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);
CGRect LOT_RectAttachedTopLeftToRect(CGRect rect, CGSize size, CGFloat marginWidth, CGFloat marginHeight, BOOL integral);
// Combining
// Adds all values of the 2nd rect to the first rect
CGRect LOT_RectAddRect(CGRect rect, CGRect other);
CGRect LOT_RectAddPoint(CGRect rect, CGPoint point);
CGRect LOT_RectAddSize(CGRect rect, CGSize size);
CGRect LOT_RectBounded(CGRect rect);
CGPoint LOT_PointAddedToPoint(CGPoint point1, CGPoint point2);
CGRect LOT_RectSetHeight(CGRect rect, CGFloat height);
CGFloat LOT_PointDistanceFromPoint(CGPoint point1, CGPoint point2);
CGFloat LOT_DegreesToRadians(CGFloat degrees);
GLKMatrix4 LOT_GLKMatrix4FromCATransform(CATransform3D xform);
CATransform3D LOT_CATransform3DFromGLKMatrix4(GLKMatrix4 xform);
CATransform3D LOT_CATransform3DSlerpToTransform(CATransform3D fromXorm, CATransform3D toXform, CGFloat amount );
CGFloat LOT_RemapValue(CGFloat value, CGFloat low1, CGFloat high1, CGFloat low2, CGFloat high2 );
CGPoint LOT_PointByLerpingPoints(CGPoint point1, CGPoint point2, CGFloat value);
UIColor+Expanded
- (NSString *)LOT_colorSpaceString;
- (NSArray *)LOT_arrayFromRGBAComponents;
- (BOOL)LOT_red:(CGFloat *)r green:(CGFloat *)g blue:(CGFloat *)b alpha:(CGFloat *)a;
- (UIColor *)LOT_colorByLuminanceMapping;
- (UIColor *)LOT_colorByMultiplyingByRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
- (UIColor *) LOT_colorByAddingRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
- (UIColor *) LOT_colorByLighteningToRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
- (UIColor *) LOT_colorByDarkeningToRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha;
- (UIColor *)LOT_colorByMultiplyingBy:(CGFloat)f;
- (UIColor *) LOT_colorByAdding:(CGFloat)f;
- (UIColor *) LOT_colorByLighteningTo:(CGFloat)f;
- (UIColor *) LOT_colorByDarkeningTo:(CGFloat)f;
- (UIColor *)LOT_colorByMultiplyingByColor:(UIColor *)color;
- (UIColor *) LOT_colorByAddingColor:(UIColor *)color;
- (UIColor *) LOT_colorByLighteningToColor:(UIColor *)color;
- (UIColor *) LOT_colorByDarkeningToColor:(UIColor *)color;
- (NSString *)LOT_stringFromColor;
- (NSString *)LOT_hexStringValue;
+ (UIColor *)LOT_randomColor;
+ (UIColor *)LOT_colorWithString:(NSString *)stringToConvert;
+ (UIColor *)LOT_colorWithRGBHex:(UInt32)hex;
+ (UIColor *)LOT_colorWithHexString:(NSString *)stringToConvert;
+ (UIColor *)LOT_colorWithName:(NSString *)cssColorName;
+ (UIColor *)LOT_colorByLerpingFromColor:(UIColor *)fromColor toColor:(UIColor *)toColor amount:(CGFloat)amount;
搞清楚里面各个函数的作用,起码会多学些数学知识。
先写到这吧,等有空了再增加点。