$ pod search Lottie
$ pod 'lottie-ios', '~> 2.5.0'
而後建立 pod 文件 $ pod init,而後將 lottie-ios 添加到 Podfile 中
最後執行 $ pod install 就完成了 Lottie 庫的接入
複製代碼
咱們能夠在 ae 中下載插件,將 aep 文件導出爲 json 形式,具體如何操做請看 Lottie開源動畫庫介紹ios
導出的 json 文件格式化後類型以下:web
在 Lottie 的 API 中咱們能夠看到,只須要簡單的 [firstAnimationView play]
便可播放動畫。到這裏簡單的需求基本完成,有時咱們還須要在動畫上添加文字和按鈕來讓這個 page 可以響應一些事件,這時候咱們就須要先將這個 LOTAniamtionView
放到視圖層級的最底層,使用 sendSubviewToBack:
這個方法便可簡單作到。而後再添加一個 backgroundView 做爲容器來盛放 Label,Button 等,當要求文字與按鈕也和動畫同樣有漸入效果時,咱們就能夠在滑動到這個 page 時作一個簡單的動畫,使用下面這個方法便可作到objective-c
-(void)animatedForPageOne {
[self.firstAnimationView playToProgress:1 withCompletion:nil];
[UIView animateWithDuration:1.5 animations:^{
self.p1bottomView.alpha = 1;
}];
}
複製代碼
當用戶若是滑動到第三個 page 後並無進入應用,而是滑動返回到第二個 page 時,例如文章開始時給出的動圖,這時咱們就須要當動畫播放完後,再返回動畫的第一幀,不然就會有一個跳幀的效果,很是很差看,具體的方法也很簡單,只須要在監聽到 ScrollView 滾動翻頁時設置 self.firstAnimationView.animationProgress = 0;
便可將上一個 page 的動畫調到第一幀,當執行返回時便可從第一幀開始從新播放。json
LOTAniamtionView
上添加其餘控件時,會發現有時候會出現與動畫不兼容的現象,好比遮擋等,這時候咱們能夠選擇修改控件的位置或者直接將動畫的某一部分刪除,這個過程也很是簡單,咱們只須要找到 json 文件,找到相應的控件,把它刪除便可。@interface LOTAnimationView : LOTView
// 默認從 main bundle 中加載 json 文件和圖片
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName NS_SWIFT_NAME(init(name:));
// 從給定的 bundle 中加載 json 文件和圖片,這個 animationName 參數實際上就是 json 文件的文件名
+ (nonnull instancetype)animationNamed:(nonnull NSString *)animationName inBundle:(nonnull NSBundle *)bundle NS_SWIFT_NAME(init(name:bundle:));
// 直接從給定的 json 文件中加載動畫,默認從 mainBundle 中加載
+ (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:));
/// 直接使用 LOTCompostion 來建立動畫效果
- (nonnull instancetype)initWithModel:(nullable LOTComposition *)model inBundle:(nullable NSBundle *)bundle;
/// 異步的從指定的 url 中加載動畫,這個 url 爲 webUrl
- (nonnull instancetype)initWithContentsOfURL:(nonnull NSURL *)url;
複製代碼
至於最後是如何初始化的,咱們能夠根據初始化方法在 LOTComposition
類中找到👇這個方法,這個方法用來解析 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 withFramerate:_framerate];
}
// 動畫layer層數組
NSArray *layersJSON = jsonDictionary[@"layers"];
if (layersJSON) {
_layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON
withAssetGroup:_assetGroup
withFramerate:_framerate];
}
[_assetGroup finalizeInitializationWithFramerate:_framerate];
}
複製代碼
在這個方法中,咱們注意到了它調用了圖片數組的初始化方法和 layer 層數組的初始化方法,具體來看一下:緩存
// LOTAssetGroup 初始化方法到最後咱們在 LOTAsset 中找到了這個解析 json 數據的方法
- (void)_mapFromJSON:(NSDictionary *)jsonDictionary
withAssetGroup:(LOTAssetGroup * _Nullable)assetGroup
withFramerate:(NSNumber *)framerate {
_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"]; // 對圖片 layer 的配置,具體來看一下
if (layersJSON) {
_layerGroup = [[LOTLayerGroup alloc] initWithLayerJSON:layersJSON
withAssetGroup:assetGroup
withFramerate:framerate];
}
複製代碼
LOTALayerGroup 文件中對 json 文件的解析bash
- (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];
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 的類型,這個變量是一個枚舉類型
// typedef enum : NSInteger {
// LOTLayerTypePrecomp,
// LOTLayerTypeSolid,
// LOTLayerTypeImage,
// LOTLayerTypeNull,
// LOTLayerTypeShape,
// LOTLayerTypeUnknown
// } LOTLayerType;
_layerType = layerType.integerValue;
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
if (jsonDictionary[@"sr"]) { // 縮放
_timeStretch = [jsonDictionary[@"sr"] copy];
} else {
_timeStretch = @1;
}
// 判斷 layer 是哪一種類型,而且作相應的處理
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);
}
}
}
}
複製代碼
由上能夠看到,在 LOTComposition , LOTLayer, LOTAssets 這幾個類中完成了 json 數據的解析。app
首先咱們能夠經過上述方法初始化一個 animationView, 而後繼續觀察 API 中的屬性和方法:異步
/// 判斷是否正在播放動畫
@property (nonatomic, readonly) BOOL isAnimationPlaying;
/// 判斷動畫是否須要循環播放
@property (nonatomic, assign) BOOL loopAnimation;
/// 若是這個屬性值和 loopAnimation 都爲 YES, 那麼動畫會先倒序播放,而後再正序播放
@property (nonatomic, assign) BOOL autoReverseAnimation;
// 設置 progress 的值 從 0 ~ 1,表示這個動畫執行的程度,當設定爲 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;
複製代碼
在上述屬性中咱們能夠看到一個緩存 cache 屬性,等一下咱們將會一塊兒來探究 Lottie 到底是如何緩存動畫的,緊接着咱們來接着看 LOTAniamtionView 的 API 中所定義的部分方法:ide
/*
* 這個方法表示 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
**/
- (void)playWithCompletion:(nullable LOTAnimationCompletionBlock)completion;
/// 簡單的播放到動畫結尾,播放完成後會調用完成block
- (void)play;
/// 暫停動畫而且調用完成block
- (void)pause;
/// 中止當前動畫而且倒回到最開始的那一幀,完成後調用block
- (void)stop;
/// 設置當前的動畫到指定的 frame,若是當前動畫正在播放則會中止動畫而且調用完成block
- (void)setProgressWithFrame:(nonnull NSNumber *)currentFrame;
/// 強制對如今的 frame 進行升級
- (void)forceDrawingUpdate;
/// 打印全部繼承鏈上的 keypath
- (void)logHierarchyKeypaths;
複製代碼
在 LOTAniamtionCache 的實現文件中能夠看到如下方法:
// 表示是否支持緩存
- (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];
}
}
// 具體方法以下:
// 能夠看到,LOTAnimationCache 維護了一個添加 key 和 value 到字典和一個數組
@implementation LOTAnimationCache {
NSMutableDictionary *animationsCache_;
NSMutableArray *lruOrderArray_;
}
// 當添加動畫時:首先須要判斷當前數組的 size 是否已經大於了最大的 size,若是是的話,則先清除最前面緩存的動畫,而後再添加新的動畫,而這個 const NSInteger 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];
}
複製代碼
接下來在閱讀 LOTAniamtionCache 的實現文件,能夠發現,整個 cache 是一個單例,也就是存在於 app 的整個生命週期中不會被銷燬,一旦 app 關閉,因爲數據存儲也僅僅是簡單的使用一個數組和一個字典來存儲,並未進行持久化處理,單例中所緩存的數據也會被銷燬,因此咱們對於動畫的緩存僅限於咱們在使用 app 時。
+ (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;
}
複製代碼