【iOS】Animation 动画特效图片资源文件的处理实现策略

2020/05/25 iOS 共 12329 字,约 36 分钟

导读: 最近项目中需要实现一个动画特效,初始的设计思路是通过 UI 设计组给定的图片资源通过 UIImageView帧动画 实现,之后考虑可以通过 SVGA 文件播放资源文件也可以实现,但是这两种方式的实现都需要通过将较大的 图片资源文件SVGA 资源文件打包到 ipa 文件包中,无形之中增大了安装包的体积,对于用户来的下载安装来讲并不是一种很好的体验。

虽然实现上都比较方便,甚至可以通过打包安装包时不带对应的资源文件,通过用户打开 App 后远程缓存对应的图片资源文件或 SVGA 资源文件的方式,但是操作仍比较繁琐,仍需要花费比较多的流量进行缓存下载才能实现对应的动画特效,对于用户而言仍不是一种很好的方案。

最终调研发现,可以使用一种新的方案,Lottie File 的方式,只需要导入对应的开源库 lottie-ios、添加少量的配置文件,以及添加少量的代码,即可实现酷炫的动画效果。

此处会简单介绍一下 UIImageView 的帧动画、SVGA 动画、Lottie File 动画。至于苹果原生的 Animation 动画效果的实现方式,本篇文章中暂不介绍。

UIImageView 帧动画

UIImageView 帧动画简单介绍

对于 Video 实际上就是由一组组连续的图片共同合成的动态可视化视图。

所以,UIImageView 动画也是通过 UIImageView 加载一组连续的图片数组,形成的简单帧动画。

UIImageView 相关属性方法

// these allow a set of images to be animated. the array may contain multiple copies of the same

// 动画图片数组
open var animationImages: [UIImage]? // The array must contain UIImages. Setting hides the single image. default is nil
// 高亮动画图片数组
@available(iOS 3.0, *)
open var highlightedAnimationImages: [UIImage]? // The array must contain UIImages. Setting hides the single image. default is nil
// 动画持续时间
open var animationDuration: TimeInterval // for one cycle of images. default is number of images * 1/30th of a second (i.e. 30 fps)
// 重复次数(默认 0,无限循环)
open var animationRepeatCount: Int // 0 means infinite (default is 0)
// 获取是否动画中...
open var isAnimating: Bool { get }

// 开始动画
open func startAnimating()
// 结束动画
open func stopAnimating()

示例代码

UIImageViewFrameAnimation.gif

import UIKit

class UIImageViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        navigationItem.title = "UIImageView Frame Animation"
        view.backgroundColor = .white
        
        setupImageViewAnimation()
    }
    
    func setupImageViewAnimation() {
        
        //MARK: - Create UIImageView
        
        let imageV = UIImageView()
        imageV.center = view.center
        imageV.bounds = CGRect(x: 0, y: 0, width: 300, height: 300)
        imageV.backgroundColor = UIColor(white: 0.9, alpha: 1)
        view.addSubview(imageV)
        
        
        //MARK: - Add frame animation with UIImageView
        
        /// Add  images with 'UIImage' type to Array
        var array = [UIImage]()
        for i in 0...21 {
            array.append(UIImage(named: "image_\(i)") ?? UIImage())
        }
        
        /// animation duration
        imageV.animationDuration = 3.0
        /// animation images resources
        imageV.animationImages = array
        /// animation repeat count,0 means infinite
        imageV.animationRepeatCount = 0
        
        /// start animation
        imageV.startAnimating()
    }
}

SVGA 动画

SVGA 动画简单介绍

SVGA 是一种 跨平台 的开源动画格式,同时兼容 iOS / Android / Web。SVGA 除了使用简单,性能卓越,同时让动画开发分工明确,各自专注各自的领域,大大减少动画交互的沟通成本,提升开发效率。动画设计师专注动画设计,通过工具输出 svga 动画文件,提供给开发工程师在集成 svga player 之后直接使用。动画开发从未如此简单!

SVGA 除了使用简单,性能卓越,同时让动画开发分工明确,各自专注各自的领域,大大减少动画交互的沟通成本,提升开发效率。

动画设计师专注动画设计,通过工具输出 svga 动画文件,提供给开发工程师在集成 SVGAPlayer 之后直接使用。

—— 摘自 “SVGA 官网”

Similar to Lottie. Render After Effects / Animate CC (Flash) animations natively on Android and iOS, Web. 使用 SVGAPlayer 在 Android、iOS、Web中播放 After Effects / Animate CC (Flash) 动画。

—— 摘自 “GitHub - SVGA”

SVGA 官网

GitHub - SVGA

SVGA 支持平台

暂时已知的是,SVGA 已经支持 iOS / Android / Web / Flutter 平台。

SVGA 动画原理

SVGA 使用展示流程

SVGA 最初的 目标是为降低序列帧动画开销而生的,因此,性能问题一直是 SVGA 关注的焦点。SVGA 实质上做了一件非常重要的事情:

在动画播放前,一次性地上传所有纹理到 GPU,接着,在播放的过程中,这些纹理会被重复使用;CPU 与 GPU 交换的次数大大减少,同时,纹理的数目也在可控范围。内存、CPU、GPU 占用能达到最优状态。

SVGA 做的事情实际上非常简单,Converter 会负责从 FlashAE 源文件中提取所有动画元素(位图、矢量),并将其在时间轴中的每帧表现(位移、缩放、旋转、透明度)导出。 Player 会负责将这些信息还原至画布上。

SVGA 不同于 LottieLottie 需要在 Player 一层完整地将 After Effects 所有逻辑实现,而 SVGA 则将这些逻辑免去。因此,SVGA 可以同时支持 Flash,我们相信 Flash 以及其继承者 Animate CC 仍然有强大的生命力,以及完善的设计生态。

项目集成(iOS)

通过 Cocoapods 添加库依赖支持:

platform :ios, '11.0'

target '001-LottieAndSVGADemo' do
use_frameworks!

	pod 'SVGAPlayer'

source 'https://github.com/CocoaPods/Specs.git'

end

然后通过 pod install 安装依赖支持。

核心代码

GitHub - SVGA - SVGAParser.m

*********** 核心方法 ************

// 通过 URL 播放动画(底层实现依赖于 parseWithURLRequest 方法)
- (void)parseWithURL:(nonnull NSURL *)URL
     completionBlock:(void ( ^ _Nonnull )(SVGAVideoEntity * _Nullable videoItem))completionBlock
        failureBlock:(void ( ^ _Nullable)(NSError * _Nullable error))failureBlock;

// 最核心方法,通过网络请求 URLRequest 播放动画
// 原理:先通过 parseWithCacheKey 方法查询是否有对应的缓存,如果有直接返回;如果没有,则通过 NSURLSession 的 dataTaskWithRequest 方法进行缓存下载再返回
- (void)parseWithURLRequest:(nonnull NSURLRequest *)URLRequest
            completionBlock:(void ( ^ _Nonnull )(SVGAVideoEntity * _Nullable videoItem))completionBlock
               failureBlock:(void ( ^ _Nullable)(NSError * _Nullable error))failureBlock;

// 通过 data 加载播放动画
- (void)parseWithData:(nonnull NSData *)data
             cacheKey:(nonnull NSString *)cacheKey
      completionBlock:(void ( ^ _Nullable)(SVGAVideoEntity * _Nonnull videoItem))completionBlock
         failureBlock:(void ( ^ _Nullable)(NSError * _Nonnull error))failureBlock;

// 本地加载播放动画,通过文件资源名
- (void)parseWithNamed:(nonnull NSString *)named
              inBundle:(nullable NSBundle *)inBundle
       completionBlock:(void ( ^ _Nullable)(SVGAVideoEntity * _Nonnull videoItem))completionBlock
          failureBlock:(void ( ^ _Nullable)(NSError * _Nonnull error))failureBlock;


*********** 代理方法 ************

// 动画完成时回调
- (void)svgaPlayerDidFinishedAnimation:(SVGAPlayer *)player;
// 当前执行动画对应图库资源的 frame 帧 index
- (void)svgaPlayerDidAnimatedToFrame:(NSInteger)frame;
// 动画执行进度
- (void)svgaPlayerDidAnimatedToPercentage:(CGFloat)percentage;


*********** 本地 ************

// 创建 SVGAPlayer 实例
SVGAPlayer *localSVGAPlayer = [[SVGAPlayer alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[self.view addSubview:localSVGAPlayer]; // Add subview by yourself.

// 创建 SVGAParser 实例,并使用对应的方法加载动画
SVGAParser *parser = [[SVGAParser alloc] init];
[parser parseWithNamed:@"posche" inBundle:nil completionBlock:^(SVGAVideoEntity * _Nonnull videoItem) {
    localSVGAPlayer.videoItem = svgaItem
    localSVGAPlayer.startAnimation()
} failureBlock:nil];


*********** 远程 ************

// 创建 SVGAPlayer 实例
SVGAPlayer *player = [[SVGAPlayer alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[self.view addSubview:player]; // Add subview by yourself.

// 创建 SVGAParser 实例,并使用对应的方法加载动画
SVGAParser *parser = [[SVGAParser alloc] init];
[parser parseWithURL:[NSURL URLWithString:@"https://github.com/yyued/SVGA-Samples/blob/master/posche.svga?raw=true"] completionBlock:^(SVGAVideoEntity * _Nullable videoItem) {
    if (videoItem != nil) {
        player.videoItem = videoItem;
        [player startAnimation];
    }
} failureBlock:nil];

示例代码

SVGA 示例动画

import UIKit
import SVGAPlayer

class SVGAViewController: UIViewController {
    
    let remoteSVGAUrl = "https://gitee.com/jijiucheng/MyFile/raw/master/airplane.svga"
    let localSVGAName = "airplane"

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "SVGA Animation"
        view.backgroundColor = .white
        
        setupSVGAAnimation()
    }
    
    func setupSVGAAnimation() {
        
        //MARK: - Local
        
        let localLabel = UILabel(frame: CGRect(x: 20, y: 100, width: UIScreen.main.bounds.size.width - 20*2, height: 25))
        localLabel.text = "Local SVGA Animation"
        localLabel.textAlignment = .center
        localLabel.font = UIFont.boldSystemFont(ofSize: 18)
        view.addSubview(localLabel)
        
        let localSVGAPlayer = SVGAPlayer(frame: CGRect(x: 20, y: 150, width: 130, height: 130))
        localSVGAPlayer.backgroundColor = .darkGray
//        localSVGAPlayer.delegate = self
        localSVGAPlayer.loops = 0       // repeat count,0 means infinite
        localSVGAPlayer.clearsAfterStop = false     // Remove or clear after stop
        view.addSubview(localSVGAPlayer)

        let localSVGAParser = SVGAParser()
        localSVGAParser.parse(withNamed: localSVGAName, in: nil, completionBlock: { (svgaItem) in
            localSVGAPlayer.videoItem = svgaItem
            localSVGAPlayer.startAnimation()
        }, failureBlock: nil)
        
        
        //MARK: - Remote
        
        let remoteLabel = UILabel(frame: CGRect(x: 20, y: 400, width: UIScreen.main.bounds.size.width - 20*2, height: 25))
        remoteLabel.text = "Remote SVGA Animation"
        remoteLabel.textAlignment = .center
        remoteLabel.font = UIFont.boldSystemFont(ofSize: 18)
        view.addSubview(remoteLabel)
        
        let remoteSVGAPlayer = SVGAPlayer(frame: CGRect(x: 20, y: 450, width: 150, height: 150))
        remoteSVGAPlayer.backgroundColor = .darkGray
        view.addSubview(remoteSVGAPlayer)
        
        if let url = URL(string: remoteSVGAUrl) {
            let remoteSVGAParser = SVGAParser()
            remoteSVGAParser.parse(with: url, completionBlock: { (svgaItem) in
                remoteSVGAPlayer.videoItem = svgaItem
                remoteSVGAPlayer.startAnimation()
            }, failureBlock: { (error) in
                print("--------------------- \(String(describing: error))")
            })
        }
    }
}

extension SVGAViewController: SVGAPlayerDelegate {
    
    /// SVGA animation progress
    func svgaPlayerDidAnimated(toPercentage percentage: CGFloat) {
        print("precent ------- \(percentage)")
    }
    
    /// SVGA frame index with images resource
    func svgaPlayerDidAnimated(toFrame frame: Int) {
        print("frame ------- \(frame)")
    }
    
    /// doing after SVGA animation end or stop
    func svgaPlayerDidFinishedAnimation(_ player: SVGAPlayer!) {
        print("play end ---------------")
    }
}

备注:

此处示例代码中测试的远程端加载 SVGA 链接,无法正常播放,暂时原因未明,需要后面继续调试测试。

Lottie File 动画

Lottie File 动画简单介绍

Airbnb 开发的 Lottie 是一个将 After Effects 动画提供给任意一个 iOS,macOS,Android 还有 React Native 原生 APP 的文件库。这些动画通过一个叫 Bodymovin 的开源 After Effects 插件,以 JSON 文件的形式进行输出。Lottie 通过 JSON 格式下载动画数据并实时提供给开发者。

项目集成(iOS)

通过 Cocoapods 添加库依赖支持:

platform :ios, '11.0'

target '001-LottieAndSVGADemo' do
use_frameworks!

  pod 'lottie-ios'

source 'https://github.com/CocoaPods/Specs.git'

end

然后通过 pod install 安装依赖支持。

常用属性方法

// 是否循环播放动画
/// Tells the animation to loop indefinitely.
open var loopAnimation: Bool

// 当支持循环播放时,是否反向播放
/// The animation will play forward and then backwards if loopAnimation is also YES
open var autoReverseAnimation: Bool

// 播放速度
/// Sets the speed of the animation. Accepts a negative value for reversing animation.
open var animationSpeed: CGFloat

// 播放时间,只读属性,要设置时长可以通过 animationSpeed 设置播放速度   
/// Read only of the duration in seconds of the animation at speed of 1
open var animationDuration: CGFloat { get }

示例代码

Lottie 示例动画

import UIKit
import Lottie

class LottieViewController: UIViewController {
    
    /// 源资源文件链接:https://lottiefiles.com/159-servishero-loading
    let remoteUrl = "https://assets5.lottiefiles.com/datafiles/Hc0DflKIkYg1j3u/data.json"
    let localName = "159-servishero-loading.json"

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Lottie 动画"
        view.backgroundColor = .gray
        
        setupLottieAnimation()
    }
    
    func setupLottieAnimation() {
        
        /// local
        
        let localLabel = UILabel(frame: CGRect(x: 20, y: 100, width: UIScreen.main.bounds.size.width - 20*2, height: 25))
        localLabel.text = "Local Lottie Animation"
        localLabel.textAlignment = .center
        localLabel.font = UIFont.boldSystemFont(ofSize: 18)
        view.addSubview(localLabel)
        
        let localLottie = LOTAnimationView(name: localName)
        localLottie.backgroundColor = .white
        localLottie.frame = CGRect(x: 100, y: 150, width: 200, height: 200)
        localLottie.contentMode = .scaleAspectFit
        localLottie.animationSpeed = 3
        localLottie.loopAnimation = true
        view.addSubview(localLottie)
        
        localLottie.play()
        
        
        /// remote
        
        let remoteLabel = UILabel(frame: CGRect(x: 20, y: 400, width: UIScreen.main.bounds.size.width - 20*2, height: 25))
        remoteLabel.text = "Remote Lottie Animation"
        remoteLabel.textAlignment = .center
        remoteLabel.font = UIFont.boldSystemFont(ofSize: 18)
        view.addSubview(remoteLabel)
        
        if let url = URL(string: remoteUrl) {
            let remoteLottie = LOTAnimationView(contentsOf: url)
            remoteLottie.backgroundColor = .white
            remoteLottie.frame = CGRect(x: 100, y: 450, width: 200, height: 200)
            remoteLottie.contentMode = .scaleAspectFit
            remoteLottie.animationSpeed = 0.5
            remoteLottie.loopAnimation = true
            view.addSubview(remoteLottie)
            
            remoteLottie.play()
        }
    }
}

SVGA 和 Lottie 对比

性能特点对比

多种动画库方案对比 SVGA 和 Lottie 性能对比

SVGA 和 Lottie 思路对比

SVGA 动画库源码思路
  • 一帧一帧

  • 通过设置帧率,来生成一个配置文件,使得每一帧都有一个配置,每一帧都是关键帧,通过帧率去刷每一帧的画面,这个思路跟 gif 很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程)。

Lottie 动画库源码思路
  • 一层一层

  • 完全按照设计工具的设计思路来进行还原,将动画脚本导出并解析。动画脚本非常的轻量。
  • 将所有的动画拆成多个层级,每个层级 layer 都有一个动画配置,播放时解析多个 layer 的配置,并给每个 layer 做相应的动画。也达到了图片可以复用。当需要解析高阶插值(二次线性方程,贝塞尔曲线方程)时,性能相对而言差一点。

SVGA 和 Lottie 设计开发成本对比

SVGA 设计开发成本

不支持类型:

  • 不支持复杂的矢量形状图层;
  • AE自带的渐变、生成、描边、擦除…
  • 对设计工具原生动画不友好,对图片动画友好(适合礼物场景)

优点:

  • 资源包小
  • 测试工具齐全
  • 三端可用
  • 回调完整
  • Protobuf 序列化结构数据格式,序列化的数据体更小,传递效率比 xml,json 更高。
  • 导出工具开源(开源地址

缺点:

  • 每个礼物播放时都去新解压,需要改一套缓存策略
  • svga 用 zlib 打包(字节流数据压缩程序库),不方便解压和追踪包内容。

插入动画头像功能:

  • 支持,需定义一套专属的头像配置的协议。
Lottie 设计开发成本

不支持类型:

  • 基本满足所有种类的矢量动画和图片动画。

优点:

  • 三端可用
  • 回调完整
  • 项目已经存在一套缓存逻辑
  • 当前的库可以满足业务需求,不需要二次开发
  • 有导出插件

缺点:

  • 资源包相较SVGA而言会大一倍多
  • 图片需要重命名 & 偶先播不出来动效。

插入动画头像功能:

  • 已经支持。

SVGA 和 Lottie 对比

CreateJS/Lottie 实现的原理来看,都是将设计软件中的时间轴完整地导出来,包括里面的各种关键帧信息、矢量路径、样式等等。其中,最为关键的是『关键帧』!由于使用的是关键帧动画描述,那么对应的 Player 就难免变得复杂起来。如果只是简单的一次线性方程,比如,物体从 A 点匀速移动至 B 点,还 OK。但是,当遇到二次线性方程、贝塞尔曲线方程这些高阶插值计算的时候,Player 就会很吃力。(矢量动画就是通过一些公式能表达的矢量线条和色块,以此来表示每一帧动画)。

CreateJS 可以导出 Flash 动画,Lottie 可以导出 After Effects 动画,但是它们都不能导出对方的动画(每种设计工具的脚本都是不一致的),这也意味着,你家的设计师只能使用其中一种设计工具。

Lottie 在导出位图动画方面不够友好,从 UI 设计师和开发人员使用角度来看都比较麻烦,开发人员需要先将做好的效果打包成 ZIP,再解压到本地目录,才能播放。

SVGA 使用另外一套逻辑,它不关心关键帧,因为 SVGA 里面的每一帧都是关键帧 !也就是说,SVGA 已经在导出动画的时候,把每一帧的信息都计算好了,如此一来,Player 也就不用关心插值计算的过程。正因为如此,SVGA 可以同时支持 Flash 和 After Effects 的导出,只需要在各自转换器中计算差值就好了。

SVGA 在设计之初就支持位图元素,在 1.x 格式时使用 ZIP 进行打包,在 2.x 格式中,直接使用 ProtoBuf+Zlib 方式打包,作为开发者,完全不用关心应该怎样下载、解压、获取位图文件,直接把 svga 文件扔进去 Player 就可以了。

功能上,SVGA 更贴合直播应用场景,SVGA所提供的动态文本、动态图像功能都是其它库所不能提供的

—— 摘自 “掘金 - 直播App中Android酷炫礼物动画实现方案(下篇):SVGA由来与Lottie的对比”

推荐参考博客

Demo

参考链接

版权声明

原文作者苜蓿鬼仙(苜蓿、jijiucheng)

原文链接GitHub.io - 苜蓿鬼仙 - 【iOS】Animation 动画特效图片资源文件的处理实现策略

发表日期:2020/05/25 09:00:00

更新日期:2020/07/08 14:40:00

-

GitHubGitHub - jijiucheng

个人博客GitHub.io - 苜蓿鬼仙

小专栏小专栏 - 苜蓿鬼仙

掘金掘金 - 苜蓿鬼仙

微博微博 - 苜蓿鬼仙

公众号微信 - 苜蓿小站

小程序微信 - 苜蓿小站

文档信息

Search

    Table of Contents