扩展Glide支持加载SVGA动图

4,204 阅读8分钟

背景

SVGA作为一个常见的动图加载方案,在项目上被广泛使用。但是使用的工程中面临下面的一些问题

  • SVGAImageView只支持在XMl指定资源路劲,如果动态加载 需要自己创建SVGA解析器实例,并管理解析器的生命周期
  • 每一处需要支持SVGA动画的位置都需要进行重新开发
  • SVGAImageView 在ViewPager 页面切换的时候已经加载过的SVGA会消失,需要重新添加业务逻辑进行支持
  • SVGA仅仅做了文件缓存,针对同一个资源,内存中可能存在多个对象
  • SVGAImageView 不支持动态修改图片的缩放类型

诉求

期望加载SVGA动图时能够像加载普通图片一样,不需要进行场景区分。通过ImageView就可以直接显示SVGA图片。

解决

为了达到加载SVGA动图时能够像加载普通图片一样,不需要进行场景区分的目标,结合我们常用的图片加载框架是Glide,因此结合SVGAPlayer-Android 对 Glide进行扩展支持svga 图片

Glide 图片加载流程

Glide图片简易加载流程,详细的Glide加载原理可以看看 我以前的 Glide原理总结

在扩展Glide加载SVGA的过程中我们要解决的问题是:

  1. 在没有缓存时直接加载svga
  1. 将SVGA资源加载并缓存到本地
  1. 从本地缓存文件中解析出SVGA图片

扩展代码实现

Drawable实现

  • SVGAPlayer-Android 绘制原理

通过阅读SVGAPlayer-Android 源码,SVGA动效的实现是通过SVGAImageView 和SVGADrawable配合实现的,SVGAImageView中通过动画计算当前svga的绘制帧,在动画更新时更新SVGADrawable#currentFrame 。而SVGADrawable 实际的绘制者是SVGACanvasDrawer。

  • Glide GifDrawable 实现

    •   GifDrawabe 通过实现Animatable 接口在GifDrawable 在图片加载完成之后会通过ImageViewTarget#maybeUpdateAnimatable主动调用start 开启动效。动效开启之后会通过GifFrameLoader去加载每一帧的数据,当帧图准备好之后 会通过Drawable#invalidateSelf 请求重新绘制当前Deawable
  • SVGAAnimationDrawable 扩展实现

结合SVGAPlayer-Android 和 Glide 的GifDrawable 的处理逻辑 SVGAAnimationDrawable 内部实现了svga帧计算的逻辑,同时实现Animatable 接口 让Glide来管理svga动效的开始和结束。从而将svga 动效与View 分开。

ModelLoader扩展

Glide 通过ModelLoader决定加载的数据类型,和输出可被解码的数据。

为了达到无感感知的加载SVGA 图片效果 增加了

StringSVGAModelLoader

UriSVGAModelLoader

同时为了在已经确定加载的内容是SVGA的情况下定制处理 定制了加载数据 SVGAModel 和扩展的 MultiSVGAModelLoaderV2

这些扩展ModelLoader 加载出来的数据都是SVGALoadKey

SVGALoadKey

SVGALoadKey包含SVGAModel 和InputStream ,他们的作用分别是:

SVGAModel svga定制的加载模型,

InputStream 从svga资源中加载出的是数据流

class SVGALoadKey(val svgaModel: SVGAModel, var inputStream: InputStream?):Serializable {

}

SVGALoadKey 编码解码

  • SVGALoadKey解码

    •   在SVGA不需要Glide进行缓存时,通过SVGALoadKeySVGADecoder 直接将SVGALoadKey 转换成对应的Drawable。 因为SVGAParser 会进行文件缓存并且在加载的过程中会经过多次线程切换,因此参考SVGAParser 编写了SVGASimpleParser 只负责进行svga数据解析。
  • SVGALoadKey编码

    •   在svga数据解析出来之后,需要对数据进行编码存储为文件,方便下次直接进行使用。SVGALoadKeyEncoder 编码缓存了3个内容
    •   SVGAModel 对象 及其占用的字节数
    •   SVGA 音频缓存路径 及其占用字节数
    •   SVGA 动图数据
  • 从缓存文件中加载出svga动图

    •   Svga 的数据经过SVGALoadKeyEncoder编码成文件,Glide提供了加载文件输出Stream的ModelLoader,我们需要做的是将输出的Stream 流解码出缓存的数据
    •   SVGAModel 对象 及其占用的字节数
    •   SVGA 音频缓存路径 及其占用字节数
    •   SVGA 动图数据

使用Glide加载SVGA图片

下面的所有SVGA动效的承载控件都是 ImageView

加载asset目录下的svga

      private val imageString = "file:///android_asset/theme_award_beans.svga"
      Glide.with(this).load(imageString).into(glideSVGAImg)

加载网络的svga

    private val imageString = "https://jojopublicfat.jojoread.com/cc/cc-admin/course/420954735744875520/1657003430002ba7b4355ed6e6948ecedecfc33440e82.svga"
    Glide.with(this).load(imageString).into(glideSVGAImg)

替换SVGA中的资源

  对于有需要替换资源的svga 图片不能做到使用的时候无感知。但是在扩展的时候也做了对应的支持,在使用的时候可以选择使用SVGAPlayer-Android 的方式。也可以使用Glide的方式

      private fun glideLoad() {
        val dynamicEntity = SVGADynamicEntity()
        val textPaint = TextPaint()
        textPaint.color = Color.WHITE //字体颜色

        textPaint.textSize = 24f //字体大小

        textPaint.setShadowLayer(3f, 2f, 2f, -0x1000000) //字体阴影,不需要可以不用设置

        dynamicEntity.setDynamicText(
            "30",
            textPaint,
            "text_day"
        )
        Glide.with(this).load(imageString).addListener(object : RequestListener<Drawable> {
            override fun onLoadFailed(
                e: GlideException?,
                model: Any?,
                target: Target<Drawable>?,
                isFirstResource: Boolean,
            ): Boolean {
                return false
            }

            override fun onResourceReady(
                resource: Drawable?,
                model: Any?,
                target: Target<Drawable>?,
                dataSource: DataSource?,
                isFirstResource: Boolean,
            ): Boolean {
                if (resource is SVGAAnimationDrawable) {
                    resource.resetDynamicEntity(dynamicEntity)
                }
                return false
            }
        }).into(glideSVGAImg)
    }
  

设置SVGA动画的重复次数、重复模式

为了方便进行数据设置扩展时自定义了SVGAModel

/ ** * 定义SVGA 加载模型 比如动画重复次数 * 动画监听对象 * 文本替换对象 * @ param p ath 加载的实际数据类型 比如 String,Uri * @ param t ypeClass 指定加载资源的类型  需要与type 的class保持一致 * @ param r epeatCount 重复次数 * @ param r epeatMode 重复模式 * @ param m arkCacheKey 标记Glide缓存的key  如果想要某个资源不被重用、共享 那么给markCacheKey传递不同的值 * */ open class SVGAModel(
    val path: Any = "",
    val typeClass: SVGALoadType = SVGALoadType.String,
    val repeatCount: Int = ValueAnimator.INFINITE,
    val repeatMode: Int = ValueAnimator.RESTART,
    val markCacheKey:String = ""
)
private val imageString = "file:///android_asset/theme_award_beans.svga"
Glide.with(this).load(SVGAModel(imageString,repeatCount = 2)).into(glideSVGAImg)

注意:在实现SVGA动画的重复模式是使用了ValueAnimator 当设置重复模式ValueAnimator.REVERSE时动效会倒着放。与 SVGAPlayer-Android 并不相同。

添加SVGA动效监听

private val imageString = "file:///android_asset/theme_award_beans.svga"
Glide.with(this).load(SVGAModel(imageString,repeatCount = 0)).addListener(object :RequestListener<Drawable>{
    override fun onLoadFailed(
        e: GlideException?,
        model: Any?,
        target: Target<Drawable>?,
        isFirstResource: Boolean,
    ): Boolean {
        return false
    }

    override fun onResourceReady(
        resource: Drawable?,
        model: Any?,
        target: Target<Drawable>?,
        dataSource: DataSource?,
        isFirstResource: Boolean,
    ): Boolean {
        if(resource is SVGAAnimationDrawable){
            resource.animatorListener = object :Animator.AnimatorListener {
                override fun onAnimationStart(animation: Animator?) {
                    Log.d("AddListenerActivity","onAnimationStart")
                }

                override fun onAnimationEnd(animation: Animator?) {
                    Log.d("AddListenerActivity","onAnimationEnd")
                }

                override fun onAnimationCancel(animation: Animator?) {
                    Log.d("AddListenerActivity","onAnimationCancel")
                }

                override fun onAnimationRepeat(animation: Animator?) {
                    Log.d("AddListenerActivity","onAnimationRepeat")
                }

            }
        }
        return false
    }
}).into(glideSVGAImg)

播放带音频的svga资源

扩展时实现了次功能,只要svga中包含音频就可以实现播放功能

几个疑问点

缩放模式问题

ImageView 在处理图片缩放模式的时候会根据对应图片的实际宽高来计算mDrawMatrix 当图片的实际宽高有一个 <= 0 时mDrawMatrix会被置空。而SVGADrawable 没有重写返回实际大小的发放。因此返回的是默认值-1 导致ImageView 的缩放模式未生效。SVGADrawable的缩放模式是通过预先设置ImageView#ScaleType 。SVGACanvasDrawer在每一次绘制的时候动态的计算来实现缩放,这样的问题是在ImageView#ScaleType改变的时候,SVGADrawable并不能及时感知。导致切换缩放模式失效。个人认为通过ImageView 自身的缩放来处理更为好一点。因此在SVGACanvasDrawer中增加了一个配置参数scaleBySelf 如果使用SVGADrawable 那么就按照原来的缩放模式。如果使用的SVGAAnimationDrawable就使用ImageView自己的缩放

SVGAAnimationDrawable 通过下面的方式返回了svga图片的实际大小

override fun getIntrinsicWidth(): Int {
    return videoItem.videoSize.width.toInt()
}

override fun getIntrinsicHeight(): Int {
    return videoItem.videoSize.height.toInt()
}

图片采样率修复

我们知道一般而言图片加载,特别是大图 不会将整个图片加载进入内存,加载流程一般是根据当前使用的大小与图片的实际大小进行采样率计算。SVGAPlayer-Android中SVGAParser提供了setFrameSize来设置期望图片的大小,但是项目的使用率并不高。因为实际的使用大小需要等待ImageView 的布局出来之后才知道。如果做这一步。会加大许多的开发逻辑。所以项目上所有的图片都是全量加载的。

并且setFrameSize应该是有问题的。我们知道svga 是由许多的图片组成的图片。如果我们对每一张图都设置同样的期望大小。那么针对不同大小的图片就有不同的采样率。比如 svga 实际大小是900X900 内部包含两张图片 A:600x600 B :300X300 加载期望大小是300X300 这样 A的采样率与B 的采样率各不相同。但是最终会合成在同一张svga图片中。我的理解是应该根据svga 实际大小和期望大小进行采样计算。然后对svga中的所有图片以相同的采样率加载到内存中。

为什么不是直接去扩展Glide 而是在SVGAPlayer-Android 中写Glide的扩展?

SVGAPlayer-Android本身没有只提SVGA供数据处理能力的内核。

本身在使用SVGAPlayer-Android这个开源库,想要数据解析部分始终与此保持一致。

当前版本问题

不能加载压缩资源

待处理:

默认svga 是重复播放 但是如果此时svga 包含音频会重复播放

代码地址:

github.com/xiaolutang/…

运行ext-glide-test 能够看到所有的demo