欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SVGA源码

程序员文章站 2022-05-04 07:49:10
...


### 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)) ?: [email protected] (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) {
                    [email protected] null
                }
                [email protected] SVGADrawerSprite(it.imageKey, it.frames[frameIndex])
            }
            [email protected] 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 ?: [email protected]
                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 ?: [email protected]
                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很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程),这种思路真是清奇呀,赞赞赞。
相关标签: SVGA Android 源码