iOS 动画(基于Lottie封装)

一般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
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

转载于:https://www.cnblogs.com/soulDn/p/9713367.html

封装了常见的动画类,很好用 注释非常详细 如下: + (void)showAnimationType:(NSString *)type withSubType:(NSString *)subType duration:(CFTimeInterval)duration timingFunction:(NSString *)timingFunction view:(UIView *)theView { /** CATransition * * @see http://www.dreamingwish.com/dream-2012/the-concept-of-coreanimation-programming-guide.html * @see http://geeklu.com/2012/09/animation-in-ios/ * * CATransition 常用设置及属性注解如下: */ CATransition *animation = [CATransition animation]; /** delegate * * 动画的代理,如果你想在动画开始和结束的时候做一些事,可以设置此属性,它会自动回调两个代理方法. * * @see CAAnimationDelegate (按下command键点击) */ animation.delegate = self; /** duration * * 动画持续时间 */ animation.duration = duration; /** timingFunction * * 用于变化起点和终点之间的插值计算,形象点说它决定了动画运行的节奏,比如是均匀变化(相同时间变化量相同)还是 * 先快后慢,先慢后快还是先慢再快再慢. * * 动画的开始与结束的快慢,有五个预置分别为(下同): * kCAMediaTimingFunctionLinear 线性,即匀速 * kCAMediaTimingFunctionEaseIn 先慢后快 * kCAMediaTimingFunctionEaseOut 先快后慢 * kCAMediaTimingFunctionEaseInEaseOut 先慢后快再慢 * kCAMediaTimingFunctionDefault 实际效果是动画中间比较快. */ /** timingFunction * * 当上面的预置不能满足你的需求的时候,你可以使用下面的两个方法来自定义你的timingFunction * 具体参见下面的URL * * @see http://developer.apple.com/library/ios/#documentation/Cocoa/Reference/CAMediaTimingFunction_class/Introduction/Introduction.html * * + (id)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y; * * - (id)initWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y; */ animation.timingFunction = [CAMediaTimingFunction functionWithName:timingFunction]; /** fillMode * * 决定当前对象过了非active时间段的行为,比如动画开始之前,动画结束之后. * 预置为: * kCAFillModeRemoved 默认,当动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态 * kCAFillModeForwards 当动画结束后,layer会一直保持着动画最后的状态 * kCAFillModeBackwards 和kCAFillModeForwards相对,具体参考上面的URL * kCAFillModeBoth kCAFillModeForwards和kCAFillModeBackwards在一起的效果 */ animation.fillMode = kCAFillModeForwards; /** removedOnCompletion * * 这个属性默认为YES.一般情况下,不需要设置这个属性. * * 但如果是CAAnimation动画,并且需要设置 fillMode 属性,那么需要将 removedOnCompletion 设置为NO,否则 * fillMode无效 */ // animation.removedOnCompletion = NO; /** type * * 各种动画效果 其中除了'fade', `moveIn', `push' , `reveal' ,其他属于似有的API(我是这么认为的,可以点进去看下注释). * ↑↑↑上面四个可以分别使用'kCATransitionFade', 'kCATransitionMoveIn', 'kCATransitionPush', 'kCATransitionReveal'来调用. * @"cube" 立方体翻滚效果 * @"moveIn" 新视图移到旧视图上面 * @"reveal" 显露效果(将旧视图移开,显示下面的新视图) * @"fade" 交叉淡化过渡(不支持过渡方向) (默认为此效果) * @"pageCurl" 向上翻一页 * @"pageUnCurl" 向下翻一页 * @"suckEffect" 收缩效果,类似系统最小化窗口时的神奇效果(不支持过渡方向) * @"rippleEffect" 滴水效果,(不支持过渡方向) * @"oglFlip" 上下左右翻转效果 * @"rotate" 旋转效果 * @"push" * @"cameraIrisHollowOpen" 相机镜头打开效果(不支持过渡方向) * @"cameraIrisHollowClose" 相机镜头关上效果(不支持过渡方向) */ /** type * * kCATransitionFade 交叉淡化过渡 * kCATransitionMoveIn 新视图移到旧视图上面 * kCATransitionPush 新视图把旧视图推出去 * kCATransitionReveal 将旧视图移开,显示下面的新视图 */ animation.type = type; /** subtype * * 各种动画方向 * * kCATransitionFromRight; 同字面意思(下同) * kCATransitionFromLeft; * kCATransitionFromTop; * kCATransitionFromBottom; */ /** subtype * * 当type为@"rotate"(旋转)的时候,它也有几个对应的subtype,分别为: * 90cw 逆时针旋转90° * 90ccw 顺时针旋转90° * 180cw 逆时针旋转180° * 180ccw 顺时针旋转180° */ /** * type与subtype的对应关系(必看),如果对应错误,动画不会显现. * * @see http://iphonedevwiki.net/index.php/CATransition */ animation.subtype = subType; /** * 所有核心动画和特效都是基于CAAnimation,而CAAnimation是作用于CALayer的.所以把动画添加到layer上. * forKey 可以是任意字符串. */ [theView.layer addAnimation:animation forKey:nil]; }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值