常常在网上看到一些有意思的 GIF 图,有些 GIF 图倒放以后,甚至变得更有意思,简直是每日的快乐源泉;html
好比下面这个java
正放的时候很搞笑,很悲催;倒放的时候竟然很炫酷,简直比段誉的凌波微步还牛逼,有没有一种盖世神功已练成的感受 😎😎😎😎😎,是否是能够和绿巨人一战 😀😀。python
再来一个git
小男生的快乐与悲伤竟然如此简单,嘤嘤婴 😊😊😊😊github
🤣🤣🤣🤣🤣🤣🤣🤣,真是能让人笑上三天三夜。下面就来看看如何实现 GIF 图的倒放。编程
如下全部实现细节源码已同步至 GitHub 仓库,可参考最后源码缓存
想要倒放 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 的倒序须要作三件事
上面两步,对集合逆序不是什么难事,主要看看如何实现第一步和第三步。
这个听起来很复杂,作起来好像也挺难,Android 没有提供相似的 API 能够作这件事,平时加载图片用的三方库 Glide,Fresco 等貌似也没有提供能够作相似事情的接口。但其实咱们稍微深刻看一下三方库是实现 GIF 播放的代码,就会找到突破口,这里以 Glide 为例,假设你研究过 Glide 的源码(若是没有看过,也没关系,能够略过这段,直接看实现)
在 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 图。
把上述三个步骤简单整理一下
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 作了一次封装。而后就能够愉快的使用了。
GifFactory.getReverseRes(mContext,source)
.subscribe {
Glide.with(mContext).load(it).into(reversed)
}
复制代码
掘金 GIF 图上传限制为 5MB,图有点被压缩了,感谢的能够拉源码尝试一下效果
是的,就是这么简单,提供原始 GIF 资源的路径,便可返回实现倒序的 GIF 图。
不得不说,Glide 的内部实现很是强大,对移动端图片加载的各类场景作了很是复杂的考虑和设计,所以也致使它的源码很是的难于阅读。可是,若是仅仅从某个的出发,好比缓存、网络、图片解码和编码的角度出发,脱离整个流程,去看局部仍是有收获的。
回到上述 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