用chatGpt 扩展Glide支持腾讯libpag PAG 动效图片

4,434 阅读7分钟

背景

最近项目上有接入腾讯libpag 用作动效实现,至此项目上已经包含了三种格式的动效,svga,lottile,pag。而业务需求是客户端要能够支持多种格式的动效处理。 因此业务代码实现经常是下面的逻辑


when(图片类型){
    "svga"->{
        处理svga的逻辑
    }
    "lottile"->{
        处理了lottile的逻辑
    }
    "pag"->{
        处理pag动效的逻辑
    }
    else->{
        处理为普通图片
    }
}

这样的业务逻辑代码分布在项目的各个地方,使用的使用不仅需要重复的处理对应的业务逻辑,也不利于维护。并且腾讯 libpag 的 PagView 作为ui的呈现, 支持的是从本地文件路径或者直接设置一个 PAGComposition 对象到 PagView 进行显示,并不对文件的下载过程进行处理。 对应文件的下载功能自然落在了开发者身上,如果为了接入libpag 而自己去实现一套 下载缓存的功能功能庞大且复杂,且整个过程需要研发、测试的完全介入,成本相对较高。使用项目上已有的下载方案自然是一个不错的选择。 Glide 作为一个成熟稳定的图片加载框架,拥有图片加载和文件下载的能力。并在内部实现了多级缓存的机制,与列表错位的处理。

将 libpag 与 Glide 进行结合不仅效果稳定并且图片加载的方式不会进行改变,使用者也不需要额外的学习成本。

目标与设计实现

本次扩展主要有两个目标

  1. 能够使用Glide 加载显示到 PagView
  2. 做到图片加载的时候不需要进行业务区分,让 Glide 像加载普通图片一样加载pag

方案实现:

目标一:

Glide 数据加载大致有 数据加载-> 解码 -> 转码 三个流程 现在Glide 会将网络数据加载成InputStream 流 我们需要扩展的是当检测到stram 是 PAG 时将流解码成文件,再对文件进行转码,转码结果是 PagView 需要的 PAGComposition 。

目标二

Glide 默认加载出的对象是Drawable,然后直接将Drawable对象设置给ImageView 使用,而 PagView 需要的是一个 PAGComposition 。为此 可以自定义一个Drawable 对象 PAGFileDrawable 内部包含PAGComposition 对下。 但是 PagView 并不是直接继承自ImageView 并不能直接使用Drawable对象。为了达到使用时不区分具体的资源类型的目的,我们封装一个ViewGroup AutoAnimationView,AutoAnimationView 内部包含ImageView和PagView 当加载结果是 PAGFileDrawable 时显示 PagView 隐藏 ImageView 反之显示 ImageView 隐藏 PagView

代码实现

说了这么多,主角终于来了。尝试让chatGpt 帮我实现上面的逻辑

  1. 先进行下铺垫 看看gpt能不能扩展,显然gpt知道如何扩展,并给出一段wep解码的示例

image.png

  1. 紧接着我们让gpt按照 增加一个解码器

image.png

public class PagDecoder implements ResourceDecoder<InputStream, File> {
    private final Context context;

    public PagDecoder(Context context) {
        this.context = context;
    }

    @Override
    public Resource<File> decode(InputStream source, int width, int height, Options options) throws IOException {
        // 将输入流保存为本地文件
        File file = saveInputStreamToFile(source);
        // 返回本地文件的Resource对象
        return new FileResource(file);
    }

    @Override
    public boolean handles(InputStream source, Options options) throws IOException {
        // 判断输入流是否为PAG类型
        return isPag(source);
    }

    private boolean isPag(InputStream source) {
        // 判断输入流是否为PAG类型的逻辑
        // 这里只是一个示例,需要根据实际情况自行实现
        return true;
    }

    private File saveInputStreamToFile(InputStream input) throws IOException {
        // 创建一个临时文件来保存输入流
        File outputDir = context.getCacheDir();
        File outputFile = File.createTempFile("temp", ".tmp", outputDir);

        // 使用流的方式将输入流写入文件
        OutputStream output = new FileOutputStream(outputFile);
        byte[] buffer = new byte[4 * 1024]; // 4KB
        int bytesRead;
        while ((bytesRead = input.read(buffer)) != -1) {
            output.write(buffer, 0, bytesRead);
        }
        output.flush();
        output.close();

        return outputFile;
    }
}

  1. 明显看到这里的isPag 直接返回了true 这个不是我们想要的,于是我们又问gpt 怎么判断,图片的类型

image.png 4. 重写刚才的类

image.png 因为gpt 实现是java 我又让它把代码转换成kotlin

class PagDecoder(private val context: Context) : ResourceDecoder<InputStream, File> {
    override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<File>? {
        // 将输入流保存为本地文件
        val file = saveInputStreamToFile(source)
        // 返回本地文件的Resource对象
        return FileResource(file)
    }

    override fun handles(source: InputStream, options: Options): Boolean {
        // 判断输入流是否为PAG类型
        return isPag(source)
    }

    private fun isPag(source: InputStream): Boolean {
        // 判断输入流是否为PAG类型的逻辑
        // 这里只是一个示例,需要根据实际情况自行实现
        val header = ByteArray(4)
        try {
            source.read(header)
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return header.contentEquals(byteArrayOf(0x50, 0x41, 0x47, 0x00))
    }

    @Throws(IOException::class)
    private fun saveInputStreamToFile(input: InputStream): File {
        // 创建一个临时文件来保存输入流
        val outputDir: File = context.cacheDir
        val outputFile = File.createTempFile("temp", ".tmp", outputDir)

        // 使用流的方式将输入流写入文件
        val output: OutputStream = FileOutputStream(outputFile)
        val buffer = ByteArray(4 * 1024) // 4KB
        var bytesRead: Int
        while (input.read(buffer).also { bytesRead = it } != -1) {
            output.write(buffer, 0, bytesRead)
        }
        output.flush()
        output.close()

        return outputFile
    }
}


5.转码器实现

image.png

这一次gpt并没有实现对应的转码器 而是实现了一个解码器,因此我再次强调强调了一遍是转码器。而gpt 也认识到了他的错误。就不教训它了

image.png

class PagFileTranscoder : ResourceTranscoder<File, PAGFile> {
    override fun transcode(toTranscode: Resource<File>, options: Options): Resource<PAGFile>? {
        // 获取文件路径
        val filePath = toTranscode.get().path
        // 使用 PAGFile.Load 方法将文件转换成 PAGFile 对象
        val pagFile = PAGFile.Load(filePath)
        // 返回 PAGFile 对象的 Resource 对象
        return PagFileResource(pagFile)
    }

    override fun getTranscodeOptions(options: Options): Options {
        return options
    }

    class PagFileResource(val pagFile: PAGFile) : Resource<PAGFile> {
        override fun getResourceClass(): Class<PAGFile> {
            return PAGFile::class.java
        }

        override fun get(): PAGFile {
            return pagFile
        }

        override fun getSize(): Int {
            return pagFile.getData().size
        }

        override fun recycle() {
            // 不需要进行资源回收操作
        }
    }
}

明显可以看到的是转码的时候 是都所有文件执行的。而我们想转码的文件是PAG 动效资源。于是

image.png 同时让gpt 删除一些不必要的代码之后最终的代码是

class PagFileTranscoder(private val context: Context) : ResourceTranscoder<File, PAGFile> {
    override fun transcode(
        toTranscode: Resource<File>,
        options: Options
    ): Resource<PAGFile>? {
        // 获取文件路径
        val filePath = toTranscode.get().path
        // 读取文件头部信息,用于判断文件类型
        val header = ByteArray(8)
        FileInputStream(filePath).use { inputStream ->
            inputStream.read(header)
        }
        // 判断文件是否是 PAG 类型的文件
        if (isPAGFile(header)) {
            // 使用 PAGFile.Load 方法将文件转换成 PAGFile 对象
            val pagFile = PAGFile.Load(filePath)
            // 返回 PAGFile 对象的 Resource 对象
            return SimpleResource(pagFile)
        }
        return null
    }

    private fun isPAGFile(header: ByteArray): Boolean {
        // 判断文件头部信息是否与 PAG 文件的头部信息相同
        return header[0] == 'P'.toByte() &&
                header[1] == 'A'.toByte() &&
                header[2] == 'G'.toByte() &&
                header[3] == 'F'.toByte()
    }
}

然后我自己实现简单实现了一些封装

class PAGViewTarget(
    private val view: PAGView
) : CustomViewTarget<PAGView, PAGFile>(view) {

    override fun onResourceLoading(placeholder: Drawable?) {
        view.stop()
    }

    override fun onResourceCleared(placeholder: Drawable?) {
        view.stop()
    }

    override fun onResourceReady(resource: PAGFile, transition: Transition<in PAGFile>?) {
        view.composition = resource
        view.setRepeatCount(50)
        view.play()
    }

    override fun onLoadFailed(errorDrawable: Drawable?) {

    }
}

fun RequestBuilder<PAGFile>.into(pagView:PAGView){
    into(PAGViewTarget(pagView))
}

fun RequestManager.asPAGFile():RequestBuilder<PAGFile>{
    return `as`(PAGFile::class.java)
}

在Application中加解码器和转码器注册

Glide.get(this).registry.append(InputStream::class.java, File::class.java, PagDecoder(this))
    .register(
        File::class.java,
        PAGFile::class.java,
        PagFileTranscoder()
    )

在页面当中进行使用

Glide.with(this).asPAGFile().load("https://github.com/Tencent/libpag/blob/main/assets/data_video.pag?raw=true").into(pagView)

然后加载失败了,经过排查 原来是gpt 判断文件类型出错了。

image.png

至此 扩展Glide 加载 pag 图片的功能已经完成啦

至于目标二的实现 就不粘贴与gpt的聊天内容了。

最终结果

这里直接献上 最后整理的代码 和 效果图 刚兴趣的小伙伴可以直接下载代码运行尝试

image.png

代码地址: github.com/xiaolutang/…
注意demo上的pag 地址是腾讯官方 github 地址 访问要挂vpn

使用chatgpt的感受

当前chatgpt 对于一些深入的业务场景还没办法拥有足够的理解能力。并不能直接输出可用代码。甚至经常输出错误的代码。但是这并不妨碍gpt 未来对程序员工作的影响。总体而言 对于代码的落地 需要我们不断的引导gpt进行修正。当与gpt 沟通的次数足够多的是他就能够按照你的要求产生你想要的东西(但是这个沟通过程可能极其痛苦,某些时候 你明明已经纠正了但是gpt 还是会按照错误的内容进行输出)。 对于个人而言,在工作中有效的使用gpt 能够提高个人的开发效率,每个人都应该拥有自己独立的人工智能助手,通过训练这个助手让人工智能更加了解你的工作习惯,从而提高个人的开发效率。