一般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获取当前所有礼物idversionurl,如果有新的礼物或者礼物需要更新动画,则根据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();
}
}
}
  • 同时把获取到的礼物idversion等数据保存到数据库中,并且如果下载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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

iOS 动画(基于Lottie封装)的更多相关文章

  1. 基于iOS 10、realm封装的下载器

    代码地址如下:http://www.demodashi.com/demo/11653.html 概要 在决定自己封装一个下载器前,我本以为没有那么复杂,可在实际开发过程中困难重重,再加上iOS10和X ...

  2. iOS 动画

    图层树.寄宿图以及图层几何学(一)图层的树状结构 技术交流新QQ群:414971585 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命 ...

  3. iOS 动画学习

    图层树.寄宿图以及图层几何学(一)图层的树状结构 技术交流新QQ群:414971585 巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 Core Animation其实是一个令人误解的命 ...

  4. iOS 动画基础

    原文:http://www.cnblogs.com/lujianwenance/p/5733846.html   今天说一下有关动画的基础,希望能帮助到一些刚接触iOS动画或者刚开始学习iOS的同学, ...

  5. IOS 动画专题 --iOS核心动画

    iOS开发系列--让你的应用“动”起来 --iOS核心动画 概览 通过核心动画创建基础动画.关键帧动画.动画组.转场动画,如何通过UIView的装饰方法对这些动画操作进行简化等.在今天的文章里您可以看 ...

  6. IOS动画(Core Animation)总结 (参考多方文章)

    一.简介 iOS 动画主要是指Core Animation框架.官方使用文档地址为:Core Animation Guide. Core Animation是IOS和OS X平台上负责图形渲染与动画的 ...

  7. 简析iOS动画原理及实现——Core Animation

    本文转载至 http://www.tuicool.com/articles/e2qaYjA 原文  https://tech.imdada.cn/2016/06/21/ios-core-animati ...

  8. iOS之基于FreeStreamer的简单音乐播放器(模仿QQ音乐)

    代码地址如下:http://www.demodashi.com/demo/11944.html 天道酬勤 前言 作为一名iOS开发者,每当使用APP的时候,总难免会情不自禁的去想想,这个怎么做的?该怎 ...

  9. 基于better-scroll封装一个上拉加载下拉刷新组件

    1.起因 上拉加载和下拉刷新在移动端项目中是很常见的需求,遂自己便基于better-scroll封装了一个下拉刷新上拉加载组件. 2.过程 better-scroll是目前比较好用的开源滚动库,提供很 ...

随机推荐

  1. JavaScript模板引擎使用

    1. [代码]tmpl.js     // Simple JavaScript Templating// John Resig - http://ejohn.org/ - MIT Licensed(f ...

  2. C#在一段数字区间内随机生成若干个互不相同的随机数

    /// <summary>        /// Random ra=new Random();  系统自动选取当前时前作随机种子:        /// Random ra=new Ra ...

  3. codeforces 691B B. s-palindrome(水题)

    题目链接: B. s-palindrome 题意: 问给定的字符串是否是镜面对称; 思路: 直接看哪些字母是镜面对称的就行: AC代码: //#include <bits/stdc++.h> ...

  4. 管理 Word 博客账户

    1.1 多个博客账户 笔者的电脑上,Word 2013 有多个博客账户,如下图所示: 图1.1 多个博客账户 这些账户的名称在 Word 里是自动生成的,无法更改.账户一多就无法与相应的网站一一对应, ...

  5. BZOJ_4327_JSOI2012 玄武密码_AC自动机

    BZOJ_4327_JSOI2012 玄武密码_AC自动机 Description 在美丽的玄武湖畔,鸡鸣寺边,鸡笼山前,有一块富饶而秀美的土地,人们唤作进香河.相传一日,一缕紫气从天而至,只一瞬间便 ...

  6. 「LuoguP4752」牧 Divided Prime(判质数

    Description 给定一个数字 A,这个 A 由 a1,a2,⋯,aN相乘得到. 给定一个数字 B,这个 B 由 b1,b2,⋯,bM相乘得到. 如果 A/B 是一个质数,请输出YES,否则输出 ...

  7. GridFS大文件的添加、获取、查看、删除

    GridFS是一种在MongoDB中存储大二进制文件的机制,使用GridFS的原因有以下几种: 存储巨大的文件,比如视频.高清图片等. 利用GridFS可以简化需求. GridFS会直接利用已经建立的 ...

  8. PDB文件说明

    文/玄魂 .PDB文件,全称为“程序数据库”文件.我们使用它(更确切的说是看到它被应用)大多数场景是调试应用程序.目前我们对.PDB文件的普遍认知是它存储了被编译文件的调试信息,作为符号文件存在.那么 ...

  9. 【转】IntelliJ IDEA2017.3激活

    原文地址:https://blog.csdn.net/qq_27686779/article/details/78870816 http://idea.java.sx/ 简单快捷!! ———————— ...

  10. HTTP 请求的组成 方法 已经 请求的状态码

    HTTP请求是指从客户端到服务器端的请求消息. 包括:消息首行中,对资源的请求方法.资源的标识符及使用的协议.从客户端到服务器端的请求消息包括,消息首行中,对资源的请求方法.资源的标识符及使用的协议. ...