一般app中都会带有动画,而如果是一些复杂的动画,不但实现成本比较高,而且实现效果可能还不能达到UI想要的效果,于是我们可以借助lottie来完成我们想要的动画。
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去寻找资源播放。
- 每次启动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];
}
}
- 最后再配置一个开关在后台控制两个模式的切换就完成了。