Lottie是Airbnb開(kāi)源的一套動(dòng)畫(huà)框架晴叨,它可以幫助把開(kāi)發(fā)人員從動(dòng)畫(huà)的制作上解放出來(lái)。設(shè)計(jì)師可以直接通過(guò)AE設(shè)計(jì)并導(dǎo)出動(dòng)畫(huà),客戶(hù)端無(wú)需做處理就可以直接使用昔头。這確實(shí)是一個(gè)偉大的創(chuàng)新伊群,強(qiáng)烈推薦大家使用考杉。關(guān)于Lottie的安裝及使用這里就不啰嗦了策精,屬于Baidu+翻墻+Google可以解決的問(wèn)題。(https://github.com/airbnb/lottie-ios)
正好最近一段時(shí)間得閑崇棠,所以就看了看Lottie-iOS的源碼咽袜,在這里記錄一點(diǎn),有不正確的地方也希望大家可以不吝指教枕稀。
1. 動(dòng)畫(huà)
關(guān)于iOS動(dòng)畫(huà)的一些基本知識(shí)可以參見(jiàn) https://www.gitbook.com/book/zsisme/ios-/details 很棒的一本書(shū)询刹,內(nèi)容豐富講解細(xì)致,推薦大家讀讀萎坷。
2. 代碼組織
整套代碼同時(shí)支持Mac和iOS為了方便我們只分析iOS的部分凹联。
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#import <UIKit/UIKit.h>
@compatibility_alias LOTView UIView;
#else
#import <AppKit/AppKit.h>
@compatibility_alias LOTView NSView;
#endif
代碼功能組織如下
AnimatableLayers: 動(dòng)畫(huà)View和Layer的相關(guān)定義和實(shí)現(xiàn)
AnimatableProperties: 屬性動(dòng)畫(huà)的相關(guān)定義和實(shí)現(xiàn)
AnimationCache: 動(dòng)畫(huà)數(shù)據(jù)的LRU Cache
Extensions: 相關(guān)類(lèi)的擴(kuò)展
MacCompatability: 為支持Mac的一些移植文件
Models: 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)的定義
Private/PublicHeaders: Interface的定義
3. 代碼詳解
從PublicHeaders我們可以看到Lottie暴露出來(lái)的給我們的是兩個(gè)類(lèi):
LOTAnimationView(動(dòng)畫(huà)的View)
LOTAnimationTransitionController(Controller了之間的專(zhuān)場(chǎng)動(dòng)畫(huà))
本文著重分析LOTAnimationView
LOTAnimationView結(jié)構(gòu)
大體上來(lái)講LOTAnimationView的結(jié)構(gòu)如下,負(fù)責(zé)動(dòng)畫(huà)顯示的LOTCompositionLayer哆档,記錄動(dòng)畫(huà)狀態(tài)的LOTAnimationState以及負(fù)責(zé)動(dòng)畫(huà)生命周期的CADisplayLink蔽挠。
LOTCompositionLayer通過(guò)動(dòng)畫(huà)數(shù)據(jù)LOTComposition來(lái)初始化。Lottie的動(dòng)畫(huà)數(shù)據(jù)為json文件/數(shù)據(jù)瓜浸,針對(duì)json文件在性能上也實(shí)現(xiàn)了針對(duì)LOTComposition數(shù)據(jù)的LRU Cache也就是LOTAnimationCache澳淑,這點(diǎn)我們可以從頭文件定義和內(nèi)部實(shí)現(xiàn)上可以看到。
+ (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 <Foundation/Foundation.h>
@class LOTComposition;
@interface LOTAnimationCache : NSObject
+ (instancetype)sharedCache;
- (void)addAnimation:(LOTComposition *)animation forKey:(NSString *)key;
- (LOTComposition *)animationForKey:(NSString *)key;
@end
LOTAnimationCache的實(shí)現(xiàn)
LOTAnimationCache是一個(gè)LRU的Cache插佛,如果不了解LRU Cache可以去https://en.wikipedia.org/wiki/Cache_replacement_policies 科普下
@implementation LOTAnimationCache {
NSMutableDictionary *animationsCache_; // cache數(shù)據(jù)
NSMutableArray *lruOrderArray_; // 通過(guò)key數(shù)組來(lái)保證lru順序杠巡,最近使用的在數(shù)組最尾部,該淘汰的在數(shù)組最頂部朗涩,
}
- (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 (構(gòu)圖數(shù)據(jù))
@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;
看到上面的屬性值是不是一頭霧水忽孽,但是也可以稍微做一些推測(cè):動(dòng)畫(huà)包含了整個(gè)顯示大小,時(shí)長(zhǎng)谢床,開(kāi)始和結(jié)束幀兄一,圖層資源和圖片資源,如果希望進(jìn)一步理解則需要從底層數(shù)據(jù)結(jié)構(gòu)往上進(jìn)行分析识腿。
所以我們看看AnimatableProperties出革,Models,是不是第一眼看上去覺(jué)得和一些我們已知的概念相似渡讼?animatable properties骂束,是不是聯(lián)想到了CALayer的屬性動(dòng)畫(huà)?我們知道CALayer的屬性值修改時(shí)會(huì)有隱式動(dòng)畫(huà)成箫,我們可以溫習(xí)一下 https://zsisme.gitbooks.io/ios-/content/chapter7/transactions.html
當(dāng)你改變一個(gè)屬性展箱,Core Animation是如何判斷動(dòng)畫(huà)類(lèi)型和持續(xù)時(shí)間的呢?實(shí)際上動(dòng)畫(huà)執(zhí)行的時(shí)間取決于當(dāng)前事務(wù)的設(shè)置蹬昌,動(dòng)畫(huà)類(lèi)型取決于圖層行為混驰。
事務(wù)實(shí)際上是Core Animation用來(lái)包含一系列屬性動(dòng)畫(huà)集合的機(jī)制,任何用指定事務(wù)去改變可以做動(dòng)畫(huà)的圖層屬性都不會(huì)立刻發(fā)生變化,而是當(dāng)事務(wù)一旦提交的時(shí)候開(kāi)始用一個(gè)動(dòng)畫(huà)過(guò)渡到新值栖榨。
事務(wù)是通過(guò)CATransaction類(lèi)來(lái)做管理昆汹,這個(gè)類(lèi)的設(shè)計(jì)有些奇怪,不像你從它的命名預(yù)期的那樣去管理一個(gè)簡(jiǎn)單的事務(wù)婴栽,而是管理了一疊你不能訪(fǎng)問(wèn)的事務(wù)满粗。CATransaction沒(méi)有屬性或者實(shí)例方法,并且也不能用+alloc和-init方法創(chuàng)建它愚争。但是可以用+begin和+commit分別來(lái)入椨辰裕或者出棧。
任何可以做動(dòng)畫(huà)的圖層屬性都會(huì)被添加到棧頂?shù)氖聞?wù)轰枝,你可以通過(guò)+setAnimationDuration:方法設(shè)置當(dāng)前事務(wù)的動(dòng)畫(huà)時(shí)間劫扒,或者通過(guò)+animationDuration方法來(lái)獲取值(默認(rèn)0.25秒)。
Core Animation在每個(gè)run loop周期中自動(dòng)開(kāi)始一次新的事務(wù)(run loop是iOS負(fù)責(zé)收集用戶(hù)輸入狸膏,處理定時(shí)器或者網(wǎng)絡(luò)事件并且重新繪制屏幕的東西),即使你不顯式的用[CATransaction begin]開(kāi)始一次事務(wù)添怔,任何在一次run loop循環(huán)中屬性的改變都會(huì)被集中起來(lái)湾戳,然后做一次0.25秒的動(dòng)畫(huà)。
在這里广料,這些屬性值也是有異曲同工之妙的砾脑,我們可以看看定義
@interface LOTAnimatableColorValue ()
@property (nonatomic, readonly) NSArray *colorKeyframes;
@property (nonatomic, readonly) NSArray<NSNumber *> *keyTimes;
@property (nonatomic, readonly) NSArray<CAMediaTimingFunction *> *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都包含如上相似的結(jié)構(gòu),timingFunctions.count + 1 = colorKeyframes.count
包含了屬性的值數(shù)組,以及開(kāi)始結(jié)束幀艾杏,持續(xù)時(shí)長(zhǎng)韧衣,幀與幀之間的時(shí)間函數(shù)等。而這些就是構(gòu)成整個(gè)動(dòng)畫(huà)的基礎(chǔ)购桑,也可以簡(jiǎn)單的理解為一組更加靈活的動(dòng)畫(huà)屬性畅铭。值得說(shuō)一點(diǎn)的是,Lottie的動(dòng)畫(huà)核心基礎(chǔ)是CAKeyframeAnimation 關(guān)鍵幀動(dòng)畫(huà)勃蜘,是貫穿整個(gè)動(dòng)畫(huà)最底層的原理硕噩。
@protocol LOTAnimatableValue <NSObject>
- (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;
}
引用一段關(guān)鍵幀動(dòng)畫(huà)的描述
關(guān)鍵幀動(dòng)畫(huà),是CAPropertyAnimation的子類(lèi)缭贡,與CABasicAnimation的區(qū)別是:
CABasicAnimation只能從一個(gè)數(shù)值(fromValue)變到另一個(gè)數(shù)值(toValue)炉擅,而CAKeyframeAnimation會(huì)使用一個(gè)NSArray保存這些數(shù)值
屬性說(shuō)明:
values:上述的NSArray對(duì)象。里面的元素稱(chēng)為“關(guān)鍵幀”(keyframe)阳惹。動(dòng)畫(huà)對(duì)象會(huì)在指定的時(shí)間(duration)內(nèi)谍失,依次顯示values數(shù)組中的每一個(gè)關(guān)鍵幀
path:代表路徑可以設(shè)置一個(gè)CGPathRef、CGMutablePathRef莹汤,讓圖層按照路徑軌跡移動(dòng)快鱼。path只對(duì)CALayer的anchorPoint和position起作用。如果設(shè)置了path,那么values將被忽略
keyTimes:可以為對(duì)應(yīng)的關(guān)鍵幀指定對(duì)應(yīng)的時(shí)間點(diǎn)攒巍,其取值范圍為0到1.0嗽仪,keyTimes中的每一個(gè)時(shí)間值都對(duì)應(yīng)values中的每一幀。如果沒(méi)有設(shè)置keyTimes柒莉,各個(gè)關(guān)鍵幀的時(shí)間是平分的
那么有了這么些屬性值闻坚,再往上我們可以猜測(cè)應(yīng)該到了構(gòu)建layer的時(shí)候了。
在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"
這些類(lèi)定義了Lottie的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)兢孝,注意的是LOTLayer等并不是圖層窿凤,而是圖層的數(shù)據(jù),有興趣的同學(xué)可以對(duì)比著CALayer的屬性看一看跨蟹,結(jié)構(gòu)上有很多相通的地方雳殊。
再往上就是通過(guò)數(shù)據(jù)構(gòu)成的AnimatableLayers
值得一提的是,在構(gòu)建Layer上涉及到很多數(shù)學(xué)知識(shí)窗轩,有興趣的可以研究下兩個(gè)擴(kuò)展文件
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;
搞清楚里面各個(gè)函數(shù)的作用夯秃,起碼會(huì)多學(xué)些數(shù)學(xué)知識(shí)。
先寫(xiě)到這吧痢艺,等有空了再增加點(diǎn)仓洼。