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