SVGA源码



### SVG 概念
* SVG 实际上指的是设计软件中的概念:SVG图片格式,一种矢量图形。


* 另一个角度来讲一张图或者一个动画,是由很多上下层级的图层构成。
比如当前的简单的图,看到的是一张图,但在设计工具中是三个图层构成,有着不同的上下层级顺序。


![image](https://github.com/jfson/ImgResource/blob/master/31.png?raw=true=200x300)


### SVGA成本
* SVGA目不支持种类:
    * 不支持复杂的矢量形状图层
    * AE自带的渐变、生成、描边、擦除…
    * 对设计工具原生动画不友好,对图片动画友好(适合映客礼物场景)
* 导出工具[开源](https://github.com/yyued/SVGA-FLConverter)
##### 开发成本
* 1.优点
    * 资源包小
    * 测试工具齐全
    * 三端可用
    * 回调完整
    * Protobuf 序列化结构数据格式,序列化的数据体更小,传递效率比xml,json 更高。
* 2.缺点
    * 每个礼物播放时都去重新解压,需要改一套缓存策略
    * svga 用zlib打包(字节流数据压缩程序库),不方便解压和追踪包内容。


* 4.插入动画头像功能
    * 支持,需定义一套专属的头像配置的协议。




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


* 源码类图

![image](https://github.com/jfson/ImgResource/blob/master/35.png?raw=true)
* 版本2.1.2(应该是这个版本...)
* 小解

SVGAImageView imageView = new SVGAImageView(this);
parser = new SVGAParser(this);
parser.parse(new URL("http://legox.yy.com/svga/svga-me/angel.svga"), new SVGAParser.ParseCompletion() { // -----> 下文 1
    @Override
    public void onComplete(@NotNull SVGAVideoEntity videoItem) {
        SVGADrawable drawable = new SVGADrawable(videoItem);
        imageView.setImageDrawable(drawable); // -----> 下文 2
        imageView.startAnimation();// -----> 下文 3
    }
    @Override
    public void onError() {


    }
});


* 1.解析 SVGAParser
    * a. AE导出动画文件,在解析出的SVGAVideoEntity为动画数据源,在使用时调用 SVGAParser(this).parse(url) 最后返回SVGAVideoEntity。
    * b.parse中是一整套的网络下载,根据下载url作为缓存KEY值,缓存动画文件,如果已经下载过的文件,直接去读取文件流并解析。可以看到关键源码如下。PS:这里引申出一个问题,数据源SVGAVideoEntity并没有做缓存,所以每次播放之时,即便是动画文件已经download下来,还是要重新去解析,这是可以跟需要改进的地方。
    
open fun parse(url: URL, callback: ParseCompletion) {
        if (cacheDir(cacheKey(url)).exists()) {
            parseWithCacheKey(cacheKey(url))?.let {
                Handler(context.mainLooper).post {
                    callback.onComplete(it)
                }
                return
            }
        }
        fileDownloader.resume(url, {
            val videoItem = parse(it, cacheKey(url)) ?: return@resume (Handler(context.mainLooper).post { callback.onError() } as? Unit ?: Unit)
            Handler(context.mainLooper).post {
                callback.onComplete(videoItem)
            }
        }, {
            Handler(context.mainLooper).post {
                callback.onError()
            }
        })
    }
    
open fun parseWithCacheKey(cacheKey: String): SVGAVideoEntity? {
        synchronized(sharedLock, {
            try {
                val cacheDir = File(context.cacheDir.absolutePath + File.separator + SVGA_RESOURCE + "/" + cacheKey + "/")
                File(cacheDir, "movie.binary").takeIf { it.isFile }?.let { binaryFile ->
                    try {
                        FileInputStream(binaryFile).let {
                            val videoItem = SVGAVideoEntity(MovieEntity.ADAPTER.decode(it), cacheDir)
                            it.close()
                            return videoItem
                        }
                    } catch (e: Exception) {
                        cacheDir.delete()
                        binaryFile.delete()
                        throw e
                    }
                }
                File(cacheDir, "movie.spec").takeIf { it.isFile }?.let { jsonFile ->
                    try {
                        FileInputStream(jsonFile).let { fileInputStream ->
                            val byteArrayOutputStream = ByteArrayOutputStream()
                            val buffer = ByteArray(2048)
                            while (true) {
                                val size = fileInputStream.read(buffer, 0, buffer.size)
                                if (size == -1) {
                                    break
                                }
                                byteArrayOutputStream.write(buffer, 0, size)
                            }
                            byteArrayOutputStream.toString().let {
                                JSONObject(it).let {
                                    fileInputStream.close()
                                    return SVGAVideoEntity(it, cacheDir)
                                }
                            }
                        }
                    } catch (e: Exception) {
                        cacheDir.delete()
                        jsonFile.delete()
                        throw e
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        })
        return null
    }



    
* 2. 数据源包装类 SVGADrawable
    * a. 将SVGAVideoEntity数据源 设置到SVGADrawable
```
imageView.setImageDrawable(drawable)
```


* 3.startAnimation()
    * a. 开始播放动画后,拿到已经解析后SVGADrawable的drawable,关键参数动画的时长:animator.duration(根据配置的帧数,时长计算),动画的帧率。数值动画会变更SVGADrawable中的currentFrame,这是重点。
    * b.currentFrame 设置后会触发invalidateSelf()
imageView.startAnimation();


fun startAnimation(range: SVGARange?, reverse: Boolean = false) {
        stopAnimation(false)
        val drawable = drawable as? SVGADrawable ?: return
        drawable.cleared = false
        drawable.scaleType = scaleType
        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)
            ...
            animator.interpolator = LinearInterpolator()
            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()))
            }
            animator.addListener(object : Animator.AnimatorListener {
               ...
            })
            if (reverse) {
                animator.reverse()
            }
            else {
                animator.start()
            }
            this.animator = animator
        }
    }
```


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


* 4.SVGADrawable
    * a.SVGADrawable的invalidateSelf()会触发自身的draw()
    * b. SVGACanvasDrawer(videoItem: SVGAVideoEntity, val dynamicItem: SVGADynamicEntity) 可以看到数据源已经被传到这里
    * 可以理解为不断的通过触发drawFrame() 来刷新,看到这里基本看出来SVGA的原理来了,也是上面总结的:通过帧率去刷每一帧的画面,这个思路跟gif很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程)
 override fun draw(canvas: Canvas?) {
        if (cleared) {
            return
        }
        canvas?.let {
            //drawer --> SVGACanvasDrawer
            drawer.drawFrame(it,currentFrame, scaleType)
        }
    }
    
 override fun drawFrame(canvas :Canvas, frameIndex: Int, scaleType: ImageView.ScaleType) {
        super.drawFrame(canvas,frameIndex, scaleType)
        resetCachePath(canvas)
        val sprites = requestFrameSprites(frameIndex)
        sprites.forEach {
            drawSprite(it,canvas)
        }
    }



* 5.分类:矢量元素动画 or 图片动画
    * 看上方总结的类图
    * 如果是矢量动画会取List<SVGADrawerSprite>中的对应每一帧的数据list,而如果有图片的话,会跟图片imageKey进行一一映射,并返回
    

internal fun requestFrameSprites(frameIndex: Int): List<SVGADrawerSprite> {
        return videoItem.sprites.mapNotNull {
            if (frameIndex < it.frames.size) {
                if (it.frames[frameIndex].alpha <= 0.0) {
                    return@mapNotNull null
                }
                return@mapNotNull SVGADrawerSprite(it.imageKey, it.frames[frameIndex])
            }
            return@mapNotNull null
        }
    }

* 6.draw 
    * 最后.. 图片有了,对应图片显示的参数也有了,剩下的就是canvas.drawBitmap,canvas.drawPath...


* 7.图挺乱的...已经凌晨了,就这样咯~。。。2333睡觉
* 最后,贴上绘制的代码,感兴趣的筒子们请看。
 
private fun drawSprite(sprite: SVGADrawerSprite,canvas :Canvas) {
        drawImage(sprite, canvas)
        drawShape(sprite, canvas)
    }


    private fun drawImage(sprite: SVGADrawerSprite, canvas :Canvas) {
        val imageKey = sprite.imageKey ?: return
        dynamicItem.dynamicHidden[imageKey]?.takeIf { it }?.let { return }
        (dynamicItem.dynamicImage[imageKey] ?: videoItem.images[imageKey])?.let {
            resetShareMatrix(sprite.frameEntity.transform)
            sharedPaint.reset()
            sharedPaint.isAntiAlias = videoItem.antiAlias
            sharedPaint.isFilterBitmap = videoItem.antiAlias
            sharedPaint.alpha = (sprite.frameEntity.alpha * 255).toInt()
            if (sprite.frameEntity.maskPath != null) {
                val maskPath = sprite.frameEntity.maskPath ?: return@let
                canvas.save()
                sharedPath.reset()
                maskPath.buildPath(sharedPath)
                sharedPath.transform(sharedFrameMatrix)
                canvas.clipPath(sharedPath)
                sharedFrameMatrix.preScale((sprite.frameEntity.layout.width / it.width).toFloat(), (sprite.frameEntity.layout.width / it.width).toFloat())
                canvas.drawBitmap(it, sharedFrameMatrix, sharedPaint)
                canvas.restore()
            }
            else {
                sharedFrameMatrix.preScale((sprite.frameEntity.layout.width / it.width).toFloat(), (sprite.frameEntity.layout.width / it.width).toFloat())
                canvas.drawBitmap(it, sharedFrameMatrix, sharedPaint)
            }
            drawText(canvas,it, sprite)
        }
    }


    private fun drawText(canvas :Canvas, drawingBitmap: Bitmap, sprite: SVGADrawerSprite) {
        if (dynamicItem.isTextDirty) {
            this.drawTextCache.clear()
            dynamicItem.isTextDirty = false
        }
        val imageKey = sprite.imageKey ?: return
        var textBitmap: Bitmap? = null
        dynamicItem.dynamicText[imageKey]?.let { drawingText ->
            dynamicItem.dynamicTextPaint[imageKey]?.let { drawingTextPaint ->
                drawTextCache[imageKey]?.let {
                    textBitmap = it
                } ?: kotlin.run {
                    textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888)
                    val textCanvas = Canvas(textBitmap)
                    drawingTextPaint.isAntiAlias = true
                    val bounds = Rect()
                    drawingTextPaint.getTextBounds(drawingText, 0, drawingText.length, bounds)
                    val x = (drawingBitmap.width - bounds.width()) / 2.0
                    val targetRectTop = 0
                    val targetRectBottom = drawingBitmap.height
                    val y = (targetRectBottom + targetRectTop - drawingTextPaint.fontMetrics.bottom - drawingTextPaint.fontMetrics.top) / 2
                    textCanvas.drawText(drawingText, x.toFloat(), y, drawingTextPaint)
                    drawTextCache.put(imageKey, textBitmap as Bitmap)
                }
            }
        }
        dynamicItem.dynamicLayoutText[imageKey]?.let {
            drawTextCache[imageKey]?.let {
                textBitmap = it
            } ?: kotlin.run {
                it.paint.isAntiAlias = true
                var layout = StaticLayout(it.text, 0, it.text.length, it.paint, drawingBitmap.width, it.alignment, it.spacingMultiplier, it.spacingAdd, false)
                textBitmap = Bitmap.createBitmap(drawingBitmap.width, drawingBitmap.height, Bitmap.Config.ARGB_8888)
                val textCanvas = Canvas(textBitmap)
                textCanvas.translate(0f, ((drawingBitmap.height - layout.height) / 2).toFloat())
                layout.draw(textCanvas)
                drawTextCache.put(imageKey, textBitmap as Bitmap)
            }
        }
        textBitmap?.let { textBitmap ->
            sharedPaint.reset()
            sharedPaint.isAntiAlias = videoItem.antiAlias
            if (sprite.frameEntity.maskPath != null) {
                val maskPath = sprite.frameEntity.maskPath ?: return@let
                canvas.save()
                canvas.concat(sharedFrameMatrix)
                canvas.clipRect(0, 0, drawingBitmap.width, drawingBitmap.height)
                val bitmapShader = BitmapShader(textBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
                sharedPaint.shader = bitmapShader
                sharedPath.reset()
                maskPath.buildPath(sharedPath)
                canvas.drawPath(sharedPath, sharedPaint)
                canvas.restore()
            }
            else {
                sharedPaint.isFilterBitmap = videoItem.antiAlias
                canvas.drawBitmap(textBitmap, sharedFrameMatrix, sharedPaint)
            }
        }
    }


    private fun drawShape(sprite: SVGADrawerSprite, canvas :Canvas) {
        resetShareMatrix(sprite.frameEntity.transform)
        sprite.frameEntity.shapes.forEach { shape ->
            shape.buildPath()
            shape.shapePath?.let {
                sharedPaint.reset()
                sharedPaint.isAntiAlias = videoItem.antiAlias
                sharedPaint.alpha = (sprite.frameEntity.alpha * 255).toInt()
                if(!drawPathCache.containsKey(shape)){
                    sharedShapeMatrix.reset()
                    shape.transform?.let {
                        sharedShapeMatrix.postConcat(it)
                    }
                    sharedShapeMatrix.postConcat(sharedFrameMatrix)
                    val path = Path()
                    path.set(shape.shapePath)
                    path.transform(sharedShapeMatrix)
                    drawPathCache.put(shape,path)
                }


                shape.styles?.fill?.let {
                    if (it != 0x00000000) {
                        sharedPaint.color = it
                        if (sprite.frameEntity.maskPath !== null) canvas.save()
                        sprite.frameEntity.maskPath?.let { maskPath ->
                            sharedPath2.reset()
                            maskPath.buildPath(sharedPath2)
                            sharedPath2.transform(this.sharedFrameMatrix)
                            canvas.clipPath(sharedPath2)
                        }
                        canvas.drawPath(drawPathCache.get(shape), sharedPaint)
                        if (sprite.frameEntity.maskPath !== null) canvas.restore()
                    }
                }


                shape.styles?.strokeWidth?.let {
                    if (it > 0) {
                        resetShapeStrokePaint(shape)
                        if (sprite.frameEntity.maskPath !== null) canvas.save()
                        sprite.frameEntity.maskPath?.let { maskPath ->
                            sharedPath2.reset()
                            maskPath.buildPath(sharedPath2)
                            sharedPath2.transform(this.sharedFrameMatrix)
                            canvas.clipPath(sharedPath2)
                        }
                        canvas.drawPath(drawPathCache.get(shape), sharedPaint)
                        if (sprite.frameEntity.maskPath !== null) canvas.restore()
                    }
                }
            }
        }
    }



#### 总结
* 其实就一句话:通过帧率去刷每一帧的画面,这个思路跟gif很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程),这种思路真是清奇呀,赞赞赞。

猜你喜欢

转载自blog.csdn.net/sinat_24196195/article/details/80754586