前言
经常在网上看到一些有意思的 GIF 图,有些 GIF 图倒放之后,甚至变得更有意思,简直是每日的快乐源泉;
比如下面这个
正放的时候很搞笑,很悲催;倒放的时候居然很炫酷,简直比段誉的凌波微步还牛逼,有没有一种盖世神功已练成的感觉 😎😎😎😎😎,是不是可以和绿巨人一战 😀😀。
再来一个
小男生的快乐与悲伤居然如此简单,嘤嘤婴 😊😊😊😊
🤣🤣🤣🤣🤣🤣🤣🤣,真是能让人笑上三天三夜。下面就来看看如何实现 GIF 图的倒放。
以下所有实现细节源码已同步至 GitHub 仓库,可参考最后源码
GIF 是怎么播放的,如何把GIF倒序?
想要倒放 GIF 图,首先了解一下 GIF 的原理;这里建议看看这篇来自腾讯手Q团队的文章浓缩的才是精华:浅析GIF格式图片的存储和压缩。总的来说,GIF 和图通图片最大的不同点就是它是由许多帧组成的。既然如此我们很容易想到,从 GIF 里把所有帧拿出来,然后倒序组合这些帧,然后在合成一张 GIF 不就可以了吗?
是的,道理就是就么简单。如果你现在去 Google GIF 倒序的实现,会看到很多 Python 的实现版本,类似如下:
1 | import os |
不得不说,Python 的各种三方库的确很强大,几行代码就实现了 GIF 倒序的功能。但是作为一个稍微有点追求的人,难道就到此为止了吗?下次如果有个好玩的 GIF 图片,如果想看倒序图,难道还要打开电脑用用上述脚本转一次吗?
尤其是作为一个 Android 开发者,这种事情用手机不也能做吗?为了每日的快乐源泉,就算天崩地裂,海枯石烂也要做出来(其实好像也不是很难o(╯□╰)o)
好了,不吹牛逼了,下面来看看怎么实现。
GIF 倒放的实现
上面已经说过了,要实现 GIF 的倒序需要做三件事
- 从 GIF 图里把每一帧摘出来,组成序列
- 把序列逆序
- 用逆序后的每一帧再重新生成一张新的 GIF 图
上面两步,对集合逆序不是什么难事,主要看看如何实现第一步和第三步。
从 GIF 图里把每一帧抠出来
这个听起来很复杂,做起来好像也挺难,Android 没有提供类似的 API 可以做这件事,平时加载图片用的三方库 Glide,Fresco 等貌似也没有提供可以做类似事情的接口。但其实我们稍微深入看一下三方库是实现 GIF 播放的代码,就会找到突破口,这里以 Glide 为例,假设你研究过 Glide 的源码(如果没有看过,也不要紧,可以略过这段,直接看实现)
GifFrameLoader.loadNextFrame
在 GifFrameLoader 的 loadNextFrame 实现中(我们可以猜测到这就是 Glide 加载每一帧图片的实现)
1 | private void loadNextFrame() { |
可以看到具体的实现是由 gifDecoder 这个对象实现的。这里最关键的一句就是
1 | gifDecoder.advance(); |
我们可以看看这个方法的定义
1 | /** |
就是跳转到下一帧的意思。
好了,至此我们知道如果可以获取到 GifDeCoder 和 GifFrameLoader 的实例,那么就可以手动控制和获取 GIF 图里每一帧了。但是,我们回过去看 Glide 提供的 API 发现,我们没有办法直接获取 GifFrameLoader 和 GifDeCoder,因为在源码里这些变量都是 private 的。🤦🤦🤦 ,难道这就走到了死胡同吗?不然,前人曾说过,编程领域的任何问题都可以通过添加一个中间层实现。我们这里的中间层就是 反射。使用反射可以获取就可以访问 GifFrameLoader 和 GifDeCoder 了;那么后续的实现就变得简单了。
获取每一帧图片并保存在集合中
1 | Glide.with(mContext).asGif().listener(object :RequestListener<GifDrawable>{ |
这里的实现很简单,监听 GIF 的加载过程,加载成功后得到一个 GifDrawable 的实例 resource ,通过这个实例用反射的方式(具体实现可参考源码,非常简单)获取到了 GifDecode 的实例,有了这个实例就可以获取每一帧了,这里还需要记录一下每一帧播放时间间隔,返回的每一个帧就是一个 Bitmap ,我们把这些 Bitmap 保存在应用的安装目录下,然后用一个列表记录下所有帧的信息,包含当前帧的延迟时间和当前帧对应的 Bitmap 的存储路径。
每一帧的集合序列有了,序列反转一行代码的事情,剩下的就是用这个序列生成新的 GIF 图片了。
用帧序列再次生成图片
用已有的图片组成和一个新的图片,这个并不是什么难事,网上已经有很多实现了。甚至包括 GIF 的再次生成,也可以借助 GifMaker 这样的三方库完成。
1 | private fun genGifByFrames(context: Context, frames: List<ResFrame>): String { |
借助 AnimatedGifEncoder 非常简单把之前保存的序列再次拼接成了一张新的 GIF 图。
GIF 倒放
把上述三个步骤简单整理一下
1 | private fun reverseRes(context: Context, resource: GifDrawable?): String { |
需要注意的是,这三步操作都是涉及到 UI 的耗时操作,因此这里简单用 RxJava 做了一次封装。然后就可以愉快的使用了。
demo
1 | GifFactory.getReverseRes(mContext,source) |
是的,就是这么简单,提供原始 GIF 资源的路径,即可返回实现倒序的 GIF 图。
总结
不得不说,Glide 的内部实现非常强大,对移动端图片加载的各种场景做了非常复杂的考虑和设计,因此也导致它的源码非常的难于阅读。但是,如果仅仅从某个的出发,比如缓存、网络、图片解码和编码的角度出发,脱离整个流程,去看局部还是有收获的。
回到上述 GIF 倒序的步骤,总的来说有以下几个关键步骤
- Glide 根据 URL 加载 GIF 图片,同时监听加载过程
- 通过 GifDrawable 反射获取到 GifDecoder
- 通过 GifDecoder 获取所有帧(包含保存这些帧 Bitmap)
- 反转帧序列 frames
- 通过 frame 再次生成 GIF 图片
上述步骤中 1 和 4 的执行速度是基本上是线性的,也是无法再过多干预的。而步骤 2,3,5 也是 GIF 反转实现的核心,因此对方法耗时简单做了下记录。
1 |
|
可以看到,虽然我们获取 GifDecoder 的过程使用了反射,但其实这比不是性能瓶颈;获取所有帧信息的方法 getResourceFrames 耗时,也是和 GIF 图的大小有关,基本上是一个可接受的值。但是通过帧序列再次生成 GIF 图的方法执行时间就有点恐怖了,即便我的测试机是 kirin(麒麟)960 ,运行内存有 6G 😳😳。
但是同样的图片在 PC 上用 Python 脚本基本上是毫秒级完成。所以纯粹用 java 实现(AnimatedGifEncoder 是 java 写的,不算 kotlin 👀)图片二次编码还是有些性能差距的。
虽然,此次的实现转换较慢,但也算是一次不错的尝试吧。
源码
本文所有实现细节源码已同步至 GitHub 仓库 AndroidAnimationExercise, 可以参考 ReverseGifActivity