SVGA 源码

Posted by Jfson on 2018-06-21

SVG 概念

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

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

image

SVGA成本

  • SVGA目不支持种类:
    • 不支持复杂的矢量形状图层
    • AE自带的渐变、生成、描边、擦除…
    • 对设计工具原生动画不友好,对图片动画友好(适合映客礼物场景)
  • 导出工具开源
    开发成本
  • 1.优点
    • 资源包小
    • 测试工具齐全
    • 三端可用
    • 回调完整
    • Protobuf 序列化结构数据格式,序列化的数据体更小,传递效率比xml,json 更高。
  • 2.缺点

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

    • 支持,需定义一套专属的头像配置的协议。

SVGA 动画库源码思路

  • 通过设置帧率,来生成一个配置文件,使得每一帧都有一个配置,每一帧都是关键帧,
  • 通过帧率去刷每一帧的画面,这个思路跟gif很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程)
  • 源码类图
    image
  • 版本2.1.2(应该是这个版本…)
  • 小解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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下来,还是要重新去解析,这是可以跟需要改进的地方。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
}

    1. 数据源包装类 SVGADrawable
      • a. 将SVGAVideoEntity数据源 设置到SVGADrawable
        1
        imageView.setImageDrawable(drawable)
  • 3.startAnimation()

    • a. 开始播放动画后,拿到已经解析后SVGADrawable的drawable,关键参数动画的时长:animator.duration(根据配置的帧数,时长计算),动画的帧率。数值动画会变更SVGADrawable中的currentFrame,这是重点。
    • b.currentFrame 设置后会触发invalidateSelf()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
}
}
1
2
3
4
5
6
7
8
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很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      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中的对应每一帧的数据list,而如果有图片的话,会跟图片imageKey进行一一映射,并返回
1
2
3
4
5
6
7
8
9
10
11
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睡觉

  • 最后,贴上绘制的代码,感兴趣的筒子们请看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
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很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程),这种思路真是清奇呀,赞赞赞。

pv UV: