简单实现 GIF 图的倒序播放

前言

常常在网上看到一些有意思的 GIF 图,有些 GIF 图倒放以后,甚至变得更有意思,简直是每日的快乐源泉;html

好比下面这个java

正放的时候很搞笑,很悲催;倒放的时候竟然很炫酷,简直比段誉的凌波微步还牛逼,有没有一种盖世神功已练成的感受 😎😎😎😎😎,是否是能够和绿巨人一战 😀😀。python

再来一个git

小男生的快乐与悲伤竟然如此简单,嘤嘤婴 😊😊😊😊github

🤣🤣🤣🤣🤣🤣🤣🤣,真是能让人笑上三天三夜。下面就来看看如何实现 GIF 图的倒放。编程

如下全部实现细节源码已同步至 GitHub 仓库,可参考最后源码缓存

GIF 是怎么播放的,如何把GIF倒序?

想要倒放 GIF 图,首先了解一下 GIF 的原理;这里建议看看这篇来自腾讯手Q团队的文章浓缩的才是精华:浅析GIF格式图片的存储和压缩。总的来讲,GIF 和图通图片最大的不一样点就是它是由许多帧组成的。既然如此咱们很容易想到,从 GIF 里把全部帧拿出来,而后倒序组合这些帧,而后在合成一张 GIF 不就能够了吗?bash

是的,道理就是就么简单。若是你如今去 Google GIF 倒序的实现,会看到不少 Python 的实现版本,相似以下:网络

import os
import sys
from PIL import Image, ImageSequence

path = sys.path[0]                          # 设置路径 -- 系统当前路径
dirs = os.listdir(path)                     # 获取该路径下的文件
for i in dirs:                              # 循环读取全部文件
    if os.path.splitext(i)[1] == ".gif":    # 筛选gif文件
        print(i)                            # 输出全部的gif文件名
        #将gif倒放保存
        with Image.open(i) as im:
            if im.is_animated:
                frames = [f.copy() for f in ImageSequence.Iterator(im)]
                frames.reverse()  # 内置列表倒序
                frames[0].save('./save/reverse_'+i+'.gif',save_all=True, append_images=frames[1:])# 保存
复制代码

不得不说,Python 的各类三方库的确很强大,几行代码就实现了 GIF 倒序的功能。可是做为一个稍微有点追求的人,难道就到此为止了吗?下次若是有个好玩的 GIF 图片,若是想看倒序图,难道还要打开电脑用用上述脚本转一次吗?app

尤为是做为一个 Android 开发者,这种事情用手机不也能作吗?为了每日的快乐源泉,就算天崩地裂,海枯石烂也要作出来(其实好像也不是很难o(╯□╰)o)

好了,不吹牛逼了,下面来看看怎么实现。

GIF 倒放的实现

上面已经说过了,要实现 GIF 的倒序须要作三件事

  • 从 GIF 图里把每一帧摘出来,组成序列
  • 把序列逆序
  • 用逆序后的每一帧再从新生成一张新的 GIF 图

上面两步,对集合逆序不是什么难事,主要看看如何实现第一步和第三步。

从 GIF 图里把每一帧抠出来

这个听起来很复杂,作起来好像也挺难,Android 没有提供相似的 API 能够作这件事,平时加载图片用的三方库 Glide,Fresco 等貌似也没有提供能够作相似事情的接口。但其实咱们稍微深刻看一下三方库是实现 GIF 播放的代码,就会找到突破口,这里以 Glide 为例,假设你研究过 Glide 的源码(若是没有看过,也没关系,能够略过这段,直接看实现

GifFrameLoader.loadNextFrame

在 GifFrameLoader 的 loadNextFrame 实现中(咱们能够猜想到这就是 Glide 加载每一帧图片的实现)

private void loadNextFrame() {
    if (!isRunning || isLoadPending) {
      return;
    }
    if (startFromFirstFrame) {
      Preconditions.checkArgument(
          pendingTarget == null, "Pending target must be null when starting from the first frame");
      gifDecoder.resetFrameIndex();
      startFromFirstFrame = false;
    }
    if (pendingTarget != null) {
      DelayTarget temp = pendingTarget;
      pendingTarget = null;
      onFrameReady(temp);
      return;
    }
    isLoadPending = true;
    // Get the delay before incrementing the pointer because the delay indicates the amount of time
    // we want to spend on the current frame.
    int delay = gifDecoder.getNextDelay();
    long targetTime = SystemClock.uptimeMillis() + delay;

    gifDecoder.advance();
    next = new DelayTarget(handler, gifDecoder.getCurrentFrameIndex(), targetTime);
    requestBuilder.apply(signatureOf(getFrameSignature())).load(gifDecoder).into(next);
  }
复制代码

能够看到具体的实现是由 gifDecoder 这个对象实现的。这里最关键的一句就是

gifDecoder.advance();
复制代码

咱们能够看看这个方法的定义

/** * Move the animation frame counter forward. */
  void advance();
复制代码

就是跳转到下一帧的意思。

好了,至此咱们知道若是能够获取到 GifDeCoder 和 GifFrameLoader 的实例,那么就能够手动控制和获取 GIF 图里每一帧了。可是,咱们回过去看 Glide 提供的 API 发现,咱们没有办法直接获取 GifFrameLoader 和 GifDeCoder,由于在源码里这些变量都是 private 的。🤦‍🤦‍🤦‍ ,难道这就走到了死胡同吗?否则,前人曾说过,编程领域的任何问题均可以经过添加一个中间层实现。咱们这里的中间层就是 反射。使用反射能够获取就能够访问 GifFrameLoader 和 GifDeCoder 了;那么后续的实现就变得简单了。

获取每一帧图片并保存在集合中

Glide.with(mContext).asGif().listener(object :RequestListener<GifDrawable>{
            
            ...fail stuff...

            override fun onResourceReady(resource: GifDrawable, model: Any?, target: Target<GifDrawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                val frames = ArrayList<ResFrame>()
                val decoder = getGifDecoder(resource)
                if (decoder != null) {

                    for (i in 0..resource.frameCount) {
                        val bitmap = decoder.nextFrame
                        val path = IOTool.saveBitmap2Box(context, bitmap, "pic_$i")
                        val frame = ResFrame(decoder.getDelay(i), path)
                        frames.add(frame)
                        decoder.advance()
                    }
                }
                return false
            }

        }).load(originalUrl).into(original)
复制代码

这里的实现很简单,监听 GIF 的加载过程,加载成功后获得一个 GifDrawable 的实例 resource ,经过这个实例用反射的方式(具体实现可参考源码,很是简单)获取到了 GifDecode 的实例,有了这个实例就能够获取每一帧了,这里还须要记录一下每一帧播放时间间隔,返回的每个帧就是一个 Bitmap ,咱们把这些 Bitmap 保存在应用的安装目录下,而后用一个列表记录下全部帧的信息,包含当前帧的延迟时间和当前帧对应的 Bitmap 的存储路径。

每一帧的集合序列有了,序列反转一行代码的事情,剩下的就是用这个序列生成新的 GIF 图片了。

用帧序列再次生成图片

用已有的图片组成和一个新的图片,这个并非什么难事,网上已经有不少实现了。甚至包括 GIF 的再次生成,也能够借助 GifMaker 这样的三方库完成。

private fun genGifByFrames(context: Context, frames: List<ResFrame>): String {
        
        val os = ByteArrayOutputStream()
        val encoder = AnimatedGifEncoder()
        encoder.start(os)
        encoder.setRepeat(0)
        for (value in frames) {
            val bitmap = BitmapFactory.decodeFile(value.path)
            encoder.setDelay(value.delay)
            encoder.addFrame(bitmap)
        }
        encoder.finish()

        val path = IOTool.saveStreamToSDCard("test", os)
        IOTool.notifySystemGallay(context, path)
    
        return path
    }
复制代码

借助 AnimatedGifEncoder 很是简单把以前保存的序列再次拼接成了一张新的 GIF 图。

GIF 倒放

把上述三个步骤简单整理一下

private fun reverseRes(context: Context, resource: GifDrawable?): String {
        if (resource == null) {
            return ""
        }
        // 获取全部帧信息集合
        val frames = getResourceFrames(resource, context)

        // 逆序集合
        reverse(frames)

        // 生成新的 GIF 图片
        return genGifByFrames(context, frames)
    }
复制代码

须要注意的是,这三步操做都是涉及到 UI 的耗时操做,所以这里简单用 RxJava 作了一次封装。而后就能够愉快的使用了。

demo

GifFactory.getReverseRes(mContext,source)
                .subscribe { 
                    Glide.with(mContext).load(it).into(reversed)
                }
复制代码

掘金 GIF 图上传限制为 5MB,图有点被压缩了,感谢的能够拉源码尝试一下效果

是的,就是这么简单,提供原始 GIF 资源的路径,便可返回实现倒序的 GIF 图。

总结

不得不说,Glide 的内部实现很是强大,对移动端图片加载的各类场景作了很是复杂的考虑和设计,所以也致使它的源码很是的难于阅读。可是,若是仅仅从某个的出发,好比缓存、网络、图片解码和编码的角度出发,脱离整个流程,去看局部仍是有收获的。

回到上述 GIF 倒序的步骤,总的来讲有如下几个关键步骤

  1. Glide 根据 URL 加载 GIF 图片,同时监听加载过程
  2. 经过 GifDrawable 反射获取到 GifDecoder
  3. 经过 GifDecoder 获取全部帧(包含保存这些帧 Bitmap)
  4. 反转帧序列 frames
  5. 经过 frame 再次生成 GIF 图片

上述步骤中 1 和 4 的执行速度是基本上是线性的,也是没法再过多干预的。而步骤 2,3,5 也是 GIF 反转实现的核心,所以对方法耗时简单作了下记录。

GIF 图 size = 8.9M

E/GifFactory: 方法: getGifDecoder        耗时 0.001000 second
E/GifFactory: 方法: getResourceFrames    耗时 1.489000 second
E/GifFactory: 方法: genGifByFrames       耗时 9.397000 second

GIF 图 size = 11.9M

E/GifFactory: 方法: getGifDecoder        耗时 0.000000 second
E/GifFactory: 方法: getResourceFrames    耗时 1.074000 second
E/GifFactory: 方法: genGifByFrames       耗时 9.559000 second

GIF 图 size = 3.3M

E/GifFactory: 方法: getGifDecoder        耗时 0.001000 second
E/GifFactory: 方法: getResourceFrames    耗时 0.437000 second
E/GifFactory: 方法: genGifByFrames       耗时 2.907000 second

GIF 图 size = 8.1M

E/GifFactory: 方法: getGifDecoder        耗时 0.000000 second
E/GifFactory: 方法: getResourceFrames    耗时 0.854000 second
E/GifFactory: 方法: genGifByFrames       耗时 6.416000 second
复制代码

能够看到,虽然咱们获取 GifDecoder 的过程使用了反射,但其实这比不是性能瓶颈;获取全部帧信息的方法 getResourceFrames 耗时,也是和 GIF 图的大小有关,基本上是一个可接受的值。可是经过帧序列再次生成 GIF 图的方法执行时间就有点恐怖了,即使个人测试机是 kirin(麒麟)960 ,运行内存有 6G 😳😳。

可是一样的图片在 PC 上用 Python 脚本基本上是毫秒级完成。因此纯粹用 java 实现(AnimatedGifEncoder 是 java 写的,不算 kotlin 👀)图片二次编码仍是有些性能差距的。

虽然,这次的实现转换较慢,但也算是一次不错的尝试吧。若是对最后一个步骤,有什么更优雅的方式,能够缩短 GIF 合成时间,能够提 PR 到 GitHub ,全部的观点讨论都是很是欢迎的。

源码

本文全部实现细节源码已同步至 GitHub 仓库 AndroidAnimationExercise, 本节入口能够参考 ReverseGifActivity

参考文档

浓缩的才是精华:浅析GIF格式图片的存储和压缩

相关文章
相关标签/搜索