常常在網上看到一些有意思的 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