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很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程),这种思路真是清奇呀,赞赞赞。
上一篇: JMS 发布/订阅模式案例
下一篇: 读Zepto源码之操作DOM
推荐阅读