SVGA动画框架介绍

2019-10-13 Views svga android

人都是视觉动物,一个App的动效做的炫酷,优雅,是很吸引用户使用的。

Android实现复杂动效的方式有如下几种:

  • 原生补间动画和属性动画实现
  • 自定义View draw canvas实现
  • 序列帧,使用帧动画显示,apng
  • GIF动图实现
  • Lottie,svga
  • 显示视频

自定义View:需要自己实现动画的细节,开发效率和效果可能不够好。

APNG:一整个png的序列图。png是支持alpha通道的,因此显示效果和以png为主的帧动画一致。但android原生ImageView并不支持这种图片格式的显示,需要第三方库去支持。

Lottie:
Lottie是Airbnb推出的开源跨平台动画库,通过AE做出动效导出json文件,然后移动端使用播放器进行原生渲染播放。
image

SVGA:
SVGA是YY直播公司开源的跨平台动效播放方案,和Lottie有着很相似的功能。

svga和svg关系?

svg:

Scalable Vector Graphics 可缩放矢量图形,是一个基于XML语言,描述二维矢量图形的图形格式。
svg是由w3c制定,是一个开放标准。常见的图片格式如png,jpg,gif等都是位图,位图是由点构成的,矢量图则是由一些形状元素构成。该图中显示放大位图可以看到点,而放大矢量图看到的仍然是形状。SVG属于矢量图,因此能够无级缩放,而不会导致马赛克。

svga是描述动效的文件,由Adobe After Effects导出动效,内部包含矢量图像,音效等,在1.x格式时使用ZIP进行打包,在2.x格式中,直接使用ProtoBuf+Zlib方式打包。因此svga文件不可直接打开预览。当然我们做为移动端开发人员不用关心svga内部文件结构实现。

protocol buffers:
Protocol buffers 是一种语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。Protocol buffers 在序列化数据方面,它是灵活的,高效的。相比于 XML 来说,Protocol buffers 更加小巧,更加快速,更加简单。一旦定义了要处理的数据的数据结构之后,就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

下面开始着重介绍一下svga框架。

image

上图是svga.io官网上的介绍图。从图中我们可知,svga格式动效支持Android、iOS、web。设计师在After Effects完成动效设计,使用SVGA Converter工具将动效资源转成svga文件。客户端使用SVGA-Parser将动效生成相应的player进行播放。

下面我们针对Android平台源码进行分析。

SVGA动画播放示例
显示一个asset中svga动效流程如下:

//显示svga动效 demo
private void loadAnimation() {
    //创建Parser
    SVGAParser parser = new SVGAParser(this);
    //从Asset文件夹中加载svga文件
    parser.decodeFromAssets("test.svga", new SVGAParser.ParseCompletion() {
        @Override
        public void onComplete(@NotNull SVGAVideoEntity videoItem) {
            //将转化后的VideoEntity设置给SVGAImageView
            animationView.setVideoItem(videoItem);
            //开启动效
            animationView.stepToFrame(0, true);
        }
        @Override
        public void onError() {

        }
    });
}

SVGA动画文件解析

SVGAParser

SVGAParser类是将svga文件转化成SVGAImageView识别的VideoEntity的转化类。根据源码可知,它支持三种方式加载svga文件。

decodeFromAssets(string, callback)
decodeFromURL(string,callback)
decodeFromInputStream(inputStream,callback)
image.png

最终都会将已有的svga以InputStream方式加载,我们看一下decodeFromInputStream代码

fun decodeFromInputStream(inputStream: InputStream, cacheKey: String, callback: ParseCompletion, closeInputStream: Boolean = false) {
    threadPoolExecutor.execute {
        try {
            readAsBytes(inputStream)?.let { bytes ->
                //检查当前传入文件的是否有缓存
                if (bytes.size > 4 && bytes[0].toInt() == 80 && bytes[1].toInt() == 75 && bytes[2].toInt() == 3 && bytes[3].toInt() == 4) {
                    //检查缓存文件
                    if (!buildCacheDir(cacheKey).exists()) {
                        ByteArrayInputStream(bytes).use {
                            //解压到该缓存目录下
                            unzip(it, cacheKey)
                        }
                    }
                    //从缓存中加载svga文件
                    this.decodeFromCacheKey(cacheKey, callback)
                }
                else {
                    inflate(bytes)?.let {
                        //使用SVGAVideoEntity类将传入的inputStream
                        val videoItem = SVGAVideoEntity(MovieEntity.ADAPTER.decode(it), File(cacheKey))
                        //将VideoEntity回调到主线程处理
                        videoItem.prepare {
                            this.invokeCompleteCallback(videoItem, callback)
                        }
                    }
                }
            }
        } catch (e: java.lang.Exception) {
            this.invokeErrorCallback(e, callback)
        } finally {
            if (closeInputStream) {
                inputStream.close()
            }
        }
    }
}

MovieEntity是SVGA文件通过protocol buffers解释器解释后的产物。它内部包括了关键帧sprites、图片images、音频资源audios等信息。

debug-svga.png

SVGAVideoEntity就是将MovieEntity的属性拿出来设置给自己,并通过传入的cacheKey文件添加缓存信息。

class SVGAVideoEntity {
    ...
    var antiAlias = true
    var videoSize //动画显示大小
    var FPS = 15 //fps值
    var frames: Int = 0 //帧数
    //绘制帧
    internal var sprites: List<SVGAVideoSpriteEntity> = listOf()
    //音频
    internal var audios: List<SVGAAudioEntity> = listOf()
    //使用soundPool播放
    internal var soundPool: SoundPool? = null
    //图片
    internal var images = HashMap<String, Bitmap>()
    //缓存路径
    private var cacheDir: File

    constructor(obj: JSONObject, cacheDir: File) {
        this.cacheDir = cacheDir
        obj.optJSONObject("movie")?.let {
            it.optJSONObject("viewBox")?.let {
                videoSize = SVGARect(0.0, 0.0, it.optDouble("width", 0.0), it.optDouble("height", 0.0))
            }
            FPS = it.optInt("fps", 20)
            frames = it.optInt("frames", 0)
        }
        resetImages(obj)
        resetSprites(obj)
    }
    ...
    }

SVGA动画播放

梳理完svga文件解析,下面介绍一下动画是如何播放的。
svga动画的播放主要涉及到3个类:SVGAImageView、SVGADrawable、SVGACanvasDrawer。

如下图所示,SVGAImageView接收解析后的VideoEntitiy数据然后包装成SVGADrawable,通过SVGADrawable内部的SVGADrawar来绘制动画图形。

svga-imageview.png

SVGAImageView是显示动画的类,继承自ImageView。主要关注两个方法:setVideoItem和startAnimtion。

fun setVideoItem(videoItem: SVGAVideoEntity?, dynamicItem: SVGADynamicEntity?) {
    if (videoItem == null) {
        setImageDrawable(null)
        return
    }
    //将传入的VideoEntity和DynamicEntity包装成一个SVGADrawable
    val drawable = SVGADrawable(videoItem, dynamicItem ?: SVGADynamicEntity())
    drawable.cleared = clearsAfterStop
    //将此Drawable设置为imageView的Drawable
    setImageDrawable(drawable)
}


fun startAnimation(range: SVGARange?, reverse: Boolean = false) {
    stopAnimation(false)
    //当前的Drawable是SVGADrawable
    val drawable = drawable as? SVGADrawable ?: return
    //初始化Drawable的属性
    drawable.cleared = false
    drawable.scaleType = scaleType
    //VideoItem中记录着动画的帧数和显示内容
    drawable.videoItem.let {
        var durationScale = 1.0
        //确定起始帧
        val startFrame = Math.max(0, range?.location ?: 0)
        val endFrame = Math.min(it.frames - 1, ((range?.location ?: 0) + (range?.length ?: Int.MAX_VALUE) - 1))
        val animator = ValueAnimator.ofInt(startFrame, endFrame)
        //这个try-catch代码块主要是恢复在开发者选项中设置的动画缩放参数,防止在播放SVGA动画时出现帧数错误
        try {
            ...
        } catch (e: Exception) {}
        animator.interpolator = LinearInterpolator()
        //动画时长是由帧数和FPS决定的
        animator.duration = ((endFrame - startFrame + 1) * (1000 / it.FPS) / durationScale).toLong()
        animator.repeatCount = if (loops <= 0) 99999 else loops - 1
        animator.addUpdateListener {
            //更新关键帧
            drawable.currentFrame = animator.animatedValue as Int
            callback?.onStep(drawable.currentFrame, ((drawable.currentFrame + 1).toDouble() / drawable.videoItem.frames.toDouble()))
        }
        ...忽略其他...
    }
}

SVGADrawable内部有个关键的属性currentFrame,用来记录当前绘制的哪一帧,发生改变时调用自身的invalidateSelf()方法进行绘制。

var currentFrame = 0
    internal set (value) {
        if (field == value) {
            return
        }
        field = value
        invalidateSelf()
    }

SVGACanvasDrawer是最终的绘制类,主要调用drawFrame来绘制每个元素

override fun drawFrame(canvas: Canvas, frameIndex: Int, scaleType: ImageView.ScaleType) {
    super.drawFrame(canvas,frameIndex, scaleType)
    this.pathCache.onSizeChanged(canvas)
    //获取当前要绘制的数据
    val sprites = requestFrameSprites(frameIndex)
    sprites.forEach {
        //绘制每一个元素
        drawSprite(it, canvas, frameIndex)
    }
    //播放音频
    playAudio(frameIndex)
}

流程基本梳理完成了,简单来说就是svga内部记录了所有要绘制的元素和每一帧信息,直接通过遍历当前帧进行绘制即可,效率比较高。

下面做一个svga和Lottie的对比

以Lottie官方给出的点赞动效为例,我下载了最新版Adobe After Effects 2019,并且安装了svga和Lottie导出动画的插件BodyMovin和SGVA Converter。分别导出这个动效的json文件和svga文件

并且放到一个简单页面中播放对比。两个文件都放置在asset文件夹中,播放页面中没有其他UI元素。

svga

demo-svga.gif

Lottie

demo-lottie.gif
文件 文件大小 CPU 内存占用 GPU程式分析
svga 11k 6% 108.8m 如下图:每一帧都在绿线以下
Lottie 35k 13% 121.2m 绿色和蓝色较长表示进行了过多的运算

svga

svga-gpu.png

Lottie
lottie-gpu.png

分析可知:
Lottie文件是导出的json文件,内部的元素明文显示。svga则是使用了protocol buffers+zlib方式做了压缩,文件体积比较小。在播放动效时,svga在导出时已经做了高阶差值计算,因此只用绘制图形即可,相较于Lottie绘制效率较高。

参考:

  • https://zh.wikipedia.org/wiki/%E5%8F%AF%E7%B8%AE%E6%94%BE%E5%90%91%E9%87%8F%E5%9C%96%E5%BD%A2
  • https://halfrost.com/protobuf_encode/
  • https://svga.io/intro.html
  • https://jfson.github.io/2018/01/08/41-anim/
  • https://jfson.github.io/2018/06/21/49-svga/

赏 支付宝扫码打赏 微信打赏

△