一般app中都會帶有動畫,而如果是一些復雜的動畫,不但實現成本比較高,而且實現效果可能還不能達到UI想要的效果,於是我們可以借助lottie來完成我們想要的動畫。
lottie動畫1.gif
lottie動畫2.gif
Lottie動畫庫
- Lottie是Airbnb開源的一個庫,通過bodymovin可以將AE設計好的動畫導出為json格式的文件,交付給開發完成動畫。以上兩個gif就是用AE導出的動畫。
- 關於Lottie有很多優點,Airbnb的人員也一直在更新,不到一年時間已經有1w+star,UI只需要導出一份json和圖片即可完成動畫開發,Lottie有ios和安卓庫,兩端都適用(想想要是用gif或者自己實現,那需要很大的成本並且還不一定做的好)。
動畫管理類
- 有了
Lottie這個庫,開發也不用費精力去斟酌動畫的實現,只需調用api完成實現,但是這樣產生一個問題:當動畫數量比較多時,如果都放在bundle下,會造成app體積增大。所以我們的做法是把所有的json和圖片資源放在服務器分別打包成zip包,然后download下來放在library/caches下解壓,播放時根據禮物的id去尋找資源播放。
動畫管理.png
- 每次啟動app時,動畫管理類都會去請求api獲取當前所有禮物
id,version和url,如果有新的禮物或者禮物需要更新動畫,則根據url下載zip包。 - 下載完zip包,使用zipZap去完成解壓操作,並解壓到指定的路徑下.
/** 解壓 @param filePath zip路徑 @param locationPatch 解壓文件夾的路徑 */ - (void)unZipWithFilePath:(NSString *)filePath locationPatch:(NSString *)locationPatch success:(OBDynamicGiftManagerDownloadSuccessBlock)successBlock failureBlock:(OBDynamicGiftManagerDownloadFailureBlock)failureBlock { NSFileManager* fileManager = [NSFileManager defaultManager]; NSURL* path = [NSURL fileURLWithPath:locationPatch]; NSString * zipPath = filePath; ZZArchive* archive = [ZZArchive archiveWithURL:[NSURL fileURLWithPath:zipPath] error:nil]; // ZZArchive* archive = [ZZArchive archiveWithURL:path error:nil]; NSError *error = nil; for (ZZArchiveEntry* entry in archive.entries) { NSURL* targetPath = [path URLByAppendingPathComponent:entry.fileName]; if (entry.fileMode & S_IFDIR) // check if directory bit is set [fileManager createDirectoryAtURL:targetPath withIntermediateDirectories:YES attributes:nil error:&error]; else { // Some archives don't have a separate entry for each directory // and just include the directory's name in the filename. // Make sure that directory exists before writing a file into it. [fileManager createDirectoryAtURL: [targetPath URLByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&error]; [[entry newDataWithError:nil] writeToURL:targetPath atomically:NO]; } } if (error) { if (failureBlock) { failureBlock(error); } } else { if (successBlock) { successBlock(); } } }
- 同時把獲取到的禮物
id、version等數據保存到數據庫中,並且如果下載zip包還需要把下載的狀態記錄要數據庫中,使用的是fmdb。
// 插入禮物相關數據 - (BOOL)insertPresentGif:(OBPresentGif *)presentGif { __block BOOL result = NO; [[self databaseQueue] inDatabase:^(FMDatabase *db) { if (![db open]) { NSLog(@"打開失敗!"); }; NSString *query = [NSString stringWithFormat:@"select * from presentGifts where presentId= '%@'", presentGif.presentId]; FMResultSet *set = [db executeQuery:query]; if (![set next]) { // 如果數據不存在再執行插入數據操作 result = [db executeUpdate:@"insert OR REPLACE into presentGifts (presentId, name, download, version)values(?,?,?,?)", presentGif.presentId, presentGif.name, presentGif.download, presentGif.version]; } [db close]; }]; return result; } // 檢查對比禮物版本號 - (BOOL)checkPresentGifVersionWithPresentGif:(OBPresentGif *)presentGif { __block BOOL result = YES; __block long currentVersion; [[self databaseQueue] inDatabase:^(FMDatabase *db) { if (![db open]) { NSLog(@"打開失敗!"); }; FMResultSet *set = [db executeQuery:@"select version from presentGifts WHERE presentId = (?)", presentGif.presentId]; while ([set next]) { if ([set longForColumn:@"version"]) { currentVersion = [set longForColumn:@"version"]; } // 判斷版本是否一樣 result = [presentGif.version longValue] == currentVersion ? YES : NO; } [db close]; }]; return result; } // 更新禮物zip包下載狀態,如果下載失敗或者沒下載完,那么下次啟動 / 播放禮物時將會檢查並添加到下載隊列下載 - (BOOL)updatePresentGiftDownLoadState:(NSInteger )state presentId:(NSInteger )presentId { __block BOOL result = NO; [[self databaseQueue] inDatabase:^(FMDatabase *db) { if (![db open]) { NSLog(@"打開失敗!"); }; NSString *str = [NSString stringWithFormat:@"UPDATE presentGifts SET downLoadStatus = %@ WHERE presentId = %@", [NSNumber numberWithInteger:state], [NSNumber numberWithInteger:presentId]]; result = [db executeUpdate:str]; [db close]; }]; return result; } // 根據禮物id獲取url - (NSString *)downloadUrlWithPresentId:(NSInteger)presentId { __block NSString *downloadUrl; [[self databaseQueue] inDatabase:^(FMDatabase *db) { if (![db open]) { NSLog(@"打開失敗!"); }; FMResultSet *set = [db executeQuery:@"select download from presentGifts WHERE presentId = (?)", [NSNumber numberWithInteger:presentId]]; while ([set next]) { if ([set stringForColumn:@"download"]) { downloadUrl = [set stringForColumn:@"download"]; } } [db close]; }]; return downloadUrl; }
動畫的播放
假如在同一時間有多個動畫進行播放,那么還得考慮一個問題:是放在一個隊列里有序播放,還是后面的動畫頂掉前面的動畫播放? 然而機智的產品讓我們兩套都做了。。。
隊列播放
- 從IM協議收到禮物動畫消息后,把禮物動畫添加到一個數組里面,然后播放順序播放數組里面的動畫。
- 因為業務需要,用戶在觀看禮物時,可以進行個別操作,所以還需要控制動畫的圖層位置。
/** 動畫隊列播放 @param giftId 禮物id @param view 父視圖 @param belowView belowView */ - (void)showDynamicGiftWithGiftId:(NSInteger)giftId toView:(nonnull UIView *)view belowView:(nullable UIView *)belowView { NSString *dynamicGiftPath = [self getDynamicGiftPathWithGiftId:giftId]; NSString *jsonPath = [dynamicGiftPath stringByAppendingPathComponent:@"data.json"]; // 判斷data.json是否存在 if ([[NSFileManager defaultManager] fileExistsAtPath:jsonPath]) { [_jsonPathQueryArray addObject:jsonPath]; if (view && belowView) { NSArray *viewArr = [NSArray arrayWithObjects:view, belowView, nil]; [self animationToView:viewArr]; } else if (belowView == nil) { NSArray *viewArr = [NSArray arrayWithObjects:view, nil]; [self animationToView:viewArr]; } } // 如果不存在,應該重新下載. else { [self redownloadDynamicGiftWithGiftId:giftId]; } } - (void)animationToView:(NSArray *)viewArr { if (self.isAnimationPlaying == YES) { return; } else { if (viewArr.count == 2) { UIView *backgroundView = viewArr[0]; UIView *belowView = viewArr[1]; if (_closeButtonAddingToView == NO) { // 添加關閉按鈕,可以關閉動畫 [backgroundView addSubview:self.closeButton]; _closeButtonAddingToView = YES; [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(backgroundView); make.bottom.equalTo(backgroundView).offset(SCREEN_RU(-64)); }]; [backgroundView layoutIfNeeded]; } kWSELF if (_jsonPathQueryArray.count > 0) { // 加載json動畫 NSString *jsonPath = [_jsonPathQueryArray firstObject]; _currentAnimation = [LOTAnimationView animationWithFilePath:jsonPath]; _currentAnimation.frame = CGRectMake(0, 0, ScreenWidth, ScreenHeight); // 緩存動畫 _currentAnimation.cacheEnable = YES; [backgroundView insertSubview:_currentAnimation belowSubview:belowView]; self.isAnimationPlaying = YES; [_currentAnimation playWithCompletion:^(BOOL animationFinished) { [_currentAnimation removeFromSuperview]; // 移除動畫 self.isAnimationPlaying = NO; if (_jsonPathQueryArray.count > 1) { // 播放動畫完成后 檢測播放隊列是否還有需要播放的動畫,如果有,移除播放完的動畫,然后播放新的。 [_jsonPathQueryArray removeObjectAtIndex:0]; [wself animationToView:viewArr]; } else { // 如果是最后一個動畫,播放完后,移除動畫,並且把關閉按鈕也移除掉。 if (_jsonPathQueryArray.count == 1) { [_jsonPathQueryArray removeObjectAtIndex:0]; } [wself.closeButton removeFromSuperview]; _closeButtonAddingToView = NO; } }]; } } } }
頂替播放
- 在播放動畫的時候,如果IM來了個新動畫,就把之前的動畫移除,直接播放新的動畫。
// 如果有動畫正在播放,並且超過一定時間 則關閉 if (_currentAnimation && (_currentAnimation.animationProgress >= 0.3)) { [_currentAnimation pause]; [_currentAnimation removeFromSuperview]; _currentAnimation = nil; [self replaceModeAnimationShowDynamicGiftWithGiftId:giftId toView:view belowView:belowView]; } else if (!_currentAnimation) { [self replaceModeAnimationShowDynamicGiftWithGiftId:giftId toView:view belowView:belowView]; } - (void)replaceModeAnimationShowDynamicGiftWithGiftId:(NSInteger)giftId toView:(UIView *)view belowView:(UIView *)belowView { NSString *dynamicGiftPath = [self getDynamicGiftPathWithGiftId:giftId]; NSString *jsonPath = [dynamicGiftPath stringByAppendingPathComponent:@"data.json"]; // 判斷data.json是否存在 if ([[NSFileManager defaultManager] fileExistsAtPath:jsonPath]) { // 加載動畫 _currentAnimation = [LOTAnimationView animationWithFilePath:jsonPath]; self.animationDuration = _currentAnimation.animationDuration; _currentAnimation.frame = CGRectMake(0, 0, ScreenWidth, ScreenHeight); _currentAnimation.contentMode = UIViewContentModeScaleAspectFill; _currentAnimation.cacheEnable = YES; if (_closeButtonAddingToView == NO) { [view addSubview:self.closeButton]; _closeButtonAddingToView = YES; [self.closeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(view); make.bottom.equalTo(view).offset(SCREEN_RU(-64)); }]; } self.isAnimationPlaying = YES; kWSELF // 由於在block中防止循環引用需要用weak self, 但是block中 多次使用wself, 有可能在調用第一個方法后釋放掉,所以需要強引用 weak self 保證在block內不被釋放 if (view && belowView) { __strong __typeof (wself) sself = wself; [view insertSubview:_currentAnimation belowSubview:belowView]; [_currentAnimation playWithCompletion:^(BOOL animationFinished) { [sself->_currentAnimation removeFromSuperview]; _currentAnimation = nil; [wself.closeButton removeFromSuperview]; _closeButtonAddingToView = NO; sself.isAnimationPlaying = NO; }]; } else if (belowView == nil) { __strong __typeof (wself) sself = wself; [view insertSubview:_currentAnimation belowSubview:self.closeButton]; [_currentAnimation playWithCompletion:^(BOOL animationFinished) { [sself->_currentAnimation removeFromSuperview]; _currentAnimation = nil; [wself.closeButton removeFromSuperview]; _closeButtonAddingToView = NO; sself.isAnimationPlaying = NO; }]; } } // 如果不存在,應該重新下載. else { [self redownloadDynamicGiftWithGiftId:giftId]; } }
- 最后再配置一個開關在后台控制兩個模式的切換就完成了。
作者:iOShuihui
鏈接:https://www.jianshu.com/p/c1b3fcc7b16d
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。
