人都是视觉动物,一个App的动效做的炫酷,优雅,是很吸引用户使用的。 Android实现复杂动效的方式有如下几种: 自定义View:需要自己实现动画的细节,开发效率和效果可能不够好。 APNG:一整个png的序列图。png是支持alpha通道的,因此显示效果和以png为主的帧动画一致。但android原生ImageView并不支持这种图片格式的显示,需要第三方库去支持。 Lottie: SVGA: svga和svg关系? svg: Scalable Vector Graphics 可缩放矢量图形,是一个基于XML语言,描述二维矢量图形的图形格式。 svga是描述动效的文件,由Adobe After Effects导出动效,内部包含矢量图像,音效等,在1.x格式时使用ZIP进行打包,在2.x格式中,直接使用ProtoBuf+Zlib方式打包。因此svga文件不可直接打开预览。当然我们做为移动端开发人员不用关心svga内部文件结构实现。 protocol buffers: 下面开始着重介绍一下svga框架。 上图是svga.io官网上的介绍图。从图中我们可知,svga格式动效支持Android、iOS、web。设计师在After Effects完成动效设计,使用SVGA Converter工具将动效资源转成svga文件。客户端使用SVGA-Parser将动效生成相应的player进行播放。 下面我们针对Android平台源码进行分析。 SVGA动画播放示例 SVGAParser 最终都会将已有的svga以 梳理完svga文件解析,下面介绍一下动画是如何播放的。 如下图所示, 流程基本梳理完成了,简单来说就是svga内部记录了所有要绘制的元素和每一帧信息,直接通过遍历当前帧进行绘制即可,效率比较高。 下面做一个svga和Lottie的对比 以Lottie官方给出的点赞动效为例,我下载了最新版 并且放到一个简单页面中播放对比。两个文件都放置在asset文件夹中,播放页面中没有其他UI元素。 svga Lottie 分析可知: 参考:
Lottie是Airbnb推出的开源跨平台动画库,通过AE做出动效导出json文件,然后移动端使用播放器进行原生渲染播放。

SVGA是YY直播公司开源的跨平台动效播放方案,和Lottie有着很相似的功能。
svg是由w3c制定,是一个开放标准。常见的图片格式如png,jpg,gif等都是位图,位图是由点构成的,矢量图则是由一些形状元素构成。该图中显示放大位图可以看到点,而放大矢量图看到的仍然是形状。SVG属于矢量图,因此能够无级缩放,而不会导致马赛克。
Protocol buffers 是一种语言中立,平台无关,可扩展的序列化数据的格式,可用于通信协议,数据存储等。Protocol buffers 在序列化数据方面,它是灵活的,高效的。相比于 XML 来说,Protocol buffers 更加小巧,更加快速,更加简单。一旦定义了要处理的数据的数据结构之后,就可以利用 Protocol buffers 的代码生成工具生成相关的代码。甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
Protocol buffers 很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
显示一个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类是将svga文件转化成SVGAImageView识别的VideoEntity的转化类。根据源码可知,它支持三种方式加载svga文件。decodeFromAssets(string, callback)
decodeFromURL(string,callback)
decodeFromInputStream(inputStream,callback)

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等信息。
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动画的播放主要涉及到3个类:SVGAImageView、SVGADrawable、SVGACanvasDrawer。SVGAImageView接收解析后的VideoEntitiy数据然后包装成SVGADrawable,通过SVGADrawable内部的SVGADrawar来绘制动画图形。
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)
}
Adobe After Effects 2019,并且安装了svga和Lottie导出动画的插件BodyMovin和SGVA Converter。分别导出这个动效的json文件和svga文件svga

Lottie

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


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