zoukankan      html  css  js  c++  java
  • 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
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    sqlserver-一次updlock和withnolock和with check option 的报错原因分析
    类库文件引用web服务报错解决方法-在 ServiceModel 客户端配置部分中,找不到引用协定的默认终结点元素
    用timer自定义计划任务时间
    console 程序随系统启动及隐藏当前程序窗口
    爱积多合作农场正式上线试运营
    CSS-三列布局
    CSS-垂直居中
    form表单重置
    CSS3 选择器
    Tab选项卡
  • 原文地址:https://www.cnblogs.com/soulDn/p/9713367.html
Copyright © 2011-2022 走看看